From ed25ae36420848c42bcce74aaff3166bac11dffa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucas=20Mind=C3=AAllo=20de=20Andrade?= Date: Thu, 10 Sep 2026 15:19:49 -0300 Subject: [PATCH] Add Midea Water Heater platform (#180693) --- homeassistant/components/midea/__init__.py | 1 + homeassistant/components/midea/strings.json | 8 + .../components/midea/water_heater.py | 430 +++++++++++++++ .../midea/snapshots/test_water_heater.ambr | 374 +++++++++++++ tests/components/midea/test_water_heater.py | 515 ++++++++++++++++++ 5 files changed, 1328 insertions(+) create mode 100644 homeassistant/components/midea/water_heater.py create mode 100644 tests/components/midea/snapshots/test_water_heater.ambr create mode 100644 tests/components/midea/test_water_heater.py diff --git a/homeassistant/components/midea/__init__.py b/homeassistant/components/midea/__init__.py index be4db2e9a387..7fea47831bd4 100644 --- a/homeassistant/components/midea/__init__.py +++ b/homeassistant/components/midea/__init__.py @@ -40,6 +40,7 @@ _PLATFORMS: list[Platform] = [ Platform.SENSOR, Platform.SWITCH, Platform.TIME, + Platform.WATER_HEATER, ] diff --git a/homeassistant/components/midea/strings.json b/homeassistant/components/midea/strings.json index 356573180aeb..9a2f0ccb8c26 100644 --- a/homeassistant/components/midea/strings.json +++ b/homeassistant/components/midea/strings.json @@ -721,6 +721,14 @@ "timing_regeneration": { "name": "Regeneration time" } + }, + "water_heater": { + "water_heater_bathing": { + "name": "Bathing water heater" + }, + "water_heater_heating": { + "name": "Heating water heater" + } } }, "exceptions": { diff --git a/homeassistant/components/midea/water_heater.py b/homeassistant/components/midea/water_heater.py new file mode 100644 index 000000000000..9be9a3b92135 --- /dev/null +++ b/homeassistant/components/midea/water_heater.py @@ -0,0 +1,430 @@ +"""Water Heater platform for Midea.""" + +from dataclasses import dataclass +from typing import Any, ClassVar, cast, override + +from midealocal.device import DeviceType +from midealocal.devices.c3 import DeviceAttributes as C3Attributes, MideaC3Device +from midealocal.devices.cd import DeviceAttributes as CDAttributes, MideaCDDevice +from midealocal.devices.e2 import DeviceAttributes as E2Attributes, MideaE2Device +from midealocal.devices.e3 import DeviceAttributes as E3Attributes, MideaE3Device +from midealocal.devices.e6 import DeviceAttributes as E6Attributes, MideaE6Device + +from homeassistant.components.water_heater import ( + WaterHeaterEntity, + WaterHeaterEntityDescription, + WaterHeaterEntityFeature, +) +from homeassistant.const import ( + ATTR_TEMPERATURE, + PRECISION_HALVES, + PRECISION_WHOLE, + STATE_OFF, + STATE_ON, + UnitOfTemperature, +) +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 MideaWaterHeaterEntityDescription(WaterHeaterEntityDescription): + """Description for a Midea water heater entity.""" + + models: list[DeviceType] + zone: int = 0 + + +WATER_HEATERS: list[MideaWaterHeaterEntityDescription] = [ + MideaWaterHeaterEntityDescription( + key="water_heater", + translation_key="water_heater", + models=[ + DeviceType.C3, + DeviceType.CD, + DeviceType.E2, + DeviceType.E3, + ], + ), + MideaWaterHeaterEntityDescription( + key="water_heater_heating", + translation_key="water_heater_heating", + models=[ + DeviceType.E6, + ], + ), + MideaWaterHeaterEntityDescription( + key="water_heater_bathing", + translation_key="water_heater_bathing", + models=[ + DeviceType.E6, + ], + zone=1, + ), +] + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: MideaConfigEntry, + async_add_entities: AddConfigEntryEntitiesCallback, +) -> None: + """Set up water heater entries.""" + device = config_entry.runtime_data + + entities: list[MideaWaterHeater] = [] + for description in WATER_HEATERS: + if device.device_type not in description.models: + continue + if device.device_type == DeviceType.E2: + entities.append( + MideaE2WaterHeater(cast("MideaE2Device", device), description) + ) + elif device.device_type == DeviceType.E3: + entities.append( + MideaE3WaterHeater(cast("MideaE3Device", device), description) + ) + elif device.device_type == DeviceType.E6: + entities.append( + MideaE6WaterHeater(cast("MideaE6Device", device), description) + ) + elif device.device_type == DeviceType.C3: + entities.append( + MideaC3WaterHeater(cast("MideaC3Device", device), description) + ) + elif device.device_type == DeviceType.CD: + entities.append( + MideaCDWaterHeater(cast("MideaCDDevice", device), description) + ) + + async_add_entities(entities) + + +type MideaWaterHeaterDevice = ( + MideaE2Device | MideaE3Device | MideaC3Device | MideaE6Device | MideaCDDevice +) + + +class MideaWaterHeater(MideaEntity, WaterHeaterEntity): + """Midea Water Heater Entries Base Class.""" + + _device: MideaWaterHeaterDevice + + def __init__( + self, + device: MideaWaterHeaterDevice, + description: MideaWaterHeaterEntityDescription, + ) -> None: + """Midea Water Heater entity init.""" + super().__init__(device, description) + self._operations: list[str] = [] + self._attr_supported_features = ( + WaterHeaterEntityFeature.TARGET_TEMPERATURE + | WaterHeaterEntityFeature.ON_OFF + ) + self._attr_precision = float(PRECISION_WHOLE) + self._attr_temperature_unit = UnitOfTemperature.CELSIUS + + @property + @override + def target_temperature_step(self) -> float | None: + """Return the supported target temperature step.""" + return cast("float | None", getattr(self._device, "temperature_step", None)) + + @property + @override + def current_operation(self) -> str | None: + """Midea Water Heater current operation.""" + return STATE_ON if self._device.get_attribute("power") else STATE_OFF + + @property + @override + def current_temperature(self) -> float: + """Midea Water Heater current temperature.""" + return cast("float", self._device.get_attribute("current_temperature")) + + @property + @override + def target_temperature(self) -> float: + """Midea Water Heater target temperature.""" + return cast("float", self._device.get_attribute("target_temperature")) + + @override + def set_temperature(self, **kwargs: Any) -> None: + """Midea Water Heater set temperature.""" + if ATTR_TEMPERATURE not in kwargs: + return # pragma: no cover + with midea_api_call(): + temperature = float(kwargs[ATTR_TEMPERATURE]) + self._device.set_attribute("target_temperature", temperature) + + @override + def turn_on(self, **kwargs: Any) -> None: + """Midea Water Heater turn on.""" + with midea_api_call(): + self._device.set_attribute(attr="power", value=True) + + @override + def turn_off(self, **kwargs: Any) -> None: + """Midea Water Heater turn off.""" + with midea_api_call(): + self._device.set_attribute(attr="power", value=False) + + +class MideaE2WaterHeater(MideaWaterHeater): + """Midea E2 Water Heater Entries.""" + + _device: MideaE2Device + + @property + @override + def min_temp(self) -> float: + """Midea E2 Water Heater min temperature.""" + return cast("float", self._device.get_attribute(E2Attributes.temperature_min)) + + @property + @override + def max_temp(self) -> float: + """Midea E2 Water Heater max temperature.""" + return cast("float", self._device.get_attribute(E2Attributes.temperature_max)) + + +class MideaE3WaterHeater(MideaWaterHeater): + """Midea E3 Water Heater Entries.""" + + _device: MideaE3Device + + @property + @override + def min_temp(self) -> float: + """Midea E3 Water Heater min temperature.""" + return cast("float", self._device.get_attribute(E3Attributes.temperature_min)) + + @property + @override + def max_temp(self) -> float: + """Midea E3 Water Heater max temperature.""" + return cast("float", self._device.get_attribute(E3Attributes.temperature_max)) + + @property + @override + def precision(self) -> float: + """Midea E3 Water Heater precision.""" + return float( + PRECISION_HALVES if self._device.precision_halves else PRECISION_WHOLE, + ) + + +class MideaC3WaterHeater(MideaWaterHeater): + """Midea C3 Water Heater Entries.""" + + _device: MideaC3Device + + @property + @override + def current_operation(self) -> str: + """Midea C3 Water Heater current operation.""" + return ( + STATE_ON + if self._device.get_attribute(C3Attributes.dhw_power) + else STATE_OFF + ) + + @property + @override + def current_temperature(self) -> float: + """Midea C3 Water Heater current temperature.""" + return cast( + "float", + self._device.get_attribute(C3Attributes.tank_actual_temperature), + ) + + @property + @override + def target_temperature(self) -> float: + """Midea C3 Water Heater target temperature.""" + return cast("float", self._device.get_attribute(C3Attributes.dhw_target_temp)) + + @override + def set_temperature(self, **kwargs: Any) -> None: + """Midea C3 Water Heater set temperature.""" + if ATTR_TEMPERATURE not in kwargs: + return # pragma: no cover + with midea_api_call(): + temperature = float(kwargs[ATTR_TEMPERATURE]) + self._device.set_attribute(C3Attributes.dhw_target_temp, temperature) + + @property + @override + def min_temp(self) -> float: + """Midea C3 Water Heater min temperature.""" + return cast("float", self._device.get_attribute(C3Attributes.dhw_temp_min)) + + @property + @override + def max_temp(self) -> float: + """Midea C3 Water Heater max temperature.""" + return cast("float", self._device.get_attribute(C3Attributes.dhw_temp_max)) + + @override + def turn_on(self, **kwargs: Any) -> None: + """Midea C3 Water Heater turn on.""" + with midea_api_call(): + self._device.set_attribute(attr=C3Attributes.dhw_power, value=True) + + @override + def turn_off(self, **kwargs: Any) -> None: + """Midea C3 Water Heater turn off.""" + with midea_api_call(): + self._device.set_attribute(attr=C3Attributes.dhw_power, value=False) + + +class MideaE6WaterHeater(MideaWaterHeater): + """Midea E6 Water Heater Entries.""" + + _device: MideaE6Device + entity_description: MideaWaterHeaterEntityDescription + + _powers: ClassVar[list[E6Attributes]] = [ + E6Attributes.heating_power, + E6Attributes.main_power, + ] + _current_temperatures: ClassVar[list[E6Attributes]] = [ + E6Attributes.heating_leaving_temperature, + E6Attributes.bathing_leaving_temperature, + ] + _target_temperatures: ClassVar[list[E6Attributes]] = [ + E6Attributes.heating_temperature, + E6Attributes.bathing_temperature, + ] + + def __init__( + self, device: MideaE6Device, description: MideaWaterHeaterEntityDescription + ) -> None: + """Midea E6 Water Heater entity init.""" + super().__init__(device, description) + self._power_attr = MideaE6WaterHeater._powers[description.zone] + self._current_temperature_attr = MideaE6WaterHeater._current_temperatures[ + description.zone + ] + self._target_temperature_attr = MideaE6WaterHeater._target_temperatures[ + description.zone + ] + self._attr_supported_features = ( + WaterHeaterEntityFeature.TARGET_TEMPERATURE + | WaterHeaterEntityFeature.ON_OFF + ) + if description.zone == 0: + self._attr_supported_features |= WaterHeaterEntityFeature.AWAY_MODE + + @property + @override + def current_operation(self) -> str: + """Midea E6 Water Heater current operation.""" + if self.entity_description.zone == 0: + return ( + STATE_ON + if self._device.get_attribute(E6Attributes.main_power) + and self._device.get_attribute(E6Attributes.heating_power) + else STATE_OFF + ) + return ( + STATE_ON + if self._device.get_attribute(E6Attributes.main_power) + else STATE_OFF + ) + + @property + @override + def current_temperature(self) -> float: + """Midea E6 Water Heater current temperature.""" + return cast("float", self._device.get_attribute(self._current_temperature_attr)) + + @property + @override + def target_temperature(self) -> float: + """Midea E6 Water Heater target temperature.""" + return cast("float", self._device.get_attribute(self._target_temperature_attr)) + + @override + def set_temperature(self, **kwargs: Any) -> None: + """Midea E6 Water Heater set temperature.""" + if ATTR_TEMPERATURE not in kwargs: + return # pragma: no cover + with midea_api_call(): + temperature = float(kwargs[ATTR_TEMPERATURE]) + self._device.set_attribute(self._target_temperature_attr, temperature) + + @property + @override + def min_temp(self) -> float: + """Midea E6 Water Heater min temperature.""" + min_temperature = cast( + "list[float]", + self._device.get_attribute(E6Attributes.temperature_min), + ) + return min_temperature[self.entity_description.zone] + + @property + @override + def max_temp(self) -> float: + """Midea E6 Water Heater max temperature.""" + max_temperature = cast( + "list[float]", + self._device.get_attribute(E6Attributes.temperature_max), + ) + return max_temperature[self.entity_description.zone] + + @override + def turn_on(self, **kwargs: Any) -> None: + """Midea E6 Water Heater turn on.""" + with midea_api_call(): + self._device.set_attribute(attr=self._power_attr, value=True) + + @override + def turn_off(self, **kwargs: Any) -> None: + """Midea E6 Water Heater turn off.""" + with midea_api_call(): + self._device.set_attribute(attr=self._power_attr, value=False) + + @property + @override + def is_away_mode_on(self) -> bool: + """Return whether E6 away mode is active.""" + return self._device.get_attribute(E6Attributes.heating_modes) == "out" + + @override + def turn_away_mode_on(self) -> None: + """Midea Water Heater turn away mode on.""" + with midea_api_call(): + self._device.set_attribute(attr=E6Attributes.heating_modes, value="out") + + @override + def turn_away_mode_off(self) -> None: + """Midea Water Heater turn away mode off.""" + with midea_api_call(): + self._device.set_attribute( + attr=E6Attributes.heating_modes, value=self._device.preset_modes[0] + ) + + +class MideaCDWaterHeater(MideaWaterHeater): + """Midea CD Water Heater Entries.""" + + _device: MideaCDDevice + + @property + @override + def min_temp(self) -> float: + """Midea CD Water Heater min temperature.""" + return cast("float", self._device.get_attribute(CDAttributes.min_temperature)) + + @property + @override + def max_temp(self) -> float: + """Midea CD Water Heater max temperature.""" + return cast("float", self._device.get_attribute(CDAttributes.max_temperature)) diff --git a/tests/components/midea/snapshots/test_water_heater.ambr b/tests/components/midea/snapshots/test_water_heater.ambr new file mode 100644 index 000000000000..372cc771901e --- /dev/null +++ b/tests/components/midea/snapshots/test_water_heater.ambr @@ -0,0 +1,374 @@ +# serializer version: 1 +# name: test_midea_water_heater_default_state[c3][water_heater.heat_pump_wi_fi_controller-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + : 60, + : 20, + : 1, + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'water_heater', + 'entity_category': None, + 'entity_id': 'water_heater.heat_pump_wi_fi_controller', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'midea', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': , + 'translation_key': 'water_heater', + 'unique_id': '12345678_water_heater', + 'unit_of_measurement': None, + }) +# --- +# name: test_midea_water_heater_default_state[c3][water_heater.heat_pump_wi_fi_controller-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : None, + : 'Heat Pump Wi-Fi Controller', + : 60, + : 20, + : , + : None, + : None, + : 1, + : 25, + }), + 'context': , + 'entity_id': 'water_heater.heat_pump_wi_fi_controller', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_midea_water_heater_default_state[cd][water_heater.heat_pump_water_heater-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + : 65, + : 35, + : 1, + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'water_heater', + 'entity_category': None, + 'entity_id': 'water_heater.heat_pump_water_heater', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'midea', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': , + 'translation_key': 'water_heater', + 'unique_id': '12345678_water_heater', + 'unit_of_measurement': None, + }) +# --- +# name: test_midea_water_heater_default_state[cd][water_heater.heat_pump_water_heater-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 41, + : 'Heat Pump Water Heater', + : 65, + : 35, + : , + : None, + : None, + : 1, + : 40, + }), + 'context': , + 'entity_id': 'water_heater.heat_pump_water_heater', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_midea_water_heater_default_state[e2][water_heater.electric_water_heater-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + : 75, + : 30, + : 1, + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'water_heater', + 'entity_category': None, + 'entity_id': 'water_heater.electric_water_heater', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'midea', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': , + 'translation_key': 'water_heater', + 'unique_id': '12345678_water_heater', + 'unit_of_measurement': None, + }) +# --- +# name: test_midea_water_heater_default_state[e2][water_heater.electric_water_heater-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 21, + : 'Electric Water Heater', + : 75, + : 30, + : , + : None, + : None, + : 1, + : 40, + }), + 'context': , + 'entity_id': 'water_heater.electric_water_heater', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_midea_water_heater_default_state[e3][water_heater.gas_water_heater-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + : 65.0, + : 35.0, + : 1, + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'water_heater', + 'entity_category': None, + 'entity_id': 'water_heater.gas_water_heater', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'midea', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': , + 'translation_key': 'water_heater', + 'unique_id': '12345678_water_heater', + 'unit_of_measurement': None, + }) +# --- +# name: test_midea_water_heater_default_state[e3][water_heater.gas_water_heater-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 38.0, + : 'Gas Water Heater', + : 65.0, + : 35.0, + : , + : None, + : None, + : 1, + : 40.0, + }), + 'context': , + 'entity_id': 'water_heater.gas_water_heater', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_midea_water_heater_default_state[e6][water_heater.gas_boiler_bathing_water_heater-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + : 60, + : 35, + : 1, + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'water_heater', + 'entity_category': None, + 'entity_id': 'water_heater.gas_boiler_bathing_water_heater', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Bathing water heater', + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Bathing water heater', + 'platform': 'midea', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': , + 'translation_key': 'water_heater_bathing', + 'unique_id': '12345678_water_heater_bathing', + 'unit_of_measurement': None, + }) +# --- +# name: test_midea_water_heater_default_state[e6][water_heater.gas_boiler_bathing_water_heater-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : None, + : 'Gas Boiler Bathing water heater', + : 60, + : 35, + : , + : None, + : None, + : 1, + : 40, + }), + 'context': , + 'entity_id': 'water_heater.gas_boiler_bathing_water_heater', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_midea_water_heater_default_state[e6][water_heater.gas_boiler_heating_water_heater-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + : 80, + : 30, + : 1, + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'water_heater', + 'entity_category': None, + 'entity_id': 'water_heater.gas_boiler_heating_water_heater', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Heating water heater', + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Heating water heater', + 'platform': 'midea', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': , + 'translation_key': 'water_heater_heating', + 'unique_id': '12345678_water_heater_heating', + 'unit_of_measurement': None, + }) +# --- +# name: test_midea_water_heater_default_state[e6][water_heater.gas_boiler_heating_water_heater-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'off', + : None, + : 'Gas Boiler Heating water heater', + : 80, + : 30, + : , + : None, + : None, + : 1, + : 50, + }), + 'context': , + 'entity_id': 'water_heater.gas_boiler_heating_water_heater', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- diff --git a/tests/components/midea/test_water_heater.py b/tests/components/midea/test_water_heater.py new file mode 100644 index 000000000000..06433b2129fc --- /dev/null +++ b/tests/components/midea/test_water_heater.py @@ -0,0 +1,515 @@ +"""Tests for the Midea water heater platform.""" + +from collections.abc import Callable +from unittest.mock import patch + +from midealocal.const import DeviceType +from midealocal.devices.c3 import C3SilentLevel, DeviceAttributes as C3Attributes +from midealocal.devices.cd import DeviceAttributes as CDAttributes +from midealocal.devices.e2 import DeviceAttributes as E2Attributes +from midealocal.devices.e3 import DeviceAttributes as E3Attributes +from midealocal.devices.e6 import DeviceAttributes as E6Attributes +import pytest +from syrupy.assertion import SnapshotAssertion + +from homeassistant.components.water_heater import ( + ATTR_AWAY_MODE, + ATTR_TEMPERATURE, + DOMAIN as WATER_HEATER_DOMAIN, + SERVICE_SET_AWAY_MODE, + SERVICE_SET_TEMPERATURE, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, +) +from homeassistant.const import ATTR_ENTITY_ID, Platform +from homeassistant.core import HomeAssistant +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 _c3_device() -> DummyDevice: + return DummyDevice( + DeviceType.C3, + attributes={ + C3Attributes.zone1_power: False, + C3Attributes.zone2_power: False, + C3Attributes.dhw_power: False, + C3Attributes.zone1_curve: False, + C3Attributes.zone2_curve: False, + C3Attributes.disinfect: False, + C3Attributes.fast_dhw: False, + C3Attributes.zone_temp_type: [False, False], + C3Attributes.zone1_room_temp_mode: False, + C3Attributes.zone2_room_temp_mode: False, + C3Attributes.zone1_water_temp_mode: False, + C3Attributes.zone2_water_temp_mode: False, + C3Attributes.silent_mode: False, + C3Attributes.silent_level: C3SilentLevel.OFF.name.lower(), + C3Attributes.eco_mode: False, + C3Attributes.tbh: False, + C3Attributes.mode: 1, + C3Attributes.mode_auto: 1, + C3Attributes.zone_target_temp: [25.0, 25.0], + C3Attributes.dhw_target_temp: 25.0, + C3Attributes.room_target_temp: 30.0, + C3Attributes.zone_heating_temp_max: [55.0, 55.0], + C3Attributes.zone_heating_temp_min: [25.0, 25.0], + C3Attributes.zone_cooling_temp_max: [25.0, 25.0], + C3Attributes.zone_cooling_temp_min: [5.0, 5.0], + C3Attributes.room_temp_max: 60.0, + C3Attributes.room_temp_min: 34.0, + C3Attributes.dhw_temp_max: 60.0, + C3Attributes.dhw_temp_min: 20.0, + C3Attributes.tank_actual_temperature: None, + C3Attributes.target_temperature: [25.0, 25.0], + C3Attributes.temperature_max: [0.0, 0.0], + C3Attributes.temperature_min: [0.0, 0.0], + C3Attributes.total_energy_consumption: None, + C3Attributes.status_heating: None, + C3Attributes.status_dhw: None, + C3Attributes.status_tbh: None, + C3Attributes.status_ibh: None, + C3Attributes.total_produced_energy: None, + C3Attributes.outdoor_temperature: None, + C3Attributes.temp_tw_in: None, + C3Attributes.temp_tw_out: None, + C3Attributes.instant_power0: None, + C3Attributes.error_code: 0, + }, + ) + + +def _cd_device() -> DummyDevice: + device = DummyDevice( + DeviceType.CD, + attributes={ + CDAttributes.power: True, + CDAttributes.mode: "standard", + CDAttributes.max_temperature: 65.0, + CDAttributes.min_temperature: 35.0, + CDAttributes.target_temperature: 40.0, + CDAttributes.current_temperature: 41.0, + CDAttributes.outdoor_temperature: 25.0, + CDAttributes.condenser_temperature: 23.0, + CDAttributes.compressor_temperature: 30.0, + CDAttributes.compressor_status: None, + CDAttributes.water_level: None, + CDAttributes.fahrenheit: False, + CDAttributes.heat: None, + CDAttributes.dual_heat: None, + CDAttributes.elec_heat: None, + CDAttributes.top_elec_heat: None, + CDAttributes.bottom_elec_heat: None, + CDAttributes.water_pump: None, + CDAttributes.four_way: None, + CDAttributes.back_water: None, + CDAttributes.sterilize: None, + CDAttributes.disinfect: None, + CDAttributes.disinfection_temperature: None, + CDAttributes.top_temperature: None, + CDAttributes.bottom_temperature: None, + CDAttributes.wind: None, + CDAttributes.eco: None, + CDAttributes.smart_grid: None, + CDAttributes.multi_terminal: None, + CDAttributes.mute_effect: None, + CDAttributes.mute_status: None, + CDAttributes.maintenance_reminder: None, + CDAttributes.maintain_warn_tag: None, + CDAttributes.maintain_warn: None, + CDAttributes.error_code: None, + CDAttributes.typeinfo: None, + CDAttributes.vacation_mode: False, + CDAttributes.vacation_days: 0, + CDAttributes.vacation_temperature: None, + CDAttributes.vacation_start_year: None, + CDAttributes.vacation_start_month: None, + CDAttributes.vacation_start_day: None, + CDAttributes.order1_effect: None, + CDAttributes.order2_effect: None, + CDAttributes.auto_sterilize_week: None, + CDAttributes.auto_sterilize_hour: None, + CDAttributes.auto_sterilize_minute: None, + CDAttributes.weekly_effects: None, + CDAttributes.weekly_schedule: None, + CDAttributes.daily_timer_schedule: None, + }, + ) + device.preset_modes = [ + "none", + "energy_save", + "standard", + "dual", + "smart", + ] + return device + + +def _e2_device() -> DummyDevice: + return DummyDevice( + DeviceType.E2, + attributes={ + E2Attributes.power: True, + E2Attributes.heating: True, + E2Attributes.keep_warm: True, + E2Attributes.protection: False, + E2Attributes.temperature_max: 75.0, + E2Attributes.temperature_min: 30.0, + E2Attributes.current_temperature: 21.0, + E2Attributes.target_temperature: 40.0, + E2Attributes.whole_tank_heating: True, + E2Attributes.variable_heating: True, + E2Attributes.heating_time_remaining: 50, + E2Attributes.water_consumption: None, + E2Attributes.heating_power: None, + E2Attributes.fast_hot_power: None, + E2Attributes.water_flow: None, + E2Attributes.sterilization: None, + E2Attributes.heat_water_level: None, + E2Attributes.eplus: None, + E2Attributes.memory: None, + E2Attributes.fast_wash: None, + E2Attributes.half_heat: None, + E2Attributes.summer: None, + E2Attributes.winter: None, + E2Attributes.efficient: None, + E2Attributes.night: None, + E2Attributes.screen_off: None, + E2Attributes.sleep: None, + E2Attributes.cloud: None, + E2Attributes.appoint_wash: None, + E2Attributes.now_wash: None, + E2Attributes.smart_sterilize: None, + E2Attributes.sterilize_high_temp: None, + E2Attributes.uv_sterilize: None, + E2Attributes.discharge_status: None, + E2Attributes.top_temp: None, + E2Attributes.bottom_heat: None, + E2Attributes.top_heat: None, + E2Attributes.water_cyclic: None, + E2Attributes.water_system: None, + E2Attributes.in_temperature: None, + E2Attributes.day_water_consumption: None, + E2Attributes.volume: None, + E2Attributes.rate: None, + }, + ) + + +def _e3_device() -> DummyDevice: + device = DummyDevice( + DeviceType.E3, + attributes={ + E3Attributes.power: False, + E3Attributes.burning_state: False, + E3Attributes.zero_cold_water: False, + E3Attributes.protection: False, + E3Attributes.zero_cold_pulse: False, + E3Attributes.smart_volume: False, + E3Attributes.current_temperature: 38.0, + E3Attributes.target_temperature: 40.0, + E3Attributes.temperature_min: 35.0, + E3Attributes.temperature_max: 65.0, + }, + ) + device.precision_halves = True + return device + + +def _e6_device() -> DummyDevice: + device = DummyDevice( + DeviceType.E6, + attributes={ + E6Attributes.main_power: False, + E6Attributes.heating_power: True, + E6Attributes.heating_working: None, + E6Attributes.bathing_working: None, + E6Attributes.temperature_min: [30.0, 35.0], + E6Attributes.temperature_max: [80.0, 60.0], + E6Attributes.heating_temperature: 50.0, + E6Attributes.bathing_temperature: 40.0, + E6Attributes.heating_leaving_temperature: None, + E6Attributes.bathing_leaving_temperature: None, + E6Attributes.cold_water_single: None, + E6Attributes.cold_water_dot: None, + E6Attributes.heating_modes: None, + }, + ) + device.preset_modes = [ + "normal", + "out", + "home", + "sleep", + ] + return device + + +async def _assert_service_calls( + hass: HomeAssistant, + entity_id: str, + service: str, + service_data: dict, + expected_calls: list[tuple], + device: DummyDevice, +) -> None: + """Call a water heater service and assert the fake device recorded the right call.""" + device.calls.clear() + await hass.services.async_call( + WATER_HEATER_DOMAIN, + service, + {ATTR_ENTITY_ID: entity_id, **service_data}, + blocking=True, + ) + assert device.calls == expected_calls + + +@pytest.mark.parametrize( + "device", + [ + pytest.param( + _c3_device(), + id="c3", + ), + pytest.param( + _cd_device(), + id="cd", + ), + pytest.param( + _e2_device(), + id="e2", + ), + pytest.param( + _e3_device(), + id="e3", + ), + pytest.param( + _e6_device(), + id="e6", + ), + ], +) +async def test_midea_water_heater_default_state( + hass: HomeAssistant, + mock_config_entry: Callable[[DummyDevice], MockConfigEntry], + device: DummyDevice, + snapshot: SnapshotAssertion, + entity_registry: er.EntityRegistry, +) -> None: + """Test water heater entities are created and service calls reach the device.""" + config_entry = mock_config_entry(device) + with patch("homeassistant.components.midea._PLATFORMS", [Platform.WATER_HEATER]): + await setup_integration(hass, config_entry, device) + + await snapshot_platform(hass, entity_registry, snapshot, config_entry.entry_id) + + +@pytest.mark.parametrize( + ("device", "services", "services_data", "expected_calls", "entity_suffix"), + [ + pytest.param( + _c3_device(), + [ + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + SERVICE_SET_TEMPERATURE, + ], + [ + {}, + {}, + {ATTR_TEMPERATURE: 50.0}, + ], + [ + [("set_attribute", C3Attributes.dhw_power, False)], + [("set_attribute", C3Attributes.dhw_power, True)], + [("set_attribute", C3Attributes.dhw_target_temp, 50.0)], + ], + "water_heater", + id="c3", + ), + pytest.param( + _cd_device(), + [ + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + SERVICE_SET_TEMPERATURE, + ], + [ + {}, + {}, + {ATTR_TEMPERATURE: 50.0}, + ], + [ + [("set_attribute", CDAttributes.power, False)], + [("set_attribute", CDAttributes.power, True)], + [("set_attribute", CDAttributes.target_temperature, 50.0)], + ], + "water_heater", + id="cd", + ), + pytest.param( + _e2_device(), + [ + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + SERVICE_SET_TEMPERATURE, + ], + [ + {}, + {}, + {ATTR_TEMPERATURE: 50.0}, + ], + [ + [("set_attribute", E2Attributes.power, False)], + [("set_attribute", E2Attributes.power, True)], + [("set_attribute", E2Attributes.target_temperature, 50.0)], + ], + "water_heater", + id="e2", + ), + pytest.param( + _e3_device(), + [ + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + SERVICE_SET_TEMPERATURE, + ], + [ + {}, + {}, + {ATTR_TEMPERATURE: 50.0}, + ], + [ + [("set_attribute", E3Attributes.power, False)], + [("set_attribute", E3Attributes.power, True)], + [("set_attribute", E3Attributes.target_temperature, 50.0)], + ], + "water_heater", + id="e3", + ), + pytest.param( + _e6_device(), + [ + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + SERVICE_SET_TEMPERATURE, + SERVICE_SET_AWAY_MODE, + SERVICE_SET_AWAY_MODE, + ], + [ + {}, + {}, + {ATTR_TEMPERATURE: 50.0}, + {ATTR_AWAY_MODE: True}, + {ATTR_AWAY_MODE: False}, + ], + [ + [ + ("set_attribute", E6Attributes.heating_power, False), + ], + [ + ("set_attribute", E6Attributes.heating_power, True), + ], + [ + ("set_attribute", E6Attributes.heating_temperature, 50.0), + ], + [ + ("set_attribute", E6Attributes.heating_modes, "out"), + ], + [ + ("set_attribute", E6Attributes.heating_modes, "normal"), + ], + ], + "water_heater_heating", + id="e6_heating", + ), + pytest.param( + _e6_device(), + [ + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + SERVICE_SET_TEMPERATURE, + ], + [ + {}, + {}, + {ATTR_TEMPERATURE: 50.0}, + ], + [ + [ + ("set_attribute", E6Attributes.main_power, False), + ], + [ + ("set_attribute", E6Attributes.main_power, True), + ], + [ + ("set_attribute", E6Attributes.bathing_temperature, 50.0), + ], + ], + "water_heater_bathing", + id="e6_bathing", + ), + ], +) +async def test_midea_water_heater_services( + hass: HomeAssistant, + mock_config_entry: Callable[[DummyDevice], MockConfigEntry], + device: DummyDevice, + services: list[str], + services_data: list[dict], + expected_calls: list[list[tuple]], + entity_suffix: str, +) -> None: + """Test water heater service calls reach the device.""" + config_entry = mock_config_entry(device) + with patch("homeassistant.components.midea._PLATFORMS", [Platform.WATER_HEATER]): + await setup_integration(hass, config_entry, device) + + entity_entry = entity_entries(hass, config_entry)[ + f"{TEST_DEVICE_ID}_{entity_suffix}" + ] + for i, service in enumerate(services): + await _assert_service_calls( + hass, + entity_entry.entity_id, + service, + services_data[i], + expected_calls[i], + device, + ) + + +async def test_e2_min_max_temperature_from_device( + hass: HomeAssistant, + mock_config_entry: Callable[[DummyDevice], MockConfigEntry], +) -> None: + """Test E2 min/max temperature are read from the device attributes.""" + device = _e2_device() + config_entry = mock_config_entry(device) + await setup_integration(hass, config_entry, device) + entity_entry = entity_entries(hass, config_entry)[f"{TEST_DEVICE_ID}_water_heater"] + entity = hass.data[WATER_HEATER_DOMAIN].get_entity(entity_entry.entity_id) + + assert entity is not None + assert entity.min_temp == 30.0 + assert entity.max_temp == 75.0 + + +async def test_e3_min_max_temperature_from_device( + hass: HomeAssistant, + mock_config_entry: Callable[[DummyDevice], MockConfigEntry], +) -> None: + """Test E3 min/max temperature are read from the device attributes.""" + device = _e3_device() + config_entry = mock_config_entry(device) + await setup_integration(hass, config_entry, device) + entity_entry = entity_entries(hass, config_entry)[f"{TEST_DEVICE_ID}_water_heater"] + entity = hass.data[WATER_HEATER_DOMAIN].get_entity(entity_entry.entity_id) + + assert entity is not None + assert entity.min_temp == 35.0 + assert entity.max_temp == 65.0