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