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