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