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,
84 @SuppressWarnings("null")
86 protected void stopBackgroundDiscovery() {
87 if (scheduledFuture != null && !scheduledFuture.isCancelled()) {
88 scheduledFuture.cancel(true);
93 protected void startScan() {
94 logger.debug("Starting Interactive Scan");
98 protected synchronized void doRunRun() {
99 logger.debug("Sending SSDP discover.");
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);
108 } catch (IOException e) {
109 logger.debug("Error discovering devices", e);
114 * Broadcasts a SSDP discovery message into the network to find provided
117 * @return The Socket the answers will arrive at.
118 * @throws UnknownHostException
119 * @throws IOException
120 * @throws SocketException
121 * @throws UnsupportedEncodingException
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");
129 Enumeration<InetAddress> addrs = ni.getInetAddresses();
130 InetAddress a = null;
131 while (addrs.hasMoreElements()) {
132 a = addrs.nextElement();
133 if (a instanceof Inet4Address) {
140 logger.debug("no ipv4 address on {}", ni.getName());
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);
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);
159 // Try to ensure that joinGroup has taken effect. Without this delay, the query
160 // packet ends up going out before the group join.
163 socket.send(datagramPacket);
165 byte[] buf = new byte[4096];
166 DatagramPacket packet = new DatagramPacket(buf, buf.length);
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);
178 logger.debug("Bridge device scan interrupted");
179 } catch (SocketTimeoutException e) {
181 "Timed out waiting for multicast response. Presumably all devices have already responded.");
184 socket.leaveGroup(m);
187 } catch (IOException | InterruptedException e) {
188 logger.debug("got exception: {}", e.getMessage());
194 * Scans all messages that arrive on the socket and scans them for the
195 * search keywords. The search is not case sensitive.
198 * The socket where the answers arrive.
200 * The keywords to be searched for.
202 * @throws IOException
205 protected void parseResponse(String response) {
206 DiscoveryResult result;
208 String name = "unknownName";
209 String uuid = "unknownThermostat";
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) {
220 String key = pair[0].toLowerCase();
221 String value = pair[1].trim();
222 logger.debug("key: {} value: {}.", key, value);
223 if ("location".equals(key)) {
226 ip = new URL(value).getHost();
227 } catch (MalformedURLException e) {
228 logger.debug("Malfored URL {}", e.getMessage());
234 logger.debug("Found thermostat, ip: {} ", ip);
237 logger.debug("Bad Format from thermostat");
243 boolean isCT80 = false;
246 // Run the HTTP request and get the JSON response from the thermostat
247 sysinfo = HttpUtil.executeUrl("GET", url, 20000);
248 content = new JsonParser().parse(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());
256 String nameinfo = HttpUtil.executeUrl("GET", url + "name", 20000);
257 content = new JsonParser().parse(nameinfo).getAsJsonObject();
258 name = content.get("name").getAsString();
259 } catch (IOException | JsonSyntaxException e) {
260 logger.debug("Cannot get name from thermostat {} {}", ip, e.getMessage());
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());
270 logger.debug("Discovery returned: {} uuid {} name {}", sysinfo, uuid, name);
272 ThingUID thingUid = new ThingUID(RadioThermostatBindingConstants.THING_TYPE_RTHERM, uuid);
274 logger.debug("Got discovered device.");
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);