From 94d9fb7d36b0be55a478910bd15a936393dc27be Mon Sep 17 00:00:00 2001 From: GiviMAD Date: Fri, 3 Nov 2023 12:45:48 -0700 Subject: [PATCH] [androiddebugbridge] Reconnect on max timeouts and improve volume channel (#15788) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [androiddebugbridge] Reconnect on max timeouts and improve volume channel --------- Signed-off-by: Miguel Álvarez --- .../README.md | 51 ++++++++++--------- .../AndroidDebugBridgeConfiguration.java | 8 +++ .../internal/AndroidDebugBridgeDevice.java | 48 ++++++++--------- .../internal/AndroidDebugBridgeHandler.java | 42 +++++++++++++-- .../AndroidDebugBridgeHandlerFactory.java | 1 + .../OH-INF/i18n/androiddebugbridge.properties | 4 ++ .../resources/OH-INF/thing/thing-types.xml | 14 ++++- 7 files changed, 113 insertions(+), 55 deletions(-) diff --git a/bundles/org.openhab.binding.androiddebugbridge/README.md b/bundles/org.openhab.binding.androiddebugbridge/README.md index 860fcfab2..c7cb11b20 100644 --- a/bundles/org.openhab.binding.androiddebugbridge/README.md +++ b/bundles/org.openhab.binding.androiddebugbridge/README.md @@ -10,11 +10,12 @@ If you are not familiar with adb I suggest you to search "How to enable adb over This binding was tested on : -| Device | Android version | Comments | -|--------------------|-----------------|----------------------------| -| Fire TV Stick | 7.1.2 | Volume control not working | -| Nexus5x | 8.1.0 | Everything works nice | -| Freebox Pop Player | 9 | Everything works nice | +| Device | Android version | Comments | +|------------------------|-----------------|------------------------------------| +| Fire TV Stick | 7.1.2 | Volume control not working | +| Nexus5x | 8.1.0 | Everything works nice | +| Freebox Pop Player | 9 | Everything works nice | +| ChromeCast Google TV | 12 | Volume control partially working | Please update this document if you tested it with other android versions to reflect the compatibility of the binding. @@ -30,29 +31,31 @@ You could customize the discovery process through the binding options. ## Binding Configuration -| Config | Type | description | -|----------|----------|------------------------------| -| discoveryPort | int | Port used on discovery to connect to the device through adb | -| discoveryReachableMs | int | Milliseconds to wait while discovering to determine if the ip is reachable | -| discoveryIpRangeMin | int | Used to limit the number of IPs checked while discovering | -| discoveryIpRangeMax | int | Used to limit the number of IPs checked while discovering | +| Config | Type | description | +|---------------------|----------|-----------------------------------------------------------------------------------| +| discoveryPort | int | Port used on discovery to connect to the device through adb | +| discoveryReachableMs| int | Milliseconds to wait while discovering to determine if the ip is reachable | +| discoveryIpRangeMin | int | Used to limit the number of IPs checked while discovering | +| discoveryIpRangeMax | int | Used to limit the number of IPs checked while discovering | ## Thing Configuration -| ThingTypeID | description | -|----------|------------------------------| -| android | Android device | +| ThingTypeID | Description | +|---------------|-------------------------| +| android | Android device | -| Config | Type | description | -|----------|----------|------------------------------| -| ip | String | Device ip address | -| port | int | Device port listening to adb connections (default: 5555) | -| refreshTime | int | Seconds between device status refreshes (default: 30) | -| timeout | int | Command timeout in seconds (default: 5) | -| recordDuration | int | Record input duration in seconds | -| deviceMaxVolume | int | Assumed max volume for devices with android versions that do not expose this value. | -| volumeSettingKey | String | Settings key for android versions where volume is gather using settings command (>=android 11). | -| mediaStateJSONConfig | String | Expects a JSON array. Allow to configure the media state detection method per app. Described in the following section | +| Config | Type | Description | +|----------------------|--------|------------------------------------------------------------------------------------------------------------------------| +| ip | String | Device ip address. | +| port | int | Device port listening to adb connections. (default: 5555) | +| refreshTime | int | Seconds between device status refreshes. (default: 30) | +| timeout | int | Command timeout in seconds. (default: 5) | +| recordDuration | int | Record input duration in seconds. | +| deviceMaxVolume | int | Assumed max volume for devices with android versions that do not expose this value. | +| volumeSettingKey | String | Settings key for android versions where volume is gather using settings command. (>=android 11) | +| volumeStepPercent | int | Percent to increase/decrease volume. | +| mediaStateJSONConfig | String | Expects a JSON array. Allow to configure the media state detection method per app. Described in the following section. | +| maxADBTimeouts | int | Max ADB command consecutive timeouts to force to reset the connection. | ## Media State Detection diff --git a/bundles/org.openhab.binding.androiddebugbridge/src/main/java/org/openhab/binding/androiddebugbridge/internal/AndroidDebugBridgeConfiguration.java b/bundles/org.openhab.binding.androiddebugbridge/src/main/java/org/openhab/binding/androiddebugbridge/internal/AndroidDebugBridgeConfiguration.java index acd8a7760..9e64d7a6d 100644 --- a/bundles/org.openhab.binding.androiddebugbridge/src/main/java/org/openhab/binding/androiddebugbridge/internal/AndroidDebugBridgeConfiguration.java +++ b/bundles/org.openhab.binding.androiddebugbridge/src/main/java/org/openhab/binding/androiddebugbridge/internal/AndroidDebugBridgeConfiguration.java @@ -42,10 +42,18 @@ public class AndroidDebugBridgeConfiguration { * Record input duration in seconds. */ public int recordDuration = 5; + /** + * Percent to increase/decrease volume. + */ + public int volumeStepPercent = 15; /** * Assumed max volume for devices with android versions that do not expose this value (>=android 11). */ public int deviceMaxVolume = 25; + /** + * Max ADB command consecutive timeouts to force to reset the connection. (0 for disabled) + */ + public int maxADBTimeouts; /** * Settings key for android versions where volume is gather using settings command (>=android 11). */ 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 8765072b5..c00c93b4c 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 @@ -13,14 +13,14 @@ package org.openhab.binding.androiddebugbridge.internal; import java.io.ByteArrayOutputStream; -import java.io.File; import java.io.IOException; import java.net.InetSocketAddress; import java.net.Socket; import java.net.URI; import java.net.URLEncoder; -import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; import java.security.NoSuchAlgorithmException; import java.security.spec.InvalidKeySpecException; import java.util.ArrayList; @@ -56,8 +56,8 @@ import com.tananaev.adblib.AdbStream; */ @NonNullByDefault public class AndroidDebugBridgeDevice { + private static final Path ADB_FOLDER = Path.of(OpenHAB.getUserDataFolder(), ".adb"); public static final int ANDROID_MEDIA_STREAM = 3; - private static final String ADB_FOLDER = OpenHAB.getUserDataFolder() + File.separator + ".adb"; private final Logger logger = LoggerFactory.getLogger(AndroidDebugBridgeDevice.class); private static final Pattern VOLUME_PATTERN = Pattern .compile("volume is (?\\d.*) in range \\[(?\\d.*)\\.\\.(?\\d.*)]"); @@ -76,20 +76,6 @@ public class AndroidDebugBridgeDevice { private static @Nullable AdbCrypto adbCrypto; - static { - var logger = LoggerFactory.getLogger(AndroidDebugBridgeDevice.class); - try { - File directory = new File(ADB_FOLDER); - if (!directory.exists()) { - directory.mkdir(); - } - adbCrypto = loadKeyPair(ADB_FOLDER + File.separator + "adb_pub.key", - ADB_FOLDER + File.separator + "adb.key"); - } catch (NoSuchAlgorithmException | IOException | InvalidKeySpecException e) { - logger.warn("Unable to setup adb keys: {}", e.getMessage()); - } - } - private final ScheduledExecutorService scheduler; private final ReentrantLock commandLock = new ReentrantLock(); @@ -793,20 +779,30 @@ public class AndroidDebugBridgeDevice { } } - private static AdbBase64 getBase64Impl() { - Charset asciiCharset = Charset.forName("ASCII"); - return bytes -> new String(Base64.getEncoder().encode(bytes), asciiCharset); + public static void initADB() { + Logger logger = LoggerFactory.getLogger(AndroidDebugBridgeDevice.class); + try { + if (!Files.exists(ADB_FOLDER) || !Files.isDirectory(ADB_FOLDER)) { + Files.createDirectory(ADB_FOLDER); + logger.info("Binding folder {} created", ADB_FOLDER); + } + adbCrypto = loadKeyPair(ADB_FOLDER.resolve("adb_pub.key"), ADB_FOLDER.resolve("adb.key")); + } catch (NoSuchAlgorithmException | IOException | InvalidKeySpecException e) { + logger.warn("Unable to setup adb keys: {}", e.getMessage()); + } } - private static AdbCrypto loadKeyPair(String pubKeyFile, String privKeyFile) + private static AdbBase64 getBase64Impl() { + return bytes -> new String(Base64.getEncoder().encode(bytes), StandardCharsets.US_ASCII); + } + + private static AdbCrypto loadKeyPair(Path pubKey, Path privKey) throws NoSuchAlgorithmException, IOException, InvalidKeySpecException { - File pub = new File(pubKeyFile); - File priv = new File(privKeyFile); AdbCrypto c = null; // load key pair - if (pub.exists() && priv.exists()) { + if (Files.exists(pubKey) && Files.exists(privKey)) { try { - c = AdbCrypto.loadAdbKeyPair(getBase64Impl(), priv, pub); + c = AdbCrypto.loadAdbKeyPair(getBase64Impl(), privKey.toFile(), pubKey.toFile()); } catch (IOException ignored) { // Keys don't exits } @@ -814,7 +810,7 @@ public class AndroidDebugBridgeDevice { if (c == null) { // generate key pair c = AdbCrypto.generateAdbKeyPair(getBase64Impl()); - c.saveAdbKeyPair(priv, pub); + c.saveAdbKeyPair(privKey.toFile(), pubKey.toFile()); } return c; } 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 69ac4cb5a..ceec6ca78 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 @@ -27,6 +27,7 @@ import java.util.stream.Collectors; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.openhab.core.library.types.DecimalType; +import org.openhab.core.library.types.IncreaseDecreaseType; import org.openhab.core.library.types.NextPreviousType; import org.openhab.core.library.types.OnOffType; import org.openhab.core.library.types.PercentType; @@ -74,6 +75,7 @@ public class AndroidDebugBridgeHandler extends BaseThingHandler { private @Nullable ScheduledFuture connectionCheckerSchedule; private AndroidDebugBridgeMediaStatePackageConfig @Nullable [] packageConfigs = null; private boolean deviceAwake = false; + private int consecutiveTimeouts = 0; public AndroidDebugBridgeHandler(Thing thing, AndroidDebugBridgeDynamicCommandDescriptionProvider commandDescriptionProvider) { @@ -101,6 +103,7 @@ public class AndroidDebugBridgeHandler extends BaseThingHandler { logger.warn("{} - read error: {}", currentConfig.ip, e.getMessage()); } catch (TimeoutException e) { logger.warn("{} - timeout error", currentConfig.ip); + disconnectOnMaxADBTimeouts(); } } @@ -196,6 +199,7 @@ public class AndroidDebugBridgeHandler extends BaseThingHandler { } break; } + consecutiveTimeouts = 0; } private void recordDeviceInput(Command recordNameCommand) @@ -236,6 +240,16 @@ public class AndroidDebugBridgeHandler extends BaseThingHandler { var volumeInfo = adbConnection.getMediaVolume(); maxMediaVolume = volumeInfo.max; updateState(channelUID, new PercentType((int) Math.round(toPercent(volumeInfo.current, volumeInfo.max)))); + } else if (command instanceof IncreaseDecreaseType) { + var volumeInfo = adbConnection.getMediaVolume(); + var volumeStep = fromPercent(config.volumeStepPercent, volumeInfo.max); + logger.debug("Device {} volume step: {}", getThing().getUID(), volumeStep); + var targetVolume = (int) Math + .round(IncreaseDecreaseType.INCREASE.equals(command) ? volumeInfo.current + volumeStep + : volumeInfo.current - volumeStep); + var newVolume = Integer.max(0, Integer.min(targetVolume, volumeInfo.max)); + logger.debug("Device {} new volume : {}", getThing().getUID(), newVolume); + adbConnection.setMediaVolume(newVolume); } else { if (maxMediaVolume == 0) { return; // We can not transform percentage @@ -250,8 +264,8 @@ public class AndroidDebugBridgeHandler extends BaseThingHandler { return (value / maxValue) * 100; } - private double fromPercent(double value, double maxValue) { - return (value / 100) * maxValue; + private double fromPercent(double percent, double maxValue) { + return (percent / 100) * maxValue; } private void handleMediaControlCommand(ChannelUID channelUID, Command command) @@ -398,8 +412,16 @@ public class AndroidDebugBridgeHandler extends BaseThingHandler { // 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()); + try { + editProperties.put(Thing.PROPERTY_SERIAL_NUMBER, adbConnection.getSerialNo()); + } catch (AndroidDebugBridgeDeviceReadException ignored) { + // Allow devices without serial number. + } + try { + editProperties.put(Thing.PROPERTY_MODEL_ID, adbConnection.getModel()); + } catch (AndroidDebugBridgeDeviceReadException ignored) { + // Allow devices without model id. + } var androidVersion = adbConnection.getAndroidVersion(); editProperties.put(Thing.PROPERTY_FIRMWARE_VERSION, androidVersion); // refresh android version to use @@ -426,8 +448,10 @@ public class AndroidDebugBridgeHandler extends BaseThingHandler { } catch (TimeoutException e) { // happen a lot when device is sleeping; abort refresh other channels logger.debug("Unable to refresh awake state: Timeout; aborting channels refresh"); + disconnectOnMaxADBTimeouts(); return; } + consecutiveTimeouts = 0; var awakeStateChannelUID = new ChannelUID(this.thing.getUID(), AWAKE_STATE_CHANNEL); if (isLinked(awakeStateChannelUID)) { updateState(awakeStateChannelUID, OnOffType.from(awakeState)); @@ -474,6 +498,16 @@ public class AndroidDebugBridgeHandler extends BaseThingHandler { } } + private void disconnectOnMaxADBTimeouts() { + consecutiveTimeouts++; + if (config.maxADBTimeouts > 0 && consecutiveTimeouts >= config.maxADBTimeouts) { + logger.debug("Max consecutive timeouts reached, aborting connection"); + adbConnection.disconnect(); + checkConnection(); + consecutiveTimeouts = 0; + } + } + static class AndroidDebugBridgeMediaStatePackageConfig { public String name = ""; public @Nullable String label; diff --git a/bundles/org.openhab.binding.androiddebugbridge/src/main/java/org/openhab/binding/androiddebugbridge/internal/AndroidDebugBridgeHandlerFactory.java b/bundles/org.openhab.binding.androiddebugbridge/src/main/java/org/openhab/binding/androiddebugbridge/internal/AndroidDebugBridgeHandlerFactory.java index 2dbe5e5bf..bc2c02b1c 100644 --- a/bundles/org.openhab.binding.androiddebugbridge/src/main/java/org/openhab/binding/androiddebugbridge/internal/AndroidDebugBridgeHandlerFactory.java +++ b/bundles/org.openhab.binding.androiddebugbridge/src/main/java/org/openhab/binding/androiddebugbridge/internal/AndroidDebugBridgeHandlerFactory.java @@ -41,6 +41,7 @@ public class AndroidDebugBridgeHandlerFactory extends BaseThingHandlerFactory { public AndroidDebugBridgeHandlerFactory( final @Reference AndroidDebugBridgeDynamicCommandDescriptionProvider commandDescriptionProvider) { this.commandDescriptionProvider = commandDescriptionProvider; + AndroidDebugBridgeDevice.initADB(); } @Override diff --git a/bundles/org.openhab.binding.androiddebugbridge/src/main/resources/OH-INF/i18n/androiddebugbridge.properties b/bundles/org.openhab.binding.androiddebugbridge/src/main/resources/OH-INF/i18n/androiddebugbridge.properties index 4d5386d08..49b922c42 100644 --- a/bundles/org.openhab.binding.androiddebugbridge/src/main/resources/OH-INF/i18n/androiddebugbridge.properties +++ b/bundles/org.openhab.binding.androiddebugbridge/src/main/resources/OH-INF/i18n/androiddebugbridge.properties @@ -25,6 +25,8 @@ thing-type.config.androiddebugbridge.android.deviceMaxVolume.label = Device Max thing-type.config.androiddebugbridge.android.deviceMaxVolume.description = Assumed max volume for devices with android versions that do not expose this value (>=android 11). thing-type.config.androiddebugbridge.android.ip.label = IP Address thing-type.config.androiddebugbridge.android.ip.description = Device ip address. +thing-type.config.androiddebugbridge.android.maxADBTimeouts.label = Max ADB Timeouts +thing-type.config.androiddebugbridge.android.maxADBTimeouts.description = Max ADB command consecutive timeouts to force to reset the connection. (0 for disabled) thing-type.config.androiddebugbridge.android.mediaStateJSONConfig.label = Media State Config thing-type.config.androiddebugbridge.android.mediaStateJSONConfig.description = JSON config that allows to modify the media state detection strategy for each app. Refer to the binding documentation. thing-type.config.androiddebugbridge.android.port.label = Port @@ -45,6 +47,8 @@ thing-type.config.androiddebugbridge.android.volumeSettingKey.option.volume_musi thing-type.config.androiddebugbridge.android.volumeSettingKey.option.volume_music_headset = volume music headset thing-type.config.androiddebugbridge.android.volumeSettingKey.option.volume_music_usb_headset = volume music usb headset thing-type.config.androiddebugbridge.android.volumeSettingKey.option.volume_system = volume system +thing-type.config.androiddebugbridge.android.volumeStepPercent.label = Volume Step Percent +thing-type.config.androiddebugbridge.android.volumeStepPercent.description = Percent to increase/decrease volume. # channel types 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 481459927..d05c28910 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 @@ -38,7 +38,7 @@ Device port listening to adb connections. 5555 - + Seconds between device status refreshes. 30 @@ -75,12 +75,24 @@ true + + + Percent to increase/decrease volume. + 15 + true + Assumed max volume for devices with android versions that do not expose this value (>=android 11). 25 true + + + Max ADB command consecutive timeouts to force to reset the connection. (0 for disabled) + 0 + true +