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