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