Commit | Line | Data |
---|---|---|
66a7748d | 1 | import { secondsToMilliseconds } from 'date-fns' |
38ae4ce2 | 2 | import { isEmpty } from 'rambda' |
be4c6702 | 3 | |
66a7748d | 4 | import { BaseError, type OCPPError } from '../../exception/index.js' |
10db00b2 | 5 | import { |
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 | 40 | import { Constants, convertToInt, isAsyncFunction, logger } from '../../utils/index.js' |
66a7748d JB |
41 | import type { ChargingStation } from '../ChargingStation.js' |
42 | import { getConfigurationKey } from '../ConfigurationKeyUtils.js' | |
43 | import { buildMeterValue } from '../ocpp/index.js' | |
4c3f6c20 | 44 | import { WorkerBroadcastChannel } from './WorkerBroadcastChannel.js' |
89b7a234 | 45 | |
66a7748d | 46 | const moduleName = 'ChargingStationWorkerBroadcastChannel' |
4e3ff94d | 47 | |
a9ed42b2 | 48 | type 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 | 57 | type 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 | 62 | export 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 | } |