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