[miio] Improved unsupported handler that build experimental support (#8716)
* [miio] Improved unsupported handler that build experimental support New unsupported handler that tries to find channels for unsupported devices and creates experimental database file for it. * [miio] logger fixes Signed-off-by: Marcel Verpaalen <marcel@verpaalen.com>
This commit is contained in:
parent
54791d97d0
commit
aa98737e9f
|
@ -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.
|
||||
|
||||
|
|
|
@ -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 (<a name="philips-light-dlight">philips.light.dlight</a>) Channels
|
||||
### Philips Down Light (<a name="philips-light-dlight">philips.light.dlight</a>) 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" <status>
|
||||
Group G_light "Philips Down Light" <status>
|
||||
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"}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<Integer, MiIoBasicChannel> testChannelList = new LinkedHashMap<>();
|
||||
private LinkedHashMap<MiIoBasicChannel, String> supportedChannelList = new LinkedHashMap<>();
|
||||
private String model = conf.model != null ? conf.model : "";
|
||||
|
||||
private final ExpiringCache<Boolean> 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<String, MiIoBasicChannel> 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<String, MiIoBasicChannel> collectProperties(String model) {
|
||||
LinkedHashMap<String, MiIoBasicChannel> testChannelsList = new LinkedHashMap<>();
|
||||
LinkedHashSet<MiIoDevices> 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<MiIoBasicChannel> 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<MiIoBasicChannel> 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<String> 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -30,7 +30,7 @@
|
|||
|
||||
<channel-type id="testcommands">
|
||||
<item-type>Switch</item-type>
|
||||
<label>(experimental)Execute Test Commands</label>
|
||||
<description>(experimental)Execute Test Commands to support development for your device. (NB device can switch modes)</description>
|
||||
<label>(experimental)Execute test to find supported channels</label>
|
||||
<description>(experimental)Execute test for all known properties to find channels supported by your device.</description>
|
||||
</channel-type>
|
||||
</thing:thing-descriptions>
|
||||
|
|
Loading…
Reference in New Issue