]> git.basschouten.com Git - openhab-addons.git/commitdiff
[miio] Automatic create experimental support for (unsupported) miot devices (#11149)
authorMarcel <marcel@verpaalen.com>
Sun, 19 Sep 2021 20:01:22 +0000 (22:01 +0200)
committerGitHub <noreply@github.com>
Sun, 19 Sep 2021 20:01:22 +0000 (22:01 +0200)
Signed-off-by: Marcel Verpaalen <marcel@verpaalen.com>
20 files changed:
bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/MiIoBindingConstants.java
bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/MiIoHandlerFactory.java
bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/MiIoQuantiyTypes.java
bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/basic/StateDescriptionDTO.java
bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/handler/MiIoUnsupportedHandler.java
bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/miot/ActionDTO.java [new file with mode: 0644]
bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/miot/EventDTO.java [new file with mode: 0644]
bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/miot/MiIoQuantiyTypesConversion.java [new file with mode: 0644]
bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/miot/MiotDeviceDataDTO.java [new file with mode: 0644]
bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/miot/MiotParseException.java [new file with mode: 0644]
bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/miot/MiotParser.java [new file with mode: 0644]
bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/miot/ModelUrnsDTO.java [new file with mode: 0644]
bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/miot/OptionsValueDescriptionsListDTO.java [new file with mode: 0644]
bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/miot/PropertyDTO.java [new file with mode: 0644]
bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/miot/ServiceDTO.java [new file with mode: 0644]
bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/miot/UrnsDTO.java [new file with mode: 0644]
bundles/org.openhab.binding.miio/src/main/resources/OH-INF/thing/unsupportedThing.xml
bundles/org.openhab.binding.miio/src/main/resources/database/dreame.vacuum.mc1808-miot.json
bundles/org.openhab.binding.miio/src/test/java/org/openhab/binding/miio/internal/MiIoQuantiyTypesConversionTest.java [new file with mode: 0644]
bundles/org.openhab.binding.miio/src/test/java/org/openhab/binding/miio/internal/MiotJsonFileCreator.java [new file with mode: 0644]

index 079b41a566c5b3e00bb3a74f6f014dc5bd02f0e8..4a887e7f76b39033b53872881fb1283c060dc288 100644 (file)
@@ -65,6 +65,7 @@ public final class MiIoBindingConstants {
     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";
+    public static final String CHANNEL_TESTMIOT = "actions#testmiot";
     public static final String CHANNEL_POWER = "actions#power";
 
     public static final String CHANNEL_SSID = "network#ssid";
index 47896deed838fac4762d8ee0b6a20a736522ee9b..523efc2565904ef5caa5384efd99961d37f31619 100644 (file)
@@ -29,6 +29,7 @@ import org.openhab.binding.miio.internal.handler.MiIoGenericHandler;
 import org.openhab.binding.miio.internal.handler.MiIoUnsupportedHandler;
 import org.openhab.binding.miio.internal.handler.MiIoVacuumHandler;
 import org.openhab.core.common.ThreadPoolManager;
+import org.openhab.core.io.net.http.HttpClientFactory;
 import org.openhab.core.thing.Thing;
 import org.openhab.core.thing.ThingTypeUID;
 import org.openhab.core.thing.binding.BaseThingHandlerFactory;
@@ -54,6 +55,7 @@ public class MiIoHandlerFactory extends BaseThingHandlerFactory {
     private static final String THING_HANDLER_THREADPOOL_NAME = "thingHandler";
     protected final ScheduledExecutorService scheduler = ThreadPoolManager
             .getScheduledPool(THING_HANDLER_THREADPOOL_NAME);
+    private final HttpClientFactory httpClientFactory;
     private MiIoDatabaseWatchService miIoDatabaseWatchService;
     private CloudConnector cloudConnector;
     private ChannelTypeRegistry channelTypeRegistry;
@@ -62,9 +64,11 @@ public class MiIoHandlerFactory extends BaseThingHandlerFactory {
     private final Logger logger = LoggerFactory.getLogger(MiIoHandlerFactory.class);
 
     @Activate
-    public MiIoHandlerFactory(@Reference ChannelTypeRegistry channelTypeRegistry,
+    public MiIoHandlerFactory(@Reference HttpClientFactory httpClientFactory,
+            @Reference ChannelTypeRegistry channelTypeRegistry,
             @Reference MiIoDatabaseWatchService miIoDatabaseWatchService, @Reference CloudConnector cloudConnector,
             @Reference BasicChannelTypeProvider basicChannelTypeProvider, Map<String, Object> properties) {
+        this.httpClientFactory = httpClientFactory;
         this.miIoDatabaseWatchService = miIoDatabaseWatchService;
         this.channelTypeRegistry = channelTypeRegistry;
         this.basicChannelTypeProvider = basicChannelTypeProvider;
@@ -113,6 +117,7 @@ public class MiIoHandlerFactory extends BaseThingHandlerFactory {
         if (thingTypeUID.equals(THING_TYPE_VACUUM)) {
             return new MiIoVacuumHandler(thing, miIoDatabaseWatchService, cloudConnector, channelTypeRegistry);
         }
-        return new MiIoUnsupportedHandler(thing, miIoDatabaseWatchService, cloudConnector);
+        return new MiIoUnsupportedHandler(thing, miIoDatabaseWatchService, cloudConnector,
+                httpClientFactory.getCommonHttpClient());
     }
 }
index 7ebf9c40c8ea7b0169cbe5b470a1559a9e463ea5..2cea22614a416b8fd53003eac7c80e340648fd5d 100644 (file)
@@ -48,7 +48,7 @@ public enum MiIoQuantiyTypes {
     MILLI_AMPERE(MILLI(Units.AMPERE), "mA"),
     VOLT(Units.VOLT),
     MILLI_VOLT(MILLI(Units.VOLT), "mV"),
-    WATT(Units.WATT),
+    WATT(Units.WATT, "W", "w"),
     LITRE(Units.LITRE, "liter"),
     LUX(Units.LUX),
     RADIANS(Units.RADIAN, "radians"),
index 4d8e704ad7abf2cb83152ee5b4f2e9b5a1ab9ddf..053331cd2525e765237b03aec59420e873e9ad2a 100644 (file)
@@ -59,7 +59,7 @@ public class StateDescriptionDTO {
         return minimum;
     }
 
-    public void setMinimum(BigDecimal minimum) {
+    public void setMinimum(@Nullable BigDecimal minimum) {
         this.minimum = minimum;
     }
 
@@ -68,7 +68,7 @@ public class StateDescriptionDTO {
         return maximum;
     }
 
-    public void setMaximum(BigDecimal maximum) {
+    public void setMaximum(@Nullable BigDecimal maximum) {
         this.maximum = maximum;
     }
 
@@ -77,7 +77,7 @@ public class StateDescriptionDTO {
         return step;
     }
 
-    public void setStep(BigDecimal step) {
+    public void setStep(@Nullable BigDecimal step) {
         this.step = step;
     }
 
@@ -86,7 +86,7 @@ public class StateDescriptionDTO {
         return pattern;
     }
 
-    public void setPattern(String pattern) {
+    public void setPattern(@Nullable String pattern) {
         this.pattern = pattern;
     }
 
@@ -95,7 +95,7 @@ public class StateDescriptionDTO {
         return readOnly;
     }
 
-    public void setReadOnly(Boolean readOnly) {
+    public void setReadOnly(@Nullable Boolean readOnly) {
         this.readOnly = readOnly;
     }
 
@@ -104,7 +104,7 @@ public class StateDescriptionDTO {
         return options;
     }
 
-    public void setOptions(List<OptionsValueListDTO> options) {
+    public void setOptions(@Nullable List<OptionsValueListDTO> options) {
         this.options = options;
     }
 }
index 20be68f71733d68e1b8d88cd3ec543b05d20edee..21e0dbdb967ff3ef2707a1575ea34b68110b53c7 100644 (file)
@@ -31,6 +31,7 @@ import java.util.concurrent.TimeUnit;
 
 import org.eclipse.jdt.annotation.NonNullByDefault;
 import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.jetty.client.HttpClient;
 import org.openhab.binding.miio.internal.MiIoBindingConfiguration;
 import org.openhab.binding.miio.internal.MiIoCommand;
 import org.openhab.binding.miio.internal.MiIoDevices;
@@ -41,6 +42,7 @@ 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.binding.miio.internal.miot.MiotParser;
 import org.openhab.core.cache.ExpiringCache;
 import org.openhab.core.library.types.OnOffType;
 import org.openhab.core.thing.ChannelUID;
@@ -67,6 +69,7 @@ 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 HttpClient httpClient;
 
     private final Logger logger = LoggerFactory.getLogger(MiIoUnsupportedHandler.class);
     private final MiIoBindingConfiguration conf = getConfigAs(MiIoBindingConfiguration.class);
@@ -84,8 +87,9 @@ public class MiIoUnsupportedHandler extends MiIoAbstractHandler {
     });
 
     public MiIoUnsupportedHandler(Thing thing, MiIoDatabaseWatchService miIoDatabaseWatchService,
-            CloudConnector cloudConnector) {
+            CloudConnector cloudConnector, HttpClient httpClientFactory) {
         super(thing, miIoDatabaseWatchService, cloudConnector);
+        this.httpClient = httpClientFactory;
     }
 
     @Override
@@ -112,6 +116,9 @@ public class MiIoUnsupportedHandler extends MiIoAbstractHandler {
         if (channelUID.getId().equals(CHANNEL_TESTCOMMANDS)) {
             executeExperimentalCommands();
         }
+        if (channelUID.getId().equals(CHANNEL_TESTMIOT)) {
+            executeCreateMiotTestFile();
+        }
     }
 
     @Override
@@ -189,6 +196,59 @@ public class MiIoUnsupportedHandler extends MiIoAbstractHandler {
         logger.info("{}", sb.toString());
     }
 
+    private void executeCreateMiotTestFile() {
+        sb = new StringBuilder();
+        try {
+            MiotParser miotParser;
+            miotParser = MiotParser.parse(model, httpClient);
+            logger.info("urn: {}", miotParser.getUrn());
+            logger.info("{}", miotParser.getUrnData());
+            MiIoBasicDevice device = miotParser.getDevice();
+            if (device == null) {
+                logger.debug("Error while creating experimental miot db file for {}.", conf.model);
+                return;
+            }
+            sendCommand(MiIoCommand.MIIO_INFO);
+            logger.info("Start experimental creation of database file based on MIOT spec for device '{}'. ",
+                    miDevice.toString());
+            sb.append("Info for ");
+            sb.append(conf.model);
+            sb.append("\r\n");
+            sb.append("Database file created:");
+            sb.append(writeDevice(device, true));
+            sb.append("\r\n");
+            sb.append(MiotParser.toJson(device));
+            sb.append("\r\n");
+            sb.append("Testing Properties:\r\n");
+            int lastCommand = -1;
+            for (MiIoBasicChannel ch : device.getDevice().getChannels()) {
+                if (ch.isMiOt() && ch.getRefresh()) {
+                    JsonObject json = new JsonObject();
+                    json.addProperty("did", ch.getProperty());
+                    json.addProperty("siid", ch.getSiid());
+                    json.addProperty("piid", ch.getPiid());
+                    String cmd = ch.getChannelCustomRefreshCommand().isBlank()
+                            ? ("get_properties[" + json.toString() + "]")
+                            : ch.getChannelCustomRefreshCommand();
+                    sb.append(ch.getChannel());
+                    sb.append(" -> ");
+                    sb.append(cmd);
+                    sb.append(" -> ");
+                    lastCommand = sendCommand(cmd);
+                    sb.append(lastCommand);
+                    sb.append(", \r\n");
+                    testChannelList.put(lastCommand, ch);
+                }
+            }
+            this.lastCommand = lastCommand;
+            sb.append("\r\n");
+            logger.info("{}", sb.toString());
+        } catch (Exception e) {
+            logger.debug("Error while creating experimental miot db file for {}", conf.model);
+            logger.info("{}", sb.toString());
+        }
+    }
+
     private LinkedHashMap<String, MiIoBasicChannel> collectProperties(@Nullable String model) {
         LinkedHashMap<String, MiIoBasicChannel> testChannelsList = new LinkedHashMap<>();
         LinkedHashSet<MiIoDevices> testDeviceList = new LinkedHashSet<>();
@@ -255,7 +315,14 @@ public class MiIoUnsupportedHandler extends MiIoAbstractHandler {
         sb.append("===================================\r\n");
         sb.append("Device Info: ");
         sb.append(info);
+        sb.append("\r\n");
+        sb.append(supportedChannelList.size());
+        sb.append(" channels with responses.\r\n");
+        int miotChannels = 0;
         for (MiIoBasicChannel ch : supportedChannelList.keySet()) {
+            if (ch.isMiOt()) {
+                miotChannels++;
+            }
             sb.append("Property: ");
             sb.append(Utils.minLengthString(ch.getProperty(), 15));
             sb.append(" Friendly Name: ");
@@ -264,15 +331,17 @@ public class MiIoUnsupportedHandler extends MiIoAbstractHandler {
             sb.append(supportedChannelList.get(ch));
             sb.append("\r\n");
         }
-        if (!supportedChannelList.isEmpty()) {
+        boolean isMiot = miotChannels > supportedChannelList.size() / 2;
+        if (!supportedChannelList.isEmpty() && !isMiot) {
             MiIoBasicDevice mbd = createBasicDeviceDb(model, new ArrayList<>(supportedChannelList.keySet()));
             sb.append("Created experimental database for your device:\r\n");
             sb.append(GSONP.toJson(mbd));
             sb.append("\r\nDatabase file saved to: ");
-            sb.append(writeDevice(mbd));
+            sb.append(writeDevice(mbd, false));
             isIdentified = false;
         } else {
-            sb.append("No supported channels found.\r\n");
+            sb.append(isMiot ? "Miot file already created. Manually remove non-functional channels.\r\n"
+                    : "No supported channels found.\r\n");
         }
         sb.append("\r\nDevice testing file saved to: ");
         sb.append(writeLog());
@@ -303,14 +372,14 @@ public class MiIoUnsupportedHandler extends MiIoAbstractHandler {
         return device;
     }
 
-    private String writeDevice(MiIoBasicDevice device) {
+    private String writeDevice(MiIoBasicDevice device, boolean miot) {
         File folder = new File(BINDING_DATABASE_PATH);
         if (!folder.exists()) {
             folder.mkdirs();
         }
-        File dataFile = new File(folder, model + "-experimental.json");
+        File dataFile = new File(folder, model + (miot ? "-miot" : "") + "-experimental.json");
         try (FileWriter writer = new FileWriter(dataFile)) {
-            writer.write(GSONP.toJson(device));
+            writer.write(miot ? MiotParser.toJson(device) : GSONP.toJson(device));
             logger.debug("Experimental database file created: {}", dataFile.getAbsolutePath());
             return dataFile.getAbsolutePath().toString();
         } catch (IOException e) {
diff --git a/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/miot/ActionDTO.java b/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/miot/ActionDTO.java
new file mode 100644 (file)
index 0000000..b302181
--- /dev/null
@@ -0,0 +1,42 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.miio.internal.miot;
+
+import java.util.List;
+
+import com.google.gson.annotations.Expose;
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * Mapping properties from json for miot device info
+ *
+ * @author Marcel Verpaalen - Initial contribution
+ */
+public class ActionDTO {
+
+    @SerializedName("iid")
+    @Expose
+    public Integer iid;
+    @SerializedName("type")
+    @Expose
+    public String type;
+    @SerializedName("description")
+    @Expose
+    public String description;
+    @SerializedName("in")
+    @Expose
+    public List<Object> in = null;
+    @SerializedName("out")
+    @Expose
+    public List<Object> out = null;
+}
diff --git a/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/miot/EventDTO.java b/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/miot/EventDTO.java
new file mode 100644 (file)
index 0000000..d3d0477
--- /dev/null
@@ -0,0 +1,39 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.miio.internal.miot;
+
+import java.util.List;
+
+import com.google.gson.annotations.Expose;
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * Mapping properties from json for miot device info
+ *
+ * @author Marcel Verpaalen - Initial contribution
+ */
+public class EventDTO {
+
+    @SerializedName("iid")
+    @Expose
+    public Integer iid;
+    @SerializedName("type")
+    @Expose
+    public String type;
+    @SerializedName("description")
+    @Expose
+    public String description;
+    @SerializedName("arguments")
+    @Expose
+    public List<Integer> arguments = null;
+}
diff --git a/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/miot/MiIoQuantiyTypesConversion.java b/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/miot/MiIoQuantiyTypesConversion.java
new file mode 100644 (file)
index 0000000..f8e73c6
--- /dev/null
@@ -0,0 +1,93 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.miio.internal.miot;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+/**
+ * Enum of the unitTypes used in the miio protocol
+ * Used to find the right {@link javax.measure.unitType} given the string of the unitType
+ *
+ * @author Marcel Verpaalen - Initial contribution
+ */
+@NonNullByDefault
+public enum MiIoQuantiyTypesConversion {
+
+    ANGLE("Angle", "arcdegrees", "radians"),
+    DENSITY("Density", "mg/m3"),
+    DIMENSIONLESS("Dimensionless", "percent", "percentage", "ppm"),
+    ELECTRIC_POTENTIAL("ElectricPotential", "volt"),
+    POWER("Power", "watt", "w"),
+    CURRENT("ElectricCurrent", "ampere", "mA"),
+    ILLUMINANCE("Illuminance", "lux"),
+    PRESSURE("Pressure", "pascal"),
+    TEMPERATURE("Temperature", "c", "celcius", "celsius", "f", "farenheith", "kelvin", "K"),
+    TIME("Time", "seconds", "minutes", "minute", "hour", "hours", "days", "Months"),
+    VOLUME("Volume", "litre", "liter", "m3");
+
+    /*
+     * available options according to miot spec:
+     * percentage
+     * Celsius degrees Celsius
+     * seconds
+     * minutes
+     * hours
+     * days
+     * kelvin temperature scale
+     * pascal Pascal (atmospheric pressure unit)
+     * arcdegrees radians (angle units)
+     * rgb RGB (color)
+     * watt (power)
+     * litre
+     * ppm ppm concentration
+     * lux Lux (illuminance)
+     * mg/m3 milligrams per cubic meter
+     */
+
+    private final String unitType;
+    private final String[] aliasses;
+
+    private static Map<String, String> aliasMap() {
+        Map<String, String> aliassesMap = new HashMap<>();
+        for (MiIoQuantiyTypesConversion miIoQuantiyType : values()) {
+            for (String alias : miIoQuantiyType.getAliasses()) {
+                aliassesMap.put(alias.toLowerCase(), miIoQuantiyType.getunitType());
+            }
+        }
+        return aliassesMap;
+    }
+
+    private MiIoQuantiyTypesConversion(String unitType, String... aliasses) {
+        this.unitType = unitType;
+        this.aliasses = aliasses;
+    }
+
+    public String getunitType() {
+        return unitType;
+    }
+
+    public String[] getAliasses() {
+        return aliasses;
+    }
+
+    public static @Nullable String getType(@Nullable String unitTypeName) {
+        if (unitTypeName != null) {
+            return aliasMap().get(unitTypeName.toLowerCase());
+        }
+        return null;
+    }
+}
diff --git a/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/miot/MiotDeviceDataDTO.java b/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/miot/MiotDeviceDataDTO.java
new file mode 100644 (file)
index 0000000..a80d4e2
--- /dev/null
@@ -0,0 +1,36 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.miio.internal.miot;
+
+import java.util.List;
+
+import com.google.gson.annotations.Expose;
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * Mapping properties from json for miot device info
+ *
+ * @author Marcel Verpaalen - Initial contribution
+ */
+public class MiotDeviceDataDTO {
+
+    @SerializedName("type")
+    @Expose
+    public String type;
+    @SerializedName("description")
+    @Expose
+    public String description;
+    @SerializedName("services")
+    @Expose
+    public List<ServiceDTO> services = null;
+}
diff --git a/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/miot/MiotParseException.java b/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/miot/MiotParseException.java
new file mode 100644 (file)
index 0000000..7cfcf20
--- /dev/null
@@ -0,0 +1,41 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.miio.internal.miot;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+/**
+ * Will be thrown for cloud errors
+ *
+ * @author Marcel Verpaalen - Initial contribution
+ */
+@NonNullByDefault
+public class MiotParseException extends Exception {
+    /**
+     * required variable to avoid IncorrectMultilineIndexException warning
+     */
+    private static final long serialVersionUID = -1280858607995252322L;
+
+    public MiotParseException() {
+        super();
+    }
+
+    public MiotParseException(@Nullable String message) {
+        super(message);
+    }
+
+    public MiotParseException(@Nullable String message, @Nullable Exception e) {
+        super(message, e);
+    }
+}
diff --git a/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/miot/MiotParser.java b/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/miot/MiotParser.java
new file mode 100644 (file)
index 0000000..b5051a8
--- /dev/null
@@ -0,0 +1,454 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+
+package org.openhab.binding.miio.internal.miot;
+
+import java.io.FileNotFoundException;
+import java.io.PrintWriter;
+import java.math.BigDecimal;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.stream.Collectors;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.jetty.client.HttpClient;
+import org.eclipse.jetty.client.api.ContentResponse;
+import org.openhab.binding.miio.internal.MiIoCommand;
+import org.openhab.binding.miio.internal.basic.CommandParameterType;
+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.MiIoDeviceAction;
+import org.openhab.binding.miio.internal.basic.MiIoDeviceActionCondition;
+import org.openhab.binding.miio.internal.basic.OptionsValueListDTO;
+import org.openhab.binding.miio.internal.basic.StateDescriptionDTO;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonParseException;
+import com.google.gson.JsonParser;
+
+/**
+ * Support creation of the miot db files
+ * based on the the online miot spec files
+ *
+ *
+ * @author Marcel Verpaalen - Initial contribution
+ */
+@NonNullByDefault
+public class MiotParser {
+    private final Logger logger = LoggerFactory.getLogger(MiotParser.class);
+
+    private static final String BASEURL = "http://miot-spec.org/miot-spec-v2/";
+    private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();
+    private static final boolean SKIP_SIID_1 = true;
+
+    private String model;
+    private @Nullable String urn;
+    private @Nullable JsonElement urnData;
+    private @Nullable MiIoBasicDevice device;
+
+    public MiotParser(String model) {
+        this.model = model;
+    }
+
+    public static MiotParser parse(String model, HttpClient httpClient) throws MiotParseException {
+        MiotParser miotParser = new MiotParser(model);
+        try {
+            String urn = miotParser.getURN(model, httpClient);
+            if (urn == null) {
+                throw new MiotParseException("Device not found in in miot specs : " + model);
+            }
+            JsonElement urnData = miotParser.getUrnData(urn, httpClient);
+            miotParser.getDevice(urnData);
+            return miotParser;
+        } catch (Exception e) {
+            throw new MiotParseException("Error parsing miot data: " + e.getMessage(), e);
+        }
+    }
+
+    /**
+     * Outputs the device json file touched up so the format matches the regular OH standard formatting
+     *
+     * @param device
+     * @return
+     */
+    static public String toJson(MiIoBasicDevice device) {
+        String usersJson = GSON.toJson(device);
+        usersJson = usersJson.replace(".0,\n", ",\n");
+        usersJson = usersJson.replace("\n", "\r\n").replace("  ", "\t");
+        return usersJson;
+    }
+
+    public void writeDevice(String path, MiIoBasicDevice device) {
+        try (PrintWriter out = new PrintWriter(path)) {
+            out.println(toJson(device));
+            logger.info("Database file created:{}", path);
+        } catch (FileNotFoundException e) {
+            logger.info("Error writing file: {}", e.getMessage());
+        }
+    }
+
+    public MiIoBasicDevice getDevice(JsonElement urnData) throws MiotParseException {
+        Set<String> unknownUnits = new HashSet<>();
+        Map<ActionDTO, ServiceDTO> deviceActions = new LinkedHashMap<>();
+        StringBuilder channelConfigText = new StringBuilder("Suggested additional channelType \r\n");
+
+        StringBuilder actionText = new StringBuilder("Manual actions for execution\r\n");
+
+        MiIoBasicDevice device = new MiIoBasicDevice();
+        DeviceMapping deviceMapping = new DeviceMapping();
+        MiotDeviceDataDTO miotDevice = GSON.fromJson(urnData, MiotDeviceDataDTO.class);
+        if (miotDevice == null) {
+            throw new MiotParseException("Error parsing miot data: null");
+        }
+        List<MiIoBasicChannel> miIoBasicChannels = new ArrayList<>();
+        deviceMapping.setPropertyMethod(MiIoCommand.GET_PROPERTIES.getCommand());
+        deviceMapping.setMaxProperties(1);
+        deviceMapping.setExperimental(true);
+        deviceMapping.setId(Arrays.asList(new String[] { model }));
+        Set<String> propCheck = new HashSet<>();
+
+        for (ServiceDTO service : miotDevice.services) {
+            String serviceId = service.type.substring(service.type.indexOf("service:")).split(":")[1];
+            logger.info("SID: {}, description: {}, identifier: {}", service.siid, service.description, serviceId);
+
+            if (service.properties != null) {
+                for (PropertyDTO property : service.properties) {
+                    String propertyId = property.type.substring(property.type.indexOf("property:")).split(":")[1];
+                    logger.info("siid: {}, description: {}, piid: {}, description: {}, identifier: {}", service.siid,
+                            service.description, property.piid, property.description, propertyId);
+                    if (service.siid == 1 && SKIP_SIID_1) {
+                        continue;
+                    }
+                    if (property.access.contains("read") || property.access.contains("write")) {
+                        MiIoBasicChannel miIoBasicChannel = new MiIoBasicChannel();
+                        miIoBasicChannel
+                                .setFriendlyName((isPureAscii(service.description) && !service.description.isBlank()
+                                        ? captializedName(service.description)
+                                        : captializedName(serviceId))
+                                        + " - "
+                                        + (isPureAscii(property.description) && !property.description.isBlank()
+                                                ? captializedName(property.description)
+                                                : captializedName(propertyId)));
+                        miIoBasicChannel.setSiid(service.siid);
+                        miIoBasicChannel.setPiid(property.piid);
+                        // avoid duplicates and make camel case and avoid invalid channel names
+                        String chanId = propertyId.replace(" ", "").replace(".", "_").replace("-", "_");
+
+                        int cnt = 0;
+                        while (propCheck.contains(chanId + Integer.toString(cnt))) {
+                            cnt++;
+                        }
+                        propCheck.add(chanId.concat(Integer.toString(cnt)));
+                        if (cnt > 0) {
+                            chanId = chanId.concat(Integer.toString(cnt));
+                            propertyId = propertyId.concat(Integer.toString(cnt));
+                            logger.warn("duplicate for property:{} - {} ({}", chanId, property.description, cnt);
+                        }
+                        if (property.unit != null && !property.unit.isBlank()) {
+                            if (!property.unit.contains("none")) {
+                                miIoBasicChannel.setUnit(property.unit);
+                            }
+                        }
+                        miIoBasicChannel.setProperty(propertyId);
+                        miIoBasicChannel.setChannel(chanId);
+                        switch (property.format) {
+                            case "bool":
+                                miIoBasicChannel.setType("Switch");
+                                break;
+                            case "uint8":
+                            case "uint16":
+                            case "uint32":
+                            case "int8":
+                            case "int16":
+                            case "int32":
+                            case "int64":
+                            case "float":
+                                StateDescriptionDTO stateDescription = miIoBasicChannel.getStateDescription();
+                                int decimals = -1;
+                                String unit = "";
+                                if (stateDescription == null) {
+                                    stateDescription = new StateDescriptionDTO();
+                                }
+                                String type = MiIoQuantiyTypesConversion.getType(property.unit);
+                                if (type != null) {
+                                    miIoBasicChannel.setType("Number" + ":" + type);
+                                    unit = " %unit%";
+                                    decimals = property.format.contentEquals("float") ? 1 : 0;
+
+                                } else {
+                                    miIoBasicChannel.setType("Number");
+                                    decimals = property.format.contentEquals("uint8") ? 0 : 1;
+                                    if (property.unit != null) {
+                                        unknownUnits.add(property.unit);
+                                    }
+                                }
+                                if (property.valueRange != null && property.valueRange.size() == 3) {
+                                    stateDescription
+                                            .setMinimum(BigDecimal.valueOf(property.valueRange.get(0).doubleValue()));
+                                    stateDescription
+                                            .setMaximum(BigDecimal.valueOf(property.valueRange.get(1).doubleValue()));
+
+                                    double step = property.valueRange.get(2).doubleValue();
+                                    if (step != 0) {
+                                        stateDescription.setStep(BigDecimal.valueOf(step));
+                                        if (step >= 1) {
+                                            decimals = 0;
+                                        }
+                                    }
+                                }
+                                if (decimals > -1) {
+                                    stateDescription.setPattern("%." + Integer.toString(decimals) + "f" + unit);
+                                }
+                                miIoBasicChannel.setStateDescription(stateDescription);
+                                break;
+                            case "string":
+                                miIoBasicChannel.setType("String");
+                                break;
+                            case "hex":
+                                miIoBasicChannel.setType("String");
+                                logger.info("no type mapping implemented for {}", property.format);
+                                break;
+                            default:
+                                miIoBasicChannel.setType("String");
+                                logger.info("no type mapping for {}", property.format);
+                                break;
+                        }
+                        miIoBasicChannel.setRefresh(property.access.contains("read"));
+                        // add option values
+                        if (property.valueList != null && property.valueList.size() > 0) {
+                            StateDescriptionDTO stateDescription = miIoBasicChannel.getStateDescription();
+                            if (stateDescription == null) {
+                                stateDescription = new StateDescriptionDTO();
+                            }
+                            stateDescription.setPattern(null);
+                            List<OptionsValueListDTO> channeloptions = new LinkedList<>();
+                            for (OptionsValueDescriptionsListDTO miotOption : property.valueList) {
+                                // miIoBasicChannel.setValueList(property.valueList);
+                                OptionsValueListDTO basicOption = new OptionsValueListDTO();
+                                basicOption.setLabel(miotOption.getDescription());
+                                basicOption.setValue(String.valueOf(miotOption.value));
+                                channeloptions.add(basicOption);
+                            }
+                            stateDescription.setOptions(channeloptions);
+                            miIoBasicChannel.setStateDescription(stateDescription);
+
+                            // Add the mapping for the readme
+                            StringBuilder mapping = new StringBuilder();
+                            mapping.append("Value mapping [");
+
+                            for (OptionsValueDescriptionsListDTO valueMap : property.valueList) {
+                                mapping.append(String.format("\"%d\"=\"%s\",", valueMap.value, valueMap.description));
+                            }
+                            mapping.deleteCharAt(mapping.length() - 1);
+                            mapping.append("]");
+                            miIoBasicChannel.setReadmeComment(mapping.toString());
+                        }
+                        if (property.access.contains("write")) {
+                            List<MiIoDeviceAction> miIoDeviceActions = new ArrayList<>();
+                            MiIoDeviceAction action = new MiIoDeviceAction();
+                            action.setCommand("set_properties");
+                            switch (property.format) {
+                                case "bool":
+                                    action.setparameterType(CommandParameterType.ONOFFBOOL);
+                                    break;
+                                case "uint8":
+                                case "int32":
+                                case "float":
+                                    action.setparameterType(CommandParameterType.NUMBER);
+                                    break;
+                                case "string":
+                                    action.setparameterType(CommandParameterType.STRING);
+
+                                    break;
+                                default:
+                                    action.setparameterType(CommandParameterType.STRING);
+                                    break;
+                            }
+                            miIoDeviceActions.add(action);
+                            miIoBasicChannel.setActions(miIoDeviceActions);
+                        } else {
+                            StateDescriptionDTO stateDescription = miIoBasicChannel.getStateDescription();
+                            if (stateDescription == null) {
+                                stateDescription = new StateDescriptionDTO();
+                            }
+                            stateDescription.setReadOnly(true);
+                            miIoBasicChannel.setStateDescription(stateDescription);
+                        }
+                        miIoBasicChannels.add(miIoBasicChannel);
+                    } else {
+                        logger.info("No reading siid: {}, description: {}, piid: {},description: {}", service.siid,
+                                service.description, property.piid, property.description);
+                    }
+                }
+                if (service.actions != null) {
+                    for (ActionDTO action : service.actions) {
+                        deviceActions.put(action, service);
+                        String actionId = action.type.substring(action.type.indexOf("action:")).split(":")[1];
+                        actionText.append("`action{");
+                        actionText.append(String.format("\"did\":\"%s-%s\",", serviceId, actionId));
+                        actionText.append(String.format("\"siid\":%d,", service.siid));
+                        actionText.append(String.format("\"aiid\":%d,", action.iid));
+                        actionText.append(String.format("\"in\":%s", action.in));
+                        actionText.append("}`\r\n");
+                    }
+
+                }
+            } else {
+                logger.info("SID: {}, description: {} has no identified properties", service.siid, service.description);
+            }
+        }
+        if (!deviceActions.isEmpty()) {
+            miIoBasicChannels.add(0, actionChannel(deviceActions));
+        }
+        deviceMapping.setChannels(miIoBasicChannels);
+        device.setDevice(deviceMapping);
+        if (actionText.length() > 35) {
+            deviceMapping.setReadmeComment(
+                    "Identified " + actionText.toString().replace("Manual", "manual").replace("\r\n", "<br />")
+                            + "Please test and feedback if they are working to they can be linked to a channel.");
+        }
+        logger.info(channelConfigText.toString());
+        if (actionText.length() > 30) {
+            logger.info("{}", actionText);
+        } else {
+            logger.info("No actions defined for device");
+        }
+        unknownUnits.remove("none");
+        if (!unknownUnits.isEmpty()) {
+            logger.info("New units identified (inform developer): {}", String.join(", ", unknownUnits));
+        }
+
+        this.device = device;
+        return device;
+    }
+
+    private MiIoBasicChannel actionChannel(Map<ActionDTO, ServiceDTO> deviceActions) {
+        MiIoBasicChannel miIoBasicChannel = new MiIoBasicChannel();
+        if (!deviceActions.isEmpty()) {
+            miIoBasicChannel.setProperty("");
+            miIoBasicChannel.setChannel("actions");
+            miIoBasicChannel.setFriendlyName("Actions");
+            miIoBasicChannel.setType("String");
+            miIoBasicChannel.setRefresh(false);
+            StateDescriptionDTO stateDescription = new StateDescriptionDTO();
+            List<OptionsValueListDTO> options = new LinkedList<>();
+            List<MiIoDeviceAction> miIoDeviceActions = new LinkedList<>();
+            deviceActions.forEach((action, service) -> {
+                String actionId = action.type.substring(action.type.indexOf("action:")).split(":")[1];
+                String serviceId = service.type.substring(service.type.indexOf("service:")).split(":")[1];
+                String description = String.format("%s-%s", serviceId, actionId);
+                OptionsValueListDTO option = new OptionsValueListDTO();
+                option.label = captializedName(description);
+                option.value = description;
+                options.add(option);
+                MiIoDeviceAction miIoDeviceAction = new MiIoDeviceAction();
+                miIoDeviceAction.setCommand("action");
+                miIoDeviceAction.setparameterType(CommandParameterType.EMPTY);
+                miIoDeviceAction.setSiid(service.siid);
+                miIoDeviceAction.setAiid(action.iid);
+                if (!action.in.isEmpty()) {
+                    miIoDeviceAction.setParameters(JsonParser.parseString(GSON.toJson(action.in)).getAsJsonArray());
+                    miIoDeviceAction.setparameterType("fromparameter");
+                }
+                MiIoDeviceActionCondition miIoDeviceActionCondition = new MiIoDeviceActionCondition();
+                String json = String.format("[{ \"matchValue\"=\"%s\"}]", description);
+                miIoDeviceActionCondition.setName("matchValue");
+                miIoDeviceActionCondition.setParameters(JsonParser.parseString(json).getAsJsonArray());
+                miIoDeviceAction.setCondition(miIoDeviceActionCondition);
+                miIoDeviceActions.add(miIoDeviceAction);
+            });
+            stateDescription.setOptions(options);
+            miIoBasicChannel.setStateDescription(stateDescription);
+            miIoBasicChannel.setActions(miIoDeviceActions);
+        }
+        return miIoBasicChannel;
+    }
+
+    private static String captializedName(String name) {
+        if (name.isEmpty()) {
+            return name;
+        }
+        String str = name.replace("-", " ").replace(".", " ");
+        return Arrays.stream(str.split("\\s+")).map(t -> t.substring(0, 1).toUpperCase() + t.substring(1))
+                .collect(Collectors.joining(" "));
+    }
+
+    public static boolean isPureAscii(String v) {
+        return StandardCharsets.US_ASCII.newEncoder().canEncode(v);
+    }
+
+    private JsonElement getUrnData(String urn, HttpClient httpClient)
+            throws InterruptedException, TimeoutException, ExecutionException, JsonParseException {
+        ContentResponse response;
+        String urlStr = BASEURL + "instance?type=" + urn;
+        logger.info("miot info: {}", urlStr);
+        response = httpClient.newRequest(urlStr).timeout(15, TimeUnit.SECONDS).send();
+        JsonElement json = JsonParser.parseString(response.getContentAsString());
+        this.urnData = json;
+        return json;
+    }
+
+    private @Nullable String getURN(String model, HttpClient httpClient) {
+        ContentResponse response;
+        try {
+            response = httpClient.newRequest(BASEURL + "instances?status=released").timeout(15, TimeUnit.SECONDS)
+                    .send();
+            JsonElement json = JsonParser.parseString(response.getContentAsString());
+            UrnsDTO data = GSON.fromJson(json, UrnsDTO.class);
+            for (ModelUrnsDTO device : data.getInstances()) {
+                if (device.getModel().contentEquals(model)) {
+                    this.urn = device.getType();
+                    return device.getType();
+                }
+            }
+        } catch (InterruptedException | TimeoutException | ExecutionException e) {
+            logger.debug("Failed downloading models: {}", e.getMessage());
+        } catch (JsonParseException e) {
+            logger.debug("Failed parsing downloading models: {}", e.getMessage());
+        }
+
+        return null;
+    }
+
+    public String getModel() {
+        return model;
+    }
+
+    public @Nullable String getUrn() {
+        return urn;
+    }
+
+    public @Nullable JsonElement getUrnData() {
+        return urnData;
+    }
+
+    public @Nullable MiIoBasicDevice getDevice() {
+        return device;
+    }
+}
diff --git a/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/miot/ModelUrnsDTO.java b/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/miot/ModelUrnsDTO.java
new file mode 100644 (file)
index 0000000..609c80c
--- /dev/null
@@ -0,0 +1,63 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+
+package org.openhab.binding.miio.internal.miot;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+import com.google.gson.annotations.Expose;
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * Model Urns DTO for miot spec file.
+ *
+ * To read http://miot-spec.org/miot-spec-v2/instances?status=released
+ *
+ * @author Marcel Verpaalen - Initial contribution
+ */
+@NonNullByDefault
+public class ModelUrnsDTO {
+    @SerializedName("model")
+    @Expose
+    private String model = "";
+    @SerializedName("version")
+    @Expose
+    private Integer version = 0;
+    @SerializedName("type")
+    @Expose
+    private String type = "";
+
+    public String getModel() {
+        return model;
+    }
+
+    public void setModel(String model) {
+        this.model = model;
+    }
+
+    public Integer getVersion() {
+        return version;
+    }
+
+    public void setVersion(Integer version) {
+        this.version = version;
+    }
+
+    public String getType() {
+        return type;
+    }
+
+    public void setType(String type) {
+        this.type = type;
+    }
+}
diff --git a/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/miot/OptionsValueDescriptionsListDTO.java b/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/miot/OptionsValueDescriptionsListDTO.java
new file mode 100644 (file)
index 0000000..70c7598
--- /dev/null
@@ -0,0 +1,55 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.miio.internal.miot;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+import com.google.gson.annotations.Expose;
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * Mapping properties from json for miot device info
+ *
+ * @author Marcel Verpaalen - Initial contribution
+ */
+@NonNullByDefault
+public class OptionsValueDescriptionsListDTO {
+
+    @SerializedName("value")
+    @Expose
+    @Nullable
+    public Integer value;
+    @SerializedName("description")
+    @Expose
+    @Nullable
+    public String description;
+
+    public int getValue() {
+        final Integer val = this.value;
+        return val != null ? val.intValue() : 0;
+    }
+
+    public void setValue(Integer value) {
+        this.value = value;
+    }
+
+    public String getDescription() {
+        final String description = this.description;
+        return description != null ? description : "";
+    }
+
+    public void setDescription(String description) {
+        this.description = description;
+    }
+}
diff --git a/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/miot/PropertyDTO.java b/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/miot/PropertyDTO.java
new file mode 100644 (file)
index 0000000..f978e61
--- /dev/null
@@ -0,0 +1,51 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.miio.internal.miot;
+
+import java.util.List;
+
+import com.google.gson.annotations.Expose;
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * Mapping properties from json for miot device info
+ *
+ * @author Marcel Verpaalen - Initial contribution
+ */
+public class PropertyDTO {
+
+    @SerializedName("iid")
+    @Expose
+    public Integer piid;
+    @SerializedName("type")
+    @Expose
+    public String type;
+    @SerializedName("description")
+    @Expose
+    public String description;
+    @SerializedName("format")
+    @Expose
+    public String format;
+    @SerializedName("access")
+    @Expose
+    public List<String> access = null;
+    @SerializedName("value-list")
+    @Expose
+    public List<OptionsValueDescriptionsListDTO> valueList = null;
+    @SerializedName("value-range")
+    @Expose
+    public List<Integer> valueRange = null;
+    @SerializedName("unit")
+    @Expose
+    public String unit;
+}
diff --git a/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/miot/ServiceDTO.java b/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/miot/ServiceDTO.java
new file mode 100644 (file)
index 0000000..1be53c9
--- /dev/null
@@ -0,0 +1,45 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.miio.internal.miot;
+
+import java.util.List;
+
+import com.google.gson.annotations.Expose;
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * Mapping properties from json for miot device info
+ *
+ * @author Marcel Verpaalen - Initial contribution
+ */
+public class ServiceDTO {
+
+    @SerializedName("iid")
+    @Expose
+    public Integer siid;
+    @SerializedName("type")
+    @Expose
+    public String type;
+    @SerializedName("description")
+    @Expose
+    public String description;
+    @SerializedName("properties")
+    @Expose
+    public List<PropertyDTO> properties = null;
+    @SerializedName("actions")
+    @Expose
+    public List<ActionDTO> actions = null;
+    @SerializedName("events")
+    @Expose
+    public List<EventDTO> events = null;
+}
diff --git a/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/miot/UrnsDTO.java b/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/miot/UrnsDTO.java
new file mode 100644 (file)
index 0000000..1451f00
--- /dev/null
@@ -0,0 +1,40 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+
+package org.openhab.binding.miio.internal.miot;
+
+import java.util.Collections;
+import java.util.List;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+import com.google.gson.annotations.Expose;
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * Urns DTO for miot spec file.
+ *
+ * To read http://miot-spec.org/miot-spec-v2/instances?status=released
+ *
+ * @author Marcel Verpaalen - Initial contribution
+ */
+@NonNullByDefault
+public class UrnsDTO {
+    @SerializedName("instances")
+    @Expose
+    private List<ModelUrnsDTO> instances = Collections.emptyList();
+
+    public List<ModelUrnsDTO> getInstances() {
+        return instances;
+    }
+}
index 5ff01a7c3447cc411ec90bb561933ca0c750a2e0..218b4553f4c5b40f93b9fead48bb4aa4cbef82e2 100644 (file)
                        <channel id="commands" typeId="commands"/>
                        <channel id="rpc" typeId="rpc"/>
                        <channel id="testcommands" typeId="testcommands"/>
+                       <channel id="testmiot" typeId="testmiot"/>
+
                </channels>
        </channel-group-type>
 
        <channel-type id="testcommands">
                <item-type>Switch</item-type>
                <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>
+               <description>Execute test for all known properties to find channels supported by your device. Check your log, share
+                       your results.</description>
+               <category>settings</category>
+       </channel-type>
+
+       <channel-type id="testmiot">
+               <item-type>Switch</item-type>
+               <label>(experimental) Create experimental support for new MIOT protocol devices</label>
+               <description>Create experimental support for MIOT protocol devices based on the online specification. Check your log,
+                       share your results.</description>
                <category>settings</category>
        </channel-type>
+
 </thing:thing-descriptions>
index 98b4ef21279167e81a69ae9fca830feacb942c5b..df25b193ecab1735cc7e7f09b5f5afed6363b603 100644 (file)
                                "actions": [],
                                "readmeComment": "Value mapping `[\"1\"\u003d\"Charging\",\"2\"\u003d\"Not Charging\",\"4\"\u003d\"Charging\",\"5\"\u003d\"Go Charging\"]`"
                        },
+                       {
+                               "property": "water-mode",
+                               "siid": 18,
+                               "piid": 20,
+                               "friendlyName": "Water Mode",
+                               "channel": "water-mode",
+                               "type": "Number",
+                               "stateDescription": {
+                                       "readOnly": true,
+                                       "options": [
+                                               {
+                                                       "value": "1",
+                                                       "label": "Low"
+                                               },
+                                               {
+                                                       "value": "2",
+                                                       "label": "Medium"
+                                               },
+                                               {
+                                                       "value": "4",
+                                                       "label": "High"
+                                               }
+                                       ]
+                               },
+                               "refresh": true,
+                               "actions": [
+                                       {
+                                               "command": "set_properties",
+                                               "parameterType": "NUMBER"
+                                       }
+                               ],
+                               "readmeComment": "Value mapping [\"1\"\u003d\"Low\",\"2\"\u003d\"Medium\",\"4\"\u003d\"High\"]"
+                       },
                        {
                                "property": "fault",
                                "siid": 3,
diff --git a/bundles/org.openhab.binding.miio/src/test/java/org/openhab/binding/miio/internal/MiIoQuantiyTypesConversionTest.java b/bundles/org.openhab.binding.miio/src/test/java/org/openhab/binding/miio/internal/MiIoQuantiyTypesConversionTest.java
new file mode 100644 (file)
index 0000000..4e57b16
--- /dev/null
@@ -0,0 +1,52 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.miio.internal;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.jupiter.api.Test;
+import org.openhab.binding.miio.internal.miot.MiIoQuantiyTypesConversion;
+
+/**
+ * Test case for {@link MiIoQuantiyTypesConversion}
+ *
+ * @author Marcel Verpaalen - Initial contribution
+ *
+ */
+@NonNullByDefault
+public class MiIoQuantiyTypesConversionTest {
+
+    @Test
+    public void UnknownUnitTest() {
+
+        String unitName = "some none existent unit";
+        assertNull(MiIoQuantiyTypesConversion.getType(unitName));
+    }
+
+    @Test
+    public void NullUnitTest() {
+        String unitName = null;
+        assertNull(MiIoQuantiyTypesConversion.getType(unitName));
+    }
+
+    @Test
+    public void regularsUnitTest() {
+
+        String unitName = "minute";
+        assertEquals("Time", MiIoQuantiyTypesConversion.getType(unitName));
+
+        unitName = "Minute";
+        assertEquals("Time", MiIoQuantiyTypesConversion.getType(unitName));
+    }
+}
diff --git a/bundles/org.openhab.binding.miio/src/test/java/org/openhab/binding/miio/internal/MiotJsonFileCreator.java b/bundles/org.openhab.binding.miio/src/test/java/org/openhab/binding/miio/internal/MiotJsonFileCreator.java
new file mode 100644 (file)
index 0000000..d245c59
--- /dev/null
@@ -0,0 +1,135 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+
+package org.openhab.binding.miio.internal;
+
+import java.io.File;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.Map.Entry;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jetty.client.HttpClient;
+import org.eclipse.jetty.util.ssl.SslContextFactory;
+import org.junit.jupiter.api.Disabled;
+import org.openhab.binding.miio.internal.basic.MiIoBasicDevice;
+import org.openhab.binding.miio.internal.miot.MiotParseException;
+import org.openhab.binding.miio.internal.miot.MiotParser;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+
+/**
+ * Supporting tool for creation of the json database files for miot devices
+ * *
+ * Run in IDE with 'run as java application' or run in command line as:
+ * mvn exec:java -Dexec.mainClass="org.openhab.binding.miio.internal.MiotJsonFileCreator" -Dexec.classpathScope="test"
+ * -Dexec.args="zhimi.humidifier.ca4"
+ *
+ * The argument is the model string to create the database file for.
+ * If the digit at the end of the model is omitted, it will try a range of devices
+ *
+ * @author Marcel Verpaalen - Initial contribution
+ *
+ */
+@NonNullByDefault
+public class MiotJsonFileCreator {
+    private static final Logger LOGGER = LoggerFactory.getLogger(MiotJsonFileCreator.class);
+    private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();
+
+    private static final String BASEDIR = "./src/main/resources/database/";
+    private static final String FILENAME_EXTENSION = "-miot.json";
+    private static final boolean OVERWRITE_EXISTING_DATABASE_FILE = false;
+
+    @Disabled
+    public static void main(String[] args) {
+
+        LinkedHashMap<String, String> checksums = new LinkedHashMap<>();
+        LinkedHashSet<String> models = new LinkedHashSet<>();
+        if (args.length > 0) {
+            models.add(args[0]);
+        }
+
+        String m = models.isEmpty() ? "" : (String) models.toArray()[0];
+        boolean scan = m.isEmpty() ? false : !Character.isDigit(m.charAt(m.length() - 1));
+        if (scan) {
+            for (int i = 1; i <= 12; i++) {
+                models.add(models.toArray()[0] + String.valueOf(i));
+            }
+        }
+
+        MiotParser miotParser;
+        for (String model : models) {
+            LOGGER.info("Processing: {}", model);
+            HttpClient httpClient = null;
+            try {
+                httpClient = new HttpClient(new SslContextFactory.Client());
+                httpClient.setFollowRedirects(false);
+                httpClient.start();
+                miotParser = MiotParser.parse(model, httpClient);
+                LOGGER.info("urn: ", miotParser.getUrn());
+                LOGGER.info("{}", miotParser.getUrnData());
+                MiIoBasicDevice device = miotParser.getDevice();
+                if (device != null) {
+                    LOGGER.info("Device: {}", device);
+                    String fileName = String.format("%s%s%s", BASEDIR, model, FILENAME_EXTENSION);
+                    if (!OVERWRITE_EXISTING_DATABASE_FILE) {
+                        int counter = 0;
+                        while (new File(fileName).isFile()) {
+                            fileName = String.format("%s%s-%d%s", BASEDIR, model, counter, FILENAME_EXTENSION);
+                            counter++;
+                        }
+                    }
+                    miotParser.writeDevice(fileName, device);
+                    String channelsJson = GSON.toJson(device.getDevice().getChannels()).toString();
+                    checksums.put(model, checksumMD5(channelsJson));
+                }
+                LOGGER.info("Finished");
+            } catch (MiotParseException e) {
+                LOGGER.info("Error processing model {}: {}", model, e.getMessage());
+            } catch (Exception e) {
+                LOGGER.info("Failed to initiate http Client: {}", e.getMessage());
+            } finally {
+                try {
+                    if (httpClient != null && httpClient.isRunning()) {
+                        httpClient.stop();
+                    }
+                } catch (Exception e) {
+                    // ignore
+                }
+            }
+        }
+        StringBuilder sb = new StringBuilder();
+        for (Entry<String, String> ch : checksums.entrySet()) {
+            sb.append(ch.getValue());
+            sb.append(" --> ");
+            sb.append(ch.getKey());
+            sb.append("\r\n");
+        }
+        LOGGER.info("Checksums for device comparisons\r\n{}", sb);
+    }
+
+    public static String checksumMD5(String input) {
+        try {
+            MessageDigest md = MessageDigest.getInstance("MD5");
+            md.update(input.getBytes());
+            return Utils.getHex(md.digest());
+        } catch (NoSuchAlgorithmException e) {
+            return "No MD5";
+        }
+    }
+}