Add error handling to JSON schemas file reading
[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 './ChargingStation';
8 import BaseError from '../exception/BaseError';
9 import type { ChargingStationInfo } from '../types/ChargingStationInfo';
10 import {
11 AmpereUnits,
12 type ChargingStationTemplate,
13 CurrentType,
14 Voltage,
15 } from '../types/ChargingStationTemplate';
16 import { ChargingProfileKindType, RecurrencyKindType } from '../types/ocpp/1.6/ChargingProfile';
17 import type { OCPP16BootNotificationRequest } from '../types/ocpp/1.6/Requests';
18 import { BootReasonEnumType, type OCPP20BootNotificationRequest } from '../types/ocpp/2.0/Requests';
19 import {
20 type ChargingProfile,
21 ChargingRateUnitType,
22 type ChargingSchedulePeriod,
23 } from '../types/ocpp/ChargingProfile';
24 import { OCPPVersion } from '../types/ocpp/OCPPVersion';
25 import type { BootNotificationRequest } from '../types/ocpp/Requests';
26 import { WorkerProcessType } from '../types/Worker';
27 import Configuration from '../utils/Configuration';
28 import Constants from '../utils/Constants';
29 import { ACElectricUtils, DCElectricUtils } from '../utils/ElectricUtils';
30 import logger from '../utils/Logger';
31 import Utils from '../utils/Utils';
32
33 const moduleName = 'ChargingStationUtils';
34
35 export class ChargingStationUtils {
36 private constructor() {
37 // This is intentional
38 }
39
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;
46 const idSuffix = stationTemplate.nameSuffix ?? '';
47 const idStr = `000000000${index.toString()}`;
48 return stationTemplate?.fixedName
49 ? stationTemplate.baseName
50 : `${stationTemplate.baseName}-${instanceIndex.toString()}${idStr.substring(
51 idStr.length - 4
52 )}${idSuffix}`;
53 }
54
55 public static getHashId(index: number, stationTemplate: ChargingStationTemplate): string {
56 const chargingStationInfo = {
57 chargePointModel: stationTemplate.chargePointModel,
58 chargePointVendor: stationTemplate.chargePointVendor,
59 ...(!Utils.isUndefined(stationTemplate.chargeBoxSerialNumberPrefix) && {
60 chargeBoxSerialNumber: stationTemplate.chargeBoxSerialNumberPrefix,
61 }),
62 ...(!Utils.isUndefined(stationTemplate.chargePointSerialNumberPrefix) && {
63 chargePointSerialNumber: stationTemplate.chargePointSerialNumberPrefix,
64 }),
65 // FIXME?: Should a firmware version change always reference a new configuration file?
66 ...(!Utils.isUndefined(stationTemplate.firmwareVersion) && {
67 firmwareVersion: stationTemplate.firmwareVersion,
68 }),
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,
73 }),
74 ...(!Utils.isUndefined(stationTemplate.meterType) && {
75 meterType: stationTemplate.meterType,
76 }),
77 };
78 return crypto
79 .createHash(Constants.DEFAULT_HASH_ALGORITHM)
80 .update(
81 `${JSON.stringify(chargingStationInfo)}${ChargingStationUtils.getChargingStationId(
82 index,
83 stationTemplate
84 )}`
85 )
86 .digest('hex');
87 }
88
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
113 public static getConfiguredNumberOfConnectors(stationTemplate: ChargingStationTemplate): number {
114 let configuredMaxConnectors: number;
115 if (Utils.isEmptyArray(stationTemplate.numberOfConnectors) === false) {
116 const numberOfConnectors = stationTemplate.numberOfConnectors as number[];
117 configuredMaxConnectors =
118 numberOfConnectors[Math.floor(Utils.secureRandom() * numberOfConnectors.length)];
119 } else if (Utils.isUndefined(stationTemplate.numberOfConnectors) === false) {
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
141 public static createBootNotificationRequest(
142 stationInfo: ChargingStationInfo,
143 bootReason: BootReasonEnumType = BootReasonEnumType.PowerUp
144 ): BootNotificationRequest {
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 {
172 reason: bootReason,
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 }),
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 }),
188 },
189 } as OCPP20BootNotificationRequest;
190 }
191 }
192
193 public static workerPoolInUse(): boolean {
194 return [WorkerProcessType.DYNAMIC_POOL, WorkerProcessType.STATIC_POOL].includes(
195 Configuration.getWorker().processType
196 );
197 }
198
199 public static workerDynamicPoolInUse(): boolean {
200 return Configuration.getWorker().processType === WorkerProcessType.DYNAMIC_POOL;
201 }
202
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])) {
211 logger.warn(
212 `${logPrefix} Deprecated template key '${key}' usage in file '${templateFile}'${
213 !Utils.isEmptyString(logMsgToAppend) && `. ${logMsgToAppend}`
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
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;
240 delete stationTemplate.meterSerialNumberPrefix;
241 return stationTemplate as unknown as ChargingStationInfo;
242 }
243
244 public static createStationInfoHash(stationInfo: ChargingStationInfo): void {
245 delete stationInfo.infoHash;
246 stationInfo.infoHash = crypto
247 .createHash(Constants.DEFAULT_HASH_ALGORITHM)
248 .update(JSON.stringify(stationInfo))
249 .digest('hex');
250 }
251
252 public static createSerialNumber(
253 stationTemplate: ChargingStationTemplate,
254 stationInfo: ChargingStationInfo,
255 params: {
256 randomSerialNumberUpperCase?: boolean;
257 randomSerialNumber?: boolean;
258 } = {
259 randomSerialNumberUpperCase: true,
260 randomSerialNumber: true,
261 }
262 ): void {
263 params = params ?? {};
264 params.randomSerialNumberUpperCase = params?.randomSerialNumberUpperCase ?? true;
265 params.randomSerialNumber = params?.randomSerialNumber ?? true;
266 const serialNumberSuffix = params?.randomSerialNumber
267 ? ChargingStationUtils.getRandomSerialNumberSuffix({
268 upperCase: params.randomSerialNumberUpperCase,
269 })
270 : '';
271 stationInfo.chargePointSerialNumber =
272 !Utils.isEmptyString(stationTemplate?.chargePointSerialNumberPrefix) &&
273 `${stationTemplate.chargePointSerialNumberPrefix}${serialNumberSuffix}`;
274 stationInfo.chargeBoxSerialNumber =
275 !Utils.isEmptyString(stationTemplate?.chargeBoxSerialNumberPrefix) &&
276 `${stationTemplate.chargeBoxSerialNumberPrefix}${serialNumberSuffix}`;
277 stationInfo.meterSerialNumber =
278 !Utils.isEmptyString(stationTemplate?.meterSerialNumberPrefix) &&
279 `${stationTemplate.meterSerialNumberPrefix}${serialNumberSuffix}`;
280 }
281
282 public static propagateSerialNumber(
283 stationTemplate: ChargingStationTemplate,
284 stationInfoSrc: ChargingStationInfo,
285 stationInfoDst: ChargingStationInfo
286 ) {
287 if (!stationInfoSrc || !stationTemplate) {
288 throw new BaseError(
289 'Missing charging station template or existing configuration to propagate serial number'
290 );
291 }
292 stationTemplate?.chargePointSerialNumberPrefix && stationInfoSrc?.chargePointSerialNumber
293 ? (stationInfoDst.chargePointSerialNumber = stationInfoSrc.chargePointSerialNumber)
294 : stationInfoDst?.chargePointSerialNumber && delete stationInfoDst.chargePointSerialNumber;
295 stationTemplate?.chargeBoxSerialNumberPrefix && stationInfoSrc?.chargeBoxSerialNumber
296 ? (stationInfoDst.chargeBoxSerialNumber = stationInfoSrc.chargeBoxSerialNumber)
297 : stationInfoDst?.chargeBoxSerialNumber && delete stationInfoDst.chargeBoxSerialNumber;
298 stationTemplate?.meterSerialNumberPrefix && stationInfoSrc?.meterSerialNumber
299 ? (stationInfoDst.meterSerialNumber = stationInfoSrc.meterSerialNumber)
300 : stationInfoDst?.meterSerialNumber && delete stationInfoDst.meterSerialNumber;
301 }
302
303 public static getAmperageLimitationUnitDivider(stationInfo: ChargingStationInfo): number {
304 let unitDivider = 1;
305 switch (stationInfo.amperageLimitationUnit) {
306 case AmpereUnits.DECI_AMPERE:
307 unitDivider = 10;
308 break;
309 case AmpereUnits.CENTI_AMPERE:
310 unitDivider = 100;
311 break;
312 case AmpereUnits.MILLI_AMPERE:
313 unitDivider = 1000;
314 break;
315 }
316 return unitDivider;
317 }
318
319 public static getChargingStationConnectorChargingProfilesPowerLimit(
320 chargingStation: ChargingStation,
321 connectorId: number
322 ): number | undefined {
323 let limit: number, matchingChargingProfile: ChargingProfile;
324 let chargingProfiles: ChargingProfile[] = [];
325 // Get charging profiles for connector and sort by stack level
326 chargingProfiles = chargingStation
327 .getConnectorStatus(connectorId)
328 ?.chargingProfiles?.sort((a, b) => b.stackLevel - a.stackLevel);
329 // Get profiles on connector 0
330 if (chargingStation.getConnectorStatus(0)?.chargingProfiles) {
331 chargingProfiles.push(
332 ...chargingStation
333 .getConnectorStatus(0)
334 .chargingProfiles.sort((a, b) => b.stackLevel - a.stackLevel)
335 );
336 }
337 if (!Utils.isEmptyArray(chargingProfiles)) {
338 const result = ChargingStationUtils.getLimitFromChargingProfiles(
339 chargingProfiles,
340 chargingStation.logPrefix()
341 );
342 if (!Utils.isNullOrUndefined(result)) {
343 limit = result?.limit;
344 matchingChargingProfile = result?.matchingChargingProfile;
345 switch (chargingStation.getCurrentOutType()) {
346 case CurrentType.AC:
347 limit =
348 matchingChargingProfile.chargingSchedule.chargingRateUnit ===
349 ChargingRateUnitType.WATT
350 ? limit
351 : ACElectricUtils.powerTotal(
352 chargingStation.getNumberOfPhases(),
353 chargingStation.getVoltageOut(),
354 limit
355 );
356 break;
357 case CurrentType.DC:
358 limit =
359 matchingChargingProfile.chargingSchedule.chargingRateUnit ===
360 ChargingRateUnitType.WATT
361 ? limit
362 : DCElectricUtils.power(chargingStation.getVoltageOut(), limit);
363 }
364 const connectorMaximumPower =
365 chargingStation.getMaximumPower() / chargingStation.powerDivider;
366 if (limit > connectorMaximumPower) {
367 logger.error(
368 `${chargingStation.logPrefix()} Charging profile id ${
369 matchingChargingProfile.chargingProfileId
370 } limit ${limit} is greater than connector id ${connectorId} maximum ${connectorMaximumPower}: %j`,
371 result
372 );
373 limit = connectorMaximumPower;
374 }
375 }
376 }
377 return limit;
378 }
379
380 public static getDefaultVoltageOut(
381 currentType: CurrentType,
382 templateFile: string,
383 logPrefix: string
384 ): Voltage {
385 const errMsg = `Unknown ${currentType} currentOutType in template file ${templateFile}, cannot define default voltage out`;
386 let defaultVoltageOut: number;
387 switch (currentType) {
388 case CurrentType.AC:
389 defaultVoltageOut = Voltage.VOLTAGE_230;
390 break;
391 case CurrentType.DC:
392 defaultVoltageOut = Voltage.VOLTAGE_400;
393 break;
394 default:
395 logger.error(`${logPrefix} ${errMsg}`);
396 throw new BaseError(errMsg);
397 }
398 return defaultVoltageOut;
399 }
400
401 public static getAuthorizationFile(stationInfo: ChargingStationInfo): string | undefined {
402 return (
403 stationInfo.authorizationFile &&
404 path.join(
405 path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../'),
406 'assets',
407 path.basename(stationInfo.authorizationFile)
408 )
409 );
410 }
411
412 /**
413 * Charging profiles should already be sorted by connectorId and stack level (highest stack level has priority)
414 *
415 * @param chargingProfiles -
416 * @param logPrefix -
417 * @returns
418 */
419 private static getLimitFromChargingProfiles(
420 chargingProfiles: ChargingProfile[],
421 logPrefix: string
422 ): {
423 limit: number;
424 matchingChargingProfile: ChargingProfile;
425 } | null {
426 const debugLogMsg = `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Matching charging profile found for power limitation: %j`;
427 for (const chargingProfile of chargingProfiles) {
428 // Set helpers
429 const currentMoment = moment();
430 const chargingSchedule = chargingProfile.chargingSchedule;
431 // Check type (recurring) and if it is already active
432 // Adjust the daily recurring schedule to today
433 if (
434 chargingProfile.chargingProfileKind === ChargingProfileKindType.RECURRING &&
435 chargingProfile.recurrencyKind === RecurrencyKindType.DAILY &&
436 currentMoment.isAfter(chargingSchedule.startSchedule)
437 ) {
438 const currentDate = new Date();
439 chargingSchedule.startSchedule = new Date(chargingSchedule.startSchedule);
440 chargingSchedule.startSchedule.setFullYear(
441 currentDate.getFullYear(),
442 currentDate.getMonth(),
443 currentDate.getDate()
444 );
445 // Check if the start of the schedule is yesterday
446 if (moment(chargingSchedule.startSchedule).isAfter(currentMoment)) {
447 chargingSchedule.startSchedule.setDate(currentDate.getDate() - 1);
448 }
449 } else if (moment(chargingSchedule.startSchedule).isAfter(currentMoment)) {
450 return null;
451 }
452 // Check if the charging profile is active
453 if (
454 moment(chargingSchedule.startSchedule)
455 .add(chargingSchedule.duration, 's')
456 .isAfter(currentMoment)
457 ) {
458 let lastButOneSchedule: ChargingSchedulePeriod;
459 // Search the right schedule period
460 for (const schedulePeriod of chargingSchedule.chargingSchedulePeriod) {
461 // Handling of only one period
462 if (
463 chargingSchedule.chargingSchedulePeriod.length === 1 &&
464 schedulePeriod.startPeriod === 0
465 ) {
466 const result = {
467 limit: schedulePeriod.limit,
468 matchingChargingProfile: chargingProfile,
469 };
470 logger.debug(debugLogMsg, result);
471 return result;
472 }
473 // Find the right schedule period
474 if (
475 moment(chargingSchedule.startSchedule)
476 .add(schedulePeriod.startPeriod, 's')
477 .isAfter(currentMoment)
478 ) {
479 // Found the schedule: last but one is the correct one
480 const result = {
481 limit: lastButOneSchedule.limit,
482 matchingChargingProfile: chargingProfile,
483 };
484 logger.debug(debugLogMsg, result);
485 return result;
486 }
487 // Keep it
488 lastButOneSchedule = schedulePeriod;
489 // Handle the last schedule period
490 if (
491 schedulePeriod.startPeriod ===
492 chargingSchedule.chargingSchedulePeriod[
493 chargingSchedule.chargingSchedulePeriod.length - 1
494 ].startPeriod
495 ) {
496 const result = {
497 limit: lastButOneSchedule.limit,
498 matchingChargingProfile: chargingProfile,
499 };
500 logger.debug(debugLogMsg, result);
501 return result;
502 }
503 }
504 }
505 }
506 return null;
507 }
508
509 private static getRandomSerialNumberSuffix(params?: {
510 randomBytesLength?: number;
511 upperCase?: boolean;
512 }): string {
513 const randomSerialNumberSuffix = crypto
514 .randomBytes(params?.randomBytesLength ?? 16)
515 .toString('hex');
516 if (params?.upperCase) {
517 return randomSerialNumberSuffix.toUpperCase();
518 }
519 return randomSerialNumberSuffix;
520 }
521 }