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