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