diff --git a/README.md b/README.md index e3e488f..d8c5fa1 100644 --- a/README.md +++ b/README.md @@ -15,11 +15,13 @@ ## 目前支持的设备类型 +- T0x21 中央空调网关 - T0x26 浴霸 - T0xA1 除湿机 - T0xAC 空调 - T0xB2 电蒸箱 - T0xB3 消毒碗柜 +- T0xB6 抽油烟机 - T0xB7 燃气灶 - T0xB8 智能扫地机器人 - T0xCA 对开门冰箱 diff --git a/custom_components/midea_auto_cloud/climate.py b/custom_components/midea_auto_cloud/climate.py index 448ad9e..5291bfd 100644 --- a/custom_components/midea_auto_cloud/climate.py +++ b/custom_components/midea_auto_cloud/climate.py @@ -51,6 +51,9 @@ async def async_setup_entry( class MideaClimateEntity(MideaEntity, ClimateEntity): def __init__(self, coordinator, device, manufacturer, rationale, entity_key, config): + # 自动判断是否为中央空调设备(T0x21) + self._is_central_ac = device.device_type == 0x21 + super().__init__( coordinator, device.device_id, @@ -108,23 +111,31 @@ class MideaClimateEntity(MideaEntity, ClimateEntity): @property def target_temperature(self): - if isinstance(self._key_target_temperature, list): - temp_int = self._get_nested_value(self._key_target_temperature[0]) - tem_dec = self._get_nested_value(self._key_target_temperature[1]) - if temp_int is not None and tem_dec is not None: - try: - return float(temp_int) + float(tem_dec) - except (ValueError, TypeError): - return None + if self._is_central_ac: + run_mode = self._get_nested_value(self._key_power) or "0" + if run_mode == "2": # 制冷模式 + return self._get_nested_value("cool_temp_set") + elif run_mode == "3": # 制热模式 + return self._get_nested_value("cool_temp_set") return None else: - temp = self._get_nested_value(self._key_target_temperature) - if temp is not None: - try: - return float(temp) - except (ValueError, TypeError): - return None - return None + if isinstance(self._key_target_temperature, list): + temp_int = self._get_nested_value(self._key_target_temperature[0]) + tem_dec = self._get_nested_value(self._key_target_temperature[1]) + if temp_int is not None and tem_dec is not None: + try: + return float(temp_int) + float(tem_dec) + except (ValueError, TypeError): + return None + return None + else: + temp = self._get_nested_value(self._key_target_temperature) + if temp is not None: + try: + return float(temp) + except (ValueError, TypeError): + return None + return None @property def min_temp(self): @@ -166,11 +177,21 @@ class MideaClimateEntity(MideaEntity, ClimateEntity): @property def swing_modes(self): - return list(self._key_swing_modes.keys()) + if self._is_central_ac: + return ["off", "on"] + else: + return list(self._key_swing_modes.keys()) @property def swing_mode(self): - return self._dict_get_selected(self._key_swing_modes) + if self._is_central_ac: + extflag = self._get_nested_value("extflag") or "0" + # extflag: 4=摇摆, 6=电辅热+摇摆 + if extflag in ["4", "6"]: + return "on" + return "off" + else: + return self._dict_get_selected(self._key_swing_modes) @property def is_on(self) -> bool: @@ -204,35 +225,80 @@ class MideaClimateEntity(MideaEntity, ClimateEntity): if ATTR_TEMPERATURE not in kwargs: return temperature = kwargs.get(ATTR_TEMPERATURE) - temp_int, temp_dec = divmod(temperature, 1) - temp_int = int(temp_int) - hvac_mode = kwargs.get(ATTR_HVAC_MODE) - if hvac_mode is not None: - new_status = self._key_hvac_modes.get(hvac_mode) + + if self._is_central_ac: + run_mode = self._get_nested_value(self._key_power) or "0" + control = {} + + if run_mode == "2": # 制冷模式 + control["cooling_temp"] = str(temperature) + elif run_mode == "3": # 制热模式 + control["cooling_temp"] = str(temperature) + control["heating_temp"] = str(temperature) + + if control: + await self.coordinator.async_send_central_ac_control(control) else: - new_status = {} - if isinstance(self._key_target_temperature, list): - new_status[self._key_target_temperature[0]] = temp_int - new_status[self._key_target_temperature[1]] = temp_dec - else: - new_status[self._key_target_temperature] = temperature - await self.async_set_attributes(new_status) + temp_int, temp_dec = divmod(temperature, 1) + temp_int = int(temp_int) + hvac_mode = kwargs.get(ATTR_HVAC_MODE) + if hvac_mode is not None: + new_status = self._key_hvac_modes.get(hvac_mode) + else: + new_status = {} + if isinstance(self._key_target_temperature, list): + new_status[self._key_target_temperature[0]] = temp_int + new_status[self._key_target_temperature[1]] = temp_dec + else: + new_status[self._key_target_temperature] = temperature + await self.async_set_attributes(new_status) async def async_set_fan_mode(self, fan_mode: str): - new_status = self._key_fan_modes.get(fan_mode) - await self.async_set_attributes(new_status) + if self._is_central_ac: + fan_speed = self._key_fan_modes.get(fan_mode) + await self.coordinator.async_send_central_ac_control(fan_speed) + else: + new_status = self._key_fan_modes.get(fan_mode) + await self.async_set_attributes(new_status) async def async_set_preset_mode(self, preset_mode: str): - new_status = self._key_preset_modes.get(preset_mode) - await self.async_set_attributes(new_status) + if self._is_central_ac: + new_status = self._key_preset_modes.get(preset_mode) + await self.coordinator.async_send_central_ac_control(new_status) + else: + new_status = self._key_preset_modes.get(preset_mode) + await self.async_set_attributes(new_status) async def async_set_hvac_mode(self, hvac_mode: str): - new_status = self._key_hvac_modes.get(hvac_mode) - await self.async_set_attributes(new_status) + if self._is_central_ac: + run_mode = self._key_hvac_modes.get(hvac_mode) + await self.coordinator.async_send_central_ac_control(run_mode) + else: + new_status = self._key_hvac_modes.get(hvac_mode) + await self.async_set_attributes(new_status) async def async_set_swing_mode(self, swing_mode: str): - new_status = self._key_swing_modes.get(swing_mode) - await self.async_set_attributes(new_status) + if self._is_central_ac: + current_extflag = self._get_nested_value("extflag") or "0" + + if swing_mode == "on": + # 开启摆风:如果当前有电辅热(2),则设为6(电辅热+摆风),否则设为4(摆风) + if current_extflag == "2": + new_extflag = "6" # 电辅热+摆风 + else: + new_extflag = "4" # 仅摆风 + else: + # 关闭摆风:如果当前是6(电辅热+摆风),则设为2(仅电辅热),否则设为0(关闭) + if current_extflag == "6": + new_extflag = "2" # 仅电辅热 + else: + new_extflag = "0" # 关闭 + + control = {"extflag": new_extflag} + await self.coordinator.async_send_central_ac_control(control) + else: + new_status = self._key_swing_modes.get(swing_mode) + await self.async_set_attributes(new_status) async def async_turn_aux_heat_on(self) -> None: await self._async_set_status_on_off(self._key_aux_heat, True) diff --git a/custom_components/midea_auto_cloud/core/cloud.py b/custom_components/midea_auto_cloud/core/cloud.py index fe84257..7f93e98 100644 --- a/custom_components/midea_auto_cloud/core/cloud.py +++ b/custom_components/midea_auto_cloud/core/cloud.py @@ -198,6 +198,14 @@ class MideaCloud: 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() + + async def send_central_ac_control(self, appliance_code: int, nodeid: str, modelid: str, idtype: int, control: dict) -> bool: + """Send control to central AC subdevice. Subclasses should implement if supported.""" + raise NotImplementedError() + + async def get_central_ac_status(self, appliance_codes: list) -> dict | None: + """Get status of central AC devices. Subclasses should implement if supported.""" + raise NotImplementedError() class MeijuCloud(MideaCloud): @@ -223,6 +231,7 @@ class MeijuCloud(MideaCloud): password=password, api_url=clouds[cloud_name]["api_url"] ) + self._homegroup_id = None async def login(self) -> bool: if login_id := await self._get_login_id(): @@ -275,6 +284,8 @@ class MeijuCloud(MideaCloud): return None async def list_appliances(self, home_id) -> dict | None: + # 存储当前使用的 homegroupId 用于后续的中央空调控制 + self._homegroup_id = str(home_id) data = { "homegroupId": home_id } @@ -333,6 +344,66 @@ class MeijuCloud(MideaCloud): data=data ) return response is not None + + async def send_central_ac_control(self, appliance_code: int, nodeid: str, modelid: str, idtype: int, control: dict) -> bool: + """Send control to central AC subdevice using the special T0x21 API.""" + import uuid + import json + + # 构建中央空调控制命令 + command_data = { + "nodeid": nodeid, + "acattri_ctrl": { + "aclist": [{ + "nodeid": nodeid, + "modelid": modelid, + "type": idtype + }], + "event": control + } + } + + # 构建完整的请求数据 + request_data = { + "applianceCode": str(appliance_code), + "modelId": modelid, + "topic": "/subdevice/multicontrol", + "command": command_data, + "msgId": str(uuid.uuid4()).replace("-", "") + } + request_data_str = json.dumps(request_data).encode("utf-8") + MideaLogger.debug(f"Sending control to central AC device {appliance_code}: {request_data_str}") + # 发送到特殊的中央空调API + if response := await self._api_request( + endpoint="/v1/gateway/transport/send", + data={ + 'applianceCode': str(appliance_code), + 'order': self._security.aes_encrypt(request_data_str).hex(), + 'homegroupId': self._homegroup_id, + } + ): + if response and response.get('reply'): + reply_data = self._security.aes_decrypt(bytes.fromhex(response['reply'])) + MideaLogger.debug(f"[{appliance_code}] Gateway command response: {reply_data}") + return reply_data + else: + MideaLogger.warning(f"[{appliance_code}] Gateway command failed: {response}") + + + async def get_central_ac_status(self, appliance_codes: list) -> dict | None: + """Get status of central AC devices using the aggregator API.""" + + # 构建请求数据 + request_data = { + "entities": ["endlist", "tips"], + "appliances": [{"id": str(code), "type": "0x21"} for code in appliance_codes], + } + + response = await self._api_request( + endpoint="/api/v1/aggregator/appliances", + data=request_data + ) + return response async def download_lua( self, path: str, diff --git a/custom_components/midea_auto_cloud/core/device.py b/custom_components/midea_auto_cloud/core/device.py index adf8bcb..0c47139 100644 --- a/custom_components/midea_auto_cloud/core/device.py +++ b/custom_components/midea_auto_cloud/core/device.py @@ -272,10 +272,6 @@ class MiedaDevice(threading.Thread): if self._lua_runtime is not None: if query_cmd := self._lua_runtime.build_query(query): await self._build_send(query_cmd) - else: - cloud = self._cloud - if cloud and hasattr(cloud, "get_device_status"): - await cloud.get_device_status(self._device_id, query=query) def _parse_cloud_message(self, decrypted): # MideaLogger.debug(f"Received: {decrypted}") diff --git a/custom_components/midea_auto_cloud/data_coordinator.py b/custom_components/midea_auto_cloud/data_coordinator.py index 05c2c9e..2dc3f75 100644 --- a/custom_components/midea_auto_cloud/data_coordinator.py +++ b/custom_components/midea_auto_cloud/data_coordinator.py @@ -90,17 +90,11 @@ class MideaDataUpdateCoordinator(DataUpdateCoordinator[MideaDeviceData]): return self.data try: - await self.device.refresh_status() - # # 使用传入的 cloud 实例(若可用) - # cloud = self._cloud - # 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}") + # 检查是否为中央空调设备(T0x21) + if self.device.device_type == 0x21: + await self._poll_central_ac_state() + else: + await self.device.refresh_status() # 返回并推送当前状态 updated = MideaDeviceData( @@ -117,6 +111,51 @@ class MideaDataUpdateCoordinator(DataUpdateCoordinator[MideaDeviceData]): available=False, connected=False, ) + + async def _poll_central_ac_state(self) -> None: + """轮询中央空调状态""" + try: + cloud = self._cloud + if cloud and hasattr(cloud, "get_central_ac_status"): + status_data = await cloud.get_central_ac_status([self._device_id]) + if status_data and "appliances" in status_data: + # 找到对应的设备数据并更新到设备属性中 + for appliance in status_data["appliances"]: + if appliance.get("type") == "0x21" and "extraData" in appliance: + extra_data = appliance["extraData"] + if "attr" in extra_data and "state" in extra_data["attr"]: + state = extra_data["attr"]["state"] + + if "nodeid" in extra_data["attr"]: + self.device._attributes["nodeid"] = extra_data["attr"]["nodeid"] + if "masterId" in extra_data["attr"]: + self.device._attributes["masterId"] = extra_data["attr"]["masterId"] + if "modelid" in extra_data["attr"]: + self.device._attributes["modelid"] = extra_data["attr"]["modelid"] + if "idType" in extra_data["attr"]: + self.device._attributes["idType"] = extra_data["attr"]["idType"] + + if "condition_attribute" in state: + condition = state["condition_attribute"] + # 将状态数据更新到设备属性中 + for key, value in condition.items(): + # 尝试将数字字符串转换为数字 + if key.find("temp") > -1: + try: + # 尝试转换为整数 + if '.' not in value: + self.device._attributes[key] = int(value) + else: + # 尝试转换为浮点数 + self.device._attributes[key] = float(value) + except (ValueError, TypeError): + # 如果转换失败,保持原值 + self.device._attributes[key] = value + else: + self.device._attributes[key] = value + break + except Exception as e: + MideaLogger.debug(f"Error polling central AC state: {e}") async def async_set_attribute(self, attribute: str, value) -> None: """Set a device attribute.""" @@ -140,4 +179,75 @@ class MideaDataUpdateCoordinator(DataUpdateCoordinator[MideaDeviceData]): self.device.send_command(cmd_type, cmd_body_bytes) except ValueError as e: _LOGGER.error(f"Invalid command body: {e}") - raise \ No newline at end of file + raise + + async def async_send_central_ac_control(self, control: dict) -> bool: + """发送中央空调控制命令""" + try: + cloud = self._cloud + if cloud and hasattr(cloud, "send_central_ac_control"): + # 从设备属性中获取nodeid + masterid = self.device.attributes.get("masterId") + nodeid = self.device.attributes.get("nodeid") + modelid = self.device.attributes.get("modelid") + idtype = int(self.device.attributes.get("idType")) + + if not nodeid: + MideaLogger.warning(f"No nodeid found for central AC device {self._device_id}") + return False + + # 构建完整的控制命令,包含centralized中的所有字段 + full_control = self._build_full_central_ac_control(control) + MideaLogger.debug(f"Sending control to {self.device.device_name}: {full_control}") + success = await cloud.send_central_ac_control( + masterid, + nodeid, + modelid, + idtype, + full_control + ) + + if success: + # 更新本地状态 + self.device.attributes.update(control) + self.mute_state_update_for_a_while() + self.async_update_listeners() + return True + else: + MideaLogger.debug(f"Failed to send control to {self.device.device_name}") + return False + else: + MideaLogger.debug("Cloud service not available for central AC control") + return False + except Exception as e: + MideaLogger.debug(f"Error sending control to {self.device.device_name}: {e}") + return False + + def _build_full_central_ac_control(self, new_control: dict) -> dict: + """构建完整控制命令""" + full_control = {} + full_control["run_mode"] = self.device.attributes.get("run_mode") + full_control["cooling_temp"] = str(self.device.attributes.get("cool_temp_set") or 26.0) + full_control["heating_temp"] = str(self.device.attributes.get("heat_temp_set") or 20.0) + full_control["fan_speed"] = self.device.attributes.get("fan_speed") + swing_mode = self.device.attributes.get("is_swing") + is_elec_heat = self.device.attributes.get("is_elec_heat") + + if swing_mode == "1": + # 开启摆风:如果当前有电辅热(2),则设为6(电辅热+摆风),否则设为4(摆风) + if is_elec_heat == "1": + new_extflag = "6" # 电辅热+摆风 + else: + new_extflag = "4" # 仅摆风 + else: + # 关闭摆风:如果当前是6(电辅热+摆风),则设为2(仅电辅热),否则设为0(关闭) + if is_elec_heat == "1": + new_extflag = "2" # 仅电辅热 + else: + new_extflag = "0" # 关闭 + + full_control["extflag"] = new_extflag + + # 然后用新的控制值覆盖 + full_control.update(new_control) + return full_control \ No newline at end of file diff --git a/custom_components/midea_auto_cloud/device_mapping/T0x21.py b/custom_components/midea_auto_cloud/device_mapping/T0x21.py new file mode 100644 index 0000000..19c3e7e --- /dev/null +++ b/custom_components/midea_auto_cloud/device_mapping/T0x21.py @@ -0,0 +1,89 @@ +from homeassistant.const import Platform, UnitOfTemperature, PRECISION_HALVES +from homeassistant.components.sensor import SensorStateClass, SensorDeviceClass +from homeassistant.components.switch import SwitchDeviceClass + +DEVICE_MAPPING = { + "default": { + "rationale": ["off", "on"], + "queries": [{}], + "centralized": ["run_mode", "fan_speed", "cooling_temp", "heating_temp", "extflag"], + "entities": { + Platform.CLIMATE: { + "thermostat": { + "power": "run_mode", + "hvac_modes": { + "off": {"run_mode": "0"}, + "fan_only": {"run_mode": "1"}, + "cool": {"run_mode": "2"}, + "heat": {"run_mode": "3"}, + "auto": {"run_mode": "4"}, + "dry": {"run_mode": "5"} + }, + "fan_modes": { + "off": {"fan_speed": "0"}, + "1": {"fan_speed": "1"}, + "2": {"fan_speed": "2"}, + "3": {"fan_speed": "3"}, + "4": {"fan_speed": "4"}, + "5": {"fan_speed": "5"}, + "6": {"fan_speed": "6"}, + "7": {"fan_speed": "7"}, + "auto": {"fan_speed": "8"} + }, + "preset_modes": { + "none": {"extflag": "0"}, + "electric_heat": {"extflag": "2"}, + "swing": {"extflag": "4"}, + "electric_heat_swing": {"extflag": "6"} + }, + "target_temperature": ["temperature", "small_temperature"], + "current_temperature": "room_temp", + "pre_mode": "mode", + "aux_heat": "ptc", + "min_temp": 17, + "max_temp": 30, + "temperature_unit": UnitOfTemperature.CELSIUS, + "precision": PRECISION_HALVES, + } + }, + Platform.SWITCH: { + "is_lock_heat": { + "device_class": SwitchDeviceClass.SWITCH, + "rationale": ['0', '1'] + }, + "is_lock_cool": { + "device_class": SwitchDeviceClass.SWITCH, + "rationale": ['0', '1'] + }, + "fan_speed_lock": { + "device_class": SwitchDeviceClass.SWITCH, + "rationale": ['0', '1'] + }, + "is_lock_rc": { + "device_class": SwitchDeviceClass.SWITCH, + "rationale": ['0', '1'] + }, + }, + Platform.SENSOR: { + "room_temp": { + "device_class": SensorDeviceClass.TEMPERATURE, + "unit_of_measurement": UnitOfTemperature.CELSIUS, + "state_class": SensorStateClass.MEASUREMENT, + "precision": PRECISION_HALVES + }, + "cool_temp_set": { + "device_class": SensorDeviceClass.TEMPERATURE, + "unit_of_measurement": UnitOfTemperature.CELSIUS, + "state_class": SensorStateClass.MEASUREMENT, + "precision": PRECISION_HALVES + }, + "heat_temp_set": { + "device_class": SensorDeviceClass.TEMPERATURE, + "unit_of_measurement": UnitOfTemperature.CELSIUS, + "state_class": SensorStateClass.MEASUREMENT, + "precision": PRECISION_HALVES + }, + } + } + } +} diff --git a/custom_components/midea_auto_cloud/device_mapping/T0xA1.py b/custom_components/midea_auto_cloud/device_mapping/T0xA1.py index 3779c2d..a05d292 100644 --- a/custom_components/midea_auto_cloud/device_mapping/T0xA1.py +++ b/custom_components/midea_auto_cloud/device_mapping/T0xA1.py @@ -25,6 +25,7 @@ DEVICE_MAPPING = { }, "filter_tip": { "device_class": SwitchDeviceClass.SWITCH, + "rationale": [0, 1] }, }, Platform.HUMIDIFIER: { diff --git a/custom_components/midea_auto_cloud/translations/en.json b/custom_components/midea_auto_cloud/translations/en.json index b8500e6..98cacd8 100644 --- a/custom_components/midea_auto_cloud/translations/en.json +++ b/custom_components/midea_auto_cloud/translations/en.json @@ -1486,6 +1486,15 @@ }, "water_full_level": { "name": "Water Full Level" + }, + "room_temp": { + "name": "Room Temperature" + }, + "cool_temp_set": { + "name": "Cool Temperature Set" + }, + "heat_temp_set": { + "name": "Heat Temperature Set" } }, "switch": { @@ -2328,6 +2337,18 @@ }, "cold_water_master": { "name": "Cold Water Master" + }, + "is_lock_heat": { + "name": "Heat Lock" + }, + "is_lock_cool": { + "name": "Cool Lock" + }, + "fan_speed_lock": { + "name": "Fan Speed Lock" + }, + "is_lock_rc": { + "name": "Remote Control Lock" } } } diff --git a/custom_components/midea_auto_cloud/translations/zh-Hans.json b/custom_components/midea_auto_cloud/translations/zh-Hans.json index 43f0344..ec764f0 100644 --- a/custom_components/midea_auto_cloud/translations/zh-Hans.json +++ b/custom_components/midea_auto_cloud/translations/zh-Hans.json @@ -1486,6 +1486,15 @@ }, "water_full_level": { "name": "满水水位" + }, + "room_temp": { + "name": "室内温度" + }, + "cool_temp_set": { + "name": "制冷设定温度" + }, + "heat_temp_set": { + "name": "制热设定温度" } }, "switch": { @@ -2328,6 +2337,18 @@ }, "cold_water_master": { "name": "单次零冷水" + }, + "is_lock_heat": { + "name": "制热锁定" + }, + "is_lock_cool": { + "name": "制冷锁定" + }, + "fan_speed_lock": { + "name": "风速锁定" + }, + "is_lock_rc": { + "name": "遥控锁定" } } }