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