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