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