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