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