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