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