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