Compare commits
25 Commits
7.1.0.beta
...
jmcollin78
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
93823bf93c | ||
|
|
a2f3b24928 | ||
|
|
d1ef83f422 | ||
|
|
42ac2b0f98 | ||
|
|
e71d8dba86 | ||
|
|
3e96247b63 | ||
|
|
05e31358a4 | ||
|
|
3af0318c2f | ||
|
|
12b67ba3e0 | ||
|
|
1d675f22c7 | ||
|
|
3fd9ffe93d | ||
|
|
43d8e5eb3c | ||
|
|
f8050e2ed7 | ||
|
|
13443402d0 | ||
|
|
4d7bc1b5b3 | ||
|
|
2b164d3dab | ||
|
|
0333c403f8 | ||
|
|
ae1a86f484 | ||
|
|
2db574da42 | ||
|
|
d236cc8fbb | ||
|
|
a637c2841c | ||
|
|
ee3b803db1 | ||
|
|
22b2b965c1 | ||
|
|
9c8a965dba | ||
|
|
68e05bef31 |
@@ -54,6 +54,7 @@
|
||||
"python.analysis.autoSearchPaths": true,
|
||||
"pylint.lintOnChange": false,
|
||||
"python.formatting.provider": "black",
|
||||
"python.formatting.blackArgs": ["--line-length", "180"],
|
||||
"python.formatting.blackPath": "/usr/local/py-utils/bin/black",
|
||||
"editor.formatOnPaste": false,
|
||||
"editor.formatOnSave": true,
|
||||
|
||||
7
.github/ISSUE_TEMPLATE/issue.md
vendored
7
.github/ISSUE_TEMPLATE/issue.md
vendored
@@ -4,7 +4,10 @@ about: Create a report to help us improve
|
||||
|
||||
---
|
||||
|
||||
> Please read carefuly this instructions and fill this form before writing an issue. It helps me to help you.
|
||||
# Read this carefully
|
||||
|
||||
> Please read carefully this instructions and fill this form before writing an issue. It helps me to help you.
|
||||
> If you choose to not follow this template, you accept to have no answer from the author. The tag on the issue 'Template not respected' means you don't respect this template. Potentially, you will not have a relevant answer.
|
||||
|
||||
<!-- This template will allow the maintainer to be efficient and post the more accurante response as possible. There is many types / modes / configuration possible, so the analysis can be very tricky. If don't follow this template, your issue could be rejected without any message. Please help me to help you. -->
|
||||
|
||||
@@ -12,7 +15,7 @@ about: Create a report to help us improve
|
||||
|
||||
If you have a simple question or you are not sure this is an issue, don't open an issue but open a new discussion [here](https://github.com/jmcollin78/versatile_thermostat/discussions).
|
||||
|
||||
Check also in the [Troubleshooting](#troubleshooting) paragrah of the README if the aswer is not already given.
|
||||
Check also in the [Troubleshooting] paragrah of the README if the aswer is not already given.
|
||||
|
||||
Issues not containing the minimum requirements will be closed:
|
||||
|
||||
|
||||
49
README-fr.md
49
README-fr.md
@@ -35,31 +35,38 @@ Un grand merci à tous mes fournisseurs de bières pour leurs dons et leurs enco
|
||||
|
||||
_AC_ : Air conditionné. Un équipement est AC si il fait du froid. Les températures sont alors inversées : Eco est plus chaud que Confort qui est plus chaud que Boost. Les algorithmes tiennent compte de cette information.
|
||||
|
||||
_EMA_ : Exponential Moving Average. Utilisé pour lisser les mesures de températures de capteur. Elle correspond à une moyenne glissante de la température de la pièce. Elle est utilisée pour calculer la pente de la courbe de température (slope) qui serait trop instable sur la courbe brute.
|
||||
|
||||
_slope_ : la pente de la courbe de température. Elle est mesurée en °(C ou K)/h. Elle est positive si la température augmente et négative si elle diminue. Cette pente est calculée sur l'_EMA_
|
||||
|
||||
_PAC_ : Pompe à chaleur
|
||||
|
||||
# Documentation
|
||||
|
||||
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. [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)
|
||||
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)
|
||||
|
||||
|
||||
# Quelques résultats
|
||||
|
||||
@@ -34,6 +34,12 @@ A big thank you to all my beer sponsors for their donations and encouragements.
|
||||
|
||||
_AC_: Air Conditioning. An AC device cools instead of heats. Temperatures are reversed: Eco is warmer than Comfort, which is warmer than Boost. The algorithms take this information into account.
|
||||
|
||||
_EMA_: Exponential Moving Average. Used to smooth sensor temperature measurements. It represents a moving average of the room's temperature and is used to calculate the slope of the temperature curve, which would be too unstable on the raw data.
|
||||
|
||||
_slope_: The slope of the temperature curve, measured in ° (C or K)/h. It is positive when the temperature increases and negative when it decreases. This slope is calculated based on the _EMA_.
|
||||
|
||||
_PAC_ : Heat pump
|
||||
|
||||
# Documentation
|
||||
|
||||
The documentation is now divided into several pages for easier reading and searching:
|
||||
|
||||
@@ -16,10 +16,10 @@ _LOGGER = logging.getLogger(__name__)
|
||||
class BaseFeatureManager:
|
||||
"""A base class for all feature"""
|
||||
|
||||
def __init__(self, vtherm: Any, hass: HomeAssistant):
|
||||
def __init__(self, vtherm: Any, hass: HomeAssistant, name: str = None):
|
||||
"""Init of a featureManager"""
|
||||
self._vtherm = vtherm
|
||||
self._name = vtherm.name
|
||||
self._name = vtherm.name if vtherm else name
|
||||
self._active_listener: list[CALLBACK_TYPE] = []
|
||||
self._hass = hass
|
||||
|
||||
@@ -27,7 +27,7 @@ class BaseFeatureManager:
|
||||
"""Initialize the attributes of the FeatureManager"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def start_listening(self):
|
||||
async def start_listening(self):
|
||||
"""Start listening the underlying entity"""
|
||||
raise NotImplementedError()
|
||||
|
||||
@@ -38,6 +38,10 @@ class BaseFeatureManager:
|
||||
|
||||
self._active_listener = []
|
||||
|
||||
async def refresh_state(self):
|
||||
"""Refresh the state and return True if a change have been made"""
|
||||
return False
|
||||
|
||||
def add_listener(self, func: CALLBACK_TYPE) -> None:
|
||||
"""Add a listener to the list of active listener"""
|
||||
self._active_listener.append(func)
|
||||
|
||||
@@ -50,7 +50,6 @@ from homeassistant.const import (
|
||||
ATTR_TEMPERATURE,
|
||||
STATE_UNAVAILABLE,
|
||||
STATE_UNKNOWN,
|
||||
STATE_ON,
|
||||
)
|
||||
|
||||
from .const import * # pylint: disable=wildcard-import, unused-wildcard-import
|
||||
@@ -99,7 +98,6 @@ 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",
|
||||
@@ -191,7 +189,6 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
|
||||
|
||||
self._ema_temp = None
|
||||
self._ema_algo = None
|
||||
self._now = None
|
||||
|
||||
self._attr_fan_mode = None
|
||||
|
||||
@@ -446,10 +443,6 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
|
||||
)
|
||||
)
|
||||
|
||||
# start listening for all managers
|
||||
for manager in self._managers:
|
||||
manager.start_listening()
|
||||
|
||||
self.async_on_remove(self.remove_thermostat)
|
||||
|
||||
# issue 428. Link to others entities will start at link
|
||||
@@ -482,6 +475,10 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
|
||||
_LOGGER.debug("%s - Calling async_startup_internal", self)
|
||||
need_write_state = False
|
||||
|
||||
# start listening for all managers
|
||||
for manager in self._managers:
|
||||
await manager.start_listening()
|
||||
|
||||
await self.get_my_previous_state()
|
||||
|
||||
await self.init_presets(central_configuration)
|
||||
@@ -608,8 +605,6 @@ 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(
|
||||
@@ -1036,6 +1031,10 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
|
||||
|
||||
return
|
||||
|
||||
# Remove eventual overpoering if we want to turn-off
|
||||
if hvac_mode == HVACMode.OFF and self.power_manager.is_overpowering_detected:
|
||||
await self.power_manager.set_overpowering(False)
|
||||
|
||||
self._hvac_mode = hvac_mode
|
||||
|
||||
# Delegate to all underlying
|
||||
@@ -1068,9 +1067,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
|
||||
save_state()
|
||||
|
||||
@overrides
|
||||
async def async_set_preset_mode(
|
||||
self, preset_mode: str, overwrite_saved_preset=True
|
||||
):
|
||||
async def async_set_preset_mode(self, preset_mode: str, overwrite_saved_preset=True):
|
||||
"""Set new preset mode."""
|
||||
|
||||
# We accept a new preset when:
|
||||
@@ -1098,14 +1095,10 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
|
||||
|
||||
return
|
||||
|
||||
await self.async_set_preset_mode_internal(
|
||||
preset_mode, force=False, overwrite_saved_preset=overwrite_saved_preset
|
||||
)
|
||||
await self.async_set_preset_mode_internal(preset_mode, force=False, overwrite_saved_preset=overwrite_saved_preset)
|
||||
await self.async_control_heating(force=True)
|
||||
|
||||
async def async_set_preset_mode_internal(
|
||||
self, preset_mode: str, force=False, overwrite_saved_preset=True
|
||||
):
|
||||
async def async_set_preset_mode_internal(self, preset_mode: str, force=False, overwrite_saved_preset=True):
|
||||
"""Set new preset mode."""
|
||||
_LOGGER.info("%s - Set preset_mode: %s force=%s", self, preset_mode, force)
|
||||
if (
|
||||
@@ -1454,7 +1447,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
|
||||
control_heating to turn all off"""
|
||||
|
||||
for under in self._underlyings:
|
||||
await under.turn_off()
|
||||
await under.turn_off_and_cancel_cycle()
|
||||
|
||||
def save_preset_mode(self):
|
||||
"""Save the current preset mode to be restored later
|
||||
@@ -1466,7 +1459,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
|
||||
):
|
||||
self._saved_preset_mode = self._attr_preset_mode
|
||||
|
||||
async def restore_preset_mode(self):
|
||||
async def restore_preset_mode(self, force=False):
|
||||
"""Restore a previous preset mode
|
||||
We never restore a hidden preset mode. Normally that is not possible
|
||||
"""
|
||||
@@ -1474,7 +1467,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
|
||||
self._saved_preset_mode not in HIDDEN_PRESETS
|
||||
and self._saved_preset_mode is not None
|
||||
):
|
||||
await self.async_set_preset_mode_internal(self._saved_preset_mode)
|
||||
await self.async_set_preset_mode_internal(self._saved_preset_mode, force=force)
|
||||
|
||||
def save_hvac_mode(self):
|
||||
"""Save the current hvac-mode to be restored later"""
|
||||
@@ -1574,23 +1567,12 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
|
||||
and HVACMode.HEAT in self.hvac_modes
|
||||
):
|
||||
await self.async_set_hvac_mode(HVACMode.HEAT)
|
||||
await self.async_set_preset_mode(
|
||||
PRESET_FROST_PROTECTION, overwrite_saved_preset=False
|
||||
)
|
||||
await self.async_set_preset_mode(PRESET_FROST_PROTECTION, overwrite_saved_preset=False)
|
||||
else:
|
||||
self.set_hvac_off_reason(HVAC_OFF_REASON_MANUAL)
|
||||
await self.async_set_hvac_mode(HVACMode.OFF)
|
||||
return
|
||||
|
||||
def _set_now(self, now: datetime):
|
||||
"""Set the now timestamp. This is only for tests purpose"""
|
||||
self._now = now
|
||||
|
||||
@property
|
||||
def now(self) -> datetime:
|
||||
"""Get now. The local datetime or the overloaded _set_now date"""
|
||||
return self._now if self._now is not None else NowClass.get_now(self._hass)
|
||||
|
||||
@property
|
||||
def is_initialized(self) -> bool:
|
||||
"""Check if all underlyings are initialized
|
||||
@@ -1620,11 +1602,8 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
|
||||
return False
|
||||
|
||||
# Check overpowering condition
|
||||
# Not necessary for switch because each switch is checking at startup
|
||||
overpowering = await self._power_manager.check_overpowering()
|
||||
if overpowering == STATE_ON:
|
||||
_LOGGER.debug("%s - End of cycle (overpowering)", self)
|
||||
return True
|
||||
# Not usefull. Will be done at the next power refresh
|
||||
# await VersatileThermostatAPI.get_vtherm_api().central_power_manager.refresh_state()
|
||||
|
||||
safety: bool = await self._safety_manager.refresh_state()
|
||||
if safety and self.is_over_climate:
|
||||
@@ -1813,9 +1792,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
|
||||
# If the changed preset is active, change the current temperature
|
||||
# Issue #119 - reload new preset temperature also in ac mode
|
||||
if preset.startswith(self._attr_preset_mode):
|
||||
await self.async_set_preset_mode_internal(
|
||||
preset.rstrip(PRESET_AC_SUFFIX), force=True
|
||||
)
|
||||
await self.async_set_preset_mode_internal(preset.rstrip(PRESET_AC_SUFFIX), force=True)
|
||||
await self.async_control_heating(force=True)
|
||||
|
||||
async def SERVICE_SET_SAFETY(
|
||||
@@ -1965,3 +1942,34 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
|
||||
def is_preset_configured(self, preset) -> bool:
|
||||
"""Returns True if the preset in argument is configured"""
|
||||
return self._presets.get(preset, None) is not None
|
||||
|
||||
# For testing purpose
|
||||
# @deprecated
|
||||
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
|
||||
|
||||
# @deprecated
|
||||
@property
|
||||
def now(self) -> datetime:
|
||||
"""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
|
||||
|
||||
@@ -0,0 +1,309 @@
|
||||
""" Implements a central Power Feature Manager for Versatile Thermostat """
|
||||
|
||||
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 (
|
||||
ClimateEntity,
|
||||
DOMAIN as CLIMATE_DOMAIN,
|
||||
)
|
||||
|
||||
|
||||
from .const import * # pylint: disable=wildcard-import, unused-wildcard-import
|
||||
from .commons import ConfigData
|
||||
from .base_manager import BaseFeatureManager
|
||||
|
||||
# circular dependency
|
||||
# from .base_thermostat import BaseThermostat
|
||||
|
||||
MIN_DTEMP_SECS = 20
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CentralFeaturePowerManager(BaseFeatureManager):
|
||||
"""A central Power feature manager"""
|
||||
|
||||
def __init__(self, hass: HomeAssistant, vtherm_api: Any):
|
||||
"""Init of a featureManager"""
|
||||
super().__init__(None, hass, "centralPowerManager")
|
||||
self._hass: HomeAssistant = hass
|
||||
self._vtherm_api = vtherm_api # no type due to circular reference
|
||||
self._is_configured: bool = False
|
||||
self._power_sensor_entity_id: str = None
|
||||
self._max_power_sensor_entity_id: str = None
|
||||
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):
|
||||
"""Gets the configuration parameters"""
|
||||
central_config = self._vtherm_api.find_central_configuration()
|
||||
if not central_config:
|
||||
_LOGGER.info("No central configuration is found. Power management will be deactivated")
|
||||
return
|
||||
|
||||
self._power_sensor_entity_id = entry_infos.get(CONF_POWER_SENSOR)
|
||||
self._max_power_sensor_entity_id = entry_infos.get(CONF_MAX_POWER_SENSOR)
|
||||
self._power_temp = entry_infos.get(CONF_PRESET_POWER)
|
||||
|
||||
self._is_configured = False
|
||||
self._current_power = None
|
||||
self._current_max_power = None
|
||||
if (
|
||||
entry_infos.get(CONF_USE_POWER_FEATURE, False)
|
||||
and self._max_power_sensor_entity_id
|
||||
and self._power_sensor_entity_id
|
||||
and self._power_temp
|
||||
):
|
||||
self._is_configured = True
|
||||
else:
|
||||
_LOGGER.info("Power management is not fully configured and will be deactivated")
|
||||
|
||||
async def start_listening(self):
|
||||
"""Start listening the power sensor"""
|
||||
if not self._is_configured:
|
||||
return
|
||||
|
||||
self.stop_listening()
|
||||
|
||||
self.add_listener(
|
||||
async_track_state_change_event(
|
||||
self.hass,
|
||||
[self._power_sensor_entity_id],
|
||||
self._power_sensor_changed,
|
||||
)
|
||||
)
|
||||
|
||||
self.add_listener(
|
||||
async_track_state_change_event(
|
||||
self.hass,
|
||||
[self._max_power_sensor_entity_id],
|
||||
self._max_power_sensor_changed,
|
||||
)
|
||||
)
|
||||
|
||||
@callback
|
||||
async def _power_sensor_changed(self, event: Event[EventStateChangedData]):
|
||||
"""Handle power changes."""
|
||||
_LOGGER.debug("Receive new Power event")
|
||||
_LOGGER.debug(event)
|
||||
await self.refresh_state()
|
||||
|
||||
@callback
|
||||
async def _max_power_sensor_changed(self, event: Event[EventStateChangedData]):
|
||||
"""Handle power max changes."""
|
||||
_LOGGER.debug("Receive new Power Max event")
|
||||
_LOGGER.debug(event)
|
||||
await self.refresh_state()
|
||||
|
||||
@overrides
|
||||
async def refresh_state(self) -> bool:
|
||||
"""Tries to get the last state from sensor
|
||||
Returns True if a change has been made"""
|
||||
|
||||
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
|
||||
|
||||
if not self._is_configured:
|
||||
return False
|
||||
|
||||
# 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()
|
||||
|
||||
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()
|
||||
|
||||
# shedding only
|
||||
if available_power < 0:
|
||||
_LOGGER.debug(
|
||||
"The available power is is < 0 (%s). Set overpowering only for list: %s",
|
||||
available_power,
|
||||
vtherms_sorted,
|
||||
)
|
||||
# we will set overpowering for the nearest target temp first
|
||||
total_power_gain = 0
|
||||
|
||||
for vtherm in vtherms_sorted:
|
||||
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)
|
||||
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()
|
||||
_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
|
||||
|
||||
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,
|
||||
)
|
||||
|
||||
_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
|
||||
|
||||
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")
|
||||
|
||||
def get_climate_components_entities(self) -> list:
|
||||
"""Get all VTherms entitites"""
|
||||
vtherms = []
|
||||
component: EntityComponent[ClimateEntity] = self._hass.data.get(
|
||||
CLIMATE_DOMAIN, None
|
||||
)
|
||||
if component:
|
||||
for entity in component.entities:
|
||||
# A little hack to test if the climate is a VTherm. Cannot use isinstance
|
||||
# due to circular dependency of BaseThermostat
|
||||
if (
|
||||
entity.device_info
|
||||
and entity.device_info.get("model", None) == DOMAIN
|
||||
):
|
||||
vtherms.append(entity)
|
||||
return vtherms
|
||||
|
||||
def find_all_vtherm_with_power_management_sorted_by_dtemp(
|
||||
self,
|
||||
) -> list:
|
||||
"""Returns all the VTherms with power management activated"""
|
||||
entities = self.get_climate_components_entities()
|
||||
vtherms = [
|
||||
vtherm
|
||||
for vtherm in entities
|
||||
if vtherm.power_manager.is_configured and vtherm.is_on
|
||||
]
|
||||
|
||||
# sort the result with the min temp difference first. A and B should be BaseThermostat class
|
||||
def cmp_temps(a, b) -> int:
|
||||
diff_a = float("inf")
|
||||
diff_b = float("inf")
|
||||
a_target = a.target_temperature if not a.power_manager.is_overpowering_detected else a.saved_target_temp
|
||||
b_target = b.target_temperature if not b.power_manager.is_overpowering_detected else b.saved_target_temp
|
||||
if a.current_temperature is not None and a_target is not None:
|
||||
diff_a = a_target - a.current_temperature
|
||||
if b.current_temperature is not None and b_target is not None:
|
||||
diff_b = b_target - b.current_temperature
|
||||
|
||||
if diff_a == diff_b:
|
||||
return 0
|
||||
return 1 if diff_a > diff_b else -1
|
||||
|
||||
vtherms.sort(key=cmp_to_key(cmp_temps))
|
||||
return vtherms
|
||||
|
||||
@property
|
||||
def is_configured(self) -> bool:
|
||||
"""True if the FeatureManager is fully configured"""
|
||||
return self._is_configured
|
||||
|
||||
@property
|
||||
def current_power(self) -> float | None:
|
||||
"""Return the current power from sensor"""
|
||||
return self._current_power
|
||||
|
||||
@property
|
||||
def current_max_power(self) -> float | None:
|
||||
"""Return the current power from sensor"""
|
||||
return self._current_max_power
|
||||
|
||||
@property
|
||||
def power_temperature(self) -> float | None:
|
||||
"""Return the power temperature"""
|
||||
return self._power_temp
|
||||
|
||||
@property
|
||||
def power_sensor_entity_id(self) -> float | None:
|
||||
"""Return the power sensor entity id"""
|
||||
return self._power_sensor_entity_id
|
||||
|
||||
@property
|
||||
def max_power_sensor_entity_id(self) -> float | None:
|
||||
"""Return the max power sensor entity id"""
|
||||
return self._max_power_sensor_entity_id
|
||||
|
||||
def __str__(self):
|
||||
return "CentralPowerManager"
|
||||
@@ -28,6 +28,7 @@ from .thermostat_switch import ThermostatOverSwitch
|
||||
from .thermostat_climate import ThermostatOverClimate
|
||||
from .thermostat_valve import ThermostatOverValve
|
||||
from .thermostat_climate_valve import ThermostatOverClimateValve
|
||||
from .vtherm_api import VersatileThermostatAPI
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -51,6 +52,9 @@ async def async_setup_entry(
|
||||
)
|
||||
|
||||
if vt_type == CONF_THERMOSTAT_CENTRAL_CONFIG:
|
||||
# Initialize the central power manager
|
||||
vtherm_api = VersatileThermostatAPI.get_vtherm_api(hass)
|
||||
vtherm_api.central_power_manager.post_init(entry.data)
|
||||
return
|
||||
|
||||
# Instantiate the right base class
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
# pylint: disable=line-too-long
|
||||
|
||||
import logging
|
||||
import warnings
|
||||
from types import MappingProxyType
|
||||
from typing import Any, TypeVar
|
||||
|
||||
@@ -132,3 +133,20 @@ def check_and_extract_service_configuration(service_config) -> dict:
|
||||
"check_and_extract_service_configuration(%s) gives '%s'", service_config, ret
|
||||
)
|
||||
return ret
|
||||
|
||||
|
||||
def deprecated(message):
|
||||
"""A decorator to indicate that the method/attribut is deprecated"""
|
||||
|
||||
def decorator(func):
|
||||
def wrapper(*args, **kwargs):
|
||||
warnings.warn(
|
||||
f"{func.__name__} is deprecated: {message}",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
return func(*args, **kwargs)
|
||||
|
||||
return wrapper
|
||||
|
||||
return decorator
|
||||
|
||||
@@ -90,11 +90,10 @@ 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_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_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_PRESENCE_FEATURE] = (
|
||||
self._infos.get(CONF_USE_PRESENCE_CENTRAL_CONFIG, False)
|
||||
@@ -184,7 +183,7 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
|
||||
Data has the keys from STEP_*_DATA_SCHEMA with values provided by the user.
|
||||
"""
|
||||
|
||||
# check the heater_entity_id
|
||||
# check the entity_ids
|
||||
for conf in [
|
||||
CONF_UNDERLYING_LIST,
|
||||
CONF_TEMP_SENSOR,
|
||||
@@ -319,9 +318,8 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
|
||||
):
|
||||
return False
|
||||
|
||||
if infos.get(CONF_UNDERLYING_LIST, None) is not None and not infos.get(
|
||||
CONF_UNDERLYING_LIST, None
|
||||
):
|
||||
# checks that at least one underlying is set but not it central configuration
|
||||
if len(infos.get(CONF_UNDERLYING_LIST, [])) < 1:
|
||||
return False
|
||||
|
||||
if (
|
||||
@@ -330,14 +328,7 @@ 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_POWER_SENSOR, None) is None
|
||||
or infos.get(CONF_MAX_POWER_SENSOR, 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_PRESET_POWER, None) is None:
|
||||
return False
|
||||
|
||||
if (
|
||||
@@ -815,7 +806,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_CENTRAL_POWER_DATA_SCHEMA
|
||||
schema = STEP_NON_CENTRAL_POWER_DATA_SCHEMA
|
||||
|
||||
self._infos[COMES_FROM] = "async_step_spec_power"
|
||||
|
||||
@@ -909,10 +900,42 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
|
||||
return await self.generic_step("advanced", schema, user_input, next_step)
|
||||
|
||||
async def async_step_finalize(self, _):
|
||||
"""Should be implemented by Leaf classes"""
|
||||
raise HomeAssistantError(
|
||||
"async_finalize not implemented on VersatileThermostat sub-class"
|
||||
)
|
||||
"""Finalize the creation. Should be overriden by underlyings"""
|
||||
if not self._infos[CONF_USE_WINDOW_FEATURE]:
|
||||
self._infos[CONF_USE_WINDOW_CENTRAL_CONFIG] = False
|
||||
if CONF_WINDOW_SENSOR in self._infos:
|
||||
del self._infos[CONF_WINDOW_SENSOR]
|
||||
if CONF_WINDOW_AUTO_CLOSE_THRESHOLD in self._infos:
|
||||
del self._infos[CONF_WINDOW_AUTO_CLOSE_THRESHOLD]
|
||||
if CONF_WINDOW_AUTO_OPEN_THRESHOLD in self._infos:
|
||||
del self._infos[CONF_WINDOW_AUTO_OPEN_THRESHOLD]
|
||||
if CONF_WINDOW_AUTO_MAX_DURATION in self._infos:
|
||||
del self._infos[CONF_WINDOW_AUTO_MAX_DURATION]
|
||||
if not self._infos[CONF_USE_MOTION_FEATURE]:
|
||||
self._infos[CONF_USE_MOTION_CENTRAL_CONFIG] = False
|
||||
if CONF_MOTION_SENSOR in self._infos:
|
||||
del self._infos[CONF_MOTION_SENSOR]
|
||||
if not self._infos[CONF_USE_POWER_FEATURE]:
|
||||
self._infos[CONF_USE_POWER_CENTRAL_CONFIG] = False
|
||||
if CONF_POWER_SENSOR in self._infos:
|
||||
del self._infos[CONF_POWER_SENSOR]
|
||||
if CONF_MAX_POWER_SENSOR in self._infos:
|
||||
del self._infos[CONF_MAX_POWER_SENSOR]
|
||||
if not self._infos[CONF_USE_PRESENCE_FEATURE]:
|
||||
self._infos[CONF_USE_PRESENCE_CENTRAL_CONFIG] = False
|
||||
if CONF_PRESENCE_SENSOR in self._infos:
|
||||
del self._infos[CONF_PRESENCE_SENSOR]
|
||||
if not self._infos[CONF_USE_CENTRAL_BOILER_FEATURE]:
|
||||
if CONF_CENTRAL_BOILER_ACTIVATION_SRV in self._infos:
|
||||
del self._infos[CONF_CENTRAL_BOILER_ACTIVATION_SRV]
|
||||
if CONF_CENTRAL_BOILER_DEACTIVATION_SRV in self._infos:
|
||||
del self._infos[CONF_CENTRAL_BOILER_DEACTIVATION_SRV]
|
||||
if not self._infos[CONF_USE_AUTO_START_STOP_FEATURE]:
|
||||
self._infos[CONF_AUTO_START_STOP_LEVEL] = AUTO_START_STOP_LEVEL_NONE
|
||||
|
||||
# Removes temporary value
|
||||
if COMES_FROM in self._infos:
|
||||
del self._infos[COMES_FROM]
|
||||
|
||||
|
||||
class VersatileThermostatConfigFlow( # pylint: disable=abstract-method
|
||||
@@ -936,9 +959,8 @@ class VersatileThermostatConfigFlow( # pylint: disable=abstract-method
|
||||
async def async_step_finalize(self, _):
|
||||
"""Finalization of the ConfigEntry creation"""
|
||||
_LOGGER.debug("ConfigFlow.async_finalize")
|
||||
# Removes temporary value
|
||||
if COMES_FROM in self._infos:
|
||||
del self._infos[COMES_FROM]
|
||||
await super().async_step_finalize(_)
|
||||
|
||||
return self.async_create_entry(title=self._infos[CONF_NAME], data=self._infos)
|
||||
|
||||
|
||||
@@ -976,37 +998,13 @@ class VersatileThermostatOptionsFlowHandler(
|
||||
|
||||
async def async_step_finalize(self, _):
|
||||
"""Finalization of the ConfigEntry creation"""
|
||||
if not self._infos[CONF_USE_WINDOW_FEATURE]:
|
||||
self._infos[CONF_USE_WINDOW_CENTRAL_CONFIG] = False
|
||||
self._infos[CONF_WINDOW_SENSOR] = None
|
||||
self._infos[CONF_WINDOW_AUTO_CLOSE_THRESHOLD] = None
|
||||
self._infos[CONF_WINDOW_AUTO_OPEN_THRESHOLD] = None
|
||||
self._infos[CONF_WINDOW_AUTO_MAX_DURATION] = None
|
||||
if not self._infos[CONF_USE_MOTION_FEATURE]:
|
||||
self._infos[CONF_USE_MOTION_CENTRAL_CONFIG] = False
|
||||
self._infos[CONF_MOTION_SENSOR] = None
|
||||
if not self._infos[CONF_USE_POWER_FEATURE]:
|
||||
self._infos[CONF_USE_POWER_CENTRAL_CONFIG] = False
|
||||
self._infos[CONF_POWER_SENSOR] = None
|
||||
self._infos[CONF_MAX_POWER_SENSOR] = None
|
||||
if not self._infos[CONF_USE_PRESENCE_FEATURE]:
|
||||
self._infos[CONF_USE_PRESENCE_CENTRAL_CONFIG] = False
|
||||
self._infos[CONF_PRESENCE_SENSOR] = None
|
||||
if not self._infos[CONF_USE_CENTRAL_BOILER_FEATURE]:
|
||||
self._infos[CONF_CENTRAL_BOILER_ACTIVATION_SRV] = None
|
||||
self._infos[CONF_CENTRAL_BOILER_DEACTIVATION_SRV] = None
|
||||
if not self._infos[CONF_USE_AUTO_START_STOP_FEATURE]:
|
||||
self._infos[CONF_AUTO_START_STOP_LEVEL] = AUTO_START_STOP_LEVEL_NONE
|
||||
|
||||
_LOGGER.info(
|
||||
"Recreating entry %s due to configuration change. New config is now: %s",
|
||||
self.config_entry.entry_id,
|
||||
self._infos,
|
||||
)
|
||||
|
||||
# Removes temporary value
|
||||
if COMES_FROM in self._infos:
|
||||
del self._infos[COMES_FROM]
|
||||
await super().async_step_finalize(_)
|
||||
|
||||
self.hass.config_entries.async_update_entry(self.config_entry, data=self._infos)
|
||||
return self.async_create_entry(title=None, data=None)
|
||||
|
||||
@@ -231,16 +231,8 @@ STEP_TPI_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name
|
||||
|
||||
STEP_CENTRAL_TPI_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name
|
||||
{
|
||||
vol.Required(CONF_TPI_COEF_INT, default=0.6): selector.NumberSelector(
|
||||
selector.NumberSelectorConfig(
|
||||
min=0.0, max=1.0, step=0.01, mode=selector.NumberSelectorMode.BOX
|
||||
)
|
||||
),
|
||||
vol.Required(CONF_TPI_COEF_EXT, default=0.01): selector.NumberSelector(
|
||||
selector.NumberSelectorConfig(
|
||||
min=0.0, max=1.0, step=0.01, mode=selector.NumberSelectorMode.BOX
|
||||
)
|
||||
),
|
||||
vol.Required(CONF_TPI_COEF_INT, default=0.6): selector.NumberSelector(selector.NumberSelectorConfig(min=0.0, max=1.0, step=0.01, mode=selector.NumberSelectorMode.BOX)),
|
||||
vol.Required(CONF_TPI_COEF_EXT, default=0.01): selector.NumberSelector(selector.NumberSelectorConfig(min=0.0, max=1.0, step=0.001, mode=selector.NumberSelectorMode.BOX)),
|
||||
}
|
||||
)
|
||||
|
||||
@@ -265,12 +257,11 @@ STEP_WINDOW_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name
|
||||
STEP_CENTRAL_WINDOW_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name
|
||||
{
|
||||
vol.Optional(CONF_WINDOW_DELAY, default=30): cv.positive_int,
|
||||
vol.Optional(CONF_WINDOW_OFF_DELAY, default=30): cv.positive_int,
|
||||
vol.Optional(CONF_WINDOW_AUTO_OPEN_THRESHOLD, default=3): vol.Coerce(float),
|
||||
vol.Optional(CONF_WINDOW_AUTO_CLOSE_THRESHOLD, default=0): vol.Coerce(float),
|
||||
vol.Optional(CONF_WINDOW_AUTO_MAX_DURATION, default=30): cv.positive_int,
|
||||
vol.Optional(
|
||||
CONF_WINDOW_ACTION, default=CONF_WINDOW_TURN_OFF
|
||||
): selector.SelectSelector(
|
||||
vol.Optional(CONF_WINDOW_ACTION, default=CONF_WINDOW_TURN_OFF): selector.SelectSelector(
|
||||
selector.SelectSelectorConfig(
|
||||
options=CONF_WINDOW_ACTIONS,
|
||||
translation_key="window_action",
|
||||
@@ -283,9 +274,8 @@ STEP_CENTRAL_WINDOW_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name
|
||||
STEP_CENTRAL_WINDOW_WO_AUTO_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name
|
||||
{
|
||||
vol.Optional(CONF_WINDOW_DELAY, default=30): cv.positive_int,
|
||||
vol.Optional(
|
||||
CONF_WINDOW_ACTION, default=CONF_WINDOW_TURN_OFF
|
||||
): selector.SelectSelector(
|
||||
vol.Optional(CONF_WINDOW_OFF_DELAY, default=30): cv.positive_int,
|
||||
vol.Optional(CONF_WINDOW_ACTION, default=CONF_WINDOW_TURN_OFF): selector.SelectSelector(
|
||||
selector.SelectSelectorConfig(
|
||||
options=CONF_WINDOW_ACTIONS,
|
||||
translation_key="window_action",
|
||||
@@ -339,6 +329,12 @@ STEP_CENTRAL_POWER_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name
|
||||
}
|
||||
)
|
||||
|
||||
STEP_NON_CENTRAL_POWER_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name
|
||||
{
|
||||
vol.Optional(CONF_PRESET_POWER, default="13"): vol.Coerce(float),
|
||||
}
|
||||
)
|
||||
|
||||
STEP_POWER_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name
|
||||
{
|
||||
vol.Required(CONF_USE_POWER_CENTRAL_CONFIG, default=True): cv.boolean,
|
||||
|
||||
@@ -73,6 +73,7 @@ CONF_DEVICE_POWER = "device_power"
|
||||
CONF_CYCLE_MIN = "cycle_min"
|
||||
CONF_PROP_FUNCTION = "proportional_function"
|
||||
CONF_WINDOW_DELAY = "window_delay"
|
||||
CONF_WINDOW_OFF_DELAY = "window_off_delay"
|
||||
CONF_MOTION_DELAY = "motion_delay"
|
||||
CONF_MOTION_OFF_DELAY = "motion_off_delay"
|
||||
CONF_MOTION_PRESET = "motion_preset"
|
||||
@@ -270,6 +271,7 @@ ALL_CONF = (
|
||||
CONF_MAX_POWER_SENSOR,
|
||||
CONF_WINDOW_SENSOR,
|
||||
CONF_WINDOW_DELAY,
|
||||
CONF_WINDOW_OFF_DELAY,
|
||||
CONF_WINDOW_AUTO_OPEN_THRESHOLD,
|
||||
CONF_WINDOW_AUTO_CLOSE_THRESHOLD,
|
||||
CONF_WINDOW_AUTO_MAX_DURATION,
|
||||
@@ -503,6 +505,8 @@ def get_safe_float(hass, entity_id: str):
|
||||
if (
|
||||
entity_id is None
|
||||
or not (state := hass.states.get(entity_id))
|
||||
or state.state is None
|
||||
or state.state == "None"
|
||||
or state.state == "unknown"
|
||||
or state.state == "unavailable"
|
||||
):
|
||||
|
||||
@@ -70,8 +70,12 @@ class FeatureAutoStartStopManager(BaseFeatureManager):
|
||||
self._auto_start_stop_level, self.name
|
||||
)
|
||||
|
||||
# Fix an eventual incoherent state
|
||||
if self._vtherm.is_on and self._vtherm.hvac_off_reason == HVAC_OFF_REASON_AUTO_START_STOP:
|
||||
self._vtherm.hvac_off_reason = None
|
||||
|
||||
@overrides
|
||||
def start_listening(self):
|
||||
async def start_listening(self):
|
||||
"""Start listening the underlying entity"""
|
||||
|
||||
@overrides
|
||||
|
||||
@@ -86,7 +86,7 @@ class FeatureMotionManager(BaseFeatureManager):
|
||||
self._motion_state = STATE_UNKNOWN
|
||||
|
||||
@overrides
|
||||
def start_listening(self):
|
||||
async def start_listening(self):
|
||||
"""Start listening the underlying entity"""
|
||||
if self._is_configured:
|
||||
self.stop_listening()
|
||||
|
||||
@@ -12,22 +12,15 @@ from homeassistant.const import (
|
||||
STATE_UNKNOWN,
|
||||
)
|
||||
|
||||
|
||||
from homeassistant.core import (
|
||||
HomeAssistant,
|
||||
callback,
|
||||
Event,
|
||||
)
|
||||
from homeassistant.helpers.event import (
|
||||
async_track_state_change_event,
|
||||
EventStateChangedData,
|
||||
)
|
||||
from homeassistant.components.climate import HVACMode
|
||||
|
||||
from .const import * # pylint: disable=wildcard-import, unused-wildcard-import
|
||||
from .commons import ConfigData
|
||||
|
||||
from .base_manager import BaseFeatureManager
|
||||
from .vtherm_api import VersatileThermostatAPI
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -50,194 +43,94 @@ class FeaturePowerManager(BaseFeatureManager):
|
||||
def __init__(self, vtherm: Any, hass: HomeAssistant):
|
||||
"""Init of a featureManager"""
|
||||
super().__init__(vtherm, hass)
|
||||
self._power_sensor_entity_id = None
|
||||
self._max_power_sensor_entity_id = None
|
||||
self._current_power = None
|
||||
self._current_max_power = None
|
||||
self._power_temp = None
|
||||
self._overpowering_state = STATE_UNAVAILABLE
|
||||
self._is_configured: bool = False
|
||||
self._device_power: float = 0
|
||||
self._use_power_feature: bool = False
|
||||
|
||||
@overrides
|
||||
def post_init(self, entry_infos: ConfigData):
|
||||
"""Reinit of the manager"""
|
||||
|
||||
# Power management
|
||||
self._power_sensor_entity_id = entry_infos.get(CONF_POWER_SENSOR)
|
||||
self._max_power_sensor_entity_id = entry_infos.get(CONF_MAX_POWER_SENSOR)
|
||||
self._power_temp = entry_infos.get(CONF_PRESET_POWER)
|
||||
|
||||
self._device_power = entry_infos.get(CONF_DEVICE_POWER) or 0
|
||||
self._use_power_feature = entry_infos.get(CONF_USE_POWER_FEATURE, False)
|
||||
self._is_configured = False
|
||||
self._current_power = None
|
||||
self._current_max_power = None
|
||||
if (
|
||||
entry_infos.get(CONF_USE_POWER_FEATURE, False)
|
||||
and self._max_power_sensor_entity_id
|
||||
and self._power_sensor_entity_id
|
||||
and self._device_power
|
||||
):
|
||||
|
||||
@overrides
|
||||
async 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:
|
||||
self._is_configured = True
|
||||
self._overpowering_state = STATE_UNKNOWN
|
||||
# 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
|
||||
else:
|
||||
_LOGGER.info("%s - Power management is not fully configured", self)
|
||||
|
||||
@overrides
|
||||
def start_listening(self):
|
||||
"""Start listening the underlying entity"""
|
||||
if self._is_configured:
|
||||
self.stop_listening()
|
||||
else:
|
||||
return
|
||||
|
||||
self.add_listener(
|
||||
async_track_state_change_event(
|
||||
self.hass,
|
||||
[self._power_sensor_entity_id],
|
||||
self._async_power_sensor_changed,
|
||||
)
|
||||
)
|
||||
|
||||
self.add_listener(
|
||||
async_track_state_change_event(
|
||||
self.hass,
|
||||
[self._max_power_sensor_entity_id],
|
||||
self._async_max_power_sensor_changed,
|
||||
)
|
||||
)
|
||||
|
||||
@overrides
|
||||
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
|
||||
current_power_state = self.hass.states.get(self._power_sensor_entity_id)
|
||||
if current_power_state and current_power_state.state not in (
|
||||
STATE_UNAVAILABLE,
|
||||
STATE_UNKNOWN,
|
||||
):
|
||||
self._current_power = float(current_power_state.state)
|
||||
_LOGGER.debug(
|
||||
"%s - Current power have been retrieved: %.3f",
|
||||
self,
|
||||
self._current_power,
|
||||
)
|
||||
ret = True
|
||||
|
||||
# Try to acquire power max
|
||||
current_power_max_state = self.hass.states.get(
|
||||
self._max_power_sensor_entity_id
|
||||
)
|
||||
if current_power_max_state and current_power_max_state.state not in (
|
||||
STATE_UNAVAILABLE,
|
||||
STATE_UNKNOWN,
|
||||
):
|
||||
self._current_max_power = float(current_power_max_state.state)
|
||||
_LOGGER.debug(
|
||||
"%s - Current power max have been retrieved: %.3f",
|
||||
self,
|
||||
self._current_max_power,
|
||||
)
|
||||
ret = True
|
||||
|
||||
return ret
|
||||
|
||||
@callback
|
||||
async def _async_power_sensor_changed(self, event: Event[EventStateChangedData]):
|
||||
"""Handle power changes."""
|
||||
_LOGGER.debug("Thermostat %s - Receive new Power event", self)
|
||||
_LOGGER.debug(event)
|
||||
new_state = event.data.get("new_state")
|
||||
old_state = event.data.get("old_state")
|
||||
if (
|
||||
new_state is None
|
||||
or new_state.state in (STATE_UNAVAILABLE, STATE_UNKNOWN)
|
||||
or (old_state is not None and new_state.state == old_state.state)
|
||||
):
|
||||
return
|
||||
|
||||
try:
|
||||
current_power = float(new_state.state)
|
||||
if math.isnan(current_power) or math.isinf(current_power):
|
||||
raise ValueError(f"Sensor has illegal state {new_state.state}")
|
||||
self._current_power = current_power
|
||||
|
||||
if self._vtherm.preset_mode == PRESET_POWER:
|
||||
await self._vtherm.async_control_heating()
|
||||
|
||||
except ValueError as ex:
|
||||
_LOGGER.error("Unable to update current_power from sensor: %s", ex)
|
||||
|
||||
@callback
|
||||
async def _async_max_power_sensor_changed(
|
||||
self, event: Event[EventStateChangedData]
|
||||
):
|
||||
"""Handle power max changes."""
|
||||
_LOGGER.debug("Thermostat %s - Receive new Power Max event", self.name)
|
||||
_LOGGER.debug(event)
|
||||
new_state = event.data.get("new_state")
|
||||
old_state = event.data.get("old_state")
|
||||
if (
|
||||
new_state is None
|
||||
or new_state.state in (STATE_UNAVAILABLE, STATE_UNKNOWN)
|
||||
or (old_state is not None and new_state.state == old_state.state)
|
||||
):
|
||||
return
|
||||
|
||||
try:
|
||||
current_power_max = float(new_state.state)
|
||||
if math.isnan(current_power_max) or math.isinf(current_power_max):
|
||||
raise ValueError(f"Sensor has illegal state {new_state.state}")
|
||||
self._current_max_power = current_power_max
|
||||
if self._vtherm.preset_mode == PRESET_POWER:
|
||||
await self._vtherm.async_control_heating()
|
||||
|
||||
except ValueError as ex:
|
||||
_LOGGER.error("Unable to update current_power from sensor: %s", ex)
|
||||
if self._use_power_feature:
|
||||
if not central_power_configuration:
|
||||
_LOGGER.warning(
|
||||
"%s - Power management is not fully configured. You have to configure the central configuration power",
|
||||
self,
|
||||
)
|
||||
else:
|
||||
_LOGGER.warning(
|
||||
"%s - Power management is not fully configured. You have to configure the power feature of the VTherm",
|
||||
self,
|
||||
)
|
||||
|
||||
def add_custom_attributes(self, extra_state_attributes: dict[str, Any]):
|
||||
"""Add some custom attributes"""
|
||||
vtherm_api = VersatileThermostatAPI.get_vtherm_api()
|
||||
extra_state_attributes.update(
|
||||
{
|
||||
"power_sensor_entity_id": self._power_sensor_entity_id,
|
||||
"max_power_sensor_entity_id": self._max_power_sensor_entity_id,
|
||||
"power_sensor_entity_id": vtherm_api.central_power_manager.power_sensor_entity_id,
|
||||
"max_power_sensor_entity_id": vtherm_api.central_power_manager.max_power_sensor_entity_id,
|
||||
"overpowering_state": self._overpowering_state,
|
||||
"is_power_configured": self._is_configured,
|
||||
"device_power": self._device_power,
|
||||
"power_temp": self._power_temp,
|
||||
"current_power": self._current_power,
|
||||
"current_max_power": self._current_max_power,
|
||||
"current_power": vtherm_api.central_power_manager.current_power,
|
||||
"current_max_power": vtherm_api.central_power_manager.current_max_power,
|
||||
"mean_cycle_power": self.mean_cycle_power,
|
||||
}
|
||||
)
|
||||
|
||||
async def check_overpowering(self) -> bool:
|
||||
"""Check the overpowering condition
|
||||
Turn the preset_mode of the heater to 'power' if power conditions are exceeded
|
||||
Returns True if overpowering is 'on'
|
||||
async def check_power_available(self) -> bool:
|
||||
"""Check if the Vtherm can be started considering overpowering.
|
||||
Returns True if no overpowering conditions are found
|
||||
"""
|
||||
|
||||
if not self._is_configured:
|
||||
return False
|
||||
|
||||
vtherm_api = VersatileThermostatAPI.get_vtherm_api()
|
||||
if (
|
||||
self._current_power is None
|
||||
not self._is_configured
|
||||
or not vtherm_api.central_power_manager.is_configured
|
||||
):
|
||||
return True
|
||||
|
||||
current_power = vtherm_api.central_power_manager.current_power
|
||||
current_max_power = vtherm_api.central_power_manager.current_max_power
|
||||
if (
|
||||
current_power is None
|
||||
or current_max_power is None
|
||||
or self._device_power is None
|
||||
or self._current_max_power is None
|
||||
):
|
||||
_LOGGER.warning(
|
||||
"%s - power not valued. check_overpowering not available", self
|
||||
"%s - power not valued. check_power_available not available", self
|
||||
)
|
||||
return False
|
||||
return True
|
||||
|
||||
_LOGGER.debug(
|
||||
"%s - overpowering check: power=%.3f, max_power=%.3f heater power=%.3f",
|
||||
self,
|
||||
self._current_power,
|
||||
self._current_max_power,
|
||||
current_power,
|
||||
current_max_power,
|
||||
self._device_power,
|
||||
)
|
||||
|
||||
@@ -253,62 +146,78 @@ class FeaturePowerManager(BaseFeatureManager):
|
||||
self._device_power * self._vtherm.proportional_algorithm.on_percent,
|
||||
)
|
||||
|
||||
ret = (self._current_power + power_consumption_max) >= self._current_max_power
|
||||
if (
|
||||
self._overpowering_state == STATE_OFF
|
||||
and ret
|
||||
and self._vtherm.hvac_mode != HVACMode.OFF
|
||||
):
|
||||
ret = (current_power + power_consumption_max) < current_max_power
|
||||
if not ret:
|
||||
_LOGGER.info(
|
||||
"%s - there is not enough power available power=%.3f, max_power=%.3f heater power=%.3f",
|
||||
self,
|
||||
current_power,
|
||||
current_max_power,
|
||||
self._device_power,
|
||||
)
|
||||
return ret
|
||||
|
||||
async def set_overpowering(self, overpowering: bool, power_consumption_max=0):
|
||||
"""Force the overpowering state for the VTherm"""
|
||||
|
||||
vtherm_api = VersatileThermostatAPI.get_vtherm_api()
|
||||
current_power = vtherm_api.central_power_manager.current_power
|
||||
current_max_power = vtherm_api.central_power_manager.current_max_power
|
||||
|
||||
if overpowering and not self.is_overpowering_detected:
|
||||
_LOGGER.warning(
|
||||
"%s - overpowering is detected. Heater preset will be set to 'power'",
|
||||
self,
|
||||
)
|
||||
|
||||
self._overpowering_state = STATE_ON
|
||||
|
||||
if self._vtherm.is_over_climate:
|
||||
self._vtherm.save_hvac_mode()
|
||||
|
||||
self._vtherm.save_preset_mode()
|
||||
await self._vtherm.async_underlying_entity_turn_off()
|
||||
await self._vtherm.async_set_preset_mode_internal(PRESET_POWER)
|
||||
await self._vtherm.async_set_preset_mode_internal(PRESET_POWER, force=True)
|
||||
self._vtherm.send_event(
|
||||
EventType.POWER_EVENT,
|
||||
{
|
||||
"type": "start",
|
||||
"current_power": self._current_power,
|
||||
"current_power": current_power,
|
||||
"device_power": self._device_power,
|
||||
"current_max_power": self._current_max_power,
|
||||
"current_max_power": current_max_power,
|
||||
"current_power_consumption": power_consumption_max,
|
||||
},
|
||||
)
|
||||
|
||||
# Check if we need to remove the POWER preset
|
||||
if (
|
||||
self._overpowering_state == STATE_ON
|
||||
and not ret
|
||||
and self._vtherm.preset_mode == PRESET_POWER
|
||||
):
|
||||
elif not overpowering and self.is_overpowering_detected:
|
||||
_LOGGER.warning(
|
||||
"%s - end of overpowering is detected. Heater preset will be restored to '%s'",
|
||||
self,
|
||||
self._vtherm._saved_preset_mode, # pylint: disable=protected-access
|
||||
)
|
||||
self._overpowering_state = STATE_OFF
|
||||
|
||||
# restore state
|
||||
if self._vtherm.is_over_climate:
|
||||
await self._vtherm.restore_hvac_mode(False)
|
||||
await self._vtherm.restore_hvac_mode()
|
||||
await self._vtherm.restore_preset_mode()
|
||||
# restart cycle
|
||||
await self._vtherm.async_control_heating(force=True)
|
||||
self._vtherm.send_event(
|
||||
EventType.POWER_EVENT,
|
||||
{
|
||||
"type": "end",
|
||||
"current_power": self._current_power,
|
||||
"current_power": current_power,
|
||||
"device_power": self._device_power,
|
||||
"current_max_power": self._current_max_power,
|
||||
"current_max_power": current_max_power,
|
||||
},
|
||||
)
|
||||
|
||||
new_overpowering_state = STATE_ON if ret else STATE_OFF
|
||||
if self._overpowering_state != new_overpowering_state:
|
||||
self._overpowering_state = new_overpowering_state
|
||||
self._vtherm.update_custom_attributes()
|
||||
|
||||
return self._overpowering_state == STATE_ON
|
||||
elif not overpowering and self._overpowering_state != STATE_OFF:
|
||||
# just set to not overpowering the state which was not set
|
||||
self._overpowering_state = STATE_OFF
|
||||
else:
|
||||
# Nothing to do (already in the right state)
|
||||
return
|
||||
self._vtherm.update_custom_attributes()
|
||||
|
||||
@overrides
|
||||
@property
|
||||
@@ -325,14 +234,9 @@ class FeaturePowerManager(BaseFeatureManager):
|
||||
return self._overpowering_state
|
||||
|
||||
@property
|
||||
def max_power_sensor_entity_id(self) -> bool:
|
||||
"""Return the power max entity id"""
|
||||
return self._max_power_sensor_entity_id
|
||||
|
||||
@property
|
||||
def power_sensor_entity_id(self) -> bool:
|
||||
"""Return the power entity id"""
|
||||
return self._power_sensor_entity_id
|
||||
def is_overpowering_detected(self) -> str | None:
|
||||
"""Return True if the Vtherm is in overpowering state"""
|
||||
return self._overpowering_state == STATE_ON
|
||||
|
||||
@property
|
||||
def power_temperature(self) -> bool:
|
||||
@@ -344,16 +248,6 @@ class FeaturePowerManager(BaseFeatureManager):
|
||||
"""Return the device power"""
|
||||
return self._device_power
|
||||
|
||||
@property
|
||||
def current_power(self) -> bool:
|
||||
"""Return the current power from sensor"""
|
||||
return self._current_power
|
||||
|
||||
@property
|
||||
def current_max_power(self) -> bool:
|
||||
"""Return the current power from sensor"""
|
||||
return self._current_max_power
|
||||
|
||||
@property
|
||||
def mean_cycle_power(self) -> float | None:
|
||||
"""Returns the mean power consumption during the cycle"""
|
||||
|
||||
@@ -23,12 +23,7 @@ from homeassistant.helpers.event import (
|
||||
EventStateChangedData,
|
||||
)
|
||||
|
||||
from homeassistant.components.climate import (
|
||||
PRESET_ACTIVITY,
|
||||
PRESET_BOOST,
|
||||
PRESET_COMFORT,
|
||||
PRESET_ECO,
|
||||
)
|
||||
from homeassistant.components.climate import PRESET_ACTIVITY, PRESET_BOOST, PRESET_COMFORT, PRESET_ECO
|
||||
|
||||
from .const import * # pylint: disable=wildcard-import, unused-wildcard-import
|
||||
from .commons import ConfigData
|
||||
@@ -67,7 +62,7 @@ class FeaturePresenceManager(BaseFeatureManager):
|
||||
self._presence_state = STATE_UNKNOWN
|
||||
|
||||
@overrides
|
||||
def start_listening(self):
|
||||
async def start_listening(self):
|
||||
"""Start listening the underlying entity"""
|
||||
if self._is_configured:
|
||||
self.stop_listening()
|
||||
@@ -146,6 +141,7 @@ class FeaturePresenceManager(BaseFeatureManager):
|
||||
PRESET_COMFORT,
|
||||
PRESET_ECO,
|
||||
PRESET_ACTIVITY,
|
||||
PRESET_FROST_PROTECTION,
|
||||
]:
|
||||
return old_presence_state != self._presence_state
|
||||
|
||||
|
||||
@@ -70,7 +70,7 @@ class FeatureSafetyManager(BaseFeatureManager):
|
||||
self._is_configured = True
|
||||
|
||||
@overrides
|
||||
def start_listening(self):
|
||||
async def start_listening(self):
|
||||
"""Start listening the underlying entity"""
|
||||
|
||||
@overrides
|
||||
|
||||
@@ -46,6 +46,7 @@ class FeatureWindowManager(BaseFeatureManager):
|
||||
"is_window_configured",
|
||||
"is_window_bypass",
|
||||
"window_delay_sec",
|
||||
"window_off_delay_sec",
|
||||
"window_auto_configured",
|
||||
"window_auto_open_threshold",
|
||||
"window_auto_close_threshold",
|
||||
@@ -67,6 +68,7 @@ class FeatureWindowManager(BaseFeatureManager):
|
||||
self._is_window_bypass: bool = False
|
||||
self._window_action: str = None
|
||||
self._window_delay_sec: int | None = 0
|
||||
self._window_off_delay_sec: int | None = 0
|
||||
self._is_configured: bool = False
|
||||
self._is_window_auto_configured: bool = False
|
||||
self._window_call_cancel: callable = None
|
||||
@@ -81,6 +83,8 @@ class FeatureWindowManager(BaseFeatureManager):
|
||||
|
||||
self._window_sensor_entity_id = entry_infos.get(CONF_WINDOW_SENSOR)
|
||||
self._window_delay_sec = entry_infos.get(CONF_WINDOW_DELAY)
|
||||
# default is the WINDOW_ON delay if not configured
|
||||
self._window_off_delay_sec = entry_infos.get(CONF_WINDOW_OFF_DELAY, self._window_delay_sec)
|
||||
|
||||
self._window_action = (
|
||||
entry_infos.get(CONF_WINDOW_ACTION) or CONF_WINDOW_TURN_OFF
|
||||
@@ -124,7 +128,7 @@ class FeatureWindowManager(BaseFeatureManager):
|
||||
self._window_state = STATE_UNKNOWN
|
||||
|
||||
@overrides
|
||||
def start_listening(self):
|
||||
async def start_listening(self):
|
||||
"""Start listening the underlying entity"""
|
||||
if self._is_configured:
|
||||
self.stop_listening()
|
||||
@@ -191,7 +195,7 @@ class FeatureWindowManager(BaseFeatureManager):
|
||||
self._hass,
|
||||
self._window_sensor_entity_id,
|
||||
new_state.state,
|
||||
timedelta(seconds=self._window_delay_sec),
|
||||
timedelta(seconds=delay),
|
||||
)
|
||||
except ConditionError:
|
||||
long_enough = False
|
||||
@@ -221,13 +225,12 @@ class FeatureWindowManager(BaseFeatureManager):
|
||||
|
||||
self._vtherm.update_custom_attributes()
|
||||
|
||||
delay = self._window_delay_sec if new_state.state == STATE_ON else self._window_off_delay_sec
|
||||
if new_state is None or old_state is None or new_state.state == old_state.state:
|
||||
return try_window_condition
|
||||
|
||||
self.dearm_window_timer()
|
||||
self._window_call_cancel = async_call_later(
|
||||
self.hass, timedelta(seconds=self._window_delay_sec), try_window_condition
|
||||
)
|
||||
self._window_call_cancel = async_call_later(self.hass, timedelta(seconds=delay), try_window_condition)
|
||||
# For testing purpose we need to access the inner function
|
||||
return try_window_condition
|
||||
|
||||
@@ -433,6 +436,7 @@ class FeatureWindowManager(BaseFeatureManager):
|
||||
"is_window_bypass": self._is_window_bypass,
|
||||
"window_sensor_entity_id": self._window_sensor_entity_id,
|
||||
"window_delay_sec": self._window_delay_sec,
|
||||
"window_off_delay_sec": self._window_off_delay_sec,
|
||||
"is_window_configured": self._is_configured,
|
||||
"is_window_auto_configured": self._is_window_auto_configured,
|
||||
"window_auto_open_threshold": self._window_auto_open_threshold,
|
||||
@@ -512,9 +516,14 @@ class FeatureWindowManager(BaseFeatureManager):
|
||||
|
||||
@property
|
||||
def window_delay_sec(self) -> bool:
|
||||
"""Return the motion delay"""
|
||||
"""Return the window on delay"""
|
||||
return self._window_delay_sec
|
||||
|
||||
@property
|
||||
def window_off_delay_sec(self) -> bool:
|
||||
"""Return the window off delay"""
|
||||
return self._window_off_delay_sec
|
||||
|
||||
@property
|
||||
def window_action(self) -> bool:
|
||||
"""Return the window action"""
|
||||
|
||||
@@ -14,6 +14,6 @@
|
||||
"quality_scale": "silver",
|
||||
"requirements": [],
|
||||
"ssdp": [],
|
||||
"version": "7.0.0",
|
||||
"version": "7.1.5",
|
||||
"zeroconf": []
|
||||
}
|
||||
@@ -303,6 +303,10 @@ class ValveOpenPercentSensor(VersatileThermostatBaseEntity, SensorEntity):
|
||||
"""Called when my climate have change"""
|
||||
# _LOGGER.debug("%s - climate state change", self._attr_unique_id)
|
||||
|
||||
if not self.my_climate or not hasattr(self.my_climate, "valve_open_percent"):
|
||||
_LOGGER.warning("%s - my_climate not found or no valve_open_percent property found. This could be normal at startup. Ignore the underlying device change.", self)
|
||||
return
|
||||
|
||||
old_state = self._attr_native_value
|
||||
self._attr_native_value = self.my_climate.valve_open_percent
|
||||
if old_state != self._attr_native_value:
|
||||
|
||||
@@ -123,7 +123,8 @@
|
||||
"description": "Open window management.\nYou can also configure automatic window open detection based on temperature decrease",
|
||||
"data": {
|
||||
"window_sensor_entity_id": "Window sensor entity id",
|
||||
"window_delay": "Window sensor delay (seconds)",
|
||||
"window_delay": "Window sensor 'on' delay (seconds)",
|
||||
"window_off_delay": "Window sensor 'off' delay (seconds)",
|
||||
"window_auto_open_threshold": "Temperature decrease threshold for automatic window open detection (in °/hours)",
|
||||
"window_auto_close_threshold": "Temperature increase threshold for end of automatic detection (in °/hours)",
|
||||
"window_auto_max_duration": "Maximum duration of automatic window open detection (in min)",
|
||||
@@ -132,7 +133,8 @@
|
||||
},
|
||||
"data_description": {
|
||||
"window_sensor_entity_id": "Leave empty if no window sensor should be used and to use the automatic detection",
|
||||
"window_delay": "The delay in seconds before sensor detection is taken into account",
|
||||
"window_delay": "The delay in seconds before sensor 'on' detection is taken into account",
|
||||
"window_off_delay": "The delay in seconds before sensor 'off' detection is taken into account. Leave it empty to use the same value as window on delay",
|
||||
"window_auto_open_threshold": "Recommended value: between 3 and 10. Leave empty if automatic window open detection is not used",
|
||||
"window_auto_close_threshold": "Recommended value: 0. Leave empty if automatic window open detection is not used",
|
||||
"window_auto_max_duration": "Recommended value: 60 (one hour). Leave empty if automatic window open detection is not used",
|
||||
@@ -228,11 +230,11 @@
|
||||
"min_opening_degrees": "Min opening degrees"
|
||||
},
|
||||
"data_description": {
|
||||
"offset_calibration_entity_ids": "The list of the 'offset calibration' entities. Set it if your TRV have the entity for better regulation. There should be one per underlying climate entities",
|
||||
"offset_calibration_entity_ids": "The list of the 'offset calibration' entities. Set it if your TRV has the entity for better regulation. There should be one per underlying climate entities",
|
||||
"opening_degree_entity_ids": "The list of the 'opening degree' entities. There should be one per underlying climate entities",
|
||||
"closing_degree_entity_ids": "The list of the 'closing degree' entities. Set it if your TRV have the entity for better regulation. There should be one per underlying climate entities",
|
||||
"closing_degree_entity_ids": "The list of the 'closing degree' entities. Set it if your TRV 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)",
|
||||
"min_opening_degrees": "A comma seperated list of minimal opening degrees. Default to 0. Example: 20, 25, 30"
|
||||
"min_opening_degrees": "Opening degree minimum value for each underlying device, comma separated. Default to 0. Example: 20, 25, 30"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -369,7 +371,8 @@
|
||||
"description": "Open window management.\nYou can also configure automatic window open detection based on temperature decrease",
|
||||
"data": {
|
||||
"window_sensor_entity_id": "Window sensor entity id",
|
||||
"window_delay": "Window sensor delay (seconds)",
|
||||
"window_delay": "Window sensor 'on' delay (seconds)",
|
||||
"window_off_delay": "Window sensor 'off' delay (seconds)",
|
||||
"window_auto_open_threshold": "Temperature decrease threshold for automatic window open detection (in °/hours)",
|
||||
"window_auto_close_threshold": "Temperature increase threshold for end of automatic detection (in °/hours)",
|
||||
"window_auto_max_duration": "Maximum duration of automatic window open detection (in min)",
|
||||
@@ -378,8 +381,8 @@
|
||||
},
|
||||
"data_description": {
|
||||
"window_sensor_entity_id": "Leave empty if no window sensor should be used and to use the automatic detection",
|
||||
"window_delay": "The delay in seconds before sensor detection is taken into account",
|
||||
"window_auto_open_threshold": "Recommended value: between 3 and 10. Leave empty if automatic window open detection is not used",
|
||||
"window_delay": "The delay in seconds before sensor 'on' detection is taken into account",
|
||||
"window_off_delay": "The delay in seconds before sensor 'off' detection is taken into account. Leave it empty to use the same value as window on delay",
|
||||
"window_auto_close_threshold": "Recommended value: 0. Leave empty if automatic window open detection is not used",
|
||||
"window_auto_max_duration": "Recommended value: 60 (one hour). Leave empty if automatic window open detection is not used",
|
||||
"use_window_central_config": "Check to use the central window configuration. Uncheck to use a specific window configuration for this VTherm",
|
||||
@@ -474,11 +477,11 @@
|
||||
"min_opening_degrees": "Min opening degrees"
|
||||
},
|
||||
"data_description": {
|
||||
"offset_calibration_entity_ids": "The list of the 'offset calibration' entities. Set it if your TRV have the entity for better regulation. There should be one per underlying climate entities",
|
||||
"offset_calibration_entity_ids": "The list of the 'offset calibration' entities. Set it if your TRV has the entity for better regulation. There should be one per underlying climate entities",
|
||||
"opening_degree_entity_ids": "The list of the 'opening degree' entities. There should be one per underlying climate entities",
|
||||
"closing_degree_entity_ids": "The list of the 'closing degree' entities. Set it if your TRV have the entity for better regulation. There should be one per underlying climate entities",
|
||||
"closing_degree_entity_ids": "The list of the 'closing degree' entities. Set it if your TRV 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)",
|
||||
"min_opening_degrees": "A comma seperated list of minimal opening degrees. Default to 0. Example: 20, 25, 30"
|
||||
"min_opening_degrees": "Opening degree minimum value for each underlying device, comma separated. Default to 0. Example: 20, 25, 30"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import logging
|
||||
from datetime import timedelta, datetime
|
||||
|
||||
from homeassistant.const import STATE_ON
|
||||
from homeassistant.const import STATE_ON, STATE_UNAVAILABLE, STATE_UNKNOWN
|
||||
from homeassistant.core import Event, HomeAssistant, State, callback
|
||||
from homeassistant.helpers.event import (
|
||||
async_track_state_change_event,
|
||||
@@ -614,7 +614,7 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
|
||||
return
|
||||
|
||||
# Find the underlying which have change
|
||||
under = self.find_underlying_by_entity_id(new_state.entity_id)
|
||||
under: UnderlyingClimate = self.find_underlying_by_entity_id(new_state.entity_id)
|
||||
|
||||
if not under:
|
||||
_LOGGER.warning(
|
||||
@@ -626,6 +626,16 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
|
||||
new_hvac_mode = new_state.state
|
||||
|
||||
old_state = event.data.get("old_state")
|
||||
|
||||
# Issue #829 - refresh underlying command if it comes back to life
|
||||
if old_state is not None and new_state.state not in (STATE_UNAVAILABLE, STATE_UNKNOWN) and old_state.state in (STATE_UNAVAILABLE, STATE_UNKNOWN):
|
||||
_LOGGER.warning("%s - underlying %s come back to life. New state=%s, old_state=%s. Will refresh its status", self, under.entity_id, new_state.state, old_state.state)
|
||||
# Force hvac_mode and target temperature
|
||||
await under.set_hvac_mode(self.hvac_mode)
|
||||
await self._send_regulated_temperature(force=True)
|
||||
|
||||
return
|
||||
|
||||
old_hvac_action = (
|
||||
old_state.attributes.get("hvac_action")
|
||||
if old_state and old_state.attributes
|
||||
@@ -833,7 +843,8 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
|
||||
and under.last_sent_temperature is not None
|
||||
):
|
||||
_LOGGER.debug(
|
||||
"Do temperature check. under.last_sent_temperature is %s, new_target_temp is %s",
|
||||
"%s - Do temperature check. under.last_sent_temperature is %s, new_target_temp is %s",
|
||||
self,
|
||||
under.last_sent_temperature,
|
||||
new_target_temp,
|
||||
)
|
||||
|
||||
@@ -255,6 +255,13 @@ class ThermostatOverClimateValve(ThermostatOverClimate):
|
||||
self._attr_min_temp,
|
||||
)
|
||||
|
||||
self._last_regulation_change = self.now
|
||||
self.reset_last_change_time_from_vtherm()
|
||||
|
||||
_LOGGER.debug(
|
||||
"%s - last_regulation_change is now: %s and last_change_from_vtherm is now: %s", self, self._last_regulation_change, self._last_change_time_from_vtherm
|
||||
) # pylint: disable=protected-access
|
||||
|
||||
for under in self._underlyings_valve_regulation:
|
||||
await under.set_valve_open_percent()
|
||||
|
||||
@@ -263,19 +270,6 @@ 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"""
|
||||
# return self._hvac_list
|
||||
|
||||
@property
|
||||
def valve_open_percent(self) -> int:
|
||||
"""Gives the percentage of valve needed"""
|
||||
|
||||
@@ -26,23 +26,21 @@ _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(
|
||||
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( # 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",
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
@@ -61,14 +59,6 @@ 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"""
|
||||
|
||||
@@ -123,7 +123,8 @@
|
||||
"description": "Open window management.\nYou can also configure automatic window open detection based on temperature decrease",
|
||||
"data": {
|
||||
"window_sensor_entity_id": "Window sensor entity id",
|
||||
"window_delay": "Window sensor delay (seconds)",
|
||||
"window_delay": "Window sensor 'on' delay (seconds)",
|
||||
"window_off_delay": "Window sensor 'off' delay (seconds)",
|
||||
"window_auto_open_threshold": "Temperature decrease threshold for automatic window open detection (in °/hours)",
|
||||
"window_auto_close_threshold": "Temperature increase threshold for end of automatic detection (in °/hours)",
|
||||
"window_auto_max_duration": "Maximum duration of automatic window open detection (in min)",
|
||||
@@ -132,7 +133,8 @@
|
||||
},
|
||||
"data_description": {
|
||||
"window_sensor_entity_id": "Leave empty if no window sensor should be used and to use the automatic detection",
|
||||
"window_delay": "The delay in seconds before sensor detection is taken into account",
|
||||
"window_delay": "The delay in seconds before sensor 'on' detection is taken into account",
|
||||
"window_off_delay": "The delay in seconds before sensor 'off' detection is taken into account. Leave it empty to use the same value as window on delay",
|
||||
"window_auto_open_threshold": "Recommended value: between 3 and 10. Leave empty if automatic window open detection is not used",
|
||||
"window_auto_close_threshold": "Recommended value: 0. Leave empty if automatic window open detection is not used",
|
||||
"window_auto_max_duration": "Recommended value: 60 (one hour). Leave empty if automatic window open detection is not used",
|
||||
@@ -228,9 +230,9 @@
|
||||
"min_opening_degrees": "Min opening degrees"
|
||||
},
|
||||
"data_description": {
|
||||
"offset_calibration_entity_ids": "The list of the 'offset calibration' entities. Set it if your TRV have the entity for better regulation. There should be one per underlying climate entities",
|
||||
"offset_calibration_entity_ids": "The list of the 'offset calibration' entities. Set it if your TRV has the entity for better regulation. There should be one per underlying climate entities",
|
||||
"opening_degree_entity_ids": "The list of the 'opening degree' entities. There should be one per underlying climate entities",
|
||||
"closing_degree_entity_ids": "The list of the 'closing degree' entities. Set it if your TRV have the entity for better regulation. There should be one per underlying climate entities",
|
||||
"closing_degree_entity_ids": "The list of the 'closing degree' entities. Set it if your TRV 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)",
|
||||
"min_opening_degrees": "A comma seperated list of minimal opening degrees. Default to 0. Example: 20, 25, 30"
|
||||
}
|
||||
@@ -369,7 +371,8 @@
|
||||
"description": "Open window management.\nYou can also configure automatic window open detection based on temperature decrease",
|
||||
"data": {
|
||||
"window_sensor_entity_id": "Window sensor entity id",
|
||||
"window_delay": "Window sensor delay (seconds)",
|
||||
"window_delay": "Window sensor 'on' delay (seconds)",
|
||||
"window_off_delay": "Window sensor 'off' delay (seconds)",
|
||||
"window_auto_open_threshold": "Temperature decrease threshold for automatic window open detection (in °/hours)",
|
||||
"window_auto_close_threshold": "Temperature increase threshold for end of automatic detection (in °/hours)",
|
||||
"window_auto_max_duration": "Maximum duration of automatic window open detection (in min)",
|
||||
@@ -378,8 +381,8 @@
|
||||
},
|
||||
"data_description": {
|
||||
"window_sensor_entity_id": "Leave empty if no window sensor should be used and to use the automatic detection",
|
||||
"window_delay": "The delay in seconds before sensor detection is taken into account",
|
||||
"window_auto_open_threshold": "Recommended value: between 3 and 10. Leave empty if automatic window open detection is not used",
|
||||
"window_delay": "The delay in seconds before sensor 'on' detection is taken into account",
|
||||
"window_off_delay": "The delay in seconds before sensor 'off' detection is taken into account. Leave it empty to use the same value as window on delay",
|
||||
"window_auto_close_threshold": "Recommended value: 0. Leave empty if automatic window open detection is not used",
|
||||
"window_auto_max_duration": "Recommended value: 60 (one hour). Leave empty if automatic window open detection is not used",
|
||||
"use_window_central_config": "Check to use the central window configuration. Uncheck to use a specific window configuration for this VTherm",
|
||||
@@ -474,9 +477,9 @@
|
||||
"min_opening_degrees": "Min opening degrees"
|
||||
},
|
||||
"data_description": {
|
||||
"offset_calibration_entity_ids": "The list of the 'offset calibration' entities. Set it if your TRV have the entity for better regulation. There should be one per underlying climate entities",
|
||||
"offset_calibration_entity_ids": "The list of the 'offset calibration' entities. Set it if your TRV has the entity for better regulation. There should be one per underlying climate entities",
|
||||
"opening_degree_entity_ids": "The list of the 'opening degree' entities. There should be one per underlying climate entities",
|
||||
"closing_degree_entity_ids": "The list of the 'closing degree' entities. Set it if your TRV have the entity for better regulation. There should be one per underlying climate entities",
|
||||
"closing_degree_entity_ids": "The list of the 'closing degree' entities. Set it if your TRV 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)",
|
||||
"min_opening_degrees": "A comma seperated list of minimal opening degrees. Default to 0. Example: 20, 25, 30"
|
||||
}
|
||||
@@ -488,7 +491,8 @@
|
||||
"window_open_detection_method": "Only one window open detection method should be used. Use either window sensor or automatic detection through temperature threshold but not both",
|
||||
"no_central_config": "You cannot check 'use central configuration' because no central configuration was found. You need to create a Versatile Thermostat of type 'Central Configuration' to use it.",
|
||||
"service_configuration_format": "The format of the service configuration is wrong",
|
||||
"valve_regulation_nb_entities_incorrect": "The number of valve entities for valve regulation should be equal to the number of underlyings"
|
||||
"valve_regulation_nb_entities_incorrect": "The number of valve entities for valve regulation should be equal to the number of underlyings",
|
||||
"min_opening_degrees_format": "A comma separated list of positive integer is expected. Example: 20, 25, 30"
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "Device is already configured"
|
||||
|
||||
@@ -123,7 +123,8 @@
|
||||
"description": "Coupe le radiateur si l'ouverture est ouverte.\nLaissez l'id d'entité vide pour utiliser la détection automatique.",
|
||||
"data": {
|
||||
"window_sensor_entity_id": "Détecteur d'ouverture (entity id)",
|
||||
"window_delay": "Délai avant extinction (secondes)",
|
||||
"window_delay": "Délai de prise en compte à l'ouverture (secondes)",
|
||||
"window_off_delay": "Délai de prise compte à la fermeture (secondes)",
|
||||
"window_auto_open_threshold": "Seuil haut de chute de température pour la détection automatique (en °/heure)",
|
||||
"window_auto_close_threshold": "Seuil bas de chute de température pour la fin de détection automatique (en °/heure)",
|
||||
"window_auto_max_duration": "Durée maximum d'une extinction automatique (en min)",
|
||||
@@ -132,7 +133,8 @@
|
||||
},
|
||||
"data_description": {
|
||||
"window_sensor_entity_id": "Laissez vide si vous n'avez de détecteur et pour utiliser la détection automatique",
|
||||
"window_delay": "Le délai (en secondes) avant que le changement du détecteur soit pris en compte",
|
||||
"window_delay": "Le délai (en secondes) avant que le changement du détecteur soit pris en compte lors de la détection d'une ouverture",
|
||||
"window_off_delay": "Le délai (en secondes) avant que le changement du détecteur soit pris en compte lors de la détection d'une fermeture. Laissez vide pour utiliser le même délai à l'ouveture et à la fermeture",
|
||||
"window_auto_open_threshold": "Valeur recommandée: entre 3 et 10. Laissez vide si vous n'utilisez pas la détection automatique",
|
||||
"window_auto_close_threshold": "Valeur recommandée: 0. Laissez vide si vous n'utilisez pas la détection automatique",
|
||||
"window_auto_max_duration": "Valeur recommandée: 60 (1 heure). Laissez vide si vous n'utilisez pas la détection automatique",
|
||||
@@ -364,6 +366,7 @@
|
||||
"data": {
|
||||
"window_sensor_entity_id": "Détecteur d'ouverture (entity id)",
|
||||
"window_delay": "Délai avant extinction (secondes)",
|
||||
"window_off_delay": "Délai de prise compte à la fermeture (secondes)",
|
||||
"window_auto_open_threshold": "Seuil haut de chute de température pour la détection automatique (en °/heure)",
|
||||
"window_auto_close_threshold": "Seuil bas de chute de température pour la fin de détection automatique (en °/heure)",
|
||||
"window_auto_max_duration": "Durée maximum d'une extinction automatique (en min)",
|
||||
@@ -373,6 +376,7 @@
|
||||
"data_description": {
|
||||
"window_sensor_entity_id": "Laissez vide si vous n'avez de détecteur et pour utiliser la détection automatique",
|
||||
"window_delay": "Le délai (en secondes) avant que le changement du détecteur soit pris en compte",
|
||||
"window_off_delay": "Le délai (en secondes) avant que le changement du détecteur soit pris en compte lors de la détection d'une fermeture. Laissez vide pour utiliser le même délai à l'ouveture et à la fermeture",
|
||||
"window_auto_open_threshold": "Valeur recommandée: entre 3 et 10. Laissez vide si vous n'utilisez pas la détection automatique",
|
||||
"window_auto_close_threshold": "Valeur recommandée: 0. Laissez vide si vous n'utilisez pas la détection automatique",
|
||||
"window_auto_max_duration": "Valeur recommandée: 60 (1 heure). Laissez vide si vous n'utilisez pas la détection automatique",
|
||||
@@ -495,7 +499,7 @@
|
||||
"thermostat_central_config": "Configuration centrale",
|
||||
"thermostat_over_switch": "Thermostat sur un switch",
|
||||
"thermostat_over_climate": "Thermostat sur un autre thermostat",
|
||||
"thermostat_over_valve": "Thermostat sur une valve"
|
||||
"thermostat_over_valve": "Thermostat sur une vanne"
|
||||
}
|
||||
},
|
||||
"auto_regulation_mode": {
|
||||
|
||||
@@ -190,6 +190,21 @@ class UnderlyingEntity:
|
||||
"""capping of the value send to the underlying eqt"""
|
||||
return value
|
||||
|
||||
async def turn_off_and_cancel_cycle(self):
|
||||
"""Turn off and cancel eventual running cycle"""
|
||||
self._cancel_cycle()
|
||||
await self.turn_off()
|
||||
|
||||
async def check_overpowering(self) -> bool:
|
||||
"""Check that a underlying can be turned on, else
|
||||
activate the overpowering state of the VTherm associated.
|
||||
Returns True if the check is ok (no overpowering needed)"""
|
||||
if not await self._thermostat.power_manager.check_power_available():
|
||||
_LOGGER.debug("%s - overpowering is detected", self)
|
||||
await self._thermostat.power_manager.set_overpowering(True)
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
class UnderlyingSwitch(UnderlyingEntity):
|
||||
"""Represent a underlying switch"""
|
||||
@@ -313,6 +328,10 @@ class UnderlyingSwitch(UnderlyingEntity):
|
||||
"""Turn heater toggleable device on."""
|
||||
self._keep_alive.cancel() # Cancel early to avoid a turn_on/turn_off race condition
|
||||
_LOGGER.debug("%s - Starting underlying entity %s", self, self._entity_id)
|
||||
|
||||
if not await self.check_overpowering():
|
||||
return False
|
||||
|
||||
command = SERVICE_TURN_ON if not self.is_inversed else SERVICE_TURN_OFF
|
||||
domain = self._entity_id.split(".")[0]
|
||||
try:
|
||||
@@ -320,6 +339,7 @@ class UnderlyingSwitch(UnderlyingEntity):
|
||||
data = {ATTR_ENTITY_ID: self._entity_id}
|
||||
await self._hass.services.async_call(domain, command, data)
|
||||
self._keep_alive.set_async_action(self._keep_alive_callback)
|
||||
return True
|
||||
except Exception:
|
||||
self._keep_alive.cancel()
|
||||
raise
|
||||
@@ -409,9 +429,6 @@ class UnderlyingSwitch(UnderlyingEntity):
|
||||
await self.turn_off()
|
||||
return
|
||||
|
||||
if await self._thermostat.power_manager.check_overpowering():
|
||||
_LOGGER.debug("%s - End of cycle (3)", self)
|
||||
return
|
||||
# safety mode could have change the on_time percent
|
||||
await self._thermostat.safety_manager.refresh_state()
|
||||
time = self._on_time_sec
|
||||
@@ -426,7 +443,8 @@ class UnderlyingSwitch(UnderlyingEntity):
|
||||
time // 60,
|
||||
time % 60,
|
||||
)
|
||||
await self.turn_on()
|
||||
if not await self.turn_on():
|
||||
return
|
||||
else:
|
||||
_LOGGER.debug("%s - No action on heater cause duration is 0", self)
|
||||
self._async_cancel_cycle = self.call_later(
|
||||
@@ -551,6 +569,10 @@ class UnderlyingClimate(UnderlyingEntity):
|
||||
)
|
||||
return False
|
||||
|
||||
# When turning on a climate, check that power is available
|
||||
if hvac_mode in (HVACMode.HEAT, HVACMode.COOL) and not await self.check_overpowering():
|
||||
return False
|
||||
|
||||
data = {ATTR_ENTITY_ID: self._entity_id, "hvac_mode": hvac_mode}
|
||||
await self._hass.services.async_call(
|
||||
CLIMATE_DOMAIN,
|
||||
@@ -626,22 +648,23 @@ class UnderlyingClimate(UnderlyingEntity):
|
||||
|
||||
# Issue 508 we have to take care of service set_temperature or set_range
|
||||
target_temp = self.cap_sent_value(temperature)
|
||||
if (
|
||||
ClimateEntityFeature.TARGET_TEMPERATURE_RANGE
|
||||
in self._underlying_climate.supported_features
|
||||
):
|
||||
data = {
|
||||
ATTR_ENTITY_ID: self._entity_id,
|
||||
"target_temp_high": target_temp,
|
||||
"target_temp_low": target_temp,
|
||||
# issue 518 - we should send also the target temperature, even in TARGET RANGE
|
||||
"temperature": target_temp,
|
||||
}
|
||||
else:
|
||||
data = {
|
||||
ATTR_ENTITY_ID: self._entity_id,
|
||||
"temperature": target_temp,
|
||||
}
|
||||
data = {
|
||||
ATTR_ENTITY_ID: self._entity_id,
|
||||
}
|
||||
|
||||
_LOGGER.info("%s - Set setpoint temperature to: %s", self, target_temp)
|
||||
|
||||
# Issue 807 add TARGET_TEMPERATURE only if in the features
|
||||
if ClimateEntityFeature.TARGET_TEMPERATURE_RANGE in self._underlying_climate.supported_features:
|
||||
data.update(
|
||||
{
|
||||
"target_temp_high": target_temp,
|
||||
"target_temp_low": target_temp,
|
||||
}
|
||||
)
|
||||
|
||||
if ClimateEntityFeature.TARGET_TEMPERATURE in self._underlying_climate.supported_features:
|
||||
data["temperature"] = target_temp
|
||||
|
||||
await self._hass.services.async_call(
|
||||
CLIMATE_DOMAIN,
|
||||
@@ -650,6 +673,7 @@ class UnderlyingClimate(UnderlyingEntity):
|
||||
)
|
||||
|
||||
self._last_sent_temperature = target_temp
|
||||
_LOGGER.debug("%s - Last_sent_temperature is now: %s", self, self._last_sent_temperature)
|
||||
|
||||
@property
|
||||
def last_sent_temperature(self) -> float | None:
|
||||
@@ -1080,10 +1104,17 @@ class UnderlyingValveRegulation(UnderlyingValve):
|
||||
)
|
||||
return
|
||||
|
||||
# Send opening_degree
|
||||
if 0 < self._percent_open < self._min_opening_degree:
|
||||
self._percent_open = self._min_opening_degree
|
||||
# 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
|
||||
await super().send_percent_open()
|
||||
|
||||
# Send closing_degree if set
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
""" The API of Versatile Thermostat"""
|
||||
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
|
||||
@@ -16,8 +17,11 @@ from .const import (
|
||||
CONF_THERMOSTAT_TYPE,
|
||||
CONF_THERMOSTAT_CENTRAL_CONFIG,
|
||||
CONF_MAX_ON_PERCENT,
|
||||
NowClass,
|
||||
)
|
||||
|
||||
from .central_feature_power_manager import CentralFeaturePowerManager
|
||||
|
||||
VTHERM_API_NAME = "vtherm_api"
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -62,6 +66,12 @@ class VersatileThermostatAPI(dict):
|
||||
# A dict that will store all Number entities which holds the temperature
|
||||
self._number_temperatures = dict()
|
||||
self._max_on_percent = None
|
||||
self._central_power_manager = CentralFeaturePowerManager(
|
||||
VersatileThermostatAPI._hass, self
|
||||
)
|
||||
|
||||
# the current time (for testing purpose)
|
||||
self._now = None
|
||||
|
||||
def find_central_configuration(self):
|
||||
"""Search for a central configuration"""
|
||||
@@ -176,6 +186,10 @@ class VersatileThermostatAPI(dict):
|
||||
if entry_id is None or entry_id == entity.unique_id:
|
||||
await entity.async_startup(self.find_central_configuration())
|
||||
|
||||
# start listening for the central power manager if not only one vtherm reload
|
||||
if not entry_id:
|
||||
await self.central_power_manager.start_listening()
|
||||
|
||||
async def init_vtherm_preset_with_central(self):
|
||||
"""Init all VTherm presets when the VTherm uses central temperature"""
|
||||
# Initialization of all preset for all VTherm
|
||||
@@ -289,3 +303,18 @@ class VersatileThermostatAPI(dict):
|
||||
def hass(self):
|
||||
"""Get the HomeAssistant object"""
|
||||
return VersatileThermostatAPI._hass
|
||||
|
||||
@property
|
||||
def central_power_manager(self) -> any:
|
||||
"""Returns the central power manager"""
|
||||
return self._central_power_manager
|
||||
|
||||
# For testing purpose
|
||||
def _set_now(self, now: datetime):
|
||||
"""Set the now timestamp. This is only for tests purpose"""
|
||||
self._now = now
|
||||
|
||||
@property
|
||||
def now(self) -> datetime:
|
||||
"""Get now. The local datetime or the overloaded _set_now date"""
|
||||
return self._now if self._now is not None else NowClass.get_now(self._hass)
|
||||
|
||||
@@ -15,7 +15,7 @@ Provide the mandatory main attributes. These attributes are common to all VTherm
|
||||
1. For `over_switch`: VTherm will turn the radiator on/off, modulating the proportion of time it is on,
|
||||
2. For `over_valve`: VTherm will calculate a new valve opening level and send it if it has changed,
|
||||
3. For `over_climate`: The cycle performs basic controls and recalculates the self-regulation coefficients. The cycle may result in a new setpoint sent to underlying devices or a valve opening adjustment in the case of a controllable TRV.
|
||||
5. The equipment's power, which will activate power and energy consumption sensors for the device. If multiple devices are linked to the same VTherm, specify the total maximum power of all devices here,
|
||||
5. The equipment's power, which will activate power and energy consumption sensors for the device. If multiple devices are linked to the same VTherm, specify the total maximum power of all devices here. The power unit is not important here. What is important is that all _VTherm_ and all power sensors have the same unit (see: Power shedding feature),
|
||||
6. The option to use additional parameters from centralized configuration:
|
||||
1. Outdoor temperature sensor,
|
||||
2. Minimum/maximum temperature and temperature step size,
|
||||
@@ -42,4 +42,4 @@ Choose the features you want to use for this VTherm:
|
||||
>  _*Notes*_
|
||||
> 1. The list of available functions adapts to your VTherm type.
|
||||
> 2. When you enable a function, a new menu entry is added to configure it.
|
||||
> 3. You cannot validate the creation of a VTherm if all parameters for all enabled functions have not been configured.
|
||||
> 3. You cannot validate the creation of a VTherm if all parameters for all enabled functions have not been configured.
|
||||
|
||||
@@ -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, 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, as Sonoff TRVZB, this type is recommended if you have such devices.
|
||||
|
||||
The underlying entities for this VTherm type are exclusively `climate`.
|
||||
|
||||
|
||||
@@ -33,6 +33,8 @@ 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.
|
||||
|
||||
>  _*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.
|
||||
|
||||
@@ -1,46 +1,51 @@
|
||||
# Power Management - Load Shedding
|
||||
|
||||
- [Power Management - Load Shedding](#power-management---load-shedding)
|
||||
- [Configure Power Management](#configure-power-management)
|
||||
- [Example Use Case:](#example-use-case)
|
||||
- [Configuring Power Management](#configuring-power-management)
|
||||
|
||||
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**.
|
||||
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**.
|
||||
|
||||
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.
|
||||
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).
|
||||
4. When a _VTherm_ starts, a check is performed to determine if the declared power is available. If not, the _VTherm_ is put into shed mode.
|
||||
|
||||
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.
|
||||
**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.
|
||||
|
||||
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)
|
||||
### 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).
|
||||
|
||||
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.
|
||||
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 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).
|
||||
## Configuring Power Management
|
||||
|
||||
## Configure Power Management
|
||||
|
||||
If you have chosen the `With power detection` feature, configure it as follows:
|
||||
In the centralized configuration, if you have selected the `With power detection` feature, configure it as follows:
|
||||
|
||||

|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
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.
|
||||
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).
|
||||
|
||||
>  _*Notes*_
|
||||
>
|
||||
> 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: .
|
||||
> 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: .
|
||||
> 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.
|
||||
@@ -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#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.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:
|
||||
- **Eco**: the device is in energy-saving mode
|
||||
- **Comfort**: the device is in comfort mode
|
||||
- **Boost**: the device fully opens all valves
|
||||
|
||||
BIN
documentation/en/images/card-trv-jeffodilo.png
Normal file
BIN
documentation/en/images/card-trv-jeffodilo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 144 KiB |
File diff suppressed because it is too large
Load Diff
@@ -25,7 +25,8 @@ The installation should look like this:
|
||||
|
||||
## Configuration
|
||||
|
||||
Click on the "Underlying Entities" option from the menu, and you will see this configuration page:
|
||||
First, configure the main settings common to all _VTherms_ (see [main settings](base-attributes.md)).
|
||||
Then, click on the "Underlying Entities" option from the menu, and you will see this configuration page:
|
||||
|
||||

|
||||
|
||||
|
||||
@@ -26,7 +26,8 @@ The installation should look like this:
|
||||
|
||||
## Configuration
|
||||
|
||||
Click on the "Underlying Entities" option from the menu, and you will see this configuration page:
|
||||
First, configure the main settings common to all _VTherms_ (see [main settings](base-attributes.md)).
|
||||
Then, click on the "Underlying Entities" option from the menu, and you will see this configuration page:
|
||||
|
||||

|
||||
|
||||
|
||||
@@ -21,7 +21,8 @@ The installation should be similar to the `over_switch` VTherm setup, except tha
|
||||
|
||||
## Configuration
|
||||
|
||||
Click on the "Underlying Entities" option from the menu, and you will see this configuration page, you should add the `number` entities that will be controlled by VTherm. Only `number` or `input_number` entities are accepted.
|
||||
First, configure the main settings common to all _VTherms_ (see [main settings](base-attributes.md)).
|
||||
Then, click on the "Underlying Entities" option from the menu, and you will see this configuration page, you should add the `number` entities that will be controlled by VTherm. Only `number` or `input_number` entities are accepted.
|
||||
|
||||

|
||||
|
||||
|
||||
@@ -40,4 +40,5 @@ Some TRV type thermostats are known to be incompatible with Versatile Thermostat
|
||||
5. TRVs like Aqara SRTS-A01 and MOES TV01-ZB, which lack the `hvac_action` state feedback to determine whether they are heating or not. Therefore, state feedback is inaccurate, but the rest seems functional.
|
||||
6. Airwell air conditioners with the "Midea AC LAN" integration. If two VTherm commands are too close together, the air conditioner stops itself.
|
||||
7. Climates based on the Overkiz integration do not work. It seems impossible to turn off or even change the temperature on these systems.
|
||||
8. Heating systems based on Netatmo perform poorly. Netatmo schedules conflict with _VTherm_ programming. Netatmo devices constantly revert to `Auto` mode, which is poorly managed with _VTherm_. In this mode, _VTherm_ cannot determine whether the system is heating or cooling, making it impossible to select the correct algorithm. Some users have managed to make it work using a virtual switch between _VTherm_ and the underlying system, but stability is not guaranteed. An example is provided in the [troubleshooting](troubleshooting.md) section.
|
||||
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
|
||||

|
||||
|
||||
> * **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).
|
||||
|
||||
|
||||
@@ -38,6 +38,8 @@ 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:
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
- [Troubleshooting](#troubleshooting)
|
||||
- [Using a Heatzy](#using-a-heatzy)
|
||||
- [Using a radiator with a pilot wire (Nodon SIN-4-FP-21)](#using-a-radiator-with-a-pilot-wire-nodon-sin-4-fp-21)
|
||||
- [Using a Netatmo System](#using-a-netatmo-system)
|
||||
- [Only the first radiator heats](#only-the-first-radiator-heats)
|
||||
- [The radiator heats even though the setpoint temperature is exceeded, or it does not heat when the room temperature is well below the setpoint](#the-radiator-heats-even-though-the-setpoint-temperature-is-exceeded-or-it-does-not-heat-when-the-room-temperature-is-well-below-the-setpoint)
|
||||
- [Type `over_switch` or `over_valve`](#type-over_switch-or-over_valve)
|
||||
@@ -16,6 +17,7 @@
|
||||
- [Using a Group of People as a Presence Sensor](#using-a-group-of-people-as-a-presence-sensor)
|
||||
- [Enable Logs for the Versatile Thermostat](#enable-logs-for-the-versatile-thermostat)
|
||||
- [VTherm does not track setpoint changes made directly on the underlying device (`over_climate`)](#vtherm-does-not-track-setpoint-changes-made-directly-on-the-underlying-device-over_climate)
|
||||
- [VTherm Automatically Switches to 'Cooling' or 'Heating' Mode](#vtherm-automatically-switches-to-cooling-or-heating-mode)
|
||||
|
||||
|
||||
## Using a Heatzy
|
||||
@@ -85,6 +87,17 @@ Example:
|
||||
|
||||
Another more complex example is [here](https://github.com/jmcollin78/versatile_thermostat/discussions/431#discussioncomment-11393065)
|
||||
|
||||
## Using a Netatmo System
|
||||
|
||||
The system based on Netatmo TRVs does not work well with _VTherm_. You can find a discussion about the specific behavior of Netatmo systems (in French) here: [https://forum.hacf.fr/t/vannes-netatmo-et-vtherm/56063](https://forum.hacf.fr/t/vannes-netatmo-et-vtherm/56063).
|
||||
|
||||
However, some users have successfully integrated _VTherm_ with Netatmo by incorporating a virtual switch between _VTherm_ and the Netatmo `climate` entity, as follows:
|
||||
|
||||
```
|
||||
TODO
|
||||
```
|
||||
|
||||
|
||||
## Only the first radiator heats
|
||||
|
||||
In `over_switch` mode, if multiple radiators are configured for the same VTherm, the heating will be triggered sequentially to smooth out the consumption peaks as much as possible.
|
||||
@@ -213,4 +226,10 @@ Be careful, in debug mode, Versatile Thermostat is very verbose and can quickly
|
||||
|
||||
## VTherm does not track setpoint changes made directly on the underlying device (`over_climate`)
|
||||
|
||||
See the details of this feature [here](over-climate.md#track-underlying-temperature-changes).
|
||||
See the details of this feature [here](over-climate.md#track-underlying-temperature-changes).
|
||||
|
||||
## VTherm Automatically Switches to 'Cooling' or 'Heating' Mode
|
||||
|
||||
Some reversible heat pumps have modes that allow the heat pump to decide whether to heat or cool. These modes are labeled as 'Auto' or 'Heat_cool' depending on the brand. These two modes should not be used with _VTherm_ because _VTherm_'s algorithms require explicit knowledge of whether the system is in heating or cooling mode, which these modes do not provide.
|
||||
|
||||
You should only use the following modes: `Heat`, `Cool`, `Off`, or optionally `Fan` (although `Fan` has no practical purpose with _VTherm_).
|
||||
@@ -5,6 +5,7 @@
|
||||
- [Composant Scheduler !](#composant-scheduler-)
|
||||
- [Courbes de régulattion avec Plotly](#courbes-de-régulattion-avec-plotly)
|
||||
- [Les notification avec l'AappDaemon NOTIFIER](#les-notification-avec-laappdaemon-notifier)
|
||||
- [Une très belle carte (merci @Jeffodilo)](#une-très-belle-carte-merci-jeffodilo)
|
||||
|
||||
|
||||
## Versatile Thermostat UI Card
|
||||
@@ -213,4 +214,275 @@ action:
|
||||
mode: queued
|
||||
max: 30
|
||||
```
|
||||
</details>
|
||||
|
||||
## Une très belle carte (merci @Jeffodilo)
|
||||
|
||||
@Jeffodilo a réalisé et partagé le code d'une très belle carte adaptée au TRV :
|
||||
|
||||

|
||||
|
||||
Cette carte n’utilise pas card_mod, elle utilise les cartes custom assez courante suivantes
|
||||
On garde une partie de l’interface UI, sauf pour l’horizontale de la 2ème ligne
|
||||
- custom:vertical-stack-in-card
|
||||
- custom:stack-in-card
|
||||
- custom:mini-graph-card
|
||||
- custom:mushroom-template-card
|
||||
- custom:button-card
|
||||
|
||||
L'original est ici (En Français): [forum HACF](https://forum.hacf.fr/t/carte-mise-en-forme-vanne-avec-thermostant-versatile/56132)
|
||||
|
||||
Evidemment, vous devez l'adapter à votre code.
|
||||
|
||||
Le code:
|
||||
|
||||
<details>
|
||||
|
||||
```yaml
|
||||
type: vertical-stack
|
||||
cards:
|
||||
- type: heading
|
||||
icon: mdi:bed-double
|
||||
heading: Parents
|
||||
heading_style: title
|
||||
- type: custom:vertical-stack-in-card
|
||||
cards:
|
||||
- type: custom:mini-graph-card
|
||||
entities:
|
||||
- entity: sensor.sonde_parents_temperature
|
||||
name: Température
|
||||
state_adaptive_color: true
|
||||
- entity: climate.valve_parents
|
||||
name: Temp
|
||||
attribute: current_temperature
|
||||
unit: °C
|
||||
state_adaptive_color: true
|
||||
show_graph: false
|
||||
show_state: true
|
||||
hour24: true
|
||||
hours_to_show: 24
|
||||
points_per_hour: 2
|
||||
font_size: 50
|
||||
show:
|
||||
name: false
|
||||
icon: false
|
||||
legend: false
|
||||
labels: true
|
||||
extrema: false
|
||||
color_thresholds:
|
||||
- color: "#33ccff"
|
||||
value: 19
|
||||
- color: "#00ffff"
|
||||
value: 19.5
|
||||
- color: "#33ffcc"
|
||||
value: 20
|
||||
- color: "#00ff99"
|
||||
value: 20.5
|
||||
- color: "#ffff99"
|
||||
value: 21
|
||||
- color: "#ffff33"
|
||||
value: 21.5
|
||||
- color: "#ff9933"
|
||||
value: 22
|
||||
- color: "#cc6633"
|
||||
value: 24
|
||||
- color: "#ff6000"
|
||||
value: 26
|
||||
- type: custom:stack-in-card
|
||||
mode: horizontal
|
||||
cards:
|
||||
- type: custom:mushroom-template-card
|
||||
secondary: ""
|
||||
layout: horizontal
|
||||
tap_action:
|
||||
action: more-info
|
||||
entity: sensor.sonde_parents_temperature
|
||||
fill_container: false
|
||||
multiline_secondary: false
|
||||
primary: >-
|
||||
{% if is_state_attr('climate.versatile_parents','hvac_action',
|
||||
'idle') %}
|
||||
🗜️ {{ states('number.valve_parents_valve_opening_degree', with_unit=True,)}} |🔋{{ states('sensor.valve_parents_battery') }} % | Inactif
|
||||
{% elif is_state_attr('climate.versatile_parents','hvac_action',
|
||||
'heating') %}
|
||||
🗜️ {{ states('number.valve_parents_valve_opening_degree', with_unit=True,)}} |🔋{{ states('sensor.valve_parents_battery') }} % | Chauffe
|
||||
{% else %} 🗜️ {{
|
||||
states('number.valve_parents_valve_opening_degree',
|
||||
with_unit=True,)}} | 🔋{{ states('sensor.valve_parents_battery')
|
||||
}} % | Off {% endif %}
|
||||
icon: ""
|
||||
- type: horizontal-stack
|
||||
cards:
|
||||
- type: custom:button-card
|
||||
name: Conf.
|
||||
entity: climate.versatile_parents
|
||||
show_state: false
|
||||
show_icon: true
|
||||
show_name: false
|
||||
icon: mdi:fire
|
||||
size: 80%
|
||||
styles:
|
||||
icon:
|
||||
- color: |
|
||||
[[[
|
||||
if (states['climate.versatile_parents']) {
|
||||
if (states['climate.versatile_parents'].attributes.preset_mode == 'comfort')
|
||||
return 'darkorange';
|
||||
else
|
||||
return 'white'; }
|
||||
]]]
|
||||
name:
|
||||
- color: white
|
||||
- font-size: 60%
|
||||
card:
|
||||
- height: 40px
|
||||
- width: 30px
|
||||
tap_action:
|
||||
action: perform-action
|
||||
perform_action: climate.set_preset_mode
|
||||
target:
|
||||
entity_id:
|
||||
- climate.versatile_parents
|
||||
data:
|
||||
preset_mode: comfort
|
||||
- type: custom:button-card
|
||||
name: Eco
|
||||
entity: climate.versatile_parents
|
||||
show_state: false
|
||||
show_icon: true
|
||||
show_name: false
|
||||
icon: mdi:leaf
|
||||
size: 80%
|
||||
styles:
|
||||
icon:
|
||||
- color: |
|
||||
[[[
|
||||
if (states['climate.versatile_parents']) {
|
||||
if (states['climate.versatile_parents'].attributes.preset_mode == 'eco')
|
||||
return 'lightgreen';
|
||||
else
|
||||
return 'white'; }
|
||||
]]]
|
||||
name:
|
||||
- color: white
|
||||
- font-size: 60%
|
||||
card:
|
||||
- height: 40px
|
||||
- width: 30px
|
||||
tap_action:
|
||||
action: perform-action
|
||||
perform_action: climate.set_preset_mode
|
||||
target:
|
||||
entity_id:
|
||||
- climate.versatile_parents
|
||||
data:
|
||||
preset_mode: eco
|
||||
- type: custom:button-card
|
||||
name: Manu
|
||||
entity: climate.versatile_parents
|
||||
show_state: false
|
||||
show_icon: true
|
||||
show_name: false
|
||||
icon: mdi:hand-back-left
|
||||
size: 80%
|
||||
styles:
|
||||
icon:
|
||||
- color: |
|
||||
[[[
|
||||
if (states['climate.versatile_parents']) {
|
||||
if (states['climate.versatile_parents'].attributes.preset_mode == 'none')
|
||||
return 'indianred';
|
||||
else
|
||||
return 'white'; }
|
||||
]]]
|
||||
name:
|
||||
- color: white
|
||||
- font-size: 60%
|
||||
card:
|
||||
- height: 40px
|
||||
- width: 30px
|
||||
tap_action:
|
||||
action: perform-action
|
||||
perform_action: climate.set_preset_mode
|
||||
target:
|
||||
entity_id:
|
||||
- climate.versatile_parents
|
||||
data:
|
||||
preset_mode: none
|
||||
- type: custom:button-card
|
||||
name: Abs.
|
||||
entity: climate.versatile_parents
|
||||
show_state: false
|
||||
show_icon: true
|
||||
show_name: false
|
||||
icon: mdi:snowflake
|
||||
size: 80%
|
||||
styles:
|
||||
icon:
|
||||
- color: |
|
||||
[[[
|
||||
if (states['climate.versatile_parents']) {
|
||||
if (states['climate.versatile_parents'].attributes.preset_mode == 'frost')
|
||||
return 'skyblue';
|
||||
else
|
||||
return 'white'; }
|
||||
]]]
|
||||
name:
|
||||
- color: white
|
||||
- font-size: 60%
|
||||
card:
|
||||
- height: 40px
|
||||
- width: 30px
|
||||
tap_action:
|
||||
action: perform-action
|
||||
perform_action: climate.set_preset_mode
|
||||
target:
|
||||
entity_id:
|
||||
- climate.versatile_parents
|
||||
data:
|
||||
preset_mode: frost
|
||||
- type: custom:button-card
|
||||
name: Boost
|
||||
entity: climate.versatile_parents
|
||||
show_state: false
|
||||
show_icon: true
|
||||
show_name: false
|
||||
icon: mdi:rocket-launch
|
||||
size: 80%
|
||||
styles:
|
||||
icon:
|
||||
- color: |
|
||||
[[[
|
||||
if (states['climate.versatile_parents']) {
|
||||
if (states['climate.versatile_parents'].attributes.preset_mode == 'boost')
|
||||
return 'red';
|
||||
else
|
||||
return 'white'; }
|
||||
]]]
|
||||
name:
|
||||
- color: white
|
||||
- font-size: 60%
|
||||
card:
|
||||
- height: 40px
|
||||
- width: 30px
|
||||
tap_action:
|
||||
action: perform-action
|
||||
perform_action: climate.set_preset_mode
|
||||
target:
|
||||
entity_id:
|
||||
- climate.versatile_parents
|
||||
data:
|
||||
preset_mode: boost
|
||||
- type: custom:mushroom-climate-card
|
||||
entity: climate.versatile_parents
|
||||
show_temperature_control: true
|
||||
hvac_modes: []
|
||||
tap_action:
|
||||
action: more-info
|
||||
primary_info: state
|
||||
icon: mdi:radiator
|
||||
secondary_info: last-updated
|
||||
fill_container: false
|
||||
layout: horizontal
|
||||
```
|
||||
</details>
|
||||
@@ -15,7 +15,7 @@ Donnez les principaux attributs obligatoires. Ces attributs sont communs à tous
|
||||
1. `over_switch` : VTherm allumera/éteindra le radiateur en modulant la proportion de temps allumé,
|
||||
2. `over_valve` : VTherm calculera une nouvelle ouverture de la vanne et lui enverra si elle a changée,
|
||||
3. `over_climate` : le cycle permet d'effectuer les contrôles de base et recalcule les coefficients de l'auto-régulation. Le cycle peut déboucher sur une nouvelle consigne envoyée au sous-jacents ou sur une modification d'ouverture de la vanne dans le cas d'un _TRV_ dont la vanne est commandable.
|
||||
7. une puissance de l'équipement ce qui va activer les capteurs de puissance et énergie consommée par l'appareil. Si plusieurs équipements sont reliés au même VTherm, il faut indiquer ici le total des puissances max des équipements,
|
||||
7. une puissance de l'équipement ce qui va activer les capteurs de puissance et énergie consommée par l'appareil. Si plusieurs équipements sont reliés au même VTherm, il faut indiquer ici le total des puissances max des équipements. L'unité n'est pas importante. Ce qui est important c'est toutes les puissances de tous les _VTherms_ soient dans la même unité ainsi que les éventuels capteurs de puissance (cf. la fonction de délestage),
|
||||
8. la possibilité d'utiliser des paramètres complémentaires venant de la configuration centralisée :
|
||||
1. capteur de température extérieure,
|
||||
2. température minimale / maximale et pas de température
|
||||
|
||||
@@ -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, 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, comme les Sonoff TRVZB, ce type est recommandé si vous êtes équipés.
|
||||
|
||||
Les entités sous-jacentes de ce type de VTherm sont donc des `climate` exclusivement.
|
||||
|
||||
|
||||
@@ -34,6 +34,8 @@ 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.
|
||||
|
||||
|
||||
>  _*Notes*_
|
||||
> 1. L'algorithme de détection est décrit [ici](algorithms.md#lalgorithme-de-la-fonction-dauto-startstop).
|
||||
|
||||
@@ -6,11 +6,13 @@
|
||||
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 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.
|
||||
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),
|
||||
4. au démarrage d'un _VTherm_, une vérification est effectuée pour savoir si la puissance déclarée est disponible. Si non, le _VTherm_ est passé en délestage.
|
||||
|
||||
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é.
|
||||
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é.
|
||||
|
||||
Cas d'usage type:
|
||||
1. vous avez un compteur électrique limité à 11 kW,
|
||||
@@ -26,7 +28,7 @@ Si le vehicle n'est pas en charge, la puissance totale consommé est de 1 kW, un
|
||||
|
||||
## Configurer la gestion de la puissance
|
||||
|
||||
Si vous avez choisi la fonctionnalité `Avec détection de la puissance`, vous la configurez de la façon suivante :
|
||||
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 :
|
||||
|
||||

|
||||
|
||||
@@ -37,10 +39,13 @@ Si vous avez choisi la fonctionnalité `Avec détection de la puissance`, vous l
|
||||
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)
|
||||
|
||||
>  _*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 : .
|
||||
> 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 : ,
|
||||
> 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.
|
||||
@@ -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#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.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 :
|
||||
- **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
|
||||
|
||||
BIN
documentation/fr/images/card-trv-jeffodilo.png
Normal file
BIN
documentation/fr/images/card-trv-jeffodilo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 144 KiB |
File diff suppressed because it is too large
Load Diff
@@ -26,7 +26,8 @@ L'installation doit ressembler à ça :
|
||||
|
||||
## Configuration
|
||||
|
||||
Cliquer sur l'option de menu "Sous-jacents" et vous allez avoir cette page de configuration :
|
||||
Configurez d'abord les paramètres principaux et communs à tous les _VTherm_ (cf. [paramètres principaux](base-attributes.md)).
|
||||
Cliquez ensuite sur l'option de menu "Sous-jacents" et vous allez avoir cette page de configuration :
|
||||
|
||||

|
||||
|
||||
@@ -39,7 +40,7 @@ Il est possible de choisir un thermostat `over_climate` qui commande une climati
|
||||
|
||||
### L'auto-régulation
|
||||
|
||||
En mode `over_cliamte`, le device utilise son propre algorithme de régulation : il s'allume / s'éteint et se met en pause tout seul en fonction de la consigne transmise par le VTherm à travers son entité `climate`. Il utilise pour ça son thermomètre interne et la consigne reçue.
|
||||
En mode `over_climate`, le device utilise son propre algorithme de régulation : il s'allume / s'éteint et se met en pause tout seul en fonction de la consigne transmise par le VTherm à travers son entité `climate`. Il utilise pour ça son thermomètre interne et la consigne reçue.
|
||||
|
||||
Selon l'équipement cette régulation interne peut être plus ou moins bonne. Ca dépend beaucoup de la qualité de l'équipement, du fonctionnement de son thermomètre interne et de son algorithme interne. Pour améliorer les équipements qui régule mal, VTherm propose de tricher un peu sur la consigne qui lui est envoyée en augmentant ou diminuant celle-ci en fonction cette fois de la température de la pièce mesurée par VTherm et non plus de la température interne.
|
||||
|
||||
|
||||
@@ -25,7 +25,8 @@ L'installation doit ressembler à ça :
|
||||
|
||||
## Configuration
|
||||
|
||||
Cliquer sur l'option de menu "Sous-jacents" et vous allez avoir cette page de configuration :
|
||||
Configurez d'abord les paramètres principaux et communs à tous les _VTherm_ (cf. [paramètres principaux](base-attributes.md)).
|
||||
Ensuite cliquez sur l'option de menu "Sous-jacents" et vous allez avoir cette page de configuration :
|
||||
|
||||

|
||||
|
||||
|
||||
@@ -22,11 +22,12 @@ L'installation doit ressembler à celle pour le VTherm `over_switch` sauf que l'
|
||||
|
||||
## Configuration
|
||||
|
||||
Cliquer sur l'option de menu "Sous-jacents" et vous allez avoir cette page de configuration. Vous mettez les entités `numnber` ou `input_number`qui vont être controllés par le VTherm :
|
||||
Configurez d'abord les paramètres principaux et communs à tous les _VTherm_ (cf. [paramètres principaux](base-attributes.md)).
|
||||
Ensuite cliquez sur l'option de menu "Sous-jacents" et vous allez avoir cette page de configuration. Vous mettez les entités `numnber` ou `input_number`qui vont être controllés par le VTherm :
|
||||
|
||||

|
||||
|
||||
L'algorithme à utiliser est aujourd'hui limité à TPI est disponible. Voir [algorithme](#algorithme).
|
||||
|
||||
Il est possible de choisir un thermostat over valve qui commande une climatisation en cochant la case "AC Mode". Dans ce cas, seul le mode refroidissement sera visible.
|
||||
Il est possible de choisir un thermostat `over-valve` qui commande une climatisation en cochant la case "AC Mode". Dans ce cas, seul le mode refroidissement sera visible.
|
||||
|
||||
|
||||
@@ -5,8 +5,8 @@ Ce thermostat peut piloter 3 types d'équipements :
|
||||
2. une sonde de température pour la pièce (ou un input_number),
|
||||
3. un capteur de température externe (pensez à l'intégration météo si vous n'en avez pas)
|
||||
2. un autre thermostat qui a ses propres modes de fonctionnement (nommé ```thermostat_over_climate```). Pour ce type de thermostat la configuration minimale nécessite :
|
||||
1. un équipement - comme une climatisation, une valve thermostatique - qui est pilotée par sa propre entity de type ```climate```,
|
||||
3. un équipement qui peut prendre une valeur de 0 à 100% (nommée ```thermostat_over_valve```). A 0 le chauffage est coupé, 100% il est ouvert à fond. Ce type permet de piloter une valve thermostatique (cf. valve Shelly) qui expose une entité de type `number.` permetttant de piloter directement l'ouverture de la vanne. Versatile Thermostat régule la température de la pièce en jouant sur le pourcentage d'ouverture, à l'aide des capteurs de température intérieur et extérieur en utilisant l'algorithme TPI décrit ci-dessous.
|
||||
1. un équipement - comme une climatisation, une vanne thermostatique - qui est pilotée par sa propre entity de type ```climate```,
|
||||
3. un équipement qui peut prendre une valeur de 0 à 100% (nommée ```thermostat_over_valve```). A 0 le chauffage est coupé, 100% il est ouvert à fond. Ce type permet de piloter une vanne thermostatique (cf. TRV Shelly) qui expose une entité de type `number.` permetttant de piloter directement l'ouverture de la vanne. Versatile Thermostat régule la température de la pièce en jouant sur le pourcentage d'ouverture, à l'aide des capteurs de température intérieur et extérieur en utilisant l'algorithme TPI décrit ci-dessous.
|
||||
|
||||
Le type `over_climate` vous permet d'ajouter à votre équipement existant toutes les fonctionnalités apportées par VersatileThermostat. L'entité `climate` VersatileThermostat contrôlera votre entité climate sous-jacente, l'éteindra si les fenêtres sont ouvertes, la fera passer en mode Eco si personne n'est présent, etc. Voir [ici] (#pourquoi-un-nouveau-thermostat-implémentation). Pour ce type de thermostat, tous les cycles de chauffage sont contrôlés par l'entité climate sous-jacente et non par le thermostat polyvalent lui-même. Une fonction facultative d'auto-régulation permet au Versatile Thermostat d'ajuster la température donnée en consigne au sous-jacent afin d'atteindre la consigne.
|
||||
|
||||
@@ -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 chager de preset lorsqu'une **porte ou des fenêtres sont ouvertes/fermées** après un certain délai,
|
||||
- É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,
|
||||
- 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é.
|
||||
@@ -33,11 +33,12 @@ Toutes ces fonctions sont configurables de façon centralisée ou individuelle e
|
||||
|
||||
## Incompatibilités
|
||||
Certains thermostat de type TRV sont réputés incompatibles avec le Versatile Thermostat. C'est le cas des vannes suivantes :
|
||||
1. les vannes POPP de Danfoss avec retour de température. Il est impossible d'éteindre cette vanne et elle s'auto-régule d'elle-même causant des conflits avec le VTherm,
|
||||
1. les vannes POPP de Danfoss avec retour de température. Il est impossible d'éteindre cette vanne et elle s'auto-régule d'elle-même causant des conflits avec le _VTherm_,
|
||||
2. Les thermostats « Homematic » (et éventuellement Homematic IP) sont connus pour rencontrer des problèmes avec le Versatile Thermostat en raison des limitations du protocole RF sous-jacent. Ce problème se produit particulièrement lorsque vous essayez de contrôler plusieurs thermostats Homematic à la fois dans une seule instance de VTherm. Afin de réduire la charge du cycle de service, vous pouvez par ex. regroupez les thermostats avec des procédures spécifiques à Homematic (par exemple en utilisant un thermostat mural) et laissez Versatile Thermostat contrôler uniquement le thermostat mural directement. Une autre option consiste à contrôler un seul thermostat et à propager les changements de mode CVC et de température par un automatisme,
|
||||
3. les thermostats de type Heatzy qui ne supportent pas les commandes de type set_temperature
|
||||
4. les thermostats de type Rointe ont tendance a se réveiller tout seul. Le reste fonctionne normalement.
|
||||
5. les TRV de type Aqara SRTS-A01 et MOES TV01-ZB qui n'ont pas le retour d'état `hvac_action` permettant de savoir si elle chauffe ou pas. Donc les retours d'état sont faussés, le reste à l'air fonctionnel.
|
||||
6. La clim Airwell avec l'intégration "Midea AC LAN". Si 2 commandes de VTherm sont trop rapprochées, la clim s'arrête d'elle même.
|
||||
7. Les climates basés sur l'intégration Overkiz ne fonctionnent pas. Il parait impossible d'éteindre ni même de changer la température sur ces systèmes.
|
||||
8. Les systèmes de chauffage basés sur Netatmo fonctionnent mal. Les plannings Netatmo viennent en concurrence de la programmation _VTherm_. Les sous-jacents Netatmo repasse en mode `Auto` tout le temps et ce mode est très mal géré avec _VTherm_ qui ne peut pas savoir si le sysème chauffe ou refroidit et donc quel algorithme choisir. Certains ont quand même réussi à le faire fonctionner avec un switch virtuel entre le _VTherm_ et le sous-jacent mais sans garantie de stabilité. Un exemple est donné dans la section [dépannage](troubleshooting.md)
|
||||
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
|
||||

|
||||
|
||||
> * **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).
|
||||
|
||||
@@ -24,7 +27,7 @@
|
||||
|
||||
> * **Release 5.3** : Ajout d'une fonction de pilotage d'une chaudière centrale [#234](https://github.com/jmcollin78/versatile_thermostat/issues/234) - plus d'infos ici: [Le contrôle d'une chaudière centrale](#le-contrôle-dune-chaudière-centrale). Ajout de la possibilité de désactiver le mode sécurité pour le thermomètre extérieur [#343](https://github.com/jmcollin78/versatile_thermostat/issues/343)
|
||||
> * **Release 5.2** : Ajout d'un `central_mode` permettant de piloter tous les VTherms de façon centralisée [#158](https://github.com/jmcollin78/versatile_thermostat/issues/158).
|
||||
> * **Release 5.1** : Limitation des valeurs envoyées aux valves et au température envoyées au climate sous-jacent.
|
||||
> * **Release 5.1** : Limitation des valeurs envoyées aux vannes et au température envoyées au climate sous-jacent.
|
||||
> * **Release 5.0** : Ajout d'une configuration centrale permettant de mettre en commun les attributs qui peuvent l'être [#239](https://github.com/jmcollin78/versatile_thermostat/issues/239).
|
||||
> * **Release 4.3** : Ajout d'un mode auto-fan pour le type `over_climate` permettant d'activer la ventilation si l'écart de température est important [#223](https://github.com/jmcollin78/versatile_thermostat/issues/223).
|
||||
> * **Release 4.2** : Le calcul de la pente de la courbe de température se fait maintenant en °/heure et non plus en °/min [#242](https://github.com/jmcollin78/versatile_thermostat/issues/242). Correction de la détection automatique des ouvertures par l'ajout d'un lissage de la courbe de température .
|
||||
|
||||
@@ -39,6 +39,8 @@ 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 :
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
- [Dépannages](#dépannages)
|
||||
- [Utilisation d'un Heatzy](#utilisation-dun-heatzy)
|
||||
- [Utilisation d'un radiateur avec un fil pilote (Nodon SIN-4-FP-21)](#utilisation-dun-radiateur-avec-un-fil-pilote-nodon-sin-4-fp-21)
|
||||
- [Utilisation d'un système Netatmo](#utilisation-dun-système-netatmo)
|
||||
- [Seul le premier radiateur chauffe](#seul-le-premier-radiateur-chauffe)
|
||||
- [Le radiateur chauffe alors que la température de consigne est dépassée ou ne chauffe pas alors que la température de la pièce est bien en-dessous de la consigne](#le-radiateur-chauffe-alors-que-la-température-de-consigne-est-dépassée-ou-ne-chauffe-pas-alors-que-la-température-de-la-pièce-est-bien-en-dessous-de-la-consigne)
|
||||
- [Type `over_switch` ou `over_valve`](#type-over_switch-ou-over_valve)
|
||||
@@ -84,6 +85,15 @@ Exemple :
|
||||
```
|
||||
Un exemple plus complet est [ici](https://github.com/jmcollin78/versatile_thermostat/discussions/431#discussioncomment-11393065)
|
||||
|
||||
## Utilisation d'un système Netatmo
|
||||
Le système à base de TRV Netatmo fonctionne mal avec _VTherm_. Vous avez ici une discussion sur le fonctionnement particulier des systèmes Netatmo (en Français) : https://forum.hacf.fr/t/vannes-netatmo-et-vtherm/56063
|
||||
Cependant certains ont réussi une intégration _VTerm_ Netatmo en intégrant un switch virtuel entre _VTherm_ et le `climate` Netatmo suivant :
|
||||
```
|
||||
TODO add virtual switch code
|
||||
```
|
||||
|
||||
|
||||
|
||||
## Seul le premier radiateur chauffe
|
||||
|
||||
En mode `over_switch` si plusieurs radiateurs sont configurés pour un même VTherm, l'alllumage va se faire de façon séquentiel pour lisser au plus possible les pics de consommation.
|
||||
@@ -210,4 +220,9 @@ Attention, en mode debug Versatile Thermostat est très verbeux et peut vite ral
|
||||
|
||||
## VTherm ne suit pas les changements de consigne faits directement depuis le sous-jacents (`over_climate`)
|
||||
|
||||
Voir le détail de cette fonction [ici](over-climate.md#suivre-les-changements-de-température-du-sous-jacent).
|
||||
Voir le détail de cette fonction [ici](over-climate.md#suivre-les-changements-de-température-du-sous-jacent).
|
||||
|
||||
## VTherm passe tout seul en mode 'clim' ou en mode 'chauffage'
|
||||
|
||||
Certaine _PAC_ réversibles ont des modes qui permettent de laisser le choix à la _PAC_ de chauffer ou de réfroidir. Ces modes sont 'Auto' or 'Heat_cool' selon les marques. Ces 2 modes ne doivent pas être utilisés avec _VTherm_ car les algorithmes de _VTherm_ ont besoin de savoir si ils sont en mode chauffe ou refroidissement ce que ne permettent pas ces modes.
|
||||
Vous devez donc utiliser uniquement les modes : `Heat`, `Cool`, `Off` ou `Fan` éventuellement (bien que fan n'a aucun sens avec _Vtherm)
|
||||
|
||||
@@ -3,5 +3,5 @@
|
||||
"content_in_root": false,
|
||||
"render_readme": true,
|
||||
"hide_default_branch": false,
|
||||
"homeassistant": "2024.12.4"
|
||||
"homeassistant": "2025.1.2"
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
[tool.black]
|
||||
# don't work. Options are in the devcontainer.yaml
|
||||
line-length = 180
|
||||
@@ -1 +1 @@
|
||||
homeassistant==2024.12.3
|
||||
homeassistant==2025.1.2
|
||||
|
||||
@@ -59,8 +59,6 @@ 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,
|
||||
@@ -89,7 +87,6 @@ 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_FULL_FEATURES
|
||||
| MOCK_WINDOW_CONFIG
|
||||
| MOCK_MOTION_CONFIG
|
||||
@@ -104,7 +101,6 @@ 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
|
||||
@@ -114,12 +110,7 @@ FULL_SWITCH_AC_CONFIG = (
|
||||
)
|
||||
|
||||
PARTIAL_CLIMATE_CONFIG = (
|
||||
MOCK_TH_OVER_CLIMATE_USER_CONFIG
|
||||
| MOCK_TH_OVER_CLIMATE_MAIN_CONFIG
|
||||
| MOCK_TH_OVER_CLIMATE_CENTRAL_MAIN_CONFIG
|
||||
| MOCK_TH_OVER_CLIMATE_TYPE_CONFIG
|
||||
| MOCK_PRESETS_CONFIG
|
||||
| MOCK_ADVANCED_CONFIG
|
||||
MOCK_TH_OVER_CLIMATE_USER_CONFIG | MOCK_TH_OVER_CLIMATE_MAIN_CONFIG | MOCK_TH_OVER_CLIMATE_CENTRAL_MAIN_CONFIG | MOCK_TH_OVER_CLIMATE_TYPE_CONFIG | MOCK_ADVANCED_CONFIG
|
||||
)
|
||||
|
||||
PARTIAL_CLIMATE_CONFIG_USE_DEVICE_TEMP = (
|
||||
@@ -127,7 +118,6 @@ 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_ADVANCED_CONFIG
|
||||
)
|
||||
|
||||
@@ -136,24 +126,17 @@ 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_ADVANCED_CONFIG
|
||||
)
|
||||
|
||||
PARTIAL_CLIMATE_AC_CONFIG = (
|
||||
MOCK_TH_OVER_CLIMATE_USER_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_ADVANCED_CONFIG
|
||||
MOCK_TH_OVER_CLIMATE_USER_CONFIG | MOCK_TH_OVER_CLIMATE_TYPE_AC_CONFIG | MOCK_TH_OVER_CLIMATE_MAIN_CONFIG | MOCK_TH_OVER_CLIMATE_CENTRAL_MAIN_CONFIG | MOCK_ADVANCED_CONFIG
|
||||
)
|
||||
|
||||
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_WINDOW_CONFIG
|
||||
| MOCK_MOTION_CONFIG
|
||||
| MOCK_POWER_CONFIG
|
||||
@@ -185,6 +168,7 @@ FULL_CENTRAL_CONFIG = {
|
||||
"comfort_ac_away_temp": 0,
|
||||
"boost_ac_away_temp": 30.7,
|
||||
CONF_WINDOW_DELAY: 15,
|
||||
CONF_WINDOW_OFF_DELAY: 30,
|
||||
CONF_WINDOW_AUTO_OPEN_THRESHOLD: 4,
|
||||
CONF_WINDOW_AUTO_CLOSE_THRESHOLD: 1,
|
||||
CONF_WINDOW_AUTO_MAX_DURATION: 31,
|
||||
@@ -592,7 +576,10 @@ class MockNumber(NumberEntity):
|
||||
|
||||
|
||||
async def create_thermostat(
|
||||
hass: HomeAssistant, entry: MockConfigEntry, entity_id: str
|
||||
hass: HomeAssistant,
|
||||
entry: MockConfigEntry,
|
||||
entity_id: str,
|
||||
temps: dict | None = None,
|
||||
) -> BaseThermostat:
|
||||
"""Creates and return a TPI Thermostat"""
|
||||
entry.add_to_hass(hass)
|
||||
@@ -601,6 +588,11 @@ async def create_thermostat(
|
||||
|
||||
entity = search_entity(hass, entity_id, CLIMATE_DOMAIN)
|
||||
|
||||
if entity and temps:
|
||||
await set_all_climate_preset_temp(
|
||||
hass, entity, temps, entity.entity_id.replace("climate.", "")
|
||||
)
|
||||
|
||||
return entity
|
||||
|
||||
|
||||
@@ -741,9 +733,11 @@ async def send_power_change_event(entity: BaseThermostat, new_power, date, sleep
|
||||
)
|
||||
},
|
||||
)
|
||||
await entity.power_manager._async_power_sensor_changed(power_event)
|
||||
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 asyncio.sleep(0.1)
|
||||
await entity.hass.async_block_till_done()
|
||||
|
||||
|
||||
async def send_max_power_change_event(
|
||||
@@ -767,9 +761,11 @@ async def send_max_power_change_event(
|
||||
)
|
||||
},
|
||||
)
|
||||
await entity.power_manager._async_max_power_sensor_changed(power_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 asyncio.sleep(0.1)
|
||||
await entity.hass.async_block_till_done()
|
||||
|
||||
|
||||
async def send_window_change_event(
|
||||
@@ -1027,6 +1023,16 @@ default_temperatures_ac_away = {
|
||||
"boost_ac_away": 23.1,
|
||||
}
|
||||
|
||||
default_temperatures_ac = {
|
||||
"frost": 7.0,
|
||||
"eco": 17.0,
|
||||
"comfort": 19.0,
|
||||
"boost": 21.0,
|
||||
"eco_ac": 27.0,
|
||||
"comfort_ac": 25.0,
|
||||
"boost_ac": 23.0,
|
||||
}
|
||||
|
||||
default_temperatures_away = {
|
||||
"frost": 7.0,
|
||||
"eco": 17.0,
|
||||
@@ -1101,3 +1107,9 @@ class SideEffects:
|
||||
def add_or_update_side_effect(self, key: str, new_value: Any):
|
||||
"""Update the value of a side effect"""
|
||||
self._current_side_effects[key] = new_value
|
||||
|
||||
|
||||
async def do_central_power_refresh(hass):
|
||||
"""Do a central power refresh"""
|
||||
await VersatileThermostatAPI.get_vtherm_api().central_power_manager.refresh_state()
|
||||
return hass.async_block_till_done()
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
# https://github.com/miketheman/pytest-socket/pull/275
|
||||
from pytest_socket import socket_allow_hosts
|
||||
|
||||
from homeassistant.core import StateMachine
|
||||
|
||||
@@ -26,6 +28,12 @@ from custom_components.versatile_thermostat.config_flow import (
|
||||
VersatileThermostatBaseConfigFlow,
|
||||
)
|
||||
|
||||
from custom_components.versatile_thermostat.const import (
|
||||
CONF_POWER_SENSOR,
|
||||
CONF_MAX_POWER_SENSOR,
|
||||
CONF_USE_POWER_FEATURE,
|
||||
CONF_PRESET_POWER,
|
||||
)
|
||||
from custom_components.versatile_thermostat.vtherm_api import VersatileThermostatAPI
|
||||
from custom_components.versatile_thermostat.base_thermostat import BaseThermostat
|
||||
|
||||
@@ -35,12 +43,6 @@ from .commons import (
|
||||
FULL_CENTRAL_CONFIG_WITH_BOILER,
|
||||
)
|
||||
|
||||
# https://github.com/miketheman/pytest-socket/pull/275
|
||||
from pytest_socket import socket_allow_hosts
|
||||
|
||||
# ...
|
||||
|
||||
|
||||
# ...
|
||||
def pytest_runtest_setup():
|
||||
"""setup tests"""
|
||||
@@ -51,16 +53,6 @@ def pytest_runtest_setup():
|
||||
|
||||
pytest_plugins = "pytest_homeassistant_custom_component" # pylint: disable=invalid-name
|
||||
|
||||
# Permet d'exclure certains test en mode d'ex
|
||||
# sequential = pytest.mark.sequential
|
||||
|
||||
|
||||
# This fixture allow to execute some tests first and not in //
|
||||
# @pytest.fixture
|
||||
# def order():
|
||||
# return 1
|
||||
#
|
||||
|
||||
# This fixture enables loading custom integrations in all tests.
|
||||
# Remove to enable selective use of this fixture
|
||||
@pytest.fixture(autouse=True)
|
||||
@@ -167,3 +159,24 @@ async def init_central_config_with_boiler_fixture(
|
||||
await create_central_config(hass, FULL_CENTRAL_CONFIG_WITH_BOILER)
|
||||
|
||||
yield
|
||||
|
||||
|
||||
@pytest.fixture(name="init_central_power_manager")
|
||||
async def init_central_power_manager_fixture(
|
||||
hass, init_central_config
|
||||
): # pylint: disable=unused-argument
|
||||
"""Initialize the central power_manager"""
|
||||
vtherm_api: VersatileThermostatAPI = VersatileThermostatAPI.get_vtherm_api(hass)
|
||||
|
||||
# 1. creation / init
|
||||
vtherm_api.central_power_manager.post_init(
|
||||
{
|
||||
CONF_POWER_SENSOR: "sensor.the_power_sensor",
|
||||
CONF_MAX_POWER_SENSOR: "sensor.the_max_power_sensor",
|
||||
CONF_USE_POWER_FEATURE: True,
|
||||
CONF_PRESET_POWER: 13,
|
||||
}
|
||||
)
|
||||
assert vtherm_api.central_power_manager.is_configured
|
||||
|
||||
yield
|
||||
|
||||
@@ -140,25 +140,6 @@ 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
|
||||
@@ -184,12 +165,16 @@ MOCK_MOTION_CONFIG = {
|
||||
CONF_NO_MOTION_PRESET: PRESET_ECO,
|
||||
}
|
||||
|
||||
MOCK_POWER_CONFIG = {
|
||||
MOCK_CENTRAL_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",
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# pylint: disable=wildcard-import, unused-wildcard-import, unused-argument, line-too-long
|
||||
# pylint: disable=wildcard-import, unused-wildcard-import, unused-argument, line-too-long, protected-access
|
||||
|
||||
""" Test the normal start of a Thermostat """
|
||||
from unittest.mock import patch
|
||||
@@ -107,9 +107,16 @@ async def test_overpowering_binary_sensors(
|
||||
skip_hass_states_is_state,
|
||||
skip_turn_on_off_heater,
|
||||
skip_send_event,
|
||||
init_central_power_manager,
|
||||
):
|
||||
"""Test the overpowering binary sensors in thermostat type"""
|
||||
|
||||
temps = {
|
||||
"eco": 17,
|
||||
"comfort": 18,
|
||||
"boost": 19,
|
||||
}
|
||||
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
title="TheOverSwitchMockName",
|
||||
@@ -122,9 +129,6 @@ async def test_overpowering_binary_sensors(
|
||||
CONF_CYCLE_MIN: 5,
|
||||
CONF_TEMP_MIN: 15,
|
||||
CONF_TEMP_MAX: 30,
|
||||
"eco_temp": 17,
|
||||
"comfort_temp": 18,
|
||||
"boost_temp": 19,
|
||||
CONF_USE_WINDOW_FEATURE: False,
|
||||
CONF_USE_MOTION_FEATURE: False,
|
||||
CONF_USE_POWER_FEATURE: True,
|
||||
@@ -136,15 +140,13 @@ async def test_overpowering_binary_sensors(
|
||||
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,
|
||||
},
|
||||
)
|
||||
|
||||
entity: BaseThermostat = await create_thermostat(
|
||||
hass, entry, "climate.theoverswitchmockname"
|
||||
hass, entry, "climate.theoverswitchmockname", temps
|
||||
)
|
||||
assert entity
|
||||
|
||||
@@ -153,35 +155,55 @@ async def test_overpowering_binary_sensors(
|
||||
)
|
||||
assert overpowering_binary_sensor
|
||||
|
||||
now: datetime = datetime.now(tz=get_tz(hass))
|
||||
now: datetime = NowClass.get_now(hass)
|
||||
VersatileThermostatAPI.get_vtherm_api()._set_now(now)
|
||||
|
||||
# Overpowering should be not set because poer have not been received
|
||||
await entity.async_set_preset_mode(PRESET_COMFORT)
|
||||
await entity.async_set_hvac_mode(HVACMode.HEAT)
|
||||
await send_temperature_change_event(entity, 15, now)
|
||||
assert await entity.power_manager.check_overpowering() is False
|
||||
assert entity.power_manager.is_overpowering_detected is False
|
||||
assert entity.power_manager.overpowering_state is STATE_UNKNOWN
|
||||
|
||||
await overpowering_binary_sensor.async_my_climate_changed()
|
||||
assert overpowering_binary_sensor.state is STATE_OFF
|
||||
assert overpowering_binary_sensor.device_class == BinarySensorDeviceClass.POWER
|
||||
|
||||
await send_power_change_event(entity, 100, now)
|
||||
await send_max_power_change_event(entity, 150, now)
|
||||
assert await entity.power_manager.check_overpowering() is True
|
||||
assert entity.power_manager.overpowering_state is STATE_ON
|
||||
# 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),
|
||||
},
|
||||
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"):
|
||||
# fmt: on
|
||||
await send_power_change_event(entity, 150, now)
|
||||
await send_max_power_change_event(entity, 100, now)
|
||||
|
||||
# Simulate the event reception
|
||||
await overpowering_binary_sensor.async_my_climate_changed()
|
||||
assert overpowering_binary_sensor.state == STATE_ON
|
||||
assert entity.power_manager.is_overpowering_detected is True
|
||||
assert entity.power_manager.overpowering_state is STATE_ON
|
||||
|
||||
# Simulate the event reception
|
||||
await overpowering_binary_sensor.async_my_climate_changed()
|
||||
assert overpowering_binary_sensor.state == STATE_ON
|
||||
|
||||
# set max power to a low value
|
||||
await send_max_power_change_event(entity, 201, now)
|
||||
assert await entity.power_manager.check_overpowering() is False
|
||||
assert entity.power_manager.overpowering_state is STATE_OFF
|
||||
# Simulate the event reception
|
||||
await overpowering_binary_sensor.async_my_climate_changed()
|
||||
assert overpowering_binary_sensor.state == STATE_OFF
|
||||
side_effects.add_or_update_side_effect("sensor.the_max_power_sensor", State("sensor.the_max_power_sensor", 251))
|
||||
# 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)
|
||||
assert entity.power_manager.is_overpowering_detected is False
|
||||
assert entity.power_manager.overpowering_state is STATE_OFF
|
||||
# Simulate the event reception
|
||||
await overpowering_binary_sensor.async_my_climate_changed()
|
||||
assert overpowering_binary_sensor.state == STATE_OFF
|
||||
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
|
||||
@@ -266,7 +266,9 @@ async def test_bug_272(
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_bug_407(hass: HomeAssistant, skip_hass_states_is_state):
|
||||
async def test_bug_407(
|
||||
hass: HomeAssistant, skip_hass_states_is_state, init_central_power_manager
|
||||
):
|
||||
"""Test the followin case in power management:
|
||||
1. a heater is active (heating). So the power consumption takes the heater power into account. We suppose the power consumption is near the threshold,
|
||||
2. the user switch preset let's say from Comfort to Boost,
|
||||
@@ -275,6 +277,12 @@ async def test_bug_407(hass: HomeAssistant, skip_hass_states_is_state):
|
||||
|
||||
"""
|
||||
|
||||
temps = {
|
||||
"eco": 17,
|
||||
"comfort": 18,
|
||||
"boost": 19,
|
||||
}
|
||||
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
title="TheOverSwitchMockName",
|
||||
@@ -287,9 +295,6 @@ async def test_bug_407(hass: HomeAssistant, skip_hass_states_is_state):
|
||||
CONF_CYCLE_MIN: 5,
|
||||
CONF_TEMP_MIN: 15,
|
||||
CONF_TEMP_MAX: 30,
|
||||
"eco_temp": 17,
|
||||
"comfort_temp": 18,
|
||||
"boost_temp": 19,
|
||||
CONF_USE_WINDOW_FEATURE: False,
|
||||
CONF_USE_MOTION_FEATURE: False,
|
||||
CONF_USE_POWER_FEATURE: True,
|
||||
@@ -301,34 +306,43 @@ async def test_bug_407(hass: HomeAssistant, skip_hass_states_is_state):
|
||||
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,
|
||||
},
|
||||
)
|
||||
|
||||
entity: ThermostatOverSwitch = await create_thermostat(
|
||||
hass, entry, "climate.theoverswitchmockname"
|
||||
hass, entry, "climate.theoverswitchmockname", temps
|
||||
)
|
||||
assert entity
|
||||
|
||||
tpi_algo = entity._prop_algorithm
|
||||
assert tpi_algo
|
||||
|
||||
tz = get_tz(hass) # pylint: disable=invalid-name
|
||||
now: datetime = datetime.now(tz=tz)
|
||||
now: datetime = NowClass.get_now(hass)
|
||||
VersatileThermostatAPI.get_vtherm_api()._set_now(now)
|
||||
|
||||
await send_temperature_change_event(entity, 16, now)
|
||||
await send_ext_temperature_change_event(entity, 10, now)
|
||||
|
||||
# 1. An already active heater will not switch to overpowering
|
||||
side_effects = SideEffects(
|
||||
{
|
||||
"sensor.the_power_sensor": State("sensor.the_power_sensor", 100),
|
||||
"sensor.the_max_power_sensor": State("sensor.the_max_power_sensor", 110),
|
||||
},
|
||||
State("unknown.entity_id", "unknown"),
|
||||
)
|
||||
|
||||
with patch(
|
||||
"homeassistant.core.ServiceRegistry.async_call"
|
||||
) as mock_service_call, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
|
||||
new_callable=PropertyMock,
|
||||
return_value=True,
|
||||
), patch(
|
||||
"homeassistant.core.StateMachine.get",
|
||||
side_effect=side_effects.get_side_effects(),
|
||||
):
|
||||
await entity.async_set_hvac_mode(HVACMode.HEAT)
|
||||
await entity.async_set_preset_mode(PRESET_COMFORT)
|
||||
@@ -337,16 +351,17 @@ async def test_bug_407(hass: HomeAssistant, skip_hass_states_is_state):
|
||||
assert entity.power_manager.overpowering_state is STATE_UNKNOWN
|
||||
assert entity.target_temperature == 18
|
||||
# waits that the heater starts
|
||||
await asyncio.sleep(0.1)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert mock_service_call.call_count >= 1
|
||||
assert entity.is_device_active is True
|
||||
|
||||
# Send power max mesurement
|
||||
await send_max_power_change_event(entity, 110, datetime.now())
|
||||
await send_max_power_change_event(entity, 110, now)
|
||||
# Send power mesurement (theheater is already in the power measurement)
|
||||
await send_power_change_event(entity, 100, datetime.now())
|
||||
await send_power_change_event(entity, 100, now)
|
||||
# No overpowering yet
|
||||
assert await entity.power_manager.check_overpowering() is False
|
||||
assert entity.power_manager.is_overpowering_detected is False
|
||||
# All configuration is complete and power is < power_max
|
||||
assert entity.preset_mode is PRESET_COMFORT
|
||||
assert entity.power_manager.overpowering_state is STATE_OFF
|
||||
@@ -359,33 +374,55 @@ async def test_bug_407(hass: HomeAssistant, skip_hass_states_is_state):
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
|
||||
new_callable=PropertyMock,
|
||||
return_value=True,
|
||||
), patch(
|
||||
"homeassistant.core.StateMachine.get",
|
||||
side_effect=side_effects.get_side_effects(),
|
||||
):
|
||||
now = now + timedelta(seconds=30)
|
||||
VersatileThermostatAPI.get_vtherm_api()._set_now(now)
|
||||
|
||||
# change preset to Boost
|
||||
await entity.async_set_preset_mode(PRESET_BOOST)
|
||||
# waits that the heater starts
|
||||
await asyncio.sleep(0.1)
|
||||
# doesn't work for call_later
|
||||
# await hass.async_block_till_done()
|
||||
|
||||
assert await entity.power_manager.check_overpowering() is False
|
||||
# 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.hvac_mode is HVACMode.HEAT
|
||||
assert entity.preset_mode is PRESET_BOOST
|
||||
assert entity.power_manager.overpowering_state is STATE_OFF
|
||||
assert entity.target_temperature == 19
|
||||
assert mock_service_call.call_count >= 1
|
||||
|
||||
# 3. if heater is stopped (is_device_active==False), then overpowering should be started
|
||||
# 3. Evenif heater is stopped (is_device_active==False) and power is over max, then overpowering should be started
|
||||
# due to check before start heating
|
||||
side_effects.add_or_update_side_effect("sensor.the_power_sensor", State("sensor.the_power_sensor", 150))
|
||||
with patch(
|
||||
"homeassistant.core.ServiceRegistry.async_call"
|
||||
) as mock_service_call, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
|
||||
new_callable=PropertyMock,
|
||||
return_value=False,
|
||||
), patch(
|
||||
"homeassistant.core.StateMachine.get",
|
||||
side_effect=side_effects.get_side_effects(),
|
||||
):
|
||||
# change preset to Boost
|
||||
now = now + timedelta(seconds=30)
|
||||
VersatileThermostatAPI.get_vtherm_api()._set_now(now)
|
||||
|
||||
# change preset to Comfort
|
||||
await entity.async_set_preset_mode(PRESET_COMFORT)
|
||||
# waits that the heater starts
|
||||
# waits the eventual heater starts
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
assert await entity.power_manager.check_overpowering() is True
|
||||
# simulate a refresh for central power (not necessary because it is checked before start)
|
||||
# await do_central_power_refresh(hass)
|
||||
|
||||
assert entity.power_manager.is_overpowering_detected is True
|
||||
assert entity.hvac_mode is HVACMode.HEAT
|
||||
assert entity.preset_mode is PRESET_POWER
|
||||
assert entity.power_manager.overpowering_state is STATE_ON
|
||||
@@ -445,8 +482,6 @@ 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
|
||||
@@ -456,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 True
|
||||
assert flow._infos[CONF_USE_POWER_FEATURE] is False
|
||||
assert flow._infos[CONF_USE_PRESENCE_FEATURE] is True
|
||||
assert flow._infos[CONF_USE_MOTION_FEATURE] is True
|
||||
|
||||
|
||||
@@ -188,6 +188,18 @@ async def test_full_over_switch_wo_central_config(
|
||||
hass: HomeAssistant, skip_hass_states_is_state, init_vtherm_api
|
||||
):
|
||||
"""Tests that a VTherm without any central_configuration is working with its own attributes"""
|
||||
|
||||
temps = {
|
||||
"frost": 10,
|
||||
"eco": 17,
|
||||
"comfort": 18,
|
||||
"boost": 21,
|
||||
"frost_away": 13,
|
||||
"eco_away": 13,
|
||||
"comfort_away": 13,
|
||||
"boost_away": 13,
|
||||
}
|
||||
|
||||
# Add a Switch VTherm
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
@@ -202,19 +214,11 @@ async def test_full_over_switch_wo_central_config(
|
||||
CONF_TEMP_MIN: 8,
|
||||
CONF_TEMP_MAX: 18,
|
||||
CONF_STEP_TEMPERATURE: 0.3,
|
||||
"frost_temp": 10,
|
||||
"eco_temp": 17,
|
||||
"comfort_temp": 18,
|
||||
"boost_temp": 21,
|
||||
"frost_away_temp": 13,
|
||||
"eco_away_temp": 13,
|
||||
"comfort_away_temp": 13,
|
||||
"boost_away_temp": 13,
|
||||
CONF_USE_WINDOW_FEATURE: True,
|
||||
CONF_USE_MOTION_FEATURE: True,
|
||||
CONF_USE_POWER_FEATURE: True,
|
||||
CONF_USE_PRESENCE_FEATURE: True,
|
||||
CONF_HEATER: "switch.mock_switch",
|
||||
CONF_UNDERLYING_LIST: ["switch.mock_switch"],
|
||||
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
|
||||
CONF_INVERSE_SWITCH: False,
|
||||
CONF_TPI_COEF_INT: 0.3,
|
||||
@@ -233,8 +237,6 @@ async def test_full_over_switch_wo_central_config(
|
||||
CONF_MOTION_PRESET: "comfort",
|
||||
CONF_NO_MOTION_PRESET: "eco",
|
||||
CONF_MOTION_SENSOR: "binary_sensor.mock_motion_sensor",
|
||||
CONF_POWER_SENSOR: "sensor.mock_power_sensor",
|
||||
CONF_MAX_POWER_SENSOR: "sensor.mock_max_power_sensor",
|
||||
CONF_PRESENCE_SENSOR: "binary_sensor.mock_presence_sensor",
|
||||
CONF_USE_MAIN_CENTRAL_CONFIG: False,
|
||||
CONF_USE_TPI_CENTRAL_CONFIG: False,
|
||||
@@ -249,7 +251,7 @@ async def test_full_over_switch_wo_central_config(
|
||||
|
||||
with patch("homeassistant.core.ServiceRegistry.async_call"):
|
||||
entity: ThermostatOverSwitch = await create_thermostat(
|
||||
hass, entry, "climate.theoverswitchmockname"
|
||||
hass, entry, "climate.theoverswitchmockname", temps
|
||||
)
|
||||
assert entity
|
||||
assert entity.name == "TheOverSwitchMockName"
|
||||
@@ -300,10 +302,13 @@ async def test_full_over_switch_wo_central_config(
|
||||
assert entity.motion_manager.motion_preset == "comfort"
|
||||
assert entity.motion_manager.no_motion_preset == "eco"
|
||||
|
||||
assert entity.power_manager.power_sensor_entity_id == "sensor.mock_power_sensor"
|
||||
assert (
|
||||
entity.power_manager.max_power_sensor_entity_id
|
||||
== "sensor.mock_max_power_sensor"
|
||||
VersatileThermostatAPI.get_vtherm_api().central_power_manager.power_sensor_entity_id
|
||||
is None
|
||||
)
|
||||
assert (
|
||||
VersatileThermostatAPI.get_vtherm_api().central_power_manager.max_power_sensor_entity_id
|
||||
is None
|
||||
)
|
||||
|
||||
assert (
|
||||
@@ -317,7 +322,7 @@ async def test_full_over_switch_wo_central_config(
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_full_over_switch_with_central_config(
|
||||
hass: HomeAssistant, skip_hass_states_is_state, init_central_config
|
||||
hass: HomeAssistant, skip_hass_states_is_state, init_central_power_manager
|
||||
):
|
||||
"""Tests that a VTherm with central_configuration is working with the central_config attributes"""
|
||||
# Add a Switch VTherm
|
||||
@@ -334,15 +339,11 @@ async def test_full_over_switch_with_central_config(
|
||||
CONF_TEMP_MIN: 8,
|
||||
CONF_TEMP_MAX: 18,
|
||||
CONF_STEP_TEMPERATURE: 0.3,
|
||||
"frost_temp": 10,
|
||||
"eco_temp": 17,
|
||||
"comfort_temp": 18,
|
||||
"boost_temp": 21,
|
||||
CONF_USE_WINDOW_FEATURE: True,
|
||||
CONF_USE_MOTION_FEATURE: True,
|
||||
CONF_USE_POWER_FEATURE: True,
|
||||
CONF_USE_PRESENCE_FEATURE: True,
|
||||
CONF_HEATER: "switch.mock_switch",
|
||||
CONF_UNDERLYING_LIST: ["switch.mock_switch"],
|
||||
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
|
||||
CONF_INVERSE_SWITCH: False,
|
||||
CONF_TPI_COEF_INT: 0.3,
|
||||
@@ -361,8 +362,6 @@ async def test_full_over_switch_with_central_config(
|
||||
CONF_MOTION_PRESET: "comfort",
|
||||
CONF_NO_MOTION_PRESET: "eco",
|
||||
CONF_MOTION_SENSOR: "binary_sensor.mock_motion_sensor",
|
||||
CONF_POWER_SENSOR: "sensor.mock_power_sensor",
|
||||
CONF_MAX_POWER_SENSOR: "sensor.mock_max_power_sensor",
|
||||
CONF_PRESENCE_SENSOR: "binary_sensor.mock_presence_sensor",
|
||||
CONF_USE_MAIN_CENTRAL_CONFIG: True,
|
||||
CONF_USE_TPI_CENTRAL_CONFIG: True,
|
||||
@@ -426,10 +425,13 @@ async def test_full_over_switch_with_central_config(
|
||||
assert entity.motion_manager.motion_preset == "boost"
|
||||
assert entity.motion_manager.no_motion_preset == "frost"
|
||||
|
||||
assert entity.power_manager.power_sensor_entity_id == "sensor.mock_power_sensor"
|
||||
assert (
|
||||
entity.power_manager.max_power_sensor_entity_id
|
||||
== "sensor.mock_max_power_sensor"
|
||||
VersatileThermostatAPI.get_vtherm_api().central_power_manager.power_sensor_entity_id
|
||||
== "sensor.the_power_sensor"
|
||||
)
|
||||
assert (
|
||||
VersatileThermostatAPI.get_vtherm_api().central_power_manager.max_power_sensor_entity_id
|
||||
== "sensor.the_max_power_sensor"
|
||||
)
|
||||
|
||||
assert (
|
||||
@@ -450,7 +452,7 @@ async def test_over_switch_with_central_config_but_no_central_config(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["type"] == FlowResultType.FORM
|
||||
assert result["step_id"] == SOURCE_USER
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
|
||||
702
tests/test_central_power_manager.py
Normal file
702
tests/test_central_power_manager.py
Normal file
@@ -0,0 +1,702 @@
|
||||
# pylint: disable=protected-access, unused-argument, line-too-long
|
||||
""" Test the Central Power management """
|
||||
from unittest.mock import patch, AsyncMock, MagicMock, PropertyMock
|
||||
from datetime import datetime, timedelta
|
||||
import logging
|
||||
|
||||
from custom_components.versatile_thermostat.feature_power_manager import (
|
||||
FeaturePowerManager,
|
||||
)
|
||||
from custom_components.versatile_thermostat.central_feature_power_manager import (
|
||||
CentralFeaturePowerManager,
|
||||
)
|
||||
from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import
|
||||
|
||||
logging.getLogger().setLevel(logging.DEBUG)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"use_power_feature, power_entity_id, max_power_entity_id, power_temp, is_configured",
|
||||
[
|
||||
(True, "sensor.power_id", "sensor.max_power_id", 13, True),
|
||||
(True, None, "sensor.max_power_id", 13, False),
|
||||
(True, "sensor.power_id", None, 13, False),
|
||||
(True, "sensor.power_id", "sensor.max_power_id", None, False),
|
||||
(False, "sensor.power_id", "sensor.max_power_id", 13, False),
|
||||
],
|
||||
)
|
||||
async def test_central_power_manager_init(
|
||||
hass: HomeAssistant,
|
||||
use_power_feature,
|
||||
power_entity_id,
|
||||
max_power_entity_id,
|
||||
power_temp,
|
||||
is_configured,
|
||||
):
|
||||
"""Test creation and post_init of the Central Power Manager"""
|
||||
vtherm_api: VersatileThermostatAPI = MagicMock(spec=VersatileThermostatAPI)
|
||||
central_power_manager = CentralFeaturePowerManager(hass, vtherm_api)
|
||||
|
||||
assert central_power_manager.is_configured is False
|
||||
assert central_power_manager.current_max_power is None
|
||||
assert central_power_manager.current_power is None
|
||||
assert central_power_manager.power_temperature is None
|
||||
assert central_power_manager.name == "centralPowerManager"
|
||||
|
||||
# 2. post_init
|
||||
central_power_manager.post_init(
|
||||
{
|
||||
CONF_POWER_SENSOR: power_entity_id,
|
||||
CONF_MAX_POWER_SENSOR: max_power_entity_id,
|
||||
CONF_USE_POWER_FEATURE: use_power_feature,
|
||||
CONF_PRESET_POWER: power_temp,
|
||||
}
|
||||
)
|
||||
|
||||
assert central_power_manager.is_configured == is_configured
|
||||
assert central_power_manager.current_max_power is None
|
||||
assert central_power_manager.current_power is None
|
||||
assert central_power_manager.power_temperature == power_temp
|
||||
|
||||
# 3. start listening
|
||||
await central_power_manager.start_listening()
|
||||
assert len(central_power_manager._active_listener) == (2 if is_configured else 0)
|
||||
|
||||
# 4. stop listening
|
||||
central_power_manager.stop_listening()
|
||||
assert len(central_power_manager._active_listener) == 0
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"vtherm_configs, results",
|
||||
[
|
||||
# simple sort
|
||||
(
|
||||
[
|
||||
{
|
||||
"name": "vtherm1",
|
||||
"is_configured": True,
|
||||
"is_on": True,
|
||||
"current_temperature": 13,
|
||||
"target_temperature": 12,
|
||||
"saved_target_temp": 18,
|
||||
"is_overpowering_detected": False,
|
||||
},
|
||||
{
|
||||
"name": "vtherm2",
|
||||
"is_configured": True,
|
||||
"is_on": True,
|
||||
"current_temperature": 18,
|
||||
"target_temperature": 12,
|
||||
"saved_target_temp": 18,
|
||||
"is_overpowering_detected": False,
|
||||
},
|
||||
{
|
||||
"name": "vtherm3",
|
||||
"is_configured": True,
|
||||
"is_on": True,
|
||||
"current_temperature": 12,
|
||||
"target_temperature": 18,
|
||||
"saved_target_temp": 18,
|
||||
"is_overpowering_detected": False,
|
||||
},
|
||||
],
|
||||
["vtherm2", "vtherm1", "vtherm3"],
|
||||
),
|
||||
# Ignore power not configured and not on
|
||||
(
|
||||
[
|
||||
{
|
||||
"name": "vtherm1",
|
||||
"is_configured": False,
|
||||
"is_on": True,
|
||||
"current_temperature": 13,
|
||||
"target_temperature": 12,
|
||||
"saved_target_temp": 18,
|
||||
"is_overpowering_detected": False,
|
||||
},
|
||||
{
|
||||
"name": "vtherm2",
|
||||
"is_configured": True,
|
||||
"is_on": False,
|
||||
"current_temperature": 18,
|
||||
"target_temperature": 12,
|
||||
"saved_target_temp": 18,
|
||||
"is_overpowering_detected": False,
|
||||
},
|
||||
{
|
||||
"name": "vtherm3",
|
||||
"is_configured": True,
|
||||
"is_on": True,
|
||||
"current_temperature": 12,
|
||||
"target_temperature": 18,
|
||||
"saved_target_temp": 18,
|
||||
"is_overpowering_detected": False,
|
||||
},
|
||||
],
|
||||
["vtherm3"],
|
||||
),
|
||||
# None current_temperature are in last
|
||||
(
|
||||
[
|
||||
{
|
||||
"name": "vtherm1",
|
||||
"is_configured": True,
|
||||
"is_on": True,
|
||||
"current_temperature": 13,
|
||||
"target_temperature": 12,
|
||||
"saved_target_temp": 18,
|
||||
"is_overpowering_detected": False,
|
||||
},
|
||||
{
|
||||
"name": "vtherm2",
|
||||
"is_configured": True,
|
||||
"is_on": True,
|
||||
"current_temperature": None,
|
||||
"target_temperature": 12,
|
||||
"saved_target_temp": 18,
|
||||
"is_overpowering_detected": False,
|
||||
},
|
||||
{
|
||||
"name": "vtherm3",
|
||||
"is_configured": True,
|
||||
"is_on": True,
|
||||
"current_temperature": 12,
|
||||
"target_temperature": 18,
|
||||
"saved_target_temp": 18,
|
||||
"is_overpowering_detected": False,
|
||||
},
|
||||
],
|
||||
["vtherm1", "vtherm3", "vtherm2"],
|
||||
),
|
||||
# None target_temperature are in last
|
||||
(
|
||||
[
|
||||
{
|
||||
"name": "vtherm1",
|
||||
"is_configured": True,
|
||||
"is_on": True,
|
||||
"current_temperature": 13,
|
||||
"target_temperature": 12,
|
||||
"saved_target_temp": 18,
|
||||
"is_overpowering_detected": False,
|
||||
},
|
||||
{
|
||||
"name": "vtherm2",
|
||||
"is_configured": True,
|
||||
"is_on": True,
|
||||
"current_temperature": 18,
|
||||
"target_temperature": None,
|
||||
"saved_target_temp": 18,
|
||||
"is_overpowering_detected": False,
|
||||
},
|
||||
{
|
||||
"name": "vtherm3",
|
||||
"is_configured": True,
|
||||
"is_on": True,
|
||||
"current_temperature": 12,
|
||||
"target_temperature": 18,
|
||||
"saved_target_temp": 18,
|
||||
"is_overpowering_detected": False,
|
||||
},
|
||||
],
|
||||
["vtherm1", "vtherm3", "vtherm2"],
|
||||
),
|
||||
# simple sort with overpowering detected
|
||||
(
|
||||
[
|
||||
{
|
||||
"name": "vtherm1",
|
||||
"is_configured": True,
|
||||
"is_on": True,
|
||||
"current_temperature": 13,
|
||||
# "target_temperature": 12,
|
||||
"saved_target_temp": 21,
|
||||
"is_overpowering_detected": True,
|
||||
},
|
||||
{
|
||||
"name": "vtherm2",
|
||||
"is_configured": True,
|
||||
"is_on": True,
|
||||
"current_temperature": 18,
|
||||
# "target_temperature": 12,
|
||||
"saved_target_temp": 17,
|
||||
"is_overpowering_detected": True,
|
||||
},
|
||||
{
|
||||
"name": "vtherm3",
|
||||
"is_configured": True,
|
||||
"is_on": True,
|
||||
"current_temperature": 12,
|
||||
# "target_temperature": 18,
|
||||
"saved_target_temp": 16,
|
||||
"is_overpowering_detected": True,
|
||||
},
|
||||
],
|
||||
["vtherm2", "vtherm3", "vtherm1"],
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_central_power_manageer_find_vtherms(
|
||||
hass: HomeAssistant, vtherm_configs, results
|
||||
):
|
||||
"""Test the find_all_vtherm_with_power_management_sorted_by_dtemp"""
|
||||
vtherm_api: VersatileThermostatAPI = MagicMock(spec=VersatileThermostatAPI)
|
||||
central_power_manager = CentralFeaturePowerManager(hass, vtherm_api)
|
||||
|
||||
vtherms = []
|
||||
for vtherm_config in vtherm_configs:
|
||||
vtherm = MagicMock(spec=BaseThermostat)
|
||||
vtherm.name = vtherm_config.get("name")
|
||||
vtherm.is_on = vtherm_config.get("is_on")
|
||||
vtherm.current_temperature = vtherm_config.get("current_temperature")
|
||||
vtherm.target_temperature = vtherm_config.get("target_temperature")
|
||||
vtherm.saved_target_temp = vtherm_config.get("saved_target_temp")
|
||||
vtherm.power_manager.is_configured = vtherm_config.get("is_configured")
|
||||
vtherm.power_manager.is_overpowering_detected = vtherm_config.get("is_overpowering_detected")
|
||||
vtherms.append(vtherm)
|
||||
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.central_feature_power_manager.CentralFeaturePowerManager.get_climate_components_entities",
|
||||
return_value=vtherms,
|
||||
):
|
||||
vtherm_sorted = (
|
||||
central_power_manager.find_all_vtherm_with_power_management_sorted_by_dtemp()
|
||||
)
|
||||
|
||||
# extract results
|
||||
vtherm_results = [vtherm.name for vtherm in vtherm_sorted]
|
||||
|
||||
assert vtherm_results == results
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"current_power, current_max_power, vtherm_configs, expected_results",
|
||||
[
|
||||
# simple nominal test (initialize overpowering state in VTherm)
|
||||
(
|
||||
1000,
|
||||
5000,
|
||||
[
|
||||
{
|
||||
"name": "vtherm1",
|
||||
"device_power": 100,
|
||||
"is_device_active": False,
|
||||
"is_over_climate": False,
|
||||
"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},
|
||||
),
|
||||
# Un-shedding only (will be taken in reverse order)
|
||||
(
|
||||
1000,
|
||||
2000,
|
||||
[
|
||||
# should be not unshedded (too much power will be added)
|
||||
{
|
||||
"name": "vtherm1",
|
||||
"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
|
||||
{
|
||||
"name": "vtherm4",
|
||||
"device_power": 300,
|
||||
"is_device_active": False,
|
||||
"is_over_climate": False,
|
||||
"nb_underlying_entities": 1,
|
||||
"on_percent": 1,
|
||||
"is_overpowering_detected": True,
|
||||
"overpowering_state": STATE_ON,
|
||||
},
|
||||
],
|
||||
{"vtherm4": False, "vtherm3": False},
|
||||
),
|
||||
# Shedding
|
||||
(
|
||||
2000,
|
||||
1000,
|
||||
[
|
||||
# should be overpowering
|
||||
{
|
||||
"name": "vtherm1",
|
||||
"device_power": 300,
|
||||
"is_device_active": True,
|
||||
"is_over_climate": False,
|
||||
"nb_underlying_entities": 1,
|
||||
"on_percent": 1,
|
||||
"is_overpowering_detected": False,
|
||||
"overpowering_state": STATE_OFF,
|
||||
},
|
||||
# should be overpowering with many underlmying entities
|
||||
{
|
||||
"name": "vtherm2",
|
||||
"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,
|
||||
},
|
||||
# over_climate should be overpowering
|
||||
{
|
||||
"name": "vtherm3",
|
||||
"device_power": 100,
|
||||
"is_device_active": True,
|
||||
"is_over_climate": True,
|
||||
"is_overpowering_detected": False,
|
||||
"overpowering_state": STATE_OFF,
|
||||
},
|
||||
# should pass cause not active
|
||||
{
|
||||
"name": "vtherm4",
|
||||
"device_power": 800,
|
||||
"is_device_active": False,
|
||||
"is_over_climate": False,
|
||||
"nb_underlying_entities": 1,
|
||||
"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},
|
||||
),
|
||||
],
|
||||
)
|
||||
# @pytest.mark.skip
|
||||
async def test_central_power_manageer_calculate_shedding(
|
||||
hass: HomeAssistant,
|
||||
current_power,
|
||||
current_max_power,
|
||||
vtherm_configs,
|
||||
expected_results,
|
||||
):
|
||||
"""Test the calculate_shedding of the CentralPowerManager"""
|
||||
vtherm_api: VersatileThermostatAPI = MagicMock(spec=VersatileThermostatAPI)
|
||||
central_power_manager = CentralFeaturePowerManager(hass, vtherm_api)
|
||||
|
||||
registered_calls = {}
|
||||
|
||||
def register_call(vtherm, overpowering):
|
||||
"""Register a call to set_overpowering"""
|
||||
registered_calls.update({vtherm.name: overpowering})
|
||||
|
||||
vtherms = []
|
||||
for vtherm_config in vtherm_configs:
|
||||
vtherm = MagicMock(spec=BaseThermostat)
|
||||
vtherm.name = vtherm_config.get("name")
|
||||
vtherm.is_device_active = vtherm_config.get("is_device_active")
|
||||
vtherm.is_over_climate = vtherm_config.get("is_over_climate")
|
||||
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.power_manager = MagicMock(spec=FeaturePowerManager)
|
||||
vtherm.power_manager._vtherm = vtherm
|
||||
|
||||
vtherm.power_manager.is_overpowering_detected = vtherm_config.get(
|
||||
"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
|
||||
):
|
||||
register_call(v, overpowering)
|
||||
|
||||
vtherm.power_manager.set_overpowering = mock_set_overpowering
|
||||
|
||||
vtherms.append(vtherm)
|
||||
|
||||
# fmt:off
|
||||
with patch("custom_components.versatile_thermostat.central_feature_power_manager.CentralFeaturePowerManager.find_all_vtherm_with_power_management_sorted_by_dtemp", return_value=vtherms), \
|
||||
patch("custom_components.versatile_thermostat.central_feature_power_manager.CentralFeaturePowerManager.current_max_power", new_callable=PropertyMock, return_value=current_max_power), \
|
||||
patch("custom_components.versatile_thermostat.central_feature_power_manager.CentralFeaturePowerManager.current_power", new_callable=PropertyMock, return_value=current_power), \
|
||||
patch("custom_components.versatile_thermostat.central_feature_power_manager.CentralFeaturePowerManager.is_configured", new_callable=PropertyMock, return_value=True):
|
||||
# fmt:on
|
||||
|
||||
await central_power_manager.calculate_shedding()
|
||||
|
||||
# Check registered calls
|
||||
assert registered_calls == expected_results
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"dsecs, power, nb_call",
|
||||
[
|
||||
(0, 1000, 1),
|
||||
(0, None, 0),
|
||||
(0, STATE_UNAVAILABLE, 0),
|
||||
(0, STATE_UNKNOWN, 0),
|
||||
(21, 1000, 1),
|
||||
(19, 1000, 1),
|
||||
],
|
||||
)
|
||||
async def test_central_power_manager_power_event(
|
||||
hass: HomeAssistant, dsecs, power, nb_call
|
||||
):
|
||||
"""Tests the Power sensor event"""
|
||||
vtherm_api: VersatileThermostatAPI = MagicMock(spec=VersatileThermostatAPI)
|
||||
central_power_manager = CentralFeaturePowerManager(hass, vtherm_api)
|
||||
|
||||
assert central_power_manager.current_power is None
|
||||
assert central_power_manager.power_temperature is None
|
||||
assert central_power_manager.name == "centralPowerManager"
|
||||
|
||||
# 2. post_init
|
||||
central_power_manager.post_init(
|
||||
{
|
||||
CONF_POWER_SENSOR: "sensor.power_entity_id",
|
||||
CONF_MAX_POWER_SENSOR: "sensor.max_power_entity_id",
|
||||
CONF_USE_POWER_FEATURE: True,
|
||||
CONF_PRESET_POWER: 13,
|
||||
}
|
||||
)
|
||||
|
||||
assert central_power_manager.is_configured is True
|
||||
assert central_power_manager.current_max_power is None
|
||||
assert central_power_manager.current_power is None
|
||||
assert central_power_manager.power_temperature == 13
|
||||
|
||||
# 3. start listening (not really useful but don't eat bread)
|
||||
await central_power_manager.start_listening()
|
||||
assert len(central_power_manager._active_listener) == 2
|
||||
|
||||
now: datetime = NowClass.get_now(hass)
|
||||
# vtherm_api._set_now(now) vtherm_api is a MagicMock
|
||||
vtherm_api.now = now
|
||||
|
||||
# 4. Call the _power_sensor_changed
|
||||
side_effects = SideEffects(
|
||||
{
|
||||
"sensor.power_entity_id": State("sensor.power_entity_id", power),
|
||||
"sensor.max_power_entity_id": State("sensor.max_power_entity_id", power),
|
||||
},
|
||||
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.central_feature_power_manager.CentralFeaturePowerManager.calculate_shedding", new_callable=AsyncMock) as mock_calculate_shedding:
|
||||
# fmt:on
|
||||
# set a default value to see if it has been replaced
|
||||
central_power_manager._current_power = -999
|
||||
await central_power_manager._power_sensor_changed(event=Event(
|
||||
event_type=EVENT_STATE_CHANGED,
|
||||
data={
|
||||
"entity_id": "sensor.power_entity_id",
|
||||
"new_state": State("sensor.power_entity_id", power),
|
||||
"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
|
||||
|
||||
# Do another call x seconds later
|
||||
now = now + timedelta(seconds=dsecs)
|
||||
vtherm_api.now = now
|
||||
# fmt:off
|
||||
with patch("homeassistant.core.StateMachine.get", side_effect=side_effects.get_side_effects()), \
|
||||
patch("custom_components.versatile_thermostat.central_feature_power_manager.CentralFeaturePowerManager.calculate_shedding", new_callable=AsyncMock) as mock_calculate_shedding:
|
||||
# fmt:on
|
||||
central_power_manager._current_power = -999
|
||||
|
||||
await central_power_manager._power_sensor_changed(event=Event(
|
||||
event_type=EVENT_STATE_CHANGED,
|
||||
data={
|
||||
"entity_id": "sensor.power_entity_id",
|
||||
"new_state": State("sensor.power_entity_id", power),
|
||||
"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
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"dsecs, max_power, nb_call",
|
||||
[
|
||||
(0, 1000, 1),
|
||||
(0, None, 0),
|
||||
(0, STATE_UNAVAILABLE, 0),
|
||||
(0, STATE_UNKNOWN, 0),
|
||||
(21, 1000, 1),
|
||||
(19, 1000, 1),
|
||||
],
|
||||
)
|
||||
async def test_central_power_manager_max_power_event(
|
||||
hass: HomeAssistant, dsecs, max_power, nb_call
|
||||
):
|
||||
"""Tests the Power sensor event"""
|
||||
vtherm_api: VersatileThermostatAPI = MagicMock(spec=VersatileThermostatAPI)
|
||||
central_power_manager = CentralFeaturePowerManager(hass, vtherm_api)
|
||||
|
||||
assert central_power_manager.current_power is None
|
||||
assert central_power_manager.power_temperature is None
|
||||
assert central_power_manager.name == "centralPowerManager"
|
||||
|
||||
# 2. post_init
|
||||
central_power_manager.post_init(
|
||||
{
|
||||
CONF_POWER_SENSOR: "sensor.power_entity_id",
|
||||
CONF_MAX_POWER_SENSOR: "sensor.max_power_entity_id",
|
||||
CONF_USE_POWER_FEATURE: True,
|
||||
CONF_PRESET_POWER: 13,
|
||||
}
|
||||
)
|
||||
|
||||
assert central_power_manager.is_configured is True
|
||||
assert central_power_manager.current_max_power is None
|
||||
assert central_power_manager.current_power is None
|
||||
assert central_power_manager.power_temperature == 13
|
||||
|
||||
# 3. start listening (not really useful but don't eat bread)
|
||||
await central_power_manager.start_listening()
|
||||
assert len(central_power_manager._active_listener) == 2
|
||||
|
||||
now: datetime = NowClass.get_now(hass)
|
||||
# vtherm_api._set_now(now) vtherm_api is a MagicMock
|
||||
vtherm_api.now = now
|
||||
|
||||
# 4. Call the _power_sensor_changed
|
||||
side_effects = SideEffects(
|
||||
{
|
||||
"sensor.power_entity_id": State("sensor.power_entity_id", max_power),
|
||||
"sensor.max_power_entity_id": State(
|
||||
"sensor.max_power_entity_id", max_power
|
||||
),
|
||||
},
|
||||
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.central_feature_power_manager.CentralFeaturePowerManager.calculate_shedding", new_callable=AsyncMock) as mock_calculate_shedding:
|
||||
# fmt:on
|
||||
# set a default value to see if it has been replaced
|
||||
central_power_manager._current_max_power = -999
|
||||
await central_power_manager._power_sensor_changed(event=Event(
|
||||
event_type=EVENT_STATE_CHANGED,
|
||||
data={
|
||||
"entity_id": "sensor.max_power_entity_id",
|
||||
"new_state": State("sensor.max_power_entity_id", max_power),
|
||||
"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
|
||||
|
||||
# Do another call x seconds later
|
||||
now = now + timedelta(seconds=dsecs)
|
||||
vtherm_api.now = now
|
||||
# fmt:off
|
||||
with patch("homeassistant.core.StateMachine.get", side_effect=side_effects.get_side_effects()), \
|
||||
patch("custom_components.versatile_thermostat.central_feature_power_manager.CentralFeaturePowerManager.calculate_shedding", new_callable=AsyncMock) as mock_calculate_shedding:
|
||||
# fmt:on
|
||||
central_power_manager._current_max_power = -999
|
||||
|
||||
await central_power_manager._power_sensor_changed(event=Event(
|
||||
event_type=EVENT_STATE_CHANGED,
|
||||
data={
|
||||
"entity_id": "sensor.max_power_entity_id",
|
||||
"new_state": State("sensor.max_power_entity_id", max_power),
|
||||
"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
|
||||
@@ -287,6 +287,7 @@ async def test_user_config_flow_over_switch(
|
||||
CONF_USE_POWER_FEATURE: True,
|
||||
CONF_USE_PRESENCE_FEATURE: True,
|
||||
CONF_USE_CENTRAL_BOILER_FEATURE: False,
|
||||
CONF_AUTO_START_STOP_LEVEL: AUTO_START_STOP_LEVEL_NONE,
|
||||
}
|
||||
)
|
||||
assert result["result"]
|
||||
@@ -492,9 +493,7 @@ async def test_user_config_flow_over_climate(
|
||||
)
|
||||
assert result["type"] == FlowResultType.CREATE_ENTRY
|
||||
assert result.get("errors") is None
|
||||
assert result[
|
||||
"data"
|
||||
] == MOCK_TH_OVER_CLIMATE_USER_CONFIG | MOCK_TH_OVER_CLIMATE_MAIN_CONFIG | MOCK_TH_OVER_CLIMATE_CENTRAL_MAIN_CONFIG | MOCK_TH_OVER_CLIMATE_TYPE_CONFIG | {
|
||||
assert result["data"] == MOCK_TH_OVER_CLIMATE_USER_CONFIG | MOCK_TH_OVER_CLIMATE_MAIN_CONFIG | MOCK_TH_OVER_CLIMATE_CENTRAL_MAIN_CONFIG | MOCK_TH_OVER_CLIMATE_TYPE_CONFIG | {
|
||||
CONF_MINIMAL_ACTIVATION_DELAY: 10,
|
||||
CONF_SAFETY_DELAY_MIN: 5,
|
||||
CONF_SAFETY_MIN_ON_PERCENT: 0.4,
|
||||
@@ -517,6 +516,7 @@ async def test_user_config_flow_over_climate(
|
||||
CONF_USED_BY_CENTRAL_BOILER: False,
|
||||
CONF_USE_CENTRAL_MODE: False,
|
||||
CONF_AUTO_REGULATION_MODE: CONF_AUTO_REGULATION_STRONG,
|
||||
CONF_AUTO_START_STOP_LEVEL: AUTO_START_STOP_LEVEL_NONE,
|
||||
}
|
||||
assert result["result"]
|
||||
assert result["result"].domain == DOMAIN
|
||||
@@ -1378,6 +1378,7 @@ async def test_user_config_flow_over_switch_bug_552_tpi(
|
||||
CONF_USE_POWER_FEATURE: False,
|
||||
CONF_USE_PRESENCE_FEATURE: False,
|
||||
CONF_USE_CENTRAL_BOILER_FEATURE: False,
|
||||
CONF_AUTO_START_STOP_LEVEL: AUTO_START_STOP_LEVEL_NONE,
|
||||
}
|
||||
)
|
||||
assert result["result"]
|
||||
@@ -1681,9 +1682,7 @@ async def test_user_config_flow_over_climate_valve(
|
||||
)
|
||||
assert result["type"] == FlowResultType.CREATE_ENTRY
|
||||
assert result.get("errors") is None
|
||||
assert result[
|
||||
"data"
|
||||
] == MOCK_TH_OVER_CLIMATE_USER_CONFIG | MOCK_TH_OVER_CLIMATE_MAIN_CONFIG | MOCK_TH_OVER_CLIMATE_CENTRAL_MAIN_CONFIG | MOCK_TH_OVER_CLIMATE_TYPE_CONFIG | {
|
||||
assert result["data"] == MOCK_TH_OVER_CLIMATE_USER_CONFIG | MOCK_TH_OVER_CLIMATE_MAIN_CONFIG | MOCK_TH_OVER_CLIMATE_CENTRAL_MAIN_CONFIG | MOCK_TH_OVER_CLIMATE_TYPE_CONFIG | {
|
||||
CONF_MINIMAL_ACTIVATION_DELAY: 10,
|
||||
CONF_SAFETY_DELAY_MIN: 5,
|
||||
CONF_SAFETY_MIN_ON_PERCENT: 0.4,
|
||||
@@ -1717,6 +1716,7 @@ async def test_user_config_flow_over_climate_valve(
|
||||
CONF_TPI_COEF_INT: 0.3,
|
||||
CONF_TPI_COEF_EXT: 0.1,
|
||||
CONF_MIN_OPENING_DEGREES: "10, 20,0",
|
||||
CONF_AUTO_START_STOP_LEVEL: AUTO_START_STOP_LEVEL_NONE,
|
||||
}
|
||||
assert result["result"]
|
||||
assert result["result"].domain == DOMAIN
|
||||
|
||||
@@ -90,7 +90,7 @@ async def test_motion_feature_manager_refresh(
|
||||
assert custom_attributes["motion_off_delay_sec"] == 30
|
||||
|
||||
# 3. start listening
|
||||
motion_manager.start_listening()
|
||||
await 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,
|
||||
}
|
||||
)
|
||||
motion_manager.start_listening()
|
||||
await motion_manager.start_listening()
|
||||
|
||||
# 2. test _motion_sensor_changed with the parametrized
|
||||
# fmt: off
|
||||
|
||||
@@ -721,10 +721,14 @@ async def test_multiple_climates_underlying_changes_not_aligned(
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_multiple_switch_power_management(
|
||||
hass: HomeAssistant, skip_hass_states_is_state
|
||||
hass: HomeAssistant, skip_hass_states_is_state, init_central_power_manager
|
||||
):
|
||||
"""Test the Power management"""
|
||||
|
||||
temps = {
|
||||
"eco": 17,
|
||||
"comfort": 18,
|
||||
"boost": 19,
|
||||
}
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
title="TheOverSwitchMockName",
|
||||
@@ -737,17 +741,16 @@ async def test_multiple_switch_power_management(
|
||||
CONF_CYCLE_MIN: 8,
|
||||
CONF_TEMP_MIN: 15,
|
||||
CONF_TEMP_MAX: 30,
|
||||
"eco_temp": 17,
|
||||
"comfort_temp": 18,
|
||||
"boost_temp": 19,
|
||||
CONF_USE_WINDOW_FEATURE: False,
|
||||
CONF_USE_MOTION_FEATURE: False,
|
||||
CONF_USE_POWER_FEATURE: True,
|
||||
CONF_USE_PRESENCE_FEATURE: False,
|
||||
CONF_HEATER: "switch.mock_switch1",
|
||||
CONF_HEATER_2: "switch.mock_switch2",
|
||||
CONF_HEATER_3: "switch.mock_switch3",
|
||||
CONF_HEATER_4: "switch.mock_switch4",
|
||||
CONF_UNDERLYING_LIST: [
|
||||
"switch.mock_switch1",
|
||||
"switch.mock_switch2",
|
||||
"switch.mock_switch3",
|
||||
"switch.mock_switch4",
|
||||
],
|
||||
CONF_HEATER_KEEP_ALIVE: 0,
|
||||
CONF_MINIMAL_ACTIVATION_DELAY: 30,
|
||||
CONF_SAFETY_DELAY_MIN: 5,
|
||||
@@ -755,15 +758,13 @@ async def test_multiple_switch_power_management(
|
||||
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
|
||||
CONF_TPI_COEF_INT: 0.3,
|
||||
CONF_TPI_COEF_EXT: 0.01,
|
||||
CONF_POWER_SENSOR: "sensor.mock_power_sensor",
|
||||
CONF_MAX_POWER_SENSOR: "sensor.mock_power_max_sensor",
|
||||
CONF_DEVICE_POWER: 100,
|
||||
CONF_PRESET_POWER: 12,
|
||||
},
|
||||
)
|
||||
|
||||
entity: BaseThermostat = await create_thermostat(
|
||||
hass, entry, "climate.theover4switchmockname"
|
||||
hass, entry, "climate.theover4switchmockname", temps
|
||||
)
|
||||
assert entity
|
||||
assert entity.is_over_climate is False
|
||||
@@ -772,6 +773,9 @@ async def test_multiple_switch_power_management(
|
||||
tpi_algo = entity._prop_algorithm
|
||||
assert tpi_algo
|
||||
|
||||
now: datetime = NowClass.get_now(hass)
|
||||
VersatileThermostatAPI.get_vtherm_api()._set_now(now)
|
||||
|
||||
await entity.async_set_hvac_mode(HVACMode.HEAT)
|
||||
await entity.async_set_preset_mode(PRESET_BOOST)
|
||||
assert entity.hvac_mode is HVACMode.HEAT
|
||||
@@ -779,77 +783,109 @@ 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
|
||||
await send_power_change_event(entity, 50, datetime.now())
|
||||
side_effects = SideEffects(
|
||||
{
|
||||
"sensor.the_power_sensor": State("sensor.the_power_sensor", 50),
|
||||
"sensor.the_max_power_sensor": State("sensor.the_max_power_sensor", 300),
|
||||
},
|
||||
State("unknown.entity_id", "unknown"),
|
||||
)
|
||||
|
||||
# Send power max mesurement
|
||||
await send_max_power_change_event(entity, 300, datetime.now())
|
||||
assert await entity.power_manager.check_overpowering() is False
|
||||
# All configuration is complete and power is < power_max
|
||||
assert entity.preset_mode is PRESET_BOOST
|
||||
assert entity.power_manager.overpowering_state is STATE_OFF
|
||||
# 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_power_change_event(entity, 50, datetime.now())
|
||||
await send_max_power_change_event(entity, 300, datetime.now())
|
||||
assert entity.power_manager.is_overpowering_detected is False
|
||||
# All configuration is complete and power is < power_max
|
||||
assert entity.preset_mode is PRESET_BOOST
|
||||
assert entity.power_manager.overpowering_state is STATE_OFF
|
||||
|
||||
# 2. Send power max mesurement too low and HVACMode is on
|
||||
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:
|
||||
# 100 of the device / 4 -> 25, current power 50 so max is 75
|
||||
await send_max_power_change_event(entity, 74, datetime.now())
|
||||
assert await entity.power_manager.check_overpowering() is True
|
||||
# All configuration is complete and power is > power_max we switch to POWER preset
|
||||
assert entity.preset_mode is PRESET_POWER
|
||||
assert entity.power_manager.overpowering_state is STATE_ON
|
||||
assert entity.target_temperature == 12
|
||||
side_effects.add_or_update_side_effect("sensor.the_max_power_sensor", State("sensor.the_max_power_sensor", 49))
|
||||
|
||||
assert mock_send_event.call_count == 2
|
||||
mock_send_event.assert_has_calls(
|
||||
[
|
||||
call.send_event(EventType.PRESET_EVENT, {"preset": PRESET_POWER}),
|
||||
call.send_event(
|
||||
EventType.POWER_EVENT,
|
||||
{
|
||||
"type": "start",
|
||||
"current_power": 50,
|
||||
"device_power": 100,
|
||||
"current_max_power": 74,
|
||||
"current_power_consumption": 25.0,
|
||||
},
|
||||
),
|
||||
],
|
||||
any_order=True,
|
||||
)
|
||||
assert mock_heater_on.call_count == 0
|
||||
assert mock_heater_off.call_count == 4 # The fourth are shutdown
|
||||
#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())
|
||||
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
|
||||
assert entity.power_manager.overpowering_state is STATE_ON
|
||||
assert entity.target_temperature == 12
|
||||
|
||||
assert mock_send_event.call_count == 2
|
||||
mock_send_event.assert_has_calls(
|
||||
[
|
||||
call.send_event(EventType.PRESET_EVENT, {"preset": PRESET_POWER}),
|
||||
call.send_event(
|
||||
EventType.POWER_EVENT,
|
||||
{
|
||||
"type": "start",
|
||||
"current_power": 50,
|
||||
"device_power": 100,
|
||||
"current_max_power": 49,
|
||||
"current_power_consumption": 100,
|
||||
},
|
||||
),
|
||||
],
|
||||
any_order=True,
|
||||
)
|
||||
assert mock_heater_on.call_count == 0
|
||||
assert mock_heater_off.call_count == 4 # The fourth are shutdown
|
||||
|
||||
# 3. change PRESET
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
|
||||
) as mock_send_event:
|
||||
await entity.async_set_preset_mode(PRESET_ECO)
|
||||
assert entity.preset_mode is PRESET_ECO
|
||||
# No change
|
||||
assert entity.power_manager.overpowering_state is STATE_ON
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
|
||||
) as mock_send_event:
|
||||
now = now + timedelta(seconds=30)
|
||||
VersatileThermostatAPI.get_vtherm_api()._set_now(now)
|
||||
|
||||
await entity.async_set_preset_mode(PRESET_ECO)
|
||||
assert entity.preset_mode is PRESET_ECO
|
||||
# No change cause temperature is very low
|
||||
assert entity.power_manager.overpowering_state is STATE_ON
|
||||
|
||||
# 4. Send hugh power max mesurement to release overpowering
|
||||
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:
|
||||
# 100 of the device / 4 -> 25, current power 50 so max is 75. With 150 no overheating
|
||||
await send_max_power_change_event(entity, 150, datetime.now())
|
||||
assert await entity.power_manager.check_overpowering() is False
|
||||
# All configuration is complete and power is > power_max we switch to POWER preset
|
||||
assert entity.preset_mode is PRESET_ECO
|
||||
assert entity.power_manager.overpowering_state is STATE_OFF
|
||||
assert entity.target_temperature == 17
|
||||
side_effects.add_or_update_side_effect("sensor.the_max_power_sensor", State("sensor.the_max_power_sensor", 150))
|
||||
|
||||
assert (
|
||||
mock_heater_on.call_count == 0
|
||||
) # The fourth are not restarted because temperature is enought
|
||||
assert mock_heater_off.call_count == 0
|
||||
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:
|
||||
now = now + timedelta(seconds=30)
|
||||
VersatileThermostatAPI.get_vtherm_api()._set_now(now)
|
||||
|
||||
# 100 of the device / 4 -> 25, current power 50 so max is 75. With 150 no overheating
|
||||
await send_max_power_change_event(entity, 150, datetime.now())
|
||||
assert entity.power_manager.is_overpowering_detected is False
|
||||
# All configuration is complete and power is > power_max we switch to POWER preset
|
||||
assert entity.preset_mode is PRESET_ECO
|
||||
assert entity.power_manager.overpowering_state is STATE_OFF
|
||||
assert entity.target_temperature == 17
|
||||
|
||||
assert (
|
||||
mock_heater_on.call_count == 0
|
||||
) # The fourth are not restarted because temperature is enought
|
||||
assert mock_heater_off.call_count == 0
|
||||
|
||||
@@ -212,6 +212,13 @@ 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",
|
||||
@@ -232,7 +239,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")
|
||||
entity = await create_thermostat(hass, entry, "climate.theoverclimatemockname", temps)
|
||||
|
||||
assert entity
|
||||
assert entity.name == "TheOverClimateMockName"
|
||||
@@ -354,6 +361,13 @@ 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",
|
||||
@@ -374,7 +388,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")
|
||||
entity = await create_thermostat(hass, entry, "climate.theoverclimatemockname", temps)
|
||||
|
||||
assert entity
|
||||
|
||||
@@ -566,7 +580,6 @@ async def test_bug_508(
|
||||
# "temperature": 17.5,
|
||||
"target_temp_high": 10,
|
||||
"target_temp_low": 10,
|
||||
"temperature": 10,
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -590,7 +603,6 @@ async def test_bug_508(
|
||||
"entity_id": "climate.mock_climate",
|
||||
"target_temp_high": 31,
|
||||
"target_temp_low": 31,
|
||||
"temperature": 31,
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -726,6 +738,13 @@ 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",
|
||||
@@ -746,7 +765,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")
|
||||
entity = await create_thermostat(hass, entry, "climate.theoverclimatemockname", temps)
|
||||
|
||||
assert entity
|
||||
|
||||
@@ -1214,3 +1233,111 @@ async def test_manual_hvac_off_should_take_the_lead_over_auto_start_stop(
|
||||
assert vtherm.hvac_off_reason == HVAC_OFF_REASON_MANUAL
|
||||
assert vtherm._saved_hvac_mode == HVACMode.OFF
|
||||
assert mock_send_event.call_count == 0 # nothing have change
|
||||
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_underlying_from_comes_back_to_life(
|
||||
hass: HomeAssistant,
|
||||
skip_hass_states_is_state,
|
||||
skip_turn_on_off_heater,
|
||||
skip_send_event,
|
||||
):
|
||||
"""Test that when a underlying climate comes back to life (from unkwown or unavailable) the last state is send"""
|
||||
|
||||
tz = get_tz(hass) # pylint: disable=invalid-name
|
||||
now: datetime = datetime.now(tz=tz)
|
||||
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
title="TheOverClimateMockName",
|
||||
unique_id="uniqueId",
|
||||
data={
|
||||
CONF_NAME: "TheOverClimateMockName",
|
||||
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
|
||||
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_CLIMATE,
|
||||
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: False,
|
||||
CONF_USE_AUTO_START_STOP_FEATURE: False,
|
||||
CONF_USE_PRESENCE_FEATURE: False,
|
||||
CONF_UNDERLYING_LIST: ["climate.mock_climate"],
|
||||
CONF_MINIMAL_ACTIVATION_DELAY: 30,
|
||||
CONF_SAFETY_DELAY_MIN: 5,
|
||||
CONF_SAFETY_MIN_ON_PERCENT: 0.3,
|
||||
CONF_AUTO_FAN_MODE: CONF_AUTO_FAN_NONE,
|
||||
CONF_AC_MODE: True,
|
||||
}, # 5 minutes security delay
|
||||
)
|
||||
|
||||
# Underlying is in HEAT mode but should be shutdown at startup
|
||||
fake_underlying_climate = MockClimate(hass, "mockUniqueId", "MockClimateName", {}, HVACMode.COOL, HVACAction.COOLING)
|
||||
|
||||
# 1. initialize the vtherm in COOL with Boost
|
||||
# fmt: off
|
||||
with patch("custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",return_value=fake_underlying_climate) as mock_find_climate:
|
||||
# fmt: on
|
||||
entity = await create_thermostat(hass, entry, "climate.theoverclimatemockname", temps=default_temperatures_ac)
|
||||
|
||||
assert entity
|
||||
assert entity.name == "TheOverClimateMockName"
|
||||
assert entity.is_over_climate is True
|
||||
|
||||
# Set hvac_mode to COOL
|
||||
await entity.async_set_hvac_mode(HVACMode.COOL)
|
||||
await entity.async_set_preset_mode(PRESET_BOOST)
|
||||
|
||||
# it is very hot today
|
||||
await send_temperature_change_event(entity, 27, now, False)
|
||||
await send_ext_temperature_change_event(entity, 35, now, False)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert entity.hvac_mode is HVACMode.COOL
|
||||
# because in MockClimate HVACAction is HEATING if hvac_mode is not set
|
||||
assert entity.hvac_action is HVACAction.COOLING
|
||||
assert entity.preset_mode is PRESET_BOOST
|
||||
assert entity.target_temperature == 23
|
||||
|
||||
|
||||
# 2. send under state event comes back from life
|
||||
# fmt: off
|
||||
with patch("custom_components.versatile_thermostat.underlyings.UnderlyingClimate.set_hvac_mode") as mock_underlying_set_hvac_mode, \
|
||||
patch("custom_components.versatile_thermostat.underlyings.UnderlyingClimate.set_temperature") as mock_underlying_set_temperature:
|
||||
# fmt: on
|
||||
now = now + timedelta(minutes=2)
|
||||
# 2. Change the target temp of underlying thermostat at now -> the event will be disgarded because to fast (to avoid loop cf issue 121)
|
||||
await send_climate_change_event_with_temperature(
|
||||
entity,
|
||||
HVACMode.HEAT,
|
||||
STATE_UNKNOWN,
|
||||
HVACAction.OFF,
|
||||
STATE_UNKNOWN,
|
||||
now,
|
||||
entity.min_temp + 1,
|
||||
True,
|
||||
"climate.mock_climate", # the underlying climate entity id
|
||||
)
|
||||
|
||||
assert mock_underlying_set_hvac_mode.call_count == 1
|
||||
mock_underlying_set_hvac_mode.assert_has_calls(
|
||||
[
|
||||
call.set_hvac_mode(HVACMode.COOL),
|
||||
]
|
||||
)
|
||||
|
||||
assert mock_underlying_set_temperature.call_count == 1
|
||||
mock_underlying_set_temperature.assert_has_calls(
|
||||
[
|
||||
call.set_temperature(23, 30, 15),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
# Nothing should have changed
|
||||
assert entity.target_temperature == 23
|
||||
assert entity.preset_mode is PRESET_BOOST
|
||||
assert entity.hvac_mode is HVACMode.COOL
|
||||
|
||||
@@ -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': 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': 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': 3.0}, target={'entity_id': 'number.mock_offset_calibration1'}),
|
||||
call(domain='number', service='set_value', service_data={'value': 70}, target={'entity_id': 'number.mock_opening_degree2'}),
|
||||
call(domain='number', service='set_value', service_data={'value': 30}, target={'entity_id': 'number.mock_closing_degree2'}),
|
||||
call(domain='number', service='set_value', service_data={'value': 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': 12}, target={'entity_id': 'number.mock_offset_calibration2'})
|
||||
]
|
||||
)
|
||||
|
||||
@@ -10,6 +10,7 @@ from custom_components.versatile_thermostat.thermostat_switch import (
|
||||
from custom_components.versatile_thermostat.feature_power_manager import (
|
||||
FeaturePowerManager,
|
||||
)
|
||||
|
||||
from custom_components.versatile_thermostat.prop_algorithm import PropAlgorithm
|
||||
from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import
|
||||
|
||||
@@ -17,28 +18,28 @@ logging.getLogger().setLevel(logging.DEBUG)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"is_over_climate, is_device_active, power, max_power, current_overpowering_state, overpowering_state, nb_call, changed, check_overpowering_ret",
|
||||
"is_over_climate, is_device_active, power, max_power, check_power_available",
|
||||
[
|
||||
# don't switch to overpower (power is enough)
|
||||
(False, False, 1000, 3000, STATE_OFF, STATE_OFF, 0, True, False),
|
||||
(False, False, 1000, 3000, True),
|
||||
# switch to overpower (power is not enough)
|
||||
(False, False, 2000, 3000, STATE_OFF, STATE_ON, 1, True, True),
|
||||
(False, False, 2000, 3000, False),
|
||||
# don't switch to overpower (power is not enough but device is already on)
|
||||
(False, True, 2000, 3000, STATE_OFF, STATE_OFF, 0, True, False),
|
||||
(False, True, 2000, 3000, True),
|
||||
# Same with a over_climate
|
||||
# don't switch to overpower (power is enough)
|
||||
(True, False, 1000, 3000, STATE_OFF, STATE_OFF, 0, True, False),
|
||||
(True, False, 1000, 3000, True),
|
||||
# switch to overpower (power is not enough)
|
||||
(True, False, 2000, 3000, STATE_OFF, STATE_ON, 1, True, True),
|
||||
(True, False, 2000, 3000, False),
|
||||
# don't switch to overpower (power is not enough but device is already on)
|
||||
(True, True, 2000, 3000, STATE_OFF, STATE_OFF, 0, True, False),
|
||||
(True, True, 2000, 3000, True),
|
||||
# Leave overpowering state
|
||||
# switch to not overpower (power is enough)
|
||||
(False, False, 1000, 3000, STATE_ON, STATE_OFF, 1, True, False),
|
||||
(False, False, 1000, 3000, True),
|
||||
# don't switch to overpower (power is still not enough)
|
||||
(False, False, 2000, 3000, STATE_ON, STATE_ON, 0, True, True),
|
||||
(False, False, 2000, 3000, False),
|
||||
# keep overpower (power is not enough but device is already on)
|
||||
(False, True, 3000, 3000, STATE_ON, STATE_ON, 0, True, True),
|
||||
(False, True, 3000, 3000, False),
|
||||
],
|
||||
)
|
||||
async def test_power_feature_manager(
|
||||
@@ -47,17 +48,15 @@ async def test_power_feature_manager(
|
||||
is_device_active,
|
||||
power,
|
||||
max_power,
|
||||
current_overpowering_state,
|
||||
overpowering_state,
|
||||
nb_call,
|
||||
changed,
|
||||
check_overpowering_ret,
|
||||
check_power_available,
|
||||
):
|
||||
"""Test the FeaturePresenceManager class direclty"""
|
||||
|
||||
fake_vtherm = MagicMock(spec=BaseThermostat)
|
||||
type(fake_vtherm).name = PropertyMock(return_value="the name")
|
||||
|
||||
vtherm_api: VersatileThermostatAPI = VersatileThermostatAPI.get_vtherm_api(hass)
|
||||
|
||||
# 1. creation
|
||||
power_manager = FeaturePowerManager(fake_vtherm, hass)
|
||||
|
||||
@@ -80,16 +79,27 @@ async def test_power_feature_manager(
|
||||
assert custom_attributes["current_max_power"] is None
|
||||
|
||||
# 2. post_init
|
||||
power_manager.post_init(
|
||||
vtherm_api.find_central_configuration = MagicMock()
|
||||
vtherm_api.central_power_manager.post_init(
|
||||
{
|
||||
CONF_POWER_SENSOR: "sensor.the_power_sensor",
|
||||
CONF_MAX_POWER_SENSOR: "sensor.the_max_power_sensor",
|
||||
CONF_USE_POWER_FEATURE: True,
|
||||
CONF_PRESET_POWER: 13,
|
||||
}
|
||||
)
|
||||
assert vtherm_api.central_power_manager.is_configured
|
||||
|
||||
power_manager.post_init(
|
||||
{
|
||||
CONF_USE_POWER_FEATURE: True,
|
||||
CONF_PRESET_POWER: 10,
|
||||
CONF_DEVICE_POWER: 1234,
|
||||
}
|
||||
)
|
||||
|
||||
await power_manager.start_listening()
|
||||
|
||||
assert power_manager.is_configured is True
|
||||
assert power_manager.overpowering_state == STATE_UNKNOWN
|
||||
|
||||
@@ -107,25 +117,18 @@ async def test_power_feature_manager(
|
||||
assert custom_attributes["current_max_power"] is None
|
||||
|
||||
# 3. start listening
|
||||
power_manager.start_listening()
|
||||
await power_manager.start_listening()
|
||||
assert power_manager.is_configured is True
|
||||
assert power_manager.overpowering_state == STATE_UNKNOWN
|
||||
|
||||
assert len(power_manager._active_listener) == 2
|
||||
assert len(power_manager._active_listener) == 0 # no more listening
|
||||
|
||||
# 4. test refresh and check_overpowering with the parametrized
|
||||
side_effects = SideEffects(
|
||||
{
|
||||
"sensor.the_power_sensor": State("sensor.the_power_sensor", power),
|
||||
"sensor.the_max_power_sensor": State(
|
||||
"sensor.the_max_power_sensor", max_power
|
||||
),
|
||||
},
|
||||
State("unknown.entity_id", "unknown"),
|
||||
)
|
||||
# fmt:off
|
||||
with patch("homeassistant.core.StateMachine.get", side_effect=side_effects.get_side_effects()) as mock_get_state:
|
||||
with patch("custom_components.versatile_thermostat.central_feature_power_manager.CentralFeaturePowerManager.current_max_power", new_callable=PropertyMock, return_value=max_power), \
|
||||
patch("custom_components.versatile_thermostat.central_feature_power_manager.CentralFeaturePowerManager.current_power", new_callable=PropertyMock, return_value=power):
|
||||
# fmt:on
|
||||
|
||||
# Finish the mock configuration
|
||||
tpi_algo = PropAlgorithm(PROPORTIONAL_FUNCTION_TPI, 0.6, 0.01, 5, 0, "climate.vtherm")
|
||||
tpi_algo._on_percent = 1 # pylint: disable="protected-access"
|
||||
@@ -134,8 +137,84 @@ async def test_power_feature_manager(
|
||||
type(fake_vtherm).is_over_climate = PropertyMock(return_value=is_over_climate)
|
||||
type(fake_vtherm).proportional_algorithm = PropertyMock(return_value=tpi_algo)
|
||||
type(fake_vtherm).nb_underlying_entities = PropertyMock(return_value=1)
|
||||
type(fake_vtherm).preset_mode = PropertyMock(return_value=PRESET_COMFORT if current_overpowering_state == STATE_OFF else PRESET_POWER)
|
||||
type(fake_vtherm)._saved_preset_mode = PropertyMock(return_value=PRESET_ECO)
|
||||
|
||||
ret = await power_manager.check_power_available()
|
||||
assert ret == check_power_available
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"is_over_climate, current_overpowering_state, is_overpowering, new_overpowering_state, msg_sent",
|
||||
[
|
||||
# false -> false
|
||||
(False, STATE_OFF, False, STATE_OFF, False),
|
||||
# false -> true
|
||||
(False, STATE_OFF, True, STATE_ON, True),
|
||||
# true -> true
|
||||
(False, STATE_ON, True, STATE_ON, False),
|
||||
# true -> False
|
||||
(False, STATE_ON, False, STATE_OFF, True),
|
||||
# Same with over_climate
|
||||
# false -> false
|
||||
(True, STATE_OFF, False, STATE_OFF, False),
|
||||
# false -> true
|
||||
(True, STATE_OFF, True, STATE_ON, True),
|
||||
# true -> true
|
||||
(True, STATE_ON, True, STATE_ON, False),
|
||||
# true -> False
|
||||
(True, STATE_ON, False, STATE_OFF, True),
|
||||
],
|
||||
)
|
||||
async def test_power_feature_manager_set_overpowering(
|
||||
hass,
|
||||
is_over_climate,
|
||||
current_overpowering_state,
|
||||
is_overpowering,
|
||||
new_overpowering_state,
|
||||
msg_sent,
|
||||
):
|
||||
"""Test the set_overpowering method of FeaturePowerManager"""
|
||||
fake_vtherm = MagicMock(spec=BaseThermostat)
|
||||
type(fake_vtherm).name = PropertyMock(return_value="the name")
|
||||
|
||||
vtherm_api: VersatileThermostatAPI = VersatileThermostatAPI.get_vtherm_api(hass)
|
||||
|
||||
# 1. creation / init
|
||||
power_manager = FeaturePowerManager(fake_vtherm, hass)
|
||||
vtherm_api.find_central_configuration = MagicMock()
|
||||
vtherm_api.central_power_manager.post_init(
|
||||
{
|
||||
CONF_POWER_SENSOR: "sensor.the_power_sensor",
|
||||
CONF_MAX_POWER_SENSOR: "sensor.the_max_power_sensor",
|
||||
CONF_USE_POWER_FEATURE: True,
|
||||
CONF_PRESET_POWER: 13,
|
||||
}
|
||||
)
|
||||
assert vtherm_api.central_power_manager.is_configured
|
||||
|
||||
power_manager.post_init(
|
||||
{
|
||||
CONF_USE_POWER_FEATURE: True,
|
||||
CONF_PRESET_POWER: 10,
|
||||
CONF_DEVICE_POWER: 1234,
|
||||
}
|
||||
)
|
||||
|
||||
await power_manager.start_listening()
|
||||
|
||||
assert power_manager.is_configured is True
|
||||
assert power_manager.overpowering_state == STATE_UNKNOWN
|
||||
|
||||
# check overpowering
|
||||
power_manager._overpowering_state = current_overpowering_state
|
||||
|
||||
# fmt:off
|
||||
with patch("custom_components.versatile_thermostat.central_feature_power_manager.CentralFeaturePowerManager.current_max_power", new_callable=PropertyMock, return_value=2000), \
|
||||
patch("custom_components.versatile_thermostat.central_feature_power_manager.CentralFeaturePowerManager.current_power", new_callable=PropertyMock, return_value=1000):
|
||||
# fmt:on
|
||||
# Finish mocking
|
||||
fake_vtherm.is_over_climate = is_over_climate
|
||||
fake_vtherm.preset_mode = MagicMock(return_value=PRESET_COMFORT if current_overpowering_state == STATE_OFF else PRESET_POWER)
|
||||
fake_vtherm._saved_preset_mode = PRESET_ECO
|
||||
|
||||
fake_vtherm.save_hvac_mode = MagicMock()
|
||||
fake_vtherm.restore_hvac_mode = AsyncMock()
|
||||
@@ -147,26 +226,17 @@ async def test_power_feature_manager(
|
||||
fake_vtherm.update_custom_attributes = MagicMock()
|
||||
|
||||
|
||||
ret = await power_manager.refresh_state()
|
||||
assert ret == changed
|
||||
assert power_manager.is_configured is True
|
||||
assert power_manager.overpowering_state == STATE_UNKNOWN
|
||||
assert power_manager.current_power == power
|
||||
assert power_manager.current_max_power == max_power
|
||||
# Call set_overpowering
|
||||
await power_manager.set_overpowering(is_overpowering, 1234)
|
||||
|
||||
# check overpowering
|
||||
power_manager._overpowering_state = current_overpowering_state
|
||||
ret2 = await power_manager.check_overpowering()
|
||||
assert ret2 == check_overpowering_ret
|
||||
assert power_manager.overpowering_state == overpowering_state
|
||||
assert mock_get_state.call_count == 2
|
||||
assert power_manager.overpowering_state == new_overpowering_state
|
||||
|
||||
if power_manager.overpowering_state == STATE_OFF:
|
||||
if not is_overpowering:
|
||||
assert power_manager.overpowering_state == STATE_OFF
|
||||
assert fake_vtherm.save_hvac_mode.call_count == 0
|
||||
assert fake_vtherm.save_preset_mode.call_count == 0
|
||||
assert fake_vtherm.async_underlying_entity_turn_off.call_count == 0
|
||||
assert fake_vtherm.async_set_preset_mode_internal.call_count == 0
|
||||
assert fake_vtherm.send_event.call_count == nb_call
|
||||
|
||||
if current_overpowering_state == STATE_ON:
|
||||
assert fake_vtherm.update_custom_attributes.call_count == 1
|
||||
@@ -178,18 +248,24 @@ async def test_power_feature_manager(
|
||||
else:
|
||||
assert fake_vtherm.update_custom_attributes.call_count == 0
|
||||
|
||||
if nb_call == 1:
|
||||
if msg_sent:
|
||||
fake_vtherm.send_event.assert_has_calls(
|
||||
[
|
||||
call.fake_vtherm.send_event(
|
||||
EventType.POWER_EVENT,
|
||||
{'type': 'end', 'current_power': power, 'device_power': 1234, 'current_max_power': max_power}),
|
||||
{
|
||||
"type": "end",
|
||||
"current_power": 1000,
|
||||
"device_power": 1234,
|
||||
"current_max_power": 2000,
|
||||
},
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
elif power_manager.overpowering_state == STATE_ON:
|
||||
if is_over_climate:
|
||||
# is_overpowering is True
|
||||
else:
|
||||
assert power_manager.overpowering_state == STATE_ON
|
||||
if is_over_climate and current_overpowering_state == STATE_OFF:
|
||||
assert fake_vtherm.save_hvac_mode.call_count == 1
|
||||
else:
|
||||
assert fake_vtherm.save_hvac_mode.call_count == 0
|
||||
@@ -209,30 +285,37 @@ async def test_power_feature_manager(
|
||||
assert fake_vtherm.restore_hvac_mode.call_count == 0
|
||||
assert fake_vtherm.restore_preset_mode.call_count == 0
|
||||
|
||||
if nb_call == 1:
|
||||
if msg_sent:
|
||||
fake_vtherm.send_event.assert_has_calls(
|
||||
[
|
||||
call.fake_vtherm.send_event(
|
||||
EventType.POWER_EVENT,
|
||||
{'type': 'start', 'current_power': power, 'device_power': 1234, 'current_max_power': max_power, 'current_power_consumption': 1234.0}),
|
||||
{
|
||||
"type": "start",
|
||||
"current_power": 1000,
|
||||
"device_power": 1234,
|
||||
"current_max_power": 2000,
|
||||
"current_power_consumption": 1234.0,
|
||||
},
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
fake_vtherm.reset_mock()
|
||||
|
||||
# 5. Check custom_attributes
|
||||
# 5. Check custom_attributes
|
||||
custom_attributes = {}
|
||||
power_manager.add_custom_attributes(custom_attributes)
|
||||
assert custom_attributes["power_sensor_entity_id"] == "sensor.the_power_sensor"
|
||||
assert (
|
||||
custom_attributes["max_power_sensor_entity_id"] == "sensor.the_max_power_sensor"
|
||||
)
|
||||
assert custom_attributes["overpowering_state"] == overpowering_state
|
||||
assert custom_attributes["overpowering_state"] == new_overpowering_state
|
||||
assert custom_attributes["is_power_configured"] is True
|
||||
assert custom_attributes["device_power"] == 1234
|
||||
assert custom_attributes["power_temp"] == 10
|
||||
assert custom_attributes["current_power"] == power
|
||||
assert custom_attributes["current_max_power"] == max_power
|
||||
assert custom_attributes["current_power"] == 1000
|
||||
assert custom_attributes["current_max_power"] == 2000
|
||||
|
||||
power_manager.stop_listening()
|
||||
await hass.async_block_till_done()
|
||||
@@ -241,10 +324,15 @@ async def test_power_feature_manager(
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_power_management_hvac_off(
|
||||
hass: HomeAssistant, skip_hass_states_is_state
|
||||
hass: HomeAssistant, skip_hass_states_is_state, init_central_power_manager
|
||||
):
|
||||
"""Test the Power management"""
|
||||
|
||||
temps = {
|
||||
"eco": 17,
|
||||
"comfort": 18,
|
||||
"boost": 19,
|
||||
}
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
title="TheOverSwitchMockName",
|
||||
@@ -257,29 +345,24 @@ async def test_power_management_hvac_off(
|
||||
CONF_CYCLE_MIN: 5,
|
||||
CONF_TEMP_MIN: 15,
|
||||
CONF_TEMP_MAX: 30,
|
||||
"eco_temp": 17,
|
||||
"comfort_temp": 18,
|
||||
"boost_temp": 19,
|
||||
CONF_USE_WINDOW_FEATURE: False,
|
||||
CONF_USE_MOTION_FEATURE: False,
|
||||
CONF_USE_POWER_FEATURE: True,
|
||||
CONF_USE_PRESENCE_FEATURE: False,
|
||||
CONF_HEATER: "switch.mock_switch",
|
||||
CONF_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_POWER_SENSOR: "sensor.mock_power_sensor",
|
||||
CONF_MAX_POWER_SENSOR: "sensor.mock_power_max_sensor",
|
||||
CONF_DEVICE_POWER: 100,
|
||||
CONF_PRESET_POWER: 12,
|
||||
},
|
||||
)
|
||||
|
||||
entity: ThermostatOverSwitch = await create_thermostat(
|
||||
hass, entry, "climate.theoverswitchmockname"
|
||||
hass, entry, "climate.theoverswitchmockname", temps
|
||||
)
|
||||
assert entity
|
||||
|
||||
@@ -292,34 +375,53 @@ async def test_power_management_hvac_off(
|
||||
assert entity.power_manager.overpowering_state is STATE_UNKNOWN
|
||||
assert entity.hvac_mode == HVACMode.OFF
|
||||
|
||||
now: datetime = NowClass.get_now(hass)
|
||||
VersatileThermostatAPI.get_vtherm_api()._set_now(now)
|
||||
|
||||
# Send power mesurement
|
||||
await send_power_change_event(entity, 50, datetime.now())
|
||||
assert await entity.power_manager.check_overpowering() is False
|
||||
# fmt:off
|
||||
side_effects = SideEffects(
|
||||
{
|
||||
"sensor.the_power_sensor": State("sensor.the_power_sensor", 50),
|
||||
"sensor.the_max_power_sensor": State("sensor.the_max_power_sensor", 300),
|
||||
},
|
||||
State("unknown.entity_id", "unknown"),
|
||||
)
|
||||
# fmt:off
|
||||
with patch("homeassistant.core.StateMachine.get", side_effect=side_effects.get_side_effects()):
|
||||
# fmt: on
|
||||
await send_power_change_event(entity, 50, now)
|
||||
assert entity.power_manager.is_overpowering_detected is False
|
||||
|
||||
# All configuration is not complete
|
||||
assert entity.preset_mode is PRESET_BOOST
|
||||
assert entity.power_manager.overpowering_state is STATE_UNKNOWN
|
||||
# All configuration is not complete
|
||||
assert entity.preset_mode is PRESET_BOOST
|
||||
assert entity.power_manager.overpowering_state is STATE_UNKNOWN # due to hvac_off
|
||||
|
||||
# Send power max mesurement
|
||||
await send_max_power_change_event(entity, 300, datetime.now())
|
||||
assert await entity.power_manager.check_overpowering() is False
|
||||
# All configuration is complete and power is < power_max
|
||||
assert entity.preset_mode is PRESET_BOOST
|
||||
assert entity.power_manager.overpowering_state is STATE_OFF
|
||||
# Send power max mesurement
|
||||
now = now + timedelta(seconds=30)
|
||||
VersatileThermostatAPI.get_vtherm_api()._set_now(now)
|
||||
await send_max_power_change_event(entity, 300, now)
|
||||
assert entity.power_manager.is_overpowering_detected is False
|
||||
# All configuration is complete and power is < power_max
|
||||
assert entity.preset_mode is PRESET_BOOST
|
||||
assert entity.power_manager.overpowering_state is STATE_UNKNOWN # # due to hvac_off
|
||||
|
||||
# Send power max mesurement too low but HVACMode is 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:
|
||||
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:
|
||||
# fmt: on
|
||||
now = now + timedelta(seconds=30)
|
||||
VersatileThermostatAPI.get_vtherm_api()._set_now(now)
|
||||
|
||||
await send_max_power_change_event(entity, 149, datetime.now())
|
||||
assert await entity.power_manager.check_overpowering() is True
|
||||
assert entity.power_manager.is_overpowering_detected is False
|
||||
# All configuration is complete and power is > power_max but we stay in Boost cause thermostat if Off
|
||||
assert entity.preset_mode is PRESET_BOOST
|
||||
assert entity.power_manager.overpowering_state is STATE_ON
|
||||
assert entity.power_manager.overpowering_state is STATE_UNKNOWN
|
||||
|
||||
assert mock_send_event.call_count == 0
|
||||
assert mock_heater_on.call_count == 0
|
||||
@@ -328,9 +430,17 @@ async def test_power_management_hvac_off(
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_power_management_hvac_on(hass: HomeAssistant, skip_hass_states_is_state):
|
||||
async def test_power_management_hvac_on(
|
||||
hass: HomeAssistant, skip_hass_states_is_state, init_central_power_manager
|
||||
):
|
||||
"""Test the Power management"""
|
||||
|
||||
temps = {
|
||||
"eco": 17,
|
||||
"comfort": 18,
|
||||
"boost": 19,
|
||||
}
|
||||
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
title="TheOverSwitchMockName",
|
||||
@@ -343,32 +453,30 @@ async def test_power_management_hvac_on(hass: HomeAssistant, skip_hass_states_is
|
||||
CONF_CYCLE_MIN: 5,
|
||||
CONF_TEMP_MIN: 15,
|
||||
CONF_TEMP_MAX: 30,
|
||||
"eco_temp": 17,
|
||||
"comfort_temp": 18,
|
||||
"boost_temp": 19,
|
||||
CONF_USE_WINDOW_FEATURE: False,
|
||||
CONF_USE_MOTION_FEATURE: False,
|
||||
CONF_USE_POWER_FEATURE: True,
|
||||
CONF_USE_PRESENCE_FEATURE: False,
|
||||
CONF_HEATER: "switch.mock_switch",
|
||||
CONF_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_POWER_SENSOR: "sensor.mock_power_sensor",
|
||||
CONF_MAX_POWER_SENSOR: "sensor.mock_power_max_sensor",
|
||||
CONF_DEVICE_POWER: 100,
|
||||
CONF_PRESET_POWER: 12,
|
||||
},
|
||||
)
|
||||
|
||||
entity: ThermostatOverSwitch = await create_thermostat(
|
||||
hass, entry, "climate.theoverswitchmockname"
|
||||
hass, entry, "climate.theoverswitchmockname", temps
|
||||
)
|
||||
assert entity
|
||||
|
||||
now: datetime = NowClass.get_now(hass)
|
||||
VersatileThermostatAPI.get_vtherm_api()._set_now(now)
|
||||
|
||||
tpi_algo = entity._prop_algorithm
|
||||
assert tpi_algo
|
||||
|
||||
@@ -379,25 +487,49 @@ async def test_power_management_hvac_on(hass: HomeAssistant, skip_hass_states_is
|
||||
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
|
||||
await send_power_change_event(entity, 50, datetime.now())
|
||||
# Send power max mesurement
|
||||
await send_max_power_change_event(entity, 300, datetime.now())
|
||||
assert await entity.power_manager.check_overpowering() is False
|
||||
# All configuration is complete and power is < power_max
|
||||
assert entity.preset_mode is PRESET_BOOST
|
||||
assert entity.power_manager.overpowering_state is STATE_OFF
|
||||
side_effects = SideEffects(
|
||||
{
|
||||
"sensor.the_power_sensor": State("sensor.the_power_sensor", 50),
|
||||
"sensor.the_max_power_sensor": State("sensor.the_max_power_sensor", 300),
|
||||
},
|
||||
State("unknown.entity_id", "unknown"),
|
||||
)
|
||||
# fmt:off
|
||||
with patch("homeassistant.core.StateMachine.get", side_effect=side_effects.get_side_effects()):
|
||||
# fmt: on
|
||||
await send_power_change_event(entity, 50, datetime.now())
|
||||
# Send power max mesurement
|
||||
now = now + timedelta(seconds=30)
|
||||
VersatileThermostatAPI.get_vtherm_api()._set_now(now)
|
||||
await send_max_power_change_event(entity, 300, datetime.now())
|
||||
|
||||
assert entity.power_manager.is_overpowering_detected is False
|
||||
# All configuration is complete and power is < power_max
|
||||
assert entity.preset_mode is PRESET_BOOST
|
||||
assert entity.power_manager.overpowering_state is STATE_OFF
|
||||
|
||||
# Send power max mesurement too low and HVACMode is on
|
||||
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:
|
||||
await send_max_power_change_event(entity, 149, datetime.now())
|
||||
assert await entity.power_manager.check_overpowering() is True
|
||||
side_effects.add_or_update_side_effect("sensor.the_max_power_sensor", State("sensor.the_max_power_sensor", 49))
|
||||
# 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"):
|
||||
# fmt: on
|
||||
now = now + timedelta(seconds=30)
|
||||
VersatileThermostatAPI.get_vtherm_api()._set_now(now)
|
||||
|
||||
await send_max_power_change_event(entity, 49, 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
|
||||
assert entity.power_manager.overpowering_state is STATE_ON
|
||||
@@ -413,7 +545,7 @@ async def test_power_management_hvac_on(hass: HomeAssistant, skip_hass_states_is
|
||||
"type": "start",
|
||||
"current_power": 50,
|
||||
"device_power": 100,
|
||||
"current_max_power": 149,
|
||||
"current_max_power": 49,
|
||||
"current_power_consumption": 100.0,
|
||||
},
|
||||
),
|
||||
@@ -423,16 +555,20 @@ async def test_power_management_hvac_on(hass: HomeAssistant, skip_hass_states_is
|
||||
assert mock_heater_on.call_count == 0
|
||||
assert mock_heater_off.call_count == 1
|
||||
|
||||
# Send power mesurement low to unseet power preset
|
||||
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:
|
||||
await send_power_change_event(entity, 48, datetime.now())
|
||||
assert await entity.power_manager.check_overpowering() is False
|
||||
# Send power mesurement low to unset 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, \
|
||||
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: on
|
||||
now = now + timedelta(seconds=30)
|
||||
VersatileThermostatAPI.get_vtherm_api()._set_now(now)
|
||||
|
||||
await send_power_change_event(entity, 48, 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
|
||||
assert entity.power_manager.overpowering_state is STATE_OFF
|
||||
@@ -462,10 +598,16 @@ async def test_power_management_hvac_on(hass: HomeAssistant, skip_hass_states_is
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_power_management_energy_over_switch(
|
||||
hass: HomeAssistant, skip_hass_states_is_state
|
||||
hass: HomeAssistant, skip_hass_states_is_state, init_central_power_manager
|
||||
):
|
||||
"""Test the Power management energy mesurement"""
|
||||
|
||||
temps = {
|
||||
"eco": 17,
|
||||
"comfort": 18,
|
||||
"boost": 19,
|
||||
}
|
||||
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
title="TheOverSwitchMockName",
|
||||
@@ -478,30 +620,24 @@ async def test_power_management_energy_over_switch(
|
||||
CONF_CYCLE_MIN: 5,
|
||||
CONF_TEMP_MIN: 15,
|
||||
CONF_TEMP_MAX: 30,
|
||||
"eco_temp": 17,
|
||||
"comfort_temp": 18,
|
||||
"boost_temp": 19,
|
||||
CONF_USE_WINDOW_FEATURE: False,
|
||||
CONF_USE_MOTION_FEATURE: False,
|
||||
CONF_USE_POWER_FEATURE: True,
|
||||
CONF_USE_PRESENCE_FEATURE: False,
|
||||
CONF_HEATER: "switch.mock_switch",
|
||||
CONF_HEATER_2: "switch.mock_switch2",
|
||||
CONF_UNDERLYING_LIST: ["switch.mock_switch", "switch.mock_switch2"],
|
||||
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_POWER_SENSOR: "sensor.mock_power_sensor",
|
||||
CONF_MAX_POWER_SENSOR: "sensor.mock_power_max_sensor",
|
||||
CONF_DEVICE_POWER: 100,
|
||||
CONF_PRESET_POWER: 12,
|
||||
},
|
||||
)
|
||||
|
||||
entity: ThermostatOverSwitch = await create_thermostat(
|
||||
hass, entry, "climate.theoverswitchmockname"
|
||||
hass, entry, "climate.theoverswitchmockname", temps
|
||||
)
|
||||
assert entity
|
||||
|
||||
@@ -523,6 +659,8 @@ async def test_power_management_energy_over_switch(
|
||||
await entity.async_set_preset_mode(PRESET_BOOST)
|
||||
await send_temperature_change_event(entity, 15, datetime.now())
|
||||
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert entity.hvac_mode is HVACMode.HEAT
|
||||
assert entity.preset_mode is PRESET_BOOST
|
||||
assert entity.target_temperature == 19
|
||||
@@ -594,6 +732,12 @@ async def test_power_management_energy_over_climate(
|
||||
):
|
||||
"""Test the Power management for a over_climate thermostat"""
|
||||
|
||||
temps = {
|
||||
"eco": 17,
|
||||
"comfort": 18,
|
||||
"boost": 19,
|
||||
}
|
||||
|
||||
the_mock_underlying = MagicMockClimate()
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
|
||||
@@ -611,26 +755,21 @@ async def test_power_management_energy_over_climate(
|
||||
CONF_CYCLE_MIN: 5,
|
||||
CONF_TEMP_MIN: 15,
|
||||
CONF_TEMP_MAX: 30,
|
||||
"eco_temp": 17,
|
||||
"comfort_temp": 18,
|
||||
"boost_temp": 19,
|
||||
CONF_USE_WINDOW_FEATURE: False,
|
||||
CONF_USE_MOTION_FEATURE: False,
|
||||
CONF_USE_POWER_FEATURE: True,
|
||||
CONF_USE_PRESENCE_FEATURE: False,
|
||||
CONF_CLIMATE: "climate.mock_climate",
|
||||
CONF_UNDERLYING_LIST: ["climate.mock_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,
|
||||
},
|
||||
)
|
||||
|
||||
entity: ThermostatOverSwitch = await create_thermostat(
|
||||
hass, entry, "climate.theoverclimatemockname"
|
||||
hass, entry, "climate.theoverclimatemockname", temps
|
||||
)
|
||||
assert entity
|
||||
assert entity.is_over_climate
|
||||
@@ -686,3 +825,132 @@ async def test_power_management_energy_over_climate(
|
||||
# Test the re-increment
|
||||
entity.incremente_energy()
|
||||
assert entity.total_energy == 2 * 100 * 3.0 / 60
|
||||
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_power_management_turn_off_while_shedding(hass: HomeAssistant, skip_hass_states_is_state, init_central_power_manager):
|
||||
"""Test the Power management and that we can turn off a Vtherm that
|
||||
is in overpowering state"""
|
||||
|
||||
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: 100,
|
||||
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)
|
||||
|
||||
tpi_algo = entity._prop_algorithm
|
||||
assert tpi_algo
|
||||
|
||||
await entity.async_set_hvac_mode(HVACMode.HEAT)
|
||||
await entity.async_set_preset_mode(PRESET_BOOST)
|
||||
assert entity.hvac_mode is HVACMode.HEAT
|
||||
assert entity.preset_mode is PRESET_BOOST
|
||||
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
|
||||
|
||||
side_effects = SideEffects(
|
||||
{
|
||||
"sensor.the_power_sensor": State("sensor.the_power_sensor", 50),
|
||||
"sensor.the_max_power_sensor": State("sensor.the_max_power_sensor", 49),
|
||||
},
|
||||
State("unknown.entity_id", "unknown"),
|
||||
)
|
||||
# # fmt:off
|
||||
# with patch("homeassistant.core.StateMachine.get", side_effect=side_effects.get_side_effects()):
|
||||
# # fmt: on
|
||||
# await send_power_change_event(entity, 50, datetime.now())
|
||||
# # Send power max mesurement
|
||||
# now = now + timedelta(seconds=30)
|
||||
# VersatileThermostatAPI.get_vtherm_api()._set_now(now)
|
||||
# await send_max_power_change_event(entity, 300, datetime.now())
|
||||
#
|
||||
# assert entity.power_manager.is_overpowering_detected is False
|
||||
# # All configuration is complete and power is < power_max
|
||||
# assert entity.preset_mode is PRESET_BOOST
|
||||
# assert entity.power_manager.overpowering_state is STATE_OFF
|
||||
|
||||
# 1. Set VTherm to overpowering
|
||||
# Send power max mesurement too low and HVACMode is on and device is active
|
||||
# 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"), \
|
||||
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)
|
||||
|
||||
await send_max_power_change_event(entity, 49, 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
|
||||
assert entity.power_manager.overpowering_state is STATE_ON
|
||||
assert entity.target_temperature == 12
|
||||
|
||||
assert mock_heater_on.call_count == 0
|
||||
assert mock_heater_off.call_count == 1
|
||||
|
||||
# 2. Turn-off Vtherm
|
||||
# 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"):
|
||||
# fmt: on
|
||||
now = now + timedelta(seconds=30)
|
||||
VersatileThermostatAPI.get_vtherm_api()._set_now(now)
|
||||
|
||||
await entity.async_set_hvac_mode(HVACMode.OFF)
|
||||
await VersatileThermostatAPI.get_vtherm_api().central_power_manager._do_immediate_shedding()
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# VTherm is off and overpowering if off also
|
||||
assert entity.hvac_mode == HVACMode.OFF
|
||||
assert entity.power_manager.is_overpowering_detected is False
|
||||
assert entity.preset_mode is PRESET_BOOST
|
||||
assert entity.power_manager.overpowering_state is STATE_OFF
|
||||
assert entity.target_temperature == 19
|
||||
|
||||
@@ -75,7 +75,7 @@ async def test_presence_feature_manager(
|
||||
assert custom_attributes["is_presence_configured"] is True
|
||||
|
||||
# 3. start listening
|
||||
presence_manager.start_listening()
|
||||
await 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
|
||||
|
||||
@@ -224,8 +224,6 @@ 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,
|
||||
},
|
||||
|
||||
@@ -26,6 +26,23 @@ 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",
|
||||
@@ -57,21 +74,7 @@ async def test_over_switch_ac_full_start(
|
||||
assert isinstance(entity, ThermostatOverSwitch)
|
||||
|
||||
# Initialise the preset temp
|
||||
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
|
||||
)
|
||||
await set_all_climate_preset_temp(hass, entity, temps, "theoverswitchmockname")
|
||||
|
||||
assert entity.name == "TheOverSwitchMockName"
|
||||
assert entity.is_over_climate is False # pylint: disable=protected-access
|
||||
|
||||
@@ -51,8 +51,6 @@ 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,
|
||||
|
||||
@@ -170,7 +170,7 @@ async def test_window_feature_manager_refresh_sensor_action_turn_off(
|
||||
)
|
||||
|
||||
# 3. start listening
|
||||
window_manager.start_listening()
|
||||
await 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
|
||||
window_manager.start_listening()
|
||||
await 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
|
||||
window_manager.start_listening()
|
||||
await 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
|
||||
window_manager.start_listening()
|
||||
await 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
|
||||
window_manager.start_listening()
|
||||
await window_manager.start_listening()
|
||||
|
||||
# 2. Call manage window auto
|
||||
tz = get_tz(hass) # pylint: disable=invalid-name
|
||||
|
||||
Reference in New Issue
Block a user