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