Commit | Line | Data |
---|---|---|
6c8f5d90 | 1 | import BaseError from '../exception/BaseError'; |
a9ed42b2 | 2 | import type OCPPError from '../exception/OCPPError'; |
d3195f0a | 3 | import { StandardParametersKey } from '../types/ocpp/Configuration'; |
10db00b2 JB |
4 | import { |
5 | HeartbeatRequest, | |
d3195f0a | 6 | MeterValuesRequest, |
10db00b2 JB |
7 | RequestCommand, |
8 | type StatusNotificationRequest, | |
9 | } from '../types/ocpp/Requests'; | |
d3195f0a JB |
10 | import type { |
11 | HeartbeatResponse, | |
12 | MeterValuesResponse, | |
13 | StatusNotificationResponse, | |
14 | } from '../types/ocpp/Responses'; | |
89b7a234 | 15 | import { |
6c8f5d90 | 16 | AuthorizationStatus, |
1984f194 JB |
17 | AuthorizeRequest, |
18 | AuthorizeResponse, | |
89b7a234 JB |
19 | StartTransactionRequest, |
20 | StartTransactionResponse, | |
89b7a234 JB |
21 | StopTransactionRequest, |
22 | StopTransactionResponse, | |
23 | } from '../types/ocpp/Transaction'; | |
24 | import { | |
25 | BroadcastChannelProcedureName, | |
26 | BroadcastChannelRequest, | |
6c8f5d90 JB |
27 | BroadcastChannelRequestPayload, |
28 | BroadcastChannelResponsePayload, | |
29 | MessageEvent, | |
89b7a234 | 30 | } from '../types/WorkerBroadcastChannel'; |
f27eb751 | 31 | import { ResponseStatus } from '../ui/web/src/types/UIProtocol'; |
d3195f0a | 32 | import Constants from '../utils/Constants'; |
6c8f5d90 | 33 | import logger from '../utils/Logger'; |
a9ed42b2 | 34 | import Utils from '../utils/Utils'; |
db2336d9 | 35 | import type ChargingStation from './ChargingStation'; |
d3195f0a JB |
36 | import { ChargingStationConfigurationUtils } from './ChargingStationConfigurationUtils'; |
37 | import { OCPP16ServiceUtils } from './ocpp/1.6/OCPP16ServiceUtils'; | |
1598b27c | 38 | import WorkerBroadcastChannel from './WorkerBroadcastChannel'; |
89b7a234 | 39 | |
4e3ff94d JB |
40 | const moduleName = 'ChargingStationWorkerBroadcastChannel'; |
41 | ||
a9ed42b2 JB |
42 | type CommandResponse = |
43 | | StartTransactionResponse | |
44 | | StopTransactionResponse | |
1984f194 | 45 | | AuthorizeResponse |
10db00b2 | 46 | | StatusNotificationResponse |
d3195f0a JB |
47 | | HeartbeatResponse |
48 | | MeterValuesResponse; | |
89b7a234 | 49 | |
d273692c JB |
50 | type CommandHandler = ( |
51 | requestPayload?: BroadcastChannelRequestPayload | |
52 | ) => Promise<CommandResponse | void> | void; | |
53 | ||
1598b27c | 54 | export default class ChargingStationWorkerBroadcastChannel extends WorkerBroadcastChannel { |
d273692c | 55 | private readonly commandHandlers: Map<BroadcastChannelProcedureName, CommandHandler>; |
9d73266c | 56 | |
89b7a234 JB |
57 | private readonly chargingStation: ChargingStation; |
58 | ||
59 | constructor(chargingStation: ChargingStation) { | |
1598b27c | 60 | super(); |
d273692c | 61 | this.commandHandlers = new Map<BroadcastChannelProcedureName, CommandHandler>([ |
9d73266c JB |
62 | [BroadcastChannelProcedureName.START_CHARGING_STATION, () => this.chargingStation.start()], |
63 | [ | |
64 | BroadcastChannelProcedureName.STOP_CHARGING_STATION, | |
d273692c | 65 | async () => this.chargingStation.stop(), |
9d73266c JB |
66 | ], |
67 | [ | |
68 | BroadcastChannelProcedureName.OPEN_CONNECTION, | |
69 | () => this.chargingStation.openWSConnection(), | |
70 | ], | |
71 | [ | |
72 | BroadcastChannelProcedureName.CLOSE_CONNECTION, | |
73 | () => this.chargingStation.closeWSConnection(), | |
74 | ], | |
623b39b5 JB |
75 | [ |
76 | BroadcastChannelProcedureName.START_AUTOMATIC_TRANSACTION_GENERATOR, | |
77 | (requestPayload?: BroadcastChannelRequestPayload) => | |
78 | this.chargingStation.startAutomaticTransactionGenerator(requestPayload.connectorIds), | |
79 | ], | |
80 | [ | |
81 | BroadcastChannelProcedureName.STOP_AUTOMATIC_TRANSACTION_GENERATOR, | |
82 | (requestPayload?: BroadcastChannelRequestPayload) => | |
83 | this.chargingStation.stopAutomaticTransactionGenerator(requestPayload.connectorIds), | |
84 | ], | |
9d73266c JB |
85 | [ |
86 | BroadcastChannelProcedureName.START_TRANSACTION, | |
87 | async (requestPayload?: BroadcastChannelRequestPayload) => | |
88 | this.chargingStation.ocppRequestService.requestHandler< | |
89 | StartTransactionRequest, | |
90 | StartTransactionResponse | |
1984f194 | 91 | >(this.chargingStation, RequestCommand.START_TRANSACTION, requestPayload), |
9d73266c JB |
92 | ], |
93 | [ | |
94 | BroadcastChannelProcedureName.STOP_TRANSACTION, | |
95 | async (requestPayload?: BroadcastChannelRequestPayload) => | |
96 | this.chargingStation.ocppRequestService.requestHandler< | |
97 | StopTransactionRequest, | |
98 | StartTransactionResponse | |
99 | >(this.chargingStation, RequestCommand.STOP_TRANSACTION, { | |
1984f194 | 100 | ...requestPayload, |
d3195f0a JB |
101 | meterStop: |
102 | requestPayload.meterStop ?? | |
103 | this.chargingStation.getEnergyActiveImportRegisterByTransactionId( | |
104 | requestPayload.transactionId, | |
105 | true | |
106 | ), | |
9d73266c JB |
107 | }), |
108 | ], | |
1984f194 JB |
109 | [ |
110 | BroadcastChannelProcedureName.AUTHORIZE, | |
111 | async (requestPayload?: BroadcastChannelRequestPayload) => | |
112 | this.chargingStation.ocppRequestService.requestHandler< | |
113 | AuthorizeRequest, | |
114 | AuthorizeResponse | |
115 | >(this.chargingStation, RequestCommand.AUTHORIZE, requestPayload), | |
116 | ], | |
9d73266c JB |
117 | [ |
118 | BroadcastChannelProcedureName.STATUS_NOTIFICATION, | |
119 | async (requestPayload?: BroadcastChannelRequestPayload) => | |
120 | this.chargingStation.ocppRequestService.requestHandler< | |
121 | StatusNotificationRequest, | |
122 | StatusNotificationResponse | |
1984f194 | 123 | >(this.chargingStation, RequestCommand.STATUS_NOTIFICATION, requestPayload), |
9d73266c JB |
124 | ], |
125 | [ | |
126 | BroadcastChannelProcedureName.HEARTBEAT, | |
1984f194 JB |
127 | async (requestPayload?: BroadcastChannelRequestPayload) => |
128 | this.chargingStation.ocppRequestService.requestHandler< | |
9d73266c JB |
129 | HeartbeatRequest, |
130 | HeartbeatResponse | |
1984f194 | 131 | >(this.chargingStation, RequestCommand.HEARTBEAT, requestPayload), |
9d73266c | 132 | ], |
d3195f0a JB |
133 | [ |
134 | BroadcastChannelProcedureName.METER_VALUES, | |
135 | async (requestPayload?: BroadcastChannelRequestPayload) => { | |
136 | const configuredMeterValueSampleInterval = | |
137 | ChargingStationConfigurationUtils.getConfigurationKey( | |
138 | chargingStation, | |
139 | StandardParametersKey.MeterValueSampleInterval | |
140 | ); | |
141 | return this.chargingStation.ocppRequestService.requestHandler< | |
142 | MeterValuesRequest, | |
143 | MeterValuesResponse | |
144 | >(this.chargingStation, RequestCommand.METER_VALUES, { | |
145 | ...requestPayload, | |
146 | meterValue: requestPayload.meterValue ?? [ | |
147 | OCPP16ServiceUtils.buildMeterValue( | |
148 | this.chargingStation, | |
149 | requestPayload.connectorId, | |
150 | this.chargingStation.getConnectorStatus(requestPayload.connectorId)?.transactionId, | |
151 | configuredMeterValueSampleInterval | |
152 | ? Utils.convertToInt(configuredMeterValueSampleInterval.value) * 1000 | |
153 | : Constants.DEFAULT_METER_VALUES_INTERVAL | |
154 | ), | |
155 | ], | |
156 | }); | |
157 | }, | |
158 | ], | |
9d73266c | 159 | ]); |
89b7a234 | 160 | this.chargingStation = chargingStation; |
02a6943a | 161 | this.onmessage = this.requestHandler.bind(this) as (message: MessageEvent) => void; |
6c8f5d90 | 162 | this.onmessageerror = this.messageErrorHandler.bind(this) as (message: MessageEvent) => void; |
89b7a234 JB |
163 | } |
164 | ||
02a6943a | 165 | private async requestHandler(messageEvent: MessageEvent): Promise<void> { |
5dea4c94 JB |
166 | const validatedMessageEvent = this.validateMessageEvent(messageEvent); |
167 | if (validatedMessageEvent === false) { | |
6c8f5d90 JB |
168 | return; |
169 | } | |
5dea4c94 JB |
170 | if (this.isResponse(validatedMessageEvent.data) === true) { |
171 | return; | |
172 | } | |
173 | const [uuid, command, requestPayload] = validatedMessageEvent.data as BroadcastChannelRequest; | |
6c8f5d90 | 174 | |
2afb4d15 JB |
175 | if ( |
176 | requestPayload?.hashIds !== undefined && | |
177 | requestPayload?.hashIds?.includes(this.chargingStation.stationInfo.hashId) === false | |
178 | ) { | |
179 | return; | |
180 | } | |
181 | if (requestPayload?.hashId !== undefined) { | |
182 | logger.error( | |
183 | `${this.chargingStation.logPrefix()} ${moduleName}.requestHandler: 'hashId' field usage in PDU is deprecated, use 'hashIds' instead` | |
184 | ); | |
185 | return; | |
4eca248c | 186 | } |
6c8f5d90 | 187 | let responsePayload: BroadcastChannelResponsePayload; |
9d73266c | 188 | let commandResponse: CommandResponse | void; |
6c8f5d90 JB |
189 | try { |
190 | commandResponse = await this.commandHandler(command, requestPayload); | |
9d73266c | 191 | if (commandResponse === undefined || commandResponse === null) { |
10d244c0 | 192 | responsePayload = { |
51c83d6f | 193 | hashId: this.chargingStation.stationInfo.hashId, |
10d244c0 JB |
194 | status: ResponseStatus.SUCCESS, |
195 | }; | |
6c8f5d90 | 196 | } else { |
d3195f0a | 197 | responsePayload = this.commandResponseToResponsePayload( |
1984f194 | 198 | command, |
d3195f0a | 199 | requestPayload, |
1984f194 JB |
200 | commandResponse as CommandResponse |
201 | ); | |
6c8f5d90 JB |
202 | } |
203 | } catch (error) { | |
204 | logger.error( | |
205 | `${this.chargingStation.logPrefix()} ${moduleName}.requestHandler: Handle request error:`, | |
206 | error | |
207 | ); | |
208 | responsePayload = { | |
51c83d6f | 209 | hashId: this.chargingStation.stationInfo.hashId, |
6c8f5d90 JB |
210 | status: ResponseStatus.FAILURE, |
211 | command, | |
212 | requestPayload, | |
9d73266c | 213 | commandResponse: commandResponse as CommandResponse, |
6c8f5d90 JB |
214 | errorMessage: (error as Error).message, |
215 | errorStack: (error as Error).stack, | |
a9ed42b2 | 216 | errorDetails: (error as OCPPError).details, |
6c8f5d90 | 217 | }; |
623b39b5 JB |
218 | } finally { |
219 | this.sendResponse([uuid, responsePayload]); | |
6c8f5d90 | 220 | } |
6c8f5d90 JB |
221 | } |
222 | ||
223 | private messageErrorHandler(messageEvent: MessageEvent): void { | |
224 | logger.error( | |
225 | `${this.chargingStation.logPrefix()} ${moduleName}.messageErrorHandler: Error at handling message:`, | |
5e3cb728 | 226 | { messageEvent } |
6c8f5d90 JB |
227 | ); |
228 | } | |
229 | ||
230 | private async commandHandler( | |
231 | command: BroadcastChannelProcedureName, | |
232 | requestPayload: BroadcastChannelRequestPayload | |
9d73266c JB |
233 | ): Promise<CommandResponse | void> { |
234 | if (this.commandHandlers.has(command) === true) { | |
1984f194 | 235 | this.cleanRequestPayload(command, requestPayload); |
9d73266c | 236 | return this.commandHandlers.get(command)(requestPayload); |
6c8f5d90 | 237 | } |
9d73266c | 238 | throw new BaseError(`Unknown worker broadcast channel command: ${command}`); |
6c8f5d90 JB |
239 | } |
240 | ||
1984f194 JB |
241 | private cleanRequestPayload( |
242 | command: BroadcastChannelProcedureName, | |
243 | requestPayload: BroadcastChannelRequestPayload | |
244 | ): void { | |
245 | delete requestPayload.hashId; | |
246 | delete requestPayload.hashIds; | |
247 | [ | |
248 | BroadcastChannelProcedureName.START_AUTOMATIC_TRANSACTION_GENERATOR, | |
249 | BroadcastChannelProcedureName.STOP_AUTOMATIC_TRANSACTION_GENERATOR, | |
250 | ].includes(command) === false && delete requestPayload.connectorIds; | |
251 | } | |
252 | ||
d3195f0a JB |
253 | private commandResponseToResponsePayload( |
254 | command: BroadcastChannelProcedureName, | |
255 | requestPayload: BroadcastChannelRequestPayload, | |
256 | commandResponse: CommandResponse | |
257 | ): BroadcastChannelResponsePayload { | |
258 | const commandResponseStatus = this.commandResponseStatusToResponseStatus( | |
259 | command, | |
260 | commandResponse | |
261 | ); | |
262 | if (commandResponseStatus === ResponseStatus.SUCCESS) { | |
263 | return { | |
264 | hashId: this.chargingStation.stationInfo.hashId, | |
265 | status: commandResponseStatus, | |
266 | }; | |
267 | } | |
268 | return { | |
269 | hashId: this.chargingStation.stationInfo.hashId, | |
270 | status: commandResponseStatus, | |
271 | command, | |
272 | requestPayload, | |
273 | commandResponse, | |
274 | }; | |
275 | } | |
276 | ||
277 | private commandResponseStatusToResponseStatus( | |
10db00b2 JB |
278 | command: BroadcastChannelProcedureName, |
279 | commandResponse: CommandResponse | |
280 | ): ResponseStatus { | |
281 | switch (command) { | |
282 | case BroadcastChannelProcedureName.START_TRANSACTION: | |
283 | case BroadcastChannelProcedureName.STOP_TRANSACTION: | |
1984f194 | 284 | case BroadcastChannelProcedureName.AUTHORIZE: |
10db00b2 | 285 | if ( |
1984f194 JB |
286 | ( |
287 | commandResponse as | |
288 | | StartTransactionResponse | |
289 | | StopTransactionResponse | |
290 | | AuthorizeResponse | |
291 | )?.idTagInfo?.status === AuthorizationStatus.ACCEPTED | |
10db00b2 JB |
292 | ) { |
293 | return ResponseStatus.SUCCESS; | |
294 | } | |
295 | return ResponseStatus.FAILURE; | |
296 | case BroadcastChannelProcedureName.STATUS_NOTIFICATION: | |
d3195f0a | 297 | case BroadcastChannelProcedureName.METER_VALUES: |
10db00b2 JB |
298 | if (Utils.isEmptyObject(commandResponse) === true) { |
299 | return ResponseStatus.SUCCESS; | |
300 | } | |
301 | return ResponseStatus.FAILURE; | |
302 | case BroadcastChannelProcedureName.HEARTBEAT: | |
303 | if ('currentTime' in commandResponse) { | |
304 | return ResponseStatus.SUCCESS; | |
305 | } | |
306 | return ResponseStatus.FAILURE; | |
307 | default: | |
308 | return ResponseStatus.FAILURE; | |
89b7a234 JB |
309 | } |
310 | } | |
311 | } |