]> git.basschouten.com Git - openhab-addons.git/commitdiff
[androiddebugbridge] Added mDNS discovery for Fire TV Stick (#11881)
authorChristoph Weitkamp <github@christophweitkamp.de>
Tue, 18 Jan 2022 08:02:38 +0000 (09:02 +0100)
committerGitHub <noreply@github.com>
Tue, 18 Jan 2022 08:02:38 +0000 (09:02 +0100)
* Added mDNS discovery for Fire TV Stick

Signed-off-by: Christoph Weitkamp <github@christophweitkamp.de>
bundles/org.openhab.binding.androiddebugbridge/README.md
bundles/org.openhab.binding.androiddebugbridge/src/main/feature/feature.xml
bundles/org.openhab.binding.androiddebugbridge/src/main/java/org/openhab/binding/androiddebugbridge/internal/AndroidDebugBridgeBindingConstants.java
bundles/org.openhab.binding.androiddebugbridge/src/main/java/org/openhab/binding/androiddebugbridge/internal/AndroidDebugBridgeDevice.java
bundles/org.openhab.binding.androiddebugbridge/src/main/java/org/openhab/binding/androiddebugbridge/internal/AndroidDebugBridgeDeviceException.java
bundles/org.openhab.binding.androiddebugbridge/src/main/java/org/openhab/binding/androiddebugbridge/internal/AndroidDebugBridgeDeviceReadException.java
bundles/org.openhab.binding.androiddebugbridge/src/main/java/org/openhab/binding/androiddebugbridge/internal/AndroidDebugBridgeDiscoveryService.java [deleted file]
bundles/org.openhab.binding.androiddebugbridge/src/main/java/org/openhab/binding/androiddebugbridge/internal/AndroidDebugBridgeHandler.java
bundles/org.openhab.binding.androiddebugbridge/src/main/java/org/openhab/binding/androiddebugbridge/internal/discovery/AndroidDebugBridgeDiscoveryService.java [new file with mode: 0644]
bundles/org.openhab.binding.androiddebugbridge/src/main/java/org/openhab/binding/androiddebugbridge/internal/discovery/FireTVStickMDNSDiscoveryParticipant.java [new file with mode: 0644]
bundles/org.openhab.binding.androiddebugbridge/src/main/resources/OH-INF/thing/thing-types.xml

index e8cd54748c2e1d71f1f1aff97daf769d3344dc51..1d7b85980ec66dd44b16865dcfcd8f0c508d5049 100644 (file)
@@ -60,11 +60,13 @@ This is a sample of the mediaStateJSONConfig thing configuration:
 `[{"name": "com.amazon.tv.launcher", "mode": "idle"},{"name": "org.jellyfin.androidtv", "mode": "wake_lock", "wakeLockPlayStates": [2,3]},{"name": "com.amazon.firetv.youtube", "mode": "wake_lock", "wakeLockPlayStates": [2]}]`
 
 ## Record/Send input events
+
 As the execution of key events takes a while, you can use input events as an alternative way to control your device. 
 
 They are pretty device specific, so you should use the record-input and recorded-input channels to store/send those events.
 
 An example of what you can do:
+
 * You can send the command `UP` to the `record-input` channel the binding will then capture the events you send through your remote for the defined recordDuration config for the thing, so press once the UP key on your remote and wait a while.
 * Now that you have recorded your input, you can send the `UP` command to the `recorded-input` event and it will send the recorded event to the android device.
 
index 6a2e829c1293ad53576c96971fcfecec678a7806..8f0e3bdff0b028659a92f48088b0e3be3e90c8c0 100644 (file)
@@ -4,6 +4,7 @@
 
        <feature name="openhab-binding-androiddebugbridge" description="Android Debug Bridge Binding" version="${project.version}">
                <feature>openhab-runtime-base</feature>
+               <feature>openhab-transport-mdns</feature>
                <bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.androiddebugbridge/${project.version}</bundle>
        </feature>
 </features>
index 0d5cec59e857914a212727efa4b3bee0c545f70e..c831e123aca4cc0df63cd2c04af1fca7f0c51c25 100644 (file)
@@ -12,7 +12,6 @@
  */
 package org.openhab.binding.androiddebugbridge.internal;
 
-import java.util.Collections;
 import java.util.Set;
 
 import org.eclipse.jdt.annotation.NonNullByDefault;
@@ -32,7 +31,7 @@ public class AndroidDebugBridgeBindingConstants {
 
     // List of all Thing Type UIDs
     public static final ThingTypeUID THING_TYPE_ANDROID_DEVICE = new ThingTypeUID(BINDING_ID, "android");
-    public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Collections.singleton(THING_TYPE_ANDROID_DEVICE);
+    public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Set.of(THING_TYPE_ANDROID_DEVICE);
     // List of all Channel ids
     public static final String KEY_EVENT_CHANNEL = "key-event";
     public static final String TEXT_CHANNEL = "text";
index 33baa264c39e4d4643d71c6d9499bea326d9a159..0fbba7202cbd39be183044b8ec4b66ed0c600897 100644 (file)
@@ -25,7 +25,11 @@ import java.security.spec.InvalidKeySpecException;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Base64;
-import java.util.concurrent.*;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
 import java.util.concurrent.locks.ReentrantLock;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
@@ -88,7 +92,7 @@ public class AndroidDebugBridgeDevice {
     private @Nullable AdbConnection connection;
     private @Nullable Future<String> commandFuture;
 
-    AndroidDebugBridgeDevice(ScheduledExecutorService scheduler) {
+    public AndroidDebugBridgeDevice(ScheduledExecutorService scheduler) {
         this.scheduler = scheduler;
     }
 
@@ -260,10 +264,12 @@ public class AndroidDebugBridgeDevice {
             try {
                 return Integer.parseInt(lockResp.replace("\n", "").split("=")[1].trim());
             } catch (NumberFormatException e) {
-                logger.debug("Unable to parse device wake lock: {}", e.getMessage());
+                String message = String.format("Unable to parse device wake-lock '%s'", lockResp);
+                logger.debug("{}: {}", message, e.getMessage());
+                throw new AndroidDebugBridgeDeviceReadException(message);
             }
         }
-        throw new AndroidDebugBridgeDeviceReadException("Unable to read wake lock");
+        throw new AndroidDebugBridgeDeviceReadException(String.format("Unable to read wake-lock '%s'", lockResp));
     }
 
     private void setVolume(int stream, int volume)
@@ -291,11 +297,16 @@ public class AndroidDebugBridgeDevice {
         return getDeviceProp("ro.serialno");
     }
 
+    public String getMacAddress() throws AndroidDebugBridgeDeviceException, InterruptedException,
+            AndroidDebugBridgeDeviceReadException, TimeoutException, ExecutionException {
+        return getDeviceProp("ro.boot.wifimacaddr").toLowerCase();
+    }
+
     private String getDeviceProp(String name) throws AndroidDebugBridgeDeviceException, InterruptedException,
             AndroidDebugBridgeDeviceReadException, TimeoutException, ExecutionException {
         var propValue = runAdbShell("getprop", name, "&&", "sleep", "0.3").replace("\n", "").replace("\r", "");
         if (propValue.length() == 0) {
-            throw new AndroidDebugBridgeDeviceReadException("Unable to get device property");
+            throw new AndroidDebugBridgeDeviceReadException(String.format("Unable to get device property '%s'", name));
         }
         return propValue;
     }
@@ -382,7 +393,7 @@ public class AndroidDebugBridgeDevice {
             sock.connect(new InetSocketAddress(ip, port), (int) TimeUnit.SECONDS.toMillis(15));
         } catch (IOException e) {
             logger.debug("Error connecting to {}: [{}] {}", ip, e.getClass().getName(), e.getMessage());
-            if (e.getMessage().equals("Socket closed")) {
+            if ("Socket closed".equals(e.getMessage())) {
                 // Connection aborted by us
                 throw new InterruptedException();
             }
@@ -415,14 +426,12 @@ public class AndroidDebugBridgeDevice {
                 var byteArrayOutputStream = new ByteArrayOutputStream();
                 String cmd = String.join(" ", args);
                 logger.debug("{} - shell:{}", ip, cmd);
-                try {
-                    AdbStream stream = adb.open("shell:" + cmd);
+                try (AdbStream stream = adb.open("shell:" + cmd)) {
                     do {
                         byteArrayOutputStream.writeBytes(stream.read());
                     } while (!stream.isClosed());
                 } catch (IOException e) {
-                    String message = e.getMessage();
-                    if (message != null && !message.equals("Stream closed")) {
+                    if (!"Stream closed".equals(e.getMessage())) {
                         throw e;
                     }
                 }
index 3eac1023a992a4c2b5e689d812c85d46449dbb5e..24a690014454306094496dc12dd0d5b0f2e6ee63 100644 (file)
@@ -13,6 +13,7 @@
 package org.openhab.binding.androiddebugbridge.internal;
 
 import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.androiddebugbridge.internal.discovery.AndroidDebugBridgeDiscoveryService;
 
 /**
  * The {@link AndroidDebugBridgeDiscoveryService} discover Android ADB Instances in the network.
index 7915cb6504471c8d031b02e630e5975360c4030a..0ff22e25ce971db145b977c76970d2daa39d0633 100644 (file)
@@ -13,6 +13,7 @@
 package org.openhab.binding.androiddebugbridge.internal;
 
 import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.androiddebugbridge.internal.discovery.AndroidDebugBridgeDiscoveryService;
 
 /**
  * The {@link AndroidDebugBridgeDiscoveryService} discover Android ADB Instances in the network.
diff --git a/bundles/org.openhab.binding.androiddebugbridge/src/main/java/org/openhab/binding/androiddebugbridge/internal/AndroidDebugBridgeDiscoveryService.java b/bundles/org.openhab.binding.androiddebugbridge/src/main/java/org/openhab/binding/androiddebugbridge/internal/AndroidDebugBridgeDiscoveryService.java
deleted file mode 100644 (file)
index 669a7b6..0000000
+++ /dev/null
@@ -1,185 +0,0 @@
-/**
- * Copyright (c) 2010-2022 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.androiddebugbridge.internal;
-
-import static org.openhab.binding.androiddebugbridge.internal.AndroidDebugBridgeBindingConstants.*;
-
-import java.io.IOException;
-import java.net.Inet4Address;
-import java.net.InetAddress;
-import java.net.NetworkInterface;
-import java.net.SocketException;
-import java.util.Collections;
-import java.util.Dictionary;
-import java.util.Enumeration;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.TimeoutException;
-import java.util.function.Function;
-import java.util.stream.Collectors;
-
-import org.eclipse.jdt.annotation.NonNullByDefault;
-import org.eclipse.jdt.annotation.Nullable;
-import org.openhab.core.config.discovery.AbstractDiscoveryService;
-import org.openhab.core.config.discovery.DiscoveryResultBuilder;
-import org.openhab.core.config.discovery.DiscoveryService;
-import org.openhab.core.thing.Thing;
-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;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-/**
- * The {@link AndroidDebugBridgeDiscoveryService} discover Android ADB Instances in the network.
- *
- * @author Miguel Alvarez - Initial contribution
- */
-@NonNullByDefault
-@Component(service = DiscoveryService.class, configurationPid = "discovery.androiddebugbridge")
-public class AndroidDebugBridgeDiscoveryService extends AbstractDiscoveryService {
-    static final int TIMEOUT_MS = 60000;
-    private static final long DISCOVERY_RESULT_TTL_SEC = 300;
-    public static final String LOCAL_INTERFACE_IP = "127.0.0.1";
-    public static final int MAX_RETRIES = 2;
-    private final Logger logger = LoggerFactory.getLogger(AndroidDebugBridgeDiscoveryService.class);
-    private final ConfigurationAdmin admin;
-    private boolean discoveryRunning = false;
-
-    @Activate
-    public AndroidDebugBridgeDiscoveryService(@Reference ConfigurationAdmin admin) {
-        super(SUPPORTED_THING_TYPES, TIMEOUT_MS, false);
-        this.admin = admin;
-    }
-
-    @Override
-    protected void startScan() {
-        logger.debug("scan started: searching android devices");
-        discoveryRunning = true;
-        Enumeration<NetworkInterface> nets;
-        AndroidDebugBridgeBindingConfiguration configuration = getConfig();
-        if (configuration == null) {
-            return;
-        }
-        try {
-            nets = NetworkInterface.getNetworkInterfaces();
-            for (NetworkInterface netint : Collections.list(nets)) {
-                Enumeration<InetAddress> inetAddresses = netint.getInetAddresses();
-                for (InetAddress inetAddress : Collections.list(inetAddresses)) {
-                    if (!discoveryRunning) {
-                        break;
-                    }
-                    if (!(inetAddress instanceof Inet4Address)
-                            || inetAddress.getHostAddress().equals(LOCAL_INTERFACE_IP)) {
-                        continue;
-                    }
-                    String[] ipParts = inetAddress.getHostAddress().split("\\.");
-                    for (int i = configuration.discoveryIpRangeMin; i <= configuration.discoveryIpRangeMax; i++) {
-                        if (!discoveryRunning) {
-                            break;
-                        }
-                        ipParts[3] = Integer.toString(i);
-                        String currentIp = String.join(".", ipParts);
-                        try {
-                            var currentAddress = InetAddress.getByName(currentIp);
-                            logger.debug("address: {}", currentIp);
-                            if (currentAddress.isReachable(configuration.discoveryReachableMs)) {
-                                logger.debug("Reachable ip: {}", currentIp);
-                                int retries = 0;
-                                while (retries < MAX_RETRIES) {
-                                    try {
-                                        discoverWithADB(currentIp, configuration.discoveryPort);
-                                    } catch (AndroidDebugBridgeDeviceReadException | TimeoutException e) {
-                                        retries++;
-                                        if (retries < MAX_RETRIES) {
-                                            logger.debug("retrying - pending {}", MAX_RETRIES - retries);
-                                            continue;
-                                        }
-                                        throw e;
-                                    }
-                                    break;
-                                }
-                            }
-                        } catch (IOException | AndroidDebugBridgeDeviceException | AndroidDebugBridgeDeviceReadException
-                                | TimeoutException | ExecutionException e) {
-                            logger.debug("Error connecting to device at {}: {}", currentIp, e.getMessage());
-                        }
-                    }
-                }
-            }
-        } catch (SocketException | InterruptedException e) {
-            logger.warn("Error while discovering: {}", e.getMessage());
-        }
-    }
-
-    private void discoverWithADB(String ip, int port) throws InterruptedException, AndroidDebugBridgeDeviceException,
-            AndroidDebugBridgeDeviceReadException, TimeoutException, ExecutionException {
-        var device = new AndroidDebugBridgeDevice(scheduler);
-        device.configure(ip, port, 10, 0);
-        try {
-            device.connect();
-            logger.debug("connected adb at {}:{}", ip, port);
-            String serialNo = device.getSerialNo();
-            String model = device.getModel();
-            String androidVersion = device.getAndroidVersion();
-            String brand = device.getBrand();
-            logger.debug("discovered: {} - {} - {} - {}", model, serialNo, androidVersion, brand);
-            onDiscoverResult(serialNo, ip, port, model, androidVersion, brand);
-        } finally {
-            device.disconnect();
-        }
-    }
-
-    @Override
-    protected void stopScan() {
-        super.stopScan();
-        discoveryRunning = false;
-        logger.debug("scan stopped");
-    }
-
-    private void onDiscoverResult(String serialNo, String ip, int port, String model, String androidVersion,
-            String brand) {
-        Map<String, Object> properties = new HashMap<>();
-        properties.put(Thing.PROPERTY_SERIAL_NUMBER, serialNo);
-        properties.put(PARAMETER_IP, ip);
-        properties.put(PARAMETER_PORT, port);
-        properties.put(Thing.PROPERTY_MODEL_ID, model);
-        properties.put(Thing.PROPERTY_VENDOR, brand);
-        properties.put(Thing.PROPERTY_FIRMWARE_VERSION, androidVersion);
-        thingDiscovered(DiscoveryResultBuilder.create(new ThingUID(THING_TYPE_ANDROID_DEVICE, serialNo))
-                .withTTL(DISCOVERY_RESULT_TTL_SEC).withRepresentationProperty(Thing.PROPERTY_SERIAL_NUMBER)
-                .withProperties(properties).withLabel(String.format("%s (%s)", model, serialNo)).build());
-    }
-
-    private @Nullable AndroidDebugBridgeBindingConfiguration getConfig() {
-        try {
-            Configuration configOnline = admin.getConfiguration(BINDING_CONFIGURATION_PID, null);
-            if (configOnline != null) {
-                Dictionary<String, Object> props = configOnline.getProperties();
-                if (props != null) {
-                    Map<String, Object> propMap = Collections.list(props.keys()).stream()
-                            .collect(Collectors.toMap(Function.identity(), props::get));
-                    return new org.openhab.core.config.core.Configuration(propMap)
-                            .as(AndroidDebugBridgeBindingConfiguration.class);
-                }
-            }
-        } catch (IOException e) {
-            logger.warn("Unable to read configuration: {}", e.getMessage());
-        }
-        return null;
-    }
-}
index 12199ecc76ddf1c8ed0eb963e29329d587ca61c4..2728f98c716db3c371f03c84e552a2c52712a2fa 100644 (file)
@@ -16,6 +16,7 @@ import static org.openhab.binding.androiddebugbridge.internal.AndroidDebugBridge
 
 import java.util.Arrays;
 import java.util.List;
+import java.util.Map;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.ScheduledFuture;
 import java.util.concurrent.TimeUnit;
@@ -78,10 +79,7 @@ public class AndroidDebugBridgeHandler extends BaseThingHandler {
 
     @Override
     public void handleCommand(ChannelUID channelUID, Command command) {
-        var currentConfig = config;
-        if (currentConfig == null) {
-            return;
-        }
+        AndroidDebugBridgeConfiguration currentConfig = config;
         try {
             if (!adbConnection.isConnected()) {
                 // try reconnect
@@ -305,7 +303,7 @@ public class AndroidDebugBridgeHandler extends BaseThingHandler {
 
     @Override
     public void initialize() {
-        var currentConfig = getConfigAs(AndroidDebugBridgeConfiguration.class);
+        AndroidDebugBridgeConfiguration currentConfig = getConfigAs(AndroidDebugBridgeConfiguration.class);
         config = currentConfig;
         var mediaStateJSONConfig = currentConfig.mediaStateJSONConfig;
         if (mediaStateJSONConfig != null && !mediaStateJSONConfig.isEmpty()) {
@@ -340,14 +338,12 @@ public class AndroidDebugBridgeHandler extends BaseThingHandler {
     }
 
     public void checkConnection() {
-        var currentConfig = config;
-        if (currentConfig == null) {
-            return;
-        }
+        AndroidDebugBridgeConfiguration currentConfig = config;
         try {
             logger.debug("Refresh device {} status", currentConfig.ip);
             if (adbConnection.isConnected()) {
                 updateStatus(ThingStatus.ONLINE);
+                refreshProperties();
                 refreshStatus();
             } else {
                 try {
@@ -361,17 +357,39 @@ public class AndroidDebugBridgeHandler extends BaseThingHandler {
                 }
                 if (adbConnection.isConnected()) {
                     updateStatus(ThingStatus.ONLINE);
+                    refreshProperties();
                     refreshStatus();
                 }
             }
         } catch (InterruptedException ignored) {
-        } catch (AndroidDebugBridgeDeviceException | ExecutionException e) {
+        } catch (AndroidDebugBridgeDeviceException | AndroidDebugBridgeDeviceReadException | ExecutionException e) {
             logger.debug("Connection checker error: {}", e.getMessage());
             adbConnection.disconnect();
             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
         }
     }
 
+    private void refreshProperties() throws InterruptedException, AndroidDebugBridgeDeviceException,
+            AndroidDebugBridgeDeviceReadException, ExecutionException {
+        // Add some information about the device
+        try {
+            Map<String, String> editProperties = editProperties();
+            editProperties.put(Thing.PROPERTY_SERIAL_NUMBER, adbConnection.getSerialNo());
+            editProperties.put(Thing.PROPERTY_MODEL_ID, adbConnection.getModel());
+            editProperties.put(Thing.PROPERTY_FIRMWARE_VERSION, adbConnection.getAndroidVersion());
+            editProperties.put(Thing.PROPERTY_VENDOR, adbConnection.getBrand());
+            try {
+                editProperties.put(Thing.PROPERTY_MAC_ADDRESS, adbConnection.getMacAddress());
+            } catch (AndroidDebugBridgeDeviceReadException e) {
+                logger.debug("Refresh properties error: {}", e.getMessage());
+            }
+            updateProperties(editProperties);
+        } catch (TimeoutException e) {
+            logger.debug("Refresh properties error: Timeout");
+            return;
+        }
+    }
+
     private void refreshStatus() throws InterruptedException, AndroidDebugBridgeDeviceException, ExecutionException {
         boolean awakeState;
         boolean prevDeviceAwake = deviceAwake;
diff --git a/bundles/org.openhab.binding.androiddebugbridge/src/main/java/org/openhab/binding/androiddebugbridge/internal/discovery/AndroidDebugBridgeDiscoveryService.java b/bundles/org.openhab.binding.androiddebugbridge/src/main/java/org/openhab/binding/androiddebugbridge/internal/discovery/AndroidDebugBridgeDiscoveryService.java
new file mode 100644 (file)
index 0000000..a64975f
--- /dev/null
@@ -0,0 +1,197 @@
+/**
+ * Copyright (c) 2010-2022 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.androiddebugbridge.internal.discovery;
+
+import static org.openhab.binding.androiddebugbridge.internal.AndroidDebugBridgeBindingConstants.*;
+
+import java.io.IOException;
+import java.net.Inet4Address;
+import java.net.InetAddress;
+import java.net.NetworkInterface;
+import java.net.SocketException;
+import java.util.Collections;
+import java.util.Dictionary;
+import java.util.Enumeration;
+import java.util.Map;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeoutException;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.androiddebugbridge.internal.AndroidDebugBridgeBindingConfiguration;
+import org.openhab.binding.androiddebugbridge.internal.AndroidDebugBridgeDevice;
+import org.openhab.binding.androiddebugbridge.internal.AndroidDebugBridgeDeviceException;
+import org.openhab.binding.androiddebugbridge.internal.AndroidDebugBridgeDeviceReadException;
+import org.openhab.core.config.discovery.AbstractDiscoveryService;
+import org.openhab.core.config.discovery.DiscoveryResultBuilder;
+import org.openhab.core.config.discovery.DiscoveryService;
+import org.openhab.core.thing.Thing;
+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;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link AndroidDebugBridgeDiscoveryService} discover Android ADB Instances in the network.
+ *
+ * @author Miguel Alvarez - Initial contribution
+ */
+@NonNullByDefault
+@Component(service = DiscoveryService.class, configurationPid = "discovery.androiddebugbridge")
+public class AndroidDebugBridgeDiscoveryService extends AbstractDiscoveryService {
+
+    static final int TIMEOUT_MS = 60000;
+    private static final long DISCOVERY_RESULT_TTL_SEC = 300;
+    public static final String LOCAL_INTERFACE_IP = "127.0.0.1";
+    public static final int MAX_RETRIES = 2;
+    private final Logger logger = LoggerFactory.getLogger(AndroidDebugBridgeDiscoveryService.class);
+    private final ConfigurationAdmin admin;
+    private boolean discoveryRunning = false;
+
+    @Activate
+    public AndroidDebugBridgeDiscoveryService(final @Reference ConfigurationAdmin admin) {
+        super(SUPPORTED_THING_TYPES, TIMEOUT_MS, false);
+        this.admin = admin;
+    }
+
+    @Override
+    protected void startScan() {
+        logger.debug("scan started: searching android devices");
+        discoveryRunning = true;
+        Enumeration<NetworkInterface> nets;
+        AndroidDebugBridgeBindingConfiguration configuration = getConfig();
+        if (configuration == null) {
+            return;
+        }
+        try {
+            nets = NetworkInterface.getNetworkInterfaces();
+            for (NetworkInterface netint : Collections.list(nets)) {
+                Enumeration<InetAddress> inetAddresses = netint.getInetAddresses();
+                for (InetAddress inetAddress : Collections.list(inetAddresses)) {
+                    if (!discoveryRunning) {
+                        break;
+                    }
+                    if (!(inetAddress instanceof Inet4Address)
+                            || inetAddress.getHostAddress().equals(LOCAL_INTERFACE_IP)) {
+                        continue;
+                    }
+                    String[] ipParts = inetAddress.getHostAddress().split("\\.");
+                    for (int i = configuration.discoveryIpRangeMin; i <= configuration.discoveryIpRangeMax; i++) {
+                        if (!discoveryRunning) {
+                            break;
+                        }
+                        ipParts[3] = Integer.toString(i);
+                        String currentIp = String.join(".", ipParts);
+                        try {
+                            var currentAddress = InetAddress.getByName(currentIp);
+                            logger.debug("address: {}", currentIp);
+                            if (currentAddress.isReachable(configuration.discoveryReachableMs)) {
+                                logger.debug("Reachable ip: {}", currentIp);
+                                int retries = 0;
+                                while (retries < MAX_RETRIES) {
+                                    try {
+                                        discoverWithADB(currentIp, configuration.discoveryPort,
+                                                new String(netint.getHardwareAddress()).toLowerCase());
+                                    } catch (AndroidDebugBridgeDeviceReadException | TimeoutException e) {
+                                        retries++;
+                                        if (retries < MAX_RETRIES) {
+                                            logger.debug("retrying - pending {}", MAX_RETRIES - retries);
+                                            continue;
+                                        }
+                                        throw e;
+                                    }
+                                    break;
+                                }
+                            }
+                        } catch (IOException | AndroidDebugBridgeDeviceException | AndroidDebugBridgeDeviceReadException
+                                | TimeoutException | ExecutionException e) {
+                            logger.debug("Error connecting to device at {}: {}", currentIp, e.getMessage());
+                        }
+                    }
+                }
+            }
+        } catch (SocketException | InterruptedException e) {
+            logger.warn("Error while discovering: {}", e.getMessage());
+        }
+    }
+
+    private void discoverWithADB(String ip, int port, String macAddress)
+            throws InterruptedException, AndroidDebugBridgeDeviceException, AndroidDebugBridgeDeviceReadException,
+            TimeoutException, ExecutionException {
+        var device = new AndroidDebugBridgeDevice(scheduler);
+        device.configure(ip, port, 10, 0);
+        try {
+            device.connect();
+            logger.debug("connected adb at {}:{}", ip, port);
+            String serialNo = device.getSerialNo();
+            String model = device.getModel();
+            String androidVersion = device.getAndroidVersion();
+            String brand = device.getBrand();
+            logger.debug("discovered: {} - {} - {} - {} - {}", model, serialNo, androidVersion, brand, macAddress);
+            onDiscoverResult(serialNo, ip, port, model, androidVersion, brand, macAddress);
+        } finally {
+            device.disconnect();
+        }
+    }
+
+    @Override
+    protected void stopScan() {
+        super.stopScan();
+        discoveryRunning = false;
+        logger.debug("scan stopped");
+    }
+
+    private void onDiscoverResult(String serialNo, String ip, int port, String model, String androidVersion,
+            String brand, String macAddress) {
+        String friendlyName = String.format("%s (%s)", model, ip);
+        thingDiscovered(
+                DiscoveryResultBuilder.create(new ThingUID(THING_TYPE_ANDROID_DEVICE, macAddress.replaceAll(":", ""))) //
+                        .withProperties(Map.of( //
+                                PARAMETER_IP, ip, //
+                                PARAMETER_PORT, port, //
+                                Thing.PROPERTY_MAC_ADDRESS, macAddress, //
+                                Thing.PROPERTY_SERIAL_NUMBER, serialNo, //
+                                Thing.PROPERTY_MODEL_ID, model, //
+                                Thing.PROPERTY_VENDOR, brand, //
+                                Thing.PROPERTY_FIRMWARE_VERSION, androidVersion //
+                        )) //
+                        .withLabel(friendlyName) //
+                        .withRepresentationProperty(Thing.PROPERTY_MAC_ADDRESS) //
+                        .withTTL(DISCOVERY_RESULT_TTL_SEC) //
+                        .build());
+    }
+
+    private @Nullable AndroidDebugBridgeBindingConfiguration getConfig() {
+        try {
+            Configuration configOnline = admin.getConfiguration(BINDING_CONFIGURATION_PID, null);
+            if (configOnline != null) {
+                Dictionary<String, Object> props = configOnline.getProperties();
+                if (props != null) {
+                    Map<String, Object> propMap = Collections.list(props.keys()).stream()
+                            .collect(Collectors.toMap(Function.identity(), props::get));
+                    return new org.openhab.core.config.core.Configuration(propMap)
+                            .as(AndroidDebugBridgeBindingConfiguration.class);
+                }
+            }
+        } catch (IOException e) {
+            logger.warn("Unable to read configuration: {}", e.getMessage());
+        }
+        return null;
+    }
+}
diff --git a/bundles/org.openhab.binding.androiddebugbridge/src/main/java/org/openhab/binding/androiddebugbridge/internal/discovery/FireTVStickMDNSDiscoveryParticipant.java b/bundles/org.openhab.binding.androiddebugbridge/src/main/java/org/openhab/binding/androiddebugbridge/internal/discovery/FireTVStickMDNSDiscoveryParticipant.java
new file mode 100644 (file)
index 0000000..e14f4be
--- /dev/null
@@ -0,0 +1,111 @@
+/**
+ * Copyright (c) 2010-2022 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.androiddebugbridge.internal.discovery;
+
+import static org.openhab.binding.androiddebugbridge.internal.AndroidDebugBridgeBindingConstants.*;
+
+import java.util.Dictionary;
+import java.util.Map;
+import java.util.Set;
+
+import javax.jmdns.ServiceInfo;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.core.config.discovery.DiscoveryResult;
+import org.openhab.core.config.discovery.DiscoveryResultBuilder;
+import org.openhab.core.config.discovery.DiscoveryService;
+import org.openhab.core.config.discovery.mdns.MDNSDiscoveryParticipant;
+import org.openhab.core.config.discovery.mdns.internal.MDNSDiscoveryService;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingTypeUID;
+import org.openhab.core.thing.ThingUID;
+import org.osgi.service.component.ComponentContext;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Modified;
+
+/**
+ * The {@link FireTVStickMDNSDiscoveryParticipant} is responsible for discovering new and removed Fire TV Stick. It uses
+ * the central {@link MDNSDiscoveryService}.
+ *
+ * @author Christoph Weitkamp - Initial contribution
+ */
+@Component(configurationPid = "discovery.androiddebugbridge")
+@NonNullByDefault
+public class FireTVStickMDNSDiscoveryParticipant implements MDNSDiscoveryParticipant {
+
+    private static final String SERVICE_TYPE = "_amzn-wplay._tcp.local.";
+    private static final String MDNS_PROPERTY_NAME = "n";
+    private static final String MDNS_PROPERTY_MAC_ADDRESS = "c";
+
+    private boolean isAutoDiscoveryEnabled = true;
+
+    @Activate
+    protected void activate(ComponentContext componentContext) {
+        activateOrModifyService(componentContext);
+    }
+
+    @Modified
+    protected void modified(ComponentContext componentContext) {
+        activateOrModifyService(componentContext);
+    }
+
+    private void activateOrModifyService(ComponentContext componentContext) {
+        Dictionary<String, @Nullable Object> properties = componentContext.getProperties();
+        String autoDiscoveryPropertyValue = (String) properties
+                .get(DiscoveryService.CONFIG_PROPERTY_BACKGROUND_DISCOVERY);
+        if (autoDiscoveryPropertyValue != null && !autoDiscoveryPropertyValue.isBlank()) {
+            isAutoDiscoveryEnabled = Boolean.valueOf(autoDiscoveryPropertyValue);
+        }
+    }
+
+    @Override
+    public Set<ThingTypeUID> getSupportedThingTypeUIDs() {
+        return SUPPORTED_THING_TYPES;
+    }
+
+    @Override
+    public String getServiceType() {
+        return SERVICE_TYPE;
+    }
+
+    @Override
+    public @Nullable DiscoveryResult createResult(ServiceInfo service) {
+        if (isAutoDiscoveryEnabled) {
+            ThingUID uid = getThingUID(service);
+            if (uid != null) {
+                String ip = service.getHostAddresses()[0];
+                String macAddress = service.getPropertyString(MDNS_PROPERTY_MAC_ADDRESS);
+                String friendlyName = String.format("%s (%s)", service.getPropertyString(MDNS_PROPERTY_NAME), ip);
+                return DiscoveryResultBuilder.create(uid) //
+                        .withProperties(Map.of( //
+                                PARAMETER_IP, ip, //
+                                Thing.PROPERTY_MAC_ADDRESS, macAddress.toLowerCase())) //
+                        .withLabel(friendlyName) //
+                        .withRepresentationProperty(Thing.PROPERTY_MAC_ADDRESS) //
+                        .build();
+            }
+        }
+        return null;
+    }
+
+    @Override
+    public @Nullable ThingUID getThingUID(ServiceInfo service) {
+        String macAddress = service.getPropertyString(MDNS_PROPERTY_MAC_ADDRESS);
+        if (macAddress != null && !macAddress.isBlank()) {
+            return new ThingUID(THING_TYPE_ANDROID_DEVICE, macAddress.replaceAll(":", "").toLowerCase());
+        }
+        return null;
+    }
+}
index df8d9a26d417ac5b9d8bbac4dd05b44124ef39b5..94fc30c383a6a48aa8a22e41d79730ae6452d6d9 100644 (file)
@@ -25,7 +25,7 @@
                        <channel id="shutdown" typeId="shutdown-channel"/>
                        <channel id="awake-state" typeId="awake-state-channel"/>
                </channels>
-               <representation-property>serial</representation-property>
+               <representation-property>macAddress</representation-property>
                <config-description>
                        <parameter name="ip" type="text" required="true">
                                <context>network-address</context>