fix: brown paper bag issue at referencing the same literal object instance
[e-mobility-charging-stations-simulator.git] / src / charging-station / ChargingStationUtils.ts
CommitLineData
43ee4373 1import crypto from 'node:crypto';
130783a7
JB
2import path from 'node:path';
3import { fileURLToPath } from 'node:url';
8114d10e
JB
4
5import moment from 'moment';
6
2896e06d 7import type { ChargingStation } from './internal';
268a74bb 8import { BaseError } from '../exception';
981ebfbe 9import {
492cf6ab 10 AmpereUnits,
268a74bb
JB
11 type BootNotificationRequest,
12 BootReasonEnumType,
15068be9 13 type ChargingProfile,
268a74bb 14 ChargingProfileKindType,
15068be9
JB
15 ChargingRateUnitType,
16 type ChargingSchedulePeriod,
268a74bb
JB
17 type ChargingStationInfo,
18 type ChargingStationTemplate,
19 CurrentType,
20 type OCPP16BootNotificationRequest,
21 type OCPP20BootNotificationRequest,
22 OCPPVersion,
23 RecurrencyKindType,
24 Voltage,
25} from '../types';
60a74391
JB
26import {
27 ACElectricUtils,
28 Configuration,
29 Constants,
30 DCElectricUtils,
31 Utils,
32 logger,
33} from '../utils';
268a74bb 34import { WorkerProcessType } from '../worker';
17ac262c 35
91a4f151
JB
36const moduleName = 'ChargingStationUtils';
37
17ac262c 38export class ChargingStationUtils {
d5bd1c00
JB
39 private constructor() {
40 // This is intentional
41 }
42
17ac262c
JB
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;
1b271a54 49 const idSuffix = stationTemplate?.nameSuffix ?? '';
44eb6026 50 const idStr = `000000000${index.toString()}`;
ccb1d6e9 51 return stationTemplate?.fixedName
17ac262c 52 ? stationTemplate.baseName
44eb6026
JB
53 : `${stationTemplate.baseName}-${instanceIndex.toString()}${idStr.substring(
54 idStr.length - 4
55 )}${idSuffix}`;
17ac262c
JB
56 }
57
fa7bccf4 58 public static getHashId(index: number, stationTemplate: ChargingStationTemplate): string {
99e92237 59 const chargingStationInfo = {
fa7bccf4
JB
60 chargePointModel: stationTemplate.chargePointModel,
61 chargePointVendor: stationTemplate.chargePointVendor,
62 ...(!Utils.isUndefined(stationTemplate.chargeBoxSerialNumberPrefix) && {
63 chargeBoxSerialNumber: stationTemplate.chargeBoxSerialNumberPrefix,
17ac262c 64 }),
fa7bccf4
JB
65 ...(!Utils.isUndefined(stationTemplate.chargePointSerialNumberPrefix) && {
66 chargePointSerialNumber: stationTemplate.chargePointSerialNumberPrefix,
17ac262c 67 }),
33d7ecc7 68 // FIXME?: Should a firmware version change always reference a new configuration file?
fa7bccf4
JB
69 ...(!Utils.isUndefined(stationTemplate.firmwareVersion) && {
70 firmwareVersion: stationTemplate.firmwareVersion,
17ac262c 71 }),
fa7bccf4
JB
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,
17ac262c 76 }),
fa7bccf4
JB
77 ...(!Utils.isUndefined(stationTemplate.meterType) && {
78 meterType: stationTemplate.meterType,
17ac262c
JB
79 }),
80 };
81 return crypto
82 .createHash(Constants.DEFAULT_HASH_ALGORITHM)
fa7bccf4 83 .update(
14ecae6a
JB
84 `${JSON.stringify(chargingStationInfo)}${ChargingStationUtils.getChargingStationId(
85 index,
86 stationTemplate
87 )}`
fa7bccf4 88 )
17ac262c
JB
89 .digest('hex');
90 }
91
fa7bccf4
JB
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
c72f6634 116 public static getConfiguredNumberOfConnectors(stationTemplate: ChargingStationTemplate): number {
fa7bccf4 117 let configuredMaxConnectors: number;
53ac516c 118 if (Utils.isNotEmptyArray(stationTemplate.numberOfConnectors) === true) {
fa7bccf4 119 const numberOfConnectors = stationTemplate.numberOfConnectors as number[];
c72f6634
JB
120 configuredMaxConnectors =
121 numberOfConnectors[Math.floor(Utils.secureRandom() * numberOfConnectors.length)];
122 } else if (Utils.isUndefined(stationTemplate.numberOfConnectors) === false) {
fa7bccf4
JB
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
17ac262c 144 public static createBootNotificationRequest(
15068be9
JB
145 stationInfo: ChargingStationInfo,
146 bootReason: BootReasonEnumType = BootReasonEnumType.PowerUp
17ac262c 147 ): BootNotificationRequest {
d270cc87
JB
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 {
15068be9 175 reason: bootReason,
d270cc87
JB
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 }),
98fc1389
JB
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 }),
d270cc87
JB
191 },
192 } as OCPP20BootNotificationRequest;
193 }
17ac262c
JB
194 }
195
196 public static workerPoolInUse(): boolean {
721646e9 197 return [WorkerProcessType.dynamicPool, WorkerProcessType.staticPool].includes(
cf2a5d9b 198 Configuration.getWorker().processType
17ac262c
JB
199 );
200 }
201
202 public static workerDynamicPoolInUse(): boolean {
721646e9 203 return Configuration.getWorker().processType === WorkerProcessType.dynamicPool;
17ac262c
JB
204 }
205
17ac262c
JB
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])) {
17ac262c
JB
214 logger.warn(
215 `${logPrefix} Deprecated template key '${key}' usage in file '${templateFile}'${
5a2a53cf 216 Utils.isNotEmptyString(logMsgToAppend) && `. ${logMsgToAppend}`
17ac262c
JB
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
fa7bccf4
JB
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;
fec4d204 243 delete stationTemplate.meterSerialNumberPrefix;
51c83d6f 244 return stationTemplate as unknown as ChargingStationInfo;
fa7bccf4
JB
245 }
246
247 public static createStationInfoHash(stationInfo: ChargingStationInfo): void {
ccb1d6e9 248 delete stationInfo.infoHash;
7c72977b 249 stationInfo.infoHash = crypto
ccb1d6e9
JB
250 .createHash(Constants.DEFAULT_HASH_ALGORITHM)
251 .update(JSON.stringify(stationInfo))
252 .digest('hex');
17ac262c
JB
253 }
254
255 public static createSerialNumber(
fa7bccf4 256 stationTemplate: ChargingStationTemplate,
4d20f040 257 stationInfo: ChargingStationInfo,
fa7bccf4
JB
258 params: {
259 randomSerialNumberUpperCase?: boolean;
260 randomSerialNumber?: boolean;
261 } = {
17ac262c
JB
262 randomSerialNumberUpperCase: true,
263 randomSerialNumber: true,
264 }
265 ): void {
abe9e9dd 266 params = params ?? {};
17ac262c
JB
267 params.randomSerialNumberUpperCase = params?.randomSerialNumberUpperCase ?? true;
268 params.randomSerialNumber = params?.randomSerialNumber ?? true;
fa7bccf4
JB
269 const serialNumberSuffix = params?.randomSerialNumber
270 ? ChargingStationUtils.getRandomSerialNumberSuffix({
271 upperCase: params.randomSerialNumberUpperCase,
272 })
273 : '';
9a15316c
JB
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;
fec4d204
JB
287 }
288
289 public static propagateSerialNumber(
290 stationTemplate: ChargingStationTemplate,
291 stationInfoSrc: ChargingStationInfo,
4d20f040 292 stationInfoDst: ChargingStationInfo
fec4d204
JB
293 ) {
294 if (!stationInfoSrc || !stationTemplate) {
baf93dda
JB
295 throw new BaseError(
296 'Missing charging station template or existing configuration to propagate serial number'
297 );
fec4d204
JB
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;
17ac262c
JB
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
15068be9
JB
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)
1895299d 335 ?.chargingProfiles?.sort((a, b) => b.stackLevel - a.stackLevel);
15068be9 336 // Get profiles on connector 0
1895299d 337 if (chargingStation.getConnectorStatus(0)?.chargingProfiles) {
15068be9
JB
338 chargingProfiles.push(
339 ...chargingStation
340 .getConnectorStatus(0)
341 .chargingProfiles.sort((a, b) => b.stackLevel - a.stackLevel)
342 );
343 }
53ac516c 344 if (Utils.isNotEmptyArray(chargingProfiles)) {
15068be9
JB
345 const result = ChargingStationUtils.getLimitFromChargingProfiles(
346 chargingProfiles,
347 chargingStation.logPrefix()
348 );
349 if (!Utils.isNullOrUndefined(result)) {
1895299d
JB
350 limit = result?.limit;
351 matchingChargingProfile = result?.matchingChargingProfile;
15068be9
JB
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
17ac262c
JB
419 /**
420 * Charging profiles should already be sorted by connectorId and stack level (highest stack level has priority)
421 *
0e4fa348
JB
422 * @param chargingProfiles -
423 * @param logPrefix -
424 * @returns
17ac262c 425 */
15068be9 426 private static getLimitFromChargingProfiles(
17ac262c
JB
427 chargingProfiles: ChargingProfile[],
428 logPrefix: string
429 ): {
430 limit: number;
431 matchingChargingProfile: ChargingProfile;
432 } | null {
91a4f151 433 const debugLogMsg = `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Matching charging profile found for power limitation: %j`;
17ac262c
JB
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 };
91a4f151 477 logger.debug(debugLogMsg, result);
17ac262c
JB
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 };
91a4f151 491 logger.debug(debugLogMsg, result);
17ac262c
JB
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 };
91a4f151 507 logger.debug(debugLogMsg, result);
17ac262c
JB
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}