refactor: cleanup isNullOrdefined usage
[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 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
133 requestPayload!.transactionId!,
134 true
135 ),
136 ...requestPayload
137 },
138 requestParams
139 )
140 ],
141 [
142 BroadcastChannelProcedureName.AUTHORIZE,
143 async (requestPayload?: BroadcastChannelRequestPayload) =>
144 await this.chargingStation.ocppRequestService.requestHandler<
145 AuthorizeRequest,
146 AuthorizeResponse
147 >(this.chargingStation, RequestCommand.AUTHORIZE, requestPayload, requestParams)
148 ],
149 [
150 BroadcastChannelProcedureName.BOOT_NOTIFICATION,
151 async (requestPayload?: BroadcastChannelRequestPayload) => {
152 this.chargingStation.bootNotificationResponse =
153 await this.chargingStation.ocppRequestService.requestHandler<
154 BootNotificationRequest,
155 BootNotificationResponse
156 >(
157 this.chargingStation,
158 RequestCommand.BOOT_NOTIFICATION,
159 {
160 ...this.chargingStation.bootNotificationRequest,
161 ...requestPayload
162 },
163 {
164 skipBufferingOnError: true,
165 throwError: true
166 }
167 )
168 return this.chargingStation.bootNotificationResponse
169 }
170 ],
171 [
172 BroadcastChannelProcedureName.STATUS_NOTIFICATION,
173 async (requestPayload?: BroadcastChannelRequestPayload) =>
174 await this.chargingStation.ocppRequestService.requestHandler<
175 StatusNotificationRequest,
176 StatusNotificationResponse
177 >(
178 this.chargingStation,
179 RequestCommand.STATUS_NOTIFICATION,
180 requestPayload,
181 requestParams
182 )
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 async requestHandler (messageEvent: MessageEvent): Promise<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 !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 | undefined
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<void | CommandResponse> {
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 }