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