[miio] Add discovery from cloud feature. (#9176)

* [miio] Add discovery from cloud feature.

This allows to discover things that are not directly on the same subnet

Signed-off-by: Marcel Verpaalen <marcel@verpaalen.com>
Co-authored-by: Connor Petty <mistercpp2000+gitsignoff@gmail.com>
This commit is contained in:
Marcel 2020-12-04 21:26:44 -08:00 committed by GitHub
parent 568da33684
commit 05b7bbdccf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 131 additions and 8 deletions

View File

@ -101,4 +101,22 @@ public final class Utils {
public static String minLengthString(String string, int length) { public static String minLengthString(String string, int length) {
return String.format("%-" + length + "s", string); return String.format("%-" + length + "s", string);
} }
public static String toHEX(String value) {
try {
return String.format("%08X", Long.parseUnsignedLong(value));
} catch (NumberFormatException e) {
//
}
return value;
}
public static String fromHEX(String value) {
try {
return String.format("%d", Long.parseUnsignedLong(value, 16));
} catch (NumberFormatException e) {
//
}
return value;
}
} }

View File

@ -20,14 +20,19 @@ import java.net.DatagramSocket;
import java.net.InetAddress; import java.net.InetAddress;
import java.net.SocketException; import java.net.SocketException;
import java.util.Arrays; import java.util.Arrays;
import java.util.Dictionary;
import java.util.HashSet; import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ScheduledFuture; import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable; import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.miio.internal.Message; import org.openhab.binding.miio.internal.Message;
import org.openhab.binding.miio.internal.MiIoDevices;
import org.openhab.binding.miio.internal.Utils; import org.openhab.binding.miio.internal.Utils;
import org.openhab.binding.miio.internal.cloud.CloudConnector; import org.openhab.binding.miio.internal.cloud.CloudConnector;
import org.openhab.binding.miio.internal.cloud.CloudDeviceDTO; import org.openhab.binding.miio.internal.cloud.CloudDeviceDTO;
@ -37,6 +42,8 @@ import org.openhab.core.config.discovery.DiscoveryService;
import org.openhab.core.net.NetUtil; import org.openhab.core.net.NetUtil;
import org.openhab.core.thing.ThingTypeUID; import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.ThingUID; import org.openhab.core.thing.ThingUID;
import org.osgi.service.cm.Configuration;
import org.osgi.service.cm.ConfigurationAdmin;
import org.osgi.service.component.annotations.Activate; import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component; import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference; import org.osgi.service.component.annotations.Reference;
@ -58,6 +65,9 @@ public class MiIoDiscovery extends AbstractDiscoveryService {
private static final long SEARCH_INTERVAL = 600; private static final long SEARCH_INTERVAL = 600;
private static final int BUFFER_LENGTH = 1024; private static final int BUFFER_LENGTH = 1024;
private static final int DISCOVERY_TIME = 10; private static final int DISCOVERY_TIME = 10;
private static final String DISABLED = "disabled";
private static final String SUPPORTED = "supportedonly";
private static final String ALL = "all";
private @Nullable ScheduledFuture<?> miIoDiscoveryJob; private @Nullable ScheduledFuture<?> miIoDiscoveryJob;
protected @Nullable DatagramSocket clientSocket; protected @Nullable DatagramSocket clientSocket;
@ -66,11 +76,37 @@ public class MiIoDiscovery extends AbstractDiscoveryService {
private final Logger logger = LoggerFactory.getLogger(MiIoDiscovery.class); private final Logger logger = LoggerFactory.getLogger(MiIoDiscovery.class);
private final CloudConnector cloudConnector; private final CloudConnector cloudConnector;
private Map<String, String> cloudDevices = new ConcurrentHashMap<>();
private @Nullable Configuration miioConfig;
@Activate @Activate
public MiIoDiscovery(@Reference CloudConnector cloudConnector) throws IllegalArgumentException { public MiIoDiscovery(@Reference CloudConnector cloudConnector, @Reference ConfigurationAdmin configAdmin)
throws IllegalArgumentException {
super(DISCOVERY_TIME); super(DISCOVERY_TIME);
this.cloudConnector = cloudConnector; this.cloudConnector = cloudConnector;
try {
miioConfig = configAdmin.getConfiguration("binding.miio");
} catch (IOException | SecurityException e) {
logger.debug("Error getting configuration: {}", e.getMessage());
}
}
private String getCloudDiscoveryMode() {
if (miioConfig != null) {
try {
Dictionary<String, @Nullable Object> properties = miioConfig.getProperties();
String cloudDiscoveryModeConfig = (String) properties.get("cloudDiscoveryMode");
if (cloudDiscoveryModeConfig == null) {
cloudDiscoveryModeConfig = DISABLED;
} else {
cloudDiscoveryModeConfig = cloudDiscoveryModeConfig.toLowerCase();
}
return Set.of(SUPPORTED, ALL).contains(cloudDiscoveryModeConfig) ? cloudDiscoveryModeConfig : DISABLED;
} catch (ClassCastException | SecurityException e) {
logger.debug("Error getting cloud discovery configuration: {}", e.getMessage());
}
}
return DISABLED;
} }
@Override @Override
@ -80,7 +116,7 @@ public class MiIoDiscovery extends AbstractDiscoveryService {
@Override @Override
protected void startBackgroundDiscovery() { protected void startBackgroundDiscovery() {
logger.debug("Start Xiaomi Mi IO background discovery"); logger.debug("Start Xiaomi Mi IO background discovery with cloudDiscoveryMode: {}", getCloudDiscoveryMode());
final @Nullable ScheduledFuture<?> miIoDiscoveryJob = this.miIoDiscoveryJob; final @Nullable ScheduledFuture<?> miIoDiscoveryJob = this.miIoDiscoveryJob;
if (miIoDiscoveryJob == null || miIoDiscoveryJob.isCancelled()) { if (miIoDiscoveryJob == null || miIoDiscoveryJob.isCancelled()) {
this.miIoDiscoveryJob = scheduler.scheduleWithFixedDelay(this::discover, 0, SEARCH_INTERVAL, this.miIoDiscoveryJob = scheduler.scheduleWithFixedDelay(this::discover, 0, SEARCH_INTERVAL,
@ -111,7 +147,11 @@ public class MiIoDiscovery extends AbstractDiscoveryService {
@Override @Override
protected void startScan() { protected void startScan() {
logger.debug("Start Xiaomi Mi IO discovery"); String cloudDiscoveryMode = getCloudDiscoveryMode();
logger.debug("Start Xiaomi Mi IO discovery with cloudDiscoveryMode: {}", cloudDiscoveryMode);
if (!cloudDiscoveryMode.contentEquals(DISABLED)) {
cloudDiscovery();
}
final DatagramSocket clientSocket = getSocket(); final DatagramSocket clientSocket = getSocket();
if (clientSocket != null) { if (clientSocket != null) {
logger.debug("Discovery using socket on port {}", clientSocket.getLocalPort()); logger.debug("Discovery using socket on port {}", clientSocket.getLocalPort());
@ -133,36 +173,73 @@ public class MiIoDiscovery extends AbstractDiscoveryService {
} }
} }
private void cloudDiscovery() {
String cloudDiscoveryMode = getCloudDiscoveryMode();
cloudDevices.clear();
if (cloudConnector.isConnected()) {
List<CloudDeviceDTO> dv = cloudConnector.getDevicesList();
for (CloudDeviceDTO device : dv) {
String id = Utils.toHEX(device.getDid());
if (cloudDiscoveryMode.contentEquals(SUPPORTED)) {
if (MiIoDevices.getType(device.getModel()).getThingType().equals(THING_TYPE_UNSUPPORTED)) {
logger.warn("Discovered from cloud, but ignored because not supported: {} {}", id, device);
}
}
if (device.getIsOnline()) {
logger.debug("Discovered from cloud: {} {}", id, device);
cloudDevices.put(id, device.getLocalip());
String token = device.getToken();
String label = device.getName() + " " + id + " (" + device.getDid() + ")";
String country = device.getServer();
boolean isOnline = device.getIsOnline();
String ip = device.getLocalip();
submitDiscovery(ip, token, id, label, country, isOnline);
} else {
logger.debug("Discovered from cloud, but ignored because not online: {} {}", id, device);
}
}
}
}
private void discovered(String ip, byte[] response) { private void discovered(String ip, byte[] response) {
logger.trace("Discovery responses from : {}:{}", ip, Utils.getSpacedHex(response)); logger.trace("Discovery responses from : {}:{}", ip, Utils.getSpacedHex(response));
Message msg = new Message(response); Message msg = new Message(response);
String token = Utils.getHex(msg.getChecksum()); String token = Utils.getHex(msg.getChecksum());
String id = Utils.getHex(msg.getDeviceId()); String id = Utils.getHex(msg.getDeviceId());
String label = "Xiaomi Mi Device " + id + " (" + Long.parseUnsignedLong(id, 16) + ")"; String label = "Xiaomi Mi Device " + id + " (" + Utils.fromHEX(id) + ")";
String country = ""; String country = "";
boolean isOnline = false; boolean isOnline = false;
if (ip.equals(cloudDevices.get(id))) {
logger.debug("Skipped adding local found {}. Already discovered by cloud.", label);
return;
}
if (cloudConnector.isConnected()) { if (cloudConnector.isConnected()) {
cloudConnector.getDevicesList(); cloudConnector.getDevicesList();
CloudDeviceDTO cloudInfo = cloudConnector.getDeviceInfo(id); CloudDeviceDTO cloudInfo = cloudConnector.getDeviceInfo(id);
if (cloudInfo != null) { if (cloudInfo != null) {
logger.debug("Cloud Info: {}", cloudInfo); logger.debug("Cloud Info: {}", cloudInfo);
token = cloudInfo.getToken(); token = cloudInfo.getToken();
label = cloudInfo.getName() + " " + id + " (" + Long.parseUnsignedLong(id, 16) + ")"; label = cloudInfo.getName() + " " + id + " (" + Utils.fromHEX(id) + ")";
country = cloudInfo.getServer(); country = cloudInfo.getServer();
isOnline = cloudInfo.getIsOnline(); isOnline = cloudInfo.getIsOnline();
} }
} }
submitDiscovery(ip, token, id, label, country, isOnline);
}
private void submitDiscovery(String ip, String token, String id, String label, String country, boolean isOnline) {
ThingUID uid = new ThingUID(THING_TYPE_MIIO, id); ThingUID uid = new ThingUID(THING_TYPE_MIIO, id);
logger.debug("Discovered Mi Device {} ({}) at {} as {}", id, Long.parseUnsignedLong(id, 16), ip, uid);
DiscoveryResultBuilder dr = DiscoveryResultBuilder.create(uid).withProperty(PROPERTY_HOST_IP, ip) DiscoveryResultBuilder dr = DiscoveryResultBuilder.create(uid).withProperty(PROPERTY_HOST_IP, ip)
.withProperty(PROPERTY_DID, id); .withProperty(PROPERTY_DID, id);
if (IGNORED_TOKENS.contains(token)) { if (IGNORED_TOKENS.contains(token)) {
logger.debug("Discovered Mi Device {} ({}) at {} as {}", id, Utils.fromHEX(id), ip, uid);
logger.debug( logger.debug(
"No token discovered for device {}. For options how to get the token, check the binding readme.", "No token discovered for device {}. For options how to get the token, check the binding readme.",
id); id);
dr = dr.withRepresentationProperty(PROPERTY_DID).withLabel(label); dr = dr.withRepresentationProperty(PROPERTY_DID).withLabel(label);
} else { } else {
logger.debug("Discovered token for device {}: {}", id, token); logger.debug("Discovered Mi Device {} ({}) at {} as {} with token {}", id, Utils.fromHEX(id), ip, uid,
token);
dr = dr.withProperty(PROPERTY_TOKEN, token).withRepresentationProperty(PROPERTY_DID) dr = dr.withProperty(PROPERTY_TOKEN, token).withRepresentationProperty(PROPERTY_DID)
.withLabel(label + " with token"); .withLabel(label + " with token");
} }

View File

@ -21,6 +21,18 @@
binding readme for country to server mapping</description> binding readme for country to server mapping</description>
<required>false</required> <required>false</required>
</parameter> </parameter>
<parameter name="cloudDiscoveryMode" type="text">
<default>disabled</default>
<label>Cloud Discovery Mode</label>
<description>Allow for discovery via the cloud. This may be used for devices that are not on the same network as
OpenHAB server</description>
<options>
<option value="disabled">Local discovery only (Default)</option>
<option value="supportedOnly">Discover online supported devices from Xiaomi cloud</option>
<option value="all">Discover all online devices from Xiaomi cloud</option>
</options>
<required>false</required>
</parameter>
</config-description> </config-description>
</binding:binding> </binding:binding>

View File

@ -12,7 +12,7 @@
*/ */
package org.openhab.binding.miio.internal; package org.openhab.binding.miio.internal;
import static org.junit.jupiter.api.Assertions.*; import static org.junit.jupiter.api.Assertions.assertEquals;
import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
@ -50,4 +50,20 @@ public class UtilsTest {
assertEquals("66147986XXXXXXXXXXXXXXXXda22479a6614798643fe781563c1eebeda22479a", assertEquals("66147986XXXXXXXXXXXXXXXXda22479a6614798643fe781563c1eebeda22479a",
Utils.obfuscateToken(tokenString)); Utils.obfuscateToken(tokenString));
} }
@Test
public void fromToDiD() {
String did = "03BD3CE5";
assertEquals("62733541", Utils.fromHEX(did));
did = "0ABD3CE5";
assertEquals("180174053", Utils.fromHEX(did));
did = "62733541";
assertEquals("03BD3CE5", Utils.toHEX(did));
did = "cant parse";
assertEquals("cant parse", Utils.toHEX(did));
assertEquals("cant parse", Utils.fromHEX(did));
}
} }