diff --git a/bundles/org.openhab.binding.freeboxos/README.md b/bundles/org.openhab.binding.freeboxos/README.md index 95712571c..21c246b58 100644 --- a/bundles/org.openhab.binding.freeboxos/README.md +++ b/bundles/org.openhab.binding.freeboxos/README.md @@ -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. diff --git a/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/api/rest/FreeboxOsSession.java b/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/api/rest/FreeboxOsSession.java index f02553357..64db7f3b4 100644 --- a/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/api/rest/FreeboxOsSession.java +++ b/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/api/rest/FreeboxOsSession.java @@ -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) { diff --git a/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/api/rest/WebSocketManager.java b/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/api/rest/WebSocketManager.java index 9607c7b6a..5e5c21577 100644 --- a/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/api/rest/WebSocketManager.java +++ b/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/api/rest/WebSocketManager.java @@ -12,12 +12,18 @@ */ 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 lanHosts = new HashMap<>(); - private final Map vms = new HashMap<>(); + private final Map listeners = new HashMap<>(); + private final ScheduledExecutorService scheduler = ThreadPoolManager.getScheduledPool(BINDING_ID); private final ApiHandler apiHandler; - + private final WebSocketClient client; + private Optional> reconnectJob = Optional.empty(); private volatile @Nullable Session wsSession; private record Register(String action, List 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); - - try { - client.start(); - client.connect(this, uri, request); - } catch (Exception e) { - throw new FreeboxException(e, "Exception connecting websocket client"); + 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()); + } } } - public void closeSession() { + private void stopReconnect() { + reconnectJob.ifPresent(job -> job.cancel(true)); + reconnectJob = Optional.empty(); + } + + public void dispose() { + stopReconnect(); + closeSession(); + try { + client.stop(); + } catch (Exception e) { + logger.warn("Error stopping websocket client: {}", e.getMessage()); + } + } + + 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); } } diff --git a/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/config/FreeboxOsConfiguration.java b/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/config/FreeboxOsConfiguration.java index 92c8a132a..885ca6209 100644 --- a/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/config/FreeboxOsConfiguration.java +++ b/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/config/FreeboxOsConfiguration.java @@ -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; diff --git a/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/handler/ApiConsumerHandler.java b/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/handler/ApiConsumerHandler.java index e50782076..40e09d372 100644 --- a/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/handler/ApiConsumerHandler.java +++ b/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/handler/ApiConsumerHandler.java @@ -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> 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()); diff --git a/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/handler/HostHandler.java b/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/handler/HostHandler.java index 45557d01b..1550a746a 100644 --- a/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/handler/HostHandler.java +++ b/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/handler/HostHandler.java @@ -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 { diff --git a/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/handler/VmHandler.java b/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/handler/VmHandler.java index 2f1ab7b06..6251f151a 100644 --- a/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/handler/VmHandler.java +++ b/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/handler/VmHandler.java @@ -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) { diff --git a/bundles/org.openhab.binding.freeboxos/src/main/resources/OH-INF/config/bridge-config.xml b/bundles/org.openhab.binding.freeboxos/src/main/resources/OH-INF/config/bridge-config.xml index 39702e799..19ead6a76 100644 --- a/bundles/org.openhab.binding.freeboxos/src/main/resources/OH-INF/config/bridge-config.xml +++ b/bundles/org.openhab.binding.freeboxos/src/main/resources/OH-INF/config/bridge-config.xml @@ -17,7 +17,7 @@ password Token generated by the Freebox server - + Background discovery interval in minutes (default 10 - 0 disables background discovery) @@ -42,6 +42,12 @@ true 15682 + + + Disconnection interval, in minutes- 0 disables websocket usage + true + 60 + diff --git a/bundles/org.openhab.binding.freeboxos/src/main/resources/OH-INF/i18n/freeboxos.properties b/bundles/org.openhab.binding.freeboxos/src/main/resources/OH-INF/i18n/freeboxos.properties index 201eb4c13..a71e59d6b 100644 --- a/bundles/org.openhab.binding.freeboxos/src/main/resources/OH-INF/i18n/freeboxos.properties +++ b/bundles/org.openhab.binding.freeboxos/src/main/resources/OH-INF/i18n/freeboxos.properties @@ -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