/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
<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>
--- /dev/null
+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
--- /dev/null
+# 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" }
+```
--- /dev/null
+<?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>
--- /dev/null
+<?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>
--- /dev/null
+/**
+ * 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";
+}
--- /dev/null
+/**
+ * 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;
+}
--- /dev/null
+/**
+ * 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;
+ }
+}
--- /dev/null
+/**
+ * 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;
+ }
+ }
+}
--- /dev/null
+/**
+ * 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");
+ }
+}
--- /dev/null
+/**
+ * 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;
+ }
+}
--- /dev/null
+<?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>
--- /dev/null
+<?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>
<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>