perf: reduce OCPPUtils memory usage
[e-mobility-charging-stations-simulator.git] / src / charging-station / ocpp / OCPPServiceUtils.ts
1 import { readFileSync } from 'node:fs';
2 import { dirname, join } from 'node:path';
3 import { fileURLToPath } from 'node:url';
4
5 import type { DefinedError, ErrorObject, JSONSchemaType } from 'ajv';
6 import { isDate } from 'date-fns';
7
8 import { OCPP16Constants } from './1.6/OCPP16Constants';
9 import { OCPP20Constants } from './2.0/OCPP20Constants';
10 import { OCPPConstants } from './OCPPConstants';
11 import { type ChargingStation, getConfigurationKey, getIdTagsFile } from '../../charging-station';
12 import { BaseError } from '../../exception';
13 import {
14 AuthorizationStatus,
15 type AuthorizeRequest,
16 type AuthorizeResponse,
17 ChargePointErrorCode,
18 ChargingStationEvents,
19 type ConnectorStatus,
20 type ConnectorStatusEnum,
21 ErrorType,
22 FileType,
23 IncomingRequestCommand,
24 type JsonType,
25 MessageTrigger,
26 MessageType,
27 MeterValueMeasurand,
28 type MeterValuePhase,
29 type OCPP16StatusNotificationRequest,
30 type OCPP20StatusNotificationRequest,
31 OCPPVersion,
32 RequestCommand,
33 type SampledValueTemplate,
34 StandardParametersKey,
35 type StatusNotificationRequest,
36 type StatusNotificationResponse,
37 } from '../../types';
38 import {
39 handleFileException,
40 isNotEmptyArray,
41 isNotEmptyString,
42 logPrefix,
43 logger,
44 max,
45 min,
46 } from '../../utils';
47
48 export const getMessageTypeString = (messageType: MessageType): string => {
49 switch (messageType) {
50 case MessageType.CALL_MESSAGE:
51 return 'request';
52 case MessageType.CALL_RESULT_MESSAGE:
53 return 'response';
54 case MessageType.CALL_ERROR_MESSAGE:
55 return 'error';
56 default:
57 return 'unknown';
58 }
59 };
60
61 export const buildStatusNotificationRequest = (
62 chargingStation: ChargingStation,
63 connectorId: number,
64 status: ConnectorStatusEnum,
65 evseId?: number,
66 ): StatusNotificationRequest => {
67 switch (chargingStation.stationInfo?.ocppVersion) {
68 case OCPPVersion.VERSION_16:
69 return {
70 connectorId,
71 status,
72 errorCode: ChargePointErrorCode.NO_ERROR,
73 } as OCPP16StatusNotificationRequest;
74 case OCPPVersion.VERSION_20:
75 case OCPPVersion.VERSION_201:
76 return {
77 timestamp: new Date(),
78 connectorStatus: status,
79 connectorId,
80 evseId,
81 } as OCPP20StatusNotificationRequest;
82 default:
83 throw new BaseError('Cannot build status notification payload: OCPP version not supported');
84 }
85 };
86
87 export const isIdTagAuthorized = async (
88 chargingStation: ChargingStation,
89 connectorId: number,
90 idTag: string,
91 ): Promise<boolean> => {
92 if (
93 !chargingStation.getLocalAuthListEnabled() &&
94 !chargingStation.stationInfo?.remoteAuthorization
95 ) {
96 logger.warn(
97 `${chargingStation.logPrefix()} The charging station expects to authorize RFID tags but nor local authorization nor remote authorization are enabled. Misbehavior may occur`,
98 );
99 }
100 if (
101 chargingStation.getLocalAuthListEnabled() === true &&
102 isIdTagLocalAuthorized(chargingStation, idTag)
103 ) {
104 const connectorStatus: ConnectorStatus = chargingStation.getConnectorStatus(connectorId)!;
105 connectorStatus.localAuthorizeIdTag = idTag;
106 connectorStatus.idTagLocalAuthorized = true;
107 return true;
108 } else if (chargingStation.stationInfo?.remoteAuthorization) {
109 return await isIdTagRemoteAuthorized(chargingStation, connectorId, idTag);
110 }
111 return false;
112 };
113
114 const isIdTagLocalAuthorized = (chargingStation: ChargingStation, idTag: string): boolean => {
115 return (
116 chargingStation.hasIdTags() === true &&
117 isNotEmptyString(
118 chargingStation.idTagsCache
119 .getIdTags(getIdTagsFile(chargingStation.stationInfo)!)
120 ?.find((tag) => tag === idTag),
121 )
122 );
123 };
124
125 const isIdTagRemoteAuthorized = async (
126 chargingStation: ChargingStation,
127 connectorId: number,
128 idTag: string,
129 ): Promise<boolean> => {
130 chargingStation.getConnectorStatus(connectorId)!.authorizeIdTag = idTag;
131 return (
132 (
133 await chargingStation.ocppRequestService.requestHandler<AuthorizeRequest, AuthorizeResponse>(
134 chargingStation,
135 RequestCommand.AUTHORIZE,
136 {
137 idTag,
138 },
139 )
140 )?.idTagInfo?.status === AuthorizationStatus.ACCEPTED
141 );
142 };
143
144 export const sendAndSetConnectorStatus = async (
145 chargingStation: ChargingStation,
146 connectorId: number,
147 status: ConnectorStatusEnum,
148 evseId?: number,
149 options?: { send: boolean },
150 ): Promise<void> => {
151 options = { send: true, ...options };
152 if (options.send) {
153 checkConnectorStatusTransition(chargingStation, connectorId, status);
154 await chargingStation.ocppRequestService.requestHandler<
155 StatusNotificationRequest,
156 StatusNotificationResponse
157 >(
158 chargingStation,
159 RequestCommand.STATUS_NOTIFICATION,
160 buildStatusNotificationRequest(chargingStation, connectorId, status, evseId),
161 );
162 }
163 chargingStation.getConnectorStatus(connectorId)!.status = status;
164 chargingStation.emit(ChargingStationEvents.connectorStatusChanged, {
165 connectorId,
166 ...chargingStation.getConnectorStatus(connectorId),
167 });
168 };
169
170 const checkConnectorStatusTransition = (
171 chargingStation: ChargingStation,
172 connectorId: number,
173 status: ConnectorStatusEnum,
174 ): boolean => {
175 const fromStatus = chargingStation.getConnectorStatus(connectorId)!.status;
176 let transitionAllowed = false;
177 switch (chargingStation.stationInfo?.ocppVersion) {
178 case OCPPVersion.VERSION_16:
179 if (
180 (connectorId === 0 &&
181 OCPP16Constants.ChargePointStatusChargingStationTransitions.findIndex(
182 (transition) => transition.from === fromStatus && transition.to === status,
183 ) !== -1) ||
184 (connectorId > 0 &&
185 OCPP16Constants.ChargePointStatusConnectorTransitions.findIndex(
186 (transition) => transition.from === fromStatus && transition.to === status,
187 ) !== -1)
188 ) {
189 transitionAllowed = true;
190 }
191 break;
192 case OCPPVersion.VERSION_20:
193 case OCPPVersion.VERSION_201:
194 if (
195 (connectorId === 0 &&
196 OCPP20Constants.ChargingStationStatusTransitions.findIndex(
197 (transition) => transition.from === fromStatus && transition.to === status,
198 ) !== -1) ||
199 (connectorId > 0 &&
200 OCPP20Constants.ConnectorStatusTransitions.findIndex(
201 (transition) => transition.from === fromStatus && transition.to === status,
202 ) !== -1)
203 ) {
204 transitionAllowed = true;
205 }
206 break;
207 default:
208 throw new BaseError(
209 // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
210 `Cannot check connector status transition: OCPP version ${chargingStation.stationInfo?.ocppVersion} not supported`,
211 );
212 }
213 if (transitionAllowed === false) {
214 logger.warn(
215 `${chargingStation.logPrefix()} OCPP ${chargingStation.stationInfo
216 ?.ocppVersion} connector id ${connectorId} status transition from '${
217 chargingStation.getConnectorStatus(connectorId)!.status
218 }' to '${status}' is not allowed`,
219 );
220 }
221 return transitionAllowed;
222 };
223
224 export class OCPPServiceUtils {
225 public static getMessageTypeString = getMessageTypeString;
226 public static sendAndSetConnectorStatus = sendAndSetConnectorStatus;
227 public static isIdTagAuthorized = isIdTagAuthorized;
228
229 protected constructor() {
230 // This is intentional
231 }
232
233 public static ajvErrorsToErrorType(errors: ErrorObject[] | null | undefined): ErrorType {
234 if (isNotEmptyArray(errors) === true) {
235 for (const error of errors as DefinedError[]) {
236 switch (error.keyword) {
237 case 'type':
238 return ErrorType.TYPE_CONSTRAINT_VIOLATION;
239 case 'dependencies':
240 case 'required':
241 return ErrorType.OCCURRENCE_CONSTRAINT_VIOLATION;
242 case 'pattern':
243 case 'format':
244 return ErrorType.PROPERTY_CONSTRAINT_VIOLATION;
245 }
246 }
247 }
248 return ErrorType.FORMAT_VIOLATION;
249 }
250
251 public static isRequestCommandSupported(
252 chargingStation: ChargingStation,
253 command: RequestCommand,
254 ): boolean {
255 const isRequestCommand = Object.values<RequestCommand>(RequestCommand).includes(command);
256 if (
257 isRequestCommand === true &&
258 !chargingStation.stationInfo?.commandsSupport?.outgoingCommands
259 ) {
260 return true;
261 } else if (
262 isRequestCommand === true &&
263 chargingStation.stationInfo?.commandsSupport?.outgoingCommands?.[command]
264 ) {
265 return chargingStation.stationInfo?.commandsSupport?.outgoingCommands[command];
266 }
267 logger.error(`${chargingStation.logPrefix()} Unknown outgoing OCPP command '${command}'`);
268 return false;
269 }
270
271 public static isIncomingRequestCommandSupported(
272 chargingStation: ChargingStation,
273 command: IncomingRequestCommand,
274 ): boolean {
275 const isIncomingRequestCommand =
276 Object.values<IncomingRequestCommand>(IncomingRequestCommand).includes(command);
277 if (
278 isIncomingRequestCommand === true &&
279 !chargingStation.stationInfo?.commandsSupport?.incomingCommands
280 ) {
281 return true;
282 } else if (
283 isIncomingRequestCommand === true &&
284 chargingStation.stationInfo?.commandsSupport?.incomingCommands?.[command]
285 ) {
286 return chargingStation.stationInfo?.commandsSupport?.incomingCommands[command];
287 }
288 logger.error(`${chargingStation.logPrefix()} Unknown incoming OCPP command '${command}'`);
289 return false;
290 }
291
292 public static isMessageTriggerSupported(
293 chargingStation: ChargingStation,
294 messageTrigger: MessageTrigger,
295 ): boolean {
296 const isMessageTrigger = Object.values(MessageTrigger).includes(messageTrigger);
297 if (isMessageTrigger === true && !chargingStation.stationInfo?.messageTriggerSupport) {
298 return true;
299 } else if (
300 isMessageTrigger === true &&
301 chargingStation.stationInfo?.messageTriggerSupport?.[messageTrigger]
302 ) {
303 return chargingStation.stationInfo?.messageTriggerSupport[messageTrigger];
304 }
305 logger.error(
306 `${chargingStation.logPrefix()} Unknown incoming OCPP message trigger '${messageTrigger}'`,
307 );
308 return false;
309 }
310
311 public static isConnectorIdValid(
312 chargingStation: ChargingStation,
313 ocppCommand: IncomingRequestCommand,
314 connectorId: number,
315 ): boolean {
316 if (connectorId < 0) {
317 logger.error(
318 `${chargingStation.logPrefix()} ${ocppCommand} incoming request received with invalid connector id ${connectorId}`,
319 );
320 return false;
321 }
322 return true;
323 }
324
325 public static convertDateToISOString<T extends JsonType>(obj: T): void {
326 for (const key in obj) {
327 // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
328 if (isDate(obj![key])) {
329 // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
330 (obj![key] as string) = (obj![key] as Date).toISOString();
331 // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
332 } else if (obj![key] !== null && typeof obj![key] === 'object') {
333 // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
334 OCPPServiceUtils.convertDateToISOString<T>(obj![key] as T);
335 }
336 }
337 }
338
339 public static startHeartbeatInterval(chargingStation: ChargingStation, interval: number): void {
340 if (chargingStation.heartbeatSetInterval === undefined) {
341 chargingStation.startHeartbeat();
342 } else if (chargingStation.getHeartbeatInterval() !== interval) {
343 chargingStation.restartHeartbeat();
344 }
345 }
346
347 protected static parseJsonSchemaFile<T extends JsonType>(
348 relativePath: string,
349 ocppVersion: OCPPVersion,
350 moduleName?: string,
351 methodName?: string,
352 ): JSONSchemaType<T> {
353 const filePath = join(dirname(fileURLToPath(import.meta.url)), relativePath);
354 try {
355 return JSON.parse(readFileSync(filePath, 'utf8')) as JSONSchemaType<T>;
356 } catch (error) {
357 handleFileException(
358 filePath,
359 FileType.JsonSchema,
360 error as NodeJS.ErrnoException,
361 OCPPServiceUtils.logPrefix(ocppVersion, moduleName, methodName),
362 { throwError: false },
363 );
364 return {} as JSONSchemaType<T>;
365 }
366 }
367
368 protected static getSampledValueTemplate(
369 chargingStation: ChargingStation,
370 connectorId: number,
371 measurand: MeterValueMeasurand = MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER,
372 phase?: MeterValuePhase,
373 ): SampledValueTemplate | undefined {
374 const onPhaseStr = phase ? `on phase ${phase} ` : '';
375 if (OCPPConstants.OCPP_MEASURANDS_SUPPORTED.includes(measurand) === false) {
376 logger.warn(
377 `${chargingStation.logPrefix()} Trying to get unsupported MeterValues measurand '${measurand}' ${onPhaseStr}in template on connector id ${connectorId}`,
378 );
379 return;
380 }
381 if (
382 measurand !== MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER &&
383 getConfigurationKey(
384 chargingStation,
385 StandardParametersKey.MeterValuesSampledData,
386 )?.value?.includes(measurand) === false
387 ) {
388 logger.debug(
389 `${chargingStation.logPrefix()} Trying to get MeterValues measurand '${measurand}' ${onPhaseStr}in template on connector id ${connectorId} not found in '${
390 StandardParametersKey.MeterValuesSampledData
391 }' OCPP parameter`,
392 );
393 return;
394 }
395 const sampledValueTemplates: SampledValueTemplate[] =
396 chargingStation.getConnectorStatus(connectorId)!.MeterValues;
397 for (
398 let index = 0;
399 isNotEmptyArray(sampledValueTemplates) === true && index < sampledValueTemplates.length;
400 index++
401 ) {
402 if (
403 OCPPConstants.OCPP_MEASURANDS_SUPPORTED.includes(
404 sampledValueTemplates[index]?.measurand ??
405 MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER,
406 ) === false
407 ) {
408 logger.warn(
409 `${chargingStation.logPrefix()} Unsupported MeterValues measurand '${measurand}' ${onPhaseStr}in template on connector id ${connectorId}`,
410 );
411 } else if (
412 phase &&
413 sampledValueTemplates[index]?.phase === phase &&
414 sampledValueTemplates[index]?.measurand === measurand &&
415 getConfigurationKey(
416 chargingStation,
417 StandardParametersKey.MeterValuesSampledData,
418 )?.value?.includes(measurand) === true
419 ) {
420 return sampledValueTemplates[index];
421 } else if (
422 !phase &&
423 !sampledValueTemplates[index]?.phase &&
424 sampledValueTemplates[index]?.measurand === measurand &&
425 getConfigurationKey(
426 chargingStation,
427 StandardParametersKey.MeterValuesSampledData,
428 )?.value?.includes(measurand) === true
429 ) {
430 return sampledValueTemplates[index];
431 } else if (
432 measurand === MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER &&
433 (!sampledValueTemplates[index]?.measurand ||
434 sampledValueTemplates[index]?.measurand === measurand)
435 ) {
436 return sampledValueTemplates[index];
437 }
438 }
439 if (measurand === MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER) {
440 const errorMsg = `Missing MeterValues for default measurand '${measurand}' in template on connector id ${connectorId}`;
441 logger.error(`${chargingStation.logPrefix()} ${errorMsg}`);
442 throw new BaseError(errorMsg);
443 }
444 logger.debug(
445 `${chargingStation.logPrefix()} No MeterValues for measurand '${measurand}' ${onPhaseStr}in template on connector id ${connectorId}`,
446 );
447 }
448
449 protected static getLimitFromSampledValueTemplateCustomValue(
450 value: string | undefined,
451 maxLimit: number,
452 minLimit: number,
453 options?: { limitationEnabled?: boolean; fallbackValue?: number; unitMultiplier?: number },
454 ): number {
455 options = {
456 ...{
457 limitationEnabled: false,
458 unitMultiplier: 1,
459 fallbackValue: 0,
460 },
461 ...options,
462 };
463 const parsedValue = parseInt(value ?? '');
464 if (options?.limitationEnabled) {
465 return max(
466 min((!isNaN(parsedValue) ? parsedValue : Infinity) * options.unitMultiplier!, maxLimit),
467 minLimit,
468 );
469 }
470 return (!isNaN(parsedValue) ? parsedValue : options.fallbackValue!) * options.unitMultiplier!;
471 }
472
473 private static logPrefix = (
474 ocppVersion: OCPPVersion,
475 moduleName?: string,
476 methodName?: string,
477 ): string => {
478 const logMsg =
479 isNotEmptyString(moduleName) && isNotEmptyString(methodName)
480 ? ` OCPP ${ocppVersion} | ${moduleName}.${methodName}:`
481 : ` OCPP ${ocppVersion} |`;
482 return logPrefix(logMsg);
483 };
484 }