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