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.venstarthermostat.internal.discovery;
15 import java.io.IOException;
16 import java.net.DatagramPacket;
17 import java.net.Inet4Address;
18 import java.net.InetAddress;
19 import java.net.InetSocketAddress;
20 import java.net.MulticastSocket;
21 import java.net.NetworkInterface;
22 import java.net.SocketException;
23 import java.net.UnknownHostException;
24 import java.nio.charset.StandardCharsets;
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
118 private @Nullable MulticastSocket sendDiscoveryBroacast(NetworkInterface ni)
119 throws UnknownHostException, SocketException {
120 InetAddress m = InetAddress.getByName("239.255.255.250");
121 final int port = 1900;
123 logger.trace("Considering {}", ni.getName());
125 if (!ni.isUp() || !ni.supportsMulticast()) {
126 logger.trace("skipping interface {}", ni.getName());
130 Enumeration<InetAddress> addrs = ni.getInetAddresses();
131 InetAddress a = null;
132 while (addrs.hasMoreElements()) {
133 a = addrs.nextElement();
134 if (a instanceof Inet4Address) {
141 logger.trace("no ipv4 address on {}", ni.getName());
145 // for whatever reason, the venstar thermostat responses will not be seen
146 // if we bind this socket to a particular address.
147 // this seems to be okay on linux systems, but osx apparently prefers ipv6, so this
148 // prevents responses from being received unless the ipv4 stack is given preference.
149 MulticastSocket socket = new MulticastSocket(new InetSocketAddress(port));
150 socket.setSoTimeout(2000);
151 socket.setReuseAddress(true);
152 socket.setNetworkInterface(ni);
155 logger.trace("Joined UPnP Multicast group on Interface: {}", ni.getName());
156 byte[] requestMessage = COLOR_TOUCH_DISCOVERY_MESSAGE.getBytes(StandardCharsets.UTF_8);
157 DatagramPacket datagramPacket = new DatagramPacket(requestMessage, requestMessage.length, m, port);
158 socket.send(datagramPacket);
160 } catch (IOException e) {
161 logger.trace("got ioexception: {}", e.getMessage());
168 * Scans all messages that arrive on the socket and scans them for the
169 * search keywords. The search is not case sensitive.
172 * The socket where the answers arrive.
174 * The keywords to be searched for.
176 * @throws IOException
178 private void scanResposesForKeywords(MulticastSocket socket, String... keywords) throws IOException {
179 // In the worst case a SocketTimeoutException raises
181 byte[] rxbuf = new byte[8192];
182 DatagramPacket packet = new DatagramPacket(rxbuf, rxbuf.length);
184 socket.receive(packet);
185 } catch (IOException e) {
186 logger.trace("Got exception while trying to receive UPnP packets: {}", e.getMessage());
189 String response = new String(packet.getData());
190 if (response.contains(SSDP_MATCH)) {
191 logger.trace("Match: {} ", response);
192 parseResponse(response);
197 protected void parseResponse(String response) {
198 DiscoveryResult result;
204 Scanner scanner = new Scanner(response);
205 while (scanner.hasNextLine()) {
206 String line = scanner.nextLine();
207 String[] pair = line.split(":", 2);
208 if (pair.length != 2) {
211 String key = pair[0].toLowerCase();
212 String value = pair[1].trim();
213 logger.trace("key: {} value: {}.", key, value);
219 Matcher m = USN_PATTERN.matcher(value);
231 logger.trace("Found thermostat, name: {} uuid: {} url: {}", name, uuid, url);
233 if (name == null || uuid == null || url == null) {
234 logger.trace("Bad Format from thermostat");
238 uuid = uuid.replace(":", "").toLowerCase();
240 ThingUID thingUid = new ThingUID(VenstarThermostatBindingConstants.THING_TYPE_COLOR_TOUCH, uuid);
242 logger.trace("Got discovered device.");
244 String label = String.format("Venstar Thermostat (%s)", name);
245 result = DiscoveryResultBuilder.create(thingUid).withLabel(label)
246 .withRepresentationProperty(VenstarThermostatBindingConstants.PROPERTY_UUID)
247 .withProperty(VenstarThermostatBindingConstants.PROPERTY_UUID, uuid)
248 .withProperty(VenstarThermostatBindingConstants.PROPERTY_URL, url).build();
249 logger.trace("New venstar thermostat discovered with ID=<{}>", uuid);
250 this.thingDiscovered(result);