refactor: rename a template key to a more sensible name
[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 // 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 warnDeprecatedTemplateKey(
216 template: ChargingStationTemplate,
217 key: string,
218 templateFile: string,
219 logPrefix: string,
220 logMsgToAppend = ''
221 ): void {
222 if (!Utils.isUndefined(template[key])) {
223 const logMsg = `Deprecated template key '${key}' usage in file '${templateFile}'${
224 Utils.isNotEmptyString(logMsgToAppend) ? `. ${logMsgToAppend}` : ''
225 }`;
226 logger.warn(`${logPrefix} ${logMsg}`);
227 console.warn(chalk.yellow(`${logMsg}`));
228 }
229 }
230
231 public static convertDeprecatedTemplateKey(
232 template: ChargingStationTemplate,
233 deprecatedKey: string,
234 key: string
235 ): void {
236 if (!Utils.isUndefined(template[deprecatedKey])) {
237 template[key] = template[deprecatedKey] as unknown;
238 delete template[deprecatedKey];
239 }
240 }
241
242 public static stationTemplateToStationInfo(
243 stationTemplate: ChargingStationTemplate
244 ): ChargingStationInfo {
245 stationTemplate = Utils.cloneObject(stationTemplate);
246 delete stationTemplate.power;
247 delete stationTemplate.powerUnit;
248 delete stationTemplate.Configuration;
249 delete stationTemplate.AutomaticTransactionGenerator;
250 delete stationTemplate.chargeBoxSerialNumberPrefix;
251 delete stationTemplate.chargePointSerialNumberPrefix;
252 delete stationTemplate.meterSerialNumberPrefix;
253 return stationTemplate as unknown as ChargingStationInfo;
254 }
255
256 public static createStationInfoHash(stationInfo: ChargingStationInfo): void {
257 delete stationInfo.infoHash;
258 stationInfo.infoHash = crypto
259 .createHash(Constants.DEFAULT_HASH_ALGORITHM)
260 .update(JSON.stringify(stationInfo))
261 .digest('hex');
262 }
263
264 public static createSerialNumber(
265 stationTemplate: ChargingStationTemplate,
266 stationInfo: ChargingStationInfo,
267 params: {
268 randomSerialNumberUpperCase?: boolean;
269 randomSerialNumber?: boolean;
270 } = {
271 randomSerialNumberUpperCase: true,
272 randomSerialNumber: true,
273 }
274 ): void {
275 params = params ?? {};
276 params.randomSerialNumberUpperCase = params?.randomSerialNumberUpperCase ?? true;
277 params.randomSerialNumber = params?.randomSerialNumber ?? true;
278 const serialNumberSuffix = params?.randomSerialNumber
279 ? ChargingStationUtils.getRandomSerialNumberSuffix({
280 upperCase: params.randomSerialNumberUpperCase,
281 })
282 : '';
283 stationInfo.chargePointSerialNumber = Utils.isNotEmptyString(
284 stationTemplate?.chargePointSerialNumberPrefix
285 )
286 ? `${stationTemplate.chargePointSerialNumberPrefix}${serialNumberSuffix}`
287 : undefined;
288 stationInfo.chargeBoxSerialNumber = Utils.isNotEmptyString(
289 stationTemplate?.chargeBoxSerialNumberPrefix
290 )
291 ? `${stationTemplate.chargeBoxSerialNumberPrefix}${serialNumberSuffix}`
292 : undefined;
293 stationInfo.meterSerialNumber = Utils.isNotEmptyString(stationTemplate?.meterSerialNumberPrefix)
294 ? `${stationTemplate.meterSerialNumberPrefix}${serialNumberSuffix}`
295 : undefined;
296 }
297
298 public static propagateSerialNumber(
299 stationTemplate: ChargingStationTemplate,
300 stationInfoSrc: ChargingStationInfo,
301 stationInfoDst: ChargingStationInfo
302 ) {
303 if (!stationInfoSrc || !stationTemplate) {
304 throw new BaseError(
305 'Missing charging station template or existing configuration to propagate serial number'
306 );
307 }
308 stationTemplate?.chargePointSerialNumberPrefix && stationInfoSrc?.chargePointSerialNumber
309 ? (stationInfoDst.chargePointSerialNumber = stationInfoSrc.chargePointSerialNumber)
310 : stationInfoDst?.chargePointSerialNumber && delete stationInfoDst.chargePointSerialNumber;
311 stationTemplate?.chargeBoxSerialNumberPrefix && stationInfoSrc?.chargeBoxSerialNumber
312 ? (stationInfoDst.chargeBoxSerialNumber = stationInfoSrc.chargeBoxSerialNumber)
313 : stationInfoDst?.chargeBoxSerialNumber && delete stationInfoDst.chargeBoxSerialNumber;
314 stationTemplate?.meterSerialNumberPrefix && stationInfoSrc?.meterSerialNumber
315 ? (stationInfoDst.meterSerialNumber = stationInfoSrc.meterSerialNumber)
316 : stationInfoDst?.meterSerialNumber && delete stationInfoDst.meterSerialNumber;
317 }
318
319 public static getAmperageLimitationUnitDivider(stationInfo: ChargingStationInfo): number {
320 let unitDivider = 1;
321 switch (stationInfo.amperageLimitationUnit) {
322 case AmpereUnits.DECI_AMPERE:
323 unitDivider = 10;
324 break;
325 case AmpereUnits.CENTI_AMPERE:
326 unitDivider = 100;
327 break;
328 case AmpereUnits.MILLI_AMPERE:
329 unitDivider = 1000;
330 break;
331 }
332 return unitDivider;
333 }
334
335 public static getChargingStationConnectorChargingProfilesPowerLimit(
336 chargingStation: ChargingStation,
337 connectorId: number
338 ): number | undefined {
339 let limit: number, matchingChargingProfile: ChargingProfile;
340 let chargingProfiles: ChargingProfile[] = [];
341 // Get charging profiles for connector and sort by stack level
342 chargingProfiles = chargingStation
343 .getConnectorStatus(connectorId)
344 ?.chargingProfiles?.sort((a, b) => b.stackLevel - a.stackLevel);
345 // Get profiles on connector 0
346 if (chargingStation.getConnectorStatus(0)?.chargingProfiles) {
347 chargingProfiles.push(
348 ...chargingStation
349 .getConnectorStatus(0)
350 .chargingProfiles.sort((a, b) => b.stackLevel - a.stackLevel)
351 );
352 }
353 if (Utils.isNotEmptyArray(chargingProfiles)) {
354 const result = ChargingStationUtils.getLimitFromChargingProfiles(
355 chargingProfiles,
356 chargingStation.logPrefix()
357 );
358 if (!Utils.isNullOrUndefined(result)) {
359 limit = result?.limit;
360 matchingChargingProfile = result?.matchingChargingProfile;
361 switch (chargingStation.getCurrentOutType()) {
362 case CurrentType.AC:
363 limit =
364 matchingChargingProfile.chargingSchedule.chargingRateUnit ===
365 ChargingRateUnitType.WATT
366 ? limit
367 : ACElectricUtils.powerTotal(
368 chargingStation.getNumberOfPhases(),
369 chargingStation.getVoltageOut(),
370 limit
371 );
372 break;
373 case CurrentType.DC:
374 limit =
375 matchingChargingProfile.chargingSchedule.chargingRateUnit ===
376 ChargingRateUnitType.WATT
377 ? limit
378 : DCElectricUtils.power(chargingStation.getVoltageOut(), limit);
379 }
380 const connectorMaximumPower =
381 chargingStation.getMaximumPower() / chargingStation.powerDivider;
382 if (limit > connectorMaximumPower) {
383 logger.error(
384 `${chargingStation.logPrefix()} Charging profile id ${
385 matchingChargingProfile.chargingProfileId
386 } limit ${limit} is greater than connector id ${connectorId} maximum ${connectorMaximumPower}: %j`,
387 result
388 );
389 limit = connectorMaximumPower;
390 }
391 }
392 }
393 return limit;
394 }
395
396 public static getDefaultVoltageOut(
397 currentType: CurrentType,
398 templateFile: string,
399 logPrefix: string
400 ): Voltage {
401 const errMsg = `Unknown ${currentType} currentOutType in template file ${templateFile}, cannot define default voltage out`;
402 let defaultVoltageOut: number;
403 switch (currentType) {
404 case CurrentType.AC:
405 defaultVoltageOut = Voltage.VOLTAGE_230;
406 break;
407 case CurrentType.DC:
408 defaultVoltageOut = Voltage.VOLTAGE_400;
409 break;
410 default:
411 logger.error(`${logPrefix} ${errMsg}`);
412 throw new BaseError(errMsg);
413 }
414 return defaultVoltageOut;
415 }
416
417 public static getIdTagsFile(stationInfo: ChargingStationInfo): string | undefined {
418 return (
419 stationInfo.idTagsFile &&
420 path.join(
421 path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../'),
422 'assets',
423 path.basename(stationInfo.idTagsFile)
424 )
425 );
426 }
427
428 /**
429 * Charging profiles should already be sorted by connectorId and stack level (highest stack level has priority)
430 *
431 * @param chargingProfiles -
432 * @param logPrefix -
433 * @returns
434 */
435 private static getLimitFromChargingProfiles(
436 chargingProfiles: ChargingProfile[],
437 logPrefix: string
438 ): {
439 limit: number;
440 matchingChargingProfile: ChargingProfile;
441 } | null {
442 const debugLogMsg = `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Matching charging profile found for power limitation: %j`;
443 for (const chargingProfile of chargingProfiles) {
444 // Set helpers
445 const currentMoment = moment();
446 const chargingSchedule = chargingProfile.chargingSchedule;
447 // Check type (recurring) and if it is already active
448 // Adjust the daily recurring schedule to today
449 if (
450 chargingProfile.chargingProfileKind === ChargingProfileKindType.RECURRING &&
451 chargingProfile.recurrencyKind === RecurrencyKindType.DAILY &&
452 currentMoment.isAfter(chargingSchedule.startSchedule)
453 ) {
454 const currentDate = new Date();
455 chargingSchedule.startSchedule = new Date(chargingSchedule.startSchedule);
456 chargingSchedule.startSchedule.setFullYear(
457 currentDate.getFullYear(),
458 currentDate.getMonth(),
459 currentDate.getDate()
460 );
461 // Check if the start of the schedule is yesterday
462 if (moment(chargingSchedule.startSchedule).isAfter(currentMoment)) {
463 chargingSchedule.startSchedule.setDate(currentDate.getDate() - 1);
464 }
465 } else if (moment(chargingSchedule.startSchedule).isAfter(currentMoment)) {
466 return null;
467 }
468 // Check if the charging profile is active
469 if (
470 moment(chargingSchedule.startSchedule)
471 .add(chargingSchedule.duration, 's')
472 .isAfter(currentMoment)
473 ) {
474 let lastButOneSchedule: ChargingSchedulePeriod;
475 // Search the right schedule period
476 for (const schedulePeriod of chargingSchedule.chargingSchedulePeriod) {
477 // Handling of only one period
478 if (
479 chargingSchedule.chargingSchedulePeriod.length === 1 &&
480 schedulePeriod.startPeriod === 0
481 ) {
482 const result = {
483 limit: schedulePeriod.limit,
484 matchingChargingProfile: chargingProfile,
485 };
486 logger.debug(debugLogMsg, result);
487 return result;
488 }
489 // Find the right schedule period
490 if (
491 moment(chargingSchedule.startSchedule)
492 .add(schedulePeriod.startPeriod, 's')
493 .isAfter(currentMoment)
494 ) {
495 // Found the schedule: last but one is the correct one
496 const result = {
497 limit: lastButOneSchedule.limit,
498 matchingChargingProfile: chargingProfile,
499 };
500 logger.debug(debugLogMsg, result);
501 return result;
502 }
503 // Keep it
504 lastButOneSchedule = schedulePeriod;
505 // Handle the last schedule period
506 if (
507 schedulePeriod.startPeriod ===
508 chargingSchedule.chargingSchedulePeriod[
509 chargingSchedule.chargingSchedulePeriod.length - 1
510 ].startPeriod
511 ) {
512 const result = {
513 limit: lastButOneSchedule.limit,
514 matchingChargingProfile: chargingProfile,
515 };
516 logger.debug(debugLogMsg, result);
517 return result;
518 }
519 }
520 }
521 }
522 return null;
523 }
524
525 private static getRandomSerialNumberSuffix(params?: {
526 randomBytesLength?: number;
527 upperCase?: boolean;
528 }): string {
529 const randomSerialNumberSuffix = crypto
530 .randomBytes(params?.randomBytesLength ?? 16)
531 .toString('hex');
532 if (params?.upperCase) {
533 return randomSerialNumberSuffix.toUpperCase();
534 }
535 return randomSerialNumberSuffix;
536 }
537 }