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.openhab.binding.venstarthermostat.internal.VenstarThermostatBindingConstants;
33 import org.openhab.core.config.discovery.AbstractDiscoveryService;
34 import org.openhab.core.config.discovery.DiscoveryResult;
35 import org.openhab.core.config.discovery.DiscoveryResultBuilder;
36 import org.openhab.core.config.discovery.DiscoveryService;
37 import org.openhab.core.thing.ThingUID;
38 import org.osgi.service.component.annotations.Component;
39 import org.slf4j.Logger;
40 import org.slf4j.LoggerFactory;
43 * The {@link VenstarThermostatDiscoveryService} is responsible for discovery of
44 * Venstar thermostats on the local network
46 * @author William Welliver - Initial contribution
47 * @author Dan Cunningham - Refactoring and Improvements
50 @Component(service = DiscoveryService.class, configurationPid = "discovery.venstarthermostat")
51 public class VenstarThermostatDiscoveryService extends AbstractDiscoveryService {
52 private final Logger logger = LoggerFactory.getLogger(VenstarThermostatDiscoveryService.class);
53 private static final String COLOR_TOUCH_DISCOVERY_MESSAGE = "M-SEARCH * HTTP/1.1\r\n"
54 + "Host: 239.255.255.250:1900\r\n" + "Man: ssdp:discover\r\n" + "ST: colortouch:ecp\r\n" + "\r\n";
55 private static final Pattern USN_PATTERN = Pattern
56 .compile("^(colortouch:)?ecp((?::[0-9a-fA-F]{2}){6}):name:(.+)(?::type:(\\w+))");
57 private static final String SSDP_MATCH = "colortouch:ecp";
58 private static final int BACKGROUND_SCAN_INTERVAL_SECONDS = 300;
60 private ScheduledFuture<?> scheduledFuture = null;
62 public VenstarThermostatDiscoveryService() {
63 super(VenstarThermostatBindingConstants.SUPPORTED_THING_TYPES, 30, true);
67 protected void startBackgroundDiscovery() {
68 logger.debug("Starting Background Scan");
69 stopBackgroundDiscovery();
70 scheduledFuture = scheduler.scheduleAtFixedRate(this::doRunRun, 0, BACKGROUND_SCAN_INTERVAL_SECONDS,
75 protected void stopBackgroundDiscovery() {
76 if (scheduledFuture != null && !scheduledFuture.isCancelled()) {
77 scheduledFuture.cancel(true);
82 protected void startScan() {
83 logger.debug("Starting Interactive Scan");
87 protected synchronized void doRunRun() {
88 logger.trace("Sending SSDP discover.");
89 for (int i = 0; i < 5; i++) {
91 Enumeration<NetworkInterface> nets = NetworkInterface.getNetworkInterfaces();
92 while (nets.hasMoreElements()) {
93 NetworkInterface ni = nets.nextElement();
94 MulticastSocket socket = sendDiscoveryBroacast(ni);
96 scanResposesForKeywords(socket);
99 } catch (IOException e) {
100 logger.debug("Error discoverying devices", e);
106 * Broadcasts a SSDP discovery message into the network to find provided
109 * @return The Socket the answers will arrive at.
110 * @throws UnknownHostException
111 * @throws IOException
112 * @throws SocketException
113 * @throws UnsupportedEncodingException
115 private MulticastSocket sendDiscoveryBroacast(NetworkInterface ni)
116 throws UnknownHostException, SocketException, UnsupportedEncodingException {
117 InetAddress m = InetAddress.getByName("239.255.255.250");
118 final int port = 1900;
120 logger.trace("Considering {}", ni.getName());
122 if (!ni.isUp() || !ni.supportsMulticast()) {
123 logger.trace("skipping interface {}", ni.getName());
127 Enumeration<InetAddress> addrs = ni.getInetAddresses();
128 InetAddress a = null;
129 while (addrs.hasMoreElements()) {
130 a = addrs.nextElement();
131 if (a instanceof Inet4Address) {
138 logger.trace("no ipv4 address on {}", ni.getName());
142 // for whatever reason, the venstar thermostat responses will not be seen
143 // if we bind this socket to a particular address.
144 // this seems to be okay on linux systems, but osx apparently prefers ipv6, so this
145 // prevents responses from being received unless the ipv4 stack is given preference.
146 MulticastSocket socket = new MulticastSocket(new InetSocketAddress(port));
147 socket.setSoTimeout(2000);
148 socket.setReuseAddress(true);
149 socket.setNetworkInterface(ni);
152 logger.trace("Joined UPnP Multicast group on Interface: {}", ni.getName());
153 byte[] requestMessage = COLOR_TOUCH_DISCOVERY_MESSAGE.getBytes("UTF-8");
154 DatagramPacket datagramPacket = new DatagramPacket(requestMessage, requestMessage.length, m, port);
155 socket.send(datagramPacket);
157 } catch (IOException e) {
158 logger.trace("got ioexception: {}", e.getMessage());
165 * Scans all messages that arrive on the socket and scans them for the
166 * search keywords. The search is not case sensitive.
169 * The socket where the answers arrive.
171 * The keywords to be searched for.
173 * @throws IOException
175 private void scanResposesForKeywords(MulticastSocket socket, String... keywords) throws IOException {
176 // In the worst case a SocketTimeoutException raises
178 byte[] rxbuf = new byte[8192];
179 DatagramPacket packet = new DatagramPacket(rxbuf, rxbuf.length);
181 socket.receive(packet);
182 } catch (IOException e) {
183 logger.trace("Got exception while trying to receive UPnP packets: {}", e.getMessage());
186 String response = new String(packet.getData());
187 if (response.contains(SSDP_MATCH)) {
188 logger.trace("Match: {} ", response);
189 parseResponse(response);
194 protected void parseResponse(String response) {
195 DiscoveryResult result;
201 Scanner scanner = new Scanner(response);
202 while (scanner.hasNextLine()) {
203 String line = scanner.nextLine();
204 String[] pair = line.split(":", 2);
205 if (pair.length != 2) {
208 String key = pair[0].toLowerCase();
209 String value = pair[1].trim();
210 logger.trace("key: {} value: {}.", key, value);
216 Matcher m = USN_PATTERN.matcher(value);
228 logger.trace("Found thermostat, name: {} uuid: {} url: {}", name, uuid, url);
230 if (name == null || uuid == null || url == null) {
231 logger.trace("Bad Format from thermostat");
235 uuid = uuid.replace(":", "").toLowerCase();
237 ThingUID thingUid = new ThingUID(VenstarThermostatBindingConstants.THING_TYPE_COLOR_TOUCH, uuid);
239 logger.trace("Got discovered device.");
241 String label = String.format("Venstar Thermostat (%s)", name);
242 result = DiscoveryResultBuilder.create(thingUid).withLabel(label)
243 .withRepresentationProperty(VenstarThermostatBindingConstants.PROPERTY_UUID)
244 .withProperty(VenstarThermostatBindingConstants.PROPERTY_UUID, uuid)
245 .withProperty(VenstarThermostatBindingConstants.PROPERTY_URL, url).build();
246 logger.trace("New venstar thermostat discovered with ID=<{}>", uuid);
247 this.thingDiscovered(result);