[freeboxos] Add websocket connection refresh mechanism (#15543)

* 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>
pull/15720/head
Gaël L'hopital 2023-10-08 10:47:08 +02:00 committed by GitHub
parent 58d20839c1
commit bef7744c56
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 122 additions and 84 deletions

View 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.

View 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) {

View File

@ -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<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);
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);
}
}

View 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;

View 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());

View 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 {

View 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) {

View 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)
@ -42,6 +42,12 @@
<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>

View 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