From 156fed2f7e7906edd05b042d10fb188b954fd852 Mon Sep 17 00:00:00 2001 From: GiviMAD Date: Tue, 22 Sep 2020 17:56:33 +0200 Subject: [PATCH 1/5] [unifiedremote] add binding code Signed-off-by: GiviMAD --- CODEOWNERS | 1 + bom/openhab-addons/pom.xml | 5 + .../.classpath | 32 +++ .../.project | 23 ++ .../org.openhab.binding.unifiedremote/NOTICE | 13 + .../README.md | 30 ++ .../org.openhab.binding.unifiedremote/pom.xml | 17 ++ .../src/main/feature/feature.xml | 23 ++ .../UnifiedRemoteBindingConstants.java | 45 +++ .../internal/UnifiedRemoteConfiguration.java | 27 ++ .../internal/UnifiedRemoteConnection.java | 272 ++++++++++++++++++ .../UnifiedRemoteDiscoveryService.java | 187 ++++++++++++ .../internal/UnifiedRemoteHandler.java | 144 ++++++++++ .../internal/UnifiedRemoteHandlerFactory.java | 46 +++ .../main/resources/OH-INF/binding/binding.xml | 11 + .../resources/OH-INF/thing/thing-types.xml | 82 ++++++ bundles/pom.xml | 1 + 17 files changed, 959 insertions(+) create mode 100644 bundles/org.openhab.binding.unifiedremote/.classpath create mode 100644 bundles/org.openhab.binding.unifiedremote/.project create mode 100644 bundles/org.openhab.binding.unifiedremote/NOTICE create mode 100644 bundles/org.openhab.binding.unifiedremote/README.md create mode 100644 bundles/org.openhab.binding.unifiedremote/pom.xml create mode 100644 bundles/org.openhab.binding.unifiedremote/src/main/feature/feature.xml create mode 100644 bundles/org.openhab.binding.unifiedremote/src/main/java/org/openhab/binding/unifiedremote/internal/UnifiedRemoteBindingConstants.java create mode 100644 bundles/org.openhab.binding.unifiedremote/src/main/java/org/openhab/binding/unifiedremote/internal/UnifiedRemoteConfiguration.java create mode 100644 bundles/org.openhab.binding.unifiedremote/src/main/java/org/openhab/binding/unifiedremote/internal/UnifiedRemoteConnection.java create mode 100644 bundles/org.openhab.binding.unifiedremote/src/main/java/org/openhab/binding/unifiedremote/internal/UnifiedRemoteDiscoveryService.java create mode 100644 bundles/org.openhab.binding.unifiedremote/src/main/java/org/openhab/binding/unifiedremote/internal/UnifiedRemoteHandler.java create mode 100644 bundles/org.openhab.binding.unifiedremote/src/main/java/org/openhab/binding/unifiedremote/internal/UnifiedRemoteHandlerFactory.java create mode 100644 bundles/org.openhab.binding.unifiedremote/src/main/resources/OH-INF/binding/binding.xml create mode 100644 bundles/org.openhab.binding.unifiedremote/src/main/resources/OH-INF/thing/thing-types.xml diff --git a/CODEOWNERS b/CODEOWNERS index 1c3bb324cad64..61f445693a447 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -241,6 +241,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.upnpcontrol/ @mherwege /bundles/org.openhab.binding.upb/ @marcusb /bundles/org.openhab.binding.urtsi/ @OLibutzki diff --git a/bom/openhab-addons/pom.xml b/bom/openhab-addons/pom.xml index e40b93f074bc3..cd4bab7c2c084 100644 --- a/bom/openhab-addons/pom.xml +++ b/bom/openhab-addons/pom.xml @@ -1211,6 +1211,11 @@ org.openhab.binding.unifi ${project.version} + + org.openhab.addons.bundles + org.openhab.binding.unifiedremote + ${project.version} + org.openhab.addons.bundles org.openhab.binding.upnpcontrol diff --git a/bundles/org.openhab.binding.unifiedremote/.classpath b/bundles/org.openhab.binding.unifiedremote/.classpath new file mode 100644 index 0000000000000..234db15be4950 --- /dev/null +++ b/bundles/org.openhab.binding.unifiedremote/.classpath @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/bundles/org.openhab.binding.unifiedremote/.project b/bundles/org.openhab.binding.unifiedremote/.project new file mode 100644 index 0000000000000..e1eefd58198d8 --- /dev/null +++ b/bundles/org.openhab.binding.unifiedremote/.project @@ -0,0 +1,23 @@ + + + org.openhab.binding.unifiedremote + + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.m2e.core.maven2Builder + + + + + + org.eclipse.jdt.core.javanature + org.eclipse.m2e.core.maven2Nature + + diff --git a/bundles/org.openhab.binding.unifiedremote/NOTICE b/bundles/org.openhab.binding.unifiedremote/NOTICE new file mode 100644 index 0000000000000..38d625e349232 --- /dev/null +++ b/bundles/org.openhab.binding.unifiedremote/NOTICE @@ -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 diff --git a/bundles/org.openhab.binding.unifiedremote/README.md b/bundles/org.openhab.binding.unifiedremote/README.md new file mode 100644 index 0000000000000..36b5424ea2207 --- /dev/null +++ b/bundles/org.openhab.binding.unifiedremote/README.md @@ -0,0 +1,30 @@ +# UnifiedRemote Binding + +This binding integrates the [Unified Remote Server](https://www.unifiedremote.com/). + +Known Limitations: It needs the web interface to be enabled on the server settings to work. + +## Discovery + +Discovery works on the default discovery UDP port 9511. + +## Thing Configuration + +The Unified Remote Server Thing requires the host to be correctly configured in order to work correctly. +Other properties like tcpPort and udpPort are not used in the initial implementation. + +``` +Thing unifiedremote:server:xx-xx-xx-xx-xx-xx [ host="192.168.1.10" ] +``` + +## Channels + +_Here you should provide information about available channel types, what their meaning is and how they can be used._ + +_Note that it is planned to generate some part of this based on the XML files within ```src/main/resources/OH-INF/thing``` of your binding._ + +| channel | type | description | +|----------|--------|------------------------------| +| mouse-move | String | Mouse Move. Expect number JSON array ("[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 | + diff --git a/bundles/org.openhab.binding.unifiedremote/pom.xml b/bundles/org.openhab.binding.unifiedremote/pom.xml new file mode 100644 index 0000000000000..475b0af813365 --- /dev/null +++ b/bundles/org.openhab.binding.unifiedremote/pom.xml @@ -0,0 +1,17 @@ + + + + 4.0.0 + + + org.openhab.addons.bundles + org.openhab.addons.reactor.bundles + 3.0.0-SNAPSHOT + + + org.openhab.binding.unifiedremote + + openHAB Add-ons :: Bundles :: UnifiedRemote Binding + + diff --git a/bundles/org.openhab.binding.unifiedremote/src/main/feature/feature.xml b/bundles/org.openhab.binding.unifiedremote/src/main/feature/feature.xml new file mode 100644 index 0000000000000..94409d2a88c9e --- /dev/null +++ b/bundles/org.openhab.binding.unifiedremote/src/main/feature/feature.xml @@ -0,0 +1,23 @@ + + + + mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features + + + openhab-runtime-base + mvn:org.openhab.addons.bundles/org.openhab.binding.unifiedremote/${project.version} + + diff --git a/bundles/org.openhab.binding.unifiedremote/src/main/java/org/openhab/binding/unifiedremote/internal/UnifiedRemoteBindingConstants.java b/bundles/org.openhab.binding.unifiedremote/src/main/java/org/openhab/binding/unifiedremote/internal/UnifiedRemoteBindingConstants.java new file mode 100644 index 0000000000000..ed35bfe84d1f6 --- /dev/null +++ b/bundles/org.openhab.binding.unifiedremote/src/main/java/org/openhab/binding/unifiedremote/internal/UnifiedRemoteBindingConstants.java @@ -0,0 +1,45 @@ +/** + * 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 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_HOSTNAME = "host"; + public static final String PARAMETER_TCP_PORT = "udpPort"; + public static final String PARAMETER_UDP_PORT = "tcpPort"; +} diff --git a/bundles/org.openhab.binding.unifiedremote/src/main/java/org/openhab/binding/unifiedremote/internal/UnifiedRemoteConfiguration.java b/bundles/org.openhab.binding.unifiedremote/src/main/java/org/openhab/binding/unifiedremote/internal/UnifiedRemoteConfiguration.java new file mode 100644 index 0000000000000..1e13710db3205 --- /dev/null +++ b/bundles/org.openhab.binding.unifiedremote/src/main/java/org/openhab/binding/unifiedremote/internal/UnifiedRemoteConfiguration.java @@ -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; +} diff --git a/bundles/org.openhab.binding.unifiedremote/src/main/java/org/openhab/binding/unifiedremote/internal/UnifiedRemoteConnection.java b/bundles/org.openhab.binding.unifiedremote/src/main/java/org/openhab/binding/unifiedremote/internal/UnifiedRemoteConnection.java new file mode 100644 index 0000000000000..732f3ea26959f --- /dev/null +++ b/bundles/org.openhab.binding.unifiedremote/src/main/java/org/openhab/binding/unifiedremote/internal/UnifiedRemoteConnection.java @@ -0,0 +1,272 @@ +/** + * 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.io.Closeable; +import java.util.UUID; +import java.util.concurrent.ExecutionException; +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.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 implements Closeable { + private final Logger logger = LoggerFactory.getLogger(UnifiedRemoteConnection.class); + private final String url; + private final HttpClient http = new HttpClient(); + private final JsonParser jsonParser = new JsonParser(); + private final int WEB_CLIENT_PORT = 9510; + private final String CONNECTION_ID_HEADER = "UR-Connection-ID"; + private @Nullable String connectionID; + private @Nullable String connectionGUID; + + private final String MOUSE_REMOTE = "Relmtech.Basic Input"; + private final String NAVIGATION_REMOTE = "Unified.Navigation"; + private final String POWER_REMOTE = "Unified.Power"; + private final String MEDIA_REMOTE = "Unified.Media"; + private final String MONITOR_REMOTE = "Unified.Monitor"; + + public UnifiedRemoteConnection(String host) { + url = "http://" + host + ":" + WEB_CLIENT_PORT + "/client/"; + } + + public void authenticate() throws Exception { + ContentResponse response = null; + connectionGUID = "web-" + UUID.randomUUID().toString(); + if (!http.isStarted()) { + http.start(); + } + response = http.GET(getPath("connect")); + 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 = http.POST(getPath("request")); + 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; + } + + @Override + public void close() { + if (http.isStarted()) { + try { + http.stop(); + } catch (Exception e) { + } + } + } +} diff --git a/bundles/org.openhab.binding.unifiedremote/src/main/java/org/openhab/binding/unifiedremote/internal/UnifiedRemoteDiscoveryService.java b/bundles/org.openhab.binding.unifiedremote/src/main/java/org/openhab/binding/unifiedremote/internal/UnifiedRemoteDiscoveryService.java new file mode 100644 index 0000000000000..4205704e9030c --- /dev/null +++ b/bundles/org.openhab.binding.unifiedremote/src/main/java/org/openhab/binding/unifiedremote/internal/UnifiedRemoteDiscoveryService.java @@ -0,0 +1,187 @@ +/** + * 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, immediate = true, configurationPid = "discovery.unifiedremote") +@NonNullByDefault +public class UnifiedRemoteDiscoveryService extends AbstractDiscoveryService { + + private static final Logger logger = LoggerFactory.getLogger(UnifiedRemoteDiscoveryService.class); + static final int TIMEOUT_MS = 20000; + private static final long DISCOVERY_RESULT_TTL = TimeUnit.MINUTES.toSeconds(5); + + public UnifiedRemoteDiscoveryService() { + super(SUPPORTED_THING_TYPES, TIMEOUT_MS, false); + } + + @Override + protected void startScan() { + UnifiedRemoteUdpDiscovery client = new UnifiedRemoteUdpDiscovery(); + client.call(serverInfo -> addNewServer(serverInfo)); + } + + private void addNewServer(UnifiedRemoteUdpDiscovery.ServerInfo serverInfo) { + Map properties = new HashMap<>(); + 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).withProperties(properties).withLabel(serverInfo.name).build()); + } + + private class UnifiedRemoteUdpDiscovery { + /** + * 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 TIMEOUT = 3000; // milliseonds + + public class ServerInfo { + String name; + int tcpPort; + int udpPort; + String host; + String macAddress; + String publicIp; + + ServerInfo(String host, int tcpPort, int udpPort, String name, String macAddress, String publicIp) { + this.name = name; + this.tcpPort = tcpPort; + this.udpPort = udpPort; + this.host = host; + this.macAddress = macAddress; + this.publicIp = publicIp; + } + } + + /** + * 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); + 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]; + String publicIp = parts[4]; + return new ServerInfo(host, tcpPort, udpPort, name, macAddress, publicIp); + } + + /** + * 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 call(Consumer 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.error("Error creating 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 (Exception ex) { + logger.error("Exception parsing server discovery response from {}: {}", host, ex.getMessage()); + } + } + } catch (SocketTimeoutException ste) { + logger.debug("SocketTimeoutException during socket operation: {}", ste.getMessage()); + } catch (IOException ioe) { + logger.error("IOException during socket operation: {}", ioe.getMessage()); + } + socket.close(); + } + } +} diff --git a/bundles/org.openhab.binding.unifiedremote/src/main/java/org/openhab/binding/unifiedremote/internal/UnifiedRemoteHandler.java b/bundles/org.openhab.binding.unifiedremote/src/main/java/org/openhab/binding/unifiedremote/internal/UnifiedRemoteHandler.java new file mode 100644 index 0000000000000..898a31c708495 --- /dev/null +++ b/bundles/org.openhab.binding.unifiedremote/src/main/java/org/openhab/binding/unifiedremote/internal/UnifiedRemoteHandler.java @@ -0,0 +1,144 @@ +/** + * 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.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +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; +import org.openhab.core.types.RefreshType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * 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 final Logger logger = LoggerFactory.getLogger(UnifiedRemoteHandler.class); + + private @Nullable UnifiedRemoteConfiguration config; + + private @Nullable UnifiedRemoteConnection connection; + private @Nullable ScheduledFuture connectionCheckerSchedule; + + public UnifiedRemoteHandler(Thing thing) { + super(thing); + } + + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + if (command instanceof RefreshType) { + // TODO: nothing to do for now + return; + } + String channelId = channelUID.getId(); + if (!isLinked(channelId)) + return; + String stringCommand = command.toFullString(); + try { + if (connection != null) { + ContentResponse response; + switch (channelId) { + case MOUSE_CHANNEL: + response = connection.mouseMove(stringCommand); + break; + case SEND_KEY_CHANNEL: + response = connection.sendKey(stringCommand); + break; + default: + return; + } + if (isErrorResponse(response)) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Session expired"); + connection.authenticate(); + updateStatus(ThingStatus.ONLINE); + } + } else { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Connection not initialized"); + } + } catch (Exception e) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, + "Server request fail: " + e.getMessage()); + } + } + + @Override + public void initialize() { + updateStatus(ThingStatus.UNKNOWN); + connection = getNewConnection(); + initConnectionChecker(); + } + + private UnifiedRemoteConnection getNewConnection() { + config = getConfigAs(UnifiedRemoteConfiguration.class); + return new UnifiedRemoteConnection(config.host); + } + + private void initConnectionChecker() { + stopConnectionChecker(); + connectionCheckerSchedule = scheduler.scheduleWithFixedDelay(() -> { + try { + ThingStatus status = thing.getStatus(); + if ((status == ThingStatus.OFFLINE || status == ThingStatus.UNKNOWN) && connection != null) { + connection.authenticate(); + updateStatus(ThingStatus.ONLINE); + } else if (status == ThingStatus.ONLINE) { + if (isErrorResponse(connection.keepAlive())) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Keep alive failed"); + } + } + } catch (Exception e) { + if (thing.getStatus() != ThingStatus.OFFLINE) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage()); + } + } + }, 0, 40, TimeUnit.SECONDS); + } + + private void stopConnectionChecker() { + if (connectionCheckerSchedule != null && !connectionCheckerSchedule.isCancelled()) { + connectionCheckerSchedule.cancel(true); + } + connectionCheckerSchedule = null; + } + + @Override + public void dispose() { + stopConnectionChecker(); + if (connection != null) { + connection.close(); + connection = null; + } + super.dispose(); + } + + private boolean isErrorResponse(ContentResponse response) { + return response.getStatus() != 200 || response.getContentAsString().contains("Not a valid connection"); + } +} diff --git a/bundles/org.openhab.binding.unifiedremote/src/main/java/org/openhab/binding/unifiedremote/internal/UnifiedRemoteHandlerFactory.java b/bundles/org.openhab.binding.unifiedremote/src/main/java/org/openhab/binding/unifiedremote/internal/UnifiedRemoteHandlerFactory.java new file mode 100644 index 0000000000000..20cad5589f86d --- /dev/null +++ b/bundles/org.openhab.binding.unifiedremote/src/main/java/org/openhab/binding/unifiedremote/internal/UnifiedRemoteHandlerFactory.java @@ -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 org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingTypeUID; +import org.openhab.core.thing.binding.BaseThingHandlerFactory; +import org.openhab.core.thing.binding.ThingHandler; +import org.openhab.core.thing.binding.ThingHandlerFactory; +import org.osgi.service.component.annotations.Component; + +/** + * The {@link 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 { + @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); + } + return null; + } +} diff --git a/bundles/org.openhab.binding.unifiedremote/src/main/resources/OH-INF/binding/binding.xml b/bundles/org.openhab.binding.unifiedremote/src/main/resources/OH-INF/binding/binding.xml new file mode 100644 index 0000000000000..86995f66f821f --- /dev/null +++ b/bundles/org.openhab.binding.unifiedremote/src/main/resources/OH-INF/binding/binding.xml @@ -0,0 +1,11 @@ + + + + + Unified Remote Binding + This is the binding for Unified Remote Server (https://www.unifiedremote.com/). + Miguel Álvarez + + diff --git a/bundles/org.openhab.binding.unifiedremote/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.unifiedremote/src/main/resources/OH-INF/thing/thing-types.xml new file mode 100644 index 0000000000000..d56ffd6e65de9 --- /dev/null +++ b/bundles/org.openhab.binding.unifiedremote/src/main/resources/OH-INF/thing/thing-types.xml @@ -0,0 +1,82 @@ + + + + + + Unified Remote Server Thing for Unified Remote Binding + + + + + + + + + Unified Remote Server Hostname + + + + Unified Remote Server Port TCP + + + + Unified Remote Server Port UDP + + + + + + String + + Relative mouse control on the server host + + + + String + + Toggle Key + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/bundles/pom.xml b/bundles/pom.xml index 003f0585ac48b..c5b2787af6efe 100644 --- a/bundles/pom.xml +++ b/bundles/pom.xml @@ -273,6 +273,7 @@ org.openhab.binding.tplinksmarthome org.openhab.binding.tradfri org.openhab.binding.unifi + org.openhab.binding.unifiedremote org.openhab.binding.upnpcontrol org.openhab.binding.upb org.openhab.binding.urtsi From 243419e3850f4eb0ea6c65754992882eb4fbd3b7 Mon Sep 17 00:00:00 2001 From: GiviMAD Date: Sun, 18 Oct 2020 14:54:19 +0200 Subject: [PATCH 2/5] [unifiedremote] pr review changes Signed-off-by: GiviMAD --- .../.classpath | 32 ------------ .../.project | 23 -------- .../README.md | 34 ++++++++---- .../internal/UnifiedRemoteConnection.java | 52 ++++++++----------- .../UnifiedRemoteDiscoveryService.java | 24 +++++---- .../internal/UnifiedRemoteHandler.java | 42 ++++++++------- .../internal/UnifiedRemoteHandlerFactory.java | 13 ++++- .../main/resources/OH-INF/binding/binding.xml | 1 - .../resources/OH-INF/thing/thing-types.xml | 3 +- 9 files changed, 99 insertions(+), 125 deletions(-) delete mode 100644 bundles/org.openhab.binding.unifiedremote/.classpath delete mode 100644 bundles/org.openhab.binding.unifiedremote/.project diff --git a/bundles/org.openhab.binding.unifiedremote/.classpath b/bundles/org.openhab.binding.unifiedremote/.classpath deleted file mode 100644 index 234db15be4950..0000000000000 --- a/bundles/org.openhab.binding.unifiedremote/.classpath +++ /dev/null @@ -1,32 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/bundles/org.openhab.binding.unifiedremote/.project b/bundles/org.openhab.binding.unifiedremote/.project deleted file mode 100644 index e1eefd58198d8..0000000000000 --- a/bundles/org.openhab.binding.unifiedremote/.project +++ /dev/null @@ -1,23 +0,0 @@ - - - org.openhab.binding.unifiedremote - - - - - - org.eclipse.jdt.core.javabuilder - - - - - org.eclipse.m2e.core.maven2Builder - - - - - - org.eclipse.jdt.core.javanature - org.eclipse.m2e.core.maven2Nature - - diff --git a/bundles/org.openhab.binding.unifiedremote/README.md b/bundles/org.openhab.binding.unifiedremote/README.md index 36b5424ea2207..1483805b7c328 100644 --- a/bundles/org.openhab.binding.unifiedremote/README.md +++ b/bundles/org.openhab.binding.unifiedremote/README.md @@ -10,21 +10,37 @@ Discovery works on the default discovery UDP port 9511. ## Thing Configuration -The Unified Remote Server Thing requires the host to be correctly configured in order to work correctly. -Other properties like tcpPort and udpPort are not used in the initial implementation. +Only supported thing is 'Unified Remote Server Thing' witch requires the Hostname to be correctly configured in order to work. -``` -Thing unifiedremote:server:xx-xx-xx-xx-xx-xx [ host="192.168.1.10" ] -``` +| ThinTypeID | description | +|----------|------------------------------| +| server | Unified Remote Server Thing | -## Channels -_Here you should provide information about available channel types, what their meaning is and how they can be used._ +| Config | Type | description | +|----------|----------|------------------------------| +| host | String | Unified Remote Server IP | + + + +## Channels -_Note that it is planned to generate some part of this based on the XML files within ```src/main/resources/OH-INF/thing``` of your binding._ | channel | type | description | |----------|--------|------------------------------| -| mouse-move | String | Mouse Move. Expect number JSON array ("[10,10]"). | +| 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" } +``` diff --git a/bundles/org.openhab.binding.unifiedremote/src/main/java/org/openhab/binding/unifiedremote/internal/UnifiedRemoteConnection.java b/bundles/org.openhab.binding.unifiedremote/src/main/java/org/openhab/binding/unifiedremote/internal/UnifiedRemoteConnection.java index 732f3ea26959f..603f766d7a01a 100644 --- a/bundles/org.openhab.binding.unifiedremote/src/main/java/org/openhab/binding/unifiedremote/internal/UnifiedRemoteConnection.java +++ b/bundles/org.openhab.binding.unifiedremote/src/main/java/org/openhab/binding/unifiedremote/internal/UnifiedRemoteConnection.java @@ -12,9 +12,9 @@ */ package org.openhab.binding.unifiedremote.internal; -import java.io.Closeable; 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; @@ -24,6 +24,7 @@ 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; @@ -38,33 +39,34 @@ * @author Miguel Alvarez - Initial contribution */ @NonNullByDefault -public class UnifiedRemoteConnection implements Closeable { - private final Logger logger = LoggerFactory.getLogger(UnifiedRemoteConnection.class); +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 HttpClient http = new HttpClient(); private final JsonParser jsonParser = new JsonParser(); - private final int WEB_CLIENT_PORT = 9510; - private final String CONNECTION_ID_HEADER = "UR-Connection-ID"; + private HttpClient httpClient; private @Nullable String connectionID; private @Nullable String connectionGUID; - private final String MOUSE_REMOTE = "Relmtech.Basic Input"; - private final String NAVIGATION_REMOTE = "Unified.Navigation"; - private final String POWER_REMOTE = "Unified.Power"; - private final String MEDIA_REMOTE = "Unified.Media"; - private final String MONITOR_REMOTE = "Unified.Monitor"; - - public UnifiedRemoteConnection(String host) { + public UnifiedRemoteConnection(HttpClient httpClient, String host) { + this.httpClient = httpClient; url = "http://" + host + ":" + WEB_CLIENT_PORT + "/client/"; } - public void authenticate() throws Exception { + public void authenticate() throws InterruptedException, ExecutionException, TimeoutException { ContentResponse response = null; connectionGUID = "web-" + UUID.randomUUID().toString(); - if (!http.isStarted()) { - http.start(); - } - response = http.GET(getPath("connect")); + 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(); @@ -219,6 +221,7 @@ public ContentResponse keepAlive() throws InterruptedException, ExecutionExcepti 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) { @@ -236,7 +239,8 @@ private ContentResponse execRemoteAction(String remoteID, String name, @Nullable private ContentResponse request(JsonObject content) throws InterruptedException, ExecutionException, TimeoutException { - Request request = http.POST(getPath("request")); + 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); @@ -259,14 +263,4 @@ private JsonArray wrapValues(String[] commandValues) { private String getPath(String path) { return url + path; } - - @Override - public void close() { - if (http.isStarted()) { - try { - http.stop(); - } catch (Exception e) { - } - } - } } diff --git a/bundles/org.openhab.binding.unifiedremote/src/main/java/org/openhab/binding/unifiedremote/internal/UnifiedRemoteDiscoveryService.java b/bundles/org.openhab.binding.unifiedremote/src/main/java/org/openhab/binding/unifiedremote/internal/UnifiedRemoteDiscoveryService.java index 4205704e9030c..f6b23e692965b 100644 --- a/bundles/org.openhab.binding.unifiedremote/src/main/java/org/openhab/binding/unifiedremote/internal/UnifiedRemoteDiscoveryService.java +++ b/bundles/org.openhab.binding.unifiedremote/src/main/java/org/openhab/binding/unifiedremote/internal/UnifiedRemoteDiscoveryService.java @@ -41,9 +41,9 @@ @NonNullByDefault public class UnifiedRemoteDiscoveryService extends AbstractDiscoveryService { - private static final Logger logger = LoggerFactory.getLogger(UnifiedRemoteDiscoveryService.class); + private Logger logger = LoggerFactory.getLogger(UnifiedRemoteDiscoveryService.class); static final int TIMEOUT_MS = 20000; - private static final long DISCOVERY_RESULT_TTL = TimeUnit.MINUTES.toSeconds(5); + private static final long DISCOVERY_RESULT_TTL_SEC = TimeUnit.MINUTES.toSeconds(5); public UnifiedRemoteDiscoveryService() { super(SUPPORTED_THING_TYPES, TIMEOUT_MS, false); @@ -52,7 +52,7 @@ public UnifiedRemoteDiscoveryService() { @Override protected void startScan() { UnifiedRemoteUdpDiscovery client = new UnifiedRemoteUdpDiscovery(); - client.call(serverInfo -> addNewServer(serverInfo)); + client.call(this::addNewServer); } private void addNewServer(UnifiedRemoteUdpDiscovery.ServerInfo serverInfo) { @@ -62,7 +62,8 @@ private void addNewServer(UnifiedRemoteUdpDiscovery.ServerInfo serverInfo) { properties.put(PARAMETER_UDP_PORT, serverInfo.udpPort); thingDiscovered( DiscoveryResultBuilder.create(new ThingUID(THING_TYPE_UNIFIED_REMOTE_SERVER, serverInfo.macAddress)) - .withTTL(DISCOVERY_RESULT_TTL).withProperties(properties).withLabel(serverInfo.name).build()); + .withTTL(DISCOVERY_RESULT_TTL_SEC).withRepresentationProperty(serverInfo.macAddress) + .withProperties(properties).withLabel(serverInfo.name).build()); } private class UnifiedRemoteUdpDiscovery { @@ -87,7 +88,7 @@ private class UnifiedRemoteUdpDiscovery { /** * maximum time to wait for a reply, in milliseconds. */ - private static final int TIMEOUT = 3000; // milliseonds + private static final int TIMEOUT_MS = 3000; public class ServerInfo { String name; @@ -117,7 +118,7 @@ public DatagramSocket createSocket() throws SocketException { DatagramSocket socket; socket = new DatagramSocket(); socket.setBroadcast(true); - socket.setSoTimeout(TIMEOUT); + socket.setSoTimeout(TIMEOUT_MS); return socket; } @@ -154,7 +155,7 @@ public void call(Consumer listener) { try { socket = createSocket(); } catch (SocketException e) { - logger.error("Error creating socket: {}", e.getMessage()); + logger.debug("Error creating discovery socket: {}", e.getMessage()); return; } byte[] packetData = DISCOVERY_REQUEST.getBytes(); @@ -172,16 +173,17 @@ public void call(Consumer listener) { try { ServerInfo serverInfo = tryParseServerDiscovery(receivePacket); listener.accept(serverInfo); - } catch (Exception ex) { - logger.error("Exception parsing server discovery response from {}: {}", host, ex.getMessage()); + } 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.error("IOException during socket operation: {}", ioe.getMessage()); + logger.debug("IOException during socket operation: {}", ioe.getMessage()); + } finally { + socket.close(); } - socket.close(); } } } diff --git a/bundles/org.openhab.binding.unifiedremote/src/main/java/org/openhab/binding/unifiedremote/internal/UnifiedRemoteHandler.java b/bundles/org.openhab.binding.unifiedremote/src/main/java/org/openhab/binding/unifiedremote/internal/UnifiedRemoteHandler.java index 898a31c708495..705c3723982ab 100644 --- a/bundles/org.openhab.binding.unifiedremote/src/main/java/org/openhab/binding/unifiedremote/internal/UnifiedRemoteHandler.java +++ b/bundles/org.openhab.binding.unifiedremote/src/main/java/org/openhab/binding/unifiedremote/internal/UnifiedRemoteHandler.java @@ -15,11 +15,15 @@ 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.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; @@ -27,7 +31,6 @@ import org.openhab.core.thing.ThingStatusDetail; import org.openhab.core.thing.binding.BaseThingHandler; import org.openhab.core.types.Command; -import org.openhab.core.types.RefreshType; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -40,23 +43,21 @@ @NonNullByDefault public class UnifiedRemoteHandler extends BaseThingHandler { - private final Logger logger = LoggerFactory.getLogger(UnifiedRemoteHandler.class); + private Logger logger = LoggerFactory.getLogger(UnifiedRemoteHandler.class); private @Nullable UnifiedRemoteConfiguration config; private @Nullable UnifiedRemoteConnection connection; private @Nullable ScheduledFuture connectionCheckerSchedule; + private HttpClient httpClient; - public UnifiedRemoteHandler(Thing thing) { + public UnifiedRemoteHandler(Thing thing, HttpClient httpClient) { super(thing); + this.httpClient = httpClient; } @Override public void handleCommand(ChannelUID channelUID, Command command) { - if (command instanceof RefreshType) { - // TODO: nothing to do for now - return; - } String channelId = channelUID.getId(); if (!isLinked(channelId)) return; @@ -82,9 +83,14 @@ public void handleCommand(ChannelUID channelUID, Command command) { } else { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Connection not initialized"); } - } catch (Exception e) { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, - "Server request fail: " + e.getMessage()); + } catch (InterruptedException | ExecutionException | TimeoutException e) { + if (e.getCause() instanceof ConnectException) { + // we assume thing is offline + updateStatus(ThingStatus.OFFLINE); + } else { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, + "Unexpected exception: " + e.getMessage()); + } } } @@ -97,7 +103,7 @@ public void initialize() { private UnifiedRemoteConnection getNewConnection() { config = getConfigAs(UnifiedRemoteConfiguration.class); - return new UnifiedRemoteConnection(config.host); + return new UnifiedRemoteConnection(this.httpClient, config.host); } private void initConnectionChecker() { @@ -113,9 +119,13 @@ private void initConnectionChecker() { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Keep alive failed"); } } - } catch (Exception e) { - if (thing.getStatus() != ThingStatus.OFFLINE) { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage()); + } catch (InterruptedException | ExecutionException | TimeoutException e) { + if (e.getCause() instanceof ConnectException) { + // we assume thing is offline + updateStatus(ThingStatus.OFFLINE); + } else { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, + "Unexpected exception: " + e.getMessage()); } } }, 0, 40, TimeUnit.SECONDS); @@ -131,10 +141,6 @@ private void stopConnectionChecker() { @Override public void dispose() { stopConnectionChecker(); - if (connection != null) { - connection.close(); - connection = null; - } super.dispose(); } diff --git a/bundles/org.openhab.binding.unifiedremote/src/main/java/org/openhab/binding/unifiedremote/internal/UnifiedRemoteHandlerFactory.java b/bundles/org.openhab.binding.unifiedremote/src/main/java/org/openhab/binding/unifiedremote/internal/UnifiedRemoteHandlerFactory.java index 20cad5589f86d..30cb21ea9e45a 100644 --- a/bundles/org.openhab.binding.unifiedremote/src/main/java/org/openhab/binding/unifiedremote/internal/UnifiedRemoteHandlerFactory.java +++ b/bundles/org.openhab.binding.unifiedremote/src/main/java/org/openhab/binding/unifiedremote/internal/UnifiedRemoteHandlerFactory.java @@ -14,12 +14,16 @@ 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 @@ -30,6 +34,13 @@ @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); @@ -39,7 +50,7 @@ public boolean supportsThingType(ThingTypeUID thingTypeUID) { protected @Nullable ThingHandler createHandler(Thing thing) { ThingTypeUID thingTypeUID = thing.getThingTypeUID(); if (supportsThingType(thingTypeUID)) { - return new UnifiedRemoteHandler(thing); + return new UnifiedRemoteHandler(thing, httpClient); } return null; } diff --git a/bundles/org.openhab.binding.unifiedremote/src/main/resources/OH-INF/binding/binding.xml b/bundles/org.openhab.binding.unifiedremote/src/main/resources/OH-INF/binding/binding.xml index 86995f66f821f..6c455fb126f3c 100644 --- a/bundles/org.openhab.binding.unifiedremote/src/main/resources/OH-INF/binding/binding.xml +++ b/bundles/org.openhab.binding.unifiedremote/src/main/resources/OH-INF/binding/binding.xml @@ -3,7 +3,6 @@ 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"> - Unified Remote Binding This is the binding for Unified Remote Server (https://www.unifiedremote.com/). Miguel Álvarez diff --git a/bundles/org.openhab.binding.unifiedremote/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.unifiedremote/src/main/resources/OH-INF/thing/thing-types.xml index d56ffd6e65de9..7409898c15125 100644 --- a/bundles/org.openhab.binding.unifiedremote/src/main/resources/OH-INF/thing/thing-types.xml +++ b/bundles/org.openhab.binding.unifiedremote/src/main/resources/OH-INF/thing/thing-types.xml @@ -11,10 +11,11 @@ - + macAddress + network-address Unified Remote Server Hostname From 03b6ebf0d2a2c66b182dc91c4b91f52e476e8985 Mon Sep 17 00:00:00 2001 From: GiviMAD Date: Sun, 18 Oct 2020 20:33:57 +0200 Subject: [PATCH 3/5] [unifiedremote] pr review changes Signed-off-by: GiviMAD --- bundles/org.openhab.binding.unifiedremote/README.md | 5 ++++- .../internal/UnifiedRemoteBindingConstants.java | 1 + .../internal/UnifiedRemoteDiscoveryService.java | 3 ++- .../binding/unifiedremote/internal/UnifiedRemoteHandler.java | 2 +- 4 files changed, 8 insertions(+), 3 deletions(-) diff --git a/bundles/org.openhab.binding.unifiedremote/README.md b/bundles/org.openhab.binding.unifiedremote/README.md index 1483805b7c328..407b23540c364 100644 --- a/bundles/org.openhab.binding.unifiedremote/README.md +++ b/bundles/org.openhab.binding.unifiedremote/README.md @@ -10,7 +10,7 @@ Discovery works on the default discovery UDP port 9511. ## Thing Configuration -Only supported thing is 'Unified Remote Server Thing' witch requires the Hostname to be correctly configured in order to work. +Only supported thing is 'Unified Remote Server Thing' which requires the Hostname to be correctly configured in order to work. | ThinTypeID | description | |----------|------------------------------| @@ -35,10 +35,13 @@ Only supported thing is 'Unified Remote Server Thing' witch requires the Hostnam ## 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" } diff --git a/bundles/org.openhab.binding.unifiedremote/src/main/java/org/openhab/binding/unifiedremote/internal/UnifiedRemoteBindingConstants.java b/bundles/org.openhab.binding.unifiedremote/src/main/java/org/openhab/binding/unifiedremote/internal/UnifiedRemoteBindingConstants.java index ed35bfe84d1f6..e98f9a99579b2 100644 --- a/bundles/org.openhab.binding.unifiedremote/src/main/java/org/openhab/binding/unifiedremote/internal/UnifiedRemoteBindingConstants.java +++ b/bundles/org.openhab.binding.unifiedremote/src/main/java/org/openhab/binding/unifiedremote/internal/UnifiedRemoteBindingConstants.java @@ -39,6 +39,7 @@ public class UnifiedRemoteBindingConstants { 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"; diff --git a/bundles/org.openhab.binding.unifiedremote/src/main/java/org/openhab/binding/unifiedremote/internal/UnifiedRemoteDiscoveryService.java b/bundles/org.openhab.binding.unifiedremote/src/main/java/org/openhab/binding/unifiedremote/internal/UnifiedRemoteDiscoveryService.java index f6b23e692965b..2e1e32eaa8036 100644 --- a/bundles/org.openhab.binding.unifiedremote/src/main/java/org/openhab/binding/unifiedremote/internal/UnifiedRemoteDiscoveryService.java +++ b/bundles/org.openhab.binding.unifiedremote/src/main/java/org/openhab/binding/unifiedremote/internal/UnifiedRemoteDiscoveryService.java @@ -57,12 +57,13 @@ protected void startScan() { private void addNewServer(UnifiedRemoteUdpDiscovery.ServerInfo serverInfo) { Map 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(serverInfo.macAddress) + .withTTL(DISCOVERY_RESULT_TTL_SEC).withRepresentationProperty(PARAMETER_MAC_ADDRESS) .withProperties(properties).withLabel(serverInfo.name).build()); } diff --git a/bundles/org.openhab.binding.unifiedremote/src/main/java/org/openhab/binding/unifiedremote/internal/UnifiedRemoteHandler.java b/bundles/org.openhab.binding.unifiedremote/src/main/java/org/openhab/binding/unifiedremote/internal/UnifiedRemoteHandler.java index 705c3723982ab..6026934558c8e 100644 --- a/bundles/org.openhab.binding.unifiedremote/src/main/java/org/openhab/binding/unifiedremote/internal/UnifiedRemoteHandler.java +++ b/bundles/org.openhab.binding.unifiedremote/src/main/java/org/openhab/binding/unifiedremote/internal/UnifiedRemoteHandler.java @@ -132,7 +132,7 @@ private void initConnectionChecker() { } private void stopConnectionChecker() { - if (connectionCheckerSchedule != null && !connectionCheckerSchedule.isCancelled()) { + if (connectionCheckerSchedule != null) { connectionCheckerSchedule.cancel(true); } connectionCheckerSchedule = null; From b4f98a6d277424bd773d2617e0539b9c1c9b5359 Mon Sep 17 00:00:00 2001 From: GiviMAD Date: Fri, 23 Oct 2020 18:45:27 +0200 Subject: [PATCH 4/5] pr review: fix warnings and minor changes Signed-off-by: GiviMAD --- .../UnifiedRemoteDiscoveryService.java | 7 +--- .../internal/UnifiedRemoteHandler.java | 37 +++++++++---------- 2 files changed, 20 insertions(+), 24 deletions(-) diff --git a/bundles/org.openhab.binding.unifiedremote/src/main/java/org/openhab/binding/unifiedremote/internal/UnifiedRemoteDiscoveryService.java b/bundles/org.openhab.binding.unifiedremote/src/main/java/org/openhab/binding/unifiedremote/internal/UnifiedRemoteDiscoveryService.java index 2e1e32eaa8036..6d49310e6bcc3 100644 --- a/bundles/org.openhab.binding.unifiedremote/src/main/java/org/openhab/binding/unifiedremote/internal/UnifiedRemoteDiscoveryService.java +++ b/bundles/org.openhab.binding.unifiedremote/src/main/java/org/openhab/binding/unifiedremote/internal/UnifiedRemoteDiscoveryService.java @@ -97,15 +97,13 @@ public class ServerInfo { int udpPort; String host; String macAddress; - String publicIp; - ServerInfo(String host, int tcpPort, int udpPort, String name, String macAddress, String publicIp) { + 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; - this.publicIp = publicIp; } } @@ -136,8 +134,7 @@ private ServerInfo tryParseServerDiscovery(DatagramPacket receivePacket) throws int tcpPort = Integer.parseInt(parts[1]); int udpPort = Integer.parseInt(parts[3]); String macAddress = parts[2]; - String publicIp = parts[4]; - return new ServerInfo(host, tcpPort, udpPort, name, macAddress, publicIp); + return new ServerInfo(host, tcpPort, udpPort, name, macAddress); } /** diff --git a/bundles/org.openhab.binding.unifiedremote/src/main/java/org/openhab/binding/unifiedremote/internal/UnifiedRemoteHandler.java b/bundles/org.openhab.binding.unifiedremote/src/main/java/org/openhab/binding/unifiedremote/internal/UnifiedRemoteHandler.java index 6026934558c8e..80732c42db7a6 100644 --- a/bundles/org.openhab.binding.unifiedremote/src/main/java/org/openhab/binding/unifiedremote/internal/UnifiedRemoteHandler.java +++ b/bundles/org.openhab.binding.unifiedremote/src/main/java/org/openhab/binding/unifiedremote/internal/UnifiedRemoteHandler.java @@ -31,8 +31,6 @@ import org.openhab.core.thing.ThingStatusDetail; import org.openhab.core.thing.binding.BaseThingHandler; import org.openhab.core.types.Command; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; /** * The {@link UnifiedRemoteHandler} is responsible for handling commands, which are @@ -43,10 +41,6 @@ @NonNullByDefault public class UnifiedRemoteHandler extends BaseThingHandler { - private Logger logger = LoggerFactory.getLogger(UnifiedRemoteHandler.class); - - private @Nullable UnifiedRemoteConfiguration config; - private @Nullable UnifiedRemoteConnection connection; private @Nullable ScheduledFuture connectionCheckerSchedule; private HttpClient httpClient; @@ -62,29 +56,30 @@ public void handleCommand(ChannelUID channelUID, Command command) { if (!isLinked(channelId)) return; String stringCommand = command.toFullString(); + UnifiedRemoteConnection urConnection = connection; try { - if (connection != null) { + if (urConnection != null) { ContentResponse response; switch (channelId) { case MOUSE_CHANNEL: - response = connection.mouseMove(stringCommand); + response = urConnection.mouseMove(stringCommand); break; case SEND_KEY_CHANNEL: - response = connection.sendKey(stringCommand); + response = urConnection.sendKey(stringCommand); break; default: return; } if (isErrorResponse(response)) { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Session expired"); - connection.authenticate(); + urConnection.authenticate(); updateStatus(ThingStatus.ONLINE); } } else { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Connection not initialized"); } } catch (InterruptedException | ExecutionException | TimeoutException e) { - if (e.getCause() instanceof ConnectException) { + if (e instanceof TimeoutException || e.getCause() instanceof ConnectException) { // we assume thing is offline updateStatus(ThingStatus.OFFLINE); } else { @@ -102,25 +97,28 @@ public void initialize() { } private UnifiedRemoteConnection getNewConnection() { - config = getConfigAs(UnifiedRemoteConfiguration.class); - return new UnifiedRemoteConnection(this.httpClient, config.host); + 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) { - connection.authenticate(); + urConnection.authenticate(); updateStatus(ThingStatus.ONLINE); } else if (status == ThingStatus.ONLINE) { - if (isErrorResponse(connection.keepAlive())) { + if (isErrorResponse(urConnection.keepAlive())) { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Keep alive failed"); } } } catch (InterruptedException | ExecutionException | TimeoutException e) { - if (e.getCause() instanceof ConnectException) { + if (e instanceof TimeoutException || e.getCause() instanceof ConnectException) { // we assume thing is offline updateStatus(ThingStatus.OFFLINE); } else { @@ -132,10 +130,11 @@ private void initConnectionChecker() { } private void stopConnectionChecker() { - if (connectionCheckerSchedule != null) { - connectionCheckerSchedule.cancel(true); + var schedule = connectionCheckerSchedule; + if (schedule != null) { + schedule.cancel(true); + connectionCheckerSchedule = null; } - connectionCheckerSchedule = null; } @Override From 5de85c0cd6193de7503f1e6424a02d768733af58 Mon Sep 17 00:00:00 2001 From: GiviMAD Date: Sat, 24 Oct 2020 10:20:25 +0200 Subject: [PATCH 5/5] pr review and minor changes Signed-off-by: GiviMAD --- .../UnifiedRemoteDiscoveryService.java | 224 +++++++++--------- .../internal/UnifiedRemoteHandler.java | 10 +- 2 files changed, 118 insertions(+), 116 deletions(-) diff --git a/bundles/org.openhab.binding.unifiedremote/src/main/java/org/openhab/binding/unifiedremote/internal/UnifiedRemoteDiscoveryService.java b/bundles/org.openhab.binding.unifiedremote/src/main/java/org/openhab/binding/unifiedremote/internal/UnifiedRemoteDiscoveryService.java index 6d49310e6bcc3..6e230664c4a64 100644 --- a/bundles/org.openhab.binding.unifiedremote/src/main/java/org/openhab/binding/unifiedremote/internal/UnifiedRemoteDiscoveryService.java +++ b/bundles/org.openhab.binding.unifiedremote/src/main/java/org/openhab/binding/unifiedremote/internal/UnifiedRemoteDiscoveryService.java @@ -37,7 +37,7 @@ * * @author Miguel Alvarez - Initial contribution */ -@Component(service = DiscoveryService.class, immediate = true, configurationPid = "discovery.unifiedremote") +@Component(service = DiscoveryService.class, configurationPid = "discovery.unifiedremote") @NonNullByDefault public class UnifiedRemoteDiscoveryService extends AbstractDiscoveryService { @@ -45,17 +45,39 @@ public class UnifiedRemoteDiscoveryService extends AbstractDiscoveryService { 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() { - UnifiedRemoteUdpDiscovery client = new UnifiedRemoteUdpDiscovery(); - client.call(this::addNewServer); + sendBroadcast(this::addNewServer); } - private void addNewServer(UnifiedRemoteUdpDiscovery.ServerInfo serverInfo) { + private void addNewServer(ServerInfo serverInfo) { Map properties = new HashMap<>(); properties.put(PARAMETER_MAC_ADDRESS, serverInfo.macAddress); properties.put(PARAMETER_HOSTNAME, serverInfo.host); @@ -67,121 +89,95 @@ private void addNewServer(UnifiedRemoteUdpDiscovery.ServerInfo serverInfo) { .withProperties(properties).withLabel(serverInfo.name).build()); } - private class UnifiedRemoteUdpDiscovery { - /** - * 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 TIMEOUT_MS = 3000; - - 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; - } - } + /** + * 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; + } - /** - * 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); + } - 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 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; } - - /** - * 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 call(Consumer 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()); - } + 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(); } + } 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; } } } diff --git a/bundles/org.openhab.binding.unifiedremote/src/main/java/org/openhab/binding/unifiedremote/internal/UnifiedRemoteHandler.java b/bundles/org.openhab.binding.unifiedremote/src/main/java/org/openhab/binding/unifiedremote/internal/UnifiedRemoteHandler.java index 80732c42db7a6..78c4f9e509c81 100644 --- a/bundles/org.openhab.binding.unifiedremote/src/main/java/org/openhab/binding/unifiedremote/internal/UnifiedRemoteHandler.java +++ b/bundles/org.openhab.binding.unifiedremote/src/main/java/org/openhab/binding/unifiedremote/internal/UnifiedRemoteHandler.java @@ -16,6 +16,7 @@ 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; @@ -79,7 +80,7 @@ public void handleCommand(ChannelUID channelUID, Command command) { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Connection not initialized"); } } catch (InterruptedException | ExecutionException | TimeoutException e) { - if (e instanceof TimeoutException || e.getCause() instanceof ConnectException) { + if (isThingOfflineException(e)) { // we assume thing is offline updateStatus(ThingStatus.OFFLINE); } else { @@ -118,7 +119,7 @@ private void initConnectionChecker() { } } } catch (InterruptedException | ExecutionException | TimeoutException e) { - if (e instanceof TimeoutException || e.getCause() instanceof ConnectException) { + if (isThingOfflineException(e)) { // we assume thing is offline updateStatus(ThingStatus.OFFLINE); } else { @@ -129,6 +130,11 @@ private void initConnectionChecker() { }, 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) {