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