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