build(simulator): don't preserve modules
[e-mobility-charging-stations-simulator.git] / src / charging-station / ChargingStationUtils.ts
CommitLineData
43ee4373 1import crypto from 'node:crypto';
130783a7
JB
2import path from 'node:path';
3import { fileURLToPath } from 'node:url';
8114d10e 4
e302df1d 5import chalk from 'chalk';
8114d10e
JB
6import moment from 'moment';
7
4c3c0d59 8import type { ChargingStation } from './ChargingStation';
268a74bb 9import { BaseError } from '../exception';
981ebfbe 10import {
492cf6ab 11 AmpereUnits,
04b1261c 12 AvailabilityType,
268a74bb
JB
13 type BootNotificationRequest,
14 BootReasonEnumType,
15068be9 15 type ChargingProfile,
268a74bb 16 ChargingProfileKindType,
15068be9
JB
17 ChargingRateUnitType,
18 type ChargingSchedulePeriod,
268a74bb
JB
19 type ChargingStationInfo,
20 type ChargingStationTemplate,
dd08d43d 21 ConnectorPhaseRotation,
a78ef5ed 22 type ConnectorStatus,
c3b83130 23 ConnectorStatusEnum,
268a74bb 24 CurrentType,
ae25f265 25 type EvseTemplate,
268a74bb
JB
26 type OCPP16BootNotificationRequest,
27 type OCPP20BootNotificationRequest,
28 OCPPVersion,
29 RecurrencyKindType,
30 Voltage,
31} from '../types';
60a74391
JB
32import {
33 ACElectricUtils,
34 Configuration,
35 Constants,
36 DCElectricUtils,
37 Utils,
38 logger,
39} from '../utils';
268a74bb 40import { WorkerProcessType } from '../worker';
17ac262c 41
91a4f151
JB
42const moduleName = 'ChargingStationUtils';
43
17ac262c 44export class ChargingStationUtils {
d5bd1c00
JB
45 private constructor() {
46 // This is intentional
47 }
48
17ac262c
JB
49 public static getChargingStationId(
50 index: number,
51 stationTemplate: ChargingStationTemplate
52 ): string {
53 // In case of multiple instances: add instance index to charging station id
54 const instanceIndex = process.env.CF_INSTANCE_INDEX ?? 0;
1b271a54 55 const idSuffix = stationTemplate?.nameSuffix ?? '';
44eb6026 56 const idStr = `000000000${index.toString()}`;
ccb1d6e9 57 return stationTemplate?.fixedName
17ac262c 58 ? stationTemplate.baseName
44eb6026
JB
59 : `${stationTemplate.baseName}-${instanceIndex.toString()}${idStr.substring(
60 idStr.length - 4
61 )}${idSuffix}`;
17ac262c
JB
62 }
63
fa7bccf4 64 public static getHashId(index: number, stationTemplate: ChargingStationTemplate): string {
99e92237 65 const chargingStationInfo = {
fa7bccf4
JB
66 chargePointModel: stationTemplate.chargePointModel,
67 chargePointVendor: stationTemplate.chargePointVendor,
68 ...(!Utils.isUndefined(stationTemplate.chargeBoxSerialNumberPrefix) && {
69 chargeBoxSerialNumber: stationTemplate.chargeBoxSerialNumberPrefix,
17ac262c 70 }),
fa7bccf4
JB
71 ...(!Utils.isUndefined(stationTemplate.chargePointSerialNumberPrefix) && {
72 chargePointSerialNumber: stationTemplate.chargePointSerialNumberPrefix,
17ac262c 73 }),
fa7bccf4
JB
74 ...(!Utils.isUndefined(stationTemplate.meterSerialNumberPrefix) && {
75 meterSerialNumber: stationTemplate.meterSerialNumberPrefix,
17ac262c 76 }),
fa7bccf4
JB
77 ...(!Utils.isUndefined(stationTemplate.meterType) && {
78 meterType: stationTemplate.meterType,
17ac262c
JB
79 }),
80 };
81 return crypto
82 .createHash(Constants.DEFAULT_HASH_ALGORITHM)
fa7bccf4 83 .update(
14ecae6a
JB
84 `${JSON.stringify(chargingStationInfo)}${ChargingStationUtils.getChargingStationId(
85 index,
86 stationTemplate
87 )}`
fa7bccf4 88 )
17ac262c
JB
89 .digest('hex');
90 }
91
1bf29f5b
JB
92 public static checkChargingStation(chargingStation: ChargingStation, logPrefix: string): boolean {
93 if (chargingStation.started === false && chargingStation.starting === false) {
94 logger.warn(`${logPrefix} charging station is stopped, cannot proceed`);
95 return false;
96 }
97 return true;
98 }
99
be34dba5
JB
100 public static getPhaseRotationValue(
101 connectorId: number,
102 numberOfPhases: number
103 ): string | undefined {
dd08d43d
JB
104 // AC/DC
105 if (connectorId === 0 && numberOfPhases === 0) {
106 return `${connectorId}.${ConnectorPhaseRotation.RST}`;
107 } else if (connectorId > 0 && numberOfPhases === 0) {
108 return `${connectorId}.${ConnectorPhaseRotation.NotApplicable}`;
109 // AC
110 } else if (connectorId > 0 && numberOfPhases === 1) {
111 return `${connectorId}.${ConnectorPhaseRotation.NotApplicable}`;
112 } else if (connectorId > 0 && numberOfPhases === 3) {
113 return `${connectorId}.${ConnectorPhaseRotation.RST}`;
114 }
115 }
116
ae25f265
JB
117 public static getMaxNumberOfEvses(evses: Record<string, EvseTemplate>): number {
118 if (!evses) {
119 return -1;
120 }
121 return Object.keys(evses).length;
122 }
123
a78ef5ed
JB
124 public static getMaxNumberOfConnectors(connectors: Record<string, ConnectorStatus>): number {
125 if (!connectors) {
fa7bccf4
JB
126 return -1;
127 }
a78ef5ed 128 return Object.keys(connectors).length;
fa7bccf4
JB
129 }
130
c3b83130
JB
131 public static getBootConnectorStatus(
132 chargingStation: ChargingStation,
133 connectorId: number,
134 connectorStatus: ConnectorStatus
135 ): ConnectorStatusEnum {
136 let connectorBootStatus: ConnectorStatusEnum;
137 if (
138 !connectorStatus?.status &&
139 (chargingStation.isChargingStationAvailable() === false ||
140 chargingStation.isConnectorAvailable(connectorId) === false)
141 ) {
142 connectorBootStatus = ConnectorStatusEnum.Unavailable;
143 } else if (!connectorStatus?.status && connectorStatus?.bootStatus) {
144 // Set boot status in template at startup
145 connectorBootStatus = connectorStatus?.bootStatus;
146 } else if (connectorStatus?.status) {
147 // Set previous status at startup
148 connectorBootStatus = connectorStatus?.status;
149 } else {
150 // Set default status
151 connectorBootStatus = ConnectorStatusEnum.Available;
152 }
153 return connectorBootStatus;
154 }
155
8a133cc8
JB
156 public static checkTemplateFile(
157 stationTemplate: ChargingStationTemplate,
158 logPrefix: string,
159 templateFile: string
160 ) {
161 if (Utils.isNullOrUndefined(stationTemplate)) {
162 const errorMsg = `Failed to read charging station template file ${templateFile}`;
163 logger.error(`${logPrefix} ${errorMsg}`);
164 throw new BaseError(errorMsg);
165 }
166 if (Utils.isEmptyObject(stationTemplate)) {
167 const errorMsg = `Empty charging station information from template file ${templateFile}`;
168 logger.error(`${logPrefix} ${errorMsg}`);
169 throw new BaseError(errorMsg);
170 }
ac7f79af
JB
171 if (Utils.isEmptyObject(stationTemplate.AutomaticTransactionGenerator)) {
172 stationTemplate.AutomaticTransactionGenerator = {
173 enable: false,
174 minDuration: 60,
175 maxDuration: 120,
176 minDelayBetweenTwoTransactions: 15,
177 maxDelayBetweenTwoTransactions: 30,
178 probabilityOfStart: 1,
179 stopAfterHours: 0.3,
180 stopOnConnectionFailure: true,
181 };
182 logger.warn(
183 `${logPrefix} Empty automatic transaction generator configuration from template file ${templateFile}, set to default values`
184 );
185 }
8a133cc8
JB
186 }
187
cda5d0fb 188 public static checkConnectorsConfiguration(
8a133cc8
JB
189 stationTemplate: ChargingStationTemplate,
190 logPrefix: string,
191 templateFile: string
cda5d0fb
JB
192 ): {
193 configuredMaxConnectors: number;
194 templateMaxConnectors: number;
195 templateMaxAvailableConnectors: number;
196 } {
197 const configuredMaxConnectors =
198 ChargingStationUtils.getConfiguredNumberOfConnectors(stationTemplate);
199 ChargingStationUtils.checkConfiguredMaxConnectors(
200 configuredMaxConnectors,
8a133cc8
JB
201 logPrefix,
202 templateFile
cda5d0fb
JB
203 );
204 const templateMaxConnectors = ChargingStationUtils.getMaxNumberOfConnectors(
205 stationTemplate.Connectors
206 );
8a133cc8 207 ChargingStationUtils.checkTemplateMaxConnectors(templateMaxConnectors, logPrefix, templateFile);
cda5d0fb
JB
208 const templateMaxAvailableConnectors = stationTemplate?.Connectors[0]
209 ? templateMaxConnectors - 1
210 : templateMaxConnectors;
211 if (
212 configuredMaxConnectors > templateMaxAvailableConnectors &&
213 !stationTemplate?.randomConnectors
214 ) {
fa7bccf4 215 logger.warn(
cda5d0fb 216 `${logPrefix} Number of connectors exceeds the number of connector configurations in template ${templateFile}, forcing random connector configurations affectation`
fa7bccf4 217 );
cda5d0fb 218 stationTemplate.randomConnectors = true;
fa7bccf4 219 }
cda5d0fb 220 return { configuredMaxConnectors, templateMaxConnectors, templateMaxAvailableConnectors };
fa7bccf4
JB
221 }
222
04b1261c
JB
223 public static checkStationInfoConnectorStatus(
224 connectorId: number,
225 connectorStatus: ConnectorStatus,
226 logPrefix: string,
227 templateFile: string
228 ): void {
229 if (!Utils.isNullOrUndefined(connectorStatus?.status)) {
230 logger.warn(
54ebb82c 231 `${logPrefix} Charging station information from template ${templateFile} with connector id ${connectorId} status configuration defined, undefine it`
04b1261c
JB
232 );
233 delete connectorStatus.status;
234 }
235 }
236
237 public static buildConnectorsMap(
238 connectors: Record<string, ConnectorStatus>,
239 logPrefix: string,
240 templateFile: string
241 ): Map<number, ConnectorStatus> {
242 const connectorsMap = new Map<number, ConnectorStatus>();
ae25f265
JB
243 if (ChargingStationUtils.getMaxNumberOfConnectors(connectors) > 0) {
244 for (const connector in connectors) {
245 const connectorStatus = connectors[connector];
246 const connectorId = Utils.convertToInt(connector);
247 ChargingStationUtils.checkStationInfoConnectorStatus(
248 connectorId,
249 connectorStatus,
250 logPrefix,
251 templateFile
252 );
253 connectorsMap.set(connectorId, Utils.cloneObject<ConnectorStatus>(connectorStatus));
04b1261c 254 }
ae25f265
JB
255 } else {
256 logger.warn(
257 `${logPrefix} Charging station information from template ${templateFile} with no connectors, cannot build connectors map`
258 );
04b1261c
JB
259 }
260 return connectorsMap;
261 }
262
263 public static initializeConnectorsMapStatus(
264 connectors: Map<number, ConnectorStatus>,
265 logPrefix: string
266 ): void {
267 for (const connectorId of connectors.keys()) {
268 if (connectorId > 0 && connectors.get(connectorId)?.transactionStarted === true) {
269 logger.warn(
54ebb82c 270 `${logPrefix} Connector id ${connectorId} at initialization has a transaction started with id ${
04b1261c
JB
271 connectors.get(connectorId)?.transactionId
272 }`
273 );
274 }
8e2430ee 275 if (connectorId === 0) {
52952bf8
JB
276 connectors.get(connectorId).availability = AvailabilityType.Operative;
277 if (Utils.isUndefined(connectors.get(connectorId)?.chargingProfiles)) {
278 connectors.get(connectorId).chargingProfiles = [];
279 }
280 } else if (
04b1261c
JB
281 connectorId > 0 &&
282 Utils.isNullOrUndefined(connectors.get(connectorId)?.transactionStarted)
283 ) {
284 ChargingStationUtils.initializeConnectorStatus(connectors.get(connectorId));
285 }
286 }
287 }
288
04b1261c
JB
289 public static resetConnectorStatus(connectorStatus: ConnectorStatus): void {
290 connectorStatus.idTagLocalAuthorized = false;
291 connectorStatus.idTagAuthorized = false;
292 connectorStatus.transactionRemoteStarted = false;
293 connectorStatus.transactionStarted = false;
294 delete connectorStatus?.localAuthorizeIdTag;
295 delete connectorStatus?.authorizeIdTag;
296 delete connectorStatus?.transactionId;
297 delete connectorStatus?.transactionIdTag;
298 connectorStatus.transactionEnergyActiveImportRegisterValue = 0;
299 delete connectorStatus?.transactionBeginMeterValue;
300 }
301
17ac262c 302 public static createBootNotificationRequest(
15068be9
JB
303 stationInfo: ChargingStationInfo,
304 bootReason: BootReasonEnumType = BootReasonEnumType.PowerUp
17ac262c 305 ): BootNotificationRequest {
d270cc87
JB
306 const ocppVersion = stationInfo.ocppVersion ?? OCPPVersion.VERSION_16;
307 switch (ocppVersion) {
308 case OCPPVersion.VERSION_16:
309 return {
310 chargePointModel: stationInfo.chargePointModel,
311 chargePointVendor: stationInfo.chargePointVendor,
312 ...(!Utils.isUndefined(stationInfo.chargeBoxSerialNumber) && {
313 chargeBoxSerialNumber: stationInfo.chargeBoxSerialNumber,
314 }),
315 ...(!Utils.isUndefined(stationInfo.chargePointSerialNumber) && {
316 chargePointSerialNumber: stationInfo.chargePointSerialNumber,
317 }),
318 ...(!Utils.isUndefined(stationInfo.firmwareVersion) && {
319 firmwareVersion: stationInfo.firmwareVersion,
320 }),
321 ...(!Utils.isUndefined(stationInfo.iccid) && { iccid: stationInfo.iccid }),
322 ...(!Utils.isUndefined(stationInfo.imsi) && { imsi: stationInfo.imsi }),
323 ...(!Utils.isUndefined(stationInfo.meterSerialNumber) && {
324 meterSerialNumber: stationInfo.meterSerialNumber,
325 }),
326 ...(!Utils.isUndefined(stationInfo.meterType) && {
327 meterType: stationInfo.meterType,
328 }),
329 } as OCPP16BootNotificationRequest;
330 case OCPPVersion.VERSION_20:
331 case OCPPVersion.VERSION_201:
332 return {
15068be9 333 reason: bootReason,
d270cc87
JB
334 chargingStation: {
335 model: stationInfo.chargePointModel,
336 vendorName: stationInfo.chargePointVendor,
337 ...(!Utils.isUndefined(stationInfo.firmwareVersion) && {
338 firmwareVersion: stationInfo.firmwareVersion,
339 }),
340 ...(!Utils.isUndefined(stationInfo.chargeBoxSerialNumber) && {
341 serialNumber: stationInfo.chargeBoxSerialNumber,
342 }),
98fc1389
JB
343 ...((!Utils.isUndefined(stationInfo.iccid) || !Utils.isUndefined(stationInfo.imsi)) && {
344 modem: {
345 ...(!Utils.isUndefined(stationInfo.iccid) && { iccid: stationInfo.iccid }),
346 ...(!Utils.isUndefined(stationInfo.imsi) && { imsi: stationInfo.imsi }),
347 },
348 }),
d270cc87
JB
349 },
350 } as OCPP20BootNotificationRequest;
351 }
17ac262c
JB
352 }
353
354 public static workerPoolInUse(): boolean {
721646e9 355 return [WorkerProcessType.dynamicPool, WorkerProcessType.staticPool].includes(
cf2a5d9b 356 Configuration.getWorker().processType
17ac262c
JB
357 );
358 }
359
360 public static workerDynamicPoolInUse(): boolean {
721646e9 361 return Configuration.getWorker().processType === WorkerProcessType.dynamicPool;
17ac262c
JB
362 }
363
ae5020a3 364 public static warnTemplateKeysDeprecation(
ae5020a3 365 stationTemplate: ChargingStationTemplate,
8a133cc8
JB
366 logPrefix: string,
367 templateFile: string
ae5020a3
JB
368 ) {
369 const templateKeys: { key: string; deprecatedKey: string }[] = [
370 { key: 'supervisionUrls', deprecatedKey: 'supervisionUrl' },
371 { key: 'idTagsFile', deprecatedKey: 'authorizationFile' },
372 ];
373 for (const templateKey of templateKeys) {
374 ChargingStationUtils.warnDeprecatedTemplateKey(
375 stationTemplate,
376 templateKey.deprecatedKey,
ae5020a3 377 logPrefix,
8a133cc8 378 templateFile,
ae5020a3
JB
379 `Use '${templateKey.key}' instead`
380 );
381 ChargingStationUtils.convertDeprecatedTemplateKey(
382 stationTemplate,
383 templateKey.deprecatedKey,
384 templateKey.key
385 );
17ac262c
JB
386 }
387 }
388
fa7bccf4
JB
389 public static stationTemplateToStationInfo(
390 stationTemplate: ChargingStationTemplate
391 ): ChargingStationInfo {
8df5ae48 392 stationTemplate = Utils.cloneObject<ChargingStationTemplate>(stationTemplate);
fa7bccf4
JB
393 delete stationTemplate.power;
394 delete stationTemplate.powerUnit;
8a133cc8
JB
395 delete stationTemplate?.Connectors;
396 delete stationTemplate?.Evses;
fa7bccf4
JB
397 delete stationTemplate.Configuration;
398 delete stationTemplate.AutomaticTransactionGenerator;
399 delete stationTemplate.chargeBoxSerialNumberPrefix;
400 delete stationTemplate.chargePointSerialNumberPrefix;
fec4d204 401 delete stationTemplate.meterSerialNumberPrefix;
51c83d6f 402 return stationTemplate as unknown as ChargingStationInfo;
fa7bccf4
JB
403 }
404
17ac262c 405 public static createSerialNumber(
fa7bccf4 406 stationTemplate: ChargingStationTemplate,
4d20f040 407 stationInfo: ChargingStationInfo,
fa7bccf4
JB
408 params: {
409 randomSerialNumberUpperCase?: boolean;
410 randomSerialNumber?: boolean;
411 } = {
17ac262c
JB
412 randomSerialNumberUpperCase: true,
413 randomSerialNumber: true,
414 }
415 ): void {
f832e5df 416 params = { ...{ randomSerialNumberUpperCase: true, randomSerialNumber: true }, ...params };
fa7bccf4
JB
417 const serialNumberSuffix = params?.randomSerialNumber
418 ? ChargingStationUtils.getRandomSerialNumberSuffix({
419 upperCase: params.randomSerialNumberUpperCase,
420 })
421 : '';
f832e5df
JB
422 Utils.isNotEmptyString(stationTemplate?.chargePointSerialNumberPrefix) &&
423 (stationInfo.chargePointSerialNumber = `${stationTemplate.chargePointSerialNumberPrefix}${serialNumberSuffix}`);
424 Utils.isNotEmptyString(stationTemplate?.chargeBoxSerialNumberPrefix) &&
425 (stationInfo.chargeBoxSerialNumber = `${stationTemplate.chargeBoxSerialNumberPrefix}${serialNumberSuffix}`);
426 Utils.isNotEmptyString(stationTemplate?.meterSerialNumberPrefix) &&
427 (stationInfo.meterSerialNumber = `${stationTemplate.meterSerialNumberPrefix}${serialNumberSuffix}`);
fec4d204
JB
428 }
429
430 public static propagateSerialNumber(
431 stationTemplate: ChargingStationTemplate,
432 stationInfoSrc: ChargingStationInfo,
4d20f040 433 stationInfoDst: ChargingStationInfo
fec4d204
JB
434 ) {
435 if (!stationInfoSrc || !stationTemplate) {
baf93dda
JB
436 throw new BaseError(
437 'Missing charging station template or existing configuration to propagate serial number'
438 );
fec4d204
JB
439 }
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;
17ac262c
JB
449 }
450
451 public static 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
15068be9
JB
467 public static getChargingStationConnectorChargingProfilesPowerLimit(
468 chargingStation: ChargingStation,
469 connectorId: number
470 ): number | undefined {
471 let limit: number, matchingChargingProfile: ChargingProfile;
15068be9 472 // Get charging profiles for connector and sort by stack level
66a62eac 473 const chargingProfiles =
8df5ae48
JB
474 Utils.cloneObject<ChargingProfile[]>(
475 chargingStation.getConnectorStatus(connectorId)?.chargingProfiles
476 )?.sort((a, b) => b.stackLevel - a.stackLevel) ?? [];
15068be9 477 // Get profiles on connector 0
1895299d 478 if (chargingStation.getConnectorStatus(0)?.chargingProfiles) {
15068be9 479 chargingProfiles.push(
8df5ae48
JB
480 ...Utils.cloneObject<ChargingProfile[]>(
481 chargingStation.getConnectorStatus(0).chargingProfiles
482 ).sort((a, b) => b.stackLevel - a.stackLevel)
15068be9
JB
483 );
484 }
53ac516c 485 if (Utils.isNotEmptyArray(chargingProfiles)) {
15068be9
JB
486 const result = ChargingStationUtils.getLimitFromChargingProfiles(
487 chargingProfiles,
488 chargingStation.logPrefix()
489 );
490 if (!Utils.isNullOrUndefined(result)) {
1895299d
JB
491 limit = result?.limit;
492 matchingChargingProfile = result?.matchingChargingProfile;
15068be9
JB
493 switch (chargingStation.getCurrentOutType()) {
494 case CurrentType.AC:
495 limit =
496 matchingChargingProfile.chargingSchedule.chargingRateUnit ===
497 ChargingRateUnitType.WATT
498 ? limit
499 : ACElectricUtils.powerTotal(
500 chargingStation.getNumberOfPhases(),
501 chargingStation.getVoltageOut(),
502 limit
503 );
504 break;
505 case CurrentType.DC:
506 limit =
507 matchingChargingProfile.chargingSchedule.chargingRateUnit ===
508 ChargingRateUnitType.WATT
509 ? limit
510 : DCElectricUtils.power(chargingStation.getVoltageOut(), limit);
511 }
512 const connectorMaximumPower =
513 chargingStation.getMaximumPower() / chargingStation.powerDivider;
514 if (limit > connectorMaximumPower) {
515 logger.error(
516 `${chargingStation.logPrefix()} Charging profile id ${
517 matchingChargingProfile.chargingProfileId
518 } limit ${limit} is greater than connector id ${connectorId} maximum ${connectorMaximumPower}: %j`,
519 result
520 );
521 limit = connectorMaximumPower;
522 }
523 }
524 }
525 return limit;
526 }
527
528 public static getDefaultVoltageOut(
529 currentType: CurrentType,
8a133cc8
JB
530 logPrefix: string,
531 templateFile: string
15068be9 532 ): Voltage {
ded57f02 533 const errorMsg = `Unknown ${currentType} currentOutType in template file ${templateFile}, cannot define default voltage out`;
15068be9
JB
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:
ded57f02
JB
543 logger.error(`${logPrefix} ${errorMsg}`);
544 throw new BaseError(errorMsg);
15068be9
JB
545 }
546 return defaultVoltageOut;
547 }
548
e302df1d 549 public static getIdTagsFile(stationInfo: ChargingStationInfo): string | undefined {
15068be9 550 return (
e302df1d 551 stationInfo.idTagsFile &&
15068be9 552 path.join(
51022aa0 553 path.dirname(fileURLToPath(import.meta.url)),
15068be9 554 'assets',
e302df1d 555 path.basename(stationInfo.idTagsFile)
15068be9
JB
556 )
557 );
558 }
559
8a133cc8 560 private static getConfiguredNumberOfConnectors(stationTemplate: ChargingStationTemplate): number {
cda5d0fb
JB
561 let configuredMaxConnectors: number;
562 if (Utils.isNotEmptyArray(stationTemplate.numberOfConnectors) === true) {
563 const numberOfConnectors = stationTemplate.numberOfConnectors as number[];
564 configuredMaxConnectors =
565 numberOfConnectors[Math.floor(Utils.secureRandom() * numberOfConnectors.length)];
566 } else if (Utils.isUndefined(stationTemplate.numberOfConnectors) === false) {
567 configuredMaxConnectors = stationTemplate.numberOfConnectors as number;
568 } else if (stationTemplate.Connectors && !stationTemplate.Evses) {
569 configuredMaxConnectors = stationTemplate?.Connectors[0]
570 ? ChargingStationUtils.getMaxNumberOfConnectors(stationTemplate.Connectors) - 1
571 : ChargingStationUtils.getMaxNumberOfConnectors(stationTemplate.Connectors);
572 } else if (stationTemplate.Evses && !stationTemplate.Connectors) {
573 configuredMaxConnectors = 0;
574 for (const evse in stationTemplate.Evses) {
575 if (evse === '0') {
576 continue;
577 }
578 configuredMaxConnectors += ChargingStationUtils.getMaxNumberOfConnectors(
579 stationTemplate.Evses[evse].Connectors
580 );
581 }
582 }
583 return configuredMaxConnectors;
584 }
585
586 private static checkConfiguredMaxConnectors(
587 configuredMaxConnectors: number,
8a133cc8
JB
588 logPrefix: string,
589 templateFile: string
cda5d0fb
JB
590 ): void {
591 if (configuredMaxConnectors <= 0) {
592 logger.warn(
593 `${logPrefix} Charging station information from template ${templateFile} with ${configuredMaxConnectors} connectors`
594 );
595 }
596 }
597
598 private static checkTemplateMaxConnectors(
599 templateMaxConnectors: number,
8a133cc8
JB
600 logPrefix: string,
601 templateFile: string
cda5d0fb
JB
602 ): void {
603 if (templateMaxConnectors === 0) {
604 logger.warn(
605 `${logPrefix} Charging station information from template ${templateFile} with empty connectors configuration`
606 );
607 } else if (templateMaxConnectors < 0) {
608 logger.error(
609 `${logPrefix} Charging station information from template ${templateFile} with no connectors configuration defined`
610 );
611 }
612 }
613
52952bf8
JB
614 private static initializeConnectorStatus(connectorStatus: ConnectorStatus): void {
615 connectorStatus.availability = AvailabilityType.Operative;
616 connectorStatus.idTagLocalAuthorized = false;
617 connectorStatus.idTagAuthorized = false;
618 connectorStatus.transactionRemoteStarted = false;
619 connectorStatus.transactionStarted = false;
620 connectorStatus.energyActiveImportRegisterValue = 0;
621 connectorStatus.transactionEnergyActiveImportRegisterValue = 0;
622 if (Utils.isUndefined(connectorStatus.chargingProfiles)) {
623 connectorStatus.chargingProfiles = [];
624 }
625 }
626
ae5020a3
JB
627 private static warnDeprecatedTemplateKey(
628 template: ChargingStationTemplate,
629 key: string,
ae5020a3 630 logPrefix: string,
8a133cc8 631 templateFile: string,
ae5020a3
JB
632 logMsgToAppend = ''
633 ): void {
634 if (!Utils.isUndefined(template[key])) {
635 const logMsg = `Deprecated template key '${key}' usage in file '${templateFile}'${
636 Utils.isNotEmptyString(logMsgToAppend) ? `. ${logMsgToAppend}` : ''
637 }`;
638 logger.warn(`${logPrefix} ${logMsg}`);
639 console.warn(chalk.yellow(`${logMsg}`));
640 }
641 }
642
643 private static convertDeprecatedTemplateKey(
644 template: ChargingStationTemplate,
645 deprecatedKey: string,
646 key: string
647 ): void {
648 if (!Utils.isUndefined(template[deprecatedKey])) {
649 template[key] = template[deprecatedKey] as unknown;
650 delete template[deprecatedKey];
651 }
652 }
653
17ac262c 654 /**
54ebb82c 655 * Charging profiles should already be sorted by connector id and stack level (highest stack level has priority)
17ac262c 656 *
0e4fa348
JB
657 * @param chargingProfiles -
658 * @param logPrefix -
659 * @returns
17ac262c 660 */
15068be9 661 private static getLimitFromChargingProfiles(
17ac262c
JB
662 chargingProfiles: ChargingProfile[],
663 logPrefix: string
664 ): {
665 limit: number;
666 matchingChargingProfile: ChargingProfile;
667 } | null {
91a4f151 668 const debugLogMsg = `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Matching charging profile found for power limitation: %j`;
41189456
JB
669 const currentMoment = moment();
670 const currentDate = new Date();
17ac262c
JB
671 for (const chargingProfile of chargingProfiles) {
672 // Set helpers
17ac262c 673 const chargingSchedule = chargingProfile.chargingSchedule;
41189456
JB
674 if (!chargingSchedule?.startSchedule) {
675 logger.warn(
676 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: startSchedule is not defined in charging profile id ${chargingProfile.chargingProfileId}`
677 );
678 }
17ac262c
JB
679 // Check type (recurring) and if it is already active
680 // Adjust the daily recurring schedule to today
681 if (
682 chargingProfile.chargingProfileKind === ChargingProfileKindType.RECURRING &&
683 chargingProfile.recurrencyKind === RecurrencyKindType.DAILY &&
684 currentMoment.isAfter(chargingSchedule.startSchedule)
685 ) {
41189456
JB
686 if (!(chargingSchedule?.startSchedule instanceof Date)) {
687 logger.warn(
688 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: startSchedule is not a Date object in charging profile id ${chargingProfile.chargingProfileId}. Trying to convert it to a Date object`
689 );
690 chargingSchedule.startSchedule = new Date(chargingSchedule.startSchedule);
691 }
17ac262c
JB
692 chargingSchedule.startSchedule.setFullYear(
693 currentDate.getFullYear(),
694 currentDate.getMonth(),
695 currentDate.getDate()
696 );
697 // Check if the start of the schedule is yesterday
698 if (moment(chargingSchedule.startSchedule).isAfter(currentMoment)) {
699 chargingSchedule.startSchedule.setDate(currentDate.getDate() - 1);
700 }
701 } else if (moment(chargingSchedule.startSchedule).isAfter(currentMoment)) {
702 return null;
703 }
704 // Check if the charging profile is active
705 if (
706 moment(chargingSchedule.startSchedule)
707 .add(chargingSchedule.duration, 's')
708 .isAfter(currentMoment)
709 ) {
710 let lastButOneSchedule: ChargingSchedulePeriod;
711 // Search the right schedule period
712 for (const schedulePeriod of chargingSchedule.chargingSchedulePeriod) {
713 // Handling of only one period
714 if (
715 chargingSchedule.chargingSchedulePeriod.length === 1 &&
716 schedulePeriod.startPeriod === 0
717 ) {
718 const result = {
719 limit: schedulePeriod.limit,
720 matchingChargingProfile: chargingProfile,
721 };
91a4f151 722 logger.debug(debugLogMsg, result);
17ac262c
JB
723 return result;
724 }
725 // Find the right schedule period
726 if (
727 moment(chargingSchedule.startSchedule)
728 .add(schedulePeriod.startPeriod, 's')
729 .isAfter(currentMoment)
730 ) {
731 // Found the schedule: last but one is the correct one
732 const result = {
733 limit: lastButOneSchedule.limit,
734 matchingChargingProfile: chargingProfile,
735 };
91a4f151 736 logger.debug(debugLogMsg, result);
17ac262c
JB
737 return result;
738 }
739 // Keep it
740 lastButOneSchedule = schedulePeriod;
741 // Handle the last schedule period
742 if (
743 schedulePeriod.startPeriod ===
744 chargingSchedule.chargingSchedulePeriod[
745 chargingSchedule.chargingSchedulePeriod.length - 1
746 ].startPeriod
747 ) {
748 const result = {
749 limit: lastButOneSchedule.limit,
750 matchingChargingProfile: chargingProfile,
751 };
91a4f151 752 logger.debug(debugLogMsg, result);
17ac262c
JB
753 return result;
754 }
755 }
756 }
757 }
758 return null;
759 }
760
761 private static getRandomSerialNumberSuffix(params?: {
762 randomBytesLength?: number;
763 upperCase?: boolean;
764 }): string {
765 const randomSerialNumberSuffix = crypto
766 .randomBytes(params?.randomBytesLength ?? 16)
767 .toString('hex');
768 if (params?.upperCase) {
769 return randomSerialNumberSuffix.toUpperCase();
770 }
771 return randomSerialNumberSuffix;
772 }
773}