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