[androiddebugbridge] Initial contribution (#9259)
* Initial contribution Signed-off-by: Miguel <miguelwork92@gmail.com>
This commit is contained in:
@@ -0,0 +1,23 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
|
||||
Copyright (c) 2010-2020 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
|
||||
|
||||
-->
|
||||
<features name="org.openhab.binding.androiddebugbridge-${project.version}" xmlns="http://karaf.apache.org/xmlns/features/v1.4.0">
|
||||
<repository>mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features</repository>
|
||||
|
||||
<feature name="openhab-binding-androiddebugbridge" description="Android Debug Bridge Binding" version="${project.version}">
|
||||
<feature>openhab-runtime-base</feature>
|
||||
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.androiddebugbridge/${project.version}</bundle>
|
||||
</feature>
|
||||
</features>
|
||||
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2021 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;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* The {@link AndroidDebugBridgeConfiguration} class contains fields mapping binding configuration parameters.
|
||||
*
|
||||
* @author Miguel Álvarez - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class AndroidDebugBridgeBindingConfiguration {
|
||||
/**
|
||||
* Port used on discovery.
|
||||
*/
|
||||
public int discoveryPort = 5555;
|
||||
/**
|
||||
* Discovery reachable timeout.
|
||||
*/
|
||||
public int discoveryReachableMs = 3000;
|
||||
/**
|
||||
* Discovery from ip index.
|
||||
*/
|
||||
public int discoveryIpRangeMin = 0;
|
||||
/**
|
||||
* Discovery to ip index.
|
||||
*/
|
||||
public int discoveryIpRangeMax = 255;
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2021 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;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Set;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.core.thing.ThingTypeUID;
|
||||
|
||||
/**
|
||||
* The {@link AndroidDebugBridgeBindingConstants} class defines common constants, which are
|
||||
* used across the whole binding.
|
||||
*
|
||||
* @author Miguel Álvarez - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class AndroidDebugBridgeBindingConstants {
|
||||
|
||||
private static final String BINDING_ID = "androiddebugbridge";
|
||||
public static final String BINDING_CONFIGURATION_PID = "binding.androiddebugbridge";
|
||||
|
||||
// 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);
|
||||
// List of all Channel ids
|
||||
public static final String KEY_EVENT_CHANNEL = "key-event";
|
||||
public static final String TEXT_CHANNEL = "text";
|
||||
public static final String MEDIA_VOLUME_CHANNEL = "media-volume";
|
||||
public static final String MEDIA_CONTROL_CHANNEL = "media-control";
|
||||
public static final String START_PACKAGE_CHANNEL = "start-package";
|
||||
public static final String STOP_PACKAGE_CHANNEL = "stop-package";
|
||||
public static final String STOP_CURRENT_PACKAGE_CHANNEL = "stop-current-package";
|
||||
public static final String CURRENT_PACKAGE_CHANNEL = "current-package";
|
||||
public static final String WAKE_LOCK_CHANNEL = "wake-lock";
|
||||
public static final String SCREEN_STATE_CHANNEL = "screen-state";
|
||||
// List of all Parameters
|
||||
public static final String PARAMETER_IP = "ip";
|
||||
public static final String PARAMETER_PORT = "port";
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2021 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;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* The {@link AndroidDebugBridgeConfiguration} class contains fields mapping thing configuration parameters.
|
||||
*
|
||||
* @author Miguel Álvarez - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class AndroidDebugBridgeConfiguration {
|
||||
/**
|
||||
* The IP address to use for connecting to the Android device.
|
||||
*/
|
||||
public String ip = "";
|
||||
/**
|
||||
* Sample configuration parameter. Replace with your own.
|
||||
*/
|
||||
public int port;
|
||||
/**
|
||||
* Time for scheduled state check.
|
||||
*/
|
||||
public int refreshTime = 30;
|
||||
/**
|
||||
* Command timeout seconds.
|
||||
*/
|
||||
public int timeout = 5;
|
||||
/**
|
||||
* Configure media state detection behavior by package
|
||||
*/
|
||||
public @Nullable String mediaStateJSONConfig;
|
||||
}
|
||||
@@ -0,0 +1,349 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2021 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;
|
||||
|
||||
import java.io.*;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.Socket;
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.spec.InvalidKeySpecException;
|
||||
import java.util.Arrays;
|
||||
import java.util.Base64;
|
||||
import java.util.concurrent.*;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.core.OpenHAB;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import com.tananaev.adblib.AdbBase64;
|
||||
import com.tananaev.adblib.AdbConnection;
|
||||
import com.tananaev.adblib.AdbCrypto;
|
||||
import com.tananaev.adblib.AdbStream;
|
||||
|
||||
/**
|
||||
* The {@link AndroidDebugBridgeConfiguration} class encapsulates adb device connection logic.
|
||||
*
|
||||
* @author Miguel Álvarez - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class AndroidDebugBridgeDevice {
|
||||
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 String ip = "127.0.0.1";
|
||||
private int port = 5555;
|
||||
private int timeoutSec = 5;
|
||||
private @Nullable Socket socket;
|
||||
private @Nullable AdbConnection connection;
|
||||
private @Nullable Future<String> commandFuture;
|
||||
|
||||
AndroidDebugBridgeDevice(ScheduledExecutorService scheduler) {
|
||||
this.scheduler = scheduler;
|
||||
}
|
||||
|
||||
public void configure(String ip, int port, int timeout) {
|
||||
this.ip = ip;
|
||||
this.port = port;
|
||||
this.timeoutSec = timeout;
|
||||
}
|
||||
|
||||
public void sendKeyEvent(String eventCode)
|
||||
throws InterruptedException, AndroidDebugBridgeDeviceException, TimeoutException, ExecutionException {
|
||||
runAdbShell("input", "keyevent", eventCode);
|
||||
}
|
||||
|
||||
public void sendText(String text)
|
||||
throws AndroidDebugBridgeDeviceException, InterruptedException, TimeoutException, ExecutionException {
|
||||
runAdbShell("input", "text", URLEncoder.encode(text, StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
public void startPackage(String packageName)
|
||||
throws InterruptedException, AndroidDebugBridgeDeviceException, TimeoutException, ExecutionException {
|
||||
var out = runAdbShell("monkey", "--pct-syskeys", "0", "-p", packageName, "-v", "1");
|
||||
if (out.contains("monkey aborted"))
|
||||
throw new AndroidDebugBridgeDeviceException("Unable to open package");
|
||||
}
|
||||
|
||||
public void stopPackage(String packageName)
|
||||
throws AndroidDebugBridgeDeviceException, InterruptedException, TimeoutException, ExecutionException {
|
||||
runAdbShell("am", "force-stop", packageName);
|
||||
}
|
||||
|
||||
public String getCurrentPackage() throws AndroidDebugBridgeDeviceException, InterruptedException,
|
||||
AndroidDebugBridgeDeviceReadException, TimeoutException, ExecutionException {
|
||||
var out = runAdbShell("dumpsys", "window", "windows", "|", "grep", "mFocusedApp");
|
||||
var targetLine = Arrays.stream(out.split("\n")).findFirst().orElse("");
|
||||
var lineParts = targetLine.split(" ");
|
||||
if (lineParts.length >= 2) {
|
||||
var packageActivityName = lineParts[lineParts.length - 2];
|
||||
if (packageActivityName.contains("/"))
|
||||
return packageActivityName.split("/")[0];
|
||||
}
|
||||
throw new AndroidDebugBridgeDeviceReadException("can read package name");
|
||||
}
|
||||
|
||||
public boolean isScreenOn() throws InterruptedException, AndroidDebugBridgeDeviceException,
|
||||
AndroidDebugBridgeDeviceReadException, TimeoutException, ExecutionException {
|
||||
String devicesResp = runAdbShell("dumpsys", "power", "|", "grep", "'Display Power'");
|
||||
if (devicesResp.contains("=")) {
|
||||
try {
|
||||
return devicesResp.split("=")[1].equals("ON");
|
||||
} catch (NumberFormatException e) {
|
||||
logger.debug("Unable to parse device wake lock: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
throw new AndroidDebugBridgeDeviceReadException("can read screen state");
|
||||
}
|
||||
|
||||
public boolean isPlayingMedia(String currentApp)
|
||||
throws AndroidDebugBridgeDeviceException, InterruptedException, TimeoutException, ExecutionException {
|
||||
String devicesResp = runAdbShell("dumpsys", "media_session", "|", "grep", "-A", "100", "'Sessions Stack'", "|",
|
||||
"grep", "-A", "50", currentApp);
|
||||
String[] mediaSessions = devicesResp.split("\n\n");
|
||||
if (mediaSessions.length == 0) {
|
||||
// no media session found for current app
|
||||
return false;
|
||||
}
|
||||
boolean isPlaying = mediaSessions[0].contains("PlaybackState {state=3");
|
||||
logger.debug("device media state playing {}", isPlaying);
|
||||
return isPlaying;
|
||||
}
|
||||
|
||||
public boolean isPlayingAudio()
|
||||
throws AndroidDebugBridgeDeviceException, InterruptedException, TimeoutException, ExecutionException {
|
||||
String audioDump = runAdbShell("dumpsys", "audio", "|", "grep", "ID:");
|
||||
return audioDump.contains("state:started");
|
||||
}
|
||||
|
||||
public VolumeInfo getMediaVolume() throws AndroidDebugBridgeDeviceException, InterruptedException,
|
||||
AndroidDebugBridgeDeviceReadException, TimeoutException, ExecutionException {
|
||||
return getVolume(ANDROID_MEDIA_STREAM);
|
||||
}
|
||||
|
||||
public void setMediaVolume(int volume)
|
||||
throws AndroidDebugBridgeDeviceException, InterruptedException, TimeoutException, ExecutionException {
|
||||
setVolume(ANDROID_MEDIA_STREAM, volume);
|
||||
}
|
||||
|
||||
public int getPowerWakeLock() throws InterruptedException, AndroidDebugBridgeDeviceException,
|
||||
AndroidDebugBridgeDeviceReadException, TimeoutException, ExecutionException {
|
||||
String lockResp = runAdbShell("dumpsys", "power", "|", "grep", "Locks", "|", "grep", "'size='");
|
||||
if (lockResp.contains("=")) {
|
||||
try {
|
||||
return Integer.parseInt(lockResp.replace("\n", "").split("=")[1]);
|
||||
} catch (NumberFormatException e) {
|
||||
logger.debug("Unable to parse device wake lock: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
throw new AndroidDebugBridgeDeviceReadException("can read wake lock");
|
||||
}
|
||||
|
||||
private void setVolume(int stream, int volume)
|
||||
throws AndroidDebugBridgeDeviceException, InterruptedException, TimeoutException, ExecutionException {
|
||||
runAdbShell("media", "volume", "--show", "--stream", String.valueOf(stream), "--set", String.valueOf(volume));
|
||||
}
|
||||
|
||||
public String getModel() throws AndroidDebugBridgeDeviceException, InterruptedException,
|
||||
AndroidDebugBridgeDeviceReadException, TimeoutException, ExecutionException {
|
||||
return getDeviceProp("ro.product.model");
|
||||
}
|
||||
|
||||
public String getAndroidVersion() throws AndroidDebugBridgeDeviceException, InterruptedException,
|
||||
AndroidDebugBridgeDeviceReadException, TimeoutException, ExecutionException {
|
||||
return getDeviceProp("ro.build.version.release");
|
||||
}
|
||||
|
||||
public String getBrand() throws AndroidDebugBridgeDeviceException, InterruptedException,
|
||||
AndroidDebugBridgeDeviceReadException, TimeoutException, ExecutionException {
|
||||
return getDeviceProp("ro.product.brand");
|
||||
}
|
||||
|
||||
public String getSerialNo() throws AndroidDebugBridgeDeviceException, InterruptedException,
|
||||
AndroidDebugBridgeDeviceReadException, TimeoutException, ExecutionException {
|
||||
return getDeviceProp("ro.serialno");
|
||||
}
|
||||
|
||||
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");
|
||||
}
|
||||
return propValue;
|
||||
}
|
||||
|
||||
private VolumeInfo getVolume(int stream) throws AndroidDebugBridgeDeviceException, InterruptedException,
|
||||
AndroidDebugBridgeDeviceReadException, TimeoutException, ExecutionException {
|
||||
String volumeResp = runAdbShell("media", "volume", "--show", "--stream", String.valueOf(stream), "--get", "|",
|
||||
"grep", "volume");
|
||||
Matcher matcher = VOLUME_PATTERN.matcher(volumeResp);
|
||||
if (!matcher.find())
|
||||
throw new AndroidDebugBridgeDeviceReadException("Unable to get volume info");
|
||||
var volumeInfo = new VolumeInfo(Integer.parseInt(matcher.group("current")),
|
||||
Integer.parseInt(matcher.group("min")), Integer.parseInt(matcher.group("max")));
|
||||
logger.debug("Device {}:{} VolumeInfo: current {}, min {}, max {}", this.ip, this.port, volumeInfo.current,
|
||||
volumeInfo.min, volumeInfo.max);
|
||||
return volumeInfo;
|
||||
}
|
||||
|
||||
public boolean isConnected() {
|
||||
var currentSocket = socket;
|
||||
return currentSocket != null && currentSocket.isConnected();
|
||||
}
|
||||
|
||||
public void connect() throws AndroidDebugBridgeDeviceException, InterruptedException {
|
||||
this.disconnect();
|
||||
AdbConnection adbConnection;
|
||||
Socket sock;
|
||||
AdbCrypto crypto = adbCrypto;
|
||||
if (crypto == null) {
|
||||
throw new AndroidDebugBridgeDeviceException("Device not connected");
|
||||
}
|
||||
try {
|
||||
sock = new Socket();
|
||||
socket = sock;
|
||||
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")) {
|
||||
// Connection aborted by us
|
||||
throw new InterruptedException();
|
||||
}
|
||||
throw new AndroidDebugBridgeDeviceException("Can not open socket " + ip + ":" + port);
|
||||
}
|
||||
try {
|
||||
adbConnection = AdbConnection.create(sock, crypto);
|
||||
connection = adbConnection;
|
||||
adbConnection.connect(15, TimeUnit.SECONDS, false);
|
||||
} catch (IOException e) {
|
||||
logger.debug("Error connecting to {}: {}", ip, e.getMessage());
|
||||
throw new AndroidDebugBridgeDeviceException("Can not open adb connection " + ip + ":" + port);
|
||||
}
|
||||
}
|
||||
|
||||
private String runAdbShell(String... args)
|
||||
throws InterruptedException, AndroidDebugBridgeDeviceException, TimeoutException, ExecutionException {
|
||||
var adb = connection;
|
||||
if (adb == null) {
|
||||
throw new AndroidDebugBridgeDeviceException("Device not connected");
|
||||
}
|
||||
var commandFuture = scheduler.submit(() -> {
|
||||
var byteArrayOutputStream = new ByteArrayOutputStream();
|
||||
String cmd = String.join(" ", args);
|
||||
logger.debug("{} - shell:{}", ip, 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")) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
return byteArrayOutputStream.toString(StandardCharsets.US_ASCII);
|
||||
});
|
||||
this.commandFuture = commandFuture;
|
||||
return commandFuture.get(timeoutSec, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
private static AdbBase64 getBase64Impl() {
|
||||
Charset asciiCharset = Charset.forName("ASCII");
|
||||
return bytes -> new String(Base64.getEncoder().encode(bytes), asciiCharset);
|
||||
}
|
||||
|
||||
private static AdbCrypto loadKeyPair(String pubKeyFile, String privKeyFile)
|
||||
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()) {
|
||||
try {
|
||||
c = AdbCrypto.loadAdbKeyPair(getBase64Impl(), priv, pub);
|
||||
} catch (IOException ignored) {
|
||||
// Keys don't exits
|
||||
}
|
||||
}
|
||||
if (c == null) {
|
||||
// generate key pair
|
||||
c = AdbCrypto.generateAdbKeyPair(getBase64Impl());
|
||||
c.saveAdbKeyPair(priv, pub);
|
||||
}
|
||||
return c;
|
||||
}
|
||||
|
||||
public void disconnect() {
|
||||
var commandFuture = this.commandFuture;
|
||||
if (commandFuture != null && !commandFuture.isDone()) {
|
||||
commandFuture.cancel(true);
|
||||
}
|
||||
var adb = connection;
|
||||
var sock = socket;
|
||||
if (adb != null) {
|
||||
try {
|
||||
adb.close();
|
||||
} catch (IOException ignored) {
|
||||
}
|
||||
connection = null;
|
||||
}
|
||||
if (sock != null) {
|
||||
try {
|
||||
sock.close();
|
||||
} catch (IOException ignored) {
|
||||
}
|
||||
socket = null;
|
||||
}
|
||||
}
|
||||
|
||||
public static class VolumeInfo {
|
||||
public int current;
|
||||
public int min;
|
||||
public int max;
|
||||
|
||||
VolumeInfo(int current, int min, int max) {
|
||||
this.current = current;
|
||||
this.min = min;
|
||||
this.max = max;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2021 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;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* The {@link AndroidDebugBridgeDiscoveryService} discover Android ADB Instances in the network.
|
||||
*
|
||||
* @author Miguel Alvarez - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class AndroidDebugBridgeDeviceException extends Exception {
|
||||
private static final long serialVersionUID = 6608406239134276286L;
|
||||
|
||||
public AndroidDebugBridgeDeviceException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2021 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;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* The {@link AndroidDebugBridgeDiscoveryService} discover Android ADB Instances in the network.
|
||||
*
|
||||
* @author Miguel Alvarez - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class AndroidDebugBridgeDeviceReadException extends Exception {
|
||||
private static final long serialVersionUID = 6608406239134276287L;
|
||||
|
||||
public AndroidDebugBridgeDeviceReadException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2021 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;
|
||||
|
||||
import static org.openhab.binding.androiddebugbridge.internal.AndroidDebugBridgeBindingConstants.*;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.Inet4Address;
|
||||
import java.net.InetAddress;
|
||||
import java.net.NetworkInterface;
|
||||
import java.net.SocketException;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.core.config.discovery.AbstractDiscoveryService;
|
||||
import org.openhab.core.config.discovery.DiscoveryResultBuilder;
|
||||
import org.openhab.core.config.discovery.DiscoveryService;
|
||||
import org.openhab.core.thing.Thing;
|
||||
import org.openhab.core.thing.ThingUID;
|
||||
import org.osgi.service.cm.Configuration;
|
||||
import org.osgi.service.cm.ConfigurationAdmin;
|
||||
import org.osgi.service.component.annotations.Activate;
|
||||
import org.osgi.service.component.annotations.Component;
|
||||
import org.osgi.service.component.annotations.Reference;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* The {@link AndroidDebugBridgeDiscoveryService} discover Android ADB Instances in the network.
|
||||
*
|
||||
* @author Miguel Alvarez - Initial contribution
|
||||
*/
|
||||
@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";
|
||||
public static final int MAX_RETRIES = 2;
|
||||
private final Logger logger = LoggerFactory.getLogger(AndroidDebugBridgeDiscoveryService.class);
|
||||
private final ConfigurationAdmin admin;
|
||||
private boolean discoveryRunning = false;
|
||||
|
||||
@Activate
|
||||
public AndroidDebugBridgeDiscoveryService(@Reference ConfigurationAdmin admin) {
|
||||
super(SUPPORTED_THING_TYPES, TIMEOUT_MS, false);
|
||||
this.admin = admin;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void startScan() {
|
||||
logger.debug("scan started: searching android devices");
|
||||
discoveryRunning = true;
|
||||
Enumeration<NetworkInterface> nets;
|
||||
AndroidDebugBridgeBindingConfiguration configuration = getConfig();
|
||||
if (configuration == null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
nets = NetworkInterface.getNetworkInterfaces();
|
||||
for (NetworkInterface netint : Collections.list(nets)) {
|
||||
Enumeration<InetAddress> inetAddresses = netint.getInetAddresses();
|
||||
for (InetAddress inetAddress : Collections.list(inetAddresses)) {
|
||||
if (!discoveryRunning) {
|
||||
break;
|
||||
}
|
||||
if (!(inetAddress instanceof Inet4Address)
|
||||
|| inetAddress.getHostAddress().equals(LOCAL_INTERFACE_IP)) {
|
||||
continue;
|
||||
}
|
||||
String[] ipParts = inetAddress.getHostAddress().split("\\.");
|
||||
for (int i = configuration.discoveryIpRangeMin; i <= configuration.discoveryIpRangeMax; i++) {
|
||||
if (!discoveryRunning) {
|
||||
break;
|
||||
}
|
||||
ipParts[3] = Integer.toString(i);
|
||||
String currentIp = String.join(".", ipParts);
|
||||
try {
|
||||
var currentAddress = InetAddress.getByName(currentIp);
|
||||
logger.debug("address: {}", currentIp);
|
||||
if (currentAddress.isReachable(configuration.discoveryReachableMs)) {
|
||||
logger.debug("Reachable ip: {}", currentIp);
|
||||
int retries = 0;
|
||||
while (retries < MAX_RETRIES) {
|
||||
try {
|
||||
discoverWithADB(currentIp, configuration.discoveryPort);
|
||||
} catch (AndroidDebugBridgeDeviceReadException | TimeoutException e) {
|
||||
retries++;
|
||||
if (retries < MAX_RETRIES) {
|
||||
logger.debug("retrying - pending {}", MAX_RETRIES - retries);
|
||||
continue;
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (IOException | AndroidDebugBridgeDeviceException | AndroidDebugBridgeDeviceReadException
|
||||
| TimeoutException | ExecutionException e) {
|
||||
logger.debug("Error connecting to device at {}: {}", currentIp, e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (SocketException | InterruptedException e) {
|
||||
logger.warn("Error while discovering: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void discoverWithADB(String ip, int port) throws InterruptedException, AndroidDebugBridgeDeviceException,
|
||||
AndroidDebugBridgeDeviceReadException, TimeoutException, ExecutionException {
|
||||
var device = new AndroidDebugBridgeDevice(scheduler);
|
||||
device.configure(ip, port, 10);
|
||||
try {
|
||||
device.connect();
|
||||
logger.debug("connected adb at {}:{}", ip, port);
|
||||
String serialNo = device.getSerialNo();
|
||||
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);
|
||||
} finally {
|
||||
device.disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void stopScan() {
|
||||
super.stopScan();
|
||||
discoveryRunning = false;
|
||||
logger.debug("scan stopped");
|
||||
}
|
||||
|
||||
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());
|
||||
}
|
||||
|
||||
private @Nullable AndroidDebugBridgeBindingConfiguration getConfig() {
|
||||
try {
|
||||
Configuration configOnline = admin.getConfiguration(BINDING_CONFIGURATION_PID, null);
|
||||
if (configOnline != null) {
|
||||
Dictionary<String, Object> props = configOnline.getProperties();
|
||||
if (props != null) {
|
||||
Map<String, Object> propMap = Collections.list(props.keys()).stream()
|
||||
.collect(Collectors.toMap(Function.identity(), props::get));
|
||||
return new org.openhab.core.config.core.Configuration(propMap)
|
||||
.as(AndroidDebugBridgeBindingConfiguration.class);
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
logger.warn("Unable to read configuration: {}", e.getMessage());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,339 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2021 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;
|
||||
|
||||
import static org.openhab.binding.androiddebugbridge.internal.AndroidDebugBridgeBindingConstants.*;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.core.library.types.*;
|
||||
import org.openhab.core.thing.ChannelUID;
|
||||
import org.openhab.core.thing.Thing;
|
||||
import org.openhab.core.thing.ThingStatus;
|
||||
import org.openhab.core.thing.ThingStatusDetail;
|
||||
import org.openhab.core.thing.binding.BaseThingHandler;
|
||||
import org.openhab.core.types.Command;
|
||||
import org.openhab.core.types.RefreshType;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.JsonSyntaxException;
|
||||
|
||||
/**
|
||||
* The {@link AndroidDebugBridgeHandler} is responsible for handling commands, which are
|
||||
* sent to one of the channels.
|
||||
*
|
||||
* @author Miguel Álvarez - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class AndroidDebugBridgeHandler extends BaseThingHandler {
|
||||
|
||||
public static final String KEY_EVENT_PLAY = "126";
|
||||
public static final String KEY_EVENT_PAUSE = "127";
|
||||
public static final String KEY_EVENT_NEXT = "87";
|
||||
public static final String KEY_EVENT_PREVIOUS = "88";
|
||||
public static final String KEY_EVENT_MEDIA_REWIND = "89";
|
||||
public static final String KEY_EVENT_MEDIA_FAST_FORWARD = "90";
|
||||
private static final Gson GSON = new Gson();
|
||||
private final Logger logger = LoggerFactory.getLogger(AndroidDebugBridgeHandler.class);
|
||||
private final AndroidDebugBridgeDevice adbConnection;
|
||||
private int maxMediaVolume = 0;
|
||||
private AndroidDebugBridgeConfiguration config = new AndroidDebugBridgeConfiguration();
|
||||
private @Nullable ScheduledFuture<?> connectionCheckerSchedule;
|
||||
private AndroidDebugBridgeMediaStatePackageConfig @Nullable [] packageConfigs = null;
|
||||
|
||||
public AndroidDebugBridgeHandler(Thing thing) {
|
||||
super(thing);
|
||||
this.adbConnection = new AndroidDebugBridgeDevice(scheduler);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleCommand(ChannelUID channelUID, Command command) {
|
||||
var currentConfig = config;
|
||||
if (currentConfig == null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (!adbConnection.isConnected()) {
|
||||
// try reconnect
|
||||
adbConnection.connect();
|
||||
}
|
||||
handleCommandInternal(channelUID, command);
|
||||
} catch (InterruptedException ignored) {
|
||||
} catch (AndroidDebugBridgeDeviceException | ExecutionException e) {
|
||||
if (!(e.getCause() instanceof InterruptedException)) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
|
||||
adbConnection.disconnect();
|
||||
}
|
||||
} catch (AndroidDebugBridgeDeviceReadException e) {
|
||||
logger.warn("{} - read error: {}", currentConfig.ip, e.getMessage());
|
||||
} catch (TimeoutException e) {
|
||||
logger.warn("{} - timeout error", currentConfig.ip);
|
||||
}
|
||||
}
|
||||
|
||||
private void handleCommandInternal(ChannelUID channelUID, Command command)
|
||||
throws InterruptedException, AndroidDebugBridgeDeviceException, AndroidDebugBridgeDeviceReadException,
|
||||
TimeoutException, ExecutionException {
|
||||
if (!isLinked(channelUID)) {
|
||||
return;
|
||||
}
|
||||
String channelId = channelUID.getId();
|
||||
switch (channelId) {
|
||||
case KEY_EVENT_CHANNEL:
|
||||
adbConnection.sendKeyEvent(command.toFullString());
|
||||
break;
|
||||
case TEXT_CHANNEL:
|
||||
adbConnection.sendText(command.toFullString());
|
||||
break;
|
||||
case MEDIA_VOLUME_CHANNEL:
|
||||
handleMediaVolume(channelUID, command);
|
||||
break;
|
||||
case MEDIA_CONTROL_CHANNEL:
|
||||
handleMediaControlCommand(channelUID, command);
|
||||
break;
|
||||
case START_PACKAGE_CHANNEL:
|
||||
adbConnection.startPackage(command.toFullString());
|
||||
updateState(new ChannelUID(this.thing.getUID(), CURRENT_PACKAGE_CHANNEL),
|
||||
new StringType(command.toFullString()));
|
||||
break;
|
||||
case STOP_PACKAGE_CHANNEL:
|
||||
adbConnection.stopPackage(command.toFullString());
|
||||
break;
|
||||
case STOP_CURRENT_PACKAGE_CHANNEL:
|
||||
if (OnOffType.from(command.toFullString()).equals(OnOffType.OFF)) {
|
||||
adbConnection.stopPackage(adbConnection.getCurrentPackage());
|
||||
}
|
||||
break;
|
||||
case CURRENT_PACKAGE_CHANNEL:
|
||||
if (command instanceof RefreshType) {
|
||||
var packageName = adbConnection.getCurrentPackage();
|
||||
updateState(channelUID, new StringType(packageName));
|
||||
}
|
||||
break;
|
||||
case WAKE_LOCK_CHANNEL:
|
||||
if (command instanceof RefreshType) {
|
||||
int lock = adbConnection.getPowerWakeLock();
|
||||
updateState(channelUID, new DecimalType(lock));
|
||||
}
|
||||
break;
|
||||
case SCREEN_STATE_CHANNEL:
|
||||
if (command instanceof RefreshType) {
|
||||
boolean screenState = adbConnection.isScreenOn();
|
||||
updateState(channelUID, OnOffType.from(screenState));
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void handleMediaVolume(ChannelUID channelUID, Command command)
|
||||
throws InterruptedException, AndroidDebugBridgeDeviceReadException, AndroidDebugBridgeDeviceException,
|
||||
TimeoutException, ExecutionException {
|
||||
if (command instanceof RefreshType) {
|
||||
var volumeInfo = adbConnection.getMediaVolume();
|
||||
maxMediaVolume = volumeInfo.max;
|
||||
updateState(channelUID, new PercentType((int) Math.round(toPercent(volumeInfo.current, volumeInfo.max))));
|
||||
} else {
|
||||
if (maxMediaVolume == 0) {
|
||||
return; // We can not transform percentage
|
||||
}
|
||||
int targetVolume = Integer.parseInt(command.toFullString());
|
||||
adbConnection.setMediaVolume((int) Math.round(fromPercent(targetVolume, maxMediaVolume)));
|
||||
updateState(channelUID, new PercentType(targetVolume));
|
||||
}
|
||||
}
|
||||
|
||||
private double toPercent(double value, double maxValue) {
|
||||
return (value / maxValue) * 100;
|
||||
}
|
||||
|
||||
private double fromPercent(double value, double maxValue) {
|
||||
return (value / 100) * maxValue;
|
||||
}
|
||||
|
||||
private void handleMediaControlCommand(ChannelUID channelUID, Command command)
|
||||
throws InterruptedException, AndroidDebugBridgeDeviceException, AndroidDebugBridgeDeviceReadException,
|
||||
TimeoutException, ExecutionException {
|
||||
if (command instanceof RefreshType) {
|
||||
boolean playing;
|
||||
String currentPackage = adbConnection.getCurrentPackage();
|
||||
var currentPackageConfig = packageConfigs != null ? Arrays.stream(packageConfigs)
|
||||
.filter(pc -> pc.name.equals(currentPackage)).findFirst().orElse(null) : null;
|
||||
if (currentPackageConfig != null) {
|
||||
logger.debug("media stream config found for {}, mode: {}", currentPackage, currentPackageConfig.mode);
|
||||
switch (currentPackageConfig.mode) {
|
||||
case "idle":
|
||||
playing = false;
|
||||
break;
|
||||
case "wake_lock":
|
||||
int wakeLockState = adbConnection.getPowerWakeLock();
|
||||
playing = currentPackageConfig.wakeLockPlayStates.contains(wakeLockState);
|
||||
break;
|
||||
case "media_state":
|
||||
playing = adbConnection.isPlayingMedia(currentPackage);
|
||||
break;
|
||||
case "audio":
|
||||
playing = adbConnection.isPlayingAudio();
|
||||
break;
|
||||
default:
|
||||
logger.warn("media state config: package {} unsupported mode", currentPackage);
|
||||
playing = false;
|
||||
}
|
||||
} else {
|
||||
logger.debug("media stream config not found for {}", currentPackage);
|
||||
playing = adbConnection.isPlayingMedia(currentPackage);
|
||||
}
|
||||
updateState(channelUID, playing ? PlayPauseType.PLAY : PlayPauseType.PAUSE);
|
||||
} else if (command instanceof PlayPauseType) {
|
||||
if (command == PlayPauseType.PLAY) {
|
||||
adbConnection.sendKeyEvent(KEY_EVENT_PLAY);
|
||||
updateState(channelUID, PlayPauseType.PLAY);
|
||||
} else if (command == PlayPauseType.PAUSE) {
|
||||
adbConnection.sendKeyEvent(KEY_EVENT_PAUSE);
|
||||
updateState(channelUID, PlayPauseType.PAUSE);
|
||||
}
|
||||
} else if (command instanceof NextPreviousType) {
|
||||
if (command == NextPreviousType.NEXT) {
|
||||
adbConnection.sendKeyEvent(KEY_EVENT_NEXT);
|
||||
} else if (command == NextPreviousType.PREVIOUS) {
|
||||
adbConnection.sendKeyEvent(KEY_EVENT_PREVIOUS);
|
||||
}
|
||||
} else if (command instanceof RewindFastforwardType) {
|
||||
if (command == RewindFastforwardType.FASTFORWARD) {
|
||||
adbConnection.sendKeyEvent(KEY_EVENT_MEDIA_FAST_FORWARD);
|
||||
} else if (command == RewindFastforwardType.REWIND) {
|
||||
adbConnection.sendKeyEvent(KEY_EVENT_MEDIA_REWIND);
|
||||
}
|
||||
} else {
|
||||
logger.warn("Unknown media control command: {}", command);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize() {
|
||||
var currentConfig = getConfigAs(AndroidDebugBridgeConfiguration.class);
|
||||
config = currentConfig;
|
||||
var mediaStateJSONConfig = currentConfig.mediaStateJSONConfig;
|
||||
if (mediaStateJSONConfig != null && !mediaStateJSONConfig.isEmpty()) {
|
||||
loadMediaStateConfig(mediaStateJSONConfig);
|
||||
}
|
||||
adbConnection.configure(currentConfig.ip, currentConfig.port, currentConfig.timeout);
|
||||
updateStatus(ThingStatus.UNKNOWN);
|
||||
connectionCheckerSchedule = scheduler.scheduleWithFixedDelay(this::checkConnection, 0,
|
||||
currentConfig.refreshTime, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
private void loadMediaStateConfig(String mediaStateJSONConfig) {
|
||||
try {
|
||||
this.packageConfigs = GSON.fromJson(mediaStateJSONConfig,
|
||||
AndroidDebugBridgeMediaStatePackageConfig[].class);
|
||||
} catch (JsonSyntaxException e) {
|
||||
logger.warn("unable to parse media state config: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
var schedule = connectionCheckerSchedule;
|
||||
if (schedule != null) {
|
||||
schedule.cancel(true);
|
||||
connectionCheckerSchedule = null;
|
||||
}
|
||||
packageConfigs = null;
|
||||
adbConnection.disconnect();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
public void checkConnection() {
|
||||
var currentConfig = config;
|
||||
if (currentConfig == null)
|
||||
return;
|
||||
try {
|
||||
logger.debug("Refresh device {} status", currentConfig.ip);
|
||||
if (adbConnection.isConnected()) {
|
||||
updateStatus(ThingStatus.ONLINE);
|
||||
refreshStatus();
|
||||
} else {
|
||||
try {
|
||||
adbConnection.connect();
|
||||
} catch (AndroidDebugBridgeDeviceException e) {
|
||||
logger.debug("Error connecting to device; [{}]: {}", e.getClass().getCanonicalName(),
|
||||
e.getMessage());
|
||||
updateStatus(ThingStatus.OFFLINE);
|
||||
return;
|
||||
}
|
||||
if (adbConnection.isConnected()) {
|
||||
updateStatus(ThingStatus.ONLINE);
|
||||
refreshStatus();
|
||||
}
|
||||
}
|
||||
} catch (InterruptedException ignored) {
|
||||
} catch (AndroidDebugBridgeDeviceException | ExecutionException e) {
|
||||
logger.debug("Connection checker error: {}", e.getMessage());
|
||||
adbConnection.disconnect();
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void refreshStatus() throws InterruptedException, AndroidDebugBridgeDeviceException, ExecutionException {
|
||||
try {
|
||||
handleCommandInternal(new ChannelUID(this.thing.getUID(), MEDIA_VOLUME_CHANNEL), RefreshType.REFRESH);
|
||||
} catch (AndroidDebugBridgeDeviceReadException e) {
|
||||
logger.warn("Unable to refresh media volume: {}", e.getMessage());
|
||||
} catch (TimeoutException e) {
|
||||
logger.warn("Unable to refresh media volume: Timeout");
|
||||
}
|
||||
try {
|
||||
handleCommandInternal(new ChannelUID(this.thing.getUID(), MEDIA_CONTROL_CHANNEL), RefreshType.REFRESH);
|
||||
} catch (AndroidDebugBridgeDeviceReadException e) {
|
||||
logger.warn("Unable to refresh play status: {}", e.getMessage());
|
||||
} catch (TimeoutException e) {
|
||||
logger.warn("Unable to refresh play status: Timeout");
|
||||
}
|
||||
try {
|
||||
handleCommandInternal(new ChannelUID(this.thing.getUID(), CURRENT_PACKAGE_CHANNEL), RefreshType.REFRESH);
|
||||
} catch (AndroidDebugBridgeDeviceReadException e) {
|
||||
logger.warn("Unable to refresh current package: {}", e.getMessage());
|
||||
} catch (TimeoutException e) {
|
||||
logger.warn("Unable to refresh current package: Timeout");
|
||||
}
|
||||
try {
|
||||
handleCommandInternal(new ChannelUID(this.thing.getUID(), WAKE_LOCK_CHANNEL), RefreshType.REFRESH);
|
||||
} catch (AndroidDebugBridgeDeviceReadException e) {
|
||||
logger.warn("Unable to refresh wake lock: {}", e.getMessage());
|
||||
} catch (TimeoutException e) {
|
||||
logger.warn("Unable to refresh wake lock: Timeout");
|
||||
}
|
||||
try {
|
||||
handleCommandInternal(new ChannelUID(this.thing.getUID(), SCREEN_STATE_CHANNEL), RefreshType.REFRESH);
|
||||
} catch (AndroidDebugBridgeDeviceReadException e) {
|
||||
logger.warn("Unable to refresh screen state: {}", e.getMessage());
|
||||
} catch (TimeoutException e) {
|
||||
logger.warn("Unable to refresh screen state: Timeout");
|
||||
}
|
||||
}
|
||||
|
||||
static class AndroidDebugBridgeMediaStatePackageConfig {
|
||||
public String name = "";
|
||||
public String mode = "";
|
||||
public List<Integer> wakeLockPlayStates = List.of();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2021 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;
|
||||
|
||||
import static org.openhab.binding.androiddebugbridge.internal.AndroidDebugBridgeBindingConstants.*;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.core.thing.Thing;
|
||||
import org.openhab.core.thing.ThingTypeUID;
|
||||
import org.openhab.core.thing.binding.BaseThingHandlerFactory;
|
||||
import org.openhab.core.thing.binding.ThingHandler;
|
||||
import org.openhab.core.thing.binding.ThingHandlerFactory;
|
||||
import org.osgi.service.component.annotations.Component;
|
||||
|
||||
/**
|
||||
* The {@link AndroidDebugBridgeHandlerFactory} is responsible for creating things and thing
|
||||
* handlers.
|
||||
*
|
||||
* @author Miguel Álvarez - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
@Component(configurationPid = BINDING_CONFIGURATION_PID, service = ThingHandlerFactory.class)
|
||||
public class AndroidDebugBridgeHandlerFactory extends BaseThingHandlerFactory {
|
||||
|
||||
@Override
|
||||
public boolean supportsThingType(ThingTypeUID thingTypeUID) {
|
||||
return SUPPORTED_THING_TYPES.contains(thingTypeUID);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected @Nullable ThingHandler createHandler(Thing thing) {
|
||||
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
|
||||
if (THING_TYPE_ANDROID_DEVICE.equals(thingTypeUID)) {
|
||||
return new AndroidDebugBridgeHandler(thing);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<binding:binding id="androiddebugbridge" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:binding="https://openhab.org/schemas/binding/v1.0.0"
|
||||
xsi:schemaLocation="https://openhab.org/schemas/binding/v1.0.0 https://openhab.org/schemas/binding-1.0.0.xsd">
|
||||
|
||||
<name>Android Debug Bridge Binding</name>
|
||||
<description>This is the binding for connect to Android devices using the Android Debug Bridge protocol.</description>
|
||||
|
||||
<config-description>
|
||||
<parameter name="discoveryPort" type="integer" required="true">
|
||||
<label>Discovery Port</label>
|
||||
<description>Port used on discovery to connect to the device through adb.</description>
|
||||
<default>5555</default>
|
||||
</parameter>
|
||||
<parameter name="discoveryReachableMs" type="integer" required="true" unit="ms">
|
||||
<label>Discovery Reachable</label>
|
||||
<description>Milliseconds to wait while discovering to determine if the ip is reachable.</description>
|
||||
<default>3000</default>
|
||||
</parameter>
|
||||
<parameter name="discoveryIpRangeMin" type="integer" required="true" min="0" max="255">
|
||||
<label>Discovery Range IP Min</label>
|
||||
<description>Used to limit the numbers of ips checked while discovering.</description>
|
||||
<default>0</default>
|
||||
</parameter>
|
||||
<parameter name="discoveryIpRangeMax" type="integer" required="true" min="0" max="255">
|
||||
<label>Discovery Range IP Max</label>
|
||||
<description>Used to limit the numbers of ips checked while discovering.</description>
|
||||
<default>255</default>
|
||||
</parameter>
|
||||
</config-description>
|
||||
</binding:binding>
|
||||
@@ -0,0 +1,396 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<thing:thing-descriptions bindingId="androiddebugbridge"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
|
||||
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
|
||||
|
||||
<thing-type id="android">
|
||||
<label>Android Device Thing</label>
|
||||
<description>Android Device Thing for Android Debug Bridge Binding</description>
|
||||
<channels>
|
||||
<channel id="key-event" typeId="key-event-channel"/>
|
||||
<channel id="text" typeId="text-channel"/>
|
||||
<channel id="media-volume" typeId="system.volume"/>
|
||||
<channel id="media-control" typeId="system.media-control"/>
|
||||
<channel id="start-package" typeId="start-package-channel"/>
|
||||
<channel id="stop-package" typeId="stop-package-channel"/>
|
||||
<channel id="stop-current-package" typeId="stop-current-package-channel"/>
|
||||
<channel id="current-package" typeId="current-package-channel"/>
|
||||
<channel id="wake-lock" typeId="wake-lock-channel"/>
|
||||
<channel id="screen-state" typeId="screen-state-channel"/>
|
||||
</channels>
|
||||
<representation-property>serial</representation-property>
|
||||
<config-description>
|
||||
<parameter name="ip" type="text" required="true">
|
||||
<context>network-address</context>
|
||||
<label>IP Address</label>
|
||||
<description>Device ip address.</description>
|
||||
</parameter>
|
||||
<parameter name="port" type="integer" required="true" min="0" max="65535">
|
||||
<label>Port</label>
|
||||
<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">
|
||||
<label>Refresh Time</label>
|
||||
<description>Seconds between device status refreshes.</description>
|
||||
<default>30</default>
|
||||
</parameter>
|
||||
<parameter name="timeout" type="integer" min="1" max="15" unit="s" required="true">
|
||||
<label>Command Timeout</label>
|
||||
<description>Command timeout seconds.</description>
|
||||
<default>5</default>
|
||||
</parameter>
|
||||
<parameter name="mediaStateJSONConfig" type="text">
|
||||
<label>Media State Config</label>
|
||||
<description>JSON config that allows to modify the media state detection strategy for each app. Refer to the binding
|
||||
documentation.</description>
|
||||
</parameter>
|
||||
</config-description>
|
||||
</thing-type>
|
||||
|
||||
<channel-type id="key-event-channel">
|
||||
<item-type>String</item-type>
|
||||
<label>Send Key Event</label>
|
||||
<description>Send key event to android device</description>
|
||||
<state>
|
||||
<options>
|
||||
<option value="7">KEYCODE_0</option>
|
||||
<option value="8">KEYCODE_1</option>
|
||||
<option value="227">KEYCODE_11</option>
|
||||
<option value="228">KEYCODE_12</option>
|
||||
<option value="9">KEYCODE_2</option>
|
||||
<option value="10">KEYCODE_3</option>
|
||||
<option value="206">KEYCODE_3D_MODE</option>
|
||||
<option value="11">KEYCODE_4</option>
|
||||
<option value="12">KEYCODE_5</option>
|
||||
<option value="13">KEYCODE_6</option>
|
||||
<option value="14">KEYCODE_7</option>
|
||||
<option value="15">KEYCODE_8</option>
|
||||
<option value="16">KEYCODE_9</option>
|
||||
<option value="29">KEYCODE_A</option>
|
||||
<option value="284">KEYCODE_ALL_APPS</option>
|
||||
<option value="57">KEYCODE_ALT_LEFT</option>
|
||||
<option value="58">KEYCODE_ALT_RIGHT</option>
|
||||
<option value="75">KEYCODE_APOSTROPHE</option>
|
||||
<option value="187">KEYCODE_APP_SWITCH</option>
|
||||
<option value="219">KEYCODE_ASSIST</option>
|
||||
<option value="77">KEYCODE_AT</option>
|
||||
<option value="182">KEYCODE_AVR_INPUT</option>
|
||||
<option value="181">KEYCODE_AVR_POWER</option>
|
||||
<option value="30">KEYCODE_B</option>
|
||||
<option value="4">KEYCODE_BACK</option>
|
||||
<option value="73">KEYCODE_BACKSLASH</option>
|
||||
<option value="174">KEYCODE_BOOKMARK</option>
|
||||
<option value="121">KEYCODE_BREAK</option>
|
||||
<option value="220">KEYCODE_BRIGHTNESS_DOWN</option>
|
||||
<option value="221">KEYCODE_BRIGHTNESS_UP</option>
|
||||
<option value="188">KEYCODE_BUTTON_1</option>
|
||||
<option value="197">KEYCODE_BUTTON_10</option>
|
||||
<option value="198">KEYCODE_BUTTON_11</option>
|
||||
<option value="199">KEYCODE_BUTTON_12</option>
|
||||
<option value="200">KEYCODE_BUTTON_13</option>
|
||||
<option value="201">KEYCODE_BUTTON_14</option>
|
||||
<option value="202">KEYCODE_BUTTON_15</option>
|
||||
<option value="203">KEYCODE_BUTTON_16</option>
|
||||
<option value="189">KEYCODE_BUTTON_2</option>
|
||||
<option value="190">KEYCODE_BUTTON_3</option>
|
||||
<option value="191">KEYCODE_BUTTON_4</option>
|
||||
<option value="192">KEYCODE_BUTTON_5</option>
|
||||
<option value="193">KEYCODE_BUTTON_6</option>
|
||||
<option value="194">KEYCODE_BUTTON_7</option>
|
||||
<option value="195">KEYCODE_BUTTON_8</option>
|
||||
<option value="196">KEYCODE_BUTTON_9</option>
|
||||
<option value="96">KEYCODE_BUTTON_A</option>
|
||||
<option value="97">KEYCODE_BUTTON_B</option>
|
||||
<option value="98">KEYCODE_BUTTON_C</option>
|
||||
<option value="102">KEYCODE_BUTTON_L1</option>
|
||||
<option value="104">KEYCODE_BUTTON_L2</option>
|
||||
<option value="110">KEYCODE_BUTTON_MODE</option>
|
||||
<option value="103">KEYCODE_BUTTON_R1</option>
|
||||
<option value="105">KEYCODE_BUTTON_R2</option>
|
||||
<option value="109">KEYCODE_BUTTON_SELECT</option>
|
||||
<option value="108">KEYCODE_BUTTON_START</option>
|
||||
<option value="106">KEYCODE_BUTTON_THUMBL</option>
|
||||
<option value="107">KEYCODE_BUTTON_THUMBR</option>
|
||||
<option value="99">KEYCODE_BUTTON_X</option>
|
||||
<option value="100">KEYCODE_BUTTON_Y</option>
|
||||
<option value="101">KEYCODE_BUTTON_Z</option>
|
||||
<option value="31">KEYCODE_C</option>
|
||||
<option value="210">KEYCODE_CALCULATOR</option>
|
||||
<option value="208">KEYCODE_CALENDAR</option>
|
||||
<option value="5">KEYCODE_CALL</option>
|
||||
<option value="27">KEYCODE_CAMERA</option>
|
||||
<option value="115">KEYCODE_CAPS_LOCK</option>
|
||||
<option value="175">KEYCODE_CAPTIONS</option>
|
||||
<option value="167">KEYCODE_CHANNEL_DOWN</option>
|
||||
<option value="166">KEYCODE_CHANNEL_UP</option>
|
||||
<option value="28">KEYCODE_CLEAR</option>
|
||||
<option value="55">KEYCODE_COMMA</option>
|
||||
<option value="207">KEYCODE_CONTACTS</option>
|
||||
<option value="278">KEYCODE_COPY</option>
|
||||
<option value="113">KEYCODE_CTRL_LEFT</option>
|
||||
<option value="114">KEYCODE_CTRL_RIGHT</option>
|
||||
<option value="277">KEYCODE_CUT</option>
|
||||
<option value="32">KEYCODE_D</option>
|
||||
<option value="67">KEYCODE_DEL</option>
|
||||
<option value="23">KEYCODE_DPAD_CENTER</option>
|
||||
<option value="20">KEYCODE_DPAD_DOWN</option>
|
||||
<option value="269">KEYCODE_DPAD_DOWN_LEFT</option>
|
||||
<option value="271">KEYCODE_DPAD_DOWN_RIGHT</option>
|
||||
<option value="21">KEYCODE_DPAD_LEFT</option>
|
||||
<option value="22">KEYCODE_DPAD_RIGHT</option>
|
||||
<option value="19">KEYCODE_DPAD_UP</option>
|
||||
<option value="268">KEYCODE_DPAD_UP_LEFT</option>
|
||||
<option value="270">KEYCODE_DPAD_UP_RIGHT</option>
|
||||
<option value="173">KEYCODE_DVR</option>
|
||||
<option value="33">KEYCODE_E</option>
|
||||
<option value="212">KEYCODE_EISU</option>
|
||||
<option value="6">KEYCODE_ENDCALL</option>
|
||||
<option value="66">KEYCODE_ENTER</option>
|
||||
<option value="65">KEYCODE_ENVELOPE</option>
|
||||
<option value="70">KEYCODE_EQUALS</option>
|
||||
<option value="111">KEYCODE_ESCAPE</option>
|
||||
<option value="64">KEYCODE_EXPLORER</option>
|
||||
<option value="34">KEYCODE_F</option>
|
||||
<option value="131">KEYCODE_F1</option>
|
||||
<option value="140">KEYCODE_F10</option>
|
||||
<option value="141">KEYCODE_F11</option>
|
||||
<option value="142">KEYCODE_F12</option>
|
||||
<option value="132">KEYCODE_F2</option>
|
||||
<option value="133">KEYCODE_F3</option>
|
||||
<option value="134">KEYCODE_F4</option>
|
||||
<option value="135">KEYCODE_F5</option>
|
||||
<option value="136">KEYCODE_F6</option>
|
||||
<option value="137">KEYCODE_F7</option>
|
||||
<option value="138">KEYCODE_F8</option>
|
||||
<option value="139">KEYCODE_F9</option>
|
||||
<option value="80">KEYCODE_FOCUS</option>
|
||||
<option value="125">KEYCODE_FORWARD</option>
|
||||
<option value="112">KEYCODE_FORWARD_DEL</option>
|
||||
<option value="119">KEYCODE_FUNCTION</option>
|
||||
<option value="35">KEYCODE_G</option>
|
||||
<option value="68">KEYCODE_GRAVE</option>
|
||||
<option value="172">KEYCODE_GUIDE</option>
|
||||
<option value="36">KEYCODE_H</option>
|
||||
<option value="79">KEYCODE_HEADSETHOOK</option>
|
||||
<option value="259">KEYCODE_HELP</option>
|
||||
<option value="214">KEYCODE_HENKAN</option>
|
||||
<option value="3">KEYCODE_HOME</option>
|
||||
<option value="37">KEYCODE_I</option>
|
||||
<option value="165">KEYCODE_INFO</option>
|
||||
<option value="124">KEYCODE_INSERT</option>
|
||||
<option value="38">KEYCODE_J</option>
|
||||
<option value="39">KEYCODE_K</option>
|
||||
<option value="218">KEYCODE_KANA</option>
|
||||
<option value="215">KEYCODE_KATAKANA_HIRAGANA</option>
|
||||
<option value="40">KEYCODE_L</option>
|
||||
<option value="204">KEYCODE_LANGUAGE_SWITCH</option>
|
||||
<option value="229">KEYCODE_LAST_CHANNEL</option>
|
||||
<option value="71">KEYCODE_LEFT_BRACKET</option>
|
||||
<option value="41">KEYCODE_M</option>
|
||||
<option value="205">KEYCODE_MANNER_MODE</option>
|
||||
<option value="222">KEYCODE_MEDIA_AUDIO_TRACK</option>
|
||||
<option value="128">KEYCODE_MEDIA_CLOSE</option>
|
||||
<option value="129">KEYCODE_MEDIA_EJECT</option>
|
||||
<option value="90">KEYCODE_MEDIA_FAST_FORWARD</option>
|
||||
<option value="87">KEYCODE_MEDIA_NEXT</option>
|
||||
<option value="127">KEYCODE_MEDIA_PAUSE</option>
|
||||
<option value="126">KEYCODE_MEDIA_PLAY</option>
|
||||
<option value="85">KEYCODE_MEDIA_PLAY_PAUSE</option>
|
||||
<option value="88">KEYCODE_MEDIA_PREVIOUS</option>
|
||||
<option value="130">KEYCODE_MEDIA_RECORD</option>
|
||||
<option value="89">KEYCODE_MEDIA_REWIND</option>
|
||||
<option value="273">KEYCODE_MEDIA_SKIP_BACKWARD</option>
|
||||
<option value="272">KEYCODE_MEDIA_SKIP_FORWARD</option>
|
||||
<option value="275">KEYCODE_MEDIA_STEP_BACKWARD</option>
|
||||
<option value="274">KEYCODE_MEDIA_STEP_FORWARD</option>
|
||||
<option value="86">KEYCODE_MEDIA_STOP</option>
|
||||
<option value="226">KEYCODE_MEDIA_TOP_MENU</option>
|
||||
<option value="82">KEYCODE_MENU</option>
|
||||
<option value="117">KEYCODE_META_LEFT</option>
|
||||
<option value="118">KEYCODE_META_RIGHT</option>
|
||||
<option value="69">KEYCODE_MINUS</option>
|
||||
<option value="123">KEYCODE_MOVE_END</option>
|
||||
<option value="122">KEYCODE_MOVE_HOME</option>
|
||||
<option value="213">KEYCODE_MUHENKAN</option>
|
||||
<option value="209">KEYCODE_MUSIC</option>
|
||||
<option value="91">KEYCODE_MUTE</option>
|
||||
<option value="42">KEYCODE_N</option>
|
||||
<option value="262">KEYCODE_NAVIGATE_IN</option>
|
||||
<option value="261">KEYCODE_NAVIGATE_NEXT</option>
|
||||
<option value="263">KEYCODE_NAVIGATE_OUT</option>
|
||||
<option value="260">KEYCODE_NAVIGATE_PREVIOUS</option>
|
||||
<option value="83">KEYCODE_NOTIFICATION</option>
|
||||
<option value="78">KEYCODE_NUM</option>
|
||||
<option value="144">KEYCODE_NUMPAD_0</option>
|
||||
<option value="145">KEYCODE_NUMPAD_1</option>
|
||||
<option value="146">KEYCODE_NUMPAD_2</option>
|
||||
<option value="147">KEYCODE_NUMPAD_3</option>
|
||||
<option value="148">KEYCODE_NUMPAD_4</option>
|
||||
<option value="149">KEYCODE_NUMPAD_5</option>
|
||||
<option value="150">KEYCODE_NUMPAD_6</option>
|
||||
<option value="151">KEYCODE_NUMPAD_7</option>
|
||||
<option value="152">KEYCODE_NUMPAD_8</option>
|
||||
<option value="153">KEYCODE_NUMPAD_9</option>
|
||||
<option value="157">KEYCODE_NUMPAD_ADD</option>
|
||||
<option value="159">KEYCODE_NUMPAD_COMMA</option>
|
||||
<option value="154">KEYCODE_NUMPAD_DIVIDE</option>
|
||||
<option value="158">KEYCODE_NUMPAD_DOT</option>
|
||||
<option value="160">KEYCODE_NUMPAD_ENTER</option>
|
||||
<option value="161">KEYCODE_NUMPAD_EQUALS</option>
|
||||
<option value="162">KEYCODE_NUMPAD_LEFT_PAREN</option>
|
||||
<option value="155">KEYCODE_NUMPAD_MULTIPLY</option>
|
||||
<option value="163">KEYCODE_NUMPAD_RIGHT_PAREN</option>
|
||||
<option value="156">KEYCODE_NUMPAD_SUBTRACT</option>
|
||||
<option value="143">KEYCODE_NUM_LOCK</option>
|
||||
<option value="43">KEYCODE_O</option>
|
||||
<option value="44">KEYCODE_P</option>
|
||||
<option value="93">KEYCODE_PAGE_DOWN</option>
|
||||
<option value="92">KEYCODE_PAGE_UP</option>
|
||||
<option value="225">KEYCODE_PAIRING</option>
|
||||
<option value="279">KEYCODE_PASTE</option>
|
||||
<option value="56">KEYCODE_PERIOD</option>
|
||||
<option value="94">KEYCODE_PICTSYMBOLS</option>
|
||||
<option value="81">KEYCODE_PLUS</option>
|
||||
<option value="18">KEYCODE_POUND</option>
|
||||
<option value="26">KEYCODE_POWER</option>
|
||||
<option value="288">KEYCODE_PROFILE_SWITCH</option>
|
||||
<option value="186">KEYCODE_PROG_BLUE</option>
|
||||
<option value="184">KEYCODE_PROG_GREEN</option>
|
||||
<option value="183">KEYCODE_PROG_RED</option>
|
||||
<option value="185">KEYCODE_PROG_YELLOW</option>
|
||||
<option value="45">KEYCODE_Q</option>
|
||||
<option value="46">KEYCODE_R</option>
|
||||
<option value="285">KEYCODE_REFRESH</option>
|
||||
<option value="72">KEYCODE_RIGHT_BRACKET</option>
|
||||
<option value="217">KEYCODE_RO</option>
|
||||
<option value="47">KEYCODE_S</option>
|
||||
<option value="116">KEYCODE_SCROLL_LOCK</option>
|
||||
<option value="84">KEYCODE_SEARCH</option>
|
||||
<option value="74">KEYCODE_SEMICOLON</option>
|
||||
<option value="176">KEYCODE_SETTINGS</option>
|
||||
<option value="59">KEYCODE_SHIFT_LEFT</option>
|
||||
<option value="60">KEYCODE_SHIFT_RIGHT</option>
|
||||
<option value="76">KEYCODE_SLASH</option>
|
||||
<option value="223">KEYCODE_SLEEP</option>
|
||||
<option value="1">KEYCODE_SOFT_LEFT</option>
|
||||
<option value="2">KEYCODE_SOFT_RIGHT</option>
|
||||
<option value="276">KEYCODE_SOFT_SLEEP</option>
|
||||
<option value="62">KEYCODE_SPACE</option>
|
||||
<option value="17">KEYCODE_STAR</option>
|
||||
<option value="180">KEYCODE_STB_INPUT</option>
|
||||
<option value="179">KEYCODE_STB_POWER</option>
|
||||
<option value="265">KEYCODE_STEM_1</option>
|
||||
<option value="266">KEYCODE_STEM_2</option>
|
||||
<option value="267">KEYCODE_STEM_3</option>
|
||||
<option value="264">KEYCODE_STEM_PRIMARY</option>
|
||||
<option value="95">KEYCODE_SWITCH_CHARSET</option>
|
||||
<option value="63">KEYCODE_SYM</option>
|
||||
<option value="120">KEYCODE_SYSRQ</option>
|
||||
<option value="281">KEYCODE_SYSTEM_NAVIGATION_DOWN</option>
|
||||
<option value="282">KEYCODE_SYSTEM_NAVIGATION_LEFT</option>
|
||||
<option value="283">KEYCODE_SYSTEM_NAVIGATION_RIGHT</option>
|
||||
<option value="280">KEYCODE_SYSTEM_NAVIGATION_UP</option>
|
||||
<option value="48">KEYCODE_T</option>
|
||||
<option value="61">KEYCODE_TAB</option>
|
||||
<option value="287">KEYCODE_THUMBS_DOWN</option>
|
||||
<option value="286">KEYCODE_THUMBS_UP</option>
|
||||
<option value="170">KEYCODE_TV</option>
|
||||
<option value="242">KEYCODE_TV_ANTENNA_CABLE</option>
|
||||
<option value="252">KEYCODE_TV_AUDIO_DESCRIPTION</option>
|
||||
<option value="254">KEYCODE_TV_AUDIO_DESCRIPTION_MIX_DOWN</option>
|
||||
<option value="253">KEYCODE_TV_AUDIO_DESCRIPTION_MIX_UP</option>
|
||||
<option value="256">KEYCODE_TV_CONTENTS_MENU</option>
|
||||
<option value="230">KEYCODE_TV_DATA_SERVICE</option>
|
||||
<option value="178">KEYCODE_TV_INPUT</option>
|
||||
<option value="249">KEYCODE_TV_INPUT_COMPONENT_1</option>
|
||||
<option value="250">KEYCODE_TV_INPUT_COMPONENT_2</option>
|
||||
<option value="247">KEYCODE_TV_INPUT_COMPOSITE_1</option>
|
||||
<option value="248">KEYCODE_TV_INPUT_COMPOSITE_2</option>
|
||||
<option value="243">KEYCODE_TV_INPUT_HDMI_1</option>
|
||||
<option value="244">KEYCODE_TV_INPUT_HDMI_2</option>
|
||||
<option value="245">KEYCODE_TV_INPUT_HDMI_3</option>
|
||||
<option value="246">KEYCODE_TV_INPUT_HDMI_4</option>
|
||||
<option value="251">KEYCODE_TV_INPUT_VGA_1</option>
|
||||
<option value="257">KEYCODE_TV_MEDIA_CONTEXT_MENU</option>
|
||||
<option value="241">KEYCODE_TV_NETWORK</option>
|
||||
<option value="234">KEYCODE_TV_NUMBER_ENTRY</option>
|
||||
<option value="177">KEYCODE_TV_POWER</option>
|
||||
<option value="232">KEYCODE_TV_RADIO_SERVICE</option>
|
||||
<option value="237">KEYCODE_TV_SATELLITE</option>
|
||||
<option value="238">KEYCODE_TV_SATELLITE_BS</option>
|
||||
<option value="239">KEYCODE_TV_SATELLITE_CS</option>
|
||||
<option value="240">KEYCODE_TV_SATELLITE_SERVICE</option>
|
||||
<option value="233">KEYCODE_TV_TELETEXT</option>
|
||||
<option value="235">KEYCODE_TV_TERRESTRIAL_ANALOG</option>
|
||||
<option value="236">KEYCODE_TV_TERRESTRIAL_DIGITAL</option>
|
||||
<option value="258">KEYCODE_TV_TIMER_PROGRAMMING</option>
|
||||
<option value="255">KEYCODE_TV_ZOOM_MODE</option>
|
||||
<option value="49">KEYCODE_U</option>
|
||||
<option value="0">KEYCODE_UNKNOWN</option>
|
||||
<option value="50">KEYCODE_V</option>
|
||||
<option value="231">KEYCODE_VOICE_ASSIST</option>
|
||||
<option value="25">KEYCODE_VOLUME_DOWN</option>
|
||||
<option value="164">KEYCODE_VOLUME_MUTE</option>
|
||||
<option value="24">KEYCODE_VOLUME_UP</option>
|
||||
<option value="51">KEYCODE_W</option>
|
||||
<option value="224">KEYCODE_WAKEUP</option>
|
||||
<option value="171">KEYCODE_WINDOW</option>
|
||||
<option value="52">KEYCODE_X</option>
|
||||
<option value="53">KEYCODE_Y</option>
|
||||
<option value="216">KEYCODE_YEN</option>
|
||||
<option value="54">KEYCODE_Z</option>
|
||||
<option value="211">KEYCODE_ZENKAKU_HANKAKU</option>
|
||||
<option value="168">KEYCODE_ZOOM_IN</option>
|
||||
<option value="16">KEYCODE_ZOOM_OUT</option>
|
||||
</options>
|
||||
</state>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="text-channel">
|
||||
<item-type>String</item-type>
|
||||
<label>Send Text</label>
|
||||
<description>Send text to android device</description>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="start-package-channel">
|
||||
<item-type>String</item-type>
|
||||
<label>Start Package</label>
|
||||
<description>Run application by package name</description>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="stop-package-channel">
|
||||
<item-type>String</item-type>
|
||||
<label>Stop Package</label>
|
||||
<description>Stop application by package name</description>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="stop-current-package-channel">
|
||||
<item-type>Switch</item-type>
|
||||
<label>Stop Current Package</label>
|
||||
<description>Stops the top application in screen when receives an OFF command</description>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="current-package-channel">
|
||||
<item-type>String</item-type>
|
||||
<label>Current Package</label>
|
||||
<description>Package name of the top application in screen</description>
|
||||
<state readOnly="true"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="wake-lock-channel" advanced="true">
|
||||
<item-type>Number</item-type>
|
||||
<label>Wake Lock</label>
|
||||
<description>Power Wake Lock State</description>
|
||||
<state readOnly="true"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="screen-state-channel" advanced="true">
|
||||
<item-type>Switch</item-type>
|
||||
<label>Screen State</label>
|
||||
<description>Screen Power State</description>
|
||||
<state readOnly="true"/>
|
||||
</channel-type>
|
||||
|
||||
</thing:thing-descriptions>
|
||||
Reference in New Issue
Block a user