fix: remote control for device T0xED.

This commit is contained in:
sususweet
2025-09-30 14:38:50 +08:00
parent 273d4e41bf
commit bbf4d168e7
11 changed files with 141 additions and 249 deletions

View File

@@ -13,7 +13,6 @@ 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 . import load_device_config from . import load_device_config
@@ -66,10 +65,6 @@ class MideaClimateEntity(MideaEntity, ClimateEntity):
rationale=rationale, rationale=rationale,
config=config, config=config,
) )
self._device = device
self._manufacturer = manufacturer
self._rationale = rationale
self._config = config
self._key_power = self._config.get("power") self._key_power = self._config.get("power")
self._key_hvac_modes = self._config.get("hvac_modes") self._key_hvac_modes = self._config.get("hvac_modes")
self._key_preset_modes = self._config.get("preset_modes") self._key_preset_modes = self._config.get("preset_modes")

View File

@@ -1,4 +1,5 @@
from homeassistant.const import Platform, UnitOfTemperature, PRECISION_HALVES, UnitOfTime, UnitOfElectricPotential, UnitOfVolume, UnitOfMass from homeassistant.const import Platform, UnitOfTemperature, PRECISION_HALVES, UnitOfTime, UnitOfElectricPotential, \
UnitOfVolume, UnitOfMass
from homeassistant.components.sensor import SensorStateClass, SensorDeviceClass from homeassistant.components.sensor import SensorStateClass, SensorDeviceClass
from homeassistant.components.binary_sensor import BinarySensorDeviceClass from homeassistant.components.binary_sensor import BinarySensorDeviceClass
from homeassistant.components.switch import SwitchDeviceClass from homeassistant.components.switch import SwitchDeviceClass
@@ -10,61 +11,44 @@ DEVICE_MAPPING = {
"centralized": [], "centralized": [],
"entities": { "entities": {
Platform.SWITCH: { Platform.SWITCH: {
"holiday_mode": { "power": {
"device_class": SwitchDeviceClass.SWITCH, "device_class": SwitchDeviceClass.SWITCH,
}, },
"water_way": { "heat_start": {
"device_class": SwitchDeviceClass.SWITCH,
"rationale": [0, 1],
},
"lock": {
"device_class": SwitchDeviceClass.SWITCH, "device_class": SwitchDeviceClass.SWITCH,
}, },
"soften": { "sleep": {
"device_class": SwitchDeviceClass.SWITCH, "device_class": SwitchDeviceClass.SWITCH,
}, },
"leak_water_protection": { "keep_warm": {
"device_class": SwitchDeviceClass.SWITCH, "device_class": SwitchDeviceClass.SWITCH,
}, },
"cl_sterilization": { "vacation": {
"device_class": SwitchDeviceClass.SWITCH, "device_class": SwitchDeviceClass.SWITCH,
}, },
"micro_leak": { "germicidal": {
"device_class": SwitchDeviceClass.SWITCH, "device_class": SwitchDeviceClass.SWITCH,
}, },
"low_salt": { "lack_water": {
"device_class": SwitchDeviceClass.SWITCH, "device_class": SwitchDeviceClass.SWITCH,
}, },
"no_salt": { "drainage": {
"device_class": SwitchDeviceClass.SWITCH, "device_class": SwitchDeviceClass.SWITCH,
}, },
"low_battery": { "wash_enable": {
"device_class": SwitchDeviceClass.SWITCH, "device_class": SwitchDeviceClass.SWITCH,
}, },
"salt_level_sensor_error": {
"device_class": SwitchDeviceClass.SWITCH,
},
"flowmeter_error": {
"device_class": SwitchDeviceClass.SWITCH,
},
"leak_water": {
"device_class": SwitchDeviceClass.SWITCH,
},
"micro_leak_protection": {
"device_class": SwitchDeviceClass.SWITCH,
},
"maintenance_reminder_switch": {
"device_class": SwitchDeviceClass.SWITCH,
},
"rsj_stand_by": {
"device_class": SwitchDeviceClass.SWITCH,
},
"regeneration": {
"device_class": SwitchDeviceClass.SWITCH,
},
"pre_regeneration": {
"device_class": SwitchDeviceClass.SWITCH,
}
}, },
Platform.BINARY_SENSOR: { Platform.BINARY_SENSOR: {
"maintenance_remind": { "heat_status": {
"device_class": BinarySensorDeviceClass.PROBLEM, "device_class": BinarySensorDeviceClass.RUNNING,
},
"standby_status": {
"device_class": BinarySensorDeviceClass.RUNNING,
}, },
"chlorine_sterilization_error": { "chlorine_sterilization_error": {
"device_class": BinarySensorDeviceClass.PROBLEM, "device_class": BinarySensorDeviceClass.PROBLEM,
@@ -74,154 +58,31 @@ DEVICE_MAPPING = {
} }
}, },
Platform.SENSOR: { Platform.SENSOR: {
"micro_leak_protection_value": { "current_temperature": {
"device_class": SensorDeviceClass.PRESSURE, "device_class": SensorDeviceClass.TEMPERATURE,
"unit_of_measurement": "kPa", "unit_of_measurement": UnitOfTemperature.CELSIUS,
"state_class": SensorStateClass.MEASUREMENT "state_class": SensorStateClass.MEASUREMENT
}, },
"regeneration_current_stages": { "cool_target_temperature": {
"device_class": SensorDeviceClass.ENUM "device_class": SensorDeviceClass.TEMPERATURE,
"unit_of_measurement": UnitOfTemperature.CELSIUS,
"state_class": SensorStateClass.MEASUREMENT
}, },
"water_hardness": { "water_consumption_ml": {
"device_class": SensorDeviceClass.WATER, "device_class": SensorDeviceClass.VOLUME,
"unit_of_measurement": UnitOfVolume.LITERS, "unit_of_measurement": UnitOfVolume.LITERS,
"state_class": SensorStateClass.TOTAL_INCREASING "state_class": SensorStateClass.TOTAL_INCREASING
}, },
"timing_regeneration_hour": { "keep_warm_time": {
"device_class": SensorDeviceClass.DURATION,
"unit_of_measurement": UnitOfTime.HOURS,
"state_class": SensorStateClass.MEASUREMENT
},
"real_time_setting_hour": {
"device_class": SensorDeviceClass.DURATION,
"unit_of_measurement": UnitOfTime.HOURS,
"state_class": SensorStateClass.MEASUREMENT
},
"timing_regeneration_min": {
"device_class": SensorDeviceClass.DURATION, "device_class": SensorDeviceClass.DURATION,
"unit_of_measurement": UnitOfTime.MINUTES, "unit_of_measurement": UnitOfTime.MINUTES,
"state_class": SensorStateClass.MEASUREMENT "state_class": SensorStateClass.MEASUREMENT
}, },
"regeneration_left_seconds": { "warm_left_time": {
"device_class": SensorDeviceClass.DURATION,
"unit_of_measurement": UnitOfTime.SECONDS,
"state_class": SensorStateClass.MEASUREMENT
},
"maintenance_reminder_setting": {
"device_class": SensorDeviceClass.ENUM
},
"mixed_water_gear": {
"device_class": SensorDeviceClass.ENUM
},
"use_days": {
"device_class": SensorDeviceClass.DURATION,
"unit_of_measurement": UnitOfTime.DAYS,
"state_class": SensorStateClass.MEASUREMENT
},
"days_since_last_regeneration": {
"device_class": SensorDeviceClass.DURATION,
"unit_of_measurement": UnitOfTime.DAYS,
"state_class": SensorStateClass.MEASUREMENT
},
"velocity": {
"device_class": SensorDeviceClass.SPEED,
"unit_of_measurement": "m/s",
"state_class": SensorStateClass.MEASUREMENT
},
"supply_voltage": {
"device_class": SensorDeviceClass.VOLTAGE,
"unit_of_measurement": UnitOfElectricPotential.VOLT,
"state_class": SensorStateClass.MEASUREMENT
},
"left_salt": {
"device_class": SensorDeviceClass.WEIGHT,
"unit_of_measurement": UnitOfMass.KILOGRAMS,
"state_class": SensorStateClass.MEASUREMENT
},
"pre_regeneration_days": {
"device_class": SensorDeviceClass.DURATION,
"unit_of_measurement": UnitOfTime.DAYS,
"state_class": SensorStateClass.MEASUREMENT
},
"flushing_days": {
"device_class": SensorDeviceClass.DURATION,
"unit_of_measurement": UnitOfTime.DAYS,
"state_class": SensorStateClass.MEASUREMENT
},
"salt_setting": {
"device_class": SensorDeviceClass.ENUM
},
"regeneration_count": {
"device_class": SensorDeviceClass.ENUM
},
"battery_voltage": {
"device_class": SensorDeviceClass.VOLTAGE,
"unit_of_measurement": UnitOfElectricPotential.VOLT,
"state_class": SensorStateClass.MEASUREMENT
},
"error": {
"device_class": SensorDeviceClass.ENUM
},
"days_since_last_two_regeneration": {
"device_class": SensorDeviceClass.DURATION,
"unit_of_measurement": UnitOfTime.DAYS,
"state_class": SensorStateClass.MEASUREMENT
},
"remind_maintenance_days": {
"device_class": SensorDeviceClass.DURATION,
"unit_of_measurement": UnitOfTime.DAYS,
"state_class": SensorStateClass.MEASUREMENT
},
"real_date_setting_year": {
"device_class": SensorDeviceClass.ENUM
},
"real_date_setting_month": {
"device_class": SensorDeviceClass.ENUM
},
"real_date_setting_day": {
"device_class": SensorDeviceClass.ENUM
},
"category": {
"device_class": SensorDeviceClass.ENUM
},
"real_time_setting_min": {
"device_class": SensorDeviceClass.DURATION, "device_class": SensorDeviceClass.DURATION,
"unit_of_measurement": UnitOfTime.MINUTES, "unit_of_measurement": UnitOfTime.MINUTES,
"state_class": SensorStateClass.MEASUREMENT "state_class": SensorStateClass.MEASUREMENT
}, },
"regeneration_stages": {
"device_class": SensorDeviceClass.ENUM
},
"soft_available_big": {
"device_class": SensorDeviceClass.VOLUME,
"unit_of_measurement": UnitOfVolume.LITERS,
"state_class": SensorStateClass.TOTAL_INCREASING
},
"water_consumption_big": {
"device_class": SensorDeviceClass.VOLUME,
"unit_of_measurement": UnitOfVolume.LITERS,
"state_class": SensorStateClass.TOTAL_INCREASING
},
"water_consumption_today": {
"device_class": SensorDeviceClass.VOLUME,
"unit_of_measurement": UnitOfVolume.LITERS,
"state_class": SensorStateClass.TOTAL_INCREASING
},
"water_consumption_average": {
"device_class": SensorDeviceClass.VOLUME,
"unit_of_measurement": UnitOfVolume.LITERS,
"state_class": SensorStateClass.TOTAL_INCREASING
},
"salt_alarm_threshold": {
"device_class": SensorDeviceClass.WEIGHT,
"unit_of_measurement": UnitOfMass.KILOGRAMS,
"state_class": SensorStateClass.MEASUREMENT
},
"leak_water_protection_value": {
"device_class": SensorDeviceClass.PRESSURE,
"unit_of_measurement": "kPa",
"state_class": SensorStateClass.MEASUREMENT
}
} }
} }
} }

View File

@@ -44,10 +44,6 @@ class MideaFanEntity(MideaEntity, FanEntity):
rationale=rationale, rationale=rationale,
config=config, config=config,
) )
self._device = device
self._manufacturer = manufacturer
self._rationale = rationale
self._config = config
self._key_power = self._config.get("power") self._key_power = self._config.get("power")
self._key_preset_modes = self._config.get("preset_modes") self._key_preset_modes = self._config.get("preset_modes")
self._key_speeds = self._config.get("speeds") self._key_speeds = self._config.get("speeds")

View File

@@ -61,11 +61,6 @@ class MideaHumidifierEntity(MideaEntity, HumidifierEntity):
rationale=rationale, rationale=rationale,
config=config, config=config,
) )
self._device = device
self._manufacturer = manufacturer
self._rationale = rationale
self._entity_key = entity_key
self._config = config
@property @property
def device_class(self): def device_class(self):

View File

@@ -3,6 +3,7 @@
from __future__ import annotations from __future__ import annotations
import logging import logging
from enum import IntEnum
from typing import Any from typing import Any
from homeassistant.helpers.debounce import Debouncer from homeassistant.helpers.debounce import Debouncer
@@ -16,6 +17,10 @@ from .data_coordinator import MideaDataUpdateCoordinator
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
class Rationale(IntEnum):
EQUALLY = 0
GREATER = 1
LESS = 2
class MideaEntity(CoordinatorEntity[MideaDataUpdateCoordinator], Entity): class MideaEntity(CoordinatorEntity[MideaDataUpdateCoordinator], Entity):
"""Base class for Midea entities.""" """Base class for Midea entities."""
@@ -61,6 +66,7 @@ class MideaEntity(CoordinatorEntity[MideaDataUpdateCoordinator], Entity):
self._attr_unique_id = f"{DOMAIN}.{self._device_id}_{self._entity_key}" self._attr_unique_id = f"{DOMAIN}.{self._device_id}_{self._entity_key}"
self.entity_id_base = f"midea_{self._device_id}" self.entity_id_base = f"midea_{self._device_id}"
manu = "Midea" if manufacturer is None else manufacturer manu = "Midea" if manufacturer is None else manufacturer
self.manufacturer = manu
self._attr_device_info = DeviceInfo( self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, str(self._device_id))}, identifiers={(DOMAIN, str(self._device_id))},
model=self._model, model=self._model,
@@ -140,78 +146,65 @@ class MideaEntity(CoordinatorEntity[MideaDataUpdateCoordinator], Entity):
Accepts common truthy representations: True/1/"on"/"true". Accepts common truthy representations: True/1/"on"/"true".
""" """
result = False
if attribute_key is None: if attribute_key is None:
return False return result
value = self.device_attributes.get(attribute_key) status = self.device_attributes.get(attribute_key)
if isinstance(value, bool): if status is not None:
return value try:
return value in (1, "1", "on", "ON", "true", "TRUE") result = bool(self._rationale.index(status))
except ValueError:
MideaLogger.error(f"The value of attribute {attribute_key} ('{status}') "
f"is not in rationale {self._rationale}")
return result
async def _async_set_status_on_off(self, attribute_key: str | None, turn_on: bool) -> None: async def _async_set_status_on_off(self, attribute_key: str | None, turn_on: bool) -> None:
"""Set boolean attribute via coordinator, no-op if key is None.""" """Set boolean attribute via coordinator, no-op if key is None."""
if attribute_key is None: if attribute_key is None:
return return
await self.async_set_attribute(attribute_key, bool(turn_on)) MideaLogger.error(f"self._rationale: {self._rationale}, {int(turn_on)}")
await self.async_set_attribute(attribute_key, self._rationale[int(turn_on)])
def _list_get_selected(self, options: list[dict] | None, rationale: object = None) -> int | None: def _list_get_selected(self, key_of_list: list, rationale: Rationale = Rationale.EQUALLY):
"""Select index from a list of dict conditions matched against attributes. for index in range(0, len(key_of_list)):
The optional rationale supports equality/greater/less matching. It can be
a string name ("EQUALLY"/"GREATER"/"LESS") or an Enum with .name.
"""
if not options:
return None
rationale_name = getattr(rationale, "name", None) or rationale or "EQUALLY"
for index in range(0, len(options)):
match = True match = True
for attr, expected in options[index].items(): for attr, value in key_of_list[index].items():
current = self.device_attributes.get(attr) state_value = self.device_attributes.get(attr)
if current is None: if state_value is None:
match = False match = False
break break
if rationale_name == "EQUALLY" and current != expected: if rationale is Rationale.EQUALLY and state_value != value:
match = False match = False
break break
if rationale_name == "GREATER" and current < expected: if rationale is Rationale.GREATER and state_value < value:
match = False match = False
break break
if rationale_name == "LESS" and current > expected: if rationale is Rationale.LESS and state_value > value:
match = False match = False
break break
if match: if match:
return index return index
return None return None
def _dict_get_selected(self, mapping: dict | None, rationale: object = None): def _dict_get_selected(self, key_of_dict: dict, rationale: Rationale = Rationale.EQUALLY):
"""Return key from a dict whose value (a condition dict) matches attributes. for mode, status in key_of_dict.items():
The optional rationale supports equality/greater/less matching. It can be
a string name ("EQUALLY"/"GREATER"/"LESS") or an Enum with .name.
"""
if not mapping:
return None
rationale_name = getattr(rationale, "name", None) or rationale or "EQUALLY"
for key, conditions in mapping.items():
if not isinstance(conditions, dict):
continue
match = True match = True
for attr, expected in conditions.items(): for attr, value in status.items():
current = self.device_attributes.get(attr) state_value = self.device_attributes.get(attr)
if current is None: if state_value is None:
match = False match = False
break break
if rationale_name == "EQUALLY" and current != expected: if rationale is Rationale.EQUALLY and state_value != value:
match = False match = False
break break
if rationale_name == "GREATER" and current <= expected: if rationale is Rationale.GREATER and state_value < value:
match = False match = False
break break
if rationale_name == "LESS" and current >= expected: if rationale is Rationale.LESS and state_value > value:
match = False match = False
break break
if match: if match:
return key return mode
return None return None
async def publish_command_from_current_state(self) -> None: async def publish_command_from_current_state(self) -> None:

View File

@@ -44,10 +44,6 @@ class MideaSelectEntity(MideaEntity, SelectEntity):
rationale=rationale, rationale=rationale,
config=config, config=config,
) )
self._device = device
self._manufacturer = manufacturer
self._rationale = rationale
self._config = config
self._key_options = self._config.get("options") self._key_options = self._config.get("options")
@property @property

View File

@@ -57,10 +57,6 @@ class MideaSensorEntity(MideaEntity, SensorEntity):
rationale=rationale, rationale=rationale,
config=config, config=config,
) )
self._device = device
self._manufacturer = manufacturer
self._rationale = rationale
self._config = config
@property @property
def native_value(self): def native_value(self):

View File

@@ -58,24 +58,16 @@ class MideaSwitchEntity(MideaEntity, SwitchEntity):
rationale=rationale, rationale=rationale,
config=config, config=config,
) )
self._device = device
self._manufacturer = manufacturer
self._rationale = rationale
self._config = config
@property @property
def is_on(self) -> bool: def is_on(self) -> bool:
"""Return if the switch is on.""" """Return if the switch is on."""
value = self.device_attributes.get(self._entity_key) return self._get_status_on_off(self._entity_key)
if isinstance(value, bool):
return value
return value == 1 or value == "on" or value == "true"
async def async_turn_on(self): async def async_turn_on(self):
"""Turn the switch on.""" """Turn the switch on."""
await self.async_set_attribute(self._entity_key, True) await self._async_set_status_on_off(self._entity_key, True)
async def async_turn_off(self): async def async_turn_off(self):
"""Turn the switch off.""" """Turn the switch off."""
await self.async_set_attribute(self._entity_key, False) await self._async_set_status_on_off(self._entity_key, False)

View File

@@ -1075,6 +1075,18 @@
}, },
"ud_diy_up_percent": { "ud_diy_up_percent": {
"name": "UD DIY Up Percent" "name": "UD DIY Up Percent"
},
"water_consumption_ml": {
"name": "Water Consumption (ml)"
},
"cool_target_temperature": {
"name": "Cool Target Temperature"
},
"keep_warm_time": {
"name": "Keep Warm Time"
},
"warm_left_time": {
"name": "Warm Left Time"
} }
}, },
"binary_sensor": { "binary_sensor": {
@@ -1233,6 +1245,12 @@
}, },
"filter_reset": { "filter_reset": {
"name": "Filter Reset" "name": "Filter Reset"
},
"heat_status": {
"name": "Heat Status"
},
"standby_status": {
"name": "Standby Status"
} }
}, },
"climate": { "climate": {
@@ -1978,6 +1996,24 @@
}, },
"unfreeze_power": { "unfreeze_power": {
"name": "Unfreeze Power" "name": "Unfreeze Power"
},
"heat_start": {
"name": "Heat Start"
},
"keep_warm": {
"name": "Keep Warm"
},
"vacation": {
"name": "Vacation Mode"
},
"germicidal": {
"name": "Germicidal"
},
"drainage": {
"name": "Drainage"
},
"wash_enable": {
"name": "Wash Enable"
} }
} }
} }

View File

@@ -269,6 +269,12 @@
}, },
"filter_reset": { "filter_reset": {
"name": "滤网重置" "name": "滤网重置"
},
"heat_status": {
"name": "加热状态"
},
"standby_status": {
"name": "待机状态"
} }
}, },
"climate": { "climate": {
@@ -1261,6 +1267,18 @@
}, },
"ud_diy_up_percent": { "ud_diy_up_percent": {
"name": "上下自定义上百分比" "name": "上下自定义上百分比"
},
"water_consumption_ml": {
"name": "用水量(毫升)"
},
"cool_target_temperature": {
"name": "制冷目标温度"
},
"keep_warm_time": {
"name": "保温时间"
},
"warm_left_time": {
"name": "剩余加热时间"
} }
}, },
"switch": { "switch": {
@@ -1983,6 +2001,24 @@
}, },
"unfreeze_power": { "unfreeze_power": {
"name": "解冻电源" "name": "解冻电源"
},
"heat_start": {
"name": "加热启动"
},
"keep_warm": {
"name": "保温"
},
"vacation": {
"name": "度假模式"
},
"germicidal": {
"name": "杀菌"
},
"drainage": {
"name": "排水"
},
"wash_enable": {
"name": "洗涤启用"
} }
} }
} }

View File

@@ -56,10 +56,6 @@ class MideaWaterHeaterEntityEntity(MideaEntity, WaterHeaterEntity):
rationale=rationale, rationale=rationale,
config=config, config=config,
) )
self._device = device
self._manufacturer = manufacturer
self._rationale = rationale
self._config = config
self._key_power = self._config.get("power") self._key_power = self._config.get("power")
self._key_operation_list = self._config.get("operation_list") self._key_operation_list = self._config.get("operation_list")
self._key_min_temp = self._config.get("min_temp") self._key_min_temp = self._config.get("min_temp")