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