X-Git-Url: https://git.piment-noir.org/?a=blobdiff_plain;f=src%2Fcharging-station%2FChargingStationUtils.ts;h=ab781fae3cb3382022abb8f61ed7b55f021e9ef0;hb=a1c394ae6f706a51274c4a2adb1d5d89c0913596;hp=352de1101d56ca9e569c610ef55fe08a5124b948;hpb=7c72977bb3400f474c8641fc91d9f54fba25cb64;p=e-mobility-charging-stations-simulator.git diff --git a/src/charging-station/ChargingStationUtils.ts b/src/charging-station/ChargingStationUtils.ts index 352de110..ab781fae 100644 --- a/src/charging-station/ChargingStationUtils.ts +++ b/src/charging-station/ChargingStationUtils.ts @@ -1,53 +1,75 @@ -import { ChargingProfile, ChargingSchedulePeriod } from '../types/ocpp/ChargingProfile'; -import { ChargingProfileKindType, RecurrencyKindType } from '../types/ocpp/1.6/ChargingProfile'; -import ChargingStationTemplate, { +import { createHash, randomBytes } from 'node:crypto'; +import type { EventEmitter } from 'node:events'; +import { basename, dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +import chalk from 'chalk'; +import moment from 'moment'; + +import type { ChargingStation } from './ChargingStation'; +import { BaseError } from '../exception'; +import { AmpereUnits, + AvailabilityType, + type BootNotificationRequest, + BootReasonEnumType, + type ChargingProfile, + ChargingProfileKindType, + ChargingRateUnitType, + type ChargingSchedulePeriod, + type ChargingStationInfo, + type ChargingStationTemplate, + ChargingStationWorkerMessageEvents, + ConnectorPhaseRotation, + type ConnectorStatus, + ConnectorStatusEnum, CurrentType, + type EvseTemplate, + type OCPP16BootNotificationRequest, + type OCPP20BootNotificationRequest, + OCPPVersion, + RecurrencyKindType, Voltage, -} from '../types/ChargingStationTemplate'; -import { MeterValueMeasurand, MeterValuePhase } from '../types/ocpp/MeterValues'; - -import BaseError from '../exception/BaseError'; -import { BootNotificationRequest } from '../types/ocpp/Requests'; -import ChargingStation from './ChargingStation'; -import ChargingStationConfiguration from '../types/ChargingStationConfiguration'; -import { ChargingStationConfigurationUtils } from './ChargingStationConfigurationUtils'; -import ChargingStationInfo from '../types/ChargingStationInfo'; -import Configuration from '../utils/Configuration'; -import Constants from '../utils/Constants'; -import { FileType } from '../types/FileType'; -import FileUtils from '../utils/FileUtils'; -import { SampledValueTemplate } from '../types/MeasurandPerPhaseSampledValueTemplates'; -import { StandardParametersKey } from '../types/ocpp/Configuration'; -import Utils from '../utils/Utils'; -import { WebSocketCloseEventStatusString } from '../types/WebSocket'; -import { WorkerProcessType } from '../types/Worker'; -import crypto from 'crypto'; -import fs from 'fs'; -import logger from '../utils/Logger'; -import moment from 'moment'; -import path from 'path'; +} from '../types'; +import { ACElectricUtils, Constants, DCElectricUtils, Utils, logger } from '../utils'; + +const moduleName = 'ChargingStationUtils'; export class ChargingStationUtils { + private constructor() { + // This is intentional + } + public static getChargingStationId( index: number, stationTemplate: ChargingStationTemplate ): string { // In case of multiple instances: add instance index to charging station id const instanceIndex = process.env.CF_INSTANCE_INDEX ?? 0; - const idSuffix = stationTemplate.nameSuffix ?? ''; - const idStr = '000000000' + index.toString(); + const idSuffix = stationTemplate?.nameSuffix ?? ''; + const idStr = `000000000${index.toString()}`; return stationTemplate?.fixedName ? stationTemplate.baseName - : stationTemplate.baseName + - '-' + - instanceIndex.toString() + - idStr.substring(idStr.length - 4) + - idSuffix; + : `${stationTemplate.baseName}-${instanceIndex.toString()}${idStr.substring( + idStr.length - 4 + )}${idSuffix}`; + } + + public static countReservableConnectors(connectors: Map) { + let reservableConnectors = 0; + for (const [connectorId, connectorStatus] of connectors) { + if (connectorId === 0) { + continue; + } + if (connectorStatus.status === ConnectorStatusEnum.Available) { + ++reservableConnectors; + } + } + return reservableConnectors; } public static getHashId(index: number, stationTemplate: ChargingStationTemplate): string { - const hashBootNotificationRequest = { + const chargingStationInfo = { chargePointModel: stationTemplate.chargePointModel, chargePointVendor: stationTemplate.chargePointVendor, ...(!Utils.isUndefined(stationTemplate.chargeBoxSerialNumberPrefix) && { @@ -56,11 +78,6 @@ export class ChargingStationUtils { ...(!Utils.isUndefined(stationTemplate.chargePointSerialNumberPrefix) && { chargePointSerialNumber: stationTemplate.chargePointSerialNumberPrefix, }), - ...(!Utils.isUndefined(stationTemplate.firmwareVersion) && { - firmwareVersion: stationTemplate.firmwareVersion, - }), - ...(!Utils.isUndefined(stationTemplate.iccid) && { iccid: stationTemplate.iccid }), - ...(!Utils.isUndefined(stationTemplate.imsi) && { imsi: stationTemplate.imsi }), ...(!Utils.isUndefined(stationTemplate.meterSerialNumberPrefix) && { meterSerialNumber: stationTemplate.meterSerialNumberPrefix, }), @@ -68,184 +85,322 @@ export class ChargingStationUtils { meterType: stationTemplate.meterType, }), }; - return crypto - .createHash(Constants.DEFAULT_HASH_ALGORITHM) + return createHash(Constants.DEFAULT_HASH_ALGORITHM) .update( - JSON.stringify(hashBootNotificationRequest) + - ChargingStationUtils.getChargingStationId(index, stationTemplate) + `${JSON.stringify(chargingStationInfo)}${ChargingStationUtils.getChargingStationId( + index, + stationTemplate + )}` ) .digest('hex'); } - public static getTemplateMaxNumberOfConnectors(stationTemplate: ChargingStationTemplate): number { - const templateConnectors = stationTemplate?.Connectors; - if (!templateConnectors) { + public static checkChargingStation(chargingStation: ChargingStation, logPrefix: string): boolean { + if (chargingStation.started === false && chargingStation.starting === false) { + logger.warn(`${logPrefix} charging station is stopped, cannot proceed`); + return false; + } + return true; + } + + public static getPhaseRotationValue( + connectorId: number, + numberOfPhases: number + ): string | undefined { + // AC/DC + if (connectorId === 0 && numberOfPhases === 0) { + return `${connectorId}.${ConnectorPhaseRotation.RST}`; + } else if (connectorId > 0 && numberOfPhases === 0) { + return `${connectorId}.${ConnectorPhaseRotation.NotApplicable}`; + // AC + } else if (connectorId > 0 && numberOfPhases === 1) { + return `${connectorId}.${ConnectorPhaseRotation.NotApplicable}`; + } else if (connectorId > 0 && numberOfPhases === 3) { + return `${connectorId}.${ConnectorPhaseRotation.RST}`; + } + } + + public static getMaxNumberOfEvses(evses: Record): number { + if (!evses) { return -1; } - return Object.keys(templateConnectors).length; + return Object.keys(evses).length; } - public static checkTemplateMaxConnectors( - templateMaxConnectors: number, - templateFile: string, - logPrefix: string - ): void { - if (templateMaxConnectors === 0) { + public static getMaxNumberOfConnectors(connectors: Record): number { + if (!connectors) { + return -1; + } + return Object.keys(connectors).length; + } + + public static getBootConnectorStatus( + chargingStation: ChargingStation, + connectorId: number, + connectorStatus: ConnectorStatus + ): ConnectorStatusEnum { + let connectorBootStatus: ConnectorStatusEnum; + if ( + !connectorStatus?.status && + (chargingStation.isChargingStationAvailable() === false || + chargingStation.isConnectorAvailable(connectorId) === false) + ) { + connectorBootStatus = ConnectorStatusEnum.Unavailable; + } else if (!connectorStatus?.status && connectorStatus?.bootStatus) { + // Set boot status in template at startup + connectorBootStatus = connectorStatus?.bootStatus; + } else if (connectorStatus?.status) { + // Set previous status at startup + connectorBootStatus = connectorStatus?.status; + } else { + // Set default status + connectorBootStatus = ConnectorStatusEnum.Available; + } + return connectorBootStatus; + } + + public static checkTemplate( + stationTemplate: ChargingStationTemplate, + logPrefix: string, + templateFile: string + ) { + if (Utils.isNullOrUndefined(stationTemplate)) { + const errorMsg = `Failed to read charging station template file ${templateFile}`; + logger.error(`${logPrefix} ${errorMsg}`); + throw new BaseError(errorMsg); + } + if (Utils.isEmptyObject(stationTemplate)) { + const errorMsg = `Empty charging station information from template file ${templateFile}`; + logger.error(`${logPrefix} ${errorMsg}`); + throw new BaseError(errorMsg); + } + if (Utils.isEmptyObject(stationTemplate.AutomaticTransactionGenerator)) { + stationTemplate.AutomaticTransactionGenerator = Constants.DEFAULT_ATG_CONFIGURATION; logger.warn( - `${logPrefix} Charging station information from template ${templateFile} with empty connectors configuration` + `${logPrefix} Empty automatic transaction generator configuration from template file ${templateFile}, set to default: %j`, + Constants.DEFAULT_ATG_CONFIGURATION ); - } else if (templateMaxConnectors < 0) { - logger.error( - `${logPrefix} Charging station information from template ${templateFile} with no connectors configuration defined` + } + if ( + Utils.isNullOrUndefined(stationTemplate.idTagsFile) || + Utils.isEmptyString(stationTemplate.idTagsFile) + ) { + logger.warn( + `${logPrefix} Missing id tags file in template file ${templateFile}. That can lead to issues with the Automatic Transaction Generator` ); } } - public static getConfiguredNumberOfConnectors( - index: number, - stationTemplate: ChargingStationTemplate - ): number { - let configuredMaxConnectors: number; - if (!Utils.isEmptyArray(stationTemplate.numberOfConnectors)) { - const numberOfConnectors = stationTemplate.numberOfConnectors as number[]; - // Distribute evenly the number of connectors - configuredMaxConnectors = numberOfConnectors[(index - 1) % numberOfConnectors.length]; - } else if (!Utils.isUndefined(stationTemplate.numberOfConnectors)) { - configuredMaxConnectors = stationTemplate.numberOfConnectors as number; - } else { - configuredMaxConnectors = stationTemplate?.Connectors[0] - ? ChargingStationUtils.getTemplateMaxNumberOfConnectors(stationTemplate) - 1 - : ChargingStationUtils.getTemplateMaxNumberOfConnectors(stationTemplate); + public static checkConnectorsConfiguration( + stationTemplate: ChargingStationTemplate, + logPrefix: string, + templateFile: string + ): { + configuredMaxConnectors: number; + templateMaxConnectors: number; + templateMaxAvailableConnectors: number; + } { + const configuredMaxConnectors = + ChargingStationUtils.getConfiguredNumberOfConnectors(stationTemplate); + ChargingStationUtils.checkConfiguredMaxConnectors( + configuredMaxConnectors, + logPrefix, + templateFile + ); + const templateMaxConnectors = ChargingStationUtils.getMaxNumberOfConnectors( + stationTemplate.Connectors + ); + ChargingStationUtils.checkTemplateMaxConnectors(templateMaxConnectors, logPrefix, templateFile); + const templateMaxAvailableConnectors = stationTemplate?.Connectors[0] + ? templateMaxConnectors - 1 + : templateMaxConnectors; + if ( + configuredMaxConnectors > templateMaxAvailableConnectors && + !stationTemplate?.randomConnectors + ) { + logger.warn( + `${logPrefix} Number of connectors exceeds the number of connector configurations in template ${templateFile}, forcing random connector configurations affectation` + ); + stationTemplate.randomConnectors = true; } - return configuredMaxConnectors; + return { configuredMaxConnectors, templateMaxConnectors, templateMaxAvailableConnectors }; } - public static checkConfiguredMaxConnectors( - configuredMaxConnectors: number, - templateFile: string, - logPrefix: string + public static checkStationInfoConnectorStatus( + connectorId: number, + connectorStatus: ConnectorStatus, + logPrefix: string, + templateFile: string ): void { - if (configuredMaxConnectors <= 0) { + if (!Utils.isNullOrUndefined(connectorStatus?.status)) { logger.warn( - `${logPrefix} Charging station information from template ${templateFile} with ${configuredMaxConnectors} connectors` + `${logPrefix} Charging station information from template ${templateFile} with connector id ${connectorId} status configuration defined, undefine it` ); + delete connectorStatus.status; } } - public static createBootNotificationRequest( - stationInfo: ChargingStationInfo - ): BootNotificationRequest { - return { - chargePointModel: stationInfo.chargePointModel, - chargePointVendor: stationInfo.chargePointVendor, - ...(!Utils.isUndefined(stationInfo.chargeBoxSerialNumber) && { - chargeBoxSerialNumber: stationInfo.chargeBoxSerialNumber, - }), - ...(!Utils.isUndefined(stationInfo.chargePointSerialNumber) && { - chargePointSerialNumber: stationInfo.chargePointSerialNumber, - }), - ...(!Utils.isUndefined(stationInfo.firmwareVersion) && { - firmwareVersion: stationInfo.firmwareVersion, - }), - ...(!Utils.isUndefined(stationInfo.iccid) && { iccid: stationInfo.iccid }), - ...(!Utils.isUndefined(stationInfo.imsi) && { imsi: stationInfo.imsi }), - ...(!Utils.isUndefined(stationInfo.meterSerialNumber) && { - meterSerialNumber: stationInfo.meterSerialNumber, - }), - ...(!Utils.isUndefined(stationInfo.meterType) && { - meterType: stationInfo.meterType, - }), - }; + public static buildConnectorsMap( + connectors: Record, + logPrefix: string, + templateFile: string + ): Map { + const connectorsMap = new Map(); + if (ChargingStationUtils.getMaxNumberOfConnectors(connectors) > 0) { + for (const connector in connectors) { + const connectorStatus = connectors[connector]; + const connectorId = Utils.convertToInt(connector); + ChargingStationUtils.checkStationInfoConnectorStatus( + connectorId, + connectorStatus, + logPrefix, + templateFile + ); + connectorsMap.set(connectorId, Utils.cloneObject(connectorStatus)); + } + } else { + logger.warn( + `${logPrefix} Charging station information from template ${templateFile} with no connectors, cannot build connectors map` + ); + } + return connectorsMap; } - public static workerPoolInUse(): boolean { - return [WorkerProcessType.DYNAMIC_POOL, WorkerProcessType.STATIC_POOL].includes( - Configuration.getWorkerProcess() - ); + public static initializeConnectorsMapStatus( + connectors: Map, + logPrefix: string + ): void { + for (const connectorId of connectors.keys()) { + if (connectorId > 0 && connectors.get(connectorId)?.transactionStarted === true) { + logger.warn( + `${logPrefix} Connector id ${connectorId} at initialization has a transaction started with id ${ + connectors.get(connectorId)?.transactionId + }` + ); + } + if (connectorId === 0) { + connectors.get(connectorId).availability = AvailabilityType.Operative; + if (Utils.isUndefined(connectors.get(connectorId)?.chargingProfiles)) { + connectors.get(connectorId).chargingProfiles = []; + } + } else if ( + connectorId > 0 && + Utils.isNullOrUndefined(connectors.get(connectorId)?.transactionStarted) + ) { + ChargingStationUtils.initializeConnectorStatus(connectors.get(connectorId)); + } + } } - public static workerDynamicPoolInUse(): boolean { - return Configuration.getWorkerProcess() === WorkerProcessType.DYNAMIC_POOL; + public static resetConnectorStatus(connectorStatus: ConnectorStatus): void { + connectorStatus.idTagLocalAuthorized = false; + connectorStatus.idTagAuthorized = false; + connectorStatus.transactionRemoteStarted = false; + connectorStatus.transactionStarted = false; + delete connectorStatus?.localAuthorizeIdTag; + delete connectorStatus?.authorizeIdTag; + delete connectorStatus?.transactionId; + delete connectorStatus?.transactionIdTag; + connectorStatus.transactionEnergyActiveImportRegisterValue = 0; + delete connectorStatus?.transactionBeginMeterValue; } - /** - * Convert websocket error code to human readable string message - * - * @param code websocket error code - * @returns human readable string message - */ - public static getWebSocketCloseEventStatusString(code: number): string { - if (code >= 0 && code <= 999) { - return '(Unused)'; - } else if (code >= 1016) { - if (code <= 1999) { - return '(For WebSocket standard)'; - } else if (code <= 2999) { - return '(For WebSocket extensions)'; - } else if (code <= 3999) { - return '(For libraries and frameworks)'; - } else if (code <= 4999) { - return '(For applications)'; - } - } - if (!Utils.isUndefined(WebSocketCloseEventStatusString[code])) { - return WebSocketCloseEventStatusString[code] as string; + public static createBootNotificationRequest( + stationInfo: ChargingStationInfo, + bootReason: BootReasonEnumType = BootReasonEnumType.PowerUp + ): BootNotificationRequest { + const ocppVersion = stationInfo.ocppVersion ?? OCPPVersion.VERSION_16; + switch (ocppVersion) { + case OCPPVersion.VERSION_16: + return { + chargePointModel: stationInfo.chargePointModel, + chargePointVendor: stationInfo.chargePointVendor, + ...(!Utils.isUndefined(stationInfo.chargeBoxSerialNumber) && { + chargeBoxSerialNumber: stationInfo.chargeBoxSerialNumber, + }), + ...(!Utils.isUndefined(stationInfo.chargePointSerialNumber) && { + chargePointSerialNumber: stationInfo.chargePointSerialNumber, + }), + ...(!Utils.isUndefined(stationInfo.firmwareVersion) && { + firmwareVersion: stationInfo.firmwareVersion, + }), + ...(!Utils.isUndefined(stationInfo.iccid) && { iccid: stationInfo.iccid }), + ...(!Utils.isUndefined(stationInfo.imsi) && { imsi: stationInfo.imsi }), + ...(!Utils.isUndefined(stationInfo.meterSerialNumber) && { + meterSerialNumber: stationInfo.meterSerialNumber, + }), + ...(!Utils.isUndefined(stationInfo.meterType) && { + meterType: stationInfo.meterType, + }), + } as OCPP16BootNotificationRequest; + case OCPPVersion.VERSION_20: + case OCPPVersion.VERSION_201: + return { + reason: bootReason, + chargingStation: { + model: stationInfo.chargePointModel, + vendorName: stationInfo.chargePointVendor, + ...(!Utils.isUndefined(stationInfo.firmwareVersion) && { + firmwareVersion: stationInfo.firmwareVersion, + }), + ...(!Utils.isUndefined(stationInfo.chargeBoxSerialNumber) && { + serialNumber: stationInfo.chargeBoxSerialNumber, + }), + ...((!Utils.isUndefined(stationInfo.iccid) || !Utils.isUndefined(stationInfo.imsi)) && { + modem: { + ...(!Utils.isUndefined(stationInfo.iccid) && { iccid: stationInfo.iccid }), + ...(!Utils.isUndefined(stationInfo.imsi) && { imsi: stationInfo.imsi }), + }, + }), + }, + } as OCPP20BootNotificationRequest; } - return '(Unknown)'; } - public static warnDeprecatedTemplateKey( - template: ChargingStationTemplate, - key: string, - templateFile: string, + public static warnTemplateKeysDeprecation( + stationTemplate: ChargingStationTemplate, logPrefix: string, - logMsgToAppend = '' - ): void { - if (!Utils.isUndefined(template[key])) { - logger.warn( - `${logPrefix} Deprecated template key '${key}' usage in file '${templateFile}'${ - logMsgToAppend && '. ' + logMsgToAppend - }` + templateFile: string + ) { + const templateKeys: { key: string; deprecatedKey: string }[] = [ + { key: 'supervisionUrls', deprecatedKey: 'supervisionUrl' }, + { key: 'idTagsFile', deprecatedKey: 'authorizationFile' }, + ]; + for (const templateKey of templateKeys) { + ChargingStationUtils.warnDeprecatedTemplateKey( + stationTemplate, + templateKey.deprecatedKey, + logPrefix, + templateFile, + `Use '${templateKey.key}' instead` + ); + ChargingStationUtils.convertDeprecatedTemplateKey( + stationTemplate, + templateKey.deprecatedKey, + templateKey.key ); - } - } - - public static convertDeprecatedTemplateKey( - template: ChargingStationTemplate, - deprecatedKey: string, - key: string - ): void { - if (!Utils.isUndefined(template[deprecatedKey])) { - template[key] = template[deprecatedKey] as unknown; - delete template[deprecatedKey]; } } public static stationTemplateToStationInfo( stationTemplate: ChargingStationTemplate ): ChargingStationInfo { - stationTemplate = Utils.cloneObject(stationTemplate); + stationTemplate = Utils.cloneObject(stationTemplate); delete stationTemplate.power; delete stationTemplate.powerUnit; + delete stationTemplate?.Connectors; + delete stationTemplate?.Evses; delete stationTemplate.Configuration; delete stationTemplate.AutomaticTransactionGenerator; delete stationTemplate.chargeBoxSerialNumberPrefix; delete stationTemplate.chargePointSerialNumberPrefix; delete stationTemplate.meterSerialNumberPrefix; - return stationTemplate; - } - - public static createStationInfoHash(stationInfo: ChargingStationInfo): void { - delete stationInfo.infoHash; - stationInfo.infoHash = crypto - .createHash(Constants.DEFAULT_HASH_ALGORITHM) - .update(JSON.stringify(stationInfo)) - .digest('hex'); + return stationTemplate as unknown as ChargingStationInfo; } public static createSerialNumber( stationTemplate: ChargingStationTemplate, - stationInfo: ChargingStationInfo = {} as ChargingStationInfo, + stationInfo: ChargingStationInfo, params: { randomSerialNumberUpperCase?: boolean; randomSerialNumber?: boolean; @@ -254,29 +409,24 @@ export class ChargingStationUtils { randomSerialNumber: true, } ): void { - params = params ?? {}; - params.randomSerialNumberUpperCase = params?.randomSerialNumberUpperCase ?? true; - params.randomSerialNumber = params?.randomSerialNumber ?? true; + params = { ...{ randomSerialNumberUpperCase: true, randomSerialNumber: true }, ...params }; const serialNumberSuffix = params?.randomSerialNumber ? ChargingStationUtils.getRandomSerialNumberSuffix({ upperCase: params.randomSerialNumberUpperCase, }) : ''; - stationInfo.chargePointSerialNumber = - stationTemplate?.chargePointSerialNumberPrefix && - stationTemplate.chargePointSerialNumberPrefix + serialNumberSuffix; - stationInfo.chargeBoxSerialNumber = - stationTemplate?.chargeBoxSerialNumberPrefix && - stationTemplate.chargeBoxSerialNumberPrefix + serialNumberSuffix; - stationInfo.meterSerialNumber = - stationTemplate?.meterSerialNumberPrefix && - stationTemplate.meterSerialNumberPrefix + serialNumberSuffix; + Utils.isNotEmptyString(stationTemplate?.chargePointSerialNumberPrefix) && + (stationInfo.chargePointSerialNumber = `${stationTemplate.chargePointSerialNumberPrefix}${serialNumberSuffix}`); + Utils.isNotEmptyString(stationTemplate?.chargeBoxSerialNumberPrefix) && + (stationInfo.chargeBoxSerialNumber = `${stationTemplate.chargeBoxSerialNumberPrefix}${serialNumberSuffix}`); + Utils.isNotEmptyString(stationTemplate?.meterSerialNumberPrefix) && + (stationInfo.meterSerialNumber = `${stationTemplate.meterSerialNumberPrefix}${serialNumberSuffix}`); } public static propagateSerialNumber( stationTemplate: ChargingStationTemplate, stationInfoSrc: ChargingStationInfo, - stationInfoDst: ChargingStationInfo = {} as ChargingStationInfo + stationInfoDst: ChargingStationInfo ) { if (!stationInfoSrc || !stationTemplate) { throw new BaseError( @@ -310,24 +460,233 @@ export class ChargingStationUtils { return unitDivider; } + public static getChargingStationConnectorChargingProfilesPowerLimit( + chargingStation: ChargingStation, + connectorId: number + ): number | undefined { + let limit: number, matchingChargingProfile: ChargingProfile; + // Get charging profiles for connector and sort by stack level + const chargingProfiles = + Utils.cloneObject( + chargingStation.getConnectorStatus(connectorId)?.chargingProfiles + )?.sort((a, b) => b.stackLevel - a.stackLevel) ?? []; + // Get profiles on connector 0 + if (chargingStation.getConnectorStatus(0)?.chargingProfiles) { + chargingProfiles.push( + ...Utils.cloneObject( + chargingStation.getConnectorStatus(0).chargingProfiles + ).sort((a, b) => b.stackLevel - a.stackLevel) + ); + } + if (Utils.isNotEmptyArray(chargingProfiles)) { + const result = ChargingStationUtils.getLimitFromChargingProfiles( + chargingProfiles, + chargingStation.logPrefix() + ); + if (!Utils.isNullOrUndefined(result)) { + limit = result?.limit; + matchingChargingProfile = result?.matchingChargingProfile; + switch (chargingStation.getCurrentOutType()) { + case CurrentType.AC: + limit = + matchingChargingProfile.chargingSchedule.chargingRateUnit === + ChargingRateUnitType.WATT + ? limit + : ACElectricUtils.powerTotal( + chargingStation.getNumberOfPhases(), + chargingStation.getVoltageOut(), + limit + ); + break; + case CurrentType.DC: + limit = + matchingChargingProfile.chargingSchedule.chargingRateUnit === + ChargingRateUnitType.WATT + ? limit + : DCElectricUtils.power(chargingStation.getVoltageOut(), limit); + } + const connectorMaximumPower = + chargingStation.getMaximumPower() / chargingStation.powerDivider; + if (limit > connectorMaximumPower) { + logger.error( + `${chargingStation.logPrefix()} Charging profile id ${ + matchingChargingProfile.chargingProfileId + } limit ${limit} is greater than connector id ${connectorId} maximum ${connectorMaximumPower}: %j`, + result + ); + limit = connectorMaximumPower; + } + } + } + return limit; + } + + public static getDefaultVoltageOut( + currentType: CurrentType, + logPrefix: string, + templateFile: string + ): Voltage { + const errorMsg = `Unknown ${currentType} currentOutType in template file ${templateFile}, cannot define default voltage out`; + let defaultVoltageOut: number; + switch (currentType) { + case CurrentType.AC: + defaultVoltageOut = Voltage.VOLTAGE_230; + break; + case CurrentType.DC: + defaultVoltageOut = Voltage.VOLTAGE_400; + break; + default: + logger.error(`${logPrefix} ${errorMsg}`); + throw new BaseError(errorMsg); + } + return defaultVoltageOut; + } + + public static getIdTagsFile(stationInfo: ChargingStationInfo): string | undefined { + return ( + stationInfo.idTagsFile && + join(dirname(fileURLToPath(import.meta.url)), 'assets', basename(stationInfo.idTagsFile)) + ); + } + + public static waitForChargingStationEvents = async ( + emitter: EventEmitter, + event: ChargingStationWorkerMessageEvents, + eventsToWait: number + ): Promise => { + return new Promise((resolve) => { + let events = 0; + if (eventsToWait === 0) { + resolve(events); + } + emitter.on(event, () => { + ++events; + if (events === eventsToWait) { + resolve(events); + } + }); + }); + }; + + private static getConfiguredNumberOfConnectors(stationTemplate: ChargingStationTemplate): number { + let configuredMaxConnectors: number; + if (Utils.isNotEmptyArray(stationTemplate.numberOfConnectors) === true) { + const numberOfConnectors = stationTemplate.numberOfConnectors as number[]; + configuredMaxConnectors = + numberOfConnectors[Math.floor(Utils.secureRandom() * numberOfConnectors.length)]; + } else if (Utils.isUndefined(stationTemplate.numberOfConnectors) === false) { + configuredMaxConnectors = stationTemplate.numberOfConnectors as number; + } else if (stationTemplate.Connectors && !stationTemplate.Evses) { + configuredMaxConnectors = stationTemplate?.Connectors[0] + ? ChargingStationUtils.getMaxNumberOfConnectors(stationTemplate.Connectors) - 1 + : ChargingStationUtils.getMaxNumberOfConnectors(stationTemplate.Connectors); + } else if (stationTemplate.Evses && !stationTemplate.Connectors) { + configuredMaxConnectors = 0; + for (const evse in stationTemplate.Evses) { + if (evse === '0') { + continue; + } + configuredMaxConnectors += ChargingStationUtils.getMaxNumberOfConnectors( + stationTemplate.Evses[evse].Connectors + ); + } + } + return configuredMaxConnectors; + } + + private static checkConfiguredMaxConnectors( + configuredMaxConnectors: number, + logPrefix: string, + templateFile: string + ): void { + if (configuredMaxConnectors <= 0) { + logger.warn( + `${logPrefix} Charging station information from template ${templateFile} with ${configuredMaxConnectors} connectors` + ); + } + } + + private static checkTemplateMaxConnectors( + templateMaxConnectors: number, + logPrefix: string, + templateFile: string + ): void { + if (templateMaxConnectors === 0) { + logger.warn( + `${logPrefix} Charging station information from template ${templateFile} with empty connectors configuration` + ); + } else if (templateMaxConnectors < 0) { + logger.error( + `${logPrefix} Charging station information from template ${templateFile} with no connectors configuration defined` + ); + } + } + + private static initializeConnectorStatus(connectorStatus: ConnectorStatus): void { + connectorStatus.availability = AvailabilityType.Operative; + connectorStatus.idTagLocalAuthorized = false; + connectorStatus.idTagAuthorized = false; + connectorStatus.transactionRemoteStarted = false; + connectorStatus.transactionStarted = false; + connectorStatus.energyActiveImportRegisterValue = 0; + connectorStatus.transactionEnergyActiveImportRegisterValue = 0; + if (Utils.isUndefined(connectorStatus.chargingProfiles)) { + connectorStatus.chargingProfiles = []; + } + } + + private static warnDeprecatedTemplateKey( + template: ChargingStationTemplate, + key: string, + logPrefix: string, + templateFile: string, + logMsgToAppend = '' + ): void { + if (!Utils.isUndefined(template[key])) { + const logMsg = `Deprecated template key '${key}' usage in file '${templateFile}'${ + Utils.isNotEmptyString(logMsgToAppend) ? `. ${logMsgToAppend}` : '' + }`; + logger.warn(`${logPrefix} ${logMsg}`); + console.warn(chalk.yellow(`${logMsg}`)); + } + } + + private static convertDeprecatedTemplateKey( + template: ChargingStationTemplate, + deprecatedKey: string, + key: string + ): void { + if (!Utils.isUndefined(template[deprecatedKey])) { + template[key] = template[deprecatedKey] as unknown; + delete template[deprecatedKey]; + } + } + /** - * Charging profiles should already be sorted by connectorId and stack level (highest stack level has priority) + * Charging profiles should already be sorted by connector id and stack level (highest stack level has priority) * - * @param {ChargingProfile[]} chargingProfiles - * @param {string} logPrefix - * @returns {{ limit, matchingChargingProfile }} + * @param chargingProfiles - + * @param logPrefix - + * @returns */ - public static getLimitFromChargingProfiles( + private static getLimitFromChargingProfiles( chargingProfiles: ChargingProfile[], logPrefix: string ): { limit: number; matchingChargingProfile: ChargingProfile; } | null { + const debugLogMsg = `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Matching charging profile found for power limitation: %j`; + const currentMoment = moment(); + const currentDate = new Date(); for (const chargingProfile of chargingProfiles) { // Set helpers - const currentMoment = moment(); const chargingSchedule = chargingProfile.chargingSchedule; + if (!chargingSchedule?.startSchedule) { + logger.warn( + `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: startSchedule is not defined in charging profile id ${chargingProfile.chargingProfileId}` + ); + } // Check type (recurring) and if it is already active // Adjust the daily recurring schedule to today if ( @@ -335,8 +694,12 @@ export class ChargingStationUtils { chargingProfile.recurrencyKind === RecurrencyKindType.DAILY && currentMoment.isAfter(chargingSchedule.startSchedule) ) { - const currentDate = new Date(); - chargingSchedule.startSchedule = new Date(chargingSchedule.startSchedule); + if (!(chargingSchedule?.startSchedule instanceof Date)) { + logger.warn( + `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: startSchedule is not a Date object in charging profile id ${chargingProfile.chargingProfileId}. Trying to convert it to a Date object` + ); + chargingSchedule.startSchedule = new Date(chargingSchedule.startSchedule); + } chargingSchedule.startSchedule.setFullYear( currentDate.getFullYear(), currentDate.getMonth(), @@ -367,10 +730,7 @@ export class ChargingStationUtils { limit: schedulePeriod.limit, matchingChargingProfile: chargingProfile, }; - logger.debug( - `${logPrefix} Matching charging profile found for power limitation: %j`, - result - ); + logger.debug(debugLogMsg, result); return result; } // Find the right schedule period @@ -384,10 +744,7 @@ export class ChargingStationUtils { limit: lastButOneSchedule.limit, matchingChargingProfile: chargingProfile, }; - logger.debug( - `${logPrefix} Matching charging profile found for power limitation: %j`, - result - ); + logger.debug(debugLogMsg, result); return result; } // Keep it @@ -403,10 +760,7 @@ export class ChargingStationUtils { limit: lastButOneSchedule.limit, matchingChargingProfile: chargingProfile, }; - logger.debug( - `${logPrefix} Matching charging profile found for power limitation: %j`, - result - ); + logger.debug(debugLogMsg, result); return result; } } @@ -415,164 +769,11 @@ export class ChargingStationUtils { return null; } - public static getDefaultVoltageOut( - currentType: CurrentType, - templateFile: string, - logPrefix: string - ): Voltage { - const errMsg = `${logPrefix} Unknown ${currentType} currentOutType in template file ${templateFile}, cannot define default voltage out`; - let defaultVoltageOut: number; - switch (currentType) { - case CurrentType.AC: - defaultVoltageOut = Voltage.VOLTAGE_230; - break; - case CurrentType.DC: - defaultVoltageOut = Voltage.VOLTAGE_400; - break; - default: - logger.error(errMsg); - throw new Error(errMsg); - } - return defaultVoltageOut; - } - - public static getSampledValueTemplate( - chargingStation: ChargingStation, - connectorId: number, - measurand: MeterValueMeasurand = MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER, - phase?: MeterValuePhase - ): SampledValueTemplate | undefined { - const onPhaseStr = phase ? `on phase ${phase} ` : ''; - if (!Constants.SUPPORTED_MEASURANDS.includes(measurand)) { - logger.warn( - `${chargingStation.logPrefix()} Trying to get unsupported MeterValues measurand '${measurand}' ${onPhaseStr}in template on connectorId ${connectorId}` - ); - return; - } - if ( - measurand !== MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER && - !ChargingStationConfigurationUtils.getConfigurationKey( - chargingStation, - StandardParametersKey.MeterValuesSampledData - )?.value.includes(measurand) - ) { - logger.debug( - `${chargingStation.logPrefix()} Trying to get MeterValues measurand '${measurand}' ${onPhaseStr}in template on connectorId ${connectorId} not found in '${ - StandardParametersKey.MeterValuesSampledData - }' OCPP parameter` - ); - return; - } - const sampledValueTemplates: SampledValueTemplate[] = - chargingStation.getConnectorStatus(connectorId).MeterValues; - for ( - let index = 0; - !Utils.isEmptyArray(sampledValueTemplates) && index < sampledValueTemplates.length; - index++ - ) { - if ( - !Constants.SUPPORTED_MEASURANDS.includes( - sampledValueTemplates[index]?.measurand ?? - MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER - ) - ) { - logger.warn( - `${chargingStation.logPrefix()} Unsupported MeterValues measurand '${measurand}' ${onPhaseStr}in template on connectorId ${connectorId}` - ); - } else if ( - phase && - sampledValueTemplates[index]?.phase === phase && - sampledValueTemplates[index]?.measurand === measurand && - ChargingStationConfigurationUtils.getConfigurationKey( - chargingStation, - StandardParametersKey.MeterValuesSampledData - )?.value.includes(measurand) - ) { - return sampledValueTemplates[index]; - } else if ( - !phase && - !sampledValueTemplates[index].phase && - sampledValueTemplates[index]?.measurand === measurand && - ChargingStationConfigurationUtils.getConfigurationKey( - chargingStation, - StandardParametersKey.MeterValuesSampledData - )?.value.includes(measurand) - ) { - return sampledValueTemplates[index]; - } else if ( - measurand === MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER && - (!sampledValueTemplates[index].measurand || - sampledValueTemplates[index].measurand === measurand) - ) { - return sampledValueTemplates[index]; - } - } - if (measurand === MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER) { - const errorMsg = `${chargingStation.logPrefix()} Missing MeterValues for default measurand '${measurand}' in template on connectorId ${connectorId}`; - logger.error(errorMsg); - throw new Error(errorMsg); - } - logger.debug( - `${chargingStation.logPrefix()} No MeterValues for measurand '${measurand}' ${onPhaseStr}in template on connectorId ${connectorId}` - ); - } - - public static getAuthorizedTags( - stationInfo: ChargingStationInfo, - templateFile: string, - logPrefix: string - ): string[] { - let authorizedTags: string[] = []; - const authorizationFile = ChargingStationUtils.getAuthorizationFile(stationInfo); - if (authorizationFile) { - try { - // Load authorization file - authorizedTags = JSON.parse(fs.readFileSync(authorizationFile, 'utf8')) as string[]; - } catch (error) { - FileUtils.handleFileException( - logPrefix, - FileType.Authorization, - authorizationFile, - error as NodeJS.ErrnoException - ); - } - } else { - logger.info(logPrefix + ' No authorization file given in template file ' + templateFile); - } - return authorizedTags; - } - - public static getAuthorizationFile(stationInfo: ChargingStationInfo): string | undefined { - return ( - stationInfo.authorizationFile && - path.join( - path.resolve(__dirname, '../'), - 'assets', - path.basename(stationInfo.authorizationFile) - ) - ); - } - - public static isChargingStationConfigurationCacheable( - chargingStationConfiguration: ChargingStationConfiguration - ): boolean { - return ( - !Utils.isNullOrUndefined(chargingStationConfiguration?.configurationKey) && - !Utils.isNullOrUndefined(chargingStationConfiguration?.stationInfo) && - !Utils.isNullOrUndefined(chargingStationConfiguration?.configurationHash) && - !Utils.isEmptyArray(chargingStationConfiguration?.configurationKey) && - !Utils.isEmptyObject(chargingStationConfiguration?.stationInfo) && - !Utils.isEmptyString(chargingStationConfiguration?.configurationHash) - ); - } - private static getRandomSerialNumberSuffix(params?: { randomBytesLength?: number; upperCase?: boolean; }): string { - const randomSerialNumberSuffix = crypto - .randomBytes(params?.randomBytesLength ?? 16) - .toString('hex'); + const randomSerialNumberSuffix = randomBytes(params?.randomBytesLength ?? 16).toString('hex'); if (params?.upperCase) { return randomSerialNumberSuffix.toUpperCase(); }