]> git.basschouten.com Git - openhab-addons.git/blob
e8e40200662f1693a275c17ca80bf61e1b0831a3
[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 response
195      */
196
197     protected void parseResponse(String response) {
198         DiscoveryResult result;
199
200         String name = "unknownName";
201         String uuid = "unknownThermostat";
202         String ip = null;
203         String url = 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.debug("key: {} value: {}.", key, value);
215             if ("location".equals(key)) {
216                 try {
217                     url = value;
218                     ip = new URL(value).getHost();
219                 } catch (MalformedURLException e) {
220                     logger.debug("Malfored URL {}", e.getMessage());
221                 }
222             }
223         }
224         scanner.close();
225
226         logger.debug("Found thermostat, ip: {} ", ip);
227
228         if (ip == null) {
229             logger.debug("Bad Format from thermostat");
230             return;
231         }
232
233         JsonObject content;
234         String sysinfo;
235         boolean isCT80 = false;
236
237         try {
238             // Run the HTTP request and get the JSON response from the thermostat
239             sysinfo = HttpUtil.executeUrl("GET", url, 20000);
240             content = JsonParser.parseString(sysinfo).getAsJsonObject();
241             uuid = content.get("uuid").getAsString();
242         } catch (IOException | JsonSyntaxException e) {
243             logger.debug("Cannot get system info from thermostat {} {}", ip, e.getMessage());
244             sysinfo = null;
245         }
246
247         try {
248             String nameinfo = HttpUtil.executeUrl("GET", url + "name", 20000);
249             content = JsonParser.parseString(nameinfo).getAsJsonObject();
250             name = content.get("name").getAsString();
251         } catch (IOException | JsonSyntaxException e) {
252             logger.debug("Cannot get name from thermostat {} {}", ip, e.getMessage());
253         }
254
255         try {
256             String model = HttpUtil.executeUrl("GET", "http://" + ip + "/tstat/model", 20000);
257             isCT80 = (model != null && model.contains("CT80")) ? true : false;
258         } catch (IOException | JsonSyntaxException e) {
259             logger.debug("Cannot get model information from thermostat {} {}", ip, e.getMessage());
260         }
261
262         logger.debug("Discovery returned: {} uuid {} name {}", sysinfo, uuid, name);
263
264         ThingUID thingUid = new ThingUID(RadioThermostatBindingConstants.THING_TYPE_RTHERM, uuid);
265
266         logger.debug("Got discovered device.");
267
268         String label = String.format("RadioThermostat (%s)", name);
269         result = DiscoveryResultBuilder.create(thingUid).withLabel(label)
270                 .withRepresentationProperty(RadioThermostatBindingConstants.PROPERTY_IP)
271                 .withProperty(RadioThermostatBindingConstants.PROPERTY_IP, ip)
272                 .withProperty(RadioThermostatBindingConstants.PROPERTY_ISCT80, isCT80).build();
273         logger.debug("New RadioThermostat discovered with ID=<{}>", uuid);
274         this.thingDiscovered(result);
275     }
276 }