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