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