]> git.basschouten.com Git - openhab-addons.git/blob
7a9c3631b7f3c234ef45a6851af9b3e37659dbd4
[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 = "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      */
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;
122
123         logger.trace("Considering {}", ni.getName());
124         try {
125             if (!ni.isUp() || !ni.supportsMulticast()) {
126                 logger.trace("skipping interface {}", ni.getName());
127                 return null;
128             }
129
130             Enumeration<InetAddress> addrs = ni.getInetAddresses();
131             InetAddress a = null;
132             while (addrs.hasMoreElements()) {
133                 a = addrs.nextElement();
134                 if (a instanceof Inet4Address) {
135                     break;
136                 } else {
137                     a = null;
138                 }
139             }
140             if (a == null) {
141                 logger.trace("no ipv4 address on {}", ni.getName());
142                 return null;
143             }
144
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);
153             socket.joinGroup(m);
154
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);
159             return socket;
160         } catch (IOException e) {
161             logger.trace("got ioexception: {}", e.getMessage());
162         }
163
164         return null;
165     }
166
167     /**
168      * Scans all messages that arrive on the socket and scans them for the
169      * search keywords. The search is not case sensitive.
170      *
171      * @param socket
172      *            The socket where the answers arrive.
173      * @param keywords
174      *            The keywords to be searched for.
175      * @return
176      * @throws IOException
177      */
178     private void scanResposesForKeywords(MulticastSocket socket, String... keywords) throws IOException {
179         // In the worst case a SocketTimeoutException raises
180         do {
181             byte[] rxbuf = new byte[8192];
182             DatagramPacket packet = new DatagramPacket(rxbuf, rxbuf.length);
183             try {
184                 socket.receive(packet);
185             } catch (IOException e) {
186                 logger.trace("Got exception while trying to receive UPnP packets: {}", e.getMessage());
187                 return;
188             }
189             String response = new String(packet.getData());
190             if (response.contains(SSDP_MATCH)) {
191                 logger.trace("Match: {} ", response);
192                 parseResponse(response);
193             }
194         } while (true);
195     }
196
197     protected void parseResponse(String response) {
198         DiscoveryResult result;
199
200         String name = null;
201         String url = null;
202         String uuid = null;
203
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) {
209                 continue;
210             }
211             String key = pair[0].toLowerCase();
212             String value = pair[1].trim();
213             logger.trace("key: {} value: {}.", key, value);
214             switch (key) {
215                 case "location":
216                     url = value;
217                     break;
218                 case "usn":
219                     Matcher m = USN_PATTERN.matcher(value);
220                     if (m.find()) {
221                         uuid = m.group(2);
222                         name = m.group(3);
223                     }
224                     break;
225                 default:
226                     break;
227             }
228         }
229         scanner.close();
230
231         logger.trace("Found thermostat, name: {} uuid: {} url: {}", name, uuid, url);
232
233         if (name == null || uuid == null || url == null) {
234             logger.trace("Bad Format from thermostat");
235             return;
236         }
237
238         uuid = uuid.replace(":", "").toLowerCase();
239
240         ThingUID thingUid = new ThingUID(VenstarThermostatBindingConstants.THING_TYPE_COLOR_TOUCH, uuid);
241
242         logger.trace("Got discovered device.");
243
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);
251     }
252 }