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