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> {
108 return this.sendRequest(ProcedureName.DELETE_CHARGING_STATIONS, { hashIds: [hashId] })
109 }
110
f8696170
JB
111 public async setSupervisionUrl(hashId: string, supervisionUrl: string): Promise<ResponsePayload> {
112 return this.sendRequest(ProcedureName.SET_SUPERVISION_URL, {
113 hashIds: [hashId],
114 url: supervisionUrl
115 })
116 }
117
8fc2e5cc 118 public async startChargingStation(hashId: string): Promise<ResponsePayload> {
66a7748d 119 return this.sendRequest(ProcedureName.START_CHARGING_STATION, { hashIds: [hashId] })
8fc2e5cc
JB
120 }
121
122 public async stopChargingStation(hashId: string): Promise<ResponsePayload> {
66a7748d 123 return this.sendRequest(ProcedureName.STOP_CHARGING_STATION, { hashIds: [hashId] })
8fc2e5cc
JB
124 }
125
126 public async openConnection(hashId: string): Promise<ResponsePayload> {
127 return this.sendRequest(ProcedureName.OPEN_CONNECTION, {
a974c8e4 128 hashIds: [hashId]
66a7748d 129 })
8fc2e5cc
JB
130 }
131
132 public async closeConnection(hashId: string): Promise<ResponsePayload> {
133 return this.sendRequest(ProcedureName.CLOSE_CONNECTION, {
a974c8e4 134 hashIds: [hashId]
66a7748d 135 })
8fc2e5cc
JB
136 }
137
32de5a57
LM
138 public async startTransaction(
139 hashId: string,
140 connectorId: number,
66a7748d 141 idTag: string | undefined
32de5a57 142 ): Promise<ResponsePayload> {
32de5a57 143 return this.sendRequest(ProcedureName.START_TRANSACTION, {
757b2ecf 144 hashIds: [hashId],
32de5a57 145 connectorId,
a974c8e4 146 idTag
66a7748d 147 })
32de5a57
LM
148 }
149
5a010bf0
JB
150 public async stopTransaction(
151 hashId: string,
66a7748d 152 transactionId: number | undefined
5a010bf0 153 ): Promise<ResponsePayload> {
32de5a57 154 return this.sendRequest(ProcedureName.STOP_TRANSACTION, {
757b2ecf 155 hashIds: [hashId],
a974c8e4 156 transactionId
66a7748d 157 })
32de5a57
LM
158 }
159
757b2ecf
JB
160 public async startAutomaticTransactionGenerator(
161 hashId: string,
66a7748d 162 connectorId: number
757b2ecf
JB
163 ): Promise<ResponsePayload> {
164 return this.sendRequest(ProcedureName.START_AUTOMATIC_TRANSACTION_GENERATOR, {
165 hashIds: [hashId],
a974c8e4 166 connectorIds: [connectorId]
66a7748d 167 })
757b2ecf
JB
168 }
169
170 public async stopAutomaticTransactionGenerator(
171 hashId: string,
66a7748d 172 connectorId: number
757b2ecf
JB
173 ): Promise<ResponsePayload> {
174 return this.sendRequest(ProcedureName.STOP_AUTOMATIC_TRANSACTION_GENERATOR, {
175 hashIds: [hashId],
a974c8e4 176 connectorIds: [connectorId]
66a7748d 177 })
757b2ecf
JB
178 }
179
9e1d6e03 180 private openWS(): void {
329eab0e 181 const protocols =
f292861c
JB
182 this.uiServerConfiguration.authentication?.enabled === true &&
183 this.uiServerConfiguration.authentication?.type === AuthenticationType.PROTOCOL_BASIC_AUTH
329eab0e 184 ? [
f292861c
JB
185 `${this.uiServerConfiguration.protocol}${this.uiServerConfiguration.version}`,
186 `authorization.basic.${btoa(`${this.uiServerConfiguration.authentication.username}:${this.uiServerConfiguration.authentication.password}`).replace(/={1,2}$/, '')}`
329eab0e 187 ]
f292861c 188 : `${this.uiServerConfiguration.protocol}${this.uiServerConfiguration.version}`
12f26d4a 189 this.ws = new WebSocket(
f292861c 190 `${this.uiServerConfiguration.secure === true ? ApplicationProtocol.WSS : ApplicationProtocol.WS}://${this.uiServerConfiguration.host}:${this.uiServerConfiguration.port}`,
329eab0e 191 protocols
66a7748d 192 )
0344ad2b
JB
193 this.ws.onopen = () => {
194 useToast().success(
195 `WebSocket to UI server '${this.uiServerConfiguration.host}' successfully opened`
196 )
e2372e58 197 }
66a7748d 198 this.ws.onmessage = this.responseHandler.bind(this)
a974c8e4 199 this.ws.onerror = errorEvent => {
0344ad2b
JB
200 useToast().error(`Error in WebSocket to UI server '${this.uiServerConfiguration.host}'`)
201 console.error(
202 `Error in WebSocket to UI server '${this.uiServerConfiguration.host}'`,
203 errorEvent
204 )
66a7748d 205 }
0344ad2b 206 this.ws.onclose = () => {
182f8467 207 useToast().info(`WebSocket to UI server closed`)
66a7748d 208 }
5a010bf0
JB
209 }
210
757b2ecf 211 private async sendRequest(
8d6f4790
JB
212 procedureName: ProcedureName,
213 payload: RequestPayload
757b2ecf 214 ): Promise<ResponsePayload> {
5b2721db 215 return new Promise<ResponsePayload>((resolve, reject) => {
916fe456 216 if (this.ws?.readyState === WebSocket.OPEN) {
2610da71 217 const uuid = randomUUID()
8d6f4790 218 const msg = JSON.stringify([uuid, procedureName, payload])
1a32c36b 219 const sendTimeout = setTimeout(() => {
8d6f4790 220 this.responseHandlers.delete(uuid)
e2372e58 221 return reject(new Error(`Send request '${procedureName}' message: connection timeout`))
9d76f5ec 222 }, 60000)
1a32c36b 223 try {
66a7748d 224 this.ws.send(msg)
8d6f4790 225 this.responseHandlers.set(uuid, { procedureName, resolve, reject })
1a32c36b 226 } catch (error) {
8d6f4790 227 this.responseHandlers.delete(uuid)
66a7748d 228 reject(error)
1a32c36b 229 } finally {
66a7748d 230 clearTimeout(sendTimeout)
1a32c36b 231 }
1b2acf4e 232 } else {
e2372e58 233 reject(new Error(`Send request '${procedureName}' message: connection closed`))
1b2acf4e 234 }
66a7748d 235 })
32de5a57
LM
236 }
237
97f0a1a5 238 private responseHandler(messageEvent: MessageEvent<string>): void {
2c5c7443
JB
239 let response: ProtocolResponse
240 try {
9312c9d3 241 response = JSON.parse(messageEvent.data)
2c5c7443 242 } catch (error) {
22427713
JB
243 useToast().error('Invalid response JSON format')
244 console.error('Invalid response JSON format', error)
2c5c7443
JB
245 return
246 }
32de5a57 247
2c5c7443 248 if (!Array.isArray(response)) {
276e05ae
JB
249 useToast().error('Response not an array')
250 console.error('Response not an array:', response)
2c5c7443 251 return
32de5a57
LM
252 }
253
66a7748d 254 const [uuid, responsePayload] = response
32de5a57 255
22427713
JB
256 if (!validateUUID(uuid)) {
257 useToast().error('Response UUID field is invalid')
258 console.error('Response UUID field is invalid:', response)
259 return
260 }
261
2c5c7443 262 if (this.responseHandlers.has(uuid)) {
8d6f4790 263 const { procedureName, resolve, reject } = this.responseHandlers.get(uuid)!
5e3cb728 264 switch (responsePayload.status) {
32de5a57 265 case ResponseStatus.SUCCESS:
66a7748d
JB
266 resolve(responsePayload)
267 break
32de5a57 268 case ResponseStatus.FAILURE:
66a7748d
JB
269 reject(responsePayload)
270 break
32de5a57 271 default:
7c1d037f
JB
272 reject(
273 new Error(
274 `Response status for procedure '${procedureName}' not supported: '${responsePayload.status}'`
275 )
66a7748d 276 )
32de5a57 277 }
8d6f4790 278 this.responseHandlers.delete(uuid)
32de5a57 279 } else {
66a7748d 280 throw new Error(`Not a response to a request: ${JSON.stringify(response, undefined, 2)}`)
32de5a57
LM
281 }
282 }
283}