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