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