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