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