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