Apply dependencies update
[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 (
3a13fc92
JB
162 (chargingStation.isInUnknownState() === true &&
163 commandName === RequestCommand.BOOT_NOTIFICATION) ||
164 (chargingStation.getOcppStrictCompliance() === false &&
165 chargingStation.isInUnknownState() === true) ||
166 chargingStation.isInAcceptedState() === true ||
167 (chargingStation.isInPendingState() === true &&
168 (params.triggerMessage === true || messageType === MessageType.CALL_RESULT_MESSAGE))
e7aeea18 169 ) {
caad9d6b
JB
170 // eslint-disable-next-line @typescript-eslint/no-this-alias
171 const self = this;
172 // Send a message through wsConnection
e7aeea18
JB
173 return Utils.promiseWithTimeout(
174 new Promise((resolve, reject) => {
175 const messageToSend = this.buildMessageToSend(
08f130a0 176 chargingStation,
e7aeea18
JB
177 messageId,
178 messagePayload,
179 messageType,
180 commandName,
181 responseCallback,
a2d1c0f1 182 errorCallback
e7aeea18 183 );
0638ddd2 184 if (chargingStation.getEnableStatistics() === true) {
08f130a0 185 chargingStation.performanceStatistics.addRequestStatistic(commandName, messageType);
caad9d6b 186 }
e7aeea18 187 // Check if wsConnection opened
0638ddd2 188 if (chargingStation.isWebSocketConnectionOpened() === true) {
e7aeea18
JB
189 // Yes: Send Message
190 const beginId = PerformanceStatistics.beginMeasure(commandName);
191 // FIXME: Handle sending error
08f130a0 192 chargingStation.wsConnection.send(messageToSend);
e7aeea18 193 PerformanceStatistics.endMeasure(commandName, beginId);
6f35d2da 194 logger.debug(
08f130a0 195 `${chargingStation.logPrefix()} >> Command '${commandName}' sent ${this.getMessageTypeString(
9a3b8d9f
JB
196 messageType
197 )} payload: ${messageToSend}`
6f35d2da 198 );
0638ddd2 199 } else if (params.skipBufferingOnError === false) {
e7aeea18 200 // Buffer it
08f130a0 201 chargingStation.bufferMessage(messageToSend);
e7aeea18
JB
202 const ocppError = new OCPPError(
203 ErrorType.GENERIC_ERROR,
204 `WebSocket closed for buffered message id '${messageId}' with content '${messageToSend}'`,
205 commandName,
5cc4b63b 206 (messagePayload as JsonObject)?.details ?? {}
e7aeea18
JB
207 );
208 if (messageType === MessageType.CALL_MESSAGE) {
209 // Reject it but keep the request in the cache
210 return reject(ocppError);
211 }
a2d1c0f1 212 return errorCallback(ocppError, false);
e7aeea18
JB
213 } else {
214 // Reject it
a2d1c0f1 215 return errorCallback(
e7aeea18
JB
216 new OCPPError(
217 ErrorType.GENERIC_ERROR,
218 `WebSocket closed for non buffered message id '${messageId}' with content '${messageToSend}'`,
219 commandName,
5cc4b63b 220 (messagePayload as JsonObject)?.details ?? {}
e7aeea18
JB
221 ),
222 false
223 );
caad9d6b 224 }
e7aeea18
JB
225 // Response?
226 if (messageType !== MessageType.CALL_MESSAGE) {
227 // Yes: send Ok
228 return resolve(messagePayload);
229 }
230
231 /**
232 * Function that will receive the request's response
233 *
234 * @param payload
235 * @param requestPayload
236 */
237 async function responseCallback(
5cc4b63b
JB
238 payload: JsonType,
239 requestPayload: JsonType
e7aeea18 240 ): Promise<void> {
0638ddd2 241 if (chargingStation.getEnableStatistics() === true) {
08f130a0 242 chargingStation.performanceStatistics.addRequestStatistic(
e7aeea18
JB
243 commandName,
244 MessageType.CALL_RESULT_MESSAGE
245 );
246 }
247 // Handle the request's response
248 try {
f7f98c68 249 await self.ocppResponseService.responseHandler(
08f130a0 250 chargingStation,
e7aeea18
JB
251 commandName as RequestCommand,
252 payload,
253 requestPayload
254 );
255 resolve(payload);
256 } catch (error) {
257 reject(error);
e7aeea18 258 } finally {
08f130a0 259 chargingStation.requests.delete(messageId);
e7aeea18 260 }
caad9d6b 261 }
caad9d6b 262
e7aeea18
JB
263 /**
264 * Function that will receive the request's error response
265 *
266 * @param error
267 * @param requestStatistic
268 */
a2d1c0f1 269 function errorCallback(error: OCPPError, requestStatistic = true): void {
0afed85f 270 if (requestStatistic === true && chargingStation.getEnableStatistics() === true) {
08f130a0 271 chargingStation.performanceStatistics.addRequestStatistic(
e7aeea18
JB
272 commandName,
273 MessageType.CALL_ERROR_MESSAGE
274 );
275 }
276 logger.error(
fc040c43
JB
277 `${chargingStation.logPrefix()} Error occurred when calling command ${commandName} with message data ${JSON.stringify(
278 messagePayload
279 )}:`,
280 error
e7aeea18 281 );
08f130a0 282 chargingStation.requests.delete(messageId);
e7aeea18 283 reject(error);
caad9d6b 284 }
e7aeea18
JB
285 }),
286 Constants.OCPP_WEBSOCKET_TIMEOUT,
287 new OCPPError(
288 ErrorType.GENERIC_ERROR,
289 `Timeout for message id '${messageId}'`,
290 commandName,
5cc4b63b 291 (messagePayload as JsonObject)?.details ?? {}
e7aeea18
JB
292 ),
293 () => {
08f130a0 294 messageType === MessageType.CALL_MESSAGE && chargingStation.requests.delete(messageId);
caad9d6b 295 }
e7aeea18 296 );
caad9d6b 297 }
e7aeea18
JB
298 throw new OCPPError(
299 ErrorType.SECURITY_ERROR,
e3018bc4 300 `Cannot send command ${commandName} PDU when the charging station is in ${chargingStation.getRegistrationStatus()} state on the central server`,
e7aeea18
JB
301 commandName
302 );
c0560973
JB
303 }
304
e7aeea18 305 private buildMessageToSend(
08f130a0 306 chargingStation: ChargingStation,
e7aeea18 307 messageId: string,
5cc4b63b 308 messagePayload: JsonType | OCPPError,
e7aeea18
JB
309 messageType: MessageType,
310 commandName?: RequestCommand | IncomingRequestCommand,
5cc4b63b 311 responseCallback?: (payload: JsonType, requestPayload: JsonType) => Promise<void>,
a2d1c0f1 312 errorCallback?: (error: OCPPError, requestStatistic?: boolean) => void
e7aeea18 313 ): string {
e7accadb
JB
314 let messageToSend: string;
315 // Type of message
316 switch (messageType) {
317 // Request
318 case MessageType.CALL_MESSAGE:
319 // Build request
08f130a0 320 chargingStation.requests.set(messageId, [
e7aeea18 321 responseCallback,
a2d1c0f1 322 errorCallback,
e7aeea18 323 commandName,
5cc4b63b 324 messagePayload as JsonType,
e7aeea18 325 ]);
b3ec7bc1
JB
326 messageToSend = JSON.stringify([
327 messageType,
328 messageId,
329 commandName,
330 messagePayload,
331 ] as OutgoingRequest);
e7accadb
JB
332 break;
333 // Response
334 case MessageType.CALL_RESULT_MESSAGE:
335 // Build response
b3ec7bc1 336 messageToSend = JSON.stringify([messageType, messageId, messagePayload] as Response);
e7accadb
JB
337 break;
338 // Error Message
339 case MessageType.CALL_ERROR_MESSAGE:
340 // Build Error Message
e7aeea18
JB
341 messageToSend = JSON.stringify([
342 messageType,
343 messageId,
b3ec7bc1
JB
344 (messagePayload as OCPPError)?.code ?? ErrorType.GENERIC_ERROR,
345 (messagePayload as OCPPError)?.message ?? '',
346 (messagePayload as OCPPError)?.details ?? { commandName },
347 ] as ErrorResponse);
e7accadb
JB
348 break;
349 }
350 return messageToSend;
351 }
352
9a3b8d9f
JB
353 private getMessageTypeString(messageType: MessageType): string {
354 switch (messageType) {
355 case MessageType.CALL_MESSAGE:
356 return 'request';
357 case MessageType.CALL_RESULT_MESSAGE:
358 return 'response';
359 case MessageType.CALL_ERROR_MESSAGE:
360 return 'error';
361 }
362 }
363
dc922667 364 private handleSendMessageError(
08f130a0 365 chargingStation: ChargingStation,
e7aeea18
JB
366 commandName: RequestCommand | IncomingRequestCommand,
367 error: Error,
07561812 368 params: HandleErrorParams<EmptyObject> = { throwError: false }
e7aeea18 369 ): void {
60ddad53 370 logger.error(`${chargingStation.logPrefix()} Request command '${commandName}' error:`, error);
07561812 371 if (params?.throwError === true) {
e0a50bcd
JB
372 throw error;
373 }
5e0c67e8
JB
374 }
375
ef6fa3fb 376 // eslint-disable-next-line @typescript-eslint/no-unused-vars
6c1761d4 377 public abstract requestHandler<RequestType extends JsonType, ResponseType extends JsonType>(
08f130a0 378 chargingStation: ChargingStation,
94a464f9 379 commandName: RequestCommand,
5cc4b63b 380 commandParams?: JsonType,
be9b0d50 381 params?: RequestParams
6c1761d4 382 ): Promise<ResponseType>;
c0560973 383}