build(deps-dev): apply updates
[e-mobility-charging-stations-simulator.git] / src / charging-station / ChargingStationUtils.ts
1 import crypto from 'node:crypto';
2 import path from 'node:path';
3 import { fileURLToPath } from 'node:url';
4
5 import chalk from 'chalk';
6 import moment from 'moment';
7
8 import type { ChargingStation } from './internal';
9 import { BaseError } from '../exception';
10 import {
11 AmpereUnits,
12 type BootNotificationRequest,
13 BootReasonEnumType,
14 type ChargingProfile,
15 ChargingProfileKindType,
16 ChargingRateUnitType,
17 type ChargingSchedulePeriod,
18 type ChargingStationInfo,
19 type ChargingStationTemplate,
20 CurrentType,
21 type OCPP16BootNotificationRequest,
22 type OCPP20BootNotificationRequest,
23 OCPPVersion,
24 RecurrencyKindType,
25 Voltage,
26 } from '../types';
27 import {
28 ACElectricUtils,
29 Configuration,
30 Constants,
31 DCElectricUtils,
32 Utils,
33 logger,
34 } from '../utils';
35 import { WorkerProcessType } from '../worker';
36
37 const moduleName = 'ChargingStationUtils';
38
39 export class ChargingStationUtils {
40 private constructor() {
41 // This is intentional
42 }
43
44 public static getChargingStationId(
45 index: number,
46 stationTemplate: ChargingStationTemplate
47 ): string {
48 // In case of multiple instances: add instance index to charging station id
49 const instanceIndex = process.env.CF_INSTANCE_INDEX ?? 0;
50 const idSuffix = stationTemplate?.nameSuffix ?? '';
51 const idStr = `000000000${index.toString()}`;
52 return stationTemplate?.fixedName
53 ? stationTemplate.baseName
54 : `${stationTemplate.baseName}-${instanceIndex.toString()}${idStr.substring(
55 idStr.length - 4
56 )}${idSuffix}`;
57 }
58
59 public static getHashId(index: number, stationTemplate: ChargingStationTemplate): string {
60 const chargingStationInfo = {
61 chargePointModel: stationTemplate.chargePointModel,
62 chargePointVendor: stationTemplate.chargePointVendor,
63 ...(!Utils.isUndefined(stationTemplate.chargeBoxSerialNumberPrefix) && {
64 chargeBoxSerialNumber: stationTemplate.chargeBoxSerialNumberPrefix,
65 }),
66 ...(!Utils.isUndefined(stationTemplate.chargePointSerialNumberPrefix) && {
67 chargePointSerialNumber: stationTemplate.chargePointSerialNumberPrefix,
68 }),
69 ...(!Utils.isUndefined(stationTemplate.meterSerialNumberPrefix) && {
70 meterSerialNumber: stationTemplate.meterSerialNumberPrefix,
71 }),
72 ...(!Utils.isUndefined(stationTemplate.meterType) && {
73 meterType: stationTemplate.meterType,
74 }),
75 };
76 return crypto
77 .createHash(Constants.DEFAULT_HASH_ALGORITHM)
78 .update(
79 `${JSON.stringify(chargingStationInfo)}${ChargingStationUtils.getChargingStationId(
80 index,
81 stationTemplate
82 )}`
83 )
84 .digest('hex');
85 }
86
87 public static checkChargingStation(chargingStation: ChargingStation, logPrefix: string): boolean {
88 if (chargingStation.started === false && chargingStation.starting === false) {
89 logger.warn(`${logPrefix} charging station is stopped, cannot proceed`);
90 return false;
91 }
92 return true;
93 }
94
95 public static getTemplateMaxNumberOfConnectors(stationTemplate: ChargingStationTemplate): number {
96 const templateConnectors = stationTemplate?.Connectors;
97 if (!templateConnectors) {
98 return -1;
99 }
100 return Object.keys(templateConnectors).length;
101 }
102
103 public static checkTemplateMaxConnectors(
104 templateMaxConnectors: number,
105 templateFile: string,
106 logPrefix: string
107 ): void {
108 if (templateMaxConnectors === 0) {
109 logger.warn(
110 `${logPrefix} Charging station information from template ${templateFile} with empty connectors configuration`
111 );
112 } else if (templateMaxConnectors < 0) {
113 logger.error(
114 `${logPrefix} Charging station information from template ${templateFile} with no connectors configuration defined`
115 );
116 }
117 }
118
119 public static getConfiguredNumberOfConnectors(stationTemplate: ChargingStationTemplate): number {
120 let configuredMaxConnectors: number;
121 if (Utils.isNotEmptyArray(stationTemplate.numberOfConnectors) === true) {
122 const numberOfConnectors = stationTemplate.numberOfConnectors as number[];
123 configuredMaxConnectors =
124 numberOfConnectors[Math.floor(Utils.secureRandom() * numberOfConnectors.length)];
125 } else if (Utils.isUndefined(stationTemplate.numberOfConnectors) === false) {
126 configuredMaxConnectors = stationTemplate.numberOfConnectors as number;
127 } else {
128 configuredMaxConnectors = stationTemplate?.Connectors[0]
129 ? ChargingStationUtils.getTemplateMaxNumberOfConnectors(stationTemplate) - 1
130 : ChargingStationUtils.getTemplateMaxNumberOfConnectors(stationTemplate);
131 }
132 return configuredMaxConnectors;
133 }
134
135 public static checkConfiguredMaxConnectors(
136 configuredMaxConnectors: number,
137 templateFile: string,
138 logPrefix: string
139 ): void {
140 if (configuredMaxConnectors <= 0) {
141 logger.warn(
142 `${logPrefix} Charging station information from template ${templateFile} with ${configuredMaxConnectors} connectors`
143 );
144 }
145 }
146
147 public static createBootNotificationRequest(
148 stationInfo: ChargingStationInfo,
149 bootReason: BootReasonEnumType = BootReasonEnumType.PowerUp
150 ): BootNotificationRequest {
151 const ocppVersion = stationInfo.ocppVersion ?? OCPPVersion.VERSION_16;
152 switch (ocppVersion) {
153 case OCPPVersion.VERSION_16:
154 return {
155 chargePointModel: stationInfo.chargePointModel,
156 chargePointVendor: stationInfo.chargePointVendor,
157 ...(!Utils.isUndefined(stationInfo.chargeBoxSerialNumber) && {
158 chargeBoxSerialNumber: stationInfo.chargeBoxSerialNumber,
159 }),
160 ...(!Utils.isUndefined(stationInfo.chargePointSerialNumber) && {
161 chargePointSerialNumber: stationInfo.chargePointSerialNumber,
162 }),
163 ...(!Utils.isUndefined(stationInfo.firmwareVersion) && {
164 firmwareVersion: stationInfo.firmwareVersion,
165 }),
166 ...(!Utils.isUndefined(stationInfo.iccid) && { iccid: stationInfo.iccid }),
167 ...(!Utils.isUndefined(stationInfo.imsi) && { imsi: stationInfo.imsi }),
168 ...(!Utils.isUndefined(stationInfo.meterSerialNumber) && {
169 meterSerialNumber: stationInfo.meterSerialNumber,
170 }),
171 ...(!Utils.isUndefined(stationInfo.meterType) && {
172 meterType: stationInfo.meterType,
173 }),
174 } as OCPP16BootNotificationRequest;
175 case OCPPVersion.VERSION_20:
176 case OCPPVersion.VERSION_201:
177 return {
178 reason: bootReason,
179 chargingStation: {
180 model: stationInfo.chargePointModel,
181 vendorName: stationInfo.chargePointVendor,
182 ...(!Utils.isUndefined(stationInfo.firmwareVersion) && {
183 firmwareVersion: stationInfo.firmwareVersion,
184 }),
185 ...(!Utils.isUndefined(stationInfo.chargeBoxSerialNumber) && {
186 serialNumber: stationInfo.chargeBoxSerialNumber,
187 }),
188 ...((!Utils.isUndefined(stationInfo.iccid) || !Utils.isUndefined(stationInfo.imsi)) && {
189 modem: {
190 ...(!Utils.isUndefined(stationInfo.iccid) && { iccid: stationInfo.iccid }),
191 ...(!Utils.isUndefined(stationInfo.imsi) && { imsi: stationInfo.imsi }),
192 },
193 }),
194 },
195 } as OCPP20BootNotificationRequest;
196 }
197 }
198
199 public static workerPoolInUse(): boolean {
200 return [WorkerProcessType.dynamicPool, WorkerProcessType.staticPool].includes(
201 Configuration.getWorker().processType
202 );
203 }
204
205 public static workerDynamicPoolInUse(): boolean {
206 return Configuration.getWorker().processType === WorkerProcessType.dynamicPool;
207 }
208
209 public static warnTemplateKeysDeprecation(
210 templateFile: string,
211 stationTemplate: ChargingStationTemplate,
212 logPrefix: string
213 ) {
214 const templateKeys: { key: string; deprecatedKey: string }[] = [
215 { key: 'supervisionUrls', deprecatedKey: 'supervisionUrl' },
216 { key: 'idTagsFile', deprecatedKey: 'authorizationFile' },
217 ];
218 for (const templateKey of templateKeys) {
219 ChargingStationUtils.warnDeprecatedTemplateKey(
220 stationTemplate,
221 templateKey.deprecatedKey,
222 templateFile,
223 logPrefix,
224 `Use '${templateKey.key}' instead`
225 );
226 ChargingStationUtils.convertDeprecatedTemplateKey(
227 stationTemplate,
228 templateKey.deprecatedKey,
229 templateKey.key
230 );
231 }
232 }
233
234 public static stationTemplateToStationInfo(
235 stationTemplate: ChargingStationTemplate
236 ): ChargingStationInfo {
237 stationTemplate = Utils.cloneObject(stationTemplate);
238 delete stationTemplate.power;
239 delete stationTemplate.powerUnit;
240 delete stationTemplate.Configuration;
241 delete stationTemplate.AutomaticTransactionGenerator;
242 delete stationTemplate.chargeBoxSerialNumberPrefix;
243 delete stationTemplate.chargePointSerialNumberPrefix;
244 delete stationTemplate.meterSerialNumberPrefix;
245 return stationTemplate as unknown as ChargingStationInfo;
246 }
247
248 public static createStationInfoHash(stationInfo: ChargingStationInfo): void {
249 delete stationInfo.infoHash;
250 stationInfo.infoHash = crypto
251 .createHash(Constants.DEFAULT_HASH_ALGORITHM)
252 .update(JSON.stringify(stationInfo))
253 .digest('hex');
254 }
255
256 public static createSerialNumber(
257 stationTemplate: ChargingStationTemplate,
258 stationInfo: ChargingStationInfo,
259 params: {
260 randomSerialNumberUpperCase?: boolean;
261 randomSerialNumber?: boolean;
262 } = {
263 randomSerialNumberUpperCase: true,
264 randomSerialNumber: true,
265 }
266 ): void {
267 params = params ?? {};
268 params.randomSerialNumberUpperCase = params?.randomSerialNumberUpperCase ?? true;
269 params.randomSerialNumber = params?.randomSerialNumber ?? true;
270 const serialNumberSuffix = params?.randomSerialNumber
271 ? ChargingStationUtils.getRandomSerialNumberSuffix({
272 upperCase: params.randomSerialNumberUpperCase,
273 })
274 : '';
275 stationInfo.chargePointSerialNumber = Utils.isNotEmptyString(
276 stationTemplate?.chargePointSerialNumberPrefix
277 )
278 ? `${stationTemplate.chargePointSerialNumberPrefix}${serialNumberSuffix}`
279 : undefined;
280 stationInfo.chargeBoxSerialNumber = Utils.isNotEmptyString(
281 stationTemplate?.chargeBoxSerialNumberPrefix
282 )
283 ? `${stationTemplate.chargeBoxSerialNumberPrefix}${serialNumberSuffix}`
284 : undefined;
285 stationInfo.meterSerialNumber = Utils.isNotEmptyString(stationTemplate?.meterSerialNumberPrefix)
286 ? `${stationTemplate.meterSerialNumberPrefix}${serialNumberSuffix}`
287 : undefined;
288 }
289
290 public static propagateSerialNumber(
291 stationTemplate: ChargingStationTemplate,
292 stationInfoSrc: ChargingStationInfo,
293 stationInfoDst: ChargingStationInfo
294 ) {
295 if (!stationInfoSrc || !stationTemplate) {
296 throw new BaseError(
297 'Missing charging station template or existing configuration to propagate serial number'
298 );
299 }
300 stationTemplate?.chargePointSerialNumberPrefix && stationInfoSrc?.chargePointSerialNumber
301 ? (stationInfoDst.chargePointSerialNumber = stationInfoSrc.chargePointSerialNumber)
302 : stationInfoDst?.chargePointSerialNumber && delete stationInfoDst.chargePointSerialNumber;
303 stationTemplate?.chargeBoxSerialNumberPrefix && stationInfoSrc?.chargeBoxSerialNumber
304 ? (stationInfoDst.chargeBoxSerialNumber = stationInfoSrc.chargeBoxSerialNumber)
305 : stationInfoDst?.chargeBoxSerialNumber && delete stationInfoDst.chargeBoxSerialNumber;
306 stationTemplate?.meterSerialNumberPrefix && stationInfoSrc?.meterSerialNumber
307 ? (stationInfoDst.meterSerialNumber = stationInfoSrc.meterSerialNumber)
308 : stationInfoDst?.meterSerialNumber && delete stationInfoDst.meterSerialNumber;
309 }
310
311 public static getAmperageLimitationUnitDivider(stationInfo: ChargingStationInfo): number {
312 let unitDivider = 1;
313 switch (stationInfo.amperageLimitationUnit) {
314 case AmpereUnits.DECI_AMPERE:
315 unitDivider = 10;
316 break;
317 case AmpereUnits.CENTI_AMPERE:
318 unitDivider = 100;
319 break;
320 case AmpereUnits.MILLI_AMPERE:
321 unitDivider = 1000;
322 break;
323 }
324 return unitDivider;
325 }
326
327 public static getChargingStationConnectorChargingProfilesPowerLimit(
328 chargingStation: ChargingStation,
329 connectorId: number
330 ): number | undefined {
331 let limit: number, matchingChargingProfile: ChargingProfile;
332 // Get charging profiles for connector and sort by stack level
333 const chargingProfiles =
334 Utils.cloneObject(chargingStation.getConnectorStatus(connectorId)?.chargingProfiles)?.sort(
335 (a, b) => b.stackLevel - a.stackLevel
336 ) ?? [];
337 // Get profiles on connector 0
338 if (chargingStation.getConnectorStatus(0)?.chargingProfiles) {
339 chargingProfiles.push(
340 ...Utils.cloneObject(chargingStation.getConnectorStatus(0).chargingProfiles).sort(
341 (a, b) => b.stackLevel - a.stackLevel
342 )
343 );
344 }
345 if (Utils.isNotEmptyArray(chargingProfiles)) {
346 const result = ChargingStationUtils.getLimitFromChargingProfiles(
347 chargingProfiles,
348 chargingStation.logPrefix()
349 );
350 if (!Utils.isNullOrUndefined(result)) {
351 limit = result?.limit;
352 matchingChargingProfile = result?.matchingChargingProfile;
353 switch (chargingStation.getCurrentOutType()) {
354 case CurrentType.AC:
355 limit =
356 matchingChargingProfile.chargingSchedule.chargingRateUnit ===
357 ChargingRateUnitType.WATT
358 ? limit
359 : ACElectricUtils.powerTotal(
360 chargingStation.getNumberOfPhases(),
361 chargingStation.getVoltageOut(),
362 limit
363 );
364 break;
365 case CurrentType.DC:
366 limit =
367 matchingChargingProfile.chargingSchedule.chargingRateUnit ===
368 ChargingRateUnitType.WATT
369 ? limit
370 : DCElectricUtils.power(chargingStation.getVoltageOut(), limit);
371 }
372 const connectorMaximumPower =
373 chargingStation.getMaximumPower() / chargingStation.powerDivider;
374 if (limit > connectorMaximumPower) {
375 logger.error(
376 `${chargingStation.logPrefix()} Charging profile id ${
377 matchingChargingProfile.chargingProfileId
378 } limit ${limit} is greater than connector id ${connectorId} maximum ${connectorMaximumPower}: %j`,
379 result
380 );
381 limit = connectorMaximumPower;
382 }
383 }
384 }
385 return limit;
386 }
387
388 public static getDefaultVoltageOut(
389 currentType: CurrentType,
390 templateFile: string,
391 logPrefix: string
392 ): Voltage {
393 const errMsg = `Unknown ${currentType} currentOutType in template file ${templateFile}, cannot define default voltage out`;
394 let defaultVoltageOut: number;
395 switch (currentType) {
396 case CurrentType.AC:
397 defaultVoltageOut = Voltage.VOLTAGE_230;
398 break;
399 case CurrentType.DC:
400 defaultVoltageOut = Voltage.VOLTAGE_400;
401 break;
402 default:
403 logger.error(`${logPrefix} ${errMsg}`);
404 throw new BaseError(errMsg);
405 }
406 return defaultVoltageOut;
407 }
408
409 public static getIdTagsFile(stationInfo: ChargingStationInfo): string | undefined {
410 return (
411 stationInfo.idTagsFile &&
412 path.join(
413 path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../'),
414 'assets',
415 path.basename(stationInfo.idTagsFile)
416 )
417 );
418 }
419
420 private static warnDeprecatedTemplateKey(
421 template: ChargingStationTemplate,
422 key: string,
423 templateFile: string,
424 logPrefix: string,
425 logMsgToAppend = ''
426 ): void {
427 if (!Utils.isUndefined(template[key])) {
428 const logMsg = `Deprecated template key '${key}' usage in file '${templateFile}'${
429 Utils.isNotEmptyString(logMsgToAppend) ? `. ${logMsgToAppend}` : ''
430 }`;
431 logger.warn(`${logPrefix} ${logMsg}`);
432 console.warn(chalk.yellow(`${logMsg}`));
433 }
434 }
435
436 private static convertDeprecatedTemplateKey(
437 template: ChargingStationTemplate,
438 deprecatedKey: string,
439 key: string
440 ): void {
441 if (!Utils.isUndefined(template[deprecatedKey])) {
442 template[key] = template[deprecatedKey] as unknown;
443 delete template[deprecatedKey];
444 }
445 }
446
447 /**
448 * Charging profiles should already be sorted by connectorId and stack level (highest stack level has priority)
449 *
450 * @param chargingProfiles -
451 * @param logPrefix -
452 * @returns
453 */
454 private static getLimitFromChargingProfiles(
455 chargingProfiles: ChargingProfile[],
456 logPrefix: string
457 ): {
458 limit: number;
459 matchingChargingProfile: ChargingProfile;
460 } | null {
461 const debugLogMsg = `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Matching charging profile found for power limitation: %j`;
462 const currentMoment = moment();
463 const currentDate = new Date();
464 for (const chargingProfile of chargingProfiles) {
465 // Set helpers
466 const chargingSchedule = chargingProfile.chargingSchedule;
467 if (!chargingSchedule?.startSchedule) {
468 logger.warn(
469 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: startSchedule is not defined in charging profile id ${chargingProfile.chargingProfileId}`
470 );
471 }
472 // Check type (recurring) and if it is already active
473 // Adjust the daily recurring schedule to today
474 if (
475 chargingProfile.chargingProfileKind === ChargingProfileKindType.RECURRING &&
476 chargingProfile.recurrencyKind === RecurrencyKindType.DAILY &&
477 currentMoment.isAfter(chargingSchedule.startSchedule)
478 ) {
479 if (!(chargingSchedule?.startSchedule instanceof Date)) {
480 logger.warn(
481 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: startSchedule is not a Date object in charging profile id ${chargingProfile.chargingProfileId}. Trying to convert it to a Date object`
482 );
483 chargingSchedule.startSchedule = new Date(chargingSchedule.startSchedule);
484 }
485 chargingSchedule.startSchedule.setFullYear(
486 currentDate.getFullYear(),
487 currentDate.getMonth(),
488 currentDate.getDate()
489 );
490 // Check if the start of the schedule is yesterday
491 if (moment(chargingSchedule.startSchedule).isAfter(currentMoment)) {
492 chargingSchedule.startSchedule.setDate(currentDate.getDate() - 1);
493 }
494 } else if (moment(chargingSchedule.startSchedule).isAfter(currentMoment)) {
495 return null;
496 }
497 // Check if the charging profile is active
498 if (
499 moment(chargingSchedule.startSchedule)
500 .add(chargingSchedule.duration, 's')
501 .isAfter(currentMoment)
502 ) {
503 let lastButOneSchedule: ChargingSchedulePeriod;
504 // Search the right schedule period
505 for (const schedulePeriod of chargingSchedule.chargingSchedulePeriod) {
506 // Handling of only one period
507 if (
508 chargingSchedule.chargingSchedulePeriod.length === 1 &&
509 schedulePeriod.startPeriod === 0
510 ) {
511 const result = {
512 limit: schedulePeriod.limit,
513 matchingChargingProfile: chargingProfile,
514 };
515 logger.debug(debugLogMsg, result);
516 return result;
517 }
518 // Find the right schedule period
519 if (
520 moment(chargingSchedule.startSchedule)
521 .add(schedulePeriod.startPeriod, 's')
522 .isAfter(currentMoment)
523 ) {
524 // Found the schedule: last but one is the correct one
525 const result = {
526 limit: lastButOneSchedule.limit,
527 matchingChargingProfile: chargingProfile,
528 };
529 logger.debug(debugLogMsg, result);
530 return result;
531 }
532 // Keep it
533 lastButOneSchedule = schedulePeriod;
534 // Handle the last schedule period
535 if (
536 schedulePeriod.startPeriod ===
537 chargingSchedule.chargingSchedulePeriod[
538 chargingSchedule.chargingSchedulePeriod.length - 1
539 ].startPeriod
540 ) {
541 const result = {
542 limit: lastButOneSchedule.limit,
543 matchingChargingProfile: chargingProfile,
544 };
545 logger.debug(debugLogMsg, result);
546 return result;
547 }
548 }
549 }
550 }
551 return null;
552 }
553
554 private static getRandomSerialNumberSuffix(params?: {
555 randomBytesLength?: number;
556 upperCase?: boolean;
557 }): string {
558 const randomSerialNumberSuffix = crypto
559 .randomBytes(params?.randomBytesLength ?? 16)
560 .toString('hex');
561 if (params?.upperCase) {
562 return randomSerialNumberSuffix.toUpperCase();
563 }
564 return randomSerialNumberSuffix;
565 }
566 }