fix: ensure all charging station changes are pushed at start transaction
[e-mobility-charging-stations-simulator.git] / src / charging-station / Helpers.ts
1 import { createHash, randomBytes } from 'node:crypto';
2 import type { EventEmitter } from 'node:events';
3 import { basename, dirname, join } from 'node:path';
4 import { fileURLToPath } from 'node:url';
5
6 import chalk from 'chalk';
7 import {
8 addDays,
9 addSeconds,
10 addWeeks,
11 differenceInDays,
12 differenceInSeconds,
13 differenceInWeeks,
14 isAfter,
15 isBefore,
16 isDate,
17 isPast,
18 isWithinInterval,
19 toDate,
20 } from 'date-fns';
21
22 import type { ChargingStation } from './ChargingStation';
23 import { getConfigurationKey } from './ConfigurationKeyUtils';
24 import { BaseError } from '../exception';
25 import {
26 AmpereUnits,
27 AvailabilityType,
28 type BootNotificationRequest,
29 BootReasonEnumType,
30 type ChargingProfile,
31 ChargingProfileKindType,
32 ChargingRateUnitType,
33 type ChargingSchedulePeriod,
34 type ChargingStationInfo,
35 type ChargingStationTemplate,
36 ChargingStationWorkerMessageEvents,
37 ConnectorPhaseRotation,
38 type ConnectorStatus,
39 ConnectorStatusEnum,
40 CurrentType,
41 type EvseTemplate,
42 type OCPP16BootNotificationRequest,
43 type OCPP20BootNotificationRequest,
44 OCPPVersion,
45 RecurrencyKindType,
46 type Reservation,
47 ReservationTerminationReason,
48 StandardParametersKey,
49 SupportedFeatureProfiles,
50 Voltage,
51 } from '../types';
52 import {
53 ACElectricUtils,
54 Constants,
55 DCElectricUtils,
56 cloneObject,
57 convertToDate,
58 convertToInt,
59 isArraySorted,
60 isEmptyObject,
61 isEmptyString,
62 isNotEmptyArray,
63 isNotEmptyString,
64 isNullOrUndefined,
65 isUndefined,
66 isValidTime,
67 logger,
68 secureRandom,
69 } from '../utils';
70
71 const moduleName = 'Helpers';
72
73 export const getChargingStationId = (
74 index: number,
75 stationTemplate: ChargingStationTemplate,
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(
84 idStr.length - 4,
85 )}${idSuffix}`;
86 };
87
88 export const hasReservationExpired = (reservation: Reservation): boolean => {
89 return isPast(reservation.expiryDate);
90 };
91
92 export 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
118 export const getNumberOfReservableConnectors = (
119 connectors: Map<number, ConnectorStatus>,
120 ): number => {
121 let reservableConnectors = 0;
122 for (const [connectorId, connectorStatus] of connectors) {
123 if (connectorId === 0) {
124 continue;
125 }
126 if (connectorStatus.status === ConnectorStatusEnum.Available) {
127 ++reservableConnectors;
128 }
129 }
130 return reservableConnectors;
131 };
132
133 export 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
155 export const checkChargingStation = (
156 chargingStation: ChargingStation,
157 logPrefix: string,
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
166 export const getPhaseRotationValue = (
167 connectorId: number,
168 numberOfPhases: number,
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
183 export const getMaxNumberOfEvses = (evses: Record<string, EvseTemplate>): number => {
184 if (!evses) {
185 return -1;
186 }
187 return Object.keys(evses).length;
188 };
189
190 const getMaxNumberOfConnectors = (connectors: Record<string, ConnectorStatus>): number => {
191 if (!connectors) {
192 return -1;
193 }
194 return Object.keys(connectors).length;
195 };
196
197 export const getBootConnectorStatus = (
198 chargingStation: ChargingStation,
199 connectorId: number,
200 connectorStatus: ConnectorStatus,
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
222 export const checkTemplate = (
223 stationTemplate: ChargingStationTemplate,
224 logPrefix: string,
225 templateFile: string,
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 }
237 if (isEmptyObject(stationTemplate.AutomaticTransactionGenerator!)) {
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`,
241 Constants.DEFAULT_ATG_CONFIGURATION,
242 );
243 }
244 if (isNullOrUndefined(stationTemplate.idTagsFile) || isEmptyString(stationTemplate.idTagsFile)) {
245 logger.warn(
246 `${logPrefix} Missing id tags file in template file ${templateFile}. That can lead to issues with the Automatic Transaction Generator`,
247 );
248 }
249 };
250
251 export const checkConnectorsConfiguration = (
252 stationTemplate: ChargingStationTemplate,
253 logPrefix: string,
254 templateFile: string,
255 ): {
256 configuredMaxConnectors: number;
257 templateMaxConnectors: number;
258 templateMaxAvailableConnectors: number;
259 } => {
260 const configuredMaxConnectors = getConfiguredNumberOfConnectors(stationTemplate);
261 checkConfiguredMaxConnectors(configuredMaxConnectors, logPrefix, templateFile);
262 const templateMaxConnectors = getMaxNumberOfConnectors(stationTemplate.Connectors!);
263 checkTemplateMaxConnectors(templateMaxConnectors, logPrefix, templateFile);
264 const templateMaxAvailableConnectors = stationTemplate.Connectors![0]
265 ? templateMaxConnectors - 1
266 : templateMaxConnectors;
267 if (
268 configuredMaxConnectors > templateMaxAvailableConnectors &&
269 !stationTemplate?.randomConnectors
270 ) {
271 logger.warn(
272 `${logPrefix} Number of connectors exceeds the number of connector configurations in template ${templateFile}, forcing random connector configurations affectation`,
273 );
274 stationTemplate.randomConnectors = true;
275 }
276 return { configuredMaxConnectors, templateMaxConnectors, templateMaxAvailableConnectors };
277 };
278
279 export const checkStationInfoConnectorStatus = (
280 connectorId: number,
281 connectorStatus: ConnectorStatus,
282 logPrefix: string,
283 templateFile: string,
284 ): void => {
285 if (!isNullOrUndefined(connectorStatus?.status)) {
286 logger.warn(
287 `${logPrefix} Charging station information from template ${templateFile} with connector id ${connectorId} status configuration defined, undefine it`,
288 );
289 delete connectorStatus.status;
290 }
291 };
292
293 export const buildConnectorsMap = (
294 connectors: Record<string, ConnectorStatus>,
295 logPrefix: string,
296 templateFile: string,
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));
305 }
306 } else {
307 logger.warn(
308 `${logPrefix} Charging station information from template ${templateFile} with no connectors, cannot build connectors map`,
309 );
310 }
311 return connectorsMap;
312 };
313
314 export const initializeConnectorsMapStatus = (
315 connectors: Map<number, ConnectorStatus>,
316 logPrefix: string,
317 ): void => {
318 for (const connectorId of connectors.keys()) {
319 if (connectorId > 0 && connectors.get(connectorId)?.transactionStarted === true) {
320 logger.warn(
321 `${logPrefix} Connector id ${connectorId} at initialization has a transaction started with id ${connectors.get(
322 connectorId,
323 )?.transactionId}`,
324 );
325 }
326 if (connectorId === 0) {
327 connectors.get(connectorId)!.availability = AvailabilityType.Operative;
328 if (isUndefined(connectors.get(connectorId)?.chargingProfiles)) {
329 connectors.get(connectorId)!.chargingProfiles = [];
330 }
331 } else if (
332 connectorId > 0 &&
333 isNullOrUndefined(connectors.get(connectorId)?.transactionStarted)
334 ) {
335 initializeConnectorStatus(connectors.get(connectorId)!);
336 }
337 }
338 };
339
340 export const resetConnectorStatus = (connectorStatus: ConnectorStatus): void => {
341 connectorStatus.idTagLocalAuthorized = false;
342 connectorStatus.idTagAuthorized = false;
343 connectorStatus.transactionRemoteStarted = false;
344 connectorStatus.transactionStarted = false;
345 delete connectorStatus?.transactionStart;
346 delete connectorStatus?.transactionId;
347 delete connectorStatus?.localAuthorizeIdTag;
348 delete connectorStatus?.authorizeIdTag;
349 delete connectorStatus?.transactionIdTag;
350 connectorStatus.transactionEnergyActiveImportRegisterValue = 0;
351 delete connectorStatus?.transactionBeginMeterValue;
352 };
353
354 export const createBootNotificationRequest = (
355 stationInfo: ChargingStationInfo,
356 bootReason: BootReasonEnumType = BootReasonEnumType.PowerUp,
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,
389 ...(!isUndefined(stationInfo.firmwareVersion) && {
390 firmwareVersion: stationInfo.firmwareVersion,
391 }),
392 ...(!isUndefined(stationInfo.chargeBoxSerialNumber) && {
393 serialNumber: stationInfo.chargeBoxSerialNumber,
394 }),
395 ...((!isUndefined(stationInfo.iccid) || !isUndefined(stationInfo.imsi)) && {
396 modem: {
397 ...(!isUndefined(stationInfo.iccid) && { iccid: stationInfo.iccid }),
398 ...(!isUndefined(stationInfo.imsi) && { imsi: stationInfo.imsi }),
399 },
400 }),
401 },
402 } as OCPP20BootNotificationRequest;
403 }
404 };
405
406 export const warnTemplateKeysDeprecation = (
407 stationTemplate: ChargingStationTemplate,
408 logPrefix: string,
409 templateFile: string,
410 ) => {
411 const templateKeys: { deprecatedKey: string; key?: string }[] = [
412 { deprecatedKey: 'supervisionUrl', key: 'supervisionUrls' },
413 { deprecatedKey: 'authorizationFile', key: 'idTagsFile' },
414 { deprecatedKey: 'payloadSchemaValidation', key: 'ocppStrictCompliance' },
415 { deprecatedKey: 'mustAuthorizeAtRemoteStart', key: 'remoteAuthorization' },
416 ];
417 for (const templateKey of templateKeys) {
418 warnDeprecatedTemplateKey(
419 stationTemplate,
420 templateKey.deprecatedKey,
421 logPrefix,
422 templateFile,
423 !isUndefined(templateKey.key) ? `Use '${templateKey.key}' instead` : undefined,
424 );
425 convertDeprecatedTemplateKey(stationTemplate, templateKey.deprecatedKey, templateKey.key);
426 }
427 };
428
429 export const stationTemplateToStationInfo = (
430 stationTemplate: ChargingStationTemplate,
431 ): ChargingStationInfo => {
432 stationTemplate = cloneObject<ChargingStationTemplate>(stationTemplate);
433 delete stationTemplate.power;
434 delete stationTemplate.powerUnit;
435 delete stationTemplate.Connectors;
436 delete stationTemplate.Evses;
437 delete stationTemplate.Configuration;
438 delete stationTemplate.AutomaticTransactionGenerator;
439 delete stationTemplate.chargeBoxSerialNumberPrefix;
440 delete stationTemplate.chargePointSerialNumberPrefix;
441 delete stationTemplate.meterSerialNumberPrefix;
442 return stationTemplate as ChargingStationInfo;
443 };
444
445 export const createSerialNumber = (
446 stationTemplate: ChargingStationTemplate,
447 stationInfo: ChargingStationInfo,
448 params: {
449 randomSerialNumberUpperCase?: boolean;
450 randomSerialNumber?: boolean;
451 } = {
452 randomSerialNumberUpperCase: true,
453 randomSerialNumber: true,
454 },
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
470 export const propagateSerialNumber = (
471 stationTemplate: ChargingStationTemplate,
472 stationInfoSrc: ChargingStationInfo,
473 stationInfoDst: ChargingStationInfo,
474 ) => {
475 if (!stationInfoSrc || !stationTemplate) {
476 throw new BaseError(
477 'Missing charging station template or existing configuration to propagate serial number',
478 );
479 }
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
491 export 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
501 export 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
517 export const getChargingStationConnectorChargingProfilesPowerLimit = (
518 chargingStation: ChargingStation,
519 connectorId: number,
520 ): number | undefined => {
521 let limit: number | undefined, matchingChargingProfile: ChargingProfile | undefined;
522 // Get charging profiles for connector id and sort by stack level
523 const chargingProfiles =
524 cloneObject<ChargingProfile[]>(
525 chargingStation.getConnectorStatus(connectorId)!.chargingProfiles!,
526 )?.sort((a, b) => b.stackLevel - a.stackLevel) ?? [];
527 // Get charging profiles on connector 0 and sort by stack level
528 if (isNotEmptyArray(chargingStation.getConnectorStatus(0)?.chargingProfiles)) {
529 chargingProfiles.push(
530 ...cloneObject<ChargingProfile[]>(
531 chargingStation.getConnectorStatus(0)!.chargingProfiles!,
532 ).sort((a, b) => b.stackLevel - a.stackLevel),
533 );
534 }
535 if (isNotEmptyArray(chargingProfiles)) {
536 const result = getLimitFromChargingProfiles(
537 chargingStation,
538 connectorId,
539 chargingProfiles,
540 chargingStation.logPrefix(),
541 );
542 if (!isNullOrUndefined(result)) {
543 limit = result?.limit;
544 matchingChargingProfile = result?.matchingChargingProfile;
545 switch (chargingStation.getCurrentOutType()) {
546 case CurrentType.AC:
547 limit =
548 matchingChargingProfile?.chargingSchedule?.chargingRateUnit ===
549 ChargingRateUnitType.WATT
550 ? limit
551 : ACElectricUtils.powerTotal(
552 chargingStation.getNumberOfPhases(),
553 chargingStation.getVoltageOut(),
554 limit!,
555 );
556 break;
557 case CurrentType.DC:
558 limit =
559 matchingChargingProfile?.chargingSchedule?.chargingRateUnit ===
560 ChargingRateUnitType.WATT
561 ? limit
562 : DCElectricUtils.power(chargingStation.getVoltageOut(), limit!);
563 }
564 const connectorMaximumPower =
565 chargingStation.getMaximumPower() / chargingStation.powerDivider;
566 if (limit! > connectorMaximumPower) {
567 logger.error(
568 `${chargingStation.logPrefix()} ${moduleName}.getChargingStationConnectorChargingProfilesPowerLimit: Charging profile id ${matchingChargingProfile?.chargingProfileId} limit ${limit} is greater than connector id ${connectorId} maximum ${connectorMaximumPower}: %j`,
569 result,
570 );
571 limit = connectorMaximumPower;
572 }
573 }
574 }
575 return limit;
576 };
577
578 export const getDefaultVoltageOut = (
579 currentType: CurrentType,
580 logPrefix: string,
581 templateFile: string,
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);
595 }
596 return defaultVoltageOut;
597 };
598
599 export 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
606 export const waitChargingStationEvents = async (
607 emitter: EventEmitter,
608 event: ChargingStationWorkerMessageEvents,
609 eventsToWait: number,
610 ): Promise<number> => {
611 return new Promise<number>((resolve) => {
612 let events = 0;
613 if (eventsToWait === 0) {
614 resolve(events);
615 }
616 emitter.on(event, () => {
617 ++events;
618 if (events === eventsToWait) {
619 resolve(events);
620 }
621 });
622 });
623 };
624
625 const getConfiguredNumberOfConnectors = (stationTemplate: ChargingStationTemplate): number => {
626 let configuredMaxConnectors = 0;
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) {
634 configuredMaxConnectors = stationTemplate.Connectors[0]
635 ? getMaxNumberOfConnectors(stationTemplate.Connectors) - 1
636 : getMaxNumberOfConnectors(stationTemplate.Connectors);
637 } else if (stationTemplate.Evses && !stationTemplate.Connectors) {
638 for (const evse in stationTemplate.Evses) {
639 if (evse === '0') {
640 continue;
641 }
642 configuredMaxConnectors += getMaxNumberOfConnectors(stationTemplate.Evses[evse].Connectors);
643 }
644 }
645 return configuredMaxConnectors;
646 };
647
648 const checkConfiguredMaxConnectors = (
649 configuredMaxConnectors: number,
650 logPrefix: string,
651 templateFile: string,
652 ): void => {
653 if (configuredMaxConnectors <= 0) {
654 logger.warn(
655 `${logPrefix} Charging station information from template ${templateFile} with ${configuredMaxConnectors} connectors`,
656 );
657 }
658 };
659
660 const checkTemplateMaxConnectors = (
661 templateMaxConnectors: number,
662 logPrefix: string,
663 templateFile: string,
664 ): void => {
665 if (templateMaxConnectors === 0) {
666 logger.warn(
667 `${logPrefix} Charging station information from template ${templateFile} with empty connectors configuration`,
668 );
669 } else if (templateMaxConnectors < 0) {
670 logger.error(
671 `${logPrefix} Charging station information from template ${templateFile} with no connectors configuration defined`,
672 );
673 }
674 };
675
676 const 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
689 const warnDeprecatedTemplateKey = (
690 template: ChargingStationTemplate,
691 key: string,
692 logPrefix: string,
693 templateFile: string,
694 logMsgToAppend = '',
695 ): void => {
696 if (!isUndefined(template[key as keyof ChargingStationTemplate])) {
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
705 const convertDeprecatedTemplateKey = (
706 template: ChargingStationTemplate,
707 deprecatedKey: string,
708 key?: string,
709 ): void => {
710 if (!isUndefined(template[deprecatedKey as keyof ChargingStationTemplate])) {
711 if (!isUndefined(key)) {
712 (template as unknown as Record<string, unknown>)[key!] =
713 template[deprecatedKey as keyof ChargingStationTemplate];
714 }
715 delete template[deprecatedKey as keyof ChargingStationTemplate];
716 }
717 };
718
719 interface ChargingProfilesLimit {
720 limit: number;
721 matchingChargingProfile: ChargingProfile;
722 }
723
724 /**
725 * Charging profiles shall already be sorted by connector id and stack level (highest stack level has priority)
726 *
727 * @param chargingStation -
728 * @param connectorId -
729 * @param chargingProfiles -
730 * @param logPrefix -
731 * @returns ChargingProfilesLimit
732 */
733 const getLimitFromChargingProfiles = (
734 chargingStation: ChargingStation,
735 connectorId: number,
736 chargingProfiles: ChargingProfile[],
737 logPrefix: string,
738 ): ChargingProfilesLimit | undefined => {
739 const debugLogMsg = `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Matching charging profile found for power limitation: %j`;
740 const currentDate = new Date();
741 const connectorStatus = chargingStation.getConnectorStatus(connectorId);
742 for (const chargingProfile of chargingProfiles) {
743 const chargingSchedule = chargingProfile.chargingSchedule;
744 if (connectorStatus?.transactionStarted && isNullOrUndefined(chargingSchedule?.startSchedule)) {
745 logger.debug(
746 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} has no startSchedule defined. Trying to set it to the connector current transaction start date`,
747 );
748 // OCPP specifies that if startSchedule is not defined, it should be relative to start of the connector transaction
749 chargingSchedule.startSchedule = connectorStatus?.transactionStart;
750 }
751 if (!isDate(chargingSchedule?.startSchedule)) {
752 logger.warn(
753 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} startSchedule property is not a Date object. Trying to convert it to a Date object`,
754 );
755 chargingSchedule.startSchedule = convertToDate(chargingSchedule?.startSchedule)!;
756 }
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;
768 }
769 if (!canProceedChargingProfile(chargingProfile, currentDate, logPrefix)) {
770 continue;
771 }
772 // Check if the charging profile is active
773 if (
774 isValidTime(chargingSchedule?.startSchedule) &&
775 isWithinInterval(currentDate, {
776 start: chargingSchedule.startSchedule!,
777 end: addSeconds(chargingSchedule.startSchedule!, chargingSchedule.duration!),
778 })
779 ) {
780 if (isNotEmptyArray(chargingSchedule.chargingSchedulePeriod)) {
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 }
796 // Check if the first schedule period start period is equal to 0
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 );
801 continue;
802 }
803 // Handle only one schedule period
804 if (chargingSchedule.chargingSchedulePeriod.length === 1) {
805 const result: ChargingProfilesLimit = {
806 limit: chargingSchedule.chargingSchedulePeriod[0].limit,
807 matchingChargingProfile: chargingProfile,
808 };
809 logger.debug(debugLogMsg, result);
810 return result;
811 }
812 let previousChargingSchedulePeriod: ChargingSchedulePeriod | undefined;
813 // Search for the right schedule period
814 for (const [
815 index,
816 chargingSchedulePeriod,
817 ] of chargingSchedule.chargingSchedulePeriod.entries()) {
818 // Find the right schedule period
819 if (
820 isAfter(
821 addSeconds(chargingSchedule.startSchedule!, chargingSchedulePeriod.startPeriod),
822 currentDate,
823 )
824 ) {
825 // Found the schedule period: previous is the correct one
826 const result: ChargingProfilesLimit = {
827 limit: previousChargingSchedulePeriod!.limit,
828 matchingChargingProfile: chargingProfile,
829 };
830 logger.debug(debugLogMsg, result);
831 return result;
832 }
833 // Keep a reference to previous one
834 previousChargingSchedulePeriod = chargingSchedulePeriod;
835 // Handle the last schedule period within the charging profile duration
836 if (
837 index === chargingSchedule.chargingSchedulePeriod.length - 1 ||
838 (index < chargingSchedule.chargingSchedulePeriod.length - 1 &&
839 chargingSchedule.duration! >
840 differenceInSeconds(
841 addSeconds(
842 chargingSchedule.startSchedule!,
843 chargingSchedule.chargingSchedulePeriod[index + 1].startPeriod,
844 ),
845 chargingSchedule.startSchedule!,
846 ))
847 ) {
848 const result: ChargingProfilesLimit = {
849 limit: previousChargingSchedulePeriod.limit,
850 matchingChargingProfile: chargingProfile,
851 };
852 logger.debug(debugLogMsg, result);
853 return result;
854 }
855 }
856 }
857 }
858 }
859 };
860
861 const 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
893 const 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
909 /**
910 * Adjust recurring charging profile startSchedule to the current recurrency time interval if needed
911 *
912 * @param chargingProfile -
913 * @param currentDate -
914 * @param logPrefix -
915 */
916 const prepareRecurringChargingProfile = (
917 chargingProfile: ChargingProfile,
918 currentDate: Date,
919 logPrefix: string,
920 ): boolean => {
921 const chargingSchedule = chargingProfile.chargingSchedule;
922 let recurringIntervalTranslated = false;
923 let recurringInterval: Interval;
924 switch (chargingProfile.recurrencyKind) {
925 case RecurrencyKindType.DAILY:
926 recurringInterval = {
927 start: chargingSchedule.startSchedule!,
928 end: addDays(chargingSchedule.startSchedule!, 1),
929 };
930 checkRecurringChargingProfileDuration(chargingProfile, recurringInterval, logPrefix);
931 if (
932 !isWithinInterval(currentDate, recurringInterval) &&
933 isBefore(recurringInterval.end, currentDate)
934 ) {
935 chargingSchedule.startSchedule = addDays(
936 recurringInterval.start,
937 differenceInDays(currentDate, recurringInterval.start),
938 );
939 recurringInterval = {
940 start: chargingSchedule.startSchedule,
941 end: addDays(chargingSchedule.startSchedule, 1),
942 };
943 recurringIntervalTranslated = true;
944 }
945 break;
946 case RecurrencyKindType.WEEKLY:
947 recurringInterval = {
948 start: chargingSchedule.startSchedule!,
949 end: addWeeks(chargingSchedule.startSchedule!, 1),
950 };
951 checkRecurringChargingProfileDuration(chargingProfile, recurringInterval, logPrefix);
952 if (
953 !isWithinInterval(currentDate, recurringInterval) &&
954 isBefore(recurringInterval.end, currentDate)
955 ) {
956 chargingSchedule.startSchedule = addWeeks(
957 recurringInterval.start,
958 differenceInWeeks(currentDate, recurringInterval.start),
959 );
960 recurringInterval = {
961 start: chargingSchedule.startSchedule,
962 end: addWeeks(chargingSchedule.startSchedule, 1),
963 };
964 recurringIntervalTranslated = true;
965 }
966 break;
967 default:
968 logger.error(
969 `${logPrefix} ${moduleName}.prepareRecurringChargingProfile: Recurring charging profile id ${chargingProfile.chargingProfileId} recurrency kind ${chargingProfile.recurrencyKind} is not supported`,
970 );
971 }
972 if (recurringIntervalTranslated && !isWithinInterval(currentDate, recurringInterval!)) {
973 logger.error(
974 `${logPrefix} ${moduleName}.prepareRecurringChargingProfile: Recurring ${
975 chargingProfile.recurrencyKind
976 } charging profile id ${chargingProfile.chargingProfileId} recurrency time interval [${toDate(
977 recurringInterval!.start,
978 ).toISOString()}, ${toDate(
979 recurringInterval!.end,
980 ).toISOString()}] has not been properly translated to current date ${currentDate.toISOString()} `,
981 );
982 }
983 return recurringIntervalTranslated;
984 };
985
986 const checkRecurringChargingProfileDuration = (
987 chargingProfile: ChargingProfile,
988 interval: Interval,
989 logPrefix: string,
990 ): void => {
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 (
1004 chargingProfile.chargingSchedule.duration! > differenceInSeconds(interval.end, interval.start)
1005 ) {
1006 logger.warn(
1007 `${logPrefix} ${moduleName}.checkRecurringChargingProfileDuration: Recurring ${
1008 chargingProfile.chargingProfileKind
1009 } charging profile id ${chargingProfile.chargingProfileId} duration ${
1010 chargingProfile.chargingSchedule.duration
1011 } is greater than the recurrency time interval duration ${differenceInSeconds(
1012 interval.end,
1013 interval.start,
1014 )}`,
1015 );
1016 chargingProfile.chargingSchedule.duration = differenceInSeconds(interval.end, interval.start);
1017 }
1018 };
1019
1020 const 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();
1027 }
1028 return randomSerialNumberSuffix;
1029 };