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