UI Protocol: add Authorize command support
[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
150 let responsePayload: BroadcastChannelResponsePayload;
151 let commandResponse: CommandResponse | void;
152 try {
153 commandResponse = await this.commandHandler(command, requestPayload);
154 if (commandResponse === undefined || commandResponse === null) {
155 responsePayload = {
156 hashId: this.chargingStation.stationInfo.hashId,
157 status: ResponseStatus.SUCCESS,
158 };
159 } else {
160 const responseStatus = this.commandResponseToResponseStatus(
161 command,
162 commandResponse as CommandResponse
163 );
164 if (responseStatus === ResponseStatus.SUCCESS) {
165 responsePayload = {
166 hashId: this.chargingStation.stationInfo.hashId,
167 status: responseStatus,
168 };
169 } else {
170 responsePayload = {
171 hashId: this.chargingStation.stationInfo.hashId,
172 status: responseStatus,
173 command,
174 requestPayload,
175 commandResponse: commandResponse as CommandResponse,
176 };
177 }
178 }
179 } catch (error) {
180 logger.error(
181 `${this.chargingStation.logPrefix()} ${moduleName}.requestHandler: Handle request error:`,
182 error
183 );
184 responsePayload = {
185 hashId: this.chargingStation.stationInfo.hashId,
186 status: ResponseStatus.FAILURE,
187 command,
188 requestPayload,
189 commandResponse: commandResponse as CommandResponse,
190 errorMessage: (error as Error).message,
191 errorStack: (error as Error).stack,
192 errorDetails: (error as OCPPError).details,
193 };
194 }
195 this.sendResponse([uuid, responsePayload]);
196 }
197
198 private messageErrorHandler(messageEvent: MessageEvent): void {
199 logger.error(
200 `${this.chargingStation.logPrefix()} ${moduleName}.messageErrorHandler: Error at handling message:`,
201 { messageEvent }
202 );
203 }
204
205 private async commandHandler(
206 command: BroadcastChannelProcedureName,
207 requestPayload: BroadcastChannelRequestPayload
208 ): Promise<CommandResponse | void> {
209 if (this.commandHandlers.has(command) === true) {
210 this.cleanRequestPayload(command, requestPayload);
211 return this.commandHandlers.get(command)(requestPayload);
212 }
213 throw new BaseError(`Unknown worker broadcast channel command: ${command}`);
214 }
215
216 private cleanRequestPayload(
217 command: BroadcastChannelProcedureName,
218 requestPayload: BroadcastChannelRequestPayload
219 ): void {
220 delete requestPayload.hashId;
221 delete requestPayload.hashIds;
222 [
223 BroadcastChannelProcedureName.START_AUTOMATIC_TRANSACTION_GENERATOR,
224 BroadcastChannelProcedureName.STOP_AUTOMATIC_TRANSACTION_GENERATOR,
225 ].includes(command) === false && delete requestPayload.connectorIds;
226 }
227
228 private commandResponseToResponseStatus(
229 command: BroadcastChannelProcedureName,
230 commandResponse: CommandResponse
231 ): ResponseStatus {
232 switch (command) {
233 case BroadcastChannelProcedureName.START_TRANSACTION:
234 case BroadcastChannelProcedureName.STOP_TRANSACTION:
235 case BroadcastChannelProcedureName.AUTHORIZE:
236 if (
237 (
238 commandResponse as
239 | StartTransactionResponse
240 | StopTransactionResponse
241 | AuthorizeResponse
242 )?.idTagInfo?.status === AuthorizationStatus.ACCEPTED
243 ) {
244 return ResponseStatus.SUCCESS;
245 }
246 return ResponseStatus.FAILURE;
247 case BroadcastChannelProcedureName.STATUS_NOTIFICATION:
248 if (Utils.isEmptyObject(commandResponse) === true) {
249 return ResponseStatus.SUCCESS;
250 }
251 return ResponseStatus.FAILURE;
252 case BroadcastChannelProcedureName.HEARTBEAT:
253 if ('currentTime' in commandResponse) {
254 return ResponseStatus.SUCCESS;
255 }
256 return ResponseStatus.FAILURE;
257 default:
258 return ResponseStatus.FAILURE;
259 }
260 }
261 }