[miio] Cloud Communication for devices (#8981)

* [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>
This commit is contained in:
Marcel
2020-11-28 20:03:12 -08:00
committed by GitHub
co-authored by Connor Petty
parent ab7ac79fab
commit ad202edefa
19 changed files with 255 additions and 112 deletions
@@ -66,21 +66,24 @@ The configuration for model is automatically retrieved from the device in normal
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. 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 | | Parameter | Type | Required | Description |
|-----------------|---------|----------|-------------------------------------------------------------------| |-----------------|---------|----------|---------------------------------------------------------------------|
| host | text | true | Device IP address | | host | text | true | Device IP address |
| token | text | true | Token for communication (in Hex) | | token | text | true | Token for communication (in Hex) |
| deviceId | text | true | Device ID number 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 | | model | text | false | Device model string, used to determine the subtype |
| refreshInterval | integer | false | Refresh interval for refreshing the data in seconds. (0=disabled) | | refreshInterval | integer | false | Refresh interval for refreshing the data in seconds. (0=disabled) |
| timeout | integer | false | Timeout time in milliseconds | | 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 ### 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: 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 # 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?_ _Your device is on a different subnet?_
This is in most cases not working. This is in most cases not working.
Firmware of the device don't accept commands coming from other subnets. 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_ _Cloud connectivity is not working_
The most common problem is a wrong userId/password. Try to fix your userId/password. 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#bssid | String | Network BSSID |
| network#rssi | Number | Network RSSI | | network#rssi | Number | Network RSSI |
| network#life | Number | Network Life | | 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. 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.
+10 -5
View File
@@ -66,21 +66,24 @@ The configuration for model is automatically retrieved from the device in normal
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. 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 | | Parameter | Type | Required | Description |
|-----------------|---------|----------|-------------------------------------------------------------------| |-----------------|---------|----------|---------------------------------------------------------------------|
| host | text | true | Device IP address | | host | text | true | Device IP address |
| token | text | true | Token for communication (in Hex) | | token | text | true | Token for communication (in Hex) |
| deviceId | text | true | Device ID number 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 | | model | text | false | Device model string, used to determine the subtype |
| refreshInterval | integer | false | Refresh interval for refreshing the data in seconds. (0=disabled) | | refreshInterval | integer | false | Refresh interval for refreshing the data in seconds. (0=disabled) |
| timeout | integer | false | Timeout time in milliseconds | | 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 ### 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: 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 # 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?_ _Your device is on a different subnet?_
This is in most cases not working. This is in most cases not working.
Firmware of the device don't accept commands coming from other subnets. 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_ _Cloud connectivity is not working_
The most common problem is a wrong userId/password. Try to fix your userId/password. 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#bssid | String | Network BSSID |
| network#rssi | Number | Network RSSI | | network#rssi | Number | Network RSSI |
| network#life | Number | Network Life | | 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. 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.
@@ -24,6 +24,7 @@ public final class MiIoBindingConfiguration {
public String token; public String token;
public String deviceId; public String deviceId;
public String model; public String model;
public String communication;
public int refreshInterval; public int refreshInterval;
public int timeout; public int timeout;
public String cloudServer; public String cloudServer;
@@ -61,6 +61,7 @@ public final class MiIoBindingConstants {
public static final String CHANNEL_CONTROL = "actions#control"; public static final String CHANNEL_CONTROL = "actions#control";
public static final String CHANNEL_COMMAND = "actions#commands"; 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_VACUUM = "actions#vacuum";
public static final String CHANNEL_FAN_CONTROL = "actions#fan"; public static final String CHANNEL_FAN_CONTROL = "actions#fan";
public static final String CHANNEL_TESTCOMMANDS = "actions#testcommands"; public static final String CHANNEL_TESTCOMMANDS = "actions#testcommands";
@@ -79,14 +79,14 @@ public class MiIoHandlerFactory extends BaseThingHandlerFactory {
protected @Nullable ThingHandler createHandler(Thing thing) { protected @Nullable ThingHandler createHandler(Thing thing) {
ThingTypeUID thingTypeUID = thing.getThingTypeUID(); ThingTypeUID thingTypeUID = thing.getThingTypeUID();
if (thingTypeUID.equals(THING_TYPE_MIIO)) { if (thingTypeUID.equals(THING_TYPE_MIIO)) {
return new MiIoGenericHandler(thing, miIoDatabaseWatchService); return new MiIoGenericHandler(thing, miIoDatabaseWatchService, cloudConnector);
} }
if (thingTypeUID.equals(THING_TYPE_BASIC)) { 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)) { if (thingTypeUID.equals(THING_TYPE_VACUUM)) {
return new MiIoVacuumHandler(thing, miIoDatabaseWatchService, cloudConnector, channelTypeRegistry); return new MiIoVacuumHandler(thing, miIoDatabaseWatchService, cloudConnector, channelTypeRegistry);
} }
return new MiIoUnsupportedHandler(thing, miIoDatabaseWatchService); return new MiIoUnsupportedHandler(thing, miIoDatabaseWatchService, cloudConnector);
} }
} }
@@ -31,6 +31,7 @@ public class MiIoSendCommand {
private final MiIoCommand command; private final MiIoCommand command;
private final JsonObject commandJson; private final JsonObject commandJson;
private @Nullable JsonObject response; private @Nullable JsonObject response;
private String cloudServer = "";
public void setResponse(JsonObject response) { public void setResponse(JsonObject response) {
this.response = response; this.response = response;
@@ -42,6 +43,13 @@ public class MiIoSendCommand {
this.commandJson = fullCommand; 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() { public int getId() {
return id; return id;
} }
@@ -86,4 +94,12 @@ public class MiIoSendCommand {
} }
return new JsonObject(); return new JsonObject();
} }
public String getCloudServer() {
return cloudServer;
}
public void setCloudServer(String cloudServer) {
this.cloudServer = cloudServer;
}
} }
@@ -21,6 +21,7 @@ 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.eclipse.jetty.client.HttpClient; import org.eclipse.jetty.client.HttpClient;
import org.openhab.binding.miio.internal.MiIoSendCommand;
import org.openhab.core.cache.ExpiringCache; import org.openhab.core.cache.ExpiringCache;
import org.openhab.core.io.net.http.HttpClientFactory; import org.openhab.core.io.net.http.HttpClientFactory;
import org.openhab.core.io.net.http.HttpUtil; import org.openhab.core.io.net.http.HttpUtil;
@@ -115,13 +116,21 @@ public class CloudConnector {
return false; 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 { public @Nullable RawType getMap(String mapId, String country) throws MiCloudException {
logger.debug("Getting vacuum map {} from Xiaomi cloud server: '{}'", mapId, country); logger.debug("Getting vacuum map {} from Xiaomi cloud server: '{}'", mapId, country);
String mapCountry; String mapCountry;
String mapUrl = ""; String mapUrl = "";
final @Nullable MiCloudConnector cl = this.cloudConnector; final @Nullable MiCloudConnector cl = this.cloudConnector;
if (cl == null || !isConnected()) { 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()) { if (country.isEmpty()) {
logger.debug("Server not defined in thing. Trying servers: {}", this.country); logger.debug("Server not defined in thing. Trying servers: {}", this.country);
@@ -176,10 +176,27 @@ public class MiCloudConnector {
} }
public String getDeviceStatus(String device, String country) throws MiCloudException { public String getDeviceStatus(String device, String country) throws MiCloudException {
String url = getApiUrl(country) + "/home/device_list"; final String response = request("/home/device_list", country, "{\"dids\":[\"" + device + "\"]}");
Map<String, String> map = new HashMap<String, String>(); logger.debug("response: {}", response);
map.put("data", "{\"dids\":[\"" + device + "\"]}"); return response;
final String response = request(url, map); }
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); logger.debug("response: {}", response);
return response; return response;
} }
@@ -211,12 +228,9 @@ public class MiCloudConnector {
} }
public String getDeviceString(String country) { 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; String resp;
try { try {
resp = request(url, map); resp = request("/home/device_list", country, "{\"getVirtualModel\":false,\"getHuamiDevices\":0}");
logger.trace("Get devices response: {}", resp); logger.trace("Get devices response: {}", resp);
if (resp.length() > 2) { if (resp.length() > 2) {
CloudUtil.saveDeviceInfoFile(resp, country, logger); CloudUtil.saveDeviceInfoFile(resp, country, logger);
@@ -228,8 +242,15 @@ public class MiCloudConnector {
return ""; 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 { 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); String response = request(url, params);
logger.debug("Request to {} server {}. Response: {}", country, urlPart, response); logger.debug("Request to {} server {}. Response: {}", country, urlPart, response);
return response; return response;
@@ -276,7 +297,8 @@ public class MiCloudConnector {
logger.trace("fieldcontent: {}", fields.toString()); logger.trace("fieldcontent: {}", fields.toString());
final ContentResponse response = request.send(); 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 = ""; this.serviceToken = "";
} }
return response.getContentAsString(); return response.getContentAsString();
@@ -19,7 +19,9 @@ import java.time.LocalDateTime;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture; import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import org.eclipse.jdt.annotation.NonNullByDefault; 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.MiIoSendCommand;
import org.openhab.binding.miio.internal.Utils; import org.openhab.binding.miio.internal.Utils;
import org.openhab.binding.miio.internal.basic.MiIoDatabaseWatchService; 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.binding.miio.internal.transport.MiIoAsyncCommunication;
import org.openhab.core.cache.ExpiringCache; import org.openhab.core.cache.ExpiringCache;
import org.openhab.core.common.NamedThreadFactory;
import org.openhab.core.config.core.Configuration; import org.openhab.core.config.core.Configuration;
import org.openhab.core.library.types.DecimalType; import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.StringType; 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 int MAX_QUEUE = 5;
protected static final Gson GSON = new GsonBuilder().create(); protected static final Gson GSON = new GsonBuilder().create();
protected ScheduledExecutorService miIoScheduler = scheduler;
protected @Nullable ScheduledFuture<?> pollingJob; protected @Nullable ScheduledFuture<?> pollingJob;
protected MiIoDevices miDevice = MiIoDevices.UNKNOWN; protected MiIoDevices miDevice = MiIoDevices.UNKNOWN;
protected boolean isIdentified; protected boolean isIdentified;
@@ -76,6 +81,8 @@ public abstract class MiIoAbstractHandler extends BaseThingHandler implements Mi
protected @Nullable MiIoBindingConfiguration configuration; protected @Nullable MiIoBindingConfiguration configuration;
protected @Nullable MiIoAsyncCommunication miioCom; protected @Nullable MiIoAsyncCommunication miioCom;
protected CloudConnector cloudConnector;
protected String cloudServer = "";
protected int lastId; protected int lastId;
protected Map<Integer, String> cmds = new ConcurrentHashMap<>(); 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); private final Logger logger = LoggerFactory.getLogger(MiIoAbstractHandler.class);
protected MiIoDatabaseWatchService miIoDatabaseWatchService; protected MiIoDatabaseWatchService miIoDatabaseWatchService;
public MiIoAbstractHandler(Thing thing, MiIoDatabaseWatchService miIoDatabaseWatchService) { public MiIoAbstractHandler(Thing thing, MiIoDatabaseWatchService miIoDatabaseWatchService,
CloudConnector cloudConnector) {
super(thing); super(thing);
this.miIoDatabaseWatchService = miIoDatabaseWatchService; this.miIoDatabaseWatchService = miIoDatabaseWatchService;
this.cloudConnector = cloudConnector;
} }
@Override @Override
public abstract void handleCommand(ChannelUID channelUID, Command command); 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 @Override
public void initialize() { public void initialize() {
logger.debug("Initializing Mi IO device handler '{}' with thingType {}", getThing().getUID(), logger.debug("Initializing Mi IO device handler '{}' with thingType {}", getThing().getUID(),
getThing().getThingTypeUID()); 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); final MiIoBindingConfiguration configuration = getConfigAs(MiIoBindingConfiguration.class);
this.configuration = configuration; this.configuration = configuration;
if (configuration.host == null || configuration.host.isEmpty()) { 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"); updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Token required. Configure token");
return; return;
} }
cloudServer = (configuration.cloudServer != null) ? configuration.cloudServer : "";
isIdentified = false; isIdentified = false;
scheduler.schedule(this::initializeData, 1, TimeUnit.SECONDS); miIoScheduler.schedule(this::initializeData, 1, TimeUnit.SECONDS);
int pollingPeriod = configuration.refreshInterval; int pollingPeriod = configuration.refreshInterval;
if (pollingPeriod > 0) { if (pollingPeriod > 0) {
pollingJob = scheduler.scheduleWithFixedDelay(() -> { pollingJob = miIoScheduler.scheduleWithFixedDelay(() -> {
try { try {
updateData(); updateData();
} catch (Exception e) { } 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()); logger.debug("Polling job scheduled to run every {} sec. for '{}'", pollingPeriod, getThing().getUID());
} else { } else {
logger.debug("Polling job disabled. for '{}'", getThing().getUID()); 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); updateStatus(ThingStatus.OFFLINE);
} }
@@ -166,6 +195,7 @@ public abstract class MiIoAbstractHandler extends BaseThingHandler implements Mi
@Override @Override
public void dispose() { public void dispose() {
logger.debug("Disposing Xiaomi Mi IO handler '{}'", getThing().getUID()); logger.debug("Disposing Xiaomi Mi IO handler '{}'", getThing().getUID());
miIoScheduler.shutdown();
final ScheduledFuture<?> pollingJob = this.pollingJob; final ScheduledFuture<?> pollingJob = this.pollingJob;
if (pollingJob != null) { if (pollingJob != null) {
pollingJob.cancel(true); pollingJob.cancel(true);
@@ -178,6 +208,7 @@ public abstract class MiIoAbstractHandler extends BaseThingHandler implements Mi
miioCom.close(); miioCom.close();
this.miioCom = null; this.miioCom = null;
} }
miIoScheduler.shutdownNow();
} }
protected int sendCommand(MiIoCommand command) { protected int sendCommand(MiIoCommand command) {
@@ -187,7 +218,7 @@ public abstract class MiIoAbstractHandler extends BaseThingHandler implements Mi
protected int sendCommand(MiIoCommand command, String params) { protected int sendCommand(MiIoCommand command, String params) {
try { try {
final MiIoAsyncCommunication connection = getConnection(); 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) { } catch (MiIoCryptoException | IOException e) {
logger.debug("Command {} for {} failed (type: {}): {}", command.toString(), getThing().getUID(), logger.debug("Command {} for {} failed (type: {}): {}", command.toString(), getThing().getUID(),
getThing().getThingTypeUID(), e.getLocalizedMessage()); getThing().getThingTypeUID(), e.getLocalizedMessage());
@@ -195,6 +226,10 @@ public abstract class MiIoAbstractHandler extends BaseThingHandler implements Mi
return 0; 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 * This is used to execute arbitrary commands by sending to the commands channel. Command parameters to be added
* between * between
@@ -202,9 +237,10 @@ public abstract class MiIoAbstractHandler extends BaseThingHandler implements Mi
* records) * records)
* *
* @param commandString command to be executed * @param commandString command to be executed
* @param cloud server to be used or empty string for direct sending to the device
* @return vacuum response * @return vacuum response
*/ */
protected int sendCommand(String commandString) { protected int sendCommand(String commandString, String cloudServer) {
final MiIoAsyncCommunication connection = getConnection(); final MiIoAsyncCommunication connection = getConnection();
try { try {
String command = commandString.trim(); String command = commandString.trim();
@@ -216,13 +252,24 @@ public abstract class MiIoAbstractHandler extends BaseThingHandler implements Mi
param = command.substring(loc).trim(); param = command.substring(loc).trim();
command = command.substring(0, 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) { } catch (MiIoCryptoException | IOException e) {
disconnected(e.getMessage()); disconnected(e.getMessage());
} }
return 0; 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() { protected boolean skipUpdate() {
final MiIoAsyncCommunication miioCom = this.miioCom; final MiIoAsyncCommunication miioCom = this.miioCom;
if (!hasConnection() || miioCom == null) { if (!hasConnection() || miioCom == null) {
@@ -232,11 +279,7 @@ public abstract class MiIoAbstractHandler extends BaseThingHandler implements Mi
if (getThing().getStatusInfo().getStatusDetail().equals(ThingStatusDetail.CONFIGURATION_ERROR)) { if (getThing().getStatusInfo().getStatusDetail().equals(ThingStatusDetail.CONFIGURATION_ERROR)) {
logger.debug("Skipping periodic update for '{}'. Thing Status {}", getThing().getUID().toString(), logger.debug("Skipping periodic update for '{}'. Thing Status {}", getThing().getUID().toString(),
getThing().getStatusInfo().getStatusDetail()); getThing().getStatusInfo().getStatusDetail());
try { sendCommand(MiIoCommand.MIIO_INFO);
miioCom.queueCommand(MiIoCommand.MIIO_INFO);
} catch (MiIoCryptoException | IOException e) {
// ignore
}
return true; return true;
} }
if (miioCom.getQueueLength() > MAX_QUEUE) { if (miioCom.getQueueLength() > MAX_QUEUE) {
@@ -299,24 +342,30 @@ public abstract class MiIoAbstractHandler extends BaseThingHandler implements Mi
String deviceId = configuration.deviceId; String deviceId = configuration.deviceId;
try { try {
if (deviceId != null && deviceId.length() == 8 && tokenCheckPass(configuration.token)) { 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, final MiIoAsyncCommunication miioCom = new MiIoAsyncCommunication(configuration.host, token,
Utils.hexStringToByteArray(deviceId), lastId, configuration.timeout); 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); Message miIoResponse = miioCom.sendPing(configuration.host);
if (miIoResponse != null) { if (miIoResponse != null) {
logger.debug("Ping response from device {} at {}. Time stamp: {}, OH time {}, delta {}", logger.debug("Ping response from device {} at {}. Time stamp: {}, OH time {}, delta {}",
Utils.getHex(miIoResponse.getDeviceId()), configuration.host, miIoResponse.getTimestamp(), Utils.getHex(miIoResponse.getDeviceId()), configuration.host,
LocalDateTime.now(), miioCom.getTimeDelta()); miIoResponse.getTimestamp(), LocalDateTime.now(), miioCom.getTimeDelta());
miioCom.registerListener(this); miioCom.registerListener(this);
this.miioCom = miioCom; this.miioCom = miioCom;
return miioCom; return miioCom;
} else { } else {
miioCom.close(); miioCom.close();
} }
} else {
miioCom.registerListener(this);
this.miioCom = miioCom;
return miioCom;
}
} else { } else {
logger.debug("No device ID defined. Retrieving Mi device ID"); logger.debug("No device ID defined. Retrieving Mi device ID");
final MiIoAsyncCommunication miioCom = new MiIoAsyncCommunication(configuration.host, token, 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); Message miIoResponse = miioCom.sendPing(configuration.host);
if (miIoResponse != null) { if (miIoResponse != null) {
logger.debug("Ping response from device {} at {}. Time stamp: {}, OH time {}, delta {}", 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); pollingJob.cancel(true);
this.pollingJob = null; this.pollingJob = null;
} }
scheduler.schedule(() -> { miIoScheduler.schedule(() -> {
ThingBuilder thingBuilder = editThing(); ThingBuilder thingBuilder = editThing();
thingBuilder.withLabel(miDevice.getDescription()); thingBuilder.withLabel(miDevice.getDescription());
updateThing(thingBuilder.build()); updateThing(thingBuilder.build());
@@ -484,7 +533,11 @@ public abstract class MiIoAbstractHandler extends BaseThingHandler implements Mi
break; break;
} }
if (cmds.containsKey(response.getId())) { if (cmds.containsKey(response.getId())) {
if (response.getCloudServer().isBlank()) {
updateState(CHANNEL_COMMAND, new StringType(response.getResponse().toString())); updateState(CHANNEL_COMMAND, new StringType(response.getResponse().toString()));
} else {
updateState(CHANNEL_RPC, new StringType(response.getResponse().toString()));
}
cmds.remove(response.getId()); cmds.remove(response.getId());
} }
} catch (Exception e) { } catch (Exception e) {
@@ -30,7 +30,6 @@ import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable; import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.miio.internal.MiIoBindingConfiguration; import org.openhab.binding.miio.internal.MiIoBindingConfiguration;
import org.openhab.binding.miio.internal.MiIoCommand; 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.MiIoQuantiyTypes;
import org.openhab.binding.miio.internal.MiIoSendCommand; import org.openhab.binding.miio.internal.MiIoSendCommand;
import org.openhab.binding.miio.internal.Utils; 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.MiIoDatabaseWatchService;
import org.openhab.binding.miio.internal.basic.MiIoDeviceAction; import org.openhab.binding.miio.internal.basic.MiIoDeviceAction;
import org.openhab.binding.miio.internal.basic.MiIoDeviceActionCondition; 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.binding.miio.internal.transport.MiIoAsyncCommunication;
import org.openhab.core.cache.ExpiringCache; import org.openhab.core.cache.ExpiringCache;
import org.openhab.core.library.types.DecimalType; import org.openhab.core.library.types.DecimalType;
@@ -85,7 +85,7 @@ public class MiIoBasicHandler extends MiIoAbstractHandler {
private boolean hasChannelStructure; private boolean hasChannelStructure;
private final ExpiringCache<Boolean> updateDataCache = new ExpiringCache<>(CACHE_EXPIRY, () -> { 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; return true;
}); });
@@ -97,8 +97,8 @@ public class MiIoBasicHandler extends MiIoAbstractHandler {
private ChannelTypeRegistry channelTypeRegistry; private ChannelTypeRegistry channelTypeRegistry;
public MiIoBasicHandler(Thing thing, MiIoDatabaseWatchService miIoDatabaseWatchService, public MiIoBasicHandler(Thing thing, MiIoDatabaseWatchService miIoDatabaseWatchService,
ChannelTypeRegistry channelTypeRegistry) { CloudConnector cloudConnector, ChannelTypeRegistry channelTypeRegistry) {
super(thing, miIoDatabaseWatchService); super(thing, miIoDatabaseWatchService, cloudConnector);
this.channelTypeRegistry = channelTypeRegistry; this.channelTypeRegistry = channelTypeRegistry;
} }
@@ -255,7 +255,7 @@ public class MiIoBasicHandler extends MiIoAbstractHandler {
} }
} }
updateDataCache.invalidateValue(); updateDataCache.invalidateValue();
scheduler.schedule(() -> { miIoScheduler.schedule(() -> {
updateData(); updateData();
}, 3000, TimeUnit.MILLISECONDS); }, 3000, TimeUnit.MILLISECONDS);
} else { } else {
@@ -294,7 +294,7 @@ public class MiIoBasicHandler extends MiIoAbstractHandler {
} }
checkChannelStructure(); checkChannelStructure();
if (!isIdentified) { if (!isIdentified) {
miioCom.queueCommand(MiIoCommand.MIIO_INFO); sendCommand(MiIoCommand.MIIO_INFO);
} }
final MiIoBasicDevice midevice = miioDevice; final MiIoBasicDevice midevice = miioDevice;
if (midevice != null) { if (midevice != null) {
@@ -341,14 +341,7 @@ public class MiIoBasicHandler extends MiIoAbstractHandler {
} }
private void sendRefreshProperties(MiIoCommand command, JsonArray getPropString) { private void sendRefreshProperties(MiIoCommand command, JsonArray getPropString) {
try { sendCommand(command, getPropString.toString());
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);
}
} }
/** /**
@@ -12,10 +12,9 @@
*/ */
package org.openhab.binding.miio.internal.handler; 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.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.miio.internal.basic.MiIoDatabaseWatchService; 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.ChannelUID;
import org.openhab.core.thing.Thing; import org.openhab.core.thing.Thing;
import org.openhab.core.types.Command; import org.openhab.core.types.Command;
@@ -33,8 +32,9 @@ import org.slf4j.LoggerFactory;
public class MiIoGenericHandler extends MiIoAbstractHandler { public class MiIoGenericHandler extends MiIoAbstractHandler {
private final Logger logger = LoggerFactory.getLogger(MiIoGenericHandler.class); private final Logger logger = LoggerFactory.getLogger(MiIoGenericHandler.class);
public MiIoGenericHandler(Thing thing, MiIoDatabaseWatchService miIoDatabaseWatchService) { public MiIoGenericHandler(Thing thing, MiIoDatabaseWatchService miIoDatabaseWatchService,
super(thing, miIoDatabaseWatchService); CloudConnector cloudConnector) {
super(thing, miIoDatabaseWatchService, cloudConnector);
} }
@Override @Override
@@ -44,8 +44,8 @@ public class MiIoGenericHandler extends MiIoAbstractHandler {
updateData(); updateData();
return; return;
} }
if (channelUID.getId().equals(CHANNEL_COMMAND)) { if (handleCommandsChannels(channelUID, command)) {
cmds.put(sendCommand(command.toString()), command.toString()); return;
} }
} }
@@ -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.MiIoBasicChannel;
import org.openhab.binding.miio.internal.basic.MiIoBasicDevice; import org.openhab.binding.miio.internal.basic.MiIoBasicDevice;
import org.openhab.binding.miio.internal.basic.MiIoDatabaseWatchService; 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.cache.ExpiringCache;
import org.openhab.core.library.types.OnOffType; import org.openhab.core.library.types.OnOffType;
import org.openhab.core.thing.ChannelUID; import org.openhab.core.thing.ChannelUID;
@@ -78,12 +79,13 @@ public class MiIoUnsupportedHandler extends MiIoAbstractHandler {
private String model = conf.model != null ? conf.model : ""; private String model = conf.model != null ? conf.model : "";
private final ExpiringCache<Boolean> updateDataCache = new ExpiringCache<>(CACHE_EXPIRY, () -> { 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; return true;
}); });
public MiIoUnsupportedHandler(Thing thing, MiIoDatabaseWatchService miIoDatabaseWatchService) { public MiIoUnsupportedHandler(Thing thing, MiIoDatabaseWatchService miIoDatabaseWatchService,
super(thing, miIoDatabaseWatchService); CloudConnector cloudConnector) {
super(thing, miIoDatabaseWatchService, cloudConnector);
} }
@Override @Override
@@ -104,8 +106,8 @@ public class MiIoUnsupportedHandler extends MiIoAbstractHandler {
sendCommand("set_power[\"off\"]"); sendCommand("set_power[\"off\"]");
} }
} }
if (channelUID.getId().equals(CHANNEL_COMMAND)) { if (handleCommandsChannels(channelUID, command)) {
cmds.put(sendCommand(command.toString()), command.toString()); return;
} }
if (channelUID.getId().equals(CHANNEL_TESTCOMMANDS)) { if (channelUID.getId().equals(CHANNEL_TESTCOMMANDS)) {
executeExperimentalCommands(); executeExperimentalCommands();
@@ -97,15 +97,13 @@ public class MiIoVacuumHandler extends MiIoAbstractHandler {
private ExpiringCache<String> map; private ExpiringCache<String> map;
private String lastHistoryId = ""; private String lastHistoryId = "";
private String lastMap = ""; private String lastMap = "";
private CloudConnector cloudConnector;
private boolean hasChannelStructure; private boolean hasChannelStructure;
private ConcurrentHashMap<RobotCababilities, Boolean> deviceCapabilities = new ConcurrentHashMap<>(); private ConcurrentHashMap<RobotCababilities, Boolean> deviceCapabilities = new ConcurrentHashMap<>();
private ChannelTypeRegistry channelTypeRegistry; private ChannelTypeRegistry channelTypeRegistry;
public MiIoVacuumHandler(Thing thing, MiIoDatabaseWatchService miIoDatabaseWatchService, public MiIoVacuumHandler(Thing thing, MiIoDatabaseWatchService miIoDatabaseWatchService,
CloudConnector cloudConnector, ChannelTypeRegistry channelTypeRegistry) { CloudConnector cloudConnector, ChannelTypeRegistry channelTypeRegistry) {
super(thing, miIoDatabaseWatchService); super(thing, miIoDatabaseWatchService, cloudConnector);
this.cloudConnector = cloudConnector;
this.channelTypeRegistry = channelTypeRegistry; this.channelTypeRegistry = channelTypeRegistry;
mapChannelUid = new ChannelUID(thing.getUID(), CHANNEL_VACUUM_MAP); mapChannelUid = new ChannelUID(thing.getUID(), CHANNEL_VACUUM_MAP);
status = new ExpiringCache<>(CACHE_EXPIRY, () -> { status = new ExpiringCache<>(CACHE_EXPIRY, () -> {
@@ -180,6 +178,9 @@ public class MiIoVacuumHandler extends MiIoAbstractHandler {
} }
return; return;
} }
if (handleCommandsChannels(channelUID, command)) {
return;
}
if (channelUID.getId().equals(CHANNEL_VACUUM)) { if (channelUID.getId().equals(CHANNEL_VACUUM)) {
if (command instanceof OnOffType) { if (command instanceof OnOffType) {
if (command.equals(OnOffType.ON)) { if (command.equals(OnOffType.ON)) {
@@ -188,7 +189,7 @@ public class MiIoVacuumHandler extends MiIoAbstractHandler {
return; return;
} else { } else {
sendCommand(MiIoCommand.STOP_VACUUM); sendCommand(MiIoCommand.STOP_VACUUM);
scheduler.schedule(() -> { miIoScheduler.schedule(() -> {
sendCommand(MiIoCommand.CHARGE); sendCommand(MiIoCommand.CHARGE);
forceStatusUpdate(); forceStatusUpdate();
}, 2000, TimeUnit.MILLISECONDS); }, 2000, TimeUnit.MILLISECONDS);
@@ -205,7 +206,7 @@ public class MiIoVacuumHandler extends MiIoAbstractHandler {
sendCommand(MiIoCommand.PAUSE); sendCommand(MiIoCommand.PAUSE);
} else if (command.toString().equals("dock")) { } else if (command.toString().equals("dock")) {
sendCommand(MiIoCommand.STOP_VACUUM); sendCommand(MiIoCommand.STOP_VACUUM);
scheduler.schedule(() -> { miIoScheduler.schedule(() -> {
sendCommand(MiIoCommand.CHARGE); sendCommand(MiIoCommand.CHARGE);
forceStatusUpdate(); forceStatusUpdate();
}, 2000, TimeUnit.MILLISECONDS); }, 2000, TimeUnit.MILLISECONDS);
@@ -243,14 +244,13 @@ public class MiIoVacuumHandler extends MiIoAbstractHandler {
sendCommand(MiIoCommand.CONSUMABLES_RESET, "[" + command.toString() + "]"); sendCommand(MiIoCommand.CONSUMABLES_RESET, "[" + command.toString() + "]");
updateState(CHANNEL_CONSUMABLE_RESET, new StringType("none")); updateState(CHANNEL_CONSUMABLE_RESET, new StringType("none"));
} }
if (channelUID.getId().equals(CHANNEL_COMMAND)) {
cmds.put(sendCommand(command.toString()), command.toString());
}
} }
private void forceStatusUpdate() { private void forceStatusUpdate() {
status.invalidateValue(); status.invalidateValue();
miIoScheduler.schedule(() -> {
status.getValue(); status.getValue();
}, 3000, TimeUnit.MILLISECONDS);
} }
private void safeUpdateState(String channelID, @Nullable Integer state) { 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(); String mapresponse = response.getResult().getAsJsonArray().get(0).getAsString();
if (!mapresponse.contentEquals("retry") && !mapresponse.contentEquals(lastMap)) { if (!mapresponse.contentEquals("retry") && !mapresponse.contentEquals(lastMap)) {
lastMap = mapresponse; lastMap = mapresponse;
scheduler.submit(() -> updateState(CHANNEL_VACUUM_MAP, getMap(mapresponse))); miIoScheduler.submit(() -> updateState(CHANNEL_VACUUM_MAP, getMap(mapresponse)));
} }
} }
break; break;
case UNKNOWN:
updateState(CHANNEL_COMMAND, new StringType(response.getResponse().toString()));
break;
default: default:
break; break;
} }
@@ -37,6 +37,8 @@ import org.openhab.binding.miio.internal.MiIoCryptoException;
import org.openhab.binding.miio.internal.MiIoMessageListener; import org.openhab.binding.miio.internal.MiIoMessageListener;
import org.openhab.binding.miio.internal.MiIoSendCommand; import org.openhab.binding.miio.internal.MiIoSendCommand;
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.MiCloudException;
import org.openhab.core.thing.ThingStatus; import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail; import org.openhab.core.thing.ThingStatusDetail;
import org.slf4j.Logger; import org.slf4j.Logger;
@@ -78,14 +80,17 @@ public class MiIoAsyncCommunication {
private boolean needPing = true; private boolean needPing = true;
private static final int MAX_ERRORS = 3; private static final int MAX_ERRORS = 3;
private static final int MAX_ID = 15000; private static final int MAX_ID = 15000;
private final CloudConnector cloudConnector;
private ConcurrentLinkedQueue<MiIoSendCommand> concurrentLinkedQueue = new ConcurrentLinkedQueue<>(); 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.ip = ip;
this.token = token; this.token = token;
this.deviceId = did; this.deviceId = did;
this.timeout = timeout; this.timeout = timeout;
this.cloudConnector = cloudConnector;
setId(id); setId(id);
parser = new JsonParser(); parser = new JsonParser();
startReceiver(); startReceiver();
@@ -124,15 +129,16 @@ public class MiIoAsyncCommunication {
} }
} }
public int queueCommand(MiIoCommand command) throws MiIoCryptoException, IOException { public int queueCommand(MiIoCommand command, String cloudServer) throws MiIoCryptoException, IOException {
return queueCommand(command, "[]"); return queueCommand(command, "[]", cloudServer);
} }
public int queueCommand(MiIoCommand command, String params) throws MiIoCryptoException, IOException { public int queueCommand(MiIoCommand command, String params, String cloudServer)
return queueCommand(command.getCommand(), params); 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 { throws MiIoCryptoException, IOException, JsonSyntaxException {
try { try {
JsonObject fullCommand = new JsonObject(); JsonObject fullCommand = new JsonObject();
@@ -143,15 +149,17 @@ public class MiIoAsyncCommunication {
fullCommand.addProperty("id", cmdId); fullCommand.addProperty("id", cmdId);
fullCommand.addProperty("method", command); fullCommand.addProperty("method", command);
fullCommand.add("params", parser.parse(params)); 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); concurrentLinkedQueue.add(sendCmd);
if (logger.isDebugEnabled()) { if (logger.isDebugEnabled()) {
// Obfuscate part of the token to allow sharing of the logfiles // Obfuscate part of the token to allow sharing of the logfiles
String tokenText = Utils.obfuscateToken(Utils.getHex(token)); String tokenText = Utils.obfuscateToken(Utils.getHex(token));
logger.debug("Command added to Queue {} -> {} (Device: {} token: {} Queue: {})", fullCommand.toString(), logger.debug("Command added to Queue {} -> {} (Device: {} token: {} Queue: {}).{}{}",
ip, Utils.getHex(deviceId), tokenText, concurrentLinkedQueue.size()); fullCommand.toString(), ip, Utils.getHex(deviceId), tokenText, concurrentLinkedQueue.size(),
cloudServer.isBlank() ? "" : " Send via cloudserver: ", cloudServer);
} }
if (needPing) { if (needPing && cloudServer.isBlank()) {
sendPing(ip); sendPing(ip);
} }
return cmdId; return cmdId;
@@ -166,7 +174,15 @@ public class MiIoAsyncCommunication {
String errorMsg = "Unknown Error while sending command"; String errorMsg = "Unknown Error while sending command";
String decryptedResponse = ""; String decryptedResponse = "";
try { try {
if (miIoSendCommand.getCloudServer().isBlank()) {
decryptedResponse = sendCommand(miIoSendCommand.getCommandString(), token, ip, deviceId); 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 // hack due to avoid invalid json errors from some misbehaving device firmwares
decryptedResponse = decryptedResponse.replace(",,", ","); decryptedResponse = decryptedResponse.replace(",,", ",");
JsonElement response; JsonElement response;
@@ -188,6 +204,12 @@ public class MiIoAsyncCommunication {
logger.warn("Could not parse '{}' <- {} (Device: {}) gave error {}", decryptedResponse, logger.warn("Could not parse '{}' <- {} (Device: {}) gave error {}", decryptedResponse,
miIoSendCommand.getCommandString(), Utils.getHex(deviceId), e.getMessage()); miIoSendCommand.getCommandString(), Utils.getHex(deviceId), e.getMessage());
errorMsg = "Received message is invalid JSON"; 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(); JsonObject erroResp = new JsonObject();
erroResp.addProperty("error", errorMsg); erroResp.addProperty("error", errorMsg);
@@ -2,7 +2,7 @@
<config-description:config-descriptions <config-description:config-descriptions
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:config-description="https://openhab.org/schemas/config-description/v1.0.0" 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"> <config-description uri="thing-type:miio:config">
<parameter name="host" type="text" required="true"> <parameter name="host" type="text" required="true">
@@ -23,6 +23,16 @@
<description>Device model string, used to determine the subtype.</description> <description>Device model string, used to determine the subtype.</description>
<advanced>true</advanced> <advanced>true</advanced>
</parameter> </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"> <parameter name="refreshInterval" type="integer" min="0" max="9999" required="false">
<label>Refresh Interval</label> <label>Refresh Interval</label>
<description>Refresh interval for refreshing the data in seconds. (0=disabled)</description> <description>Refresh interval for refreshing the data in seconds. (0=disabled)</description>
@@ -23,6 +23,7 @@
<label>Actions</label> <label>Actions</label>
<channels> <channels>
<channel id="commands" typeId="commands"/> <channel id="commands" typeId="commands"/>
<channel id="rpc" typeId="rpc"/>
</channels> </channels>
</channel-group-type> </channel-group-type>
@@ -99,6 +99,10 @@
<item-type>String</item-type> <item-type>String</item-type>
<label>Execute Command</label> <label>Execute Command</label>
</channel-type> </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"> <channel-type id="power">
<item-type>Switch</item-type> <item-type>Switch</item-type>
<label>Power On/Off</label> <label>Power On/Off</label>
@@ -24,6 +24,7 @@
<channels> <channels>
<channel id="power" typeId="power"/> <channel id="power" typeId="power"/>
<channel id="commands" typeId="commands"/> <channel id="commands" typeId="commands"/>
<channel id="rpc" typeId="rpc"/>
<channel id="testcommands" typeId="testcommands"/> <channel id="testcommands" typeId="testcommands"/>
</channels> </channels>
</channel-group-type> </channel-group-type>
@@ -29,6 +29,7 @@
<channels> <channels>
<channel id="control" typeId="control"/> <channel id="control" typeId="control"/>
<channel id="commands" typeId="commands"/> <channel id="commands" typeId="commands"/>
<channel id="rpc" typeId="rpc"/>
<channel id="fan" typeId="fan"/> <channel id="fan" typeId="fan"/>
<channel id="vacuum" typeId="vacuum"/> <channel id="vacuum" typeId="vacuum"/>
<channel id="segment" typeId="segment"/> <channel id="segment" typeId="segment"/>