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