Fix not initialized variables at startup
[e-mobility-charging-stations-simulator.git] / src / charging-station / ChargingStationUtils.ts
1 import { ChargingProfile, ChargingSchedulePeriod } from '../types/ocpp/ChargingProfile';
2 import { ChargingProfileKindType, RecurrencyKindType } from '../types/ocpp/1.6/ChargingProfile';
3 import ChargingStationTemplate, { AmpereUnits } from '../types/ChargingStationTemplate';
4
5 import { BootNotificationRequest } from '../types/ocpp/Requests';
6 import ChargingStationInfo from '../types/ChargingStationInfo';
7 import Configuration from '../utils/Configuration';
8 import Constants from '../utils/Constants';
9 import Utils from '../utils/Utils';
10 import { WebSocketCloseEventStatusString } from '../types/WebSocket';
11 import { WorkerProcessType } from '../types/Worker';
12 import crypto from 'crypto';
13 import logger from '../utils/Logger';
14 import moment from 'moment';
15
16 export class ChargingStationUtils {
17 public static getChargingStationId(
18 index: number,
19 stationTemplate: ChargingStationTemplate
20 ): string {
21 // In case of multiple instances: add instance index to charging station id
22 const instanceIndex = process.env.CF_INSTANCE_INDEX ?? 0;
23 const idSuffix = stationTemplate.nameSuffix ?? '';
24 const idStr = '000000000' + index.toString();
25 return stationTemplate.fixedName
26 ? stationTemplate.baseName
27 : stationTemplate.baseName +
28 '-' +
29 instanceIndex.toString() +
30 idStr.substring(idStr.length - 4) +
31 idSuffix;
32 }
33
34 public static getHashId(stationInfo: ChargingStationInfo): string {
35 const hashBootNotificationRequest = {
36 chargePointModel: stationInfo.chargePointModel,
37 chargePointVendor: stationInfo.chargePointVendor,
38 ...(!Utils.isUndefined(stationInfo.chargeBoxSerialNumberPrefix) && {
39 chargeBoxSerialNumber: stationInfo.chargeBoxSerialNumberPrefix,
40 }),
41 ...(!Utils.isUndefined(stationInfo.chargePointSerialNumberPrefix) && {
42 chargePointSerialNumber: stationInfo.chargePointSerialNumberPrefix,
43 }),
44 ...(!Utils.isUndefined(stationInfo.firmwareVersion) && {
45 firmwareVersion: stationInfo.firmwareVersion,
46 }),
47 ...(!Utils.isUndefined(stationInfo.iccid) && { iccid: stationInfo.iccid }),
48 ...(!Utils.isUndefined(stationInfo.imsi) && { imsi: stationInfo.imsi }),
49 ...(!Utils.isUndefined(stationInfo.meterSerialNumberPrefix) && {
50 meterSerialNumber: stationInfo.meterSerialNumberPrefix,
51 }),
52 ...(!Utils.isUndefined(stationInfo.meterType) && {
53 meterType: stationInfo.meterType,
54 }),
55 };
56 return crypto
57 .createHash(Constants.DEFAULT_HASH_ALGORITHM)
58 .update(JSON.stringify(hashBootNotificationRequest) + stationInfo.chargingStationId)
59 .digest('hex');
60 }
61
62 public static createBootNotificationRequest(
63 stationInfo: ChargingStationInfo
64 ): BootNotificationRequest {
65 return {
66 chargePointModel: stationInfo.chargePointModel,
67 chargePointVendor: stationInfo.chargePointVendor,
68 ...(!Utils.isUndefined(stationInfo.chargeBoxSerialNumber) && {
69 chargeBoxSerialNumber: stationInfo.chargeBoxSerialNumber,
70 }),
71 ...(!Utils.isUndefined(stationInfo.chargePointSerialNumber) && {
72 chargePointSerialNumber: stationInfo.chargePointSerialNumber,
73 }),
74 ...(!Utils.isUndefined(stationInfo.firmwareVersion) && {
75 firmwareVersion: stationInfo.firmwareVersion,
76 }),
77 ...(!Utils.isUndefined(stationInfo.iccid) && { iccid: stationInfo.iccid }),
78 ...(!Utils.isUndefined(stationInfo.imsi) && { imsi: stationInfo.imsi }),
79 ...(!Utils.isUndefined(stationInfo.meterSerialNumber) && {
80 meterSerialNumber: stationInfo.meterSerialNumber,
81 }),
82 ...(!Utils.isUndefined(stationInfo.meterType) && {
83 meterType: stationInfo.meterType,
84 }),
85 };
86 }
87
88 public static workerPoolInUse(): boolean {
89 return [WorkerProcessType.DYNAMIC_POOL, WorkerProcessType.STATIC_POOL].includes(
90 Configuration.getWorkerProcess()
91 );
92 }
93
94 public static workerDynamicPoolInUse(): boolean {
95 return Configuration.getWorkerProcess() === WorkerProcessType.DYNAMIC_POOL;
96 }
97
98 /**
99 * Convert websocket error code to human readable string message
100 *
101 * @param code websocket error code
102 * @returns human readable string message
103 */
104 public static getWebSocketCloseEventStatusString(code: number): string {
105 if (code >= 0 && code <= 999) {
106 return '(Unused)';
107 } else if (code >= 1016) {
108 if (code <= 1999) {
109 return '(For WebSocket standard)';
110 } else if (code <= 2999) {
111 return '(For WebSocket extensions)';
112 } else if (code <= 3999) {
113 return '(For libraries and frameworks)';
114 } else if (code <= 4999) {
115 return '(For applications)';
116 }
117 }
118 if (!Utils.isUndefined(WebSocketCloseEventStatusString[code])) {
119 return WebSocketCloseEventStatusString[code] as string;
120 }
121 return '(Unknown)';
122 }
123
124 public static warnDeprecatedTemplateKey(
125 template: ChargingStationTemplate,
126 key: string,
127 templateFile: string,
128 logPrefix: string,
129 logMsgToAppend = ''
130 ): void {
131 if (!Utils.isUndefined(template[key])) {
132 logger.warn(
133 `${logPrefix} Deprecated template key '${key}' usage in file '${templateFile}'${
134 logMsgToAppend && '. ' + logMsgToAppend
135 }`
136 );
137 }
138 }
139
140 public static convertDeprecatedTemplateKey(
141 template: ChargingStationTemplate,
142 deprecatedKey: string,
143 key: string
144 ): void {
145 if (!Utils.isUndefined(template[deprecatedKey])) {
146 template[key] = template[deprecatedKey] as unknown;
147 delete template[deprecatedKey];
148 }
149 }
150
151 public static createStationInfoHash(stationInfo: ChargingStationInfo): ChargingStationInfo {
152 if (!Utils.isEmptyObject(stationInfo)) {
153 const previousInfoHash = stationInfo?.infoHash ?? '';
154 delete stationInfo.infoHash;
155 const currentInfoHash = crypto
156 .createHash(Constants.DEFAULT_HASH_ALGORITHM)
157 .update(JSON.stringify(stationInfo))
158 .digest('hex');
159 if (
160 Utils.isEmptyString(previousInfoHash) ||
161 (!Utils.isEmptyString(previousInfoHash) && currentInfoHash !== previousInfoHash)
162 ) {
163 stationInfo.infoHash = currentInfoHash;
164 } else {
165 stationInfo.infoHash = previousInfoHash;
166 }
167 }
168 return stationInfo;
169 }
170
171 public static createSerialNumber(
172 stationInfo: ChargingStationInfo,
173 existingStationInfo?: ChargingStationInfo,
174 params: { randomSerialNumberUpperCase?: boolean; randomSerialNumber?: boolean } = {
175 randomSerialNumberUpperCase: true,
176 randomSerialNumber: true,
177 }
178 ): void {
179 params = params ?? {};
180 params.randomSerialNumberUpperCase = params?.randomSerialNumberUpperCase ?? true;
181 params.randomSerialNumber = params?.randomSerialNumber ?? true;
182 if (!Utils.isEmptyObject(existingStationInfo)) {
183 existingStationInfo?.chargePointSerialNumber &&
184 (stationInfo.chargePointSerialNumber = existingStationInfo.chargePointSerialNumber);
185 existingStationInfo?.chargeBoxSerialNumber &&
186 (stationInfo.chargeBoxSerialNumber = existingStationInfo.chargeBoxSerialNumber);
187 existingStationInfo?.meterSerialNumber &&
188 (stationInfo.meterSerialNumber = existingStationInfo.meterSerialNumber);
189 } else {
190 const serialNumberSuffix = params?.randomSerialNumber
191 ? ChargingStationUtils.getRandomSerialNumberSuffix({
192 upperCase: params.randomSerialNumberUpperCase,
193 })
194 : '';
195 stationInfo.chargePointSerialNumber =
196 stationInfo?.chargePointSerialNumberPrefix &&
197 stationInfo.chargePointSerialNumberPrefix + serialNumberSuffix;
198 stationInfo.chargeBoxSerialNumber =
199 stationInfo?.chargeBoxSerialNumberPrefix &&
200 stationInfo.chargeBoxSerialNumberPrefix + serialNumberSuffix;
201 stationInfo.meterSerialNumber =
202 stationInfo?.meterSerialNumberPrefix &&
203 stationInfo.meterSerialNumberPrefix + serialNumberSuffix;
204 }
205 }
206
207 public static getAmperageLimitationUnitDivider(stationInfo: ChargingStationInfo): number {
208 let unitDivider = 1;
209 switch (stationInfo.amperageLimitationUnit) {
210 case AmpereUnits.DECI_AMPERE:
211 unitDivider = 10;
212 break;
213 case AmpereUnits.CENTI_AMPERE:
214 unitDivider = 100;
215 break;
216 case AmpereUnits.MILLI_AMPERE:
217 unitDivider = 1000;
218 break;
219 }
220 return unitDivider;
221 }
222
223 /**
224 * Charging profiles should already be sorted by connectorId and stack level (highest stack level has priority)
225 *
226 * @param {ChargingProfile[]} chargingProfiles
227 * @param {string} logPrefix
228 * @returns {{ limit, matchingChargingProfile }}
229 */
230 public static getLimitFromChargingProfiles(
231 chargingProfiles: ChargingProfile[],
232 logPrefix: string
233 ): {
234 limit: number;
235 matchingChargingProfile: ChargingProfile;
236 } | null {
237 for (const chargingProfile of chargingProfiles) {
238 // Set helpers
239 const currentMoment = moment();
240 const chargingSchedule = chargingProfile.chargingSchedule;
241 // Check type (recurring) and if it is already active
242 // Adjust the daily recurring schedule to today
243 if (
244 chargingProfile.chargingProfileKind === ChargingProfileKindType.RECURRING &&
245 chargingProfile.recurrencyKind === RecurrencyKindType.DAILY &&
246 currentMoment.isAfter(chargingSchedule.startSchedule)
247 ) {
248 const currentDate = new Date();
249 chargingSchedule.startSchedule = new Date(chargingSchedule.startSchedule);
250 chargingSchedule.startSchedule.setFullYear(
251 currentDate.getFullYear(),
252 currentDate.getMonth(),
253 currentDate.getDate()
254 );
255 // Check if the start of the schedule is yesterday
256 if (moment(chargingSchedule.startSchedule).isAfter(currentMoment)) {
257 chargingSchedule.startSchedule.setDate(currentDate.getDate() - 1);
258 }
259 } else if (moment(chargingSchedule.startSchedule).isAfter(currentMoment)) {
260 return null;
261 }
262 // Check if the charging profile is active
263 if (
264 moment(chargingSchedule.startSchedule)
265 .add(chargingSchedule.duration, 's')
266 .isAfter(currentMoment)
267 ) {
268 let lastButOneSchedule: ChargingSchedulePeriod;
269 // Search the right schedule period
270 for (const schedulePeriod of chargingSchedule.chargingSchedulePeriod) {
271 // Handling of only one period
272 if (
273 chargingSchedule.chargingSchedulePeriod.length === 1 &&
274 schedulePeriod.startPeriod === 0
275 ) {
276 const result = {
277 limit: schedulePeriod.limit,
278 matchingChargingProfile: chargingProfile,
279 };
280 logger.debug(
281 `${logPrefix} Matching charging profile found for power limitation: %j`,
282 result
283 );
284 return result;
285 }
286 // Find the right schedule period
287 if (
288 moment(chargingSchedule.startSchedule)
289 .add(schedulePeriod.startPeriod, 's')
290 .isAfter(currentMoment)
291 ) {
292 // Found the schedule: last but one is the correct one
293 const result = {
294 limit: lastButOneSchedule.limit,
295 matchingChargingProfile: chargingProfile,
296 };
297 logger.debug(
298 `${logPrefix} Matching charging profile found for power limitation: %j`,
299 result
300 );
301 return result;
302 }
303 // Keep it
304 lastButOneSchedule = schedulePeriod;
305 // Handle the last schedule period
306 if (
307 schedulePeriod.startPeriod ===
308 chargingSchedule.chargingSchedulePeriod[
309 chargingSchedule.chargingSchedulePeriod.length - 1
310 ].startPeriod
311 ) {
312 const result = {
313 limit: lastButOneSchedule.limit,
314 matchingChargingProfile: chargingProfile,
315 };
316 logger.debug(
317 `${logPrefix} Matching charging profile found for power limitation: %j`,
318 result
319 );
320 return result;
321 }
322 }
323 }
324 }
325 return null;
326 }
327
328 private static getRandomSerialNumberSuffix(params?: {
329 randomBytesLength?: number;
330 upperCase?: boolean;
331 }): string {
332 const randomSerialNumberSuffix = crypto
333 .randomBytes(params?.randomBytesLength ?? 16)
334 .toString('hex');
335 if (params?.upperCase) {
336 return randomSerialNumberSuffix.toUpperCase();
337 }
338 return randomSerialNumberSuffix;
339 }
340 }