]> git.basschouten.com Git - openhab-addons.git/blob
250f9251cea1cc7d6fbd057d47295b0d367f430e
[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     @Override
85     protected void stopBackgroundDiscovery() {
86         ScheduledFuture<?> scheduledFuture = this.scheduledFuture;
87         if (scheduledFuture != null) {
88             scheduledFuture.cancel(true);
89             this.scheduledFuture = null;
90         }
91     }
92
93     @Override
94     protected void startScan() {
95         logger.debug("Starting Interactive Scan");
96         doRunRun();
97     }
98
99     protected synchronized void doRunRun() {
100         logger.debug("Sending SSDP discover.");
101         try {
102             Enumeration<NetworkInterface> nets = NetworkInterface.getNetworkInterfaces();
103             while (nets.hasMoreElements()) {
104                 NetworkInterface ni = nets.nextElement();
105                 if (ni.isUp() && ni.supportsMulticast() && !ni.isLoopback()) {
106                     sendDiscoveryBroacast(ni);
107                 }
108             }
109         } catch (IOException e) {
110             logger.debug("Error discovering devices", e);
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      * @throws UnsupportedEncodingException
123      */
124     private void sendDiscoveryBroacast(NetworkInterface ni)
125             throws UnknownHostException, SocketException, UnsupportedEncodingException {
126         InetAddress m = InetAddress.getByName("239.255.255.250");
127         final int port = 1900;
128         logger.debug("Sending discovery broadcast");
129         try {
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.debug("no ipv4 address on {}", ni.getName());
142                 return;
143             }
144
145             // for whatever reason, the radio 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(null);
150             socket.setSoTimeout(5000);
151             socket.setReuseAddress(true);
152             // socket.setBroadcast(true);
153             socket.setNetworkInterface(ni);
154             socket.joinGroup(m);
155             logger.debug("Joined UPnP Multicast group on Interface: {}", ni.getName());
156             byte[] requestMessage = RADIOTHERMOSTAT_DISCOVERY_MESSAGE.getBytes("UTF-8");
157             DatagramPacket datagramPacket = new DatagramPacket(requestMessage, requestMessage.length, m, port);
158             socket.send(datagramPacket);
159             try {
160                 // Try to ensure that joinGroup has taken effect. Without this delay, the query
161                 // packet ends up going out before the group join.
162                 Thread.sleep(1000);
163
164                 socket.send(datagramPacket);
165
166                 byte[] buf = new byte[4096];
167                 DatagramPacket packet = new DatagramPacket(buf, buf.length);
168
169                 try {
170                     while (!Thread.interrupted()) {
171                         socket.receive(packet);
172                         String response = new String(packet.getData(), StandardCharsets.UTF_8);
173                         logger.debug("Response: {} ", response);
174                         if (response.contains(SSDP_MATCH)) {
175                             logger.debug("Match: {} ", response);
176                             parseResponse(response);
177                         }
178                     }
179                     logger.debug("Bridge device scan interrupted");
180                 } catch (SocketTimeoutException e) {
181                     logger.debug(
182                             "Timed out waiting for multicast response. Presumably all devices have already responded.");
183                 }
184             } finally {
185                 socket.leaveGroup(m);
186                 socket.close();
187             }
188         } catch (IOException | InterruptedException e) {
189             logger.debug("got exception: {}", e.getMessage());
190         }
191         return;
192     }
193
194     /**
195      * Scans all messages that arrive on the socket and scans them for the
196      * search keywords. The search is not case sensitive.
197      *
198      * @param socket
199      *            The socket where the answers arrive.
200      * @param keywords
201      *            The keywords to be searched for.
202      * @return
203      * @throws IOException
204      */
205
206     protected void parseResponse(String response) {
207         DiscoveryResult result;
208
209         String name = "unknownName";
210         String uuid = "unknownThermostat";
211         String ip = null;
212         String url = null;
213
214         Scanner scanner = new Scanner(response);
215         while (scanner.hasNextLine()) {
216             String line = scanner.nextLine();
217             String[] pair = line.split(":", 2);
218             if (pair.length != 2) {
219                 continue;
220             }
221             String key = pair[0].toLowerCase();
222             String value = pair[1].trim();
223             logger.debug("key: {} value: {}.", key, value);
224             if ("location".equals(key)) {
225                 try {
226                     url = value;
227                     ip = new URL(value).getHost();
228                 } catch (MalformedURLException e) {
229                     logger.debug("Malfored URL {}", e.getMessage());
230                 }
231             }
232         }
233         scanner.close();
234
235         logger.debug("Found thermostat, ip: {} ", ip);
236
237         if (ip == null) {
238             logger.debug("Bad Format from thermostat");
239             return;
240         }
241
242         JsonObject content;
243         String sysinfo;
244         boolean isCT80 = false;
245
246         try {
247             // Run the HTTP request and get the JSON response from the thermostat
248             sysinfo = HttpUtil.executeUrl("GET", url, 20000);
249             content = JsonParser.parseString(sysinfo).getAsJsonObject();
250             uuid = content.get("uuid").getAsString();
251         } catch (IOException | JsonSyntaxException e) {
252             logger.debug("Cannot get system info from thermostat {} {}", ip, e.getMessage());
253             sysinfo = null;
254         }
255
256         try {
257             String nameinfo = HttpUtil.executeUrl("GET", url + "name", 20000);
258             content = JsonParser.parseString(nameinfo).getAsJsonObject();
259             name = content.get("name").getAsString();
260         } catch (IOException | JsonSyntaxException e) {
261             logger.debug("Cannot get name from thermostat {} {}", ip, e.getMessage());
262         }
263
264         try {
265             String model = HttpUtil.executeUrl("GET", "http://" + ip + "/tstat/model", 20000);
266             isCT80 = (model != null && model.contains("CT80")) ? true : false;
267         } catch (IOException | JsonSyntaxException e) {
268             logger.debug("Cannot get model information from thermostat {} {}", ip, e.getMessage());
269         }
270
271         logger.debug("Discovery returned: {} uuid {} name {}", sysinfo, uuid, name);
272
273         ThingUID thingUid = new ThingUID(RadioThermostatBindingConstants.THING_TYPE_RTHERM, uuid);
274
275         logger.debug("Got discovered device.");
276
277         String label = String.format("RadioThermostat (%s)", name);
278         result = DiscoveryResultBuilder.create(thingUid).withLabel(label)
279                 .withRepresentationProperty(RadioThermostatBindingConstants.PROPERTY_IP)
280                 .withProperty(RadioThermostatBindingConstants.PROPERTY_IP, ip)
281                 .withProperty(RadioThermostatBindingConstants.PROPERTY_ISCT80, isCT80).build();
282         logger.debug("New RadioThermostat discovered with ID=<{}>", uuid);
283         this.thingDiscovered(result);
284     }
285 }