import path from 'node:path';
import { fileURLToPath } from 'node:url';
+import chalk from 'chalk';
import moment from 'moment';
import type { ChargingStation } from './internal';
import { BaseError } from '../exception';
import {
AmpereUnits,
+ AvailabilityType,
type BootNotificationRequest,
BootReasonEnumType,
type ChargingProfile,
type ChargingSchedulePeriod,
type ChargingStationInfo,
type ChargingStationTemplate,
+ ConnectorPhaseRotation,
+ type ConnectorStatus,
+ ConnectorStatusEnum,
CurrentType,
+ type EvseTemplate,
type OCPP16BootNotificationRequest,
type OCPP20BootNotificationRequest,
OCPPVersion,
...(!Utils.isUndefined(stationTemplate.chargePointSerialNumberPrefix) && {
chargePointSerialNumber: stationTemplate.chargePointSerialNumberPrefix,
}),
- // FIXME?: Should a firmware version change always reference a new configuration file?
- ...(!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,
}),
.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<string, EvseTemplate>): 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<string, ConnectorStatus>): 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 checkTemplateFile(
+ 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 = {
+ enable: false,
+ minDuration: 60,
+ maxDuration: 120,
+ minDelayBetweenTwoTransactions: 15,
+ maxDelayBetweenTwoTransactions: 30,
+ probabilityOfStart: 1,
+ stopAfterHours: 0.3,
+ stopOnConnectionFailure: true,
+ };
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 values`
);
- } else if (templateMaxConnectors < 0) {
- logger.error(
- `${logPrefix} Charging station information from template ${templateFile} with no connectors configuration defined`
+ }
+ }
+
+ 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, templateMaxConnectors, templateMaxAvailableConnectors };
}
- public 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;
+ public static checkStationInfoConnectorStatus(
+ connectorId: number,
+ connectorStatus: ConnectorStatus,
+ logPrefix: string,
+ templateFile: string
+ ): void {
+ if (!Utils.isNullOrUndefined(connectorStatus?.status)) {
+ logger.warn(
+ `${logPrefix} Charging station information from template ${templateFile} with connector id ${connectorId} status configuration defined, undefine it`
+ );
+ delete connectorStatus.status;
+ }
+ }
+
+ public static buildConnectorsMap(
+ connectors: Record<string, ConnectorStatus>,
+ logPrefix: string,
+ templateFile: string
+ ): Map<number, ConnectorStatus> {
+ const connectorsMap = new Map<number, ConnectorStatus>();
+ 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>(connectorStatus));
+ }
} else {
- configuredMaxConnectors = stationTemplate?.Connectors[0]
- ? ChargingStationUtils.getTemplateMaxNumberOfConnectors(stationTemplate) - 1
- : ChargingStationUtils.getTemplateMaxNumberOfConnectors(stationTemplate);
+ logger.warn(
+ `${logPrefix} Charging station information from template ${templateFile} with no connectors, cannot build connectors map`
+ );
}
- return configuredMaxConnectors;
+ return connectorsMap;
}
- public static checkConfiguredMaxConnectors(
- configuredMaxConnectors: number,
- templateFile: string,
+ public static initializeConnectorsMapStatus(
+ connectors: Map<number, ConnectorStatus>,
logPrefix: string
): void {
- if (configuredMaxConnectors <= 0) {
- logger.warn(
- `${logPrefix} Charging station information from template ${templateFile} with ${configuredMaxConnectors} connectors`
- );
+ 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 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;
+ }
+
public static createBootNotificationRequest(
stationInfo: ChargingStationInfo,
bootReason: BootReasonEnumType = BootReasonEnumType.PowerUp
return Configuration.getWorker().processType === WorkerProcessType.dynamicPool;
}
- 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}'${
- Utils.isNotEmptyString(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<ChargingStationTemplate>(stationTemplate);
delete stationTemplate.power;
delete stationTemplate.powerUnit;
+ delete stationTemplate?.Connectors;
+ delete stationTemplate?.Evses;
delete stationTemplate.Configuration;
delete stationTemplate.AutomaticTransactionGenerator;
delete stationTemplate.chargeBoxSerialNumberPrefix;
return stationTemplate as unknown as ChargingStationInfo;
}
- public static createStationInfoHash(stationInfo: ChargingStationInfo): void {
- delete stationInfo.infoHash;
- stationInfo.infoHash = crypto
- .createHash(Constants.DEFAULT_HASH_ALGORITHM)
- .update(JSON.stringify(stationInfo))
- .digest('hex');
- }
-
public static createSerialNumber(
stationTemplate: ChargingStationTemplate,
stationInfo: ChargingStationInfo,
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 = Utils.isNotEmptyString(
- stationTemplate?.chargePointSerialNumberPrefix
- )
- ? `${stationTemplate.chargePointSerialNumberPrefix}${serialNumberSuffix}`
- : undefined;
- stationInfo.chargeBoxSerialNumber = Utils.isNotEmptyString(
- stationTemplate?.chargeBoxSerialNumberPrefix
- )
- ? `${stationTemplate.chargeBoxSerialNumberPrefix}${serialNumberSuffix}`
- : undefined;
- stationInfo.meterSerialNumber = Utils.isNotEmptyString(stationTemplate?.meterSerialNumberPrefix)
- ? `${stationTemplate.meterSerialNumberPrefix}${serialNumberSuffix}`
- : undefined;
+ 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(
connectorId: number
): number | undefined {
let limit: number, matchingChargingProfile: ChargingProfile;
- let chargingProfiles: ChargingProfile[] = [];
// Get charging profiles for connector and sort by stack level
- chargingProfiles = chargingStation
- .getConnectorStatus(connectorId)
- ?.chargingProfiles?.sort((a, b) => b.stackLevel - a.stackLevel);
+ const chargingProfiles =
+ Utils.cloneObject<ChargingProfile[]>(
+ chargingStation.getConnectorStatus(connectorId)?.chargingProfiles
+ )?.sort((a, b) => b.stackLevel - a.stackLevel) ?? [];
// Get profiles on connector 0
if (chargingStation.getConnectorStatus(0)?.chargingProfiles) {
chargingProfiles.push(
- ...chargingStation
- .getConnectorStatus(0)
- .chargingProfiles.sort((a, b) => b.stackLevel - a.stackLevel)
+ ...Utils.cloneObject<ChargingProfile[]>(
+ chargingStation.getConnectorStatus(0).chargingProfiles
+ ).sort((a, b) => b.stackLevel - a.stackLevel)
);
}
if (Utils.isNotEmptyArray(chargingProfiles)) {
public static getDefaultVoltageOut(
currentType: CurrentType,
- templateFile: string,
- logPrefix: string
+ logPrefix: string,
+ templateFile: string
): Voltage {
- const errMsg = `Unknown ${currentType} currentOutType in template file ${templateFile}, cannot define default voltage out`;
+ 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_400;
break;
default:
- logger.error(`${logPrefix} ${errMsg}`);
- throw new BaseError(errMsg);
+ logger.error(`${logPrefix} ${errorMsg}`);
+ throw new BaseError(errorMsg);
}
return defaultVoltageOut;
}
- public static getAuthorizationFile(stationInfo: ChargingStationInfo): string | undefined {
+ public static getIdTagsFile(stationInfo: ChargingStationInfo): string | undefined {
return (
- stationInfo.authorizationFile &&
+ stationInfo.idTagsFile &&
path.join(
path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../'),
'assets',
- path.basename(stationInfo.authorizationFile)
+ path.basename(stationInfo.idTagsFile)
)
);
}
+ 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 chargingProfiles -
* @param logPrefix -
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 (
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(),