Permit to disable persistent configuration storage.
[e-mobility-charging-stations-simulator.git] / src / charging-station / ChargingStationUtils.ts
CommitLineData
17ac262c
JB
1import { ChargingProfile, ChargingSchedulePeriod } from '../types/ocpp/ChargingProfile';
2import { ChargingProfileKindType, RecurrencyKindType } from '../types/ocpp/1.6/ChargingProfile';
492cf6ab
JB
3import ChargingStationTemplate, {
4 AmpereUnits,
5 CurrentType,
6 Voltage,
7} from '../types/ChargingStationTemplate';
8import { MeterValueMeasurand, MeterValuePhase } from '../types/ocpp/MeterValues';
17ac262c
JB
9
10import { BootNotificationRequest } from '../types/ocpp/Requests';
492cf6ab
JB
11import ChargingStation from './ChargingStation';
12import { ChargingStationConfigurationUtils } from './ChargingStationConfigurationUtils';
17ac262c
JB
13import ChargingStationInfo from '../types/ChargingStationInfo';
14import Configuration from '../utils/Configuration';
15import Constants from '../utils/Constants';
492cf6ab
JB
16import { SampledValueTemplate } from '../types/MeasurandPerPhaseSampledValueTemplates';
17import { StandardParametersKey } from '../types/ocpp/Configuration';
17ac262c
JB
18import Utils from '../utils/Utils';
19import { WebSocketCloseEventStatusString } from '../types/WebSocket';
20import { WorkerProcessType } from '../types/Worker';
21import crypto from 'crypto';
22import logger from '../utils/Logger';
23import moment from 'moment';
24
25export class ChargingStationUtils {
26 public static getChargingStationId(
27 index: number,
28 stationTemplate: ChargingStationTemplate
29 ): string {
30 // In case of multiple instances: add instance index to charging station id
31 const instanceIndex = process.env.CF_INSTANCE_INDEX ?? 0;
32 const idSuffix = stationTemplate.nameSuffix ?? '';
33 const idStr = '000000000' + index.toString();
ccb1d6e9 34 return stationTemplate?.fixedName
17ac262c
JB
35 ? stationTemplate.baseName
36 : stationTemplate.baseName +
37 '-' +
38 instanceIndex.toString() +
39 idStr.substring(idStr.length - 4) +
40 idSuffix;
41 }
42
43 public static getHashId(stationInfo: ChargingStationInfo): string {
44 const hashBootNotificationRequest = {
45 chargePointModel: stationInfo.chargePointModel,
46 chargePointVendor: stationInfo.chargePointVendor,
47 ...(!Utils.isUndefined(stationInfo.chargeBoxSerialNumberPrefix) && {
48 chargeBoxSerialNumber: stationInfo.chargeBoxSerialNumberPrefix,
49 }),
50 ...(!Utils.isUndefined(stationInfo.chargePointSerialNumberPrefix) && {
51 chargePointSerialNumber: stationInfo.chargePointSerialNumberPrefix,
52 }),
53 ...(!Utils.isUndefined(stationInfo.firmwareVersion) && {
54 firmwareVersion: stationInfo.firmwareVersion,
55 }),
56 ...(!Utils.isUndefined(stationInfo.iccid) && { iccid: stationInfo.iccid }),
57 ...(!Utils.isUndefined(stationInfo.imsi) && { imsi: stationInfo.imsi }),
58 ...(!Utils.isUndefined(stationInfo.meterSerialNumberPrefix) && {
59 meterSerialNumber: stationInfo.meterSerialNumberPrefix,
60 }),
61 ...(!Utils.isUndefined(stationInfo.meterType) && {
62 meterType: stationInfo.meterType,
63 }),
64 };
65 return crypto
66 .createHash(Constants.DEFAULT_HASH_ALGORITHM)
67 .update(JSON.stringify(hashBootNotificationRequest) + stationInfo.chargingStationId)
68 .digest('hex');
69 }
70
71 public static createBootNotificationRequest(
72 stationInfo: ChargingStationInfo
73 ): BootNotificationRequest {
74 return {
75 chargePointModel: stationInfo.chargePointModel,
76 chargePointVendor: stationInfo.chargePointVendor,
77 ...(!Utils.isUndefined(stationInfo.chargeBoxSerialNumber) && {
78 chargeBoxSerialNumber: stationInfo.chargeBoxSerialNumber,
79 }),
80 ...(!Utils.isUndefined(stationInfo.chargePointSerialNumber) && {
81 chargePointSerialNumber: stationInfo.chargePointSerialNumber,
82 }),
83 ...(!Utils.isUndefined(stationInfo.firmwareVersion) && {
84 firmwareVersion: stationInfo.firmwareVersion,
85 }),
86 ...(!Utils.isUndefined(stationInfo.iccid) && { iccid: stationInfo.iccid }),
87 ...(!Utils.isUndefined(stationInfo.imsi) && { imsi: stationInfo.imsi }),
88 ...(!Utils.isUndefined(stationInfo.meterSerialNumber) && {
89 meterSerialNumber: stationInfo.meterSerialNumber,
90 }),
91 ...(!Utils.isUndefined(stationInfo.meterType) && {
92 meterType: stationInfo.meterType,
93 }),
94 };
95 }
96
97 public static workerPoolInUse(): boolean {
98 return [WorkerProcessType.DYNAMIC_POOL, WorkerProcessType.STATIC_POOL].includes(
99 Configuration.getWorkerProcess()
100 );
101 }
102
103 public static workerDynamicPoolInUse(): boolean {
104 return Configuration.getWorkerProcess() === WorkerProcessType.DYNAMIC_POOL;
105 }
106
107 /**
108 * Convert websocket error code to human readable string message
109 *
110 * @param code websocket error code
111 * @returns human readable string message
112 */
113 public static getWebSocketCloseEventStatusString(code: number): string {
114 if (code >= 0 && code <= 999) {
115 return '(Unused)';
116 } else if (code >= 1016) {
117 if (code <= 1999) {
118 return '(For WebSocket standard)';
119 } else if (code <= 2999) {
120 return '(For WebSocket extensions)';
121 } else if (code <= 3999) {
122 return '(For libraries and frameworks)';
123 } else if (code <= 4999) {
124 return '(For applications)';
125 }
126 }
127 if (!Utils.isUndefined(WebSocketCloseEventStatusString[code])) {
128 return WebSocketCloseEventStatusString[code] as string;
129 }
130 return '(Unknown)';
131 }
132
133 public static warnDeprecatedTemplateKey(
134 template: ChargingStationTemplate,
135 key: string,
136 templateFile: string,
137 logPrefix: string,
138 logMsgToAppend = ''
139 ): void {
140 if (!Utils.isUndefined(template[key])) {
17ac262c
JB
141 logger.warn(
142 `${logPrefix} Deprecated template key '${key}' usage in file '${templateFile}'${
143 logMsgToAppend && '. ' + logMsgToAppend
144 }`
145 );
146 }
147 }
148
149 public static convertDeprecatedTemplateKey(
150 template: ChargingStationTemplate,
151 deprecatedKey: string,
152 key: string
153 ): void {
154 if (!Utils.isUndefined(template[deprecatedKey])) {
155 template[key] = template[deprecatedKey] as unknown;
156 delete template[deprecatedKey];
157 }
158 }
159
160 public static createStationInfoHash(stationInfo: ChargingStationInfo): ChargingStationInfo {
ccb1d6e9
JB
161 const previousInfoHash = stationInfo?.infoHash ?? '';
162 delete stationInfo.infoHash;
163 const currentInfoHash = crypto
164 .createHash(Constants.DEFAULT_HASH_ALGORITHM)
165 .update(JSON.stringify(stationInfo))
166 .digest('hex');
167 if (
168 Utils.isEmptyString(previousInfoHash) ||
169 (!Utils.isEmptyString(previousInfoHash) && currentInfoHash !== previousInfoHash)
170 ) {
171 stationInfo.infoHash = currentInfoHash;
172 } else {
173 stationInfo.infoHash = previousInfoHash;
17ac262c
JB
174 }
175 return stationInfo;
176 }
177
178 public static createSerialNumber(
179 stationInfo: ChargingStationInfo,
ccb1d6e9 180 existingStationInfo?: ChargingStationInfo | null,
17ac262c
JB
181 params: { randomSerialNumberUpperCase?: boolean; randomSerialNumber?: boolean } = {
182 randomSerialNumberUpperCase: true,
183 randomSerialNumber: true,
184 }
185 ): void {
186 params = params ?? {};
187 params.randomSerialNumberUpperCase = params?.randomSerialNumberUpperCase ?? true;
188 params.randomSerialNumber = params?.randomSerialNumber ?? true;
ccb1d6e9 189 if (existingStationInfo) {
17ac262c
JB
190 existingStationInfo?.chargePointSerialNumber &&
191 (stationInfo.chargePointSerialNumber = existingStationInfo.chargePointSerialNumber);
192 existingStationInfo?.chargeBoxSerialNumber &&
193 (stationInfo.chargeBoxSerialNumber = existingStationInfo.chargeBoxSerialNumber);
194 existingStationInfo?.meterSerialNumber &&
195 (stationInfo.meterSerialNumber = existingStationInfo.meterSerialNumber);
196 } else {
197 const serialNumberSuffix = params?.randomSerialNumber
198 ? ChargingStationUtils.getRandomSerialNumberSuffix({
199 upperCase: params.randomSerialNumberUpperCase,
200 })
201 : '';
202 stationInfo.chargePointSerialNumber =
203 stationInfo?.chargePointSerialNumberPrefix &&
204 stationInfo.chargePointSerialNumberPrefix + serialNumberSuffix;
205 stationInfo.chargeBoxSerialNumber =
206 stationInfo?.chargeBoxSerialNumberPrefix &&
207 stationInfo.chargeBoxSerialNumberPrefix + serialNumberSuffix;
208 stationInfo.meterSerialNumber =
209 stationInfo?.meterSerialNumberPrefix &&
210 stationInfo.meterSerialNumberPrefix + serialNumberSuffix;
211 }
212 }
213
214 public static getAmperageLimitationUnitDivider(stationInfo: ChargingStationInfo): number {
215 let unitDivider = 1;
216 switch (stationInfo.amperageLimitationUnit) {
217 case AmpereUnits.DECI_AMPERE:
218 unitDivider = 10;
219 break;
220 case AmpereUnits.CENTI_AMPERE:
221 unitDivider = 100;
222 break;
223 case AmpereUnits.MILLI_AMPERE:
224 unitDivider = 1000;
225 break;
226 }
227 return unitDivider;
228 }
229
230 /**
231 * Charging profiles should already be sorted by connectorId and stack level (highest stack level has priority)
232 *
233 * @param {ChargingProfile[]} chargingProfiles
234 * @param {string} logPrefix
235 * @returns {{ limit, matchingChargingProfile }}
236 */
237 public static getLimitFromChargingProfiles(
238 chargingProfiles: ChargingProfile[],
239 logPrefix: string
240 ): {
241 limit: number;
242 matchingChargingProfile: ChargingProfile;
243 } | null {
244 for (const chargingProfile of chargingProfiles) {
245 // Set helpers
246 const currentMoment = moment();
247 const chargingSchedule = chargingProfile.chargingSchedule;
248 // Check type (recurring) and if it is already active
249 // Adjust the daily recurring schedule to today
250 if (
251 chargingProfile.chargingProfileKind === ChargingProfileKindType.RECURRING &&
252 chargingProfile.recurrencyKind === RecurrencyKindType.DAILY &&
253 currentMoment.isAfter(chargingSchedule.startSchedule)
254 ) {
255 const currentDate = new Date();
256 chargingSchedule.startSchedule = new Date(chargingSchedule.startSchedule);
257 chargingSchedule.startSchedule.setFullYear(
258 currentDate.getFullYear(),
259 currentDate.getMonth(),
260 currentDate.getDate()
261 );
262 // Check if the start of the schedule is yesterday
263 if (moment(chargingSchedule.startSchedule).isAfter(currentMoment)) {
264 chargingSchedule.startSchedule.setDate(currentDate.getDate() - 1);
265 }
266 } else if (moment(chargingSchedule.startSchedule).isAfter(currentMoment)) {
267 return null;
268 }
269 // Check if the charging profile is active
270 if (
271 moment(chargingSchedule.startSchedule)
272 .add(chargingSchedule.duration, 's')
273 .isAfter(currentMoment)
274 ) {
275 let lastButOneSchedule: ChargingSchedulePeriod;
276 // Search the right schedule period
277 for (const schedulePeriod of chargingSchedule.chargingSchedulePeriod) {
278 // Handling of only one period
279 if (
280 chargingSchedule.chargingSchedulePeriod.length === 1 &&
281 schedulePeriod.startPeriod === 0
282 ) {
283 const result = {
284 limit: schedulePeriod.limit,
285 matchingChargingProfile: chargingProfile,
286 };
287 logger.debug(
288 `${logPrefix} Matching charging profile found for power limitation: %j`,
289 result
290 );
291 return result;
292 }
293 // Find the right schedule period
294 if (
295 moment(chargingSchedule.startSchedule)
296 .add(schedulePeriod.startPeriod, 's')
297 .isAfter(currentMoment)
298 ) {
299 // Found the schedule: last but one is the correct one
300 const result = {
301 limit: lastButOneSchedule.limit,
302 matchingChargingProfile: chargingProfile,
303 };
304 logger.debug(
305 `${logPrefix} Matching charging profile found for power limitation: %j`,
306 result
307 );
308 return result;
309 }
310 // Keep it
311 lastButOneSchedule = schedulePeriod;
312 // Handle the last schedule period
313 if (
314 schedulePeriod.startPeriod ===
315 chargingSchedule.chargingSchedulePeriod[
316 chargingSchedule.chargingSchedulePeriod.length - 1
317 ].startPeriod
318 ) {
319 const result = {
320 limit: lastButOneSchedule.limit,
321 matchingChargingProfile: chargingProfile,
322 };
323 logger.debug(
324 `${logPrefix} Matching charging profile found for power limitation: %j`,
325 result
326 );
327 return result;
328 }
329 }
330 }
331 }
332 return null;
333 }
334
492cf6ab
JB
335 public static getDefaultVoltageOut(
336 currentType: CurrentType,
337 templateFile: string,
338 logPrefix: string
339 ): Voltage {
340 const errMsg = `${logPrefix} Unknown ${currentType} currentOutType in template file ${templateFile}, cannot define default voltage out`;
341 let defaultVoltageOut: number;
342 switch (currentType) {
343 case CurrentType.AC:
344 defaultVoltageOut = Voltage.VOLTAGE_230;
345 break;
346 case CurrentType.DC:
347 defaultVoltageOut = Voltage.VOLTAGE_400;
348 break;
349 default:
350 logger.error(errMsg);
351 throw new Error(errMsg);
352 }
353 return defaultVoltageOut;
354 }
355
356 public static getSampledValueTemplate(
357 chargingStation: ChargingStation,
358 connectorId: number,
359 measurand: MeterValueMeasurand = MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER,
360 phase?: MeterValuePhase
361 ): SampledValueTemplate | undefined {
362 const onPhaseStr = phase ? `on phase ${phase} ` : '';
363 if (!Constants.SUPPORTED_MEASURANDS.includes(measurand)) {
364 logger.warn(
365 `${chargingStation.logPrefix()} Trying to get unsupported MeterValues measurand '${measurand}' ${onPhaseStr}in template on connectorId ${connectorId}`
366 );
367 return;
368 }
369 if (
370 measurand !== MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER &&
371 !ChargingStationConfigurationUtils.getConfigurationKey(
372 chargingStation,
373 StandardParametersKey.MeterValuesSampledData
374 )?.value.includes(measurand)
375 ) {
376 logger.debug(
377 `${chargingStation.logPrefix()} Trying to get MeterValues measurand '${measurand}' ${onPhaseStr}in template on connectorId ${connectorId} not found in '${
378 StandardParametersKey.MeterValuesSampledData
379 }' OCPP parameter`
380 );
381 return;
382 }
383 const sampledValueTemplates: SampledValueTemplate[] =
384 chargingStation.getConnectorStatus(connectorId).MeterValues;
385 for (
386 let index = 0;
387 !Utils.isEmptyArray(sampledValueTemplates) && index < sampledValueTemplates.length;
388 index++
389 ) {
390 if (
391 !Constants.SUPPORTED_MEASURANDS.includes(
392 sampledValueTemplates[index]?.measurand ??
393 MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
394 )
395 ) {
396 logger.warn(
397 `${chargingStation.logPrefix()} Unsupported MeterValues measurand '${measurand}' ${onPhaseStr}in template on connectorId ${connectorId}`
398 );
399 } else if (
400 phase &&
401 sampledValueTemplates[index]?.phase === phase &&
402 sampledValueTemplates[index]?.measurand === measurand &&
403 ChargingStationConfigurationUtils.getConfigurationKey(
404 chargingStation,
405 StandardParametersKey.MeterValuesSampledData
406 )?.value.includes(measurand)
407 ) {
408 return sampledValueTemplates[index];
409 } else if (
410 !phase &&
411 !sampledValueTemplates[index].phase &&
412 sampledValueTemplates[index]?.measurand === measurand &&
413 ChargingStationConfigurationUtils.getConfigurationKey(
414 chargingStation,
415 StandardParametersKey.MeterValuesSampledData
416 )?.value.includes(measurand)
417 ) {
418 return sampledValueTemplates[index];
419 } else if (
420 measurand === MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER &&
421 (!sampledValueTemplates[index].measurand ||
422 sampledValueTemplates[index].measurand === measurand)
423 ) {
424 return sampledValueTemplates[index];
425 }
426 }
427 if (measurand === MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER) {
428 const errorMsg = `${chargingStation.logPrefix()} Missing MeterValues for default measurand '${measurand}' in template on connectorId ${connectorId}`;
429 logger.error(errorMsg);
430 throw new Error(errorMsg);
431 }
432 logger.debug(
433 `${chargingStation.logPrefix()} No MeterValues for measurand '${measurand}' ${onPhaseStr}in template on connectorId ${connectorId}`
434 );
435 }
436
17ac262c
JB
437 private static getRandomSerialNumberSuffix(params?: {
438 randomBytesLength?: number;
439 upperCase?: boolean;
440 }): string {
441 const randomSerialNumberSuffix = crypto
442 .randomBytes(params?.randomBytesLength ?? 16)
443 .toString('hex');
444 if (params?.upperCase) {
445 return randomSerialNumberSuffix.toUpperCase();
446 }
447 return randomSerialNumberSuffix;
448 }
449}