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