feat: add `deleteChargingStations` SRPC command to UI Services
[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.DELETE_CHARGING_STATIONS,
91 async (requestPayload?: BroadcastChannelRequestPayload) => {
92 await this.chargingStation.delete(requestPayload?.deleteConfiguration as boolean)
93 }
94 ],
95 [
96 BroadcastChannelProcedureName.OPEN_CONNECTION,
97 () => {
98 this.chargingStation.openWSConnection()
99 }
100 ],
101 [
102 BroadcastChannelProcedureName.CLOSE_CONNECTION,
103 () => {
104 this.chargingStation.closeWSConnection()
105 }
106 ],
107 [
108 BroadcastChannelProcedureName.START_AUTOMATIC_TRANSACTION_GENERATOR,
109 (requestPayload?: BroadcastChannelRequestPayload) => {
110 this.chargingStation.startAutomaticTransactionGenerator(requestPayload?.connectorIds)
111 }
112 ],
113 [
114 BroadcastChannelProcedureName.STOP_AUTOMATIC_TRANSACTION_GENERATOR,
115 (requestPayload?: BroadcastChannelRequestPayload) => {
116 this.chargingStation.stopAutomaticTransactionGenerator(requestPayload?.connectorIds)
117 }
118 ],
119 [
120 BroadcastChannelProcedureName.SET_SUPERVISION_URL,
121 (requestPayload?: BroadcastChannelRequestPayload) => {
122 this.chargingStation.setSupervisionUrl(requestPayload?.url as string)
123 }
124 ],
125 [
126 BroadcastChannelProcedureName.START_TRANSACTION,
127 async (requestPayload?: BroadcastChannelRequestPayload) =>
128 await this.chargingStation.ocppRequestService.requestHandler<
129 StartTransactionRequest,
130 StartTransactionResponse
131 >(this.chargingStation, RequestCommand.START_TRANSACTION, requestPayload, requestParams)
132 ],
133 [
134 BroadcastChannelProcedureName.STOP_TRANSACTION,
135 async (requestPayload?: BroadcastChannelRequestPayload) =>
136 await this.chargingStation.ocppRequestService.requestHandler<
137 StopTransactionRequest,
138 StartTransactionResponse
139 >(
140 this.chargingStation,
141 RequestCommand.STOP_TRANSACTION,
142 {
143 meterStop: this.chargingStation.getEnergyActiveImportRegisterByTransactionId(
144 requestPayload?.transactionId,
145 true
146 ),
147 ...requestPayload
148 },
149 requestParams
150 )
151 ],
152 [
153 BroadcastChannelProcedureName.AUTHORIZE,
154 async (requestPayload?: BroadcastChannelRequestPayload) =>
155 await this.chargingStation.ocppRequestService.requestHandler<
156 AuthorizeRequest,
157 AuthorizeResponse
158 >(this.chargingStation, RequestCommand.AUTHORIZE, requestPayload, requestParams)
159 ],
160 [
161 BroadcastChannelProcedureName.BOOT_NOTIFICATION,
162 async (requestPayload?: BroadcastChannelRequestPayload) => {
163 this.chargingStation.bootNotificationResponse =
164 await this.chargingStation.ocppRequestService.requestHandler<
165 BootNotificationRequest,
166 BootNotificationResponse
167 >(
168 this.chargingStation,
169 RequestCommand.BOOT_NOTIFICATION,
170 {
171 ...this.chargingStation.bootNotificationRequest,
172 ...requestPayload
173 },
174 {
175 skipBufferingOnError: true,
176 throwError: true
177 }
178 )
179 return this.chargingStation.bootNotificationResponse
180 }
181 ],
182 [
183 BroadcastChannelProcedureName.STATUS_NOTIFICATION,
184 async (requestPayload?: BroadcastChannelRequestPayload) =>
185 await this.chargingStation.ocppRequestService.requestHandler<
186 StatusNotificationRequest,
187 StatusNotificationResponse
188 >(this.chargingStation, RequestCommand.STATUS_NOTIFICATION, requestPayload, requestParams)
189 ],
190 [
191 BroadcastChannelProcedureName.HEARTBEAT,
192 async (requestPayload?: BroadcastChannelRequestPayload) =>
193 await this.chargingStation.ocppRequestService.requestHandler<
194 HeartbeatRequest,
195 HeartbeatResponse
196 >(this.chargingStation, RequestCommand.HEARTBEAT, requestPayload, requestParams)
197 ],
198 [
199 BroadcastChannelProcedureName.METER_VALUES,
200 async (requestPayload?: BroadcastChannelRequestPayload) => {
201 const configuredMeterValueSampleInterval = getConfigurationKey(
202 chargingStation,
203 StandardParametersKey.MeterValueSampleInterval
204 )
205 return await this.chargingStation.ocppRequestService.requestHandler<
206 MeterValuesRequest,
207 MeterValuesResponse
208 >(
209 this.chargingStation,
210 RequestCommand.METER_VALUES,
211 {
212 meterValue: [
213 buildMeterValue(
214 this.chargingStation,
215 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
216 requestPayload!.connectorId!,
217 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
218 this.chargingStation.getConnectorStatus(requestPayload!.connectorId!)!
219 .transactionId!,
220 configuredMeterValueSampleInterval != null
221 ? secondsToMilliseconds(convertToInt(configuredMeterValueSampleInterval.value))
222 : Constants.DEFAULT_METER_VALUES_INTERVAL
223 )
224 ],
225 ...requestPayload
226 },
227 requestParams
228 )
229 }
230 ],
231 [
232 BroadcastChannelProcedureName.DATA_TRANSFER,
233 async (requestPayload?: BroadcastChannelRequestPayload) =>
234 await this.chargingStation.ocppRequestService.requestHandler<
235 DataTransferRequest,
236 DataTransferResponse
237 >(this.chargingStation, RequestCommand.DATA_TRANSFER, requestPayload, requestParams)
238 ],
239 [
240 BroadcastChannelProcedureName.DIAGNOSTICS_STATUS_NOTIFICATION,
241 async (requestPayload?: BroadcastChannelRequestPayload) =>
242 await this.chargingStation.ocppRequestService.requestHandler<
243 DiagnosticsStatusNotificationRequest,
244 DiagnosticsStatusNotificationResponse
245 >(
246 this.chargingStation,
247 RequestCommand.DIAGNOSTICS_STATUS_NOTIFICATION,
248 requestPayload,
249 requestParams
250 )
251 ],
252 [
253 BroadcastChannelProcedureName.FIRMWARE_STATUS_NOTIFICATION,
254 async (requestPayload?: BroadcastChannelRequestPayload) =>
255 await this.chargingStation.ocppRequestService.requestHandler<
256 FirmwareStatusNotificationRequest,
257 FirmwareStatusNotificationResponse
258 >(
259 this.chargingStation,
260 RequestCommand.FIRMWARE_STATUS_NOTIFICATION,
261 requestPayload,
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
269 }
270
271 private requestHandler (messageEvent: MessageEvent): void {
272 const validatedMessageEvent = this.validateMessageEvent(messageEvent)
273 if (validatedMessageEvent === false) {
274 return
275 }
276 if (this.isResponse(validatedMessageEvent.data)) {
277 return
278 }
279 const [uuid, command, requestPayload] = validatedMessageEvent.data as BroadcastChannelRequest
280 if (
281 requestPayload.hashIds != null &&
282 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
283 !requestPayload.hashIds.includes(this.chargingStation.stationInfo!.hashId)
284 ) {
285 return
286 }
287 if (requestPayload.hashId != null) {
288 logger.error(
289 `${this.chargingStation.logPrefix()} ${moduleName}.requestHandler: 'hashId' field usage in PDU is deprecated, use 'hashIds' array instead`
290 )
291 return
292 }
293 let responsePayload: BroadcastChannelResponsePayload | undefined
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 )
314 responsePayload = {
315 hashId: this.chargingStation.stationInfo?.hashId,
316 status: ResponseStatus.FAILURE,
317 command,
318 requestPayload,
319 errorMessage: (error as OCPPError).message,
320 errorStack: (error as OCPPError).stack,
321 errorDetails: (error as OCPPError).details
322 } satisfies BroadcastChannelResponsePayload
323 })
324 .finally(() => {
325 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
326 this.sendResponse([uuid, responsePayload!])
327 })
328 }
329
330 private messageErrorHandler (messageEvent: MessageEvent): void {
331 logger.error(
332 `${this.chargingStation.logPrefix()} ${moduleName}.messageErrorHandler: Error at handling message:`,
333 messageEvent
334 )
335 }
336
337 private async commandHandler (
338 command: BroadcastChannelProcedureName,
339 requestPayload: BroadcastChannelRequestPayload
340 // eslint-disable-next-line @typescript-eslint/no-invalid-void-type
341 ): Promise<CommandResponse | void> {
342 if (this.commandHandlers.has(command)) {
343 this.cleanRequestPayload(command, requestPayload)
344 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
345 const commandHandler = this.commandHandlers.get(command)!
346 if (isAsyncFunction(commandHandler)) {
347 return await commandHandler(requestPayload)
348 }
349 return (
350 commandHandler as (
351 requestPayload?: BroadcastChannelRequestPayload
352 // eslint-disable-next-line @typescript-eslint/no-invalid-void-type
353 ) => CommandResponse | void
354 )(requestPayload)
355 }
356 throw new BaseError(`Unknown worker broadcast channel command: '${command}'`)
357 }
358
359 private cleanRequestPayload (
360 command: BroadcastChannelProcedureName,
361 requestPayload: BroadcastChannelRequestPayload
362 ): void {
363 delete requestPayload.hashId
364 delete requestPayload.hashIds
365 ![
366 BroadcastChannelProcedureName.START_AUTOMATIC_TRANSACTION_GENERATOR,
367 BroadcastChannelProcedureName.STOP_AUTOMATIC_TRANSACTION_GENERATOR
368 ].includes(command) && delete requestPayload.connectorIds
369 }
370
371 private commandResponseToResponsePayload (
372 command: BroadcastChannelProcedureName,
373 requestPayload: BroadcastChannelRequestPayload,
374 commandResponse: CommandResponse
375 ): BroadcastChannelResponsePayload {
376 const responseStatus = this.commandResponseToResponseStatus(command, commandResponse)
377 if (responseStatus === ResponseStatus.SUCCESS) {
378 return {
379 hashId: this.chargingStation.stationInfo?.hashId,
380 status: responseStatus
381 }
382 }
383 return {
384 hashId: this.chargingStation.stationInfo?.hashId,
385 status: responseStatus,
386 command,
387 requestPayload,
388 commandResponse
389 }
390 }
391
392 private commandResponseToResponseStatus (
393 command: BroadcastChannelProcedureName,
394 commandResponse: CommandResponse
395 ): ResponseStatus {
396 switch (command) {
397 case BroadcastChannelProcedureName.START_TRANSACTION:
398 case BroadcastChannelProcedureName.STOP_TRANSACTION:
399 case BroadcastChannelProcedureName.AUTHORIZE:
400 if (
401 (
402 commandResponse as
403 | StartTransactionResponse
404 | StopTransactionResponse
405 | AuthorizeResponse
406 ).idTagInfo?.status === AuthorizationStatus.ACCEPTED
407 ) {
408 return ResponseStatus.SUCCESS
409 }
410 return ResponseStatus.FAILURE
411 case BroadcastChannelProcedureName.BOOT_NOTIFICATION:
412 if (commandResponse.status === RegistrationStatusEnumType.ACCEPTED) {
413 return ResponseStatus.SUCCESS
414 }
415 return ResponseStatus.FAILURE
416 case BroadcastChannelProcedureName.DATA_TRANSFER:
417 if (commandResponse.status === DataTransferStatus.ACCEPTED) {
418 return ResponseStatus.SUCCESS
419 }
420 return ResponseStatus.FAILURE
421 case BroadcastChannelProcedureName.STATUS_NOTIFICATION:
422 case BroadcastChannelProcedureName.METER_VALUES:
423 if (isEmptyObject(commandResponse)) {
424 return ResponseStatus.SUCCESS
425 }
426 return ResponseStatus.FAILURE
427 case BroadcastChannelProcedureName.HEARTBEAT:
428 if ('currentTime' in commandResponse) {
429 return ResponseStatus.SUCCESS
430 }
431 return ResponseStatus.FAILURE
432 default:
433 return ResponseStatus.FAILURE
434 }
435 }
436 }