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 .const import DOMAIN
from .core.logger import MideaLogger
from .midea_entity import MideaEntity
from . import load_device_config
@@ -66,10 +65,6 @@ class MideaClimateEntity(MideaEntity, ClimateEntity):
rationale=rationale,
config=config,
)
self._device = device
self._manufacturer = manufacturer
self._rationale = rationale
self._config = config
self._key_power = self._config.get("power")
self._key_hvac_modes = self._config.get("hvac_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.binary_sensor import BinarySensorDeviceClass
from homeassistant.components.switch import SwitchDeviceClass
@@ -10,61 +11,44 @@ DEVICE_MAPPING = {
"centralized": [],
"entities": {
Platform.SWITCH: {
"holiday_mode": {
"power": {
"device_class": SwitchDeviceClass.SWITCH,
},
"water_way": {
"heat_start": {
"device_class": SwitchDeviceClass.SWITCH,
"rationale": [0, 1],
},
"lock": {
"device_class": SwitchDeviceClass.SWITCH,
},
"soften": {
"sleep": {
"device_class": SwitchDeviceClass.SWITCH,
},
"leak_water_protection": {
"keep_warm": {
"device_class": SwitchDeviceClass.SWITCH,
},
"cl_sterilization": {
"vacation": {
"device_class": SwitchDeviceClass.SWITCH,
},
"micro_leak": {
"germicidal": {
"device_class": SwitchDeviceClass.SWITCH,
},
"low_salt": {
"lack_water": {
"device_class": SwitchDeviceClass.SWITCH,
},
"no_salt": {
"drainage": {
"device_class": SwitchDeviceClass.SWITCH,
},
"low_battery": {
"wash_enable": {
"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: {
"maintenance_remind": {
"device_class": BinarySensorDeviceClass.PROBLEM,
"heat_status": {
"device_class": BinarySensorDeviceClass.RUNNING,
},
"standby_status": {
"device_class": BinarySensorDeviceClass.RUNNING,
},
"chlorine_sterilization_error": {
"device_class": BinarySensorDeviceClass.PROBLEM,
@@ -74,154 +58,31 @@ DEVICE_MAPPING = {
}
},
Platform.SENSOR: {
"micro_leak_protection_value": {
"device_class": SensorDeviceClass.PRESSURE,
"unit_of_measurement": "kPa",
"current_temperature": {
"device_class": SensorDeviceClass.TEMPERATURE,
"unit_of_measurement": UnitOfTemperature.CELSIUS,
"state_class": SensorStateClass.MEASUREMENT
},
"regeneration_current_stages": {
"device_class": SensorDeviceClass.ENUM
"cool_target_temperature": {
"device_class": SensorDeviceClass.TEMPERATURE,
"unit_of_measurement": UnitOfTemperature.CELSIUS,
"state_class": SensorStateClass.MEASUREMENT
},
"water_hardness": {
"device_class": SensorDeviceClass.WATER,
"water_consumption_ml": {
"device_class": SensorDeviceClass.VOLUME,
"unit_of_measurement": UnitOfVolume.LITERS,
"state_class": SensorStateClass.TOTAL_INCREASING
},
"timing_regeneration_hour": {
"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": {
"keep_warm_time": {
"device_class": SensorDeviceClass.DURATION,
"unit_of_measurement": UnitOfTime.MINUTES,
"state_class": SensorStateClass.MEASUREMENT
},
"regeneration_left_seconds": {
"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": {
"warm_left_time": {
"device_class": SensorDeviceClass.DURATION,
"unit_of_measurement": UnitOfTime.MINUTES,
"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,
config=config,
)
self._device = device
self._manufacturer = manufacturer
self._rationale = rationale
self._config = config
self._key_power = self._config.get("power")
self._key_preset_modes = self._config.get("preset_modes")
self._key_speeds = self._config.get("speeds")

View File

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

View File

@@ -3,6 +3,7 @@
from __future__ import annotations
import logging
from enum import IntEnum
from typing import Any
from homeassistant.helpers.debounce import Debouncer
@@ -16,6 +17,10 @@ from .data_coordinator import MideaDataUpdateCoordinator
_LOGGER = logging.getLogger(__name__)
class Rationale(IntEnum):
EQUALLY = 0
GREATER = 1
LESS = 2
class MideaEntity(CoordinatorEntity[MideaDataUpdateCoordinator], Entity):
"""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.entity_id_base = f"midea_{self._device_id}"
manu = "Midea" if manufacturer is None else manufacturer
self.manufacturer = manu
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, str(self._device_id))},
model=self._model,
@@ -140,78 +146,65 @@ class MideaEntity(CoordinatorEntity[MideaDataUpdateCoordinator], Entity):
Accepts common truthy representations: True/1/"on"/"true".
"""
result = False
if attribute_key is None:
return False
value = self.device_attributes.get(attribute_key)
if isinstance(value, bool):
return value
return value in (1, "1", "on", "ON", "true", "TRUE")
return result
status = self.device_attributes.get(attribute_key)
if status is not None:
try:
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:
"""Set boolean attribute via coordinator, no-op if key is None."""
if attribute_key is None:
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:
"""Select index from a list of dict conditions matched against attributes.
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)):
def _list_get_selected(self, key_of_list: list, rationale: Rationale = Rationale.EQUALLY):
for index in range(0, len(key_of_list)):
match = True
for attr, expected in options[index].items():
current = self.device_attributes.get(attr)
if current is None:
for attr, value in key_of_list[index].items():
state_value = self.device_attributes.get(attr)
if state_value is None:
match = False
break
if rationale_name == "EQUALLY" and current != expected:
if rationale is Rationale.EQUALLY and state_value != value:
match = False
break
if rationale_name == "GREATER" and current < expected:
if rationale is Rationale.GREATER and state_value < value:
match = False
break
if rationale_name == "LESS" and current > expected:
if rationale is Rationale.LESS and state_value > value:
match = False
break
if match:
return index
return None
def _dict_get_selected(self, mapping: dict | None, rationale: object = None):
"""Return key from a dict whose value (a condition dict) matches attributes.
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
def _dict_get_selected(self, key_of_dict: dict, rationale: Rationale = Rationale.EQUALLY):
for mode, status in key_of_dict.items():
match = True
for attr, expected in conditions.items():
current = self.device_attributes.get(attr)
if current is None:
for attr, value in status.items():
state_value = self.device_attributes.get(attr)
if state_value is None:
match = False
break
if rationale_name == "EQUALLY" and current != expected:
if rationale is Rationale.EQUALLY and state_value != value:
match = False
break
if rationale_name == "GREATER" and current <= expected:
if rationale is Rationale.GREATER and state_value < value:
match = False
break
if rationale_name == "LESS" and current >= expected:
if rationale is Rationale.LESS and state_value > value:
match = False
break
if match:
return key
return mode
return None
async def publish_command_from_current_state(self) -> None:

View File

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

View File

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

View File

@@ -58,24 +58,16 @@ class MideaSwitchEntity(MideaEntity, SwitchEntity):
rationale=rationale,
config=config,
)
self._device = device
self._manufacturer = manufacturer
self._rationale = rationale
self._config = config
@property
def is_on(self) -> bool:
"""Return if the switch is on."""
value = self.device_attributes.get(self._entity_key)
if isinstance(value, bool):
return value
return value == 1 or value == "on" or value == "true"
return self._get_status_on_off(self._entity_key)
async def async_turn_on(self):
"""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):
"""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": {
"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": {
@@ -1233,6 +1245,12 @@
},
"filter_reset": {
"name": "Filter Reset"
},
"heat_status": {
"name": "Heat Status"
},
"standby_status": {
"name": "Standby Status"
}
},
"climate": {
@@ -1978,6 +1996,24 @@
},
"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": {
"name": "滤网重置"
},
"heat_status": {
"name": "加热状态"
},
"standby_status": {
"name": "待机状态"
}
},
"climate": {
@@ -1261,6 +1267,18 @@
},
"ud_diy_up_percent": {
"name": "上下自定义上百分比"
},
"water_consumption_ml": {
"name": "用水量(毫升)"
},
"cool_target_temperature": {
"name": "制冷目标温度"
},
"keep_warm_time": {
"name": "保温时间"
},
"warm_left_time": {
"name": "剩余加热时间"
}
},
"switch": {
@@ -1983,6 +2001,24 @@
},
"unfreeze_power": {
"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,
config=config,
)
self._device = device
self._manufacturer = manufacturer
self._rationale = rationale
self._config = config
self._key_power = self._config.get("power")
self._key_operation_list = self._config.get("operation_list")
self._key_min_temp = self._config.get("min_temp")