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