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