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