c6e37f6095a3ddbb5a4cfe125add8cc5f8434ea4
[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 isAsyncFunction,
44 isEmptyObject,
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> | CommandResponse | 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 requestPayload?.transactionId,
139 true
140 ),
141 ...requestPayload
142 },
143 requestParams
144 )
145 ],
146 [
147 BroadcastChannelProcedureName.AUTHORIZE,
148 async (requestPayload?: BroadcastChannelRequestPayload) =>
149 await this.chargingStation.ocppRequestService.requestHandler<
150 AuthorizeRequest,
151 AuthorizeResponse
152 >(this.chargingStation, RequestCommand.AUTHORIZE, requestPayload, requestParams)
153 ],
154 [
155 BroadcastChannelProcedureName.BOOT_NOTIFICATION,
156 async (requestPayload?: BroadcastChannelRequestPayload) => {
157 this.chargingStation.bootNotificationResponse =
158 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 return this.chargingStation.bootNotificationResponse
174 }
175 ],
176 [
177 BroadcastChannelProcedureName.STATUS_NOTIFICATION,
178 async (requestPayload?: BroadcastChannelRequestPayload) =>
179 await this.chargingStation.ocppRequestService.requestHandler<
180 StatusNotificationRequest,
181 StatusNotificationResponse
182 >(this.chargingStation, RequestCommand.STATUS_NOTIFICATION, requestPayload, requestParams)
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 requestHandler (messageEvent: MessageEvent): 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 this.commandHandler(command, requestPayload)
291 .then(commandResponse => {
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 })
305 .catch(error => {
306 logger.error(
307 `${this.chargingStation.logPrefix()} ${moduleName}.requestHandler: Handle request error:`,
308 error
309 )
310 responsePayload = {
311 hashId: this.chargingStation.stationInfo?.hashId,
312 status: ResponseStatus.FAILURE,
313 command,
314 requestPayload,
315 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
316 commandResponse: commandResponse!,
317 errorMessage: (error as OCPPError).message,
318 errorStack: (error as OCPPError).stack,
319 errorDetails: (error as OCPPError).details
320 }
321 })
322 .finally(() => {
323 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
324 this.sendResponse([uuid, responsePayload!])
325 })
326 }
327
328 private messageErrorHandler (messageEvent: MessageEvent): void {
329 logger.error(
330 `${this.chargingStation.logPrefix()} ${moduleName}.messageErrorHandler: Error at handling message:`,
331 messageEvent
332 )
333 }
334
335 private async commandHandler (
336 command: BroadcastChannelProcedureName,
337 requestPayload: BroadcastChannelRequestPayload
338 // eslint-disable-next-line @typescript-eslint/no-invalid-void-type
339 ): Promise<CommandResponse | void> {
340 if (this.commandHandlers.has(command)) {
341 this.cleanRequestPayload(command, requestPayload)
342 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
343 const commandHandler = this.commandHandlers.get(command)!
344 if (isAsyncFunction(commandHandler)) {
345 return await commandHandler(requestPayload)
346 }
347 return (
348 commandHandler as (
349 requestPayload?: BroadcastChannelRequestPayload
350 // eslint-disable-next-line @typescript-eslint/no-invalid-void-type
351 ) => CommandResponse | void
352 )(requestPayload)
353 }
354 throw new BaseError(`Unknown worker broadcast channel command: '${command}'`)
355 }
356
357 private cleanRequestPayload (
358 command: BroadcastChannelProcedureName,
359 requestPayload: BroadcastChannelRequestPayload
360 ): void {
361 delete requestPayload.hashId
362 delete requestPayload.hashIds
363 ![
364 BroadcastChannelProcedureName.START_AUTOMATIC_TRANSACTION_GENERATOR,
365 BroadcastChannelProcedureName.STOP_AUTOMATIC_TRANSACTION_GENERATOR
366 ].includes(command) && delete requestPayload.connectorIds
367 }
368
369 private commandResponseToResponsePayload (
370 command: BroadcastChannelProcedureName,
371 requestPayload: BroadcastChannelRequestPayload,
372 commandResponse: CommandResponse
373 ): BroadcastChannelResponsePayload {
374 const responseStatus = this.commandResponseToResponseStatus(command, commandResponse)
375 if (responseStatus === ResponseStatus.SUCCESS) {
376 return {
377 hashId: this.chargingStation.stationInfo?.hashId,
378 status: responseStatus
379 }
380 }
381 return {
382 hashId: this.chargingStation.stationInfo?.hashId,
383 status: responseStatus,
384 command,
385 requestPayload,
386 commandResponse
387 }
388 }
389
390 private commandResponseToResponseStatus (
391 command: BroadcastChannelProcedureName,
392 commandResponse: CommandResponse
393 ): ResponseStatus {
394 switch (command) {
395 case BroadcastChannelProcedureName.START_TRANSACTION:
396 case BroadcastChannelProcedureName.STOP_TRANSACTION:
397 case BroadcastChannelProcedureName.AUTHORIZE:
398 if (
399 (
400 commandResponse as
401 | StartTransactionResponse
402 | StopTransactionResponse
403 | AuthorizeResponse
404 ).idTagInfo?.status === AuthorizationStatus.ACCEPTED
405 ) {
406 return ResponseStatus.SUCCESS
407 }
408 return ResponseStatus.FAILURE
409 case BroadcastChannelProcedureName.BOOT_NOTIFICATION:
410 if (commandResponse.status === RegistrationStatusEnumType.ACCEPTED) {
411 return ResponseStatus.SUCCESS
412 }
413 return ResponseStatus.FAILURE
414 case BroadcastChannelProcedureName.DATA_TRANSFER:
415 if (commandResponse.status === DataTransferStatus.ACCEPTED) {
416 return ResponseStatus.SUCCESS
417 }
418 return ResponseStatus.FAILURE
419 case BroadcastChannelProcedureName.STATUS_NOTIFICATION:
420 case BroadcastChannelProcedureName.METER_VALUES:
421 if (isEmptyObject(commandResponse)) {
422 return ResponseStatus.SUCCESS
423 }
424 return ResponseStatus.FAILURE
425 case BroadcastChannelProcedureName.HEARTBEAT:
426 if ('currentTime' in commandResponse) {
427 return ResponseStatus.SUCCESS
428 }
429 return ResponseStatus.FAILURE
430 default:
431 return ResponseStatus.FAILURE
432 }
433 }
434 }