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