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