]> git.basschouten.com Git - openhab-addons.git/commitdiff
[freeboxos] Add websocket connection refresh mechanism (#15543)
authorGaël L'hopital <gael@lhopital.org>
Sun, 8 Oct 2023 08:47:08 +0000 (10:47 +0200)
committerGitHub <noreply@github.com>
Sun, 8 Oct 2023 08:47:08 +0000 (10:47 +0200)
* Adding the possibility to disable websocket listening.
This is set up in order to ease debugging of the "Erreur Interne" issue.

* Enhancing websocket work with recurrent deconnection, simplification of listeners handling
* refactored function name
* Fixed the name of the channel where the refresh command is sent.
* Solving SAT issues
* Corrected doc error
* Added properties
* Removed gson 2.10 now that it is included into core.

---------

Signed-off-by: clinique <gael@lhopital.org>
bundles/org.openhab.binding.freeboxos/README.md
bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/api/rest/FreeboxOsSession.java
bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/api/rest/WebSocketManager.java
bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/config/FreeboxOsConfiguration.java
bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/handler/ApiConsumerHandler.java
bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/handler/HostHandler.java
bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/handler/VmHandler.java
bundles/org.openhab.binding.freeboxos/src/main/resources/OH-INF/config/bridge-config.xml
bundles/org.openhab.binding.freeboxos/src/main/resources/OH-INF/i18n/freeboxos.properties

index 95712571c5d536c462577fb40a5a0a016cccda26..21c246b580c0ec016ae3b365cee43458dd8db46b 100644 (file)
@@ -54,14 +54,15 @@ FreeboxOS binding has the following configuration parameters:
 
 ### API bridge
 
-| Parameter Label               | Parameter ID      | Description                                            | Required | Default              |
-|-------------------------------|-------------------|--------------------------------------------------------|----------|----------------------|
-| Freebox Server Address        | apiDomain         | The domain to use in place of hardcoded Freebox ip     | No       | mafreebox.freebox.fr |
-| Application Token             | appToken          | Token generated by the Freebox Server.                 | Yes      |                      |
-| Network Device Discovery      | discoverNetDevice | Enable the discovery of network device things.         | No       | false                |
-| Background Discovery Interval | discoveryInterval | Interval in minutes - 0 disables background discovery  | No       | 10                   |
-| HTTPS Available               | httpsAvailable    | Tells if https has been configured on the Freebox      | No       | false                |
-| HTTPS port                    | httpsPort         | Port to use for remote https access to the Freebox Api | No       | 15682                |
+| Parameter Label               | Parameter ID        | Description                                                    | Required | Default              |
+|-------------------------------|---------------------|----------------------------------------------------------------|----------|----------------------|
+| Freebox Server Address        | apiDomain           | The domain to use in place of hardcoded Freebox ip             | No       | mafreebox.freebox.fr |
+| Application Token             | appToken            | Token generated by the Freebox Server.                         | Yes      |                      |
+| Network Device Discovery      | discoverNetDevice   | Enable the discovery of network device things.                 | No       | false                |
+| Background Discovery Interval | discoveryInterval   | Interval in minutes - 0 disables background discovery          | No       | 10                   |
+| HTTPS Available               | httpsAvailable      | Tells if https has been configured on the Freebox              | No       | false                |
+| HTTPS port                    | httpsPort           | Port to use for remote https access to the Freebox Api         | No       | 15682                |
+| Websocket Reconnect Interval  | wsReconnectInterval | Disconnection interval, in minutes- 0 disables websocket usage | No       | 60                   |
 
 If the parameter *apiDomain* is not set, the binding will use the default address used by Free to access your Freebox Server (mafreebox.freebox.fr).
 The bridge thing will initialize only if a valid application token (parameter *appToken*) is filled.
index f02553357c8e51f35037b7c0e0f9f752336c1582..64db7f3b49a67d56863c783d9a5673f40421ad11 100644 (file)
@@ -51,6 +51,7 @@ public class FreeboxOsSession {
     private @NonNullByDefault({}) UriBuilder uriBuilder;
     private @Nullable Session session;
     private String appToken = "";
+    private int wsReconnectInterval;
 
     public enum BoxModel {
         FBXGW_R1_FULL, // Freebox Server (v6) revision 1
@@ -83,6 +84,7 @@ public class FreeboxOsSession {
         ApiVersion version = apiHandler.executeUri(config.getUriBuilder(API_VERSION_PATH).build(), HttpMethod.GET,
                 ApiVersion.class, null, null);
         this.uriBuilder = config.getUriBuilder(version.baseUrl());
+        this.wsReconnectInterval = config.wsReconnectInterval;
         getManager(LoginManager.class);
         getManager(NetShareManager.class);
         getManager(LanManager.class);
@@ -93,7 +95,7 @@ public class FreeboxOsSession {
 
     public void openSession(String appToken) throws FreeboxException {
         Session newSession = getManager(LoginManager.class).openSession(appToken);
-        getManager(WebSocketManager.class).openSession(newSession.sessionToken());
+        getManager(WebSocketManager.class).openSession(newSession.sessionToken(), wsReconnectInterval);
         session = newSession;
         this.appToken = appToken;
     }
@@ -106,7 +108,7 @@ public class FreeboxOsSession {
         Session currentSession = session;
         if (currentSession != null) {
             try {
-                getManager(WebSocketManager.class).closeSession();
+                getManager(WebSocketManager.class).dispose();
                 getManager(LoginManager.class).closeSession();
                 session = null;
             } catch (FreeboxException e) {
index 9607c7b6a46f773a2c85952a99219c979999368f..5e5c215775fcb44697b074f2d0962acd6f95f9d8 100644 (file)
  */
 package org.openhab.binding.freeboxos.internal.api.rest;
 
+import static org.openhab.binding.freeboxos.internal.FreeboxOsBindingConstants.*;
+
 import java.io.IOException;
 import java.net.URI;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Locale;
 import java.util.Map;
+import java.util.Optional;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
 
 import org.eclipse.jdt.annotation.NonNullByDefault;
 import org.eclipse.jdt.annotation.Nullable;
@@ -30,8 +36,12 @@ import org.openhab.binding.freeboxos.internal.api.ApiHandler;
 import org.openhab.binding.freeboxos.internal.api.FreeboxException;
 import org.openhab.binding.freeboxos.internal.api.rest.LanBrowserManager.LanHost;
 import org.openhab.binding.freeboxos.internal.api.rest.VmManager.VirtualMachine;
+import org.openhab.binding.freeboxos.internal.handler.ApiConsumerHandler;
 import org.openhab.binding.freeboxos.internal.handler.HostHandler;
 import org.openhab.binding.freeboxos.internal.handler.VmHandler;
+import org.openhab.core.common.ThreadPoolManager;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.types.RefreshType;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -49,30 +59,33 @@ public class WebSocketManager extends RestManager implements WebSocketListener {
     private static final String HOST_UNREACHABLE = "lan_host_l3addr_unreachable";
     private static final String HOST_REACHABLE = "lan_host_l3addr_reachable";
     private static final String VM_CHANGED = "vm_state_changed";
-    private static final Register REGISTRATION = new Register("register",
-            List.of(VM_CHANGED, HOST_REACHABLE, HOST_UNREACHABLE));
+    private static final Register REGISTRATION = new Register(VM_CHANGED, HOST_REACHABLE, HOST_UNREACHABLE);
     private static final String WS_PATH = "ws/event";
 
     private final Logger logger = LoggerFactory.getLogger(WebSocketManager.class);
-    private final Map<MACAddress, HostHandler> lanHosts = new HashMap<>();
-    private final Map<Integer, VmHandler> vms = new HashMap<>();
+    private final Map<MACAddress, ApiConsumerHandler> listeners = new HashMap<>();
+    private final ScheduledExecutorService scheduler = ThreadPoolManager.getScheduledPool(BINDING_ID);
     private final ApiHandler apiHandler;
-
+    private final WebSocketClient client;
+    private Optional<ScheduledFuture<?>> reconnectJob = Optional.empty();
     private volatile @Nullable Session wsSession;
 
     private record Register(String action, List<String> events) {
-
+        Register(String... events) {
+            this("register", List.of(events));
+        }
     }
 
     public WebSocketManager(FreeboxOsSession session) throws FreeboxException {
         super(session, LoginManager.Permission.NONE, session.getUriBuilder().path(WS_PATH));
         this.apiHandler = session.getApiHandler();
+        this.client = new WebSocketClient(apiHandler.getHttpClient());
     }
 
-    private static enum Action {
+    private enum Action {
         REGISTER,
         NOTIFICATION,
-        UNKNOWN;
+        UNKNOWN
     }
 
     private static record WebSocketResponse(boolean success, Action action, String event, String source,
@@ -82,25 +95,54 @@ public class WebSocketManager extends RestManager implements WebSocketListener {
         }
     }
 
-    public void openSession(@Nullable String sessionToken) throws FreeboxException {
-        WebSocketClient client = new WebSocketClient(apiHandler.getHttpClient());
-        URI uri = getUriBuilder().scheme(getUriBuilder().build().getScheme().contains("s") ? "wss" : "ws").build();
-        ClientUpgradeRequest request = new ClientUpgradeRequest();
-        request.setHeader(ApiHandler.AUTH_HEADER, sessionToken);
+    public void openSession(@Nullable String sessionToken, int reconnectInterval) {
+        if (reconnectInterval > 0) {
+            URI uri = getUriBuilder().scheme(getUriBuilder().build().getScheme().contains("s") ? "wss" : "ws").build();
+            ClientUpgradeRequest request = new ClientUpgradeRequest();
+            request.setHeader(ApiHandler.AUTH_HEADER, sessionToken);
+            try {
+                client.start();
+                stopReconnect();
+                reconnectJob = Optional.of(scheduler.scheduleWithFixedDelay(() -> {
+                    try {
+                        closeSession();
+                        client.connect(this, uri, request);
+                        // Update listeners in case we would have lost data while disconnecting / reconnecting
+                        listeners.values()
+                                .forEach(host -> host.handleCommand(new ChannelUID(host.getThing().getUID(), REACHABLE),
+                                        RefreshType.REFRESH));
+                        logger.debug("Websocket manager connected to {}", uri);
+                    } catch (IOException e) {
+                        logger.warn("Error connecting websocket client: {}", e.getMessage());
+                    }
+                }, 0, reconnectInterval, TimeUnit.MINUTES));
+            } catch (Exception e) {
+                logger.warn("Error starting websocket client: {}", e.getMessage());
+            }
+        }
+    }
 
+    private void stopReconnect() {
+        reconnectJob.ifPresent(job -> job.cancel(true));
+        reconnectJob = Optional.empty();
+    }
+
+    public void dispose() {
+        stopReconnect();
+        closeSession();
         try {
-            client.start();
-            client.connect(this, uri, request);
+            client.stop();
         } catch (Exception e) {
-            throw new FreeboxException(e, "Exception connecting websocket client");
+            logger.warn("Error stopping websocket client: {}", e.getMessage());
         }
     }
 
-    public void closeSession() {
+    private void closeSession() {
         logger.debug("Awaiting closure from remote");
         Session localSession = wsSession;
         if (localSession != null) {
             localSession.close();
+            wsSession = null;
         }
     }
 
@@ -111,7 +153,7 @@ public class WebSocketManager extends RestManager implements WebSocketListener {
         try {
             wsSession.getRemote().sendString(apiHandler.serialize(REGISTRATION));
         } catch (IOException e) {
-            logger.warn("Error connecting to websocket: {}", e.getMessage());
+            logger.warn("Error registering to websocket: {}", e.getMessage());
         }
     }
 
@@ -138,29 +180,27 @@ public class WebSocketManager extends RestManager implements WebSocketListener {
         }
     }
 
-    private void handleNotification(WebSocketResponse result) {
-        JsonElement json = result.result;
+    private void handleNotification(WebSocketResponse response) {
+        JsonElement json = response.result;
         if (json != null) {
-            switch (result.getEvent()) {
+            switch (response.getEvent()) {
                 case VM_CHANGED:
                     VirtualMachine vm = apiHandler.deserialize(VirtualMachine.class, json.toString());
                     logger.debug("Received notification for VM {}", vm.id());
-                    VmHandler vmHandler = vms.get(vm.id());
-                    if (vmHandler != null) {
+                    ApiConsumerHandler handler = listeners.get(vm.mac());
+                    if (handler instanceof VmHandler vmHandler) {
                         vmHandler.updateVmChannels(vm);
                     }
                     break;
                 case HOST_UNREACHABLE, HOST_REACHABLE:
                     LanHost host = apiHandler.deserialize(LanHost.class, json.toString());
-                    MACAddress mac = host.getMac();
-                    logger.debug("Received notification for LanHost {}", mac.toColonDelimitedString());
-                    HostHandler hostHandler = lanHosts.get(mac);
-                    if (hostHandler != null) {
+                    ApiConsumerHandler handler2 = listeners.get(host.getMac());
+                    if (handler2 instanceof HostHandler hostHandler) {
                         hostHandler.updateConnectivityChannels(host);
                     }
                     break;
                 default:
-                    logger.warn("Unhandled event received: {}", result.getEvent());
+                    logger.warn("Unhandled event received: {}", response.getEvent());
             }
         } else {
             logger.warn("Empty json element in notification");
@@ -183,19 +223,15 @@ public class WebSocketManager extends RestManager implements WebSocketListener {
         /* do nothing */
     }
 
-    public void registerListener(MACAddress mac, HostHandler hostHandler) {
-        lanHosts.put(mac, hostHandler);
+    public boolean registerListener(MACAddress mac, ApiConsumerHandler hostHandler) {
+        if (wsSession != null) {
+            listeners.put(mac, hostHandler);
+            return true;
+        }
+        return false;
     }
 
     public void unregisterListener(MACAddress mac) {
-        lanHosts.remove(mac);
-    }
-
-    public void registerVm(int clientId, VmHandler vmHandler) {
-        vms.put(clientId, vmHandler);
-    }
-
-    public void unregisterVm(int clientId) {
-        vms.remove(clientId);
+        listeners.remove(mac);
     }
 }
index 92c8a132a3d45685261c08dad4a2e067f34aba1e..885ca620974beccc2308b63321cb0f5ddba31734 100644 (file)
@@ -34,6 +34,7 @@ public class FreeboxOsConfiguration {
     public String appToken = "";
     public boolean discoverNetDevice;
     public int discoveryInterval = 10;
+    public int wsReconnectInterval = 60;
 
     private int httpsPort = 15682;
     private boolean httpsAvailable;
index e5078207668aa3a69784251cf94c0904efa3788c..40e09d372a08d5476ff18101383dd977ca257b78 100644 (file)
@@ -63,7 +63,7 @@ import inet.ipaddr.IPAddress;
  * @author Gaël L'hopital - Initial contribution
  */
 @NonNullByDefault
-abstract class ApiConsumerHandler extends BaseThingHandler implements ApiConsumerIntf {
+public abstract class ApiConsumerHandler extends BaseThingHandler implements ApiConsumerIntf {
     private final Logger logger = LoggerFactory.getLogger(ApiConsumerHandler.class);
     private final Map<String, ScheduledFuture<?>> jobs = new HashMap<>();
 
@@ -141,12 +141,16 @@ abstract class ApiConsumerHandler extends BaseThingHandler implements ApiConsume
 
     @Override
     public void handleCommand(ChannelUID channelUID, Command command) {
-        if (command instanceof RefreshType || getThing().getStatus() != ThingStatus.ONLINE) {
+        if (getThing().getStatus() != ThingStatus.ONLINE) {
             return;
         }
         try {
-            if (checkBridgeHandler() == null || !internalHandleCommand(channelUID.getIdWithoutGroup(), command)) {
-                logger.debug("Unexpected command {} on channel {}", command, channelUID.getId());
+            if (checkBridgeHandler() != null) {
+                if (command instanceof RefreshType) {
+                    internalPoll();
+                } else if (!internalHandleCommand(channelUID.getIdWithoutGroup(), command)) {
+                    logger.debug("Unexpected command {} on channel {}", command, channelUID.getId());
+                }
             }
         } catch (FreeboxException e) {
             logger.warn("Error handling command: {}", e.getMessage());
index 45557d01bb4b5c6d26f7e208937e1af5f6fe55b7..1550a746a957ecd315de45a2bfeec1ff62799ba4 100644 (file)
@@ -42,7 +42,7 @@ public class HostHandler extends ApiConsumerHandler {
     private final Logger logger = LoggerFactory.getLogger(HostHandler.class);
 
     // We start in pull mode and switch to push after a first update...
-    private boolean pushSubscribed = false;
+    protected boolean pushSubscribed = false;
 
     public HostHandler(Thing thing) {
         super(thing);
@@ -82,8 +82,7 @@ public class HostHandler extends ApiConsumerHandler {
         LanHost host = getLanHost();
         updateConnectivityChannels(host);
         logger.debug("Switching to push mode - refreshInterval will now be ignored for Connectivity data");
-        getManager(WebSocketManager.class).registerListener(host.getMac(), this);
-        pushSubscribed = true;
+        pushSubscribed = getManager(WebSocketManager.class).registerListener(host.getMac(), this);
     }
 
     protected LanHost getLanHost() throws FreeboxException {
index 2f1ab7b06cf621502ae293328cc3d14aa88600b2..6251f151af897cb268ecbce26d413ba8cb1fbd39 100644 (file)
@@ -19,7 +19,6 @@ import org.openhab.binding.freeboxos.internal.api.FreeboxException;
 import org.openhab.binding.freeboxos.internal.api.rest.VmManager;
 import org.openhab.binding.freeboxos.internal.api.rest.VmManager.Status;
 import org.openhab.binding.freeboxos.internal.api.rest.VmManager.VirtualMachine;
-import org.openhab.binding.freeboxos.internal.api.rest.WebSocketManager;
 import org.openhab.core.library.types.OnOffType;
 import org.openhab.core.thing.Thing;
 import org.openhab.core.thing.ThingStatus;
@@ -37,35 +36,19 @@ import org.slf4j.LoggerFactory;
 public class VmHandler extends HostHandler {
     private final Logger logger = LoggerFactory.getLogger(VmHandler.class);
 
-    // We start in pull mode and switch to push after a first update
-    private boolean pushSubscribed = false;
-
     public VmHandler(Thing thing) {
         super(thing);
     }
 
-    @Override
-    public void dispose() {
-        try {
-            getManager(WebSocketManager.class).unregisterVm(getClientId());
-        } catch (FreeboxException e) {
-            logger.warn("Error unregistering VM from the websocket: {}", e.getMessage());
-        }
-        super.dispose();
-    }
-
     @Override
     protected void internalPoll() throws FreeboxException {
-        if (pushSubscribed) {
-            return;
-        }
         super.internalPoll();
 
-        logger.debug("Polling Virtual machine status");
-        VirtualMachine vm = getManager(VmManager.class).getDevice(getClientId());
-        updateVmChannels(vm);
-        getManager(WebSocketManager.class).registerVm(vm.id(), this);
-        pushSubscribed = true;
+        if (!pushSubscribed) {
+            logger.debug("Polling Virtual machine status");
+            VirtualMachine vm = getManager(VmManager.class).getDevice(getClientId());
+            updateVmChannels(vm);
+        }
     }
 
     public void updateVmChannels(VirtualMachine vm) {
index 39702e7996e179a08110c72f55ef769c7db03d38..19ead6a765476016e6eba3c780208efabf24983e 100644 (file)
@@ -17,7 +17,7 @@
                        <context>password</context>
                        <description>Token generated by the Freebox server</description>
                </parameter>
-               <parameter name="discoveryInterval" type="integer" min="0" max="10080" required="false">
+               <parameter name="discoveryInterval" type="integer" min="0" max="10080" required="false" unit="min">
                        <label>Background Discovery Interval</label>
                        <description>
                                Background discovery interval in minutes (default 10 - 0 disables background discovery)
                        <advanced>true</advanced>
                        <default>15682</default>
                </parameter>
+               <parameter name="wsReconnectInterval" type="integer" min="0" max="1440" required="false" unit="min">
+                       <label>Websocket Reconnect Interval</label>
+                       <description>Disconnection interval, in minutes- 0 disables websocket usage</description>
+                       <advanced>true</advanced>
+                       <default>60</default>
+               </parameter>
        </config-description>
 
 </config-description:config-descriptions>
index 201eb4c1304401496b1f46408068d600cec57c92..a71e59d6b03e28c60ea43f8c02654aac4786433e 100644 (file)
@@ -73,14 +73,14 @@ bridge-type.config.freeboxos.api.httpsAvailable.label = HTTPS Available
 bridge-type.config.freeboxos.api.httpsAvailable.description = Tells if https has been configured on the Freebox
 bridge-type.config.freeboxos.api.httpsPort.label = HTTPS port
 bridge-type.config.freeboxos.api.httpsPort.description = Port to use for remote https access to the Freebox Api
+bridge-type.config.freeboxos.api.wsReconnectInterval.label = Websocket Reconnect Interval
+bridge-type.config.freeboxos.api.wsReconnectInterval.description = Disconnection interval, in minutes- 0 disables websocket usage
 thing-type.config.freeboxos.call.refreshInterval.label = State Refresh Interval
 thing-type.config.freeboxos.call.refreshInterval.description = The refresh interval in seconds which is used to poll for phone state.
 thing-type.config.freeboxos.home-node.id.label = ID
 thing-type.config.freeboxos.home-node.id.description = Id of the Home Node
 thing-type.config.freeboxos.home-node.refreshInterval.label = Refresh Interval
 thing-type.config.freeboxos.home-node.refreshInterval.description = The refresh interval in seconds which is used to poll the Node
-thing-type.config.freeboxos.host.mDNS.label = mDNS Name
-thing-type.config.freeboxos.host.mDNS.description = The mDNS name of the network device
 thing-type.config.freeboxos.host.macAddress.label = MAC Address
 thing-type.config.freeboxos.host.macAddress.description = The MAC address of the network device
 thing-type.config.freeboxos.host.refreshInterval.label = Refresh Interval
@@ -118,6 +118,12 @@ thing-type.config.freeboxos.vm.macAddress.label = MAC Address
 thing-type.config.freeboxos.vm.macAddress.description = The MAC address of the network device
 thing-type.config.freeboxos.vm.refreshInterval.label = Refresh Interval
 thing-type.config.freeboxos.vm.refreshInterval.description = The refresh interval in seconds which is used to poll given virtual machine
+thing-type.config.freeboxos.wifi-host.mDNS.label = mDNS Name
+thing-type.config.freeboxos.wifi-host.mDNS.description = The mDNS name of the network device
+thing-type.config.freeboxos.wifi-host.macAddress.label = MAC Address
+thing-type.config.freeboxos.wifi-host.macAddress.description = The MAC address of the network device
+thing-type.config.freeboxos.wifi-host.refreshInterval.label = Refresh Interval
+thing-type.config.freeboxos.wifi-host.refreshInterval.description = The refresh interval in seconds which is used to poll given device
 
 # channel group types