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