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