refactor: switch eslint configuration to strict type checking
[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 // 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 // eslint-disable-next-line @typescript-eslint/no-invalid-void-type
289 let commandResponse: CommandResponse | void
290 try {
291 commandResponse = await this.commandHandler(command, requestPayload)
292 if (commandResponse == null || isEmptyObject(commandResponse)) {
293 responsePayload = {
294 hashId: this.chargingStation.stationInfo?.hashId,
295 status: ResponseStatus.SUCCESS
296 }
297 } else {
298 responsePayload = this.commandResponseToResponsePayload(
299 command,
300 requestPayload,
301 commandResponse
302 )
303 }
304 } catch (error) {
305 logger.error(
306 `${this.chargingStation.logPrefix()} ${moduleName}.requestHandler: Handle request error:`,
307 error
308 )
309 responsePayload = {
310 hashId: this.chargingStation.stationInfo?.hashId,
311 status: ResponseStatus.FAILURE,
312 command,
313 requestPayload,
314 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
315 commandResponse: commandResponse!,
316 errorMessage: (error as OCPPError).message,
317 errorStack: (error as OCPPError).stack,
318 errorDetails: (error as OCPPError).details
319 }
320 } finally {
321 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
322 this.sendResponse([uuid, responsePayload!])
323 }
324 }
325
326 private messageErrorHandler (messageEvent: MessageEvent): void {
327 logger.error(
328 `${this.chargingStation.logPrefix()} ${moduleName}.messageErrorHandler: Error at handling message:`,
329 messageEvent
330 )
331 }
332
333 private async commandHandler (
334 command: BroadcastChannelProcedureName,
335 requestPayload: BroadcastChannelRequestPayload
336 // eslint-disable-next-line @typescript-eslint/no-invalid-void-type
337 ): Promise<CommandResponse | void> {
338 if (this.commandHandlers.has(command)) {
339 this.cleanRequestPayload(command, requestPayload)
340 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
341 return await this.commandHandlers.get(command)!(requestPayload)
342 }
343 throw new BaseError(`Unknown worker broadcast channel command: '${command}'`)
344 }
345
346 private cleanRequestPayload (
347 command: BroadcastChannelProcedureName,
348 requestPayload: BroadcastChannelRequestPayload
349 ): void {
350 delete requestPayload.hashId
351 delete requestPayload.hashIds
352 ![
353 BroadcastChannelProcedureName.START_AUTOMATIC_TRANSACTION_GENERATOR,
354 BroadcastChannelProcedureName.STOP_AUTOMATIC_TRANSACTION_GENERATOR
355 ].includes(command) && delete requestPayload.connectorIds
356 }
357
358 private commandResponseToResponsePayload (
359 command: BroadcastChannelProcedureName,
360 requestPayload: BroadcastChannelRequestPayload,
361 commandResponse: CommandResponse
362 ): BroadcastChannelResponsePayload {
363 const responseStatus = this.commandResponseToResponseStatus(command, commandResponse)
364 if (responseStatus === ResponseStatus.SUCCESS) {
365 return {
366 hashId: this.chargingStation.stationInfo?.hashId,
367 status: responseStatus
368 }
369 }
370 return {
371 hashId: this.chargingStation.stationInfo?.hashId,
372 status: responseStatus,
373 command,
374 requestPayload,
375 commandResponse
376 }
377 }
378
379 private commandResponseToResponseStatus (
380 command: BroadcastChannelProcedureName,
381 commandResponse: CommandResponse
382 ): ResponseStatus {
383 switch (command) {
384 case BroadcastChannelProcedureName.START_TRANSACTION:
385 case BroadcastChannelProcedureName.STOP_TRANSACTION:
386 case BroadcastChannelProcedureName.AUTHORIZE:
387 if (
388 (
389 commandResponse as
390 | StartTransactionResponse
391 | StopTransactionResponse
392 | AuthorizeResponse
393 ).idTagInfo?.status === AuthorizationStatus.ACCEPTED
394 ) {
395 return ResponseStatus.SUCCESS
396 }
397 return ResponseStatus.FAILURE
398 case BroadcastChannelProcedureName.BOOT_NOTIFICATION:
399 if (commandResponse.status === RegistrationStatusEnumType.ACCEPTED) {
400 return ResponseStatus.SUCCESS
401 }
402 return ResponseStatus.FAILURE
403 case BroadcastChannelProcedureName.DATA_TRANSFER:
404 if (commandResponse.status === DataTransferStatus.ACCEPTED) {
405 return ResponseStatus.SUCCESS
406 }
407 return ResponseStatus.FAILURE
408 case BroadcastChannelProcedureName.STATUS_NOTIFICATION:
409 case BroadcastChannelProcedureName.METER_VALUES:
410 if (isEmptyObject(commandResponse)) {
411 return ResponseStatus.SUCCESS
412 }
413 return ResponseStatus.FAILURE
414 case BroadcastChannelProcedureName.HEARTBEAT:
415 if ('currentTime' in commandResponse) {
416 return ResponseStatus.SUCCESS
417 }
418 return ResponseStatus.FAILURE
419 default:
420 return ResponseStatus.FAILURE
421 }
422 }
423 }