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