9d23f1b4d7e6135f94f9205b57c03b8b6ed175c8
[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.isNotEmptyArray(stationTemplate.numberOfConnectors) === true) {
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.isNotEmptyString(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 = 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;
284 }
285
286 public static propagateSerialNumber(
287 stationTemplate: ChargingStationTemplate,
288 stationInfoSrc: ChargingStationInfo,
289 stationInfoDst: ChargingStationInfo
290 ) {
291 if (!stationInfoSrc || !stationTemplate) {
292 throw new BaseError(
293 'Missing charging station template or existing configuration to propagate serial number'
294 );
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;
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
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)
332 ?.chargingProfiles?.sort((a, b) => b.stackLevel - a.stackLevel);
333 // Get profiles on connector 0
334 if (chargingStation.getConnectorStatus(0)?.chargingProfiles) {
335 chargingProfiles.push(
336 ...chargingStation
337 .getConnectorStatus(0)
338 .chargingProfiles.sort((a, b) => b.stackLevel - a.stackLevel)
339 );
340 }
341 if (Utils.isNotEmptyArray(chargingProfiles)) {
342 const result = ChargingStationUtils.getLimitFromChargingProfiles(
343 chargingProfiles,
344 chargingStation.logPrefix()
345 );
346 if (!Utils.isNullOrUndefined(result)) {
347 limit = result?.limit;
348 matchingChargingProfile = result?.matchingChargingProfile;
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
416 /**
417 * Charging profiles should already be sorted by connectorId and stack level (highest stack level has priority)
418 *
419 * @param chargingProfiles -
420 * @param logPrefix -
421 * @returns
422 */
423 private static getLimitFromChargingProfiles(
424 chargingProfiles: ChargingProfile[],
425 logPrefix: string
426 ): {
427 limit: number;
428 matchingChargingProfile: ChargingProfile;
429 } | null {
430 const debugLogMsg = `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Matching charging profile found for power limitation: %j`;
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 };
474 logger.debug(debugLogMsg, result);
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 };
488 logger.debug(debugLogMsg, result);
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 };
504 logger.debug(debugLogMsg, result);
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 }