Compare commits

..

4 Commits
6.0.2 ... 6.0.4

Author SHA1 Message Date
Jean-Marc Collin
9269240fe3 Release 2024-03-25 07:43:09 +00:00
Jean-Marc Collin
91ba2387b2 Persistence of boiler srv attribute 2024-03-25 07:25:42 +00:00
Jean-Marc Collin
162efb4709 [#425] - Boiler management entities are generated independently of this option selection (#426)
Co-authored-by: Jean-Marc Collin <jean-marc.collin-extern@renault.com>
2024-03-25 08:00:08 +01:00
Paulo Ferreira de Castro
6a97622226 Expose the keep_alive_sec attribute in HASS Developer Tools - States (#381)
* Typing: Make BaseThermostat generic on the UnderlyingEntity type

* Typing: Change the type of IntervalCaller._interval_sec from int to float

This makes the IntervalCaller class more reusable.

* Keep-alive: Expose UnderlyingSwitch.keep_alive_sec as a HASS Dev Tools attribute

Also improve a keep-alive log message.
2024-03-23 11:49:09 +01:00
12 changed files with 58 additions and 24 deletions

View File

@@ -7,7 +7,7 @@ import logging
from datetime import timedelta, datetime from datetime import timedelta, datetime
from types import MappingProxyType from types import MappingProxyType
from typing import Any from typing import Any, TypeVar, Generic
from homeassistant.util import dt as dt_util from homeassistant.util import dt as dt_util
from homeassistant.core import ( from homeassistant.core import (
@@ -140,6 +140,7 @@ from .ema import ExponentialMovingAverage
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
ConfigData = MappingProxyType[str, Any] ConfigData = MappingProxyType[str, Any]
T = TypeVar("T", bound=UnderlyingEntity)
def get_tz(hass: HomeAssistant): def get_tz(hass: HomeAssistant):
@@ -148,7 +149,7 @@ def get_tz(hass: HomeAssistant):
return dt_util.get_time_zone(hass.config.time_zone) return dt_util.get_time_zone(hass.config.time_zone)
class BaseThermostat(ClimateEntity, RestoreEntity): class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
"""Representation of a base class for all Versatile Thermostat device.""" """Representation of a base class for all Versatile Thermostat device."""
_entity_component_unrecorded_attributes = ( _entity_component_unrecorded_attributes = (
@@ -278,7 +279,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
self._last_change_time = None self._last_change_time = None
self._underlyings: list[UnderlyingEntity] = [] self._underlyings: list[T] = []
self._ema_temp = None self._ema_temp = None
self._ema_algo = None self._ema_algo = None

View File

@@ -39,6 +39,7 @@ from .const import (
CONF_USE_WINDOW_FEATURE, CONF_USE_WINDOW_FEATURE,
CONF_THERMOSTAT_TYPE, CONF_THERMOSTAT_TYPE,
CONF_THERMOSTAT_CENTRAL_CONFIG, CONF_THERMOSTAT_CENTRAL_CONFIG,
CONF_USE_CENTRAL_BOILER_FEATURE,
CONF_CENTRAL_BOILER_ACTIVATION_SRV, CONF_CENTRAL_BOILER_ACTIVATION_SRV,
CONF_CENTRAL_BOILER_DEACTIVATION_SRV, CONF_CENTRAL_BOILER_DEACTIVATION_SRV,
overrides, overrides,
@@ -63,10 +64,13 @@ async def async_setup_entry(
name = entry.data.get(CONF_NAME) name = entry.data.get(CONF_NAME)
vt_type = entry.data.get(CONF_THERMOSTAT_TYPE) vt_type = entry.data.get(CONF_THERMOSTAT_TYPE)
entities = None
if vt_type == CONF_THERMOSTAT_CENTRAL_CONFIG: if vt_type == CONF_THERMOSTAT_CENTRAL_CONFIG:
entities = [ if entry.data.get(CONF_USE_CENTRAL_BOILER_FEATURE):
CentralBoilerBinarySensor(hass, unique_id, name, entry.data), entities = [
] CentralBoilerBinarySensor(hass, unique_id, name, entry.data),
]
else: else:
entities = [ entities = [
SecurityBinarySensor(hass, unique_id, name, entry.data), SecurityBinarySensor(hass, unique_id, name, entry.data),
@@ -81,7 +85,8 @@ async def async_setup_entry(
if entry.data.get(CONF_USE_POWER_FEATURE): if entry.data.get(CONF_USE_POWER_FEATURE):
entities.append(OverpoweringBinarySensor(hass, unique_id, name, entry.data)) entities.append(OverpoweringBinarySensor(hass, unique_id, name, entry.data))
async_add_entities(entities, True) if entities:
async_add_entities(entities, True)
class SecurityBinarySensor(VersatileThermostatBaseEntity, BinarySensorEntity): class SecurityBinarySensor(VersatileThermostatBaseEntity, BinarySensorEntity):

View File

@@ -242,6 +242,14 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
and infos.get(CONF_PRESENCE_SENSOR, None) is None and infos.get(CONF_PRESENCE_SENSOR, None) is None
): ):
return False return False
if self._infos[CONF_USE_CENTRAL_BOILER_FEATURE] and (
not self._infos.get(CONF_CENTRAL_BOILER_ACTIVATION_SRV, False)
or len(self._infos.get(CONF_CENTRAL_BOILER_ACTIVATION_SRV)) <= 0
or not self._infos.get(CONF_CENTRAL_BOILER_DEACTIVATION_SRV, False)
or len(self._infos.get(CONF_CENTRAL_BOILER_DEACTIVATION_SRV)) <= 0
):
return False
else: else:
if ( if (
infos.get(CONF_NAME) is None infos.get(CONF_NAME) is None
@@ -857,6 +865,9 @@ class VersatileThermostatOptionsFlowHandler(
if not self._infos[CONF_USE_PRESENCE_FEATURE]: if not self._infos[CONF_USE_PRESENCE_FEATURE]:
self._infos[CONF_USE_PRESENCE_CENTRAL_CONFIG] = False self._infos[CONF_USE_PRESENCE_CENTRAL_CONFIG] = False
self._infos[CONF_PRESENCE_SENSOR] = None self._infos[CONF_PRESENCE_SENSOR] = None
if not self._infos[CONF_USE_CENTRAL_BOILER_FEATURE]:
self._infos[CONF_CENTRAL_BOILER_ACTIVATION_SRV] = None
self._infos[CONF_CENTRAL_BOILER_DEACTIVATION_SRV] = None
_LOGGER.info( _LOGGER.info(
"Recreating entry %s due to configuration change. New config is now: %s", "Recreating entry %s due to configuration change. New config is now: %s",

View File

@@ -24,11 +24,16 @@ class IntervalCaller:
Convenience wrapper around Home Assistant's `async_track_time_interval` function. Convenience wrapper around Home Assistant's `async_track_time_interval` function.
""" """
def __init__(self, hass: HomeAssistant, interval_sec: int) -> None: def __init__(self, hass: HomeAssistant, interval_sec: float) -> None:
self._hass = hass self._hass = hass
self._interval_sec = interval_sec self._interval_sec = interval_sec
self._remove_handle: CALLBACK_TYPE | None = None self._remove_handle: CALLBACK_TYPE | None = None
@property
def interval_sec(self) -> float:
"""Return the calling interval in seconds."""
return self._interval_sec
def cancel(self): def cancel(self):
"""Cancel the regular calls to the action function.""" """Cancel the regular calls to the action function."""
if self._remove_handle: if self._remove_handle:
@@ -43,7 +48,11 @@ class IntervalCaller:
async def callback(_time: datetime): async def callback(_time: datetime):
try: try:
_LOGGER.debug("Calling keep-alive action") _LOGGER.debug(
"Calling keep-alive action '%s' (%ss interval)",
action.__name__,
self._interval_sec,
)
await action() await action()
except Exception as e: # pylint: disable=broad-exception-caught except Exception as e: # pylint: disable=broad-exception-caught
_LOGGER.error(e) _LOGGER.error(e)

View File

@@ -14,6 +14,6 @@
"quality_scale": "silver", "quality_scale": "silver",
"requirements": [], "requirements": [],
"ssdp": [], "ssdp": [],
"version": "6.0.2", "version": "6.0.4",
"zeroconf": [] "zeroconf": []
} }

View File

@@ -51,6 +51,7 @@ from .const import (
CONF_USE_PRESETS_CENTRAL_CONFIG, CONF_USE_PRESETS_CENTRAL_CONFIG,
CONF_USE_PRESENCE_CENTRAL_CONFIG, CONF_USE_PRESENCE_CENTRAL_CONFIG,
CONF_USE_PRESENCE_FEATURE, CONF_USE_PRESENCE_FEATURE,
CONF_USE_CENTRAL_BOILER_FEATURE,
overrides, overrides,
) )
@@ -153,9 +154,10 @@ async def async_setup_entry(
# For central config only # For central config only
else: else:
entities.append( if entry.data.get(CONF_USE_CENTRAL_BOILER_FEATURE):
ActivateBoilerThresholdNumber(hass, unique_id, name, entry.data) entities.append(
) ActivateBoilerThresholdNumber(hass, unique_id, name, entry.data)
)
for preset in CONF_PRESETS_WITH_AC_VALUES: for preset in CONF_PRESETS_WITH_AC_VALUES:
_LOGGER.debug( _LOGGER.debug(
"%s - configuring Number central, AC, non AWAY for preset %s", "%s - configuring Number central, AC, non AWAY for preset %s",
@@ -178,7 +180,8 @@ async def async_setup_entry(
) )
) )
async_add_entities(entities, True) if len(entities) > 0:
async_add_entities(entities, True)
class ActivateBoilerThresholdNumber( class ActivateBoilerThresholdNumber(

View File

@@ -79,7 +79,6 @@ async def async_setup_entry(
entities = [ entities = [
NbActiveDeviceForBoilerSensor(hass, unique_id, name, entry.data) NbActiveDeviceForBoilerSensor(hass, unique_id, name, entry.data)
] ]
async_add_entities(entities, True)
else: else:
entities = [ entities = [
LastTemperatureSensor(hass, unique_id, name, entry.data), LastTemperatureSensor(hass, unique_id, name, entry.data),
@@ -108,6 +107,7 @@ async def async_setup_entry(
RegulatedTemperatureSensor(hass, unique_id, name, entry.data) RegulatedTemperatureSensor(hass, unique_id, name, entry.data)
) )
if entities:
async_add_entities(entities, True) async_add_entities(entities, True)

View File

@@ -58,7 +58,7 @@ from .underlyings import UnderlyingClimate
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
class ThermostatOverClimate(BaseThermostat): class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
"""Representation of a base class for a Versatile Thermostat over a climate""" """Representation of a base class for a Versatile Thermostat over a climate"""
_auto_regulation_mode: str | None = None _auto_regulation_mode: str | None = None

View File

@@ -27,7 +27,7 @@ from .prop_algorithm import PropAlgorithm
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
class ThermostatOverSwitch(BaseThermostat): class ThermostatOverSwitch(BaseThermostat[UnderlyingSwitch]):
"""Representation of a base class for a Versatile Thermostat over a switch.""" """Representation of a base class for a Versatile Thermostat over a switch."""
_entity_component_unrecorded_attributes = ( _entity_component_unrecorded_attributes = (
@@ -136,11 +136,11 @@ class ThermostatOverSwitch(BaseThermostat):
"""Custom attributes""" """Custom attributes"""
super().update_custom_attributes() super().update_custom_attributes()
under0: UnderlyingSwitch = self._underlyings[0]
self._attr_extra_state_attributes["is_over_switch"] = self.is_over_switch self._attr_extra_state_attributes["is_over_switch"] = self.is_over_switch
self._attr_extra_state_attributes["is_inversed"] = self.is_inversed self._attr_extra_state_attributes["is_inversed"] = self.is_inversed
self._attr_extra_state_attributes["underlying_switch_0"] = self._underlyings[ self._attr_extra_state_attributes["keep_alive_sec"] = under0.keep_alive_sec
0 self._attr_extra_state_attributes["underlying_switch_0"] = under0.entity_id
].entity_id
self._attr_extra_state_attributes["underlying_switch_1"] = ( self._attr_extra_state_attributes["underlying_switch_1"] = (
self._underlyings[1].entity_id if len(self._underlyings) > 1 else None self._underlyings[1].entity_id if len(self._underlyings) > 1 else None
) )

View File

@@ -31,7 +31,7 @@ from .underlyings import UnderlyingValve
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
class ThermostatOverValve(BaseThermostat): # pylint: disable=abstract-method class ThermostatOverValve(BaseThermostat[UnderlyingValve]): # pylint: disable=abstract-method
"""Representation of a class for a Versatile Thermostat over a Valve""" """Representation of a class for a Versatile Thermostat over a Valve"""
_entity_component_unrecorded_attributes = ( _entity_component_unrecorded_attributes = (

View File

@@ -188,7 +188,7 @@ class UnderlyingSwitch(UnderlyingEntity):
thermostat: Any, thermostat: Any,
switch_entity_id: str, switch_entity_id: str,
initial_delay_sec: int, initial_delay_sec: int,
keep_alive_sec: int, keep_alive_sec: float,
) -> None: ) -> None:
"""Initialize the underlying switch""" """Initialize the underlying switch"""
@@ -217,6 +217,11 @@ class UnderlyingSwitch(UnderlyingEntity):
"""Tells if the switch command should be inversed""" """Tells if the switch command should be inversed"""
return self._thermostat.is_inversed return self._thermostat.is_inversed
@property
def keep_alive_sec(self) -> float:
"""Return the switch keep-alive interval in seconds."""
return self._keep_alive.interval_sec
@overrides @overrides
def startup(self): def startup(self):
super().startup() super().startup()

View File

@@ -94,8 +94,8 @@ async def test_add_a_central_config(hass: HomeAssistant, skip_hass_states_is_sta
assert api.nb_active_device_for_boiler_entity is None assert api.nb_active_device_for_boiler_entity is None
assert api.nb_active_device_for_boiler is None assert api.nb_active_device_for_boiler is None
assert api.nb_active_device_for_boiler_threshold_entity is not None assert api.nb_active_device_for_boiler_threshold_entity is None
assert api.nb_active_device_for_boiler_threshold == 1 # the default value assert api.nb_active_device_for_boiler_threshold is None
# @pytest.mark.parametrize("expected_lingering_tasks", [True]) # @pytest.mark.parametrize("expected_lingering_tasks", [True])