ATG: add support for idTag distribution algorithms
[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 {
10 AmpereUnits,
11 type ChargingStationTemplate,
12 CurrentType,
13 Voltage,
14 } from '../types/ChargingStationTemplate';
15 import type { SampledValueTemplate } from '../types/MeasurandPerPhaseSampledValueTemplates';
16 import { ChargingProfileKindType, RecurrencyKindType } from '../types/ocpp/1.6/ChargingProfile';
17 import type { ChargingProfile, ChargingSchedulePeriod } from '../types/ocpp/ChargingProfile';
18 import { StandardParametersKey } from '../types/ocpp/Configuration';
19 import { MeterValueMeasurand, MeterValuePhase } from '../types/ocpp/MeterValues';
20 import {
21 type BootNotificationRequest,
22 IncomingRequestCommand,
23 RequestCommand,
24 } from '../types/ocpp/Requests';
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(stationTemplate: ChargingStationTemplate): number {
113 let configuredMaxConnectors: number;
114 if (Utils.isEmptyArray(stationTemplate.numberOfConnectors) === false) {
115 const numberOfConnectors = stationTemplate.numberOfConnectors as number[];
116 configuredMaxConnectors =
117 numberOfConnectors[Math.floor(Utils.secureRandom() * numberOfConnectors.length)];
118 } else if (Utils.isUndefined(stationTemplate.numberOfConnectors) === false) {
119 configuredMaxConnectors = stationTemplate.numberOfConnectors as number;
120 } else {
121 configuredMaxConnectors = stationTemplate?.Connectors[0]
122 ? ChargingStationUtils.getTemplateMaxNumberOfConnectors(stationTemplate) - 1
123 : ChargingStationUtils.getTemplateMaxNumberOfConnectors(stationTemplate);
124 }
125 return configuredMaxConnectors;
126 }
127
128 public static checkConfiguredMaxConnectors(
129 configuredMaxConnectors: number,
130 templateFile: string,
131 logPrefix: string
132 ): void {
133 if (configuredMaxConnectors <= 0) {
134 logger.warn(
135 `${logPrefix} Charging station information from template ${templateFile} with ${configuredMaxConnectors} connectors`
136 );
137 }
138 }
139
140 public static createBootNotificationRequest(
141 stationInfo: ChargingStationInfo
142 ): BootNotificationRequest {
143 return {
144 chargePointModel: stationInfo.chargePointModel,
145 chargePointVendor: stationInfo.chargePointVendor,
146 ...(!Utils.isUndefined(stationInfo.chargeBoxSerialNumber) && {
147 chargeBoxSerialNumber: stationInfo.chargeBoxSerialNumber,
148 }),
149 ...(!Utils.isUndefined(stationInfo.chargePointSerialNumber) && {
150 chargePointSerialNumber: stationInfo.chargePointSerialNumber,
151 }),
152 ...(!Utils.isUndefined(stationInfo.firmwareVersion) && {
153 firmwareVersion: stationInfo.firmwareVersion,
154 }),
155 ...(!Utils.isUndefined(stationInfo.iccid) && { iccid: stationInfo.iccid }),
156 ...(!Utils.isUndefined(stationInfo.imsi) && { imsi: stationInfo.imsi }),
157 ...(!Utils.isUndefined(stationInfo.meterSerialNumber) && {
158 meterSerialNumber: stationInfo.meterSerialNumber,
159 }),
160 ...(!Utils.isUndefined(stationInfo.meterType) && {
161 meterType: stationInfo.meterType,
162 }),
163 };
164 }
165
166 public static workerPoolInUse(): boolean {
167 return [WorkerProcessType.DYNAMIC_POOL, WorkerProcessType.STATIC_POOL].includes(
168 Configuration.getWorker().processType
169 );
170 }
171
172 public static workerDynamicPoolInUse(): boolean {
173 return Configuration.getWorker().processType === WorkerProcessType.DYNAMIC_POOL;
174 }
175
176 public static warnDeprecatedTemplateKey(
177 template: ChargingStationTemplate,
178 key: string,
179 templateFile: string,
180 logPrefix: string,
181 logMsgToAppend = ''
182 ): void {
183 if (!Utils.isUndefined(template[key])) {
184 logger.warn(
185 `${logPrefix} Deprecated template key '${key}' usage in file '${templateFile}'${
186 logMsgToAppend && '. ' + logMsgToAppend
187 }`
188 );
189 }
190 }
191
192 public static convertDeprecatedTemplateKey(
193 template: ChargingStationTemplate,
194 deprecatedKey: string,
195 key: string
196 ): void {
197 if (!Utils.isUndefined(template[deprecatedKey])) {
198 template[key] = template[deprecatedKey] as unknown;
199 delete template[deprecatedKey];
200 }
201 }
202
203 public static stationTemplateToStationInfo(
204 stationTemplate: ChargingStationTemplate
205 ): ChargingStationInfo {
206 stationTemplate = Utils.cloneObject(stationTemplate);
207 delete stationTemplate.power;
208 delete stationTemplate.powerUnit;
209 delete stationTemplate.Configuration;
210 delete stationTemplate.AutomaticTransactionGenerator;
211 delete stationTemplate.chargeBoxSerialNumberPrefix;
212 delete stationTemplate.chargePointSerialNumberPrefix;
213 delete stationTemplate.meterSerialNumberPrefix;
214 return stationTemplate as unknown as ChargingStationInfo;
215 }
216
217 public static createStationInfoHash(stationInfo: ChargingStationInfo): void {
218 delete stationInfo.infoHash;
219 stationInfo.infoHash = crypto
220 .createHash(Constants.DEFAULT_HASH_ALGORITHM)
221 .update(JSON.stringify(stationInfo))
222 .digest('hex');
223 }
224
225 public static createSerialNumber(
226 stationTemplate: ChargingStationTemplate,
227 stationInfo: ChargingStationInfo = {} as ChargingStationInfo,
228 params: {
229 randomSerialNumberUpperCase?: boolean;
230 randomSerialNumber?: boolean;
231 } = {
232 randomSerialNumberUpperCase: true,
233 randomSerialNumber: true,
234 }
235 ): void {
236 params = params ?? {};
237 params.randomSerialNumberUpperCase = params?.randomSerialNumberUpperCase ?? true;
238 params.randomSerialNumber = params?.randomSerialNumber ?? true;
239 const serialNumberSuffix = params?.randomSerialNumber
240 ? ChargingStationUtils.getRandomSerialNumberSuffix({
241 upperCase: params.randomSerialNumberUpperCase,
242 })
243 : '';
244 stationInfo.chargePointSerialNumber =
245 stationTemplate?.chargePointSerialNumberPrefix &&
246 stationTemplate.chargePointSerialNumberPrefix + serialNumberSuffix;
247 stationInfo.chargeBoxSerialNumber =
248 stationTemplate?.chargeBoxSerialNumberPrefix &&
249 stationTemplate.chargeBoxSerialNumberPrefix + serialNumberSuffix;
250 stationInfo.meterSerialNumber =
251 stationTemplate?.meterSerialNumberPrefix &&
252 stationTemplate.meterSerialNumberPrefix + serialNumberSuffix;
253 }
254
255 public static propagateSerialNumber(
256 stationTemplate: ChargingStationTemplate,
257 stationInfoSrc: ChargingStationInfo,
258 stationInfoDst: ChargingStationInfo = {} as ChargingStationInfo
259 ) {
260 if (!stationInfoSrc || !stationTemplate) {
261 throw new BaseError(
262 'Missing charging station template or existing configuration to propagate serial number'
263 );
264 }
265 stationTemplate?.chargePointSerialNumberPrefix && stationInfoSrc?.chargePointSerialNumber
266 ? (stationInfoDst.chargePointSerialNumber = stationInfoSrc.chargePointSerialNumber)
267 : stationInfoDst?.chargePointSerialNumber && delete stationInfoDst.chargePointSerialNumber;
268 stationTemplate?.chargeBoxSerialNumberPrefix && stationInfoSrc?.chargeBoxSerialNumber
269 ? (stationInfoDst.chargeBoxSerialNumber = stationInfoSrc.chargeBoxSerialNumber)
270 : stationInfoDst?.chargeBoxSerialNumber && delete stationInfoDst.chargeBoxSerialNumber;
271 stationTemplate?.meterSerialNumberPrefix && stationInfoSrc?.meterSerialNumber
272 ? (stationInfoDst.meterSerialNumber = stationInfoSrc.meterSerialNumber)
273 : stationInfoDst?.meterSerialNumber && delete stationInfoDst.meterSerialNumber;
274 }
275
276 public static getAmperageLimitationUnitDivider(stationInfo: ChargingStationInfo): number {
277 let unitDivider = 1;
278 switch (stationInfo.amperageLimitationUnit) {
279 case AmpereUnits.DECI_AMPERE:
280 unitDivider = 10;
281 break;
282 case AmpereUnits.CENTI_AMPERE:
283 unitDivider = 100;
284 break;
285 case AmpereUnits.MILLI_AMPERE:
286 unitDivider = 1000;
287 break;
288 }
289 return unitDivider;
290 }
291
292 /**
293 * Charging profiles should already be sorted by connectorId and stack level (highest stack level has priority)
294 *
295 * @param {ChargingProfile[]} chargingProfiles
296 * @param {string} logPrefix
297 * @returns {{ limit, matchingChargingProfile }}
298 */
299 public static getLimitFromChargingProfiles(
300 chargingProfiles: ChargingProfile[],
301 logPrefix: string
302 ): {
303 limit: number;
304 matchingChargingProfile: ChargingProfile;
305 } | null {
306 const debugLogMsg = `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Matching charging profile found for power limitation: %j`;
307 for (const chargingProfile of chargingProfiles) {
308 // Set helpers
309 const currentMoment = moment();
310 const chargingSchedule = chargingProfile.chargingSchedule;
311 // Check type (recurring) and if it is already active
312 // Adjust the daily recurring schedule to today
313 if (
314 chargingProfile.chargingProfileKind === ChargingProfileKindType.RECURRING &&
315 chargingProfile.recurrencyKind === RecurrencyKindType.DAILY &&
316 currentMoment.isAfter(chargingSchedule.startSchedule)
317 ) {
318 const currentDate = new Date();
319 chargingSchedule.startSchedule = new Date(chargingSchedule.startSchedule);
320 chargingSchedule.startSchedule.setFullYear(
321 currentDate.getFullYear(),
322 currentDate.getMonth(),
323 currentDate.getDate()
324 );
325 // Check if the start of the schedule is yesterday
326 if (moment(chargingSchedule.startSchedule).isAfter(currentMoment)) {
327 chargingSchedule.startSchedule.setDate(currentDate.getDate() - 1);
328 }
329 } else if (moment(chargingSchedule.startSchedule).isAfter(currentMoment)) {
330 return null;
331 }
332 // Check if the charging profile is active
333 if (
334 moment(chargingSchedule.startSchedule)
335 .add(chargingSchedule.duration, 's')
336 .isAfter(currentMoment)
337 ) {
338 let lastButOneSchedule: ChargingSchedulePeriod;
339 // Search the right schedule period
340 for (const schedulePeriod of chargingSchedule.chargingSchedulePeriod) {
341 // Handling of only one period
342 if (
343 chargingSchedule.chargingSchedulePeriod.length === 1 &&
344 schedulePeriod.startPeriod === 0
345 ) {
346 const result = {
347 limit: schedulePeriod.limit,
348 matchingChargingProfile: chargingProfile,
349 };
350 logger.debug(debugLogMsg, result);
351 return result;
352 }
353 // Find the right schedule period
354 if (
355 moment(chargingSchedule.startSchedule)
356 .add(schedulePeriod.startPeriod, 's')
357 .isAfter(currentMoment)
358 ) {
359 // Found the schedule: last but one is the correct one
360 const result = {
361 limit: lastButOneSchedule.limit,
362 matchingChargingProfile: chargingProfile,
363 };
364 logger.debug(debugLogMsg, result);
365 return result;
366 }
367 // Keep it
368 lastButOneSchedule = schedulePeriod;
369 // Handle the last schedule period
370 if (
371 schedulePeriod.startPeriod ===
372 chargingSchedule.chargingSchedulePeriod[
373 chargingSchedule.chargingSchedulePeriod.length - 1
374 ].startPeriod
375 ) {
376 const result = {
377 limit: lastButOneSchedule.limit,
378 matchingChargingProfile: chargingProfile,
379 };
380 logger.debug(debugLogMsg, result);
381 return result;
382 }
383 }
384 }
385 }
386 return null;
387 }
388
389 public static getDefaultVoltageOut(
390 currentType: CurrentType,
391 templateFile: string,
392 logPrefix: string
393 ): Voltage {
394 const errMsg = `Unknown ${currentType} currentOutType in template file ${templateFile}, cannot define default voltage out`;
395 let defaultVoltageOut: number;
396 switch (currentType) {
397 case CurrentType.AC:
398 defaultVoltageOut = Voltage.VOLTAGE_230;
399 break;
400 case CurrentType.DC:
401 defaultVoltageOut = Voltage.VOLTAGE_400;
402 break;
403 default:
404 logger.error(`${logPrefix} ${errMsg}`);
405 throw new BaseError(errMsg);
406 }
407 return defaultVoltageOut;
408 }
409
410 public static getSampledValueTemplate(
411 chargingStation: ChargingStation,
412 connectorId: number,
413 measurand: MeterValueMeasurand = MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER,
414 phase?: MeterValuePhase
415 ): SampledValueTemplate | undefined {
416 const onPhaseStr = phase ? `on phase ${phase} ` : '';
417 if (Constants.SUPPORTED_MEASURANDS.includes(measurand) === false) {
418 logger.warn(
419 `${chargingStation.logPrefix()} Trying to get unsupported MeterValues measurand '${measurand}' ${onPhaseStr}in template on connectorId ${connectorId}`
420 );
421 return;
422 }
423 if (
424 measurand !== MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER &&
425 !ChargingStationConfigurationUtils.getConfigurationKey(
426 chargingStation,
427 StandardParametersKey.MeterValuesSampledData
428 )?.value.includes(measurand)
429 ) {
430 logger.debug(
431 `${chargingStation.logPrefix()} Trying to get MeterValues measurand '${measurand}' ${onPhaseStr}in template on connectorId ${connectorId} not found in '${
432 StandardParametersKey.MeterValuesSampledData
433 }' OCPP parameter`
434 );
435 return;
436 }
437 const sampledValueTemplates: SampledValueTemplate[] =
438 chargingStation.getConnectorStatus(connectorId).MeterValues;
439 for (
440 let index = 0;
441 !Utils.isEmptyArray(sampledValueTemplates) && index < sampledValueTemplates.length;
442 index++
443 ) {
444 if (
445 Constants.SUPPORTED_MEASURANDS.includes(
446 sampledValueTemplates[index]?.measurand ??
447 MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
448 ) === false
449 ) {
450 logger.warn(
451 `${chargingStation.logPrefix()} Unsupported MeterValues measurand '${measurand}' ${onPhaseStr}in template on connectorId ${connectorId}`
452 );
453 } else if (
454 phase &&
455 sampledValueTemplates[index]?.phase === phase &&
456 sampledValueTemplates[index]?.measurand === measurand &&
457 ChargingStationConfigurationUtils.getConfigurationKey(
458 chargingStation,
459 StandardParametersKey.MeterValuesSampledData
460 )?.value.includes(measurand) === true
461 ) {
462 return sampledValueTemplates[index];
463 } else if (
464 !phase &&
465 !sampledValueTemplates[index].phase &&
466 sampledValueTemplates[index]?.measurand === measurand &&
467 ChargingStationConfigurationUtils.getConfigurationKey(
468 chargingStation,
469 StandardParametersKey.MeterValuesSampledData
470 )?.value.includes(measurand) === true
471 ) {
472 return sampledValueTemplates[index];
473 } else if (
474 measurand === MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER &&
475 (!sampledValueTemplates[index].measurand ||
476 sampledValueTemplates[index].measurand === measurand)
477 ) {
478 return sampledValueTemplates[index];
479 }
480 }
481 if (measurand === MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER) {
482 const errorMsg = `Missing MeterValues for default measurand '${measurand}' in template on connectorId ${connectorId}`;
483 logger.error(`${chargingStation.logPrefix()} ${errorMsg}`);
484 throw new BaseError(errorMsg);
485 }
486 logger.debug(
487 `${chargingStation.logPrefix()} No MeterValues for measurand '${measurand}' ${onPhaseStr}in template on connectorId ${connectorId}`
488 );
489 }
490
491 public static getAuthorizationFile(stationInfo: ChargingStationInfo): string | undefined {
492 return (
493 stationInfo.authorizationFile &&
494 path.join(
495 path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../'),
496 'assets',
497 path.basename(stationInfo.authorizationFile)
498 )
499 );
500 }
501
502 public static isRequestCommandSupported(
503 command: RequestCommand,
504 chargingStation: ChargingStation
505 ): boolean {
506 const isRequestCommand = Object.values(RequestCommand).includes(command);
507 if (
508 isRequestCommand === true &&
509 !chargingStation.stationInfo?.commandsSupport?.outgoingCommands
510 ) {
511 return true;
512 } else if (
513 isRequestCommand === true &&
514 chargingStation.stationInfo?.commandsSupport?.outgoingCommands
515 ) {
516 return chargingStation.stationInfo?.commandsSupport?.outgoingCommands[command] ?? false;
517 }
518 logger.error(`${chargingStation.logPrefix()} Unknown outgoing OCPP command '${command}'`);
519 return false;
520 }
521
522 public static isIncomingRequestCommandSupported(
523 command: IncomingRequestCommand,
524 chargingStation: ChargingStation
525 ): boolean {
526 const isIncomingRequestCommand = Object.values(IncomingRequestCommand).includes(command);
527 if (
528 isIncomingRequestCommand === true &&
529 !chargingStation.stationInfo?.commandsSupport?.incomingCommands
530 ) {
531 return true;
532 } else if (
533 isIncomingRequestCommand === true &&
534 chargingStation.stationInfo?.commandsSupport?.incomingCommands
535 ) {
536 return chargingStation.stationInfo?.commandsSupport?.incomingCommands[command] ?? false;
537 }
538 logger.error(`${chargingStation.logPrefix()} Unknown incoming OCPP command '${command}'`);
539 return false;
540 }
541
542 private static getRandomSerialNumberSuffix(params?: {
543 randomBytesLength?: number;
544 upperCase?: boolean;
545 }): string {
546 const randomSerialNumberSuffix = crypto
547 .randomBytes(params?.randomBytesLength ?? 16)
548 .toString('hex');
549 if (params?.upperCase) {
550 return randomSerialNumberSuffix.toUpperCase();
551 }
552 return randomSerialNumberSuffix;
553 }
554 }