import { createHash, randomBytes } from 'node:crypto';
import type { EventEmitter } from 'node:events';
import { basename, dirname, join } from 'node:path';
+import { env } from 'node:process';
import { fileURLToPath } from 'node:url';
import chalk from 'chalk';
isDate,
isPast,
isWithinInterval,
+ maxTime,
toDate,
} from 'date-fns';
stationTemplate: ChargingStationTemplate,
): string => {
// In case of multiple instances: add instance index to charging station id
- const instanceIndex = process.env.CF_INSTANCE_INDEX ?? 0;
+ const instanceIndex = env.CF_INSTANCE_INDEX ?? 0;
const idSuffix = stationTemplate?.nameSuffix ?? '';
const idStr = `000000000${index.toString()}`;
return stationTemplate?.fixedName
checkConfiguredMaxConnectors(configuredMaxConnectors, logPrefix, templateFile);
const templateMaxConnectors = getMaxNumberOfConnectors(stationTemplate.Connectors!);
checkTemplateMaxConnectors(templateMaxConnectors, logPrefix, templateFile);
- const templateMaxAvailableConnectors = stationTemplate.Connectors![0]
+ const templateMaxAvailableConnectors = stationTemplate.Connectors?.[0]
? templateMaxConnectors - 1
: templateMaxConnectors;
if (
};
export const resetConnectorStatus = (connectorStatus: ConnectorStatus): void => {
+ connectorStatus.chargingProfiles =
+ connectorStatus.transactionId && isNotEmptyArray(connectorStatus.chargingProfiles)
+ ? connectorStatus.chargingProfiles?.filter(
+ (chargingProfile) => chargingProfile.transactionId !== connectorStatus.transactionId,
+ )
+ : [];
connectorStatus.idTagLocalAuthorized = false;
connectorStatus.idTagAuthorized = false;
connectorStatus.transactionRemoteStarted = false;
export const createSerialNumber = (
stationTemplate: ChargingStationTemplate,
stationInfo: ChargingStationInfo,
- params: {
+ params?: {
randomSerialNumberUpperCase?: boolean;
randomSerialNumber?: boolean;
- } = {
- randomSerialNumberUpperCase: true,
- randomSerialNumber: true,
},
): void => {
params = { ...{ randomSerialNumberUpperCase: true, randomSerialNumber: true }, ...params };
return unitDivider;
};
+/**
+ * Gets the connector cloned charging profiles applying a power limitation
+ * and sorted by connector id descending then stack level descending
+ *
+ * @param chargingStation -
+ * @param connectorId -
+ * @returns connector charging profiles array
+ */
+export const getConnectorChargingProfiles = (
+ chargingStation: ChargingStation,
+ connectorId: number,
+) => {
+ return cloneObject<ChargingProfile[]>(
+ (chargingStation.getConnectorStatus(connectorId)?.chargingProfiles ?? [])
+ .sort((a, b) => b.stackLevel - a.stackLevel)
+ .concat(
+ (chargingStation.getConnectorStatus(0)?.chargingProfiles ?? []).sort(
+ (a, b) => b.stackLevel - a.stackLevel,
+ ),
+ ),
+ );
+};
+
export const getChargingStationConnectorChargingProfilesPowerLimit = (
chargingStation: ChargingStation,
connectorId: number,
): number | undefined => {
- let limit: number | undefined, matchingChargingProfile: ChargingProfile | undefined;
- // Get charging profiles for connector id and sort by stack level
- const chargingProfiles =
- cloneObject<ChargingProfile[]>(
- chargingStation.getConnectorStatus(connectorId)!.chargingProfiles!,
- )?.sort((a, b) => b.stackLevel - a.stackLevel) ?? [];
- // Get charging profiles on connector 0 and sort by stack level
- if (isNotEmptyArray(chargingStation.getConnectorStatus(0)?.chargingProfiles)) {
- chargingProfiles.push(
- ...cloneObject<ChargingProfile[]>(
- chargingStation.getConnectorStatus(0)!.chargingProfiles!,
- ).sort((a, b) => b.stackLevel - a.stackLevel),
- );
- }
+ let limit: number | undefined, chargingProfile: ChargingProfile | undefined;
+ // Get charging profiles sorted by connector id then stack level
+ const chargingProfiles = getConnectorChargingProfiles(chargingStation, connectorId);
if (isNotEmptyArray(chargingProfiles)) {
const result = getLimitFromChargingProfiles(
chargingStation,
);
if (!isNullOrUndefined(result)) {
limit = result?.limit;
- matchingChargingProfile = result?.matchingChargingProfile;
+ chargingProfile = result?.chargingProfile;
switch (chargingStation.getCurrentOutType()) {
case CurrentType.AC:
limit =
- matchingChargingProfile?.chargingSchedule?.chargingRateUnit ===
- ChargingRateUnitType.WATT
+ chargingProfile?.chargingSchedule?.chargingRateUnit === ChargingRateUnitType.WATT
? limit
: ACElectricUtils.powerTotal(
chargingStation.getNumberOfPhases(),
break;
case CurrentType.DC:
limit =
- matchingChargingProfile?.chargingSchedule?.chargingRateUnit ===
- ChargingRateUnitType.WATT
+ chargingProfile?.chargingSchedule?.chargingRateUnit === ChargingRateUnitType.WATT
? limit
: DCElectricUtils.power(chargingStation.getVoltageOut(), limit!);
}
chargingStation.getMaximumPower() / chargingStation.powerDivider;
if (limit! > connectorMaximumPower) {
logger.error(
- `${chargingStation.logPrefix()} ${moduleName}.getChargingStationConnectorChargingProfilesPowerLimit: Charging profile id ${matchingChargingProfile?.chargingProfileId} limit ${limit} is greater than connector id ${connectorId} maximum ${connectorMaximumPower}: %j`,
+ `${chargingStation.logPrefix()} ${moduleName}.getChargingStationConnectorChargingProfilesPowerLimit: Charging profile id ${chargingProfile?.chargingProfileId} limit ${limit} is greater than connector id ${connectorId} maximum ${connectorMaximumPower}: %j`,
result,
);
limit = connectorMaximumPower;
} else if (isUndefined(stationTemplate.numberOfConnectors) === false) {
configuredMaxNumberOfConnectors = stationTemplate.numberOfConnectors as number;
} else if (stationTemplate.Connectors && !stationTemplate.Evses) {
- configuredMaxNumberOfConnectors = stationTemplate.Connectors[0]
+ configuredMaxNumberOfConnectors = stationTemplate.Connectors?.[0]
? getMaxNumberOfConnectors(stationTemplate.Connectors) - 1
: getMaxNumberOfConnectors(stationTemplate.Connectors);
} else if (stationTemplate.Evses && !stationTemplate.Connectors) {
templateFile: string,
logMsgToAppend = '',
): void => {
- if (!isUndefined(template[key as keyof ChargingStationTemplate])) {
+ if (!isUndefined(template?.[key as keyof ChargingStationTemplate])) {
const logMsg = `Deprecated template key '${key}' usage in file '${templateFile}'${
isNotEmptyString(logMsgToAppend) ? `. ${logMsgToAppend}` : ''
}`;
logger.warn(`${logPrefix} ${logMsg}`);
- console.warn(chalk.yellow(`${logMsg}`));
+ console.warn(`${chalk.green(logPrefix)} ${chalk.yellow(logMsg)}`);
}
};
deprecatedKey: string,
key?: string,
): void => {
- if (!isUndefined(template[deprecatedKey as keyof ChargingStationTemplate])) {
+ if (!isUndefined(template?.[deprecatedKey as keyof ChargingStationTemplate])) {
if (!isUndefined(key)) {
(template as unknown as Record<string, unknown>)[key!] =
template[deprecatedKey as keyof ChargingStationTemplate];
interface ChargingProfilesLimit {
limit: number;
- matchingChargingProfile: ChargingProfile;
+ chargingProfile: ChargingProfile;
}
/**
- * Charging profiles shall already be sorted by connector id and stack level (highest stack level has priority)
+ * Charging profiles shall already be sorted by connector id descending then stack level descending
*
* @param chargingStation -
* @param connectorId -
): ChargingProfilesLimit | undefined => {
const debugLogMsg = `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Matching charging profile found for power limitation: %j`;
const currentDate = new Date();
- const connectorStatus = chargingStation.getConnectorStatus(connectorId);
+ const connectorStatus = chargingStation.getConnectorStatus(connectorId)!;
for (const chargingProfile of chargingProfiles) {
const chargingSchedule = chargingProfile.chargingSchedule;
- if (connectorStatus?.transactionStarted && isNullOrUndefined(chargingSchedule?.startSchedule)) {
+ if (isNullOrUndefined(chargingSchedule?.startSchedule) && connectorStatus?.transactionStarted) {
logger.debug(
`${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} has no startSchedule defined. Trying to set it to the connector current transaction start date`,
);
// OCPP specifies that if startSchedule is not defined, it should be relative to start of the connector transaction
chargingSchedule.startSchedule = connectorStatus?.transactionStart;
}
- if (!isDate(chargingSchedule?.startSchedule)) {
+ if (
+ !isNullOrUndefined(chargingSchedule?.startSchedule) &&
+ !isDate(chargingSchedule?.startSchedule)
+ ) {
logger.warn(
- `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} startSchedule property is not a Date object. Trying to convert it to a Date object`,
+ `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} startSchedule property is not a Date instance. Trying to convert it to a Date instance`,
);
chargingSchedule.startSchedule = convertToDate(chargingSchedule?.startSchedule)!;
}
- switch (chargingProfile.chargingProfileKind) {
- case ChargingProfileKindType.RECURRING:
- if (!canProceedRecurringChargingProfile(chargingProfile, logPrefix)) {
- continue;
- }
- prepareRecurringChargingProfile(chargingProfile, currentDate, logPrefix);
- break;
- case ChargingProfileKindType.RELATIVE:
- connectorStatus?.transactionStarted &&
- (chargingSchedule.startSchedule = connectorStatus?.transactionStart);
- break;
+ if (
+ !isNullOrUndefined(chargingSchedule?.startSchedule) &&
+ isNullOrUndefined(chargingSchedule?.duration)
+ ) {
+ logger.debug(
+ `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} has no duration defined and will be set to the maximum time allowed`,
+ );
+ // OCPP specifies that if duration is not defined, it should be infinite
+ chargingSchedule.duration = differenceInSeconds(maxTime, chargingSchedule.startSchedule!);
+ }
+ if (!prepareChargingProfileKind(connectorStatus, chargingProfile, currentDate, logPrefix)) {
+ continue;
}
if (!canProceedChargingProfile(chargingProfile, currentDate, logPrefix)) {
continue;
}
// Check if the charging profile is active
if (
- isValidTime(chargingSchedule?.startSchedule) &&
isWithinInterval(currentDate, {
start: chargingSchedule.startSchedule!,
end: addSeconds(chargingSchedule.startSchedule!, chargingSchedule.duration!),
b: ChargingSchedulePeriod,
) => a.startPeriod - b.startPeriod;
if (
- isArraySorted<ChargingSchedulePeriod>(
+ !isArraySorted<ChargingSchedulePeriod>(
chargingSchedule.chargingSchedulePeriod,
chargingSchedulePeriodCompareFn,
- ) === false
+ )
) {
logger.warn(
`${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} schedule periods are not sorted by start period`,
);
chargingSchedule.chargingSchedulePeriod.sort(chargingSchedulePeriodCompareFn);
}
- // Check if the first schedule period start period is equal to 0
+ // Check if the first schedule period startPeriod property is equal to 0
if (chargingSchedule.chargingSchedulePeriod[0].startPeriod !== 0) {
logger.error(
`${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} first schedule period start period ${chargingSchedule.chargingSchedulePeriod[0].startPeriod} is not equal to 0`,
if (chargingSchedule.chargingSchedulePeriod.length === 1) {
const result: ChargingProfilesLimit = {
limit: chargingSchedule.chargingSchedulePeriod[0].limit,
- matchingChargingProfile: chargingProfile,
+ chargingProfile,
};
logger.debug(debugLogMsg, result);
return result;
// Found the schedule period: previous is the correct one
const result: ChargingProfilesLimit = {
limit: previousChargingSchedulePeriod!.limit,
- matchingChargingProfile: chargingProfile,
+ chargingProfile,
};
logger.debug(debugLogMsg, result);
return result;
if (
index === chargingSchedule.chargingSchedulePeriod.length - 1 ||
(index < chargingSchedule.chargingSchedulePeriod.length - 1 &&
- chargingSchedule.duration! >
- differenceInSeconds(
- addSeconds(
- chargingSchedule.startSchedule!,
- chargingSchedule.chargingSchedulePeriod[index + 1].startPeriod,
- ),
+ differenceInSeconds(
+ addSeconds(
chargingSchedule.startSchedule!,
- ))
+ chargingSchedule.chargingSchedulePeriod[index + 1].startPeriod,
+ ),
+ chargingSchedule.startSchedule!,
+ ) > chargingSchedule.duration!)
) {
const result: ChargingProfilesLimit = {
limit: previousChargingSchedulePeriod.limit,
- matchingChargingProfile: chargingProfile,
+ chargingProfile,
};
logger.debug(debugLogMsg, result);
return result;
}
};
-const canProceedChargingProfile = (
+export const prepareChargingProfileKind = (
+ connectorStatus: ConnectorStatus,
+ chargingProfile: ChargingProfile,
+ currentDate: Date,
+ logPrefix: string,
+): boolean => {
+ switch (chargingProfile.chargingProfileKind) {
+ case ChargingProfileKindType.RECURRING:
+ if (!canProceedRecurringChargingProfile(chargingProfile, logPrefix)) {
+ return false;
+ }
+ prepareRecurringChargingProfile(chargingProfile, currentDate, logPrefix);
+ break;
+ case ChargingProfileKindType.RELATIVE:
+ if (!isNullOrUndefined(chargingProfile.chargingSchedule.startSchedule)) {
+ logger.warn(
+ `${logPrefix} ${moduleName}.prepareChargingProfileKind: Relative charging profile id ${chargingProfile.chargingProfileId} has a startSchedule property defined. It will be ignored or used if the connector has a transaction started`,
+ );
+ delete chargingProfile.chargingSchedule.startSchedule;
+ }
+ if (connectorStatus?.transactionStarted) {
+ chargingProfile.chargingSchedule.startSchedule = connectorStatus?.transactionStart;
+ }
+ // FIXME: Handle relative charging profile duration
+ break;
+ }
+ return true;
+};
+
+export const canProceedChargingProfile = (
chargingProfile: ChargingProfile,
currentDate: Date,
logPrefix: string,
);
return false;
}
- const chargingSchedule = chargingProfile.chargingSchedule;
- if (isNullOrUndefined(chargingSchedule?.startSchedule)) {
+ if (
+ isNullOrUndefined(chargingProfile.chargingSchedule.startSchedule) ||
+ isNullOrUndefined(chargingProfile.chargingSchedule.duration)
+ ) {
+ logger.error(
+ `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId} has no startSchedule or duration defined`,
+ );
+ return false;
+ }
+ if (
+ !isNullOrUndefined(chargingProfile.chargingSchedule.startSchedule) &&
+ !isValidTime(chargingProfile.chargingSchedule.startSchedule)
+ ) {
logger.error(
- `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId} has (still) no startSchedule defined`,
+ `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId} has an invalid startSchedule date defined`,
);
return false;
}
- if (isNullOrUndefined(chargingSchedule?.duration)) {
+ if (
+ !isNullOrUndefined(chargingProfile.chargingSchedule.duration) &&
+ !Number.isSafeInteger(chargingProfile.chargingSchedule.duration)
+ ) {
logger.error(
- `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId} has no duration defined, not yet supported`,
+ `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId} has non integer duration defined`,
);
return false;
}
);
return false;
}
+ if (
+ chargingProfile.chargingProfileKind === ChargingProfileKindType.RECURRING &&
+ isNullOrUndefined(chargingProfile.chargingSchedule.startSchedule)
+ ) {
+ logger.error(
+ `${logPrefix} ${moduleName}.canProceedRecurringChargingProfile: Recurring charging profile id ${chargingProfile.chargingProfileId} has no startSchedule defined`,
+ );
+ return false;
+ }
return true;
};
break;
default:
logger.error(
- `${logPrefix} ${moduleName}.prepareRecurringChargingProfile: Recurring charging profile id ${chargingProfile.chargingProfileId} recurrency kind ${chargingProfile.recurrencyKind} is not supported`,
+ `${logPrefix} ${moduleName}.prepareRecurringChargingProfile: Recurring ${chargingProfile.recurrencyKind} charging profile id ${chargingProfile.chargingProfileId} is not supported`,
);
}
if (recurringIntervalTranslated && !isWithinInterval(currentDate, recurringInterval!)) {