[freeboxos] Support randomized MAC addresses by using mDNS name for Wi-Fi hosts (#15299)

* Take care of randomized mac addresses by using mDNS name for wifi hosts.

Signed-off-by: clinique <gael@lhopital.org>
This commit is contained in:
Gaël L'hopital 2023-08-01 12:16:59 +02:00 committed by GitHub
parent d499f2c777
commit e792a92a38
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 182 additions and 43 deletions

View File

@ -97,15 +97,28 @@ The *landline* thing requires the following configuration parameters:
|------------------|-----------------|------------------------------------------------------------------------|----------|---------|
| Refresh Interval | refreshInterval | The refresh interval in seconds which is used to poll for phone state. | No | 2 |
### Network devices: Host and WifiHost
### Network devices: Host
The *host* and *wifihost* things requires the following configuration parameters:
The *host* thing requires the following configuration parameters:
| Parameter Label | Parameter ID | Description | Required | Default |
|------------------|-----------------|------------------------------------------------------------------------|----------|---------|
| MAC Address | macAddress | The MAC address of the network host . | Yes | |
| MAC Address | macAddress | The MAC address of the network host. | Yes | |
| Refresh Interval | refreshInterval | The refresh interval in seconds which is used to poll for phone state. | No | 30 |
### Network devices: WifiHost
The *wifihost* thing requires the following configuration parameters:
| Parameter Label | Parameter ID | Description | Required | Default |
|------------------|-----------------|------------------------------------------------------------------------|----------|---------|
| MAC Address | macAddress | The MAC address of the network host. | Yes | |
| Refresh Interval | refreshInterval | The refresh interval in seconds which is used to poll for phone state. | No | 30 |
| mDNS Name | mDNS | The mDNS name of the host. Useful in case of virtual MAC. | No | |
When used, mDNS will search the host based on its mDNS name and eventually update the MAC address accordingly.
This is useful with devices, especially Apple equipment, that uses randomly generated MAC addresses.
### Repeater and Vm thing
The *repeater* thing is a specialized case of a *wifihost*. The *vm* derives from *host*. They share the same configuration definition:

View File

@ -126,8 +126,8 @@ public class LanBrowserManager extends ListableRest<LanBrowserManager.Interface,
public static record LanHost(String id, @Nullable String primaryName, HostType hostType, boolean primaryNameManual,
L2Ident l2ident, @Nullable String vendorName, boolean persistent, boolean reachable,
@Nullable ZonedDateTime lastTimeReachable, boolean active, @Nullable ZonedDateTime lastActivity,
@Nullable ZonedDateTime firstActivity, List<HostName> names, List<L3Connectivity> l3connectivities,
@Nullable LanAccessPoint accessPoint) {
@Nullable ZonedDateTime firstActivity, @Nullable List<HostName> names,
List<L3Connectivity> l3connectivities, @Nullable LanAccessPoint accessPoint) {
public @Nullable LanAccessPoint accessPoint() {
return accessPoint;
@ -135,15 +135,20 @@ public class LanBrowserManager extends ListableRest<LanBrowserManager.Interface,
public String vendorName() {
String localVendor = vendorName;
return localVendor != null ? localVendor : "Unknown";
return localVendor == null || localVendor.isEmpty() ? "Unknown" : localVendor;
}
public Optional<String> getPrimaryName() {
return Optional.ofNullable(primaryName);
}
public Optional<String> getUPnPName() {
return names.stream().filter(name -> name.source == Source.UPNP).findFirst().map(name -> name.name);
public List<HostName> getNames() {
List<HostName> localNames = names;
return localNames != null ? localNames : List.of();
}
public Optional<String> getName(Source searchedSource) {
return getNames().stream().filter(name -> name.source == searchedSource).findFirst().map(HostName::name);
}
public MACAddress getMac() {
@ -216,6 +221,29 @@ public class LanBrowserManager extends ListableRest<LanBrowserManager.Interface,
return Optional.empty();
}
public Optional<LanHost> getHost(HostName identifier) throws FreeboxException {
List<LanHost> hosts = getHosts();
LanHost result = null;
boolean multiple = false;
for (LanHost host : hosts) {
Optional<String> sourcedName = host.getName(identifier.source);
if (sourcedName.isPresent() && sourcedName.get().equals(identifier.name)) {
// We will not return something if multiple hosts are found. This can happen in case of IP change that
// a previous name remains attached to a different host.
if (result == null) {
result = host;
} else if (!result.getMac().equals(host.getMac())) {
// Multiple hosts with different macs
multiple = true;
}
}
}
if (multiple) {
result = null;
}
return Optional.ofNullable(result);
}
public boolean wakeOnLan(MACAddress mac, String password) throws FreeboxException {
Optional<HostIntf> target = getHost(mac);
if (target.isPresent()) {

View File

@ -20,7 +20,6 @@ import inet.ipaddr.mac.MACAddress;
/**
* The {@link HostConfiguration} is responsible for holding
* configuration informations associated to a Freebox Network Device
* thing type
*
* @author Gaël L'hopital - Initial contribution
*/

View File

@ -0,0 +1,36 @@
/**
* Copyright (c) 2010-2023 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.freeboxos.internal.config;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.freeboxos.internal.api.rest.LanBrowserManager;
import org.openhab.binding.freeboxos.internal.api.rest.LanBrowserManager.HostName;
/**
* The {@link WifiHostConfiguration} holds configuration information needed to
* access/poll a wifi network device
*
* @author Gaël L'hopital - Initial contribution
*/
@NonNullByDefault
public class WifiHostConfiguration extends HostConfiguration {
private String mDNS = "";
public @Nullable HostName getIdentifier() {
if (!mDNS.isEmpty()) {
return new HostName(mDNS, LanBrowserManager.Source.MDNS);
}
return null;
}
}

View File

@ -126,8 +126,8 @@ public class FreeboxOsDiscoveryService extends AbstractDiscoveryService implemen
try {
ThingUID bridgeUID = handler.getThing().getUID();
List<LanHost> lanHosts = handler.getManager(LanBrowserManager.class).getHosts().stream()
.filter(LanHost::reachable).toList();
List<LanHost> lanHosts = new ArrayList<>(handler.getManager(LanBrowserManager.class).getHosts().stream()
.filter(LanHost::reachable).toList());
discoverServer(handler.getManager(SystemManager.class), bridgeUID);
discoverPhone(handler.getManager(PhoneManager.class), bridgeUID);

View File

@ -56,8 +56,6 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import inet.ipaddr.IPAddress;
import inet.ipaddr.MACAddressString;
import inet.ipaddr.mac.MACAddress;
/**
* The {@link ServerHandler} is a base abstract class for all devices made available by the FreeboxOs bridge
@ -70,6 +68,7 @@ abstract class ApiConsumerHandler extends BaseThingHandler implements ApiConsume
private final Map<String, ScheduledFuture<?>> jobs = new HashMap<>();
private @Nullable ServiceRegistration<?> reg;
protected boolean statusDrivenByBridge = true;
ApiConsumerHandler(Thing thing) {
super(thing);
@ -167,10 +166,12 @@ abstract class ApiConsumerHandler extends BaseThingHandler implements ApiConsume
Bridge bridge = getBridge();
if (bridge != null) {
BridgeHandler handler = bridge.getHandler();
if (handler instanceof FreeboxOsHandler) {
if (handler instanceof FreeboxOsHandler fbOsHandler) {
if (bridge.getStatus() == ThingStatus.ONLINE) {
updateStatus(ThingStatus.ONLINE);
return (FreeboxOsHandler) handler;
if (statusDrivenByBridge) {
updateStatus(ThingStatus.ONLINE);
}
return fbOsHandler;
}
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
} else {
@ -343,10 +344,4 @@ abstract class ApiConsumerHandler extends BaseThingHandler implements ApiConsume
public int getClientId() {
return ((BigDecimal) getConfig().get(ClientConfiguration.ID)).intValue();
}
@Override
public MACAddress getMac() {
String mac = (String) getConfig().get(Thing.PROPERTY_MAC_ADDRESS);
return new MACAddressString(mac).getAddress();
}
}

View File

@ -53,7 +53,7 @@ public class FreeplugHandler extends ApiConsumerHandler {
properties.put(Thing.PROPERTY_MODEL_ID, plug.model());
properties.put(ROLE, plug.netRole().name());
properties.put(NET_ID, plug.netId());
properties.put(ETHERNET_SPEED, String.format("%d Mb/s", plug.ethSpeed()));
properties.put(ETHERNET_SPEED, "%d Mb/s".formatted(plug.ethSpeed()));
properties.put(LOCAL, Boolean.valueOf(plug.local()).toString());
properties.put(FULL_DUPLEX, Boolean.valueOf(plug.ethFullDuplex()).toString());
@ -88,7 +88,7 @@ public class FreeplugHandler extends ApiConsumerHandler {
getManager(FreeplugManager.class).reboot(getMac());
logger.debug("Freeplug {} succesfully restarted", getMac());
} catch (FreeboxException e) {
logger.warn("Error restarting freeplug: {}", e.getMessage());
logger.warn("Error restarting freeplug {}: {}", getMac(), e.getMessage());
}
}

View File

@ -15,14 +15,13 @@ package org.openhab.binding.freeboxos.internal.handler;
import static org.openhab.binding.freeboxos.internal.FreeboxOsBindingConstants.*;
import java.util.Collection;
import java.util.Collections;
import java.util.Map;
import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.freeboxos.internal.action.HostActions;
import org.openhab.binding.freeboxos.internal.api.FreeboxException;
import org.openhab.binding.freeboxos.internal.api.rest.LanBrowserManager;
import org.openhab.binding.freeboxos.internal.api.rest.LanBrowserManager.HostIntf;
import org.openhab.binding.freeboxos.internal.api.rest.LanBrowserManager.LanHost;
import org.openhab.binding.freeboxos.internal.api.rest.LanBrowserManager.Source;
import org.openhab.binding.freeboxos.internal.api.rest.WebSocketManager;
@ -42,46 +41,56 @@ import org.slf4j.LoggerFactory;
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
// We start in pull mode and switch to push after a first update...
private boolean pushSubscribed = false;
public HostHandler(Thing thing) {
super(thing);
statusDrivenByBridge = false;
}
@Override
void initializeProperties(Map<String, String> properties) throws FreeboxException {
getManager(LanBrowserManager.class).getHost(getMac()).ifPresent(result -> {
LanHost host = result.host();
properties.put(Thing.PROPERTY_VENDOR, host.vendorName());
host.getUPnPName().ifPresent(upnpName -> properties.put(Source.UPNP.name(), upnpName));
});
LanHost host = getLanHost();
properties.put(Thing.PROPERTY_VENDOR, host.vendorName());
host.getName(Source.UPNP).ifPresent(upnpName -> properties.put(Source.UPNP.name(), upnpName));
}
@Override
public void dispose() {
try {
getManager(WebSocketManager.class).unregisterListener(getMac());
} catch (FreeboxException e) {
logger.warn("Error unregistering host from the websocket: {}", e.getMessage());
}
cancelPushSubscription();
super.dispose();
}
protected void cancelPushSubscription() {
if (pushSubscribed) {
try {
getManager(WebSocketManager.class).unregisterListener(getMac());
} catch (FreeboxException e) {
logger.warn("Error unregistering host from the websocket: {}", e.getMessage());
}
pushSubscribed = false;
}
}
@Override
protected void internalPoll() throws FreeboxException {
if (pushSubscribed) {
return;
}
HostIntf data = getManager(LanBrowserManager.class).getHost(getMac())
.orElseThrow(() -> new FreeboxException("Host data not found"));
updateConnectivityChannels(data.host());
LanHost host = getLanHost();
updateConnectivityChannels(host);
logger.debug("Switching to push mode - refreshInterval will now be ignored for Connectivity data");
getManager(WebSocketManager.class).registerListener(data.host().getMac(), this);
getManager(WebSocketManager.class).registerListener(host.getMac(), this);
pushSubscribed = true;
}
protected LanHost getLanHost() throws FreeboxException {
return getManager(LanBrowserManager.class).getHost(getMac()).map(hostIntf -> hostIntf.host())
.orElseThrow(() -> new FreeboxException("Host data not found"));
}
public void updateConnectivityChannels(LanHost host) {
updateChannelOnOff(CONNECTIVITY, REACHABLE, host.reachable());
updateChannelDateTimeState(CONNECTIVITY, LAST_SEEN, host.getLastSeen());
@ -100,6 +109,6 @@ public class HostHandler extends ApiConsumerHandler {
@Override
public Collection<Class<? extends ThingHandlerService>> getServices() {
return Collections.singletonList(HostActions.class);
return Set.of(HostActions.class);
}
}

View File

@ -22,13 +22,19 @@ import org.openhab.binding.freeboxos.internal.api.FreeboxException;
import org.openhab.binding.freeboxos.internal.api.rest.APManager;
import org.openhab.binding.freeboxos.internal.api.rest.APManager.LanAccessPoint;
import org.openhab.binding.freeboxos.internal.api.rest.APManager.Station;
import org.openhab.binding.freeboxos.internal.api.rest.LanBrowserManager;
import org.openhab.binding.freeboxos.internal.api.rest.LanBrowserManager.HostName;
import org.openhab.binding.freeboxos.internal.api.rest.LanBrowserManager.LanHost;
import org.openhab.binding.freeboxos.internal.api.rest.RepeaterManager;
import org.openhab.binding.freeboxos.internal.config.WifiHostConfiguration;
import org.openhab.core.config.core.Configuration;
import org.openhab.core.library.types.QuantityType;
import org.openhab.core.library.unit.Units;
import org.openhab.core.thing.Channel;
import org.openhab.core.thing.Thing;
import org.openhab.core.types.UnDefType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link WifiStationHandler} is responsible for handling everything associated to
@ -40,6 +46,8 @@ import org.openhab.core.types.UnDefType;
public class WifiStationHandler extends HostHandler {
private static final String SERVER_HOST = "Server";
private final Logger logger = LoggerFactory.getLogger(WifiStationHandler.class);
public WifiStationHandler(Thing thing) {
super(thing);
}
@ -90,4 +98,26 @@ public class WifiStationHandler extends HostHandler {
private int toQoS(int rssi) {
return rssi > -50 ? 4 : rssi > -60 ? 3 : rssi > -70 ? 2 : rssi > -85 ? 1 : 0;
}
@Override
protected LanHost getLanHost() throws FreeboxException {
try {
return super.getLanHost();
} catch (FreeboxException e) {
HostName identifier = getConfigAs(WifiHostConfiguration.class).getIdentifier();
if (identifier != null) {
cancelPushSubscription();
Optional<LanHost> lanHost = getManager(LanBrowserManager.class).getHost(identifier);
return lanHost.map(host -> {
Configuration thingConfig = editConfiguration();
thingConfig.put(Thing.PROPERTY_MAC_ADDRESS, host.getMac().toColonDelimitedString());
updateConfiguration(thingConfig);
logger.info("MAC address of the wifihost {} changed, configuration updated to {}", thing.getUID(),
host.getMac());
return host;
}).orElseThrow(() -> new FreeboxException("Host data not found - mDNS failed also"));
}
throw new FreeboxException("Host not found - no mDNS alternative");
}
}
}

View File

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8"?>
<config-description:config-descriptions
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:config-description="https://openhab.org/schemas/config-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/config-description/v1.0.0
https://openhab.org/schemas/config-description-1.0.0.xsd">
<config-description uri="thing-type:freeboxos:wifi-host">
<parameter name="refreshInterval" type="integer" min="1" unit="s">
<label>Refresh Interval</label>
<description>The refresh interval in seconds which is used to poll given device</description>
<default>30</default>
</parameter>
<parameter name="macAddress" type="text" pattern="([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})" required="true">
<label>MAC Address</label>
<description>The MAC address of the network device</description>
</parameter>
<parameter name="mDNS" type="text">
<label>mDNS Name</label>
<description>The mDNS name of the network device</description>
<advanced>true</advanced>
</parameter>
</config-description>
</config-description:config-descriptions>

View File

@ -67,6 +67,8 @@ bridge-type.config.freeboxos.api.appToken.label = Application Token
bridge-type.config.freeboxos.api.appToken.description = Token generated by the Freebox server
bridge-type.config.freeboxos.api.discoverNetDevice.label = Network Device Discovery
bridge-type.config.freeboxos.api.discoverNetDevice.description = Enable the discovery of network device things
bridge-type.config.freeboxos.api.discoveryInterval.label = Background Discovery Interval
bridge-type.config.freeboxos.api.discoveryInterval.description = Background discovery interval in minutes (default 10 - 0 disables background discovery)
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
@ -77,6 +79,8 @@ 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

View File

@ -19,7 +19,7 @@
<representation-property>macAddress</representation-property>
<config-description-ref uri="thing-type:freeboxos:host"/>
<config-description-ref uri="thing-type:freeboxos:wifi-host"/>
</thing-type>
</thing:thing-descriptions>