Optimize worker handlers calls by binding them to the current instance
[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 { ChargingProfile, ChargingSchedulePeriod } from '../types/ocpp/ChargingProfile';
17 import type { BootNotificationRequest } from '../types/ocpp/Requests';
18 import { WorkerProcessType } from '../types/Worker';
19 import Configuration from '../utils/Configuration';
20 import Constants from '../utils/Constants';
21 import logger from '../utils/Logger';
22 import Utils from '../utils/Utils';
23
24 const moduleName = 'ChargingStationUtils';
25
26 export class ChargingStationUtils {
27 private constructor() {
28 // This is intentional
29 }
30
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();
39 return stationTemplate?.fixedName
40 ? stationTemplate.baseName
41 : stationTemplate.baseName +
42 '-' +
43 instanceIndex.toString() +
44 idStr.substring(idStr.length - 4) +
45 idSuffix;
46 }
47
48 public static getHashId(index: number, stationTemplate: ChargingStationTemplate): string {
49 const hashBootNotificationRequest = {
50 chargePointModel: stationTemplate.chargePointModel,
51 chargePointVendor: stationTemplate.chargePointVendor,
52 ...(!Utils.isUndefined(stationTemplate.chargeBoxSerialNumberPrefix) && {
53 chargeBoxSerialNumber: stationTemplate.chargeBoxSerialNumberPrefix,
54 }),
55 ...(!Utils.isUndefined(stationTemplate.chargePointSerialNumberPrefix) && {
56 chargePointSerialNumber: stationTemplate.chargePointSerialNumberPrefix,
57 }),
58 // FIXME?: Should a firmware version change always reference a new configuration file?
59 ...(!Utils.isUndefined(stationTemplate.firmwareVersion) && {
60 firmwareVersion: stationTemplate.firmwareVersion,
61 }),
62 ...(!Utils.isUndefined(stationTemplate.iccid) && { iccid: stationTemplate.iccid }),
63 ...(!Utils.isUndefined(stationTemplate.imsi) && { imsi: stationTemplate.imsi }),
64 ...(!Utils.isUndefined(stationTemplate.meterSerialNumberPrefix) && {
65 meterSerialNumber: stationTemplate.meterSerialNumberPrefix,
66 }),
67 ...(!Utils.isUndefined(stationTemplate.meterType) && {
68 meterType: stationTemplate.meterType,
69 }),
70 };
71 return crypto
72 .createHash(Constants.DEFAULT_HASH_ALGORITHM)
73 .update(
74 JSON.stringify(hashBootNotificationRequest) +
75 ChargingStationUtils.getChargingStationId(index, stationTemplate)
76 )
77 .digest('hex');
78 }
79
80 public static getTemplateMaxNumberOfConnectors(stationTemplate: ChargingStationTemplate): number {
81 const templateConnectors = stationTemplate?.Connectors;
82 if (!templateConnectors) {
83 return -1;
84 }
85 return Object.keys(templateConnectors).length;
86 }
87
88 public static checkTemplateMaxConnectors(
89 templateMaxConnectors: number,
90 templateFile: string,
91 logPrefix: string
92 ): void {
93 if (templateMaxConnectors === 0) {
94 logger.warn(
95 `${logPrefix} Charging station information from template ${templateFile} with empty connectors configuration`
96 );
97 } else if (templateMaxConnectors < 0) {
98 logger.error(
99 `${logPrefix} Charging station information from template ${templateFile} with no connectors configuration defined`
100 );
101 }
102 }
103
104 public static getConfiguredNumberOfConnectors(stationTemplate: ChargingStationTemplate): number {
105 let configuredMaxConnectors: number;
106 if (Utils.isEmptyArray(stationTemplate.numberOfConnectors) === false) {
107 const numberOfConnectors = stationTemplate.numberOfConnectors as number[];
108 configuredMaxConnectors =
109 numberOfConnectors[Math.floor(Utils.secureRandom() * numberOfConnectors.length)];
110 } else if (Utils.isUndefined(stationTemplate.numberOfConnectors) === false) {
111 configuredMaxConnectors = stationTemplate.numberOfConnectors as number;
112 } else {
113 configuredMaxConnectors = stationTemplate?.Connectors[0]
114 ? ChargingStationUtils.getTemplateMaxNumberOfConnectors(stationTemplate) - 1
115 : ChargingStationUtils.getTemplateMaxNumberOfConnectors(stationTemplate);
116 }
117 return configuredMaxConnectors;
118 }
119
120 public static checkConfiguredMaxConnectors(
121 configuredMaxConnectors: number,
122 templateFile: string,
123 logPrefix: string
124 ): void {
125 if (configuredMaxConnectors <= 0) {
126 logger.warn(
127 `${logPrefix} Charging station information from template ${templateFile} with ${configuredMaxConnectors} connectors`
128 );
129 }
130 }
131
132 public static createBootNotificationRequest(
133 stationInfo: ChargingStationInfo
134 ): BootNotificationRequest {
135 return {
136 chargePointModel: stationInfo.chargePointModel,
137 chargePointVendor: stationInfo.chargePointVendor,
138 ...(!Utils.isUndefined(stationInfo.chargeBoxSerialNumber) && {
139 chargeBoxSerialNumber: stationInfo.chargeBoxSerialNumber,
140 }),
141 ...(!Utils.isUndefined(stationInfo.chargePointSerialNumber) && {
142 chargePointSerialNumber: stationInfo.chargePointSerialNumber,
143 }),
144 ...(!Utils.isUndefined(stationInfo.firmwareVersion) && {
145 firmwareVersion: stationInfo.firmwareVersion,
146 }),
147 ...(!Utils.isUndefined(stationInfo.iccid) && { iccid: stationInfo.iccid }),
148 ...(!Utils.isUndefined(stationInfo.imsi) && { imsi: stationInfo.imsi }),
149 ...(!Utils.isUndefined(stationInfo.meterSerialNumber) && {
150 meterSerialNumber: stationInfo.meterSerialNumber,
151 }),
152 ...(!Utils.isUndefined(stationInfo.meterType) && {
153 meterType: stationInfo.meterType,
154 }),
155 };
156 }
157
158 public static workerPoolInUse(): boolean {
159 return [WorkerProcessType.DYNAMIC_POOL, WorkerProcessType.STATIC_POOL].includes(
160 Configuration.getWorker().processType
161 );
162 }
163
164 public static workerDynamicPoolInUse(): boolean {
165 return Configuration.getWorker().processType === WorkerProcessType.DYNAMIC_POOL;
166 }
167
168 public static warnDeprecatedTemplateKey(
169 template: ChargingStationTemplate,
170 key: string,
171 templateFile: string,
172 logPrefix: string,
173 logMsgToAppend = ''
174 ): void {
175 if (!Utils.isUndefined(template[key])) {
176 logger.warn(
177 `${logPrefix} Deprecated template key '${key}' usage in file '${templateFile}'${
178 logMsgToAppend && '. ' + logMsgToAppend
179 }`
180 );
181 }
182 }
183
184 public static convertDeprecatedTemplateKey(
185 template: ChargingStationTemplate,
186 deprecatedKey: string,
187 key: string
188 ): void {
189 if (!Utils.isUndefined(template[deprecatedKey])) {
190 template[key] = template[deprecatedKey] as unknown;
191 delete template[deprecatedKey];
192 }
193 }
194
195 public static stationTemplateToStationInfo(
196 stationTemplate: ChargingStationTemplate
197 ): ChargingStationInfo {
198 stationTemplate = Utils.cloneObject(stationTemplate);
199 delete stationTemplate.power;
200 delete stationTemplate.powerUnit;
201 delete stationTemplate.Configuration;
202 delete stationTemplate.AutomaticTransactionGenerator;
203 delete stationTemplate.chargeBoxSerialNumberPrefix;
204 delete stationTemplate.chargePointSerialNumberPrefix;
205 delete stationTemplate.meterSerialNumberPrefix;
206 return stationTemplate as unknown as ChargingStationInfo;
207 }
208
209 public static createStationInfoHash(stationInfo: ChargingStationInfo): void {
210 delete stationInfo.infoHash;
211 stationInfo.infoHash = crypto
212 .createHash(Constants.DEFAULT_HASH_ALGORITHM)
213 .update(JSON.stringify(stationInfo))
214 .digest('hex');
215 }
216
217 public static createSerialNumber(
218 stationTemplate: ChargingStationTemplate,
219 stationInfo: ChargingStationInfo = {} as ChargingStationInfo,
220 params: {
221 randomSerialNumberUpperCase?: boolean;
222 randomSerialNumber?: boolean;
223 } = {
224 randomSerialNumberUpperCase: true,
225 randomSerialNumber: true,
226 }
227 ): void {
228 params = params ?? {};
229 params.randomSerialNumberUpperCase = params?.randomSerialNumberUpperCase ?? true;
230 params.randomSerialNumber = params?.randomSerialNumber ?? true;
231 const serialNumberSuffix = params?.randomSerialNumber
232 ? ChargingStationUtils.getRandomSerialNumberSuffix({
233 upperCase: params.randomSerialNumberUpperCase,
234 })
235 : '';
236 stationInfo.chargePointSerialNumber =
237 stationTemplate?.chargePointSerialNumberPrefix &&
238 stationTemplate.chargePointSerialNumberPrefix + serialNumberSuffix;
239 stationInfo.chargeBoxSerialNumber =
240 stationTemplate?.chargeBoxSerialNumberPrefix &&
241 stationTemplate.chargeBoxSerialNumberPrefix + serialNumberSuffix;
242 stationInfo.meterSerialNumber =
243 stationTemplate?.meterSerialNumberPrefix &&
244 stationTemplate.meterSerialNumberPrefix + serialNumberSuffix;
245 }
246
247 public static propagateSerialNumber(
248 stationTemplate: ChargingStationTemplate,
249 stationInfoSrc: ChargingStationInfo,
250 stationInfoDst: ChargingStationInfo = {} as ChargingStationInfo
251 ) {
252 if (!stationInfoSrc || !stationTemplate) {
253 throw new BaseError(
254 'Missing charging station template or existing configuration to propagate serial number'
255 );
256 }
257 stationTemplate?.chargePointSerialNumberPrefix && stationInfoSrc?.chargePointSerialNumber
258 ? (stationInfoDst.chargePointSerialNumber = stationInfoSrc.chargePointSerialNumber)
259 : stationInfoDst?.chargePointSerialNumber && delete stationInfoDst.chargePointSerialNumber;
260 stationTemplate?.chargeBoxSerialNumberPrefix && stationInfoSrc?.chargeBoxSerialNumber
261 ? (stationInfoDst.chargeBoxSerialNumber = stationInfoSrc.chargeBoxSerialNumber)
262 : stationInfoDst?.chargeBoxSerialNumber && delete stationInfoDst.chargeBoxSerialNumber;
263 stationTemplate?.meterSerialNumberPrefix && stationInfoSrc?.meterSerialNumber
264 ? (stationInfoDst.meterSerialNumber = stationInfoSrc.meterSerialNumber)
265 : stationInfoDst?.meterSerialNumber && delete stationInfoDst.meterSerialNumber;
266 }
267
268 public static getAmperageLimitationUnitDivider(stationInfo: ChargingStationInfo): number {
269 let unitDivider = 1;
270 switch (stationInfo.amperageLimitationUnit) {
271 case AmpereUnits.DECI_AMPERE:
272 unitDivider = 10;
273 break;
274 case AmpereUnits.CENTI_AMPERE:
275 unitDivider = 100;
276 break;
277 case AmpereUnits.MILLI_AMPERE:
278 unitDivider = 1000;
279 break;
280 }
281 return unitDivider;
282 }
283
284 /**
285 * Charging profiles should already be sorted by connectorId and stack level (highest stack level has priority)
286 *
287 * @param chargingProfiles -
288 * @param logPrefix -
289 * @returns
290 */
291 public static getLimitFromChargingProfiles(
292 chargingProfiles: ChargingProfile[],
293 logPrefix: string
294 ): {
295 limit: number;
296 matchingChargingProfile: ChargingProfile;
297 } | null {
298 const debugLogMsg = `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Matching charging profile found for power limitation: %j`;
299 for (const chargingProfile of chargingProfiles) {
300 // Set helpers
301 const currentMoment = moment();
302 const chargingSchedule = chargingProfile.chargingSchedule;
303 // Check type (recurring) and if it is already active
304 // Adjust the daily recurring schedule to today
305 if (
306 chargingProfile.chargingProfileKind === ChargingProfileKindType.RECURRING &&
307 chargingProfile.recurrencyKind === RecurrencyKindType.DAILY &&
308 currentMoment.isAfter(chargingSchedule.startSchedule)
309 ) {
310 const currentDate = new Date();
311 chargingSchedule.startSchedule = new Date(chargingSchedule.startSchedule);
312 chargingSchedule.startSchedule.setFullYear(
313 currentDate.getFullYear(),
314 currentDate.getMonth(),
315 currentDate.getDate()
316 );
317 // Check if the start of the schedule is yesterday
318 if (moment(chargingSchedule.startSchedule).isAfter(currentMoment)) {
319 chargingSchedule.startSchedule.setDate(currentDate.getDate() - 1);
320 }
321 } else if (moment(chargingSchedule.startSchedule).isAfter(currentMoment)) {
322 return null;
323 }
324 // Check if the charging profile is active
325 if (
326 moment(chargingSchedule.startSchedule)
327 .add(chargingSchedule.duration, 's')
328 .isAfter(currentMoment)
329 ) {
330 let lastButOneSchedule: ChargingSchedulePeriod;
331 // Search the right schedule period
332 for (const schedulePeriod of chargingSchedule.chargingSchedulePeriod) {
333 // Handling of only one period
334 if (
335 chargingSchedule.chargingSchedulePeriod.length === 1 &&
336 schedulePeriod.startPeriod === 0
337 ) {
338 const result = {
339 limit: schedulePeriod.limit,
340 matchingChargingProfile: chargingProfile,
341 };
342 logger.debug(debugLogMsg, result);
343 return result;
344 }
345 // Find the right schedule period
346 if (
347 moment(chargingSchedule.startSchedule)
348 .add(schedulePeriod.startPeriod, 's')
349 .isAfter(currentMoment)
350 ) {
351 // Found the schedule: last but one is the correct one
352 const result = {
353 limit: lastButOneSchedule.limit,
354 matchingChargingProfile: chargingProfile,
355 };
356 logger.debug(debugLogMsg, result);
357 return result;
358 }
359 // Keep it
360 lastButOneSchedule = schedulePeriod;
361 // Handle the last schedule period
362 if (
363 schedulePeriod.startPeriod ===
364 chargingSchedule.chargingSchedulePeriod[
365 chargingSchedule.chargingSchedulePeriod.length - 1
366 ].startPeriod
367 ) {
368 const result = {
369 limit: lastButOneSchedule.limit,
370 matchingChargingProfile: chargingProfile,
371 };
372 logger.debug(debugLogMsg, result);
373 return result;
374 }
375 }
376 }
377 }
378 return null;
379 }
380
381 public static getDefaultVoltageOut(
382 currentType: CurrentType,
383 templateFile: string,
384 logPrefix: string
385 ): Voltage {
386 const errMsg = `Unknown ${currentType} currentOutType in template file ${templateFile}, cannot define default voltage out`;
387 let defaultVoltageOut: number;
388 switch (currentType) {
389 case CurrentType.AC:
390 defaultVoltageOut = Voltage.VOLTAGE_230;
391 break;
392 case CurrentType.DC:
393 defaultVoltageOut = Voltage.VOLTAGE_400;
394 break;
395 default:
396 logger.error(`${logPrefix} ${errMsg}`);
397 throw new BaseError(errMsg);
398 }
399 return defaultVoltageOut;
400 }
401
402 public static getAuthorizationFile(stationInfo: ChargingStationInfo): string | undefined {
403 return (
404 stationInfo.authorizationFile &&
405 path.join(
406 path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../'),
407 'assets',
408 path.basename(stationInfo.authorizationFile)
409 )
410 );
411 }
412
413 private static getRandomSerialNumberSuffix(params?: {
414 randomBytesLength?: number;
415 upperCase?: boolean;
416 }): string {
417 const randomSerialNumberSuffix = crypto
418 .randomBytes(params?.randomBytesLength ?? 16)
419 .toString('hex');
420 if (params?.upperCase) {
421 return randomSerialNumberSuffix.toUpperCase();
422 }
423 return randomSerialNumberSuffix;
424 }
425 }