6d2742517fdd387ddbf40da631ddfb4616375473
[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 numberOfReservableConnectors = 0;
122 for (const [connectorId, connectorStatus] of connectors) {
123 if (connectorId === 0) {
124 continue;
125 }
126 if (connectorStatus.status === ConnectorStatusEnum.Available) {
127 ++numberOfReservableConnectors;
128 }
129 }
130 return numberOfReservableConnectors;
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 = getConfiguredMaxNumberOfConnectors(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 getConfiguredMaxNumberOfConnectors = (stationTemplate: ChargingStationTemplate): number => {
626 let configuredMaxNumberOfConnectors = 0;
627 if (isNotEmptyArray(stationTemplate.numberOfConnectors) === true) {
628 const numberOfConnectors = stationTemplate.numberOfConnectors as number[];
629 configuredMaxNumberOfConnectors =
630 numberOfConnectors[Math.floor(secureRandom() * numberOfConnectors.length)];
631 } else if (isUndefined(stationTemplate.numberOfConnectors) === false) {
632 configuredMaxNumberOfConnectors = stationTemplate.numberOfConnectors as number;
633 } else if (stationTemplate.Connectors && !stationTemplate.Evses) {
634 configuredMaxNumberOfConnectors = 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 configuredMaxNumberOfConnectors += getMaxNumberOfConnectors(
643 stationTemplate.Evses[evse].Connectors,
644 );
645 }
646 }
647 return configuredMaxNumberOfConnectors;
648 };
649
650 const checkConfiguredMaxConnectors = (
651 configuredMaxConnectors: number,
652 logPrefix: string,
653 templateFile: string,
654 ): void => {
655 if (configuredMaxConnectors <= 0) {
656 logger.warn(
657 `${logPrefix} Charging station information from template ${templateFile} with ${configuredMaxConnectors} connectors`,
658 );
659 }
660 };
661
662 const checkTemplateMaxConnectors = (
663 templateMaxConnectors: number,
664 logPrefix: string,
665 templateFile: string,
666 ): void => {
667 if (templateMaxConnectors === 0) {
668 logger.warn(
669 `${logPrefix} Charging station information from template ${templateFile} with empty connectors configuration`,
670 );
671 } else if (templateMaxConnectors < 0) {
672 logger.error(
673 `${logPrefix} Charging station information from template ${templateFile} with no connectors configuration defined`,
674 );
675 }
676 };
677
678 const initializeConnectorStatus = (connectorStatus: ConnectorStatus): void => {
679 connectorStatus.availability = AvailabilityType.Operative;
680 connectorStatus.idTagLocalAuthorized = false;
681 connectorStatus.idTagAuthorized = false;
682 connectorStatus.transactionRemoteStarted = false;
683 connectorStatus.transactionStarted = false;
684 connectorStatus.energyActiveImportRegisterValue = 0;
685 connectorStatus.transactionEnergyActiveImportRegisterValue = 0;
686 if (isUndefined(connectorStatus.chargingProfiles)) {
687 connectorStatus.chargingProfiles = [];
688 }
689 };
690
691 const warnDeprecatedTemplateKey = (
692 template: ChargingStationTemplate,
693 key: string,
694 logPrefix: string,
695 templateFile: string,
696 logMsgToAppend = '',
697 ): void => {
698 if (!isUndefined(template[key as keyof ChargingStationTemplate])) {
699 const logMsg = `Deprecated template key '${key}' usage in file '${templateFile}'${
700 isNotEmptyString(logMsgToAppend) ? `. ${logMsgToAppend}` : ''
701 }`;
702 logger.warn(`${logPrefix} ${logMsg}`);
703 console.warn(chalk.yellow(`${logMsg}`));
704 }
705 };
706
707 const convertDeprecatedTemplateKey = (
708 template: ChargingStationTemplate,
709 deprecatedKey: string,
710 key?: string,
711 ): void => {
712 if (!isUndefined(template[deprecatedKey as keyof ChargingStationTemplate])) {
713 if (!isUndefined(key)) {
714 (template as unknown as Record<string, unknown>)[key!] =
715 template[deprecatedKey as keyof ChargingStationTemplate];
716 }
717 delete template[deprecatedKey as keyof ChargingStationTemplate];
718 }
719 };
720
721 interface ChargingProfilesLimit {
722 limit: number;
723 matchingChargingProfile: ChargingProfile;
724 }
725
726 /**
727 * Charging profiles shall already be sorted by connector id and stack level (highest stack level has priority)
728 *
729 * @param chargingStation -
730 * @param connectorId -
731 * @param chargingProfiles -
732 * @param logPrefix -
733 * @returns ChargingProfilesLimit
734 */
735 const getLimitFromChargingProfiles = (
736 chargingStation: ChargingStation,
737 connectorId: number,
738 chargingProfiles: ChargingProfile[],
739 logPrefix: string,
740 ): ChargingProfilesLimit | undefined => {
741 const debugLogMsg = `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Matching charging profile found for power limitation: %j`;
742 const currentDate = new Date();
743 const connectorStatus = chargingStation.getConnectorStatus(connectorId);
744 for (const chargingProfile of chargingProfiles) {
745 const chargingSchedule = chargingProfile.chargingSchedule;
746 if (connectorStatus?.transactionStarted && isNullOrUndefined(chargingSchedule?.startSchedule)) {
747 logger.debug(
748 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} has no startSchedule defined. Trying to set it to the connector current transaction start date`,
749 );
750 // OCPP specifies that if startSchedule is not defined, it should be relative to start of the connector transaction
751 chargingSchedule.startSchedule = connectorStatus?.transactionStart;
752 }
753 if (!isDate(chargingSchedule?.startSchedule)) {
754 logger.warn(
755 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} startSchedule property is not a Date object. Trying to convert it to a Date object`,
756 );
757 chargingSchedule.startSchedule = convertToDate(chargingSchedule?.startSchedule)!;
758 }
759 switch (chargingProfile.chargingProfileKind) {
760 case ChargingProfileKindType.RECURRING:
761 if (!canProceedRecurringChargingProfile(chargingProfile, logPrefix)) {
762 continue;
763 }
764 prepareRecurringChargingProfile(chargingProfile, currentDate, logPrefix);
765 break;
766 case ChargingProfileKindType.RELATIVE:
767 connectorStatus?.transactionStarted &&
768 (chargingSchedule.startSchedule = connectorStatus?.transactionStart);
769 break;
770 }
771 if (!canProceedChargingProfile(chargingProfile, currentDate, logPrefix)) {
772 continue;
773 }
774 // Check if the charging profile is active
775 if (
776 isValidTime(chargingSchedule?.startSchedule) &&
777 isWithinInterval(currentDate, {
778 start: chargingSchedule.startSchedule!,
779 end: addSeconds(chargingSchedule.startSchedule!, chargingSchedule.duration!),
780 })
781 ) {
782 if (isNotEmptyArray(chargingSchedule.chargingSchedulePeriod)) {
783 const chargingSchedulePeriodCompareFn = (
784 a: ChargingSchedulePeriod,
785 b: ChargingSchedulePeriod,
786 ) => a.startPeriod - b.startPeriod;
787 if (
788 isArraySorted<ChargingSchedulePeriod>(
789 chargingSchedule.chargingSchedulePeriod,
790 chargingSchedulePeriodCompareFn,
791 ) === false
792 ) {
793 logger.warn(
794 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} schedule periods are not sorted by start period`,
795 );
796 chargingSchedule.chargingSchedulePeriod.sort(chargingSchedulePeriodCompareFn);
797 }
798 // Check if the first schedule period start period is equal to 0
799 if (chargingSchedule.chargingSchedulePeriod[0].startPeriod !== 0) {
800 logger.error(
801 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} first schedule period start period ${chargingSchedule.chargingSchedulePeriod[0].startPeriod} is not equal to 0`,
802 );
803 continue;
804 }
805 // Handle only one schedule period
806 if (chargingSchedule.chargingSchedulePeriod.length === 1) {
807 const result: ChargingProfilesLimit = {
808 limit: chargingSchedule.chargingSchedulePeriod[0].limit,
809 matchingChargingProfile: chargingProfile,
810 };
811 logger.debug(debugLogMsg, result);
812 return result;
813 }
814 let previousChargingSchedulePeriod: ChargingSchedulePeriod | undefined;
815 // Search for the right schedule period
816 for (const [
817 index,
818 chargingSchedulePeriod,
819 ] of chargingSchedule.chargingSchedulePeriod.entries()) {
820 // Find the right schedule period
821 if (
822 isAfter(
823 addSeconds(chargingSchedule.startSchedule!, chargingSchedulePeriod.startPeriod),
824 currentDate,
825 )
826 ) {
827 // Found the schedule period: previous is the correct one
828 const result: ChargingProfilesLimit = {
829 limit: previousChargingSchedulePeriod!.limit,
830 matchingChargingProfile: chargingProfile,
831 };
832 logger.debug(debugLogMsg, result);
833 return result;
834 }
835 // Keep a reference to previous one
836 previousChargingSchedulePeriod = chargingSchedulePeriod;
837 // Handle the last schedule period within the charging profile duration
838 if (
839 index === chargingSchedule.chargingSchedulePeriod.length - 1 ||
840 (index < chargingSchedule.chargingSchedulePeriod.length - 1 &&
841 chargingSchedule.duration! >
842 differenceInSeconds(
843 addSeconds(
844 chargingSchedule.startSchedule!,
845 chargingSchedule.chargingSchedulePeriod[index + 1].startPeriod,
846 ),
847 chargingSchedule.startSchedule!,
848 ))
849 ) {
850 const result: ChargingProfilesLimit = {
851 limit: previousChargingSchedulePeriod.limit,
852 matchingChargingProfile: chargingProfile,
853 };
854 logger.debug(debugLogMsg, result);
855 return result;
856 }
857 }
858 }
859 }
860 }
861 };
862
863 const canProceedChargingProfile = (
864 chargingProfile: ChargingProfile,
865 currentDate: Date,
866 logPrefix: string,
867 ): boolean => {
868 if (
869 (isValidTime(chargingProfile.validFrom) && isBefore(currentDate, chargingProfile.validFrom!)) ||
870 (isValidTime(chargingProfile.validTo) && isAfter(currentDate, chargingProfile.validTo!))
871 ) {
872 logger.debug(
873 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${
874 chargingProfile.chargingProfileId
875 } is not valid for the current date ${currentDate.toISOString()}`,
876 );
877 return false;
878 }
879 const chargingSchedule = chargingProfile.chargingSchedule;
880 if (isNullOrUndefined(chargingSchedule?.startSchedule)) {
881 logger.error(
882 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId} has (still) no startSchedule defined`,
883 );
884 return false;
885 }
886 if (isNullOrUndefined(chargingSchedule?.duration)) {
887 logger.error(
888 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId} has no duration defined, not yet supported`,
889 );
890 return false;
891 }
892 return true;
893 };
894
895 const canProceedRecurringChargingProfile = (
896 chargingProfile: ChargingProfile,
897 logPrefix: string,
898 ): boolean => {
899 if (
900 chargingProfile.chargingProfileKind === ChargingProfileKindType.RECURRING &&
901 isNullOrUndefined(chargingProfile.recurrencyKind)
902 ) {
903 logger.error(
904 `${logPrefix} ${moduleName}.canProceedRecurringChargingProfile: Recurring charging profile id ${chargingProfile.chargingProfileId} has no recurrencyKind defined`,
905 );
906 return false;
907 }
908 return true;
909 };
910
911 /**
912 * Adjust recurring charging profile startSchedule to the current recurrency time interval if needed
913 *
914 * @param chargingProfile -
915 * @param currentDate -
916 * @param logPrefix -
917 */
918 const prepareRecurringChargingProfile = (
919 chargingProfile: ChargingProfile,
920 currentDate: Date,
921 logPrefix: string,
922 ): boolean => {
923 const chargingSchedule = chargingProfile.chargingSchedule;
924 let recurringIntervalTranslated = false;
925 let recurringInterval: Interval;
926 switch (chargingProfile.recurrencyKind) {
927 case RecurrencyKindType.DAILY:
928 recurringInterval = {
929 start: chargingSchedule.startSchedule!,
930 end: addDays(chargingSchedule.startSchedule!, 1),
931 };
932 checkRecurringChargingProfileDuration(chargingProfile, recurringInterval, logPrefix);
933 if (
934 !isWithinInterval(currentDate, recurringInterval) &&
935 isBefore(recurringInterval.end, currentDate)
936 ) {
937 chargingSchedule.startSchedule = addDays(
938 recurringInterval.start,
939 differenceInDays(currentDate, recurringInterval.start),
940 );
941 recurringInterval = {
942 start: chargingSchedule.startSchedule,
943 end: addDays(chargingSchedule.startSchedule, 1),
944 };
945 recurringIntervalTranslated = true;
946 }
947 break;
948 case RecurrencyKindType.WEEKLY:
949 recurringInterval = {
950 start: chargingSchedule.startSchedule!,
951 end: addWeeks(chargingSchedule.startSchedule!, 1),
952 };
953 checkRecurringChargingProfileDuration(chargingProfile, recurringInterval, logPrefix);
954 if (
955 !isWithinInterval(currentDate, recurringInterval) &&
956 isBefore(recurringInterval.end, currentDate)
957 ) {
958 chargingSchedule.startSchedule = addWeeks(
959 recurringInterval.start,
960 differenceInWeeks(currentDate, recurringInterval.start),
961 );
962 recurringInterval = {
963 start: chargingSchedule.startSchedule,
964 end: addWeeks(chargingSchedule.startSchedule, 1),
965 };
966 recurringIntervalTranslated = true;
967 }
968 break;
969 default:
970 logger.error(
971 `${logPrefix} ${moduleName}.prepareRecurringChargingProfile: Recurring charging profile id ${chargingProfile.chargingProfileId} recurrency kind ${chargingProfile.recurrencyKind} is not supported`,
972 );
973 }
974 if (recurringIntervalTranslated && !isWithinInterval(currentDate, recurringInterval!)) {
975 logger.error(
976 `${logPrefix} ${moduleName}.prepareRecurringChargingProfile: Recurring ${
977 chargingProfile.recurrencyKind
978 } charging profile id ${chargingProfile.chargingProfileId} recurrency time interval [${toDate(
979 recurringInterval!.start,
980 ).toISOString()}, ${toDate(
981 recurringInterval!.end,
982 ).toISOString()}] has not been properly translated to current date ${currentDate.toISOString()} `,
983 );
984 }
985 return recurringIntervalTranslated;
986 };
987
988 const checkRecurringChargingProfileDuration = (
989 chargingProfile: ChargingProfile,
990 interval: Interval,
991 logPrefix: string,
992 ): void => {
993 if (isNullOrUndefined(chargingProfile.chargingSchedule.duration)) {
994 logger.warn(
995 `${logPrefix} ${moduleName}.checkRecurringChargingProfileDuration: Recurring ${
996 chargingProfile.chargingProfileKind
997 } charging profile id ${
998 chargingProfile.chargingProfileId
999 } duration is not defined, set it to the recurrency time interval duration ${differenceInSeconds(
1000 interval.end,
1001 interval.start,
1002 )}`,
1003 );
1004 chargingProfile.chargingSchedule.duration = differenceInSeconds(interval.end, interval.start);
1005 } else if (
1006 chargingProfile.chargingSchedule.duration! > differenceInSeconds(interval.end, interval.start)
1007 ) {
1008 logger.warn(
1009 `${logPrefix} ${moduleName}.checkRecurringChargingProfileDuration: Recurring ${
1010 chargingProfile.chargingProfileKind
1011 } charging profile id ${chargingProfile.chargingProfileId} duration ${
1012 chargingProfile.chargingSchedule.duration
1013 } is greater than the recurrency time interval duration ${differenceInSeconds(
1014 interval.end,
1015 interval.start,
1016 )}`,
1017 );
1018 chargingProfile.chargingSchedule.duration = differenceInSeconds(interval.end, interval.start);
1019 }
1020 };
1021
1022 const getRandomSerialNumberSuffix = (params?: {
1023 randomBytesLength?: number;
1024 upperCase?: boolean;
1025 }): string => {
1026 const randomSerialNumberSuffix = randomBytes(params?.randomBytesLength ?? 16).toString('hex');
1027 if (params?.upperCase) {
1028 return randomSerialNumberSuffix.toUpperCase();
1029 }
1030 return randomSerialNumberSuffix;
1031 };