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