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