build(ci): fix linter errors
[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,
da332e70 21 maxTime,
522e4b05 22 toDate,
f924d466 23} from 'date-fns';
8114d10e 24
4c3c0d59 25import type { ChargingStation } from './ChargingStation';
357a5553 26import { getConfigurationKey } from './ConfigurationKeyUtils';
268a74bb 27import { BaseError } from '../exception';
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
JB
54 Voltage,
55} from '../types';
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,
73} from '../utils';
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);
652 }
653 emitter.on(event, () => {
654 ++events;
655 if (events === eventsToWait) {
b1f1b0f6
JB
656 resolve(events);
657 }
b1f1b0f6 658 });
fba11dc6
JB
659 });
660};
661
cfc9875a
JB
662const getConfiguredMaxNumberOfConnectors = (stationTemplate: ChargingStationTemplate): number => {
663 let configuredMaxNumberOfConnectors = 0;
fba11dc6
JB
664 if (isNotEmptyArray(stationTemplate.numberOfConnectors) === true) {
665 const numberOfConnectors = stationTemplate.numberOfConnectors as number[];
cfc9875a 666 configuredMaxNumberOfConnectors =
fba11dc6
JB
667 numberOfConnectors[Math.floor(secureRandom() * numberOfConnectors.length)];
668 } else if (isUndefined(stationTemplate.numberOfConnectors) === false) {
cfc9875a 669 configuredMaxNumberOfConnectors = stationTemplate.numberOfConnectors as number;
fba11dc6 670 } else if (stationTemplate.Connectors && !stationTemplate.Evses) {
1c9de2b9 671 configuredMaxNumberOfConnectors = stationTemplate.Connectors?.[0]
fba11dc6
JB
672 ? getMaxNumberOfConnectors(stationTemplate.Connectors) - 1
673 : getMaxNumberOfConnectors(stationTemplate.Connectors);
674 } else if (stationTemplate.Evses && !stationTemplate.Connectors) {
fba11dc6
JB
675 for (const evse in stationTemplate.Evses) {
676 if (evse === '0') {
677 continue;
cda5d0fb 678 }
cfc9875a
JB
679 configuredMaxNumberOfConnectors += getMaxNumberOfConnectors(
680 stationTemplate.Evses[evse].Connectors,
681 );
cda5d0fb 682 }
cda5d0fb 683 }
cfc9875a 684 return configuredMaxNumberOfConnectors;
fba11dc6
JB
685};
686
687const checkConfiguredMaxConnectors = (
688 configuredMaxConnectors: number,
689 logPrefix: string,
5edd8ba0 690 templateFile: string,
fba11dc6
JB
691): void => {
692 if (configuredMaxConnectors <= 0) {
693 logger.warn(
5edd8ba0 694 `${logPrefix} Charging station information from template ${templateFile} with ${configuredMaxConnectors} connectors`,
fba11dc6 695 );
cda5d0fb 696 }
fba11dc6 697};
cda5d0fb 698
fba11dc6
JB
699const checkTemplateMaxConnectors = (
700 templateMaxConnectors: number,
701 logPrefix: string,
5edd8ba0 702 templateFile: string,
fba11dc6
JB
703): void => {
704 if (templateMaxConnectors === 0) {
705 logger.warn(
5edd8ba0 706 `${logPrefix} Charging station information from template ${templateFile} with empty connectors configuration`,
fba11dc6
JB
707 );
708 } else if (templateMaxConnectors < 0) {
709 logger.error(
5edd8ba0 710 `${logPrefix} Charging station information from template ${templateFile} with no connectors configuration defined`,
fba11dc6
JB
711 );
712 }
713};
714
715const initializeConnectorStatus = (connectorStatus: ConnectorStatus): void => {
716 connectorStatus.availability = AvailabilityType.Operative;
717 connectorStatus.idTagLocalAuthorized = false;
718 connectorStatus.idTagAuthorized = false;
719 connectorStatus.transactionRemoteStarted = false;
720 connectorStatus.transactionStarted = false;
721 connectorStatus.energyActiveImportRegisterValue = 0;
722 connectorStatus.transactionEnergyActiveImportRegisterValue = 0;
723 if (isUndefined(connectorStatus.chargingProfiles)) {
724 connectorStatus.chargingProfiles = [];
725 }
726};
727
728const warnDeprecatedTemplateKey = (
729 template: ChargingStationTemplate,
730 key: string,
731 logPrefix: string,
732 templateFile: string,
5edd8ba0 733 logMsgToAppend = '',
fba11dc6 734): void => {
1c9de2b9 735 if (!isUndefined(template?.[key as keyof ChargingStationTemplate])) {
fba11dc6
JB
736 const logMsg = `Deprecated template key '${key}' usage in file '${templateFile}'${
737 isNotEmptyString(logMsgToAppend) ? `. ${logMsgToAppend}` : ''
738 }`;
739 logger.warn(`${logPrefix} ${logMsg}`);
a418c77b 740 console.warn(`${chalk.green(logPrefix)} ${chalk.yellow(logMsg)}`);
fba11dc6
JB
741 }
742};
743
744const convertDeprecatedTemplateKey = (
745 template: ChargingStationTemplate,
746 deprecatedKey: string,
e1d9a0f4 747 key?: string,
fba11dc6 748): void => {
1c9de2b9 749 if (!isUndefined(template?.[deprecatedKey as keyof ChargingStationTemplate])) {
e1d9a0f4 750 if (!isUndefined(key)) {
a37fc6dc
JB
751 (template as unknown as Record<string, unknown>)[key!] =
752 template[deprecatedKey as keyof ChargingStationTemplate];
e1d9a0f4 753 }
a37fc6dc 754 delete template[deprecatedKey as keyof ChargingStationTemplate];
fba11dc6
JB
755 }
756};
757
947f048a
JB
758interface ChargingProfilesLimit {
759 limit: number;
bbb55ee4 760 chargingProfile: ChargingProfile;
947f048a
JB
761}
762
fba11dc6 763/**
21ee4dc2 764 * Charging profiles shall already be sorted by connector id descending then stack level descending
fba11dc6 765 *
d467756c
JB
766 * @param chargingStation -
767 * @param connectorId -
fba11dc6
JB
768 * @param chargingProfiles -
769 * @param logPrefix -
947f048a 770 * @returns ChargingProfilesLimit
fba11dc6
JB
771 */
772const getLimitFromChargingProfiles = (
a71d4e70
JB
773 chargingStation: ChargingStation,
774 connectorId: number,
fba11dc6 775 chargingProfiles: ChargingProfile[],
5edd8ba0 776 logPrefix: string,
947f048a 777): ChargingProfilesLimit | undefined => {
fba11dc6 778 const debugLogMsg = `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Matching charging profile found for power limitation: %j`;
fba11dc6 779 const currentDate = new Date();
0eb666db 780 const connectorStatus = chargingStation.getConnectorStatus(connectorId)!;
fba11dc6 781 for (const chargingProfile of chargingProfiles) {
fba11dc6 782 const chargingSchedule = chargingProfile.chargingSchedule;
da332e70 783 if (isNullOrUndefined(chargingSchedule?.startSchedule) && connectorStatus?.transactionStarted) {
109c677a 784 logger.debug(
ec4a242a 785 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} has no startSchedule defined. Trying to set it to the connector current transaction start date`,
cda5d0fb 786 );
a71d4e70 787 // OCPP specifies that if startSchedule is not defined, it should be relative to start of the connector transaction
b5c19509 788 chargingSchedule.startSchedule = connectorStatus?.transactionStart;
52952bf8 789 }
ef9e3b33
JB
790 if (
791 !isNullOrUndefined(chargingSchedule?.startSchedule) &&
792 !isDate(chargingSchedule?.startSchedule)
793 ) {
794 logger.warn(
795 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} startSchedule property is not a Date instance. Trying to convert it to a Date instance`,
796 );
797 chargingSchedule.startSchedule = convertToDate(chargingSchedule?.startSchedule)!;
798 }
da332e70
JB
799 if (
800 !isNullOrUndefined(chargingSchedule?.startSchedule) &&
801 isNullOrUndefined(chargingSchedule?.duration)
802 ) {
803 logger.debug(
804 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} has no duration defined and will be set to the maximum time allowed`,
805 );
806 // OCPP specifies that if duration is not defined, it should be infinite
807 chargingSchedule.duration = differenceInSeconds(maxTime, chargingSchedule.startSchedule!);
808 }
0eb666db
JB
809 if (!prepareChargingProfileKind(connectorStatus, chargingProfile, currentDate, logPrefix)) {
810 continue;
ec4a242a 811 }
0bd926c1 812 if (!canProceedChargingProfile(chargingProfile, currentDate, logPrefix)) {
142a66c9
JB
813 continue;
814 }
fba11dc6
JB
815 // Check if the charging profile is active
816 if (
975e18ec
JB
817 isWithinInterval(currentDate, {
818 start: chargingSchedule.startSchedule!,
819 end: addSeconds(chargingSchedule.startSchedule!, chargingSchedule.duration!),
820 })
fba11dc6 821 ) {
252a7d22 822 if (isNotEmptyArray(chargingSchedule.chargingSchedulePeriod)) {
80c58041
JB
823 const chargingSchedulePeriodCompareFn = (
824 a: ChargingSchedulePeriod,
825 b: ChargingSchedulePeriod,
826 ) => a.startPeriod - b.startPeriod;
827 if (
6fc0c6f3 828 !isArraySorted<ChargingSchedulePeriod>(
80c58041
JB
829 chargingSchedule.chargingSchedulePeriod,
830 chargingSchedulePeriodCompareFn,
6fc0c6f3 831 )
80c58041
JB
832 ) {
833 logger.warn(
834 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} schedule periods are not sorted by start period`,
835 );
836 chargingSchedule.chargingSchedulePeriod.sort(chargingSchedulePeriodCompareFn);
837 }
da332e70 838 // Check if the first schedule period startPeriod property is equal to 0
55f2ab60
JB
839 if (chargingSchedule.chargingSchedulePeriod[0].startPeriod !== 0) {
840 logger.error(
841 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} first schedule period start period ${chargingSchedule.chargingSchedulePeriod[0].startPeriod} is not equal to 0`,
842 );
975e18ec 843 continue;
55f2ab60 844 }
991fb26b 845 // Handle only one schedule period
975e18ec 846 if (chargingSchedule.chargingSchedulePeriod.length === 1) {
252a7d22
JB
847 const result: ChargingProfilesLimit = {
848 limit: chargingSchedule.chargingSchedulePeriod[0].limit,
bbb55ee4 849 chargingProfile,
fba11dc6
JB
850 };
851 logger.debug(debugLogMsg, result);
852 return result;
41189456 853 }
e3037969 854 let previousChargingSchedulePeriod: ChargingSchedulePeriod | undefined;
252a7d22 855 // Search for the right schedule period
e3037969
JB
856 for (const [
857 index,
858 chargingSchedulePeriod,
859 ] of chargingSchedule.chargingSchedulePeriod.entries()) {
252a7d22
JB
860 // Find the right schedule period
861 if (
862 isAfter(
e3037969 863 addSeconds(chargingSchedule.startSchedule!, chargingSchedulePeriod.startPeriod),
252a7d22
JB
864 currentDate,
865 )
866 ) {
e3037969 867 // Found the schedule period: previous is the correct one
252a7d22 868 const result: ChargingProfilesLimit = {
e3037969 869 limit: previousChargingSchedulePeriod!.limit,
6fc0c6f3 870 chargingProfile,
252a7d22
JB
871 };
872 logger.debug(debugLogMsg, result);
873 return result;
874 }
e3037969
JB
875 // Keep a reference to previous one
876 previousChargingSchedulePeriod = chargingSchedulePeriod;
975e18ec 877 // Handle the last schedule period within the charging profile duration
252a7d22 878 if (
975e18ec
JB
879 index === chargingSchedule.chargingSchedulePeriod.length - 1 ||
880 (index < chargingSchedule.chargingSchedulePeriod.length - 1 &&
ccfa30bc
JB
881 differenceInSeconds(
882 addSeconds(
d9dc6292 883 chargingSchedule.startSchedule!,
ccfa30bc
JB
884 chargingSchedule.chargingSchedulePeriod[index + 1].startPeriod,
885 ),
886 chargingSchedule.startSchedule!,
887 ) > chargingSchedule.duration!)
252a7d22
JB
888 ) {
889 const result: ChargingProfilesLimit = {
e3037969 890 limit: previousChargingSchedulePeriod.limit,
6fc0c6f3 891 chargingProfile,
252a7d22
JB
892 };
893 logger.debug(debugLogMsg, result);
894 return result;
895 }
17ac262c
JB
896 }
897 }
898 }
17ac262c 899 }
fba11dc6 900};
17ac262c 901
0eb666db
JB
902export const prepareChargingProfileKind = (
903 connectorStatus: ConnectorStatus,
904 chargingProfile: ChargingProfile,
905 currentDate: Date,
906 logPrefix: string,
907): boolean => {
908 switch (chargingProfile.chargingProfileKind) {
909 case ChargingProfileKindType.RECURRING:
910 if (!canProceedRecurringChargingProfile(chargingProfile, logPrefix)) {
911 return false;
912 }
913 prepareRecurringChargingProfile(chargingProfile, currentDate, logPrefix);
914 break;
915 case ChargingProfileKindType.RELATIVE:
ccfa30bc
JB
916 if (!isNullOrUndefined(chargingProfile.chargingSchedule.startSchedule)) {
917 logger.warn(
320d07e3 918 `${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
919 );
920 delete chargingProfile.chargingSchedule.startSchedule;
921 }
ef9e3b33
JB
922 if (connectorStatus?.transactionStarted) {
923 chargingProfile.chargingSchedule.startSchedule = connectorStatus?.transactionStart;
924 }
925 // FIXME: Handle relative charging profile duration
0eb666db
JB
926 break;
927 }
928 return true;
929};
930
ad490d5f 931export const canProceedChargingProfile = (
0bd926c1
JB
932 chargingProfile: ChargingProfile,
933 currentDate: Date,
934 logPrefix: string,
935): boolean => {
936 if (
937 (isValidTime(chargingProfile.validFrom) && isBefore(currentDate, chargingProfile.validFrom!)) ||
938 (isValidTime(chargingProfile.validTo) && isAfter(currentDate, chargingProfile.validTo!))
939 ) {
940 logger.debug(
941 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${
942 chargingProfile.chargingProfileId
943 } is not valid for the current date ${currentDate.toISOString()}`,
944 );
945 return false;
946 }
ef9e3b33
JB
947 if (
948 isNullOrUndefined(chargingProfile.chargingSchedule.startSchedule) ||
949 isNullOrUndefined(chargingProfile.chargingSchedule.duration)
950 ) {
0bd926c1 951 logger.error(
ef9e3b33
JB
952 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId} has no startSchedule or duration defined`,
953 );
954 return false;
955 }
956 if (
957 !isNullOrUndefined(chargingProfile.chargingSchedule.startSchedule) &&
958 !isValidTime(chargingProfile.chargingSchedule.startSchedule)
959 ) {
960 logger.error(
961 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId} has an invalid startSchedule date defined`,
962 );
963 return false;
964 }
965 if (
966 !isNullOrUndefined(chargingProfile.chargingSchedule.duration) &&
967 !Number.isSafeInteger(chargingProfile.chargingSchedule.duration)
968 ) {
969 logger.error(
970 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId} has non integer duration defined`,
0bd926c1 971 );
0bd926c1
JB
972 return false;
973 }
974 return true;
975};
976
0eb666db 977const canProceedRecurringChargingProfile = (
0bd926c1
JB
978 chargingProfile: ChargingProfile,
979 logPrefix: string,
980): boolean => {
981 if (
982 chargingProfile.chargingProfileKind === ChargingProfileKindType.RECURRING &&
983 isNullOrUndefined(chargingProfile.recurrencyKind)
984 ) {
985 logger.error(
986 `${logPrefix} ${moduleName}.canProceedRecurringChargingProfile: Recurring charging profile id ${chargingProfile.chargingProfileId} has no recurrencyKind defined`,
987 );
988 return false;
989 }
d929adcc
JB
990 if (
991 chargingProfile.chargingProfileKind === ChargingProfileKindType.RECURRING &&
992 isNullOrUndefined(chargingProfile.chargingSchedule.startSchedule)
993 ) {
ef9e3b33
JB
994 logger.error(
995 `${logPrefix} ${moduleName}.canProceedRecurringChargingProfile: Recurring charging profile id ${chargingProfile.chargingProfileId} has no startSchedule defined`,
996 );
997 return false;
998 }
0bd926c1
JB
999 return true;
1000};
1001
522e4b05 1002/**
ec4a242a 1003 * Adjust recurring charging profile startSchedule to the current recurrency time interval if needed
522e4b05
JB
1004 *
1005 * @param chargingProfile -
1006 * @param currentDate -
1007 * @param logPrefix -
1008 */
0eb666db 1009const prepareRecurringChargingProfile = (
76dab5a9
JB
1010 chargingProfile: ChargingProfile,
1011 currentDate: Date,
1012 logPrefix: string,
ec4a242a 1013): boolean => {
76dab5a9 1014 const chargingSchedule = chargingProfile.chargingSchedule;
ec4a242a 1015 let recurringIntervalTranslated = false;
522e4b05 1016 let recurringInterval: Interval;
76dab5a9
JB
1017 switch (chargingProfile.recurrencyKind) {
1018 case RecurrencyKindType.DAILY:
522e4b05
JB
1019 recurringInterval = {
1020 start: chargingSchedule.startSchedule!,
1021 end: addDays(chargingSchedule.startSchedule!, 1),
1022 };
d476bc1b 1023 checkRecurringChargingProfileDuration(chargingProfile, recurringInterval, logPrefix);
522e4b05
JB
1024 if (
1025 !isWithinInterval(currentDate, recurringInterval) &&
991fb26b 1026 isBefore(recurringInterval.end, currentDate)
522e4b05
JB
1027 ) {
1028 chargingSchedule.startSchedule = addDays(
991fb26b 1029 recurringInterval.start,
05b52716 1030 differenceInDays(currentDate, recurringInterval.start),
76dab5a9 1031 );
522e4b05
JB
1032 recurringInterval = {
1033 start: chargingSchedule.startSchedule,
1034 end: addDays(chargingSchedule.startSchedule, 1),
1035 };
ec4a242a 1036 recurringIntervalTranslated = true;
76dab5a9
JB
1037 }
1038 break;
1039 case RecurrencyKindType.WEEKLY:
522e4b05
JB
1040 recurringInterval = {
1041 start: chargingSchedule.startSchedule!,
1042 end: addWeeks(chargingSchedule.startSchedule!, 1),
1043 };
d476bc1b 1044 checkRecurringChargingProfileDuration(chargingProfile, recurringInterval, logPrefix);
522e4b05
JB
1045 if (
1046 !isWithinInterval(currentDate, recurringInterval) &&
991fb26b 1047 isBefore(recurringInterval.end, currentDate)
522e4b05
JB
1048 ) {
1049 chargingSchedule.startSchedule = addWeeks(
991fb26b 1050 recurringInterval.start,
05b52716 1051 differenceInWeeks(currentDate, recurringInterval.start),
76dab5a9 1052 );
522e4b05
JB
1053 recurringInterval = {
1054 start: chargingSchedule.startSchedule,
1055 end: addWeeks(chargingSchedule.startSchedule, 1),
1056 };
ec4a242a 1057 recurringIntervalTranslated = true;
76dab5a9
JB
1058 }
1059 break;
ec4a242a
JB
1060 default:
1061 logger.error(
320d07e3 1062 `${logPrefix} ${moduleName}.prepareRecurringChargingProfile: Recurring ${chargingProfile.recurrencyKind} charging profile id ${chargingProfile.chargingProfileId} is not supported`,
ec4a242a 1063 );
76dab5a9 1064 }
ec4a242a 1065 if (recurringIntervalTranslated && !isWithinInterval(currentDate, recurringInterval!)) {
522e4b05 1066 logger.error(
aa5c5ad4 1067 `${logPrefix} ${moduleName}.prepareRecurringChargingProfile: Recurring ${
522e4b05 1068 chargingProfile.recurrencyKind
991fb26b 1069 } charging profile id ${chargingProfile.chargingProfileId} recurrency time interval [${toDate(
522e4b05 1070 recurringInterval!.start,
991fb26b
JB
1071 ).toISOString()}, ${toDate(
1072 recurringInterval!.end,
ec4a242a 1073 ).toISOString()}] has not been properly translated to current date ${currentDate.toISOString()} `,
522e4b05
JB
1074 );
1075 }
ec4a242a 1076 return recurringIntervalTranslated;
76dab5a9
JB
1077};
1078
d476bc1b
JB
1079const checkRecurringChargingProfileDuration = (
1080 chargingProfile: ChargingProfile,
1081 interval: Interval,
1082 logPrefix: string,
ec4a242a 1083): void => {
142a66c9
JB
1084 if (isNullOrUndefined(chargingProfile.chargingSchedule.duration)) {
1085 logger.warn(
1086 `${logPrefix} ${moduleName}.checkRecurringChargingProfileDuration: Recurring ${
1087 chargingProfile.chargingProfileKind
1088 } charging profile id ${
1089 chargingProfile.chargingProfileId
1090 } duration is not defined, set it to the recurrency time interval duration ${differenceInSeconds(
1091 interval.end,
1092 interval.start,
1093 )}`,
1094 );
1095 chargingProfile.chargingSchedule.duration = differenceInSeconds(interval.end, interval.start);
1096 } else if (
d476bc1b
JB
1097 chargingProfile.chargingSchedule.duration! > differenceInSeconds(interval.end, interval.start)
1098 ) {
1099 logger.warn(
aa5c5ad4 1100 `${logPrefix} ${moduleName}.checkRecurringChargingProfileDuration: Recurring ${
d476bc1b
JB
1101 chargingProfile.chargingProfileKind
1102 } charging profile id ${chargingProfile.chargingProfileId} duration ${
1103 chargingProfile.chargingSchedule.duration
710d50eb 1104 } is greater than the recurrency time interval duration ${differenceInSeconds(
d476bc1b
JB
1105 interval.end,
1106 interval.start,
710d50eb 1107 )}`,
d476bc1b 1108 );
55f2ab60 1109 chargingProfile.chargingSchedule.duration = differenceInSeconds(interval.end, interval.start);
d476bc1b
JB
1110 }
1111};
1112
fba11dc6
JB
1113const getRandomSerialNumberSuffix = (params?: {
1114 randomBytesLength?: number;
1115 upperCase?: boolean;
1116}): string => {
1117 const randomSerialNumberSuffix = randomBytes(params?.randomBytesLength ?? 16).toString('hex');
1118 if (params?.upperCase) {
1119 return randomSerialNumberSuffix.toUpperCase();
17ac262c 1120 }
fba11dc6
JB
1121 return randomSerialNumberSuffix;
1122};