From 2bfc2b9fbe932214b00d6990f03a677ad0a86582 Mon Sep 17 00:00:00 2001 From: sususweet Date: Wed, 17 Sep 2025 22:46:38 +0800 Subject: [PATCH] Add cloud control. --- .../midea_auto_codec/__init__.py | 90 ++++++++++++++++++- custom_components/midea_auto_codec/climate.py | 13 ++- .../midea_auto_codec/core/cloud.py | 34 +++++++ .../midea_auto_codec/data_coordinator.py | 52 ++++++++--- 4 files changed, 174 insertions(+), 15 deletions(-) diff --git a/custom_components/midea_auto_codec/__init__.py b/custom_components/midea_auto_codec/__init__.py index 084239d..c9e1680 100644 --- a/custom_components/midea_auto_codec/__init__.py +++ b/custom_components/midea_auto_codec/__init__.py @@ -197,6 +197,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry): # 拉取家庭与设备列表 appliances = None + first_home_id = None try: homes = await cloud.list_home() if homes and len(homes) > 0: @@ -213,7 +214,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry): hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN].setdefault("accounts", {}) - bucket = {"device_list": {}, "coordinator_map": {}} + bucket = {"device_list": {}, "coordinator_map": {}, "cloud": cloud, "home_id": first_home_id} # 为每台设备构建占位设备与协调器(不连接本地) for appliance_code, info in appliances.items(): @@ -235,6 +236,93 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry): sn8=info.get(CONF_SN8) or info.get("sn8"), lua_file=None, ) + # 加载并应用设备映射(queries/centralized/calculate),并预置 attributes 键 + try: + mapping = 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 diff --git a/custom_components/midea_auto_codec/climate.py b/custom_components/midea_auto_codec/climate.py index 92ea7c1..88f4bf8 100644 --- a/custom_components/midea_auto_codec/climate.py +++ b/custom_components/midea_auto_codec/climate.py @@ -13,6 +13,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 .midea_entities import Rationale from . import load_device_config @@ -42,6 +43,10 @@ async def async_setup_entry( rationale = config.get("rationale") 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 @@ -144,7 +149,7 @@ class MideaClimateEntity(MideaEntity, ClimateEntity): @property def fan_mode(self): - return self._dict_get_selected(self._key_fan_modes, Rationale.LESS) + return self._dict_get_selected(self._key_fan_modes, Rationale.EQUALLY) @property def swing_modes(self): @@ -235,13 +240,17 @@ class MideaClimateEntity(MideaEntity, ClimateEntity): """Get selected value from dictionary configuration.""" if dict_config is None: return None - + + MideaLogger.debug(f"dict_config={dict_config}, rationale={rationale}, self.device_attributes={self.device_attributes} ") for key, config in dict_config.items(): if isinstance(config, dict): # Check if all conditions match match = True for attr_key, attr_value in config.items(): device_value = self.device_attributes.get(attr_key) + if device_value is None: + match = False + break if rationale == Rationale.EQUALLY: if device_value != attr_value: match = False diff --git a/custom_components/midea_auto_codec/core/cloud.py b/custom_components/midea_auto_codec/core/cloud.py index 713efde..0c5c48f 100644 --- a/custom_components/midea_auto_codec/core/cloud.py +++ b/custom_components/midea_auto_codec/core/cloud.py @@ -154,6 +154,10 @@ class MideaCloud: ): raise NotImplementedError() + async def send_device_control(self, appliance_code: int, control: dict, status: dict | None = None) -> bool: + """Send control to a device via cloud. Subclasses should implement if supported.""" + raise NotImplementedError() + class MeijuCloud(MideaCloud): APP_ID = "900" @@ -259,6 +263,36 @@ class MeijuCloud(MideaCloud): return appliances return None + async def get_device_status(self, appliance_code: int) -> dict | None: + data = { + "applianceCode": str(appliance_code), + "command": { + "query": {"query_type": "total_query"} + } + } + if response := await self._api_request( + endpoint="/mjl/v1/device/status/lua/get", + data=data + ): + # 预期返回形如 { ... 状态键 ... } + return response + return None + + async def send_device_control(self, appliance_code: int, control: dict, status: dict | None = None) -> bool: + data = { + "applianceCode": str(appliance_code), + "command": { + "control": control + } + } + if status and isinstance(status, dict): + data["command"]["status"] = status + response = await self._api_request( + endpoint="/mjl/v1/device/lua/control", + data=data + ) + return response is not None + async def download_lua( self, path: str, device_type: int, diff --git a/custom_components/midea_auto_codec/data_coordinator.py b/custom_components/midea_auto_codec/data_coordinator.py index 24e9346..cd73a81 100644 --- a/custom_components/midea_auto_codec/data_coordinator.py +++ b/custom_components/midea_auto_codec/data_coordinator.py @@ -10,6 +10,8 @@ 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__) @@ -46,11 +48,8 @@ class MideaDataUpdateCoordinator(DataUpdateCoordinator[MideaDeviceData]): async def _async_setup(self) -> None: """Set up the coordinator.""" - self.data = MideaDeviceData( - attributes=self.device.attributes, - available=self.device.connected, - connected=self.device.connected, - ) + # Immediate first refresh to avoid waiting for the interval + self.data = await self.poll_device_state() # Register for device updates self.device.register_update(self._device_update_callback) @@ -71,10 +70,9 @@ class MideaDataUpdateCoordinator(DataUpdateCoordinator[MideaDeviceData]): if self.state_update_muted: return - # Update device attributes + # Update device attributes (allow new keys to be added) for key, value in status.items(): - if key in self.device.attributes: - self.device.attributes[key] = value + self.device.attributes[key] = value # Update coordinator data self.async_set_updated_data( @@ -91,12 +89,26 @@ class MideaDataUpdateCoordinator(DataUpdateCoordinator[MideaDeviceData]): return self.data try: - # The device handles its own polling, so we just return current state - return MideaDeviceData( + # 尝试账号模式下的云端轮询(如果 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 + if cloud and hasattr(cloud, "get_device_status"): + try: + status = await cloud.get_device_status(self._device_id) + if isinstance(status, dict) and len(status) > 0: + for k, v in status.items(): + self.device.attributes[k] = v + except Exception as e: + MideaLogger.debug(f"Cloud status fetch failed: {e}") + + # 返回并推送当前状态 + updated = MideaDeviceData( attributes=self.device.attributes, available=self.device.connected, connected=self.device.connected, ) + self.async_set_updated_data(updated) + return updated except Exception as e: _LOGGER.error(f"Error polling device state: {e}") return MideaDeviceData( @@ -107,13 +119,29 @@ class MideaDataUpdateCoordinator(DataUpdateCoordinator[MideaDeviceData]): async def async_set_attribute(self, attribute: str, value) -> None: """Set a device attribute.""" - self.device.set_attribute(attribute, value) + # 云端控制:构造 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 + control = {attribute: value} + status = dict(self.device.attributes) + if cloud and hasattr(cloud, "send_device_control"): + ok = await cloud.send_device_control(self._device_id, control=control, status=status) + if ok: + # 本地先行更新,随后依赖轮询或设备事件校正 + self.device.attributes[attribute] = value self.mute_state_update_for_a_while() self.async_update_listeners() async def async_set_attributes(self, attributes: dict) -> None: """Set multiple device attributes.""" - self.device.set_attributes(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 + control = dict(attributes) + status = dict(self.device.attributes) + if cloud and hasattr(cloud, "send_device_control"): + ok = await cloud.send_device_control(self._device_id, control=control, status=status) + if ok: + self.device.attributes.update(attributes) self.mute_state_update_for_a_while() self.async_update_listeners()