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