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