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