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