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 }
e1d9a0f4 184 if (isEmptyObject(stationTemplate.AutomaticTransactionGenerator!)) {
fba11dc6
JB
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);
e1d9a0f4 209 const templateMaxConnectors = getMaxNumberOfConnectors(stationTemplate.Connectors!);
fba11dc6 210 checkTemplateMaxConnectors(templateMaxConnectors, logPrefix, templateFile);
e1d9a0f4 211 const templateMaxAvailableConnectors = stationTemplate.Connectors![0]
fba11dc6
JB
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 273 if (connectorId === 0) {
e1d9a0f4 274 connectors.get(connectorId)!.availability = AvailabilityType.Operative;
fba11dc6 275 if (isUndefined(connectors.get(connectorId)?.chargingProfiles)) {
e1d9a0f4 276 connectors.get(connectorId)!.chargingProfiles = [];
04b1261c 277 }
fba11dc6
JB
278 } else if (
279 connectorId > 0 &&
280 isNullOrUndefined(connectors.get(connectorId)?.transactionStarted)
281 ) {
e1d9a0f4 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 356) => {
e4c6cf05
JB
357 const templateKeys: { deprecatedKey: string; key?: string }[] = [
358 { deprecatedKey: 'supervisionUrl', key: 'supervisionUrls' },
359 { deprecatedKey: 'authorizationFile', key: 'idTagsFile' },
360 { deprecatedKey: 'payloadSchemaValidation', key: 'ocppStrictCompliance' },
fba11dc6
JB
361 ];
362 for (const templateKey of templateKeys) {
363 warnDeprecatedTemplateKey(
364 stationTemplate,
365 templateKey.deprecatedKey,
366 logPrefix,
367 templateFile,
e1d9a0f4 368 !isUndefined(templateKey.key) ? `Use '${templateKey.key}' instead` : undefined,
fba11dc6
JB
369 );
370 convertDeprecatedTemplateKey(stationTemplate, templateKey.deprecatedKey, templateKey.key);
371 }
372};
373
374export const stationTemplateToStationInfo = (
5edd8ba0 375 stationTemplate: ChargingStationTemplate,
fba11dc6
JB
376): ChargingStationInfo => {
377 stationTemplate = cloneObject<ChargingStationTemplate>(stationTemplate);
378 delete stationTemplate.power;
379 delete stationTemplate.powerUnit;
e1d9a0f4
JB
380 delete stationTemplate.Connectors;
381 delete stationTemplate.Evses;
fba11dc6
JB
382 delete stationTemplate.Configuration;
383 delete stationTemplate.AutomaticTransactionGenerator;
384 delete stationTemplate.chargeBoxSerialNumberPrefix;
385 delete stationTemplate.chargePointSerialNumberPrefix;
386 delete stationTemplate.meterSerialNumberPrefix;
387 return stationTemplate as unknown as ChargingStationInfo;
388};
389
390export const createSerialNumber = (
391 stationTemplate: ChargingStationTemplate,
392 stationInfo: ChargingStationInfo,
393 params: {
394 randomSerialNumberUpperCase?: boolean;
395 randomSerialNumber?: boolean;
396 } = {
397 randomSerialNumberUpperCase: true,
398 randomSerialNumber: true,
5edd8ba0 399 },
fba11dc6
JB
400): void => {
401 params = { ...{ randomSerialNumberUpperCase: true, randomSerialNumber: true }, ...params };
402 const serialNumberSuffix = params?.randomSerialNumber
403 ? getRandomSerialNumberSuffix({
404 upperCase: params.randomSerialNumberUpperCase,
405 })
406 : '';
407 isNotEmptyString(stationTemplate?.chargePointSerialNumberPrefix) &&
408 (stationInfo.chargePointSerialNumber = `${stationTemplate.chargePointSerialNumberPrefix}${serialNumberSuffix}`);
409 isNotEmptyString(stationTemplate?.chargeBoxSerialNumberPrefix) &&
410 (stationInfo.chargeBoxSerialNumber = `${stationTemplate.chargeBoxSerialNumberPrefix}${serialNumberSuffix}`);
411 isNotEmptyString(stationTemplate?.meterSerialNumberPrefix) &&
412 (stationInfo.meterSerialNumber = `${stationTemplate.meterSerialNumberPrefix}${serialNumberSuffix}`);
413};
414
415export const propagateSerialNumber = (
416 stationTemplate: ChargingStationTemplate,
417 stationInfoSrc: ChargingStationInfo,
5edd8ba0 418 stationInfoDst: ChargingStationInfo,
fba11dc6
JB
419) => {
420 if (!stationInfoSrc || !stationTemplate) {
421 throw new BaseError(
5edd8ba0 422 'Missing charging station template or existing configuration to propagate serial number',
fba11dc6 423 );
17ac262c 424 }
fba11dc6
JB
425 stationTemplate?.chargePointSerialNumberPrefix && stationInfoSrc?.chargePointSerialNumber
426 ? (stationInfoDst.chargePointSerialNumber = stationInfoSrc.chargePointSerialNumber)
427 : stationInfoDst?.chargePointSerialNumber && delete stationInfoDst.chargePointSerialNumber;
428 stationTemplate?.chargeBoxSerialNumberPrefix && stationInfoSrc?.chargeBoxSerialNumber
429 ? (stationInfoDst.chargeBoxSerialNumber = stationInfoSrc.chargeBoxSerialNumber)
430 : stationInfoDst?.chargeBoxSerialNumber && delete stationInfoDst.chargeBoxSerialNumber;
431 stationTemplate?.meterSerialNumberPrefix && stationInfoSrc?.meterSerialNumber
432 ? (stationInfoDst.meterSerialNumber = stationInfoSrc.meterSerialNumber)
433 : stationInfoDst?.meterSerialNumber && delete stationInfoDst.meterSerialNumber;
434};
435
436export const getAmperageLimitationUnitDivider = (stationInfo: ChargingStationInfo): number => {
437 let unitDivider = 1;
438 switch (stationInfo.amperageLimitationUnit) {
439 case AmpereUnits.DECI_AMPERE:
440 unitDivider = 10;
441 break;
442 case AmpereUnits.CENTI_AMPERE:
443 unitDivider = 100;
444 break;
445 case AmpereUnits.MILLI_AMPERE:
446 unitDivider = 1000;
447 break;
448 }
449 return unitDivider;
450};
451
452export const getChargingStationConnectorChargingProfilesPowerLimit = (
453 chargingStation: ChargingStation,
5edd8ba0 454 connectorId: number,
fba11dc6 455): number | undefined => {
e1d9a0f4 456 let limit: number | undefined, matchingChargingProfile: ChargingProfile | undefined;
fba11dc6
JB
457 // Get charging profiles for connector and sort by stack level
458 const chargingProfiles =
459 cloneObject<ChargingProfile[]>(
e1d9a0f4 460 chargingStation.getConnectorStatus(connectorId)!.chargingProfiles!,
fba11dc6
JB
461 )?.sort((a, b) => b.stackLevel - a.stackLevel) ?? [];
462 // Get profiles on connector 0
463 if (chargingStation.getConnectorStatus(0)?.chargingProfiles) {
464 chargingProfiles.push(
465 ...cloneObject<ChargingProfile[]>(
e1d9a0f4 466 chargingStation.getConnectorStatus(0)!.chargingProfiles!,
5edd8ba0 467 ).sort((a, b) => b.stackLevel - a.stackLevel),
fba11dc6 468 );
17ac262c 469 }
fba11dc6
JB
470 if (isNotEmptyArray(chargingProfiles)) {
471 const result = getLimitFromChargingProfiles(chargingProfiles, chargingStation.logPrefix());
472 if (!isNullOrUndefined(result)) {
473 limit = result?.limit;
474 matchingChargingProfile = result?.matchingChargingProfile;
475 switch (chargingStation.getCurrentOutType()) {
476 case CurrentType.AC:
477 limit =
e1d9a0f4
JB
478 matchingChargingProfile?.chargingSchedule?.chargingRateUnit ===
479 ChargingRateUnitType.WATT
fba11dc6
JB
480 ? limit
481 : ACElectricUtils.powerTotal(
482 chargingStation.getNumberOfPhases(),
483 chargingStation.getVoltageOut(),
e1d9a0f4 484 limit!,
fba11dc6
JB
485 );
486 break;
487 case CurrentType.DC:
488 limit =
e1d9a0f4
JB
489 matchingChargingProfile?.chargingSchedule?.chargingRateUnit ===
490 ChargingRateUnitType.WATT
fba11dc6 491 ? limit
e1d9a0f4 492 : DCElectricUtils.power(chargingStation.getVoltageOut(), limit!);
fba11dc6
JB
493 }
494 const connectorMaximumPower =
495 chargingStation.getMaximumPower() / chargingStation.powerDivider;
e1d9a0f4 496 if (limit! > connectorMaximumPower) {
fba11dc6 497 logger.error(
e1d9a0f4 498 `${chargingStation.logPrefix()} Charging profile id ${matchingChargingProfile?.chargingProfileId} limit ${limit} is greater than connector id ${connectorId} maximum ${connectorMaximumPower}: %j`,
5edd8ba0 499 result,
fba11dc6
JB
500 );
501 limit = connectorMaximumPower;
15068be9
JB
502 }
503 }
15068be9 504 }
fba11dc6
JB
505 return limit;
506};
507
508export const getDefaultVoltageOut = (
509 currentType: CurrentType,
510 logPrefix: string,
5edd8ba0 511 templateFile: string,
fba11dc6
JB
512): Voltage => {
513 const errorMsg = `Unknown ${currentType} currentOutType in template file ${templateFile}, cannot define default voltage out`;
514 let defaultVoltageOut: number;
515 switch (currentType) {
516 case CurrentType.AC:
517 defaultVoltageOut = Voltage.VOLTAGE_230;
518 break;
519 case CurrentType.DC:
520 defaultVoltageOut = Voltage.VOLTAGE_400;
521 break;
522 default:
523 logger.error(`${logPrefix} ${errorMsg}`);
524 throw new BaseError(errorMsg);
15068be9 525 }
fba11dc6
JB
526 return defaultVoltageOut;
527};
528
529export const getIdTagsFile = (stationInfo: ChargingStationInfo): string | undefined => {
530 return (
531 stationInfo.idTagsFile &&
532 join(dirname(fileURLToPath(import.meta.url)), 'assets', basename(stationInfo.idTagsFile))
533 );
534};
535
b2b60626 536export const waitChargingStationEvents = async (
fba11dc6
JB
537 emitter: EventEmitter,
538 event: ChargingStationWorkerMessageEvents,
5edd8ba0 539 eventsToWait: number,
fba11dc6
JB
540): Promise<number> => {
541 return new Promise((resolve) => {
542 let events = 0;
543 if (eventsToWait === 0) {
544 resolve(events);
545 }
546 emitter.on(event, () => {
547 ++events;
548 if (events === eventsToWait) {
b1f1b0f6
JB
549 resolve(events);
550 }
b1f1b0f6 551 });
fba11dc6
JB
552 });
553};
554
555const getConfiguredNumberOfConnectors = (stationTemplate: ChargingStationTemplate): number => {
e1d9a0f4 556 let configuredMaxConnectors = 0;
fba11dc6
JB
557 if (isNotEmptyArray(stationTemplate.numberOfConnectors) === true) {
558 const numberOfConnectors = stationTemplate.numberOfConnectors as number[];
559 configuredMaxConnectors =
560 numberOfConnectors[Math.floor(secureRandom() * numberOfConnectors.length)];
561 } else if (isUndefined(stationTemplate.numberOfConnectors) === false) {
562 configuredMaxConnectors = stationTemplate.numberOfConnectors as number;
563 } else if (stationTemplate.Connectors && !stationTemplate.Evses) {
e1d9a0f4 564 configuredMaxConnectors = stationTemplate.Connectors[0]
fba11dc6
JB
565 ? getMaxNumberOfConnectors(stationTemplate.Connectors) - 1
566 : getMaxNumberOfConnectors(stationTemplate.Connectors);
567 } else if (stationTemplate.Evses && !stationTemplate.Connectors) {
568 configuredMaxConnectors = 0;
569 for (const evse in stationTemplate.Evses) {
570 if (evse === '0') {
571 continue;
cda5d0fb 572 }
fba11dc6 573 configuredMaxConnectors += getMaxNumberOfConnectors(stationTemplate.Evses[evse].Connectors);
cda5d0fb 574 }
cda5d0fb 575 }
fba11dc6
JB
576 return configuredMaxConnectors;
577};
578
579const checkConfiguredMaxConnectors = (
580 configuredMaxConnectors: number,
581 logPrefix: string,
5edd8ba0 582 templateFile: string,
fba11dc6
JB
583): void => {
584 if (configuredMaxConnectors <= 0) {
585 logger.warn(
5edd8ba0 586 `${logPrefix} Charging station information from template ${templateFile} with ${configuredMaxConnectors} connectors`,
fba11dc6 587 );
cda5d0fb 588 }
fba11dc6 589};
cda5d0fb 590
fba11dc6
JB
591const checkTemplateMaxConnectors = (
592 templateMaxConnectors: number,
593 logPrefix: string,
5edd8ba0 594 templateFile: string,
fba11dc6
JB
595): void => {
596 if (templateMaxConnectors === 0) {
597 logger.warn(
5edd8ba0 598 `${logPrefix} Charging station information from template ${templateFile} with empty connectors configuration`,
fba11dc6
JB
599 );
600 } else if (templateMaxConnectors < 0) {
601 logger.error(
5edd8ba0 602 `${logPrefix} Charging station information from template ${templateFile} with no connectors configuration defined`,
fba11dc6
JB
603 );
604 }
605};
606
607const initializeConnectorStatus = (connectorStatus: ConnectorStatus): void => {
608 connectorStatus.availability = AvailabilityType.Operative;
609 connectorStatus.idTagLocalAuthorized = false;
610 connectorStatus.idTagAuthorized = false;
611 connectorStatus.transactionRemoteStarted = false;
612 connectorStatus.transactionStarted = false;
613 connectorStatus.energyActiveImportRegisterValue = 0;
614 connectorStatus.transactionEnergyActiveImportRegisterValue = 0;
615 if (isUndefined(connectorStatus.chargingProfiles)) {
616 connectorStatus.chargingProfiles = [];
617 }
618};
619
620const warnDeprecatedTemplateKey = (
621 template: ChargingStationTemplate,
622 key: string,
623 logPrefix: string,
624 templateFile: string,
5edd8ba0 625 logMsgToAppend = '',
fba11dc6
JB
626): void => {
627 if (!isUndefined(template[key])) {
628 const logMsg = `Deprecated template key '${key}' usage in file '${templateFile}'${
629 isNotEmptyString(logMsgToAppend) ? `. ${logMsgToAppend}` : ''
630 }`;
631 logger.warn(`${logPrefix} ${logMsg}`);
632 console.warn(chalk.yellow(`${logMsg}`));
633 }
634};
635
636const convertDeprecatedTemplateKey = (
637 template: ChargingStationTemplate,
638 deprecatedKey: string,
e1d9a0f4 639 key?: string,
fba11dc6
JB
640): void => {
641 if (!isUndefined(template[deprecatedKey])) {
e1d9a0f4
JB
642 if (!isUndefined(key)) {
643 template[key!] = template[deprecatedKey] as unknown;
644 }
fba11dc6
JB
645 delete template[deprecatedKey];
646 }
647};
648
649/**
650 * Charging profiles should already be sorted by connector id and stack level (highest stack level has priority)
651 *
652 * @param chargingProfiles -
653 * @param logPrefix -
654 * @returns
655 */
656const getLimitFromChargingProfiles = (
657 chargingProfiles: ChargingProfile[],
5edd8ba0 658 logPrefix: string,
fba11dc6
JB
659): {
660 limit: number;
661 matchingChargingProfile: ChargingProfile;
662} | null => {
663 const debugLogMsg = `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Matching charging profile found for power limitation: %j`;
664 const currentMoment = moment();
665 const currentDate = new Date();
666 for (const chargingProfile of chargingProfiles) {
667 // Set helpers
668 const chargingSchedule = chargingProfile.chargingSchedule;
669 if (!chargingSchedule?.startSchedule) {
cda5d0fb 670 logger.warn(
5edd8ba0 671 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: startSchedule is not defined in charging profile id ${chargingProfile.chargingProfileId}`,
cda5d0fb 672 );
52952bf8 673 }
fba11dc6
JB
674 // Check type (recurring) and if it is already active
675 // Adjust the daily recurring schedule to today
676 if (
677 chargingProfile.chargingProfileKind === ChargingProfileKindType.RECURRING &&
678 chargingProfile.recurrencyKind === RecurrencyKindType.DAILY &&
679 currentMoment.isAfter(chargingSchedule.startSchedule)
680 ) {
681 if (!(chargingSchedule?.startSchedule instanceof Date)) {
41189456 682 logger.warn(
5edd8ba0 683 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: startSchedule is not a Date object in charging profile id ${chargingProfile.chargingProfileId}. Trying to convert it to a Date object`,
41189456 684 );
e1d9a0f4 685 chargingSchedule.startSchedule = new Date(chargingSchedule.startSchedule!);
fba11dc6
JB
686 }
687 chargingSchedule.startSchedule.setFullYear(
688 currentDate.getFullYear(),
689 currentDate.getMonth(),
5edd8ba0 690 currentDate.getDate(),
fba11dc6
JB
691 );
692 // Check if the start of the schedule is yesterday
693 if (moment(chargingSchedule.startSchedule).isAfter(currentMoment)) {
694 chargingSchedule.startSchedule.setDate(currentDate.getDate() - 1);
41189456 695 }
fba11dc6
JB
696 } else if (moment(chargingSchedule.startSchedule).isAfter(currentMoment)) {
697 return null;
698 }
699 // Check if the charging profile is active
700 if (
701 moment(chargingSchedule.startSchedule)
702 .add(chargingSchedule.duration, 's')
703 .isAfter(currentMoment)
704 ) {
e1d9a0f4 705 let lastButOneSchedule: ChargingSchedulePeriod | undefined;
fba11dc6
JB
706 // Search the right schedule period
707 for (const schedulePeriod of chargingSchedule.chargingSchedulePeriod) {
708 // Handling of only one period
709 if (
710 chargingSchedule.chargingSchedulePeriod.length === 1 &&
711 schedulePeriod.startPeriod === 0
712 ) {
713 const result = {
714 limit: schedulePeriod.limit,
715 matchingChargingProfile: chargingProfile,
716 };
717 logger.debug(debugLogMsg, result);
718 return result;
41189456 719 }
fba11dc6
JB
720 // Find the right schedule period
721 if (
722 moment(chargingSchedule.startSchedule)
723 .add(schedulePeriod.startPeriod, 's')
724 .isAfter(currentMoment)
725 ) {
726 // Found the schedule: last but one is the correct one
727 const result = {
e1d9a0f4 728 limit: lastButOneSchedule!.limit,
fba11dc6
JB
729 matchingChargingProfile: chargingProfile,
730 };
731 logger.debug(debugLogMsg, result);
732 return result;
17ac262c 733 }
fba11dc6
JB
734 // Keep it
735 lastButOneSchedule = schedulePeriod;
736 // Handle the last schedule period
737 if (
738 schedulePeriod.startPeriod ===
739 chargingSchedule.chargingSchedulePeriod[
740 chargingSchedule.chargingSchedulePeriod.length - 1
741 ].startPeriod
742 ) {
743 const result = {
744 limit: lastButOneSchedule.limit,
745 matchingChargingProfile: chargingProfile,
746 };
747 logger.debug(debugLogMsg, result);
748 return result;
17ac262c
JB
749 }
750 }
751 }
17ac262c 752 }
fba11dc6
JB
753 return null;
754};
17ac262c 755
fba11dc6
JB
756const getRandomSerialNumberSuffix = (params?: {
757 randomBytesLength?: number;
758 upperCase?: boolean;
759}): string => {
760 const randomSerialNumberSuffix = randomBytes(params?.randomBytesLength ?? 16).toString('hex');
761 if (params?.upperCase) {
762 return randomSerialNumberSuffix.toUpperCase();
17ac262c 763 }
fba11dc6
JB
764 return randomSerialNumberSuffix;
765};