Move hashId to stationInfo
[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 type ChargingStationInfo from '../types/ChargingStationInfo';
9 import ChargingStationTemplate, {
10 AmpereUnits,
11 CurrentType,
12 Voltage,
13 } from '../types/ChargingStationTemplate';
14 import type { SampledValueTemplate } from '../types/MeasurandPerPhaseSampledValueTemplates';
15 import { ChargingProfileKindType, RecurrencyKindType } from '../types/ocpp/1.6/ChargingProfile';
16 import type { ChargingProfile, ChargingSchedulePeriod } from '../types/ocpp/ChargingProfile';
17 import { StandardParametersKey } from '../types/ocpp/Configuration';
18 import { MeterValueMeasurand, MeterValuePhase } from '../types/ocpp/MeterValues';
19 import {
20 BootNotificationRequest,
21 IncomingRequestCommand,
22 RequestCommand,
23 } from '../types/ocpp/Requests';
24 import { WebSocketCloseEventStatusString } from '../types/WebSocket';
25 import { WorkerProcessType } from '../types/Worker';
26 import Configuration from '../utils/Configuration';
27 import Constants from '../utils/Constants';
28 import logger from '../utils/Logger';
29 import Utils from '../utils/Utils';
30 import type ChargingStation from './ChargingStation';
31 import { ChargingStationConfigurationUtils } from './ChargingStationConfigurationUtils';
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 +
51 '-' +
52 instanceIndex.toString() +
53 idStr.substring(idStr.length - 4) +
54 idSuffix;
55 }
56
57 public static getHashId(index: number, stationTemplate: ChargingStationTemplate): string {
58 const hashBootNotificationRequest = {
59 chargePointModel: stationTemplate.chargePointModel,
60 chargePointVendor: stationTemplate.chargePointVendor,
61 ...(!Utils.isUndefined(stationTemplate.chargeBoxSerialNumberPrefix) && {
62 chargeBoxSerialNumber: stationTemplate.chargeBoxSerialNumberPrefix,
63 }),
64 ...(!Utils.isUndefined(stationTemplate.chargePointSerialNumberPrefix) && {
65 chargePointSerialNumber: stationTemplate.chargePointSerialNumberPrefix,
66 }),
67 ...(!Utils.isUndefined(stationTemplate.firmwareVersion) && {
68 firmwareVersion: stationTemplate.firmwareVersion,
69 }),
70 ...(!Utils.isUndefined(stationTemplate.iccid) && { iccid: stationTemplate.iccid }),
71 ...(!Utils.isUndefined(stationTemplate.imsi) && { imsi: stationTemplate.imsi }),
72 ...(!Utils.isUndefined(stationTemplate.meterSerialNumberPrefix) && {
73 meterSerialNumber: stationTemplate.meterSerialNumberPrefix,
74 }),
75 ...(!Utils.isUndefined(stationTemplate.meterType) && {
76 meterType: stationTemplate.meterType,
77 }),
78 };
79 return crypto
80 .createHash(Constants.DEFAULT_HASH_ALGORITHM)
81 .update(
82 JSON.stringify(hashBootNotificationRequest) +
83 ChargingStationUtils.getChargingStationId(index, stationTemplate)
84 )
85 .digest('hex');
86 }
87
88 public static getTemplateMaxNumberOfConnectors(stationTemplate: ChargingStationTemplate): number {
89 const templateConnectors = stationTemplate?.Connectors;
90 if (!templateConnectors) {
91 return -1;
92 }
93 return Object.keys(templateConnectors).length;
94 }
95
96 public static checkTemplateMaxConnectors(
97 templateMaxConnectors: number,
98 templateFile: string,
99 logPrefix: string
100 ): void {
101 if (templateMaxConnectors === 0) {
102 logger.warn(
103 `${logPrefix} Charging station information from template ${templateFile} with empty connectors configuration`
104 );
105 } else if (templateMaxConnectors < 0) {
106 logger.error(
107 `${logPrefix} Charging station information from template ${templateFile} with no connectors configuration defined`
108 );
109 }
110 }
111
112 public static getConfiguredNumberOfConnectors(
113 index: number,
114 stationTemplate: ChargingStationTemplate
115 ): number {
116 let configuredMaxConnectors: number;
117 if (!Utils.isEmptyArray(stationTemplate.numberOfConnectors)) {
118 const numberOfConnectors = stationTemplate.numberOfConnectors as number[];
119 // Distribute evenly the number of connectors
120 configuredMaxConnectors = numberOfConnectors[(index - 1) % numberOfConnectors.length];
121 } else if (!Utils.isUndefined(stationTemplate.numberOfConnectors)) {
122 configuredMaxConnectors = stationTemplate.numberOfConnectors as number;
123 } else {
124 configuredMaxConnectors = stationTemplate?.Connectors[0]
125 ? ChargingStationUtils.getTemplateMaxNumberOfConnectors(stationTemplate) - 1
126 : ChargingStationUtils.getTemplateMaxNumberOfConnectors(stationTemplate);
127 }
128 return configuredMaxConnectors;
129 }
130
131 public static checkConfiguredMaxConnectors(
132 configuredMaxConnectors: number,
133 templateFile: string,
134 logPrefix: string
135 ): void {
136 if (configuredMaxConnectors <= 0) {
137 logger.warn(
138 `${logPrefix} Charging station information from template ${templateFile} with ${configuredMaxConnectors} connectors`
139 );
140 }
141 }
142
143 public static createBootNotificationRequest(
144 stationInfo: ChargingStationInfo
145 ): BootNotificationRequest {
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 };
167 }
168
169 public static workerPoolInUse(): boolean {
170 return [WorkerProcessType.DYNAMIC_POOL, WorkerProcessType.STATIC_POOL].includes(
171 Configuration.getWorker().processType
172 );
173 }
174
175 public static workerDynamicPoolInUse(): boolean {
176 return Configuration.getWorker().processType === WorkerProcessType.DYNAMIC_POOL;
177 }
178
179 /**
180 * Convert websocket error code to human readable string message
181 *
182 * @param code websocket error code
183 * @returns human readable string message
184 */
185 public static getWebSocketCloseEventStatusString(code: number): string {
186 if (code >= 0 && code <= 999) {
187 return '(Unused)';
188 } else if (code >= 1016) {
189 if (code <= 1999) {
190 return '(For WebSocket standard)';
191 } else if (code <= 2999) {
192 return '(For WebSocket extensions)';
193 } else if (code <= 3999) {
194 return '(For libraries and frameworks)';
195 } else if (code <= 4999) {
196 return '(For applications)';
197 }
198 }
199 if (!Utils.isUndefined(WebSocketCloseEventStatusString[code])) {
200 return WebSocketCloseEventStatusString[code] as string;
201 }
202 return '(Unknown)';
203 }
204
205 public static warnDeprecatedTemplateKey(
206 template: ChargingStationTemplate,
207 key: string,
208 templateFile: string,
209 logPrefix: string,
210 logMsgToAppend = ''
211 ): void {
212 if (!Utils.isUndefined(template[key])) {
213 logger.warn(
214 `${logPrefix} Deprecated template key '${key}' usage in file '${templateFile}'${
215 logMsgToAppend && '. ' + logMsgToAppend
216 }`
217 );
218 }
219 }
220
221 public static convertDeprecatedTemplateKey(
222 template: ChargingStationTemplate,
223 deprecatedKey: string,
224 key: string
225 ): void {
226 if (!Utils.isUndefined(template[deprecatedKey])) {
227 template[key] = template[deprecatedKey] as unknown;
228 delete template[deprecatedKey];
229 }
230 }
231
232 public static stationTemplateToStationInfo(
233 stationTemplate: ChargingStationTemplate
234 ): ChargingStationInfo {
235 stationTemplate = Utils.cloneObject(stationTemplate);
236 delete stationTemplate.power;
237 delete stationTemplate.powerUnit;
238 delete stationTemplate.Configuration;
239 delete stationTemplate.AutomaticTransactionGenerator;
240 delete stationTemplate.chargeBoxSerialNumberPrefix;
241 delete stationTemplate.chargePointSerialNumberPrefix;
242 delete stationTemplate.meterSerialNumberPrefix;
243 return stationTemplate as unknown as ChargingStationInfo;
244 }
245
246 public static createStationInfoHash(stationInfo: ChargingStationInfo): void {
247 delete stationInfo.infoHash;
248 stationInfo.infoHash = crypto
249 .createHash(Constants.DEFAULT_HASH_ALGORITHM)
250 .update(JSON.stringify(stationInfo))
251 .digest('hex');
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 const debugLogMsg = `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Matching charging profile found for power limitation: %j`;
336 for (const chargingProfile of chargingProfiles) {
337 // Set helpers
338 const currentMoment = moment();
339 const chargingSchedule = chargingProfile.chargingSchedule;
340 // Check type (recurring) and if it is already active
341 // Adjust the daily recurring schedule to today
342 if (
343 chargingProfile.chargingProfileKind === ChargingProfileKindType.RECURRING &&
344 chargingProfile.recurrencyKind === RecurrencyKindType.DAILY &&
345 currentMoment.isAfter(chargingSchedule.startSchedule)
346 ) {
347 const currentDate = new Date();
348 chargingSchedule.startSchedule = new Date(chargingSchedule.startSchedule);
349 chargingSchedule.startSchedule.setFullYear(
350 currentDate.getFullYear(),
351 currentDate.getMonth(),
352 currentDate.getDate()
353 );
354 // Check if the start of the schedule is yesterday
355 if (moment(chargingSchedule.startSchedule).isAfter(currentMoment)) {
356 chargingSchedule.startSchedule.setDate(currentDate.getDate() - 1);
357 }
358 } else if (moment(chargingSchedule.startSchedule).isAfter(currentMoment)) {
359 return null;
360 }
361 // Check if the charging profile is active
362 if (
363 moment(chargingSchedule.startSchedule)
364 .add(chargingSchedule.duration, 's')
365 .isAfter(currentMoment)
366 ) {
367 let lastButOneSchedule: ChargingSchedulePeriod;
368 // Search the right schedule period
369 for (const schedulePeriod of chargingSchedule.chargingSchedulePeriod) {
370 // Handling of only one period
371 if (
372 chargingSchedule.chargingSchedulePeriod.length === 1 &&
373 schedulePeriod.startPeriod === 0
374 ) {
375 const result = {
376 limit: schedulePeriod.limit,
377 matchingChargingProfile: chargingProfile,
378 };
379 logger.debug(debugLogMsg, result);
380 return result;
381 }
382 // Find the right schedule period
383 if (
384 moment(chargingSchedule.startSchedule)
385 .add(schedulePeriod.startPeriod, 's')
386 .isAfter(currentMoment)
387 ) {
388 // Found the schedule: last but one is the correct one
389 const result = {
390 limit: lastButOneSchedule.limit,
391 matchingChargingProfile: chargingProfile,
392 };
393 logger.debug(debugLogMsg, result);
394 return result;
395 }
396 // Keep it
397 lastButOneSchedule = schedulePeriod;
398 // Handle the last schedule period
399 if (
400 schedulePeriod.startPeriod ===
401 chargingSchedule.chargingSchedulePeriod[
402 chargingSchedule.chargingSchedulePeriod.length - 1
403 ].startPeriod
404 ) {
405 const result = {
406 limit: lastButOneSchedule.limit,
407 matchingChargingProfile: chargingProfile,
408 };
409 logger.debug(debugLogMsg, result);
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 = `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(`${logPrefix} ${errMsg}`);
434 throw new BaseError(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 = `Missing MeterValues for default measurand '${measurand}' in template on connectorId ${connectorId}`;
512 logger.error(`${chargingStation.logPrefix()} ${errorMsg}`);
513 throw new BaseError(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 getAuthorizationFile(stationInfo: ChargingStationInfo): string | undefined {
521 return (
522 stationInfo.authorizationFile &&
523 path.join(
524 path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../'),
525 'assets',
526 path.basename(stationInfo.authorizationFile)
527 )
528 );
529 }
530
531 public static isRequestCommandSupported(
532 command: RequestCommand,
533 chargingStation: ChargingStation
534 ): boolean {
535 const isRequestCommand = Object.values(RequestCommand).includes(command);
536 if (isRequestCommand && !chargingStation.stationInfo?.commandsSupport?.outgoingCommands) {
537 return true;
538 } else if (isRequestCommand && chargingStation.stationInfo?.commandsSupport?.outgoingCommands) {
539 return chargingStation.stationInfo?.commandsSupport?.outgoingCommands[command] ?? false;
540 }
541 logger.error(`${chargingStation.logPrefix()} Unknown outgoing OCPP command '${command}'`);
542 return false;
543 }
544
545 public static isIncomingRequestCommandSupported(
546 command: IncomingRequestCommand,
547 chargingStation: ChargingStation
548 ): boolean {
549 const isIncomingRequestCommand = Object.values(IncomingRequestCommand).includes(command);
550 if (
551 isIncomingRequestCommand &&
552 !chargingStation.stationInfo?.commandsSupport?.incomingCommands
553 ) {
554 return true;
555 } else if (
556 isIncomingRequestCommand &&
557 chargingStation.stationInfo?.commandsSupport?.incomingCommands
558 ) {
559 return chargingStation.stationInfo?.commandsSupport?.incomingCommands[command] ?? false;
560 }
561 logger.error(`${chargingStation.logPrefix()} Unknown incoming OCPP command '${command}'`);
562 return false;
563 }
564
565 private static getRandomSerialNumberSuffix(params?: {
566 randomBytesLength?: number;
567 upperCase?: boolean;
568 }): string {
569 const randomSerialNumberSuffix = crypto
570 .randomBytes(params?.randomBytesLength ?? 16)
571 .toString('hex');
572 if (params?.upperCase) {
573 return randomSerialNumberSuffix.toUpperCase();
574 }
575 return randomSerialNumberSuffix;
576 }
577 }