2 * Copyright (c) 2010-2023 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.HarmonyHubBindingConstants;
41 import org.openhab.binding.harmonyhub.internal.handler.HarmonyHubHandler;
42 import org.openhab.core.config.discovery.AbstractDiscoveryService;
43 import org.openhab.core.config.discovery.DiscoveryResultBuilder;
44 import org.openhab.core.config.discovery.DiscoveryService;
45 import org.openhab.core.thing.ThingTypeUID;
46 import org.openhab.core.thing.ThingUID;
47 import org.osgi.service.component.annotations.Component;
48 import org.slf4j.Logger;
49 import org.slf4j.LoggerFactory;
52 * The {@link HarmonyHubDiscoveryService} class discovers Harmony hubs and adds the results to the inbox.
54 * @author Dan Cunningham - Initial contribution
55 * @author Wouter Born - Add null annotations
58 @Component(service = DiscoveryService.class, configurationPid = "discovery.harmonyhub")
59 public class HarmonyHubDiscoveryService extends AbstractDiscoveryService {
61 private final Logger logger = LoggerFactory.getLogger(HarmonyHubDiscoveryService.class);
63 // notice the port appended to the end of the string
64 private static final String DISCOVERY_STRING = "_logitech-reverse-bonjour._tcp.local.\n%d";
65 private static final int DISCOVERY_PORT = 5224;
66 private static final int TIMEOUT = 15;
67 private static final long REFRESH = 600;
69 private boolean running;
71 private @Nullable HarmonyServer server;
73 private @Nullable ScheduledFuture<?> broadcastFuture;
74 private @Nullable ScheduledFuture<?> discoveryFuture;
75 private @Nullable ScheduledFuture<?> timeoutFuture;
77 public HarmonyHubDiscoveryService() {
78 super(HarmonyHubHandler.SUPPORTED_THING_TYPES_UIDS, TIMEOUT, true);
82 public Set<ThingTypeUID> getSupportedThingTypes() {
83 return HarmonyHubHandler.SUPPORTED_THING_TYPES_UIDS;
87 public void startScan() {
88 logger.debug("StartScan called");
93 protected void startBackgroundDiscovery() {
94 logger.debug("Start Harmony Hub background discovery");
95 ScheduledFuture<?> localDiscoveryFuture = discoveryFuture;
96 if (localDiscoveryFuture == null || localDiscoveryFuture.isCancelled()) {
97 logger.debug("Start Scan");
98 discoveryFuture = scheduler.scheduleWithFixedDelay(this::startScan, 0, REFRESH, TimeUnit.SECONDS);
103 protected void stopBackgroundDiscovery() {
104 logger.debug("Stop HarmonyHub background discovery");
105 ScheduledFuture<?> localDiscoveryFuture = discoveryFuture;
106 if (localDiscoveryFuture != null && !localDiscoveryFuture.isCancelled()) {
107 localDiscoveryFuture.cancel(true);
108 discoveryFuture = null;
114 * Starts discovery for Harmony Hubs
116 private synchronized void startDiscovery() {
122 final HarmonyServer localServer = new HarmonyServer();
124 server = localServer;
126 broadcastFuture = scheduler.scheduleWithFixedDelay(() -> {
127 sendDiscoveryMessage(String.format(DISCOVERY_STRING, localServer.getPort()));
128 }, 0, 2, TimeUnit.SECONDS);
130 timeoutFuture = scheduler.schedule(this::stopDiscovery, TIMEOUT, TimeUnit.SECONDS);
133 } catch (IOException e) {
134 logger.error("Could not start Harmony discovery server ", e);
139 * Stops discovery of Harmony Hubs
141 private synchronized void stopDiscovery() {
142 ScheduledFuture<?> localBroadcastFuture = broadcastFuture;
143 if (localBroadcastFuture != null) {
144 localBroadcastFuture.cancel(true);
147 ScheduledFuture<?> localTimeoutFuture = timeoutFuture;
148 if (localTimeoutFuture != null) {
149 localTimeoutFuture.cancel(true);
152 HarmonyServer localServer = server;
153 if (localServer != null) {
161 * Send broadcast message over all active interfaces
163 * @param discoverString
164 * String to be used for the discovery
166 private void sendDiscoveryMessage(String discoverString) {
167 try (DatagramSocket bcSend = new DatagramSocket()) {
168 bcSend.setBroadcast(true);
169 byte[] sendData = discoverString.getBytes();
171 // Broadcast the message over all the network interfaces
172 Enumeration<NetworkInterface> interfaces = NetworkInterface.getNetworkInterfaces();
173 while (interfaces.hasMoreElements()) {
175 NetworkInterface networkInterface = interfaces.nextElement();
176 if (networkInterface.isLoopback() || !networkInterface.isUp()) {
179 for (InterfaceAddress interfaceAddress : networkInterface.getInterfaceAddresses()) {
180 InetAddress[] broadcast = new InetAddress[] { InetAddress.getByName("224.0.0.1"),
181 InetAddress.getByName("255.255.255.255"), interfaceAddress.getBroadcast() };
182 for (InetAddress bc : broadcast) {
183 // Send the broadcast package!
186 DatagramPacket sendPacket = new DatagramPacket(sendData, sendData.length, bc,
188 bcSend.send(sendPacket);
189 } catch (IOException e) {
190 logger.debug("IO error during HarmonyHub discovery: {}", e.getMessage());
191 } catch (Exception e) {
192 logger.debug("{}", e.getMessage(), e);
194 logger.trace("Request packet sent to: {} Interface: {}", bc.getHostAddress(),
195 networkInterface.getDisplayName());
200 } catch (IOException e) {
201 logger.debug("IO error during HarmonyHub discovery: {}", e.getMessage());
206 * Server which accepts TCP connections from Harmony Hubs during the discovery process
208 * @author Dan Cunningham - Initial contribution
211 private class HarmonyServer {
212 private final ServerSocket serverSocket;
213 private final List<String> responses = new ArrayList<>();
214 private boolean running;
216 public HarmonyServer() throws IOException {
217 serverSocket = new ServerSocket(0);
218 logger.debug("Creating Harmony server on port {}", getPort());
221 public int getPort() {
222 return serverSocket.getLocalPort();
225 public void start() {
227 Thread localThread = new Thread(this::run,
228 "OH-binding-" + HarmonyHubBindingConstants.BINDING_ID + "discoveryServer");
229 localThread.setDaemon(true);
236 serverSocket.close();
237 } catch (IOException e) {
238 logger.error("Could not stop harmony discovery socket", e);
244 try (Socket socket = serverSocket.accept();
245 Reader isr = new InputStreamReader(socket.getInputStream());
246 BufferedReader in = new BufferedReader(isr)) {
248 while ((input = in.readLine()) != null) {
252 logger.trace("READ {}", input);
253 // response format is key1:value1;key2:value2;key3:value3;
254 Map<String, String> properties = Stream.of(input.split(";")).map(line -> line.split(":", 2))
255 .collect(Collectors.toMap(entry -> entry[0], entry -> entry[1]));
256 String friendlyName = properties.get("friendlyName");
257 String hostName = properties.get("host_name");
258 String ip = properties.get("ip");
259 String uuid = properties.get("uuid");
260 if (friendlyName != null && !friendlyName.isBlank() && hostName != null && !hostName.isBlank()
261 && ip != null && !ip.isBlank() && uuid != null && !uuid.isBlank()
262 && !responses.contains(hostName)) {
263 responses.add(hostName);
264 hubDiscovered(ip, friendlyName, hostName, uuid);
267 } catch (IOException | IndexOutOfBoundsException e) {
269 logger.debug("Error connecting with found hub", e);
276 private void hubDiscovered(String ip, String friendlyName, String hostName, String uuid) {
277 String thingId = hostName.replaceAll("[^A-Za-z0-9\\-_]", "");
278 logger.trace("Adding HarmonyHub {} ({}) at host {}", friendlyName, thingId, ip);
279 ThingUID uid = new ThingUID(HARMONY_HUB_THING_TYPE, thingId);
281 thingDiscovered(DiscoveryResultBuilder.create(uid)
282 .withLabel("HarmonyHub " + friendlyName)
283 .withProperty(HUB_PROPERTY_HOST, ip)
284 .withProperty(HUB_PROPERTY_NAME, friendlyName)
285 .withProperty(HUB_PROPERTY_ID, uuid)
286 .withRepresentationProperty(HUB_PROPERTY_ID)