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.velux.internal.discovery;
15 import java.io.ByteArrayInputStream;
16 import java.io.ByteArrayOutputStream;
17 import java.io.Closeable;
18 import java.io.DataInputStream;
19 import java.io.DataOutputStream;
20 import java.io.IOException;
21 import java.net.DatagramPacket;
22 import java.net.DatagramSocket;
23 import java.net.InetAddress;
24 import java.net.MulticastSocket;
25 import java.net.SocketTimeoutException;
26 import java.nio.charset.StandardCharsets;
27 import java.util.HashSet;
28 import java.util.Random;
30 import java.util.concurrent.Callable;
31 import java.util.concurrent.ExecutionException;
32 import java.util.concurrent.Future;
33 import java.util.concurrent.ScheduledExecutorService;
35 import org.eclipse.jdt.annotation.NonNullByDefault;
36 import org.eclipse.jdt.annotation.Nullable;
37 import org.slf4j.Logger;
38 import org.slf4j.LoggerFactory;
41 * Class that uses Multicast DNS (mDNS) to discover Velux Bridges and return their ipv4 addresses
43 * @author Andrew Fiddian-Green - Initial contribution
46 public class VeluxBridgeFinder implements Closeable {
48 private final Logger logger = LoggerFactory.getLogger(VeluxBridgeFinder.class);
51 private static final int BUFFER_SIZE = 256;
52 private static final int SLEEP_MSECS = 100;
53 private static final int SOCKET_TIMEOUT_MSECS = 500;
54 private static final int SEARCH_DURATION_MSECS = 5000;
55 private static final int REPEAT_COUNT = 3;
57 // dns communication constants
58 private static final int MDNS_PORT = 5353;
59 private static final String MDNS_ADDR = "224.0.0.251";
62 private static final short FLAGS_QR = (short) 0x8000;
63 private static final short FLAGS_AA = 0x0400;
65 // dns message class constants
66 private static final short CLASS_IN = 0x0001;
67 private static final short CLASS_MASK = 0x7FFF;
69 // dns message type constants
70 private static final short TYPE_PTR = 0x000c;
72 private static final byte NULL = 0x00;
74 // Velux bridge identifiers
75 private static final String KLF_SERVICE_ID = "_http._tcp.local";
76 private static final String KLF_HOST_PREFIX = "VELUX_KLF_";
78 private short randomQueryId;
79 private ScheduledExecutorService executor;
80 private @Nullable Listener listener = null;
82 private class Listener implements Callable<Set<String>> {
84 private boolean interrupted = false;
85 private boolean started = false;
87 public void interrupt() {
91 public boolean hasStarted() {
96 * Listens for Velux Bridges and returns their IP addresses. It loops for SEARCH_DURATION_MSECS or until
97 * 'interrupt()' or 'Thread.interrupted()' are called when it terminates early after the next socket read
98 * timeout i.e. after SOCKET_TIMEOUT_MSECS
100 * @return a set of strings containing dotted IP addresses e.g. '123.123.123.123'
103 public Set<String> call() throws Exception {
104 final Set<String> ipAddresses = new HashSet<>();
106 // create a multicast listener socket
107 try (MulticastSocket rcvSocket = new MulticastSocket(MDNS_PORT)) {
109 final byte[] rcvBytes = new byte[BUFFER_SIZE];
110 final long finishTime = System.currentTimeMillis() + SEARCH_DURATION_MSECS;
112 rcvSocket.setReuseAddress(true);
113 rcvSocket.joinGroup(InetAddress.getByName(MDNS_ADDR));
114 rcvSocket.setSoTimeout(SOCKET_TIMEOUT_MSECS);
116 // tell the caller that we are ready to roll
119 // loop until time out or internally or externally interrupted
120 while ((System.currentTimeMillis() < finishTime) && (!interrupted) && (!Thread.interrupted())) {
122 DatagramPacket rcvPacket = new DatagramPacket(rcvBytes, rcvBytes.length);
124 rcvSocket.receive(rcvPacket);
125 if (isKlfLanResponse(rcvPacket.getData())) {
126 ipAddresses.add(rcvPacket.getAddress().getHostAddress());
128 } catch (SocketTimeoutException e) {
129 // time out is ok, continue listening
133 } catch (IOException e) {
134 logger.debug("listenerRunnable(): udp socket exception '{}'", e.getMessage());
136 // prevent caller waiting forever in case start up failed
143 * Build an mDNS query package to query SERVICE_ID looking for host names
145 * @return a byte array containing the query datagram payload, or an empty array if failed
147 private byte[] buildQuery() {
148 ByteArrayOutputStream byteStream = new ByteArrayOutputStream(BUFFER_SIZE);
149 DataOutputStream dataStream = new DataOutputStream(byteStream);
151 dataStream.writeShort(randomQueryId); // id
152 dataStream.writeShort(0); // flags
153 dataStream.writeShort(1); // qdCount
154 dataStream.writeShort(0); // anCount
155 dataStream.writeShort(0); // nsCount
156 dataStream.writeShort(0); // arCount
157 for (String segString : KLF_SERVICE_ID.split("\\.")) {
158 byte[] segBytes = segString.getBytes(StandardCharsets.UTF_8);
159 dataStream.writeByte(segBytes.length); // length
160 dataStream.write(segBytes); // byte string
162 dataStream.writeByte(NULL); // end of name
163 dataStream.writeShort(TYPE_PTR); // type
164 dataStream.writeShort(CLASS_IN); // class
165 return byteStream.toByteArray();
166 } catch (IOException e) {
173 * Parse an mDNS response package and check if it is from a KLF bridge
175 * @param responsePayload a byte array containing the response datagram payload
176 * @return true if the response is from a KLF bridge
178 private boolean isKlfLanResponse(byte[] responsePayload) {
179 DataInputStream dataStream = new DataInputStream(new ByteArrayInputStream(responsePayload));
181 // check if the package id matches the query
182 short id = dataStream.readShort();
183 if (id == randomQueryId) {
184 short flags = dataStream.readShort();
185 boolean isResponse = (flags & FLAGS_QR) == FLAGS_QR;
186 boolean isAuthoritative = (flags & FLAGS_AA) == FLAGS_AA;
188 // check if it is an authoritative response
189 if (isResponse && isAuthoritative) {
190 short qdCount = dataStream.readShort();
191 short anCount = dataStream.readShort();
193 dataStream.readShort(); // nsCount
194 dataStream.readShort(); // arCount
196 // check it is an answer (and not a query)
197 if ((anCount == 0) || (qdCount != 0)) {
202 for (short an = 0; an < anCount; an++) {
204 byte[] str = new byte[BUFFER_SIZE];
207 while ((segLength = dataStream.readByte()) > 0) {
208 i += dataStream.read(str, i, segLength);
212 String name = new String(str, 0, i, StandardCharsets.UTF_8);
213 short typ = dataStream.readShort();
214 short clazz = (short) (CLASS_MASK & dataStream.readShort());
215 if (!(name.startsWith(KLF_SERVICE_ID)) || (typ != TYPE_PTR) || (clazz != CLASS_IN)) {
219 // if we got here, the name and response type are valid
220 dataStream.readInt(); // TTL
221 dataStream.readShort(); // dataLen
223 // parse the host name
225 while ((segLength = dataStream.readByte()) > 0) {
226 i += dataStream.read(str, i, segLength);
231 // check if the host name matches
232 String host = new String(str, 0, i, StandardCharsets.UTF_8);
233 if (host.startsWith(KLF_HOST_PREFIX)) {
239 } catch (IOException e) {
246 * Private synchronized method that searches for Velux Bridges and returns their IP addresses. Takes
247 * SEARCH_DURATION_MSECS to complete.
249 * @return a set of strings containing dotted IP addresses e.g. '123.123.123.123'
251 private synchronized Set<String> discoverBridgeIpAddresses() {
253 Set<String> result = null;
255 // create a random query id
256 Random random = new Random();
257 randomQueryId = (short) random.nextInt(Short.MAX_VALUE);
259 // create the listener task and start it
260 Listener listener = this.listener = new Listener();
262 // create a datagram socket
263 try (DatagramSocket socket = new DatagramSocket()) {
264 // prepare query packet
265 byte[] dnsBytes = buildQuery();
266 DatagramPacket dnsPacket = new DatagramPacket(dnsBytes, 0, dnsBytes.length,
267 InetAddress.getByName(MDNS_ADDR), MDNS_PORT);
269 // create listener and wait until it has started
270 Future<Set<String>> future = executor.submit(listener);
271 while (!listener.hasStarted()) {
272 Thread.sleep(SLEEP_MSECS);
275 // send the query several times
276 for (int i = 0; i < REPEAT_COUNT; i++) {
277 // send the query several times
278 socket.send(dnsPacket);
279 Thread.sleep(SLEEP_MSECS);
282 // wait for the listener future to get the result
283 result = future.get();
284 } catch (InterruptedException | IOException | ExecutionException e) {
285 logger.debug("discoverBridgeIpAddresses(): unexpected exception '{}'", e.getMessage());
288 // clean up listener task (just in case) and return
289 listener.interrupt();
290 this.listener = null;
291 return result != null ? result : new HashSet<>();
297 * @param executor the caller's task executor
299 public VeluxBridgeFinder(ScheduledExecutorService executor) {
300 this.executor = executor;
304 * Interrupt the {@link Listener}
306 * @throws IOException (not)
309 public void close() throws IOException {
310 Listener listener = this.listener;
311 if (listener != null) {
312 listener.interrupt();
313 this.listener = null;
318 * Static method to search for Velux Bridges and return their IP addresses. NOTE: it takes SEARCH_DURATION_MSECS to
319 * complete, so don't call it on the main thread!
321 * @return set of dotted IP address e.g. '123.123.123.123'
323 public static Set<String> discoverIpAddresses(ScheduledExecutorService scheduler) {
324 try (VeluxBridgeFinder finder = new VeluxBridgeFinder(scheduler)) {
325 return finder.discoverBridgeIpAddresses();
326 } catch (IOException e) {
327 return new HashSet<>();