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