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