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