mirror of
https://github.com/openhab/openhab-addons.git
synced 2025-01-10 07:02:02 +01:00
[unifiedremote] Initial contribution (#8546)
Signed-off-by: GiviMAD <miguelwork92@gmail.com>
This commit is contained in:
parent
cb5d8711b8
commit
8b8b79cf04
@ -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
|
||||
|
@ -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>
|
||||
|
13
bundles/org.openhab.binding.unifiedremote/NOTICE
Normal file
13
bundles/org.openhab.binding.unifiedremote/NOTICE
Normal 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
|
49
bundles/org.openhab.binding.unifiedremote/README.md
Normal file
49
bundles/org.openhab.binding.unifiedremote/README.md
Normal 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" }
|
||||
```
|
17
bundles/org.openhab.binding.unifiedremote/pom.xml
Normal file
17
bundles/org.openhab.binding.unifiedremote/pom.xml
Normal 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>
|
@ -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>
|
@ -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";
|
||||
}
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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");
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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>
|
@ -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>
|
@ -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>
|
||||
|
Loading…
Reference in New Issue
Block a user