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.
## 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
* 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).
*/
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;
*/
@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.*)]");
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();
}
}
+ 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
}
if (c == null) {
// generate key pair
c = AdbCrypto.generateAdbKeyPair(getBase64Impl());
- c.saveAdbKeyPair(priv, pub);
+ c.saveAdbKeyPair(privKey.toFile(), pubKey.toFile());
}
return c;
}
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;
private @Nullable ScheduledFuture<?> connectionCheckerSchedule;
private AndroidDebugBridgeMediaStatePackageConfig @Nullable [] packageConfigs = null;
private boolean deviceAwake = false;
+ private int consecutiveTimeouts = 0;
public AndroidDebugBridgeHandler(Thing thing,
AndroidDebugBridgeDynamicCommandDescriptionProvider commandDescriptionProvider) {
logger.warn("{} - read error: {}", currentConfig.ip, e.getMessage());
} catch (TimeoutException e) {
logger.warn("{} - timeout error", currentConfig.ip);
+ disconnectOnMaxADBTimeouts();
}
}
}
break;
}
+ consecutiveTimeouts = 0;
}
private void recordDeviceInput(Command recordNameCommand)
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
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)
// 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
} 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));
}
}
+ 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;
public AndroidDebugBridgeHandlerFactory(
final @Reference AndroidDebugBridgeDynamicCommandDescriptionProvider commandDescriptionProvider) {
this.commandDescriptionProvider = commandDescriptionProvider;
+ AndroidDebugBridgeDevice.initADB();
}
@Override
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
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
<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>