2 * Copyright (c) 2010-2023 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.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;
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;
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;
45 import com.google.gson.JsonObject;
46 import com.google.gson.JsonParser;
47 import com.google.gson.JsonSyntaxException;
50 * The {@link RadioThermostatDiscoveryService} is responsible for discovery of
51 * RadioThermostats on the local network
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
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";
65 private static final String SSDP_MATCH = "WM-NOTIFY";
66 private static final int BACKGROUND_SCAN_INTERVAL_SECONDS = 300;
68 private @Nullable ScheduledFuture<?> scheduledFuture = null;
70 public RadioThermostatDiscoveryService() {
71 super(RadioThermostatBindingConstants.SUPPORTED_THING_TYPES_UIDS, 30, true);
75 protected void startBackgroundDiscovery() {
76 logger.debug("Starting Background Scan");
77 stopBackgroundDiscovery();
78 scheduledFuture = scheduler.scheduleWithFixedDelay(this::doRunRun, 0, BACKGROUND_SCAN_INTERVAL_SECONDS,
83 protected void stopBackgroundDiscovery() {
84 ScheduledFuture<?> scheduledFuture = this.scheduledFuture;
85 if (scheduledFuture != null) {
86 scheduledFuture.cancel(true);
87 this.scheduledFuture = null;
92 protected void startScan() {
93 logger.debug("Starting Interactive Scan");
97 protected synchronized void doRunRun() {
98 logger.debug("Sending SSDP discover.");
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);
107 } catch (IOException e) {
108 logger.debug("Error discovering devices", e);
113 * Broadcasts a SSDP discovery message into the network to find provided
116 * @return The Socket the answers will arrive at.
117 * @throws UnknownHostException
118 * @throws IOException
119 * @throws SocketException
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");
126 Enumeration<InetAddress> addrs = ni.getInetAddresses();
127 InetAddress a = null;
128 while (addrs.hasMoreElements()) {
129 a = addrs.nextElement();
130 if (a instanceof Inet4Address) {
137 logger.debug("no ipv4 address on {}", ni.getName());
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);
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);
156 // Try to ensure that joinGroup has taken effect. Without this delay, the query
157 // packet ends up going out before the group join.
160 socket.send(datagramPacket);
162 byte[] buf = new byte[4096];
163 DatagramPacket packet = new DatagramPacket(buf, buf.length);
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);
175 logger.debug("Bridge device scan interrupted");
176 } catch (SocketTimeoutException e) {
178 "Timed out waiting for multicast response. Presumably all devices have already responded.");
181 socket.leaveGroup(m);
184 } catch (IOException | InterruptedException e) {
185 logger.debug("got exception: {}", e.getMessage());
191 * Scans all messages that arrive on the socket and scans them for the
192 * search keywords. The search is not case sensitive.
197 protected void parseResponse(String response) {
198 DiscoveryResult result;
200 String name = "unknownName";
201 String uuid = "unknownThermostat";
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) {
212 String key = pair[0].toLowerCase();
213 String value = pair[1].trim();
214 logger.debug("key: {} value: {}.", key, value);
215 if ("location".equals(key)) {
218 ip = new URL(value).getHost();
219 } catch (MalformedURLException e) {
220 logger.debug("Malfored URL {}", e.getMessage());
226 logger.debug("Found thermostat, ip: {} ", ip);
229 logger.debug("Bad Format from thermostat");
235 boolean isCT80 = false;
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());
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());
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());
262 logger.debug("Discovery returned: {} uuid {} name {}", sysinfo, uuid, name);
264 ThingUID thingUid = new ThingUID(RadioThermostatBindingConstants.THING_TYPE_RTHERM, uuid);
266 logger.debug("Got discovered device.");
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);