forked from HomeAssistant/midea-meiju-codec
Compare commits
6 Commits
v0.0.2-alp
...
v0.0.3-alp
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2af99c11da | ||
|
|
b1bf76f292 | ||
|
|
51f0fcc8dc | ||
|
|
39f88365e5 | ||
|
|
2f88658fda | ||
|
|
a9e2b784b5 |
10
README.md
10
README.md
@@ -14,6 +14,16 @@
|
||||
- 所有设备默认可生成一个名为Status的二进制传感器,其属性中列出了设备可访问的所有属性,当然有些值不可设置
|
||||
- Status实体前几项列出了该设备的分类信息,供参考
|
||||
|
||||
## 目前支持的设备类型
|
||||
|
||||
- T0xAC 空调
|
||||
- T0xB8 智能扫地机器人
|
||||
- T0xE1 洗碗机
|
||||
- T0xEA 电饭锅
|
||||
- T0xED 软水机
|
||||
|
||||
欢迎合作开发添加更多设备支持。
|
||||
|
||||
## 实体映射
|
||||
|
||||
映射文件位于`device_mapping`下, 每个大的品类一个映射文件,目前支持映射的实体类型如下:
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import os
|
||||
import base64
|
||||
import voluptuous as vol
|
||||
from importlib import import_module
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.util.json import load_json
|
||||
@@ -13,7 +12,6 @@ from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.core import (
|
||||
HomeAssistant,
|
||||
ServiceCall
|
||||
)
|
||||
from homeassistant.const import (
|
||||
Platform,
|
||||
@@ -50,8 +48,8 @@ from .const import CONF_PASSWORD as CONF_PASSWORD_KEY, CONF_SERVER as CONF_SERVE
|
||||
|
||||
PLATFORMS: list[Platform] = [
|
||||
Platform.BINARY_SENSOR,
|
||||
# Platform.SENSOR,
|
||||
# Platform.SWITCH,
|
||||
Platform.SENSOR,
|
||||
Platform.SWITCH,
|
||||
Platform.CLIMATE,
|
||||
Platform.SELECT,
|
||||
Platform.WATER_HEATER,
|
||||
@@ -85,16 +83,16 @@ async def load_device_config(hass: HomeAssistant, device_type, sn8):
|
||||
config_file = hass.config.path(f"{CONFIG_PATH}/{sn8}.json")
|
||||
raw = await hass.async_add_executor_job(_ensure_dir_and_load, config_dir, config_file)
|
||||
json_data = {}
|
||||
if isinstance(raw, dict) and len(raw) > 0:
|
||||
# 兼容两种文件结构:
|
||||
# 1) { "<sn8>": { ...mapping... } }
|
||||
# 2) { ...mapping... }(直接就是映射体)
|
||||
if sn8 in raw:
|
||||
json_data = raw.get(sn8) or {}
|
||||
else:
|
||||
# 如果像映射体(包含 entities/centralized 等关键字段),直接使用
|
||||
if any(k in raw for k in ["entities", "centralized", "queries", "manufacturer"]):
|
||||
json_data = raw
|
||||
# if isinstance(raw, dict) and len(raw) > 0:
|
||||
# # 兼容两种文件结构:
|
||||
# # 1) { "<sn8>": { ...mapping... } }
|
||||
# # 2) { ...mapping... }(直接就是映射体)
|
||||
# if sn8 in raw:
|
||||
# json_data = raw.get(sn8) or {}
|
||||
# else:
|
||||
# # 如果像映射体(包含 entities/centralized 等关键字段),直接使用
|
||||
# if any(k in raw for k in ["entities", "centralized", "queries", "manufacturer"]):
|
||||
# json_data = raw
|
||||
if not json_data:
|
||||
device_path = f".device_mapping.{'T0x%02X' % device_type}"
|
||||
try:
|
||||
@@ -103,12 +101,12 @@ async def load_device_config(hass: HomeAssistant, device_type, sn8):
|
||||
json_data = mapping_module.DEVICE_MAPPING[sn8]
|
||||
elif "default" in mapping_module.DEVICE_MAPPING:
|
||||
json_data = mapping_module.DEVICE_MAPPING["default"]
|
||||
if len(json_data) > 0:
|
||||
save_data = {sn8: json_data}
|
||||
# offload save_json as well
|
||||
await hass.async_add_executor_job(save_json, config_file, save_data)
|
||||
except ModuleNotFoundError:
|
||||
MideaLogger.warning(f"Can't load mapping file for type {'T0x%02X' % device_type}")
|
||||
|
||||
save_data = {sn8: json_data}
|
||||
# offload save_json as well
|
||||
await hass.async_add_executor_job(save_json, config_file, save_data)
|
||||
return json_data
|
||||
|
||||
async def update_listener(hass: HomeAssistant, config_entry: ConfigEntry):
|
||||
@@ -164,142 +162,137 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry):
|
||||
return False
|
||||
|
||||
# 拉取家庭与设备列表
|
||||
appliances = None
|
||||
first_home_id = None
|
||||
try:
|
||||
homes = await cloud.list_home()
|
||||
if homes and len(homes) > 0:
|
||||
first_home_id = list(homes.keys())[0]
|
||||
appliances = await cloud.list_appliances(first_home_id)
|
||||
else:
|
||||
appliances = await cloud.list_appliances(None)
|
||||
hass.data.setdefault(DOMAIN, {})
|
||||
hass.data[DOMAIN].setdefault("accounts", {})
|
||||
bucket = {"device_list": {}, "coordinator_map": {}}
|
||||
home_ids = list(homes.keys())
|
||||
|
||||
for home_id in home_ids:
|
||||
appliances = await cloud.list_appliances(home_id)
|
||||
if appliances is None:
|
||||
continue
|
||||
|
||||
# 为每台设备构建占位设备与协调器(不连接本地)
|
||||
for appliance_code, info in appliances.items():
|
||||
MideaLogger.debug(f"info={info} ")
|
||||
try:
|
||||
device = MiedaDevice(
|
||||
name=info.get(CONF_NAME) or info.get("name"),
|
||||
device_id=appliance_code,
|
||||
device_type=info.get(CONF_TYPE) or info.get("type"),
|
||||
ip_address=None,
|
||||
port=None,
|
||||
token=None,
|
||||
key=None,
|
||||
connected=info.get("online"),
|
||||
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"),
|
||||
)
|
||||
# 加载并应用设备映射(queries/centralized/calculate),并预置 attributes 键
|
||||
try:
|
||||
mapping = await load_device_config(
|
||||
hass,
|
||||
info.get(CONF_TYPE) or info.get("type"),
|
||||
info.get(CONF_SN8) or info.get("sn8"),
|
||||
) or {}
|
||||
except Exception:
|
||||
mapping = {}
|
||||
|
||||
try:
|
||||
device.set_queries(mapping.get("queries", []))
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
device.set_centralized(mapping.get("centralized", []))
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
device.set_calculate(mapping.get("calculate", {}))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 预置 attributes:包含 centralized 里声明的所有键、entities 中使用到的所有属性键
|
||||
try:
|
||||
preset_keys = set(mapping.get("centralized", []))
|
||||
entities_cfg = (mapping.get("entities") or {})
|
||||
# 收集实体配置中直接引用的属性键
|
||||
for platform_cfg in entities_cfg.values():
|
||||
if not isinstance(platform_cfg, dict):
|
||||
continue
|
||||
for _, ecfg in platform_cfg.items():
|
||||
if not isinstance(ecfg, dict):
|
||||
continue
|
||||
# 常见直接属性字段
|
||||
for k in [
|
||||
"power",
|
||||
"aux_heat",
|
||||
"current_temperature",
|
||||
"target_temperature",
|
||||
"oscillate",
|
||||
"min_temp",
|
||||
"max_temp",
|
||||
]:
|
||||
v = ecfg.get(k)
|
||||
if isinstance(v, str):
|
||||
preset_keys.add(v)
|
||||
elif isinstance(v, list):
|
||||
for vv in v:
|
||||
if isinstance(vv, str):
|
||||
preset_keys.add(vv)
|
||||
# 模式映射里的条件字段
|
||||
for map_key in [
|
||||
"hvac_modes",
|
||||
"preset_modes",
|
||||
"swing_modes",
|
||||
"fan_modes",
|
||||
"operation_list",
|
||||
"options",
|
||||
]:
|
||||
maps = ecfg.get(map_key) or {}
|
||||
if isinstance(maps, dict):
|
||||
for _, cond in maps.items():
|
||||
if isinstance(cond, dict):
|
||||
for attr_name in cond.keys():
|
||||
preset_keys.add(attr_name)
|
||||
# 传感器/开关等实体 key 本身也加入(其 key 即属性名)
|
||||
for platform_name, platform_cfg in entities_cfg.items():
|
||||
if not isinstance(platform_cfg, dict):
|
||||
continue
|
||||
platform_str = str(platform_name)
|
||||
if platform_str in [
|
||||
str(Platform.SENSOR),
|
||||
str(Platform.BINARY_SENSOR),
|
||||
str(Platform.SWITCH),
|
||||
str(Platform.FAN),
|
||||
str(Platform.SELECT),
|
||||
]:
|
||||
for entity_key in platform_cfg.keys():
|
||||
preset_keys.add(entity_key)
|
||||
# 写入默认空值
|
||||
for k in preset_keys:
|
||||
if k not in device.attributes:
|
||||
device.attributes[k] = None
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
coordinator = MideaDataUpdateCoordinator(hass, config_entry, device, cloud=cloud)
|
||||
# 后台刷新,避免初始化阻塞
|
||||
hass.async_create_task(coordinator.async_config_entry_first_refresh())
|
||||
bucket["device_list"][appliance_code] = info
|
||||
bucket["coordinator_map"][appliance_code] = coordinator
|
||||
except Exception as e:
|
||||
MideaLogger.error(f"Init device failed: {appliance_code}, error: {e}")
|
||||
hass.data[DOMAIN]["accounts"][config_entry.entry_id] = bucket
|
||||
|
||||
except Exception as e:
|
||||
MideaLogger.error(f"Fetch appliances failed: {e}")
|
||||
appliances = None
|
||||
|
||||
if appliances is None:
|
||||
appliances = {}
|
||||
|
||||
hass.data.setdefault(DOMAIN, {})
|
||||
hass.data[DOMAIN].setdefault("accounts", {})
|
||||
bucket = {"device_list": {}, "coordinator_map": {}, "cloud": cloud, "home_id": first_home_id}
|
||||
|
||||
# 为每台设备构建占位设备与协调器(不连接本地)
|
||||
for appliance_code, info in appliances.items():
|
||||
MideaLogger.debug(f"info={info} ")
|
||||
try:
|
||||
device = MiedaDevice(
|
||||
name=info.get(CONF_NAME) or info.get("name"),
|
||||
device_id=appliance_code,
|
||||
device_type=info.get(CONF_TYPE) or info.get("type"),
|
||||
ip_address=None,
|
||||
port=None,
|
||||
token=None,
|
||||
key=None,
|
||||
connected=info.get("online"),
|
||||
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"),
|
||||
)
|
||||
# 加载并应用设备映射(queries/centralized/calculate),并预置 attributes 键
|
||||
try:
|
||||
mapping = await load_device_config(
|
||||
hass,
|
||||
info.get(CONF_TYPE) or info.get("type"),
|
||||
info.get(CONF_SN8) or info.get("sn8"),
|
||||
) or {}
|
||||
except Exception:
|
||||
mapping = {}
|
||||
|
||||
try:
|
||||
device.set_queries(mapping.get("queries", []))
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
device.set_centralized(mapping.get("centralized", []))
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
device.set_calculate(mapping.get("calculate", {}))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 预置 attributes:包含 centralized 里声明的所有键、entities 中使用到的所有属性键
|
||||
try:
|
||||
preset_keys = set(mapping.get("centralized", []))
|
||||
entities_cfg = (mapping.get("entities") or {})
|
||||
# 收集实体配置中直接引用的属性键
|
||||
for platform_cfg in entities_cfg.values():
|
||||
if not isinstance(platform_cfg, dict):
|
||||
continue
|
||||
for _, ecfg in platform_cfg.items():
|
||||
if not isinstance(ecfg, dict):
|
||||
continue
|
||||
# 常见直接属性字段
|
||||
for k in [
|
||||
"power",
|
||||
"aux_heat",
|
||||
"current_temperature",
|
||||
"target_temperature",
|
||||
"oscillate",
|
||||
"min_temp",
|
||||
"max_temp",
|
||||
]:
|
||||
v = ecfg.get(k)
|
||||
if isinstance(v, str):
|
||||
preset_keys.add(v)
|
||||
elif isinstance(v, list):
|
||||
for vv in v:
|
||||
if isinstance(vv, str):
|
||||
preset_keys.add(vv)
|
||||
# 模式映射里的条件字段
|
||||
for map_key in [
|
||||
"hvac_modes",
|
||||
"preset_modes",
|
||||
"swing_modes",
|
||||
"fan_modes",
|
||||
"operation_list",
|
||||
"options",
|
||||
]:
|
||||
maps = ecfg.get(map_key) or {}
|
||||
if isinstance(maps, dict):
|
||||
for _, cond in maps.items():
|
||||
if isinstance(cond, dict):
|
||||
for attr_name in cond.keys():
|
||||
preset_keys.add(attr_name)
|
||||
# 传感器/开关等实体 key 本身也加入(其 key 即属性名)
|
||||
for platform_name, platform_cfg in entities_cfg.items():
|
||||
if not isinstance(platform_cfg, dict):
|
||||
continue
|
||||
platform_str = str(platform_name)
|
||||
if platform_str in [
|
||||
str(Platform.SENSOR),
|
||||
str(Platform.BINARY_SENSOR),
|
||||
str(Platform.SWITCH),
|
||||
str(Platform.FAN),
|
||||
str(Platform.SELECT),
|
||||
]:
|
||||
for entity_key in platform_cfg.keys():
|
||||
preset_keys.add(entity_key)
|
||||
# 写入默认空值
|
||||
for k in preset_keys:
|
||||
if k not in device.attributes:
|
||||
device.attributes[k] = None
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
coordinator = MideaDataUpdateCoordinator(hass, config_entry, device)
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
bucket["device_list"][appliance_code] = info
|
||||
bucket["coordinator_map"][appliance_code] = coordinator
|
||||
except Exception as e:
|
||||
MideaLogger.error(f"Init device failed: {appliance_code}, error: {e}")
|
||||
|
||||
hass.data[DOMAIN]["accounts"][config_entry.entry_id] = bucket
|
||||
|
||||
hass.async_create_task(hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS))
|
||||
await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS)
|
||||
return True
|
||||
|
||||
|
||||
|
||||
@@ -57,18 +57,17 @@ class MideaDeviceStatusSensorEntity(MideaEntity, BinarySensorEntity):
|
||||
device.sn,
|
||||
device.sn8,
|
||||
device.model,
|
||||
entity_key,
|
||||
device=device,
|
||||
manufacturer=manufacturer,
|
||||
rationale=rationale,
|
||||
config=config,
|
||||
)
|
||||
self._device = device
|
||||
self._manufacturer = manufacturer
|
||||
self._rationale = rationale
|
||||
self._entity_key = entity_key
|
||||
self._config = config
|
||||
|
||||
@property
|
||||
def entity_id_suffix(self) -> str:
|
||||
"""Return the suffix for entity ID."""
|
||||
return "status"
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
"""Return the device class."""
|
||||
@@ -102,6 +101,11 @@ class MideaBinarySensorEntity(MideaEntity, BinarySensorEntity):
|
||||
device.sn,
|
||||
device.sn8,
|
||||
device.model,
|
||||
entity_key,
|
||||
device=device,
|
||||
manufacturer=manufacturer,
|
||||
rationale=rationale,
|
||||
config=config,
|
||||
)
|
||||
self._device = device
|
||||
self._manufacturer = manufacturer
|
||||
@@ -109,11 +113,6 @@ class MideaBinarySensorEntity(MideaEntity, BinarySensorEntity):
|
||||
self._entity_key = entity_key
|
||||
self._config = config
|
||||
|
||||
@property
|
||||
def entity_id_suffix(self) -> str:
|
||||
"""Return the suffix for entity ID."""
|
||||
return f"binary_sensor_{self._entity_key}"
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return if the binary sensor is on."""
|
||||
|
||||
@@ -15,7 +15,6 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from .const import DOMAIN
|
||||
from .core.logger import MideaLogger
|
||||
from .midea_entity import MideaEntity
|
||||
from .midea_entities import Rationale
|
||||
from . import load_device_config
|
||||
|
||||
|
||||
@@ -44,9 +43,6 @@ async def async_setup_entry(
|
||||
coordinator = coordinator_map.get(device_id)
|
||||
device = coordinator.device if coordinator else None
|
||||
|
||||
|
||||
MideaLogger.debug(f"entities_cfg={entities_cfg} ")
|
||||
|
||||
for entity_key, ecfg in entities_cfg.items():
|
||||
devs.append(MideaClimateEntity(
|
||||
coordinator, device, manufacturer, rationale, entity_key, ecfg
|
||||
@@ -64,11 +60,15 @@ class MideaClimateEntity(MideaEntity, ClimateEntity):
|
||||
device.sn,
|
||||
device.sn8,
|
||||
device.model,
|
||||
entity_key,
|
||||
device=device,
|
||||
manufacturer=manufacturer,
|
||||
rationale=rationale,
|
||||
config=config,
|
||||
)
|
||||
self._device = device
|
||||
self._manufacturer = manufacturer
|
||||
self._rationale = rationale
|
||||
self._entity_key = entity_key
|
||||
self._config = config
|
||||
self._key_power = self._config.get("power")
|
||||
self._key_hvac_modes = self._config.get("hvac_modes")
|
||||
@@ -149,7 +149,7 @@ class MideaClimateEntity(MideaEntity, ClimateEntity):
|
||||
|
||||
@property
|
||||
def fan_mode(self):
|
||||
return self._dict_get_selected(self._key_fan_modes, Rationale.EQUALLY)
|
||||
return self._dict_get_selected(self._key_fan_modes, "EQUALLY")
|
||||
|
||||
@property
|
||||
def swing_modes(self):
|
||||
@@ -157,7 +157,7 @@ class MideaClimateEntity(MideaEntity, ClimateEntity):
|
||||
|
||||
@property
|
||||
def swing_mode(self):
|
||||
return self._dict_get_selected(self._key_swing_modes)
|
||||
return self._dict_get_selected(self._key_swing_modes, "EQUALLY")
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
@@ -165,7 +165,7 @@ class MideaClimateEntity(MideaEntity, ClimateEntity):
|
||||
|
||||
@property
|
||||
def hvac_mode(self):
|
||||
return self._dict_get_selected(self._key_hvac_modes)
|
||||
return self._dict_get_selected(self._key_hvac_modes, "EQUALLY")
|
||||
|
||||
@property
|
||||
def hvac_modes(self):
|
||||
@@ -236,7 +236,7 @@ class MideaClimateEntity(MideaEntity, ClimateEntity):
|
||||
return
|
||||
await self.async_set_attribute(key, value)
|
||||
|
||||
def _dict_get_selected(self, dict_config, rationale=Rationale.EQUALLY):
|
||||
def _dict_get_selected(self, dict_config, rationale="EQUALLY"):
|
||||
"""Get selected value from dictionary configuration."""
|
||||
if dict_config is None:
|
||||
return None
|
||||
@@ -251,15 +251,15 @@ class MideaClimateEntity(MideaEntity, ClimateEntity):
|
||||
if device_value is None:
|
||||
match = False
|
||||
break
|
||||
if rationale == Rationale.EQUALLY:
|
||||
if rationale == "EQUALLY":
|
||||
if device_value != attr_value:
|
||||
match = False
|
||||
break
|
||||
elif rationale == Rationale.LESS:
|
||||
elif rationale == "LESS":
|
||||
if device_value >= attr_value:
|
||||
match = False
|
||||
break
|
||||
elif rationale == Rationale.GREATER:
|
||||
elif rationale == "GREATER":
|
||||
if device_value <= attr_value:
|
||||
match = False
|
||||
break
|
||||
|
||||
@@ -60,7 +60,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
data_schema=vol.Schema({
|
||||
vol.Required(CONF_ACCOUNT): str,
|
||||
vol.Required(CONF_PASSWORD): str,
|
||||
vol.Required(CONF_SERVER, default=1): vol.In(CONF_SERVERS)
|
||||
vol.Required(CONF_SERVER, default=2): vol.In(CONF_SERVERS)
|
||||
}),
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
@@ -3,9 +3,9 @@ import time
|
||||
import datetime
|
||||
import json
|
||||
import base64
|
||||
from threading import Lock
|
||||
from aiohttp import ClientSession
|
||||
from secrets import token_hex
|
||||
from .logger import MideaLogger
|
||||
from .security import CloudSecurity, MeijuCloudSecurity, MSmartCloudSecurity
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -50,7 +50,6 @@ class MideaCloud:
|
||||
self._device_id = CloudSecurity.get_deviceid(account)
|
||||
self._session = session
|
||||
self._security = security
|
||||
self._api_lock = Lock()
|
||||
self._app_key = app_key
|
||||
self._account = account
|
||||
self._password = password
|
||||
@@ -86,16 +85,15 @@ class MideaCloud:
|
||||
"accesstoken": self._access_token
|
||||
})
|
||||
response:dict = {"code": -1}
|
||||
for i in range(0, 3):
|
||||
try:
|
||||
with self._api_lock:
|
||||
r = await self._session.request("POST", url, headers=header, data=dump_data, timeout=10)
|
||||
raw = await r.read()
|
||||
_LOGGER.debug(f"Midea cloud API url: {url}, data: {data}, response: {raw}")
|
||||
response = json.loads(raw)
|
||||
break
|
||||
except Exception as e:
|
||||
pass
|
||||
_LOGGER.debug(f"Midea cloud API header: {header}")
|
||||
_LOGGER.debug(f"Midea cloud API dump_data: {dump_data}")
|
||||
try:
|
||||
r = await self._session.request("POST", url, headers=header, data=dump_data, timeout=5)
|
||||
raw = await r.read()
|
||||
_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"]
|
||||
@@ -251,7 +249,7 @@ class MeijuCloud(MideaCloud):
|
||||
"sn": self._security.aes_decrypt(appliance.get("sn")) if appliance.get("sn") else "",
|
||||
"sn8": appliance.get("sn8", "00000000"),
|
||||
"model_number": appliance.get("modelNumber", "0"),
|
||||
"manufacturer_code":appliance.get("enterpriseCode", "0000"),
|
||||
"manufacturer_code": appliance.get("enterpriseCode", "0000"),
|
||||
"model": appliance.get("productModel"),
|
||||
"online": appliance.get("onlineStatus") == "1",
|
||||
}
|
||||
@@ -267,7 +265,7 @@ class MeijuCloud(MideaCloud):
|
||||
data = {
|
||||
"applianceCode": str(appliance_code),
|
||||
"command": {
|
||||
"query": {"query_type": "total_query"}
|
||||
"query": {}
|
||||
}
|
||||
}
|
||||
if response := await self._api_request(
|
||||
|
||||
@@ -10,7 +10,6 @@ from homeassistant.helpers.event import async_call_later
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
||||
|
||||
from .core.device import MiedaDevice
|
||||
from .const import DOMAIN
|
||||
from .core.logger import MideaLogger
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -31,6 +30,7 @@ class MideaDataUpdateCoordinator(DataUpdateCoordinator[MideaDeviceData]):
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
device: MiedaDevice,
|
||||
cloud=None,
|
||||
) -> None:
|
||||
"""Initialize the coordinator."""
|
||||
super().__init__(
|
||||
@@ -45,6 +45,7 @@ class MideaDataUpdateCoordinator(DataUpdateCoordinator[MideaDeviceData]):
|
||||
self.device = device
|
||||
self.state_update_muted: CALLBACK_TYPE | None = None
|
||||
self._device_id = device.device_id
|
||||
self._cloud = cloud
|
||||
|
||||
async def _async_setup(self) -> None:
|
||||
"""Set up the coordinator."""
|
||||
@@ -89,9 +90,8 @@ class MideaDataUpdateCoordinator(DataUpdateCoordinator[MideaDeviceData]):
|
||||
return self.data
|
||||
|
||||
try:
|
||||
# 尝试账号模式下的云端轮询(如果 cloud 存在且支持)
|
||||
account_bucket = self.hass.data.get(DOMAIN, {}).get("accounts", {}).get(self.config_entry.entry_id)
|
||||
cloud = account_bucket.get("cloud") if account_bucket else None
|
||||
# 使用传入的 cloud 实例(若可用)
|
||||
cloud = self._cloud
|
||||
if cloud and hasattr(cloud, "get_device_status"):
|
||||
try:
|
||||
status = await cloud.get_device_status(self._device_id)
|
||||
@@ -120,8 +120,7 @@ class MideaDataUpdateCoordinator(DataUpdateCoordinator[MideaDeviceData]):
|
||||
async def async_set_attribute(self, attribute: str, value) -> None:
|
||||
"""Set a device attribute."""
|
||||
# 云端控制:构造 control 与 status(携带当前状态作为上下文)
|
||||
account_bucket = self.hass.data.get(DOMAIN, {}).get("accounts", {}).get(self.config_entry.entry_id)
|
||||
cloud = account_bucket.get("cloud") if account_bucket else None
|
||||
cloud = self._cloud
|
||||
control = {attribute: value}
|
||||
status = dict(self.device.attributes)
|
||||
if cloud and hasattr(cloud, "send_device_control"):
|
||||
@@ -134,8 +133,7 @@ class MideaDataUpdateCoordinator(DataUpdateCoordinator[MideaDeviceData]):
|
||||
|
||||
async def async_set_attributes(self, attributes: dict) -> None:
|
||||
"""Set multiple device attributes."""
|
||||
account_bucket = self.hass.data.get(DOMAIN, {}).get("accounts", {}).get(self.config_entry.entry_id)
|
||||
cloud = account_bucket.get("cloud") if account_bucket else None
|
||||
cloud = self._cloud
|
||||
control = dict(attributes)
|
||||
status = dict(self.device.attributes)
|
||||
if cloud and hasattr(cloud, "send_device_control"):
|
||||
|
||||
@@ -15,7 +15,6 @@ DEVICE_MAPPING = {
|
||||
"entities": {
|
||||
Platform.CLIMATE: {
|
||||
"thermostat": {
|
||||
"name": "Thermostat",
|
||||
"power": "power",
|
||||
"hvac_modes": {
|
||||
"off": {"power": "off"},
|
||||
@@ -96,7 +95,6 @@ DEVICE_MAPPING = {
|
||||
"entities": {
|
||||
Platform.CLIMATE: {
|
||||
"thermostat": {
|
||||
"name": "Thermostat",
|
||||
"power": "power",
|
||||
"hvac_modes": {
|
||||
"off": {"power": "off"},
|
||||
|
||||
159
custom_components/midea_auto_cloud/device_mapping/T0xB8.py
Normal file
159
custom_components/midea_auto_cloud/device_mapping/T0xB8.py
Normal file
@@ -0,0 +1,159 @@
|
||||
from homeassistant.const import Platform, UnitOfTemperature, PRECISION_HALVES, UnitOfTime, UnitOfArea, UnitOfVolume
|
||||
from homeassistant.components.sensor import SensorStateClass, SensorDeviceClass
|
||||
from homeassistant.components.binary_sensor import BinarySensorDeviceClass
|
||||
from homeassistant.components.switch import SwitchDeviceClass
|
||||
|
||||
DEVICE_MAPPING = {
|
||||
"default": {
|
||||
"rationale": ["off", "on"],
|
||||
"queries": [{}],
|
||||
"centralized": [],
|
||||
"entities": {
|
||||
Platform.SELECT: {
|
||||
"fan_level": {
|
||||
"options": {
|
||||
"low": {"fan_level": "low"},
|
||||
"medium": {"fan_level": "medium"},
|
||||
"high": {"fan_level": "high"},
|
||||
"auto": {"fan_level": "auto"}
|
||||
}
|
||||
},
|
||||
"work_mode": {
|
||||
"options": {
|
||||
"none": {"work_mode": "none"},
|
||||
"auto": {"work_mode": "auto"},
|
||||
"spot": {"work_mode": "spot"},
|
||||
"edge": {"work_mode": "edge"},
|
||||
"single_room": {"work_mode": "single_room"},
|
||||
"custom": {"work_mode": "custom"}
|
||||
}
|
||||
},
|
||||
"work_status": {
|
||||
"options": {
|
||||
"idle": {"work_status": "idle"},
|
||||
"cleaning": {"work_status": "cleaning"},
|
||||
"returning": {"work_status": "returning"},
|
||||
"docked": {"work_status": "docked"},
|
||||
"on_base": {"work_status": "on_base"},
|
||||
"charging": {"work_status": "charging"},
|
||||
"error": {"work_status": "error"}
|
||||
}
|
||||
},
|
||||
"move_direction": {
|
||||
"options": {
|
||||
"none": {"move_direction": "none"},
|
||||
"forward": {"move_direction": "forward"},
|
||||
"backward": {"move_direction": "backward"},
|
||||
"left": {"move_direction": "left"},
|
||||
"right": {"move_direction": "right"}
|
||||
}
|
||||
},
|
||||
"query_type": {
|
||||
"options": {
|
||||
"work": {"query_type": "work"},
|
||||
"status": {"query_type": "status"},
|
||||
"battery": {"query_type": "battery"},
|
||||
"error": {"query_type": "error"}
|
||||
}
|
||||
},
|
||||
"sub_work_status": {
|
||||
"options": {
|
||||
"idle": {"sub_work_status": "idle"},
|
||||
"cleaning": {"sub_work_status": "cleaning"},
|
||||
"charging": {"sub_work_status": "charging"},
|
||||
"charge_finish": {"sub_work_status": "charge_finish"},
|
||||
"error": {"sub_work_status": "error"}
|
||||
}
|
||||
},
|
||||
"water_level": {
|
||||
"options": {
|
||||
"low": {"water_level": "low"},
|
||||
"normal": {"water_level": "normal"},
|
||||
"high": {"water_level": "high"}
|
||||
}
|
||||
},
|
||||
"mop_status": {
|
||||
"options": {
|
||||
"normal": {"mop_status": "normal"},
|
||||
"lack_water": {"mop_status": "lack_water"},
|
||||
"full_water": {"mop_status": "full_water"}
|
||||
}
|
||||
},
|
||||
"error_type": {
|
||||
"options": {
|
||||
"no_error": {"error_type": "no_error"},
|
||||
"can_fix": {"error_type": "can_fix"},
|
||||
"need_help": {"error_type": "need_help"}
|
||||
}
|
||||
},
|
||||
"control_type": {
|
||||
"options": {
|
||||
"none": {"control_type": "none"},
|
||||
"app": {"control_type": "app"},
|
||||
"remote": {"control_type": "remote"},
|
||||
"auto": {"control_type": "auto"}
|
||||
}
|
||||
}
|
||||
},
|
||||
Platform.BINARY_SENSOR: {
|
||||
"carpet_switch": {
|
||||
"device_class": BinarySensorDeviceClass.RUNNING,
|
||||
},
|
||||
"have_reserve_task": {
|
||||
"device_class": BinarySensorDeviceClass.RUNNING,
|
||||
}
|
||||
},
|
||||
Platform.SENSOR: {
|
||||
"dust_count": {
|
||||
"device_class": SensorDeviceClass.ENUM,
|
||||
"state_class": SensorStateClass.MEASUREMENT
|
||||
},
|
||||
"area": {
|
||||
"device_class": SensorDeviceClass.AREA,
|
||||
"unit_of_measurement": UnitOfArea.SQUARE_METERS,
|
||||
"state_class": SensorStateClass.MEASUREMENT
|
||||
},
|
||||
"voice_level": {
|
||||
"device_class": SensorDeviceClass.BATTERY,
|
||||
"unit_of_measurement": "%",
|
||||
"state_class": SensorStateClass.MEASUREMENT
|
||||
},
|
||||
"switch_status": {
|
||||
"device_class": SensorDeviceClass.ENUM,
|
||||
"state_class": SensorStateClass.MEASUREMENT
|
||||
},
|
||||
"water_station_status": {
|
||||
"device_class": SensorDeviceClass.ENUM,
|
||||
"state_class": SensorStateClass.MEASUREMENT
|
||||
},
|
||||
"work_time": {
|
||||
"device_class": SensorDeviceClass.DURATION,
|
||||
"unit_of_measurement": UnitOfTime.MINUTES,
|
||||
"state_class": SensorStateClass.MEASUREMENT
|
||||
},
|
||||
"battery_percent": {
|
||||
"device_class": SensorDeviceClass.BATTERY,
|
||||
"unit_of_measurement": "%",
|
||||
"state_class": SensorStateClass.MEASUREMENT
|
||||
},
|
||||
"planner_status": {
|
||||
"device_class": SensorDeviceClass.ENUM,
|
||||
"state_class": SensorStateClass.MEASUREMENT
|
||||
},
|
||||
"sweep_then_mop_mode_progress": {
|
||||
"device_class": SensorDeviceClass.BATTERY,
|
||||
"unit_of_measurement": "%",
|
||||
"state_class": SensorStateClass.MEASUREMENT
|
||||
},
|
||||
"error_desc": {
|
||||
"device_class": SensorDeviceClass.ENUM,
|
||||
"state_class": SensorStateClass.MEASUREMENT
|
||||
},
|
||||
"station_error_desc": {
|
||||
"device_class": SensorDeviceClass.ENUM,
|
||||
"state_class": SensorStateClass.MEASUREMENT
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
103
custom_components/midea_auto_cloud/device_mapping/T0xE1.py
Normal file
103
custom_components/midea_auto_cloud/device_mapping/T0xE1.py
Normal file
@@ -0,0 +1,103 @@
|
||||
from homeassistant.components.binary_sensor import BinarySensorDeviceClass
|
||||
from homeassistant.const import Platform, UnitOfTemperature, PRECISION_HALVES, UnitOfTime
|
||||
from homeassistant.components.sensor import SensorStateClass, SensorDeviceClass
|
||||
from homeassistant.components.switch import SwitchDeviceClass
|
||||
|
||||
DEVICE_MAPPING = {
|
||||
"default": {
|
||||
"rationale": ["off", "on"],
|
||||
"queries": [{}],
|
||||
"centralized": [],
|
||||
"entities": {
|
||||
Platform.SWITCH: {
|
||||
"airswitch": {
|
||||
"device_class": SwitchDeviceClass.SWITCH,
|
||||
},
|
||||
"waterswitch": {
|
||||
"device_class": SwitchDeviceClass.SWITCH,
|
||||
},
|
||||
"uvswitch": {
|
||||
"device_class": SwitchDeviceClass.SWITCH,
|
||||
},
|
||||
"doorswitch": {
|
||||
"device_class": SwitchDeviceClass.SWITCH,
|
||||
},
|
||||
"dryswitch": {
|
||||
"device_class": SwitchDeviceClass.SWITCH,
|
||||
},
|
||||
"dry_step_switch": {
|
||||
"device_class": SwitchDeviceClass.SWITCH,
|
||||
}
|
||||
},
|
||||
Platform.BINARY_SENSOR: {
|
||||
"air_status": {
|
||||
"device_class": BinarySensorDeviceClass.RUNNING,
|
||||
},
|
||||
"water_lack": {
|
||||
"device_class": BinarySensorDeviceClass.RUNNING,
|
||||
},
|
||||
"softwater_lack": {
|
||||
"device_class": BinarySensorDeviceClass.RUNNING,
|
||||
},
|
||||
"wash_stage":{
|
||||
"device_class": BinarySensorDeviceClass.RUNNING,
|
||||
},
|
||||
"bright_lack": {
|
||||
"device_class": BinarySensorDeviceClass.RUNNING,
|
||||
},
|
||||
"diy_flag": {
|
||||
"device_class": BinarySensorDeviceClass.RUNNING,
|
||||
},
|
||||
"diy_main_wash": {
|
||||
"device_class": BinarySensorDeviceClass.RUNNING,
|
||||
},
|
||||
"diy_piao_wash": {
|
||||
"device_class": BinarySensorDeviceClass.RUNNING,
|
||||
},
|
||||
"diy_times": {
|
||||
"device_class": BinarySensorDeviceClass.RUNNING,
|
||||
},
|
||||
},
|
||||
Platform.SELECT: {
|
||||
"work_status": {
|
||||
"options": {
|
||||
"power_off": {"work_status": "power_off" },
|
||||
"power_on": {"work_status": "power_on" },
|
||||
}
|
||||
},
|
||||
},
|
||||
Platform.SENSOR: {
|
||||
"bright": {
|
||||
"device_class": SensorDeviceClass.ILLUMINANCE,
|
||||
"unit_of_measurement": "lx",
|
||||
"state_class": SensorStateClass.MEASUREMENT
|
||||
},
|
||||
"temperature": {
|
||||
"device_class": SensorDeviceClass.TEMPERATURE,
|
||||
"unit_of_measurement": UnitOfTemperature.CELSIUS,
|
||||
"state_class": SensorStateClass.MEASUREMENT
|
||||
},
|
||||
"softwater": {
|
||||
"device_class": SensorDeviceClass.TEMPERATURE,
|
||||
"unit_of_measurement": UnitOfTemperature.CELSIUS,
|
||||
"state_class": SensorStateClass.MEASUREMENT
|
||||
},
|
||||
"left_time": {
|
||||
"device_class": SensorDeviceClass.DURATION,
|
||||
"unit_of_measurement": UnitOfTime.HOURS,
|
||||
"state_class": SensorStateClass.MEASUREMENT
|
||||
},
|
||||
"air_set_hour": {
|
||||
"device_class": SensorDeviceClass.DURATION,
|
||||
"unit_of_measurement": UnitOfTime.HOURS,
|
||||
"state_class": SensorStateClass.MEASUREMENT
|
||||
},
|
||||
"air_left_hour": {
|
||||
"device_class": SensorDeviceClass.DURATION,
|
||||
"unit_of_measurement": UnitOfTime.HOURS,
|
||||
"state_class": SensorStateClass.MEASUREMENT
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
235
custom_components/midea_auto_cloud/device_mapping/T0xED.py
Normal file
235
custom_components/midea_auto_cloud/device_mapping/T0xED.py
Normal file
@@ -0,0 +1,235 @@
|
||||
from homeassistant.const import Platform, UnitOfTemperature, PRECISION_HALVES, UnitOfTime, UnitOfElectricPotential, UnitOfVolume, UnitOfMass
|
||||
from homeassistant.components.sensor import SensorStateClass, SensorDeviceClass
|
||||
from homeassistant.components.binary_sensor import BinarySensorDeviceClass
|
||||
from homeassistant.components.switch import SwitchDeviceClass
|
||||
|
||||
DEVICE_MAPPING = {
|
||||
"default": {
|
||||
"rationale": ["off", "on"],
|
||||
"queries": [{}],
|
||||
"centralized": [],
|
||||
"entities": {
|
||||
Platform.SWITCH: {
|
||||
"holiday_mode": {
|
||||
"device_class": SwitchDeviceClass.SWITCH,
|
||||
},
|
||||
"water_way": {
|
||||
"device_class": SwitchDeviceClass.SWITCH,
|
||||
},
|
||||
"soften": {
|
||||
"device_class": SwitchDeviceClass.SWITCH,
|
||||
},
|
||||
"leak_water_protection": {
|
||||
"device_class": SwitchDeviceClass.SWITCH,
|
||||
},
|
||||
"cl_sterilization": {
|
||||
"device_class": SwitchDeviceClass.SWITCH,
|
||||
},
|
||||
"micro_leak": {
|
||||
"device_class": SwitchDeviceClass.SWITCH,
|
||||
},
|
||||
"low_salt": {
|
||||
"device_class": SwitchDeviceClass.SWITCH,
|
||||
},
|
||||
"no_salt": {
|
||||
"device_class": SwitchDeviceClass.SWITCH,
|
||||
},
|
||||
"low_battery": {
|
||||
"device_class": SwitchDeviceClass.SWITCH,
|
||||
},
|
||||
"salt_level_sensor_error": {
|
||||
"device_class": SwitchDeviceClass.SWITCH,
|
||||
},
|
||||
"flowmeter_error": {
|
||||
"device_class": SwitchDeviceClass.SWITCH,
|
||||
},
|
||||
"leak_water": {
|
||||
"device_class": SwitchDeviceClass.SWITCH,
|
||||
},
|
||||
"micro_leak_protection": {
|
||||
"device_class": SwitchDeviceClass.SWITCH,
|
||||
},
|
||||
"maintenance_reminder_switch": {
|
||||
"device_class": SwitchDeviceClass.SWITCH,
|
||||
},
|
||||
"rsj_stand_by": {
|
||||
"device_class": SwitchDeviceClass.SWITCH,
|
||||
},
|
||||
"regeneration": {
|
||||
"device_class": SwitchDeviceClass.SWITCH,
|
||||
},
|
||||
"pre_regeneration": {
|
||||
"device_class": SwitchDeviceClass.SWITCH,
|
||||
}
|
||||
},
|
||||
Platform.BINARY_SENSOR: {
|
||||
"maintenance_remind": {
|
||||
"device_class": BinarySensorDeviceClass.PROBLEM,
|
||||
},
|
||||
"chlorine_sterilization_error": {
|
||||
"device_class": BinarySensorDeviceClass.PROBLEM,
|
||||
},
|
||||
"rtc_error": {
|
||||
"device_class": BinarySensorDeviceClass.PROBLEM,
|
||||
}
|
||||
},
|
||||
Platform.SENSOR: {
|
||||
"micro_leak_protection_value": {
|
||||
"device_class": SensorDeviceClass.PRESSURE,
|
||||
"state_class": SensorStateClass.MEASUREMENT
|
||||
},
|
||||
"regeneration_current_stages": {
|
||||
"device_class": SensorDeviceClass.ENUM,
|
||||
"state_class": SensorStateClass.MEASUREMENT
|
||||
},
|
||||
"water_hardness": {
|
||||
"device_class": SensorDeviceClass.WATER,
|
||||
"state_class": SensorStateClass.MEASUREMENT
|
||||
},
|
||||
"timing_regeneration_hour": {
|
||||
"device_class": SensorDeviceClass.DURATION,
|
||||
"unit_of_measurement": UnitOfTime.HOURS,
|
||||
"state_class": SensorStateClass.MEASUREMENT
|
||||
},
|
||||
"real_time_setting_hour": {
|
||||
"device_class": SensorDeviceClass.DURATION,
|
||||
"unit_of_measurement": UnitOfTime.HOURS,
|
||||
"state_class": SensorStateClass.MEASUREMENT
|
||||
},
|
||||
"timing_regeneration_min": {
|
||||
"device_class": SensorDeviceClass.DURATION,
|
||||
"unit_of_measurement": UnitOfTime.MINUTES,
|
||||
"state_class": SensorStateClass.MEASUREMENT
|
||||
},
|
||||
"regeneration_left_seconds": {
|
||||
"device_class": SensorDeviceClass.DURATION,
|
||||
"unit_of_measurement": UnitOfTime.SECONDS,
|
||||
"state_class": SensorStateClass.MEASUREMENT
|
||||
},
|
||||
"maintenance_reminder_setting": {
|
||||
"device_class": SensorDeviceClass.ENUM,
|
||||
"state_class": SensorStateClass.MEASUREMENT
|
||||
},
|
||||
"mixed_water_gear": {
|
||||
"device_class": SensorDeviceClass.ENUM,
|
||||
"state_class": SensorStateClass.MEASUREMENT
|
||||
},
|
||||
"use_days": {
|
||||
"device_class": SensorDeviceClass.DURATION,
|
||||
"unit_of_measurement": UnitOfTime.DAYS,
|
||||
"state_class": SensorStateClass.MEASUREMENT
|
||||
},
|
||||
"days_since_last_regeneration": {
|
||||
"device_class": SensorDeviceClass.DURATION,
|
||||
"unit_of_measurement": UnitOfTime.DAYS,
|
||||
"state_class": SensorStateClass.MEASUREMENT
|
||||
},
|
||||
"velocity": {
|
||||
"device_class": SensorDeviceClass.SPEED,
|
||||
"state_class": SensorStateClass.MEASUREMENT
|
||||
},
|
||||
"supply_voltage": {
|
||||
"device_class": SensorDeviceClass.VOLTAGE,
|
||||
"unit_of_measurement": UnitOfElectricPotential.VOLT,
|
||||
"state_class": SensorStateClass.MEASUREMENT
|
||||
},
|
||||
"left_salt": {
|
||||
"device_class": SensorDeviceClass.WEIGHT,
|
||||
"unit_of_measurement": UnitOfMass.KILOGRAMS,
|
||||
"state_class": SensorStateClass.MEASUREMENT
|
||||
},
|
||||
"pre_regeneration_days": {
|
||||
"device_class": SensorDeviceClass.DURATION,
|
||||
"unit_of_measurement": UnitOfTime.DAYS,
|
||||
"state_class": SensorStateClass.MEASUREMENT
|
||||
},
|
||||
"flushing_days": {
|
||||
"device_class": SensorDeviceClass.DURATION,
|
||||
"unit_of_measurement": UnitOfTime.DAYS,
|
||||
"state_class": SensorStateClass.MEASUREMENT
|
||||
},
|
||||
"salt_setting": {
|
||||
"device_class": SensorDeviceClass.ENUM,
|
||||
"state_class": SensorStateClass.MEASUREMENT
|
||||
},
|
||||
"regeneration_count": {
|
||||
"device_class": SensorDeviceClass.ENUM,
|
||||
"state_class": SensorStateClass.MEASUREMENT
|
||||
},
|
||||
"battery_voltage": {
|
||||
"device_class": SensorDeviceClass.VOLTAGE,
|
||||
"unit_of_measurement": UnitOfElectricPotential.VOLT,
|
||||
"state_class": SensorStateClass.MEASUREMENT
|
||||
},
|
||||
"error": {
|
||||
"device_class": SensorDeviceClass.ENUM,
|
||||
"state_class": SensorStateClass.MEASUREMENT
|
||||
},
|
||||
"days_since_last_two_regeneration": {
|
||||
"device_class": SensorDeviceClass.DURATION,
|
||||
"unit_of_measurement": UnitOfTime.DAYS,
|
||||
"state_class": SensorStateClass.MEASUREMENT
|
||||
},
|
||||
"remind_maintenance_days": {
|
||||
"device_class": SensorDeviceClass.DURATION,
|
||||
"unit_of_measurement": UnitOfTime.DAYS,
|
||||
"state_class": SensorStateClass.MEASUREMENT
|
||||
},
|
||||
"real_date_setting_year": {
|
||||
"device_class": SensorDeviceClass.ENUM,
|
||||
"state_class": SensorStateClass.MEASUREMENT
|
||||
},
|
||||
"real_date_setting_month": {
|
||||
"device_class": SensorDeviceClass.ENUM,
|
||||
"state_class": SensorStateClass.MEASUREMENT
|
||||
},
|
||||
"real_date_setting_day": {
|
||||
"device_class": SensorDeviceClass.ENUM,
|
||||
"state_class": SensorStateClass.MEASUREMENT
|
||||
},
|
||||
"category": {
|
||||
"device_class": SensorDeviceClass.ENUM,
|
||||
"state_class": SensorStateClass.MEASUREMENT
|
||||
},
|
||||
"real_time_setting_min": {
|
||||
"device_class": SensorDeviceClass.DURATION,
|
||||
"unit_of_measurement": UnitOfTime.MINUTES,
|
||||
"state_class": SensorStateClass.MEASUREMENT
|
||||
},
|
||||
"regeneration_stages": {
|
||||
"device_class": SensorDeviceClass.ENUM,
|
||||
"state_class": SensorStateClass.MEASUREMENT
|
||||
},
|
||||
"soft_available_big": {
|
||||
"device_class": SensorDeviceClass.VOLUME,
|
||||
"unit_of_measurement": UnitOfVolume.LITERS,
|
||||
"state_class": SensorStateClass.MEASUREMENT
|
||||
},
|
||||
"water_consumption_big": {
|
||||
"device_class": SensorDeviceClass.VOLUME,
|
||||
"unit_of_measurement": UnitOfVolume.LITERS,
|
||||
"state_class": SensorStateClass.MEASUREMENT
|
||||
},
|
||||
"water_consumption_today": {
|
||||
"device_class": SensorDeviceClass.VOLUME,
|
||||
"unit_of_measurement": UnitOfVolume.LITERS,
|
||||
"state_class": SensorStateClass.MEASUREMENT
|
||||
},
|
||||
"water_consumption_average": {
|
||||
"device_class": SensorDeviceClass.VOLUME,
|
||||
"unit_of_measurement": UnitOfVolume.LITERS,
|
||||
"state_class": SensorStateClass.MEASUREMENT
|
||||
},
|
||||
"salt_alarm_threshold": {
|
||||
"device_class": SensorDeviceClass.WEIGHT,
|
||||
"unit_of_measurement": UnitOfMass.KILOGRAMS,
|
||||
"state_class": SensorStateClass.MEASUREMENT
|
||||
},
|
||||
"leak_water_protection_value": {
|
||||
"device_class": SensorDeviceClass.PRESSURE,
|
||||
"state_class": SensorStateClass.MEASUREMENT
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
from homeassistant.components.fan import FanEntity, FanEntityFeature
|
||||
from homeassistant.const import Platform
|
||||
from .const import DOMAIN
|
||||
from .midea_entities import MideaEntity
|
||||
from .midea_entity import MideaEntity
|
||||
from . import load_device_config
|
||||
|
||||
|
||||
@@ -24,13 +24,30 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
coordinator = coordinator_map.get(device_id)
|
||||
device = coordinator.device if coordinator else None
|
||||
for entity_key, ecfg in entities_cfg.items():
|
||||
devs.append(MideaFanEntity(device, manufacturer, rationale, entity_key, ecfg))
|
||||
devs.append(MideaFanEntity(coordinator, device, manufacturer, rationale, entity_key, ecfg))
|
||||
async_add_entities(devs)
|
||||
|
||||
|
||||
class MideaFanEntity(MideaEntity, FanEntity):
|
||||
def __init__(self, device, manufacturer, rationale, entity_key, config):
|
||||
super().__init__(device, manufacturer, rationale, entity_key, config)
|
||||
def __init__(self, coordinator, device, manufacturer, rationale, entity_key, config):
|
||||
super().__init__(
|
||||
coordinator,
|
||||
device.device_id,
|
||||
device.device_name,
|
||||
f"T0x{device.device_type:02X}",
|
||||
device.sn,
|
||||
device.sn8,
|
||||
device.model,
|
||||
entity_key,
|
||||
device=device,
|
||||
manufacturer=manufacturer,
|
||||
rationale=rationale,
|
||||
config=config,
|
||||
)
|
||||
self._device = device
|
||||
self._manufacturer = manufacturer
|
||||
self._rationale = rationale
|
||||
self._config = config
|
||||
self._key_power = self._config.get("power")
|
||||
self._key_preset_modes = self._config.get("preset_modes")
|
||||
self._key_speeds = self._config.get("speeds")
|
||||
@@ -74,41 +91,48 @@ class MideaFanEntity(MideaEntity, FanEntity):
|
||||
def oscillating(self):
|
||||
return self._get_status_on_off(self._key_oscillate)
|
||||
|
||||
def turn_on(
|
||||
async def async_turn_on(
|
||||
self,
|
||||
percentage: int | None = None,
|
||||
preset_mode: str | None = None,
|
||||
**kwargs,
|
||||
):
|
||||
if preset_mode is not None:
|
||||
new_status = self._key_preset_modes.get(preset_mode)
|
||||
else:
|
||||
new_status = {}
|
||||
if percentage is not None:
|
||||
new_status = {}
|
||||
if preset_mode is not None and self._key_preset_modes is not None:
|
||||
new_status.update(self._key_preset_modes.get(preset_mode, {}))
|
||||
if percentage is not None and self._key_speeds:
|
||||
index = round(percentage * self._attr_speed_count / 100) - 1
|
||||
index = max(0, min(index, len(self._key_speeds) - 1))
|
||||
new_status.update(self._key_speeds[index])
|
||||
new_status[self._key_power] = self._rationale[1]
|
||||
self._device.set_attributes(new_status)
|
||||
if self._key_power is not None:
|
||||
new_status[self._key_power] = True
|
||||
if new_status:
|
||||
await self.async_set_attributes(new_status)
|
||||
|
||||
def turn_off(self):
|
||||
self._set_status_on_off(self._key_power, False)
|
||||
async def async_turn_off(self):
|
||||
await self._async_set_status_on_off(self._key_power, False)
|
||||
|
||||
def set_percentage(self, percentage: int):
|
||||
async def async_set_percentage(self, percentage: int):
|
||||
if not self._key_speeds:
|
||||
return
|
||||
index = round(percentage * self._attr_speed_count / 100)
|
||||
if 0 < index < len(self._key_speeds):
|
||||
if 0 < index <= len(self._key_speeds):
|
||||
new_status = self._key_speeds[index - 1]
|
||||
self._device.set_attributes(new_status)
|
||||
await self.async_set_attributes(new_status)
|
||||
|
||||
def set_preset_mode(self, preset_mode: str):
|
||||
async def async_set_preset_mode(self, preset_mode: str):
|
||||
if not self._key_preset_modes:
|
||||
return
|
||||
new_status = self._key_preset_modes.get(preset_mode)
|
||||
self._device.set_attributes(new_status)
|
||||
if new_status:
|
||||
await self.async_set_attributes(new_status)
|
||||
|
||||
def oscillate(self, oscillating: bool):
|
||||
async def async_oscillate(self, oscillating: bool):
|
||||
if self.oscillating != oscillating:
|
||||
self._set_status_on_off(self._key_oscillate, oscillating)
|
||||
await self._async_set_status_on_off(self._key_oscillate, oscillating)
|
||||
|
||||
def update_state(self, status):
|
||||
try:
|
||||
self.schedule_update_ha_state()
|
||||
except Exception as e:
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"iot_class": "cloud_push",
|
||||
"issue_tracker": "https://github.com/sususweet/midea-meiju-codec/issues",
|
||||
"requirements": [],
|
||||
"version": "v0.0.1"
|
||||
"version": "v0.0.3"
|
||||
}
|
||||
@@ -1,130 +0,0 @@
|
||||
from enum import IntEnum
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.const import (
|
||||
STATE_ON,
|
||||
STATE_OFF
|
||||
)
|
||||
from .const import DOMAIN
|
||||
from .core.logger import MideaLogger
|
||||
|
||||
|
||||
class Rationale(IntEnum):
|
||||
EQUALLY = 0
|
||||
GREATER = 1
|
||||
LESS = 2
|
||||
|
||||
|
||||
class MideaEntity(Entity):
|
||||
def __init__(self, device, manufacturer: str | None, rationale: list | None, entity_key: str, config: dict):
|
||||
self._device = device
|
||||
self._device.register_update(self.update_state)
|
||||
self._entity_key = entity_key
|
||||
self._config = config
|
||||
self._device_name = self._device.device_name
|
||||
self._rationale = rationale
|
||||
if rationale_local := config.get("rationale"):
|
||||
self._rationale = rationale_local
|
||||
if self._rationale is None:
|
||||
self._rationale = ["off", "on"]
|
||||
self._attr_native_unit_of_measurement = self._config.get("unit_of_measurement")
|
||||
self._attr_device_class = self._config.get("device_class")
|
||||
self._attr_state_class = self._config.get("state_class")
|
||||
self._attr_icon = self._config.get("icon")
|
||||
self._attr_unique_id = f"{DOMAIN}.{self._device.device_id}_{self._entity_key}"
|
||||
self._attr_device_info = {
|
||||
"manufacturer": "Midea" if manufacturer is None else manufacturer,
|
||||
"model": f"{self._device.model}",
|
||||
"identifiers": {(DOMAIN, self._device.device_id)},
|
||||
"name": self._device_name
|
||||
}
|
||||
name = self._config.get("name")
|
||||
if name is None:
|
||||
name = self._entity_key.replace("_", " ").title()
|
||||
self._attr_name = f"{self._device_name} {name}"
|
||||
self.entity_id = self._attr_unique_id
|
||||
|
||||
@property
|
||||
def device(self):
|
||||
return self._device
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
return False
|
||||
|
||||
@property
|
||||
def available(self):
|
||||
return self._device.connected
|
||||
|
||||
def _get_status_on_off(self, status_key: str):
|
||||
result = False
|
||||
status = self._device.get_attribute(status_key)
|
||||
if status is not None:
|
||||
try:
|
||||
result = bool(self._rationale.index(status))
|
||||
except ValueError:
|
||||
MideaLogger.error(f"The value of attribute {status_key} ('{status}') "
|
||||
f"is not in rationale {self._rationale}")
|
||||
return result
|
||||
|
||||
def _set_status_on_off(self, status_key: str, turn_on: bool):
|
||||
self._device.set_attribute(status_key, self._rationale[int(turn_on)])
|
||||
|
||||
def _list_get_selected(self, key_of_list: list, rationale: Rationale = Rationale.EQUALLY):
|
||||
for index in range(0, len(key_of_list)):
|
||||
match = True
|
||||
for attr, value in key_of_list[index].items():
|
||||
state_value = self._device.get_attribute(attr)
|
||||
if state_value is None:
|
||||
match = False
|
||||
break
|
||||
if rationale is Rationale.EQUALLY and state_value != value:
|
||||
match = False
|
||||
break
|
||||
if rationale is Rationale.GREATER and state_value < value:
|
||||
match = False
|
||||
break
|
||||
if rationale is Rationale.LESS and state_value > value:
|
||||
match = False
|
||||
break
|
||||
if match:
|
||||
return index
|
||||
return None
|
||||
|
||||
def _dict_get_selected(self, key_of_dict: dict, rationale: Rationale = Rationale.EQUALLY):
|
||||
for mode, status in key_of_dict.items():
|
||||
match = True
|
||||
for attr, value in status.items():
|
||||
state_value = self._device.get_attribute(attr)
|
||||
if state_value is None:
|
||||
match = False
|
||||
break
|
||||
if rationale is Rationale.EQUALLY and state_value != value:
|
||||
match = False
|
||||
break
|
||||
if rationale is Rationale.GREATER and state_value < value:
|
||||
match = False
|
||||
break
|
||||
if rationale is Rationale.LESS and state_value > value:
|
||||
match = False
|
||||
break
|
||||
if match:
|
||||
return mode
|
||||
return None
|
||||
|
||||
def update_state(self, status):
|
||||
if self._entity_key in status or "connected" in status:
|
||||
try:
|
||||
self.schedule_update_ha_state()
|
||||
except Exception as e:
|
||||
pass
|
||||
|
||||
|
||||
class MideaBinaryBaseEntity(MideaEntity):
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
return STATE_ON if self.is_on else STATE_OFF
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
return self._get_status_on_off(self._entity_key)
|
||||
@@ -29,27 +29,72 @@ class MideaEntity(CoordinatorEntity[MideaDataUpdateCoordinator], Entity):
|
||||
sn: str,
|
||||
sn8: str,
|
||||
model: str,
|
||||
entity_key: str,
|
||||
*,
|
||||
device: Any | None = None,
|
||||
manufacturer: str | None = None,
|
||||
rationale: list | None = None,
|
||||
config: dict | None = None,
|
||||
) -> None:
|
||||
"""Initialize the entity."""
|
||||
super().__init__(coordinator)
|
||||
self._device_id = device_id
|
||||
self._device_name = device_name
|
||||
self._device_type = device_type
|
||||
self._entity_key = entity_key
|
||||
self._sn = sn
|
||||
self._sn8 = sn8
|
||||
self._model = model
|
||||
# Legacy/extended fields
|
||||
self._device = device
|
||||
self._config = config or {}
|
||||
self._rationale = rationale
|
||||
if (self._config.get("rationale")) is not None:
|
||||
self._rationale = self._config.get("rationale")
|
||||
if self._rationale is None:
|
||||
self._rationale = ["off", "on"]
|
||||
|
||||
# Display and identification
|
||||
self._attr_has_entity_name = True
|
||||
self._attr_unique_id = f"{sn8}_{self.entity_id_suffix}"
|
||||
self.entity_id_base = f"midea_{sn8.lower()}"
|
||||
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, sn8)},
|
||||
model=model,
|
||||
serial_number=sn,
|
||||
manufacturer="Midea",
|
||||
name=device_name,
|
||||
)
|
||||
# Prefer legacy unique_id scheme if device object is available (device_id based)
|
||||
if self._device is not None:
|
||||
self._attr_unique_id = f"{DOMAIN}.{self._device_id}_{self._entity_key}"
|
||||
self.entity_id_base = f"midea_{self._device_id}"
|
||||
manu = "Midea" if manufacturer is None else manufacturer
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, str(self._device_id))},
|
||||
model=self._model,
|
||||
serial_number=sn,
|
||||
manufacturer=manu,
|
||||
name=device_name,
|
||||
)
|
||||
# Presentation attributes from config
|
||||
self._attr_native_unit_of_measurement = self._config.get("unit_of_measurement")
|
||||
self._attr_device_class = self._config.get("device_class")
|
||||
self._attr_state_class = self._config.get("state_class")
|
||||
self._attr_icon = self._config.get("icon")
|
||||
# Prefer translated name; allow explicit override via config.name
|
||||
self._attr_translation_key = self._config.get("translation_key") or self._entity_key
|
||||
name_cfg = self._config.get("name")
|
||||
if name_cfg is not None:
|
||||
self._attr_name = f"{name_cfg}"
|
||||
self.entity_id = self._attr_unique_id
|
||||
# Register device updates for HA state refresh
|
||||
try:
|
||||
self._device.register_update(self.update_state) # type: ignore[attr-defined]
|
||||
except Exception:
|
||||
pass
|
||||
else:
|
||||
# Fallback to sn8-based unique id/device info
|
||||
self._attr_unique_id = f"{sn8}_{self.entity_id_suffix}"
|
||||
self.entity_id_base = f"midea_{sn8.lower()}"
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, sn8)},
|
||||
model=model,
|
||||
serial_number=sn,
|
||||
manufacturer="Midea",
|
||||
name=device_name,
|
||||
)
|
||||
|
||||
# Debounced command publishing
|
||||
self._debounced_publish_command = Debouncer(
|
||||
@@ -87,6 +132,86 @@ class MideaEntity(CoordinatorEntity[MideaDataUpdateCoordinator], Entity):
|
||||
# This will be implemented by subclasses
|
||||
pass
|
||||
|
||||
# ===== Unified helpers migrated from legacy entity base =====
|
||||
def _get_status_on_off(self, attribute_key: str | None) -> bool:
|
||||
"""Return boolean value from device attributes for given key.
|
||||
|
||||
Accepts common truthy representations: True/1/"on"/"true".
|
||||
"""
|
||||
if attribute_key is None:
|
||||
return False
|
||||
value = self.device_attributes.get(attribute_key)
|
||||
if isinstance(value, bool):
|
||||
return value
|
||||
return value in (1, "1", "on", "ON", "true", "TRUE")
|
||||
|
||||
async def _async_set_status_on_off(self, attribute_key: str | None, turn_on: bool) -> None:
|
||||
"""Set boolean attribute via coordinator, no-op if key is None."""
|
||||
if attribute_key is None:
|
||||
return
|
||||
await self.async_set_attribute(attribute_key, bool(turn_on))
|
||||
|
||||
def _list_get_selected(self, options: list[dict] | None, rationale: object = None) -> int | None:
|
||||
"""Select index from a list of dict conditions matched against attributes.
|
||||
|
||||
The optional rationale supports equality/greater/less matching. It can be
|
||||
a string name ("EQUALLY"/"GREATER"/"LESS") or an Enum with .name.
|
||||
"""
|
||||
if not options:
|
||||
return None
|
||||
|
||||
rationale_name = getattr(rationale, "name", None) or rationale or "EQUALLY"
|
||||
for index in range(0, len(options)):
|
||||
match = True
|
||||
for attr, expected in options[index].items():
|
||||
current = self.device_attributes.get(attr)
|
||||
if current is None:
|
||||
match = False
|
||||
break
|
||||
if rationale_name == "EQUALLY" and current != expected:
|
||||
match = False
|
||||
break
|
||||
if rationale_name == "GREATER" and current < expected:
|
||||
match = False
|
||||
break
|
||||
if rationale_name == "LESS" and current > expected:
|
||||
match = False
|
||||
break
|
||||
if match:
|
||||
return index
|
||||
return None
|
||||
|
||||
def _dict_get_selected(self, mapping: dict | None, rationale: object = None):
|
||||
"""Return key from a dict whose value (a condition dict) matches attributes.
|
||||
|
||||
The optional rationale supports equality/greater/less matching. It can be
|
||||
a string name ("EQUALLY"/"GREATER"/"LESS") or an Enum with .name.
|
||||
"""
|
||||
if not mapping:
|
||||
return None
|
||||
rationale_name = getattr(rationale, "name", None) or rationale or "EQUALLY"
|
||||
for key, conditions in mapping.items():
|
||||
if not isinstance(conditions, dict):
|
||||
continue
|
||||
match = True
|
||||
for attr, expected in conditions.items():
|
||||
current = self.device_attributes.get(attr)
|
||||
if current is None:
|
||||
match = False
|
||||
break
|
||||
if rationale_name == "EQUALLY" and current != expected:
|
||||
match = False
|
||||
break
|
||||
if rationale_name == "GREATER" and current <= expected:
|
||||
match = False
|
||||
break
|
||||
if rationale_name == "LESS" and current >= expected:
|
||||
match = False
|
||||
break
|
||||
if match:
|
||||
return key
|
||||
return None
|
||||
|
||||
async def publish_command_from_current_state(self) -> None:
|
||||
"""Publish commands to the device from current state."""
|
||||
self.coordinator.mute_state_update_for_a_while()
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from homeassistant.components.select import SelectEntity
|
||||
from homeassistant.const import Platform
|
||||
from .const import DOMAIN
|
||||
from .midea_entities import MideaEntity
|
||||
from .midea_entity import MideaEntity
|
||||
from . import load_device_config
|
||||
|
||||
|
||||
@@ -24,13 +24,30 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
coordinator = coordinator_map.get(device_id)
|
||||
device = coordinator.device if coordinator else None
|
||||
for entity_key, ecfg in entities_cfg.items():
|
||||
devs.append(MideaSelectEntity(device, manufacturer, rationale, entity_key, ecfg))
|
||||
devs.append(MideaSelectEntity(coordinator, device, manufacturer, rationale, entity_key, ecfg))
|
||||
async_add_entities(devs)
|
||||
|
||||
|
||||
class MideaSelectEntity(MideaEntity, SelectEntity):
|
||||
def __init__(self, device, manufacturer, rationale, entity_key, config):
|
||||
super().__init__(device, manufacturer, rationale, entity_key, config)
|
||||
def __init__(self, coordinator, device, manufacturer, rationale, entity_key, config):
|
||||
super().__init__(
|
||||
coordinator,
|
||||
device.device_id,
|
||||
device.device_name,
|
||||
f"T0x{device.device_type:02X}",
|
||||
device.sn,
|
||||
device.sn8,
|
||||
device.model,
|
||||
entity_key,
|
||||
device=device,
|
||||
manufacturer=manufacturer,
|
||||
rationale=rationale,
|
||||
config=config,
|
||||
)
|
||||
self._device = device
|
||||
self._manufacturer = manufacturer
|
||||
self._rationale = rationale
|
||||
self._config = config
|
||||
self._key_options = self._config.get("options")
|
||||
|
||||
@property
|
||||
@@ -41,13 +58,14 @@ class MideaSelectEntity(MideaEntity, SelectEntity):
|
||||
def current_option(self):
|
||||
return self._dict_get_selected(self._key_options)
|
||||
|
||||
def select_option(self, option: str):
|
||||
async def async_select_option(self, option: str):
|
||||
new_status = self._key_options.get(option)
|
||||
self._device.set_attributes(new_status)
|
||||
if new_status:
|
||||
await self.async_set_attributes(new_status)
|
||||
|
||||
def update_state(self, status):
|
||||
try:
|
||||
self.schedule_update_ha_state()
|
||||
except Exception as e:
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from .const import DOMAIN
|
||||
from .core.logger import MideaLogger
|
||||
from .midea_entity import MideaEntity
|
||||
from . import load_device_config
|
||||
|
||||
@@ -51,18 +52,17 @@ class MideaSensorEntity(MideaEntity, SensorEntity):
|
||||
device.sn,
|
||||
device.sn8,
|
||||
device.model,
|
||||
entity_key,
|
||||
device=device,
|
||||
manufacturer=manufacturer,
|
||||
rationale=rationale,
|
||||
config=config,
|
||||
)
|
||||
self._device = device
|
||||
self._manufacturer = manufacturer
|
||||
self._rationale = rationale
|
||||
self._entity_key = entity_key
|
||||
self._config = config
|
||||
|
||||
@property
|
||||
def entity_id_suffix(self) -> str:
|
||||
"""Return the suffix for entity ID."""
|
||||
return f"sensor_{self._entity_key}"
|
||||
|
||||
@property
|
||||
def native_value(self):
|
||||
"""Return the native value of the sensor."""
|
||||
|
||||
@@ -27,7 +27,7 @@ async def async_setup_entry(
|
||||
for device_id, info in device_list.items():
|
||||
device_type = info.get("type")
|
||||
sn8 = info.get("sn8")
|
||||
config = load_device_config(hass, device_type, sn8) or {}
|
||||
config = await load_device_config(hass, device_type, sn8) or {}
|
||||
entities_cfg = (config.get("entities") or {}).get(Platform.SWITCH, {})
|
||||
manufacturer = config.get("manufacturer")
|
||||
rationale = config.get("rationale")
|
||||
@@ -52,17 +52,17 @@ class MideaSwitchEntity(MideaEntity, SwitchEntity):
|
||||
device.sn,
|
||||
device.sn8,
|
||||
device.model,
|
||||
entity_key,
|
||||
device=device,
|
||||
manufacturer=manufacturer,
|
||||
rationale=rationale,
|
||||
config=config,
|
||||
)
|
||||
self._device = device
|
||||
self._manufacturer = manufacturer
|
||||
self._rationale = rationale
|
||||
self._entity_key = entity_key
|
||||
self._config = config
|
||||
|
||||
@property
|
||||
def entity_id_suffix(self) -> str:
|
||||
"""Return the suffix for entity ID."""
|
||||
return f"switch_{self._entity_key}"
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
|
||||
@@ -102,5 +102,141 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"select": {
|
||||
"work_status": {
|
||||
"name": "Work Status"
|
||||
},
|
||||
"fan_level": { "name": "Fan Level" },
|
||||
"work_mode": { "name": "Work Mode" },
|
||||
"move_direction": { "name": "Move Direction" },
|
||||
"query_type": { "name": "Query Type" },
|
||||
"sub_work_status": { "name": "Sub Work Status" },
|
||||
"water_level": { "name": "Water Level" },
|
||||
"mop_status": { "name": "Mop Status" },
|
||||
"error_type": { "name": "Error Type" },
|
||||
"control_type": { "name": "Control Type" },
|
||||
"mode": { "name": "Mode" },
|
||||
"rice_type": { "name": "Rice Type" }
|
||||
},
|
||||
"sensor": {
|
||||
"bright": { "name": "Brightness" },
|
||||
"temperature": { "name": "Temperature" },
|
||||
"softwater": { "name": "Soft Water Temp" },
|
||||
"left_time": { "name": "Remaining Time" },
|
||||
"air_set_hour": { "name": "AC Set Time" },
|
||||
"air_left_hour": { "name": "AC Remaining Time" },
|
||||
"micro_leak_protection_value": { "name": "Micro Leak Protection Value" },
|
||||
"regeneration_current_stages": { "name": "Regeneration Current Stages" },
|
||||
"water_hardness": { "name": "Water Hardness" },
|
||||
"timing_regeneration_hour": { "name": "Timing Regeneration Hour" },
|
||||
"real_time_setting_hour": { "name": "Real Time Setting Hour" },
|
||||
"timing_regeneration_min": { "name": "Timing Regeneration Min" },
|
||||
"regeneration_left_seconds": { "name": "Regeneration Left Seconds" },
|
||||
"maintenance_reminder_setting": { "name": "Maintenance Reminder Setting" },
|
||||
"mixed_water_gear": { "name": "Mixed Water Gear" },
|
||||
"use_days": { "name": "Use Days" },
|
||||
"days_since_last_regeneration": { "name": "Days Since Last Regeneration" },
|
||||
"velocity": { "name": "Velocity" },
|
||||
"supply_voltage": { "name": "Supply Voltage" },
|
||||
"left_salt": { "name": "Left Salt" },
|
||||
"pre_regeneration_days": { "name": "Pre Regeneration Days" },
|
||||
"flushing_days": { "name": "Flushing Days" },
|
||||
"salt_setting": { "name": "Salt Setting" },
|
||||
"e_version": { "name": "E Version" },
|
||||
"regeneration_count": { "name": "Regeneration Count" },
|
||||
"battery_voltage": { "name": "Battery Voltage" },
|
||||
"error": { "name": "Error" },
|
||||
"days_since_last_two_regeneration": { "name": "Days Since Last Two Regeneration" },
|
||||
"remind_maintenance_days": { "name": "Remind Maintenance Days" },
|
||||
"real_date_setting_year": { "name": "Real Date Setting Year" },
|
||||
"real_date_setting_month": { "name": "Real Date Setting Month" },
|
||||
"real_date_setting_day": { "name": "Real Date Setting Day" },
|
||||
"category": { "name": "Category" },
|
||||
"real_time_setting_min": { "name": "Real Time Setting Min" },
|
||||
"regeneration_stages": { "name": "Regeneration Stages" },
|
||||
"v_version": { "name": "V Version" },
|
||||
"k_version": { "name": "K Version" },
|
||||
"w_version": { "name": "W Version" },
|
||||
"soft_available_big": { "name": "Soft Available Big" },
|
||||
"water_consumption_big": { "name": "Water Consumption Big" },
|
||||
"water_consumption_today": { "name": "Water Consumption Today" },
|
||||
"water_consumption_average": { "name": "Water Consumption Average" },
|
||||
"salt_alarm_threshold": { "name": "Salt Alarm Threshold" },
|
||||
"leak_water_protection_value": { "name": "Leak Water Protection Value" },
|
||||
"sn8": { "name": "SN8" },
|
||||
"version": { "name": "Version" },
|
||||
"dust_count": { "name": "Dust Count" },
|
||||
"area": { "name": "Area" },
|
||||
"voice_level": { "name": "Voice Level" },
|
||||
"switch_status": { "name": "Switch Status" },
|
||||
"water_station_status": { "name": "Water Station Status" },
|
||||
"work_time": { "name": "Work Time" },
|
||||
"battery_percent": { "name": "Battery Percent" },
|
||||
"planner_status": { "name": "Planner Status" },
|
||||
"sweep_then_mop_mode_progress": { "name": "Sweep Then Mop Mode Progress" },
|
||||
"error_desc": { "name": "Error Description" },
|
||||
"station_error_desc": { "name": "Station Error Description" },
|
||||
"work_stage": { "name": "Work Stage" },
|
||||
"voltage": { "name": "Voltage" },
|
||||
"top_temperature": { "name": "Top Temperature" },
|
||||
"bottom_temperature": { "name": "Bottom Temperature" },
|
||||
"remaining_time": { "name": "Remaining Time" },
|
||||
"warming_time": { "name": "Warming Time" },
|
||||
"delay_time": { "name": "Delay Time" },
|
||||
"indoor_temperature": { "name": "Indoor Temperature" },
|
||||
"outdoor_temperature": { "name": "Outdoor Temperature" }
|
||||
},
|
||||
"binary_sensor": {
|
||||
"air_status": { "name": "Air Running" },
|
||||
"water_lack": { "name": "Water Lack" },
|
||||
"softwater_lack": { "name": "Soft Water Lack" },
|
||||
"wash_stage": { "name": "Washing Stage" },
|
||||
"bright_lack": { "name": "Brightness Lack" },
|
||||
"diy_flag": { "name": "DIY Flag" },
|
||||
"diy_main_wash": { "name": "DIY Main Wash" },
|
||||
"diy_piao_wash": { "name": "DIY Rinse" },
|
||||
"diy_times": { "name": "DIY Times" },
|
||||
"maintenance_remind": { "name": "Maintenance Remind" },
|
||||
"chlorine_sterilization_error": { "name": "Chlorine Sterilization Error" },
|
||||
"rtc_error": { "name": "RTC Error" },
|
||||
"carpet_switch": { "name": "Carpet Switch" },
|
||||
"have_reserve_task": { "name": "Have Reserve Task" },
|
||||
"top_hot": { "name": "Top Hot" },
|
||||
"flank_hot": { "name": "Flank Hot" },
|
||||
"bottom_hot": { "name": "Bottom Hot" }
|
||||
},
|
||||
"climate": {
|
||||
"thermostat": { "name": "Thermostat" }
|
||||
},
|
||||
"switch": {
|
||||
"airswitch": { "name": "AC Switch" },
|
||||
"waterswitch": { "name": "Water Switch" },
|
||||
"uvswitch": { "name": "UV Switch" },
|
||||
"doorswitch": { "name": "Door Switch" },
|
||||
"dryswitch": { "name": "Dry Switch" },
|
||||
"dry_step_switch": { "name": "Dry Step Switch" },
|
||||
"holiday_mode": { "name": "Holiday Mode" },
|
||||
"water_way": { "name": "Water Way" },
|
||||
"soften": { "name": "Soften" },
|
||||
"leak_water_protection": { "name": "Leak Water Protection" },
|
||||
"cl_sterilization": { "name": "Chlorine Sterilization" },
|
||||
"micro_leak": { "name": "Micro Leak" },
|
||||
"low_salt": { "name": "Low Salt" },
|
||||
"no_salt": { "name": "No Salt" },
|
||||
"low_battery": { "name": "Low Battery" },
|
||||
"salt_level_sensor_error": { "name": "Salt Level Sensor Error" },
|
||||
"flowmeter_error": { "name": "Flowmeter Error" },
|
||||
"leak_water": { "name": "Leak Water" },
|
||||
"micro_leak_protection": { "name": "Micro Leak Protection" },
|
||||
"maintenance_reminder_switch": { "name": "Maintenance Reminder Switch" },
|
||||
"rsj_stand_by": { "name": "RSJ Stand By" },
|
||||
"regeneration": { "name": "Regeneration" },
|
||||
"pre_regeneration": { "name": "Pre Regeneration" },
|
||||
"dry": { "name": "Dry" },
|
||||
"prevent_straight_wind": { "name": "Prevent Straight Wind" },
|
||||
"aux_heat": { "name": "Aux Heat" }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -102,5 +102,141 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"select": {
|
||||
"work_status": {
|
||||
"name": "工作状态"
|
||||
},
|
||||
"fan_level": { "name": "风扇档位" },
|
||||
"work_mode": { "name": "工作模式" },
|
||||
"move_direction": { "name": "移动方向" },
|
||||
"query_type": { "name": "查询类型" },
|
||||
"sub_work_status": { "name": "子工作状态" },
|
||||
"water_level": { "name": "水位" },
|
||||
"mop_status": { "name": "拖地状态" },
|
||||
"error_type": { "name": "错误类型" },
|
||||
"control_type": { "name": "控制类型" },
|
||||
"mode": { "name": "模式" },
|
||||
"rice_type": { "name": "米种类型" }
|
||||
},
|
||||
"sensor": {
|
||||
"bright": { "name": "亮度" },
|
||||
"temperature": { "name": "温度" },
|
||||
"softwater": { "name": "软水温度" },
|
||||
"left_time": { "name": "剩余时间" },
|
||||
"air_set_hour": { "name": "空调设置时间" },
|
||||
"air_left_hour": { "name": "空调剩余时间" },
|
||||
"micro_leak_protection_value": { "name": "微漏保护值" },
|
||||
"regeneration_current_stages": { "name": "再生当前阶段" },
|
||||
"water_hardness": { "name": "水质硬度" },
|
||||
"timing_regeneration_hour": { "name": "定时再生小时" },
|
||||
"real_time_setting_hour": { "name": "实时设置小时" },
|
||||
"timing_regeneration_min": { "name": "定时再生分钟" },
|
||||
"regeneration_left_seconds": { "name": "再生剩余秒数" },
|
||||
"maintenance_reminder_setting": { "name": "维护提醒设置" },
|
||||
"mixed_water_gear": { "name": "混合水档位" },
|
||||
"use_days": { "name": "使用天数" },
|
||||
"days_since_last_regeneration": { "name": "距离上次再生天数" },
|
||||
"velocity": { "name": "流速" },
|
||||
"supply_voltage": { "name": "供电电压" },
|
||||
"left_salt": { "name": "剩余盐量" },
|
||||
"pre_regeneration_days": { "name": "预再生天数" },
|
||||
"flushing_days": { "name": "冲洗天数" },
|
||||
"salt_setting": { "name": "盐设置" },
|
||||
"e_version": { "name": "E 版本" },
|
||||
"regeneration_count": { "name": "再生次数" },
|
||||
"battery_voltage": { "name": "电池电压" },
|
||||
"error": { "name": "错误" },
|
||||
"days_since_last_two_regeneration": { "name": "距离上次两次再生天数" },
|
||||
"remind_maintenance_days": { "name": "提醒维护天数" },
|
||||
"real_date_setting_year": { "name": "实际日期设置年" },
|
||||
"real_date_setting_month": { "name": "实际日期设置月" },
|
||||
"real_date_setting_day": { "name": "实际日期设置日" },
|
||||
"category": { "name": "类别" },
|
||||
"real_time_setting_min": { "name": "实时设置分钟" },
|
||||
"regeneration_stages": { "name": "再生阶段" },
|
||||
"v_version": { "name": "V 版本" },
|
||||
"k_version": { "name": "K 版本" },
|
||||
"w_version": { "name": "W 版本" },
|
||||
"soft_available_big": { "name": "软水可用量" },
|
||||
"water_consumption_big": { "name": "用水量" },
|
||||
"water_consumption_today": { "name": "今日用水量" },
|
||||
"water_consumption_average": { "name": "平均用水量" },
|
||||
"salt_alarm_threshold": { "name": "盐报警阈值" },
|
||||
"leak_water_protection_value": { "name": "漏水保护值" },
|
||||
"sn8": { "name": "SN8" },
|
||||
"version": { "name": "版本" },
|
||||
"dust_count": { "name": "灰尘计数" },
|
||||
"area": { "name": "面积" },
|
||||
"voice_level": { "name": "语音音量" },
|
||||
"switch_status": { "name": "开关状态" },
|
||||
"water_station_status": { "name": "水站状态" },
|
||||
"work_time": { "name": "工作时间" },
|
||||
"battery_percent": { "name": "电池百分比" },
|
||||
"planner_status": { "name": "规划器状态" },
|
||||
"sweep_then_mop_mode_progress": { "name": "先扫后拖模式进度" },
|
||||
"error_desc": { "name": "错误描述" },
|
||||
"station_error_desc": { "name": "基站错误描述" },
|
||||
"work_stage": { "name": "工作阶段" },
|
||||
"voltage": { "name": "电压" },
|
||||
"top_temperature": { "name": "上盖温度" },
|
||||
"bottom_temperature": { "name": "底部温度" },
|
||||
"remaining_time": { "name": "剩余时间" },
|
||||
"warming_time": { "name": "保温时间" },
|
||||
"delay_time": { "name": "预约时间" },
|
||||
"indoor_temperature": { "name": "室内温度" },
|
||||
"outdoor_temperature": { "name": "室外温度" }
|
||||
},
|
||||
"binary_sensor": {
|
||||
"air_status": { "name": "空气运行" },
|
||||
"water_lack": { "name": "缺水" },
|
||||
"softwater_lack": { "name": "软水不足" },
|
||||
"wash_stage": { "name": "洗涤阶段" },
|
||||
"bright_lack": { "name": "亮光不足" },
|
||||
"diy_flag": { "name": "自定义标记" },
|
||||
"diy_main_wash": { "name": "自定义主洗" },
|
||||
"diy_piao_wash": { "name": "自定义漂洗" },
|
||||
"diy_times": { "name": "自定义次数" },
|
||||
"maintenance_remind": { "name": "维护提醒" },
|
||||
"chlorine_sterilization_error": { "name": "氯气杀菌错误" },
|
||||
"rtc_error": { "name": "RTC 错误" },
|
||||
"carpet_switch": { "name": "地毯开关" },
|
||||
"have_reserve_task": { "name": "有预约任务" },
|
||||
"top_hot": { "name": "上盖加热" },
|
||||
"flank_hot": { "name": "侧壁加热" },
|
||||
"bottom_hot": { "name": "底部加热" }
|
||||
},
|
||||
"climate": {
|
||||
"thermostat": { "name": "温控器" }
|
||||
},
|
||||
"switch": {
|
||||
"airswitch": { "name": "空调开关" },
|
||||
"waterswitch": { "name": "热水开关" },
|
||||
"uvswitch": { "name": "UV 开关" },
|
||||
"doorswitch": { "name": "门锁开关" },
|
||||
"dryswitch": { "name": "烘干开关" },
|
||||
"dry_step_switch": { "name": "烘干分步开关" },
|
||||
"holiday_mode": { "name": "假日模式" },
|
||||
"water_way": { "name": "水路" },
|
||||
"soften": { "name": "软化" },
|
||||
"leak_water_protection": { "name": "漏水保护" },
|
||||
"cl_sterilization": { "name": "氯气杀菌" },
|
||||
"micro_leak": { "name": "微漏" },
|
||||
"low_salt": { "name": "低盐" },
|
||||
"no_salt": { "name": "无盐" },
|
||||
"low_battery": { "name": "低电量" },
|
||||
"salt_level_sensor_error": { "name": "盐位传感器错误" },
|
||||
"flowmeter_error": { "name": "流量计错误" },
|
||||
"leak_water": { "name": "漏水" },
|
||||
"micro_leak_protection": { "name": "微漏保护" },
|
||||
"maintenance_reminder_switch": { "name": "维护提醒开关" },
|
||||
"rsj_stand_by": { "name": "RSJ 待机" },
|
||||
"regeneration": { "name": "再生" },
|
||||
"pre_regeneration": { "name": "预再生" },
|
||||
"dry": { "name": "干燥" },
|
||||
"prevent_straight_wind": { "name": "防直吹" },
|
||||
"aux_heat": { "name": "电辅热" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ from homeassistant.const import (
|
||||
ATTR_TEMPERATURE
|
||||
)
|
||||
from .const import DOMAIN
|
||||
from .midea_entities import MideaEntity
|
||||
from .midea_entity import MideaEntity
|
||||
from . import load_device_config
|
||||
|
||||
|
||||
@@ -27,13 +27,54 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
coordinator = coordinator_map.get(device_id)
|
||||
device = coordinator.device if coordinator else None
|
||||
for entity_key, ecfg in entities_cfg.items():
|
||||
devs.append(MideaWaterHeaterEntityEntity(device, manufacturer, rationale, entity_key, ecfg))
|
||||
devs.append(MideaWaterHeaterEntityEntity(coordinator, device, manufacturer, rationale, entity_key, ecfg))
|
||||
async_add_entities(devs)
|
||||
|
||||
|
||||
class MideaWaterHeaterEntityEntity(MideaEntity, WaterHeaterEntity):
|
||||
def __init__(self, device, manufacturer, rationale, entity_key, config):
|
||||
super().__init__(device, manufacturer, rationale, entity_key, config)
|
||||
def __init__(self, coordinator, device, manufacturer, rationale, entity_key, config):
|
||||
super().__init__(
|
||||
coordinator,
|
||||
device.device_id,
|
||||
device.device_name,
|
||||
f"T0x{device.device_type:02X}",
|
||||
device.sn,
|
||||
device.sn8,
|
||||
device.model,
|
||||
entity_key,
|
||||
device=device,
|
||||
manufacturer=manufacturer,
|
||||
rationale=rationale,
|
||||
config=config,
|
||||
)
|
||||
self._device = device
|
||||
self._manufacturer = manufacturer
|
||||
self._rationale = rationale
|
||||
self._config = config
|
||||
# Legacy compatibility: register update and restore display attributes
|
||||
if self._device:
|
||||
self._device.register_update(self.update_state)
|
||||
if (rationale_local := self._config.get("rationale")) is not None:
|
||||
self._rationale = rationale_local
|
||||
if self._rationale is None:
|
||||
self._rationale = ["off", "on"]
|
||||
self._attr_native_unit_of_measurement = self._config.get("unit_of_measurement")
|
||||
self._attr_device_class = self._config.get("device_class")
|
||||
self._attr_state_class = self._config.get("state_class")
|
||||
self._attr_icon = self._config.get("icon")
|
||||
from .const import DOMAIN as _DOMAIN
|
||||
self._attr_unique_id = f"{_DOMAIN}.{self._device.device_id}_{self._entity_key}"
|
||||
self._attr_device_info = {
|
||||
"manufacturer": "Midea" if self._manufacturer is None else self._manufacturer,
|
||||
"model": f"{self._device.model}",
|
||||
"identifiers": {( _DOMAIN, self._device.device_id)},
|
||||
"name": self._device.device_name
|
||||
}
|
||||
name = self._config.get("name")
|
||||
if name is None:
|
||||
name = self._entity_key.replace("_", " ").title()
|
||||
self._attr_name = f"{self._device.device_name} {name}"
|
||||
self.entity_id = self._attr_unique_id
|
||||
self._key_power = self._config.get("power")
|
||||
self._key_operation_list = self._config.get("operation_list")
|
||||
self._key_min_temp = self._config.get("min_temp")
|
||||
@@ -62,30 +103,30 @@ class MideaWaterHeaterEntityEntity(MideaEntity, WaterHeaterEntity):
|
||||
|
||||
@property
|
||||
def current_temperature(self):
|
||||
return self._device.get_attribute(self._key_current_temperature)
|
||||
return self.device_attributes.get(self._key_current_temperature)
|
||||
|
||||
@property
|
||||
def target_temperature(self):
|
||||
if isinstance(self._key_target_temperature, list):
|
||||
temp_int = self._device.get_attribute(self._key_target_temperature[0])
|
||||
tem_dec = self._device.get_attribute(self._key_target_temperature[1])
|
||||
temp_int = self.device_attributes.get(self._key_target_temperature[0])
|
||||
tem_dec = self.device_attributes.get(self._key_target_temperature[1])
|
||||
if temp_int is not None and tem_dec is not None:
|
||||
return temp_int + tem_dec
|
||||
return None
|
||||
else:
|
||||
return self._device.get_attribute(self._key_target_temperature)
|
||||
return self.device_attributes.get(self._key_target_temperature)
|
||||
|
||||
@property
|
||||
def min_temp(self):
|
||||
if isinstance(self._key_min_temp, str):
|
||||
return float(self._device.get_attribute(self._key_min_temp))
|
||||
return float(self.device_attributes.get(self._key_min_temp))
|
||||
else:
|
||||
return float(self._key_min_temp)
|
||||
|
||||
@property
|
||||
def max_temp(self):
|
||||
if isinstance(self._key_max_temp, str):
|
||||
return float(self._device.get_attribute(self._key_max_temp))
|
||||
return float(self.device_attributes.get(self._key_max_temp))
|
||||
else:
|
||||
return float(self._key_max_temp)
|
||||
|
||||
@@ -101,13 +142,13 @@ class MideaWaterHeaterEntityEntity(MideaEntity, WaterHeaterEntity):
|
||||
def is_on(self) -> bool:
|
||||
return self._get_status_on_off(self._key_power)
|
||||
|
||||
def turn_on(self):
|
||||
self._set_status_on_off(self._key_power, True)
|
||||
async def async_turn_on(self):
|
||||
await self._async_set_status_on_off(self._key_power, True)
|
||||
|
||||
def turn_off(self):
|
||||
self._set_status_on_off(self._key_power, False)
|
||||
async def async_turn_off(self):
|
||||
await self._async_set_status_on_off(self._key_power, False)
|
||||
|
||||
def set_temperature(self, **kwargs):
|
||||
async def async_set_temperature(self, **kwargs):
|
||||
if ATTR_TEMPERATURE not in kwargs:
|
||||
return
|
||||
temperature = kwargs.get(ATTR_TEMPERATURE)
|
||||
@@ -119,15 +160,16 @@ class MideaWaterHeaterEntityEntity(MideaEntity, WaterHeaterEntity):
|
||||
new_status[self._key_target_temperature[1]] = temp_dec
|
||||
else:
|
||||
new_status[self._key_target_temperature] = temperature
|
||||
self._device.set_attributes(new_status)
|
||||
await self.async_set_attributes(new_status)
|
||||
|
||||
def set_operation_mode(self, operation_mode: str) -> None:
|
||||
async def async_set_operation_mode(self, operation_mode: str) -> None:
|
||||
new_status = self._key_operation_list.get(operation_mode)
|
||||
self._device.set_attributes(new_status)
|
||||
if new_status:
|
||||
await self.async_set_attributes(new_status)
|
||||
|
||||
def update_state(self, status):
|
||||
try:
|
||||
self.schedule_update_ha_state()
|
||||
except Exception as e:
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
Reference in New Issue
Block a user