Convert sendAuthorize to OCPP message sending handler
[e-mobility-charging-stations-simulator.git] / src / charging-station / ocpp / OCPPRequestService.ts
1 import {
2 DiagnosticsStatus,
3 IncomingRequestCommand,
4 RequestCommand,
5 ResponseType,
6 SendParams,
7 } from '../../types/ocpp/Requests';
8 import {
9 StartTransactionResponse,
10 StopTransactionReason,
11 StopTransactionResponse,
12 } from '../../types/ocpp/Transaction';
13
14 import type ChargingStation from '../ChargingStation';
15 import Constants from '../../utils/Constants';
16 import { EmptyObject } from '../../types/EmptyObject';
17 import { ErrorType } from '../../types/ocpp/ErrorType';
18 import { HandleErrorParams } from '../../types/Error';
19 import { JsonType } from '../../types/JsonType';
20 import { MessageType } from '../../types/ocpp/MessageType';
21 import { MeterValue } from '../../types/ocpp/MeterValues';
22 import OCPPError from '../../exception/OCPPError';
23 import type OCPPResponseService from './OCPPResponseService';
24 import PerformanceStatistics from '../../performance/PerformanceStatistics';
25 import Utils from '../../utils/Utils';
26 import logger from '../../utils/Logger';
27
28 export default abstract class OCPPRequestService {
29 private static readonly instances: Map<string, OCPPRequestService> = new Map<
30 string,
31 OCPPRequestService
32 >();
33
34 protected readonly chargingStation: ChargingStation;
35 private readonly ocppResponseService: OCPPResponseService;
36
37 protected constructor(
38 chargingStation: ChargingStation,
39 ocppResponseService: OCPPResponseService
40 ) {
41 this.chargingStation = chargingStation;
42 this.ocppResponseService = ocppResponseService;
43 this.sendMessageHandler.bind(this);
44 }
45
46 public static getInstance<T extends OCPPRequestService>(
47 this: new (chargingStation: ChargingStation, ocppResponseService: OCPPResponseService) => T,
48 chargingStation: ChargingStation,
49 ocppResponseService: OCPPResponseService
50 ): T {
51 if (!OCPPRequestService.instances.has(chargingStation.id)) {
52 OCPPRequestService.instances.set(
53 chargingStation.id,
54 new this(chargingStation, ocppResponseService)
55 );
56 }
57 return OCPPRequestService.instances.get(chargingStation.id) as T;
58 }
59
60 public async sendResult(
61 messageId: string,
62 messagePayload: JsonType,
63 commandName: IncomingRequestCommand
64 ): Promise<ResponseType> {
65 try {
66 // Send result message
67 return await this.internalSendMessage(
68 messageId,
69 messagePayload,
70 MessageType.CALL_RESULT_MESSAGE,
71 commandName
72 );
73 } catch (error) {
74 this.handleRequestError(commandName, error as Error);
75 }
76 }
77
78 public async sendError(
79 messageId: string,
80 ocppError: OCPPError,
81 commandName: IncomingRequestCommand
82 ): Promise<ResponseType> {
83 try {
84 // Send error message
85 return await this.internalSendMessage(
86 messageId,
87 ocppError,
88 MessageType.CALL_ERROR_MESSAGE,
89 commandName
90 );
91 } catch (error) {
92 this.handleRequestError(commandName, error as Error);
93 }
94 }
95
96 protected async sendMessage(
97 messageId: string,
98 messagePayload: JsonType,
99 commandName: RequestCommand,
100 params: SendParams = {
101 skipBufferingOnError: false,
102 triggerMessage: false,
103 }
104 ): Promise<ResponseType> {
105 try {
106 return await this.internalSendMessage(
107 messageId,
108 messagePayload,
109 MessageType.CALL_MESSAGE,
110 commandName,
111 params
112 );
113 } catch (error) {
114 this.handleRequestError(commandName, error as Error, { throwError: false });
115 }
116 }
117
118 private async internalSendMessage(
119 messageId: string,
120 messagePayload: JsonType | OCPPError,
121 messageType: MessageType,
122 commandName?: RequestCommand | IncomingRequestCommand,
123 params: SendParams = {
124 skipBufferingOnError: false,
125 triggerMessage: false,
126 }
127 ): Promise<ResponseType> {
128 if (
129 (this.chargingStation.isInUnknownState() &&
130 commandName === RequestCommand.BOOT_NOTIFICATION) ||
131 (!this.chargingStation.getOcppStrictCompliance() &&
132 this.chargingStation.isInUnknownState()) ||
133 this.chargingStation.isInAcceptedState() ||
134 (this.chargingStation.isInPendingState() && params.triggerMessage)
135 ) {
136 // eslint-disable-next-line @typescript-eslint/no-this-alias
137 const self = this;
138 // Send a message through wsConnection
139 return Utils.promiseWithTimeout(
140 new Promise((resolve, reject) => {
141 const messageToSend = this.buildMessageToSend(
142 messageId,
143 messagePayload,
144 messageType,
145 commandName,
146 responseCallback,
147 rejectCallback
148 );
149 if (this.chargingStation.getEnableStatistics()) {
150 this.chargingStation.performanceStatistics.addRequestStatistic(
151 commandName,
152 messageType
153 );
154 }
155 // Check if wsConnection opened
156 if (this.chargingStation.isWebSocketConnectionOpened()) {
157 // Yes: Send Message
158 const beginId = PerformanceStatistics.beginMeasure(commandName);
159 // FIXME: Handle sending error
160 this.chargingStation.wsConnection.send(messageToSend);
161 PerformanceStatistics.endMeasure(commandName, beginId);
162 } else if (!params.skipBufferingOnError) {
163 // Buffer it
164 this.chargingStation.bufferMessage(messageToSend);
165 const ocppError = new OCPPError(
166 ErrorType.GENERIC_ERROR,
167 `WebSocket closed for buffered message id '${messageId}' with content '${messageToSend}'`,
168 commandName,
169 (messagePayload?.details as JsonType) ?? {}
170 );
171 if (messageType === MessageType.CALL_MESSAGE) {
172 // Reject it but keep the request in the cache
173 return reject(ocppError);
174 }
175 return rejectCallback(ocppError, false);
176 } else {
177 // Reject it
178 return rejectCallback(
179 new OCPPError(
180 ErrorType.GENERIC_ERROR,
181 `WebSocket closed for non buffered message id '${messageId}' with content '${messageToSend}'`,
182 commandName,
183 (messagePayload?.details as JsonType) ?? {}
184 ),
185 false
186 );
187 }
188 // Response?
189 if (messageType !== MessageType.CALL_MESSAGE) {
190 // Yes: send Ok
191 return resolve(messagePayload);
192 }
193
194 /**
195 * Function that will receive the request's response
196 *
197 * @param payload
198 * @param requestPayload
199 */
200 async function responseCallback(
201 payload: JsonType | string,
202 requestPayload: JsonType
203 ): Promise<void> {
204 if (self.chargingStation.getEnableStatistics()) {
205 self.chargingStation.performanceStatistics.addRequestStatistic(
206 commandName,
207 MessageType.CALL_RESULT_MESSAGE
208 );
209 }
210 // Handle the request's response
211 try {
212 await self.ocppResponseService.handleResponse(
213 commandName as RequestCommand,
214 payload,
215 requestPayload
216 );
217 resolve(payload);
218 } catch (error) {
219 reject(error);
220 throw error;
221 } finally {
222 self.chargingStation.requests.delete(messageId);
223 }
224 }
225
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);
247 }
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);
259 }
260 );
261 }
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 );
267 }
268
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 {
277 let messageToSend: string;
278 // Type of message
279 switch (messageType) {
280 // Request
281 case MessageType.CALL_MESSAGE:
282 // Build request
283 this.chargingStation.requests.set(messageId, [
284 responseCallback,
285 rejectCallback,
286 commandName,
287 messagePayload,
288 ]);
289 messageToSend = JSON.stringify([messageType, messageId, commandName, messagePayload]);
290 break;
291 // Response
292 case MessageType.CALL_RESULT_MESSAGE:
293 // Build response
294 messageToSend = JSON.stringify([messageType, messageId, messagePayload]);
295 break;
296 // Error Message
297 case MessageType.CALL_ERROR_MESSAGE:
298 // Build Error Message
299 messageToSend = JSON.stringify([
300 messageType,
301 messageId,
302 messagePayload?.code ?? ErrorType.GENERIC_ERROR,
303 messagePayload?.message ?? '',
304 messagePayload?.details ?? { commandName },
305 ]);
306 break;
307 }
308 return messageToSend;
309 }
310
311 private handleRequestError(
312 commandName: RequestCommand | IncomingRequestCommand,
313 error: Error,
314 params: HandleErrorParams<EmptyObject> = { throwError: true }
315 ): void {
316 logger.error(
317 this.chargingStation.logPrefix() + ' Request command %s error: %j',
318 commandName,
319 error
320 );
321 if (params?.throwError) {
322 throw error;
323 }
324 }
325
326 public abstract sendMessageHandler(
327 commandName: RequestCommand,
328 commandParams?: JsonType,
329 params?: SendParams
330 ): Promise<ResponseType>;
331
332 public abstract sendStartTransaction(
333 connectorId: number,
334 idTag?: string
335 ): Promise<StartTransactionResponse>;
336
337 public abstract sendStopTransaction(
338 transactionId: number,
339 meterStop: number,
340 idTag?: string,
341 reason?: StopTransactionReason
342 ): Promise<StopTransactionResponse>;
343
344 public abstract sendMeterValues(
345 connectorId: number,
346 transactionId: number,
347 interval: number
348 ): Promise<void>;
349
350 public abstract sendTransactionBeginMeterValues(
351 connectorId: number,
352 transactionId: number,
353 beginMeterValue: MeterValue
354 ): Promise<void>;
355
356 public abstract sendTransactionEndMeterValues(
357 connectorId: number,
358 transactionId: number,
359 endMeterValue: MeterValue
360 ): Promise<void>;
361
362 public abstract sendDiagnosticsStatusNotification(
363 diagnosticsStatus: DiagnosticsStatus
364 ): Promise<void>;
365 }