Compare commits

..

2 Commits

Author SHA1 Message Date
Jean-Marc Collin dc7739d53b Fix slope None 2024-12-08 17:18:02 +00:00
Jean-Marc Collin 729f263cc8 Fix 2024-12-08 17:05:39 +00:00
35 changed files with 179 additions and 1226 deletions
+4 -3
View File
@@ -5,7 +5,9 @@
"dockerfile": "Dockerfile" "dockerfile": "Dockerfile"
}, },
"name": "Versatile Thermostat integration", "name": "Versatile Thermostat integration",
"appPort": ["8123:8123"], "appPort": [
"8123:8123"
],
// "postCreateCommand": "container install", // "postCreateCommand": "container install",
"postCreateCommand": "./container dev-setup", "postCreateCommand": "./container dev-setup",
@@ -36,8 +38,7 @@
"vscode.markdown-math", "vscode.markdown-math",
"yzhang.markdown-all-in-one", "yzhang.markdown-all-in-one",
"github.vscode-github-actions", "github.vscode-github-actions",
"azuretools.vscode-docker", "azuretools.vscode-docker"
"huizhou.githd",
], ],
"settings": { "settings": {
"files.eol": "\n", "files.eol": "\n",
@@ -57,13 +57,10 @@ class AutoStartStopDetectionAlgorithm:
_accumulated_error: float = 0 _accumulated_error: float = 0
_error_threshold: float | None = None _error_threshold: float | None = None
_last_calculation_date: datetime | None = None _last_calculation_date: datetime | None = None
_last_switch_date: datetime | None = None
def __init__(self, level: TYPE_AUTO_START_STOP_LEVELS, vtherm_name) -> None: def __init__(self, level: TYPE_AUTO_START_STOP_LEVELS, vtherm_name) -> None:
"""Initalize a new algorithm with the right constants""" """Initalize a new algorithm with the right constants"""
self._vtherm_name = vtherm_name self._vtherm_name = vtherm_name
self._last_calculation_date = None
self._last_switch_date = None
self._init_level(level) self._init_level(level)
def _init_level(self, level: TYPE_AUTO_START_STOP_LEVELS): def _init_level(self, level: TYPE_AUTO_START_STOP_LEVELS):
@@ -146,26 +143,17 @@ class AutoStartStopDetectionAlgorithm:
temp_at_dt = current_temp + slope_min * self._dt temp_at_dt = current_temp + slope_min * self._dt
# Calculate the number of minute from last_switch
nb_minutes_since_last_switch = 999
if self._last_switch_date is not None:
nb_minutes_since_last_switch = (
now - self._last_switch_date
).total_seconds() / 60
# Check to turn-off # Check to turn-off
# When we hit the threshold, that mean we can turn off # When we hit the threshold, that mean we can turn off
if hvac_mode == HVACMode.HEAT: if hvac_mode == HVACMode.HEAT:
if ( if (
self._accumulated_error <= -self._error_threshold self._accumulated_error <= -self._error_threshold
and temp_at_dt >= target_temp + TEMP_HYSTERESIS and temp_at_dt >= target_temp + TEMP_HYSTERESIS
and nb_minutes_since_last_switch >= self._dt
): ):
_LOGGER.info( _LOGGER.info(
"%s - We need to stop, there is no need for heating for a long time.", "%s - We need to stop, there is no need for heating for a long time.",
self, self,
) )
self._last_switch_date = now
return AUTO_START_STOP_ACTION_OFF return AUTO_START_STOP_ACTION_OFF
else: else:
_LOGGER.debug("%s - nothing to do, we are heating", self) _LOGGER.debug("%s - nothing to do, we are heating", self)
@@ -175,13 +163,11 @@ class AutoStartStopDetectionAlgorithm:
if ( if (
self._accumulated_error >= self._error_threshold self._accumulated_error >= self._error_threshold
and temp_at_dt <= target_temp - TEMP_HYSTERESIS and temp_at_dt <= target_temp - TEMP_HYSTERESIS
and nb_minutes_since_last_switch >= self._dt
): ):
_LOGGER.info( _LOGGER.info(
"%s - We need to stop, there is no need for cooling for a long time.", "%s - We need to stop, there is no need for cooling for a long time.",
self, self,
) )
self._last_switch_date = now
return AUTO_START_STOP_ACTION_OFF return AUTO_START_STOP_ACTION_OFF
else: else:
_LOGGER.debug( _LOGGER.debug(
@@ -192,15 +178,11 @@ class AutoStartStopDetectionAlgorithm:
# check to turn on # check to turn on
if hvac_mode == HVACMode.OFF and saved_hvac_mode == HVACMode.HEAT: if hvac_mode == HVACMode.OFF and saved_hvac_mode == HVACMode.HEAT:
if ( if temp_at_dt <= target_temp - TEMP_HYSTERESIS:
temp_at_dt <= target_temp - TEMP_HYSTERESIS
and nb_minutes_since_last_switch >= self._dt
):
_LOGGER.info( _LOGGER.info(
"%s - We need to start, because it will be time to heat", "%s - We need to start, because it will be time to heat",
self, self,
) )
self._last_switch_date = now
return AUTO_START_STOP_ACTION_ON return AUTO_START_STOP_ACTION_ON
else: else:
_LOGGER.debug( _LOGGER.debug(
@@ -210,15 +192,11 @@ class AutoStartStopDetectionAlgorithm:
return AUTO_START_STOP_ACTION_NOTHING return AUTO_START_STOP_ACTION_NOTHING
if hvac_mode == HVACMode.OFF and saved_hvac_mode == HVACMode.COOL: if hvac_mode == HVACMode.OFF and saved_hvac_mode == HVACMode.COOL:
if ( if temp_at_dt >= target_temp + TEMP_HYSTERESIS:
temp_at_dt >= target_temp + TEMP_HYSTERESIS
and nb_minutes_since_last_switch >= self._dt
):
_LOGGER.info( _LOGGER.info(
"%s - We need to start, because it will be time to cool", "%s - We need to start, because it will be time to cool",
self, self,
) )
self._last_switch_date = now
return AUTO_START_STOP_ACTION_ON return AUTO_START_STOP_ACTION_ON
else: else:
_LOGGER.debug( _LOGGER.debug(
@@ -257,10 +235,5 @@ class AutoStartStopDetectionAlgorithm:
"""Get the level value""" """Get the level value"""
return self._level return self._level
@property
def last_switch_date(self) -> datetime | None:
"""Get the last of the last switch"""
return self._last_switch_date
def __str__(self) -> str: def __str__(self) -> str:
return f"AutoStartStopDetectionAlgorithm-{self._vtherm_name}" return f"AutoStartStopDetectionAlgorithm-{self._vtherm_name}"
@@ -82,10 +82,6 @@ T = TypeVar("T", bound=UnderlyingEntity)
class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]): 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."""
# breaking change with 2024.12 climate workaround
_attr_swing_horizontal_modes = []
_attr_swing_horizontal_mode = ""
_entity_component_unrecorded_attributes = ( _entity_component_unrecorded_attributes = (
ClimateEntity._entity_component_unrecorded_attributes.union( ClimateEntity._entity_component_unrecorded_attributes.union(
frozenset( frozenset(
@@ -131,13 +127,12 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
"max_power_sensor_entity_id", "max_power_sensor_entity_id",
"temperature_unit", "temperature_unit",
"is_device_active", "is_device_active",
"device_actives", "nb_device_actives",
"target_temperature_step", "target_temperature_step",
"is_used_by_central_boiler", "is_used_by_central_boiler",
"temperature_slope", "temperature_slope",
"max_on_percent", "max_on_percent",
"have_valve_regulation", "have_valve_regulation",
"last_change_time_from_vtherm",
} }
) )
) )
@@ -220,9 +215,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
self._current_tz = dt_util.get_time_zone(self._hass.config.time_zone) self._current_tz = dt_util.get_time_zone(self._hass.config.time_zone)
# Last change time is the datetime of the last change sent by VTherm to the device self._last_change_time = None
# it is used in `over_cliamte` when a state have change from underlying to avoid loops
self._last_change_time_from_vtherm = None
self._underlyings: list[T] = [] self._underlyings: list[T] = []
@@ -752,7 +745,14 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
self.hass.create_task(self._check_initial_state()) self.hass.create_task(self._check_initial_state())
self.reset_last_change_time_from_vtherm() 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"""
@@ -996,19 +996,14 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
return True return True
return False return False
@property
def device_actives(self) -> int:
"""Calculate the active devices"""
ret = []
for under in self._underlyings:
if under.is_device_active:
ret.append(under.entity_id)
return ret
@property @property
def nb_device_actives(self) -> int: def nb_device_actives(self) -> int:
"""Calculate the number of active devices""" """Calculate the number of active devices"""
return len(self.device_actives) ret = 0
for under in self._underlyings:
if under.is_device_active:
ret += 1
return ret
@property @property
def current_temperature(self) -> float | None: def current_temperature(self) -> float | None:
@@ -1219,7 +1214,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
return return
def save_state(): def save_state():
self.reset_last_change_time_from_vtherm() self.reset_last_change_time()
self.update_custom_attributes() self.update_custom_attributes()
self.async_write_ha_state() self.async_write_ha_state()
self.send_event(EventType.HVAC_MODE_EVENT, {"hvac_mode": self._hvac_mode}) self.send_event(EventType.HVAC_MODE_EVENT, {"hvac_mode": self._hvac_mode})
@@ -1269,7 +1264,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
): ):
"""Set new preset mode.""" """Set new preset mode."""
# We accept a new preset when: # Wer accept a new preset when:
# 1. last_central_mode is not set, # 1. last_central_mode is not set,
# 2. or last_central_mode is AUTO, # 2. or last_central_mode is AUTO,
# 3. or last_central_mode is CENTRAL_MODE_FROST_PROTECTION and preset_mode is PRESET_FROST_PROTECTION (to be abel to re-set the preset_mode) # 3. or last_central_mode is CENTRAL_MODE_FROST_PROTECTION and preset_mode is PRESET_FROST_PROTECTION (to be abel to re-set the preset_mode)
@@ -1326,7 +1321,6 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
return return
old_preset_mode = self._attr_preset_mode old_preset_mode = self._attr_preset_mode
recalculate = True
if preset_mode == PRESET_NONE: if preset_mode == PRESET_NONE:
self._attr_preset_mode = PRESET_NONE self._attr_preset_mode = PRESET_NONE
if self._saved_target_temp: if self._saved_target_temp:
@@ -1338,18 +1332,10 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
if self._attr_preset_mode == PRESET_NONE: if self._attr_preset_mode == PRESET_NONE:
self._saved_target_temp = self._target_temp self._saved_target_temp = self._target_temp
self._attr_preset_mode = preset_mode self._attr_preset_mode = preset_mode
# Switch the temperature if window is not 'on'
if self.window_state != STATE_ON:
await self._async_internal_set_temperature( await self._async_internal_set_temperature(
self.find_preset_temp(preset_mode) self.find_preset_temp(preset_mode)
) )
else:
# Window is on, so we just save the new expected temp
# so that closing the window will restore it
recalculate = False
self._saved_target_temp = self.find_preset_temp(preset_mode)
if recalculate:
self.reset_last_temperature_time(old_preset_mode) self.reset_last_temperature_time(old_preset_mode)
if overwrite_saved_preset: if overwrite_saved_preset:
@@ -1360,14 +1346,12 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
if self._attr_preset_mode != old_preset_mode: if self._attr_preset_mode != old_preset_mode:
self.send_event(EventType.PRESET_EVENT, {"preset": self._attr_preset_mode}) self.send_event(EventType.PRESET_EVENT, {"preset": self._attr_preset_mode})
def reset_last_change_time_from_vtherm( def reset_last_change_time(
self, old_preset_mode: str | None = None self, old_preset_mode: str | None = None
): # pylint: disable=unused-argument ): # pylint: disable=unused-argument
"""Reset to now the last change time""" """Reset to now the last change time"""
self._last_change_time_from_vtherm = self.now self._last_change_time = self.now
_LOGGER.debug( _LOGGER.debug("%s - last_change_time is now %s", self, self._last_change_time)
"%s - last_change_time is now %s", self, self._last_change_time_from_vtherm
)
def reset_last_temperature_time(self, old_preset_mode: str | None = None): def reset_last_temperature_time(self, old_preset_mode: str | None = None):
"""Reset to now the last temperature time if conditions are satisfied""" """Reset to now the last temperature time if conditions are satisfied"""
@@ -1464,20 +1448,19 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
_LOGGER.info("%s - Set target temp: %s", self, temperature) _LOGGER.info("%s - Set target temp: %s", self, temperature)
if temperature is None: if temperature is None:
return return
self._attr_preset_mode = PRESET_NONE
if self.window_state != STATE_ON:
await self._async_internal_set_temperature(temperature) await self._async_internal_set_temperature(temperature)
self._attr_preset_mode = PRESET_NONE
self.recalculate() self.recalculate()
self.reset_last_change_time_from_vtherm() self.reset_last_change_time()
await self.async_control_heating(force=True) await self.async_control_heating(force=True)
else:
self._saved_target_temp = temperature
async def _async_internal_set_temperature(self, temperature: float): async def _async_internal_set_temperature(self, temperature: float):
"""Set the target temperature and the target temperature of underlying climate if any""" """Set the target temperature and the target temperature of underlying climate if any
For testing purpose you can pass an event_timestamp.
"""
if temperature: if temperature:
self._target_temp = temperature self._target_temp = temperature
return
def get_state_date_or_now(self, state: State) -> datetime: def get_state_date_or_now(self, state: State) -> datetime:
"""Extract the last_changed state from State or return now if not available""" """Extract the last_changed state from State or return now if not available"""
@@ -1537,8 +1520,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
self._last_temperature_measure = self.get_last_updated_date_or_now( self._last_temperature_measure = self.get_last_updated_date_or_now(
new_state new_state
) )
# issue 690 - don't reset the last change time on lastSeen self.reset_last_change_time()
# self.reset_last_change_time_from_vtherm()
_LOGGER.debug( _LOGGER.debug(
"%s - new last_temperature_measure is now: %s", "%s - new last_temperature_measure is now: %s",
self, self,
@@ -2694,7 +2676,6 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
"timezone": str(self._current_tz), "timezone": str(self._current_tz),
"temperature_unit": self.temperature_unit, "temperature_unit": self.temperature_unit,
"is_device_active": self.is_device_active, "is_device_active": self.is_device_active,
"device_actives": self.device_actives,
"nb_device_actives": self.nb_device_actives, "nb_device_actives": self.nb_device_actives,
"ema_temp": self._ema_temp, "ema_temp": self._ema_temp,
"is_used_by_central_boiler": self.is_used_by_central_boiler, "is_used_by_central_boiler": self.is_used_by_central_boiler,
@@ -2702,13 +2683,6 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
"hvac_off_reason": self.hvac_off_reason, "hvac_off_reason": self.hvac_off_reason,
"max_on_percent": self._max_on_percent, "max_on_percent": self._max_on_percent,
"have_valve_regulation": self.have_valve_regulation, "have_valve_regulation": self.have_valve_regulation,
"last_change_time_from_vtherm": (
self._last_change_time_from_vtherm.astimezone(
self._current_tz
).isoformat()
if self._last_change_time_from_vtherm is not None
else None
),
} }
_LOGGER.debug( _LOGGER.debug(
@@ -118,7 +118,7 @@ async def async_setup_entry(
SERVICE_SET_AUTO_REGULATION_MODE, SERVICE_SET_AUTO_REGULATION_MODE,
{ {
vol.Required("auto_regulation_mode"): vol.In( vol.Required("auto_regulation_mode"): vol.In(
["None", "Light", "Medium", "Strong", "Slow", "Expert"] ["None", "Light", "Medium", "Strong", "Slow"]
), ),
}, },
"service_set_auto_regulation_mode", "service_set_auto_regulation_mode",
@@ -259,21 +259,6 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
if not self.check_valve_regulation_nb_entities(data, step_id): if not self.check_valve_regulation_nb_entities(data, step_id):
raise ValveRegulationNbEntitiesIncorrect() raise ValveRegulationNbEntitiesIncorrect()
# Check that the min_opening_degrees is correctly set
raw_list = data.get(CONF_MIN_OPENING_DEGREES, None)
if raw_list:
try:
# Validation : Convertir la liste saisie
int_list = [int(x.strip()) for x in raw_list.split(",")]
# Optionnel : Vérifiez des conditions supplémentaires sur la liste
if any(x < 0 for x in int_list):
raise ValueError
except ValueError as exc:
raise ValveRegulationMinOpeningDegreesIncorrect(
CONF_MIN_OPENING_DEGREES
) from exc
def check_config_complete(self, infos) -> bool: def check_config_complete(self, infos) -> bool:
"""True if the config is now complete (ie all mandatory attributes are set)""" """True if the config is now complete (ie all mandatory attributes are set)"""
is_central_config = ( is_central_config = (
@@ -414,8 +399,6 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
errors["base"] = "configuration_not_complete" errors["base"] = "configuration_not_complete"
except ValveRegulationNbEntitiesIncorrect as err: except ValveRegulationNbEntitiesIncorrect as err:
errors["base"] = "valve_regulation_nb_entities_incorrect" errors["base"] = "valve_regulation_nb_entities_incorrect"
except ValveRegulationMinOpeningDegreesIncorrect as err:
errors[str(err)] = "min_opening_degrees_format"
except Exception: # pylint: disable=broad-except except Exception: # pylint: disable=broad-except
_LOGGER.exception("Unexpected exception") _LOGGER.exception("Unexpected exception")
errors["base"] = "unknown" errors["base"] = "unknown"
@@ -915,7 +898,7 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
) )
class VersatileThermostatConfigFlow( # pylint: disable=abstract-method class VersatileThermostatConfigFlow(
VersatileThermostatBaseConfigFlow, HAConfigFlow, domain=DOMAIN VersatileThermostatBaseConfigFlow, HAConfigFlow, domain=DOMAIN
): ):
"""Handle a config flow for Versatile Thermostat.""" """Handle a config flow for Versatile Thermostat."""
@@ -929,8 +912,6 @@ class VersatileThermostatConfigFlow( # pylint: disable=abstract-method
@callback @callback
def async_get_options_flow(config_entry: ConfigEntry): def async_get_options_flow(config_entry: ConfigEntry):
"""Get options flow for this handler""" """Get options flow for this handler"""
# #713 doesn't work as explained here:https://developers.home-assistant.io/blog/2024/11/12/options-flow
# should be - return VersatileThermostatOptionsFlowHandler() but hass is not initialized
return VersatileThermostatOptionsFlowHandler(config_entry) return VersatileThermostatOptionsFlowHandler(config_entry)
async def async_step_finalize(self, _): async def async_step_finalize(self, _):
@@ -949,12 +930,8 @@ class VersatileThermostatOptionsFlowHandler(
def __init__(self, config_entry: ConfigEntry) -> None: def __init__(self, config_entry: ConfigEntry) -> None:
"""Initialize options flow.""" """Initialize options flow."""
self._conf_app_id: str | None = None
super().__init__(config_entry.data.copy()) super().__init__(config_entry.data.copy())
# #713 self.config_entry = config_entry
# self.config_entry = config_entry
_LOGGER.debug( _LOGGER.debug(
"CTOR VersatileThermostatOptionsFlowHandler info: %s, entry_id: %s", "CTOR VersatileThermostatOptionsFlowHandler info: %s, entry_id: %s",
self._infos, self._infos,
@@ -219,7 +219,6 @@ STEP_VALVE_REGULATION = vol.Schema( # pylint: disable=invalid-name
PROPORTIONAL_FUNCTION_TPI, PROPORTIONAL_FUNCTION_TPI,
] ]
), ),
vol.Optional(CONF_MIN_OPENING_DEGREES, default=""): str,
} }
) )
@@ -123,7 +123,6 @@ CONF_STEP_TEMPERATURE = "step_temperature"
CONF_OFFSET_CALIBRATION_LIST = "offset_calibration_entity_ids" CONF_OFFSET_CALIBRATION_LIST = "offset_calibration_entity_ids"
CONF_OPENING_DEGREE_LIST = "opening_degree_entity_ids" CONF_OPENING_DEGREE_LIST = "opening_degree_entity_ids"
CONF_CLOSING_DEGREE_LIST = "closing_degree_entity_ids" CONF_CLOSING_DEGREE_LIST = "closing_degree_entity_ids"
CONF_MIN_OPENING_DEGREES = "min_opening_degrees"
# Deprecated # Deprecated
CONF_HEATER = "heater_entity_id" CONF_HEATER = "heater_entity_id"
@@ -553,10 +552,6 @@ class ValveRegulationNbEntitiesIncorrect(HomeAssistantError):
The number of specific entities is incorrect.""" The number of specific entities is incorrect."""
class ValveRegulationMinOpeningDegreesIncorrect(HomeAssistantError):
"""Error to indicate that the minimal opening degrees is not a list of int separated by coma"""
class overrides: # pylint: disable=invalid-name class overrides: # pylint: disable=invalid-name
"""An annotation to inform overrides""" """An annotation to inform overrides"""
@@ -14,6 +14,6 @@
"quality_scale": "silver", "quality_scale": "silver",
"requirements": [], "requirements": [],
"ssdp": [], "ssdp": [],
"version": "6.8.4", "version": "6.8.0",
"zeroconf": [] "zeroconf": []
} }
@@ -644,10 +644,6 @@ class NbActiveDeviceForBoilerSensor(SensorEntity):
"""Representation of the threshold of the number of VTherm """Representation of the threshold of the number of VTherm
which should be active to activate the boiler""" which should be active to activate the boiler"""
_entity_component_unrecorded_attributes = SensorEntity._entity_component_unrecorded_attributes.union( # pylint: disable=protected-access
frozenset({"active_device_ids"})
)
def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None: def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None:
"""Initialize the energy sensor""" """Initialize the energy sensor"""
self._hass = hass self._hass = hass
@@ -657,14 +653,6 @@ class NbActiveDeviceForBoilerSensor(SensorEntity):
self._attr_unique_id = "nb_device_active_boiler" self._attr_unique_id = "nb_device_active_boiler"
self._attr_value = self._attr_native_value = None # default value self._attr_value = self._attr_native_value = None # default value
self._entities = [] self._entities = []
self._attr_active_device_ids = [] # Holds the entity ids of active devices
@property
def extra_state_attributes(self) -> dict:
"""Return additional attributes for the sensor."""
return {
"active_device_ids": self._attr_active_device_ids,
}
@property @property
def icon(self) -> str | None: def icon(self) -> str | None:
@@ -730,19 +718,19 @@ class NbActiveDeviceForBoilerSensor(SensorEntity):
self.calculate_nb_active_devices, self.calculate_nb_active_devices,
) )
_LOGGER.info( _LOGGER.info(
"%s - the underlyings that could control the central boiler are %s", "%s - the underlyings that could controls the central boiler are %s",
self, self,
underlying_entities_id, underlying_entities_id,
) )
self.async_on_remove(listener_cancel) self.async_on_remove(listener_cancel)
else: else:
_LOGGER.debug("%s - no VTherm could control the central boiler", self) _LOGGER.debug("%s - no VTherm could controls the central boiler", self)
await self.calculate_nb_active_devices(None) await self.calculate_nb_active_devices(None)
async def calculate_nb_active_devices(self, event: Event): async def calculate_nb_active_devices(self, event: Event):
"""Calculate the number of active VTherm that have an """Calculate the number of active VTherm that have an
influence on the central boiler and update the list of active device names.""" influence on central boiler"""
# _LOGGER.debug("%s- calculate_nb_active_devices - the event is %s ", self, event) # _LOGGER.debug("%s- calculate_nb_active_devices - the event is %s ", self, event)
@@ -769,8 +757,6 @@ class NbActiveDeviceForBoilerSensor(SensorEntity):
old_state is not None old_state is not None
and new_state.state == old_state.state and new_state.state == old_state.state
and new_hvac_action == old_hvac_action and new_hvac_action == old_hvac_action
# issue 698 - force recalculation when underlying climate doesn't have any hvac_action
and new_hvac_action is not None
): ):
# A false state change # A false state change
return return
@@ -788,28 +774,20 @@ class NbActiveDeviceForBoilerSensor(SensorEntity):
) )
nb_active = 0 nb_active = 0
active_device_ids = []
for entity in self._entities: for entity in self._entities:
device_actives = entity.device_actives nb_active += entity.nb_device_actives
_LOGGER.debug( _LOGGER.debug(
"After examining the hvac_action of %s, device_actives is %s", "After examining the hvac_action of %s, nb_active is %s",
entity.name, entity.name,
device_actives, nb_active,
) )
nb_active += len(device_actives)
active_device_ids.extend(device_actives)
self._attr_native_value = nb_active self._attr_native_value = nb_active
self._attr_active_device_ids = active_device_ids _LOGGER.debug(
"%s - Number of active underlying entities is %s", self, nb_active
)
self.async_write_ha_state() self.async_write_ha_state()
@property
def active_device_ids(self) -> list:
"""Get the list of active device id"""
return self._attr_active_device_ids
def __str__(self): def __str__(self):
return f"VersatileThermostat-{self.name}" return f"VersatileThermostat-{self.name}"
@@ -224,15 +224,13 @@
"offset_calibration_entity_ids": "Offset calibration entities", "offset_calibration_entity_ids": "Offset calibration entities",
"opening_degree_entity_ids": "Opening degree entities", "opening_degree_entity_ids": "Opening degree entities",
"closing_degree_entity_ids": "Closing degree entities", "closing_degree_entity_ids": "Closing degree entities",
"proportional_function": "Algorithm", "proportional_function": "Algorithm"
"min_opening_degrees": "Min opening degrees"
}, },
"data_description": { "data_description": {
"offset_calibration_entity_ids": "The list of the 'offset calibration' entities. Set it if your TRV have the entity for better regulation. There should be one per underlying climate entities", "offset_calibration_entity_ids": "The list of the 'offset calibration' entities. Set it if your TRV have the entity for better regulation. There should be one per underlying climate entities",
"opening_degree_entity_ids": "The list of the 'opening degree' entities. There should be one per underlying climate entities", "opening_degree_entity_ids": "The list of the 'opening degree' entities. There should be one per underlying climate entities",
"closing_degree_entity_ids": "The list of the 'closing degree' entities. Set it if your TRV have the entity for better regulation. There should be one per underlying climate entities", "closing_degree_entity_ids": "The list of the 'closing degree' entities. Set it if your TRV have the entity for better regulation. There should be one per underlying climate entities",
"proportional_function": "Algorithm to use (TPI is the only one for now)", "proportional_function": "Algorithm to use (TPI is the only one for now)"
"min_opening_degrees": "A comma seperated list of minimal opening degrees. Default to 0. Example: 20, 25, 30"
} }
} }
}, },
@@ -470,15 +468,13 @@
"offset_calibration_entity_ids": "Offset calibration entities", "offset_calibration_entity_ids": "Offset calibration entities",
"opening_degree_entity_ids": "Opening degree entities", "opening_degree_entity_ids": "Opening degree entities",
"closing_degree_entity_ids": "Closing degree entities", "closing_degree_entity_ids": "Closing degree entities",
"proportional_function": "Algorithm", "proportional_function": "Algorithm"
"min_opening_degrees": "Min opening degrees"
}, },
"data_description": { "data_description": {
"offset_calibration_entity_ids": "The list of the 'offset calibration' entities. Set it if your TRV have the entity for better regulation. There should be one per underlying climate entities", "offset_calibration_entity_ids": "The list of the 'offset calibration' entities. Set it if your TRV have the entity for better regulation. There should be one per underlying climate entities",
"opening_degree_entity_ids": "The list of the 'opening degree' entities. There should be one per underlying climate entities", "opening_degree_entity_ids": "The list of the 'opening degree' entities. There should be one per underlying climate entities",
"closing_degree_entity_ids": "The list of the 'closing degree' entities. Set it if your TRV have the entity for better regulation. There should be one per underlying climate entities", "closing_degree_entity_ids": "The list of the 'closing degree' entities. Set it if your TRV have the entity for better regulation. There should be one per underlying climate entities",
"proportional_function": "Algorithm to use (TPI is the only one for now)", "proportional_function": "Algorithm to use (TPI is the only one for now)"
"min_opening_degrees": "A comma seperated list of minimal opening degrees. Default to 0. Example: 20, 25, 30"
} }
} }
}, },
@@ -488,8 +484,7 @@
"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 check 'use central configuration' because no central configuration was found. You need to create a Versatile Thermostat of type 'Central Configuration' to use it.",
"service_configuration_format": "The format of the service configuration is wrong", "service_configuration_format": "The format of the service configuration is wrong",
"valve_regulation_nb_entities_incorrect": "The number of valve entities for valve regulation should be equal to the number of underlyings", "valve_regulation_nb_entities_incorrect": "The number of valve entities for valve regulation should be equal to the number of underlyings"
"min_opening_degrees_format": "A comma separated list of positive integer is expected. Example: 20, 25, 30"
}, },
"abort": { "abort": {
"already_configured": "Device is already configured" "already_configured": "Device is already configured"
@@ -60,7 +60,6 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
"auto_start_stop_enable", "auto_start_stop_enable",
"auto_start_stop_accumulated_error", "auto_start_stop_accumulated_error",
"auto_start_stop_accumulated_error_threshold", "auto_start_stop_accumulated_error_threshold",
"auto_start_stop_last_switch_date",
"follow_underlying_temp_change", "follow_underlying_temp_change",
} }
) )
@@ -183,8 +182,8 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
await super()._async_internal_set_temperature(temperature) await super()._async_internal_set_temperature(temperature)
self._regulation_algo.set_target_temp(self.target_temperature) self._regulation_algo.set_target_temp(self.target_temperature)
# Is necessary cause control_heating method will not force the update. # is done by control_heating method. No need to do it here
await self._send_regulated_temperature(force=True) # await self._send_regulated_temperature(force=True)
async def _send_regulated_temperature(self, force=False): async def _send_regulated_temperature(self, force=False):
"""Sends the regulated temperature to all underlying""" """Sends the regulated temperature to all underlying"""
@@ -556,10 +555,6 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
"auto_start_stop_accumulated_error_threshold" "auto_start_stop_accumulated_error_threshold"
] = self._auto_start_stop_algo.accumulated_error_threshold ] = self._auto_start_stop_algo.accumulated_error_threshold
self._attr_extra_state_attributes["auto_start_stop_last_switch_date"] = (
self._auto_start_stop_algo.last_switch_date
)
self._attr_extra_state_attributes["follow_underlying_temp_change"] = ( self._attr_extra_state_attributes["follow_underlying_temp_change"] = (
self._follow_underlying_temp_change self._follow_underlying_temp_change
) )
@@ -709,9 +704,7 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
under_temp_diff = ( under_temp_diff = (
(new_target_temp - last_sent_temperature) if new_target_temp else 0 (new_target_temp - last_sent_temperature) if new_target_temp else 0
) )
if -1 < under_temp_diff < 1:
step = self.target_temperature_step or 1
if -step < under_temp_diff < step:
under_temp_diff = 0 under_temp_diff = 0
# Issue 99 - some AC turn hvac_mode=cool and hvac_action=idle when sending a HVACMode_OFF command # Issue 99 - some AC turn hvac_mode=cool and hvac_action=idle when sending a HVACMode_OFF command
@@ -768,7 +761,7 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
_LOGGER.debug( _LOGGER.debug(
"%s - last_change_time=%s old_state_date_changed=%s old_state_date_updated=%s new_state_date_changed=%s new_state_date_updated=%s", "%s - last_change_time=%s old_state_date_changed=%s old_state_date_updated=%s new_state_date_changed=%s new_state_date_updated=%s",
self, self,
self._last_change_time_from_vtherm, self._last_change_time,
old_state_date_changed, old_state_date_changed,
old_state_date_updated, old_state_date_updated,
new_state_date_changed, new_state_date_changed,
@@ -811,10 +804,8 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
# Filter new state when received just after a change from VTherm # Filter new state when received just after a change from VTherm
# Issue #120 - Some TRV are changing target temperature a very long time (6 sec) after the change. # Issue #120 - Some TRV are changing target temperature a very long time (6 sec) after the change.
# In that case a loop is possible if a user change multiple times during this 6 sec. # In that case a loop is possible if a user change multiple times during this 6 sec.
if new_state_date_updated and self._last_change_time_from_vtherm: if new_state_date_updated and self._last_change_time:
delta = ( delta = (new_state_date_updated - self._last_change_time).total_seconds()
new_state_date_updated - self._last_change_time_from_vtherm
).total_seconds()
if delta < 10: if delta < 10:
_LOGGER.info( _LOGGER.info(
"%s - underlying event is received less than 10 sec after command. Forget it to avoid loop", "%s - underlying event is received less than 10 sec after command. Forget it to avoid loop",
@@ -1123,6 +1114,15 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
return self._support_flags return self._support_flags
# We keep the step configured for the VTherm and not the step of the underlying
# @property
# def target_temperature_step(self) -> float | None:
# """Return the supported step of target temperature."""
# if self.underlying_entity(0):
# return self.underlying_entity(0).target_temperature_step
#
# return None
@property @property
def target_temperature_high(self) -> float | None: def target_temperature_high(self) -> float | None:
"""Return the highbound target temperature we try to reach. """Return the highbound target temperature we try to reach.
@@ -37,7 +37,6 @@ class ThermostatOverClimateValve(ThermostatOverClimate):
"tpi_coef_int", "tpi_coef_int",
"tpi_coef_ext", "tpi_coef_ext",
"power_percent", "power_percent",
"min_opening_degrees",
} }
) )
) )
@@ -52,7 +51,6 @@ class ThermostatOverClimateValve(ThermostatOverClimate):
self._last_calculation_timestamp: datetime | None = None self._last_calculation_timestamp: datetime | None = None
self._auto_regulation_dpercent: float | None = None self._auto_regulation_dpercent: float | None = None
self._auto_regulation_period_min: int | None = None self._auto_regulation_period_min: int | None = None
self._min_opening_degress: list[int] = []
super().__init__(hass, unique_id, name, entry_infos) super().__init__(hass, unique_id, name, entry_infos)
@@ -88,14 +86,6 @@ class ThermostatOverClimateValve(ThermostatOverClimate):
offset_list = config_entry.get(CONF_OFFSET_CALIBRATION_LIST, []) offset_list = config_entry.get(CONF_OFFSET_CALIBRATION_LIST, [])
opening_list = config_entry.get(CONF_OPENING_DEGREE_LIST) opening_list = config_entry.get(CONF_OPENING_DEGREE_LIST)
closing_list = config_entry.get(CONF_CLOSING_DEGREE_LIST, []) closing_list = config_entry.get(CONF_CLOSING_DEGREE_LIST, [])
self._min_opening_degrees = config_entry.get(CONF_MIN_OPENING_DEGREES, None)
min_opening_degrees_list = []
if self._min_opening_degrees:
min_opening_degrees_list = [
int(x.strip()) for x in self._min_opening_degrees.split(",")
]
for idx, _ in enumerate(config_entry.get(CONF_UNDERLYING_LIST)): for idx, _ in enumerate(config_entry.get(CONF_UNDERLYING_LIST)):
offset = offset_list[idx] if idx < len(offset_list) else None offset = offset_list[idx] if idx < len(offset_list) else None
# number of opening should equal number of underlying # number of opening should equal number of underlying
@@ -108,11 +98,6 @@ class ThermostatOverClimateValve(ThermostatOverClimate):
opening_degree_entity_id=opening, opening_degree_entity_id=opening,
closing_degree_entity_id=closing, closing_degree_entity_id=closing,
climate_underlying=self._underlyings[idx], climate_underlying=self._underlyings[idx],
min_opening_degree=(
min_opening_degrees_list[idx]
if idx < len(min_opening_degrees_list)
else 0
),
) )
self._underlyings_valve_regulation.append(under) self._underlyings_valve_regulation.append(under)
@@ -145,10 +130,6 @@ class ThermostatOverClimateValve(ThermostatOverClimate):
self._attr_extra_state_attributes["tpi_coef_int"] = self._tpi_coef_int self._attr_extra_state_attributes["tpi_coef_int"] = self._tpi_coef_int
self._attr_extra_state_attributes["tpi_coef_ext"] = self._tpi_coef_ext self._attr_extra_state_attributes["tpi_coef_ext"] = self._tpi_coef_ext
self._attr_extra_state_attributes["min_opening_degrees"] = (
self._min_opening_degrees
)
self._attr_extra_state_attributes["valve_open_percent"] = ( self._attr_extra_state_attributes["valve_open_percent"] = (
self.valve_open_percent self.valve_open_percent
) )
@@ -296,15 +277,12 @@ class ThermostatOverClimateValve(ThermostatOverClimate):
return self.valve_open_percent > 0 return self.valve_open_percent > 0
@property @property
def device_actives(self) -> int: def nb_device_actives(self) -> int:
"""Calculate the number of active devices""" """Calculate the number of active devices"""
if self.is_device_active: if self.is_device_active:
return [ return len(self._underlyings_valve_regulation)
under.opening_degree_entity_id
for under in self._underlyings_valve_regulation
]
else: else:
return [] return 0
@property @property
def activable_underlying_entities(self) -> list | None: def activable_underlying_entities(self) -> list | None:
@@ -224,15 +224,13 @@
"offset_calibration_entity_ids": "Offset calibration entities", "offset_calibration_entity_ids": "Offset calibration entities",
"opening_degree_entity_ids": "Opening degree entities", "opening_degree_entity_ids": "Opening degree entities",
"closing_degree_entity_ids": "Closing degree entities", "closing_degree_entity_ids": "Closing degree entities",
"proportional_function": "Algorithm", "proportional_function": "Algorithm"
"min_opening_degrees": "Min opening degrees"
}, },
"data_description": { "data_description": {
"offset_calibration_entity_ids": "The list of the 'offset calibration' entities. Set it if your TRV have the entity for better regulation. There should be one per underlying climate entities", "offset_calibration_entity_ids": "The list of the 'offset calibration' entities. Set it if your TRV have the entity for better regulation. There should be one per underlying climate entities",
"opening_degree_entity_ids": "The list of the 'opening degree' entities. There should be one per underlying climate entities", "opening_degree_entity_ids": "The list of the 'opening degree' entities. There should be one per underlying climate entities",
"closing_degree_entity_ids": "The list of the 'closing degree' entities. Set it if your TRV have the entity for better regulation. There should be one per underlying climate entities", "closing_degree_entity_ids": "The list of the 'closing degree' entities. Set it if your TRV have the entity for better regulation. There should be one per underlying climate entities",
"proportional_function": "Algorithm to use (TPI is the only one for now)", "proportional_function": "Algorithm to use (TPI is the only one for now)"
"min_opening_degrees": "A comma seperated list of minimal opening degrees. Default to 0. Example: 20, 25, 30"
} }
} }
}, },
@@ -470,15 +468,13 @@
"offset_calibration_entity_ids": "Offset calibration entities", "offset_calibration_entity_ids": "Offset calibration entities",
"opening_degree_entity_ids": "Opening degree entities", "opening_degree_entity_ids": "Opening degree entities",
"closing_degree_entity_ids": "Closing degree entities", "closing_degree_entity_ids": "Closing degree entities",
"proportional_function": "Algorithm", "proportional_function": "Algorithm"
"min_opening_degrees": "Min opening degrees"
}, },
"data_description": { "data_description": {
"offset_calibration_entity_ids": "The list of the 'offset calibration' entities. Set it if your TRV have the entity for better regulation. There should be one per underlying climate entities", "offset_calibration_entity_ids": "The list of the 'offset calibration' entities. Set it if your TRV have the entity for better regulation. There should be one per underlying climate entities",
"opening_degree_entity_ids": "The list of the 'opening degree' entities. There should be one per underlying climate entities", "opening_degree_entity_ids": "The list of the 'opening degree' entities. There should be one per underlying climate entities",
"closing_degree_entity_ids": "The list of the 'closing degree' entities. Set it if your TRV have the entity for better regulation. There should be one per underlying climate entities", "closing_degree_entity_ids": "The list of the 'closing degree' entities. Set it if your TRV have the entity for better regulation. There should be one per underlying climate entities",
"proportional_function": "Algorithm to use (TPI is the only one for now)", "proportional_function": "Algorithm to use (TPI is the only one for now)"
"min_opening_degrees": "A comma seperated list of minimal opening degrees. Default to 0. Example: 20, 25, 30"
} }
} }
}, },
@@ -218,21 +218,19 @@
} }
}, },
"valve_regulation": { "valve_regulation": {
"title": "Auto-régulation par vanne", "title": "Auto-régulation par vanne - {name}",
"description": "Configuration de l'auto-régulation par controle direct de la vanne", "description": "Configuration de l'auto-régulation par controle direct de la vanne",
"data": { "data": {
"offset_calibration_entity_ids": "Entités de 'calibrage du décalage''", "offset_calibration_entity_ids": "Entités de 'calibrage du décalage''",
"opening_degree_entity_ids": "Entités 'ouverture de vanne'", "opening_degree_entity_ids": "Entités 'ouverture de vanne'",
"closing_degree_entity_ids": "Entités 'fermeture de la vanne'", "closing_degree_entity_ids": "Entités 'fermeture de la vanne'",
"proportional_function": "Algorithme", "proportional_function": "Algorithme"
"min_opening_degrees": "Ouvertures minimales"
}, },
"data_description": { "data_description": {
"offset_calibration_entity_ids": "La liste des entités 'calibrage du décalage' (offset calibration). Configurez le si votre TRV possède cette fonction pour une meilleure régulation. Il doit y en avoir une par entité climate sous-jacente", "offset_calibration_entity_ids": "La liste des entités 'calibrage du décalage' (offset calibration). Configurez le si votre TRV possède cette fonction pour une meilleure régulation. Il doit y en avoir une par entité climate sous-jacente",
"opening_degree_entity_ids": "La liste des entités 'ouverture de vanne'. Il doit y en avoir une par entité climate sous-jacente", "opening_degree_entity_ids": "La liste des entités 'ouverture de vanne'. Il doit y en avoir une par entité climate sous-jacente",
"closing_degree_entity_ids": "La liste des entités 'fermeture de la vanne'. Configurez le si votre TRV possède cette fonction pour une meilleure régulation. Il doit y en avoir une par entité climate sous-jacente", "closing_degree_entity_ids": "La liste des entités 'fermeture de la vanne'. Configurez le si votre TRV possède cette fonction pour une meilleure régulation. Il doit y en avoir une par entité climate sous-jacente",
"proportional_function": "Algorithme à utiliser (seulement TPI est disponible)", "proportional_function": "Algorithme à utiliser (seulement TPI est disponible)"
"min_opening_degrees": "Une liste séparée par des virgules de minimum d'ouverture. Valeur par défaut : 0. Exemple : 20, 25, 30"
} }
} }
}, },
@@ -259,7 +257,7 @@
} }
}, },
"menu": { "menu": {
"title": "Menu", "title": "Menu - {name}",
"description": "Paramétrez votre thermostat. Vous pourrez finaliser la configuration quand tous les paramètres auront été saisis.", "description": "Paramétrez votre thermostat. Vous pourrez finaliser la configuration quand tous les paramètres auront été saisis.",
"menu_options": { "menu_options": {
"main": "Principaux Attributs", "main": "Principaux Attributs",
@@ -464,15 +462,13 @@
"offset_calibration_entity_ids": "Entités de 'calibrage du décalage''", "offset_calibration_entity_ids": "Entités de 'calibrage du décalage''",
"opening_degree_entity_ids": "Entités 'ouverture de vanne'", "opening_degree_entity_ids": "Entités 'ouverture de vanne'",
"closing_degree_entity_ids": "Entités 'fermeture de la vanne'", "closing_degree_entity_ids": "Entités 'fermeture de la vanne'",
"proportional_function": "Algorithme", "proportional_function": "Algorithme"
"min_opening_degrees": "Ouvertures minimales"
}, },
"data_description": { "data_description": {
"offset_calibration_entity_ids": "La liste des entités 'calibrage du décalage' (offset calibration). Configurez le si votre TRV possède cette fonction pour une meilleure régulation. Il doit y en avoir une par entité climate sous-jacente", "offset_calibration_entity_ids": "La liste des entités 'calibrage du décalage' (offset calibration). Configurez le si votre TRV possède cette fonction pour une meilleure régulation. Il doit y en avoir une par entité climate sous-jacente",
"opening_degree_entity_ids": "La liste des entités 'ouverture de vanne'. Il doit y en avoir une par entité climate sous-jacente", "opening_degree_entity_ids": "La liste des entités 'ouverture de vanne'. Il doit y en avoir une par entité climate sous-jacente",
"closing_degree_entity_ids": "La liste des entités 'fermeture de la vanne'. Configurez le si votre TRV possède cette fonction pour une meilleure régulation. Il doit y en avoir une par entité climate sous-jacente", "closing_degree_entity_ids": "La liste des entités 'fermeture de la vanne'. Configurez le si votre TRV possède cette fonction pour une meilleure régulation. Il doit y en avoir une par entité climate sous-jacente",
"proportional_function": "Algorithme à utiliser (seulement TPI est disponible)", "proportional_function": "Algorithme à utiliser (seulement TPI est disponible)"
"min_opening_degrees": "Une liste séparée par des virgules de minimum d'ouverture. Valeur par défaut : 0. Exemple : 20, 25, 30"
} }
} }
}, },
@@ -482,8 +478,7 @@
"window_open_detection_method": "Une seule méthode de détection des ouvertures ouvertes doit être utilisée. Utilisez le détecteur d'ouverture ou les seuils de température mais pas les deux.", "window_open_detection_method": "Une seule méthode de détection des ouvertures ouvertes doit être utilisée. Utilisez le détecteur d'ouverture ou les seuils de température mais pas les deux.",
"no_central_config": "Vous ne pouvez pas cocher 'Utiliser la configuration centrale' car aucune configuration centrale n'a été trouvée. Vous devez créer un Versatile Thermostat de type 'Central Configuration' pour pouvoir l'utiliser.", "no_central_config": "Vous ne pouvez pas cocher 'Utiliser la configuration centrale' car aucune configuration centrale n'a été trouvée. Vous devez créer un Versatile Thermostat de type 'Central Configuration' pour pouvoir l'utiliser.",
"service_configuration_format": "Mauvais format de la configuration du service", "service_configuration_format": "Mauvais format de la configuration du service",
"valve_regulation_nb_entities_incorrect": "Le nombre d'entités pour la régulation par vanne doit être égal au nombre d'entité sous-jacentes", "valve_regulation_nb_entities_incorrect": "Le nombre d'entités pour la régulation par vanne doit être égal au nombre d'entité sous-jacentes"
"min_opening_degrees_format": "Une liste d'entiers positifs séparés par des ',' est attendu. Exemple : 20, 25, 30"
}, },
"abort": { "abort": {
"already_configured": "Le device est déjà configuré" "already_configured": "Le device est déjà configuré"
@@ -252,7 +252,7 @@ class UnderlyingSwitch(UnderlyingEntity):
self._cancel_cycle() self._cancel_cycle()
if self.hvac_mode != hvac_mode: if self.hvac_mode != hvac_mode:
await super().set_hvac_mode(hvac_mode) super().set_hvac_mode(hvac_mode)
return True return True
else: else:
return False return False
@@ -1029,7 +1029,6 @@ class UnderlyingValveRegulation(UnderlyingValve):
opening_degree_entity_id: str, opening_degree_entity_id: str,
closing_degree_entity_id: str, closing_degree_entity_id: str,
climate_underlying: UnderlyingClimate, climate_underlying: UnderlyingClimate,
min_opening_degree: int = 0,
) -> None: ) -> None:
"""Initialize the underlying TRV with valve regulation""" """Initialize the underlying TRV with valve regulation"""
super().__init__( super().__init__(
@@ -1046,7 +1045,6 @@ class UnderlyingValveRegulation(UnderlyingValve):
self._max_opening_degree: float = None self._max_opening_degree: float = None
self._min_offset_calibration: float = None self._min_offset_calibration: float = None
self._max_offset_calibration: float = None self._max_offset_calibration: float = None
self._min_opening_degree: int = min_opening_degree
async def send_percent_open(self): async def send_percent_open(self):
"""Send the percent open to the underlying valve""" """Send the percent open to the underlying valve"""
@@ -1081,9 +1079,6 @@ class UnderlyingValveRegulation(UnderlyingValve):
return return
# Send opening_degree # Send opening_degree
if 0 < self._percent_open < self._min_opening_degree:
self._percent_open = self._min_opening_degree
await super().send_percent_open() await super().send_percent_open()
# Send closing_degree if set # Send closing_degree if set
@@ -1143,11 +1138,6 @@ class UnderlyingValveRegulation(UnderlyingValve):
"""The offset_calibration_entity_id""" """The offset_calibration_entity_id"""
return self._closing_degree_entity_id return self._closing_degree_entity_id
@property
def min_opening_degree(self) -> int:
"""The minimum opening degree"""
return self._min_opening_degree
@property @property
def have_closing_degree_entity(self) -> bool: def have_closing_degree_entity(self) -> bool:
"""Return True if the underlying have a closing_degree entity""" """Return True if the underlying have a closing_degree entity"""
Binary file not shown.

Before

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 67 KiB

-4
View File
@@ -271,9 +271,5 @@ The custom attributes are as follows:
| ``auto_start_stop_enable`` | Indicates if the VTherm is allowed to auto start/stop | | ``auto_start_stop_enable`` | Indicates if the VTherm is allowed to auto start/stop |
| ``auto_start_stop_level`` | Indicates the auto start/stop level | | ``auto_start_stop_level`` | Indicates the auto start/stop level |
| ``hvac_off_reason`` | Indicates the reason for the thermostat's off state (hvac_off). It can be Window, Auto-start/stop, or Manual | | ``hvac_off_reason`` | Indicates the reason for the thermostat's off state (hvac_off). It can be Window, Auto-start/stop, or Manual |
| ``last_change_time_from_vtherm`` | The date and time of the last change done by VTherm |
| ``nb_device_actives`` | The number of underlying devices seen as active |
| ``device_actives`` | The list of underlying devices seen as active |
These attributes will be requested when you need assistance. These attributes will be requested when you need assistance.
-1
View File
@@ -32,7 +32,6 @@ You need to provide:
1. As many valve opening control entities as there are underlying devices, and in the same order. These parameters are mandatory. 1. As many valve opening control entities as there are underlying devices, and in the same order. These parameters are mandatory.
2. As many temperature calibration entities as there are underlying devices, and in the same order. These parameters are optional; they must either all be provided or none. 2. As many temperature calibration entities as there are underlying devices, and in the same order. These parameters are optional; they must either all be provided or none.
3. As many valve closure control entities as there are underlying devices, and in the same order. These parameters are optional; they must either all be provided or none. 3. As many valve closure control entities as there are underlying devices, and in the same order. These parameters are optional; they must either all be provided or none.
4. A list of minimum opening values for the valve when it needs to be opened. This field is a list of integers. If the valve needs to be opened, it will be opened at a minimum of this opening value. This allows enough water to pass through when it needs to be opened.
The opening rate calculation algorithm is based on the _TPI_ algorithm described [here](algorithms.md). This is the same algorithm used for _VTherms_ `over_switch` and `over_valve`. The opening rate calculation algorithm is based on the _TPI_ algorithm described [here](algorithms.md). This is the same algorithm used for _VTherms_ `over_switch` and `over_valve`.
-5
View File
@@ -15,7 +15,6 @@
- [How to Fix It?](#how-to-fix-it) - [How to Fix It?](#how-to-fix-it)
- [Using a Group of People as a Presence Sensor](#using-a-group-of-people-as-a-presence-sensor) - [Using a Group of People as a Presence Sensor](#using-a-group-of-people-as-a-presence-sensor)
- [Enable Logs for the Versatile Thermostat](#enable-logs-for-the-versatile-thermostat) - [Enable Logs for the Versatile Thermostat](#enable-logs-for-the-versatile-thermostat)
- [VTherm does not track setpoint changes made directly on the underlying device (`over_climate`)](#vtherm-does-not-track-setpoint-changes-made-directly-on-the-underlying-device-over_climate)
## Using a Heatzy ## Using a Heatzy
@@ -210,7 +209,3 @@ logs:
You must reload the YAML configuration (Developer Tools / YAML / Reload all YAML configuration) or restart Home Assistant for this change to take effect. You must reload the YAML configuration (Developer Tools / YAML / Reload all YAML configuration) or restart Home Assistant for this change to take effect.
Be careful, in debug mode, Versatile Thermostat is very verbose and can quickly slow down Home Assistant or saturate your hard drive. If you switch to debug mode for anomaly analysis, do so only for the time needed to reproduce the bug and disable debug mode immediately afterward. Be careful, in debug mode, Versatile Thermostat is very verbose and can quickly slow down Home Assistant or saturate your hard drive. If you switch to debug mode for anomaly analysis, do so only for the time needed to reproduce the bug and disable debug mode immediately afterward.
## VTherm does not track setpoint changes made directly on the underlying device (`over_climate`)
See the details of this feature [here](over-climate.md#track-underlying-temperature-changes).
Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

After

Width:  |  Height:  |  Size: 71 KiB

-3
View File
@@ -270,8 +270,5 @@ Les attributs personnalisés sont les suivants :
| ``auto_start_stop_enable`` | Indique si le VTherm est autorisé à s'auto démarrer/arrêter | | ``auto_start_stop_enable`` | Indique si le VTherm est autorisé à s'auto démarrer/arrêter |
| ``auto_start_stop_level`` | Indique le niveau d'auto start/stop | | ``auto_start_stop_level`` | Indique le niveau d'auto start/stop |
| ``hvac_off_reason`` | Indique la raison de l'arrêt (hvac_off) du VTherm. Ce peut être Window, Auto-start/stop ou Manuel | | ``hvac_off_reason`` | Indique la raison de l'arrêt (hvac_off) du VTherm. Ce peut être Window, Auto-start/stop ou Manuel |
| ``last_change_time_from_vtherm`` | La date/heure du dernier changement fait par VTherm |
| ``nb_device_actives`` | Le nombre de devices sous-jacents actuellement vus comme actifs |
| ``device_actives`` | La liste des devices sous-jacents actuellement vus comme actifs |
Ces attributs vous seront demandés lors d'une demande d'aide. Ces attributs vous seront demandés lors d'une demande d'aide.
+1 -2
View File
@@ -32,8 +32,7 @@ Elle permet de configurer les entités de contrôle de la vanne :
Vous devez donner : Vous devez donner :
1. autant d'entités de contrôle d'ouverture de la vanne qu'il y a de sous-jacents et dans le même odre. Ces paramètres sont obligatoires, 1. autant d'entités de contrôle d'ouverture de la vanne qu'il y a de sous-jacents et dans le même odre. Ces paramètres sont obligatoires,
2. autant d'entités de calibrage du décalage de température qu'il y a de sous-jacents et dans le même ordre. Ces paramètres sont facultatifs ; ils doivent être tous founis ou aucun, 2. autant d'entités de calibrage du décalage de température qu'il y a de sous-jacents et dans le même ordre. Ces paramètres sont facultatifs ; ils doivent être tous founis ou aucun,
3. autant d'entités de de contrôile du taux de fermture qu'il y a de sous-jacents et dans le même ordre. Ces paramètres sont facultatifs ; ils doivent être tous founis ou aucun, 3. autant d'entités de de contrôile du taux de fermture qu'il y a de sous-jacents et dans le même ordre. Ces paramètres sont facultatifs ; ils doivent être tous founis ou aucun
4. une liste de valeurs minimales d'ouverture de la vanne lorsqu'elle doit être ouverte. Ce champ est une liste d'entier. Si la vanne doit être ouverte, elle le sera au minimum avec cette valeur d'ouverture. Cela permet de laisser passer suffisamment d'eau lorsqu'elle doit être ouverte.
L'algorithme de calcul du taux d'ouverture est basé sur le _TPI_ qui est décrit [ici](algorithms.md). C'est le même algorithme qui est utilisé pour les _VTherm_ `over_switch` et `over_valve`. L'algorithme de calcul du taux d'ouverture est basé sur le _TPI_ qui est décrit [ici](algorithms.md). C'est le même algorithme qui est utilisé pour les _VTherm_ `over_switch` et `over_valve`.
-5
View File
@@ -15,7 +15,6 @@
- [Comment réparer ?](#comment-réparer-) - [Comment réparer ?](#comment-réparer-)
- [Utilisation d'un groupe de personnes comme capteur de présence](#utilisation-dun-groupe-de-personnes-comme-capteur-de-présence) - [Utilisation d'un groupe de personnes comme capteur de présence](#utilisation-dun-groupe-de-personnes-comme-capteur-de-présence)
- [Activer les logs du Versatile Thermostat](#activer-les-logs-du-versatile-thermostat) - [Activer les logs du Versatile Thermostat](#activer-les-logs-du-versatile-thermostat)
- [VTherm ne suit pas les changements de consigne faits directement depuis le sous-jacents (`over_climate`)](#vtherm-ne-suit-pas-les-changements-de-consigne-faits-directement-depuis-le-sous-jacents-over_climate)
## Utilisation d'un Heatzy ## Utilisation d'un Heatzy
@@ -207,7 +206,3 @@ logs:
Vous devez recharger la configuration yaml (Outils de dev / Yaml / Toute la configuration Yaml) ou redémarrer Home Assistant pour que ce changement soit pris en compte. Vous devez recharger la configuration yaml (Outils de dev / Yaml / Toute la configuration Yaml) ou redémarrer Home Assistant pour que ce changement soit pris en compte.
Attention, en mode debug Versatile Thermostat est très verbeux et peut vite ralentir Home Assistant ou saturer votre disque dur. Si vous passez en mode debug pour une analyse d'anomalie il faut s'y mettre juste le temps de reproduire le bug et désactiver le mode debug juste après. Attention, en mode debug Versatile Thermostat est très verbeux et peut vite ralentir Home Assistant ou saturer votre disque dur. Si vous passez en mode debug pour une analyse d'anomalie il faut s'y mettre juste le temps de reproduire le bug et désactiver le mode debug juste après.
## VTherm ne suit pas les changements de consigne faits directement depuis le sous-jacents (`over_climate`)
Voir le détail de cette fonction [ici](over-climate.md#suivre-les-changements-de-température-du-sous-jacent).
+1 -1
View File
@@ -3,5 +3,5 @@
"content_in_root": false, "content_in_root": false,
"render_readme": true, "render_readme": true,
"hide_default_branch": false, "hide_default_branch": false,
"homeassistant": "2024.12.3" "homeassistant": "2024.10.4"
} }
+1 -1
View File
@@ -1 +1 @@
homeassistant==2024.12.3 homeassistant==2024.10.4
-1
View File
@@ -579,7 +579,6 @@ class MockNumber(NumberEntity):
def set_native_value(self, value: float): def set_native_value(self, value: float):
"""Change the value""" """Change the value"""
self._attr_native_value = value self._attr_native_value = value
self.async_write_ha_state()
async def create_thermostat( async def create_thermostat(
+1 -1
View File
@@ -309,7 +309,7 @@ async def test_over_climate_regulation_limitations(
assert entity.hvac_action == HVACAction.HEATING assert entity.hvac_action == HVACAction.HEATING
# the regulated temperature will not change because when we set temp manually it is forced # the regulated temperature will not change because when we set temp manually it is forced
assert entity.regulated_target_temp == 19.5 assert entity.regulated_target_temp == 17 # 19.5
# 2. set manual target temp (at now - 18) -> the regulation should be taken into account # 2. set manual target temp (at now - 18) -> the regulation should be taken into account
event_timestamp = now - timedelta(minutes=18) event_timestamp = now - timedelta(minutes=18)
+2 -107
View File
@@ -15,7 +15,6 @@ from custom_components.versatile_thermostat.auto_start_stop_algorithm import (
AutoStartStopDetectionAlgorithm, AutoStartStopDetectionAlgorithm,
AUTO_START_STOP_ACTION_NOTHING, AUTO_START_STOP_ACTION_NOTHING,
AUTO_START_STOP_ACTION_OFF, AUTO_START_STOP_ACTION_OFF,
AUTO_START_STOP_ACTION_ON,
) )
from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import
@@ -45,7 +44,6 @@ async def test_auto_start_stop_algo_slow_heat_off(hass: HomeAssistant):
) )
assert ret == AUTO_START_STOP_ACTION_NOTHING assert ret == AUTO_START_STOP_ACTION_NOTHING
assert algo.accumulated_error == -1 assert algo.accumulated_error == -1
assert algo.last_switch_date is None
# 2. should not stop (accumulated_error too low) # 2. should not stop (accumulated_error too low)
now = now + timedelta(minutes=5) now = now + timedelta(minutes=5)
@@ -59,7 +57,6 @@ async def test_auto_start_stop_algo_slow_heat_off(hass: HomeAssistant):
) )
assert ret == AUTO_START_STOP_ACTION_NOTHING assert ret == AUTO_START_STOP_ACTION_NOTHING
assert algo.accumulated_error == -6 assert algo.accumulated_error == -6
assert algo.last_switch_date is None
# 3. should not stop (accumulated_error too low) # 3. should not stop (accumulated_error too low)
now = now + timedelta(minutes=2) now = now + timedelta(minutes=2)
@@ -73,7 +70,6 @@ async def test_auto_start_stop_algo_slow_heat_off(hass: HomeAssistant):
) )
assert algo.accumulated_error == -8 assert algo.accumulated_error == -8
assert ret == AUTO_START_STOP_ACTION_NOTHING assert ret == AUTO_START_STOP_ACTION_NOTHING
assert algo.last_switch_date is None
# 4 .No change on accumulated error because the new measure is too near the last one # 4 .No change on accumulated error because the new measure is too near the last one
now = now + timedelta(seconds=11) now = now + timedelta(seconds=11)
@@ -87,7 +83,6 @@ async def test_auto_start_stop_algo_slow_heat_off(hass: HomeAssistant):
) )
assert algo.accumulated_error == -8 assert algo.accumulated_error == -8
assert ret == AUTO_START_STOP_ACTION_NOTHING assert ret == AUTO_START_STOP_ACTION_NOTHING
assert algo.last_switch_date is None
# 5. should stop now because accumulated_error is > ERROR_THRESHOLD for slow (10) # 5. should stop now because accumulated_error is > ERROR_THRESHOLD for slow (10)
now = now + timedelta(minutes=4) now = now + timedelta(minutes=4)
@@ -101,9 +96,6 @@ async def test_auto_start_stop_algo_slow_heat_off(hass: HomeAssistant):
) )
assert algo.accumulated_error == -10 assert algo.accumulated_error == -10
assert ret == AUTO_START_STOP_ACTION_OFF assert ret == AUTO_START_STOP_ACTION_OFF
assert algo.last_switch_date is not None
assert algo.last_switch_date == now
last_now = now
# 6. inverse the temperature (target > current) -> accumulated_error should be divided by 2 # 6. inverse the temperature (target > current) -> accumulated_error should be divided by 2
now = now + timedelta(minutes=2) now = now + timedelta(minutes=2)
@@ -117,111 +109,14 @@ async def test_auto_start_stop_algo_slow_heat_off(hass: HomeAssistant):
) )
assert algo.accumulated_error == -4 # -10/2 + 1 assert algo.accumulated_error == -4 # -10/2 + 1
assert ret == AUTO_START_STOP_ACTION_NOTHING assert ret == AUTO_START_STOP_ACTION_NOTHING
assert algo.last_switch_date == last_now
# 7. change level to slow (no real change) -> error_accumulated should not reset to 0 # 7. change level to slow (no real change) -> error_accumulated should not reset to 0
algo.set_level(AUTO_START_STOP_LEVEL_SLOW) algo.set_level(AUTO_START_STOP_LEVEL_SLOW)
assert algo.accumulated_error == -4 assert algo.accumulated_error == -4
assert algo.last_switch_date == last_now
# 8. change level -> error_accumulated should reset to 0 # 8. change level -> error_accumulated should reset to 0
algo.set_level(AUTO_START_STOP_LEVEL_FAST) algo.set_level(AUTO_START_STOP_LEVEL_FAST)
assert algo.accumulated_error == 0 assert algo.accumulated_error == 0
assert algo.last_switch_date == last_now
async def test_auto_start_stop_too_fast_change(hass: HomeAssistant):
"""Testing directly the algorithm in Slow level"""
algo: AutoStartStopDetectionAlgorithm = AutoStartStopDetectionAlgorithm(
AUTO_START_STOP_LEVEL_SLOW, "testu"
)
tz = get_tz(hass) # pylint: disable=invalid-name
now: datetime = datetime.now(tz=tz)
assert algo._dt == 30
assert algo._vtherm_name == "testu"
#
# Testing with turn_on
#
# 1. should stop
algo._accumulated_error = -100
ret = algo.calculate_action(
hvac_mode=HVACMode.HEAT,
saved_hvac_mode=HVACMode.OFF,
target_temp=10,
current_temp=21,
slope_min=0.5,
now=now,
)
assert ret == AUTO_START_STOP_ACTION_OFF
assert algo.last_switch_date is not None
assert algo.last_switch_date == now
last_now = now
# 2. now we should turn on but to near the last change -> no nothing to do
now = now + timedelta(minutes=2)
algo._accumulated_error = -100
ret = algo.calculate_action(
hvac_mode=HVACMode.OFF,
saved_hvac_mode=HVACMode.HEAT,
target_temp=21,
current_temp=17,
slope_min=-0.1,
now=now,
)
assert ret == AUTO_START_STOP_ACTION_NOTHING
assert algo.last_switch_date == last_now
# 3. now we should turn on and now is much later ->
now = now + timedelta(minutes=30)
algo._accumulated_error = -100
ret = algo.calculate_action(
hvac_mode=HVACMode.OFF,
saved_hvac_mode=HVACMode.HEAT,
target_temp=21,
current_temp=17,
slope_min=-0.1,
now=now,
)
assert ret == AUTO_START_STOP_ACTION_ON
assert algo.last_switch_date == now
last_now = now
#
# Testing with turn_off
#
# 4. try to turn_off but too speed (29 min)
now = now + timedelta(minutes=29)
algo._accumulated_error = -100
ret = algo.calculate_action(
hvac_mode=HVACMode.HEAT,
saved_hvac_mode=HVACMode.OFF,
target_temp=17,
current_temp=21,
slope_min=0.5,
now=now,
)
assert ret == AUTO_START_STOP_ACTION_NOTHING
assert algo.last_switch_date == last_now
# 5. turn_off much later (29 min + 1 min)
now = now + timedelta(minutes=1)
algo._accumulated_error = -100
ret = algo.calculate_action(
hvac_mode=HVACMode.HEAT,
saved_hvac_mode=HVACMode.OFF,
target_temp=17,
current_temp=21,
slope_min=0.5,
now=now,
)
assert ret == AUTO_START_STOP_ACTION_OFF
assert algo.last_switch_date == now
async def test_auto_start_stop_algo_medium_cool_off(hass: HomeAssistant): async def test_auto_start_stop_algo_medium_cool_off(hass: HomeAssistant):
@@ -1299,7 +1194,7 @@ async def test_auto_start_stop_fast_heat_window(
now: datetime = datetime.now(tz=tz) now: datetime = datetime.now(tz=tz)
# 2. Set mode to Heat and preset to Comfort and close the window # 2. Set mode to Heat and preset to Comfort and close the window
await send_window_change_event(vtherm, False, False, now, False) send_window_change_event(vtherm, False, False, now, False)
await send_presence_change_event(vtherm, True, False, now) await send_presence_change_event(vtherm, True, False, now)
await send_temperature_change_event(vtherm, 18, now, True) await send_temperature_change_event(vtherm, 18, now, True)
await vtherm.async_set_hvac_mode(HVACMode.HEAT) await vtherm.async_set_hvac_mode(HVACMode.HEAT)
@@ -1474,7 +1369,7 @@ async def test_auto_start_stop_fast_heat_window_mixed(
now: datetime = datetime.now(tz=tz) now: datetime = datetime.now(tz=tz)
# 2. Set mode to Heat and preset to Comfort and close the window # 2. Set mode to Heat and preset to Comfort and close the window
await send_window_change_event(vtherm, False, False, now, False) send_window_change_event(vtherm, False, False, now, False)
await send_presence_change_event(vtherm, True, False, now) await send_presence_change_event(vtherm, True, False, now)
await send_temperature_change_event(vtherm, 18, now, True) await send_temperature_change_event(vtherm, 18, now, True)
await vtherm.async_set_hvac_mode(HVACMode.HEAT) await vtherm.async_set_hvac_mode(HVACMode.HEAT)
+17 -377
View File
@@ -2,7 +2,7 @@
""" Test the central_configuration """ """ Test the central_configuration """
import asyncio import asyncio
from datetime import datetime, timedelta from datetime import datetime
from unittest.mock import patch, call from unittest.mock import patch, call
@@ -29,8 +29,6 @@ from custom_components.versatile_thermostat.binary_sensor import (
CentralBoilerBinarySensor, CentralBoilerBinarySensor,
) )
from custom_components.versatile_thermostat.sensor import NbActiveDeviceForBoilerSensor
from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import
from .const import * # pylint: disable=wildcard-import, unused-wildcard-import from .const import * # pylint: disable=wildcard-import, unused-wildcard-import
@@ -105,7 +103,7 @@ async def test_update_central_boiler_state_simple(
CONF_USE_MOTION_FEATURE: False, CONF_USE_MOTION_FEATURE: False,
CONF_USE_POWER_FEATURE: False, CONF_USE_POWER_FEATURE: False,
CONF_USE_PRESENCE_FEATURE: False, CONF_USE_PRESENCE_FEATURE: False,
CONF_UNDERLYING_LIST: [switch1.entity_id], CONF_HEATER: switch1.entity_id,
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI, CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
CONF_INVERSE_SWITCH: False, CONF_INVERSE_SWITCH: False,
CONF_TPI_COEF_INT: 0.3, CONF_TPI_COEF_INT: 0.3,
@@ -149,13 +147,6 @@ async def test_update_central_boiler_state_simple(
assert boiler_binary_sensor is not None assert boiler_binary_sensor is not None
assert boiler_binary_sensor.state == STATE_OFF assert boiler_binary_sensor.state == STATE_OFF
nb_device_active_sensor: NbActiveDeviceForBoilerSensor = search_entity(
hass, "sensor.nb_device_active_for_boiler", "sensor"
)
assert nb_device_active_sensor is not None
assert nb_device_active_sensor.state == 0
assert nb_device_active_sensor.active_device_ids == []
# 1. start a heater # 1. start a heater
with patch( with patch(
"homeassistant.core.ServiceRegistry.async_call" "homeassistant.core.ServiceRegistry.async_call"
@@ -204,9 +195,6 @@ async def test_update_central_boiler_state_simple(
assert api.nb_active_device_for_boiler == 1 assert api.nb_active_device_for_boiler == 1
assert boiler_binary_sensor.state == STATE_ON assert boiler_binary_sensor.state == STATE_ON
assert nb_device_active_sensor.state == 1
assert nb_device_active_sensor.active_device_ids == ["switch.switch1"]
# 2. stop a heater # 2. stop a heater
with patch( with patch(
"homeassistant.core.ServiceRegistry.async_call" "homeassistant.core.ServiceRegistry.async_call"
@@ -247,9 +235,6 @@ async def test_update_central_boiler_state_simple(
assert api.nb_active_device_for_boiler == 0 assert api.nb_active_device_for_boiler == 0
assert boiler_binary_sensor.state == STATE_OFF assert boiler_binary_sensor.state == STATE_OFF
assert nb_device_active_sensor.state == 0
assert nb_device_active_sensor.active_device_ids == []
entity.remove_thermostat() entity.remove_thermostat()
@@ -287,12 +272,10 @@ async def test_update_central_boiler_state_multiple(
CONF_USE_MOTION_FEATURE: False, CONF_USE_MOTION_FEATURE: False,
CONF_USE_POWER_FEATURE: False, CONF_USE_POWER_FEATURE: False,
CONF_USE_PRESENCE_FEATURE: False, CONF_USE_PRESENCE_FEATURE: False,
CONF_UNDERLYING_LIST: [ CONF_HEATER: switch1.entity_id,
switch1.entity_id, CONF_HEATER_2: switch2.entity_id,
switch2.entity_id, CONF_HEATER_3: switch3.entity_id,
switch3.entity_id, CONF_HEATER_4: switch4.entity_id,
switch4.entity_id,
],
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI, CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
CONF_INVERSE_SWITCH: False, CONF_INVERSE_SWITCH: False,
CONF_TPI_COEF_INT: 0.3, CONF_TPI_COEF_INT: 0.3,
@@ -319,18 +302,10 @@ async def test_update_central_boiler_state_multiple(
assert entity.underlying_entities[1].entity_id == "switch.switch2" assert entity.underlying_entities[1].entity_id == "switch.switch2"
assert entity.underlying_entities[2].entity_id == "switch.switch3" assert entity.underlying_entities[2].entity_id == "switch.switch3"
assert entity.underlying_entities[3].entity_id == "switch.switch4" assert entity.underlying_entities[3].entity_id == "switch.switch4"
assert entity.device_actives == [] assert entity.nb_device_actives == 0
assert api.nb_active_device_for_boiler_threshold == 1 assert api.nb_active_device_for_boiler_threshold == 1
assert api.nb_active_device_for_boiler == 0 assert api.nb_active_device_for_boiler == 0
nb_device_active_sensor: NbActiveDeviceForBoilerSensor = search_entity(
hass, "sensor.nb_device_active_for_boiler", "sensor"
)
assert nb_device_active_sensor is not None
assert nb_device_active_sensor.state == 0
assert nb_device_active_sensor.active_device_ids == []
# Force the VTherm to heat # Force the VTherm to heat
await entity.async_set_hvac_mode(HVACMode.HEAT) await entity.async_set_hvac_mode(HVACMode.HEAT)
await entity.async_set_preset_mode(PRESET_BOOST) await entity.async_set_preset_mode(PRESET_BOOST)
@@ -363,7 +338,7 @@ async def test_update_central_boiler_state_multiple(
await asyncio.sleep(0.1) await asyncio.sleep(0.1)
assert entity.hvac_action == HVACAction.HEATING assert entity.hvac_action == HVACAction.HEATING
assert entity.device_actives == ["switch.switch1"] assert entity.nb_device_actives == 1
assert mock_service_call.call_count == 1 assert mock_service_call.call_count == 1
# No switch of the boiler # No switch of the boiler
@@ -381,9 +356,6 @@ async def test_update_central_boiler_state_multiple(
assert api.nb_active_device_for_boiler == 1 assert api.nb_active_device_for_boiler == 1
assert boiler_binary_sensor.state == STATE_OFF assert boiler_binary_sensor.state == STATE_OFF
assert nb_device_active_sensor.state == 1
assert nb_device_active_sensor.active_device_ids == ["switch.switch1"]
# 2. start a 2nd heater # 2. start a 2nd heater
with patch( with patch(
"homeassistant.core.ServiceRegistry.async_call" "homeassistant.core.ServiceRegistry.async_call"
@@ -396,7 +368,7 @@ async def test_update_central_boiler_state_multiple(
await asyncio.sleep(0.1) await asyncio.sleep(0.1)
assert entity.hvac_action == HVACAction.HEATING assert entity.hvac_action == HVACAction.HEATING
assert entity.device_actives == ["switch.switch1", "switch.switch2"] assert entity.nb_device_actives == 2
# Only the first heater is started by the algo # Only the first heater is started by the algo
assert mock_service_call.call_count == 1 assert mock_service_call.call_count == 1
@@ -416,12 +388,6 @@ async def test_update_central_boiler_state_multiple(
assert api.nb_active_device_for_boiler == 2 assert api.nb_active_device_for_boiler == 2
assert boiler_binary_sensor.state == STATE_OFF assert boiler_binary_sensor.state == STATE_OFF
assert nb_device_active_sensor.state == 2
assert nb_device_active_sensor.active_device_ids == [
"switch.switch1",
"switch.switch2",
]
# 3. start a 3rd heater # 3. start a 3rd heater
with patch( with patch(
"homeassistant.core.ServiceRegistry.async_call" "homeassistant.core.ServiceRegistry.async_call"
@@ -470,13 +436,6 @@ async def test_update_central_boiler_state_multiple(
assert api.nb_active_device_for_boiler == 3 assert api.nb_active_device_for_boiler == 3
assert boiler_binary_sensor.state == STATE_ON assert boiler_binary_sensor.state == STATE_ON
assert nb_device_active_sensor.state == 3
assert nb_device_active_sensor.active_device_ids == [
"switch.switch1",
"switch.switch2",
"switch.switch3",
]
# 4. start a 4th heater # 4. start a 4th heater
with patch( with patch(
"homeassistant.core.ServiceRegistry.async_call" "homeassistant.core.ServiceRegistry.async_call"
@@ -507,14 +466,6 @@ async def test_update_central_boiler_state_multiple(
assert api.nb_active_device_for_boiler == 4 assert api.nb_active_device_for_boiler == 4
assert boiler_binary_sensor.state == STATE_ON assert boiler_binary_sensor.state == STATE_ON
assert nb_device_active_sensor.state == 4
assert nb_device_active_sensor.active_device_ids == [
"switch.switch1",
"switch.switch2",
"switch.switch3",
"switch.switch4",
]
# 5. stop a heater # 5. stop a heater
with patch( with patch(
"homeassistant.core.ServiceRegistry.async_call" "homeassistant.core.ServiceRegistry.async_call"
@@ -533,13 +484,6 @@ async def test_update_central_boiler_state_multiple(
assert api.nb_active_device_for_boiler == 3 assert api.nb_active_device_for_boiler == 3
assert boiler_binary_sensor.state == STATE_ON assert boiler_binary_sensor.state == STATE_ON
assert nb_device_active_sensor.state == 3
assert nb_device_active_sensor.active_device_ids == [
"switch.switch2",
"switch.switch3",
"switch.switch4",
]
# 6. stop a 2nd heater # 6. stop a 2nd heater
with patch( with patch(
"homeassistant.core.ServiceRegistry.async_call" "homeassistant.core.ServiceRegistry.async_call"
@@ -580,12 +524,6 @@ async def test_update_central_boiler_state_multiple(
assert api.nb_active_device_for_boiler == 2 assert api.nb_active_device_for_boiler == 2
assert boiler_binary_sensor.state == STATE_OFF assert boiler_binary_sensor.state == STATE_OFF
assert nb_device_active_sensor.state == 2
assert nb_device_active_sensor.active_device_ids == [
"switch.switch2",
"switch.switch3",
]
entity.remove_thermostat() entity.remove_thermostat()
@@ -620,7 +558,7 @@ async def test_update_central_boiler_state_simple_valve(
CONF_USE_MOTION_FEATURE: False, CONF_USE_MOTION_FEATURE: False,
CONF_USE_POWER_FEATURE: False, CONF_USE_POWER_FEATURE: False,
CONF_USE_PRESENCE_FEATURE: False, CONF_USE_PRESENCE_FEATURE: False,
CONF_UNDERLYING_LIST: [valve1.entity_id], CONF_VALVE: valve1.entity_id,
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI, CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
CONF_INVERSE_SWITCH: False, CONF_INVERSE_SWITCH: False,
CONF_TPI_COEF_INT: 0.3, CONF_TPI_COEF_INT: 0.3,
@@ -656,7 +594,7 @@ async def test_update_central_boiler_state_simple_valve(
now: datetime = datetime.now(tz=tz) now: datetime = datetime.now(tz=tz)
assert entity.hvac_mode == HVACMode.HEAT assert entity.hvac_mode == HVACMode.HEAT
assert entity.device_actives == [] assert entity.nb_device_actives == 0
boiler_binary_sensor: CentralBoilerBinarySensor = search_entity( boiler_binary_sensor: CentralBoilerBinarySensor = search_entity(
hass, "binary_sensor.central_boiler", "binary_sensor" hass, "binary_sensor.central_boiler", "binary_sensor"
@@ -664,13 +602,6 @@ async def test_update_central_boiler_state_simple_valve(
assert boiler_binary_sensor is not None assert boiler_binary_sensor is not None
assert boiler_binary_sensor.state == STATE_OFF assert boiler_binary_sensor.state == STATE_OFF
nb_device_active_sensor: NbActiveDeviceForBoilerSensor = search_entity(
hass, "sensor.nb_device_active_for_boiler", "sensor"
)
assert nb_device_active_sensor is not None
assert nb_device_active_sensor.state == 0
assert nb_device_active_sensor.active_device_ids == []
# 1. start a valve # 1. start a valve
with patch( with patch(
"homeassistant.core.ServiceRegistry.async_call" "homeassistant.core.ServiceRegistry.async_call"
@@ -685,7 +616,7 @@ async def test_update_central_boiler_state_simple_valve(
await asyncio.sleep(0.1) await asyncio.sleep(0.1)
assert entity.hvac_action == HVACAction.HEATING assert entity.hvac_action == HVACAction.HEATING
assert entity.device_actives == ["number.valve1"] assert entity.nb_device_actives == 1
assert mock_service_call.call_count >= 1 assert mock_service_call.call_count >= 1
mock_service_call.assert_has_calls( mock_service_call.assert_has_calls(
@@ -713,11 +644,6 @@ async def test_update_central_boiler_state_simple_valve(
assert api.nb_active_device_for_boiler == 1 assert api.nb_active_device_for_boiler == 1
assert boiler_binary_sensor.state == STATE_ON assert boiler_binary_sensor.state == STATE_ON
assert nb_device_active_sensor.state == 1
assert nb_device_active_sensor.active_device_ids == [
"number.valve1",
]
# 2. stop a heater # 2. stop a heater
with patch( with patch(
"homeassistant.core.ServiceRegistry.async_call" "homeassistant.core.ServiceRegistry.async_call"
@@ -732,7 +658,7 @@ async def test_update_central_boiler_state_simple_valve(
await asyncio.sleep(0.1) await asyncio.sleep(0.1)
assert entity.hvac_action == HVACAction.IDLE assert entity.hvac_action == HVACAction.IDLE
assert entity.device_actives == [] assert entity.nb_device_actives == 0
assert mock_service_call.call_count >= 1 assert mock_service_call.call_count >= 1
mock_service_call.assert_has_calls( mock_service_call.assert_has_calls(
@@ -761,9 +687,6 @@ async def test_update_central_boiler_state_simple_valve(
assert api.nb_active_device_for_boiler == 0 assert api.nb_active_device_for_boiler == 0
assert boiler_binary_sensor.state == STATE_OFF assert boiler_binary_sensor.state == STATE_OFF
assert nb_device_active_sensor.state == 0
assert nb_device_active_sensor.active_device_ids == []
entity.remove_thermostat() entity.remove_thermostat()
@@ -798,7 +721,7 @@ async def test_update_central_boiler_state_simple_climate(
CONF_USE_MOTION_FEATURE: False, CONF_USE_MOTION_FEATURE: False,
CONF_USE_POWER_FEATURE: False, CONF_USE_POWER_FEATURE: False,
CONF_USE_PRESENCE_FEATURE: False, CONF_USE_PRESENCE_FEATURE: False,
CONF_UNDERLYING_LIST: [climate1.entity_id], CONF_CLIMATE: climate1.entity_id,
CONF_MINIMAL_ACTIVATION_DELAY: 30, CONF_MINIMAL_ACTIVATION_DELAY: 30,
CONF_SECURITY_DELAY_MIN: 5, CONF_SECURITY_DELAY_MIN: 5,
CONF_SECURITY_MIN_ON_PERCENT: 0.3, CONF_SECURITY_MIN_ON_PERCENT: 0.3,
@@ -825,13 +748,6 @@ async def test_update_central_boiler_state_simple_climate(
assert api.nb_active_device_for_boiler_threshold == 1 assert api.nb_active_device_for_boiler_threshold == 1
assert api.nb_active_device_for_boiler == 0 assert api.nb_active_device_for_boiler == 0
nb_device_active_sensor: NbActiveDeviceForBoilerSensor = search_entity(
hass, "sensor.nb_device_active_for_boiler", "sensor"
)
assert nb_device_active_sensor is not None
assert nb_device_active_sensor.state == 0
assert nb_device_active_sensor.active_device_ids == []
# Force the VTherm to heat # Force the VTherm to heat
await entity.async_set_hvac_mode(HVACMode.HEAT) await entity.async_set_hvac_mode(HVACMode.HEAT)
await entity.async_set_preset_mode(PRESET_BOOST) await entity.async_set_preset_mode(PRESET_BOOST)
@@ -840,7 +756,7 @@ async def test_update_central_boiler_state_simple_climate(
now: datetime = datetime.now(tz=tz) now: datetime = datetime.now(tz=tz)
assert entity.hvac_mode == HVACMode.HEAT assert entity.hvac_mode == HVACMode.HEAT
assert entity.device_actives == [] assert entity.nb_device_actives == 0
boiler_binary_sensor: CentralBoilerBinarySensor = search_entity( boiler_binary_sensor: CentralBoilerBinarySensor = search_entity(
hass, "binary_sensor.central_boiler", "binary_sensor" hass, "binary_sensor.central_boiler", "binary_sensor"
@@ -863,7 +779,7 @@ async def test_update_central_boiler_state_simple_climate(
await asyncio.sleep(0.5) await asyncio.sleep(0.5)
assert entity.hvac_action == HVACAction.HEATING assert entity.hvac_action == HVACAction.HEATING
assert entity.device_actives == ["climate.climate1"] assert entity.nb_device_actives == 1
assert mock_service_call.call_count >= 1 assert mock_service_call.call_count >= 1
mock_service_call.assert_has_calls( mock_service_call.assert_has_calls(
@@ -891,11 +807,6 @@ async def test_update_central_boiler_state_simple_climate(
assert api.nb_active_device_for_boiler == 1 assert api.nb_active_device_for_boiler == 1
assert boiler_binary_sensor.state == STATE_ON assert boiler_binary_sensor.state == STATE_ON
assert nb_device_active_sensor.state == 1
assert nb_device_active_sensor.active_device_ids == [
"climate.climate1",
]
# 2. stop a climate # 2. stop a climate
with patch( with patch(
"homeassistant.core.ServiceRegistry.async_call" "homeassistant.core.ServiceRegistry.async_call"
@@ -910,7 +821,7 @@ async def test_update_central_boiler_state_simple_climate(
await asyncio.sleep(0.5) await asyncio.sleep(0.5)
assert entity.hvac_action == HVACAction.IDLE assert entity.hvac_action == HVACAction.IDLE
assert entity.device_actives == [] assert entity.nb_device_actives == 0
assert mock_service_call.call_count >= 1 assert mock_service_call.call_count >= 1
mock_service_call.assert_has_calls( mock_service_call.assert_has_calls(
@@ -939,277 +850,6 @@ async def test_update_central_boiler_state_simple_climate(
assert api.nb_active_device_for_boiler == 0 assert api.nb_active_device_for_boiler == 0
assert boiler_binary_sensor.state == STATE_OFF assert boiler_binary_sensor.state == STATE_OFF
assert nb_device_active_sensor.state == 0
assert nb_device_active_sensor.active_device_ids == []
entity.remove_thermostat()
async def test_update_central_boiler_state_simple_climate_valve_regulation(
hass: HomeAssistant,
# skip_hass_states_is_state,
# skip_hass_states_get,
init_central_config_with_boiler_fixture,
):
"""Test that the central boiler state behavior with a climate with valve regulation"""
api = VersatileThermostatAPI.get_vtherm_api(hass)
climate1 = MockClimate(hass, "climate1", "theClimate1")
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_UNDERLYING_LIST: [climate1.entity_id],
CONF_OPENING_DEGREE_LIST: ["number.mock_opening_degree"],
CONF_CLOSING_DEGREE_LIST: [],
CONF_OFFSET_CALIBRATION_LIST: [],
CONF_AUTO_REGULATION_MODE: CONF_AUTO_REGULATION_VALVE,
CONF_AUTO_REGULATION_DTEMP: 0,
CONF_AUTO_REGULATION_PERIOD_MIN: 0,
CONF_TPI_COEF_INT: 0.3,
CONF_TPI_COEF_EXT: 0.1,
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
CONF_AUTO_FAN_MODE: CONF_AUTO_FAN_HIGH,
CONF_AUTO_REGULATION_USE_DEVICE_TEMP: False,
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,
},
)
open_degree_entity = MockNumber(hass, "mock_opening_degree", "Opening degree")
open_degree_entity.set_native_value(0)
# mock_get_state will be called for each OPENING/CLOSING/OFFSET_CALIBRATION list
mock_get_state_side_effect = SideEffects(
{
open_degree_entity.entity_id: State(
open_degree_entity.entity_id,
open_degree_entity.state,
{"min": 0, "max": 100},
),
"number.mock_closing_degree": State(
"number.mock_closing_degree", "0", {"min": 0, "max": 100}
),
"number.mock_offset_calibration": State(
"number.mock_offset_calibration", "0", {"min": -12, "max": 12}
),
},
State("unknown.entity_id", "unknown"),
)
with patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
return_value=climate1,
), patch(
"homeassistant.core.StateMachine.get",
side_effect=mock_get_state_side_effect.get_side_effects(),
):
entity: ThermostatOverClimate = 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
assert api.nb_active_device_for_boiler == 0
nb_device_active_sensor: NbActiveDeviceForBoilerSensor = search_entity(
hass, "sensor.nb_device_active_for_boiler", "sensor"
)
assert nb_device_active_sensor is not None
assert nb_device_active_sensor.state == 0
assert nb_device_active_sensor.active_device_ids == []
# Force the VTherm to heat
tz = get_tz(hass) # pylint: disable=invalid-name
now: datetime = datetime.now(tz=tz)
entity._set_now(now)
await send_temperature_change_event(entity, 30, now)
await send_ext_temperature_change_event(entity, 30, now)
await hass.async_block_till_done()
await entity.async_set_hvac_mode(HVACMode.HEAT)
await entity.async_set_preset_mode(PRESET_BOOST)
# the VTherm should not heat now
assert entity.hvac_mode == HVACMode.HEAT
assert entity.hvac_action == HVACAction.OFF
assert entity.activable_underlying_entities[0]._percent_open == 0
assert entity.device_actives == []
boiler_binary_sensor: CentralBoilerBinarySensor = search_entity(
hass, "binary_sensor.central_boiler", "binary_sensor"
)
assert boiler_binary_sensor is not None
assert boiler_binary_sensor.state == STATE_OFF
# 1. start a climate
open_degree_entity.set_native_value(100)
mock_get_state_side_effect = SideEffects(
{
open_degree_entity.entity_id: State(
open_degree_entity.entity_id,
open_degree_entity.state,
{"min": 0, "max": 100},
),
"number.mock_closing_degree": State(
"number.mock_closing_degree", "0", {"min": 0, "max": 100}
),
"number.mock_offset_calibration": State(
"number.mock_offset_calibration", "0", {"min": -12, "max": 12}
),
},
State("unknown.entity_id", "unknown"),
)
with patch(
"homeassistant.core.ServiceRegistry.async_call"
) as mock_service_call, patch(
"custom_components.versatile_thermostat.binary_sensor.send_vtherm_event"
) as mock_send_event, patch(
"homeassistant.core.StateMachine.get",
side_effect=mock_get_state_side_effect.get_side_effects(),
):
now = now + timedelta(minutes=1)
entity._set_now(now)
await send_temperature_change_event(entity, 10, now)
# we have to simulate the climate also else the test don't work
climate1.set_hvac_mode(HVACMode.HEAT)
climate1.set_hvac_action(HVACAction.HEATING)
climate1.async_write_ha_state()
open_degree_entity.set_native_value(100)
# Wait for state event propagation
await hass.async_block_till_done()
assert entity.hvac_action == HVACAction.HEATING
assert entity.device_actives == ["number.mock_opening_degree"]
assert api.nb_active_device_for_boiler == 1
assert boiler_binary_sensor.state == STATE_ON
assert nb_device_active_sensor.state == 1
assert nb_device_active_sensor.active_device_ids == [
"number.mock_opening_degree",
]
assert mock_service_call.call_count >= 1
mock_service_call.assert_has_calls(
[
call.service_call(
"switch",
"turn_on",
service_data={},
target={"entity_id": "switch.pompe_chaudiere"},
),
]
)
assert mock_send_event.call_count >= 1
mock_send_event.assert_has_calls(
[
call.send_vtherm_event(
hass=hass,
event_type=EventType.CENTRAL_BOILER_EVENT,
entity=api.central_boiler_entity,
data={"central_boiler": True},
)
]
)
# 2. stop a climate
open_degree_entity.set_native_value(0)
mock_get_state_side_effect = SideEffects(
{
open_degree_entity.entity_id: State(
open_degree_entity.entity_id,
open_degree_entity.state,
{"min": 0, "max": 100},
),
"number.mock_closing_degree": State(
"number.mock_closing_degree", "0", {"min": 0, "max": 100}
),
"number.mock_offset_calibration": State(
"number.mock_offset_calibration", "0", {"min": -12, "max": 12}
),
},
State("unknown.entity_id", "unknown"),
)
with patch(
"homeassistant.core.ServiceRegistry.async_call"
) as mock_service_call, patch(
"custom_components.versatile_thermostat.binary_sensor.send_vtherm_event"
) as mock_send_event, patch(
"homeassistant.core.StateMachine.get",
side_effect=mock_get_state_side_effect.get_side_effects(),
):
await send_temperature_change_event(entity, 25, now)
climate1.set_hvac_mode(HVACMode.HEAT)
climate1.set_hvac_action(HVACAction.IDLE)
climate1.async_write_ha_state()
open_degree_entity.set_native_value(0)
# Wait for state event propagation
await asyncio.sleep(0.5)
assert entity.hvac_action == HVACAction.OFF
assert entity.device_actives == []
assert mock_service_call.call_count >= 1
mock_service_call.assert_has_calls(
[
call(
"switch",
"turn_off",
service_data={},
target={"entity_id": "switch.pompe_chaudiere"},
)
]
)
assert mock_send_event.call_count >= 1
mock_send_event.assert_has_calls(
[
call.send_vtherm_event(
hass=hass,
event_type=EventType.CENTRAL_BOILER_EVENT,
entity=api.central_boiler_entity,
data={"central_boiler": False},
)
]
)
assert api.nb_active_device_for_boiler == 0
assert boiler_binary_sensor.state == STATE_OFF
assert nb_device_active_sensor.state == 0
assert nb_device_active_sensor.active_device_ids == []
entity.remove_thermostat() entity.remove_thermostat()
-3
View File
@@ -1581,7 +1581,6 @@ async def test_user_config_flow_over_climate_valve(
CONF_OFFSET_CALIBRATION_LIST: ["number.offset_calibration1"], CONF_OFFSET_CALIBRATION_LIST: ["number.offset_calibration1"],
CONF_OPENING_DEGREE_LIST: ["number.opening_degree1"], CONF_OPENING_DEGREE_LIST: ["number.opening_degree1"],
CONF_CLOSING_DEGREE_LIST: ["number.closing_degree1"], CONF_CLOSING_DEGREE_LIST: ["number.closing_degree1"],
CONF_MIN_OPENING_DEGREES: "10, 20,0",
}, },
) )
assert result["type"] == FlowResultType.FORM assert result["type"] == FlowResultType.FORM
@@ -1620,7 +1619,6 @@ async def test_user_config_flow_over_climate_valve(
"number.opening_degree2", "number.opening_degree2",
], ],
CONF_CLOSING_DEGREE_LIST: [], CONF_CLOSING_DEGREE_LIST: [],
CONF_MIN_OPENING_DEGREES: "10, 20,0",
}, },
) )
assert result["type"] == FlowResultType.MENU assert result["type"] == FlowResultType.MENU
@@ -1717,7 +1715,6 @@ async def test_user_config_flow_over_climate_valve(
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI, CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
CONF_TPI_COEF_INT: 0.3, CONF_TPI_COEF_INT: 0.3,
CONF_TPI_COEF_EXT: 0.1, CONF_TPI_COEF_EXT: 0.1,
CONF_MIN_OPENING_DEGREES: "10, 20,0",
} }
assert result["result"] assert result["result"]
assert result["result"].domain == DOMAIN assert result["result"].domain == DOMAIN
-6
View File
@@ -84,8 +84,6 @@ async def test_last_seen_feature(hass: HomeAssistant, skip_hass_states_is_state)
await entity.async_set_hvac_mode(HVACMode.HEAT) await entity.async_set_hvac_mode(HVACMode.HEAT)
assert entity.hvac_mode == HVACMode.HEAT assert entity.hvac_mode == HVACMode.HEAT
last_change_time_from_vtherm = entity._last_change_time_from_vtherm
# 2. activate security feature when date is expired # 2. activate security feature when date is expired
with patch( with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event" "custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
@@ -130,13 +128,9 @@ async def test_last_seen_feature(hass: HomeAssistant, skip_hass_states_is_state)
assert mock_heater_on.call_count == 1 assert mock_heater_on.call_count == 1
assert entity._last_change_time_from_vtherm == last_change_time_from_vtherm
# 3. change the last seen sensor # 3. change the last seen sensor
event_timestamp = now - timedelta(minutes=4) event_timestamp = now - timedelta(minutes=4)
await send_last_seen_temperature_change_event(entity, event_timestamp) await send_last_seen_temperature_change_event(entity, event_timestamp)
assert entity.security_state is False assert entity.security_state is False
assert entity.preset_mode is PRESET_COMFORT assert entity.preset_mode is PRESET_COMFORT
assert entity._last_temperature_measure == event_timestamp assert entity._last_temperature_measure == event_timestamp
assert entity._last_change_time_from_vtherm == last_change_time_from_vtherm
+4 -4
View File
@@ -323,7 +323,7 @@ async def test_underlying_change_follow(
assert entity.target_temperature == entity.min_temp + 1 assert entity.target_temperature == entity.min_temp + 1
assert entity.preset_mode is PRESET_NONE assert entity.preset_mode is PRESET_NONE
# 4. Change the target temp with < 0.1 (step) value. The value should not be taken # 4. Change the target temp with < 1 value. The value should not be taken
# Wait 11 sec # Wait 11 sec
event_timestamp = now + timedelta(seconds=11) event_timestamp = now + timedelta(seconds=11)
await send_climate_change_event_with_temperature( await send_climate_change_event_with_temperature(
@@ -333,7 +333,7 @@ async def test_underlying_change_follow(
HVACAction.OFF, HVACAction.OFF,
HVACAction.OFF, HVACAction.OFF,
event_timestamp, event_timestamp,
entity.min_temp + 1.09, entity.min_temp + 1.5,
True, True,
"climate.mock_climate", # the underlying climate entity id "climate.mock_climate", # the underlying climate entity id
) )
@@ -949,7 +949,7 @@ async def test_manual_hvac_off_should_take_the_lead_over_window(
now: datetime = datetime.now(tz=tz) now: datetime = datetime.now(tz=tz)
# 1. Set mode to Heat and preset to Comfort and close the window # 1. Set mode to Heat and preset to Comfort and close the window
await send_window_change_event(vtherm, False, False, now, False) send_window_change_event(vtherm, False, False, now, False)
await send_presence_change_event(vtherm, True, False, now) await send_presence_change_event(vtherm, True, False, now)
await send_temperature_change_event(vtherm, 18, now, True) await send_temperature_change_event(vtherm, 18, now, True)
await vtherm.async_set_hvac_mode(HVACMode.HEAT) await vtherm.async_set_hvac_mode(HVACMode.HEAT)
@@ -1123,7 +1123,7 @@ async def test_manual_hvac_off_should_take_the_lead_over_auto_start_stop(
now: datetime = datetime.now(tz=tz) now: datetime = datetime.now(tz=tz)
# 1. Set mode to Heat and preset to Comfort # 1. Set mode to Heat and preset to Comfort
await send_window_change_event(vtherm, False, False, now, False) send_window_change_event(vtherm, False, False, now, False)
await send_presence_change_event(vtherm, True, False, now) await send_presence_change_event(vtherm, True, False, now)
await send_temperature_change_event(vtherm, 18, now, True) await send_temperature_change_event(vtherm, 18, now, True)
await vtherm.async_set_hvac_mode(HVACMode.HEAT) await vtherm.async_set_hvac_mode(HVACMode.HEAT)
+4 -188
View File
@@ -18,8 +18,8 @@ from .const import *
logging.getLogger().setLevel(logging.DEBUG) logging.getLogger().setLevel(logging.DEBUG)
@pytest.mark.parametrize("expected_lingering_tasks", [True]) # @pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True]) # @pytest.mark.parametrize("expected_lingering_timers", [True])
# this test fails if run in // with the next because the underlying_valve_regulation is mixed. Don't know why # this test fails if run in // with the next because the underlying_valve_regulation is mixed. Don't know why
# @pytest.mark.skip # @pytest.mark.skip
async def test_over_climate_valve_mono(hass: HomeAssistant, skip_hass_states_get): async def test_over_climate_valve_mono(hass: HomeAssistant, skip_hass_states_get):
@@ -138,13 +138,13 @@ async def test_over_climate_valve_mono(hass: HomeAssistant, skip_hass_states_get
assert mock_service_call.call_count == 3 assert mock_service_call.call_count == 3
mock_service_call.assert_has_calls( mock_service_call.assert_has_calls(
[ [
call(domain='number', service='set_value', service_data={'value': 0}, target={'entity_id': 'number.mock_opening_degree'}),
call(domain='number', service='set_value', service_data={'value': 100}, target={'entity_id': 'number.mock_closing_degree'}),
call("climate","set_temperature",{ call("climate","set_temperature",{
"entity_id": "climate.mock_climate", "entity_id": "climate.mock_climate",
"temperature": 15, # temp-min "temperature": 15, # temp-min
}, },
), ),
call(domain='number', service='set_value', service_data={'value': 0}, target={'entity_id': 'number.mock_opening_degree'}),
call(domain='number', service='set_value', service_data={'value': 100}, target={'entity_id': 'number.mock_closing_degree'}),
# we have no current_temperature yet # we have no current_temperature yet
# call(domain='number', service='set_value', service_data={'value': 12}, target={'entity_id': 'number.mock_offset_calibration'}), # call(domain='number', service='set_value', service_data={'value': 12}, target={'entity_id': 'number.mock_offset_calibration'}),
] ]
@@ -300,7 +300,6 @@ async def test_over_climate_valve_mono(hass: HomeAssistant, skip_hass_states_get
await hass.async_block_till_done() await hass.async_block_till_done()
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_over_climate_valve_multi_presence( async def test_over_climate_valve_multi_presence(
hass: HomeAssistant, skip_hass_states_get hass: HomeAssistant, skip_hass_states_get
): ):
@@ -483,186 +482,3 @@ async def test_over_climate_valve_multi_presence(
) )
assert vtherm.nb_device_actives == 0 assert vtherm.nb_device_actives == 0
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_over_climate_valve_multi_min_opening_degrees(
hass: HomeAssistant, skip_hass_states_get
):
"""Test the normal full start of a thermostat in thermostat_over_climate type
with valve_regulation and min_opening_degreess set"""
entry = MockConfigEntry(
domain=DOMAIN,
title="TheOverClimateMockName",
unique_id="uniqueId",
data={
CONF_NAME: "TheOverClimateMockName",
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
CONF_CYCLE_MIN: 5,
CONF_DEVICE_POWER: 1,
CONF_USE_MAIN_CENTRAL_CONFIG: False,
CONF_USE_CENTRAL_MODE: False,
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_CLIMATE,
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
CONF_TEMP_MIN: 15,
CONF_TEMP_MAX: 30,
CONF_STEP_TEMPERATURE: 0.1,
CONF_UNDERLYING_LIST: ["climate.mock_climate1", "climate.mock_climate2"],
CONF_AC_MODE: False,
CONF_AUTO_REGULATION_MODE: CONF_AUTO_REGULATION_VALVE,
CONF_AUTO_REGULATION_DTEMP: 0.01,
CONF_AUTO_REGULATION_PERIOD_MIN: 0,
CONF_AUTO_FAN_MODE: CONF_AUTO_FAN_HIGH,
CONF_AUTO_REGULATION_USE_DEVICE_TEMP: False,
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
CONF_TPI_COEF_INT: 0.3,
CONF_TPI_COEF_EXT: 0.1,
CONF_OPENING_DEGREE_LIST: [
"number.mock_opening_degree1",
"number.mock_opening_degree2",
],
CONF_CLOSING_DEGREE_LIST: [
"number.mock_closing_degree1",
"number.mock_closing_degree2",
],
CONF_OFFSET_CALIBRATION_LIST: [
"number.mock_offset_calibration1",
"number.mock_offset_calibration2",
],
CONF_USE_PRESENCE_FEATURE: False,
CONF_USE_WINDOW_FEATURE: False,
CONF_USE_MOTION_FEATURE: False,
CONF_USE_POWER_FEATURE: False,
CONF_MIN_OPENING_DEGREES: "60,70",
}
| MOCK_DEFAULT_CENTRAL_CONFIG
| MOCK_ADVANCED_CONFIG,
)
fake_underlying_climate1 = MockClimate(
hass, "mockUniqueId1", "MockClimateName1", {}
)
fake_underlying_climate2 = MockClimate(
hass, "mockUniqueId2", "MockClimateName2", {}
)
# mock_get_state will be called for each OPENING/CLOSING/OFFSET_CALIBRATION list
mock_get_state_side_effect = SideEffects(
{
# Valve 1 is open
"number.mock_opening_degree1": State(
"number.mock_opening_degree1", "10", {"min": 0, "max": 100}
),
"number.mock_closing_degree1": State(
"number.mock_closing_degree1", "90", {"min": 0, "max": 100}
),
"number.mock_offset_calibration1": State(
"number.mock_offset_calibration1", "0", {"min": -12, "max": 12}
),
# Valve 2 is closed
"number.mock_opening_degree2": State(
"number.mock_opening_degree2", "0", {"min": 0, "max": 100}
),
"number.mock_closing_degree2": State(
"number.mock_closing_degree2", "100", {"min": 0, "max": 100}
),
"number.mock_offset_calibration2": State(
"number.mock_offset_calibration2", "10", {"min": -12, "max": 12}
),
},
State("unknown.entity_id", "unknown"),
)
# 1. initialize the VTherm
tz = get_tz(hass) # pylint: disable=invalid-name
now: datetime = datetime.now(tz=tz)
# fmt: off
with patch("custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event") as mock_send_event, \
patch("custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate", side_effect=[fake_underlying_climate1, fake_underlying_climate2]) as mock_find_climate, \
patch("homeassistant.core.ServiceRegistry.async_call") as mock_service_call,\
patch("homeassistant.core.StateMachine.get", side_effect=mock_get_state_side_effect.get_side_effects()) as mock_get_state:
# fmt: on
vtherm: ThermostatOverClimateValve = await create_thermostat(hass, entry, "climate.theoverclimatemockname")
assert vtherm
assert isinstance(vtherm, ThermostatOverClimateValve)
assert vtherm.name == "TheOverClimateMockName"
assert vtherm.is_over_climate is True
assert vtherm.have_valve_regulation is True
vtherm._set_now(now)
# initialize the temps
await set_all_climate_preset_temp(hass, vtherm, default_temperatures, "theoverclimatemockname")
await send_temperature_change_event(vtherm, 20, now, True)
await send_ext_temperature_change_event(vtherm, 20, now, True)
await send_presence_change_event(vtherm, False, True, now)
await vtherm.async_set_preset_mode(PRESET_COMFORT)
await vtherm.async_set_hvac_mode(HVACMode.HEAT)
assert vtherm.target_temperature == 19
assert vtherm.nb_device_actives == 0
# 2: set temperature -> should activate the valve and change target
# fmt: off
with patch("custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event") as mock_send_event, \
patch("homeassistant.core.ServiceRegistry.async_call") as mock_service_call,\
patch("homeassistant.core.StateMachine.get", side_effect=mock_get_state_side_effect.get_side_effects()) as mock_get_state:
# fmt: on
now = now + timedelta(minutes=3)
vtherm._set_now(now)
await send_temperature_change_event(vtherm, 18, now, True)
await hass.async_block_till_done()
assert vtherm.is_device_active is True
assert vtherm.valve_open_percent == 20
# the underlying set temperature call and the call to the valve
assert mock_service_call.call_count == 6
mock_service_call.assert_has_calls([
# min is 60
call(domain='number', service='set_value', service_data={'value': 60}, target={'entity_id': 'number.mock_opening_degree1'}),
call(domain='number', service='set_value', service_data={'value': 40}, target={'entity_id': 'number.mock_closing_degree1'}),
call(domain='number', service='set_value', service_data={'value': 3.0}, target={'entity_id': 'number.mock_offset_calibration1'}),
call(domain='number', service='set_value', service_data={'value': 70}, target={'entity_id': 'number.mock_opening_degree2'}),
call(domain='number', service='set_value', service_data={'value': 30}, target={'entity_id': 'number.mock_closing_degree2'}),
call(domain='number', service='set_value', service_data={'value': 12}, target={'entity_id': 'number.mock_offset_calibration2'})
]
)
assert vtherm.nb_device_actives >= 2 # should be 2 but when run in // with the first test it give 3
# 3: set high temperature -> should deactivate the valve and change target
# fmt: off
with patch("custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event") as mock_send_event, \
patch("homeassistant.core.ServiceRegistry.async_call") as mock_service_call,\
patch("homeassistant.core.StateMachine.get", side_effect=mock_get_state_side_effect.get_side_effects()) as mock_get_state:
# fmt: on
now = now + timedelta(minutes=3)
vtherm._set_now(now)
await send_temperature_change_event(vtherm, 22, now, True)
await hass.async_block_till_done()
assert vtherm.is_device_active is False
assert vtherm.valve_open_percent == 0
# the underlying set temperature call and the call to the valve
assert mock_service_call.call_count == 6
mock_service_call.assert_has_calls([
call(domain='number', service='set_value', service_data={'value': 0}, target={'entity_id': 'number.mock_opening_degree1'}),
call(domain='number', service='set_value', service_data={'value': 100}, target={'entity_id': 'number.mock_closing_degree1'}),
call(domain='number', service='set_value', service_data={'value': 7.0}, target={'entity_id': 'number.mock_offset_calibration1'}),
call(domain='number', service='set_value', service_data={'value': 0}, target={'entity_id': 'number.mock_opening_degree2'}),
call(domain='number', service='set_value', service_data={'value': 100}, target={'entity_id': 'number.mock_closing_degree2'}),
call(domain='number', service='set_value', service_data={'value': 12}, target={'entity_id': 'number.mock_offset_calibration2'})
]
)
assert vtherm.nb_device_actives == 0
+3 -223
View File
@@ -246,7 +246,7 @@ async def test_window_management_time_enough(
assert entity.preset_mode is PRESET_BOOST assert entity.preset_mode is PRESET_BOOST
assert entity.hvac_mode is HVACMode.HEAT assert entity.hvac_mode is HVACMode.HEAT
assert entity._saved_hvac_mode is HVACMode.HEAT # No change assert entity._saved_hvac_mode is HVACMode.HEAT # No change
assert entity.hvac_off_reason is None assert entity.hvac_off_reason == None
# Clean the entity # Clean the entity
entity.remove_thermostat() entity.remove_thermostat()
@@ -1762,7 +1762,7 @@ async def test_window_action_frost_temp(hass: HomeAssistant, skip_hass_states_is
CONF_USE_MOTION_FEATURE: False, CONF_USE_MOTION_FEATURE: False,
CONF_USE_POWER_FEATURE: False, CONF_USE_POWER_FEATURE: False,
CONF_USE_PRESENCE_FEATURE: False, CONF_USE_PRESENCE_FEATURE: False,
CONF_UNDERLYING_LIST: ["switch.mock_switch"], CONF_HEATER: "switch.mock_switch",
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI, CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
CONF_TPI_COEF_INT: 0.3, CONF_TPI_COEF_INT: 0.3,
CONF_TPI_COEF_EXT: 0.01, CONF_TPI_COEF_EXT: 0.01,
@@ -1927,7 +1927,7 @@ async def test_window_action_frost_temp(hass: HomeAssistant, skip_hass_states_is
assert entity.hvac_mode is HVACMode.HEAT assert entity.hvac_mode is HVACMode.HEAT
# No change on preset # No change on preset
assert entity.preset_mode is PRESET_BOOST assert entity.preset_mode is PRESET_BOOST
# The Boost temp # The eco temp
assert entity.target_temperature == 21 assert entity.target_temperature == 21
# Clean the entity # Clean the entity
@@ -2091,223 +2091,3 @@ async def test_bug_66(
assert entity.window_state == STATE_OFF assert entity.window_state == STATE_OFF
assert entity.hvac_mode is HVACMode.HEAT assert entity.hvac_mode is HVACMode.HEAT
assert entity.preset_mode is PRESET_BOOST assert entity.preset_mode is PRESET_BOOST
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_window_action_frost_temp_preset_change(
hass: HomeAssistant, skip_hass_states_is_state
):
"""Test the Window management with the frost_temp option and change the preset during
the window is open. This should restore the new preset temperature"""
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,
CONF_USE_WINDOW_FEATURE: True,
CONF_USE_MOTION_FEATURE: False,
CONF_USE_POWER_FEATURE: False,
CONF_USE_PRESENCE_FEATURE: False,
CONF_UNDERLYING_LIST: ["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_WINDOW_ACTION: CONF_WINDOW_FROST_TEMP,
CONF_WINDOW_SENSOR: "binary_sensor.fake_window_sensor",
CONF_WINDOW_DELAY: 1,
},
)
vtherm: BaseThermostat = await create_thermostat(
hass, entry, "climate.theoverswitchmockname"
)
assert vtherm
await set_all_climate_preset_temp(
hass, vtherm, default_temperatures, "theoverswitchmockname"
)
tz = get_tz(hass) # pylint: disable=invalid-name
now = datetime.now(tz)
await vtherm.async_set_hvac_mode(HVACMode.HEAT)
await vtherm.async_set_preset_mode(PRESET_BOOST)
await hass.async_block_till_done()
assert vtherm.hvac_mode is HVACMode.HEAT
assert vtherm.preset_mode is PRESET_BOOST
assert vtherm.target_temperature == 21
assert vtherm.window_state is STATE_OFF
assert vtherm.is_window_auto_enabled is False
# 1. Turn on the window sensor
now = now + timedelta(minutes=1)
vtherm._set_now(now)
with patch("homeassistant.helpers.condition.state", return_value=True):
try_function = await send_window_change_event(vtherm, True, False, now)
now = now + timedelta(minutes=2)
vtherm._set_now(now)
await try_function(None)
# VTherm should have taken the window action
assert vtherm.target_temperature == 7 # Frost
# No change
assert vtherm.preset_mode is PRESET_BOOST
assert vtherm.hvac_mode is HVACMode.HEAT
# 2. Change the preset to comfort
now = now + timedelta(minutes=1)
vtherm._set_now(now)
await vtherm.async_set_preset_mode(PRESET_COMFORT)
await hass.async_block_till_done()
# VTherm should have taken the new preset temperature
assert vtherm.target_temperature == 7 # frost (window is still open)
assert vtherm.preset_mode is PRESET_COMFORT
assert vtherm.hvac_mode is HVACMode.HEAT
# 3.Turn off the window sensor
now = now + timedelta(minutes=1)
vtherm._set_now(now)
with patch("homeassistant.helpers.condition.state", return_value=True):
try_function = await send_window_change_event(vtherm, False, True, now)
now = now + timedelta(minutes=2)
vtherm._set_now(now)
await try_function(None)
# VTherm should have restore the Comfort preset temperature
assert vtherm.target_temperature == 19 # restore comfort
# No change
assert vtherm.preset_mode is PRESET_COMFORT
assert vtherm.hvac_mode is HVACMode.HEAT
# Clean the entity
vtherm.remove_thermostat()
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_window_action_frost_temp_temp_change(
hass: HomeAssistant, skip_hass_states_is_state
):
"""Test the Window management with the frost_temp option and change the target temp during
the window is open. This should restore the new temperature"""
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,
CONF_USE_WINDOW_FEATURE: True,
CONF_USE_MOTION_FEATURE: False,
CONF_USE_POWER_FEATURE: False,
CONF_USE_PRESENCE_FEATURE: False,
CONF_UNDERLYING_LIST: ["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_WINDOW_ACTION: CONF_WINDOW_FROST_TEMP,
CONF_WINDOW_SENSOR: "binary_sensor.fake_window_sensor",
CONF_WINDOW_DELAY: 1,
},
)
vtherm: BaseThermostat = await create_thermostat(
hass, entry, "climate.theoverswitchmockname"
)
assert vtherm
await set_all_climate_preset_temp(
hass, vtherm, default_temperatures, "theoverswitchmockname"
)
tz = get_tz(hass) # pylint: disable=invalid-name
now = datetime.now(tz)
await vtherm.async_set_hvac_mode(HVACMode.HEAT)
await vtherm.async_set_preset_mode(PRESET_BOOST)
await hass.async_block_till_done()
assert vtherm.hvac_mode is HVACMode.HEAT
assert vtherm.preset_mode is PRESET_BOOST
assert vtherm.target_temperature == 21
assert vtherm.window_state is STATE_OFF
assert vtherm.is_window_auto_enabled is False
# 1. Turn on the window sensor
now = now + timedelta(minutes=1)
vtherm._set_now(now)
with patch("homeassistant.helpers.condition.state", return_value=True):
try_function = await send_window_change_event(vtherm, True, False, now)
now = now + timedelta(minutes=2)
vtherm._set_now(now)
await try_function(None)
# VTherm should have taken the window action
assert vtherm.target_temperature == 7 # Frost
# No change
assert vtherm.preset_mode is PRESET_BOOST
assert vtherm.hvac_mode is HVACMode.HEAT
# 2. Change the target temperature
now = now + timedelta(minutes=1)
vtherm._set_now(now)
await vtherm.async_set_temperature(temperature=18.5)
await hass.async_block_till_done()
# VTherm should have taken the new preset temperature
assert vtherm.target_temperature == 7 # frost (window is still open)
assert vtherm.preset_mode is PRESET_NONE
assert vtherm.hvac_mode is HVACMode.HEAT
# 3.Turn off the window sensor
now = now + timedelta(minutes=1)
vtherm._set_now(now)
with patch("homeassistant.helpers.condition.state", return_value=True):
try_function = await send_window_change_event(vtherm, False, True, now)
now = now + timedelta(minutes=2)
vtherm._set_now(now)
await try_function(None)
# VTherm should have restore the new target temperature
assert vtherm.target_temperature == 18.5 # restore new target temperature
# No change
assert vtherm.preset_mode is PRESET_NONE
assert vtherm.hvac_mode is HVACMode.HEAT
# Clean the entity
vtherm.remove_thermostat()