]> git.basschouten.com Git - openhab-addons.git/blob
60c66153b5cee91c446fe4ab625672f992a441eb
[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.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 = "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;
62
63     private @Nullable ScheduledFuture<?> scheduledFuture = null;
64
65     public VenstarThermostatDiscoveryService() {
66         super(VenstarThermostatBindingConstants.SUPPORTED_THING_TYPES, 30, true);
67     }
68
69     @Override
70     protected void startBackgroundDiscovery() {
71         logger.debug("Starting Background Scan");
72         stopBackgroundDiscovery();
73         scheduledFuture = scheduler.scheduleWithFixedDelay(this::doRunRun, 0, BACKGROUND_SCAN_INTERVAL_SECONDS,
74                 TimeUnit.SECONDS);
75     }
76
77     @Override
78     protected void stopBackgroundDiscovery() {
79         ScheduledFuture<?> scheduledFutureLocal = scheduledFuture;
80         if (scheduledFutureLocal != null && !scheduledFutureLocal.isCancelled()) {
81             scheduledFutureLocal.cancel(true);
82         }
83     }
84
85     @Override
86     protected void startScan() {
87         logger.debug("Starting Interactive Scan");
88         doRunRun();
89     }
90
91     protected synchronized void doRunRun() {
92         logger.trace("Sending SSDP discover.");
93         for (int i = 0; i < 5; i++) {
94             try {
95                 Enumeration<NetworkInterface> nets = NetworkInterface.getNetworkInterfaces();
96                 while (nets.hasMoreElements()) {
97                     NetworkInterface ni = nets.nextElement();
98                     MulticastSocket socket = sendDiscoveryBroacast(ni);
99                     if (socket != null) {
100                         scanResposesForKeywords(socket);
101                     }
102                 }
103             } catch (IOException e) {
104                 logger.debug("Error discoverying devices", e);
105             }
106         }
107     }
108
109     /**
110      * Broadcasts a SSDP discovery message into the network to find provided
111      * services.
112      *
113      * @return The Socket the answers will arrive at.
114      * @throws UnknownHostException
115      * @throws IOException
116      * @throws SocketException
117      * @throws UnsupportedEncodingException
118      */
119     private @Nullable MulticastSocket sendDiscoveryBroacast(NetworkInterface ni)
120             throws UnknownHostException, SocketException, UnsupportedEncodingException {
121         InetAddress m = InetAddress.getByName("239.255.255.250");
122         final int port = 1900;
123
124         logger.trace("Considering {}", ni.getName());
125         try {
126             if (!ni.isUp() || !ni.supportsMulticast()) {
127                 logger.trace("skipping interface {}", ni.getName());
128                 return null;
129             }
130
131             Enumeration<InetAddress> addrs = ni.getInetAddresses();
132             InetAddress a = null;
133             while (addrs.hasMoreElements()) {
134                 a = addrs.nextElement();
135                 if (a instanceof Inet4Address) {
136                     break;
137                 } else {
138                     a = null;
139                 }
140             }
141             if (a == null) {
142                 logger.trace("no ipv4 address on {}", ni.getName());
143                 return null;
144             }
145
146             // for whatever reason, the venstar thermostat responses will not be seen
147             // if we bind this socket to a particular address.
148             // this seems to be okay on linux systems, but osx apparently prefers ipv6, so this
149             // prevents responses from being received unless the ipv4 stack is given preference.
150             MulticastSocket socket = new MulticastSocket(new InetSocketAddress(port));
151             socket.setSoTimeout(2000);
152             socket.setReuseAddress(true);
153             socket.setNetworkInterface(ni);
154             socket.joinGroup(m);
155
156             logger.trace("Joined UPnP Multicast group on Interface: {}", ni.getName());
157             byte[] requestMessage = COLOR_TOUCH_DISCOVERY_MESSAGE.getBytes("UTF-8");
158             DatagramPacket datagramPacket = new DatagramPacket(requestMessage, requestMessage.length, m, port);
159             socket.send(datagramPacket);
160             return socket;
161         } catch (IOException e) {
162             logger.trace("got ioexception: {}", e.getMessage());
163         }
164
165         return null;
166     }
167
168     /**
169      * Scans all messages that arrive on the socket and scans them for the
170      * search keywords. The search is not case sensitive.
171      *
172      * @param socket
173      *            The socket where the answers arrive.
174      * @param keywords
175      *            The keywords to be searched for.
176      * @return
177      * @throws IOException
178      */
179     private void scanResposesForKeywords(MulticastSocket socket, String... keywords) throws IOException {
180         // In the worst case a SocketTimeoutException raises
181         do {
182             byte[] rxbuf = new byte[8192];
183             DatagramPacket packet = new DatagramPacket(rxbuf, rxbuf.length);
184             try {
185                 socket.receive(packet);
186             } catch (IOException e) {
187                 logger.trace("Got exception while trying to receive UPnP packets: {}", e.getMessage());
188                 return;
189             }
190             String response = new String(packet.getData());
191             if (response.contains(SSDP_MATCH)) {
192                 logger.trace("Match: {} ", response);
193                 parseResponse(response);
194             }
195         } while (true);
196     }
197
198     protected void parseResponse(String response) {
199         DiscoveryResult result;
200
201         String name = null;
202         String url = null;
203         String uuid = null;
204
205         Scanner scanner = new Scanner(response);
206         while (scanner.hasNextLine()) {
207             String line = scanner.nextLine();
208             String[] pair = line.split(":", 2);
209             if (pair.length != 2) {
210                 continue;
211             }
212             String key = pair[0].toLowerCase();
213             String value = pair[1].trim();
214             logger.trace("key: {} value: {}.", key, value);
215             switch (key) {
216                 case "location":
217                     url = value;
218                     break;
219                 case "usn":
220                     Matcher m = USN_PATTERN.matcher(value);
221                     if (m.find()) {
222                         uuid = m.group(2);
223                         name = m.group(3);
224                     }
225                     break;
226                 default:
227                     break;
228             }
229         }
230         scanner.close();
231
232         logger.trace("Found thermostat, name: {} uuid: {} url: {}", name, uuid, url);
233
234         if (name == null || uuid == null || url == null) {
235             logger.trace("Bad Format from thermostat");
236             return;
237         }
238
239         uuid = uuid.replace(":", "").toLowerCase();
240
241         ThingUID thingUid = new ThingUID(VenstarThermostatBindingConstants.THING_TYPE_COLOR_TOUCH, uuid);
242
243         logger.trace("Got discovered device.");
244
245         String label = String.format("Venstar Thermostat (%s)", name);
246         result = DiscoveryResultBuilder.create(thingUid).withLabel(label)
247                 .withRepresentationProperty(VenstarThermostatBindingConstants.PROPERTY_UUID)
248                 .withProperty(VenstarThermostatBindingConstants.PROPERTY_UUID, uuid)
249                 .withProperty(VenstarThermostatBindingConstants.PROPERTY_URL, url).build();
250         logger.trace("New venstar thermostat discovered with ID=<{}>", uuid);
251         this.thingDiscovered(result);
252     }
253 }