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