[miio] Automatic create experimental support for (unsupported) miot devices (#11149)
Signed-off-by: Marcel Verpaalen <marcel@verpaalen.com>
This commit is contained in:
parent
b0cbefdf7a
commit
2fb86d7138
@ -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";
|
||||
|
||||
@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@ -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"),
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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;
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -26,13 +26,25 @@
|
||||
<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>
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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));
|
||||
}
|
||||
}
|
||||
@ -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";
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user