refactor(simulator): factor out data transfer response
[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 163 if (Utils.isEmptyObject(stationTemplate.AutomaticTransactionGenerator)) {
1fe0632a 164 stationTemplate.AutomaticTransactionGenerator = Constants.DEFAULT_ATG_CONFIGURATION;
ac7f79af 165 logger.warn(
1fe0632a
JB
166 `${logPrefix} Empty automatic transaction generator configuration from template file ${templateFile}, set to default: %j`,
167 Constants.DEFAULT_ATG_CONFIGURATION
ac7f79af
JB
168 );
169 }
111aaf89
JB
170 if (
171 Utils.isNullOrUndefined(stationTemplate.idTagsFile) ||
172 Utils.isEmptyString(stationTemplate.idTagsFile)
173 ) {
174 logger.warn(
175 `${logPrefix} Missing id tags file in template file ${templateFile}. That can lead to issues with the Automatic Transaction Generator`
176 );
177 }
8a133cc8
JB
178 }
179
cda5d0fb 180 public static checkConnectorsConfiguration(
8a133cc8
JB
181 stationTemplate: ChargingStationTemplate,
182 logPrefix: string,
183 templateFile: string
cda5d0fb
JB
184 ): {
185 configuredMaxConnectors: number;
186 templateMaxConnectors: number;
187 templateMaxAvailableConnectors: number;
188 } {
189 const configuredMaxConnectors =
190 ChargingStationUtils.getConfiguredNumberOfConnectors(stationTemplate);
191 ChargingStationUtils.checkConfiguredMaxConnectors(
192 configuredMaxConnectors,
8a133cc8
JB
193 logPrefix,
194 templateFile
cda5d0fb
JB
195 );
196 const templateMaxConnectors = ChargingStationUtils.getMaxNumberOfConnectors(
197 stationTemplate.Connectors
198 );
8a133cc8 199 ChargingStationUtils.checkTemplateMaxConnectors(templateMaxConnectors, logPrefix, templateFile);
cda5d0fb
JB
200 const templateMaxAvailableConnectors = stationTemplate?.Connectors[0]
201 ? templateMaxConnectors - 1
202 : templateMaxConnectors;
203 if (
204 configuredMaxConnectors > templateMaxAvailableConnectors &&
205 !stationTemplate?.randomConnectors
206 ) {
fa7bccf4 207 logger.warn(
cda5d0fb 208 `${logPrefix} Number of connectors exceeds the number of connector configurations in template ${templateFile}, forcing random connector configurations affectation`
fa7bccf4 209 );
cda5d0fb 210 stationTemplate.randomConnectors = true;
fa7bccf4 211 }
cda5d0fb 212 return { configuredMaxConnectors, templateMaxConnectors, templateMaxAvailableConnectors };
fa7bccf4
JB
213 }
214
04b1261c
JB
215 public static checkStationInfoConnectorStatus(
216 connectorId: number,
217 connectorStatus: ConnectorStatus,
218 logPrefix: string,
219 templateFile: string
220 ): void {
221 if (!Utils.isNullOrUndefined(connectorStatus?.status)) {
222 logger.warn(
54ebb82c 223 `${logPrefix} Charging station information from template ${templateFile} with connector id ${connectorId} status configuration defined, undefine it`
04b1261c
JB
224 );
225 delete connectorStatus.status;
226 }
227 }
228
229 public static buildConnectorsMap(
230 connectors: Record<string, ConnectorStatus>,
231 logPrefix: string,
232 templateFile: string
233 ): Map<number, ConnectorStatus> {
234 const connectorsMap = new Map<number, ConnectorStatus>();
ae25f265
JB
235 if (ChargingStationUtils.getMaxNumberOfConnectors(connectors) > 0) {
236 for (const connector in connectors) {
237 const connectorStatus = connectors[connector];
238 const connectorId = Utils.convertToInt(connector);
239 ChargingStationUtils.checkStationInfoConnectorStatus(
240 connectorId,
241 connectorStatus,
242 logPrefix,
243 templateFile
244 );
245 connectorsMap.set(connectorId, Utils.cloneObject<ConnectorStatus>(connectorStatus));
04b1261c 246 }
ae25f265
JB
247 } else {
248 logger.warn(
249 `${logPrefix} Charging station information from template ${templateFile} with no connectors, cannot build connectors map`
250 );
04b1261c
JB
251 }
252 return connectorsMap;
253 }
254
255 public static initializeConnectorsMapStatus(
256 connectors: Map<number, ConnectorStatus>,
257 logPrefix: string
258 ): void {
259 for (const connectorId of connectors.keys()) {
260 if (connectorId > 0 && connectors.get(connectorId)?.transactionStarted === true) {
261 logger.warn(
54ebb82c 262 `${logPrefix} Connector id ${connectorId} at initialization has a transaction started with id ${
04b1261c
JB
263 connectors.get(connectorId)?.transactionId
264 }`
265 );
266 }
8e2430ee 267 if (connectorId === 0) {
52952bf8
JB
268 connectors.get(connectorId).availability = AvailabilityType.Operative;
269 if (Utils.isUndefined(connectors.get(connectorId)?.chargingProfiles)) {
270 connectors.get(connectorId).chargingProfiles = [];
271 }
272 } else if (
04b1261c
JB
273 connectorId > 0 &&
274 Utils.isNullOrUndefined(connectors.get(connectorId)?.transactionStarted)
275 ) {
276 ChargingStationUtils.initializeConnectorStatus(connectors.get(connectorId));
277 }
278 }
279 }
280
04b1261c
JB
281 public static resetConnectorStatus(connectorStatus: ConnectorStatus): void {
282 connectorStatus.idTagLocalAuthorized = false;
283 connectorStatus.idTagAuthorized = false;
284 connectorStatus.transactionRemoteStarted = false;
285 connectorStatus.transactionStarted = false;
286 delete connectorStatus?.localAuthorizeIdTag;
287 delete connectorStatus?.authorizeIdTag;
288 delete connectorStatus?.transactionId;
289 delete connectorStatus?.transactionIdTag;
290 connectorStatus.transactionEnergyActiveImportRegisterValue = 0;
291 delete connectorStatus?.transactionBeginMeterValue;
292 }
293
17ac262c 294 public static createBootNotificationRequest(
15068be9
JB
295 stationInfo: ChargingStationInfo,
296 bootReason: BootReasonEnumType = BootReasonEnumType.PowerUp
17ac262c 297 ): BootNotificationRequest {
d270cc87
JB
298 const ocppVersion = stationInfo.ocppVersion ?? OCPPVersion.VERSION_16;
299 switch (ocppVersion) {
300 case OCPPVersion.VERSION_16:
301 return {
302 chargePointModel: stationInfo.chargePointModel,
303 chargePointVendor: stationInfo.chargePointVendor,
304 ...(!Utils.isUndefined(stationInfo.chargeBoxSerialNumber) && {
305 chargeBoxSerialNumber: stationInfo.chargeBoxSerialNumber,
306 }),
307 ...(!Utils.isUndefined(stationInfo.chargePointSerialNumber) && {
308 chargePointSerialNumber: stationInfo.chargePointSerialNumber,
309 }),
310 ...(!Utils.isUndefined(stationInfo.firmwareVersion) && {
311 firmwareVersion: stationInfo.firmwareVersion,
312 }),
313 ...(!Utils.isUndefined(stationInfo.iccid) && { iccid: stationInfo.iccid }),
314 ...(!Utils.isUndefined(stationInfo.imsi) && { imsi: stationInfo.imsi }),
315 ...(!Utils.isUndefined(stationInfo.meterSerialNumber) && {
316 meterSerialNumber: stationInfo.meterSerialNumber,
317 }),
318 ...(!Utils.isUndefined(stationInfo.meterType) && {
319 meterType: stationInfo.meterType,
320 }),
321 } as OCPP16BootNotificationRequest;
322 case OCPPVersion.VERSION_20:
323 case OCPPVersion.VERSION_201:
324 return {
15068be9 325 reason: bootReason,
d270cc87
JB
326 chargingStation: {
327 model: stationInfo.chargePointModel,
328 vendorName: stationInfo.chargePointVendor,
329 ...(!Utils.isUndefined(stationInfo.firmwareVersion) && {
330 firmwareVersion: stationInfo.firmwareVersion,
331 }),
332 ...(!Utils.isUndefined(stationInfo.chargeBoxSerialNumber) && {
333 serialNumber: stationInfo.chargeBoxSerialNumber,
334 }),
98fc1389
JB
335 ...((!Utils.isUndefined(stationInfo.iccid) || !Utils.isUndefined(stationInfo.imsi)) && {
336 modem: {
337 ...(!Utils.isUndefined(stationInfo.iccid) && { iccid: stationInfo.iccid }),
338 ...(!Utils.isUndefined(stationInfo.imsi) && { imsi: stationInfo.imsi }),
339 },
340 }),
d270cc87
JB
341 },
342 } as OCPP20BootNotificationRequest;
343 }
17ac262c
JB
344 }
345
ae5020a3 346 public static warnTemplateKeysDeprecation(
ae5020a3 347 stationTemplate: ChargingStationTemplate,
8a133cc8
JB
348 logPrefix: string,
349 templateFile: string
ae5020a3
JB
350 ) {
351 const templateKeys: { key: string; deprecatedKey: string }[] = [
352 { key: 'supervisionUrls', deprecatedKey: 'supervisionUrl' },
353 { key: 'idTagsFile', deprecatedKey: 'authorizationFile' },
354 ];
355 for (const templateKey of templateKeys) {
356 ChargingStationUtils.warnDeprecatedTemplateKey(
357 stationTemplate,
358 templateKey.deprecatedKey,
ae5020a3 359 logPrefix,
8a133cc8 360 templateFile,
ae5020a3
JB
361 `Use '${templateKey.key}' instead`
362 );
363 ChargingStationUtils.convertDeprecatedTemplateKey(
364 stationTemplate,
365 templateKey.deprecatedKey,
366 templateKey.key
367 );
17ac262c
JB
368 }
369 }
370
fa7bccf4
JB
371 public static stationTemplateToStationInfo(
372 stationTemplate: ChargingStationTemplate
373 ): ChargingStationInfo {
8df5ae48 374 stationTemplate = Utils.cloneObject<ChargingStationTemplate>(stationTemplate);
fa7bccf4
JB
375 delete stationTemplate.power;
376 delete stationTemplate.powerUnit;
8a133cc8
JB
377 delete stationTemplate?.Connectors;
378 delete stationTemplate?.Evses;
fa7bccf4
JB
379 delete stationTemplate.Configuration;
380 delete stationTemplate.AutomaticTransactionGenerator;
381 delete stationTemplate.chargeBoxSerialNumberPrefix;
382 delete stationTemplate.chargePointSerialNumberPrefix;
fec4d204 383 delete stationTemplate.meterSerialNumberPrefix;
51c83d6f 384 return stationTemplate as unknown as ChargingStationInfo;
fa7bccf4
JB
385 }
386
17ac262c 387 public static createSerialNumber(
fa7bccf4 388 stationTemplate: ChargingStationTemplate,
4d20f040 389 stationInfo: ChargingStationInfo,
fa7bccf4
JB
390 params: {
391 randomSerialNumberUpperCase?: boolean;
392 randomSerialNumber?: boolean;
393 } = {
17ac262c
JB
394 randomSerialNumberUpperCase: true,
395 randomSerialNumber: true,
396 }
397 ): void {
f832e5df 398 params = { ...{ randomSerialNumberUpperCase: true, randomSerialNumber: true }, ...params };
fa7bccf4
JB
399 const serialNumberSuffix = params?.randomSerialNumber
400 ? ChargingStationUtils.getRandomSerialNumberSuffix({
401 upperCase: params.randomSerialNumberUpperCase,
402 })
403 : '';
f832e5df
JB
404 Utils.isNotEmptyString(stationTemplate?.chargePointSerialNumberPrefix) &&
405 (stationInfo.chargePointSerialNumber = `${stationTemplate.chargePointSerialNumberPrefix}${serialNumberSuffix}`);
406 Utils.isNotEmptyString(stationTemplate?.chargeBoxSerialNumberPrefix) &&
407 (stationInfo.chargeBoxSerialNumber = `${stationTemplate.chargeBoxSerialNumberPrefix}${serialNumberSuffix}`);
408 Utils.isNotEmptyString(stationTemplate?.meterSerialNumberPrefix) &&
409 (stationInfo.meterSerialNumber = `${stationTemplate.meterSerialNumberPrefix}${serialNumberSuffix}`);
fec4d204
JB
410 }
411
412 public static propagateSerialNumber(
413 stationTemplate: ChargingStationTemplate,
414 stationInfoSrc: ChargingStationInfo,
4d20f040 415 stationInfoDst: ChargingStationInfo
fec4d204
JB
416 ) {
417 if (!stationInfoSrc || !stationTemplate) {
baf93dda
JB
418 throw new BaseError(
419 'Missing charging station template or existing configuration to propagate serial number'
420 );
fec4d204
JB
421 }
422 stationTemplate?.chargePointSerialNumberPrefix && stationInfoSrc?.chargePointSerialNumber
423 ? (stationInfoDst.chargePointSerialNumber = stationInfoSrc.chargePointSerialNumber)
424 : stationInfoDst?.chargePointSerialNumber && delete stationInfoDst.chargePointSerialNumber;
425 stationTemplate?.chargeBoxSerialNumberPrefix && stationInfoSrc?.chargeBoxSerialNumber
426 ? (stationInfoDst.chargeBoxSerialNumber = stationInfoSrc.chargeBoxSerialNumber)
427 : stationInfoDst?.chargeBoxSerialNumber && delete stationInfoDst.chargeBoxSerialNumber;
428 stationTemplate?.meterSerialNumberPrefix && stationInfoSrc?.meterSerialNumber
429 ? (stationInfoDst.meterSerialNumber = stationInfoSrc.meterSerialNumber)
430 : stationInfoDst?.meterSerialNumber && delete stationInfoDst.meterSerialNumber;
17ac262c
JB
431 }
432
433 public static getAmperageLimitationUnitDivider(stationInfo: ChargingStationInfo): number {
434 let unitDivider = 1;
435 switch (stationInfo.amperageLimitationUnit) {
436 case AmpereUnits.DECI_AMPERE:
437 unitDivider = 10;
438 break;
439 case AmpereUnits.CENTI_AMPERE:
440 unitDivider = 100;
441 break;
442 case AmpereUnits.MILLI_AMPERE:
443 unitDivider = 1000;
444 break;
445 }
446 return unitDivider;
447 }
448
15068be9
JB
449 public static getChargingStationConnectorChargingProfilesPowerLimit(
450 chargingStation: ChargingStation,
451 connectorId: number
452 ): number | undefined {
453 let limit: number, matchingChargingProfile: ChargingProfile;
15068be9 454 // Get charging profiles for connector and sort by stack level
66a62eac 455 const chargingProfiles =
8df5ae48
JB
456 Utils.cloneObject<ChargingProfile[]>(
457 chargingStation.getConnectorStatus(connectorId)?.chargingProfiles
458 )?.sort((a, b) => b.stackLevel - a.stackLevel) ?? [];
15068be9 459 // Get profiles on connector 0
1895299d 460 if (chargingStation.getConnectorStatus(0)?.chargingProfiles) {
15068be9 461 chargingProfiles.push(
8df5ae48
JB
462 ...Utils.cloneObject<ChargingProfile[]>(
463 chargingStation.getConnectorStatus(0).chargingProfiles
464 ).sort((a, b) => b.stackLevel - a.stackLevel)
15068be9
JB
465 );
466 }
53ac516c 467 if (Utils.isNotEmptyArray(chargingProfiles)) {
15068be9
JB
468 const result = ChargingStationUtils.getLimitFromChargingProfiles(
469 chargingProfiles,
470 chargingStation.logPrefix()
471 );
472 if (!Utils.isNullOrUndefined(result)) {
1895299d
JB
473 limit = result?.limit;
474 matchingChargingProfile = result?.matchingChargingProfile;
15068be9
JB
475 switch (chargingStation.getCurrentOutType()) {
476 case CurrentType.AC:
477 limit =
478 matchingChargingProfile.chargingSchedule.chargingRateUnit ===
479 ChargingRateUnitType.WATT
480 ? limit
481 : ACElectricUtils.powerTotal(
482 chargingStation.getNumberOfPhases(),
483 chargingStation.getVoltageOut(),
484 limit
485 );
486 break;
487 case CurrentType.DC:
488 limit =
489 matchingChargingProfile.chargingSchedule.chargingRateUnit ===
490 ChargingRateUnitType.WATT
491 ? limit
492 : DCElectricUtils.power(chargingStation.getVoltageOut(), limit);
493 }
494 const connectorMaximumPower =
495 chargingStation.getMaximumPower() / chargingStation.powerDivider;
496 if (limit > connectorMaximumPower) {
497 logger.error(
498 `${chargingStation.logPrefix()} Charging profile id ${
499 matchingChargingProfile.chargingProfileId
500 } limit ${limit} is greater than connector id ${connectorId} maximum ${connectorMaximumPower}: %j`,
501 result
502 );
503 limit = connectorMaximumPower;
504 }
505 }
506 }
507 return limit;
508 }
509
510 public static getDefaultVoltageOut(
511 currentType: CurrentType,
8a133cc8
JB
512 logPrefix: string,
513 templateFile: string
15068be9 514 ): Voltage {
ded57f02 515 const errorMsg = `Unknown ${currentType} currentOutType in template file ${templateFile}, cannot define default voltage out`;
15068be9
JB
516 let defaultVoltageOut: number;
517 switch (currentType) {
518 case CurrentType.AC:
519 defaultVoltageOut = Voltage.VOLTAGE_230;
520 break;
521 case CurrentType.DC:
522 defaultVoltageOut = Voltage.VOLTAGE_400;
523 break;
524 default:
ded57f02
JB
525 logger.error(`${logPrefix} ${errorMsg}`);
526 throw new BaseError(errorMsg);
15068be9
JB
527 }
528 return defaultVoltageOut;
529 }
530
e302df1d 531 public static getIdTagsFile(stationInfo: ChargingStationInfo): string | undefined {
15068be9 532 return (
e302df1d 533 stationInfo.idTagsFile &&
15068be9 534 path.join(
51022aa0 535 path.dirname(fileURLToPath(import.meta.url)),
15068be9 536 'assets',
e302df1d 537 path.basename(stationInfo.idTagsFile)
15068be9
JB
538 )
539 );
540 }
541
8a133cc8 542 private static getConfiguredNumberOfConnectors(stationTemplate: ChargingStationTemplate): number {
cda5d0fb
JB
543 let configuredMaxConnectors: number;
544 if (Utils.isNotEmptyArray(stationTemplate.numberOfConnectors) === true) {
545 const numberOfConnectors = stationTemplate.numberOfConnectors as number[];
546 configuredMaxConnectors =
547 numberOfConnectors[Math.floor(Utils.secureRandom() * numberOfConnectors.length)];
548 } else if (Utils.isUndefined(stationTemplate.numberOfConnectors) === false) {
549 configuredMaxConnectors = stationTemplate.numberOfConnectors as number;
550 } else if (stationTemplate.Connectors && !stationTemplate.Evses) {
551 configuredMaxConnectors = stationTemplate?.Connectors[0]
552 ? ChargingStationUtils.getMaxNumberOfConnectors(stationTemplate.Connectors) - 1
553 : ChargingStationUtils.getMaxNumberOfConnectors(stationTemplate.Connectors);
554 } else if (stationTemplate.Evses && !stationTemplate.Connectors) {
555 configuredMaxConnectors = 0;
556 for (const evse in stationTemplate.Evses) {
557 if (evse === '0') {
558 continue;
559 }
560 configuredMaxConnectors += ChargingStationUtils.getMaxNumberOfConnectors(
561 stationTemplate.Evses[evse].Connectors
562 );
563 }
564 }
565 return configuredMaxConnectors;
566 }
567
568 private static checkConfiguredMaxConnectors(
569 configuredMaxConnectors: number,
8a133cc8
JB
570 logPrefix: string,
571 templateFile: string
cda5d0fb
JB
572 ): void {
573 if (configuredMaxConnectors <= 0) {
574 logger.warn(
575 `${logPrefix} Charging station information from template ${templateFile} with ${configuredMaxConnectors} connectors`
576 );
577 }
578 }
579
580 private static checkTemplateMaxConnectors(
581 templateMaxConnectors: number,
8a133cc8
JB
582 logPrefix: string,
583 templateFile: string
cda5d0fb
JB
584 ): void {
585 if (templateMaxConnectors === 0) {
586 logger.warn(
587 `${logPrefix} Charging station information from template ${templateFile} with empty connectors configuration`
588 );
589 } else if (templateMaxConnectors < 0) {
590 logger.error(
591 `${logPrefix} Charging station information from template ${templateFile} with no connectors configuration defined`
592 );
593 }
594 }
595
52952bf8
JB
596 private static initializeConnectorStatus(connectorStatus: ConnectorStatus): void {
597 connectorStatus.availability = AvailabilityType.Operative;
598 connectorStatus.idTagLocalAuthorized = false;
599 connectorStatus.idTagAuthorized = false;
600 connectorStatus.transactionRemoteStarted = false;
601 connectorStatus.transactionStarted = false;
602 connectorStatus.energyActiveImportRegisterValue = 0;
603 connectorStatus.transactionEnergyActiveImportRegisterValue = 0;
604 if (Utils.isUndefined(connectorStatus.chargingProfiles)) {
605 connectorStatus.chargingProfiles = [];
606 }
607 }
608
ae5020a3
JB
609 private static warnDeprecatedTemplateKey(
610 template: ChargingStationTemplate,
611 key: string,
ae5020a3 612 logPrefix: string,
8a133cc8 613 templateFile: string,
ae5020a3
JB
614 logMsgToAppend = ''
615 ): void {
616 if (!Utils.isUndefined(template[key])) {
617 const logMsg = `Deprecated template key '${key}' usage in file '${templateFile}'${
618 Utils.isNotEmptyString(logMsgToAppend) ? `. ${logMsgToAppend}` : ''
619 }`;
620 logger.warn(`${logPrefix} ${logMsg}`);
621 console.warn(chalk.yellow(`${logMsg}`));
622 }
623 }
624
625 private static convertDeprecatedTemplateKey(
626 template: ChargingStationTemplate,
627 deprecatedKey: string,
628 key: string
629 ): void {
630 if (!Utils.isUndefined(template[deprecatedKey])) {
631 template[key] = template[deprecatedKey] as unknown;
632 delete template[deprecatedKey];
633 }
634 }
635
17ac262c 636 /**
54ebb82c 637 * Charging profiles should already be sorted by connector id and stack level (highest stack level has priority)
17ac262c 638 *
0e4fa348
JB
639 * @param chargingProfiles -
640 * @param logPrefix -
641 * @returns
17ac262c 642 */
15068be9 643 private static getLimitFromChargingProfiles(
17ac262c
JB
644 chargingProfiles: ChargingProfile[],
645 logPrefix: string
646 ): {
647 limit: number;
648 matchingChargingProfile: ChargingProfile;
649 } | null {
91a4f151 650 const debugLogMsg = `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Matching charging profile found for power limitation: %j`;
41189456
JB
651 const currentMoment = moment();
652 const currentDate = new Date();
17ac262c
JB
653 for (const chargingProfile of chargingProfiles) {
654 // Set helpers
17ac262c 655 const chargingSchedule = chargingProfile.chargingSchedule;
41189456
JB
656 if (!chargingSchedule?.startSchedule) {
657 logger.warn(
658 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: startSchedule is not defined in charging profile id ${chargingProfile.chargingProfileId}`
659 );
660 }
17ac262c
JB
661 // Check type (recurring) and if it is already active
662 // Adjust the daily recurring schedule to today
663 if (
664 chargingProfile.chargingProfileKind === ChargingProfileKindType.RECURRING &&
665 chargingProfile.recurrencyKind === RecurrencyKindType.DAILY &&
666 currentMoment.isAfter(chargingSchedule.startSchedule)
667 ) {
41189456
JB
668 if (!(chargingSchedule?.startSchedule instanceof Date)) {
669 logger.warn(
670 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: startSchedule is not a Date object in charging profile id ${chargingProfile.chargingProfileId}. Trying to convert it to a Date object`
671 );
672 chargingSchedule.startSchedule = new Date(chargingSchedule.startSchedule);
673 }
17ac262c
JB
674 chargingSchedule.startSchedule.setFullYear(
675 currentDate.getFullYear(),
676 currentDate.getMonth(),
677 currentDate.getDate()
678 );
679 // Check if the start of the schedule is yesterday
680 if (moment(chargingSchedule.startSchedule).isAfter(currentMoment)) {
681 chargingSchedule.startSchedule.setDate(currentDate.getDate() - 1);
682 }
683 } else if (moment(chargingSchedule.startSchedule).isAfter(currentMoment)) {
684 return null;
685 }
686 // Check if the charging profile is active
687 if (
688 moment(chargingSchedule.startSchedule)
689 .add(chargingSchedule.duration, 's')
690 .isAfter(currentMoment)
691 ) {
692 let lastButOneSchedule: ChargingSchedulePeriod;
693 // Search the right schedule period
694 for (const schedulePeriod of chargingSchedule.chargingSchedulePeriod) {
695 // Handling of only one period
696 if (
697 chargingSchedule.chargingSchedulePeriod.length === 1 &&
698 schedulePeriod.startPeriod === 0
699 ) {
700 const result = {
701 limit: schedulePeriod.limit,
702 matchingChargingProfile: chargingProfile,
703 };
91a4f151 704 logger.debug(debugLogMsg, result);
17ac262c
JB
705 return result;
706 }
707 // Find the right schedule period
708 if (
709 moment(chargingSchedule.startSchedule)
710 .add(schedulePeriod.startPeriod, 's')
711 .isAfter(currentMoment)
712 ) {
713 // Found the schedule: last but one is the correct one
714 const result = {
715 limit: lastButOneSchedule.limit,
716 matchingChargingProfile: chargingProfile,
717 };
91a4f151 718 logger.debug(debugLogMsg, result);
17ac262c
JB
719 return result;
720 }
721 // Keep it
722 lastButOneSchedule = schedulePeriod;
723 // Handle the last schedule period
724 if (
725 schedulePeriod.startPeriod ===
726 chargingSchedule.chargingSchedulePeriod[
727 chargingSchedule.chargingSchedulePeriod.length - 1
728 ].startPeriod
729 ) {
730 const result = {
731 limit: lastButOneSchedule.limit,
732 matchingChargingProfile: chargingProfile,
733 };
91a4f151 734 logger.debug(debugLogMsg, result);
17ac262c
JB
735 return result;
736 }
737 }
738 }
739 }
740 return null;
741 }
742
743 private static getRandomSerialNumberSuffix(params?: {
744 randomBytesLength?: number;
745 upperCase?: boolean;
746 }): string {
747 const randomSerialNumberSuffix = crypto
748 .randomBytes(params?.randomBytesLength ?? 16)
749 .toString('hex');
750 if (params?.upperCase) {
751 return randomSerialNumberSuffix.toUpperCase();
752 }
753 return randomSerialNumberSuffix;
754 }
755}