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