Added Integration of Lacroix
MODIFY
src/integration/asset/AssetFactory.ts
src/integration/asset/AssetFactory.ts
@@ -3,6 +3,7 @@ import { AssetConnectionType, AssetSetting } from '../../types/Setting';
import AssetIntegration from './AssetIntegration';
import GreencomAssetIntegration from './greencom/GreencomAssetIntegration';
import IothinkAssetIntegration from './iothink/IothinkAssetIntegration';
+import LacroixAssetIntegration from './lacroix/LacroixAssetIntegration';
import Logging from '../../utils/Logging';
import SchneiderAssetIntegration from './schneider/SchneiderAssetIntegration';
import { ServerAction } from '../../types/Server';
@@ -38,6 +39,9 @@ export default class AssetFactory {
case AssetConnectionType.WIT:
assetIntegrationImpl = new WitAssetIntegration(tenant, settings.asset, foundConnection);
break;
+ case AssetConnectionType.LACROIX:
+ assetIntegrationImpl = new LacroixAssetIntegration(tenant, settings.asset, foundConnection);
+ break;
}
return assetIntegrationImpl;
}

ADD
src/integration/asset/lacroix/LacroixAssetIntegration.ts
+import Asset, { LacroixPeriods, LacroixResponse } from '../../../types/Asset';
+import { AssetConnectionSetting, AssetSetting } from '../../../types/Setting';
+
+import { AbstractCurrentConsumption } from '../../../types/Consumption';
+import AssetIntegration from '../AssetIntegration';
+import AxiosFactory from '../../../utils/AxiosFactory';
+import { AxiosInstance } from 'axios';
+import BackendError from '../../../exception/BackendError';
+import Constants from '../../../utils/Constants';
+import Cypher from '../../../utils/Cypher';
+import Logging from '../../../utils/Logging';
+import { ServerAction } from '../../../types/Server';
+import Tenant from '../../../types/Tenant';
+import TransactionStorage from '../../../storage/mongodb/TransactionStorage';
+import Utils from '../../../utils/Utils';
+import moment from 'moment';
+
+const MODULE_NAME = 'LacroixAssetIntegration';
+
+export default class LacroixAssetIntegration extends AssetIntegration<AssetSetting> {
+ private axiosInstance: AxiosInstance;
+
+ public constructor(tenant: Tenant, settings: AssetSetting, connection: AssetConnectionSetting) {
+ super(tenant, settings, connection);
+ this.axiosInstance = AxiosFactory.getAxiosInstance(tenant.id);
+ }
+
+ public async checkConnection(): Promise<void> {
+ await this.connect();
+ }
+
+ public async retrieveConsumptions(asset: Asset, manualCall: boolean): Promise<AbstractCurrentConsumption[]> {
+ // Check if refresh interval of connection is exceeded
+ if (!manualCall && !this.checkIfIntervalExceeded(asset)) {
+ return [];
+ }
+ const timeSinceLastConsumption = moment().diff(asset.lastConsumption?.timestamp ? asset.lastConsumption.timestamp : moment().startOf('day').toISOString(), 'minutes');
+ let period = LacroixPeriods.FIVE_MINUTES;
+ if (timeSinceLastConsumption > 5 && timeSinceLastConsumption < 60) {
+ period = LacroixPeriods.ONE_HOUR;
+ } else if (timeSinceLastConsumption > 60) {
+ period = LacroixPeriods.ONE_DAY;
+ }
+ const request = manualCall ?
+ `${this.connection.url}/${asset.meterID}/getPowerData?period=${LacroixPeriods.LAST}&group_by=1m` :
+ // Check if it is first consumption for this asset
+ `${this.connection.url}/${asset.meterID}/getPowerData?period=${period}&group_by=1m`;
+ try {
+ // Get consumption
+ const response = await this.axiosInstance.get(
+ request,
+ {
+ auth: {
+ username: this.connection.lacroixConnection.user,
+ password: await Cypher.decrypt(this.tenant.id, this.connection.lacroixConnection.password)
+ }
+ }
+ );
+ await Logging.logDebug({
+ tenantID: this.tenant.id,
+ source: Constants.CENTRAL_SERVER,
+ action: ServerAction.RETRIEVE_ASSET_CONSUMPTION,
+ message: `${asset.name} > Lacroix web service has been called successfully`,
+ module: MODULE_NAME, method: 'retrieveConsumption',
+ detailedMessages: { response: response.data }
+ });
+ return await this.filterConsumptionRequest(asset, response.data, manualCall);
+ } catch (error) {
+ throw new BackendError({
+ source: Constants.CENTRAL_SERVER,
+ module: MODULE_NAME,
+ method: 'retrieveConsumption',
+ action: ServerAction.RETRIEVE_ASSET_CONSUMPTION,
+ message: 'Error while retrieving the asset consumption',
+ detailedMessages: { request, error: error.stack, asset }
+ });
+ }
+ }
+
+ private async filterConsumptionRequest(asset: Asset, responseData: LacroixResponse, manualCall: boolean): Promise<AbstractCurrentConsumption[]> {
+ const consumptions: AbstractCurrentConsumption[] = [];
+ for (const dataPoint of responseData.data) {
+ const consumption = {} as AbstractCurrentConsumption;
+ consumption.currentInstantWatts = dataPoint.powerApparentConsumedTotal;
+ consumption.currentInstantWattsL1 = dataPoint.powerApparentConsumed1;
+ consumption.currentInstantWattsL2 = dataPoint.powerApparentConsumed2;
+ consumption.currentInstantWattsL3 = dataPoint.powerApparentConsumed3;
+ consumption.currentTotalConsumptionWh = Utils.createDecimal(consumption.currentInstantWatts).mul(60).toNumber();
+ consumption.lastConsumption = {
+ timestamp: new Date(dataPoint.date),
+ value: consumption.currentTotalConsumptionWh
+ };
+ consumptions.push(consumption);
+ }
+ const transactionResponse = await TransactionStorage.getTransactions(this.tenant.id, { siteAreaIDs:[asset.siteAreaID],
+ stop: { $exists: false } }, Constants.DB_PARAMS_MAX_LIMIT);
+ for (const transaction of transactionResponse.result) {
+ consumptions[consumptions.length - 1].currentInstantWatts -= transaction.currentInstantWatts;
+ consumptions[consumptions.length - 1].currentInstantWattsL1 -= transaction.currentInstantWattsL1;
+ consumptions[consumptions.length - 1].currentInstantWattsL2 -= transaction.currentInstantWattsL2;
+ consumptions[consumptions.length - 1].currentInstantWattsL3 -= transaction.currentInstantWattsL3;
+ }
+ if (asset.siteArea?.voltage) {
+ for (const consumption of consumptions) {
+ consumption.currentInstantAmps = Utils.createDecimal(consumption.currentInstantWatts).div(asset.siteArea.voltage).toNumber();
+ consumption.currentInstantAmps = Utils.createDecimal(consumption.currentInstantWattsL1).div(asset.siteArea.voltage).toNumber();
+ consumption.currentInstantAmps = Utils.createDecimal(consumption.currentInstantWattsL2).div(asset.siteArea.voltage).toNumber();
+ consumption.currentInstantAmps = Utils.createDecimal(consumption.currentInstantWattsL3).div(asset.siteArea.voltage).toNumber();
+ }
+ }
+ if (manualCall) {
+ return !Utils.isEmptyArray(consumptions) ? [consumptions[consumptions.length - 1]] : [];
+ }
+ return consumptions;
+ }
+
+ private async connect(): Promise<void> {
+ // Check if connection is initialized
+ this.checkConnectionIsProvided();
+ // Get credential params
+ const credentials = await this.getCredentialParams();
+ // Send credentials to get the token
+ await Utils.executePromiseWithTimeout(5000,
+ this.axiosInstance.post(`${this.connection.url}/login`,
+ credentials,
+ {
+ 'axios-retry': {
+ retries: 0
+ },
+ headers: this.buildFormHeaders(),
+ }),
+ `Time out error (5s) when getting the token with the connection URL '${this.connection.url}/token'`
+ );
+ }
+
+ private checkConnectionIsProvided(): void {
+ if (!this.connection) {
+ throw new BackendError({
+ source: Constants.CENTRAL_SERVER,
+ module: MODULE_NAME,
+ method: 'checkConnectionIsProvided',
+ action: ServerAction.CHECK_CONNECTION,
+ message: 'No connection provided'
+ });
+ }
+ }
+
+ private buildFormHeaders(): any {
+ return {
+ 'Content-Type': 'application/x-www-form-urlencoded'
+ };
+ }
+
+ private async getCredentialParams(): Promise<URLSearchParams> {
+ const params = new URLSearchParams();
+ params.append('email', this.connection.lacroixConnection.user);
+ params.append('plainPassword', await Cypher.decrypt(this.tenant.id, this.connection.lacroixConnection.password));
+ return params;
+ }
+}

MODIFY
src/server/rest/v1/service/security/SettingSecurity.ts
src/server/rest/v1/service/security/SettingSecurity.ts
@@ -244,6 +244,12 @@ export default class SettingSecurity {
password: sanitize(connection.witConnection.password),
};
break;
+ case AssetConnectionType.LACROIX:
+ sanitizedConnection.lacroixConnection = {
+ user: sanitize(connection.lacroixConnection.user),
+ password: sanitize(connection.lacroixConnection.password),
+ };
+ break;
}
settings.content.asset.connections.push(sanitizedConnection);
}

MODIFY
src/types/Asset.ts
src/types/Asset.ts
@@ -72,3 +72,27 @@ export enum IothinkProperty {
IO_ENERGY_DISCHARGE = 'io-energy-discharge',
IO_SOC = 'io-soc',
}
+
+export interface LacroixResponse {
+ userID: string;
+ subscriptionRef: string
+ period: string;
+ groupBy: string;
+ data: LacroixDataPoint[]
+}
+
+export interface LacroixDataPoint {
+ date: string;
+ powerApparentConsumedTotal: number;
+ powerApparentReference: number;
+ powerApparentConsumed1: number;
+ powerApparentConsumed2: number;
+ powerApparentConsumed3: number;
+}
+
+export enum LacroixPeriods {
+ LAST = 'last',
+ FIVE_MINUTES = '5m',
+ ONE_HOUR = '1h',
+ ONE_DAY = 'day',
+}

MODIFY
src/types/Performance.ts
src/types/Performance.ts
@@ -39,6 +39,7 @@ export enum PerformanceRecordGroup {
STRIPE = 'stripe',
RECAPTCHA = 'recaptcha',
IOTHINK = 'iothink',
+ LACROIX = 'lacroix',
EV_DATABASE = 'ev-database',
WIT = 'wit',
SAP_SMART_CHARGING = 'sap-smart-charging',

MODIFY
src/types/Setting.ts
src/types/Setting.ts
@@ -280,6 +280,7 @@ export interface AssetConnectionSetting {
greencomConnection?: AssetGreencomConnectionType;
iothinkConnection?: AssetIothinkConnectionType;
witConnection?: AssetWitConnectionType;
+ lacroixConnection?: AssetLacroixConnectionType
}
export enum AssetConnectionType {
@@ -287,6 +288,7 @@ export enum AssetConnectionType {
GREENCOM = 'greencom',
IOTHINK = 'iothink',
WIT = 'wit',
+ LACROIX = 'lacroix'
}
export interface AssetUserPasswordConnectionType {
@@ -299,13 +301,11 @@ export interface AssetGreencomConnectionType {
clientSecret: string;
}
-// eslint-disable-next-line @typescript-eslint/no-empty-interface
-export interface AssetSchneiderConnectionType extends AssetUserPasswordConnectionType {
-}
+export type AssetSchneiderConnectionType = AssetUserPasswordConnectionType;
-// eslint-disable-next-line @typescript-eslint/no-empty-interface
-export interface AssetIothinkConnectionType extends AssetUserPasswordConnectionType {
-}
+export type AssetIothinkConnectionType = AssetUserPasswordConnectionType;
+
+export type AssetLacroixConnectionType = AssetUserPasswordConnectionType;
export interface AssetWitConnectionType extends AssetUserPasswordConnectionType {
clientId: string;

MODIFY
src/utils/Utils.ts
src/utils/Utils.ts
@@ -1453,6 +1453,10 @@ export default class Utils {
if (url.includes('kheiron')) {
return PerformanceRecordGroup.IOTHINK;
}
+ // Lacroix
+ if (url.includes('esoflink')) {
+ return PerformanceRecordGroup.LACROIX;
+ }
// EV Database
if (url.includes('ev-database')) {
return PerformanceRecordGroup.EV_DATABASE;

Added Integration of Lacroix