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