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