Change repo URI to SAP one
[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();
34 return stationTemplate.fixedName
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 {
161 if (!Utils.isEmptyObject(stationInfo)) {
162 const previousInfoHash = stationInfo?.infoHash ?? '';
163 delete stationInfo.infoHash;
164 const currentInfoHash = crypto
165 .createHash(Constants.DEFAULT_HASH_ALGORITHM)
166 .update(JSON.stringify(stationInfo))
167 .digest('hex');
168 if (
169 Utils.isEmptyString(previousInfoHash) ||
170 (!Utils.isEmptyString(previousInfoHash) && currentInfoHash !== previousInfoHash)
171 ) {
172 stationInfo.infoHash = currentInfoHash;
173 } else {
174 stationInfo.infoHash = previousInfoHash;
175 }
176 }
177 return stationInfo;
178 }
179
180 public static createSerialNumber(
181 stationInfo: ChargingStationInfo,
182 existingStationInfo?: ChargingStationInfo,
183 params: { randomSerialNumberUpperCase?: boolean; randomSerialNumber?: boolean } = {
184 randomSerialNumberUpperCase: true,
185 randomSerialNumber: true,
186 }
187 ): void {
188 params = params ?? {};
189 params.randomSerialNumberUpperCase = params?.randomSerialNumberUpperCase ?? true;
190 params.randomSerialNumber = params?.randomSerialNumber ?? true;
191 if (!Utils.isEmptyObject(existingStationInfo)) {
192 existingStationInfo?.chargePointSerialNumber &&
193 (stationInfo.chargePointSerialNumber = existingStationInfo.chargePointSerialNumber);
194 existingStationInfo?.chargeBoxSerialNumber &&
195 (stationInfo.chargeBoxSerialNumber = existingStationInfo.chargeBoxSerialNumber);
196 existingStationInfo?.meterSerialNumber &&
197 (stationInfo.meterSerialNumber = existingStationInfo.meterSerialNumber);
198 } else {
199 const serialNumberSuffix = params?.randomSerialNumber
200 ? ChargingStationUtils.getRandomSerialNumberSuffix({
201 upperCase: params.randomSerialNumberUpperCase,
202 })
203 : '';
204 stationInfo.chargePointSerialNumber =
205 stationInfo?.chargePointSerialNumberPrefix &&
206 stationInfo.chargePointSerialNumberPrefix + serialNumberSuffix;
207 stationInfo.chargeBoxSerialNumber =
208 stationInfo?.chargeBoxSerialNumberPrefix &&
209 stationInfo.chargeBoxSerialNumberPrefix + serialNumberSuffix;
210 stationInfo.meterSerialNumber =
211 stationInfo?.meterSerialNumberPrefix &&
212 stationInfo.meterSerialNumberPrefix + serialNumberSuffix;
213 }
214 }
215
216 public static getAmperageLimitationUnitDivider(stationInfo: ChargingStationInfo): number {
217 let unitDivider = 1;
218 switch (stationInfo.amperageLimitationUnit) {
219 case AmpereUnits.DECI_AMPERE:
220 unitDivider = 10;
221 break;
222 case AmpereUnits.CENTI_AMPERE:
223 unitDivider = 100;
224 break;
225 case AmpereUnits.MILLI_AMPERE:
226 unitDivider = 1000;
227 break;
228 }
229 return unitDivider;
230 }
231
232 /**
233 * Charging profiles should already be sorted by connectorId and stack level (highest stack level has priority)
234 *
235 * @param {ChargingProfile[]} chargingProfiles
236 * @param {string} logPrefix
237 * @returns {{ limit, matchingChargingProfile }}
238 */
239 public static getLimitFromChargingProfiles(
240 chargingProfiles: ChargingProfile[],
241 logPrefix: string
242 ): {
243 limit: number;
244 matchingChargingProfile: ChargingProfile;
245 } | null {
246 for (const chargingProfile of chargingProfiles) {
247 // Set helpers
248 const currentMoment = moment();
249 const chargingSchedule = chargingProfile.chargingSchedule;
250 // Check type (recurring) and if it is already active
251 // Adjust the daily recurring schedule to today
252 if (
253 chargingProfile.chargingProfileKind === ChargingProfileKindType.RECURRING &&
254 chargingProfile.recurrencyKind === RecurrencyKindType.DAILY &&
255 currentMoment.isAfter(chargingSchedule.startSchedule)
256 ) {
257 const currentDate = new Date();
258 chargingSchedule.startSchedule = new Date(chargingSchedule.startSchedule);
259 chargingSchedule.startSchedule.setFullYear(
260 currentDate.getFullYear(),
261 currentDate.getMonth(),
262 currentDate.getDate()
263 );
264 // Check if the start of the schedule is yesterday
265 if (moment(chargingSchedule.startSchedule).isAfter(currentMoment)) {
266 chargingSchedule.startSchedule.setDate(currentDate.getDate() - 1);
267 }
268 } else if (moment(chargingSchedule.startSchedule).isAfter(currentMoment)) {
269 return null;
270 }
271 // Check if the charging profile is active
272 if (
273 moment(chargingSchedule.startSchedule)
274 .add(chargingSchedule.duration, 's')
275 .isAfter(currentMoment)
276 ) {
277 let lastButOneSchedule: ChargingSchedulePeriod;
278 // Search the right schedule period
279 for (const schedulePeriod of chargingSchedule.chargingSchedulePeriod) {
280 // Handling of only one period
281 if (
282 chargingSchedule.chargingSchedulePeriod.length === 1 &&
283 schedulePeriod.startPeriod === 0
284 ) {
285 const result = {
286 limit: schedulePeriod.limit,
287 matchingChargingProfile: chargingProfile,
288 };
289 logger.debug(
290 `${logPrefix} Matching charging profile found for power limitation: %j`,
291 result
292 );
293 return result;
294 }
295 // Find the right schedule period
296 if (
297 moment(chargingSchedule.startSchedule)
298 .add(schedulePeriod.startPeriod, 's')
299 .isAfter(currentMoment)
300 ) {
301 // Found the schedule: last but one is the correct one
302 const result = {
303 limit: lastButOneSchedule.limit,
304 matchingChargingProfile: chargingProfile,
305 };
306 logger.debug(
307 `${logPrefix} Matching charging profile found for power limitation: %j`,
308 result
309 );
310 return result;
311 }
312 // Keep it
313 lastButOneSchedule = schedulePeriod;
314 // Handle the last schedule period
315 if (
316 schedulePeriod.startPeriod ===
317 chargingSchedule.chargingSchedulePeriod[
318 chargingSchedule.chargingSchedulePeriod.length - 1
319 ].startPeriod
320 ) {
321 const result = {
322 limit: lastButOneSchedule.limit,
323 matchingChargingProfile: chargingProfile,
324 };
325 logger.debug(
326 `${logPrefix} Matching charging profile found for power limitation: %j`,
327 result
328 );
329 return result;
330 }
331 }
332 }
333 }
334 return null;
335 }
336
492cf6ab
JB
337 public static getDefaultVoltageOut(
338 currentType: CurrentType,
339 templateFile: string,
340 logPrefix: string
341 ): Voltage {
342 const errMsg = `${logPrefix} Unknown ${currentType} currentOutType in template file ${templateFile}, cannot define default voltage out`;
343 let defaultVoltageOut: number;
344 switch (currentType) {
345 case CurrentType.AC:
346 defaultVoltageOut = Voltage.VOLTAGE_230;
347 break;
348 case CurrentType.DC:
349 defaultVoltageOut = Voltage.VOLTAGE_400;
350 break;
351 default:
352 logger.error(errMsg);
353 throw new Error(errMsg);
354 }
355 return defaultVoltageOut;
356 }
357
358 public static getSampledValueTemplate(
359 chargingStation: ChargingStation,
360 connectorId: number,
361 measurand: MeterValueMeasurand = MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER,
362 phase?: MeterValuePhase
363 ): SampledValueTemplate | undefined {
364 const onPhaseStr = phase ? `on phase ${phase} ` : '';
365 if (!Constants.SUPPORTED_MEASURANDS.includes(measurand)) {
366 logger.warn(
367 `${chargingStation.logPrefix()} Trying to get unsupported MeterValues measurand '${measurand}' ${onPhaseStr}in template on connectorId ${connectorId}`
368 );
369 return;
370 }
371 if (
372 measurand !== MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER &&
373 !ChargingStationConfigurationUtils.getConfigurationKey(
374 chargingStation,
375 StandardParametersKey.MeterValuesSampledData
376 )?.value.includes(measurand)
377 ) {
378 logger.debug(
379 `${chargingStation.logPrefix()} Trying to get MeterValues measurand '${measurand}' ${onPhaseStr}in template on connectorId ${connectorId} not found in '${
380 StandardParametersKey.MeterValuesSampledData
381 }' OCPP parameter`
382 );
383 return;
384 }
385 const sampledValueTemplates: SampledValueTemplate[] =
386 chargingStation.getConnectorStatus(connectorId).MeterValues;
387 for (
388 let index = 0;
389 !Utils.isEmptyArray(sampledValueTemplates) && index < sampledValueTemplates.length;
390 index++
391 ) {
392 if (
393 !Constants.SUPPORTED_MEASURANDS.includes(
394 sampledValueTemplates[index]?.measurand ??
395 MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
396 )
397 ) {
398 logger.warn(
399 `${chargingStation.logPrefix()} Unsupported MeterValues measurand '${measurand}' ${onPhaseStr}in template on connectorId ${connectorId}`
400 );
401 } else if (
402 phase &&
403 sampledValueTemplates[index]?.phase === phase &&
404 sampledValueTemplates[index]?.measurand === measurand &&
405 ChargingStationConfigurationUtils.getConfigurationKey(
406 chargingStation,
407 StandardParametersKey.MeterValuesSampledData
408 )?.value.includes(measurand)
409 ) {
410 return sampledValueTemplates[index];
411 } else if (
412 !phase &&
413 !sampledValueTemplates[index].phase &&
414 sampledValueTemplates[index]?.measurand === measurand &&
415 ChargingStationConfigurationUtils.getConfigurationKey(
416 chargingStation,
417 StandardParametersKey.MeterValuesSampledData
418 )?.value.includes(measurand)
419 ) {
420 return sampledValueTemplates[index];
421 } else if (
422 measurand === MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER &&
423 (!sampledValueTemplates[index].measurand ||
424 sampledValueTemplates[index].measurand === measurand)
425 ) {
426 return sampledValueTemplates[index];
427 }
428 }
429 if (measurand === MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER) {
430 const errorMsg = `${chargingStation.logPrefix()} Missing MeterValues for default measurand '${measurand}' in template on connectorId ${connectorId}`;
431 logger.error(errorMsg);
432 throw new Error(errorMsg);
433 }
434 logger.debug(
435 `${chargingStation.logPrefix()} No MeterValues for measurand '${measurand}' ${onPhaseStr}in template on connectorId ${connectorId}`
436 );
437 }
438
17ac262c
JB
439 private static getRandomSerialNumberSuffix(params?: {
440 randomBytesLength?: number;
441 upperCase?: boolean;
442 }): string {
443 const randomSerialNumberSuffix = crypto
444 .randomBytes(params?.randomBytesLength ?? 16)
445 .toString('hex');
446 if (params?.upperCase) {
447 return randomSerialNumberSuffix.toUpperCase();
448 }
449 return randomSerialNumberSuffix;
450 }
451}