]> git.basschouten.com Git - openhab-addons.git/commitdiff
[miio] Add discovery from cloud feature. (#9176)
authorMarcel <marcel@verpaalen.com>
Sat, 5 Dec 2020 05:26:44 +0000 (21:26 -0800)
committerGitHub <noreply@github.com>
Sat, 5 Dec 2020 05:26:44 +0000 (21:26 -0800)
* [miio] Add discovery from cloud feature.

This allows to discover things that are not directly on the same subnet

Signed-off-by: Marcel Verpaalen <marcel@verpaalen.com>
Co-authored-by: Connor Petty <mistercpp2000+gitsignoff@gmail.com>
bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/Utils.java
bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/discovery/MiIoDiscovery.java
bundles/org.openhab.binding.miio/src/main/resources/OH-INF/binding/binding.xml
bundles/org.openhab.binding.miio/src/test/java/org/openhab/binding/miio/internal/UtilsTest.java

index 4a0e71c5eac2a80eaa317756476e2a25307805dd..bd277afa400ffd6518596ca53934261c8903f147 100644 (file)
@@ -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;
+    }
 }
index 4b279c21495b24b77bb2dfe30fde606ce2ae45f2..7715de5649ddd6145b6d0577ac952d44023ca627 100644 (file)
@@ -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<String, String> 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<String, @Nullable Object> 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<CloudDeviceDTO> 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");
         }
index cf5e2e22dcca7f1eacf299eef713e1b86746bb92..4950a52f0d4aabe6a31cae553025dfbcfcb65cc2 100644 (file)
                                binding readme for country to server mapping</description>
                        <required>false</required>
                </parameter>
+               <parameter name="cloudDiscoveryMode" type="text">
+                       <default>disabled</default>
+                       <label>Cloud Discovery Mode</label>
+                       <description>Allow for discovery via the cloud. This may be used for devices that are not on the same network as
+                               OpenHAB server</description>
+                       <options>
+                               <option value="disabled">Local discovery only (Default)</option>
+                               <option value="supportedOnly">Discover online supported devices from Xiaomi cloud</option>
+                               <option value="all">Discover all online devices from Xiaomi cloud</option>
+                       </options>
+                       <required>false</required>
+               </parameter>
        </config-description>
 
 </binding:binding>
index 6a27b64303554b20382a5a865b8a8820e6d077de..f4bd14afa8f908f1e11642b77a60e48ba2dd41f3 100644 (file)
@@ -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));
+    }
 }