added migrated 2.x add-ons
Signed-off-by: Kai Kreuzer <kai@openhab.org>
This commit is contained in:
@@ -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>
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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/";
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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 + ")";
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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() + "]";
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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() : "") + "]";
|
||||
}
|
||||
}
|
||||
@@ -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() + "]";
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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": []
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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": []
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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": [
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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": [
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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": []
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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": []
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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": []
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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": []
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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": []
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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": []
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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 |
@@ -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
Reference in New Issue
Block a user