mirror of
https://github.com/openhab/openhab-addons.git
synced 2025-01-25 14:55:55 +01:00
[androiddebugbridge] Reconnect on max timeouts and improve volume channel (#15788)
* [androiddebugbridge] Reconnect on max timeouts and improve volume channel --------- Signed-off-by: Miguel Álvarez <miguelwork92@gmail.com>
This commit is contained in:
parent
cb74d85eb0
commit
94d9fb7d36
@ -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
|
||||
|
||||
|
@ -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).
|
||||
*/
|
||||
|
@ -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 (?<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 {
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -41,6 +41,7 @@ public class AndroidDebugBridgeHandlerFactory extends BaseThingHandlerFactory {
|
||||
public AndroidDebugBridgeHandlerFactory(
|
||||
final @Reference AndroidDebugBridgeDynamicCommandDescriptionProvider commandDescriptionProvider) {
|
||||
this.commandDescriptionProvider = commandDescriptionProvider;
|
||||
AndroidDebugBridgeDevice.initADB();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -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
|
||||
|
||||
|
@ -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>
|
||||
@ -75,12 +75,24 @@
|
||||
</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>
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user