2 * Copyright (c) 2010-2020 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.harmonyhub.internal.discovery;
15 import static org.openhab.binding.harmonyhub.internal.HarmonyHubBindingConstants.*;
17 import java.io.BufferedReader;
18 import java.io.IOException;
19 import java.io.InputStreamReader;
20 import java.io.Reader;
21 import java.net.DatagramPacket;
22 import java.net.DatagramSocket;
23 import java.net.InetAddress;
24 import java.net.InterfaceAddress;
25 import java.net.NetworkInterface;
26 import java.net.ServerSocket;
27 import java.net.Socket;
28 import java.util.ArrayList;
29 import java.util.Enumeration;
30 import java.util.List;
33 import java.util.concurrent.ScheduledFuture;
34 import java.util.concurrent.TimeUnit;
35 import java.util.stream.Collectors;
36 import java.util.stream.Stream;
38 import org.eclipse.jdt.annotation.NonNullByDefault;
39 import org.eclipse.jdt.annotation.Nullable;
40 import org.openhab.binding.harmonyhub.internal.handler.HarmonyHubHandler;
41 import org.openhab.core.config.discovery.AbstractDiscoveryService;
42 import org.openhab.core.config.discovery.DiscoveryResultBuilder;
43 import org.openhab.core.config.discovery.DiscoveryService;
44 import org.openhab.core.thing.ThingTypeUID;
45 import org.openhab.core.thing.ThingUID;
46 import org.osgi.service.component.annotations.Component;
47 import org.slf4j.Logger;
48 import org.slf4j.LoggerFactory;
51 * The {@link HarmonyHubDiscoveryService} class discovers Harmony hubs and adds the results to the inbox.
53 * @author Dan Cunningham - Initial contribution
54 * @author Wouter Born - Add null annotations
57 @Component(service = DiscoveryService.class, configurationPid = "discovery.harmonyhub")
58 public class HarmonyHubDiscoveryService extends AbstractDiscoveryService {
60 private final Logger logger = LoggerFactory.getLogger(HarmonyHubDiscoveryService.class);
62 // notice the port appended to the end of the string
63 private static final String DISCOVERY_STRING = "_logitech-reverse-bonjour._tcp.local.\n%d";
64 private static final int DISCOVERY_PORT = 5224;
65 private static final int TIMEOUT = 15;
66 private static final long REFRESH = 600;
68 private boolean running;
70 private @Nullable HarmonyServer server;
72 private @Nullable ScheduledFuture<?> broadcastFuture;
73 private @Nullable ScheduledFuture<?> discoveryFuture;
74 private @Nullable ScheduledFuture<?> timeoutFuture;
76 public HarmonyHubDiscoveryService() {
77 super(HarmonyHubHandler.SUPPORTED_THING_TYPES_UIDS, TIMEOUT, true);
81 public Set<ThingTypeUID> getSupportedThingTypes() {
82 return HarmonyHubHandler.SUPPORTED_THING_TYPES_UIDS;
86 public void startScan() {
87 logger.debug("StartScan called");
92 protected void startBackgroundDiscovery() {
93 logger.debug("Start Harmony Hub background discovery");
94 ScheduledFuture<?> localDiscoveryFuture = discoveryFuture;
95 if (localDiscoveryFuture == null || localDiscoveryFuture.isCancelled()) {
96 logger.debug("Start Scan");
97 discoveryFuture = scheduler.scheduleWithFixedDelay(this::startScan, 0, REFRESH, TimeUnit.SECONDS);
102 protected void stopBackgroundDiscovery() {
103 logger.debug("Stop HarmonyHub background discovery");
104 ScheduledFuture<?> localDiscoveryFuture = discoveryFuture;
105 if (localDiscoveryFuture != null && !localDiscoveryFuture.isCancelled()) {
106 localDiscoveryFuture.cancel(true);
107 discoveryFuture = null;
113 * Starts discovery for Harmony Hubs
115 private synchronized void startDiscovery() {
121 final HarmonyServer localServer = new HarmonyServer();
123 server = localServer;
125 broadcastFuture = scheduler.scheduleWithFixedDelay(() -> {
126 sendDiscoveryMessage(String.format(DISCOVERY_STRING, localServer.getPort()));
127 }, 0, 2, TimeUnit.SECONDS);
129 timeoutFuture = scheduler.schedule(this::stopDiscovery, TIMEOUT, TimeUnit.SECONDS);
132 } catch (IOException e) {
133 logger.error("Could not start Harmony discovery server ", e);
138 * Stops discovery of Harmony Hubs
140 private synchronized void stopDiscovery() {
141 ScheduledFuture<?> localBroadcastFuture = broadcastFuture;
142 if (localBroadcastFuture != null) {
143 localBroadcastFuture.cancel(true);
146 ScheduledFuture<?> localTimeoutFuture = timeoutFuture;
147 if (localTimeoutFuture != null) {
148 localTimeoutFuture.cancel(true);
151 HarmonyServer localServer = server;
152 if (localServer != null) {
160 * Send broadcast message over all active interfaces
162 * @param discoverString
163 * String to be used for the discovery
165 private void sendDiscoveryMessage(String discoverString) {
166 try (DatagramSocket bcSend = new DatagramSocket()) {
167 bcSend.setBroadcast(true);
168 byte[] sendData = discoverString.getBytes();
170 // Broadcast the message over all the network interfaces
171 Enumeration<NetworkInterface> interfaces = NetworkInterface.getNetworkInterfaces();
172 while (interfaces.hasMoreElements()) {
173 NetworkInterface networkInterface = interfaces.nextElement();
174 if (networkInterface.isLoopback() || !networkInterface.isUp()) {
177 for (InterfaceAddress interfaceAddress : networkInterface.getInterfaceAddresses()) {
178 InetAddress[] broadcast = new InetAddress[] { InetAddress.getByName("224.0.0.1"),
179 InetAddress.getByName("255.255.255.255"), interfaceAddress.getBroadcast() };
180 for (InetAddress bc : broadcast) {
181 // Send the broadcast package!
184 DatagramPacket sendPacket = new DatagramPacket(sendData, sendData.length, bc,
186 bcSend.send(sendPacket);
187 } catch (IOException e) {
188 logger.debug("IO error during HarmonyHub discovery: {}", e.getMessage());
189 } catch (Exception e) {
190 logger.debug("{}", e.getMessage(), e);
192 logger.trace("Request packet sent to: {} Interface: {}", bc.getHostAddress(),
193 networkInterface.getDisplayName());
198 } catch (IOException e) {
199 logger.debug("IO error during HarmonyHub discovery: {}", e.getMessage());
204 * Server which accepts TCP connections from Harmony Hubs during the discovery process
206 * @author Dan Cunningham - Initial contribution
209 private class HarmonyServer {
210 private final ServerSocket serverSocket;
211 private final List<String> responses = new ArrayList<>();
212 private boolean running;
214 public HarmonyServer() throws IOException {
215 serverSocket = new ServerSocket(0);
216 logger.debug("Creating Harmony server on port {}", getPort());
219 public int getPort() {
220 return serverSocket.getLocalPort();
223 public void start() {
225 Thread localThread = new Thread(this::run, "HarmonyDiscoveryServer(tcp/" + getPort() + ")");
232 serverSocket.close();
233 } catch (IOException e) {
234 logger.error("Could not stop harmony discovery socket", e);
240 try (Socket socket = serverSocket.accept();
241 Reader isr = new InputStreamReader(socket.getInputStream());
242 BufferedReader in = new BufferedReader(isr)) {
244 while ((input = in.readLine()) != null) {
248 logger.trace("READ {}", input);
249 // response format is key1:value1;key2:value2;key3:value3;
250 Map<String, String> properties = Stream.of(input.split(";")).map(line -> line.split(":", 2))
251 .collect(Collectors.toMap(entry -> entry[0], entry -> entry[1]));
252 String friendlyName = properties.get("friendlyName");
253 String hostName = properties.get("host_name");
254 String ip = properties.get("ip");
255 if (friendlyName != null && !friendlyName.isBlank() && hostName != null && !hostName.isBlank()
256 && ip != null && !ip.isBlank() && !responses.contains(hostName)) {
257 responses.add(hostName);
258 hubDiscovered(ip, friendlyName, hostName);
261 } catch (IOException | IndexOutOfBoundsException e) {
263 logger.debug("Error connecting with found hub", e);
270 private void hubDiscovered(String ip, String friendlyName, String hostName) {
271 String thingId = hostName.replaceAll("[^A-Za-z0-9\\-_]", "");
272 logger.trace("Adding HarmonyHub {} ({}) at host {}", friendlyName, thingId, ip);
273 ThingUID uid = new ThingUID(HARMONY_HUB_THING_TYPE, thingId);
275 thingDiscovered(DiscoveryResultBuilder.create(uid)
276 .withLabel("HarmonyHub " + friendlyName)
277 .withProperty(HUB_PROPERTY_HOST, ip)
278 .withProperty(HUB_PROPERTY_NAME, friendlyName)