| 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 |
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.
| 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 |
|------------------|---------|-------------------------------------|------------|
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"}
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;
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.
*/
@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);
}
}
- // 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()) {
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());
+ }
+ }
}