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