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