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>
This commit is contained in:
Marc Mueller 2025-06-15 10:17:01 +02:00 committed by GitHub
parent c988d1ce36
commit 29ce17abf4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 124 additions and 119 deletions

View File

@ -6,7 +6,6 @@ from typing import TYPE_CHECKING
from eq3btsmart import Thermostat from eq3btsmart import Thermostat
from eq3btsmart.exceptions import Eq3Exception from eq3btsmart.exceptions import Eq3Exception
from eq3btsmart.thermostat_config import ThermostatConfig
from homeassistant.components import bluetooth from homeassistant.components import bluetooth
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
@ -53,12 +52,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: Eq3ConfigEntry) -> bool:
f"[{eq3_config.mac_address}] Device could not be found" f"[{eq3_config.mac_address}] Device could not be found"
) )
thermostat = Thermostat( thermostat = Thermostat(mac_address=device) # type: ignore[arg-type]
thermostat_config=ThermostatConfig(
mac_address=mac_address,
),
ble_device=device,
)
entry.runtime_data = Eq3ConfigEntryData( entry.runtime_data = Eq3ConfigEntryData(
eq3_config=eq3_config, thermostat=thermostat eq3_config=eq3_config, thermostat=thermostat

View File

@ -2,7 +2,6 @@
from collections.abc import Callable from collections.abc import Callable
from dataclasses import dataclass from dataclasses import dataclass
from typing import TYPE_CHECKING
from eq3btsmart.models import Status from eq3btsmart.models import Status
@ -80,7 +79,4 @@ class Eq3BinarySensorEntity(Eq3Entity, BinarySensorEntity):
def is_on(self) -> bool: def is_on(self) -> bool:
"""Return the state of the binary sensor.""" """Return the state of the binary sensor."""
if TYPE_CHECKING:
assert self._thermostat.status is not None
return self.entity_description.value_func(self._thermostat.status) return self.entity_description.value_func(self._thermostat.status)

View File

@ -1,9 +1,16 @@
"""Platform for eQ-3 climate entities.""" """Platform for eQ-3 climate entities."""
from datetime import timedelta
import logging import logging
from typing import Any from typing import Any
from eq3btsmart.const import EQ3BT_MAX_TEMP, EQ3BT_OFF_TEMP, Eq3Preset, OperationMode from eq3btsmart.const import (
EQ3_DEFAULT_AWAY_TEMP,
EQ3_MAX_TEMP,
EQ3_OFF_TEMP,
Eq3OperationMode,
Eq3Preset,
)
from eq3btsmart.exceptions import Eq3Exception from eq3btsmart.exceptions import Eq3Exception
from homeassistant.components.climate import ( from homeassistant.components.climate import (
@ -20,9 +27,11 @@ from homeassistant.exceptions import ServiceValidationError
from homeassistant.helpers import device_registry as dr from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.device_registry import CONNECTION_BLUETOOTH from homeassistant.helpers.device_registry import CONNECTION_BLUETOOTH
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
import homeassistant.util.dt as dt_util
from . import Eq3ConfigEntry from . import Eq3ConfigEntry
from .const import ( from .const import (
DEFAULT_AWAY_HOURS,
EQ_TO_HA_HVAC, EQ_TO_HA_HVAC,
HA_TO_EQ_HVAC, HA_TO_EQ_HVAC,
CurrentTemperatureSelector, CurrentTemperatureSelector,
@ -57,8 +66,8 @@ class Eq3Climate(Eq3Entity, ClimateEntity):
| ClimateEntityFeature.TURN_ON | ClimateEntityFeature.TURN_ON
) )
_attr_temperature_unit = UnitOfTemperature.CELSIUS _attr_temperature_unit = UnitOfTemperature.CELSIUS
_attr_min_temp = EQ3BT_OFF_TEMP _attr_min_temp = EQ3_OFF_TEMP
_attr_max_temp = EQ3BT_MAX_TEMP _attr_max_temp = EQ3_MAX_TEMP
_attr_precision = PRECISION_HALVES _attr_precision = PRECISION_HALVES
_attr_hvac_modes = list(HA_TO_EQ_HVAC.keys()) _attr_hvac_modes = list(HA_TO_EQ_HVAC.keys())
_attr_preset_modes = list(Preset) _attr_preset_modes = list(Preset)
@ -70,38 +79,21 @@ class Eq3Climate(Eq3Entity, ClimateEntity):
_target_temperature: float | None = None _target_temperature: float | None = None
@callback @callback
def _async_on_updated(self) -> None: def _async_on_status_updated(self, data: Any) -> None:
"""Handle updated data from the thermostat."""
if self._thermostat.status is not None:
self._async_on_status_updated()
if self._thermostat.device_data is not None:
self._async_on_device_updated()
super()._async_on_updated()
@callback
def _async_on_status_updated(self) -> None:
"""Handle updated status from the thermostat.""" """Handle updated status from the thermostat."""
if self._thermostat.status is None: self._target_temperature = self._thermostat.status.target_temperature
return
self._target_temperature = self._thermostat.status.target_temperature.value
self._attr_hvac_mode = EQ_TO_HA_HVAC[self._thermostat.status.operation_mode] self._attr_hvac_mode = EQ_TO_HA_HVAC[self._thermostat.status.operation_mode]
self._attr_current_temperature = self._get_current_temperature() self._attr_current_temperature = self._get_current_temperature()
self._attr_target_temperature = self._get_target_temperature() self._attr_target_temperature = self._get_target_temperature()
self._attr_preset_mode = self._get_current_preset_mode() self._attr_preset_mode = self._get_current_preset_mode()
self._attr_hvac_action = self._get_current_hvac_action() self._attr_hvac_action = self._get_current_hvac_action()
super()._async_on_status_updated(data)
@callback @callback
def _async_on_device_updated(self) -> None: def _async_on_device_updated(self, data: Any) -> None:
"""Handle updated device data from the thermostat.""" """Handle updated device data from the thermostat."""
if self._thermostat.device_data is None:
return
device_registry = dr.async_get(self.hass) device_registry = dr.async_get(self.hass)
if device := device_registry.async_get_device( if device := device_registry.async_get_device(
connections={(CONNECTION_BLUETOOTH, self._eq3_config.mac_address)}, connections={(CONNECTION_BLUETOOTH, self._eq3_config.mac_address)},
@ -109,8 +101,9 @@ class Eq3Climate(Eq3Entity, ClimateEntity):
device_registry.async_update_device( device_registry.async_update_device(
device.id, device.id,
sw_version=str(self._thermostat.device_data.firmware_version), sw_version=str(self._thermostat.device_data.firmware_version),
serial_number=self._thermostat.device_data.device_serial.value, serial_number=self._thermostat.device_data.device_serial,
) )
super()._async_on_device_updated(data)
def _get_current_temperature(self) -> float | None: def _get_current_temperature(self) -> float | None:
"""Return the current temperature.""" """Return the current temperature."""
@ -119,17 +112,11 @@ class Eq3Climate(Eq3Entity, ClimateEntity):
case CurrentTemperatureSelector.NOTHING: case CurrentTemperatureSelector.NOTHING:
return None return None
case CurrentTemperatureSelector.VALVE: case CurrentTemperatureSelector.VALVE:
if self._thermostat.status is None:
return None
return float(self._thermostat.status.valve_temperature) return float(self._thermostat.status.valve_temperature)
case CurrentTemperatureSelector.UI: case CurrentTemperatureSelector.UI:
return self._target_temperature return self._target_temperature
case CurrentTemperatureSelector.DEVICE: case CurrentTemperatureSelector.DEVICE:
if self._thermostat.status is None: return float(self._thermostat.status.target_temperature)
return None
return float(self._thermostat.status.target_temperature.value)
case CurrentTemperatureSelector.ENTITY: case CurrentTemperatureSelector.ENTITY:
state = self.hass.states.get(self._eq3_config.external_temp_sensor) state = self.hass.states.get(self._eq3_config.external_temp_sensor)
if state is not None: if state is not None:
@ -147,16 +134,12 @@ class Eq3Climate(Eq3Entity, ClimateEntity):
case TargetTemperatureSelector.TARGET: case TargetTemperatureSelector.TARGET:
return self._target_temperature return self._target_temperature
case TargetTemperatureSelector.LAST_REPORTED: case TargetTemperatureSelector.LAST_REPORTED:
if self._thermostat.status is None: return float(self._thermostat.status.target_temperature)
return None
return float(self._thermostat.status.target_temperature.value)
def _get_current_preset_mode(self) -> str: def _get_current_preset_mode(self) -> str:
"""Return the current preset mode.""" """Return the current preset mode."""
if (status := self._thermostat.status) is None: status = self._thermostat.status
return PRESET_NONE
if status.is_window_open: if status.is_window_open:
return Preset.WINDOW_OPEN return Preset.WINDOW_OPEN
if status.is_boost: if status.is_boost:
@ -165,7 +148,7 @@ class Eq3Climate(Eq3Entity, ClimateEntity):
return Preset.LOW_BATTERY return Preset.LOW_BATTERY
if status.is_away: if status.is_away:
return Preset.AWAY return Preset.AWAY
if status.operation_mode is OperationMode.ON: if status.operation_mode is Eq3OperationMode.ON:
return Preset.OPEN return Preset.OPEN
if status.presets is None: if status.presets is None:
return PRESET_NONE return PRESET_NONE
@ -179,10 +162,7 @@ class Eq3Climate(Eq3Entity, ClimateEntity):
def _get_current_hvac_action(self) -> HVACAction: def _get_current_hvac_action(self) -> HVACAction:
"""Return the current hvac action.""" """Return the current hvac action."""
if ( if self._thermostat.status.operation_mode is Eq3OperationMode.OFF:
self._thermostat.status is None
or self._thermostat.status.operation_mode is OperationMode.OFF
):
return HVACAction.OFF return HVACAction.OFF
if self._thermostat.status.valve == 0: if self._thermostat.status.valve == 0:
return HVACAction.IDLE return HVACAction.IDLE
@ -227,7 +207,7 @@ class Eq3Climate(Eq3Entity, ClimateEntity):
"""Set new target hvac mode.""" """Set new target hvac mode."""
if hvac_mode is HVACMode.OFF: if hvac_mode is HVACMode.OFF:
await self.async_set_temperature(temperature=EQ3BT_OFF_TEMP) await self.async_set_temperature(temperature=EQ3_OFF_TEMP)
try: try:
await self._thermostat.async_set_mode(HA_TO_EQ_HVAC[hvac_mode]) await self._thermostat.async_set_mode(HA_TO_EQ_HVAC[hvac_mode])
@ -241,10 +221,11 @@ class Eq3Climate(Eq3Entity, ClimateEntity):
case Preset.BOOST: case Preset.BOOST:
await self._thermostat.async_set_boost(True) await self._thermostat.async_set_boost(True)
case Preset.AWAY: case Preset.AWAY:
await self._thermostat.async_set_away(True) 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: case Preset.ECO:
await self._thermostat.async_set_preset(Eq3Preset.ECO) await self._thermostat.async_set_preset(Eq3Preset.ECO)
case Preset.COMFORT: case Preset.COMFORT:
await self._thermostat.async_set_preset(Eq3Preset.COMFORT) await self._thermostat.async_set_preset(Eq3Preset.COMFORT)
case Preset.OPEN: case Preset.OPEN:
await self._thermostat.async_set_mode(OperationMode.ON) await self._thermostat.async_set_mode(Eq3OperationMode.ON)

View File

@ -2,7 +2,7 @@
from enum import Enum from enum import Enum
from eq3btsmart.const import OperationMode from eq3btsmart.const import Eq3OperationMode
from homeassistant.components.climate import ( from homeassistant.components.climate import (
PRESET_AWAY, PRESET_AWAY,
@ -34,17 +34,17 @@ ENTITY_KEY_AWAY_UNTIL = "away_until"
GET_DEVICE_TIMEOUT = 5 # seconds GET_DEVICE_TIMEOUT = 5 # seconds
EQ_TO_HA_HVAC: dict[OperationMode, HVACMode] = { EQ_TO_HA_HVAC: dict[Eq3OperationMode, HVACMode] = {
OperationMode.OFF: HVACMode.OFF, Eq3OperationMode.OFF: HVACMode.OFF,
OperationMode.ON: HVACMode.HEAT, Eq3OperationMode.ON: HVACMode.HEAT,
OperationMode.AUTO: HVACMode.AUTO, Eq3OperationMode.AUTO: HVACMode.AUTO,
OperationMode.MANUAL: HVACMode.HEAT, Eq3OperationMode.MANUAL: HVACMode.HEAT,
} }
HA_TO_EQ_HVAC = { HA_TO_EQ_HVAC = {
HVACMode.OFF: OperationMode.OFF, HVACMode.OFF: Eq3OperationMode.OFF,
HVACMode.AUTO: OperationMode.AUTO, HVACMode.AUTO: Eq3OperationMode.AUTO,
HVACMode.HEAT: OperationMode.MANUAL, HVACMode.HEAT: Eq3OperationMode.MANUAL,
} }
@ -81,6 +81,7 @@ class TargetTemperatureSelector(str, Enum):
DEFAULT_CURRENT_TEMP_SELECTOR = CurrentTemperatureSelector.DEVICE DEFAULT_CURRENT_TEMP_SELECTOR = CurrentTemperatureSelector.DEVICE
DEFAULT_TARGET_TEMP_SELECTOR = TargetTemperatureSelector.TARGET DEFAULT_TARGET_TEMP_SELECTOR = TargetTemperatureSelector.TARGET
DEFAULT_SCAN_INTERVAL = 10 # seconds DEFAULT_SCAN_INTERVAL = 10 # seconds
DEFAULT_AWAY_HOURS = 30 * 24
SIGNAL_THERMOSTAT_DISCONNECTED = f"{DOMAIN}.thermostat_disconnected" SIGNAL_THERMOSTAT_DISCONNECTED = f"{DOMAIN}.thermostat_disconnected"
SIGNAL_THERMOSTAT_CONNECTED = f"{DOMAIN}.thermostat_connected" SIGNAL_THERMOSTAT_CONNECTED = f"{DOMAIN}.thermostat_connected"

View File

@ -1,5 +1,10 @@
"""Base class for all eQ-3 entities.""" """Base class for all eQ-3 entities."""
from typing import Any
from eq3btsmart import Eq3Exception
from eq3btsmart.const import Eq3Event
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.helpers.device_registry import ( from homeassistant.helpers.device_registry import (
CONNECTION_BLUETOOTH, CONNECTION_BLUETOOTH,
@ -45,7 +50,15 @@ class Eq3Entity(Entity):
async def async_added_to_hass(self) -> None: async def async_added_to_hass(self) -> None:
"""Run when entity about to be added to hass.""" """Run when entity about to be added to hass."""
self._thermostat.register_update_callback(self._async_on_updated) self._thermostat.register_callback(
Eq3Event.DEVICE_DATA_RECEIVED, self._async_on_device_updated
)
self._thermostat.register_callback(
Eq3Event.STATUS_RECEIVED, self._async_on_status_updated
)
self._thermostat.register_callback(
Eq3Event.SCHEDULE_RECEIVED, self._async_on_status_updated
)
self.async_on_remove( self.async_on_remove(
async_dispatcher_connect( async_dispatcher_connect(
@ -65,10 +78,25 @@ class Eq3Entity(Entity):
async def async_will_remove_from_hass(self) -> None: async def async_will_remove_from_hass(self) -> None:
"""Run when entity will be removed from hass.""" """Run when entity will be removed from hass."""
self._thermostat.unregister_update_callback(self._async_on_updated) self._thermostat.unregister_callback(
Eq3Event.DEVICE_DATA_RECEIVED, self._async_on_device_updated
)
self._thermostat.unregister_callback(
Eq3Event.STATUS_RECEIVED, self._async_on_status_updated
)
self._thermostat.unregister_callback(
Eq3Event.SCHEDULE_RECEIVED, self._async_on_status_updated
)
def _async_on_updated(self) -> None: @callback
"""Handle updated data from the thermostat.""" def _async_on_status_updated(self, data: Any) -> None:
"""Handle updated status from the thermostat."""
self.async_write_ha_state()
@callback
def _async_on_device_updated(self, data: Any) -> None:
"""Handle updated device data from the thermostat."""
self.async_write_ha_state() self.async_write_ha_state()
@ -90,4 +118,9 @@ class Eq3Entity(Entity):
def available(self) -> bool: def available(self) -> bool:
"""Whether the entity is available.""" """Whether the entity is available."""
return self._thermostat.status is not None and self._attr_available try:
_ = self._thermostat.status
except Eq3Exception:
return False
return self._attr_available

View File

@ -22,5 +22,5 @@
"integration_type": "device", "integration_type": "device",
"iot_class": "local_polling", "iot_class": "local_polling",
"loggers": ["eq3btsmart"], "loggers": ["eq3btsmart"],
"requirements": ["eq3btsmart==1.4.1", "bleak-esphome==2.16.0"] "requirements": ["eq3btsmart==2.1.0", "bleak-esphome==2.16.0"]
} }

View File

@ -1,17 +1,12 @@
"""Platform for eq3 number entities.""" """Platform for eq3 number entities."""
from collections.abc import Awaitable, Callable from collections.abc import Callable, Coroutine
from dataclasses import dataclass from dataclasses import dataclass
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from eq3btsmart import Thermostat from eq3btsmart import Thermostat
from eq3btsmart.const import ( from eq3btsmart.const import EQ3_MAX_OFFSET, EQ3_MAX_TEMP, EQ3_MIN_OFFSET, EQ3_MIN_TEMP
EQ3BT_MAX_OFFSET, from eq3btsmart.models import Presets, Status
EQ3BT_MAX_TEMP,
EQ3BT_MIN_OFFSET,
EQ3BT_MIN_TEMP,
)
from eq3btsmart.models import Presets
from homeassistant.components.number import ( from homeassistant.components.number import (
NumberDeviceClass, NumberDeviceClass,
@ -42,7 +37,7 @@ class Eq3NumberEntityDescription(NumberEntityDescription):
value_func: Callable[[Presets], float] value_func: Callable[[Presets], float]
value_set_func: Callable[ value_set_func: Callable[
[Thermostat], [Thermostat],
Callable[[float], Awaitable[None]], Callable[[float], Coroutine[None, None, Status]],
] ]
mode: NumberMode = NumberMode.BOX mode: NumberMode = NumberMode.BOX
entity_category: EntityCategory | None = EntityCategory.CONFIG entity_category: EntityCategory | None = EntityCategory.CONFIG
@ -51,44 +46,44 @@ class Eq3NumberEntityDescription(NumberEntityDescription):
NUMBER_ENTITY_DESCRIPTIONS = [ NUMBER_ENTITY_DESCRIPTIONS = [
Eq3NumberEntityDescription( Eq3NumberEntityDescription(
key=ENTITY_KEY_COMFORT, key=ENTITY_KEY_COMFORT,
value_func=lambda presets: presets.comfort_temperature.value, value_func=lambda presets: presets.comfort_temperature,
value_set_func=lambda thermostat: thermostat.async_configure_comfort_temperature, value_set_func=lambda thermostat: thermostat.async_configure_comfort_temperature,
translation_key=ENTITY_KEY_COMFORT, translation_key=ENTITY_KEY_COMFORT,
native_min_value=EQ3BT_MIN_TEMP, native_min_value=EQ3_MIN_TEMP,
native_max_value=EQ3BT_MAX_TEMP, native_max_value=EQ3_MAX_TEMP,
native_step=EQ3BT_STEP, native_step=EQ3BT_STEP,
native_unit_of_measurement=UnitOfTemperature.CELSIUS, native_unit_of_measurement=UnitOfTemperature.CELSIUS,
device_class=NumberDeviceClass.TEMPERATURE, device_class=NumberDeviceClass.TEMPERATURE,
), ),
Eq3NumberEntityDescription( Eq3NumberEntityDescription(
key=ENTITY_KEY_ECO, key=ENTITY_KEY_ECO,
value_func=lambda presets: presets.eco_temperature.value, value_func=lambda presets: presets.eco_temperature,
value_set_func=lambda thermostat: thermostat.async_configure_eco_temperature, value_set_func=lambda thermostat: thermostat.async_configure_eco_temperature,
translation_key=ENTITY_KEY_ECO, translation_key=ENTITY_KEY_ECO,
native_min_value=EQ3BT_MIN_TEMP, native_min_value=EQ3_MIN_TEMP,
native_max_value=EQ3BT_MAX_TEMP, native_max_value=EQ3_MAX_TEMP,
native_step=EQ3BT_STEP, native_step=EQ3BT_STEP,
native_unit_of_measurement=UnitOfTemperature.CELSIUS, native_unit_of_measurement=UnitOfTemperature.CELSIUS,
device_class=NumberDeviceClass.TEMPERATURE, device_class=NumberDeviceClass.TEMPERATURE,
), ),
Eq3NumberEntityDescription( Eq3NumberEntityDescription(
key=ENTITY_KEY_WINDOW_OPEN_TEMPERATURE, key=ENTITY_KEY_WINDOW_OPEN_TEMPERATURE,
value_func=lambda presets: presets.window_open_temperature.value, value_func=lambda presets: presets.window_open_temperature,
value_set_func=lambda thermostat: thermostat.async_configure_window_open_temperature, value_set_func=lambda thermostat: thermostat.async_configure_window_open_temperature,
translation_key=ENTITY_KEY_WINDOW_OPEN_TEMPERATURE, translation_key=ENTITY_KEY_WINDOW_OPEN_TEMPERATURE,
native_min_value=EQ3BT_MIN_TEMP, native_min_value=EQ3_MIN_TEMP,
native_max_value=EQ3BT_MAX_TEMP, native_max_value=EQ3_MAX_TEMP,
native_step=EQ3BT_STEP, native_step=EQ3BT_STEP,
native_unit_of_measurement=UnitOfTemperature.CELSIUS, native_unit_of_measurement=UnitOfTemperature.CELSIUS,
device_class=NumberDeviceClass.TEMPERATURE, device_class=NumberDeviceClass.TEMPERATURE,
), ),
Eq3NumberEntityDescription( Eq3NumberEntityDescription(
key=ENTITY_KEY_OFFSET, key=ENTITY_KEY_OFFSET,
value_func=lambda presets: presets.offset_temperature.value, value_func=lambda presets: presets.offset_temperature,
value_set_func=lambda thermostat: thermostat.async_configure_temperature_offset, value_set_func=lambda thermostat: thermostat.async_configure_temperature_offset,
translation_key=ENTITY_KEY_OFFSET, translation_key=ENTITY_KEY_OFFSET,
native_min_value=EQ3BT_MIN_OFFSET, native_min_value=EQ3_MIN_OFFSET,
native_max_value=EQ3BT_MAX_OFFSET, native_max_value=EQ3_MAX_OFFSET,
native_step=EQ3BT_STEP, native_step=EQ3BT_STEP,
native_unit_of_measurement=UnitOfTemperature.CELSIUS, native_unit_of_measurement=UnitOfTemperature.CELSIUS,
device_class=NumberDeviceClass.TEMPERATURE, device_class=NumberDeviceClass.TEMPERATURE,
@ -96,7 +91,7 @@ NUMBER_ENTITY_DESCRIPTIONS = [
Eq3NumberEntityDescription( Eq3NumberEntityDescription(
key=ENTITY_KEY_WINDOW_OPEN_TIMEOUT, key=ENTITY_KEY_WINDOW_OPEN_TIMEOUT,
value_set_func=lambda thermostat: thermostat.async_configure_window_open_duration, value_set_func=lambda thermostat: thermostat.async_configure_window_open_duration,
value_func=lambda presets: presets.window_open_time.value.total_seconds() / 60, value_func=lambda presets: presets.window_open_time.total_seconds() / 60,
translation_key=ENTITY_KEY_WINDOW_OPEN_TIMEOUT, translation_key=ENTITY_KEY_WINDOW_OPEN_TIMEOUT,
native_min_value=0, native_min_value=0,
native_max_value=60, native_max_value=60,
@ -137,7 +132,6 @@ class Eq3NumberEntity(Eq3Entity, NumberEntity):
"""Return the state of the entity.""" """Return the state of the entity."""
if TYPE_CHECKING: if TYPE_CHECKING:
assert self._thermostat.status is not None
assert self._thermostat.status.presets is not None assert self._thermostat.status.presets is not None
return self.entity_description.value_func(self._thermostat.status.presets) return self.entity_description.value_func(self._thermostat.status.presets)
@ -152,7 +146,7 @@ class Eq3NumberEntity(Eq3Entity, NumberEntity):
"""Return whether the entity is available.""" """Return whether the entity is available."""
return ( return (
self._thermostat.status is not None super().available
and self._thermostat.status.presets is not None and self._thermostat.status.presets is not None
and self._attr_available and self._attr_available
) )

View File

@ -1,12 +1,12 @@
"""Voluptuous schemas for eq3btsmart.""" """Voluptuous schemas for eq3btsmart."""
from eq3btsmart.const import EQ3BT_MAX_TEMP, EQ3BT_MIN_TEMP from eq3btsmart.const import EQ3_MAX_TEMP, EQ3_MIN_TEMP
import voluptuous as vol import voluptuous as vol
from homeassistant.const import CONF_MAC from homeassistant.const import CONF_MAC
from homeassistant.helpers import config_validation as cv from homeassistant.helpers import config_validation as cv
SCHEMA_TEMPERATURE = vol.Range(min=EQ3BT_MIN_TEMP, max=EQ3BT_MAX_TEMP) SCHEMA_TEMPERATURE = vol.Range(min=EQ3_MIN_TEMP, max=EQ3_MAX_TEMP)
SCHEMA_DEVICE = vol.Schema({vol.Required(CONF_MAC): cv.string}) SCHEMA_DEVICE = vol.Schema({vol.Required(CONF_MAC): cv.string})
SCHEMA_MAC = vol.Schema( SCHEMA_MAC = vol.Schema(
{ {

View File

@ -3,7 +3,6 @@
from collections.abc import Callable from collections.abc import Callable
from dataclasses import dataclass from dataclasses import dataclass
from datetime import datetime from datetime import datetime
from typing import TYPE_CHECKING
from eq3btsmart.models import Status from eq3btsmart.models import Status
@ -40,9 +39,7 @@ SENSOR_ENTITY_DESCRIPTIONS = [
Eq3SensorEntityDescription( Eq3SensorEntityDescription(
key=ENTITY_KEY_AWAY_UNTIL, key=ENTITY_KEY_AWAY_UNTIL,
translation_key=ENTITY_KEY_AWAY_UNTIL, translation_key=ENTITY_KEY_AWAY_UNTIL,
value_func=lambda status: ( value_func=lambda status: (status.away_until if status.away_until else None),
status.away_until.value if status.away_until else None
),
device_class=SensorDeviceClass.DATE, device_class=SensorDeviceClass.DATE,
), ),
] ]
@ -78,7 +75,4 @@ class Eq3SensorEntity(Eq3Entity, SensorEntity):
def native_value(self) -> int | datetime | None: def native_value(self) -> int | datetime | None:
"""Return the value reported by the sensor.""" """Return the value reported by the sensor."""
if TYPE_CHECKING:
assert self._thermostat.status is not None
return self.entity_description.value_func(self._thermostat.status) return self.entity_description.value_func(self._thermostat.status)

View File

@ -1,26 +1,45 @@
"""Platform for eq3 switch entities.""" """Platform for eq3 switch entities."""
from collections.abc import Awaitable, Callable from collections.abc import Callable, Coroutine
from dataclasses import dataclass from dataclasses import dataclass
from typing import TYPE_CHECKING, Any from datetime import timedelta
from functools import partial
from typing import Any
from eq3btsmart import Thermostat from eq3btsmart import Thermostat
from eq3btsmart.const import EQ3_DEFAULT_AWAY_TEMP, Eq3OperationMode
from eq3btsmart.models import Status from eq3btsmart.models import Status
from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
import homeassistant.util.dt as dt_util
from . import Eq3ConfigEntry from . import Eq3ConfigEntry
from .const import ENTITY_KEY_AWAY, ENTITY_KEY_BOOST, ENTITY_KEY_LOCK from .const import (
DEFAULT_AWAY_HOURS,
ENTITY_KEY_AWAY,
ENTITY_KEY_BOOST,
ENTITY_KEY_LOCK,
)
from .entity import Eq3Entity from .entity import Eq3Entity
async def async_set_away(thermostat: Thermostat, enable: bool) -> Status:
"""Backport old async_set_away behavior."""
if not enable:
return await thermostat.async_set_mode(Eq3OperationMode.AUTO)
away_until = dt_util.now() + timedelta(hours=DEFAULT_AWAY_HOURS)
return await thermostat.async_set_away(away_until, EQ3_DEFAULT_AWAY_TEMP)
@dataclass(frozen=True, kw_only=True) @dataclass(frozen=True, kw_only=True)
class Eq3SwitchEntityDescription(SwitchEntityDescription): class Eq3SwitchEntityDescription(SwitchEntityDescription):
"""Entity description for eq3 switch entities.""" """Entity description for eq3 switch entities."""
toggle_func: Callable[[Thermostat], Callable[[bool], Awaitable[None]]] toggle_func: Callable[[Thermostat], Callable[[bool], Coroutine[None, None, Status]]]
value_func: Callable[[Status], bool] value_func: Callable[[Status], bool]
@ -40,7 +59,7 @@ SWITCH_ENTITY_DESCRIPTIONS = [
Eq3SwitchEntityDescription( Eq3SwitchEntityDescription(
key=ENTITY_KEY_AWAY, key=ENTITY_KEY_AWAY,
translation_key=ENTITY_KEY_AWAY, translation_key=ENTITY_KEY_AWAY,
toggle_func=lambda thermostat: thermostat.async_set_away, toggle_func=lambda thermostat: partial(async_set_away, thermostat),
value_func=lambda status: status.is_away, value_func=lambda status: status.is_away,
), ),
] ]
@ -88,7 +107,4 @@ class Eq3SwitchEntity(Eq3Entity, SwitchEntity):
def is_on(self) -> bool: def is_on(self) -> bool:
"""Return the state of the switch.""" """Return the state of the switch."""
if TYPE_CHECKING:
assert self._thermostat.status is not None
return self.entity_description.value_func(self._thermostat.status) return self.entity_description.value_func(self._thermostat.status)

2
requirements_all.txt generated
View File

@ -893,7 +893,7 @@ epion==0.0.3
epson-projector==0.5.1 epson-projector==0.5.1
# homeassistant.components.eq3btsmart # homeassistant.components.eq3btsmart
eq3btsmart==1.4.1 eq3btsmart==2.1.0
# homeassistant.components.esphome # homeassistant.components.esphome
esphome-dashboard-api==1.3.0 esphome-dashboard-api==1.3.0

View File

@ -772,7 +772,7 @@ epion==0.0.3
epson-projector==0.5.1 epson-projector==0.5.1
# homeassistant.components.eq3btsmart # homeassistant.components.eq3btsmart
eq3btsmart==1.4.1 eq3btsmart==2.1.0
# homeassistant.components.esphome # homeassistant.components.esphome
esphome-dashboard-api==1.3.0 esphome-dashboard-api==1.3.0

View File

@ -331,10 +331,6 @@ PYTHON_VERSION_CHECK_EXCEPTIONS: dict[str, dict[str, set[str]]] = {
# https://github.com/hbldh/bleak/pull/1718 (not yet released) # https://github.com/hbldh/bleak/pull/1718 (not yet released)
"homeassistant": {"bleak"} "homeassistant": {"bleak"}
}, },
"eq3btsmart": {
# https://github.com/EuleMitKeule/eq3btsmart/releases/tag/2.0.0
"homeassistant": {"eq3btsmart"}
},
"python_script": { "python_script": {
# Security audits are needed for each Python version # Security audits are needed for each Python version
"homeassistant": {"restrictedpython"} "homeassistant": {"restrictedpython"}