540ec4ffa36bfef0dccbdb45afc2a8ca1e6a0769
[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 { ChargingProfileKindType, RecurrencyKindType } from '../types/ocpp/1.6/ChargingProfile';
16 import type { OCPP16BootNotificationRequest } from '../types/ocpp/1.6/Requests';
17 import { BootReasonEnumType, OCPP20BootNotificationRequest } from '../types/ocpp/2.0/Requests';
18 import type { ChargingProfile, ChargingSchedulePeriod } from '../types/ocpp/ChargingProfile';
19 import { OCPPVersion } from '../types/ocpp/OCPPVersion';
20 import type { BootNotificationRequest } from '../types/ocpp/Requests';
21 import { WorkerProcessType } from '../types/Worker';
22 import Configuration from '../utils/Configuration';
23 import Constants from '../utils/Constants';
24 import logger from '../utils/Logger';
25 import Utils from '../utils/Utils';
26
27 const moduleName = 'ChargingStationUtils';
28
29 export class ChargingStationUtils {
30 private constructor() {
31 // This is intentional
32 }
33
34 public static getChargingStationId(
35 index: number,
36 stationTemplate: ChargingStationTemplate
37 ): string {
38 // In case of multiple instances: add instance index to charging station id
39 const instanceIndex = process.env.CF_INSTANCE_INDEX ?? 0;
40 const idSuffix = stationTemplate.nameSuffix ?? '';
41 const idStr = '000000000' + index.toString();
42 return stationTemplate?.fixedName
43 ? stationTemplate.baseName
44 : stationTemplate.baseName +
45 '-' +
46 instanceIndex.toString() +
47 idStr.substring(idStr.length - 4) +
48 idSuffix;
49 }
50
51 public static getHashId(index: number, stationTemplate: ChargingStationTemplate): string {
52 const hashBootNotificationRequest = {
53 chargePointModel: stationTemplate.chargePointModel,
54 chargePointVendor: stationTemplate.chargePointVendor,
55 ...(!Utils.isUndefined(stationTemplate.chargeBoxSerialNumberPrefix) && {
56 chargeBoxSerialNumber: stationTemplate.chargeBoxSerialNumberPrefix,
57 }),
58 ...(!Utils.isUndefined(stationTemplate.chargePointSerialNumberPrefix) && {
59 chargePointSerialNumber: stationTemplate.chargePointSerialNumberPrefix,
60 }),
61 // FIXME?: Should a firmware version change always reference a new configuration file?
62 ...(!Utils.isUndefined(stationTemplate.firmwareVersion) && {
63 firmwareVersion: stationTemplate.firmwareVersion,
64 }),
65 ...(!Utils.isUndefined(stationTemplate.iccid) && { iccid: stationTemplate.iccid }),
66 ...(!Utils.isUndefined(stationTemplate.imsi) && { imsi: stationTemplate.imsi }),
67 ...(!Utils.isUndefined(stationTemplate.meterSerialNumberPrefix) && {
68 meterSerialNumber: stationTemplate.meterSerialNumberPrefix,
69 }),
70 ...(!Utils.isUndefined(stationTemplate.meterType) && {
71 meterType: stationTemplate.meterType,
72 }),
73 };
74 return crypto
75 .createHash(Constants.DEFAULT_HASH_ALGORITHM)
76 .update(
77 JSON.stringify(hashBootNotificationRequest) +
78 ChargingStationUtils.getChargingStationId(index, stationTemplate)
79 )
80 .digest('hex');
81 }
82
83 public static getTemplateMaxNumberOfConnectors(stationTemplate: ChargingStationTemplate): number {
84 const templateConnectors = stationTemplate?.Connectors;
85 if (!templateConnectors) {
86 return -1;
87 }
88 return Object.keys(templateConnectors).length;
89 }
90
91 public static checkTemplateMaxConnectors(
92 templateMaxConnectors: number,
93 templateFile: string,
94 logPrefix: string
95 ): void {
96 if (templateMaxConnectors === 0) {
97 logger.warn(
98 `${logPrefix} Charging station information from template ${templateFile} with empty connectors configuration`
99 );
100 } else if (templateMaxConnectors < 0) {
101 logger.error(
102 `${logPrefix} Charging station information from template ${templateFile} with no connectors configuration defined`
103 );
104 }
105 }
106
107 public static getConfiguredNumberOfConnectors(stationTemplate: ChargingStationTemplate): number {
108 let configuredMaxConnectors: number;
109 if (Utils.isEmptyArray(stationTemplate.numberOfConnectors) === false) {
110 const numberOfConnectors = stationTemplate.numberOfConnectors as number[];
111 configuredMaxConnectors =
112 numberOfConnectors[Math.floor(Utils.secureRandom() * numberOfConnectors.length)];
113 } else if (Utils.isUndefined(stationTemplate.numberOfConnectors) === false) {
114 configuredMaxConnectors = stationTemplate.numberOfConnectors as number;
115 } else {
116 configuredMaxConnectors = stationTemplate?.Connectors[0]
117 ? ChargingStationUtils.getTemplateMaxNumberOfConnectors(stationTemplate) - 1
118 : ChargingStationUtils.getTemplateMaxNumberOfConnectors(stationTemplate);
119 }
120 return configuredMaxConnectors;
121 }
122
123 public static checkConfiguredMaxConnectors(
124 configuredMaxConnectors: number,
125 templateFile: string,
126 logPrefix: string
127 ): void {
128 if (configuredMaxConnectors <= 0) {
129 logger.warn(
130 `${logPrefix} Charging station information from template ${templateFile} with ${configuredMaxConnectors} connectors`
131 );
132 }
133 }
134
135 public static createBootNotificationRequest(
136 stationInfo: ChargingStationInfo
137 ): BootNotificationRequest {
138 const ocppVersion = stationInfo.ocppVersion ?? OCPPVersion.VERSION_16;
139 switch (ocppVersion) {
140 case OCPPVersion.VERSION_16:
141 return {
142 chargePointModel: stationInfo.chargePointModel,
143 chargePointVendor: stationInfo.chargePointVendor,
144 ...(!Utils.isUndefined(stationInfo.chargeBoxSerialNumber) && {
145 chargeBoxSerialNumber: stationInfo.chargeBoxSerialNumber,
146 }),
147 ...(!Utils.isUndefined(stationInfo.chargePointSerialNumber) && {
148 chargePointSerialNumber: stationInfo.chargePointSerialNumber,
149 }),
150 ...(!Utils.isUndefined(stationInfo.firmwareVersion) && {
151 firmwareVersion: stationInfo.firmwareVersion,
152 }),
153 ...(!Utils.isUndefined(stationInfo.iccid) && { iccid: stationInfo.iccid }),
154 ...(!Utils.isUndefined(stationInfo.imsi) && { imsi: stationInfo.imsi }),
155 ...(!Utils.isUndefined(stationInfo.meterSerialNumber) && {
156 meterSerialNumber: stationInfo.meterSerialNumber,
157 }),
158 ...(!Utils.isUndefined(stationInfo.meterType) && {
159 meterType: stationInfo.meterType,
160 }),
161 } as OCPP16BootNotificationRequest;
162 case OCPPVersion.VERSION_20:
163 case OCPPVersion.VERSION_201:
164 return {
165 reason: BootReasonEnumType.PowerUp,
166 chargingStation: {
167 model: stationInfo.chargePointModel,
168 vendorName: stationInfo.chargePointVendor,
169 ...(!Utils.isUndefined(stationInfo.firmwareVersion) && {
170 firmwareVersion: stationInfo.firmwareVersion,
171 }),
172 ...(!Utils.isUndefined(stationInfo.chargeBoxSerialNumber) && {
173 serialNumber: stationInfo.chargeBoxSerialNumber,
174 }),
175 modem: {
176 ...(!Utils.isUndefined(stationInfo.iccid) && { iccid: stationInfo.iccid }),
177 ...(!Utils.isUndefined(stationInfo.imsi) && { imsi: stationInfo.imsi }),
178 },
179 },
180 } as OCPP20BootNotificationRequest;
181 }
182 }
183
184 public static workerPoolInUse(): boolean {
185 return [WorkerProcessType.DYNAMIC_POOL, WorkerProcessType.STATIC_POOL].includes(
186 Configuration.getWorker().processType
187 );
188 }
189
190 public static workerDynamicPoolInUse(): boolean {
191 return Configuration.getWorker().processType === WorkerProcessType.DYNAMIC_POOL;
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 as unknown as ChargingStationInfo;
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 chargingProfiles -
314 * @param logPrefix -
315 * @returns
316 */
317 public static getLimitFromChargingProfiles(
318 chargingProfiles: ChargingProfile[],
319 logPrefix: string
320 ): {
321 limit: number;
322 matchingChargingProfile: ChargingProfile;
323 } | null {
324 const debugLogMsg = `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Matching charging profile found for power limitation: %j`;
325 for (const chargingProfile of chargingProfiles) {
326 // Set helpers
327 const currentMoment = moment();
328 const chargingSchedule = chargingProfile.chargingSchedule;
329 // Check type (recurring) and if it is already active
330 // Adjust the daily recurring schedule to today
331 if (
332 chargingProfile.chargingProfileKind === ChargingProfileKindType.RECURRING &&
333 chargingProfile.recurrencyKind === RecurrencyKindType.DAILY &&
334 currentMoment.isAfter(chargingSchedule.startSchedule)
335 ) {
336 const currentDate = new Date();
337 chargingSchedule.startSchedule = new Date(chargingSchedule.startSchedule);
338 chargingSchedule.startSchedule.setFullYear(
339 currentDate.getFullYear(),
340 currentDate.getMonth(),
341 currentDate.getDate()
342 );
343 // Check if the start of the schedule is yesterday
344 if (moment(chargingSchedule.startSchedule).isAfter(currentMoment)) {
345 chargingSchedule.startSchedule.setDate(currentDate.getDate() - 1);
346 }
347 } else if (moment(chargingSchedule.startSchedule).isAfter(currentMoment)) {
348 return null;
349 }
350 // Check if the charging profile is active
351 if (
352 moment(chargingSchedule.startSchedule)
353 .add(chargingSchedule.duration, 's')
354 .isAfter(currentMoment)
355 ) {
356 let lastButOneSchedule: ChargingSchedulePeriod;
357 // Search the right schedule period
358 for (const schedulePeriod of chargingSchedule.chargingSchedulePeriod) {
359 // Handling of only one period
360 if (
361 chargingSchedule.chargingSchedulePeriod.length === 1 &&
362 schedulePeriod.startPeriod === 0
363 ) {
364 const result = {
365 limit: schedulePeriod.limit,
366 matchingChargingProfile: chargingProfile,
367 };
368 logger.debug(debugLogMsg, result);
369 return result;
370 }
371 // Find the right schedule period
372 if (
373 moment(chargingSchedule.startSchedule)
374 .add(schedulePeriod.startPeriod, 's')
375 .isAfter(currentMoment)
376 ) {
377 // Found the schedule: last but one is the correct one
378 const result = {
379 limit: lastButOneSchedule.limit,
380 matchingChargingProfile: chargingProfile,
381 };
382 logger.debug(debugLogMsg, result);
383 return result;
384 }
385 // Keep it
386 lastButOneSchedule = schedulePeriod;
387 // Handle the last schedule period
388 if (
389 schedulePeriod.startPeriod ===
390 chargingSchedule.chargingSchedulePeriod[
391 chargingSchedule.chargingSchedulePeriod.length - 1
392 ].startPeriod
393 ) {
394 const result = {
395 limit: lastButOneSchedule.limit,
396 matchingChargingProfile: chargingProfile,
397 };
398 logger.debug(debugLogMsg, result);
399 return result;
400 }
401 }
402 }
403 }
404 return null;
405 }
406
407 public static getDefaultVoltageOut(
408 currentType: CurrentType,
409 templateFile: string,
410 logPrefix: string
411 ): Voltage {
412 const errMsg = `Unknown ${currentType} currentOutType in template file ${templateFile}, cannot define default voltage out`;
413 let defaultVoltageOut: number;
414 switch (currentType) {
415 case CurrentType.AC:
416 defaultVoltageOut = Voltage.VOLTAGE_230;
417 break;
418 case CurrentType.DC:
419 defaultVoltageOut = Voltage.VOLTAGE_400;
420 break;
421 default:
422 logger.error(`${logPrefix} ${errMsg}`);
423 throw new BaseError(errMsg);
424 }
425 return defaultVoltageOut;
426 }
427
428 public static getAuthorizationFile(stationInfo: ChargingStationInfo): string | undefined {
429 return (
430 stationInfo.authorizationFile &&
431 path.join(
432 path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../'),
433 'assets',
434 path.basename(stationInfo.authorizationFile)
435 )
436 );
437 }
438
439 private static getRandomSerialNumberSuffix(params?: {
440 randomBytesLength?: number;
441 upperCase?: boolean;
442 }): string {
443 const randomSerialNumberSuffix = crypto
444 .randomBytes(params?.randomBytesLength ?? 16)
445 .toString('hex');
446 if (params?.upperCase) {
447 return randomSerialNumberSuffix.toUpperCase();
448 }
449 return randomSerialNumberSuffix;
450 }
451 }