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