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 = """
58 Host: 239.255.255.250:1900
63 private static final Pattern USN_PATTERN = Pattern
64 .compile("^(colortouch:)?ecp((?::[0-9a-fA-F]{2}){6}):name:(.+)(?::type:(\\w+))");
65 private static final String SSDP_MATCH = "colortouch:ecp";
66 private static final int BACKGROUND_SCAN_INTERVAL_SECONDS = 300;
68 private @Nullable ScheduledFuture<?> scheduledFuture = null;
70 public VenstarThermostatDiscoveryService() {
71 super(VenstarThermostatBindingConstants.SUPPORTED_THING_TYPES, 30, true);
75 protected void startBackgroundDiscovery() {
76 logger.debug("Starting Background Scan");
77 stopBackgroundDiscovery();
78 scheduledFuture = scheduler.scheduleWithFixedDelay(this::doRunRun, 0, BACKGROUND_SCAN_INTERVAL_SECONDS,
83 protected void stopBackgroundDiscovery() {
84 ScheduledFuture<?> scheduledFutureLocal = scheduledFuture;
85 if (scheduledFutureLocal != null && !scheduledFutureLocal.isCancelled()) {
86 scheduledFutureLocal.cancel(true);
91 protected void startScan() {
92 logger.debug("Starting Interactive Scan");
96 protected synchronized void doRunRun() {
97 logger.trace("Sending SSDP discover.");
98 for (int i = 0; i < 5; i++) {
100 Enumeration<NetworkInterface> nets = NetworkInterface.getNetworkInterfaces();
101 while (nets.hasMoreElements()) {
102 NetworkInterface ni = nets.nextElement();
103 MulticastSocket socket = sendDiscoveryBroacast(ni);
104 if (socket != null) {
105 scanResposesForKeywords(socket);
108 } catch (IOException e) {
109 logger.debug("Error discoverying devices", e);
115 * Broadcasts a SSDP discovery message into the network to find provided
118 * @return The Socket the answers will arrive at.
119 * @throws UnknownHostException
120 * @throws IOException
121 * @throws SocketException
123 private @Nullable MulticastSocket sendDiscoveryBroacast(NetworkInterface ni)
124 throws UnknownHostException, SocketException {
125 InetAddress m = InetAddress.getByName("239.255.255.250");
126 final int port = 1900;
128 logger.trace("Considering {}", ni.getName());
130 if (!ni.isUp() || !ni.supportsMulticast()) {
131 logger.trace("skipping interface {}", ni.getName());
135 Enumeration<InetAddress> addrs = ni.getInetAddresses();
136 InetAddress a = null;
137 while (addrs.hasMoreElements()) {
138 a = addrs.nextElement();
139 if (a instanceof Inet4Address) {
146 logger.trace("no ipv4 address on {}", ni.getName());
150 // for whatever reason, the venstar thermostat responses will not be seen
151 // if we bind this socket to a particular address.
152 // this seems to be okay on linux systems, but osx apparently prefers ipv6, so this
153 // prevents responses from being received unless the ipv4 stack is given preference.
154 MulticastSocket socket = new MulticastSocket(new InetSocketAddress(port));
155 socket.setSoTimeout(2000);
156 socket.setReuseAddress(true);
157 socket.setNetworkInterface(ni);
160 logger.trace("Joined UPnP Multicast group on Interface: {}", ni.getName());
161 byte[] requestMessage = COLOR_TOUCH_DISCOVERY_MESSAGE.getBytes(StandardCharsets.UTF_8);
162 DatagramPacket datagramPacket = new DatagramPacket(requestMessage, requestMessage.length, m, port);
163 socket.send(datagramPacket);
165 } catch (IOException e) {
166 logger.trace("got ioexception: {}", e.getMessage());
173 * Scans all messages that arrive on the socket and scans them for the
174 * search keywords. The search is not case sensitive.
177 * The socket where the answers arrive.
179 * The keywords to be searched for.
181 * @throws IOException
183 private void scanResposesForKeywords(MulticastSocket socket, String... keywords) throws IOException {
184 // In the worst case a SocketTimeoutException raises
186 byte[] rxbuf = new byte[8192];
187 DatagramPacket packet = new DatagramPacket(rxbuf, rxbuf.length);
189 socket.receive(packet);
190 } catch (IOException e) {
191 logger.trace("Got exception while trying to receive UPnP packets: {}", e.getMessage());
194 String response = new String(packet.getData());
195 if (response.contains(SSDP_MATCH)) {
196 logger.trace("Match: {} ", response);
197 parseResponse(response);
202 protected void parseResponse(String response) {
203 DiscoveryResult result;
209 Scanner scanner = new Scanner(response);
210 while (scanner.hasNextLine()) {
211 String line = scanner.nextLine();
212 String[] pair = line.split(":", 2);
213 if (pair.length != 2) {
216 String key = pair[0].toLowerCase();
217 String value = pair[1].trim();
218 logger.trace("key: {} value: {}.", key, value);
224 Matcher m = USN_PATTERN.matcher(value);
236 logger.trace("Found thermostat, name: {} uuid: {} url: {}", name, uuid, url);
238 if (name == null || uuid == null || url == null) {
239 logger.trace("Bad Format from thermostat");
243 uuid = uuid.replace(":", "").toLowerCase();
245 ThingUID thingUid = new ThingUID(VenstarThermostatBindingConstants.THING_TYPE_COLOR_TOUCH, uuid);
247 logger.trace("Got discovered device.");
249 String label = String.format("Venstar Thermostat (%s)", name);
250 result = DiscoveryResultBuilder.create(thingUid).withLabel(label)
251 .withRepresentationProperty(VenstarThermostatBindingConstants.PROPERTY_UUID)
252 .withProperty(VenstarThermostatBindingConstants.PROPERTY_UUID, uuid)
253 .withProperty(VenstarThermostatBindingConstants.PROPERTY_URL, url).build();
254 logger.trace("New venstar thermostat discovered with ID=<{}>", uuid);
255 this.thingDiscovered(result);