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