[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
parent
58d20839c1
commit
bef7744c56
|
@ -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.
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
Loading…
Reference in New Issue