]> git.basschouten.com Git - openhab-addons.git/blob
c2756fbb8749dc3789e495f4ac2c3f517e3d9ff9
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2021 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.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;
31
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;
41
42 /**
43  * The {@link VenstarThermostatDiscoveryService} is responsible for discovery of
44  * Venstar thermostats on the local network
45  *
46  * @author William Welliver - Initial contribution
47  * @author Dan Cunningham - Refactoring and Improvements
48  */
49
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;
59
60     private ScheduledFuture<?> scheduledFuture = null;
61
62     public VenstarThermostatDiscoveryService() {
63         super(VenstarThermostatBindingConstants.SUPPORTED_THING_TYPES, 30, true);
64     }
65
66     @Override
67     protected void startBackgroundDiscovery() {
68         logger.debug("Starting Background Scan");
69         stopBackgroundDiscovery();
70         scheduledFuture = scheduler.scheduleAtFixedRate(this::doRunRun, 0, BACKGROUND_SCAN_INTERVAL_SECONDS,
71                 TimeUnit.SECONDS);
72     }
73
74     @Override
75     protected void stopBackgroundDiscovery() {
76         if (scheduledFuture != null && !scheduledFuture.isCancelled()) {
77             scheduledFuture.cancel(true);
78         }
79     }
80
81     @Override
82     protected void startScan() {
83         logger.debug("Starting Interactive Scan");
84         doRunRun();
85     }
86
87     protected synchronized void doRunRun() {
88         logger.trace("Sending SSDP discover.");
89         for (int i = 0; i < 5; i++) {
90             try {
91                 Enumeration<NetworkInterface> nets = NetworkInterface.getNetworkInterfaces();
92                 while (nets.hasMoreElements()) {
93                     NetworkInterface ni = nets.nextElement();
94                     MulticastSocket socket = sendDiscoveryBroacast(ni);
95                     if (socket != null) {
96                         scanResposesForKeywords(socket);
97                     }
98                 }
99             } catch (IOException e) {
100                 logger.debug("Error discoverying devices", e);
101             }
102         }
103     }
104
105     /**
106      * Broadcasts a SSDP discovery message into the network to find provided
107      * services.
108      *
109      * @return The Socket the answers will arrive at.
110      * @throws UnknownHostException
111      * @throws IOException
112      * @throws SocketException
113      * @throws UnsupportedEncodingException
114      */
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;
119
120         logger.trace("Considering {}", ni.getName());
121         try {
122             if (!ni.isUp() || !ni.supportsMulticast()) {
123                 logger.trace("skipping interface {}", ni.getName());
124                 return null;
125             }
126
127             Enumeration<InetAddress> addrs = ni.getInetAddresses();
128             InetAddress a = null;
129             while (addrs.hasMoreElements()) {
130                 a = addrs.nextElement();
131                 if (a instanceof Inet4Address) {
132                     break;
133                 } else {
134                     a = null;
135                 }
136             }
137             if (a == null) {
138                 logger.trace("no ipv4 address on {}", ni.getName());
139                 return null;
140             }
141
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);
150             socket.joinGroup(m);
151
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);
156             return socket;
157         } catch (IOException e) {
158             logger.trace("got ioexception: {}", e.getMessage());
159         }
160
161         return null;
162     }
163
164     /**
165      * Scans all messages that arrive on the socket and scans them for the
166      * search keywords. The search is not case sensitive.
167      *
168      * @param socket
169      *            The socket where the answers arrive.
170      * @param keywords
171      *            The keywords to be searched for.
172      * @return
173      * @throws IOException
174      */
175     private void scanResposesForKeywords(MulticastSocket socket, String... keywords) throws IOException {
176         // In the worst case a SocketTimeoutException raises
177         do {
178             byte[] rxbuf = new byte[8192];
179             DatagramPacket packet = new DatagramPacket(rxbuf, rxbuf.length);
180             try {
181                 socket.receive(packet);
182             } catch (IOException e) {
183                 logger.trace("Got exception while trying to receive UPnP packets: {}", e.getMessage());
184                 return;
185             }
186             String response = new String(packet.getData());
187             if (response.contains(SSDP_MATCH)) {
188                 logger.trace("Match: {} ", response);
189                 parseResponse(response);
190             }
191         } while (true);
192     }
193
194     protected void parseResponse(String response) {
195         DiscoveryResult result;
196
197         String name = null;
198         String url = null;
199         String uuid = null;
200
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) {
206                 continue;
207             }
208             String key = pair[0].toLowerCase();
209             String value = pair[1].trim();
210             logger.trace("key: {} value: {}.", key, value);
211             switch (key) {
212                 case "location":
213                     url = value;
214                     break;
215                 case "usn":
216                     Matcher m = USN_PATTERN.matcher(value);
217                     if (m.find()) {
218                         uuid = m.group(2);
219                         name = m.group(3);
220                     }
221                     break;
222                 default:
223                     break;
224             }
225         }
226         scanner.close();
227
228         logger.trace("Found thermostat, name: {} uuid: {} url: {}", name, uuid, url);
229
230         if (name == null || uuid == null || url == null) {
231             logger.trace("Bad Format from thermostat");
232             return;
233         }
234
235         uuid = uuid.replace(":", "").toLowerCase();
236
237         ThingUID thingUid = new ThingUID(VenstarThermostatBindingConstants.THING_TYPE_COLOR_TOUCH, uuid);
238
239         logger.trace("Got discovered device.");
240
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);
248     }
249 }