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