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