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