31afa58ecf2aeb6893dde3a13363c633997ad6ac
[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 { StandardParametersKey } from '../types/ocpp/Configuration';
4 import {
5 type BootNotificationRequest,
6 type HeartbeatRequest,
7 type MeterValuesRequest,
8 RequestCommand,
9 type StatusNotificationRequest,
10 } from '../types/ocpp/Requests';
11 import {
12 type BootNotificationResponse,
13 type HeartbeatResponse,
14 type MeterValuesResponse,
15 RegistrationStatus,
16 type StatusNotificationResponse,
17 } from '../types/ocpp/Responses';
18 import {
19 AuthorizationStatus,
20 type AuthorizeRequest,
21 type AuthorizeResponse,
22 type StartTransactionRequest,
23 type StartTransactionResponse,
24 type StopTransactionRequest,
25 type StopTransactionResponse,
26 } from '../types/ocpp/Transaction';
27 import {
28 BroadcastChannelProcedureName,
29 type BroadcastChannelRequest,
30 type BroadcastChannelRequestPayload,
31 type BroadcastChannelResponsePayload,
32 type MessageEvent,
33 } from '../types/WorkerBroadcastChannel';
34 import { ResponseStatus } from '../ui/web/src/types/UIProtocol';
35 import Constants from '../utils/Constants';
36 import logger from '../utils/Logger';
37 import Utils from '../utils/Utils';
38 import type ChargingStation from './ChargingStation';
39 import { ChargingStationConfigurationUtils } from './ChargingStationConfigurationUtils';
40 import { OCPP16ServiceUtils } from './ocpp/1.6/OCPP16ServiceUtils';
41 import WorkerBroadcastChannel from './WorkerBroadcastChannel';
42
43 const moduleName = 'ChargingStationWorkerBroadcastChannel';
44
45 type CommandResponse =
46 | StartTransactionResponse
47 | StopTransactionResponse
48 | AuthorizeResponse
49 | BootNotificationResponse
50 | StatusNotificationResponse
51 | HeartbeatResponse
52 | MeterValuesResponse;
53
54 type CommandHandler = (
55 requestPayload?: BroadcastChannelRequestPayload
56 ) => Promise<CommandResponse | void> | void;
57
58 export default class ChargingStationWorkerBroadcastChannel extends WorkerBroadcastChannel {
59 private readonly commandHandlers: Map<BroadcastChannelProcedureName, CommandHandler>;
60 private readonly chargingStation: ChargingStation;
61
62 constructor(chargingStation: ChargingStation) {
63 super();
64 this.commandHandlers = new Map<BroadcastChannelProcedureName, CommandHandler>([
65 [BroadcastChannelProcedureName.START_CHARGING_STATION, () => this.chargingStation.start()],
66 [
67 BroadcastChannelProcedureName.STOP_CHARGING_STATION,
68 async () => this.chargingStation.stop(),
69 ],
70 [
71 BroadcastChannelProcedureName.OPEN_CONNECTION,
72 () => this.chargingStation.openWSConnection(),
73 ],
74 [
75 BroadcastChannelProcedureName.CLOSE_CONNECTION,
76 () => this.chargingStation.closeWSConnection(),
77 ],
78 [
79 BroadcastChannelProcedureName.START_AUTOMATIC_TRANSACTION_GENERATOR,
80 (requestPayload?: BroadcastChannelRequestPayload) =>
81 this.chargingStation.startAutomaticTransactionGenerator(requestPayload.connectorIds),
82 ],
83 [
84 BroadcastChannelProcedureName.STOP_AUTOMATIC_TRANSACTION_GENERATOR,
85 (requestPayload?: BroadcastChannelRequestPayload) =>
86 this.chargingStation.stopAutomaticTransactionGenerator(requestPayload.connectorIds),
87 ],
88 [
89 BroadcastChannelProcedureName.START_TRANSACTION,
90 async (requestPayload?: BroadcastChannelRequestPayload) =>
91 this.chargingStation.ocppRequestService.requestHandler<
92 StartTransactionRequest,
93 StartTransactionResponse
94 >(this.chargingStation, RequestCommand.START_TRANSACTION, requestPayload),
95 ],
96 [
97 BroadcastChannelProcedureName.STOP_TRANSACTION,
98 async (requestPayload?: BroadcastChannelRequestPayload) =>
99 this.chargingStation.ocppRequestService.requestHandler<
100 StopTransactionRequest,
101 StartTransactionResponse
102 >(this.chargingStation, RequestCommand.STOP_TRANSACTION, {
103 meterStop:
104 requestPayload.meterStop ??
105 this.chargingStation.getEnergyActiveImportRegisterByTransactionId(
106 requestPayload.transactionId,
107 true
108 ),
109 ...requestPayload,
110 }),
111 ],
112 [
113 BroadcastChannelProcedureName.AUTHORIZE,
114 async (requestPayload?: BroadcastChannelRequestPayload) =>
115 this.chargingStation.ocppRequestService.requestHandler<
116 AuthorizeRequest,
117 AuthorizeResponse
118 >(this.chargingStation, RequestCommand.AUTHORIZE, requestPayload),
119 ],
120 [
121 BroadcastChannelProcedureName.BOOT_NOTIFICATION,
122 async (requestPayload?: BroadcastChannelRequestPayload) => {
123 this.chargingStation.bootNotificationResponse =
124 await this.chargingStation.ocppRequestService.requestHandler<
125 BootNotificationRequest,
126 BootNotificationResponse
127 >(
128 this.chargingStation,
129 RequestCommand.BOOT_NOTIFICATION,
130 {
131 ...this.chargingStation.bootNotificationRequest,
132 ...requestPayload,
133 },
134 {
135 skipBufferingOnError: true,
136 }
137 );
138 return this.chargingStation.bootNotificationResponse;
139 },
140 ],
141 [
142 BroadcastChannelProcedureName.STATUS_NOTIFICATION,
143 async (requestPayload?: BroadcastChannelRequestPayload) =>
144 this.chargingStation.ocppRequestService.requestHandler<
145 StatusNotificationRequest,
146 StatusNotificationResponse
147 >(this.chargingStation, RequestCommand.STATUS_NOTIFICATION, requestPayload),
148 ],
149 [
150 BroadcastChannelProcedureName.HEARTBEAT,
151 async (requestPayload?: BroadcastChannelRequestPayload) =>
152 this.chargingStation.ocppRequestService.requestHandler<
153 HeartbeatRequest,
154 HeartbeatResponse
155 >(this.chargingStation, RequestCommand.HEARTBEAT, requestPayload),
156 ],
157 [
158 BroadcastChannelProcedureName.METER_VALUES,
159 async (requestPayload?: BroadcastChannelRequestPayload) => {
160 const configuredMeterValueSampleInterval =
161 ChargingStationConfigurationUtils.getConfigurationKey(
162 chargingStation,
163 StandardParametersKey.MeterValueSampleInterval
164 );
165 return this.chargingStation.ocppRequestService.requestHandler<
166 MeterValuesRequest,
167 MeterValuesResponse
168 >(this.chargingStation, RequestCommand.METER_VALUES, {
169 meterValue: [
170 OCPP16ServiceUtils.buildMeterValue(
171 this.chargingStation,
172 requestPayload.connectorId,
173 this.chargingStation.getConnectorStatus(requestPayload.connectorId)?.transactionId,
174 configuredMeterValueSampleInterval
175 ? Utils.convertToInt(configuredMeterValueSampleInterval.value) * 1000
176 : Constants.DEFAULT_METER_VALUES_INTERVAL
177 ),
178 ],
179 ...requestPayload,
180 });
181 },
182 ],
183 ]);
184 this.chargingStation = chargingStation;
185 this.onmessage = this.requestHandler.bind(this) as (message: MessageEvent) => void;
186 this.onmessageerror = this.messageErrorHandler.bind(this) as (message: MessageEvent) => void;
187 }
188
189 private async requestHandler(messageEvent: MessageEvent): Promise<void> {
190 const validatedMessageEvent = this.validateMessageEvent(messageEvent);
191 if (validatedMessageEvent === false) {
192 return;
193 }
194 if (this.isResponse(validatedMessageEvent.data) === true) {
195 return;
196 }
197 const [uuid, command, requestPayload] = validatedMessageEvent.data as BroadcastChannelRequest;
198 if (
199 requestPayload?.hashIds !== undefined &&
200 requestPayload?.hashIds?.includes(this.chargingStation.stationInfo.hashId) === false
201 ) {
202 return;
203 }
204 if (requestPayload?.hashId !== undefined) {
205 logger.error(
206 `${this.chargingStation.logPrefix()} ${moduleName}.requestHandler: 'hashId' field usage in PDU is deprecated, use 'hashIds' instead`
207 );
208 return;
209 }
210 let responsePayload: BroadcastChannelResponsePayload;
211 let commandResponse: CommandResponse | void;
212 try {
213 commandResponse = await this.commandHandler(command, requestPayload);
214 if (commandResponse === undefined || commandResponse === null) {
215 responsePayload = {
216 hashId: this.chargingStation.stationInfo.hashId,
217 status: ResponseStatus.SUCCESS,
218 };
219 } else {
220 responsePayload = this.commandResponseToResponsePayload(
221 command,
222 requestPayload,
223 commandResponse as CommandResponse
224 );
225 }
226 } catch (error) {
227 logger.error(
228 `${this.chargingStation.logPrefix()} ${moduleName}.requestHandler: Handle request error:`,
229 error
230 );
231 responsePayload = {
232 hashId: this.chargingStation.stationInfo.hashId,
233 status: ResponseStatus.FAILURE,
234 command,
235 requestPayload,
236 commandResponse: commandResponse as CommandResponse,
237 errorMessage: (error as Error).message,
238 errorStack: (error as Error).stack,
239 errorDetails: (error as OCPPError).details,
240 };
241 } finally {
242 this.sendResponse([uuid, responsePayload]);
243 }
244 }
245
246 private messageErrorHandler(messageEvent: MessageEvent): void {
247 logger.error(
248 `${this.chargingStation.logPrefix()} ${moduleName}.messageErrorHandler: Error at handling message:`,
249 messageEvent
250 );
251 }
252
253 private async commandHandler(
254 command: BroadcastChannelProcedureName,
255 requestPayload: BroadcastChannelRequestPayload
256 ): Promise<CommandResponse | void> {
257 if (this.commandHandlers.has(command) === true) {
258 this.cleanRequestPayload(command, requestPayload);
259 return this.commandHandlers.get(command)(requestPayload);
260 }
261 throw new BaseError(`Unknown worker broadcast channel command: ${command}`);
262 }
263
264 private cleanRequestPayload(
265 command: BroadcastChannelProcedureName,
266 requestPayload: BroadcastChannelRequestPayload
267 ): void {
268 delete requestPayload.hashId;
269 delete requestPayload.hashIds;
270 [
271 BroadcastChannelProcedureName.START_AUTOMATIC_TRANSACTION_GENERATOR,
272 BroadcastChannelProcedureName.STOP_AUTOMATIC_TRANSACTION_GENERATOR,
273 ].includes(command) === false && delete requestPayload.connectorIds;
274 }
275
276 private commandResponseToResponsePayload(
277 command: BroadcastChannelProcedureName,
278 requestPayload: BroadcastChannelRequestPayload,
279 commandResponse: CommandResponse
280 ): BroadcastChannelResponsePayload {
281 const responseStatus = this.commandResponseToResponseStatus(command, commandResponse);
282 if (responseStatus === ResponseStatus.SUCCESS) {
283 return {
284 hashId: this.chargingStation.stationInfo.hashId,
285 status: responseStatus,
286 };
287 }
288 return {
289 hashId: this.chargingStation.stationInfo.hashId,
290 status: responseStatus,
291 command,
292 requestPayload,
293 commandResponse,
294 };
295 }
296
297 private commandResponseToResponseStatus(
298 command: BroadcastChannelProcedureName,
299 commandResponse: CommandResponse
300 ): ResponseStatus {
301 switch (command) {
302 case BroadcastChannelProcedureName.START_TRANSACTION:
303 case BroadcastChannelProcedureName.STOP_TRANSACTION:
304 case BroadcastChannelProcedureName.AUTHORIZE:
305 if (
306 (
307 commandResponse as
308 | StartTransactionResponse
309 | StopTransactionResponse
310 | AuthorizeResponse
311 )?.idTagInfo?.status === AuthorizationStatus.ACCEPTED
312 ) {
313 return ResponseStatus.SUCCESS;
314 }
315 return ResponseStatus.FAILURE;
316 case BroadcastChannelProcedureName.BOOT_NOTIFICATION:
317 if (commandResponse?.status === RegistrationStatus.ACCEPTED) {
318 return ResponseStatus.SUCCESS;
319 }
320 return ResponseStatus.FAILURE;
321 case BroadcastChannelProcedureName.STATUS_NOTIFICATION:
322 case BroadcastChannelProcedureName.METER_VALUES:
323 if (Utils.isEmptyObject(commandResponse) === true) {
324 return ResponseStatus.SUCCESS;
325 }
326 return ResponseStatus.FAILURE;
327 case BroadcastChannelProcedureName.HEARTBEAT:
328 if ('currentTime' in commandResponse) {
329 return ResponseStatus.SUCCESS;
330 }
331 return ResponseStatus.FAILURE;
332 default:
333 return ResponseStatus.FAILURE;
334 }
335 }
336 }