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
_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.
| 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.
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
_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.
| 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.
public String token;
public String deviceId;
public String model;
+ public String communication;
public int refreshInterval;
public int timeout;
public String cloudServer;
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";
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);
}
}
private final MiIoCommand command;
private final JsonObject commandJson;
private @Nullable JsonObject response;
+ private String cloudServer = "";
public void setResponse(JsonObject response) {
this.response = response;
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;
}
}
return new JsonObject();
}
+
+ public String getCloudServer() {
+ return cloudServer;
+ }
+
+ public void setCloudServer(String cloudServer) {
+ this.cloudServer = cloudServer;
+ }
}
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;
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);
}
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;
}
}
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);
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;
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();
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;
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;
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;
protected @Nullable MiIoBindingConfiguration configuration;
protected @Nullable MiIoAsyncCommunication miioCom;
+ protected CloudConnector cloudConnector;
+ protected String cloudServer = "";
protected int lastId;
protected Map<Integer, String> cmds = new ConcurrentHashMap<>();
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()) {
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) {
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);
}
@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);
miioCom.close();
this.miioCom = null;
}
+ miIoScheduler.shutdownNow();
}
protected int sendCommand(MiIoCommand command) {
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());
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
* 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();
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) {
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) {
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 {}",
pollingJob.cancel(true);
this.pollingJob = null;
}
- scheduler.schedule(() -> {
+ miIoScheduler.schedule(() -> {
ThingBuilder thingBuilder = editThing();
thingBuilder.withLabel(miDevice.getDescription());
updateThing(thingBuilder.build());
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) {
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;
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;
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;
});
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;
}
}
}
updateDataCache.invalidateValue();
- scheduler.schedule(() -> {
+ miIoScheduler.schedule(() -> {
updateData();
}, 3000, TimeUnit.MILLISECONDS);
} else {
}
checkChannelStructure();
if (!isIdentified) {
- miioCom.queueCommand(MiIoCommand.MIIO_INFO);
+ sendCommand(MiIoCommand.MIIO_INFO);
}
final MiIoBasicDevice midevice = miioDevice;
if (midevice != null) {
}
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());
}
/**
*/
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;
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
updateData();
return;
}
- if (channelUID.getId().equals(CHANNEL_COMMAND)) {
- cmds.put(sendCommand(command.toString()), command.toString());
+ if (handleCommandsChannels(channelUID, command)) {
+ return;
}
}
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;
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
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();
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, () -> {
}
return;
}
+ if (handleCommandsChannels(channelUID, command)) {
+ return;
+ }
if (channelUID.getId().equals(CHANNEL_VACUUM)) {
if (command instanceof OnOffType) {
if (command.equals(OnOffType.ON)) {
return;
} else {
sendCommand(MiIoCommand.STOP_VACUUM);
- scheduler.schedule(() -> {
+ miIoScheduler.schedule(() -> {
sendCommand(MiIoCommand.CHARGE);
forceStatusUpdate();
}, 2000, TimeUnit.MILLISECONDS);
sendCommand(MiIoCommand.PAUSE);
} else if (command.toString().equals("dock")) {
sendCommand(MiIoCommand.STOP_VACUUM);
- scheduler.schedule(() -> {
+ miIoScheduler.schedule(() -> {
sendCommand(MiIoCommand.CHARGE);
forceStatusUpdate();
}, 2000, TimeUnit.MILLISECONDS);
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) {
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;
}
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;
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();
}
}
- 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();
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;
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;
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);
<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>
<label>Actions</label>
<channels>
<channel id="commands" typeId="commands"/>
+ <channel id="rpc" typeId="rpc"/>
</channels>
</channel-group-type>
<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>
<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>
<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"/>