2 * Copyright (c) 2010-2021 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
7 * This program and the accompanying materials are made available under the
8 * terms of the Eclipse Public License 2.0 which is available at
9 * http://www.eclipse.org/legal/epl-2.0
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.unifiedremote.internal;
15 import static org.openhab.binding.unifiedremote.internal.UnifiedRemoteBindingConstants.*;
17 import java.io.IOException;
19 import java.text.ParseException;
20 import java.util.Arrays;
21 import java.util.HashMap;
23 import java.util.concurrent.TimeUnit;
24 import java.util.function.Consumer;
26 import org.eclipse.jdt.annotation.NonNullByDefault;
27 import org.openhab.core.config.discovery.AbstractDiscoveryService;
28 import org.openhab.core.config.discovery.DiscoveryResultBuilder;
29 import org.openhab.core.config.discovery.DiscoveryService;
30 import org.openhab.core.thing.ThingUID;
31 import org.osgi.service.component.annotations.Component;
32 import org.slf4j.Logger;
33 import org.slf4j.LoggerFactory;
36 * The {@link UnifiedRemoteDiscoveryService} discover Unified Remote Server Instances in the network.
38 * @author Miguel Alvarez - Initial contribution
40 @Component(service = DiscoveryService.class, configurationPid = "discovery.unifiedremote")
42 public class UnifiedRemoteDiscoveryService extends AbstractDiscoveryService {
44 private Logger logger = LoggerFactory.getLogger(UnifiedRemoteDiscoveryService.class);
45 static final int TIMEOUT_MS = 20000;
46 private static final long DISCOVERY_RESULT_TTL_SEC = TimeUnit.MINUTES.toSeconds(5);
49 * Port used for broadcast and listening.
51 public static final int DISCOVERY_PORT = 9511;
53 * String the client sends, to disambiguate packets on this port.
55 public static final String DISCOVERY_REQUEST = "6N T|-Ar-A6N T|-Ar-A6N T|-Ar-A";
57 * String the client sends, to disambiguate packets on this port.
59 public static final String DISCOVERY_RESPONSE_PREFIX = ")-b@ h): :)i)-b@ h): :)i)-b@ h): :)";
61 * String used to replace non printable characters on service response
63 public static final String NON_PRINTABLE_CHARTS_REPLACEMENT = ": :";
65 private static final int MAX_PACKET_SIZE = 2048;
67 * maximum time to wait for a reply, in milliseconds.
69 private static final int SOCKET_TIMEOUT_MS = 3000;
71 public UnifiedRemoteDiscoveryService() {
72 super(SUPPORTED_THING_TYPES, TIMEOUT_MS, false);
76 protected void startScan() {
77 sendBroadcast(this::addNewServer);
80 private void addNewServer(ServerInfo serverInfo) {
81 Map<String, Object> properties = new HashMap<>();
82 properties.put(PARAMETER_MAC_ADDRESS, serverInfo.macAddress);
83 properties.put(PARAMETER_HOSTNAME, serverInfo.host);
84 properties.put(PARAMETER_TCP_PORT, serverInfo.tcpPort);
85 properties.put(PARAMETER_UDP_PORT, serverInfo.udpPort);
87 DiscoveryResultBuilder.create(new ThingUID(THING_TYPE_UNIFIED_REMOTE_SERVER, serverInfo.macAddress))
88 .withTTL(DISCOVERY_RESULT_TTL_SEC).withRepresentationProperty(PARAMETER_MAC_ADDRESS)
89 .withProperties(properties).withLabel(serverInfo.name).build());
93 * Create a UDP socket on the service discovery broadcast port.
95 * @return open DatagramSocket if successful
96 * @throws RuntimeException if cannot create the socket
98 public DatagramSocket createSocket() throws SocketException {
99 DatagramSocket socket;
100 socket = new DatagramSocket();
101 socket.setBroadcast(true);
102 socket.setSoTimeout(TIMEOUT_MS);
106 private ServerInfo tryParseServerDiscovery(DatagramPacket receivePacket) throws ParseException {
107 String host = receivePacket.getAddress().getHostAddress();
108 String reply = new String(receivePacket.getData()).replaceAll("[\\p{C}]", NON_PRINTABLE_CHARTS_REPLACEMENT)
109 .replaceAll("[^\\x00-\\x7F]", NON_PRINTABLE_CHARTS_REPLACEMENT);
110 if (!reply.startsWith(DISCOVERY_RESPONSE_PREFIX))
111 throw new ParseException("Bad discovery response prefix", 0);
112 String[] parts = Arrays
113 .stream(reply.replace(DISCOVERY_RESPONSE_PREFIX, "").split(NON_PRINTABLE_CHARTS_REPLACEMENT))
114 .filter((String e) -> e.length() != 0).toArray(String[]::new);
115 String name = parts[0];
116 int tcpPort = Integer.parseInt(parts[1]);
117 int udpPort = Integer.parseInt(parts[3]);
118 String macAddress = parts[2];
119 return new ServerInfo(host, tcpPort, udpPort, name, macAddress);
123 * Send broadcast packets with service request string until a response
124 * is received. Return the response as String (even though it should
125 * contain an internet address).
127 * @return String received from server. Should be server IP address.
128 * Returns empty string if failed to get valid reply.
130 public void sendBroadcast(Consumer<ServerInfo> listener) {
131 byte[] receiveBuffer = new byte[MAX_PACKET_SIZE];
132 DatagramPacket receivePacket = new DatagramPacket(receiveBuffer, receiveBuffer.length);
134 DatagramSocket socket = null;
136 socket = createSocket();
137 } catch (SocketException e) {
138 logger.debug("Error creating discovery socket: {}", e.getMessage());
141 byte[] packetData = DISCOVERY_REQUEST.getBytes();
143 InetAddress broadcastAddress = InetAddress.getByName("255.255.255.255");
144 int servicePort = DISCOVERY_PORT;
145 DatagramPacket packet = new DatagramPacket(packetData, packetData.length, broadcastAddress, servicePort);
147 logger.debug("Sent packet to {}:{}", broadcastAddress.getHostAddress(), servicePort);
148 for (int i = 0; i < 20; i++) {
149 socket.receive(receivePacket);
150 String host = receivePacket.getAddress().getHostAddress();
151 logger.debug("Received reply from {}", host);
153 ServerInfo serverInfo = tryParseServerDiscovery(receivePacket);
154 listener.accept(serverInfo);
155 } catch (ParseException ex) {
156 logger.debug("Unable to parse server discovery response from {}: {}", host, ex.getMessage());
159 } catch (SocketTimeoutException ste) {
160 logger.debug("SocketTimeoutException during socket operation: {}", ste.getMessage());
161 } catch (IOException ioe) {
162 logger.debug("IOException during socket operation: {}", ioe.getMessage());
168 public class ServerInfo {
175 ServerInfo(String host, int tcpPort, int udpPort, String name, String macAddress) {
177 this.tcpPort = tcpPort;
178 this.udpPort = udpPort;
180 this.macAddress = macAddress;