refactor: cleanup default worker options handling
[e-mobility-charging-stations-simulator.git] / src / charging-station / broadcast-channel / ChargingStationWorkerBroadcastChannel.ts
CommitLineData
4c3c0d59 1import { WorkerBroadcastChannel } from './WorkerBroadcastChannel';
7671fa0b 2import { BaseError, type OCPPError } from '../../exception';
10db00b2 3import {
268a74bb
JB
4 AuthorizationStatus,
5 type AuthorizeRequest,
6 type AuthorizeResponse,
8bfbc743 7 type BootNotificationRequest,
8bfbc743 8 type BootNotificationResponse,
268a74bb
JB
9 BroadcastChannelProcedureName,
10 type BroadcastChannelRequest,
11 type BroadcastChannelRequestPayload,
12 type BroadcastChannelResponsePayload,
13 type DataTransferRequest,
91a7d3ea
JB
14 type DataTransferResponse,
15 DataTransferStatus,
268a74bb 16 type DiagnosticsStatusNotificationRequest,
c9a4f9ea 17 type DiagnosticsStatusNotificationResponse,
268a74bb 18 type FirmwareStatusNotificationRequest,
c9a4f9ea 19 type FirmwareStatusNotificationResponse,
268a74bb 20 type HeartbeatRequest,
8bfbc743 21 type HeartbeatResponse,
268a74bb
JB
22 type MessageEvent,
23 type MeterValuesRequest,
8bfbc743 24 type MeterValuesResponse,
d270cc87 25 RegistrationStatusEnumType,
268a74bb
JB
26 RequestCommand,
27 type RequestParams,
28 ResponseStatus,
29 StandardParametersKey,
8bfbc743
JB
30 type StartTransactionRequest,
31 type StartTransactionResponse,
268a74bb
JB
32 type StatusNotificationRequest,
33 type StatusNotificationResponse,
8bfbc743
JB
34 type StopTransactionRequest,
35 type StopTransactionResponse,
7671fa0b 36} from '../../types';
9bf0ef23 37import { Constants, convertToInt, isEmptyObject, isNullOrUndefined, logger } from '../../utils';
7671fa0b 38import type { ChargingStation } from '../ChargingStation';
f2d5e3d9 39import { getConfigurationKey } from '../ChargingStationConfigurationUtils';
7671fa0b 40import { OCPP16ServiceUtils } from '../ocpp';
89b7a234 41
4e3ff94d
JB
42const moduleName = 'ChargingStationWorkerBroadcastChannel';
43
a9ed42b2
JB
44type CommandResponse =
45 | StartTransactionResponse
46 | StopTransactionResponse
1984f194 47 | AuthorizeResponse
8bfbc743 48 | BootNotificationResponse
10db00b2 49 | StatusNotificationResponse
d3195f0a 50 | HeartbeatResponse
e1d9a0f4 51 | DataTransferResponse;
89b7a234 52
d273692c 53type CommandHandler = (
5edd8ba0 54 requestPayload?: BroadcastChannelRequestPayload,
d273692c
JB
55) => Promise<CommandResponse | void> | void;
56
268a74bb 57export class ChargingStationWorkerBroadcastChannel extends WorkerBroadcastChannel {
d273692c 58 private readonly commandHandlers: Map<BroadcastChannelProcedureName, CommandHandler>;
89b7a234
JB
59 private readonly chargingStation: ChargingStation;
60
61 constructor(chargingStation: ChargingStation) {
1598b27c 62 super();
8ec8e3d0
JB
63 const requestParams: RequestParams = {
64 throwError: true,
65 };
d273692c 66 this.commandHandlers = new Map<BroadcastChannelProcedureName, CommandHandler>([
9d73266c
JB
67 [BroadcastChannelProcedureName.START_CHARGING_STATION, () => this.chargingStation.start()],
68 [
69 BroadcastChannelProcedureName.STOP_CHARGING_STATION,
d273692c 70 async () => this.chargingStation.stop(),
9d73266c
JB
71 ],
72 [
73 BroadcastChannelProcedureName.OPEN_CONNECTION,
74 () => this.chargingStation.openWSConnection(),
75 ],
76 [
77 BroadcastChannelProcedureName.CLOSE_CONNECTION,
78 () => this.chargingStation.closeWSConnection(),
79 ],
623b39b5
JB
80 [
81 BroadcastChannelProcedureName.START_AUTOMATIC_TRANSACTION_GENERATOR,
82 (requestPayload?: BroadcastChannelRequestPayload) =>
72092cfc 83 this.chargingStation.startAutomaticTransactionGenerator(requestPayload?.connectorIds),
623b39b5
JB
84 ],
85 [
86 BroadcastChannelProcedureName.STOP_AUTOMATIC_TRANSACTION_GENERATOR,
87 (requestPayload?: BroadcastChannelRequestPayload) =>
72092cfc 88 this.chargingStation.stopAutomaticTransactionGenerator(requestPayload?.connectorIds),
623b39b5 89 ],
269de583
JB
90 [
91 BroadcastChannelProcedureName.SET_SUPERVISION_URL,
92 (requestPayload?: BroadcastChannelRequestPayload) =>
93 this.chargingStation.setSupervisionUrl(requestPayload?.url as string),
94 ],
9d73266c
JB
95 [
96 BroadcastChannelProcedureName.START_TRANSACTION,
97 async (requestPayload?: BroadcastChannelRequestPayload) =>
98 this.chargingStation.ocppRequestService.requestHandler<
99 StartTransactionRequest,
100 StartTransactionResponse
8ec8e3d0 101 >(this.chargingStation, RequestCommand.START_TRANSACTION, requestPayload, requestParams),
9d73266c
JB
102 ],
103 [
104 BroadcastChannelProcedureName.STOP_TRANSACTION,
105 async (requestPayload?: BroadcastChannelRequestPayload) =>
106 this.chargingStation.ocppRequestService.requestHandler<
107 StopTransactionRequest,
108 StartTransactionResponse
1969f643
JB
109 >(
110 this.chargingStation,
111 RequestCommand.STOP_TRANSACTION,
112 {
113 meterStop: this.chargingStation.getEnergyActiveImportRegisterByTransactionId(
e1d9a0f4 114 requestPayload!.transactionId!,
5edd8ba0 115 true,
1969f643
JB
116 ),
117 ...requestPayload,
118 },
5edd8ba0 119 requestParams,
1969f643 120 ),
9d73266c 121 ],
1984f194
JB
122 [
123 BroadcastChannelProcedureName.AUTHORIZE,
124 async (requestPayload?: BroadcastChannelRequestPayload) =>
125 this.chargingStation.ocppRequestService.requestHandler<
126 AuthorizeRequest,
127 AuthorizeResponse
8ec8e3d0 128 >(this.chargingStation, RequestCommand.AUTHORIZE, requestPayload, requestParams),
1984f194 129 ],
8bfbc743
JB
130 [
131 BroadcastChannelProcedureName.BOOT_NOTIFICATION,
132 async (requestPayload?: BroadcastChannelRequestPayload) => {
133 this.chargingStation.bootNotificationResponse =
134 await this.chargingStation.ocppRequestService.requestHandler<
135 BootNotificationRequest,
136 BootNotificationResponse
137 >(
138 this.chargingStation,
139 RequestCommand.BOOT_NOTIFICATION,
140 {
141 ...this.chargingStation.bootNotificationRequest,
142 ...requestPayload,
143 },
144 {
145 skipBufferingOnError: true,
8ec8e3d0 146 throwError: true,
5edd8ba0 147 },
8bfbc743
JB
148 );
149 return this.chargingStation.bootNotificationResponse;
150 },
151 ],
9d73266c
JB
152 [
153 BroadcastChannelProcedureName.STATUS_NOTIFICATION,
154 async (requestPayload?: BroadcastChannelRequestPayload) =>
155 this.chargingStation.ocppRequestService.requestHandler<
156 StatusNotificationRequest,
157 StatusNotificationResponse
8ec8e3d0
JB
158 >(
159 this.chargingStation,
160 RequestCommand.STATUS_NOTIFICATION,
161 requestPayload,
5edd8ba0 162 requestParams,
8ec8e3d0 163 ),
9d73266c
JB
164 ],
165 [
166 BroadcastChannelProcedureName.HEARTBEAT,
1984f194
JB
167 async (requestPayload?: BroadcastChannelRequestPayload) =>
168 this.chargingStation.ocppRequestService.requestHandler<
9d73266c
JB
169 HeartbeatRequest,
170 HeartbeatResponse
8ec8e3d0 171 >(this.chargingStation, RequestCommand.HEARTBEAT, requestPayload, requestParams),
9d73266c 172 ],
d3195f0a
JB
173 [
174 BroadcastChannelProcedureName.METER_VALUES,
175 async (requestPayload?: BroadcastChannelRequestPayload) => {
f2d5e3d9
JB
176 const configuredMeterValueSampleInterval = getConfigurationKey(
177 chargingStation,
178 StandardParametersKey.MeterValueSampleInterval,
179 );
d3195f0a
JB
180 return this.chargingStation.ocppRequestService.requestHandler<
181 MeterValuesRequest,
182 MeterValuesResponse
1969f643
JB
183 >(
184 this.chargingStation,
185 RequestCommand.METER_VALUES,
186 {
187 meterValue: [
188 // FIXME: Implement OCPP version agnostic helpers
189 OCPP16ServiceUtils.buildMeterValue(
190 this.chargingStation,
e1d9a0f4
JB
191 requestPayload!.connectorId!,
192 this.chargingStation.getConnectorStatus(requestPayload!.connectorId!)!
193 .transactionId!,
1969f643 194 configuredMeterValueSampleInterval
9bf0ef23 195 ? convertToInt(configuredMeterValueSampleInterval.value) * 1000
5edd8ba0 196 : Constants.DEFAULT_METER_VALUES_INTERVAL,
1969f643
JB
197 ),
198 ],
199 ...requestPayload,
200 },
5edd8ba0 201 requestParams,
1969f643 202 );
d3195f0a
JB
203 },
204 ],
91a7d3ea
JB
205 [
206 BroadcastChannelProcedureName.DATA_TRANSFER,
207 async (requestPayload?: BroadcastChannelRequestPayload) =>
208 this.chargingStation.ocppRequestService.requestHandler<
209 DataTransferRequest,
210 DataTransferResponse
8ec8e3d0 211 >(this.chargingStation, RequestCommand.DATA_TRANSFER, requestPayload, requestParams),
91a7d3ea 212 ],
c9a4f9ea
JB
213 [
214 BroadcastChannelProcedureName.DIAGNOSTICS_STATUS_NOTIFICATION,
215 async (requestPayload?: BroadcastChannelRequestPayload) =>
216 this.chargingStation.ocppRequestService.requestHandler<
217 DiagnosticsStatusNotificationRequest,
218 DiagnosticsStatusNotificationResponse
8ec8e3d0
JB
219 >(
220 this.chargingStation,
221 RequestCommand.DIAGNOSTICS_STATUS_NOTIFICATION,
222 requestPayload,
5edd8ba0 223 requestParams,
8ec8e3d0 224 ),
c9a4f9ea
JB
225 ],
226 [
227 BroadcastChannelProcedureName.FIRMWARE_STATUS_NOTIFICATION,
228 async (requestPayload?: BroadcastChannelRequestPayload) =>
229 this.chargingStation.ocppRequestService.requestHandler<
230 FirmwareStatusNotificationRequest,
231 FirmwareStatusNotificationResponse
8ec8e3d0
JB
232 >(
233 this.chargingStation,
234 RequestCommand.FIRMWARE_STATUS_NOTIFICATION,
235 requestPayload,
5edd8ba0 236 requestParams,
8ec8e3d0 237 ),
c9a4f9ea 238 ],
9d73266c 239 ]);
89b7a234 240 this.chargingStation = chargingStation;
a37fc6dc
JB
241 this.onmessage = this.requestHandler.bind(this) as (message: unknown) => void;
242 this.onmessageerror = this.messageErrorHandler.bind(this) as (message: unknown) => void;
89b7a234
JB
243 }
244
02a6943a 245 private async requestHandler(messageEvent: MessageEvent): Promise<void> {
5dea4c94
JB
246 const validatedMessageEvent = this.validateMessageEvent(messageEvent);
247 if (validatedMessageEvent === false) {
6c8f5d90
JB
248 return;
249 }
5dea4c94
JB
250 if (this.isResponse(validatedMessageEvent.data) === true) {
251 return;
252 }
253 const [uuid, command, requestPayload] = validatedMessageEvent.data as BroadcastChannelRequest;
2afb4d15 254 if (
4a3807d1
JB
255 !isNullOrUndefined(requestPayload.hashIds) &&
256 requestPayload.hashIds?.includes(this.chargingStation.stationInfo.hashId) === false
2afb4d15
JB
257 ) {
258 return;
259 }
4a3807d1 260 if (!isNullOrUndefined(requestPayload.hashId)) {
2afb4d15 261 logger.error(
5edd8ba0 262 `${this.chargingStation.logPrefix()} ${moduleName}.requestHandler: 'hashId' field usage in PDU is deprecated, use 'hashIds' array instead`,
2afb4d15
JB
263 );
264 return;
4eca248c 265 }
e1d9a0f4
JB
266 let responsePayload: BroadcastChannelResponsePayload | undefined;
267 let commandResponse: CommandResponse | void | undefined;
6c8f5d90
JB
268 try {
269 commandResponse = await this.commandHandler(command, requestPayload);
9bf0ef23 270 if (isNullOrUndefined(commandResponse) || isEmptyObject(commandResponse as CommandResponse)) {
10d244c0 271 responsePayload = {
51c83d6f 272 hashId: this.chargingStation.stationInfo.hashId,
10d244c0
JB
273 status: ResponseStatus.SUCCESS,
274 };
6c8f5d90 275 } else {
d3195f0a 276 responsePayload = this.commandResponseToResponsePayload(
1984f194 277 command,
d3195f0a 278 requestPayload,
5edd8ba0 279 commandResponse as CommandResponse,
1984f194 280 );
6c8f5d90
JB
281 }
282 } catch (error) {
283 logger.error(
284 `${this.chargingStation.logPrefix()} ${moduleName}.requestHandler: Handle request error:`,
5edd8ba0 285 error,
6c8f5d90
JB
286 );
287 responsePayload = {
51c83d6f 288 hashId: this.chargingStation.stationInfo.hashId,
6c8f5d90
JB
289 status: ResponseStatus.FAILURE,
290 command,
291 requestPayload,
9d73266c 292 commandResponse: commandResponse as CommandResponse,
6c8f5d90
JB
293 errorMessage: (error as Error).message,
294 errorStack: (error as Error).stack,
a9ed42b2 295 errorDetails: (error as OCPPError).details,
6c8f5d90 296 };
623b39b5 297 } finally {
e1d9a0f4 298 this.sendResponse([uuid, responsePayload!]);
6c8f5d90 299 }
6c8f5d90
JB
300 }
301
302 private messageErrorHandler(messageEvent: MessageEvent): void {
303 logger.error(
304 `${this.chargingStation.logPrefix()} ${moduleName}.messageErrorHandler: Error at handling message:`,
5edd8ba0 305 messageEvent,
6c8f5d90
JB
306 );
307 }
308
309 private async commandHandler(
310 command: BroadcastChannelProcedureName,
5edd8ba0 311 requestPayload: BroadcastChannelRequestPayload,
9d73266c
JB
312 ): Promise<CommandResponse | void> {
313 if (this.commandHandlers.has(command) === true) {
1984f194 314 this.cleanRequestPayload(command, requestPayload);
e1d9a0f4 315 return this.commandHandlers.get(command)!(requestPayload);
6c8f5d90 316 }
9d73266c 317 throw new BaseError(`Unknown worker broadcast channel command: ${command}`);
6c8f5d90
JB
318 }
319
1984f194
JB
320 private cleanRequestPayload(
321 command: BroadcastChannelProcedureName,
5edd8ba0 322 requestPayload: BroadcastChannelRequestPayload,
1984f194
JB
323 ): void {
324 delete requestPayload.hashId;
325 delete requestPayload.hashIds;
326 [
327 BroadcastChannelProcedureName.START_AUTOMATIC_TRANSACTION_GENERATOR,
328 BroadcastChannelProcedureName.STOP_AUTOMATIC_TRANSACTION_GENERATOR,
8f879946 329 ].includes(command) === false && delete requestPayload.connectorIds;
1984f194
JB
330 }
331
d3195f0a
JB
332 private commandResponseToResponsePayload(
333 command: BroadcastChannelProcedureName,
334 requestPayload: BroadcastChannelRequestPayload,
5edd8ba0 335 commandResponse: CommandResponse,
d3195f0a 336 ): BroadcastChannelResponsePayload {
cfa257f5
JB
337 const responseStatus = this.commandResponseToResponseStatus(command, commandResponse);
338 if (responseStatus === ResponseStatus.SUCCESS) {
d3195f0a
JB
339 return {
340 hashId: this.chargingStation.stationInfo.hashId,
cfa257f5 341 status: responseStatus,
d3195f0a
JB
342 };
343 }
344 return {
345 hashId: this.chargingStation.stationInfo.hashId,
cfa257f5 346 status: responseStatus,
d3195f0a
JB
347 command,
348 requestPayload,
349 commandResponse,
350 };
351 }
352
cfa257f5 353 private commandResponseToResponseStatus(
10db00b2 354 command: BroadcastChannelProcedureName,
5edd8ba0 355 commandResponse: CommandResponse,
10db00b2
JB
356 ): ResponseStatus {
357 switch (command) {
358 case BroadcastChannelProcedureName.START_TRANSACTION:
359 case BroadcastChannelProcedureName.STOP_TRANSACTION:
1984f194 360 case BroadcastChannelProcedureName.AUTHORIZE:
10db00b2 361 if (
1984f194
JB
362 (
363 commandResponse as
364 | StartTransactionResponse
365 | StopTransactionResponse
366 | AuthorizeResponse
367 )?.idTagInfo?.status === AuthorizationStatus.ACCEPTED
10db00b2
JB
368 ) {
369 return ResponseStatus.SUCCESS;
370 }
371 return ResponseStatus.FAILURE;
8bfbc743 372 case BroadcastChannelProcedureName.BOOT_NOTIFICATION:
d270cc87 373 if (commandResponse?.status === RegistrationStatusEnumType.ACCEPTED) {
8bfbc743
JB
374 return ResponseStatus.SUCCESS;
375 }
376 return ResponseStatus.FAILURE;
91a7d3ea
JB
377 case BroadcastChannelProcedureName.DATA_TRANSFER:
378 if (commandResponse?.status === DataTransferStatus.ACCEPTED) {
379 return ResponseStatus.SUCCESS;
380 }
381 return ResponseStatus.FAILURE;
10db00b2 382 case BroadcastChannelProcedureName.STATUS_NOTIFICATION:
d3195f0a 383 case BroadcastChannelProcedureName.METER_VALUES:
9bf0ef23 384 if (isEmptyObject(commandResponse) === true) {
10db00b2
JB
385 return ResponseStatus.SUCCESS;
386 }
387 return ResponseStatus.FAILURE;
388 case BroadcastChannelProcedureName.HEARTBEAT:
389 if ('currentTime' in commandResponse) {
390 return ResponseStatus.SUCCESS;
391 }
392 return ResponseStatus.FAILURE;
393 default:
394 return ResponseStatus.FAILURE;
89b7a234
JB
395 }
396 }
397}