UI Server: Add a unique request handler for procedure forwarded to charging
[e-mobility-charging-stations-simulator.git] / src / charging-station / ChargingStationWorkerBroadcastChannel.ts
1 import BaseError from '../exception/BaseError';
2 import type OCPPError from '../exception/OCPPError';
3 import {
4 HeartbeatRequest,
5 RequestCommand,
6 type StatusNotificationRequest,
7 } from '../types/ocpp/Requests';
8 import type { HeartbeatResponse, StatusNotificationResponse } from '../types/ocpp/Responses';
9 import {
10 AuthorizationStatus,
11 AuthorizeRequest,
12 AuthorizeResponse,
13 StartTransactionRequest,
14 StartTransactionResponse,
15 StopTransactionRequest,
16 StopTransactionResponse,
17 } from '../types/ocpp/Transaction';
18 import {
19 BroadcastChannelProcedureName,
20 BroadcastChannelRequest,
21 BroadcastChannelRequestPayload,
22 BroadcastChannelResponsePayload,
23 MessageEvent,
24 } from '../types/WorkerBroadcastChannel';
25 import { ResponseStatus } from '../ui/web/src/types/UIProtocol';
26 import logger from '../utils/Logger';
27 import Utils from '../utils/Utils';
28 import type ChargingStation from './ChargingStation';
29 import WorkerBroadcastChannel from './WorkerBroadcastChannel';
30
31 const moduleName = 'ChargingStationWorkerBroadcastChannel';
32
33 type CommandResponse =
34 | StartTransactionResponse
35 | StopTransactionResponse
36 | AuthorizeResponse
37 | StatusNotificationResponse
38 | HeartbeatResponse;
39
40 type CommandHandler = (
41 requestPayload?: BroadcastChannelRequestPayload
42 ) => Promise<CommandResponse | void> | void;
43
44 export default class ChargingStationWorkerBroadcastChannel extends WorkerBroadcastChannel {
45 private readonly commandHandlers: Map<BroadcastChannelProcedureName, CommandHandler>;
46
47 private readonly chargingStation: ChargingStation;
48
49 constructor(chargingStation: ChargingStation) {
50 super();
51 this.commandHandlers = new Map<BroadcastChannelProcedureName, CommandHandler>([
52 [BroadcastChannelProcedureName.START_CHARGING_STATION, () => this.chargingStation.start()],
53 [
54 BroadcastChannelProcedureName.STOP_CHARGING_STATION,
55 async () => this.chargingStation.stop(),
56 ],
57 [
58 BroadcastChannelProcedureName.OPEN_CONNECTION,
59 () => this.chargingStation.openWSConnection(),
60 ],
61 [
62 BroadcastChannelProcedureName.CLOSE_CONNECTION,
63 () => this.chargingStation.closeWSConnection(),
64 ],
65 [
66 BroadcastChannelProcedureName.START_TRANSACTION,
67 async (requestPayload?: BroadcastChannelRequestPayload) =>
68 this.chargingStation.ocppRequestService.requestHandler<
69 StartTransactionRequest,
70 StartTransactionResponse
71 >(this.chargingStation, RequestCommand.START_TRANSACTION, requestPayload),
72 ],
73 [
74 BroadcastChannelProcedureName.STOP_TRANSACTION,
75 async (requestPayload?: BroadcastChannelRequestPayload) =>
76 this.chargingStation.ocppRequestService.requestHandler<
77 StopTransactionRequest,
78 StartTransactionResponse
79 >(this.chargingStation, RequestCommand.STOP_TRANSACTION, {
80 ...requestPayload,
81 meterStop: this.chargingStation.getEnergyActiveImportRegisterByTransactionId(
82 requestPayload.transactionId,
83 true
84 ),
85 }),
86 ],
87 [
88 BroadcastChannelProcedureName.START_AUTOMATIC_TRANSACTION_GENERATOR,
89 (requestPayload?: BroadcastChannelRequestPayload) =>
90 this.chargingStation.startAutomaticTransactionGenerator(requestPayload.connectorIds),
91 ],
92 [
93 BroadcastChannelProcedureName.STOP_AUTOMATIC_TRANSACTION_GENERATOR,
94 (requestPayload?: BroadcastChannelRequestPayload) =>
95 this.chargingStation.stopAutomaticTransactionGenerator(requestPayload.connectorIds),
96 ],
97 [
98 BroadcastChannelProcedureName.AUTHORIZE,
99 async (requestPayload?: BroadcastChannelRequestPayload) =>
100 this.chargingStation.ocppRequestService.requestHandler<
101 AuthorizeRequest,
102 AuthorizeResponse
103 >(this.chargingStation, RequestCommand.AUTHORIZE, requestPayload),
104 ],
105 [
106 BroadcastChannelProcedureName.STATUS_NOTIFICATION,
107 async (requestPayload?: BroadcastChannelRequestPayload) =>
108 this.chargingStation.ocppRequestService.requestHandler<
109 StatusNotificationRequest,
110 StatusNotificationResponse
111 >(this.chargingStation, RequestCommand.STATUS_NOTIFICATION, requestPayload),
112 ],
113 [
114 BroadcastChannelProcedureName.HEARTBEAT,
115 async (requestPayload?: BroadcastChannelRequestPayload) =>
116 this.chargingStation.ocppRequestService.requestHandler<
117 HeartbeatRequest,
118 HeartbeatResponse
119 >(this.chargingStation, RequestCommand.HEARTBEAT, requestPayload),
120 ],
121 ]);
122 this.chargingStation = chargingStation;
123 this.onmessage = this.requestHandler.bind(this) as (message: MessageEvent) => void;
124 this.onmessageerror = this.messageErrorHandler.bind(this) as (message: MessageEvent) => void;
125 }
126
127 private async requestHandler(messageEvent: MessageEvent): Promise<void> {
128 const validatedMessageEvent = this.validateMessageEvent(messageEvent);
129 if (validatedMessageEvent === false) {
130 return;
131 }
132 if (this.isResponse(validatedMessageEvent.data) === true) {
133 return;
134 }
135 const [uuid, command, requestPayload] = validatedMessageEvent.data as BroadcastChannelRequest;
136
137 if (
138 requestPayload?.hashIds !== undefined &&
139 requestPayload?.hashIds?.includes(this.chargingStation.stationInfo.hashId) === false
140 ) {
141 return;
142 }
143 if (requestPayload?.hashId !== undefined) {
144 logger.error(
145 `${this.chargingStation.logPrefix()} ${moduleName}.requestHandler: 'hashId' field usage in PDU is deprecated, use 'hashIds' instead`
146 );
147 return;
148 }
149 let responsePayload: BroadcastChannelResponsePayload;
150 let commandResponse: CommandResponse | void;
151 try {
152 commandResponse = await this.commandHandler(command, requestPayload);
153 if (commandResponse === undefined || commandResponse === null) {
154 responsePayload = {
155 hashId: this.chargingStation.stationInfo.hashId,
156 status: ResponseStatus.SUCCESS,
157 };
158 } else {
159 const commandResponseStatus = this.commandResponseToResponseStatus(
160 command,
161 commandResponse as CommandResponse
162 );
163 if (commandResponseStatus === ResponseStatus.SUCCESS) {
164 responsePayload = {
165 hashId: this.chargingStation.stationInfo.hashId,
166 status: commandResponseStatus,
167 };
168 } else {
169 responsePayload = {
170 hashId: this.chargingStation.stationInfo.hashId,
171 status: commandResponseStatus,
172 command,
173 requestPayload,
174 commandResponse: commandResponse as CommandResponse,
175 };
176 }
177 }
178 } catch (error) {
179 logger.error(
180 `${this.chargingStation.logPrefix()} ${moduleName}.requestHandler: Handle request error:`,
181 error
182 );
183 responsePayload = {
184 hashId: this.chargingStation.stationInfo.hashId,
185 status: ResponseStatus.FAILURE,
186 command,
187 requestPayload,
188 commandResponse: commandResponse as CommandResponse,
189 errorMessage: (error as Error).message,
190 errorStack: (error as Error).stack,
191 errorDetails: (error as OCPPError).details,
192 };
193 }
194 this.sendResponse([uuid, responsePayload]);
195 }
196
197 private messageErrorHandler(messageEvent: MessageEvent): void {
198 logger.error(
199 `${this.chargingStation.logPrefix()} ${moduleName}.messageErrorHandler: Error at handling message:`,
200 { messageEvent }
201 );
202 }
203
204 private async commandHandler(
205 command: BroadcastChannelProcedureName,
206 requestPayload: BroadcastChannelRequestPayload
207 ): Promise<CommandResponse | void> {
208 if (this.commandHandlers.has(command) === true) {
209 this.cleanRequestPayload(command, requestPayload);
210 return this.commandHandlers.get(command)(requestPayload);
211 }
212 throw new BaseError(`Unknown worker broadcast channel command: ${command}`);
213 }
214
215 private cleanRequestPayload(
216 command: BroadcastChannelProcedureName,
217 requestPayload: BroadcastChannelRequestPayload
218 ): void {
219 delete requestPayload.hashId;
220 delete requestPayload.hashIds;
221 [
222 BroadcastChannelProcedureName.START_AUTOMATIC_TRANSACTION_GENERATOR,
223 BroadcastChannelProcedureName.STOP_AUTOMATIC_TRANSACTION_GENERATOR,
224 ].includes(command) === false && delete requestPayload.connectorIds;
225 }
226
227 private commandResponseToResponseStatus(
228 command: BroadcastChannelProcedureName,
229 commandResponse: CommandResponse
230 ): ResponseStatus {
231 switch (command) {
232 case BroadcastChannelProcedureName.START_TRANSACTION:
233 case BroadcastChannelProcedureName.STOP_TRANSACTION:
234 case BroadcastChannelProcedureName.AUTHORIZE:
235 if (
236 (
237 commandResponse as
238 | StartTransactionResponse
239 | StopTransactionResponse
240 | AuthorizeResponse
241 )?.idTagInfo?.status === AuthorizationStatus.ACCEPTED
242 ) {
243 return ResponseStatus.SUCCESS;
244 }
245 return ResponseStatus.FAILURE;
246 case BroadcastChannelProcedureName.STATUS_NOTIFICATION:
247 if (Utils.isEmptyObject(commandResponse) === true) {
248 return ResponseStatus.SUCCESS;
249 }
250 return ResponseStatus.FAILURE;
251 case BroadcastChannelProcedureName.HEARTBEAT:
252 if ('currentTime' in commandResponse) {
253 return ResponseStatus.SUCCESS;
254 }
255 return ResponseStatus.FAILURE;
256 default:
257 return ResponseStatus.FAILURE;
258 }
259 }
260 }