build: switch to NodeNext module resolution
[e-mobility-charging-stations-simulator.git] / src / charging-station / Helpers.ts
CommitLineData
d972af76
JB
1import { createHash, randomBytes } from 'node:crypto';
2import type { EventEmitter } from 'node:events';
3import { basename, dirname, join } from 'node:path';
10687422 4import { env } from 'node:process';
130783a7 5import { fileURLToPath } from 'node:url';
8114d10e 6
e302df1d 7import chalk from 'chalk';
f924d466 8import {
f1e3871b 9 type Interval,
f924d466 10 addDays,
f924d466
JB
11 addSeconds,
12 addWeeks,
497588ef 13 differenceInDays,
d476bc1b 14 differenceInSeconds,
497588ef 15 differenceInWeeks,
f924d466
JB
16 isAfter,
17 isBefore,
0bd926c1 18 isDate,
90aceaf6 19 isPast,
252a7d22 20 isWithinInterval,
522e4b05 21 toDate,
f924d466 22} from 'date-fns';
c7c86b6f 23import { maxTime } from 'date-fns/constants';
8114d10e 24
a6ef1ece
JB
25import type { ChargingStation } from './ChargingStation.js';
26import { getConfigurationKey } from './ConfigurationKeyUtils.js';
27import { BaseError } from '../exception/index.js';
981ebfbe 28import {
492cf6ab 29 AmpereUnits,
04b1261c 30 AvailabilityType,
268a74bb
JB
31 type BootNotificationRequest,
32 BootReasonEnumType,
15068be9 33 type ChargingProfile,
268a74bb 34 ChargingProfileKindType,
15068be9
JB
35 ChargingRateUnitType,
36 type ChargingSchedulePeriod,
73edcc94 37 type ChargingStationConfiguration,
268a74bb
JB
38 type ChargingStationInfo,
39 type ChargingStationTemplate,
b1f1b0f6 40 ChargingStationWorkerMessageEvents,
dd08d43d 41 ConnectorPhaseRotation,
a78ef5ed 42 type ConnectorStatus,
c3b83130 43 ConnectorStatusEnum,
268a74bb 44 CurrentType,
ae25f265 45 type EvseTemplate,
268a74bb
JB
46 type OCPP16BootNotificationRequest,
47 type OCPP20BootNotificationRequest,
48 OCPPVersion,
49 RecurrencyKindType,
90aceaf6
JB
50 type Reservation,
51 ReservationTerminationReason,
d8093be1
JB
52 StandardParametersKey,
53 SupportedFeatureProfiles,
268a74bb 54 Voltage,
a6ef1ece 55} from '../types/index.js';
9bf0ef23
JB
56import {
57 ACElectricUtils,
58 Constants,
59 DCElectricUtils,
60 cloneObject,
b85cef4c 61 convertToDate,
9bf0ef23 62 convertToInt,
80c58041 63 isArraySorted,
9bf0ef23
JB
64 isEmptyObject,
65 isEmptyString,
66 isNotEmptyArray,
67 isNotEmptyString,
68 isNullOrUndefined,
69 isUndefined,
0bd926c1 70 isValidTime,
9bf0ef23
JB
71 logger,
72 secureRandom,
a6ef1ece 73} from '../utils/index.js';
17ac262c 74
08b58f00 75const moduleName = 'Helpers';
91a4f151 76
fba11dc6
JB
77export const getChargingStationId = (
78 index: number,
c1f16afd 79 stationTemplate: ChargingStationTemplate | undefined,
fba11dc6 80): string => {
f1bd9d19 81 if (stationTemplate === undefined) {
c1f16afd
JB
82 return "Unknown 'chargingStationId'";
83 }
fba11dc6 84 // In case of multiple instances: add instance index to charging station id
10687422 85 const instanceIndex = env.CF_INSTANCE_INDEX ?? 0;
fba11dc6
JB
86 const idSuffix = stationTemplate?.nameSuffix ?? '';
87 const idStr = `000000000${index.toString()}`;
88 return stationTemplate?.fixedName
89 ? stationTemplate.baseName
f1bd9d19 90 : `${stationTemplate.baseName}-${instanceIndex.toString()}${idStr.substring(
5edd8ba0 91 idStr.length - 4,
fba11dc6
JB
92 )}${idSuffix}`;
93};
94
90aceaf6
JB
95export const hasReservationExpired = (reservation: Reservation): boolean => {
96 return isPast(reservation.expiryDate);
97};
98
99export const removeExpiredReservations = async (
100 chargingStation: ChargingStation,
101): Promise<void> => {
102 if (chargingStation.hasEvses) {
103 for (const evseStatus of chargingStation.evses.values()) {
104 for (const connectorStatus of evseStatus.connectors.values()) {
105 if (connectorStatus.reservation && hasReservationExpired(connectorStatus.reservation)) {
106 await chargingStation.removeReservation(
107 connectorStatus.reservation,
108 ReservationTerminationReason.EXPIRED,
109 );
110 }
111 }
112 }
113 } else {
114 for (const connectorStatus of chargingStation.connectors.values()) {
115 if (connectorStatus.reservation && hasReservationExpired(connectorStatus.reservation)) {
116 await chargingStation.removeReservation(
117 connectorStatus.reservation,
118 ReservationTerminationReason.EXPIRED,
119 );
120 }
121 }
122 }
123};
124
125export const getNumberOfReservableConnectors = (
126 connectors: Map<number, ConnectorStatus>,
127): number => {
cfc9875a 128 let numberOfReservableConnectors = 0;
fba11dc6
JB
129 for (const [connectorId, connectorStatus] of connectors) {
130 if (connectorId === 0) {
131 continue;
3fa7f799 132 }
fba11dc6 133 if (connectorStatus.status === ConnectorStatusEnum.Available) {
cfc9875a 134 ++numberOfReservableConnectors;
1bf29f5b 135 }
1bf29f5b 136 }
cfc9875a 137 return numberOfReservableConnectors;
fba11dc6
JB
138};
139
140export const getHashId = (index: number, stationTemplate: ChargingStationTemplate): string => {
141 const chargingStationInfo = {
142 chargePointModel: stationTemplate.chargePointModel,
143 chargePointVendor: stationTemplate.chargePointVendor,
144 ...(!isUndefined(stationTemplate.chargeBoxSerialNumberPrefix) && {
145 chargeBoxSerialNumber: stationTemplate.chargeBoxSerialNumberPrefix,
146 }),
147 ...(!isUndefined(stationTemplate.chargePointSerialNumberPrefix) && {
148 chargePointSerialNumber: stationTemplate.chargePointSerialNumberPrefix,
149 }),
150 ...(!isUndefined(stationTemplate.meterSerialNumberPrefix) && {
151 meterSerialNumber: stationTemplate.meterSerialNumberPrefix,
152 }),
153 ...(!isUndefined(stationTemplate.meterType) && {
154 meterType: stationTemplate.meterType,
155 }),
156 };
157 return createHash(Constants.DEFAULT_HASH_ALGORITHM)
158 .update(`${JSON.stringify(chargingStationInfo)}${getChargingStationId(index, stationTemplate)}`)
159 .digest('hex');
160};
161
162export const checkChargingStation = (
163 chargingStation: ChargingStation,
5edd8ba0 164 logPrefix: string,
fba11dc6
JB
165): boolean => {
166 if (chargingStation.started === false && chargingStation.starting === false) {
167 logger.warn(`${logPrefix} charging station is stopped, cannot proceed`);
168 return false;
169 }
170 return true;
171};
172
173export const getPhaseRotationValue = (
174 connectorId: number,
5edd8ba0 175 numberOfPhases: number,
fba11dc6
JB
176): string | undefined => {
177 // AC/DC
178 if (connectorId === 0 && numberOfPhases === 0) {
179 return `${connectorId}.${ConnectorPhaseRotation.RST}`;
180 } else if (connectorId > 0 && numberOfPhases === 0) {
181 return `${connectorId}.${ConnectorPhaseRotation.NotApplicable}`;
182 // AC
d181b12b 183 } else if (connectorId >= 0 && numberOfPhases === 1) {
fba11dc6 184 return `${connectorId}.${ConnectorPhaseRotation.NotApplicable}`;
d181b12b 185 } else if (connectorId >= 0 && numberOfPhases === 3) {
fba11dc6
JB
186 return `${connectorId}.${ConnectorPhaseRotation.RST}`;
187 }
188};
189
190export const getMaxNumberOfEvses = (evses: Record<string, EvseTemplate>): number => {
191 if (!evses) {
192 return -1;
193 }
194 return Object.keys(evses).length;
195};
196
197const getMaxNumberOfConnectors = (connectors: Record<string, ConnectorStatus>): number => {
198 if (!connectors) {
199 return -1;
200 }
201 return Object.keys(connectors).length;
202};
203
204export const getBootConnectorStatus = (
205 chargingStation: ChargingStation,
206 connectorId: number,
5edd8ba0 207 connectorStatus: ConnectorStatus,
fba11dc6
JB
208): ConnectorStatusEnum => {
209 let connectorBootStatus: ConnectorStatusEnum;
210 if (
211 !connectorStatus?.status &&
212 (chargingStation.isChargingStationAvailable() === false ||
213 chargingStation.isConnectorAvailable(connectorId) === false)
214 ) {
215 connectorBootStatus = ConnectorStatusEnum.Unavailable;
216 } else if (!connectorStatus?.status && connectorStatus?.bootStatus) {
217 // Set boot status in template at startup
218 connectorBootStatus = connectorStatus?.bootStatus;
219 } else if (connectorStatus?.status) {
220 // Set previous status at startup
221 connectorBootStatus = connectorStatus?.status;
222 } else {
223 // Set default status
224 connectorBootStatus = ConnectorStatusEnum.Available;
225 }
226 return connectorBootStatus;
227};
228
229export const checkTemplate = (
230 stationTemplate: ChargingStationTemplate,
231 logPrefix: string,
5edd8ba0 232 templateFile: string,
fba11dc6
JB
233): void => {
234 if (isNullOrUndefined(stationTemplate)) {
235 const errorMsg = `Failed to read charging station template file ${templateFile}`;
236 logger.error(`${logPrefix} ${errorMsg}`);
237 throw new BaseError(errorMsg);
238 }
239 if (isEmptyObject(stationTemplate)) {
240 const errorMsg = `Empty charging station information from template file ${templateFile}`;
241 logger.error(`${logPrefix} ${errorMsg}`);
242 throw new BaseError(errorMsg);
243 }
e1d9a0f4 244 if (isEmptyObject(stationTemplate.AutomaticTransactionGenerator!)) {
fba11dc6
JB
245 stationTemplate.AutomaticTransactionGenerator = Constants.DEFAULT_ATG_CONFIGURATION;
246 logger.warn(
247 `${logPrefix} Empty automatic transaction generator configuration from template file ${templateFile}, set to default: %j`,
5edd8ba0 248 Constants.DEFAULT_ATG_CONFIGURATION,
fba11dc6 249 );
fa7bccf4 250 }
fba11dc6
JB
251 if (isNullOrUndefined(stationTemplate.idTagsFile) || isEmptyString(stationTemplate.idTagsFile)) {
252 logger.warn(
5edd8ba0 253 `${logPrefix} Missing id tags file in template file ${templateFile}. That can lead to issues with the Automatic Transaction Generator`,
fba11dc6 254 );
c3b83130 255 }
fba11dc6
JB
256};
257
73edcc94
JB
258export const checkConfiguration = (
259 stationConfiguration: ChargingStationConfiguration | undefined,
260 logPrefix: string,
261 configurationFile: string,
262): void => {
263 if (isNullOrUndefined(stationConfiguration)) {
264 const errorMsg = `Failed to read charging station configuration file ${configurationFile}`;
265 logger.error(`${logPrefix} ${errorMsg}`);
266 throw new BaseError(errorMsg);
267 }
268 if (isEmptyObject(stationConfiguration!)) {
269 const errorMsg = `Empty charging station configuration from file ${configurationFile}`;
270 logger.error(`${logPrefix} ${errorMsg}`);
271 throw new BaseError(errorMsg);
272 }
273};
274
fba11dc6
JB
275export const checkConnectorsConfiguration = (
276 stationTemplate: ChargingStationTemplate,
277 logPrefix: string,
5edd8ba0 278 templateFile: string,
fba11dc6
JB
279): {
280 configuredMaxConnectors: number;
281 templateMaxConnectors: number;
282 templateMaxAvailableConnectors: number;
283} => {
cfc9875a 284 const configuredMaxConnectors = getConfiguredMaxNumberOfConnectors(stationTemplate);
fba11dc6 285 checkConfiguredMaxConnectors(configuredMaxConnectors, logPrefix, templateFile);
e1d9a0f4 286 const templateMaxConnectors = getMaxNumberOfConnectors(stationTemplate.Connectors!);
fba11dc6 287 checkTemplateMaxConnectors(templateMaxConnectors, logPrefix, templateFile);
1c9de2b9 288 const templateMaxAvailableConnectors = stationTemplate.Connectors?.[0]
fba11dc6
JB
289 ? templateMaxConnectors - 1
290 : templateMaxConnectors;
291 if (
292 configuredMaxConnectors > templateMaxAvailableConnectors &&
293 !stationTemplate?.randomConnectors
8a133cc8 294 ) {
fba11dc6 295 logger.warn(
5edd8ba0 296 `${logPrefix} Number of connectors exceeds the number of connector configurations in template ${templateFile}, forcing random connector configurations affectation`,
cda5d0fb 297 );
fba11dc6
JB
298 stationTemplate.randomConnectors = true;
299 }
300 return { configuredMaxConnectors, templateMaxConnectors, templateMaxAvailableConnectors };
301};
302
303export const checkStationInfoConnectorStatus = (
304 connectorId: number,
305 connectorStatus: ConnectorStatus,
306 logPrefix: string,
5edd8ba0 307 templateFile: string,
fba11dc6
JB
308): void => {
309 if (!isNullOrUndefined(connectorStatus?.status)) {
310 logger.warn(
5edd8ba0 311 `${logPrefix} Charging station information from template ${templateFile} with connector id ${connectorId} status configuration defined, undefine it`,
cda5d0fb 312 );
fba11dc6
JB
313 delete connectorStatus.status;
314 }
315};
316
317export const buildConnectorsMap = (
318 connectors: Record<string, ConnectorStatus>,
319 logPrefix: string,
5edd8ba0 320 templateFile: string,
fba11dc6
JB
321): Map<number, ConnectorStatus> => {
322 const connectorsMap = new Map<number, ConnectorStatus>();
323 if (getMaxNumberOfConnectors(connectors) > 0) {
324 for (const connector in connectors) {
325 const connectorStatus = connectors[connector];
326 const connectorId = convertToInt(connector);
327 checkStationInfoConnectorStatus(connectorId, connectorStatus, logPrefix, templateFile);
328 connectorsMap.set(connectorId, cloneObject<ConnectorStatus>(connectorStatus));
fa7bccf4 329 }
fba11dc6
JB
330 } else {
331 logger.warn(
5edd8ba0 332 `${logPrefix} Charging station information from template ${templateFile} with no connectors, cannot build connectors map`,
fba11dc6 333 );
fa7bccf4 334 }
fba11dc6
JB
335 return connectorsMap;
336};
fa7bccf4 337
fba11dc6
JB
338export const initializeConnectorsMapStatus = (
339 connectors: Map<number, ConnectorStatus>,
5edd8ba0 340 logPrefix: string,
fba11dc6
JB
341): void => {
342 for (const connectorId of connectors.keys()) {
343 if (connectorId > 0 && connectors.get(connectorId)?.transactionStarted === true) {
04b1261c 344 logger.warn(
5edd8ba0
JB
345 `${logPrefix} Connector id ${connectorId} at initialization has a transaction started with id ${connectors.get(
346 connectorId,
347 )?.transactionId}`,
04b1261c 348 );
04b1261c 349 }
fba11dc6 350 if (connectorId === 0) {
e1d9a0f4 351 connectors.get(connectorId)!.availability = AvailabilityType.Operative;
fba11dc6 352 if (isUndefined(connectors.get(connectorId)?.chargingProfiles)) {
e1d9a0f4 353 connectors.get(connectorId)!.chargingProfiles = [];
04b1261c 354 }
fba11dc6
JB
355 } else if (
356 connectorId > 0 &&
357 isNullOrUndefined(connectors.get(connectorId)?.transactionStarted)
358 ) {
e1d9a0f4 359 initializeConnectorStatus(connectors.get(connectorId)!);
04b1261c
JB
360 }
361 }
fba11dc6
JB
362};
363
364export const resetConnectorStatus = (connectorStatus: ConnectorStatus): void => {
86f51b96
JB
365 connectorStatus.chargingProfiles =
366 connectorStatus.transactionId && isNotEmptyArray(connectorStatus.chargingProfiles)
367 ? connectorStatus.chargingProfiles?.filter(
368 (chargingProfile) => chargingProfile.transactionId !== connectorStatus.transactionId,
369 )
370 : [];
fba11dc6
JB
371 connectorStatus.idTagLocalAuthorized = false;
372 connectorStatus.idTagAuthorized = false;
373 connectorStatus.transactionRemoteStarted = false;
374 connectorStatus.transactionStarted = false;
a71d4e70
JB
375 delete connectorStatus?.transactionStart;
376 delete connectorStatus?.transactionId;
fba11dc6
JB
377 delete connectorStatus?.localAuthorizeIdTag;
378 delete connectorStatus?.authorizeIdTag;
fba11dc6
JB
379 delete connectorStatus?.transactionIdTag;
380 connectorStatus.transactionEnergyActiveImportRegisterValue = 0;
381 delete connectorStatus?.transactionBeginMeterValue;
382};
383
384export const createBootNotificationRequest = (
385 stationInfo: ChargingStationInfo,
5edd8ba0 386 bootReason: BootReasonEnumType = BootReasonEnumType.PowerUp,
fba11dc6 387): BootNotificationRequest => {
5398cecf 388 const ocppVersion = stationInfo.ocppVersion!;
fba11dc6
JB
389 switch (ocppVersion) {
390 case OCPPVersion.VERSION_16:
391 return {
392 chargePointModel: stationInfo.chargePointModel,
393 chargePointVendor: stationInfo.chargePointVendor,
394 ...(!isUndefined(stationInfo.chargeBoxSerialNumber) && {
395 chargeBoxSerialNumber: stationInfo.chargeBoxSerialNumber,
396 }),
397 ...(!isUndefined(stationInfo.chargePointSerialNumber) && {
398 chargePointSerialNumber: stationInfo.chargePointSerialNumber,
399 }),
400 ...(!isUndefined(stationInfo.firmwareVersion) && {
401 firmwareVersion: stationInfo.firmwareVersion,
402 }),
403 ...(!isUndefined(stationInfo.iccid) && { iccid: stationInfo.iccid }),
404 ...(!isUndefined(stationInfo.imsi) && { imsi: stationInfo.imsi }),
405 ...(!isUndefined(stationInfo.meterSerialNumber) && {
406 meterSerialNumber: stationInfo.meterSerialNumber,
407 }),
408 ...(!isUndefined(stationInfo.meterType) && {
409 meterType: stationInfo.meterType,
410 }),
411 } as OCPP16BootNotificationRequest;
412 case OCPPVersion.VERSION_20:
413 case OCPPVersion.VERSION_201:
414 return {
415 reason: bootReason,
416 chargingStation: {
417 model: stationInfo.chargePointModel,
418 vendorName: stationInfo.chargePointVendor,
9bf0ef23 419 ...(!isUndefined(stationInfo.firmwareVersion) && {
d270cc87
JB
420 firmwareVersion: stationInfo.firmwareVersion,
421 }),
fba11dc6
JB
422 ...(!isUndefined(stationInfo.chargeBoxSerialNumber) && {
423 serialNumber: stationInfo.chargeBoxSerialNumber,
d270cc87 424 }),
fba11dc6
JB
425 ...((!isUndefined(stationInfo.iccid) || !isUndefined(stationInfo.imsi)) && {
426 modem: {
427 ...(!isUndefined(stationInfo.iccid) && { iccid: stationInfo.iccid }),
428 ...(!isUndefined(stationInfo.imsi) && { imsi: stationInfo.imsi }),
429 },
d270cc87 430 }),
fba11dc6
JB
431 },
432 } as OCPP20BootNotificationRequest;
433 }
434};
435
436export const warnTemplateKeysDeprecation = (
437 stationTemplate: ChargingStationTemplate,
438 logPrefix: string,
5edd8ba0 439 templateFile: string,
fba11dc6 440) => {
e4c6cf05
JB
441 const templateKeys: { deprecatedKey: string; key?: string }[] = [
442 { deprecatedKey: 'supervisionUrl', key: 'supervisionUrls' },
443 { deprecatedKey: 'authorizationFile', key: 'idTagsFile' },
444 { deprecatedKey: 'payloadSchemaValidation', key: 'ocppStrictCompliance' },
cfdf901d 445 { deprecatedKey: 'mustAuthorizeAtRemoteStart', key: 'remoteAuthorization' },
fba11dc6
JB
446 ];
447 for (const templateKey of templateKeys) {
448 warnDeprecatedTemplateKey(
449 stationTemplate,
450 templateKey.deprecatedKey,
451 logPrefix,
452 templateFile,
e1d9a0f4 453 !isUndefined(templateKey.key) ? `Use '${templateKey.key}' instead` : undefined,
fba11dc6
JB
454 );
455 convertDeprecatedTemplateKey(stationTemplate, templateKey.deprecatedKey, templateKey.key);
456 }
457};
458
459export const stationTemplateToStationInfo = (
5edd8ba0 460 stationTemplate: ChargingStationTemplate,
fba11dc6
JB
461): ChargingStationInfo => {
462 stationTemplate = cloneObject<ChargingStationTemplate>(stationTemplate);
463 delete stationTemplate.power;
464 delete stationTemplate.powerUnit;
e1d9a0f4
JB
465 delete stationTemplate.Connectors;
466 delete stationTemplate.Evses;
fba11dc6
JB
467 delete stationTemplate.Configuration;
468 delete stationTemplate.AutomaticTransactionGenerator;
469 delete stationTemplate.chargeBoxSerialNumberPrefix;
470 delete stationTemplate.chargePointSerialNumberPrefix;
471 delete stationTemplate.meterSerialNumberPrefix;
66b537dc 472 return stationTemplate as ChargingStationInfo;
fba11dc6
JB
473};
474
475export const createSerialNumber = (
476 stationTemplate: ChargingStationTemplate,
477 stationInfo: ChargingStationInfo,
7f3decca 478 params?: {
fba11dc6
JB
479 randomSerialNumberUpperCase?: boolean;
480 randomSerialNumber?: boolean;
5edd8ba0 481 },
fba11dc6
JB
482): void => {
483 params = { ...{ randomSerialNumberUpperCase: true, randomSerialNumber: true }, ...params };
484 const serialNumberSuffix = params?.randomSerialNumber
485 ? getRandomSerialNumberSuffix({
486 upperCase: params.randomSerialNumberUpperCase,
487 })
488 : '';
489 isNotEmptyString(stationTemplate?.chargePointSerialNumberPrefix) &&
490 (stationInfo.chargePointSerialNumber = `${stationTemplate.chargePointSerialNumberPrefix}${serialNumberSuffix}`);
491 isNotEmptyString(stationTemplate?.chargeBoxSerialNumberPrefix) &&
492 (stationInfo.chargeBoxSerialNumber = `${stationTemplate.chargeBoxSerialNumberPrefix}${serialNumberSuffix}`);
493 isNotEmptyString(stationTemplate?.meterSerialNumberPrefix) &&
494 (stationInfo.meterSerialNumber = `${stationTemplate.meterSerialNumberPrefix}${serialNumberSuffix}`);
495};
496
497export const propagateSerialNumber = (
498 stationTemplate: ChargingStationTemplate,
499 stationInfoSrc: ChargingStationInfo,
5edd8ba0 500 stationInfoDst: ChargingStationInfo,
fba11dc6
JB
501) => {
502 if (!stationInfoSrc || !stationTemplate) {
503 throw new BaseError(
5edd8ba0 504 'Missing charging station template or existing configuration to propagate serial number',
fba11dc6 505 );
17ac262c 506 }
fba11dc6
JB
507 stationTemplate?.chargePointSerialNumberPrefix && stationInfoSrc?.chargePointSerialNumber
508 ? (stationInfoDst.chargePointSerialNumber = stationInfoSrc.chargePointSerialNumber)
509 : stationInfoDst?.chargePointSerialNumber && delete stationInfoDst.chargePointSerialNumber;
510 stationTemplate?.chargeBoxSerialNumberPrefix && stationInfoSrc?.chargeBoxSerialNumber
511 ? (stationInfoDst.chargeBoxSerialNumber = stationInfoSrc.chargeBoxSerialNumber)
512 : stationInfoDst?.chargeBoxSerialNumber && delete stationInfoDst.chargeBoxSerialNumber;
513 stationTemplate?.meterSerialNumberPrefix && stationInfoSrc?.meterSerialNumber
514 ? (stationInfoDst.meterSerialNumber = stationInfoSrc.meterSerialNumber)
515 : stationInfoDst?.meterSerialNumber && delete stationInfoDst.meterSerialNumber;
516};
517
d8093be1
JB
518export const hasFeatureProfile = (
519 chargingStation: ChargingStation,
520 featureProfile: SupportedFeatureProfiles,
521): boolean | undefined => {
522 return getConfigurationKey(
523 chargingStation,
524 StandardParametersKey.SupportedFeatureProfiles,
525 )?.value?.includes(featureProfile);
526};
527
fba11dc6
JB
528export const getAmperageLimitationUnitDivider = (stationInfo: ChargingStationInfo): number => {
529 let unitDivider = 1;
530 switch (stationInfo.amperageLimitationUnit) {
531 case AmpereUnits.DECI_AMPERE:
532 unitDivider = 10;
533 break;
534 case AmpereUnits.CENTI_AMPERE:
535 unitDivider = 100;
536 break;
537 case AmpereUnits.MILLI_AMPERE:
538 unitDivider = 1000;
539 break;
540 }
541 return unitDivider;
542};
543
6fc0c6f3
JB
544/**
545 * Gets the connector cloned charging profiles applying a power limitation
21ee4dc2 546 * and sorted by connector id descending then stack level descending
6fc0c6f3
JB
547 *
548 * @param chargingStation -
549 * @param connectorId -
550 * @returns connector charging profiles array
551 */
552export const getConnectorChargingProfiles = (
553 chargingStation: ChargingStation,
554 connectorId: number,
555) => {
556 return cloneObject<ChargingProfile[]>(
21ee4dc2 557 (chargingStation.getConnectorStatus(connectorId)?.chargingProfiles ?? [])
6fc0c6f3
JB
558 .sort((a, b) => b.stackLevel - a.stackLevel)
559 .concat(
21ee4dc2 560 (chargingStation.getConnectorStatus(0)?.chargingProfiles ?? []).sort(
6fc0c6f3
JB
561 (a, b) => b.stackLevel - a.stackLevel,
562 ),
563 ),
564 );
565};
566
fba11dc6
JB
567export const getChargingStationConnectorChargingProfilesPowerLimit = (
568 chargingStation: ChargingStation,
5edd8ba0 569 connectorId: number,
fba11dc6 570): number | undefined => {
bbb55ee4 571 let limit: number | undefined, chargingProfile: ChargingProfile | undefined;
6fc0c6f3
JB
572 // Get charging profiles sorted by connector id then stack level
573 const chargingProfiles = getConnectorChargingProfiles(chargingStation, connectorId);
fba11dc6 574 if (isNotEmptyArray(chargingProfiles)) {
a71d4e70
JB
575 const result = getLimitFromChargingProfiles(
576 chargingStation,
577 connectorId,
578 chargingProfiles,
579 chargingStation.logPrefix(),
580 );
fba11dc6
JB
581 if (!isNullOrUndefined(result)) {
582 limit = result?.limit;
bbb55ee4 583 chargingProfile = result?.chargingProfile;
5398cecf 584 switch (chargingStation.stationInfo?.currentOutType) {
fba11dc6
JB
585 case CurrentType.AC:
586 limit =
bbb55ee4 587 chargingProfile?.chargingSchedule?.chargingRateUnit === ChargingRateUnitType.WATT
fba11dc6
JB
588 ? limit
589 : ACElectricUtils.powerTotal(
590 chargingStation.getNumberOfPhases(),
5398cecf 591 chargingStation.stationInfo.voltageOut!,
e1d9a0f4 592 limit!,
fba11dc6
JB
593 );
594 break;
595 case CurrentType.DC:
596 limit =
bbb55ee4 597 chargingProfile?.chargingSchedule?.chargingRateUnit === ChargingRateUnitType.WATT
fba11dc6 598 ? limit
5398cecf 599 : DCElectricUtils.power(chargingStation.stationInfo.voltageOut!, limit!);
fba11dc6
JB
600 }
601 const connectorMaximumPower =
5398cecf 602 chargingStation.stationInfo.maximumPower! / chargingStation.powerDivider;
e1d9a0f4 603 if (limit! > connectorMaximumPower) {
fba11dc6 604 logger.error(
bbb55ee4 605 `${chargingStation.logPrefix()} ${moduleName}.getChargingStationConnectorChargingProfilesPowerLimit: Charging profile id ${chargingProfile?.chargingProfileId} limit ${limit} is greater than connector id ${connectorId} maximum ${connectorMaximumPower}: %j`,
5edd8ba0 606 result,
fba11dc6
JB
607 );
608 limit = connectorMaximumPower;
15068be9
JB
609 }
610 }
15068be9 611 }
fba11dc6
JB
612 return limit;
613};
614
615export const getDefaultVoltageOut = (
616 currentType: CurrentType,
617 logPrefix: string,
5edd8ba0 618 templateFile: string,
fba11dc6
JB
619): Voltage => {
620 const errorMsg = `Unknown ${currentType} currentOutType in template file ${templateFile}, cannot define default voltage out`;
621 let defaultVoltageOut: number;
622 switch (currentType) {
623 case CurrentType.AC:
624 defaultVoltageOut = Voltage.VOLTAGE_230;
625 break;
626 case CurrentType.DC:
627 defaultVoltageOut = Voltage.VOLTAGE_400;
628 break;
629 default:
630 logger.error(`${logPrefix} ${errorMsg}`);
631 throw new BaseError(errorMsg);
15068be9 632 }
fba11dc6
JB
633 return defaultVoltageOut;
634};
635
636export const getIdTagsFile = (stationInfo: ChargingStationInfo): string | undefined => {
637 return (
638 stationInfo.idTagsFile &&
639 join(dirname(fileURLToPath(import.meta.url)), 'assets', basename(stationInfo.idTagsFile))
640 );
641};
642
b2b60626 643export const waitChargingStationEvents = async (
fba11dc6
JB
644 emitter: EventEmitter,
645 event: ChargingStationWorkerMessageEvents,
5edd8ba0 646 eventsToWait: number,
fba11dc6 647): Promise<number> => {
474d4ffc 648 return new Promise<number>((resolve) => {
fba11dc6
JB
649 let events = 0;
650 if (eventsToWait === 0) {
651 resolve(events);
2def357c 652 return;
fba11dc6
JB
653 }
654 emitter.on(event, () => {
655 ++events;
656 if (events === eventsToWait) {
b1f1b0f6
JB
657 resolve(events);
658 }
b1f1b0f6 659 });
fba11dc6
JB
660 });
661};
662
cfc9875a
JB
663const getConfiguredMaxNumberOfConnectors = (stationTemplate: ChargingStationTemplate): number => {
664 let configuredMaxNumberOfConnectors = 0;
fba11dc6
JB
665 if (isNotEmptyArray(stationTemplate.numberOfConnectors) === true) {
666 const numberOfConnectors = stationTemplate.numberOfConnectors as number[];
cfc9875a 667 configuredMaxNumberOfConnectors =
fba11dc6
JB
668 numberOfConnectors[Math.floor(secureRandom() * numberOfConnectors.length)];
669 } else if (isUndefined(stationTemplate.numberOfConnectors) === false) {
cfc9875a 670 configuredMaxNumberOfConnectors = stationTemplate.numberOfConnectors as number;
fba11dc6 671 } else if (stationTemplate.Connectors && !stationTemplate.Evses) {
1c9de2b9 672 configuredMaxNumberOfConnectors = stationTemplate.Connectors?.[0]
fba11dc6
JB
673 ? getMaxNumberOfConnectors(stationTemplate.Connectors) - 1
674 : getMaxNumberOfConnectors(stationTemplate.Connectors);
675 } else if (stationTemplate.Evses && !stationTemplate.Connectors) {
fba11dc6
JB
676 for (const evse in stationTemplate.Evses) {
677 if (evse === '0') {
678 continue;
cda5d0fb 679 }
cfc9875a
JB
680 configuredMaxNumberOfConnectors += getMaxNumberOfConnectors(
681 stationTemplate.Evses[evse].Connectors,
682 );
cda5d0fb 683 }
cda5d0fb 684 }
cfc9875a 685 return configuredMaxNumberOfConnectors;
fba11dc6
JB
686};
687
688const checkConfiguredMaxConnectors = (
689 configuredMaxConnectors: number,
690 logPrefix: string,
5edd8ba0 691 templateFile: string,
fba11dc6
JB
692): void => {
693 if (configuredMaxConnectors <= 0) {
694 logger.warn(
5edd8ba0 695 `${logPrefix} Charging station information from template ${templateFile} with ${configuredMaxConnectors} connectors`,
fba11dc6 696 );
cda5d0fb 697 }
fba11dc6 698};
cda5d0fb 699
fba11dc6
JB
700const checkTemplateMaxConnectors = (
701 templateMaxConnectors: number,
702 logPrefix: string,
5edd8ba0 703 templateFile: string,
fba11dc6
JB
704): void => {
705 if (templateMaxConnectors === 0) {
706 logger.warn(
5edd8ba0 707 `${logPrefix} Charging station information from template ${templateFile} with empty connectors configuration`,
fba11dc6
JB
708 );
709 } else if (templateMaxConnectors < 0) {
710 logger.error(
5edd8ba0 711 `${logPrefix} Charging station information from template ${templateFile} with no connectors configuration defined`,
fba11dc6
JB
712 );
713 }
714};
715
716const initializeConnectorStatus = (connectorStatus: ConnectorStatus): void => {
717 connectorStatus.availability = AvailabilityType.Operative;
718 connectorStatus.idTagLocalAuthorized = false;
719 connectorStatus.idTagAuthorized = false;
720 connectorStatus.transactionRemoteStarted = false;
721 connectorStatus.transactionStarted = false;
722 connectorStatus.energyActiveImportRegisterValue = 0;
723 connectorStatus.transactionEnergyActiveImportRegisterValue = 0;
724 if (isUndefined(connectorStatus.chargingProfiles)) {
725 connectorStatus.chargingProfiles = [];
726 }
727};
728
729const warnDeprecatedTemplateKey = (
730 template: ChargingStationTemplate,
731 key: string,
732 logPrefix: string,
733 templateFile: string,
5edd8ba0 734 logMsgToAppend = '',
fba11dc6 735): void => {
1c9de2b9 736 if (!isUndefined(template?.[key as keyof ChargingStationTemplate])) {
fba11dc6
JB
737 const logMsg = `Deprecated template key '${key}' usage in file '${templateFile}'${
738 isNotEmptyString(logMsgToAppend) ? `. ${logMsgToAppend}` : ''
739 }`;
740 logger.warn(`${logPrefix} ${logMsg}`);
a418c77b 741 console.warn(`${chalk.green(logPrefix)} ${chalk.yellow(logMsg)}`);
fba11dc6
JB
742 }
743};
744
745const convertDeprecatedTemplateKey = (
746 template: ChargingStationTemplate,
747 deprecatedKey: string,
e1d9a0f4 748 key?: string,
fba11dc6 749): void => {
1c9de2b9 750 if (!isUndefined(template?.[deprecatedKey as keyof ChargingStationTemplate])) {
e1d9a0f4 751 if (!isUndefined(key)) {
a37fc6dc
JB
752 (template as unknown as Record<string, unknown>)[key!] =
753 template[deprecatedKey as keyof ChargingStationTemplate];
e1d9a0f4 754 }
a37fc6dc 755 delete template[deprecatedKey as keyof ChargingStationTemplate];
fba11dc6
JB
756 }
757};
758
947f048a
JB
759interface ChargingProfilesLimit {
760 limit: number;
bbb55ee4 761 chargingProfile: ChargingProfile;
947f048a
JB
762}
763
fba11dc6 764/**
21ee4dc2 765 * Charging profiles shall already be sorted by connector id descending then stack level descending
fba11dc6 766 *
d467756c
JB
767 * @param chargingStation -
768 * @param connectorId -
fba11dc6
JB
769 * @param chargingProfiles -
770 * @param logPrefix -
947f048a 771 * @returns ChargingProfilesLimit
fba11dc6
JB
772 */
773const getLimitFromChargingProfiles = (
a71d4e70
JB
774 chargingStation: ChargingStation,
775 connectorId: number,
fba11dc6 776 chargingProfiles: ChargingProfile[],
5edd8ba0 777 logPrefix: string,
947f048a 778): ChargingProfilesLimit | undefined => {
fba11dc6 779 const debugLogMsg = `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Matching charging profile found for power limitation: %j`;
fba11dc6 780 const currentDate = new Date();
0eb666db 781 const connectorStatus = chargingStation.getConnectorStatus(connectorId)!;
fba11dc6 782 for (const chargingProfile of chargingProfiles) {
fba11dc6 783 const chargingSchedule = chargingProfile.chargingSchedule;
da332e70 784 if (isNullOrUndefined(chargingSchedule?.startSchedule) && connectorStatus?.transactionStarted) {
109c677a 785 logger.debug(
ec4a242a 786 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} has no startSchedule defined. Trying to set it to the connector current transaction start date`,
cda5d0fb 787 );
a71d4e70 788 // OCPP specifies that if startSchedule is not defined, it should be relative to start of the connector transaction
b5c19509 789 chargingSchedule.startSchedule = connectorStatus?.transactionStart;
52952bf8 790 }
ef9e3b33
JB
791 if (
792 !isNullOrUndefined(chargingSchedule?.startSchedule) &&
793 !isDate(chargingSchedule?.startSchedule)
794 ) {
795 logger.warn(
796 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} startSchedule property is not a Date instance. Trying to convert it to a Date instance`,
797 );
798 chargingSchedule.startSchedule = convertToDate(chargingSchedule?.startSchedule)!;
799 }
da332e70
JB
800 if (
801 !isNullOrUndefined(chargingSchedule?.startSchedule) &&
802 isNullOrUndefined(chargingSchedule?.duration)
803 ) {
804 logger.debug(
805 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} has no duration defined and will be set to the maximum time allowed`,
806 );
807 // OCPP specifies that if duration is not defined, it should be infinite
808 chargingSchedule.duration = differenceInSeconds(maxTime, chargingSchedule.startSchedule!);
809 }
0eb666db
JB
810 if (!prepareChargingProfileKind(connectorStatus, chargingProfile, currentDate, logPrefix)) {
811 continue;
ec4a242a 812 }
0bd926c1 813 if (!canProceedChargingProfile(chargingProfile, currentDate, logPrefix)) {
142a66c9
JB
814 continue;
815 }
fba11dc6
JB
816 // Check if the charging profile is active
817 if (
975e18ec
JB
818 isWithinInterval(currentDate, {
819 start: chargingSchedule.startSchedule!,
820 end: addSeconds(chargingSchedule.startSchedule!, chargingSchedule.duration!),
821 })
fba11dc6 822 ) {
252a7d22 823 if (isNotEmptyArray(chargingSchedule.chargingSchedulePeriod)) {
80c58041
JB
824 const chargingSchedulePeriodCompareFn = (
825 a: ChargingSchedulePeriod,
826 b: ChargingSchedulePeriod,
827 ) => a.startPeriod - b.startPeriod;
828 if (
6fc0c6f3 829 !isArraySorted<ChargingSchedulePeriod>(
80c58041
JB
830 chargingSchedule.chargingSchedulePeriod,
831 chargingSchedulePeriodCompareFn,
6fc0c6f3 832 )
80c58041
JB
833 ) {
834 logger.warn(
835 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} schedule periods are not sorted by start period`,
836 );
837 chargingSchedule.chargingSchedulePeriod.sort(chargingSchedulePeriodCompareFn);
838 }
da332e70 839 // Check if the first schedule period startPeriod property is equal to 0
55f2ab60
JB
840 if (chargingSchedule.chargingSchedulePeriod[0].startPeriod !== 0) {
841 logger.error(
842 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} first schedule period start period ${chargingSchedule.chargingSchedulePeriod[0].startPeriod} is not equal to 0`,
843 );
975e18ec 844 continue;
55f2ab60 845 }
991fb26b 846 // Handle only one schedule period
975e18ec 847 if (chargingSchedule.chargingSchedulePeriod.length === 1) {
252a7d22
JB
848 const result: ChargingProfilesLimit = {
849 limit: chargingSchedule.chargingSchedulePeriod[0].limit,
bbb55ee4 850 chargingProfile,
fba11dc6
JB
851 };
852 logger.debug(debugLogMsg, result);
853 return result;
41189456 854 }
e3037969 855 let previousChargingSchedulePeriod: ChargingSchedulePeriod | undefined;
252a7d22 856 // Search for the right schedule period
e3037969
JB
857 for (const [
858 index,
859 chargingSchedulePeriod,
860 ] of chargingSchedule.chargingSchedulePeriod.entries()) {
252a7d22
JB
861 // Find the right schedule period
862 if (
863 isAfter(
e3037969 864 addSeconds(chargingSchedule.startSchedule!, chargingSchedulePeriod.startPeriod),
252a7d22
JB
865 currentDate,
866 )
867 ) {
e3037969 868 // Found the schedule period: previous is the correct one
252a7d22 869 const result: ChargingProfilesLimit = {
e3037969 870 limit: previousChargingSchedulePeriod!.limit,
6fc0c6f3 871 chargingProfile,
252a7d22
JB
872 };
873 logger.debug(debugLogMsg, result);
874 return result;
875 }
e3037969
JB
876 // Keep a reference to previous one
877 previousChargingSchedulePeriod = chargingSchedulePeriod;
975e18ec 878 // Handle the last schedule period within the charging profile duration
252a7d22 879 if (
975e18ec
JB
880 index === chargingSchedule.chargingSchedulePeriod.length - 1 ||
881 (index < chargingSchedule.chargingSchedulePeriod.length - 1 &&
ccfa30bc
JB
882 differenceInSeconds(
883 addSeconds(
d9dc6292 884 chargingSchedule.startSchedule!,
ccfa30bc
JB
885 chargingSchedule.chargingSchedulePeriod[index + 1].startPeriod,
886 ),
887 chargingSchedule.startSchedule!,
888 ) > chargingSchedule.duration!)
252a7d22
JB
889 ) {
890 const result: ChargingProfilesLimit = {
e3037969 891 limit: previousChargingSchedulePeriod.limit,
6fc0c6f3 892 chargingProfile,
252a7d22
JB
893 };
894 logger.debug(debugLogMsg, result);
895 return result;
896 }
17ac262c
JB
897 }
898 }
899 }
17ac262c 900 }
fba11dc6 901};
17ac262c 902
0eb666db
JB
903export const prepareChargingProfileKind = (
904 connectorStatus: ConnectorStatus,
905 chargingProfile: ChargingProfile,
906 currentDate: Date,
907 logPrefix: string,
908): boolean => {
909 switch (chargingProfile.chargingProfileKind) {
910 case ChargingProfileKindType.RECURRING:
911 if (!canProceedRecurringChargingProfile(chargingProfile, logPrefix)) {
912 return false;
913 }
914 prepareRecurringChargingProfile(chargingProfile, currentDate, logPrefix);
915 break;
916 case ChargingProfileKindType.RELATIVE:
ccfa30bc
JB
917 if (!isNullOrUndefined(chargingProfile.chargingSchedule.startSchedule)) {
918 logger.warn(
320d07e3 919 `${logPrefix} ${moduleName}.prepareChargingProfileKind: Relative charging profile id ${chargingProfile.chargingProfileId} has a startSchedule property defined. It will be ignored or used if the connector has a transaction started`,
ccfa30bc
JB
920 );
921 delete chargingProfile.chargingSchedule.startSchedule;
922 }
ef9e3b33
JB
923 if (connectorStatus?.transactionStarted) {
924 chargingProfile.chargingSchedule.startSchedule = connectorStatus?.transactionStart;
925 }
926 // FIXME: Handle relative charging profile duration
0eb666db
JB
927 break;
928 }
929 return true;
930};
931
ad490d5f 932export const canProceedChargingProfile = (
0bd926c1
JB
933 chargingProfile: ChargingProfile,
934 currentDate: Date,
935 logPrefix: string,
936): boolean => {
937 if (
938 (isValidTime(chargingProfile.validFrom) && isBefore(currentDate, chargingProfile.validFrom!)) ||
939 (isValidTime(chargingProfile.validTo) && isAfter(currentDate, chargingProfile.validTo!))
940 ) {
941 logger.debug(
942 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${
943 chargingProfile.chargingProfileId
944 } is not valid for the current date ${currentDate.toISOString()}`,
945 );
946 return false;
947 }
ef9e3b33
JB
948 if (
949 isNullOrUndefined(chargingProfile.chargingSchedule.startSchedule) ||
950 isNullOrUndefined(chargingProfile.chargingSchedule.duration)
951 ) {
0bd926c1 952 logger.error(
ef9e3b33
JB
953 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId} has no startSchedule or duration defined`,
954 );
955 return false;
956 }
957 if (
958 !isNullOrUndefined(chargingProfile.chargingSchedule.startSchedule) &&
959 !isValidTime(chargingProfile.chargingSchedule.startSchedule)
960 ) {
961 logger.error(
962 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId} has an invalid startSchedule date defined`,
963 );
964 return false;
965 }
966 if (
967 !isNullOrUndefined(chargingProfile.chargingSchedule.duration) &&
968 !Number.isSafeInteger(chargingProfile.chargingSchedule.duration)
969 ) {
970 logger.error(
971 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId} has non integer duration defined`,
0bd926c1 972 );
0bd926c1
JB
973 return false;
974 }
975 return true;
976};
977
0eb666db 978const canProceedRecurringChargingProfile = (
0bd926c1
JB
979 chargingProfile: ChargingProfile,
980 logPrefix: string,
981): boolean => {
982 if (
983 chargingProfile.chargingProfileKind === ChargingProfileKindType.RECURRING &&
984 isNullOrUndefined(chargingProfile.recurrencyKind)
985 ) {
986 logger.error(
987 `${logPrefix} ${moduleName}.canProceedRecurringChargingProfile: Recurring charging profile id ${chargingProfile.chargingProfileId} has no recurrencyKind defined`,
988 );
989 return false;
990 }
d929adcc
JB
991 if (
992 chargingProfile.chargingProfileKind === ChargingProfileKindType.RECURRING &&
993 isNullOrUndefined(chargingProfile.chargingSchedule.startSchedule)
994 ) {
ef9e3b33
JB
995 logger.error(
996 `${logPrefix} ${moduleName}.canProceedRecurringChargingProfile: Recurring charging profile id ${chargingProfile.chargingProfileId} has no startSchedule defined`,
997 );
998 return false;
999 }
0bd926c1
JB
1000 return true;
1001};
1002
522e4b05 1003/**
ec4a242a 1004 * Adjust recurring charging profile startSchedule to the current recurrency time interval if needed
522e4b05
JB
1005 *
1006 * @param chargingProfile -
1007 * @param currentDate -
1008 * @param logPrefix -
1009 */
0eb666db 1010const prepareRecurringChargingProfile = (
76dab5a9
JB
1011 chargingProfile: ChargingProfile,
1012 currentDate: Date,
1013 logPrefix: string,
ec4a242a 1014): boolean => {
76dab5a9 1015 const chargingSchedule = chargingProfile.chargingSchedule;
ec4a242a 1016 let recurringIntervalTranslated = false;
522e4b05 1017 let recurringInterval: Interval;
76dab5a9
JB
1018 switch (chargingProfile.recurrencyKind) {
1019 case RecurrencyKindType.DAILY:
522e4b05
JB
1020 recurringInterval = {
1021 start: chargingSchedule.startSchedule!,
1022 end: addDays(chargingSchedule.startSchedule!, 1),
1023 };
d476bc1b 1024 checkRecurringChargingProfileDuration(chargingProfile, recurringInterval, logPrefix);
522e4b05
JB
1025 if (
1026 !isWithinInterval(currentDate, recurringInterval) &&
991fb26b 1027 isBefore(recurringInterval.end, currentDate)
522e4b05
JB
1028 ) {
1029 chargingSchedule.startSchedule = addDays(
991fb26b 1030 recurringInterval.start,
05b52716 1031 differenceInDays(currentDate, recurringInterval.start),
76dab5a9 1032 );
522e4b05
JB
1033 recurringInterval = {
1034 start: chargingSchedule.startSchedule,
1035 end: addDays(chargingSchedule.startSchedule, 1),
1036 };
ec4a242a 1037 recurringIntervalTranslated = true;
76dab5a9
JB
1038 }
1039 break;
1040 case RecurrencyKindType.WEEKLY:
522e4b05
JB
1041 recurringInterval = {
1042 start: chargingSchedule.startSchedule!,
1043 end: addWeeks(chargingSchedule.startSchedule!, 1),
1044 };
d476bc1b 1045 checkRecurringChargingProfileDuration(chargingProfile, recurringInterval, logPrefix);
522e4b05
JB
1046 if (
1047 !isWithinInterval(currentDate, recurringInterval) &&
991fb26b 1048 isBefore(recurringInterval.end, currentDate)
522e4b05
JB
1049 ) {
1050 chargingSchedule.startSchedule = addWeeks(
991fb26b 1051 recurringInterval.start,
05b52716 1052 differenceInWeeks(currentDate, recurringInterval.start),
76dab5a9 1053 );
522e4b05
JB
1054 recurringInterval = {
1055 start: chargingSchedule.startSchedule,
1056 end: addWeeks(chargingSchedule.startSchedule, 1),
1057 };
ec4a242a 1058 recurringIntervalTranslated = true;
76dab5a9
JB
1059 }
1060 break;
ec4a242a
JB
1061 default:
1062 logger.error(
320d07e3 1063 `${logPrefix} ${moduleName}.prepareRecurringChargingProfile: Recurring ${chargingProfile.recurrencyKind} charging profile id ${chargingProfile.chargingProfileId} is not supported`,
ec4a242a 1064 );
76dab5a9 1065 }
ec4a242a 1066 if (recurringIntervalTranslated && !isWithinInterval(currentDate, recurringInterval!)) {
522e4b05 1067 logger.error(
aa5c5ad4 1068 `${logPrefix} ${moduleName}.prepareRecurringChargingProfile: Recurring ${
522e4b05 1069 chargingProfile.recurrencyKind
991fb26b 1070 } charging profile id ${chargingProfile.chargingProfileId} recurrency time interval [${toDate(
522e4b05 1071 recurringInterval!.start,
991fb26b
JB
1072 ).toISOString()}, ${toDate(
1073 recurringInterval!.end,
ec4a242a 1074 ).toISOString()}] has not been properly translated to current date ${currentDate.toISOString()} `,
522e4b05
JB
1075 );
1076 }
ec4a242a 1077 return recurringIntervalTranslated;
76dab5a9
JB
1078};
1079
d476bc1b
JB
1080const checkRecurringChargingProfileDuration = (
1081 chargingProfile: ChargingProfile,
1082 interval: Interval,
1083 logPrefix: string,
ec4a242a 1084): void => {
142a66c9
JB
1085 if (isNullOrUndefined(chargingProfile.chargingSchedule.duration)) {
1086 logger.warn(
1087 `${logPrefix} ${moduleName}.checkRecurringChargingProfileDuration: Recurring ${
1088 chargingProfile.chargingProfileKind
1089 } charging profile id ${
1090 chargingProfile.chargingProfileId
1091 } duration is not defined, set it to the recurrency time interval duration ${differenceInSeconds(
1092 interval.end,
1093 interval.start,
1094 )}`,
1095 );
1096 chargingProfile.chargingSchedule.duration = differenceInSeconds(interval.end, interval.start);
1097 } else if (
d476bc1b
JB
1098 chargingProfile.chargingSchedule.duration! > differenceInSeconds(interval.end, interval.start)
1099 ) {
1100 logger.warn(
aa5c5ad4 1101 `${logPrefix} ${moduleName}.checkRecurringChargingProfileDuration: Recurring ${
d476bc1b
JB
1102 chargingProfile.chargingProfileKind
1103 } charging profile id ${chargingProfile.chargingProfileId} duration ${
1104 chargingProfile.chargingSchedule.duration
710d50eb 1105 } is greater than the recurrency time interval duration ${differenceInSeconds(
d476bc1b
JB
1106 interval.end,
1107 interval.start,
710d50eb 1108 )}`,
d476bc1b 1109 );
55f2ab60 1110 chargingProfile.chargingSchedule.duration = differenceInSeconds(interval.end, interval.start);
d476bc1b
JB
1111 }
1112};
1113
fba11dc6
JB
1114const getRandomSerialNumberSuffix = (params?: {
1115 randomBytesLength?: number;
1116 upperCase?: boolean;
1117}): string => {
1118 const randomSerialNumberSuffix = randomBytes(params?.randomBytesLength ?? 16).toString('hex');
1119 if (params?.upperCase) {
1120 return randomSerialNumberSuffix.toUpperCase();
17ac262c 1121 }
fba11dc6
JB
1122 return randomSerialNumberSuffix;
1123};