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