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