[androiddebugbridge] Added mDNS discovery for Fire TV Stick (#11881)

* Added mDNS discovery for Fire TV Stick

Signed-off-by: Christoph Weitkamp <github@christophweitkamp.de>
This commit is contained in:
Christoph Weitkamp
2022-01-18 09:02:38 +01:00
committed by GitHub
parent 39ee01687f
commit 58ff256288
10 changed files with 196 additions and 42 deletions

View 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>

View 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";

View 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;
}
}

View 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.

View 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.

View 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;

View File

@@ -10,7 +10,7 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.androiddebugbridge.internal;
package org.openhab.binding.androiddebugbridge.internal.discovery;
import static org.openhab.binding.androiddebugbridge.internal.AndroidDebugBridgeBindingConstants.*;
@@ -22,7 +22,6 @@ 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;
@@ -31,6 +30,10 @@ 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;
@@ -52,6 +55,7 @@ import org.slf4j.LoggerFactory;
@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";
@@ -61,7 +65,7 @@ public class AndroidDebugBridgeDiscoveryService extends AbstractDiscoveryService
private boolean discoveryRunning = false;
@Activate
public AndroidDebugBridgeDiscoveryService(@Reference ConfigurationAdmin admin) {
public AndroidDebugBridgeDiscoveryService(final @Reference ConfigurationAdmin admin) {
super(SUPPORTED_THING_TYPES, TIMEOUT_MS, false);
this.admin = admin;
}
@@ -102,7 +106,8 @@ public class AndroidDebugBridgeDiscoveryService extends AbstractDiscoveryService
int retries = 0;
while (retries < MAX_RETRIES) {
try {
discoverWithADB(currentIp, configuration.discoveryPort);
discoverWithADB(currentIp, configuration.discoveryPort,
new String(netint.getHardwareAddress()).toLowerCase());
} catch (AndroidDebugBridgeDeviceReadException | TimeoutException e) {
retries++;
if (retries < MAX_RETRIES) {
@@ -126,8 +131,9 @@ public class AndroidDebugBridgeDiscoveryService extends AbstractDiscoveryService
}
}
private void discoverWithADB(String ip, int port) throws InterruptedException, AndroidDebugBridgeDeviceException,
AndroidDebugBridgeDeviceReadException, TimeoutException, ExecutionException {
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 {
@@ -137,8 +143,8 @@ public class AndroidDebugBridgeDiscoveryService extends AbstractDiscoveryService
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);
logger.debug("discovered: {} - {} - {} - {} - {}", model, serialNo, androidVersion, brand, macAddress);
onDiscoverResult(serialNo, ip, port, model, androidVersion, brand, macAddress);
} finally {
device.disconnect();
}
@@ -152,17 +158,23 @@ public class AndroidDebugBridgeDiscoveryService extends AbstractDiscoveryService
}
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());
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() {

View File

@@ -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;
}
}

View 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>