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