Compare commits

..

2 Commits

Author SHA1 Message Date
Jean-Marc Collin 93823bf93c Release 2025-01-18 17:03:50 +00:00
Jean-Marc Collin a2f3b24928 [Feature Request] - Resend state to underlying device is device switch from Unknown state.
Fixes #829
2025-01-18 16:48:04 +00:00
30 changed files with 221 additions and 497 deletions
+1 -7
View File
@@ -8,13 +8,12 @@ recorder:
domains: domains:
- input_boolean - input_boolean
- input_number - input_number
- input_select
- switch - switch
- climate - climate
- sensor - sensor
- binary_sensor - binary_sensor
- number - number
- select - input_select
- versatile_thermostat - versatile_thermostat
logger: logger:
@@ -244,11 +243,6 @@ climate:
heater: input_boolean.fake_valve_sonoff_trvzb2 heater: input_boolean.fake_valve_sonoff_trvzb2
target_sensor: input_number.fake_temperature_sensor1 target_sensor: input_number.fake_temperature_sensor1
ac_mode: false ac_mode: false
- platform: generic_thermostat
name: Underlying switch climate
heater: input_boolean.fake_heater_switch2
target_sensor: input_number.fake_temperature_sensor1
ac_mode: false
input_datetime: input_datetime:
fake_last_seen: fake_last_seen:
-1
View File
@@ -1 +0,0 @@
blank_issues_enabled: false
+7 -3
View File
@@ -13,11 +13,15 @@ Ce composant personnalisé pour Home Assistant est une mise à niveau et une ré
# Quoi de neuf ? # Quoi de neuf ?
![Nouveau](images/new-icon.png) ![Nouveau](images/new-icon.png)
> * **Release 7.2**: > * **Release 6.8**:
> >
> - Prise en compte native des équipements pilotable via une entité de type `select` (ou `input_select`) ou `climate` pour des _VTherm_ de type `over_switch`. Cette évolution rend obsolète, la création de switch virtuels pour l'intégration des Nodon ou Heaty ou eCosy ... etc. Plus d'informations [ici](documentation/fr/over-switch.md#la-personnalisation-des-commandes). > Ajout d'une nouvelle méthode de régulation pour les Versatile Thermostat de type `over_climate`. Cette méthode nommée 'Contrôle direct de la vanne' permet de contrôler directement la vanne d'un TRV et éventuellement un décalage pour calibrer le thermomètre interne de votre TRV. Cette nouvelle méthode a été testée avec des Sonoff TRVZB et généralisée pour d'autre type de TRV pour lesquels la vanne est directement commandable via des entités de type `number`.
> >
> - Lien vers la documentation : cette version 7.2 expérimente des liens vers la documentation depuis les pages de configuration. Le lien est accessible via l'icone [![?](https://img.icons8.com/color/18/help.png)](https://github.com/jmcollin78/versatile_thermostat/blob/main/documentation/fr/over-switch.md#configuration). Elle est expérimentée sur la page de configuration des sous-jacents des _VTherm_ `over_switch`. > Plus d'informations [ici](documentation/fr/over-climate.md) et [ici](documentation/fr/self-regulation.md).
>
> * **Refonte de la documentation**:
>
> Avec toutes les évolutions réalisées depuis le début de l'intégration, la documentation nécessitait une profonde re-organisation, c'est chose faite sur cette version. Tous vos retours sur cette nouvelle organisation seront les bienvenus.
# 🍻 Merci pour les bières [buymecoffee](https://www.buymeacoffee.com/jmcollin78) 🍻 # 🍻 Merci pour les bières [buymecoffee](https://www.buymeacoffee.com/jmcollin78) 🍻
@@ -1050,7 +1050,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
self._hvac_mode in [HVACMode.COOL, HVACMode.HEAT, HVACMode.HEAT_COOL] self._hvac_mode in [HVACMode.COOL, HVACMode.HEAT, HVACMode.HEAT_COOL]
and self.preset_mode != PRESET_NONE and self.preset_mode != PRESET_NONE
): ):
if self.preset_mode != PRESET_FROST_PROTECTION or self._hvac_mode in [HVACMode.HEAT, HVACMode.HEAT_COOL]: if self.preset_mode != PRESET_FROST_PROTECTION:
await self.async_set_preset_mode_internal(self.preset_mode, True) await self.async_set_preset_mode_internal(self.preset_mode, True)
else: else:
await self.async_set_preset_mode_internal(PRESET_ECO, True, False) await self.async_set_preset_mode_internal(PRESET_ECO, True, False)
@@ -1244,12 +1244,12 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
async def async_set_humidity(self, humidity: int): async def async_set_humidity(self, humidity: int):
"""Set new target humidity.""" """Set new target humidity."""
_LOGGER.info("%s - Set humidity: %s", self, humidity) _LOGGER.info("%s - Set fan mode: %s", self, humidity)
return return
async def async_set_swing_mode(self, swing_mode: str): async def async_set_swing_mode(self, swing_mode: str):
"""Set new target swing operation.""" """Set new target swing operation."""
_LOGGER.info("%s - Set swing mode: %s", self, swing_mode) _LOGGER.info("%s - Set fan mode: %s", self, swing_mode)
return return
async def async_set_temperature(self, **kwargs): async def async_set_temperature(self, **kwargs):
@@ -1528,8 +1528,8 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
is_window_detected = self._window_manager.is_window_detected is_window_detected = self._window_manager.is_window_detected
if new_central_mode == CENTRAL_MODE_AUTO: if new_central_mode == CENTRAL_MODE_AUTO:
if not is_window_detected and not first_init: if not is_window_detected and not first_init:
await self.restore_preset_mode(force=False) await self.restore_hvac_mode()
await self.restore_hvac_mode(need_control_heating=True) await self.restore_preset_mode()
elif is_window_detected and self.hvac_mode == HVACMode.OFF: elif is_window_detected and self.hvac_mode == HVACMode.OFF:
# do not restore but mark the reason of off with window detection # do not restore but mark the reason of off with window detection
self.set_hvac_off_reason(HVAC_OFF_REASON_WINDOW_DETECTION) self.set_hvac_off_reason(HVAC_OFF_REASON_WINDOW_DETECTION)
@@ -47,7 +47,6 @@ class CentralFeaturePowerManager(BaseFeatureManager):
self._current_max_power: float = None self._current_max_power: float = None
self._power_temp: float = None self._power_temp: float = None
self._cancel_calculate_shedding_call = None self._cancel_calculate_shedding_call = None
self._started_vtherm_total_power: float = None
# Not used now # Not used now
self._last_shedding_date = None self._last_shedding_date = None
@@ -72,7 +71,6 @@ class CentralFeaturePowerManager(BaseFeatureManager):
and self._power_temp and self._power_temp
): ):
self._is_configured = True self._is_configured = True
self._started_vtherm_total_power = 0
else: else:
_LOGGER.info("Power management is not fully configured and will be deactivated") _LOGGER.info("Power management is not fully configured and will be deactivated")
@@ -104,8 +102,6 @@ class CentralFeaturePowerManager(BaseFeatureManager):
"""Handle power changes.""" """Handle power changes."""
_LOGGER.debug("Receive new Power event") _LOGGER.debug("Receive new Power event")
_LOGGER.debug(event) _LOGGER.debug(event)
self._started_vtherm_total_power = 0
await self.refresh_state() await self.refresh_state()
@callback @callback
@@ -279,12 +275,6 @@ class CentralFeaturePowerManager(BaseFeatureManager):
vtherms.sort(key=cmp_to_key(cmp_temps)) vtherms.sort(key=cmp_to_key(cmp_temps))
return vtherms return vtherms
def add_started_vtherm_total_power(self, started_power: float):
"""Add the power into the _started_vtherm_total_power which holds all VTherm started after
the last power measurement"""
self._started_vtherm_total_power += started_power
_LOGGER.debug("%s - started_vtherm_total_power is now %s", self, self._started_vtherm_total_power)
@property @property
def is_configured(self) -> bool: def is_configured(self) -> bool:
"""True if the FeatureManager is fully configured""" """True if the FeatureManager is fully configured"""
@@ -315,10 +305,5 @@ class CentralFeaturePowerManager(BaseFeatureManager):
"""Return the max power sensor entity id""" """Return the max power sensor entity id"""
return self._max_power_sensor_entity_id return self._max_power_sensor_entity_id
@property
def started_vtherm_total_power(self) -> float | None:
"""Return the started_vtherm_total_power"""
return self._started_vtherm_total_power
def __str__(self): def __str__(self):
return "CentralPowerManager" return "CentralPowerManager"
@@ -4,12 +4,13 @@
from __future__ import annotations from __future__ import annotations
from typing import Any from typing import Any
import re
import logging import logging
import copy import copy
from collections.abc import Mapping # pylint: disable=import-error from collections.abc import Mapping # pylint: disable=import-error
import voluptuous as vol import voluptuous as vol
from homeassistant.exceptions import HomeAssistantError
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.config_entries import ( from homeassistant.config_entries import (
ConfigEntry, ConfigEntry,
@@ -272,34 +273,6 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
CONF_MIN_OPENING_DEGREES CONF_MIN_OPENING_DEGREES
) from exc ) from exc
# Check the VSWITCH configuration. There should be the same number of vswitch_on (resp. vswitch_off) than the number of underlying entity
if self._infos.get(CONF_THERMOSTAT_TYPE) == CONF_THERMOSTAT_SWITCH and step_id == "type":
if not self.check_vswitch_configuration(data):
raise VirtualSwitchConfigurationIncorrect(CONF_VSWITCH_ON_CMD_LIST)
def check_vswitch_configuration(self, data) -> bool:
"""Check the Virtual switch configuration and return True if the configuration is correct"""
nb_under = len(data.get(CONF_UNDERLYING_LIST, []))
# check format of each command_on
for command in data.get(CONF_VSWITCH_ON_CMD_LIST, []) + data.get(CONF_VSWITCH_OFF_CMD_LIST, []):
pattern = r"^(?P<command>[a-zA-Z0-9_]+)(?:/(?P<argument>[a-zA-Z0-9_]+)(?::(?P<value>[a-zA-Z0-9_]+))?)?$"
if not re.match(pattern, command):
return False
nb_command_on = len(data.get(CONF_VSWITCH_ON_CMD_LIST, []))
nb_command_off = len(data.get(CONF_VSWITCH_OFF_CMD_LIST, []))
if (nb_command_on == nb_under or nb_command_on == 0) and (nb_command_off == nb_under or nb_command_off == 0):
# There is enough command_on and off
# Check if one under is not a switch (which have default command).
if any(
not thermostat_type.startswith(SWITCH_DOMAIN) and not thermostat_type.startswith(INPUT_BOOLEAN_DOMAIN) for thermostat_type in data.get(CONF_UNDERLYING_LIST, [])
):
if nb_command_on != nb_under or nb_command_off != nb_under:
return False
return True
return False
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 = (
@@ -434,8 +407,6 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
errors["base"] = "valve_regulation_nb_entities_incorrect" errors["base"] = "valve_regulation_nb_entities_incorrect"
except ValveRegulationMinOpeningDegreesIncorrect as err: except ValveRegulationMinOpeningDegreesIncorrect as err:
errors[str(err)] = "min_opening_degrees_format" errors[str(err)] = "min_opening_degrees_format"
except VirtualSwitchConfigurationIncorrect as err:
errors["base"] = "vswitch_configuration_incorrect"
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"
@@ -16,14 +16,6 @@ from homeassistant.components.input_number import (
DOMAIN as INPUT_NUMBER_DOMAIN, DOMAIN as INPUT_NUMBER_DOMAIN,
) )
from homeassistant.components.select import (
DOMAIN as SELECT_DOMAIN,
)
from homeassistant.components.input_select import (
DOMAIN as INPUT_SELECT_DOMAIN,
)
from homeassistant.components.input_datetime import ( from homeassistant.components.input_datetime import (
DOMAIN as INPUT_DATETIME_DOMAIN, DOMAIN as INPUT_DATETIME_DOMAIN,
) )
@@ -128,7 +120,9 @@ STEP_CENTRAL_BOILER_SCHEMA = vol.Schema(
STEP_THERMOSTAT_SWITCH = vol.Schema( # pylint: disable=invalid-name STEP_THERMOSTAT_SWITCH = vol.Schema( # pylint: disable=invalid-name
{ {
vol.Required(CONF_UNDERLYING_LIST): selector.EntitySelector( vol.Required(CONF_UNDERLYING_LIST): selector.EntitySelector(
selector.EntitySelectorConfig(domain=[SWITCH_DOMAIN, INPUT_BOOLEAN_DOMAIN, SELECT_DOMAIN, INPUT_SELECT_DOMAIN, CLIMATE_DOMAIN], multiple=True), selector.EntitySelectorConfig(
domain=[SWITCH_DOMAIN, INPUT_BOOLEAN_DOMAIN], multiple=True
),
), ),
vol.Optional(CONF_HEATER_KEEP_ALIVE): cv.positive_int, vol.Optional(CONF_HEATER_KEEP_ALIVE): cv.positive_int,
vol.Required(CONF_PROP_FUNCTION, default=PROPORTIONAL_FUNCTION_TPI): vol.In( vol.Required(CONF_PROP_FUNCTION, default=PROPORTIONAL_FUNCTION_TPI): vol.In(
@@ -138,10 +132,6 @@ STEP_THERMOSTAT_SWITCH = vol.Schema( # pylint: disable=invalid-name
), ),
vol.Optional(CONF_AC_MODE, default=False): cv.boolean, vol.Optional(CONF_AC_MODE, default=False): cv.boolean,
vol.Optional(CONF_INVERSE_SWITCH, default=False): cv.boolean, vol.Optional(CONF_INVERSE_SWITCH, default=False): cv.boolean,
vol.Optional("on_command_text"): vol.In([]),
vol.Optional(CONF_VSWITCH_ON_CMD_LIST): selector.TextSelector(selector.TextSelectorConfig(type=selector.TextSelectorType.TEXT, multiple=True)),
vol.Optional("off_command_text"): vol.In([]),
vol.Optional(CONF_VSWITCH_OFF_CMD_LIST): selector.TextSelector(selector.TextSelectorConfig(type=selector.TextSelectorType.TEXT, multiple=True)),
} }
) )
@@ -127,9 +127,6 @@ 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" CONF_MIN_OPENING_DEGREES = "min_opening_degrees"
CONF_VSWITCH_ON_CMD_LIST = "vswitch_on_command"
CONF_VSWITCH_OFF_CMD_LIST = "vswitch_off_command"
# Deprecated # Deprecated
CONF_HEATER = "heater_entity_id" CONF_HEATER = "heater_entity_id"
CONF_HEATER_2 = "heater_entity2_id" CONF_HEATER_2 = "heater_entity2_id"
@@ -565,10 +562,6 @@ class ValveRegulationMinOpeningDegreesIncorrect(HomeAssistantError):
"""Error to indicate that the minimal opening degrees is not a list of int separated by coma""" """Error to indicate that the minimal opening degrees is not a list of int separated by coma"""
class VirtualSwitchConfigurationIncorrect(HomeAssistantError):
"""Error when a virtual switch is not configured correctly"""
class overrides: # pylint: disable=invalid-name class overrides: # pylint: disable=invalid-name
"""An annotation to inform overrides""" """An annotation to inform overrides"""
@@ -104,8 +104,7 @@ class FeaturePowerManager(BaseFeatureManager):
async def check_power_available(self) -> bool: async def check_power_available(self) -> bool:
"""Check if the Vtherm can be started considering overpowering. """Check if the Vtherm can be started considering overpowering.
Returns True if no overpowering conditions are found. Returns True if no overpowering conditions are found
If True the vtherm power is written into the temporay vtherm started
""" """
vtherm_api = VersatileThermostatAPI.get_vtherm_api() vtherm_api = VersatileThermostatAPI.get_vtherm_api()
@@ -117,7 +116,6 @@ class FeaturePowerManager(BaseFeatureManager):
current_power = vtherm_api.central_power_manager.current_power current_power = vtherm_api.central_power_manager.current_power
current_max_power = vtherm_api.central_power_manager.current_max_power current_max_power = vtherm_api.central_power_manager.current_max_power
started_vtherm_total_power = vtherm_api.central_power_manager.started_vtherm_total_power
if ( if (
current_power is None current_power is None
or current_max_power is None or current_max_power is None
@@ -148,7 +146,7 @@ class FeaturePowerManager(BaseFeatureManager):
self._device_power * self._vtherm.proportional_algorithm.on_percent, self._device_power * self._vtherm.proportional_algorithm.on_percent,
) )
ret = (current_power + started_vtherm_total_power + power_consumption_max) < current_max_power ret = (current_power + power_consumption_max) < current_max_power
if not ret: if not ret:
_LOGGER.info( _LOGGER.info(
"%s - there is not enough power available power=%.3f, max_power=%.3f heater power=%.3f", "%s - there is not enough power available power=%.3f, max_power=%.3f heater power=%.3f",
@@ -157,10 +155,6 @@ class FeaturePowerManager(BaseFeatureManager):
current_max_power, current_max_power,
self._device_power, self._device_power,
) )
else:
# Adds the current_power_max to the started vtherm total power
vtherm_api.central_power_manager.add_started_vtherm_total_power(power_consumption_max)
return ret return ret
async def set_overpowering(self, overpowering: bool, power_consumption_max=0): async def set_overpowering(self, overpowering: bool, power_consumption_max=0):
@@ -14,6 +14,6 @@
"quality_scale": "silver", "quality_scale": "silver",
"requirements": [], "requirements": [],
"ssdp": [], "ssdp": [],
"version": "7.2.0", "version": "7.1.5",
"zeroconf": [] "zeroconf": []
} }
@@ -82,9 +82,7 @@
"auto_regulation_periode_min": "Regulation minimum period", "auto_regulation_periode_min": "Regulation minimum period",
"auto_regulation_use_device_temp": "Use internal temperature of the underlying", "auto_regulation_use_device_temp": "Use internal temperature of the underlying",
"inverse_switch_command": "Inverse switch command", "inverse_switch_command": "Inverse switch command",
"auto_fan_mode": "Auto fan mode", "auto_fan_mode": "Auto fan mode"
"vswitch_on_command": "Optional turn on commands",
"vswitch_off_command": "Optional turn off commands"
}, },
"data_description": { "data_description": {
"underlying_entity_ids": "The device(s) to be controlled - 1 is required", "underlying_entity_ids": "The device(s) to be controlled - 1 is required",
@@ -96,9 +94,7 @@
"auto_regulation_periode_min": "Duration in minutes between two regulation update", "auto_regulation_periode_min": "Duration in minutes between two regulation update",
"auto_regulation_use_device_temp": "Use the eventual internal temperature sensor of the underlying to speedup the self-regulation", "auto_regulation_use_device_temp": "Use the eventual internal temperature sensor of the underlying to speedup the self-regulation",
"inverse_switch_command": "For switch with pilot wire and diode you may need to inverse the command", "inverse_switch_command": "For switch with pilot wire and diode you may need to inverse the command",
"auto_fan_mode": "Automatically activate fan when huge heating/cooling is necessary", "auto_fan_mode": "Automatically activate fan when huge heating/cooling is necessary"
"vswitch_on_command": "A list of turn on command in the form: action:parameter. Example: select_option:comfort. See README for more examples",
"vswitch_off_command": "A list of turn on command in the form: action:parameter. Example: select_option:frost. See README for more examples"
} }
}, },
"tpi": { "tpi": {
@@ -334,9 +330,7 @@
"auto_regulation_periode_min": "Regulation minimum period", "auto_regulation_periode_min": "Regulation minimum period",
"auto_regulation_use_device_temp": "Use internal temperature of the underlying", "auto_regulation_use_device_temp": "Use internal temperature of the underlying",
"inverse_switch_command": "Inverse switch command", "inverse_switch_command": "Inverse switch command",
"auto_fan_mode": "Auto fan mode", "auto_fan_mode": "Auto fan mode"
"vswitch_on_command": "Optional turn on commands",
"vswitch_off_command": "Optional turn off commands"
}, },
"data_description": { "data_description": {
"underlying_entity_ids": "The device(s) to be controlled - 1 is required", "underlying_entity_ids": "The device(s) to be controlled - 1 is required",
@@ -348,9 +342,7 @@
"auto_regulation_periode_min": "Duration in minutes between two regulation update", "auto_regulation_periode_min": "Duration in minutes between two regulation update",
"auto_regulation_use_device_temp": "Use the eventual internal temperature sensor of the underlying to speedup the self-regulation", "auto_regulation_use_device_temp": "Use the eventual internal temperature sensor of the underlying to speedup the self-regulation",
"inverse_switch_command": "For switch with pilot wire and diode you may need to invert the command", "inverse_switch_command": "For switch with pilot wire and diode you may need to invert the command",
"auto_fan_mode": "Automatically activate fan when huge heating/cooling is necessary", "auto_fan_mode": "Automatically activate fan when huge heating/cooling is necessary"
"vswitch_on_command": "A list of turn on command in the form: action:parameter. Example: select_option:comfort. See README for more examples",
"vswitch_off_command": "A list of turn on command in the form: action:parameter. Example: select_option:frost. See README for more examples"
} }
}, },
"tpi": { "tpi": {
@@ -1104,7 +1104,7 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
@overrides @overrides
async def async_set_humidity(self, humidity: int): async def async_set_humidity(self, humidity: int):
"""Set new target humidity.""" """Set new target humidity."""
_LOGGER.info("%s - Set humidity: %s", self, humidity) _LOGGER.info("%s - Set fan mode: %s", self, humidity)
if humidity is None: if humidity is None:
return return
for under in self._underlyings: for under in self._underlyings:
@@ -1115,7 +1115,7 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
@overrides @overrides
async def async_set_swing_mode(self, swing_mode): async def async_set_swing_mode(self, swing_mode):
"""Set new target swing operation.""" """Set new target swing operation."""
_LOGGER.info("%s - Set swing mode: %s", self, swing_mode) _LOGGER.info("%s - Set fan mode: %s", self, swing_mode)
if swing_mode is None: if swing_mode is None:
return return
for under in self._underlyings: for under in self._underlyings:
@@ -14,8 +14,6 @@ from .const import (
CONF_UNDERLYING_LIST, CONF_UNDERLYING_LIST,
CONF_HEATER_KEEP_ALIVE, CONF_HEATER_KEEP_ALIVE,
CONF_INVERSE_SWITCH, CONF_INVERSE_SWITCH,
CONF_VSWITCH_ON_CMD_LIST,
CONF_VSWITCH_OFF_CMD_LIST,
overrides, overrides,
) )
@@ -42,8 +40,6 @@ class ThermostatOverSwitch(BaseThermostat[UnderlyingSwitch]):
"tpi_coef_ext", "tpi_coef_ext",
"power_percent", "power_percent",
"calculated_on_percent", "calculated_on_percent",
"vswitch_on_commands",
"vswitch_off_commands",
} }
) )
) )
@@ -51,8 +47,6 @@ class ThermostatOverSwitch(BaseThermostat[UnderlyingSwitch]):
def __init__(self, hass: HomeAssistant, unique_id, name, config_entry) -> None: def __init__(self, hass: HomeAssistant, unique_id, name, config_entry) -> None:
"""Initialize the thermostat over switch.""" """Initialize the thermostat over switch."""
self._is_inversed: bool | None = None self._is_inversed: bool | None = None
self._lst_vswitch_on: list[str] = []
self._lst_vswitch_off: list[str] = []
super().__init__(hass, unique_id, name, config_entry) super().__init__(hass, unique_id, name, config_entry)
@property @property
@@ -82,13 +76,9 @@ class ThermostatOverSwitch(BaseThermostat[UnderlyingSwitch]):
) )
lst_switches = config_entry.get(CONF_UNDERLYING_LIST) lst_switches = config_entry.get(CONF_UNDERLYING_LIST)
self._lst_vswitch_on = config_entry.get(CONF_VSWITCH_ON_CMD_LIST, [])
self._lst_vswitch_off = config_entry.get(CONF_VSWITCH_OFF_CMD_LIST, [])
delta_cycle = self._cycle_min * 60 / len(lst_switches) delta_cycle = self._cycle_min * 60 / len(lst_switches)
for idx, switch in enumerate(lst_switches): for idx, switch in enumerate(lst_switches):
vswitch_on = self._lst_vswitch_on[idx] if idx < len(self._lst_vswitch_on) else None
vswitch_off = self._lst_vswitch_off[idx] if idx < len(self._lst_vswitch_off) else None
self._underlyings.append( self._underlyings.append(
UnderlyingSwitch( UnderlyingSwitch(
hass=self._hass, hass=self._hass,
@@ -96,8 +86,6 @@ class ThermostatOverSwitch(BaseThermostat[UnderlyingSwitch]):
switch_entity_id=switch, switch_entity_id=switch,
initial_delay_sec=idx * delta_cycle, initial_delay_sec=idx * delta_cycle,
keep_alive_sec=config_entry.get(CONF_HEATER_KEEP_ALIVE, 0), keep_alive_sec=config_entry.get(CONF_HEATER_KEEP_ALIVE, 0),
vswitch_on=vswitch_on,
vswitch_off=vswitch_off,
) )
) )
@@ -154,9 +142,6 @@ class ThermostatOverSwitch(BaseThermostat[UnderlyingSwitch]):
"calculated_on_percent" "calculated_on_percent"
] = self._prop_algorithm.calculated_on_percent ] = self._prop_algorithm.calculated_on_percent
self._attr_extra_state_attributes["vswitch_on_commands"] = self._lst_vswitch_on
self._attr_extra_state_attributes["vswitch_off_commands"] = self._lst_vswitch_off
self.async_write_ha_state() self.async_write_ha_state()
_LOGGER.debug( _LOGGER.debug(
"%s - Calling update_custom_attributes: %s", "%s - Calling update_custom_attributes: %s",
@@ -82,9 +82,7 @@
"auto_regulation_periode_min": "Regulation minimum period", "auto_regulation_periode_min": "Regulation minimum period",
"auto_regulation_use_device_temp": "Use internal temperature of the underlying", "auto_regulation_use_device_temp": "Use internal temperature of the underlying",
"inverse_switch_command": "Inverse switch command", "inverse_switch_command": "Inverse switch command",
"auto_fan_mode": "Auto fan mode", "auto_fan_mode": "Auto fan mode"
"vswitch_on_command": "Optional turn on commands",
"vswitch_off_command": "Optional turn off commands"
}, },
"data_description": { "data_description": {
"underlying_entity_ids": "The device(s) to be controlled - 1 is required", "underlying_entity_ids": "The device(s) to be controlled - 1 is required",
@@ -96,9 +94,7 @@
"auto_regulation_periode_min": "Duration in minutes between two regulation update", "auto_regulation_periode_min": "Duration in minutes between two regulation update",
"auto_regulation_use_device_temp": "Use the eventual internal temperature sensor of the underlying to speedup the self-regulation", "auto_regulation_use_device_temp": "Use the eventual internal temperature sensor of the underlying to speedup the self-regulation",
"inverse_switch_command": "For switch with pilot wire and diode you may need to inverse the command", "inverse_switch_command": "For switch with pilot wire and diode you may need to inverse the command",
"auto_fan_mode": "Automatically activate fan when huge heating/cooling is necessary", "auto_fan_mode": "Automatically activate fan when huge heating/cooling is necessary"
"vswitch_on_command": "A list of turn on command in the form: action:parameter. Example: select_option:comfort. See README for more examples",
"vswitch_off_command": "A list of turn on command in the form: action:parameter. Example: select_option:frost. See README for more examples"
} }
}, },
"tpi": { "tpi": {
@@ -238,7 +234,7 @@
"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 has 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 has 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": "Opening degree minimum value for each underlying device, comma separated. Default to 0. Example: 20, 25, 30" "min_opening_degrees": "A comma seperated list of minimal opening degrees. Default to 0. Example: 20, 25, 30"
} }
} }
}, },
@@ -334,9 +330,7 @@
"auto_regulation_periode_min": "Regulation minimum period", "auto_regulation_periode_min": "Regulation minimum period",
"auto_regulation_use_device_temp": "Use internal temperature of the underlying", "auto_regulation_use_device_temp": "Use internal temperature of the underlying",
"inverse_switch_command": "Inverse switch command", "inverse_switch_command": "Inverse switch command",
"auto_fan_mode": "Auto fan mode", "auto_fan_mode": "Auto fan mode"
"vswitch_on_command": "Optional turn on commands",
"vswitch_off_command": "Optional turn off commands"
}, },
"data_description": { "data_description": {
"underlying_entity_ids": "The device(s) to be controlled - 1 is required", "underlying_entity_ids": "The device(s) to be controlled - 1 is required",
@@ -348,9 +342,7 @@
"auto_regulation_periode_min": "Duration in minutes between two regulation update", "auto_regulation_periode_min": "Duration in minutes between two regulation update",
"auto_regulation_use_device_temp": "Use the eventual internal temperature sensor of the underlying to speedup the self-regulation", "auto_regulation_use_device_temp": "Use the eventual internal temperature sensor of the underlying to speedup the self-regulation",
"inverse_switch_command": "For switch with pilot wire and diode you may need to invert the command", "inverse_switch_command": "For switch with pilot wire and diode you may need to invert the command",
"auto_fan_mode": "Automatically activate fan when huge heating/cooling is necessary", "auto_fan_mode": "Automatically activate fan when huge heating/cooling is necessary"
"vswitch_on_command": "A list of turn on command in the form: action:parameter. Example: select_option:comfort. See README for more examples",
"vswitch_off_command": "A list of turn on command in the form: action:parameter. Example: select_option:frost. See README for more examples"
} }
}, },
"tpi": { "tpi": {
@@ -489,7 +481,7 @@
"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 has 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 has 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": "Opening degree minimum value for each underlying device, comma separated. Default to 0. Example: 20, 25, 30" "min_opening_degrees": "A comma seperated list of minimal opening degrees. Default to 0. Example: 20, 25, 30"
} }
} }
}, },
@@ -71,7 +71,7 @@
}, },
"type": { "type": {
"title": "Entité(s) liée(s)", "title": "Entité(s) liée(s)",
"description": "Attributs de(s) l'entité(s) liée(s)&nbsp;[![?](https://img.icons8.com/color/18/help.png)](https://github.com/jmcollin78/versatile_thermostat/blob/main/documentation/fr/over-switch.md#configuration)", "description": "Attributs de(s) l'entité(s) liée(s)",
"data": { "data": {
"underlying_entity_ids": "Les équipements à controller", "underlying_entity_ids": "Les équipements à controller",
"heater_keep_alive": "keep-alive (sec)", "heater_keep_alive": "keep-alive (sec)",
@@ -82,11 +82,7 @@
"auto_regulation_periode_min": "Période minimale de régulation", "auto_regulation_periode_min": "Période minimale de régulation",
"auto_regulation_use_device_temp": "Compenser la température interne du sous-jacent", "auto_regulation_use_device_temp": "Compenser la température interne du sous-jacent",
"inverse_switch_command": "Inverser la commande", "inverse_switch_command": "Inverser la commande",
"auto_fan_mode": " Auto ventilation mode", "auto_fan_mode": " Auto ventilation mode"
"on_command_text": "Personnalisation des commandes d'allumage",
"vswitch_on_command": "Commande d'allumage (optionnel)",
"off_command_text": "Personnalisation des commandes d'extinction",
"vswitch_off_command": "Commande d'extinction (optionnel)"
}, },
"data_description": { "data_description": {
"underlying_entity_ids": "La liste des équipements qui seront controlés par ce VTherm", "underlying_entity_ids": "La liste des équipements qui seront controlés par ce VTherm",
@@ -98,8 +94,7 @@
"auto_regulation_periode_min": "La durée en minutes entre deux mise à jour faites par la régulation", "auto_regulation_periode_min": "La durée en minutes entre deux mise à jour faites par la régulation",
"auto_regulation_use_device_temp": "Compenser la temperature interne du sous-jacent pour accélérer l'auto-régulation", "auto_regulation_use_device_temp": "Compenser la temperature interne du sous-jacent pour accélérer l'auto-régulation",
"inverse_switch_command": "Inverse la commande du switch pour une installation avec fil pilote et diode", "inverse_switch_command": "Inverse la commande du switch pour une installation avec fil pilote et diode",
"auto_fan_mode": "Active la ventilation automatiquement en cas d'écart important", "auto_fan_mode": "Active la ventilation automatiquement en cas d'écart important"
"on_command_text": "Pour les sous-jacents de type `select` ou `climate` vous devez personnaliser les commandes."
} }
}, },
"tpi": { "tpi": {
@@ -160,7 +155,7 @@
}, },
"data_description": { "data_description": {
"motion_sensor_entity_id": "Id d'entité du détecteur de mouvement", "motion_sensor_entity_id": "Id d'entité du détecteur de mouvement",
"motion_delay": "Délai avant activation lorsqu'un mouvement est détecté (secondes)", "motion_delay": "Délai avant activation lorsqu'un mouvement est détecté (secondss)",
"motion_off_delai": "Délai avant désactivation lorsqu'aucun mouvement n'est détecté (secondes)", "motion_off_delai": "Délai avant désactivation lorsqu'aucun mouvement n'est détecté (secondes)",
"motion_preset": "Preset à utiliser si mouvement détecté", "motion_preset": "Preset à utiliser si mouvement détecté",
"no_motion_preset": "Preset à utiliser si pas de mouvement détecté", "no_motion_preset": "Preset à utiliser si pas de mouvement détecté",
@@ -205,7 +200,7 @@
"use_advanced_central_config": "Utiliser la configuration centrale avancée" "use_advanced_central_config": "Utiliser la configuration centrale avancée"
}, },
"data_description": { "data_description": {
"minimal_activation_delay": "Délai en secondes en-dessous duquel l'équipement ne sera pas activé", "minimal_activation_delay": "Délai en seondes en-dessous duquel l'équipement ne sera pas activé",
"safety_delay_min": "Délai maximal autorisé en minutes entre 2 mesures de températures. Au-dessus de ce délai, le thermostat se mettra en position de sécurité", "safety_delay_min": "Délai maximal autorisé en minutes entre 2 mesures de températures. Au-dessus de ce délai, le thermostat se mettra en position de sécurité",
"safety_min_on_percent": "Seuil minimal de pourcentage de chauffage en-dessous duquel le préréglage sécurité ne sera jamais activé", "safety_min_on_percent": "Seuil minimal de pourcentage de chauffage en-dessous duquel le préréglage sécurité ne sera jamais activé",
"safety_default_on_percent": "Valeur par défaut pour le pourcentage de chauffage en mode sécurité. Mettre 0 pour éteindre le radiateur en mode sécurité", "safety_default_on_percent": "Valeur par défaut pour le pourcentage de chauffage en mode sécurité. Mettre 0 pour éteindre le radiateur en mode sécurité",
@@ -324,7 +319,7 @@
}, },
"type": { "type": {
"title": "Entité(s) liée(s) - {name}", "title": "Entité(s) liée(s) - {name}",
"description": "Attributs de(s) l'entité(s) liée(s)&nbsp;[![?](https://img.icons8.com/color/18/help.png)](https://github.com/jmcollin78/versatile_thermostat/blob/main/documentation/fr/over-switch.md#configuration)", "description": "Attributs de(s) l'entité(s) liée(s)",
"data": { "data": {
"underlying_entity_ids": "Les équipements à controller", "underlying_entity_ids": "Les équipements à controller",
"heater_keep_alive": "keep-alive (sec)", "heater_keep_alive": "keep-alive (sec)",
@@ -335,11 +330,7 @@
"auto_regulation_periode_min": "Période minimale de régulation", "auto_regulation_periode_min": "Période minimale de régulation",
"auto_regulation_use_device_temp": "Compenser la température interne du sous-jacent", "auto_regulation_use_device_temp": "Compenser la température interne du sous-jacent",
"inverse_switch_command": "Inverser la commande", "inverse_switch_command": "Inverser la commande",
"auto_fan_mode": " Auto ventilation mode", "auto_fan_mode": " Auto ventilation mode"
"on_command_text": "Personnalisation des commandes d'allumage",
"vswitch_on_command": "Commande d'allumage (optionnel)",
"off_command_text": "Personnalisation des commandes d'extinction",
"vswitch_off_command": "Commande d'extinction (optionnel)"
}, },
"data_description": { "data_description": {
"underlying_entity_ids": "La liste des équipements qui seront controlés par ce VTherm", "underlying_entity_ids": "La liste des équipements qui seront controlés par ce VTherm",
@@ -351,8 +342,7 @@
"auto_regulation_periode_min": "La durée en minutes entre deux mise à jour faites par la régulation", "auto_regulation_periode_min": "La durée en minutes entre deux mise à jour faites par la régulation",
"auto_regulation_use_device_temp": "Compenser la temperature interne du sous-jacent pour accélérer l'auto-régulation", "auto_regulation_use_device_temp": "Compenser la temperature interne du sous-jacent pour accélérer l'auto-régulation",
"inverse_switch_command": "Inverse la commande du switch pour une installation avec fil pilote et diode", "inverse_switch_command": "Inverse la commande du switch pour une installation avec fil pilote et diode",
"auto_fan_mode": "Active la ventilation automatiquement en cas d'écart important", "auto_fan_mode": "Active la ventilation automatiquement en cas d'écart important"
"on_command_text": "Pour les sous-jacents de type `select` ou `climate`"
} }
}, },
"tpi": { "tpi": {
@@ -497,8 +487,7 @@
"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", "min_opening_degrees_format": "Une liste d'entiers positifs séparés par des ',' est attendu. Exemple : 20, 25, 30"
"vswitch_configuration_incorrect": "La configuration de la personnalisation des commandes est incorrecte. Elle est obligatoire pour les sous-jacents non switch et le format doit être 'service_name[/attribut:valeur]'. Plus d'informations dans le README."
}, },
"abort": { "abort": {
"already_configured": "Le device est déjà configuré" "already_configured": "Le device est déjà configuré"
@@ -2,12 +2,10 @@
""" Underlying entities classes """ """ Underlying entities classes """
import logging import logging
import re from typing import Any
from typing import Any, Dict, Tuple
from enum import StrEnum from enum import StrEnum
from homeassistant.const import ATTR_ENTITY_ID, STATE_ON, STATE_OFF, STATE_UNAVAILABLE from homeassistant.const import ATTR_ENTITY_ID, STATE_ON, STATE_UNAVAILABLE
from homeassistant.core import State from homeassistant.core import State
from homeassistant.exceptions import ServiceNotFound from homeassistant.exceptions import ServiceNotFound
@@ -211,8 +209,17 @@ class UnderlyingEntity:
class UnderlyingSwitch(UnderlyingEntity): class UnderlyingSwitch(UnderlyingEntity):
"""Represent a underlying switch""" """Represent a underlying switch"""
_initialDelaySec: int
_on_time_sec: int
_off_time_sec: int
def __init__( def __init__(
self, hass: HomeAssistant, thermostat: Any, switch_entity_id: str, initial_delay_sec: int, keep_alive_sec: float, vswitch_on: str = None, vswitch_off: str = None self,
hass: HomeAssistant,
thermostat: Any,
switch_entity_id: str,
initial_delay_sec: int,
keep_alive_sec: float,
) -> None: ) -> None:
"""Initialize the underlying switch""" """Initialize the underlying switch"""
@@ -228,14 +235,6 @@ class UnderlyingSwitch(UnderlyingEntity):
self._on_time_sec = 0 self._on_time_sec = 0
self._off_time_sec = 0 self._off_time_sec = 0
self._keep_alive = IntervalCaller(hass, keep_alive_sec) self._keep_alive = IntervalCaller(hass, keep_alive_sec)
self._vswitch_on = vswitch_on
self._vswitch_off = vswitch_off
self._domain = self._entity_id.split(".")[0]
# build command
command, data, state_on = self.build_command(use_on=True)
self._on_command = {"command": command, "data": data, "state": state_on}
command, data, state_off = self.build_command(use_on=False)
self._off_command = {"command": command, "data": data, "state": state_off}
@property @property
def initial_delay_sec(self): def initial_delay_sec(self):
@@ -276,11 +275,10 @@ class UnderlyingSwitch(UnderlyingEntity):
@property @property
def is_device_active(self): def is_device_active(self):
"""If the toggleable device is currently active.""" """If the toggleable device is currently active."""
# real_state = self._hass.states.is_state(self._entity_id, STATE_ON) real_state = self._hass.states.is_state(self._entity_id, STATE_ON)
# return (self.is_inversed and not real_state) or ( return (self.is_inversed and not real_state) or (
# not self.is_inversed and real_state not self.is_inversed and real_state
# ) )
return self._hass.states.is_state(self._entity_id, self._on_command.get("state"))
async def _keep_alive_callback(self): async def _keep_alive_callback(self):
"""Keep alive: Turn on if already turned on, turn off if already turned off.""" """Keep alive: Turn on if already turned on, turn off if already turned off."""
@@ -307,44 +305,18 @@ class UnderlyingSwitch(UnderlyingEntity):
) )
await (self.turn_on() if self.is_device_active else self.turn_off()) await (self.turn_on() if self.is_device_active else self.turn_off())
def build_command(self, use_on: bool) -> Tuple[str, Dict[str, str]]: # @overrides this breaks some unit tests TypeError: object MagicMock can't be used in 'await' expression
"""Build a command and returns a command and a dict as data"""
value = None
data = {ATTR_ENTITY_ID: self._entity_id}
vswitch = self._vswitch_on if use_on and not self.is_inversed else self._vswitch_off
if vswitch:
pattern = r"^(?P<command>[^/]+)(?:/(?P<argument>[^:]+)(?::(?P<value>.*))?)?$"
match = re.match(pattern, vswitch)
if match:
# Extraire les groupes nommés
command = match.group("command")
argument = match.group("argument")
value = match.group("value")
data.update({argument: value})
else:
raise ValueError(f"Invalid input format: {vswitch}")
else:
command = SERVICE_TURN_ON if use_on and not self.is_inversed else SERVICE_TURN_OFF
value = STATE_ON if use_on and not self.is_inversed else STATE_OFF
return command, data, value
async def turn_off(self): async def turn_off(self):
"""Turn heater toggleable device off.""" """Turn heater toggleable device off."""
self._keep_alive.cancel() # Cancel early to avoid a turn_on/turn_off race condition self._keep_alive.cancel() # Cancel early to avoid a turn_on/turn_off race condition
_LOGGER.debug("%s - Stopping underlying entity %s", self, self._entity_id) _LOGGER.debug("%s - Stopping underlying entity %s", self, self._entity_id)
command = SERVICE_TURN_OFF if not self.is_inversed else SERVICE_TURN_ON
command = self._off_command.get("command") domain = self._entity_id.split(".")[0]
data = self._off_command.get("data")
# This may fails if called after shutdown # This may fails if called after shutdown
try: try:
try: try:
_LOGGER.debug("%s - Sending command %s with data=%s", self, command, data) data = {ATTR_ENTITY_ID: self._entity_id}
await self._hass.services.async_call(self._domain, command, data) await self._hass.services.async_call(domain, command, data)
self._keep_alive.set_async_action(self._keep_alive_callback) self._keep_alive.set_async_action(self._keep_alive_callback)
except Exception: except Exception:
self._keep_alive.cancel() self._keep_alive.cancel()
@@ -360,12 +332,12 @@ class UnderlyingSwitch(UnderlyingEntity):
if not await self.check_overpowering(): if not await self.check_overpowering():
return False return False
command = self._on_command.get("command") command = SERVICE_TURN_ON if not self.is_inversed else SERVICE_TURN_OFF
data = self._on_command.get("data") domain = self._entity_id.split(".")[0]
try: try:
try: try:
_LOGGER.debug("%s - Sending command %s with data=%s", self, command, data) data = {ATTR_ENTITY_ID: self._entity_id}
await self._hass.services.async_call(self._domain, command, data) await self._hass.services.async_call(domain, command, data)
self._keep_alive.set_async_action(self._keep_alive_callback) self._keep_alive.set_async_action(self._keep_alive_callback)
return True return True
except Exception: except Exception:
@@ -639,7 +611,7 @@ class UnderlyingClimate(UnderlyingEntity):
async def set_humidity(self, humidity: int): async def set_humidity(self, humidity: int):
"""Set new target humidity.""" """Set new target humidity."""
_LOGGER.info("%s - Set humidity: %s", self, humidity) _LOGGER.info("%s - Set fan mode: %s", self, humidity)
if not self.is_initialized: if not self.is_initialized:
return return
data = { data = {
@@ -655,7 +627,7 @@ class UnderlyingClimate(UnderlyingEntity):
async def set_swing_mode(self, swing_mode): async def set_swing_mode(self, swing_mode):
"""Set new target swing operation.""" """Set new target swing operation."""
_LOGGER.info("%s - Set swing mode: %s", self, swing_mode) _LOGGER.info("%s - Set fan mode: %s", self, swing_mode)
if not self.is_initialized: if not self.is_initialized:
return return
data = { data = {
+1 -3
View File
@@ -60,8 +60,6 @@ Of course, your underlying equipment must have ventilation, and it must be contr
### Compensating for the Internal Temperature of the Underlying Equipment ### Compensating for the Internal Temperature of the Underlying Equipment
Warning: This option must not be used with direct valve control regulation if a calibration entity has been provided.
Sometimes, the internal thermometer of the underlying equipment (TRV, air conditioner, etc.) is inaccurate to the point that self-regulation is insufficient. This happens when the internal thermometer is placed too close to the heat source. The internal temperature rises much faster than the room temperature, leading to regulation failures. Sometimes, the internal thermometer of the underlying equipment (TRV, air conditioner, etc.) is inaccurate to the point that self-regulation is insufficient. This happens when the internal thermometer is placed too close to the heat source. The internal temperature rises much faster than the room temperature, leading to regulation failures.
Example: Example:
1. Room temperature is 18°, setpoint is 20°. 1. Room temperature is 18°, setpoint is 20°.
@@ -101,4 +99,4 @@ When this entity is 'On', all temperature or state changes made directly on the
Be careful, if you use this feature, your equipment is now controlled in two ways: _VTherm_ and directly by you. The commands might be contradictory, which could lead to confusion about the equipment's state. _VTherm_ is equipped with a delay mechanism that prevents loops: the user gives a setpoint, which is captured by _VTherm_ and changes the setpoint, ... This delay may cause the change made directly on the equipment to be ignored if these changes are too close together in time. Be careful, if you use this feature, your equipment is now controlled in two ways: _VTherm_ and directly by you. The commands might be contradictory, which could lead to confusion about the equipment's state. _VTherm_ is equipped with a delay mechanism that prevents loops: the user gives a setpoint, which is captured by _VTherm_ and changes the setpoint, ... This delay may cause the change made directly on the equipment to be ignored if these changes are too close together in time.
Some equipment (like Daikin, for example) changes state by itself. If the checkbox is checked, it may turn off the _VTherm_ when that's not what you intended. Some equipment (like Daikin, for example) changes state by itself. If the checkbox is checked, it may turn off the _VTherm_ when that's not what you intended.
That's why it's better not to use it. It generates a lot of confusion and many support requests. That's why it's better not to use it. It generates a lot of confusion and many support requests.
+3 -3
View File
@@ -31,8 +31,8 @@ This allows you to configure the valve control entities:
You need to provide: 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. For Sonoff TRVZB, you should not configure this entity. See the note below. 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, else it will be set to 0 (to ensure the valve is closed). This allows enough water to pass through when it needs to be opened. 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`.
@@ -152,4 +152,4 @@ To apply the changes, you must either **restart Home Assistant completely** or j
## Summary of the Auto-Regulation Algorithm ## Summary of the Auto-Regulation Algorithm
A summary of the auto-regulation algorithm is described [here](algorithms.md#the-auto-regulation-algorithm-without-valve-control) A summary of the auto-regulation algorithm is described [here](algorithms.md#the-auto-regulation-algorithm-without-valve-control)
Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

+1 -3
View File
@@ -63,8 +63,6 @@ Une fois l'écart de température redevenu faible, la ventilation se mettra dans
### Compenser la température interne du sous-jacent ### Compenser la température interne du sous-jacent
Attention : cette option ne doit pas être utilisée avec une régulation par contrôle direct de la vanne si une entité de calibrage a été fournie.
Quelque fois, il arrive que le thermomètre interne du sous-jacent (TRV, climatisation, ...) soit tellement faux que l' auto-régulation ne suffise pas à réguler. Quelque fois, il arrive que le thermomètre interne du sous-jacent (TRV, climatisation, ...) soit tellement faux que l' auto-régulation ne suffise pas à réguler.
Cela arrive lorsque le thermomètre interne est trop près de la source de chaleur. La température interne monte alors beaucoup plus vite que la température de la pièce, ce qui génère des défauts dans la régulation. Cela arrive lorsque le thermomètre interne est trop près de la source de chaleur. La température interne monte alors beaucoup plus vite que la température de la pièce, ce qui génère des défauts dans la régulation.
Exemple : Exemple :
@@ -105,4 +103,4 @@ Lorsque cette entité est 'On', tous les changements de température ou d'état
Attention, si vous utilisez cette fonction, votre équipement est maintenant contrôlé par 2 moyens : _VTherm_ et par vous même directement. Les ordres peuvent être contradictoires et cela peut induire une incompréhension sur l'état de l'équipement. _VTherm_ est équipé d'un mécanisme de temporisation qui évite les boucles : l'utilisateur donne une consigne qui est captée par _VTherm_ qui change la consigne, ... Cette temporisation peut faire que le changement fait directement sur l'équipement est ignoré si ces changements sont trop rapprochés dans le temps. Attention, si vous utilisez cette fonction, votre équipement est maintenant contrôlé par 2 moyens : _VTherm_ et par vous même directement. Les ordres peuvent être contradictoires et cela peut induire une incompréhension sur l'état de l'équipement. _VTherm_ est équipé d'un mécanisme de temporisation qui évite les boucles : l'utilisateur donne une consigne qui est captée par _VTherm_ qui change la consigne, ... Cette temporisation peut faire que le changement fait directement sur l'équipement est ignoré si ces changements sont trop rapprochés dans le temps.
Certains équipements (Daikin par exemple), changent d'état tout seul. Si la case est cochée, cela peut éteindre le _VTherm_ alors que ce n'est pas ce que vous souhaitiez. Certains équipements (Daikin par exemple), changent d'état tout seul. Si la case est cochée, cela peut éteindre le _VTherm_ alors que ce n'est pas ce que vous souhaitiez.
C'est pour ça qu'il est préférable de ne pas l'utiliser. Cela génère beaucoup d'incompréhensions et de nombreuses demandes de support. C'est pour ça qu'il est préférable de ne pas l'utiliser. Cela génère beaucoup d'incompréhensions et de nombreuses demandes de support.
+3 -40
View File
@@ -7,7 +7,6 @@
- [Le keep-alive](#le-keep-alive) - [Le keep-alive](#le-keep-alive)
- [Le mode AC](#le-mode-ac) - [Le mode AC](#le-mode-ac)
- [L'inversion de la commande](#linversion-de-la-commande) - [L'inversion de la commande](#linversion-de-la-commande)
- [La personnalisation des commandes](#la-personnalisation-des-commandes)
## Pré-requis ## Pré-requis
@@ -18,8 +17,8 @@ L'installation doit ressembler à ça :
1. L'utilisateur ou une automatisation ou le Sheduler programme une consigne (setpoint) par le biais d'un pre-réglage ou directement d'une température, 1. L'utilisateur ou une automatisation ou le Sheduler programme une consigne (setpoint) par le biais d'un pre-réglage ou directement d'une température,
2. régulièrement le thermomètre intérieur (2) ou extérieur (2b) envoie la température mesurée. Le thermomètre interieur doit être placé à une place pertinente pour le ressenti de l'utilisateur : idéalement au milieu du lieu de vie. Evitez de le mettre trop près d'une fenêtre ou trop proche du radiateur, 2. régulièrement le thermomètre intérieur (2) ou extérieur (2b) envoie la température mesurée. Le thermomètre interieur doit être placé à une place pertinente pour le ressenti de l'utilisateur : idéalement au milieu du lieu de vie. Evitez de le mettre trop près d'une fenêtre ou trop proche du radiateur,
3. avec les valeurs de consigne, les différentes températures et des paramètres de l'algorithme TPI (cf. [TPI](algorithms.md#lalgorithme-tpi)), VTherm va calculer un pourcentage de temps d'allumage, 3. avec les valeurs de consigne, les différentes températures et des paramètres de l'algorithme TPI (cf. [TPI](algorithms.md#lalgorithme-tpi)), VTherm va calculer un pourcentage de temps d'allumage,
4. et va régulièrement commander l'allumage et l'extinction du ou des entités `switch` (ou `select` ou `climate`) sous-jacentes, 4. et va régulièrement commander l'allumage et l'extinction du ou des entités `switch` sous-jacentes,
5. ces entités sous-jacentes vont alors commander l'équipement physique 5. ces entités switchs sous-jacentes vont alors commander le switch physique
6. la commande du switch physique allumera ou éteindra le radiateur. 6. la commande du switch physique allumera ou éteindra le radiateur.
> Le pourcentage d'allumage est recalculé à chaque cycle et c'est ce qui permet de réguler la température de la pièce. > Le pourcentage d'allumage est recalculé à chaque cycle et c'est ce qui permet de réguler la température de la pièce.
@@ -32,9 +31,7 @@ Ensuite cliquez sur l'option de menu "Sous-jacents" et vous allez avoir cette pa
![image](images/config-linked-entity.png) ![image](images/config-linked-entity.png)
### les sous-jacents ### les sous-jacents
Dans la "liste des équipements à contrôler" vous mettez les switchs qui vont être controllés par le VTherm. Seuls les entités de type `switch` ou `input_boolean` ou `select` ou `input_select` ou `climate` sont acceptées. Dans la "liste des équipements à contrôler" vous mettez les switchs qui vont être controllés par le VTherm. Seuls les entités de type `switch` ou `input_boolean` sont acceptées.
Si un des sous-jacents n'est pas un `switch` alors la personnalisation des commandes est obligatoires. Par défaut pour les `switch` les commandes sont les commandes classique d'allumage / extinction du switch (`turn_on`, `turn_off`)
L'algorithme à utiliser est aujourd'hui limité à TPI est disponible. Voir [algorithme](#algorithme). L'algorithme à utiliser est aujourd'hui limité à TPI est disponible. Voir [algorithme](#algorithme).
Si plusieurs entités de type sont configurées, la thermostat décale les activations afin de minimiser le nombre de switch actif à un instant t. Ca permet une meilleure répartition de la puissance puisque chaque radiateur va s'allumer à son tour. Si plusieurs entités de type sont configurées, la thermostat décale les activations afin de minimiser le nombre de switch actif à un instant t. Ca permet une meilleure répartition de la puissance puisque chaque radiateur va s'allumer à son tour.
@@ -57,37 +54,3 @@ Il est possible de choisir un thermostat over switch qui commande une climatisat
Si votre équipement est commandé par un fil pilote avec un diode, vous aurez certainement besoin de cocher la case "Inverser la case". Elle permet de mettre le switch à `On` lorsqu'on doit étiendre l'équipement et à `Off` lorsqu'on doit l'allumer. Les temps de cycle sont donc inversés avec cette option. Si votre équipement est commandé par un fil pilote avec un diode, vous aurez certainement besoin de cocher la case "Inverser la case". Elle permet de mettre le switch à `On` lorsqu'on doit étiendre l'équipement et à `Off` lorsqu'on doit l'allumer. Les temps de cycle sont donc inversés avec cette option.
### La personnalisation des commandes
Cette section de configuration permet de personnaliser les commandes d'allumage et d'extinction envoyée à l'équipement sous-jacent,
Ces commandes sont obligatoires si un des sous-jacents n'est pas un `switch` (pour les `switchs` les commandes d'allumage/extinction classiques sont utilisées).
Pour personnaliser les commande, cliquez sur `Ajouter` en bas de page sur les commandes d'allumage et sur les commandes d'extinction :
![virtual switch](images/config-vswitch1.png)
et donner la commande d'allumage et d'exinction avec le format `commande[/attribut[:valeur]]`.
Les commandes possibles dépendent du type de sous-jacents :
| type de sous-jacent | commandes d'allumage possibles | commandes d'extinction possibles | S'applique à |
| --------------------------- | ------------------------------------- | ----------------------------------- | ------------------------------ |
| `switch` ou `input_boolean` | `turn_on` | `turn_off` | tous les switchs |
| `select` ou `input_select` | `select_option/option:comfort` | `set_option/option:frost` | Nodon SIN-4-FP-21 et assimilés |
| `climate` (hvac_mode) | `set_hvac_mode/hvac_mode:heat` | `set_hvac_mode/hvac_mode:off` | eCosy (via Tuya Local) |
| `climate` (preset) | `set_preset_mode/preset_mode:comfort` | `set_preset_mode/preset_mode:frost` | Heatzy |
Evidemment, tous ces exemples peuvent être adaptés à votre cas.
Exemple pour un Nodon SIN-4-FP-21 :
![virtual switch Nodon](images/config-vswitch2.png)
Cliquez sur valider pour accepter les modifications.
Si l'erreur suivante se produit :
> La configuration de la personnalisation des commandes est incorrecte. Elle est obligatoire pour les sous-jacents non switch et le format doit être 'service_name[/attribut:valeur]'. Plus d'informations dans le README.
Cela signifie que une des commandes saisies est invalide. Les règles à respecter sont les suivantes :
1. chaque commande doit avoir le format `commande[/attribut[:valeur]]` (ex: `select_option/option:comfort` ou `turn_on`) sans blanc et sans caractères spéciaux sauf '_',
2. il doit y avoir autant de commandes qu'il y a de sous-jacents déclarés sauf si tous les sous-jacents sont des `switchs` auquel cas il n'est pas nécessaire de paramétrer les commandes,
3. si plusieurs sous-jacents sont configurés, les commandes doivent être dans le même ordre. Le nombre de commandes d'allumage doit être égal au nombre de commandes d'extinction et de sous-jacents (dans l'ordre donc). Il est possible de mettre des sous-jacents de type différent. À partir du moment où un sous-jacent n'est pas un `switch`, il faut paramétrer toutes les commandes de tous les sous-jacents y compris des éventuels switchs.
+3 -3
View File
@@ -32,8 +32,8 @@ 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. Pour les Sonoff TRVZB, ils ne doivent pas être fournis, 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, sinon elle sera totalement close (0). Cela permet de laisser passer suffisamment d'eau lorsqu'elle doit être ouverte mais garde la fermeeture complète si il n'y a pas besoin de chauffer. 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`.
@@ -152,4 +152,4 @@ Pour que les modifications soient prises en compte, il faut soit **relancer tota
## Synthèse de l'algorithme d'auto-régulation ## Synthèse de l'algorithme d'auto-régulation
Une synthèse de l'algorithme d'auto-régulation est décrite [ici](algorithms.md#lalgorithme-dauto-régulation-sans-contrôle-de-la-vanne) Une synthèse de l'algorithme d'auto-régulation est décrite [ici](algorithms.md#lalgorithme-dauto-régulation-sans-contrôle-de-la-vanne)
Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 KiB

+2 -2
View File
@@ -1,7 +1,7 @@
# pylint: disable=wildcard-import, unused-wildcard-import, unused-argument, line-too-long, protected-access # pylint: disable=wildcard-import, unused-wildcard-import, unused-argument, line-too-long, protected-access
""" Test the normal start of a Thermostat """ """ Test the normal start of a Thermostat """
from unittest.mock import patch, PropertyMock from unittest.mock import patch
from datetime import timedelta, datetime from datetime import timedelta, datetime
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
@@ -179,7 +179,7 @@ async def test_overpowering_binary_sensors(
) )
# fmt:off # fmt:off
with patch("homeassistant.core.StateMachine.get", side_effect=side_effects.get_side_effects()), \ with patch("homeassistant.core.StateMachine.get", side_effect=side_effects.get_side_effects()), \
patch("custom_components.versatile_thermostat.thermostat_switch.ThermostatOverSwitch.is_device_active", new_callable=PropertyMock, return_value=True): patch("custom_components.versatile_thermostat.thermostat_switch.ThermostatOverSwitch.is_device_active", return_value="True"):
# fmt: on # fmt: on
await send_power_change_event(entity, 150, now) await send_power_change_event(entity, 150, now)
await send_max_power_change_event(entity, 100, now) await send_max_power_change_event(entity, 100, now)
-156
View File
@@ -1,6 +1,5 @@
# pylint: disable=protected-access, unused-argument, line-too-long # pylint: disable=protected-access, unused-argument, line-too-long
""" Test the Central Power management """ """ Test the Central Power management """
import asyncio
from unittest.mock import patch, AsyncMock, MagicMock, PropertyMock from unittest.mock import patch, AsyncMock, MagicMock, PropertyMock
from datetime import datetime, timedelta from datetime import datetime, timedelta
import logging import logging
@@ -11,14 +10,6 @@ from custom_components.versatile_thermostat.feature_power_manager import (
from custom_components.versatile_thermostat.central_feature_power_manager import ( from custom_components.versatile_thermostat.central_feature_power_manager import (
CentralFeaturePowerManager, CentralFeaturePowerManager,
) )
from custom_components.versatile_thermostat.thermostat_switch import (
ThermostatOverSwitch,
)
from custom_components.versatile_thermostat.thermostat_climate import (
ThermostatOverClimate,
)
from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import
logging.getLogger().setLevel(logging.DEBUG) logging.getLogger().setLevel(logging.DEBUG)
@@ -709,150 +700,3 @@ async def test_central_power_manager_max_power_event(
assert central_power_manager.current_max_power == expected_power assert central_power_manager.current_max_power == expected_power
assert mock_calculate_shedding.call_count == nb_call assert mock_calculate_shedding.call_count == nb_call
async def test_central_power_manager_start_vtherm_power(hass: HomeAssistant, skip_hass_states_is_state, init_central_power_manager):
"""Tests the central power start VTherm power. The objective is to starts VTherm until the power max is exceeded"""
temps = {
"eco": 17,
"comfort": 18,
"boost": 19,
}
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: False,
CONF_USE_MOTION_FEATURE: False,
CONF_USE_POWER_FEATURE: True,
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_SAFETY_DELAY_MIN: 5,
CONF_SAFETY_MIN_ON_PERCENT: 0.3,
CONF_DEVICE_POWER: 1000,
CONF_PRESET_POWER: 12,
},
)
entity: ThermostatOverSwitch = await create_thermostat(hass, entry, "climate.theoverswitchmockname", temps)
assert entity
now: datetime = NowClass.get_now(hass)
VersatileThermostatAPI.get_vtherm_api()._set_now(now)
central_power_manager = VersatileThermostatAPI.get_vtherm_api().central_power_manager
assert central_power_manager
side_effects = SideEffects(
{
"sensor.the_power_sensor": State("sensor.the_power_sensor", 1000),
"sensor.the_max_power_sensor": State("sensor.the_max_power_sensor", 2100),
},
State("unknown.entity_id", "unknown"),
)
# 1. Make the heater heats
# fmt: off
with patch("homeassistant.core.StateMachine.get", side_effect=side_effects.get_side_effects()), \
patch("custom_components.versatile_thermostat.thermostat_switch.ThermostatOverSwitch.is_device_active", new_callable=PropertyMock, return_value=False):
# fmt: on
# make the heater heats
await send_power_change_event(entity, 1000, now)
await send_max_power_change_event(entity, 2100, now)
await send_temperature_change_event(entity, 15, now)
await send_ext_temperature_change_event(entity, 1, now)
await entity.async_set_preset_mode(PRESET_BOOST)
assert entity.preset_mode is PRESET_BOOST
assert entity.power_manager.overpowering_state is STATE_UNKNOWN
assert entity.target_temperature == 19
await hass.async_block_till_done()
await entity.async_set_hvac_mode(HVACMode.HEAT)
assert entity.hvac_mode is HVACMode.HEAT
await hass.async_block_till_done()
await asyncio.sleep(0.1)
# the power of Vtherm should have been added
assert central_power_manager.started_vtherm_total_power == 1000
# 2. Check that another heater cannot heat
entry2 = MockConfigEntry(
domain=DOMAIN,
title="TheOverClimateMockName2",
unique_id="uniqueId2",
data={
CONF_NAME: "TheOverClimateMockName2",
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: 15,
CONF_TEMP_MAX: 30,
CONF_USE_WINDOW_FEATURE: False,
CONF_USE_MOTION_FEATURE: False,
CONF_USE_POWER_FEATURE: True,
CONF_USE_PRESENCE_FEATURE: False,
CONF_UNDERLYING_LIST: ["switch.mock_climate"],
CONF_MINIMAL_ACTIVATION_DELAY: 30,
CONF_SAFETY_DELAY_MIN: 5,
CONF_SAFETY_MIN_ON_PERCENT: 0.3,
CONF_DEVICE_POWER: 150,
CONF_PRESET_POWER: 12,
},
)
entity2: ThermostatOverClimate = await create_thermostat(hass, entry2, "climate.theoverclimatemockname2", temps)
assert entity2
fake_underlying_climate = MockClimate(
hass=hass,
unique_id="mockUniqueId",
name="MockClimateName",
)
# fmt: off
with patch("homeassistant.core.StateMachine.get", side_effect=side_effects.get_side_effects()), \
patch("custom_components.versatile_thermostat.thermostat_switch.ThermostatOverSwitch.is_device_active", new_callable=PropertyMock, return_value=False), \
patch("custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",return_value=fake_underlying_climate):
# fmt: on
# make the heater heats
await entity2.async_set_preset_mode(PRESET_COMFORT)
assert entity2.preset_mode is PRESET_COMFORT
assert entity2.power_manager.overpowering_state is STATE_UNKNOWN
assert entity2.target_temperature == 18
await entity2.async_set_hvac_mode(HVACMode.HEAT)
assert entity2.hvac_mode is HVACMode.HEAT
await hass.async_block_till_done()
await asyncio.sleep(0.1)
# the power of Vtherm should have not been added (cause it has not started) and the entity2 should be shedding
assert central_power_manager.started_vtherm_total_power == 1000
assert entity2.power_manager.overpowering_state is STATE_ON
# 3. sends a new power sensor event
await send_max_power_change_event(entity, 2150, now)
# No change
assert central_power_manager.started_vtherm_total_power == 1000
await send_power_change_event(entity, 1010, now)
assert central_power_manager.started_vtherm_total_power == 0
+66 -33
View File
@@ -347,17 +347,22 @@ async def test_motion_management_time_not_enough(
assert entity.presence_state == STATE_ON assert entity.presence_state == STATE_ON
# 2. starts detecting motion with time not enough # 2. starts detecting motion with time not enough
with patch("custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event") as mock_send_event, patch( with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on" "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on, patch("custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off") as mock_heater_off, patch( ) as mock_heater_on, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active", "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
new_callable=PropertyMock,
return_value=False, return_value=False,
), patch( ), patch(
"homeassistant.helpers.condition.state", return_value=False "homeassistant.helpers.condition.state", return_value=False
) as mock_condition, patch( ) as mock_condition, patch(
"homeassistant.core.StateMachine.get", "homeassistant.core.StateMachine.get",
return_value=State(entity_id="binary_sensor.mock_motion_sensor", state=STATE_OFF), return_value=State(
entity_id="binary_sensor.mock_motion_sensor", state=STATE_OFF
),
): ):
event_timestamp = now - timedelta(minutes=4) event_timestamp = now - timedelta(minutes=4)
try_condition = await send_motion_change_event( try_condition = await send_motion_change_event(
@@ -382,11 +387,14 @@ async def test_motion_management_time_not_enough(
assert mock_send_event.call_count == 0 assert mock_send_event.call_count == 0
# starts detecting motion with time enough this time # starts detecting motion with time enough this time
with patch("custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event") as mock_send_event, patch( with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on" "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on, patch("custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off") as mock_heater_off, patch( ) as mock_heater_on, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active", "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
new_callable=PropertyMock,
return_value=False, return_value=False,
), patch( ), patch(
"homeassistant.helpers.condition.state", return_value=True "homeassistant.helpers.condition.state", return_value=True
@@ -407,17 +415,22 @@ async def test_motion_management_time_not_enough(
assert entity.presence_state == STATE_ON assert entity.presence_state == STATE_ON
# stop detecting motion with off delay too low # stop detecting motion with off delay too low
with patch("custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event") as mock_send_event, patch( with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on" "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on, patch("custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off") as mock_heater_off, patch( ) as mock_heater_on, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active", "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
new_callable=PropertyMock,
return_value=True, return_value=True,
) as mock_device_active, patch( ) as mock_device_active, patch(
"homeassistant.helpers.condition.state", return_value=False "homeassistant.helpers.condition.state", return_value=False
) as mock_condition, patch( ) as mock_condition, patch(
"homeassistant.core.StateMachine.get", "homeassistant.core.StateMachine.get",
return_value=State(entity_id="binary_sensor.mock_motion_sensor", state=STATE_OFF), return_value=State(
entity_id="binary_sensor.mock_motion_sensor", state=STATE_OFF
),
): ):
event_timestamp = now - timedelta(minutes=2) event_timestamp = now - timedelta(minutes=2)
try_condition = await send_motion_change_event( try_condition = await send_motion_change_event(
@@ -441,11 +454,14 @@ async def test_motion_management_time_not_enough(
assert mock_send_event.call_count == 0 assert mock_send_event.call_count == 0
# stop detecting motion with off delay enough long # stop detecting motion with off delay enough long
with patch("custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event") as mock_send_event, patch( with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on" "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on, patch("custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off") as mock_heater_off, patch( ) as mock_heater_on, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active", "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
new_callable=PropertyMock,
return_value=True, return_value=True,
) as mock_device_active, patch( ) as mock_device_active, patch(
"homeassistant.helpers.condition.state", return_value=True "homeassistant.helpers.condition.state", return_value=True
@@ -546,11 +562,14 @@ async def test_motion_management_time_enough_and_presence(
assert entity.presence_state == "on" assert entity.presence_state == "on"
# starts detecting motion # starts detecting motion
with patch("custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event") as mock_send_event, patch( with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on" "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on, patch("custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off") as mock_heater_off, patch( ) as mock_heater_on, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active", "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
new_callable=PropertyMock,
return_value=False, return_value=False,
), patch( ), patch(
"homeassistant.helpers.condition.state", return_value=True "homeassistant.helpers.condition.state", return_value=True
@@ -571,11 +590,14 @@ async def test_motion_management_time_enough_and_presence(
assert mock_send_event.call_count == 0 assert mock_send_event.call_count == 0
# stop detecting motion with confirmation of stop # stop detecting motion with confirmation of stop
with patch("custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event") as mock_send_event, patch( with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on" "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on, patch("custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off") as mock_heater_off, patch( ) as mock_heater_on, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active", "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
new_callable=PropertyMock,
return_value=True, return_value=True,
), patch( ), patch(
"homeassistant.helpers.condition.state", return_value=True "homeassistant.helpers.condition.state", return_value=True
@@ -671,11 +693,14 @@ async def test_motion_management_time_enough_and_not_presence(
assert entity.presence_state == STATE_OFF assert entity.presence_state == STATE_OFF
# starts detecting motion # starts detecting motion
with patch("custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event") as mock_send_event, patch( with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on" "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on, patch("custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off") as mock_heater_off, patch( ) as mock_heater_on, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active", "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
new_callable=PropertyMock,
return_value=False, return_value=False,
), patch( ), patch(
"homeassistant.helpers.condition.state", return_value=True "homeassistant.helpers.condition.state", return_value=True
@@ -697,11 +722,14 @@ async def test_motion_management_time_enough_and_not_presence(
assert mock_send_event.call_count == 0 assert mock_send_event.call_count == 0
# stop detecting motion with confirmation of stop # stop detecting motion with confirmation of stop
with patch("custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event") as mock_send_event, patch( with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on" "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on, patch("custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off") as mock_heater_off, patch( ) as mock_heater_on, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active", "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
new_callable=PropertyMock,
return_value=True, return_value=True,
), patch( ), patch(
"homeassistant.helpers.condition.state", return_value=True "homeassistant.helpers.condition.state", return_value=True
@@ -798,11 +826,14 @@ async def test_motion_management_with_stop_during_condition(
assert entity.presence_state == STATE_OFF assert entity.presence_state == STATE_OFF
# starts detecting motion # starts detecting motion
with patch("custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event") as mock_send_event, patch( with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on" "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on, patch("custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off") as mock_heater_off, patch( ) as mock_heater_on, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active", "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
new_callable=PropertyMock,
return_value=True, return_value=True,
), patch( ), patch(
"homeassistant.helpers.condition.state", return_value=True "homeassistant.helpers.condition.state", return_value=True
@@ -928,11 +959,12 @@ async def test_motion_management_with_stop_during_condition_last_state_on(
# 1. starts detecting motion but the sensor is off # 1. starts detecting motion but the sensor is off
with patch( with patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active", "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
new_callable=PropertyMock,
return_value=True, return_value=True,
), patch("homeassistant.helpers.condition.state", return_value=False), patch( ), patch("homeassistant.helpers.condition.state", return_value=False), patch(
"homeassistant.core.StateMachine.get", "homeassistant.core.StateMachine.get",
return_value=State(entity_id="binary_sensor.mock_motion_sensor", state=STATE_OFF), return_value=State(
entity_id="binary_sensor.mock_motion_sensor", state=STATE_OFF
),
): ):
event_timestamp = now - timedelta(minutes=5) event_timestamp = now - timedelta(minutes=5)
try_condition1 = await send_motion_change_event( try_condition1 = await send_motion_change_event(
@@ -950,11 +982,12 @@ async def test_motion_management_with_stop_during_condition_last_state_on(
# 2. starts detecting motion but the sensor is on # 2. starts detecting motion but the sensor is on
with patch( with patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active", "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
new_callable=PropertyMock,
return_value=True, return_value=True,
), patch("homeassistant.helpers.condition.state", return_value=False), patch( ), patch("homeassistant.helpers.condition.state", return_value=False), patch(
"homeassistant.core.StateMachine.get", "homeassistant.core.StateMachine.get",
return_value=State(entity_id="binary_sensor.mock_motion_sensor", state=STATE_ON), return_value=State(
entity_id="binary_sensor.mock_motion_sensor", state=STATE_ON
),
): ):
event_timestamp = now - timedelta(minutes=5) event_timestamp = now - timedelta(minutes=5)
try_condition1 = await send_motion_change_event( try_condition1 = await send_motion_change_event(
+46 -27
View File
@@ -2,7 +2,7 @@
""" Test the Multiple switch management """ """ Test the Multiple switch management """
import asyncio import asyncio
from unittest.mock import patch, call, ANY, PropertyMock from unittest.mock import patch, call, ANY
from datetime import datetime, timedelta from datetime import datetime, timedelta
import logging import logging
@@ -84,11 +84,14 @@ async def test_one_switch_cycle(
assert mock_is_state.call_count == 1 assert mock_is_state.call_count == 1
# Set temperature to a low level # Set temperature to a low level
with patch("custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event") as mock_send_event, patch( with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on" "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on, patch("custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off") as mock_heater_off, patch( ) as mock_heater_on, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active", "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
new_callable=PropertyMock,
return_value=False, return_value=False,
) as mock_device_active, patch( ) as mock_device_active, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.call_later", "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.call_later",
@@ -104,8 +107,7 @@ async def test_one_switch_cycle(
# assert mock_heater_on.call_count == 1 # assert mock_heater_on.call_count == 1
assert mock_heater_on.call_count == 0 assert mock_heater_on.call_count == 0
# There is no check if active # There is no check if active
# don't work with PropertyMock assert mock_device_active.call_count == 0
# assert mock_device_active.call_count == 0
# 4 calls dispatched along the cycle # 4 calls dispatched along the cycle
assert mock_call_later.call_count == 1 assert mock_call_later.call_count == 1
@@ -117,11 +119,14 @@ async def test_one_switch_cycle(
# Set a temperature at middle level # Set a temperature at middle level
event_timestamp = now - timedelta(minutes=4) event_timestamp = now - timedelta(minutes=4)
with patch("custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event") as mock_send_event, patch( with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on" "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on, patch("custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off") as mock_heater_off, patch( ) as mock_heater_on, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active", "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
new_callable=PropertyMock,
return_value=False, return_value=False,
) as mock_device_active: ) as mock_device_active:
await send_temperature_change_event(entity, 18, event_timestamp) await send_temperature_change_event(entity, 18, event_timestamp)
@@ -136,11 +141,14 @@ async def test_one_switch_cycle(
# Set another temperature at middle level # Set another temperature at middle level
event_timestamp = now - timedelta(minutes=3) event_timestamp = now - timedelta(minutes=3)
with patch("custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event") as mock_send_event, patch( with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on" "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on, patch("custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off") as mock_heater_off, patch( ) as mock_heater_on, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active", "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
new_callable=PropertyMock,
return_value=True, return_value=True,
) as mock_device_active: ) as mock_device_active:
await send_temperature_change_event(entity, 18.1, event_timestamp) await send_temperature_change_event(entity, 18.1, event_timestamp)
@@ -168,11 +176,14 @@ async def test_one_switch_cycle(
# Simulate the end of heater on cycle # Simulate the end of heater on cycle
event_timestamp = now - timedelta(minutes=3) event_timestamp = now - timedelta(minutes=3)
with patch("custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event") as mock_send_event, patch( with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on" "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on, patch("custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off") as mock_heater_off, patch( ) as mock_heater_on, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active", "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
new_callable=PropertyMock,
return_value=True, return_value=True,
) as mock_device_active: ) as mock_device_active:
await entity.underlying_entity( await entity.underlying_entity(
@@ -190,11 +201,14 @@ async def test_one_switch_cycle(
# Simulate the start of heater on cycle # Simulate the start of heater on cycle
event_timestamp = now - timedelta(minutes=3) event_timestamp = now - timedelta(minutes=3)
with patch("custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event") as mock_send_event, patch( with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on" "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on, patch("custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off") as mock_heater_off, patch( ) as mock_heater_on, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active", "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
new_callable=PropertyMock,
return_value=True, return_value=True,
) as mock_device_active: ) as mock_device_active:
await entity.underlying_entity( await entity.underlying_entity(
@@ -292,11 +306,14 @@ async def test_multiple_switchs(
) )
# Set temperature to a low level # Set temperature to a low level
with patch("custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event") as mock_send_event, patch( with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on" "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on, patch("custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off") as mock_heater_off, patch( ) as mock_heater_on, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active", "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
new_callable=PropertyMock,
return_value=False, return_value=False,
) as mock_device_active, patch( ) as mock_device_active, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.call_later", "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.call_later",
@@ -312,8 +329,7 @@ async def test_multiple_switchs(
# assert mock_heater_on.call_count == 1 # assert mock_heater_on.call_count == 1
assert mock_heater_on.call_count == 0 assert mock_heater_on.call_count == 0
# There is no check if active # There is no check if active
# don't work with PropertyMock assert mock_device_active.call_count == 0
# assert mock_device_active.call_count == 0
# 4 calls dispatched along the cycle # 4 calls dispatched along the cycle
assert mock_call_later.call_count == 4 assert mock_call_later.call_count == 4
@@ -328,11 +344,14 @@ async def test_multiple_switchs(
# Set a temperature at middle level # Set a temperature at middle level
event_timestamp = now - timedelta(minutes=4) event_timestamp = now - timedelta(minutes=4)
with patch("custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event") as mock_send_event, patch( with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on" "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on, patch("custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off") as mock_heater_off, patch( ) as mock_heater_on, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active", "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
new_callable=PropertyMock,
return_value=False, return_value=False,
) as mock_device_active: ) as mock_device_active:
await send_temperature_change_event(entity, 18, event_timestamp) await send_temperature_change_event(entity, 18, event_timestamp)
@@ -799,7 +818,7 @@ async def test_multiple_switch_power_management(
with patch("custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event") as mock_send_event, \ with patch("custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event") as mock_send_event, \
patch("custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on") as mock_heater_on, \ patch("custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on") as mock_heater_on, \
patch("custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off") as mock_heater_off, \ patch("custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off") as mock_heater_off, \
patch("custom_components.versatile_thermostat.thermostat_switch.ThermostatOverSwitch.is_device_active", new_callable=PropertyMock, return_value=True): patch("custom_components.versatile_thermostat.thermostat_switch.ThermostatOverSwitch.is_device_active", return_value="True"):
#fmt: on #fmt: on
now = now + timedelta(seconds=30) now = now + timedelta(seconds=30)
VersatileThermostatAPI.get_vtherm_api()._set_now(now) VersatileThermostatAPI.get_vtherm_api()._set_now(now)
+3 -6
View File
@@ -523,7 +523,7 @@ async def test_power_management_hvac_on(
patch("custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event") as mock_send_event, \ patch("custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event") as mock_send_event, \
patch("custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on") as mock_heater_on, \ patch("custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on") as mock_heater_on, \
patch("custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off") as mock_heater_off, \ patch("custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off") as mock_heater_off, \
patch("custom_components.versatile_thermostat.thermostat_switch.ThermostatOverSwitch.is_device_active", new_callable=PropertyMock, return_value=True): patch("custom_components.versatile_thermostat.thermostat_switch.ThermostatOverSwitch.is_device_active", return_value="True"):
# fmt: on # fmt: on
now = now + timedelta(seconds=30) now = now + timedelta(seconds=30)
VersatileThermostatAPI.get_vtherm_api()._set_now(now) VersatileThermostatAPI.get_vtherm_api()._set_now(now)
@@ -913,15 +913,12 @@ async def test_power_management_turn_off_while_shedding(hass: HomeAssistant, ski
# 1. Set VTherm to overpowering # 1. Set VTherm to overpowering
# Send power max mesurement too low and HVACMode is on and device is active # Send power max mesurement too low and HVACMode is on and device is active
#
#
# fmt:off # fmt:off
with patch("homeassistant.core.StateMachine.get", side_effect=side_effects.get_side_effects()), \ with patch("homeassistant.core.StateMachine.get", side_effect=side_effects.get_side_effects()), \
patch("custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"), \ patch("custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"), \
patch("custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on") as mock_heater_on, \ patch("custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on") as mock_heater_on, \
patch("custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off") as mock_heater_off, \ patch("custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off") as mock_heater_off, \
patch("custom_components.versatile_thermostat.thermostat_switch.ThermostatOverSwitch.is_device_active", new_callable=PropertyMock, return_value=True): patch("custom_components.versatile_thermostat.thermostat_switch.ThermostatOverSwitch.is_device_active", return_value="True"):
# fmt: on # fmt: on
now = now + timedelta(seconds=30) now = now + timedelta(seconds=30)
VersatileThermostatAPI.get_vtherm_api()._set_now(now) VersatileThermostatAPI.get_vtherm_api()._set_now(now)
@@ -942,7 +939,7 @@ async def test_power_management_turn_off_while_shedding(hass: HomeAssistant, ski
patch("custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event") as mock_send_event, \ patch("custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event") as mock_send_event, \
patch("custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on") as mock_heater_on, \ patch("custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on") as mock_heater_on, \
patch("custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off") as mock_heater_off, \ patch("custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off") as mock_heater_off, \
patch("custom_components.versatile_thermostat.thermostat_switch.ThermostatOverSwitch.is_device_active", new_callable=PropertyMock, return_value=True): patch("custom_components.versatile_thermostat.thermostat_switch.ThermostatOverSwitch.is_device_active", return_value="True"):
# fmt: on # fmt: on
now = now + timedelta(seconds=30) now = now + timedelta(seconds=30)
VersatileThermostatAPI.get_vtherm_api()._set_now(now) VersatileThermostatAPI.get_vtherm_api()._set_now(now)
+24 -12
View File
@@ -2039,13 +2039,16 @@ async def test_bug_66(
assert entity.window_state is STATE_UNKNOWN assert entity.window_state is STATE_UNKNOWN
# Open the window and let the thermostat shut down # Open the window and let the thermostat shut down
with patch("custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event") as mock_send_event, patch( with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on" "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on, patch("custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off") as mock_heater_off, patch( ) as mock_heater_on, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off, patch(
"homeassistant.helpers.condition.state", return_value=True "homeassistant.helpers.condition.state", return_value=True
) as mock_condition, patch( ) as mock_condition, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active", "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
new_callable=PropertyMock,
return_value=True, return_value=True,
): ):
await send_temperature_change_event(entity, 15, now) await send_temperature_change_event(entity, 15, now)
@@ -2064,13 +2067,16 @@ async def test_bug_66(
assert entity.window_state == STATE_ON assert entity.window_state == STATE_ON
# Close the window but too shortly # Close the window but too shortly
with patch("custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event") as mock_send_event, patch( with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on" "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on, patch("custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off") as mock_heater_off, patch( ) as mock_heater_on, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off, patch(
"homeassistant.helpers.condition.state", return_value=False "homeassistant.helpers.condition.state", return_value=False
) as mock_condition, patch( ) as mock_condition, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active", "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
new_callable=PropertyMock,
return_value=False, return_value=False,
): ):
event_timestamp = now + timedelta(minutes=1) event_timestamp = now + timedelta(minutes=1)
@@ -2084,13 +2090,16 @@ async def test_bug_66(
assert entity.window_state == STATE_ON assert entity.window_state == STATE_ON
# Reopen immediatly with sufficient time # Reopen immediatly with sufficient time
with patch("custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event") as mock_send_event, patch( with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on" "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on, patch("custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off") as mock_heater_off, patch( ) as mock_heater_on, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off, patch(
"homeassistant.helpers.condition.state", return_value=True "homeassistant.helpers.condition.state", return_value=True
) as mock_condition, patch( ) as mock_condition, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active", "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
new_callable=PropertyMock,
return_value=False, return_value=False,
): ):
try_window_condition = await send_window_change_event( try_window_condition = await send_window_change_event(
@@ -2104,13 +2113,16 @@ async def test_bug_66(
assert entity.hvac_mode == HVACMode.OFF assert entity.hvac_mode == HVACMode.OFF
# Close the window but with sufficient time this time # Close the window but with sufficient time this time
with patch("custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event") as mock_send_event, patch( with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on" "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on, patch("custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off") as mock_heater_off, patch( ) as mock_heater_on, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off, patch(
"homeassistant.helpers.condition.state", return_value=True "homeassistant.helpers.condition.state", return_value=True
) as mock_condition, patch( ) as mock_condition, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active", "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
new_callable=PropertyMock,
return_value=False, return_value=False,
): ):
event_timestamp = now + timedelta(minutes=2) event_timestamp = now + timedelta(minutes=2)