mirror of
https://github.com/home-assistant/core.git
synced 2026-08-24 02:24:51 -05:00
Add number platform to Midea (#179247)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
This commit is contained in:
co-authored by
Joost Lekkerkerker
parent
c830734535
commit
2aa09bc9d4
@@ -23,6 +23,7 @@ from .entity import MideaConfigEntry
|
||||
_PLATFORMS: list[Platform] = [
|
||||
Platform.CLIMATE,
|
||||
Platform.HUMIDIFIER,
|
||||
Platform.NUMBER,
|
||||
Platform.SELECT,
|
||||
]
|
||||
|
||||
|
||||
@@ -8,6 +8,9 @@ MIDEA_DEVICE_NAMES: dict[DeviceType, str] = {
|
||||
DeviceType.CC: "MDV Wi-Fi Controller",
|
||||
DeviceType.CF: "Heat Pump",
|
||||
DeviceType.FB: "Electric Heater",
|
||||
DeviceType.C2: "Toilet",
|
||||
DeviceType.CD: "Heat Pump Water Heater",
|
||||
DeviceType.ED: "Water Drinking Appliance",
|
||||
DeviceType.X40: "Integrated Ceiling Fan",
|
||||
DeviceType.A1: "Dehumidifier",
|
||||
DeviceType.FA: "Fan",
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
"""Number for Midea."""
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from typing import cast, override
|
||||
|
||||
from midealocal.const import DeviceType
|
||||
from midealocal.device import MideaDevice
|
||||
from midealocal.devices.c2 import MideaC2Device
|
||||
|
||||
from homeassistant.components.number import (
|
||||
NumberDeviceClass,
|
||||
NumberEntity,
|
||||
NumberEntityDescription,
|
||||
)
|
||||
from homeassistant.const import UnitOfTime, UnitOfVolume
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .entity import MideaConfigEntry, MideaEntity, midea_api_call
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
|
||||
@dataclass(kw_only=True, frozen=True)
|
||||
class MideaNumberEntityDescription(NumberEntityDescription):
|
||||
"""Description for a Midea number entity."""
|
||||
|
||||
models: list[DeviceType]
|
||||
max_value_fn: Callable[[MideaDevice], float | None] | None = None
|
||||
|
||||
|
||||
NUMBERS: list[MideaNumberEntityDescription] = [
|
||||
MideaNumberEntityDescription(
|
||||
key="dry_level",
|
||||
translation_key="dry_level",
|
||||
models=[DeviceType.C2],
|
||||
native_min_value=0,
|
||||
max_value_fn=lambda device: cast(MideaC2Device, device).max_dry_level,
|
||||
native_step=1,
|
||||
),
|
||||
MideaNumberEntityDescription(
|
||||
key="water_temp_level",
|
||||
translation_key="water_temp_level",
|
||||
models=[DeviceType.C2],
|
||||
native_min_value=0,
|
||||
max_value_fn=lambda device: cast(MideaC2Device, device).max_water_temp_level,
|
||||
native_step=1,
|
||||
),
|
||||
MideaNumberEntityDescription(
|
||||
key="seat_temp_level",
|
||||
translation_key="seat_temp_level",
|
||||
models=[DeviceType.C2],
|
||||
native_min_value=0,
|
||||
max_value_fn=lambda device: cast(MideaC2Device, device).max_seat_temp_level,
|
||||
native_step=1,
|
||||
),
|
||||
MideaNumberEntityDescription(
|
||||
key="vacation_days",
|
||||
translation_key="vacation_days",
|
||||
models=[DeviceType.CD],
|
||||
device_class=NumberDeviceClass.DURATION,
|
||||
native_min_value=1,
|
||||
native_max_value=360,
|
||||
native_step=1,
|
||||
native_unit_of_measurement=UnitOfTime.DAYS,
|
||||
),
|
||||
MideaNumberEntityDescription(
|
||||
key="water_hardness",
|
||||
translation_key="water_hardness",
|
||||
models=[DeviceType.ED],
|
||||
native_min_value=0,
|
||||
native_max_value=65535,
|
||||
native_step=1,
|
||||
),
|
||||
MideaNumberEntityDescription(
|
||||
key="flushing_days",
|
||||
translation_key="flushing_days",
|
||||
models=[DeviceType.ED],
|
||||
device_class=NumberDeviceClass.DURATION,
|
||||
native_min_value=0,
|
||||
native_max_value=99,
|
||||
native_step=1,
|
||||
native_unit_of_measurement=UnitOfTime.DAYS,
|
||||
),
|
||||
MideaNumberEntityDescription(
|
||||
key="leak_water_protection_value",
|
||||
translation_key="leak_water_protection_value",
|
||||
models=[DeviceType.ED],
|
||||
device_class=NumberDeviceClass.VOLUME,
|
||||
native_min_value=0,
|
||||
native_max_value=2550,
|
||||
native_step=50,
|
||||
native_unit_of_measurement=UnitOfVolume.LITERS,
|
||||
),
|
||||
MideaNumberEntityDescription(
|
||||
key="heating_level",
|
||||
translation_key="heating_level",
|
||||
models=[DeviceType.FB],
|
||||
native_min_value=1,
|
||||
native_max_value=10,
|
||||
native_step=1,
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MideaConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up numbers for device."""
|
||||
device = config_entry.runtime_data
|
||||
|
||||
async_add_entities(
|
||||
MideaNumber(device, description)
|
||||
for description in NUMBERS
|
||||
if device.device_type in description.models
|
||||
# None means the model doesn't support this attribute at all,
|
||||
# unlike select.py's key-presence check.
|
||||
and device.attributes.get(description.key) is not None
|
||||
)
|
||||
|
||||
|
||||
class MideaNumber(MideaEntity, NumberEntity):
|
||||
"""Represent a Midea number."""
|
||||
|
||||
entity_description: MideaNumberEntityDescription
|
||||
|
||||
@property
|
||||
@override
|
||||
def native_max_value(self) -> float:
|
||||
"""Return the maximum value, reading it off the device if dynamic."""
|
||||
if self.entity_description.max_value_fn is not None:
|
||||
value = self.entity_description.max_value_fn(self._device)
|
||||
if value is not None:
|
||||
return value
|
||||
return super().native_max_value
|
||||
|
||||
@property
|
||||
@override
|
||||
def native_value(self) -> float | None:
|
||||
"""Return the current value."""
|
||||
value = self._device.get_attribute(self.entity_description.key)
|
||||
if not isinstance(value, (int, float)):
|
||||
return None
|
||||
return float(value)
|
||||
|
||||
@override
|
||||
def set_native_value(self, value: float) -> None:
|
||||
"""Set the value."""
|
||||
step = self.step
|
||||
value = round(value / step) * step
|
||||
with midea_api_call():
|
||||
self._device.set_attribute(
|
||||
attr=self.entity_description.key, value=round(value)
|
||||
)
|
||||
@@ -106,6 +106,32 @@
|
||||
"name": "Zone 2 thermostat"
|
||||
}
|
||||
},
|
||||
"number": {
|
||||
"dry_level": {
|
||||
"name": "Dry level"
|
||||
},
|
||||
"flushing_days": {
|
||||
"name": "Flushing days"
|
||||
},
|
||||
"heating_level": {
|
||||
"name": "Heating level"
|
||||
},
|
||||
"leak_water_protection_value": {
|
||||
"name": "Leak water protection value"
|
||||
},
|
||||
"seat_temp_level": {
|
||||
"name": "Seat temperature level"
|
||||
},
|
||||
"vacation_days": {
|
||||
"name": "Vacation days"
|
||||
},
|
||||
"water_hardness": {
|
||||
"name": "Water hardness"
|
||||
},
|
||||
"water_temp_level": {
|
||||
"name": "Water temperature level"
|
||||
}
|
||||
},
|
||||
"select": {
|
||||
"detect_mode": {
|
||||
"name": "Detect mode",
|
||||
|
||||
@@ -0,0 +1,479 @@
|
||||
# serializer version: 1
|
||||
# name: test_number_state_snapshot[c2][number.bedroom_ac_dry_level-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
None,
|
||||
]),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
<NumberEntityCapabilityAttribute.MAX: 'max'>: 3,
|
||||
<NumberEntityCapabilityAttribute.MIN: 'min'>: 0,
|
||||
<NumberEntityCapabilityAttribute.MODE: 'mode'>: <NumberMode.AUTO: 'auto'>,
|
||||
<NumberEntityCapabilityAttribute.STEP: 'step'>: 1,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'number',
|
||||
'entity_category': None,
|
||||
'entity_id': 'number.bedroom_ac_dry_level',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': 'Dry level',
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': 'Dry level',
|
||||
'platform': 'midea',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'dry_level',
|
||||
'unique_id': '12345678_dry_level',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_number_state_snapshot[c2][number.bedroom_ac_dry_level-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Bedroom AC Dry level',
|
||||
<NumberEntityCapabilityAttribute.MAX: 'max'>: 3,
|
||||
<NumberEntityCapabilityAttribute.MIN: 'min'>: 0,
|
||||
<NumberEntityCapabilityAttribute.MODE: 'mode'>: <NumberMode.AUTO: 'auto'>,
|
||||
<NumberEntityCapabilityAttribute.STEP: 'step'>: 1,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'number.bedroom_ac_dry_level',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '1.0',
|
||||
})
|
||||
# ---
|
||||
# name: test_number_state_snapshot[c2][number.bedroom_ac_seat_temperature_level-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
None,
|
||||
]),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
<NumberEntityCapabilityAttribute.MAX: 'max'>: 5,
|
||||
<NumberEntityCapabilityAttribute.MIN: 'min'>: 0,
|
||||
<NumberEntityCapabilityAttribute.MODE: 'mode'>: <NumberMode.AUTO: 'auto'>,
|
||||
<NumberEntityCapabilityAttribute.STEP: 'step'>: 1,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'number',
|
||||
'entity_category': None,
|
||||
'entity_id': 'number.bedroom_ac_seat_temperature_level',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': 'Seat temperature level',
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': 'Seat temperature level',
|
||||
'platform': 'midea',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'seat_temp_level',
|
||||
'unique_id': '12345678_seat_temp_level',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_number_state_snapshot[c2][number.bedroom_ac_seat_temperature_level-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Bedroom AC Seat temperature level',
|
||||
<NumberEntityCapabilityAttribute.MAX: 'max'>: 5,
|
||||
<NumberEntityCapabilityAttribute.MIN: 'min'>: 0,
|
||||
<NumberEntityCapabilityAttribute.MODE: 'mode'>: <NumberMode.AUTO: 'auto'>,
|
||||
<NumberEntityCapabilityAttribute.STEP: 'step'>: 1,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'number.bedroom_ac_seat_temperature_level',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '3.0',
|
||||
})
|
||||
# ---
|
||||
# name: test_number_state_snapshot[c2][number.bedroom_ac_water_temperature_level-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
None,
|
||||
]),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
<NumberEntityCapabilityAttribute.MAX: 'max'>: 5,
|
||||
<NumberEntityCapabilityAttribute.MIN: 'min'>: 0,
|
||||
<NumberEntityCapabilityAttribute.MODE: 'mode'>: <NumberMode.AUTO: 'auto'>,
|
||||
<NumberEntityCapabilityAttribute.STEP: 'step'>: 1,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'number',
|
||||
'entity_category': None,
|
||||
'entity_id': 'number.bedroom_ac_water_temperature_level',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': 'Water temperature level',
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': 'Water temperature level',
|
||||
'platform': 'midea',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'water_temp_level',
|
||||
'unique_id': '12345678_water_temp_level',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_number_state_snapshot[c2][number.bedroom_ac_water_temperature_level-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Bedroom AC Water temperature level',
|
||||
<NumberEntityCapabilityAttribute.MAX: 'max'>: 5,
|
||||
<NumberEntityCapabilityAttribute.MIN: 'min'>: 0,
|
||||
<NumberEntityCapabilityAttribute.MODE: 'mode'>: <NumberMode.AUTO: 'auto'>,
|
||||
<NumberEntityCapabilityAttribute.STEP: 'step'>: 1,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'number.bedroom_ac_water_temperature_level',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '2.0',
|
||||
})
|
||||
# ---
|
||||
# name: test_number_state_snapshot[cd][number.bedroom_ac_vacation_days-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
None,
|
||||
]),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
<NumberEntityCapabilityAttribute.MAX: 'max'>: 360,
|
||||
<NumberEntityCapabilityAttribute.MIN: 'min'>: 1,
|
||||
<NumberEntityCapabilityAttribute.MODE: 'mode'>: <NumberMode.AUTO: 'auto'>,
|
||||
<NumberEntityCapabilityAttribute.STEP: 'step'>: 1,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'number',
|
||||
'entity_category': None,
|
||||
'entity_id': 'number.bedroom_ac_vacation_days',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': 'Vacation days',
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <NumberDeviceClass.DURATION: 'duration'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Vacation days',
|
||||
'platform': 'midea',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'vacation_days',
|
||||
'unique_id': '12345678_vacation_days',
|
||||
'unit_of_measurement': <UnitOfTime.DAYS: 'd'>,
|
||||
})
|
||||
# ---
|
||||
# name: test_number_state_snapshot[cd][number.bedroom_ac_vacation_days-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'duration',
|
||||
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Bedroom AC Vacation days',
|
||||
<NumberEntityCapabilityAttribute.MAX: 'max'>: 360,
|
||||
<NumberEntityCapabilityAttribute.MIN: 'min'>: 1,
|
||||
<NumberEntityCapabilityAttribute.MODE: 'mode'>: <NumberMode.AUTO: 'auto'>,
|
||||
<NumberEntityCapabilityAttribute.STEP: 'step'>: 1,
|
||||
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfTime.DAYS: 'd'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'number.bedroom_ac_vacation_days',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '7.0',
|
||||
})
|
||||
# ---
|
||||
# name: test_number_state_snapshot[ed][number.bedroom_ac_flushing_days-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
None,
|
||||
]),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
<NumberEntityCapabilityAttribute.MAX: 'max'>: 99,
|
||||
<NumberEntityCapabilityAttribute.MIN: 'min'>: 0,
|
||||
<NumberEntityCapabilityAttribute.MODE: 'mode'>: <NumberMode.AUTO: 'auto'>,
|
||||
<NumberEntityCapabilityAttribute.STEP: 'step'>: 1,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'number',
|
||||
'entity_category': None,
|
||||
'entity_id': 'number.bedroom_ac_flushing_days',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': 'Flushing days',
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <NumberDeviceClass.DURATION: 'duration'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Flushing days',
|
||||
'platform': 'midea',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'flushing_days',
|
||||
'unique_id': '12345678_flushing_days',
|
||||
'unit_of_measurement': <UnitOfTime.DAYS: 'd'>,
|
||||
})
|
||||
# ---
|
||||
# name: test_number_state_snapshot[ed][number.bedroom_ac_flushing_days-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'duration',
|
||||
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Bedroom AC Flushing days',
|
||||
<NumberEntityCapabilityAttribute.MAX: 'max'>: 99,
|
||||
<NumberEntityCapabilityAttribute.MIN: 'min'>: 0,
|
||||
<NumberEntityCapabilityAttribute.MODE: 'mode'>: <NumberMode.AUTO: 'auto'>,
|
||||
<NumberEntityCapabilityAttribute.STEP: 'step'>: 1,
|
||||
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfTime.DAYS: 'd'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'number.bedroom_ac_flushing_days',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '14.0',
|
||||
})
|
||||
# ---
|
||||
# name: test_number_state_snapshot[ed][number.bedroom_ac_leak_water_protection_value-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
None,
|
||||
]),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
<NumberEntityCapabilityAttribute.MAX: 'max'>: 2550,
|
||||
<NumberEntityCapabilityAttribute.MIN: 'min'>: 0,
|
||||
<NumberEntityCapabilityAttribute.MODE: 'mode'>: <NumberMode.AUTO: 'auto'>,
|
||||
<NumberEntityCapabilityAttribute.STEP: 'step'>: 50,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'number',
|
||||
'entity_category': None,
|
||||
'entity_id': 'number.bedroom_ac_leak_water_protection_value',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': 'Leak water protection value',
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <NumberDeviceClass.VOLUME: 'volume'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Leak water protection value',
|
||||
'platform': 'midea',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'leak_water_protection_value',
|
||||
'unique_id': '12345678_leak_water_protection_value',
|
||||
'unit_of_measurement': <UnitOfVolume.LITERS: 'L'>,
|
||||
})
|
||||
# ---
|
||||
# name: test_number_state_snapshot[ed][number.bedroom_ac_leak_water_protection_value-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'volume',
|
||||
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Bedroom AC Leak water protection value',
|
||||
<NumberEntityCapabilityAttribute.MAX: 'max'>: 2550,
|
||||
<NumberEntityCapabilityAttribute.MIN: 'min'>: 0,
|
||||
<NumberEntityCapabilityAttribute.MODE: 'mode'>: <NumberMode.AUTO: 'auto'>,
|
||||
<NumberEntityCapabilityAttribute.STEP: 'step'>: 50,
|
||||
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfVolume.LITERS: 'L'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'number.bedroom_ac_leak_water_protection_value',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '500.0',
|
||||
})
|
||||
# ---
|
||||
# name: test_number_state_snapshot[ed][number.bedroom_ac_water_hardness-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
None,
|
||||
]),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
<NumberEntityCapabilityAttribute.MAX: 'max'>: 65535,
|
||||
<NumberEntityCapabilityAttribute.MIN: 'min'>: 0,
|
||||
<NumberEntityCapabilityAttribute.MODE: 'mode'>: <NumberMode.AUTO: 'auto'>,
|
||||
<NumberEntityCapabilityAttribute.STEP: 'step'>: 1,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'number',
|
||||
'entity_category': None,
|
||||
'entity_id': 'number.bedroom_ac_water_hardness',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': 'Water hardness',
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': 'Water hardness',
|
||||
'platform': 'midea',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'water_hardness',
|
||||
'unique_id': '12345678_water_hardness',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_number_state_snapshot[ed][number.bedroom_ac_water_hardness-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Bedroom AC Water hardness',
|
||||
<NumberEntityCapabilityAttribute.MAX: 'max'>: 65535,
|
||||
<NumberEntityCapabilityAttribute.MIN: 'min'>: 0,
|
||||
<NumberEntityCapabilityAttribute.MODE: 'mode'>: <NumberMode.AUTO: 'auto'>,
|
||||
<NumberEntityCapabilityAttribute.STEP: 'step'>: 1,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'number.bedroom_ac_water_hardness',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '120.0',
|
||||
})
|
||||
# ---
|
||||
# name: test_number_state_snapshot[fb][number.bedroom_ac_heating_level-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
None,
|
||||
]),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
<NumberEntityCapabilityAttribute.MAX: 'max'>: 10,
|
||||
<NumberEntityCapabilityAttribute.MIN: 'min'>: 1,
|
||||
<NumberEntityCapabilityAttribute.MODE: 'mode'>: <NumberMode.AUTO: 'auto'>,
|
||||
<NumberEntityCapabilityAttribute.STEP: 'step'>: 1,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'number',
|
||||
'entity_category': None,
|
||||
'entity_id': 'number.bedroom_ac_heating_level',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': 'Heating level',
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': 'Heating level',
|
||||
'platform': 'midea',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'heating_level',
|
||||
'unique_id': '12345678_heating_level',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_number_state_snapshot[fb][number.bedroom_ac_heating_level-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Bedroom AC Heating level',
|
||||
<NumberEntityCapabilityAttribute.MAX: 'max'>: 10,
|
||||
<NumberEntityCapabilityAttribute.MIN: 'min'>: 1,
|
||||
<NumberEntityCapabilityAttribute.MODE: 'mode'>: <NumberMode.AUTO: 'auto'>,
|
||||
<NumberEntityCapabilityAttribute.STEP: 'step'>: 1,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'number.bedroom_ac_heating_level',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '5.0',
|
||||
})
|
||||
# ---
|
||||
@@ -0,0 +1,306 @@
|
||||
"""Tests for midea number.py."""
|
||||
|
||||
from collections.abc import Callable
|
||||
from unittest.mock import patch
|
||||
|
||||
from midealocal.const import DeviceType
|
||||
from midealocal.devices.ac import DeviceAttributes as ACAttributes
|
||||
from midealocal.devices.c2 import DeviceAttributes as C2Attributes
|
||||
from midealocal.devices.cd import DeviceAttributes as CDAttributes
|
||||
from midealocal.devices.ed import DeviceAttributes as EDAttributes
|
||||
from midealocal.devices.fb import DeviceAttributes as FBAttributes
|
||||
from midealocal.exceptions import SocketException
|
||||
import pytest
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
from homeassistant.components.number import (
|
||||
ATTR_MAX,
|
||||
ATTR_MIN,
|
||||
ATTR_STEP,
|
||||
ATTR_VALUE,
|
||||
DOMAIN as NUMBER_DOMAIN,
|
||||
SERVICE_SET_VALUE,
|
||||
)
|
||||
from homeassistant.const import ATTR_ENTITY_ID, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
||||
from . import setup_integration
|
||||
from .conftest import DummyDevice, entity_entries
|
||||
from .const import TEST_DEVICE_ID
|
||||
|
||||
from tests.common import MockConfigEntry, snapshot_platform
|
||||
|
||||
|
||||
def _c2_device() -> DummyDevice:
|
||||
device = DummyDevice(
|
||||
DeviceType.C2,
|
||||
attributes={
|
||||
C2Attributes.dry_level: 1,
|
||||
C2Attributes.water_temp_level: 2,
|
||||
C2Attributes.seat_temp_level: 3,
|
||||
},
|
||||
)
|
||||
device.max_dry_level = 3
|
||||
device.max_water_temp_level = 5
|
||||
device.max_seat_temp_level = 5
|
||||
return device
|
||||
|
||||
|
||||
def _cd_device() -> DummyDevice:
|
||||
return DummyDevice(
|
||||
DeviceType.CD,
|
||||
attributes={CDAttributes.vacation_days: 7},
|
||||
)
|
||||
|
||||
|
||||
def _ed_device() -> DummyDevice:
|
||||
return DummyDevice(
|
||||
DeviceType.ED,
|
||||
attributes={
|
||||
EDAttributes.water_hardness: 120,
|
||||
EDAttributes.flushing_days: 14,
|
||||
EDAttributes.leak_water_protection_value: 500,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def _fb_device() -> DummyDevice:
|
||||
return DummyDevice(
|
||||
DeviceType.FB,
|
||||
attributes={FBAttributes.heating_level: 5},
|
||||
)
|
||||
|
||||
|
||||
async def _assert_service_call(
|
||||
hass: HomeAssistant,
|
||||
entity_id: str,
|
||||
value: float,
|
||||
expected_calls: list[tuple],
|
||||
device: DummyDevice,
|
||||
) -> None:
|
||||
"""Call number.set_value and assert the fake device recorded the right call."""
|
||||
device.calls.clear()
|
||||
await hass.services.async_call(
|
||||
NUMBER_DOMAIN,
|
||||
SERVICE_SET_VALUE,
|
||||
{ATTR_ENTITY_ID: entity_id, ATTR_VALUE: value},
|
||||
blocking=True,
|
||||
)
|
||||
assert device.calls == expected_calls
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"device",
|
||||
[
|
||||
pytest.param(_c2_device(), id="c2"),
|
||||
pytest.param(_cd_device(), id="cd"),
|
||||
pytest.param(_ed_device(), id="ed"),
|
||||
pytest.param(_fb_device(), id="fb"),
|
||||
],
|
||||
)
|
||||
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
|
||||
async def test_number_state_snapshot(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: Callable[[DummyDevice], MockConfigEntry],
|
||||
snapshot: SnapshotAssertion,
|
||||
entity_registry: er.EntityRegistry,
|
||||
device: DummyDevice,
|
||||
) -> None:
|
||||
"""Test async_setup_entry creates the right number entities per device type."""
|
||||
config_entry = mock_config_entry(device)
|
||||
with patch("homeassistant.components.midea._PLATFORMS", [Platform.NUMBER]):
|
||||
await setup_integration(hass, config_entry, device)
|
||||
|
||||
await snapshot_platform(hass, entity_registry, snapshot, config_entry.entry_id)
|
||||
|
||||
|
||||
async def test_c2_number_dynamic_max_and_services(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: Callable[[DummyDevice], MockConfigEntry],
|
||||
) -> None:
|
||||
"""Test C2 number entities read their max from a device property and can be set."""
|
||||
device = _c2_device()
|
||||
config_entry = mock_config_entry(device)
|
||||
with patch("homeassistant.components.midea._PLATFORMS", [Platform.NUMBER]):
|
||||
await setup_integration(hass, config_entry, device)
|
||||
|
||||
entity_entry = entity_entries(hass, config_entry)[f"{TEST_DEVICE_ID}_dry_level"]
|
||||
assert (state := hass.states.get(entity_entry.entity_id)) is not None
|
||||
assert float(state.state) == 1
|
||||
assert state.attributes[ATTR_MIN] == 0
|
||||
assert state.attributes[ATTR_MAX] == 3
|
||||
assert state.attributes[ATTR_STEP] == 1
|
||||
|
||||
await _assert_service_call(
|
||||
hass,
|
||||
entity_entry.entity_id,
|
||||
2,
|
||||
[("set_attribute", "dry_level", 2)],
|
||||
device,
|
||||
)
|
||||
|
||||
water_entry = entity_entries(hass, config_entry)[
|
||||
f"{TEST_DEVICE_ID}_water_temp_level"
|
||||
]
|
||||
assert (water_state := hass.states.get(water_entry.entity_id)) is not None
|
||||
assert water_state.attributes[ATTR_MAX] == 5
|
||||
|
||||
seat_entry = entity_entries(hass, config_entry)[f"{TEST_DEVICE_ID}_seat_temp_level"]
|
||||
assert (seat_state := hass.states.get(seat_entry.entity_id)) is not None
|
||||
assert seat_state.attributes[ATTR_MAX] == 5
|
||||
|
||||
|
||||
async def test_cd_number_static_range(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: Callable[[DummyDevice], MockConfigEntry],
|
||||
) -> None:
|
||||
"""Test CD's vacation_days uses a static min/max/step range."""
|
||||
device = _cd_device()
|
||||
config_entry = mock_config_entry(device)
|
||||
with patch("homeassistant.components.midea._PLATFORMS", [Platform.NUMBER]):
|
||||
await setup_integration(hass, config_entry, device)
|
||||
|
||||
entity_entry = entity_entries(hass, config_entry)[f"{TEST_DEVICE_ID}_vacation_days"]
|
||||
assert (state := hass.states.get(entity_entry.entity_id)) is not None
|
||||
assert float(state.state) == 7
|
||||
assert state.attributes[ATTR_MIN] == 1
|
||||
assert state.attributes[ATTR_MAX] == 360
|
||||
|
||||
await _assert_service_call(
|
||||
hass,
|
||||
entity_entry.entity_id,
|
||||
30,
|
||||
[("set_attribute", "vacation_days", 30)],
|
||||
device,
|
||||
)
|
||||
|
||||
|
||||
async def test_ed_number_entities(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: Callable[[DummyDevice], MockConfigEntry],
|
||||
) -> None:
|
||||
"""Test ED exposes water_hardness, flushing_days and leak_water_protection_value."""
|
||||
device = _ed_device()
|
||||
config_entry = mock_config_entry(device)
|
||||
with patch("homeassistant.components.midea._PLATFORMS", [Platform.NUMBER]):
|
||||
await setup_integration(hass, config_entry, device)
|
||||
|
||||
entities = entity_entries(hass, config_entry)
|
||||
assert f"{TEST_DEVICE_ID}_water_hardness" in entities
|
||||
assert f"{TEST_DEVICE_ID}_flushing_days" in entities
|
||||
assert f"{TEST_DEVICE_ID}_leak_water_protection_value" in entities
|
||||
|
||||
leak_entry = entities[f"{TEST_DEVICE_ID}_leak_water_protection_value"]
|
||||
await _assert_service_call(
|
||||
hass,
|
||||
leak_entry.entity_id,
|
||||
550,
|
||||
[("set_attribute", "leak_water_protection_value", 550)],
|
||||
device,
|
||||
)
|
||||
|
||||
|
||||
async def test_fb_heating_level(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: Callable[[DummyDevice], MockConfigEntry],
|
||||
) -> None:
|
||||
"""Test FB's heating_level number entity."""
|
||||
device = _fb_device()
|
||||
config_entry = mock_config_entry(device)
|
||||
with patch("homeassistant.components.midea._PLATFORMS", [Platform.NUMBER]):
|
||||
await setup_integration(hass, config_entry, device)
|
||||
|
||||
entity_entry = entity_entries(hass, config_entry)[f"{TEST_DEVICE_ID}_heating_level"]
|
||||
assert (state := hass.states.get(entity_entry.entity_id)) is not None
|
||||
assert float(state.state) == 5
|
||||
|
||||
await _assert_service_call(
|
||||
hass,
|
||||
entity_entry.entity_id,
|
||||
8,
|
||||
[("set_attribute", "heating_level", 8)],
|
||||
device,
|
||||
)
|
||||
|
||||
|
||||
async def test_number_unknown_when_attribute_not_numeric(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: Callable[[DummyDevice], MockConfigEntry],
|
||||
) -> None:
|
||||
"""Test native_value gracefully reports unknown if a later update clears it."""
|
||||
device = _fb_device()
|
||||
config_entry = mock_config_entry(device)
|
||||
with patch("homeassistant.components.midea._PLATFORMS", [Platform.NUMBER]):
|
||||
await setup_integration(hass, config_entry, device)
|
||||
|
||||
entity_entry = entity_entries(hass, config_entry)[f"{TEST_DEVICE_ID}_heating_level"]
|
||||
assert (state := hass.states.get(entity_entry.entity_id))
|
||||
assert float(state.state) == 5
|
||||
|
||||
device.attributes[FBAttributes.heating_level] = None
|
||||
device.notify_update({FBAttributes.heating_level: None})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert (state := hass.states.get(entity_entry.entity_id))
|
||||
assert state.state == "unknown"
|
||||
|
||||
|
||||
async def test_number_not_created_when_attribute_missing(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: Callable[[DummyDevice], MockConfigEntry],
|
||||
) -> None:
|
||||
"""Test no number entity is created when the device does not report the attribute."""
|
||||
device = DummyDevice(DeviceType.FB, attributes={})
|
||||
config_entry = mock_config_entry(device)
|
||||
with patch("homeassistant.components.midea._PLATFORMS", [Platform.NUMBER]):
|
||||
await setup_integration(hass, config_entry, device)
|
||||
|
||||
assert entity_entries(hass, config_entry) == {}
|
||||
|
||||
|
||||
async def test_number_not_created_for_other_device_type(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: Callable[[DummyDevice], MockConfigEntry],
|
||||
) -> None:
|
||||
"""Test no number entity is created for a device type without one (e.g. AC's fan_speed)."""
|
||||
device = DummyDevice(
|
||||
DeviceType.AC,
|
||||
attributes={
|
||||
ACAttributes.power: True,
|
||||
ACAttributes.mode: 1,
|
||||
ACAttributes.target_temperature: 22.0,
|
||||
ACAttributes.indoor_temperature: 21.0,
|
||||
ACAttributes.fan_speed: 60,
|
||||
},
|
||||
)
|
||||
config_entry = mock_config_entry(device)
|
||||
with patch("homeassistant.components.midea._PLATFORMS", [Platform.NUMBER]):
|
||||
await setup_integration(hass, config_entry, device)
|
||||
|
||||
assert entity_entries(hass, config_entry) == {}
|
||||
|
||||
|
||||
async def test_number_set_value_raises_on_device_communication_error(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: Callable[[DummyDevice], MockConfigEntry],
|
||||
) -> None:
|
||||
"""Test a device communication failure surfaces as a HomeAssistantError."""
|
||||
device = _fb_device()
|
||||
config_entry = mock_config_entry(device)
|
||||
with patch("homeassistant.components.midea._PLATFORMS", [Platform.NUMBER]):
|
||||
await setup_integration(hass, config_entry, device)
|
||||
|
||||
entity_entry = entity_entries(hass, config_entry)[f"{TEST_DEVICE_ID}_heating_level"]
|
||||
|
||||
with (
|
||||
patch.object(device, "set_attribute", side_effect=SocketException("offline")),
|
||||
pytest.raises(HomeAssistantError),
|
||||
):
|
||||
await hass.services.async_call(
|
||||
NUMBER_DOMAIN,
|
||||
SERVICE_SET_VALUE,
|
||||
{ATTR_ENTITY_ID: entity_entry.entity_id, ATTR_VALUE: 3},
|
||||
blocking=True,
|
||||
)
|
||||
Reference in New Issue
Block a user