[unifiedremote] Initial contribution (#8546)

Signed-off-by: GiviMAD <miguelwork92@gmail.com>
This commit is contained in:
GiviMAD 2020-10-24 19:00:25 +02:00 committed by GitHub
parent cb5d8711b8
commit 8b8b79cf04
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 936 additions and 0 deletions

View File

@ -246,6 +246,7 @@
/bundles/org.openhab.binding.tplinksmarthome/ @Hilbrand
/bundles/org.openhab.binding.tradfri/ @cweitkamp @kaikreuzer
/bundles/org.openhab.binding.unifi/ @mgbowman
/bundles/org.openhab.binding.unifiedremote/ @GiviMAD
/bundles/org.openhab.binding.upb/ @marcusb
/bundles/org.openhab.binding.upnpcontrol/ @mherwege
/bundles/org.openhab.binding.urtsi/ @OLibutzki

View File

@ -1216,6 +1216,11 @@
<artifactId>org.openhab.binding.unifi</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.binding.unifiedremote</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.binding.upb</artifactId>

View File

@ -0,0 +1,13 @@
This content is produced and maintained by the openHAB project.
* Project home: https://www.openhab.org
== Declared Project Licenses
This program and the accompanying materials are made available under the terms
of the Eclipse Public License 2.0 which is available at
https://www.eclipse.org/legal/epl-2.0/.
== Source Code
https://github.com/openhab/openhab-addons

View File

@ -0,0 +1,49 @@
# UnifiedRemote Binding
This binding integrates the [Unified Remote Server](https://www.unifiedremote.com/).
<b>Known Limitations: It needs the web interface to be enabled on the server settings to work.</b>
## Discovery
Discovery works on the default discovery UDP port 9511.
## Thing Configuration
Only supported thing is 'Unified Remote Server Thing' which requires the Hostname to be correctly configured in order to work.
| ThinTypeID | description |
|----------|------------------------------|
| server | Unified Remote Server Thing |
| Config | Type | description |
|----------|----------|------------------------------|
| host | String | Unified Remote Server IP |
## Channels
| channel | type | description |
|----------|--------|------------------------------|
| mouse-move | String | Relative mouse move in pixels. Expect number JSON array [x,y] ("[10,10]"). |
| send-key | String | Use server key. Supported keys are: LEFT_CLICK, RIGHT_CLICK, LOCK, UNLOCK, SLEEP, SHUTDOWN, RESTART, LOGOFF, PLAY, PLAY, PAUSE, NEXT, PREVIOUS, STOP, VOLUME_MUTE, VOLUME_UP, VOLUME_DOWN, BRIGHTNESS_UP, BRIGHTNESS_DOWN, MONITOR_OFF, MONITOR_ON, ESCAPE, SPACE, BACK, LWIN, CONTROL, TAB, MENU, RETURN, UP, DOWN, LEFT, RIGHT |
## Full Example
### Sample Thing
```
Thing unifiedremote:server:xx-xx-xx-xx-xx-xx [ host="192.168.1.10" ]
```
### Sample Items
```
Group pcRemote "Living room PC"
String PC_SendKey "Send Key" (pcRemote) { channel="unifiedremote:server:xx-xx-xx-xx-xx-xx:send-key" }
String PC_MouseMove "Mouse Move" (pcRemote) { channel="samsungtv:tv:livingroom:mouse-move" }
```

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.addons.reactor.bundles</artifactId>
<version>3.0.0-SNAPSHOT</version>
</parent>
<artifactId>org.openhab.binding.unifiedremote</artifactId>
<name>openHAB Add-ons :: Bundles :: UnifiedRemote Binding</name>
</project>

View File

@ -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.unifiedremote-${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-unifiedremote" description="UnifiedRemote Binding" version="${project.version}">
<feature>openhab-runtime-base</feature>
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.unifiedremote/${project.version}</bundle>
</feature>
</features>

View File

@ -0,0 +1,46 @@
/**
* 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
*/
package org.openhab.binding.unifiedremote.internal;
import java.util.Collections;
import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.thing.ThingTypeUID;
/**
* The {@link UnifiedRemoteBindingConstants} class defines common constants, which are
* used across the whole binding.
*
* @author Miguel Álvarez - Initial contribution
*/
@NonNullByDefault
public class UnifiedRemoteBindingConstants {
private static final String BINDING_ID = "unifiedremote";
// List of all Thing Type UIDs
public static final ThingTypeUID THING_TYPE_UNIFIED_REMOTE_SERVER = new ThingTypeUID(BINDING_ID, "server");
public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Collections
.singleton(THING_TYPE_UNIFIED_REMOTE_SERVER);
// List of all Channel ids
public static final String MOUSE_CHANNEL = "mouse-move";
public static final String SEND_KEY_CHANNEL = "send-key";
// List of all Parameters
public static final String PARAMETER_MAC_ADDRESS = "macAddress";
public static final String PARAMETER_HOSTNAME = "host";
public static final String PARAMETER_TCP_PORT = "udpPort";
public static final String PARAMETER_UDP_PORT = "tcpPort";
}

View File

@ -0,0 +1,27 @@
/**
* 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
*/
package org.openhab.binding.unifiedremote.internal;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link UnifiedRemoteConfiguration} class contains fields mapping thing configuration parameters.
*
* @author Miguel Álvarez - Initial contribution
*/
@NonNullByDefault
public class UnifiedRemoteConfiguration {
public String host = "";
public int tcpPort;
public int udpPort;
}

View File

@ -0,0 +1,266 @@
/**
* 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
*/
package org.openhab.binding.unifiedremote.internal;
import java.util.UUID;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.api.ContentResponse;
import org.eclipse.jetty.client.api.Request;
import org.eclipse.jetty.client.util.StringContentProvider;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpMethod;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
/**
* The {@link UnifiedRemoteConnection} Handles Remote Server Communications
*
* @author Miguel Alvarez - Initial contribution
*/
@NonNullByDefault
public class UnifiedRemoteConnection {
private static final int WEB_CLIENT_PORT = 9510;
private static final int TIMEOUT_SEC = 10;
private static final String CONNECTION_ID_HEADER = "UR-Connection-ID";
private static final String MOUSE_REMOTE = "Relmtech.Basic Input";
private static final String NAVIGATION_REMOTE = "Unified.Navigation";
private static final String POWER_REMOTE = "Unified.Power";
private static final String MEDIA_REMOTE = "Unified.Media";
private static final String MONITOR_REMOTE = "Unified.Monitor";
private Logger logger = LoggerFactory.getLogger(UnifiedRemoteConnection.class);
private final String url;
private final JsonParser jsonParser = new JsonParser();
private HttpClient httpClient;
private @Nullable String connectionID;
private @Nullable String connectionGUID;
public UnifiedRemoteConnection(HttpClient httpClient, String host) {
this.httpClient = httpClient;
url = "http://" + host + ":" + WEB_CLIENT_PORT + "/client/";
}
public void authenticate() throws InterruptedException, ExecutionException, TimeoutException {
ContentResponse response = null;
connectionGUID = "web-" + UUID.randomUUID().toString();
response = httpClient.newRequest(getPath("connect")).method(HttpMethod.GET)
.timeout(TIMEOUT_SEC, TimeUnit.SECONDS).send();
JsonObject responseBody = jsonParser.parse(response.getContentAsString()).getAsJsonObject();
connectionID = responseBody.get("id").getAsString();
String password = UUID.randomUUID().toString();
JsonObject authPayload = new JsonObject();
authPayload.addProperty("Action", 0);
authPayload.addProperty("Request", 0);
authPayload.addProperty("Version", 10);
authPayload.addProperty("Password", password);
authPayload.addProperty("Platform", "web");
authPayload.addProperty("Source", connectionGUID);
request(authPayload);
JsonObject capabilitiesPayload = new JsonObject();
JsonObject capabilitiesInnerPayload = new JsonObject();
capabilitiesInnerPayload.addProperty("Actions", true);
capabilitiesInnerPayload.addProperty("Sync", true);
capabilitiesInnerPayload.addProperty("Grid", true);
capabilitiesInnerPayload.addProperty("Fast", false);
capabilitiesInnerPayload.addProperty("Loading", true);
capabilitiesInnerPayload.addProperty("Encryption2", true);
capabilitiesPayload.add("Capabilities", capabilitiesInnerPayload);
capabilitiesPayload.addProperty("Action", 1);
capabilitiesPayload.addProperty("Request", 1);
capabilitiesPayload.addProperty("Source", connectionGUID);
request(capabilitiesPayload);
}
public ContentResponse mouseMove(String jsonIntArray)
throws InterruptedException, ExecutionException, TimeoutException {
JsonArray cordinates = jsonParser.parse(jsonIntArray).getAsJsonArray();
int x = cordinates.get(0).getAsInt();
int y = cordinates.get(1).getAsInt();
return this.execRemoteAction("Relmtech.Basic Input", "delta",
wrapValues(new String[] { "0", Integer.toString(x), Integer.toString(y) }));
}
public ContentResponse sendKey(String key) throws InterruptedException, ExecutionException, TimeoutException {
String remoteID = "";
String actionName = "";
String value = null;
switch (key) {
case "LEFT_CLICK":
remoteID = MOUSE_REMOTE;
actionName = "left";
break;
case "RIGHT_CLICK":
remoteID = MOUSE_REMOTE;
actionName = "right";
break;
case "LOCK":
remoteID = POWER_REMOTE;
actionName = "lock";
break;
case "UNLOCK":
remoteID = POWER_REMOTE;
actionName = "unlock";
break;
case "SLEEP":
remoteID = POWER_REMOTE;
actionName = "sleep";
break;
case "SHUTDOWN":
remoteID = POWER_REMOTE;
actionName = "shutdown";
break;
case "RESTART":
remoteID = POWER_REMOTE;
actionName = "restart";
break;
case "LOGOFF":
remoteID = POWER_REMOTE;
actionName = "logoff";
break;
case "PLAY/PAUSE":
case "PLAY":
case "PAUSE":
remoteID = MEDIA_REMOTE;
actionName = "play_pause";
break;
case "NEXT":
remoteID = MEDIA_REMOTE;
actionName = "next";
break;
case "PREVIOUS":
remoteID = MEDIA_REMOTE;
actionName = "previous";
break;
case "STOP":
remoteID = MEDIA_REMOTE;
actionName = "stop";
break;
case "VOLUME_MUTE":
remoteID = MEDIA_REMOTE;
actionName = "volume_mute";
break;
case "VOLUME_UP":
remoteID = MEDIA_REMOTE;
actionName = "volume_up";
break;
case "VOLUME_DOWN":
remoteID = MEDIA_REMOTE;
actionName = "volume_down";
break;
case "BRIGHTNESS_UP":
remoteID = MONITOR_REMOTE;
actionName = "brightness_up";
break;
case "BRIGHTNESS_DOWN":
remoteID = MONITOR_REMOTE;
actionName = "brightness_down";
break;
case "MONITOR_OFF":
remoteID = MONITOR_REMOTE;
actionName = "turn_off";
break;
case "MONITOR_ON":
remoteID = MONITOR_REMOTE;
actionName = "turn_on";
break;
case "ESCAPE":
case "SPACE":
case "BACK":
case "LWIN":
case "CONTROL":
case "TAB":
case "MENU":
case "RETURN":
case "UP":
case "DOWN":
case "LEFT":
case "RIGHT":
remoteID = NAVIGATION_REMOTE;
actionName = "toggle";
value = key;
break;
}
JsonArray wrappedValues = null;
if (value != null) {
wrappedValues = wrapValues(new String[] { value });
}
return this.execRemoteAction(remoteID, actionName, wrappedValues);
}
public ContentResponse keepAlive() throws InterruptedException, ExecutionException, TimeoutException {
JsonObject payload = new JsonObject();
payload.addProperty("KeepAlive", true);
payload.addProperty("Source", connectionGUID);
return request(payload);
}
private ContentResponse execRemoteAction(String remoteID, String name, @Nullable JsonElement values)
throws InterruptedException, ExecutionException, TimeoutException {
JsonObject payload = new JsonObject();
JsonObject runInnerPayload = new JsonObject();
JsonObject extrasInnerPayload = new JsonObject();
if (values != null) {
extrasInnerPayload.add("Values", values);
runInnerPayload.add("Extras", extrasInnerPayload);
}
runInnerPayload.addProperty("Name", name);
payload.addProperty("ID", remoteID);
payload.addProperty("Action", 7);
payload.addProperty("Request", 7);
payload.add("Run", runInnerPayload);
payload.addProperty("Source", connectionGUID);
return request(payload);
}
private ContentResponse request(JsonObject content)
throws InterruptedException, ExecutionException, TimeoutException {
Request request = httpClient.newRequest(getPath("request")).method(HttpMethod.POST).timeout(TIMEOUT_SEC,
TimeUnit.SECONDS);
request.header(HttpHeader.CONTENT_TYPE, "application/json");
if (connectionID != null)
request.header(CONNECTION_ID_HEADER, connectionID);
String stringContent = content.toString();
logger.debug("[Request Payload {} ]", stringContent);
request.content(new StringContentProvider(stringContent, "utf-8"));
return request.send();
}
private JsonArray wrapValues(String[] commandValues) {
JsonArray values = new JsonArray();
for (String value : commandValues) {
JsonObject valueWrapper = new JsonObject();
valueWrapper.addProperty("Value", value);
values.add(valueWrapper);
}
return values;
}
private String getPath(String path) {
return url + path;
}
}

View File

@ -0,0 +1,183 @@
/**
* 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
*/
package org.openhab.binding.unifiedremote.internal;
import static org.openhab.binding.unifiedremote.internal.UnifiedRemoteBindingConstants.*;
import java.io.IOException;
import java.net.*;
import java.text.ParseException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import org.eclipse.jdt.annotation.NonNullByDefault;
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.ThingUID;
import org.osgi.service.component.annotations.Component;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link UnifiedRemoteDiscoveryService} discover Unified Remote Server Instances in the network.
*
* @author Miguel Alvarez - Initial contribution
*/
@Component(service = DiscoveryService.class, configurationPid = "discovery.unifiedremote")
@NonNullByDefault
public class UnifiedRemoteDiscoveryService extends AbstractDiscoveryService {
private Logger logger = LoggerFactory.getLogger(UnifiedRemoteDiscoveryService.class);
static final int TIMEOUT_MS = 20000;
private static final long DISCOVERY_RESULT_TTL_SEC = TimeUnit.MINUTES.toSeconds(5);
/**
* Port used for broadcast and listening.
*/
public static final int DISCOVERY_PORT = 9511;
/**
* String the client sends, to disambiguate packets on this port.
*/
public static final String DISCOVERY_REQUEST = "6N T|-Ar-A6N T|-Ar-A6N T|-Ar-A";
/**
* String the client sends, to disambiguate packets on this port.
*/
public static final String DISCOVERY_RESPONSE_PREFIX = ")-b@ h): :)i)-b@ h): :)i)-b@ h): :)";
/**
* String used to replace non printable characters on service response
*/
public static final String NON_PRINTABLE_CHARTS_REPLACEMENT = ": :";
private static final int MAX_PACKET_SIZE = 2048;
/**
* maximum time to wait for a reply, in milliseconds.
*/
private static final int SOCKET_TIMEOUT_MS = 3000;
public UnifiedRemoteDiscoveryService() {
super(SUPPORTED_THING_TYPES, TIMEOUT_MS, false);
}
@Override
protected void startScan() {
sendBroadcast(this::addNewServer);
}
private void addNewServer(ServerInfo serverInfo) {
Map<String, Object> properties = new HashMap<>();
properties.put(PARAMETER_MAC_ADDRESS, serverInfo.macAddress);
properties.put(PARAMETER_HOSTNAME, serverInfo.host);
properties.put(PARAMETER_TCP_PORT, serverInfo.tcpPort);
properties.put(PARAMETER_UDP_PORT, serverInfo.udpPort);
thingDiscovered(
DiscoveryResultBuilder.create(new ThingUID(THING_TYPE_UNIFIED_REMOTE_SERVER, serverInfo.macAddress))
.withTTL(DISCOVERY_RESULT_TTL_SEC).withRepresentationProperty(PARAMETER_MAC_ADDRESS)
.withProperties(properties).withLabel(serverInfo.name).build());
}
/**
* Create a UDP socket on the service discovery broadcast port.
*
* @return open DatagramSocket if successful
* @throws RuntimeException if cannot create the socket
*/
public DatagramSocket createSocket() throws SocketException {
DatagramSocket socket;
socket = new DatagramSocket();
socket.setBroadcast(true);
socket.setSoTimeout(TIMEOUT_MS);
return socket;
}
private ServerInfo tryParseServerDiscovery(DatagramPacket receivePacket) throws ParseException {
String host = receivePacket.getAddress().getHostAddress();
String reply = new String(receivePacket.getData()).replaceAll("[\\p{C}]", NON_PRINTABLE_CHARTS_REPLACEMENT)
.replaceAll("[^\\x00-\\x7F]", NON_PRINTABLE_CHARTS_REPLACEMENT);
if (!reply.startsWith(DISCOVERY_RESPONSE_PREFIX))
throw new ParseException("Bad discovery response prefix", 0);
String[] parts = Arrays
.stream(reply.replace(DISCOVERY_RESPONSE_PREFIX, "").split(NON_PRINTABLE_CHARTS_REPLACEMENT))
.filter((String e) -> e.length() != 0).toArray(String[]::new);
String name = parts[0];
int tcpPort = Integer.parseInt(parts[1]);
int udpPort = Integer.parseInt(parts[3]);
String macAddress = parts[2];
return new ServerInfo(host, tcpPort, udpPort, name, macAddress);
}
/**
* Send broadcast packets with service request string until a response
* is received. Return the response as String (even though it should
* contain an internet address).
*
* @return String received from server. Should be server IP address.
* Returns empty string if failed to get valid reply.
*/
public void sendBroadcast(Consumer<ServerInfo> listener) {
byte[] receiveBuffer = new byte[MAX_PACKET_SIZE];
DatagramPacket receivePacket = new DatagramPacket(receiveBuffer, receiveBuffer.length);
DatagramSocket socket = null;
try {
socket = createSocket();
} catch (SocketException e) {
logger.debug("Error creating discovery socket: {}", e.getMessage());
return;
}
byte[] packetData = DISCOVERY_REQUEST.getBytes();
try {
InetAddress broadcastAddress = InetAddress.getByName("255.255.255.255");
int servicePort = DISCOVERY_PORT;
DatagramPacket packet = new DatagramPacket(packetData, packetData.length, broadcastAddress, servicePort);
socket.send(packet);
logger.debug("Sent packet to {}:{}", broadcastAddress.getHostAddress(), servicePort);
for (int i = 0; i < 20; i++) {
socket.receive(receivePacket);
String host = receivePacket.getAddress().getHostAddress();
logger.debug("Received reply from {}", host);
try {
ServerInfo serverInfo = tryParseServerDiscovery(receivePacket);
listener.accept(serverInfo);
} catch (ParseException ex) {
logger.debug("Unable to parse server discovery response from {}: {}", host, ex.getMessage());
}
}
} catch (SocketTimeoutException ste) {
logger.debug("SocketTimeoutException during socket operation: {}", ste.getMessage());
} catch (IOException ioe) {
logger.debug("IOException during socket operation: {}", ioe.getMessage());
} finally {
socket.close();
}
}
public class ServerInfo {
String name;
int tcpPort;
int udpPort;
String host;
String macAddress;
ServerInfo(String host, int tcpPort, int udpPort, String name, String macAddress) {
this.name = name;
this.tcpPort = tcpPort;
this.udpPort = udpPort;
this.host = host;
this.macAddress = macAddress;
}
}
}

View File

@ -0,0 +1,155 @@
/**
* 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
*/
package org.openhab.binding.unifiedremote.internal;
import static org.openhab.binding.unifiedremote.internal.UnifiedRemoteBindingConstants.MOUSE_CHANNEL;
import static org.openhab.binding.unifiedremote.internal.UnifiedRemoteBindingConstants.SEND_KEY_CHANNEL;
import java.net.ConnectException;
import java.net.NoRouteToHostException;
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.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.api.ContentResponse;
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;
/**
* The {@link UnifiedRemoteHandler} is responsible for handling commands, which are
* sent to one of the channels.
*
* @author Miguel Alvarez - Initial contribution
*/
@NonNullByDefault
public class UnifiedRemoteHandler extends BaseThingHandler {
private @Nullable UnifiedRemoteConnection connection;
private @Nullable ScheduledFuture<?> connectionCheckerSchedule;
private HttpClient httpClient;
public UnifiedRemoteHandler(Thing thing, HttpClient httpClient) {
super(thing);
this.httpClient = httpClient;
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
String channelId = channelUID.getId();
if (!isLinked(channelId))
return;
String stringCommand = command.toFullString();
UnifiedRemoteConnection urConnection = connection;
try {
if (urConnection != null) {
ContentResponse response;
switch (channelId) {
case MOUSE_CHANNEL:
response = urConnection.mouseMove(stringCommand);
break;
case SEND_KEY_CHANNEL:
response = urConnection.sendKey(stringCommand);
break;
default:
return;
}
if (isErrorResponse(response)) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Session expired");
urConnection.authenticate();
updateStatus(ThingStatus.ONLINE);
}
} else {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Connection not initialized");
}
} catch (InterruptedException | ExecutionException | TimeoutException e) {
if (isThingOfflineException(e)) {
// we assume thing is offline
updateStatus(ThingStatus.OFFLINE);
} else {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
"Unexpected exception: " + e.getMessage());
}
}
}
@Override
public void initialize() {
updateStatus(ThingStatus.UNKNOWN);
connection = getNewConnection();
initConnectionChecker();
}
private UnifiedRemoteConnection getNewConnection() {
UnifiedRemoteConfiguration currentConfiguration = getConfigAs(UnifiedRemoteConfiguration.class);
return new UnifiedRemoteConnection(this.httpClient, currentConfiguration.host);
}
private void initConnectionChecker() {
stopConnectionChecker();
connectionCheckerSchedule = scheduler.scheduleWithFixedDelay(() -> {
try {
UnifiedRemoteConnection urConnection = connection;
if (urConnection == null)
return;
ThingStatus status = thing.getStatus();
if ((status == ThingStatus.OFFLINE || status == ThingStatus.UNKNOWN) && connection != null) {
urConnection.authenticate();
updateStatus(ThingStatus.ONLINE);
} else if (status == ThingStatus.ONLINE) {
if (isErrorResponse(urConnection.keepAlive())) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Keep alive failed");
}
}
} catch (InterruptedException | ExecutionException | TimeoutException e) {
if (isThingOfflineException(e)) {
// we assume thing is offline
updateStatus(ThingStatus.OFFLINE);
} else {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
"Unexpected exception: " + e.getMessage());
}
}
}, 0, 40, TimeUnit.SECONDS);
}
private boolean isThingOfflineException(Exception e) {
return e instanceof TimeoutException || e.getCause() instanceof ConnectException
|| e.getCause() instanceof NoRouteToHostException;
}
private void stopConnectionChecker() {
var schedule = connectionCheckerSchedule;
if (schedule != null) {
schedule.cancel(true);
connectionCheckerSchedule = null;
}
}
@Override
public void dispose() {
stopConnectionChecker();
super.dispose();
}
private boolean isErrorResponse(ContentResponse response) {
return response.getStatus() != 200 || response.getContentAsString().contains("Not a valid connection");
}
}

View File

@ -0,0 +1,57 @@
/**
* 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
*/
package org.openhab.binding.unifiedremote.internal;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.HttpClient;
import org.openhab.core.io.net.http.HttpClientFactory;
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.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
/**
* The {@link UnifiedRemoteHandlerFactory} is responsible for creating things and thing
* handlers.
*
* @author Miguel Álvarez - Initial contribution
*/
@NonNullByDefault
@Component(configurationPid = "binding.unifiedremote", service = ThingHandlerFactory.class)
public class UnifiedRemoteHandlerFactory extends BaseThingHandlerFactory {
private final HttpClient httpClient;
@Activate
public UnifiedRemoteHandlerFactory(@Reference HttpClientFactory httpClientFactory) {
this.httpClient = httpClientFactory.getCommonHttpClient();
}
@Override
public boolean supportsThingType(ThingTypeUID thingTypeUID) {
return UnifiedRemoteBindingConstants.SUPPORTED_THING_TYPES.contains(thingTypeUID);
}
@Override
protected @Nullable ThingHandler createHandler(Thing thing) {
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
if (supportsThingType(thingTypeUID)) {
return new UnifiedRemoteHandler(thing, httpClient);
}
return null;
}
}

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<binding:binding id="unifiedremote" 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>Unified Remote Binding</name>
<description>This is the binding for Unified Remote Server (https://www.unifiedremote.com/).</description>
<author>Miguel Álvarez</author>
</binding:binding>

View File

@ -0,0 +1,83 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="unifiedremote"
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="server">
<label>Unified Remote Server</label>
<description>Unified Remote Server Thing for Unified Remote Binding</description>
<channels>
<channel id="mouse-move" typeId="mouse-move-channel"/>
<channel id="send-key" typeId="send-key-channel"/>
</channels>
<representation-property>macAddress</representation-property>
<config-description>
<parameter name="host" type="text" required="true">
<label>Hostname</label>
<context>network-address</context>
<description>Unified Remote Server Hostname</description>
</parameter>
<parameter name="tcpPort" type="integer">
<label>TCP Port</label>
<description>Unified Remote Server Port TCP</description>
</parameter>
<parameter name="udpPort" type="integer">
<label>UDP Port</label>
<description>Unified Remote Server Port UDP</description>
</parameter>
</config-description>
</thing-type>
<channel-type id="mouse-move-channel">
<item-type>String</item-type>
<label>Mouse Move Channel</label>
<description>Relative mouse control on the server host</description>
</channel-type>
<channel-type id="send-key-channel">
<item-type>String</item-type>
<label>Toggle Key Channel</label>
<description>Toggle Key</description>
<state>
<options>
<!-- MOUSE -->
<option value="LEFT_CLICK">LEFT_CLICK</option>
<option value="RIGHT_CLICK">RIGHT_CLICK</option>
<!-- SYSTEM -->
<option value="LOCK">LOCK</option>
<option value="UNLOCK">UNLOCK</option>
<option value="SLEEP">SLEEP</option>
<option value="SHUTDOWN">SHUTDOWN</option>
<option value="RESTART">RESTART</option>
<option value="LOGOFF">LOGOFF</option>
<!-- Media -->
<option value="PLAY/PAUSE">PLAY/PAUSE</option>
<option value="NEXT">NEXT</option>
<option value="PREVIOUS">PREVIOUS</option>
<option value="STOP">STOP</option>
<option value="VOLUME_MUTE">VOLUME_MUTE</option>
<option value="VOLUME_UP">VOLUME_UP</option>
<option value="VOLUME_DOWN">VOLUME_DOWN</option>
<option value="BRIGHTNESS_UP">BRIGHTNESS_UP</option>
<option value="BRIGHTNESS_DOWN">BRIGHTNESS_DOWN</option>
<option value="MONITOR_OFF">MONITOR_OFF</option>
<option value="MONITOR_ON">MONITOR_ON</option>
<!-- Navigation -->
<option value="ESCAPE">ESCAPE</option>
<option value="SPACE">SPACE</option>
<option value="BACK">BACK</option>
<option value="LWIN">LWIN</option>
<option value="CONTROL">CONTROL</option>
<option value="TAB">TAB</option>
<option value="MENU">MENU</option>
<option value="RETURN">RETURN</option>
<option value="UP">UP</option>
<option value="DOWN">DOWN</option>
<option value="LEFT">LEFT</option>
<option value="RIGHT">RIGHT</option>
</options>
</state>
</channel-type>
</thing:thing-descriptions>

View File

@ -278,6 +278,7 @@
<module>org.openhab.binding.tplinksmarthome</module>
<module>org.openhab.binding.tradfri</module>
<module>org.openhab.binding.unifi</module>
<module>org.openhab.binding.unifiedremote</module>
<module>org.openhab.binding.upnpcontrol</module>
<module>org.openhab.binding.upb</module>
<module>org.openhab.binding.urtsi</module>