]> git.basschouten.com Git - openhab-addons.git/blob
99aca9298ff717236fbf8baed1cf90121a3e63e1
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2024 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
7  * This program and the accompanying materials are made available under the
8  * terms of the Eclipse Public License 2.0 which is available at
9  * http://www.eclipse.org/legal/epl-2.0
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.freeboxos.internal.api.rest;
14
15 import static org.openhab.binding.freeboxos.internal.FreeboxOsBindingConstants.*;
16
17 import java.io.IOException;
18 import java.net.URI;
19 import java.util.HashMap;
20 import java.util.List;
21 import java.util.Locale;
22 import java.util.Map;
23 import java.util.Optional;
24 import java.util.concurrent.ScheduledExecutorService;
25 import java.util.concurrent.ScheduledFuture;
26 import java.util.concurrent.TimeUnit;
27
28 import org.eclipse.jdt.annotation.NonNullByDefault;
29 import org.eclipse.jdt.annotation.Nullable;
30 import org.eclipse.jetty.websocket.api.Session;
31 import org.eclipse.jetty.websocket.api.StatusCode;
32 import org.eclipse.jetty.websocket.api.WebSocketListener;
33 import org.eclipse.jetty.websocket.client.ClientUpgradeRequest;
34 import org.eclipse.jetty.websocket.client.WebSocketClient;
35 import org.openhab.binding.freeboxos.internal.api.ApiHandler;
36 import org.openhab.binding.freeboxos.internal.api.FreeboxException;
37 import org.openhab.binding.freeboxos.internal.api.rest.LanBrowserManager.LanHost;
38 import org.openhab.binding.freeboxos.internal.api.rest.VmManager.VirtualMachine;
39 import org.openhab.binding.freeboxos.internal.handler.ApiConsumerHandler;
40 import org.openhab.binding.freeboxos.internal.handler.HostHandler;
41 import org.openhab.binding.freeboxos.internal.handler.VmHandler;
42 import org.openhab.core.common.ThreadPoolManager;
43 import org.openhab.core.thing.ChannelUID;
44 import org.openhab.core.types.RefreshType;
45 import org.slf4j.Logger;
46 import org.slf4j.LoggerFactory;
47
48 import com.google.gson.JsonElement;
49
50 import inet.ipaddr.mac.MACAddress;
51
52 /**
53  * The {@link WebSocketManager} is the Java class register to the websocket server and handle notifications
54  *
55  * @author GaĆ«l L'hopital - Initial contribution
56  */
57 @NonNullByDefault
58 public class WebSocketManager extends RestManager implements WebSocketListener {
59     private static final String HOST_UNREACHABLE = "lan_host_l3addr_unreachable";
60     private static final String HOST_REACHABLE = "lan_host_l3addr_reachable";
61     private static final String VM_CHANGED = "vm_state_changed";
62     private static final Register REGISTRATION = new Register(VM_CHANGED, HOST_REACHABLE, HOST_UNREACHABLE);
63     private static final Register REGISTRATION_WITHOUT_VM = new Register(HOST_REACHABLE, HOST_UNREACHABLE);
64     private static final String WS_PATH = "ws/event";
65
66     private final Logger logger = LoggerFactory.getLogger(WebSocketManager.class);
67     private final Map<MACAddress, ApiConsumerHandler> listeners = new HashMap<>();
68     private final ScheduledExecutorService scheduler = ThreadPoolManager.getScheduledPool(BINDING_ID);
69     private final ApiHandler apiHandler;
70     private final WebSocketClient client;
71     private Optional<ScheduledFuture<?>> reconnectJob = Optional.empty();
72     private volatile @Nullable Session wsSession;
73     @Nullable
74     private String sessionToken;
75     private int reconnectInterval;
76     private boolean vmSupported = true;
77     private boolean retryConnectWithoutVm = false;
78
79     private record Register(String action, List<String> events) {
80         Register(String... events) {
81             this("register", List.of(events));
82         }
83     }
84
85     public WebSocketManager(FreeboxOsSession session) throws FreeboxException {
86         super(session, LoginManager.Permission.NONE, session.getUriBuilder().path(WS_PATH));
87         this.apiHandler = session.getApiHandler();
88         this.client = new WebSocketClient(apiHandler.getHttpClient());
89     }
90
91     private enum Action {
92         REGISTER,
93         NOTIFICATION,
94         UNKNOWN
95     }
96
97     private static record WebSocketResponse(boolean success, @Nullable String msg, Action action, String event,
98             String source, @Nullable JsonElement result) {
99         public String getEvent() {
100             return source + "_" + event;
101         }
102     }
103
104     public void openSession(@Nullable String sessionToken, int reconnectInterval) {
105         this.sessionToken = sessionToken;
106         this.reconnectInterval = reconnectInterval;
107         if (reconnectInterval > 0) {
108             try {
109                 client.start();
110                 startReconnect();
111             } catch (Exception e) {
112                 logger.warn("Error starting websocket client: {}", e.getMessage());
113             }
114         }
115     }
116
117     private void startReconnect() {
118         URI uri = getUriBuilder().scheme(getUriBuilder().build().getScheme().contains("s") ? "wss" : "ws").build();
119         ClientUpgradeRequest request = new ClientUpgradeRequest();
120         request.setHeader(ApiHandler.AUTH_HEADER, sessionToken);
121         stopReconnect();
122         reconnectJob = Optional.of(scheduler.scheduleWithFixedDelay(() -> {
123             try {
124                 closeSession();
125                 client.connect(this, uri, request);
126                 // Update listeners in case we would have lost data while disconnecting / reconnecting
127                 listeners.values().forEach(host -> host
128                         .handleCommand(new ChannelUID(host.getThing().getUID(), REACHABLE), RefreshType.REFRESH));
129                 logger.debug("Websocket manager connected to {}", uri);
130             } catch (IOException e) {
131                 logger.warn("Error connecting websocket client: {}", e.getMessage());
132             }
133         }, 0, reconnectInterval, TimeUnit.MINUTES));
134     }
135
136     private void stopReconnect() {
137         reconnectJob.ifPresent(job -> job.cancel(true));
138         reconnectJob = Optional.empty();
139     }
140
141     public void dispose() {
142         stopReconnect();
143         closeSession();
144         try {
145             client.stop();
146         } catch (Exception e) {
147             logger.warn("Error stopping websocket client: {}", e.getMessage());
148         }
149     }
150
151     private void closeSession() {
152         logger.debug("Awaiting closure from remote");
153         Session localSession = wsSession;
154         if (localSession != null) {
155             localSession.close();
156             wsSession = null;
157         }
158     }
159
160     @Override
161     public void onWebSocketConnect(@NonNullByDefault({}) Session wsSession) {
162         this.wsSession = wsSession;
163         logger.debug("Websocket connection establisehd");
164         try {
165             wsSession.getRemote()
166                     .sendString(apiHandler.serialize(vmSupported ? REGISTRATION : REGISTRATION_WITHOUT_VM));
167         } catch (IOException e) {
168             logger.warn("Error registering to websocket: {}", e.getMessage());
169         }
170     }
171
172     @Override
173     public void onWebSocketText(@NonNullByDefault({}) String message) {
174         logger.debug("Websocket: received message: {}", message);
175         Session localSession = wsSession;
176         if (message.toLowerCase(Locale.US).contains("bye") && localSession != null) {
177             localSession.close(StatusCode.NORMAL, "Thanks");
178             return;
179         }
180
181         WebSocketResponse result = apiHandler.deserialize(WebSocketResponse.class, message);
182         if (result.success) {
183             switch (result.action) {
184                 case REGISTER:
185                     logger.debug("Event registration successfull");
186                     break;
187                 case NOTIFICATION:
188                     handleNotification(result);
189                     break;
190                 default:
191                     logger.warn("Unhandled notification received: {}", result.action);
192             }
193         } else if (result.action == Action.REGISTER) {
194             logger.debug("Event registration failed!");
195             if (vmSupported && "unsupported event vm_state_changed".equals(result.msg)) {
196                 vmSupported = false;
197                 retryConnectWithoutVm = true;
198             }
199         }
200     }
201
202     private void handleNotification(WebSocketResponse response) {
203         JsonElement json = response.result;
204         if (json != null) {
205             switch (response.getEvent()) {
206                 case VM_CHANGED:
207                     VirtualMachine vm = apiHandler.deserialize(VirtualMachine.class, json.toString());
208                     logger.debug("Received notification for VM {}", vm.id());
209                     ApiConsumerHandler handler = listeners.get(vm.mac());
210                     if (handler instanceof VmHandler vmHandler) {
211                         vmHandler.updateVmChannels(vm);
212                     }
213                     break;
214                 case HOST_UNREACHABLE, HOST_REACHABLE:
215                     LanHost host = apiHandler.deserialize(LanHost.class, json.toString());
216                     ApiConsumerHandler handler2 = listeners.get(host.getMac());
217                     if (handler2 instanceof HostHandler hostHandler) {
218                         logger.debug("Received notification for mac {} : thing {} is {}reachable",
219                                 host.getMac().toColonDelimitedString(), hostHandler.getThing().getUID(),
220                                 host.reachable() ? "" : "not ");
221                         hostHandler.updateConnectivityChannels(host);
222                     }
223                     break;
224                 default:
225                     logger.warn("Unhandled event received: {}", response.getEvent());
226             }
227         } else {
228             logger.warn("Empty json element in notification");
229         }
230     }
231
232     @Override
233     public void onWebSocketClose(int statusCode, @NonNullByDefault({}) String reason) {
234         logger.debug("Socket Closed: [{}] - reason {}", statusCode, reason);
235         this.wsSession = null;
236         if (retryConnectWithoutVm) {
237             logger.debug("Retry connecting websocket client without VM support");
238             retryConnectWithoutVm = false;
239             startReconnect();
240         }
241     }
242
243     @Override
244     public void onWebSocketError(@NonNullByDefault({}) Throwable cause) {
245         logger.warn("Error on websocket: {}", cause.getMessage());
246     }
247
248     @Override
249     public void onWebSocketBinary(byte @Nullable [] payload, int offset, int len) {
250         /* do nothing */
251     }
252
253     public boolean registerListener(MACAddress mac, ApiConsumerHandler hostHandler) {
254         if (wsSession != null) {
255             listeners.put(mac, hostHandler);
256             return true;
257         }
258         return false;
259     }
260
261     public void unregisterListener(MACAddress mac) {
262         listeners.remove(mac);
263     }
264 }