]> git.basschouten.com Git - openhab-addons.git/blob
4b279c21495b24b77bb2dfe30fde606ce2ae45f2
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2020 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.miio.internal.discovery;
14
15 import static org.openhab.binding.miio.internal.MiIoBindingConstants.*;
16
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;
24 import java.util.Set;
25 import java.util.concurrent.ScheduledFuture;
26 import java.util.concurrent.TimeUnit;
27
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;
45
46 /**
47  * The {@link MiIoDiscovery} is responsible for discovering new Xiaomi Mi IO devices
48  * and their token
49  *
50  * @author Marcel Verpaalen - Initial contribution
51  *
52  */
53 @NonNullByDefault
54 @Component(service = DiscoveryService.class, configurationPid = "discovery.miio")
55 public class MiIoDiscovery extends AbstractDiscoveryService {
56
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;
61
62     private @Nullable ScheduledFuture<?> miIoDiscoveryJob;
63     protected @Nullable DatagramSocket clientSocket;
64     private @Nullable Thread socketReceiveThread;
65     private Set<String> responseIps = new HashSet<>();
66
67     private final Logger logger = LoggerFactory.getLogger(MiIoDiscovery.class);
68     private final CloudConnector cloudConnector;
69
70     @Activate
71     public MiIoDiscovery(@Reference CloudConnector cloudConnector) throws IllegalArgumentException {
72         super(DISCOVERY_TIME);
73         this.cloudConnector = cloudConnector;
74     }
75
76     @Override
77     public Set<ThingTypeUID> getSupportedThingTypes() {
78         return SUPPORTED_THING_TYPES_UIDS;
79     }
80
81     @Override
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,
87                     TimeUnit.SECONDS);
88         }
89     }
90
91     @Override
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;
98         }
99     }
100
101     @Override
102     protected void deactivate() {
103         stopReceiverThreat();
104         final DatagramSocket clientSocket = this.clientSocket;
105         if (clientSocket != null) {
106             clientSocket.close();
107         }
108         this.clientSocket = null;
109         super.deactivate();
110     }
111
112     @Override
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());
118             discover();
119         } else {
120             logger.debug("Discovery not started. Client DatagramSocket null");
121         }
122     }
123
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);
133         }
134     }
135
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) + ")";
142         String country = "";
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();
153             }
154         }
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)) {
160             logger.debug(
161                     "No token discovered for device {}. For options how to get the token, check the binding readme.",
162                     id);
163             dr = dr.withRepresentationProperty(PROPERTY_DID).withLabel(label);
164         } else {
165             logger.debug("Discovered token for device {}: {}", id, token);
166             dr = dr.withProperty(PROPERTY_TOKEN, token).withRepresentationProperty(PROPERTY_DID)
167                     .withLabel(label + " with token");
168         }
169         if (!country.isEmpty() && isOnline) {
170             dr = dr.withProperty(PROPERTY_CLOUDSERVER, country);
171         }
172         thingDiscovered(dr.build());
173     }
174
175     synchronized @Nullable DatagramSocket getSocket() {
176         DatagramSocket clientSocket = this.clientSocket;
177         if (clientSocket != null && clientSocket.isBound()) {
178             return clientSocket;
179         }
180         try {
181             logger.debug("Getting new socket for discovery");
182             clientSocket = new DatagramSocket();
183             clientSocket.setReuseAddress(true);
184             clientSocket.setBroadcast(true);
185             this.clientSocket = clientSocket;
186             return clientSocket;
187         } catch (SocketException | SecurityException e) {
188             logger.debug("Error getting socket for discovery: {}", e.getMessage());
189         }
190         return null;
191     }
192
193     private void closeSocket() {
194         final @Nullable DatagramSocket clientSocket = this.clientSocket;
195         if (clientSocket != null) {
196             clientSocket.close();
197         } else {
198             return;
199         }
200         this.clientSocket = null;
201     }
202
203     private void sendDiscoveryRequest(String ipAddress) {
204         final @Nullable DatagramSocket socket = getSocket();
205         if (socket != null) {
206             try {
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);
214                 }
215             } catch (IOException e) {
216                 logger.trace("Discovery on {} error: {}", ipAddress, e.getMessage());
217             }
218         }
219     }
220
221     /**
222      * starts the {@link ReceiverThread} thread
223      */
224     private synchronized void startReceiverThreat() {
225         final Thread srt = socketReceiveThread;
226         if (srt != null) {
227             if (srt.isAlive() && !srt.isInterrupted()) {
228                 return;
229             }
230         }
231         stopReceiverThreat();
232         Thread socketReceiveThread = new ReceiverThread();
233         socketReceiveThread.start();
234         this.socketReceiveThread = socketReceiveThread;
235     }
236
237     /**
238      * Stops the {@link ReceiverThread} thread
239      */
240     private synchronized void stopReceiverThreat() {
241         if (socketReceiveThread != null) {
242             socketReceiveThread.interrupt();
243             socketReceiveThread = null;
244         }
245         closeSocket();
246     }
247
248     /**
249      * The thread, which waits for data and submits the unique results addresses to the discovery results
250      *
251      */
252     private class ReceiverThread extends Thread {
253         @Override
254         public void run() {
255             DatagramSocket socket = getSocket();
256             if (socket != null) {
257                 logger.debug("Starting discovery receiver thread for socket on port {}", socket.getLocalPort());
258                 receiveData(socket);
259             }
260         }
261
262         /**
263          * This method waits for data and submits the unique results addresses to the discovery results
264          *
265          * @param socket - The multicast socket to (re)use
266          */
267         private void receiveData(DatagramSocket socket) {
268             DatagramPacket receivePacket = new DatagramPacket(new byte[BUFFER_LENGTH], BUFFER_LENGTH);
269             try {
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());
276
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());
283                     }
284                     if (!responseIps.contains(hostAddress)) {
285                         scheduler.schedule(() -> {
286                             try {
287                                 discovered(hostAddress, messageBuf);
288                             } catch (Exception e) {
289                                 logger.debug("Error submitting discovered Mi IO device at {}", hostAddress, e);
290                             }
291                         }, 0, TimeUnit.SECONDS);
292                     }
293                     responseIps.add(hostAddress);
294                 }
295             } catch (SocketException e) {
296                 logger.debug("Receiver thread received SocketException: {}", e.getMessage());
297             } catch (IOException e) {
298                 logger.trace("Receiver thread was interrupted");
299             }
300             logger.debug("Receiver thread ended");
301         }
302     }
303 }