Add cloud control.

This commit is contained in:
sususweet
2025-09-17 22:46:38 +08:00
parent 85365338f4
commit 2bfc2b9fbe
4 changed files with 174 additions and 15 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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,

View File

@@ -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()