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