fix: authorize remotely only if configured
[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' },
fba11dc6
JB
380 ];
381 for (const templateKey of templateKeys) {
382 warnDeprecatedTemplateKey(
383 stationTemplate,
384 templateKey.deprecatedKey,
385 logPrefix,
386 templateFile,
e1d9a0f4 387 !isUndefined(templateKey.key) ? `Use '${templateKey.key}' instead` : undefined,
fba11dc6
JB
388 );
389 convertDeprecatedTemplateKey(stationTemplate, templateKey.deprecatedKey, templateKey.key);
390 }
391};
392
393export const stationTemplateToStationInfo = (
5edd8ba0 394 stationTemplate: ChargingStationTemplate,
fba11dc6
JB
395): ChargingStationInfo => {
396 stationTemplate = cloneObject<ChargingStationTemplate>(stationTemplate);
397 delete stationTemplate.power;
398 delete stationTemplate.powerUnit;
e1d9a0f4
JB
399 delete stationTemplate.Connectors;
400 delete stationTemplate.Evses;
fba11dc6
JB
401 delete stationTemplate.Configuration;
402 delete stationTemplate.AutomaticTransactionGenerator;
403 delete stationTemplate.chargeBoxSerialNumberPrefix;
404 delete stationTemplate.chargePointSerialNumberPrefix;
405 delete stationTemplate.meterSerialNumberPrefix;
66b537dc 406 return stationTemplate as ChargingStationInfo;
fba11dc6
JB
407};
408
409export const createSerialNumber = (
410 stationTemplate: ChargingStationTemplate,
411 stationInfo: ChargingStationInfo,
412 params: {
413 randomSerialNumberUpperCase?: boolean;
414 randomSerialNumber?: boolean;
415 } = {
416 randomSerialNumberUpperCase: true,
417 randomSerialNumber: true,
5edd8ba0 418 },
fba11dc6
JB
419): void => {
420 params = { ...{ randomSerialNumberUpperCase: true, randomSerialNumber: true }, ...params };
421 const serialNumberSuffix = params?.randomSerialNumber
422 ? getRandomSerialNumberSuffix({
423 upperCase: params.randomSerialNumberUpperCase,
424 })
425 : '';
426 isNotEmptyString(stationTemplate?.chargePointSerialNumberPrefix) &&
427 (stationInfo.chargePointSerialNumber = `${stationTemplate.chargePointSerialNumberPrefix}${serialNumberSuffix}`);
428 isNotEmptyString(stationTemplate?.chargeBoxSerialNumberPrefix) &&
429 (stationInfo.chargeBoxSerialNumber = `${stationTemplate.chargeBoxSerialNumberPrefix}${serialNumberSuffix}`);
430 isNotEmptyString(stationTemplate?.meterSerialNumberPrefix) &&
431 (stationInfo.meterSerialNumber = `${stationTemplate.meterSerialNumberPrefix}${serialNumberSuffix}`);
432};
433
434export const propagateSerialNumber = (
435 stationTemplate: ChargingStationTemplate,
436 stationInfoSrc: ChargingStationInfo,
5edd8ba0 437 stationInfoDst: ChargingStationInfo,
fba11dc6
JB
438) => {
439 if (!stationInfoSrc || !stationTemplate) {
440 throw new BaseError(
5edd8ba0 441 'Missing charging station template or existing configuration to propagate serial number',
fba11dc6 442 );
17ac262c 443 }
fba11dc6
JB
444 stationTemplate?.chargePointSerialNumberPrefix && stationInfoSrc?.chargePointSerialNumber
445 ? (stationInfoDst.chargePointSerialNumber = stationInfoSrc.chargePointSerialNumber)
446 : stationInfoDst?.chargePointSerialNumber && delete stationInfoDst.chargePointSerialNumber;
447 stationTemplate?.chargeBoxSerialNumberPrefix && stationInfoSrc?.chargeBoxSerialNumber
448 ? (stationInfoDst.chargeBoxSerialNumber = stationInfoSrc.chargeBoxSerialNumber)
449 : stationInfoDst?.chargeBoxSerialNumber && delete stationInfoDst.chargeBoxSerialNumber;
450 stationTemplate?.meterSerialNumberPrefix && stationInfoSrc?.meterSerialNumber
451 ? (stationInfoDst.meterSerialNumber = stationInfoSrc.meterSerialNumber)
452 : stationInfoDst?.meterSerialNumber && delete stationInfoDst.meterSerialNumber;
453};
454
d8093be1
JB
455export const hasFeatureProfile = (
456 chargingStation: ChargingStation,
457 featureProfile: SupportedFeatureProfiles,
458): boolean | undefined => {
459 return getConfigurationKey(
460 chargingStation,
461 StandardParametersKey.SupportedFeatureProfiles,
462 )?.value?.includes(featureProfile);
463};
464
fba11dc6
JB
465export const getAmperageLimitationUnitDivider = (stationInfo: ChargingStationInfo): number => {
466 let unitDivider = 1;
467 switch (stationInfo.amperageLimitationUnit) {
468 case AmpereUnits.DECI_AMPERE:
469 unitDivider = 10;
470 break;
471 case AmpereUnits.CENTI_AMPERE:
472 unitDivider = 100;
473 break;
474 case AmpereUnits.MILLI_AMPERE:
475 unitDivider = 1000;
476 break;
477 }
478 return unitDivider;
479};
480
481export const getChargingStationConnectorChargingProfilesPowerLimit = (
482 chargingStation: ChargingStation,
5edd8ba0 483 connectorId: number,
fba11dc6 484): number | undefined => {
e1d9a0f4 485 let limit: number | undefined, matchingChargingProfile: ChargingProfile | undefined;
252a7d22 486 // Get charging profiles for connector id and sort by stack level
fba11dc6
JB
487 const chargingProfiles =
488 cloneObject<ChargingProfile[]>(
e1d9a0f4 489 chargingStation.getConnectorStatus(connectorId)!.chargingProfiles!,
fba11dc6 490 )?.sort((a, b) => b.stackLevel - a.stackLevel) ?? [];
252a7d22 491 // Get charging profiles on connector 0 and sort by stack level
710d50eb 492 if (isNotEmptyArray(chargingStation.getConnectorStatus(0)?.chargingProfiles)) {
fba11dc6
JB
493 chargingProfiles.push(
494 ...cloneObject<ChargingProfile[]>(
e1d9a0f4 495 chargingStation.getConnectorStatus(0)!.chargingProfiles!,
5edd8ba0 496 ).sort((a, b) => b.stackLevel - a.stackLevel),
fba11dc6 497 );
17ac262c 498 }
fba11dc6 499 if (isNotEmptyArray(chargingProfiles)) {
a71d4e70
JB
500 const result = getLimitFromChargingProfiles(
501 chargingStation,
502 connectorId,
503 chargingProfiles,
504 chargingStation.logPrefix(),
505 );
fba11dc6
JB
506 if (!isNullOrUndefined(result)) {
507 limit = result?.limit;
508 matchingChargingProfile = result?.matchingChargingProfile;
509 switch (chargingStation.getCurrentOutType()) {
510 case CurrentType.AC:
511 limit =
e1d9a0f4
JB
512 matchingChargingProfile?.chargingSchedule?.chargingRateUnit ===
513 ChargingRateUnitType.WATT
fba11dc6
JB
514 ? limit
515 : ACElectricUtils.powerTotal(
516 chargingStation.getNumberOfPhases(),
517 chargingStation.getVoltageOut(),
e1d9a0f4 518 limit!,
fba11dc6
JB
519 );
520 break;
521 case CurrentType.DC:
522 limit =
e1d9a0f4
JB
523 matchingChargingProfile?.chargingSchedule?.chargingRateUnit ===
524 ChargingRateUnitType.WATT
fba11dc6 525 ? limit
e1d9a0f4 526 : DCElectricUtils.power(chargingStation.getVoltageOut(), limit!);
fba11dc6
JB
527 }
528 const connectorMaximumPower =
529 chargingStation.getMaximumPower() / chargingStation.powerDivider;
e1d9a0f4 530 if (limit! > connectorMaximumPower) {
fba11dc6 531 logger.error(
aa5c5ad4 532 `${chargingStation.logPrefix()} ${moduleName}.getChargingStationConnectorChargingProfilesPowerLimit: Charging profile id ${matchingChargingProfile?.chargingProfileId} limit ${limit} is greater than connector id ${connectorId} maximum ${connectorMaximumPower}: %j`,
5edd8ba0 533 result,
fba11dc6
JB
534 );
535 limit = connectorMaximumPower;
15068be9
JB
536 }
537 }
15068be9 538 }
fba11dc6
JB
539 return limit;
540};
541
542export const getDefaultVoltageOut = (
543 currentType: CurrentType,
544 logPrefix: string,
5edd8ba0 545 templateFile: string,
fba11dc6
JB
546): Voltage => {
547 const errorMsg = `Unknown ${currentType} currentOutType in template file ${templateFile}, cannot define default voltage out`;
548 let defaultVoltageOut: number;
549 switch (currentType) {
550 case CurrentType.AC:
551 defaultVoltageOut = Voltage.VOLTAGE_230;
552 break;
553 case CurrentType.DC:
554 defaultVoltageOut = Voltage.VOLTAGE_400;
555 break;
556 default:
557 logger.error(`${logPrefix} ${errorMsg}`);
558 throw new BaseError(errorMsg);
15068be9 559 }
fba11dc6
JB
560 return defaultVoltageOut;
561};
562
563export const getIdTagsFile = (stationInfo: ChargingStationInfo): string | undefined => {
564 return (
565 stationInfo.idTagsFile &&
566 join(dirname(fileURLToPath(import.meta.url)), 'assets', basename(stationInfo.idTagsFile))
567 );
568};
569
b2b60626 570export const waitChargingStationEvents = async (
fba11dc6
JB
571 emitter: EventEmitter,
572 event: ChargingStationWorkerMessageEvents,
5edd8ba0 573 eventsToWait: number,
fba11dc6 574): Promise<number> => {
474d4ffc 575 return new Promise<number>((resolve) => {
fba11dc6
JB
576 let events = 0;
577 if (eventsToWait === 0) {
578 resolve(events);
579 }
580 emitter.on(event, () => {
581 ++events;
582 if (events === eventsToWait) {
b1f1b0f6
JB
583 resolve(events);
584 }
b1f1b0f6 585 });
fba11dc6
JB
586 });
587};
588
589const getConfiguredNumberOfConnectors = (stationTemplate: ChargingStationTemplate): number => {
e1d9a0f4 590 let configuredMaxConnectors = 0;
fba11dc6
JB
591 if (isNotEmptyArray(stationTemplate.numberOfConnectors) === true) {
592 const numberOfConnectors = stationTemplate.numberOfConnectors as number[];
593 configuredMaxConnectors =
594 numberOfConnectors[Math.floor(secureRandom() * numberOfConnectors.length)];
595 } else if (isUndefined(stationTemplate.numberOfConnectors) === false) {
596 configuredMaxConnectors = stationTemplate.numberOfConnectors as number;
597 } else if (stationTemplate.Connectors && !stationTemplate.Evses) {
e1d9a0f4 598 configuredMaxConnectors = stationTemplate.Connectors[0]
fba11dc6
JB
599 ? getMaxNumberOfConnectors(stationTemplate.Connectors) - 1
600 : getMaxNumberOfConnectors(stationTemplate.Connectors);
601 } else if (stationTemplate.Evses && !stationTemplate.Connectors) {
fba11dc6
JB
602 for (const evse in stationTemplate.Evses) {
603 if (evse === '0') {
604 continue;
cda5d0fb 605 }
fba11dc6 606 configuredMaxConnectors += getMaxNumberOfConnectors(stationTemplate.Evses[evse].Connectors);
cda5d0fb 607 }
cda5d0fb 608 }
fba11dc6
JB
609 return configuredMaxConnectors;
610};
611
612const checkConfiguredMaxConnectors = (
613 configuredMaxConnectors: number,
614 logPrefix: string,
5edd8ba0 615 templateFile: string,
fba11dc6
JB
616): void => {
617 if (configuredMaxConnectors <= 0) {
618 logger.warn(
5edd8ba0 619 `${logPrefix} Charging station information from template ${templateFile} with ${configuredMaxConnectors} connectors`,
fba11dc6 620 );
cda5d0fb 621 }
fba11dc6 622};
cda5d0fb 623
fba11dc6
JB
624const checkTemplateMaxConnectors = (
625 templateMaxConnectors: number,
626 logPrefix: string,
5edd8ba0 627 templateFile: string,
fba11dc6
JB
628): void => {
629 if (templateMaxConnectors === 0) {
630 logger.warn(
5edd8ba0 631 `${logPrefix} Charging station information from template ${templateFile} with empty connectors configuration`,
fba11dc6
JB
632 );
633 } else if (templateMaxConnectors < 0) {
634 logger.error(
5edd8ba0 635 `${logPrefix} Charging station information from template ${templateFile} with no connectors configuration defined`,
fba11dc6
JB
636 );
637 }
638};
639
640const initializeConnectorStatus = (connectorStatus: ConnectorStatus): void => {
641 connectorStatus.availability = AvailabilityType.Operative;
642 connectorStatus.idTagLocalAuthorized = false;
643 connectorStatus.idTagAuthorized = false;
644 connectorStatus.transactionRemoteStarted = false;
645 connectorStatus.transactionStarted = false;
646 connectorStatus.energyActiveImportRegisterValue = 0;
647 connectorStatus.transactionEnergyActiveImportRegisterValue = 0;
648 if (isUndefined(connectorStatus.chargingProfiles)) {
649 connectorStatus.chargingProfiles = [];
650 }
651};
652
653const warnDeprecatedTemplateKey = (
654 template: ChargingStationTemplate,
655 key: string,
656 logPrefix: string,
657 templateFile: string,
5edd8ba0 658 logMsgToAppend = '',
fba11dc6 659): void => {
a37fc6dc 660 if (!isUndefined(template[key as keyof ChargingStationTemplate])) {
fba11dc6
JB
661 const logMsg = `Deprecated template key '${key}' usage in file '${templateFile}'${
662 isNotEmptyString(logMsgToAppend) ? `. ${logMsgToAppend}` : ''
663 }`;
664 logger.warn(`${logPrefix} ${logMsg}`);
665 console.warn(chalk.yellow(`${logMsg}`));
666 }
667};
668
669const convertDeprecatedTemplateKey = (
670 template: ChargingStationTemplate,
671 deprecatedKey: string,
e1d9a0f4 672 key?: string,
fba11dc6 673): void => {
a37fc6dc 674 if (!isUndefined(template[deprecatedKey as keyof ChargingStationTemplate])) {
e1d9a0f4 675 if (!isUndefined(key)) {
a37fc6dc
JB
676 (template as unknown as Record<string, unknown>)[key!] =
677 template[deprecatedKey as keyof ChargingStationTemplate];
e1d9a0f4 678 }
a37fc6dc 679 delete template[deprecatedKey as keyof ChargingStationTemplate];
fba11dc6
JB
680 }
681};
682
947f048a
JB
683interface ChargingProfilesLimit {
684 limit: number;
685 matchingChargingProfile: ChargingProfile;
686}
687
fba11dc6 688/**
d467756c 689 * Charging profiles shall already be sorted by connector id and stack level (highest stack level has priority)
fba11dc6 690 *
d467756c
JB
691 * @param chargingStation -
692 * @param connectorId -
fba11dc6
JB
693 * @param chargingProfiles -
694 * @param logPrefix -
947f048a 695 * @returns ChargingProfilesLimit
fba11dc6
JB
696 */
697const getLimitFromChargingProfiles = (
a71d4e70
JB
698 chargingStation: ChargingStation,
699 connectorId: number,
fba11dc6 700 chargingProfiles: ChargingProfile[],
5edd8ba0 701 logPrefix: string,
947f048a 702): ChargingProfilesLimit | undefined => {
fba11dc6 703 const debugLogMsg = `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Matching charging profile found for power limitation: %j`;
fba11dc6 704 const currentDate = new Date();
b5c19509 705 const connectorStatus = chargingStation.getConnectorStatus(connectorId);
fba11dc6 706 for (const chargingProfile of chargingProfiles) {
fba11dc6 707 const chargingSchedule = chargingProfile.chargingSchedule;
5543b88d 708 if (connectorStatus?.transactionStarted && isNullOrUndefined(chargingSchedule?.startSchedule)) {
109c677a 709 logger.debug(
ec4a242a 710 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} has no startSchedule defined. Trying to set it to the connector current transaction start date`,
cda5d0fb 711 );
a71d4e70 712 // OCPP specifies that if startSchedule is not defined, it should be relative to start of the connector transaction
b5c19509 713 chargingSchedule.startSchedule = connectorStatus?.transactionStart;
52952bf8 714 }
0bd926c1 715 if (!isDate(chargingSchedule?.startSchedule)) {
8d75a403 716 logger.warn(
ec4a242a 717 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} startSchedule property is not a Date object. Trying to convert it to a Date object`,
8d75a403 718 );
991fb26b 719 chargingSchedule.startSchedule = convertToDate(chargingSchedule?.startSchedule)!;
8d75a403 720 }
0bd926c1
JB
721 switch (chargingProfile.chargingProfileKind) {
722 case ChargingProfileKindType.RECURRING:
723 if (!canProceedRecurringChargingProfile(chargingProfile, logPrefix)) {
724 continue;
725 }
726 prepareRecurringChargingProfile(chargingProfile, currentDate, logPrefix);
727 break;
728 case ChargingProfileKindType.RELATIVE:
729 connectorStatus?.transactionStarted &&
730 (chargingSchedule.startSchedule = connectorStatus?.transactionStart);
731 break;
ec4a242a 732 }
0bd926c1 733 if (!canProceedChargingProfile(chargingProfile, currentDate, logPrefix)) {
142a66c9
JB
734 continue;
735 }
fba11dc6
JB
736 // Check if the charging profile is active
737 if (
0bd926c1 738 isValidTime(chargingSchedule?.startSchedule) &&
975e18ec
JB
739 isWithinInterval(currentDate, {
740 start: chargingSchedule.startSchedule!,
741 end: addSeconds(chargingSchedule.startSchedule!, chargingSchedule.duration!),
742 })
fba11dc6 743 ) {
252a7d22 744 if (isNotEmptyArray(chargingSchedule.chargingSchedulePeriod)) {
80c58041
JB
745 const chargingSchedulePeriodCompareFn = (
746 a: ChargingSchedulePeriod,
747 b: ChargingSchedulePeriod,
748 ) => a.startPeriod - b.startPeriod;
749 if (
750 isArraySorted<ChargingSchedulePeriod>(
751 chargingSchedule.chargingSchedulePeriod,
752 chargingSchedulePeriodCompareFn,
753 ) === false
754 ) {
755 logger.warn(
756 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} schedule periods are not sorted by start period`,
757 );
758 chargingSchedule.chargingSchedulePeriod.sort(chargingSchedulePeriodCompareFn);
759 }
975e18ec 760 // Check if the first schedule period start period is equal to 0
55f2ab60
JB
761 if (chargingSchedule.chargingSchedulePeriod[0].startPeriod !== 0) {
762 logger.error(
763 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} first schedule period start period ${chargingSchedule.chargingSchedulePeriod[0].startPeriod} is not equal to 0`,
764 );
975e18ec 765 continue;
55f2ab60 766 }
991fb26b 767 // Handle only one schedule period
975e18ec 768 if (chargingSchedule.chargingSchedulePeriod.length === 1) {
252a7d22
JB
769 const result: ChargingProfilesLimit = {
770 limit: chargingSchedule.chargingSchedulePeriod[0].limit,
fba11dc6
JB
771 matchingChargingProfile: chargingProfile,
772 };
773 logger.debug(debugLogMsg, result);
774 return result;
41189456 775 }
e3037969 776 let previousChargingSchedulePeriod: ChargingSchedulePeriod | undefined;
252a7d22 777 // Search for the right schedule period
e3037969
JB
778 for (const [
779 index,
780 chargingSchedulePeriod,
781 ] of chargingSchedule.chargingSchedulePeriod.entries()) {
252a7d22
JB
782 // Find the right schedule period
783 if (
784 isAfter(
e3037969 785 addSeconds(chargingSchedule.startSchedule!, chargingSchedulePeriod.startPeriod),
252a7d22
JB
786 currentDate,
787 )
788 ) {
e3037969 789 // Found the schedule period: previous is the correct one
252a7d22 790 const result: ChargingProfilesLimit = {
e3037969 791 limit: previousChargingSchedulePeriod!.limit,
252a7d22
JB
792 matchingChargingProfile: chargingProfile,
793 };
794 logger.debug(debugLogMsg, result);
795 return result;
796 }
e3037969
JB
797 // Keep a reference to previous one
798 previousChargingSchedulePeriod = chargingSchedulePeriod;
975e18ec 799 // Handle the last schedule period within the charging profile duration
252a7d22 800 if (
975e18ec
JB
801 index === chargingSchedule.chargingSchedulePeriod.length - 1 ||
802 (index < chargingSchedule.chargingSchedulePeriod.length - 1 &&
803 chargingSchedule.duration! >
804 differenceInSeconds(
975e18ec
JB
805 addSeconds(
806 chargingSchedule.startSchedule!,
807 chargingSchedule.chargingSchedulePeriod[index + 1].startPeriod,
808 ),
d9dc6292 809 chargingSchedule.startSchedule!,
975e18ec 810 ))
252a7d22
JB
811 ) {
812 const result: ChargingProfilesLimit = {
e3037969 813 limit: previousChargingSchedulePeriod.limit,
252a7d22
JB
814 matchingChargingProfile: chargingProfile,
815 };
816 logger.debug(debugLogMsg, result);
817 return result;
818 }
17ac262c
JB
819 }
820 }
821 }
17ac262c 822 }
fba11dc6 823};
17ac262c 824
0bd926c1
JB
825const canProceedChargingProfile = (
826 chargingProfile: ChargingProfile,
827 currentDate: Date,
828 logPrefix: string,
829): boolean => {
830 if (
831 (isValidTime(chargingProfile.validFrom) && isBefore(currentDate, chargingProfile.validFrom!)) ||
832 (isValidTime(chargingProfile.validTo) && isAfter(currentDate, chargingProfile.validTo!))
833 ) {
834 logger.debug(
835 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${
836 chargingProfile.chargingProfileId
837 } is not valid for the current date ${currentDate.toISOString()}`,
838 );
839 return false;
840 }
841 const chargingSchedule = chargingProfile.chargingSchedule;
842 if (isNullOrUndefined(chargingSchedule?.startSchedule)) {
843 logger.error(
844 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId} has (still) no startSchedule defined`,
845 );
846 return false;
847 }
848 if (isNullOrUndefined(chargingSchedule?.duration)) {
849 logger.error(
850 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId} has no duration defined, not yet supported`,
851 );
852 return false;
853 }
854 return true;
855};
856
857const canProceedRecurringChargingProfile = (
858 chargingProfile: ChargingProfile,
859 logPrefix: string,
860): boolean => {
861 if (
862 chargingProfile.chargingProfileKind === ChargingProfileKindType.RECURRING &&
863 isNullOrUndefined(chargingProfile.recurrencyKind)
864 ) {
865 logger.error(
866 `${logPrefix} ${moduleName}.canProceedRecurringChargingProfile: Recurring charging profile id ${chargingProfile.chargingProfileId} has no recurrencyKind defined`,
867 );
868 return false;
869 }
870 return true;
871};
872
522e4b05 873/**
ec4a242a 874 * Adjust recurring charging profile startSchedule to the current recurrency time interval if needed
522e4b05
JB
875 *
876 * @param chargingProfile -
877 * @param currentDate -
878 * @param logPrefix -
879 */
76dab5a9
JB
880const prepareRecurringChargingProfile = (
881 chargingProfile: ChargingProfile,
882 currentDate: Date,
883 logPrefix: string,
ec4a242a 884): boolean => {
76dab5a9 885 const chargingSchedule = chargingProfile.chargingSchedule;
ec4a242a 886 let recurringIntervalTranslated = false;
522e4b05 887 let recurringInterval: Interval;
76dab5a9
JB
888 switch (chargingProfile.recurrencyKind) {
889 case RecurrencyKindType.DAILY:
522e4b05
JB
890 recurringInterval = {
891 start: chargingSchedule.startSchedule!,
892 end: addDays(chargingSchedule.startSchedule!, 1),
893 };
d476bc1b 894 checkRecurringChargingProfileDuration(chargingProfile, recurringInterval, logPrefix);
522e4b05
JB
895 if (
896 !isWithinInterval(currentDate, recurringInterval) &&
991fb26b 897 isBefore(recurringInterval.end, currentDate)
522e4b05
JB
898 ) {
899 chargingSchedule.startSchedule = addDays(
991fb26b 900 recurringInterval.start,
05b52716 901 differenceInDays(currentDate, recurringInterval.start),
76dab5a9 902 );
522e4b05
JB
903 recurringInterval = {
904 start: chargingSchedule.startSchedule,
905 end: addDays(chargingSchedule.startSchedule, 1),
906 };
ec4a242a 907 recurringIntervalTranslated = true;
76dab5a9
JB
908 }
909 break;
910 case RecurrencyKindType.WEEKLY:
522e4b05
JB
911 recurringInterval = {
912 start: chargingSchedule.startSchedule!,
913 end: addWeeks(chargingSchedule.startSchedule!, 1),
914 };
d476bc1b 915 checkRecurringChargingProfileDuration(chargingProfile, recurringInterval, logPrefix);
522e4b05
JB
916 if (
917 !isWithinInterval(currentDate, recurringInterval) &&
991fb26b 918 isBefore(recurringInterval.end, currentDate)
522e4b05
JB
919 ) {
920 chargingSchedule.startSchedule = addWeeks(
991fb26b 921 recurringInterval.start,
05b52716 922 differenceInWeeks(currentDate, recurringInterval.start),
76dab5a9 923 );
522e4b05
JB
924 recurringInterval = {
925 start: chargingSchedule.startSchedule,
926 end: addWeeks(chargingSchedule.startSchedule, 1),
927 };
ec4a242a 928 recurringIntervalTranslated = true;
76dab5a9
JB
929 }
930 break;
ec4a242a
JB
931 default:
932 logger.error(
933 `${logPrefix} ${moduleName}.prepareRecurringChargingProfile: Recurring charging profile id ${chargingProfile.chargingProfileId} recurrency kind ${chargingProfile.recurrencyKind} is not supported`,
934 );
76dab5a9 935 }
ec4a242a 936 if (recurringIntervalTranslated && !isWithinInterval(currentDate, recurringInterval!)) {
522e4b05 937 logger.error(
aa5c5ad4 938 `${logPrefix} ${moduleName}.prepareRecurringChargingProfile: Recurring ${
522e4b05 939 chargingProfile.recurrencyKind
991fb26b 940 } charging profile id ${chargingProfile.chargingProfileId} recurrency time interval [${toDate(
522e4b05 941 recurringInterval!.start,
991fb26b
JB
942 ).toISOString()}, ${toDate(
943 recurringInterval!.end,
ec4a242a 944 ).toISOString()}] has not been properly translated to current date ${currentDate.toISOString()} `,
522e4b05
JB
945 );
946 }
ec4a242a 947 return recurringIntervalTranslated;
76dab5a9
JB
948};
949
d476bc1b
JB
950const checkRecurringChargingProfileDuration = (
951 chargingProfile: ChargingProfile,
952 interval: Interval,
953 logPrefix: string,
ec4a242a 954): void => {
142a66c9
JB
955 if (isNullOrUndefined(chargingProfile.chargingSchedule.duration)) {
956 logger.warn(
957 `${logPrefix} ${moduleName}.checkRecurringChargingProfileDuration: Recurring ${
958 chargingProfile.chargingProfileKind
959 } charging profile id ${
960 chargingProfile.chargingProfileId
961 } duration is not defined, set it to the recurrency time interval duration ${differenceInSeconds(
962 interval.end,
963 interval.start,
964 )}`,
965 );
966 chargingProfile.chargingSchedule.duration = differenceInSeconds(interval.end, interval.start);
967 } else if (
d476bc1b
JB
968 chargingProfile.chargingSchedule.duration! > differenceInSeconds(interval.end, interval.start)
969 ) {
970 logger.warn(
aa5c5ad4 971 `${logPrefix} ${moduleName}.checkRecurringChargingProfileDuration: Recurring ${
d476bc1b
JB
972 chargingProfile.chargingProfileKind
973 } charging profile id ${chargingProfile.chargingProfileId} duration ${
974 chargingProfile.chargingSchedule.duration
710d50eb 975 } is greater than the recurrency time interval duration ${differenceInSeconds(
d476bc1b
JB
976 interval.end,
977 interval.start,
710d50eb 978 )}`,
d476bc1b 979 );
55f2ab60 980 chargingProfile.chargingSchedule.duration = differenceInSeconds(interval.end, interval.start);
d476bc1b
JB
981 }
982};
983
fba11dc6
JB
984const getRandomSerialNumberSuffix = (params?: {
985 randomBytesLength?: number;
986 upperCase?: boolean;
987}): string => {
988 const randomSerialNumberSuffix = randomBytes(params?.randomBytesLength ?? 16).toString('hex');
989 if (params?.upperCase) {
990 return randomSerialNumberSuffix.toUpperCase();
17ac262c 991 }
fba11dc6
JB
992 return randomSerialNumberSuffix;
993};