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