refactor: more coding style fixes
[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 isEmptyObject,
44 isNullOrUndefined,
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> | 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 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
139 requestPayload!.transactionId!,
140 true
141 ),
142 ...requestPayload
143 },
144 requestParams
145 )
146 ],
147 [
148 BroadcastChannelProcedureName.AUTHORIZE,
149 async (requestPayload?: BroadcastChannelRequestPayload) =>
150 await this.chargingStation.ocppRequestService.requestHandler<
151 AuthorizeRequest,
152 AuthorizeResponse
153 >(this.chargingStation, RequestCommand.AUTHORIZE, requestPayload, requestParams)
154 ],
155 [
156 BroadcastChannelProcedureName.BOOT_NOTIFICATION,
157 async (requestPayload?: BroadcastChannelRequestPayload) => {
158 this.chargingStation.bootNotificationResponse =
159 await this.chargingStation.ocppRequestService.requestHandler<
160 BootNotificationRequest,
161 BootNotificationResponse
162 >(
163 this.chargingStation,
164 RequestCommand.BOOT_NOTIFICATION,
165 {
166 ...this.chargingStation.bootNotificationRequest,
167 ...requestPayload
168 },
169 {
170 skipBufferingOnError: true,
171 throwError: true
172 }
173 )
174 return this.chargingStation.bootNotificationResponse
175 }
176 ],
177 [
178 BroadcastChannelProcedureName.STATUS_NOTIFICATION,
179 async (requestPayload?: BroadcastChannelRequestPayload) =>
180 await this.chargingStation.ocppRequestService.requestHandler<
181 StatusNotificationRequest,
182 StatusNotificationResponse
183 >(
184 this.chargingStation,
185 RequestCommand.STATUS_NOTIFICATION,
186 requestPayload,
187 requestParams
188 )
189 ],
190 [
191 BroadcastChannelProcedureName.HEARTBEAT,
192 async (requestPayload?: BroadcastChannelRequestPayload) =>
193 await this.chargingStation.ocppRequestService.requestHandler<
194 HeartbeatRequest,
195 HeartbeatResponse
196 >(this.chargingStation, RequestCommand.HEARTBEAT, requestPayload, requestParams)
197 ],
198 [
199 BroadcastChannelProcedureName.METER_VALUES,
200 async (requestPayload?: BroadcastChannelRequestPayload) => {
201 const configuredMeterValueSampleInterval = getConfigurationKey(
202 chargingStation,
203 StandardParametersKey.MeterValueSampleInterval
204 )
205 return await this.chargingStation.ocppRequestService.requestHandler<
206 MeterValuesRequest,
207 MeterValuesResponse
208 >(
209 this.chargingStation,
210 RequestCommand.METER_VALUES,
211 {
212 meterValue: [
213 buildMeterValue(
214 this.chargingStation,
215 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
216 requestPayload!.connectorId!,
217 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
218 this.chargingStation.getConnectorStatus(requestPayload!.connectorId!)!
219 .transactionId!,
220 configuredMeterValueSampleInterval != null
221 ? secondsToMilliseconds(convertToInt(configuredMeterValueSampleInterval.value))
222 : Constants.DEFAULT_METER_VALUES_INTERVAL
223 )
224 ],
225 ...requestPayload
226 },
227 requestParams
228 )
229 }
230 ],
231 [
232 BroadcastChannelProcedureName.DATA_TRANSFER,
233 async (requestPayload?: BroadcastChannelRequestPayload) =>
234 await this.chargingStation.ocppRequestService.requestHandler<
235 DataTransferRequest,
236 DataTransferResponse
237 >(this.chargingStation, RequestCommand.DATA_TRANSFER, requestPayload, requestParams)
238 ],
239 [
240 BroadcastChannelProcedureName.DIAGNOSTICS_STATUS_NOTIFICATION,
241 async (requestPayload?: BroadcastChannelRequestPayload) =>
242 await this.chargingStation.ocppRequestService.requestHandler<
243 DiagnosticsStatusNotificationRequest,
244 DiagnosticsStatusNotificationResponse
245 >(
246 this.chargingStation,
247 RequestCommand.DIAGNOSTICS_STATUS_NOTIFICATION,
248 requestPayload,
249 requestParams
250 )
251 ],
252 [
253 BroadcastChannelProcedureName.FIRMWARE_STATUS_NOTIFICATION,
254 async (requestPayload?: BroadcastChannelRequestPayload) =>
255 await this.chargingStation.ocppRequestService.requestHandler<
256 FirmwareStatusNotificationRequest,
257 FirmwareStatusNotificationResponse
258 >(
259 this.chargingStation,
260 RequestCommand.FIRMWARE_STATUS_NOTIFICATION,
261 requestPayload,
262 requestParams
263 )
264 ]
265 ])
266 this.chargingStation = chargingStation
267 this.onmessage = this.requestHandler.bind(this) as (message: unknown) => void
268 this.onmessageerror = this.messageErrorHandler.bind(this) as (message: unknown) => void
269 }
270
271 private async requestHandler (messageEvent: MessageEvent): Promise<void> {
272 const validatedMessageEvent = this.validateMessageEvent(messageEvent)
273 if (validatedMessageEvent === false) {
274 return
275 }
276 if (this.isResponse(validatedMessageEvent.data)) {
277 return
278 }
279 const [uuid, command, requestPayload] = validatedMessageEvent.data as BroadcastChannelRequest
280 if (
281 !isNullOrUndefined(requestPayload.hashIds) &&
282 requestPayload.hashIds?.includes(this.chargingStation.stationInfo.hashId) === false
283 ) {
284 return
285 }
286 if (!isNullOrUndefined(requestPayload.hashId)) {
287 logger.error(
288 `${this.chargingStation.logPrefix()} ${moduleName}.requestHandler: 'hashId' field usage in PDU is deprecated, use 'hashIds' array instead`
289 )
290 return
291 }
292 let responsePayload: BroadcastChannelResponsePayload | undefined
293 // eslint-disable-next-line @typescript-eslint/no-invalid-void-type
294 let commandResponse: CommandResponse | void | undefined
295 try {
296 commandResponse = await this.commandHandler(command, requestPayload)
297 if (isNullOrUndefined(commandResponse) || isEmptyObject(commandResponse as CommandResponse)) {
298 responsePayload = {
299 hashId: this.chargingStation.stationInfo.hashId,
300 status: ResponseStatus.SUCCESS
301 }
302 } else {
303 responsePayload = this.commandResponseToResponsePayload(
304 command,
305 requestPayload,
306 commandResponse as CommandResponse
307 )
308 }
309 } catch (error) {
310 logger.error(
311 `${this.chargingStation.logPrefix()} ${moduleName}.requestHandler: Handle request error:`,
312 error
313 )
314 responsePayload = {
315 hashId: this.chargingStation.stationInfo.hashId,
316 status: ResponseStatus.FAILURE,
317 command,
318 requestPayload,
319 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
320 commandResponse: commandResponse!,
321 errorMessage: (error as OCPPError).message,
322 errorStack: (error as OCPPError).stack,
323 errorDetails: (error as OCPPError).details
324 }
325 } finally {
326 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
327 this.sendResponse([uuid, responsePayload!])
328 }
329 }
330
331 private messageErrorHandler (messageEvent: MessageEvent): void {
332 logger.error(
333 `${this.chargingStation.logPrefix()} ${moduleName}.messageErrorHandler: Error at handling message:`,
334 messageEvent
335 )
336 }
337
338 private async commandHandler (
339 command: BroadcastChannelProcedureName,
340 requestPayload: BroadcastChannelRequestPayload
341 // eslint-disable-next-line @typescript-eslint/no-invalid-void-type
342 ): Promise<void | CommandResponse> {
343 if (this.commandHandlers.has(command)) {
344 this.cleanRequestPayload(command, requestPayload)
345 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
346 return await this.commandHandlers.get(command)!(requestPayload)
347 }
348 throw new BaseError(`Unknown worker broadcast channel command: '${command}'`)
349 }
350
351 private cleanRequestPayload (
352 command: BroadcastChannelProcedureName,
353 requestPayload: BroadcastChannelRequestPayload
354 ): void {
355 delete requestPayload.hashId
356 delete requestPayload.hashIds
357 ![
358 BroadcastChannelProcedureName.START_AUTOMATIC_TRANSACTION_GENERATOR,
359 BroadcastChannelProcedureName.STOP_AUTOMATIC_TRANSACTION_GENERATOR
360 ].includes(command) && delete requestPayload.connectorIds
361 }
362
363 private commandResponseToResponsePayload (
364 command: BroadcastChannelProcedureName,
365 requestPayload: BroadcastChannelRequestPayload,
366 commandResponse: CommandResponse
367 ): BroadcastChannelResponsePayload {
368 const responseStatus = this.commandResponseToResponseStatus(command, commandResponse)
369 if (responseStatus === ResponseStatus.SUCCESS) {
370 return {
371 hashId: this.chargingStation.stationInfo.hashId,
372 status: responseStatus
373 }
374 }
375 return {
376 hashId: this.chargingStation.stationInfo.hashId,
377 status: responseStatus,
378 command,
379 requestPayload,
380 commandResponse
381 }
382 }
383
384 private commandResponseToResponseStatus (
385 command: BroadcastChannelProcedureName,
386 commandResponse: CommandResponse
387 ): ResponseStatus {
388 switch (command) {
389 case BroadcastChannelProcedureName.START_TRANSACTION:
390 case BroadcastChannelProcedureName.STOP_TRANSACTION:
391 case BroadcastChannelProcedureName.AUTHORIZE:
392 if (
393 (
394 commandResponse as
395 | StartTransactionResponse
396 | StopTransactionResponse
397 | AuthorizeResponse
398 )?.idTagInfo?.status === AuthorizationStatus.ACCEPTED
399 ) {
400 return ResponseStatus.SUCCESS
401 }
402 return ResponseStatus.FAILURE
403 case BroadcastChannelProcedureName.BOOT_NOTIFICATION:
404 if (commandResponse?.status === RegistrationStatusEnumType.ACCEPTED) {
405 return ResponseStatus.SUCCESS
406 }
407 return ResponseStatus.FAILURE
408 case BroadcastChannelProcedureName.DATA_TRANSFER:
409 if (commandResponse?.status === DataTransferStatus.ACCEPTED) {
410 return ResponseStatus.SUCCESS
411 }
412 return ResponseStatus.FAILURE
413 case BroadcastChannelProcedureName.STATUS_NOTIFICATION:
414 case BroadcastChannelProcedureName.METER_VALUES:
415 if (isEmptyObject(commandResponse)) {
416 return ResponseStatus.SUCCESS
417 }
418 return ResponseStatus.FAILURE
419 case BroadcastChannelProcedureName.HEARTBEAT:
420 if ('currentTime' in commandResponse) {
421 return ResponseStatus.SUCCESS
422 }
423 return ResponseStatus.FAILURE
424 default:
425 return ResponseStatus.FAILURE
426 }
427 }
428 }