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