Optimize worker handlers calls by binding them to the current instance
[e-mobility-charging-stations-simulator.git] / src / charging-station / ocpp / OCPPRequestService.ts
CommitLineData
6c1761d4 1import type { JSONSchemaType } from 'ajv';
b52c969d
JB
2import Ajv from 'ajv-draft-04';
3import ajvFormats from 'ajv-formats';
4
8114d10e
JB
5import OCPPError from '../../exception/OCPPError';
6import PerformanceStatistics from '../../performance/PerformanceStatistics';
6c1761d4
JB
7import type { EmptyObject } from '../../types/EmptyObject';
8import type { HandleErrorParams } from '../../types/Error';
9import type { JsonObject, JsonType } from '../../types/JsonType';
8114d10e
JB
10import { ErrorType } from '../../types/ocpp/ErrorType';
11import { MessageType } from '../../types/ocpp/MessageType';
e7aeea18 12import {
d900c8d7 13 ErrorCallback,
e7aeea18 14 IncomingRequestCommand,
b3ec7bc1 15 OutgoingRequest,
e7aeea18 16 RequestCommand,
be9b0d50 17 RequestParams,
d900c8d7 18 ResponseCallback,
e7aeea18 19 ResponseType,
e7aeea18 20} from '../../types/ocpp/Requests';
6c1761d4 21import type { ErrorResponse, Response } from '../../types/ocpp/Responses';
c0560973 22import Constants from '../../utils/Constants';
9f2e3130 23import logger from '../../utils/Logger';
8114d10e
JB
24import Utils from '../../utils/Utils';
25import type ChargingStation from '../ChargingStation';
26import type OCPPResponseService from './OCPPResponseService';
cbb3711f 27import { OCPPServiceUtils } from './OCPPServiceUtils';
c0560973 28
e3018bc4
JB
29const moduleName = 'OCPPRequestService';
30
c0560973 31export default abstract class OCPPRequestService {
08f130a0 32 private static instance: OCPPRequestService | null = null;
012ae1a9 33 private readonly ajv: Ajv;
10068088 34
9f2e3130 35 private readonly ocppResponseService: OCPPResponseService;
c0560973 36
08f130a0 37 protected constructor(ocppResponseService: OCPPResponseService) {
c0560973 38 this.ocppResponseService = ocppResponseService;
9952c548
JB
39 this.ajv = new Ajv();
40 ajvFormats(this.ajv);
f7f98c68 41 this.requestHandler.bind(this);
511c1897 42 this.sendMessage.bind(this);
c75a6675 43 this.sendResponse.bind(this);
54ce9b7d 44 this.sendError.bind(this);
9952c548
JB
45 this.internalSendMessage.bind(this);
46 this.buildMessageToSend.bind(this);
47 this.validateRequestPayload.bind(this);
c0560973
JB
48 }
49
e7aeea18 50 public static getInstance<T extends OCPPRequestService>(
08f130a0 51 this: new (ocppResponseService: OCPPResponseService) => T,
e7aeea18
JB
52 ocppResponseService: OCPPResponseService
53 ): T {
1ca780f9 54 if (OCPPRequestService.instance === null) {
08f130a0 55 OCPPRequestService.instance = new this(ocppResponseService);
9f2e3130 56 }
08f130a0 57 return OCPPRequestService.instance as T;
9f2e3130
JB
58 }
59
c75a6675 60 public async sendResponse(
08f130a0 61 chargingStation: ChargingStation,
e7aeea18 62 messageId: string,
5cc4b63b 63 messagePayload: JsonType,
e7aeea18
JB
64 commandName: IncomingRequestCommand
65 ): Promise<ResponseType> {
5e0c67e8 66 try {
c75a6675 67 // Send response message
e7aeea18 68 return await this.internalSendMessage(
08f130a0 69 chargingStation,
e7aeea18
JB
70 messageId,
71 messagePayload,
72 MessageType.CALL_RESULT_MESSAGE,
73 commandName
74 );
5e0c67e8 75 } catch (error) {
dc922667 76 this.handleSendMessageError(chargingStation, commandName, error as Error, {
07561812 77 throwError: true,
dc922667 78 });
5e0c67e8
JB
79 }
80 }
81
e7aeea18 82 public async sendError(
08f130a0 83 chargingStation: ChargingStation,
e7aeea18
JB
84 messageId: string,
85 ocppError: OCPPError,
b3ec7bc1 86 commandName: RequestCommand | IncomingRequestCommand
e7aeea18 87 ): Promise<ResponseType> {
5e0c67e8
JB
88 try {
89 // Send error message
e7aeea18 90 return await this.internalSendMessage(
08f130a0 91 chargingStation,
e7aeea18
JB
92 messageId,
93 ocppError,
94 MessageType.CALL_ERROR_MESSAGE,
95 commandName
96 );
5e0c67e8 97 } catch (error) {
dc922667 98 this.handleSendMessageError(chargingStation, commandName, error as Error);
5e0c67e8
JB
99 }
100 }
101
e7aeea18 102 protected async sendMessage(
08f130a0 103 chargingStation: ChargingStation,
e7aeea18 104 messageId: string,
5cc4b63b 105 messagePayload: JsonType,
e7aeea18 106 commandName: RequestCommand,
be9b0d50 107 params: RequestParams = {
e7aeea18
JB
108 skipBufferingOnError: false,
109 triggerMessage: false,
110 }
111 ): Promise<ResponseType> {
5e0c67e8 112 try {
e7aeea18 113 return await this.internalSendMessage(
08f130a0 114 chargingStation,
e7aeea18
JB
115 messageId,
116 messagePayload,
117 MessageType.CALL_MESSAGE,
118 commandName,
119 params
120 );
5e0c67e8 121 } catch (error) {
07561812 122 this.handleSendMessageError(chargingStation, commandName, error as Error);
5e0c67e8
JB
123 }
124 }
125
b52c969d
JB
126 protected validateRequestPayload<T extends JsonType>(
127 chargingStation: ChargingStation,
128 commandName: RequestCommand,
129 schema: JSONSchemaType<T>,
130 payload: T
131 ): boolean {
0638ddd2 132 if (chargingStation.getPayloadSchemaValidation() === false) {
b52c969d
JB
133 return true;
134 }
135 const validate = this.ajv.compile(schema);
136 if (validate(payload)) {
137 return true;
138 }
139 logger.error(
140 `${chargingStation.logPrefix()} ${moduleName}.validateRequestPayload: Request PDU is invalid: %j`,
141 validate.errors
142 );
e909d2a7 143 // OCPPError usage here is debatable: it's an error in the OCPP stack but not targeted to sendError().
b52c969d 144 throw new OCPPError(
01a4dcbb 145 OCPPServiceUtils.ajvErrorsToErrorType(validate.errors),
b52c969d
JB
146 'Request PDU is invalid',
147 commandName,
148 JSON.stringify(validate.errors, null, 2)
149 );
150 }
151
e7aeea18 152 private async internalSendMessage(
08f130a0 153 chargingStation: ChargingStation,
e7aeea18 154 messageId: string,
5cc4b63b 155 messagePayload: JsonType | OCPPError,
e7aeea18
JB
156 messageType: MessageType,
157 commandName?: RequestCommand | IncomingRequestCommand,
be9b0d50 158 params: RequestParams = {
e7aeea18
JB
159 skipBufferingOnError: false,
160 triggerMessage: false,
161 }
162 ): Promise<ResponseType> {
163 if (
3a13fc92
JB
164 (chargingStation.isInUnknownState() === true &&
165 commandName === RequestCommand.BOOT_NOTIFICATION) ||
166 (chargingStation.getOcppStrictCompliance() === false &&
167 chargingStation.isInUnknownState() === true) ||
168 chargingStation.isInAcceptedState() === true ||
169 (chargingStation.isInPendingState() === true &&
170 (params.triggerMessage === true || messageType === MessageType.CALL_RESULT_MESSAGE))
e7aeea18 171 ) {
caad9d6b
JB
172 // eslint-disable-next-line @typescript-eslint/no-this-alias
173 const self = this;
174 // Send a message through wsConnection
e7aeea18
JB
175 return Utils.promiseWithTimeout(
176 new Promise((resolve, reject) => {
177 const messageToSend = this.buildMessageToSend(
08f130a0 178 chargingStation,
e7aeea18
JB
179 messageId,
180 messagePayload,
181 messageType,
182 commandName,
183 responseCallback,
a2d1c0f1 184 errorCallback
e7aeea18 185 );
0638ddd2 186 if (chargingStation.getEnableStatistics() === true) {
08f130a0 187 chargingStation.performanceStatistics.addRequestStatistic(commandName, messageType);
caad9d6b 188 }
e7aeea18 189 // Check if wsConnection opened
0638ddd2 190 if (chargingStation.isWebSocketConnectionOpened() === true) {
e7aeea18
JB
191 // Yes: Send Message
192 const beginId = PerformanceStatistics.beginMeasure(commandName);
193 // FIXME: Handle sending error
08f130a0 194 chargingStation.wsConnection.send(messageToSend);
e7aeea18 195 PerformanceStatistics.endMeasure(commandName, beginId);
6f35d2da 196 logger.debug(
08f130a0 197 `${chargingStation.logPrefix()} >> Command '${commandName}' sent ${this.getMessageTypeString(
9a3b8d9f
JB
198 messageType
199 )} payload: ${messageToSend}`
6f35d2da 200 );
0638ddd2 201 } else if (params.skipBufferingOnError === false) {
e7aeea18 202 // Buffer it
08f130a0 203 chargingStation.bufferMessage(messageToSend);
e7aeea18
JB
204 const ocppError = new OCPPError(
205 ErrorType.GENERIC_ERROR,
206 `WebSocket closed for buffered message id '${messageId}' with content '${messageToSend}'`,
207 commandName,
5cc4b63b 208 (messagePayload as JsonObject)?.details ?? {}
e7aeea18
JB
209 );
210 if (messageType === MessageType.CALL_MESSAGE) {
211 // Reject it but keep the request in the cache
212 return reject(ocppError);
213 }
a2d1c0f1 214 return errorCallback(ocppError, false);
e7aeea18
JB
215 } else {
216 // Reject it
a2d1c0f1 217 return errorCallback(
e7aeea18
JB
218 new OCPPError(
219 ErrorType.GENERIC_ERROR,
220 `WebSocket closed for non buffered message id '${messageId}' with content '${messageToSend}'`,
221 commandName,
5cc4b63b 222 (messagePayload as JsonObject)?.details ?? {}
e7aeea18
JB
223 ),
224 false
225 );
caad9d6b 226 }
e7aeea18
JB
227 // Response?
228 if (messageType !== MessageType.CALL_MESSAGE) {
229 // Yes: send Ok
230 return resolve(messagePayload);
231 }
232
233 /**
234 * Function that will receive the request's response
235 *
0e4fa348
JB
236 * @param payload -
237 * @param requestPayload -
e7aeea18 238 */
3a148e48 239 function responseCallback(payload: JsonType, requestPayload: JsonType): void {
0638ddd2 240 if (chargingStation.getEnableStatistics() === true) {
08f130a0 241 chargingStation.performanceStatistics.addRequestStatistic(
e7aeea18
JB
242 commandName,
243 MessageType.CALL_RESULT_MESSAGE
244 );
245 }
246 // Handle the request's response
3a148e48
JB
247 self.ocppResponseService
248 .responseHandler(
08f130a0 249 chargingStation,
e7aeea18
JB
250 commandName as RequestCommand,
251 payload,
252 requestPayload
3a148e48
JB
253 )
254 .then(() => {
255 resolve(payload);
256 })
257 .catch((error) => {
258 reject(error);
259 })
260 .finally(() => {
261 chargingStation.requests.delete(messageId);
262 });
caad9d6b 263 }
caad9d6b 264
e7aeea18
JB
265 /**
266 * Function that will receive the request's error response
267 *
0e4fa348
JB
268 * @param error -
269 * @param requestStatistic -
e7aeea18 270 */
a2d1c0f1 271 function errorCallback(error: OCPPError, requestStatistic = true): void {
0afed85f 272 if (requestStatistic === true && chargingStation.getEnableStatistics() === true) {
08f130a0 273 chargingStation.performanceStatistics.addRequestStatistic(
e7aeea18
JB
274 commandName,
275 MessageType.CALL_ERROR_MESSAGE
276 );
277 }
278 logger.error(
fc040c43
JB
279 `${chargingStation.logPrefix()} Error occurred when calling command ${commandName} with message data ${JSON.stringify(
280 messagePayload
281 )}:`,
282 error
e7aeea18 283 );
08f130a0 284 chargingStation.requests.delete(messageId);
e7aeea18 285 reject(error);
caad9d6b 286 }
e7aeea18
JB
287 }),
288 Constants.OCPP_WEBSOCKET_TIMEOUT,
289 new OCPPError(
290 ErrorType.GENERIC_ERROR,
291 `Timeout for message id '${messageId}'`,
292 commandName,
5cc4b63b 293 (messagePayload as JsonObject)?.details ?? {}
e7aeea18
JB
294 ),
295 () => {
08f130a0 296 messageType === MessageType.CALL_MESSAGE && chargingStation.requests.delete(messageId);
caad9d6b 297 }
e7aeea18 298 );
caad9d6b 299 }
e7aeea18
JB
300 throw new OCPPError(
301 ErrorType.SECURITY_ERROR,
e3018bc4 302 `Cannot send command ${commandName} PDU when the charging station is in ${chargingStation.getRegistrationStatus()} state on the central server`,
e7aeea18
JB
303 commandName
304 );
c0560973
JB
305 }
306
e7aeea18 307 private buildMessageToSend(
08f130a0 308 chargingStation: ChargingStation,
e7aeea18 309 messageId: string,
5cc4b63b 310 messagePayload: JsonType | OCPPError,
e7aeea18
JB
311 messageType: MessageType,
312 commandName?: RequestCommand | IncomingRequestCommand,
d900c8d7
JB
313 responseCallback?: ResponseCallback,
314 errorCallback?: ErrorCallback
e7aeea18 315 ): string {
e7accadb
JB
316 let messageToSend: string;
317 // Type of message
318 switch (messageType) {
319 // Request
320 case MessageType.CALL_MESSAGE:
321 // Build request
08f130a0 322 chargingStation.requests.set(messageId, [
e7aeea18 323 responseCallback,
a2d1c0f1 324 errorCallback,
e7aeea18 325 commandName,
5cc4b63b 326 messagePayload as JsonType,
e7aeea18 327 ]);
b3ec7bc1
JB
328 messageToSend = JSON.stringify([
329 messageType,
330 messageId,
331 commandName,
332 messagePayload,
333 ] as OutgoingRequest);
e7accadb
JB
334 break;
335 // Response
336 case MessageType.CALL_RESULT_MESSAGE:
337 // Build response
b3ec7bc1 338 messageToSend = JSON.stringify([messageType, messageId, messagePayload] as Response);
e7accadb
JB
339 break;
340 // Error Message
341 case MessageType.CALL_ERROR_MESSAGE:
342 // Build Error Message
e7aeea18
JB
343 messageToSend = JSON.stringify([
344 messageType,
345 messageId,
b3ec7bc1
JB
346 (messagePayload as OCPPError)?.code ?? ErrorType.GENERIC_ERROR,
347 (messagePayload as OCPPError)?.message ?? '',
348 (messagePayload as OCPPError)?.details ?? { commandName },
349 ] as ErrorResponse);
e7accadb
JB
350 break;
351 }
352 return messageToSend;
353 }
354
9a3b8d9f
JB
355 private getMessageTypeString(messageType: MessageType): string {
356 switch (messageType) {
357 case MessageType.CALL_MESSAGE:
358 return 'request';
359 case MessageType.CALL_RESULT_MESSAGE:
360 return 'response';
361 case MessageType.CALL_ERROR_MESSAGE:
362 return 'error';
363 }
364 }
365
dc922667 366 private handleSendMessageError(
08f130a0 367 chargingStation: ChargingStation,
e7aeea18
JB
368 commandName: RequestCommand | IncomingRequestCommand,
369 error: Error,
07561812 370 params: HandleErrorParams<EmptyObject> = { throwError: false }
e7aeea18 371 ): void {
60ddad53 372 logger.error(`${chargingStation.logPrefix()} Request command '${commandName}' error:`, error);
07561812 373 if (params?.throwError === true) {
e0a50bcd
JB
374 throw error;
375 }
5e0c67e8
JB
376 }
377
ef6fa3fb 378 // eslint-disable-next-line @typescript-eslint/no-unused-vars
6c1761d4 379 public abstract requestHandler<RequestType extends JsonType, ResponseType extends JsonType>(
08f130a0 380 chargingStation: ChargingStation,
94a464f9 381 commandName: RequestCommand,
5cc4b63b 382 commandParams?: JsonType,
be9b0d50 383 params?: RequestParams
6c1761d4 384 ): Promise<ResponseType>;
c0560973 385}