refactor: cleanup UI service response
[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
ba9a56a6
JB
288 this.commandHandler(command, requestPayload)
289 .then(commandResponse => {
290 if (commandResponse == null || isEmptyObject(commandResponse)) {
291 responsePayload = {
292 hashId: this.chargingStation.stationInfo?.hashId,
293 status: ResponseStatus.SUCCESS
294 }
295 } else {
296 responsePayload = this.commandResponseToResponsePayload(
297 command,
298 requestPayload,
299 commandResponse
300 )
301 }
302 })
303 .catch(error => {
304 logger.error(
305 `${this.chargingStation.logPrefix()} ${moduleName}.requestHandler: Handle request error:`,
306 error
307 )
10d244c0 308 responsePayload = {
5199f9fd 309 hashId: this.chargingStation.stationInfo?.hashId,
ba9a56a6 310 status: ResponseStatus.FAILURE,
1984f194 311 command,
d3195f0a 312 requestPayload,
ba9a56a6
JB
313 errorMessage: (error as OCPPError).message,
314 errorStack: (error as OCPPError).stack,
315 errorDetails: (error as OCPPError).details
859099b6 316 } satisfies BroadcastChannelResponsePayload
ba9a56a6
JB
317 })
318 .finally(() => {
66a7748d 319 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
ba9a56a6
JB
320 this.sendResponse([uuid, responsePayload!])
321 })
6c8f5d90
JB
322 }
323
66a7748d 324 private messageErrorHandler (messageEvent: MessageEvent): void {
6c8f5d90
JB
325 logger.error(
326 `${this.chargingStation.logPrefix()} ${moduleName}.messageErrorHandler: Error at handling message:`,
66a7748d
JB
327 messageEvent
328 )
6c8f5d90
JB
329 }
330
66a7748d 331 private async commandHandler (
6c8f5d90 332 command: BroadcastChannelProcedureName,
66a7748d
JB
333 requestPayload: BroadcastChannelRequestPayload
334 // eslint-disable-next-line @typescript-eslint/no-invalid-void-type
cc6845fc 335 ): Promise<CommandResponse | void> {
66a7748d
JB
336 if (this.commandHandlers.has(command)) {
337 this.cleanRequestPayload(command, requestPayload)
338 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
4b9332af
JB
339 const commandHandler = this.commandHandlers.get(command)!
340 if (isAsyncFunction(commandHandler)) {
341 return await commandHandler(requestPayload)
342 }
343 return (
7f79ef45
JB
344 commandHandler as (
345 requestPayload?: BroadcastChannelRequestPayload
346 // eslint-disable-next-line @typescript-eslint/no-invalid-void-type
347 ) => CommandResponse | void
4b9332af 348 )(requestPayload)
6c8f5d90 349 }
66a7748d 350 throw new BaseError(`Unknown worker broadcast channel command: '${command}'`)
6c8f5d90
JB
351 }
352
66a7748d 353 private cleanRequestPayload (
1984f194 354 command: BroadcastChannelProcedureName,
66a7748d 355 requestPayload: BroadcastChannelRequestPayload
1984f194 356 ): void {
66a7748d
JB
357 delete requestPayload.hashId
358 delete requestPayload.hashIds
359 ![
1984f194 360 BroadcastChannelProcedureName.START_AUTOMATIC_TRANSACTION_GENERATOR,
66a7748d
JB
361 BroadcastChannelProcedureName.STOP_AUTOMATIC_TRANSACTION_GENERATOR
362 ].includes(command) && delete requestPayload.connectorIds
1984f194
JB
363 }
364
66a7748d 365 private commandResponseToResponsePayload (
d3195f0a
JB
366 command: BroadcastChannelProcedureName,
367 requestPayload: BroadcastChannelRequestPayload,
66a7748d 368 commandResponse: CommandResponse
d3195f0a 369 ): BroadcastChannelResponsePayload {
66a7748d 370 const responseStatus = this.commandResponseToResponseStatus(command, commandResponse)
cfa257f5 371 if (responseStatus === ResponseStatus.SUCCESS) {
d3195f0a 372 return {
5199f9fd 373 hashId: this.chargingStation.stationInfo?.hashId,
66a7748d
JB
374 status: responseStatus
375 }
d3195f0a
JB
376 }
377 return {
5199f9fd 378 hashId: this.chargingStation.stationInfo?.hashId,
cfa257f5 379 status: responseStatus,
d3195f0a
JB
380 command,
381 requestPayload,
66a7748d
JB
382 commandResponse
383 }
d3195f0a
JB
384 }
385
66a7748d 386 private commandResponseToResponseStatus (
10db00b2 387 command: BroadcastChannelProcedureName,
66a7748d 388 commandResponse: CommandResponse
10db00b2
JB
389 ): ResponseStatus {
390 switch (command) {
391 case BroadcastChannelProcedureName.START_TRANSACTION:
392 case BroadcastChannelProcedureName.STOP_TRANSACTION:
1984f194 393 case BroadcastChannelProcedureName.AUTHORIZE:
10db00b2 394 if (
1984f194
JB
395 (
396 commandResponse as
397 | StartTransactionResponse
398 | StopTransactionResponse
399 | AuthorizeResponse
5199f9fd 400 ).idTagInfo?.status === AuthorizationStatus.ACCEPTED
10db00b2 401 ) {
66a7748d 402 return ResponseStatus.SUCCESS
10db00b2 403 }
66a7748d 404 return ResponseStatus.FAILURE
8bfbc743 405 case BroadcastChannelProcedureName.BOOT_NOTIFICATION:
5199f9fd 406 if (commandResponse.status === RegistrationStatusEnumType.ACCEPTED) {
66a7748d 407 return ResponseStatus.SUCCESS
8bfbc743 408 }
66a7748d 409 return ResponseStatus.FAILURE
91a7d3ea 410 case BroadcastChannelProcedureName.DATA_TRANSFER:
5199f9fd 411 if (commandResponse.status === DataTransferStatus.ACCEPTED) {
66a7748d 412 return ResponseStatus.SUCCESS
91a7d3ea 413 }
66a7748d 414 return ResponseStatus.FAILURE
10db00b2 415 case BroadcastChannelProcedureName.STATUS_NOTIFICATION:
d3195f0a 416 case BroadcastChannelProcedureName.METER_VALUES:
66a7748d
JB
417 if (isEmptyObject(commandResponse)) {
418 return ResponseStatus.SUCCESS
10db00b2 419 }
66a7748d 420 return ResponseStatus.FAILURE
10db00b2
JB
421 case BroadcastChannelProcedureName.HEARTBEAT:
422 if ('currentTime' in commandResponse) {
66a7748d 423 return ResponseStatus.SUCCESS
10db00b2 424 }
66a7748d 425 return ResponseStatus.FAILURE
10db00b2 426 default:
66a7748d 427 return ResponseStatus.FAILURE
89b7a234
JB
428 }
429 }
430}