Optimize worker handlers calls by binding them to the current instance
[e-mobility-charging-stations-simulator.git] / src / charging-station / ChargingStationUtils.ts
CommitLineData
8114d10e
JB
1import crypto from 'crypto';
2import path from 'path';
3import { fileURLToPath } from 'url';
4
5import moment from 'moment';
6
7import BaseError from '../exception/BaseError';
981ebfbe
JB
8import type { ChargingStationInfo } from '../types/ChargingStationInfo';
9import {
492cf6ab 10 AmpereUnits,
981ebfbe 11 type ChargingStationTemplate,
492cf6ab
JB
12 CurrentType,
13 Voltage,
14} from '../types/ChargingStationTemplate';
8114d10e 15import { ChargingProfileKindType, RecurrencyKindType } from '../types/ocpp/1.6/ChargingProfile';
6c1761d4 16import type { ChargingProfile, ChargingSchedulePeriod } from '../types/ocpp/ChargingProfile';
ed3d2808 17import type { BootNotificationRequest } from '../types/ocpp/Requests';
17ac262c 18import { WorkerProcessType } from '../types/Worker';
8114d10e
JB
19import Configuration from '../utils/Configuration';
20import Constants from '../utils/Constants';
17ac262c 21import logger from '../utils/Logger';
8114d10e 22import Utils from '../utils/Utils';
17ac262c 23
91a4f151
JB
24const moduleName = 'ChargingStationUtils';
25
17ac262c 26export class ChargingStationUtils {
d5bd1c00
JB
27 private constructor() {
28 // This is intentional
29 }
30
17ac262c
JB
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 }),
33d7ecc7 58 // FIXME?: Should a firmware version change always reference a new configuration file?
fa7bccf4
JB
59 ...(!Utils.isUndefined(stationTemplate.firmwareVersion) && {
60 firmwareVersion: stationTemplate.firmwareVersion,
17ac262c 61 }),
fa7bccf4
JB
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,
17ac262c 66 }),
fa7bccf4
JB
67 ...(!Utils.isUndefined(stationTemplate.meterType) && {
68 meterType: stationTemplate.meterType,
17ac262c
JB
69 }),
70 };
71 return crypto
72 .createHash(Constants.DEFAULT_HASH_ALGORITHM)
fa7bccf4
JB
73 .update(
74 JSON.stringify(hashBootNotificationRequest) +
75 ChargingStationUtils.getChargingStationId(index, stationTemplate)
76 )
17ac262c
JB
77 .digest('hex');
78 }
79
fa7bccf4
JB
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
c72f6634 104 public static getConfiguredNumberOfConnectors(stationTemplate: ChargingStationTemplate): number {
fa7bccf4 105 let configuredMaxConnectors: number;
c72f6634 106 if (Utils.isEmptyArray(stationTemplate.numberOfConnectors) === false) {
fa7bccf4 107 const numberOfConnectors = stationTemplate.numberOfConnectors as number[];
c72f6634
JB
108 configuredMaxConnectors =
109 numberOfConnectors[Math.floor(Utils.secureRandom() * numberOfConnectors.length)];
110 } else if (Utils.isUndefined(stationTemplate.numberOfConnectors) === false) {
fa7bccf4
JB
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
17ac262c
JB
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(
cf2a5d9b 160 Configuration.getWorker().processType
17ac262c
JB
161 );
162 }
163
164 public static workerDynamicPoolInUse(): boolean {
cf2a5d9b 165 return Configuration.getWorker().processType === WorkerProcessType.DYNAMIC_POOL;
17ac262c
JB
166 }
167
17ac262c
JB
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])) {
17ac262c
JB
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
fa7bccf4
JB
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;
fec4d204 205 delete stationTemplate.meterSerialNumberPrefix;
51c83d6f 206 return stationTemplate as unknown as ChargingStationInfo;
fa7bccf4
JB
207 }
208
209 public static createStationInfoHash(stationInfo: ChargingStationInfo): void {
ccb1d6e9 210 delete stationInfo.infoHash;
7c72977b 211 stationInfo.infoHash = crypto
ccb1d6e9
JB
212 .createHash(Constants.DEFAULT_HASH_ALGORITHM)
213 .update(JSON.stringify(stationInfo))
214 .digest('hex');
17ac262c
JB
215 }
216
217 public static createSerialNumber(
fa7bccf4 218 stationTemplate: ChargingStationTemplate,
fec4d204 219 stationInfo: ChargingStationInfo = {} as ChargingStationInfo,
fa7bccf4
JB
220 params: {
221 randomSerialNumberUpperCase?: boolean;
222 randomSerialNumber?: boolean;
223 } = {
17ac262c
JB
224 randomSerialNumberUpperCase: true,
225 randomSerialNumber: true,
226 }
227 ): void {
228 params = params ?? {};
229 params.randomSerialNumberUpperCase = params?.randomSerialNumberUpperCase ?? true;
230 params.randomSerialNumber = params?.randomSerialNumber ?? true;
fa7bccf4
JB
231 const serialNumberSuffix = params?.randomSerialNumber
232 ? ChargingStationUtils.getRandomSerialNumberSuffix({
233 upperCase: params.randomSerialNumberUpperCase,
234 })
235 : '';
fec4d204
JB
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) {
baf93dda
JB
253 throw new BaseError(
254 'Missing charging station template or existing configuration to propagate serial number'
255 );
fec4d204
JB
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;
17ac262c
JB
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 *
0e4fa348
JB
287 * @param chargingProfiles -
288 * @param logPrefix -
289 * @returns
17ac262c
JB
290 */
291 public static getLimitFromChargingProfiles(
292 chargingProfiles: ChargingProfile[],
293 logPrefix: string
294 ): {
295 limit: number;
296 matchingChargingProfile: ChargingProfile;
297 } | null {
91a4f151 298 const debugLogMsg = `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Matching charging profile found for power limitation: %j`;
17ac262c
JB
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 };
91a4f151 342 logger.debug(debugLogMsg, result);
17ac262c
JB
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 };
91a4f151 356 logger.debug(debugLogMsg, result);
17ac262c
JB
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 };
91a4f151 372 logger.debug(debugLogMsg, result);
17ac262c
JB
373 return result;
374 }
375 }
376 }
377 }
378 return null;
379 }
380
492cf6ab
JB
381 public static getDefaultVoltageOut(
382 currentType: CurrentType,
383 templateFile: string,
384 logPrefix: string
385 ): Voltage {
fc040c43 386 const errMsg = `Unknown ${currentType} currentOutType in template file ${templateFile}, cannot define default voltage out`;
492cf6ab
JB
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:
fc040c43 396 logger.error(`${logPrefix} ${errMsg}`);
6c8f5d90 397 throw new BaseError(errMsg);
492cf6ab
JB
398 }
399 return defaultVoltageOut;
400 }
401
fa7bccf4
JB
402 public static getAuthorizationFile(stationInfo: ChargingStationInfo): string | undefined {
403 return (
404 stationInfo.authorizationFile &&
405 path.join(
0d8140bd 406 path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../'),
fa7bccf4
JB
407 'assets',
408 path.basename(stationInfo.authorizationFile)
409 )
410 );
411 }
412
17ac262c
JB
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}