build(simulator): constants, requests and responses for reservation scenario added
[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';
aa7d6d95 32import { ACElectricUtils, Constants, DCElectricUtils, Utils, logger } from '../utils';
17ac262c 33
91a4f151
JB
34const moduleName = 'ChargingStationUtils';
35
17ac262c 36export class ChargingStationUtils {
d5bd1c00
JB
37 private constructor() {
38 // This is intentional
39 }
40
17ac262c
JB
41 public static getChargingStationId(
42 index: number,
43 stationTemplate: ChargingStationTemplate
44 ): string {
45 // In case of multiple instances: add instance index to charging station id
46 const instanceIndex = process.env.CF_INSTANCE_INDEX ?? 0;
1b271a54 47 const idSuffix = stationTemplate?.nameSuffix ?? '';
44eb6026 48 const idStr = `000000000${index.toString()}`;
ccb1d6e9 49 return stationTemplate?.fixedName
17ac262c 50 ? stationTemplate.baseName
44eb6026
JB
51 : `${stationTemplate.baseName}-${instanceIndex.toString()}${idStr.substring(
52 idStr.length - 4
53 )}${idSuffix}`;
17ac262c
JB
54 }
55
fa7bccf4 56 public static getHashId(index: number, stationTemplate: ChargingStationTemplate): string {
99e92237 57 const chargingStationInfo = {
fa7bccf4
JB
58 chargePointModel: stationTemplate.chargePointModel,
59 chargePointVendor: stationTemplate.chargePointVendor,
60 ...(!Utils.isUndefined(stationTemplate.chargeBoxSerialNumberPrefix) && {
61 chargeBoxSerialNumber: stationTemplate.chargeBoxSerialNumberPrefix,
17ac262c 62 }),
fa7bccf4
JB
63 ...(!Utils.isUndefined(stationTemplate.chargePointSerialNumberPrefix) && {
64 chargePointSerialNumber: stationTemplate.chargePointSerialNumberPrefix,
17ac262c 65 }),
fa7bccf4
JB
66 ...(!Utils.isUndefined(stationTemplate.meterSerialNumberPrefix) && {
67 meterSerialNumber: stationTemplate.meterSerialNumberPrefix,
17ac262c 68 }),
fa7bccf4
JB
69 ...(!Utils.isUndefined(stationTemplate.meterType) && {
70 meterType: stationTemplate.meterType,
17ac262c
JB
71 }),
72 };
73 return crypto
74 .createHash(Constants.DEFAULT_HASH_ALGORITHM)
fa7bccf4 75 .update(
14ecae6a
JB
76 `${JSON.stringify(chargingStationInfo)}${ChargingStationUtils.getChargingStationId(
77 index,
78 stationTemplate
79 )}`
fa7bccf4 80 )
17ac262c
JB
81 .digest('hex');
82 }
83
1bf29f5b
JB
84 public static checkChargingStation(chargingStation: ChargingStation, logPrefix: string): boolean {
85 if (chargingStation.started === false && chargingStation.starting === false) {
86 logger.warn(`${logPrefix} charging station is stopped, cannot proceed`);
87 return false;
88 }
89 return true;
90 }
91
be34dba5
JB
92 public static getPhaseRotationValue(
93 connectorId: number,
94 numberOfPhases: number
95 ): string | undefined {
dd08d43d
JB
96 // AC/DC
97 if (connectorId === 0 && numberOfPhases === 0) {
98 return `${connectorId}.${ConnectorPhaseRotation.RST}`;
99 } else if (connectorId > 0 && numberOfPhases === 0) {
100 return `${connectorId}.${ConnectorPhaseRotation.NotApplicable}`;
101 // AC
102 } else if (connectorId > 0 && numberOfPhases === 1) {
103 return `${connectorId}.${ConnectorPhaseRotation.NotApplicable}`;
104 } else if (connectorId > 0 && numberOfPhases === 3) {
105 return `${connectorId}.${ConnectorPhaseRotation.RST}`;
106 }
107 }
108
ae25f265
JB
109 public static getMaxNumberOfEvses(evses: Record<string, EvseTemplate>): number {
110 if (!evses) {
111 return -1;
112 }
113 return Object.keys(evses).length;
114 }
115
a78ef5ed
JB
116 public static getMaxNumberOfConnectors(connectors: Record<string, ConnectorStatus>): number {
117 if (!connectors) {
fa7bccf4
JB
118 return -1;
119 }
a78ef5ed 120 return Object.keys(connectors).length;
fa7bccf4
JB
121 }
122
c3b83130
JB
123 public static getBootConnectorStatus(
124 chargingStation: ChargingStation,
125 connectorId: number,
126 connectorStatus: ConnectorStatus
127 ): ConnectorStatusEnum {
128 let connectorBootStatus: ConnectorStatusEnum;
129 if (
130 !connectorStatus?.status &&
131 (chargingStation.isChargingStationAvailable() === false ||
132 chargingStation.isConnectorAvailable(connectorId) === false)
133 ) {
134 connectorBootStatus = ConnectorStatusEnum.Unavailable;
135 } else if (!connectorStatus?.status && connectorStatus?.bootStatus) {
136 // Set boot status in template at startup
137 connectorBootStatus = connectorStatus?.bootStatus;
138 } else if (connectorStatus?.status) {
139 // Set previous status at startup
140 connectorBootStatus = connectorStatus?.status;
141 } else {
142 // Set default status
143 connectorBootStatus = ConnectorStatusEnum.Available;
144 }
145 return connectorBootStatus;
146 }
147
e1893686 148 public static checkTemplate(
8a133cc8
JB
149 stationTemplate: ChargingStationTemplate,
150 logPrefix: string,
151 templateFile: string
152 ) {
153 if (Utils.isNullOrUndefined(stationTemplate)) {
154 const errorMsg = `Failed to read charging station template file ${templateFile}`;
155 logger.error(`${logPrefix} ${errorMsg}`);
156 throw new BaseError(errorMsg);
157 }
158 if (Utils.isEmptyObject(stationTemplate)) {
159 const errorMsg = `Empty charging station information from template file ${templateFile}`;
160 logger.error(`${logPrefix} ${errorMsg}`);
161 throw new BaseError(errorMsg);
162 }
ac7f79af
JB
163 if (Utils.isEmptyObject(stationTemplate.AutomaticTransactionGenerator)) {
164 stationTemplate.AutomaticTransactionGenerator = {
165 enable: false,
166 minDuration: 60,
167 maxDuration: 120,
168 minDelayBetweenTwoTransactions: 15,
169 maxDelayBetweenTwoTransactions: 30,
170 probabilityOfStart: 1,
171 stopAfterHours: 0.3,
172 stopOnConnectionFailure: true,
173 };
174 logger.warn(
175 `${logPrefix} Empty automatic transaction generator configuration from template file ${templateFile}, set to default values`
176 );
177 }
111aaf89
JB
178 if (
179 Utils.isNullOrUndefined(stationTemplate.idTagsFile) ||
180 Utils.isEmptyString(stationTemplate.idTagsFile)
181 ) {
182 logger.warn(
183 `${logPrefix} Missing id tags file in template file ${templateFile}. That can lead to issues with the Automatic Transaction Generator`
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
ae5020a3 354 public static warnTemplateKeysDeprecation(
ae5020a3 355 stationTemplate: ChargingStationTemplate,
8a133cc8
JB
356 logPrefix: string,
357 templateFile: string
ae5020a3
JB
358 ) {
359 const templateKeys: { key: string; deprecatedKey: string }[] = [
360 { key: 'supervisionUrls', deprecatedKey: 'supervisionUrl' },
361 { key: 'idTagsFile', deprecatedKey: 'authorizationFile' },
362 ];
363 for (const templateKey of templateKeys) {
364 ChargingStationUtils.warnDeprecatedTemplateKey(
365 stationTemplate,
366 templateKey.deprecatedKey,
ae5020a3 367 logPrefix,
8a133cc8 368 templateFile,
ae5020a3
JB
369 `Use '${templateKey.key}' instead`
370 );
371 ChargingStationUtils.convertDeprecatedTemplateKey(
372 stationTemplate,
373 templateKey.deprecatedKey,
374 templateKey.key
375 );
17ac262c
JB
376 }
377 }
378
fa7bccf4
JB
379 public static stationTemplateToStationInfo(
380 stationTemplate: ChargingStationTemplate
381 ): ChargingStationInfo {
8df5ae48 382 stationTemplate = Utils.cloneObject<ChargingStationTemplate>(stationTemplate);
fa7bccf4
JB
383 delete stationTemplate.power;
384 delete stationTemplate.powerUnit;
8a133cc8
JB
385 delete stationTemplate?.Connectors;
386 delete stationTemplate?.Evses;
fa7bccf4
JB
387 delete stationTemplate.Configuration;
388 delete stationTemplate.AutomaticTransactionGenerator;
389 delete stationTemplate.chargeBoxSerialNumberPrefix;
390 delete stationTemplate.chargePointSerialNumberPrefix;
fec4d204 391 delete stationTemplate.meterSerialNumberPrefix;
51c83d6f 392 return stationTemplate as unknown as ChargingStationInfo;
fa7bccf4
JB
393 }
394
17ac262c 395 public static createSerialNumber(
fa7bccf4 396 stationTemplate: ChargingStationTemplate,
4d20f040 397 stationInfo: ChargingStationInfo,
fa7bccf4
JB
398 params: {
399 randomSerialNumberUpperCase?: boolean;
400 randomSerialNumber?: boolean;
401 } = {
17ac262c
JB
402 randomSerialNumberUpperCase: true,
403 randomSerialNumber: true,
404 }
405 ): void {
f832e5df 406 params = { ...{ randomSerialNumberUpperCase: true, randomSerialNumber: true }, ...params };
fa7bccf4
JB
407 const serialNumberSuffix = params?.randomSerialNumber
408 ? ChargingStationUtils.getRandomSerialNumberSuffix({
409 upperCase: params.randomSerialNumberUpperCase,
410 })
411 : '';
f832e5df
JB
412 Utils.isNotEmptyString(stationTemplate?.chargePointSerialNumberPrefix) &&
413 (stationInfo.chargePointSerialNumber = `${stationTemplate.chargePointSerialNumberPrefix}${serialNumberSuffix}`);
414 Utils.isNotEmptyString(stationTemplate?.chargeBoxSerialNumberPrefix) &&
415 (stationInfo.chargeBoxSerialNumber = `${stationTemplate.chargeBoxSerialNumberPrefix}${serialNumberSuffix}`);
416 Utils.isNotEmptyString(stationTemplate?.meterSerialNumberPrefix) &&
417 (stationInfo.meterSerialNumber = `${stationTemplate.meterSerialNumberPrefix}${serialNumberSuffix}`);
fec4d204
JB
418 }
419
420 public static propagateSerialNumber(
421 stationTemplate: ChargingStationTemplate,
422 stationInfoSrc: ChargingStationInfo,
4d20f040 423 stationInfoDst: ChargingStationInfo
fec4d204
JB
424 ) {
425 if (!stationInfoSrc || !stationTemplate) {
baf93dda
JB
426 throw new BaseError(
427 'Missing charging station template or existing configuration to propagate serial number'
428 );
fec4d204
JB
429 }
430 stationTemplate?.chargePointSerialNumberPrefix && stationInfoSrc?.chargePointSerialNumber
431 ? (stationInfoDst.chargePointSerialNumber = stationInfoSrc.chargePointSerialNumber)
432 : stationInfoDst?.chargePointSerialNumber && delete stationInfoDst.chargePointSerialNumber;
433 stationTemplate?.chargeBoxSerialNumberPrefix && stationInfoSrc?.chargeBoxSerialNumber
434 ? (stationInfoDst.chargeBoxSerialNumber = stationInfoSrc.chargeBoxSerialNumber)
435 : stationInfoDst?.chargeBoxSerialNumber && delete stationInfoDst.chargeBoxSerialNumber;
436 stationTemplate?.meterSerialNumberPrefix && stationInfoSrc?.meterSerialNumber
437 ? (stationInfoDst.meterSerialNumber = stationInfoSrc.meterSerialNumber)
438 : stationInfoDst?.meterSerialNumber && delete stationInfoDst.meterSerialNumber;
17ac262c
JB
439 }
440
441 public static getAmperageLimitationUnitDivider(stationInfo: ChargingStationInfo): number {
442 let unitDivider = 1;
443 switch (stationInfo.amperageLimitationUnit) {
444 case AmpereUnits.DECI_AMPERE:
445 unitDivider = 10;
446 break;
447 case AmpereUnits.CENTI_AMPERE:
448 unitDivider = 100;
449 break;
450 case AmpereUnits.MILLI_AMPERE:
451 unitDivider = 1000;
452 break;
453 }
454 return unitDivider;
455 }
456
15068be9
JB
457 public static getChargingStationConnectorChargingProfilesPowerLimit(
458 chargingStation: ChargingStation,
459 connectorId: number
460 ): number | undefined {
461 let limit: number, matchingChargingProfile: ChargingProfile;
15068be9 462 // Get charging profiles for connector and sort by stack level
66a62eac 463 const chargingProfiles =
8df5ae48
JB
464 Utils.cloneObject<ChargingProfile[]>(
465 chargingStation.getConnectorStatus(connectorId)?.chargingProfiles
466 )?.sort((a, b) => b.stackLevel - a.stackLevel) ?? [];
15068be9 467 // Get profiles on connector 0
1895299d 468 if (chargingStation.getConnectorStatus(0)?.chargingProfiles) {
15068be9 469 chargingProfiles.push(
8df5ae48
JB
470 ...Utils.cloneObject<ChargingProfile[]>(
471 chargingStation.getConnectorStatus(0).chargingProfiles
472 ).sort((a, b) => b.stackLevel - a.stackLevel)
15068be9
JB
473 );
474 }
53ac516c 475 if (Utils.isNotEmptyArray(chargingProfiles)) {
15068be9
JB
476 const result = ChargingStationUtils.getLimitFromChargingProfiles(
477 chargingProfiles,
478 chargingStation.logPrefix()
479 );
480 if (!Utils.isNullOrUndefined(result)) {
1895299d
JB
481 limit = result?.limit;
482 matchingChargingProfile = result?.matchingChargingProfile;
15068be9
JB
483 switch (chargingStation.getCurrentOutType()) {
484 case CurrentType.AC:
485 limit =
486 matchingChargingProfile.chargingSchedule.chargingRateUnit ===
487 ChargingRateUnitType.WATT
488 ? limit
489 : ACElectricUtils.powerTotal(
490 chargingStation.getNumberOfPhases(),
491 chargingStation.getVoltageOut(),
492 limit
493 );
494 break;
495 case CurrentType.DC:
496 limit =
497 matchingChargingProfile.chargingSchedule.chargingRateUnit ===
498 ChargingRateUnitType.WATT
499 ? limit
500 : DCElectricUtils.power(chargingStation.getVoltageOut(), limit);
501 }
502 const connectorMaximumPower =
503 chargingStation.getMaximumPower() / chargingStation.powerDivider;
504 if (limit > connectorMaximumPower) {
505 logger.error(
506 `${chargingStation.logPrefix()} Charging profile id ${
507 matchingChargingProfile.chargingProfileId
508 } limit ${limit} is greater than connector id ${connectorId} maximum ${connectorMaximumPower}: %j`,
509 result
510 );
511 limit = connectorMaximumPower;
512 }
513 }
514 }
515 return limit;
516 }
517
518 public static getDefaultVoltageOut(
519 currentType: CurrentType,
8a133cc8
JB
520 logPrefix: string,
521 templateFile: string
15068be9 522 ): Voltage {
ded57f02 523 const errorMsg = `Unknown ${currentType} currentOutType in template file ${templateFile}, cannot define default voltage out`;
15068be9
JB
524 let defaultVoltageOut: number;
525 switch (currentType) {
526 case CurrentType.AC:
527 defaultVoltageOut = Voltage.VOLTAGE_230;
528 break;
529 case CurrentType.DC:
530 defaultVoltageOut = Voltage.VOLTAGE_400;
531 break;
532 default:
ded57f02
JB
533 logger.error(`${logPrefix} ${errorMsg}`);
534 throw new BaseError(errorMsg);
15068be9
JB
535 }
536 return defaultVoltageOut;
537 }
538
e302df1d 539 public static getIdTagsFile(stationInfo: ChargingStationInfo): string | undefined {
15068be9 540 return (
e302df1d 541 stationInfo.idTagsFile &&
15068be9 542 path.join(
51022aa0 543 path.dirname(fileURLToPath(import.meta.url)),
15068be9 544 'assets',
e302df1d 545 path.basename(stationInfo.idTagsFile)
15068be9
JB
546 )
547 );
548 }
549
8a133cc8 550 private static getConfiguredNumberOfConnectors(stationTemplate: ChargingStationTemplate): number {
cda5d0fb
JB
551 let configuredMaxConnectors: number;
552 if (Utils.isNotEmptyArray(stationTemplate.numberOfConnectors) === true) {
553 const numberOfConnectors = stationTemplate.numberOfConnectors as number[];
554 configuredMaxConnectors =
555 numberOfConnectors[Math.floor(Utils.secureRandom() * numberOfConnectors.length)];
556 } else if (Utils.isUndefined(stationTemplate.numberOfConnectors) === false) {
557 configuredMaxConnectors = stationTemplate.numberOfConnectors as number;
558 } else if (stationTemplate.Connectors && !stationTemplate.Evses) {
559 configuredMaxConnectors = stationTemplate?.Connectors[0]
560 ? ChargingStationUtils.getMaxNumberOfConnectors(stationTemplate.Connectors) - 1
561 : ChargingStationUtils.getMaxNumberOfConnectors(stationTemplate.Connectors);
562 } else if (stationTemplate.Evses && !stationTemplate.Connectors) {
563 configuredMaxConnectors = 0;
564 for (const evse in stationTemplate.Evses) {
565 if (evse === '0') {
566 continue;
567 }
568 configuredMaxConnectors += ChargingStationUtils.getMaxNumberOfConnectors(
569 stationTemplate.Evses[evse].Connectors
570 );
571 }
572 }
573 return configuredMaxConnectors;
574 }
575
576 private static checkConfiguredMaxConnectors(
577 configuredMaxConnectors: number,
8a133cc8
JB
578 logPrefix: string,
579 templateFile: string
cda5d0fb
JB
580 ): void {
581 if (configuredMaxConnectors <= 0) {
582 logger.warn(
583 `${logPrefix} Charging station information from template ${templateFile} with ${configuredMaxConnectors} connectors`
584 );
585 }
586 }
587
588 private static checkTemplateMaxConnectors(
589 templateMaxConnectors: number,
8a133cc8
JB
590 logPrefix: string,
591 templateFile: string
cda5d0fb
JB
592 ): void {
593 if (templateMaxConnectors === 0) {
594 logger.warn(
595 `${logPrefix} Charging station information from template ${templateFile} with empty connectors configuration`
596 );
597 } else if (templateMaxConnectors < 0) {
598 logger.error(
599 `${logPrefix} Charging station information from template ${templateFile} with no connectors configuration defined`
600 );
601 }
602 }
603
52952bf8
JB
604 private static initializeConnectorStatus(connectorStatus: ConnectorStatus): void {
605 connectorStatus.availability = AvailabilityType.Operative;
606 connectorStatus.idTagLocalAuthorized = false;
607 connectorStatus.idTagAuthorized = false;
608 connectorStatus.transactionRemoteStarted = false;
609 connectorStatus.transactionStarted = false;
610 connectorStatus.energyActiveImportRegisterValue = 0;
611 connectorStatus.transactionEnergyActiveImportRegisterValue = 0;
612 if (Utils.isUndefined(connectorStatus.chargingProfiles)) {
613 connectorStatus.chargingProfiles = [];
614 }
615 }
616
ae5020a3
JB
617 private static warnDeprecatedTemplateKey(
618 template: ChargingStationTemplate,
619 key: string,
ae5020a3 620 logPrefix: string,
8a133cc8 621 templateFile: string,
ae5020a3
JB
622 logMsgToAppend = ''
623 ): void {
624 if (!Utils.isUndefined(template[key])) {
625 const logMsg = `Deprecated template key '${key}' usage in file '${templateFile}'${
626 Utils.isNotEmptyString(logMsgToAppend) ? `. ${logMsgToAppend}` : ''
627 }`;
628 logger.warn(`${logPrefix} ${logMsg}`);
629 console.warn(chalk.yellow(`${logMsg}`));
630 }
631 }
632
633 private static convertDeprecatedTemplateKey(
634 template: ChargingStationTemplate,
635 deprecatedKey: string,
636 key: string
637 ): void {
638 if (!Utils.isUndefined(template[deprecatedKey])) {
639 template[key] = template[deprecatedKey] as unknown;
640 delete template[deprecatedKey];
641 }
642 }
643
17ac262c 644 /**
54ebb82c 645 * Charging profiles should already be sorted by connector id and stack level (highest stack level has priority)
17ac262c 646 *
0e4fa348
JB
647 * @param chargingProfiles -
648 * @param logPrefix -
649 * @returns
17ac262c 650 */
15068be9 651 private static getLimitFromChargingProfiles(
17ac262c
JB
652 chargingProfiles: ChargingProfile[],
653 logPrefix: string
654 ): {
655 limit: number;
656 matchingChargingProfile: ChargingProfile;
657 } | null {
91a4f151 658 const debugLogMsg = `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Matching charging profile found for power limitation: %j`;
41189456
JB
659 const currentMoment = moment();
660 const currentDate = new Date();
17ac262c
JB
661 for (const chargingProfile of chargingProfiles) {
662 // Set helpers
17ac262c 663 const chargingSchedule = chargingProfile.chargingSchedule;
41189456
JB
664 if (!chargingSchedule?.startSchedule) {
665 logger.warn(
666 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: startSchedule is not defined in charging profile id ${chargingProfile.chargingProfileId}`
667 );
668 }
17ac262c
JB
669 // Check type (recurring) and if it is already active
670 // Adjust the daily recurring schedule to today
671 if (
672 chargingProfile.chargingProfileKind === ChargingProfileKindType.RECURRING &&
673 chargingProfile.recurrencyKind === RecurrencyKindType.DAILY &&
674 currentMoment.isAfter(chargingSchedule.startSchedule)
675 ) {
41189456
JB
676 if (!(chargingSchedule?.startSchedule instanceof Date)) {
677 logger.warn(
678 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: startSchedule is not a Date object in charging profile id ${chargingProfile.chargingProfileId}. Trying to convert it to a Date object`
679 );
680 chargingSchedule.startSchedule = new Date(chargingSchedule.startSchedule);
681 }
17ac262c
JB
682 chargingSchedule.startSchedule.setFullYear(
683 currentDate.getFullYear(),
684 currentDate.getMonth(),
685 currentDate.getDate()
686 );
687 // Check if the start of the schedule is yesterday
688 if (moment(chargingSchedule.startSchedule).isAfter(currentMoment)) {
689 chargingSchedule.startSchedule.setDate(currentDate.getDate() - 1);
690 }
691 } else if (moment(chargingSchedule.startSchedule).isAfter(currentMoment)) {
692 return null;
693 }
694 // Check if the charging profile is active
695 if (
696 moment(chargingSchedule.startSchedule)
697 .add(chargingSchedule.duration, 's')
698 .isAfter(currentMoment)
699 ) {
700 let lastButOneSchedule: ChargingSchedulePeriod;
701 // Search the right schedule period
702 for (const schedulePeriod of chargingSchedule.chargingSchedulePeriod) {
703 // Handling of only one period
704 if (
705 chargingSchedule.chargingSchedulePeriod.length === 1 &&
706 schedulePeriod.startPeriod === 0
707 ) {
708 const result = {
709 limit: schedulePeriod.limit,
710 matchingChargingProfile: chargingProfile,
711 };
91a4f151 712 logger.debug(debugLogMsg, result);
17ac262c
JB
713 return result;
714 }
715 // Find the right schedule period
716 if (
717 moment(chargingSchedule.startSchedule)
718 .add(schedulePeriod.startPeriod, 's')
719 .isAfter(currentMoment)
720 ) {
721 // Found the schedule: last but one is the correct one
722 const result = {
723 limit: lastButOneSchedule.limit,
724 matchingChargingProfile: chargingProfile,
725 };
91a4f151 726 logger.debug(debugLogMsg, result);
17ac262c
JB
727 return result;
728 }
729 // Keep it
730 lastButOneSchedule = schedulePeriod;
731 // Handle the last schedule period
732 if (
733 schedulePeriod.startPeriod ===
734 chargingSchedule.chargingSchedulePeriod[
735 chargingSchedule.chargingSchedulePeriod.length - 1
736 ].startPeriod
737 ) {
738 const result = {
739 limit: lastButOneSchedule.limit,
740 matchingChargingProfile: chargingProfile,
741 };
91a4f151 742 logger.debug(debugLogMsg, result);
17ac262c
JB
743 return result;
744 }
745 }
746 }
747 }
748 return null;
749 }
750
751 private static getRandomSerialNumberSuffix(params?: {
752 randomBytesLength?: number;
753 upperCase?: boolean;
754 }): string {
755 const randomSerialNumberSuffix = crypto
756 .randomBytes(params?.randomBytesLength ?? 16)
757 .toString('hex');
758 if (params?.upperCase) {
759 return randomSerialNumberSuffix.toUpperCase();
760 }
761 return randomSerialNumberSuffix;
762 }
763}