diff --git a/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/Utils.java b/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/Utils.java index 4a0e71c5e..bd277afa4 100644 --- a/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/Utils.java +++ b/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/Utils.java @@ -101,4 +101,22 @@ public final class Utils { public static String minLengthString(String string, int length) { return String.format("%-" + length + "s", string); } + + public static String toHEX(String value) { + try { + return String.format("%08X", Long.parseUnsignedLong(value)); + } catch (NumberFormatException e) { + // + } + return value; + } + + public static String fromHEX(String value) { + try { + return String.format("%d", Long.parseUnsignedLong(value, 16)); + } catch (NumberFormatException e) { + // + } + return value; + } } diff --git a/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/discovery/MiIoDiscovery.java b/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/discovery/MiIoDiscovery.java index 4b279c214..7715de564 100644 --- a/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/discovery/MiIoDiscovery.java +++ b/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/discovery/MiIoDiscovery.java @@ -20,14 +20,19 @@ import java.net.DatagramSocket; import java.net.InetAddress; import java.net.SocketException; import java.util.Arrays; +import java.util.Dictionary; import java.util.HashSet; +import java.util.List; +import java.util.Map; import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.miio.internal.Message; +import org.openhab.binding.miio.internal.MiIoDevices; import org.openhab.binding.miio.internal.Utils; import org.openhab.binding.miio.internal.cloud.CloudConnector; import org.openhab.binding.miio.internal.cloud.CloudDeviceDTO; @@ -37,6 +42,8 @@ import org.openhab.core.config.discovery.DiscoveryService; import org.openhab.core.net.NetUtil; import org.openhab.core.thing.ThingTypeUID; import org.openhab.core.thing.ThingUID; +import org.osgi.service.cm.Configuration; +import org.osgi.service.cm.ConfigurationAdmin; import org.osgi.service.component.annotations.Activate; import org.osgi.service.component.annotations.Component; import org.osgi.service.component.annotations.Reference; @@ -58,6 +65,9 @@ public class MiIoDiscovery extends AbstractDiscoveryService { private static final long SEARCH_INTERVAL = 600; private static final int BUFFER_LENGTH = 1024; private static final int DISCOVERY_TIME = 10; + private static final String DISABLED = "disabled"; + private static final String SUPPORTED = "supportedonly"; + private static final String ALL = "all"; private @Nullable ScheduledFuture miIoDiscoveryJob; protected @Nullable DatagramSocket clientSocket; @@ -66,11 +76,37 @@ public class MiIoDiscovery extends AbstractDiscoveryService { private final Logger logger = LoggerFactory.getLogger(MiIoDiscovery.class); private final CloudConnector cloudConnector; + private Map cloudDevices = new ConcurrentHashMap<>(); + private @Nullable Configuration miioConfig; @Activate - public MiIoDiscovery(@Reference CloudConnector cloudConnector) throws IllegalArgumentException { + public MiIoDiscovery(@Reference CloudConnector cloudConnector, @Reference ConfigurationAdmin configAdmin) + throws IllegalArgumentException { super(DISCOVERY_TIME); this.cloudConnector = cloudConnector; + try { + miioConfig = configAdmin.getConfiguration("binding.miio"); + } catch (IOException | SecurityException e) { + logger.debug("Error getting configuration: {}", e.getMessage()); + } + } + + private String getCloudDiscoveryMode() { + if (miioConfig != null) { + try { + Dictionary properties = miioConfig.getProperties(); + String cloudDiscoveryModeConfig = (String) properties.get("cloudDiscoveryMode"); + if (cloudDiscoveryModeConfig == null) { + cloudDiscoveryModeConfig = DISABLED; + } else { + cloudDiscoveryModeConfig = cloudDiscoveryModeConfig.toLowerCase(); + } + return Set.of(SUPPORTED, ALL).contains(cloudDiscoveryModeConfig) ? cloudDiscoveryModeConfig : DISABLED; + } catch (ClassCastException | SecurityException e) { + logger.debug("Error getting cloud discovery configuration: {}", e.getMessage()); + } + } + return DISABLED; } @Override @@ -80,7 +116,7 @@ public class MiIoDiscovery extends AbstractDiscoveryService { @Override protected void startBackgroundDiscovery() { - logger.debug("Start Xiaomi Mi IO background discovery"); + logger.debug("Start Xiaomi Mi IO background discovery with cloudDiscoveryMode: {}", getCloudDiscoveryMode()); final @Nullable ScheduledFuture miIoDiscoveryJob = this.miIoDiscoveryJob; if (miIoDiscoveryJob == null || miIoDiscoveryJob.isCancelled()) { this.miIoDiscoveryJob = scheduler.scheduleWithFixedDelay(this::discover, 0, SEARCH_INTERVAL, @@ -111,7 +147,11 @@ public class MiIoDiscovery extends AbstractDiscoveryService { @Override protected void startScan() { - logger.debug("Start Xiaomi Mi IO discovery"); + String cloudDiscoveryMode = getCloudDiscoveryMode(); + logger.debug("Start Xiaomi Mi IO discovery with cloudDiscoveryMode: {}", cloudDiscoveryMode); + if (!cloudDiscoveryMode.contentEquals(DISABLED)) { + cloudDiscovery(); + } final DatagramSocket clientSocket = getSocket(); if (clientSocket != null) { logger.debug("Discovery using socket on port {}", clientSocket.getLocalPort()); @@ -133,36 +173,73 @@ public class MiIoDiscovery extends AbstractDiscoveryService { } } + private void cloudDiscovery() { + String cloudDiscoveryMode = getCloudDiscoveryMode(); + cloudDevices.clear(); + if (cloudConnector.isConnected()) { + List dv = cloudConnector.getDevicesList(); + for (CloudDeviceDTO device : dv) { + String id = Utils.toHEX(device.getDid()); + if (cloudDiscoveryMode.contentEquals(SUPPORTED)) { + if (MiIoDevices.getType(device.getModel()).getThingType().equals(THING_TYPE_UNSUPPORTED)) { + logger.warn("Discovered from cloud, but ignored because not supported: {} {}", id, device); + } + } + if (device.getIsOnline()) { + logger.debug("Discovered from cloud: {} {}", id, device); + cloudDevices.put(id, device.getLocalip()); + String token = device.getToken(); + String label = device.getName() + " " + id + " (" + device.getDid() + ")"; + String country = device.getServer(); + boolean isOnline = device.getIsOnline(); + String ip = device.getLocalip(); + submitDiscovery(ip, token, id, label, country, isOnline); + } else { + logger.debug("Discovered from cloud, but ignored because not online: {} {}", id, device); + } + } + } + } + private void discovered(String ip, byte[] response) { logger.trace("Discovery responses from : {}:{}", ip, Utils.getSpacedHex(response)); Message msg = new Message(response); String token = Utils.getHex(msg.getChecksum()); String id = Utils.getHex(msg.getDeviceId()); - String label = "Xiaomi Mi Device " + id + " (" + Long.parseUnsignedLong(id, 16) + ")"; + String label = "Xiaomi Mi Device " + id + " (" + Utils.fromHEX(id) + ")"; String country = ""; boolean isOnline = false; + if (ip.equals(cloudDevices.get(id))) { + logger.debug("Skipped adding local found {}. Already discovered by cloud.", label); + return; + } if (cloudConnector.isConnected()) { cloudConnector.getDevicesList(); CloudDeviceDTO cloudInfo = cloudConnector.getDeviceInfo(id); if (cloudInfo != null) { logger.debug("Cloud Info: {}", cloudInfo); token = cloudInfo.getToken(); - label = cloudInfo.getName() + " " + id + " (" + Long.parseUnsignedLong(id, 16) + ")"; + label = cloudInfo.getName() + " " + id + " (" + Utils.fromHEX(id) + ")"; country = cloudInfo.getServer(); isOnline = cloudInfo.getIsOnline(); } } + submitDiscovery(ip, token, id, label, country, isOnline); + } + + private void submitDiscovery(String ip, String token, String id, String label, String country, boolean isOnline) { ThingUID uid = new ThingUID(THING_TYPE_MIIO, id); - logger.debug("Discovered Mi Device {} ({}) at {} as {}", id, Long.parseUnsignedLong(id, 16), ip, uid); DiscoveryResultBuilder dr = DiscoveryResultBuilder.create(uid).withProperty(PROPERTY_HOST_IP, ip) .withProperty(PROPERTY_DID, id); if (IGNORED_TOKENS.contains(token)) { + logger.debug("Discovered Mi Device {} ({}) at {} as {}", id, Utils.fromHEX(id), ip, uid); logger.debug( "No token discovered for device {}. For options how to get the token, check the binding readme.", id); dr = dr.withRepresentationProperty(PROPERTY_DID).withLabel(label); } else { - logger.debug("Discovered token for device {}: {}", id, token); + logger.debug("Discovered Mi Device {} ({}) at {} as {} with token {}", id, Utils.fromHEX(id), ip, uid, + token); dr = dr.withProperty(PROPERTY_TOKEN, token).withRepresentationProperty(PROPERTY_DID) .withLabel(label + " with token"); } diff --git a/bundles/org.openhab.binding.miio/src/main/resources/OH-INF/binding/binding.xml b/bundles/org.openhab.binding.miio/src/main/resources/OH-INF/binding/binding.xml index cf5e2e22d..4950a52f0 100644 --- a/bundles/org.openhab.binding.miio/src/main/resources/OH-INF/binding/binding.xml +++ b/bundles/org.openhab.binding.miio/src/main/resources/OH-INF/binding/binding.xml @@ -21,6 +21,18 @@ binding readme for country to server mapping false + + disabled + + Allow for discovery via the cloud. This may be used for devices that are not on the same network as + OpenHAB server + + + + + + false + diff --git a/bundles/org.openhab.binding.miio/src/test/java/org/openhab/binding/miio/internal/UtilsTest.java b/bundles/org.openhab.binding.miio/src/test/java/org/openhab/binding/miio/internal/UtilsTest.java index 6a27b6430..f4bd14afa 100644 --- a/bundles/org.openhab.binding.miio/src/test/java/org/openhab/binding/miio/internal/UtilsTest.java +++ b/bundles/org.openhab.binding.miio/src/test/java/org/openhab/binding/miio/internal/UtilsTest.java @@ -12,7 +12,7 @@ */ package org.openhab.binding.miio.internal; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertEquals; import org.eclipse.jdt.annotation.NonNullByDefault; import org.junit.jupiter.api.Test; @@ -50,4 +50,20 @@ public class UtilsTest { assertEquals("66147986XXXXXXXXXXXXXXXXda22479a6614798643fe781563c1eebeda22479a", Utils.obfuscateToken(tokenString)); } + + @Test + public void fromToDiD() { + String did = "03BD3CE5"; + assertEquals("62733541", Utils.fromHEX(did)); + + did = "0ABD3CE5"; + assertEquals("180174053", Utils.fromHEX(did)); + + did = "62733541"; + assertEquals("03BD3CE5", Utils.toHEX(did)); + + did = "cant parse"; + assertEquals("cant parse", Utils.toHEX(did)); + assertEquals("cant parse", Utils.fromHEX(did)); + } }