Marc Mueller 29ce17abf4
Update eq3btsmart to 2.1.0 (#146335)
* Update eq3btsmart to 2.1.0

* Update import names

* Update register callbacks

* Updated data model

* Update Thermostat set value methods

* Update Thermostat init

* Thermostat status and device_data are always given

* Minor compatibility fixes

---------

Co-authored-by: Lennard Beers <l.beers@outlook.de>
2025-06-15 10:17:01 +02:00

232 lines
8.1 KiB
Python

"""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)