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