feat(ui): add helper to dynamically set configuration on UI client
[e-mobility-charging-stations-simulator.git] / ui / web / src / composables / UIClient.ts
1 import {
2 ApplicationProtocol,
3 AuthenticationType,
4 ProcedureName,
5 type ProtocolResponse,
6 type RequestPayload,
7 type ResponsePayload,
8 ResponseStatus,
9 type UIServerConfigurationSection
10 } from '@/types'
11
12 type ResponseHandler = {
13 procedureName: ProcedureName
14 resolve: (value: ResponsePayload | PromiseLike<ResponsePayload>) => void
15 reject: (reason?: unknown) => void
16 }
17
18 export class UIClient {
19 private static instance: UIClient | null = null
20
21 private ws!: WebSocket
22 private responseHandlers: Map<string, ResponseHandler>
23
24 private constructor(private uiServerConfiguration: UIServerConfigurationSection) {
25 this.openWS()
26 this.responseHandlers = new Map<string, ResponseHandler>()
27 }
28
29 public static getInstance(uiServerConfiguration: UIServerConfigurationSection): UIClient {
30 if (UIClient.instance === null) {
31 UIClient.instance = new UIClient(uiServerConfiguration)
32 }
33 return UIClient.instance
34 }
35
36 public setConfiguration(uiServerConfiguration: UIServerConfigurationSection): void {
37 this.ws.close()
38 this.uiServerConfiguration = uiServerConfiguration
39 this.openWS()
40 }
41
42 public registerWSEventListener<K extends keyof WebSocketEventMap>(
43 event: K,
44 listener: (event: WebSocketEventMap[K]) => void
45 ) {
46 this.ws.addEventListener(event, listener)
47 }
48
49 public async startSimulator(): Promise<ResponsePayload> {
50 return this.sendRequest(ProcedureName.START_SIMULATOR, {})
51 }
52
53 public async stopSimulator(): Promise<ResponsePayload> {
54 return this.sendRequest(ProcedureName.STOP_SIMULATOR, {})
55 }
56
57 public async listTemplates(): Promise<ResponsePayload> {
58 return this.sendRequest(ProcedureName.LIST_TEMPLATES, {})
59 }
60
61 public async listChargingStations(): Promise<ResponsePayload> {
62 return this.sendRequest(ProcedureName.LIST_CHARGING_STATIONS, {})
63 }
64
65 public async addChargingStations(
66 template: string,
67 numberOfStations: number
68 ): Promise<ResponsePayload> {
69 return this.sendRequest(ProcedureName.ADD_CHARGING_STATIONS, { template, numberOfStations })
70 }
71
72 public async deleteChargingStation(hashId: string): Promise<ResponsePayload> {
73 return this.sendRequest(ProcedureName.DELETE_CHARGING_STATIONS, { hashIds: [hashId] })
74 }
75
76 public async setSupervisionUrl(hashId: string, supervisionUrl: string): Promise<ResponsePayload> {
77 return this.sendRequest(ProcedureName.SET_SUPERVISION_URL, {
78 hashIds: [hashId],
79 url: supervisionUrl
80 })
81 }
82
83 public async startChargingStation(hashId: string): Promise<ResponsePayload> {
84 return this.sendRequest(ProcedureName.START_CHARGING_STATION, { hashIds: [hashId] })
85 }
86
87 public async stopChargingStation(hashId: string): Promise<ResponsePayload> {
88 return this.sendRequest(ProcedureName.STOP_CHARGING_STATION, { hashIds: [hashId] })
89 }
90
91 public async openConnection(hashId: string): Promise<ResponsePayload> {
92 return this.sendRequest(ProcedureName.OPEN_CONNECTION, {
93 hashIds: [hashId]
94 })
95 }
96
97 public async closeConnection(hashId: string): Promise<ResponsePayload> {
98 return this.sendRequest(ProcedureName.CLOSE_CONNECTION, {
99 hashIds: [hashId]
100 })
101 }
102
103 public async startTransaction(
104 hashId: string,
105 connectorId: number,
106 idTag: string | undefined
107 ): Promise<ResponsePayload> {
108 return this.sendRequest(ProcedureName.START_TRANSACTION, {
109 hashIds: [hashId],
110 connectorId,
111 idTag
112 })
113 }
114
115 public async stopTransaction(
116 hashId: string,
117 transactionId: number | undefined
118 ): Promise<ResponsePayload> {
119 return this.sendRequest(ProcedureName.STOP_TRANSACTION, {
120 hashIds: [hashId],
121 transactionId
122 })
123 }
124
125 public async startAutomaticTransactionGenerator(
126 hashId: string,
127 connectorId: number
128 ): Promise<ResponsePayload> {
129 return this.sendRequest(ProcedureName.START_AUTOMATIC_TRANSACTION_GENERATOR, {
130 hashIds: [hashId],
131 connectorIds: [connectorId]
132 })
133 }
134
135 public async stopAutomaticTransactionGenerator(
136 hashId: string,
137 connectorId: number
138 ): Promise<ResponsePayload> {
139 return this.sendRequest(ProcedureName.STOP_AUTOMATIC_TRANSACTION_GENERATOR, {
140 hashIds: [hashId],
141 connectorIds: [connectorId]
142 })
143 }
144
145 private openWS(): void {
146 const protocols =
147 this.uiServerConfiguration.authentication?.enabled === true &&
148 this.uiServerConfiguration.authentication?.type === AuthenticationType.PROTOCOL_BASIC_AUTH
149 ? [
150 `${this.uiServerConfiguration.protocol}${this.uiServerConfiguration.version}`,
151 `authorization.basic.${btoa(`${this.uiServerConfiguration.authentication.username}:${this.uiServerConfiguration.authentication.password}`).replace(/={1,2}$/, '')}`
152 ]
153 : `${this.uiServerConfiguration.protocol}${this.uiServerConfiguration.version}`
154 this.ws = new WebSocket(
155 `${this.uiServerConfiguration.secure === true ? ApplicationProtocol.WSS : ApplicationProtocol.WS}://${this.uiServerConfiguration.host}:${this.uiServerConfiguration.port}`,
156 protocols
157 )
158 this.ws.onopen = openEvent => {
159 console.info('WebSocket opened', openEvent)
160 }
161 this.ws.onmessage = this.responseHandler.bind(this)
162 this.ws.onerror = errorEvent => {
163 console.error('WebSocket error: ', errorEvent)
164 }
165 this.ws.onclose = closeEvent => {
166 console.info('WebSocket closed: ', closeEvent)
167 }
168 }
169
170 private async sendRequest(
171 procedureName: ProcedureName,
172 payload: RequestPayload
173 ): Promise<ResponsePayload> {
174 return new Promise<ResponsePayload>((resolve, reject) => {
175 if (this.ws.readyState === WebSocket.OPEN) {
176 const uuid = crypto.randomUUID()
177 const msg = JSON.stringify([uuid, procedureName, payload])
178 const sendTimeout = setTimeout(() => {
179 this.responseHandlers.delete(uuid)
180 return reject(new Error(`Send request '${procedureName}' message: connection timeout`))
181 }, 60000)
182 try {
183 this.ws.send(msg)
184 this.responseHandlers.set(uuid, { procedureName, resolve, reject })
185 } catch (error) {
186 this.responseHandlers.delete(uuid)
187 reject(error)
188 } finally {
189 clearTimeout(sendTimeout)
190 }
191 } else {
192 reject(new Error(`Send request '${procedureName}' message: connection closed`))
193 }
194 })
195 }
196
197 private responseHandler(messageEvent: MessageEvent<string>): void {
198 const response = JSON.parse(messageEvent.data) as ProtocolResponse
199
200 if (Array.isArray(response) === false) {
201 throw new Error(`Response not an array: ${JSON.stringify(response, undefined, 2)}`)
202 }
203
204 const [uuid, responsePayload] = response
205
206 if (this.responseHandlers.has(uuid) === true) {
207 const { procedureName, resolve, reject } = this.responseHandlers.get(uuid)!
208 switch (responsePayload.status) {
209 case ResponseStatus.SUCCESS:
210 resolve(responsePayload)
211 break
212 case ResponseStatus.FAILURE:
213 reject(responsePayload)
214 break
215 default:
216 reject(
217 new Error(
218 `Response status for procedure '${procedureName}' not supported: '${responsePayload.status}'`
219 )
220 )
221 }
222 this.responseHandlers.delete(uuid)
223 } else {
224 throw new Error(`Not a response to a request: ${JSON.stringify(response, undefined, 2)}`)
225 }
226 }
227 }