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