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