added migrated 2.x add-ons

Signed-off-by: Kai Kreuzer <kai@openhab.org>
This commit is contained in:
Kai Kreuzer
2020-09-21 01:58:32 +02:00
parent bbf1a7fd29
commit 6df6783b60
11662 changed files with 1302875 additions and 11 deletions

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<features name="org.openhab.binding.miio-${project.version}" xmlns="http://karaf.apache.org/xmlns/features/v1.4.0">
<repository>mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features</repository>
<feature name="openhab-binding-miio" description="Xiaomi Mi IO Binding" version="${project.version}">
<feature>openhab-runtime-base</feature>
<feature>openhab-transport-mdns</feature>
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.miio/${project.version}</bundle>
</feature>
</features>

View File

@@ -0,0 +1,219 @@
/**
* Copyright (c) 2010-2020 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.nio.ByteBuffer;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.Arrays;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link Message} is responsible for creating Xiaomi messages.
*
* @author Marcel Verpaalen - Initial contribution
*/
@NonNullByDefault
public class Message {
private static final byte[] MAGIC = Utils.hexStringToByteArray("2131");
private byte[] data;
private byte[] header;
private byte[] magic;
private int length = 0;
private byte[] unknowns = new byte[4];
private byte[] deviceId = new byte[4];
private byte[] tsByte = new byte[4];
private byte[] checksum;
private byte[] raw;
public Message(byte[] raw) {
this.raw = java.util.Arrays.copyOf(raw, (raw.length < 32) ? 32 : raw.length);
this.header = java.util.Arrays.copyOf(this.raw, 16);
this.magic = java.util.Arrays.copyOf(this.raw, 2);
byte[] msgL = java.util.Arrays.copyOfRange(this.raw, 2, 4);
this.length = ByteBuffer.wrap(msgL).getShort();
this.unknowns = java.util.Arrays.copyOfRange(this.raw, 4, 8);
this.deviceId = java.util.Arrays.copyOfRange(this.raw, 8, 12);
this.tsByte = java.util.Arrays.copyOfRange(this.raw, 12, 16);
this.checksum = java.util.Arrays.copyOfRange(this.raw, 16, 32);
this.data = java.util.Arrays.copyOfRange(this.raw, 32, length);
}
public static Message createMsg(byte[] data, byte[] token, byte[] deviceID, int timeStamp)
throws MiIoCryptoException {
return new Message(createMsgData(data, token, deviceID, timeStamp));
}
public static byte[] createMsgData(byte[] data, byte[] token, byte[] deviceID, int timeStamp)
throws MiIoCryptoException {
short msgLength = (short) (data.length + 32);
ByteBuffer header = ByteBuffer.allocate(16);
header.put(MAGIC);
header.putShort(msgLength);
header.put(new byte[4]);
header.put(deviceID);
header.putInt(timeStamp);
ByteBuffer msg = ByteBuffer.allocate(msgLength);
msg.put(header.array());
msg.put(getChecksum(header.array(), token, data));
msg.put(data);
return msg.array();
}
public static byte[] getChecksum(byte[] header, byte[] token, byte[] data) throws MiIoCryptoException {
ByteBuffer msg = ByteBuffer.allocate(header.length + token.length + data.length);
msg.put(header);
msg.put(token);
msg.put(data);
return MiIoCrypto.md5(msg.array());
}
public String toSting() {
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
String formattedDate = getTimestamp().format(formatter);
String s = "Message:\r\nHeader : " + Utils.getSpacedHex(header) + "\r\nchecksum: "
+ Utils.getSpacedHex(checksum);
if (getLength() > 32) {
s += "\r\ncontent : " + Utils.getSpacedHex(data);
} else {
s += "\r\ncontent : N/A";
}
s += "\r\nHeader Details: Magic:" + Utils.getSpacedHex(magic) + "\r\nLength: " + Integer.toString(length);
s += "\r\nSerial: " + Utils.getSpacedHex(deviceId) + "\r\nTS:" + formattedDate;
return s;
}
/**
* @return the data block
*/
public byte[] getData() {
return data;
}
/**
* @param data the data to set
*/
public void setData(byte[] data) {
this.data = data;
}
/**
* @return the header
*/
public byte[] getHeader() {
return header;
}
/**
* @param header the header to set
*/
public void setHeader(byte[] header) {
this.magic = header;
}
/**
* @return the length
*/
public int getLength() {
return length;
}
/**
* @param length the length to set
*/
public void setLength(int length) {
this.length = length;
}
/**
* @return the unknowns
*/
public byte[] getUnknowns() {
return unknowns;
}
/**
* @param unknowns the unknowns to set
*/
public void setUnknowns(byte[] unknowns) {
this.unknowns = unknowns;
}
/**
* @return the deviceID
*/
public byte[] getDeviceId() {
return deviceId;
}
/**
* @param serialByte - Device Id
*/
public void setDeviceId(byte[] serialByte) {
this.deviceId = serialByte;
}
/**
* @return the timestamp
*/
public LocalDateTime getTimestamp() {
return LocalDateTime.ofInstant(Instant.ofEpochSecond(getTimestampAsInt()), ZoneId.systemDefault());
}
/**
* @return the timestamp
*/
public int getTimestampAsInt() {
return ByteBuffer.wrap(tsByte).getInt();
}
/**
* @return the raw message
*/
public byte[] getRaw() {
return raw;
}
/**
* Set the message content
*
* @param raw byte array containing the message
*/
public void setRaw(byte[] raw) {
this.raw = java.util.Arrays.copyOf(raw, (raw.length < 32) ? 32 : raw.length);
}
/**
* @return the checksum
*/
public byte[] getChecksum() {
return checksum;
}
/**
* @param checksum the checksum to set
*/
public void setChecksum(byte[] checksum) {
this.checksum = checksum;
}
public boolean isChecksumValid() {
return Arrays.equals(getChecksum(), checksum);
}
}

View File

@@ -0,0 +1,30 @@
/**
* Copyright (c) 2010-2020 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;
/**
* The {@link MiIoBindingConfiguration} class defines variables which are
* used for the binding configuration.
*
* @author Marcel Verpaalen - Initial contribution
*/
@SuppressWarnings("null")
public final class MiIoBindingConfiguration {
public String host;
public String token;
public String deviceId;
public String model;
public int refreshInterval;
public int timeout;
public String cloudServer;
}

View File

@@ -0,0 +1,114 @@
/**
* Copyright (c) 2010-2020 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.util.Collections;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.thing.ThingTypeUID;
/**
* The {@link MiIoBindingConstants} class defines common constants, which are
* used across the whole binding.
*
* @author Marcel Verpaalen - Initial contribution
*/
@NonNullByDefault
public final class MiIoBindingConstants {
public static final String BINDING_ID = "miio";
// List of all Thing Type UIDs
public static final ThingTypeUID THING_TYPE_MIIO = new ThingTypeUID(BINDING_ID, "generic");
public static final ThingTypeUID THING_TYPE_BASIC = new ThingTypeUID(BINDING_ID, "basic");
public static final ThingTypeUID THING_TYPE_VACUUM = new ThingTypeUID(BINDING_ID, "vacuum");
public static final ThingTypeUID THING_TYPE_UNSUPPORTED = new ThingTypeUID(BINDING_ID, "unsupported");
public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Collections
.unmodifiableSet(Stream.of(THING_TYPE_MIIO, THING_TYPE_BASIC, THING_TYPE_VACUUM, THING_TYPE_UNSUPPORTED)
.collect(Collectors.toSet()));
public static final Set<ThingTypeUID> NONGENERIC_THING_TYPES_UIDS = Collections.unmodifiableSet(
Stream.of(THING_TYPE_BASIC, THING_TYPE_VACUUM, THING_TYPE_UNSUPPORTED).collect(Collectors.toSet()));
// List of all Channel IDs
public static final String CHANNEL_BATTERY = "status#battery";
public static final String CHANNEL_CLEAN_AREA = "status#clean_area";
public static final String CHANNEL_CLEAN_TIME = "status#clean_time";
public static final String CHANNEL_DND_ENABLED = "status#dnd_enabled";
public static final String CHANNEL_ERROR_CODE = "status#error_code";
public static final String CHANNEL_ERROR_ID = "status#error_id";
public static final String CHANNEL_FAN_POWER = "status#fan_power";
public static final String CHANNEL_IN_CLEANING = "status#in_cleaning";
public static final String CHANNEL_MAP_PRESENT = "status#map_present";
public static final String CHANNEL_STATE = "status#state";
public static final String CHANNEL_STATE_ID = "status#state_id";
public static final String CHANNEL_CONTROL = "actions#control";
public static final String CHANNEL_COMMAND = "actions#commands";
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_POWER = "actions#power";
public static final String CHANNEL_SSID = "network#ssid";
public static final String CHANNEL_BSSID = "network#bssid";
public static final String CHANNEL_RSSI = "network#rssi";
public static final String CHANNEL_LIFE = "network#life";
public static final String CHANNEL_CONSUMABLE_MAIN_PERC = "consumables#main_brush_percent";
public static final String CHANNEL_CONSUMABLE_SIDE_PERC = "consumables#side_brush_percent";
public static final String CHANNEL_CONSUMABLE_FILTER_PERC = "consumables#filter_percent";
public static final String CHANNEL_CONSUMABLE_SENSOR_PERC = "consumables#sensor_dirt_percent";
public static final String CHANNEL_CONSUMABLE_MAIN_TIME = "consumables#main_brush_time";
public static final String CHANNEL_CONSUMABLE_SIDE_TIME = "consumables#side_brush_time";
public static final String CHANNEL_CONSUMABLE_FILTER_TIME = "consumables#filter_time";
public static final String CHANNEL_CONSUMABLE_SENSOR_TIME = "consumables#sensor_dirt_time";
public static final String CHANNEL_CONSUMABLE_RESET = "consumables#consumable_reset";
public static final String CHANNEL_DND_FUNCTION = "dnd#dnd_function";
public static final String CHANNEL_DND_START = "dnd#dnd_start";
public static final String CHANNEL_DND_END = "dnd#dnd_end";
public static final String CHANNEL_HISTORY_TOTALTIME = "history#total_clean_time";
public static final String CHANNEL_HISTORY_TOTALAREA = "history#total_clean_area";
public static final String CHANNEL_HISTORY_COUNT = "history#total_clean_count";
public static final String CHANNEL_HISTORY_START_TIME = "cleaning#last_clean_start_time";
public static final String CHANNEL_HISTORY_END_TIME = "cleaning#last_clean_end_time";
public static final String CHANNEL_HISTORY_AREA = "cleaning#last_clean_area";
public static final String CHANNEL_HISTORY_DURATION = "cleaning#last_clean_duration";
public static final String CHANNEL_HISTORY_ERROR = "cleaning#last_clean_error";
public static final String CHANNEL_HISTORY_FINISH = "cleaning#last_clean_finish";
public static final String CHANNEL_HISTORY_RECORD = "cleaning#last_clean_record";
public static final String CHANNEL_VACUUM_MAP = "cleaning#map";
public static final String PROPERTY_HOST_IP = "host";
public static final String PROPERTY_DID = "deviceId";
public static final String PROPERTY_TOKEN = "token";
public static final String PROPERTY_MODEL = "model";
public static final String PROPERTY_REFRESH_INTERVAL = "refreshInterval";
public static final String PROPERTY_TIMEOUT = "timeout";
public static final String PROPERTY_CLOUDSERVER = "cloudServer";
public static final byte[] DISCOVER_STRING = org.openhab.binding.miio.internal.Utils
.hexStringToByteArray("21310020ffffffffffffffffffffffffffffffffffffffffffffffffffffffff");
public static final int PORT = 54321;
public static final Set<String> IGNORED_TOKENS = Collections.unmodifiableSet(Stream
.of("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF", "00000000000000000000000000000000").collect(Collectors.toSet()));
public static final String DATABASE_PATH = "database/";
}

View File

@@ -0,0 +1,109 @@
/**
* Copyright (c) 2010-2020 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 org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link MiIoCommand} contains all known commands for the Xiaomi vacuum and various Mi IO commands for basic
* devices
*
* @author Marcel Verpaalen - Initial contribution
*/
@NonNullByDefault
public enum MiIoCommand {
MIIO_INFO("miIO.info"),
MIIO_WIFI("miIO.wifi_assoc_state"),
MIIO_ROUTERCONFIG("miIO.miIO.config_router"),
// Basic device commands
GET_PROPERTY("get_prop"),
GET_PROPERTIES("get_properties"),
GET_VALUE("get_value"),
SET_PROPERTIES("set_properties"),
SET_MODE_BASIC("set_mode"),
SET_POWER("set_power"),
SET_BRIGHT("set_bright"),
SET_RGB("set_rgb"),
SET_WIFI_LET("set_wifi_led"),
SET_FAVORITE("set_level_favorite"),
ACTION("action"),
// vacuum commands
START_VACUUM("app_start"),
STOP_VACUUM("app_stop"),
START_SPOT("app_spot"),
PAUSE("app_pause"),
CHARGE("app_charge"),
START_ZONE("app_zoned_clean"),
FIND_ME("find_me"),
START_SEGMENT("app_segment_clean"),
CONSUMABLES_GET("get_consumable"),
CONSUMABLES_RESET("reset_consumable"),
CLEAN_SUMMARY_GET("get_clean_summary"),
CLEAN_RECORD_GET("get_clean_record"),
CLEAN_RECORD_MAP_GET("get_clean_record_map"),
GET_MAP("get_map_v1"),
GET_STATUS("get_status"),
GET_SERIAL_NUMBER("get_serial_number"),
DND_GET("get_dnd_timer"),
DND_SET("set_dnd_timer"),
DND_CLOSE("close_dnd_timer"),
TIMER_SET("set_timer"),
TIMER_UPDATE("upd_timer"),
TIMER_GET("get_timer"),
TIMER_DEL("del_timer"),
SOUND_INSTALL("dnld_install_sound"),
SOUND_GET_CURRENT("get_current_sound"),
LOG_UPLOAD_GET("get_log_upload_status"),
LOG_UPLOAD_ENABLE("enable_log_upload"),
SET_MODE("set_custom_mode"),
GET_MODE("get_custom_mode"),
SET_WATERBOX_MODE("set_water_box_custom_mode"),
TIMERZONE_SET("set_timezone"),
TIMERZONE_GET("get_timezone"),
GATEWAY("gateway"),
REMOTE_START("app_rc_start"),
REMOTE_END("app_rc_end"),
REMOTE_MOVE("app_rc_move"),
UNKNOWN("");
private final String command;
private MiIoCommand(String command) {
this.command = command;
}
public String getCommand() {
return command;
}
public static MiIoCommand getCommand(String commandString) {
for (MiIoCommand mioCmd : MiIoCommand.values()) {
if (mioCmd.getCommand().equals(commandString)) {
return mioCmd;
}
}
return UNKNOWN;
}
}

View File

@@ -0,0 +1,112 @@
/**
* Copyright (c) 2010-2020 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.UnsupportedEncodingException;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link MiIoCrypto} is responsible for creating Xiaomi messages.
*
* @author Marcel Verpaalen - Initial contribution
*/
@NonNullByDefault
public class MiIoCrypto {
public static byte[] md5(byte[] source) throws MiIoCryptoException {
try {
MessageDigest m = MessageDigest.getInstance("MD5");
return m.digest(source);
} catch (NoSuchAlgorithmException e) {
throw new MiIoCryptoException(e.getMessage(), e);
}
}
public static byte[] iv(byte[] token) throws MiIoCryptoException {
try {
MessageDigest m = MessageDigest.getInstance("MD5");
byte[] ivbuf = new byte[32];
System.arraycopy(m.digest(token), 0, ivbuf, 0, 16);
System.arraycopy(token, 0, ivbuf, 16, 16);
return m.digest(ivbuf);
} catch (NoSuchAlgorithmException e) {
throw new MiIoCryptoException(e.getMessage(), e);
}
}
public static byte[] encrypt(byte[] cipherText, byte[] key, byte[] iv) throws MiIoCryptoException {
try {
IvParameterSpec vector = new IvParameterSpec(iv);
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
SecretKeySpec keySpec = new SecretKeySpec(key, "AES");
cipher.init(Cipher.ENCRYPT_MODE, keySpec, vector);
byte[] encrypted = cipher.doFinal(cipherText);
return encrypted;
} catch (InvalidKeyException | NoSuchAlgorithmException | NoSuchPaddingException
| InvalidAlgorithmParameterException | IllegalBlockSizeException | BadPaddingException e) {
throw new MiIoCryptoException(e.getMessage(), e);
}
}
public static byte[] encrypt(byte[] text, byte[] token) throws MiIoCryptoException {
return encrypt(text, md5(token), iv(token));
}
public static byte[] decrypt(byte[] cipherText, byte[] key, byte[] iv) throws MiIoCryptoException {
try {
IvParameterSpec vector = new IvParameterSpec(iv);
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
SecretKeySpec keySpec = new SecretKeySpec(key, "AES");
cipher.init(Cipher.DECRYPT_MODE, keySpec, vector);
byte[] crypted = cipher.doFinal(cipherText);
return (crypted);
} catch (InvalidKeyException | NoSuchAlgorithmException | NoSuchPaddingException
| InvalidAlgorithmParameterException | IllegalBlockSizeException | BadPaddingException e) {
throw new MiIoCryptoException(e.getMessage(), e);
}
}
public static byte[] decrypt(byte[] cipherText, byte[] token) throws MiIoCryptoException {
return decrypt(cipherText, md5(token), iv(token));
}
public static String decryptToken(byte[] cipherText) throws MiIoCryptoException {
try {
Cipher cipher = Cipher.getInstance("AES/ECB/NoPadding");
SecretKeySpec keySpec = new SecretKeySpec(new byte[16], "AES");
cipher.init(Cipher.DECRYPT_MODE, keySpec);
byte[] decrypted = cipher.doFinal(cipherText);
try {
return new String(decrypted, "UTF-8").trim();
} catch (UnsupportedEncodingException e) {
return new String(decrypted).trim();
}
} catch (InvalidKeyException | NoSuchAlgorithmException | NoSuchPaddingException | IllegalBlockSizeException
| BadPaddingException e) {
throw new MiIoCryptoException(e.getMessage(), e);
}
}
}

View File

@@ -0,0 +1,41 @@
/**
* Copyright (c) 2010-2020 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 org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Will be thrown instead of the many possible errors in the crypto module
*
* @author Marcel Verpaalen - Initial contribution
*/
@NonNullByDefault
public class MiIoCryptoException extends Exception {
/**
* required variable to avoid IncorrectMultilineIndexException warning
*/
private static final long serialVersionUID = -1280858607995252320L;
public MiIoCryptoException() {
super();
}
public MiIoCryptoException(String message) {
super(message);
}
public MiIoCryptoException(String message, Exception e) {
super(message, e);
}
}

View File

@@ -0,0 +1,227 @@
/**
* Copyright (c) 2010-2020 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.openhab.binding.miio.internal.MiIoBindingConstants.*;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.thing.ThingTypeUID;
/**
* Mi IO Devices
*
* @author Marcel Verpaalen - Initial contribution
*/
@NonNullByDefault
public enum MiIoDevices {
AIRCONDITION_A1("aux.aircondition.v1", "AUX Air Conditioner", THING_TYPE_UNSUPPORTED),
AIRCONDITION_I1("idelan.aircondition.v1", "Idelan Air Conditioner", THING_TYPE_UNSUPPORTED),
AIRCONDITION_M1("midea.aircondition.v1", "Midea Air Conditioner v2", THING_TYPE_UNSUPPORTED),
AIRCONDITION_M2("midea.aircondition.v2", "Midea Air Conditioner v2", THING_TYPE_UNSUPPORTED),
AIRCONDITION_MXA1("midea.aircondition.xa1", "Midea Air Conditioner xa1", THING_TYPE_UNSUPPORTED),
AIRMONITOR1("zhimi.airmonitor.v1", "Mi Air Monitor v1", THING_TYPE_BASIC),
AIRMONITOR_B1("cgllc.airmonitor.b1", "Mi Air Quality Monitor 2gen", THING_TYPE_BASIC),
AIRMONITOR_S1("cgllc.airmonitor.s1", "Mi Air Quality Monitor S1", THING_TYPE_BASIC),
AIR_HUMIDIFIER_V1("zhimi.humidifier.v1", "Mi Air Humidifier", THING_TYPE_BASIC),
AIR_HUMIDIFIER_CA1("zhimi.humidifier.ca1", "Mi Air Humidifier", THING_TYPE_BASIC),
AIR_HUMIDIFIER_CB1("zhimi.humidifier.cb1", "Mi Air Humidifier 2", THING_TYPE_BASIC),
AIR_HUMIDIFIER_MJJSQ("deerma.humidifier.mjjsq", "Mija Smart humidifier", THING_TYPE_BASIC),
AIR_PURIFIER1("zhimi.airpurifier.v1", "Mi Air Purifier v1", THING_TYPE_BASIC),
AIR_PURIFIER2("zhimi.airpurifier.v2", "Mi Air Purifier v2", THING_TYPE_BASIC),
AIR_PURIFIER3("zhimi.airpurifier.v3", "Mi Air Purifier v3", THING_TYPE_BASIC),
AIR_PURIFIER5("zhimi.airpurifier.v5", "Mi Air Purifier v5", THING_TYPE_BASIC),
AIR_PURIFIER6("zhimi.airpurifier.v6", "Mi Air Purifier Pro v6", THING_TYPE_BASIC),
AIR_PURIFIER7("zhimi.airpurifier.v7", "Mi Air Purifier Pro v7", THING_TYPE_BASIC),
AIR_PURIFIERM("zhimi.airpurifier.m1", "Mi Air Purifier 2 (mini)", THING_TYPE_BASIC),
AIR_PURIFIERM2("zhimi.airpurifier.m2", "Mi Air Purifier (mini)", THING_TYPE_BASIC),
AIR_PURIFIERMA1("zhimi.airpurifier.ma1", "Mi Air Purifier MS1", THING_TYPE_BASIC),
AIR_PURIFIERMA2("zhimi.airpurifier.ma2", "Mi Air Purifier MS2", THING_TYPE_BASIC),
AIR_PURIFIERMA4("zhimi.airpurifier.ma4", "Mi Air Purifier 3", THING_TYPE_BASIC),
AIR_PURIFIERMMB3("zhimi.airpurifier.mb3", "Mi Air Purifier 3", THING_TYPE_BASIC),
AIR_PURIFIERSA1("zhimi.airpurifier.sa1", "Mi Air Purifier Super", THING_TYPE_BASIC),
AIR_PURIFIERSA2("zhimi.airpurifier.sa2", "Mi Air Purifier Super 2", THING_TYPE_BASIC),
AIRFRESH_T2017("dmaker.airfresh.t2017", "Mi Fresh Air Ventilator", THING_TYPE_BASIC),
AIRFRESH_A1("dmaker.airfresh.a1", "Mi Fresh Air Ventilator A1", THING_TYPE_BASIC),
ALARM_CLOCK_MYK01("zimi.clock.myk01", "Xiao AI Smart Alarm Clock", THING_TYPE_UNSUPPORTED),
BATHHEATER_V2("yeelight.bhf_light.v2", "Yeelight Smart Bath Heater", THING_TYPE_UNSUPPORTED),
CUCOPLUG_CP1("cuco.plug.cp1", "Gosund Plug", THING_TYPE_BASIC),
DEHUMIDIFIER_FW1("nwt.derh.wdh318efw1", "XIAOMI MIJIA WIDETECH WDH318EFW1 Dehumidifier", THING_TYPE_UNSUPPORTED),
ZHIMI_AIRPURIFIER_MB1("zhimi.airpurifier.mb1", "Mi Air Purifier mb1", THING_TYPE_BASIC),
ZHIMI_AIRPURIFIER_MC1("zhimi.airpurifier.mc1", "Mi Air Purifier 2S", THING_TYPE_BASIC),
ZHIMI_AIRPURIFIER_MC2("zhimi.airpurifier.mc2", "Mi Air Purifier 2S", THING_TYPE_BASIC),
ZHIMI_AIRPURIFIER_VIRTUAL("zhimi.airpurifier.virtual", "Mi Air Purifier virtual", THING_TYPE_UNSUPPORTED),
ZHIMI_AIRPURIFIER_VTL_M1("zhimi.airpurifier.vtl_m1", "Mi Air Purifier vtl m1", THING_TYPE_UNSUPPORTED),
CHUANGMI_IR2("chuangmi.ir.v2", "Mi Remote v2", THING_TYPE_UNSUPPORTED),
CHUANGMI_V2("chuangmi.remote.v2", "Xiaomi IR Remote", THING_TYPE_UNSUPPORTED),
COOKER1("chunmi.cooker.normal1", "MiJia Rice Cooker", THING_TYPE_UNSUPPORTED),
COOKER2("chunmi.cooker.normal2", "MiJia Rice Cooker", THING_TYPE_UNSUPPORTED),
COOKER3("hunmi.cooker.normal3", "MiJia Rice Cooker", THING_TYPE_UNSUPPORTED),
COOKER4("chunmi.cooker.normal4", "MiJia Rice Cooker", THING_TYPE_UNSUPPORTED),
COOKER_P1("chunmi.cooker.press1", "MiJia Heating Pressure Rice Cooker", THING_TYPE_UNSUPPORTED),
COOKER_P2("chunmi.cooker.press2", "MiJia Heating Pressure Rice Cooker", THING_TYPE_UNSUPPORTED),
FAN1("zhimi.fan.v1", "Mi Smart Fan", THING_TYPE_BASIC),
FAN2("zhimi.fan.v2", "Mi Smart Fan", THING_TYPE_BASIC),
FAN3("zhimi.fan.v3", "Mi Smart Pedestal Fan", THING_TYPE_BASIC),
FAN_SA1("zhimi.fan.sa1", "Xiaomi Mi Smart Pedestal Fan", THING_TYPE_BASIC),
FAN_ZA1("zhimi.fan.za1", "Xiaomi Mi Smart Pedestal Fan", THING_TYPE_BASIC),
FAN_ZA4("zhimi.fan.za4", "Xiaomi Mi Smart Pedestal Fan", THING_TYPE_BASIC),
FAN_1C("dmaker.fan.1c", "Xiaomi Mijia Smart Tower Fan", THING_TYPE_BASIC),
FAN_P5("dmaker.fan.p5", "Xiaomi Mijia Smart Tower Fan", THING_TYPE_BASIC),
FAN_P8("dmaker.fan.p8", "Xiaomi Mijia Smart Tower Fan", THING_TYPE_BASIC),
FAN_P9("dmaker.fan.p9", "Xiaomi Mijia Smart Tower Fan", THING_TYPE_BASIC),
FAN_P10("dmaker.fan.p10", "Xiaomi Mijia Smart Tower Fan", THING_TYPE_BASIC),
FRIDGE_V3("viomi.fridge.v3", "Viomi Internet refrigerator iLive", THING_TYPE_UNSUPPORTED),
GATEWAY1("lumi.gateway.v1", "Mi Smart Home Gateway v1", THING_TYPE_UNSUPPORTED),
GATEWAY2("lumi.gateway.v2", "Mi Smart Home Gateway v2", THING_TYPE_UNSUPPORTED),
GATEWAY3("lumi.gateway.v3", "Mi Smart Home Gateway v3", THING_TYPE_UNSUPPORTED),
HUMIDIFIER("zhimi.humidifier.v1", "Mi Humdifier", THING_TYPE_BASIC),
LUMI_C11("lumi.ctrl_neutral1.v1", "Light Control (Wall Switch)", THING_TYPE_UNSUPPORTED),
LUMI_C12("lumi.ctrl_neutral2.v1", "Light Control (Wall Switch)", THING_TYPE_UNSUPPORTED),
PHILIPS_R1("philips.light.sread1", "Xiaomi Philips Eyecare Smart Lamp 2", THING_TYPE_BASIC),
PHILIPS_C("philips.light.ceiling", "Xiaomi Philips LED Ceiling Lamp", THING_TYPE_BASIC),
PHILIPS_C2("philips.light.zyceiling", "Xiaomi Philips LED Ceiling Lamp", THING_TYPE_BASIC),
PHILIPS_BULB("philips.light.bulb", "Xiaomi Philips Bulb", THING_TYPE_BASIC),
PHILIPS_HBULB("philips.light.hbulb", "Xiaomi Philips Wi-Fi Bulb E27 White", THING_TYPE_BASIC),
PHILIPS_CANDLE("philips.light.candle", "PHILIPS Zhirui Smart LED Bulb E14 Candle Lamp", THING_TYPE_BASIC),
PHILIPS_DOWN("philips.light.downlight", "Xiaomi Philips Downlight", THING_TYPE_BASIC),
PHILIPS_MOON("philips.light.moonlight", "Xiaomi Philips ZhiRui bedside lamp", THING_TYPE_BASIC),
PHILIPS_LIGHT_CANDLE2("philips.light.candle2", "Xiaomi PHILIPS Zhirui Smart LED Bulb E14 Candle Lamp White Crystal",
THING_TYPE_BASIC),
PHILIPS_LIGHT_MONO1("philips.light.mono1", "philips.light.mono1", THING_TYPE_BASIC),
PHILIPS_LIGHT_VIRTUAL("philips.light.virtual", "philips.light.virtual", THING_TYPE_BASIC),
PHILIPS_LIGHT_ZYSREAD("philips.light.zysread", "philips.light.zysread", THING_TYPE_BASIC),
PHILIPS_LIGHT_ZYSTRIP("philips.light.zystrip", "philips.light.zystrip", THING_TYPE_BASIC),
POWERPLUG("chuangmi.plug.m1", "Mi Power-plug", THING_TYPE_BASIC),
POWERPLUG1("chuangmi.plug.v1", "Mi Power-plug v1", THING_TYPE_BASIC),
POWERPLUG2("chuangmi.plug.v2", "Mi Power-plug v2", THING_TYPE_BASIC),
POWERPLUG3("chuangmi.plug.v3", "Mi Power-plug v3", THING_TYPE_BASIC),
POWERPLUGM3("chuangmi.plug.m3", "Mi Power-plug", THING_TYPE_BASIC),
POWERPLUG_HMI205("chuangmi.plug.hmi205", "Mi Smart Plug", THING_TYPE_BASIC),
POWERSTRIP("qmi.powerstrip.v1", "Qing Mi Smart Power Strip v1", THING_TYPE_BASIC),
POWERSTRIP2("zimi.powerstrip.v2", "Mi Power-strip v2", THING_TYPE_BASIC),
TOOTHBRUSH("soocare.toothbrush.x3", "Mi Toothbrush", THING_TYPE_UNSUPPORTED),
VACUUM("rockrobo.vacuum.v1", "Mi Robot Vacuum", THING_TYPE_VACUUM),
VACUUM_C1("roborock.vacuum.c1", "Mi Xiaowa Vacuum c1", THING_TYPE_VACUUM),
VACUUM_A08("roborock.vacuum.a08", "Roborock Vacuum S6 pure", THING_TYPE_VACUUM),
VACUUM_A09("roborock.vacuum.a09", "Roborock S6 MaxV / T7 Pro", THING_TYPE_VACUUM),
VACUUM_A10("roborock.vacuum.a10", "Roborock S6 MaxV / T7 Pro", THING_TYPE_VACUUM),
VACUUM_A11("roborock.vacuum.a11", "Roborock S6 MaxV / T7 Pro", THING_TYPE_VACUUM),
VACUUM_P5("roborock.vacuum.p5", "Roborock Vacuum S6 pure", THING_TYPE_VACUUM),
VACUUM2("roborock.vacuum.s5", "Mi Robot Vacuum v2", THING_TYPE_VACUUM),
VACUUM1S("roborock.vacuum.m1s", "Mi Robot Vacuum 1S", THING_TYPE_VACUUM),
VACUUMS4("roborock.vacuum.s4", "Mi Robot Vacuum S4", THING_TYPE_VACUUM),
VACUUMSTS4V2("roborock.vacuum.s4v2", "Roborock Vacuum S4v2", THING_TYPE_VACUUM),
VACUUMST6("roborock.vacuum.t6", "Roborock Vacuum T6", THING_TYPE_VACUUM),
VACUUMST6V2("roborock.vacuum.t6v2", "Roborock Vacuum T6 v2", THING_TYPE_VACUUM),
VACUUMST6V3("roborock.vacuum.t6v3", "Roborock Vacuum T6 v3", THING_TYPE_VACUUM),
VACUUMST4("roborock.vacuum.t4", "Roborock Vacuum T4", THING_TYPE_VACUUM),
VACUUMST4V2("roborock.vacuum.t4v2", "Roborock Vacuum T4 v2", THING_TYPE_VACUUM),
VACUUMST4V3("roborock.vacuum.t4v3", "Roborock Vacuum T4 v3", THING_TYPE_VACUUM),
VACUUMST7("roborock.vacuum.t7", "Roborock Vacuum T7", THING_TYPE_VACUUM),
VACUUMST7V2("roborock.vacuum.t7v2", "Roborock Vacuum T7 v2", THING_TYPE_VACUUM),
VACUUMST7V3("roborock.vacuum.t7v3", "Roborock Vacuum T7 v3", THING_TYPE_VACUUM),
VACUUMST7P("roborock.vacuum.t7p", "Roborock Vacuum T7p", THING_TYPE_VACUUM),
VACUUMST7PV2("roborock.vacuum.t7pv2", "Roborock Vacuum T7 v2", THING_TYPE_VACUUM),
VACUUMST7PV3("roborock.vacuum.t7pv3", "Roborock Vacuum T7 v3", THING_TYPE_VACUUM),
VACUUMS5MAX("roborock.vacuum.s5e", "Roborock Vacuum S5 Max", THING_TYPE_VACUUM),
VACUUMSS6("rockrobo.vacuum.s6", "Roborock Vacuum S6", THING_TYPE_VACUUM),
VACUUMSS62("roborock.vacuum.s6", "Roborock Vacuum S6", THING_TYPE_VACUUM),
VACUUME2("roborock.vacuum.e2", "Rockrobo Xiaowa Vacuum v2", THING_TYPE_UNSUPPORTED),
VACUUME_V6("viomi.vacuum.v6", "Xiaomi Mijia vacuum V-RVCLM21B", THING_TYPE_BASIC),
VACUUME_V7("viomi.vacuum.v7", "Xiaomi Mijia vacuum mop STYJ02YM", THING_TYPE_BASIC),
VACUUME_V8("viomi.vacuum.v8", "Xiaomi Mijia vacuum mop STYJ02YM v2", THING_TYPE_BASIC),
VACUUM_MC1808("dreame.vacuum.mc1808", "Vacuum 1C STYTJ01ZHM", THING_TYPE_BASIC),
ROBOROCK_VACUUM_C1("roborock.vacuum.c1", "roborock.vacuum.c1", THING_TYPE_UNSUPPORTED),
SWEEPER2("roborock.sweeper.e2v2", "Rockrobo Xiaowa Sweeper v2", THING_TYPE_UNSUPPORTED),
SWEEPER3("roborock.sweeper.e2v3", "Rockrobo Xiaowa Sweeper v3", THING_TYPE_UNSUPPORTED),
SWITCH01("090615.switch.xswitch01", " Mijia 1 Gang Wall Smart Switch (WIFI) - PTX switch", THING_TYPE_BASIC),
SWITCH02("090615.switch.xswitch02", " Mijia 2 Gang Wall Smart Switch (WIFI) - PTX switch", THING_TYPE_BASIC),
SWITCH03("090615.switch.xswitch03", " Mijia 3 Gang Wall Smart Switch (WIFI) - PTX switch", THING_TYPE_BASIC),
WATER_PURIFIER2("yunmi.waterpuri.v2", "Mi Water Purifier v2", THING_TYPE_BASIC),
WATER_PURIFIERLX2("yunmi.waterpuri.lx2", "Mi Water Purifier lx2", THING_TYPE_BASIC),
WATER_PURIFIERLX3("yunmi.waterpuri.lx3", "Mi Water Purifier lx3", THING_TYPE_BASIC),
WATER_PURIFIERLX4("yunmi.waterpuri.lx4", "Mi Water Purifier lx4", THING_TYPE_BASIC),
WATER_PURIFIER("yunmi.waterpurifier.v2", "Mi Water Purifier v2", THING_TYPE_BASIC),
WATER_PURIFIER3("yunmi.waterpurifier.v3", "Mi Water Purifier v3", THING_TYPE_BASIC),
WATER_PURIFIER4("yunmi.waterpurifier.v4", "Mi Water Purifier v4", THING_TYPE_BASIC),
WIFI2("xiaomi.repeater.v2", "Xiaomi Wifi Extender", THING_TYPE_UNSUPPORTED),
WIFISPEAKER("xiaomi.wifispeaker.v1", "Mi Internet Speaker", THING_TYPE_UNSUPPORTED),
YEELIGHT_BSLAMP("yeelink.light.bslamp1", "Yeelight Lamp", THING_TYPE_BASIC),
YEELIGHT_BSLAMP2("yeelink.light.bslamp2", "Yeelight Lamp", THING_TYPE_BASIC),
YEELIGHT_CEIL1("yeelink.light.ceiling1", "Yeelight LED Ceiling Lamp", THING_TYPE_BASIC),
YEELIGHT_CEIL2("yeelink.light.ceiling2", "Yeelight LED Ceiling Lamp v2", THING_TYPE_BASIC),
YEELIGHT_CEIL3("yeelink.light.ceiling3", "Yeelight LED Ceiling Lamp v3", THING_TYPE_BASIC),
YEELIGHT_CEIL4("yeelink.light.ceiling4", "Yeelight LED Ceiling Lamp v4 (JIAOYUE 650 RGB)", THING_TYPE_BASIC),
YEELIGHT_CEIL4A("yeelink.light.ceiling4.ambi", "Yeelight LED Ceiling Lamp v4", THING_TYPE_BASIC),
YEELIGHT_CEIL5("yeelink.light.ceiling5", "Yeelight LED Ceiling Lamp v5", THING_TYPE_BASIC),
YEELIGHT_CEIL6("yeelink.light.ceiling6", "Yeelight LED Ceiling Lamp v6", THING_TYPE_BASIC),
YEELIGHT_CEIL7("yeelink.light.ceiling7", "Yeelight LED Ceiling Lamp v7", THING_TYPE_BASIC),
YEELIGHT_CEIL8("yeelink.light.ceiling8", "Yeelight LED Ceiling Lamp v8", THING_TYPE_BASIC),
YEELIGHT_CEIL9("yeelink.light.ceiling9", "Yeelight LED Ceiling Lamp v9", THING_TYPE_BASIC),
YEELIGHT_CEIL10("yeelink.light.ceiling10", "Yeelight LED Meteorite lamp", THING_TYPE_BASIC),
YEELIGHT_CEIL11("yeelink.light.ceiling11", "Yeelight LED Ceiling Lamp v11", THING_TYPE_BASIC),
YEELIGHT_CEIL12("yeelink.light.ceiling12", "Yeelight LED Ceiling Lamp v12", THING_TYPE_BASIC),
YEELIGHT_CEIL13("yeelink.light.ceiling13", "Yeelight LED Ceiling Lamp v13", THING_TYPE_BASIC),
YEELIGHT_CT2("yeelink.light.ct2", "Yeelight ct2", THING_TYPE_BASIC),
YEELIGHT_DOLPHIN("yeelink.light.mono1", "Yeelight White Bulb", THING_TYPE_BASIC),
YEELIGHT_DOLPHIN2("yeelink.light.mono2", "Yeelight White Bulb v2", THING_TYPE_BASIC),
YEELIGHT_DONUT("yeelink.wifispeaker.v1", "Yeelight Wifi Speaker", THING_TYPE_UNSUPPORTED),
YEELIGHT_MANGO("yeelink.light.lamp1", "Yeelight", THING_TYPE_BASIC),
YEELIGHT_MANGO2("yeelink.light.lamp2", "Yeelight", THING_TYPE_BASIC),
YEELIGHT_MANGO3("yeelink.light.lamp3", "Yeelight", THING_TYPE_BASIC),
YEELIGHT_STRIP("yeelink.light.strip1", "Yeelight Strip", THING_TYPE_BASIC),
YEELIGHT_STRIP2("yeelink.light.strip2", "Yeelight Strip", THING_TYPE_BASIC),
YEELIGHT_VIRT("yeelink.light.virtual", "Yeelight", THING_TYPE_BASIC),
YEELIGHT_C1("yeelink.light.color1", "Yeelight Color Bulb", THING_TYPE_BASIC),
YEELIGHT_C2("yeelink.light.color2", "Yeelight Color Bulb YLDP06YL 10W", THING_TYPE_BASIC),
YEELIGHT_C3("yeelink.light.color3", "Yeelight Color Bulb YLDP02YL 9W", THING_TYPE_BASIC),
YEELIGHT_C4("yeelink.light.color4", "Yeelight Bulb YLDP13YL (8,5W)", THING_TYPE_BASIC),
UNKNOWN("unknown", "Unknown Mi IO Device", THING_TYPE_UNSUPPORTED);
public static MiIoDevices getType(String modelString) {
for (MiIoDevices mioDev : MiIoDevices.values()) {
if (mioDev.getModel().equals(modelString)) {
return mioDev;
}
}
return UNKNOWN;
}
private final String model;
private final String description;
private final ThingTypeUID thingType;
MiIoDevices(String model, String description, ThingTypeUID thingType) {
this.model = model;
this.description = description;
this.thingType = thingType;
}
public String getDescription() {
return description;
}
public String getModel() {
return model;
}
public ThingTypeUID getThingType() {
return thingType;
}
@Override
public String toString() {
return description + " (" + model + ")";
}
}

View File

@@ -0,0 +1,92 @@
/**
* Copyright (c) 2010-2020 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.openhab.binding.miio.internal.MiIoBindingConstants.*;
import java.util.Map;
import java.util.concurrent.ScheduledExecutorService;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.miio.internal.basic.MiIoDatabaseWatchService;
import org.openhab.binding.miio.internal.cloud.CloudConnector;
import org.openhab.binding.miio.internal.handler.MiIoBasicHandler;
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.thing.Thing;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.binding.BaseThingHandlerFactory;
import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.thing.binding.ThingHandlerFactory;
import org.openhab.core.thing.type.ChannelTypeRegistry;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
/**
* The {@link MiIoHandlerFactory} is responsible for creating things and thing
* handlers.
*
* @author Marcel Verpaalen - Initial contribution
*/
@Component(service = ThingHandlerFactory.class, configurationPid = "binding.miio")
@NonNullByDefault
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 MiIoDatabaseWatchService miIoDatabaseWatchService;
private CloudConnector cloudConnector;
private ChannelTypeRegistry channelTypeRegistry;
@Activate
public MiIoHandlerFactory(@Reference ChannelTypeRegistry channelTypeRegistry,
@Reference MiIoDatabaseWatchService miIoDatabaseWatchService, @Reference CloudConnector cloudConnector,
Map<String, Object> properties) {
this.miIoDatabaseWatchService = miIoDatabaseWatchService;
this.cloudConnector = cloudConnector;
@Nullable
String username = (String) properties.get("username");
@Nullable
String password = (String) properties.get("password");
@Nullable
String country = (String) properties.get("country");
cloudConnector.setCredentials(username, password, country);
scheduler.submit(() -> cloudConnector.isConnected());
this.channelTypeRegistry = channelTypeRegistry;
}
@Override
public boolean supportsThingType(ThingTypeUID thingTypeUID) {
return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID);
}
@Override
protected @Nullable ThingHandler createHandler(Thing thing) {
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
if (thingTypeUID.equals(THING_TYPE_MIIO)) {
return new MiIoGenericHandler(thing, miIoDatabaseWatchService);
}
if (thingTypeUID.equals(THING_TYPE_BASIC)) {
return new MiIoBasicHandler(thing, miIoDatabaseWatchService);
}
if (thingTypeUID.equals(THING_TYPE_VACUUM)) {
return new MiIoVacuumHandler(thing, miIoDatabaseWatchService, cloudConnector, channelTypeRegistry);
}
return new MiIoUnsupportedHandler(thing, miIoDatabaseWatchService);
}
}

View File

@@ -0,0 +1,57 @@
/**
* Copyright (c) 2010-2020 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 com.google.gson.annotations.Expose;
import com.google.gson.annotations.SerializedName;
/**
* Mapping properties from json for miio info response
*
* @author Marcel Verpaalen - Initial contribution
*/
public class MiIoInfoDTO {
@SerializedName("life")
@Expose
public Long life;
@SerializedName("cfg_time")
@Expose
public Long cfgTime;
@SerializedName("token")
@Expose
public String token;
@SerializedName("mac")
@Expose
public String mac;
@SerializedName("fw_ver")
@Expose
public String fwVer;
@SerializedName("hw_ver")
@Expose
public String hwVer;
@SerializedName("uid")
@Expose
public Long uid;
@SerializedName("model")
@Expose
public String model;
@SerializedName("wifi_fw_ver")
@Expose
public String wifiFwVer;
@SerializedName("mcu_fw_ver")
@Expose
public String mcuFwVer;
@SerializedName("mmfree")
@Expose
public Long mmfree;
}

View File

@@ -0,0 +1,41 @@
/**
* Copyright (c) 2010-2020 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 org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
/**
* Interface for a listener on the {@link org.openhab.binding.miio.internal.transport.MiIoAsyncCommunication}.
* Informs when a message is received.
*
* @author Marcel Verpaalen - Initial contribution
*/
@NonNullByDefault
public interface MiIoMessageListener {
/**
* Callback method for the {@link MiIoMessageListener}
*
* @param cmd The received message in JSON format
*/
void onMessageReceived(MiIoSendCommand cmd);
/**
* Callback method for the {@link MiIoMessageListener}
*
* @param status Status online/offline
* @param statusDetail Status details text
*/
void onStatusUpdated(ThingStatus status, ThingStatusDetail statusDetail);
}

View File

@@ -0,0 +1,76 @@
/**
* Copyright (c) 2010-2020 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 org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
/**
* Commands to be send
*
* @author Marcel Verpaalen - Initial contribution
*/
@NonNullByDefault
public class MiIoSendCommand {
private final int id;
private final MiIoCommand command;
private final String commandString;
private @Nullable JsonObject response;
public void setResponse(JsonObject response) {
this.response = response;
}
public MiIoSendCommand(int id, MiIoCommand command, String commandString) {
this.id = id;
this.command = command;
this.commandString = commandString;
}
public int getId() {
return id;
}
public MiIoCommand getCommand() {
return command;
}
public String getCommandString() {
return commandString;
}
public JsonObject getResponse() {
final @Nullable JsonObject response = this.response;
return response != null ? response : new JsonObject();
}
public boolean isError() {
final @Nullable JsonObject response = this.response;
if (response != null) {
return response.has("error");
}
return true;
}
public JsonElement getResult() {
final @Nullable JsonObject response = this.response;
if (response != null && response.has("result")) {
return response.get("result");
}
return new JsonObject();
}
}

View File

@@ -0,0 +1,100 @@
/**
* Copyright (c) 2010-2020 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.IOException;
import java.net.URL;
import org.apache.commons.io.IOUtils;
import org.eclipse.jdt.annotation.NonNullByDefault;
import com.google.gson.JsonElement;
import com.google.gson.JsonIOException;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.google.gson.JsonSyntaxException;
/**
* Utility class for common tasks within the Xiaomi vacuum binding.
*
* @author Marcel Verpaalen - Initial contribution
*
*/
@NonNullByDefault
public final class Utils {
/**
* Convert a string representation of hexadecimal to a byte array.
*
* For example: String s = "00010203" returned byte array is {0x00, 0x01, 0x03}
*
* @param hex hex input string
* @return byte array equivalent to hex string
**/
public static byte[] hexStringToByteArray(String hex) {
String s = hex.replace(" ", "");
int len = s.length();
byte[] data = new byte[len / 2];
for (int i = 0; i < len; i += 2) {
data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4) + Character.digit(s.charAt(i + 1), 16));
}
return data;
}
private static final String HEXES = "0123456789ABCDEF";
/**
* Convert a byte array to a string representation of hexadecimals.
*
* For example: byte array is {0x00, 0x01, 0x03} returned String s =
* "00 01 02 03"
*
* @param raw byte array
* @return String equivalent to hex string
**/
public static String getSpacedHex(byte[] raw) {
final StringBuilder hex = new StringBuilder(3 * raw.length);
for (final byte b : raw) {
hex.append(HEXES.charAt((b & 0xF0) >> 4)).append(HEXES.charAt((b & 0x0F))).append(" ");
}
hex.delete(hex.length() - 1, hex.length());
return hex.toString();
}
public static String getHex(byte[] raw) {
final StringBuilder hex = new StringBuilder(2 * raw.length);
for (final byte b : raw) {
hex.append(HEXES.charAt((b & 0xF0) >> 4)).append(HEXES.charAt((b & 0x0F)));
}
return hex.toString();
}
public static String obfuscateToken(String tokenString) {
if (tokenString.length() > 8) {
String tokenText = tokenString.substring(0, 8)
.concat((tokenString.length() < 24) ? tokenString.substring(8).replaceAll(".", "X")
: tokenString.substring(8, 24).replaceAll(".", "X").concat(tokenString.substring(24)));
return tokenText;
} else {
return tokenString;
}
}
public static JsonObject convertFileToJSON(URL fileName) throws JsonIOException, JsonSyntaxException, IOException {
JsonObject jsonObject = new JsonObject();
JsonParser parser = new JsonParser();
JsonElement jsonElement = parser.parse(IOUtils.toString(fileName));
jsonObject = jsonElement.getAsJsonObject();
return jsonObject;
}
}

View File

@@ -0,0 +1,167 @@
/**
* Copyright (c) 2010-2020 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.basic;
import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.library.types.HSBType;
import org.openhab.core.types.Command;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonPrimitive;
/**
* Conditional Execution of rules
*
* @author Marcel Verpaalen - Initial contribution
*/
@NonNullByDefault
public class ActionConditions {
private static final Logger LOGGER = LoggerFactory.getLogger(ActionConditions.class);
/**
* Check if it matches the firmware version.
*
* @param condition
* @param deviceVariables
* @param value
* @return value in case firmware is matching, return null if not
*/
private static @Nullable JsonElement firmwareCheck(MiIoDeviceActionCondition condition,
@Nullable Map<String, Object> deviceVariables, @Nullable JsonElement value) {
// TODO: placeholder for firmware version check to allow for firmware dependent actions
return value;
}
/**
* Check if the value is a valid brightness for operating power On/Off switch.
* If brightness <1 returns Off, if >=1 returns On
*
* @param value
* @return
*/
private static @Nullable JsonElement brightness(@Nullable JsonElement value) {
if (value != null && value.isJsonPrimitive() && value.getAsJsonPrimitive().isNumber()) {
if (value.getAsInt() < 1) {
return new JsonPrimitive("off");
} else {
return new JsonPrimitive("on");
}
} else {
LOGGER.debug("Could not parse brightness. Value '{}' is not an int", value);
}
return value;
}
/**
* Check if the value is a valid brightness between 1-100 which can be send to brightness channel.
* If not returns a null
*
* @param value
* @return
*/
private static @Nullable JsonElement brightnessExists(@Nullable JsonElement value) {
if (value != null && value.isJsonPrimitive() && value.getAsJsonPrimitive().isNumber()) {
int intVal = value.getAsInt();
if (intVal > 0 && intVal <= 100) {
return value;
} else if (intVal > 100) {
return new JsonPrimitive(100);
}
return null;
} else {
LOGGER.debug("Could not parse brightness. Value '{}' is not an int", value);
}
return value;
}
/**
* Check if the value is a color which can be send to Color channel.
* If not returns a null
*
* @param command
*
* @param value
* @return
*/
private static @Nullable JsonElement hsbOnly(@Nullable Command command, @Nullable JsonElement value) {
if (command != null && command instanceof HSBType) {
return value;
}
return null;
}
/**
* Check if the command value matches the condition value.
* The condition parameter element should be a Json array, containing Json objects with a matchValue element.
* Optionally it can contain a 'returnValue'element which will be returned in case of match.
* If no match this function will return a null
*
* @param condition.
* @param command
* @param value
* @return returnValue or value in case matching, return null if no match
*/
private static @Nullable JsonElement matchValue(MiIoDeviceActionCondition condition, @Nullable Command command,
@Nullable JsonElement value) {
if (condition.getParameters().isJsonArray() && command != null) {
JsonArray conditionArray = condition.getParameters().getAsJsonArray();
for (int i = 0; i < conditionArray.size(); i++) {
if (conditionArray.get(i).isJsonObject() && conditionArray.get(i).getAsJsonObject().has("matchValue")) {
JsonObject matchCondition = conditionArray.get(i).getAsJsonObject();
String matchvalue = matchCondition.get("matchValue").getAsString();
boolean matching = command.toString().matches(matchvalue);
LOGGER.trace("Matching '{}' with '{}': {}", matchvalue, command, matching);
if (matching) {
if (matchCondition.has("returnValue")) {
return matchCondition.get("returnValue");
} else {
return value;
}
}
} else {
LOGGER.debug("Json DB condition is missing matchValue element in match parameter array.");
}
}
} else {
LOGGER.debug("Json DB condition is missing match parameter array.");
}
return null;
}
public static @Nullable JsonElement executeAction(MiIoDeviceActionCondition condition,
@Nullable Map<String, Object> deviceVariables, @Nullable JsonElement value, @Nullable Command command) {
switch (condition.getName().toUpperCase()) {
case "FIRMWARE":
return firmwareCheck(condition, deviceVariables, value);
case "BRIGHTNESSEXISTING":
return brightnessExists(value);
case "BRIGHTNESSONOFF":
return brightness(value);
case "HSBONLY":
return hsbOnly(command, value);
case "MATCHVALUE":
return matchValue(condition, command, value);
default:
LOGGER.debug("Condition {} not found. Returning '{}'", condition,
value != null ? value.toString() : "");
return value;
}
}
}

View File

@@ -0,0 +1,50 @@
/**
* Copyright (c) 2010-2020 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.basic;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Various types of parameters to be send
*
* @author Marcel Verpaalen - Initial contribution
*/
@NonNullByDefault
public enum CommandParameterType {
NONE("none"),
EMPTY("empty"),
ONOFF("onoff"),
ONOFFPARA("onoffpara"),
ONOFFBOOL("onoffbool"),
ONOFFBOOLSTRING("onoffboolstring"),
STRING("string"),
CUSTOMSTRING("customstring"),
NUMBER("number"),
COLOR("color"),
UNKNOWN("unknown");
private String text;
CommandParameterType(String text) {
this.text = text;
}
public static CommandParameterType fromString(String text) {
for (CommandParameterType param : CommandParameterType.values()) {
if (param.text.equalsIgnoreCase(text)) {
return param;
}
}
return UNKNOWN;
}
}

View File

@@ -0,0 +1,75 @@
/**
* Copyright (c) 2010-2020 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.basic;
import java.util.concurrent.TimeUnit;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.JsonElement;
import com.google.gson.JsonPrimitive;
/**
* Conversion for values
*
* @author Marcel Verpaalen - Initial contribution
*/
@NonNullByDefault
public class Conversions {
private static final Logger LOGGER = LoggerFactory.getLogger(Conversions.class);
public static JsonElement secondsToHours(JsonElement seconds) {
long hours = TimeUnit.SECONDS.toHours(seconds.getAsInt());
return new JsonPrimitive(hours);
}
public static JsonElement yeelightSceneConversion(JsonElement intValue) {
switch (intValue.getAsInt()) {
case 1:
return new JsonPrimitive("color");
case 2:
return new JsonPrimitive("hsv");
case 3:
return new JsonPrimitive("ct");
case 4:
return new JsonPrimitive("nightlight");
case 5: // don't know the number for colorflow...
return new JsonPrimitive("cf");
case 6: // don't know the number for auto_delay_off, or if it is even in the properties visible...
return new JsonPrimitive("auto_delay_off");
default:
return new JsonPrimitive("unknown");
}
}
public static JsonElement divideTen(JsonElement value10) {
double value = value10.getAsDouble() / 10;
return new JsonPrimitive(value);
}
public static JsonElement execute(String transfortmation, JsonElement value) {
switch (transfortmation.toUpperCase()) {
case "YEELIGHTSCENEID":
return yeelightSceneConversion(value);
case "SECONDSTOHOURS":
return secondsToHours(value);
case "/10":
return divideTen(value);
default:
LOGGER.debug("Transformation {} not found. Returning '{}'", transfortmation, value.toString());
return value;
}
}
}

View File

@@ -0,0 +1,79 @@
/**
* Copyright (c) 2010-2020 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.basic;
import java.util.ArrayList;
import java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.miio.internal.MiIoCommand;
import com.google.gson.annotations.Expose;
import com.google.gson.annotations.SerializedName;
/**
* Mapping devices from json
*
* @author Marcel Verpaalen - Initial contribution
*/
@NonNullByDefault
public class DeviceMapping {
@SerializedName("id")
@Expose
private List<String> id = new ArrayList<>();
@SerializedName("propertyMethod")
@Expose
private @Nullable String propertyMethod;
@SerializedName("maxProperties")
@Expose
private @Nullable Integer maxProperties;
@SerializedName("channels")
@Expose
private List<MiIoBasicChannel> miIoBasicChannels = new ArrayList<>();
public List<String> getId() {
return id;
}
public void setId(List<String> id) {
this.id = id;
}
public String getPropertyMethod() {
final String propertyMethod = this.propertyMethod;
return propertyMethod != null ? propertyMethod : MiIoCommand.GET_PROPERTY.getCommand();
}
public void setPropertyMethod(String propertyMethod) {
this.propertyMethod = propertyMethod;
}
public int getMaxProperties() {
final Integer maxProperties = this.maxProperties;
return maxProperties != null ? maxProperties.intValue() : 5;
}
public void setMaxProperties(int maxProperties) {
this.maxProperties = maxProperties;
}
public List<MiIoBasicChannel> getChannels() {
return miIoBasicChannels;
}
public void setChannels(List<MiIoBasicChannel> miIoBasicChannels) {
this.miIoBasicChannels = miIoBasicChannels;
}
}

View File

@@ -0,0 +1,193 @@
/**
* Copyright (c) 2010-2020 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.basic;
import static org.openhab.binding.miio.internal.MiIoBindingConstants.BINDING_ID;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
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
*
* @author Marcel Verpaalen - Initial contribution
*/
@NonNullByDefault
public class MiIoBasicChannel {
@SerializedName("property")
@Expose
private @Nullable String property;
@SerializedName("siid")
@Expose
private @Nullable Integer siid;
@SerializedName("piid")
@Expose
private @Nullable Integer piid;
@SerializedName("friendlyName")
@Expose
private @Nullable String friendlyName;
@SerializedName("channel")
@Expose
private @Nullable String channel;
@SerializedName("channelType")
@Expose
private @Nullable String channelType;
@SerializedName("type")
@Expose
private @Nullable String type;
@SerializedName("refresh")
@Expose
private @Nullable Boolean refresh;
@SerializedName("transformation")
@Expose
private @Nullable String transfortmation;
@SerializedName("ChannelGroup")
@Expose
private @Nullable String channelGroup;
@SerializedName("actions")
@Expose
private @Nullable List<MiIoDeviceAction> miIoDeviceActions = new ArrayList<>();
public String getProperty() {
final String property = this.property;
return (property != null) ? property : "";
}
public void setProperty(String property) {
this.property = property;
}
public int getSiid() {
final Integer siid = this.siid;
if (siid != null) {
return siid.intValue();
} else {
return 0;
}
}
public void setSiid(Integer siid) {
this.siid = siid;
}
public int getPiid() {
final Integer piid = this.piid;
if (piid != null) {
return piid.intValue();
} else {
return 0;
}
}
public void setPiid(Integer piid) {
this.piid = piid;
}
public boolean isMiOt() {
if (piid != null && siid != null && (getPiid() != 0 || getSiid() != 0)) {
return true;
} else {
return false;
}
}
public String getFriendlyName() {
final String fn = friendlyName;
return (fn == null || type == null || fn.isEmpty()) ? getChannel() : fn;
}
public void setFriendlyName(String friendlyName) {
this.friendlyName = friendlyName;
}
public String getChannel() {
final @Nullable String channel = this.channel;
return channel != null ? channel : "";
}
public void setChannel(String channel) {
this.channel = channel;
}
public String getChannelType() {
final @Nullable String ct = channelType;
if (ct == null || ct.isEmpty()) {
return BINDING_ID + ":" + getChannel();
} else {
return (ct.startsWith("system") ? ct : BINDING_ID + ":" + ct);
}
}
public void setChannelType(String channelType) {
this.channelType = channelType;
}
public String getType() {
final @Nullable String type = this.type;
return type != null ? type : "";
}
public void setType(String type) {
this.type = type;
}
public Boolean getRefresh() {
final @Nullable Boolean rf = refresh;
return rf != null && rf.booleanValue() && !getProperty().isEmpty();
}
public void setRefresh(Boolean refresh) {
this.refresh = refresh;
}
public String getChannelGroup() {
final @Nullable String channelGroup = this.channelGroup;
return channelGroup != null ? channelGroup : "";
}
public void setChannelGroup(String channelGroup) {
this.channelGroup = channelGroup;
}
public List<MiIoDeviceAction> getActions() {
final @Nullable List<MiIoDeviceAction> miIoDeviceActions = this.miIoDeviceActions;
return (miIoDeviceActions != null) ? miIoDeviceActions : Collections.emptyList();
}
public void setActions(List<MiIoDeviceAction> miIoDeviceActions) {
this.miIoDeviceActions = miIoDeviceActions;
}
public @Nullable String getTransfortmation() {
return transfortmation;
}
public void setTransfortmation(String transfortmation) {
this.transfortmation = transfortmation;
}
@Override
public String toString() {
return "[ Channel = " + getChannel() + ", friendlyName = " + getFriendlyName() + ", type = " + getType()
+ ", channelType = " + getChannelType() + ", ChannelGroup = " + getChannelGroup() + ", channel = "
+ getChannel() + ", property = " + getProperty() + ", refresh = " + getRefresh() + "]";
}
}

View File

@@ -0,0 +1,39 @@
/**
* Copyright (c) 2010-2020 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.basic;
import org.eclipse.jdt.annotation.NonNullByDefault;
import com.google.gson.annotations.Expose;
import com.google.gson.annotations.SerializedName;
/**
* Mapping devices from json
*
* @author Marcel Verpaalen - Initial contribution
*/
@NonNullByDefault
public class MiIoBasicDevice {
@SerializedName("deviceMapping")
@Expose
private DeviceMapping deviceMapping = new DeviceMapping();
public DeviceMapping getDevice() {
return deviceMapping;
}
public void setDevice(DeviceMapping deviceMapping) {
this.deviceMapping = deviceMapping;
}
}

View File

@@ -0,0 +1,149 @@
/**
* Copyright (c) 2010-2020 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.basic;
import static java.nio.file.StandardWatchEventKinds.*;
import static org.openhab.binding.miio.internal.MiIoBindingConstants.BINDING_ID;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.WatchEvent;
import java.nio.file.WatchEvent.Kind;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.miio.internal.MiIoBindingConstants;
import org.openhab.binding.miio.internal.Utils;
import org.openhab.core.config.core.ConfigConstants;
import org.openhab.core.service.AbstractWatchService;
import org.osgi.framework.Bundle;
import org.osgi.framework.FrameworkUtil;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonIOException;
import com.google.gson.JsonObject;
import com.google.gson.JsonSyntaxException;
/**
* The {@link MiIoDatabaseWatchService} creates a registry of database file per ModelId
*
* @author Marcel Verpaalen - Initial contribution
*/
@Component(service = MiIoDatabaseWatchService.class)
@NonNullByDefault
public class MiIoDatabaseWatchService extends AbstractWatchService {
private static final String LOCAL_DATABASE_PATH = ConfigConstants.getConfigFolder() + File.separator + "misc"
+ File.separator + BINDING_ID;
private static final String DATABASE_FILES = ".json";
private static final Gson GSON = new GsonBuilder().serializeNulls().create();
private final Logger logger = LoggerFactory.getLogger(MiIoDatabaseWatchService.class);
private Map<String, URL> databaseList = new HashMap<>();
@Activate
public MiIoDatabaseWatchService() {
super(LOCAL_DATABASE_PATH);
logger.debug(
"Started miio basic devices local databases watch service. Watching for database files at path: {}",
LOCAL_DATABASE_PATH);
processWatchEvent(null, null, Paths.get(LOCAL_DATABASE_PATH));
populateDatabase();
if (logger.isTraceEnabled()) {
for (String device : databaseList.keySet()) {
logger.trace("Device: {} using URL: {}", device, databaseList.get(device));
}
}
}
@Override
protected boolean watchSubDirectories() {
return true;
}
@Override
protected Kind<?>[] getWatchEventKinds(@Nullable Path directory) {
return new Kind<?>[] { ENTRY_CREATE, ENTRY_DELETE, ENTRY_MODIFY };
}
@Override
protected void processWatchEvent(@Nullable WatchEvent<?> event, @Nullable Kind<?> kind, @Nullable Path path) {
if (path != null) {
final Path p = path.getFileName();
if (p != null && p.toString().endsWith(DATABASE_FILES)) {
logger.debug("Local Databases file {} changed. Refreshing device database.", p.getFileName());
populateDatabase();
}
}
}
/**
* Return the database file URL for a given modelId
*
* @param modelId the model
* @return URL with the definition for the model
*/
public @Nullable URL getDatabaseUrl(String modelId) {
return databaseList.get(modelId);
}
private void populateDatabase() {
Map<String, URL> workingDatabaseList = new HashMap<>();
List<URL> urlEntries = findDatabaseFiles();
for (URL db : urlEntries) {
logger.trace("Adding devices for db file: {}", db);
try {
JsonObject deviceMapping = Utils.convertFileToJSON(db);
MiIoBasicDevice devdb = GSON.fromJson(deviceMapping, MiIoBasicDevice.class);
for (String id : devdb.getDevice().getId()) {
workingDatabaseList.put(id, db);
}
} catch (JsonIOException | JsonSyntaxException | IOException e) {
logger.debug("Error while processing database '{}': {}", db, e.getMessage());
}
databaseList = workingDatabaseList;
}
}
private List<URL> findDatabaseFiles() {
List<URL> urlEntries = new ArrayList<>();
Bundle bundle = FrameworkUtil.getBundle(getClass());
urlEntries.addAll(Collections.list(bundle.findEntries(MiIoBindingConstants.DATABASE_PATH, "*.json", false)));
String userDbFolder = ConfigConstants.getConfigFolder() + File.separator + "misc" + File.separator + BINDING_ID;
try {
File[] userDbFiles = new File(userDbFolder).listFiles((dir, name) -> name.endsWith(".json"));
if (userDbFiles != null) {
for (File f : userDbFiles) {
urlEntries.add(f.toURI().toURL());
logger.debug("Adding local json db file: {}, {}", f.getName(), f.toURI().toURL());
}
}
} catch (IOException e) {
logger.debug("Error while searching for database files: {}", e.getMessage());
}
return urlEntries;
}
}

View File

@@ -0,0 +1,122 @@
/**
* Copyright (c) 2010-2020 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.basic;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import com.google.gson.JsonArray;
import com.google.gson.annotations.Expose;
import com.google.gson.annotations.SerializedName;
/**
* Mapping actions from json
*
* @author Marcel Verpaalen - Initial contribution
*/
@NonNullByDefault
public class MiIoDeviceAction {
@SerializedName("command")
@Expose
private @Nullable String command;
@SerializedName("parameterType")
@Expose
private CommandParameterType commandParameterType = CommandParameterType.EMPTY;
@SerializedName("parameters")
@Expose
private @Nullable JsonArray parameters;
@SerializedName("siid")
@Expose
private @Nullable Integer siid;
@SerializedName("aiid")
@Expose
private @Nullable Integer aiid;
@SerializedName("condition")
@Expose
private @Nullable MiIoDeviceActionCondition condition;
public JsonArray getParameters() {
final @Nullable JsonArray parameter = this.parameters;
return parameter != null ? parameter : new JsonArray();
}
public void setParameters(JsonArray parameters) {
this.parameters = parameters;
}
public String getCommand() {
final @Nullable String command = this.command;
return command != null ? command : "";
}
public void setCommand(String command) {
this.command = command;
}
public CommandParameterType getparameterType() {
return commandParameterType;
}
public void setparameterType(CommandParameterType type) {
this.commandParameterType = type;
}
public void setparameterType(String type) {
this.commandParameterType = org.openhab.binding.miio.internal.basic.CommandParameterType.fromString(type);
}
public int getSiid() {
final Integer siid = this.siid;
if (siid != null) {
return siid.intValue();
} else {
return 0;
}
}
public void setSiid(Integer siid) {
this.siid = siid;
}
public int getAiid() {
final Integer aiid = this.aiid;
if (aiid != null) {
return aiid.intValue();
} else {
return 0;
}
}
public void setAiid(Integer aiid) {
this.aiid = aiid;
}
public boolean isMiOtAction() {
return aiid != null && siid != null && (getAiid() != 0 || getSiid() != 0);
}
public @Nullable MiIoDeviceActionCondition getCondition() {
return condition;
}
public void setCondition(@Nullable MiIoDeviceActionCondition condition) {
this.condition = condition;
}
@Override
public String toString() {
return "MiIoDeviceAction [command=" + command + ", commandParameterType=" + commandParameterType
+ (parameters != null ? ", parameters=" + getParameters().toString() : "") + "]";
}
}

View File

@@ -0,0 +1,61 @@
/**
* Copyright (c) 2010-2020 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.basic;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonNull;
import com.google.gson.annotations.Expose;
import com.google.gson.annotations.SerializedName;
/**
* Mapping actions conditions
*
* @author Marcel Verpaalen - Initial contribution
*/
@NonNullByDefault
public class MiIoDeviceActionCondition {
@SerializedName("name")
@Expose
private @Nullable String name;
@SerializedName("parameters")
@Expose
private @Nullable JsonElement parameters;
public String getName() {
final @Nullable String command = this.name;
return command != null ? command : "";
}
public void setName(String command) {
this.name = command;
}
public JsonElement getParameters() {
final JsonElement parameter = this.parameters;
return parameter != null ? parameter : JsonNull.INSTANCE;
}
public void setParameters(JsonArray parameters) {
this.parameters = parameters;
}
@Override
public String toString() {
return "MiIoDeviceActionCondition [condition=" + name + ",parameters=" + getParameters().toString() + "]";
}
}

View File

@@ -0,0 +1,215 @@
/**
* Copyright (c) 2010-2020 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.cloud;
import static org.openhab.binding.miio.internal.MiIoBindingConstants.BINDING_ID;
import java.util.ArrayList;
import java.util.List;
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.core.cache.ExpiringCache;
import org.openhab.core.io.net.http.HttpClientFactory;
import org.openhab.core.io.net.http.HttpUtil;
import org.openhab.core.library.types.RawType;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Deactivate;
import org.osgi.service.component.annotations.Reference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.JsonParseException;
/**
* The {@link CloudConnector} is responsible for connecting OH to the Xiaomi cloud communication.
*
* @author Marcel Verpaalen - Initial contribution
*/
@Component(service = CloudConnector.class)
@NonNullByDefault
public class CloudConnector {
private static final long CACHE_EXPIRY = TimeUnit.SECONDS.toMillis(60);
private static enum DeviceListState {
FAILED,
STARTING,
REFRESHING,
AVAILABLE,
}
private volatile DeviceListState deviceListState = DeviceListState.STARTING;
private String username = "";
private String password = "";
private String country = "ru,us,tw,sg,cn,de";
private List<CloudDeviceDTO> deviceList = new ArrayList<>();
private boolean connected;
private final HttpClient httpClient;
private @Nullable MiCloudConnector cloudConnector;
private final Logger logger = LoggerFactory.getLogger(CloudConnector.class);
private ExpiringCache<Boolean> logonCache = new ExpiringCache<Boolean>(CACHE_EXPIRY, () -> {
return logon();
});
private ExpiringCache<String> refreshDeviceList = new ExpiringCache<String>(CACHE_EXPIRY, () -> {
if (deviceListState == DeviceListState.FAILED && !isConnected()) {
return ("Could not connect to Xiaomi cloud");
}
final @Nullable MiCloudConnector cl = this.cloudConnector;
if (cl == null) {
return ("Could not connect to Xiaomi cloud");
}
deviceListState = DeviceListState.REFRESHING;
deviceList.clear();
for (String server : country.split(",")) {
try {
deviceList.addAll(cl.getDevices(server));
} catch (JsonParseException e) {
logger.debug("Parsing error getting devices: {}", e.getMessage());
}
}
deviceListState = DeviceListState.AVAILABLE;
return "done";// deviceList;
});
@Activate
public CloudConnector(@Reference HttpClientFactory httpClientFactory) {
this.httpClient = httpClientFactory.createHttpClient(BINDING_ID);
}
@Deactivate
public void dispose() {
final MiCloudConnector cl = cloudConnector;
if (cl != null) {
cl.stopClient();
}
cloudConnector = null;
}
public boolean isConnected() {
final MiCloudConnector cl = cloudConnector;
if (cl != null && cl.hasLoginToken()) {
return true;
}
final @Nullable Boolean c = logonCache.getValue();
if (c != null && c.booleanValue()) {
return true;
}
deviceListState = DeviceListState.FAILED;
return false;
}
public @Nullable RawType getMap(String mapId, String country) throws MiCloudException {
logger.debug("Getting vacuum map {} from Xiaomi cloud server: '{}'", mapId, country);
String mapCountry;
String mapUrl = "";
final @Nullable MiCloudConnector cl = this.cloudConnector;
if (cl == null || !isConnected()) {
throw new MiCloudException("Cannot execute request. Cloudservice not available");
}
if (country.isEmpty()) {
logger.debug("Server not defined in thing. Trying servers: {}", this.country);
for (String mapCountryServer : this.country.split(",")) {
mapCountry = mapCountryServer.trim().toLowerCase();
mapUrl = cl.getMapUrl(mapId, mapCountry);
logger.debug("Map download from server {} returned {}", mapCountry, mapUrl);
if (!mapUrl.isEmpty()) {
break;
}
}
} else {
mapCountry = country.trim().toLowerCase();
mapUrl = cl.getMapUrl(mapId, mapCountry);
}
@Nullable
RawType mapData = HttpUtil.downloadData(mapUrl, null, false, -1);
if (mapData != null) {
return mapData;
} else {
logger.debug("Could not download '{}'", mapUrl);
return null;
}
}
public void setCredentials(@Nullable String username, @Nullable String password, @Nullable String country) {
if (country != null) {
this.country = country;
}
if (username != null && password != null) {
this.username = username;
this.password = password;
}
}
private boolean logon() {
if (username.isEmpty() || password.isEmpty()) {
logger.debug("No Xiaomi cloud credentials. Cloud connectivity disabled");
logger.debug("Logon details: username: '{}', pass: '{}', country: '{}'", username,
password.replaceAll(".", "*"), country);
return connected;
}
try {
final MiCloudConnector cl = new MiCloudConnector(username, password, httpClient);
this.cloudConnector = cl;
connected = cl.login();
if (connected) {
getDevicesList();
} else {
deviceListState = DeviceListState.FAILED;
}
} catch (MiCloudException e) {
connected = false;
deviceListState = DeviceListState.FAILED;
logger.debug("Xiaomi cloud login failed: {}", e.getMessage());
}
return connected;
}
public List<CloudDeviceDTO> getDevicesList() {
refreshDeviceList.getValue();
return deviceList;
}
public @Nullable CloudDeviceDTO getDeviceInfo(String id) {
getDevicesList();
if (deviceListState != DeviceListState.AVAILABLE) {
return null;
}
String did = Long.toString(Long.parseUnsignedLong(id, 16));
List<CloudDeviceDTO> devicedata = new ArrayList<>();
for (CloudDeviceDTO deviceDetails : deviceList) {
if (deviceDetails.getDid().contentEquals(did)) {
devicedata.add(deviceDetails);
}
}
if (devicedata.isEmpty()) {
return null;
}
for (CloudDeviceDTO device : devicedata) {
if (device.getIsOnline()) {
return device;
}
}
if (devicedata.size() > 1) {
logger.debug("Found multiple servers for device {} {} returning first", devicedata.get(0).getDid(),
devicedata.get(0).getName());
}
return devicedata.get(0);
}
}

View File

@@ -0,0 +1,69 @@
/**
* Copyright (c) 2010-2020 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.cloud;
import java.security.InvalidKeyException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Base64;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.miio.internal.MiIoCryptoException;
/**
* The {@link CloudCrypto} is responsible for encryption for Xiaomi cloud communication.
*
* @author Marcel Verpaalen - Initial contribution
*/
@NonNullByDefault
public class CloudCrypto {
/**
* Compute SHA256 hash value for the byte array
*
* @param inBytes ByteArray to be hashed
* @return BASE64 encoded hash value
* @throws MiIoCryptoException
*/
public static String sha256Hash(byte[] inBytes) throws MiIoCryptoException {
try {
MessageDigest md = MessageDigest.getInstance("SHA-256");
return Base64.getEncoder().encodeToString(md.digest(inBytes));
} catch (NoSuchAlgorithmException e) {
throw new MiIoCryptoException(e.getMessage(), e);
}
}
/**
* Compute HmacSHA256 hash value for the byte array
*
* @param key for encoding
* @param cipherText ByteArray to be encoded
* @return BASE64 encoded hash value
* @throws MiIoCryptoException
*/
public static String hMacSha256Encode(byte[] key, byte[] cipherText) throws MiIoCryptoException {
try {
Mac sha256Hmac = Mac.getInstance("HmacSHA256");
SecretKeySpec secretKey = new SecretKeySpec(key, "HmacSHA256");
sha256Hmac.init(secretKey);
return Base64.getEncoder().encodeToString(sha256Hmac.doFinal(cipherText));
} catch (NoSuchAlgorithmException | InvalidKeyException e) {
throw new MiIoCryptoException(e.getMessage(), e);
}
}
}

View File

@@ -0,0 +1,210 @@
/**
* Copyright (c) 2010-2020 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.cloud;
import org.eclipse.jdt.annotation.NonNull;
import com.google.gson.annotations.Expose;
import com.google.gson.annotations.SerializedName;
/**
* This DTO class wraps the device info json structure
*
* @author Marcel Verpaalen - Initial contribution
*/
public class CloudDeviceDTO {
@SerializedName("did")
@Expose
private String did;
@SerializedName("token")
@Expose
private String token;
@SerializedName("longitude")
@Expose
private String longitude;
@SerializedName("latitude")
@Expose
private String latitude;
@SerializedName("name")
@Expose
private String name;
@SerializedName("pid")
@Expose
private String pid;
@SerializedName("localip")
@Expose
private String localip;
@SerializedName("mac")
@Expose
private String mac;
@SerializedName("ssid")
@Expose
private String ssid;
@SerializedName("bssid")
@Expose
private String bssid;
@SerializedName("parent_id")
@Expose
private String parentId;
@SerializedName("parent_model")
@Expose
private String parentModel;
@SerializedName("show_mode")
@Expose
private Integer showMode;
@SerializedName("model")
@Expose
private String model;
@SerializedName("adminFlag")
@Expose
private Integer adminFlag;
@SerializedName("shareFlag")
@Expose
private Integer shareFlag;
@SerializedName("permitLevel")
@Expose
private Integer permitLevel;
@SerializedName("isOnline")
@Expose
private Boolean isOnline;
@SerializedName("desc")
@Expose
private String desc;
@SerializedName("uid")
@Expose
private Integer uid;
@SerializedName("pd_id")
@Expose
private Integer pdId;
@SerializedName("password")
@Expose
private String password;
@SerializedName("rssi")
@Expose
private Integer rssi;
@SerializedName("family_id")
@Expose
private Integer familyId;
private @NonNull String server = "undefined";
public @NonNull String getDid() {
return did != null ? did : "";
}
public @NonNull String getToken() {
return token != null ? token : "";
}
public String getLongitude() {
return longitude;
}
public String getLatitude() {
return latitude;
}
public @NonNull String getName() {
return name != null ? name : "";
}
public String getPid() {
return pid;
}
public @NonNull String getLocalip() {
return localip != null ? localip : "";
}
public String getMac() {
return mac;
}
public String getSsid() {
return ssid;
}
public String getBssid() {
return bssid;
}
public String getParentId() {
return parentId;
}
public String getParentModel() {
return parentModel;
}
public Integer getShowMode() {
return showMode;
}
public String getModel() {
return model;
}
public Integer getAdminFlag() {
return adminFlag;
}
public Integer getShareFlag() {
return shareFlag;
}
public Integer getPermitLevel() {
return permitLevel;
}
public Boolean getIsOnline() {
return isOnline;
}
public String getDesc() {
return desc;
}
public Integer getUid() {
return uid;
}
public Integer getPdId() {
return pdId;
}
public String getPassword() {
return password;
}
public Integer getRssi() {
return rssi;
}
public Integer getFamilyId() {
return familyId;
}
public @NonNull String getServer() {
return server;
}
public void setServer(@NonNull String server) {
this.server = server;
}
@Override
public String toString() {
return "Device name: '" + getName() + "', did: '" + getDid() + "', token: '" + getToken() + "', ip: "
+ getLocalip() + ", server: " + server;
}
}

View File

@@ -0,0 +1,34 @@
/**
* Copyright (c) 2010-2020 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.cloud;
import java.util.List;
import com.google.gson.annotations.Expose;
import com.google.gson.annotations.SerializedName;
/**
* This DTO class wraps the device list info json structure
*
* @author Marcel Verpaalen - Initial contribution
*/
public class CloudDeviceListDTO {
@SerializedName("list")
@Expose
private List<CloudDeviceDTO> cloudDevices = null;
public List<CloudDeviceDTO> getCloudDevices() {
return cloudDevices;
}
}

View File

@@ -0,0 +1,126 @@
/**
* Copyright (c) 2010-2020 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.cloud;
import org.jetbrains.annotations.NotNull;
import com.google.gson.annotations.Expose;
import com.google.gson.annotations.SerializedName;
/**
* This DTO class wraps the login step 2 json structure
*
* @author Marcel Verpaalen - Initial contribution
*/
public class CloudLoginDTO {
@SerializedName("qs")
@Expose
private String qs;
@SerializedName("psecurity")
@Expose
private String psecurity;
@SerializedName("nonce")
@Expose
private Integer nonce;
@SerializedName("ssecurity")
@Expose
private String ssecurity;
@SerializedName("passToken")
@Expose
private String passToken;
@SerializedName("userId")
@Expose
private String userId;
@SerializedName("cUserId")
@Expose
private String cUserId;
@SerializedName("securityStatus")
@Expose
private Integer securityStatus;
@SerializedName("pwd")
@Expose
private Integer pwd;
@SerializedName("code")
@Expose
private String code;
@SerializedName("desc")
@Expose
private String desc;
@SerializedName("location")
@Expose
private String location;
@SerializedName("captchaUrl")
@Expose
private Object captchaUrl;
public @NotNull String getSsecurity() {
return ssecurity != null ? ssecurity : "";
}
public @NotNull String getUserId() {
return userId != null ? userId : "";
}
public @NotNull String getcUserId() {
return cUserId != null ? cUserId : "";
}
public @NotNull String getPassToken() {
return passToken != null ? passToken : "";
}
public @NotNull String getLocation() {
return location != null ? location : "";
}
public String getCode() {
return code;
}
public String getQs() {
return qs;
}
public String getPsecurity() {
return psecurity;
}
public Integer getNonce() {
return nonce;
}
public String getCUserId() {
return cUserId;
}
public Integer getSecurityStatus() {
return securityStatus;
}
public Integer getPwd() {
return pwd;
}
public String getDesc() {
return desc;
}
public Object getCaptchaUrl() {
return captchaUrl;
}
public void setCaptchaUrl(Object captchaUrl) {
this.captchaUrl = captchaUrl;
}
}

View File

@@ -0,0 +1,141 @@
/**
* Copyright (c) 2010-2020 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.cloud;
import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Base64;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.Set;
import java.util.TreeMap;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.miio.internal.MiIoBindingConstants;
import org.openhab.binding.miio.internal.MiIoCryptoException;
import org.openhab.core.config.core.ConfigConstants;
import org.slf4j.Logger;
/**
* The {@link CloudUtil} class is used for supporting functions for Xiaomi cloud access
*
* @author Marcel Verpaalen - Initial contribution
*/
@NonNullByDefault
public class CloudUtil {
private static final Random RANDOM = new Random();
private static final String DB_FOLDER_NAME = ConfigConstants.getUserDataFolder() + File.separator
+ MiIoBindingConstants.BINDING_ID;
/**
* Saves the Xiaomi cloud device info with tokens to file
*
* @param data file content
* @param country county server
* @param logger
*/
public static void saveDeviceInfoFile(String data, String country, Logger logger) {
File folder = new File(DB_FOLDER_NAME);
if (!folder.exists()) {
folder.mkdirs();
}
File dataFile = new File(folder, "miioTokens-" + country + ".json");
try (FileWriter writer = new FileWriter(dataFile)) {
writer.write(data);
logger.debug("Devices token info saved to {}", dataFile.getAbsolutePath());
} catch (IOException e) {
logger.debug("Failed to write token file '{}': {}", dataFile.getName(), e.getMessage());
}
}
/**
* Generate signature for the request.
*
* @param method http request method. GET or POST
* @param requestUrl the full request url. e.g.: http://api.xiaomi.com/getUser?id=123321
* @param params request params. This should be a TreeMap because the
* parameters are required to be in lexicographic order.
* @param signedNonce secret key for encryption.
* @return hash value for the values provided
* @throws MiIoCryptoException
*/
public static String generateSignature(@Nullable String requestUrl, @Nullable String signedNonce, String nonce,
@Nullable Map<String, String> params) throws MiIoCryptoException {
if (signedNonce == null || signedNonce.length() == 0) {
throw new MiIoCryptoException("key is not nullable");
}
List<String> exps = new ArrayList<String>();
if (requestUrl != null) {
URI uri = URI.create(requestUrl);
exps.add(uri.getPath());
}
exps.add(signedNonce);
exps.add(nonce);
if (params != null && !params.isEmpty()) {
final TreeMap<String, String> sortedParams = new TreeMap<String, String>(params);
Set<Map.Entry<String, String>> entries = sortedParams.entrySet();
for (Map.Entry<String, String> entry : entries) {
exps.add(String.format("%s=%s", entry.getKey(), entry.getValue()));
}
}
boolean first = true;
StringBuilder sb = new StringBuilder();
for (String s : exps) {
if (!first) {
sb.append('&');
} else {
first = false;
}
sb.append(s);
}
return CloudCrypto.hMacSha256Encode(Base64.getDecoder().decode(signedNonce),
sb.toString().getBytes(StandardCharsets.UTF_8));
}
public static String generateNonce(long milli) throws IOException {
ByteArrayOutputStream output = new ByteArrayOutputStream();
DataOutputStream dataOutputStream = new DataOutputStream(output);
dataOutputStream.writeLong(RANDOM.nextLong());
dataOutputStream.writeInt((int) (milli / 60000));
dataOutputStream.flush();
return Base64.getEncoder().encodeToString(output.toByteArray());
}
public static String signedNonce(String ssecret, String nonce) throws IOException, MiIoCryptoException {
byte[] byteArrayS = Base64.getDecoder().decode(ssecret.getBytes(StandardCharsets.UTF_8));
byte[] byteArrayN = Base64.getDecoder().decode(nonce.getBytes(StandardCharsets.UTF_8));
ByteArrayOutputStream output = new ByteArrayOutputStream();
output.write(byteArrayS);
output.write(byteArrayN);
return CloudCrypto.sha256Hash(output.toByteArray());
}
public static void writeBytesToFileNio(byte[] bFile, String fileDest) throws IOException {
Path path = Paths.get(fileDest);
Files.write(path, bFile);
}
}

View File

@@ -0,0 +1,509 @@
/**
* Copyright (c) 2010-2020 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.cloud;
import java.io.IOException;
import java.net.CookieStore;
import java.net.HttpCookie;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URL;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Random;
import java.util.TimeZone;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.HttpResponseException;
import org.eclipse.jetty.client.api.ContentResponse;
import org.eclipse.jetty.client.api.Request;
import org.eclipse.jetty.client.util.FormContentProvider;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpMethod;
import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.util.Fields;
import org.openhab.binding.miio.internal.MiIoCrypto;
import org.openhab.binding.miio.internal.MiIoCryptoException;
import org.openhab.binding.miio.internal.Utils;
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.JsonObject;
import com.google.gson.JsonParser;
import com.google.gson.JsonSyntaxException;
/**
* The {@link MiCloudConnector} class is used for connecting to the Xiaomi cloud access
*
* @author Marcel Verpaalen - Initial contribution
*/
@NonNullByDefault
public class MiCloudConnector {
private static final int REQUEST_TIMEOUT_SECONDS = 10;
private static final String UNEXPECTED = "Unexpected :";
private static final String AGENT_ID = (new Random().ints(65, 70).limit(13)
.collect(StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append).toString());
private static final String USERAGENT = "Android-7.1.1-1.0.0-ONEPLUS A3010-136-" + AGENT_ID
+ " APP/xiaomi.smarthome APPV/62830";
private static Locale locale = Locale.getDefault();
private static final TimeZone TZ = TimeZone.getDefault();
private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("OOOO");
private static final Gson GSON = new GsonBuilder().serializeNulls().create();
private static final JsonParser PARSER = new JsonParser();
private final String clientId;
private String username;
private String password;
private String userId = "";
private String serviceToken = "";
private String ssecurity = "";
private int loginFailedCounter = 0;
private HttpClient httpClient;
private final Logger logger = LoggerFactory.getLogger(MiCloudConnector.class);
public MiCloudConnector(String username, String password, HttpClient httpClient) throws MiCloudException {
this.username = username;
this.password = password;
this.httpClient = httpClient;
if (!checkCredentials()) {
throw new MiCloudException("username or password can't be empty");
}
clientId = (new Random().ints(97, 122 + 1).limit(6)
.collect(StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append).toString());
}
void startClient() throws MiCloudException {
if (!httpClient.isStarted()) {
try {
httpClient.start();
CookieStore cookieStore = httpClient.getCookieStore();
// set default cookies
addCookie(cookieStore, "sdkVersion", "accountsdk-18.8.15", "mi.com");
addCookie(cookieStore, "sdkVersion", "accountsdk-18.8.15", "xiaomi.com");
addCookie(cookieStore, "deviceId", this.clientId, "mi.com");
addCookie(cookieStore, "deviceId", this.clientId, "xiaomi.com");
} catch (Exception e) {
throw new MiCloudException("No http client cannot be started: " + e.getMessage(), e);
}
}
}
public void stopClient() {
try {
this.httpClient.stop();
} catch (Exception e) {
logger.debug("Error stopping httpclient :{}", e.getMessage(), e);
}
}
private boolean checkCredentials() {
if (username.trim().isEmpty() || password.trim().isEmpty()) {
logger.info("Xiaomi Cloud: username or password missing.");
return false;
}
return true;
}
private String getApiUrl(String country) {
return "https://" + (country.trim().equalsIgnoreCase("cn") ? "" : country.trim().toLowerCase() + ".")
+ "api.io.mi.com/app";
}
public String getClientId() {
return clientId;
}
String parseJson(String data) {
if (data.contains("&&&START&&&")) {
return data.replace("&&&START&&&", "");
} else {
return UNEXPECTED.concat(data);
}
}
public String getMapUrl(String vacuumMap, String country) throws MiCloudException {
String url = getApiUrl(country) + "/home/getmapfileurl";
Map<String, String> map = new HashMap<String, String>();
map.put("data", "{\"obj_name\":\"" + vacuumMap + "\"}");
String mapResponse = request(url, map);
logger.trace("response: {}", mapResponse);
String errorMsg = "";
JsonElement response = PARSER.parse(mapResponse);
if (response.isJsonObject()) {
logger.debug("Received JSON message {}", response.toString());
if (response.getAsJsonObject().has("result") && response.getAsJsonObject().get("result").isJsonObject()) {
JsonObject jo = response.getAsJsonObject().get("result").getAsJsonObject();
if (jo.has("url")) {
return jo.get("url").getAsString();
} else {
errorMsg = "Could not get url";
}
} else {
errorMsg = "Could not get result";
}
} else {
errorMsg = "Received message is invalid JSON";
}
logger.debug("{}: {}", errorMsg, mapResponse);
return "";
}
public String getDeviceStatus(String device, String country) throws MiCloudException {
String url = getApiUrl(country) + "/home/device_list";
Map<String, String> map = new HashMap<String, String>();
map.put("data", "{\"dids\":[\"" + device + "\"]}");
final String response = request(url, map);
logger.debug("response: {}", response);
return response;
}
public List<CloudDeviceDTO> getDevices(String country) {
final String response = getDeviceString(country);
List<CloudDeviceDTO> devicesList = new ArrayList<>();
try {
final JsonElement resp = PARSER.parse(response);
if (resp.isJsonObject()) {
final JsonObject jor = resp.getAsJsonObject();
if (jor.has("result")) {
devicesList = GSON.fromJson(jor.get("result"), CloudDeviceListDTO.class).getCloudDevices();
for (CloudDeviceDTO device : devicesList) {
device.setServer(country);
logger.debug("Xiaomi cloud info: {}", device);
}
} else {
logger.debug("Response missing result: '{}'", response);
}
} else {
logger.debug("Response is not a json object: '{}'", response);
}
} catch (JsonSyntaxException | IllegalStateException | ClassCastException e) {
logger.info("Error while parsing devices: {}", e.getMessage());
}
return devicesList;
}
public String getDeviceString(String country) {
String url = getApiUrl(country) + "/home/device_list";
Map<String, String> map = new HashMap<String, String>();
map.put("data", "{\"getVirtualModel\":false,\"getHuamiDevices\":0}");
String resp;
try {
resp = request(url, map);
logger.trace("Get devices response: {}", resp);
if (resp.length() > 2) {
CloudUtil.saveDeviceInfoFile(resp, country, logger);
return resp;
}
} catch (MiCloudException e) {
logger.info("{}", e.getMessage());
}
return "";
}
public String request(String urlPart, String country, Map<String, String> params) throws MiCloudException {
String url = getApiUrl(country) + urlPart;
String response = request(url, params);
logger.debug("Request to {} server {}. Response: {}", country, urlPart, response);
return response;
}
public String request(String url, Map<String, String> params) throws MiCloudException {
if (this.serviceToken.isEmpty() || this.userId.isEmpty()) {
throw new MiCloudException("Cannot execute request. service token or userId missing");
}
startClient();
logger.debug("Send request: {} to {}", params.get("data"), url);
Request request = httpClient.newRequest(url).timeout(REQUEST_TIMEOUT_SECONDS, TimeUnit.SECONDS);
request.agent(USERAGENT);
request.header("x-xiaomi-protocal-flag-cli", "PROTOCAL-HTTP2");
request.header(HttpHeader.CONTENT_TYPE, "application/x-www-form-urlencoded");
request.cookie(new HttpCookie("userId", this.userId));
request.cookie(new HttpCookie("yetAnotherServiceToken", this.serviceToken));
request.cookie(new HttpCookie("serviceToken", this.serviceToken));
request.cookie(new HttpCookie("locale", locale.toString()));
request.cookie(new HttpCookie("timezone", ZonedDateTime.now().format(FORMATTER)));
request.cookie(new HttpCookie("is_daylight", TZ.inDaylightTime(new Date()) ? "1" : "0"));
request.cookie(new HttpCookie("dst_offset", Integer.toString(TZ.getDSTSavings())));
request.cookie(new HttpCookie("channel", "MI_APP_STORE"));
if (logger.isTraceEnabled()) {
for (HttpCookie cookie : request.getCookies()) {
logger.trace("Cookie set for request ({}) : {} --> {} (path: {})", cookie.getDomain(),
cookie.getName(), cookie.getValue(), cookie.getPath());
}
}
String method = "POST";
request.method(method);
try {
String nonce = CloudUtil.generateNonce(System.currentTimeMillis());
String signedNonce = CloudUtil.signedNonce(ssecurity, nonce);
String signature = CloudUtil.generateSignature(url.replace("/app", ""), signedNonce, nonce, params);
Fields fields = new Fields();
fields.put("signature", signature);
fields.put("_nonce", nonce);
fields.put("data", params.get("data"));
request.content(new FormContentProvider(fields));
logger.trace("fieldcontent: {}", fields.toString());
final ContentResponse response = request.send();
if (response.getStatus() == HttpStatus.FORBIDDEN_403) {
this.serviceToken = "";
}
return response.getContentAsString();
} catch (HttpResponseException e) {
serviceToken = "";
logger.debug("Error while executing request to {} :{}", url, e.getMessage());
} catch (InterruptedException | TimeoutException | ExecutionException | IOException e) {
logger.debug("Error while executing request to {} :{}", url, e.getMessage());
} catch (MiIoCryptoException e) {
logger.debug("Error while decrypting response of request to {} :{}", url, e.getMessage(), e);
}
return "";
}
private void addCookie(CookieStore cookieStore, String name, String value, String domain) {
HttpCookie cookie = new HttpCookie(name, value);
cookie.setDomain("." + domain);
cookie.setPath("/");
cookieStore.add(URI.create("https://" + domain), cookie);
}
public synchronized boolean login() {
if (!checkCredentials()) {
return false;
}
if (!userId.isEmpty() && !serviceToken.isEmpty()) {
return true;
}
logger.debug("Xiaomi cloud login with userid {}", username);
try {
if (loginRequest()) {
loginFailedCounter = 0;
} else {
loginFailedCounter++;
logger.debug("Xiaomi cloud login attempt {}", loginFailedCounter);
}
} catch (MiCloudException e) {
logger.info("Error logging on to Xiaomi cloud ({}): {}", loginFailedCounter, e.getMessage());
loginFailedCounter++;
serviceToken = "";
if (loginFailedCounter > 10) {
logger.info("Repeated errors logging on to Xiaomi cloud. Cleaning stored cookies");
dumpCookies(".xiaomi.com", true);
dumpCookies(".mi.com", true);
}
return false;
}
return true;
}
protected boolean loginRequest() throws MiCloudException {
try {
startClient();
String sign = loginStep1();
String location;
if (!sign.startsWith("http")) {
location = loginStep2(sign);
} else {
location = sign; // seems we already have login location
}
final ContentResponse responseStep3 = loginStep3(location);
switch (responseStep3.getStatus()) {
case HttpStatus.FORBIDDEN_403:
throw new MiCloudException("Access denied. Did you set the correct api-key and/or username?");
case HttpStatus.OK_200:
return true;
default:
logger.trace("request returned status '{}', reason: {}, content = {}", responseStep3.getStatus(),
responseStep3.getReason(), responseStep3.getContentAsString());
throw new MiCloudException(responseStep3.getStatus() + responseStep3.getReason());
}
} catch (InterruptedException | TimeoutException | ExecutionException e) {
throw new MiCloudException("Cannot logon to Xiaomi cloud: " + e.getMessage(), e);
} catch (MiIoCryptoException e) {
throw new MiCloudException("Error decrypting. Cannot logon to Xiaomi cloud: " + e.getMessage(), e);
} catch (MalformedURLException e) {
throw new MiCloudException("Error getting logon URL. Cannot logon to Xiaomi cloud: " + e.getMessage(), e);
}
}
private String loginStep1() throws InterruptedException, TimeoutException, ExecutionException, MiCloudException {
final ContentResponse responseStep1;
logger.trace("Xiaomi Login step 1");
String url = "https://account.xiaomi.com/pass/serviceLogin?sid=xiaomiio&_json=true";
Request request = httpClient.newRequest(url).timeout(REQUEST_TIMEOUT_SECONDS, TimeUnit.SECONDS);
request.agent(USERAGENT);
request.header(HttpHeader.CONTENT_TYPE, "application/x-www-form-urlencoded");
request.cookie(new HttpCookie("userId", this.userId.length() > 0 ? this.userId : this.username));
responseStep1 = request.send();
final String content = responseStep1.getContentAsString();
logger.trace("Xiaomi Login step 1 content response= {}", content);
logger.trace("Xiaomi Login step 1 response = {}", responseStep1);
try {
JsonElement resp = new JsonParser().parse(parseJson(content));
if (resp.getAsJsonObject().has("_sign")) {
String sign = resp.getAsJsonObject().get("_sign").getAsString();
logger.trace("Xiaomi Login step 1 sign = {}", sign);
return sign;
} else {
logger.trace("Xiaomi Login _sign missing. Maybe still has login cookie.");
return "";
}
} catch (JsonSyntaxException | NullPointerException e) {
throw new MiCloudException("Error getting logon sign. Cannot parse response: " + e.getMessage(), e);
}
}
private String loginStep2(String sign)
throws MiIoCryptoException, InterruptedException, TimeoutException, ExecutionException, MiCloudException {
String passToken;
String cUserId;
logger.trace("Xiaomi Login step 2");
String url = "https://account.xiaomi.com/pass/serviceLoginAuth2";
Request request = httpClient.newRequest(url).timeout(REQUEST_TIMEOUT_SECONDS, TimeUnit.SECONDS);
request.agent(USERAGENT);
request.method(HttpMethod.POST);
final ContentResponse responseStep2;
Fields fields = new Fields();
fields.put("sid", "xiaomiio");
fields.put("hash", Utils.getHex(MiIoCrypto.md5(password.getBytes())));
fields.put("callback", "https://sts.api.io.mi.com/sts");
fields.put("qs", "%3Fsid%3Dxiaomiio%26_json%3Dtrue");
fields.put("user", username);
if (!sign.isEmpty()) {
fields.put("_sign", sign);
}
fields.put("_json", "true");
request.content(new FormContentProvider(fields));
responseStep2 = request.send();
final String content2 = responseStep2.getContentAsString();
logger.trace("Xiaomi login step 2 response = {}", responseStep2);
logger.trace("Xiaomi login step 2 content = {}", content2);
JsonElement resp2 = new JsonParser().parse(parseJson(content2));
CloudLoginDTO jsonResp = GSON.fromJson(resp2, CloudLoginDTO.class);
ssecurity = jsonResp.getSsecurity();
userId = jsonResp.getUserId();
cUserId = jsonResp.getcUserId();
passToken = jsonResp.getPassToken();
String location = jsonResp.getLocation();
String code = jsonResp.getCode();
logger.trace("Xiaomi login ssecurity = {}", ssecurity);
logger.trace("Xiaomi login userId = {}", userId);
logger.trace("Xiaomi login cUserId = {}", cUserId);
logger.trace("Xiaomi login passToken = {}", passToken);
logger.trace("Xiaomi login location = {}", location);
logger.trace("Xiaomi login code = {}", code);
if (logger.isTraceEnabled()) {
dumpCookies(url, false);
}
if (!location.isEmpty()) {
return location;
} else {
throw new MiCloudException("Error getting logon location URL. Return code: " + code);
}
}
private ContentResponse loginStep3(String location)
throws MalformedURLException, InterruptedException, TimeoutException, ExecutionException {
final ContentResponse responseStep3;
Request request;
logger.trace("Xiaomi Login step 3 @ {}", (new URL(location)).getHost());
request = httpClient.newRequest(location).timeout(REQUEST_TIMEOUT_SECONDS, TimeUnit.SECONDS);
request.agent(USERAGENT);
request.header(HttpHeader.CONTENT_TYPE, "application/x-www-form-urlencoded");
responseStep3 = request.send();
logger.trace("Xiaomi login step 3 content = {}", responseStep3.getContentAsString());
logger.trace("Xiaomi login step 3 response = {}", responseStep3);
if (logger.isTraceEnabled()) {
dumpCookies(location, false);
}
URI uri = URI.create("http://sts.api.io.mi.com");
String serviceToken = extractServiceToken(uri);
if (!serviceToken.isEmpty()) {
this.serviceToken = serviceToken;
}
return responseStep3;
}
private void dumpCookies(String url, boolean delete) {
if (logger.isTraceEnabled()) {
try {
URI uri = URI.create(url);
if (uri != null) {
logger.trace("Cookie dump for {}", uri);
CookieStore cs = httpClient.getCookieStore();
List<HttpCookie> cookies = cs.get(uri);
for (HttpCookie cookie : cookies) {
logger.trace("Cookie ({}) : {} --> {} (path: {}. Removed: {})", cookie.getDomain(),
cookie.getName(), cookie.getValue(), cookie.getPath(), delete);
if (delete) {
cs.remove(uri, cookie);
}
}
} else {
logger.trace("Could not create URI from {}", url);
}
} catch (IllegalArgumentException | NullPointerException e) {
logger.trace("Error dumping cookies from {}: {}", url, e.getMessage(), e);
}
}
}
private String extractServiceToken(URI uri) {
String serviceToken = "";
List<HttpCookie> cookies = httpClient.getCookieStore().get(uri);
for (HttpCookie cookie : cookies) {
logger.trace("Cookie :{} --> {}", cookie.getName(), cookie.getValue());
if (cookie.getName().contentEquals("serviceToken")) {
serviceToken = cookie.getValue();
logger.debug("Xiaomi cloud logon succesfull.");
logger.trace("Xiaomi cloud servicetoken: {}", serviceToken);
}
}
return serviceToken;
}
public boolean hasLoginToken() {
return !serviceToken.isEmpty();
}
}

View File

@@ -0,0 +1,40 @@
/**
* Copyright (c) 2010-2020 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.cloud;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Will be thrown for cloud errors
*
* @author Marcel Verpaalen - Initial contribution
*/
@NonNullByDefault
public class MiCloudException extends Exception {
/**
* required variable to avoid IncorrectMultilineIndexException warning
*/
private static final long serialVersionUID = -1280858607995252321L;
public MiCloudException() {
super();
}
public MiCloudException(String message) {
super(message);
}
public MiCloudException(String message, Exception e) {
super(message, e);
}
}

View File

@@ -0,0 +1,303 @@
/**
* Copyright (c) 2010-2020 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.discovery;
import static org.openhab.binding.miio.internal.MiIoBindingConstants.*;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.SocketException;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.miio.internal.Message;
import org.openhab.binding.miio.internal.Utils;
import org.openhab.binding.miio.internal.cloud.CloudConnector;
import org.openhab.binding.miio.internal.cloud.CloudDeviceDTO;
import org.openhab.core.config.discovery.AbstractDiscoveryService;
import org.openhab.core.config.discovery.DiscoveryResultBuilder;
import org.openhab.core.config.discovery.DiscoveryService;
import org.openhab.core.net.NetUtil;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.ThingUID;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link MiIoDiscovery} is responsible for discovering new Xiaomi Mi IO devices
* and their token
*
* @author Marcel Verpaalen - Initial contribution
*
*/
@NonNullByDefault
@Component(service = DiscoveryService.class, immediate = true, configurationPid = "discovery.miio")
public class MiIoDiscovery extends AbstractDiscoveryService {
/** The refresh interval for background discovery */
private static final long SEARCH_INTERVAL = 600;
private static final int BUFFER_LENGTH = 1024;
private static final int DISCOVERY_TIME = 10;
private @Nullable ScheduledFuture<?> miIoDiscoveryJob;
protected @Nullable DatagramSocket clientSocket;
private @Nullable Thread socketReceiveThread;
private Set<String> responseIps = new HashSet<>();
private final Logger logger = LoggerFactory.getLogger(MiIoDiscovery.class);
private final CloudConnector cloudConnector;
@Activate
public MiIoDiscovery(@Reference CloudConnector cloudConnector) throws IllegalArgumentException {
super(DISCOVERY_TIME);
this.cloudConnector = cloudConnector;
}
@Override
public Set<ThingTypeUID> getSupportedThingTypes() {
return SUPPORTED_THING_TYPES_UIDS;
}
@Override
protected void startBackgroundDiscovery() {
logger.debug("Start Xiaomi Mi IO background discovery");
final @Nullable ScheduledFuture<?> miIoDiscoveryJob = this.miIoDiscoveryJob;
if (miIoDiscoveryJob == null || miIoDiscoveryJob.isCancelled()) {
this.miIoDiscoveryJob = scheduler.scheduleWithFixedDelay(this::discover, 0, SEARCH_INTERVAL,
TimeUnit.SECONDS);
}
}
@Override
protected void stopBackgroundDiscovery() {
logger.debug("Stop Xiaomi Mi IO background discovery");
final @Nullable ScheduledFuture<?> miIoDiscoveryJob = this.miIoDiscoveryJob;
if (miIoDiscoveryJob != null) {
miIoDiscoveryJob.cancel(true);
this.miIoDiscoveryJob = null;
}
}
@Override
protected void deactivate() {
stopReceiverThreat();
final DatagramSocket clientSocket = this.clientSocket;
if (clientSocket != null) {
clientSocket.close();
}
this.clientSocket = null;
super.deactivate();
}
@Override
protected void startScan() {
logger.debug("Start Xiaomi Mi IO discovery");
final DatagramSocket clientSocket = getSocket();
if (clientSocket != null) {
logger.debug("Discovery using socket on port {}", clientSocket.getLocalPort());
discover();
} else {
logger.debug("Discovery not started. Client DatagramSocket null");
}
}
private void discover() {
startReceiverThreat();
responseIps = new HashSet<>();
HashSet<String> broadcastAddresses = new HashSet<>();
broadcastAddresses.add("224.0.0.1");
broadcastAddresses.add("224.0.0.50");
broadcastAddresses.addAll(NetUtil.getAllBroadcastAddresses());
for (String broadcastAdress : broadcastAddresses) {
sendDiscoveryRequest(broadcastAdress);
}
}
private void discovered(String ip, byte[] response) {
logger.trace("Discovery responses from : {}:{}", ip, Utils.getSpacedHex(response));
Message msg = new Message(response);
String token = Utils.getHex(msg.getChecksum());
String id = Utils.getHex(msg.getDeviceId());
String label = "Xiaomi Mi Device " + id + " (" + Long.parseUnsignedLong(id, 16) + ")";
String country = "";
boolean isOnline = false;
if (cloudConnector.isConnected()) {
cloudConnector.getDevicesList();
CloudDeviceDTO cloudInfo = cloudConnector.getDeviceInfo(id);
if (cloudInfo != null) {
logger.debug("Cloud Info: {}", cloudInfo);
token = cloudInfo.getToken();
label = cloudInfo.getName() + " " + id + " (" + Long.parseUnsignedLong(id, 16) + ")";
country = cloudInfo.getServer();
isOnline = cloudInfo.getIsOnline();
}
}
ThingUID uid = new ThingUID(THING_TYPE_MIIO, id);
logger.debug("Discovered Mi Device {} ({}) at {} as {}", id, Long.parseUnsignedLong(id, 16), ip, uid);
DiscoveryResultBuilder dr = DiscoveryResultBuilder.create(uid).withProperty(PROPERTY_HOST_IP, ip)
.withProperty(PROPERTY_DID, id);
if (IGNORED_TOKENS.contains(token)) {
logger.debug(
"No token discovered for device {}. For options how to get the token, check the binding readme.",
id);
dr = dr.withRepresentationProperty(PROPERTY_DID).withLabel(label);
} else {
logger.debug("Discovered token for device {}: {}", id, token);
dr = dr.withProperty(PROPERTY_TOKEN, token).withRepresentationProperty(PROPERTY_DID)
.withLabel(label + " with token");
}
if (!country.isEmpty() && isOnline) {
dr = dr.withProperty(PROPERTY_CLOUDSERVER, country);
}
thingDiscovered(dr.build());
}
synchronized @Nullable DatagramSocket getSocket() {
DatagramSocket clientSocket = this.clientSocket;
if (clientSocket != null && clientSocket.isBound()) {
return clientSocket;
}
try {
logger.debug("Getting new socket for discovery");
clientSocket = new DatagramSocket();
clientSocket.setReuseAddress(true);
clientSocket.setBroadcast(true);
this.clientSocket = clientSocket;
return clientSocket;
} catch (SocketException | SecurityException e) {
logger.debug("Error getting socket for discovery: {}", e.getMessage());
}
return null;
}
private void closeSocket() {
final @Nullable DatagramSocket clientSocket = this.clientSocket;
if (clientSocket != null) {
clientSocket.close();
} else {
return;
}
this.clientSocket = null;
}
private void sendDiscoveryRequest(String ipAddress) {
final @Nullable DatagramSocket socket = getSocket();
if (socket != null) {
try {
byte[] sendData = DISCOVER_STRING;
logger.trace("Discovery sending ping to {} from {}:{}", ipAddress, socket.getLocalAddress(),
socket.getLocalPort());
DatagramPacket sendPacket = new DatagramPacket(sendData, sendData.length,
InetAddress.getByName(ipAddress), PORT);
for (int i = 1; i <= 1; i++) {
socket.send(sendPacket);
}
} catch (IOException e) {
logger.trace("Discovery on {} error: {}", ipAddress, e.getMessage());
}
}
}
/**
* starts the {@link ReceiverThread} thread
*/
private synchronized void startReceiverThreat() {
final Thread srt = socketReceiveThread;
if (srt != null) {
if (srt.isAlive() && !srt.isInterrupted()) {
return;
}
}
stopReceiverThreat();
Thread socketReceiveThread = new ReceiverThread();
socketReceiveThread.start();
this.socketReceiveThread = socketReceiveThread;
}
/**
* Stops the {@link ReceiverThread} thread
*/
private synchronized void stopReceiverThreat() {
if (socketReceiveThread != null) {
socketReceiveThread.interrupt();
socketReceiveThread = null;
}
closeSocket();
}
/**
* The thread, which waits for data and submits the unique results addresses to the discovery results
*
*/
private class ReceiverThread extends Thread {
@Override
public void run() {
DatagramSocket socket = getSocket();
if (socket != null) {
logger.debug("Starting discovery receiver thread for socket on port {}", socket.getLocalPort());
receiveData(socket);
}
}
/**
* This method waits for data and submits the unique results addresses to the discovery results
*
* @param socket - The multicast socket to (re)use
*/
private void receiveData(DatagramSocket socket) {
DatagramPacket receivePacket = new DatagramPacket(new byte[BUFFER_LENGTH], BUFFER_LENGTH);
try {
while (!interrupted()) {
logger.trace("Thread {} waiting for data on port {}", this, socket.getLocalPort());
socket.receive(receivePacket);
String hostAddress = receivePacket.getAddress().getHostAddress();
logger.trace("Received {} bytes response from {}:{} on Port {}", receivePacket.getLength(),
hostAddress, receivePacket.getPort(), socket.getLocalPort());
byte[] messageBuf = Arrays.copyOfRange(receivePacket.getData(), receivePacket.getOffset(),
receivePacket.getOffset() + receivePacket.getLength());
if (logger.isTraceEnabled()) {
Message miIoResponse = new Message(messageBuf);
logger.trace("Discovery response received from {} DeviceID: {}\r\n{}", hostAddress,
Utils.getHex(miIoResponse.getDeviceId()), miIoResponse.toSting());
}
if (!responseIps.contains(hostAddress)) {
scheduler.schedule(() -> {
try {
discovered(hostAddress, messageBuf);
} catch (Exception e) {
logger.debug("Error submitting discovered Mi IO device at {}", hostAddress, e);
}
}, 0, TimeUnit.SECONDS);
}
responseIps.add(hostAddress);
}
} catch (SocketException e) {
logger.debug("Receiver thread received SocketException: {}", e.getMessage());
} catch (IOException e) {
logger.trace("Receiver thread was interrupted");
}
logger.debug("Receiver thread ended");
}
}
}

View File

@@ -0,0 +1,142 @@
/**
* Copyright (c) 2010-2020 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.discovery;
import static org.openhab.binding.miio.internal.MiIoBindingConstants.*;
import java.net.InetAddress;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import javax.jmdns.ServiceInfo;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.miio.internal.MiIoDevices;
import org.openhab.binding.miio.internal.cloud.CloudConnector;
import org.openhab.binding.miio.internal.cloud.CloudDeviceDTO;
import org.openhab.core.config.discovery.DiscoveryResult;
import org.openhab.core.config.discovery.DiscoveryResultBuilder;
import org.openhab.core.config.discovery.mdns.MDNSDiscoveryParticipant;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.ThingUID;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Discovers Mi IO devices announced by mDNS
*
* @author Marcel Verpaalen - Initial contribution
*
*/
@NonNullByDefault
@Component(service = MDNSDiscoveryParticipant.class, immediate = true)
public class MiIoDiscoveryParticipant implements MDNSDiscoveryParticipant {
private final CloudConnector cloudConnector;
private Logger logger = LoggerFactory.getLogger(MiIoDiscoveryParticipant.class);
@Activate
public MiIoDiscoveryParticipant(@Reference CloudConnector cloudConnector) {
this.cloudConnector = cloudConnector;
logger.debug("Start Xiaomi Mi IO mDNS discovery");
}
@Override
public Set<ThingTypeUID> getSupportedThingTypeUIDs() {
return (NONGENERIC_THING_TYPES_UIDS);
}
@Override
public String getServiceType() {
return "_miio._udp.local.";
}
@Override
public @Nullable ThingUID getThingUID(@Nullable ServiceInfo service) {
if (service == null) {
return null;
}
logger.trace("ServiceInfo: {}", service);
String id[] = service.getName().split("_miio");
if (id.length != 2) {
logger.trace("mDNS Could not identify Type / Device Id from '{}'", service.getName());
return null;
}
long did;
try {
did = Long.parseUnsignedLong(id[1]);
} catch (Exception e) {
logger.trace("mDNS Could not identify Device ID from '{}'", id[1]);
return null;
}
ThingTypeUID thingType = MiIoDevices.getType(id[0].replaceAll("-", ".")).getThingType();
String uidName = String.format("%08X", did);
logger.debug("mDNS {} identified as thingtype {} with did {} ({})", id[0], thingType, uidName, did);
return new ThingUID(thingType, uidName);
}
private @Nullable InetAddress getIpAddress(ServiceInfo service) {
InetAddress address = null;
for (InetAddress addr : service.getInet4Addresses()) {
return addr;
}
// Fallback for Inet6addresses
for (InetAddress addr : service.getInet6Addresses()) {
return addr;
}
return address;
}
@Override
public @Nullable DiscoveryResult createResult(ServiceInfo service) {
DiscoveryResult result = null;
ThingUID uid = getThingUID(service);
if (uid != null) {
Map<String, Object> properties = new HashMap<>(2);
// remove the domain from the name
InetAddress ip = getIpAddress(service);
if (ip == null) {
logger.debug("Mi IO mDNS Discovery could not determine ip address from service info: {}", service);
return null;
}
String inetAddress = ip.toString().substring(1); // trim leading slash
String id = uid.getId();
String label = "Xiaomi Mi Device " + id + " (" + Long.parseUnsignedLong(id, 16) + ") " + service.getName();
if (cloudConnector.isConnected()) {
cloudConnector.getDevicesList();
CloudDeviceDTO cloudInfo = cloudConnector.getDeviceInfo(id);
if (cloudInfo != null) {
logger.debug("Cloud Info: {}", cloudInfo);
properties.put(PROPERTY_TOKEN, cloudInfo.getToken());
label = label + " with token";
String country = cloudInfo.getServer();
if (!country.isEmpty() && cloudInfo.getIsOnline()) {
properties.put(PROPERTY_CLOUDSERVER, country);
}
}
}
properties.put(PROPERTY_HOST_IP, inetAddress);
properties.put(PROPERTY_DID, id);
result = DiscoveryResultBuilder.create(uid).withProperties(properties)
.withRepresentationProperty(PROPERTY_DID).withLabel(label).build();
logger.debug("Mi IO mDNS Discovery found {} with address '{}:{}' name '{}'", uid, inetAddress,
service.getPort(), label);
}
return result;
}
}

View File

@@ -0,0 +1,492 @@
/**
* Copyright (c) 2010-2020 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.handler;
import static org.openhab.binding.miio.internal.MiIoBindingConstants.*;
import java.io.IOException;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.miio.internal.Message;
import org.openhab.binding.miio.internal.MiIoBindingConfiguration;
import org.openhab.binding.miio.internal.MiIoCommand;
import org.openhab.binding.miio.internal.MiIoCrypto;
import org.openhab.binding.miio.internal.MiIoCryptoException;
import org.openhab.binding.miio.internal.MiIoDevices;
import org.openhab.binding.miio.internal.MiIoInfoDTO;
import org.openhab.binding.miio.internal.MiIoMessageListener;
import org.openhab.binding.miio.internal.MiIoSendCommand;
import org.openhab.binding.miio.internal.Utils;
import org.openhab.binding.miio.internal.basic.MiIoDatabaseWatchService;
import org.openhab.binding.miio.internal.transport.MiIoAsyncCommunication;
import org.openhab.core.cache.ExpiringCache;
import org.openhab.core.config.core.Configuration;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.StringType;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.binding.BaseThingHandler;
import org.openhab.core.thing.binding.builder.ThingBuilder;
import org.openhab.core.types.Command;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
/**
* The {@link MiIoAbstractHandler} is responsible for handling commands, which are
* sent to one of the channels.
*
* @author Marcel Verpaalen - Initial contribution
*/
@NonNullByDefault
public abstract class MiIoAbstractHandler extends BaseThingHandler implements MiIoMessageListener {
protected static final int MAX_QUEUE = 5;
protected static final Gson GSON = new GsonBuilder().create();
protected @Nullable ScheduledFuture<?> pollingJob;
protected MiIoDevices miDevice = MiIoDevices.UNKNOWN;
protected boolean isIdentified;
protected final JsonParser parser = new JsonParser();
protected byte[] token = new byte[0];
protected @Nullable MiIoBindingConfiguration configuration;
protected @Nullable MiIoAsyncCommunication miioCom;
protected int lastId;
protected Map<Integer, String> cmds = new ConcurrentHashMap<>();
protected Map<String, Object> deviceVariables = new HashMap<>();
protected final ExpiringCache<String> network = new ExpiringCache<>(CACHE_EXPIRY_NETWORK, () -> {
int ret = sendCommand(MiIoCommand.MIIO_INFO);
if (ret != 0) {
return "id:" + ret;
}
return "failed";
});;
protected static final long CACHE_EXPIRY = TimeUnit.SECONDS.toMillis(5);
protected static final long CACHE_EXPIRY_NETWORK = TimeUnit.SECONDS.toMillis(60);
private final Logger logger = LoggerFactory.getLogger(MiIoAbstractHandler.class);
protected MiIoDatabaseWatchService miIoDatabaseWatchService;
public MiIoAbstractHandler(Thing thing, MiIoDatabaseWatchService miIoDatabaseWatchService) {
super(thing);
this.miIoDatabaseWatchService = miIoDatabaseWatchService;
}
@Override
public abstract void handleCommand(ChannelUID channelUID, Command command);
@Override
public void initialize() {
logger.debug("Initializing Mi IO device handler '{}' with thingType {}", getThing().getUID(),
getThing().getThingTypeUID());
final MiIoBindingConfiguration configuration = getConfigAs(MiIoBindingConfiguration.class);
this.configuration = configuration;
if (configuration.host == null || configuration.host.isEmpty()) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"IP address required. Configure IP address");
return;
}
if (!tokenCheckPass(configuration.token)) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Token required. Configure token");
return;
}
isIdentified = false;
scheduler.schedule(this::initializeData, 1, TimeUnit.SECONDS);
int pollingPeriod = configuration.refreshInterval;
if (pollingPeriod > 0) {
pollingJob = scheduler.scheduleWithFixedDelay(() -> {
try {
updateData();
} catch (Exception e) {
logger.debug("Unexpected error during refresh.", e);
}
}, 10, pollingPeriod, TimeUnit.SECONDS);
logger.debug("Polling job scheduled to run every {} sec. for '{}'", pollingPeriod, getThing().getUID());
} else {
logger.debug("Polling job disabled. for '{}'", getThing().getUID());
scheduler.schedule(this::updateData, 10, TimeUnit.SECONDS);
}
updateStatus(ThingStatus.OFFLINE);
}
private boolean tokenCheckPass(@Nullable String tokenSting) {
if (tokenSting == null) {
return false;
}
switch (tokenSting.length()) {
case 16:
token = tokenSting.getBytes();
return true;
case 32:
if (!IGNORED_TOKENS.contains(tokenSting)) {
token = Utils.hexStringToByteArray(tokenSting);
return true;
}
return false;
case 96:
try {
token = Utils.hexStringToByteArray(MiIoCrypto.decryptToken(Utils.hexStringToByteArray(tokenSting)));
logger.debug("IOS token decrypted to {}", Utils.getHex(token));
} catch (MiIoCryptoException e) {
logger.warn("Could not decrypt token {}{}", tokenSting, e.getMessage());
return false;
}
return true;
default:
return false;
}
}
@Override
public void dispose() {
logger.debug("Disposing Xiaomi Mi IO handler '{}'", getThing().getUID());
final ScheduledFuture<?> pollingJob = this.pollingJob;
if (pollingJob != null) {
pollingJob.cancel(true);
this.pollingJob = null;
}
final @Nullable MiIoAsyncCommunication miioCom = this.miioCom;
if (miioCom != null) {
lastId = miioCom.getId();
miioCom.unregisterListener(this);
miioCom.close();
this.miioCom = null;
}
}
protected int sendCommand(MiIoCommand command) {
return sendCommand(command, "[]");
}
protected int sendCommand(MiIoCommand command, String params) {
try {
final MiIoAsyncCommunication connection = getConnection();
return (connection != null) ? connection.queueCommand(command, params) : 0;
} catch (MiIoCryptoException | IOException e) {
logger.debug("Command {} for {} failed (type: {}): {}", command.toString(), getThing().getUID(),
getThing().getThingTypeUID(), e.getLocalizedMessage());
}
return 0;
}
/**
* This is used to execute arbitrary commands by sending to the commands channel. Command parameters to be added
* between
* [] brackets. This to allow for unimplemented commands to be executed (e.g. get detailed historical cleaning
* records)
*
* @param commandString command to be executed
* @return vacuum response
*/
protected int sendCommand(String commandString) {
final MiIoAsyncCommunication connection = getConnection();
try {
String command = commandString.trim();
String param = "[]";
int loc = command.indexOf("[");
loc = (loc > 0 ? loc : command.indexOf("{"));
if (loc > 0) {
param = command.substring(loc).trim();
command = command.substring(0, loc).trim();
}
return (connection != null) ? connection.queueCommand(command, param) : 0;
} catch (MiIoCryptoException | IOException e) {
disconnected(e.getMessage());
}
return 0;
}
protected boolean skipUpdate() {
final MiIoAsyncCommunication miioCom = this.miioCom;
if (!hasConnection() || miioCom == null) {
logger.debug("Skipping periodic update for '{}'. No Connection", getThing().getUID().toString());
return true;
}
if (getThing().getStatusInfo().getStatusDetail().equals(ThingStatusDetail.CONFIGURATION_ERROR)) {
logger.debug("Skipping periodic update for '{}'. Thing Status {}", getThing().getUID().toString(),
getThing().getStatusInfo().getStatusDetail());
try {
miioCom.queueCommand(MiIoCommand.MIIO_INFO);
} catch (MiIoCryptoException | IOException e) {
// ignore
}
return true;
}
if (miioCom.getQueueLength() > MAX_QUEUE) {
logger.debug("Skipping periodic update for '{}'. {} elements in queue.", getThing().getUID().toString(),
miioCom.getQueueLength());
return true;
}
return false;
}
protected abstract void updateData();
protected boolean updateNetwork(JsonObject networkData) {
try {
updateState(CHANNEL_SSID, new StringType(networkData.getAsJsonObject("ap").get("ssid").getAsString()));
updateState(CHANNEL_BSSID, new StringType(networkData.getAsJsonObject("ap").get("bssid").getAsString()));
if (networkData.getAsJsonObject("ap").get("rssi") != null) {
updateState(CHANNEL_RSSI, new DecimalType(networkData.getAsJsonObject("ap").get("rssi").getAsLong()));
} else if (networkData.getAsJsonObject("ap").get("wifi_rssi") != null) {
updateState(CHANNEL_RSSI,
new DecimalType(networkData.getAsJsonObject("ap").get("wifi_rssi").getAsLong()));
} else {
logger.debug("No RSSI info in response");
}
updateState(CHANNEL_LIFE, new DecimalType(networkData.get("life").getAsLong()));
return true;
} catch (Exception e) {
logger.debug("Could not parse network response: {}", networkData, e);
}
return false;
}
protected boolean hasConnection() {
return getConnection() != null;
}
protected void disconnectedNoResponse() {
disconnected("No Response from device");
}
protected void disconnected(String message) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, message);
final MiIoAsyncCommunication miioCom = this.miioCom;
if (miioCom != null) {
lastId = miioCom.getId();
lastId += 10;
}
}
protected synchronized @Nullable MiIoAsyncCommunication getConnection() {
if (miioCom != null) {
return miioCom;
}
final MiIoBindingConfiguration configuration = getConfigAs(MiIoBindingConfiguration.class);
if (configuration.host == null || configuration.host.isEmpty()) {
return null;
}
@Nullable
String deviceId = configuration.deviceId;
try {
if (deviceId != null && deviceId.length() == 8 && tokenCheckPass(configuration.token)) {
logger.debug("Ping Mi device {} at {}", deviceId, configuration.host);
final MiIoAsyncCommunication miioCom = new MiIoAsyncCommunication(configuration.host, token,
Utils.hexStringToByteArray(deviceId), lastId, configuration.timeout);
Message miIoResponse = miioCom.sendPing(configuration.host);
if (miIoResponse != null) {
logger.debug("Ping response from device {} at {}. Time stamp: {}, OH time {}, delta {}",
Utils.getHex(miIoResponse.getDeviceId()), configuration.host, miIoResponse.getTimestamp(),
LocalDateTime.now(), miioCom.getTimeDelta());
miioCom.registerListener(this);
this.miioCom = miioCom;
return miioCom;
} else {
miioCom.close();
}
} else {
logger.debug("No device ID defined. Retrieving Mi device ID");
final MiIoAsyncCommunication miioCom = new MiIoAsyncCommunication(configuration.host, token,
new byte[0], lastId, configuration.timeout);
Message miIoResponse = miioCom.sendPing(configuration.host);
if (miIoResponse != null) {
logger.debug("Ping response from device {} at {}. Time stamp: {}, OH time {}, delta {}",
Utils.getHex(miIoResponse.getDeviceId()), configuration.host, miIoResponse.getTimestamp(),
LocalDateTime.now(), miioCom.getTimeDelta());
deviceId = Utils.getHex(miIoResponse.getDeviceId());
logger.debug("Ping response from device {} at {}. Time stamp: {}, OH time {}, delta {}", deviceId,
configuration.host, miIoResponse.getTimestamp(), LocalDateTime.now(),
miioCom.getTimeDelta());
miioCom.setDeviceId(miIoResponse.getDeviceId());
logger.debug("Using retrieved Mi device ID: {}", deviceId);
updateDeviceIdConfig(deviceId);
miioCom.registerListener(this);
this.miioCom = miioCom;
return miioCom;
} else {
miioCom.close();
}
}
logger.debug("Ping response from device {} at {} FAILED", configuration.deviceId, configuration.host);
disconnectedNoResponse();
return null;
} catch (IOException e) {
logger.debug("Could not connect to {} at {}", getThing().getUID().toString(), configuration.host);
disconnected(e.getMessage());
return null;
}
}
private void updateDeviceIdConfig(String deviceId) {
if (!deviceId.isEmpty()) {
updateProperty(Thing.PROPERTY_SERIAL_NUMBER, deviceId);
Configuration config = editConfiguration();
config.put(PROPERTY_DID, deviceId);
updateConfiguration(config);
} else {
logger.debug("Could not update config with device ID: {}", deviceId);
}
}
protected boolean initializeData() {
this.miioCom = getConnection();
return true;
}
protected void refreshNetwork() {
network.getValue();
}
protected void defineDeviceType(JsonObject miioInfo) {
updateProperties(miioInfo);
isIdentified = updateThingType(miioInfo);
}
private void updateProperties(JsonObject miioInfo) {
final MiIoInfoDTO info = GSON.fromJson(miioInfo, MiIoInfoDTO.class);
Map<String, String> properties = editProperties();
if (info.model != null) {
properties.put(Thing.PROPERTY_MODEL_ID, info.model);
}
if (info.fwVer != null) {
properties.put(Thing.PROPERTY_FIRMWARE_VERSION, info.fwVer);
}
if (info.hwVer != null) {
properties.put(Thing.PROPERTY_HARDWARE_VERSION, info.hwVer);
}
if (info.wifiFwVer != null) {
properties.put("wifiFirmware", info.wifiFwVer);
}
if (info.mcuFwVer != null) {
properties.put("mcuFirmware", info.mcuFwVer);
}
deviceVariables.putAll(properties);
updateProperties(properties);
}
protected boolean updateThingType(JsonObject miioInfo) {
MiIoBindingConfiguration configuration = getConfigAs(MiIoBindingConfiguration.class);
String model = miioInfo.get("model").getAsString();
miDevice = MiIoDevices.getType(model);
if (configuration.model == null || configuration.model.isEmpty()) {
Configuration config = editConfiguration();
config.put(PROPERTY_MODEL, model);
updateConfiguration(config);
configuration = getConfigAs(MiIoBindingConfiguration.class);
}
if (!configuration.model.equals(model)) {
logger.info("Mi Device model {} has model config: {}. Unexpected unless manual override", model,
configuration.model);
}
if (miDevice.getThingType().equals(getThing().getThingTypeUID())
&& !(miDevice.getThingType().equals(THING_TYPE_UNSUPPORTED)
&& miIoDatabaseWatchService.getDatabaseUrl(model) != null)) {
logger.debug("Mi Device model {} identified as: {}. Matches thingtype {}", model, miDevice.toString(),
miDevice.getThingType().toString());
return true;
} else {
if (getThing().getThingTypeUID().equals(THING_TYPE_MIIO)
|| getThing().getThingTypeUID().equals(THING_TYPE_UNSUPPORTED)) {
changeType(model);
} else {
logger.info(
"Mi Device model {} identified as: {}, thingtype {}. Does not matches thingtype {}. Unexpected, unless manual override.",
miDevice.toString(), miDevice.getThingType(), getThing().getThingTypeUID().toString(),
miDevice.getThingType().toString());
return true;
}
}
return false;
}
/**
* Changes the {@link org.openhab.core.thing.type.ThingType} to the right type once it is retrieved from
* the device.
*
* @param modelId String with the model id
*/
private void changeType(final String modelId) {
final ScheduledFuture<?> pollingJob = this.pollingJob;
if (pollingJob != null) {
pollingJob.cancel(true);
this.pollingJob = null;
}
scheduler.schedule(() -> {
ThingBuilder thingBuilder = editThing();
thingBuilder.withLabel(miDevice.getDescription());
updateThing(thingBuilder.build());
logger.info("Mi Device model {} identified as: {}. Does not match thingtype {}. Changing thingtype to {}",
modelId, miDevice.toString(), getThing().getThingTypeUID().toString(),
miDevice.getThingType().toString());
ThingTypeUID thingTypeUID = MiIoDevices.getType(modelId).getThingType();
if (thingTypeUID.equals(THING_TYPE_UNSUPPORTED)
&& miIoDatabaseWatchService.getDatabaseUrl(modelId) != null) {
thingTypeUID = THING_TYPE_BASIC;
}
changeThingType(thingTypeUID, getConfig());
}, 10, TimeUnit.SECONDS);
}
@Override
public void onStatusUpdated(ThingStatus status, ThingStatusDetail statusDetail) {
updateStatus(status, statusDetail);
}
@Override
public void onMessageReceived(MiIoSendCommand response) {
logger.debug("Received response for {} type: {}, result: {}, fullresponse: {}", getThing().getUID().getId(),
response.getCommand(), response.getResult(), response.getResponse());
if (response.isError()) {
logger.debug("Error received: {}", response.getResponse().get("error"));
if (MiIoCommand.MIIO_INFO.equals(response.getCommand())) {
network.invalidateValue();
}
return;
}
try {
switch (response.getCommand()) {
case MIIO_INFO:
if (!isIdentified) {
defineDeviceType(response.getResult().getAsJsonObject());
}
updateNetwork(response.getResult().getAsJsonObject());
break;
default:
break;
}
if (cmds.containsKey(response.getId())) {
updateState(CHANNEL_COMMAND, new StringType(response.getResponse().toString()));
cmds.remove(response.getId());
}
} catch (Exception e) {
logger.debug("Error while handing message {}", response.getResponse(), e);
}
}
}

View File

@@ -0,0 +1,534 @@
/**
* Copyright (c) 2010-2020 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.handler;
import static org.openhab.binding.miio.internal.MiIoBindingConstants.CHANNEL_COMMAND;
import java.awt.Color;
import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.miio.internal.MiIoBindingConfiguration;
import org.openhab.binding.miio.internal.MiIoCommand;
import org.openhab.binding.miio.internal.MiIoCryptoException;
import org.openhab.binding.miio.internal.MiIoSendCommand;
import org.openhab.binding.miio.internal.Utils;
import org.openhab.binding.miio.internal.basic.ActionConditions;
import org.openhab.binding.miio.internal.basic.CommandParameterType;
import org.openhab.binding.miio.internal.basic.Conversions;
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.basic.MiIoDeviceAction;
import org.openhab.binding.miio.internal.basic.MiIoDeviceActionCondition;
import org.openhab.binding.miio.internal.transport.MiIoAsyncCommunication;
import org.openhab.core.cache.ExpiringCache;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.HSBType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.PercentType;
import org.openhab.core.library.types.StringType;
import org.openhab.core.thing.Channel;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.binding.builder.ChannelBuilder;
import org.openhab.core.thing.binding.builder.ThingBuilder;
import org.openhab.core.thing.type.ChannelTypeUID;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonIOException;
import com.google.gson.JsonObject;
import com.google.gson.JsonPrimitive;
import com.google.gson.JsonSyntaxException;
/**
* The {@link MiIoBasicHandler} is responsible for handling commands, which are
* sent to one of the channels.
*
* @author Marcel Verpaalen - Initial contribution
*/
@NonNullByDefault
public class MiIoBasicHandler extends MiIoAbstractHandler {
private final Logger logger = LoggerFactory.getLogger(MiIoBasicHandler.class);
private boolean hasChannelStructure;
private final ExpiringCache<Boolean> updateDataCache = new ExpiringCache<>(CACHE_EXPIRY, () -> {
scheduler.schedule(this::updateData, 0, TimeUnit.SECONDS);
return true;
});
List<MiIoBasicChannel> refreshList = new ArrayList<>();
private @Nullable MiIoBasicDevice miioDevice;
private Map<ChannelUID, MiIoBasicChannel> actions = new HashMap<>();
public MiIoBasicHandler(Thing thing, MiIoDatabaseWatchService miIoDatabaseWatchService) {
super(thing, miIoDatabaseWatchService);
}
@Override
public void initialize() {
super.initialize();
hasChannelStructure = false;
isIdentified = false;
refreshList = new ArrayList<>();
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
if (command == RefreshType.REFRESH) {
if (updateDataCache.isExpired()) {
logger.debug("Refreshing {}", channelUID);
updateDataCache.getValue();
} else {
logger.debug("Refresh {} skipped. Already refreshing", channelUID);
}
return;
}
if (channelUID.getId().equals(CHANNEL_COMMAND)) {
cmds.put(sendCommand(command.toString()), command.toString());
return;
}
logger.debug("Locating action for channel '{}': '{}'", channelUID.getId(), command);
if (!actions.isEmpty()) {
if (actions.containsKey(channelUID)) {
int valuePos = 0;
MiIoBasicChannel miIoBasicChannel = actions.get(channelUID);
for (MiIoDeviceAction action : miIoBasicChannel.getActions()) {
@Nullable
JsonElement value = null;
JsonArray parameters = action.getParameters().deepCopy();
for (int i = 0; i < action.getParameters().size(); i++) {
JsonElement p = action.getParameters().get(i);
if (p.isJsonPrimitive() && p.getAsString().toLowerCase().contains("$value$")) {
valuePos = i;
break;
}
}
String cmd = action.getCommand();
CommandParameterType paramType = action.getparameterType();
if (paramType == CommandParameterType.COLOR) {
if (command instanceof HSBType) {
HSBType hsb = (HSBType) command;
Color color = Color.getHSBColor(hsb.getHue().floatValue() / 360,
hsb.getSaturation().floatValue() / 100, hsb.getBrightness().floatValue() / 100);
value = new JsonPrimitive(
(color.getRed() << 16) + (color.getGreen() << 8) + color.getBlue());
} else if (command instanceof DecimalType) {
// actually brightness is being set instead of a color
value = new JsonPrimitive(((DecimalType) command).toBigDecimal());
} else if (command instanceof OnOffType) {
value = new JsonPrimitive(command == OnOffType.ON ? 100 : 0);
} else {
logger.debug("Unsupported command for COLOR: {}", command);
}
} else if (command instanceof OnOffType) {
if (paramType == CommandParameterType.ONOFF) {
value = new JsonPrimitive(command == OnOffType.ON ? "on" : "off");
} else if (paramType == CommandParameterType.ONOFFPARA) {
cmd = cmd.replace("*", command == OnOffType.ON ? "on" : "off");
} else if (paramType == CommandParameterType.ONOFFBOOL) {
boolean boolCommand = command == OnOffType.ON;
value = new JsonPrimitive(boolCommand);
} else if (paramType == CommandParameterType.ONOFFBOOLSTRING) {
value = new JsonPrimitive(command == OnOffType.ON ? "true" : "false");
}
} else if (command instanceof DecimalType) {
value = new JsonPrimitive(((DecimalType) command).toBigDecimal());
} else if (command instanceof StringType) {
if (paramType == CommandParameterType.STRING) {
value = new JsonPrimitive(command.toString().toLowerCase());
} else if (paramType == CommandParameterType.CUSTOMSTRING) {
value = new JsonPrimitive(parameters.get(valuePos).getAsString().replace("$value",
command.toString().toLowerCase()));
}
} else {
value = new JsonPrimitive(command.toString().toLowerCase());
}
if (paramType == CommandParameterType.EMPTY) {
value = new JsonArray();
}
final MiIoDeviceActionCondition miIoDeviceActionCondition = action.getCondition();
if (miIoDeviceActionCondition != null) {
value = ActionConditions.executeAction(miIoDeviceActionCondition, deviceVariables, value,
command);
}
// Check for miot channel
if (value != null) {
if (action.isMiOtAction()) {
value = miotActionTransform(action, miIoBasicChannel, value);
} else if (miIoBasicChannel.isMiOt()) {
value = miotTransform(miIoBasicChannel, value);
}
}
if (paramType != CommandParameterType.NONE && value != null) {
if (parameters.size() > 0) {
parameters.set(valuePos, value);
} else {
parameters.add(value);
}
}
cmd = cmd + parameters.toString();
if (value != null) {
logger.debug("Sending command {}", cmd);
sendCommand(cmd);
} else {
if (miIoDeviceActionCondition != null) {
logger.debug("Conditional command {} not send, condition '{}' not met", cmd,
miIoDeviceActionCondition.getName());
} else {
logger.debug("Command not send. Value null");
}
}
}
} else {
logger.debug("Channel Id {} not in mapping.", channelUID.getId());
if (logger.isTraceEnabled()) {
for (ChannelUID a : actions.keySet()) {
logger.trace("Available entries: {} : {}", a, actions.get(a).getFriendlyName());
}
}
}
updateDataCache.invalidateValue();
updateData();
} else {
logger.debug("Actions not loaded yet");
}
}
private @Nullable JsonElement miotTransform(MiIoBasicChannel miIoBasicChannel, @Nullable JsonElement value) {
JsonObject json = new JsonObject();
json.addProperty("did", miIoBasicChannel.getChannel());
json.addProperty("siid", miIoBasicChannel.getSiid());
json.addProperty("piid", miIoBasicChannel.getPiid());
json.add("value", value);
return json;
}
private @Nullable JsonElement miotActionTransform(MiIoDeviceAction action, MiIoBasicChannel miIoBasicChannel,
@Nullable JsonElement value) {
JsonObject json = new JsonObject();
json.addProperty("did", miIoBasicChannel.getChannel());
json.addProperty("siid", action.getSiid());
json.addProperty("aiid", action.getAiid());
if (value != null) {
json.add("in", value);
}
return json;
}
@Override
protected synchronized void updateData() {
logger.debug("Periodic update for '{}' ({})", getThing().getUID().toString(), getThing().getThingTypeUID());
final MiIoAsyncCommunication miioCom = getConnection();
try {
if (!hasConnection() || skipUpdate() || miioCom == null) {
return;
}
checkChannelStructure();
if (!isIdentified) {
miioCom.queueCommand(MiIoCommand.MIIO_INFO);
}
final MiIoBasicDevice midevice = miioDevice;
if (midevice != null) {
refreshProperties(midevice);
refreshNetwork();
}
} catch (Exception e) {
logger.debug("Error while updating '{}': ", getThing().getUID().toString(), e);
}
}
private boolean refreshProperties(MiIoBasicDevice device) {
MiIoCommand command = MiIoCommand.getCommand(device.getDevice().getPropertyMethod());
int maxProperties = device.getDevice().getMaxProperties();
JsonArray getPropString = new JsonArray();
for (MiIoBasicChannel miChannel : refreshList) {
JsonElement property;
if (miChannel.isMiOt()) {
JsonObject json = new JsonObject();
json.addProperty("did", miChannel.getProperty());
json.addProperty("siid", miChannel.getSiid());
json.addProperty("piid", miChannel.getPiid());
property = json;
} else {
property = new JsonPrimitive(miChannel.getProperty());
}
getPropString.add(property);
if (getPropString.size() >= maxProperties) {
sendRefreshProperties(command, getPropString);
getPropString = new JsonArray();
}
}
if (getPropString.size() > 0) {
sendRefreshProperties(command, getPropString);
}
return true;
}
private void sendRefreshProperties(MiIoCommand command, JsonArray getPropString) {
try {
final MiIoAsyncCommunication miioCom = this.miioCom;
if (miioCom != null) {
miioCom.queueCommand(command, getPropString.toString());
}
} catch (MiIoCryptoException | IOException e) {
logger.debug("Send refresh failed {}", e.getMessage(), e);
}
}
/**
* Checks if the channel structure has been build already based on the model data. If not build it.
*/
private void checkChannelStructure() {
final MiIoBindingConfiguration configuration = this.configuration;
if (configuration == null) {
return;
}
if (!hasChannelStructure) {
if (configuration.model == null || configuration.model.isEmpty()) {
logger.debug("Model needs to be determined");
isIdentified = false;
} else {
hasChannelStructure = buildChannelStructure(configuration.model);
}
}
if (hasChannelStructure) {
refreshList = new ArrayList<>();
final MiIoBasicDevice miioDevice = this.miioDevice;
if (miioDevice != null) {
for (MiIoBasicChannel miChannel : miioDevice.getDevice().getChannels()) {
if (miChannel.getRefresh()) {
refreshList.add(miChannel);
}
}
}
}
}
private boolean buildChannelStructure(String deviceName) {
logger.debug("Building Channel Structure for {} - Model: {}", getThing().getUID().toString(), deviceName);
URL fn = miIoDatabaseWatchService.getDatabaseUrl(deviceName);
if (fn == null) {
logger.warn("Database entry for model '{}' cannot be found.", deviceName);
return false;
}
try {
JsonObject deviceMapping = Utils.convertFileToJSON(fn);
logger.debug("Using device database: {} for device {}", fn.getFile(), deviceName);
Gson gson = new GsonBuilder().serializeNulls().create();
miioDevice = gson.fromJson(deviceMapping, MiIoBasicDevice.class);
for (Channel ch : getThing().getChannels()) {
logger.debug("Current thing channels {}, type: {}", ch.getUID(), ch.getChannelTypeUID());
}
ThingBuilder thingBuilder = editThing();
int channelsAdded = 0;
// make a map of the actions
actions = new HashMap<>();
final MiIoBasicDevice device = this.miioDevice;
if (device != null) {
for (MiIoBasicChannel miChannel : device.getDevice().getChannels()) {
logger.debug("properties {}", miChannel);
if (!miChannel.getType().isEmpty()) {
ChannelUID channelUID = addChannel(thingBuilder, miChannel.getChannel(),
miChannel.getChannelType(), miChannel.getType(), miChannel.getFriendlyName());
if (channelUID != null) {
actions.put(channelUID, miChannel);
channelsAdded++;
} else {
logger.debug("Channel for {} ({}) not loaded", miChannel.getChannel(),
miChannel.getFriendlyName());
}
} else {
logger.debug("Channel {} ({}), not loaded, missing type", miChannel.getChannel(),
miChannel.getFriendlyName());
}
}
}
// only update if channels were added/removed
if (channelsAdded > 0) {
logger.debug("Current thing channels added: {}", channelsAdded);
updateThing(thingBuilder.build());
}
return true;
} catch (JsonIOException | JsonSyntaxException e) {
logger.warn("Error parsing database Json", e);
} catch (IOException e) {
logger.warn("Error reading database file", e);
} catch (Exception e) {
logger.warn("Error creating channel structure", e);
}
return false;
}
private @Nullable ChannelUID addChannel(ThingBuilder thingBuilder, @Nullable String channel, String channelType,
@Nullable String datatype, String friendlyName) {
if (channel == null || channel.isEmpty() || datatype == null || datatype.isEmpty()) {
logger.info("Channel '{}', UID '{}' cannot be added incorrectly configured database. ", channel,
getThing().getUID());
return null;
}
ChannelUID channelUID = new ChannelUID(getThing().getUID(), channel);
ChannelTypeUID channelTypeUID = new ChannelTypeUID(channelType);
// TODO: Need to understand if this harms anything. If yes, channel only to be added when not there already.
// current way allows to have no issues when channels are changing.
if (getThing().getChannel(channel) != null) {
logger.info("Channel '{}' for thing {} already exist... removing", channel, getThing().getUID());
thingBuilder.withoutChannel(new ChannelUID(getThing().getUID(), channel));
}
Channel newChannel = ChannelBuilder.create(channelUID, datatype).withType(channelTypeUID)
.withLabel(friendlyName).build();
thingBuilder.withChannel(newChannel);
return channelUID;
}
private @Nullable MiIoBasicChannel getChannel(String parameter) {
for (MiIoBasicChannel refreshEntry : refreshList) {
if (refreshEntry.getProperty().equals(parameter)) {
return refreshEntry;
}
}
logger.trace("Did not find channel for {} in {}", parameter, refreshList);
return null;
}
private void updatePropsFromJsonArray(MiIoSendCommand response) {
JsonArray res = response.getResult().getAsJsonArray();
JsonArray para = parser.parse(response.getCommandString()).getAsJsonObject().get("params").getAsJsonArray();
if (res.size() != para.size()) {
logger.debug("Unexpected size different. Request size {}, response size {}. (Req: {}, Resp:{})",
para.size(), res.size(), para, res);
}
for (int i = 0; i < para.size(); i++) {
// This is a miot parameter
String param;
final JsonElement paraElement = para.get(i);
if (paraElement.isJsonObject()) { // miot channel
param = paraElement.getAsJsonObject().get("did").getAsString();
} else {
param = paraElement.getAsString();
}
JsonElement val = res.get(i);
if (val.isJsonNull()) {
logger.debug("Property '{}' returned null (is it supported?).", param);
continue;
} else if (val.isJsonObject()) { // miot channel
val = val.getAsJsonObject().get("value");
}
MiIoBasicChannel basicChannel = getChannel(param);
updateChannel(basicChannel, param, val);
}
}
private void updatePropsFromJsonObject(MiIoSendCommand response) {
JsonObject res = response.getResult().getAsJsonObject();
for (Object k : res.keySet()) {
String param = (String) k;
JsonElement val = res.get(param);
if (val.isJsonNull()) {
logger.debug("Property '{}' returned null (is it supported?).", param);
continue;
}
MiIoBasicChannel basicChannel = getChannel(param);
updateChannel(basicChannel, param, val);
}
}
private void updateChannel(@Nullable MiIoBasicChannel basicChannel, String param, JsonElement value) {
JsonElement val = value;
if (basicChannel == null) {
logger.debug("Channel not found for {}", param);
return;
}
final String transformation = basicChannel.getTransfortmation();
if (transformation != null) {
JsonElement transformed = Conversions.execute(transformation, val);
logger.debug("Transformed with '{}': {} {} -> {} ", transformation, basicChannel.getFriendlyName(), val,
transformed);
val = transformed;
}
try {
switch (basicChannel.getType().toLowerCase()) {
case "number":
updateState(basicChannel.getChannel(), new DecimalType(val.getAsBigDecimal()));
break;
case "dimmer":
updateState(basicChannel.getChannel(), new PercentType(val.getAsBigDecimal()));
break;
case "string":
updateState(basicChannel.getChannel(), new StringType(val.getAsString()));
break;
case "switch":
updateState(basicChannel.getChannel(), val.getAsString().toLowerCase().equals("on")
|| val.getAsString().toLowerCase().equals("true") ? OnOffType.ON : OnOffType.OFF);
break;
case "color":
Color rgb = new Color(val.getAsInt());
HSBType hsb = HSBType.fromRGB(rgb.getRed(), rgb.getGreen(), rgb.getBlue());
updateState(basicChannel.getChannel(), hsb);
break;
default:
logger.debug("No update logic for channeltype '{}' ", basicChannel.getType());
}
} catch (Exception e) {
logger.debug("Error updating {} property {} with '{}' : {}: {}", getThing().getUID(),
basicChannel.getChannel(), val, e.getClass().getCanonicalName(), e.getMessage());
logger.trace("Property update error detail:", e);
}
}
@Override
public void onMessageReceived(MiIoSendCommand response) {
super.onMessageReceived(response);
if (response.isError()) {
return;
}
try {
switch (response.getCommand()) {
case MIIO_INFO:
break;
case GET_VALUE:
case GET_PROPERTIES:
case GET_PROPERTY:
if (response.getResult().isJsonArray()) {
updatePropsFromJsonArray(response);
} else if (response.getResult().isJsonObject()) {
updatePropsFromJsonObject(response);
}
break;
default:
break;
}
} catch (Exception e) {
logger.debug("Error while handing message {}", response.getResponse(), e);
}
}
}

View File

@@ -0,0 +1,64 @@
/**
* Copyright (c) 2010-2020 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.handler;
import static org.openhab.binding.miio.internal.MiIoBindingConstants.CHANNEL_COMMAND;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.miio.internal.basic.MiIoDatabaseWatchService;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link MiIoGenericHandler} is responsible for handling commands for devices that are not yet defined.
* Once the device has been determined, the proper handler is loaded.
*
* @author Marcel Verpaalen - Initial contribution
*/
@NonNullByDefault
public class MiIoGenericHandler extends MiIoAbstractHandler {
private final Logger logger = LoggerFactory.getLogger(MiIoGenericHandler.class);
public MiIoGenericHandler(Thing thing, MiIoDatabaseWatchService miIoDatabaseWatchService) {
super(thing, miIoDatabaseWatchService);
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
if (command == RefreshType.REFRESH) {
logger.debug("Refreshing {}", channelUID);
updateData();
return;
}
if (channelUID.getId().equals(CHANNEL_COMMAND)) {
cmds.put(sendCommand(command.toString()), command.toString());
}
}
@Override
protected synchronized void updateData() {
if (skipUpdate()) {
return;
}
logger.debug("Periodic update for '{}' ({})", getThing().getUID().toString(), getThing().getThingTypeUID());
try {
refreshNetwork();
} catch (Exception e) {
logger.debug("Error while updating '{}'", getThing().getUID().toString(), e);
}
}
}

View File

@@ -0,0 +1,120 @@
/**
* Copyright (c) 2010-2020 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.handler;
import static org.openhab.binding.miio.internal.MiIoBindingConstants.*;
import java.util.concurrent.TimeUnit;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.miio.internal.basic.MiIoDatabaseWatchService;
import org.openhab.core.cache.ExpiringCache;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link MiIoUnsupportedHandler} is responsible for handling commands, which are
* sent to one of the channels.
*
* @author Marcel Verpaalen - Initial contribution
*/
@NonNullByDefault
public class MiIoUnsupportedHandler extends MiIoAbstractHandler {
private final Logger logger = LoggerFactory.getLogger(MiIoUnsupportedHandler.class);
private final ExpiringCache<Boolean> updateDataCache = new ExpiringCache<>(CACHE_EXPIRY, () -> {
scheduler.schedule(this::updateData, 0, TimeUnit.SECONDS);
return true;
});
public MiIoUnsupportedHandler(Thing thing, MiIoDatabaseWatchService miIoDatabaseWatchService) {
super(thing, miIoDatabaseWatchService);
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
if (command == RefreshType.REFRESH) {
if (updateDataCache.isExpired()) {
logger.debug("Refreshing {}", channelUID);
updateDataCache.getValue();
} else {
logger.debug("Refresh {} skipped. Already refreshing", channelUID);
}
return;
}
if (channelUID.getId().equals(CHANNEL_POWER)) {
if (command.equals(OnOffType.ON)) {
sendCommand("set_power[\"on\"]");
} else {
sendCommand("set_power[\"off\"]");
}
}
if (channelUID.getId().equals(CHANNEL_COMMAND)) {
cmds.put(sendCommand(command.toString()), command.toString());
}
if (channelUID.getId().equals(CHANNEL_TESTCOMMANDS)) {
executeExperimentalCommands();
}
}
// TODO: In future version this ideally would test all known commands (e.g. from the database) and create/enable a
// channel if they appear to be supported
private void executeExperimentalCommands() {
String[] testCommands = new String[0];
switch (miDevice) {
case POWERPLUG:
case POWERPLUG2:
case POWERSTRIP:
case POWERSTRIP2:
case YEELIGHT_C1:
break;
case VACUUM:
testCommands = new String[] { "miIO.info", "get_current_sound", "get_map_v1", "get_serial_number",
"get_timezone" };
break;
case AIR_PURIFIERM:
case AIR_PURIFIER1:
case AIR_PURIFIER2:
case AIR_PURIFIER3:
case AIR_PURIFIER6:
break;
default:
testCommands = new String[] { "miIO.info" };
break;
}
logger.info("Start Experimental Testing of commands for device '{}'. ", miDevice.toString());
for (String c : testCommands) {
logger.info("Test command '{}'. Response: '{}'", c, sendCommand(c));
}
}
@Override
protected synchronized void updateData() {
if (skipUpdate()) {
return;
}
logger.debug("Periodic update for '{}' ({})", getThing().getUID().toString(), getThing().getThingTypeUID());
try {
refreshNetwork();
} catch (Exception e) {
logger.debug("Error while updating '{}' ({})", getThing().getUID().toString(), getThing().getThingTypeUID(),
e);
}
}
}

View File

@@ -0,0 +1,615 @@
/**
* Copyright (c) 2010-2020 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.handler;
import static org.openhab.binding.miio.internal.MiIoBindingConstants.*;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.time.Instant;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.Date;
import java.util.Map.Entry;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import javax.imageio.ImageIO;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.miio.internal.MiIoBindingConfiguration;
import org.openhab.binding.miio.internal.MiIoCommand;
import org.openhab.binding.miio.internal.MiIoSendCommand;
import org.openhab.binding.miio.internal.basic.MiIoDatabaseWatchService;
import org.openhab.binding.miio.internal.cloud.CloudConnector;
import org.openhab.binding.miio.internal.cloud.CloudUtil;
import org.openhab.binding.miio.internal.cloud.MiCloudException;
import org.openhab.binding.miio.internal.robot.ConsumablesType;
import org.openhab.binding.miio.internal.robot.FanModeType;
import org.openhab.binding.miio.internal.robot.RRMapDraw;
import org.openhab.binding.miio.internal.robot.RobotCababilities;
import org.openhab.binding.miio.internal.robot.StatusDTO;
import org.openhab.binding.miio.internal.robot.StatusType;
import org.openhab.binding.miio.internal.robot.VacuumErrorType;
import org.openhab.binding.miio.internal.transport.MiIoAsyncCommunication;
import org.openhab.core.cache.ExpiringCache;
import org.openhab.core.config.core.ConfigConstants;
import org.openhab.core.library.types.DateTimeType;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.RawType;
import org.openhab.core.library.types.StringType;
import org.openhab.core.thing.Channel;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.binding.builder.ChannelBuilder;
import org.openhab.core.thing.binding.builder.ThingBuilder;
import org.openhab.core.thing.type.ChannelType;
import org.openhab.core.thing.type.ChannelTypeRegistry;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
import org.openhab.core.types.State;
import org.openhab.core.types.UnDefType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
/**
* The {@link MiIoVacuumHandler} is responsible for handling commands, which are
* sent to one of the channels.
*
* @author Marcel Verpaalen - Initial contribution
*/
@NonNullByDefault
public class MiIoVacuumHandler extends MiIoAbstractHandler {
private final Logger logger = LoggerFactory.getLogger(MiIoVacuumHandler.class);
private static final float MAP_SCALE = 2.0f;
private static final SimpleDateFormat DATEFORMATTER = new SimpleDateFormat("yyyyMMdd-HHmmss");
private static final String MAP_PATH = ConfigConstants.getUserDataFolder() + File.separator + BINDING_ID
+ File.separator;
private static final Gson GSON = new GsonBuilder().serializeNulls().create();
private final ChannelUID mapChannelUid;
private ExpiringCache<String> status;
private ExpiringCache<String> consumables;
private ExpiringCache<String> dnd;
private ExpiringCache<String> history;
private int stateId;
private ExpiringCache<String> map;
private String lastHistoryId = "";
private String lastMap = "";
private CloudConnector cloudConnector;
private boolean hasChannelStructure;
private ConcurrentHashMap<RobotCababilities, Boolean> deviceCapabilities = new ConcurrentHashMap<>();
private ChannelTypeRegistry channelTypeRegistry;
public MiIoVacuumHandler(Thing thing, MiIoDatabaseWatchService miIoDatabaseWatchService,
CloudConnector cloudConnector, ChannelTypeRegistry channelTypeRegistry) {
super(thing, miIoDatabaseWatchService);
this.cloudConnector = cloudConnector;
this.channelTypeRegistry = channelTypeRegistry;
mapChannelUid = new ChannelUID(thing.getUID(), CHANNEL_VACUUM_MAP);
status = new ExpiringCache<>(CACHE_EXPIRY, () -> {
try {
int ret = sendCommand(MiIoCommand.GET_STATUS);
if (ret != 0) {
return "id:" + ret;
}
} catch (Exception e) {
logger.debug("Error during status refresh: {}", e.getMessage(), e);
}
return null;
});
consumables = new ExpiringCache<>(CACHE_EXPIRY, () -> {
try {
int ret = sendCommand(MiIoCommand.CONSUMABLES_GET);
if (ret != 0) {
return "id:" + ret;
}
} catch (Exception e) {
logger.debug("Error during consumables refresh: {}", e.getMessage(), e);
}
return null;
});
dnd = new ExpiringCache<>(CACHE_EXPIRY, () -> {
try {
int ret = sendCommand(MiIoCommand.DND_GET);
if (ret != 0) {
return "id:" + ret;
}
} catch (Exception e) {
logger.debug("Error during dnd refresh: {}", e.getMessage(), e);
}
return null;
});
history = new ExpiringCache<>(CACHE_EXPIRY, () -> {
try {
int ret = sendCommand(MiIoCommand.CLEAN_SUMMARY_GET);
if (ret != 0) {
return "id:" + ret;
}
} catch (Exception e) {
logger.debug("Error during cleaning data refresh: {}", e.getMessage(), e);
}
return null;
});
map = new ExpiringCache<String>(CACHE_EXPIRY, () -> {
try {
int ret = sendCommand(MiIoCommand.GET_MAP);
if (ret != 0) {
return "id:" + ret;
}
} catch (Exception e) {
logger.debug("Error during dnd refresh: {}", e.getMessage(), e);
}
return null;
});
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
if (getConnection() == null) {
logger.debug("Vacuum {} not online. Command {} ignored", getThing().getUID(), command.toString());
return;
}
if (command == RefreshType.REFRESH) {
logger.debug("Refreshing {}", channelUID);
updateData();
lastMap = "";
if (channelUID.getId().equals(CHANNEL_VACUUM_MAP)) {
sendCommand(MiIoCommand.GET_MAP);
}
return;
}
if (channelUID.getId().equals(CHANNEL_VACUUM)) {
if (command instanceof OnOffType) {
if (command.equals(OnOffType.ON)) {
sendCommand(MiIoCommand.START_VACUUM);
forceStatusUpdate();
return;
} else {
sendCommand(MiIoCommand.STOP_VACUUM);
scheduler.schedule(() -> {
sendCommand(MiIoCommand.CHARGE);
forceStatusUpdate();
}, 2000, TimeUnit.MILLISECONDS);
return;
}
}
}
if (channelUID.getId().equals(CHANNEL_CONTROL)) {
if (command.toString().equals("vacuum")) {
sendCommand(MiIoCommand.START_VACUUM);
} else if (command.toString().equals("spot")) {
sendCommand(MiIoCommand.START_SPOT);
} else if (command.toString().equals("pause")) {
sendCommand(MiIoCommand.PAUSE);
} else if (command.toString().equals("dock")) {
sendCommand(MiIoCommand.STOP_VACUUM);
scheduler.schedule(() -> {
sendCommand(MiIoCommand.CHARGE);
forceStatusUpdate();
}, 2000, TimeUnit.MILLISECONDS);
return;
} else {
logger.info("Command {} not recognised", command.toString());
}
forceStatusUpdate();
return;
}
if (channelUID.getId().equals(CHANNEL_FAN_POWER)) {
sendCommand(MiIoCommand.SET_MODE, "[" + command.toString() + "]");
forceStatusUpdate();
return;
}
if (channelUID.getId().equals(RobotCababilities.WATERBOX_MODE.getChannel())) {
sendCommand(MiIoCommand.SET_WATERBOX_MODE, "[" + command.toString() + "]");
forceStatusUpdate();
return;
}
if (channelUID.getId().equals(RobotCababilities.SEGMENT_CLEAN.getChannel()) && !command.toString().isEmpty()) {
sendCommand(MiIoCommand.START_SEGMENT, "[" + command.toString() + "]");
updateState(RobotCababilities.SEGMENT_CLEAN.getChannel(), UnDefType.UNDEF);
forceStatusUpdate();
return;
}
if (channelUID.getId().equals(CHANNEL_FAN_CONTROL)) {
if (Integer.valueOf(command.toString()) > 0) {
sendCommand(MiIoCommand.SET_MODE, "[" + command.toString() + "]");
}
forceStatusUpdate();
return;
}
if (channelUID.getId().equals(CHANNEL_CONSUMABLE_RESET)) {
sendCommand(MiIoCommand.CONSUMABLES_RESET, "[" + command.toString() + "]");
updateState(CHANNEL_CONSUMABLE_RESET, new StringType("none"));
}
if (channelUID.getId().equals(CHANNEL_COMMAND)) {
cmds.put(sendCommand(command.toString()), command.toString());
}
}
private void forceStatusUpdate() {
status.invalidateValue();
status.getValue();
}
private void safeUpdateState(String channelID, @Nullable Integer state) {
if (state != null) {
updateState(channelID, new DecimalType(state));
} else {
logger.debug("Channel {} not update. value not available.", channelID);
}
}
private boolean updateVacuumStatus(JsonObject statusData) {
StatusDTO statusInfo = GSON.fromJson(statusData, StatusDTO.class);
safeUpdateState(CHANNEL_BATTERY, statusInfo.getBattery());
if (statusInfo.getCleanArea() != null) {
updateState(CHANNEL_CLEAN_AREA, new DecimalType(statusInfo.getCleanArea() / 1000000.0));
}
if (statusInfo.getCleanTime() != null) {
updateState(CHANNEL_CLEAN_TIME, new DecimalType(TimeUnit.SECONDS.toMinutes(statusInfo.getCleanTime())));
}
safeUpdateState(CHANNEL_DND_ENABLED, statusInfo.getDndEnabled());
if (statusInfo.getErrorCode() != null) {
updateState(CHANNEL_ERROR_CODE,
new StringType(VacuumErrorType.getType(statusInfo.getErrorCode()).getDescription()));
safeUpdateState(CHANNEL_ERROR_ID, statusInfo.getErrorCode());
}
if (statusInfo.getFanPower() != null) {
updateState(CHANNEL_FAN_POWER, new DecimalType(statusInfo.getFanPower()));
updateState(CHANNEL_FAN_CONTROL, new DecimalType(FanModeType.getType(statusInfo.getFanPower()).getId()));
}
safeUpdateState(CHANNEL_IN_CLEANING, statusInfo.getInCleaning());
safeUpdateState(CHANNEL_MAP_PRESENT, statusInfo.getMapPresent());
if (statusInfo.getState() != null) {
StatusType state = StatusType.getType(statusInfo.getState());
updateState(CHANNEL_STATE, new StringType(state.getDescription()));
updateState(CHANNEL_STATE_ID, new DecimalType(statusInfo.getState()));
State vacuum = OnOffType.OFF;
String control;
switch (state) {
case ZONE:
case ROOM:
case CLEANING:
case RETURNING:
control = "vacuum";
vacuum = OnOffType.ON;
break;
case CHARGING:
case CHARGING_ERROR:
case DOCKING:
case FULL:
control = "dock";
break;
case SLEEPING:
case PAUSED:
case IDLE:
control = "pause";
break;
case SPOTCLEAN:
control = "spot";
vacuum = OnOffType.ON;
break;
default:
control = "undef";
break;
}
if (control.equals("undef")) {
updateState(CHANNEL_CONTROL, UnDefType.UNDEF);
} else {
updateState(CHANNEL_CONTROL, new StringType(control));
}
updateState(CHANNEL_VACUUM, vacuum);
}
if (deviceCapabilities.containsKey(RobotCababilities.WATERBOX_MODE)) {
safeUpdateState(RobotCababilities.WATERBOX_MODE.getChannel(), statusInfo.getWaterBoxMode());
}
if (deviceCapabilities.containsKey(RobotCababilities.WATERBOX_STATUS)) {
safeUpdateState(RobotCababilities.WATERBOX_STATUS.getChannel(), statusInfo.getWaterBoxStatus());
}
if (deviceCapabilities.containsKey(RobotCababilities.WATERBOX_CARRIAGE)) {
safeUpdateState(RobotCababilities.WATERBOX_CARRIAGE.getChannel(), statusInfo.getWaterBoxCarriageStatus());
}
if (deviceCapabilities.containsKey(RobotCababilities.LOCKSTATUS)) {
safeUpdateState(RobotCababilities.LOCKSTATUS.getChannel(), statusInfo.getLockStatus());
}
if (deviceCapabilities.containsKey(RobotCababilities.MOP_FORBIDDEN)) {
safeUpdateState(RobotCababilities.MOP_FORBIDDEN.getChannel(), statusInfo.getMopForbiddenEnable());
}
return true;
}
private boolean updateConsumables(JsonObject consumablesData) {
int mainBrush = consumablesData.get("main_brush_work_time").getAsInt();
int sideBrush = consumablesData.get("side_brush_work_time").getAsInt();
int filter = consumablesData.get("filter_work_time").getAsInt();
int sensor = consumablesData.get("sensor_dirty_time").getAsInt();
updateState(CHANNEL_CONSUMABLE_MAIN_TIME,
new DecimalType(ConsumablesType.remainingHours(mainBrush, ConsumablesType.MAIN_BRUSH)));
updateState(CHANNEL_CONSUMABLE_MAIN_PERC,
new DecimalType(ConsumablesType.remainingPercent(mainBrush, ConsumablesType.MAIN_BRUSH)));
updateState(CHANNEL_CONSUMABLE_SIDE_TIME,
new DecimalType(ConsumablesType.remainingHours(sideBrush, ConsumablesType.SIDE_BRUSH)));
updateState(CHANNEL_CONSUMABLE_SIDE_PERC,
new DecimalType(ConsumablesType.remainingPercent(sideBrush, ConsumablesType.SIDE_BRUSH)));
updateState(CHANNEL_CONSUMABLE_FILTER_TIME,
new DecimalType(ConsumablesType.remainingHours(filter, ConsumablesType.FILTER)));
updateState(CHANNEL_CONSUMABLE_FILTER_PERC,
new DecimalType(ConsumablesType.remainingPercent(filter, ConsumablesType.FILTER)));
updateState(CHANNEL_CONSUMABLE_SENSOR_TIME,
new DecimalType(ConsumablesType.remainingHours(sensor, ConsumablesType.SENSOR)));
updateState(CHANNEL_CONSUMABLE_SENSOR_PERC,
new DecimalType(ConsumablesType.remainingPercent(sensor, ConsumablesType.SENSOR)));
return true;
}
private boolean updateDnD(JsonObject dndData) {
logger.trace("Do not disturb data: {}", dndData.toString());
updateState(CHANNEL_DND_FUNCTION, new DecimalType(dndData.get("enabled").getAsBigDecimal()));
updateState(CHANNEL_DND_START, new StringType(String.format("%02d:%02d", dndData.get("start_hour").getAsInt(),
dndData.get("start_minute").getAsInt())));
updateState(CHANNEL_DND_END, new StringType(
String.format("%02d:%02d", dndData.get("end_hour").getAsInt(), dndData.get("end_minute").getAsInt())));
return true;
}
private boolean updateHistory(JsonArray historyData) {
logger.trace("Cleaning history data: {}", historyData.toString());
updateState(CHANNEL_HISTORY_TOTALTIME,
new DecimalType(TimeUnit.SECONDS.toMinutes(historyData.get(0).getAsLong())));
updateState(CHANNEL_HISTORY_TOTALAREA, new DecimalType(historyData.get(1).getAsDouble() / 1000000D));
updateState(CHANNEL_HISTORY_COUNT, new DecimalType(historyData.get(2).toString()));
if (historyData.get(3).getAsJsonArray().size() > 0) {
String lastClean = historyData.get(3).getAsJsonArray().get(0).getAsString();
if (!lastClean.equals(lastHistoryId)) {
lastHistoryId = lastClean;
sendCommand(MiIoCommand.CLEAN_RECORD_GET, "[" + lastClean + "]");
}
}
return true;
}
private void updateHistoryRecord(JsonArray historyData) {
ZonedDateTime startTime = ZonedDateTime.ofInstant(Instant.ofEpochSecond(historyData.get(0).getAsLong()),
ZoneId.systemDefault());
ZonedDateTime endTime = ZonedDateTime.ofInstant(Instant.ofEpochSecond(historyData.get(1).getAsLong()),
ZoneId.systemDefault());
long duration = TimeUnit.SECONDS.toMinutes(historyData.get(2).getAsLong());
double area = historyData.get(3).getAsDouble() / 1000000D;
int error = historyData.get(4).getAsInt();
int finished = historyData.get(5).getAsInt();
JsonObject historyRecord = new JsonObject();
historyRecord.addProperty("start", startTime.toString());
historyRecord.addProperty("end", endTime.toString());
historyRecord.addProperty("duration", duration);
historyRecord.addProperty("area", area);
historyRecord.addProperty("error", error);
historyRecord.addProperty("finished", finished);
updateState(CHANNEL_HISTORY_START_TIME, new DateTimeType(startTime));
updateState(CHANNEL_HISTORY_END_TIME, new DateTimeType(endTime));
updateState(CHANNEL_HISTORY_DURATION, new DecimalType(duration));
updateState(CHANNEL_HISTORY_AREA, new DecimalType(area));
updateState(CHANNEL_HISTORY_ERROR, new DecimalType(error));
updateState(CHANNEL_HISTORY_FINISH, new DecimalType(finished));
updateState(CHANNEL_HISTORY_RECORD, new StringType(historyRecord.toString()));
}
@Override
protected boolean skipUpdate() {
if (!hasConnection()) {
logger.debug("Skipping periodic update for '{}'. No Connection", getThing().getUID().toString());
return true;
}
if (ThingStatusDetail.CONFIGURATION_ERROR.equals(getThing().getStatusInfo().getStatusDetail())) {
logger.debug("Skipping periodic update for '{}' UID '{}'. Thing Status", getThing().getUID().toString(),
getThing().getStatusInfo().getStatusDetail());
refreshNetwork();
return true;
}
final MiIoAsyncCommunication mc = miioCom;
if (mc != null && mc.getQueueLength() > MAX_QUEUE) {
logger.debug("Skipping periodic update for '{}'. {} elements in queue.", getThing().getUID().toString(),
mc.getQueueLength());
return true;
}
return false;
}
@Override
protected synchronized void updateData() {
if (!hasConnection() || skipUpdate()) {
return;
}
logger.debug("Periodic update for '{}' ({})", getThing().getUID().toString(), getThing().getThingTypeUID());
try {
dnd.getValue();
history.getValue();
status.getValue();
refreshNetwork();
consumables.getValue();
if (lastMap.isEmpty() || stateId != 8) {
if (isLinked(mapChannelUid)) {
map.getValue();
}
}
} catch (Exception e) {
logger.debug("Error while updating '{}': '{}", getThing().getUID().toString(), e.getLocalizedMessage());
}
}
@Override
public void initialize() {
super.initialize();
hasChannelStructure = false;
}
@Override
protected boolean initializeData() {
updateState(CHANNEL_CONSUMABLE_RESET, new StringType("none"));
return super.initializeData();
}
@Override
public void onMessageReceived(MiIoSendCommand response) {
super.onMessageReceived(response);
if (response.isError()) {
return;
}
switch (response.getCommand()) {
case GET_STATUS:
if (response.getResult().isJsonArray()) {
JsonObject statusResponse = response.getResult().getAsJsonArray().get(0).getAsJsonObject();
if (!hasChannelStructure) {
setCapabilities(statusResponse);
createCapabilityChannels();
}
updateVacuumStatus(statusResponse);
}
break;
case CONSUMABLES_GET:
if (response.getResult().isJsonArray()) {
updateConsumables(response.getResult().getAsJsonArray().get(0).getAsJsonObject());
}
break;
case DND_GET:
if (response.getResult().isJsonArray()) {
updateDnD(response.getResult().getAsJsonArray().get(0).getAsJsonObject());
}
break;
case CLEAN_SUMMARY_GET:
if (response.getResult().isJsonArray()) {
updateHistory(response.getResult().getAsJsonArray());
}
break;
case CLEAN_RECORD_GET:
if (response.getResult().isJsonArray() && response.getResult().getAsJsonArray().size() > 0
&& response.getResult().getAsJsonArray().get(0).isJsonArray()) {
updateHistoryRecord(response.getResult().getAsJsonArray().get(0).getAsJsonArray());
} else {
logger.debug("Could not extract cleaning history record from: {}", response);
}
break;
case GET_MAP:
if (response.getResult().isJsonArray()) {
String mapresponse = response.getResult().getAsJsonArray().get(0).getAsString();
if (!mapresponse.contentEquals("retry") && !mapresponse.contentEquals(lastMap)) {
lastMap = mapresponse;
scheduler.submit(() -> updateState(CHANNEL_VACUUM_MAP, getMap(mapresponse)));
}
}
break;
case UNKNOWN:
updateState(CHANNEL_COMMAND, new StringType(response.getResponse().toString()));
break;
default:
break;
}
}
private void setCapabilities(JsonObject statusResponse) {
for (RobotCababilities capability : RobotCababilities.values()) {
if (statusResponse.has(capability.getStatusFieldName())) {
deviceCapabilities.putIfAbsent(capability, false);
logger.debug("Setting additional vacuum {}", capability);
}
}
}
private void createCapabilityChannels() {
ThingBuilder thingBuilder = editThing();
int cnt = 0;
for (Entry<RobotCababilities, Boolean> robotCapability : deviceCapabilities.entrySet()) {
RobotCababilities capability = robotCapability.getKey();
Boolean channelCreated = robotCapability.getValue();
if (!channelCreated) {
if (thing.getChannels().stream()
.anyMatch(ch -> ch.getUID().getId().equalsIgnoreCase(capability.getChannel()))) {
logger.debug("Channel already available...skip creation of channel '{}'.", capability.getChannel());
deviceCapabilities.replace(capability, true);
continue;
}
logger.debug("Creating dynamic channel for capability {}", capability);
ChannelType channelType = channelTypeRegistry.getChannelType(capability.getChannelType());
if (channelType != null) {
logger.debug("Found channelType '{}' for capability {}", channelType, capability.name());
ChannelUID channelUID = new ChannelUID(getThing().getUID(), capability.getChannel());
Channel channel = ChannelBuilder.create(channelUID, channelType.getItemType())
.withType(capability.getChannelType()).withLabel(channelType.getLabel()).build();
thingBuilder.withChannel(channel);
cnt++;
} else {
logger.debug("ChannelType {} not found (Unexpected). Available types:",
capability.getChannelType());
for (ChannelType ct : channelTypeRegistry.getChannelTypes()) {
logger.debug("Available channelType: '{}' '{}' '{}'", ct.getUID(), ct.toString(),
ct.getConfigDescriptionURI());
}
}
}
}
if (cnt > 0) {
updateThing(thingBuilder.build());
}
hasChannelStructure = true;
}
private State getMap(String map) {
final MiIoBindingConfiguration configuration = this.configuration;
if (configuration != null && cloudConnector.isConnected()) {
try {
final @Nullable RawType mapDl = cloudConnector.getMap(map,
(configuration.cloudServer != null) ? configuration.cloudServer : "");
if (mapDl != null) {
byte[] mapData = mapDl.getBytes();
RRMapDraw rrMap = RRMapDraw.loadImage(new ByteArrayInputStream(mapData));
ByteArrayOutputStream baos = new ByteArrayOutputStream();
if (logger.isDebugEnabled()) {
final String mapPath = MAP_PATH + map + DATEFORMATTER.format(new Date()) + ".rrmap";
CloudUtil.writeBytesToFileNio(mapData, mapPath);
logger.debug("Mapdata saved to {}", mapPath);
}
ImageIO.write(rrMap.getImage(MAP_SCALE), "jpg", baos);
byte[] byteArray = baos.toByteArray();
if (byteArray != null && byteArray.length > 0) {
return new RawType(byteArray, "image/jpeg");
} else {
logger.debug("Mapdata empty removing image");
return UnDefType.UNDEF;
}
}
} catch (MiCloudException e) {
logger.debug("Error getting data from Xiaomi cloud. Mapdata could not be updated: {}", e.getMessage());
} catch (IOException e) {
logger.debug("Mapdata could not be updated: {}", e.getMessage());
}
} else {
logger.debug("Not connected to Xiaomi cloud. Cannot retreive new map: {}", map);
}
return UnDefType.UNDEF;
}
}

View File

@@ -0,0 +1,60 @@
/**
* Copyright (c) 2010-2020 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.robot;
import java.util.concurrent.TimeUnit;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Vacuum Consumables
*
* @author Marcel Verpaalen - Initial contribution
*/
@NonNullByDefault
public enum ConsumablesType {
MAIN_BRUSH(300, "Main Brush"),
SIDE_BRUSH(200, "Side Brush"),
FILTER(150, "Filter"),
SENSOR(30, "Sensor"),
UNKNOWN(0, "Unknown");
private final int lifeTime;
private final String description;
ConsumablesType(int lifeTime, String description) {
this.lifeTime = lifeTime;
this.description = description;
}
public static double remainingHours(int usedSeconds, ConsumablesType consumableType) {
return Math.max((double) consumableType.lifeTime - TimeUnit.SECONDS.toHours(usedSeconds), 0);
}
public static int remainingPercent(int usedSeconds, ConsumablesType consumableType) {
return (int) (100D * remainingHours(usedSeconds, consumableType) / consumableType.lifeTime);
}
public int getLifeTime() {
return lifeTime;
}
public String getDescription() {
return description;
}
@Override
public String toString() {
return description;
}
}

View File

@@ -0,0 +1,67 @@
/**
* Copyright (c) 2010-2020 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.robot;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* List of Errors
*
* @author Marcel Verpaalen - Initial contribution
*/
@NonNullByDefault
public enum FanModeType {
SILENT(38, "Silent"),
STANDARD(60, "Standard"),
TURBO(75, "Turbo"),
POWER(77, "Power"),
FULL(90, "Full"),
MAX(100, "Max"),
QUIET(101, "Quiet"),
BALANCED(102, "Balanced"),
TURBO2(103, "Turbo"),
MAX2(104, "Max"),
MOB(105, "Mob"),
CUSTOM(-1, "Custom");
private final int id;
private final String description;
FanModeType(int id, String description) {
this.id = id;
this.description = description;
}
public int getId() {
return id;
}
public static FanModeType getType(int value) {
for (FanModeType st : FanModeType.values()) {
if (st.getId() == value) {
return st;
}
}
return CUSTOM;
}
public String getDescription() {
return description;
}
@Override
public String toString() {
return "Status " + Integer.toString(id) + ": " + description;
}
}

View File

@@ -0,0 +1,434 @@
/**
* Copyright (c) 2010-2020 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.robot;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics2D;
import java.awt.GraphicsEnvironment;
import java.awt.Polygon;
import java.awt.Stroke;
import java.awt.geom.AffineTransform;
import java.awt.geom.Ellipse2D;
import java.awt.geom.Line2D;
import java.awt.geom.Path2D;
import java.awt.geom.Rectangle2D;
import java.awt.image.AffineTransformOp;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import javax.imageio.ImageIO;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.osgi.framework.Bundle;
import org.osgi.framework.FrameworkUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Draws the vacuum map file to an image
*
* @author Marcel Verpaalen - Initial contribution
*/
@NonNullByDefault
public class RRMapDraw {
private static final float MM = 50.0f;
private static final int MAP_OUTSIDE = 0x00;
private static final int MAP_WALL = 0x01;
private static final int MAP_INSIDE = 0xFF;
private static final int MAP_SCAN = 0x07;
private static final Color COLOR_MAP_INSIDE = new Color(32, 115, 185);
private static final Color COLOR_MAP_OUTSIDE = new Color(19, 87, 148);
private static final Color COLOR_MAP_WALL = new Color(100, 196, 254);
private static final Color COLOR_GREY_WALL = new Color(93, 109, 126);
private static final Color COLOR_PATH = new Color(147, 194, 238);
private static final Color COLOR_ZONES = new Color(0xAD, 0xD8, 0xFF, 0x8F);
private static final Color COLOR_NO_GO_ZONES = new Color(255, 33, 55, 127);
private static final Color COLOR_CHARGER_HALO = new Color(0x66, 0xfe, 0xda, 0x7f);
private static final Color COLOR_ROBO = new Color(75, 235, 149);
private static final Color COLOR_SCAN = new Color(0xDF, 0xDF, 0xDF);
private static final Color ROOM1 = new Color(240, 178, 122);
private static final Color ROOM2 = new Color(133, 193, 233);
private static final Color ROOM3 = new Color(217, 136, 128);
private static final Color ROOM4 = new Color(52, 152, 219);
private static final Color ROOM5 = new Color(205, 97, 85);
private static final Color ROOM6 = new Color(243, 156, 18);
private static final Color ROOM7 = new Color(88, 214, 141);
private static final Color ROOM8 = new Color(245, 176, 65);
private static final Color ROOM9 = new Color(0xFc, 0xD4, 0x51);
private static final Color ROOM10 = new Color(72, 201, 176);
private static final Color ROOM11 = new Color(84, 153, 199);
private static final Color ROOM12 = new Color(133, 193, 233);
private static final Color ROOM13 = new Color(245, 176, 65);
private static final Color ROOM14 = new Color(82, 190, 128);
private static final Color ROOM15 = new Color(72, 201, 176);
private static final Color ROOM16 = new Color(165, 105, 189);
private static final Color[] ROOM_COLORS = { ROOM1, ROOM2, ROOM3, ROOM4, ROOM5, ROOM6, ROOM7, ROOM8, ROOM9, ROOM10,
ROOM11, ROOM12, ROOM13, ROOM14, ROOM15, ROOM16 };
private final @Nullable Bundle bundle = FrameworkUtil.getBundle(getClass());
private boolean multicolor = false;
private final RRMapFileParser rmfp;
private final Logger logger = LoggerFactory.getLogger(RRMapDraw.class);
public RRMapDraw(RRMapFileParser rmfp) {
this.rmfp = rmfp;
}
public int getWidth() {
return rmfp.getImgWidth();
}
public int getHeight() {
return rmfp.getImgHeight();
}
public RRMapFileParser getMapParseDetails() {
return this.rmfp;
}
/**
* load Gzipped RR inputstream
*
* @throws IOException
*/
public static RRMapDraw loadImage(InputStream is) throws IOException {
byte[] inputdata = RRMapFileParser.readRRMapFile(is);
RRMapFileParser rf = new RRMapFileParser(inputdata);
return new RRMapDraw(rf);
}
/**
* load Gzipped RR file
*
* @throws IOException
*/
public static RRMapDraw loadImage(File file) throws IOException {
return loadImage(new FileInputStream(file));
}
/**
* draws the map from the individual pixels
*/
private void drawMap(Graphics2D g2d, float scale) {
Stroke stroke = new BasicStroke(1.1f * scale);
g2d.setStroke(stroke);
for (int y = 0; y < rmfp.getImgHeight() - 1; y++) {
for (int x = 0; x < rmfp.getImgWidth() + 1; x++) {
byte walltype = rmfp.getImage()[x + rmfp.getImgWidth() * y];
switch (walltype & 0xFF) {
case MAP_OUTSIDE:
g2d.setColor(COLOR_MAP_OUTSIDE);
break;
case MAP_WALL:
g2d.setColor(COLOR_MAP_WALL);
break;
case MAP_INSIDE:
g2d.setColor(COLOR_MAP_INSIDE);
break;
case MAP_SCAN:
g2d.setColor(COLOR_SCAN);
break;
default:
int obstacle = (walltype & 0x07);
int mapId = (walltype & 0xFF) >>> 3;
switch (obstacle) {
case 0:
g2d.setColor(COLOR_GREY_WALL);
break;
case 1:
g2d.setColor(Color.BLACK);
break;
case 7:
g2d.setColor(ROOM_COLORS[Math.round(mapId / 2)]);
multicolor = true;
break;
default:
g2d.setColor(Color.WHITE);
break;
}
}
float xPos = scale * (rmfp.getImgWidth() - x);
float yP = scale * y;
g2d.draw(new Line2D.Float(xPos, yP, xPos, yP));
}
}
}
/**
* draws the vacuum path
*
* @param scale
*/
private void drawPath(Graphics2D g2d, float scale) {
Stroke stroke = new BasicStroke(0.5f * scale);
g2d.setStroke(stroke);
for (Integer pathType : rmfp.getPaths().keySet()) {
switch (pathType) {
case RRMapFileParser.PATH:
if (!multicolor) {
g2d.setColor(COLOR_PATH);
} else {
g2d.setColor(Color.WHITE);
}
break;
case RRMapFileParser.GOTO_PATH:
g2d.setColor(Color.GREEN);
break;
case RRMapFileParser.GOTO_PREDICTED_PATH:
g2d.setColor(Color.YELLOW);
break;
default:
g2d.setColor(Color.CYAN);
}
float prvX = 0;
float prvY = 0;
for (float[] point : rmfp.getPaths().get(pathType)) {
float x = toXCoord(point[0]) * scale;
float y = toYCoord(point[1]) * scale;
if (prvX > 1) {
g2d.draw(new Line2D.Float(prvX, prvY, x, y));
}
prvX = x;
prvY = y;
}
}
}
private void drawZones(Graphics2D g2d, float scale) {
for (float[] point : rmfp.getZones()) {
float x = toXCoord(point[0]) * scale;
float y = toYCoord(point[1]) * scale;
float x1 = toXCoord(point[2]) * scale;
float y1 = toYCoord(point[3]) * scale;
float sx = Math.min(x, x1);
float w = Math.max(x, x1) - sx;
float sy = Math.min(y, y1);
float h = Math.max(y, y1) - sy;
g2d.setColor(COLOR_ZONES);
g2d.fill(new Rectangle2D.Float(sx, sy, w, h));
}
}
private void drawNoGo(Graphics2D g2d, float scale) {
for (Integer area : rmfp.getAreas().keySet()) {
for (float[] point : rmfp.getAreas().get(area)) {
float x = toXCoord(point[0]) * scale;
float y = toYCoord(point[1]) * scale;
float x1 = toXCoord(point[2]) * scale;
float y1 = toYCoord(point[3]) * scale;
float x2 = toXCoord(point[4]) * scale;
float y2 = toYCoord(point[5]) * scale;
float x3 = toXCoord(point[6]) * scale;
float y3 = toYCoord(point[7]) * scale;
Path2D noGo = new Path2D.Float();
noGo.moveTo(x, y);
noGo.lineTo(x1, y1);
noGo.lineTo(x2, y2);
noGo.lineTo(x3, y3);
noGo.lineTo(x, y);
g2d.setColor(COLOR_NO_GO_ZONES);
g2d.fill(noGo);
g2d.setColor(area == 9 ? Color.RED : Color.WHITE);
g2d.draw(noGo);
}
}
}
private void drawWalls(Graphics2D g2d, float scale) {
Stroke stroke = new BasicStroke(3 * scale);
g2d.setStroke(stroke);
for (float[] point : rmfp.getWalls()) {
float x = toXCoord(point[0]) * scale;
float y = toYCoord(point[1]) * scale;
float x1 = toXCoord(point[2]) * scale;
float y1 = toYCoord(point[3]) * scale;
g2d.setColor(Color.RED);
g2d.draw(new Line2D.Float(x, y, x1, y1));
}
}
private void drawRobo(Graphics2D g2d, float scale) {
float radius = 3 * scale;
Stroke stroke = new BasicStroke(2 * scale);
g2d.setStroke(stroke);
g2d.setColor(COLOR_CHARGER_HALO);
final float chargerX = toXCoord(rmfp.getChargerX()) * scale;
final float chargerY = toYCoord(rmfp.getChargerY()) * scale;
drawCircle(g2d, chargerX, chargerY, radius);
drawCenteredImg(g2d, scale / 8, "charger.png", chargerX, chargerY);
radius = 3 * scale;
g2d.setColor(COLOR_ROBO);
final float roboX = toXCoord(rmfp.getRoboX()) * scale;
final float roboY = toYCoord(rmfp.getRoboY()) * scale;
drawCircle(g2d, roboX, roboY, radius);
if (scale > 1.5) {
drawCenteredImg(g2d, scale / 15, "robo.png", roboX, roboY);
}
}
private void drawCircle(Graphics2D g2d, float x, float y, float radius) {
g2d.draw(new Ellipse2D.Double(x - radius, y - radius, 2.0 * radius, 2.0 * radius));
}
private void drawCenteredImg(Graphics2D g2d, float scale, String imgFile, float x, float y) {
URL image = getImageUrl(imgFile);
try {
if (image != null) {
BufferedImage addImg = ImageIO.read(image);
int xpos = Math.round(x - (addImg.getWidth() / 2 * scale));
int ypos = Math.round(y - (addImg.getHeight() / 2 * scale));
AffineTransform at = new AffineTransform();
at.scale(scale, scale);
AffineTransformOp scaleOp = new AffineTransformOp(at, AffineTransformOp.TYPE_BILINEAR);
g2d.drawImage(addImg, scaleOp, xpos, ypos);
} else {
logger.debug("Error loading image {}: File not be found.", imgFile);
}
} catch (IOException e) {
logger.debug("Error loading image {}: {}", image, e.getMessage());
}
}
private void drawGoTo(Graphics2D g2d, float scale) {
float x = toXCoord(rmfp.getGotoX()) * scale;
float y = toYCoord(rmfp.getGotoY()) * scale;
if (!(x == 0 && y == 0)) {
g2d.setStroke(new BasicStroke());
g2d.setColor(Color.YELLOW);
int x3[] = { (int) x, (int) (x - 2 * scale), (int) (x + 2 * scale) };
int y3[] = { (int) y, (int) (y - 5 * scale), (int) (y - 5 * scale) };
g2d.fill(new Polygon(x3, y3, 3));
}
}
private void drawOpenHabRocks(Graphics2D g2d, int width, int height, float scale) {
// easter egg gift
int offset = 5;
int textPos = 55;
URL image = getImageUrl("ohlogo.png");
try {
if (image != null) {
BufferedImage ohLogo = ImageIO.read(image);
textPos = (int) (ohLogo.getWidth() * scale / 2 + offset * scale);
AffineTransform at = new AffineTransform();
at.scale(scale / 2, scale / 2);
AffineTransformOp scaleOp = new AffineTransformOp(at, AffineTransformOp.TYPE_BILINEAR);
g2d.drawImage(ohLogo, scaleOp, offset,
height - (int) (ohLogo.getHeight() * scale / 2) - (int) (offset * scale));
} else {
logger.debug("Error loading image ohlogo.png: File not be found.");
}
} catch (IOException e) {
logger.debug("Error loading image ohlogo.png:: {}", e.getMessage());
}
String fontName = getAvailableFont("Helvetica,Arial,Roboto,Verdana,Times,Serif,Dialog".split(","));
if (fontName == null) {
return; // no available fonts to draw text
}
Font font = new Font(fontName, Font.BOLD, 14);
g2d.setFont(font);
String message = "Openhab rocks your Xiaomi vacuum!";
FontMetrics fontMetrics = g2d.getFontMetrics();
int stringWidth = fontMetrics.stringWidth(message);
if ((stringWidth + textPos) > rmfp.getImgWidth() * scale) {
font = new Font(fontName, Font.BOLD,
(int) Math.floor(14 * (rmfp.getImgWidth() * scale - textPos - offset * scale) / stringWidth));
g2d.setFont(font);
}
int stringHeight = fontMetrics.getAscent();
g2d.setPaint(Color.white);
g2d.drawString(message, textPos, height - offset * scale - stringHeight / 2);
}
private @Nullable String getAvailableFont(String[] preferedFonts) {
final GraphicsEnvironment gEv = GraphicsEnvironment.getLocalGraphicsEnvironment();
if (gEv == null) {
return null;
}
String[] fonts = gEv.getAvailableFontFamilyNames();
if (fonts.length == 0) {
return null;
}
for (int j = 0; j < preferedFonts.length; j++) {
for (int i = 0; i < fonts.length; i++) {
if (fonts[i].equalsIgnoreCase(preferedFonts[j])) {
return preferedFonts[j];
}
}
}
// Preferred fonts not available... just go with the first one
return fonts[0];
}
private @Nullable URL getImageUrl(String image) {
if (bundle != null) {
return bundle.getEntry("images/" + image);
}
try {
File fn = new File("src" + File.separator + "main" + File.separator + "resources" + File.separator
+ "images" + File.separator + image);
return fn.toURI().toURL();
} catch (MalformedURLException | SecurityException e) {
logger.debug("Could create URL for {}: {}", image, e.getMessage());
return null;
}
}
public BufferedImage getImage(float scale) {
int width = (int) Math.floor(rmfp.getImgWidth() * scale);
int height = (int) Math.floor(rmfp.getImgHeight() * scale);
BufferedImage bi = new BufferedImage(width, height, BufferedImage.TYPE_3BYTE_BGR);
Graphics2D g2d = bi.createGraphics();
AffineTransform tx = AffineTransform.getScaleInstance(-1, -1);
tx.translate(-width, -height);
g2d.setTransform(tx);
drawMap(g2d, scale);
drawZones(g2d, scale);
drawNoGo(g2d, scale);
drawWalls(g2d, scale);
drawPath(g2d, scale);
drawRobo(g2d, scale);
drawGoTo(g2d, scale);
g2d = bi.createGraphics();
drawOpenHabRocks(g2d, width, height, scale);
return bi;
}
public boolean writePic(String filename, String formatName, float scale) throws IOException {
return ImageIO.write(getImage(scale), formatName, new File(filename));
}
private float toXCoord(float x) {
return rmfp.getImgWidth() + rmfp.getLeft() - (x / MM);
}
private float toYCoord(float y) {
return y / MM - rmfp.getTop();
}
@Override
public String toString() {
return rmfp.toString();
}
}

View File

@@ -0,0 +1,426 @@
/**
* Copyright (c) 2010-2020 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.robot;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.zip.GZIPInputStream;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.miio.internal.Utils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link RRMapFileParser} is used to parse the RR map file format created by Xiaomi / RockRobo vacuum
*
* @author Marcel Verpaalen - Initial contribution
*/
@NonNullByDefault
public class RRMapFileParser {
public static final int CHARGER = 1;
public static final int IMAGE = 2;
public static final int PATH = 3;
public static final int GOTO_PATH = 4;
public static final int GOTO_PREDICTED_PATH = 5;
public static final int CURRENTLY_CLEANED_ZONES = 6;
public static final int GOTO_TARGET = 7;
public static final int ROBOT_POSITION = 8;
public static final int NO_GO_AREAS = 9;
public static final int VIRTUAL_WALLS = 10;
public static final int BLOCKS = 11;
public static final int MFBZS_AREA = 12;
public static final int OBSTACLES = 13;
public static final int DIGEST = 1024;
public static final int HEADER = 0x7272;
public static final String PATH_POINT_LENGTH = "pointLength";
public static final String PATH_POINT_SIZE = "pointSize";
public static final String PATH_ANGLE = "angle";
private byte[] image = new byte[] { 0 };
private final int majorVersion;
private final int minorVersion;
private final int mapIndex;
private final int mapSequence;
private boolean isValid;
private int imgHeight;
private int imgWidth;
private int imageSize;
private int top;
private int left;
private int chargerX;
private int chargerY;
private int roboX;
private int roboY;
private int roboA;
private float gotoX = 0;
private float gotoY = 0;
private Map<Integer, ArrayList<float[]>> paths = new HashMap<>();
private Map<Integer, Map<String, Integer>> pathsDetails = new HashMap<>();
private Map<Integer, ArrayList<float[]>> areas = new HashMap<>();
private ArrayList<float[]> walls = new ArrayList<>();
private ArrayList<float[]> zones = new ArrayList<>();
private ArrayList<int[]> obstacles = new ArrayList<>();
private byte[] blocks = new byte[0];
private final Logger logger = LoggerFactory.getLogger(RRMapFileParser.class);
public RRMapFileParser(byte[] raw) {
boolean printBlockDetails = false;
int mapHeaderLength = getUInt16(raw, 0x02);
int mapDataLength = getUInt32LE(raw, 0x04);
this.majorVersion = getUInt16(raw, 0x08);
this.minorVersion = getUInt16(raw, 0x0A);
this.mapIndex = getUInt32LE(raw, 0x0C);
this.mapSequence = getUInt32LE(raw, 0x10);
int blockStartPos = getUInt16(raw, 0x02); // main header length
while (blockStartPos < raw.length) {
int blockHeaderLength = getUInt16(raw, blockStartPos + 0x02);
byte[] header = getBytes(raw, blockStartPos, blockHeaderLength);
int blocktype = getUInt16(header, 0x00);
int blockDataLength = getUInt32LE(header, 0x04);
int blockDataStart = blockStartPos + blockHeaderLength;
byte[] data = getBytes(raw, blockDataStart, blockDataLength);
switch (blocktype) {
case CHARGER:
this.chargerX = getUInt32LE(raw, blockStartPos + 0x08);
this.chargerY = getUInt32LE(raw, blockStartPos + 0x0C);
break;
case IMAGE:
this.imageSize = blockDataLength;// (getUInt32LE(raw, blockStartPos + 0x04));
if (blockHeaderLength > 0x1C) {
logger.debug("block 2 unknown value @pos 8: {}", getUInt32LE(header, 0x08));
}
this.top = getUInt32LE(header, blockHeaderLength - 16);
this.left = getUInt32LE(header, blockHeaderLength - 12);
this.imgHeight = (getUInt32LE(header, blockHeaderLength - 8));
this.imgWidth = getUInt32LE(header, blockHeaderLength - 4);
this.image = data;
break;
case ROBOT_POSITION:
this.roboX = getUInt32LE(data, 0x00);
this.roboY = getUInt32LE(data, 0x04);
if (blockDataLength > 8) { // model S6
this.roboA = getUInt32LE(data, 0x08);
}
break;
case PATH:
case GOTO_PATH:
case GOTO_PREDICTED_PATH:
ArrayList<float[]> path = new ArrayList<float[]>();
Map<String, Integer> detail = new HashMap<String, Integer>();
int pairs = getUInt32LE(header, 0x04) / 4;
detail.put(PATH_POINT_LENGTH, getUInt32LE(header, 0x08));
detail.put(PATH_POINT_SIZE, getUInt32LE(header, 0x0C));
detail.put(PATH_ANGLE, getUInt32LE(header, 0x10));
for (int pathpair = 0; pathpair < pairs; pathpair++) {
float x = (getUInt16(getBytes(raw, blockDataStart + pathpair * 4, 2)));
float y = getUInt16(getBytes(raw, blockDataStart + pathpair * 4 + 2, 2));
path.add(new float[] { x, y });
}
paths.put(blocktype, path);
pathsDetails.put(blocktype, detail);
break;
case CURRENTLY_CLEANED_ZONES:
int zonePairs = getUInt16(header, 0x08);
for (int zonePair = 0; zonePair < zonePairs; zonePair++) {
float x0 = (getUInt16(raw, blockDataStart + zonePair * 8));
float y0 = getUInt16(raw, blockDataStart + zonePair * 8 + 2);
float x1 = (getUInt16(raw, blockDataStart + zonePair * 8 + 4));
float y1 = getUInt16(raw, blockDataStart + zonePair * 8 + 6);
zones.add(new float[] { x0, y0, x1, y1 });
}
break;
case GOTO_TARGET:
this.gotoX = getUInt16(data, 0x00);
this.gotoY = getUInt16(data, 0x02);
break;
case DIGEST:
isValid = Arrays.equals(data, sha1Hash(getBytes(raw, 0, mapHeaderLength + mapDataLength - 20)));
break;
case VIRTUAL_WALLS:
int wallPairs = getUInt16(header, 0x08);
for (int wallPair = 0; wallPair < wallPairs; wallPair++) {
float x0 = (getUInt16(raw, blockDataStart + wallPair * 8));
float y0 = getUInt16(raw, blockDataStart + wallPair * 8 + 2);
float x1 = (getUInt16(raw, blockDataStart + wallPair * 8 + 4));
float y1 = getUInt16(raw, blockDataStart + wallPair * 8 + 6);
walls.add(new float[] { x0, y0, x1, y1 });
}
break;
case NO_GO_AREAS:
case MFBZS_AREA:
int areaPairs = getUInt16(header, 0x08);
ArrayList<float[]> area = new ArrayList<float[]>();
for (int areaPair = 0; areaPair < areaPairs; areaPair++) {
float x0 = (getUInt16(raw, blockDataStart + areaPair * 16));
float y0 = getUInt16(raw, blockDataStart + areaPair * 16 + 2);
float x1 = (getUInt16(raw, blockDataStart + areaPair * 16 + 4));
float y1 = getUInt16(raw, blockDataStart + areaPair * 16 + 6);
float x2 = (getUInt16(raw, blockDataStart + areaPair * 16 + 8));
float y2 = getUInt16(raw, blockDataStart + areaPair * 16 + 10);
float x3 = (getUInt16(raw, blockDataStart + areaPair * 16 + 12));
float y3 = getUInt16(raw, blockDataStart + areaPair * 16 + 14);
area.add(new float[] { x0, y0, x1, y1, x2, y2, x3, y3 });
}
areas.put(Integer.valueOf(blocktype & 0xFF), area);
break;
case OBSTACLES:
int obstaclePairs = getUInt16(header, 0x08);
for (int obstaclePair = 0; obstaclePair < obstaclePairs; obstaclePair++) {
int x0 = getUInt16(data, obstaclePair * 5 + 0);
int y0 = getUInt16(data, obstaclePair * 5 + 2);
int u = data[obstaclePair * 5 + 0] & 0xFF;
obstacles.add(new int[] { x0, y0, u });
}
break;
case BLOCKS:
int blocksPairs = getUInt16(header, 0x08);
blocks = getBytes(data, 0, blocksPairs);
break;
default:
logger.info("Unknown blocktype (pls report to author)");
printBlockDetails = true;
}
if (logger.isTraceEnabled() || printBlockDetails) {
logger.debug("Blocktype: {}", Integer.toString(blocktype));
logger.debug("Header len: {} data len: {} ", Integer.toString(blockHeaderLength),
Integer.toString(blockDataLength));
logger.debug("H: {}", Utils.getSpacedHex(header));
if (blockDataLength > 0) {
logger.debug("D: {}", (blockDataLength < 60 ? Utils.getSpacedHex(data)
: Utils.getSpacedHex(getBytes(data, 0, 60))));
}
printBlockDetails = false;
}
blockStartPos = blockStartPos + blockDataLength + (header[2] & 0xFF);
}
}
public static byte[] readRRMapFile(File file) throws IOException {
return readRRMapFile(new FileInputStream(file));
}
public static byte[] readRRMapFile(InputStream is) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try (GZIPInputStream in = new GZIPInputStream(is)) {
int bufsize = 1024;
byte[] buf = new byte[bufsize];
int readbytes = 0;
readbytes = in.read(buf);
while (readbytes != -1) {
baos.write(buf, 0, readbytes);
readbytes = in.read(buf);
}
baos.flush();
return baos.toByteArray();
}
}
private byte[] getBytes(byte[] raw, int pos, int len) {
return java.util.Arrays.copyOfRange(raw, pos, pos + len);
}
private int getUInt32LE(byte[] bytes, int pos) {
int value = bytes[0 + pos] & 0xFF;
value |= (bytes[1 + pos] << 8) & 0xFFFF;
value |= (bytes[2 + pos] << 16) & 0xFFFFFF;
value |= (bytes[3 + pos] << 24) & 0xFFFFFFFF;
return value;
}
private int getUInt16(byte[] bytes) {
return getUInt16(bytes, 0);
}
private int getUInt16(byte[] bytes, int pos) {
int value = bytes[0 + pos] & 0xFF;
value |= (bytes[1 + pos] << 8) & 0xFFFF;
return value;
}
@Override
public String toString() {
StringWriter sw = new StringWriter();
PrintWriter pw = new PrintWriter(sw);
pw.printf("RR Map:\tMajor Version: %d Minor version: %d Map Index: %d Map Sequence: %d\r\n", majorVersion,
minorVersion, mapIndex, mapSequence);
pw.printf("Image:\tsize: %9d\ttop: %9d\tleft: %9d height: %9d width: %9d\r\n", imageSize, top, left, imgHeight,
imgWidth);
pw.printf("Charger pos:\tX: %.0f\tY: %.0f\r\n", getChargerX(), getChargerY());
pw.printf("Robo pos:\tX: %.0f\tY: %.0f\tAngle: %d\r\n", getRoboX(), getRoboY(), getRoboA());
pw.printf("Goto:\tX: %.0f\tY: %.0f\r\n", getGotoX(), getGotoY());
for (Integer area : areas.keySet()) {
pw.print(area == NO_GO_AREAS ? "No Go zones:\t" : "MFBZS zones:\t");
pw.printf("%d\r\n", areas.get(area).size());
printAreaDetails(areas.get(area), pw);
}
pw.printf("Walls:\t%d\r\n", walls.size());
printAreaDetails(walls, pw);
pw.printf("Zones:\t%d\r\n", zones.size());
printAreaDetails(zones, pw);
pw.printf("Obstacles:\t%d\r\n", obstacles.size());
pw.printf("Blocks:\t%d\r\n", blocks.length);
pw.print("Paths:");
for (Integer p : pathsDetails.keySet()) {
pw.printf("\r\nPath type:\t%d", p);
for (String detail : pathsDetails.get(p).keySet()) {
pw.printf(" %s: %d", detail, pathsDetails.get(p).get(detail));
}
}
pw.println();
pw.close();
return sw.toString();
}
private void printAreaDetails(ArrayList<float[]> areas, PrintWriter pw) {
areas.forEach(area -> {
pw.printf("\tArea coordinates:");
for (int i = 0; i < area.length; i++) {
pw.printf("\t%.0f", area[i]);
}
pw.println();
});
}
/**
* Compute SHA-1 hash value for the byte array
*
* @param inBytes ByteArray to be hashed
* @return hash value
*/
public static byte[] sha1Hash(byte[] inBytes) {
try {
MessageDigest md = MessageDigest.getInstance("SHA-1");
return md.digest(inBytes);
} catch (NoSuchAlgorithmException e) {
return new byte[] { 0x00 };
}
}
public int getMajorVersion() {
return majorVersion;
}
public int getMinorVersion() {
return minorVersion;
}
public int getMapIndex() {
return mapIndex;
}
public int getMapSequence() {
return mapSequence;
}
public boolean isValid() {
return isValid;
}
public byte[] getImage() {
return image;
}
public int getImageSize() {
return imageSize;
}
public int getImgHeight() {
return imgHeight;
}
public int getImgWidth() {
return imgWidth;
}
public int getTop() {
return top;
}
public int getLeft() {
return left;
}
public ArrayList<float[]> getZones() {
return zones;
}
public float getRoboX() {
return roboX;
}
public float getRoboY() {
return roboY;
}
public float getChargerX() {
return chargerX;
}
public float getChargerY() {
return chargerY;
}
public float getGotoX() {
return gotoX;
}
public float getGotoY() {
return gotoY;
}
public int getRoboA() {
return roboA;
}
public Map<Integer, ArrayList<float[]>> getPaths() {
return paths;
}
public Map<Integer, Map<String, Integer>> getPathsDetails() {
return pathsDetails;
}
public ArrayList<float[]> getWalls() {
return walls;
}
public Map<Integer, ArrayList<float[]>> getAreas() {
return areas;
}
public ArrayList<int[]> getObstacles() {
return obstacles;
}
public byte[] getBlocks() {
return blocks;
}
}

View File

@@ -0,0 +1,61 @@
/**
* Copyright (c) 2010-2020 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.robot;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.thing.type.ChannelTypeUID;
/**
* List of additional capabilities
*
* @author Marcel Verpaalen - Initial contribution
*/
@NonNullByDefault
public enum RobotCababilities {
WATERBOX_STATUS("water_box_status", "status#water_box_status", "miio:water_box_status"),
LOCKSTATUS("lock_status", "status#lock_status", "miio:lock_status"),
WATERBOX_MODE("water_box_mode", "status#water_box_mode", "miio:water_box_mode"),
WATERBOX_CARRIAGE("water_box_carriage_status", "status#water_box_carriage_status",
"miio:water_box_carriage_status"),
MOP_FORBIDDEN("mop_forbidden_enable", "status#mop_forbidden_enable", "miio:mop_forbidden_enable"),
SEGMENT_CLEAN("", "actions#segment", "miio:segment");
private final String statusFieldName;
private final String channel;
private final String channelType;
RobotCababilities(String statusKey, String channel, String channelType) {
this.statusFieldName = statusKey;
this.channel = channel;
this.channelType = channelType;
}
public String getStatusFieldName() {
return statusFieldName;
}
public String getChannel() {
return channel;
}
public ChannelTypeUID getChannelType() {
return new ChannelTypeUID(channelType);
}
@Override
public String toString() {
return String.format("Capability %s: status field name: '%s', channel: '%s', channeltype: '%s'.", this.name(),
statusFieldName, channel, channelType);
}
}

View File

@@ -0,0 +1,172 @@
/**
* Copyright (c) 2010-2020 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.robot;
import com.google.gson.annotations.Expose;
import com.google.gson.annotations.SerializedName;
/**
* This DTO class wraps the status message json structure
*
* @author Marcel Verpaalen - Initial contribution
*/
public class StatusDTO {
@SerializedName("msg_ver")
@Expose
private Integer msgVer;
@SerializedName("msg_seq")
@Expose
private Integer msgSeq;
@SerializedName("state")
@Expose
private Integer state;
@SerializedName("battery")
@Expose
private Integer battery;
@SerializedName("clean_time")
@Expose
private Long cleanTime;
@SerializedName("clean_area")
@Expose
private Integer cleanArea;
@SerializedName("error_code")
@Expose
private Integer errorCode;
@SerializedName("map_present")
@Expose
private Integer mapPresent;
@SerializedName("in_cleaning")
@Expose
private Integer inCleaning;
@SerializedName("fan_power")
@Expose
private Integer fanPower;
@SerializedName("dnd_enabled")
@Expose
private Integer dndEnabled;
@SerializedName("in_returning")
@Expose
private Integer inReturning;
@SerializedName("in_fresh_state")
@Expose
private Integer inFreshState;
@SerializedName("lab_status")
@Expose
private Integer labStatus;
@SerializedName("water_box_status")
@Expose
private Integer waterBoxStatus;
@SerializedName("map_status")
@Expose
private Integer mapStatus;
@SerializedName("is_locating")
@Expose
private Integer isLocating;
@SerializedName("lock_status")
@Expose
private Integer lockStatus;
@SerializedName("water_box_mode")
@Expose
private Integer waterBoxMode;
@SerializedName("water_box_carriage_status")
@Expose
private Integer waterBoxCarriageStatus;
@SerializedName("mop_forbidden_enable")
@Expose
private Integer mopForbiddenEnable;
public final Integer getMsgVer() {
return msgVer;
}
public final Integer getMsgSeq() {
return msgSeq;
}
public final Integer getState() {
return state;
}
public final Integer getBattery() {
return battery;
}
public final Long getCleanTime() {
return cleanTime;
}
public final Integer getCleanArea() {
return cleanArea;
}
public final Integer getErrorCode() {
return errorCode;
}
public final Integer getMapPresent() {
return mapPresent;
}
public final Integer getInCleaning() {
return inCleaning;
}
public final Integer getFanPower() {
return fanPower;
}
public final Integer getDndEnabled() {
return dndEnabled;
}
public final Integer getInReturning() {
return inReturning;
}
public final Integer getInFreshState() {
return inFreshState;
}
public final Integer getLabStatus() {
return labStatus;
}
public final Integer getWaterBoxStatus() {
return waterBoxStatus;
}
public final Integer getMapStatus() {
return mapStatus;
}
public final Integer getIsLocating() {
return isLocating;
}
public final Integer getLockStatus() {
return lockStatus;
}
public final Integer getWaterBoxMode() {
return waterBoxMode;
}
public final Integer getWaterBoxCarriageStatus() {
return waterBoxCarriageStatus;
}
public final Integer getMopForbiddenEnable() {
return mopForbiddenEnable;
}
}

View File

@@ -0,0 +1,75 @@
/**
* Copyright (c) 2010-2020 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.robot;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* List of available states
*
* @author Marcel Verpaalen - Initial contribution
*/
@NonNullByDefault
public enum StatusType {
UNKNOWN(0, "Unknown"),
INITIATING(1, "Initiating"),
SLEEPING(2, "Sleeping"),
IDLE(3, "Idle"),
REMOTE(4, "Remote Control"),
CLEANING(5, "Cleaning"),
RETURNING(6, "Returning Dock"),
MANUAL(7, "Manual Mode"),
CHARGING(8, "Charging"),
CHARGING_ERROR(9, "Charging Error"),
PAUSED(10, "Paused"),
SPOTCLEAN(11, "Spot cleaning"),
ERROR(12, "In Error"),
SHUTTING_DOWN(13, "Shutting Down"),
UPDATING(14, "Updating"),
DOCKING(15, "Docking"),
GOTO(16, "Go To"),
ZONE(17, "Zone Clean"),
ROOM(18, "Room Clean"),
FULL(100, "Full");
private final int id;
private final String description;
StatusType(int id, String description) {
this.id = id;
this.description = description;
}
public int getId() {
return id;
}
public static StatusType getType(int value) {
for (StatusType st : StatusType.values()) {
if (st.getId() == value) {
return st;
}
}
return UNKNOWN;
}
public String getDescription() {
return description;
}
@Override
public String toString() {
return "Status " + Integer.toString(id) + ": " + description;
}
}

View File

@@ -0,0 +1,84 @@
/**
* Copyright (c) 2010-2020 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.robot;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* List of Errors
* derived from vacuum_cleaner-EN.pdf
*
* @author Marcel Verpaalen - Initial contribution
*/
@NonNullByDefault
public enum VacuumErrorType {
ERROR00(0, "No error"),
ERROR01(1, "Laser sensor fault"),
ERROR02(2, "Collision sensor fault"),
ERROR03(3, "Wheel floating"),
ERROR04(4, "Cliff sensor fault"),
ERROR05(5, "Main brush blocked"),
ERROR06(6, "Side brush blocked"),
ERROR07(7, "Wheel blocked"),
ERROR08(8, "Device stuck"),
ERROR09(9, "Dust bin missing"),
ERROR10(10, "Filter blocked"),
ERROR11(11, "Magnetic field detected"),
ERROR12(12, "Low battery"),
ERROR13(13, "Charging problem"),
ERROR14(14, "Battery failure"),
ERROR15(15, "Wall sensor fault"),
ERROR16(16, "Uneven surface"),
ERROR17(17, "Side brush failure"),
ERROR18(18, "Suction fan failure"),
ERROR19(19, "Unpowered charging station"),
ERROR20(20, "Unknown Error"),
ERROR21(21, "Laser pressure sensor problem"),
ERROR22(22, "Charge sensor problem"),
ERROR23(23, "Dock problem"),
ERROR24(24, "No-go zone or invisible wall detected"),
ERROR254(254, "Bin full"),
ERROR255(255, "Internal error"),
UNKNOWN(-1, "Unknown Error");
private final int id;
private final String description;
VacuumErrorType(int id, String description) {
this.id = id;
this.description = description;
}
public int getId() {
return id;
}
public static VacuumErrorType getType(int value) {
for (VacuumErrorType st : VacuumErrorType.values()) {
if (st.getId() == value) {
return st;
}
}
return UNKNOWN;
}
public String getDescription() {
return description;
}
@Override
public String toString() {
return "Error " + Integer.toString(id) + ": " + description;
}
}

View File

@@ -0,0 +1,446 @@
/**
* Copyright (c) 2010-2020 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.transport;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.SocketException;
import java.net.SocketTimeoutException;
import java.util.Arrays;
import java.util.Calendar;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.miio.internal.Message;
import org.openhab.binding.miio.internal.MiIoBindingConstants;
import org.openhab.binding.miio.internal.MiIoCommand;
import org.openhab.binding.miio.internal.MiIoCrypto;
import org.openhab.binding.miio.internal.MiIoCryptoException;
import org.openhab.binding.miio.internal.MiIoMessageListener;
import org.openhab.binding.miio.internal.MiIoSendCommand;
import org.openhab.binding.miio.internal.Utils;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.google.gson.JsonSyntaxException;
/**
* The {@link MiIoAsyncCommunication} is responsible for communications with the Mi IO devices
*
* @author Marcel Verpaalen - Initial contribution
*/
@NonNullByDefault
public class MiIoAsyncCommunication {
private static final int MSG_BUFFER_SIZE = 2048;
private final Logger logger = LoggerFactory.getLogger(MiIoAsyncCommunication.class);
private final String ip;
private final byte[] token;
private byte[] deviceId;
private @Nullable DatagramSocket socket;
private List<MiIoMessageListener> listeners = new CopyOnWriteArrayList<>();
private AtomicInteger id = new AtomicInteger(-1);
private int timeDelta;
private int timeStamp;
private final JsonParser parser;
private @Nullable MessageSenderThread senderThread;
private boolean connected;
private ThingStatusDetail status = ThingStatusDetail.NONE;
private int errorCounter;
private int timeout;
private boolean needPing = true;
private static final int MAX_ERRORS = 3;
private static final int MAX_ID = 15000;
private ConcurrentLinkedQueue<MiIoSendCommand> concurrentLinkedQueue = new ConcurrentLinkedQueue<>();
public MiIoAsyncCommunication(String ip, byte[] token, byte[] did, int id, int timeout) {
this.ip = ip;
this.token = token;
this.deviceId = did;
this.timeout = timeout;
setId(id);
parser = new JsonParser();
startReceiver();
}
protected List<MiIoMessageListener> getListeners() {
return listeners;
}
/**
* Registers a {@link MiIoMessageListener} to be called back, when data is received.
* If no {@link MessageSenderThread} exists, when the method is called, it is being set up.
*
* @param listener {@link MiIoMessageListener} to be called back
*/
public synchronized void registerListener(MiIoMessageListener listener) {
needPing = true;
startReceiver();
if (!getListeners().contains(listener)) {
logger.trace("Adding socket listener {}", listener);
getListeners().add(listener);
}
}
/**
* Unregisters a {@link MiIoMessageListener}. If there are no listeners left,
* the {@link MessageSenderThread} is being closed.
*
* @param listener {@link MiIoMessageListener} to be unregistered
*/
public synchronized void unregisterListener(MiIoMessageListener listener) {
getListeners().remove(listener);
if (getListeners().isEmpty()) {
concurrentLinkedQueue.clear();
close();
}
}
public int queueCommand(MiIoCommand command) throws MiIoCryptoException, IOException {
return queueCommand(command, "[]");
}
public int queueCommand(MiIoCommand command, String params) throws MiIoCryptoException, IOException {
return queueCommand(command.getCommand(), params);
}
public int queueCommand(String command, String params)
throws MiIoCryptoException, IOException, JsonSyntaxException {
try {
JsonObject fullCommand = new JsonObject();
int cmdId = id.incrementAndGet();
if (cmdId > MAX_ID) {
id.set(0);
}
fullCommand.addProperty("id", cmdId);
fullCommand.addProperty("method", command);
fullCommand.add("params", parser.parse(params));
MiIoSendCommand sendCmd = new MiIoSendCommand(cmdId, MiIoCommand.getCommand(command),
fullCommand.toString());
concurrentLinkedQueue.add(sendCmd);
if (logger.isDebugEnabled()) {
// Obfuscate part of the token to allow sharing of the logfiles
String tokenText = Utils.obfuscateToken(Utils.getHex(token));
logger.debug("Command added to Queue {} -> {} (Device: {} token: {} Queue: {})", fullCommand.toString(),
ip, Utils.getHex(deviceId), tokenText, concurrentLinkedQueue.size());
}
if (needPing) {
sendPing(ip);
}
return cmdId;
} catch (JsonSyntaxException e) {
logger.warn("Send command '{}' with parameters {} -> {} (Device: {}) gave error {}", command, params, ip,
Utils.getHex(deviceId), e.getMessage());
throw e;
}
}
MiIoSendCommand sendMiIoSendCommand(MiIoSendCommand miIoSendCommand) {
String errorMsg = "Unknown Error while sending command";
String decryptedResponse = "";
try {
decryptedResponse = sendCommand(miIoSendCommand.getCommandString(), token, ip, deviceId);
// hack due to avoid invalid json errors from some misbehaving device firmwares
decryptedResponse = decryptedResponse.replace(",,", ",");
JsonElement response;
response = parser.parse(decryptedResponse);
if (response.isJsonObject()) {
needPing = false;
logger.trace("Received JSON message {}", response.toString());
miIoSendCommand.setResponse(response.getAsJsonObject());
return miIoSendCommand;
} else {
errorMsg = "Received message is invalid JSON";
logger.debug("{}: {}", errorMsg, decryptedResponse);
}
} catch (MiIoCryptoException | IOException e) {
logger.debug("Send command '{}' -> {} (Device: {}) gave error {}", miIoSendCommand.getCommandString(), ip,
Utils.getHex(deviceId), e.getMessage());
errorMsg = e.getMessage();
} catch (JsonSyntaxException e) {
logger.warn("Could not parse '{}' <- {} (Device: {}) gave error {}", decryptedResponse,
miIoSendCommand.getCommandString(), Utils.getHex(deviceId), e.getMessage());
errorMsg = "Received message is invalid JSON";
}
JsonObject erroResp = new JsonObject();
erroResp.addProperty("error", errorMsg);
miIoSendCommand.setResponse(erroResp);
return miIoSendCommand;
}
public synchronized void startReceiver() {
MessageSenderThread senderThread = this.senderThread;
if (senderThread == null || !senderThread.isAlive()) {
senderThread = new MessageSenderThread();
senderThread.start();
this.senderThread = senderThread;
}
}
/**
* The {@link MessageSenderThread} is responsible for consuming messages from the queue and sending these to the
* device
*
*/
private class MessageSenderThread extends Thread {
public MessageSenderThread() {
super("Mi IO MessageSenderThread");
setDaemon(true);
}
@Override
public void run() {
logger.debug("Starting Mi IO MessageSenderThread");
while (!interrupted()) {
try {
if (concurrentLinkedQueue.isEmpty()) {
Thread.sleep(100);
continue;
}
MiIoSendCommand queuedMessage = concurrentLinkedQueue.remove();
MiIoSendCommand miIoSendCommand = sendMiIoSendCommand(queuedMessage);
for (MiIoMessageListener listener : listeners) {
logger.trace("inform listener {}, data {} from {}", listener, queuedMessage, miIoSendCommand);
try {
listener.onMessageReceived(miIoSendCommand);
} catch (Exception e) {
logger.debug("Could not inform listener {}: {}: ", listener, e.getMessage(), e);
}
}
} catch (NoSuchElementException e) {
// ignore
} catch (InterruptedException e) {
// That's our signal to stop
break;
} catch (Exception e) {
logger.warn("Error while polling/sending message", e);
}
}
closeSocket();
logger.debug("Finished Mi IO MessageSenderThread");
}
}
private String sendCommand(String command, byte[] token, String ip, byte[] deviceId)
throws MiIoCryptoException, IOException {
byte[] encr;
encr = MiIoCrypto.encrypt(command.getBytes(), token);
timeStamp = (int) TimeUnit.MILLISECONDS.toSeconds(Calendar.getInstance().getTime().getTime());
byte[] sendMsg = Message.createMsgData(encr, token, deviceId, timeStamp + timeDelta);
Message miIoResponseMsg = sendData(sendMsg, ip);
if (miIoResponseMsg == null) {
if (logger.isTraceEnabled()) {
logger.trace("No response from device {} at {} for command {}.\r\n{}", Utils.getHex(deviceId), ip,
command, (new Message(sendMsg)).toSting());
} else {
logger.debug("No response from device {} at {} for command {}.", Utils.getHex(deviceId), ip, command);
}
errorCounter++;
if (errorCounter > MAX_ERRORS) {
status = ThingStatusDetail.CONFIGURATION_ERROR;
sendPing(ip);
}
return "{\"error\":\"No Response\"}";
}
if (!miIoResponseMsg.isChecksumValid()) {
return "{\"error\":\"Message has invalid checksum\"}";
}
if (errorCounter > 0) {
errorCounter = 0;
status = ThingStatusDetail.NONE;
updateStatus(ThingStatus.ONLINE, status);
}
if (!connected) {
pingSuccess();
}
String decryptedResponse = new String(MiIoCrypto.decrypt(miIoResponseMsg.getData(), token), "UTF-8").trim();
logger.trace("Received response from {}: {}", ip, decryptedResponse);
return decryptedResponse;
}
public @Nullable Message sendPing(String ip) throws IOException {
for (int i = 0; i < 3; i++) {
logger.debug("Sending Ping {} ({})", Utils.getHex(deviceId), ip);
Message resp = sendData(MiIoBindingConstants.DISCOVER_STRING, ip);
if (resp != null) {
pingSuccess();
return resp;
}
}
pingFail();
return null;
}
private void pingFail() {
logger.debug("Ping {} ({}) failed", Utils.getHex(deviceId), ip);
connected = false;
status = ThingStatusDetail.COMMUNICATION_ERROR;
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
}
private void pingSuccess() {
logger.debug("Ping {} ({}) success", Utils.getHex(deviceId), ip);
if (!connected) {
connected = true;
status = ThingStatusDetail.NONE;
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE);
} else {
if (ThingStatusDetail.CONFIGURATION_ERROR.equals(status)) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR);
} else {
status = ThingStatusDetail.NONE;
updateStatus(ThingStatus.ONLINE, status);
}
}
}
private void updateStatus(ThingStatus status, ThingStatusDetail statusDetail) {
for (MiIoMessageListener listener : listeners) {
logger.trace("inform listener {}, data {} from {}", listener, status, statusDetail);
try {
listener.onStatusUpdated(status, statusDetail);
} catch (Exception e) {
logger.debug("Could not inform listener {}: {}", listener, e.getMessage(), e);
}
}
}
private @Nullable Message sendData(byte[] sendMsg, String ip) throws IOException {
byte[] response = comms(sendMsg, ip);
if (response.length >= 32) {
Message miIoResponse = new Message(response);
timeStamp = (int) TimeUnit.MILLISECONDS.toSeconds(Calendar.getInstance().getTime().getTime());
timeDelta = miIoResponse.getTimestampAsInt() - timeStamp;
logger.trace("Message Details:{} ", miIoResponse.toSting());
return miIoResponse;
} else {
logger.trace("Reponse length <32 : {}", response.length);
return null;
}
}
private synchronized byte[] comms(byte[] message, String ip) throws IOException {
InetAddress ipAddress = InetAddress.getByName(ip);
DatagramSocket clientSocket = getSocket();
DatagramPacket receivePacket = new DatagramPacket(new byte[MSG_BUFFER_SIZE], MSG_BUFFER_SIZE);
try {
logger.trace("Connection {}:{}", ip, clientSocket.getLocalPort());
byte[] sendData = new byte[MSG_BUFFER_SIZE];
sendData = message;
DatagramPacket sendPacket = new DatagramPacket(sendData, sendData.length, ipAddress,
MiIoBindingConstants.PORT);
clientSocket.send(sendPacket);
sendPacket.setData(new byte[MSG_BUFFER_SIZE]);
clientSocket.receive(receivePacket);
byte[] response = Arrays.copyOfRange(receivePacket.getData(), receivePacket.getOffset(),
receivePacket.getOffset() + receivePacket.getLength());
return response;
} catch (SocketTimeoutException e) {
logger.debug("Communication error for Mi device at {}: {}", ip, e.getMessage());
needPing = true;
return new byte[0];
}
}
private DatagramSocket getSocket() throws SocketException {
@Nullable
DatagramSocket socket = this.socket;
if (socket == null || socket.isClosed()) {
socket = new DatagramSocket();
socket.setSoTimeout(timeout);
logger.debug("Opening socket on port: {} ", socket.getLocalPort());
this.socket = socket;
return socket;
} else {
return socket;
}
}
public void close() {
try {
final MessageSenderThread senderThread = this.senderThread;
if (senderThread != null) {
senderThread.interrupt();
}
} catch (SecurityException e) {
logger.debug("Error while closing: {} ", e.getMessage());
}
closeSocket();
}
public void closeSocket() {
try {
final DatagramSocket socket = this.socket;
if (socket != null) {
logger.debug("Closing socket for port: {} ", socket.getLocalPort());
socket.close();
this.socket = null;
}
} catch (SecurityException e) {
logger.debug("Error while closing: {} ", e.getMessage());
}
}
/**
* @return the id
*/
public int getId() {
return id.incrementAndGet();
}
/**
* @param id the id to set
*/
public void setId(int id) {
this.id.set(id);
}
/**
* Time delta between device time and server time
*
* @return delta
*/
public int getTimeDelta() {
return timeDelta;
}
public byte[] getDeviceId() {
return deviceId;
}
public void setDeviceId(byte[] deviceId) {
this.deviceId = deviceId;
}
public int getQueueLength() {
return concurrentLinkedQueue.size();
}
}

View File

@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<binding:binding id="miio" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:binding="https://openhab.org/schemas/binding/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/binding/v1.0.0 https://openhab.org/schemas/binding-1.0.0.xsd">
<name>Xiaomi Mi IO Binding</name>
<description>Binding for Xiaomi Mi IO devices like Mi Robot Vacuum</description>
<author>Marcel Verpaalen</author>
<config-description>
<parameter name="username" type="text">
<label>Xiaomi cloud username</label>
<description>Xiaomi cloud username. Typically your email</description>
<required>false</required>
</parameter>
<parameter name="password" type="text">
<label>Xiaomi cloud password</label>
<required>false</required>
</parameter>
<parameter name="country" type="text">
<label>Xiaomi server country</label>
<description>Xiaomi server country(s) (e.g. sg,de). Separate multiple servers with comma</description>
<required>false</required>
</parameter>
</config-description>
</binding:binding>

View File

@@ -0,0 +1,43 @@
<?xml version="1.0" encoding="UTF-8"?>
<config-description:config-descriptions
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:config-description="https://openhab.org/schemas/config-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/config-description/v1.0.0https://openhab.org/schemas/config-description-1.0.0.xsd">
<config-description uri="thing-type:miio:config">
<parameter name="host" type="text" required="true">
<context>network-address</context>
<label>IP Address</label>
</parameter>
<parameter name="token" type="text" pattern="^([A-Fa-f0-9]{96}|[A-Fa-f0-9]{32}|.{16})$" required="true">
<label>Token</label>
<description>Token for communication (in Hex)</description>
</parameter>
<parameter name="deviceId" type="text" required="true">
<label>Device ID</label>
<description>Device ID number for communication (in Hex)</description>
<advanced>true</advanced>
</parameter>
<parameter name="model" type="text" required="false">
<label>Device Model String</label>
<description>Device model string, used to determine the subtype.</description>
<advanced>true</advanced>
</parameter>
<parameter name="refreshInterval" type="integer" min="0" max="9999" required="false">
<label>Refresh Interval</label>
<description>Refresh interval for refreshing the data in seconds. (0=disabled)</description>
<default>30</default>
<advanced>true</advanced>
</parameter>
<parameter name="timeout" type="integer" min="1000" max="60000" required="false">
<label>Timeout</label>
<description>Timeout time in milliseconds</description>
<default>15000</default>
<advanced>true</advanced>
</parameter>
<parameter name="cloudServer" type="text" required="false">
<label>Xiaomi cloud Server (county code)</label>
<advanced>true</advanced>
</parameter>
</config-description>
</config-description:config-descriptions>

View File

@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="miio"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<thing-type id="basic">
<label>Xiaomi Mi Basic Device</label>
<channel-groups>
<channel-group id="network" typeId="network"/>
<channel-group id="actions" typeId="basicactions"/>
</channel-groups>
<properties>
<property name="vendor">Xiaomi</property>
</properties>
<config-description-ref uri="thing-type:miio:config"/>
</thing-type>
<channel-group-type id="basicactions">
<label>Actions</label>
<channels>
<channel id="commands" typeId="commands"/>
</channels>
</channel-group-type>
</thing:thing-descriptions>

View File

@@ -0,0 +1,486 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="miio"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<channel-group-type id="network">
<label>Network</label>
<channels>
<channel id="ssid" typeId="ssid"/>
<channel id="bssid" typeId="bssid"/>
<channel id="rssi" typeId="rssi"/>
<channel id="life" typeId="life"/>
</channels>
</channel-group-type>
<!-- Network channels -->
<channel-type id="ssid" advanced="true">
<item-type>String</item-type>
<label>SSID</label>
<state readOnly="true"/>
</channel-type>
<channel-type id="bssid" advanced="true">
<item-type>String</item-type>
<label>BSSID</label>
<state readOnly="true"/>
</channel-type>
<channel-type id="rssi" advanced="true">
<item-type>Number</item-type>
<label>RSSI</label>
<state readOnly="true"/>
</channel-type>
<channel-type id="life" advanced="true">
<item-type>Number</item-type>
<label>Life</label>
<state readOnly="true"/>
</channel-type>
<!-- Common Actions channels -->
<channel-type id="commands" advanced="true">
<item-type>String</item-type>
<label>Execute Command</label>
</channel-type>
<channel-type id="power">
<item-type>Switch</item-type>
<label>Power On/Off</label>
</channel-type>
<channel-type id="ambientPower">
<item-type>Switch</item-type>
<label>Ambient Power On/Off</label>
</channel-type>
<channel-type id="eyecare">
<item-type>Switch</item-type>
<label>Eyecare Mode On/Off</label>
</channel-type>
<channel-type id="mode">
<item-type>String</item-type>
<label>Mode</label>
</channel-type>
<channel-type id="modeGreen">
<item-type>String</item-type>
<label>Mode</label>
<state>
<options>
<option value="green">Green</option>
<option value="normal">Normal</option>
</options>
</state>
</channel-type>
<channel-type id="brightness">
<item-type>Number</item-type>
<label>Brightness</label>
</channel-type>
<channel-type id="ambientBrightness">
<item-type>Number</item-type>
<label>Ambient Brightness</label>
</channel-type>
<channel-type id="nightlightBrightness">
<item-type>Number</item-type>
<label>Nightlight Brightness</label>
</channel-type>
<channel-type id="illumination">
<item-type>Number</item-type>
<label>Environment Illumination</label>
</channel-type>
<channel-type id="led">
<item-type>Switch</item-type>
<label>LED</label>
</channel-type>
<channel-type id="colorMode">
<item-type>Number</item-type>
<label>Color Mode</label>
</channel-type>
<channel-type id="ambientColorMode">
<item-type>Number</item-type>
<label>Ambient Color Mode</label>
</channel-type>
<channel-type id="colorTemperature">
<item-type>Number</item-type>
<label>Color Temperature</label>
</channel-type>
<channel-type id="ambientColorTemperature">
<item-type>Number</item-type>
<label>Ambient Color Temperature</label>
</channel-type>
<channel-type id="cct">
<item-type>Number</item-type>
<label>Correlated Color Temperature</label>
</channel-type>
<channel-type id="scene">
<item-type>Number</item-type>
<label>Scene</label>
</channel-type>
<channel-type id="customScene">
<item-type>String</item-type>
<label>Custom Scene</label>
</channel-type>
<channel-type id="dv">
<item-type>Number</item-type>
<label>DV</label>
</channel-type>
<channel-type id="delayoff">
<item-type>Number</item-type>
<label>Delay Off</label>
</channel-type>
<channel-type id="act_det">
<item-type>Switch</item-type>
<label>AirAutoDetect</label>
</channel-type>
<channel-type id="buzzer">
<item-type>Switch</item-type>
<label>Buzzer</label>
</channel-type>
<channel-type id="childlock">
<item-type>Switch</item-type>
<label>Child Lock</label>
</channel-type>
<channel-type id="color">
<item-type>Color</item-type>
<label>Color</label>
</channel-type>
<channel-type id="ambientColor">
<item-type>Color</item-type>
<label>Ambient Color</label>
</channel-type>
<channel-type id="setHumidity">
<item-type>Number</item-type>
<label>Humidity Set</label>
</channel-type>
<!-- Common Properties channels -->
<channel-type id="temperature">
<item-type>Number</item-type>
<label>Temperature</label>
<state pattern="%.1f" readOnly="true"/>
</channel-type>
<channel-type id="humidity">
<item-type>Number</item-type>
<label>Humidity</label>
<state pattern="%.1f" readOnly="true"/>
</channel-type>
<channel-type id="pm25">
<item-type>Number</item-type>
<label>PM2.5</label>
<description>Particulate Matter 2.5</description>
<state pattern="%.1f" readOnly="true"/>
</channel-type>
<channel-type id="co2">
<item-type>Number</item-type>
<label>CO2</label>
<description>Carbon Dioxide</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="tvoc">
<item-type>Number</item-type>
<label>tVOC</label>
<description>Total Volatile Organic Compounds</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="aqi">
<item-type>Number</item-type>
<label>Air Quality Index</label>
<state readOnly="true"/>
</channel-type>
<channel-type id="averageaqi">
<item-type>Number</item-type>
<label>Air Quality Index Average</label>
<state readOnly="true"/>
</channel-type>
<channel-type id="favoritelevel">
<item-type>Number</item-type>
<label>Favorite_level</label>
<state pattern="%.0f"/>
</channel-type>
<channel-type id="filtermaxlife">
<item-type>Number</item-type>
<label>Filter Max Life</label>
<state readOnly="true"/>
</channel-type>
<channel-type id="filterhours">
<item-type>Number</item-type>
<label>Filter Use Time</label>
<state pattern="%.0f" readOnly="true"/>
</channel-type>
<channel-type id="usedhours">
<item-type>Number</item-type>
<label>Run Time</label>
<state pattern="%.0f" readOnly="true"/>
</channel-type>
<channel-type id="motorspeed">
<item-type>Number</item-type>
<label>Motor Speed</label>
<state pattern="%.0f" readOnly="true"/>
</channel-type>
<channel-type id="filterlive">
<item-type>Number</item-type>
<label>Filter Life</label>
<state pattern="%.0f%%" readOnly="true"/>
</channel-type>
<channel-type id="purifyvolume">
<item-type>Number</item-type>
<label>Volume Purified</label>
<state pattern="%.0f m3" readOnly="true"/>
</channel-type>
<channel-type id="current">
<item-type>Number</item-type>
<label>Current</label>
<state pattern="%.2fA" readOnly="true"/>
</channel-type>
<channel-type id="powerUsage">
<item-type>Number</item-type>
<label>Power Usage</label>
<state pattern="%.0fW" readOnly="true"/>
</channel-type>
<channel-type id="powerPrice">
<item-type>Number</item-type>
<label>Power Price</label>
<state pattern="%.0f kW/h"/>
</channel-type>
<channel-type id="translevel">
<item-type>Number</item-type>
<label>Trans Level</label>
</channel-type>
<channel-type id="dry">
<item-type>Switch</item-type>
<label>Dry</label>
</channel-type>
<channel-type id="depth">
<item-type>Number</item-type>
<label>Depth</label>
</channel-type>
<channel-type id="angleEnable">
<item-type>Switch</item-type>
<label>Enable Angle</label>
</channel-type>
<channel-type id="angle">
<item-type>Number</item-type>
<label>Angle</label>
<state min="0" max="360" step="1" pattern="%.0f" readOnly="false"/>
</channel-type>
<channel-type id="move">
<item-type>String</item-type>
<label>Move</label>
<state>
<options>
<option value="left">left</option>
<option value="right">right</option>
</options>
</state>
</channel-type>
<channel-type id="poweroffTime">
<item-type>Number</item-type>
<label>Power Off Timer</label>
<state min="0" max="28800" step="1" pattern="%.0f" readOnly="false"/>
</channel-type>
<channel-type id="speed">
<item-type>Number</item-type>
<label>Speed</label>
<state pattern="%.0f" readOnly="false"/>
</channel-type>
<channel-type id="speedLevel">
<item-type>Number</item-type>
<label>Speed Level</label>
<state min="0" max="99" step="1" pattern="%.0f%%" readOnly="false"/>
</channel-type>
<channel-type id="naturalLevel">
<item-type>Number</item-type>
<label>Natural Fan Level</label>
<state min="0" max="99" step="1" pattern="%.0f%%" readOnly="false"/>
</channel-type>
<channel-type id="acPower">
<item-type>Switch</item-type>
<label>AC Power</label>
<state readOnly="true"/>
</channel-type>
<channel-type id="bat_state">
<item-type>String</item-type>
<label>Battery State</label>
<state readOnly="true"/>
</channel-type>
<channel-type id="gonight">
<item-type>Switch</item-type>
<label>Go Night</label>
<state readOnly="false"/>
</channel-type>
<channel-type id="usb">
<item-type>Switch</item-type>
<label>USB Power</label>
<state readOnly="false"/>
</channel-type>
<channel-type id="humidifierMode">
<item-type>String</item-type>
<label>Mode</label>
<state>
<options>
<option value="auto">Auto</option>
<option value="silent">Silent</option>
<option value="medium">Medium</option>
<option value="high">High</option>
</options>
</state>
</channel-type>
<channel-type id="airFreshMode">
<item-type>String</item-type>
<label>Mode</label>
<state>
<options>
<option value="auto">Auto</option>
<option value="favourite">Favorite</option>
<option value="sleep">Sleep</option>
</options>
</state>
</channel-type>
<channel-type id="airFreshFavoriteSpeed">
<item-type>Number</item-type>
<label>Favorite Speed</label>
<state min="0" max="300" pattern="%.0f" readOnly="false"/>
</channel-type>
<channel-type id="airFreshDisplay">
<item-type>Switch</item-type>
<label>Light On/Off</label>
</channel-type>
<channel-type id="airFreshSound">
<item-type>Switch</item-type>
<label>Sound On/Off</label>
</channel-type>
<channel-type id="airFreshChildLock">
<item-type>Switch</item-type>
<label>Child Lock On/Off</label>
</channel-type>
<channel-type id="airFreshDisplayDirection">
<item-type>String</item-type>
<label>Display Direction</label>
<state>
<options>
<option value="forward">Normal</option>
<option value="left">Counterclockwise 90°</option>
<option value="right">Clockwise 90°</option>
</options>
</state>
</channel-type>
<channel-type id="airFreshPTCPower">
<item-type>Switch</item-type>
<label>PTC Power On/Off</label>
</channel-type>
<channel-type id="airFreshPTCStatus">
<item-type>Switch</item-type>
<label>PTC Status On/Off</label>
<state readOnly="true"/>
</channel-type>
<channel-type id="airFreshPtcLevel">
<item-type>String</item-type>
<label>PTC Level</label>
<state>
<options>
<option value="low">Low</option>
<option value="medium">Medium</option>
<option value="high">High</option>
</options>
</state>
</channel-type>
<channel-type id="airFreshCurrentSpeed">
<item-type>Number</item-type>
<label>Current Speed</label>
<state pattern="%.0f m³/h" readOnly="true"/>
</channel-type>
<channel-type id="airFreshCO2">
<item-type>Number</item-type>
<label>CO2</label>
<state pattern="%.0f" readOnly="true"/>
</channel-type>
<channel-type id="airFreshPM25">
<item-type>Number</item-type>
<label>PM2.5</label>
<state pattern="%.0f" readOnly="true"/>
</channel-type>
<channel-type id="airFreshTemperature">
<item-type>Number</item-type>
<label>Temperature Outside</label>
<state pattern="%.0f°C" readOnly="true"/>
</channel-type>
<channel-type id="airFreshResetFilter">
<item-type>String</item-type>
<label>Reset Filter</label>
<state>
<options>
<option value="intermediate">Reset Filter</option>
<option value="efficient">Reset Filter Pro</option>
</options>
</state>
</channel-type>
<channel-type id="airFreshResetFilterA1">
<item-type>String</item-type>
<label>Reset Filter</label>
<state>
<options>
<option value="">Reset Filter</option>
</options>
</state>
</channel-type>
<channel-type id="airFreshFilterPercents">
<item-type>Number</item-type>
<label>Filter Percents Remaining</label>
<state pattern="%.0f%%" readOnly="true"/>
</channel-type>
<channel-type id="airFreshFilterDays">
<item-type>Number</item-type>
<label>Filter Days Remaining</label>
<state pattern="%.0f" readOnly="true"/>
</channel-type>
<channel-type id="airFreshFilterProPercents">
<item-type>Number</item-type>
<label>Filter Pro Percents Remaining</label>
<state pattern="%.0f%%" readOnly="true"/>
</channel-type>
<channel-type id="airFreshFilterProDays">
<item-type>Number</item-type>
<label>Filter Pro Days Remaining</label>
<state pattern="%.0f" readOnly="true"/>
</channel-type>
<channel-type id="miot_uint8">
<item-type>Number</item-type>
<label>Generic Number</label>
</channel-type>
<channel-type id="miot_int32">
<item-type>Number</item-type>
<label>Generic Number</label>
</channel-type>
<channel-type id="miot_float">
<item-type>Number</item-type>
<label>Generic Number</label>
</channel-type>
<channel-type id="miot_string">
<item-type>String</item-type>
<label>Generic String</label>
</channel-type>
<channel-type id="miot_bool">
<item-type>Switch</item-type>
<label>Generic Bool</label>
</channel-type>
<channel-type id="vacuumaction">
<item-type>Number</item-type>
<label>Vacuum Action</label>
<state>
<options>
<option value="0">Stop</option>
<option value="1">Vacuum</option>
<option value="2">Pause</option>
</options>
</state>
</channel-type>
<channel-type id="dreameControl">
<item-type>String</item-type>
<label>Control Vacuum</label>
<command>
<options>
<option value="vacuum">Vacuum</option>
<option value="pause">Pause</option>
<option value="stop">Stop</option>
<option value="dock">Dock</option>
<option value="sweep">Sweep</option>
<option value="stopsweep">Stop Sweep</option>
</options>
</command>
</channel-type>
</thing:thing-descriptions>

View File

@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="miio"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<thing-type id="generic">
<label>Xiaomi Mi Device</label>
<channel-groups>
<channel-group id="network" typeId="network"/>
<channel-group id="actions" typeId="miioactions"/>
</channel-groups>
<properties>
<property name="vendor">Xiaomi</property>
</properties>
<config-description-ref uri="thing-type:miio:config"/>
</thing-type>
</thing:thing-descriptions>

View File

@@ -0,0 +1,36 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="miio"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<thing-type id="unsupported">
<label>Unsupported Xiaomi Mi Device</label>
<channel-groups>
<channel-group id="network" typeId="network"/>
<channel-group id="actions" typeId="miioactions"/>
</channel-groups>
<properties>
<property name="vendor">Xiaomi</property>
</properties>
<config-description-ref uri="thing-type:miio:config"/>
</thing-type>
<channel-group-type id="miioactions">
<label>Actions</label>
<channels>
<channel id="power" typeId="power"/>
<channel id="commands" typeId="commands"/>
<channel id="testcommands" typeId="testcommands"/>
</channels>
</channel-group-type>
<channel-type id="testcommands">
<item-type>Switch</item-type>
<label>(experimental)Execute Test Commands</label>
<description>(experimental)Execute Test Commands to support development for your device. (NB device can switch modes)</description>
</channel-type>
</thing:thing-descriptions>

View File

@@ -0,0 +1,357 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="miio"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<thing-type id="vacuum">
<label>Xiaomi Robot Vacuum</label>
<channel-groups>
<channel-group id="actions" typeId="actions"/>
<channel-group id="status" typeId="status"/>
<channel-group id="consumables" typeId="consumables"/>
<channel-group id="dnd" typeId="dnd"/>
<channel-group id="history" typeId="history"/>
<channel-group id="cleaning" typeId="cleaning"/>
<channel-group id="network" typeId="network"/>
</channel-groups>
<properties>
<property name="vendor">Xiaomi</property>
</properties>
<config-description-ref uri="thing-type:miio:config"/>
</thing-type>
<channel-group-type id="actions">
<label>Action</label>
<channels>
<channel id="control" typeId="control"/>
<channel id="commands" typeId="commands"/>
<channel id="fan" typeId="fan"/>
<channel id="vacuum" typeId="vacuum"/>
<channel id="segment" typeId="segment"/>
</channels>
</channel-group-type>
<channel-group-type id="status">
<label>Status</label>
<channels>
<channel id="battery" typeId="system.battery-level"/>
<channel id="clean_area" typeId="clean_area"/>
<channel id="clean_time" typeId="clean_time"/>
<channel id="error_code" typeId="error_code"/>
<channel id="error_id" typeId="error_id"/>
<channel id="fan_power" typeId="fan_power"/>
<channel id="in_cleaning" typeId="in_cleaning"/>
<channel id="dnd_enabled" typeId="dnd_enabled"/>
<channel id="map_present" typeId="map_present"/>
<channel id="state" typeId="state"/>
<channel id="state_id" typeId="state_id"/>
</channels>
</channel-group-type>
<channel-group-type id="consumables">
<label>Consumables</label>
<channels>
<channel id="main_brush_percent" typeId="main_brush_percent"/>
<channel id="side_brush_percent" typeId="side_brush_percent"/>
<channel id="filter_percent" typeId="filter_percent"/>
<channel id="sensor_dirt_percent" typeId="sensor_dirt_percent"/>
<channel id="main_brush_time" typeId="main_brush_time"/>
<channel id="side_brush_time" typeId="side_brush_time"/>
<channel id="filter_time" typeId="filter_time"/>
<channel id="sensor_dirt_time" typeId="sensor_dirt_time"/>
<channel id="consumable_reset" typeId="consumable_reset"/>
</channels>
</channel-group-type>
<channel-group-type id="dnd">
<label>Do Not Disturb</label>
<channels>
<channel id="dnd_function" typeId="dnd_function"/>
<channel id="dnd_start" typeId="dnd_start"/>
<channel id="dnd_end" typeId="dnd_end"/>
</channels>
</channel-group-type>
<channel-group-type id="cleaning">
<label>Last Cleaning Details</label>
<channels>
<channel id="last_clean_start_time" typeId="last_clean_start_time"/>
<channel id="last_clean_end_time" typeId="last_clean_end_time"/>
<channel id="last_clean_area" typeId="last_clean_area"/>
<channel id="last_clean_duration" typeId="last_clean_duration"/>
<channel id="last_clean_error" typeId="last_clean_error"/>
<channel id="last_clean_finish" typeId="last_clean_finish"/>
<channel id="last_clean_record" typeId="last_clean_record"/>
<channel id="map" typeId="map"/>
</channels>
</channel-group-type>
<channel-group-type id="history">
<label>History</label>
<channels>
<channel id="total_clean_area" typeId="total_clean_area"/>
<channel id="total_clean_time" typeId="total_clean_time"/>
<channel id="total_clean_count" typeId="total_clean_count"/>
</channels>
</channel-group-type>
<!-- Status channels -->
<channel-type id="clean_area">
<item-type>Number</item-type>
<label>Cleaning Area</label>
<state pattern="%.2f m²" readOnly="true"/>
</channel-type>
<channel-type id="clean_time">
<item-type>Number</item-type>
<label>Cleaning Time</label>
<state pattern="%.0f'" readOnly="true"/>
</channel-type>
<channel-type id="dnd_enabled">
<item-type>Switch</item-type>
<label>Do Not Disturb</label>
</channel-type>
<channel-type id="error_code">
<item-type>String</item-type>
<label>Error Code</label>
<state readOnly="true"/>
</channel-type>
<channel-type id="error_id">
<item-type>Number</item-type>
<label>Error ID</label>
<state readOnly="true"/>
</channel-type>
<channel-type id="fan_power">
<item-type>Number</item-type>
<label>Fan Power</label>
<state min="1" max="99" step="1" pattern="%.0f%%" readOnly="false"/>
</channel-type>
<channel-type id="in_cleaning" advanced="true">
<item-type>Number</item-type>
<label>In Cleaning</label>
<state pattern="%.0f" readOnly="true"/>
</channel-type>
<channel-type id="map_present" advanced="true">
<item-type>Number</item-type>
<label>Map Present</label>
<category>Energy</category>
<state pattern="%.0f" readOnly="true"/>
</channel-type>
<channel-type id="msg_seq" advanced="true">
<item-type>Number</item-type>
<label>Msg Seq</label>
<state readOnly="true"/>
</channel-type>
<channel-type id="state">
<item-type>String</item-type>
<label>State</label>
<state readOnly="true"/>
</channel-type>
<channel-type id="state_id">
<item-type>Number</item-type>
<label>State ID</label>
<state readOnly="true"/>
</channel-type>
<!-- Dynamic Status Channels -->
<channel-type id="water_box_mode">
<item-type>Number</item-type>
<label>Water Box Mode</label>
<state min="200" max="204" step="1" pattern="%.0f%%" readOnly="false"/>
</channel-type>
<channel-type id="water_box_status">
<item-type>Switch</item-type>
<label>Water Box State</label>
<state readOnly="true"/>
</channel-type>
<channel-type id="water_box_carriage_status">
<item-type>Switch</item-type>
<label>Water Box Carriage State</label>
<state readOnly="true"/>
</channel-type>
<channel-type id="lock_status">
<item-type>Switch</item-type>
<label>Lock Status</label>
<state readOnly="true"/>
</channel-type>
<channel-type id="mop_forbidden_enable">
<item-type>Switch</item-type>
<label>Mop Forbidden</label>
<state readOnly="true"/>
</channel-type>
<!-- Consumables channels -->
<channel-type id="main_brush_percent">
<item-type>Number</item-type>
<label>Main Brush Remaining</label>
<state pattern="%.0f%%" readOnly="true"/>
</channel-type>
<channel-type id="side_brush_percent">
<item-type>Number</item-type>
<label>Side Brush Remaining</label>
<state pattern="%.0f%%" readOnly="true"/>
</channel-type>
<channel-type id="filter_percent">
<item-type>Number</item-type>
<label>Filter Usage Remaining</label>
<state pattern="%.0f%%" readOnly="true"/>
</channel-type>
<channel-type id="sensor_dirt_percent">
<item-type>Number</item-type>
<label>Sensor Dirt Remaining</label>
<state pattern="%.0f%%" readOnly="true"/>
</channel-type>
<channel-type id="main_brush_time" advanced="true">
<item-type>Number</item-type>
<label>Main Brush Hours till Replacement</label>
<state pattern="%.0fh" readOnly="true"/>
</channel-type>
<channel-type id="side_brush_time" advanced="true">
<item-type>Number</item-type>
<label>Side Brush Hours till Replacement</label>
<state pattern="%.0fh" readOnly="true"/>
</channel-type>
<channel-type id="filter_time" advanced="true">
<item-type>Number</item-type>
<label>Filter Hours till Replacement</label>
<state pattern="%.0fh" readOnly="true"/>
</channel-type>
<channel-type id="sensor_dirt_time" advanced="true">
<item-type>Number</item-type>
<label>Sensor Dirt Time till Cleaning</label>
<state pattern="%.0fh" readOnly="true"/>
</channel-type>
<!-- actions -->
<channel-type id="control">
<item-type>String</item-type>
<label>Control Vacuum</label>
<state>
<options>
<option value="vacuum">Vacuum</option>
<option value="spot">Spot Clean</option>
<option value="pause">Pause</option>
<option value="dock">Dock</option>
</options>
</state>
</channel-type>
<channel-type id="fan">
<item-type>Number</item-type>
<label>Control Fan Level</label>
<state min="-1" max="105" step="1" readOnly="false">
<options>
<option value="38">Silent</option>
<option value="60">Standard</option>
<option value="75">Turbo</option>
<option value="77">Power</option>
<option value="90">Full</option>
<option value="100">Max</option>
<option value="101">Quiet</option>
<option value="102">Balanced</option>
<option value="103">Turbov2</option>
<option value="104">Maxv2</option>
<option value="105">Mob</option>
<option value="-1">Custom</option>
</options>
</state>
</channel-type>
<channel-type id="vacuum" advanced="true">
<item-type>Switch</item-type>
<label>Vacuum On/Off</label>
</channel-type>
<channel-type id="segment" advanced="true">
<item-type>String</item-type>
<label>Vacuum Room [room#]</label>
</channel-type>
<channel-type id="consumable_reset">
<item-type>String</item-type>
<label>Reset Consumable</label>
<state>
<options>
<option value="none">Select Consumable</option>
<option value="main_brush_work_time">Reset Mainbrush</option>
<option value="side_brush_work_time">Reset Sidebrush</option>
<option value="filter_work_time">Reset Filter</option>
<option value="sensor_dirty_time">Reset Sensors</option>
</options>
</state>
</channel-type>
<!-- DND Setting -->
<channel-type id="dnd_function">
<item-type>Switch</item-type>
<label>Do Not Disturb Functionality</label>
</channel-type>
<channel-type id="dnd_start">
<item-type>String</item-type>
<label>Start Time DND</label>
</channel-type>
<channel-type id="dnd_end">
<item-type>String</item-type>
<label>End Time DND</label>
</channel-type>
<!-- Clean History -->
<channel-type id="total_clean_time">
<item-type>Number</item-type>
<label>Total Cleaning Time</label>
<description>Total Cleaning Time in minutes</description>
<state pattern="%.0f'" readOnly="true"/>
</channel-type>
<channel-type id="total_clean_area">
<item-type>Number</item-type>
<label>Total Cleaning Area</label>
<state pattern="%.1f m²" readOnly="true"/>
</channel-type>
<channel-type id="total_clean_count">
<item-type>Number</item-type>
<label>Total Cleanings</label>
<state pattern="%.0f" readOnly="true"/>
</channel-type>
<!-- Clean Record -->
<channel-type id="last_clean_start_time">
<item-type>DateTime</item-type>
<label>Cleaning Start</label>
<description>Last Cleaning Start Time</description>
<category>Date</category>
<state readOnly="true"/>
</channel-type>
<channel-type id="last_clean_end_time">
<item-type>DateTime</item-type>
<label>Cleaning End</label>
<description>Last Cleaning End Time</description>
<category>Date</category>
<state readOnly="true"/>
</channel-type>
<channel-type id="last_clean_duration">
<item-type>Number</item-type>
<label>Cleaning Duration</label>
<description>Cleaning Duration in minutes</description>
<state pattern="%.0f'" readOnly="true"/>
</channel-type>
<channel-type id="last_clean_area">
<item-type>Number</item-type>
<label>Cleaning Area</label>
<state pattern="%.1f m²" readOnly="true"/>
</channel-type>
<channel-type id="last_clean_error">
<item-type>Number</item-type>
<label>Error</label>
<state pattern="%.0f" readOnly="true"/>
</channel-type>
<channel-type id="last_clean_finish">
<item-type>Number</item-type>
<label>Cleaning Finished</label>
<state readOnly="true"/>
</channel-type>
<channel-type id="last_clean_record" advanced="true">
<item-type>String</item-type>
<label>Cleaning Record</label>
<state readOnly="true"/>
</channel-type>
<channel-type id="map" advanced="false">
<item-type>Image</item-type>
<label>Cleaning Map</label>
<state readOnly="true"/>
</channel-type>
</thing:thing-descriptions>

View File

@@ -0,0 +1,39 @@
{
"deviceMapping": {
"id": [
"090615.switch.xswitch01"
],
"propertyMethod": "get_prop",
"maxProperties": 1,
"channels": [
{
"property": "switch1",
"friendlyName": "Switch 1",
"channel": "switch1state",
"type": "Number",
"refresh": true,
"ChannelGroup": "actions",
"actions": [
{
"command": "SetSwitch1",
"parameterType": "NUMBER"
}
]
},
{
"property": "switchname1",
"friendlyName": "Switch Name 1",
"channel": "switch1name",
"type": "String",
"refresh": true,
"ChannelGroup": "actions",
"actions": [
{
"command": "SetSwtichname1",
"parameterType": "STRING"
}
]
}
]
}
}

View File

@@ -0,0 +1,67 @@
{
"deviceMapping": {
"id": [
"090615.switch.xswitch02"
],
"propertyMethod": "get_prop",
"maxProperties": 1,
"channels": [
{
"property": "switch1",
"friendlyName": "Switch 1",
"channel": "switch1state",
"type": "Number",
"refresh": true,
"ChannelGroup": "actions",
"actions": [
{
"command": "SetSwitch1",
"parameterType": "NUMBER"
}
]
},
{
"property": "switch2",
"friendlyName": "Switch 2",
"channel": "switch2state",
"type": "Number",
"refresh": true,
"ChannelGroup": "actions",
"actions": [
{
"command": "SetSwitch2",
"parameterType": "NUMBER"
}
]
},
{
"property": "switchname1",
"friendlyName": "Switch Name 1",
"channel": "switch1name",
"type": "String",
"refresh": true,
"ChannelGroup": "actions",
"actions": [
{
"command": "SetSwtichname1",
"parameterType": "STRING"
}
]
},
{
"property": "switchname2",
"friendlyName": "Switch Name 2",
"channel": "switch2name",
"type": "String",
"refresh": true,
"ChannelGroup": "actions",
"actions": [
{
"command": "SetSwtichname2",
"parameterType": "STRING"
}
]
}
]
}
}

View File

@@ -0,0 +1,95 @@
{
"deviceMapping": {
"id": [
"090615.switch.xswitch03"
],
"propertyMethod": "get_prop",
"maxProperties": 1,
"channels": [
{
"property": "switch1",
"friendlyName": "Switch 1",
"channel": "switch1state",
"type": "Number",
"refresh": true,
"ChannelGroup": "actions",
"actions": [
{
"command": "SetSwitch1",
"parameterType": "NUMBER"
}
]
},
{
"property": "switch2",
"friendlyName": "Switch 2",
"channel": "switch2state",
"type": "Number",
"refresh": true,
"ChannelGroup": "actions",
"actions": [
{
"command": "SetSwitch2",
"parameterType": "NUMBER"
}
]
},
{
"property": "switch3",
"friendlyName": "Switch 3",
"channel": "switch3state",
"type": "Number",
"refresh": true,
"ChannelGroup": "actions",
"actions": [
{
"command": "SetSwitch3",
"parameterType": "NUMBER"
}
]
},
{
"property": "switchname1",
"friendlyName": "Switch Name 1",
"channel": "switch1name",
"type": "String",
"refresh": true,
"ChannelGroup": "actions",
"actions": [
{
"command": "SetSwtichname1",
"parameterType": "STRING"
}
]
},
{
"property": "switchname2",
"friendlyName": "Switch Name 2",
"channel": "switch2name",
"type": "String",
"refresh": true,
"ChannelGroup": "actions",
"actions": [
{
"command": "SetSwtichname2",
"parameterType": "STRING"
}
]
},
{
"property": "switchname3",
"friendlyName": "Switch Name 3",
"channel": "switch3name",
"type": "String",
"refresh": true,
"ChannelGroup": "actions",
"actions": [
{
"command": "SetSwtichname3",
"parameterType": "STRING"
}
]
}
]
}
}

View File

@@ -0,0 +1,66 @@
{
"deviceMapping": {
"id": [
"cgllc.airmonitor.b1"
],
"propertyMethod": "get_value",
"maxProperties": 6,
"channels": [
{
"property": "battery",
"friendlyName": "Battery",
"channel": "battery",
"channelType": "system:battery-level",
"type": "Number",
"refresh": true,
"ChannelGroup": "Status",
"actions": []
},
{
"property": "pm25",
"friendlyName": "PM2.5",
"channel": "pm25",
"type": "Number",
"refresh": true,
"ChannelGroup": "Status",
"actions": []
},
{
"property": "co2e",
"friendlyName": "CO2e",
"channel": "co2",
"type": "Number",
"refresh": true,
"ChannelGroup": "Status",
"actions": []
},
{
"property": "tvoc",
"friendlyName": "tVOC",
"channel": "tvoc",
"type": "Number",
"refresh": true,
"ChannelGroup": "Status",
"actions": []
},
{
"property": "humidity",
"friendlyName": "Humidity",
"channel": "humidity",
"type": "Number",
"refresh": true,
"ChannelGroup": "Status",
"actions": []
},
{
"property": "temperature",
"friendlyName": "Temperature",
"channel": "temperature",
"type": "Number",
"refresh": true,
"ChannelGroup": "Status",
"actions": []
}
]
}
}

View File

@@ -0,0 +1,66 @@
{
"deviceMapping": {
"id": [
"cgllc.airmonitor.s1"
],
"propertyMethod": "get_value",
"maxProperties": 6,
"channels": [
{
"property": "battery",
"friendlyName": "Battery",
"channel": "battery",
"channelType": "system:battery-level",
"type": "Number",
"refresh": true,
"ChannelGroup": "Status",
"actions": []
},
{
"property": "pm25",
"friendlyName": "PM2.5",
"channel": "pm25",
"type": "Number",
"refresh": true,
"ChannelGroup": "Status",
"actions": []
},
{
"property": "co2",
"friendlyName": "CO2",
"channel": "co2",
"type": "Number",
"refresh": true,
"ChannelGroup": "Status",
"actions": []
},
{
"property": "tvoc",
"friendlyName": "tVOC",
"channel": "tvoc",
"type": "Number",
"refresh": true,
"ChannelGroup": "Status",
"actions": []
},
{
"property": "humidity",
"friendlyName": "Humidity",
"channel": "humidity",
"type": "Number",
"refresh": true,
"ChannelGroup": "Status",
"actions": []
},
{
"property": "temperature",
"friendlyName": "Temperature",
"channel": "temperature",
"type": "Number",
"refresh": true,
"ChannelGroup": "Status",
"actions": []
}
]
}
}

View File

@@ -0,0 +1,48 @@
{
"deviceMapping": {
"id": [
"chuangmi.plug.m1",
"chuangmi.plug.m3",
"chuangmi.plug.hmi205"
],
"channels": [
{
"property": "power",
"friendlyName": "Power",
"channel": "power",
"type": "Switch",
"refresh": true,
"ChannelGroup": "",
"actions": [
{
"command": "set_power",
"parameterType": "ONOFF"
}
]
},
{
"property": "temperature",
"friendlyName": "Temperature",
"channel": "temperature",
"type": "Number",
"refresh": true,
"ChannelGroup": "",
"actions": []
},
{
"property": "wifi_led",
"friendlyName": "Indicator light",
"channel": "led",
"type": "Switch",
"refresh": true,
"ChannelGroup": "",
"actions": [
{
"command": "set_wifi_led",
"parameterType": "ONOFF"
}
]
}
]
}
}

View File

@@ -0,0 +1,36 @@
{
"deviceMapping": {
"id": [
"chuangmi.plug.v1"
],
"channels": [
{
"property": "on",
"friendlyName": "Power",
"channel": "power",
"type": "Switch",
"refresh": true,
"actions": [
{
"command": "set_*",
"parameterType": "ONOFFPARA"
}
]
},
{
"property": "usb_on",
"friendlyName": "USB",
"channel": "usb",
"type": "Switch",
"refresh": true,
"ChannelGroup": "actions",
"actions": [
{
"command": "set_usb_*",
"parameterType": "ONOFFPARA"
}
]
}
]
}
}

View File

@@ -0,0 +1,37 @@
{
"deviceMapping": {
"id": [
"chuangmi.plug.v2"
],
"channels": [
{
"property": "power",
"friendlyName": "Power",
"channel": "power",
"type": "Switch",
"refresh": true,
"ChannelGroup": "",
"actions": [
{
"command": "set_power",
"parameterType": "ONOFF"
}
]
},
{
"property": "usb_on",
"friendlyName": "USB",
"channel": "usb",
"type": "Switch",
"refresh": true,
"ChannelGroup": "actions",
"actions": [
{
"command": "set_usb_*",
"parameterType": "ONOFFPARA"
}
]
}
]
}
}

View File

@@ -0,0 +1,59 @@
{
"deviceMapping": {
"id": [
"chuangmi.plug.v3"
],
"channels": [
{
"property": "power",
"friendlyName": "Power",
"channel": "power",
"type": "Switch",
"refresh": true,
"actions": [
{
"command": "set_*",
"parameterType": "ONOFFPARA"
}
]
},
{
"property": "usb_on",
"friendlyName": "USB",
"channel": "usb",
"type": "Switch",
"refresh": true,
"ChannelGroup": "actions",
"actions": [
{
"command": "set_usb_*",
"parameterType": "ONOFFPARA"
}
]
},
{
"property": "temperature",
"friendlyName": "Temperature",
"channel": "temperature",
"type": "Number",
"refresh": true,
"ChannelGroup": "",
"actions": []
},
{
"property": "wifi_led",
"friendlyName": "Wifi LED",
"channel": "led",
"type": "Switch",
"refresh": true,
"ChannelGroup": "actions",
"actions": [
{
"command": "set_wifi_led",
"parameterType": "ONOFF"
}
]
}
]
}
}

View File

@@ -0,0 +1,60 @@
{
"deviceMapping": {
"id": [
"chuangmi.plug.v3fw"
],
"channels": [
{
"property": "power",
"friendlyName": "Power",
"channel": "power",
"type": "Switch",
"refresh": true,
"ChannelGroup": "",
"actions": [
{
"command": "set_power",
"parameterType": "ONOFF"
}
]
},
{
"property": "usb_on",
"friendlyName": "USB",
"channel": "usb",
"type": "Switch",
"refresh": true,
"ChannelGroup": "actions",
"actions": [
{
"command": "set_usb_*",
"parameterType": "ONOFFPARA"
}
]
},
{
"property": "temperature",
"friendlyName": "Temperature",
"channel": "temperature",
"type": "Number",
"refresh": true,
"ChannelGroup": "",
"actions": []
},
{
"property": "wifi_led",
"friendlyName": "Wifi LED",
"channel": "led",
"type": "Switch",
"refresh": true,
"ChannelGroup": "actions",
"actions": [
{
"command": "set_wifi_led",
"parameterType": "ONOFF"
}
]
}
]
}
}

View File

@@ -0,0 +1,71 @@
{
"deviceMapping": {
"id": [
"cuco.plug.cp1"
],
"propertyMethod": "get_properties",
"maxProperties": 2,
"channels": [
{
"property": "firmware-revision",
"siid": 1,
"piid": 4,
"friendlyName": "Device Information-CurrentFirmware Version",
"channel": "FirmwareRevision",
"channelType": "miot_string",
"type": "String",
"refresh": true,
"actions": []
},
{
"property": "manufacturer",
"siid": 1,
"piid": 1,
"friendlyName": "Device Information-Device Manufacturer",
"channel": "Manufacturer",
"channelType": "miot_string",
"type": "String",
"refresh": true,
"actions": []
},
{
"property": "model",
"siid": 1,
"piid": 2,
"friendlyName": "Device Information-Device Model",
"channel": "Model",
"channelType": "miot_string",
"type": "String",
"refresh": true,
"actions": []
},
{
"property": "serial-number",
"siid": 1,
"piid": 3,
"friendlyName": "Device Information-Device Serial Number",
"channel": "SerialNumber",
"channelType": "miot_string",
"type": "String",
"refresh": true,
"actions": []
},
{
"property": "on",
"siid": 2,
"piid": 1,
"friendlyName": "Switch-Switch Status",
"channel": "On",
"channelType": "miot_bool",
"type": "Switch",
"refresh": true,
"actions": [
{
"command": "set_properties",
"parameterType": "ONOFFBOOL"
}
]
}
]
}
}

View File

@@ -0,0 +1,177 @@
{
"deviceMapping": {
"id": [
"dmaker.airfresh.a1"
],
"channels": [
{
"property": "power",
"friendlyName": "Power",
"channel": "power",
"type": "Switch",
"refresh": true,
"actions": [
{
"command": "set_power",
"parameterType": "ONOFFBOOL"
}
]
},
{
"property": "mode",
"friendlyName": "Mode",
"channel": "airFreshMode",
"type": "String",
"refresh": true,
"actions": [
{
"command": "set_mode",
"parameterType": "STRING"
}
]
},
{
"property": "ptc_on",
"friendlyName": "PTC",
"channel": "airFreshPTCPower",
"type": "Switch",
"refresh": true,
"actions": [
{
"command": "set_ptc_on",
"parameterType": "ONOFFBOOL"
}
]
},
{
"property": "ptc_status",
"friendlyName": "PTC Status",
"channel": "airFreshPTCStatus",
"type": "Switch",
"refresh": true,
"ChannelGroup": "Status",
"actions": []
},
{
"property": "display",
"friendlyName": "Display",
"channel": "airFreshDisplay",
"type": "Switch",
"refresh": true,
"actions": [
{
"command": "set_display",
"parameterType": "ONOFFBOOL"
}
]
},
{
"property": "child_lock",
"friendlyName": "Child Lock",
"channel": "airFreshChildLock",
"type": "Switch",
"refresh": true,
"actions": [
{
"command": "set_child_lock",
"parameterType": "ONOFFBOOL"
}
]
},
{
"property": "sound",
"friendlyName": "Sound",
"channel": "airFreshSound",
"type": "Switch",
"refresh": true,
"actions": [
{
"command": "set_sound",
"parameterType": "ONOFFBOOL"
}
]
},
{
"property": "pm25",
"friendlyName": "PM2.5",
"channel": "airFreshPM25",
"type": "Number",
"refresh": true,
"ChannelGroup": "Status",
"actions": []
},
{
"property": "co2",
"friendlyName": "CO2",
"channel": "airFreshCO2",
"type": "Number",
"refresh": true,
"ChannelGroup": "Status",
"actions": []
},
{
"property": "control_speed",
"friendlyName": "Current Speed",
"channel": "airFreshCurrentSpeed",
"type": "Number",
"refresh": true,
"ChannelGroup": "Status",
"actions": []
},
{
"property": "favourite_speed",
"friendlyName": "Favorite Speed",
"channel": "airFreshFavoriteSpeed",
"type": "Number",
"refresh": true,
"actions": [
{
"command": "set_favourite_speed",
"parameterType": "NUMBER"
}
]
},
{
"property": "temperature_outside",
"friendlyName": "Temperature Outside",
"channel": "airFreshTemperature",
"type": "Number",
"refresh": true,
"ChannelGroup": "Status",
"actions": []
},
{
"property": "filter_rate",
"friendlyName": "Filter Percents Remaining",
"channel": "airFreshFilterPercents",
"type": "Number",
"refresh": true,
"ChannelGroup": "",
"actions": []
},
{
"property": "filter_day",
"friendlyName": "Filter Days Remaining",
"channel": "airFreshFilterDays",
"type": "Number",
"refresh": true,
"ChannelGroup": "Status",
"actions": []
},
{
"property": "",
"friendlyName": "Reset Filter",
"channel": "airFreshResetFilterA1",
"type": "String",
"refresh": false,
"ChannelGroup": "actions",
"actions": [
{
"command": "set_filter_reset",
"parameterType": "STRING"
}
]
}
]
}
}

View File

@@ -0,0 +1,221 @@
{
"deviceMapping": {
"id": [
"dmaker.airfresh.t2017"
],
"channels": [
{
"property": "power",
"friendlyName": "Power",
"channel": "power",
"type": "Switch",
"refresh": true,
"actions": [
{
"command": "set_power",
"parameterType": "ONOFFBOOL"
}
]
},
{
"property": "mode",
"friendlyName": "Mode",
"channel": "airFreshMode",
"type": "String",
"refresh": true,
"actions": [
{
"command": "set_mode",
"parameterType": "STRING"
}
]
},
{
"property": "ptc_on",
"friendlyName": "PTC",
"channel": "airFreshPTCPower",
"type": "Switch",
"refresh": true,
"actions": [
{
"command": "set_ptc_on",
"parameterType": "ONOFFBOOL"
}
]
},
{
"property": "ptc_level",
"friendlyName": "PTC Level",
"channel": "airFreshPtcLevel",
"type": "String",
"refresh": true,
"actions": [
{
"command": "set_ptc_level",
"parameterType": "STRING"
}
]
},
{
"property": "ptc_status",
"friendlyName": "PTC Status",
"channel": "airFreshPTCStatus",
"type": "Switch",
"refresh": true,
"ChannelGroup": "Status",
"actions": []
},
{
"property": "screen_direction",
"friendlyName": "Screen direction",
"channel": "airFreshDisplayDirection",
"type": "String",
"refresh": true,
"actions": [
{
"command": "set_screen_direction",
"parameterType": "STRING"
}
]
},
{
"property": "display",
"friendlyName": "Display",
"channel": "airFreshDisplay",
"type": "Switch",
"refresh": true,
"actions": [
{
"command": "set_display",
"parameterType": "ONOFFBOOL"
}
]
},
{
"property": "child_lock",
"friendlyName": "Child Lock",
"channel": "airFreshChildLock",
"type": "Switch",
"refresh": true,
"actions": [
{
"command": "set_child_lock",
"parameterType": "ONOFFBOOL"
}
]
},
{
"property": "sound",
"friendlyName": "Sound",
"channel": "airFreshSound",
"type": "Switch",
"refresh": true,
"actions": [
{
"command": "set_sound",
"parameterType": "ONOFFBOOL"
}
]
},
{
"property": "pm25",
"friendlyName": "PM2.5",
"channel": "airFreshPM25",
"type": "Number",
"refresh": true,
"ChannelGroup": "Status",
"actions": []
},
{
"property": "co2",
"friendlyName": "CO2",
"channel": "airFreshCO2",
"type": "Number",
"refresh": true,
"ChannelGroup": "Status",
"actions": []
},
{
"property": "control_speed",
"friendlyName": "Current Speed",
"channel": "airFreshCurrentSpeed",
"type": "Number",
"refresh": true,
"ChannelGroup": "Status",
"actions": []
},
{
"property": "favourite_speed",
"friendlyName": "Favorite Speed",
"channel": "airFreshFavoriteSpeed",
"type": "Number",
"refresh": true,
"actions": [
{
"command": "set_favourite_speed",
"parameterType": "NUMBER"
}
]
},
{
"property": "temperature_outside",
"friendlyName": "Temperature Outside",
"channel": "airFreshTemperature",
"type": "Number",
"refresh": true,
"ChannelGroup": "Status",
"actions": []
},
{
"property": "filter_intermediate",
"friendlyName": "Filter Percents Remaining",
"channel": "airFreshFilterPercents",
"type": "Number",
"refresh": true,
"ChannelGroup": "",
"actions": []
},
{
"property": "filter_inter_day",
"friendlyName": "Filter Days Remaining",
"channel": "airFreshFilterDays",
"type": "Number",
"refresh": true,
"ChannelGroup": "Status",
"actions": []
},
{
"property": "filter_efficient",
"friendlyName": "Filter Pro Percents Remaining",
"channel": "airFreshFilterProPercents",
"type": "Number",
"refresh": true,
"ChannelGroup": "Status",
"actions": []
},
{
"property": "filter_effi_day",
"friendlyName": "Filter Pro Days Remaining",
"channel": "airFreshFilterProDays",
"type": "Number",
"refresh": true,
"ChannelGroup": "Status",
"actions": []
},
{
"property": "",
"friendlyName": "Reset Filter",
"channel": "airFreshResetFilter",
"type": "String",
"refresh": false,
"ChannelGroup": "actions",
"actions": [
{
"command": "set_filter_reset",
"parameterType": "STRING"
}
]
}
]
}
}

View File

@@ -0,0 +1,133 @@
{
"deviceMapping": {
"id": [
"dmaker.fan.p5"
],
"channels": [
{
"property": "power",
"friendlyName": "Power",
"channel": "power",
"type": "Switch",
"refresh": true,
"actions": [
{
"command": "s_power",
"parameterType": "ONOFF"
}
]
},
{
"property": "roll",
"friendlyName": "Rotation",
"channel": "roll",
"type": "Switch",
"refresh": true,
"actions": [
{
"command": "s_roll",
"parameterType": "ONOFF"
}
]
},
{
"property": "mode",
"friendlyName": "Mode",
"channel": "mode",
"type": "Number",
"refresh": true,
"ChannelGroup": "actions",
"actions": [
{
"command": "s_mode",
"parameterType": "STRING"
}
]
},
{
"property": "roll_angle",
"friendlyName": "Angle",
"channel": "angle",
"type": "Number",
"refresh": true,
"ChannelGroup": "actions",
"actions": [
{
"command": "s_angle",
"parameterType": "NUMBER"
}
]
},
{
"property": "timer_off",
"friendlyName": "Timer",
"channel": "timer",
"type": "Number",
"refresh": true,
"ChannelGroup": "actions",
"actions": [
{
"command": "s_t_off",
"parameterType": "NUMBER"
}
]
},
{
"property": "beep_sound",
"friendlyName": "Beep Sound",
"channel": "beep",
"type": "Switch",
"refresh": true,
"ChannelGroup": "actions",
"actions": [
{
"command": "s_beep_sound",
"parameterType": "ONOFF"
}
]
},
{
"property": "light",
"friendlyName": "Light",
"channel": "light",
"type": "Number",
"refresh": true,
"ChannelGroup": "actions",
"actions": [
{
"command": "s_light",
"parameterType": "NUMBER"
}
]
},
{
"property": "child_lock",
"friendlyName": "Child Lock",
"channel": "child_lock",
"type": "Switch",
"refresh": true,
"ChannelGroup": "actions",
"actions": [
{
"command": "s_lock",
"parameterType": "ONOFF"
}
]
},
{
"property": "speed",
"friendlyName": "Speed",
"channel": "speed",
"type": "Number",
"refresh": true,
"ChannelGroup": "actions",
"actions": [
{
"command": "s_speed",
"parameterType": "NUMBER"
}
]
}
]
}
}

View File

@@ -0,0 +1,188 @@
{
"deviceMapping": {
"id": [
"dmaker.fan.p8",
"dmaker.fan.1c"
],
"propertyMethod": "get_properties",
"maxProperties": 2,
"channels": [
{
"property": "manufacturer",
"siid": 1,
"piid": 1,
"friendlyName": "Device Information-Device Manufacturer",
"channel": "Manufacturer",
"channelType": "miot_string",
"type": "String",
"refresh": true,
"actions": [
]
},
{
"property": "model",
"siid": 1,
"piid": 2,
"friendlyName": "Device Information-Device Model",
"channel": "Model",
"channelType": "miot_string",
"type": "String",
"refresh": true,
"actions": [
]
},
{
"property": "serial-number",
"siid": 1,
"piid": 3,
"friendlyName": "Device Information-Device Serial Number",
"channel": "SerialNumber",
"channelType": "miot_string",
"type": "String",
"refresh": true,
"actions": [
]
},
{
"property": "firmware-revision",
"siid": 1,
"piid": 4,
"friendlyName": "Device Information-Current Firmware Version",
"channel": "FirmwareRevision",
"channelType": "miot_string",
"type": "String",
"refresh": true,
"actions": [
]
},
{
"property": "on",
"siid": 2,
"piid": 1,
"friendlyName": "Fan-Switch Status",
"channel": "On",
"channelType": "miot_bool",
"type": "Switch",
"refresh": true,
"actions": [
{
"command": "set_properties",
"parameterType": "ONOFFBOOL"
}
]
},
{
"property": "fan-level",
"siid": 2,
"piid": 2,
"friendlyName": "Fan-Fan Level",
"channel": "FanLevel",
"channelType": "miot_uint8",
"type": "Number",
"refresh": true,
"actions": [
{
"command": "set_properties",
"parameterType": "NUMBER"
}
]
},
{
"property": "horizontal-swing",
"siid": 2,
"piid": 3,
"friendlyName": "Fan-Horizontal Swing",
"channel": "HorizontalSwing",
"channelType": "miot_bool",
"type": "Switch",
"refresh": true,
"actions": [
{
"command": "set_properties",
"parameterType": "ONOFFBOOL"
}
]
},
{
"property": "mode",
"siid": 2,
"piid": 7,
"friendlyName": "Fan-Mode",
"channel": "Mode",
"channelType": "miot_uint8",
"type": "Number",
"refresh": true,
"actions": [
{
"command": "set_properties",
"parameterType": "NUMBER"
}
]
},
{
"property": "off-delay-time",
"siid": 2,
"piid": 10,
"friendlyName": "Fan-Power Off Delay Time",
"channel": "OffDelayTime",
"channelType": "miot_uint16",
"type": "Number",
"refresh": true,
"actions": [
{
"command": "set_properties",
"parameterType": "NUMBER"
}
]
},
{
"property": "alarm",
"siid": 2,
"piid": 11,
"friendlyName": "Fan-Alarm",
"channel": "Alarm",
"channelType": "miot_bool",
"type": "Switch",
"refresh": true,
"actions": [
{
"command": "set_properties",
"parameterType": "ONOFFBOOL"
}
]
},
{
"property": "brightness",
"siid": 2,
"piid": 12,
"friendlyName": "Fan-Brightness",
"channel": "Brightness",
"channelType": "miot_bool",
"type": "Switch",
"refresh": true,
"actions": [
{
"command": "set_properties",
"parameterType": "ONOFFBOOL"
}
]
},
{
"property": "physical-controls-locked",
"siid": 3,
"piid": 1,
"friendlyName": "Physical Control Locked-Physical Control Locked",
"channel": "PhysicalControlsLocked",
"channelType": "miot_bool",
"type": "Switch",
"refresh": true,
"actions": [
{
"command": "set_properties",
"parameterType": "ONOFFBOOL"
}
]
}
]
}
}

View File

@@ -0,0 +1,236 @@
{
"deviceMapping": {
"id": [
"dmaker.fan.p9",
"dmaker.fan.p10"
],
"propertyMethod": "get_properties",
"maxProperties": 2,
"channels": [
{
"property": "manufacturer",
"siid": 1,
"piid": 1,
"friendlyName": "Device Information-Device Manufacturer",
"channel": "Manufacturer",
"channelType": "miot_string",
"type": "String",
"refresh": true,
"actions": [
]
},
{
"property": "model",
"siid": 1,
"piid": 2,
"friendlyName": "Device Information-Device Model",
"channel": "Model",
"channelType": "miot_string",
"type": "String",
"refresh": true,
"actions": [
]
},
{
"property": "serial-number",
"siid": 1,
"piid": 3,
"friendlyName": "Device Information-Device Serial Number",
"channel": "SerialNumber",
"channelType": "miot_string",
"type": "String",
"refresh": true,
"actions": [
]
},
{
"property": "firmware-revision",
"siid": 1,
"piid": 4,
"friendlyName": "Device Information-Current Firmware Version",
"channel": "FirmwareRevision",
"channelType": "miot_string",
"type": "String",
"refresh": true,
"actions": [
]
},
{
"property": "on",
"siid": 2,
"piid": 1,
"friendlyName": "Fan-Switch Status",
"channel": "On",
"channelType": "miot_bool",
"type": "Switch",
"refresh": true,
"actions": [
{
"command": "set_properties",
"parameterType": "ONOFFBOOL"
}
]
},
{
"property": "fan-level",
"siid": 2,
"piid": 2,
"friendlyName": "Fan-Fan Level",
"channel": "FanLevel",
"channelType": "miot_uint8",
"type": "Number",
"refresh": true,
"actions": [
{
"command": "set_properties",
"parameterType": "NUMBER"
}
]
},
{
"property": "mode",
"siid": 2,
"piid": 4,
"friendlyName": "Fan-Mode",
"channel": "Mode",
"channelType": "miot_uint8",
"type": "Number",
"refresh": true,
"actions": [
{
"command": "set_properties",
"parameterType": "NUMBER"
}
]
},
{
"property": "horizontal-swing",
"siid": 2,
"piid": 5,
"friendlyName": "Fan-Horizontal Swing",
"channel": "HorizontalSwing",
"channelType": "miot_bool",
"type": "Switch",
"refresh": true,
"actions": [
{
"command": "set_properties",
"parameterType": "ONOFFBOOL"
}
]
},
{
"property": "horizontal-angle",
"siid": 2,
"piid": 6,
"friendlyName": "Fan-Horizontal Angle",
"channel": "HorizontalAngle",
"channelType": "miot_uint8",
"type": "Number",
"refresh": true,
"actions": [
{
"command": "set_properties",
"parameterType": "NUMBER"
}
]
},
{
"property": "alarm",
"siid": 2,
"piid": 7,
"friendlyName": "Fan-Alarm",
"channel": "Alarm",
"channelType": "miot_bool",
"type": "Switch",
"refresh": true,
"actions": [
{
"command": "set_properties",
"parameterType": "ONOFFBOOL"
}
]
},
{
"property": "off-delay-time",
"siid": 2,
"piid": 8,
"friendlyName": "Fan-Power Off Delay Time",
"channel": "OffDelayTime",
"channelType": "miot_uint8",
"type": "Number",
"refresh": true,
"actions": [
{
"command": "set_properties",
"parameterType": "NUMBER"
}
]
},
{
"property": "brightness",
"siid": 2,
"piid": 9,
"friendlyName": "Fan-Brightness",
"channel": "Brightness",
"channelType": "miot_bool",
"type": "Switch",
"refresh": true,
"actions": [
{
"command": "set_properties",
"parameterType": "ONOFFBOOL"
}
]
},
{
"property": "motor-control",
"siid": 2,
"piid": 10,
"friendlyName": "Fan-Motor Control",
"channel": "MotorControl",
"channelType": "miot_uint8",
"type": "Number",
"refresh": true,
"actions": [
{
"command": "set_properties",
"parameterType": "NUMBER"
}
]
},
{
"property": "speed-level",
"siid": 2,
"piid": 11,
"friendlyName": "Fan-Speed Level",
"channel": "SpeedLevel",
"channelType": "miot_uint8",
"type": "Number",
"refresh": true,
"actions": [
{
"command": "set_properties",
"parameterType": "NUMBER"
}
]
},
{
"property": "physical-controls-locked",
"siid": 3,
"piid": 1,
"friendlyName": "Physical Control Locked-Physical Control Locked",
"channel": "PhysicalControlsLocked",
"channelType": "miot_bool",
"type": "Switch",
"refresh": true,
"actions": [
{
"command": "set_properties",
"parameterType": "ONOFFBOOL"
}
]
}
]
}
}

View File

@@ -0,0 +1,510 @@
{
"deviceMapping": {
"id": [
"dreame.vacuum.mc1808"
],
"propertyMethod": "get_properties",
"maxProperties": 2,
"channels": [
{
"property": "",
"friendlyName": "Vacuum Action",
"channel": "vacuumaction",
"channelType": "dreameControl",
"type": "String",
"refresh": false,
"actions": [
{
"command": "action",
"parameterType": "EMPTY",
"siid": 18,
"aiid": 1,
"condition": {
"name": "matchValue",
"parameters": [
{
"matchValue": "vacuum",
"returnValue": [
{
"piid": 1,
"value": 2
}
]
},
{
"matchValue": "start",
"returnValue": [
{
"piid": 1,
"value": 2
}
]
}
]
}
},
{
"command": "action",
"parameterType": "EMPTY",
"siid": 18,
"aiid": 2,
"condition": {
"name": "matchValue",
"parameters": [
{
"matchValue": "stop"
}
]
}
},
{
"command": "action",
"parameterType": "EMPTY",
"siid": 3,
"aiid": 1,
"condition": {
"name": "matchValue",
"parameters": [
{
"matchValue": "sweep"
}
]
}
},
{
"command": "action",
"parameterType": "EMPTY",
"siid": 3,
"aiid": 2,
"condition": {
"name": "matchValue",
"parameters": [
{
"matchValue": "stopsweep"
}
]
}
},
{
"command": "action",
"parameterType": "EMPTY",
"siid": 2,
"aiid": 1,
"condition": {
"name": "matchValue",
"parameters": [
{
"matchValue": "dock"
}
]
}
}
]
},
{
"property": "battery-level",
"siid": 2,
"piid": 1,
"friendlyName": "Battery-Battery Level",
"channel": "BatteryLevel",
"channelType": "miot_uint8",
"type": "Number",
"refresh": true,
"actions": [
]
},
{
"property": "charging-state",
"siid": 2,
"piid": 2,
"friendlyName": "Battery-Charging State",
"channel": "ChargingState",
"channelType": "miot_uint8",
"type": "Number",
"refresh": true,
"actions": [
]
},
{
"property": "fault",
"siid": 3,
"piid": 1,
"friendlyName": "Robot Cleaner-Device Fault",
"channel": "Fault",
"channelType": "miot_uint8",
"type": "Number",
"refresh": true,
"actions": [
]
},
{
"property": "status",
"siid": 3,
"piid": 2,
"friendlyName": "Robot Cleaner-Status",
"channel": "Status",
"channelType": "miot_int8",
"type": "Number",
"refresh": true,
"actions": [
]
},
{
"property": "brush-left-time",
"siid": 26,
"piid": 1,
"friendlyName": "Main Cleaning Brush-Brush Left Time",
"channel": "BrushLeftTime",
"channelType": "miot_uint16",
"type": "String",
"refresh": true,
"actions": [
]
},
{
"property": "brush-life-level",
"siid": 26,
"piid": 2,
"friendlyName": "Main Cleaning Brush-Brush Life Level",
"channel": "BrushLifeLevel",
"channelType": "miot_uint8",
"type": "Number",
"refresh": true,
"actions": [
]
},
{
"property": "filter-life-level",
"siid": 27,
"piid": 1,
"friendlyName": "Filter-Filter Life Level",
"channel": "FilterLifeLevel",
"channelType": "miot_uint8",
"type": "Number",
"refresh": true,
"actions": [
]
},
{
"property": "filter-left-time",
"siid": 27,
"piid": 2,
"friendlyName": "Filter-Filter Left Time",
"channel": "FilterLeftTime",
"channelType": "miot_uint16",
"type": "String",
"refresh": true,
"actions": [
]
},
{
"property": "brush-left-time1",
"siid": 28,
"piid": 1,
"friendlyName": "Side Cleaning Brush-Brush Left Time",
"channel": "BrushLeftTime1",
"channelType": "miot_uint16",
"type": "String",
"refresh": true,
"actions": [
]
},
{
"property": "brush-life-level1",
"siid": 28,
"piid": 2,
"friendlyName": "Side Cleaning Brush-Brush Life Level",
"channel": "BrushLifeLevel1",
"channelType": "miot_uint8",
"type": "Number",
"refresh": true,
"actions": [
]
},
{
"property": "work-mode",
"siid": 18,
"piid": 1,
"friendlyName": "clean-workmode",
"channel": "WorkMode",
"channelType": "miot_int32",
"type": "Number",
"refresh": true,
"actions": [
]
},
{
"property": "area",
"siid": 18,
"piid": 4,
"friendlyName": "clean-area",
"channel": "Area",
"channelType": "miot_string",
"type": "String",
"refresh": true,
"actions": [
{
"command": "set_properties",
"parameterType": "STRING"
}
]
},
{
"property": "timer",
"siid": 18,
"piid": 5,
"friendlyName": "clean-timer",
"channel": "Timer",
"channelType": "miot_string",
"type": "String",
"refresh": true,
"actions": [
{
"command": "set_properties",
"parameterType": "STRING"
}
]
},
{
"property": "mode",
"siid": 18,
"piid": 6,
"friendlyName": "clean-mode",
"channel": "Mode",
"channelType": "miot_int32",
"type": "Number",
"refresh": true,
"actions": [
{
"command": "set_properties",
"parameterType": "NUMBER"
}
]
},
{
"property": "total-clean-time",
"siid": 18,
"piid": 13,
"friendlyName": "clean-total time",
"channel": "TotalCleanTime",
"channelType": "miot_uint32",
"type": "String",
"refresh": true,
"actions": [
]
},
{
"property": "total-clean-times",
"siid": 18,
"piid": 14,
"friendlyName": "clean-total times",
"channel": "TotalCleanTimes",
"channelType": "miot_uint32",
"type": "String",
"refresh": true,
"actions": [
]
},
{
"property": "total-clean-area",
"siid": 18,
"piid": 15,
"friendlyName": "clean-Total area",
"channel": "TotalCleanArea",
"channelType": "miot_uint32",
"type": "String",
"refresh": true,
"actions": [
]
},
{
"property": "clean-log-start-time",
"siid": 18,
"piid": 16,
"friendlyName": "clean-Start Time",
"channel": "CleanLogStartTime",
"channelType": "miot_uint32",
"type": "String",
"refresh": true,
"actions": [
]
},
{
"property": "button-led",
"siid": 18,
"piid": 17,
"friendlyName": "clean-led",
"channel": "ButtonLed",
"channelType": "miot_uint16",
"type": "String",
"refresh": true,
"actions": [
]
},
{
"property": "task-done",
"siid": 18,
"piid": 18,
"friendlyName": "clean-task done",
"channel": "TaskDone",
"channelType": "miot_uint8",
"type": "Number",
"refresh": true,
"actions": [
]
},
{
"property": "life-sieve",
"siid": 19,
"piid": 1,
"friendlyName": "consumable-life-sieve",
"channel": "LifeSieve",
"channelType": "miot_string",
"type": "String",
"refresh": true,
"actions": [
{
"command": "set_properties",
"parameterType": "STRING"
}
]
},
{
"property": "life-brush-side",
"siid": 19,
"piid": 2,
"friendlyName": "consumable-life-brush-side",
"channel": "LifeBrushSide",
"channelType": "miot_string",
"type": "String",
"refresh": true,
"actions": [
{
"command": "set_properties",
"parameterType": "STRING"
}
]
},
{
"property": "life-brush-main",
"siid": 19,
"piid": 3,
"friendlyName": "consumable-life-brush-main",
"channel": "LifeBrushMain",
"channelType": "miot_string",
"type": "String",
"refresh": true,
"actions": [
{
"command": "set_properties",
"parameterType": "STRING"
}
]
},
{
"property": "enable",
"siid": 20,
"piid": 1,
"friendlyName": "annoy-enable",
"channel": "Enable",
"channelType": "miot_bool",
"type": "Switch",
"refresh": true,
"actions": [
{
"command": "set_properties",
"parameterType": "ONOFFBOOL"
}
]
},
{
"property": "start-time",
"siid": 20,
"piid": 2,
"friendlyName": "annoy-start-time",
"channel": "StartTime",
"channelType": "miot_string",
"type": "String",
"refresh": true,
"actions": [
{
"command": "set_properties",
"parameterType": "STRING"
}
]
},
{
"property": "stop-time",
"siid": 20,
"piid": 3,
"friendlyName": "annoy-stop-time",
"channel": "StopTime",
"channelType": "miot_string",
"type": "String",
"refresh": true,
"actions": [
{
"command": "set_properties",
"parameterType": "STRING"
}
]
},
{
"property": "map-view",
"siid": 23,
"piid": 1,
"friendlyName": "map-map-view",
"channel": "MapView",
"channelType": "miot_string",
"type": "String",
"refresh": true,
"actions": [
]
},
{
"property": "volume",
"siid": 24,
"piid": 1,
"friendlyName": "audio-volume",
"channel": "Volume",
"channelType": "miot_int32",
"type": "Number",
"refresh": true,
"actions": [
{
"command": "set_properties",
"parameterType": "NUMBER"
}
]
},
{
"property": "voice-packets",
"siid": 24,
"piid": 3,
"friendlyName": "audio-voiceId",
"channel": "VoicePackets",
"channelType": "miot_string",
"type": "String",
"refresh": true,
"actions": [
{
"command": "set_properties",
"parameterType": "STRING"
}
]
},
{
"property": "time-zone",
"siid": 25,
"piid": 1,
"friendlyName": "timezone",
"channel": "TimeZone",
"channelType": "miot_string",
"type": "String",
"refresh": true,
"actions": [
]
}
]
}
}

View File

@@ -0,0 +1,121 @@
{
"deviceMapping": {
"id": [
"philips.light.bulb",
"philips.light.downlight",
"philips.light.virtual",
"philips.light.zysread",
"philips.light.zystrip",
"philips.light.hbulb"
],
"channels": [
{
"property": "power",
"friendlyName": "Power",
"channel": "power",
"type": "Switch",
"refresh": true,
"ChannelGroup": "",
"actions": [
{
"command": "set_power",
"parameterType": "ONOFF"
}
]
},
{
"property": "bright",
"friendlyName": "Brightness",
"channel": "brightness",
"type": "Dimmer",
"refresh": true,
"ChannelGroup": "actions",
"actions": [
{
"command": "set_bright",
"parameterType": "NUMBER",
"condition": {
"name": "BrightnessExisting"
}
},
{
"command": "set_power",
"parameterType": "ONOFF",
"condition": {
"name": "BrightnessOnOff"
}
}
]
},
{
"property": "cct",
"friendlyName": "Correlated Color Temperature",
"channel": "cct",
"type": "Dimmer",
"refresh": true,
"ChannelGroup": "actions",
"actions": [
{
"command": "set_cct",
"parameterType": "NUMBER"
}
]
},
{
"property": "snm",
"friendlyName": "Scene",
"channel": "scene",
"type": "Number",
"refresh": true,
"ChannelGroup": "actions",
"actions": [
{
"command": "apply_scene",
"parameterType": "NUMBER"
}
]
},
{
"property": "dv",
"friendlyName": "DV",
"channel": "dv",
"type": "Number",
"refresh": true,
"ChannelGroup": "actions",
"actions": [
{
"parameterType": "EMPTY"
}
]
},
{
"property": "",
"friendlyName": "Switch Scene",
"channel": "switchscene",
"type": "Switch",
"refresh": false,
"ChannelGroup": "actions",
"actions": [
{
"command": "switch_the_scene",
"parameterType": "EMPTY"
}
]
},
{
"property": "",
"friendlyName": "Delay Off",
"channel": "delayoff",
"type": "Switch",
"refresh": false,
"ChannelGroup": "actions",
"actions": [
{
"command": "delay_off",
"parameterType": "NUMBER"
}
]
}
]
}
}

View File

@@ -0,0 +1,104 @@
{
"deviceMapping": {
"id": [
"philips.light.candle",
"philips.light.candle2"
],
"channels": [
{
"property": "power",
"friendlyName": "Power",
"channel": "power",
"type": "Switch",
"refresh": true,
"ChannelGroup": "",
"actions": [
{
"command": "set_power",
"parameterType": "ONOFF"
}
]
},
{
"property": "bright",
"friendlyName": "Brightness",
"channel": "brightness",
"type": "Dimmer",
"refresh": true,
"ChannelGroup": "actions",
"actions": [
{
"command": "set_bright",
"parameterType": "NUMBER",
"condition": {
"name": "BrightnessExisting"
}
},
{
"command": "set_power",
"parameterType": "ONOFF",
"condition": {
"name": "BrightnessOnOff"
}
}
]
},
{
"property": "cct",
"friendlyName": "Correlated Color Temperature",
"channel": "cct",
"type": "Dimmer",
"refresh": true,
"ChannelGroup": "actions",
"actions": [
{
"command": "set_cct",
"parameterType": "NUMBER"
}
]
},
{
"property": "snm",
"friendlyName": "Scene",
"channel": "scene",
"type": "Number",
"refresh": true,
"ChannelGroup": "actions",
"actions": [
{
"command": "apply_scene",
"parameterType": "NUMBER"
}
]
},
{
"property": "",
"friendlyName": "Delay Off",
"channel": "delayoff",
"type": "Switch",
"refresh": false,
"ChannelGroup": "actions",
"actions": [
{
"command": "delay_off",
"parameterType": "NUMBER"
}
]
},
{
"property": "",
"friendlyName": "Toggle",
"channel": "toggle",
"type": "Switch",
"refresh": false,
"ChannelGroup": "actions",
"actions": [
{
"command": "toggle",
"parameterType": "EMPTY"
}
]
}
]
}
}

View File

@@ -0,0 +1,104 @@
{
"deviceMapping": {
"id": [
"philips.light.ceiling",
"philips.light.zyceiling"
],
"channels": [
{
"property": "power",
"friendlyName": "Power",
"channel": "power",
"type": "Switch",
"refresh": true,
"ChannelGroup": "",
"actions": [
{
"command": "set_power",
"parameterType": "ONOFF"
}
]
},
{
"property": "bright",
"friendlyName": "Brightness",
"channel": "brightness",
"type": "Dimmer",
"refresh": true,
"ChannelGroup": "actions",
"actions": [
{
"command": "set_bright",
"parameterType": "NUMBER",
"condition": {
"name": "BrightnessExisting"
}
},
{
"command": "set_power",
"parameterType": "ONOFF",
"condition": {
"name": "BrightnessOnOff"
}
}
]
},
{
"property": "cct",
"friendlyName": "Correlated Color Temperature",
"channel": "cct",
"type": "Dimmer",
"refresh": true,
"ChannelGroup": "actions",
"actions": [
{
"command": "set_cct",
"parameterType": "NUMBER"
}
]
},
{
"property": "snm",
"friendlyName": "Scene",
"channel": "scene",
"type": "Number",
"refresh": true,
"ChannelGroup": "actions",
"actions": [
{
"command": "apply_fixed_scene",
"parameterType": "NUMBER"
}
]
},
{
"property": "",
"friendlyName": "Switch Scene",
"channel": "switchscene",
"type": "Switch",
"refresh": false,
"ChannelGroup": "actions",
"actions": [
{
"command": "switch_the_scene",
"parameterType": "EMPTY"
}
]
},
{
"property": "",
"friendlyName": "Toggle",
"channel": "toggle",
"type": "Switch",
"refresh": false,
"ChannelGroup": "actions",
"actions": [
{
"command": "toggle",
"parameterType": "EMPTY"
}
]
}
]
}
}

View File

@@ -0,0 +1,61 @@
{
"deviceMapping": {
"id": [
"philips.light.mono1"
],
"channels": [
{
"property": "power",
"friendlyName": "Power",
"channel": "power",
"type": "Switch",
"refresh": true,
"ChannelGroup": "",
"actions": [
{
"command": "set_power",
"parameterType": "ONOFF"
}
]
},
{
"property": "bright",
"friendlyName": "Brightness",
"channel": "brightness",
"type": "Dimmer",
"refresh": true,
"ChannelGroup": "actions",
"actions": [
{
"command": "set_bright",
"parameterType": "NUMBER",
"condition": {
"name": "BrightnessExisting"
}
},
{
"command": "set_power",
"parameterType": "ONOFF",
"condition": {
"name": "BrightnessOnOff"
}
}
]
},
{
"property": "scene_num",
"friendlyName": "Scene",
"channel": "scene",
"type": "Number",
"refresh": true,
"ChannelGroup": "actions",
"actions": [
{
"command": "set_user_scene",
"parameterType": "NUMBER"
}
]
}
]
}
}

View File

@@ -0,0 +1,130 @@
{
"deviceMapping": {
"id": [
"philips.light.moonlight"
],
"channels": [
{
"property": "pow",
"friendlyName": "Power",
"channel": "power",
"type": "Switch",
"refresh": true,
"ChannelGroup": "",
"actions": [
{
"command": "set_power",
"parameterType": "ONOFF"
}
]
},
{
"property": "bright",
"friendlyName": "Brightness",
"channel": "brightness",
"type": "Dimmer",
"refresh": true,
"ChannelGroup": "actions",
"actions": [
{
"command": "set_bright",
"parameterType": "NUMBER",
"condition": {
"name": "BrightnessExisting"
}
},
{
"command": "set_power",
"parameterType": "ONOFF",
"condition": {
"name": "BrightnessOnOff"
}
}
]
},
{
"property": "cct",
"friendlyName": "Correlated Color Temperature",
"channel": "cct",
"type": "Dimmer",
"refresh": true,
"ChannelGroup": "actions",
"actions": [
{
"command": "set_cct",
"parameterType": "NUMBER"
}
]
},
{
"property": "snm",
"friendlyName": "Scene",
"channel": "scene",
"type": "Number",
"refresh": true,
"ChannelGroup": "actions",
"actions": [
{
"command": "apply_scene",
"parameterType": "NUMBER"
}
]
},
{
"property": "dv",
"friendlyName": "DV",
"channel": "dv",
"type": "Number",
"refresh": true,
"ChannelGroup": "actions",
"actions": [
{
"parameterType": "EMPTY"
}
]
},
{
"property": "",
"friendlyName": "Go Night",
"channel": "gonight",
"type": "Switch",
"refresh": false,
"ChannelGroup": "actions",
"actions": [
{
"command": "go_night",
"parameterType": "EMPTY"
}
]
},
{
"property": "",
"friendlyName": "Delay Off",
"channel": "delayoff",
"type": "Switch",
"refresh": false,
"ChannelGroup": "actions",
"actions": [
{
"command": "delay_off",
"parameterType": "NUMBER"
}
]
},
{
"property": "",
"friendlyName": "Toggle",
"channel": "toggle",
"type": "Switch",
"refresh": false,
"ChannelGroup": "actions",
"actions": [
{
"command": "toggle",
"parameterType": "EMPTY"
}
]
}
]
}
}

View File

@@ -0,0 +1,98 @@
{
"deviceMapping": {
"id": [
"philips.light.sread1"
],
"channels": [
{
"property": "power",
"friendlyName": "Power",
"channel": "power",
"type": "Switch",
"refresh": true,
"ChannelGroup": "",
"actions": [
{
"command": "set_power",
"parameterType": "ONOFF"
}
]
},
{
"property": "bright",
"friendlyName": "Brightness",
"channel": "brightness",
"type": "Dimmer",
"refresh": true,
"ChannelGroup": "actions",
"actions": [
{
"command": "set_bright",
"parameterType": "NUMBER",
"condition": {
"name": "BrightnessExisting"
}
},
{
"command": "set_power",
"parameterType": "ONOFF",
"condition": {
"name": "BrightnessOnOff"
}
}
]
},
{
"property": "ambstatus",
"friendlyName": "Ambient Power",
"channel": "ambientPower",
"type": "Switch",
"refresh": true,
"ChannelGroup": "actions",
"actions": [
{
"command": "enable_amb",
"parameterType": "ONOFF"
}
]
},
{
"property": "ambvalue",
"friendlyName": "Ambient Brightness",
"channel": "ambientBrightness",
"type": "Number",
"refresh": true,
"ChannelGroup": "actions",
"actions": [
{
"command": "set_amb_bright",
"parameterType": "NUMBER"
}
]
},
{
"property": "dvalue",
"friendlyName": "Ambient Illumination",
"channel": "illumination",
"type": "Number",
"refresh": true,
"ChannelGroup": "",
"actions": []
},
{
"property": "eyecare",
"friendlyName": "Eyecare",
"channel": "eyecare",
"type": "Switch",
"refresh": true,
"ChannelGroup": "actions",
"actions": [
{
"command": "set_eyecare",
"parameterType": "ONOFF"
}
]
}
]
}
}

View File

@@ -0,0 +1,182 @@
{
"deviceMapping": {
"id": [
"viomi.vacuum.v6",
"viomi.vacuum.v7",
"viomi.vacuum.v8"
],
"channels": [
{
"property": "",
"friendlyName": "Vacuum Action",
"channel": "vacuumaction",
"channelType": "vacuumaction",
"type": "Number",
"refresh": false,
"actions": [
{
"command": "set_mode_withroom",
"parameterType": "NUMBER",
"parameters": [
0,
"$value$",
0
]
}
]
},
{
"property": "run_state",
"friendlyName": "State",
"channel": "state",
"type": "Number",
"refresh": true,
"actions": [
]
},
{
"property": "mode",
"friendlyName": "Mode",
"channel": "mode",
"type": "Number",
"refresh": true,
"actions": [
{
"command": "set_mode",
"parameterType": "NUMBER"
}
]
},
{
"property": "err_state",
"friendlyName": "Error",
"channel": "err_state",
"type": "Number",
"refresh": true,
"actions": [
]
},
{
"property": "battery_life",
"friendlyName": "Battery",
"channel": "battery_life",
"type": "Number",
"refresh": true,
"actions": [
]
},
{
"property": "box_type",
"friendlyName": "Box type",
"channel": "box_type",
"type": "Number",
"refresh": true,
"actions": [
{
}
]
},
{
"property": "mop_type",
"friendlyName": "mop_type",
"channel": "mop_type",
"type": "Number",
"refresh": true,
"actions": [
{
"command": "set_mode",
"parameterType": "NUMBER"
}
]
},
{
"property": "s_time",
"friendlyName": "Clean time",
"channel": "s_time",
"type": "Number",
"refresh": true,
"actions": [
{
"command": "set_mode",
"parameterType": "NUMBER"
}
]
},
{
"property": "s_area",
"friendlyName": "Clean Area",
"channel": "s_area",
"type": "Number",
"refresh": true,
"actions": [
{
"command": "set_mode",
"parameterType": "NUMBER"
}
]
},
{
"property": "suction_grade",
"friendlyName": "suction_grade",
"channel": "suction_grade",
"type": "Number",
"refresh": true,
"actions": [
{
"command": "set_suction",
"parameterType": "NUMBER"
}
]
},
{
"property": "water_grade",
"friendlyName": "water_grade",
"channel": "water_grade",
"type": "Number",
"refresh": true,
"actions": [
{
"command": "set_suction",
"parameterType": "NUMBER"
}
]
},
{
"property": "remember_map",
"friendlyName": "remember_map",
"channel": "remember_map",
"type": "Number",
"refresh": true,
"actions": [
]
},
{
"property": "has_map",
"friendlyName": "has_map",
"channel": "has_map",
"type": "Number",
"refresh": true,
"actions": [
]
},
{
"property": "is_mop",
"friendlyName": "is_mop",
"channel": "is_mop",
"type": "Number",
"refresh": true,
"actions": [
]
},
{
"property": "has_newmap",
"friendlyName": "has_newmap",
"channel": "has_newmap",
"type": "Number",
"refresh": true,
"actions": [
]
}
]
}
}

View File

@@ -0,0 +1,139 @@
{
"deviceMapping": {
"id": [
"yeelink.light.ceiling1",
"yeelink.light.ceiling3",
"yeelink.light.ceiling5",
"yeelink.light.ceiling6",
"yeelink.light.ceiling7",
"yeelink.light.ceiling8",
"yeelink.light.ceiling9",
"yeelink.light.ceiling11",
"yeelink.light.ceiling12",
"yeelink.light.ceiling13"
],
"channels": [
{
"property": "power",
"friendlyName": "Power",
"channel": "power",
"type": "Switch",
"refresh": true,
"ChannelGroup": "",
"actions": [
{
"command": "set_power",
"parameterType": "ONOFF"
}
]
},
{
"property": "bright",
"friendlyName": "Brightness",
"channel": "brightness",
"type": "Dimmer",
"refresh": true,
"ChannelGroup": "actions",
"actions": [
{
"command": "set_bright",
"parameterType": "NUMBER",
"condition": {
"name": "BrightnessExisting"
}
},
{
"command": "set_power",
"parameterType": "ONOFF",
"condition": {
"name": "BrightnessOnOff"
}
}
]
},
{
"property": "delayoff",
"friendlyName": "Shutdown Timer",
"channel": "delayoff",
"type": "Number",
"refresh": true,
"ChannelGroup": "actions",
"actions": [
{
"command": "cron_add",
"parameterType": "NUMBER",
"parameters": [
0,
"$value$"
]
}
]
},
{
"property": "ct",
"friendlyName": "Color Temperature",
"channel": "colorTemperature",
"type": "Number",
"refresh": true,
"ChannelGroup": "actions",
"actions": [
{
"command": "set_ct_abx",
"parameterType": "NUMBER",
"parameters": [
"$value$",
"smooth",
500
]
}
]
},
{
"property": "color_mode",
"friendlyName": "Color Mode",
"channel": "colorMode",
"type": "Number",
"refresh": true,
"ChannelGroup": "",
"actions": []
},
{
"property": "name",
"friendlyName": "Name",
"channel": "name",
"type": "String",
"refresh": true,
"ChannelGroup": "actions",
"actions": [
{
"command": "set_name",
"parameterType": "STRING"
}
]
},
{
"property": "",
"friendlyName": "Set Scene",
"channel": "customScene",
"type": "String",
"refresh": false,
"ChannelGroup": "actions",
"actions": [
{
"command": "set_scene",
"parameterType": "CUSTOMSTRING"
}
]
},
{
"property": "nl_br",
"friendlyName": "Nightlight Brightness",
"channel": "nightlightBrightness",
"type": "Number",
"refresh": true,
"ChannelGroup": "",
"actions": []
}
]
}
}

View File

@@ -0,0 +1,130 @@
{
"deviceMapping": {
"id": [
"yeelink.light.ceiling2"
],
"channels": [
{
"property": "power",
"friendlyName": "Power",
"channel": "power",
"type": "Switch",
"refresh": true,
"ChannelGroup": "",
"actions": [
{
"command": "set_power",
"parameterType": "ONOFF"
}
]
},
{
"property": "bright",
"friendlyName": "Brightness",
"channel": "brightness",
"type": "Dimmer",
"refresh": true,
"ChannelGroup": "actions",
"actions": [
{
"command": "set_bright",
"parameterType": "NUMBER",
"condition": {
"name": "BrightnessExisting"
}
},
{
"command": "set_power",
"parameterType": "ONOFF",
"condition": {
"name": "BrightnessOnOff"
}
}
]
},
{
"property": "delayoff",
"friendlyName": "Shutdown Timer",
"channel": "delayoff",
"type": "Number",
"refresh": true,
"ChannelGroup": "actions",
"actions": [
{
"command": "cron_add",
"parameterType": "NUMBER",
"parameters": [
0,
"$value$"
]
}
]
},
{
"property": "ct",
"friendlyName": "Color Temperature",
"channel": "colorTemperature",
"type": "Number",
"refresh": true,
"ChannelGroup": "actions",
"actions": [
{
"command": "set_ct_abx",
"parameterType": "NUMBER",
"parameters": [
"$value$",
"smooth",
500
]
}
]
},
{
"property": "color_mode",
"friendlyName": "Color Mode",
"channel": "colorMode",
"type": "Number",
"refresh": true,
"ChannelGroup": "",
"actions": []
},
{
"property": "name",
"friendlyName": "Name",
"channel": "name",
"type": "String",
"refresh": true,
"ChannelGroup": "actions",
"actions": [
{
"command": "set_name",
"parameterType": "STRING"
}
]
},
{
"property": "",
"friendlyName": "Set Scene",
"channel": "customScene",
"type": "String",
"refresh": false,
"ChannelGroup": "actions",
"actions": [
{
"command": "set_scene",
"parameterType": "CUSTOMSTRING"
}
]
},
{
"property": "nl_br",
"friendlyName": "Nightlight Brightness",
"channel": "nightlightBrightness",
"type": "Number",
"refresh": true,
"ChannelGroup": "",
"actions": []
}
]
}
}

View File

@@ -0,0 +1,204 @@
{
"deviceMapping": {
"id": [
"yeelink.light.ceiling4",
"yeelink.light.ceiling10",
"yeelink.light.ceiling4.ambi"
],
"channels": [
{
"property": "power",
"friendlyName": "Power",
"channel": "power",
"type": "Switch",
"refresh": true,
"ChannelGroup": "",
"actions": [
{
"command": "set_power",
"parameterType": "ONOFF"
}
]
},
{
"property": "bright",
"friendlyName": "Brightness",
"channel": "brightness",
"type": "Dimmer",
"refresh": true,
"ChannelGroup": "actions",
"actions": [
{
"command": "set_bright",
"parameterType": "NUMBER",
"condition": {
"name": "BrightnessExisting"
}
},
{
"command": "set_power",
"parameterType": "ONOFF",
"condition": {
"name": "BrightnessOnOff"
}
}
]
},
{
"property": "bg_bright",
"friendlyName": "Ambient Brightness",
"channel": "ambientBrightness",
"type": "Number",
"refresh": true,
"ChannelGroup": "actions",
"actions": [
{
"command": "bg_set_bright",
"parameterType": "NUMBER"
}
]
},
{
"property": "delayoff",
"friendlyName": "Shutdown Timer",
"channel": "delayoff",
"type": "Number",
"refresh": true,
"ChannelGroup": "actions",
"actions": [
{
"command": "cron_add",
"parameterType": "NUMBER",
"parameters": [
0,
"$value$"
]
}
]
},
{
"property": "ct",
"friendlyName": "Color Temperature",
"channel": "colorTemperature",
"type": "Number",
"refresh": true,
"ChannelGroup": "actions",
"actions": [
{
"command": "set_ct_abx",
"parameterType": "NUMBER",
"parameters": [
"$value$",
"smooth",
500
]
}
]
},
{
"property": "color_mode",
"friendlyName": "Color Mode",
"channel": "colorMode",
"type": "Number",
"refresh": true,
"ChannelGroup": "",
"actions": []
},
{
"property": "name",
"friendlyName": "Name",
"channel": "name",
"type": "String",
"refresh": true,
"ChannelGroup": "actions",
"actions": [
{
"command": "set_name",
"parameterType": "STRING"
}
]
},
{
"property": "bg_power",
"friendlyName": "Ambient Power",
"channel": "ambientPower",
"type": "Switch",
"refresh": true,
"ChannelGroup": "actions",
"actions": [
{
"command": "bg_set_power",
"parameterType": "ONOFF"
}
]
},
{
"property": "bg_rgb",
"friendlyName": "Ambient Color",
"channel": "ambientColor",
"type": "Color",
"refresh": true,
"ChannelGroup": "actions",
"actions": [
{
"command": "bg_set_rgb",
"parameterType": "COLOR"
}
]
},
{
"property": "bg_ct",
"friendlyName": "Ambient Color Temperature",
"channel": "ambientColorTemperature",
"type": "Number",
"refresh": true,
"ChannelGroup": "actions",
"actions": [
{
"command": "bg_set_ct_abx",
"parameterType": "NUMBER",
"parameter1": "\"smooth\"",
"parameter2": "500",
"parameters": [
"$value$",
"smooth",
500
]
}
]
},
{
"property": "",
"friendlyName": "Set Scene",
"channel": "customScene",
"type": "String",
"refresh": false,
"ChannelGroup": "actions",
"actions": [
{
"command": "set_scene",
"parameterType": "CUSTOMSTRING"
}
]
},
{
"property": "bg_lmode",
"friendlyName": "Ambient Color Mode",
"channel": "ambientColorMode",
"type": "Number",
"refresh": true,
"ChannelGroup": "",
"actions": []
},
{
"property": "nl_br",
"friendlyName": "Nightlight Brightness",
"channel": "nightlightBrightness",
"type": "Number",
"refresh": true,
"ChannelGroup": "",
"actions": []
}
]
}
}

View File

@@ -0,0 +1,165 @@
{
"deviceMapping": {
"id": [
"yeelink.light.color1",
"yeelink.light.color2",
"yeelink.light.color3",
"yeelink.light.color4",
"yeelink.light.strip1",
"yeelink.light.strip2",
"yeelink.light.bslamp1",
"yeelink.light.bslamp2"
],
"maxProperties": 7,
"channels": [
{
"property": "power",
"friendlyName": "Power",
"channel": "power",
"type": "Switch",
"refresh": true,
"ChannelGroup": "",
"actions": [
{
"command": "set_power",
"parameterType": "ONOFF"
}
]
},
{
"property": "bright",
"friendlyName": "Brightness",
"channel": "brightness",
"type": "Dimmer",
"refresh": true,
"ChannelGroup": "actions",
"actions": [
{
"command": "set_bright",
"parameterType": "NUMBER",
"condition": {
"name": "BrightnessExisting"
}
},
{
"command": "set_power",
"parameterType": "ONOFF",
"condition": {
"name": "BrightnessOnOff"
}
}
]
},
{
"property": "delayoff",
"friendlyName": "Shutdown Timer",
"channel": "delayoff",
"type": "Number",
"refresh": true,
"ChannelGroup": "actions",
"actions": [
{
"command": "cron_add",
"parameterType": "NUMBER",
"parameters": [
0,
"$value$"
]
}
]
},
{
"property": "ct",
"friendlyName": "Color Temperature",
"channel": "colorTemperature",
"type": "Number",
"refresh": true,
"ChannelGroup": "actions",
"actions": [
{
"command": "set_ct_abx",
"parameterType": "NUMBER",
"parameters": [
"$value$",
"smooth",
500
]
}
]
},
{
"property": "color_mode",
"friendlyName": "Color Mode",
"channel": "colorMode",
"type": "String",
"refresh": true,
"ChannelGroup": "actions",
"actions": []
},
{
"property": "toggle",
"friendlyName": "toggle",
"channel": "toggle",
"type": "Switch",
"refresh": false,
"ChannelGroup": "actions",
"actions": [
{
"command": "toggle",
"parameterType": "EMPTY"
}
]
},
{
"property": "rgb",
"friendlyName": "RGB Color",
"channel": "rgbColor",
"type": "Color",
"refresh": true,
"ChannelGroup": "actions",
"actions": [
{
"command": "set_rgb",
"parameterType": "COLOR",
"condition": {
"name": "HSBOnly"
},
"parameters": [
"$value$",
"smooth",
500
]
},
{
"command": "set_bright",
"parameterType": "NUMBER",
"condition": {
"name": "BrightnessExisting"
}
},
{
"command": "set_power",
"parameterType": "ONOFF",
"condition": {
"name": "BrightnessOnOff"
}
}
]
},
{
"property": "name",
"friendlyName": "Name",
"channel": "name",
"type": "String",
"refresh": true,
"ChannelGroup": "actions",
"actions": [
{
"command": "set_name",
"parameterType": "STRING"
}
]
}
]
}
}

View File

@@ -0,0 +1,118 @@
{
"deviceMapping": {
"id": [
"yeelink.light.lamp1",
"yeelink.light.lamp2",
"yeelink.light.lamp3",
"yeelink.light.ct2",
"yeelink.light.mono1",
"yeelink.light.mono2",
"yeelink.light.virtual"
],
"channels": [
{
"property": "power",
"friendlyName": "Power",
"channel": "power",
"type": "Switch",
"refresh": true,
"ChannelGroup": "",
"actions": [
{
"command": "set_power",
"parameterType": "ONOFF"
}
]
},
{
"property": "bright",
"friendlyName": "Brightness",
"channel": "brightness",
"type": "Dimmer",
"refresh": true,
"ChannelGroup": "actions",
"actions": [
{
"command": "set_bright",
"parameterType": "NUMBER",
"condition": {
"name": "BrightnessExisting"
}
},
{
"command": "set_power",
"parameterType": "ONOFF",
"condition": {
"name": "BrightnessOnOff"
}
}
]
},
{
"property": "delayoff",
"friendlyName": "Shutdown Timer",
"channel": "delayoff",
"type": "Number",
"refresh": true,
"ChannelGroup": "actions",
"actions": [
{
"command": "cron_add",
"parameterType": "NUMBER",
"parameters": [
0,
"$value$"
]
}
]
},
{
"property": "ct",
"friendlyName": "Color Temperature",
"channel": "colorTemperature",
"type": "Number",
"refresh": true,
"ChannelGroup": "actions",
"actions": [
{
"command": "set_ct_abx",
"parameterType": "NUMBER",
"parameters": [
"$value$",
"smooth",
500
]
}
]
},
{
"property": "color_mode",
"friendlyName": "Color Mode",
"channel": "colorMode",
"type": "Number",
"refresh": true,
"ChannelGroup": "actions",
"actions": [
{
"command": "set_scene",
"parameterType": "NUMBER"
}
]
},
{
"property": "name",
"friendlyName": "Name",
"channel": "name",
"type": "String",
"refresh": true,
"ChannelGroup": "actions",
"actions": [
{
"command": "set_name",
"parameterType": "STRING"
}
]
}
]
}
}

View File

@@ -0,0 +1,28 @@
{
"deviceMapping": {
"id": [
"yunmi.waterpuri.v2",
"yunmi.waterpuri.lx2",
"yunmi.waterpuri.lx3",
"yunmi.waterpuri.lx4",
"yunmi.waterpurifier.v2",
"yunmi.waterpurifier.v3",
"yunmi.waterpurifier.v4"
],
"channels": [
{
"property": "power",
"friendlyName": "Power",
"channel": "power",
"type": "Switch",
"refresh": true,
"actions": [
{
"command": "set_power",
"parameterType": "ONOFF"
}
]
}
]
}
}

View File

@@ -0,0 +1,92 @@
{
"deviceMapping": {
"id": [
"zhimi.airmonitor.v1"
],
"channels": [
{
"property": "power",
"friendlyName": "Power",
"channel": "power",
"type": "Switch",
"refresh": true,
"ChannelGroup": "",
"actions": [
{
"command": "set_power",
"parameterType": "ONOFF"
}
]
},
{
"property": "aqi",
"friendlyName": "Air Quality Index",
"channel": "aqi",
"type": "Number",
"refresh": true,
"ChannelGroup": "Status",
"actions": []
},
{
"property": "battery",
"friendlyName": "Battery",
"channel": "battery",
"channelType": "system:battery-level",
"type": "Number",
"refresh": true,
"ChannelGroup": "Status",
"actions": []
},
{
"property": "usb_state",
"friendlyName": "USB State",
"channel": "usb_state",
"type": "Switch",
"refresh": true,
"ChannelGroup": "Status",
"actions": []
},
{
"property": "time_state",
"friendlyName": "Time State",
"channel": "time_state",
"type": "Switch",
"refresh": true,
"ChannelGroup": "Status",
"actions": []
},
{
"property": "night_state",
"friendlyName": "Night State",
"channel": "night_state",
"type": "Switch",
"refresh": true,
"ChannelGroup": "",
"actions": [
{
"command": "set_night_state",
"parameterType": "ONOFF"
}
]
},
{
"property": "night_beg_time",
"friendlyName": "Night Begin Time",
"channel": "night_begin",
"type": "Number",
"refresh": true,
"ChannelGroup": "Status",
"actions": []
},
{
"property": "night_end_time",
"friendlyName": "Night End Time",
"channel": "night_end",
"type": "Number",
"refresh": true,
"ChannelGroup": "Status",
"actions": []
}
]
}
}

View File

@@ -0,0 +1,196 @@
{
"deviceMapping": {
"id": [
"zhimi.airpurifier.m1",
"zhimi.airpurifier.m2",
"zhimi.airpurifier.v1",
"zhimi.airpurifier.v2",
"zhimi.airpurifier.v3",
"zhimi.airpurifier.v5",
"zhimi.airpurifier.ma1",
"zhimi.airpurifier.sa1",
"zhimi.airpurifier.sa2",
"zhimi.airpurifier.mb1",
"zhimi.airpurifier.mc1",
"zhimi.airpurifier.mc2"
],
"channels": [
{
"property": "power",
"friendlyName": "Power",
"channel": "power",
"type": "Switch",
"refresh": true,
"actions": [
{
"command": "set_power",
"parameterType": "ONOFF"
}
]
},
{
"property": "mode",
"friendlyName": "Mode",
"channel": "mode",
"type": "String",
"refresh": true,
"ChannelGroup": "actions",
"actions": [
{
"command": "set_mode",
"parameterType": "STRING"
}
]
},
{
"property": "humidity",
"friendlyName": "Humidity",
"channel": "humidity",
"type": "Number",
"refresh": true,
"ChannelGroup": "Status",
"actions": []
},
{
"property": "aqi",
"friendlyName": "Air Quality Index",
"channel": "aqi",
"type": "Number",
"refresh": true,
"ChannelGroup": "Status",
"actions": []
},
{
"property": "average_aqi",
"friendlyName": "Average Air Quality Index",
"channel": "averageaqi",
"type": "Number",
"refresh": true,
"ChannelGroup": "Status",
"actions": []
},
{
"property": "led",
"friendlyName": "LED Status",
"channel": "led",
"type": "Switch",
"refresh": true,
"ChannelGroup": "actions",
"actions": [
{
"command": "set_led",
"parameterType": "ONOFF"
}
]
},
{
"property": "buzzer",
"friendlyName": "Buzzer Status",
"channel": "buzzer",
"type": "Switch",
"refresh": true,
"ChannelGroup": "actions",
"actions": [
{
"command": "set_buzzer",
"parameterType": "ONOFF"
}
]
},
{
"property": "f1_hour",
"friendlyName": "Filter Max Life",
"channel": "filtermaxlife",
"type": "Number",
"refresh": true,
"ChannelGroup": "Status",
"actions": []
},
{
"property": "f1_hour_used",
"friendlyName": "Filter Hours used",
"channel": "filterhours",
"type": "Number",
"refresh": true,
"transformation": "SecondsToHours",
"ChannelGroup": "Status",
"actions": []
},
{
"property": "use_time",
"friendlyName": "Run Time",
"channel": "usedhours",
"type": "Number",
"refresh": true,
"transformation": "SecondsToHours",
"ChannelGroup": "Status",
"actions": []
},
{
"property": "motor1_speed",
"friendlyName": "Motor Speed",
"channel": "motorspeed",
"type": "Number",
"refresh": true,
"ChannelGroup": "Status",
"actions": []
},
{
"property": "filter1_life",
"friendlyName": "Filter Life",
"channel": "filterlife",
"type": "Number",
"refresh": true,
"ChannelGroup": "Status",
"actions": []
},
{
"property": "favorite_level",
"friendlyName": "Favorite Level",
"channel": "favoritelevel",
"type": "Number",
"refresh": true,
"ChannelGroup": "Status",
"actions": [
{
"command": "set_level_favorite",
"parameterType": "NUMBER"
}
]
},
{
"property": "temp_dec",
"friendlyName": "Temperature",
"channel": "temperature",
"type": "Number",
"refresh": true,
"transformation": "/10",
"ChannelGroup": "Status",
"actions": []
},
{
"property": "purify_volume",
"friendlyName": "Purivied Volume",
"channel": "purifyvolume",
"type": "Number",
"refresh": true,
"ChannelGroup": "Status",
"actions": []
},
{
"property": "child_lock",
"friendlyName": "Child Lock",
"channel": "childlock",
"type": "Switch",
"refresh": true,
"ChannelGroup": "Status",
"actions": [
{
"command": "set_child_lock",
"parameterType": "ONOFF"
}
]
}
]
}
}

View File

@@ -0,0 +1,715 @@
{
"deviceMapping": {
"id": [
"zhimi.airpurifier.ma4"
],
"propertyMethod": "get_properties",
"maxProperties": 2,
"channels": [
{
"property": "fault",
"siid": 2,
"piid": 1,
"friendlyName": "Air Purifier-Device Fault",
"channel": "Fault",
"channelType": "miot_uint8",
"type": "Number",
"refresh": true,
"actions": []
},
{
"property": "on",
"siid": 2,
"piid": 2,
"friendlyName": "Air Purifier-Switch Status",
"channel": "On",
"channelType": "miot_bool",
"type": "Switch",
"refresh": true,
"actions": [
{
"command": "set_properties",
"parameterType": "ONOFFBOOL"
}
]
},
{
"property": "fan-level",
"siid": 2,
"piid": 4,
"friendlyName": "Air Purifier-Fan Level",
"channel": "FanLevel",
"channelType": "miot_uint8",
"type": "Number",
"refresh": true,
"actions": [
{
"command": "set_properties",
"parameterType": "NUMBER"
}
]
},
{
"property": "mode",
"siid": 2,
"piid": 5,
"friendlyName": "Air Purifier-Mode",
"channel": "Mode",
"channelType": "miot_uint8",
"type": "Number",
"refresh": true,
"actions": [
{
"command": "set_properties",
"parameterType": "NUMBER"
}
]
},
{
"property": "firmware-revision",
"siid": 1,
"piid": 4,
"friendlyName": "Device Information-Current Firmware Version",
"channel": "FirmwareRevision",
"channelType": "miot_string",
"type": "String",
"refresh": true,
"actions": []
},
{
"property": "manufacturer",
"siid": 1,
"piid": 1,
"friendlyName": "Device Information-Device Manufacturer",
"channel": "Manufacturer",
"channelType": "miot_string",
"type": "String",
"refresh": true,
"actions": []
},
{
"property": "model",
"siid": 1,
"piid": 2,
"friendlyName": "Device Information-Device Model",
"channel": "Model",
"channelType": "miot_string",
"type": "String",
"refresh": true,
"actions": []
},
{
"property": "serial-number",
"siid": 1,
"piid": 3,
"friendlyName": "Device Information-Device Serial Number",
"channel": "SerialNumber",
"channelType": "miot_string",
"type": "String",
"refresh": true,
"actions": []
},
{
"property": "pm2.5-density",
"siid": 3,
"piid": 6,
"friendlyName": "Environment-PM2.5 Density",
"channel": "Pm25Density",
"channelType": "miot_float",
"type": "Number",
"refresh": true,
"actions": []
},
{
"property": "relative-humidity",
"siid": 3,
"piid": 7,
"friendlyName": "Environment-Relative Humidity",
"channel": "RelativeHumidity",
"channelType": "miot_uint8",
"type": "Number",
"refresh": true,
"actions": []
},
{
"property": "temperature",
"siid": 3,
"piid": 8,
"friendlyName": "Environment-Temperature",
"channel": "Temperature",
"channelType": "miot_float",
"type": "Number",
"refresh": true,
"actions": []
},
{
"property": "filter-life-level",
"siid": 4,
"piid": 3,
"friendlyName": "Filter-Filter Life Level",
"channel": "FilterLifeLevel",
"channelType": "miot_uint8",
"type": "Number",
"refresh": true,
"actions": []
},
{
"property": "filter-used-time",
"siid": 4,
"piid": 5,
"friendlyName": "Filter-Filter Used Time",
"channel": "FilterUsedTime",
"channelType": "miot_uint16",
"type": "String",
"refresh": true,
"actions": []
},
{
"property": "alarm",
"siid": 5,
"piid": 1,
"friendlyName": "Alarm-Alarm",
"channel": "Alarm",
"channelType": "miot_bool",
"type": "Switch",
"refresh": true,
"actions": [
{
"command": "set_properties",
"parameterType": "ONOFFBOOL"
}
]
},
{
"property": "brightness",
"siid": 6,
"piid": 1,
"friendlyName": "Indicator Light-Brightness",
"channel": "Brightness",
"channelType": "miot_uint8",
"type": "Number",
"refresh": true,
"actions": [
{
"command": "set_properties",
"parameterType": "NUMBER"
}
]
},
{
"property": "on1",
"siid": 6,
"piid": 6,
"friendlyName": "Indicator Light-Switch Status",
"channel": "On1",
"channelType": "miot_bool",
"type": "Switch",
"refresh": true,
"actions": [
{
"command": "set_properties",
"parameterType": "ONOFFBOOL"
}
]
},
{
"property": "physical-controls-locked",
"siid": 7,
"piid": 1,
"friendlyName": "Physical Control Locked-Physical Control Locked",
"channel": "PhysicalControlsLocked",
"channelType": "miot_bool",
"type": "Switch",
"refresh": true,
"actions": [
{
"command": "set_properties",
"parameterType": "ONOFFBOOL"
}
]
},
{
"property": "button-pressed",
"siid": 8,
"piid": 1,
"friendlyName": "button-button_pressed",
"channel": "ButtonPressed",
"channelType": "miot_string",
"type": "String",
"refresh": true,
"actions": []
},
{
"property": "filter-max-time",
"siid": 9,
"piid": 1,
"friendlyName": "filter-time-filter-max-time",
"channel": "FilterMaxTime",
"channelType": "miot_int32",
"type": "Number",
"refresh": true,
"actions": [
{
"command": "set_properties",
"parameterType": "NUMBER"
}
]
},
{
"property": "filter-hour-used-debug",
"siid": 9,
"piid": 2,
"friendlyName": "filter-time-filter-hour-used-debug",
"channel": "FilterHourUsedDebug",
"channelType": "miot_int32",
"type": "Number",
"refresh": true,
"actions": [
{
"command": "set_properties",
"parameterType": "NUMBER"
}
]
},
{
"property": "m1-strong",
"siid": 10,
"piid": 1,
"friendlyName": "motor-speed-m1-strong",
"channel": "M1Strong",
"channelType": "miot_int32",
"type": "Number",
"refresh": true,
"actions": [
{
"command": "set_properties",
"parameterType": "NUMBER"
}
]
},
{
"property": "m1-high",
"siid": 10,
"piid": 2,
"friendlyName": "motor-speed-m1-high",
"channel": "M1High",
"channelType": "miot_int32",
"type": "Number",
"refresh": true,
"actions": [
{
"command": "set_properties",
"parameterType": "NUMBER"
}
]
},
{
"property": "m1-med",
"siid": 10,
"piid": 3,
"friendlyName": "motor-speed-m1-med",
"channel": "M1Med",
"channelType": "miot_int32",
"type": "Number",
"refresh": true,
"actions": [
{
"command": "set_properties",
"parameterType": "NUMBER"
}
]
},
{
"property": "m1-med-l",
"siid": 10,
"piid": 4,
"friendlyName": "motor-speed-m1-med-l",
"channel": "M1MedL",
"channelType": "miot_int32",
"type": "Number",
"refresh": true,
"actions": [
{
"command": "set_properties",
"parameterType": "NUMBER"
}
]
},
{
"property": "m1-low",
"siid": 10,
"piid": 5,
"friendlyName": "motor-speed-m1-low",
"channel": "M1Low",
"channelType": "miot_int32",
"type": "Number",
"refresh": true,
"actions": [
{
"command": "set_properties",
"parameterType": "NUMBER"
}
]
},
{
"property": "m1-silent",
"siid": 10,
"piid": 6,
"friendlyName": "motor-speed-m1-silent",
"channel": "M1Silent",
"channelType": "miot_int32",
"type": "Number",
"refresh": true,
"actions": [
{
"command": "set_properties",
"parameterType": "NUMBER"
}
]
},
{
"property": "m1-favorite",
"siid": 10,
"piid": 7,
"friendlyName": "motor-speed-m1-favorite",
"channel": "M1Favorite",
"channelType": "miot_int32",
"type": "Number",
"refresh": true,
"actions": [
{
"command": "set_properties",
"parameterType": "NUMBER"
}
]
},
{
"property": "motor1-speed",
"siid": 10,
"piid": 8,
"friendlyName": "motor-speed-motor1-speed",
"channel": "Motor1Speed",
"channelType": "miot_int32",
"type": "Number",
"refresh": true,
"actions": []
},
{
"property": "motor1-set-speed",
"siid": 10,
"piid": 9,
"friendlyName": "motor-speed-motor1-set-speed",
"channel": "Motor1SetSpeed",
"channelType": "miot_int32",
"type": "Number",
"refresh": true,
"actions": [
{
"command": "set_properties",
"parameterType": "NUMBER"
}
]
},
{
"property": "favorite-fan-level",
"siid": 10,
"piid": 10,
"friendlyName": "motor-speed-favorite fan level",
"channel": "FavoriteFanLevel",
"channelType": "miot_int32",
"type": "Number",
"refresh": true,
"actions": [
{
"command": "set_properties",
"parameterType": "NUMBER"
}
]
},
{
"property": "use-time",
"siid": 12,
"piid": 1,
"friendlyName": "use-time-use-time",
"channel": "UseTime",
"channelType": "miot_int32",
"type": "Number",
"refresh": true,
"actions": []
},
{
"property": "purify-volume",
"siid": 13,
"piid": 1,
"friendlyName": "aqi-purify-volume",
"channel": "PurifyVolume",
"channelType": "miot_int32",
"type": "Number",
"refresh": true,
"actions": []
},
{
"property": "average-aqi",
"siid": 13,
"piid": 2,
"friendlyName": "aqi-average-aqi",
"channel": "AverageAqi",
"channelType": "miot_int32",
"type": "Number",
"refresh": true,
"actions": []
},
{
"property": "average-aqi-cnt",
"siid": 13,
"piid": 3,
"friendlyName": "aqi-average-aqi-cnt",
"channel": "AverageAqiCnt",
"channelType": "miot_int32",
"type": "Number",
"refresh": true,
"actions": []
},
{
"property": "aqi-zone",
"siid": 13,
"piid": 4,
"friendlyName": "aqi-aqi-zone",
"channel": "AqiZone",
"channelType": "miot_string",
"type": "String",
"refresh": true,
"actions": []
},
{
"property": "sensor-state",
"siid": 13,
"piid": 5,
"friendlyName": "aqi-sensor-state",
"channel": "SensorState",
"channelType": "miot_string",
"type": "String",
"refresh": true,
"actions": []
},
{
"property": "aqi-goodh",
"siid": 13,
"piid": 6,
"friendlyName": "aqi-aqi-goodh",
"channel": "AqiGoodh",
"channelType": "miot_int32",
"type": "Number",
"refresh": true,
"actions": [
{
"command": "set_properties",
"parameterType": "NUMBER"
}
]
},
{
"property": "aqi-runstate",
"siid": 13,
"piid": 7,
"friendlyName": "aqi-aqi-runstate",
"channel": "AqiRunstate",
"channelType": "miot_int32",
"type": "Number",
"refresh": true,
"actions": []
},
{
"property": "aqi-state",
"siid": 13,
"piid": 8,
"friendlyName": "aqi-aqi-state",
"channel": "AqiState",
"channelType": "miot_int32",
"type": "Number",
"refresh": true,
"actions": []
},
{
"property": "aqi-updata-heartbeat",
"siid": 13,
"piid": 9,
"friendlyName": "aqi-aqi-updata-heartbeat",
"channel": "AqiUpdataHeartbeat",
"channelType": "miot_int32",
"type": "Number",
"refresh": true,
"actions": [
{
"command": "set_properties",
"parameterType": "NUMBER"
}
]
},
{
"property": "rfid-tag",
"siid": 14,
"piid": 1,
"friendlyName": "rfid-rfid-tag",
"channel": "RfidTag",
"channelType": "miot_string",
"type": "String",
"refresh": true,
"actions": []
},
{
"property": "rfid-factory-id",
"siid": 14,
"piid": 2,
"friendlyName": "rfid-rfid-factory-id",
"channel": "RfidFactoryId",
"channelType": "miot_string",
"type": "String",
"refresh": true,
"actions": []
},
{
"property": "rfid-product-id",
"siid": 14,
"piid": 3,
"friendlyName": "rfid-rfid-product-id",
"channel": "RfidProductId",
"channelType": "miot_string",
"type": "String",
"refresh": true,
"actions": []
},
{
"property": "rfid-time",
"siid": 14,
"piid": 4,
"friendlyName": "rfid-rfid-time",
"channel": "RfidTime",
"channelType": "miot_string",
"type": "String",
"refresh": true,
"actions": []
},
{
"property": "rfid-serial-num",
"siid": 14,
"piid": 5,
"friendlyName": "rfid-rfid-serial-num",
"channel": "RfidSerialNum",
"channelType": "miot_string",
"type": "String",
"refresh": true,
"actions": []
},
{
"property": "app-extra",
"siid": 15,
"piid": 1,
"friendlyName": "others-app-extra",
"channel": "AppExtra",
"channelType": "miot_int32",
"type": "Number",
"refresh": true,
"actions": [
{
"command": "set_properties",
"parameterType": "NUMBER"
}
]
},
{
"property": "main-channel",
"siid": 15,
"piid": 2,
"friendlyName": "others-main-channel",
"channel": "MainChannel",
"channelType": "miot_int32",
"type": "Number",
"refresh": true,
"actions": []
},
{
"property": "slave-channel",
"siid": 15,
"piid": 3,
"friendlyName": "others-slave-channel",
"channel": "SlaveChannel",
"channelType": "miot_int32",
"type": "Number",
"refresh": true,
"actions": []
},
{
"property": "cola",
"siid": 15,
"piid": 4,
"friendlyName": "others-cola",
"channel": "Cola",
"channelType": "miot_string",
"type": "String",
"refresh": true,
"actions": [
{
"command": "set_properties",
"parameterType": "STRING"
}
]
},
{
"property": "buttom-door",
"siid": 15,
"piid": 5,
"friendlyName": "others-buttom-door",
"channel": "ButtomDoor",
"channelType": "miot_bool",
"type": "Switch",
"refresh": true,
"actions": []
},
{
"property": "reboot-cause",
"siid": 15,
"piid": 6,
"friendlyName": "others-reboot_cause",
"channel": "RebootCause",
"channelType": "miot_int32",
"type": "Number",
"refresh": true,
"actions": []
},
{
"property": "hw-version",
"siid": 15,
"piid": 8,
"friendlyName": "others-hw-version",
"channel": "HwVersion",
"channelType": "miot_int32",
"type": "Number",
"refresh": true,
"actions": []
},
{
"property": "i2c-error-count",
"siid": 15,
"piid": 9,
"friendlyName": "others-i2c-error-count",
"channel": "I2cErrorCount",
"channelType": "miot_int32",
"type": "Number",
"refresh": true,
"actions": []
},
{
"property": "manual-level",
"siid": 15,
"piid": 10,
"friendlyName": "others-manual-level",
"channel": "ManualLevel",
"channelType": "miot_int32",
"type": "Number",
"refresh": true,
"actions": []
}
]
}
}

View File

@@ -0,0 +1,736 @@
{
"deviceMapping": {
"id": [
"zhimi.airpurifier.mb3"
],
"propertyMethod": "get_properties",
"maxProperties": 2,
"channels": [
{
"property": "fault",
"siid": 2,
"piid": 1,
"friendlyName": "Air Purifier-fault",
"channel": "Fault",
"channelType": "miot_uint8",
"type": "Number",
"refresh": true,
"actions": []
},
{
"property": "on",
"siid": 2,
"piid": 2,
"friendlyName": "Air Purifier-Switch Status",
"channel": "On",
"channelType": "miot_bool",
"type": "Switch",
"refresh": true,
"actions": [
{
"command": "set_properties",
"parameterType": "ONOFFBOOL"
}
]
},
{
"property": "fan-level",
"siid": 2,
"piid": 4,
"friendlyName": "Air Purifier-Fan Level",
"channel": "FanLevel",
"channelType": "miot_uint8",
"type": "Number",
"refresh": true,
"actions": [
{
"command": "set_properties",
"parameterType": "NUMBER"
}
]
},
{
"property": "mode",
"siid": 2,
"piid": 5,
"friendlyName": "Air Purifier-Mode",
"channel": "Mode",
"channelType": "miot_uint8",
"type": "Number",
"refresh": true,
"actions": [
{
"command": "set_properties",
"parameterType": "NUMBER"
}
]
},
{
"property": "firmware-revision",
"siid": 1,
"piid": 4,
"friendlyName": "Device Information-Current Firmware Version",
"channel": "FirmwareRevision",
"channelType": "miot_string",
"type": "String",
"refresh": true,
"actions": []
},
{
"property": "manufacturer",
"siid": 1,
"piid": 1,
"friendlyName": "Device Information-Device Manufacturer",
"channel": "Manufacturer",
"channelType": "miot_string",
"type": "String",
"refresh": true,
"actions": []
},
{
"property": "model",
"siid": 1,
"piid": 2,
"friendlyName": "Device Information-Device Model",
"channel": "Model",
"channelType": "miot_string",
"type": "String",
"refresh": true,
"actions": []
},
{
"property": "serial-number",
"siid": 1,
"piid": 3,
"friendlyName": "Device Information-Device Serial Number",
"channel": "SerialNumber",
"channelType": "miot_string",
"type": "String",
"refresh": true,
"actions": []
},
{
"property": "pm2.5-density",
"siid": 3,
"piid": 6,
"friendlyName": "Environment-PM2.5",
"channel": "Pm25Density",
"channelType": "miot_float",
"type": "Number",
"refresh": true,
"actions": []
},
{
"property": "relative-humidity",
"siid": 3,
"piid": 7,
"friendlyName": "Environment-Relative Humidity",
"channel": "RelativeHumidity",
"channelType": "miot_uint8",
"type": "Number",
"refresh": true,
"actions": []
},
{
"property": "temperature",
"siid": 3,
"piid": 8,
"friendlyName": "Environment-Temperature",
"channel": "Temperature",
"channelType": "miot_float",
"type": "Number",
"refresh": true,
"actions": []
},
{
"property": "filter-life-level",
"siid": 4,
"piid": 3,
"friendlyName": "Filter-Filter Life Level",
"channel": "FilterLifeLevel",
"channelType": "miot_uint8",
"type": "Number",
"refresh": true,
"actions": []
},
{
"property": "filter-used-time",
"siid": 4,
"piid": 5,
"friendlyName": "Filter-Filter Used Time",
"channel": "FilterUsedTime",
"channelType": "miot_uint16",
"type": "String",
"refresh": true,
"actions": []
},
{
"property": "alarm",
"siid": 5,
"piid": 1,
"friendlyName": "Alarm-Alarm",
"channel": "Alarm",
"channelType": "miot_bool",
"type": "Switch",
"refresh": true,
"actions": [
{
"command": "set_properties",
"parameterType": "ONOFFBOOL"
}
]
},
{
"property": "brightness",
"siid": 6,
"piid": 1,
"friendlyName": "Indicator Light-brightness",
"channel": "Brightness",
"channelType": "miot_uint8",
"type": "Number",
"refresh": true,
"actions": [
{
"command": "set_properties",
"parameterType": "NUMBER"
}
]
},
{
"property": "on1",
"siid": 6,
"piid": 6,
"friendlyName": "Indicator Light-Switch Status",
"channel": "On1",
"channelType": "miot_bool",
"type": "Switch",
"refresh": true,
"actions": [
{
"command": "set_properties",
"parameterType": "ONOFFBOOL"
}
]
},
{
"property": "physical-controls-locked",
"siid": 7,
"piid": 1,
"friendlyName": "Physical Control Locked-Physical Control Locked",
"channel": "PhysicalControlsLocked",
"channelType": "miot_bool",
"type": "Switch",
"refresh": true,
"actions": [
{
"command": "set_properties",
"parameterType": "ONOFFBOOL"
}
]
},
{
"property": "button-pressed",
"siid": 8,
"piid": 1,
"friendlyName": "Button-button-pressed",
"channel": "ButtonPressed",
"channelType": "miot_string",
"type": "String",
"refresh": true,
"actions": []
},
{
"property": "filter-max-time",
"siid": 9,
"piid": 1,
"friendlyName": "filter-time-filter-max-time",
"channel": "FilterMaxTime",
"channelType": "miot_int32",
"type": "Number",
"refresh": true,
"actions": [
{
"command": "set_properties",
"parameterType": "NUMBER"
}
]
},
{
"property": "filter-hour-debug",
"siid": 9,
"piid": 2,
"friendlyName": "filter-time-filter-hour-debug",
"channel": "FilterHourDebug",
"channelType": "miot_int32",
"type": "Number",
"refresh": true,
"actions": [
{
"command": "set_properties",
"parameterType": "NUMBER"
}
]
},
{
"property": "motor-strong",
"siid": 10,
"piid": 1,
"friendlyName": "motor-speed-motor-strong",
"channel": "MotorStrong",
"channelType": "miot_int32",
"type": "Number",
"refresh": true,
"actions": [
{
"command": "set_properties",
"parameterType": "NUMBER"
}
]
},
{
"property": "motor-high",
"siid": 10,
"piid": 2,
"friendlyName": "motor-speed-motor-high",
"channel": "MotorHigh",
"channelType": "miot_int32",
"type": "Number",
"refresh": true,
"actions": [
{
"command": "set_properties",
"parameterType": "NUMBER"
}
]
},
{
"property": "motor-med",
"siid": 10,
"piid": 3,
"friendlyName": "motor-speed-motor-med",
"channel": "MotorMed",
"channelType": "miot_int32",
"type": "Number",
"refresh": true,
"actions": [
{
"command": "set_properties",
"parameterType": "NUMBER"
}
]
},
{
"property": "motor-med-l",
"siid": 10,
"piid": 4,
"friendlyName": "motor-speed-motor-med-l",
"channel": "MotorMedL",
"channelType": "miot_int32",
"type": "Number",
"refresh": true,
"actions": [
{
"command": "set_properties",
"parameterType": "NUMBER"
}
]
},
{
"property": "motor-low",
"siid": 10,
"piid": 5,
"friendlyName": "motor-speed-motor-low",
"channel": "MotorLow",
"channelType": "miot_int32",
"type": "Number",
"refresh": true,
"actions": [
{
"command": "set_properties",
"parameterType": "NUMBER"
}
]
},
{
"property": "motor-silent",
"siid": 10,
"piid": 6,
"friendlyName": "motor-speed-motor-silent",
"channel": "MotorSilent",
"channelType": "miot_int32",
"type": "Number",
"refresh": true,
"actions": [
{
"command": "set_properties",
"parameterType": "NUMBER"
}
]
},
{
"property": "motor-favorite",
"siid": 10,
"piid": 7,
"friendlyName": "motor-speed-motor-favorite",
"channel": "MotorFavorite",
"channelType": "miot_int32",
"type": "Number",
"refresh": true,
"actions": [
{
"command": "set_properties",
"parameterType": "NUMBER"
}
]
},
{
"property": "motor-speed",
"siid": 10,
"piid": 8,
"friendlyName": "motor-speed-motor-speed",
"channel": "MotorSpeed",
"channelType": "miot_int32",
"type": "Number",
"refresh": true,
"actions": []
},
{
"property": "motor-set-speed",
"siid": 10,
"piid": 9,
"friendlyName": "motor-speed-motor-set-speed",
"channel": "MotorSetSpeed",
"channelType": "miot_int32",
"type": "Number",
"refresh": true,
"actions": []
},
{
"property": "favorite-fan-level",
"siid": 10,
"piid": 10,
"friendlyName": "motor-speed-favorite-fan-level",
"channel": "FavoriteFanLevel",
"channelType": "miot_int32",
"type": "Number",
"refresh": true,
"actions": [
{
"command": "set_properties",
"parameterType": "NUMBER"
}
]
},
{
"property": "use-time",
"siid": 12,
"piid": 1,
"friendlyName": "use-time-use-time",
"channel": "UseTime",
"channelType": "miot_int32",
"type": "Number",
"refresh": true,
"actions": []
},
{
"property": "purify-volume",
"siid": 13,
"piid": 1,
"friendlyName": "aqi-purify-volume",
"channel": "PurifyVolume",
"channelType": "miot_int32",
"type": "Number",
"refresh": true,
"actions": []
},
{
"property": "average-aqi",
"siid": 13,
"piid": 2,
"friendlyName": "aqi-average-aqi",
"channel": "AverageAqi",
"channelType": "miot_int32",
"type": "Number",
"refresh": true,
"actions": []
},
{
"property": "average-aqi-cnt",
"siid": 13,
"piid": 3,
"friendlyName": "aqi-average-aqi-cnt",
"channel": "AverageAqiCnt",
"channelType": "miot_int32",
"type": "Number",
"refresh": true,
"actions": []
},
{
"property": "aqi-zone",
"siid": 13,
"piid": 4,
"friendlyName": "aqi-aqi-zone",
"channel": "AqiZone",
"channelType": "miot_string",
"type": "String",
"refresh": true,
"actions": []
},
{
"property": "sensor-state",
"siid": 13,
"piid": 5,
"friendlyName": "aqi-sensor-state",
"channel": "SensorState",
"channelType": "miot_string",
"type": "String",
"refresh": true,
"actions": []
},
{
"property": "aqi-goodh",
"siid": 13,
"piid": 6,
"friendlyName": "aqi-aqi-goodh",
"channel": "AqiGoodh",
"channelType": "miot_int32",
"type": "Number",
"refresh": true,
"actions": [
{
"command": "set_properties",
"parameterType": "NUMBER"
}
]
},
{
"property": "aqi-runstate",
"siid": 13,
"piid": 7,
"friendlyName": "aqi-aqi-runstate",
"channel": "AqiRunstate",
"channelType": "miot_int32",
"type": "Number",
"refresh": true,
"actions": []
},
{
"property": "aqi-state",
"siid": 13,
"piid": 8,
"friendlyName": "aqi-aqi-state",
"channel": "AqiState",
"channelType": "miot_int32",
"type": "Number",
"refresh": true,
"actions": []
},
{
"property": "aqi-updata-heartbeat",
"siid": 13,
"piid": 9,
"friendlyName": "aqi-aqi-updata-heartbeat",
"channel": "AqiUpdataHeartbeat",
"channelType": "miot_int32",
"type": "Number",
"refresh": true,
"actions": [
{
"command": "set_properties",
"parameterType": "NUMBER"
}
]
},
{
"property": "rfid-tag",
"siid": 14,
"piid": 1,
"friendlyName": "rfid-rfid-tag",
"channel": "RfidTag",
"channelType": "miot_string",
"type": "String",
"refresh": true,
"actions": []
},
{
"property": "rfid-factory-id",
"siid": 14,
"piid": 2,
"friendlyName": "rfid-rfid-factory-id",
"channel": "RfidFactoryId",
"channelType": "miot_string",
"type": "String",
"refresh": true,
"actions": []
},
{
"property": "rfid-product-id",
"siid": 14,
"piid": 3,
"friendlyName": "rfid-rfid-product-id",
"channel": "RfidProductId",
"channelType": "miot_string",
"type": "String",
"refresh": true,
"actions": []
},
{
"property": "rfid-time",
"siid": 14,
"piid": 4,
"friendlyName": "rfid-rfid-time",
"channel": "RfidTime",
"channelType": "miot_string",
"type": "String",
"refresh": true,
"actions": []
},
{
"property": "rfid-serial-num",
"siid": 14,
"piid": 5,
"friendlyName": "rfid-rfid-serial-num",
"channel": "RfidSerialNum",
"channelType": "miot_string",
"type": "String",
"refresh": true,
"actions": []
},
{
"property": "app-extra",
"siid": 15,
"piid": 1,
"friendlyName": "others-app-extra",
"channel": "AppExtra",
"channelType": "miot_int32",
"type": "Number",
"refresh": true,
"actions": [
{
"command": "set_properties",
"parameterType": "NUMBER"
}
]
},
{
"property": "main-channel",
"siid": 15,
"piid": 2,
"friendlyName": "others-main-channel",
"channel": "MainChannel",
"channelType": "miot_int32",
"type": "Number",
"refresh": true,
"actions": [
{
"command": "set_properties",
"parameterType": "NUMBER"
}
]
},
{
"property": "slave-channel",
"siid": 15,
"piid": 3,
"friendlyName": "others-slave-channel",
"channel": "SlaveChannel",
"channelType": "miot_int32",
"type": "Number",
"refresh": true,
"actions": [
{
"command": "set_properties",
"parameterType": "NUMBER"
}
]
},
{
"property": "cola",
"siid": 15,
"piid": 4,
"friendlyName": "others-cola",
"channel": "Cola",
"channelType": "miot_string",
"type": "String",
"refresh": true,
"actions": [
{
"command": "set_properties",
"parameterType": "STRING"
}
]
},
{
"property": "buttom-door",
"siid": 15,
"piid": 5,
"friendlyName": "others-buttom-door",
"channel": "ButtomDoor",
"channelType": "miot_bool",
"type": "Switch",
"refresh": true,
"actions": []
},
{
"property": "reboot-cause",
"siid": 15,
"piid": 6,
"friendlyName": "others-reboot-cause",
"channel": "RebootCause",
"channelType": "miot_int32",
"type": "Number",
"refresh": true,
"actions": []
},
{
"property": "hw-version",
"siid": 15,
"piid": 8,
"friendlyName": "others-hw-version",
"channel": "HwVersion",
"channelType": "miot_int32",
"type": "Number",
"refresh": true,
"actions": []
},
{
"property": "iic-error-count",
"siid": 15,
"piid": 9,
"friendlyName": "others-iic-error-count",
"channel": "IicErrorCount",
"channelType": "miot_int32",
"type": "Number",
"refresh": true,
"actions": []
},
{
"property": "manual-level",
"siid": 15,
"piid": 10,
"friendlyName": "others-manual-level",
"channel": "ManualLevel",
"channelType": "miot_int32",
"type": "Number",
"refresh": true,
"actions": []
},
{
"property": "country-code",
"siid": 15,
"piid": 11,
"friendlyName": "others-National code",
"channel": "CountryCode",
"channelType": "miot_int32",
"type": "Number",
"refresh": true,
"actions": [
{
"command": "set_properties",
"parameterType": "NUMBER"
}
]
}
]
}
}

View File

@@ -0,0 +1,123 @@
{
"deviceMapping": {
"id": [
"zhimi.airpurifier.v1"
],
"channels": [
{
"property": "power",
"friendlyName": "Power",
"channel": "power",
"type": "Switch",
"refresh": true,
"actions": [
{
"command": "set_power",
"parameterType": "ONOFF"
}
]
},
{
"property": "mode",
"friendlyName": "Mode",
"channel": "mode",
"type": "String",
"refresh": true,
"ChannelGroup": "actions",
"actions": [
{
"command": "set_mode",
"parameterType": "STRING"
}
]
},
{
"property": "humidity",
"friendlyName": "Humidity",
"channel": "humidity",
"type": "Number",
"refresh": true,
"ChannelGroup": "Status",
"actions": []
},
{
"property": "aqi",
"friendlyName": "Air Quality Index",
"channel": "aqi",
"type": "Number",
"refresh": true,
"ChannelGroup": "Status",
"actions": []
},
{
"property": "bright",
"friendlyName": "Brightness",
"channel": "brightness",
"type": "Number",
"refresh": true,
"ChannelGroup": "actions",
"actions": [
{
"command": "set_led_b",
"parameterType": "NUMBER"
}
]
},
{
"property": "led",
"friendlyName": "LED Status",
"channel": "led",
"type": "Switch",
"refresh": true,
"ChannelGroup": "actions",
"actions": [
{
"command": "set_led",
"parameterType": "ONOFF"
}
]
},
{
"property": "act_det",
"friendlyName": "Air AutoDetect",
"channel": "act_det",
"type": "Switch",
"refresh": true,
"ChannelGroup": "actions",
"actions": []
},
{
"property": "buzzer",
"friendlyName": "Buzzer Status",
"channel": "buzzer",
"type": "Switch",
"refresh": true,
"ChannelGroup": "actions",
"actions": [
{
"command": "set_buzzer",
"parameterType": "ONOFF"
}
]
},
{
"property": "f1_hour",
"friendlyName": "Filter Max Life",
"channel": "filtermaxlife",
"type": "Number",
"refresh": true,
"ChannelGroup": "Status",
"actions": []
},
{
"property": "filter1_life",
"friendlyName": "Filter Life",
"channel": "filterlive",
"type": "Number",
"refresh": true,
"ChannelGroup": "Status",
"actions": []
}
]
}
}

View File

@@ -0,0 +1,186 @@
{
"deviceMapping": {
"id": [
"zhimi.airpurifier.v6",
"zhimi.airpurifier.ma2"
],
"channels": [
{
"property": "power",
"friendlyName": "Power",
"channel": "power",
"type": "Switch",
"refresh": true,
"actions": [
{
"command": "set_power",
"parameterType": "ONOFF"
}
]
},
{
"property": "mode",
"friendlyName": "Mode",
"channel": "mode",
"type": "String",
"refresh": true,
"ChannelGroup": "actions",
"actions": [
{
"command": "set_mode",
"parameterType": "STRING"
}
]
},
{
"property": "humidity",
"friendlyName": "Humidity",
"channel": "humidity",
"type": "Number",
"refresh": true,
"ChannelGroup": "Status",
"actions": []
},
{
"property": "aqi",
"friendlyName": "Air Quality Index",
"channel": "aqi",
"type": "Number",
"refresh": true,
"ChannelGroup": "Status",
"actions": []
},
{
"property": "average_aqi",
"friendlyName": "Average Air Quality Index",
"channel": "averageaqi",
"type": "Number",
"refresh": true,
"ChannelGroup": "Status",
"actions": []
},
{
"property": "led",
"friendlyName": "LED Status",
"channel": "led",
"type": "Switch",
"refresh": true,
"ChannelGroup": "actions",
"actions": [
{
"command": "set_led",
"parameterType": "ONOFF"
}
]
},
{
"property": "bright",
"friendlyName": "LED Brightness",
"channel": "bright",
"type": "Number",
"refresh": true,
"ChannelGroup": "actions",
"actions": [
{
"command": "set_led_b",
"parameterType": "NUMBER"
}
]
},
{
"property": "f1_hour",
"friendlyName": "Filter Max Life",
"channel": "filtermaxlife",
"type": "Number",
"refresh": true,
"ChannelGroup": "Status",
"actions": []
},
{
"property": "f1_hour_used",
"friendlyName": "Filter Hours used",
"channel": "filterhours",
"type": "Number",
"refresh": true,
"transformation": "SecondsToHours",
"ChannelGroup": "Status",
"actions": []
},
{
"property": "use_time",
"friendlyName": "Run Time",
"channel": "usedhours",
"type": "Number",
"refresh": true,
"transformation": "SecondsToHours",
"ChannelGroup": "Status",
"actions": []
},
{
"property": "motor1_speed",
"friendlyName": "Motor Speed",
"channel": "motorspeed",
"type": "Number",
"refresh": true,
"ChannelGroup": "Status",
"actions": []
},
{
"property": "filter1_life",
"friendlyName": "Filter Life",
"channel": "filterlife",
"type": "Number",
"refresh": true,
"ChannelGroup": "Status",
"actions": []
},
{
"property": "favorite_level",
"friendlyName": "Favorite Level",
"channel": "favoritelevel",
"type": "Number",
"refresh": true,
"ChannelGroup": "Status",
"actions": [
{
"command": "set_level_favorite",
"parameterType": "NUMBER"
}
]
},
{
"property": "temp_dec",
"friendlyName": "Temperature",
"channel": "temperature",
"type": "Number",
"refresh": true,
"transformation": "/10",
"ChannelGroup": "Status",
"actions": []
},
{
"property": "purify_volume",
"friendlyName": "Purivied Volume",
"channel": "purifyvolume",
"type": "Number",
"refresh": true,
"ChannelGroup": "Status",
"actions": []
},
{
"property": "child_lock",
"friendlyName": "Child Lock",
"channel": "childlock",
"type": "Switch",
"refresh": true,
"ChannelGroup": "Status",
"actions": [
{
"command": "set_child_lock",
"parameterType": "ONOFF"
}
]
}
]
}
}

View File

@@ -0,0 +1,179 @@
{
"deviceMapping": {
"id": [
"zhimi.airpurifier.v7"
],
"channels": [
{
"property": "power",
"friendlyName": "Power",
"channel": "power",
"type": "Switch",
"refresh": true,
"actions": [
{
"command": "set_power",
"parameterType": "ONOFF"
}
]
},
{
"property": "mode",
"friendlyName": "Mode",
"channel": "mode",
"type": "String",
"refresh": true,
"ChannelGroup": "actions",
"actions": [
{
"command": "set_mode",
"parameterType": "STRING"
}
]
},
{
"property": "humidity",
"friendlyName": "Humidity",
"channel": "humidity",
"type": "Number",
"refresh": true,
"ChannelGroup": "Status",
"actions": []
},
{
"property": "aqi",
"friendlyName": "Air Quality Index",
"channel": "aqi",
"type": "Number",
"refresh": true,
"ChannelGroup": "Status",
"actions": []
},
{
"property": "average_aqi",
"friendlyName": "Average Air Quality Index",
"channel": "averageaqi",
"type": "Number",
"refresh": true,
"ChannelGroup": "Status",
"actions": []
},
{
"property": "volume",
"friendlyName": "Volume",
"channel": "volume",
"type": "Number",
"refresh": true,
"ChannelGroup": "Status",
"actions": []
},
{
"property": "led",
"friendlyName": "LED Status",
"channel": "led",
"type": "Switch",
"refresh": true,
"ChannelGroup": "actions",
"actions": [
{
"command": "set_led",
"parameterType": "ONOFF"
}
]
},
{
"property": "bright",
"friendlyName": "Illuminance",
"channel": "illuminance",
"type": "Number",
"refresh": true,
"ChannelGroup": "actions",
"actions": []
},
{
"property": "f1_hour",
"friendlyName": "Filter Max Life",
"channel": "filtermaxlife",
"type": "Number",
"refresh": true,
"ChannelGroup": "Status",
"actions": []
},
{
"property": "f1_hour_used",
"friendlyName": "Filter Hours used",
"channel": "filterhours",
"type": "Number",
"refresh": true,
"transformation": "SecondsToHours",
"ChannelGroup": "Status",
"actions": []
},
{
"property": "motor1_speed",
"friendlyName": "Motor Speed",
"channel": "motorspeed",
"type": "Number",
"refresh": true,
"ChannelGroup": "Status",
"actions": []
},
{
"property": "motor2_speed",
"friendlyName": "Motor Speed 2",
"channel": "motorspeed2",
"type": "Number",
"refresh": true,
"ChannelGroup": "Status",
"actions": []
},
{
"property": "filter1_life",
"friendlyName": "Filter Life",
"channel": "filterlife",
"type": "Number",
"refresh": true,
"ChannelGroup": "Status",
"actions": []
},
{
"property": "favorite_level",
"friendlyName": "Favorite Level",
"channel": "favoritelevel",
"type": "Number",
"refresh": true,
"ChannelGroup": "Status",
"actions": [
{
"command": "set_level_favorite",
"parameterType": "NUMBER"
}
]
},
{
"property": "temp_dec",
"friendlyName": "Temperature",
"channel": "temperature",
"type": "Number",
"refresh": true,
"transformation": "/10",
"ChannelGroup": "Status",
"actions": []
},
{
"property": "child_lock",
"friendlyName": "Child Lock",
"channel": "childlock",
"type": "Switch",
"refresh": true,
"ChannelGroup": "Status",
"actions": [
{
"command": "set_child_lock",
"parameterType": "ONOFF"
}
]
}
]
}
}

View File

@@ -0,0 +1,181 @@
{
"deviceMapping": {
"id": [
"zhimi.fan.sa1",
"zhimi.fan.za1"
],
"channels": [
{
"property": "power",
"friendlyName": "Power",
"channel": "power",
"type": "Switch",
"refresh": true,
"actions": [
{
"command": "set_power",
"parameterType": "ONOFF"
}
]
},
{
"property": "angle_enable",
"friendlyName": "Rotation",
"channel": "angleEnable",
"type": "Switch",
"refresh": true,
"actions": [
{
"command": "set_angle_enable",
"parameterType": "ONOFF"
}
]
},
{
"property": "use_time",
"friendlyName": "Run Time",
"channel": "usedhours",
"type": "Number",
"refresh": true,
"transformation": "SecondsToHours",
"ChannelGroup": "Status",
"actions": []
},
{
"property": "angle",
"friendlyName": "Angle",
"channel": "angle",
"type": "Number",
"refresh": true,
"ChannelGroup": "actions",
"actions": [
{
"command": "set_angle",
"parameterType": "NUMBER"
}
]
},
{
"property": "poweroff_time",
"friendlyName": "Timer",
"channel": "poweroffTime",
"type": "Number",
"refresh": true,
"ChannelGroup": "actions",
"actions": [
{
"command": "set_poweroff_time",
"parameterType": "NUMBER"
}
]
},
{
"property": "buzzer",
"friendlyName": "Buzzer",
"channel": "buzzer",
"type": "Switch",
"refresh": true,
"ChannelGroup": "actions",
"actions": [
{
"command": "set_buzzer",
"parameterType": "ONOFF"
}
]
},
{
"property": "led_b",
"friendlyName": "LED",
"channel": "led_b",
"type": "Number",
"refresh": true,
"ChannelGroup": "actions",
"actions": [
{
"command": "set_led_b",
"parameterType": "NUMBER"
}
]
},
{
"property": "child_lock",
"friendlyName": "Child Lock",
"channel": "child_lock",
"type": "Switch",
"refresh": true,
"ChannelGroup": "actions",
"actions": [
{
"command": "set_child_lock",
"parameterType": "ONOFF"
}
]
},
{
"property": "speed_level",
"friendlyName": "Speed Level",
"channel": "speedLevel",
"type": "Number",
"refresh": true,
"ChannelGroup": "actions",
"actions": [
{
"command": "set_speed_level",
"parameterType": "NUMBER"
}
]
},
{
"property": "speed",
"friendlyName": "Speed",
"channel": "speed",
"type": "Number",
"refresh": true,
"ChannelGroup": "actions",
"actions": [
{
"command": "set_speed",
"parameterType": "NUMBER"
}
]
},
{
"property": "natural_level",
"friendlyName": "Natural Level",
"channel": "naturalLevel",
"type": "Number",
"refresh": true,
"ChannelGroup": "actions",
"actions": [
{
"command": "set_natural_level",
"parameterType": "NUMBER"
}
]
},
{
"property": "ac_power",
"friendlyName": "AC Power",
"channel": "acPower",
"type": "Switch",
"refresh": true,
"ChannelGroup": "Status",
"actions": []
},
{
"property": "",
"friendlyName": "Move Direction",
"channel": "move",
"type": "String",
"refresh": true,
"ChannelGroup": "actions",
"actions": [
{
"command": "set_move",
"parameterType": "STRING"
}
]
}
]
}
}

View File

@@ -0,0 +1,220 @@
{
"deviceMapping": {
"id": [
"zhimi.fan.v1",
"zhimi.fan.v2",
"zhimi.fan.v3"
],
"channels": [
{
"property": "power",
"friendlyName": "Power",
"channel": "power",
"type": "Switch",
"refresh": true,
"actions": [
{
"command": "set_power",
"parameterType": "ONOFF"
}
]
},
{
"property": "angle_enable",
"friendlyName": "Rotation",
"channel": "angleEnable",
"type": "Switch",
"refresh": true,
"actions": [
{
"command": "set_angle_enable",
"parameterType": "ONOFF"
}
]
},
{
"property": "use_time",
"friendlyName": "Run Time",
"channel": "usedhours",
"type": "Number",
"refresh": true,
"transformation": "SecondsToHours",
"ChannelGroup": "Status",
"actions": []
},
{
"property": "angle",
"friendlyName": "Angle",
"channel": "angle",
"type": "Number",
"refresh": true,
"ChannelGroup": "actions",
"actions": [
{
"command": "set_angle",
"parameterType": "NUMBER"
}
]
},
{
"property": "poweroff_time",
"friendlyName": "Timer",
"channel": "poweroffTime",
"type": "Number",
"refresh": true,
"ChannelGroup": "actions",
"actions": [
{
"command": "set_poweroff_time",
"parameterType": "NUMBER"
}
]
},
{
"property": "buzzer",
"friendlyName": "Buzzer",
"channel": "buzzer",
"type": "Switch",
"refresh": true,
"ChannelGroup": "actions",
"actions": [
{
"command": "set_buzzer",
"parameterType": "ONOFF"
}
]
},
{
"property": "led_b",
"friendlyName": "LED",
"channel": "led_b",
"type": "Number",
"refresh": true,
"ChannelGroup": "actions",
"actions": [
{
"command": "set_led_b",
"parameterType": "NUMBER"
}
]
},
{
"property": "child_lock",
"friendlyName": "Child Lock",
"channel": "child_lock",
"type": "Switch",
"refresh": true,
"ChannelGroup": "actions",
"actions": [
{
"command": "set_child_lock",
"parameterType": "ONOFF"
}
]
},
{
"property": "speed_level",
"friendlyName": "Speed Level",
"channel": "speedLevel",
"type": "Number",
"refresh": true,
"ChannelGroup": "actions",
"actions": [
{
"command": "set_speed_level",
"parameterType": "NUMBER"
}
]
},
{
"property": "speed",
"friendlyName": "Speed",
"channel": "speed",
"type": "Number",
"refresh": true,
"ChannelGroup": "actions",
"actions": [
{
"command": "set_speed",
"parameterType": "NUMBER"
}
]
},
{
"property": "natural_level",
"friendlyName": "Natural Level",
"channel": "naturalLevel",
"type": "Number",
"refresh": true,
"ChannelGroup": "actions",
"actions": [
{
"command": "set_natural_level",
"parameterType": "NUMBER"
}
]
},
{
"property": "temp_dec",
"friendlyName": "Temperature",
"channel": "temp_dec",
"type": "Number",
"refresh": true,
"transformation": "/10",
"ChannelGroup": "Status",
"actions": []
},
{
"property": "humidity",
"friendlyName": "Humidity",
"channel": "humidity",
"type": "Number",
"refresh": true,
"ChannelGroup": "Status",
"actions": []
},
{
"property": "ac_power",
"friendlyName": "AC Power",
"channel": "acPower",
"type": "String",
"refresh": true,
"ChannelGroup": "Status",
"actions": []
},
{
"property": "bat_charge",
"friendlyName": "Battery Charge",
"channel": "mode",
"type": "String",
"refresh": true,
"ChannelGroup": "actions",
"actions": []
},
{
"property": "battery",
"friendlyName": "Battery",
"channel": "battery",
"channelType": "system:battery-level",
"type": "Number",
"refresh": true,
"ChannelGroup": "Status",
"actions": []
},
{
"property": "",
"friendlyName": "Move Direction",
"channel": "move",
"type": "String",
"refresh": true,
"ChannelGroup": "actions",
"actions": [
{
"command": "set_move",
"parameterType": "STRING"
}
]
}
]
}
}

View File

@@ -0,0 +1,171 @@
{
"deviceMapping": {
"id": [
"zhimi.fan.za4"
],
"channels": [
{
"property": "power",
"friendlyName": "Power",
"channel": "power",
"type": "Switch",
"refresh": true,
"actions": [
{
"command": "set_power",
"parameterType": "ONOFF"
}
]
},
{
"property": "angle_enable",
"friendlyName": "Rotation",
"channel": "angleEnable",
"type": "Switch",
"refresh": true,
"actions": [
{
"command": "set_angle_enable",
"parameterType": "ONOFF"
}
]
},
{
"property": "use_time",
"friendlyName": "Run Time",
"channel": "usedhours",
"type": "Number",
"refresh": true,
"transformation": "SecondsToHours",
"ChannelGroup": "Status",
"actions": []
},
{
"property": "angle",
"friendlyName": "Angle",
"channel": "angle",
"type": "Number",
"refresh": true,
"ChannelGroup": "actions",
"actions": [
{
"command": "set_angle",
"parameterType": "NUMBER"
}
]
},
{
"property": "poweroff_time",
"friendlyName": "Timer",
"channel": "poweroffTime",
"type": "Number",
"refresh": true,
"ChannelGroup": "actions",
"actions": [
{
"command": "set_poweroff_time",
"parameterType": "NUMBER"
}
]
},
{
"property": "buzzer",
"friendlyName": "Buzzer",
"channel": "buzzer",
"type": "Number",
"refresh": true,
"ChannelGroup": "actions",
"actions": [
{
"command": "set_buzzer",
"parameterType": "NUMBER"
}
]
},
{
"property": "led_b",
"friendlyName": "LED",
"channel": "led_b",
"type": "Number",
"refresh": true,
"ChannelGroup": "actions",
"actions": [
{
"command": "set_led_b",
"parameterType": "NUMBER"
}
]
},
{
"property": "child_lock",
"friendlyName": "Child Lock",
"channel": "child_lock",
"type": "Switch",
"refresh": true,
"ChannelGroup": "actions",
"actions": [
{
"command": "set_child_lock",
"parameterType": "ONOFF"
}
]
},
{
"property": "speed_level",
"friendlyName": "Speed Level",
"channel": "speedLevel",
"type": "Number",
"refresh": true,
"ChannelGroup": "actions",
"actions": [
{
"command": "set_speed_level",
"parameterType": "NUMBER"
}
]
},
{
"property": "speed",
"friendlyName": "Speed",
"channel": "speed",
"type": "Number",
"refresh": true,
"ChannelGroup": "actions",
"actions": [
{
"command": "set_speed",
"parameterType": "NUMBER"
}
]
},
{
"property": "natural_level",
"friendlyName": "Natural Level",
"channel": "naturalLevel",
"type": "Number",
"refresh": true,
"ChannelGroup": "actions",
"actions": [
{
"command": "set_natural_level",
"parameterType": "NUMBER"
}
]
},
{
"property": "",
"friendlyName": "Move Direction",
"channel": "move",
"type": "String",
"refresh": true,
"ChannelGroup": "actions",
"actions": [
{
"command": "set_move",
"parameterType": "STRING"
}
]
}
]
}
}

View File

@@ -0,0 +1,152 @@
{
"deviceMapping": {
"id": [
"zhimi.humidifier.cb1"
],
"channels": [
{
"property": "power",
"friendlyName": "Power",
"channel": "power",
"type": "Switch",
"refresh": true,
"actions": [
{
"command": "set_power",
"parameterType": "ONOFF"
}
]
},
{
"property": "mode",
"friendlyName": "Humidifier Mode",
"channel": "humidifierMode",
"type": "String",
"refresh": true,
"ChannelGroup": "actions",
"actions": [
{
"command": "set_mode",
"parameterType": "STRING"
}
]
},
{
"property": "humidity",
"friendlyName": "Humidity",
"channel": "humidity",
"type": "Number",
"refresh": true,
"ChannelGroup": "Status",
"actions": []
},
{
"property": "limit_hum",
"friendlyName": "Humidity Set",
"channel": "setHumidity",
"type": "Number",
"refresh": true,
"ChannelGroup": "Status",
"actions": [
{
"command": "set_limit_hum",
"parameterType": "NUMBER"
}
]
},
{
"property": "led_b",
"friendlyName": "LED Brightness",
"channel": "bright",
"type": "Number",
"refresh": true,
"ChannelGroup": "actions",
"actions": [
{
"command": "set_led_b",
"parameterType": "NUMBER"
}
]
},
{
"property": "buzzer",
"friendlyName": "Buzzer Status",
"channel": "buzzer",
"type": "Switch",
"refresh": true,
"ChannelGroup": "actions",
"actions": [
{
"command": "set_buzzer",
"parameterType": "ONOFF"
}
]
},
{
"property": "depth",
"friendlyName": "Depth",
"channel": "depth",
"type": "Number",
"refresh": true,
"ChannelGroup": "Status",
"actions": []
},
{
"property": "dry",
"friendlyName": "Dry",
"channel": "dry",
"type": "Switch",
"refresh": true,
"ChannelGroup": "Status",
"actions": [
{
"command": "set_dry",
"parameterType": "ONOFF"
}
]
},
{
"property": "use_time",
"friendlyName": "Run Time",
"channel": "usedhours",
"type": "Number",
"refresh": true,
"transformation": "SecondsToHours",
"ChannelGroup": "Status",
"actions": []
},
{
"property": "speed",
"friendlyName": "Motor Speed",
"channel": "motorspeed",
"type": "Number",
"refresh": true,
"ChannelGroup": "Status",
"actions": []
},
{
"property": "temperature",
"friendlyName": "Temperature",
"channel": "temperature",
"type": "Number",
"refresh": true,
"ChannelGroup": "Status",
"actions": []
},
{
"property": "child_lock",
"friendlyName": "Child Lock",
"channel": "childlock",
"type": "Switch",
"refresh": true,
"ChannelGroup": "Status",
"actions": [
{
"command": "set_child_lock",
"parameterType": "ONOFF"
}
]
}
]
}
}

View File

@@ -0,0 +1,172 @@
{
"deviceMapping": {
"id": [
"zhimi.humidifier.v1",
"zhimi.humidifier.ca1"
],
"channels": [
{
"property": "power",
"friendlyName": "Power",
"channel": "power",
"type": "Switch",
"refresh": true,
"actions": [
{
"command": "set_power",
"parameterType": "ONOFF"
}
]
},
{
"property": "mode",
"friendlyName": "Mode",
"channel": "mode",
"type": "String",
"refresh": true,
"ChannelGroup": "actions",
"actions": [
{
"command": "set_mode",
"parameterType": "STRING"
}
]
},
{
"property": "humidity",
"friendlyName": "Humidity",
"channel": "humidity",
"type": "Number",
"refresh": true,
"ChannelGroup": "Status",
"actions": []
},
{
"property": "limit_hum",
"friendlyName": "Humidity Set",
"channel": "setHumidity",
"type": "Number",
"refresh": true,
"ChannelGroup": "Status",
"actions": [
{
"command": "set_limit_hum",
"parameterType": "NUMBER"
}
]
},
{
"property": "aqi",
"friendlyName": "Air Quality Index",
"channel": "aqi",
"type": "Number",
"refresh": true,
"ChannelGroup": "Status",
"actions": []
},
{
"property": "trans_level",
"friendlyName": "Trans_level",
"channel": "translevel",
"type": "Number",
"refresh": true,
"ChannelGroup": "Status",
"actions": []
},
{
"property": "led_b",
"friendlyName": "LED Brightness",
"channel": "bright",
"type": "Number",
"refresh": true,
"ChannelGroup": "actions",
"actions": [
{
"command": "set_led_b",
"parameterType": "NUMBER"
}
]
},
{
"property": "buzzer",
"friendlyName": "Buzzer Status",
"channel": "buzzer",
"type": "Switch",
"refresh": true,
"ChannelGroup": "actions",
"actions": [
{
"command": "set_buzzer",
"parameterType": "ONOFF"
}
]
},
{
"property": "depth",
"friendlyName": "Depth",
"channel": "depth",
"type": "Number",
"refresh": true,
"ChannelGroup": "Status",
"actions": []
},
{
"property": "dry",
"friendlyName": "Dry",
"channel": "dry",
"type": "Switch",
"refresh": true,
"ChannelGroup": "Status",
"actions": [
{
"command": "set_dry",
"parameterType": "ONOFF"
}
]
},
{
"property": "use_time",
"friendlyName": "Run Time",
"channel": "usedhours",
"type": "Number",
"refresh": true,
"transformation": "SecondsToHours",
"ChannelGroup": "Status",
"actions": []
},
{
"property": "speed",
"friendlyName": "Motor Speed",
"channel": "motorspeed",
"type": "Number",
"refresh": true,
"ChannelGroup": "Status",
"actions": []
},
{
"property": "temp_dec",
"friendlyName": "Temperature",
"channel": "temperature",
"type": "Number",
"refresh": true,
"transformation": "/10",
"ChannelGroup": "Status",
"actions": []
},
{
"property": "child_lock",
"friendlyName": "Child Lock",
"channel": "childlock",
"type": "Switch",
"refresh": true,
"ChannelGroup": "Status",
"actions": [
{
"command": "set_child_lock",
"parameterType": "ONOFF"
}
]
}
]
}
}

View File

@@ -0,0 +1,83 @@
{
"deviceMapping": {
"id": [
"qmi.powerstrip.v1",
"zimi.powerstrip.v2"
],
"channels": [
{
"property": "power",
"friendlyName": "Power",
"channel": "power",
"type": "Switch",
"refresh": true,
"ChannelGroup": "",
"actions": [
{
"command": "set_power",
"parameterType": "ONOFF"
}
]
},
{
"property": "power_consume_rate",
"friendlyName": "Power Consumption",
"channel": "powerUsage",
"type": "Number",
"refresh": true,
"ChannelGroup": "",
"actions": [
{
"parameterType": "EMPTY"
}
]
},
{
"property": "wifi_led",
"friendlyName": "wifi LED",
"channel": "led",
"type": "Switch",
"refresh": true,
"ChannelGroup": "",
"actions": [
{
"command": "set_wifi_led",
"parameterType": "ONOFF"
}
]
},
{
"property": "power_price",
"friendlyName": "power_price",
"channel": "power_price",
"type": "Number",
"refresh": true,
"ChannelGroup": "",
"actions": [
{
"command": "set_power_price",
"parameterType": "NUMBER"
}
]
},
{
"property": "current",
"friendlyName": "Current",
"channel": "current",
"type": "Number",
"refresh": true,
"ChannelGroup": "",
"actions": []
},
{
"property": "temperature",
"friendlyName": "Temperature",
"channel": "temperature",
"type": "Number",
"refresh": true,
"ChannelGroup": "",
"actions": []
}
]
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

View File

@@ -0,0 +1,71 @@
/**
* Copyright (c) 2010-2020 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.Assert.*;
import java.util.Collections;
import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.Test;
import org.openhab.binding.miio.internal.basic.ActionConditions;
import org.openhab.binding.miio.internal.basic.MiIoDeviceActionCondition;
import com.google.gson.JsonElement;
import com.google.gson.JsonPrimitive;
/**
* Test case for {@link ActionConditions}
*
* @author Marcel Verpaalen - Initial contribution
*
*/
@NonNullByDefault
public class ActionConditionTest {
@Test
public void assertBrightnessExisting() {
MiIoDeviceActionCondition condition = new MiIoDeviceActionCondition();
condition.setName("BrightnessExisting");
Map<String, Object> deviceVariables = Collections.emptyMap();
JsonElement value = new JsonPrimitive(1);
JsonElement resp = ActionConditions.executeAction(condition, deviceVariables, value, null);
// dimmed to 1
assertNotNull(resp);
assertEquals(new JsonPrimitive(1), resp);
// fully on
value = new JsonPrimitive(100);
resp = ActionConditions.executeAction(condition, deviceVariables, value, null);
assertNotNull(resp);
assertEquals(new JsonPrimitive(100), resp);
// >100
value = new JsonPrimitive(200);
resp = ActionConditions.executeAction(condition, deviceVariables, value, null);
assertNotNull(resp);
assertEquals(new JsonPrimitive(100), resp);
// switched off = invalid brightness
value = new JsonPrimitive(0);
resp = ActionConditions.executeAction(condition, deviceVariables, value, null);
assertNull(resp);
assertNotEquals(new JsonPrimitive(0), resp);
value = new JsonPrimitive(-1);
resp = ActionConditions.executeAction(condition, deviceVariables, value, null);
assertNull(resp);
assertNotEquals(new JsonPrimitive(-1), resp);
}
}

Some files were not shown because too many files have changed in this diff Show More