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