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