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