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