Merge pull request #111 from Cyborg2017/master

Add Vacuum Platform Foundation (344b061)
Feature: Initial vacuum platform setup
Add Platform.VACUUM to supported platforms list
Implement vacuum.py with core vacuum functionality
Update device registry to handle vacuum devices
Extend T0xB8 Mapping for Vacuum (2822b90)
Feature: Enable vacuum support for T0xB8 devices
Update T0xB8 device mapping to support vacuum entities
Add vacuum entity type to device registry for T0xB8
Maintain backward compatibility with existing T0xB8 mappings
Fix T0xD9 Binary Sensor (fed8519)
Fix: Correct binary sensor implementation
Update T0xD9 binary sensor logic
Fix translation keys for consistency
Enhance T0xD9 Washing Machine Logic (987ec12)
Enhancement: Improve position handling for washing machines
Add automatic position adjustment when db_position is 0
Enhance synchronization between control and operational status
Improve refresh_status query logic to follow position settings
Update multiple methods for consistent position handling
This commit is contained in:
Yingqi Tang
2026-02-11 10:00:36 +08:00
committed by GitHub
7 changed files with 319 additions and 123 deletions

View File

@@ -61,7 +61,8 @@ PLATFORMS: list[Platform] = [
Platform.LIGHT,
Platform.HUMIDIFIER,
Platform.NUMBER,
Platform.BUTTON
Platform.BUTTON,
Platform.VACUUM
]
async def import_module_async(module_name):
@@ -381,6 +382,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry):
str(Platform.SWITCH),
str(Platform.FAN),
str(Platform.SELECT),
str(Platform.VACUUM),
]:
for entity_key in platform_cfg.keys():
preset_keys.add(entity_key)

View File

@@ -84,13 +84,48 @@ class MiedaDevice(threading.Thread):
self._lua_runtime = MideaCodec(lua_file, device_type=self._attributes.get("device_type"), sn=sn, subtype=subtype) if lua_file is not None else None
self._cloud = cloud
def _determine_control_status_based_on_running(self, running_status):
def _handle_t0xd9_db_location_selection(self, status, value):
# 处理T0xD9复式洗衣机的db_location_selection更新
if value == "left":
status["db_location"] = 1
self._attributes["db_location"] = 1
elif value == "right":
status["db_location"] = 2
self._attributes["db_location"] = 2
def _adjust_t0xd9_db_location_based_on_position(self, status=None):
# 根据db_position调整T0xD9复式洗衣机的db_location
db_position = self._attributes.get("db_position", 1)
current_location = self._attributes.get("db_location", 1)
if db_position == 1:
# db_position = 1db_location 保持不变
calculated_location = current_location
elif db_position == 0:
# db_position = 0db_location 切换为另一个选项
calculated_location = 2 if current_location == 1 else 1
if status is not None:
status["db_location"] = calculated_location
return calculated_location
def _sync_t0xd9_location_selection(self, location):
# 同步T0xD9复式洗衣机的db_location和db_location_selection
if location == 1:
self._attributes["db_location_selection"] = "left"
elif location == 2:
self._attributes["db_location_selection"] = "right"
def _adjust_t0xd9_control_status(self, running_status):
# 依据db_running_status调整T0xD9复式洗衣机的db_control_status
# 根据运行状态确定控制状态, 只有当运行状态是"start"时,控制状态才为"start"
if running_status == "start":
return "start"
control_status = "start"
# 其他所有情况(包括standby、pause、off、error等)控制状态应为pause
else:
return "pause"
control_status = "pause"
self._attributes["db_control_status"] = control_status
@property
def device_name(self):
@@ -179,39 +214,14 @@ class MiedaDevice(threading.Thread):
new_status[attr] = self._attributes.get(attr)
new_status[attribute] = value
# 针对T0xD9复式洗衣机切换筒选择时,立即刷新状态以显示新筒的状态
if self._device_type == 0xD9 and attribute == "db_location_selection":
# 更新属性
self._attributes[attribute] = value
# 更新db_location用于查询
if value == "left":
self._attributes["db_location"] = 1
elif value == "right":
self._attributes["db_location"] = 2
# 立即刷新状态以显示新筒的状态
await self.refresh_status()
# 获取当前运行状态
running_status = self._attributes.get("db_running_status")
if running_status is not None:
# 根据运行状态确定控制状态
control_status = self._determine_control_status_based_on_running(running_status)
# 更新本地属性
self._attributes["db_control_status"] = control_status
# 添加到要发送的状态中(如果需要发送到云端)
new_status["db_control_status"] = control_status
# return # 发送到云端所以注释teturn
# 针对T0xD9复式洗衣机根据选择的筒添加db_location参数
if self._device_type == 0xD9 and attribute != "db_location_selection":
location_selection = self._attributes.get("db_location_selection", "left")
if location_selection == "left":
new_status["db_location"] = 1
elif location_selection == "right":
new_status["db_location"] = 2
# 针对T0xD9复式洗衣机本地变更 db_location_selection 时,调整 db_location
if self._device_type == 0xD9:
if attribute == "db_location_selection":
self._handle_t0xd9_db_location_selection(new_status, value)
# 非 db_location_selection 更新,根据 db_position 设置 db_location
else:
self._adjust_t0xd9_db_location_based_on_position(new_status)
# Convert dot-notation attributes to nested structure for transmission
nested_status = self._convert_to_nested_structure(new_status)
@@ -239,31 +249,6 @@ class MiedaDevice(threading.Thread):
await cloud.send_device_control(self._device_id, control=nested_status, status=self._attributes)
async def set_attributes(self, attributes):
# 针对T0xD9复式洗衣机当切换筒选择时
if self._device_type == 0xD9 and "db_location_selection" in attributes:
location_selection = attributes["db_location_selection"]
# 更新本地属性
self._attributes["db_location_selection"] = location_selection
# 更新db_location用于查询
if location_selection == "left":
self._attributes["db_location"] = 1
elif location_selection == "right":
self._attributes["db_location"] = 2
# 立即刷新状态以显示新筒的状态
await self.refresh_status()
# 获取当前运行状态
running_status = self._attributes.get("db_running_status")
if running_status is not None:
# 根据运行状态确定控制状态
control_status = self._determine_control_status_based_on_running(running_status)
# 更新本地属性
self._attributes["db_control_status"] = control_status
# return # 发送到云端所以注释teturn
new_status = {}
for attr in self._centralized:
new_status[attr] = self._attributes.get(attr)
@@ -273,25 +258,15 @@ class MiedaDevice(threading.Thread):
has_new = True
new_status[attribute] = value
# 针对T0xD9复式洗衣机确保发送到云端的控制命令包含筒位置信息
# 针对T0xD9复式洗衣机根据 db_location_selection 调整 db_location
if self._device_type == 0xD9:
# 如果attributes中有db_location_selection确保new_status也有
if "db_location_selection" in attributes:
location_selection = attributes["db_location_selection"]
new_status["db_location_selection"] = location_selection
# 添加对应的db_location
if location_selection == "left":
new_status["db_location"] = 1
elif location_selection == "right":
new_status["db_location"] = 2
# 如果没有db_location_selection但当前有选择添加db_location
elif "db_location_selection" not in attributes and self._attributes.get("db_location_selection"):
location_selection = self._attributes.get("db_location_selection", "left")
if location_selection == "left":
new_status["db_location"] = 1
elif location_selection == "right":
new_status["db_location"] = 2
self._handle_t0xd9_db_location_selection(new_status, location_selection)
else:
# 非 db_location_selection 更新,根据 db_position 设置 db_location
self._adjust_t0xd9_db_location_based_on_position(new_status)
# Convert dot-notation attributes to nested structure for transmission
nested_status = self._convert_to_nested_structure(new_status)
@@ -381,15 +356,15 @@ class MiedaDevice(threading.Thread):
async def refresh_status(self):
for query in self._queries:
# 针对T0xD9复式洗衣机根据选择的筒动态添加db_location参数
# 针对T0xD9复式洗衣机根据 db_position 动态调整 db_location
actual_query = query.copy() if isinstance(query, dict) else query
if self._device_type == 0xD9 and isinstance(actual_query, dict):
location_selection = self._attributes.get("db_location_selection", "left")
if location_selection == "left":
actual_query["db_location"] = 1
elif location_selection == "right":
actual_query["db_location"] = 2
# 根据 db_position 调整 db_location
calculated_location = self._adjust_t0xd9_db_location_based_on_position(actual_query)
# 同步更新db_location_selection
self._sync_t0xd9_location_selection(calculated_location)
cloud = self._cloud
if cloud and hasattr(cloud, "get_device_status"):
if isinstance(cloud, MSmartHomeCloud):
@@ -434,6 +409,12 @@ class MiedaDevice(threading.Thread):
if single not in self._attributes or self._attributes[single] != value:
# self._attributes[single] = value
new_status[single] = value
# 对于T0xD9复式洗衣机依据云端 db_running_status调整本地 db_control_status
if self._device_type == 0xD9 and "db_running_status" in new_status:
running_status = new_status["db_running_status"]
self._adjust_t0xd9_control_status(running_status)
if len(new_status) > 0:
for c in self._calculate_get:
lvalue = c.get("lvalue")
@@ -623,5 +604,3 @@ class MiedaDevice(threading.Thread):
# f"{e.__traceback__.tb_lineno}, {repr(e)}")
# self.disconnect()
# break

View File

@@ -8,15 +8,19 @@ DEVICE_MAPPING = {
"queries": [{}],
"centralized": [],
"entities": {
Platform.SELECT: {
"fan_setting": {
"options": {
Platform.VACUUM: {
"vacuum": {
"battery_level": "battery_percent",
"status": "work_status",
"fan_speeds": {
"soft": {"level": "soft"},
"normal": {"level": "normal"},
"high": {"level": "high"},
"super": {"level": "super"}
}
},
}
},
Platform.SELECT: {
"work_mode": {
"options": {
"sweep_and_mop": {"work_mode": "sweep_and_mop"},
@@ -27,15 +31,6 @@ DEVICE_MAPPING = {
},
"work_status": {
"options": {
"charge": {"work_status": "charge"},
"charge_pause": {"work_status": "charge_pause"},
"charge_continue": {"work_status": "charge_continue"},
"auto_clean": {"work_status": "auto_clean"},
"auto_clean_pause": {"work_status": "auto_clean_pause"},
"auto_clean_continue": {"work_status": "auto_clean_continue"},
"pause": {"work_status": "pause"},
"stop": {"work_status": "stop"},
"work": {"work_status": "work"},
"video_cruise_start": {"work_status": "video_cruise_start"},
"video_cruise_pause": {"work_status": "video_cruise_pause"},
"mop_clean": {"mop_clean_setting": {"mode_type": "common", "clean_level": "normal"}},
@@ -125,27 +120,18 @@ DEVICE_MAPPING = {
],
"centralized": ["work_status", "battery_percent", "sweep_mop_mode", "mop", "sub_work_status"],
"entities": {
Platform.SELECT: {
"work_status": {
"options": {
"charge": {"work_status": "charge"},
"charge_pause": {"work_status": "charge_pause"},
"charge_continue": {"work_status": "charge_continue"},
"auto_clean": {"work_status": "auto_clean"},
"auto_clean_pause": {"work_status": "auto_clean_pause"},
"auto_clean_continue": {"work_status": "auto_clean_continue"},
"pause": {"work_status": "pause"},
"stop": {"work_status": "stop"},
"work": {"work_status": "work"}
}
},
"fan_level": {
"options": {
Platform.VACUUM: {
"vacuum": {
"battery_level": "battery_percent",
"status": "work_status",
"fan_speeds": {
"soft": {"fan_setting": {"level": "soft"}},
"normal": {"fan_setting": {"level": "normal"}},
"high": {"fan_setting": {"level": "high"}}
}
},
}
},
Platform.SELECT: {
"sweep_mop_mode": {
"options": {
"sweep_and_mop": {"work_mode_setting": {"work_mode": "sweep_and_mop"}},

View File

@@ -19,17 +19,21 @@ DEVICE_MAPPING = {
},
"entities": {
Platform.BINARY_SENSOR: {
"db_power": {
"device_class": BinarySensorDeviceClass.RUNNING,
},
"door_opened": {
"db_door_opened": {
"device_class": BinarySensorDeviceClass.OPENING,
"translation_key": "door_opened"
},
"bucket_water_overheating": {
"db_bucket_water_overheating": {
"device_class": BinarySensorDeviceClass.PROBLEM,
"translation_key": "bucket_water_overheating"
},
"drying_tunnel_overheating": {
"db_drying_tunnel_overheating": {
"device_class": BinarySensorDeviceClass.PROBLEM,
"translation_key": "drying_tunnel_overheating"
},
"db_detergent_needed": {
"device_class": BinarySensorDeviceClass.PROBLEM,
"translation_key": "detergent_lack"
}
},
Platform.SWITCH: {

View File

@@ -3397,6 +3397,21 @@
"sedentary_remind": {
"name": "Sedentary Remind"
}
},
"vacuum": {
"vacuum": {
"name": "Robotic Vacuum",
"state_attributes": {
"fan_speed": {
"state": {
"soft": "Soft",
"normal": "Normal",
"high": "High",
"super": "Super"
}
}
}
}
}
}
}

View File

@@ -3728,6 +3728,21 @@
"sedentary_remind": {
"name": "久坐提醒"
}
},
"vacuum": {
"vacuum": {
"name": "扫地机器人",
"state_attributes": {
"fan_speed": {
"state": {
"soft": "轻柔",
"normal": "标准",
"high": "强力",
"super": "超强"
}
}
}
}
}
}
}

View File

@@ -0,0 +1,195 @@
from homeassistant.components.vacuum import (
StateVacuumEntity,
VacuumEntityFeature,
VacuumActivity,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform
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
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up vacuum entities for Midea devices."""
# 账号型 entry从 __init__ 写入的 accounts 桶加载设备和协调器
account_bucket = hass.data.get(DOMAIN, {}).get("accounts", {}).get(config_entry.entry_id)
if not account_bucket:
async_add_entities([])
return
device_list = account_bucket.get("device_list", {})
coordinator_map = account_bucket.get("coordinator_map", {})
devs = []
for device_id, info in device_list.items():
device_type = info.get("type")
sn8 = info.get("sn8")
config = await load_device_config(hass, device_type, sn8) or {}
entities_cfg = (config.get("entities") or {}).get(Platform.VACUUM, {})
manufacturer = config.get("manufacturer")
rationale = config.get("rationale")
coordinator = coordinator_map.get(device_id)
device = coordinator.device if coordinator else None
for entity_key, ecfg in entities_cfg.items():
devs.append(MideaVacuumEntity(
coordinator, device, manufacturer, rationale, entity_key, ecfg
))
async_add_entities(devs)
class MideaVacuumEntity(MideaEntity, StateVacuumEntity):
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._key_battery_level = self._config.get("battery_level")
self._key_status = self._config.get("status")
self._key_fan_speeds = self._config.get("fan_speeds")
#self._key_locate = self._config.get("locate")
#self._key_clean_spot = self._config.get("clean_spot")
#self._key_map = self._config.get("map")
@property
def supported_features(self):
features = VacuumEntityFeature(0)
features |= VacuumEntityFeature.STOP
features |= VacuumEntityFeature.PAUSE
features |= VacuumEntityFeature.START
features |= VacuumEntityFeature.RETURN_HOME
features |= VacuumEntityFeature.FAN_SPEED
features |= VacuumEntityFeature.STATUS
features |= VacuumEntityFeature.BATTERY
#features |= VacuumEntityFeature.LOCATE
#features |= VacuumEntityFeature.CLEAN_SPOT
#features |= VacuumEntityFeature.MAP
return features
@property
def battery_level(self):
"""Return the battery level of the vacuum cleaner."""
battery = self._get_nested_value(self._key_battery_level)
if battery is not None:
try:
return int(battery)
except (ValueError, TypeError):
return None
return None
@property
def status(self):
"""Return the status of the vacuum cleaner."""
status = self._get_nested_value(self._key_status)
if status is not None:
return status
return None
@property
def state(self):
"""Return the state of the vacuum cleaner."""
status = self.status
if not status:
return None
# Map Midea status to Home Assistant states
status_mapping = {
# === 清洁中状态 (CLEANING) ===
"work": VacuumActivity.CLEANING, # 清扫中
"auto_clean": VacuumActivity.CLEANING, # 自动清扫中
# === 已停靠状态 (DOCKED) ===
"charging_on_dock": VacuumActivity.DOCKED, # 座充中
"on_base": VacuumActivity.DOCKED, # 在基站上
"charge_finish": VacuumActivity.DOCKED, # 充电完成
# === 空闲状态 (IDLE) ===
"stop": VacuumActivity.IDLE, # 已停止
"sleep": VacuumActivity.IDLE, # 休眠中
# === 暂停状态 (PAUSED) ===
"clean_pause": VacuumActivity.PAUSED, # 清扫暂停
"charge_pause": VacuumActivity.PAUSED, # 充电暂停
# === 返回中状态 (RETURNING) ===
"charging": VacuumActivity.RETURNING, # 返回基站中
# === 错误状态 (ERROR) ===
"error": VacuumActivity.ERROR, # 错误
}
return status_mapping.get(status, status)
@property
def fan_speed(self):
"""Return the current fan speed."""
return self._dict_get_selected(self._key_fan_speeds)
@property
def fan_speed_list(self):
"""Return the list of available fan speeds."""
return list(self._key_fan_speeds.keys())
async def async_start(self):
"""Start or resume the cleaning task."""
# 设置为工作状态
if self._key_status:
await self.async_set_attribute(self._key_status, "work")
else:
await self._async_set_status_on_off(self._key_power, True)
async def async_stop(self):
"""Stop the vacuum cleaner."""
# 设置为停止状态
if self._key_status:
await self.async_set_attribute(self._key_status, "stop")
else:
await self._async_set_status_on_off(self._key_power, False)
async def async_pause(self):
"""Pause the cleaning task."""
# 设置为暂停状态
if self._key_status:
await self.async_set_attribute(self._key_status, "pause")
async def async_return_to_base(self):
"""Return the vacuum cleaner to its base."""
# 设置为回基站状态
if self._key_status:
await self.async_set_attribute(self._key_status, "charge")
async def async_set_fan_speed(self, fan_speed: str):
"""Set the fan speed."""
new_status = self._key_fan_speeds.get(fan_speed)
if new_status is not None:
await self.async_set_attributes(new_status)
#async def async_locate(self):
#"""Locate the vacuum cleaner."""
# 定位设备
# 具体实现取决于设备的控制方式
#if hasattr(self, "_key_locate"):
#await self.async_set_attribute(self._key_locate, True)
#async def async_clean_spot(self):
#"""Perform a clean spot."""
# 执行定点清扫
# 具体实现取决于设备的控制方式
#if hasattr(self, "_key_clean_spot"):
#await self.async_set_attribute(self._key_clean_spot, True)