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