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