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