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