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