2 * Copyright (c) 2010-2024 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
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
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.freeboxos.internal.api.rest;
15 import static org.openhab.binding.freeboxos.internal.FreeboxOsBindingConstants.*;
17 import java.io.IOException;
19 import java.util.HashMap;
20 import java.util.List;
21 import java.util.Locale;
23 import java.util.Optional;
24 import java.util.concurrent.ScheduledExecutorService;
25 import java.util.concurrent.ScheduledFuture;
26 import java.util.concurrent.TimeUnit;
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;
48 import com.google.gson.JsonElement;
50 import inet.ipaddr.mac.MACAddress;
53 * The {@link WebSocketManager} is the Java class register to the websocket server and handle notifications
55 * @author Gaƫl L'hopital - Initial contribution
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";
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;
74 private String sessionToken;
75 private int reconnectInterval;
76 private boolean vmSupported = true;
77 private boolean retryConnectWithoutVm = false;
79 private record Register(String action, List<String> events) {
80 Register(String... events) {
81 this("register", List.of(events));
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());
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;
104 public void openSession(@Nullable String sessionToken, int reconnectInterval) {
105 this.sessionToken = sessionToken;
106 this.reconnectInterval = reconnectInterval;
107 if (reconnectInterval > 0) {
111 } catch (Exception e) {
112 logger.warn("Error starting websocket client: {}", e.getMessage());
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);
122 reconnectJob = Optional.of(scheduler.scheduleWithFixedDelay(() -> {
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());
133 }, 0, reconnectInterval, TimeUnit.MINUTES));
136 private void stopReconnect() {
137 reconnectJob.ifPresent(job -> job.cancel(true));
138 reconnectJob = Optional.empty();
141 public void dispose() {
146 } catch (Exception e) {
147 logger.warn("Error stopping websocket client: {}", e.getMessage());
151 private void closeSession() {
152 logger.debug("Awaiting closure from remote");
153 Session localSession = wsSession;
154 if (localSession != null) {
155 localSession.close();
161 public void onWebSocketConnect(@NonNullByDefault({}) Session wsSession) {
162 this.wsSession = wsSession;
163 logger.debug("Websocket connection establisehd");
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());
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");
181 WebSocketResponse result = apiHandler.deserialize(WebSocketResponse.class, message);
182 if (result.success) {
183 switch (result.action) {
185 logger.debug("Event registration successfull");
188 handleNotification(result);
191 logger.warn("Unhandled notification received: {}", result.action);
193 } else if (result.action == Action.REGISTER) {
194 logger.debug("Event registration failed!");
195 if (vmSupported && "unsupported event vm_state_changed".equals(result.msg)) {
197 retryConnectWithoutVm = true;
202 private void handleNotification(WebSocketResponse response) {
203 JsonElement json = response.result;
205 switch (response.getEvent()) {
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);
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);
225 logger.warn("Unhandled event received: {}", response.getEvent());
228 logger.warn("Empty json element in notification");
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;
244 public void onWebSocketError(@NonNullByDefault({}) Throwable cause) {
245 logger.warn("Error on websocket: {}", cause.getMessage());
249 public void onWebSocketBinary(byte @Nullable [] payload, int offset, int len) {
253 public boolean registerListener(MACAddress mac, ApiConsumerHandler hostHandler) {
254 if (wsSession != null) {
255 listeners.put(mac, hostHandler);
261 public void unregisterListener(MACAddress mac) {
262 listeners.remove(mac);