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