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