fix: ensure all charging station changes are pushed at start transaction
[e-mobility-charging-stations-simulator.git] / src / charging-station / Helpers.ts
CommitLineData
d972af76
JB
1import { createHash, randomBytes } from 'node:crypto';
2import type { EventEmitter } from 'node:events';
3import { basename, dirname, join } from 'node:path';
130783a7 4import { fileURLToPath } from 'node:url';
8114d10e 5
e302df1d 6import chalk from 'chalk';
f924d466
JB
7import {
8 addDays,
f924d466
JB
9 addSeconds,
10 addWeeks,
497588ef 11 differenceInDays,
d476bc1b 12 differenceInSeconds,
497588ef 13 differenceInWeeks,
f924d466
JB
14 isAfter,
15 isBefore,
0bd926c1 16 isDate,
90aceaf6 17 isPast,
252a7d22 18 isWithinInterval,
522e4b05 19 toDate,
f924d466 20} from 'date-fns';
8114d10e 21
4c3c0d59 22import type { ChargingStation } from './ChargingStation';
357a5553 23import { getConfigurationKey } from './ConfigurationKeyUtils';
268a74bb 24import { BaseError } from '../exception';
981ebfbe 25import {
492cf6ab 26 AmpereUnits,
04b1261c 27 AvailabilityType,
268a74bb
JB
28 type BootNotificationRequest,
29 BootReasonEnumType,
15068be9 30 type ChargingProfile,
268a74bb 31 ChargingProfileKindType,
15068be9
JB
32 ChargingRateUnitType,
33 type ChargingSchedulePeriod,
268a74bb
JB
34 type ChargingStationInfo,
35 type ChargingStationTemplate,
b1f1b0f6 36 ChargingStationWorkerMessageEvents,
dd08d43d 37 ConnectorPhaseRotation,
a78ef5ed 38 type ConnectorStatus,
c3b83130 39 ConnectorStatusEnum,
268a74bb 40 CurrentType,
ae25f265 41 type EvseTemplate,
268a74bb
JB
42 type OCPP16BootNotificationRequest,
43 type OCPP20BootNotificationRequest,
44 OCPPVersion,
45 RecurrencyKindType,
90aceaf6
JB
46 type Reservation,
47 ReservationTerminationReason,
d8093be1
JB
48 StandardParametersKey,
49 SupportedFeatureProfiles,
268a74bb
JB
50 Voltage,
51} from '../types';
9bf0ef23
JB
52import {
53 ACElectricUtils,
54 Constants,
55 DCElectricUtils,
56 cloneObject,
b85cef4c 57 convertToDate,
9bf0ef23 58 convertToInt,
80c58041 59 isArraySorted,
9bf0ef23
JB
60 isEmptyObject,
61 isEmptyString,
62 isNotEmptyArray,
63 isNotEmptyString,
64 isNullOrUndefined,
65 isUndefined,
0bd926c1 66 isValidTime,
9bf0ef23
JB
67 logger,
68 secureRandom,
69} from '../utils';
17ac262c 70
08b58f00 71const moduleName = 'Helpers';
91a4f151 72
fba11dc6
JB
73export const getChargingStationId = (
74 index: number,
5edd8ba0 75 stationTemplate: ChargingStationTemplate,
fba11dc6
JB
76): string => {
77 // In case of multiple instances: add instance index to charging station id
78 const instanceIndex = process.env.CF_INSTANCE_INDEX ?? 0;
79 const idSuffix = stationTemplate?.nameSuffix ?? '';
80 const idStr = `000000000${index.toString()}`;
81 return stationTemplate?.fixedName
82 ? stationTemplate.baseName
83 : `${stationTemplate.baseName}-${instanceIndex.toString()}${idStr.substring(
5edd8ba0 84 idStr.length - 4,
fba11dc6
JB
85 )}${idSuffix}`;
86};
87
90aceaf6
JB
88export const hasReservationExpired = (reservation: Reservation): boolean => {
89 return isPast(reservation.expiryDate);
90};
91
92export const removeExpiredReservations = async (
93 chargingStation: ChargingStation,
94): Promise<void> => {
95 if (chargingStation.hasEvses) {
96 for (const evseStatus of chargingStation.evses.values()) {
97 for (const connectorStatus of evseStatus.connectors.values()) {
98 if (connectorStatus.reservation && hasReservationExpired(connectorStatus.reservation)) {
99 await chargingStation.removeReservation(
100 connectorStatus.reservation,
101 ReservationTerminationReason.EXPIRED,
102 );
103 }
104 }
105 }
106 } else {
107 for (const connectorStatus of chargingStation.connectors.values()) {
108 if (connectorStatus.reservation && hasReservationExpired(connectorStatus.reservation)) {
109 await chargingStation.removeReservation(
110 connectorStatus.reservation,
111 ReservationTerminationReason.EXPIRED,
112 );
113 }
114 }
115 }
116};
117
118export const getNumberOfReservableConnectors = (
119 connectors: Map<number, ConnectorStatus>,
120): number => {
fba11dc6
JB
121 let reservableConnectors = 0;
122 for (const [connectorId, connectorStatus] of connectors) {
123 if (connectorId === 0) {
124 continue;
3fa7f799 125 }
fba11dc6
JB
126 if (connectorStatus.status === ConnectorStatusEnum.Available) {
127 ++reservableConnectors;
1bf29f5b 128 }
1bf29f5b 129 }
fba11dc6
JB
130 return reservableConnectors;
131};
132
133export const getHashId = (index: number, stationTemplate: ChargingStationTemplate): string => {
134 const chargingStationInfo = {
135 chargePointModel: stationTemplate.chargePointModel,
136 chargePointVendor: stationTemplate.chargePointVendor,
137 ...(!isUndefined(stationTemplate.chargeBoxSerialNumberPrefix) && {
138 chargeBoxSerialNumber: stationTemplate.chargeBoxSerialNumberPrefix,
139 }),
140 ...(!isUndefined(stationTemplate.chargePointSerialNumberPrefix) && {
141 chargePointSerialNumber: stationTemplate.chargePointSerialNumberPrefix,
142 }),
143 ...(!isUndefined(stationTemplate.meterSerialNumberPrefix) && {
144 meterSerialNumber: stationTemplate.meterSerialNumberPrefix,
145 }),
146 ...(!isUndefined(stationTemplate.meterType) && {
147 meterType: stationTemplate.meterType,
148 }),
149 };
150 return createHash(Constants.DEFAULT_HASH_ALGORITHM)
151 .update(`${JSON.stringify(chargingStationInfo)}${getChargingStationId(index, stationTemplate)}`)
152 .digest('hex');
153};
154
155export const checkChargingStation = (
156 chargingStation: ChargingStation,
5edd8ba0 157 logPrefix: string,
fba11dc6
JB
158): boolean => {
159 if (chargingStation.started === false && chargingStation.starting === false) {
160 logger.warn(`${logPrefix} charging station is stopped, cannot proceed`);
161 return false;
162 }
163 return true;
164};
165
166export const getPhaseRotationValue = (
167 connectorId: number,
5edd8ba0 168 numberOfPhases: number,
fba11dc6
JB
169): string | undefined => {
170 // AC/DC
171 if (connectorId === 0 && numberOfPhases === 0) {
172 return `${connectorId}.${ConnectorPhaseRotation.RST}`;
173 } else if (connectorId > 0 && numberOfPhases === 0) {
174 return `${connectorId}.${ConnectorPhaseRotation.NotApplicable}`;
175 // AC
176 } else if (connectorId > 0 && numberOfPhases === 1) {
177 return `${connectorId}.${ConnectorPhaseRotation.NotApplicable}`;
178 } else if (connectorId > 0 && numberOfPhases === 3) {
179 return `${connectorId}.${ConnectorPhaseRotation.RST}`;
180 }
181};
182
183export const getMaxNumberOfEvses = (evses: Record<string, EvseTemplate>): number => {
184 if (!evses) {
185 return -1;
186 }
187 return Object.keys(evses).length;
188};
189
190const getMaxNumberOfConnectors = (connectors: Record<string, ConnectorStatus>): number => {
191 if (!connectors) {
192 return -1;
193 }
194 return Object.keys(connectors).length;
195};
196
197export const getBootConnectorStatus = (
198 chargingStation: ChargingStation,
199 connectorId: number,
5edd8ba0 200 connectorStatus: ConnectorStatus,
fba11dc6
JB
201): ConnectorStatusEnum => {
202 let connectorBootStatus: ConnectorStatusEnum;
203 if (
204 !connectorStatus?.status &&
205 (chargingStation.isChargingStationAvailable() === false ||
206 chargingStation.isConnectorAvailable(connectorId) === false)
207 ) {
208 connectorBootStatus = ConnectorStatusEnum.Unavailable;
209 } else if (!connectorStatus?.status && connectorStatus?.bootStatus) {
210 // Set boot status in template at startup
211 connectorBootStatus = connectorStatus?.bootStatus;
212 } else if (connectorStatus?.status) {
213 // Set previous status at startup
214 connectorBootStatus = connectorStatus?.status;
215 } else {
216 // Set default status
217 connectorBootStatus = ConnectorStatusEnum.Available;
218 }
219 return connectorBootStatus;
220};
221
222export const checkTemplate = (
223 stationTemplate: ChargingStationTemplate,
224 logPrefix: string,
5edd8ba0 225 templateFile: string,
fba11dc6
JB
226): void => {
227 if (isNullOrUndefined(stationTemplate)) {
228 const errorMsg = `Failed to read charging station template file ${templateFile}`;
229 logger.error(`${logPrefix} ${errorMsg}`);
230 throw new BaseError(errorMsg);
231 }
232 if (isEmptyObject(stationTemplate)) {
233 const errorMsg = `Empty charging station information from template file ${templateFile}`;
234 logger.error(`${logPrefix} ${errorMsg}`);
235 throw new BaseError(errorMsg);
236 }
e1d9a0f4 237 if (isEmptyObject(stationTemplate.AutomaticTransactionGenerator!)) {
fba11dc6
JB
238 stationTemplate.AutomaticTransactionGenerator = Constants.DEFAULT_ATG_CONFIGURATION;
239 logger.warn(
240 `${logPrefix} Empty automatic transaction generator configuration from template file ${templateFile}, set to default: %j`,
5edd8ba0 241 Constants.DEFAULT_ATG_CONFIGURATION,
fba11dc6 242 );
fa7bccf4 243 }
fba11dc6
JB
244 if (isNullOrUndefined(stationTemplate.idTagsFile) || isEmptyString(stationTemplate.idTagsFile)) {
245 logger.warn(
5edd8ba0 246 `${logPrefix} Missing id tags file in template file ${templateFile}. That can lead to issues with the Automatic Transaction Generator`,
fba11dc6 247 );
c3b83130 248 }
fba11dc6
JB
249};
250
251export const checkConnectorsConfiguration = (
252 stationTemplate: ChargingStationTemplate,
253 logPrefix: string,
5edd8ba0 254 templateFile: string,
fba11dc6
JB
255): {
256 configuredMaxConnectors: number;
257 templateMaxConnectors: number;
258 templateMaxAvailableConnectors: number;
259} => {
260 const configuredMaxConnectors = getConfiguredNumberOfConnectors(stationTemplate);
261 checkConfiguredMaxConnectors(configuredMaxConnectors, logPrefix, templateFile);
e1d9a0f4 262 const templateMaxConnectors = getMaxNumberOfConnectors(stationTemplate.Connectors!);
fba11dc6 263 checkTemplateMaxConnectors(templateMaxConnectors, logPrefix, templateFile);
e1d9a0f4 264 const templateMaxAvailableConnectors = stationTemplate.Connectors![0]
fba11dc6
JB
265 ? templateMaxConnectors - 1
266 : templateMaxConnectors;
267 if (
268 configuredMaxConnectors > templateMaxAvailableConnectors &&
269 !stationTemplate?.randomConnectors
8a133cc8 270 ) {
fba11dc6 271 logger.warn(
5edd8ba0 272 `${logPrefix} Number of connectors exceeds the number of connector configurations in template ${templateFile}, forcing random connector configurations affectation`,
cda5d0fb 273 );
fba11dc6
JB
274 stationTemplate.randomConnectors = true;
275 }
276 return { configuredMaxConnectors, templateMaxConnectors, templateMaxAvailableConnectors };
277};
278
279export const checkStationInfoConnectorStatus = (
280 connectorId: number,
281 connectorStatus: ConnectorStatus,
282 logPrefix: string,
5edd8ba0 283 templateFile: string,
fba11dc6
JB
284): void => {
285 if (!isNullOrUndefined(connectorStatus?.status)) {
286 logger.warn(
5edd8ba0 287 `${logPrefix} Charging station information from template ${templateFile} with connector id ${connectorId} status configuration defined, undefine it`,
cda5d0fb 288 );
fba11dc6
JB
289 delete connectorStatus.status;
290 }
291};
292
293export const buildConnectorsMap = (
294 connectors: Record<string, ConnectorStatus>,
295 logPrefix: string,
5edd8ba0 296 templateFile: string,
fba11dc6
JB
297): Map<number, ConnectorStatus> => {
298 const connectorsMap = new Map<number, ConnectorStatus>();
299 if (getMaxNumberOfConnectors(connectors) > 0) {
300 for (const connector in connectors) {
301 const connectorStatus = connectors[connector];
302 const connectorId = convertToInt(connector);
303 checkStationInfoConnectorStatus(connectorId, connectorStatus, logPrefix, templateFile);
304 connectorsMap.set(connectorId, cloneObject<ConnectorStatus>(connectorStatus));
fa7bccf4 305 }
fba11dc6
JB
306 } else {
307 logger.warn(
5edd8ba0 308 `${logPrefix} Charging station information from template ${templateFile} with no connectors, cannot build connectors map`,
fba11dc6 309 );
fa7bccf4 310 }
fba11dc6
JB
311 return connectorsMap;
312};
fa7bccf4 313
fba11dc6
JB
314export const initializeConnectorsMapStatus = (
315 connectors: Map<number, ConnectorStatus>,
5edd8ba0 316 logPrefix: string,
fba11dc6
JB
317): void => {
318 for (const connectorId of connectors.keys()) {
319 if (connectorId > 0 && connectors.get(connectorId)?.transactionStarted === true) {
04b1261c 320 logger.warn(
5edd8ba0
JB
321 `${logPrefix} Connector id ${connectorId} at initialization has a transaction started with id ${connectors.get(
322 connectorId,
323 )?.transactionId}`,
04b1261c 324 );
04b1261c 325 }
fba11dc6 326 if (connectorId === 0) {
e1d9a0f4 327 connectors.get(connectorId)!.availability = AvailabilityType.Operative;
fba11dc6 328 if (isUndefined(connectors.get(connectorId)?.chargingProfiles)) {
e1d9a0f4 329 connectors.get(connectorId)!.chargingProfiles = [];
04b1261c 330 }
fba11dc6
JB
331 } else if (
332 connectorId > 0 &&
333 isNullOrUndefined(connectors.get(connectorId)?.transactionStarted)
334 ) {
e1d9a0f4 335 initializeConnectorStatus(connectors.get(connectorId)!);
04b1261c
JB
336 }
337 }
fba11dc6
JB
338};
339
340export const resetConnectorStatus = (connectorStatus: ConnectorStatus): void => {
341 connectorStatus.idTagLocalAuthorized = false;
342 connectorStatus.idTagAuthorized = false;
343 connectorStatus.transactionRemoteStarted = false;
344 connectorStatus.transactionStarted = false;
a71d4e70
JB
345 delete connectorStatus?.transactionStart;
346 delete connectorStatus?.transactionId;
fba11dc6
JB
347 delete connectorStatus?.localAuthorizeIdTag;
348 delete connectorStatus?.authorizeIdTag;
fba11dc6
JB
349 delete connectorStatus?.transactionIdTag;
350 connectorStatus.transactionEnergyActiveImportRegisterValue = 0;
351 delete connectorStatus?.transactionBeginMeterValue;
352};
353
354export const createBootNotificationRequest = (
355 stationInfo: ChargingStationInfo,
5edd8ba0 356 bootReason: BootReasonEnumType = BootReasonEnumType.PowerUp,
fba11dc6
JB
357): BootNotificationRequest => {
358 const ocppVersion = stationInfo.ocppVersion ?? OCPPVersion.VERSION_16;
359 switch (ocppVersion) {
360 case OCPPVersion.VERSION_16:
361 return {
362 chargePointModel: stationInfo.chargePointModel,
363 chargePointVendor: stationInfo.chargePointVendor,
364 ...(!isUndefined(stationInfo.chargeBoxSerialNumber) && {
365 chargeBoxSerialNumber: stationInfo.chargeBoxSerialNumber,
366 }),
367 ...(!isUndefined(stationInfo.chargePointSerialNumber) && {
368 chargePointSerialNumber: stationInfo.chargePointSerialNumber,
369 }),
370 ...(!isUndefined(stationInfo.firmwareVersion) && {
371 firmwareVersion: stationInfo.firmwareVersion,
372 }),
373 ...(!isUndefined(stationInfo.iccid) && { iccid: stationInfo.iccid }),
374 ...(!isUndefined(stationInfo.imsi) && { imsi: stationInfo.imsi }),
375 ...(!isUndefined(stationInfo.meterSerialNumber) && {
376 meterSerialNumber: stationInfo.meterSerialNumber,
377 }),
378 ...(!isUndefined(stationInfo.meterType) && {
379 meterType: stationInfo.meterType,
380 }),
381 } as OCPP16BootNotificationRequest;
382 case OCPPVersion.VERSION_20:
383 case OCPPVersion.VERSION_201:
384 return {
385 reason: bootReason,
386 chargingStation: {
387 model: stationInfo.chargePointModel,
388 vendorName: stationInfo.chargePointVendor,
9bf0ef23 389 ...(!isUndefined(stationInfo.firmwareVersion) && {
d270cc87
JB
390 firmwareVersion: stationInfo.firmwareVersion,
391 }),
fba11dc6
JB
392 ...(!isUndefined(stationInfo.chargeBoxSerialNumber) && {
393 serialNumber: stationInfo.chargeBoxSerialNumber,
d270cc87 394 }),
fba11dc6
JB
395 ...((!isUndefined(stationInfo.iccid) || !isUndefined(stationInfo.imsi)) && {
396 modem: {
397 ...(!isUndefined(stationInfo.iccid) && { iccid: stationInfo.iccid }),
398 ...(!isUndefined(stationInfo.imsi) && { imsi: stationInfo.imsi }),
399 },
d270cc87 400 }),
fba11dc6
JB
401 },
402 } as OCPP20BootNotificationRequest;
403 }
404};
405
406export const warnTemplateKeysDeprecation = (
407 stationTemplate: ChargingStationTemplate,
408 logPrefix: string,
5edd8ba0 409 templateFile: string,
fba11dc6 410) => {
e4c6cf05
JB
411 const templateKeys: { deprecatedKey: string; key?: string }[] = [
412 { deprecatedKey: 'supervisionUrl', key: 'supervisionUrls' },
413 { deprecatedKey: 'authorizationFile', key: 'idTagsFile' },
414 { deprecatedKey: 'payloadSchemaValidation', key: 'ocppStrictCompliance' },
cfdf901d 415 { deprecatedKey: 'mustAuthorizeAtRemoteStart', key: 'remoteAuthorization' },
fba11dc6
JB
416 ];
417 for (const templateKey of templateKeys) {
418 warnDeprecatedTemplateKey(
419 stationTemplate,
420 templateKey.deprecatedKey,
421 logPrefix,
422 templateFile,
e1d9a0f4 423 !isUndefined(templateKey.key) ? `Use '${templateKey.key}' instead` : undefined,
fba11dc6
JB
424 );
425 convertDeprecatedTemplateKey(stationTemplate, templateKey.deprecatedKey, templateKey.key);
426 }
427};
428
429export const stationTemplateToStationInfo = (
5edd8ba0 430 stationTemplate: ChargingStationTemplate,
fba11dc6
JB
431): ChargingStationInfo => {
432 stationTemplate = cloneObject<ChargingStationTemplate>(stationTemplate);
433 delete stationTemplate.power;
434 delete stationTemplate.powerUnit;
e1d9a0f4
JB
435 delete stationTemplate.Connectors;
436 delete stationTemplate.Evses;
fba11dc6
JB
437 delete stationTemplate.Configuration;
438 delete stationTemplate.AutomaticTransactionGenerator;
439 delete stationTemplate.chargeBoxSerialNumberPrefix;
440 delete stationTemplate.chargePointSerialNumberPrefix;
441 delete stationTemplate.meterSerialNumberPrefix;
66b537dc 442 return stationTemplate as ChargingStationInfo;
fba11dc6
JB
443};
444
445export const createSerialNumber = (
446 stationTemplate: ChargingStationTemplate,
447 stationInfo: ChargingStationInfo,
448 params: {
449 randomSerialNumberUpperCase?: boolean;
450 randomSerialNumber?: boolean;
451 } = {
452 randomSerialNumberUpperCase: true,
453 randomSerialNumber: true,
5edd8ba0 454 },
fba11dc6
JB
455): void => {
456 params = { ...{ randomSerialNumberUpperCase: true, randomSerialNumber: true }, ...params };
457 const serialNumberSuffix = params?.randomSerialNumber
458 ? getRandomSerialNumberSuffix({
459 upperCase: params.randomSerialNumberUpperCase,
460 })
461 : '';
462 isNotEmptyString(stationTemplate?.chargePointSerialNumberPrefix) &&
463 (stationInfo.chargePointSerialNumber = `${stationTemplate.chargePointSerialNumberPrefix}${serialNumberSuffix}`);
464 isNotEmptyString(stationTemplate?.chargeBoxSerialNumberPrefix) &&
465 (stationInfo.chargeBoxSerialNumber = `${stationTemplate.chargeBoxSerialNumberPrefix}${serialNumberSuffix}`);
466 isNotEmptyString(stationTemplate?.meterSerialNumberPrefix) &&
467 (stationInfo.meterSerialNumber = `${stationTemplate.meterSerialNumberPrefix}${serialNumberSuffix}`);
468};
469
470export const propagateSerialNumber = (
471 stationTemplate: ChargingStationTemplate,
472 stationInfoSrc: ChargingStationInfo,
5edd8ba0 473 stationInfoDst: ChargingStationInfo,
fba11dc6
JB
474) => {
475 if (!stationInfoSrc || !stationTemplate) {
476 throw new BaseError(
5edd8ba0 477 'Missing charging station template or existing configuration to propagate serial number',
fba11dc6 478 );
17ac262c 479 }
fba11dc6
JB
480 stationTemplate?.chargePointSerialNumberPrefix && stationInfoSrc?.chargePointSerialNumber
481 ? (stationInfoDst.chargePointSerialNumber = stationInfoSrc.chargePointSerialNumber)
482 : stationInfoDst?.chargePointSerialNumber && delete stationInfoDst.chargePointSerialNumber;
483 stationTemplate?.chargeBoxSerialNumberPrefix && stationInfoSrc?.chargeBoxSerialNumber
484 ? (stationInfoDst.chargeBoxSerialNumber = stationInfoSrc.chargeBoxSerialNumber)
485 : stationInfoDst?.chargeBoxSerialNumber && delete stationInfoDst.chargeBoxSerialNumber;
486 stationTemplate?.meterSerialNumberPrefix && stationInfoSrc?.meterSerialNumber
487 ? (stationInfoDst.meterSerialNumber = stationInfoSrc.meterSerialNumber)
488 : stationInfoDst?.meterSerialNumber && delete stationInfoDst.meterSerialNumber;
489};
490
d8093be1
JB
491export const hasFeatureProfile = (
492 chargingStation: ChargingStation,
493 featureProfile: SupportedFeatureProfiles,
494): boolean | undefined => {
495 return getConfigurationKey(
496 chargingStation,
497 StandardParametersKey.SupportedFeatureProfiles,
498 )?.value?.includes(featureProfile);
499};
500
fba11dc6
JB
501export const getAmperageLimitationUnitDivider = (stationInfo: ChargingStationInfo): number => {
502 let unitDivider = 1;
503 switch (stationInfo.amperageLimitationUnit) {
504 case AmpereUnits.DECI_AMPERE:
505 unitDivider = 10;
506 break;
507 case AmpereUnits.CENTI_AMPERE:
508 unitDivider = 100;
509 break;
510 case AmpereUnits.MILLI_AMPERE:
511 unitDivider = 1000;
512 break;
513 }
514 return unitDivider;
515};
516
517export const getChargingStationConnectorChargingProfilesPowerLimit = (
518 chargingStation: ChargingStation,
5edd8ba0 519 connectorId: number,
fba11dc6 520): number | undefined => {
e1d9a0f4 521 let limit: number | undefined, matchingChargingProfile: ChargingProfile | undefined;
252a7d22 522 // Get charging profiles for connector id and sort by stack level
fba11dc6
JB
523 const chargingProfiles =
524 cloneObject<ChargingProfile[]>(
e1d9a0f4 525 chargingStation.getConnectorStatus(connectorId)!.chargingProfiles!,
fba11dc6 526 )?.sort((a, b) => b.stackLevel - a.stackLevel) ?? [];
252a7d22 527 // Get charging profiles on connector 0 and sort by stack level
710d50eb 528 if (isNotEmptyArray(chargingStation.getConnectorStatus(0)?.chargingProfiles)) {
fba11dc6
JB
529 chargingProfiles.push(
530 ...cloneObject<ChargingProfile[]>(
e1d9a0f4 531 chargingStation.getConnectorStatus(0)!.chargingProfiles!,
5edd8ba0 532 ).sort((a, b) => b.stackLevel - a.stackLevel),
fba11dc6 533 );
17ac262c 534 }
fba11dc6 535 if (isNotEmptyArray(chargingProfiles)) {
a71d4e70
JB
536 const result = getLimitFromChargingProfiles(
537 chargingStation,
538 connectorId,
539 chargingProfiles,
540 chargingStation.logPrefix(),
541 );
fba11dc6
JB
542 if (!isNullOrUndefined(result)) {
543 limit = result?.limit;
544 matchingChargingProfile = result?.matchingChargingProfile;
545 switch (chargingStation.getCurrentOutType()) {
546 case CurrentType.AC:
547 limit =
e1d9a0f4
JB
548 matchingChargingProfile?.chargingSchedule?.chargingRateUnit ===
549 ChargingRateUnitType.WATT
fba11dc6
JB
550 ? limit
551 : ACElectricUtils.powerTotal(
552 chargingStation.getNumberOfPhases(),
553 chargingStation.getVoltageOut(),
e1d9a0f4 554 limit!,
fba11dc6
JB
555 );
556 break;
557 case CurrentType.DC:
558 limit =
e1d9a0f4
JB
559 matchingChargingProfile?.chargingSchedule?.chargingRateUnit ===
560 ChargingRateUnitType.WATT
fba11dc6 561 ? limit
e1d9a0f4 562 : DCElectricUtils.power(chargingStation.getVoltageOut(), limit!);
fba11dc6
JB
563 }
564 const connectorMaximumPower =
565 chargingStation.getMaximumPower() / chargingStation.powerDivider;
e1d9a0f4 566 if (limit! > connectorMaximumPower) {
fba11dc6 567 logger.error(
aa5c5ad4 568 `${chargingStation.logPrefix()} ${moduleName}.getChargingStationConnectorChargingProfilesPowerLimit: Charging profile id ${matchingChargingProfile?.chargingProfileId} limit ${limit} is greater than connector id ${connectorId} maximum ${connectorMaximumPower}: %j`,
5edd8ba0 569 result,
fba11dc6
JB
570 );
571 limit = connectorMaximumPower;
15068be9
JB
572 }
573 }
15068be9 574 }
fba11dc6
JB
575 return limit;
576};
577
578export const getDefaultVoltageOut = (
579 currentType: CurrentType,
580 logPrefix: string,
5edd8ba0 581 templateFile: string,
fba11dc6
JB
582): Voltage => {
583 const errorMsg = `Unknown ${currentType} currentOutType in template file ${templateFile}, cannot define default voltage out`;
584 let defaultVoltageOut: number;
585 switch (currentType) {
586 case CurrentType.AC:
587 defaultVoltageOut = Voltage.VOLTAGE_230;
588 break;
589 case CurrentType.DC:
590 defaultVoltageOut = Voltage.VOLTAGE_400;
591 break;
592 default:
593 logger.error(`${logPrefix} ${errorMsg}`);
594 throw new BaseError(errorMsg);
15068be9 595 }
fba11dc6
JB
596 return defaultVoltageOut;
597};
598
599export const getIdTagsFile = (stationInfo: ChargingStationInfo): string | undefined => {
600 return (
601 stationInfo.idTagsFile &&
602 join(dirname(fileURLToPath(import.meta.url)), 'assets', basename(stationInfo.idTagsFile))
603 );
604};
605
b2b60626 606export const waitChargingStationEvents = async (
fba11dc6
JB
607 emitter: EventEmitter,
608 event: ChargingStationWorkerMessageEvents,
5edd8ba0 609 eventsToWait: number,
fba11dc6 610): Promise<number> => {
474d4ffc 611 return new Promise<number>((resolve) => {
fba11dc6
JB
612 let events = 0;
613 if (eventsToWait === 0) {
614 resolve(events);
615 }
616 emitter.on(event, () => {
617 ++events;
618 if (events === eventsToWait) {
b1f1b0f6
JB
619 resolve(events);
620 }
b1f1b0f6 621 });
fba11dc6
JB
622 });
623};
624
625const getConfiguredNumberOfConnectors = (stationTemplate: ChargingStationTemplate): number => {
e1d9a0f4 626 let configuredMaxConnectors = 0;
fba11dc6
JB
627 if (isNotEmptyArray(stationTemplate.numberOfConnectors) === true) {
628 const numberOfConnectors = stationTemplate.numberOfConnectors as number[];
629 configuredMaxConnectors =
630 numberOfConnectors[Math.floor(secureRandom() * numberOfConnectors.length)];
631 } else if (isUndefined(stationTemplate.numberOfConnectors) === false) {
632 configuredMaxConnectors = stationTemplate.numberOfConnectors as number;
633 } else if (stationTemplate.Connectors && !stationTemplate.Evses) {
e1d9a0f4 634 configuredMaxConnectors = stationTemplate.Connectors[0]
fba11dc6
JB
635 ? getMaxNumberOfConnectors(stationTemplate.Connectors) - 1
636 : getMaxNumberOfConnectors(stationTemplate.Connectors);
637 } else if (stationTemplate.Evses && !stationTemplate.Connectors) {
fba11dc6
JB
638 for (const evse in stationTemplate.Evses) {
639 if (evse === '0') {
640 continue;
cda5d0fb 641 }
fba11dc6 642 configuredMaxConnectors += getMaxNumberOfConnectors(stationTemplate.Evses[evse].Connectors);
cda5d0fb 643 }
cda5d0fb 644 }
fba11dc6
JB
645 return configuredMaxConnectors;
646};
647
648const checkConfiguredMaxConnectors = (
649 configuredMaxConnectors: number,
650 logPrefix: string,
5edd8ba0 651 templateFile: string,
fba11dc6
JB
652): void => {
653 if (configuredMaxConnectors <= 0) {
654 logger.warn(
5edd8ba0 655 `${logPrefix} Charging station information from template ${templateFile} with ${configuredMaxConnectors} connectors`,
fba11dc6 656 );
cda5d0fb 657 }
fba11dc6 658};
cda5d0fb 659
fba11dc6
JB
660const checkTemplateMaxConnectors = (
661 templateMaxConnectors: number,
662 logPrefix: string,
5edd8ba0 663 templateFile: string,
fba11dc6
JB
664): void => {
665 if (templateMaxConnectors === 0) {
666 logger.warn(
5edd8ba0 667 `${logPrefix} Charging station information from template ${templateFile} with empty connectors configuration`,
fba11dc6
JB
668 );
669 } else if (templateMaxConnectors < 0) {
670 logger.error(
5edd8ba0 671 `${logPrefix} Charging station information from template ${templateFile} with no connectors configuration defined`,
fba11dc6
JB
672 );
673 }
674};
675
676const initializeConnectorStatus = (connectorStatus: ConnectorStatus): void => {
677 connectorStatus.availability = AvailabilityType.Operative;
678 connectorStatus.idTagLocalAuthorized = false;
679 connectorStatus.idTagAuthorized = false;
680 connectorStatus.transactionRemoteStarted = false;
681 connectorStatus.transactionStarted = false;
682 connectorStatus.energyActiveImportRegisterValue = 0;
683 connectorStatus.transactionEnergyActiveImportRegisterValue = 0;
684 if (isUndefined(connectorStatus.chargingProfiles)) {
685 connectorStatus.chargingProfiles = [];
686 }
687};
688
689const warnDeprecatedTemplateKey = (
690 template: ChargingStationTemplate,
691 key: string,
692 logPrefix: string,
693 templateFile: string,
5edd8ba0 694 logMsgToAppend = '',
fba11dc6 695): void => {
a37fc6dc 696 if (!isUndefined(template[key as keyof ChargingStationTemplate])) {
fba11dc6
JB
697 const logMsg = `Deprecated template key '${key}' usage in file '${templateFile}'${
698 isNotEmptyString(logMsgToAppend) ? `. ${logMsgToAppend}` : ''
699 }`;
700 logger.warn(`${logPrefix} ${logMsg}`);
701 console.warn(chalk.yellow(`${logMsg}`));
702 }
703};
704
705const convertDeprecatedTemplateKey = (
706 template: ChargingStationTemplate,
707 deprecatedKey: string,
e1d9a0f4 708 key?: string,
fba11dc6 709): void => {
a37fc6dc 710 if (!isUndefined(template[deprecatedKey as keyof ChargingStationTemplate])) {
e1d9a0f4 711 if (!isUndefined(key)) {
a37fc6dc
JB
712 (template as unknown as Record<string, unknown>)[key!] =
713 template[deprecatedKey as keyof ChargingStationTemplate];
e1d9a0f4 714 }
a37fc6dc 715 delete template[deprecatedKey as keyof ChargingStationTemplate];
fba11dc6
JB
716 }
717};
718
947f048a
JB
719interface ChargingProfilesLimit {
720 limit: number;
721 matchingChargingProfile: ChargingProfile;
722}
723
fba11dc6 724/**
d467756c 725 * Charging profiles shall already be sorted by connector id and stack level (highest stack level has priority)
fba11dc6 726 *
d467756c
JB
727 * @param chargingStation -
728 * @param connectorId -
fba11dc6
JB
729 * @param chargingProfiles -
730 * @param logPrefix -
947f048a 731 * @returns ChargingProfilesLimit
fba11dc6
JB
732 */
733const getLimitFromChargingProfiles = (
a71d4e70
JB
734 chargingStation: ChargingStation,
735 connectorId: number,
fba11dc6 736 chargingProfiles: ChargingProfile[],
5edd8ba0 737 logPrefix: string,
947f048a 738): ChargingProfilesLimit | undefined => {
fba11dc6 739 const debugLogMsg = `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Matching charging profile found for power limitation: %j`;
fba11dc6 740 const currentDate = new Date();
b5c19509 741 const connectorStatus = chargingStation.getConnectorStatus(connectorId);
fba11dc6 742 for (const chargingProfile of chargingProfiles) {
fba11dc6 743 const chargingSchedule = chargingProfile.chargingSchedule;
5543b88d 744 if (connectorStatus?.transactionStarted && isNullOrUndefined(chargingSchedule?.startSchedule)) {
109c677a 745 logger.debug(
ec4a242a 746 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} has no startSchedule defined. Trying to set it to the connector current transaction start date`,
cda5d0fb 747 );
a71d4e70 748 // OCPP specifies that if startSchedule is not defined, it should be relative to start of the connector transaction
b5c19509 749 chargingSchedule.startSchedule = connectorStatus?.transactionStart;
52952bf8 750 }
0bd926c1 751 if (!isDate(chargingSchedule?.startSchedule)) {
8d75a403 752 logger.warn(
ec4a242a 753 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} startSchedule property is not a Date object. Trying to convert it to a Date object`,
8d75a403 754 );
991fb26b 755 chargingSchedule.startSchedule = convertToDate(chargingSchedule?.startSchedule)!;
8d75a403 756 }
0bd926c1
JB
757 switch (chargingProfile.chargingProfileKind) {
758 case ChargingProfileKindType.RECURRING:
759 if (!canProceedRecurringChargingProfile(chargingProfile, logPrefix)) {
760 continue;
761 }
762 prepareRecurringChargingProfile(chargingProfile, currentDate, logPrefix);
763 break;
764 case ChargingProfileKindType.RELATIVE:
765 connectorStatus?.transactionStarted &&
766 (chargingSchedule.startSchedule = connectorStatus?.transactionStart);
767 break;
ec4a242a 768 }
0bd926c1 769 if (!canProceedChargingProfile(chargingProfile, currentDate, logPrefix)) {
142a66c9
JB
770 continue;
771 }
fba11dc6
JB
772 // Check if the charging profile is active
773 if (
0bd926c1 774 isValidTime(chargingSchedule?.startSchedule) &&
975e18ec
JB
775 isWithinInterval(currentDate, {
776 start: chargingSchedule.startSchedule!,
777 end: addSeconds(chargingSchedule.startSchedule!, chargingSchedule.duration!),
778 })
fba11dc6 779 ) {
252a7d22 780 if (isNotEmptyArray(chargingSchedule.chargingSchedulePeriod)) {
80c58041
JB
781 const chargingSchedulePeriodCompareFn = (
782 a: ChargingSchedulePeriod,
783 b: ChargingSchedulePeriod,
784 ) => a.startPeriod - b.startPeriod;
785 if (
786 isArraySorted<ChargingSchedulePeriod>(
787 chargingSchedule.chargingSchedulePeriod,
788 chargingSchedulePeriodCompareFn,
789 ) === false
790 ) {
791 logger.warn(
792 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} schedule periods are not sorted by start period`,
793 );
794 chargingSchedule.chargingSchedulePeriod.sort(chargingSchedulePeriodCompareFn);
795 }
975e18ec 796 // Check if the first schedule period start period is equal to 0
55f2ab60
JB
797 if (chargingSchedule.chargingSchedulePeriod[0].startPeriod !== 0) {
798 logger.error(
799 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} first schedule period start period ${chargingSchedule.chargingSchedulePeriod[0].startPeriod} is not equal to 0`,
800 );
975e18ec 801 continue;
55f2ab60 802 }
991fb26b 803 // Handle only one schedule period
975e18ec 804 if (chargingSchedule.chargingSchedulePeriod.length === 1) {
252a7d22
JB
805 const result: ChargingProfilesLimit = {
806 limit: chargingSchedule.chargingSchedulePeriod[0].limit,
fba11dc6
JB
807 matchingChargingProfile: chargingProfile,
808 };
809 logger.debug(debugLogMsg, result);
810 return result;
41189456 811 }
e3037969 812 let previousChargingSchedulePeriod: ChargingSchedulePeriod | undefined;
252a7d22 813 // Search for the right schedule period
e3037969
JB
814 for (const [
815 index,
816 chargingSchedulePeriod,
817 ] of chargingSchedule.chargingSchedulePeriod.entries()) {
252a7d22
JB
818 // Find the right schedule period
819 if (
820 isAfter(
e3037969 821 addSeconds(chargingSchedule.startSchedule!, chargingSchedulePeriod.startPeriod),
252a7d22
JB
822 currentDate,
823 )
824 ) {
e3037969 825 // Found the schedule period: previous is the correct one
252a7d22 826 const result: ChargingProfilesLimit = {
e3037969 827 limit: previousChargingSchedulePeriod!.limit,
252a7d22
JB
828 matchingChargingProfile: chargingProfile,
829 };
830 logger.debug(debugLogMsg, result);
831 return result;
832 }
e3037969
JB
833 // Keep a reference to previous one
834 previousChargingSchedulePeriod = chargingSchedulePeriod;
975e18ec 835 // Handle the last schedule period within the charging profile duration
252a7d22 836 if (
975e18ec
JB
837 index === chargingSchedule.chargingSchedulePeriod.length - 1 ||
838 (index < chargingSchedule.chargingSchedulePeriod.length - 1 &&
839 chargingSchedule.duration! >
840 differenceInSeconds(
975e18ec
JB
841 addSeconds(
842 chargingSchedule.startSchedule!,
843 chargingSchedule.chargingSchedulePeriod[index + 1].startPeriod,
844 ),
d9dc6292 845 chargingSchedule.startSchedule!,
975e18ec 846 ))
252a7d22
JB
847 ) {
848 const result: ChargingProfilesLimit = {
e3037969 849 limit: previousChargingSchedulePeriod.limit,
252a7d22
JB
850 matchingChargingProfile: chargingProfile,
851 };
852 logger.debug(debugLogMsg, result);
853 return result;
854 }
17ac262c
JB
855 }
856 }
857 }
17ac262c 858 }
fba11dc6 859};
17ac262c 860
0bd926c1
JB
861const canProceedChargingProfile = (
862 chargingProfile: ChargingProfile,
863 currentDate: Date,
864 logPrefix: string,
865): boolean => {
866 if (
867 (isValidTime(chargingProfile.validFrom) && isBefore(currentDate, chargingProfile.validFrom!)) ||
868 (isValidTime(chargingProfile.validTo) && isAfter(currentDate, chargingProfile.validTo!))
869 ) {
870 logger.debug(
871 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${
872 chargingProfile.chargingProfileId
873 } is not valid for the current date ${currentDate.toISOString()}`,
874 );
875 return false;
876 }
877 const chargingSchedule = chargingProfile.chargingSchedule;
878 if (isNullOrUndefined(chargingSchedule?.startSchedule)) {
879 logger.error(
880 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId} has (still) no startSchedule defined`,
881 );
882 return false;
883 }
884 if (isNullOrUndefined(chargingSchedule?.duration)) {
885 logger.error(
886 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId} has no duration defined, not yet supported`,
887 );
888 return false;
889 }
890 return true;
891};
892
893const canProceedRecurringChargingProfile = (
894 chargingProfile: ChargingProfile,
895 logPrefix: string,
896): boolean => {
897 if (
898 chargingProfile.chargingProfileKind === ChargingProfileKindType.RECURRING &&
899 isNullOrUndefined(chargingProfile.recurrencyKind)
900 ) {
901 logger.error(
902 `${logPrefix} ${moduleName}.canProceedRecurringChargingProfile: Recurring charging profile id ${chargingProfile.chargingProfileId} has no recurrencyKind defined`,
903 );
904 return false;
905 }
906 return true;
907};
908
522e4b05 909/**
ec4a242a 910 * Adjust recurring charging profile startSchedule to the current recurrency time interval if needed
522e4b05
JB
911 *
912 * @param chargingProfile -
913 * @param currentDate -
914 * @param logPrefix -
915 */
76dab5a9
JB
916const prepareRecurringChargingProfile = (
917 chargingProfile: ChargingProfile,
918 currentDate: Date,
919 logPrefix: string,
ec4a242a 920): boolean => {
76dab5a9 921 const chargingSchedule = chargingProfile.chargingSchedule;
ec4a242a 922 let recurringIntervalTranslated = false;
522e4b05 923 let recurringInterval: Interval;
76dab5a9
JB
924 switch (chargingProfile.recurrencyKind) {
925 case RecurrencyKindType.DAILY:
522e4b05
JB
926 recurringInterval = {
927 start: chargingSchedule.startSchedule!,
928 end: addDays(chargingSchedule.startSchedule!, 1),
929 };
d476bc1b 930 checkRecurringChargingProfileDuration(chargingProfile, recurringInterval, logPrefix);
522e4b05
JB
931 if (
932 !isWithinInterval(currentDate, recurringInterval) &&
991fb26b 933 isBefore(recurringInterval.end, currentDate)
522e4b05
JB
934 ) {
935 chargingSchedule.startSchedule = addDays(
991fb26b 936 recurringInterval.start,
05b52716 937 differenceInDays(currentDate, recurringInterval.start),
76dab5a9 938 );
522e4b05
JB
939 recurringInterval = {
940 start: chargingSchedule.startSchedule,
941 end: addDays(chargingSchedule.startSchedule, 1),
942 };
ec4a242a 943 recurringIntervalTranslated = true;
76dab5a9
JB
944 }
945 break;
946 case RecurrencyKindType.WEEKLY:
522e4b05
JB
947 recurringInterval = {
948 start: chargingSchedule.startSchedule!,
949 end: addWeeks(chargingSchedule.startSchedule!, 1),
950 };
d476bc1b 951 checkRecurringChargingProfileDuration(chargingProfile, recurringInterval, logPrefix);
522e4b05
JB
952 if (
953 !isWithinInterval(currentDate, recurringInterval) &&
991fb26b 954 isBefore(recurringInterval.end, currentDate)
522e4b05
JB
955 ) {
956 chargingSchedule.startSchedule = addWeeks(
991fb26b 957 recurringInterval.start,
05b52716 958 differenceInWeeks(currentDate, recurringInterval.start),
76dab5a9 959 );
522e4b05
JB
960 recurringInterval = {
961 start: chargingSchedule.startSchedule,
962 end: addWeeks(chargingSchedule.startSchedule, 1),
963 };
ec4a242a 964 recurringIntervalTranslated = true;
76dab5a9
JB
965 }
966 break;
ec4a242a
JB
967 default:
968 logger.error(
969 `${logPrefix} ${moduleName}.prepareRecurringChargingProfile: Recurring charging profile id ${chargingProfile.chargingProfileId} recurrency kind ${chargingProfile.recurrencyKind} is not supported`,
970 );
76dab5a9 971 }
ec4a242a 972 if (recurringIntervalTranslated && !isWithinInterval(currentDate, recurringInterval!)) {
522e4b05 973 logger.error(
aa5c5ad4 974 `${logPrefix} ${moduleName}.prepareRecurringChargingProfile: Recurring ${
522e4b05 975 chargingProfile.recurrencyKind
991fb26b 976 } charging profile id ${chargingProfile.chargingProfileId} recurrency time interval [${toDate(
522e4b05 977 recurringInterval!.start,
991fb26b
JB
978 ).toISOString()}, ${toDate(
979 recurringInterval!.end,
ec4a242a 980 ).toISOString()}] has not been properly translated to current date ${currentDate.toISOString()} `,
522e4b05
JB
981 );
982 }
ec4a242a 983 return recurringIntervalTranslated;
76dab5a9
JB
984};
985
d476bc1b
JB
986const checkRecurringChargingProfileDuration = (
987 chargingProfile: ChargingProfile,
988 interval: Interval,
989 logPrefix: string,
ec4a242a 990): void => {
142a66c9
JB
991 if (isNullOrUndefined(chargingProfile.chargingSchedule.duration)) {
992 logger.warn(
993 `${logPrefix} ${moduleName}.checkRecurringChargingProfileDuration: Recurring ${
994 chargingProfile.chargingProfileKind
995 } charging profile id ${
996 chargingProfile.chargingProfileId
997 } duration is not defined, set it to the recurrency time interval duration ${differenceInSeconds(
998 interval.end,
999 interval.start,
1000 )}`,
1001 );
1002 chargingProfile.chargingSchedule.duration = differenceInSeconds(interval.end, interval.start);
1003 } else if (
d476bc1b
JB
1004 chargingProfile.chargingSchedule.duration! > differenceInSeconds(interval.end, interval.start)
1005 ) {
1006 logger.warn(
aa5c5ad4 1007 `${logPrefix} ${moduleName}.checkRecurringChargingProfileDuration: Recurring ${
d476bc1b
JB
1008 chargingProfile.chargingProfileKind
1009 } charging profile id ${chargingProfile.chargingProfileId} duration ${
1010 chargingProfile.chargingSchedule.duration
710d50eb 1011 } is greater than the recurrency time interval duration ${differenceInSeconds(
d476bc1b
JB
1012 interval.end,
1013 interval.start,
710d50eb 1014 )}`,
d476bc1b 1015 );
55f2ab60 1016 chargingProfile.chargingSchedule.duration = differenceInSeconds(interval.end, interval.start);
d476bc1b
JB
1017 }
1018};
1019
fba11dc6
JB
1020const getRandomSerialNumberSuffix = (params?: {
1021 randomBytesLength?: number;
1022 upperCase?: boolean;
1023}): string => {
1024 const randomSerialNumberSuffix = randomBytes(params?.randomBytesLength ?? 16).toString('hex');
1025 if (params?.upperCase) {
1026 return randomSerialNumberSuffix.toUpperCase();
17ac262c 1027 }
fba11dc6
JB
1028 return randomSerialNumberSuffix;
1029};