HEX
Server: Apache/2.4.63 (Unix)
System: Linux Synopilou92 4.4.302+ #72806 SMP Mon Jul 21 23:16:00 CST 2025 x86_64
User: pilou92 (1026)
PHP: 8.0.30
Disabled: NONE
Upload Files
File: /volume1/docker/homeassistant2025/custom_components/xiaomi_miot/water_heater.py
"""Support for Xiaomi water heaters."""
import logging
import math

from homeassistant.const import (
    ATTR_TEMPERATURE,
    UnitOfTemperature,
)
from homeassistant.components.water_heater import (
    DOMAIN as ENTITY_DOMAIN,
    WaterHeaterEntity,
    WaterHeaterEntityFeature,  # v2022.5
)

from . import (
    DOMAIN,
    CONF_MODEL,
    XIAOMI_CONFIG_SCHEMA as PLATFORM_SCHEMA,  # noqa: F401
    HassEntry,
    MiotToggleEntity,
    async_setup_config_entry,
    bind_services_to_entries,
)
from .core.miot_spec import (
    MiotSpec,
    MiotService,
)

_LOGGER = logging.getLogger(__name__)
DATA_KEY = f'{ENTITY_DOMAIN}.{DOMAIN}'

DEFAULT_MIN_TEMP = 40
DEFAULT_MAX_TEMP = 65

SERVICE_TO_METHOD = {}


async def async_setup_entry(hass, config_entry, async_add_entities):
    HassEntry.init(hass, config_entry).new_adder(ENTITY_DOMAIN, async_add_entities)
    await async_setup_config_entry(hass, config_entry, async_setup_platform, async_add_entities, ENTITY_DOMAIN)


async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
    hass.data.setdefault(DATA_KEY, {})
    hass.data[DOMAIN]['add_entities'][ENTITY_DOMAIN] = async_add_entities
    config['hass'] = hass
    model = str(config.get(CONF_MODEL) or '')
    spec = hass.data[DOMAIN]['miot_specs'].get(model)
    entities = []
    if isinstance(spec, MiotSpec):
        for srv in spec.get_services(ENTITY_DOMAIN, 'kettle', 'water_dispenser'):
            if not srv.get_property('mode', 'target_temperature'):
                continue
            entities.append(MiotWaterHeaterEntity(config, srv))
    for entity in entities:
        hass.data[DOMAIN]['entities'][entity.unique_id] = entity
    async_add_entities(entities, update_before_add=True)
    bind_services_to_entries(hass, SERVICE_TO_METHOD)


class MiotWaterHeaterEntity(MiotToggleEntity, WaterHeaterEntity):
    def __init__(self, config, miot_service: MiotService):
        super().__init__(miot_service, config=config, logger=_LOGGER)

        self._prop_status = miot_service.get_property('status')
        self._prop_mode = miot_service.get_property('mode')
        self._prop_modes = []
        if self._prop_mode:
            self._prop_modes.append(self._prop_mode)
        self._prop_modes.extend(miot_service.get_properties('water_level'))
        self._prop_temperature = miot_service.get_property('temperature', 'indoor_temperature')
        self._prop_target_temp = miot_service.get_property('target_temperature')
        self._prev_target_temp = None

        if self._prop_target_temp:
            self._supported_features |= WaterHeaterEntityFeature.TARGET_TEMPERATURE
        if self._prop_modes:
            self._supported_features |= WaterHeaterEntityFeature.OPERATION_MODE

    async def async_update(self):
        await super().async_update()
        if not self._available:
            return
        if self._prop_power:
            if not self._prop_power.readable and self._prop_status:
                # https://github.com/al-one/hass-xiaomi-miot/issues/30
                val = self._prop_status.from_device(self.device)
                if val is not None:
                    off = val in self._prop_status.list_search('Off')
                    await self.async_update_attrs({
                        self._prop_power.full_name: not off,
                    })

    @property
    def current_operation(self):
        """Return current operation ie. eco, electric, performance, ..."""
        for p in self._prop_modes:
            val = p.from_device(self.device)
            if val is not None:
                return p.list_description(val)
        return None

    @property
    def operation_list(self):
        """Return the list of available operation modes."""
        for p in self._prop_modes:
            return p.list_descriptions() or []
        return None

    async def async_set_operation_mode(self, mode):
        """Set new target operation mode."""
        for p in self._prop_modes:
            val = p.list_value(mode)
            return await self.async_set_property(p, val)
        raise NotImplementedError()

    @property
    def current_temperature(self):
        """Return the current temperature."""
        if self._prop_temperature:
            return round(self._prop_temperature.from_device(self.device) or 0, 2)
        return None

    @property
    def temperature_unit(self):
        prop = self._prop_temperature or self._prop_target_temp
        if prop:
            if prop.unit in ['celsius', UnitOfTemperature.CELSIUS]:
                return UnitOfTemperature.CELSIUS
            if prop.unit in ['fahrenheit', UnitOfTemperature.FAHRENHEIT]:
                return UnitOfTemperature.FAHRENHEIT
            if prop.unit in ['kelvin', UnitOfTemperature.KELVIN]:
                return UnitOfTemperature.KELVIN
        return UnitOfTemperature.CELSIUS

    async def async_set_temperature(self, **kwargs):
        """Set new target temperature."""
        if self._prop_target_temp:
            val = kwargs.get(ATTR_TEMPERATURE) or 0
            stp = self._prop_target_temp.range_step()
            if stp and stp > 1:
                val = round(val / stp) * stp
            elif self._prev_target_temp is None:
                val = round(val)
            elif val >= self._prev_target_temp:
                val = math.ceil(val)
            else:
                val = int(val)
            ret = await self.async_set_property(self._prop_target_temp, val)
            if ret:
                self._prev_target_temp = val
            return ret
        raise NotImplementedError()

    @property
    def target_temperature(self):
        """Return the temperature we try to reach."""
        if self._prop_target_temp:
            val = round(self._prop_target_temp.from_device(self.device) or 0, 2)
            if val:
                self._prev_target_temp = val
            elif self._prev_target_temp:
                val = self._prev_target_temp
            return val
        return None

    @property
    def target_temperature_high(self):
        """Return the highbound target temperature we try to reach."""
        if self._prop_target_temp:
            return self._prop_target_temp.range_max()
        return None

    @property
    def target_temperature_low(self):
        """Return the lowbound target temperature we try to reach."""
        if self._prop_target_temp:
            return self._prop_target_temp.range_min()
        return None

    @property
    def min_temp(self):
        """Return the minimum temperature."""
        return self.target_temperature_low or DEFAULT_MIN_TEMP

    @property
    def max_temp(self):
        """Return the maximum temperature."""
        return self.target_temperature_high or DEFAULT_MAX_TEMP

    @property
    def is_away_mode_on(self):
        """Return true if away mode is on."""
        return None

    def turn_away_mode_on(self):
        """Turn away mode on."""
        raise NotImplementedError()

    def turn_away_mode_off(self):
        """Turn away mode off."""
        raise NotImplementedError()