diff --git a/custom_components/midea_auto_cloud/__init__.py b/custom_components/midea_auto_cloud/__init__.py index 3f671c3..ae8e85d 100644 --- a/custom_components/midea_auto_cloud/__init__.py +++ b/custom_components/midea_auto_cloud/__init__.py @@ -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) diff --git a/custom_components/midea_auto_cloud/core/device.py b/custom_components/midea_auto_cloud/core/device.py index d6c4f8e..152037e 100644 --- a/custom_components/midea_auto_cloud/core/device.py +++ b/custom_components/midea_auto_cloud/core/device.py @@ -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 = 1,db_location 保持不变 + calculated_location = current_location + elif db_position == 0: + # db_position = 0,db_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 - - diff --git a/custom_components/midea_auto_cloud/device_mapping/T0xB8.py b/custom_components/midea_auto_cloud/device_mapping/T0xB8.py index 2fff58f..73b0bda 100644 --- a/custom_components/midea_auto_cloud/device_mapping/T0xB8.py +++ b/custom_components/midea_auto_cloud/device_mapping/T0xB8.py @@ -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"}}, diff --git a/custom_components/midea_auto_cloud/device_mapping/T0xD9.py b/custom_components/midea_auto_cloud/device_mapping/T0xD9.py index 0c3a39c..6fb3636 100644 --- a/custom_components/midea_auto_cloud/device_mapping/T0xD9.py +++ b/custom_components/midea_auto_cloud/device_mapping/T0xD9.py @@ -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: { diff --git a/custom_components/midea_auto_cloud/translations/en.json b/custom_components/midea_auto_cloud/translations/en.json index d576d91..9ced33e 100644 --- a/custom_components/midea_auto_cloud/translations/en.json +++ b/custom_components/midea_auto_cloud/translations/en.json @@ -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" + } + } + } + } } } } diff --git a/custom_components/midea_auto_cloud/translations/zh-Hans.json b/custom_components/midea_auto_cloud/translations/zh-Hans.json index 625f2c6..c98f6a8 100644 --- a/custom_components/midea_auto_cloud/translations/zh-Hans.json +++ b/custom_components/midea_auto_cloud/translations/zh-Hans.json @@ -3728,6 +3728,21 @@ "sedentary_remind": { "name": "久坐提醒" } + }, + "vacuum": { + "vacuum": { + "name": "扫地机器人", + "state_attributes": { + "fan_speed": { + "state": { + "soft": "轻柔", + "normal": "标准", + "high": "强力", + "super": "超强" + } + } + } + } } } } diff --git a/custom_components/midea_auto_cloud/vacuum.py b/custom_components/midea_auto_cloud/vacuum.py new file mode 100644 index 0000000..a744526 --- /dev/null +++ b/custom_components/midea_auto_cloud/vacuum.py @@ -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)