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