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