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