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