]> git.basschouten.com Git - openhab-addons.git/commitdiff
[androiddebugbridge] Reconnect on max timeouts and improve volume channel (#15788)
authorGiviMAD <GiviMAD@users.noreply.github.com>
Fri, 3 Nov 2023 19:45:48 +0000 (12:45 -0700)
committerGitHub <noreply@github.com>
Fri, 3 Nov 2023 19:45:48 +0000 (20:45 +0100)
* [androiddebugbridge] Reconnect on max timeouts and improve volume channel

---------

Signed-off-by: Miguel Álvarez <miguelwork92@gmail.com>
bundles/org.openhab.binding.androiddebugbridge/README.md
bundles/org.openhab.binding.androiddebugbridge/src/main/java/org/openhab/binding/androiddebugbridge/internal/AndroidDebugBridgeConfiguration.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/AndroidDebugBridgeHandler.java
bundles/org.openhab.binding.androiddebugbridge/src/main/java/org/openhab/binding/androiddebugbridge/internal/AndroidDebugBridgeHandlerFactory.java
bundles/org.openhab.binding.androiddebugbridge/src/main/resources/OH-INF/i18n/androiddebugbridge.properties
bundles/org.openhab.binding.androiddebugbridge/src/main/resources/OH-INF/thing/thing-types.xml

index 860fcfab21c899fcb40ce3f23c50823f192b43f7..c7cb11b203357336cd3887515cdbddb172c7cf3f 100644 (file)
@@ -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 |
-
-| 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 |
+| 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)                        |
+| 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
 
index acd8a776007f366f3b30dfd4a0e2e332d404d3de..9e64d7a6d71cb60aa223035c3da1edd609f535d3 100644 (file)
@@ -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).
      */
index 8765072b5241454ea936939fddfec5e0bccb1337..c00c93b4c4695f6b0501e751d196db622a710244 100644 (file)
 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 (?<current>\\d.*) in range \\[(?<min>\\d.*)\\.\\.(?<max>\\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 {
         }
     }
 
+    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 AdbBase64 getBase64Impl() {
-        Charset asciiCharset = Charset.forName("ASCII");
-        return bytes -> new String(Base64.getEncoder().encode(bytes), asciiCharset);
+        return bytes -> new String(Base64.getEncoder().encode(bytes), StandardCharsets.US_ASCII);
     }
 
-    private static AdbCrypto loadKeyPair(String pubKeyFile, String privKeyFile)
+    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;
     }
index 69ac4cb5a1bfc0016a99c2255c04e0758026424a..ceec6ca78c3f1106e8dafcc673f408da808bad27 100644 (file)
@@ -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<String, String> 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;
index 2dbe5e5bf3cf6c3a3461eaa191751ae0aa6adeb6..bc2c02b1c114f903913b60b47d5856a73addc7b3 100644 (file)
@@ -41,6 +41,7 @@ public class AndroidDebugBridgeHandlerFactory extends BaseThingHandlerFactory {
     public AndroidDebugBridgeHandlerFactory(
             final @Reference AndroidDebugBridgeDynamicCommandDescriptionProvider commandDescriptionProvider) {
         this.commandDescriptionProvider = commandDescriptionProvider;
+        AndroidDebugBridgeDevice.initADB();
     }
 
     @Override
index 4d5386d08188c3ab483fb39767dcb36b2470da88..49b922c42d8603b1b4a2982db4fcd279b338c4ae 100644 (file)
@@ -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
 
index 4814599276e38e075753634c1a30924e1a3a7561..d05c289108a0395b16c454d6a197f130380a621e 100644 (file)
@@ -38,7 +38,7 @@
                                <description>Device port listening to adb connections.</description>
                                <default>5555</default>
                        </parameter>
-                       <parameter name="refreshTime" type="integer" min="10" max="120" unit="s" required="true">
+                       <parameter name="refreshTime" type="integer" min="5" max="120" unit="s" required="true">
                                <label>Refresh Time</label>
                                <description>Seconds between device status refreshes.</description>
                                <default>30</default>
                                </options>
                                <advanced>true</advanced>
                        </parameter>
+                       <parameter name="volumeStepPercent" type="integer" min="1" max="100">
+                               <label>Volume Step Percent</label>
+                               <description>Percent to increase/decrease volume.</description>
+                               <default>15</default>
+                               <advanced>true</advanced>
+                       </parameter>
                        <parameter name="deviceMaxVolume" type="integer" min="1" max="100">
                                <label>Device Max Volume</label>
                                <description>Assumed max volume for devices with android versions that do not expose this value (>=android 11).</description>
                                <default>25</default>
                                <advanced>true</advanced>
                        </parameter>
+                       <parameter name="maxADBTimeouts" type="integer" min="0">
+                               <label>Max ADB Timeouts</label>
+                               <description>Max ADB command consecutive timeouts to force to reset the connection. (0 for disabled)</description>
+                               <default>0</default>
+                               <advanced>true</advanced>
+                       </parameter>
                </config-description>
        </thing-type>