]> git.basschouten.com Git - openhab-addons.git/blob
7ea80306aa6e6f5df92191bcc7ce4198122ac611
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2020 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
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
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.harmonyhub.internal.discovery;
14
15 import static org.openhab.binding.harmonyhub.internal.HarmonyHubBindingConstants.*;
16
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;
31 import java.util.Map;
32 import java.util.Set;
33 import java.util.concurrent.ScheduledFuture;
34 import java.util.concurrent.TimeUnit;
35 import java.util.stream.Collectors;
36 import java.util.stream.Stream;
37
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;
49
50 /**
51  * The {@link HarmonyHubDiscoveryService} class discovers Harmony hubs and adds the results to the inbox.
52  *
53  * @author Dan Cunningham - Initial contribution
54  * @author Wouter Born - Add null annotations
55  */
56 @NonNullByDefault
57 @Component(service = DiscoveryService.class, configurationPid = "discovery.harmonyhub")
58 public class HarmonyHubDiscoveryService extends AbstractDiscoveryService {
59
60     private final Logger logger = LoggerFactory.getLogger(HarmonyHubDiscoveryService.class);
61
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;
67
68     private boolean running;
69
70     private @Nullable HarmonyServer server;
71
72     private @Nullable ScheduledFuture<?> broadcastFuture;
73     private @Nullable ScheduledFuture<?> discoveryFuture;
74     private @Nullable ScheduledFuture<?> timeoutFuture;
75
76     public HarmonyHubDiscoveryService() {
77         super(HarmonyHubHandler.SUPPORTED_THING_TYPES_UIDS, TIMEOUT, true);
78     }
79
80     @Override
81     public Set<ThingTypeUID> getSupportedThingTypes() {
82         return HarmonyHubHandler.SUPPORTED_THING_TYPES_UIDS;
83     }
84
85     @Override
86     public void startScan() {
87         logger.debug("StartScan called");
88         startDiscovery();
89     }
90
91     @Override
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);
98         }
99     }
100
101     @Override
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;
108         }
109         stopDiscovery();
110     }
111
112     /**
113      * Starts discovery for Harmony Hubs
114      */
115     private synchronized void startDiscovery() {
116         if (running) {
117             return;
118         }
119
120         try {
121             final HarmonyServer localServer = new HarmonyServer();
122             localServer.start();
123             server = localServer;
124
125             broadcastFuture = scheduler.scheduleWithFixedDelay(() -> {
126                 sendDiscoveryMessage(String.format(DISCOVERY_STRING, localServer.getPort()));
127             }, 0, 2, TimeUnit.SECONDS);
128
129             timeoutFuture = scheduler.schedule(this::stopDiscovery, TIMEOUT, TimeUnit.SECONDS);
130
131             running = true;
132         } catch (IOException e) {
133             logger.error("Could not start Harmony discovery server ", e);
134         }
135     }
136
137     /**
138      * Stops discovery of Harmony Hubs
139      */
140     private synchronized void stopDiscovery() {
141         ScheduledFuture<?> localBroadcastFuture = broadcastFuture;
142         if (localBroadcastFuture != null) {
143             localBroadcastFuture.cancel(true);
144         }
145
146         ScheduledFuture<?> localTimeoutFuture = timeoutFuture;
147         if (localTimeoutFuture != null) {
148             localTimeoutFuture.cancel(true);
149         }
150
151         HarmonyServer localServer = server;
152         if (localServer != null) {
153             localServer.stop();
154         }
155
156         running = false;
157     }
158
159     /**
160      * Send broadcast message over all active interfaces
161      *
162      * @param discoverString
163      *            String to be used for the discovery
164      */
165     private void sendDiscoveryMessage(String discoverString) {
166         try (DatagramSocket bcSend = new DatagramSocket()) {
167             bcSend.setBroadcast(true);
168             byte[] sendData = discoverString.getBytes();
169
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()) {
175                     continue;
176                 }
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!
182                         if (bc != null) {
183                             try {
184                                 DatagramPacket sendPacket = new DatagramPacket(sendData, sendData.length, bc,
185                                         DISCOVERY_PORT);
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);
191                             }
192                             logger.trace("Request packet sent to: {} Interface: {}", bc.getHostAddress(),
193                                     networkInterface.getDisplayName());
194                         }
195                     }
196                 }
197             }
198         } catch (IOException e) {
199             logger.debug("IO error during HarmonyHub discovery: {}", e.getMessage());
200         }
201     }
202
203     /**
204      * Server which accepts TCP connections from Harmony Hubs during the discovery process
205      *
206      * @author Dan Cunningham - Initial contribution
207      *
208      */
209     private class HarmonyServer {
210         private final ServerSocket serverSocket;
211         private final List<String> responses = new ArrayList<>();
212         private boolean running;
213
214         public HarmonyServer() throws IOException {
215             serverSocket = new ServerSocket(0);
216             logger.debug("Creating Harmony server on port {}", getPort());
217         }
218
219         public int getPort() {
220             return serverSocket.getLocalPort();
221         }
222
223         public void start() {
224             running = true;
225             Thread localThread = new Thread(this::run, "HarmonyDiscoveryServer(tcp/" + getPort() + ")");
226             localThread.start();
227         }
228
229         public void stop() {
230             running = false;
231             try {
232                 serverSocket.close();
233             } catch (IOException e) {
234                 logger.error("Could not stop harmony discovery socket", e);
235             }
236         }
237
238         private void run() {
239             while (running) {
240                 try (Socket socket = serverSocket.accept();
241                         Reader isr = new InputStreamReader(socket.getInputStream());
242                         BufferedReader in = new BufferedReader(isr)) {
243                     String input;
244                     while ((input = in.readLine()) != null) {
245                         if (!running) {
246                             break;
247                         }
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);
259                         }
260                     }
261                 } catch (IOException | IndexOutOfBoundsException e) {
262                     if (running) {
263                         logger.debug("Error connecting with found hub", e);
264                     }
265                 }
266             }
267         }
268     }
269
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);
274         // @formatter:off
275         thingDiscovered(DiscoveryResultBuilder.create(uid)
276                 .withLabel("HarmonyHub " + friendlyName)
277                 .withProperty(HUB_PROPERTY_HOST, ip)
278                 .withProperty(HUB_PROPERTY_NAME, friendlyName)
279                 .build());
280         // @formatter:on
281     }
282 }