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