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