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