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_SN8,
|
||||||
CONF_SN,
|
CONF_SN,
|
||||||
CONF_MODEL_NUMBER,
|
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
|
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():
|
for appliance_code, info in appliances.items():
|
||||||
MideaLogger.debug(f"info={info} ")
|
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:
|
try:
|
||||||
device = MiedaDevice(
|
device = MiedaDevice(
|
||||||
name=info.get(CONF_NAME) or info.get("name"),
|
name=info.get(CONF_NAME),
|
||||||
device_id=appliance_code,
|
device_id=appliance_code,
|
||||||
device_type=info.get(CONF_TYPE) or info.get("type"),
|
device_type=info.get(CONF_TYPE),
|
||||||
ip_address=None,
|
ip_address=None,
|
||||||
port=None,
|
port=None,
|
||||||
token=None,
|
token=None,
|
||||||
@@ -192,8 +202,10 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry):
|
|||||||
protocol=info.get(CONF_PROTOCOL) or 2,
|
protocol=info.get(CONF_PROTOCOL) or 2,
|
||||||
model=info.get(CONF_MODEL),
|
model=info.get(CONF_MODEL),
|
||||||
subtype=info.get(CONF_MODEL_NUMBER),
|
subtype=info.get(CONF_MODEL_NUMBER),
|
||||||
sn=info.get(CONF_SN) or info.get("sn"),
|
sn=info.get(CONF_SN),
|
||||||
sn8=info.get(CONF_SN8) or info.get("sn8"),
|
sn8=info.get(CONF_SN8),
|
||||||
|
lua_file=file,
|
||||||
|
cloud=cloud,
|
||||||
)
|
)
|
||||||
# 加载并应用设备映射(queries/centralized/calculate),并预置 attributes 键
|
# 加载并应用设备映射(queries/centralized/calculate),并预置 attributes 键
|
||||||
try:
|
try:
|
||||||
@@ -289,6 +301,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry):
|
|||||||
bucket["coordinator_map"][appliance_code] = coordinator
|
bucket["coordinator_map"][appliance_code] = coordinator
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
MideaLogger.error(f"Init device failed: {appliance_code}, error: {e}")
|
MideaLogger.error(f"Init device failed: {appliance_code}, error: {e}")
|
||||||
|
# break
|
||||||
hass.data[DOMAIN]["accounts"][config_entry.entry_id] = bucket
|
hass.data[DOMAIN]["accounts"][config_entry.entry_id] = bucket
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
@@ -256,7 +256,6 @@ class MideaClimateEntity(MideaEntity, ClimateEntity):
|
|||||||
if dict_config is None:
|
if dict_config is None:
|
||||||
return 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():
|
for key, config in dict_config.items():
|
||||||
if isinstance(config, dict):
|
if isinstance(config, dict):
|
||||||
# Check if all conditions match
|
# 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 datetime
|
||||||
import json
|
import json
|
||||||
import base64
|
import base64
|
||||||
|
import asyncio
|
||||||
|
import requests
|
||||||
from aiohttp import ClientSession
|
from aiohttp import ClientSession
|
||||||
from secrets import token_hex
|
from secrets import token_hex
|
||||||
from .logger import MideaLogger
|
from .logger import MideaLogger
|
||||||
from .security import CloudSecurity, MeijuCloudSecurity, MSmartCloudSecurity
|
from .security import CloudSecurity, MeijuCloudSecurity, MSmartCloudSecurity
|
||||||
|
from .util import bytes_to_dec_string
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -100,6 +103,46 @@ class MideaCloud:
|
|||||||
|
|
||||||
return None
|
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:
|
async def _get_login_id(self) -> str | None:
|
||||||
data = self._make_general_data()
|
data = self._make_general_data()
|
||||||
data.update({
|
data.update({
|
||||||
@@ -115,27 +158,27 @@ class MideaCloud:
|
|||||||
async def login(self) -> bool:
|
async def login(self) -> bool:
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
async def get_keys(self, appliance_id: int):
|
async def send_cloud(self, appliance_id: int, data: bytearray):
|
||||||
result = {}
|
appliance_code = str(appliance_id)
|
||||||
for method in [1, 2]:
|
params = {
|
||||||
udp_id = self._security.get_udp_id(appliance_id, method)
|
'applianceCode': appliance_code,
|
||||||
data = self._make_general_data()
|
'order': self._security.aes_encrypt(bytes_to_dec_string(data)).hex(),
|
||||||
data.update({
|
'timestamp': 'true',
|
||||||
"udpid": udp_id
|
"isFull": "false"
|
||||||
})
|
}
|
||||||
response = await self._api_request(
|
|
||||||
endpoint="/v1/iot/secure/getToken",
|
if response := await self._api_request(
|
||||||
data=data
|
endpoint='/v1/appliance/transparent/send',
|
||||||
)
|
data=params,
|
||||||
if response and "tokenlist" in response:
|
):
|
||||||
for token in response["tokenlist"]:
|
if response and response.get('reply'):
|
||||||
if token["udpId"] == udp_id:
|
_LOGGER.debug("[%s] Cloud command response: %s", appliance_code, response)
|
||||||
result[method] = {
|
reply_data = self._security.aes_decrypt(bytes.fromhex(response['reply']))
|
||||||
"token": token["token"].lower(),
|
return reply_data
|
||||||
"key": token["key"].lower()
|
else:
|
||||||
}
|
_LOGGER.warning("[%s] Cloud command failed: %s", appliance_code, response)
|
||||||
result.update(default_keys)
|
|
||||||
return result
|
return None
|
||||||
|
|
||||||
async def list_home(self) -> dict | None:
|
async def list_home(self) -> dict | None:
|
||||||
return {1: "My home"}
|
return {1: "My home"}
|
||||||
|
@@ -1,10 +1,14 @@
|
|||||||
import threading
|
import threading
|
||||||
import socket
|
import socket
|
||||||
from enum import IntEnum
|
from enum import IntEnum
|
||||||
|
|
||||||
|
from .cloud import MideaCloud
|
||||||
from .security import LocalSecurity, MSGTYPE_HANDSHAKE_REQUEST, MSGTYPE_ENCRYPTED_REQUEST
|
from .security import LocalSecurity, MSGTYPE_HANDSHAKE_REQUEST, MSGTYPE_ENCRYPTED_REQUEST
|
||||||
from .packet_builder import PacketBuilder
|
from .packet_builder import PacketBuilder
|
||||||
from .message import MessageQuestCustom
|
from .message import MessageQuestCustom
|
||||||
from .logger import MideaLogger
|
from .logger import MideaLogger
|
||||||
|
from .lua_runtime import MideaCodec
|
||||||
|
from .util import dec_string_to_bytes
|
||||||
|
|
||||||
|
|
||||||
class AuthException(Exception):
|
class AuthException(Exception):
|
||||||
@@ -39,7 +43,9 @@ class MiedaDevice(threading.Thread):
|
|||||||
subtype: int | None,
|
subtype: int | None,
|
||||||
connected: bool,
|
connected: bool,
|
||||||
sn: str | None,
|
sn: str | None,
|
||||||
sn8: str | None):
|
sn8: str | None,
|
||||||
|
lua_file: str | None,
|
||||||
|
cloud: MideaCloud | None):
|
||||||
threading.Thread.__init__(self)
|
threading.Thread.__init__(self)
|
||||||
self._socket = None
|
self._socket = None
|
||||||
self._ip_address = ip_address
|
self._ip_address = ip_address
|
||||||
@@ -71,6 +77,8 @@ class MiedaDevice(threading.Thread):
|
|||||||
self._centralized = []
|
self._centralized = []
|
||||||
self._calculate_get = []
|
self._calculate_get = []
|
||||||
self._calculate_set = []
|
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
|
@property
|
||||||
def device_name(self):
|
def device_name(self):
|
||||||
@@ -126,14 +134,16 @@ class MiedaDevice(threading.Thread):
|
|||||||
def get_attribute(self, attribute):
|
def get_attribute(self, attribute):
|
||||||
return self._attributes.get(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():
|
if attribute in self._attributes.keys():
|
||||||
new_status = {}
|
new_status = {}
|
||||||
for attr in self._centralized:
|
for attr in self._centralized:
|
||||||
new_status[attr] = self._attributes.get(attr)
|
new_status[attr] = self._attributes.get(attr)
|
||||||
new_status[attribute] = value
|
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 = {}
|
new_status = {}
|
||||||
for attr in self._centralized:
|
for attr in self._centralized:
|
||||||
new_status[attr] = self._attributes.get(attr)
|
new_status[attr] = self._attributes.get(attr)
|
||||||
@@ -142,6 +152,9 @@ class MiedaDevice(threading.Thread):
|
|||||||
if attribute in self._attributes.keys():
|
if attribute in self._attributes.keys():
|
||||||
has_new = True
|
has_new = True
|
||||||
new_status[attribute] = value
|
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):
|
def set_ip_address(self, ip_address):
|
||||||
MideaLogger.debug(f"Update IP address to {ip_address}")
|
MideaLogger.debug(f"Update IP address to {ip_address}")
|
||||||
@@ -188,12 +201,6 @@ class MiedaDevice(threading.Thread):
|
|||||||
response = response[8: 72]
|
response = response[8: 72]
|
||||||
self._security.tcp_key(response, self._key)
|
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):
|
def _send_message_v2(self, data):
|
||||||
if self._socket is not None:
|
if self._socket is not None:
|
||||||
self._socket.send(data)
|
self._socket.send(data)
|
||||||
@@ -204,11 +211,128 @@ class MiedaDevice(threading.Thread):
|
|||||||
data = self._security.encode_8370(data, msg_type)
|
data = self._security.encode_8370(data, msg_type)
|
||||||
self._send_message_v2(data)
|
self._send_message_v2(data)
|
||||||
|
|
||||||
def _build_send(self, cmd: str):
|
async def _build_send(self, cmd: str):
|
||||||
MideaLogger.debug(f"Sending: {cmd.lower()}")
|
MideaLogger.debug(f"Sending: {cmd.lower()}")
|
||||||
bytes_cmd = bytes.fromhex(cmd)
|
bytes_cmd = bytes.fromhex(cmd)
|
||||||
msg = PacketBuilder(self._device_id, bytes_cmd).finalize()
|
await self._send_message(bytes_cmd)
|
||||||
self._send_message(msg)
|
|
||||||
|
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):
|
def _device_connected(self, connected=True):
|
||||||
self._connected = connected
|
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
|
return self.data
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# 使用传入的 cloud 实例(若可用)
|
await self.device.refresh_status()
|
||||||
cloud = self._cloud
|
# # 使用传入的 cloud 实例(若可用)
|
||||||
if cloud and hasattr(cloud, "get_device_status"):
|
# cloud = self._cloud
|
||||||
try:
|
# if cloud and hasattr(cloud, "get_device_status"):
|
||||||
status = await cloud.get_device_status(self._device_id)
|
# try:
|
||||||
if isinstance(status, dict) and len(status) > 0:
|
# status = await cloud.get_device_status(self._device_id)
|
||||||
for k, v in status.items():
|
# if isinstance(status, dict) and len(status) > 0:
|
||||||
self.device.attributes[k] = v
|
# for k, v in status.items():
|
||||||
except Exception as e:
|
# self.device.attributes[k] = v
|
||||||
MideaLogger.debug(f"Cloud status fetch failed: {e}")
|
# except Exception as e:
|
||||||
|
# MideaLogger.debug(f"Cloud status fetch failed: {e}")
|
||||||
|
|
||||||
# 返回并推送当前状态
|
# 返回并推送当前状态
|
||||||
updated = MideaDeviceData(
|
updated = MideaDeviceData(
|
||||||
@@ -120,26 +121,15 @@ class MideaDataUpdateCoordinator(DataUpdateCoordinator[MideaDeviceData]):
|
|||||||
async def async_set_attribute(self, attribute: str, value) -> None:
|
async def async_set_attribute(self, attribute: str, value) -> None:
|
||||||
"""Set a device attribute."""
|
"""Set a device attribute."""
|
||||||
# 云端控制:构造 control 与 status(携带当前状态作为上下文)
|
# 云端控制:构造 control 与 status(携带当前状态作为上下文)
|
||||||
cloud = self._cloud
|
await self.device.set_attribute(attribute, value)
|
||||||
control = {attribute: value}
|
self.device.attributes[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
|
|
||||||
self.mute_state_update_for_a_while()
|
self.mute_state_update_for_a_while()
|
||||||
self.async_update_listeners()
|
self.async_update_listeners()
|
||||||
|
|
||||||
async def async_set_attributes(self, attributes: dict) -> None:
|
async def async_set_attributes(self, attributes: dict) -> None:
|
||||||
"""Set multiple device attributes."""
|
"""Set multiple device attributes."""
|
||||||
cloud = self._cloud
|
await self.device.set_attributes(attributes)
|
||||||
control = dict(attributes)
|
self.device.attributes.update(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)
|
|
||||||
self.mute_state_update_for_a_while()
|
self.mute_state_update_for_a_while()
|
||||||
self.async_update_listeners()
|
self.async_update_listeners()
|
||||||
|
|
||||||
|
@@ -6,10 +6,10 @@ from homeassistant.components.switch import SwitchDeviceClass
|
|||||||
DEVICE_MAPPING = {
|
DEVICE_MAPPING = {
|
||||||
"default": {
|
"default": {
|
||||||
"rationale": ["off", "on"],
|
"rationale": ["off", "on"],
|
||||||
"queries": [{}, {"query_type": "prevent_straight_wind"}],
|
"queries": [{}],
|
||||||
"centralized": [
|
"centralized": [
|
||||||
"power", "temperature", "small_temperature", "mode", "eco",
|
"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"
|
"wind_swing_lr", "wind_swing_lr", "wind_speed","ptc", "dry"
|
||||||
],
|
],
|
||||||
"entities": {
|
"entities": {
|
||||||
@@ -28,12 +28,12 @@ DEVICE_MAPPING = {
|
|||||||
"none": {
|
"none": {
|
||||||
"eco": "off",
|
"eco": "off",
|
||||||
"comfort_power_save": "off",
|
"comfort_power_save": "off",
|
||||||
"comfort_sleep": "off",
|
# "comfort_sleep": "off",
|
||||||
"strong_wind": "off"
|
"strong_wind": "off"
|
||||||
},
|
},
|
||||||
"eco": {"eco": "on"},
|
"eco": {"eco": "on"},
|
||||||
"comfort": {"comfort_power_save": "on"},
|
"comfort": {"comfort_power_save": "on"},
|
||||||
"sleep": {"comfort_sleep": "on"},
|
# "sleep": {"comfort_sleep": "on"},
|
||||||
"boost": {"strong_wind": "on"}
|
"boost": {"strong_wind": "on"}
|
||||||
},
|
},
|
||||||
"swing_modes": {
|
"swing_modes": {
|
||||||
@@ -87,9 +87,9 @@ DEVICE_MAPPING = {
|
|||||||
},
|
},
|
||||||
"22012227": {
|
"22012227": {
|
||||||
"rationale": ["off", "on"],
|
"rationale": ["off", "on"],
|
||||||
"queries": [{}, {"query_type": "prevent_straight_wind"}],
|
"queries": [{}],
|
||||||
"centralized": ["power", "temperature", "small_temperature", "mode", "eco", "comfort_power_save",
|
"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"],
|
"ptc", "dry"],
|
||||||
|
|
||||||
"entities": {
|
"entities": {
|
||||||
@@ -108,12 +108,12 @@ DEVICE_MAPPING = {
|
|||||||
"none": {
|
"none": {
|
||||||
"eco": "off",
|
"eco": "off",
|
||||||
"comfort_power_save": "off",
|
"comfort_power_save": "off",
|
||||||
"comfort_sleep": "off",
|
# "comfort_sleep": "off",
|
||||||
"strong_wind": "off"
|
"strong_wind": "off"
|
||||||
},
|
},
|
||||||
"eco": {"eco": "on"},
|
"eco": {"eco": "on"},
|
||||||
"comfort": {"comfort_power_save": "on"},
|
"comfort": {"comfort_power_save": "on"},
|
||||||
"sleep": {"comfort_sleep": "on"},
|
# "sleep": {"comfort_sleep": "on"},
|
||||||
"boost": {"strong_wind": "on"}
|
"boost": {"strong_wind": "on"}
|
||||||
},
|
},
|
||||||
"swing_modes": {
|
"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",
|
"documentation": "https://github.com/sususweet/midea-meiju-codec#readme",
|
||||||
"iot_class": "cloud_push",
|
"iot_class": "cloud_push",
|
||||||
"issue_tracker": "https://github.com/sususweet/midea-meiju-codec/issues",
|
"issue_tracker": "https://github.com/sususweet/midea-meiju-codec/issues",
|
||||||
"requirements": [],
|
"requirements": ["lupa>=2.0"],
|
||||||
"version": "v0.0.5"
|
"version": "v0.0.6"
|
||||||
}
|
}
|
Reference in New Issue
Block a user