feat(ui): introduce toggle button and use it for actions
[e-mobility-charging-stations-simulator.git] / ui / web / src / composables / UIClient.ts
1 import { useToast } from 'vue-toast-notification'
2 import {
3 ApplicationProtocol,
4 AuthenticationType,
5 type ChargingStationOptions,
6 ProcedureName,
7 type ProtocolResponse,
8 type RequestPayload,
9 type ResponsePayload,
10 ResponseStatus,
11 type UIServerConfigurationSection
12 } from '@/types'
13 import { randomUUID } from '@/composables'
14
15 type ResponseHandler = {
16 procedureName: ProcedureName
17 resolve: (value: ResponsePayload | PromiseLike<ResponsePayload>) => void
18 reject: (reason?: unknown) => void
19 }
20
21 export class UIClient {
22 private static instance: UIClient | null = null
23
24 private ws?: WebSocket
25 private responseHandlers: Map<string, ResponseHandler>
26
27 private constructor(private uiServerConfiguration: UIServerConfigurationSection) {
28 this.openWS()
29 this.responseHandlers = new Map<string, ResponseHandler>()
30 }
31
32 public static getInstance(uiServerConfiguration: UIServerConfigurationSection): UIClient {
33 if (UIClient.instance === null) {
34 UIClient.instance = new UIClient(uiServerConfiguration)
35 }
36 return UIClient.instance
37 }
38
39 public setConfiguration(uiServerConfiguration: UIServerConfigurationSection): void {
40 if (this.ws?.readyState === WebSocket.OPEN) {
41 this.ws.close()
42 delete this.ws
43 }
44 this.uiServerConfiguration = uiServerConfiguration
45 this.openWS()
46 }
47
48 public registerWSEventListener<K extends keyof WebSocketEventMap>(
49 event: K,
50 listener: (event: WebSocketEventMap[K]) => void,
51 options?: boolean | AddEventListenerOptions
52 ) {
53 this.ws?.addEventListener(event, listener, options)
54 }
55
56 public async startSimulator(): Promise<ResponsePayload> {
57 return this.sendRequest(ProcedureName.START_SIMULATOR, {})
58 }
59
60 public async stopSimulator(): Promise<ResponsePayload> {
61 return this.sendRequest(ProcedureName.STOP_SIMULATOR, {})
62 }
63
64 public async listTemplates(): Promise<ResponsePayload> {
65 return this.sendRequest(ProcedureName.LIST_TEMPLATES, {})
66 }
67
68 public async listChargingStations(): Promise<ResponsePayload> {
69 return this.sendRequest(ProcedureName.LIST_CHARGING_STATIONS, {})
70 }
71
72 public async addChargingStations(
73 template: string,
74 numberOfStations: number,
75 options?: ChargingStationOptions
76 ): Promise<ResponsePayload> {
77 return this.sendRequest(ProcedureName.ADD_CHARGING_STATIONS, {
78 template,
79 numberOfStations,
80 options
81 })
82 }
83
84 public async deleteChargingStation(hashId: string): Promise<ResponsePayload> {
85 return this.sendRequest(ProcedureName.DELETE_CHARGING_STATIONS, { hashIds: [hashId] })
86 }
87
88 public async setSupervisionUrl(hashId: string, supervisionUrl: string): Promise<ResponsePayload> {
89 return this.sendRequest(ProcedureName.SET_SUPERVISION_URL, {
90 hashIds: [hashId],
91 url: supervisionUrl
92 })
93 }
94
95 public async startChargingStation(hashId: string): Promise<ResponsePayload> {
96 return this.sendRequest(ProcedureName.START_CHARGING_STATION, { hashIds: [hashId] })
97 }
98
99 public async stopChargingStation(hashId: string): Promise<ResponsePayload> {
100 return this.sendRequest(ProcedureName.STOP_CHARGING_STATION, { hashIds: [hashId] })
101 }
102
103 public async openConnection(hashId: string): Promise<ResponsePayload> {
104 return this.sendRequest(ProcedureName.OPEN_CONNECTION, {
105 hashIds: [hashId]
106 })
107 }
108
109 public async closeConnection(hashId: string): Promise<ResponsePayload> {
110 return this.sendRequest(ProcedureName.CLOSE_CONNECTION, {
111 hashIds: [hashId]
112 })
113 }
114
115 public async startTransaction(
116 hashId: string,
117 connectorId: number,
118 idTag: string | undefined
119 ): Promise<ResponsePayload> {
120 return this.sendRequest(ProcedureName.START_TRANSACTION, {
121 hashIds: [hashId],
122 connectorId,
123 idTag
124 })
125 }
126
127 public async stopTransaction(
128 hashId: string,
129 transactionId: number | undefined
130 ): Promise<ResponsePayload> {
131 return this.sendRequest(ProcedureName.STOP_TRANSACTION, {
132 hashIds: [hashId],
133 transactionId
134 })
135 }
136
137 public async startAutomaticTransactionGenerator(
138 hashId: string,
139 connectorId: number
140 ): Promise<ResponsePayload> {
141 return this.sendRequest(ProcedureName.START_AUTOMATIC_TRANSACTION_GENERATOR, {
142 hashIds: [hashId],
143 connectorIds: [connectorId]
144 })
145 }
146
147 public async stopAutomaticTransactionGenerator(
148 hashId: string,
149 connectorId: number
150 ): Promise<ResponsePayload> {
151 return this.sendRequest(ProcedureName.STOP_AUTOMATIC_TRANSACTION_GENERATOR, {
152 hashIds: [hashId],
153 connectorIds: [connectorId]
154 })
155 }
156
157 private openWS(): void {
158 const protocols =
159 this.uiServerConfiguration.authentication?.enabled === true &&
160 this.uiServerConfiguration.authentication?.type === AuthenticationType.PROTOCOL_BASIC_AUTH
161 ? [
162 `${this.uiServerConfiguration.protocol}${this.uiServerConfiguration.version}`,
163 `authorization.basic.${btoa(`${this.uiServerConfiguration.authentication.username}:${this.uiServerConfiguration.authentication.password}`).replace(/={1,2}$/, '')}`
164 ]
165 : `${this.uiServerConfiguration.protocol}${this.uiServerConfiguration.version}`
166 this.ws = new WebSocket(
167 `${this.uiServerConfiguration.secure === true ? ApplicationProtocol.WSS : ApplicationProtocol.WS}://${this.uiServerConfiguration.host}:${this.uiServerConfiguration.port}`,
168 protocols
169 )
170 this.ws.onopen = () => {
171 useToast().success(
172 `WebSocket to UI server '${this.uiServerConfiguration.host}' successfully opened`
173 )
174 }
175 this.ws.onmessage = this.responseHandler.bind(this)
176 this.ws.onerror = errorEvent => {
177 useToast().error(`Error in WebSocket to UI server '${this.uiServerConfiguration.host}'`)
178 console.error(
179 `Error in WebSocket to UI server '${this.uiServerConfiguration.host}'`,
180 errorEvent
181 )
182 }
183 this.ws.onclose = () => {
184 useToast().info(`WebSocket to UI server closed`)
185 }
186 }
187
188 private async sendRequest(
189 procedureName: ProcedureName,
190 payload: RequestPayload
191 ): Promise<ResponsePayload> {
192 return new Promise<ResponsePayload>((resolve, reject) => {
193 if (this.ws?.readyState === WebSocket.OPEN) {
194 const uuid = randomUUID()
195 const msg = JSON.stringify([uuid, procedureName, payload])
196 const sendTimeout = setTimeout(() => {
197 this.responseHandlers.delete(uuid)
198 return reject(new Error(`Send request '${procedureName}' message: connection timeout`))
199 }, 60000)
200 try {
201 this.ws.send(msg)
202 this.responseHandlers.set(uuid, { procedureName, resolve, reject })
203 } catch (error) {
204 this.responseHandlers.delete(uuid)
205 reject(error)
206 } finally {
207 clearTimeout(sendTimeout)
208 }
209 } else {
210 reject(new Error(`Send request '${procedureName}' message: connection closed`))
211 }
212 })
213 }
214
215 private responseHandler(messageEvent: MessageEvent<string>): void {
216 const response = JSON.parse(messageEvent.data) as ProtocolResponse
217
218 if (Array.isArray(response) === false) {
219 throw new Error(`Response not an array: ${JSON.stringify(response, undefined, 2)}`)
220 }
221
222 const [uuid, responsePayload] = response
223
224 if (this.responseHandlers.has(uuid) === true) {
225 const { procedureName, resolve, reject } = this.responseHandlers.get(uuid)!
226 switch (responsePayload.status) {
227 case ResponseStatus.SUCCESS:
228 resolve(responsePayload)
229 break
230 case ResponseStatus.FAILURE:
231 reject(responsePayload)
232 break
233 default:
234 reject(
235 new Error(
236 `Response status for procedure '${procedureName}' not supported: '${responsePayload.status}'`
237 )
238 )
239 }
240 this.responseHandlers.delete(uuid)
241 } else {
242 throw new Error(`Not a response to a request: ${JSON.stringify(response, undefined, 2)}`)
243 }
244 }
245 }