Commit | Line | Data |
---|---|---|
0344ad2b | 1 | import { useToast } from 'vue-toast-notification' |
84ec8d34 | 2 | |
0f71040c | 3 | import { |
217db058 | 4 | ApplicationProtocol, |
329eab0e | 5 | AuthenticationType, |
093ca832 | 6 | type ChargingStationOptions, |
0f71040c | 7 | ProcedureName, |
82a77234 | 8 | type ProtocolResponse, |
0f71040c JB |
9 | type RequestPayload, |
10 | type ResponsePayload, | |
f292861c JB |
11 | ResponseStatus, |
12 | type UIServerConfigurationSection | |
66a7748d | 13 | } from '@/types' |
32de5a57 | 14 | |
22427713 | 15 | import { randomUUID, validateUUID } from './Utils' |
84ec8d34 | 16 | |
32de5a57 | 17 | type ResponseHandler = { |
66a7748d JB |
18 | procedureName: ProcedureName |
19 | resolve: (value: ResponsePayload | PromiseLike<ResponsePayload>) => void | |
20 | reject: (reason?: unknown) => void | |
21 | } | |
32de5a57 | 22 | |
8137295e | 23 | export class UIClient { |
c25a4046 | 24 | private static instance: UIClient | null = null |
32de5a57 | 25 | |
916fe456 | 26 | private ws?: WebSocket |
2c5c7443 JB |
27 | private responseHandlers: Map< |
28 | `${string}-${string}-${string}-${string}-${string}`, | |
29 | ResponseHandler | |
30 | > | |
32de5a57 | 31 | |
f292861c | 32 | private constructor(private uiServerConfiguration: UIServerConfigurationSection) { |
66a7748d | 33 | this.openWS() |
2c5c7443 JB |
34 | this.responseHandlers = new Map< |
35 | `${string}-${string}-${string}-${string}-${string}`, | |
36 | ResponseHandler | |
37 | >() | |
32de5a57 LM |
38 | } |
39 | ||
39cd8fcb | 40 | public static getInstance(uiServerConfiguration?: UIServerConfigurationSection): UIClient { |
c25a4046 | 41 | if (UIClient.instance === null) { |
39cd8fcb JB |
42 | if (uiServerConfiguration == null) { |
43 | throw new Error('Cannot initialize UIClient if no configuration is provided') | |
44 | } | |
c25a4046 | 45 | UIClient.instance = new UIClient(uiServerConfiguration) |
32de5a57 | 46 | } |
c25a4046 | 47 | return UIClient.instance |
32de5a57 LM |
48 | } |
49 | ||
a4baab63 | 50 | public setConfiguration(uiServerConfiguration: UIServerConfigurationSection): void { |
916fe456 JB |
51 | if (this.ws?.readyState === WebSocket.OPEN) { |
52 | this.ws.close() | |
53 | delete this.ws | |
54 | } | |
a4baab63 JB |
55 | this.uiServerConfiguration = uiServerConfiguration |
56 | this.openWS() | |
57 | } | |
58 | ||
ca1e5439 JB |
59 | public registerWSEventListener<K extends keyof WebSocketEventMap>( |
60 | event: K, | |
7e2e8c9b JB |
61 | listener: (event: WebSocketEventMap[K]) => void, |
62 | options?: boolean | AddEventListenerOptions | |
ca1e5439 | 63 | ) { |
916fe456 | 64 | this.ws?.addEventListener(event, listener, options) |
32de5a57 LM |
65 | } |
66 | ||
aa9b0a1e JB |
67 | public unregisterWSEventListener<K extends keyof WebSocketEventMap>( |
68 | event: K, | |
69 | listener: (event: WebSocketEventMap[K]) => void, | |
70 | options?: boolean | AddEventListenerOptions | |
71 | ) { | |
72 | this.ws?.removeEventListener(event, listener, options) | |
73 | } | |
74 | ||
240fa4da JB |
75 | public async simulatorState(): Promise<ResponsePayload> { |
76 | return this.sendRequest(ProcedureName.SIMULATOR_STATE, {}) | |
77 | } | |
78 | ||
5a010bf0 | 79 | public async startSimulator(): Promise<ResponsePayload> { |
66a7748d | 80 | return this.sendRequest(ProcedureName.START_SIMULATOR, {}) |
5a010bf0 JB |
81 | } |
82 | ||
83 | public async stopSimulator(): Promise<ResponsePayload> { | |
66a7748d | 84 | return this.sendRequest(ProcedureName.STOP_SIMULATOR, {}) |
5a010bf0 | 85 | } |
32de5a57 | 86 | |
c317ae3e JB |
87 | public async listTemplates(): Promise<ResponsePayload> { |
88 | return this.sendRequest(ProcedureName.LIST_TEMPLATES, {}) | |
a64b9a64 JB |
89 | } |
90 | ||
5a010bf0 | 91 | public async listChargingStations(): Promise<ResponsePayload> { |
66a7748d | 92 | return this.sendRequest(ProcedureName.LIST_CHARGING_STATIONS, {}) |
32de5a57 LM |
93 | } |
94 | ||
c317ae3e JB |
95 | public async addChargingStations( |
96 | template: string, | |
093ca832 JB |
97 | numberOfStations: number, |
98 | options?: ChargingStationOptions | |
c317ae3e | 99 | ): Promise<ResponsePayload> { |
093ca832 JB |
100 | return this.sendRequest(ProcedureName.ADD_CHARGING_STATIONS, { |
101 | template, | |
102 | numberOfStations, | |
103 | options | |
104 | }) | |
c317ae3e JB |
105 | } |
106 | ||
107 | public async deleteChargingStation(hashId: string): Promise<ResponsePayload> { | |
48847bc0 JB |
108 | return this.sendRequest(ProcedureName.DELETE_CHARGING_STATIONS, { |
109 | hashIds: [hashId] | |
110 | }) | |
c317ae3e JB |
111 | } |
112 | ||
f8696170 JB |
113 | public async setSupervisionUrl(hashId: string, supervisionUrl: string): Promise<ResponsePayload> { |
114 | return this.sendRequest(ProcedureName.SET_SUPERVISION_URL, { | |
115 | hashIds: [hashId], | |
116 | url: supervisionUrl | |
117 | }) | |
118 | } | |
119 | ||
8fc2e5cc | 120 | public async startChargingStation(hashId: string): Promise<ResponsePayload> { |
48847bc0 JB |
121 | return this.sendRequest(ProcedureName.START_CHARGING_STATION, { |
122 | hashIds: [hashId] | |
123 | }) | |
8fc2e5cc JB |
124 | } |
125 | ||
126 | public async stopChargingStation(hashId: string): Promise<ResponsePayload> { | |
48847bc0 JB |
127 | return this.sendRequest(ProcedureName.STOP_CHARGING_STATION, { |
128 | hashIds: [hashId] | |
129 | }) | |
8fc2e5cc JB |
130 | } |
131 | ||
132 | public async openConnection(hashId: string): Promise<ResponsePayload> { | |
133 | return this.sendRequest(ProcedureName.OPEN_CONNECTION, { | |
a974c8e4 | 134 | hashIds: [hashId] |
66a7748d | 135 | }) |
8fc2e5cc JB |
136 | } |
137 | ||
138 | public async closeConnection(hashId: string): Promise<ResponsePayload> { | |
139 | return this.sendRequest(ProcedureName.CLOSE_CONNECTION, { | |
a974c8e4 | 140 | hashIds: [hashId] |
66a7748d | 141 | }) |
8fc2e5cc JB |
142 | } |
143 | ||
32de5a57 LM |
144 | public async startTransaction( |
145 | hashId: string, | |
146 | connectorId: number, | |
66a7748d | 147 | idTag: string | undefined |
32de5a57 | 148 | ): Promise<ResponsePayload> { |
32de5a57 | 149 | return this.sendRequest(ProcedureName.START_TRANSACTION, { |
757b2ecf | 150 | hashIds: [hashId], |
32de5a57 | 151 | connectorId, |
a974c8e4 | 152 | idTag |
66a7748d | 153 | }) |
32de5a57 LM |
154 | } |
155 | ||
5a010bf0 JB |
156 | public async stopTransaction( |
157 | hashId: string, | |
66a7748d | 158 | transactionId: number | undefined |
5a010bf0 | 159 | ): Promise<ResponsePayload> { |
32de5a57 | 160 | return this.sendRequest(ProcedureName.STOP_TRANSACTION, { |
757b2ecf | 161 | hashIds: [hashId], |
a974c8e4 | 162 | transactionId |
66a7748d | 163 | }) |
32de5a57 LM |
164 | } |
165 | ||
757b2ecf JB |
166 | public async startAutomaticTransactionGenerator( |
167 | hashId: string, | |
66a7748d | 168 | connectorId: number |
757b2ecf JB |
169 | ): Promise<ResponsePayload> { |
170 | return this.sendRequest(ProcedureName.START_AUTOMATIC_TRANSACTION_GENERATOR, { | |
171 | hashIds: [hashId], | |
a974c8e4 | 172 | connectorIds: [connectorId] |
66a7748d | 173 | }) |
757b2ecf JB |
174 | } |
175 | ||
176 | public async stopAutomaticTransactionGenerator( | |
177 | hashId: string, | |
66a7748d | 178 | connectorId: number |
757b2ecf JB |
179 | ): Promise<ResponsePayload> { |
180 | return this.sendRequest(ProcedureName.STOP_AUTOMATIC_TRANSACTION_GENERATOR, { | |
181 | hashIds: [hashId], | |
a974c8e4 | 182 | connectorIds: [connectorId] |
66a7748d | 183 | }) |
757b2ecf JB |
184 | } |
185 | ||
9e1d6e03 | 186 | private openWS(): void { |
329eab0e | 187 | const protocols = |
f292861c JB |
188 | this.uiServerConfiguration.authentication?.enabled === true && |
189 | this.uiServerConfiguration.authentication?.type === AuthenticationType.PROTOCOL_BASIC_AUTH | |
329eab0e | 190 | ? [ |
f292861c | 191 | `${this.uiServerConfiguration.protocol}${this.uiServerConfiguration.version}`, |
48847bc0 JB |
192 | `authorization.basic.${btoa( |
193 | `${this.uiServerConfiguration.authentication.username}:${this.uiServerConfiguration.authentication.password}` | |
194 | ).replace(/={1,2}$/, '')}` | |
329eab0e | 195 | ] |
f292861c | 196 | : `${this.uiServerConfiguration.protocol}${this.uiServerConfiguration.version}` |
12f26d4a | 197 | this.ws = new WebSocket( |
48847bc0 JB |
198 | `${ |
199 | this.uiServerConfiguration.secure === true | |
200 | ? ApplicationProtocol.WSS | |
201 | : ApplicationProtocol.WS | |
202 | }://${this.uiServerConfiguration.host}:${this.uiServerConfiguration.port}`, | |
329eab0e | 203 | protocols |
66a7748d | 204 | ) |
0344ad2b JB |
205 | this.ws.onopen = () => { |
206 | useToast().success( | |
207 | `WebSocket to UI server '${this.uiServerConfiguration.host}' successfully opened` | |
208 | ) | |
e2372e58 | 209 | } |
66a7748d | 210 | this.ws.onmessage = this.responseHandler.bind(this) |
a974c8e4 | 211 | this.ws.onerror = errorEvent => { |
0344ad2b JB |
212 | useToast().error(`Error in WebSocket to UI server '${this.uiServerConfiguration.host}'`) |
213 | console.error( | |
214 | `Error in WebSocket to UI server '${this.uiServerConfiguration.host}'`, | |
215 | errorEvent | |
216 | ) | |
66a7748d | 217 | } |
0344ad2b | 218 | this.ws.onclose = () => { |
182f8467 | 219 | useToast().info(`WebSocket to UI server closed`) |
66a7748d | 220 | } |
5a010bf0 JB |
221 | } |
222 | ||
757b2ecf | 223 | private async sendRequest( |
8d6f4790 JB |
224 | procedureName: ProcedureName, |
225 | payload: RequestPayload | |
757b2ecf | 226 | ): Promise<ResponsePayload> { |
5b2721db | 227 | return new Promise<ResponsePayload>((resolve, reject) => { |
916fe456 | 228 | if (this.ws?.readyState === WebSocket.OPEN) { |
2610da71 | 229 | const uuid = randomUUID() |
8d6f4790 | 230 | const msg = JSON.stringify([uuid, procedureName, payload]) |
1a32c36b | 231 | const sendTimeout = setTimeout(() => { |
8d6f4790 | 232 | this.responseHandlers.delete(uuid) |
e2372e58 | 233 | return reject(new Error(`Send request '${procedureName}' message: connection timeout`)) |
9d76f5ec | 234 | }, 60000) |
1a32c36b | 235 | try { |
66a7748d | 236 | this.ws.send(msg) |
8d6f4790 | 237 | this.responseHandlers.set(uuid, { procedureName, resolve, reject }) |
1a32c36b | 238 | } catch (error) { |
8d6f4790 | 239 | this.responseHandlers.delete(uuid) |
66a7748d | 240 | reject(error) |
1a32c36b | 241 | } finally { |
66a7748d | 242 | clearTimeout(sendTimeout) |
1a32c36b | 243 | } |
1b2acf4e | 244 | } else { |
e2372e58 | 245 | reject(new Error(`Send request '${procedureName}' message: connection closed`)) |
1b2acf4e | 246 | } |
66a7748d | 247 | }) |
32de5a57 LM |
248 | } |
249 | ||
97f0a1a5 | 250 | private responseHandler(messageEvent: MessageEvent<string>): void { |
2c5c7443 JB |
251 | let response: ProtocolResponse |
252 | try { | |
9312c9d3 | 253 | response = JSON.parse(messageEvent.data) |
2c5c7443 | 254 | } catch (error) { |
22427713 JB |
255 | useToast().error('Invalid response JSON format') |
256 | console.error('Invalid response JSON format', error) | |
2c5c7443 JB |
257 | return |
258 | } | |
32de5a57 | 259 | |
2c5c7443 | 260 | if (!Array.isArray(response)) { |
276e05ae JB |
261 | useToast().error('Response not an array') |
262 | console.error('Response not an array:', response) | |
2c5c7443 | 263 | return |
32de5a57 LM |
264 | } |
265 | ||
66a7748d | 266 | const [uuid, responsePayload] = response |
32de5a57 | 267 | |
22427713 JB |
268 | if (!validateUUID(uuid)) { |
269 | useToast().error('Response UUID field is invalid') | |
270 | console.error('Response UUID field is invalid:', response) | |
271 | return | |
272 | } | |
273 | ||
2c5c7443 | 274 | if (this.responseHandlers.has(uuid)) { |
8d6f4790 | 275 | const { procedureName, resolve, reject } = this.responseHandlers.get(uuid)! |
5e3cb728 | 276 | switch (responsePayload.status) { |
32de5a57 | 277 | case ResponseStatus.SUCCESS: |
66a7748d JB |
278 | resolve(responsePayload) |
279 | break | |
32de5a57 | 280 | case ResponseStatus.FAILURE: |
66a7748d JB |
281 | reject(responsePayload) |
282 | break | |
32de5a57 | 283 | default: |
7c1d037f JB |
284 | reject( |
285 | new Error( | |
286 | `Response status for procedure '${procedureName}' not supported: '${responsePayload.status}'` | |
287 | ) | |
66a7748d | 288 | ) |
32de5a57 | 289 | } |
8d6f4790 | 290 | this.responseHandlers.delete(uuid) |
32de5a57 | 291 | } else { |
66a7748d | 292 | throw new Error(`Not a response to a request: ${JSON.stringify(response, undefined, 2)}`) |
32de5a57 LM |
293 | } |
294 | } | |
295 | } |