build(deps-dev): apply updates
[e-mobility-charging-stations-simulator.git] / src / charging-station / broadcast-channel / ChargingStationWorkerBroadcastChannel.ts
1 import { secondsToMilliseconds } from 'date-fns'
2 import { isEmpty } from 'rambda'
3
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, isAsyncFunction, 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 import { WorkerBroadcastChannel } from './WorkerBroadcastChannel.js'
45
46 const moduleName = 'ChargingStationWorkerBroadcastChannel'
47
48 type CommandResponse =
49 | EmptyObject
50 | StartTransactionResponse
51 | StopTransactionResponse
52 | AuthorizeResponse
53 | BootNotificationResponse
54 | HeartbeatResponse
55 | DataTransferResponse
56
57 type CommandHandler = (
58 requestPayload?: BroadcastChannelRequestPayload
59 // eslint-disable-next-line @typescript-eslint/no-invalid-void-type
60 ) => Promise<CommandResponse | void> | CommandResponse | void
61
62 export class ChargingStationWorkerBroadcastChannel extends WorkerBroadcastChannel {
63 private readonly commandHandlers: Map<BroadcastChannelProcedureName, CommandHandler>
64 private readonly chargingStation: ChargingStation
65
66 constructor (chargingStation: ChargingStation) {
67 super()
68 const requestParams: RequestParams = {
69 throwError: true
70 }
71 this.commandHandlers = new Map<BroadcastChannelProcedureName, CommandHandler>([
72 [
73 BroadcastChannelProcedureName.START_CHARGING_STATION,
74 () => {
75 this.chargingStation.start()
76 }
77 ],
78 [
79 BroadcastChannelProcedureName.STOP_CHARGING_STATION,
80 async () => {
81 await this.chargingStation.stop()
82 }
83 ],
84 [
85 BroadcastChannelProcedureName.DELETE_CHARGING_STATIONS,
86 async (requestPayload?: BroadcastChannelRequestPayload) => {
87 await this.chargingStation.delete(requestPayload?.deleteConfiguration as boolean)
88 }
89 ],
90 [
91 BroadcastChannelProcedureName.OPEN_CONNECTION,
92 () => {
93 this.chargingStation.openWSConnection()
94 }
95 ],
96 [
97 BroadcastChannelProcedureName.CLOSE_CONNECTION,
98 () => {
99 this.chargingStation.closeWSConnection()
100 }
101 ],
102 [
103 BroadcastChannelProcedureName.START_AUTOMATIC_TRANSACTION_GENERATOR,
104 (requestPayload?: BroadcastChannelRequestPayload) => {
105 this.chargingStation.startAutomaticTransactionGenerator(requestPayload?.connectorIds)
106 }
107 ],
108 [
109 BroadcastChannelProcedureName.STOP_AUTOMATIC_TRANSACTION_GENERATOR,
110 (requestPayload?: BroadcastChannelRequestPayload) => {
111 this.chargingStation.stopAutomaticTransactionGenerator(requestPayload?.connectorIds)
112 }
113 ],
114 [
115 BroadcastChannelProcedureName.SET_SUPERVISION_URL,
116 (requestPayload?: BroadcastChannelRequestPayload) => {
117 this.chargingStation.setSupervisionUrl(requestPayload?.url as string)
118 }
119 ],
120 [
121 BroadcastChannelProcedureName.START_TRANSACTION,
122 async (requestPayload?: BroadcastChannelRequestPayload) =>
123 await this.chargingStation.ocppRequestService.requestHandler<
124 StartTransactionRequest,
125 StartTransactionResponse
126 >(this.chargingStation, RequestCommand.START_TRANSACTION, requestPayload, requestParams)
127 ],
128 [
129 BroadcastChannelProcedureName.STOP_TRANSACTION,
130 async (requestPayload?: BroadcastChannelRequestPayload) =>
131 await this.chargingStation.ocppRequestService.requestHandler<
132 StopTransactionRequest,
133 StartTransactionResponse
134 >(
135 this.chargingStation,
136 RequestCommand.STOP_TRANSACTION,
137 {
138 meterStop: this.chargingStation.getEnergyActiveImportRegisterByTransactionId(
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 return await this.chargingStation.ocppRequestService.requestHandler<
159 BootNotificationRequest,
160 BootNotificationResponse
161 >(
162 this.chargingStation,
163 RequestCommand.BOOT_NOTIFICATION,
164 {
165 ...this.chargingStation.bootNotificationRequest,
166 ...requestPayload
167 },
168 {
169 skipBufferingOnError: true,
170 throwError: true
171 }
172 )
173 }
174 ],
175 [
176 BroadcastChannelProcedureName.STATUS_NOTIFICATION,
177 async (requestPayload?: BroadcastChannelRequestPayload) =>
178 await this.chargingStation.ocppRequestService.requestHandler<
179 StatusNotificationRequest,
180 StatusNotificationResponse
181 >(this.chargingStation, RequestCommand.STATUS_NOTIFICATION, requestPayload, requestParams)
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 requestHandler (messageEvent: MessageEvent): 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 this.commandHandler(command, requestPayload)
288 .then(commandResponse => {
289 if (commandResponse == null || isEmpty(commandResponse)) {
290 responsePayload = {
291 hashId: this.chargingStation.stationInfo?.hashId,
292 status: ResponseStatus.SUCCESS
293 }
294 } else {
295 responsePayload = this.commandResponseToResponsePayload(
296 command,
297 requestPayload,
298 commandResponse
299 )
300 }
301 })
302 .catch((error: unknown) => {
303 logger.error(
304 `${this.chargingStation.logPrefix()} ${moduleName}.requestHandler: Handle request error:`,
305 error
306 )
307 responsePayload = {
308 hashId: this.chargingStation.stationInfo?.hashId,
309 status: ResponseStatus.FAILURE,
310 command,
311 requestPayload,
312 errorMessage: (error as OCPPError).message,
313 errorStack: (error as OCPPError).stack,
314 errorDetails: (error as OCPPError).details
315 } satisfies BroadcastChannelResponsePayload
316 })
317 .finally(() => {
318 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
319 this.sendResponse([uuid, responsePayload!])
320 })
321 }
322
323 private messageErrorHandler (messageEvent: MessageEvent): void {
324 logger.error(
325 `${this.chargingStation.logPrefix()} ${moduleName}.messageErrorHandler: Error at handling message:`,
326 messageEvent
327 )
328 }
329
330 private async commandHandler (
331 command: BroadcastChannelProcedureName,
332 requestPayload: BroadcastChannelRequestPayload
333 // eslint-disable-next-line @typescript-eslint/no-invalid-void-type
334 ): Promise<CommandResponse | void> {
335 if (this.commandHandlers.has(command)) {
336 this.cleanRequestPayload(command, requestPayload)
337 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
338 const commandHandler = this.commandHandlers.get(command)!
339 if (isAsyncFunction(commandHandler)) {
340 return await commandHandler(requestPayload)
341 }
342 return (
343 commandHandler as (
344 requestPayload?: BroadcastChannelRequestPayload
345 // eslint-disable-next-line @typescript-eslint/no-invalid-void-type
346 ) => CommandResponse | void
347 )(requestPayload)
348 }
349 throw new BaseError(`Unknown worker broadcast channel command: '${command}'`)
350 }
351
352 private cleanRequestPayload (
353 command: BroadcastChannelProcedureName,
354 requestPayload: BroadcastChannelRequestPayload
355 ): void {
356 delete requestPayload.hashId
357 delete requestPayload.hashIds
358 ![
359 BroadcastChannelProcedureName.START_AUTOMATIC_TRANSACTION_GENERATOR,
360 BroadcastChannelProcedureName.STOP_AUTOMATIC_TRANSACTION_GENERATOR
361 ].includes(command) && delete requestPayload.connectorIds
362 }
363
364 private commandResponseToResponsePayload (
365 command: BroadcastChannelProcedureName,
366 requestPayload: BroadcastChannelRequestPayload,
367 commandResponse: CommandResponse
368 ): BroadcastChannelResponsePayload {
369 const responseStatus = this.commandResponseToResponseStatus(command, commandResponse)
370 if (responseStatus === ResponseStatus.SUCCESS) {
371 return {
372 hashId: this.chargingStation.stationInfo?.hashId,
373 status: responseStatus
374 }
375 }
376 return {
377 hashId: this.chargingStation.stationInfo?.hashId,
378 status: responseStatus,
379 command,
380 requestPayload,
381 commandResponse
382 }
383 }
384
385 private commandResponseToResponseStatus (
386 command: BroadcastChannelProcedureName,
387 commandResponse: CommandResponse
388 ): ResponseStatus {
389 switch (command) {
390 case BroadcastChannelProcedureName.START_TRANSACTION:
391 case BroadcastChannelProcedureName.STOP_TRANSACTION:
392 case BroadcastChannelProcedureName.AUTHORIZE:
393 if (
394 (
395 commandResponse as
396 | StartTransactionResponse
397 | StopTransactionResponse
398 | AuthorizeResponse
399 ).idTagInfo?.status === AuthorizationStatus.ACCEPTED
400 ) {
401 return ResponseStatus.SUCCESS
402 }
403 return ResponseStatus.FAILURE
404 case BroadcastChannelProcedureName.BOOT_NOTIFICATION:
405 if (commandResponse.status === RegistrationStatusEnumType.ACCEPTED) {
406 return ResponseStatus.SUCCESS
407 }
408 return ResponseStatus.FAILURE
409 case BroadcastChannelProcedureName.DATA_TRANSFER:
410 if (commandResponse.status === DataTransferStatus.ACCEPTED) {
411 return ResponseStatus.SUCCESS
412 }
413 return ResponseStatus.FAILURE
414 case BroadcastChannelProcedureName.STATUS_NOTIFICATION:
415 case BroadcastChannelProcedureName.METER_VALUES:
416 if (isEmpty(commandResponse)) {
417 return ResponseStatus.SUCCESS
418 }
419 return ResponseStatus.FAILURE
420 case BroadcastChannelProcedureName.HEARTBEAT:
421 if ('currentTime' in commandResponse) {
422 return ResponseStatus.SUCCESS
423 }
424 return ResponseStatus.FAILURE
425 default:
426 return ResponseStatus.FAILURE
427 }
428 }
429 }