From: Christoph Weitkamp Date: Tue, 18 Jan 2022 08:02:38 +0000 (+0100) Subject: [androiddebugbridge] Added mDNS discovery for Fire TV Stick (#11881) X-Git-Url: https://git.basschouten.com/?a=commitdiff_plain;h=58ff2562888504d063389e274010ae8efa18b299;p=openhab-addons.git [androiddebugbridge] Added mDNS discovery for Fire TV Stick (#11881) * Added mDNS discovery for Fire TV Stick Signed-off-by: Christoph Weitkamp --- diff --git a/bundles/org.openhab.binding.androiddebugbridge/README.md b/bundles/org.openhab.binding.androiddebugbridge/README.md index e8cd54748c..1d7b85980e 100644 --- a/bundles/org.openhab.binding.androiddebugbridge/README.md +++ b/bundles/org.openhab.binding.androiddebugbridge/README.md @@ -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. diff --git a/bundles/org.openhab.binding.androiddebugbridge/src/main/feature/feature.xml b/bundles/org.openhab.binding.androiddebugbridge/src/main/feature/feature.xml index 6a2e829c12..8f0e3bdff0 100644 --- a/bundles/org.openhab.binding.androiddebugbridge/src/main/feature/feature.xml +++ b/bundles/org.openhab.binding.androiddebugbridge/src/main/feature/feature.xml @@ -4,6 +4,7 @@ openhab-runtime-base + openhab-transport-mdns mvn:org.openhab.addons.bundles/org.openhab.binding.androiddebugbridge/${project.version} diff --git a/bundles/org.openhab.binding.androiddebugbridge/src/main/java/org/openhab/binding/androiddebugbridge/internal/AndroidDebugBridgeBindingConstants.java b/bundles/org.openhab.binding.androiddebugbridge/src/main/java/org/openhab/binding/androiddebugbridge/internal/AndroidDebugBridgeBindingConstants.java index 0d5cec59e8..c831e123ac 100644 --- a/bundles/org.openhab.binding.androiddebugbridge/src/main/java/org/openhab/binding/androiddebugbridge/internal/AndroidDebugBridgeBindingConstants.java +++ b/bundles/org.openhab.binding.androiddebugbridge/src/main/java/org/openhab/binding/androiddebugbridge/internal/AndroidDebugBridgeBindingConstants.java @@ -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 SUPPORTED_THING_TYPES = Collections.singleton(THING_TYPE_ANDROID_DEVICE); + public static final Set 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"; diff --git a/bundles/org.openhab.binding.androiddebugbridge/src/main/java/org/openhab/binding/androiddebugbridge/internal/AndroidDebugBridgeDevice.java b/bundles/org.openhab.binding.androiddebugbridge/src/main/java/org/openhab/binding/androiddebugbridge/internal/AndroidDebugBridgeDevice.java index 33baa264c3..0fbba7202c 100644 --- a/bundles/org.openhab.binding.androiddebugbridge/src/main/java/org/openhab/binding/androiddebugbridge/internal/AndroidDebugBridgeDevice.java +++ b/bundles/org.openhab.binding.androiddebugbridge/src/main/java/org/openhab/binding/androiddebugbridge/internal/AndroidDebugBridgeDevice.java @@ -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 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; } } diff --git a/bundles/org.openhab.binding.androiddebugbridge/src/main/java/org/openhab/binding/androiddebugbridge/internal/AndroidDebugBridgeDeviceException.java b/bundles/org.openhab.binding.androiddebugbridge/src/main/java/org/openhab/binding/androiddebugbridge/internal/AndroidDebugBridgeDeviceException.java index 3eac1023a9..24a6900144 100644 --- a/bundles/org.openhab.binding.androiddebugbridge/src/main/java/org/openhab/binding/androiddebugbridge/internal/AndroidDebugBridgeDeviceException.java +++ b/bundles/org.openhab.binding.androiddebugbridge/src/main/java/org/openhab/binding/androiddebugbridge/internal/AndroidDebugBridgeDeviceException.java @@ -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/AndroidDebugBridgeDeviceReadException.java b/bundles/org.openhab.binding.androiddebugbridge/src/main/java/org/openhab/binding/androiddebugbridge/internal/AndroidDebugBridgeDeviceReadException.java index 7915cb6504..0ff22e25ce 100644 --- a/bundles/org.openhab.binding.androiddebugbridge/src/main/java/org/openhab/binding/androiddebugbridge/internal/AndroidDebugBridgeDeviceReadException.java +++ b/bundles/org.openhab.binding.androiddebugbridge/src/main/java/org/openhab/binding/androiddebugbridge/internal/AndroidDebugBridgeDeviceReadException.java @@ -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 index 669a7b6753..0000000000 --- a/bundles/org.openhab.binding.androiddebugbridge/src/main/java/org/openhab/binding/androiddebugbridge/internal/AndroidDebugBridgeDiscoveryService.java +++ /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 nets; - AndroidDebugBridgeBindingConfiguration configuration = getConfig(); - if (configuration == null) { - return; - } - try { - nets = NetworkInterface.getNetworkInterfaces(); - for (NetworkInterface netint : Collections.list(nets)) { - Enumeration 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 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 props = configOnline.getProperties(); - if (props != null) { - Map 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/AndroidDebugBridgeHandler.java b/bundles/org.openhab.binding.androiddebugbridge/src/main/java/org/openhab/binding/androiddebugbridge/internal/AndroidDebugBridgeHandler.java index 12199ecc76..2728f98c71 100644 --- a/bundles/org.openhab.binding.androiddebugbridge/src/main/java/org/openhab/binding/androiddebugbridge/internal/AndroidDebugBridgeHandler.java +++ b/bundles/org.openhab.binding.androiddebugbridge/src/main/java/org/openhab/binding/androiddebugbridge/internal/AndroidDebugBridgeHandler.java @@ -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 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 index 0000000000..a64975f5c5 --- /dev/null +++ b/bundles/org.openhab.binding.androiddebugbridge/src/main/java/org/openhab/binding/androiddebugbridge/internal/discovery/AndroidDebugBridgeDiscoveryService.java @@ -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 nets; + AndroidDebugBridgeBindingConfiguration configuration = getConfig(); + if (configuration == null) { + return; + } + try { + nets = NetworkInterface.getNetworkInterfaces(); + for (NetworkInterface netint : Collections.list(nets)) { + Enumeration 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 props = configOnline.getProperties(); + if (props != null) { + Map 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 index 0000000000..e14f4beb47 --- /dev/null +++ b/bundles/org.openhab.binding.androiddebugbridge/src/main/java/org/openhab/binding/androiddebugbridge/internal/discovery/FireTVStickMDNSDiscoveryParticipant.java @@ -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 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 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; + } +} diff --git a/bundles/org.openhab.binding.androiddebugbridge/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.androiddebugbridge/src/main/resources/OH-INF/thing/thing-types.xml index df8d9a26d4..94fc30c383 100644 --- a/bundles/org.openhab.binding.androiddebugbridge/src/main/resources/OH-INF/thing/thing-types.xml +++ b/bundles/org.openhab.binding.androiddebugbridge/src/main/resources/OH-INF/thing/thing-types.xml @@ -25,7 +25,7 @@ - serial + macAddress network-address