forked from HomeAssistant/midea-meiju-codec
feat: add transparent protocol.
This commit is contained in:
@@ -41,7 +41,7 @@ from .const import (
|
||||
CONF_SN8,
|
||||
CONF_SN,
|
||||
CONF_MODEL_NUMBER,
|
||||
CONF_SERVERS
|
||||
CONF_SERVERS, STORAGE_PATH, CONF_MANUFACTURER_CODE
|
||||
)
|
||||
# 账号型:登录云端、获取设备列表,并为每台设备建立协调器(无本地控制)
|
||||
from .const import CONF_PASSWORD as CONF_PASSWORD_KEY, CONF_SERVER as CONF_SERVER_KEY
|
||||
@@ -179,11 +179,21 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry):
|
||||
# 为每台设备构建占位设备与协调器(不连接本地)
|
||||
for appliance_code, info in appliances.items():
|
||||
MideaLogger.debug(f"info={info} ")
|
||||
|
||||
os.makedirs(hass.config.path(STORAGE_PATH), exist_ok=True)
|
||||
path = hass.config.path(STORAGE_PATH)
|
||||
file = await cloud.download_lua(
|
||||
path=path,
|
||||
device_type=info.get(CONF_TYPE),
|
||||
sn=info.get(CONF_SN),
|
||||
model_number=info.get(CONF_MODEL_NUMBER),
|
||||
manufacturer_code=info.get(CONF_MANUFACTURER_CODE),
|
||||
)
|
||||
try:
|
||||
device = MiedaDevice(
|
||||
name=info.get(CONF_NAME) or info.get("name"),
|
||||
name=info.get(CONF_NAME),
|
||||
device_id=appliance_code,
|
||||
device_type=info.get(CONF_TYPE) or info.get("type"),
|
||||
device_type=info.get(CONF_TYPE),
|
||||
ip_address=None,
|
||||
port=None,
|
||||
token=None,
|
||||
@@ -192,8 +202,10 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry):
|
||||
protocol=info.get(CONF_PROTOCOL) or 2,
|
||||
model=info.get(CONF_MODEL),
|
||||
subtype=info.get(CONF_MODEL_NUMBER),
|
||||
sn=info.get(CONF_SN) or info.get("sn"),
|
||||
sn8=info.get(CONF_SN8) or info.get("sn8"),
|
||||
sn=info.get(CONF_SN),
|
||||
sn8=info.get(CONF_SN8),
|
||||
lua_file=file,
|
||||
cloud=cloud,
|
||||
)
|
||||
# 加载并应用设备映射(queries/centralized/calculate),并预置 attributes 键
|
||||
try:
|
||||
@@ -289,6 +301,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry):
|
||||
bucket["coordinator_map"][appliance_code] = coordinator
|
||||
except Exception as e:
|
||||
MideaLogger.error(f"Init device failed: {appliance_code}, error: {e}")
|
||||
# break
|
||||
hass.data[DOMAIN]["accounts"][config_entry.entry_id] = bucket
|
||||
|
||||
except Exception as e:
|
||||
|
@@ -256,7 +256,6 @@ class MideaClimateEntity(MideaEntity, ClimateEntity):
|
||||
if dict_config is None:
|
||||
return None
|
||||
|
||||
MideaLogger.debug(f"dict_config={dict_config}, rationale={rationale}, self.device_attributes={self.device_attributes} ")
|
||||
for key, config in dict_config.items():
|
||||
if isinstance(config, dict):
|
||||
# Check if all conditions match
|
||||
|
File diff suppressed because one or more lines are too long
@@ -3,10 +3,13 @@ import time
|
||||
import datetime
|
||||
import json
|
||||
import base64
|
||||
import asyncio
|
||||
import requests
|
||||
from aiohttp import ClientSession
|
||||
from secrets import token_hex
|
||||
from .logger import MideaLogger
|
||||
from .security import CloudSecurity, MeijuCloudSecurity, MSmartCloudSecurity
|
||||
from .util import bytes_to_dec_string
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -100,6 +103,46 @@ class MideaCloud:
|
||||
|
||||
return None
|
||||
|
||||
def _api_request_sync(self, endpoint: str, data: dict, header=None) -> dict | None:
|
||||
header = header or {}
|
||||
if not data.get("reqId"):
|
||||
data.update({
|
||||
"reqId": token_hex(16)
|
||||
})
|
||||
if not data.get("stamp"):
|
||||
data.update({
|
||||
"stamp": datetime.datetime.now().strftime("%Y%m%d%H%M%S")
|
||||
})
|
||||
random = str(int(time.time()))
|
||||
url = self._api_url + endpoint
|
||||
dump_data = json.dumps(data)
|
||||
sign = self._security.sign(dump_data, random)
|
||||
header.update({
|
||||
"content-type": "application/json; charset=utf-8",
|
||||
"secretVersion": "1",
|
||||
"sign": sign,
|
||||
"random": random,
|
||||
})
|
||||
if self._access_token is not None:
|
||||
header.update({
|
||||
"accesstoken": self._access_token
|
||||
})
|
||||
response:dict = {"code": -1}
|
||||
_LOGGER.debug(f"Midea cloud API header: {header}")
|
||||
_LOGGER.debug(f"Midea cloud API dump_data: {dump_data}")
|
||||
try:
|
||||
r = requests.post(url, headers=header, data=dump_data, timeout=5)
|
||||
raw = r.content
|
||||
_LOGGER.debug(f"Midea cloud API url: {url}, data: {data}, response: {raw}")
|
||||
response = json.loads(raw)
|
||||
except Exception as e:
|
||||
_LOGGER.debug(f"API request attempt failed: {e}")
|
||||
|
||||
if int(response["code"]) == 0 and "data" in response:
|
||||
return response["data"]
|
||||
|
||||
return None
|
||||
|
||||
async def _get_login_id(self) -> str | None:
|
||||
data = self._make_general_data()
|
||||
data.update({
|
||||
@@ -115,27 +158,27 @@ class MideaCloud:
|
||||
async def login(self) -> bool:
|
||||
raise NotImplementedError()
|
||||
|
||||
async def get_keys(self, appliance_id: int):
|
||||
result = {}
|
||||
for method in [1, 2]:
|
||||
udp_id = self._security.get_udp_id(appliance_id, method)
|
||||
data = self._make_general_data()
|
||||
data.update({
|
||||
"udpid": udp_id
|
||||
})
|
||||
response = await self._api_request(
|
||||
endpoint="/v1/iot/secure/getToken",
|
||||
data=data
|
||||
)
|
||||
if response and "tokenlist" in response:
|
||||
for token in response["tokenlist"]:
|
||||
if token["udpId"] == udp_id:
|
||||
result[method] = {
|
||||
"token": token["token"].lower(),
|
||||
"key": token["key"].lower()
|
||||
}
|
||||
result.update(default_keys)
|
||||
return result
|
||||
async def send_cloud(self, appliance_id: int, data: bytearray):
|
||||
appliance_code = str(appliance_id)
|
||||
params = {
|
||||
'applianceCode': appliance_code,
|
||||
'order': self._security.aes_encrypt(bytes_to_dec_string(data)).hex(),
|
||||
'timestamp': 'true',
|
||||
"isFull": "false"
|
||||
}
|
||||
|
||||
if response := await self._api_request(
|
||||
endpoint='/v1/appliance/transparent/send',
|
||||
data=params,
|
||||
):
|
||||
if response and response.get('reply'):
|
||||
_LOGGER.debug("[%s] Cloud command response: %s", appliance_code, response)
|
||||
reply_data = self._security.aes_decrypt(bytes.fromhex(response['reply']))
|
||||
return reply_data
|
||||
else:
|
||||
_LOGGER.warning("[%s] Cloud command failed: %s", appliance_code, response)
|
||||
|
||||
return None
|
||||
|
||||
async def list_home(self) -> dict | None:
|
||||
return {1: "My home"}
|
||||
|
@@ -1,10 +1,14 @@
|
||||
import threading
|
||||
import socket
|
||||
from enum import IntEnum
|
||||
|
||||
from .cloud import MideaCloud
|
||||
from .security import LocalSecurity, MSGTYPE_HANDSHAKE_REQUEST, MSGTYPE_ENCRYPTED_REQUEST
|
||||
from .packet_builder import PacketBuilder
|
||||
from .message import MessageQuestCustom
|
||||
from .logger import MideaLogger
|
||||
from .lua_runtime import MideaCodec
|
||||
from .util import dec_string_to_bytes
|
||||
|
||||
|
||||
class AuthException(Exception):
|
||||
@@ -39,7 +43,9 @@ class MiedaDevice(threading.Thread):
|
||||
subtype: int | None,
|
||||
connected: bool,
|
||||
sn: str | None,
|
||||
sn8: str | None):
|
||||
sn8: str | None,
|
||||
lua_file: str | None,
|
||||
cloud: MideaCloud | None):
|
||||
threading.Thread.__init__(self)
|
||||
self._socket = None
|
||||
self._ip_address = ip_address
|
||||
@@ -71,6 +77,8 @@ class MiedaDevice(threading.Thread):
|
||||
self._centralized = []
|
||||
self._calculate_get = []
|
||||
self._calculate_set = []
|
||||
self._lua_runtime = MideaCodec(lua_file, sn=sn, subtype=subtype) if lua_file is not None else None
|
||||
self._cloud = cloud
|
||||
|
||||
@property
|
||||
def device_name(self):
|
||||
@@ -126,14 +134,16 @@ class MiedaDevice(threading.Thread):
|
||||
def get_attribute(self, attribute):
|
||||
return self._attributes.get(attribute)
|
||||
|
||||
def set_attribute(self, attribute, value):
|
||||
async def set_attribute(self, attribute, value):
|
||||
if attribute in self._attributes.keys():
|
||||
new_status = {}
|
||||
for attr in self._centralized:
|
||||
new_status[attr] = self._attributes.get(attr)
|
||||
new_status[attribute] = value
|
||||
if set_cmd := self._lua_runtime.build_control(new_status):
|
||||
await self._build_send(set_cmd)
|
||||
|
||||
def set_attributes(self, attributes):
|
||||
async def set_attributes(self, attributes):
|
||||
new_status = {}
|
||||
for attr in self._centralized:
|
||||
new_status[attr] = self._attributes.get(attr)
|
||||
@@ -142,6 +152,9 @@ class MiedaDevice(threading.Thread):
|
||||
if attribute in self._attributes.keys():
|
||||
has_new = True
|
||||
new_status[attribute] = value
|
||||
if has_new:
|
||||
if set_cmd := self._lua_runtime.build_control(new_status):
|
||||
await self._build_send(set_cmd)
|
||||
|
||||
def set_ip_address(self, ip_address):
|
||||
MideaLogger.debug(f"Update IP address to {ip_address}")
|
||||
@@ -188,12 +201,6 @@ class MiedaDevice(threading.Thread):
|
||||
response = response[8: 72]
|
||||
self._security.tcp_key(response, self._key)
|
||||
|
||||
def _send_message(self, data):
|
||||
if self._protocol == 3:
|
||||
self._send_message_v3(data, msg_type=MSGTYPE_ENCRYPTED_REQUEST)
|
||||
else:
|
||||
self._send_message_v2(data)
|
||||
|
||||
def _send_message_v2(self, data):
|
||||
if self._socket is not None:
|
||||
self._socket.send(data)
|
||||
@@ -204,11 +211,128 @@ class MiedaDevice(threading.Thread):
|
||||
data = self._security.encode_8370(data, msg_type)
|
||||
self._send_message_v2(data)
|
||||
|
||||
def _build_send(self, cmd: str):
|
||||
async def _build_send(self, cmd: str):
|
||||
MideaLogger.debug(f"Sending: {cmd.lower()}")
|
||||
bytes_cmd = bytes.fromhex(cmd)
|
||||
msg = PacketBuilder(self._device_id, bytes_cmd).finalize()
|
||||
self._send_message(msg)
|
||||
await self._send_message(bytes_cmd)
|
||||
|
||||
async def refresh_status(self):
|
||||
for query in self._queries:
|
||||
if query_cmd := self._lua_runtime.build_query(query):
|
||||
await self._build_send(query_cmd)
|
||||
|
||||
def _parse_cloud_message(self, decrypted):
|
||||
# MideaLogger.debug(f"Received: {decrypted}")
|
||||
if status := self._lua_runtime.decode_status(dec_string_to_bytes(decrypted).hex()):
|
||||
MideaLogger.debug(f"Decoded: {status}")
|
||||
new_status = {}
|
||||
for single in status.keys():
|
||||
value = status.get(single)
|
||||
if single not in self._attributes or self._attributes[single] != value:
|
||||
self._attributes[single] = value
|
||||
new_status[single] = value
|
||||
if len(new_status) > 0:
|
||||
for c in self._calculate_get:
|
||||
lvalue = c.get("lvalue")
|
||||
rvalue = c.get("rvalue")
|
||||
if lvalue and rvalue:
|
||||
calculate = False
|
||||
for s, v in new_status.items():
|
||||
if rvalue.find(f"[{s}]") >= 0:
|
||||
calculate = True
|
||||
break
|
||||
if calculate:
|
||||
calculate_str1 = \
|
||||
(f"{lvalue.replace('[', 'self._attributes[')} = "
|
||||
f"{rvalue.replace('[', 'self._attributes[')}") \
|
||||
.replace("[", "[\"").replace("]", "\"]")
|
||||
calculate_str2 = \
|
||||
(f"{lvalue.replace('[', 'new_status[')} = "
|
||||
f"{rvalue.replace('[', 'self._attributes[')}") \
|
||||
.replace("[", "[\"").replace("]", "\"]")
|
||||
try:
|
||||
exec(calculate_str1)
|
||||
exec(calculate_str2)
|
||||
except Exception:
|
||||
MideaLogger.warning(
|
||||
f"Calculation Error: {lvalue} = {rvalue}", self._device_id
|
||||
)
|
||||
self._update_all(new_status)
|
||||
return ParseMessageResult.SUCCESS
|
||||
|
||||
def _parse_message(self, msg):
|
||||
if self._protocol == 3:
|
||||
messages, self._buffer = self._security.decode_8370(self._buffer + msg)
|
||||
else:
|
||||
messages, self._buffer = self.fetch_v2_message(self._buffer + msg)
|
||||
if len(messages) == 0:
|
||||
return ParseMessageResult.PADDING
|
||||
for message in messages:
|
||||
if message == b"ERROR":
|
||||
return ParseMessageResult.ERROR
|
||||
payload_len = message[4] + (message[5] << 8) - 56
|
||||
payload_type = message[2] + (message[3] << 8)
|
||||
if payload_type in [0x1001, 0x0001]:
|
||||
# Heartbeat detected
|
||||
pass
|
||||
elif len(message) > 56:
|
||||
cryptographic = message[40:-16]
|
||||
if payload_len % 16 == 0:
|
||||
decrypted = self._security.aes_decrypt(cryptographic)
|
||||
MideaLogger.debug(f"Received: {decrypted.hex().lower()}")
|
||||
if status := self._lua_runtime.decode_status(decrypted.hex()):
|
||||
MideaLogger.debug(f"Decoded: {status}")
|
||||
new_status = {}
|
||||
for single in status.keys():
|
||||
value = status.get(single)
|
||||
if single not in self._attributes or self._attributes[single] != value:
|
||||
self._attributes[single] = value
|
||||
new_status[single] = value
|
||||
if len(new_status) > 0:
|
||||
for c in self._calculate_get:
|
||||
lvalue = c.get("lvalue")
|
||||
rvalue = c.get("rvalue")
|
||||
if lvalue and rvalue:
|
||||
calculate = False
|
||||
for s, v in new_status.items():
|
||||
if rvalue.find(f"[{s}]") >= 0:
|
||||
calculate = True
|
||||
break
|
||||
if calculate:
|
||||
calculate_str1 = \
|
||||
(f"{lvalue.replace('[', 'self._attributes[')} = "
|
||||
f"{rvalue.replace('[', 'self._attributes[')}") \
|
||||
.replace("[", "[\"").replace("]", "\"]")
|
||||
calculate_str2 = \
|
||||
(f"{lvalue.replace('[', 'new_status[')} = "
|
||||
f"{rvalue.replace('[', 'self._attributes[')}") \
|
||||
.replace("[", "[\"").replace("]", "\"]")
|
||||
try:
|
||||
exec(calculate_str1)
|
||||
exec(calculate_str2)
|
||||
except Exception:
|
||||
MideaLogger.warning(
|
||||
f"Calculation Error: {lvalue} = {rvalue}", self._device_id
|
||||
)
|
||||
self._update_all(new_status)
|
||||
return ParseMessageResult.SUCCESS
|
||||
|
||||
async def _send_message(self, data):
|
||||
if reply := await self._cloud.send_cloud(self._device_id, data):
|
||||
result = self._parse_cloud_message(reply)
|
||||
if result == ParseMessageResult.ERROR:
|
||||
MideaLogger.debug(f"Message 'ERROR' received")
|
||||
elif result == ParseMessageResult.SUCCESS:
|
||||
timeout_counter = 0
|
||||
|
||||
# if self._protocol == 3:
|
||||
# self._send_message_v3(data, msg_type=MSGTYPE_ENCRYPTED_REQUEST)
|
||||
# else:
|
||||
# self._send_message_v2(data)
|
||||
|
||||
async def _send_heartbeat(self):
|
||||
msg = PacketBuilder(self._device_id, bytearray([0x00])).finalize(msg_type=0)
|
||||
await self._send_message(msg)
|
||||
|
||||
def _device_connected(self, connected=True):
|
||||
self._connected = connected
|
||||
|
91
custom_components/midea_auto_cloud/core/lua_runtime.py
Normal file
91
custom_components/midea_auto_cloud/core/lua_runtime.py
Normal file
@@ -0,0 +1,91 @@
|
||||
import lupa
|
||||
import threading
|
||||
import json
|
||||
from .logger import MideaLogger
|
||||
|
||||
|
||||
class LuaRuntime:
|
||||
def __init__(self, file):
|
||||
self._runtimes = lupa.LuaRuntime()
|
||||
string = f'dofile("{file}")'
|
||||
self._runtimes.execute(string)
|
||||
self._lock = threading.Lock()
|
||||
self._json_to_data = self._runtimes.eval("function(param) return jsonToData(param) end")
|
||||
self._data_to_json = self._runtimes.eval("function(param) return dataToJson(param) end")
|
||||
|
||||
def json_to_data(self, json_value):
|
||||
with self._lock:
|
||||
result = self._json_to_data(json_value)
|
||||
|
||||
return result
|
||||
|
||||
def data_to_json(self, data_value):
|
||||
with self._lock:
|
||||
result = self._data_to_json(data_value)
|
||||
return result
|
||||
|
||||
|
||||
class MideaCodec(LuaRuntime):
|
||||
def __init__(self, file, sn=None, subtype=None):
|
||||
super().__init__(file)
|
||||
self._sn = sn
|
||||
self._subtype = subtype
|
||||
|
||||
def _build_base_dict(self):
|
||||
device_info ={}
|
||||
if self._sn is not None:
|
||||
device_info["deviceSN"] = self._sn
|
||||
if self._subtype is not None:
|
||||
device_info["deviceSubType"] = self._subtype
|
||||
base_dict = {
|
||||
"deviceinfo": device_info
|
||||
}
|
||||
return base_dict
|
||||
|
||||
def build_query(self, append=None):
|
||||
query_dict = self._build_base_dict()
|
||||
query_dict["query"] = {} if append is None else append
|
||||
json_str = json.dumps(query_dict)
|
||||
try:
|
||||
result = self.json_to_data(json_str)
|
||||
return result
|
||||
except lupa.LuaError as e:
|
||||
MideaLogger.error(f"LuaRuntimeError in build_query {json_str}: {repr(e)}")
|
||||
return None
|
||||
|
||||
def build_control(self, append=None):
|
||||
query_dict = self._build_base_dict()
|
||||
query_dict["control"] = {} if append is None else append
|
||||
json_str = json.dumps(query_dict)
|
||||
try:
|
||||
result = self.json_to_data(json_str)
|
||||
return result
|
||||
except lupa.LuaError as e:
|
||||
MideaLogger.error(f"LuaRuntimeError in build_control {json_str}: {repr(e)}")
|
||||
return None
|
||||
|
||||
def build_status(self, append=None):
|
||||
query_dict = self._build_base_dict()
|
||||
query_dict["status"] = {} if append is None else append
|
||||
json_str = json.dumps(query_dict)
|
||||
try:
|
||||
result = self.json_to_data(json_str)
|
||||
return result
|
||||
except lupa.LuaError as e:
|
||||
MideaLogger.error(f"LuaRuntimeError in build_status {json_str}: {repr(e)}")
|
||||
return None
|
||||
|
||||
def decode_status(self, data: str):
|
||||
data_dict = self._build_base_dict()
|
||||
data_dict["msg"] = {
|
||||
"data": data
|
||||
}
|
||||
json_str = json.dumps(data_dict)
|
||||
try:
|
||||
result = self.data_to_json(json_str)
|
||||
status = json.loads(result)
|
||||
return status.get("status")
|
||||
except lupa.LuaError as e:
|
||||
MideaLogger.error(f"LuaRuntimeError in decode_status {data}: {repr(e)}")
|
||||
return None
|
||||
|
50
custom_components/midea_auto_cloud/core/util.py
Normal file
50
custom_components/midea_auto_cloud/core/util.py
Normal file
@@ -0,0 +1,50 @@
|
||||
def bytes_to_dec_string(data: bytearray) -> bytearray:
|
||||
"""
|
||||
将 bytearray 转换为逗号分隔的十进制字符串格式,然后返回 bytearray
|
||||
对应 Java 的 bytesToDecString 方法
|
||||
"""
|
||||
# 处理有符号字节(模拟 Java 的 byte 类型 -128 到 127)
|
||||
result = []
|
||||
for b in data:
|
||||
# 将无符号字节转换为有符号字节
|
||||
signed_byte = b if b < 128 else b - 256
|
||||
result.append(str(signed_byte))
|
||||
|
||||
decimal_string = ','.join(result)
|
||||
return bytearray(decimal_string, 'utf-8')
|
||||
|
||||
|
||||
def dec_string_to_bytes(dec_string: str) -> bytearray:
|
||||
"""
|
||||
将逗号分隔的十进制字符串转换为字节数组
|
||||
对应 Java 的 decStringToBytes 方法
|
||||
|
||||
Args:
|
||||
dec_string: 逗号分隔的十进制字符串,如 "1,2,-3,127"
|
||||
|
||||
Returns:
|
||||
bytearray: 转换后的字节数组
|
||||
"""
|
||||
if dec_string is None:
|
||||
return bytearray()
|
||||
|
||||
# 按逗号分割字符串
|
||||
split_values = dec_string.split(',')
|
||||
result = bytearray(len(split_values))
|
||||
|
||||
for i, value_str in enumerate(split_values):
|
||||
try:
|
||||
# 解析十进制字符串为整数,然后转换为字节
|
||||
int_value = int(value_str.strip())
|
||||
# 确保值在字节范围内 (-128 到 127)
|
||||
if int_value < -128:
|
||||
int_value = -128
|
||||
elif int_value > 127:
|
||||
int_value = 127
|
||||
result[i] = int_value & 0xFF # 转换为无符号字节
|
||||
except (ValueError, IndexError) as e:
|
||||
# 如果解析失败,记录错误并跳过该值
|
||||
print(f"dec_string_to_bytes() error: {e}")
|
||||
result[i] = 0 # 默认值
|
||||
|
||||
return result
|
@@ -90,16 +90,17 @@ class MideaDataUpdateCoordinator(DataUpdateCoordinator[MideaDeviceData]):
|
||||
return self.data
|
||||
|
||||
try:
|
||||
# 使用传入的 cloud 实例(若可用)
|
||||
cloud = self._cloud
|
||||
if cloud and hasattr(cloud, "get_device_status"):
|
||||
try:
|
||||
status = await cloud.get_device_status(self._device_id)
|
||||
if isinstance(status, dict) and len(status) > 0:
|
||||
for k, v in status.items():
|
||||
self.device.attributes[k] = v
|
||||
except Exception as e:
|
||||
MideaLogger.debug(f"Cloud status fetch failed: {e}")
|
||||
await self.device.refresh_status()
|
||||
# # 使用传入的 cloud 实例(若可用)
|
||||
# cloud = self._cloud
|
||||
# if cloud and hasattr(cloud, "get_device_status"):
|
||||
# try:
|
||||
# status = await cloud.get_device_status(self._device_id)
|
||||
# if isinstance(status, dict) and len(status) > 0:
|
||||
# for k, v in status.items():
|
||||
# self.device.attributes[k] = v
|
||||
# except Exception as e:
|
||||
# MideaLogger.debug(f"Cloud status fetch failed: {e}")
|
||||
|
||||
# 返回并推送当前状态
|
||||
updated = MideaDeviceData(
|
||||
@@ -120,26 +121,15 @@ class MideaDataUpdateCoordinator(DataUpdateCoordinator[MideaDeviceData]):
|
||||
async def async_set_attribute(self, attribute: str, value) -> None:
|
||||
"""Set a device attribute."""
|
||||
# 云端控制:构造 control 与 status(携带当前状态作为上下文)
|
||||
cloud = self._cloud
|
||||
control = {attribute: value}
|
||||
status = dict(self.device.attributes)
|
||||
if cloud and hasattr(cloud, "send_device_control"):
|
||||
ok = await cloud.send_device_control(self._device_id, control=control, status=status)
|
||||
if ok:
|
||||
# 本地先行更新,随后依赖轮询或设备事件校正
|
||||
self.device.attributes[attribute] = value
|
||||
await self.device.set_attribute(attribute, value)
|
||||
self.device.attributes[attribute] = value
|
||||
self.mute_state_update_for_a_while()
|
||||
self.async_update_listeners()
|
||||
|
||||
async def async_set_attributes(self, attributes: dict) -> None:
|
||||
"""Set multiple device attributes."""
|
||||
cloud = self._cloud
|
||||
control = dict(attributes)
|
||||
status = dict(self.device.attributes)
|
||||
if cloud and hasattr(cloud, "send_device_control"):
|
||||
ok = await cloud.send_device_control(self._device_id, control=control, status=status)
|
||||
if ok:
|
||||
self.device.attributes.update(attributes)
|
||||
await self.device.set_attributes(attributes)
|
||||
self.device.attributes.update(attributes)
|
||||
self.mute_state_update_for_a_while()
|
||||
self.async_update_listeners()
|
||||
|
||||
|
@@ -6,10 +6,10 @@ from homeassistant.components.switch import SwitchDeviceClass
|
||||
DEVICE_MAPPING = {
|
||||
"default": {
|
||||
"rationale": ["off", "on"],
|
||||
"queries": [{}, {"query_type": "prevent_straight_wind"}],
|
||||
"queries": [{}],
|
||||
"centralized": [
|
||||
"power", "temperature", "small_temperature", "mode", "eco",
|
||||
"comfort_power_save", "comfort_sleep", "strong_wind",
|
||||
"comfort_power_save", "strong_wind",
|
||||
"wind_swing_lr", "wind_swing_lr", "wind_speed","ptc", "dry"
|
||||
],
|
||||
"entities": {
|
||||
@@ -28,12 +28,12 @@ DEVICE_MAPPING = {
|
||||
"none": {
|
||||
"eco": "off",
|
||||
"comfort_power_save": "off",
|
||||
"comfort_sleep": "off",
|
||||
# "comfort_sleep": "off",
|
||||
"strong_wind": "off"
|
||||
},
|
||||
"eco": {"eco": "on"},
|
||||
"comfort": {"comfort_power_save": "on"},
|
||||
"sleep": {"comfort_sleep": "on"},
|
||||
# "sleep": {"comfort_sleep": "on"},
|
||||
"boost": {"strong_wind": "on"}
|
||||
},
|
||||
"swing_modes": {
|
||||
@@ -87,9 +87,9 @@ DEVICE_MAPPING = {
|
||||
},
|
||||
"22012227": {
|
||||
"rationale": ["off", "on"],
|
||||
"queries": [{}, {"query_type": "prevent_straight_wind"}],
|
||||
"queries": [{}],
|
||||
"centralized": ["power", "temperature", "small_temperature", "mode", "eco", "comfort_power_save",
|
||||
"comfort_sleep", "strong_wind", "wind_swing_lr", "wind_swing_ud", "wind_speed",
|
||||
"strong_wind", "wind_swing_lr", "wind_swing_ud", "wind_speed",
|
||||
"ptc", "dry"],
|
||||
|
||||
"entities": {
|
||||
@@ -108,12 +108,12 @@ DEVICE_MAPPING = {
|
||||
"none": {
|
||||
"eco": "off",
|
||||
"comfort_power_save": "off",
|
||||
"comfort_sleep": "off",
|
||||
# "comfort_sleep": "off",
|
||||
"strong_wind": "off"
|
||||
},
|
||||
"eco": {"eco": "on"},
|
||||
"comfort": {"comfort_power_save": "on"},
|
||||
"sleep": {"comfort_sleep": "on"},
|
||||
# "sleep": {"comfort_sleep": "on"},
|
||||
"boost": {"strong_wind": "on"}
|
||||
},
|
||||
"swing_modes": {
|
||||
|
466
custom_components/midea_auto_cloud/device_mapping/T0xE2.py
Normal file
466
custom_components/midea_auto_cloud/device_mapping/T0xE2.py
Normal file
@@ -0,0 +1,466 @@
|
||||
from homeassistant.const import Platform, UnitOfTemperature, UnitOfTime, PERCENTAGE, PRECISION_HALVES
|
||||
from homeassistant.components.sensor import SensorStateClass, SensorDeviceClass
|
||||
from homeassistant.components.binary_sensor import BinarySensorDeviceClass
|
||||
from homeassistant.components.switch import SwitchDeviceClass
|
||||
|
||||
DEVICE_MAPPING = {
|
||||
"default": {
|
||||
"manufacturer": "美的",
|
||||
"rationale": ["off", "on"],
|
||||
"queries": [{}],
|
||||
"centralized": [
|
||||
"power", "temperature", "mode", "heat", "music", "ti_protect", "fast_wash",
|
||||
"ali_manager", "water_quality", "rate", "ele_exception", "communication_error",
|
||||
"cur_rate", "sterilize_left_days", "uv_sterilize_minute", "uv_sterilize_second",
|
||||
"eplus", "summer", "winter", "efficient", "night", "bath_person", "cloud",
|
||||
"bath", "half_heat", "whole_heat", "sterilization", "frequency_hot", "scene",
|
||||
"big_water", "wash", "negative_ions", "screen_off", "t_hot", "baby_wash",
|
||||
"dad_wash", "mom_wash", "wash_with_temp", "single_wash", "people_wash",
|
||||
"wash_temperature", "one_egg", "two_egg", "always_fell", "smart_sterilize",
|
||||
"sterilize_cycle_index", "sound_dad", "screen_light", "morning_night_bash",
|
||||
"version", "tds_value", "door_status", "limit_error", "sensor_error",
|
||||
"scene_id", "auto_off", "clean", "volume", "passwater_lowbyte", "cloud_appoint",
|
||||
"protect", "midea_manager", "sleep", "memory", "shower", "scroll_hot",
|
||||
"fast_hot_power", "hot_power", "safe", "water_flow", "heat_water_level",
|
||||
"flow", "appoint_wash", "now_wash", "end_time_hour", "end_time_minute",
|
||||
"get_time", "get_temp", "func_select", "warm_power", "type_select",
|
||||
"cur_temperature", "sterilize_high_temp", "discharge_status", "top_temp",
|
||||
"bottom_heat", "top_heat", "show_h", "uv_sterilize", "machine", "error_code",
|
||||
"need_discharge", "elec_warning", "bottom_temp", "water_cyclic", "water_system",
|
||||
"discharge_left_time", "in_temperature", "mg_remain", "waterday_lowbyte",
|
||||
"waterday_highbyte", "tech_water", "protect_show", "appoint_power"
|
||||
],
|
||||
"entities": {
|
||||
Platform.WATER_HEATER: {
|
||||
"water_heater": {
|
||||
"name": "Water Heater",
|
||||
"power": "power",
|
||||
"operation_list": {
|
||||
"off": {"power": "off"},
|
||||
"heat": {"power": "on", "mode": "heat"},
|
||||
"auto": {"power": "on", "mode": "auto"},
|
||||
"eco": {"power": "on", "mode": "eco"},
|
||||
"fast": {"power": "on", "mode": "fast"}
|
||||
},
|
||||
"target_temperature": "temperature",
|
||||
"current_temperature": "cur_temperature",
|
||||
"min_temp": 30,
|
||||
"max_temp": 75,
|
||||
"temperature_unit": UnitOfTemperature.CELSIUS,
|
||||
"precision": PRECISION_HALVES,
|
||||
}
|
||||
},
|
||||
Platform.SWITCH: {
|
||||
"music": {
|
||||
"device_class": SwitchDeviceClass.SWITCH,
|
||||
},
|
||||
"ti_protect": {
|
||||
"device_class": SwitchDeviceClass.SWITCH,
|
||||
},
|
||||
"fast_wash": {
|
||||
"device_class": SwitchDeviceClass.SWITCH,
|
||||
},
|
||||
"ali_manager": {
|
||||
"device_class": SwitchDeviceClass.SWITCH,
|
||||
},
|
||||
"heat": {
|
||||
"device_class": SwitchDeviceClass.SWITCH,
|
||||
},
|
||||
"ele_exception": {
|
||||
"device_class": SwitchDeviceClass.SWITCH,
|
||||
},
|
||||
"communication_error": {
|
||||
"device_class": SwitchDeviceClass.SWITCH,
|
||||
},
|
||||
"eplus": {
|
||||
"device_class": SwitchDeviceClass.SWITCH,
|
||||
},
|
||||
"summer": {
|
||||
"device_class": SwitchDeviceClass.SWITCH,
|
||||
},
|
||||
"winter": {
|
||||
"device_class": SwitchDeviceClass.SWITCH,
|
||||
},
|
||||
"efficient": {
|
||||
"device_class": SwitchDeviceClass.SWITCH,
|
||||
},
|
||||
"night": {
|
||||
"device_class": SwitchDeviceClass.SWITCH,
|
||||
},
|
||||
"bath_person": {
|
||||
"device_class": SwitchDeviceClass.SWITCH,
|
||||
},
|
||||
"cloud": {
|
||||
"device_class": SwitchDeviceClass.SWITCH,
|
||||
},
|
||||
"bath": {
|
||||
"device_class": SwitchDeviceClass.SWITCH,
|
||||
},
|
||||
"half_heat": {
|
||||
"device_class": SwitchDeviceClass.SWITCH,
|
||||
},
|
||||
"whole_heat": {
|
||||
"device_class": SwitchDeviceClass.SWITCH,
|
||||
},
|
||||
"sterilization": {
|
||||
"device_class": SwitchDeviceClass.SWITCH,
|
||||
},
|
||||
"frequency_hot": {
|
||||
"device_class": SwitchDeviceClass.SWITCH,
|
||||
},
|
||||
"scene": {
|
||||
"device_class": SwitchDeviceClass.SWITCH,
|
||||
},
|
||||
"big_water": {
|
||||
"device_class": SwitchDeviceClass.SWITCH,
|
||||
},
|
||||
"wash": {
|
||||
"device_class": SwitchDeviceClass.SWITCH,
|
||||
},
|
||||
"negative_ions": {
|
||||
"device_class": SwitchDeviceClass.SWITCH,
|
||||
},
|
||||
"screen_off": {
|
||||
"device_class": SwitchDeviceClass.SWITCH,
|
||||
},
|
||||
"t_hot": {
|
||||
"device_class": SwitchDeviceClass.SWITCH,
|
||||
},
|
||||
"baby_wash": {
|
||||
"device_class": SwitchDeviceClass.SWITCH,
|
||||
},
|
||||
"dad_wash": {
|
||||
"device_class": SwitchDeviceClass.SWITCH,
|
||||
},
|
||||
"mom_wash": {
|
||||
"device_class": SwitchDeviceClass.SWITCH,
|
||||
},
|
||||
"wash_with_temp": {
|
||||
"device_class": SwitchDeviceClass.SWITCH,
|
||||
},
|
||||
"single_wash": {
|
||||
"device_class": SwitchDeviceClass.SWITCH,
|
||||
},
|
||||
"people_wash": {
|
||||
"device_class": SwitchDeviceClass.SWITCH,
|
||||
},
|
||||
"one_egg": {
|
||||
"device_class": SwitchDeviceClass.SWITCH,
|
||||
},
|
||||
"two_egg": {
|
||||
"device_class": SwitchDeviceClass.SWITCH,
|
||||
},
|
||||
"always_fell": {
|
||||
"device_class": SwitchDeviceClass.SWITCH,
|
||||
},
|
||||
"smart_sterilize": {
|
||||
"device_class": SwitchDeviceClass.SWITCH,
|
||||
},
|
||||
"sound_dad": {
|
||||
"device_class": SwitchDeviceClass.SWITCH,
|
||||
},
|
||||
"door_status": {
|
||||
"device_class": SwitchDeviceClass.SWITCH,
|
||||
},
|
||||
"limit_error": {
|
||||
"device_class": SwitchDeviceClass.SWITCH,
|
||||
},
|
||||
"sensor_error": {
|
||||
"device_class": SwitchDeviceClass.SWITCH,
|
||||
},
|
||||
"auto_off": {
|
||||
"device_class": SwitchDeviceClass.SWITCH,
|
||||
},
|
||||
"clean": {
|
||||
"device_class": SwitchDeviceClass.SWITCH,
|
||||
},
|
||||
"cloud_appoint": {
|
||||
"device_class": SwitchDeviceClass.SWITCH,
|
||||
},
|
||||
"protect": {
|
||||
"device_class": SwitchDeviceClass.SWITCH,
|
||||
},
|
||||
"midea_manager": {
|
||||
"device_class": SwitchDeviceClass.SWITCH,
|
||||
},
|
||||
"sleep": {
|
||||
"device_class": SwitchDeviceClass.SWITCH,
|
||||
},
|
||||
"memory": {
|
||||
"device_class": SwitchDeviceClass.SWITCH,
|
||||
},
|
||||
"shower": {
|
||||
"device_class": SwitchDeviceClass.SWITCH,
|
||||
},
|
||||
"scroll_hot": {
|
||||
"device_class": SwitchDeviceClass.SWITCH,
|
||||
},
|
||||
"fast_hot_power": {
|
||||
"device_class": SwitchDeviceClass.SWITCH,
|
||||
},
|
||||
"hot_power": {
|
||||
"device_class": SwitchDeviceClass.SWITCH,
|
||||
},
|
||||
"safe": {
|
||||
"device_class": SwitchDeviceClass.SWITCH,
|
||||
},
|
||||
"water_flow": {
|
||||
"device_class": SwitchDeviceClass.SWITCH,
|
||||
},
|
||||
"appoint_wash": {
|
||||
"device_class": SwitchDeviceClass.SWITCH,
|
||||
},
|
||||
"now_wash": {
|
||||
"device_class": SwitchDeviceClass.SWITCH,
|
||||
},
|
||||
"get_time": {
|
||||
"device_class": SwitchDeviceClass.SWITCH,
|
||||
},
|
||||
"get_temp": {
|
||||
"device_class": SwitchDeviceClass.SWITCH,
|
||||
},
|
||||
"warm_power": {
|
||||
"device_class": SwitchDeviceClass.SWITCH,
|
||||
},
|
||||
"sterilize_high_temp": {
|
||||
"device_class": SwitchDeviceClass.SWITCH,
|
||||
},
|
||||
"bottom_heat": {
|
||||
"device_class": SwitchDeviceClass.SWITCH,
|
||||
},
|
||||
"top_heat": {
|
||||
"device_class": SwitchDeviceClass.SWITCH,
|
||||
},
|
||||
"show_h": {
|
||||
"device_class": SwitchDeviceClass.SWITCH,
|
||||
},
|
||||
"uv_sterilize": {
|
||||
"device_class": SwitchDeviceClass.SWITCH,
|
||||
},
|
||||
"need_discharge": {
|
||||
"device_class": SwitchDeviceClass.SWITCH,
|
||||
},
|
||||
"elec_warning": {
|
||||
"device_class": SwitchDeviceClass.SWITCH,
|
||||
},
|
||||
"water_cyclic": {
|
||||
"device_class": SwitchDeviceClass.SWITCH,
|
||||
},
|
||||
"tech_water": {
|
||||
"device_class": SwitchDeviceClass.SWITCH,
|
||||
},
|
||||
"protect_show": {
|
||||
"device_class": SwitchDeviceClass.SWITCH,
|
||||
},
|
||||
"appoint_power": {
|
||||
"device_class": SwitchDeviceClass.SWITCH,
|
||||
}
|
||||
},
|
||||
Platform.SELECT: {
|
||||
"mode": {
|
||||
"options": {
|
||||
"none": {"mode": "none"},
|
||||
"heat": {"mode": "heat"},
|
||||
"auto": {"mode": "auto"},
|
||||
"eco": {"mode": "eco"},
|
||||
"fast": {"mode": "fast"}
|
||||
}
|
||||
},
|
||||
"water_quality": {
|
||||
"options": {
|
||||
"0": {"water_quality": 0},
|
||||
"1": {"water_quality": 1},
|
||||
"2": {"water_quality": 2},
|
||||
"3": {"water_quality": 3}
|
||||
}
|
||||
},
|
||||
"func_select": {
|
||||
"options": {
|
||||
"low": {"func_select": "low"},
|
||||
"medium": {"func_select": "medium"},
|
||||
"high": {"func_select": "high"}
|
||||
}
|
||||
},
|
||||
"type_select": {
|
||||
"options": {
|
||||
"normal": {"type_select": "normal"},
|
||||
"eco": {"type_select": "eco"},
|
||||
"fast": {"type_select": "fast"}
|
||||
}
|
||||
},
|
||||
"machine": {
|
||||
"options": {
|
||||
"real_machine": {"machine": "real_machine"},
|
||||
"virtual_machine": {"machine": "virtual_machine"}
|
||||
}
|
||||
}
|
||||
},
|
||||
Platform.SENSOR: {
|
||||
"temperature": {
|
||||
"device_class": SensorDeviceClass.TEMPERATURE,
|
||||
"unit_of_measurement": UnitOfTemperature.CELSIUS,
|
||||
"state_class": SensorStateClass.MEASUREMENT
|
||||
},
|
||||
"cur_temperature": {
|
||||
"device_class": SensorDeviceClass.TEMPERATURE,
|
||||
"unit_of_measurement": UnitOfTemperature.CELSIUS,
|
||||
"state_class": SensorStateClass.MEASUREMENT
|
||||
},
|
||||
"top_temp": {
|
||||
"device_class": SensorDeviceClass.TEMPERATURE,
|
||||
"unit_of_measurement": UnitOfTemperature.CELSIUS,
|
||||
"state_class": SensorStateClass.MEASUREMENT
|
||||
},
|
||||
"bottom_temp": {
|
||||
"device_class": SensorDeviceClass.TEMPERATURE,
|
||||
"unit_of_measurement": UnitOfTemperature.CELSIUS,
|
||||
"state_class": SensorStateClass.MEASUREMENT
|
||||
},
|
||||
"in_temperature": {
|
||||
"device_class": SensorDeviceClass.TEMPERATURE,
|
||||
"unit_of_measurement": UnitOfTemperature.CELSIUS,
|
||||
"state_class": SensorStateClass.MEASUREMENT
|
||||
},
|
||||
"passwater_lowbyte": {
|
||||
"device_class": SensorDeviceClass.WATER,
|
||||
"unit_of_measurement": "L",
|
||||
"state_class": SensorStateClass.MEASUREMENT
|
||||
},
|
||||
"passwater_highbyte": {
|
||||
"device_class": SensorDeviceClass.WATER,
|
||||
"unit_of_measurement": "L",
|
||||
"state_class": SensorStateClass.MEASUREMENT
|
||||
},
|
||||
"rate": {
|
||||
"device_class": SensorDeviceClass.WATER,
|
||||
"unit_of_measurement": "L/min",
|
||||
"state_class": SensorStateClass.MEASUREMENT
|
||||
},
|
||||
"cur_rate": {
|
||||
"device_class": SensorDeviceClass.WATER,
|
||||
"unit_of_measurement": "L/min",
|
||||
"state_class": SensorStateClass.MEASUREMENT
|
||||
},
|
||||
"sterilize_left_days": {
|
||||
"device_class": SensorDeviceClass.DURATION,
|
||||
"unit_of_measurement": UnitOfTime.DAYS,
|
||||
"state_class": SensorStateClass.MEASUREMENT
|
||||
},
|
||||
"uv_sterilize_minute": {
|
||||
"device_class": SensorDeviceClass.DURATION,
|
||||
"unit_of_measurement": UnitOfTime.MINUTES,
|
||||
"state_class": SensorStateClass.MEASUREMENT
|
||||
},
|
||||
"uv_sterilize_second": {
|
||||
"device_class": SensorDeviceClass.DURATION,
|
||||
"unit_of_measurement": UnitOfTime.SECONDS,
|
||||
"state_class": SensorStateClass.MEASUREMENT
|
||||
},
|
||||
"screen_light": {
|
||||
"device_class": SensorDeviceClass.ILLUMINANCE,
|
||||
"unit_of_measurement": "lx",
|
||||
"state_class": SensorStateClass.MEASUREMENT
|
||||
},
|
||||
"morning_night_bash": {
|
||||
"device_class": SensorDeviceClass.DURATION,
|
||||
"unit_of_measurement": UnitOfTime.MINUTES,
|
||||
"state_class": SensorStateClass.MEASUREMENT
|
||||
},
|
||||
"version": {
|
||||
"device_class": SensorDeviceClass.ENUM
|
||||
},
|
||||
"tds_value": {
|
||||
"device_class": SensorDeviceClass.WATER,
|
||||
"unit_of_measurement": "ppm",
|
||||
"state_class": SensorStateClass.MEASUREMENT
|
||||
},
|
||||
"scene_id": {
|
||||
"device_class": SensorDeviceClass.ENUM
|
||||
},
|
||||
"volume": {
|
||||
"device_class": SensorDeviceClass.SOUND_PRESSURE,
|
||||
"unit_of_measurement": "%",
|
||||
"state_class": SensorStateClass.MEASUREMENT
|
||||
},
|
||||
"heat_water_level": {
|
||||
"device_class": SensorDeviceClass.WATER,
|
||||
"unit_of_measurement": "%",
|
||||
"state_class": SensorStateClass.MEASUREMENT
|
||||
},
|
||||
"flow": {
|
||||
"device_class": SensorDeviceClass.WATER,
|
||||
"unit_of_measurement": "L/min",
|
||||
"state_class": SensorStateClass.MEASUREMENT
|
||||
},
|
||||
"end_time_hour": {
|
||||
"device_class": SensorDeviceClass.DURATION,
|
||||
"unit_of_measurement": UnitOfTime.HOURS,
|
||||
"state_class": SensorStateClass.MEASUREMENT
|
||||
},
|
||||
"end_time_minute": {
|
||||
"device_class": SensorDeviceClass.DURATION,
|
||||
"unit_of_measurement": UnitOfTime.MINUTES,
|
||||
"state_class": SensorStateClass.MEASUREMENT
|
||||
},
|
||||
"wash_temperature": {
|
||||
"device_class": SensorDeviceClass.TEMPERATURE,
|
||||
"unit_of_measurement": UnitOfTemperature.CELSIUS,
|
||||
"state_class": SensorStateClass.MEASUREMENT
|
||||
},
|
||||
"sterilize_cycle_index": {
|
||||
"device_class": SensorDeviceClass.ENUM
|
||||
},
|
||||
"discharge_status": {
|
||||
"device_class": SensorDeviceClass.ENUM
|
||||
},
|
||||
"error_code": {
|
||||
"device_class": SensorDeviceClass.ENUM
|
||||
},
|
||||
"water_system": {
|
||||
"device_class": SensorDeviceClass.ENUM
|
||||
},
|
||||
"discharge_left_time": {
|
||||
"device_class": SensorDeviceClass.DURATION,
|
||||
"unit_of_measurement": UnitOfTime.MINUTES,
|
||||
"state_class": SensorStateClass.MEASUREMENT
|
||||
},
|
||||
"mg_remain": {
|
||||
"device_class": SensorDeviceClass.WATER,
|
||||
"unit_of_measurement": "mg",
|
||||
"state_class": SensorStateClass.MEASUREMENT
|
||||
},
|
||||
"waterday_lowbyte": {
|
||||
"device_class": SensorDeviceClass.WATER,
|
||||
"unit_of_measurement": "L",
|
||||
"state_class": SensorStateClass.MEASUREMENT
|
||||
},
|
||||
"waterday_highbyte": {
|
||||
"device_class": SensorDeviceClass.WATER,
|
||||
"unit_of_measurement": "L",
|
||||
"state_class": SensorStateClass.MEASUREMENT
|
||||
}
|
||||
},
|
||||
Platform.BINARY_SENSOR: {
|
||||
"door_status": {
|
||||
"device_class": BinarySensorDeviceClass.DOOR,
|
||||
},
|
||||
"limit_error": {
|
||||
"device_class": BinarySensorDeviceClass.PROBLEM,
|
||||
},
|
||||
"sensor_error": {
|
||||
"device_class": BinarySensorDeviceClass.PROBLEM,
|
||||
},
|
||||
"communication_error": {
|
||||
"device_class": BinarySensorDeviceClass.PROBLEM,
|
||||
},
|
||||
"ele_exception": {
|
||||
"device_class": BinarySensorDeviceClass.PROBLEM,
|
||||
},
|
||||
"elec_warning": {
|
||||
"device_class": BinarySensorDeviceClass.PROBLEM,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -6,6 +6,6 @@
|
||||
"documentation": "https://github.com/sususweet/midea-meiju-codec#readme",
|
||||
"iot_class": "cloud_push",
|
||||
"issue_tracker": "https://github.com/sususweet/midea-meiju-codec/issues",
|
||||
"requirements": [],
|
||||
"version": "v0.0.5"
|
||||
"requirements": ["lupa>=2.0"],
|
||||
"version": "v0.0.6"
|
||||
}
|
Reference in New Issue
Block a user