Compare commits

..

10 Commits

Author SHA1 Message Date
Jean-Marc Collin
c512cb6f74 [#428] - Refacto start versatile_thermostat (#430)
Co-authored-by: Jean-Marc Collin <jean-marc.collin-extern@renault.com>
2024-03-26 21:06:25 +01:00
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
Jean-Marc Collin
5db7a49e75 [#339] - AUTO mode not counted for active boiler (#424)
Co-authored-by: Jean-Marc Collin <jean-marc.collin-extern@renault.com>
2024-03-23 09:07:55 +01:00
Jean-Marc Collin
d7cdf79561 [#407] - shredding if heating (#423)
Co-authored-by: Jean-Marc Collin <jean-marc.collin-extern@renault.com>
2024-03-23 08:30:17 +01:00
Jean-Marc Collin
07ac7beb7d [#419] - Turnon don't work anymore (#422)
Co-authored-by: Jean-Marc Collin <jean-marc.collin-extern@renault.com>
2024-03-23 07:21:18 +01:00
Jean-Marc Collin
7ded723c8b [#420] - Message d'erreur dans home assistant (#421)
Co-authored-by: Jean-Marc Collin <jean-marc.collin-extern@renault.com>
2024-03-23 07:17:55 +01:00
EPicLURcher
5369111f2d English translations (#417)
* Update strings.json

* Update en.json

* Update strings.json

* Update strings.json

* Update en.json
2024-03-20 23:11:50 +01:00
21 changed files with 632 additions and 301 deletions

View File

@@ -3,9 +3,9 @@ default_config:
logger: logger:
default: warning default: warning
logs: logs:
# custom_components.versatile_thermostat: debug custom_components.versatile_thermostat: info
# custom_components.versatile_thermostat.underlyings: debug custom_components.versatile_thermostat.underlyings: info
# custom_components.versatile_thermostat.climate: debug custom_components.versatile_thermostat.climate: info
# If you need to debug uncommment the line below (doc: https://www.home-assistant.io/integrations/debugpy/) # If you need to debug uncommment the line below (doc: https://www.home-assistant.io/integrations/debugpy/)
debugpy: debugpy:

View File

@@ -107,6 +107,8 @@ async def async_setup(
"VersatileThermostat - HA is started, initialize all links between VTherm entities" "VersatileThermostat - HA is started, initialize all links between VTherm entities"
) )
await api.init_vtherm_links() await api.init_vtherm_links()
await api.notify_central_mode_change()
await api.reload_central_boiler_entities_list()
if hass.state == CoreState.running: if hass.state == CoreState.running:
await _async_startup_internal() await _async_startup_internal()
@@ -156,8 +158,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
await api.reload_central_boiler_entities_list() if hass.state == CoreState.running:
await api.init_vtherm_links() await api.reload_central_boiler_entities_list()
await api.init_vtherm_links()
return True return True

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
@@ -628,7 +629,8 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
self.async_on_remove(self.remove_thermostat) self.async_on_remove(self.remove_thermostat)
await self.async_startup() # issue 428. Link to others entities will start at link
# await self.async_startup()
def remove_thermostat(self): def remove_thermostat(self):
"""Called when the thermostat will be removed""" """Called when the thermostat will be removed"""
@@ -636,155 +638,157 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
for under in self._underlyings: for under in self._underlyings:
under.remove_entity() under.remove_entity()
async def async_startup(self): async def async_startup(self, central_configuration):
"""Triggered on startup, used to get old state and set internal states accordingly""" """Triggered on startup, used to get old state and set internal states accordingly"""
_LOGGER.debug("%s - Calling async_startup", self) _LOGGER.debug("%s - Calling async_startup", self)
@callback _LOGGER.debug("%s - Calling async_startup_internal", self)
async def _async_startup_internal(*_): need_write_state = False
_LOGGER.debug("%s - Calling async_startup_internal", self)
need_write_state = False
# Initialize all UnderlyingEntities await self.get_my_previous_state()
self.init_underlyings()
temperature_state = self.hass.states.get(self._temp_sensor_entity_id) await self.init_presets(central_configuration)
if temperature_state and temperature_state.state not in (
# Initialize all UnderlyingEntities
self.init_underlyings()
temperature_state = self.hass.states.get(self._temp_sensor_entity_id)
if temperature_state and temperature_state.state not in (
STATE_UNAVAILABLE,
STATE_UNKNOWN,
):
_LOGGER.debug(
"%s - temperature sensor have been retrieved: %.1f",
self,
float(temperature_state.state),
)
await self._async_update_temp(temperature_state)
need_write_state = True
if self._ext_temp_sensor_entity_id:
ext_temperature_state = self.hass.states.get(
self._ext_temp_sensor_entity_id
)
if ext_temperature_state and ext_temperature_state.state not in (
STATE_UNAVAILABLE, STATE_UNAVAILABLE,
STATE_UNKNOWN, STATE_UNKNOWN,
): ):
_LOGGER.debug( _LOGGER.debug(
"%s - temperature sensor have been retrieved: %.1f", "%s - external temperature sensor have been retrieved: %.1f",
self, self,
float(temperature_state.state), float(ext_temperature_state.state),
) )
await self._async_update_temp(temperature_state) await self._async_update_ext_temp(ext_temperature_state)
need_write_state = True
if self._ext_temp_sensor_entity_id:
ext_temperature_state = self.hass.states.get(
self._ext_temp_sensor_entity_id
)
if ext_temperature_state and ext_temperature_state.state not in (
STATE_UNAVAILABLE,
STATE_UNKNOWN,
):
_LOGGER.debug(
"%s - external temperature sensor have been retrieved: %.1f",
self,
float(ext_temperature_state.state),
)
await self._async_update_ext_temp(ext_temperature_state)
else:
_LOGGER.debug(
"%s - external temperature sensor have NOT been retrieved cause unknown or unavailable",
self,
)
else: else:
_LOGGER.debug( _LOGGER.debug(
"%s - external temperature sensor have NOT been retrieved cause no external sensor", "%s - external temperature sensor have NOT been retrieved cause unknown or unavailable",
self, self,
) )
if self._pmax_on:
# try to acquire current power and power max
current_power_state = self.hass.states.get(self._power_sensor_entity_id)
if current_power_state and current_power_state.state not in (
STATE_UNAVAILABLE,
STATE_UNKNOWN,
):
self._current_power = float(current_power_state.state)
_LOGGER.debug(
"%s - Current power have been retrieved: %.3f",
self,
self._current_power,
)
need_write_state = True
# Try to acquire power max
current_power_max_state = self.hass.states.get(
self._max_power_sensor_entity_id
)
if current_power_max_state and current_power_max_state.state not in (
STATE_UNAVAILABLE,
STATE_UNKNOWN,
):
self._current_power_max = float(current_power_max_state.state)
_LOGGER.debug(
"%s - Current power max have been retrieved: %.3f",
self,
self._current_power_max,
)
need_write_state = True
# try to acquire window entity state
if self._window_sensor_entity_id:
window_state = self.hass.states.get(self._window_sensor_entity_id)
if window_state and window_state.state not in (
STATE_UNAVAILABLE,
STATE_UNKNOWN,
):
self._window_state = window_state.state == STATE_ON
_LOGGER.debug(
"%s - Window state have been retrieved: %s",
self,
self._window_state,
)
need_write_state = True
# try to acquire motion entity state
if self._motion_sensor_entity_id:
motion_state = self.hass.states.get(self._motion_sensor_entity_id)
if motion_state and motion_state.state not in (
STATE_UNAVAILABLE,
STATE_UNKNOWN,
):
self._motion_state = motion_state.state
_LOGGER.debug(
"%s - Motion state have been retrieved: %s",
self,
self._motion_state,
)
# recalculate the right target_temp in activity mode
await self._async_update_motion_temp()
need_write_state = True
if self._presence_on:
# try to acquire presence entity state
presence_state = self.hass.states.get(self._presence_sensor_entity_id)
if presence_state and presence_state.state not in (
STATE_UNAVAILABLE,
STATE_UNKNOWN,
):
await self._async_update_presence(presence_state.state)
_LOGGER.debug(
"%s - Presence have been retrieved: %s",
self,
presence_state.state,
)
need_write_state = True
if need_write_state:
self.async_write_ha_state()
if self._prop_algorithm:
self._prop_algorithm.calculate(
self._target_temp,
self._cur_temp,
self._cur_ext_temp,
self._hvac_mode or HVACMode.OFF,
)
self.reset_last_change_time()
await self.get_my_previous_state()
if self.hass.state == CoreState.running:
await _async_startup_internal()
else: else:
self.hass.bus.async_listen_once( _LOGGER.debug(
EVENT_HOMEASSISTANT_START, _async_startup_internal "%s - external temperature sensor have NOT been retrieved cause no external sensor",
self,
) )
if self._pmax_on:
# try to acquire current power and power max
current_power_state = self.hass.states.get(self._power_sensor_entity_id)
if current_power_state and current_power_state.state not in (
STATE_UNAVAILABLE,
STATE_UNKNOWN,
):
self._current_power = float(current_power_state.state)
_LOGGER.debug(
"%s - Current power have been retrieved: %.3f",
self,
self._current_power,
)
need_write_state = True
# Try to acquire power max
current_power_max_state = self.hass.states.get(
self._max_power_sensor_entity_id
)
if current_power_max_state and current_power_max_state.state not in (
STATE_UNAVAILABLE,
STATE_UNKNOWN,
):
self._current_power_max = float(current_power_max_state.state)
_LOGGER.debug(
"%s - Current power max have been retrieved: %.3f",
self,
self._current_power_max,
)
need_write_state = True
# try to acquire window entity state
if self._window_sensor_entity_id:
window_state = self.hass.states.get(self._window_sensor_entity_id)
if window_state and window_state.state not in (
STATE_UNAVAILABLE,
STATE_UNKNOWN,
):
self._window_state = window_state.state == STATE_ON
_LOGGER.debug(
"%s - Window state have been retrieved: %s",
self,
self._window_state,
)
need_write_state = True
# try to acquire motion entity state
if self._motion_sensor_entity_id:
motion_state = self.hass.states.get(self._motion_sensor_entity_id)
if motion_state and motion_state.state not in (
STATE_UNAVAILABLE,
STATE_UNKNOWN,
):
self._motion_state = motion_state.state
_LOGGER.debug(
"%s - Motion state have been retrieved: %s",
self,
self._motion_state,
)
# recalculate the right target_temp in activity mode
await self._async_update_motion_temp()
need_write_state = True
if self._presence_on:
# try to acquire presence entity state
presence_state = self.hass.states.get(self._presence_sensor_entity_id)
if presence_state and presence_state.state not in (
STATE_UNAVAILABLE,
STATE_UNKNOWN,
):
await self._async_update_presence(presence_state.state)
_LOGGER.debug(
"%s - Presence have been retrieved: %s",
self,
presence_state.state,
)
need_write_state = True
if need_write_state:
self.async_write_ha_state()
if self._prop_algorithm:
self._prop_algorithm.calculate(
self._target_temp,
self._cur_temp,
self._cur_ext_temp,
self._hvac_mode or HVACMode.OFF,
)
self.hass.create_task(self._check_initial_state())
self.reset_last_change_time()
# if self.hass.state == CoreState.running:
# await _async_startup_internal()
# else:
# self.hass.bus.async_listen_once(
# EVENT_HOMEASSISTANT_START, _async_startup_internal
# )
def init_underlyings(self): def init_underlyings(self):
"""Initialize all underlyings. Should be overriden if necessary""" """Initialize all underlyings. Should be overriden if necessary"""
@@ -824,19 +828,20 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
# Never restore a Power or Security preset # Never restore a Power or Security preset
if old_preset_mode is not None and old_preset_mode not in HIDDEN_PRESETS: if old_preset_mode is not None and old_preset_mode not in HIDDEN_PRESETS:
# old_preset_mode in self._attr_preset_modes # old_preset_mode in self._attr_preset_modes
self._attr_preset_mode = old_state.attributes.get(ATTR_PRESET_MODE) self._attr_preset_mode = old_preset_mode
self.save_preset_mode() self.save_preset_mode()
else: else:
self._attr_preset_mode = PRESET_NONE self._attr_preset_mode = PRESET_NONE
if not self._hvac_mode and old_state.state in [ if old_state.state in [
HVACMode.OFF, HVACMode.OFF,
HVACMode.HEAT, HVACMode.HEAT,
HVACMode.COOL, HVACMode.COOL,
]: ]:
self._hvac_mode = old_state.state self._hvac_mode = old_state.state
else: else:
self._hvac_mode = HVACMode.OFF if not self._hvac_mode:
self._hvac_mode = HVACMode.OFF
old_total_energy = old_state.attributes.get(ATTR_TOTAL_ENERGY) old_total_energy = old_state.attributes.get(ATTR_TOTAL_ENERGY)
if old_total_energy: if old_total_energy:
@@ -2005,13 +2010,17 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
self._device_power, self._device_power,
) )
if self.is_over_climate: # issue 407 - power_consumption_max is power we need to add. If already active we don't need to add more power
power_consumption_max = self._device_power if self.is_device_active:
power_consumption_max = 0
else: else:
power_consumption_max = max( if self.is_over_climate:
self._device_power / self.nb_underlying_entities, power_consumption_max = self._device_power
self._device_power * self._prop_algorithm.on_percent, else:
) power_consumption_max = max(
self._device_power / self.nb_underlying_entities,
self._device_power * self._prop_algorithm.on_percent,
)
ret = (self._current_power + power_consumption_max) >= self._current_power_max ret = (self._current_power + power_consumption_max) >= self._current_power_max
if not self._overpowering_state and ret and self._hvac_mode != HVACMode.OFF: if not self._overpowering_state and ret and self._hvac_mode != HVACMode.OFF:
@@ -2080,6 +2089,8 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
new_central_mode, new_central_mode,
) )
first_init = self._last_central_mode == None
self._last_central_mode = new_central_mode self._last_central_mode = new_central_mode
def save_all(): def save_all():
@@ -2088,7 +2099,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
self.save_hvac_mode() self.save_hvac_mode()
if new_central_mode == CENTRAL_MODE_AUTO: if new_central_mode == CENTRAL_MODE_AUTO:
if self.window_state is not STATE_ON: if self.window_state is not STATE_ON and not first_init:
await self.restore_hvac_mode() await self.restore_hvac_mode()
await self.restore_preset_mode() await self.restore_preset_mode()
@@ -2702,8 +2713,6 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
if self._attr_preset_mode: if self._attr_preset_mode:
await self._async_set_preset_mode_internal(self._attr_preset_mode, True) await self._async_set_preset_mode_internal(self._attr_preset_mode, True)
self.hass.create_task(self._check_initial_state())
async def async_turn_off(self) -> None: async def async_turn_off(self) -> None:
await self.async_set_hvac_mode(HVACMode.OFF) await self.async_set_hvac_mode(HVACMode.OFF)
@@ -2711,4 +2720,4 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
if self._ac_mode: if self._ac_mode:
await self.async_set_hvac_mode(HVACMode.COOL) await self.async_set_hvac_mode(HVACMode.COOL)
else: else:
await self.async_set_hvac_mode(HVACMode.HEATING) await self.async_set_hvac_mode(HVACMode.HEAT)

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.1", "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,
) )
@@ -100,7 +101,7 @@ async def async_setup_entry(
if not entry.data.get(CONF_USE_PRESETS_CENTRAL_CONFIG, False): if not entry.data.get(CONF_USE_PRESETS_CENTRAL_CONFIG, False):
if entry.data.get(CONF_AC_MODE, False): if entry.data.get(CONF_AC_MODE, False):
for preset in CONF_PRESETS_WITH_AC_VALUES: for preset in CONF_PRESETS_WITH_AC_VALUES:
_LOGGER.warning( _LOGGER.debug(
"%s - configuring Number non central, AC, non AWAY for preset %s", "%s - configuring Number non central, AC, non AWAY for preset %s",
name, name,
preset, preset,
@@ -112,7 +113,7 @@ async def async_setup_entry(
) )
else: else:
for preset in CONF_PRESETS_VALUES: for preset in CONF_PRESETS_VALUES:
_LOGGER.warning( _LOGGER.debug(
"%s - configuring Number non central, non AC, non AWAY for preset %s", "%s - configuring Number non central, non AC, non AWAY for preset %s",
name, name,
preset, preset,
@@ -128,7 +129,7 @@ async def async_setup_entry(
) is True and not entry.data.get(CONF_USE_PRESENCE_CENTRAL_CONFIG, False): ) is True and not entry.data.get(CONF_USE_PRESENCE_CENTRAL_CONFIG, False):
if entry.data.get(CONF_AC_MODE, False): if entry.data.get(CONF_AC_MODE, False):
for preset in CONF_PRESETS_AWAY_WITH_AC_VALUES: for preset in CONF_PRESETS_AWAY_WITH_AC_VALUES:
_LOGGER.warning( _LOGGER.debug(
"%s - configuring Number non central, AC, AWAY for preset %s", "%s - configuring Number non central, AC, AWAY for preset %s",
name, name,
preset, preset,
@@ -140,7 +141,7 @@ async def async_setup_entry(
) )
else: else:
for preset in CONF_PRESETS_AWAY_VALUES: for preset in CONF_PRESETS_AWAY_VALUES:
_LOGGER.warning( _LOGGER.debug(
"%s - configuring Number non central, non AC, AWAY for preset %s", "%s - configuring Number non central, non AC, AWAY for preset %s",
name, name,
preset, preset,
@@ -153,11 +154,12 @@ 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.warning( _LOGGER.debug(
"%s - configuring Number central, AC, non AWAY for preset %s", "%s - configuring Number central, AC, non AWAY for preset %s",
name, name,
preset, preset,
@@ -169,7 +171,7 @@ async def async_setup_entry(
) )
for preset in CONF_PRESETS_AWAY_WITH_AC_VALUES: for preset in CONF_PRESETS_AWAY_WITH_AC_VALUES:
_LOGGER.warning( _LOGGER.debug(
"%s - configuring Number central, AC, AWAY for preset %s", name, preset "%s - configuring Number central, AC, AWAY for preset %s", name, preset
) )
entities.append( entities.append(
@@ -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(
@@ -350,7 +353,7 @@ class CentralConfigTemperatureNumber(
# We have to reload all VTherm for which uses the central configuration # We have to reload all VTherm for which uses the central configuration
api: VersatileThermostatAPI = VersatileThermostatAPI.get_vtherm_api(self.hass) api: VersatileThermostatAPI = VersatileThermostatAPI.get_vtherm_api(self.hass)
# Update the VTherms which have temperature in central config # Update the VTherms which have temperature in central config
self.hass.create_task(api.init_vtherm_links(only_use_central=True)) self.hass.create_task(api.init_vtherm_preset_with_central())
def __str__(self): def __str__(self):
return f"VersatileThermostat-{self.name}" return f"VersatileThermostat-{self.name}"

View File

@@ -18,6 +18,9 @@ from custom_components.versatile_thermostat.base_thermostat import (
BaseThermostat, BaseThermostat,
ConfigData, ConfigData,
) )
from custom_components.versatile_thermostat.vtherm_api import VersatileThermostatAPI
from .const import ( from .const import (
DOMAIN, DOMAIN,
DEVICE_MANUFACTURER, DEVICE_MANUFACTURER,
@@ -96,17 +99,20 @@ class CentralModeSelect(SelectEntity, RestoreEntity):
if old_state is not None: if old_state is not None:
self._attr_current_option = old_state.state self._attr_current_option = old_state.state
@callback api: VersatileThermostatAPI = VersatileThermostatAPI.get_vtherm_api(self.hass)
async def _async_startup_internal(*_): api.register_central_mode_select(self)
_LOGGER.debug("%s - Calling async_startup_internal", self)
await self.notify_central_mode_change()
if self.hass.state == CoreState.running: # @callback
await _async_startup_internal() # async def _async_startup_internal(*_):
else: # _LOGGER.debug("%s - Calling async_startup_internal", self)
self.hass.bus.async_listen_once( # await self.notify_central_mode_change()
EVENT_HOMEASSISTANT_START, _async_startup_internal #
) # if self.hass.state == CoreState.running:
# await _async_startup_internal()
# else:
# self.hass.bus.async_listen_once(
# EVENT_HOMEASSISTANT_START, _async_startup_internal
# )
@overrides @overrides
async def async_select_option(self, option: str) -> None: async def async_select_option(self, option: str) -> None:
@@ -122,17 +128,9 @@ class CentralModeSelect(SelectEntity, RestoreEntity):
async def notify_central_mode_change(self, old_central_mode: str | None = None): async def notify_central_mode_change(self, old_central_mode: str | None = None):
"""Notify all VTherm that the central_mode have change""" """Notify all VTherm that the central_mode have change"""
api: VersatileThermostatAPI = VersatileThermostatAPI.get_vtherm_api(self.hass)
# Update all VTherm states # Update all VTherm states
component: EntityComponent[ClimateEntity] = self.hass.data[CLIMATE_DOMAIN] await api.notify_central_mode_change(old_central_mode)
for entity in component.entities:
if isinstance(entity, BaseThermostat):
_LOGGER.debug(
"Changing the central_mode. We have find %s to update",
entity.name,
)
await entity.check_central_mode(
self._attr_current_option, old_central_mode
)
def __str__(self) -> str: def __str__(self) -> str:
return f"VersatileThermostat-{self.name}" return f"VersatileThermostat-{self.name}"

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)
@@ -730,7 +730,7 @@ class NbActiveDeviceForBoilerSensor(SensorEntity):
entity.name, entity.name,
) )
if ( if (
entity.hvac_mode == HVACMode.HEAT entity.hvac_mode in [HVACMode.HEAT, HVACMode.AUTO]
and entity.hvac_action == HVACAction.HEATING and entity.hvac_action == HVACAction.HEATING
): ):
for under in entity.underlying_entities: for under in entity.underlying_entities:

View File

@@ -14,7 +14,7 @@
}, },
"menu": { "menu": {
"title": "Menu", "title": "Menu",
"description": "Configure your thermostat. You will be able to finalize the configuration when all needed parameters are valued.", "description": "Configure your thermostat. You will be able to finalize the configuration when all required parameters are entered.",
"menu_options": { "menu_options": {
"main": "Main attributes", "main": "Main attributes",
"central_boiler": "Central boiler", "central_boiler": "Central boiler",
@@ -40,11 +40,11 @@
"temperature_sensor_entity_id": "Room temperature sensor entity id", "temperature_sensor_entity_id": "Room temperature sensor entity id",
"external_temperature_sensor_entity_id": "Outdoor temperature sensor entity id", "external_temperature_sensor_entity_id": "Outdoor temperature sensor entity id",
"cycle_min": "Cycle duration (minutes)", "cycle_min": "Cycle duration (minutes)",
"temp_min": "Minimal temperature allowed", "temp_min": "Minimum temperature allowed",
"temp_max": "Maximal temperature allowed", "temp_max": "Maximum temperature allowed",
"step_temperature": "Temperature step", "step_temperature": "Temperature step",
"device_power": "Device power", "device_power": "Device power",
"use_central_mode": "Enable the control by central entity (need central config). Check to enable the control of the VTherm with the select central_mode entities.", "use_central_mode": "Enable the control by central entity (requires central config). Check to enable the control of the VTherm with the select central_mode entities.",
"use_main_central_config": "Use additional central main configuration. Check to use the central main configuration (outdoor temperature, min, max, step, ...).", "use_main_central_config": "Use additional central main configuration. Check to use the central main configuration (outdoor temperature, min, max, step, ...).",
"used_by_controls_central_boiler": "Used by central boiler. Check if this VTherm should have control on the central boiler" "used_by_controls_central_boiler": "Used by central boiler. Check if this VTherm should have control on the central boiler"
}, },
@@ -60,7 +60,7 @@
"use_motion_feature": "Use motion detection", "use_motion_feature": "Use motion detection",
"use_power_feature": "Use power management", "use_power_feature": "Use power management",
"use_presence_feature": "Use presence detection", "use_presence_feature": "Use presence detection",
"use_central_boiler_feature": "Use a central boiler. Check to add a control to your central boiler. You will have to configure the VTherm which will have a control of the central boiler after seecting this checkbox to take effect. If one VTherm need heating, the boiler will be turned on. If no VTherm needs heating, the boiler will be turned off. Commands for turning on/off the central boiler are given in the related configuration page" "use_central_boiler_feature": "Use a central boiler. Check to add a control to your central boiler. You will have to configure the VTherm which will have a control of the central boiler after seecting this checkbox to take effect. If one VTherm requires heating, the boiler will be turned on. If no VTherm requires heating, the boiler will be turned off. Commands for turning on/off the central boiler are given in the related configuration page"
} }
}, },
"type": { "type": {
@@ -84,16 +84,16 @@
"valve_entity4_id": "4th valve number", "valve_entity4_id": "4th valve number",
"auto_regulation_mode": "Self-regulation", "auto_regulation_mode": "Self-regulation",
"auto_regulation_dtemp": "Regulation threshold", "auto_regulation_dtemp": "Regulation threshold",
"auto_regulation_periode_min": "Regulation minimal period", "auto_regulation_periode_min": "Regulation minimum period",
"auto_regulation_use_device_temp": "Use internal temperature of the underlying", "auto_regulation_use_device_temp": "Use internal temperature of the underlying",
"inverse_switch_command": "Inverse switch command", "inverse_switch_command": "Inverse switch command",
"auto_fan_mode": " Auto fan mode" "auto_fan_mode": " Auto fan mode"
}, },
"data_description": { "data_description": {
"heater_entity_id": "Mandatory heater entity id", "heater_entity_id": "Mandatory heater entity id",
"heater_entity2_id": "Optional 2nd Heater entity id. Leave empty if not used", "heater_entity2_id": "Optional 2nd Heater entity id. Leave empty if not required",
"heater_entity3_id": "Optional 3rd Heater entity id. Leave empty if not used", "heater_entity3_id": "Optional 3rd Heater entity id. Leave empty if not required",
"heater_entity4_id": "Optional 4th Heater entity id. Leave empty if not used", "heater_entity4_id": "Optional 4th Heater entity id. Leave empty if not required",
"heater_keep_alive": "Optional heater switch state refresh interval. Leave empty if not required.", "heater_keep_alive": "Optional heater switch state refresh interval. Leave empty if not required.",
"proportional_function": "Algorithm to use (TPI is the only one for now)", "proportional_function": "Algorithm to use (TPI is the only one for now)",
"climate_entity_id": "Underlying climate entity id", "climate_entity_id": "Underlying climate entity id",
@@ -129,7 +129,7 @@
}, },
"presets": { "presets": {
"title": "Presets", "title": "Presets",
"description": "Check if the thermostat will use central presets. Uncheck and the thermostat will have its own preset entities", "description": "Select if the thermostat will use central preset - deselect for the thermostat to have its own presets",
"data": { "data": {
"use_presets_central_config": "Use central presets configuration" "use_presets_central_config": "Use central presets configuration"
} }
@@ -152,8 +152,8 @@
"window_auto_open_threshold": "Recommended value: between 3 and 10. Leave empty if automatic window open detection is not used", "window_auto_open_threshold": "Recommended value: between 3 and 10. Leave empty if automatic window open detection is not used",
"window_auto_close_threshold": "Recommended value: 0. Leave empty if automatic window open detection is not used", "window_auto_close_threshold": "Recommended value: 0. Leave empty if automatic window open detection is not used",
"window_auto_max_duration": "Recommended value: 60 (one hour). Leave empty if automatic window open detection is not used", "window_auto_max_duration": "Recommended value: 60 (one hour). Leave empty if automatic window open detection is not used",
"use_window_central_config": "Check to use the central window configuration. Uncheck to use a specific window configuration for this VTherm", "use_window_central_config": "Select to use the central window configuration. Deselect to use a specific window configuration for this VTherm",
"window_action": "Action to do if window is deteted as open" "window_action": "Action to perform if window is deteted as open"
} }
}, },
"motion": { "motion": {
@@ -178,7 +178,7 @@
}, },
"power": { "power": {
"title": "Power management", "title": "Power management",
"description": "Power management attributes.\nGives the power and max power sensor of your home.\nThen specify the power consumption of the heater when on.\nAll sensors and device power should have the same unit (kW or W).", "description": "Power management attributes.\nGives the power and max power sensor of your home.\nSpecify the power consumption of the heater when on.\nAll sensors and device power should use the same unit (kW or W).",
"data": { "data": {
"power_sensor_entity_id": "Power", "power_sensor_entity_id": "Power",
"max_power_sensor_entity_id": "Max power", "max_power_sensor_entity_id": "Max power",
@@ -197,7 +197,7 @@
"description": "Presence management attributes.\nGives the a presence sensor of your home (true is someone is present) and give the corresponding temperature preset setting.", "description": "Presence management attributes.\nGives the a presence sensor of your home (true is someone is present) and give the corresponding temperature preset setting.",
"data": { "data": {
"presence_sensor_entity_id": "Presence sensor", "presence_sensor_entity_id": "Presence sensor",
"use_presence_central_config": "Use central presence temperature configuration. Uncheck to use specific temperature entities" "use_presence_central_config": "Use central presence temperature configuration. Deselect to use specific temperature entities"
}, },
"data_description": { "data_description": {
"presence_sensor_entity_id": "Presence sensor entity id" "presence_sensor_entity_id": "Presence sensor entity id"
@@ -207,16 +207,16 @@
"title": "Advanced parameters", "title": "Advanced parameters",
"description": "Configuration of advanced parameters. Leave the default values if you don't know what you are doing.\nThese parameters can lead to very poor temperature control or bad power regulation.", "description": "Configuration of advanced parameters. Leave the default values if you don't know what you are doing.\nThese parameters can lead to very poor temperature control or bad power regulation.",
"data": { "data": {
"minimal_activation_delay": "Minimal activation delay", "minimal_activation_delay": "Minimum activation delay",
"security_delay_min": "Safety delay (in minutes)", "security_delay_min": "Safety delay (in minutes)",
"security_min_on_percent": "Minimal power percent to enable safety mode", "security_min_on_percent": "Minimum power percent to enable safety mode",
"security_default_on_percent": "Power percent to use in safety mode", "security_default_on_percent": "Power percent to use in safety mode",
"use_advanced_central_config": "Use central advanced configuration" "use_advanced_central_config": "Use central advanced configuration"
}, },
"data_description": { "data_description": {
"minimal_activation_delay": "Delay in seconds under which the equipment will not be activated", "minimal_activation_delay": "Delay in seconds under which the equipment will not be activated",
"security_delay_min": "Maximum allowed delay in minutes between two temperature measurements. Above this delay the thermostat will turn to a safety off state", "security_delay_min": "Maximum allowed delay in minutes between two temperature measurements. Above this delay the thermostat will turn to a safety off state",
"security_min_on_percent": "Minimal heating percent value for safety preset activation. Below this amount of power percent the thermostat won't go into safety preset", "security_min_on_percent": "Minimum heating percent value for safety preset activation. Below this amount of power percent the thermostat won't go into safety preset",
"security_default_on_percent": "The default heating power percent value in safety preset. Set to 0 to switch off heater in safety preset", "security_default_on_percent": "The default heating power percent value in safety preset. Set to 0 to switch off heater in safety preset",
"use_advanced_central_config": "Check to use the central advanced configuration. Uncheck to use a specific advanced configuration for this VTherm" "use_advanced_central_config": "Check to use the central advanced configuration. Uncheck to use a specific advanced configuration for this VTherm"
} }
@@ -226,7 +226,7 @@
"unknown": "Unexpected error", "unknown": "Unexpected error",
"unknown_entity": "Unknown entity id", "unknown_entity": "Unknown entity id",
"window_open_detection_method": "Only one window open detection method should be used. Use either window sensor or automatic detection through temperature threshold but not both", "window_open_detection_method": "Only one window open detection method should be used. Use either window sensor or automatic detection through temperature threshold but not both",
"no_central_config": "You cannot check 'use central configuration' because no central configuration was found. You need to create a Versatile Thermostat of type 'Central Configuration' to use it." "no_central_config": "You cannot select 'use central configuration' because no central configuration was found. You need to create a Versatile Thermostat of type 'Central Configuration' to use it."
}, },
"abort": { "abort": {
"already_configured": "Device is already configured" "already_configured": "Device is already configured"
@@ -246,7 +246,7 @@
}, },
"menu": { "menu": {
"title": "Menu", "title": "Menu",
"description": "Configure your thermostat. You will be able to finalize the configuration when all needed parameters are valued.", "description": "Configure your thermostat. You will be able to finalize the configuration when all required parameters are entered.",
"menu_options": { "menu_options": {
"main": "Main attributes", "main": "Main attributes",
"central_boiler": "Central boiler", "central_boiler": "Central boiler",
@@ -272,11 +272,11 @@
"temperature_sensor_entity_id": "Room temperature sensor entity id", "temperature_sensor_entity_id": "Room temperature sensor entity id",
"external_temperature_sensor_entity_id": "Outdoor temperature sensor entity id", "external_temperature_sensor_entity_id": "Outdoor temperature sensor entity id",
"cycle_min": "Cycle duration (minutes)", "cycle_min": "Cycle duration (minutes)",
"temp_min": "Minimal temperature allowed", "temp_min": "Minimum temperature allowed",
"temp_max": "Maximal temperature allowed", "temp_max": "Maximum temperature allowed",
"step_temperature": "Temperature step", "step_temperature": "Temperature step",
"device_power": "Device power", "device_power": "Device power",
"use_central_mode": "Enable the control by central entity (need central config). Check to enable the control of the VTherm with the select central_mode entities.", "use_central_mode": "Enable the control by central entity (requires central config). Check to enable the control of the VTherm with the select central_mode entities.",
"use_main_central_config": "Use additional central main configuration. Check to use the central main configuration (outdoor temperature, min, max, step, ...).", "use_main_central_config": "Use additional central main configuration. Check to use the central main configuration (outdoor temperature, min, max, step, ...).",
"used_by_controls_central_boiler": "Used by central boiler. Check if this VTherm should have control on the central boiler" "used_by_controls_central_boiler": "Used by central boiler. Check if this VTherm should have control on the central boiler"
}, },
@@ -292,7 +292,7 @@
"use_motion_feature": "Use motion detection", "use_motion_feature": "Use motion detection",
"use_power_feature": "Use power management", "use_power_feature": "Use power management",
"use_presence_feature": "Use presence detection", "use_presence_feature": "Use presence detection",
"use_central_boiler_feature": "Use a central boiler. Check to add a control to your central boiler. You will have to configure the VTherm which will have a control of the central boiler after seecting this checkbox to take effect. If one VTherm need heating, the boiler will be turned on. If no VTherm needs heating, the boiler will be turned off. Commands for turning on/off the central boiler are given in the related configuration page" "use_central_boiler_feature": "Use a central boiler. Check to add a control to your central boiler. You will have to configure the VTherm which will have a control of the central boiler after seecting this checkbox to take effect. If one VTherm requires heating, the boiler will be turned on. If no VTherm requires heating, the boiler will be turned off. Commands for turning on/off the central boiler are given in the related configuration page"
} }
}, },
"type": { "type": {
@@ -316,7 +316,7 @@
"valve_entity4_id": "4th valve number", "valve_entity4_id": "4th valve number",
"auto_regulation_mode": "Self-regulation", "auto_regulation_mode": "Self-regulation",
"auto_regulation_dtemp": "Regulation threshold", "auto_regulation_dtemp": "Regulation threshold",
"auto_regulation_periode_min": "Regulation minimal period", "auto_regulation_periode_min": "Regulation minimum period",
"auto_regulation_use_device_temp": "Use internal temperature of the underlying", "auto_regulation_use_device_temp": "Use internal temperature of the underlying",
"inverse_switch_command": "Inverse switch command", "inverse_switch_command": "Inverse switch command",
"auto_fan_mode": " Auto fan mode" "auto_fan_mode": " Auto fan mode"
@@ -439,16 +439,16 @@
"title": "Advanced - {name}", "title": "Advanced - {name}",
"description": "Advanced parameters. Leave the default values if you don't know what you are doing.\nThese parameters can lead to very poor temperature control or bad power regulation.", "description": "Advanced parameters. Leave the default values if you don't know what you are doing.\nThese parameters can lead to very poor temperature control or bad power regulation.",
"data": { "data": {
"minimal_activation_delay": "Minimal activation delay", "minimal_activation_delay": "Minimum activation delay",
"security_delay_min": "Safety delay (in minutes)", "security_delay_min": "Safety delay (in minutes)",
"security_min_on_percent": "Minimal power percent to enable safety mode", "security_min_on_percent": "Minimum power percent to enable safety mode",
"security_default_on_percent": "Power percent to use in safety mode", "security_default_on_percent": "Power percent to use in safety mode",
"use_advanced_central_config": "Use central advanced configuration" "use_advanced_central_config": "Use central advanced configuration"
}, },
"data_description": { "data_description": {
"minimal_activation_delay": "Delay in seconds under which the equipment will not be activated", "minimal_activation_delay": "Delay in seconds under which the equipment will not be activated",
"security_delay_min": "Maximum allowed delay in minutes between two temperature measurements. Above this delay the thermostat will turn to a safety off state", "security_delay_min": "Maximum allowed delay in minutes between two temperature measurements. Above this delay the thermostat will turn to a safety off state",
"security_min_on_percent": "Minimal heating percent value for safety preset activation. Below this amount of power percent the thermostat won't go into safety preset", "security_min_on_percent": "Minimum heating percent value for safety preset activation. Below this amount of power percent the thermostat won't go into safety preset",
"security_default_on_percent": "The default heating power percent value in safety preset. Set to 0 to switch off heater in safety preset", "security_default_on_percent": "The default heating power percent value in safety preset. Set to 0 to switch off heater in safety preset",
"use_advanced_central_config": "Check to use the central advanced configuration. Uncheck to use a specific advanced configuration for this VTherm" "use_advanced_central_config": "Check to use the central advanced configuration. Uncheck to use a specific advanced configuration for this VTherm"
} }

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

@@ -14,7 +14,7 @@
}, },
"menu": { "menu": {
"title": "Menu", "title": "Menu",
"description": "Configure your thermostat. You will be able to finalize the configuration when all needed parameters are valued.", "description": "Configure your thermostat. You will be able to finalize the configuration when all required parameters are entered.",
"menu_options": { "menu_options": {
"main": "Main attributes", "main": "Main attributes",
"central_boiler": "Central boiler", "central_boiler": "Central boiler",
@@ -40,11 +40,11 @@
"temperature_sensor_entity_id": "Room temperature sensor entity id", "temperature_sensor_entity_id": "Room temperature sensor entity id",
"external_temperature_sensor_entity_id": "Outdoor temperature sensor entity id", "external_temperature_sensor_entity_id": "Outdoor temperature sensor entity id",
"cycle_min": "Cycle duration (minutes)", "cycle_min": "Cycle duration (minutes)",
"temp_min": "Minimal temperature allowed", "temp_min": "Minimum temperature allowed",
"temp_max": "Maximal temperature allowed", "temp_max": "Maximum temperature allowed",
"step_temperature": "Temperature step", "step_temperature": "Temperature step",
"device_power": "Device power", "device_power": "Device power",
"use_central_mode": "Enable the control by central entity (need central config). Check to enable the control of the VTherm with the select central_mode entities.", "use_central_mode": "Enable the control by central entity (requires central config). Check to enable the control of the VTherm with the select central_mode entities.",
"use_main_central_config": "Use additional central main configuration. Check to use the central main configuration (outdoor temperature, min, max, step, ...).", "use_main_central_config": "Use additional central main configuration. Check to use the central main configuration (outdoor temperature, min, max, step, ...).",
"used_by_controls_central_boiler": "Used by central boiler. Check if this VTherm should have control on the central boiler" "used_by_controls_central_boiler": "Used by central boiler. Check if this VTherm should have control on the central boiler"
}, },
@@ -60,7 +60,7 @@
"use_motion_feature": "Use motion detection", "use_motion_feature": "Use motion detection",
"use_power_feature": "Use power management", "use_power_feature": "Use power management",
"use_presence_feature": "Use presence detection", "use_presence_feature": "Use presence detection",
"use_central_boiler_feature": "Use a central boiler. Check to add a control to your central boiler. You will have to configure the VTherm which will have a control of the central boiler after seecting this checkbox to take effect. If one VTherm need heating, the boiler will be turned on. If no VTherm needs heating, the boiler will be turned off. Commands for turning on/off the central boiler are given in the related configuration page" "use_central_boiler_feature": "Use a central boiler. Check to add a control to your central boiler. You will have to configure the VTherm which will have a control of the central boiler after seecting this checkbox to take effect. If one VTherm requires heating, the boiler will be turned on. If no VTherm requires heating, the boiler will be turned off. Commands for turning on/off the central boiler are given in the related configuration page"
} }
}, },
"type": { "type": {
@@ -84,16 +84,16 @@
"valve_entity4_id": "4th valve number", "valve_entity4_id": "4th valve number",
"auto_regulation_mode": "Self-regulation", "auto_regulation_mode": "Self-regulation",
"auto_regulation_dtemp": "Regulation threshold", "auto_regulation_dtemp": "Regulation threshold",
"auto_regulation_periode_min": "Regulation minimal period", "auto_regulation_periode_min": "Regulation minimum period",
"auto_regulation_use_device_temp": "Use internal temperature of the underlying", "auto_regulation_use_device_temp": "Use internal temperature of the underlying",
"inverse_switch_command": "Inverse switch command", "inverse_switch_command": "Inverse switch command",
"auto_fan_mode": " Auto fan mode" "auto_fan_mode": " Auto fan mode"
}, },
"data_description": { "data_description": {
"heater_entity_id": "Mandatory heater entity id", "heater_entity_id": "Mandatory heater entity id",
"heater_entity2_id": "Optional 2nd Heater entity id. Leave empty if not used", "heater_entity2_id": "Optional 2nd Heater entity id. Leave empty if not required",
"heater_entity3_id": "Optional 3rd Heater entity id. Leave empty if not used", "heater_entity3_id": "Optional 3rd Heater entity id. Leave empty if not required",
"heater_entity4_id": "Optional 4th Heater entity id. Leave empty if not used", "heater_entity4_id": "Optional 4th Heater entity id. Leave empty if not required",
"heater_keep_alive": "Optional heater switch state refresh interval. Leave empty if not required.", "heater_keep_alive": "Optional heater switch state refresh interval. Leave empty if not required.",
"proportional_function": "Algorithm to use (TPI is the only one for now)", "proportional_function": "Algorithm to use (TPI is the only one for now)",
"climate_entity_id": "Underlying climate entity id", "climate_entity_id": "Underlying climate entity id",
@@ -129,7 +129,7 @@
}, },
"presets": { "presets": {
"title": "Presets", "title": "Presets",
"description": "Check if the thermostat will use central presets. Uncheck and the thermostat will have its own preset entities", "description": "Select if the thermostat will use central preset - deselect for the thermostat to have its own presets",
"data": { "data": {
"use_presets_central_config": "Use central presets configuration" "use_presets_central_config": "Use central presets configuration"
} }
@@ -152,8 +152,8 @@
"window_auto_open_threshold": "Recommended value: between 3 and 10. Leave empty if automatic window open detection is not used", "window_auto_open_threshold": "Recommended value: between 3 and 10. Leave empty if automatic window open detection is not used",
"window_auto_close_threshold": "Recommended value: 0. Leave empty if automatic window open detection is not used", "window_auto_close_threshold": "Recommended value: 0. Leave empty if automatic window open detection is not used",
"window_auto_max_duration": "Recommended value: 60 (one hour). Leave empty if automatic window open detection is not used", "window_auto_max_duration": "Recommended value: 60 (one hour). Leave empty if automatic window open detection is not used",
"use_window_central_config": "Check to use the central window configuration. Uncheck to use a specific window configuration for this VTherm", "use_window_central_config": "Select to use the central window configuration. Deselect to use a specific window configuration for this VTherm",
"window_action": "Action to do if window is deteted as open" "window_action": "Action to perform if window is deteted as open"
} }
}, },
"motion": { "motion": {
@@ -178,7 +178,7 @@
}, },
"power": { "power": {
"title": "Power management", "title": "Power management",
"description": "Power management attributes.\nGives the power and max power sensor of your home.\nThen specify the power consumption of the heater when on.\nAll sensors and device power should have the same unit (kW or W).", "description": "Power management attributes.\nGives the power and max power sensor of your home.\nSpecify the power consumption of the heater when on.\nAll sensors and device power should use the same unit (kW or W).",
"data": { "data": {
"power_sensor_entity_id": "Power", "power_sensor_entity_id": "Power",
"max_power_sensor_entity_id": "Max power", "max_power_sensor_entity_id": "Max power",
@@ -197,7 +197,7 @@
"description": "Presence management attributes.\nGives the a presence sensor of your home (true is someone is present) and give the corresponding temperature preset setting.", "description": "Presence management attributes.\nGives the a presence sensor of your home (true is someone is present) and give the corresponding temperature preset setting.",
"data": { "data": {
"presence_sensor_entity_id": "Presence sensor", "presence_sensor_entity_id": "Presence sensor",
"use_presence_central_config": "Use central presence temperature configuration. Uncheck to use specific temperature entities" "use_presence_central_config": "Use central presence temperature configuration. Deselect to use specific temperature entities"
}, },
"data_description": { "data_description": {
"presence_sensor_entity_id": "Presence sensor entity id" "presence_sensor_entity_id": "Presence sensor entity id"
@@ -207,16 +207,16 @@
"title": "Advanced parameters", "title": "Advanced parameters",
"description": "Configuration of advanced parameters. Leave the default values if you don't know what you are doing.\nThese parameters can lead to very poor temperature control or bad power regulation.", "description": "Configuration of advanced parameters. Leave the default values if you don't know what you are doing.\nThese parameters can lead to very poor temperature control or bad power regulation.",
"data": { "data": {
"minimal_activation_delay": "Minimal activation delay", "minimal_activation_delay": "Minimum activation delay",
"security_delay_min": "Safety delay (in minutes)", "security_delay_min": "Safety delay (in minutes)",
"security_min_on_percent": "Minimal power percent to enable safety mode", "security_min_on_percent": "Minimum power percent to enable safety mode",
"security_default_on_percent": "Power percent to use in safety mode", "security_default_on_percent": "Power percent to use in safety mode",
"use_advanced_central_config": "Use central advanced configuration" "use_advanced_central_config": "Use central advanced configuration"
}, },
"data_description": { "data_description": {
"minimal_activation_delay": "Delay in seconds under which the equipment will not be activated", "minimal_activation_delay": "Delay in seconds under which the equipment will not be activated",
"security_delay_min": "Maximum allowed delay in minutes between two temperature measurements. Above this delay the thermostat will turn to a safety off state", "security_delay_min": "Maximum allowed delay in minutes between two temperature measurements. Above this delay the thermostat will turn to a safety off state",
"security_min_on_percent": "Minimal heating percent value for safety preset activation. Below this amount of power percent the thermostat won't go into safety preset", "security_min_on_percent": "Minimum heating percent value for safety preset activation. Below this amount of power percent the thermostat won't go into safety preset",
"security_default_on_percent": "The default heating power percent value in safety preset. Set to 0 to switch off heater in safety preset", "security_default_on_percent": "The default heating power percent value in safety preset. Set to 0 to switch off heater in safety preset",
"use_advanced_central_config": "Check to use the central advanced configuration. Uncheck to use a specific advanced configuration for this VTherm" "use_advanced_central_config": "Check to use the central advanced configuration. Uncheck to use a specific advanced configuration for this VTherm"
} }
@@ -226,7 +226,7 @@
"unknown": "Unexpected error", "unknown": "Unexpected error",
"unknown_entity": "Unknown entity id", "unknown_entity": "Unknown entity id",
"window_open_detection_method": "Only one window open detection method should be used. Use either window sensor or automatic detection through temperature threshold but not both", "window_open_detection_method": "Only one window open detection method should be used. Use either window sensor or automatic detection through temperature threshold but not both",
"no_central_config": "You cannot check 'use central configuration' because no central configuration was found. You need to create a Versatile Thermostat of type 'Central Configuration' to use it." "no_central_config": "You cannot select 'use central configuration' because no central configuration was found. You need to create a Versatile Thermostat of type 'Central Configuration' to use it."
}, },
"abort": { "abort": {
"already_configured": "Device is already configured" "already_configured": "Device is already configured"
@@ -246,7 +246,7 @@
}, },
"menu": { "menu": {
"title": "Menu", "title": "Menu",
"description": "Configure your thermostat. You will be able to finalize the configuration when all needed parameters are valued.", "description": "Configure your thermostat. You will be able to finalize the configuration when all required parameters are entered.",
"menu_options": { "menu_options": {
"main": "Main attributes", "main": "Main attributes",
"central_boiler": "Central boiler", "central_boiler": "Central boiler",
@@ -272,11 +272,11 @@
"temperature_sensor_entity_id": "Room temperature sensor entity id", "temperature_sensor_entity_id": "Room temperature sensor entity id",
"external_temperature_sensor_entity_id": "Outdoor temperature sensor entity id", "external_temperature_sensor_entity_id": "Outdoor temperature sensor entity id",
"cycle_min": "Cycle duration (minutes)", "cycle_min": "Cycle duration (minutes)",
"temp_min": "Minimal temperature allowed", "temp_min": "Minimum temperature allowed",
"temp_max": "Maximal temperature allowed", "temp_max": "Maximum temperature allowed",
"step_temperature": "Temperature step", "step_temperature": "Temperature step",
"device_power": "Device power", "device_power": "Device power",
"use_central_mode": "Enable the control by central entity (need central config). Check to enable the control of the VTherm with the select central_mode entities.", "use_central_mode": "Enable the control by central entity (requires central config). Check to enable the control of the VTherm with the select central_mode entities.",
"use_main_central_config": "Use additional central main configuration. Check to use the central main configuration (outdoor temperature, min, max, step, ...).", "use_main_central_config": "Use additional central main configuration. Check to use the central main configuration (outdoor temperature, min, max, step, ...).",
"used_by_controls_central_boiler": "Used by central boiler. Check if this VTherm should have control on the central boiler" "used_by_controls_central_boiler": "Used by central boiler. Check if this VTherm should have control on the central boiler"
}, },
@@ -292,7 +292,7 @@
"use_motion_feature": "Use motion detection", "use_motion_feature": "Use motion detection",
"use_power_feature": "Use power management", "use_power_feature": "Use power management",
"use_presence_feature": "Use presence detection", "use_presence_feature": "Use presence detection",
"use_central_boiler_feature": "Use a central boiler. Check to add a control to your central boiler. You will have to configure the VTherm which will have a control of the central boiler after seecting this checkbox to take effect. If one VTherm need heating, the boiler will be turned on. If no VTherm needs heating, the boiler will be turned off. Commands for turning on/off the central boiler are given in the related configuration page" "use_central_boiler_feature": "Use a central boiler. Check to add a control to your central boiler. You will have to configure the VTherm which will have a control of the central boiler after seecting this checkbox to take effect. If one VTherm requires heating, the boiler will be turned on. If no VTherm requires heating, the boiler will be turned off. Commands for turning on/off the central boiler are given in the related configuration page"
} }
}, },
"type": { "type": {
@@ -316,7 +316,7 @@
"valve_entity4_id": "4th valve number", "valve_entity4_id": "4th valve number",
"auto_regulation_mode": "Self-regulation", "auto_regulation_mode": "Self-regulation",
"auto_regulation_dtemp": "Regulation threshold", "auto_regulation_dtemp": "Regulation threshold",
"auto_regulation_periode_min": "Regulation minimal period", "auto_regulation_periode_min": "Regulation minimum period",
"auto_regulation_use_device_temp": "Use internal temperature of the underlying", "auto_regulation_use_device_temp": "Use internal temperature of the underlying",
"inverse_switch_command": "Inverse switch command", "inverse_switch_command": "Inverse switch command",
"auto_fan_mode": " Auto fan mode" "auto_fan_mode": " Auto fan mode"
@@ -439,16 +439,16 @@
"title": "Advanced - {name}", "title": "Advanced - {name}",
"description": "Advanced parameters. Leave the default values if you don't know what you are doing.\nThese parameters can lead to very poor temperature control or bad power regulation.", "description": "Advanced parameters. Leave the default values if you don't know what you are doing.\nThese parameters can lead to very poor temperature control or bad power regulation.",
"data": { "data": {
"minimal_activation_delay": "Minimal activation delay", "minimal_activation_delay": "Minimum activation delay",
"security_delay_min": "Safety delay (in minutes)", "security_delay_min": "Safety delay (in minutes)",
"security_min_on_percent": "Minimal power percent to enable safety mode", "security_min_on_percent": "Minimum power percent to enable safety mode",
"security_default_on_percent": "Power percent to use in safety mode", "security_default_on_percent": "Power percent to use in safety mode",
"use_advanced_central_config": "Use central advanced configuration" "use_advanced_central_config": "Use central advanced configuration"
}, },
"data_description": { "data_description": {
"minimal_activation_delay": "Delay in seconds under which the equipment will not be activated", "minimal_activation_delay": "Delay in seconds under which the equipment will not be activated",
"security_delay_min": "Maximum allowed delay in minutes between two temperature measurements. Above this delay the thermostat will turn to a safety off state", "security_delay_min": "Maximum allowed delay in minutes between two temperature measurements. Above this delay the thermostat will turn to a safety off state",
"security_min_on_percent": "Minimal heating percent value for safety preset activation. Below this amount of power percent the thermostat won't go into safety preset", "security_min_on_percent": "Minimum heating percent value for safety preset activation. Below this amount of power percent the thermostat won't go into safety preset",
"security_default_on_percent": "The default heating power percent value in safety preset. Set to 0 to switch off heater in safety preset", "security_default_on_percent": "The default heating power percent value in safety preset. Set to 0 to switch off heater in safety preset",
"use_advanced_central_config": "Check to use the central advanced configuration. Uncheck to use a specific advanced configuration for this VTherm" "use_advanced_central_config": "Check to use the central advanced configuration. Uncheck to use a specific advanced configuration for this VTherm"
} }

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()
@@ -481,8 +486,8 @@ class UnderlyingClimate(UnderlyingEntity):
self._underlying_climate, self._underlying_climate,
) )
else: else:
_LOGGER.error( _LOGGER.info(
"%s - Cannot find the underlying climate entity: %s. Thermostat will not be operational", "%s - Cannot find the underlying climate entity: %s. Thermostat will not be operational. Will try later.",
self, self,
self.entity_id, self.entity_id,
) )
@@ -775,15 +780,19 @@ class UnderlyingValve(UnderlyingEntity):
"""Send the percent open to the underlying valve""" """Send the percent open to the underlying valve"""
# This may fails if called after shutdown # This may fails if called after shutdown
try: try:
data = {ATTR_ENTITY_ID: self._entity_id, "value": self._percent_open} data = {"value": self._percent_open}
target = {ATTR_ENTITY_ID: self._entity_id}
domain = self._entity_id.split(".")[0] domain = self._entity_id.split(".")[0]
await self._hass.services.async_call( await self._hass.services.async_call(
domain, domain=domain,
SERVICE_SET_VALUE, service=SERVICE_SET_VALUE,
data, service_data=data,
target=target,
) )
except ServiceNotFound as err: except ServiceNotFound as err:
_LOGGER.error(err) _LOGGER.error(err)
# This could happens in unit test if input_number domain is not yet loaded
# raise err
async def turn_off(self): async def turn_off(self):
"""Turn heater toggleable device off.""" """Turn heater toggleable device off."""

View File

@@ -57,6 +57,7 @@ class VersatileThermostatAPI(dict):
self._threshold_number_entity = None self._threshold_number_entity = None
self._nb_active_number_entity = None self._nb_active_number_entity = None
self._central_configuration = None self._central_configuration = None
self._central_mode_select = None
# A dict that will store all Number entities which holds the temperature # A dict that will store all Number entities which holds the temperature
self._number_temperatures = dict() self._number_temperatures = dict()
@@ -149,8 +150,8 @@ class VersatileThermostatAPI(dict):
return entity.state return entity.state
return None return None
async def init_vtherm_links(self, only_use_central=False): async def init_vtherm_links(self):
"""INitialize all VTherms entities links """Initialize all VTherms entities links
This method is called when HA is fully started (and all entities should be initialized) This method is called when HA is fully started (and all entities should be initialized)
Or when we need to reload all VTherm links (with Number temp entities, central boiler, ...) Or when we need to reload all VTherm links (with Number temp entities, central boiler, ...)
""" """
@@ -162,12 +163,34 @@ class VersatileThermostatAPI(dict):
) )
if component: if component:
for entity in component.entities: for entity in component.entities:
if hasattr(entity, "init_presets"): # if hasattr(entity, "init_presets"):
if ( # if (
only_use_central is False # only_use_central is False
or entity.use_central_config_temperature # or entity.use_central_config_temperature
): # ):
await entity.init_presets(self.find_central_configuration()) # await entity.init_presets(self.find_central_configuration())
# A little hack to test if the climate is a VTherm. Cannot use isinstance due to circular dependency of BaseThermostat
if (
entity.device_info
and entity.device_info.get("model", None) == DOMAIN
):
await entity.async_startup(self.find_central_configuration())
async def init_vtherm_preset_with_central(self):
"""Init all VTherm presets when the VTherm uses central temperature"""
# Initialization of all preset for all VTherm
component: EntityComponent[ClimateEntity] = self._hass.data.get(
CLIMATE_DOMAIN, None
)
if component:
for entity in component.entities:
if (
entity.device_info
and entity.device_info.get("model", None) == DOMAIN
and entity.use_central_config_temperature
):
await entity.init_presets(self.find_central_configuration())
async def reload_central_boiler_binary_listener(self): async def reload_central_boiler_binary_listener(self):
"""Reloads the BinarySensor entity which listen to the number of """Reloads the BinarySensor entity which listen to the number of
@@ -180,6 +203,27 @@ class VersatileThermostatAPI(dict):
if self._nb_active_number_entity is not None: if self._nb_active_number_entity is not None:
await self._nb_active_number_entity.listen_vtherms_entities() await self._nb_active_number_entity.listen_vtherms_entities()
def register_central_mode_select(self, central_mode_select):
"""Register the select entity which holds the central_mode"""
self._central_mode_select = central_mode_select
async def notify_central_mode_change(self, old_central_mode: str | None = None):
"""Notify all VTherm that the central_mode have change"""
if self._central_mode_select is None:
return
# Update all VTherm states
component: EntityComponent[ClimateEntity] = self.hass.data[CLIMATE_DOMAIN]
for entity in component.entities:
if entity.device_info and entity.device_info.get("model", None) == DOMAIN:
_LOGGER.debug(
"Changing the central_mode. We have find %s to update",
entity.name,
)
await entity.check_central_mode(
self._central_mode_select.state, old_central_mode
)
@property @property
def self_regulation_expert(self): def self_regulation_expert(self):
"""Get the self regulation params""" """Get the self regulation params"""
@@ -229,6 +273,14 @@ class VersatileThermostatAPI(dict):
return None return None
return int(self._threshold_number_entity.native_value) return int(self._threshold_number_entity.native_value)
@property
def central_mode(self) -> str | None:
"""Get the current central mode or None"""
if self._central_mode_select:
return self._central_mode_select.state
else:
return None
@property @property
def hass(self): def hass(self):
"""Get the HomeAssistant object""" """Get the HomeAssistant object"""

View File

@@ -1,7 +1,9 @@
# pylint: disable=wildcard-import, unused-wildcard-import, protected-access, unused-argument, line-too-long # pylint: disable=wildcard-import, unused-wildcard-import, protected-access, unused-argument, line-too-long
""" Test the Window management """ """ Test the Window management """
from unittest.mock import patch, call import asyncio
from unittest.mock import patch, call, PropertyMock
from datetime import datetime, timedelta from datetime import datetime, timedelta
import logging import logging
@@ -718,3 +720,207 @@ async def test_bug_272(
), ),
] ]
) )
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_bug_407(hass: HomeAssistant, skip_hass_states_is_state):
"""Test the followin case in power management:
1. a heater is active (heating). So the power consumption takes the heater power into account. We suppose the power consumption is near the threshold,
2. the user switch preset let's say from Comfort to Boost,
3. expected: no shredding should occur because the heater was already active,
4. constated: the heater goes into shredding.
"""
entry = MockConfigEntry(
domain=DOMAIN,
title="TheOverSwitchMockName",
unique_id="uniqueId",
data={
CONF_NAME: "TheOverSwitchMockName",
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_SWITCH,
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
CONF_CYCLE_MIN: 5,
CONF_TEMP_MIN: 15,
CONF_TEMP_MAX: 30,
"eco_temp": 17,
"comfort_temp": 18,
"boost_temp": 19,
CONF_USE_WINDOW_FEATURE: False,
CONF_USE_MOTION_FEATURE: False,
CONF_USE_POWER_FEATURE: True,
CONF_USE_PRESENCE_FEATURE: False,
CONF_HEATER: "switch.mock_switch",
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
CONF_TPI_COEF_INT: 0.3,
CONF_TPI_COEF_EXT: 0.01,
CONF_MINIMAL_ACTIVATION_DELAY: 30,
CONF_SECURITY_DELAY_MIN: 5,
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
CONF_POWER_SENSOR: "sensor.mock_power_sensor",
CONF_MAX_POWER_SENSOR: "sensor.mock_power_max_sensor",
CONF_DEVICE_POWER: 100,
CONF_PRESET_POWER: 12,
},
)
entity: ThermostatOverSwitch = await create_thermostat(
hass, entry, "climate.theoverswitchmockname"
)
assert entity
tpi_algo = entity._prop_algorithm
assert tpi_algo
tz = get_tz(hass) # pylint: disable=invalid-name
now: datetime = datetime.now(tz=tz)
await send_temperature_change_event(entity, 16, now)
await send_ext_temperature_change_event(entity, 10, now)
# 1. An already active heater will not switch to overpowering
with patch(
"homeassistant.core.ServiceRegistry.async_call"
) as mock_service_call, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
new_callable=PropertyMock,
return_value=True,
):
await entity.async_set_hvac_mode(HVACMode.HEAT)
await entity.async_set_preset_mode(PRESET_COMFORT)
assert entity.hvac_mode is HVACMode.HEAT
assert entity.preset_mode is PRESET_COMFORT
assert entity.overpowering_state is None
assert entity.target_temperature == 18
# waits that the heater starts
await asyncio.sleep(0.1)
assert mock_service_call.call_count >= 1
assert entity.is_device_active is True
# Send power max mesurement
await send_max_power_change_event(entity, 110, datetime.now())
# Send power mesurement (theheater is already in the power measurement)
await send_power_change_event(entity, 100, datetime.now())
# No overpowering yet
assert await entity.check_overpowering() is False
# All configuration is complete and power is < power_max
assert entity.preset_mode is PRESET_COMFORT
assert entity.overpowering_state is False
assert entity.is_device_active is True
# 2. An already active heater that switch preset will not switch to overpowering
with patch(
"homeassistant.core.ServiceRegistry.async_call"
) as mock_service_call, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
new_callable=PropertyMock,
return_value=True,
):
# change preset to Boost
await entity.async_set_preset_mode(PRESET_BOOST)
# waits that the heater starts
await asyncio.sleep(0.1)
assert await entity.check_overpowering() is False
assert entity.hvac_mode is HVACMode.HEAT
assert entity.preset_mode is PRESET_BOOST
assert entity.overpowering_state is False
assert entity.target_temperature == 19
assert mock_service_call.call_count >= 1
# 3. if heater is stopped (is_device_active==False), then overpowering should be started
with patch(
"homeassistant.core.ServiceRegistry.async_call"
) as mock_service_call, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
new_callable=PropertyMock,
return_value=False,
):
# change preset to Boost
await entity.async_set_preset_mode(PRESET_COMFORT)
# waits that the heater starts
await asyncio.sleep(0.1)
assert await entity.check_overpowering() is True
assert entity.hvac_mode is HVACMode.HEAT
assert entity.preset_mode is PRESET_POWER
assert entity.overpowering_state is True
async def test_bug_339(
hass: HomeAssistant,
# skip_hass_states_is_state,
init_central_config_with_boiler_fixture,
):
"""Test that the counter of active Vtherm in central boiler is
correctly updated with underlying is in auto and device is active
"""
api = VersatileThermostatAPI.get_vtherm_api(hass)
climate1 = MockClimate(
hass=hass,
unique_id="climate1",
name="theClimate1",
hvac_mode=HVACMode.AUTO,
hvac_modes=[HVACMode.AUTO, HVACMode.OFF, HVACMode.HEAT, HVACMode.COOL],
hvac_action=HVACAction.HEATING,
)
entry = MockConfigEntry(
domain=DOMAIN,
title="TheOverClimateMockName",
unique_id="uniqueId",
data={
CONF_NAME: "TheOverClimateMockName",
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_CLIMATE,
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
CONF_CYCLE_MIN: 5,
CONF_TEMP_MIN: 8,
CONF_TEMP_MAX: 18,
"frost_temp": 10,
"eco_temp": 17,
"comfort_temp": 18,
"boost_temp": 21,
CONF_USE_WINDOW_FEATURE: False,
CONF_USE_MOTION_FEATURE: False,
CONF_USE_POWER_FEATURE: False,
CONF_USE_PRESENCE_FEATURE: False,
CONF_CLIMATE: climate1.entity_id,
CONF_MINIMAL_ACTIVATION_DELAY: 30,
CONF_SECURITY_DELAY_MIN: 5,
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
CONF_SECURITY_DEFAULT_ON_PERCENT: 0.1,
CONF_USE_MAIN_CENTRAL_CONFIG: True,
CONF_USE_PRESETS_CENTRAL_CONFIG: True,
CONF_USE_ADVANCED_CENTRAL_CONFIG: True,
CONF_USED_BY_CENTRAL_BOILER: True,
},
)
with patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
return_value=climate1,
):
entity: ThermostatOverValve = await create_thermostat(
hass, entry, "climate.theoverclimatemockname"
)
assert entity
assert entity.name == "TheOverClimateMockName"
assert entity.is_over_climate
assert entity.underlying_entities[0].entity_id == "climate.climate1"
assert api.nb_active_device_for_boiler_threshold == 1
await entity.async_set_hvac_mode(HVACMode.AUTO)
# Simulate a state change in underelying
await api.nb_active_device_for_boiler_entity.calculate_nb_active_devices(None)
# The VTherm should be active
assert entity.underlying_entity(0).is_device_active is True
assert entity.is_device_active is True
assert api.nb_active_device_for_boiler == 1
entity.remove_thermostat()

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

View File

@@ -181,14 +181,18 @@ async def test_over_valve_full_start(
mock_service_call.assert_has_calls( mock_service_call.assert_has_calls(
[ [
call.async_call( call.async_call(
"number", domain="number",
"set_value", service="set_value",
{"entity_id": "number.mock_valve", "value": 90}, service_data={"value": 90},
target={"entity_id": "number.mock_valve"},
# {"entity_id": "number.mock_valve", "value": 90},
), ),
call.async_call( call.async_call(
"number", domain="number",
"set_value", service="set_value",
{"entity_id": "number.mock_valve", "value": 98}, service_data={"value": 98},
target={"entity_id": "number.mock_valve"},
# {"entity_id": "number.mock_valve", "value": 98},
), ),
] ]
) )
@@ -241,9 +245,10 @@ async def test_over_valve_full_start(
mock_service_call.assert_has_calls( mock_service_call.assert_has_calls(
[ [
call.async_call( call.async_call(
"number", domain="number",
"set_value", service="set_value",
{"entity_id": "number.mock_valve", "value": 10}, service_data={"value": 10},
target={"entity_id": "number.mock_valve"},
) )
] ]
) )
@@ -254,20 +259,16 @@ async def test_over_valve_full_start(
mock_service_call.assert_has_calls( mock_service_call.assert_has_calls(
[ [
call.async_call( call.async_call(
"number", domain="number",
"set_value", service="set_value",
{ service_data={"value": 10},
"entity_id": "number.mock_valve", target={"entity_id": "number.mock_valve"}, # the min allowed value
"value": 10,
}, # the min allowed value
), ),
call.async_call( call.async_call(
"number", domain="number",
"set_value", service="set_value",
{ service_data={"value": 50}, # the min allowed value
"entity_id": "number.mock_valve", target={"entity_id": "number.mock_valve"},
"value": 50,
}, # the max allowed value
), ),
] ]
) )
@@ -466,9 +467,10 @@ async def test_over_valve_regulation(
mock_service_call.assert_has_calls( mock_service_call.assert_has_calls(
[ [
call.async_call( call.async_call(
"number", domain="number",
"set_value", service="set_value",
{"entity_id": "number.mock_valve", "value": 90}, service_data={"value": 90},
target={"entity_id": "number.mock_valve"},
), ),
] ]
) )
@@ -524,9 +526,10 @@ async def test_over_valve_regulation(
mock_service_call.assert_has_calls( mock_service_call.assert_has_calls(
[ [
call.async_call( call.async_call(
"number", domain="number",
"set_value", service="set_value",
{"entity_id": "number.mock_valve", "value": 96}, service_data={"value": 96},
target={"entity_id": "number.mock_valve"},
), ),
] ]
) )

View File

@@ -168,6 +168,7 @@ async def test_window_management_time_enough(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off" "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off, patch( ) as mock_heater_off, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active", "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
new_callable=PropertyMock,
return_value=False, return_value=False,
): ):
await send_temperature_change_event(entity, 15, datetime.now()) await send_temperature_change_event(entity, 15, datetime.now())
@@ -188,6 +189,7 @@ async def test_window_management_time_enough(
"homeassistant.helpers.condition.state", return_value=True "homeassistant.helpers.condition.state", return_value=True
) as mock_condition, patch( ) as mock_condition, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active", "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
new_callable=PropertyMock,
return_value=True, return_value=True,
): ):
await send_window_change_event(entity, True, False, datetime.now()) await send_window_change_event(entity, True, False, datetime.now())
@@ -216,6 +218,7 @@ async def test_window_management_time_enough(
"homeassistant.helpers.condition.state", return_value=True "homeassistant.helpers.condition.state", return_value=True
) as mock_condition, patch( ) as mock_condition, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active", "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
new_callable=PropertyMock,
return_value=False, return_value=False,
): ):
try_function = await send_window_change_event( try_function = await send_window_change_event(
@@ -319,6 +322,7 @@ async def test_window_auto_fast(hass: HomeAssistant, skip_hass_states_is_state):
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off" "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off, patch( ) as mock_heater_off, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active", "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
new_callable=PropertyMock,
return_value=True, return_value=True,
): ):
event_timestamp = event_timestamp + timedelta(minutes=1) event_timestamp = event_timestamp + timedelta(minutes=1)
@@ -341,6 +345,7 @@ async def test_window_auto_fast(hass: HomeAssistant, skip_hass_states_is_state):
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off" "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off, patch( ) as mock_heater_off, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active", "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
new_callable=PropertyMock,
return_value=True, return_value=True,
): ):
event_timestamp = event_timestamp + timedelta(minutes=1) event_timestamp = event_timestamp + timedelta(minutes=1)
@@ -514,6 +519,7 @@ async def test_window_auto_fast_and_sensor(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off" "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off, patch( ) as mock_heater_off, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active", "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
new_callable=PropertyMock,
return_value=True, return_value=True,
): ):
event_timestamp = event_timestamp + timedelta(minutes=1) event_timestamp = event_timestamp + timedelta(minutes=1)
@@ -536,6 +542,7 @@ async def test_window_auto_fast_and_sensor(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off" "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off, patch( ) as mock_heater_off, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active", "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
new_callable=PropertyMock,
return_value=True, return_value=True,
): ):
event_timestamp = event_timestamp + timedelta(minutes=1) event_timestamp = event_timestamp + timedelta(minutes=1)
@@ -646,6 +653,7 @@ async def test_window_auto_auto_stop(hass: HomeAssistant, skip_hass_states_is_st
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.set_hvac_mode" "custom_components.versatile_thermostat.underlyings.UnderlyingClimate.set_hvac_mode"
) as mock_set_hvac_mode, patch( ) as mock_set_hvac_mode, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active", "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
new_callable=PropertyMock,
return_value=True, return_value=True,
): ):
event_timestamp = event_timestamp + timedelta(minutes=1) event_timestamp = event_timestamp + timedelta(minutes=1)
@@ -689,7 +697,8 @@ async def test_window_auto_auto_stop(hass: HomeAssistant, skip_hass_states_is_st
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.set_hvac_mode" "custom_components.versatile_thermostat.underlyings.UnderlyingClimate.set_hvac_mode"
) as mock_set_hvac_mode, patch( ) as mock_set_hvac_mode, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active", "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
return_value=False, new_callable=PropertyMock,
return_value=True,
): ):
# simulate the expiration of the delay # simulate the expiration of the delay
await dearm_window_auto(None) await dearm_window_auto(None)
@@ -783,6 +792,7 @@ async def test_window_auto_no_on_percent(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off" "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off, patch( ) as mock_heater_off, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active", "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
new_callable=PropertyMock,
return_value=True, return_value=True,
): ):
event_timestamp = event_timestamp + timedelta(minutes=1) event_timestamp = event_timestamp + timedelta(minutes=1)
@@ -805,6 +815,7 @@ async def test_window_auto_no_on_percent(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off" "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off, patch( ) as mock_heater_off, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active", "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
new_callable=PropertyMock,
return_value=True, return_value=True,
): ):
event_timestamp = event_timestamp + timedelta(minutes=1) event_timestamp = event_timestamp + timedelta(minutes=1)
@@ -890,6 +901,7 @@ async def test_window_bypass(hass: HomeAssistant, skip_hass_states_is_state):
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off" "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off, patch( ) as mock_heater_off, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active", "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
new_callable=PropertyMock,
return_value=False, return_value=False,
): ):
await send_temperature_change_event(entity, 15, datetime.now()) await send_temperature_change_event(entity, 15, datetime.now())
@@ -916,6 +928,7 @@ async def test_window_bypass(hass: HomeAssistant, skip_hass_states_is_state):
"homeassistant.helpers.condition.state", return_value=True "homeassistant.helpers.condition.state", return_value=True
) as mock_condition, patch( ) as mock_condition, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active", "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
new_callable=PropertyMock,
return_value=True, return_value=True,
): ):
await send_window_change_event(entity, True, False, datetime.now()) await send_window_change_event(entity, True, False, datetime.now())
@@ -941,6 +954,7 @@ async def test_window_bypass(hass: HomeAssistant, skip_hass_states_is_state):
"homeassistant.helpers.condition.state", return_value=True "homeassistant.helpers.condition.state", return_value=True
) as mock_condition, patch( ) as mock_condition, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active", "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
new_callable=PropertyMock,
return_value=False, return_value=False,
): ):
try_function = await send_window_change_event( try_function = await send_window_change_event(
@@ -1038,6 +1052,7 @@ async def test_window_auto_bypass(hass: HomeAssistant, skip_hass_states_is_state
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off" "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off, patch( ) as mock_heater_off, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active", "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
new_callable=PropertyMock,
return_value=True, return_value=True,
): ):
event_timestamp = event_timestamp + timedelta(minutes=1) event_timestamp = event_timestamp + timedelta(minutes=1)
@@ -1062,6 +1077,7 @@ async def test_window_auto_bypass(hass: HomeAssistant, skip_hass_states_is_state
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off" "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off, patch( ) as mock_heater_off, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active", "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
new_callable=PropertyMock,
return_value=True, return_value=True,
): ):
event_timestamp = event_timestamp + timedelta(minutes=1) event_timestamp = event_timestamp + timedelta(minutes=1)
@@ -1145,6 +1161,7 @@ async def test_window_bypass_reactivate(hass: HomeAssistant, skip_hass_states_is
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off" "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off, patch( ) as mock_heater_off, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active", "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
new_callable=PropertyMock,
return_value=False, return_value=False,
): ):
await send_temperature_change_event(entity, 15, datetime.now()) await send_temperature_change_event(entity, 15, datetime.now())
@@ -1165,6 +1182,7 @@ async def test_window_bypass_reactivate(hass: HomeAssistant, skip_hass_states_is
"homeassistant.helpers.condition.state", return_value=True "homeassistant.helpers.condition.state", return_value=True
) as mock_condition, patch( ) as mock_condition, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active", "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
new_callable=PropertyMock,
return_value=True, return_value=True,
): ):
await send_window_change_event(entity, True, False, datetime.now()) await send_window_change_event(entity, True, False, datetime.now())
@@ -1191,6 +1209,7 @@ async def test_window_bypass_reactivate(hass: HomeAssistant, skip_hass_states_is
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off" "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off, patch( ) as mock_heater_off, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active", "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
new_callable=PropertyMock,
return_value=False, return_value=False,
): ):
await entity.service_set_window_bypass_state(True) await entity.service_set_window_bypass_state(True)
@@ -1588,6 +1607,7 @@ async def test_window_action_eco_temp(hass: HomeAssistant, skip_hass_states_is_s
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off" "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off, patch( ) as mock_heater_off, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active", "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
new_callable=PropertyMock,
return_value=True, return_value=True,
): ):
event_timestamp = event_timestamp + timedelta(minutes=1) event_timestamp = event_timestamp + timedelta(minutes=1)
@@ -1609,6 +1629,7 @@ async def test_window_action_eco_temp(hass: HomeAssistant, skip_hass_states_is_s
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off" "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off, patch( ) as mock_heater_off, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active", "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
new_callable=PropertyMock,
return_value=True, return_value=True,
): ):
event_timestamp = event_timestamp + timedelta(minutes=1) event_timestamp = event_timestamp + timedelta(minutes=1)
@@ -1783,6 +1804,7 @@ async def test_window_action_frost_temp(hass: HomeAssistant, skip_hass_states_is
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off" "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off, patch( ) as mock_heater_off, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active", "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
new_callable=PropertyMock,
return_value=True, return_value=True,
): ):
event_timestamp = event_timestamp + timedelta(minutes=1) event_timestamp = event_timestamp + timedelta(minutes=1)
@@ -1804,6 +1826,7 @@ async def test_window_action_frost_temp(hass: HomeAssistant, skip_hass_states_is
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off" "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off, patch( ) as mock_heater_off, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active", "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
new_callable=PropertyMock,
return_value=True, return_value=True,
): ):
event_timestamp = event_timestamp + timedelta(minutes=1) event_timestamp = event_timestamp + timedelta(minutes=1)