perf: reduce OCPPUtils memory usage
[e-mobility-charging-stations-simulator.git] / src / charging-station / ocpp / OCPPServiceUtils.ts
CommitLineData
d972af76
JB
1import { readFileSync } from 'node:fs';
2import { dirname, join } from 'node:path';
51022aa0 3import { fileURLToPath } from 'node:url';
7164966d
JB
4
5import type { DefinedError, ErrorObject, JSONSchemaType } from 'ajv';
3dcf7b67 6import { isDate } from 'date-fns';
06ad945f 7
4c3c0d59
JB
8import { OCPP16Constants } from './1.6/OCPP16Constants';
9import { OCPP20Constants } from './2.0/OCPP20Constants';
c3da35d4 10import { OCPPConstants } from './OCPPConstants';
ae725be3 11import { type ChargingStation, getConfigurationKey, getIdTagsFile } from '../../charging-station';
268a74bb 12import { BaseError } from '../../exception';
6e939d9e 13import {
ae725be3
JB
14 AuthorizationStatus,
15 type AuthorizeRequest,
16 type AuthorizeResponse,
268a74bb 17 ChargePointErrorCode,
3e888c65 18 ChargingStationEvents,
ae725be3 19 type ConnectorStatus,
268a74bb
JB
20 type ConnectorStatusEnum,
21 ErrorType,
22 FileType,
6e939d9e 23 IncomingRequestCommand,
268a74bb 24 type JsonType,
6e939d9e 25 MessageTrigger,
268a74bb
JB
26 MessageType,
27 MeterValueMeasurand,
28 type MeterValuePhase,
29 type OCPP16StatusNotificationRequest,
30 type OCPP20StatusNotificationRequest,
31 OCPPVersion,
6e939d9e 32 RequestCommand,
268a74bb
JB
33 type SampledValueTemplate,
34 StandardParametersKey,
6e939d9e 35 type StatusNotificationRequest,
48b75072 36 type StatusNotificationResponse,
268a74bb 37} from '../../types';
9bf0ef23
JB
38import {
39 handleFileException,
40 isNotEmptyArray,
41 isNotEmptyString,
42 logPrefix,
43 logger,
d71ce3fa 44 max,
5adf6ca4 45 min,
9bf0ef23 46} from '../../utils';
06ad945f 47
041365be
JB
48export 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
61export 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
87export 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
114const 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
125const 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
144export 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
170const 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
90befdb8 224export class OCPPServiceUtils {
041365be
JB
225 public static getMessageTypeString = getMessageTypeString;
226 public static sendAndSetConnectorStatus = sendAndSetConnectorStatus;
227 public static isIdTagAuthorized = isIdTagAuthorized;
228
d5bd1c00
JB
229 protected constructor() {
230 // This is intentional
231 }
232
9ff486f4
JB
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 }
06ad945f
JB
246 }
247 }
248 return ErrorType.FORMAT_VIOLATION;
249 }
250
ed3d2808 251 public static isRequestCommandSupported(
fd3c56d1 252 chargingStation: ChargingStation,
5edd8ba0 253 command: RequestCommand,
ed3d2808 254 ): boolean {
edd13439 255 const isRequestCommand = Object.values<RequestCommand>(RequestCommand).includes(command);
ed3d2808
JB
256 if (
257 isRequestCommand === true &&
258 !chargingStation.stationInfo?.commandsSupport?.outgoingCommands
259 ) {
260 return true;
261 } else if (
262 isRequestCommand === true &&
1c9de2b9 263 chargingStation.stationInfo?.commandsSupport?.outgoingCommands?.[command]
ed3d2808 264 ) {
1c9de2b9 265 return chargingStation.stationInfo?.commandsSupport?.outgoingCommands[command];
ed3d2808
JB
266 }
267 logger.error(`${chargingStation.logPrefix()} Unknown outgoing OCPP command '${command}'`);
268 return false;
269 }
270
271 public static isIncomingRequestCommandSupported(
fd3c56d1 272 chargingStation: ChargingStation,
5edd8ba0 273 command: IncomingRequestCommand,
ed3d2808 274 ): boolean {
edd13439
JB
275 const isIncomingRequestCommand =
276 Object.values<IncomingRequestCommand>(IncomingRequestCommand).includes(command);
ed3d2808
JB
277 if (
278 isIncomingRequestCommand === true &&
279 !chargingStation.stationInfo?.commandsSupport?.incomingCommands
280 ) {
281 return true;
282 } else if (
283 isIncomingRequestCommand === true &&
1c9de2b9 284 chargingStation.stationInfo?.commandsSupport?.incomingCommands?.[command]
ed3d2808 285 ) {
1c9de2b9 286 return chargingStation.stationInfo?.commandsSupport?.incomingCommands[command];
ed3d2808
JB
287 }
288 logger.error(`${chargingStation.logPrefix()} Unknown incoming OCPP command '${command}'`);
289 return false;
290 }
291
c60ed4b8
JB
292 public static isMessageTriggerSupported(
293 chargingStation: ChargingStation,
5edd8ba0 294 messageTrigger: MessageTrigger,
c60ed4b8
JB
295 ): boolean {
296 const isMessageTrigger = Object.values(MessageTrigger).includes(messageTrigger);
297 if (isMessageTrigger === true && !chargingStation.stationInfo?.messageTriggerSupport) {
298 return true;
1c9de2b9
JB
299 } else if (
300 isMessageTrigger === true &&
301 chargingStation.stationInfo?.messageTriggerSupport?.[messageTrigger]
302 ) {
303 return chargingStation.stationInfo?.messageTriggerSupport[messageTrigger];
c60ed4b8
JB
304 }
305 logger.error(
5edd8ba0 306 `${chargingStation.logPrefix()} Unknown incoming OCPP message trigger '${messageTrigger}'`,
c60ed4b8
JB
307 );
308 return false;
309 }
310
311 public static isConnectorIdValid(
312 chargingStation: ChargingStation,
313 ocppCommand: IncomingRequestCommand,
5edd8ba0 314 connectorId: number,
c60ed4b8
JB
315 ): boolean {
316 if (connectorId < 0) {
317 logger.error(
5edd8ba0 318 `${chargingStation.logPrefix()} ${ocppCommand} incoming request received with invalid connector id ${connectorId}`,
c60ed4b8
JB
319 );
320 return false;
321 }
322 return true;
323 }
324
1799761a 325 public static convertDateToISOString<T extends JsonType>(obj: T): void {
a37fc6dc
JB
326 for (const key in obj) {
327 // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
3dcf7b67 328 if (isDate(obj![key])) {
a37fc6dc
JB
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
e1d9a0f4 332 } else if (obj![key] !== null && typeof obj![key] === 'object') {
a37fc6dc 333 // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
e1d9a0f4 334 OCPPServiceUtils.convertDateToISOString<T>(obj![key] as T);
1799761a
JB
335 }
336 }
337 }
338
8f953431 339 public static startHeartbeatInterval(chargingStation: ChargingStation, interval: number): void {
2a2ad81b 340 if (chargingStation.heartbeatSetInterval === undefined) {
8f953431
JB
341 chargingStation.startHeartbeat();
342 } else if (chargingStation.getHeartbeatInterval() !== interval) {
343 chargingStation.restartHeartbeat();
344 }
345 }
346
7164966d 347 protected static parseJsonSchemaFile<T extends JsonType>(
51022aa0 348 relativePath: string,
1b271a54
JB
349 ocppVersion: OCPPVersion,
350 moduleName?: string,
5edd8ba0 351 methodName?: string,
7164966d 352 ): JSONSchemaType<T> {
d972af76 353 const filePath = join(dirname(fileURLToPath(import.meta.url)), relativePath);
7164966d 354 try {
d972af76 355 return JSON.parse(readFileSync(filePath, 'utf8')) as JSONSchemaType<T>;
7164966d 356 } catch (error) {
fa5995d6 357 handleFileException(
7164966d
JB
358 filePath,
359 FileType.JsonSchema,
360 error as NodeJS.ErrnoException,
1b271a54 361 OCPPServiceUtils.logPrefix(ocppVersion, moduleName, methodName),
5edd8ba0 362 { throwError: false },
7164966d 363 );
e1d9a0f4 364 return {} as JSONSchemaType<T>;
7164966d 365 }
130783a7
JB
366 }
367
884a6fdf
JB
368 protected static getSampledValueTemplate(
369 chargingStation: ChargingStation,
370 connectorId: number,
371 measurand: MeterValueMeasurand = MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER,
5edd8ba0 372 phase?: MeterValuePhase,
884a6fdf
JB
373 ): SampledValueTemplate | undefined {
374 const onPhaseStr = phase ? `on phase ${phase} ` : '';
c3da35d4 375 if (OCPPConstants.OCPP_MEASURANDS_SUPPORTED.includes(measurand) === false) {
884a6fdf 376 logger.warn(
5edd8ba0 377 `${chargingStation.logPrefix()} Trying to get unsupported MeterValues measurand '${measurand}' ${onPhaseStr}in template on connector id ${connectorId}`,
884a6fdf
JB
378 );
379 return;
380 }
381 if (
382 measurand !== MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER &&
f2d5e3d9 383 getConfigurationKey(
884a6fdf 384 chargingStation,
5edd8ba0 385 StandardParametersKey.MeterValuesSampledData,
72092cfc 386 )?.value?.includes(measurand) === false
884a6fdf
JB
387 ) {
388 logger.debug(
54ebb82c 389 `${chargingStation.logPrefix()} Trying to get MeterValues measurand '${measurand}' ${onPhaseStr}in template on connector id ${connectorId} not found in '${
884a6fdf 390 StandardParametersKey.MeterValuesSampledData
5edd8ba0 391 }' OCPP parameter`,
884a6fdf
JB
392 );
393 return;
394 }
395 const sampledValueTemplates: SampledValueTemplate[] =
e1d9a0f4 396 chargingStation.getConnectorStatus(connectorId)!.MeterValues;
884a6fdf
JB
397 for (
398 let index = 0;
9bf0ef23 399 isNotEmptyArray(sampledValueTemplates) === true && index < sampledValueTemplates.length;
884a6fdf
JB
400 index++
401 ) {
402 if (
c3da35d4 403 OCPPConstants.OCPP_MEASURANDS_SUPPORTED.includes(
884a6fdf 404 sampledValueTemplates[index]?.measurand ??
5edd8ba0 405 MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER,
884a6fdf
JB
406 ) === false
407 ) {
408 logger.warn(
5edd8ba0 409 `${chargingStation.logPrefix()} Unsupported MeterValues measurand '${measurand}' ${onPhaseStr}in template on connector id ${connectorId}`,
884a6fdf
JB
410 );
411 } else if (
412 phase &&
413 sampledValueTemplates[index]?.phase === phase &&
414 sampledValueTemplates[index]?.measurand === measurand &&
f2d5e3d9 415 getConfigurationKey(
884a6fdf 416 chargingStation,
5edd8ba0 417 StandardParametersKey.MeterValuesSampledData,
72092cfc 418 )?.value?.includes(measurand) === true
884a6fdf
JB
419 ) {
420 return sampledValueTemplates[index];
421 } else if (
422 !phase &&
1c9de2b9 423 !sampledValueTemplates[index]?.phase &&
884a6fdf 424 sampledValueTemplates[index]?.measurand === measurand &&
f2d5e3d9 425 getConfigurationKey(
884a6fdf 426 chargingStation,
5edd8ba0 427 StandardParametersKey.MeterValuesSampledData,
72092cfc 428 )?.value?.includes(measurand) === true
884a6fdf
JB
429 ) {
430 return sampledValueTemplates[index];
431 } else if (
432 measurand === MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER &&
1c9de2b9
JB
433 (!sampledValueTemplates[index]?.measurand ||
434 sampledValueTemplates[index]?.measurand === measurand)
884a6fdf
JB
435 ) {
436 return sampledValueTemplates[index];
437 }
438 }
439 if (measurand === MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER) {
54ebb82c 440 const errorMsg = `Missing MeterValues for default measurand '${measurand}' in template on connector id ${connectorId}`;
884a6fdf
JB
441 logger.error(`${chargingStation.logPrefix()} ${errorMsg}`);
442 throw new BaseError(errorMsg);
443 }
444 logger.debug(
5edd8ba0 445 `${chargingStation.logPrefix()} No MeterValues for measurand '${measurand}' ${onPhaseStr}in template on connector id ${connectorId}`,
884a6fdf
JB
446 );
447 }
448
90befdb8 449 protected static getLimitFromSampledValueTemplateCustomValue(
969c488d 450 value: string | undefined,
d71ce3fa
JB
451 maxLimit: number,
452 minLimit: number,
453 options?: { limitationEnabled?: boolean; fallbackValue?: number; unitMultiplier?: number },
90befdb8 454 ): number {
20f0b76c
JB
455 options = {
456 ...{
d624c9af 457 limitationEnabled: false,
20f0b76c 458 unitMultiplier: 1,
d71ce3fa 459 fallbackValue: 0,
20f0b76c
JB
460 },
461 ...options,
462 };
969c488d 463 const parsedValue = parseInt(value ?? '');
77684af8 464 if (options?.limitationEnabled) {
d71ce3fa
JB
465 return max(
466 min((!isNaN(parsedValue) ? parsedValue : Infinity) * options.unitMultiplier!, maxLimit),
467 minLimit,
468 );
77684af8 469 }
d71ce3fa 470 return (!isNaN(parsedValue) ? parsedValue : options.fallbackValue!) * options.unitMultiplier!;
90befdb8 471 }
7164966d 472
1b271a54
JB
473 private static logPrefix = (
474 ocppVersion: OCPPVersion,
475 moduleName?: string,
5edd8ba0 476 methodName?: string,
1b271a54
JB
477 ): string => {
478 const logMsg =
9bf0ef23 479 isNotEmptyString(moduleName) && isNotEmptyString(methodName)
1b271a54
JB
480 ? ` OCPP ${ocppVersion} | ${moduleName}.${methodName}:`
481 : ` OCPP ${ocppVersion} |`;
9bf0ef23 482 return logPrefix(logMsg);
8b7072dc 483 };
90befdb8 484}