refactor: cleanup connection retries logic
[e-mobility-charging-stations-simulator.git] / src / charging-station / broadcast-channel / ChargingStationWorkerBroadcastChannel.ts
CommitLineData
66a7748d 1import { secondsToMilliseconds } from 'date-fns'
38ae4ce2 2import { isEmpty } from 'rambda'
be4c6702 3
66a7748d 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'
38ae4ce2 40import { Constants, convertToInt, isAsyncFunction, logger } from '../../utils/index.js'
66a7748d
JB
41import type { ChargingStation } from '../ChargingStation.js'
42import { getConfigurationKey } from '../ConfigurationKeyUtils.js'
43import { buildMeterValue } from '../ocpp/index.js'
4c3f6c20 44import { WorkerBroadcastChannel } from './WorkerBroadcastChannel.js'
89b7a234 45
66a7748d 46const moduleName = 'ChargingStationWorkerBroadcastChannel'
4e3ff94d 47
a9ed42b2 48type CommandResponse =
346b47e0 49 | EmptyObject
a9ed42b2
JB
50 | StartTransactionResponse
51 | StopTransactionResponse
1984f194 52 | AuthorizeResponse
8bfbc743 53 | BootNotificationResponse
d3195f0a 54 | HeartbeatResponse
66a7748d 55 | DataTransferResponse
89b7a234 56
d273692c 57type CommandHandler = (
66a7748d
JB
58 requestPayload?: BroadcastChannelRequestPayload
59 // eslint-disable-next-line @typescript-eslint/no-invalid-void-type
4b9332af 60) => Promise<CommandResponse | void> | CommandResponse | void
d273692c 61
268a74bb 62export class ChargingStationWorkerBroadcastChannel extends WorkerBroadcastChannel {
66a7748d
JB
63 private readonly commandHandlers: Map<BroadcastChannelProcedureName, CommandHandler>
64 private readonly chargingStation: ChargingStation
89b7a234 65
66a7748d
JB
66 constructor (chargingStation: ChargingStation) {
67 super()
8ec8e3d0 68 const requestParams: RequestParams = {
66a7748d
JB
69 throwError: true
70 }
d273692c 71 this.commandHandlers = new Map<BroadcastChannelProcedureName, CommandHandler>([
66a7748d
JB
72 [
73 BroadcastChannelProcedureName.START_CHARGING_STATION,
74 () => {
75 this.chargingStation.start()
76 }
77 ],
9d73266c
JB
78 [
79 BroadcastChannelProcedureName.STOP_CHARGING_STATION,
66a7748d
JB
80 async () => {
81 await this.chargingStation.stop()
82 }
9d73266c 83 ],
09e5a7a8
JB
84 [
85 BroadcastChannelProcedureName.DELETE_CHARGING_STATIONS,
86 async (requestPayload?: BroadcastChannelRequestPayload) => {
87 await this.chargingStation.delete(requestPayload?.deleteConfiguration as boolean)
88 }
89 ],
9d73266c
JB
90 [
91 BroadcastChannelProcedureName.OPEN_CONNECTION,
66a7748d
JB
92 () => {
93 this.chargingStation.openWSConnection()
94 }
9d73266c
JB
95 ],
96 [
97 BroadcastChannelProcedureName.CLOSE_CONNECTION,
66a7748d
JB
98 () => {
99 this.chargingStation.closeWSConnection()
100 }
9d73266c 101 ],
623b39b5
JB
102 [
103 BroadcastChannelProcedureName.START_AUTOMATIC_TRANSACTION_GENERATOR,
66a7748d
JB
104 (requestPayload?: BroadcastChannelRequestPayload) => {
105 this.chargingStation.startAutomaticTransactionGenerator(requestPayload?.connectorIds)
106 }
623b39b5
JB
107 ],
108 [
109 BroadcastChannelProcedureName.STOP_AUTOMATIC_TRANSACTION_GENERATOR,
66a7748d
JB
110 (requestPayload?: BroadcastChannelRequestPayload) => {
111 this.chargingStation.stopAutomaticTransactionGenerator(requestPayload?.connectorIds)
112 }
623b39b5 113 ],
269de583
JB
114 [
115 BroadcastChannelProcedureName.SET_SUPERVISION_URL,
66a7748d
JB
116 (requestPayload?: BroadcastChannelRequestPayload) => {
117 this.chargingStation.setSupervisionUrl(requestPayload?.url as string)
118 }
269de583 119 ],
9d73266c
JB
120 [
121 BroadcastChannelProcedureName.START_TRANSACTION,
122 async (requestPayload?: BroadcastChannelRequestPayload) =>
66a7748d
JB
123 await this.chargingStation.ocppRequestService.requestHandler<
124 StartTransactionRequest,
125 StartTransactionResponse
126 >(this.chargingStation, RequestCommand.START_TRANSACTION, requestPayload, requestParams)
9d73266c
JB
127 ],
128 [
129 BroadcastChannelProcedureName.STOP_TRANSACTION,
130 async (requestPayload?: BroadcastChannelRequestPayload) =>
66a7748d
JB
131 await this.chargingStation.ocppRequestService.requestHandler<
132 StopTransactionRequest,
133 StartTransactionResponse
1969f643
JB
134 >(
135 this.chargingStation,
136 RequestCommand.STOP_TRANSACTION,
137 {
138 meterStop: this.chargingStation.getEnergyActiveImportRegisterByTransactionId(
f938317f 139 requestPayload?.transactionId,
66a7748d 140 true
1969f643 141 ),
66a7748d 142 ...requestPayload
1969f643 143 },
66a7748d
JB
144 requestParams
145 )
9d73266c 146 ],
1984f194
JB
147 [
148 BroadcastChannelProcedureName.AUTHORIZE,
149 async (requestPayload?: BroadcastChannelRequestPayload) =>
66a7748d
JB
150 await this.chargingStation.ocppRequestService.requestHandler<
151 AuthorizeRequest,
152 AuthorizeResponse
153 >(this.chargingStation, RequestCommand.AUTHORIZE, requestPayload, requestParams)
1984f194 154 ],
8bfbc743
JB
155 [
156 BroadcastChannelProcedureName.BOOT_NOTIFICATION,
157 async (requestPayload?: BroadcastChannelRequestPayload) => {
d9c13bca 158 return await this.chargingStation.ocppRequestService.requestHandler<
ab29e682
JB
159 BootNotificationRequest,
160 BootNotificationResponse
161 >(
162 this.chargingStation,
163 RequestCommand.BOOT_NOTIFICATION,
164 {
165 ...this.chargingStation.bootNotificationRequest,
166 ...requestPayload
167 },
168 {
169 skipBufferingOnError: true,
170 throwError: true
171 }
172 )
66a7748d 173 }
8bfbc743 174 ],
9d73266c
JB
175 [
176 BroadcastChannelProcedureName.STATUS_NOTIFICATION,
177 async (requestPayload?: BroadcastChannelRequestPayload) =>
66a7748d
JB
178 await this.chargingStation.ocppRequestService.requestHandler<
179 StatusNotificationRequest,
180 StatusNotificationResponse
a223d9be 181 >(this.chargingStation, RequestCommand.STATUS_NOTIFICATION, requestPayload, requestParams)
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
ba9a56a6 264 private requestHandler (messageEvent: MessageEvent): void {
66a7748d 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
ba9a56a6
JB
287 this.commandHandler(command, requestPayload)
288 .then(commandResponse => {
38ae4ce2 289 if (commandResponse == null || isEmpty(commandResponse)) {
ba9a56a6
JB
290 responsePayload = {
291 hashId: this.chargingStation.stationInfo?.hashId,
292 status: ResponseStatus.SUCCESS
293 }
294 } else {
295 responsePayload = this.commandResponseToResponsePayload(
296 command,
297 requestPayload,
298 commandResponse
299 )
300 }
301 })
ea32ea05 302 .catch((error: unknown) => {
ba9a56a6
JB
303 logger.error(
304 `${this.chargingStation.logPrefix()} ${moduleName}.requestHandler: Handle request error:`,
305 error
306 )
10d244c0 307 responsePayload = {
5199f9fd 308 hashId: this.chargingStation.stationInfo?.hashId,
ba9a56a6 309 status: ResponseStatus.FAILURE,
1984f194 310 command,
d3195f0a 311 requestPayload,
ba9a56a6
JB
312 errorMessage: (error as OCPPError).message,
313 errorStack: (error as OCPPError).stack,
314 errorDetails: (error as OCPPError).details
859099b6 315 } satisfies BroadcastChannelResponsePayload
ba9a56a6
JB
316 })
317 .finally(() => {
66a7748d 318 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
ba9a56a6
JB
319 this.sendResponse([uuid, responsePayload!])
320 })
6c8f5d90
JB
321 }
322
66a7748d 323 private messageErrorHandler (messageEvent: MessageEvent): void {
6c8f5d90
JB
324 logger.error(
325 `${this.chargingStation.logPrefix()} ${moduleName}.messageErrorHandler: Error at handling message:`,
66a7748d
JB
326 messageEvent
327 )
6c8f5d90
JB
328 }
329
66a7748d 330 private async commandHandler (
6c8f5d90 331 command: BroadcastChannelProcedureName,
66a7748d
JB
332 requestPayload: BroadcastChannelRequestPayload
333 // eslint-disable-next-line @typescript-eslint/no-invalid-void-type
cc6845fc 334 ): Promise<CommandResponse | void> {
66a7748d
JB
335 if (this.commandHandlers.has(command)) {
336 this.cleanRequestPayload(command, requestPayload)
337 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
4b9332af
JB
338 const commandHandler = this.commandHandlers.get(command)!
339 if (isAsyncFunction(commandHandler)) {
340 return await commandHandler(requestPayload)
341 }
342 return (
7f79ef45
JB
343 commandHandler as (
344 requestPayload?: BroadcastChannelRequestPayload
345 // eslint-disable-next-line @typescript-eslint/no-invalid-void-type
346 ) => CommandResponse | void
4b9332af 347 )(requestPayload)
6c8f5d90 348 }
66a7748d 349 throw new BaseError(`Unknown worker broadcast channel command: '${command}'`)
6c8f5d90
JB
350 }
351
66a7748d 352 private cleanRequestPayload (
1984f194 353 command: BroadcastChannelProcedureName,
66a7748d 354 requestPayload: BroadcastChannelRequestPayload
1984f194 355 ): void {
66a7748d
JB
356 delete requestPayload.hashId
357 delete requestPayload.hashIds
358 ![
1984f194 359 BroadcastChannelProcedureName.START_AUTOMATIC_TRANSACTION_GENERATOR,
66a7748d
JB
360 BroadcastChannelProcedureName.STOP_AUTOMATIC_TRANSACTION_GENERATOR
361 ].includes(command) && delete requestPayload.connectorIds
1984f194
JB
362 }
363
66a7748d 364 private commandResponseToResponsePayload (
d3195f0a
JB
365 command: BroadcastChannelProcedureName,
366 requestPayload: BroadcastChannelRequestPayload,
66a7748d 367 commandResponse: CommandResponse
d3195f0a 368 ): BroadcastChannelResponsePayload {
66a7748d 369 const responseStatus = this.commandResponseToResponseStatus(command, commandResponse)
cfa257f5 370 if (responseStatus === ResponseStatus.SUCCESS) {
d3195f0a 371 return {
5199f9fd 372 hashId: this.chargingStation.stationInfo?.hashId,
66a7748d
JB
373 status: responseStatus
374 }
d3195f0a
JB
375 }
376 return {
5199f9fd 377 hashId: this.chargingStation.stationInfo?.hashId,
cfa257f5 378 status: responseStatus,
d3195f0a
JB
379 command,
380 requestPayload,
66a7748d
JB
381 commandResponse
382 }
d3195f0a
JB
383 }
384
66a7748d 385 private commandResponseToResponseStatus (
10db00b2 386 command: BroadcastChannelProcedureName,
66a7748d 387 commandResponse: CommandResponse
10db00b2
JB
388 ): ResponseStatus {
389 switch (command) {
390 case BroadcastChannelProcedureName.START_TRANSACTION:
391 case BroadcastChannelProcedureName.STOP_TRANSACTION:
1984f194 392 case BroadcastChannelProcedureName.AUTHORIZE:
10db00b2 393 if (
1984f194
JB
394 (
395 commandResponse as
396 | StartTransactionResponse
397 | StopTransactionResponse
398 | AuthorizeResponse
5199f9fd 399 ).idTagInfo?.status === AuthorizationStatus.ACCEPTED
10db00b2 400 ) {
66a7748d 401 return ResponseStatus.SUCCESS
10db00b2 402 }
66a7748d 403 return ResponseStatus.FAILURE
8bfbc743 404 case BroadcastChannelProcedureName.BOOT_NOTIFICATION:
5199f9fd 405 if (commandResponse.status === RegistrationStatusEnumType.ACCEPTED) {
66a7748d 406 return ResponseStatus.SUCCESS
8bfbc743 407 }
66a7748d 408 return ResponseStatus.FAILURE
91a7d3ea 409 case BroadcastChannelProcedureName.DATA_TRANSFER:
5199f9fd 410 if (commandResponse.status === DataTransferStatus.ACCEPTED) {
66a7748d 411 return ResponseStatus.SUCCESS
91a7d3ea 412 }
66a7748d 413 return ResponseStatus.FAILURE
10db00b2 414 case BroadcastChannelProcedureName.STATUS_NOTIFICATION:
d3195f0a 415 case BroadcastChannelProcedureName.METER_VALUES:
38ae4ce2 416 if (isEmpty(commandResponse)) {
66a7748d 417 return ResponseStatus.SUCCESS
10db00b2 418 }
66a7748d 419 return ResponseStatus.FAILURE
10db00b2
JB
420 case BroadcastChannelProcedureName.HEARTBEAT:
421 if ('currentTime' in commandResponse) {
66a7748d 422 return ResponseStatus.SUCCESS
10db00b2 423 }
66a7748d 424 return ResponseStatus.FAILURE
10db00b2 425 default:
66a7748d 426 return ResponseStatus.FAILURE
89b7a234
JB
427 }
428 }
429}