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