diff --git a/bundles/org.openhab.binding.miio/README.base.md b/bundles/org.openhab.binding.miio/README.base.md
index 65f4af23e..f61bb09a2 100644
--- a/bundles/org.openhab.binding.miio/README.base.md
+++ b/bundles/org.openhab.binding.miio/README.base.md
@@ -88,12 +88,25 @@ or in case of unknown models include the model information e.g.:
Newer devices may not yet be supported.
However, many devices share large similarities with existing devices.
The binding allows to try/test if your new device is working with database files of older devices as well.
+
+There are 2 ways to get unsupported devices working, by overriding the model with the model of a supported item or by test all known properties to see which are supported by your device.
+
+## Substitute model for unsupported devices
+Replace the model with the model which is already supported.
For this, first remove your unsupported thing. Manually add a miio:basic thing.
Besides the regular configuration (like ip address, token) the modelId needs to be provided.
Normally the modelId is populated with the model of your device, however in this case, use the modelId of a similar device.
Look at the openHAB forum, or the openHAB GitHub repository for the modelId of similar devices.
-# Advanced: adding local database files to support new devices
+## Supported property test
+The unsupported device has a test channel with switch. When switching on, all known properties are tested, this may take few minutes.
+A test report will be shown in the log and is saved in the userdata/miio folder.
+If supported properties are found, an experimental database file is saved to the conf/misc/miio folder (see below chapter).
+The thing will go offline and will come back online as basic device, supporting the found channels.
+The database file may need to be modified to display the right channel names.
+After validation, please share the logfile and json files on the openHAB forum or the openHAB GitHub to build future support for this model.
+
+## Advanced: adding local database files to support new devices
Things using the basic handler (miio:basic things) are driven by json 'database' files.
This instructs the binding which channels to create, which properties and actions are associated with the channels etc.
@@ -101,7 +114,7 @@ The conf/misc/miio (e.g. in Linux `/opt/openhab2/conf/misc/miio/`) is scanned fo
Note that local database files take preference over build-in ones, hence if a json file is local and in the database the local file will be used.
For format, please check the current database files in openHAB GitHub.
-## Channels
+# Channels
Depending on the device, different channels are available.
diff --git a/bundles/org.openhab.binding.miio/README.md b/bundles/org.openhab.binding.miio/README.md
index 6cbccfe5a..0c4baba6b 100644
--- a/bundles/org.openhab.binding.miio/README.md
+++ b/bundles/org.openhab.binding.miio/README.md
@@ -163,7 +163,7 @@ or in case of unknown models include the model information e.g.:
| Philips Light | miio:basic | [philips.light.lrceiling](#philips-light-lrceiling) | Yes | Experimental support. Please report back if all channels are functional. Preferably share the debug log of property refresh and command responses |
| Xiaomi PHILIPS Zhirui Smart LED Bulb E14 Candle Lamp White Crystal | miio:basic | [philips.light.candle2](#philips-light-candle2) | Yes | |
| philips.light.mono1 | miio:basic | [philips.light.mono1](#philips-light-mono1) | Yes | |
-| Light | miio:basic | [philips.light.dlight](#philips-light-dlight) | Yes | Experimental support. Please report back if all channels are functional. Preferably share the debug log of property refresh and command responses |
+| Philips Down Light | miio:basic | [philips.light.dlight](#philips-light-dlight) | Yes | Experimental support. Please report back if all channels are functional. Preferably share the debug log of property refresh and command responses |
| Philips Ceiling Light | miio:basic | [philips.light.mceil](#philips-light-mceil) | Yes | Experimental support. Please report back if all channels are functional. Preferably share the debug log of property refresh and command responses |
| Philips Ceiling Light | miio:basic | [philips.light.mceilm](#philips-light-mceilm) | Yes | Experimental support. Please report back if all channels are functional. Preferably share the debug log of property refresh and command responses |
| Philips Ceiling Light | miio:basic | [philips.light.mceils](#philips-light-mceils) | Yes | Experimental support. Please report back if all channels are functional. Preferably share the debug log of property refresh and command responses |
@@ -311,11 +311,24 @@ or in case of unknown models include the model information e.g.:
Newer devices may not yet be supported.
However, many devices share large similarities with existing devices.
The binding allows to try/test if your new device is working with database files of older devices as well.
+
+There are 2 ways to get unsupported devices working, by overriding the model with the model of a supported item or by test all known properties to see which are supported by your device.
+
+## Substitute model for unsupported devices
+Replace the model with the model which is already supported.
For this, first remove your unsupported thing. Manually add a miio:basic thing.
Besides the regular configuration (like ip address, token) the modelId needs to be provided.
Normally the modelId is populated with the model of your device, however in this case, use the modelId of a similar device.
Look at the openHAB forum, or the openHAB GitHub repository for the modelId of similar devices.
+## Supported property test
+The unsupported device has a test channel with switch. When switching on, all known properties are tested, this may take few minutes.
+A test report will be shown in the log and is saved in the userdata/miio folder.
+If supported properties are found, an experimental database file is saved to the conf/misc/miio folder (see below chapter).
+The thing will go offline and will come back online as basic device, supporting the found channels.
+The database file may need to be modified to display the right channel names.
+After validation, please share the logfile and json files on the openHAB forum or the openHAB GitHub to build future support for this model.
+
# Advanced: adding local database files to support new devices
Things using the basic handler (miio:basic things) are driven by json 'database' files.
@@ -1394,7 +1407,7 @@ e.g. `smarthome:send actionCommand 'upd_timer["1498595904821", "on"]'` would ena
| brightness | Dimmer | Brightness | |
| scene | Number | Scene | |
-### Light (philips.light.dlight) Channels
+### Philips Down Light (philips.light.dlight) Channels
| Channel | Type | Description | Comment |
|------------------|---------|-------------------------------------|------------|
@@ -4211,12 +4224,12 @@ Dimmer brightness "Brightness" (G_light) {channel="miio:basic:light:brightness"}
Number scene "Scene" (G_light) {channel="miio:basic:light:scene"}
```
-### Light (philips.light.dlight) item file lines
+### Philips Down Light (philips.light.dlight) item file lines
note: Autogenerated example. Replace the id (light) in the channel with your own. Replace `basic` with `generic` in the thing UID depending on how your thing was discovered.
```java
-Group G_light "Light"
+Group G_light "Philips Down Light"
Switch on "Power" (G_light) {channel="miio:basic:light:on"}
Number mode "Mode" (G_light) {channel="miio:basic:light:mode"}
Number brightness "Brightness" (G_light) {channel="miio:basic:light:brightness"}
diff --git a/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/MiIoBindingConstants.java b/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/MiIoBindingConstants.java
index 9875434ae..da843cd87 100644
--- a/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/MiIoBindingConstants.java
+++ b/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/MiIoBindingConstants.java
@@ -12,12 +12,14 @@
*/
package org.openhab.binding.miio.internal;
+import java.io.File;
import java.util.Collections;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.core.OpenHAB;
import org.openhab.core.thing.ThingTypeUID;
/**
@@ -111,4 +113,8 @@ public final class MiIoBindingConstants {
.of("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF", "00000000000000000000000000000000").collect(Collectors.toSet()));
public static final String DATABASE_PATH = "database/";
+ public static final String BINDING_DATABASE_PATH = OpenHAB.getConfigFolder() + File.separator + "misc"
+ + File.separator + BINDING_ID;
+ public static final String BINDING_USERDATA_PATH = OpenHAB.getUserDataFolder() + File.separator
+ + MiIoBindingConstants.BINDING_ID;
}
diff --git a/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/Utils.java b/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/Utils.java
index 444b5b1fd..4a0e71c5e 100644
--- a/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/Utils.java
+++ b/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/Utils.java
@@ -97,4 +97,8 @@ public final class Utils {
jsonObject = jsonElement.getAsJsonObject();
return jsonObject;
}
+
+ public static String minLengthString(String string, int length) {
+ return String.format("%-" + length + "s", string);
+ }
}
diff --git a/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/handler/MiIoUnsupportedHandler.java b/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/handler/MiIoUnsupportedHandler.java
index b5d67b28a..4dfaaf3e8 100644
--- a/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/handler/MiIoUnsupportedHandler.java
+++ b/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/handler/MiIoUnsupportedHandler.java
@@ -14,9 +14,30 @@ package org.openhab.binding.miio.internal.handler;
import static org.openhab.binding.miio.internal.MiIoBindingConstants.*;
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.net.URL;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
import java.util.concurrent.TimeUnit;
import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.miio.internal.MiIoBindingConfiguration;
+import org.openhab.binding.miio.internal.MiIoCommand;
+import org.openhab.binding.miio.internal.MiIoDevices;
+import org.openhab.binding.miio.internal.MiIoSendCommand;
+import org.openhab.binding.miio.internal.Utils;
+import org.openhab.binding.miio.internal.basic.DeviceMapping;
+import org.openhab.binding.miio.internal.basic.MiIoBasicChannel;
+import org.openhab.binding.miio.internal.basic.MiIoBasicDevice;
import org.openhab.binding.miio.internal.basic.MiIoDatabaseWatchService;
import org.openhab.core.cache.ExpiringCache;
import org.openhab.core.library.types.OnOffType;
@@ -27,6 +48,12 @@ import org.openhab.core.types.RefreshType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.JsonIOException;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonSyntaxException;
+
/**
* The {@link MiIoUnsupportedHandler} is responsible for handling commands, which are
* sent to one of the channels.
@@ -35,7 +62,19 @@ import org.slf4j.LoggerFactory;
*/
@NonNullByDefault
public class MiIoUnsupportedHandler extends MiIoAbstractHandler {
+
+ private static final DateTimeFormatter DATEFORMATTER = DateTimeFormatter.ofPattern("yyyyMMdd-HHmmss");
+ private static final Gson GSONP = new GsonBuilder().setPrettyPrinting().create();
+
private final Logger logger = LoggerFactory.getLogger(MiIoUnsupportedHandler.class);
+ private final MiIoBindingConfiguration conf = getConfigAs(MiIoBindingConfiguration.class);
+
+ private StringBuilder sb = new StringBuilder();
+ private String info = "";
+ private int lastCommand = -1;
+ private LinkedHashMap testChannelList = new LinkedHashMap<>();
+ private LinkedHashMap supportedChannelList = new LinkedHashMap<>();
+ private String model = conf.model != null ? conf.model : "";
private final ExpiringCache updateDataCache = new ExpiringCache<>(CACHE_EXPIRY, () -> {
scheduler.schedule(this::updateData, 0, TimeUnit.SECONDS);
@@ -72,38 +111,6 @@ public class MiIoUnsupportedHandler extends MiIoAbstractHandler {
}
}
- // TODO: In future version this ideally would test all known commands (e.g. from the database) and create/enable a
- // channel if they appear to be supported
- private void executeExperimentalCommands() {
- String[] testCommands = new String[0];
- switch (miDevice) {
- case POWERPLUG:
- case POWERPLUG2:
- case POWERSTRIP:
- case POWERSTRIP2:
- case YEELIGHT_C1:
- break;
- case VACUUM:
- testCommands = new String[] { "miIO.info", "get_current_sound", "get_map_v1", "get_serial_number",
- "get_timezone" };
- break;
- case AIR_PURIFIERM:
- case AIR_PURIFIER1:
- case AIR_PURIFIER2:
- case AIR_PURIFIER3:
- case AIR_PURIFIER6:
- break;
-
- default:
- testCommands = new String[] { "miIO.info" };
- break;
- }
- logger.info("Start Experimental Testing of commands for device '{}'. ", miDevice.toString());
- for (String c : testCommands) {
- logger.info("Test command '{}'. Response: '{}'", c, sendCommand(c));
- }
- }
-
@Override
protected synchronized void updateData() {
if (skipUpdate()) {
@@ -117,4 +124,192 @@ public class MiIoUnsupportedHandler extends MiIoAbstractHandler {
e);
}
}
+
+ @Override
+ public void onMessageReceived(MiIoSendCommand response) {
+ super.onMessageReceived(response);
+ if (MiIoCommand.MIIO_INFO.equals(response.getCommand()) && !response.isError()) {
+ JsonObject miioinfo = response.getResult().getAsJsonObject();
+ if (miioinfo.has("model")) {
+ model = miioinfo.get("model").getAsString();
+ }
+ miioinfo.remove("token");
+ miioinfo.remove("ap");
+ miioinfo.remove("mac");
+ info = miioinfo.toString();
+ sb.append(info);
+ sb.append("\r\n");
+ }
+ if (lastCommand >= response.getId()) {
+ sb.append(response.getCommandString());
+ sb.append(" -> ");
+ sb.append(response.getResponse());
+ sb.append("\r\n");
+ String res = response.getResult().toString();
+ if (!response.isError() && !res.contentEquals("[null]") && !res.contentEquals("[]")) {
+ if (testChannelList.containsKey(response.getId())) {
+ supportedChannelList.put(testChannelList.get(response.getId()), res);
+ }
+ }
+ }
+ if (lastCommand >= 0 & lastCommand <= response.getId()) {
+ lastCommand = -1;
+ finishChannelTest();
+ updateState(CHANNEL_TESTCOMMANDS, OnOffType.OFF);
+ }
+ }
+
+ private void executeExperimentalCommands() {
+ LinkedHashMap channelList = collectProperties(conf.model);
+ sendCommand(MiIoCommand.MIIO_INFO);
+ sb = new StringBuilder();
+ logger.info("Start experimental testing of supported properties for device '{}'. ", miDevice.toString());
+ sb.append("Info for ");
+ sb.append(conf.model);
+ sb.append("\r\n");
+ sb.append("Properties: ");
+ int lastCommand = -1;
+ for (String c : channelList.keySet()) {
+ String cmd = "get_prop[" + c + "]";
+ sb.append(c);
+ sb.append(" -> ");
+ lastCommand = sendCommand(cmd);
+ sb.append(lastCommand);
+ sb.append(", ");
+ testChannelList.put(lastCommand, channelList.get(c));
+ }
+ this.lastCommand = lastCommand;
+ sb.append("\r\n");
+ logger.info("{}", sb.toString());
+ }
+
+ private LinkedHashMap collectProperties(String model) {
+ LinkedHashMap testChannelsList = new LinkedHashMap<>();
+ LinkedHashSet testDeviceList = new LinkedHashSet<>();
+
+ // first add similar devices to test those channels first, then test all others
+ int[] subset = { model.length() - 1, model.lastIndexOf("."), model.indexOf("."), 0 };
+ for (int i : subset) {
+ try {
+ final String mm = model.substring(0, model.lastIndexOf("."));
+ for (MiIoDevices dev : MiIoDevices.values()) {
+ if (dev.getThingType().equals(THING_TYPE_BASIC) && (i == 0 || dev.getModel().contains(mm))) {
+ testDeviceList.add(dev);
+ }
+ }
+ } catch (IndexOutOfBoundsException e) {
+ // swallow
+ }
+ }
+ for (MiIoDevices dev : testDeviceList) {
+ for (MiIoBasicChannel ch : getBasicChannels(dev.getModel())) {
+ if (!ch.isMiOt() && !ch.getProperty().isBlank() && !testChannelsList.containsKey(ch.getProperty())) {
+ testChannelsList.put(ch.getProperty(), ch);
+ }
+ }
+ }
+ return testChannelsList;
+ }
+
+ private List getBasicChannels(String deviceName) {
+ logger.debug("Adding Channels from model: {}", deviceName);
+ URL fn = miIoDatabaseWatchService.getDatabaseUrl(deviceName);
+ if (fn == null) {
+ logger.warn("Database entry for model '{}' cannot be found.", deviceName);
+ return Collections.emptyList();
+ }
+ try {
+ JsonObject deviceMapping = Utils.convertFileToJSON(fn);
+ logger.debug("Using device database: {} for device {}", fn.getFile(), deviceName);
+ final MiIoBasicDevice device = GSONP.fromJson(deviceMapping, MiIoBasicDevice.class);
+ return device.getDevice().getChannels();
+ } catch (JsonIOException | JsonSyntaxException e) {
+ logger.warn("Error parsing database Json", e);
+ } catch (IOException e) {
+ logger.warn("Error reading database file", e);
+ } catch (Exception e) {
+ logger.warn("Error creating channel structure", e);
+ }
+ return Collections.emptyList();
+ }
+
+ private void finishChannelTest() {
+ sb.append("===================================\r\n");
+ sb.append("Responsive properties\r\n");
+ sb.append("===================================\r\n");
+ sb.append("Device Info: ");
+ sb.append(info);
+ for (MiIoBasicChannel ch : supportedChannelList.keySet()) {
+ sb.append("Property: ");
+ sb.append(Utils.minLengthString(ch.getProperty(), 15));
+ sb.append(" Friendly Name: ");
+ sb.append(Utils.minLengthString(ch.getFriendlyName(), 25));
+ sb.append(" Response: ");
+ sb.append(supportedChannelList.get(ch));
+ sb.append("\r\n");
+ }
+ if (supportedChannelList.size() > 0) {
+ MiIoBasicDevice mbd = createBasicDeviceDb(model, new ArrayList<>(supportedChannelList.keySet()));
+ writeDevice(mbd);
+ sb.append("Created experimental database for your device:\r\n");
+ sb.append(GSONP.toJson(mbd));
+ isIdentified = false;
+ } else {
+ sb.append("No supported channels found.\r\n");
+ }
+ sb.append(
+ "\r\nPlease share your this output on the community forum or github to get this device supported.\r\n");
+ logger.info("{}", sb.toString());
+ writeLog();
+ }
+
+ private MiIoBasicDevice createBasicDeviceDb(String model, List miIoBasicChannels) {
+ MiIoBasicDevice device = new MiIoBasicDevice();
+ DeviceMapping deviceMapping = new DeviceMapping();
+ deviceMapping.setPropertyMethod(MiIoCommand.GET_PROPERTY.getCommand());
+ deviceMapping.setMaxProperties(2);
+ deviceMapping.setId(Arrays.asList(new String[] { model }));
+ int duplicates = 0;
+ HashSet chNames = new HashSet<>();
+ for (MiIoBasicChannel ch : miIoBasicChannels) {
+ String channelName = ch.getChannel();
+ if (chNames.contains(channelName)) {
+ channelName = channelName + duplicates;
+ ch.setChannel(channelName);
+ duplicates++;
+ }
+ chNames.add(channelName);
+ }
+ deviceMapping.setChannels(miIoBasicChannels);
+ device.setDevice(deviceMapping);
+ return device;
+ }
+
+ private void writeDevice(MiIoBasicDevice device) {
+ File folder = new File(BINDING_DATABASE_PATH);
+ if (!folder.exists()) {
+ folder.mkdirs();
+ }
+ File dataFile = new File(folder, model + "-experimental.json");
+ try (FileWriter writer = new FileWriter(dataFile)) {
+ writer.write(GSONP.toJson(device));
+ logger.info("Database file created: {}", dataFile.getAbsolutePath());
+ } catch (IOException e) {
+ logger.info("Error writing database file {}: {}", dataFile.getAbsolutePath(), e.getMessage());
+ }
+ }
+
+ private void writeLog() {
+ File folder = new File(BINDING_USERDATA_PATH);
+ if (!folder.exists()) {
+ folder.mkdirs();
+ }
+ File dataFile = new File(folder, "test-" + model + "-" + LocalDateTime.now().format(DATEFORMATTER) + ".txt");
+ try (FileWriter writer = new FileWriter(dataFile)) {
+ writer.write(sb.toString());
+ logger.info("Saved device testing file to {}", dataFile.getAbsolutePath());
+ } catch (IOException e) {
+ logger.info("Error writing file {}: {}", dataFile.getAbsolutePath(), e.getMessage());
+ }
+ }
}
diff --git a/bundles/org.openhab.binding.miio/src/main/resources/OH-INF/thing/unsupportedThing.xml b/bundles/org.openhab.binding.miio/src/main/resources/OH-INF/thing/unsupportedThing.xml
index 4473a413f..34b51f1ba 100644
--- a/bundles/org.openhab.binding.miio/src/main/resources/OH-INF/thing/unsupportedThing.xml
+++ b/bundles/org.openhab.binding.miio/src/main/resources/OH-INF/thing/unsupportedThing.xml
@@ -30,7 +30,7 @@
Switch
-
- (experimental)Execute Test Commands to support development for your device. (NB device can switch modes)
+
+ (experimental)Execute test for all known properties to find channels supported by your device.