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