"""Platform for eQ-3 climate entities.""" from datetime import timedelta import logging from typing import Any from eq3btsmart.const import ( EQ3_DEFAULT_AWAY_TEMP, EQ3_MAX_TEMP, EQ3_OFF_TEMP, Eq3OperationMode, Eq3Preset, ) from eq3btsmart.exceptions import Eq3Exception from homeassistant.components.climate import ( ATTR_HVAC_MODE, PRESET_NONE, ClimateEntity, ClimateEntityFeature, HVACAction, HVACMode, ) from homeassistant.const import ATTR_TEMPERATURE, PRECISION_HALVES, UnitOfTemperature from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ServiceValidationError from homeassistant.helpers import device_registry as dr from homeassistant.helpers.device_registry import CONNECTION_BLUETOOTH from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback import homeassistant.util.dt as dt_util from . import Eq3ConfigEntry from .const import ( DEFAULT_AWAY_HOURS, EQ_TO_HA_HVAC, HA_TO_EQ_HVAC, CurrentTemperatureSelector, Preset, TargetTemperatureSelector, ) from .entity import Eq3Entity _LOGGER = logging.getLogger(__name__) async def async_setup_entry( hass: HomeAssistant, entry: Eq3ConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Handle config entry setup.""" async_add_entities( [Eq3Climate(entry)], ) class Eq3Climate(Eq3Entity, ClimateEntity): """Climate entity to represent a eQ-3 thermostat.""" _attr_name = None _attr_supported_features = ( ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.PRESET_MODE | ClimateEntityFeature.TURN_OFF | ClimateEntityFeature.TURN_ON ) _attr_temperature_unit = UnitOfTemperature.CELSIUS _attr_min_temp = EQ3_OFF_TEMP _attr_max_temp = EQ3_MAX_TEMP _attr_precision = PRECISION_HALVES _attr_hvac_modes = list(HA_TO_EQ_HVAC.keys()) _attr_preset_modes = list(Preset) _attr_should_poll = False _attr_available = False _attr_hvac_mode: HVACMode | None = None _attr_hvac_action: HVACAction | None = None _attr_preset_mode: str | None = None _target_temperature: float | None = None @callback def _async_on_status_updated(self, data: Any) -> None: """Handle updated status from the thermostat.""" self._target_temperature = self._thermostat.status.target_temperature self._attr_hvac_mode = EQ_TO_HA_HVAC[self._thermostat.status.operation_mode] self._attr_current_temperature = self._get_current_temperature() self._attr_target_temperature = self._get_target_temperature() self._attr_preset_mode = self._get_current_preset_mode() self._attr_hvac_action = self._get_current_hvac_action() super()._async_on_status_updated(data) @callback def _async_on_device_updated(self, data: Any) -> None: """Handle updated device data from the thermostat.""" device_registry = dr.async_get(self.hass) if device := device_registry.async_get_device( connections={(CONNECTION_BLUETOOTH, self._eq3_config.mac_address)}, ): device_registry.async_update_device( device.id, sw_version=str(self._thermostat.device_data.firmware_version), serial_number=self._thermostat.device_data.device_serial, ) super()._async_on_device_updated(data) def _get_current_temperature(self) -> float | None: """Return the current temperature.""" match self._eq3_config.current_temp_selector: case CurrentTemperatureSelector.NOTHING: return None case CurrentTemperatureSelector.VALVE: return float(self._thermostat.status.valve_temperature) case CurrentTemperatureSelector.UI: return self._target_temperature case CurrentTemperatureSelector.DEVICE: return float(self._thermostat.status.target_temperature) case CurrentTemperatureSelector.ENTITY: state = self.hass.states.get(self._eq3_config.external_temp_sensor) if state is not None: try: return float(state.state) except ValueError: pass return None def _get_target_temperature(self) -> float | None: """Return the target temperature.""" match self._eq3_config.target_temp_selector: case TargetTemperatureSelector.TARGET: return self._target_temperature case TargetTemperatureSelector.LAST_REPORTED: return float(self._thermostat.status.target_temperature) def _get_current_preset_mode(self) -> str: """Return the current preset mode.""" status = self._thermostat.status if status.is_window_open: return Preset.WINDOW_OPEN if status.is_boost: return Preset.BOOST if status.is_low_battery: return Preset.LOW_BATTERY if status.is_away: return Preset.AWAY if status.operation_mode is Eq3OperationMode.ON: return Preset.OPEN if status.presets is None: return PRESET_NONE if status.target_temperature == status.presets.eco_temperature: return Preset.ECO if status.target_temperature == status.presets.comfort_temperature: return Preset.COMFORT return PRESET_NONE def _get_current_hvac_action(self) -> HVACAction: """Return the current hvac action.""" if self._thermostat.status.operation_mode is Eq3OperationMode.OFF: return HVACAction.OFF if self._thermostat.status.valve == 0: return HVACAction.IDLE return HVACAction.HEATING async def async_set_temperature(self, **kwargs: Any) -> None: """Set new target temperature.""" if ATTR_HVAC_MODE in kwargs: mode: HVACMode | None if (mode := kwargs.get(ATTR_HVAC_MODE)) is None: return if mode is not HVACMode.OFF: await self.async_set_hvac_mode(mode) else: raise ServiceValidationError( f"[{self._eq3_config.mac_address}] Can't change HVAC mode to off while changing temperature", ) temperature: float | None if (temperature := kwargs.get(ATTR_TEMPERATURE)) is None: return previous_temperature = self._target_temperature self._target_temperature = temperature self.async_write_ha_state() try: await self._thermostat.async_set_temperature(temperature) except Eq3Exception: _LOGGER.error( "[%s] Failed setting temperature", self._eq3_config.mac_address ) self._target_temperature = previous_temperature self.async_write_ha_state() except ValueError as ex: raise ServiceValidationError("Invalid temperature") from ex async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: """Set new target hvac mode.""" if hvac_mode is HVACMode.OFF: await self.async_set_temperature(temperature=EQ3_OFF_TEMP) try: await self._thermostat.async_set_mode(HA_TO_EQ_HVAC[hvac_mode]) except Eq3Exception: _LOGGER.error("[%s] Failed setting HVAC mode", self._eq3_config.mac_address) async def async_set_preset_mode(self, preset_mode: str) -> None: """Set new preset mode.""" match preset_mode: case Preset.BOOST: await self._thermostat.async_set_boost(True) case Preset.AWAY: away_until = dt_util.now() + timedelta(hours=DEFAULT_AWAY_HOURS) await self._thermostat.async_set_away(away_until, EQ3_DEFAULT_AWAY_TEMP) case Preset.ECO: await self._thermostat.async_set_preset(Eq3Preset.ECO) case Preset.COMFORT: await self._thermostat.async_set_preset(Eq3Preset.COMFORT) case Preset.OPEN: await self._thermostat.async_set_mode(Eq3OperationMode.ON)