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