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