]> git.basschouten.com Git - openhab-addons.git/commitdiff
[miio] Cloud Communication for devices (#8981)
authorMarcel <marcelrv@users.noreply.github.com>
Sun, 29 Nov 2020 04:03:12 +0000 (05:03 +0100)
committerGitHub <noreply@github.com>
Sun, 29 Nov 2020 04:03:12 +0000 (20:03 -0800)
* [miio]  Cloud Communication for devices

Allows to define if communication to devices is direct or send via the
Xiaomi cloud.
Introduce additional channel to execute commands via cloud.

Other small improvements
* Use common method from abstract handler to send commands
* Common way to handle custom commands
* Introduce small delay before refreshing robot properties after sending
commands (similar to the basic handler) so devices have time to update
their properties

* [miio] simplify cloudconnector
* [miio] Cleanup all jobs when unloading
* [miio] update to use dedicated ScheduledExecutorService

Use dedicated ScheduledExecutorService to avoid unloading problems

* Update bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/handler/MiIoAbstractHandler.java
* [miio] fix for removeif
* miio- Improve scheduler
* [miio] fix communication error if device is not on the network
* [miio] update with comments from feedback

* remove scheduler tracking
* improve status setting for cloud communication
* [miio] update with feedback review

Signed-off-by: Marcel Verpaalen <marcel@verpaalen.com>
Co-authored-by: Connor Petty <mistercpp2000@gmail.com>
19 files changed:
bundles/org.openhab.binding.miio/README.base.md
bundles/org.openhab.binding.miio/README.md
bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/MiIoBindingConfiguration.java
bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/MiIoBindingConstants.java
bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/MiIoHandlerFactory.java
bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/MiIoSendCommand.java
bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/cloud/CloudConnector.java
bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/cloud/MiCloudConnector.java
bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/handler/MiIoAbstractHandler.java
bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/handler/MiIoBasicHandler.java
bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/handler/MiIoGenericHandler.java
bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/handler/MiIoUnsupportedHandler.java
bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/handler/MiIoVacuumHandler.java
bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/transport/MiIoAsyncCommunication.java
bundles/org.openhab.binding.miio/src/main/resources/OH-INF/config/config.xml
bundles/org.openhab.binding.miio/src/main/resources/OH-INF/thing/basicThing.xml
bundles/org.openhab.binding.miio/src/main/resources/OH-INF/thing/commonChannels.xml
bundles/org.openhab.binding.miio/src/main/resources/OH-INF/thing/unsupportedThing.xml
bundles/org.openhab.binding.miio/src/main/resources/OH-INF/thing/vacuumThing.xml

index 2591657065b6940f72009d02de40d4f32ecd32a0..ffe2337ec2c5f400aa3a292297ebcf01c75573f8 100644 (file)
@@ -65,22 +65,25 @@ Optional configuration is the refresh interval and the deviceID. Note that the d
 The configuration for model is automatically retrieved from the device in normal operation. 
 However, for devices that are unsupported, you may override the value and try to use a model string from a similar device to experimentally use your device with the binding.
 
-| Parameter       | Type    | Required | Description                                                       |
-|-----------------|---------|----------|-------------------------------------------------------------------|
-| host            | text    | true     | Device IP address                                                 |
-| token           | text    | true     | Token for communication (in Hex)                                  |
-| deviceId        | text    | true     | Device ID number for communication (in Hex)                       |
-| model           | text    | false    | Device model string, used to determine the subtype                |
-| refreshInterval | integer | false    | Refresh interval for refreshing the data in seconds. (0=disabled) |
-| timeout         | integer | false    | Timeout time in milliseconds                                      |
+| Parameter       | Type    | Required | Description                                                         |
+|-----------------|---------|----------|---------------------------------------------------------------------|
+| host            | text    | true     | Device IP address                                                   |
+| token           | text    | true     | Token for communication (in Hex)                                    |
+| deviceId        | text    | true     | Device ID number for communication (in Hex)                         |
+| model           | text    | false    | Device model string, used to determine the subtype                  |
+| refreshInterval | integer | false    | Refresh interval for refreshing the data in seconds. (0=disabled)   |
+| timeout         | integer | false    | Timeout time in milliseconds                                        |
+| communication   | test    | false    | Communicate direct or via cloud (options values: 'direct', 'cloud') |
+
+Note: Suggest to use the cloud communication only for devices that require it. It is unknown at this time if Xiaomi has a rate limit or other limitations on the cloud usage. e.g. if having many devices would trigger some throttling from the cloud side.
 
 ### Example Thing file
 
-`Thing miio:basic:light "My Light" [ host="192.168.x.x", token="put here your token", deviceId="0326xxxx", model="philips.light.bulb" ]` 
+`Thing miio:basic:light "My Light" [ host="192.168.x.x", token="put here your token", deviceId="0326xxxx", model="philips.light.bulb", communication="direct"  ]` 
 
 or in case of unknown models include the model information of a similar device that is supported:
 
-`Thing miio:vacuum:s50 "vacuum" @ "livingroom" [ host="192.168.15.20", token="xxxxxxx", deviceId=“0470DDAA”, model="roborock.vacuum.s4" ]`
+`Thing miio:vacuum:s50 "vacuum" @ "livingroom" [ host="192.168.15.20", token="xxxxxxx", deviceId=“0470DDAA”, model="roborock.vacuum.s4", communication="cloud"]`
 
 # Mi IO Devices
 
@@ -152,6 +155,7 @@ Alternatively as described above, double check for multiple connections for sing
 _Your device is on a different subnet?_
 This is in most cases not working. 
 Firmware of the device don't accept commands coming from other subnets.
+Set the communication in the thing configuration to 'cloud'.
 
 _Cloud connectivity is not working_
 The most common problem is a wrong userId/password. Try to fix your userId/password.
@@ -177,9 +181,10 @@ All devices have available the following channels (marked as advanced) besides t
 | network#bssid    | String  | Network BSSID                       |
 | network#rssi     | Number  | Network RSSI                        |
 | network#life     | Number  | Network Life                        |
-| actions#commands | String  | send commands. see below            |
+| actions#commands | String  | send commands direct. see below     |
+| actions#rpc      | String  | send commands via cloud. see below  |
 
-note: the ADVANCED  `actions#commands` channel can be used to send commands that are not automated via the binding. This is available for all devices
+note: the ADVANCED  `actions#commands` and `actions#rpc` channels can be used to send commands that are not automated via the binding. This is available for all devices
 e.g. `smarthome:send actionCommand 'upd_timer["1498595904821", "on"]'` would enable a pre-configured timer. See https://github.com/marcelrv/XiaomiRobotVacuumProtocol for all known available commands.
 
 
index d601e28eb57500f2382481cc882ed0b0f473e6ed..3b7432cf0a268988fedf3e50686f85a3da409a6f 100644 (file)
@@ -65,22 +65,25 @@ Optional configuration is the refresh interval and the deviceID. Note that the d
 The configuration for model is automatically retrieved from the device in normal operation. 
 However, for devices that are unsupported, you may override the value and try to use a model string from a similar device to experimentally use your device with the binding.
 
-| Parameter       | Type    | Required | Description                                                       |
-|-----------------|---------|----------|-------------------------------------------------------------------|
-| host            | text    | true     | Device IP address                                                 |
-| token           | text    | true     | Token for communication (in Hex)                                  |
-| deviceId        | text    | true     | Device ID number for communication (in Hex)                       |
-| model           | text    | false    | Device model string, used to determine the subtype                |
-| refreshInterval | integer | false    | Refresh interval for refreshing the data in seconds. (0=disabled) |
-| timeout         | integer | false    | Timeout time in milliseconds                                      |
+| Parameter       | Type    | Required | Description                                                         |
+|-----------------|---------|----------|---------------------------------------------------------------------|
+| host            | text    | true     | Device IP address                                                   |
+| token           | text    | true     | Token for communication (in Hex)                                    |
+| deviceId        | text    | true     | Device ID number for communication (in Hex)                         |
+| model           | text    | false    | Device model string, used to determine the subtype                  |
+| refreshInterval | integer | false    | Refresh interval for refreshing the data in seconds. (0=disabled)   |
+| timeout         | integer | false    | Timeout time in milliseconds                                        |
+| communication   | test    | false    | Communicate direct or via cloud (options values: 'direct', 'cloud') |
+
+Note: Suggest to use the cloud communication only for devices that require it. It is unknown at this time if Xiaomi has a rate limit or other limitations on the cloud usage. e.g. if having many devices would trigger some throttling from the cloud side.
 
 ### Example Thing file
 
-`Thing miio:basic:light "My Light" [ host="192.168.x.x", token="put here your token", deviceId="0326xxxx", model="philips.light.bulb" ]` 
+`Thing miio:basic:light "My Light" [ host="192.168.x.x", token="put here your token", deviceId="0326xxxx", model="philips.light.bulb", communication="direct"  ]` 
 
 or in case of unknown models include the model information of a similar device that is supported:
 
-`Thing miio:vacuum:s50 "vacuum" @ "livingroom" [ host="192.168.15.20", token="xxxxxxx", deviceId=“0470DDAA”, model="roborock.vacuum.s4" ]`
+`Thing miio:vacuum:s50 "vacuum" @ "livingroom" [ host="192.168.15.20", token="xxxxxxx", deviceId=“0470DDAA”, model="roborock.vacuum.s4", communication="cloud"]`
 
 # Mi IO Devices
 
@@ -396,6 +399,7 @@ Alternatively as described above, double check for multiple connections for sing
 _Your device is on a different subnet?_
 This is in most cases not working. 
 Firmware of the device don't accept commands coming from other subnets.
+Set the communication in the thing configuration to 'cloud'.
 
 _Cloud connectivity is not working_
 The most common problem is a wrong userId/password. Try to fix your userId/password.
@@ -421,9 +425,10 @@ All devices have available the following channels (marked as advanced) besides t
 | network#bssid    | String  | Network BSSID                       |
 | network#rssi     | Number  | Network RSSI                        |
 | network#life     | Number  | Network Life                        |
-| actions#commands | String  | send commands. see below            |
+| actions#commands | String  | send commands direct. see below     |
+| actions#rpc      | String  | send commands via cloud. see below  |
 
-note: the ADVANCED  `actions#commands` channel can be used to send commands that are not automated via the binding. This is available for all devices
+note: the ADVANCED  `actions#commands` and `actions#rpc` channels can be used to send commands that are not automated via the binding. This is available for all devices
 e.g. `smarthome:send actionCommand 'upd_timer["1498595904821", "on"]'` would enable a pre-configured timer. See https://github.com/marcelrv/XiaomiRobotVacuumProtocol for all known available commands.
 
 
index ca71dd4cf813512e475776201ad31d374d89dccb..153dd4d34eed45a40a456ba99a61ed42f4667d72 100644 (file)
@@ -24,6 +24,7 @@ public final class MiIoBindingConfiguration {
     public String token;
     public String deviceId;
     public String model;
+    public String communication;
     public int refreshInterval;
     public int timeout;
     public String cloudServer;
index da843cd87e25bf28fc4126eda910c7048c69db54..13c7668b4a4937b54dd8622b763eeb565d8b0006 100644 (file)
@@ -61,6 +61,7 @@ public final class MiIoBindingConstants {
 
     public static final String CHANNEL_CONTROL = "actions#control";
     public static final String CHANNEL_COMMAND = "actions#commands";
+    public static final String CHANNEL_RPC = "actions#rpc";
     public static final String CHANNEL_VACUUM = "actions#vacuum";
     public static final String CHANNEL_FAN_CONTROL = "actions#fan";
     public static final String CHANNEL_TESTCOMMANDS = "actions#testcommands";
index 78099746323b4ca6cfe4229232baf2dbb0047501..b08b0cf23d86679108b80422f63161369118ec6f 100644 (file)
@@ -79,14 +79,14 @@ public class MiIoHandlerFactory extends BaseThingHandlerFactory {
     protected @Nullable ThingHandler createHandler(Thing thing) {
         ThingTypeUID thingTypeUID = thing.getThingTypeUID();
         if (thingTypeUID.equals(THING_TYPE_MIIO)) {
-            return new MiIoGenericHandler(thing, miIoDatabaseWatchService);
+            return new MiIoGenericHandler(thing, miIoDatabaseWatchService, cloudConnector);
         }
         if (thingTypeUID.equals(THING_TYPE_BASIC)) {
-            return new MiIoBasicHandler(thing, miIoDatabaseWatchService, channelTypeRegistry);
+            return new MiIoBasicHandler(thing, miIoDatabaseWatchService, cloudConnector, channelTypeRegistry);
         }
         if (thingTypeUID.equals(THING_TYPE_VACUUM)) {
             return new MiIoVacuumHandler(thing, miIoDatabaseWatchService, cloudConnector, channelTypeRegistry);
         }
-        return new MiIoUnsupportedHandler(thing, miIoDatabaseWatchService);
+        return new MiIoUnsupportedHandler(thing, miIoDatabaseWatchService, cloudConnector);
     }
 }
index 47aa56fb506ad888588f48b281c377c2c8b2b733..b7463ed68150dc8feb8d9d46b810a9bae6060273 100644 (file)
@@ -31,6 +31,7 @@ public class MiIoSendCommand {
     private final MiIoCommand command;
     private final JsonObject commandJson;
     private @Nullable JsonObject response;
+    private String cloudServer = "";
 
     public void setResponse(JsonObject response) {
         this.response = response;
@@ -42,6 +43,13 @@ public class MiIoSendCommand {
         this.commandJson = fullCommand;
     }
 
+    public MiIoSendCommand(int id, MiIoCommand command, JsonObject fullCommand, String cloudServer) {
+        this.id = id;
+        this.command = command;
+        this.commandJson = fullCommand;
+        this.cloudServer = cloudServer;
+    }
+
     public int getId() {
         return id;
     }
@@ -86,4 +94,12 @@ public class MiIoSendCommand {
         }
         return new JsonObject();
     }
+
+    public String getCloudServer() {
+        return cloudServer;
+    }
+
+    public void setCloudServer(String cloudServer) {
+        this.cloudServer = cloudServer;
+    }
 }
index e3660dada0e203b8640c6b279da3b8f57dff550a..d04d9734d7f881a927f7dac4bcf3da019d0d992a 100644 (file)
@@ -21,6 +21,7 @@ import java.util.concurrent.TimeUnit;
 import org.eclipse.jdt.annotation.NonNullByDefault;
 import org.eclipse.jdt.annotation.Nullable;
 import org.eclipse.jetty.client.HttpClient;
+import org.openhab.binding.miio.internal.MiIoSendCommand;
 import org.openhab.core.cache.ExpiringCache;
 import org.openhab.core.io.net.http.HttpClientFactory;
 import org.openhab.core.io.net.http.HttpUtil;
@@ -115,13 +116,21 @@ public class CloudConnector {
         return false;
     }
 
+    public String sendRPCCommand(String device, String country, MiIoSendCommand command) throws MiCloudException {
+        final @Nullable MiCloudConnector cl = this.cloudConnector;
+        if (cl == null || !isConnected()) {
+            throw new MiCloudException("Cannot execute request. Cloud service not available");
+        }
+        return cl.sendRPCCommand(device, country.trim().toLowerCase(), command.getCommandString());
+    }
+
     public @Nullable RawType getMap(String mapId, String country) throws MiCloudException {
         logger.debug("Getting vacuum map {} from Xiaomi cloud server: '{}'", mapId, country);
         String mapCountry;
         String mapUrl = "";
         final @Nullable MiCloudConnector cl = this.cloudConnector;
         if (cl == null || !isConnected()) {
-            throw new MiCloudException("Cannot execute request. Cloudservice not available");
+            throw new MiCloudException("Cannot execute request. Cloud service not available");
         }
         if (country.isEmpty()) {
             logger.debug("Server not defined in thing. Trying servers: {}", this.country);
index 085542f11ae4314b3409a7c80481f53359f3a0f7..5077294b097f7e7ba1fa33b8bb2b440f6c0f5e3a 100644 (file)
@@ -176,10 +176,27 @@ public class MiCloudConnector {
     }
 
     public String getDeviceStatus(String device, String country) throws MiCloudException {
-        String url = getApiUrl(country) + "/home/device_list";
-        Map<String, String> map = new HashMap<String, String>();
-        map.put("data", "{\"dids\":[\"" + device + "\"]}");
-        final String response = request(url, map);
+        final String response = request("/home/device_list", country, "{\"dids\":[\"" + device + "\"]}");
+        logger.debug("response: {}", response);
+        return response;
+    }
+
+    public String sendRPCCommand(String device, String country, String command) throws MiCloudException {
+        if (device.length() != 8) {
+            logger.debug("Device ID ('{}') incorrect or missing. Command not send: {}", device, command);
+        }
+        if (country.length() > 3 || country.length() < 2) {
+            logger.debug("Country ('{}') incorrect or missing. Command not send: {}", device, command);
+        }
+        String id = "";
+        try {
+            id = String.valueOf(Long.parseUnsignedLong(device, 16));
+        } catch (NumberFormatException e) {
+            String err = "Could not parse device ID ('" + device.toString() + "')";
+            logger.debug("{}", err);
+            throw new MiCloudException(err, e);
+        }
+        final String response = request("/home/rpc/" + id, country, command);
         logger.debug("response: {}", response);
         return response;
     }
@@ -211,12 +228,9 @@ public class MiCloudConnector {
     }
 
     public String getDeviceString(String country) {
-        String url = getApiUrl(country) + "/home/device_list";
-        Map<String, String> map = new HashMap<String, String>();
-        map.put("data", "{\"getVirtualModel\":false,\"getHuamiDevices\":0}");
         String resp;
         try {
-            resp = request(url, map);
+            resp = request("/home/device_list", country, "{\"getVirtualModel\":false,\"getHuamiDevices\":0}");
             logger.trace("Get devices response: {}", resp);
             if (resp.length() > 2) {
                 CloudUtil.saveDeviceInfoFile(resp, country, logger);
@@ -228,8 +242,15 @@ public class MiCloudConnector {
         return "";
     }
 
+    public String request(String urlPart, String country, String params) throws MiCloudException {
+        Map<String, String> map = new HashMap<String, String>();
+        map.put("data", params);
+        return request(urlPart, country, map);
+    }
+
     public String request(String urlPart, String country, Map<String, String> params) throws MiCloudException {
-        String url = getApiUrl(country) + urlPart;
+        String url = urlPart.trim();
+        url = getApiUrl(country) + (url.startsWith("/app") ? url.substring(4) : url);
         String response = request(url, params);
         logger.debug("Request to {} server {}. Response: {}", country, urlPart, response);
         return response;
@@ -276,7 +297,8 @@ public class MiCloudConnector {
 
             logger.trace("fieldcontent: {}", fields.toString());
             final ContentResponse response = request.send();
-            if (response.getStatus() == HttpStatus.FORBIDDEN_403) {
+            if (response.getStatus() >= HttpStatus.BAD_REQUEST_400
+                    && response.getStatus() < HttpStatus.INTERNAL_SERVER_ERROR_500) {
                 this.serviceToken = "";
             }
             return response.getContentAsString();
index 62298de92ab4c05654b093dcd72d2221be4d4290..3d2f436a89665f42a69a349e16311e33a61e7e15 100644 (file)
@@ -19,7 +19,9 @@ import java.time.LocalDateTime;
 import java.util.HashMap;
 import java.util.Map;
 import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ScheduledExecutorService;
 import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.ScheduledThreadPoolExecutor;
 import java.util.concurrent.TimeUnit;
 
 import org.eclipse.jdt.annotation.NonNullByDefault;
@@ -35,8 +37,10 @@ import org.openhab.binding.miio.internal.MiIoMessageListener;
 import org.openhab.binding.miio.internal.MiIoSendCommand;
 import org.openhab.binding.miio.internal.Utils;
 import org.openhab.binding.miio.internal.basic.MiIoDatabaseWatchService;
+import org.openhab.binding.miio.internal.cloud.CloudConnector;
 import org.openhab.binding.miio.internal.transport.MiIoAsyncCommunication;
 import org.openhab.core.cache.ExpiringCache;
+import org.openhab.core.common.NamedThreadFactory;
 import org.openhab.core.config.core.Configuration;
 import org.openhab.core.library.types.DecimalType;
 import org.openhab.core.library.types.StringType;
@@ -67,6 +71,7 @@ public abstract class MiIoAbstractHandler extends BaseThingHandler implements Mi
     protected static final int MAX_QUEUE = 5;
     protected static final Gson GSON = new GsonBuilder().create();
 
+    protected ScheduledExecutorService miIoScheduler = scheduler;
     protected @Nullable ScheduledFuture<?> pollingJob;
     protected MiIoDevices miDevice = MiIoDevices.UNKNOWN;
     protected boolean isIdentified;
@@ -76,6 +81,8 @@ public abstract class MiIoAbstractHandler extends BaseThingHandler implements Mi
 
     protected @Nullable MiIoBindingConfiguration configuration;
     protected @Nullable MiIoAsyncCommunication miioCom;
+    protected CloudConnector cloudConnector;
+    protected String cloudServer = "";
     protected int lastId;
 
     protected Map<Integer, String> cmds = new ConcurrentHashMap<>();
@@ -93,18 +100,39 @@ public abstract class MiIoAbstractHandler extends BaseThingHandler implements Mi
     private final Logger logger = LoggerFactory.getLogger(MiIoAbstractHandler.class);
     protected MiIoDatabaseWatchService miIoDatabaseWatchService;
 
-    public MiIoAbstractHandler(Thing thing, MiIoDatabaseWatchService miIoDatabaseWatchService) {
+    public MiIoAbstractHandler(Thing thing, MiIoDatabaseWatchService miIoDatabaseWatchService,
+            CloudConnector cloudConnector) {
         super(thing);
         this.miIoDatabaseWatchService = miIoDatabaseWatchService;
+        this.cloudConnector = cloudConnector;
     }
 
     @Override
     public abstract void handleCommand(ChannelUID channelUID, Command command);
 
+    protected boolean handleCommandsChannels(ChannelUID channelUID, Command command) {
+        if (channelUID.getId().equals(CHANNEL_COMMAND)) {
+            cmds.put(sendCommand(command.toString(), ""), command.toString());
+            return true;
+        }
+        if (channelUID.getId().equals(CHANNEL_RPC)) {
+            cmds.put(sendCommand(command.toString(), cloudServer), command.toString());
+            return true;
+        }
+        return false;
+    }
+
     @Override
     public void initialize() {
         logger.debug("Initializing Mi IO device handler '{}' with thingType {}", getThing().getUID(),
                 getThing().getThingTypeUID());
+
+        ScheduledThreadPoolExecutor miIoScheduler = new ScheduledThreadPoolExecutor(3,
+                new NamedThreadFactory(getThing().getUID().getAsString(), true));
+        miIoScheduler.setExecuteExistingDelayedTasksAfterShutdownPolicy(false);
+        miIoScheduler.setRemoveOnCancelPolicy(true);
+        this.miIoScheduler = miIoScheduler;
+
         final MiIoBindingConfiguration configuration = getConfigAs(MiIoBindingConfiguration.class);
         this.configuration = configuration;
         if (configuration.host == null || configuration.host.isEmpty()) {
@@ -116,11 +144,12 @@ public abstract class MiIoAbstractHandler extends BaseThingHandler implements Mi
             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Token required. Configure token");
             return;
         }
+        cloudServer = (configuration.cloudServer != null) ? configuration.cloudServer : "";
         isIdentified = false;
-        scheduler.schedule(this::initializeData, 1, TimeUnit.SECONDS);
+        miIoScheduler.schedule(this::initializeData, 1, TimeUnit.SECONDS);
         int pollingPeriod = configuration.refreshInterval;
         if (pollingPeriod > 0) {
-            pollingJob = scheduler.scheduleWithFixedDelay(() -> {
+            pollingJob = miIoScheduler.scheduleWithFixedDelay(() -> {
                 try {
                     updateData();
                 } catch (Exception e) {
@@ -130,7 +159,7 @@ public abstract class MiIoAbstractHandler extends BaseThingHandler implements Mi
             logger.debug("Polling job scheduled to run every {} sec. for '{}'", pollingPeriod, getThing().getUID());
         } else {
             logger.debug("Polling job disabled. for '{}'", getThing().getUID());
-            scheduler.schedule(this::updateData, 10, TimeUnit.SECONDS);
+            miIoScheduler.schedule(this::updateData, 10, TimeUnit.SECONDS);
         }
         updateStatus(ThingStatus.OFFLINE);
     }
@@ -166,6 +195,7 @@ public abstract class MiIoAbstractHandler extends BaseThingHandler implements Mi
     @Override
     public void dispose() {
         logger.debug("Disposing Xiaomi Mi IO handler '{}'", getThing().getUID());
+        miIoScheduler.shutdown();
         final ScheduledFuture<?> pollingJob = this.pollingJob;
         if (pollingJob != null) {
             pollingJob.cancel(true);
@@ -178,6 +208,7 @@ public abstract class MiIoAbstractHandler extends BaseThingHandler implements Mi
             miioCom.close();
             this.miioCom = null;
         }
+        miIoScheduler.shutdownNow();
     }
 
     protected int sendCommand(MiIoCommand command) {
@@ -187,7 +218,7 @@ public abstract class MiIoAbstractHandler extends BaseThingHandler implements Mi
     protected int sendCommand(MiIoCommand command, String params) {
         try {
             final MiIoAsyncCommunication connection = getConnection();
-            return (connection != null) ? connection.queueCommand(command, params) : 0;
+            return (connection != null) ? connection.queueCommand(command, params, getCloudServer()) : 0;
         } catch (MiIoCryptoException | IOException e) {
             logger.debug("Command {} for {} failed (type: {}): {}", command.toString(), getThing().getUID(),
                     getThing().getThingTypeUID(), e.getLocalizedMessage());
@@ -195,6 +226,10 @@ public abstract class MiIoAbstractHandler extends BaseThingHandler implements Mi
         return 0;
     }
 
+    protected int sendCommand(String commandString) {
+        return sendCommand(commandString, getCloudServer());
+    }
+
     /**
      * This is used to execute arbitrary commands by sending to the commands channel. Command parameters to be added
      * between
@@ -202,9 +237,10 @@ public abstract class MiIoAbstractHandler extends BaseThingHandler implements Mi
      * records)
      *
      * @param commandString command to be executed
+     * @param cloud server to be used or empty string for direct sending to the device
      * @return vacuum response
      */
-    protected int sendCommand(String commandString) {
+    protected int sendCommand(String commandString, String cloudServer) {
         final MiIoAsyncCommunication connection = getConnection();
         try {
             String command = commandString.trim();
@@ -216,13 +252,24 @@ public abstract class MiIoAbstractHandler extends BaseThingHandler implements Mi
                 param = command.substring(loc).trim();
                 command = command.substring(0, loc).trim();
             }
-            return (connection != null) ? connection.queueCommand(command, param) : 0;
+            return (connection != null) ? connection.queueCommand(command, param, cloudServer) : 0;
         } catch (MiIoCryptoException | IOException e) {
             disconnected(e.getMessage());
         }
         return 0;
     }
 
+    String getCloudServer() {
+        // This can be improved in the future with additional / more advanced options like e.g. directFirst which would
+        // use direct communications and in case of failures fall back to cloud communication. For now we keep it
+        // simple and only have the option for cloud or direct.
+        final MiIoBindingConfiguration configuration = this.configuration;
+        if (configuration != null && configuration.communication != null) {
+            return configuration.communication.equals("cloud") ? cloudServer : "";
+        }
+        return "";
+    }
+
     protected boolean skipUpdate() {
         final MiIoAsyncCommunication miioCom = this.miioCom;
         if (!hasConnection() || miioCom == null) {
@@ -232,11 +279,7 @@ public abstract class MiIoAbstractHandler extends BaseThingHandler implements Mi
         if (getThing().getStatusInfo().getStatusDetail().equals(ThingStatusDetail.CONFIGURATION_ERROR)) {
             logger.debug("Skipping periodic update for '{}'. Thing Status {}", getThing().getUID().toString(),
                     getThing().getStatusInfo().getStatusDetail());
-            try {
-                miioCom.queueCommand(MiIoCommand.MIIO_INFO);
-            } catch (MiIoCryptoException | IOException e) {
-                // ignore
-            }
+            sendCommand(MiIoCommand.MIIO_INFO);
             return true;
         }
         if (miioCom.getQueueLength() > MAX_QUEUE) {
@@ -299,24 +342,30 @@ public abstract class MiIoAbstractHandler extends BaseThingHandler implements Mi
         String deviceId = configuration.deviceId;
         try {
             if (deviceId != null && deviceId.length() == 8 && tokenCheckPass(configuration.token)) {
-                logger.debug("Ping Mi device {} at {}", deviceId, configuration.host);
                 final MiIoAsyncCommunication miioCom = new MiIoAsyncCommunication(configuration.host, token,
-                        Utils.hexStringToByteArray(deviceId), lastId, configuration.timeout);
-                Message miIoResponse = miioCom.sendPing(configuration.host);
-                if (miIoResponse != null) {
-                    logger.debug("Ping response from device {} at {}. Time stamp: {}, OH time {}, delta {}",
-                            Utils.getHex(miIoResponse.getDeviceId()), configuration.host, miIoResponse.getTimestamp(),
-                            LocalDateTime.now(), miioCom.getTimeDelta());
+                        Utils.hexStringToByteArray(deviceId), lastId, configuration.timeout, cloudConnector);
+                if (getCloudServer().isBlank()) {
+                    logger.debug("Ping Mi device {} at {}", deviceId, configuration.host);
+                    Message miIoResponse = miioCom.sendPing(configuration.host);
+                    if (miIoResponse != null) {
+                        logger.debug("Ping response from device {} at {}. Time stamp: {}, OH time {}, delta {}",
+                                Utils.getHex(miIoResponse.getDeviceId()), configuration.host,
+                                miIoResponse.getTimestamp(), LocalDateTime.now(), miioCom.getTimeDelta());
+                        miioCom.registerListener(this);
+                        this.miioCom = miioCom;
+                        return miioCom;
+                    } else {
+                        miioCom.close();
+                    }
+                } else {
                     miioCom.registerListener(this);
                     this.miioCom = miioCom;
                     return miioCom;
-                } else {
-                    miioCom.close();
                 }
             } else {
                 logger.debug("No device ID defined. Retrieving Mi device ID");
                 final MiIoAsyncCommunication miioCom = new MiIoAsyncCommunication(configuration.host, token,
-                        new byte[0], lastId, configuration.timeout);
+                        new byte[0], lastId, configuration.timeout, cloudConnector);
                 Message miIoResponse = miioCom.sendPing(configuration.host);
                 if (miIoResponse != null) {
                     logger.debug("Ping response from device {} at {}. Time stamp: {}, OH time {}, delta {}",
@@ -440,7 +489,7 @@ public abstract class MiIoAbstractHandler extends BaseThingHandler implements Mi
             pollingJob.cancel(true);
             this.pollingJob = null;
         }
-        scheduler.schedule(() -> {
+        miIoScheduler.schedule(() -> {
             ThingBuilder thingBuilder = editThing();
             thingBuilder.withLabel(miDevice.getDescription());
             updateThing(thingBuilder.build());
@@ -484,7 +533,11 @@ public abstract class MiIoAbstractHandler extends BaseThingHandler implements Mi
                     break;
             }
             if (cmds.containsKey(response.getId())) {
-                updateState(CHANNEL_COMMAND, new StringType(response.getResponse().toString()));
+                if (response.getCloudServer().isBlank()) {
+                    updateState(CHANNEL_COMMAND, new StringType(response.getResponse().toString()));
+                } else {
+                    updateState(CHANNEL_RPC, new StringType(response.getResponse().toString()));
+                }
                 cmds.remove(response.getId());
             }
         } catch (Exception e) {
index 31b1d6b64ad4d781202d6b50f17675b800e4e039..874051dfae2d1859306922945d54a6b98c1ef5e4 100644 (file)
@@ -30,7 +30,6 @@ import org.eclipse.jdt.annotation.NonNullByDefault;
 import org.eclipse.jdt.annotation.Nullable;
 import org.openhab.binding.miio.internal.MiIoBindingConfiguration;
 import org.openhab.binding.miio.internal.MiIoCommand;
-import org.openhab.binding.miio.internal.MiIoCryptoException;
 import org.openhab.binding.miio.internal.MiIoQuantiyTypes;
 import org.openhab.binding.miio.internal.MiIoSendCommand;
 import org.openhab.binding.miio.internal.Utils;
@@ -42,6 +41,7 @@ import org.openhab.binding.miio.internal.basic.MiIoBasicDevice;
 import org.openhab.binding.miio.internal.basic.MiIoDatabaseWatchService;
 import org.openhab.binding.miio.internal.basic.MiIoDeviceAction;
 import org.openhab.binding.miio.internal.basic.MiIoDeviceActionCondition;
+import org.openhab.binding.miio.internal.cloud.CloudConnector;
 import org.openhab.binding.miio.internal.transport.MiIoAsyncCommunication;
 import org.openhab.core.cache.ExpiringCache;
 import org.openhab.core.library.types.DecimalType;
@@ -85,7 +85,7 @@ public class MiIoBasicHandler extends MiIoAbstractHandler {
     private boolean hasChannelStructure;
 
     private final ExpiringCache<Boolean> updateDataCache = new ExpiringCache<>(CACHE_EXPIRY, () -> {
-        scheduler.schedule(this::updateData, 0, TimeUnit.SECONDS);
+        miIoScheduler.schedule(this::updateData, 0, TimeUnit.SECONDS);
         return true;
     });
 
@@ -97,8 +97,8 @@ public class MiIoBasicHandler extends MiIoAbstractHandler {
     private ChannelTypeRegistry channelTypeRegistry;
 
     public MiIoBasicHandler(Thing thing, MiIoDatabaseWatchService miIoDatabaseWatchService,
-            ChannelTypeRegistry channelTypeRegistry) {
-        super(thing, miIoDatabaseWatchService);
+            CloudConnector cloudConnector, ChannelTypeRegistry channelTypeRegistry) {
+        super(thing, miIoDatabaseWatchService, cloudConnector);
         this.channelTypeRegistry = channelTypeRegistry;
     }
 
@@ -255,7 +255,7 @@ public class MiIoBasicHandler extends MiIoAbstractHandler {
                 }
             }
             updateDataCache.invalidateValue();
-            scheduler.schedule(() -> {
+            miIoScheduler.schedule(() -> {
                 updateData();
             }, 3000, TimeUnit.MILLISECONDS);
         } else {
@@ -294,7 +294,7 @@ public class MiIoBasicHandler extends MiIoAbstractHandler {
             }
             checkChannelStructure();
             if (!isIdentified) {
-                miioCom.queueCommand(MiIoCommand.MIIO_INFO);
+                sendCommand(MiIoCommand.MIIO_INFO);
             }
             final MiIoBasicDevice midevice = miioDevice;
             if (midevice != null) {
@@ -341,14 +341,7 @@ public class MiIoBasicHandler extends MiIoAbstractHandler {
     }
 
     private void sendRefreshProperties(MiIoCommand command, JsonArray getPropString) {
-        try {
-            final MiIoAsyncCommunication miioCom = this.miioCom;
-            if (miioCom != null) {
-                miioCom.queueCommand(command, getPropString.toString());
-            }
-        } catch (MiIoCryptoException | IOException e) {
-            logger.debug("Send refresh failed {}", e.getMessage(), e);
-        }
+        sendCommand(command, getPropString.toString());
     }
 
     /**
index a77af44cf3a7b87be73bfcfa34f766a5223b58ba..3ec9f4e8a2bd23ca60033fb2c43651be8f4cf18f 100644 (file)
  */
 package org.openhab.binding.miio.internal.handler;
 
-import static org.openhab.binding.miio.internal.MiIoBindingConstants.CHANNEL_COMMAND;
-
 import org.eclipse.jdt.annotation.NonNullByDefault;
 import org.openhab.binding.miio.internal.basic.MiIoDatabaseWatchService;
+import org.openhab.binding.miio.internal.cloud.CloudConnector;
 import org.openhab.core.thing.ChannelUID;
 import org.openhab.core.thing.Thing;
 import org.openhab.core.types.Command;
@@ -33,8 +32,9 @@ import org.slf4j.LoggerFactory;
 public class MiIoGenericHandler extends MiIoAbstractHandler {
     private final Logger logger = LoggerFactory.getLogger(MiIoGenericHandler.class);
 
-    public MiIoGenericHandler(Thing thing, MiIoDatabaseWatchService miIoDatabaseWatchService) {
-        super(thing, miIoDatabaseWatchService);
+    public MiIoGenericHandler(Thing thing, MiIoDatabaseWatchService miIoDatabaseWatchService,
+            CloudConnector cloudConnector) {
+        super(thing, miIoDatabaseWatchService, cloudConnector);
     }
 
     @Override
@@ -44,8 +44,8 @@ public class MiIoGenericHandler extends MiIoAbstractHandler {
             updateData();
             return;
         }
-        if (channelUID.getId().equals(CHANNEL_COMMAND)) {
-            cmds.put(sendCommand(command.toString()), command.toString());
+        if (handleCommandsChannels(channelUID, command)) {
+            return;
         }
     }
 
index 9862b92e8e9872ac76d5f2daea4e78bd0d56e6b9..f6b735f4de96cbbc30691a98cb34fd85bc8d4a49 100644 (file)
@@ -40,6 +40,7 @@ import org.openhab.binding.miio.internal.basic.DeviceMapping;
 import org.openhab.binding.miio.internal.basic.MiIoBasicChannel;
 import org.openhab.binding.miio.internal.basic.MiIoBasicDevice;
 import org.openhab.binding.miio.internal.basic.MiIoDatabaseWatchService;
+import org.openhab.binding.miio.internal.cloud.CloudConnector;
 import org.openhab.core.cache.ExpiringCache;
 import org.openhab.core.library.types.OnOffType;
 import org.openhab.core.thing.ChannelUID;
@@ -78,12 +79,13 @@ public class MiIoUnsupportedHandler extends MiIoAbstractHandler {
     private String model = conf.model != null ? conf.model : "";
 
     private final ExpiringCache<Boolean> updateDataCache = new ExpiringCache<>(CACHE_EXPIRY, () -> {
-        scheduler.schedule(this::updateData, 0, TimeUnit.SECONDS);
+        miIoScheduler.schedule(this::updateData, 0, TimeUnit.SECONDS);
         return true;
     });
 
-    public MiIoUnsupportedHandler(Thing thing, MiIoDatabaseWatchService miIoDatabaseWatchService) {
-        super(thing, miIoDatabaseWatchService);
+    public MiIoUnsupportedHandler(Thing thing, MiIoDatabaseWatchService miIoDatabaseWatchService,
+            CloudConnector cloudConnector) {
+        super(thing, miIoDatabaseWatchService, cloudConnector);
     }
 
     @Override
@@ -104,8 +106,8 @@ public class MiIoUnsupportedHandler extends MiIoAbstractHandler {
                 sendCommand("set_power[\"off\"]");
             }
         }
-        if (channelUID.getId().equals(CHANNEL_COMMAND)) {
-            cmds.put(sendCommand(command.toString()), command.toString());
+        if (handleCommandsChannels(channelUID, command)) {
+            return;
         }
         if (channelUID.getId().equals(CHANNEL_TESTCOMMANDS)) {
             executeExperimentalCommands();
index 18d20e9a4d6d2beda07151bc34610fd18ef1c554..0e5bccf610c57c841e405e919b2f1740a1587ef4 100644 (file)
@@ -97,15 +97,13 @@ public class MiIoVacuumHandler extends MiIoAbstractHandler {
     private ExpiringCache<String> map;
     private String lastHistoryId = "";
     private String lastMap = "";
-    private CloudConnector cloudConnector;
     private boolean hasChannelStructure;
     private ConcurrentHashMap<RobotCababilities, Boolean> deviceCapabilities = new ConcurrentHashMap<>();
     private ChannelTypeRegistry channelTypeRegistry;
 
     public MiIoVacuumHandler(Thing thing, MiIoDatabaseWatchService miIoDatabaseWatchService,
             CloudConnector cloudConnector, ChannelTypeRegistry channelTypeRegistry) {
-        super(thing, miIoDatabaseWatchService);
-        this.cloudConnector = cloudConnector;
+        super(thing, miIoDatabaseWatchService, cloudConnector);
         this.channelTypeRegistry = channelTypeRegistry;
         mapChannelUid = new ChannelUID(thing.getUID(), CHANNEL_VACUUM_MAP);
         status = new ExpiringCache<>(CACHE_EXPIRY, () -> {
@@ -180,6 +178,9 @@ public class MiIoVacuumHandler extends MiIoAbstractHandler {
             }
             return;
         }
+        if (handleCommandsChannels(channelUID, command)) {
+            return;
+        }
         if (channelUID.getId().equals(CHANNEL_VACUUM)) {
             if (command instanceof OnOffType) {
                 if (command.equals(OnOffType.ON)) {
@@ -188,7 +189,7 @@ public class MiIoVacuumHandler extends MiIoAbstractHandler {
                     return;
                 } else {
                     sendCommand(MiIoCommand.STOP_VACUUM);
-                    scheduler.schedule(() -> {
+                    miIoScheduler.schedule(() -> {
                         sendCommand(MiIoCommand.CHARGE);
                         forceStatusUpdate();
                     }, 2000, TimeUnit.MILLISECONDS);
@@ -205,7 +206,7 @@ public class MiIoVacuumHandler extends MiIoAbstractHandler {
                 sendCommand(MiIoCommand.PAUSE);
             } else if (command.toString().equals("dock")) {
                 sendCommand(MiIoCommand.STOP_VACUUM);
-                scheduler.schedule(() -> {
+                miIoScheduler.schedule(() -> {
                     sendCommand(MiIoCommand.CHARGE);
                     forceStatusUpdate();
                 }, 2000, TimeUnit.MILLISECONDS);
@@ -243,14 +244,13 @@ public class MiIoVacuumHandler extends MiIoAbstractHandler {
             sendCommand(MiIoCommand.CONSUMABLES_RESET, "[" + command.toString() + "]");
             updateState(CHANNEL_CONSUMABLE_RESET, new StringType("none"));
         }
-        if (channelUID.getId().equals(CHANNEL_COMMAND)) {
-            cmds.put(sendCommand(command.toString()), command.toString());
-        }
     }
 
     private void forceStatusUpdate() {
         status.invalidateValue();
-        status.getValue();
+        miIoScheduler.schedule(() -> {
+            status.getValue();
+        }, 3000, TimeUnit.MILLISECONDS);
     }
 
     private void safeUpdateState(String channelID, @Nullable Integer state) {
@@ -521,13 +521,10 @@ public class MiIoVacuumHandler extends MiIoAbstractHandler {
                     String mapresponse = response.getResult().getAsJsonArray().get(0).getAsString();
                     if (!mapresponse.contentEquals("retry") && !mapresponse.contentEquals(lastMap)) {
                         lastMap = mapresponse;
-                        scheduler.submit(() -> updateState(CHANNEL_VACUUM_MAP, getMap(mapresponse)));
+                        miIoScheduler.submit(() -> updateState(CHANNEL_VACUUM_MAP, getMap(mapresponse)));
                     }
                 }
                 break;
-            case UNKNOWN:
-                updateState(CHANNEL_COMMAND, new StringType(response.getResponse().toString()));
-                break;
             default:
                 break;
         }
index 9fb9ccd32e2fefa2c4fb419b19b19e99fe8b0369..ad32070719ba531b30f5ac2ef662861979e41b80 100644 (file)
@@ -37,6 +37,8 @@ import org.openhab.binding.miio.internal.MiIoCryptoException;
 import org.openhab.binding.miio.internal.MiIoMessageListener;
 import org.openhab.binding.miio.internal.MiIoSendCommand;
 import org.openhab.binding.miio.internal.Utils;
+import org.openhab.binding.miio.internal.cloud.CloudConnector;
+import org.openhab.binding.miio.internal.cloud.MiCloudException;
 import org.openhab.core.thing.ThingStatus;
 import org.openhab.core.thing.ThingStatusDetail;
 import org.slf4j.Logger;
@@ -78,14 +80,17 @@ public class MiIoAsyncCommunication {
     private boolean needPing = true;
     private static final int MAX_ERRORS = 3;
     private static final int MAX_ID = 15000;
+    private final CloudConnector cloudConnector;
 
     private ConcurrentLinkedQueue<MiIoSendCommand> concurrentLinkedQueue = new ConcurrentLinkedQueue<>();
 
-    public MiIoAsyncCommunication(String ip, byte[] token, byte[] did, int id, int timeout) {
+    public MiIoAsyncCommunication(String ip, byte[] token, byte[] did, int id, int timeout,
+            CloudConnector cloudConnector) {
         this.ip = ip;
         this.token = token;
         this.deviceId = did;
         this.timeout = timeout;
+        this.cloudConnector = cloudConnector;
         setId(id);
         parser = new JsonParser();
         startReceiver();
@@ -124,15 +129,16 @@ public class MiIoAsyncCommunication {
         }
     }
 
-    public int queueCommand(MiIoCommand command) throws MiIoCryptoException, IOException {
-        return queueCommand(command, "[]");
+    public int queueCommand(MiIoCommand command, String cloudServer) throws MiIoCryptoException, IOException {
+        return queueCommand(command, "[]", cloudServer);
     }
 
-    public int queueCommand(MiIoCommand command, String params) throws MiIoCryptoException, IOException {
-        return queueCommand(command.getCommand(), params);
+    public int queueCommand(MiIoCommand command, String params, String cloudServer)
+            throws MiIoCryptoException, IOException {
+        return queueCommand(command.getCommand(), params, cloudServer);
     }
 
-    public int queueCommand(String command, String params)
+    public int queueCommand(String command, String params, String cloudServer)
             throws MiIoCryptoException, IOException, JsonSyntaxException {
         try {
             JsonObject fullCommand = new JsonObject();
@@ -143,15 +149,17 @@ public class MiIoAsyncCommunication {
             fullCommand.addProperty("id", cmdId);
             fullCommand.addProperty("method", command);
             fullCommand.add("params", parser.parse(params));
-            MiIoSendCommand sendCmd = new MiIoSendCommand(cmdId, MiIoCommand.getCommand(command), fullCommand);
+            MiIoSendCommand sendCmd = new MiIoSendCommand(cmdId, MiIoCommand.getCommand(command), fullCommand,
+                    cloudServer);
             concurrentLinkedQueue.add(sendCmd);
             if (logger.isDebugEnabled()) {
                 // Obfuscate part of the token to allow sharing of the logfiles
                 String tokenText = Utils.obfuscateToken(Utils.getHex(token));
-                logger.debug("Command added to Queue {} -> {} (Device: {} token: {} Queue: {})", fullCommand.toString(),
-                        ip, Utils.getHex(deviceId), tokenText, concurrentLinkedQueue.size());
+                logger.debug("Command added to Queue {} -> {} (Device: {} token: {} Queue: {}).{}{}",
+                        fullCommand.toString(), ip, Utils.getHex(deviceId), tokenText, concurrentLinkedQueue.size(),
+                        cloudServer.isBlank() ? "" : " Send via cloudserver: ", cloudServer);
             }
-            if (needPing) {
+            if (needPing && cloudServer.isBlank()) {
                 sendPing(ip);
             }
             return cmdId;
@@ -166,7 +174,15 @@ public class MiIoAsyncCommunication {
         String errorMsg = "Unknown Error while sending command";
         String decryptedResponse = "";
         try {
-            decryptedResponse = sendCommand(miIoSendCommand.getCommandString(), token, ip, deviceId);
+            if (miIoSendCommand.getCloudServer().isBlank()) {
+                decryptedResponse = sendCommand(miIoSendCommand.getCommandString(), token, ip, deviceId);
+            } else {
+                decryptedResponse = cloudConnector.sendRPCCommand(Utils.getHex(deviceId),
+                        miIoSendCommand.getCloudServer(), miIoSendCommand);
+                logger.debug("Command {} send via cloudserver {}", miIoSendCommand.getCommandString(),
+                        miIoSendCommand.getCloudServer());
+                updateStatus(ThingStatus.ONLINE, ThingStatusDetail.NONE);
+            }
             // hack due to avoid invalid json errors from some misbehaving device firmwares
             decryptedResponse = decryptedResponse.replace(",,", ",");
             JsonElement response;
@@ -188,6 +204,12 @@ public class MiIoAsyncCommunication {
             logger.warn("Could not parse '{}' <- {} (Device: {}) gave error {}", decryptedResponse,
                     miIoSendCommand.getCommandString(), Utils.getHex(deviceId), e.getMessage());
             errorMsg = "Received message is invalid JSON";
+        } catch (MiCloudException e) {
+            logger.debug("Send command '{}'  -> cloudserver '{}' (Device: {}) gave error {}",
+                    miIoSendCommand.getCommandString(), miIoSendCommand.getCloudServer(), Utils.getHex(deviceId),
+                    e.getMessage());
+            errorMsg = e.getMessage();
+            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
         }
         JsonObject erroResp = new JsonObject();
         erroResp.addProperty("error", errorMsg);
index 8bc3b453637a9fa0413b6734bc258e27c1a048cb..6a099e99813a13f0bf301c6af57d70d820407d21 100644 (file)
@@ -2,7 +2,7 @@
 <config-description:config-descriptions
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:config-description="https://openhab.org/schemas/config-description/v1.0.0"
-       xsi:schemaLocation="https://openhab.org/schemas/config-description/v1.0.0https://openhab.org/schemas/config-description-1.0.0.xsd">
+       xsi:schemaLocation="https://openhab.org/schemas/config-description/v1.0.0 https://openhab.org/schemas/config-description-1.0.0.xsd">
 
        <config-description uri="thing-type:miio:config">
                <parameter name="host" type="text" required="true">
                        <description>Device model string, used to determine the subtype.</description>
                        <advanced>true</advanced>
                </parameter>
+               <parameter name="communication" type="text" required="false">
+                       <default>direct</default>
+                       <label>Communication Method</label>
+                       <description>Determines how the binding communicates with this device</description>
+                       <options>
+                               <option value="direct">Direct (Default)</option>
+                               <option value="cloud">Cloud</option>
+                       </options>
+                       <advanced>true</advanced>
+               </parameter>
                <parameter name="refreshInterval" type="integer" min="0" max="9999" required="false">
                        <label>Refresh Interval</label>
                        <description>Refresh interval for refreshing the data in seconds. (0=disabled)</description>
index 18589aadb1cc1a38ca1b485ccb1a0e43e8b95988..82ec8b00a9baba4f1f608a7b187efa87a55c720a 100644 (file)
@@ -23,6 +23,7 @@
                <label>Actions</label>
                <channels>
                        <channel id="commands" typeId="commands"/>
+                       <channel id="rpc" typeId="rpc"/>
                </channels>
        </channel-group-type>
 
index 0fc1ee99bc93e4fcb2ac3d4a80d43e2e8e9762c7..f7ff791ed6eaeffa3c69790e54644042fb3cdc64 100644 (file)
                <item-type>String</item-type>
                <label>Execute Command</label>
        </channel-type>
+       <channel-type id="rpc" advanced="true">
+               <item-type>String</item-type>
+               <label>Execute RPC (cloud) Command</label>
+       </channel-type>
        <channel-type id="power">
                <item-type>Switch</item-type>
                <label>Power On/Off</label>
index 34b51f1ba7d5e308f6c81a26312c51929c1058ed..d7c70988d854974031ad6ce507dbe558d5a799f8 100644 (file)
@@ -24,6 +24,7 @@
                <channels>
                        <channel id="power" typeId="power"/>
                        <channel id="commands" typeId="commands"/>
+                       <channel id="rpc" typeId="rpc"/>
                        <channel id="testcommands" typeId="testcommands"/>
                </channels>
        </channel-group-type>
index 7faebc6bf5e9bcd6979484232fba0e05237e33c4..7e813654f04bfdbc4a92f29be7dc64cd3e0b0af7 100644 (file)
@@ -29,6 +29,7 @@
                <channels>
                        <channel id="control" typeId="control"/>
                        <channel id="commands" typeId="commands"/>
+                       <channel id="rpc" typeId="rpc"/>
                        <channel id="fan" typeId="fan"/>
                        <channel id="vacuum" typeId="vacuum"/>
                        <channel id="segment" typeId="segment"/>