mirror of
https://github.com/sususweet/midea-meiju-codec.git
synced 2025-09-27 18:22:41 +00:00
Add cloud control.
This commit is contained in:
@@ -197,6 +197,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry):
|
|||||||
|
|
||||||
# 拉取家庭与设备列表
|
# 拉取家庭与设备列表
|
||||||
appliances = None
|
appliances = None
|
||||||
|
first_home_id = None
|
||||||
try:
|
try:
|
||||||
homes = await cloud.list_home()
|
homes = await cloud.list_home()
|
||||||
if homes and len(homes) > 0:
|
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.setdefault(DOMAIN, {})
|
||||||
hass.data[DOMAIN].setdefault("accounts", {})
|
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():
|
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"),
|
sn8=info.get(CONF_SN8) or info.get("sn8"),
|
||||||
lua_file=None,
|
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)
|
coordinator = MideaDataUpdateCoordinator(hass, config_entry, device)
|
||||||
await coordinator.async_config_entry_first_refresh()
|
await coordinator.async_config_entry_first_refresh()
|
||||||
bucket["device_list"][appliance_code] = info
|
bucket["device_list"][appliance_code] = info
|
||||||
|
@@ -13,6 +13,7 @@ from homeassistant.core import HomeAssistant
|
|||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
|
||||||
from .const import DOMAIN
|
from .const import DOMAIN
|
||||||
|
from .core.logger import MideaLogger
|
||||||
from .midea_entity import MideaEntity
|
from .midea_entity import MideaEntity
|
||||||
from .midea_entities import Rationale
|
from .midea_entities import Rationale
|
||||||
from . import load_device_config
|
from . import load_device_config
|
||||||
@@ -42,6 +43,10 @@ async def async_setup_entry(
|
|||||||
rationale = config.get("rationale")
|
rationale = config.get("rationale")
|
||||||
coordinator = coordinator_map.get(device_id)
|
coordinator = coordinator_map.get(device_id)
|
||||||
device = coordinator.device if coordinator else None
|
device = coordinator.device if coordinator else None
|
||||||
|
|
||||||
|
|
||||||
|
MideaLogger.debug(f"entities_cfg={entities_cfg} ")
|
||||||
|
|
||||||
for entity_key, ecfg in entities_cfg.items():
|
for entity_key, ecfg in entities_cfg.items():
|
||||||
devs.append(MideaClimateEntity(
|
devs.append(MideaClimateEntity(
|
||||||
coordinator, device, manufacturer, rationale, entity_key, ecfg
|
coordinator, device, manufacturer, rationale, entity_key, ecfg
|
||||||
@@ -144,7 +149,7 @@ class MideaClimateEntity(MideaEntity, ClimateEntity):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def fan_mode(self):
|
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
|
@property
|
||||||
def swing_modes(self):
|
def swing_modes(self):
|
||||||
@@ -235,13 +240,17 @@ class MideaClimateEntity(MideaEntity, ClimateEntity):
|
|||||||
"""Get selected value from dictionary configuration."""
|
"""Get selected value from dictionary configuration."""
|
||||||
if dict_config is None:
|
if dict_config is None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
MideaLogger.debug(f"dict_config={dict_config}, rationale={rationale}, self.device_attributes={self.device_attributes} ")
|
||||||
for key, config in dict_config.items():
|
for key, config in dict_config.items():
|
||||||
if isinstance(config, dict):
|
if isinstance(config, dict):
|
||||||
# Check if all conditions match
|
# Check if all conditions match
|
||||||
match = True
|
match = True
|
||||||
for attr_key, attr_value in config.items():
|
for attr_key, attr_value in config.items():
|
||||||
device_value = self.device_attributes.get(attr_key)
|
device_value = self.device_attributes.get(attr_key)
|
||||||
|
if device_value is None:
|
||||||
|
match = False
|
||||||
|
break
|
||||||
if rationale == Rationale.EQUALLY:
|
if rationale == Rationale.EQUALLY:
|
||||||
if device_value != attr_value:
|
if device_value != attr_value:
|
||||||
match = False
|
match = False
|
||||||
|
@@ -154,6 +154,10 @@ class MideaCloud:
|
|||||||
):
|
):
|
||||||
raise NotImplementedError()
|
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):
|
class MeijuCloud(MideaCloud):
|
||||||
APP_ID = "900"
|
APP_ID = "900"
|
||||||
@@ -259,6 +263,36 @@ class MeijuCloud(MideaCloud):
|
|||||||
return appliances
|
return appliances
|
||||||
return None
|
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(
|
async def download_lua(
|
||||||
self, path: str,
|
self, path: str,
|
||||||
device_type: int,
|
device_type: int,
|
||||||
|
@@ -10,6 +10,8 @@ from homeassistant.helpers.event import async_call_later
|
|||||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
||||||
|
|
||||||
from .core.device import MiedaDevice
|
from .core.device import MiedaDevice
|
||||||
|
from .const import DOMAIN
|
||||||
|
from .core.logger import MideaLogger
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -46,11 +48,8 @@ class MideaDataUpdateCoordinator(DataUpdateCoordinator[MideaDeviceData]):
|
|||||||
|
|
||||||
async def _async_setup(self) -> None:
|
async def _async_setup(self) -> None:
|
||||||
"""Set up the coordinator."""
|
"""Set up the coordinator."""
|
||||||
self.data = MideaDeviceData(
|
# Immediate first refresh to avoid waiting for the interval
|
||||||
attributes=self.device.attributes,
|
self.data = await self.poll_device_state()
|
||||||
available=self.device.connected,
|
|
||||||
connected=self.device.connected,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Register for device updates
|
# Register for device updates
|
||||||
self.device.register_update(self._device_update_callback)
|
self.device.register_update(self._device_update_callback)
|
||||||
@@ -71,10 +70,9 @@ class MideaDataUpdateCoordinator(DataUpdateCoordinator[MideaDeviceData]):
|
|||||||
if self.state_update_muted:
|
if self.state_update_muted:
|
||||||
return
|
return
|
||||||
|
|
||||||
# Update device attributes
|
# Update device attributes (allow new keys to be added)
|
||||||
for key, value in status.items():
|
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
|
# Update coordinator data
|
||||||
self.async_set_updated_data(
|
self.async_set_updated_data(
|
||||||
@@ -91,12 +89,26 @@ class MideaDataUpdateCoordinator(DataUpdateCoordinator[MideaDeviceData]):
|
|||||||
return self.data
|
return self.data
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# The device handles its own polling, so we just return current state
|
# 尝试账号模式下的云端轮询(如果 cloud 存在且支持)
|
||||||
return MideaDeviceData(
|
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,
|
attributes=self.device.attributes,
|
||||||
available=self.device.connected,
|
available=self.device.connected,
|
||||||
connected=self.device.connected,
|
connected=self.device.connected,
|
||||||
)
|
)
|
||||||
|
self.async_set_updated_data(updated)
|
||||||
|
return updated
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
_LOGGER.error(f"Error polling device state: {e}")
|
_LOGGER.error(f"Error polling device state: {e}")
|
||||||
return MideaDeviceData(
|
return MideaDeviceData(
|
||||||
@@ -107,13 +119,29 @@ class MideaDataUpdateCoordinator(DataUpdateCoordinator[MideaDeviceData]):
|
|||||||
|
|
||||||
async def async_set_attribute(self, attribute: str, value) -> None:
|
async def async_set_attribute(self, attribute: str, value) -> None:
|
||||||
"""Set a device attribute."""
|
"""Set a device attribute."""
|
||||||
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.mute_state_update_for_a_while()
|
||||||
self.async_update_listeners()
|
self.async_update_listeners()
|
||||||
|
|
||||||
async def async_set_attributes(self, attributes: dict) -> None:
|
async def async_set_attributes(self, attributes: dict) -> None:
|
||||||
"""Set multiple device attributes."""
|
"""Set multiple device attributes."""
|
||||||
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.mute_state_update_for_a_while()
|
||||||
self.async_update_listeners()
|
self.async_update_listeners()
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user