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