2 * Copyright (c) 2010-2021 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
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
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.radiothermostat.internal.discovery;
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;
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;
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;
46 import com.google.gson.JsonObject;
47 import com.google.gson.JsonParser;
48 import com.google.gson.JsonSyntaxException;
51 * The {@link RadioThermostatDiscoveryService} is responsible for discovery of
52 * RadioThermostats on the local network
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
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";
67 private static final String SSDP_MATCH = "WM-NOTIFY";
68 private static final int BACKGROUND_SCAN_INTERVAL_SECONDS = 300;
70 private @Nullable ScheduledFuture<?> scheduledFuture = null;
72 public RadioThermostatDiscoveryService() {
73 super(RadioThermostatBindingConstants.SUPPORTED_THING_TYPES_UIDS, 30, true);
77 protected void startBackgroundDiscovery() {
78 logger.debug("Starting Background Scan");
79 stopBackgroundDiscovery();
80 scheduledFuture = scheduler.scheduleWithFixedDelay(this::doRunRun, 0, BACKGROUND_SCAN_INTERVAL_SECONDS,
85 protected void stopBackgroundDiscovery() {
86 ScheduledFuture<?> scheduledFuture = this.scheduledFuture;
87 if (scheduledFuture != null) {
88 scheduledFuture.cancel(true);
89 this.scheduledFuture = null;
94 protected void startScan() {
95 logger.debug("Starting Interactive Scan");
99 protected synchronized void doRunRun() {
100 logger.debug("Sending SSDP discover.");
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);
109 } catch (IOException e) {
110 logger.debug("Error discovering devices", e);
115 * Broadcasts a SSDP discovery message into the network to find provided
118 * @return The Socket the answers will arrive at.
119 * @throws UnknownHostException
120 * @throws IOException
121 * @throws SocketException
122 * @throws UnsupportedEncodingException
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");
130 Enumeration<InetAddress> addrs = ni.getInetAddresses();
131 InetAddress a = null;
132 while (addrs.hasMoreElements()) {
133 a = addrs.nextElement();
134 if (a instanceof Inet4Address) {
141 logger.debug("no ipv4 address on {}", ni.getName());
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);
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);
160 // Try to ensure that joinGroup has taken effect. Without this delay, the query
161 // packet ends up going out before the group join.
164 socket.send(datagramPacket);
166 byte[] buf = new byte[4096];
167 DatagramPacket packet = new DatagramPacket(buf, buf.length);
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);
179 logger.debug("Bridge device scan interrupted");
180 } catch (SocketTimeoutException e) {
182 "Timed out waiting for multicast response. Presumably all devices have already responded.");
185 socket.leaveGroup(m);
188 } catch (IOException | InterruptedException e) {
189 logger.debug("got exception: {}", e.getMessage());
195 * Scans all messages that arrive on the socket and scans them for the
196 * search keywords. The search is not case sensitive.
199 * The socket where the answers arrive.
201 * The keywords to be searched for.
203 * @throws IOException
206 protected void parseResponse(String response) {
207 DiscoveryResult result;
209 String name = "unknownName";
210 String uuid = "unknownThermostat";
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) {
221 String key = pair[0].toLowerCase();
222 String value = pair[1].trim();
223 logger.debug("key: {} value: {}.", key, value);
224 if ("location".equals(key)) {
227 ip = new URL(value).getHost();
228 } catch (MalformedURLException e) {
229 logger.debug("Malfored URL {}", e.getMessage());
235 logger.debug("Found thermostat, ip: {} ", ip);
238 logger.debug("Bad Format from thermostat");
244 boolean isCT80 = false;
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());
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());
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());
271 logger.debug("Discovery returned: {} uuid {} name {}", sysinfo, uuid, name);
273 ThingUID thingUid = new ThingUID(RadioThermostatBindingConstants.THING_TYPE_RTHERM, uuid);
275 logger.debug("Got discovered device.");
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);