]> git.basschouten.com Git - openhab-addons.git/blob
7aea198193d7bbb678e338f429ce2829d171cea9
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
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
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.venstarthermostat.internal.discovery;
14
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;
31
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;
43
44 /**
45  * The {@link VenstarThermostatDiscoveryService} is responsible for discovery of
46  * Venstar thermostats on the local network
47  *
48  * @author William Welliver - Initial contribution
49  * @author Dan Cunningham - Refactoring and Improvements
50  */
51
52 @NonNullByDefault
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 = """
57             M-SEARCH * HTTP/1.1
58             Host: 239.255.255.250:1900
59             Man: ssdp:discover
60             ST: colortouch:ecp
61
62             """;
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;
67
68     private @Nullable ScheduledFuture<?> scheduledFuture = null;
69
70     public VenstarThermostatDiscoveryService() {
71         super(VenstarThermostatBindingConstants.SUPPORTED_THING_TYPES, 30, true);
72     }
73
74     @Override
75     protected void startBackgroundDiscovery() {
76         logger.debug("Starting Background Scan");
77         stopBackgroundDiscovery();
78         scheduledFuture = scheduler.scheduleWithFixedDelay(this::doRunRun, 0, BACKGROUND_SCAN_INTERVAL_SECONDS,
79                 TimeUnit.SECONDS);
80     }
81
82     @Override
83     protected void stopBackgroundDiscovery() {
84         ScheduledFuture<?> scheduledFutureLocal = scheduledFuture;
85         if (scheduledFutureLocal != null && !scheduledFutureLocal.isCancelled()) {
86             scheduledFutureLocal.cancel(true);
87         }
88     }
89
90     @Override
91     protected void startScan() {
92         logger.debug("Starting Interactive Scan");
93         doRunRun();
94     }
95
96     protected synchronized void doRunRun() {
97         logger.trace("Sending SSDP discover.");
98         for (int i = 0; i < 5; i++) {
99             try {
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);
106                     }
107                 }
108             } catch (IOException e) {
109                 logger.debug("Error discoverying devices", e);
110             }
111         }
112     }
113
114     /**
115      * Broadcasts a SSDP discovery message into the network to find provided
116      * services.
117      *
118      * @return The Socket the answers will arrive at.
119      * @throws UnknownHostException
120      * @throws IOException
121      * @throws SocketException
122      */
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;
127
128         logger.trace("Considering {}", ni.getName());
129         try {
130             if (!ni.isUp() || !ni.supportsMulticast()) {
131                 logger.trace("skipping interface {}", ni.getName());
132                 return null;
133             }
134
135             Enumeration<InetAddress> addrs = ni.getInetAddresses();
136             InetAddress a = null;
137             while (addrs.hasMoreElements()) {
138                 a = addrs.nextElement();
139                 if (a instanceof Inet4Address) {
140                     break;
141                 } else {
142                     a = null;
143                 }
144             }
145             if (a == null) {
146                 logger.trace("no ipv4 address on {}", ni.getName());
147                 return null;
148             }
149
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);
158             socket.joinGroup(m);
159
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);
164             return socket;
165         } catch (IOException e) {
166             logger.trace("got ioexception: {}", e.getMessage());
167         }
168
169         return null;
170     }
171
172     /**
173      * Scans all messages that arrive on the socket and scans them for the
174      * search keywords. The search is not case sensitive.
175      *
176      * @param socket
177      *            The socket where the answers arrive.
178      * @param keywords
179      *            The keywords to be searched for.
180      * @return
181      * @throws IOException
182      */
183     private void scanResposesForKeywords(MulticastSocket socket, String... keywords) throws IOException {
184         // In the worst case a SocketTimeoutException raises
185         do {
186             byte[] rxbuf = new byte[8192];
187             DatagramPacket packet = new DatagramPacket(rxbuf, rxbuf.length);
188             try {
189                 socket.receive(packet);
190             } catch (IOException e) {
191                 logger.trace("Got exception while trying to receive UPnP packets: {}", e.getMessage());
192                 return;
193             }
194             String response = new String(packet.getData());
195             if (response.contains(SSDP_MATCH)) {
196                 logger.trace("Match: {} ", response);
197                 parseResponse(response);
198             }
199         } while (true);
200     }
201
202     protected void parseResponse(String response) {
203         DiscoveryResult result;
204
205         String name = null;
206         String url = null;
207         String uuid = null;
208
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) {
214                 continue;
215             }
216             String key = pair[0].toLowerCase();
217             String value = pair[1].trim();
218             logger.trace("key: {} value: {}.", key, value);
219             switch (key) {
220                 case "location":
221                     url = value;
222                     break;
223                 case "usn":
224                     Matcher m = USN_PATTERN.matcher(value);
225                     if (m.find()) {
226                         uuid = m.group(2);
227                         name = m.group(3);
228                     }
229                     break;
230                 default:
231                     break;
232             }
233         }
234         scanner.close();
235
236         logger.trace("Found thermostat, name: {} uuid: {} url: {}", name, uuid, url);
237
238         if (name == null || uuid == null || url == null) {
239             logger.trace("Bad Format from thermostat");
240             return;
241         }
242
243         uuid = uuid.replace(":", "").toLowerCase();
244
245         ThingUID thingUid = new ThingUID(VenstarThermostatBindingConstants.THING_TYPE_COLOR_TOUCH, uuid);
246
247         logger.trace("Got discovered device.");
248
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);
256     }
257 }