e70e128db294374d50f8dde1cc89372f6e2b3a59
[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 type { ChargingStation } from '../ChargingStation.js'
5
6 import { BaseError, type OCPPError } from '../../exception/index.js'
7 import {
8 AuthorizationStatus,
9 type AuthorizeRequest,
10 type AuthorizeResponse,
11 type BootNotificationRequest,
12 type BootNotificationResponse,
13 BroadcastChannelProcedureName,
14 type BroadcastChannelRequest,
15 type BroadcastChannelRequestPayload,
16 type BroadcastChannelResponsePayload,
17 type DataTransferRequest,
18 type DataTransferResponse,
19 DataTransferStatus,
20 type DiagnosticsStatusNotificationRequest,
21 type DiagnosticsStatusNotificationResponse,
22 type EmptyObject,
23 type FirmwareStatusNotificationRequest,
24 type FirmwareStatusNotificationResponse,
25 type HeartbeatRequest,
26 type HeartbeatResponse,
27 type MessageEvent,
28 type MeterValuesRequest,
29 type MeterValuesResponse,
30 RegistrationStatusEnumType,
31 RequestCommand,
32 type RequestParams,
33 ResponseStatus,
34 StandardParametersKey,
35 type StartTransactionRequest,
36 type StartTransactionResponse,
37 type StatusNotificationRequest,
38 type StatusNotificationResponse,
39 type StopTransactionRequest,
40 type StopTransactionResponse,
41 } from '../../types/index.js'
42 import { Constants, convertToInt, isAsyncFunction, logger } from '../../utils/index.js'
43 import { getConfigurationKey } from '../ConfigurationKeyUtils.js'
44 import { buildMeterValue } from '../ocpp/index.js'
45 import { WorkerBroadcastChannel } from './WorkerBroadcastChannel.js'
46
47 const moduleName = 'ChargingStationWorkerBroadcastChannel'
48
49 type CommandResponse =
50 | AuthorizeResponse
51 | BootNotificationResponse
52 | DataTransferResponse
53 | EmptyObject
54 | HeartbeatResponse
55 | StartTransactionResponse
56 | StopTransactionResponse
57
58 type CommandHandler = (
59 requestPayload?: BroadcastChannelRequestPayload
60 // eslint-disable-next-line @typescript-eslint/no-invalid-void-type
61 ) => CommandResponse | Promise<CommandResponse | void> | void
62
63 export class ChargingStationWorkerBroadcastChannel extends WorkerBroadcastChannel {
64 private readonly chargingStation: ChargingStation
65 private readonly commandHandlers: Map<BroadcastChannelProcedureName, CommandHandler>
66
67 constructor (chargingStation: ChargingStation) {
68 super()
69 const requestParams: RequestParams = {
70 throwError: true,
71 }
72 this.commandHandlers = new Map<BroadcastChannelProcedureName, CommandHandler>([
73 [
74 BroadcastChannelProcedureName.AUTHORIZE,
75 async (requestPayload?: BroadcastChannelRequestPayload) =>
76 await this.chargingStation.ocppRequestService.requestHandler<
77 AuthorizeRequest,
78 AuthorizeResponse
79 >(
80 this.chargingStation,
81 RequestCommand.AUTHORIZE,
82 requestPayload as AuthorizeRequest,
83 requestParams
84 ),
85 ],
86 [
87 BroadcastChannelProcedureName.BOOT_NOTIFICATION,
88 async (requestPayload?: BroadcastChannelRequestPayload) => {
89 return await this.chargingStation.ocppRequestService.requestHandler<
90 BootNotificationRequest,
91 BootNotificationResponse
92 >(
93 this.chargingStation,
94 RequestCommand.BOOT_NOTIFICATION,
95 {
96 ...this.chargingStation.bootNotificationRequest,
97 ...requestPayload,
98 } as BootNotificationRequest,
99 {
100 skipBufferingOnError: true,
101 throwError: true,
102 }
103 )
104 },
105 ],
106 [
107 BroadcastChannelProcedureName.CLOSE_CONNECTION,
108 () => {
109 this.chargingStation.closeWSConnection()
110 },
111 ],
112 [
113 BroadcastChannelProcedureName.DATA_TRANSFER,
114 async (requestPayload?: BroadcastChannelRequestPayload) =>
115 await this.chargingStation.ocppRequestService.requestHandler<
116 DataTransferRequest,
117 DataTransferResponse
118 >(
119 this.chargingStation,
120 RequestCommand.DATA_TRANSFER,
121 requestPayload as DataTransferRequest,
122 requestParams
123 ),
124 ],
125 [
126 BroadcastChannelProcedureName.DELETE_CHARGING_STATIONS,
127 async (requestPayload?: BroadcastChannelRequestPayload) => {
128 await this.chargingStation.delete(requestPayload?.deleteConfiguration as boolean)
129 },
130 ],
131 [
132 BroadcastChannelProcedureName.DIAGNOSTICS_STATUS_NOTIFICATION,
133 async (requestPayload?: BroadcastChannelRequestPayload) =>
134 await this.chargingStation.ocppRequestService.requestHandler<
135 DiagnosticsStatusNotificationRequest,
136 DiagnosticsStatusNotificationResponse
137 >(
138 this.chargingStation,
139 RequestCommand.DIAGNOSTICS_STATUS_NOTIFICATION,
140 requestPayload as DiagnosticsStatusNotificationRequest,
141 requestParams
142 ),
143 ],
144 [
145 BroadcastChannelProcedureName.FIRMWARE_STATUS_NOTIFICATION,
146 async (requestPayload?: BroadcastChannelRequestPayload) =>
147 await this.chargingStation.ocppRequestService.requestHandler<
148 FirmwareStatusNotificationRequest,
149 FirmwareStatusNotificationResponse
150 >(
151 this.chargingStation,
152 RequestCommand.FIRMWARE_STATUS_NOTIFICATION,
153 requestPayload as FirmwareStatusNotificationRequest,
154 requestParams
155 ),
156 ],
157 [
158 BroadcastChannelProcedureName.HEARTBEAT,
159 async (requestPayload?: BroadcastChannelRequestPayload) =>
160 await this.chargingStation.ocppRequestService.requestHandler<
161 HeartbeatRequest,
162 HeartbeatResponse
163 >(
164 this.chargingStation,
165 RequestCommand.HEARTBEAT,
166 requestPayload as HeartbeatRequest,
167 requestParams
168 ),
169 ],
170 [
171 BroadcastChannelProcedureName.METER_VALUES,
172 async (requestPayload?: BroadcastChannelRequestPayload) => {
173 const configuredMeterValueSampleInterval = getConfigurationKey(
174 chargingStation,
175 StandardParametersKey.MeterValueSampleInterval
176 )
177 return await this.chargingStation.ocppRequestService.requestHandler<
178 MeterValuesRequest,
179 MeterValuesResponse
180 >(
181 this.chargingStation,
182 RequestCommand.METER_VALUES,
183 {
184 meterValue: [
185 buildMeterValue(
186 this.chargingStation,
187 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
188 requestPayload!.connectorId!,
189 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
190 this.chargingStation.getConnectorStatus(requestPayload!.connectorId!)!
191 .transactionId!,
192 configuredMeterValueSampleInterval != null
193 ? secondsToMilliseconds(convertToInt(configuredMeterValueSampleInterval.value))
194 : Constants.DEFAULT_METER_VALUES_INTERVAL
195 ),
196 ],
197 ...requestPayload,
198 } as MeterValuesRequest,
199 requestParams
200 )
201 },
202 ],
203 [
204 BroadcastChannelProcedureName.OPEN_CONNECTION,
205 () => {
206 this.chargingStation.openWSConnection()
207 },
208 ],
209 [
210 BroadcastChannelProcedureName.SET_SUPERVISION_URL,
211 (requestPayload?: BroadcastChannelRequestPayload) => {
212 this.chargingStation.setSupervisionUrl(requestPayload?.url as string)
213 },
214 ],
215 [
216 BroadcastChannelProcedureName.START_AUTOMATIC_TRANSACTION_GENERATOR,
217 (requestPayload?: BroadcastChannelRequestPayload) => {
218 this.chargingStation.startAutomaticTransactionGenerator(requestPayload?.connectorIds)
219 },
220 ],
221 [
222 BroadcastChannelProcedureName.START_CHARGING_STATION,
223 () => {
224 this.chargingStation.start()
225 },
226 ],
227 [
228 BroadcastChannelProcedureName.START_TRANSACTION,
229 async (requestPayload?: BroadcastChannelRequestPayload) =>
230 await this.chargingStation.ocppRequestService.requestHandler<
231 StartTransactionRequest,
232 StartTransactionResponse
233 >(
234 this.chargingStation,
235 RequestCommand.START_TRANSACTION,
236 requestPayload as StartTransactionRequest,
237 requestParams
238 ),
239 ],
240 [
241 BroadcastChannelProcedureName.STATUS_NOTIFICATION,
242 async (requestPayload?: BroadcastChannelRequestPayload) =>
243 await this.chargingStation.ocppRequestService.requestHandler<
244 StatusNotificationRequest,
245 StatusNotificationResponse
246 >(
247 this.chargingStation,
248 RequestCommand.STATUS_NOTIFICATION,
249 requestPayload as StatusNotificationRequest,
250 requestParams
251 ),
252 ],
253 [
254 BroadcastChannelProcedureName.STOP_AUTOMATIC_TRANSACTION_GENERATOR,
255 (requestPayload?: BroadcastChannelRequestPayload) => {
256 this.chargingStation.stopAutomaticTransactionGenerator(requestPayload?.connectorIds)
257 },
258 ],
259 [
260 BroadcastChannelProcedureName.STOP_CHARGING_STATION,
261 async () => {
262 await this.chargingStation.stop()
263 },
264 ],
265 [
266 BroadcastChannelProcedureName.STOP_TRANSACTION,
267 async (requestPayload?: BroadcastChannelRequestPayload) =>
268 await this.chargingStation.ocppRequestService.requestHandler<
269 StopTransactionRequest,
270 StartTransactionResponse
271 >(
272 this.chargingStation,
273 RequestCommand.STOP_TRANSACTION,
274 {
275 meterStop: this.chargingStation.getEnergyActiveImportRegisterByTransactionId(
276 requestPayload?.transactionId,
277 true
278 ),
279 ...requestPayload,
280 } as StopTransactionRequest,
281 requestParams
282 ),
283 ],
284 ])
285 this.chargingStation = chargingStation
286 this.onmessage = this.requestHandler.bind(this) as (message: unknown) => void
287 this.onmessageerror = this.messageErrorHandler.bind(this) as (message: unknown) => void
288 }
289
290 private cleanRequestPayload (
291 command: BroadcastChannelProcedureName,
292 requestPayload: BroadcastChannelRequestPayload
293 ): void {
294 delete requestPayload.hashId
295 delete requestPayload.hashIds
296 ![
297 BroadcastChannelProcedureName.START_AUTOMATIC_TRANSACTION_GENERATOR,
298 BroadcastChannelProcedureName.STOP_AUTOMATIC_TRANSACTION_GENERATOR,
299 ].includes(command) && delete requestPayload.connectorIds
300 }
301
302 private async commandHandler (
303 command: BroadcastChannelProcedureName,
304 requestPayload: BroadcastChannelRequestPayload
305 // eslint-disable-next-line @typescript-eslint/no-invalid-void-type
306 ): Promise<CommandResponse | void> {
307 if (this.commandHandlers.has(command)) {
308 this.cleanRequestPayload(command, requestPayload)
309 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
310 const commandHandler = this.commandHandlers.get(command)!
311 if (isAsyncFunction(commandHandler)) {
312 return await commandHandler(requestPayload)
313 }
314 return (
315 commandHandler as (
316 requestPayload?: BroadcastChannelRequestPayload
317 // eslint-disable-next-line @typescript-eslint/no-invalid-void-type
318 ) => CommandResponse | void
319 )(requestPayload)
320 }
321 throw new BaseError(`Unknown worker broadcast channel command: '${command}'`)
322 }
323
324 private commandResponseToResponsePayload (
325 command: BroadcastChannelProcedureName,
326 requestPayload: BroadcastChannelRequestPayload,
327 commandResponse: CommandResponse
328 ): BroadcastChannelResponsePayload {
329 const responseStatus = this.commandResponseToResponseStatus(command, commandResponse)
330 if (responseStatus === ResponseStatus.SUCCESS) {
331 return {
332 hashId: this.chargingStation.stationInfo?.hashId,
333 status: responseStatus,
334 }
335 }
336 return {
337 command,
338 commandResponse,
339 hashId: this.chargingStation.stationInfo?.hashId,
340 requestPayload,
341 status: responseStatus,
342 }
343 }
344
345 private commandResponseToResponseStatus (
346 command: BroadcastChannelProcedureName,
347 commandResponse: CommandResponse
348 ): ResponseStatus {
349 switch (command) {
350 case BroadcastChannelProcedureName.AUTHORIZE:
351 case BroadcastChannelProcedureName.START_TRANSACTION:
352 case BroadcastChannelProcedureName.STOP_TRANSACTION:
353 if (
354 (
355 commandResponse as
356 | AuthorizeResponse
357 | StartTransactionResponse
358 | StopTransactionResponse
359 ).idTagInfo?.status === AuthorizationStatus.ACCEPTED
360 ) {
361 return ResponseStatus.SUCCESS
362 }
363 return ResponseStatus.FAILURE
364 case BroadcastChannelProcedureName.BOOT_NOTIFICATION:
365 if (commandResponse.status === RegistrationStatusEnumType.ACCEPTED) {
366 return ResponseStatus.SUCCESS
367 }
368 return ResponseStatus.FAILURE
369 case BroadcastChannelProcedureName.DATA_TRANSFER:
370 if (commandResponse.status === DataTransferStatus.ACCEPTED) {
371 return ResponseStatus.SUCCESS
372 }
373 return ResponseStatus.FAILURE
374 case BroadcastChannelProcedureName.HEARTBEAT:
375 if ('currentTime' in commandResponse) {
376 return ResponseStatus.SUCCESS
377 }
378 return ResponseStatus.FAILURE
379 case BroadcastChannelProcedureName.METER_VALUES:
380 case BroadcastChannelProcedureName.STATUS_NOTIFICATION:
381 if (isEmpty(commandResponse)) {
382 return ResponseStatus.SUCCESS
383 }
384 return ResponseStatus.FAILURE
385 default:
386 return ResponseStatus.FAILURE
387 }
388 }
389
390 private messageErrorHandler (messageEvent: MessageEvent): void {
391 logger.error(
392 `${this.chargingStation.logPrefix()} ${moduleName}.messageErrorHandler: Error at handling message:`,
393 messageEvent
394 )
395 }
396
397 private requestHandler (messageEvent: MessageEvent): void {
398 const validatedMessageEvent = this.validateMessageEvent(messageEvent)
399 if (validatedMessageEvent === false) {
400 return
401 }
402 if (this.isResponse(validatedMessageEvent.data)) {
403 return
404 }
405 const [uuid, command, requestPayload] = validatedMessageEvent.data as BroadcastChannelRequest
406 if (
407 requestPayload.hashIds != null &&
408 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
409 !requestPayload.hashIds.includes(this.chargingStation.stationInfo!.hashId)
410 ) {
411 return
412 }
413 if (requestPayload.hashId != null) {
414 logger.error(
415 `${this.chargingStation.logPrefix()} ${moduleName}.requestHandler: 'hashId' field usage in PDU is deprecated, use 'hashIds' array instead`
416 )
417 return
418 }
419 let responsePayload: BroadcastChannelResponsePayload | undefined
420 this.commandHandler(command, requestPayload)
421 .then(commandResponse => {
422 if (commandResponse == null || isEmpty(commandResponse)) {
423 responsePayload = {
424 hashId: this.chargingStation.stationInfo?.hashId,
425 status: ResponseStatus.SUCCESS,
426 }
427 } else {
428 responsePayload = this.commandResponseToResponsePayload(
429 command,
430 requestPayload,
431 commandResponse
432 )
433 }
434 return undefined
435 })
436 .finally(() => {
437 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
438 this.sendResponse([uuid, responsePayload!])
439 })
440 .catch((error: unknown) => {
441 logger.error(
442 `${this.chargingStation.logPrefix()} ${moduleName}.requestHandler: Handle request error:`,
443 error
444 )
445 responsePayload = {
446 command,
447 errorDetails: (error as OCPPError).details,
448 errorMessage: (error as OCPPError).message,
449 errorStack: (error as OCPPError).stack,
450 hashId: this.chargingStation.stationInfo?.hashId,
451 requestPayload,
452 status: ResponseStatus.FAILURE,
453 } satisfies BroadcastChannelResponsePayload
454 })
455 }
456 }