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