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