Skip to content

Commit

Permalink
[freeboxos] Add websocket connection refresh mechanism (openhab#15543)
Browse files Browse the repository at this point in the history
* 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>
  • Loading branch information
clinique authored Oct 8, 2023
1 parent 58d2083 commit bef7744
Show file tree
Hide file tree
Showing 9 changed files with 119 additions and 81 deletions.
17 changes: 9 additions & 8 deletions bundles/org.openhab.binding.freeboxos/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -83,6 +84,7 @@ public void initialize(FreeboxOsConfiguration config) throws FreeboxException, I
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);
Expand All @@ -93,7 +95,7 @@ public void initialize(FreeboxOsConfiguration config) throws FreeboxException, I

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;
}
Expand All @@ -106,7 +108,7 @@ public void closeSession() {
Session currentSession = session;
if (currentSession != null) {
try {
getManager(WebSocketManager.class).closeSession();
getManager(WebSocketManager.class).dispose();
getManager(LoginManager.class).closeSession();
session = null;
} catch (FreeboxException e) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -30,8 +36,12 @@
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;

Expand All @@ -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,
Expand All @@ -82,25 +95,54 @@ public String getEvent() {
}
}

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

Expand All @@ -111,7 +153,7 @@ public void onWebSocketConnect(@NonNullByDefault({}) Session wsSession) {
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());
}
}

Expand All @@ -138,29 +180,27 @@ public void onWebSocketText(@NonNullByDefault({}) String message) {
}
}

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");
Expand All @@ -183,19 +223,15 @@ public void onWebSocketBinary(byte @Nullable [] payload, int offset, int len) {
/* 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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@
* @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<>();

Expand Down Expand Up @@ -141,12 +141,16 @@ public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {

@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());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -82,8 +82,7 @@ protected void internalPoll() throws FreeboxException {
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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@
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;
Expand All @@ -37,35 +36,19 @@
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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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>
Loading

0 comments on commit bef7744

Please sign in to comment.