2 * Copyright (c) 2010-2020 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.miio.internal.discovery;
15 import static org.openhab.binding.miio.internal.MiIoBindingConstants.*;
17 import java.io.IOException;
18 import java.net.DatagramPacket;
19 import java.net.DatagramSocket;
20 import java.net.InetAddress;
21 import java.net.SocketException;
22 import java.util.Arrays;
23 import java.util.HashSet;
25 import java.util.concurrent.ScheduledFuture;
26 import java.util.concurrent.TimeUnit;
28 import org.eclipse.jdt.annotation.NonNullByDefault;
29 import org.eclipse.jdt.annotation.Nullable;
30 import org.openhab.binding.miio.internal.Message;
31 import org.openhab.binding.miio.internal.Utils;
32 import org.openhab.binding.miio.internal.cloud.CloudConnector;
33 import org.openhab.binding.miio.internal.cloud.CloudDeviceDTO;
34 import org.openhab.core.config.discovery.AbstractDiscoveryService;
35 import org.openhab.core.config.discovery.DiscoveryResultBuilder;
36 import org.openhab.core.config.discovery.DiscoveryService;
37 import org.openhab.core.net.NetUtil;
38 import org.openhab.core.thing.ThingTypeUID;
39 import org.openhab.core.thing.ThingUID;
40 import org.osgi.service.component.annotations.Activate;
41 import org.osgi.service.component.annotations.Component;
42 import org.osgi.service.component.annotations.Reference;
43 import org.slf4j.Logger;
44 import org.slf4j.LoggerFactory;
47 * The {@link MiIoDiscovery} is responsible for discovering new Xiaomi Mi IO devices
50 * @author Marcel Verpaalen - Initial contribution
54 @Component(service = DiscoveryService.class, immediate = true, configurationPid = "discovery.miio")
55 public class MiIoDiscovery extends AbstractDiscoveryService {
57 /** The refresh interval for background discovery */
58 private static final long SEARCH_INTERVAL = 600;
59 private static final int BUFFER_LENGTH = 1024;
60 private static final int DISCOVERY_TIME = 10;
62 private @Nullable ScheduledFuture<?> miIoDiscoveryJob;
63 protected @Nullable DatagramSocket clientSocket;
64 private @Nullable Thread socketReceiveThread;
65 private Set<String> responseIps = new HashSet<>();
67 private final Logger logger = LoggerFactory.getLogger(MiIoDiscovery.class);
68 private final CloudConnector cloudConnector;
71 public MiIoDiscovery(@Reference CloudConnector cloudConnector) throws IllegalArgumentException {
72 super(DISCOVERY_TIME);
73 this.cloudConnector = cloudConnector;
77 public Set<ThingTypeUID> getSupportedThingTypes() {
78 return SUPPORTED_THING_TYPES_UIDS;
82 protected void startBackgroundDiscovery() {
83 logger.debug("Start Xiaomi Mi IO background discovery");
84 final @Nullable ScheduledFuture<?> miIoDiscoveryJob = this.miIoDiscoveryJob;
85 if (miIoDiscoveryJob == null || miIoDiscoveryJob.isCancelled()) {
86 this.miIoDiscoveryJob = scheduler.scheduleWithFixedDelay(this::discover, 0, SEARCH_INTERVAL,
92 protected void stopBackgroundDiscovery() {
93 logger.debug("Stop Xiaomi Mi IO background discovery");
94 final @Nullable ScheduledFuture<?> miIoDiscoveryJob = this.miIoDiscoveryJob;
95 if (miIoDiscoveryJob != null) {
96 miIoDiscoveryJob.cancel(true);
97 this.miIoDiscoveryJob = null;
102 protected void deactivate() {
103 stopReceiverThreat();
104 final DatagramSocket clientSocket = this.clientSocket;
105 if (clientSocket != null) {
106 clientSocket.close();
108 this.clientSocket = null;
113 protected void startScan() {
114 logger.debug("Start Xiaomi Mi IO discovery");
115 final DatagramSocket clientSocket = getSocket();
116 if (clientSocket != null) {
117 logger.debug("Discovery using socket on port {}", clientSocket.getLocalPort());
120 logger.debug("Discovery not started. Client DatagramSocket null");
124 private void discover() {
125 startReceiverThreat();
126 responseIps = new HashSet<>();
127 HashSet<String> broadcastAddresses = new HashSet<>();
128 broadcastAddresses.add("224.0.0.1");
129 broadcastAddresses.add("224.0.0.50");
130 broadcastAddresses.addAll(NetUtil.getAllBroadcastAddresses());
131 for (String broadcastAdress : broadcastAddresses) {
132 sendDiscoveryRequest(broadcastAdress);
136 private void discovered(String ip, byte[] response) {
137 logger.trace("Discovery responses from : {}:{}", ip, Utils.getSpacedHex(response));
138 Message msg = new Message(response);
139 String token = Utils.getHex(msg.getChecksum());
140 String id = Utils.getHex(msg.getDeviceId());
141 String label = "Xiaomi Mi Device " + id + " (" + Long.parseUnsignedLong(id, 16) + ")";
143 boolean isOnline = false;
144 if (cloudConnector.isConnected()) {
145 cloudConnector.getDevicesList();
146 CloudDeviceDTO cloudInfo = cloudConnector.getDeviceInfo(id);
147 if (cloudInfo != null) {
148 logger.debug("Cloud Info: {}", cloudInfo);
149 token = cloudInfo.getToken();
150 label = cloudInfo.getName() + " " + id + " (" + Long.parseUnsignedLong(id, 16) + ")";
151 country = cloudInfo.getServer();
152 isOnline = cloudInfo.getIsOnline();
155 ThingUID uid = new ThingUID(THING_TYPE_MIIO, id);
156 logger.debug("Discovered Mi Device {} ({}) at {} as {}", id, Long.parseUnsignedLong(id, 16), ip, uid);
157 DiscoveryResultBuilder dr = DiscoveryResultBuilder.create(uid).withProperty(PROPERTY_HOST_IP, ip)
158 .withProperty(PROPERTY_DID, id);
159 if (IGNORED_TOKENS.contains(token)) {
161 "No token discovered for device {}. For options how to get the token, check the binding readme.",
163 dr = dr.withRepresentationProperty(PROPERTY_DID).withLabel(label);
165 logger.debug("Discovered token for device {}: {}", id, token);
166 dr = dr.withProperty(PROPERTY_TOKEN, token).withRepresentationProperty(PROPERTY_DID)
167 .withLabel(label + " with token");
169 if (!country.isEmpty() && isOnline) {
170 dr = dr.withProperty(PROPERTY_CLOUDSERVER, country);
172 thingDiscovered(dr.build());
175 synchronized @Nullable DatagramSocket getSocket() {
176 DatagramSocket clientSocket = this.clientSocket;
177 if (clientSocket != null && clientSocket.isBound()) {
181 logger.debug("Getting new socket for discovery");
182 clientSocket = new DatagramSocket();
183 clientSocket.setReuseAddress(true);
184 clientSocket.setBroadcast(true);
185 this.clientSocket = clientSocket;
187 } catch (SocketException | SecurityException e) {
188 logger.debug("Error getting socket for discovery: {}", e.getMessage());
193 private void closeSocket() {
194 final @Nullable DatagramSocket clientSocket = this.clientSocket;
195 if (clientSocket != null) {
196 clientSocket.close();
200 this.clientSocket = null;
203 private void sendDiscoveryRequest(String ipAddress) {
204 final @Nullable DatagramSocket socket = getSocket();
205 if (socket != null) {
207 byte[] sendData = DISCOVER_STRING;
208 logger.trace("Discovery sending ping to {} from {}:{}", ipAddress, socket.getLocalAddress(),
209 socket.getLocalPort());
210 DatagramPacket sendPacket = new DatagramPacket(sendData, sendData.length,
211 InetAddress.getByName(ipAddress), PORT);
212 for (int i = 1; i <= 1; i++) {
213 socket.send(sendPacket);
215 } catch (IOException e) {
216 logger.trace("Discovery on {} error: {}", ipAddress, e.getMessage());
222 * starts the {@link ReceiverThread} thread
224 private synchronized void startReceiverThreat() {
225 final Thread srt = socketReceiveThread;
227 if (srt.isAlive() && !srt.isInterrupted()) {
231 stopReceiverThreat();
232 Thread socketReceiveThread = new ReceiverThread();
233 socketReceiveThread.start();
234 this.socketReceiveThread = socketReceiveThread;
238 * Stops the {@link ReceiverThread} thread
240 private synchronized void stopReceiverThreat() {
241 if (socketReceiveThread != null) {
242 socketReceiveThread.interrupt();
243 socketReceiveThread = null;
249 * The thread, which waits for data and submits the unique results addresses to the discovery results
252 private class ReceiverThread extends Thread {
255 DatagramSocket socket = getSocket();
256 if (socket != null) {
257 logger.debug("Starting discovery receiver thread for socket on port {}", socket.getLocalPort());
263 * This method waits for data and submits the unique results addresses to the discovery results
265 * @param socket - The multicast socket to (re)use
267 private void receiveData(DatagramSocket socket) {
268 DatagramPacket receivePacket = new DatagramPacket(new byte[BUFFER_LENGTH], BUFFER_LENGTH);
270 while (!interrupted()) {
271 logger.trace("Thread {} waiting for data on port {}", this, socket.getLocalPort());
272 socket.receive(receivePacket);
273 String hostAddress = receivePacket.getAddress().getHostAddress();
274 logger.trace("Received {} bytes response from {}:{} on Port {}", receivePacket.getLength(),
275 hostAddress, receivePacket.getPort(), socket.getLocalPort());
277 byte[] messageBuf = Arrays.copyOfRange(receivePacket.getData(), receivePacket.getOffset(),
278 receivePacket.getOffset() + receivePacket.getLength());
279 if (logger.isTraceEnabled()) {
280 Message miIoResponse = new Message(messageBuf);
281 logger.trace("Discovery response received from {} DeviceID: {}\r\n{}", hostAddress,
282 Utils.getHex(miIoResponse.getDeviceId()), miIoResponse.toSting());
284 if (!responseIps.contains(hostAddress)) {
285 scheduler.schedule(() -> {
287 discovered(hostAddress, messageBuf);
288 } catch (Exception e) {
289 logger.debug("Error submitting discovered Mi IO device at {}", hostAddress, e);
291 }, 0, TimeUnit.SECONDS);
293 responseIps.add(hostAddress);
295 } catch (SocketException e) {
296 logger.debug("Receiver thread received SocketException: {}", e.getMessage());
297 } catch (IOException e) {
298 logger.trace("Receiver thread was interrupted");
300 logger.debug("Receiver thread ended");