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