Commit | Line | Data |
---|---|---|
66a7748d | 1 | import { secondsToMilliseconds } from 'date-fns' |
be4c6702 | 2 | |
66a7748d JB |
3 | import { WorkerBroadcastChannel } from './WorkerBroadcastChannel.js' |
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' | |
401fa922 | 40 | import { Constants, convertToInt, isEmptyObject, 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' | |
89b7a234 | 44 | |
66a7748d | 45 | const moduleName = 'ChargingStationWorkerBroadcastChannel' |
4e3ff94d | 46 | |
a9ed42b2 | 47 | type 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 | 56 | type 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 | 61 | export 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 | |
a223d9be | 176 | >(this.chargingStation, RequestCommand.STATUS_NOTIFICATION, requestPayload, requestParams) |
9d73266c JB |
177 | ], |
178 | [ | |
179 | BroadcastChannelProcedureName.HEARTBEAT, | |
1984f194 | 180 | async (requestPayload?: BroadcastChannelRequestPayload) => |
66a7748d JB |
181 | await this.chargingStation.ocppRequestService.requestHandler< |
182 | HeartbeatRequest, | |
183 | HeartbeatResponse | |
184 | >(this.chargingStation, RequestCommand.HEARTBEAT, requestPayload, requestParams) | |
9d73266c | 185 | ], |
d3195f0a JB |
186 | [ |
187 | BroadcastChannelProcedureName.METER_VALUES, | |
188 | async (requestPayload?: BroadcastChannelRequestPayload) => { | |
f2d5e3d9 JB |
189 | const configuredMeterValueSampleInterval = getConfigurationKey( |
190 | chargingStation, | |
66a7748d JB |
191 | StandardParametersKey.MeterValueSampleInterval |
192 | ) | |
193 | return await this.chargingStation.ocppRequestService.requestHandler< | |
194 | MeterValuesRequest, | |
195 | MeterValuesResponse | |
1969f643 JB |
196 | >( |
197 | this.chargingStation, | |
198 | RequestCommand.METER_VALUES, | |
199 | { | |
200 | meterValue: [ | |
41f3983a | 201 | buildMeterValue( |
1969f643 | 202 | this.chargingStation, |
66a7748d | 203 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion |
e1d9a0f4 | 204 | requestPayload!.connectorId!, |
66a7748d | 205 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion |
e1d9a0f4 JB |
206 | this.chargingStation.getConnectorStatus(requestPayload!.connectorId!)! |
207 | .transactionId!, | |
a807045b | 208 | configuredMeterValueSampleInterval != null |
be4c6702 | 209 | ? secondsToMilliseconds(convertToInt(configuredMeterValueSampleInterval.value)) |
66a7748d JB |
210 | : Constants.DEFAULT_METER_VALUES_INTERVAL |
211 | ) | |
1969f643 | 212 | ], |
66a7748d | 213 | ...requestPayload |
1969f643 | 214 | }, |
66a7748d JB |
215 | requestParams |
216 | ) | |
217 | } | |
d3195f0a | 218 | ], |
91a7d3ea JB |
219 | [ |
220 | BroadcastChannelProcedureName.DATA_TRANSFER, | |
221 | async (requestPayload?: BroadcastChannelRequestPayload) => | |
66a7748d JB |
222 | await this.chargingStation.ocppRequestService.requestHandler< |
223 | DataTransferRequest, | |
224 | DataTransferResponse | |
225 | >(this.chargingStation, RequestCommand.DATA_TRANSFER, requestPayload, requestParams) | |
91a7d3ea | 226 | ], |
c9a4f9ea JB |
227 | [ |
228 | BroadcastChannelProcedureName.DIAGNOSTICS_STATUS_NOTIFICATION, | |
229 | async (requestPayload?: BroadcastChannelRequestPayload) => | |
66a7748d JB |
230 | await this.chargingStation.ocppRequestService.requestHandler< |
231 | DiagnosticsStatusNotificationRequest, | |
232 | DiagnosticsStatusNotificationResponse | |
8ec8e3d0 JB |
233 | >( |
234 | this.chargingStation, | |
235 | RequestCommand.DIAGNOSTICS_STATUS_NOTIFICATION, | |
236 | requestPayload, | |
66a7748d JB |
237 | requestParams |
238 | ) | |
c9a4f9ea JB |
239 | ], |
240 | [ | |
241 | BroadcastChannelProcedureName.FIRMWARE_STATUS_NOTIFICATION, | |
242 | async (requestPayload?: BroadcastChannelRequestPayload) => | |
66a7748d JB |
243 | await this.chargingStation.ocppRequestService.requestHandler< |
244 | FirmwareStatusNotificationRequest, | |
245 | FirmwareStatusNotificationResponse | |
8ec8e3d0 JB |
246 | >( |
247 | this.chargingStation, | |
248 | RequestCommand.FIRMWARE_STATUS_NOTIFICATION, | |
249 | requestPayload, | |
66a7748d JB |
250 | requestParams |
251 | ) | |
252 | ] | |
253 | ]) | |
254 | this.chargingStation = chargingStation | |
255 | this.onmessage = this.requestHandler.bind(this) as (message: unknown) => void | |
256 | this.onmessageerror = this.messageErrorHandler.bind(this) as (message: unknown) => void | |
89b7a234 JB |
257 | } |
258 | ||
ba9a56a6 | 259 | private requestHandler (messageEvent: MessageEvent): void { |
66a7748d | 260 | const validatedMessageEvent = this.validateMessageEvent(messageEvent) |
5dea4c94 | 261 | if (validatedMessageEvent === false) { |
66a7748d | 262 | return |
6c8f5d90 | 263 | } |
66a7748d JB |
264 | if (this.isResponse(validatedMessageEvent.data)) { |
265 | return | |
5dea4c94 | 266 | } |
66a7748d | 267 | const [uuid, command, requestPayload] = validatedMessageEvent.data as BroadcastChannelRequest |
2afb4d15 | 268 | if ( |
401fa922 | 269 | requestPayload.hashIds != null && |
5199f9fd JB |
270 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion |
271 | !requestPayload.hashIds.includes(this.chargingStation.stationInfo!.hashId) | |
2afb4d15 | 272 | ) { |
66a7748d | 273 | return |
2afb4d15 | 274 | } |
401fa922 | 275 | if (requestPayload.hashId != null) { |
2afb4d15 | 276 | logger.error( |
66a7748d JB |
277 | `${this.chargingStation.logPrefix()} ${moduleName}.requestHandler: 'hashId' field usage in PDU is deprecated, use 'hashIds' array instead` |
278 | ) | |
279 | return | |
4eca248c | 280 | } |
66a7748d | 281 | let responsePayload: BroadcastChannelResponsePayload | undefined |
a807045b | 282 | // eslint-disable-next-line @typescript-eslint/no-invalid-void-type |
cc6845fc | 283 | let commandResponse: CommandResponse | void |
ba9a56a6 JB |
284 | this.commandHandler(command, requestPayload) |
285 | .then(commandResponse => { | |
286 | if (commandResponse == null || isEmptyObject(commandResponse)) { | |
287 | responsePayload = { | |
288 | hashId: this.chargingStation.stationInfo?.hashId, | |
289 | status: ResponseStatus.SUCCESS | |
290 | } | |
291 | } else { | |
292 | responsePayload = this.commandResponseToResponsePayload( | |
293 | command, | |
294 | requestPayload, | |
295 | commandResponse | |
296 | ) | |
297 | } | |
298 | }) | |
299 | .catch(error => { | |
300 | logger.error( | |
301 | `${this.chargingStation.logPrefix()} ${moduleName}.requestHandler: Handle request error:`, | |
302 | error | |
303 | ) | |
10d244c0 | 304 | responsePayload = { |
5199f9fd | 305 | hashId: this.chargingStation.stationInfo?.hashId, |
ba9a56a6 | 306 | status: ResponseStatus.FAILURE, |
1984f194 | 307 | command, |
d3195f0a | 308 | requestPayload, |
ba9a56a6 JB |
309 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion |
310 | commandResponse: commandResponse!, | |
311 | errorMessage: (error as OCPPError).message, | |
312 | errorStack: (error as OCPPError).stack, | |
313 | errorDetails: (error as OCPPError).details | |
314 | } | |
315 | }) | |
316 | .finally(() => { | |
66a7748d | 317 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion |
ba9a56a6 JB |
318 | this.sendResponse([uuid, responsePayload!]) |
319 | }) | |
6c8f5d90 JB |
320 | } |
321 | ||
66a7748d | 322 | private messageErrorHandler (messageEvent: MessageEvent): void { |
6c8f5d90 JB |
323 | logger.error( |
324 | `${this.chargingStation.logPrefix()} ${moduleName}.messageErrorHandler: Error at handling message:`, | |
66a7748d JB |
325 | messageEvent |
326 | ) | |
6c8f5d90 JB |
327 | } |
328 | ||
66a7748d | 329 | private async commandHandler ( |
6c8f5d90 | 330 | command: BroadcastChannelProcedureName, |
66a7748d JB |
331 | requestPayload: BroadcastChannelRequestPayload |
332 | // eslint-disable-next-line @typescript-eslint/no-invalid-void-type | |
cc6845fc | 333 | ): Promise<CommandResponse | void> { |
66a7748d JB |
334 | if (this.commandHandlers.has(command)) { |
335 | this.cleanRequestPayload(command, requestPayload) | |
336 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion | |
337 | return await this.commandHandlers.get(command)!(requestPayload) | |
6c8f5d90 | 338 | } |
66a7748d | 339 | throw new BaseError(`Unknown worker broadcast channel command: '${command}'`) |
6c8f5d90 JB |
340 | } |
341 | ||
66a7748d | 342 | private cleanRequestPayload ( |
1984f194 | 343 | command: BroadcastChannelProcedureName, |
66a7748d | 344 | requestPayload: BroadcastChannelRequestPayload |
1984f194 | 345 | ): void { |
66a7748d JB |
346 | delete requestPayload.hashId |
347 | delete requestPayload.hashIds | |
348 | ![ | |
1984f194 | 349 | BroadcastChannelProcedureName.START_AUTOMATIC_TRANSACTION_GENERATOR, |
66a7748d JB |
350 | BroadcastChannelProcedureName.STOP_AUTOMATIC_TRANSACTION_GENERATOR |
351 | ].includes(command) && delete requestPayload.connectorIds | |
1984f194 JB |
352 | } |
353 | ||
66a7748d | 354 | private commandResponseToResponsePayload ( |
d3195f0a JB |
355 | command: BroadcastChannelProcedureName, |
356 | requestPayload: BroadcastChannelRequestPayload, | |
66a7748d | 357 | commandResponse: CommandResponse |
d3195f0a | 358 | ): BroadcastChannelResponsePayload { |
66a7748d | 359 | const responseStatus = this.commandResponseToResponseStatus(command, commandResponse) |
cfa257f5 | 360 | if (responseStatus === ResponseStatus.SUCCESS) { |
d3195f0a | 361 | return { |
5199f9fd | 362 | hashId: this.chargingStation.stationInfo?.hashId, |
66a7748d JB |
363 | status: responseStatus |
364 | } | |
d3195f0a JB |
365 | } |
366 | return { | |
5199f9fd | 367 | hashId: this.chargingStation.stationInfo?.hashId, |
cfa257f5 | 368 | status: responseStatus, |
d3195f0a JB |
369 | command, |
370 | requestPayload, | |
66a7748d JB |
371 | commandResponse |
372 | } | |
d3195f0a JB |
373 | } |
374 | ||
66a7748d | 375 | private commandResponseToResponseStatus ( |
10db00b2 | 376 | command: BroadcastChannelProcedureName, |
66a7748d | 377 | commandResponse: CommandResponse |
10db00b2 JB |
378 | ): ResponseStatus { |
379 | switch (command) { | |
380 | case BroadcastChannelProcedureName.START_TRANSACTION: | |
381 | case BroadcastChannelProcedureName.STOP_TRANSACTION: | |
1984f194 | 382 | case BroadcastChannelProcedureName.AUTHORIZE: |
10db00b2 | 383 | if ( |
1984f194 JB |
384 | ( |
385 | commandResponse as | |
386 | | StartTransactionResponse | |
387 | | StopTransactionResponse | |
388 | | AuthorizeResponse | |
5199f9fd | 389 | ).idTagInfo?.status === AuthorizationStatus.ACCEPTED |
10db00b2 | 390 | ) { |
66a7748d | 391 | return ResponseStatus.SUCCESS |
10db00b2 | 392 | } |
66a7748d | 393 | return ResponseStatus.FAILURE |
8bfbc743 | 394 | case BroadcastChannelProcedureName.BOOT_NOTIFICATION: |
5199f9fd | 395 | if (commandResponse.status === RegistrationStatusEnumType.ACCEPTED) { |
66a7748d | 396 | return ResponseStatus.SUCCESS |
8bfbc743 | 397 | } |
66a7748d | 398 | return ResponseStatus.FAILURE |
91a7d3ea | 399 | case BroadcastChannelProcedureName.DATA_TRANSFER: |
5199f9fd | 400 | if (commandResponse.status === DataTransferStatus.ACCEPTED) { |
66a7748d | 401 | return ResponseStatus.SUCCESS |
91a7d3ea | 402 | } |
66a7748d | 403 | return ResponseStatus.FAILURE |
10db00b2 | 404 | case BroadcastChannelProcedureName.STATUS_NOTIFICATION: |
d3195f0a | 405 | case BroadcastChannelProcedureName.METER_VALUES: |
66a7748d JB |
406 | if (isEmptyObject(commandResponse)) { |
407 | return ResponseStatus.SUCCESS | |
10db00b2 | 408 | } |
66a7748d | 409 | return ResponseStatus.FAILURE |
10db00b2 JB |
410 | case BroadcastChannelProcedureName.HEARTBEAT: |
411 | if ('currentTime' in commandResponse) { | |
66a7748d | 412 | return ResponseStatus.SUCCESS |
10db00b2 | 413 | } |
66a7748d | 414 | return ResponseStatus.FAILURE |
10db00b2 | 415 | default: |
66a7748d | 416 | return ResponseStatus.FAILURE |
89b7a234 JB |
417 | } |
418 | } | |
419 | } |