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