af23a1f4fcbdd55ac4c357dfc67c12e3c35d7e8e
[e-mobility-charging-stations-simulator.git] / src / charging-station / ocpp / OCPPRequestService.ts
1 import {
2 IncomingRequestCommand,
3 RequestCommand,
4 ResponseType,
5 SendParams,
6 } from '../../types/ocpp/Requests';
7
8 import type ChargingStation from '../ChargingStation';
9 import Constants from '../../utils/Constants';
10 import { EmptyObject } from '../../types/EmptyObject';
11 import { ErrorType } from '../../types/ocpp/ErrorType';
12 import { HandleErrorParams } from '../../types/Error';
13 import { JsonType } from '../../types/JsonType';
14 import { MessageType } from '../../types/ocpp/MessageType';
15 import OCPPError from '../../exception/OCPPError';
16 import type OCPPResponseService from './OCPPResponseService';
17 import PerformanceStatistics from '../../performance/PerformanceStatistics';
18 import Utils from '../../utils/Utils';
19 import chalk from 'chalk';
20 import logger from '../../utils/Logger';
21
22 export default abstract class OCPPRequestService {
23 private static readonly instances: Map<string, OCPPRequestService> = new Map<
24 string,
25 OCPPRequestService
26 >();
27
28 protected readonly chargingStation: ChargingStation;
29 private readonly ocppResponseService: OCPPResponseService;
30
31 protected constructor(
32 chargingStation: ChargingStation,
33 ocppResponseService: OCPPResponseService
34 ) {
35 this.chargingStation = chargingStation;
36 this.ocppResponseService = ocppResponseService;
37 this.sendMessageHandler.bind(this);
38 this.sendResult.bind(this);
39 this.sendError.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.hashId)) {
48 OCPPRequestService.instances.set(
49 chargingStation.hashId,
50 new this(chargingStation, ocppResponseService)
51 );
52 }
53 return OCPPRequestService.instances.get(chargingStation.hashId) 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 console.log(chalk`{blue >> Sending message = ${messageToSend}}`);
156 // FIXME: Handle sending error
157 this.chargingStation.wsConnection.send(messageToSend);
158 PerformanceStatistics.endMeasure(commandName, beginId);
159 } else if (!params.skipBufferingOnError) {
160 // Buffer it
161 this.chargingStation.bufferMessage(messageToSend);
162 const ocppError = new OCPPError(
163 ErrorType.GENERIC_ERROR,
164 `WebSocket closed for buffered message id '${messageId}' with content '${messageToSend}'`,
165 commandName,
166 (messagePayload?.details as JsonType) ?? {}
167 );
168 if (messageType === MessageType.CALL_MESSAGE) {
169 // Reject it but keep the request in the cache
170 return reject(ocppError);
171 }
172 return rejectCallback(ocppError, false);
173 } else {
174 // Reject it
175 return rejectCallback(
176 new OCPPError(
177 ErrorType.GENERIC_ERROR,
178 `WebSocket closed for non buffered message id '${messageId}' with content '${messageToSend}'`,
179 commandName,
180 (messagePayload?.details as JsonType) ?? {}
181 ),
182 false
183 );
184 }
185 // Response?
186 if (messageType !== MessageType.CALL_MESSAGE) {
187 // Yes: send Ok
188 return resolve(messagePayload);
189 }
190
191 /**
192 * Function that will receive the request's response
193 *
194 * @param payload
195 * @param requestPayload
196 */
197 async function responseCallback(
198 payload: JsonType | string,
199 requestPayload: JsonType
200 ): Promise<void> {
201 if (self.chargingStation.getEnableStatistics()) {
202 self.chargingStation.performanceStatistics.addRequestStatistic(
203 commandName,
204 MessageType.CALL_RESULT_MESSAGE
205 );
206 }
207 // Handle the request's response
208 try {
209 await self.ocppResponseService.handleResponse(
210 commandName as RequestCommand,
211 payload,
212 requestPayload
213 );
214 resolve(payload);
215 } catch (error) {
216 reject(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 // eslint-disable-next-line @typescript-eslint/no-unused-vars
323 public abstract sendMessageHandler<Request extends JsonType, Response extends JsonType>(
324 commandName: RequestCommand,
325 commandParams?: JsonType,
326 params?: SendParams
327 ): Promise<Response>;
328 }