Compare commits

..

10 Commits

Author SHA1 Message Date
Jean-Marc Collin
10c8281b32 Integration tests ok 2025-01-04 17:38:17 +00:00
Jean-Marc Collin
c01f96c955 Change shedding calculation delay to 20 sec (vs 60 sec) 2025-01-04 15:50:30 +00:00
Jean-Marc Collin
33c7c710ee enhance the overpowering algo if current_power > max_power 2025-01-04 15:26:29 +00:00
Jean-Marc Collin
6d0ebbaaab integrattion tests - Do startup works 2025-01-04 11:07:41 +00:00
Jean-Marc Collin
0b5d937968 All tests ok 2025-01-04 09:39:42 +00:00
Jean-Marc Collin
24fcb7a161 All tests not ok 2025-01-03 17:43:27 +00:00
Jean-Marc Collin
03fbc5362a All tests ok for central_feature_power_manager 2025-01-03 10:24:09 +00:00
Jean-Marc Collin
9f3199a053 Commit for rebase 2025-01-03 08:10:39 +00:00
Jean-Marc Collin
7a636c0a72 With tests of calculate_shedding ok 2025-01-03 08:10:39 +00:00
Jean-Marc Collin
6237273029 First implem + tests (not finished) 2025-01-03 08:10:39 +00:00
47 changed files with 3785 additions and 463 deletions

View File

@@ -39,28 +39,27 @@ Un grand merci à tous mes fournisseurs de bières pour leurs dons et leurs enco
La documentation est maintenant découpée en plusieurs pages pour faciliter la lecture et la recherche d'informations :
1. [présentation](documentation/fr/presentation.md),
2. [Installation](documentation/fr/installation.md),
3. [choisir un type de VTherm](documentation/fr/creation.md),
4. [les attributs de base](documentation/fr/base-attributes.md)
5. [configurer un VTherm sur un `switch`](documentation/fr/over-switch.md)
6. [configurer un VTherm sur un `climate`](documentation/fr/over-climate.md)
7. [configurer un VTherm sur une vanne](documentation/fr/over-valve.md)
8. [les pré-régages (preset)](documentation/fr/feature-presets.md)
9. [la gestion des ouvertures](documentation/fr/feature-window.md)
10. [la gestion de la présence](documentation/fr/feature-presence.md)
11. [la gestion de mouvement](documentation/fr/feature-motion.md)
12. [la gestion de la puissance](documentation/fr/feature-power.md)
13. [l'auto start and stop](documentation/fr/feature-auto-start-stop.md)
14. [la contrôle centralisé de tous vos VTherms](documentation/fr/feature-central-mode.md)
15. [la commande du chauffage central](documentation/fr/feature-central-boiler.md)
16. [aspects avancés, mode sécurité](documentation/fr/feature-advanced.md)
17. [l'auto-régulation](documentation/fr/self-regulation.md)
18. [exemples de réglages](documentation/fr/tuning-examples.md)
19. [les différents algorithmes](documentation/fr/algorithms.md)
20. [documentation de référence](documentation/fr/reference.md)
21. [exemple de réglages](documentation/fr/tuning-examples.md)
22. [dépannage](documentation/fr/troubleshooting.md)
23. [notes de version](documentation/fr/releases.md)
2. [choisir un type de VTherm](documentation/fr/creation.md),
3. [les attributs de base](documentation/fr/base-attributes.md)
3. [configurer un VTherm sur un `switch`](documentation/fr/over-switch.md)
3. [configurer un VTherm sur un `climate`](documentation/fr/over-climate.md)
3. [configurer un VTherm sur une vanne](documentation/fr/over-valve.md)
4. [les pré-régages (preset)](documentation/fr/feature-presets.md)
5. [la gestion des ouvertures](documentation/fr/feature-window.md)
6. [la gestion de la présence](documentation/fr/feature-presence.md)
7. [la gestion de mouvement](documentation/fr/feature-motion.md)
8. [la gestion de la puissance](documentation/fr/feature-power.md)
9. [l'auto start and stop](documentation/fr/feature-auto-start-stop.md)
10. [la contrôle centralisé de tous vos VTherms](documentation/fr/feature-central-mode.md)
11. [la commande du chauffage central](documentation/fr/feature-central-boiler.md)
12. [aspects avancés, mode sécurité](documentation/fr/feature-advanced.md)
12. [l'auto-régulation](documentation/fr/self-regulation.md)
13. [exemples de réglages](documentation/fr/tuning-examples.md)
14. [les différents algorithmes](documentation/fr/algorithms.md)
15. [documentation de référence](documentation/fr/reference.md)
16. [exemple de réglages](documentation/fr/tuning-examples.md)
17. [dépannage](documentation/fr/troubleshooting.md)
18. [notes de version](documentation/fr/releases.md)
# Quelques résultats

View File

@@ -27,7 +27,7 @@ class BaseFeatureManager:
"""Initialize the attributes of the FeatureManager"""
raise NotImplementedError()
async def start_listening(self):
def start_listening(self):
"""Start listening the underlying entity"""
raise NotImplementedError()

View File

@@ -53,7 +53,7 @@ from homeassistant.const import (
)
from .const import * # pylint: disable=wildcard-import, unused-wildcard-import
from .commons import ConfigData, T
from .commons import ConfigData, T, deprecated
from .config_schema import * # pylint: disable=wildcard-import, unused-wildcard-import
@@ -98,6 +98,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
"comfort_away_temp",
"power_temp",
"ac_mode",
"current_max_power",
"saved_preset_mode",
"saved_target_temp",
"saved_hvac_mode",
@@ -477,7 +478,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
# start listening for all managers
for manager in self._managers:
await manager.start_listening()
manager.start_listening()
await self.get_my_previous_state()
@@ -605,6 +606,8 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
"saved_preset_mode", None
)
self._hvac_off_reason = old_state.attributes.get("hvac_mode_reason", None)
old_total_energy = old_state.attributes.get(ATTR_TOTAL_ENERGY)
self._total_energy = old_total_energy if old_total_energy is not None else 0
_LOGGER.debug(
@@ -1606,8 +1609,14 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
return False
# Check overpowering condition
# Not usefull. Will be done at the next power refresh
# await VersatileThermostatAPI.get_vtherm_api().central_power_manager.refresh_state()
await VersatileThermostatAPI.get_vtherm_api().central_power_manager.refresh_state()
# TODO remove this
# overpowering is now centralized
# overpowering = await self._power_manager.check_overpowering()
# if overpowering == STATE_ON:
# _LOGGER.debug("%s - End of cycle (overpowering)", self)
# return True
safety: bool = await self._safety_manager.refresh_state()
if safety and self.is_over_climate:
@@ -1954,7 +1963,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
def _set_now(self, now: datetime):
"""Set the now timestamp. This is only for tests purpose
This method should be replaced by the vthermAPI equivalent"""
VersatileThermostatAPI.get_vtherm_api(self._hass)._set_now(now) # pylint: disable=protected-access
VersatileThermostatAPI.get_vtherm_api(self._hass)._set_now(now)
# @deprecated
@property
@@ -1962,20 +1971,3 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
"""Get now. The local datetime or the overloaded _set_now date
This method should be replaced by the vthermAPI equivalent"""
return VersatileThermostatAPI.get_vtherm_api(self._hass).now
@property
def power_percent(self) -> float | None:
"""Get the current on_percent as a percentage value. valid only for Vtherm with a TPI algo
Get the current on_percent value"""
if self._prop_algorithm and self._prop_algorithm.on_percent is not None:
return round(self._prop_algorithm.on_percent * 100, 0)
else:
return None
@property
def on_percent(self) -> float | None:
"""Get the current on_percent value. valid only for Vtherm with a TPI algo"""
if self._prop_algorithm and self._prop_algorithm.on_percent is not None:
return self._prop_algorithm.on_percent
else:
return None

View File

@@ -4,14 +4,10 @@ import logging
from typing import Any
from functools import cmp_to_key
from datetime import timedelta
from homeassistant.const import STATE_OFF
from homeassistant.core import HomeAssistant, Event, callback
from homeassistant.helpers.event import (
async_track_state_change_event,
EventStateChangedData,
async_call_later,
)
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.components.climate import (
@@ -46,8 +42,6 @@ class CentralFeaturePowerManager(BaseFeatureManager):
self._current_power: float = None
self._current_max_power: float = None
self._power_temp: float = None
self._cancel_calculate_shedding_call = None
# Not used now
self._last_shedding_date = None
def post_init(self, entry_infos: ConfigData):
@@ -74,7 +68,7 @@ class CentralFeaturePowerManager(BaseFeatureManager):
else:
_LOGGER.info("Power management is not fully configured and will be deactivated")
async def start_listening(self):
def start_listening(self):
"""Start listening the power sensor"""
if not self._is_configured:
return
@@ -115,54 +109,45 @@ class CentralFeaturePowerManager(BaseFeatureManager):
async def refresh_state(self) -> bool:
"""Tries to get the last state from sensor
Returns True if a change has been made"""
ret = False
if self._is_configured:
# try to acquire current power and power max
if (
new_state := get_safe_float(self._hass, self._power_sensor_entity_id)
) is not None:
self._current_power = new_state
_LOGGER.debug("Current power have been retrieved: %.3f", self._current_power)
ret = True
async def _calculate_shedding_internal(_):
_LOGGER.debug("Do the shedding calculation")
await self.calculate_shedding()
if self._cancel_calculate_shedding_call:
self._cancel_calculate_shedding_call()
self._cancel_calculate_shedding_call = None
# Try to acquire power max
if (
new_state := get_safe_float(
self._hass, self._max_power_sensor_entity_id
)
) is not None:
self._current_max_power = new_state
_LOGGER.debug("Current power max have been retrieved: %.3f", self._current_max_power)
ret = True
if not self._is_configured:
return False
# check if we need to re-calculate shedding
if ret:
now = self._vtherm_api.now
dtimestamp = (
(now - self._last_shedding_date).seconds
if self._last_shedding_date
else 999
)
if dtimestamp >= MIN_DTEMP_SECS:
await self.calculate_shedding()
self._last_shedding_date = now
# Retrieve current power
new_power = get_safe_float(self._hass, self._power_sensor_entity_id)
power_changed = new_power is not None and self._current_power != new_power
if power_changed:
self._current_power = new_power
_LOGGER.debug("New current power has been retrieved: %.3f", self._current_power)
# Retrieve max power
new_max_power = get_safe_float(self._hass, self._max_power_sensor_entity_id)
max_power_changed = new_max_power is not None and self._current_max_power != new_max_power
if max_power_changed:
self._current_max_power = new_max_power
_LOGGER.debug("New current max power has been retrieved: %.3f", self._current_max_power)
# Schedule shedding calculation if there's any change
if power_changed or max_power_changed:
if not self._cancel_calculate_shedding_call:
self._cancel_calculate_shedding_call = async_call_later(self.hass, timedelta(seconds=MIN_DTEMP_SECS), _calculate_shedding_internal)
return True
return False
# For testing purpose only, do an immediate shedding calculation
async def _do_immediate_shedding(self):
"""Do an immmediate shedding calculation if a timer was programmed.
Else, do nothing"""
if self._cancel_calculate_shedding_call:
self._cancel_calculate_shedding_call()
self._cancel_calculate_shedding_call = None
await self.calculate_shedding()
return ret
async def calculate_shedding(self):
"""Do the shedding calculation and set/unset VTherm into overpowering state"""
if not self.is_configured or self.current_max_power is None or self.current_power is None:
return
_LOGGER.debug("-------- Start of calculate_shedding")
# Find all VTherms
available_power = self.current_max_power - self.current_power
vtherms_sorted = self.find_all_vtherm_with_power_management_sorted_by_dtemp()
@@ -178,56 +163,50 @@ class CentralFeaturePowerManager(BaseFeatureManager):
total_power_gain = 0
for vtherm in vtherms_sorted:
device_power = vtherm.power_manager.device_power
if vtherm.is_device_active and not vtherm.power_manager.is_overpowering_detected:
device_power = vtherm.power_manager.device_power
total_power_gain += device_power
_LOGGER.info("vtherm %s should be in overpowering state (device_power=%.2f)", vtherm.name, device_power)
_LOGGER.debug("vtherm %s should be in overpowering state", vtherm.name)
await vtherm.power_manager.set_overpowering(True, device_power)
_LOGGER.debug("after vtherm %s total_power_gain=%s, available_power=%s", vtherm.name, total_power_gain, available_power)
if total_power_gain >= -available_power:
_LOGGER.debug("We have found enough vtherm to set to overpowering")
break
# unshedding only
else:
vtherms_sorted.reverse()
# vtherms_sorted.reverse()
_LOGGER.debug("The available power is is > 0 (%s). Do a complete shedding/un-shedding calculation for list: %s", available_power, vtherms_sorted)
total_power_added = 0
total_affected_power = 0
force_overpowering = False
for vtherm in vtherms_sorted:
# We want to do always unshedding in order to initialize the state
# so we cannot use is_overpowering_detected which test also UNKNOWN and UNAVAILABLE
if vtherm.power_manager.overpowering_state == STATE_OFF:
continue
power_consumption_max = device_power = vtherm.power_manager.device_power
# calculate the power_consumption_max
if vtherm.on_percent is not None:
power_consumption_max = max(
device_power / vtherm.nb_underlying_entities,
device_power * vtherm.on_percent,
)
device_power = vtherm.power_manager.device_power
if vtherm.is_device_active:
power_consumption_max = 0
else:
if vtherm.is_over_climate:
power_consumption_max = device_power
else:
power_consumption_max = max(
device_power / vtherm.nb_underlying_entities,
device_power * vtherm.proportional_algorithm.on_percent,
)
_LOGGER.debug("vtherm %s power_consumption_max is %s (device_power=%s, overclimate=%s)", vtherm.name, power_consumption_max, device_power, vtherm.is_over_climate)
# or not ... is for initializing the overpowering state if not already done
if total_power_added + power_consumption_max < available_power or not vtherm.power_manager.is_overpowering_detected:
# we count the unshedding only if the VTherm was in shedding
if vtherm.power_manager.is_overpowering_detected:
_LOGGER.info("vtherm %s should not be in overpowering state (power_consumption_max=%.2f)", vtherm.name, power_consumption_max)
total_power_added += power_consumption_max
if force_overpowering or (total_affected_power + power_consumption_max >= available_power):
_LOGGER.debug("vtherm %s should be in overpowering state", vtherm.name)
if not vtherm.power_manager.is_overpowering_detected:
# To force all others vtherms to be in overpowering
force_overpowering = True
await vtherm.power_manager.set_overpowering(True, power_consumption_max)
else:
total_affected_power += power_consumption_max
# Always set to false to init the state
_LOGGER.debug("vtherm %s should not be in overpowering state", vtherm.name)
await vtherm.power_manager.set_overpowering(False)
if total_power_added >= available_power:
_LOGGER.debug("We have found enough vtherm to set to non-overpowering")
break
_LOGGER.debug("after vtherm %s total_power_added=%s, available_power=%s", vtherm.name, total_power_added, available_power)
self._last_shedding_date = self._vtherm_api.now
_LOGGER.debug("-------- End of calculate_shedding")
_LOGGER.debug("after vtherm %s total_affected_power=%s, available_power=%s", vtherm.name, total_affected_power, available_power)
def get_climate_components_entities(self) -> list:
"""Get all VTherms entitites"""

View File

@@ -90,10 +90,11 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
CONF_USE_MOTION_FEATURE, False
) and (self._infos.get(CONF_MOTION_SENSOR) is not None or is_central_config)
self._infos[CONF_USE_POWER_FEATURE] = (
self._infos.get(CONF_USE_POWER_CENTRAL_CONFIG, False)
or self._infos.get(CONF_USE_POWER_FEATURE, False)
or (is_central_config and self._infos.get(CONF_POWER_SENSOR) is not None and self._infos.get(CONF_MAX_POWER_SENSOR) is not None)
self._infos[CONF_USE_POWER_FEATURE] = self._infos.get(
CONF_USE_POWER_CENTRAL_CONFIG, False
) or (
self._infos.get(CONF_POWER_SENSOR) is not None
and self._infos.get(CONF_MAX_POWER_SENSOR) is not None
)
self._infos[CONF_USE_PRESENCE_FEATURE] = (
self._infos.get(CONF_USE_PRESENCE_CENTRAL_CONFIG, False)
@@ -183,7 +184,7 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
Data has the keys from STEP_*_DATA_SCHEMA with values provided by the user.
"""
# check the entity_ids
# check the heater_entity_id
for conf in [
CONF_UNDERLYING_LIST,
CONF_TEMP_SENSOR,
@@ -329,7 +330,14 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
):
return False
if infos.get(CONF_USE_POWER_FEATURE, False) is True and infos.get(CONF_USE_POWER_CENTRAL_CONFIG, False) is False and infos.get(CONF_PRESET_POWER, None) is None:
if (
infos.get(CONF_USE_POWER_FEATURE, False) is True
and infos.get(CONF_USE_POWER_CENTRAL_CONFIG, False) is False
and (
infos.get(CONF_POWER_SENSOR, None) is None
or infos.get(CONF_MAX_POWER_SENSOR, None) is None
)
):
return False
if (
@@ -807,7 +815,7 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
"""Handle the specific power flow steps"""
_LOGGER.debug("Into ConfigFlow.async_step_spec_power user_input=%s", user_input)
schema = STEP_NON_CENTRAL_POWER_DATA_SCHEMA
schema = STEP_CENTRAL_POWER_DATA_SCHEMA
self._infos[COMES_FROM] = "async_step_spec_power"

View File

@@ -71,7 +71,7 @@ class FeatureAutoStartStopManager(BaseFeatureManager):
)
@overrides
async def start_listening(self):
def start_listening(self):
"""Start listening the underlying entity"""
@overrides

View File

@@ -86,7 +86,7 @@ class FeatureMotionManager(BaseFeatureManager):
self._motion_state = STATE_UNKNOWN
@overrides
async def start_listening(self):
def start_listening(self):
"""Start listening the underlying entity"""
if self._is_configured:
self.stop_listening()

View File

@@ -61,17 +61,19 @@ class FeaturePowerManager(BaseFeatureManager):
self._is_configured = False
@overrides
async def start_listening(self):
def start_listening(self):
"""Start listening the underlying entity. There is nothing to listen"""
central_power_configuration = (
VersatileThermostatAPI.get_vtherm_api().central_power_manager.is_configured
)
if self._use_power_feature and self._device_power and central_power_configuration:
if (
self._use_power_feature
and self._device_power
and central_power_configuration
):
self._is_configured = True
# Try to restore _overpowering_state from previous state
old_state = await self._vtherm.async_get_last_state()
self._overpowering_state = STATE_ON if old_state and old_state.attributes and old_state.attributes.get("overpowering_state") == STATE_ON else STATE_UNKNOWN
self._overpowering_state = STATE_UNKNOWN
else:
if self._use_power_feature:
if not central_power_configuration:

View File

@@ -67,7 +67,7 @@ class FeaturePresenceManager(BaseFeatureManager):
self._presence_state = STATE_UNKNOWN
@overrides
async def start_listening(self):
def start_listening(self):
"""Start listening the underlying entity"""
if self._is_configured:
self.stop_listening()

View File

@@ -70,7 +70,7 @@ class FeatureSafetyManager(BaseFeatureManager):
self._is_configured = True
@overrides
async def start_listening(self):
def start_listening(self):
"""Start listening the underlying entity"""
@overrides

View File

@@ -124,7 +124,7 @@ class FeatureWindowManager(BaseFeatureManager):
self._window_state = STATE_UNKNOWN
@overrides
async def start_listening(self):
def start_listening(self):
"""Start listening the underlying entity"""
if self._is_configured:
self.stop_listening()

View File

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

View File

@@ -263,6 +263,14 @@ class ThermostatOverClimateValve(ThermostatOverClimate):
"""True if the Thermostat is regulated by valve"""
return True
@property
def power_percent(self) -> float | None:
"""Get the current on_percent value"""
if self._prop_algorithm:
return round(self._prop_algorithm.on_percent * 100, 0)
else:
return None
# @property
# def hvac_modes(self) -> list[HVACMode]:
# """Get the hvac_modes"""

View File

@@ -26,21 +26,23 @@ _LOGGER = logging.getLogger(__name__)
class ThermostatOverSwitch(BaseThermostat[UnderlyingSwitch]):
"""Representation of a base class for a Versatile Thermostat over a switch."""
_entity_component_unrecorded_attributes = BaseThermostat._entity_component_unrecorded_attributes.union( # pylint: disable=protected-access
frozenset(
{
"is_over_switch",
"is_inversed",
"underlying_entities",
"on_time_sec",
"off_time_sec",
"cycle_min",
"function",
"tpi_coef_int",
"tpi_coef_ext",
"power_percent",
"calculated_on_percent",
}
_entity_component_unrecorded_attributes = (
BaseThermostat._entity_component_unrecorded_attributes.union(
frozenset(
{
"is_over_switch",
"is_inversed",
"underlying_entities",
"on_time_sec",
"off_time_sec",
"cycle_min",
"function",
"tpi_coef_int",
"tpi_coef_ext",
"power_percent",
"calculated_on_percent",
}
)
)
)
@@ -59,6 +61,14 @@ class ThermostatOverSwitch(BaseThermostat[UnderlyingSwitch]):
"""True if the switch is inversed (for pilot wire and diode)"""
return self._is_inversed is True
@property
def power_percent(self) -> float | None:
"""Get the current on_percent value"""
if self._prop_algorithm:
return round(self._prop_algorithm.on_percent * 100, 0)
else:
return None
@overrides
def post_init(self, config_entry: ConfigData):
"""Initialize the Thermostat"""

View File

@@ -1086,17 +1086,10 @@ class UnderlyingValveRegulation(UnderlyingValve):
)
return
# Caclulate percent_open
if self._percent_open >= 1:
self._percent_open = round(
self._min_opening_degree
+ (self._percent_open
* (100 - self._min_opening_degree) / 100)
)
else:
self._percent_open = 0
# Send opening_degree
if 0 < self._percent_open < self._min_opening_degree:
self._percent_open = self._min_opening_degree
await super().send_percent_open()
# Send closing_degree if set

View File

@@ -188,7 +188,7 @@ class VersatileThermostatAPI(dict):
# start listening for the central power manager if not only one vtherm reload
if not entry_id:
await self.central_power_manager.start_listening()
self.central_power_manager.start_listening()
async def init_vtherm_preset_with_central(self):
"""Init all VTherm presets when the VTherm uses central temperature"""

View File

@@ -49,7 +49,7 @@ When your device is controlled by a `climate` entity in Home Assistant and you o
This type also includes advanced self-regulation features to adjust the setpoint sent to the underlying device, helping to achieve the target temperature faster and mitigating poor internal regulation. For example, if the device's internal thermometer is too close to the heating element, it may incorrectly assume the room is warm while the setpoint is far from being achieved in other areas.
Since version 6.8, this VTherm type can also regulate directly by controlling the valve. Ideal for controllable TRVs, as Sonoff TRVZB, this type is recommended if you have such devices.
Since version 6.8, this VTherm type can also regulate directly by controlling the valve. Ideal for controllable TRVs, this type is recommended if you have such devices.
The underlying entities for this VTherm type are exclusively `climate`.

View File

@@ -33,8 +33,6 @@ Once the function is configured, you will now have a new `switch` type entity th
Check the box to allow auto-start and auto-stop, and leave it unchecked to disable the feature.
Note: The auto-start/stop function will only turn a _VTherm_ back on if it was turned off by this function. This prevents unwanted or unexpected activations. Naturally, the off state is preserved even after a Home Assistant restart.
> ![Tip](images/tips.png) _*Notes*_
> 1. The detection algorithm is described [here](algorithms.md#auto-startstop-algorithm).
> 2. Some appliances (boilers, underfloor heating, _PAC_, etc.) may not like being started/stopped too frequently. If that's the case, it might be better to disable the function when you know the appliance will be used. For example, I disable this feature during the day when presence is detected because I know my _PAC_ will turn on often. I enable auto-start/stop at night or when no one is home, as the setpoint is lowered and it rarely triggers.

View File

@@ -1,50 +1,46 @@
# Power Management - Load Shedding
- [Power Management - Load Shedding](#power-management---load-shedding)
- [Example Use Case:](#example-use-case)
- [Configuring Power Management](#configuring-power-management)
- [Configure Power Management](#configure-power-management)
This feature allows you to regulate the electrical consumption of your heaters. Known as load shedding, it lets you limit the electrical consumption of your heating equipment if overconsumption conditions are detected.
You will need a **sensor for the total instantaneous power consumption** of your home and a **sensor for the maximum allowed power**.
This feature allows you to regulate the electricity consumption of your heaters. Known as load shedding, this feature enables you to limit the electrical consumption of your heating device if overcapacity conditions are detected.
You will need a **sensor for the total instantaneous power consumption** of your home, as well as a **sensor for the maximum allowed power**.
The behavior of this feature is as follows:
1. When a new measurement of the home's power consumption or the maximum allowed power is received,
2. If the maximum power is exceeded, the central command will shed the load of all active devices starting with those closest to the setpoint. This continues until enough _VTherms_ are shed,
3. If there is available power reserve and some _VTherms_ are shed, the central command will re-enable as many devices as possible, starting with those furthest from the setpoint (at the time they were shed).
The behavior of this feature is basic:
1. when the _VTherm_ is about to turn on a device,
2. it compares the last known value of the power consumption sensor with the last value of the maximum allowed power. If there is a remaining margin greater than or equal to the declared power of the _VTherm_'s devices, then the _VTherm_ and its devices will be turned on. Otherwise, they will remain off until the next cycle.
**WARNING:** This is **not a safety feature** but an optimization function to manage consumption at the expense of some heating degradation. Overconsumption is still possible depending on the frequency of your consumption sensor updates and the actual power used by your equipment. Always maintain a safety margin.
WARNING: This very basic operation **is not a safety function** but more of an optimization feature to manage consumption at the cost of heating performance. Overloads may occur depending on the frequency of updates from your consumption sensors, and the actual power used by your devices. Therefore, you must always maintain a safety margin.
### Example Use Case:
1. You have an electric meter limited to 11 kW,
2. You occasionally charge an electric vehicle at 5 kW,
3. This leaves 6 kW for everything else, including heating,
4. You have 1 kW of other active devices,
5. You declare a sensor (`input_number`) for the maximum allowed power at 9 kW (= 11 kW - reserved power for other devices - safety margin).
Typical use case:
1. you have an electricity meter limited to 11 kW,
2. you occasionally charge an electric vehicle at 5 kW,
3. that leaves 6 kW for everything else, including heating,
4. you have 1 kW of other equipment running,
5. you have declared a sensor (`input_number`) for the maximum allowed power at 9 kW (= 11 kW - the reserve for other devices - margin)
If the vehicle is charging, the total consumed power is 6 kW (5 + 1), and a _VTherm_ will only turn on if its declared power is a maximum of 3 kW (9 kW - 6 kW).
If the vehicle is charging and another _VTherm_ of 2 kW is on, the total consumed power is 8 kW (5 + 1 + 2), and a _VTherm_ will only turn on if its declared power is a maximum of 1 kW (9 kW - 8 kW). Otherwise, it will skip its turn (cycle).
If the vehicle is not charging, the total consumed power is 1 kW, and a _VTherm_ will only turn on if its declared power is a maximum of 8 kW (9 kW - 1 kW).
If the vehicle is charging, the total power consumed is 6 kW (5+1), and a _VTherm_ will only turn on if its declared power is 3 kW max (9 kW - 6 kW).
If the vehicle is charging and another _VTherm_ of 2 kW is running, the total power consumed is 8 kW (5+1+2), and a _VTherm_ will only turn on if its declared power is 1 kW max (9 kW - 8 kW). Otherwise, it will wait until the next cycle.
## Configuring Power Management
If the vehicle is not charging, the total power consumed is 1 kW, and a _VTherm_ will only turn on if its declared power is 8 kW max (9 kW - 1 kW).
In the centralized configuration, if you have selected the `With power detection` feature, configure it as follows:
## Configure Power Management
If you have chosen the `With power detection` feature, configure it as follows:
![image](images/config-power.png)
1. The entity ID of the **sensor for total instantaneous power consumption** of your home,
2. The entity ID of the **sensor for maximum allowed power**,
3. The temperature to apply if load shedding is activated.
1. the entity ID of the **instantaneous power consumption sensor** for your home,
2. the entity ID of the **maximum allowed power sensor**,
3. the temperature to apply if load shedding is activated.
Ensure that all power values use the same units (e.g., kW or W).
Having a **sensor for maximum allowed power** allows you to modify the maximum power dynamically using a scheduler or automation.
Note that due to centralized load-shedding, it is not possible to override the consumption and maximum consumption sensors on individual _VTherms_. This configuration must be done in the centralized settings. See [Centralized Configuration](./creation.md#centralized-configuration).
Note that all power values must have the same units (kW or W, for example).
Having a **maximum allowed power sensor** allows you to adjust the maximum power over time using a scheduler or automation.
> ![Tip](images/tips.png) _*Notes*_
>
> 1. During load shedding, the heater is set to the preset named `power`. This is a hidden preset that cannot be manually selected.
> 2. Always maintain a margin, as the maximum power can briefly be exceeded while waiting for the next cycle's calculation or due to uncontrolled devices.
> 3. If you do not wish to use this feature, uncheck it in the 'Features' menu.
> 4. If a single _VTherm_ controls multiple devices, the **declared heating power consumption** should correspond to the total power of all devices.
> 5. If you use the Versatile Thermostat UI card (see [here](additions.md#better-with-the-versatile-thermostat-ui-card)), load shedding is represented as follows: ![load shedding](images/power-exceeded-icon.png).
> 6. There may be a delay of up to 20 seconds between receiving a new value from the power consumption sensor and triggering load shedding for _VTherms_. This delay prevents overloading Home Assistant if your consumption updates are very frequent.
> 1. In case of load shedding, the radiator is set to the preset named `power`. This is a hidden preset, and you cannot select it manually.
> 2. Always keep a margin, as the maximum power may briefly be exceeded while waiting for the next cycle calculation, or due to unregulated equipment.
> 3. If you don't want to use this feature, uncheck it in the 'Functions' menu.
> 4. If a _VTherm_ controls multiple devices, the **electrical consumption of your heating** must match the sum of the powers.
> 5. If you are using the Versatile Thermostat UI card (see [here](additions.md#much-better-with-the-versatile-thermostat-ui-card)), load shedding is represented as follows: ![load shedding](images/power-exceeded-icon.png).

View File

@@ -5,7 +5,7 @@
## Configure Pre-configured Temperatures
The preset mode allows you to pre-configure the target temperature. Used in conjunction with Scheduler (see [scheduler](additions.md#the-scheduler-component)), you'll have a powerful and simple way to optimize the temperature relative to the electricity consumption in your home. The managed presets are as follows:
The preset mode allows you to pre-configure the target temperature. Used in conjunction with Scheduler (see [scheduler](additions#the-scheduler-component)), you'll have a powerful and simple way to optimize the temperature relative to the electricity consumption in your home. The managed presets are as follows:
- **Eco**: the device is in energy-saving mode
- **Comfort**: the device is in comfort mode
- **Boost**: the device fully opens all valves

1676
documentation/en/one-page.md Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -2,9 +2,6 @@
![New](images/new-icon.png)
> * **Release 7.1**:
> - Redesign of the load-shedding function (power management). Load-shedding is now handled centrally (previously, each _VTherm_ was autonomous). This allows for much more efficient management and prioritization of load-shedding on devices that are close to the setpoint. Note that you must have a centralized configuration with power management enabled for this to work. More info [here](./feature-power.md).
> * **Release 6.8**:
> - Added a new regulation method for `over_climate` type Versatile Thermostats. This method, called 'Direct Valve Control', allows direct control of a TRV valve and possibly an offset to calibrate the internal thermometer of your TRV. This new method has been tested with Sonoff TRVZB and extended to other TRV types where the valve can be directly controlled via `number` entities. More information [here](over-climate.md#lauto-régulation) and [here](self-regulation.md#auto-régulation-par-contrôle-direct-de-la-vanne).

View File

@@ -38,8 +38,6 @@ The opening rate calculation algorithm is based on the _TPI_ algorithm described
If a valve closure rate entity is configured, it will be set to 100 minus the opening rate to force the valve into a particular state.
Note: for Sonoff TRVZB you should not configure the "closing degree" parameter. This leads to a bug in the TRV and the `hvac_action` is no more working.
### Other self-regulation
In the second case, Versatile Thermostat calculates an offset based on the following information:

View File

@@ -50,7 +50,7 @@ Les entités sous-jacentes sont donc des `switchs` ou des `input_boolean`.
Lorsque votre équipement est contrôlé par une entité de type `climate` dans Home Assistant et que vous n'avez que ça à disposition, vous devez utiliser ce type de VTherm. Dans ce cas, le VTherm va simplement commander la température de consigne du `climate` sous-jacent.
Ce type est aussi équipé de fonction d' auto-régulations avancées permettant de moduler la consigne donnée aux sous-jacent pour atteindre plus vite la consigne et de s'affranchir de la régulation interne de ces équipements qui est parfois mauvaise. C'est le cas, si le thermomètre interne de l'équipement est trop proche du corps de chauffe. L'équipement peut croire qu'il fait chaud alors qu'au bout de la pièce, la consigne n'est pas du tout atteinte.
Depuis la version 6.8, ce type de VTherm permet aussi de réguler avec une action directe sur la vanne. Idéal pour les _TRV_ pour lesquels la vanne est commandable, comme les Sonoff TRVZB, ce type est recommandé si vous êtes équipés.
Depuis la version 6.8, ce type de VTherm permet aussi de réguler avec une action directe sur la vanne. Idéal pour les _TRV_ pour lesquels la vanne est commandable, ce type est recommandé si vous êtes équipés.
Les entités sous-jacentes de ce type de VTherm sont donc des `climate` exclusivement.

View File

@@ -34,8 +34,6 @@ Une fois la fonction paramétrée, vous aurez maintenant une nouvelle entité de
Cochez la pour autoriser le démarrage et extinction automatique et laissez là décocher si vous voulez désactiver la fonction auto-start/stop.
A noter : la fonction auto-start/stop ne rallumera un _VTherm_ que si celui-ci a été éteint par cette fonction. Ca évite des allumages intempestifs non désirés. Evidement l'état d'extinction est résistant à un redémarrage de Home Assistant.
> ![Astuce](images/tips.png) _*Notes*_
> 1. L'algorithme de détection est décrit [ici](algorithms.md#lalgorithme-de-la-fonction-dauto-startstop).

View File

@@ -6,12 +6,11 @@
Cette fonction vous permet de réguler la consommation électrique de vos radiateurs. Connue sous le nom de délestage, cette fonction vous permet de limiter la consommation électrique de votre appareil de chauffage si des conditions de surpuissance sont détectées.
Vous aurez besoin d'un **capteur de la puissance totale instantanée consommée** de votre logement ainsi que d'un **capteur donnant la puissance maximale autorisée**.
Le comportement de cette fonction est le suivant :
1. lorsqu'une nouvelle mesure de la puissance consommée du logement ou de la puissance maximale autorisée est reçue,
2. si la puissance max est dépassée, la commande centrale va mettre en délestage tous les équipements actifs en commençant par ceux qui sont le plus près de la consigne. Il fait ça jusqu'à ce que suffisament de _VTherm_ soient délestés,
3. si une réserve de puissance est disponible et que des _VTherms_ sont délestés, alors la commande centrale va délester autant d'équipements que possible en commençant par les plus loin de la consigne (au moment où il a été mis en délestage),
Le comportement de cette fonction est basique :
1. lorsque le _VTherm_ va allumer un équipement,
2. il compare la dernière valeur connue du capteur de puissance consommée avec la dernière valeur de la puissance maximale autorisée. Si il reste une marge supérieure égale à la puissance déclarée des équipements du _VTherm_ alors le VTherm et ses équipements seront allumés. Sinon ils resteront éteints jusqu'au prochain cycle.
ATTENTION: ce fonctionnement **n'est pas une fonction de sécurité** mais plus une fonction permettant une optimisation de la consommation au prix d'une dégradation du chauffage. Des dépassements sont possibles selon la fréquence de remontée de vos capteurs de consommation, la puissance réellement utilisée par votre équipements. Vous devez donc toujours garder une marge de sécurité.
ATTENTION: ce fonctionnement très basique **n'est pas une fonction de sécurité** mais plus une fonction permettant une optimisation de la consommation au prix d'une dégradation du chauffage. Des dépassements sont possibles selon la fréquence de remontée de vos capteurs de consommation, la puissance réellement utilisée par votre équipements. Vous devez donc toujours garder une marge de sécurité.
Cas d'usage type:
1. vous avez un compteur électrique limité à 11 kW,
@@ -27,7 +26,7 @@ Si le vehicle n'est pas en charge, la puissance totale consommé est de 1 kW, un
## Configurer la gestion de la puissance
Dans la configuration centralisée, si vous avez choisi la fonctionnalité `Avec détection de la puissance`, vous la configurez de la façon suivante :
Si vous avez choisi la fonctionnalité `Avec détection de la puissance`, vous la configurez de la façon suivante :
![image](images/config-power.png)
@@ -38,13 +37,10 @@ Dans la configuration centralisée, si vous avez choisi la fonctionnalité `Avec
Notez que toutes les valeurs de puissance doivent avoir les mêmes unités (kW ou W par exemple).
Le fait d'avoir un **capteur de puissance maximale autorisée**, vous permet de modifier la puissance maximale au fil du temps à l'aide d'un planificateur ou d'une automatisation.
A noter, dû à la centralisation du délestage, il n'est pas possible de sur-charger les capteurs de consommation et de consommation maximale sur les _VTherms_. Cette configuration se fait forcément dans la configuration centralisée. Cf. [Configuration centralisée](./creation.md#configuration-centralisée)
> ![Astuce](images/tips.png) _*Notes*_
>
> 1. En cas de délestage, le radiateur est réglé sur le préréglage nommé `power`. Il s'agit d'un préréglage caché, vous ne pouvez pas le sélectionner manuellement.
> 2. Gardez toujours une marge, car la puissance max peut être brièvement dépassée en attendant le calcul du prochain cycle typiquement ou par des équipements non régulés.
> 3. Si vous ne souhaitez pas utiliser cette fonctionnalité, décochez la dans le menu 'Fonctions'.
> 4. Si une _VTherm_ controlez plusieurs équipements, la **consommation électrique de votre chauffage** renseigné doit correspondre à la somme des puissances.
> 5. Si vous utilisez la carte Verstatile Thermostat UI (cf. [ici](additions.md#bien-mieux-avec-le-versatile-thermostat-ui-card)), le délestage est représenté comme suit : ![délestage](images/power-exceeded-icon.png),
> 6. Un délai pouvant aller jusqu'à 20 sec est possible entre la réception d'une nouvelle valeur du capteur de puissance consommée et la mise en délestage de _VTherm_. Ce délai évite de trop solliciter Home Assistant si vous avez des remontées rapides de votre puissance consommée.
> 5. Si vous utilisez la carte Verstatile Thermostat UI (cf. [ici](additions.md#bien-mieux-avec-le-versatile-thermostat-ui-card)), le délestage est représenté comme suit : ![délestage](images/power-exceeded-icon.png).

View File

@@ -6,7 +6,7 @@
## Configurer les températures préréglées
Le mode préréglé (preset) vous permet de préconfigurer la température ciblée. Utilisé en conjonction avec Scheduler (voir [scheduler](additions.md#composant-scheduler-)) vous aurez un moyen puissant et simple d'optimiser la température par rapport à la consommation électrique de votre maison. Les préréglages gérés sont les suivants :
Le mode préréglé (preset) vous permet de préconfigurer la température ciblée. Utilisé en conjonction avec Scheduler (voir [scheduler](additions#composant-scheduler-)) vous aurez un moyen puissant et simple d'optimiser la température par rapport à la consommation électrique de votre maison. Les préréglages gérés sont les suivants :
- **Eco** : l'appareil est en mode d'économie d'énergie
- **Confort** : l'appareil est en mode confort
- **Boost** : l'appareil tourne toutes les vannes à fond

1676
documentation/fr/one-page.md Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -18,7 +18,7 @@ Ce composant nommé __Versatile thermostat__ gère les cas d'utilisation suivant
- Configuration via l'interface graphique d'intégration standard (à l'aide du flux Config Entry),
- Utilisations complètes du **mode préréglages**,
- Désactiver le mode préréglé lorsque la température est **définie manuellement** sur un thermostat,
- Éteindre/allumer un thermostat ou changer de preset lorsqu'une **porte ou des fenêtres sont ouvertes/fermées** après un certain délai,
- Éteindre/allumer un thermostat ou chager de preset lorsqu'une **porte ou des fenêtres sont ouvertes/fermées** après un certain délai,
- Changer de preset lorsqu'une **activité est détectée** ou non dans une pièce pendant un temps défini,
- Utiliser un algorithme **TPI (Time Proportional Interval)** grâce à l'algorithme [[Argonaute](https://forum.hacf.fr/u/argonaute/summary)] ,
- Ajouter une **gestion de délestage** ou une régulation pour ne pas dépasser une puissance totale définie. Lorsque la puissance maximale est dépassée, un préréglage caché de « puissance » est défini sur l'entité climatique. Lorsque la puissance passe en dessous du maximum, le préréglage précédent est restauré.

View File

@@ -2,9 +2,6 @@
![Nouveau](images/new-icon.png)
> * **Release 7.1**:
> - Refonte de la fonction de délestage (gestion de la puissance). Le délestage est maintenant géré de façon centralisé (auparavent chaque _VTherm_ était autonome). Cela permet une gestion bien plus efficace et de prioriser le délestage sur les équipements qui sont proches de la consigne. Attention, vous devez impérativement avoir une configuration centralisée avec gestion de la puissance pour que cela fonctionne. Plus d'infos [ici](./feature-power.md)
> * **Release 6.8**:
> - 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`. Plus d'informations [ici](over-climate.md#lauto-régulation) et [ici](self-regulation.md#auto-régulation-par-contrôle-direct-de-la-vanne).

View File

@@ -39,8 +39,6 @@ L'algorithme de calcul du taux d'ouverture est basé sur le _TPI_ qui est décri
Si une entité de type taux de fermeture de la vanne est configurée, il sera positionné avec la valeur 100 - taux d'ouverture pour forcer la vanne dans un état.
Note: pour les Sonoff TRVZB, vous ne devez pas configurer les "closing degree". Cela rend inopérant le `hvac_action` qui est utilisé par _VTherm_ et qui indique que l'équipement est en chauffe.
### autres auto-régulation
Dans ce deuxième cas, le Versatile Thermostat calcule un décalage basé sur les informations suivantes :

View File

@@ -1 +1 @@
homeassistant==2024.12.4
homeassistant==2024.12.3

View File

@@ -59,6 +59,8 @@ from .const import ( # pylint: disable=unused-import
MOCK_TH_OVER_CLIMATE_TYPE_AC_CONFIG,
MOCK_TH_OVER_CLIMATE_TYPE_NOT_REGULATED_CONFIG,
MOCK_TH_OVER_SWITCH_TPI_CONFIG,
MOCK_PRESETS_CONFIG,
MOCK_PRESETS_AC_CONFIG,
MOCK_WINDOW_CONFIG,
MOCK_MOTION_CONFIG,
MOCK_POWER_CONFIG,
@@ -87,7 +89,7 @@ FULL_SWITCH_CONFIG = (
| MOCK_TH_OVER_SWITCH_CENTRAL_MAIN_CONFIG
| MOCK_TH_OVER_SWITCH_TYPE_CONFIG
| MOCK_TH_OVER_SWITCH_TPI_CONFIG
# | MOCK_PRESETS_CONFIG
| MOCK_PRESETS_CONFIG
| MOCK_FULL_FEATURES
| MOCK_WINDOW_CONFIG
| MOCK_MOTION_CONFIG
@@ -102,6 +104,7 @@ FULL_SWITCH_AC_CONFIG = (
| MOCK_TH_OVER_SWITCH_CENTRAL_MAIN_CONFIG
| MOCK_TH_OVER_SWITCH_AC_TYPE_CONFIG
| MOCK_TH_OVER_SWITCH_TPI_CONFIG
| MOCK_PRESETS_AC_CONFIG
| MOCK_FULL_FEATURES
| MOCK_WINDOW_CONFIG
| MOCK_MOTION_CONFIG
@@ -115,7 +118,7 @@ PARTIAL_CLIMATE_CONFIG = (
| MOCK_TH_OVER_CLIMATE_MAIN_CONFIG
| MOCK_TH_OVER_CLIMATE_CENTRAL_MAIN_CONFIG
| MOCK_TH_OVER_CLIMATE_TYPE_CONFIG
# | MOCK_PRESETS_CONFIG
| MOCK_PRESETS_CONFIG
| MOCK_ADVANCED_CONFIG
)
@@ -124,7 +127,7 @@ PARTIAL_CLIMATE_CONFIG_USE_DEVICE_TEMP = (
| MOCK_TH_OVER_CLIMATE_MAIN_CONFIG
| MOCK_TH_OVER_CLIMATE_CENTRAL_MAIN_CONFIG
| MOCK_TH_OVER_CLIMATE_TYPE_USE_DEVICE_TEMP_CONFIG
# | MOCK_PRESETS_CONFIG
| MOCK_PRESETS_CONFIG
| MOCK_ADVANCED_CONFIG
)
@@ -133,7 +136,7 @@ PARTIAL_CLIMATE_NOT_REGULATED_CONFIG = (
| MOCK_TH_OVER_CLIMATE_MAIN_CONFIG
| MOCK_TH_OVER_CLIMATE_CENTRAL_MAIN_CONFIG
| MOCK_TH_OVER_CLIMATE_TYPE_NOT_REGULATED_CONFIG
# | MOCK_PRESETS_CONFIG
| MOCK_PRESETS_CONFIG
| MOCK_ADVANCED_CONFIG
)
@@ -142,7 +145,7 @@ PARTIAL_CLIMATE_AC_CONFIG = (
| MOCK_TH_OVER_CLIMATE_TYPE_AC_CONFIG
| MOCK_TH_OVER_CLIMATE_MAIN_CONFIG
| MOCK_TH_OVER_CLIMATE_CENTRAL_MAIN_CONFIG
# | MOCK_PRESETS_CONFIG
| MOCK_PRESETS_CONFIG
| MOCK_ADVANCED_CONFIG
)
@@ -150,7 +153,7 @@ FULL_4SWITCH_CONFIG = (
MOCK_TH_OVER_4SWITCH_USER_CONFIG
| MOCK_TH_OVER_4SWITCH_TYPE_CONFIG
| MOCK_TH_OVER_SWITCH_TPI_CONFIG
# | MOCK_PRESETS_CONFIG
| MOCK_PRESETS_CONFIG
| MOCK_WINDOW_CONFIG
| MOCK_MOTION_CONFIG
| MOCK_POWER_CONFIG
@@ -748,7 +751,6 @@ async def send_power_change_event(entity: BaseThermostat, new_power, date, sleep
)
vtherm_api = VersatileThermostatAPI.get_vtherm_api()
await vtherm_api.central_power_manager._power_sensor_changed(power_event)
await vtherm_api.central_power_manager._do_immediate_shedding()
if sleep:
await entity.hass.async_block_till_done()
@@ -776,7 +778,6 @@ async def send_max_power_change_event(
)
vtherm_api = VersatileThermostatAPI.get_vtherm_api()
await vtherm_api.central_power_manager._max_power_sensor_changed(power_event)
await vtherm_api.central_power_manager._do_immediate_shedding()
if sleep:
await entity.hass.async_block_till_done()

View File

@@ -140,6 +140,25 @@ MOCK_TH_OVER_CLIMATE_TYPE_AC_CONFIG = {
CONF_AUTO_REGULATION_PERIOD_MIN: 1,
}
# TODO remove this later
MOCK_PRESETS_CONFIG = {
PRESET_FROST_PROTECTION + PRESET_TEMP_SUFFIX: 7,
PRESET_ECO + PRESET_TEMP_SUFFIX: 16,
PRESET_COMFORT + PRESET_TEMP_SUFFIX: 17,
PRESET_BOOST + PRESET_TEMP_SUFFIX: 18,
}
# TODO remove this later
MOCK_PRESETS_AC_CONFIG = {
PRESET_FROST_PROTECTION + PRESET_TEMP_SUFFIX: 7,
PRESET_ECO + PRESET_TEMP_SUFFIX: 17,
PRESET_COMFORT + PRESET_TEMP_SUFFIX: 19,
PRESET_BOOST + PRESET_TEMP_SUFFIX: 20,
PRESET_ECO + PRESET_AC_SUFFIX + PRESET_TEMP_SUFFIX: 25,
PRESET_COMFORT + PRESET_AC_SUFFIX + PRESET_TEMP_SUFFIX: 23,
PRESET_BOOST + PRESET_AC_SUFFIX + PRESET_TEMP_SUFFIX: 21,
}
MOCK_WINDOW_CONFIG = {
CONF_WINDOW_SENSOR: "binary_sensor.window_sensor",
# Not used normally only for tests to avoid rewrite all tests
@@ -165,16 +184,12 @@ MOCK_MOTION_CONFIG = {
CONF_NO_MOTION_PRESET: PRESET_ECO,
}
MOCK_CENTRAL_POWER_CONFIG = {
MOCK_POWER_CONFIG = {
CONF_POWER_SENSOR: "sensor.power_sensor",
CONF_MAX_POWER_SENSOR: "sensor.power_max_sensor",
CONF_PRESET_POWER: 10,
}
MOCK_POWER_CONFIG = {
CONF_PRESET_POWER: 10,
}
MOCK_PRESENCE_CONFIG = {
CONF_PRESENCE_SENSOR: "person.presence_sensor",
}

View File

@@ -172,17 +172,16 @@ async def test_overpowering_binary_sensors(
# Send power mesurement
side_effects = SideEffects(
{
"sensor.the_power_sensor": State("sensor.the_power_sensor", 150),
"sensor.the_max_power_sensor": State("sensor.the_max_power_sensor", 100),
"sensor.the_power_sensor": State("sensor.the_power_sensor", 100),
"sensor.the_max_power_sensor": State("sensor.the_max_power_sensor", 150),
},
State("unknown.entity_id", "unknown"),
)
# 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", return_value="True"):
with patch("homeassistant.core.StateMachine.get", side_effect=side_effects.get_side_effects()):
# fmt: on
await send_power_change_event(entity, 150, now)
await send_max_power_change_event(entity, 100, now)
await send_power_change_event(entity, 100, now)
await send_max_power_change_event(entity, 150, now)
assert entity.power_manager.is_overpowering_detected is True
assert entity.power_manager.overpowering_state is STATE_ON
@@ -192,13 +191,13 @@ async def test_overpowering_binary_sensors(
assert overpowering_binary_sensor.state == STATE_ON
# set max power to a low value
side_effects.add_or_update_side_effect("sensor.the_max_power_sensor", State("sensor.the_max_power_sensor", 251))
side_effects.add_or_update_side_effect("sensor.the_max_power_sensor", State("sensor.the_max_power_sensor", 201))
# fmt:off
with patch("homeassistant.core.StateMachine.get", side_effect=side_effects.get_side_effects()):
# fmt: on
now = now + timedelta(seconds=30)
VersatileThermostatAPI.get_vtherm_api()._set_now(now)
await send_max_power_change_event(entity, 251, now)
await send_max_power_change_event(entity, 201, now)
assert entity.power_manager.is_overpowering_detected is False
assert entity.power_manager.overpowering_state is STATE_OFF
# Simulate the event reception

View File

@@ -348,7 +348,7 @@ async def test_bug_407(
await entity.async_set_preset_mode(PRESET_COMFORT)
assert entity.hvac_mode is HVACMode.HEAT
assert entity.preset_mode is PRESET_COMFORT
assert entity.power_manager.overpowering_state is STATE_UNKNOWN
assert entity.power_manager.overpowering_state is STATE_OFF
assert entity.target_temperature == 18
# waits that the heater starts
await hass.async_block_till_done()
@@ -398,8 +398,7 @@ async def test_bug_407(
assert entity.target_temperature == 19
assert mock_service_call.call_count >= 1
# 3. if heater is stopped (is_device_active==False) and power is over max, then overpowering should be started
side_effects.add_or_update_side_effect("sensor.the_power_sensor", State("sensor.the_power_sensor", 150))
# 3. if heater is stopped (is_device_active==False), then overpowering should be started
with patch(
"homeassistant.core.ServiceRegistry.async_call"
) as mock_service_call, patch(
@@ -421,10 +420,10 @@ async def test_bug_407(
# simulate a refresh for central power (not necessary)
await do_central_power_refresh(hass)
assert entity.power_manager.is_overpowering_detected is False
assert entity.power_manager.is_overpowering_detected is True
assert entity.hvac_mode is HVACMode.HEAT
assert entity.preset_mode is PRESET_COMFORT
assert entity.power_manager.overpowering_state is STATE_OFF
assert entity.preset_mode is PRESET_POWER
assert entity.power_manager.overpowering_state is STATE_ON
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@@ -481,6 +480,8 @@ async def test_bug_500_3(hass: HomeAssistant, init_vtherm_api) -> None:
CONF_USE_WINDOW_CENTRAL_CONFIG: False,
CONF_WINDOW_SENSOR: "sensor.theWindowSensor",
CONF_USE_POWER_CENTRAL_CONFIG: False,
CONF_POWER_SENSOR: "sensor.thePowerSensor",
CONF_MAX_POWER_SENSOR: "sensor.theMaxPowerSensor",
CONF_USE_PRESENCE_CENTRAL_CONFIG: False,
CONF_PRESENCE_SENSOR: "sensor.thePresenceSensor",
CONF_USE_MOTION_FEATURE: True, # motion sensor need to be checked AND a motion sensor set
@@ -490,7 +491,7 @@ async def test_bug_500_3(hass: HomeAssistant, init_vtherm_api) -> None:
flow = VersatileThermostatBaseConfigFlow(config)
assert flow._infos[CONF_USE_WINDOW_FEATURE] is True
assert flow._infos[CONF_USE_POWER_FEATURE] is False
assert flow._infos[CONF_USE_POWER_FEATURE] is True
assert flow._infos[CONF_USE_PRESENCE_FEATURE] is True
assert flow._infos[CONF_USE_MOTION_FEATURE] is True

View File

@@ -59,7 +59,7 @@ async def test_central_power_manager_init(
assert central_power_manager.power_temperature == power_temp
# 3. start listening
await central_power_manager.start_listening()
central_power_manager.start_listening()
assert len(central_power_manager._active_listener) == (2 if is_configured else 0)
# 4. stop listening
@@ -273,7 +273,7 @@ async def test_central_power_manageer_find_vtherms(
@pytest.mark.parametrize(
"current_power, current_max_power, vtherm_configs, expected_results",
[
# simple nominal test (initialize overpowering state in VTherm)
# simple nominal test (no shedding)
(
1000,
5000,
@@ -286,80 +286,139 @@ async def test_central_power_manageer_find_vtherms(
"nb_underlying_entities": 1,
"on_percent": 0,
"is_overpowering_detected": False,
"overpowering_state": STATE_UNKNOWN,
},
{
"name": "vtherm2",
"device_power": 10000,
"is_device_active": True,
"is_over_climate": False,
"nb_underlying_entities": 4,
"on_percent": 100,
"is_overpowering_detected": False,
"overpowering_state": STATE_UNKNOWN,
},
{
"name": "vtherm3",
"device_power": 5000,
"is_device_active": True,
"is_over_climate": True,
"is_overpowering_detected": False,
"overpowering_state": STATE_UNKNOWN,
},
{"name": "vtherm4", "device_power": 1000, "is_device_active": True, "is_over_climate": True, "is_overpowering_detected": False, "overpowering_state": STATE_OFF},
],
# init vtherm1 to False
{"vtherm3": False, "vtherm2": False, "vtherm1": False},
{"vtherm1": False},
),
# Un-shedding only (will be taken in reverse order)
# Simple trivial shedding
(
1000,
2000,
[
# should be not unshedded (too much power will be added)
# should be overpowering
{
"name": "vtherm1",
"device_power": 1100,
"is_device_active": False,
"is_over_climate": False,
"nb_underlying_entities": 1,
"on_percent": 1,
"is_overpowering_detected": False,
},
# should be overpowering with many underlmying entities
{
"name": "vtherm2",
"device_power": 4000,
"is_device_active": False,
"is_over_climate": False,
"nb_underlying_entities": 4,
"on_percent": 0.1,
"is_overpowering_detected": False,
},
# over_climate should be overpowering
{
"name": "vtherm3",
"device_power": 1000,
"is_device_active": False,
"is_over_climate": True,
"is_overpowering_detected": False,
},
# should pass but because will be also overpowering because previous was overpowering
{
"name": "vtherm4",
"device_power": 800,
"is_device_active": False,
"is_over_climate": False,
"nb_underlying_entities": 1,
"on_percent": 1,
"is_overpowering_detected": False,
},
],
{"vtherm1": True, "vtherm2": True, "vtherm3": True, "vtherm4": True},
),
# More complex shedding
(
1000,
2000,
[
# already overpowering (non change)
{
"name": "vtherm1",
"device_power": 1100,
"is_device_active": False,
"is_over_climate": False,
"nb_underlying_entities": 1,
"on_percent": 1,
"is_overpowering_detected": True,
},
# already overpowering and already active (can be un overpowered)
{
"name": "vtherm2",
"device_power": 1100,
"is_device_active": True,
"is_over_climate": True,
"is_overpowering_detected": True,
},
# should terminate the overpowering
{
"name": "vtherm3",
"device_power": 800,
"is_device_active": False,
"is_over_climate": False,
"nb_underlying_entities": 1,
"on_percent": 1,
"is_overpowering_detected": True,
"overpowering_state": STATE_ON,
},
# already stay unshedded cause already unshedded
{
"name": "vtherm2",
"device_power": 100,
"is_device_active": True,
"is_over_climate": True,
"is_overpowering_detected": False,
"overpowering_state": STATE_OFF,
},
# should be unshedded
{
"name": "vtherm3",
"device_power": 200,
"is_device_active": False,
"is_over_climate": True,
"is_overpowering_detected": True,
"overpowering_state": STATE_ON,
},
# should be unshedded
# should terminate the overpowering and active
{
"name": "vtherm4",
"device_power": 300,
"is_device_active": False,
"device_power": 3800,
"is_device_active": True,
"is_over_climate": False,
"nb_underlying_entities": 1,
"on_percent": 1,
"is_overpowering_detected": True,
"overpowering_state": STATE_ON,
},
],
{"vtherm4": False, "vtherm3": False},
{"vtherm2": False, "vtherm3": False, "vtherm4": False},
),
# Shedding
# More complex shedding
(
1000,
2000,
[
# already overpowering (non change)
{
"name": "vtherm1",
"device_power": 1100,
"is_device_active": True,
"is_over_climate": False,
"nb_underlying_entities": 1,
"on_percent": 1,
"is_overpowering_detected": True,
},
# should be overpowering
{
"name": "vtherm2",
"device_power": 1800,
"is_device_active": False,
"is_over_climate": True,
"is_overpowering_detected": False,
},
# should terminate the overpowering and active but just before is overpowering
{
"name": "vtherm3",
"device_power": 100,
"is_device_active": True,
"is_over_climate": False,
"nb_underlying_entities": 1,
"on_percent": 1,
"is_overpowering_detected": False,
},
],
{"vtherm1": False, "vtherm2": True, "vtherm3": True},
),
# Sheeding only current_power > max_power (need to gain 1000 )
(
2000,
1000,
@@ -373,31 +432,36 @@ async def test_central_power_manageer_find_vtherms(
"nb_underlying_entities": 1,
"on_percent": 1,
"is_overpowering_detected": False,
"overpowering_state": STATE_OFF,
},
# should be overpowering with many underlmying entities
# should be overpowering but is already
{
"name": "vtherm2",
"device_power": 400,
"device_power": 600,
"is_device_active": True,
"is_over_climate": False,
"nb_underlying_entities": 4,
"on_percent": 0.1,
"is_overpowering_detected": False,
"overpowering_state": STATE_UNKNOWN,
"is_overpowering_detected": True,
},
# over_climate should be overpowering
# over_climate should be not overpowering (device not active)
{
"name": "vtherm3",
"device_power": 100,
"device_power": 690,
"is_device_active": False,
"is_over_climate": True,
"is_overpowering_detected": False,
},
# over_climate should be overpowering (device active and not already overpowering)
{
"name": "vtherm4",
"device_power": 690,
"is_device_active": True,
"is_over_climate": True,
"is_overpowering_detected": False,
"overpowering_state": STATE_OFF,
},
# should pass cause not active
# should not overpower (keep as is)
{
"name": "vtherm4",
"name": "vtherm5",
"device_power": 800,
"is_device_active": False,
"is_over_climate": False,
@@ -405,39 +469,8 @@ async def test_central_power_manageer_find_vtherms(
"on_percent": 1,
"is_overpowering_detected": False,
},
# should be not overpowering (already overpowering)
{
"name": "vtherm5",
"device_power": 400,
"is_device_active": True,
"is_over_climate": False,
"nb_underlying_entities": 4,
"on_percent": 0.1,
"is_overpowering_detected": True,
"overpowering_state": STATE_ON,
},
# should be overpowering with many underluying entities
{
"name": "vtherm6",
"device_power": 400,
"is_device_active": True,
"is_over_climate": False,
"nb_underlying_entities": 4,
"on_percent": 0.1,
"is_overpowering_detected": False,
"overpowering_state": STATE_UNKNOWN,
},
# should not be overpowering (we have enough)
{
"name": "vtherm7",
"device_power": 1000,
"is_device_active": True,
"is_over_climate": True,
"is_overpowering_detected": False,
"overpowering_state": STATE_UNKNOWN,
},
],
{"vtherm1": True, "vtherm2": True, "vtherm3": True, "vtherm6": True},
{"vtherm1": True, "vtherm4": True},
),
],
)
@@ -468,10 +501,7 @@ async def test_central_power_manageer_calculate_shedding(
vtherm.nb_underlying_entities = vtherm_config.get("nb_underlying_entities")
if not vtherm_config.get("is_over_climate"):
vtherm.proportional_algorithm = MagicMock()
vtherm.on_percent = vtherm.proportional_algorithm.on_percent = vtherm_config.get("on_percent")
else:
vtherm.on_percent = None
vtherm.proportional_algorithm = None
vtherm.proportional_algorithm.on_percent = vtherm_config.get("on_percent")
vtherm.power_manager = MagicMock(spec=FeaturePowerManager)
vtherm.power_manager._vtherm = vtherm
@@ -480,7 +510,6 @@ async def test_central_power_manageer_calculate_shedding(
"is_overpowering_detected"
)
vtherm.power_manager.device_power = vtherm_config.get("device_power")
vtherm.power_manager.overpowering_state = vtherm_config.get("overpowering_state")
async def mock_set_overpowering(
overpowering, power_consumption_max=0, v=vtherm
@@ -542,7 +571,7 @@ async def test_central_power_manager_power_event(
assert central_power_manager.power_temperature == 13
# 3. start listening (not really useful but don't eat bread)
await central_power_manager.start_listening()
central_power_manager.start_listening()
assert len(central_power_manager._active_listener) == 2
now: datetime = NowClass.get_now(hass)
@@ -571,9 +600,6 @@ async def test_central_power_manager_power_event(
"old_state": State("sensor.power_entity_id", STATE_UNAVAILABLE),
}))
if nb_call > 0:
await central_power_manager._do_immediate_shedding()
expected_power = power if isinstance(power, (int, float)) else -999
assert central_power_manager.current_power == expected_power
assert mock_calculate_shedding.call_count == nb_call
@@ -595,11 +621,8 @@ async def test_central_power_manager_power_event(
"old_state": State("sensor.power_entity_id", STATE_UNAVAILABLE),
}))
if nb_call > 0:
await central_power_manager._do_immediate_shedding()
assert central_power_manager.current_power == expected_power
assert mock_calculate_shedding.call_count == nb_call
assert mock_calculate_shedding.call_count == (nb_call if dsecs >= 20 else 0)
@pytest.mark.parametrize(
@@ -640,7 +663,7 @@ async def test_central_power_manager_max_power_event(
assert central_power_manager.power_temperature == 13
# 3. start listening (not really useful but don't eat bread)
await central_power_manager.start_listening()
central_power_manager.start_listening()
assert len(central_power_manager._active_listener) == 2
now: datetime = NowClass.get_now(hass)
@@ -671,9 +694,6 @@ async def test_central_power_manager_max_power_event(
"old_state": State("sensor.max_power_entity_id", STATE_UNAVAILABLE),
}))
if nb_call > 0:
await central_power_manager._do_immediate_shedding()
expected_power = max_power if isinstance(max_power, (int, float)) else -999
assert central_power_manager.current_max_power == expected_power
assert mock_calculate_shedding.call_count == nb_call
@@ -695,8 +715,5 @@ async def test_central_power_manager_max_power_event(
"old_state": State("sensor.max_power_entity_id", STATE_UNAVAILABLE),
}))
if nb_call > 0:
await central_power_manager._do_immediate_shedding()
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 if dsecs >= 20 else 0)

View File

@@ -90,7 +90,7 @@ async def test_motion_feature_manager_refresh(
assert custom_attributes["motion_off_delay_sec"] == 30
# 3. start listening
await motion_manager.start_listening()
motion_manager.start_listening()
assert motion_manager.is_configured is True
assert motion_manager.motion_state == STATE_UNKNOWN
assert motion_manager.is_motion_detected is False
@@ -198,7 +198,7 @@ async def test_motion_feature_manager_event(
CONF_NO_MOTION_PRESET: PRESET_ECO,
}
)
await motion_manager.start_listening()
motion_manager.start_listening()
# 2. test _motion_sensor_changed with the parametrized
# fmt: off

View File

@@ -783,11 +783,6 @@ async def test_multiple_switch_power_management(
assert entity.power_manager.overpowering_state is STATE_UNKNOWN
assert entity.target_temperature == 19
# make the heater heats
await send_temperature_change_event(entity, 15, now)
await send_ext_temperature_change_event(entity, 1, now)
await hass.async_block_till_done()
# 1. Send power mesurement
side_effects = SideEffects(
{
@@ -812,20 +807,20 @@ async def test_multiple_switch_power_management(
assert entity.power_manager.overpowering_state is STATE_OFF
# 2. Send power max mesurement too low and HVACMode is on
side_effects.add_or_update_side_effect("sensor.the_max_power_sensor", State("sensor.the_max_power_sensor", 49))
side_effects.add_or_update_side_effect("sensor.the_max_power_sensor", State("sensor.the_max_power_sensor", 74))
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_off"
) as mock_heater_off:
#fmt: off
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_off") as mock_heater_off, \
patch("custom_components.versatile_thermostat.thermostat_switch.ThermostatOverSwitch.is_device_active", return_value="True"):
#fmt: on
now = now + timedelta(seconds=30)
VersatileThermostatAPI.get_vtherm_api()._set_now(now)
assert entity.power_percent > 0
# 100 of the device / 4 -> 25, current power 50 so max is 75
await send_max_power_change_event(entity, 49, datetime.now())
await send_max_power_change_event(entity, 74, datetime.now())
assert entity.power_manager.is_overpowering_detected is True
# All configuration is complete and power is > power_max we switch to POWER preset
assert entity.preset_mode is PRESET_POWER
@@ -842,8 +837,8 @@ async def test_multiple_switch_power_management(
"type": "start",
"current_power": 50,
"device_power": 100,
"current_max_power": 49,
"current_power_consumption": 100,
"current_max_power": 74,
"current_power_consumption": 25.0,
},
),
],
@@ -861,7 +856,7 @@ async def test_multiple_switch_power_management(
await entity.async_set_preset_mode(PRESET_ECO)
assert entity.preset_mode is PRESET_ECO
# No change cause temperature is very low
# No change
assert entity.power_manager.overpowering_state is STATE_ON
# 4. Send hugh power max mesurement to release overpowering

View File

@@ -212,13 +212,6 @@ async def test_underlying_change_follow(
tz = get_tz(hass) # pylint: disable=invalid-name
now: datetime = datetime.now(tz=tz)
temps = {
PRESET_FROST_PROTECTION: 7,
PRESET_ECO: 16,
PRESET_COMFORT: 17,
PRESET_BOOST: 18,
}
entry = MockConfigEntry(
domain=DOMAIN,
title="TheOverClimateMockName",
@@ -239,7 +232,7 @@ async def test_underlying_change_follow(
) as mock_find_climate, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.set_hvac_mode"
) as mock_underlying_set_hvac_mode:
entity = await create_thermostat(hass, entry, "climate.theoverclimatemockname", temps)
entity = await create_thermostat(hass, entry, "climate.theoverclimatemockname")
assert entity
assert entity.name == "TheOverClimateMockName"
@@ -361,13 +354,6 @@ async def test_underlying_change_not_follow(
tz = get_tz(hass) # pylint: disable=invalid-name
now: datetime = datetime.now(tz=tz)
temps = {
PRESET_FROST_PROTECTION: 7,
PRESET_ECO: 16,
PRESET_COMFORT: 17,
PRESET_BOOST: 18,
}
entry = MockConfigEntry(
domain=DOMAIN,
title="TheOverClimateMockName",
@@ -388,7 +374,7 @@ async def test_underlying_change_not_follow(
) as mock_find_climate, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.set_hvac_mode"
) as mock_underlying_set_hvac_mode:
entity = await create_thermostat(hass, entry, "climate.theoverclimatemockname", temps)
entity = await create_thermostat(hass, entry, "climate.theoverclimatemockname")
assert entity
@@ -740,13 +726,6 @@ async def test_ignore_temp_outside_minmax_range(
tz = get_tz(hass) # pylint: disable=invalid-name
now: datetime = datetime.now(tz=tz)
temps = {
PRESET_FROST_PROTECTION: 7,
PRESET_ECO: 16,
PRESET_COMFORT: 17,
PRESET_BOOST: 18,
}
entry = MockConfigEntry(
domain=DOMAIN,
title="TheOverClimateMockName",
@@ -767,7 +746,7 @@ async def test_ignore_temp_outside_minmax_range(
) as mock_find_climate, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.set_hvac_mode"
) as mock_underlying_set_hvac_mode:
entity = await create_thermostat(hass, entry, "climate.theoverclimatemockname", temps)
entity = await create_thermostat(hass, entry, "climate.theoverclimatemockname")
assert entity

View File

@@ -627,11 +627,11 @@ async def test_over_climate_valve_multi_min_opening_degrees(
assert mock_service_call.call_count == 6
mock_service_call.assert_has_calls([
# min is 60
call(domain='number', service='set_value', service_data={'value': 68}, target={'entity_id': 'number.mock_opening_degree1'}),
call(domain='number', service='set_value', service_data={'value': 32}, target={'entity_id': 'number.mock_closing_degree1'}),
call(domain='number', service='set_value', service_data={'value': 60}, target={'entity_id': 'number.mock_opening_degree1'}),
call(domain='number', service='set_value', service_data={'value': 40}, target={'entity_id': 'number.mock_closing_degree1'}),
call(domain='number', service='set_value', service_data={'value': 3.0}, target={'entity_id': 'number.mock_offset_calibration1'}),
call(domain='number', service='set_value', service_data={'value': 76}, target={'entity_id': 'number.mock_opening_degree2'}),
call(domain='number', service='set_value', service_data={'value': 24}, target={'entity_id': 'number.mock_closing_degree2'}),
call(domain='number', service='set_value', service_data={'value': 70}, target={'entity_id': 'number.mock_opening_degree2'}),
call(domain='number', service='set_value', service_data={'value': 30}, target={'entity_id': 'number.mock_closing_degree2'}),
call(domain='number', service='set_value', service_data={'value': 12}, target={'entity_id': 'number.mock_offset_calibration2'})
]
)

View File

@@ -98,7 +98,7 @@ async def test_power_feature_manager(
}
)
await power_manager.start_listening()
power_manager.start_listening()
assert power_manager.is_configured is True
assert power_manager.overpowering_state == STATE_UNKNOWN
@@ -117,7 +117,7 @@ async def test_power_feature_manager(
assert custom_attributes["current_max_power"] is None
# 3. start listening
await power_manager.start_listening()
power_manager.start_listening()
assert power_manager.is_configured is True
assert power_manager.overpowering_state == STATE_UNKNOWN
@@ -199,7 +199,7 @@ async def test_power_feature_manager_set_overpowering(
}
)
await power_manager.start_listening()
power_manager.start_listening()
assert power_manager.is_configured is True
assert power_manager.overpowering_state == STATE_UNKNOWN
@@ -487,13 +487,6 @@ async def test_power_management_hvac_on(
assert entity.power_manager.overpowering_state is STATE_UNKNOWN
assert entity.target_temperature == 19
# make the heater heats
await send_temperature_change_event(entity, 15, now)
await send_ext_temperature_change_event(entity, 1, now)
await hass.async_block_till_done()
assert entity.power_percent > 0
# Send power mesurement
side_effects = SideEffects(
{
@@ -517,18 +510,17 @@ async def test_power_management_hvac_on(
assert entity.power_manager.overpowering_state is STATE_OFF
# Send power max mesurement too low and HVACMode is on
side_effects.add_or_update_side_effect("sensor.the_max_power_sensor", State("sensor.the_max_power_sensor", 49))
side_effects.add_or_update_side_effect("sensor.the_max_power_sensor", State("sensor.the_max_power_sensor", 149))
# fmt:off
with patch("homeassistant.core.StateMachine.get", side_effect=side_effects.get_side_effects()), \
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_off") as mock_heater_off, \
patch("custom_components.versatile_thermostat.thermostat_switch.ThermostatOverSwitch.is_device_active", return_value="True"):
patch("custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off") as mock_heater_off:
# fmt: on
now = now + timedelta(seconds=30)
VersatileThermostatAPI.get_vtherm_api()._set_now(now)
await send_max_power_change_event(entity, 49, now)
await send_max_power_change_event(entity, 149, datetime.now())
assert entity.power_manager.is_overpowering_detected is True
# All configuration is complete and power is > power_max we switch to POWER preset
assert entity.preset_mode is PRESET_POWER
@@ -545,7 +537,7 @@ async def test_power_management_hvac_on(
"type": "start",
"current_power": 50,
"device_power": 100,
"current_max_power": 49,
"current_max_power": 149,
"current_power_consumption": 100.0,
},
),
@@ -555,9 +547,8 @@ async def test_power_management_hvac_on(
assert mock_heater_on.call_count == 0
assert mock_heater_off.call_count == 1
# Send power mesurement low to unset power preset
# Send power mesurement low to unseet power preset
side_effects.add_or_update_side_effect("sensor.the_power_sensor", State("sensor.the_power_sensor", 48))
side_effects.add_or_update_side_effect("sensor.the_max_power_sensor", State("sensor.the_max_power_sensor", 149))
# fmt:off
with patch("homeassistant.core.StateMachine.get", side_effect=side_effects.get_side_effects()), \
patch("custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event") as mock_send_event, \
@@ -567,7 +558,7 @@ async def test_power_management_hvac_on(
now = now + timedelta(seconds=30)
VersatileThermostatAPI.get_vtherm_api()._set_now(now)
await send_power_change_event(entity, 48, now)
await send_power_change_event(entity, 48, datetime.now())
assert entity.power_manager.is_overpowering_detected is False
# All configuration is complete and power is < power_max, we restore previous preset
assert entity.preset_mode is PRESET_BOOST
@@ -763,6 +754,8 @@ async def test_power_management_energy_over_climate(
CONF_MINIMAL_ACTIVATION_DELAY: 30,
CONF_SAFETY_DELAY_MIN: 5,
CONF_SAFETY_MIN_ON_PERCENT: 0.3,
CONF_POWER_SENSOR: "sensor.mock_power_sensor",
CONF_MAX_POWER_SENSOR: "sensor.mock_power_max_sensor",
CONF_DEVICE_POWER: 100,
CONF_PRESET_POWER: 12,
},

View File

@@ -75,7 +75,7 @@ async def test_presence_feature_manager(
assert custom_attributes["is_presence_configured"] is True
# 3. start listening
await presence_manager.start_listening()
presence_manager.start_listening()
assert presence_manager.is_configured is True
assert presence_manager.presence_state == STATE_UNKNOWN
assert presence_manager.is_absence_detected is False

View File

@@ -224,6 +224,8 @@ async def test_sensors_over_climate(
CONF_MINIMAL_ACTIVATION_DELAY: 30,
CONF_SAFETY_DELAY_MIN: 5,
CONF_SAFETY_MIN_ON_PERCENT: 0.3,
CONF_POWER_SENSOR: "sensor.mock_power_sensor",
CONF_MAX_POWER_SENSOR: "sensor.mock_power_max_sensor",
CONF_DEVICE_POWER: 1.5,
CONF_PRESET_POWER: 12,
},

View File

@@ -26,23 +26,6 @@ async def test_over_switch_ac_full_start(
): # pylint: disable=unused-argument
"""Test the normal full start of a thermostat in thermostat_over_switch type"""
temps = {
PRESET_FROST_PROTECTION: 7,
PRESET_ECO: 17,
PRESET_COMFORT: 19,
PRESET_BOOST: 20,
PRESET_ECO + PRESET_AC_SUFFIX: 25,
PRESET_COMFORT + PRESET_AC_SUFFIX: 23,
PRESET_BOOST + PRESET_AC_SUFFIX: 21,
PRESET_FROST_PROTECTION + PRESET_AWAY_SUFFIX: 7,
PRESET_ECO + PRESET_AWAY_SUFFIX: 16,
PRESET_COMFORT + PRESET_AWAY_SUFFIX: 17,
PRESET_BOOST + PRESET_AWAY_SUFFIX: 18,
PRESET_ECO + PRESET_AC_SUFFIX + PRESET_AWAY_SUFFIX: 27,
PRESET_COMFORT + PRESET_AC_SUFFIX + PRESET_AWAY_SUFFIX: 26,
PRESET_BOOST + PRESET_AC_SUFFIX + PRESET_AWAY_SUFFIX: 25,
}
entry = MockConfigEntry(
domain=DOMAIN,
title="TheOverSwitchACMockName",
@@ -74,7 +57,21 @@ async def test_over_switch_ac_full_start(
assert isinstance(entity, ThermostatOverSwitch)
# Initialise the preset temp
await set_all_climate_preset_temp(hass, entity, temps, "theoverswitchmockname")
await set_climate_preset_temp(
entity, PRESET_FROST_PROTECTION + PRESET_AWAY_SUFFIX, 7
)
await set_climate_preset_temp(entity, PRESET_ECO + PRESET_AWAY_SUFFIX, 16)
await set_climate_preset_temp(entity, PRESET_COMFORT + PRESET_AWAY_SUFFIX, 17)
await set_climate_preset_temp(entity, PRESET_BOOST + PRESET_AWAY_SUFFIX, 18)
await set_climate_preset_temp(
entity, PRESET_ECO + PRESET_AC_SUFFIX + PRESET_AWAY_SUFFIX, 27
)
await set_climate_preset_temp(
entity, PRESET_COMFORT + PRESET_AC_SUFFIX + PRESET_AWAY_SUFFIX, 26
)
await set_climate_preset_temp(
entity, PRESET_BOOST + PRESET_AC_SUFFIX + PRESET_AWAY_SUFFIX, 25
)
assert entity.name == "TheOverSwitchMockName"
assert entity.is_over_climate is False # pylint: disable=protected-access

View File

@@ -51,6 +51,8 @@ async def test_over_valve_full_start(
CONF_MOTION_OFF_DELAY: 30,
CONF_MOTION_PRESET: PRESET_COMFORT,
CONF_NO_MOTION_PRESET: PRESET_ECO,
CONF_POWER_SENSOR: "sensor.power_sensor",
CONF_MAX_POWER_SENSOR: "sensor.power_max_sensor",
CONF_PRESENCE_SENSOR: "person.presence_sensor",
PRESET_FROST_PROTECTION + PRESET_AWAY_SUFFIX + PRESET_TEMP_SUFFIX: 7,
PRESET_ECO + PRESET_AWAY_SUFFIX + PRESET_TEMP_SUFFIX: 17.1,

View File

@@ -170,7 +170,7 @@ async def test_window_feature_manager_refresh_sensor_action_turn_off(
)
# 3. start listening
await window_manager.start_listening()
window_manager.start_listening()
assert window_manager.is_configured is True
assert window_manager.window_state == STATE_UNKNOWN
assert window_manager.window_auto_state == STATE_UNAVAILABLE
@@ -288,7 +288,7 @@ async def test_window_feature_manager_refresh_sensor_action_frost_only(
)
# 3. start listening
await window_manager.start_listening()
window_manager.start_listening()
assert window_manager.is_configured is True
assert window_manager.window_state == STATE_UNKNOWN
assert window_manager.window_auto_state == STATE_UNAVAILABLE
@@ -408,7 +408,7 @@ async def test_window_feature_manager_sensor_event_action_turn_off(
)
# 3. start listening
await window_manager.start_listening()
window_manager.start_listening()
assert len(window_manager._active_listener) == 1
# 4. test refresh with the parametrized
@@ -535,7 +535,7 @@ async def test_window_feature_manager_event_sensor_action_frost_only(
)
# 3. start listening
await window_manager.start_listening()
window_manager.start_listening()
# 4. test refresh with the parametrized
# fmt:off
@@ -660,7 +660,7 @@ async def test_window_feature_manager_window_auto(
}
)
assert window_manager.is_window_auto_configured is True
await window_manager.start_listening()
window_manager.start_listening()
# 2. Call manage window auto
tz = get_tz(hass) # pylint: disable=invalid-name