Strong type sendMessageHandler response with generics
[e-mobility-charging-stations-simulator.git] / src / charging-station / ocpp / OCPPRequestService.ts
CommitLineData
e7aeea18 1import {
e7aeea18
JB
2 IncomingRequestCommand,
3 RequestCommand,
4 ResponseType,
5 SendParams,
6} from '../../types/ocpp/Requests';
c0560973 7
9f2e3130 8import type ChargingStation from '../ChargingStation';
c0560973 9import Constants from '../../utils/Constants';
47086c3a 10import { EmptyObject } from '../../types/EmptyObject';
c0560973 11import { ErrorType } from '../../types/ocpp/ErrorType';
e0a50bcd 12import { HandleErrorParams } from '../../types/Error';
d1888640 13import { JsonType } from '../../types/JsonType';
c0560973 14import { MessageType } from '../../types/ocpp/MessageType';
e58068fd 15import OCPPError from '../../exception/OCPPError';
9f2e3130 16import type OCPPResponseService from './OCPPResponseService';
a6b3c6c3 17import PerformanceStatistics from '../../performance/PerformanceStatistics';
6d9abcc2 18import Utils from '../../utils/Utils';
9f2e3130 19import logger from '../../utils/Logger';
c0560973
JB
20
21export default abstract class OCPPRequestService {
e7aeea18
JB
22 private static readonly instances: Map<string, OCPPRequestService> = new Map<
23 string,
24 OCPPRequestService
25 >();
10068088 26
9f2e3130
JB
27 protected readonly chargingStation: ChargingStation;
28 private readonly ocppResponseService: OCPPResponseService;
c0560973 29
e7aeea18
JB
30 protected constructor(
31 chargingStation: ChargingStation,
32 ocppResponseService: OCPPResponseService
33 ) {
c0560973
JB
34 this.chargingStation = chargingStation;
35 this.ocppResponseService = ocppResponseService;
94a464f9 36 this.sendMessageHandler.bind(this);
c0560973
JB
37 }
38
e7aeea18
JB
39 public static getInstance<T extends OCPPRequestService>(
40 this: new (chargingStation: ChargingStation, ocppResponseService: OCPPResponseService) => T,
41 chargingStation: ChargingStation,
42 ocppResponseService: OCPPResponseService
43 ): T {
3f94cab5 44 if (!OCPPRequestService.instances.has(chargingStation.hashId)) {
e7aeea18 45 OCPPRequestService.instances.set(
3f94cab5 46 chargingStation.hashId,
e7aeea18
JB
47 new this(chargingStation, ocppResponseService)
48 );
9f2e3130 49 }
3f94cab5 50 return OCPPRequestService.instances.get(chargingStation.hashId) as T;
9f2e3130
JB
51 }
52
e7aeea18
JB
53 public async sendResult(
54 messageId: string,
55 messagePayload: JsonType,
56 commandName: IncomingRequestCommand
57 ): Promise<ResponseType> {
5e0c67e8
JB
58 try {
59 // Send result message
e7aeea18
JB
60 return await this.internalSendMessage(
61 messageId,
62 messagePayload,
63 MessageType.CALL_RESULT_MESSAGE,
64 commandName
65 );
5e0c67e8
JB
66 } catch (error) {
67 this.handleRequestError(commandName, error as Error);
68 }
69 }
70
e7aeea18
JB
71 public async sendError(
72 messageId: string,
73 ocppError: OCPPError,
74 commandName: IncomingRequestCommand
75 ): Promise<ResponseType> {
5e0c67e8
JB
76 try {
77 // Send error message
e7aeea18
JB
78 return await this.internalSendMessage(
79 messageId,
80 ocppError,
81 MessageType.CALL_ERROR_MESSAGE,
82 commandName
83 );
5e0c67e8
JB
84 } catch (error) {
85 this.handleRequestError(commandName, error as Error);
86 }
87 }
88
e7aeea18
JB
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> {
5e0c67e8 98 try {
e7aeea18
JB
99 return await this.internalSendMessage(
100 messageId,
101 messagePayload,
102 MessageType.CALL_MESSAGE,
103 commandName,
104 params
105 );
5e0c67e8 106 } catch (error) {
e0a50bcd 107 this.handleRequestError(commandName, error as Error, { throwError: false });
5e0c67e8
JB
108 }
109 }
110
e7aeea18
JB
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 ) {
caad9d6b
JB
129 // eslint-disable-next-line @typescript-eslint/no-this-alias
130 const self = this;
131 // Send a message through wsConnection
e7aeea18
JB
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 );
caad9d6b 147 }
e7aeea18
JB
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 );
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(
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);
e7aeea18
JB
213 } finally {
214 self.chargingStation.requests.delete(messageId);
215 }
caad9d6b 216 }
caad9d6b 217
e7aeea18
JB
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);
caad9d6b 239 }
e7aeea18
JB
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);
caad9d6b 251 }
e7aeea18 252 );
caad9d6b 253 }
e7aeea18
JB
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 );
c0560973
JB
259 }
260
e7aeea18
JB
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 {
e7accadb
JB
269 let messageToSend: string;
270 // Type of message
271 switch (messageType) {
272 // Request
273 case MessageType.CALL_MESSAGE:
274 // Build request
e7aeea18
JB
275 this.chargingStation.requests.set(messageId, [
276 responseCallback,
277 rejectCallback,
278 commandName,
279 messagePayload,
280 ]);
5e0c67e8 281 messageToSend = JSON.stringify([messageType, messageId, commandName, messagePayload]);
e7accadb
JB
282 break;
283 // Response
284 case MessageType.CALL_RESULT_MESSAGE:
285 // Build response
5e0c67e8 286 messageToSend = JSON.stringify([messageType, messageId, messagePayload]);
e7accadb
JB
287 break;
288 // Error Message
289 case MessageType.CALL_ERROR_MESSAGE:
290 // Build Error Message
e7aeea18
JB
291 messageToSend = JSON.stringify([
292 messageType,
293 messageId,
294 messagePayload?.code ?? ErrorType.GENERIC_ERROR,
295 messagePayload?.message ?? '',
296 messagePayload?.details ?? { commandName },
297 ]);
e7accadb
JB
298 break;
299 }
300 return messageToSend;
301 }
302
e7aeea18
JB
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 );
e0a50bcd
JB
313 if (params?.throwError) {
314 throw error;
315 }
5e0c67e8
JB
316 }
317
f22266fd 318 public abstract sendMessageHandler<Response extends JsonType>(
94a464f9
JB
319 commandName: RequestCommand,
320 commandParams?: JsonType,
321 params?: SendParams
f22266fd 322 ): Promise<Response>;
c0560973 323}