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