refactor(ui): refine configuration file type and make it conditional
[e-mobility-charging-stations-simulator.git] / ui / web / src / composables / UIClient.ts
1 import {
2 ApplicationProtocol,
3 ProcedureName,
4 type ProtocolResponse,
5 type RequestPayload,
6 type ResponsePayload,
7 ResponseStatus
8 } from '@/types'
9 // @ts-expect-error: configuration file can be non existent
10 // eslint-disable-next-line import/no-unresolved
11 import configuration from '@/assets/config'
12
13 type ResponseHandler = {
14 procedureName: ProcedureName
15 resolve: (value: ResponsePayload | PromiseLike<ResponsePayload>) => void
16 reject: (reason?: unknown) => void
17 }
18
19 export class UIClient {
20 private static instance: UIClient | null = null
21
22 private ws!: WebSocket
23 private responseHandlers: Map<string, ResponseHandler>
24
25 private constructor() {
26 this.openWS()
27 this.responseHandlers = new Map<string, ResponseHandler>()
28 }
29
30 public static getInstance() {
31 if (UIClient.instance === null) {
32 UIClient.instance = new UIClient()
33 }
34 return UIClient.instance
35 }
36
37 public registerWSonOpenListener(listener: (event: Event) => void) {
38 this.ws.addEventListener('open', listener)
39 }
40
41 public async startSimulator(): Promise<ResponsePayload> {
42 return this.sendRequest(ProcedureName.START_SIMULATOR, {})
43 }
44
45 public async stopSimulator(): Promise<ResponsePayload> {
46 return this.sendRequest(ProcedureName.STOP_SIMULATOR, {})
47 }
48
49 public async listChargingStations(): Promise<ResponsePayload> {
50 return this.sendRequest(ProcedureName.LIST_CHARGING_STATIONS, {})
51 }
52
53 public async startChargingStation(hashId: string): Promise<ResponsePayload> {
54 return this.sendRequest(ProcedureName.START_CHARGING_STATION, { hashIds: [hashId] })
55 }
56
57 public async stopChargingStation(hashId: string): Promise<ResponsePayload> {
58 return this.sendRequest(ProcedureName.STOP_CHARGING_STATION, { hashIds: [hashId] })
59 }
60
61 public async openConnection(hashId: string): Promise<ResponsePayload> {
62 return this.sendRequest(ProcedureName.OPEN_CONNECTION, {
63 hashIds: [hashId]
64 })
65 }
66
67 public async closeConnection(hashId: string): Promise<ResponsePayload> {
68 return this.sendRequest(ProcedureName.CLOSE_CONNECTION, {
69 hashIds: [hashId]
70 })
71 }
72
73 public async startTransaction(
74 hashId: string,
75 connectorId: number,
76 idTag: string | undefined
77 ): Promise<ResponsePayload> {
78 return this.sendRequest(ProcedureName.START_TRANSACTION, {
79 hashIds: [hashId],
80 connectorId,
81 idTag
82 })
83 }
84
85 public async stopTransaction(
86 hashId: string,
87 transactionId: number | undefined
88 ): Promise<ResponsePayload> {
89 return this.sendRequest(ProcedureName.STOP_TRANSACTION, {
90 hashIds: [hashId],
91 transactionId
92 })
93 }
94
95 public async startAutomaticTransactionGenerator(
96 hashId: string,
97 connectorId: number
98 ): Promise<ResponsePayload> {
99 return this.sendRequest(ProcedureName.START_AUTOMATIC_TRANSACTION_GENERATOR, {
100 hashIds: [hashId],
101 connectorIds: [connectorId]
102 })
103 }
104
105 public async stopAutomaticTransactionGenerator(
106 hashId: string,
107 connectorId: number
108 ): Promise<ResponsePayload> {
109 return this.sendRequest(ProcedureName.STOP_AUTOMATIC_TRANSACTION_GENERATOR, {
110 hashIds: [hashId],
111 connectorIds: [connectorId]
112 })
113 }
114
115 private openWS(): void {
116 this.ws = new WebSocket(
117 `${configuration.uiServer.secure === true ? ApplicationProtocol.WSS : ApplicationProtocol.WS}://${configuration.uiServer.host}:${configuration.uiServer.port}`,
118 `${configuration.uiServer.protocol}${configuration.uiServer.version}`
119 )
120 this.ws.onmessage = this.responseHandler.bind(this)
121 this.ws.onerror = errorEvent => {
122 console.error('WebSocket error: ', errorEvent)
123 }
124 this.ws.onclose = closeEvent => {
125 console.info('WebSocket closed: ', closeEvent)
126 }
127 }
128
129 private async sendRequest(
130 procedureName: ProcedureName,
131 payload: RequestPayload
132 ): Promise<ResponsePayload> {
133 return new Promise<ResponsePayload>((resolve, reject) => {
134 if (this.ws.readyState !== WebSocket.OPEN) {
135 this.openWS()
136 }
137 if (this.ws.readyState === WebSocket.OPEN) {
138 const uuid = crypto.randomUUID()
139 const msg = JSON.stringify([uuid, procedureName, payload])
140 const sendTimeout = setTimeout(() => {
141 this.responseHandlers.delete(uuid)
142 return reject(new Error(`Send request '${procedureName}' message timeout`))
143 }, 60 * 1000)
144 try {
145 this.ws.send(msg)
146 this.responseHandlers.set(uuid, { procedureName, resolve, reject })
147 } catch (error) {
148 this.responseHandlers.delete(uuid)
149 reject(error)
150 } finally {
151 clearTimeout(sendTimeout)
152 }
153 } else {
154 throw new Error(`Send request '${procedureName}' message: connection not opened`)
155 }
156 })
157 }
158
159 private responseHandler(messageEvent: MessageEvent<string>): void {
160 const response = JSON.parse(messageEvent.data) as ProtocolResponse
161
162 if (Array.isArray(response) === false) {
163 throw new Error(`Response not an array: ${JSON.stringify(response, undefined, 2)}`)
164 }
165
166 const [uuid, responsePayload] = response
167
168 if (this.responseHandlers.has(uuid) === true) {
169 const { procedureName, resolve, reject } = this.responseHandlers.get(uuid)!
170 switch (responsePayload.status) {
171 case ResponseStatus.SUCCESS:
172 resolve(responsePayload)
173 break
174 case ResponseStatus.FAILURE:
175 reject(responsePayload)
176 break
177 default:
178 console.error(
179 `Response status for procedure '${procedureName}' not supported: '${responsePayload.status}'`
180 )
181 }
182 this.responseHandlers.delete(uuid)
183 } else {
184 throw new Error(`Not a response to a request: ${JSON.stringify(response, undefined, 2)}`)
185 }
186 }
187 }