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