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.venstarthermostat.internal.discovery;
15 import java.io.IOException;
16 import java.io.UnsupportedEncodingException;
17 import java.net.DatagramPacket;
18 import java.net.Inet4Address;
19 import java.net.InetAddress;
20 import java.net.InetSocketAddress;
21 import java.net.MulticastSocket;
22 import java.net.NetworkInterface;
23 import java.net.SocketException;
24 import java.net.UnknownHostException;
25 import java.util.Enumeration;
26 import java.util.Scanner;
27 import java.util.concurrent.ScheduledFuture;
28 import java.util.concurrent.TimeUnit;
29 import java.util.regex.Matcher;
30 import java.util.regex.Pattern;
32 import org.eclipse.jdt.annotation.NonNullByDefault;
33 import org.eclipse.jdt.annotation.Nullable;
34 import org.openhab.binding.venstarthermostat.internal.VenstarThermostatBindingConstants;
35 import org.openhab.core.config.discovery.AbstractDiscoveryService;
36 import org.openhab.core.config.discovery.DiscoveryResult;
37 import org.openhab.core.config.discovery.DiscoveryResultBuilder;
38 import org.openhab.core.config.discovery.DiscoveryService;
39 import org.openhab.core.thing.ThingUID;
40 import org.osgi.service.component.annotations.Component;
41 import org.slf4j.Logger;
42 import org.slf4j.LoggerFactory;
45 * The {@link VenstarThermostatDiscoveryService} is responsible for discovery of
46 * Venstar thermostats on the local network
48 * @author William Welliver - Initial contribution
49 * @author Dan Cunningham - Refactoring and Improvements
53 @Component(service = DiscoveryService.class, configurationPid = "discovery.venstarthermostat")
54 public class VenstarThermostatDiscoveryService extends AbstractDiscoveryService {
55 private final Logger logger = LoggerFactory.getLogger(VenstarThermostatDiscoveryService.class);
56 private static final String COLOR_TOUCH_DISCOVERY_MESSAGE = "M-SEARCH * HTTP/1.1\r\n"
57 + "Host: 239.255.255.250:1900\r\n" + "Man: ssdp:discover\r\n" + "ST: colortouch:ecp\r\n" + "\r\n";
58 private static final Pattern USN_PATTERN = Pattern
59 .compile("^(colortouch:)?ecp((?::[0-9a-fA-F]{2}){6}):name:(.+)(?::type:(\\w+))");
60 private static final String SSDP_MATCH = "colortouch:ecp";
61 private static final int BACKGROUND_SCAN_INTERVAL_SECONDS = 300;
63 private @Nullable ScheduledFuture<?> scheduledFuture = null;
65 public VenstarThermostatDiscoveryService() {
66 super(VenstarThermostatBindingConstants.SUPPORTED_THING_TYPES, 30, true);
70 protected void startBackgroundDiscovery() {
71 logger.debug("Starting Background Scan");
72 stopBackgroundDiscovery();
73 scheduledFuture = scheduler.scheduleWithFixedDelay(this::doRunRun, 0, BACKGROUND_SCAN_INTERVAL_SECONDS,
78 protected void stopBackgroundDiscovery() {
79 ScheduledFuture<?> scheduledFutureLocal = scheduledFuture;
80 if (scheduledFutureLocal != null && !scheduledFutureLocal.isCancelled()) {
81 scheduledFutureLocal.cancel(true);
86 protected void startScan() {
87 logger.debug("Starting Interactive Scan");
91 protected synchronized void doRunRun() {
92 logger.trace("Sending SSDP discover.");
93 for (int i = 0; i < 5; i++) {
95 Enumeration<NetworkInterface> nets = NetworkInterface.getNetworkInterfaces();
96 while (nets.hasMoreElements()) {
97 NetworkInterface ni = nets.nextElement();
98 MulticastSocket socket = sendDiscoveryBroacast(ni);
100 scanResposesForKeywords(socket);
103 } catch (IOException e) {
104 logger.debug("Error discoverying devices", e);
110 * Broadcasts a SSDP discovery message into the network to find provided
113 * @return The Socket the answers will arrive at.
114 * @throws UnknownHostException
115 * @throws IOException
116 * @throws SocketException
117 * @throws UnsupportedEncodingException
119 private @Nullable MulticastSocket sendDiscoveryBroacast(NetworkInterface ni)
120 throws UnknownHostException, SocketException, UnsupportedEncodingException {
121 InetAddress m = InetAddress.getByName("239.255.255.250");
122 final int port = 1900;
124 logger.trace("Considering {}", ni.getName());
126 if (!ni.isUp() || !ni.supportsMulticast()) {
127 logger.trace("skipping interface {}", ni.getName());
131 Enumeration<InetAddress> addrs = ni.getInetAddresses();
132 InetAddress a = null;
133 while (addrs.hasMoreElements()) {
134 a = addrs.nextElement();
135 if (a instanceof Inet4Address) {
142 logger.trace("no ipv4 address on {}", ni.getName());
146 // for whatever reason, the venstar thermostat responses will not be seen
147 // if we bind this socket to a particular address.
148 // this seems to be okay on linux systems, but osx apparently prefers ipv6, so this
149 // prevents responses from being received unless the ipv4 stack is given preference.
150 MulticastSocket socket = new MulticastSocket(new InetSocketAddress(port));
151 socket.setSoTimeout(2000);
152 socket.setReuseAddress(true);
153 socket.setNetworkInterface(ni);
156 logger.trace("Joined UPnP Multicast group on Interface: {}", ni.getName());
157 byte[] requestMessage = COLOR_TOUCH_DISCOVERY_MESSAGE.getBytes("UTF-8");
158 DatagramPacket datagramPacket = new DatagramPacket(requestMessage, requestMessage.length, m, port);
159 socket.send(datagramPacket);
161 } catch (IOException e) {
162 logger.trace("got ioexception: {}", e.getMessage());
169 * Scans all messages that arrive on the socket and scans them for the
170 * search keywords. The search is not case sensitive.
173 * The socket where the answers arrive.
175 * The keywords to be searched for.
177 * @throws IOException
179 private void scanResposesForKeywords(MulticastSocket socket, String... keywords) throws IOException {
180 // In the worst case a SocketTimeoutException raises
182 byte[] rxbuf = new byte[8192];
183 DatagramPacket packet = new DatagramPacket(rxbuf, rxbuf.length);
185 socket.receive(packet);
186 } catch (IOException e) {
187 logger.trace("Got exception while trying to receive UPnP packets: {}", e.getMessage());
190 String response = new String(packet.getData());
191 if (response.contains(SSDP_MATCH)) {
192 logger.trace("Match: {} ", response);
193 parseResponse(response);
198 protected void parseResponse(String response) {
199 DiscoveryResult result;
205 Scanner scanner = new Scanner(response);
206 while (scanner.hasNextLine()) {
207 String line = scanner.nextLine();
208 String[] pair = line.split(":", 2);
209 if (pair.length != 2) {
212 String key = pair[0].toLowerCase();
213 String value = pair[1].trim();
214 logger.trace("key: {} value: {}.", key, value);
220 Matcher m = USN_PATTERN.matcher(value);
232 logger.trace("Found thermostat, name: {} uuid: {} url: {}", name, uuid, url);
234 if (name == null || uuid == null || url == null) {
235 logger.trace("Bad Format from thermostat");
239 uuid = uuid.replace(":", "").toLowerCase();
241 ThingUID thingUid = new ThingUID(VenstarThermostatBindingConstants.THING_TYPE_COLOR_TOUCH, uuid);
243 logger.trace("Got discovered device.");
245 String label = String.format("Venstar Thermostat (%s)", name);
246 result = DiscoveryResultBuilder.create(thingUid).withLabel(label)
247 .withRepresentationProperty(VenstarThermostatBindingConstants.PROPERTY_UUID)
248 .withProperty(VenstarThermostatBindingConstants.PROPERTY_UUID, uuid)
249 .withProperty(VenstarThermostatBindingConstants.PROPERTY_URL, url).build();
250 logger.trace("New venstar thermostat discovered with ID=<{}>", uuid);
251 this.thingDiscovered(result);