perf(simulator): build once address for tags caches addressable indexes
[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
1bf29f5b
JB
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
fa7bccf4
JB
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
c72f6634 124 public static getConfiguredNumberOfConnectors(stationTemplate: ChargingStationTemplate): number {
fa7bccf4 125 let configuredMaxConnectors: number;
53ac516c 126 if (Utils.isNotEmptyArray(stationTemplate.numberOfConnectors) === true) {
fa7bccf4 127 const numberOfConnectors = stationTemplate.numberOfConnectors as number[];
c72f6634
JB
128 configuredMaxConnectors =
129 numberOfConnectors[Math.floor(Utils.secureRandom() * numberOfConnectors.length)];
130 } else if (Utils.isUndefined(stationTemplate.numberOfConnectors) === false) {
fa7bccf4
JB
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
17ac262c 152 public static createBootNotificationRequest(
15068be9
JB
153 stationInfo: ChargingStationInfo,
154 bootReason: BootReasonEnumType = BootReasonEnumType.PowerUp
17ac262c 155 ): BootNotificationRequest {
d270cc87
JB
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 {
15068be9 183 reason: bootReason,
d270cc87
JB
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 }),
98fc1389
JB
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 }),
d270cc87
JB
199 },
200 } as OCPP20BootNotificationRequest;
201 }
17ac262c
JB
202 }
203
204 public static workerPoolInUse(): boolean {
721646e9 205 return [WorkerProcessType.dynamicPool, WorkerProcessType.staticPool].includes(
cf2a5d9b 206 Configuration.getWorker().processType
17ac262c
JB
207 );
208 }
209
210 public static workerDynamicPoolInUse(): boolean {
721646e9 211 return Configuration.getWorker().processType === WorkerProcessType.dynamicPool;
17ac262c
JB
212 }
213
17ac262c
JB
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])) {
17ac262c
JB
222 logger.warn(
223 `${logPrefix} Deprecated template key '${key}' usage in file '${templateFile}'${
5a2a53cf 224 Utils.isNotEmptyString(logMsgToAppend) && `. ${logMsgToAppend}`
17ac262c
JB
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
fa7bccf4
JB
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;
fec4d204 251 delete stationTemplate.meterSerialNumberPrefix;
51c83d6f 252 return stationTemplate as unknown as ChargingStationInfo;
fa7bccf4
JB
253 }
254
255 public static createStationInfoHash(stationInfo: ChargingStationInfo): void {
ccb1d6e9 256 delete stationInfo.infoHash;
7c72977b 257 stationInfo.infoHash = crypto
ccb1d6e9
JB
258 .createHash(Constants.DEFAULT_HASH_ALGORITHM)
259 .update(JSON.stringify(stationInfo))
260 .digest('hex');
17ac262c
JB
261 }
262
263 public static createSerialNumber(
fa7bccf4 264 stationTemplate: ChargingStationTemplate,
4d20f040 265 stationInfo: ChargingStationInfo,
fa7bccf4
JB
266 params: {
267 randomSerialNumberUpperCase?: boolean;
268 randomSerialNumber?: boolean;
269 } = {
17ac262c
JB
270 randomSerialNumberUpperCase: true,
271 randomSerialNumber: true,
272 }
273 ): void {
abe9e9dd 274 params = params ?? {};
17ac262c
JB
275 params.randomSerialNumberUpperCase = params?.randomSerialNumberUpperCase ?? true;
276 params.randomSerialNumber = params?.randomSerialNumber ?? true;
fa7bccf4
JB
277 const serialNumberSuffix = params?.randomSerialNumber
278 ? ChargingStationUtils.getRandomSerialNumberSuffix({
279 upperCase: params.randomSerialNumberUpperCase,
280 })
281 : '';
9a15316c
JB
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;
fec4d204
JB
295 }
296
297 public static propagateSerialNumber(
298 stationTemplate: ChargingStationTemplate,
299 stationInfoSrc: ChargingStationInfo,
4d20f040 300 stationInfoDst: ChargingStationInfo
fec4d204
JB
301 ) {
302 if (!stationInfoSrc || !stationTemplate) {
baf93dda
JB
303 throw new BaseError(
304 'Missing charging station template or existing configuration to propagate serial number'
305 );
fec4d204
JB
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;
17ac262c
JB
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
15068be9
JB
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)
1895299d 343 ?.chargingProfiles?.sort((a, b) => b.stackLevel - a.stackLevel);
15068be9 344 // Get profiles on connector 0
1895299d 345 if (chargingStation.getConnectorStatus(0)?.chargingProfiles) {
15068be9
JB
346 chargingProfiles.push(
347 ...chargingStation
348 .getConnectorStatus(0)
349 .chargingProfiles.sort((a, b) => b.stackLevel - a.stackLevel)
350 );
351 }
53ac516c 352 if (Utils.isNotEmptyArray(chargingProfiles)) {
15068be9
JB
353 const result = ChargingStationUtils.getLimitFromChargingProfiles(
354 chargingProfiles,
355 chargingStation.logPrefix()
356 );
357 if (!Utils.isNullOrUndefined(result)) {
1895299d
JB
358 limit = result?.limit;
359 matchingChargingProfile = result?.matchingChargingProfile;
15068be9
JB
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
17ac262c
JB
427 /**
428 * Charging profiles should already be sorted by connectorId and stack level (highest stack level has priority)
429 *
0e4fa348
JB
430 * @param chargingProfiles -
431 * @param logPrefix -
432 * @returns
17ac262c 433 */
15068be9 434 private static getLimitFromChargingProfiles(
17ac262c
JB
435 chargingProfiles: ChargingProfile[],
436 logPrefix: string
437 ): {
438 limit: number;
439 matchingChargingProfile: ChargingProfile;
440 } | null {
91a4f151 441 const debugLogMsg = `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Matching charging profile found for power limitation: %j`;
17ac262c
JB
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 };
91a4f151 485 logger.debug(debugLogMsg, result);
17ac262c
JB
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 };
91a4f151 499 logger.debug(debugLogMsg, result);
17ac262c
JB
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 };
91a4f151 515 logger.debug(debugLogMsg, result);
17ac262c
JB
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}