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