refactor: convert home made helpers to rambda ones
[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) => {
158 this.chargingStation.bootNotificationResponse =
159 await this.chargingStation.ocppRequestService.requestHandler<
66a7748d
JB
160 BootNotificationRequest,
161 BootNotificationResponse
8bfbc743
JB
162 >(
163 this.chargingStation,
164 RequestCommand.BOOT_NOTIFICATION,
165 {
166 ...this.chargingStation.bootNotificationRequest,
66a7748d 167 ...requestPayload
8bfbc743
JB
168 },
169 {
170 skipBufferingOnError: true,
66a7748d
JB
171 throwError: true
172 }
173 )
174 return this.chargingStation.bootNotificationResponse
175 }
8bfbc743 176 ],
9d73266c
JB
177 [
178 BroadcastChannelProcedureName.STATUS_NOTIFICATION,
179 async (requestPayload?: BroadcastChannelRequestPayload) =>
66a7748d
JB
180 await this.chargingStation.ocppRequestService.requestHandler<
181 StatusNotificationRequest,
182 StatusNotificationResponse
a223d9be 183 >(this.chargingStation, RequestCommand.STATUS_NOTIFICATION, requestPayload, requestParams)
9d73266c
JB
184 ],
185 [
186 BroadcastChannelProcedureName.HEARTBEAT,
1984f194 187 async (requestPayload?: BroadcastChannelRequestPayload) =>
66a7748d
JB
188 await this.chargingStation.ocppRequestService.requestHandler<
189 HeartbeatRequest,
190 HeartbeatResponse
191 >(this.chargingStation, RequestCommand.HEARTBEAT, requestPayload, requestParams)
9d73266c 192 ],
d3195f0a
JB
193 [
194 BroadcastChannelProcedureName.METER_VALUES,
195 async (requestPayload?: BroadcastChannelRequestPayload) => {
f2d5e3d9
JB
196 const configuredMeterValueSampleInterval = getConfigurationKey(
197 chargingStation,
66a7748d
JB
198 StandardParametersKey.MeterValueSampleInterval
199 )
200 return await this.chargingStation.ocppRequestService.requestHandler<
201 MeterValuesRequest,
202 MeterValuesResponse
1969f643
JB
203 >(
204 this.chargingStation,
205 RequestCommand.METER_VALUES,
206 {
207 meterValue: [
41f3983a 208 buildMeterValue(
1969f643 209 this.chargingStation,
66a7748d 210 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
e1d9a0f4 211 requestPayload!.connectorId!,
66a7748d 212 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
e1d9a0f4
JB
213 this.chargingStation.getConnectorStatus(requestPayload!.connectorId!)!
214 .transactionId!,
a807045b 215 configuredMeterValueSampleInterval != null
be4c6702 216 ? secondsToMilliseconds(convertToInt(configuredMeterValueSampleInterval.value))
66a7748d
JB
217 : Constants.DEFAULT_METER_VALUES_INTERVAL
218 )
1969f643 219 ],
66a7748d 220 ...requestPayload
1969f643 221 },
66a7748d
JB
222 requestParams
223 )
224 }
d3195f0a 225 ],
91a7d3ea
JB
226 [
227 BroadcastChannelProcedureName.DATA_TRANSFER,
228 async (requestPayload?: BroadcastChannelRequestPayload) =>
66a7748d
JB
229 await this.chargingStation.ocppRequestService.requestHandler<
230 DataTransferRequest,
231 DataTransferResponse
232 >(this.chargingStation, RequestCommand.DATA_TRANSFER, requestPayload, requestParams)
91a7d3ea 233 ],
c9a4f9ea
JB
234 [
235 BroadcastChannelProcedureName.DIAGNOSTICS_STATUS_NOTIFICATION,
236 async (requestPayload?: BroadcastChannelRequestPayload) =>
66a7748d
JB
237 await this.chargingStation.ocppRequestService.requestHandler<
238 DiagnosticsStatusNotificationRequest,
239 DiagnosticsStatusNotificationResponse
8ec8e3d0
JB
240 >(
241 this.chargingStation,
242 RequestCommand.DIAGNOSTICS_STATUS_NOTIFICATION,
243 requestPayload,
66a7748d
JB
244 requestParams
245 )
c9a4f9ea
JB
246 ],
247 [
248 BroadcastChannelProcedureName.FIRMWARE_STATUS_NOTIFICATION,
249 async (requestPayload?: BroadcastChannelRequestPayload) =>
66a7748d
JB
250 await this.chargingStation.ocppRequestService.requestHandler<
251 FirmwareStatusNotificationRequest,
252 FirmwareStatusNotificationResponse
8ec8e3d0
JB
253 >(
254 this.chargingStation,
255 RequestCommand.FIRMWARE_STATUS_NOTIFICATION,
256 requestPayload,
66a7748d
JB
257 requestParams
258 )
259 ]
260 ])
261 this.chargingStation = chargingStation
262 this.onmessage = this.requestHandler.bind(this) as (message: unknown) => void
263 this.onmessageerror = this.messageErrorHandler.bind(this) as (message: unknown) => void
89b7a234
JB
264 }
265
ba9a56a6 266 private requestHandler (messageEvent: MessageEvent): void {
66a7748d 267 const validatedMessageEvent = this.validateMessageEvent(messageEvent)
5dea4c94 268 if (validatedMessageEvent === false) {
66a7748d 269 return
6c8f5d90 270 }
66a7748d
JB
271 if (this.isResponse(validatedMessageEvent.data)) {
272 return
5dea4c94 273 }
66a7748d 274 const [uuid, command, requestPayload] = validatedMessageEvent.data as BroadcastChannelRequest
2afb4d15 275 if (
401fa922 276 requestPayload.hashIds != null &&
5199f9fd
JB
277 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
278 !requestPayload.hashIds.includes(this.chargingStation.stationInfo!.hashId)
2afb4d15 279 ) {
66a7748d 280 return
2afb4d15 281 }
401fa922 282 if (requestPayload.hashId != null) {
2afb4d15 283 logger.error(
66a7748d
JB
284 `${this.chargingStation.logPrefix()} ${moduleName}.requestHandler: 'hashId' field usage in PDU is deprecated, use 'hashIds' array instead`
285 )
286 return
4eca248c 287 }
66a7748d 288 let responsePayload: BroadcastChannelResponsePayload | undefined
ba9a56a6
JB
289 this.commandHandler(command, requestPayload)
290 .then(commandResponse => {
38ae4ce2 291 if (commandResponse == null || isEmpty(commandResponse)) {
ba9a56a6
JB
292 responsePayload = {
293 hashId: this.chargingStation.stationInfo?.hashId,
294 status: ResponseStatus.SUCCESS
295 }
296 } else {
297 responsePayload = this.commandResponseToResponsePayload(
298 command,
299 requestPayload,
300 commandResponse
301 )
302 }
303 })
304 .catch(error => {
305 logger.error(
306 `${this.chargingStation.logPrefix()} ${moduleName}.requestHandler: Handle request error:`,
307 error
308 )
10d244c0 309 responsePayload = {
5199f9fd 310 hashId: this.chargingStation.stationInfo?.hashId,
ba9a56a6 311 status: ResponseStatus.FAILURE,
1984f194 312 command,
d3195f0a 313 requestPayload,
ba9a56a6
JB
314 errorMessage: (error as OCPPError).message,
315 errorStack: (error as OCPPError).stack,
316 errorDetails: (error as OCPPError).details
859099b6 317 } satisfies BroadcastChannelResponsePayload
ba9a56a6
JB
318 })
319 .finally(() => {
66a7748d 320 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
ba9a56a6
JB
321 this.sendResponse([uuid, responsePayload!])
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
4b9332af
JB
340 const commandHandler = this.commandHandlers.get(command)!
341 if (isAsyncFunction(commandHandler)) {
342 return await commandHandler(requestPayload)
343 }
344 return (
7f79ef45
JB
345 commandHandler as (
346 requestPayload?: BroadcastChannelRequestPayload
347 // eslint-disable-next-line @typescript-eslint/no-invalid-void-type
348 ) => CommandResponse | void
4b9332af 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:
38ae4ce2 418 if (isEmpty(commandResponse)) {
66a7748d 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}