diff --git a/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/MiIoBindingConstants.java b/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/MiIoBindingConstants.java index 079b41a56..4a887e7f7 100644 --- a/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/MiIoBindingConstants.java +++ b/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/MiIoBindingConstants.java @@ -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"; diff --git a/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/MiIoHandlerFactory.java b/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/MiIoHandlerFactory.java index 47896deed..523efc256 100644 --- a/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/MiIoHandlerFactory.java +++ b/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/MiIoHandlerFactory.java @@ -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 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()); } } diff --git a/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/MiIoQuantiyTypes.java b/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/MiIoQuantiyTypes.java index 7ebf9c40c..2cea22614 100644 --- a/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/MiIoQuantiyTypes.java +++ b/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/MiIoQuantiyTypes.java @@ -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"), diff --git a/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/basic/StateDescriptionDTO.java b/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/basic/StateDescriptionDTO.java index 4d8e704ad..053331cd2 100644 --- a/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/basic/StateDescriptionDTO.java +++ b/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/basic/StateDescriptionDTO.java @@ -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 options) { + public void setOptions(@Nullable List options) { this.options = options; } } diff --git a/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/handler/MiIoUnsupportedHandler.java b/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/handler/MiIoUnsupportedHandler.java index 20be68f71..21e0dbdb9 100644 --- a/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/handler/MiIoUnsupportedHandler.java +++ b/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/handler/MiIoUnsupportedHandler.java @@ -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 collectProperties(@Nullable String model) { LinkedHashMap testChannelsList = new LinkedHashMap<>(); LinkedHashSet 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 index 000000000..b3021817c --- /dev/null +++ b/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/miot/ActionDTO.java @@ -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 in = null; + @SerializedName("out") + @Expose + public List 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 index 000000000..d3d0477d2 --- /dev/null +++ b/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/miot/EventDTO.java @@ -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 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 index 000000000..f8e73c6ad --- /dev/null +++ b/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/miot/MiIoQuantiyTypesConversion.java @@ -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 aliasMap() { + Map 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 index 000000000..a80d4e2c1 --- /dev/null +++ b/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/miot/MiotDeviceDataDTO.java @@ -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 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 index 000000000..7cfcf20c0 --- /dev/null +++ b/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/miot/MiotParseException.java @@ -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 index 000000000..b5051a819 --- /dev/null +++ b/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/miot/MiotParser.java @@ -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 unknownUnits = new HashSet<>(); + Map 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 miIoBasicChannels = new ArrayList<>(); + deviceMapping.setPropertyMethod(MiIoCommand.GET_PROPERTIES.getCommand()); + deviceMapping.setMaxProperties(1); + deviceMapping.setExperimental(true); + deviceMapping.setId(Arrays.asList(new String[] { model })); + Set 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 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 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", "
") + + "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 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 options = new LinkedList<>(); + List 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 index 000000000..609c80c39 --- /dev/null +++ b/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/miot/ModelUrnsDTO.java @@ -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 index 000000000..70c759855 --- /dev/null +++ b/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/miot/OptionsValueDescriptionsListDTO.java @@ -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 index 000000000..f978e6160 --- /dev/null +++ b/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/miot/PropertyDTO.java @@ -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 access = null; + @SerializedName("value-list") + @Expose + public List valueList = null; + @SerializedName("value-range") + @Expose + public List 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 index 000000000..1be53c997 --- /dev/null +++ b/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/miot/ServiceDTO.java @@ -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 properties = null; + @SerializedName("actions") + @Expose + public List actions = null; + @SerializedName("events") + @Expose + public List 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 index 000000000..1451f00e3 --- /dev/null +++ b/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/miot/UrnsDTO.java @@ -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 instances = Collections.emptyList(); + + public List getInstances() { + return instances; + } +} diff --git a/bundles/org.openhab.binding.miio/src/main/resources/OH-INF/thing/unsupportedThing.xml b/bundles/org.openhab.binding.miio/src/main/resources/OH-INF/thing/unsupportedThing.xml index 5ff01a7c3..218b4553f 100644 --- a/bundles/org.openhab.binding.miio/src/main/resources/OH-INF/thing/unsupportedThing.xml +++ b/bundles/org.openhab.binding.miio/src/main/resources/OH-INF/thing/unsupportedThing.xml @@ -26,13 +26,25 @@ + + Switch - (experimental)Execute test for all known properties to find channels supported by your device. + Execute test for all known properties to find channels supported by your device. Check your log, share + your results. settings + + + Switch + + Create experimental support for MIOT protocol devices based on the online specification. Check your log, + share your results. + settings + + diff --git a/bundles/org.openhab.binding.miio/src/main/resources/database/dreame.vacuum.mc1808-miot.json b/bundles/org.openhab.binding.miio/src/main/resources/database/dreame.vacuum.mc1808-miot.json index 98b4ef212..df25b193e 100644 --- a/bundles/org.openhab.binding.miio/src/main/resources/database/dreame.vacuum.mc1808-miot.json +++ b/bundles/org.openhab.binding.miio/src/main/resources/database/dreame.vacuum.mc1808-miot.json @@ -169,6 +169,39 @@ "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 index 000000000..4e57b16c0 --- /dev/null +++ b/bundles/org.openhab.binding.miio/src/test/java/org/openhab/binding/miio/internal/MiIoQuantiyTypesConversionTest.java @@ -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 index 000000000..d245c5952 --- /dev/null +++ b/bundles/org.openhab.binding.miio/src/test/java/org/openhab/binding/miio/internal/MiotJsonFileCreator.java @@ -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 checksums = new LinkedHashMap<>(); + LinkedHashSet 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 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"; + } + } +}