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";
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;
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;
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;
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());
}
}
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"),
return minimum;
}
- public void setMinimum(BigDecimal minimum) {
+ public void setMinimum(@Nullable BigDecimal minimum) {
this.minimum = minimum;
}
return maximum;
}
- public void setMaximum(BigDecimal maximum) {
+ public void setMaximum(@Nullable BigDecimal maximum) {
this.maximum = maximum;
}
return step;
}
- public void setStep(BigDecimal step) {
+ public void setStep(@Nullable BigDecimal step) {
this.step = step;
}
return pattern;
}
- public void setPattern(String pattern) {
+ public void setPattern(@Nullable String pattern) {
this.pattern = pattern;
}
return readOnly;
}
- public void setReadOnly(Boolean readOnly) {
+ public void setReadOnly(@Nullable Boolean readOnly) {
this.readOnly = readOnly;
}
return options;
}
- public void setOptions(List<OptionsValueListDTO> options) {
+ public void setOptions(@Nullable List<OptionsValueListDTO> options) {
this.options = options;
}
}
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;
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;
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);
});
public MiIoUnsupportedHandler(Thing thing, MiIoDatabaseWatchService miIoDatabaseWatchService,
- CloudConnector cloudConnector) {
+ CloudConnector cloudConnector, HttpClient httpClientFactory) {
super(thing, miIoDatabaseWatchService, cloudConnector);
+ this.httpClient = httpClientFactory;
}
@Override
if (channelUID.getId().equals(CHANNEL_TESTCOMMANDS)) {
executeExperimentalCommands();
}
+ if (channelUID.getId().equals(CHANNEL_TESTMIOT)) {
+ executeCreateMiotTestFile();
+ }
}
@Override
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<>();
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: ");
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());
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) {
--- /dev/null
+/**
+ * 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;
+}
--- /dev/null
+/**
+ * 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;
+}
--- /dev/null
+/**
+ * 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;
+ }
+}
--- /dev/null
+/**
+ * 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;
+}
--- /dev/null
+/**
+ * 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);
+ }
+}
--- /dev/null
+/**
+ * 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;
+ }
+}
--- /dev/null
+/**
+ * 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;
+ }
+}
--- /dev/null
+/**
+ * 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;
+ }
+}
--- /dev/null
+/**
+ * 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;
+}
--- /dev/null
+/**
+ * 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;
+}
--- /dev/null
+/**
+ * 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;
+ }
+}
<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>
"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,
--- /dev/null
+/**
+ * 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));
+ }
+}
--- /dev/null
+/**
+ * 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";
+ }
+ }
+}