]> git.basschouten.com Git - openhab-addons.git/blob
4c276eb6f867eb972c238bb3a7db758891dc6870
[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.radiothermostat.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.MalformedURLException;
21 import java.net.MulticastSocket;
22 import java.net.NetworkInterface;
23 import java.net.SocketException;
24 import java.net.SocketTimeoutException;
25 import java.net.URL;
26 import java.net.UnknownHostException;
27 import java.nio.charset.StandardCharsets;
28 import java.util.Enumeration;
29 import java.util.Scanner;
30 import java.util.concurrent.ScheduledFuture;
31 import java.util.concurrent.TimeUnit;
32
33 import org.eclipse.jdt.annotation.NonNullByDefault;
34 import org.eclipse.jdt.annotation.Nullable;
35 import org.openhab.binding.radiothermostat.internal.RadioThermostatBindingConstants;
36 import org.openhab.core.config.discovery.AbstractDiscoveryService;
37 import org.openhab.core.config.discovery.DiscoveryResult;
38 import org.openhab.core.config.discovery.DiscoveryResultBuilder;
39 import org.openhab.core.config.discovery.DiscoveryService;
40 import org.openhab.core.io.net.http.HttpUtil;
41 import org.openhab.core.thing.ThingUID;
42 import org.osgi.service.component.annotations.Component;
43 import org.slf4j.Logger;
44 import org.slf4j.LoggerFactory;
45
46 import com.google.gson.JsonObject;
47 import com.google.gson.JsonParser;
48 import com.google.gson.JsonSyntaxException;
49
50 /**
51  * The {@link RadioThermostatDiscoveryService} is responsible for discovery of
52  * RadioThermostats on the local network
53  *
54  * @author William Welliver - Initial contribution
55  * @author Dan Cunningham - Refactoring and Improvements
56  * @author Bill Forsyth - Modified for the RadioThermostat's peculiar discovery mode
57  * @author Michael Lobstein - Cleanup for RadioThermostat
58  *
59  */
60
61 @NonNullByDefault
62 @Component(service = DiscoveryService.class, configurationPid = "discovery.radiothermostat")
63 public class RadioThermostatDiscoveryService extends AbstractDiscoveryService {
64     private final Logger logger = LoggerFactory.getLogger(RadioThermostatDiscoveryService.class);
65     private static final String RADIOTHERMOSTAT_DISCOVERY_MESSAGE = "TYPE: WM-DISCOVER\r\nVERSION: 1.0\r\n\r\nservices:com.marvell.wm.system*\r\n\r\n";
66
67     private static final String SSDP_MATCH = "WM-NOTIFY";
68     private static final int BACKGROUND_SCAN_INTERVAL_SECONDS = 300;
69
70     private @Nullable ScheduledFuture<?> scheduledFuture = null;
71
72     public RadioThermostatDiscoveryService() {
73         super(RadioThermostatBindingConstants.SUPPORTED_THING_TYPES_UIDS, 30, true);
74     }
75
76     @Override
77     protected void startBackgroundDiscovery() {
78         logger.debug("Starting Background Scan");
79         stopBackgroundDiscovery();
80         scheduledFuture = scheduler.scheduleWithFixedDelay(this::doRunRun, 0, BACKGROUND_SCAN_INTERVAL_SECONDS,
81                 TimeUnit.SECONDS);
82     }
83
84     @SuppressWarnings("null")
85     @Override
86     protected void stopBackgroundDiscovery() {
87         if (scheduledFuture != null && !scheduledFuture.isCancelled()) {
88             scheduledFuture.cancel(true);
89         }
90     }
91
92     @Override
93     protected void startScan() {
94         logger.debug("Starting Interactive Scan");
95         doRunRun();
96     }
97
98     protected synchronized void doRunRun() {
99         logger.debug("Sending SSDP discover.");
100         try {
101             Enumeration<NetworkInterface> nets = NetworkInterface.getNetworkInterfaces();
102             while (nets.hasMoreElements()) {
103                 NetworkInterface ni = nets.nextElement();
104                 if (ni.isUp() && ni.supportsMulticast() && !ni.isLoopback()) {
105                     sendDiscoveryBroacast(ni);
106                 }
107             }
108         } catch (IOException e) {
109             logger.debug("Error discovering devices", e);
110         }
111     }
112
113     /**
114      * Broadcasts a SSDP discovery message into the network to find provided
115      * services.
116      *
117      * @return The Socket the answers will arrive at.
118      * @throws UnknownHostException
119      * @throws IOException
120      * @throws SocketException
121      * @throws UnsupportedEncodingException
122      */
123     private void sendDiscoveryBroacast(NetworkInterface ni)
124             throws UnknownHostException, SocketException, UnsupportedEncodingException {
125         InetAddress m = InetAddress.getByName("239.255.255.250");
126         final int port = 1900;
127         logger.debug("Sending discovery broadcast");
128         try {
129             Enumeration<InetAddress> addrs = ni.getInetAddresses();
130             InetAddress a = null;
131             while (addrs.hasMoreElements()) {
132                 a = addrs.nextElement();
133                 if (a instanceof Inet4Address) {
134                     break;
135                 } else {
136                     a = null;
137                 }
138             }
139             if (a == null) {
140                 logger.debug("no ipv4 address on {}", ni.getName());
141                 return;
142             }
143
144             // for whatever reason, the radio thermostat responses will not be seen
145             // if we bind this socket to a particular address.
146             // this seems to be okay on linux systems, but osx apparently prefers ipv6, so this
147             // prevents responses from being received unless the ipv4 stack is given preference.
148             MulticastSocket socket = new MulticastSocket(null);
149             socket.setSoTimeout(5000);
150             socket.setReuseAddress(true);
151             // socket.setBroadcast(true);
152             socket.setNetworkInterface(ni);
153             socket.joinGroup(m);
154             logger.debug("Joined UPnP Multicast group on Interface: {}", ni.getName());
155             byte[] requestMessage = RADIOTHERMOSTAT_DISCOVERY_MESSAGE.getBytes("UTF-8");
156             DatagramPacket datagramPacket = new DatagramPacket(requestMessage, requestMessage.length, m, port);
157             socket.send(datagramPacket);
158             try {
159                 // Try to ensure that joinGroup has taken effect. Without this delay, the query
160                 // packet ends up going out before the group join.
161                 Thread.sleep(1000);
162
163                 socket.send(datagramPacket);
164
165                 byte[] buf = new byte[4096];
166                 DatagramPacket packet = new DatagramPacket(buf, buf.length);
167
168                 try {
169                     while (!Thread.interrupted()) {
170                         socket.receive(packet);
171                         String response = new String(packet.getData(), StandardCharsets.UTF_8);
172                         logger.debug("Response: {} ", response);
173                         if (response.contains(SSDP_MATCH)) {
174                             logger.debug("Match: {} ", response);
175                             parseResponse(response);
176                         }
177                     }
178                     logger.debug("Bridge device scan interrupted");
179                 } catch (SocketTimeoutException e) {
180                     logger.debug(
181                             "Timed out waiting for multicast response. Presumably all devices have already responded.");
182                 }
183             } finally {
184                 socket.leaveGroup(m);
185                 socket.close();
186             }
187         } catch (IOException | InterruptedException e) {
188             logger.debug("got exception: {}", e.getMessage());
189         }
190         return;
191     }
192
193     /**
194      * Scans all messages that arrive on the socket and scans them for the
195      * search keywords. The search is not case sensitive.
196      *
197      * @param socket
198      *            The socket where the answers arrive.
199      * @param keywords
200      *            The keywords to be searched for.
201      * @return
202      * @throws IOException
203      */
204
205     protected void parseResponse(String response) {
206         DiscoveryResult result;
207
208         String name = "unknownName";
209         String uuid = "unknownThermostat";
210         String ip = null;
211         String url = null;
212
213         Scanner scanner = new Scanner(response);
214         while (scanner.hasNextLine()) {
215             String line = scanner.nextLine();
216             String[] pair = line.split(":", 2);
217             if (pair.length != 2) {
218                 continue;
219             }
220             String key = pair[0].toLowerCase();
221             String value = pair[1].trim();
222             logger.debug("key: {} value: {}.", key, value);
223             if ("location".equals(key)) {
224                 try {
225                     url = value;
226                     ip = new URL(value).getHost();
227                 } catch (MalformedURLException e) {
228                     logger.debug("Malfored URL {}", e.getMessage());
229                 }
230             }
231         }
232         scanner.close();
233
234         logger.debug("Found thermostat, ip: {} ", ip);
235
236         if (ip == null) {
237             logger.debug("Bad Format from thermostat");
238             return;
239         }
240
241         JsonObject content;
242         String sysinfo;
243         boolean isCT80 = false;
244
245         try {
246             // Run the HTTP request and get the JSON response from the thermostat
247             sysinfo = HttpUtil.executeUrl("GET", url, 20000);
248             content = JsonParser.parseString(sysinfo).getAsJsonObject();
249             uuid = content.get("uuid").getAsString();
250         } catch (IOException | JsonSyntaxException e) {
251             logger.debug("Cannot get system info from thermostat {} {}", ip, e.getMessage());
252             sysinfo = null;
253         }
254
255         try {
256             String nameinfo = HttpUtil.executeUrl("GET", url + "name", 20000);
257             content = JsonParser.parseString(nameinfo).getAsJsonObject();
258             name = content.get("name").getAsString();
259         } catch (IOException | JsonSyntaxException e) {
260             logger.debug("Cannot get name from thermostat {} {}", ip, e.getMessage());
261         }
262
263         try {
264             String model = HttpUtil.executeUrl("GET", "http://" + ip + "/tstat/model", 20000);
265             isCT80 = (model != null && model.contains("CT80")) ? true : false;
266         } catch (IOException | JsonSyntaxException e) {
267             logger.debug("Cannot get model information from thermostat {} {}", ip, e.getMessage());
268         }
269
270         logger.debug("Discovery returned: {} uuid {} name {}", sysinfo, uuid, name);
271
272         ThingUID thingUid = new ThingUID(RadioThermostatBindingConstants.THING_TYPE_RTHERM, uuid);
273
274         logger.debug("Got discovered device.");
275
276         String label = String.format("RadioThermostat (%s)", name);
277         result = DiscoveryResultBuilder.create(thingUid).withLabel(label)
278                 .withRepresentationProperty(RadioThermostatBindingConstants.PROPERTY_IP)
279                 .withProperty(RadioThermostatBindingConstants.PROPERTY_IP, ip)
280                 .withProperty(RadioThermostatBindingConstants.PROPERTY_ISCT80, isCT80).build();
281         logger.debug("New RadioThermostat discovered with ID=<{}>", uuid);
282         this.thingDiscovered(result);
283     }
284 }