db3ce5e02daf514d45ed1881fb33070e01f1c722
[e-mobility-charging-stations-simulator.git] / src / charging-station / ocpp / OCPPRequestService.ts
1 import { AuthorizeResponse, StartTransactionResponse, StopTransactionReason, StopTransactionResponse } from '../../types/ocpp/Transaction';
2 import { DiagnosticsStatus, IncomingRequestCommand, RequestCommand, ResponseType, SendParams } from '../../types/ocpp/Requests';
3
4 import { BootNotificationResponse } from '../../types/ocpp/Responses';
5 import { ChargePointErrorCode } from '../../types/ocpp/ChargePointErrorCode';
6 import { ChargePointStatus } from '../../types/ocpp/ChargePointStatus';
7 import type ChargingStation from '../ChargingStation';
8 import Constants from '../../utils/Constants';
9 import { ErrorType } from '../../types/ocpp/ErrorType';
10 import { HandleErrorParams } from '../../types/Error';
11 import { JsonType } from '../../types/JsonType';
12 import { MessageType } from '../../types/ocpp/MessageType';
13 import { MeterValue } from '../../types/ocpp/MeterValues';
14 import OCPPError from '../../exception/OCPPError';
15 import type OCPPResponseService from './OCPPResponseService';
16 import PerformanceStatistics from '../../performance/PerformanceStatistics';
17 import Utils from '../../utils/Utils';
18 import logger from '../../utils/Logger';
19
20 export default abstract class OCPPRequestService {
21 private static readonly instances: Map<string, OCPPRequestService> = new Map<string, OCPPRequestService>();
22 protected readonly chargingStation: ChargingStation;
23 private readonly ocppResponseService: OCPPResponseService;
24
25 protected constructor(chargingStation: ChargingStation, ocppResponseService: OCPPResponseService) {
26 this.chargingStation = chargingStation;
27 this.ocppResponseService = ocppResponseService;
28 }
29
30 public static getInstance<T extends OCPPRequestService>(this: new (chargingStation: ChargingStation, ocppResponseService: OCPPResponseService) => T, chargingStation: ChargingStation, ocppResponseService: OCPPResponseService): T {
31 if (!OCPPRequestService.instances.has(chargingStation.id)) {
32 OCPPRequestService.instances.set(chargingStation.id, new this(chargingStation, ocppResponseService));
33 }
34 return OCPPRequestService.instances.get(chargingStation.id) as T;
35 }
36
37 public async sendResult(messageId: string, messagePayload: JsonType, commandName: IncomingRequestCommand): Promise<ResponseType> {
38 try {
39 // Send result message
40 return await this.internalSendMessage(messageId, messagePayload, MessageType.CALL_RESULT_MESSAGE, commandName);
41 } catch (error) {
42 this.handleRequestError(commandName, error as Error);
43 }
44 }
45
46 public async sendError(messageId: string, ocppError: OCPPError, commandName: IncomingRequestCommand): Promise<ResponseType> {
47 try {
48 // Send error message
49 return await this.internalSendMessage(messageId, ocppError, MessageType.CALL_ERROR_MESSAGE, commandName);
50 } catch (error) {
51 this.handleRequestError(commandName, error as Error);
52 }
53 }
54
55 protected async sendMessage(messageId: string, messagePayload: JsonType, commandName: RequestCommand, params: SendParams = {
56 skipBufferingOnError: false,
57 triggerMessage: false
58 }): Promise<ResponseType> {
59 try {
60 return await this.internalSendMessage(messageId, messagePayload, MessageType.CALL_MESSAGE, commandName, params);
61 } catch (error) {
62 this.handleRequestError(commandName, error as Error, { throwError: false });
63 }
64 }
65
66 private async internalSendMessage(messageId: string, messagePayload: JsonType | OCPPError, messageType: MessageType, commandName?: RequestCommand | IncomingRequestCommand,
67 params: SendParams = {
68 skipBufferingOnError: false,
69 triggerMessage: false
70 }): Promise<ResponseType> {
71 if ((this.chargingStation.isInUnknownState() && commandName === RequestCommand.BOOT_NOTIFICATION)
72 || (!this.chargingStation.getOcppStrictCompliance() && this.chargingStation.isInUnknownState())
73 || this.chargingStation.isInAcceptedState() || (this.chargingStation.isInPendingState() && params.triggerMessage)) {
74 // eslint-disable-next-line @typescript-eslint/no-this-alias
75 const self = this;
76 // Send a message through wsConnection
77 return Utils.promiseWithTimeout(new Promise((resolve, reject) => {
78 const messageToSend = this.buildMessageToSend(messageId, messagePayload, messageType, commandName, responseCallback, rejectCallback);
79 if (this.chargingStation.getEnableStatistics()) {
80 this.chargingStation.performanceStatistics.addRequestStatistic(commandName, messageType);
81 }
82 // Check if wsConnection opened
83 if (this.chargingStation.isWebSocketConnectionOpened()) {
84 // Yes: Send Message
85 const beginId = PerformanceStatistics.beginMeasure(commandName);
86 // FIXME: Handle sending error
87 this.chargingStation.wsConnection.send(messageToSend);
88 PerformanceStatistics.endMeasure(commandName, beginId);
89 } else if (!params.skipBufferingOnError) {
90 // Buffer it
91 this.chargingStation.bufferMessage(messageToSend);
92 const ocppError = new OCPPError(ErrorType.GENERIC_ERROR, `WebSocket closed for buffered message id '${messageId}' with content '${messageToSend}'`, commandName, messagePayload?.details as JsonType ?? {});
93 if (messageType === MessageType.CALL_MESSAGE) {
94 // Reject it but keep the request in the cache
95 return reject(ocppError);
96 }
97 return rejectCallback(ocppError, false);
98 } else {
99 // Reject it
100 return rejectCallback(new OCPPError(ErrorType.GENERIC_ERROR, `WebSocket closed for non buffered message id '${messageId}' with content '${messageToSend}'`, commandName, messagePayload?.details as JsonType ?? {}), false);
101 }
102 // Response?
103 if (messageType !== MessageType.CALL_MESSAGE) {
104 // Yes: send Ok
105 return resolve(messagePayload);
106 }
107
108 /**
109 * Function that will receive the request's response
110 *
111 * @param payload
112 * @param requestPayload
113 */
114 async function responseCallback(payload: JsonType | string, requestPayload: JsonType): Promise<void> {
115 if (self.chargingStation.getEnableStatistics()) {
116 self.chargingStation.performanceStatistics.addRequestStatistic(commandName, MessageType.CALL_RESULT_MESSAGE);
117 }
118 // Handle the request's response
119 try {
120 await self.ocppResponseService.handleResponse(commandName as RequestCommand, payload, requestPayload);
121 resolve(payload);
122 } catch (error) {
123 reject(error);
124 throw error;
125 } finally {
126 self.chargingStation.requests.delete(messageId);
127 }
128 }
129
130 /**
131 * Function that will receive the request's error response
132 *
133 * @param error
134 * @param requestStatistic
135 */
136 function rejectCallback(error: OCPPError, requestStatistic = true): void {
137 if (requestStatistic && self.chargingStation.getEnableStatistics()) {
138 self.chargingStation.performanceStatistics.addRequestStatistic(commandName, MessageType.CALL_ERROR_MESSAGE);
139 }
140 logger.error(`${self.chargingStation.logPrefix()} Error %j occurred when calling command %s with message data %j`, error, commandName, messagePayload);
141 self.chargingStation.requests.delete(messageId);
142 reject(error);
143 }
144 }), Constants.OCPP_WEBSOCKET_TIMEOUT, new OCPPError(ErrorType.GENERIC_ERROR, `Timeout for message id '${messageId}'`, commandName, messagePayload?.details as JsonType ?? {}), () => {
145 messageType === MessageType.CALL_MESSAGE && this.chargingStation.requests.delete(messageId);
146 });
147 }
148 throw new OCPPError(ErrorType.SECURITY_ERROR, `Cannot send command ${commandName} payload when the charging station is in ${this.chargingStation.getRegistrationStatus()} state on the central server`, commandName);
149 }
150
151 private buildMessageToSend(messageId: string, messagePayload: JsonType | OCPPError, messageType: MessageType, commandName?: RequestCommand | IncomingRequestCommand,
152 responseCallback?: (payload: JsonType | string, requestPayload: JsonType) => Promise<void>,
153 rejectCallback?: (error: OCPPError, requestStatistic?: boolean) => void): string {
154 let messageToSend: string;
155 // Type of message
156 switch (messageType) {
157 // Request
158 case MessageType.CALL_MESSAGE:
159 // Build request
160 this.chargingStation.requests.set(messageId, [responseCallback, rejectCallback, commandName, messagePayload]);
161 messageToSend = JSON.stringify([messageType, messageId, commandName, messagePayload]);
162 break;
163 // Response
164 case MessageType.CALL_RESULT_MESSAGE:
165 // Build response
166 messageToSend = JSON.stringify([messageType, messageId, messagePayload]);
167 break;
168 // Error Message
169 case MessageType.CALL_ERROR_MESSAGE:
170 // Build Error Message
171 messageToSend = JSON.stringify([messageType, messageId, messagePayload?.code ?? ErrorType.GENERIC_ERROR, messagePayload?.message ?? '', messagePayload?.details ?? { commandName }]);
172 break;
173 }
174 return messageToSend;
175 }
176
177 private handleRequestError(commandName: RequestCommand | IncomingRequestCommand, error: Error, params: HandleErrorParams = { throwError: true }): void {
178 logger.error(this.chargingStation.logPrefix() + ' Request command %s error: %j', commandName, error);
179 if (params?.throwError) {
180 throw error;
181 }
182 }
183
184 public abstract sendHeartbeat(params?: SendParams): Promise<void>;
185 public abstract sendBootNotification(chargePointModel: string, chargePointVendor: string, chargeBoxSerialNumber?: string, firmwareVersion?: string, chargePointSerialNumber?: string, iccid?: string, imsi?: string, meterSerialNumber?: string, meterType?: string, params?: SendParams): Promise<BootNotificationResponse>;
186 public abstract sendStatusNotification(connectorId: number, status: ChargePointStatus, errorCode?: ChargePointErrorCode): Promise<void>;
187 public abstract sendAuthorize(connectorId: number, idTag?: string): Promise<AuthorizeResponse>;
188 public abstract sendStartTransaction(connectorId: number, idTag?: string): Promise<StartTransactionResponse>;
189 public abstract sendStopTransaction(transactionId: number, meterStop: number, idTag?: string, reason?: StopTransactionReason): Promise<StopTransactionResponse>;
190 public abstract sendMeterValues(connectorId: number, transactionId: number, interval: number): Promise<void>;
191 public abstract sendTransactionBeginMeterValues(connectorId: number, transactionId: number, beginMeterValue: MeterValue): Promise<void>;
192 public abstract sendTransactionEndMeterValues(connectorId: number, transactionId: number, endMeterValue: MeterValue): Promise<void>;
193 public abstract sendDiagnosticsStatusNotification(diagnosticsStatus: DiagnosticsStatus): Promise<void>;
194 }