Apply dependencies update
[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 }
38
39 public static getInstance<T extends OCPPRequestService>(
40 this: new (chargingStation: ChargingStation, ocppResponseService: OCPPResponseService) => T,
41 chargingStation: ChargingStation,
42 ocppResponseService: OCPPResponseService
43 ): T {
44 if (!OCPPRequestService.instances.has(chargingStation.id)) {
45 OCPPRequestService.instances.set(
46 chargingStation.id,
47 new this(chargingStation, ocppResponseService)
48 );
49 }
50 return OCPPRequestService.instances.get(chargingStation.id) as T;
51 }
52
53 public async sendResult(
54 messageId: string,
55 messagePayload: JsonType,
56 commandName: IncomingRequestCommand
57 ): Promise<ResponseType> {
58 try {
59 // Send result message
60 return await this.internalSendMessage(
61 messageId,
62 messagePayload,
63 MessageType.CALL_RESULT_MESSAGE,
64 commandName
65 );
66 } catch (error) {
67 this.handleRequestError(commandName, error as Error);
68 }
69 }
70
71 public async sendError(
72 messageId: string,
73 ocppError: OCPPError,
74 commandName: IncomingRequestCommand
75 ): Promise<ResponseType> {
76 try {
77 // Send error message
78 return await this.internalSendMessage(
79 messageId,
80 ocppError,
81 MessageType.CALL_ERROR_MESSAGE,
82 commandName
83 );
84 } catch (error) {
85 this.handleRequestError(commandName, error as Error);
86 }
87 }
88
89 protected async sendMessage(
90 messageId: string,
91 messagePayload: JsonType,
92 commandName: RequestCommand,
93 params: SendParams = {
94 skipBufferingOnError: false,
95 triggerMessage: false,
96 }
97 ): Promise<ResponseType> {
98 try {
99 return await this.internalSendMessage(
100 messageId,
101 messagePayload,
102 MessageType.CALL_MESSAGE,
103 commandName,
104 params
105 );
106 } catch (error) {
107 this.handleRequestError(commandName, error as Error, { throwError: false });
108 }
109 }
110
111 private async internalSendMessage(
112 messageId: string,
113 messagePayload: JsonType | OCPPError,
114 messageType: MessageType,
115 commandName?: RequestCommand | IncomingRequestCommand,
116 params: SendParams = {
117 skipBufferingOnError: false,
118 triggerMessage: false,
119 }
120 ): Promise<ResponseType> {
121 if (
122 (this.chargingStation.isInUnknownState() &&
123 commandName === RequestCommand.BOOT_NOTIFICATION) ||
124 (!this.chargingStation.getOcppStrictCompliance() &&
125 this.chargingStation.isInUnknownState()) ||
126 this.chargingStation.isInAcceptedState() ||
127 (this.chargingStation.isInPendingState() && params.triggerMessage)
128 ) {
129 // eslint-disable-next-line @typescript-eslint/no-this-alias
130 const self = this;
131 // Send a message through wsConnection
132 return Utils.promiseWithTimeout(
133 new Promise((resolve, reject) => {
134 const messageToSend = this.buildMessageToSend(
135 messageId,
136 messagePayload,
137 messageType,
138 commandName,
139 responseCallback,
140 rejectCallback
141 );
142 if (this.chargingStation.getEnableStatistics()) {
143 this.chargingStation.performanceStatistics.addRequestStatistic(
144 commandName,
145 messageType
146 );
147 }
148 // Check if wsConnection opened
149 if (this.chargingStation.isWebSocketConnectionOpened()) {
150 // Yes: Send Message
151 const beginId = PerformanceStatistics.beginMeasure(commandName);
152 // FIXME: Handle sending error
153 this.chargingStation.wsConnection.send(messageToSend);
154 PerformanceStatistics.endMeasure(commandName, beginId);
155 } else if (!params.skipBufferingOnError) {
156 // Buffer it
157 this.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?.details as JsonType) ?? {}
163 );
164 if (messageType === MessageType.CALL_MESSAGE) {
165 // Reject it but keep the request in the cache
166 return reject(ocppError);
167 }
168 return rejectCallback(ocppError, false);
169 } else {
170 // Reject it
171 return rejectCallback(
172 new OCPPError(
173 ErrorType.GENERIC_ERROR,
174 `WebSocket closed for non buffered message id '${messageId}' with content '${messageToSend}'`,
175 commandName,
176 (messagePayload?.details as JsonType) ?? {}
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 | string,
195 requestPayload: JsonType
196 ): Promise<void> {
197 if (self.chargingStation.getEnableStatistics()) {
198 self.chargingStation.performanceStatistics.addRequestStatistic(
199 commandName,
200 MessageType.CALL_RESULT_MESSAGE
201 );
202 }
203 // Handle the request's response
204 try {
205 await self.ocppResponseService.handleResponse(
206 commandName as RequestCommand,
207 payload,
208 requestPayload
209 );
210 resolve(payload);
211 } catch (error) {
212 reject(error);
213 } finally {
214 self.chargingStation.requests.delete(messageId);
215 }
216 }
217
218 /**
219 * Function that will receive the request's error response
220 *
221 * @param error
222 * @param requestStatistic
223 */
224 function rejectCallback(error: OCPPError, requestStatistic = true): void {
225 if (requestStatistic && self.chargingStation.getEnableStatistics()) {
226 self.chargingStation.performanceStatistics.addRequestStatistic(
227 commandName,
228 MessageType.CALL_ERROR_MESSAGE
229 );
230 }
231 logger.error(
232 `${self.chargingStation.logPrefix()} Error %j occurred when calling command %s with message data %j`,
233 error,
234 commandName,
235 messagePayload
236 );
237 self.chargingStation.requests.delete(messageId);
238 reject(error);
239 }
240 }),
241 Constants.OCPP_WEBSOCKET_TIMEOUT,
242 new OCPPError(
243 ErrorType.GENERIC_ERROR,
244 `Timeout for message id '${messageId}'`,
245 commandName,
246 (messagePayload?.details as JsonType) ?? {}
247 ),
248 () => {
249 messageType === MessageType.CALL_MESSAGE &&
250 this.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 ${this.chargingStation.getRegistrationStatus()} state on the central server`,
257 commandName
258 );
259 }
260
261 private buildMessageToSend(
262 messageId: string,
263 messagePayload: JsonType | OCPPError,
264 messageType: MessageType,
265 commandName?: RequestCommand | IncomingRequestCommand,
266 responseCallback?: (payload: JsonType | string, requestPayload: JsonType) => Promise<void>,
267 rejectCallback?: (error: OCPPError, requestStatistic?: boolean) => void
268 ): string {
269 let messageToSend: string;
270 // Type of message
271 switch (messageType) {
272 // Request
273 case MessageType.CALL_MESSAGE:
274 // Build request
275 this.chargingStation.requests.set(messageId, [
276 responseCallback,
277 rejectCallback,
278 commandName,
279 messagePayload,
280 ]);
281 messageToSend = JSON.stringify([messageType, messageId, commandName, messagePayload]);
282 break;
283 // Response
284 case MessageType.CALL_RESULT_MESSAGE:
285 // Build response
286 messageToSend = JSON.stringify([messageType, messageId, messagePayload]);
287 break;
288 // Error Message
289 case MessageType.CALL_ERROR_MESSAGE:
290 // Build Error Message
291 messageToSend = JSON.stringify([
292 messageType,
293 messageId,
294 messagePayload?.code ?? ErrorType.GENERIC_ERROR,
295 messagePayload?.message ?? '',
296 messagePayload?.details ?? { commandName },
297 ]);
298 break;
299 }
300 return messageToSend;
301 }
302
303 private handleRequestError(
304 commandName: RequestCommand | IncomingRequestCommand,
305 error: Error,
306 params: HandleErrorParams<EmptyObject> = { throwError: true }
307 ): void {
308 logger.error(
309 this.chargingStation.logPrefix() + ' Request command %s error: %j',
310 commandName,
311 error
312 );
313 if (params?.throwError) {
314 throw error;
315 }
316 }
317
318 public abstract sendMessageHandler(
319 commandName: RequestCommand,
320 commandParams?: JsonType,
321 params?: SendParams
322 ): Promise<ResponseType>;
323 }