Compare commits
32 Commits
6.8.2.beta
...
7.1.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
43d8e5eb3c | ||
|
|
f8050e2ed7 | ||
|
|
13443402d0 | ||
|
|
4d7bc1b5b3 | ||
|
|
2b164d3dab | ||
|
|
0333c403f8 | ||
|
|
ae1a86f484 | ||
|
|
2db574da42 | ||
|
|
d236cc8fbb | ||
|
|
a637c2841c | ||
|
|
ee3b803db1 | ||
|
|
22b2b965c1 | ||
|
|
9c8a965dba | ||
|
|
68e05bef31 | ||
|
|
e727c0628e | ||
|
|
9e52c843bc | ||
|
|
d2a94d33e8 | ||
|
|
6e5e304b71 | ||
|
|
8559dd144a | ||
|
|
65b4690e64 | ||
|
|
6c5ddc315c | ||
|
|
081a2351de | ||
|
|
d9791f6cb0 | ||
|
|
24f6445861 | ||
|
|
083a3a4c81 | ||
|
|
ee42a235c0 | ||
|
|
9839ed4920 | ||
|
|
02f60770e8 | ||
|
|
23ddff95b3 | ||
|
|
53160f1365 | ||
|
|
1aaa6d4461 | ||
|
|
a09af6a184 |
@@ -1,2 +1,2 @@
|
||||
FROM mcr.microsoft.com/devcontainers/python:1-3.12
|
||||
RUN apt update && apt install -y ffmpeg
|
||||
FROM mcr.microsoft.com/devcontainers/python:1-3.13
|
||||
RUN apt update && apt install -y ffmpeg libturbojpeg0-dev
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
// See https://aka.ms/vscode-remote/devcontainer.json for format details.
|
||||
// "image": "ghcr.io/ludeeus/devcontainer/integration:latest",
|
||||
{
|
||||
"build": {
|
||||
"dockerfile": "Dockerfile"
|
||||
},
|
||||
"name": "Versatile Thermostat integration",
|
||||
"appPort": [
|
||||
"8123:8123"
|
||||
],
|
||||
// "postCreateCommand": "container install",
|
||||
"postCreateCommand": "./container dev-setup",
|
||||
"build": {
|
||||
"dockerfile": "Dockerfile"
|
||||
},
|
||||
"name": "Versatile Thermostat integration",
|
||||
"appPort": ["8123:8123"],
|
||||
// "postCreateCommand": "container install",
|
||||
"postCreateCommand": "./container dev-setup",
|
||||
|
||||
"mounts": [
|
||||
"source=${localEnv:HOME}/.ssh,target=/home/vscode/.ssh,type=bind,consistency=cached",
|
||||
@@ -17,52 +15,54 @@
|
||||
"source=${localEnv:HOME}/SugarSync/Projets/home-assistant/versatile-thermostat-ui-card/dist,target=/workspaces/versatile_thermostat/config/www/community/versatile-thermostat-ui-card,type=bind,consistency=cached"
|
||||
],
|
||||
|
||||
"customizations": {
|
||||
"vscode": {
|
||||
"extensions": [
|
||||
"ms-python.python",
|
||||
"ms-python.pylint",
|
||||
// Doesn't work (crash). Default in python is to use Jedi see Settings / Python / Default Language
|
||||
// "ms-python.vscode-pylance",
|
||||
"ms-python.isort",
|
||||
"ms-python.black-formatter",
|
||||
"visualstudioexptteam.vscodeintellicode",
|
||||
"redhat.vscode-yaml",
|
||||
"github.vscode-pull-request-github",
|
||||
"ryanluker.vscode-coverage-gutters",
|
||||
"ferrierbenjamin.fold-unfold-all-icone",
|
||||
"LittleFoxTeam.vscode-python-test-adapter",
|
||||
"donjayamanne.githistory",
|
||||
"waderyan.gitblame",
|
||||
"keesschollaart.vscode-home-assistant",
|
||||
"vscode.markdown-math",
|
||||
"yzhang.markdown-all-in-one",
|
||||
"github.vscode-github-actions",
|
||||
"azuretools.vscode-docker"
|
||||
],
|
||||
"settings": {
|
||||
"files.eol": "\n",
|
||||
"editor.tabSize": 4,
|
||||
"terminal.integrated.profiles.linux": {
|
||||
"bash": {
|
||||
"path": "bash",
|
||||
"args": []
|
||||
}
|
||||
},
|
||||
"terminal.integrated.defaultProfile.linux": "bash",
|
||||
// "terminal.integrated.shell.linux": "/bin/bash",
|
||||
"python.pythonPath": "/usr/bin/python3",
|
||||
"python.analysis.autoSearchPaths": true,
|
||||
"pylint.lintOnChange": false,
|
||||
"python.formatting.provider": "black",
|
||||
"python.formatting.blackPath": "/usr/local/py-utils/bin/black",
|
||||
"editor.formatOnPaste": false,
|
||||
"editor.formatOnSave": true,
|
||||
"editor.formatOnType": true,
|
||||
"files.trimTrailingWhitespace": true
|
||||
// "python.experiments.optOutFrom": ["pythonTestAdapter"],
|
||||
// "python.analysis.logLevel": "Trace"
|
||||
}
|
||||
}
|
||||
}
|
||||
"customizations": {
|
||||
"vscode": {
|
||||
"extensions": [
|
||||
"ms-python.python",
|
||||
"ms-python.pylint",
|
||||
// Doesn't work (crash). Default in python is to use Jedi see Settings / Python / Default Language
|
||||
// "ms-python.vscode-pylance",
|
||||
"ms-python.isort",
|
||||
"ms-python.black-formatter",
|
||||
"visualstudioexptteam.vscodeintellicode",
|
||||
"redhat.vscode-yaml",
|
||||
"github.vscode-pull-request-github",
|
||||
"ryanluker.vscode-coverage-gutters",
|
||||
"ferrierbenjamin.fold-unfold-all-icone",
|
||||
"LittleFoxTeam.vscode-python-test-adapter",
|
||||
"donjayamanne.githistory",
|
||||
"waderyan.gitblame",
|
||||
"keesschollaart.vscode-home-assistant",
|
||||
"vscode.markdown-math",
|
||||
"yzhang.markdown-all-in-one",
|
||||
"github.vscode-github-actions",
|
||||
"azuretools.vscode-docker",
|
||||
"huizhou.githd"
|
||||
],
|
||||
"settings": {
|
||||
"files.eol": "\n",
|
||||
"editor.tabSize": 4,
|
||||
"terminal.integrated.profiles.linux": {
|
||||
"bash": {
|
||||
"path": "bash",
|
||||
"args": []
|
||||
}
|
||||
},
|
||||
"terminal.integrated.defaultProfile.linux": "bash",
|
||||
// "terminal.integrated.shell.linux": "/bin/bash",
|
||||
"python.pythonPath": "/usr/bin/python3",
|
||||
"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,
|
||||
"editor.formatOnType": true,
|
||||
"files.trimTrailingWhitespace": true
|
||||
// "python.experiments.optOutFrom": ["pythonTestAdapter"],
|
||||
// "python.analysis.logLevel": "Trace"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
2
.devcontainer/pytest.ini
Normal file
2
.devcontainer/pytest.ini
Normal file
@@ -0,0 +1,2 @@
|
||||
[pytest]
|
||||
asyncio_default_fixture_loop_scope = function
|
||||
8
.github/ISSUE_TEMPLATE/issue.md
vendored
8
.github/ISSUE_TEMPLATE/issue.md
vendored
@@ -69,10 +69,10 @@ motion_state: 'off'
|
||||
overpowering_state: false
|
||||
presence_state: 'on'
|
||||
window_auto_state: false
|
||||
window_bypass_state: false
|
||||
security_delay_min: 2
|
||||
security_min_on_percent: 0.5
|
||||
security_default_on_percent: 0.1
|
||||
is_window_bypass: false
|
||||
safety_delay_min: 2
|
||||
safety_min_on_percent: 0.5
|
||||
safety_default_on_percent: 0.1
|
||||
last_temperature_datetime: '2023-11-05T00:48:54.873157+01:00'
|
||||
last_ext_temperature_datetime: '2023-11-05T00:48:53.240122+01:00'
|
||||
security_state: true
|
||||
|
||||
43
README-fr.md
43
README-fr.md
@@ -39,27 +39,28 @@ Un grand merci à tous mes fournisseurs de bières pour leurs dons et leurs enco
|
||||
|
||||
La documentation est maintenant découpée en plusieurs pages pour faciliter la lecture et la recherche d'informations :
|
||||
1. [présentation](documentation/fr/presentation.md),
|
||||
2. [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
|
||||
|
||||
@@ -38,8 +38,9 @@ A big thank you to all my beer sponsors for their donations and encouragements.
|
||||
|
||||
The documentation is now divided into several pages for easier reading and searching:
|
||||
1. [Introduction](documentation/en/presentation.md),
|
||||
2. [Choosing a VTherm type](documentation/en/creation.md),
|
||||
3. [Basic attributes](documentation/en/base-attributes.md)
|
||||
2. [Installation](documentation/en/installation.md),
|
||||
3. [Choosing a VTherm type](documentation/en/creation.md),
|
||||
4. [Basic attributes](documentation/en/base-attributes.md)
|
||||
3. [Configuring a VTherm on a `switch`](documentation/en/over-switch.md)
|
||||
3. [Configuring a VTherm on a `climate`](documentation/en/over-climate.md)
|
||||
3. [Configuring a VTherm on a valve](documentation/en/over-valve.md)
|
||||
@@ -103,4 +104,4 @@ If you wish to contribute, please read the [contribution guidelines](CONTRIBUTIN
|
||||
[license-shield]: https://img.shields.io/github/license/jmcollin78/versatile_thermostat.svg?style=for-the-badge
|
||||
[maintenance-shield]: https://img.shields.io/badge/maintainer-Joakim%20Sørensen%20%40ludeeus-blue.svg?style=for-the-badge
|
||||
[releases-shield]: https://img.shields.io/github/release/jmcollin78/versatile_thermostat.svg?style=for-the-badge
|
||||
[releases]: https://github.com/jmcollin78/versatile_thermostat/releases
|
||||
[releases]: https://github.com/jmcollin78/versatile_thermostat/releases
|
||||
|
||||
@@ -29,6 +29,9 @@ from .const import (
|
||||
CONF_AUTO_REGULATION_EXPERT,
|
||||
CONF_SHORT_EMA_PARAMS,
|
||||
CONF_SAFETY_MODE,
|
||||
CONF_SAFETY_DELAY_MIN,
|
||||
CONF_SAFETY_MIN_ON_PERCENT,
|
||||
CONF_SAFETY_DEFAULT_ON_PERCENT,
|
||||
CONF_THERMOSTAT_CENTRAL_CONFIG,
|
||||
CONF_THERMOSTAT_TYPE,
|
||||
CONF_USE_WINDOW_FEATURE,
|
||||
@@ -291,6 +294,20 @@ async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry):
|
||||
]:
|
||||
new.pop(key, None)
|
||||
|
||||
# Migration 2.0 to 2.1 -> rename security parameters into safety
|
||||
|
||||
if config_entry.version == CONFIG_VERSION and config_entry.minor_version == 0:
|
||||
for key in [
|
||||
"security_delay_min",
|
||||
"security_min_on_percent",
|
||||
"security_default_on_percent",
|
||||
]:
|
||||
new_key = key.replace("security_", "safety_")
|
||||
old_value = config_entry.data.get(key, None)
|
||||
if old_value is not None:
|
||||
new[new_key] = old_value
|
||||
new.pop(key, None)
|
||||
|
||||
hass.config_entries.async_update_entry(
|
||||
config_entry,
|
||||
data=new,
|
||||
|
||||
119
custom_components/versatile_thermostat/base_entity.py
Normal file
119
custom_components/versatile_thermostat/base_entity.py
Normal file
@@ -0,0 +1,119 @@
|
||||
""" A base class for all VTherm entities"""
|
||||
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
from homeassistant.core import HomeAssistant, callback, Event
|
||||
from homeassistant.components.climate import ClimateEntity
|
||||
from homeassistant.components.climate.const import DOMAIN as CLIMATE_DOMAIN
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.device_registry import DeviceInfo, DeviceEntryType
|
||||
from homeassistant.helpers.event import async_track_state_change_event, async_call_later
|
||||
|
||||
|
||||
from .const import DOMAIN, DEVICE_MANUFACTURER
|
||||
|
||||
from .base_thermostat import BaseThermostat
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class VersatileThermostatBaseEntity(Entity):
|
||||
"""A base class for all entities"""
|
||||
|
||||
_my_climate: BaseThermostat
|
||||
hass: HomeAssistant
|
||||
_config_id: str
|
||||
_device_name: str
|
||||
|
||||
def __init__(self, hass: HomeAssistant, config_id, device_name) -> None:
|
||||
"""The CTOR"""
|
||||
self.hass = hass
|
||||
self._config_id = config_id
|
||||
self._device_name = device_name
|
||||
self._my_climate = None
|
||||
self._cancel_call = None
|
||||
self._attr_has_entity_name = True
|
||||
|
||||
@property
|
||||
def should_poll(self) -> bool:
|
||||
"""Do not poll for those entities"""
|
||||
return False
|
||||
|
||||
@property
|
||||
def my_climate(self) -> BaseThermostat | None:
|
||||
"""Returns my climate if found"""
|
||||
if not self._my_climate:
|
||||
self._my_climate = self.find_my_versatile_thermostat()
|
||||
if self._my_climate:
|
||||
# Only the first time
|
||||
self.my_climate_is_initialized()
|
||||
return self._my_climate
|
||||
|
||||
@property
|
||||
def device_info(self) -> DeviceInfo:
|
||||
"""Return the device info."""
|
||||
return DeviceInfo(
|
||||
entry_type=DeviceEntryType.SERVICE,
|
||||
identifiers={(DOMAIN, self._config_id)},
|
||||
name=self._device_name,
|
||||
manufacturer=DEVICE_MANUFACTURER,
|
||||
model=DOMAIN,
|
||||
)
|
||||
|
||||
def find_my_versatile_thermostat(self) -> BaseThermostat:
|
||||
"""Find the underlying climate entity"""
|
||||
try:
|
||||
component: EntityComponent[ClimateEntity] = self.hass.data[CLIMATE_DOMAIN]
|
||||
for entity in component.entities:
|
||||
# _LOGGER.debug("Device_info is %s", entity.device_info)
|
||||
if entity.device_info == self.device_info:
|
||||
_LOGGER.debug("Found %s!", entity)
|
||||
return entity
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
return None
|
||||
|
||||
@callback
|
||||
async def async_added_to_hass(self):
|
||||
"""Listen to my climate state change"""
|
||||
|
||||
# Check delay condition
|
||||
async def try_find_climate(_):
|
||||
_LOGGER.debug(
|
||||
"%s - Calling VersatileThermostatBaseEntity.async_added_to_hass", self
|
||||
)
|
||||
mcl = self.my_climate
|
||||
if mcl:
|
||||
if self._cancel_call:
|
||||
self._cancel_call()
|
||||
self._cancel_call = None
|
||||
self.async_on_remove(
|
||||
async_track_state_change_event(
|
||||
self.hass,
|
||||
[mcl.entity_id],
|
||||
self.async_my_climate_changed,
|
||||
)
|
||||
)
|
||||
else:
|
||||
_LOGGER.debug("%s - no entity to listen. Try later", self)
|
||||
self._cancel_call = async_call_later(
|
||||
self.hass, timedelta(seconds=1), try_find_climate
|
||||
)
|
||||
|
||||
await try_find_climate(None)
|
||||
|
||||
@callback
|
||||
def my_climate_is_initialized(self):
|
||||
"""Called when the associated climate is initialized"""
|
||||
return
|
||||
|
||||
@callback
|
||||
async def async_my_climate_changed(
|
||||
self, event: Event
|
||||
): # pylint: disable=unused-argument
|
||||
"""Called when my climate have change
|
||||
This method aims to be overridden to take the status change
|
||||
"""
|
||||
return
|
||||
62
custom_components/versatile_thermostat/base_manager.py
Normal file
62
custom_components/versatile_thermostat/base_manager.py
Normal file
@@ -0,0 +1,62 @@
|
||||
""" Implements a base Feature Manager for Versatile Thermostat """
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.core import CALLBACK_TYPE, HomeAssistant
|
||||
|
||||
from .const import * # pylint: disable=wildcard-import, unused-wildcard-import
|
||||
from .commons import ConfigData
|
||||
|
||||
from .config_schema import * # pylint: disable=wildcard-import, unused-wildcard-import
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class BaseFeatureManager:
|
||||
"""A base class for all feature"""
|
||||
|
||||
def __init__(self, vtherm: Any, hass: HomeAssistant, name: str = None):
|
||||
"""Init of a featureManager"""
|
||||
self._vtherm = vtherm
|
||||
self._name = vtherm.name if vtherm else name
|
||||
self._active_listener: list[CALLBACK_TYPE] = []
|
||||
self._hass = hass
|
||||
|
||||
def post_init(self, entry_infos: ConfigData):
|
||||
"""Initialize the attributes of the FeatureManager"""
|
||||
raise NotImplementedError()
|
||||
|
||||
async def start_listening(self):
|
||||
"""Start listening the underlying entity"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def stop_listening(self) -> bool:
|
||||
"""stop listening to the sensor"""
|
||||
while self._active_listener:
|
||||
self._active_listener.pop()()
|
||||
|
||||
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)
|
||||
|
||||
@property
|
||||
def is_configured(self) -> bool:
|
||||
"""True if the FeatureManager is fully configured"""
|
||||
raise NotImplementedError()
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
"""The name"""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def hass(self) -> HomeAssistant:
|
||||
"""The HA instance"""
|
||||
return self._hass
|
||||
File diff suppressed because it is too large
Load Diff
@@ -25,10 +25,8 @@ from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from .vtherm_api import VersatileThermostatAPI
|
||||
from .commons import (
|
||||
VersatileThermostatBaseEntity,
|
||||
check_and_extract_service_configuration,
|
||||
)
|
||||
from .commons import check_and_extract_service_configuration
|
||||
from .base_entity import VersatileThermostatBaseEntity
|
||||
from .const import (
|
||||
DOMAIN,
|
||||
DEVICE_MANUFACTURER,
|
||||
@@ -111,7 +109,7 @@ class SecurityBinarySensor(VersatileThermostatBaseEntity, BinarySensorEntity):
|
||||
# _LOGGER.debug("%s - climate state change", self._attr_unique_id)
|
||||
|
||||
old_state = self._attr_is_on
|
||||
self._attr_is_on = self.my_climate.security_state is True
|
||||
self._attr_is_on = self.my_climate.safety_manager.is_safety_detected
|
||||
if old_state != self._attr_is_on:
|
||||
self.async_write_ha_state()
|
||||
return
|
||||
@@ -150,7 +148,7 @@ class OverpoweringBinarySensor(VersatileThermostatBaseEntity, BinarySensorEntity
|
||||
# _LOGGER.debug("%s - climate state change", self._attr_unique_id)
|
||||
|
||||
old_state = self._attr_is_on
|
||||
self._attr_is_on = self.my_climate.overpowering_state is True
|
||||
self._attr_is_on = self.my_climate.overpowering_state is STATE_ON
|
||||
if old_state != self._attr_is_on:
|
||||
self.async_write_ha_state()
|
||||
return
|
||||
@@ -319,8 +317,8 @@ class WindowByPassBinarySensor(VersatileThermostatBaseEntity, BinarySensorEntity
|
||||
"""Called when my climate have change"""
|
||||
# _LOGGER.debug("%s - climate state change", self._attr_unique_id)
|
||||
old_state = self._attr_is_on
|
||||
if self.my_climate.window_bypass_state in [True, False]:
|
||||
self._attr_is_on = self.my_climate.window_bypass_state
|
||||
if self.my_climate.is_window_bypass in [True, False]:
|
||||
self._attr_is_on = self.my_climate.is_window_bypass
|
||||
if old_state != self._attr_is_on:
|
||||
self.async_write_ha_state()
|
||||
return
|
||||
|
||||
@@ -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
|
||||
@@ -97,13 +101,13 @@ async def async_setup_entry(
|
||||
)
|
||||
|
||||
platform.async_register_entity_service(
|
||||
SERVICE_SET_SECURITY,
|
||||
SERVICE_SET_SAFETY,
|
||||
{
|
||||
vol.Optional("delay_min"): cv.positive_int,
|
||||
vol.Optional("min_on_percent"): vol.Coerce(float),
|
||||
vol.Optional("default_on_percent"): vol.Coerce(float),
|
||||
},
|
||||
"service_set_security",
|
||||
"SERVICE_SET_SAFETY",
|
||||
)
|
||||
|
||||
platform.async_register_entity_service(
|
||||
@@ -118,7 +122,7 @@ async def async_setup_entry(
|
||||
SERVICE_SET_AUTO_REGULATION_MODE,
|
||||
{
|
||||
vol.Required("auto_regulation_mode"): vol.In(
|
||||
["None", "Light", "Medium", "Strong", "Slow"]
|
||||
["None", "Light", "Medium", "Strong", "Slow", "Expert"]
|
||||
),
|
||||
},
|
||||
"service_set_auto_regulation_mode",
|
||||
|
||||
@@ -3,17 +3,15 @@
|
||||
# pylint: disable=line-too-long
|
||||
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
from homeassistant.core import HomeAssistant, callback, Event
|
||||
from homeassistant.components.climate import ClimateEntity, DOMAIN as CLIMATE_DOMAIN
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.device_registry import DeviceInfo, DeviceEntryType
|
||||
from homeassistant.helpers.event import async_track_state_change_event, async_call_later
|
||||
import warnings
|
||||
from types import MappingProxyType
|
||||
from typing import Any, TypeVar
|
||||
|
||||
from .const import ServiceConfigurationError
|
||||
from .underlyings import UnderlyingEntity
|
||||
|
||||
from .base_thermostat import BaseThermostat
|
||||
from .const import DOMAIN, DEVICE_MANUFACTURER, ServiceConfigurationError
|
||||
ConfigData = MappingProxyType[str, Any]
|
||||
T = TypeVar("T", bound=UnderlyingEntity)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -137,102 +135,18 @@ def check_and_extract_service_configuration(service_config) -> dict:
|
||||
return ret
|
||||
|
||||
|
||||
class VersatileThermostatBaseEntity(Entity):
|
||||
"""A base class for all entities"""
|
||||
def deprecated(message):
|
||||
"""A decorator to indicate that the method/attribut is deprecated"""
|
||||
|
||||
_my_climate: BaseThermostat
|
||||
hass: HomeAssistant
|
||||
_config_id: str
|
||||
_device_name: str
|
||||
|
||||
def __init__(self, hass: HomeAssistant, config_id, device_name) -> None:
|
||||
"""The CTOR"""
|
||||
self.hass = hass
|
||||
self._config_id = config_id
|
||||
self._device_name = device_name
|
||||
self._my_climate = None
|
||||
self._cancel_call = None
|
||||
self._attr_has_entity_name = True
|
||||
|
||||
@property
|
||||
def should_poll(self) -> bool:
|
||||
"""Do not poll for those entities"""
|
||||
return False
|
||||
|
||||
@property
|
||||
def my_climate(self) -> BaseThermostat | None:
|
||||
"""Returns my climate if found"""
|
||||
if not self._my_climate:
|
||||
self._my_climate = self.find_my_versatile_thermostat()
|
||||
if self._my_climate:
|
||||
# Only the first time
|
||||
self.my_climate_is_initialized()
|
||||
return self._my_climate
|
||||
|
||||
@property
|
||||
def device_info(self) -> DeviceInfo:
|
||||
"""Return the device info."""
|
||||
return DeviceInfo(
|
||||
entry_type=DeviceEntryType.SERVICE,
|
||||
identifiers={(DOMAIN, self._config_id)},
|
||||
name=self._device_name,
|
||||
manufacturer=DEVICE_MANUFACTURER,
|
||||
model=DOMAIN,
|
||||
)
|
||||
|
||||
def find_my_versatile_thermostat(self) -> BaseThermostat:
|
||||
"""Find the underlying climate entity"""
|
||||
try:
|
||||
component: EntityComponent[ClimateEntity] = self.hass.data[CLIMATE_DOMAIN]
|
||||
for entity in component.entities:
|
||||
# _LOGGER.debug("Device_info is %s", entity.device_info)
|
||||
if entity.device_info == self.device_info:
|
||||
_LOGGER.debug("Found %s!", entity)
|
||||
return entity
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
return None
|
||||
|
||||
@callback
|
||||
async def async_added_to_hass(self):
|
||||
"""Listen to my climate state change"""
|
||||
|
||||
# Check delay condition
|
||||
async def try_find_climate(_):
|
||||
_LOGGER.debug(
|
||||
"%s - Calling VersatileThermostatBaseEntity.async_added_to_hass", self
|
||||
def decorator(func):
|
||||
def wrapper(*args, **kwargs):
|
||||
warnings.warn(
|
||||
f"{func.__name__} is deprecated: {message}",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
mcl = self.my_climate
|
||||
if mcl:
|
||||
if self._cancel_call:
|
||||
self._cancel_call()
|
||||
self._cancel_call = None
|
||||
self.async_on_remove(
|
||||
async_track_state_change_event(
|
||||
self.hass,
|
||||
[mcl.entity_id],
|
||||
self.async_my_climate_changed,
|
||||
)
|
||||
)
|
||||
else:
|
||||
_LOGGER.debug("%s - no entity to listen. Try later", self)
|
||||
self._cancel_call = async_call_later(
|
||||
self.hass, timedelta(seconds=1), try_find_climate
|
||||
)
|
||||
return func(*args, **kwargs)
|
||||
|
||||
await try_find_climate(None)
|
||||
return wrapper
|
||||
|
||||
@callback
|
||||
def my_climate_is_initialized(self):
|
||||
"""Called when the associated climate is initialized"""
|
||||
return
|
||||
|
||||
@callback
|
||||
async def async_my_climate_changed(
|
||||
self, event: Event
|
||||
): # pylint: disable=unused-argument
|
||||
"""Called when my climate have change
|
||||
This method aims to be overriden to take the status change
|
||||
"""
|
||||
return
|
||||
return decorator
|
||||
|
||||
@@ -6,7 +6,7 @@ from __future__ import annotations
|
||||
from typing import Any
|
||||
import logging
|
||||
import copy
|
||||
from collections.abc import Mapping
|
||||
from collections.abc import Mapping # pylint: disable=import-error
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
@@ -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,
|
||||
@@ -259,6 +258,21 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
|
||||
if not self.check_valve_regulation_nb_entities(data, step_id):
|
||||
raise ValveRegulationNbEntitiesIncorrect()
|
||||
|
||||
# Check that the min_opening_degrees is correctly set
|
||||
raw_list = data.get(CONF_MIN_OPENING_DEGREES, None)
|
||||
if raw_list:
|
||||
try:
|
||||
# Validation : Convertir la liste saisie
|
||||
int_list = [int(x.strip()) for x in raw_list.split(",")]
|
||||
|
||||
# Optionnel : Vérifiez des conditions supplémentaires sur la liste
|
||||
if any(x < 0 for x in int_list):
|
||||
raise ValueError
|
||||
except ValueError as exc:
|
||||
raise ValveRegulationMinOpeningDegreesIncorrect(
|
||||
CONF_MIN_OPENING_DEGREES
|
||||
) from exc
|
||||
|
||||
def check_config_complete(self, infos) -> bool:
|
||||
"""True if the config is now complete (ie all mandatory attributes are set)"""
|
||||
is_central_config = (
|
||||
@@ -315,14 +329,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 (
|
||||
@@ -399,6 +406,8 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
|
||||
errors["base"] = "configuration_not_complete"
|
||||
except ValveRegulationNbEntitiesIncorrect as err:
|
||||
errors["base"] = "valve_regulation_nb_entities_incorrect"
|
||||
except ValveRegulationMinOpeningDegreesIncorrect as err:
|
||||
errors[str(err)] = "min_opening_degrees_format"
|
||||
except Exception: # pylint: disable=broad-except
|
||||
_LOGGER.exception("Unexpected exception")
|
||||
errors["base"] = "unknown"
|
||||
@@ -798,7 +807,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"
|
||||
|
||||
@@ -898,7 +907,7 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
|
||||
)
|
||||
|
||||
|
||||
class VersatileThermostatConfigFlow(
|
||||
class VersatileThermostatConfigFlow( # pylint: disable=abstract-method
|
||||
VersatileThermostatBaseConfigFlow, HAConfigFlow, domain=DOMAIN
|
||||
):
|
||||
"""Handle a config flow for Versatile Thermostat."""
|
||||
@@ -912,6 +921,8 @@ class VersatileThermostatConfigFlow(
|
||||
@callback
|
||||
def async_get_options_flow(config_entry: ConfigEntry):
|
||||
"""Get options flow for this handler"""
|
||||
# #713 doesn't work as explained here:https://developers.home-assistant.io/blog/2024/11/12/options-flow
|
||||
# should be - return VersatileThermostatOptionsFlowHandler() but hass is not initialized
|
||||
return VersatileThermostatOptionsFlowHandler(config_entry)
|
||||
|
||||
async def async_step_finalize(self, _):
|
||||
@@ -930,8 +941,12 @@ class VersatileThermostatOptionsFlowHandler(
|
||||
|
||||
def __init__(self, config_entry: ConfigEntry) -> None:
|
||||
"""Initialize options flow."""
|
||||
|
||||
self._conf_app_id: str | None = None
|
||||
|
||||
super().__init__(config_entry.data.copy())
|
||||
self.config_entry = config_entry
|
||||
# #713
|
||||
# self.config_entry = config_entry
|
||||
_LOGGER.debug(
|
||||
"CTOR VersatileThermostatOptionsFlowHandler info: %s, entry_id: %s",
|
||||
self._infos,
|
||||
|
||||
@@ -219,6 +219,7 @@ STEP_VALVE_REGULATION = vol.Schema( # pylint: disable=invalid-name
|
||||
PROPORTIONAL_FUNCTION_TPI,
|
||||
]
|
||||
),
|
||||
vol.Optional(CONF_MIN_OPENING_DEGREES, default=""): str,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -230,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, mode=selector.NumberSelectorMode.BOX)),
|
||||
}
|
||||
)
|
||||
|
||||
@@ -264,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",
|
||||
@@ -282,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",
|
||||
@@ -338,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,
|
||||
@@ -367,14 +364,14 @@ STEP_PRESENCE_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name
|
||||
STEP_CENTRAL_ADVANCED_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name
|
||||
{
|
||||
vol.Required(CONF_MINIMAL_ACTIVATION_DELAY, default=10): cv.positive_int,
|
||||
vol.Required(CONF_SECURITY_DELAY_MIN, default=60): cv.positive_int,
|
||||
vol.Required(CONF_SAFETY_DELAY_MIN, default=60): cv.positive_int,
|
||||
vol.Required(
|
||||
CONF_SECURITY_MIN_ON_PERCENT,
|
||||
default=DEFAULT_SECURITY_MIN_ON_PERCENT,
|
||||
CONF_SAFETY_MIN_ON_PERCENT,
|
||||
default=DEFAULT_SAFETY_MIN_ON_PERCENT,
|
||||
): vol.Coerce(float),
|
||||
vol.Required(
|
||||
CONF_SECURITY_DEFAULT_ON_PERCENT,
|
||||
default=DEFAULT_SECURITY_DEFAULT_ON_PERCENT,
|
||||
CONF_SAFETY_DEFAULT_ON_PERCENT,
|
||||
default=DEFAULT_SAFETY_DEFAULT_ON_PERCENT,
|
||||
): vol.Coerce(float),
|
||||
}
|
||||
)
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
import logging
|
||||
import math
|
||||
from typing import Literal
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from enum import Enum
|
||||
@@ -28,7 +29,7 @@ from .prop_algorithm import (
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONFIG_VERSION = 2
|
||||
CONFIG_MINOR_VERSION = 0
|
||||
CONFIG_MINOR_VERSION = 1
|
||||
|
||||
PRESET_TEMP_SUFFIX = "_temp"
|
||||
PRESET_AC_SUFFIX = "_ac"
|
||||
@@ -41,10 +42,10 @@ DEVICE_MANUFACTURER = "JMCOLLIN"
|
||||
DEVICE_MODEL = "Versatile Thermostat"
|
||||
|
||||
PRESET_POWER = "power"
|
||||
PRESET_SECURITY = "security"
|
||||
PRESET_SAFETY = "security"
|
||||
PRESET_FROST_PROTECTION = "frost"
|
||||
|
||||
HIDDEN_PRESETS = [PRESET_POWER, PRESET_SECURITY]
|
||||
HIDDEN_PRESETS = [PRESET_POWER, PRESET_SAFETY]
|
||||
|
||||
DOMAIN = "versatile_thermostat"
|
||||
|
||||
@@ -72,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"
|
||||
@@ -83,9 +85,9 @@ CONF_PRESET_POWER = "power_temp"
|
||||
CONF_MINIMAL_ACTIVATION_DELAY = "minimal_activation_delay"
|
||||
CONF_TEMP_MIN = "temp_min"
|
||||
CONF_TEMP_MAX = "temp_max"
|
||||
CONF_SECURITY_DELAY_MIN = "security_delay_min"
|
||||
CONF_SECURITY_MIN_ON_PERCENT = "security_min_on_percent"
|
||||
CONF_SECURITY_DEFAULT_ON_PERCENT = "security_default_on_percent"
|
||||
CONF_SAFETY_DELAY_MIN = "safety_delay_min"
|
||||
CONF_SAFETY_MIN_ON_PERCENT = "safety_min_on_percent"
|
||||
CONF_SAFETY_DEFAULT_ON_PERCENT = "safety_default_on_percent"
|
||||
CONF_THERMOSTAT_TYPE = "thermostat_type"
|
||||
CONF_THERMOSTAT_CENTRAL_CONFIG = "thermostat_central_config"
|
||||
CONF_THERMOSTAT_SWITCH = "thermostat_over_switch"
|
||||
@@ -123,6 +125,7 @@ CONF_STEP_TEMPERATURE = "step_temperature"
|
||||
CONF_OFFSET_CALIBRATION_LIST = "offset_calibration_entity_ids"
|
||||
CONF_OPENING_DEGREE_LIST = "opening_degree_entity_ids"
|
||||
CONF_CLOSING_DEGREE_LIST = "closing_degree_entity_ids"
|
||||
CONF_MIN_OPENING_DEGREES = "min_opening_degrees"
|
||||
|
||||
# Deprecated
|
||||
CONF_HEATER = "heater_entity_id"
|
||||
@@ -268,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,
|
||||
@@ -284,9 +288,9 @@ ALL_CONF = (
|
||||
CONF_MINIMAL_ACTIVATION_DELAY,
|
||||
CONF_TEMP_MIN,
|
||||
CONF_TEMP_MAX,
|
||||
CONF_SECURITY_DELAY_MIN,
|
||||
CONF_SECURITY_MIN_ON_PERCENT,
|
||||
CONF_SECURITY_DEFAULT_ON_PERCENT,
|
||||
CONF_SAFETY_DELAY_MIN,
|
||||
CONF_SAFETY_MIN_ON_PERCENT,
|
||||
CONF_SAFETY_DEFAULT_ON_PERCENT,
|
||||
CONF_THERMOSTAT_TYPE,
|
||||
CONF_THERMOSTAT_SWITCH,
|
||||
CONF_THERMOSTAT_CLIMATE,
|
||||
@@ -372,13 +376,13 @@ SUPPORT_FLAGS = (
|
||||
|
||||
SERVICE_SET_PRESENCE = "set_presence"
|
||||
SERVICE_SET_PRESET_TEMPERATURE = "set_preset_temperature"
|
||||
SERVICE_SET_SECURITY = "set_security"
|
||||
SERVICE_SET_SAFETY = "set_safety"
|
||||
SERVICE_SET_WINDOW_BYPASS = "set_window_bypass"
|
||||
SERVICE_SET_AUTO_REGULATION_MODE = "set_auto_regulation_mode"
|
||||
SERVICE_SET_AUTO_FAN_MODE = "set_auto_fan_mode"
|
||||
|
||||
DEFAULT_SECURITY_MIN_ON_PERCENT = 0.5
|
||||
DEFAULT_SECURITY_DEFAULT_ON_PERCENT = 0.1
|
||||
DEFAULT_SAFETY_MIN_ON_PERCENT = 0.5
|
||||
DEFAULT_SAFETY_DEFAULT_ON_PERCENT = 0.1
|
||||
|
||||
ATTR_TOTAL_ENERGY = "total_energy"
|
||||
ATTR_MEAN_POWER_CYCLE = "mean_cycle_power"
|
||||
@@ -501,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"
|
||||
):
|
||||
@@ -552,6 +558,10 @@ class ValveRegulationNbEntitiesIncorrect(HomeAssistantError):
|
||||
The number of specific entities is incorrect."""
|
||||
|
||||
|
||||
class ValveRegulationMinOpeningDegreesIncorrect(HomeAssistantError):
|
||||
"""Error to indicate that the minimal opening degrees is not a list of int separated by coma"""
|
||||
|
||||
|
||||
class overrides: # pylint: disable=invalid-name
|
||||
"""An annotation to inform overrides"""
|
||||
|
||||
|
||||
@@ -0,0 +1,236 @@
|
||||
""" Implements the Auto-start/stop Feature Manager """
|
||||
|
||||
# pylint: disable=line-too-long
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.core import (
|
||||
HomeAssistant,
|
||||
)
|
||||
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 .auto_start_stop_algorithm import (
|
||||
AutoStartStopDetectionAlgorithm,
|
||||
AUTO_START_STOP_ACTION_OFF,
|
||||
AUTO_START_STOP_ACTION_ON,
|
||||
)
|
||||
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class FeatureAutoStartStopManager(BaseFeatureManager):
|
||||
"""The implementation of the AutoStartStop feature"""
|
||||
|
||||
unrecorded_attributes = frozenset(
|
||||
{
|
||||
"auto_start_stop_level",
|
||||
"auto_start_stop_dtmin",
|
||||
"auto_start_stop_enable",
|
||||
"auto_start_stop_accumulated_error",
|
||||
"auto_start_stop_accumulated_error_threshold",
|
||||
"auto_start_stop_last_switch_date",
|
||||
}
|
||||
)
|
||||
|
||||
def __init__(self, vtherm: Any, hass: HomeAssistant):
|
||||
"""Init of a featureManager"""
|
||||
super().__init__(vtherm, hass)
|
||||
|
||||
self._auto_start_stop_level: TYPE_AUTO_START_STOP_LEVELS = (
|
||||
AUTO_START_STOP_LEVEL_NONE
|
||||
)
|
||||
self._auto_start_stop_algo: AutoStartStopDetectionAlgorithm | None = None
|
||||
self._is_configured: bool = False
|
||||
self._is_auto_start_stop_enabled: bool = False
|
||||
|
||||
@overrides
|
||||
def post_init(self, entry_infos: ConfigData):
|
||||
"""Reinit of the manager"""
|
||||
|
||||
use_auto_start_stop = entry_infos.get(CONF_USE_AUTO_START_STOP_FEATURE, False)
|
||||
if use_auto_start_stop:
|
||||
self._auto_start_stop_level = (
|
||||
entry_infos.get(CONF_AUTO_START_STOP_LEVEL, None)
|
||||
or AUTO_START_STOP_LEVEL_NONE
|
||||
)
|
||||
self._is_configured = True
|
||||
else:
|
||||
self._auto_start_stop_level = AUTO_START_STOP_LEVEL_NONE
|
||||
self._is_configured = False
|
||||
|
||||
# Instanciate the auto start stop algo
|
||||
self._auto_start_stop_algo = AutoStartStopDetectionAlgorithm(
|
||||
self._auto_start_stop_level, self.name
|
||||
)
|
||||
|
||||
@overrides
|
||||
async def start_listening(self):
|
||||
"""Start listening the underlying entity"""
|
||||
|
||||
@overrides
|
||||
def stop_listening(self):
|
||||
"""Stop listening and remove the eventual timer still running"""
|
||||
|
||||
@overrides
|
||||
async def refresh_state(self) -> bool:
|
||||
"""Check the auto-start-stop and an eventual action
|
||||
Return False if we should stop the control_heating method"""
|
||||
|
||||
if not self._is_configured or not self._is_auto_start_stop_enabled:
|
||||
_LOGGER.debug("%s - auto start/stop is disabled (or not configured)", self)
|
||||
return True
|
||||
|
||||
slope = (
|
||||
self._vtherm.last_temperature_slope or 0
|
||||
) / 60 # to have the slope in °/min
|
||||
action = self._auto_start_stop_algo.calculate_action(
|
||||
self._vtherm.hvac_mode,
|
||||
self._vtherm.saved_hvac_mode,
|
||||
self._vtherm.target_temperature,
|
||||
self._vtherm.current_temperature,
|
||||
slope,
|
||||
self._vtherm.now,
|
||||
)
|
||||
_LOGGER.debug("%s - auto_start_stop action is %s", self, action)
|
||||
if action == AUTO_START_STOP_ACTION_OFF and self._vtherm.is_on:
|
||||
_LOGGER.info(
|
||||
"%s - Turning OFF the Vtherm due to auto-start-stop conditions",
|
||||
self,
|
||||
)
|
||||
self._vtherm.set_hvac_off_reason(HVAC_OFF_REASON_AUTO_START_STOP)
|
||||
await self._vtherm.async_turn_off()
|
||||
|
||||
# Send an event
|
||||
self._vtherm.send_event(
|
||||
event_type=EventType.AUTO_START_STOP_EVENT,
|
||||
data={
|
||||
"type": "stop",
|
||||
"name": self.name,
|
||||
"cause": "Auto stop conditions reached",
|
||||
"hvac_mode": self._vtherm.hvac_mode,
|
||||
"saved_hvac_mode": self._vtherm.saved_hvac_mode,
|
||||
"target_temperature": self._vtherm.target_temperature,
|
||||
"current_temperature": self._vtherm.current_temperature,
|
||||
"temperature_slope": round(slope, 3),
|
||||
"accumulated_error": self._auto_start_stop_algo.accumulated_error,
|
||||
"accumulated_error_threshold": self._auto_start_stop_algo.accumulated_error_threshold,
|
||||
},
|
||||
)
|
||||
|
||||
# Stop here
|
||||
return False
|
||||
elif (
|
||||
action == AUTO_START_STOP_ACTION_ON
|
||||
and self._vtherm.hvac_off_reason == HVAC_OFF_REASON_AUTO_START_STOP
|
||||
):
|
||||
_LOGGER.info(
|
||||
"%s - Turning ON the Vtherm due to auto-start-stop conditions", self
|
||||
)
|
||||
await self._vtherm.async_turn_on()
|
||||
|
||||
# Send an event
|
||||
self._vtherm.send_event(
|
||||
event_type=EventType.AUTO_START_STOP_EVENT,
|
||||
data={
|
||||
"type": "start",
|
||||
"name": self.name,
|
||||
"cause": "Auto start conditions reached",
|
||||
"hvac_mode": self._vtherm.hvac_mode,
|
||||
"saved_hvac_mode": self._vtherm.saved_hvac_mode,
|
||||
"target_temperature": self._vtherm.target_temperature,
|
||||
"current_temperature": self._vtherm.current_temperature,
|
||||
"temperature_slope": round(slope, 3),
|
||||
"accumulated_error": self._auto_start_stop_algo.accumulated_error,
|
||||
"accumulated_error_threshold": self._auto_start_stop_algo.accumulated_error_threshold,
|
||||
},
|
||||
)
|
||||
|
||||
self._vtherm.update_custom_attributes()
|
||||
|
||||
return True
|
||||
|
||||
def set_auto_start_stop_enable(self, is_enabled: bool):
|
||||
"""Enable/Disable the auto-start/stop feature"""
|
||||
self._is_auto_start_stop_enabled = is_enabled
|
||||
if (
|
||||
self._vtherm.hvac_mode == HVACMode.OFF
|
||||
and self._vtherm.hvac_off_reason == HVAC_OFF_REASON_AUTO_START_STOP
|
||||
):
|
||||
_LOGGER.debug(
|
||||
"%s - the vtherm is off cause auto-start/stop and enable have been set to false -> starts the VTherm"
|
||||
)
|
||||
self.hass.create_task(self._vtherm.async_turn_on())
|
||||
|
||||
# Send an event
|
||||
self._vtherm.send_event(
|
||||
event_type=EventType.AUTO_START_STOP_EVENT,
|
||||
data={
|
||||
"type": "start",
|
||||
"name": self.name,
|
||||
"cause": "Auto start stop disabled",
|
||||
"hvac_mode": self._vtherm.hvac_mode,
|
||||
"saved_hvac_mode": self._vtherm.saved_hvac_mode,
|
||||
"target_temperature": self._vtherm.target_temperature,
|
||||
"current_temperature": self._vtherm.current_temperature,
|
||||
"temperature_slope": round(
|
||||
self._vtherm.last_temperature_slope or 0, 3
|
||||
),
|
||||
"accumulated_error": self._auto_start_stop_algo.accumulated_error,
|
||||
"accumulated_error_threshold": self._auto_start_stop_algo.accumulated_error_threshold,
|
||||
},
|
||||
)
|
||||
|
||||
self._vtherm.update_custom_attributes()
|
||||
|
||||
def add_custom_attributes(self, extra_state_attributes: dict[str, Any]):
|
||||
"""Add some custom attributes"""
|
||||
extra_state_attributes.update(
|
||||
{
|
||||
"is_auto_start_stop_configured": self.is_configured,
|
||||
}
|
||||
)
|
||||
if self.is_configured:
|
||||
extra_state_attributes.update(
|
||||
{
|
||||
"auto_start_stop_enable": self.auto_start_stop_enable,
|
||||
"auto_start_stop_level": self._auto_start_stop_algo.level,
|
||||
"auto_start_stop_dtmin": self._auto_start_stop_algo.dt_min,
|
||||
"auto_start_stop_accumulated_error": self._auto_start_stop_algo.accumulated_error,
|
||||
"auto_start_stop_accumulated_error_threshold": self._auto_start_stop_algo.accumulated_error_threshold,
|
||||
"auto_start_stop_last_switch_date": self._auto_start_stop_algo.last_switch_date,
|
||||
}
|
||||
)
|
||||
|
||||
@overrides
|
||||
@property
|
||||
def is_configured(self) -> bool:
|
||||
"""Return True of the aiuto-start/stop feature is configured"""
|
||||
return self._is_configured
|
||||
|
||||
@property
|
||||
def auto_start_stop_level(self) -> TYPE_AUTO_START_STOP_LEVELS:
|
||||
"""Return the auto start/stop level."""
|
||||
return self._auto_start_stop_level
|
||||
|
||||
@property
|
||||
def auto_start_stop_enable(self) -> bool:
|
||||
"""Returns the auto_start_stop_enable"""
|
||||
return self._is_auto_start_stop_enabled
|
||||
|
||||
@property
|
||||
def is_auto_stopped(self) -> bool:
|
||||
"""Returns the is vtherm is stopped and reason is AUTO_START_STOP"""
|
||||
return (
|
||||
self._vtherm.hvac_mode == HVACMode.OFF
|
||||
and self._vtherm.hvac_off_reason == HVAC_OFF_REASON_AUTO_START_STOP
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return f"AutoStartStopManager-{self.name}"
|
||||
343
custom_components/versatile_thermostat/feature_motion_manager.py
Normal file
343
custom_components/versatile_thermostat/feature_motion_manager.py
Normal file
@@ -0,0 +1,343 @@
|
||||
""" Implements the Motion Feature Manager """
|
||||
|
||||
# pylint: disable=line-too-long
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
from datetime import timedelta
|
||||
|
||||
from homeassistant.const import (
|
||||
STATE_ON,
|
||||
STATE_OFF,
|
||||
STATE_UNAVAILABLE,
|
||||
STATE_UNKNOWN,
|
||||
)
|
||||
from homeassistant.core import (
|
||||
HomeAssistant,
|
||||
callback,
|
||||
Event,
|
||||
)
|
||||
from homeassistant.helpers.event import (
|
||||
async_track_state_change_event,
|
||||
EventStateChangedData,
|
||||
async_call_later,
|
||||
)
|
||||
|
||||
from homeassistant.components.climate import (
|
||||
PRESET_ACTIVITY,
|
||||
)
|
||||
|
||||
from homeassistant.exceptions import ConditionError
|
||||
from homeassistant.helpers import condition
|
||||
|
||||
from .const import * # pylint: disable=wildcard-import, unused-wildcard-import
|
||||
from .commons import ConfigData
|
||||
|
||||
from .base_manager import BaseFeatureManager
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class FeatureMotionManager(BaseFeatureManager):
|
||||
"""The implementation of the Motion feature"""
|
||||
|
||||
unrecorded_attributes = frozenset(
|
||||
{
|
||||
"motion_sensor_entity_id",
|
||||
"is_motion_configured",
|
||||
"motion_delay_sec",
|
||||
"motion_off_delay_sec",
|
||||
"motion_preset",
|
||||
"no_motion_preset",
|
||||
}
|
||||
)
|
||||
|
||||
def __init__(self, vtherm: Any, hass: HomeAssistant):
|
||||
"""Init of a featureManager"""
|
||||
super().__init__(vtherm, hass)
|
||||
self._motion_state: str = STATE_UNAVAILABLE
|
||||
self._motion_sensor_entity_id: str = None
|
||||
self._motion_delay_sec: int | None = 0
|
||||
self._motion_off_delay_sec: int | None = 0
|
||||
self._motion_preset: str | None = None
|
||||
self._no_motion_preset: str | None = None
|
||||
self._is_configured: bool = False
|
||||
self._motion_call_cancel: callable = None
|
||||
|
||||
@overrides
|
||||
def post_init(self, entry_infos: ConfigData):
|
||||
"""Reinit of the manager"""
|
||||
self.dearm_motion_timer()
|
||||
|
||||
self._motion_sensor_entity_id = entry_infos.get(CONF_MOTION_SENSOR, None)
|
||||
self._motion_delay_sec = entry_infos.get(CONF_MOTION_DELAY, 0)
|
||||
self._motion_off_delay_sec = entry_infos.get(CONF_MOTION_OFF_DELAY, None)
|
||||
if not self._motion_off_delay_sec:
|
||||
self._motion_off_delay_sec = self._motion_delay_sec
|
||||
|
||||
self._motion_preset = entry_infos.get(CONF_MOTION_PRESET)
|
||||
self._no_motion_preset = entry_infos.get(CONF_NO_MOTION_PRESET)
|
||||
if (
|
||||
self._motion_sensor_entity_id is not None
|
||||
and self._motion_preset is not None
|
||||
and self._no_motion_preset is not None
|
||||
):
|
||||
self._is_configured = True
|
||||
self._motion_state = STATE_UNKNOWN
|
||||
|
||||
@overrides
|
||||
async def start_listening(self):
|
||||
"""Start listening the underlying entity"""
|
||||
if self._is_configured:
|
||||
self.stop_listening()
|
||||
self.add_listener(
|
||||
async_track_state_change_event(
|
||||
self.hass,
|
||||
[self._motion_sensor_entity_id],
|
||||
self._motion_sensor_changed,
|
||||
)
|
||||
)
|
||||
|
||||
@overrides
|
||||
def stop_listening(self):
|
||||
"""Stop listening and remove the eventual timer still running"""
|
||||
self.dearm_motion_timer()
|
||||
super().stop_listening()
|
||||
|
||||
def dearm_motion_timer(self):
|
||||
"""Dearm the eventual motion time running"""
|
||||
if self._motion_call_cancel:
|
||||
self._motion_call_cancel()
|
||||
self._motion_call_cancel = None
|
||||
|
||||
@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:
|
||||
|
||||
motion_state = self.hass.states.get(self._motion_sensor_entity_id)
|
||||
if motion_state and motion_state.state not in (
|
||||
STATE_UNAVAILABLE,
|
||||
STATE_UNKNOWN,
|
||||
):
|
||||
_LOGGER.debug(
|
||||
"%s - Motion state have been retrieved: %s",
|
||||
self,
|
||||
self._motion_state,
|
||||
)
|
||||
# recalculate the right target_temp in activity mode
|
||||
ret = await self.update_motion_state(motion_state.state, False)
|
||||
|
||||
return ret
|
||||
|
||||
@callback
|
||||
async def _motion_sensor_changed(self, event: Event[EventStateChangedData]):
|
||||
"""Handle motion sensor changes."""
|
||||
new_state = event.data.get("new_state")
|
||||
_LOGGER.info(
|
||||
"%s - Motion changed. Event.new_state is %s, _attr_preset_mode=%s, activity=%s",
|
||||
self,
|
||||
new_state,
|
||||
self._vtherm.preset_mode,
|
||||
PRESET_ACTIVITY,
|
||||
)
|
||||
|
||||
if new_state is None or new_state.state not in (STATE_OFF, STATE_ON):
|
||||
return
|
||||
|
||||
# Check delay condition
|
||||
async def try_motion_condition(_):
|
||||
self.dearm_motion_timer()
|
||||
|
||||
try:
|
||||
delay = (
|
||||
self._motion_delay_sec
|
||||
if new_state.state == STATE_ON
|
||||
else self._motion_off_delay_sec
|
||||
)
|
||||
long_enough = condition.state(
|
||||
self.hass,
|
||||
self._motion_sensor_entity_id,
|
||||
new_state.state,
|
||||
timedelta(seconds=delay),
|
||||
)
|
||||
except ConditionError:
|
||||
long_enough = False
|
||||
|
||||
if not long_enough:
|
||||
_LOGGER.debug(
|
||||
"Motion delay condition is not satisfied (the sensor have change its state during the delay). Check motion sensor state"
|
||||
)
|
||||
# Get sensor current state
|
||||
motion_state = self.hass.states.get(self._motion_sensor_entity_id)
|
||||
_LOGGER.debug(
|
||||
"%s - motion_state=%s, new_state.state=%s",
|
||||
self,
|
||||
motion_state.state,
|
||||
new_state.state,
|
||||
)
|
||||
if (
|
||||
motion_state.state == new_state.state
|
||||
and new_state.state == STATE_ON
|
||||
):
|
||||
_LOGGER.debug(
|
||||
"%s - the motion sensor is finally 'on' after the delay", self
|
||||
)
|
||||
long_enough = True
|
||||
else:
|
||||
long_enough = False
|
||||
|
||||
if long_enough:
|
||||
_LOGGER.debug("%s - Motion delay condition is satisfied", self)
|
||||
await self.update_motion_state(new_state.state)
|
||||
else:
|
||||
await self.update_motion_state(
|
||||
STATE_ON if new_state.state == STATE_OFF else STATE_OFF
|
||||
)
|
||||
|
||||
im_on = self._motion_state == STATE_ON
|
||||
delay_running = self._motion_call_cancel is not None
|
||||
event_on = new_state.state == STATE_ON
|
||||
|
||||
def arm():
|
||||
"""Arm the timer"""
|
||||
delay = (
|
||||
self._motion_delay_sec
|
||||
if new_state.state == STATE_ON
|
||||
else self._motion_off_delay_sec
|
||||
)
|
||||
self._motion_call_cancel = async_call_later(
|
||||
self.hass, timedelta(seconds=delay), try_motion_condition
|
||||
)
|
||||
|
||||
# if I'm off
|
||||
if not im_on:
|
||||
if event_on and not delay_running:
|
||||
_LOGGER.debug(
|
||||
"%s - Arm delay cause i'm off and event is on and no delay is running",
|
||||
self,
|
||||
)
|
||||
arm()
|
||||
return try_motion_condition
|
||||
# Ignore the event
|
||||
_LOGGER.debug("%s - Event ignored cause i'm already off", self)
|
||||
return None
|
||||
else: # I'm On
|
||||
if not event_on and not delay_running:
|
||||
_LOGGER.info("%s - Arm delay cause i'm on and event is off", self)
|
||||
arm()
|
||||
return try_motion_condition
|
||||
if event_on and delay_running:
|
||||
_LOGGER.debug(
|
||||
"%s - Desarm off delay cause i'm on and event is on and a delay is running",
|
||||
self,
|
||||
)
|
||||
self.dearm_motion_timer()
|
||||
return None
|
||||
# Ignore the event
|
||||
_LOGGER.debug("%s - Event ignored cause i'm already on", self)
|
||||
return None
|
||||
|
||||
async def update_motion_state(
|
||||
self, new_state: str = None, recalculate: bool = True
|
||||
) -> bool:
|
||||
"""Update the value of the motion sensor and update the VTherm state accordingly
|
||||
Return true if a change has been made"""
|
||||
|
||||
_LOGGER.info("%s - Updating motion state. New state is %s", self, new_state)
|
||||
old_motion_state = self._motion_state
|
||||
if new_state is not None:
|
||||
self._motion_state = STATE_ON if new_state == STATE_ON else STATE_OFF
|
||||
|
||||
if self._vtherm.preset_mode == PRESET_ACTIVITY:
|
||||
new_preset = self.get_current_motion_preset()
|
||||
_LOGGER.info(
|
||||
"%s - Motion condition have changes. New preset temp will be %s",
|
||||
self,
|
||||
new_preset,
|
||||
)
|
||||
# We do not change the preset which is kept to ACTIVITY but only the target_temperature
|
||||
# We take the motion into account
|
||||
new_temp = self._vtherm.find_preset_temp(new_preset)
|
||||
old_temp = self._vtherm.target_temperature
|
||||
if new_temp != old_temp:
|
||||
await self._vtherm.change_target_temperature(new_temp)
|
||||
|
||||
if new_temp != old_temp and recalculate:
|
||||
self._vtherm.recalculate()
|
||||
await self._vtherm.async_control_heating(force=True)
|
||||
|
||||
return old_motion_state != self._motion_state
|
||||
|
||||
def get_current_motion_preset(self) -> str:
|
||||
"""Calculate and return the current motion preset"""
|
||||
return (
|
||||
self._motion_preset
|
||||
if self._motion_state == STATE_ON
|
||||
else self._no_motion_preset
|
||||
)
|
||||
|
||||
def add_custom_attributes(self, extra_state_attributes: dict[str, Any]):
|
||||
"""Add some custom attributes"""
|
||||
extra_state_attributes.update(
|
||||
{
|
||||
"motion_sensor_entity_id": self._motion_sensor_entity_id,
|
||||
"motion_state": self._motion_state,
|
||||
"is_motion_configured": self._is_configured,
|
||||
"motion_delay_sec": self._motion_delay_sec,
|
||||
"motion_off_delay_sec": self._motion_off_delay_sec,
|
||||
"motion_preset": self._motion_preset,
|
||||
"no_motion_preset": self._no_motion_preset,
|
||||
}
|
||||
)
|
||||
|
||||
@overrides
|
||||
@property
|
||||
def is_configured(self) -> bool:
|
||||
"""Return True of the motion is configured"""
|
||||
return self._is_configured
|
||||
|
||||
@property
|
||||
def motion_state(self) -> str | None:
|
||||
"""Return the current motion state STATE_ON or STATE_OFF
|
||||
or STATE_UNAVAILABLE if not configured"""
|
||||
if not self._is_configured:
|
||||
return STATE_UNAVAILABLE
|
||||
return self._motion_state
|
||||
|
||||
@property
|
||||
def is_motion_detected(self) -> bool:
|
||||
"""Return true if the motion is configured and motion sensor is OFF"""
|
||||
return self._is_configured and self._motion_state in [
|
||||
STATE_ON,
|
||||
]
|
||||
|
||||
@property
|
||||
def motion_sensor_entity_id(self) -> bool:
|
||||
"""Return true if the motion is configured and motion sensor is OFF"""
|
||||
return self._motion_sensor_entity_id
|
||||
|
||||
@property
|
||||
def motion_delay_sec(self) -> bool:
|
||||
"""Return the motion delay"""
|
||||
return self._motion_delay_sec
|
||||
|
||||
@property
|
||||
def motion_off_delay_sec(self) -> bool:
|
||||
"""Return motion delay off"""
|
||||
return self._motion_off_delay_sec
|
||||
|
||||
@property
|
||||
def motion_preset(self) -> bool:
|
||||
"""Return motion preset"""
|
||||
return self._motion_preset
|
||||
|
||||
@property
|
||||
def no_motion_preset(self) -> bool:
|
||||
"""Return no motion preset"""
|
||||
return self._no_motion_preset
|
||||
|
||||
def __str__(self):
|
||||
return f"MotionManager-{self.name}"
|
||||
262
custom_components/versatile_thermostat/feature_power_manager.py
Normal file
262
custom_components/versatile_thermostat/feature_power_manager.py
Normal file
@@ -0,0 +1,262 @@
|
||||
""" Implements the Power Feature Manager """
|
||||
|
||||
# pylint: disable=line-too-long
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.const import (
|
||||
STATE_ON,
|
||||
STATE_OFF,
|
||||
STATE_UNAVAILABLE,
|
||||
STATE_UNKNOWN,
|
||||
)
|
||||
|
||||
from homeassistant.core import (
|
||||
HomeAssistant,
|
||||
)
|
||||
|
||||
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__)
|
||||
|
||||
|
||||
class FeaturePowerManager(BaseFeatureManager):
|
||||
"""The implementation of the Power feature"""
|
||||
|
||||
unrecorded_attributes = frozenset(
|
||||
{
|
||||
"power_sensor_entity_id",
|
||||
"max_power_sensor_entity_id",
|
||||
"is_power_configured",
|
||||
"device_power",
|
||||
"power_temp",
|
||||
"current_power",
|
||||
"current_max_power",
|
||||
}
|
||||
)
|
||||
|
||||
def __init__(self, vtherm: Any, hass: HomeAssistant):
|
||||
"""Init of a featureManager"""
|
||||
super().__init__(vtherm, hass)
|
||||
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_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
|
||||
|
||||
@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
|
||||
# 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:
|
||||
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": 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": 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_power_available(self) -> bool:
|
||||
"""Check if the Vtherm can be started considering overpowering.
|
||||
Returns True if no overpowering conditions are found
|
||||
"""
|
||||
|
||||
vtherm_api = VersatileThermostatAPI.get_vtherm_api()
|
||||
if (
|
||||
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
|
||||
):
|
||||
_LOGGER.warning(
|
||||
"%s - power not valued. check_power_available not available", self
|
||||
)
|
||||
return True
|
||||
|
||||
_LOGGER.debug(
|
||||
"%s - overpowering check: power=%.3f, max_power=%.3f heater power=%.3f",
|
||||
self,
|
||||
current_power,
|
||||
current_max_power,
|
||||
self._device_power,
|
||||
)
|
||||
|
||||
# issue 407 - power_consumption_max is power we need to add. If already active we don't need to add more power
|
||||
if self._vtherm.is_device_active:
|
||||
power_consumption_max = 0
|
||||
else:
|
||||
if self._vtherm.is_over_climate:
|
||||
power_consumption_max = self._device_power
|
||||
else:
|
||||
power_consumption_max = max(
|
||||
self._device_power / self._vtherm.nb_underlying_entities,
|
||||
self._device_power * self._vtherm.proportional_algorithm.on_percent,
|
||||
)
|
||||
|
||||
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, force=True)
|
||||
self._vtherm.send_event(
|
||||
EventType.POWER_EVENT,
|
||||
{
|
||||
"type": "start",
|
||||
"current_power": current_power,
|
||||
"device_power": self._device_power,
|
||||
"current_max_power": current_max_power,
|
||||
"current_power_consumption": power_consumption_max,
|
||||
},
|
||||
)
|
||||
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()
|
||||
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": current_power,
|
||||
"device_power": self._device_power,
|
||||
"current_max_power": current_max_power,
|
||||
},
|
||||
)
|
||||
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
|
||||
def is_configured(self) -> bool:
|
||||
"""Return True of the presence is configured"""
|
||||
return self._is_configured
|
||||
|
||||
@property
|
||||
def overpowering_state(self) -> str | None:
|
||||
"""Return the current overpowering state STATE_ON or STATE_OFF
|
||||
or STATE_UNAVAILABLE if not configured"""
|
||||
if not self._is_configured:
|
||||
return STATE_UNAVAILABLE
|
||||
return self._overpowering_state
|
||||
|
||||
@property
|
||||
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:
|
||||
"""Return the power temperature"""
|
||||
return self._power_temp
|
||||
|
||||
@property
|
||||
def device_power(self) -> bool:
|
||||
"""Return the device power"""
|
||||
return self._device_power
|
||||
|
||||
@property
|
||||
def mean_cycle_power(self) -> float | None:
|
||||
"""Returns the mean power consumption during the cycle"""
|
||||
if not self._device_power or not self._vtherm.proportional_algorithm:
|
||||
return None
|
||||
|
||||
return float(
|
||||
self._device_power * self._vtherm.proportional_algorithm.on_percent
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return f"PowerManager-{self.name}"
|
||||
@@ -0,0 +1,200 @@
|
||||
""" Implements the Presence Feature Manager """
|
||||
|
||||
# pylint: disable=line-too-long
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.const import (
|
||||
STATE_ON,
|
||||
STATE_OFF,
|
||||
STATE_HOME,
|
||||
STATE_NOT_HOME,
|
||||
STATE_UNAVAILABLE,
|
||||
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 PRESET_ACTIVITY, PRESET_BOOST, PRESET_COMFORT, PRESET_ECO
|
||||
|
||||
from .const import * # pylint: disable=wildcard-import, unused-wildcard-import
|
||||
from .commons import ConfigData
|
||||
|
||||
from .base_manager import BaseFeatureManager
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class FeaturePresenceManager(BaseFeatureManager):
|
||||
"""The implementation of the Presence feature"""
|
||||
|
||||
unrecorded_attributes = frozenset(
|
||||
{
|
||||
"presence_sensor_entity_id",
|
||||
"is_presence_configured",
|
||||
}
|
||||
)
|
||||
|
||||
def __init__(self, vtherm: Any, hass: HomeAssistant):
|
||||
"""Init of a featureManager"""
|
||||
super().__init__(vtherm, hass)
|
||||
self._presence_state: str = STATE_UNAVAILABLE
|
||||
self._presence_sensor_entity_id: str = None
|
||||
self._is_configured: bool = False
|
||||
|
||||
@overrides
|
||||
def post_init(self, entry_infos: ConfigData):
|
||||
"""Reinit of the manager"""
|
||||
self._presence_sensor_entity_id = entry_infos.get(CONF_PRESENCE_SENSOR)
|
||||
if (
|
||||
entry_infos.get(CONF_USE_PRESENCE_FEATURE, False)
|
||||
and self._presence_sensor_entity_id is not None
|
||||
):
|
||||
self._is_configured = True
|
||||
self._presence_state = STATE_UNKNOWN
|
||||
|
||||
@overrides
|
||||
async def start_listening(self):
|
||||
"""Start listening the underlying entity"""
|
||||
if self._is_configured:
|
||||
self.stop_listening()
|
||||
self.add_listener(
|
||||
async_track_state_change_event(
|
||||
self.hass,
|
||||
[self._presence_sensor_entity_id],
|
||||
self._presence_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 presence entity state
|
||||
presence_state = self.hass.states.get(self._presence_sensor_entity_id)
|
||||
if presence_state and presence_state.state not in (
|
||||
STATE_UNAVAILABLE,
|
||||
STATE_UNKNOWN,
|
||||
):
|
||||
ret = await self.update_presence(presence_state.state)
|
||||
_LOGGER.debug(
|
||||
"%s - Presence have been retrieved: %s",
|
||||
self,
|
||||
presence_state.state,
|
||||
)
|
||||
return ret
|
||||
|
||||
@callback
|
||||
async def _presence_sensor_changed(self, event: Event[EventStateChangedData]):
|
||||
"""Handle presence changes."""
|
||||
new_state = event.data.get("new_state")
|
||||
_LOGGER.info(
|
||||
"%s - Presence changed. Event.new_state is %s, _attr_preset_mode=%s, activity=%s",
|
||||
self,
|
||||
new_state,
|
||||
self._vtherm.preset_mode,
|
||||
PRESET_ACTIVITY,
|
||||
)
|
||||
if new_state is None:
|
||||
return
|
||||
|
||||
if await self.update_presence(new_state.state):
|
||||
await self._vtherm.async_control_heating(force=True)
|
||||
|
||||
async def update_presence(self, new_state: str) -> bool:
|
||||
"""Update the value of the presence sensor and update the VTherm state accordingly
|
||||
Return true if a change has been made"""
|
||||
|
||||
_LOGGER.info("%s - Updating presence. New state is %s", self, new_state)
|
||||
old_presence_state = self._presence_state
|
||||
self._presence_state = (
|
||||
STATE_ON if new_state in (STATE_ON, STATE_HOME) else STATE_OFF
|
||||
)
|
||||
if self._vtherm.preset_mode in HIDDEN_PRESETS or self._is_configured is False:
|
||||
_LOGGER.info(
|
||||
"%s - Ignoring presence change cause in Power or Security preset or presence not configured",
|
||||
self,
|
||||
)
|
||||
return old_presence_state != self._presence_state
|
||||
|
||||
if new_state is None or new_state not in (
|
||||
STATE_OFF,
|
||||
STATE_ON,
|
||||
STATE_HOME,
|
||||
STATE_NOT_HOME,
|
||||
):
|
||||
self._presence_state = STATE_UNKNOWN
|
||||
return old_presence_state != self._presence_state
|
||||
|
||||
if self._vtherm.preset_mode not in [
|
||||
PRESET_BOOST,
|
||||
PRESET_COMFORT,
|
||||
PRESET_ECO,
|
||||
PRESET_ACTIVITY,
|
||||
PRESET_FROST_PROTECTION,
|
||||
]:
|
||||
return old_presence_state != self._presence_state
|
||||
|
||||
new_temp = self._vtherm.find_preset_temp(self._vtherm.preset_mode)
|
||||
if new_temp is not None:
|
||||
_LOGGER.debug(
|
||||
"%s - presence change in temperature mode new_temp will be: %.2f",
|
||||
self,
|
||||
new_temp,
|
||||
)
|
||||
await self._vtherm.change_target_temperature(new_temp)
|
||||
self._vtherm.recalculate()
|
||||
|
||||
return True
|
||||
|
||||
return old_presence_state != self._presence_state
|
||||
|
||||
def add_custom_attributes(self, extra_state_attributes: dict[str, Any]):
|
||||
"""Add some custom attributes"""
|
||||
extra_state_attributes.update(
|
||||
{
|
||||
"presence_sensor_entity_id": self._presence_sensor_entity_id,
|
||||
"presence_state": self._presence_state,
|
||||
"is_presence_configured": self._is_configured,
|
||||
}
|
||||
)
|
||||
|
||||
@overrides
|
||||
@property
|
||||
def is_configured(self) -> bool:
|
||||
"""Return True of the presence is configured"""
|
||||
return self._is_configured
|
||||
|
||||
@property
|
||||
def presence_state(self) -> str | None:
|
||||
"""Return the current presence state STATE_ON or STATE_OFF
|
||||
or STATE_UNAVAILABLE if not configured"""
|
||||
if not self._is_configured:
|
||||
return STATE_UNAVAILABLE
|
||||
return self._presence_state
|
||||
|
||||
@property
|
||||
def is_absence_detected(self) -> bool:
|
||||
"""Return true if the presence is configured and presence sensor is OFF"""
|
||||
return self._is_configured and self._presence_state in [
|
||||
STATE_NOT_HOME,
|
||||
STATE_OFF,
|
||||
]
|
||||
|
||||
@property
|
||||
def presence_sensor_entity_id(self) -> bool:
|
||||
"""Return true if the presence is configured and presence sensor is OFF"""
|
||||
return self._presence_sensor_entity_id
|
||||
|
||||
def __str__(self):
|
||||
return f"PresenceManager-{self.name}"
|
||||
322
custom_components/versatile_thermostat/feature_safety_manager.py
Normal file
322
custom_components/versatile_thermostat/feature_safety_manager.py
Normal file
@@ -0,0 +1,322 @@
|
||||
# pylint: disable=line-too-long
|
||||
|
||||
""" Implements the Safety as a Feature Manager"""
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.const import (
|
||||
STATE_ON,
|
||||
STATE_OFF,
|
||||
STATE_UNAVAILABLE,
|
||||
STATE_UNKNOWN,
|
||||
)
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.components.climate import HVACMode, HVACAction
|
||||
|
||||
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__)
|
||||
|
||||
|
||||
class FeatureSafetyManager(BaseFeatureManager):
|
||||
"""The implementation of the Safety feature"""
|
||||
|
||||
unrecorded_attributes = frozenset(
|
||||
{
|
||||
"safety_delay_min",
|
||||
"safety_min_on_percent",
|
||||
"safety_default_on_percent",
|
||||
"is_safety_configured",
|
||||
}
|
||||
)
|
||||
|
||||
def __init__(self, vtherm: Any, hass: HomeAssistant):
|
||||
"""Init of a featureManager"""
|
||||
super().__init__(vtherm, hass)
|
||||
|
||||
self._is_configured: bool = False
|
||||
self._safety_delay_min = None
|
||||
self._safety_min_on_percent = None
|
||||
self._safety_default_on_percent = None
|
||||
self._safety_state = STATE_UNAVAILABLE
|
||||
|
||||
@overrides
|
||||
def post_init(self, entry_infos: ConfigData):
|
||||
"""Reinit of the manager"""
|
||||
self._safety_delay_min = entry_infos.get(CONF_SAFETY_DELAY_MIN)
|
||||
self._safety_min_on_percent = (
|
||||
entry_infos.get(CONF_SAFETY_MIN_ON_PERCENT)
|
||||
if entry_infos.get(CONF_SAFETY_MIN_ON_PERCENT) is not None
|
||||
else DEFAULT_SAFETY_MIN_ON_PERCENT
|
||||
)
|
||||
self._safety_default_on_percent = (
|
||||
entry_infos.get(CONF_SAFETY_DEFAULT_ON_PERCENT)
|
||||
if entry_infos.get(CONF_SAFETY_DEFAULT_ON_PERCENT) is not None
|
||||
else DEFAULT_SAFETY_DEFAULT_ON_PERCENT
|
||||
)
|
||||
|
||||
if (
|
||||
self._safety_delay_min is not None
|
||||
and self._safety_default_on_percent is not None
|
||||
and self._safety_default_on_percent is not None
|
||||
):
|
||||
self._safety_state = STATE_UNKNOWN
|
||||
self._is_configured = True
|
||||
|
||||
@overrides
|
||||
async def start_listening(self):
|
||||
"""Start listening the underlying entity"""
|
||||
|
||||
@overrides
|
||||
def stop_listening(self):
|
||||
"""Stop listening and remove the eventual timer still running"""
|
||||
|
||||
@overrides
|
||||
async def refresh_state(self) -> bool:
|
||||
"""Check the safety and an eventual action
|
||||
Return True is safety should be active"""
|
||||
|
||||
if not self._is_configured:
|
||||
_LOGGER.debug("%s - safety is disabled (or not configured)", self)
|
||||
return False
|
||||
|
||||
now = self._vtherm.now
|
||||
current_tz = dt_util.get_time_zone(self._hass.config.time_zone)
|
||||
|
||||
is_safety_detected = self.is_safety_detected
|
||||
|
||||
delta_temp = (
|
||||
now - self._vtherm.last_temperature_measure.replace(tzinfo=current_tz)
|
||||
).total_seconds() / 60.0
|
||||
delta_ext_temp = (
|
||||
now - self._vtherm.last_ext_temperature_measure.replace(tzinfo=current_tz)
|
||||
).total_seconds() / 60.0
|
||||
|
||||
mode_cond = self._vtherm.hvac_mode != HVACMode.OFF
|
||||
|
||||
api: VersatileThermostatAPI = VersatileThermostatAPI.get_vtherm_api()
|
||||
is_outdoor_checked = (
|
||||
not api.safety_mode
|
||||
or api.safety_mode.get("check_outdoor_sensor") is not False
|
||||
)
|
||||
|
||||
temp_cond: bool = delta_temp > self._safety_delay_min or (
|
||||
is_outdoor_checked and delta_ext_temp > self._safety_delay_min
|
||||
)
|
||||
climate_cond: bool = (
|
||||
self._vtherm.is_over_climate
|
||||
and self._vtherm.hvac_action
|
||||
not in [
|
||||
HVACAction.COOLING,
|
||||
HVACAction.IDLE,
|
||||
]
|
||||
)
|
||||
switch_cond: bool = (
|
||||
not self._vtherm.is_over_climate
|
||||
and self._vtherm.proportional_algorithm is not None
|
||||
and self._vtherm.proportional_algorithm.calculated_on_percent
|
||||
>= self._safety_min_on_percent
|
||||
)
|
||||
|
||||
_LOGGER.debug(
|
||||
"%s - checking safety delta_temp=%.1f delta_ext_temp=%.1f mod_cond=%s temp_cond=%s climate_cond=%s switch_cond=%s",
|
||||
self,
|
||||
delta_temp,
|
||||
delta_ext_temp,
|
||||
mode_cond,
|
||||
temp_cond,
|
||||
climate_cond,
|
||||
switch_cond,
|
||||
)
|
||||
|
||||
# Issue 99 - a climate is regulated by the device itself and not by VTherm. So a VTherm should never be in safety !
|
||||
should_climate_be_in_security = False # temp_cond and climate_cond
|
||||
should_switch_be_in_security = temp_cond and switch_cond
|
||||
should_be_in_security = (
|
||||
should_climate_be_in_security or should_switch_be_in_security
|
||||
)
|
||||
|
||||
should_start_security = (
|
||||
mode_cond and not is_safety_detected and should_be_in_security
|
||||
)
|
||||
# attr_preset_mode is not necessary normaly. It is just here to be sure
|
||||
should_stop_security = (
|
||||
is_safety_detected
|
||||
and not should_be_in_security
|
||||
and self._vtherm.preset_mode == PRESET_SAFETY
|
||||
)
|
||||
|
||||
# Logging and event
|
||||
if should_start_security:
|
||||
if should_climate_be_in_security:
|
||||
_LOGGER.warning(
|
||||
"%s - No temperature received for more than %.1f minutes (dt=%.1f, dext=%.1f) and underlying climate is %s. Setting it into safety mode",
|
||||
self,
|
||||
self._safety_delay_min,
|
||||
delta_temp,
|
||||
delta_ext_temp,
|
||||
self.hvac_action,
|
||||
)
|
||||
elif should_switch_be_in_security:
|
||||
_LOGGER.warning(
|
||||
"%s - No temperature received for more than %.1f minutes (dt=%.1f, dext=%.1f) and on_percent (%.2f %%) is over defined value (%.2f %%). Set it into safety mode",
|
||||
self,
|
||||
self._safety_delay_min,
|
||||
delta_temp,
|
||||
delta_ext_temp,
|
||||
self._vtherm.proportional_algorithm.on_percent * 100,
|
||||
self._safety_min_on_percent * 100,
|
||||
)
|
||||
|
||||
self._vtherm.send_event(
|
||||
EventType.TEMPERATURE_EVENT,
|
||||
{
|
||||
"last_temperature_measure": self._vtherm.last_temperature_measure.replace(
|
||||
tzinfo=current_tz
|
||||
).isoformat(),
|
||||
"last_ext_temperature_measure": self._vtherm.last_ext_temperature_measure.replace(
|
||||
tzinfo=current_tz
|
||||
).isoformat(),
|
||||
"current_temp": self._vtherm.current_temperature,
|
||||
"current_ext_temp": self._vtherm.current_outdoor_temperature,
|
||||
"target_temp": self._vtherm.target_temperature,
|
||||
},
|
||||
)
|
||||
|
||||
# Start safety mode
|
||||
if should_start_security:
|
||||
self._safety_state = STATE_ON
|
||||
self._vtherm.save_hvac_mode()
|
||||
self._vtherm.save_preset_mode()
|
||||
if self._vtherm.proportional_algorithm:
|
||||
self._vtherm.proportional_algorithm.set_safety(
|
||||
self._safety_default_on_percent
|
||||
)
|
||||
await self._vtherm.async_set_preset_mode_internal(PRESET_SAFETY)
|
||||
# Turn off the underlying climate or heater if safety default on_percent is 0
|
||||
if self._vtherm.is_over_climate or self._safety_default_on_percent <= 0.0:
|
||||
await self._vtherm.async_set_hvac_mode(HVACMode.OFF, False)
|
||||
|
||||
self._vtherm.send_event(
|
||||
EventType.SECURITY_EVENT,
|
||||
{
|
||||
"type": "start",
|
||||
"last_temperature_measure": self._vtherm.last_temperature_measure.replace(
|
||||
tzinfo=current_tz
|
||||
).isoformat(),
|
||||
"last_ext_temperature_measure": self._vtherm.last_ext_temperature_measure.replace(
|
||||
tzinfo=current_tz
|
||||
).isoformat(),
|
||||
"current_temp": self._vtherm.current_temperature,
|
||||
"current_ext_temp": self._vtherm.current_outdoor_temperature,
|
||||
"target_temp": self._vtherm.target_temperature,
|
||||
},
|
||||
)
|
||||
|
||||
# Stop safety mode
|
||||
elif should_stop_security:
|
||||
_LOGGER.warning(
|
||||
"%s - End of safety mode. restoring hvac_mode to %s and preset_mode to %s",
|
||||
self,
|
||||
self._vtherm.saved_hvac_mode,
|
||||
self._vtherm.saved_preset_mode,
|
||||
)
|
||||
self._safety_state = STATE_OFF
|
||||
if self._vtherm.proportional_algorithm:
|
||||
self._vtherm.proportional_algorithm.unset_safety()
|
||||
# Restore hvac_mode if previously saved
|
||||
if self._vtherm.is_over_climate or self._safety_default_on_percent <= 0.0:
|
||||
await self._vtherm.restore_hvac_mode(False)
|
||||
await self._vtherm.restore_preset_mode()
|
||||
self._vtherm.send_event(
|
||||
EventType.SECURITY_EVENT,
|
||||
{
|
||||
"type": "end",
|
||||
"last_temperature_measure": self._vtherm.last_temperature_measure.replace(
|
||||
tzinfo=current_tz
|
||||
).isoformat(),
|
||||
"last_ext_temperature_measure": self._vtherm.last_ext_temperature_measure.replace(
|
||||
tzinfo=current_tz
|
||||
).isoformat(),
|
||||
"current_temp": self._vtherm.current_temperature,
|
||||
"current_ext_temp": self._vtherm.current_outdoor_temperature,
|
||||
"target_temp": self._vtherm.target_temperature,
|
||||
},
|
||||
)
|
||||
|
||||
# Initialize the safety_state if not already done
|
||||
elif not should_be_in_security and self._safety_state in [STATE_UNKNOWN]:
|
||||
self._safety_state = STATE_OFF
|
||||
|
||||
return should_be_in_security
|
||||
|
||||
def add_custom_attributes(self, extra_state_attributes: dict[str, Any]):
|
||||
"""Add some custom attributes"""
|
||||
|
||||
extra_state_attributes.update(
|
||||
{
|
||||
"is_safety_configured": self._is_configured,
|
||||
"safety_state": self._safety_state,
|
||||
}
|
||||
)
|
||||
|
||||
if self._is_configured:
|
||||
extra_state_attributes.update(
|
||||
{
|
||||
"safety_delay_min": self._safety_delay_min,
|
||||
"safety_min_on_percent": self._safety_min_on_percent,
|
||||
"safety_default_on_percent": self._safety_default_on_percent,
|
||||
}
|
||||
)
|
||||
|
||||
@overrides
|
||||
@property
|
||||
def is_configured(self) -> bool:
|
||||
"""Return True of the safety feature is configured"""
|
||||
return self._is_configured
|
||||
|
||||
def set_safety_delay_min(self, safety_delay_min):
|
||||
"""Set the delay min"""
|
||||
self._safety_delay_min = safety_delay_min
|
||||
|
||||
def set_safety_min_on_percent(self, safety_min_on_percent):
|
||||
"""Set the min on percent"""
|
||||
self._safety_min_on_percent = safety_min_on_percent
|
||||
|
||||
def set_safety_default_on_percent(self, safety_default_on_percent):
|
||||
"""Set the default on_percent"""
|
||||
self._safety_default_on_percent = safety_default_on_percent
|
||||
|
||||
@property
|
||||
def is_safety_detected(self) -> bool:
|
||||
"""Returns the is vtherm is in safety mode"""
|
||||
return self._safety_state == STATE_ON
|
||||
|
||||
@property
|
||||
def safety_state(self) -> str:
|
||||
"""Returns the safety state: STATE_ON, STATE_OFF, STATE_UNKWNON, STATE_UNAVAILABLE"""
|
||||
return self._safety_state
|
||||
|
||||
@property
|
||||
def safety_delay_min(self) -> bool:
|
||||
"""Returns the safety delay min"""
|
||||
return self._safety_delay_min
|
||||
|
||||
@property
|
||||
def safety_min_on_percent(self) -> bool:
|
||||
"""Returns the safety min on percent"""
|
||||
return self._safety_min_on_percent
|
||||
|
||||
@property
|
||||
def safety_default_on_percent(self) -> bool:
|
||||
"""Returns the safety safety_default_on_percent"""
|
||||
return self._safety_default_on_percent
|
||||
|
||||
def __str__(self):
|
||||
return f"SafetyManager-{self.name}"
|
||||
555
custom_components/versatile_thermostat/feature_window_manager.py
Normal file
555
custom_components/versatile_thermostat/feature_window_manager.py
Normal file
@@ -0,0 +1,555 @@
|
||||
""" Implements the Window Feature Manager """
|
||||
|
||||
# pylint: disable=line-too-long
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
from datetime import timedelta
|
||||
|
||||
from homeassistant.const import (
|
||||
STATE_ON,
|
||||
STATE_OFF,
|
||||
STATE_UNAVAILABLE,
|
||||
STATE_UNKNOWN,
|
||||
)
|
||||
from homeassistant.core import (
|
||||
HomeAssistant,
|
||||
callback,
|
||||
Event,
|
||||
)
|
||||
from homeassistant.helpers.event import (
|
||||
async_track_state_change_event,
|
||||
EventStateChangedData,
|
||||
async_call_later,
|
||||
)
|
||||
|
||||
from homeassistant.components.climate import HVACMode
|
||||
|
||||
from homeassistant.exceptions import ConditionError
|
||||
from homeassistant.helpers import condition
|
||||
|
||||
from .const import * # pylint: disable=wildcard-import, unused-wildcard-import
|
||||
from .commons import ConfigData
|
||||
|
||||
from .base_manager import BaseFeatureManager
|
||||
from .open_window_algorithm import WindowOpenDetectionAlgorithm
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class FeatureWindowManager(BaseFeatureManager):
|
||||
"""The implementation of the Window feature"""
|
||||
|
||||
unrecorded_attributes = frozenset(
|
||||
{
|
||||
"window_sensor_entity_id",
|
||||
"is_window_configured",
|
||||
"is_window_bypass",
|
||||
"window_delay_sec",
|
||||
"window_off_delay_sec",
|
||||
"window_auto_configured",
|
||||
"window_auto_open_threshold",
|
||||
"window_auto_close_threshold",
|
||||
"window_auto_max_duration",
|
||||
"window_action",
|
||||
}
|
||||
)
|
||||
|
||||
def __init__(self, vtherm: Any, hass: HomeAssistant):
|
||||
"""Init of a featureManager"""
|
||||
super().__init__(vtherm, hass)
|
||||
self._window_sensor_entity_id: str = None
|
||||
self._window_state: str = STATE_UNAVAILABLE
|
||||
self._window_auto_open_threshold: float = 0
|
||||
self._window_auto_close_threshold: float = 0
|
||||
self._window_auto_max_duration: int = 0
|
||||
self._window_auto_state: bool = False
|
||||
self._window_auto_algo: WindowOpenDetectionAlgorithm = None
|
||||
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
|
||||
|
||||
@overrides
|
||||
def post_init(self, entry_infos: ConfigData):
|
||||
"""Reinit of the manager"""
|
||||
self.dearm_window_timer()
|
||||
|
||||
self._window_auto_state = STATE_UNAVAILABLE
|
||||
self._window_state = STATE_UNAVAILABLE
|
||||
|
||||
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
|
||||
)
|
||||
|
||||
self._window_auto_open_threshold = entry_infos.get(
|
||||
CONF_WINDOW_AUTO_OPEN_THRESHOLD
|
||||
)
|
||||
self._window_auto_close_threshold = entry_infos.get(
|
||||
CONF_WINDOW_AUTO_CLOSE_THRESHOLD
|
||||
)
|
||||
self._window_auto_max_duration = entry_infos.get(CONF_WINDOW_AUTO_MAX_DURATION)
|
||||
|
||||
use_window_feature = entry_infos.get(CONF_USE_WINDOW_FEATURE, False)
|
||||
|
||||
if ( # pylint: disable=too-many-boolean-expressions
|
||||
use_window_feature
|
||||
and self._window_sensor_entity_id is None
|
||||
and self._window_auto_open_threshold is not None
|
||||
and self._window_auto_open_threshold > 0.0
|
||||
and self._window_auto_close_threshold is not None
|
||||
and self._window_auto_max_duration is not None
|
||||
and self._window_auto_max_duration > 0
|
||||
and self._window_action is not None
|
||||
):
|
||||
self._is_window_auto_configured = True
|
||||
self._window_auto_state = STATE_UNKNOWN
|
||||
|
||||
self._window_auto_algo = WindowOpenDetectionAlgorithm(
|
||||
alert_threshold=self._window_auto_open_threshold,
|
||||
end_alert_threshold=self._window_auto_close_threshold,
|
||||
)
|
||||
|
||||
if self._is_window_auto_configured or (
|
||||
use_window_feature
|
||||
and self._window_sensor_entity_id is not None
|
||||
and self._window_delay_sec is not None
|
||||
and self._window_action is not None
|
||||
):
|
||||
self._is_configured = True
|
||||
self._window_state = STATE_UNKNOWN
|
||||
|
||||
@overrides
|
||||
async def start_listening(self):
|
||||
"""Start listening the underlying entity"""
|
||||
if self._is_configured:
|
||||
self.stop_listening()
|
||||
if self._window_sensor_entity_id:
|
||||
self.add_listener(
|
||||
async_track_state_change_event(
|
||||
self.hass,
|
||||
[self._window_sensor_entity_id],
|
||||
self._window_sensor_changed,
|
||||
)
|
||||
)
|
||||
|
||||
@overrides
|
||||
def stop_listening(self):
|
||||
"""Stop listening and remove the eventual timer still running"""
|
||||
self.dearm_window_timer()
|
||||
super().stop_listening()
|
||||
|
||||
def dearm_window_timer(self):
|
||||
"""Dearm the eventual motion time running"""
|
||||
if self._window_call_cancel:
|
||||
self._window_call_cancel()
|
||||
self._window_call_cancel = None
|
||||
|
||||
@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 and self._window_sensor_entity_id is not None:
|
||||
|
||||
window_state = self.hass.states.get(self._window_sensor_entity_id)
|
||||
if window_state and window_state.state not in (
|
||||
STATE_UNAVAILABLE,
|
||||
STATE_UNKNOWN,
|
||||
):
|
||||
_LOGGER.debug(
|
||||
"%s - Window state have been retrieved: %s",
|
||||
self,
|
||||
self._window_state,
|
||||
)
|
||||
# recalculate the right target_temp in activity mode
|
||||
ret = await self.update_window_state(window_state.state)
|
||||
|
||||
return ret
|
||||
|
||||
@callback
|
||||
async def _window_sensor_changed(self, event: Event[EventStateChangedData]):
|
||||
"""Handle window sensor changes."""
|
||||
new_state = event.data.get("new_state")
|
||||
old_state = event.data.get("old_state")
|
||||
_LOGGER.info(
|
||||
"%s - Window changed. Event.new_state is %s, _hvac_mode=%s, _saved_hvac_mode=%s",
|
||||
self,
|
||||
new_state,
|
||||
self._vtherm.hvac_mode,
|
||||
self._vtherm.saved_hvac_mode,
|
||||
)
|
||||
|
||||
# Check delay condition
|
||||
async def try_window_condition(_):
|
||||
try:
|
||||
long_enough = condition.state(
|
||||
self._hass,
|
||||
self._window_sensor_entity_id,
|
||||
new_state.state,
|
||||
timedelta(seconds=delay),
|
||||
)
|
||||
except ConditionError:
|
||||
long_enough = False
|
||||
|
||||
if not long_enough:
|
||||
_LOGGER.debug(
|
||||
"Window delay condition is not satisfied. Ignore window event"
|
||||
)
|
||||
self._window_state = old_state.state or STATE_OFF
|
||||
return
|
||||
|
||||
_LOGGER.debug("%s - Window delay condition is satisfied", self)
|
||||
|
||||
if self._window_state == new_state.state:
|
||||
_LOGGER.debug("%s - no change in window state. Forget the event")
|
||||
return
|
||||
|
||||
_LOGGER.debug("%s - Window ByPass is : %s", self, self._is_window_bypass)
|
||||
if self._is_window_bypass:
|
||||
_LOGGER.info(
|
||||
"%s - Window ByPass is activated. Ignore window event", self
|
||||
)
|
||||
# We change tne state but we don't apply the change
|
||||
self._window_state = new_state.state
|
||||
else:
|
||||
await self.update_window_state(new_state.state)
|
||||
|
||||
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=delay), try_window_condition)
|
||||
# For testing purpose we need to access the inner function
|
||||
return try_window_condition
|
||||
|
||||
async def update_window_state(self, new_state: str = None) -> bool:
|
||||
"""Change the window detection state.
|
||||
new_state is on if an open window have been detected or off else
|
||||
return True if the state have changed
|
||||
"""
|
||||
|
||||
if self._window_state == new_state:
|
||||
return False
|
||||
|
||||
if new_state != STATE_ON:
|
||||
_LOGGER.info(
|
||||
"%s - Window is closed. Restoring hvac_mode '%s' if stopped by window detection or temperature %s",
|
||||
self,
|
||||
self._vtherm.saved_hvac_mode,
|
||||
self._vtherm.saved_target_temp,
|
||||
)
|
||||
|
||||
if self._window_action in [
|
||||
CONF_WINDOW_FROST_TEMP,
|
||||
CONF_WINDOW_ECO_TEMP,
|
||||
]:
|
||||
await self._vtherm.restore_target_temp()
|
||||
|
||||
# default to TURN_OFF
|
||||
elif self._window_action in [CONF_WINDOW_TURN_OFF]:
|
||||
if (
|
||||
self._vtherm.last_central_mode != CENTRAL_MODE_STOPPED
|
||||
and self._vtherm.hvac_off_reason == HVAC_OFF_REASON_WINDOW_DETECTION
|
||||
):
|
||||
self._vtherm.set_hvac_off_reason(None)
|
||||
await self._vtherm.restore_hvac_mode(True)
|
||||
elif self._window_action in [CONF_WINDOW_FAN_ONLY]:
|
||||
if self._vtherm.last_central_mode != CENTRAL_MODE_STOPPED:
|
||||
self._vtherm.set_hvac_off_reason(None)
|
||||
await self._vtherm.restore_hvac_mode(True)
|
||||
else:
|
||||
_LOGGER.error(
|
||||
"%s - undefined window_action %s. Please open a bug in the github of this project with this log",
|
||||
self,
|
||||
self._window_action,
|
||||
)
|
||||
return False
|
||||
else:
|
||||
_LOGGER.info(
|
||||
"%s - Window is open. Apply window action %s", self, self._window_action
|
||||
)
|
||||
if self._window_action == CONF_WINDOW_TURN_OFF and not self._vtherm.is_on:
|
||||
_LOGGER.debug(
|
||||
"%s is already off. Forget turning off VTherm due to window detection"
|
||||
)
|
||||
self._window_state = new_state
|
||||
return False
|
||||
|
||||
# self._window_state = new_state
|
||||
if self._vtherm.last_central_mode in [CENTRAL_MODE_AUTO, None]:
|
||||
if self._window_action in [
|
||||
CONF_WINDOW_TURN_OFF,
|
||||
CONF_WINDOW_FAN_ONLY,
|
||||
]:
|
||||
self._vtherm.save_hvac_mode()
|
||||
elif self._window_action in [
|
||||
CONF_WINDOW_FROST_TEMP,
|
||||
CONF_WINDOW_ECO_TEMP,
|
||||
]:
|
||||
self._vtherm.save_target_temp()
|
||||
|
||||
if (
|
||||
self._window_action == CONF_WINDOW_FAN_ONLY
|
||||
and HVACMode.FAN_ONLY in self._vtherm.hvac_modes
|
||||
):
|
||||
await self._vtherm.async_set_hvac_mode(HVACMode.FAN_ONLY)
|
||||
elif (
|
||||
self._window_action == CONF_WINDOW_FROST_TEMP
|
||||
and self._vtherm.is_preset_configured(PRESET_FROST_PROTECTION)
|
||||
):
|
||||
await self._vtherm.change_target_temperature(
|
||||
self._vtherm.find_preset_temp(PRESET_FROST_PROTECTION)
|
||||
)
|
||||
elif (
|
||||
self._window_action == CONF_WINDOW_ECO_TEMP
|
||||
and self._vtherm.is_preset_configured(PRESET_ECO)
|
||||
):
|
||||
await self._vtherm.change_target_temperature(
|
||||
self._vtherm.find_preset_temp(PRESET_ECO)
|
||||
)
|
||||
else: # default is to turn_off
|
||||
self._vtherm.set_hvac_off_reason(HVAC_OFF_REASON_WINDOW_DETECTION)
|
||||
await self._vtherm.async_set_hvac_mode(HVACMode.OFF)
|
||||
|
||||
self._window_state = new_state
|
||||
return True
|
||||
|
||||
async def manage_window_auto(self, in_cycle=False) -> callable:
|
||||
"""The management of the window auto feature
|
||||
Returns the dearm function used to deactivate the window auto"""
|
||||
|
||||
async def dearm_window_auto(_):
|
||||
"""Callback that will be called after end of WINDOW_AUTO_MAX_DURATION"""
|
||||
_LOGGER.info("Unset window auto because MAX_DURATION is exceeded")
|
||||
await deactivate_window_auto(auto=True)
|
||||
|
||||
async def deactivate_window_auto(auto=False):
|
||||
"""Deactivation of the Window auto state"""
|
||||
_LOGGER.warning(
|
||||
"%s - End auto detection of open window slope=%.3f", self, slope
|
||||
)
|
||||
# Send an event
|
||||
cause = "max duration expiration" if auto else "end of slope alert"
|
||||
self._vtherm.send_event(
|
||||
EventType.WINDOW_AUTO_EVENT,
|
||||
{"type": "end", "cause": cause, "curve_slope": slope},
|
||||
)
|
||||
# Set attributes
|
||||
self._window_auto_state = STATE_OFF
|
||||
await self.update_window_state(self._window_auto_state)
|
||||
# await self.restore_hvac_mode(True)
|
||||
|
||||
self.dearm_window_timer()
|
||||
|
||||
if not self._window_auto_algo:
|
||||
return None
|
||||
|
||||
if in_cycle:
|
||||
slope = self._window_auto_algo.check_age_last_measurement(
|
||||
temperature=self._vtherm.ema_temperature,
|
||||
datetime_now=self._vtherm.now,
|
||||
)
|
||||
else:
|
||||
slope = self._window_auto_algo.add_temp_measurement(
|
||||
temperature=self._vtherm.ema_temperature,
|
||||
datetime_measure=self._vtherm.last_temperature_measure,
|
||||
)
|
||||
|
||||
_LOGGER.debug(
|
||||
"%s - Window auto is on, check the alert. last slope is %.3f",
|
||||
self,
|
||||
slope if slope is not None else 0.0,
|
||||
)
|
||||
|
||||
if self.is_window_bypass or not self._is_window_auto_configured:
|
||||
_LOGGER.debug(
|
||||
"%s - Window auto event is ignored because bypass is ON or window auto detection is disabled",
|
||||
self,
|
||||
)
|
||||
return None
|
||||
|
||||
if (
|
||||
self._window_auto_algo.is_window_open_detected()
|
||||
and self._window_auto_state in [STATE_UNKNOWN, STATE_OFF]
|
||||
and self._vtherm.hvac_mode != HVACMode.OFF
|
||||
):
|
||||
if (
|
||||
self._vtherm.proportional_algorithm
|
||||
and self._vtherm.proportional_algorithm.on_percent <= 0.0
|
||||
):
|
||||
_LOGGER.info(
|
||||
"%s - Start auto detection of open window slope=%.3f but no heating detected (on_percent<=0). Forget the event",
|
||||
self,
|
||||
slope,
|
||||
)
|
||||
return dearm_window_auto
|
||||
|
||||
_LOGGER.warning(
|
||||
"%s - Start auto detection of open window slope=%.3f", self, slope
|
||||
)
|
||||
|
||||
# Send an event
|
||||
self._vtherm.send_event(
|
||||
EventType.WINDOW_AUTO_EVENT,
|
||||
{"type": "start", "cause": "slope alert", "curve_slope": slope},
|
||||
)
|
||||
# Set attributes
|
||||
self._window_auto_state = STATE_ON
|
||||
await self.update_window_state(self._window_auto_state)
|
||||
|
||||
# Arm the end trigger
|
||||
self.dearm_window_timer()
|
||||
self._window_call_cancel = async_call_later(
|
||||
self.hass,
|
||||
timedelta(minutes=self._window_auto_max_duration),
|
||||
dearm_window_auto,
|
||||
)
|
||||
|
||||
elif (
|
||||
self._window_auto_algo.is_window_close_detected()
|
||||
and self._window_auto_state == STATE_ON
|
||||
):
|
||||
await deactivate_window_auto(False)
|
||||
|
||||
# For testing purpose we need to return the inner function
|
||||
return dearm_window_auto
|
||||
|
||||
def add_custom_attributes(self, extra_state_attributes: dict[str, Any]):
|
||||
"""Add some custom attributes"""
|
||||
extra_state_attributes.update(
|
||||
{
|
||||
"window_state": self.window_state,
|
||||
"window_auto_state": self.window_auto_state,
|
||||
"window_action": self.window_action,
|
||||
"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,
|
||||
"window_auto_close_threshold": self._window_auto_close_threshold,
|
||||
"window_auto_max_duration": self._window_auto_max_duration,
|
||||
}
|
||||
)
|
||||
|
||||
async def set_window_bypass(self, window_bypass: bool) -> bool:
|
||||
"""Set the window bypass flag
|
||||
Return True if state have been changed"""
|
||||
self._is_window_bypass = window_bypass
|
||||
if not self._is_window_bypass and self._window_state:
|
||||
_LOGGER.info(
|
||||
"%s - Last window state was open & ByPass is now off. Set hvac_mode to '%s'",
|
||||
self,
|
||||
HVACMode.OFF,
|
||||
)
|
||||
self._vtherm.save_hvac_mode()
|
||||
await self._vtherm.async_set_hvac_mode(HVACMode.OFF)
|
||||
return True
|
||||
|
||||
if self._is_window_bypass and self._window_state:
|
||||
_LOGGER.info(
|
||||
"%s - Last window state was open & ByPass is now on. Set hvac_mode to last available mode",
|
||||
self,
|
||||
)
|
||||
await self._vtherm.restore_hvac_mode(True)
|
||||
return True
|
||||
return False
|
||||
|
||||
@overrides
|
||||
@property
|
||||
def is_configured(self) -> bool:
|
||||
"""Return True of the window feature is configured"""
|
||||
return self._is_configured
|
||||
|
||||
@property
|
||||
def is_window_auto_configured(self) -> bool:
|
||||
"""Return True of the window automatic detection is configured"""
|
||||
return self._is_window_auto_configured
|
||||
|
||||
@property
|
||||
def window_state(self) -> str | None:
|
||||
"""Return the current window state STATE_ON or STATE_OFF
|
||||
or STATE_UNAVAILABLE if not configured"""
|
||||
if not self._is_configured:
|
||||
return STATE_UNAVAILABLE
|
||||
return self._window_state
|
||||
|
||||
@property
|
||||
def window_auto_state(self) -> str | None:
|
||||
"""Return the current window auto state STATE_ON or STATE_OFF
|
||||
or STATE_UNAVAILABLE if not configured"""
|
||||
if not self._is_configured:
|
||||
return STATE_UNAVAILABLE
|
||||
return self._window_auto_state
|
||||
|
||||
@property
|
||||
def is_window_bypass(self) -> str | None:
|
||||
"""Return True if the window bypass is activated"""
|
||||
if not self._is_configured:
|
||||
return False
|
||||
return self._is_window_bypass
|
||||
|
||||
@property
|
||||
def is_window_detected(self) -> bool:
|
||||
"""Return true if the presence is configured and presence sensor is OFF"""
|
||||
return self._is_configured and (
|
||||
self._window_state == STATE_ON or self._window_auto_state == STATE_ON
|
||||
)
|
||||
|
||||
@property
|
||||
def window_sensor_entity_id(self) -> bool:
|
||||
"""Return true if the presence is configured and presence sensor is OFF"""
|
||||
return self._window_sensor_entity_id
|
||||
|
||||
@property
|
||||
def window_delay_sec(self) -> bool:
|
||||
"""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"""
|
||||
return self._window_action
|
||||
|
||||
@property
|
||||
def window_auto_open_threshold(self) -> bool:
|
||||
"""Return the window_auto_open_threshold"""
|
||||
return self._window_auto_open_threshold
|
||||
|
||||
@property
|
||||
def window_auto_close_threshold(self) -> bool:
|
||||
"""Return the window_auto_close_threshold"""
|
||||
return self._window_auto_close_threshold
|
||||
|
||||
@property
|
||||
def window_auto_max_duration(self) -> bool:
|
||||
"""Return the window_auto_max_duration"""
|
||||
return self._window_auto_max_duration
|
||||
|
||||
@property
|
||||
def last_slope(self) -> float:
|
||||
"""Return the last slope (in °C/hour)"""
|
||||
if not self._window_auto_algo:
|
||||
return None
|
||||
return self._window_auto_algo.last_slope
|
||||
|
||||
def __str__(self):
|
||||
return f"WindowManager-{self.name}"
|
||||
@@ -14,6 +14,6 @@
|
||||
"quality_scale": "silver",
|
||||
"requirements": [],
|
||||
"ssdp": [],
|
||||
"version": "6.8.0",
|
||||
"version": "7.1.2",
|
||||
"zeroconf": []
|
||||
}
|
||||
@@ -28,7 +28,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.util import slugify
|
||||
|
||||
from .vtherm_api import VersatileThermostatAPI
|
||||
from .commons import VersatileThermostatBaseEntity
|
||||
from .base_entity import VersatileThermostatBaseEntity
|
||||
|
||||
from .const import (
|
||||
DOMAIN,
|
||||
@@ -367,9 +367,6 @@ class CentralConfigTemperatureNumber(
|
||||
@property
|
||||
def native_unit_of_measurement(self) -> str | None:
|
||||
"""The unit of measurement"""
|
||||
# TODO Kelvin ? It seems not because all internal values are stored in
|
||||
# ° Celsius but only the render in front can be in °K depending on the
|
||||
# user configuration.
|
||||
return self.hass.config.units.temperature_unit
|
||||
|
||||
|
||||
|
||||
@@ -187,7 +187,7 @@ class PropAlgorithm:
|
||||
|
||||
self._off_time_sec = self._cycle_min * 60 - self._on_time_sec
|
||||
|
||||
def set_security(self, default_on_percent: float):
|
||||
def set_safety(self, default_on_percent: float):
|
||||
"""Set a default value for on_percent (used for safety mode)"""
|
||||
_LOGGER.info(
|
||||
"%s - Proportional Algo - set security to ON", self._vtherm_entity_id
|
||||
@@ -196,7 +196,7 @@ class PropAlgorithm:
|
||||
self._default_on_percent = default_on_percent
|
||||
self._calculate_internal()
|
||||
|
||||
def unset_security(self):
|
||||
def unset_safety(self):
|
||||
"""Unset the safety mode"""
|
||||
_LOGGER.info(
|
||||
"%s - Proportional Algo - set security to OFF", self._vtherm_entity_id
|
||||
|
||||
@@ -35,7 +35,7 @@ from homeassistant.components.climate import (
|
||||
|
||||
from .base_thermostat import BaseThermostat
|
||||
from .vtherm_api import VersatileThermostatAPI
|
||||
from .commons import VersatileThermostatBaseEntity
|
||||
from .base_entity import VersatileThermostatBaseEntity
|
||||
from .const import (
|
||||
DOMAIN,
|
||||
DEVICE_MANUFACTURER,
|
||||
@@ -165,7 +165,7 @@ class EnergySensor(VersatileThermostatBaseEntity, SensorEntity):
|
||||
if not self.my_climate:
|
||||
return None
|
||||
|
||||
if self.my_climate.device_power > THRESHOLD_WATT_KILO:
|
||||
if self.my_climate.power_manager.device_power > THRESHOLD_WATT_KILO:
|
||||
return UnitOfEnergy.WATT_HOUR
|
||||
else:
|
||||
return UnitOfEnergy.KILO_WATT_HOUR
|
||||
@@ -190,16 +190,17 @@ class MeanPowerSensor(VersatileThermostatBaseEntity, SensorEntity):
|
||||
"""Called when my climate have change"""
|
||||
# _LOGGER.debug("%s - climate state change", self._attr_unique_id)
|
||||
|
||||
if math.isnan(float(self.my_climate.mean_cycle_power)) or math.isinf(
|
||||
self.my_climate.mean_cycle_power
|
||||
):
|
||||
if math.isnan(
|
||||
float(self.my_climate.power_manager.mean_cycle_power)
|
||||
) or math.isinf(self.my_climate.power_manager.mean_cycle_power):
|
||||
raise ValueError(
|
||||
f"Sensor has illegal state {self.my_climate.mean_cycle_power}"
|
||||
f"Sensor has illegal state {self.my_climate.power_manager.mean_cycle_power}"
|
||||
)
|
||||
|
||||
old_state = self._attr_native_value
|
||||
self._attr_native_value = round(
|
||||
self.my_climate.mean_cycle_power, self.suggested_display_precision
|
||||
self.my_climate.power_manager.mean_cycle_power,
|
||||
self.suggested_display_precision,
|
||||
)
|
||||
if old_state != self._attr_native_value:
|
||||
self.async_write_ha_state()
|
||||
@@ -222,7 +223,7 @@ class MeanPowerSensor(VersatileThermostatBaseEntity, SensorEntity):
|
||||
if not self.my_climate:
|
||||
return None
|
||||
|
||||
if self.my_climate.device_power > THRESHOLD_WATT_KILO:
|
||||
if self.my_climate.power_manager.device_power > THRESHOLD_WATT_KILO:
|
||||
return UnitOfPower.WATT
|
||||
else:
|
||||
return UnitOfPower.KILO_WATT
|
||||
@@ -644,6 +645,10 @@ class NbActiveDeviceForBoilerSensor(SensorEntity):
|
||||
"""Representation of the threshold of the number of VTherm
|
||||
which should be active to activate the boiler"""
|
||||
|
||||
_entity_component_unrecorded_attributes = SensorEntity._entity_component_unrecorded_attributes.union( # pylint: disable=protected-access
|
||||
frozenset({"active_device_ids"})
|
||||
)
|
||||
|
||||
def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None:
|
||||
"""Initialize the energy sensor"""
|
||||
self._hass = hass
|
||||
@@ -653,6 +658,14 @@ class NbActiveDeviceForBoilerSensor(SensorEntity):
|
||||
self._attr_unique_id = "nb_device_active_boiler"
|
||||
self._attr_value = self._attr_native_value = None # default value
|
||||
self._entities = []
|
||||
self._attr_active_device_ids = [] # Holds the entity ids of active devices
|
||||
|
||||
@property
|
||||
def extra_state_attributes(self) -> dict:
|
||||
"""Return additional attributes for the sensor."""
|
||||
return {
|
||||
"active_device_ids": self._attr_active_device_ids,
|
||||
}
|
||||
|
||||
@property
|
||||
def icon(self) -> str | None:
|
||||
@@ -718,19 +731,19 @@ class NbActiveDeviceForBoilerSensor(SensorEntity):
|
||||
self.calculate_nb_active_devices,
|
||||
)
|
||||
_LOGGER.info(
|
||||
"%s - the underlyings that could controls the central boiler are %s",
|
||||
"%s - the underlyings that could control the central boiler are %s",
|
||||
self,
|
||||
underlying_entities_id,
|
||||
)
|
||||
self.async_on_remove(listener_cancel)
|
||||
else:
|
||||
_LOGGER.debug("%s - no VTherm could controls the central boiler", self)
|
||||
_LOGGER.debug("%s - no VTherm could control the central boiler", self)
|
||||
|
||||
await self.calculate_nb_active_devices(None)
|
||||
|
||||
async def calculate_nb_active_devices(self, event: Event):
|
||||
"""Calculate the number of active VTherm that have an
|
||||
influence on central boiler"""
|
||||
influence on the central boiler and update the list of active device names."""
|
||||
|
||||
# _LOGGER.debug("%s- calculate_nb_active_devices - the event is %s ", self, event)
|
||||
|
||||
@@ -757,6 +770,8 @@ class NbActiveDeviceForBoilerSensor(SensorEntity):
|
||||
old_state is not None
|
||||
and new_state.state == old_state.state
|
||||
and new_hvac_action == old_hvac_action
|
||||
# issue 698 - force recalculation when underlying climate doesn't have any hvac_action
|
||||
and new_hvac_action is not None
|
||||
):
|
||||
# A false state change
|
||||
return
|
||||
@@ -774,20 +789,28 @@ class NbActiveDeviceForBoilerSensor(SensorEntity):
|
||||
)
|
||||
|
||||
nb_active = 0
|
||||
active_device_ids = []
|
||||
|
||||
for entity in self._entities:
|
||||
nb_active += entity.nb_device_actives
|
||||
device_actives = entity.device_actives
|
||||
_LOGGER.debug(
|
||||
"After examining the hvac_action of %s, nb_active is %s",
|
||||
"After examining the hvac_action of %s, device_actives is %s",
|
||||
entity.name,
|
||||
nb_active,
|
||||
device_actives,
|
||||
)
|
||||
|
||||
nb_active += len(device_actives)
|
||||
active_device_ids.extend(device_actives)
|
||||
|
||||
self._attr_native_value = nb_active
|
||||
_LOGGER.debug(
|
||||
"%s - Number of active underlying entities is %s", self, nb_active
|
||||
)
|
||||
self._attr_active_device_ids = active_device_ids
|
||||
|
||||
self.async_write_ha_state()
|
||||
|
||||
@property
|
||||
def active_device_ids(self) -> list:
|
||||
"""Get the list of active device id"""
|
||||
return self._attr_active_device_ids
|
||||
|
||||
def __str__(self):
|
||||
return f"VersatileThermostat-{self.name}"
|
||||
|
||||
@@ -76,7 +76,7 @@ set_preset_temperature:
|
||||
unit_of_measurement: °
|
||||
mode: slider
|
||||
|
||||
set_security:
|
||||
set_safety:
|
||||
name: Set safety
|
||||
description: Change the safety parameters
|
||||
target:
|
||||
|
||||
@@ -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",
|
||||
@@ -192,16 +194,16 @@
|
||||
"description": "Configuration of advanced parameters. Leave the default values if you don't know what you are doing.\nThese parameters can lead to very poor temperature control or bad power regulation.",
|
||||
"data": {
|
||||
"minimal_activation_delay": "Minimum activation delay",
|
||||
"security_delay_min": "Safety delay (in minutes)",
|
||||
"security_min_on_percent": "Minimum power percent to enable safety mode",
|
||||
"security_default_on_percent": "Power percent to use in safety mode",
|
||||
"safety_delay_min": "Safety delay (in minutes)",
|
||||
"safety_min_on_percent": "Minimum power percent to enable safety mode",
|
||||
"safety_default_on_percent": "Power percent to use in safety mode",
|
||||
"use_advanced_central_config": "Use central advanced configuration"
|
||||
},
|
||||
"data_description": {
|
||||
"minimal_activation_delay": "Delay in seconds under which the equipment will not be activated",
|
||||
"security_delay_min": "Maximum allowed delay in minutes between two temperature measurements. Above this delay the thermostat will turn to a safety off state",
|
||||
"security_min_on_percent": "Minimum heating percent value for safety preset activation. Below this amount of power percent the thermostat won't go into safety preset",
|
||||
"security_default_on_percent": "The default heating power percent value in safety preset. Set to 0 to switch off heater in safety preset",
|
||||
"safety_delay_min": "Maximum allowed delay in minutes between two temperature measurements. Above this delay the thermostat will turn to a safety off state",
|
||||
"safety_min_on_percent": "Minimum heating percent value for safety preset activation. Below this amount of power percent the thermostat won't go into safety preset",
|
||||
"safety_default_on_percent": "The default heating power percent value in safety preset. Set to 0 to switch off heater in safety preset",
|
||||
"use_advanced_central_config": "Check to use the central advanced configuration. Uncheck to use a specific advanced configuration for this VTherm"
|
||||
}
|
||||
},
|
||||
@@ -224,13 +226,15 @@
|
||||
"offset_calibration_entity_ids": "Offset calibration entities",
|
||||
"opening_degree_entity_ids": "Opening degree entities",
|
||||
"closing_degree_entity_ids": "Closing degree entities",
|
||||
"proportional_function": "Algorithm"
|
||||
"proportional_function": "Algorithm",
|
||||
"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",
|
||||
"proportional_function": "Algorithm to use (TPI is the only one for now)"
|
||||
"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": "Opening degree minimum value for each underlying device, comma separated. Default to 0. Example: 20, 25, 30"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -367,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)",
|
||||
@@ -376,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",
|
||||
@@ -436,16 +441,16 @@
|
||||
"description": "Advanced parameters. Leave the default values if you don't know what you are doing.\nThese parameters can lead to very poor temperature control or bad power regulation.",
|
||||
"data": {
|
||||
"minimal_activation_delay": "Minimum activation delay",
|
||||
"security_delay_min": "Safety delay (in minutes)",
|
||||
"security_min_on_percent": "Minimum power percent to enable safety mode",
|
||||
"security_default_on_percent": "Power percent to use in safety mode",
|
||||
"safety_delay_min": "Safety delay (in minutes)",
|
||||
"safety_min_on_percent": "Minimum power percent to enable safety mode",
|
||||
"safety_default_on_percent": "Power percent to use in safety mode",
|
||||
"use_advanced_central_config": "Use central advanced configuration"
|
||||
},
|
||||
"data_description": {
|
||||
"minimal_activation_delay": "Delay in seconds under which the equipment will not be activated",
|
||||
"security_delay_min": "Maximum allowed delay in minutes between two temperature measurements. Above this delay the thermostat will turn to a safety off state",
|
||||
"security_min_on_percent": "Minimum heating percent value for safety preset activation. Below this amount of power percent the thermostat won't go into safety preset",
|
||||
"security_default_on_percent": "The default heating power percent value in safety preset. Set to 0 to switch off heater in safety preset",
|
||||
"safety_delay_min": "Maximum allowed delay in minutes between two temperature measurements. Above this delay the thermostat will turn to a safety off state",
|
||||
"safety_min_on_percent": "Minimum heating percent value for safety preset activation. Below this amount of power percent the thermostat won't go into safety preset",
|
||||
"safety_default_on_percent": "The default heating power percent value in safety preset. Set to 0 to switch off heater in safety preset",
|
||||
"use_advanced_central_config": "Check to use the central advanced configuration. Uncheck to use a specific advanced configuration for this VTherm"
|
||||
}
|
||||
},
|
||||
@@ -468,13 +473,15 @@
|
||||
"offset_calibration_entity_ids": "Offset calibration entities",
|
||||
"opening_degree_entity_ids": "Opening degree entities",
|
||||
"closing_degree_entity_ids": "Closing degree entities",
|
||||
"proportional_function": "Algorithm"
|
||||
"proportional_function": "Algorithm",
|
||||
"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",
|
||||
"proportional_function": "Algorithm to use (TPI is the only one for now)"
|
||||
"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": "Opening degree minimum value for each underlying device, comma separated. Default to 0. Example: 20, 25, 30"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -484,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"
|
||||
|
||||
@@ -12,7 +12,7 @@ from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.helpers.restore_state import RestoreEntity
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from .commons import VersatileThermostatBaseEntity
|
||||
from .base_entity import VersatileThermostatBaseEntity
|
||||
|
||||
from .const import * # pylint: disable=unused-wildcard-import,wildcard-import
|
||||
|
||||
@@ -84,8 +84,13 @@ class AutoStartStopEnable(VersatileThermostatBaseEntity, SwitchEntity, RestoreEn
|
||||
def update_my_state_and_vtherm(self):
|
||||
"""Update the auto_start_stop_enable flag in my VTherm"""
|
||||
self.async_write_ha_state()
|
||||
if self.my_climate is not None:
|
||||
self.my_climate.set_auto_start_stop_enable(self._attr_is_on)
|
||||
if (
|
||||
self.my_climate is not None
|
||||
and self.my_climate.auto_start_stop_manager is not None
|
||||
):
|
||||
self.my_climate.auto_start_stop_manager.set_auto_start_stop_enable(
|
||||
self._attr_is_on
|
||||
)
|
||||
|
||||
@callback
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
|
||||
@@ -24,11 +24,7 @@ from .const import * # pylint: disable=wildcard-import, unused-wildcard-import
|
||||
|
||||
from .vtherm_api import VersatileThermostatAPI
|
||||
from .underlyings import UnderlyingClimate
|
||||
from .auto_start_stop_algorithm import (
|
||||
AutoStartStopDetectionAlgorithm,
|
||||
AUTO_START_STOP_ACTION_OFF,
|
||||
AUTO_START_STOP_ACTION_ON,
|
||||
)
|
||||
from .feature_auto_start_stop_manager import FeatureAutoStartStopManager
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -55,15 +51,9 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
|
||||
"auto_activated_fan_mode",
|
||||
"auto_deactivated_fan_mode",
|
||||
"auto_regulation_use_device_temp",
|
||||
"auto_start_stop_level",
|
||||
"auto_start_stop_dtmin",
|
||||
"auto_start_stop_enable",
|
||||
"auto_start_stop_accumulated_error",
|
||||
"auto_start_stop_accumulated_error_threshold",
|
||||
"auto_start_stop_last_switch_date",
|
||||
"follow_underlying_temp_change",
|
||||
}
|
||||
)
|
||||
).union(FeatureAutoStartStopManager.unrecorded_attributes)
|
||||
)
|
||||
|
||||
def __init__(
|
||||
@@ -83,11 +73,6 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
|
||||
# The fan_mode name depending of the current_mode
|
||||
self._auto_activated_fan_mode: str | None = None
|
||||
self._auto_deactivated_fan_mode: str | None = None
|
||||
self._auto_start_stop_level: TYPE_AUTO_START_STOP_LEVELS = (
|
||||
AUTO_START_STOP_LEVEL_NONE
|
||||
)
|
||||
self._auto_start_stop_algo: AutoStartStopDetectionAlgorithm | None = None
|
||||
self._is_auto_start_stop_enabled: bool = False
|
||||
self._follow_underlying_temp_change: bool = False
|
||||
self._last_regulation_change = None # NowClass.get_now(hass)
|
||||
|
||||
@@ -99,6 +84,12 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
|
||||
def post_init(self, config_entry: ConfigData):
|
||||
"""Initialize the Thermostat"""
|
||||
|
||||
self._auto_start_stop_manager: FeatureAutoStartStopManager = (
|
||||
FeatureAutoStartStopManager(self, self._hass)
|
||||
)
|
||||
|
||||
self.register_manager(self._auto_start_stop_manager)
|
||||
|
||||
super().post_init(config_entry)
|
||||
|
||||
for climate in config_entry.get(CONF_UNDERLYING_LIST):
|
||||
@@ -136,19 +127,6 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
|
||||
CONF_AUTO_REGULATION_USE_DEVICE_TEMP, False
|
||||
)
|
||||
|
||||
use_auto_start_stop = config_entry.get(CONF_USE_AUTO_START_STOP_FEATURE, False)
|
||||
if use_auto_start_stop:
|
||||
self._auto_start_stop_level = config_entry.get(
|
||||
CONF_AUTO_START_STOP_LEVEL, AUTO_START_STOP_LEVEL_NONE
|
||||
)
|
||||
else:
|
||||
self._auto_start_stop_level = AUTO_START_STOP_LEVEL_NONE
|
||||
|
||||
# Instanciate the auto start stop algo
|
||||
self._auto_start_stop_algo = AutoStartStopDetectionAlgorithm(
|
||||
self._auto_start_stop_level, self.name
|
||||
)
|
||||
|
||||
@property
|
||||
def is_over_climate(self) -> bool:
|
||||
"""True if the Thermostat is over_climate"""
|
||||
@@ -178,13 +156,13 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
|
||||
return self.calculate_hvac_action(self._underlyings)
|
||||
|
||||
@overrides
|
||||
async def _async_internal_set_temperature(self, temperature: float):
|
||||
async def change_target_temperature(self, temperature: float):
|
||||
"""Set the target temperature and the target temperature of underlying climate if any"""
|
||||
await super()._async_internal_set_temperature(temperature)
|
||||
await super().change_target_temperature(temperature)
|
||||
|
||||
self._regulation_algo.set_target_temp(self.target_temperature)
|
||||
# is done by control_heating method. No need to do it here
|
||||
# await self._send_regulated_temperature(force=True)
|
||||
# Is necessary cause control_heating method will not force the update.
|
||||
await self._send_regulated_temperature(force=True)
|
||||
|
||||
async def _send_regulated_temperature(self, force=False):
|
||||
"""Sends the regulated temperature to all underlying"""
|
||||
@@ -538,28 +516,6 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
|
||||
self.auto_regulation_use_device_temp
|
||||
)
|
||||
|
||||
self._attr_extra_state_attributes["auto_start_stop_enable"] = (
|
||||
self.auto_start_stop_enable
|
||||
)
|
||||
|
||||
self._attr_extra_state_attributes["auto_start_stop_level"] = (
|
||||
self._auto_start_stop_algo.level
|
||||
)
|
||||
self._attr_extra_state_attributes["auto_start_stop_dtmin"] = (
|
||||
self._auto_start_stop_algo.dt_min
|
||||
)
|
||||
self._attr_extra_state_attributes["auto_start_stop_accumulated_error"] = (
|
||||
self._auto_start_stop_algo.accumulated_error
|
||||
)
|
||||
|
||||
self._attr_extra_state_attributes[
|
||||
"auto_start_stop_accumulated_error_threshold"
|
||||
] = self._auto_start_stop_algo.accumulated_error_threshold
|
||||
|
||||
self._attr_extra_state_attributes["auto_start_stop_last_switch_date"] = (
|
||||
self._auto_start_stop_algo.last_switch_date
|
||||
)
|
||||
|
||||
self._attr_extra_state_attributes["follow_underlying_temp_change"] = (
|
||||
self._follow_underlying_temp_change
|
||||
)
|
||||
@@ -593,7 +549,7 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
|
||||
_LOGGER.info(
|
||||
"%s - Force resent target temp cause we turn on some over climate"
|
||||
)
|
||||
await self._async_internal_set_temperature(self._target_temp)
|
||||
await self.change_target_temperature(self._target_temp)
|
||||
|
||||
@overrides
|
||||
def incremente_energy(self):
|
||||
@@ -602,13 +558,14 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
|
||||
if self.hvac_mode == HVACMode.OFF:
|
||||
return
|
||||
|
||||
device_power = self.power_manager.device_power
|
||||
added_energy = 0
|
||||
if (
|
||||
self.is_over_climate
|
||||
and self._underlying_climate_delta_t is not None
|
||||
and self._device_power
|
||||
and device_power
|
||||
):
|
||||
added_energy = self._device_power * self._underlying_climate_delta_t
|
||||
added_energy = device_power * self._underlying_climate_delta_t
|
||||
|
||||
if self._total_energy is None:
|
||||
self._total_energy = added_energy
|
||||
@@ -709,7 +666,9 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
|
||||
under_temp_diff = (
|
||||
(new_target_temp - last_sent_temperature) if new_target_temp else 0
|
||||
)
|
||||
if -1 < under_temp_diff < 1:
|
||||
|
||||
step = self.target_temperature_step or 1
|
||||
if -step < under_temp_diff < step:
|
||||
under_temp_diff = 0
|
||||
|
||||
# Issue 99 - some AC turn hvac_mode=cool and hvac_action=idle when sending a HVACMode_OFF command
|
||||
@@ -766,7 +725,7 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
|
||||
_LOGGER.debug(
|
||||
"%s - last_change_time=%s old_state_date_changed=%s old_state_date_updated=%s new_state_date_changed=%s new_state_date_updated=%s",
|
||||
self,
|
||||
self._last_change_time,
|
||||
self._last_change_time_from_vtherm,
|
||||
old_state_date_changed,
|
||||
old_state_date_updated,
|
||||
new_state_date_changed,
|
||||
@@ -809,8 +768,10 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
|
||||
# Filter new state when received just after a change from VTherm
|
||||
# Issue #120 - Some TRV are changing target temperature a very long time (6 sec) after the change.
|
||||
# In that case a loop is possible if a user change multiple times during this 6 sec.
|
||||
if new_state_date_updated and self._last_change_time:
|
||||
delta = (new_state_date_updated - self._last_change_time).total_seconds()
|
||||
if new_state_date_updated and self._last_change_time_from_vtherm:
|
||||
delta = (
|
||||
new_state_date_updated - self._last_change_time_from_vtherm
|
||||
).total_seconds()
|
||||
if delta < 10:
|
||||
_LOGGER.info(
|
||||
"%s - underlying event is received less than 10 sec after command. Forget it to avoid loop",
|
||||
@@ -894,90 +855,17 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
|
||||
|
||||
await end_climate_changed(changes)
|
||||
|
||||
async def check_auto_start_stop(self):
|
||||
"""Check the auto-start-stop and an eventual action
|
||||
Return False if we should stop the control_heating method"""
|
||||
slope = (self.last_temperature_slope or 0) / 60 # to have the slope in °/min
|
||||
action = self._auto_start_stop_algo.calculate_action(
|
||||
self.hvac_mode,
|
||||
self._saved_hvac_mode,
|
||||
self.target_temperature,
|
||||
self.current_temperature,
|
||||
slope,
|
||||
self.now,
|
||||
)
|
||||
_LOGGER.debug("%s - auto_start_stop action is %s", self, action)
|
||||
if action == AUTO_START_STOP_ACTION_OFF and self.is_on:
|
||||
_LOGGER.info(
|
||||
"%s - Turning OFF the Vtherm due to auto-start-stop conditions",
|
||||
self,
|
||||
)
|
||||
self.set_hvac_off_reason(HVAC_OFF_REASON_AUTO_START_STOP)
|
||||
await self.async_turn_off()
|
||||
|
||||
# Send an event
|
||||
self.send_event(
|
||||
event_type=EventType.AUTO_START_STOP_EVENT,
|
||||
data={
|
||||
"type": "stop",
|
||||
"name": self.name,
|
||||
"cause": "Auto stop conditions reached",
|
||||
"hvac_mode": self.hvac_mode,
|
||||
"saved_hvac_mode": self._saved_hvac_mode,
|
||||
"target_temperature": self.target_temperature,
|
||||
"current_temperature": self.current_temperature,
|
||||
"temperature_slope": round(slope, 3),
|
||||
"accumulated_error": self._auto_start_stop_algo.accumulated_error,
|
||||
"accumulated_error_threshold": self._auto_start_stop_algo.accumulated_error_threshold,
|
||||
},
|
||||
)
|
||||
|
||||
# Stop here
|
||||
return False
|
||||
elif (
|
||||
action == AUTO_START_STOP_ACTION_ON
|
||||
and self.hvac_off_reason == HVAC_OFF_REASON_AUTO_START_STOP
|
||||
):
|
||||
_LOGGER.info(
|
||||
"%s - Turning ON the Vtherm due to auto-start-stop conditions", self
|
||||
)
|
||||
await self.async_turn_on()
|
||||
|
||||
# Send an event
|
||||
self.send_event(
|
||||
event_type=EventType.AUTO_START_STOP_EVENT,
|
||||
data={
|
||||
"type": "start",
|
||||
"name": self.name,
|
||||
"cause": "Auto start conditions reached",
|
||||
"hvac_mode": self.hvac_mode,
|
||||
"saved_hvac_mode": self._saved_hvac_mode,
|
||||
"target_temperature": self.target_temperature,
|
||||
"current_temperature": self.current_temperature,
|
||||
"temperature_slope": round(slope, 3),
|
||||
"accumulated_error": self._auto_start_stop_algo.accumulated_error,
|
||||
"accumulated_error_threshold": self._auto_start_stop_algo.accumulated_error_threshold,
|
||||
},
|
||||
)
|
||||
|
||||
self.update_custom_attributes()
|
||||
|
||||
return True
|
||||
|
||||
@overrides
|
||||
async def async_control_heating(self, force=False, _=None) -> bool:
|
||||
"""The main function used to run the calculation at each cycle"""
|
||||
ret = await super().async_control_heating(force, _)
|
||||
|
||||
# Check if we need to auto start/stop the Vtherm
|
||||
if self.auto_start_stop_enable:
|
||||
continu = await self.check_auto_start_stop()
|
||||
if not continu:
|
||||
return ret
|
||||
else:
|
||||
_LOGGER.debug("%s - auto start/stop is disabled", self)
|
||||
continu = await self.auto_start_stop_manager.refresh_state()
|
||||
if not continu:
|
||||
return ret
|
||||
|
||||
# Continue the normal async_control_heating
|
||||
# Continue the normal async_control_heating
|
||||
|
||||
# Send the regulated temperature to the underlyings
|
||||
await self._send_regulated_temperature()
|
||||
@@ -987,37 +875,6 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
|
||||
|
||||
return ret
|
||||
|
||||
def set_auto_start_stop_enable(self, is_enabled: bool):
|
||||
"""Enable/Disable the auto-start/stop feature"""
|
||||
self._is_auto_start_stop_enabled = is_enabled
|
||||
if (
|
||||
self.hvac_mode == HVACMode.OFF
|
||||
and self.hvac_off_reason == HVAC_OFF_REASON_AUTO_START_STOP
|
||||
):
|
||||
_LOGGER.debug(
|
||||
"%s - the vtherm is off cause auto-start/stop and enable have been set to false -> starts the VTherm"
|
||||
)
|
||||
self.hass.create_task(self.async_turn_on())
|
||||
|
||||
# Send an event
|
||||
self.send_event(
|
||||
event_type=EventType.AUTO_START_STOP_EVENT,
|
||||
data={
|
||||
"type": "start",
|
||||
"name": self.name,
|
||||
"cause": "Auto start stop disabled",
|
||||
"hvac_mode": self.hvac_mode,
|
||||
"saved_hvac_mode": self._saved_hvac_mode,
|
||||
"target_temperature": self.target_temperature,
|
||||
"current_temperature": self.current_temperature,
|
||||
"temperature_slope": round(self.last_temperature_slope or 0, 3),
|
||||
"accumulated_error": self._auto_start_stop_algo.accumulated_error,
|
||||
"accumulated_error_threshold": self._auto_start_stop_algo.accumulated_error_threshold,
|
||||
},
|
||||
)
|
||||
|
||||
self.update_custom_attributes()
|
||||
|
||||
def set_follow_underlying_temp_change(self, follow: bool):
|
||||
"""Set the flaf follow the underlying temperature changes"""
|
||||
self._follow_underlying_temp_change = follow
|
||||
@@ -1168,21 +1025,16 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
|
||||
return False
|
||||
return True
|
||||
|
||||
@property
|
||||
def auto_start_stop_level(self) -> TYPE_AUTO_START_STOP_LEVELS:
|
||||
"""Return the auto start/stop level."""
|
||||
return self._auto_start_stop_level
|
||||
|
||||
@property
|
||||
def auto_start_stop_enable(self) -> bool:
|
||||
"""Returns the auto_start_stop_enable"""
|
||||
return self._is_auto_start_stop_enabled
|
||||
|
||||
@property
|
||||
def follow_underlying_temp_change(self) -> bool:
|
||||
"""Get the follow underlying temp change flag"""
|
||||
return self._follow_underlying_temp_change
|
||||
|
||||
@property
|
||||
def auto_start_stop_manager(self) -> FeatureAutoStartStopManager:
|
||||
"""Return the auto-start-stop Manager"""
|
||||
return self._auto_start_stop_manager
|
||||
|
||||
@overrides
|
||||
def init_underlyings(self):
|
||||
"""Init the underlyings if not already done"""
|
||||
|
||||
@@ -37,6 +37,7 @@ class ThermostatOverClimateValve(ThermostatOverClimate):
|
||||
"tpi_coef_int",
|
||||
"tpi_coef_ext",
|
||||
"power_percent",
|
||||
"min_opening_degrees",
|
||||
}
|
||||
)
|
||||
)
|
||||
@@ -51,6 +52,7 @@ class ThermostatOverClimateValve(ThermostatOverClimate):
|
||||
self._last_calculation_timestamp: datetime | None = None
|
||||
self._auto_regulation_dpercent: float | None = None
|
||||
self._auto_regulation_period_min: int | None = None
|
||||
self._min_opening_degress: list[int] = []
|
||||
|
||||
super().__init__(hass, unique_id, name, entry_infos)
|
||||
|
||||
@@ -86,6 +88,14 @@ class ThermostatOverClimateValve(ThermostatOverClimate):
|
||||
offset_list = config_entry.get(CONF_OFFSET_CALIBRATION_LIST, [])
|
||||
opening_list = config_entry.get(CONF_OPENING_DEGREE_LIST)
|
||||
closing_list = config_entry.get(CONF_CLOSING_DEGREE_LIST, [])
|
||||
|
||||
self._min_opening_degrees = config_entry.get(CONF_MIN_OPENING_DEGREES, None)
|
||||
min_opening_degrees_list = []
|
||||
if self._min_opening_degrees:
|
||||
min_opening_degrees_list = [
|
||||
int(x.strip()) for x in self._min_opening_degrees.split(",")
|
||||
]
|
||||
|
||||
for idx, _ in enumerate(config_entry.get(CONF_UNDERLYING_LIST)):
|
||||
offset = offset_list[idx] if idx < len(offset_list) else None
|
||||
# number of opening should equal number of underlying
|
||||
@@ -98,6 +108,11 @@ class ThermostatOverClimateValve(ThermostatOverClimate):
|
||||
opening_degree_entity_id=opening,
|
||||
closing_degree_entity_id=closing,
|
||||
climate_underlying=self._underlyings[idx],
|
||||
min_opening_degree=(
|
||||
min_opening_degrees_list[idx]
|
||||
if idx < len(min_opening_degrees_list)
|
||||
else 0
|
||||
),
|
||||
)
|
||||
self._underlyings_valve_regulation.append(under)
|
||||
|
||||
@@ -130,6 +145,10 @@ class ThermostatOverClimateValve(ThermostatOverClimate):
|
||||
self._attr_extra_state_attributes["tpi_coef_int"] = self._tpi_coef_int
|
||||
self._attr_extra_state_attributes["tpi_coef_ext"] = self._tpi_coef_ext
|
||||
|
||||
self._attr_extra_state_attributes["min_opening_degrees"] = (
|
||||
self._min_opening_degrees
|
||||
)
|
||||
|
||||
self._attr_extra_state_attributes["valve_open_percent"] = (
|
||||
self.valve_open_percent
|
||||
)
|
||||
@@ -244,14 +263,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"""
|
||||
@@ -277,12 +288,15 @@ class ThermostatOverClimateValve(ThermostatOverClimate):
|
||||
return self.valve_open_percent > 0
|
||||
|
||||
@property
|
||||
def nb_device_actives(self) -> int:
|
||||
def device_actives(self) -> int:
|
||||
"""Calculate the number of active devices"""
|
||||
if self.is_device_active:
|
||||
return len(self._underlyings_valve_regulation)
|
||||
return [
|
||||
under.opening_degree_entity_id
|
||||
for under in self._underlyings_valve_regulation
|
||||
]
|
||||
else:
|
||||
return 0
|
||||
return []
|
||||
|
||||
@property
|
||||
def activable_underlying_entities(self) -> list | None:
|
||||
|
||||
@@ -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"""
|
||||
@@ -182,8 +172,10 @@ class ThermostatOverSwitch(BaseThermostat[UnderlyingSwitch]):
|
||||
return
|
||||
|
||||
added_energy = 0
|
||||
if not self.is_over_climate and self.mean_cycle_power is not None:
|
||||
added_energy = self.mean_cycle_power * float(self._cycle_min) / 60.0
|
||||
if not self.is_over_climate and self.power_manager.mean_cycle_power is not None:
|
||||
added_energy = (
|
||||
self.power_manager.mean_cycle_power * float(self._cycle_min) / 60.0
|
||||
)
|
||||
|
||||
if self._total_energy is None:
|
||||
self._total_energy = added_energy
|
||||
|
||||
@@ -265,8 +265,10 @@ class ThermostatOverValve(BaseThermostat[UnderlyingValve]): # pylint: disable=a
|
||||
return
|
||||
|
||||
added_energy = 0
|
||||
if not self.is_over_climate and self.mean_cycle_power is not None:
|
||||
added_energy = self.mean_cycle_power * float(self._cycle_min) / 60.0
|
||||
if not self.is_over_climate and self.power_manager.mean_cycle_power is not None:
|
||||
added_energy = (
|
||||
self.power_manager.mean_cycle_power * float(self._cycle_min) / 60.0
|
||||
)
|
||||
|
||||
if self._total_energy is None:
|
||||
self._total_energy = added_energy
|
||||
|
||||
@@ -152,15 +152,15 @@
|
||||
"description": "Διαμόρφωση των προχωρημένων παραμέτρων. Αφήστε τις προεπιλεγμένες τιμές αν δεν γνωρίζετε τι κάνετε.\nΑυτές οι παράμετροι μπορούν να οδηγήσουν σε πολύ κακή ρύθμιση θερμοκρασίας ή ενέργειας.",
|
||||
"data": {
|
||||
"minimal_activation_delay": "Ελάχιστη καθυστέρηση ενεργοποίησης",
|
||||
"security_delay_min": "Καθυστέρηση ασφαλείας (σε λεπτά)",
|
||||
"security_min_on_percent": "Ελάχιστο ποσοστό ισχύος για ενεργοποίηση λειτουργίας ασφαλείας",
|
||||
"security_default_on_percent": "Ποσοστό ισχύος για χρήση σε λειτουργία ασφαλείας"
|
||||
"safety_delay_min": "Καθυστέρηση ασφαλείας (σε λεπτά)",
|
||||
"safety_min_on_percent": "Ελάχιστο ποσοστό ισχύος για ενεργοποίηση λειτουργίας ασφαλείας",
|
||||
"safety_default_on_percent": "Ποσοστό ισχύος για χρήση σε λειτουργία ασφαλείας"
|
||||
},
|
||||
"data_description": {
|
||||
"minimal_activation_delay": "Καθυστέρηση σε δευτερόλεπτα κάτω από την οποία η συσκευή δεν θα ενεργοποιηθεί",
|
||||
"security_delay_min": "Μέγιστη επιτρεπτή καθυστέρηση σε λεπτά μεταξύ δύο μετρήσεων θερμοκρασίας. Πέρα από αυτή την καθυστέρηση, ο θερμοστάτης θα μεταβεί σε κατάσταση ασφαλείας",
|
||||
"security_min_on_percent": "Ελάχιστη τιμή ποσοστού θέρμανσης για την ενεργοποίηση του προεπιλεγμένου ασφάλειας. Κάτω από αυτό το ποσοστό ισχύος το θερμοστάτη δεν θα πάει στο προεπιλεγμένο ασφάλειας.",
|
||||
"security_default_on_percent": "Η προεπιλεγμένη τιμή ποσοστού ισχύος θέρμανσης στο προεπιλεγμένο ασφάλειας. Ορίστε σε 0 για να απενεργοποιήσετε τη θερμάστρα στο παρόν ασφάλειας."
|
||||
"safety_delay_min": "Μέγιστη επιτρεπτή καθυστέρηση σε λεπτά μεταξύ δύο μετρήσεων θερμοκρασίας. Πέρα από αυτή την καθυστέρηση, ο θερμοστάτης θα μεταβεί σε κατάσταση ασφαλείας",
|
||||
"safety_min_on_percent": "Ελάχιστη τιμή ποσοστού θέρμανσης για την ενεργοποίηση του προεπιλεγμένου ασφάλειας. Κάτω από αυτό το ποσοστό ισχύος το θερμοστάτη δεν θα πάει στο προεπιλεγμένο ασφάλειας.",
|
||||
"safety_default_on_percent": "Η προεπιλεγμένη τιμή ποσοστού ισχύος θέρμανσης στο προεπιλεγμένο ασφάλειας. Ορίστε σε 0 για να απενεργοποιήσετε τη θερμάστρα στο παρόν ασφάλειας."
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -325,15 +325,15 @@
|
||||
"description": "Διαμόρφωση των προηγμένων παραμέτρων. Αφήστε τις προεπιλεγμένες τιμές εάν δεν γνωρίζετε τι κάνετε.\nΑυτές οι παράμετροι μπορούν να οδηγήσουν σε πολύ κακή ρύθμιση θερμοκρασίας ή ενέργειας.",
|
||||
"data": {
|
||||
"minimal_activation_delay": "Ελάχιστη καθυστέρηση ενεργοποίησης",
|
||||
"security_delay_min": "Καθυστέρηση ασφαλείας (σε λεπτά)",
|
||||
"security_min_on_percent": "Ελάχιστο ποσοστό ισχύος για τη λειτουργία ασφαλείας",
|
||||
"security_default_on_percent": "Ποσοστό ισχύος που θα χρησιμοποιηθεί στη λειτουργία ασφαλείας"
|
||||
"safety_delay_min": "Καθυστέρηση ασφαλείας (σε λεπτά)",
|
||||
"safety_min_on_percent": "Ελάχιστο ποσοστό ισχύος για τη λειτουργία ασφαλείας",
|
||||
"safety_default_on_percent": "Ποσοστό ισχύος που θα χρησιμοποιηθεί στη λειτουργία ασφαλείας"
|
||||
},
|
||||
"data_description": {
|
||||
"minimal_activation_delay": "Καθυστέρηση σε δευτερόλεπτα κάτω από την οποία ο εξοπλισμός δεν θα ενεργοποιηθεί",
|
||||
"security_delay_min": "Μέγιστη επιτρεπόμενη καθυστέρηση σε λεπτά μεταξύ δύο μετρήσεων θερμοκρασίας. Πάνω από αυτή την καθυστέρηση, ο θερμοστάτης θα μεταβεί σε κατάσταση ασφαλείας",
|
||||
"security_min_on_percent": "Ελάχιστη τιμή ποσοστού θέρμανσης για ενεργοποίηση του προεπιλεγμένου ασφαλείας. Κάτω από αυτό το ποσοστό ισχύος, ο θερμοστάτης δεν θα μεταβεί στο προεπιλεγμένο ασφαλείας",
|
||||
"security_default_on_percent": "Η προεπιλεγμένη τιμή ποσοστού ισχύος θέρμανσης στο προεπιλεγμένο ασφαλείας. Ορίστε σε 0 για να απενεργοποιήσετε τη θερμάστρα στο παρόν ασφαλείας"
|
||||
"safety_delay_min": "Μέγιστη επιτρεπόμενη καθυστέρηση σε λεπτά μεταξύ δύο μετρήσεων θερμοκρασίας. Πάνω από αυτή την καθυστέρηση, ο θερμοστάτης θα μεταβεί σε κατάσταση ασφαλείας",
|
||||
"safety_min_on_percent": "Ελάχιστη τιμή ποσοστού θέρμανσης για ενεργοποίηση του προεπιλεγμένου ασφαλείας. Κάτω από αυτό το ποσοστό ισχύος, ο θερμοστάτης δεν θα μεταβεί στο προεπιλεγμένο ασφαλείας",
|
||||
"safety_default_on_percent": "Η προεπιλεγμένη τιμή ποσοστού ισχύος θέρμανσης στο προεπιλεγμένο ασφαλείας. Ορίστε σε 0 για να απενεργοποιήσετε τη θερμάστρα στο παρόν ασφαλείας"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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",
|
||||
@@ -192,16 +194,16 @@
|
||||
"description": "Configuration of advanced parameters. Leave the default values if you don't know what you are doing.\nThese parameters can lead to very poor temperature control or bad power regulation.",
|
||||
"data": {
|
||||
"minimal_activation_delay": "Minimum activation delay",
|
||||
"security_delay_min": "Safety delay (in minutes)",
|
||||
"security_min_on_percent": "Minimum power percent to enable safety mode",
|
||||
"security_default_on_percent": "Power percent to use in safety mode",
|
||||
"safety_delay_min": "Safety delay (in minutes)",
|
||||
"safety_min_on_percent": "Minimum power percent to enable safety mode",
|
||||
"safety_default_on_percent": "Power percent to use in safety mode",
|
||||
"use_advanced_central_config": "Use central advanced configuration"
|
||||
},
|
||||
"data_description": {
|
||||
"minimal_activation_delay": "Delay in seconds under which the equipment will not be activated",
|
||||
"security_delay_min": "Maximum allowed delay in minutes between two temperature measurements. Above this delay the thermostat will turn to a safety off state",
|
||||
"security_min_on_percent": "Minimum heating percent value for safety preset activation. Below this amount of power percent the thermostat won't go into safety preset",
|
||||
"security_default_on_percent": "The default heating power percent value in safety preset. Set to 0 to switch off heater in safety preset",
|
||||
"safety_delay_min": "Maximum allowed delay in minutes between two temperature measurements. Above this delay the thermostat will turn to a safety off state",
|
||||
"safety_min_on_percent": "Minimum heating percent value for safety preset activation. Below this amount of power percent the thermostat won't go into safety preset",
|
||||
"safety_default_on_percent": "The default heating power percent value in safety preset. Set to 0 to switch off heater in safety preset",
|
||||
"use_advanced_central_config": "Check to use the central advanced configuration. Uncheck to use a specific advanced configuration for this VTherm"
|
||||
}
|
||||
},
|
||||
@@ -224,13 +226,15 @@
|
||||
"offset_calibration_entity_ids": "Offset calibration entities",
|
||||
"opening_degree_entity_ids": "Opening degree entities",
|
||||
"closing_degree_entity_ids": "Closing degree entities",
|
||||
"proportional_function": "Algorithm"
|
||||
"proportional_function": "Algorithm",
|
||||
"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",
|
||||
"proportional_function": "Algorithm to use (TPI is the only one for now)"
|
||||
"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"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -367,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)",
|
||||
@@ -376,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",
|
||||
@@ -436,16 +441,16 @@
|
||||
"description": "Advanced parameters. Leave the default values if you don't know what you are doing.\nThese parameters can lead to very poor temperature control or bad power regulation.",
|
||||
"data": {
|
||||
"minimal_activation_delay": "Minimum activation delay",
|
||||
"security_delay_min": "Safety delay (in minutes)",
|
||||
"security_min_on_percent": "Minimum power percent to enable safety mode",
|
||||
"security_default_on_percent": "Power percent to use in safety mode",
|
||||
"safety_delay_min": "Safety delay (in minutes)",
|
||||
"safety_min_on_percent": "Minimum power percent to enable safety mode",
|
||||
"safety_default_on_percent": "Power percent to use in safety mode",
|
||||
"use_advanced_central_config": "Use central advanced configuration"
|
||||
},
|
||||
"data_description": {
|
||||
"minimal_activation_delay": "Delay in seconds under which the equipment will not be activated",
|
||||
"security_delay_min": "Maximum allowed delay in minutes between two temperature measurements. Above this delay the thermostat will turn to a safety off state",
|
||||
"security_min_on_percent": "Minimum heating percent value for safety preset activation. Below this amount of power percent the thermostat won't go into safety preset",
|
||||
"security_default_on_percent": "The default heating power percent value in safety preset. Set to 0 to switch off heater in safety preset",
|
||||
"safety_delay_min": "Maximum allowed delay in minutes between two temperature measurements. Above this delay the thermostat will turn to a safety off state",
|
||||
"safety_min_on_percent": "Minimum heating percent value for safety preset activation. Below this amount of power percent the thermostat won't go into safety preset",
|
||||
"safety_default_on_percent": "The default heating power percent value in safety preset. Set to 0 to switch off heater in safety preset",
|
||||
"use_advanced_central_config": "Check to use the central advanced configuration. Uncheck to use a specific advanced configuration for this VTherm"
|
||||
}
|
||||
},
|
||||
@@ -468,13 +473,15 @@
|
||||
"offset_calibration_entity_ids": "Offset calibration entities",
|
||||
"opening_degree_entity_ids": "Opening degree entities",
|
||||
"closing_degree_entity_ids": "Closing degree entities",
|
||||
"proportional_function": "Algorithm"
|
||||
"proportional_function": "Algorithm",
|
||||
"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",
|
||||
"proportional_function": "Algorithm to use (TPI is the only one for now)"
|
||||
"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"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -484,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",
|
||||
@@ -192,16 +194,16 @@
|
||||
"description": "Configuration des paramètres avancés. Laissez les valeurs par défaut si vous ne savez pas ce que vous faites.\nCes paramètres peuvent induire des mauvais comportements du thermostat.",
|
||||
"data": {
|
||||
"minimal_activation_delay": "Délai minimal d'activation",
|
||||
"security_delay_min": "Délai maximal entre 2 mesures de températures",
|
||||
"security_min_on_percent": "Pourcentage minimal de puissance",
|
||||
"security_default_on_percent": "Pourcentage de puissance a utiliser en mode securité",
|
||||
"safety_delay_min": "Délai maximal entre 2 mesures de températures",
|
||||
"safety_min_on_percent": "Pourcentage minimal de puissance",
|
||||
"safety_default_on_percent": "Pourcentage de puissance a utiliser en mode securité",
|
||||
"use_advanced_central_config": "Utiliser la configuration centrale avancée"
|
||||
},
|
||||
"data_description": {
|
||||
"minimal_activation_delay": "Délai en seondes en-dessous duquel l'équipement ne sera pas activé",
|
||||
"security_delay_min": "Délai maximal autorisé en minutes entre 2 mesures de températures. Au-dessus de ce délai, le thermostat se mettra en position de sécurité",
|
||||
"security_min_on_percent": "Seuil minimal de pourcentage de chauffage en-dessous duquel le préréglage sécurité ne sera jamais activé",
|
||||
"security_default_on_percent": "Valeur par défaut pour le pourcentage de chauffage en mode sécurité. Mettre 0 pour éteindre le radiateur en mode sécurité",
|
||||
"safety_delay_min": "Délai maximal autorisé en minutes entre 2 mesures de températures. Au-dessus de ce délai, le thermostat se mettra en position de sécurité",
|
||||
"safety_min_on_percent": "Seuil minimal de pourcentage de chauffage en-dessous duquel le préréglage sécurité ne sera jamais activé",
|
||||
"safety_default_on_percent": "Valeur par défaut pour le pourcentage de chauffage en mode sécurité. Mettre 0 pour éteindre le radiateur en mode sécurité",
|
||||
"use_advanced_central_config": "Cochez pour utiliser la configuration centrale avancée. Décochez et saisissez les attributs pour utiliser une configuration spécifique avancée"
|
||||
}
|
||||
},
|
||||
@@ -218,19 +220,21 @@
|
||||
}
|
||||
},
|
||||
"valve_regulation": {
|
||||
"title": "Auto-régulation par vanne - {name}",
|
||||
"title": "Auto-régulation par vanne",
|
||||
"description": "Configuration de l'auto-régulation par controle direct de la vanne",
|
||||
"data": {
|
||||
"offset_calibration_entity_ids": "Entités de 'calibrage du décalage''",
|
||||
"opening_degree_entity_ids": "Entités 'ouverture de vanne'",
|
||||
"closing_degree_entity_ids": "Entités 'fermeture de la vanne'",
|
||||
"proportional_function": "Algorithme"
|
||||
"proportional_function": "Algorithme",
|
||||
"min_opening_degrees": "Ouvertures minimales"
|
||||
},
|
||||
"data_description": {
|
||||
"offset_calibration_entity_ids": "La liste des entités 'calibrage du décalage' (offset calibration). Configurez le si votre TRV possède cette fonction pour une meilleure régulation. Il doit y en avoir une par entité climate sous-jacente",
|
||||
"opening_degree_entity_ids": "La liste des entités 'ouverture de vanne'. Il doit y en avoir une par entité climate sous-jacente",
|
||||
"closing_degree_entity_ids": "La liste des entités 'fermeture de la vanne'. Configurez le si votre TRV possède cette fonction pour une meilleure régulation. Il doit y en avoir une par entité climate sous-jacente",
|
||||
"proportional_function": "Algorithme à utiliser (seulement TPI est disponible)"
|
||||
"proportional_function": "Algorithme à utiliser (seulement TPI est disponible)",
|
||||
"min_opening_degrees": "Une liste séparée par des virgules de minimum d'ouverture. Valeur par défaut : 0. Exemple : 20, 25, 30"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -257,7 +261,7 @@
|
||||
}
|
||||
},
|
||||
"menu": {
|
||||
"title": "Menu - {name}",
|
||||
"title": "Menu",
|
||||
"description": "Paramétrez votre thermostat. Vous pourrez finaliser la configuration quand tous les paramètres auront été saisis.",
|
||||
"menu_options": {
|
||||
"main": "Principaux Attributs",
|
||||
@@ -362,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)",
|
||||
@@ -371,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",
|
||||
@@ -430,16 +436,16 @@
|
||||
"description": "Paramètres avancés. Laissez les valeurs par défaut si vous ne savez pas ce que vous faites.\nCes paramètres peuvent induire des mauvais comportements du thermostat.",
|
||||
"data": {
|
||||
"minimal_activation_delay": "Délai minimal d'activation",
|
||||
"security_delay_min": "Délai maximal entre 2 mesures de températures",
|
||||
"security_min_on_percent": "Pourcentage minimal de puissance",
|
||||
"security_default_on_percent": "Pourcentage de puissance a utiliser en mode securité",
|
||||
"safety_delay_min": "Délai maximal entre 2 mesures de températures",
|
||||
"safety_min_on_percent": "Pourcentage minimal de puissance",
|
||||
"safety_default_on_percent": "Pourcentage de puissance a utiliser en mode securité",
|
||||
"use_advanced_central_config": "Utiliser la configuration centrale avancée"
|
||||
},
|
||||
"data_description": {
|
||||
"minimal_activation_delay": "Délai en seondes en-dessous duquel l'équipement ne sera pas activé",
|
||||
"security_delay_min": "Délai maximal autorisé en minutes entre 2 mesures de températures. Au-dessus de ce délai, le thermostat se mettra en position de sécurité",
|
||||
"security_min_on_percent": "Seuil minimal de pourcentage de chauffage en-dessous duquel le préréglage sécurité ne sera jamais activé",
|
||||
"security_default_on_percent": "Valeur par défaut pour le pourcentage de chauffage en mode sécurité. Mettre 0 pour éteindre le radiateur en mode sécurité",
|
||||
"safety_delay_min": "Délai maximal autorisé en minutes entre 2 mesures de températures. Au-dessus de ce délai, le thermostat se mettra en position de sécurité",
|
||||
"safety_min_on_percent": "Seuil minimal de pourcentage de chauffage en-dessous duquel le préréglage sécurité ne sera jamais activé",
|
||||
"safety_default_on_percent": "Valeur par défaut pour le pourcentage de chauffage en mode sécurité. Mettre 0 pour éteindre le radiateur en mode sécurité",
|
||||
"use_advanced_central_config": "Cochez pour utiliser la configuration centrale avancée. Décochez et saisissez les attributs pour utiliser une configuration spécifique avancée"
|
||||
}
|
||||
},
|
||||
@@ -462,13 +468,15 @@
|
||||
"offset_calibration_entity_ids": "Entités de 'calibrage du décalage''",
|
||||
"opening_degree_entity_ids": "Entités 'ouverture de vanne'",
|
||||
"closing_degree_entity_ids": "Entités 'fermeture de la vanne'",
|
||||
"proportional_function": "Algorithme"
|
||||
"proportional_function": "Algorithme",
|
||||
"min_opening_degrees": "Ouvertures minimales"
|
||||
},
|
||||
"data_description": {
|
||||
"offset_calibration_entity_ids": "La liste des entités 'calibrage du décalage' (offset calibration). Configurez le si votre TRV possède cette fonction pour une meilleure régulation. Il doit y en avoir une par entité climate sous-jacente",
|
||||
"opening_degree_entity_ids": "La liste des entités 'ouverture de vanne'. Il doit y en avoir une par entité climate sous-jacente",
|
||||
"closing_degree_entity_ids": "La liste des entités 'fermeture de la vanne'. Configurez le si votre TRV possède cette fonction pour une meilleure régulation. Il doit y en avoir une par entité climate sous-jacente",
|
||||
"proportional_function": "Algorithme à utiliser (seulement TPI est disponible)"
|
||||
"proportional_function": "Algorithme à utiliser (seulement TPI est disponible)",
|
||||
"min_opening_degrees": "Une liste séparée par des virgules de minimum d'ouverture. Valeur par défaut : 0. Exemple : 20, 25, 30"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -478,7 +486,8 @@
|
||||
"window_open_detection_method": "Une seule méthode de détection des ouvertures ouvertes doit être utilisée. Utilisez le détecteur d'ouverture ou les seuils de température mais pas les deux.",
|
||||
"no_central_config": "Vous ne pouvez pas cocher 'Utiliser la configuration centrale' car aucune configuration centrale n'a été trouvée. Vous devez créer un Versatile Thermostat de type 'Central Configuration' pour pouvoir l'utiliser.",
|
||||
"service_configuration_format": "Mauvais format de la configuration du service",
|
||||
"valve_regulation_nb_entities_incorrect": "Le nombre d'entités pour la régulation par vanne doit être égal au nombre d'entité sous-jacentes"
|
||||
"valve_regulation_nb_entities_incorrect": "Le nombre d'entités pour la régulation par vanne doit être égal au nombre d'entité sous-jacentes",
|
||||
"min_opening_degrees_format": "Une liste d'entiers positifs séparés par des ',' est attendu. Exemple : 20, 25, 30"
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "Le device est déjà configuré"
|
||||
|
||||
@@ -143,15 +143,15 @@
|
||||
"description": "Configurazione avanzata dei parametri. Lasciare i valori predefiniti se non conoscete cosa state modificando.\nQuesti parametri possono determinare una pessima gestione della temperatura e della potenza.",
|
||||
"data": {
|
||||
"minimal_activation_delay": "Ritardo minimo di accensione",
|
||||
"security_delay_min": "Ritardo di sicurezza (in minuti)",
|
||||
"security_min_on_percent": "Percentuale minima di potenza per la modalità di sicurezza",
|
||||
"security_default_on_percent": "Percentuale di potenza per la modalità di sicurezza"
|
||||
"safety_delay_min": "Ritardo di sicurezza (in minuti)",
|
||||
"safety_min_on_percent": "Percentuale minima di potenza per la modalità di sicurezza",
|
||||
"safety_default_on_percent": "Percentuale di potenza per la modalità di sicurezza"
|
||||
},
|
||||
"data_description": {
|
||||
"minimal_activation_delay": "Ritardo in secondi al di sotto del quale l'apparecchiatura non verrà attivata",
|
||||
"security_delay_min": "Ritardo massimo consentito in minuti tra due misure di temperatura. Al di sopra di questo ritardo, il termostato passerà allo stato di sicurezza",
|
||||
"security_min_on_percent": "Soglia percentuale minima di riscaldamento al di sotto della quale il preset di sicurezza non verrà mai attivato",
|
||||
"security_default_on_percent": "Valore percentuale predefinito della potenza di riscaldamento nella modalità di sicurezza. Impostare a 0 per spegnere il riscaldatore nella modalità di sicurezza"
|
||||
"safety_delay_min": "Ritardo massimo consentito in minuti tra due misure di temperatura. Al di sopra di questo ritardo, il termostato passerà allo stato di sicurezza",
|
||||
"safety_min_on_percent": "Soglia percentuale minima di riscaldamento al di sotto della quale il preset di sicurezza non verrà mai attivato",
|
||||
"safety_default_on_percent": "Valore percentuale predefinito della potenza di riscaldamento nella modalità di sicurezza. Impostare a 0 per spegnere il riscaldatore nella modalità di sicurezza"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -307,15 +307,15 @@
|
||||
"description": "Configurazione avanzata dei parametri. Lasciare i valori predefiniti se non conoscete cosa state modificando.\nQuesti parametri possono determinare una pessima gestione della temperatura e della potenza.",
|
||||
"data": {
|
||||
"minimal_activation_delay": "Ritardo minimo di accensione",
|
||||
"security_delay_min": "Ritardo di sicurezza (in minuti)",
|
||||
"security_min_on_percent": "Percentuale minima di potenza per la modalità di sicurezza",
|
||||
"security_default_on_percent": "Percentuale di potenza per la modalità di sicurezza"
|
||||
"safety_delay_min": "Ritardo di sicurezza (in minuti)",
|
||||
"safety_min_on_percent": "Percentuale minima di potenza per la modalità di sicurezza",
|
||||
"safety_default_on_percent": "Percentuale di potenza per la modalità di sicurezza"
|
||||
},
|
||||
"data_description": {
|
||||
"minimal_activation_delay": "Ritardo in secondi al di sotto del quale l'apparecchiatura non verrà attivata",
|
||||
"security_delay_min": "Ritardo massimo consentito in minuti tra due misure di temperatura. Al di sopra di questo ritardo, il termostato passerà allo stato di sicurezza",
|
||||
"security_min_on_percent": "Soglia percentuale minima di riscaldamento al di sotto della quale il preset di sicurezza non verrà mai attivato",
|
||||
"security_default_on_percent": "Valore percentuale predefinito della potenza di riscaldamento nella modalità di sicurezza. Impostare a 0 per spegnere il riscaldatore nella modalità di sicurezza"
|
||||
"safety_delay_min": "Ritardo massimo consentito in minuti tra due misure di temperatura. Al di sopra di questo ritardo, il termostato passerà allo stato di sicurezza",
|
||||
"safety_min_on_percent": "Soglia percentuale minima di riscaldamento al di sotto della quale il preset di sicurezza non verrà mai attivato",
|
||||
"safety_default_on_percent": "Valore percentuale predefinito della potenza di riscaldamento nella modalità di sicurezza. Impostare a 0 per spegnere il riscaldatore nella modalità di sicurezza"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -211,16 +211,16 @@
|
||||
"description": "Konfigurácia pokročilých parametrov. Ak neviete, čo robíte, ponechajte predvolené hodnoty.\nTento parameter môže viesť k veľmi zlej regulácii teploty alebo výkonu.",
|
||||
"data": {
|
||||
"minimal_activation_delay": "Minimálne oneskorenie aktivácie",
|
||||
"security_delay_min": "Bezpečnostné oneskorenie (v minútach)",
|
||||
"security_min_on_percent": "Minimálne percento výkonu na aktiváciu bezpečnostného režimu",
|
||||
"security_default_on_percent": "Percento výkonu na použitie v bezpečnostnom režime",
|
||||
"safety_delay_min": "Bezpečnostné oneskorenie (v minútach)",
|
||||
"safety_min_on_percent": "Minimálne percento výkonu na aktiváciu bezpečnostného režimu",
|
||||
"safety_default_on_percent": "Percento výkonu na použitie v bezpečnostnom režime",
|
||||
"use_advanced_central_config": "Použite centrálnu rozšírenú konfiguráciu"
|
||||
},
|
||||
"data_description": {
|
||||
"minimal_activation_delay": "Oneskorenie v sekundách, pri ktorom sa zariadenie neaktivuje",
|
||||
"security_delay_min": "Maximálne povolené oneskorenie v minútach medzi dvoma meraniami teploty. Po uplynutí tohto oneskorenia sa termostat prepne do bezpečnostného vypnutého stavu",
|
||||
"security_min_on_percent": "Minimálna percentuálna hodnota ohrevu pre aktiváciu prednastavenej bezpečnosti. Pod týmto percentom výkonu termostat neprejde do prednastavenia zabezpečenia",
|
||||
"security_default_on_percent": "Predvolená percentuálna hodnota vykurovacieho výkonu v bezpečnostnej predvoľbe. Nastavte na 0, ak chcete vypnúť ohrievač v zabezpečenom stave",
|
||||
"safety_delay_min": "Maximálne povolené oneskorenie v minútach medzi dvoma meraniami teploty. Po uplynutí tohto oneskorenia sa termostat prepne do bezpečnostného vypnutého stavu",
|
||||
"safety_min_on_percent": "Minimálna percentuálna hodnota ohrevu pre aktiváciu prednastavenej bezpečnosti. Pod týmto percentom výkonu termostat neprejde do prednastavenia zabezpečenia",
|
||||
"safety_default_on_percent": "Predvolená percentuálna hodnota vykurovacieho výkonu v bezpečnostnej predvoľbe. Nastavte na 0, ak chcete vypnúť ohrievač v zabezpečenom stave",
|
||||
"use_advanced_central_config": "Začiarknite, ak chcete použiť centrálnu rozšírenú konfiguráciu. Zrušte začiarknutie, ak chcete použiť špecifickú rozšírenú konfiguráciu pre tento VTherm"
|
||||
}
|
||||
}
|
||||
@@ -446,16 +446,16 @@
|
||||
"description": "Konfigurácia pokročilých parametrov. Ak neviete, čo robíte, ponechajte predvolené hodnoty.\nTento parameter môže viesť k veľmi zlej regulácii teploty alebo výkonu.",
|
||||
"data": {
|
||||
"minimal_activation_delay": "Minimálne oneskorenie aktivácie",
|
||||
"security_delay_min": "Bezpečnostné oneskorenie (v minútach)",
|
||||
"security_min_on_percent": "Minimálne percento výkonu pre bezpečnostný režim",
|
||||
"security_default_on_percent": "Percento výkonu na použitie v bezpečnostnom režime",
|
||||
"safety_delay_min": "Bezpečnostné oneskorenie (v minútach)",
|
||||
"safety_min_on_percent": "Minimálne percento výkonu pre bezpečnostný režim",
|
||||
"safety_default_on_percent": "Percento výkonu na použitie v bezpečnostnom režime",
|
||||
"use_advanced_central_config": "Použite centrálnu rozšírenú konfiguráciu"
|
||||
},
|
||||
"data_description": {
|
||||
"minimal_activation_delay": "Oneskorenie v sekundách, pri ktorom sa zariadenie neaktivuje",
|
||||
"security_delay_min": "Maximálne povolené oneskorenie v minútach medzi dvoma meraniami teploty. Po uplynutí tohto oneskorenia sa termostat prepne do bezpečnostného vypnutého stavu",
|
||||
"security_min_on_percent": "Minimálna percentuálna hodnota ohrevu pre aktiváciu prednastavenej bezpečnosti. Pod týmto percentom výkonu termostat neprejde do prednastavenia zabezpečenia",
|
||||
"security_default_on_percent": "Predvolená percentuálna hodnota vykurovacieho výkonu v bezpečnostnej predvoľbe. Nastavte na 0, ak chcete vypnúť ohrievač v zabezpečenom stave",
|
||||
"safety_delay_min": "Maximálne povolené oneskorenie v minútach medzi dvoma meraniami teploty. Po uplynutí tohto oneskorenia sa termostat prepne do bezpečnostného vypnutého stavu",
|
||||
"safety_min_on_percent": "Minimálna percentuálna hodnota ohrevu pre aktiváciu prednastavenej bezpečnosti. Pod týmto percentom výkonu termostat neprejde do prednastavenia zabezpečenia",
|
||||
"safety_default_on_percent": "Predvolená percentuálna hodnota vykurovacieho výkonu v bezpečnostnej predvoľbe. Nastavte na 0, ak chcete vypnúť ohrievač v zabezpečenom stave",
|
||||
"use_advanced_central_config": "Začiarknite, ak chcete použiť centrálnu rozšírenú konfiguráciu. Zrušte začiarknutie, ak chcete použiť špecifickú rozšírenú konfiguráciu pre tento VTherm"
|
||||
}
|
||||
}
|
||||
@@ -578,4 +578,4 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -190,6 +190,11 @@ 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()
|
||||
|
||||
|
||||
class UnderlyingSwitch(UnderlyingEntity):
|
||||
"""Represent a underlying switch"""
|
||||
@@ -252,7 +257,7 @@ class UnderlyingSwitch(UnderlyingEntity):
|
||||
self._cancel_cycle()
|
||||
|
||||
if self.hvac_mode != hvac_mode:
|
||||
super().set_hvac_mode(hvac_mode)
|
||||
await super().set_hvac_mode(hvac_mode)
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
@@ -409,11 +414,12 @@ class UnderlyingSwitch(UnderlyingEntity):
|
||||
await self.turn_off()
|
||||
return
|
||||
|
||||
if await self._thermostat.check_overpowering():
|
||||
_LOGGER.debug("%s - End of cycle (3)", self)
|
||||
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.check_safety()
|
||||
await self._thermostat.safety_manager.refresh_state()
|
||||
time = self._on_time_sec
|
||||
|
||||
action_label = "start"
|
||||
@@ -626,22 +632,20 @@ 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,
|
||||
}
|
||||
# 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,
|
||||
@@ -1029,6 +1033,7 @@ class UnderlyingValveRegulation(UnderlyingValve):
|
||||
opening_degree_entity_id: str,
|
||||
closing_degree_entity_id: str,
|
||||
climate_underlying: UnderlyingClimate,
|
||||
min_opening_degree: int = 0,
|
||||
) -> None:
|
||||
"""Initialize the underlying TRV with valve regulation"""
|
||||
super().__init__(
|
||||
@@ -1045,6 +1050,7 @@ class UnderlyingValveRegulation(UnderlyingValve):
|
||||
self._max_opening_degree: float = None
|
||||
self._min_offset_calibration: float = None
|
||||
self._max_offset_calibration: float = None
|
||||
self._min_opening_degree: int = min_opening_degree
|
||||
|
||||
async def send_percent_open(self):
|
||||
"""Send the percent open to the underlying valve"""
|
||||
@@ -1078,6 +1084,16 @@ class UnderlyingValveRegulation(UnderlyingValve):
|
||||
)
|
||||
return
|
||||
|
||||
# Caclulate percent_open
|
||||
if self._percent_open >= 1:
|
||||
self._percent_open = round(
|
||||
self._min_opening_degree
|
||||
+ (self._percent_open
|
||||
* (100 - self._min_opening_degree) / 100)
|
||||
)
|
||||
else:
|
||||
self._percent_open = 0
|
||||
|
||||
# Send opening_degree
|
||||
await super().send_percent_open()
|
||||
|
||||
@@ -1138,6 +1154,11 @@ class UnderlyingValveRegulation(UnderlyingValve):
|
||||
"""The offset_calibration_entity_id"""
|
||||
return self._closing_degree_entity_id
|
||||
|
||||
@property
|
||||
def min_opening_degree(self) -> int:
|
||||
"""The minimum opening degree"""
|
||||
return self._min_opening_degree
|
||||
|
||||
@property
|
||||
def have_closing_degree_entity(self) -> bool:
|
||||
"""Return True if the underlying have a closing_degree entity"""
|
||||
|
||||
@@ -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"""
|
||||
@@ -125,15 +135,10 @@ class VersatileThermostatAPI(dict):
|
||||
):
|
||||
"""register the two number entities needed for boiler activation"""
|
||||
self._threshold_number_entity = threshold_number_entity
|
||||
# If sensor and threshold number are initialized, reload the listener
|
||||
# if self._nb_active_number_entity and self._central_boiler_entity:
|
||||
# self._hass.async_add_job(self.reload_central_boiler_binary_listener)
|
||||
|
||||
def register_nb_device_active_boiler(self, nb_active_number_entity):
|
||||
"""register the two number entities needed for boiler activation"""
|
||||
self._nb_active_number_entity = nb_active_number_entity
|
||||
# if self._threshold_number_entity and self._central_boiler_entity:
|
||||
# self._hass.async_add_job(self.reload_central_boiler_binary_listener)
|
||||
|
||||
def register_temperature_number(
|
||||
self,
|
||||
@@ -172,13 +177,6 @@ class VersatileThermostatAPI(dict):
|
||||
)
|
||||
if component:
|
||||
for entity in component.entities:
|
||||
# if hasattr(entity, "init_presets"):
|
||||
# if (
|
||||
# only_use_central is False
|
||||
# or entity.use_central_config_temperature
|
||||
# ):
|
||||
# await entity.init_presets(self.find_central_configuration())
|
||||
|
||||
# A little hack to test if the climate is a VTherm. Cannot use isinstance
|
||||
# due to circular dependency of BaseThermostat
|
||||
if (
|
||||
@@ -188,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
|
||||
@@ -301,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`.
|
||||
|
||||
|
||||
@@ -27,12 +27,12 @@ The first delay (`minimal_activation_delay_sec`) in seconds is the minimum accep
|
||||
|
||||
### Safety Mode
|
||||
|
||||
The second delay (`security_delay_min`) is the maximum time between two temperature measurements before the _VTherm_ switches to Safety Mode.
|
||||
The second delay (`safety_delay_min`) is the maximum time between two temperature measurements before the _VTherm_ switches to Safety Mode.
|
||||
|
||||
The third parameter (`security_min_on_percent`) is the minimum `on_percent` below which Safety Mode will not be activated. This setting prevents activating Safety Mode if the controlled radiator does not heat sufficiently. In this case, there is no physical risk to the home, only the risk of overheating or underheating.
|
||||
The third parameter (`safety_min_on_percent`) is the minimum `on_percent` below which Safety Mode will not be activated. This setting prevents activating Safety Mode if the controlled radiator does not heat sufficiently. In this case, there is no physical risk to the home, only the risk of overheating or underheating.
|
||||
Setting this parameter to `0.00` will trigger Safety Mode regardless of the last heating setting, whereas `1.00` will never trigger Safety Mode (effectively disabling the feature). This can be useful to adapt the safety mechanism to your specific needs.
|
||||
|
||||
The fourth parameter (`security_default_on_percent`) defines the `on_percent` used when the thermostat switches to `security` mode. Setting it to `0` will turn off the thermostat in Safety Mode, while setting it to a value like `0.2` (20%) ensures some heating remains, avoiding a completely frozen home in case of a thermometer failure.
|
||||
The fourth parameter (`safety_default_on_percent`) defines the `on_percent` used when the thermostat switches to `security` mode. Setting it to `0` will turn off the thermostat in Safety Mode, while setting it to a value like `0.2` (20%) ensures some heating remains, avoiding a completely frozen home in case of a thermometer failure.
|
||||
|
||||
It is possible to disable Safety Mode triggered by missing data from the outdoor thermometer. Since the outdoor thermometer usually has a minor impact on regulation (depending on your configuration), it might not be critical if it's unavailable. To do this, add the following lines to your `configuration.yaml`:
|
||||
|
||||
@@ -49,5 +49,5 @@ By default, the outdoor thermometer can trigger Safety Mode if it stops sending
|
||||
> 1. When the temperature sensor resumes reporting, the preset will be restored to its previous value.
|
||||
> 2. Two temperature sources are required: the indoor and outdoor temperatures. Both must report values, or the thermostat will switch to "security" preset.
|
||||
> 3. An action is available to adjust the three safety parameters. This can help adapt Safety Mode to your needs.
|
||||
> 4. For normal use, `security_default_on_percent` should be lower than `security_min_on_percent`.
|
||||
> 4. For normal use, `safety_default_on_percent` should be lower than `safety_min_on_percent`.
|
||||
> 5. If you use the Versatile Thermostat UI card (see [here](additions.md#better-with-the-versatile-thermostat-ui-card)), a _VTherm_ in Safety Mode is indicated by a gray overlay showing the faulty thermometer and the time since its last value update: .
|
||||
@@ -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,50 @@
|
||||
# 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).
|
||||
|
||||
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/1.png
Normal file
BIN
documentation/en/images/1.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 67 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 67 KiB After Width: | Height: | Size: 62 KiB |
File diff suppressed because it is too large
Load Diff
@@ -61,8 +61,8 @@
|
||||
| ``power_temp`` | Temperature during load shedding | X | X | X | X |
|
||||
| ``presence_sensor_entity_id`` | Presence sensor entity id (true if someone is present) | X | X | X | - |
|
||||
| ``minimal_activation_delay`` | Minimum activation delay | X | - | - | X |
|
||||
| ``security_delay_min`` | Maximum delay between two temperature measurements | X | - | X | X |
|
||||
| ``security_min_on_percent`` | Minimum power percentage to enter security mode | X | - | X | X |
|
||||
| ``safety_delay_min`` | Maximum delay between two temperature measurements | X | - | X | X |
|
||||
| ``safety_min_on_percent`` | Minimum power percentage to enter security mode | X | - | X | X |
|
||||
| ``auto_regulation_mode`` | Auto-regulation mode | - | X | - | - |
|
||||
| ``auto_regulation_dtemp`` | Auto-regulation threshold | - | X | - | - |
|
||||
| ``auto_regulation_period_min`` | Minimum auto-regulation period | - | X | - | - |
|
||||
@@ -175,7 +175,7 @@ If the thermostat is in ``security`` mode, the new settings are applied immediat
|
||||
|
||||
To change the security settings, use the following code:
|
||||
```yaml
|
||||
service: versatile_thermostat.set_security
|
||||
service: versatile_thermostat.set_safety
|
||||
data:
|
||||
min_on_percent: "0.5"
|
||||
default_on_percent: "0.1"
|
||||
@@ -204,7 +204,7 @@ The following events are notified:
|
||||
|
||||
- ``versatile_thermostat_security_event``: the thermostat enters or exits the ``security`` preset
|
||||
- ``versatile_thermostat_power_event``: the thermostat enters or exits the ``power`` preset
|
||||
- ``versatile_thermostat_temperature_event``: one or both temperature measurements of the thermostat haven't been updated for more than `security_delay_min`` minutes
|
||||
- ``versatile_thermostat_temperature_event``: one or both temperature measurements of the thermostat haven't been updated for more than `safety_delay_min`` minutes
|
||||
- ``versatile_thermostat_hvac_mode_event``: the thermostat is turned on or off. This event is also broadcast at the thermostat's startup
|
||||
- ``versatile_thermostat_preset_event``: a new preset is selected on the thermostat. This event is also broadcast at the thermostat's startup
|
||||
- ``versatile_thermostat_central_boiler_event``: an event indicating a change in the boiler's state
|
||||
@@ -248,13 +248,13 @@ The custom attributes are as follows:
|
||||
| ``saved_preset_mode`` | The last preset used before automatic preset switching |
|
||||
| ``saved_target_temp`` | The last temperature used before automatic switching |
|
||||
| ``window_state`` | The last known state of the window sensor. None if the window is not configured |
|
||||
| ``window_bypass_state`` | True if the window open detection bypass is enabled |
|
||||
| ``is_window_bypass`` | True if the window open detection bypass is enabled |
|
||||
| ``motion_state`` | The last known state of the motion sensor. None if motion detection is not configured |
|
||||
| ``overpowering_state`` | The last known state of the overpower sensor. None if power management is not configured |
|
||||
| ``presence_state`` | The last known state of the presence sensor. None if presence detection is not configured |
|
||||
| ``security_delay_min`` | The delay before activating security mode when one of the two temperature sensors stops sending measurements |
|
||||
| ``security_min_on_percent`` | The heating percentage below which the thermostat will not switch to security |
|
||||
| ``security_default_on_percent`` | The heating percentage used when the thermostat is in security mode |
|
||||
| ``safety_delay_min`` | The delay before activating security mode when one of the two temperature sensors stops sending measurements |
|
||||
| ``safety_min_on_percent`` | The heating percentage below which the thermostat will not switch to security |
|
||||
| ``safety_default_on_percent`` | The heating percentage used when the thermostat is in security mode |
|
||||
| ``last_temperature_datetime`` | The date and time in ISO8866 format of the last internal temperature reception |
|
||||
| ``last_ext_temperature_datetime`` | The date and time in ISO8866 format of the last external temperature reception |
|
||||
| ``security_state`` | The security state. True or false |
|
||||
@@ -271,5 +271,9 @@ The custom attributes are as follows:
|
||||
| ``auto_start_stop_enable`` | Indicates if the VTherm is allowed to auto start/stop |
|
||||
| ``auto_start_stop_level`` | Indicates the auto start/stop level |
|
||||
| ``hvac_off_reason`` | Indicates the reason for the thermostat's off state (hvac_off). It can be Window, Auto-start/stop, or Manual |
|
||||
| ``last_change_time_from_vtherm`` | The date and time of the last change done by VTherm |
|
||||
| ``nb_device_actives`` | The number of underlying devices seen as active |
|
||||
| ``device_actives`` | The list of underlying devices seen as active |
|
||||
|
||||
|
||||
These attributes will be requested when you need assistance.
|
||||
@@ -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).
|
||||
|
||||
|
||||
@@ -32,11 +32,14 @@ You need to provide:
|
||||
1. As many valve opening control entities as there are underlying devices, and in the same order. These parameters are mandatory.
|
||||
2. As many temperature calibration entities as there are underlying devices, and in the same order. These parameters are optional; they must either all be provided or none.
|
||||
3. As many valve closure control entities as there are underlying devices, and in the same order. These parameters are optional; they must either all be provided or none.
|
||||
4. A list of minimum opening values for the valve when it needs to be opened. This field is a list of integers. If the valve needs to be opened, it will be opened at a minimum of this opening value. This allows enough water to pass through when it needs to be opened.
|
||||
|
||||
The opening rate calculation algorithm is based on the _TPI_ algorithm described [here](algorithms.md). This is the same algorithm used for _VTherms_ `over_switch` and `over_valve`.
|
||||
|
||||
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:
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
- [How to Fix It?](#how-to-fix-it)
|
||||
- [Using a Group of People as a Presence Sensor](#using-a-group-of-people-as-a-presence-sensor)
|
||||
- [Enable Logs for the Versatile Thermostat](#enable-logs-for-the-versatile-thermostat)
|
||||
- [VTherm does not track setpoint changes made directly on the underlying device (`over_climate`)](#vtherm-does-not-track-setpoint-changes-made-directly-on-the-underlying-device-over_climate)
|
||||
|
||||
|
||||
## Using a Heatzy
|
||||
@@ -124,9 +125,9 @@ These parameters are sensitive and quite difficult to adjust. Please only use th
|
||||
|
||||
## Why is my Versatile Thermostat going into Safety Mode?
|
||||
|
||||
Safety mode is only available for VTherm types `over_switch` and `over_valve`. It occurs when one of the two thermometers (providing either the room temperature or the external temperature) has not sent a value for more than `security_delay_min` minutes, and the radiator had been heating at least `security_min_on_percent`. See [safety mode](feature-advanced.md#safety-mode)
|
||||
Safety mode is only available for VTherm types `over_switch` and `over_valve`. It occurs when one of the two thermometers (providing either the room temperature or the external temperature) has not sent a value for more than `safety_delay_min` minutes, and the radiator had been heating at least `safety_min_on_percent`. See [safety mode](feature-advanced.md#safety-mode)
|
||||
|
||||
Since the algorithm relies on temperature measurements, if they are no longer received by the VTherm, there is a risk of overheating and fire. To prevent this, when the above conditions are detected, heating is limited to the `security_default_on_percent` parameter. This value should therefore be reasonably low (10% is a good value). It helps avoid a fire while preventing the radiator from being completely turned off (risk of freezing).
|
||||
Since the algorithm relies on temperature measurements, if they are no longer received by the VTherm, there is a risk of overheating and fire. To prevent this, when the above conditions are detected, heating is limited to the `safety_default_on_percent` parameter. This value should therefore be reasonably low (10% is a good value). It helps avoid a fire while preventing the radiator from being completely turned off (risk of freezing).
|
||||
|
||||
All these parameters are configured on the last page of the VTherm configuration: "Advanced Settings".
|
||||
|
||||
@@ -150,14 +151,14 @@ last_temperature_datetime: "2023-12-06T18:43:28.346010+01:00"
|
||||
last_ext_temperature_datetime: "2023-12-06T13:04:35.164367+01:00"
|
||||
last_update_datetime: "2023-12-06T18:43:28.351103+01:00"
|
||||
...
|
||||
security_delay_min: 60
|
||||
safety_delay_min: 60
|
||||
```
|
||||
|
||||
We can see that:
|
||||
1. The VTherm is indeed in safety mode (`security_state: true`),
|
||||
2. The current time is 06/12/2023 at 18:43:28 (`last_update_datetime: "2023-12-06T18:43:28.351103+01:00"`),
|
||||
3. The last reception time of the room temperature is 06/12/2023 at 18:43:28 (`last_temperature_datetime: "2023-12-06T18:43:28.346010+01:00"`), so it's recent,
|
||||
4. The last reception time of the external temperature is 06/12/2023 at 13:04:35 (`last_ext_temperature_datetime: "2023-12-06T13:04:35.164367+01:00"`). The external temperature is over 5 hours late, which triggered the safety mode, as the threshold is set to 60 minutes (`security_delay_min: 60`).
|
||||
4. The last reception time of the external temperature is 06/12/2023 at 13:04:35 (`last_ext_temperature_datetime: "2023-12-06T13:04:35.164367+01:00"`). The external temperature is over 5 hours late, which triggered the safety mode, as the threshold is set to 60 minutes (`safety_delay_min: 60`).
|
||||
|
||||
### How to Be Notified When This Happens?
|
||||
The VTherm sends an event as soon as this happens and again at the end of the safety alert. You can capture these events in an automation and send a notification, blink a light, trigger a siren, etc. It's up to you.
|
||||
@@ -167,8 +168,8 @@ For handling events generated by VTherm, see [Events](#events).
|
||||
### How to Fix It?
|
||||
It depends on the cause of the problem:
|
||||
1. If a sensor is faulty, it should be repaired (replace batteries, change it, check the weather integration that provides the external temperature, etc.),
|
||||
2. If the `security_delay_min` parameter is too small, it may generate many false alerts. A correct value is around 60 minutes, especially if you have battery-powered temperature sensors. See [my settings](tuning-examples.md#battery-powered-temperature-sensor),
|
||||
3. Some temperature sensors don't send measurements if the temperature hasn't changed. So if the temperature stays very stable for a long time, safety mode can trigger. This is not a big issue since it will deactivate once the VTherm receives a new temperature. On some thermometers (e.g., TuYA or Zigbee), you can force a max delay between two measurements. The max delay should be set to a value lower than `security_delay_min`,
|
||||
2. If the `safety_delay_min` parameter is too small, it may generate many false alerts. A correct value is around 60 minutes, especially if you have battery-powered temperature sensors. See [my settings](tuning-examples.md#battery-powered-temperature-sensor),
|
||||
3. Some temperature sensors don't send measurements if the temperature hasn't changed. So if the temperature stays very stable for a long time, safety mode can trigger. This is not a big issue since it will deactivate once the VTherm receives a new temperature. On some thermometers (e.g., TuYA or Zigbee), you can force a max delay between two measurements. The max delay should be set to a value lower than `safety_delay_min`,
|
||||
4. As soon as the temperature is received again, safety mode will turn off, and the previous preset, target temperature, and mode values will be restored.
|
||||
5. If the external temperature sensor is faulty, you can disable safety mode triggering as it has a minimal impact on the results. To do so, see [here](feature-advanced.md#safety-mode).
|
||||
|
||||
@@ -208,4 +209,8 @@ logs:
|
||||
```
|
||||
You must reload the YAML configuration (Developer Tools / YAML / Reload all YAML configuration) or restart Home Assistant for this change to take effect.
|
||||
|
||||
Be careful, in debug mode, Versatile Thermostat is very verbose and can quickly slow down Home Assistant or saturate your hard drive. If you switch to debug mode for anomaly analysis, do so only for the time needed to reproduce the bug and disable debug mode immediately afterward.
|
||||
Be careful, in debug mode, Versatile Thermostat is very verbose and can quickly slow down Home Assistant or saturate your hard drive. If you switch to debug mode for anomaly analysis, do so only for the time needed to reproduce the bug and disable debug mode immediately afterward.
|
||||
|
||||
## VTherm does not track setpoint changes made directly on the underlying device (`over_climate`)
|
||||
|
||||
See the details of this feature [here](over-climate.md#track-underlying-temperature-changes).
|
||||
@@ -18,9 +18,9 @@
|
||||
## Battery-Powered Temperature Sensor
|
||||
These sensors are often sluggish and do not always send temperature readings when the temperature is stable. Therefore, the settings should be loose to avoid false positives.
|
||||
|
||||
- security_delay_min: 60 minutes (because these sensors are sluggish)
|
||||
- security_min_on_percent: 0.7 (70% - the system goes into security mode if the heater was on more than 70% of the time)
|
||||
- security_default_on_percent: 0.4 (40% - in security mode, we maintain 40% heating time to avoid getting too cold)
|
||||
- safety_delay_min: 60 minutes (because these sensors are sluggish)
|
||||
- safety_min_on_percent: 0.7 (70% - the system goes into security mode if the heater was on more than 70% of the time)
|
||||
- safety_default_on_percent: 0.4 (40% - in security mode, we maintain 40% heating time to avoid getting too cold)
|
||||
|
||||
These settings should be understood as follows:
|
||||
|
||||
@@ -35,9 +35,9 @@ Versatile Thermostat allows you to be notified when such an event occurs. Set up
|
||||
## Reactive Temperature Sensor (plugged in)
|
||||
A powered thermometer is supposed to be very regular in sending temperature readings. If it doesn't send anything for 15 minutes, it most likely has an issue, and we can react faster without the risk of a false positive.
|
||||
|
||||
- security_delay_min: 15 minutes
|
||||
- security_min_on_percent: 0.5 (50% - the system goes into ``security`` preset if the heater was on more than 50% of the time)
|
||||
- security_default_on_percent: 0.25 (25% - in ``security`` preset, we keep 25% heating time)
|
||||
- safety_delay_min: 15 minutes
|
||||
- safety_min_on_percent: 0.5 (50% - the system goes into ``security`` preset if the heater was on more than 50% of the time)
|
||||
- safety_default_on_percent: 0.25 (25% - in ``security`` preset, we keep 25% heating time)
|
||||
|
||||
## My Presets
|
||||
This is just an example of how I use the preset. You can adapt it to your configuration, but it may be useful to understand its functionality.
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -27,12 +27,12 @@ Le premier délai (`minimal_activation_delay_sec`) en secondes est le délai min
|
||||
|
||||
### La mise en sécurité
|
||||
|
||||
Le deuxième délai (`security_delay_min`) est le délai maximal entre deux mesures de température avant de passer le _VTherm_ en mode sécurité.
|
||||
Le deuxième délai (`safety_delay_min`) est le délai maximal entre deux mesures de température avant de passer le _VTherm_ en mode sécurité.
|
||||
|
||||
Le troisième paramètre (`security_min_on_percent`) est la valeur minimal de `on_percent` en dessous de laquelle le préréglage sécurité ne sera pas activé. Ce paramètre permet de ne pas mettre en sécurité un thermostat, si le radiateur piloté ne chauffe pas suffisament. En effet, il n'y a pas de risque physique pour le logement dans ce cas mais juste un risque de surchauffe ou de sous-chauffe.
|
||||
Le troisième paramètre (`safety_min_on_percent`) est la valeur minimal de `on_percent` en dessous de laquelle le préréglage sécurité ne sera pas activé. Ce paramètre permet de ne pas mettre en sécurité un thermostat, si le radiateur piloté ne chauffe pas suffisament. En effet, il n'y a pas de risque physique pour le logement dans ce cas mais juste un risque de surchauffe ou de sous-chauffe.
|
||||
Mettre ce paramètre à ``0.00`` déclenchera le préréglage sécurité quelque soit la dernière consigne de chauffage, à l'inverse ``1.00`` ne déclenchera jamais le préréglage sécurité ( ce qui revient à désactiver la fonction). Ce peut ê
|
||||
|
||||
Le quatrième paramètre (`security_default_on_percent`) est la valeur de `on_percent` qui sera utilisée lorsque le thermostat passe en mode ``security``. Si vous mettez `0` alors le thermostat sera coupé lorsqu'il passe en mode `security`, mettre 0,2% par exemple permet de garder un peu de chauffage (20% dans ce cas), même en mode ``security``. Ca évite de retrouver son logement totalement gelé lors d'une panne de thermomètre.
|
||||
Le quatrième paramètre (`safety_default_on_percent`) est la valeur de `on_percent` qui sera utilisée lorsque le thermostat passe en mode ``security``. Si vous mettez `0` alors le thermostat sera coupé lorsqu'il passe en mode `security`, mettre 0,2% par exemple permet de garder un peu de chauffage (20% dans ce cas), même en mode ``security``. Ca évite de retrouver son logement totalement gelé lors d'une panne de thermomètre.
|
||||
|
||||
Il est possible de désactiver la mise en sécurité suite à une absence de données du thermomètre extérieure. En effet, celui-ci ayant la plupart du temps un impact faible sur la régulation (dépendant de votre paramètrage), il est possible qu'il soit absent sans mettre en danger le logement. Pour cela, il faut ajouter les lignes suivantes dans votre `configuration.yaml` :
|
||||
```yaml
|
||||
@@ -47,5 +47,5 @@ Par défaut, le thermomètre extérieur peut déclencher une mise en sécurité
|
||||
> 1. Lorsque le capteur de température viendra à la vie et renverra les températures, le préréglage sera restauré à sa valeur précédente,
|
||||
> 2. Attention, deux températures sont nécessaires : la température interne et la température externe et chacune doit donner la température, sinon le thermostat sera en préréglage "security",
|
||||
> 3. Une action est disponible qui permet de régler les 3 paramètres de sécurité. Ca peut servir à adapter la fonction de sécurité à votre usage,
|
||||
> 4. Pour un usage naturel, le ``security_default_on_percent`` doit être inférieur à ``security_min_on_percent``,
|
||||
> 4. Pour un usage naturel, le ``safety_default_on_percent`` doit être inférieur à ``safety_min_on_percent``,
|
||||
> 5. Si vous utilisez la carte Verstatile Thermostat UI (cf. [ici](additions.md#bien-mieux-avec-le-versatile-thermostat-ui-card)), un _Vtherm_ en mode sécurité est signalé par un voile grisatre qui donne le thermomètre en défaut et depuis combien de temps le thermomètre n'a pas remonté de valeur : .
|
||||
|
||||
@@ -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,12 @@
|
||||
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),
|
||||
|
||||
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 +27,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 +38,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
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 71 KiB After Width: | Height: | Size: 66 KiB |
File diff suppressed because it is too large
Load Diff
@@ -39,7 +39,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.
|
||||
|
||||
|
||||
@@ -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é.
|
||||
|
||||
@@ -61,8 +61,8 @@
|
||||
| ``power_temp`` | Température si délestaqe | X | X | X | X |
|
||||
| ``presence_sensor_entity_id`` | Capteur de présence entity id (true si quelqu'un est présent) | X | X | X | - |
|
||||
| ``minimal_activation_delay`` | Délai minimal d'activation | X | - | - | X |
|
||||
| ``security_delay_min`` | Délai maximal entre 2 mesures de températures | X | - | X | X |
|
||||
| ``security_min_on_percent`` | Pourcentage minimal de puissance pour passer en mode sécurité | X | - | X | X |
|
||||
| ``safety_delay_min`` | Délai maximal entre 2 mesures de températures | X | - | X | X |
|
||||
| ``safety_min_on_percent`` | Pourcentage minimal de puissance pour passer en mode sécurité | X | - | X | X |
|
||||
| ``auto_regulation_mode`` | Le mode d'auto-régulation | - | X | - | - |
|
||||
| ``auto_regulation_dtemp`` | La seuil d'auto-régulation | - | X | - | - |
|
||||
| ``auto_regulation_period_min`` | La période minimale d'auto-régulation | - | X | - | - |
|
||||
@@ -173,7 +173,7 @@ Si le thermostat est en mode ``security`` les nouveaux paramètres sont appliqu
|
||||
|
||||
Pour changer les paramètres de sécurité utilisez le code suivant :
|
||||
```yaml
|
||||
service : versatile_thermostat.set_security
|
||||
service : versatile_thermostat.set_safety
|
||||
data:
|
||||
min_on_percent: "0.5"
|
||||
default_on_percent: "0.1"
|
||||
@@ -202,7 +202,7 @@ Les évènements notifiés sont les suivants:
|
||||
|
||||
- ``versatile_thermostat_security_event`` : un thermostat entre ou sort du preset ``security``
|
||||
- ``versatile_thermostat_power_event`` : un thermostat entre ou sort du preset ``power``
|
||||
- ``versatile_thermostat_temperature_event`` : une ou les deux mesures de température d'un thermostat n'ont pas été mis à jour depuis plus de `security_delay_min`` minutes
|
||||
- ``versatile_thermostat_temperature_event`` : une ou les deux mesures de température d'un thermostat n'ont pas été mis à jour depuis plus de `safety_delay_min`` minutes
|
||||
- ``versatile_thermostat_hvac_mode_event`` : le thermostat est allumé ou éteint. Cet évènement est aussi diffusé au démarrage du thermostat
|
||||
- ``versatile_thermostat_preset_event`` : un nouveau preset est sélectionné sur le thermostat. Cet évènement est aussi diffusé au démarrage du thermostat
|
||||
- ``versatile_thermostat_central_boiler_event`` : un évènement indiquant un changement dans l'état de la chaudière.
|
||||
@@ -247,13 +247,13 @@ Les attributs personnalisés sont les suivants :
|
||||
| ``saved_preset_mode`` | Le dernier preset utilisé avant le basculement automatique du preset |
|
||||
| ``saved_target_temp`` | La dernière température utilisée avant la commutation automatique |
|
||||
| ``window_state`` | Le dernier état connu du capteur de fenêtre. Aucun si la fenêtre n'est pas configurée |
|
||||
| ``window_bypass_state`` | True si le bypass de la détection d'ouverture et activé |
|
||||
| ``is_window_bypass`` | True si le bypass de la détection d'ouverture et activé |
|
||||
| ``motion_state`` | Le dernier état connu du capteur de mouvement. Aucun si le mouvement n'est pas configuré |
|
||||
| ``overpowering_state`` | Le dernier état connu du capteur surpuissant. Aucun si la gestion de l'alimentation n'est pas configurée |
|
||||
| ``presence_state`` | Le dernier état connu du capteur de présence. Aucun si la gestion de présence n'est pas configurée |
|
||||
| ``security_delay_min`` | Le délai avant d'activer le mode de sécurité lorsque un des 2 capteurs de température n'envoie plus de mesures |
|
||||
| ``security_min_on_percent`` | Pourcentage de chauffe en dessous duquel le thermostat ne passera pas en sécurité |
|
||||
| ``security_default_on_percent`` | Pourcentage de chauffe utilisé lorsque le thermostat est en sécurité |
|
||||
| ``safety_delay_min`` | Le délai avant d'activer le mode de sécurité lorsque un des 2 capteurs de température n'envoie plus de mesures |
|
||||
| ``safety_min_on_percent`` | Pourcentage de chauffe en dessous duquel le thermostat ne passera pas en sécurité |
|
||||
| ``safety_default_on_percent`` | Pourcentage de chauffe utilisé lorsque le thermostat est en sécurité |
|
||||
| ``last_temperature_datetime`` | La date et l'heure au format ISO8866 de la dernière réception de température interne |
|
||||
| ``last_ext_temperature_datetime`` | La date et l'heure au format ISO8866 de la dernière réception de température extérieure |
|
||||
| ``security_state`` | L'état de sécurité. vrai ou faux |
|
||||
@@ -270,5 +270,8 @@ Les attributs personnalisés sont les suivants :
|
||||
| ``auto_start_stop_enable`` | Indique si le VTherm est autorisé à s'auto démarrer/arrêter |
|
||||
| ``auto_start_stop_level`` | Indique le niveau d'auto start/stop |
|
||||
| ``hvac_off_reason`` | Indique la raison de l'arrêt (hvac_off) du VTherm. Ce peut être Window, Auto-start/stop ou Manuel |
|
||||
| ``last_change_time_from_vtherm`` | La date/heure du dernier changement fait par VTherm |
|
||||
| ``nb_device_actives`` | Le nombre de devices sous-jacents actuellement vus comme actifs |
|
||||
| ``device_actives`` | La liste des devices sous-jacents actuellement vus comme actifs |
|
||||
|
||||
Ces attributs vous seront demandés lors d'une demande d'aide.
|
||||
|
||||
@@ -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).
|
||||
|
||||
|
||||
@@ -32,12 +32,15 @@ Elle permet de configurer les entités de contrôle de la vanne :
|
||||
Vous devez donner :
|
||||
1. autant d'entités de contrôle d'ouverture de la vanne qu'il y a de sous-jacents et dans le même odre. Ces paramètres sont obligatoires,
|
||||
2. autant d'entités de calibrage du décalage de température qu'il y a de sous-jacents et dans le même ordre. Ces paramètres sont facultatifs ; ils doivent être tous founis ou aucun,
|
||||
3. autant d'entités de de contrôile du taux de fermture qu'il y a de sous-jacents et dans le même ordre. Ces paramètres sont facultatifs ; ils doivent être tous founis ou aucun
|
||||
3. autant d'entités de de contrôile du taux de fermture qu'il y a de sous-jacents et dans le même ordre. Ces paramètres sont facultatifs ; ils doivent être tous founis ou aucun,
|
||||
4. une liste de valeurs minimales d'ouverture de la vanne lorsqu'elle doit être ouverte. Ce champ est une liste d'entier. Si la vanne doit être ouverte, elle le sera au minimum avec cette valeur d'ouverture. Cela permet de laisser passer suffisamment d'eau lorsqu'elle doit être ouverte.
|
||||
|
||||
L'algorithme de calcul du taux d'ouverture est basé sur le _TPI_ qui est décrit [ici](algorithms.md). C'est le même algorithme qui est utilisé pour les _VTherm_ `over_switch` et `over_valve`.
|
||||
|
||||
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 :
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
- [Comment réparer ?](#comment-réparer-)
|
||||
- [Utilisation d'un groupe de personnes comme capteur de présence](#utilisation-dun-groupe-de-personnes-comme-capteur-de-présence)
|
||||
- [Activer les logs du Versatile Thermostat](#activer-les-logs-du-versatile-thermostat)
|
||||
- [VTherm ne suit pas les changements de consigne faits directement depuis le sous-jacents (`over_climate`)](#vtherm-ne-suit-pas-les-changements-de-consigne-faits-directement-depuis-le-sous-jacents-over_climate)
|
||||
|
||||
|
||||
## Utilisation d'un Heatzy
|
||||
@@ -122,9 +123,9 @@ versatile_thermostat:
|
||||
Ces paramètres sont sensibles et assez difficiles à régler. Merci de ne les utiliser que si vous savez ce que vous faites et que vos mesures de température ne sont pas déjà lisses.
|
||||
|
||||
## Pourquoi mon Versatile Thermostat se met en Securite ?
|
||||
Le mode sécurité est possible sur les types de VTherm de type `over_switch` et `over_valve` uniquement. Il survient lorsqu'un des 2 thermomètres qui donne la température de la pièce ou la température extérieure n'a pas envoyé de valeur depuis plus de `security_delay_min` minutes et que le radiateur chauffait à au moins `security_min_on_percent`. Cf. [mode sécurité](feature-advanced.md#la-mise-en-sécurité)
|
||||
Le mode sécurité est possible sur les types de VTherm de type `over_switch` et `over_valve` uniquement. Il survient lorsqu'un des 2 thermomètres qui donne la température de la pièce ou la température extérieure n'a pas envoyé de valeur depuis plus de `safety_delay_min` minutes et que le radiateur chauffait à au moins `safety_min_on_percent`. Cf. [mode sécurité](feature-advanced.md#la-mise-en-sécurité)
|
||||
|
||||
Comme l'algorithme est basé sur les mesures de température, si elles ne sont plus reçues par le VTherm, il y a un risque de surchauffe et d'incendie. Pour éviter ça, lorsque les conditions rappelées ci-dessus sont détectées, la chauffe est limité au paramètre `security_default_on_percent`. Cette valeur doit donc être raisonnablement faible (10% est une bonne valeur). Elle permet d'éviter un incendie tout en évitant de couper totalement le radiateur (risque de gel).
|
||||
Comme l'algorithme est basé sur les mesures de température, si elles ne sont plus reçues par le VTherm, il y a un risque de surchauffe et d'incendie. Pour éviter ça, lorsque les conditions rappelées ci-dessus sont détectées, la chauffe est limité au paramètre `safety_default_on_percent`. Cette valeur doit donc être raisonnablement faible (10% est une bonne valeur). Elle permet d'éviter un incendie tout en évitant de couper totalement le radiateur (risque de gel).
|
||||
|
||||
Tous ces paramètres se règlent dans la dernière page de la configuration du VTherm : "Paramètres avancés".
|
||||
|
||||
@@ -148,14 +149,14 @@ last_temperature_datetime: "2023-12-06T18:43:28.346010+01:00"
|
||||
last_ext_temperature_datetime: "2023-12-06T13:04:35.164367+01:00"
|
||||
last_update_datetime: "2023-12-06T18:43:28.351103+01:00"
|
||||
...
|
||||
security_delay_min: 60
|
||||
safety_delay_min: 60
|
||||
```
|
||||
|
||||
On voit que :
|
||||
1. le VTherm est bien en mode sécurité (`security_state: true`),
|
||||
2. l'heure courante est le 06/12/2023 à 18h43:28 (`last_update_datetime: "2023-12-06T18:43:28.351103+01:00"`),
|
||||
3. l'heure de dernière réception de la température intérieure est le 06/12/2023 à 18h43:28 (`last_temperature_datetime: "2023-12-06T18:43:28.346010+01:00"`). Elle est donc récente,
|
||||
4. l'heure de dernière réception de la température extérieure est le 06/12/2023 à 13h04:35 (`last_ext_temperature_datetime: "2023-12-06T13:04:35.164367+01:00`). C'est donc l'heure extérieure qui a plus de 5 h de retard et qui a provoquée le passage en mode sécurité, car le seuil est limité à 60 min (`security_delay_min: 60`).
|
||||
4. l'heure de dernière réception de la température extérieure est le 06/12/2023 à 13h04:35 (`last_ext_temperature_datetime: "2023-12-06T13:04:35.164367+01:00`). C'est donc l'heure extérieure qui a plus de 5 h de retard et qui a provoquée le passage en mode sécurité, car le seuil est limité à 60 min (`safety_delay_min: 60`).
|
||||
|
||||
### Comment être averti lorsque cela se produit ?
|
||||
Pour être averti, le VTherm envoie un évènement dès que ça se produit et un en fin d'alerte sécurité. Vous pouvez capter ces évènements dans une automatisation et envoyer une notification par exemple, faire clignoter un voyant, déclencher une sirène, ... A vous de voir.
|
||||
@@ -165,8 +166,8 @@ Pour manipuler les évènements générés par le VTherm, cf. [Eveènements](#ev
|
||||
### Comment réparer ?
|
||||
Cela va dépendre de la cause du problème :
|
||||
1. Si un capteur est en défaut, il faut le réparer (remettre des piles, le changer, vérifier l'intégration Météo qui donne la température extérieure, ...),
|
||||
2. Si le paramètre `security_delay_min` est trop petit, cela risque de générer beaucoup de fausses alertes. Une valeur correcte est de l'ordre de 60 min, surtout si vous avez des capteurs de température à pile. Cf [mes réglages](tuning-examples.md#le-capteur-de-température-alimenté-par-batterie)
|
||||
3. Certains capteurs de température, n'envoie pas de mesure si la température n'a pas changée. Donc en cas de température très stable pendant longtemps, le mode sécurité peut se déclencher. Ce n'est pas très grave puisqu'il s'enlève dès que le VTherm reçoit à nouveau une température. Sur certain thermomètre (TuYA par exemple ou Zigbee), on peut forcer le délai max entre 2 mesures. Il conviendra de mettre un délai max < `security_delay_min`,
|
||||
2. Si le paramètre `safety_delay_min` est trop petit, cela risque de générer beaucoup de fausses alertes. Une valeur correcte est de l'ordre de 60 min, surtout si vous avez des capteurs de température à pile. Cf [mes réglages](tuning-examples.md#le-capteur-de-température-alimenté-par-batterie)
|
||||
3. Certains capteurs de température, n'envoie pas de mesure si la température n'a pas changée. Donc en cas de température très stable pendant longtemps, le mode sécurité peut se déclencher. Ce n'est pas très grave puisqu'il s'enlève dès que le VTherm reçoit à nouveau une température. Sur certain thermomètre (TuYA par exemple ou Zigbee), on peut forcer le délai max entre 2 mesures. Il conviendra de mettre un délai max < `safety_delay_min`,
|
||||
4. Dès que la température sera a nouveau reçue le mode sécurité s'enlèvera et les valeurs précédentes de preset, température cible et mode seront restaurées.
|
||||
5. Si c'est le capteur de température extérieur qui est en défaut, vous pouvez désactiver le déclenchement du mode sécurité puisqu'il influe assez peu sur le résultat. Pour ce faire, cf. [ici](feature-advanced.md#la-mise-en-sécurité)
|
||||
|
||||
@@ -205,4 +206,8 @@ logs:
|
||||
```
|
||||
Vous devez recharger la configuration yaml (Outils de dev / Yaml / Toute la configuration Yaml) ou redémarrer Home Assistant pour que ce changement soit pris en compte.
|
||||
|
||||
Attention, en mode debug Versatile Thermostat est très verbeux et peut vite ralentir Home Assistant ou saturer votre disque dur. Si vous passez en mode debug pour une analyse d'anomalie il faut s'y mettre juste le temps de reproduire le bug et désactiver le mode debug juste après.
|
||||
Attention, en mode debug Versatile Thermostat est très verbeux et peut vite ralentir Home Assistant ou saturer votre disque dur. Si vous passez en mode debug pour une analyse d'anomalie il faut s'y mettre juste le temps de reproduire le bug et désactiver le mode debug juste après.
|
||||
|
||||
## VTherm ne suit pas les changements de consigne faits directement depuis le sous-jacents (`over_climate`)
|
||||
|
||||
Voir le détail de cette fonction [ici](over-climate.md#suivre-les-changements-de-température-du-sous-jacent).
|
||||
@@ -18,9 +18,9 @@
|
||||
## Le capteur de température alimenté par batterie
|
||||
Ces capteurs sont souvent paresseux et n'envoit pas toujours de mesure de température lorsqu'elle est stable. Par conséquent, les réglages doivent être laches pour éviter les faux positifs.
|
||||
|
||||
- security_delay_min : 60 min (parce que ces capteurs sont paresseux)
|
||||
- security_min_on_percent : 0,7 (70% - on passe en mode sécurité si le radiateur chauffait plus de 70% du temps)
|
||||
- security_default_on_percent : 0,4 (40% - en mode sécurité, on garde un fond de chauffe de 40% du temps pour éviter d'avoir trop froid)
|
||||
- safety_delay_min : 60 min (parce que ces capteurs sont paresseux)
|
||||
- safety_min_on_percent : 0,7 (70% - on passe en mode sécurité si le radiateur chauffait plus de 70% du temps)
|
||||
- safety_default_on_percent : 0,4 (40% - en mode sécurité, on garde un fond de chauffe de 40% du temps pour éviter d'avoir trop froid)
|
||||
|
||||
Il faut comprendre ces réglages comme suit :
|
||||
|
||||
@@ -34,9 +34,9 @@ Versatile Thermostat vous permet d'être notifié lorsqu'un évènement de ce ty
|
||||
|
||||
## Capteur de température réactif (sur secteur)
|
||||
Un thermomètre alimenté est censé est très régulier dans l'envoi des températures. Si il n'envoie rien pendant 15 min, il a certainement un soucis et on peut réagir plus vite sans risque de faux positif.
|
||||
- security_delay_min : 15 min
|
||||
- security_min_on_percent : 0,5 (50% - on passe en preset ``security`` si le radiateur chauffait plus de 50% du temps)
|
||||
- security_default_on_percent : 0,25 (20% - en preset ``security``, on garde un fond de chauffe de 20% du temps)
|
||||
- safety_delay_min : 15 min
|
||||
- safety_min_on_percent : 0,5 (50% - on passe en preset ``security`` si le radiateur chauffait plus de 50% du temps)
|
||||
- safety_default_on_percent : 0,25 (20% - en preset ``security``, on garde un fond de chauffe de 20% du temps)
|
||||
|
||||
|
||||
## Mes presets
|
||||
|
||||
12
faq.md
12
faq.md
@@ -153,9 +153,9 @@ Ces paramètres sont sensibles et assez difficiles à régler. Merci de ne les u
|
||||
<summary>Pourquoi mon Versatile Thermostat se met en Securite ?</summary>
|
||||
|
||||
## Pourquoi mon Versatile Thermostat se met en Securite ?
|
||||
Le mode sécurité est possible sur tous les types de VTherm . Il survient lorsqu'un des 2 thermomètres qui donne la température de la pièce ou la température extérieure n'a pas envoyé de valeur depuis plus de `security_delay_min` minutes et que le radiateur chauffait à au moins `security_min_on_percent`.
|
||||
Le mode sécurité est possible sur tous les types de VTherm . Il survient lorsqu'un des 2 thermomètres qui donne la température de la pièce ou la température extérieure n'a pas envoyé de valeur depuis plus de `safety_delay_min` minutes et que le radiateur chauffait à au moins `safety_min_on_percent`.
|
||||
|
||||
Comme l'algorithme est basé sur les mesures de température, si elles ne sont plus reçues par le VTherm, il y a un risque de surchauffe et d'incendie. Pour éviter ça, lorsque les conditions rappelées ci-dessus sont détectées, la chauffe est limité au paramètre `security_default_on_percent`. Cette valeur doit donc être raisonnablement faible (10% est une bonne valeur). Elle permet d'éviter un incendie tout en évitant de couper totalement le radiateur (risque de gel).
|
||||
Comme l'algorithme est basé sur les mesures de température, si elles ne sont plus reçues par le VTherm, il y a un risque de surchauffe et d'incendie. Pour éviter ça, lorsque les conditions rappelées ci-dessus sont détectées, la chauffe est limité au paramètre `safety_default_on_percent`. Cette valeur doit donc être raisonnablement faible (10% est une bonne valeur). Elle permet d'éviter un incendie tout en évitant de couper totalement le radiateur (risque de gel).
|
||||
|
||||
Tous ces paramètres se règlent dans la dernière page de la configuration du VTherm : "Paramètres avancés".
|
||||
|
||||
@@ -178,14 +178,14 @@ last_temperature_datetime: "2023-12-06T18:43:28.346010+01:00"
|
||||
last_ext_temperature_datetime: "2023-12-06T13:04:35.164367+01:00"
|
||||
last_update_datetime: "2023-12-06T18:43:28.351103+01:00"
|
||||
...
|
||||
security_delay_min: 60
|
||||
safety_delay_min: 60
|
||||
```
|
||||
|
||||
On voit que :
|
||||
1. le VTherm est bien en mode sécurité (`security_state: true`),
|
||||
2. l'heure courante est le 06/12/2023 à 18h43:28 (`last_update_datetime: "2023-12-06T18:43:28.351103+01:00"`),
|
||||
3. l'heure de dernière réception de la température intérieure est le 06/12/2023 à 18h43:28 (`last_temperature_datetime: "2023-12-06T18:43:28.346010+01:00"`). Elle est donc récente,
|
||||
4. l'heure de dernière réception de la température extérieure est le 06/12/2023 à 13h04:35 (`last_ext_temperature_datetime: "2023-12-06T13:04:35.164367+01:00`). C'est donc l'heure extérieure qui a plus de 5 h de retard et qui a provoquée le passage en mode sécurité, car le seuil est limité à 60 min (`security_delay_min: 60`).
|
||||
4. l'heure de dernière réception de la température extérieure est le 06/12/2023 à 13h04:35 (`last_ext_temperature_datetime: "2023-12-06T13:04:35.164367+01:00`). C'est donc l'heure extérieure qui a plus de 5 h de retard et qui a provoquée le passage en mode sécurité, car le seuil est limité à 60 min (`safety_delay_min: 60`).
|
||||
|
||||
### Comment être averti lorsque cela se produit ?
|
||||
Pour être averti, le VTherm envoie un évènement dès que ça se produit et un en fin d'alerte sécurité. Vous pouvez capter ces évènements dans une automatisation et envoyer une notification par exemple, faire clignoter un voyant, déclencher une sirène, ... A vous de voir.
|
||||
@@ -195,8 +195,8 @@ Pour manipuler les évènements générés par le VTherm, cf. [Eveènements](#ev
|
||||
### Comment réparer ?
|
||||
Cela va dépendre de la cause du problème :
|
||||
1. Si un capteur est en défaut, il faut le réparer (remettre des piles, le changer, vérifier l'intégration Météo qui donne la température extérieure, ...),
|
||||
2. Si le paramètre `security_delay_min` est trop petit, cela rsique de générer beaucoup de fausses alertes. Une valeur correcte est de l'ordre de 60 min, surtout si vous avez des capteurs de température à pile.
|
||||
3. Certains capteurs de température, n'envoie pas de mesure si la température n'a pas changée. Donc en cas de température très stable pendant longtemps, le mode sécurité peut se déclencher. Ce n'est pas très grave puisqu'il s'enlève dès que le VTherm reçoit à nouveau une température. Sur certain thermomètre (TuYA par exemple), on peut forcer le délai max entre 2 mesures. Il conviendra de mettre un délai max < `security_delay_min`,
|
||||
2. Si le paramètre `safety_delay_min` est trop petit, cela rsique de générer beaucoup de fausses alertes. Une valeur correcte est de l'ordre de 60 min, surtout si vous avez des capteurs de température à pile.
|
||||
3. Certains capteurs de température, n'envoie pas de mesure si la température n'a pas changée. Donc en cas de température très stable pendant longtemps, le mode sécurité peut se déclencher. Ce n'est pas très grave puisqu'il s'enlève dès que le VTherm reçoit à nouveau une température. Sur certain thermomètre (TuYA par exemple), on peut forcer le délai max entre 2 mesures. Il conviendra de mettre un délai max < `safety_delay_min`,
|
||||
4. Dès que la température sera a nouveau reçue le mode sécurité s'enlèvera et les valeurs précédentes de preset, température cible et mode seront restaurées.
|
||||
</details>
|
||||
|
||||
|
||||
@@ -3,5 +3,5 @@
|
||||
"content_in_root": false,
|
||||
"render_readme": true,
|
||||
"hide_default_branch": false,
|
||||
"homeassistant": "2024.10.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.10.4
|
||||
homeassistant==2025.1.2
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# pylint: disable=wildcard-import, unused-wildcard-import, protected-access, unused-argument, line-too-long, abstract-method, too-many-lines, redefined-builtin
|
||||
# pylint: disable=wildcard-import, unused-wildcard-import, unused-import, protected-access, unused-argument, line-too-long, abstract-method, too-many-lines, redefined-builtin
|
||||
|
||||
""" Some common resources """
|
||||
import asyncio
|
||||
@@ -8,7 +8,16 @@ from unittest.mock import patch, MagicMock # pylint: disable=unused-import
|
||||
import pytest # pylint: disable=unused-import
|
||||
|
||||
from homeassistant.core import HomeAssistant, Event, EVENT_STATE_CHANGED, State
|
||||
from homeassistant.const import UnitOfTemperature, STATE_ON, STATE_OFF, ATTR_TEMPERATURE
|
||||
from homeassistant.const import (
|
||||
UnitOfTemperature,
|
||||
STATE_ON,
|
||||
STATE_OFF,
|
||||
ATTR_TEMPERATURE,
|
||||
STATE_UNAVAILABLE,
|
||||
STATE_UNKNOWN,
|
||||
STATE_HOME,
|
||||
STATE_NOT_HOME,
|
||||
)
|
||||
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.helpers.entity import Entity
|
||||
@@ -50,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,
|
||||
@@ -80,7 +87,7 @@ FULL_SWITCH_CONFIG = (
|
||||
| MOCK_TH_OVER_SWITCH_CENTRAL_MAIN_CONFIG
|
||||
| MOCK_TH_OVER_SWITCH_TYPE_CONFIG
|
||||
| MOCK_TH_OVER_SWITCH_TPI_CONFIG
|
||||
| MOCK_PRESETS_CONFIG
|
||||
# | MOCK_PRESETS_CONFIG
|
||||
| MOCK_FULL_FEATURES
|
||||
| MOCK_WINDOW_CONFIG
|
||||
| MOCK_MOTION_CONFIG
|
||||
@@ -95,7 +102,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
|
||||
@@ -109,7 +115,7 @@ PARTIAL_CLIMATE_CONFIG = (
|
||||
| MOCK_TH_OVER_CLIMATE_MAIN_CONFIG
|
||||
| MOCK_TH_OVER_CLIMATE_CENTRAL_MAIN_CONFIG
|
||||
| MOCK_TH_OVER_CLIMATE_TYPE_CONFIG
|
||||
| MOCK_PRESETS_CONFIG
|
||||
# | MOCK_PRESETS_CONFIG
|
||||
| MOCK_ADVANCED_CONFIG
|
||||
)
|
||||
|
||||
@@ -118,7 +124,7 @@ PARTIAL_CLIMATE_CONFIG_USE_DEVICE_TEMP = (
|
||||
| MOCK_TH_OVER_CLIMATE_MAIN_CONFIG
|
||||
| MOCK_TH_OVER_CLIMATE_CENTRAL_MAIN_CONFIG
|
||||
| MOCK_TH_OVER_CLIMATE_TYPE_USE_DEVICE_TEMP_CONFIG
|
||||
| MOCK_PRESETS_CONFIG
|
||||
# | MOCK_PRESETS_CONFIG
|
||||
| MOCK_ADVANCED_CONFIG
|
||||
)
|
||||
|
||||
@@ -127,7 +133,7 @@ PARTIAL_CLIMATE_NOT_REGULATED_CONFIG = (
|
||||
| MOCK_TH_OVER_CLIMATE_MAIN_CONFIG
|
||||
| MOCK_TH_OVER_CLIMATE_CENTRAL_MAIN_CONFIG
|
||||
| MOCK_TH_OVER_CLIMATE_TYPE_NOT_REGULATED_CONFIG
|
||||
| MOCK_PRESETS_CONFIG
|
||||
# | MOCK_PRESETS_CONFIG
|
||||
| MOCK_ADVANCED_CONFIG
|
||||
)
|
||||
|
||||
@@ -136,7 +142,7 @@ PARTIAL_CLIMATE_AC_CONFIG = (
|
||||
| MOCK_TH_OVER_CLIMATE_TYPE_AC_CONFIG
|
||||
| MOCK_TH_OVER_CLIMATE_MAIN_CONFIG
|
||||
| MOCK_TH_OVER_CLIMATE_CENTRAL_MAIN_CONFIG
|
||||
| MOCK_PRESETS_CONFIG
|
||||
# | MOCK_PRESETS_CONFIG
|
||||
| MOCK_ADVANCED_CONFIG
|
||||
)
|
||||
|
||||
@@ -144,7 +150,7 @@ FULL_4SWITCH_CONFIG = (
|
||||
MOCK_TH_OVER_4SWITCH_USER_CONFIG
|
||||
| MOCK_TH_OVER_4SWITCH_TYPE_CONFIG
|
||||
| MOCK_TH_OVER_SWITCH_TPI_CONFIG
|
||||
| MOCK_PRESETS_CONFIG
|
||||
# | MOCK_PRESETS_CONFIG
|
||||
| MOCK_WINDOW_CONFIG
|
||||
| MOCK_MOTION_CONFIG
|
||||
| MOCK_POWER_CONFIG
|
||||
@@ -176,6 +182,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,
|
||||
@@ -188,9 +195,9 @@ FULL_CENTRAL_CONFIG = {
|
||||
CONF_PRESENCE_SENSOR: "binary_sensor.mock_presence_sensor",
|
||||
CONF_PRESET_POWER: 14,
|
||||
CONF_MINIMAL_ACTIVATION_DELAY: 11,
|
||||
CONF_SECURITY_DELAY_MIN: 61,
|
||||
CONF_SECURITY_MIN_ON_PERCENT: 0.5,
|
||||
CONF_SECURITY_DEFAULT_ON_PERCENT: 0.2,
|
||||
CONF_SAFETY_DELAY_MIN: 61,
|
||||
CONF_SAFETY_MIN_ON_PERCENT: 0.5,
|
||||
CONF_SAFETY_DEFAULT_ON_PERCENT: 0.2,
|
||||
CONF_USE_CENTRAL_BOILER_FEATURE: False,
|
||||
}
|
||||
|
||||
@@ -229,9 +236,9 @@ FULL_CENTRAL_CONFIG_WITH_BOILER = {
|
||||
CONF_MAX_POWER_SENSOR: "sensor.mock_max_power_sensor",
|
||||
CONF_PRESET_POWER: 14,
|
||||
CONF_MINIMAL_ACTIVATION_DELAY: 11,
|
||||
CONF_SECURITY_DELAY_MIN: 61,
|
||||
CONF_SECURITY_MIN_ON_PERCENT: 0.5,
|
||||
CONF_SECURITY_DEFAULT_ON_PERCENT: 0.2,
|
||||
CONF_SAFETY_DELAY_MIN: 61,
|
||||
CONF_SAFETY_MIN_ON_PERCENT: 0.5,
|
||||
CONF_SAFETY_DEFAULT_ON_PERCENT: 0.2,
|
||||
CONF_USE_CENTRAL_BOILER_FEATURE: True,
|
||||
CONF_CENTRAL_BOILER_ACTIVATION_SRV: "switch.pompe_chaudiere/switch.turn_on",
|
||||
CONF_CENTRAL_BOILER_DEACTIVATION_SRV: "switch.pompe_chaudiere/switch.turn_off",
|
||||
@@ -579,22 +586,26 @@ class MockNumber(NumberEntity):
|
||||
def set_native_value(self, value: float):
|
||||
"""Change the value"""
|
||||
self._attr_native_value = value
|
||||
self.async_write_ha_state()
|
||||
|
||||
|
||||
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)
|
||||
await hass.config_entries.async_setup(entry.entry_id)
|
||||
assert entry.state is ConfigEntryState.LOADED
|
||||
|
||||
# We should reload the VTherm links
|
||||
# vtherm_api: VersatileThermostatAPI = VersatileThermostatAPI.get_vtherm_api()
|
||||
# central_config = vtherm_api.find_central_configuration()
|
||||
entity = search_entity(hass, entity_id, CLIMATE_DOMAIN)
|
||||
# if entity and hasattr(entity, "init_presets")::
|
||||
# await entity.init_presets(central_config)
|
||||
|
||||
if entity and temps:
|
||||
await set_all_climate_preset_temp(
|
||||
hass, entity, temps, entity.entity_id.replace("climate.", "")
|
||||
)
|
||||
|
||||
return entity
|
||||
|
||||
@@ -736,9 +747,11 @@ async def send_power_change_event(entity: BaseThermostat, new_power, date, sleep
|
||||
)
|
||||
},
|
||||
)
|
||||
await entity._async_power_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(
|
||||
@@ -762,9 +775,11 @@ async def send_max_power_change_event(
|
||||
)
|
||||
},
|
||||
)
|
||||
await entity._async_max_power_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(
|
||||
@@ -795,7 +810,7 @@ async def send_window_change_event(
|
||||
),
|
||||
},
|
||||
)
|
||||
ret = await entity._async_windows_changed(window_event)
|
||||
ret = await entity.window_manager._window_sensor_changed(window_event)
|
||||
if sleep:
|
||||
await asyncio.sleep(0.1)
|
||||
return ret
|
||||
@@ -829,14 +844,14 @@ async def send_motion_change_event(
|
||||
),
|
||||
},
|
||||
)
|
||||
ret = await entity._async_motion_changed(motion_event)
|
||||
ret = await entity.motion_manager._motion_sensor_changed(motion_event)
|
||||
if sleep:
|
||||
await asyncio.sleep(0.1)
|
||||
return ret
|
||||
|
||||
|
||||
async def send_presence_change_event(
|
||||
entity: BaseThermostat, new_state: bool, old_state: bool, date, sleep=True
|
||||
vtherm: BaseThermostat, new_state: bool, old_state: bool, date, sleep=True
|
||||
):
|
||||
"""Sending a new presence event simulating a change on the window state"""
|
||||
_LOGGER.info(
|
||||
@@ -844,26 +859,26 @@ async def send_presence_change_event(
|
||||
new_state,
|
||||
old_state,
|
||||
date,
|
||||
entity,
|
||||
vtherm,
|
||||
)
|
||||
presence_event = Event(
|
||||
EVENT_STATE_CHANGED,
|
||||
{
|
||||
"new_state": State(
|
||||
entity_id=entity.entity_id,
|
||||
entity_id=vtherm.entity_id,
|
||||
state=STATE_ON if new_state else STATE_OFF,
|
||||
last_changed=date,
|
||||
last_updated=date,
|
||||
),
|
||||
"old_state": State(
|
||||
entity_id=entity.entity_id,
|
||||
entity_id=vtherm.entity_id,
|
||||
state=STATE_ON if old_state else STATE_OFF,
|
||||
last_changed=date,
|
||||
last_updated=date,
|
||||
),
|
||||
},
|
||||
)
|
||||
ret = await entity._async_presence_changed(presence_event)
|
||||
ret = await vtherm._presence_manager._presence_sensor_changed(presence_event)
|
||||
if sleep:
|
||||
await asyncio.sleep(0.1)
|
||||
return ret
|
||||
@@ -999,7 +1014,7 @@ async def set_climate_preset_temp(
|
||||
await temp_entity.async_set_native_value(temp)
|
||||
else:
|
||||
_LOGGER.warning(
|
||||
"commons tests set_cliamte_preset_temp: cannot find number entity with entity_id '%s'",
|
||||
"commons tests set_climate_preset_temp: cannot find number entity with entity_id '%s'",
|
||||
number_entity_id,
|
||||
)
|
||||
|
||||
@@ -1061,9 +1076,14 @@ async def set_all_climate_preset_temp(
|
||||
NUMBER_DOMAIN,
|
||||
)
|
||||
assert temp_entity
|
||||
if not temp_entity:
|
||||
raise ConfigurationNotCompleteError(
|
||||
f"'{number_entity_name}' don't exists as number entity"
|
||||
)
|
||||
# Because set_value is not implemented in Number class (really don't understand why...)
|
||||
assert temp_entity.state == value
|
||||
|
||||
await hass.async_block_till_done()
|
||||
|
||||
#
|
||||
# Side effects management
|
||||
@@ -1091,3 +1111,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
|
||||
|
||||
@@ -89,7 +89,12 @@ MOCK_TH_OVER_SWITCH_AC_TYPE_CONFIG = {
|
||||
}
|
||||
|
||||
MOCK_TH_OVER_4SWITCH_TYPE_CONFIG = {
|
||||
CONF_UNDERLYING_LIST: ["switch.mock_4switch0", "switch.mock_4switch1","switch.mock_4switch2","switch.mock_4switch3"],
|
||||
CONF_UNDERLYING_LIST: [
|
||||
"switch.mock_4switch0",
|
||||
"switch.mock_4switch1",
|
||||
"switch.mock_4switch2",
|
||||
"switch.mock_4switch3",
|
||||
],
|
||||
CONF_HEATER_KEEP_ALIVE: 0,
|
||||
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
|
||||
CONF_AC_MODE: False,
|
||||
@@ -135,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
|
||||
@@ -179,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",
|
||||
}
|
||||
@@ -195,9 +185,9 @@ MOCK_PRESENCE_AC_CONFIG = {
|
||||
|
||||
MOCK_ADVANCED_CONFIG = {
|
||||
CONF_MINIMAL_ACTIVATION_DELAY: 10,
|
||||
CONF_SECURITY_DELAY_MIN: 5,
|
||||
CONF_SECURITY_MIN_ON_PERCENT: 0.4,
|
||||
CONF_SECURITY_DEFAULT_ON_PERCENT: 0.3,
|
||||
CONF_SAFETY_DELAY_MIN: 5,
|
||||
CONF_SAFETY_MIN_ON_PERCENT: 0.4,
|
||||
CONF_SAFETY_DEFAULT_ON_PERCENT: 0.3,
|
||||
}
|
||||
|
||||
MOCK_DEFAULT_FEATURE_CONFIG = {
|
||||
|
||||
@@ -53,8 +53,8 @@ async def test_over_climate_auto_fan_mode_turbo(
|
||||
CONF_USE_PRESENCE_FEATURE: False,
|
||||
CONF_CLIMATE: "climate.mock_climate",
|
||||
CONF_MINIMAL_ACTIVATION_DELAY: 30,
|
||||
CONF_SECURITY_DELAY_MIN: 5,
|
||||
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
|
||||
CONF_SAFETY_DELAY_MIN: 5,
|
||||
CONF_SAFETY_MIN_ON_PERCENT: 0.3,
|
||||
CONF_AUTO_FAN_MODE: CONF_AUTO_FAN_TURBO,
|
||||
},
|
||||
)
|
||||
@@ -119,8 +119,8 @@ async def test_over_climate_auto_fan_mode_not_turbo(
|
||||
CONF_USE_PRESENCE_FEATURE: False,
|
||||
CONF_CLIMATE: "climate.mock_climate",
|
||||
CONF_MINIMAL_ACTIVATION_DELAY: 30,
|
||||
CONF_SECURITY_DELAY_MIN: 5,
|
||||
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
|
||||
CONF_SAFETY_DELAY_MIN: 5,
|
||||
CONF_SAFETY_MIN_ON_PERCENT: 0.3,
|
||||
CONF_AUTO_FAN_MODE: CONF_AUTO_FAN_TURBO,
|
||||
},
|
||||
)
|
||||
@@ -189,8 +189,8 @@ async def test_over_climate_auto_fan_mode_turbo_activation(
|
||||
CONF_USE_PRESENCE_FEATURE: False,
|
||||
CONF_CLIMATE: "climate.mock_climate",
|
||||
CONF_MINIMAL_ACTIVATION_DELAY: 30,
|
||||
CONF_SECURITY_DELAY_MIN: 5,
|
||||
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
|
||||
CONF_SAFETY_DELAY_MIN: 5,
|
||||
CONF_SAFETY_MIN_ON_PERCENT: 0.3,
|
||||
CONF_AUTO_FAN_MODE: CONF_AUTO_FAN_TURBO,
|
||||
CONF_AC_MODE: True,
|
||||
},
|
||||
|
||||
@@ -309,7 +309,7 @@ async def test_over_climate_regulation_limitations(
|
||||
assert entity.hvac_action == HVACAction.HEATING
|
||||
|
||||
# the regulated temperature will not change because when we set temp manually it is forced
|
||||
assert entity.regulated_target_temp == 17 # 19.5
|
||||
assert entity.regulated_target_temp == 19.5
|
||||
|
||||
# 2. set manual target temp (at now - 18) -> the regulation should be taken into account
|
||||
event_timestamp = now - timedelta(minutes=18)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# pylint: disable=wildcard-import, unused-wildcard-import, protected-access, unused-argument, line-too-long, unused-variable
|
||||
# pylint: disable=wildcard-import, unused-wildcard-import, protected-access, unused-argument, line-too-long, unused-variable, too-many-lines
|
||||
|
||||
""" Test the Auto Start Stop algorithm management """
|
||||
from datetime import datetime, timedelta
|
||||
@@ -335,8 +335,8 @@ async def test_auto_start_stop_none_vtherm(
|
||||
CONF_PRESENCE_SENSOR: "binary_sensor.presence_sensor",
|
||||
CONF_CLIMATE: "climate.mock_climate",
|
||||
CONF_MINIMAL_ACTIVATION_DELAY: 30,
|
||||
CONF_SECURITY_DELAY_MIN: 5,
|
||||
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
|
||||
CONF_SAFETY_DELAY_MIN: 5,
|
||||
CONF_SAFETY_MIN_ON_PERCENT: 0.3,
|
||||
CONF_AUTO_FAN_MODE: CONF_AUTO_FAN_TURBO,
|
||||
CONF_AC_MODE: True,
|
||||
CONF_AUTO_START_STOP_LEVEL: AUTO_START_STOP_LEVEL_NONE,
|
||||
@@ -363,15 +363,14 @@ async def test_auto_start_stop_none_vtherm(
|
||||
# Initialize all temps
|
||||
await set_all_climate_preset_temp(hass, vtherm, temps, "overclimate")
|
||||
# Check correct initialization of auto_start_stop attributes
|
||||
assert (
|
||||
vtherm._attr_extra_state_attributes["auto_start_stop_level"]
|
||||
== AUTO_START_STOP_LEVEL_NONE
|
||||
)
|
||||
|
||||
assert vtherm._attr_extra_state_attributes["auto_start_stop_dtmin"] is None
|
||||
assert vtherm._attr_extra_state_attributes.get("auto_start_stop_level") is None
|
||||
assert vtherm._attr_extra_state_attributes.get("auto_start_stop_dtmin") is None
|
||||
|
||||
# 1. Vtherm auto-start/stop should be in NONE mode
|
||||
assert vtherm.auto_start_stop_level == AUTO_START_STOP_LEVEL_NONE
|
||||
assert (
|
||||
vtherm.auto_start_stop_manager.auto_start_stop_level
|
||||
== AUTO_START_STOP_LEVEL_NONE
|
||||
)
|
||||
|
||||
# 2. We should not find any switch Enable entity
|
||||
assert (
|
||||
@@ -427,8 +426,8 @@ async def test_auto_start_stop_medium_heat_vtherm(
|
||||
CONF_PRESENCE_SENSOR: "binary_sensor.presence_sensor",
|
||||
CONF_CLIMATE: "climate.mock_climate",
|
||||
CONF_MINIMAL_ACTIVATION_DELAY: 30,
|
||||
CONF_SECURITY_DELAY_MIN: 5,
|
||||
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
|
||||
CONF_SAFETY_DELAY_MIN: 5,
|
||||
CONF_SAFETY_MIN_ON_PERCENT: 0.3,
|
||||
CONF_AUTO_FAN_MODE: CONF_AUTO_FAN_TURBO,
|
||||
CONF_AC_MODE: True,
|
||||
CONF_AUTO_START_STOP_LEVEL: AUTO_START_STOP_LEVEL_MEDIUM,
|
||||
@@ -464,7 +463,10 @@ async def test_auto_start_stop_medium_heat_vtherm(
|
||||
assert vtherm._attr_extra_state_attributes["auto_start_stop_dtmin"] == 15
|
||||
|
||||
# 1. Vtherm auto-start/stop should be in MEDIUM mode and an enable entity should exists
|
||||
assert vtherm.auto_start_stop_level == AUTO_START_STOP_LEVEL_MEDIUM
|
||||
assert (
|
||||
vtherm.auto_start_stop_manager.auto_start_stop_level
|
||||
== AUTO_START_STOP_LEVEL_MEDIUM
|
||||
)
|
||||
enable_entity = search_entity(
|
||||
hass, "switch.overclimate_enable_auto_start_stop", SWITCH_DOMAIN
|
||||
)
|
||||
@@ -488,7 +490,7 @@ async def test_auto_start_stop_medium_heat_vtherm(
|
||||
# 3. Set current temperature to 19 5 min later
|
||||
now = now + timedelta(minutes=5)
|
||||
# reset accumulated error (only for testing)
|
||||
vtherm._auto_start_stop_algo._accumulated_error = 0
|
||||
vtherm.auto_start_stop_manager._auto_start_stop_algo._accumulated_error = 0
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
|
||||
) as mock_send_event:
|
||||
@@ -500,7 +502,7 @@ async def test_auto_start_stop_medium_heat_vtherm(
|
||||
assert vtherm.hvac_mode == HVACMode.HEAT
|
||||
assert mock_send_event.call_count == 0
|
||||
assert (
|
||||
vtherm._auto_start_stop_algo.accumulated_error == 0
|
||||
vtherm.auto_start_stop_manager._auto_start_stop_algo.accumulated_error == 0
|
||||
) # target = current = 19
|
||||
|
||||
# 4. Set current temperature to 20 5 min later
|
||||
@@ -516,7 +518,10 @@ async def test_auto_start_stop_medium_heat_vtherm(
|
||||
assert vtherm.hvac_mode == HVACMode.HEAT
|
||||
assert mock_send_event.call_count == 0
|
||||
# accumulated_error = target - current = -1 x 5 min / 2
|
||||
assert vtherm._auto_start_stop_algo.accumulated_error == -2.5
|
||||
assert (
|
||||
vtherm.auto_start_stop_manager._auto_start_stop_algo.accumulated_error
|
||||
== -2.5
|
||||
)
|
||||
|
||||
# 5. Set current temperature to 21 5 min later -> should turn off
|
||||
now = now + timedelta(minutes=5)
|
||||
@@ -532,7 +537,9 @@ async def test_auto_start_stop_medium_heat_vtherm(
|
||||
assert vtherm.hvac_off_reason == HVAC_OFF_REASON_AUTO_START_STOP
|
||||
|
||||
# accumulated_error = -2.5 + target - current = -2 x 5 min / 2 capped to 5
|
||||
assert vtherm._auto_start_stop_algo.accumulated_error == -5
|
||||
assert (
|
||||
vtherm.auto_start_stop_manager._auto_start_stop_algo.accumulated_error == -5
|
||||
)
|
||||
|
||||
# a message should have been sent
|
||||
assert mock_send_event.call_count >= 1
|
||||
@@ -577,7 +584,9 @@ async def test_auto_start_stop_medium_heat_vtherm(
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# accumulated_error = .... capped to -5
|
||||
assert vtherm._auto_start_stop_algo.accumulated_error == -5
|
||||
assert (
|
||||
vtherm.auto_start_stop_manager._auto_start_stop_algo.accumulated_error == -5
|
||||
)
|
||||
|
||||
# VTherm should stay stopped cause slope is too low to allow the turn to On
|
||||
assert vtherm.hvac_mode == HVACMode.OFF
|
||||
@@ -593,7 +602,9 @@ async def test_auto_start_stop_medium_heat_vtherm(
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# accumulated_error = -5/2 + target - current = 1 x 20 min / 2 capped to 5
|
||||
assert vtherm._auto_start_stop_algo.accumulated_error == 5
|
||||
assert (
|
||||
vtherm.auto_start_stop_manager._auto_start_stop_algo.accumulated_error == 5
|
||||
)
|
||||
|
||||
# VTherm should have been stopped
|
||||
assert vtherm.hvac_mode == HVACMode.HEAT
|
||||
@@ -680,8 +691,8 @@ async def test_auto_start_stop_fast_ac_vtherm(
|
||||
CONF_PRESENCE_SENSOR: "binary_sensor.presence_sensor",
|
||||
CONF_UNDERLYING_LIST: ["climate.mock_climate"],
|
||||
CONF_MINIMAL_ACTIVATION_DELAY: 30,
|
||||
CONF_SECURITY_DELAY_MIN: 5,
|
||||
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
|
||||
CONF_SAFETY_DELAY_MIN: 5,
|
||||
CONF_SAFETY_MIN_ON_PERCENT: 0.3,
|
||||
CONF_AUTO_FAN_MODE: CONF_AUTO_FAN_TURBO,
|
||||
CONF_AC_MODE: True,
|
||||
CONF_AUTO_START_STOP_LEVEL: AUTO_START_STOP_LEVEL_FAST,
|
||||
@@ -717,7 +728,10 @@ async def test_auto_start_stop_fast_ac_vtherm(
|
||||
assert vtherm._attr_extra_state_attributes["auto_start_stop_dtmin"] == 7
|
||||
|
||||
# 1. Vtherm auto-start/stop should be in MEDIUM mode
|
||||
assert vtherm.auto_start_stop_level == AUTO_START_STOP_LEVEL_FAST
|
||||
assert (
|
||||
vtherm.auto_start_stop_manager.auto_start_stop_level
|
||||
== AUTO_START_STOP_LEVEL_FAST
|
||||
)
|
||||
|
||||
tz = get_tz(hass) # pylint: disable=invalid-name
|
||||
now: datetime = datetime.now(tz=tz)
|
||||
@@ -736,7 +750,7 @@ async def test_auto_start_stop_fast_ac_vtherm(
|
||||
# 3. Set current temperature to 19 5 min later
|
||||
now = now + timedelta(minutes=5)
|
||||
# reset accumulated error for test
|
||||
vtherm._auto_start_stop_algo._accumulated_error = 0
|
||||
vtherm.auto_start_stop_manager._auto_start_stop_algo._accumulated_error = 0
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
|
||||
) as mock_send_event:
|
||||
@@ -748,7 +762,8 @@ async def test_auto_start_stop_fast_ac_vtherm(
|
||||
assert vtherm.hvac_mode == HVACMode.COOL
|
||||
assert mock_send_event.call_count == 0
|
||||
assert (
|
||||
vtherm._auto_start_stop_algo.accumulated_error == 0 # target = current = 25
|
||||
vtherm.auto_start_stop_manager._auto_start_stop_algo.accumulated_error
|
||||
== 0 # target = current = 25
|
||||
)
|
||||
|
||||
# 4. Set current temperature to 23 5 min later -> should turn off
|
||||
@@ -764,7 +779,9 @@ async def test_auto_start_stop_fast_ac_vtherm(
|
||||
assert vtherm.hvac_mode == HVACMode.OFF
|
||||
|
||||
# accumulated_error = target - current = 2 x 5 min / 2 capped to 2
|
||||
assert vtherm._auto_start_stop_algo.accumulated_error == 2
|
||||
assert (
|
||||
vtherm.auto_start_stop_manager._auto_start_stop_algo.accumulated_error == 2
|
||||
)
|
||||
|
||||
# a message should have been sent
|
||||
assert mock_send_event.call_count >= 1
|
||||
@@ -809,7 +826,9 @@ async def test_auto_start_stop_fast_ac_vtherm(
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# accumulated_error = 2/2 + target - current = -1 x 20 min / 2 capped to 2
|
||||
assert vtherm._auto_start_stop_algo.accumulated_error == -2
|
||||
assert (
|
||||
vtherm.auto_start_stop_manager._auto_start_stop_algo.accumulated_error == -2
|
||||
)
|
||||
|
||||
# VTherm should stay stopped
|
||||
assert vtherm.hvac_mode == HVACMode.OFF
|
||||
@@ -826,7 +845,9 @@ async def test_auto_start_stop_fast_ac_vtherm(
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# accumulated_error = 2/2 + target - current = -1 x 20 min / 2 capped to 2
|
||||
assert vtherm._auto_start_stop_algo.accumulated_error == -2
|
||||
assert (
|
||||
vtherm.auto_start_stop_manager._auto_start_stop_algo.accumulated_error == -2
|
||||
)
|
||||
|
||||
# VTherm should have been stopped
|
||||
assert vtherm.hvac_mode == HVACMode.COOL
|
||||
@@ -911,8 +932,8 @@ async def test_auto_start_stop_medium_heat_vtherm_preset_change(
|
||||
CONF_PRESENCE_SENSOR: "binary_sensor.presence_sensor",
|
||||
CONF_CLIMATE: "climate.mock_climate",
|
||||
CONF_MINIMAL_ACTIVATION_DELAY: 30,
|
||||
CONF_SECURITY_DELAY_MIN: 5,
|
||||
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
|
||||
CONF_SAFETY_DELAY_MIN: 5,
|
||||
CONF_SAFETY_MIN_ON_PERCENT: 0.3,
|
||||
CONF_AUTO_FAN_MODE: CONF_AUTO_FAN_TURBO,
|
||||
CONF_AC_MODE: True,
|
||||
CONF_AUTO_START_STOP_LEVEL: AUTO_START_STOP_LEVEL_FAST,
|
||||
@@ -948,7 +969,10 @@ async def test_auto_start_stop_medium_heat_vtherm_preset_change(
|
||||
assert vtherm._attr_extra_state_attributes["auto_start_stop_dtmin"] == 7
|
||||
|
||||
# 1. Vtherm auto-start/stop should be in MEDIUM mode
|
||||
assert vtherm.auto_start_stop_level == AUTO_START_STOP_LEVEL_FAST
|
||||
assert (
|
||||
vtherm.auto_start_stop_manager.auto_start_stop_level
|
||||
== AUTO_START_STOP_LEVEL_FAST
|
||||
)
|
||||
|
||||
tz = get_tz(hass) # pylint: disable=invalid-name
|
||||
now: datetime = datetime.now(tz=tz)
|
||||
@@ -966,7 +990,7 @@ async def test_auto_start_stop_medium_heat_vtherm_preset_change(
|
||||
|
||||
# 3. Set current temperature to 21 5 min later to auto-stop
|
||||
now = now + timedelta(minutes=5)
|
||||
vtherm._auto_start_stop_algo._accumulated_error = 0
|
||||
vtherm.auto_start_stop_manager._auto_start_stop_algo._accumulated_error = 0
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
|
||||
) as mock_send_event:
|
||||
@@ -977,7 +1001,9 @@ async def test_auto_start_stop_medium_heat_vtherm_preset_change(
|
||||
# VTherm should have been stopped
|
||||
assert vtherm.hvac_mode == HVACMode.OFF
|
||||
|
||||
assert vtherm._auto_start_stop_algo.accumulated_error == -2
|
||||
assert (
|
||||
vtherm.auto_start_stop_manager._auto_start_stop_algo.accumulated_error == -2
|
||||
)
|
||||
|
||||
# a message should have been sent
|
||||
assert mock_send_event.call_count >= 1
|
||||
@@ -1032,7 +1058,9 @@ async def test_auto_start_stop_medium_heat_vtherm_preset_change(
|
||||
await hass.async_block_till_done()
|
||||
assert vtherm.target_temperature == 21
|
||||
|
||||
assert vtherm._auto_start_stop_algo.accumulated_error == 2
|
||||
assert (
|
||||
vtherm.auto_start_stop_manager._auto_start_stop_algo.accumulated_error == 2
|
||||
)
|
||||
|
||||
# VTherm should have been restarted
|
||||
assert vtherm.hvac_mode == HVACMode.HEAT
|
||||
@@ -1117,8 +1145,8 @@ async def test_auto_start_stop_medium_heat_vtherm_preset_change_enable_false(
|
||||
CONF_PRESENCE_SENSOR: "binary_sensor.presence_sensor",
|
||||
CONF_CLIMATE: "climate.mock_climate",
|
||||
CONF_MINIMAL_ACTIVATION_DELAY: 30,
|
||||
CONF_SECURITY_DELAY_MIN: 5,
|
||||
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
|
||||
CONF_SAFETY_DELAY_MIN: 5,
|
||||
CONF_SAFETY_MIN_ON_PERCENT: 0.3,
|
||||
CONF_AUTO_FAN_MODE: CONF_AUTO_FAN_TURBO,
|
||||
CONF_AC_MODE: True,
|
||||
CONF_AUTO_START_STOP_LEVEL: AUTO_START_STOP_LEVEL_FAST,
|
||||
@@ -1154,7 +1182,10 @@ async def test_auto_start_stop_medium_heat_vtherm_preset_change_enable_false(
|
||||
assert vtherm._attr_extra_state_attributes["auto_start_stop_dtmin"] == 7
|
||||
|
||||
# 1. Vtherm auto-start/stop should be in FAST mode and enable should be on
|
||||
assert vtherm.auto_start_stop_level == AUTO_START_STOP_LEVEL_FAST
|
||||
assert (
|
||||
vtherm.auto_start_stop_manager.auto_start_stop_level
|
||||
== AUTO_START_STOP_LEVEL_FAST
|
||||
)
|
||||
enable_entity = search_entity(
|
||||
hass, "switch.overclimate_enable_auto_start_stop", SWITCH_DOMAIN
|
||||
)
|
||||
@@ -1185,7 +1216,7 @@ async def test_auto_start_stop_medium_heat_vtherm_preset_change_enable_false(
|
||||
|
||||
# 3. Set current temperature to 21 5 min later to auto-stop
|
||||
now = now + timedelta(minutes=5)
|
||||
vtherm._auto_start_stop_algo._accumulated_error = 0
|
||||
vtherm.auto_start_stop_manager._auto_start_stop_algo._accumulated_error = 0
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
|
||||
) as mock_send_event:
|
||||
@@ -1197,7 +1228,9 @@ async def test_auto_start_stop_medium_heat_vtherm_preset_change_enable_false(
|
||||
assert vtherm.hvac_mode == HVACMode.HEAT
|
||||
|
||||
# Not calculated cause enable = false
|
||||
assert vtherm._auto_start_stop_algo.accumulated_error == 0
|
||||
assert (
|
||||
vtherm.auto_start_stop_manager._auto_start_stop_algo.accumulated_error == 0
|
||||
)
|
||||
|
||||
# a message should have been sent
|
||||
assert mock_send_event.call_count == 0
|
||||
@@ -1251,8 +1284,8 @@ async def test_auto_start_stop_fast_heat_window(
|
||||
CONF_PRESENCE_SENSOR: "binary_sensor.presence_sensor",
|
||||
CONF_CLIMATE: "climate.mock_climate",
|
||||
CONF_MINIMAL_ACTIVATION_DELAY: 30,
|
||||
CONF_SECURITY_DELAY_MIN: 5,
|
||||
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
|
||||
CONF_SAFETY_DELAY_MIN: 5,
|
||||
CONF_SAFETY_MIN_ON_PERCENT: 0.3,
|
||||
CONF_AUTO_FAN_MODE: CONF_AUTO_FAN_TURBO,
|
||||
CONF_AC_MODE: True,
|
||||
CONF_AUTO_START_STOP_LEVEL: AUTO_START_STOP_LEVEL_FAST,
|
||||
@@ -1288,7 +1321,10 @@ async def test_auto_start_stop_fast_heat_window(
|
||||
assert vtherm._attr_extra_state_attributes["auto_start_stop_dtmin"] == 7
|
||||
|
||||
# 1. Vtherm auto-start/stop should be in MEDIUM mode and an enable entity should exists
|
||||
assert vtherm.auto_start_stop_level == AUTO_START_STOP_LEVEL_FAST
|
||||
assert (
|
||||
vtherm.auto_start_stop_manager.auto_start_stop_level
|
||||
== AUTO_START_STOP_LEVEL_FAST
|
||||
)
|
||||
enable_entity = search_entity(
|
||||
hass, "switch.overclimate_enable_auto_start_stop", SWITCH_DOMAIN
|
||||
)
|
||||
@@ -1299,7 +1335,7 @@ async def test_auto_start_stop_fast_heat_window(
|
||||
now: datetime = datetime.now(tz=tz)
|
||||
|
||||
# 2. Set mode to Heat and preset to Comfort and close the window
|
||||
send_window_change_event(vtherm, False, False, now, False)
|
||||
await send_window_change_event(vtherm, False, False, now, False)
|
||||
await send_presence_change_event(vtherm, True, False, now)
|
||||
await send_temperature_change_event(vtherm, 18, now, True)
|
||||
await vtherm.async_set_hvac_mode(HVACMode.HEAT)
|
||||
@@ -1310,12 +1346,12 @@ async def test_auto_start_stop_fast_heat_window(
|
||||
# VTherm should be heating
|
||||
assert vtherm.hvac_mode == HVACMode.HEAT
|
||||
# VTherm window_state should be off
|
||||
assert vtherm.window_state == STATE_OFF
|
||||
assert vtherm.window_state == STATE_UNKNOWN # cause condition is not evaluated
|
||||
|
||||
# 3. Set current temperature to 21 5 min later -> should turn off VTherm
|
||||
now = now + timedelta(minutes=5)
|
||||
# reset accumulated error (only for testing)
|
||||
vtherm._auto_start_stop_algo._accumulated_error = 0
|
||||
vtherm.auto_start_stop_manager._auto_start_stop_algo._accumulated_error = 0
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
|
||||
) as mock_send_event:
|
||||
@@ -1426,8 +1462,8 @@ async def test_auto_start_stop_fast_heat_window_mixed(
|
||||
CONF_PRESENCE_SENSOR: "binary_sensor.presence_sensor",
|
||||
CONF_CLIMATE: "climate.mock_climate",
|
||||
CONF_MINIMAL_ACTIVATION_DELAY: 30,
|
||||
CONF_SECURITY_DELAY_MIN: 5,
|
||||
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
|
||||
CONF_SAFETY_DELAY_MIN: 5,
|
||||
CONF_SAFETY_MIN_ON_PERCENT: 0.3,
|
||||
CONF_AUTO_FAN_MODE: CONF_AUTO_FAN_TURBO,
|
||||
CONF_AC_MODE: True,
|
||||
CONF_AUTO_START_STOP_LEVEL: AUTO_START_STOP_LEVEL_FAST,
|
||||
@@ -1463,7 +1499,10 @@ async def test_auto_start_stop_fast_heat_window_mixed(
|
||||
assert vtherm._attr_extra_state_attributes["auto_start_stop_dtmin"] == 7
|
||||
|
||||
# 1. Vtherm auto-start/stop should be in MEDIUM mode and an enable entity should exists
|
||||
assert vtherm.auto_start_stop_level == AUTO_START_STOP_LEVEL_FAST
|
||||
assert (
|
||||
vtherm.auto_start_stop_manager.auto_start_stop_level
|
||||
== AUTO_START_STOP_LEVEL_FAST
|
||||
)
|
||||
enable_entity = search_entity(
|
||||
hass, "switch.overclimate_enable_auto_start_stop", SWITCH_DOMAIN
|
||||
)
|
||||
@@ -1474,7 +1513,7 @@ async def test_auto_start_stop_fast_heat_window_mixed(
|
||||
now: datetime = datetime.now(tz=tz)
|
||||
|
||||
# 2. Set mode to Heat and preset to Comfort and close the window
|
||||
send_window_change_event(vtherm, False, False, now, False)
|
||||
await send_window_change_event(vtherm, False, False, now, False)
|
||||
await send_presence_change_event(vtherm, True, False, now)
|
||||
await send_temperature_change_event(vtherm, 18, now, True)
|
||||
await vtherm.async_set_hvac_mode(HVACMode.HEAT)
|
||||
@@ -1485,7 +1524,7 @@ async def test_auto_start_stop_fast_heat_window_mixed(
|
||||
# VTherm should be heating
|
||||
assert vtherm.hvac_mode == HVACMode.HEAT
|
||||
# VTherm window_state should be off
|
||||
assert vtherm.window_state == STATE_OFF
|
||||
assert vtherm.window_state == STATE_UNKNOWN # cause try_condition is not evaluated
|
||||
|
||||
# 3. Open the window and wait for the delay
|
||||
now = now + timedelta(minutes=2)
|
||||
@@ -1513,7 +1552,7 @@ async def test_auto_start_stop_fast_heat_window_mixed(
|
||||
# 4. Set current temperature to 21 5 min later -> should turn off VTherm
|
||||
now = now + timedelta(minutes=5)
|
||||
# reset accumulated error (only for testing)
|
||||
vtherm._auto_start_stop_algo._accumulated_error = 0
|
||||
vtherm.auto_start_stop_manager._auto_start_stop_algo._accumulated_error = 0
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
|
||||
) as mock_send_event:
|
||||
@@ -1605,8 +1644,8 @@ async def test_auto_start_stop_disable_vtherm_off(
|
||||
CONF_USE_PRESENCE_FEATURE: False,
|
||||
CONF_CLIMATE: "climate.mock_climate",
|
||||
CONF_MINIMAL_ACTIVATION_DELAY: 30,
|
||||
CONF_SECURITY_DELAY_MIN: 5,
|
||||
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
|
||||
CONF_SAFETY_DELAY_MIN: 5,
|
||||
CONF_SAFETY_MIN_ON_PERCENT: 0.3,
|
||||
CONF_AUTO_FAN_MODE: CONF_AUTO_FAN_TURBO,
|
||||
CONF_AC_MODE: False,
|
||||
CONF_AUTO_START_STOP_LEVEL: AUTO_START_STOP_LEVEL_FAST,
|
||||
@@ -1637,6 +1676,9 @@ async def test_auto_start_stop_disable_vtherm_off(
|
||||
await set_all_climate_preset_temp(hass, vtherm, temps, "overclimate")
|
||||
|
||||
# Check correct initialization of auto_start_stop attributes
|
||||
assert (
|
||||
vtherm._attr_extra_state_attributes["is_auto_start_stop_configured"] is True
|
||||
)
|
||||
assert (
|
||||
vtherm._attr_extra_state_attributes["auto_start_stop_level"]
|
||||
== AUTO_START_STOP_LEVEL_FAST
|
||||
@@ -1646,7 +1688,10 @@ async def test_auto_start_stop_disable_vtherm_off(
|
||||
|
||||
# 1. Vtherm auto-start/stop should be in FAST mode and enable should be on
|
||||
vtherm._set_now(now)
|
||||
assert vtherm.auto_start_stop_level == AUTO_START_STOP_LEVEL_FAST
|
||||
assert (
|
||||
vtherm.auto_start_stop_manager.auto_start_stop_level
|
||||
== AUTO_START_STOP_LEVEL_FAST
|
||||
)
|
||||
enable_entity = search_entity(
|
||||
hass, "switch.overclimate_enable_auto_start_stop", SWITCH_DOMAIN
|
||||
)
|
||||
|
||||
120
tests/test_auto_start_stop_feature_manager.py
Normal file
120
tests/test_auto_start_stop_feature_manager.py
Normal file
@@ -0,0 +1,120 @@
|
||||
# pylint: disable=unused-argument, line-too-long, protected-access, too-many-lines
|
||||
""" Test the Window management """
|
||||
import logging
|
||||
from unittest.mock import PropertyMock, MagicMock
|
||||
|
||||
from custom_components.versatile_thermostat.base_thermostat import BaseThermostat
|
||||
|
||||
from custom_components.versatile_thermostat.feature_auto_start_stop_manager import (
|
||||
FeatureAutoStartStopManager,
|
||||
)
|
||||
from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import
|
||||
|
||||
logging.getLogger().setLevel(logging.DEBUG)
|
||||
|
||||
|
||||
async def test_auto_start_stop_feature_manager_create(
|
||||
hass: HomeAssistant,
|
||||
):
|
||||
"""Test the FeatureMotionManager class direclty"""
|
||||
|
||||
fake_vtherm = MagicMock(spec=BaseThermostat)
|
||||
type(fake_vtherm).name = PropertyMock(return_value="the name")
|
||||
|
||||
# 1. creation
|
||||
auto_start_stop_manager = FeatureAutoStartStopManager(fake_vtherm, hass)
|
||||
|
||||
assert auto_start_stop_manager is not None
|
||||
assert auto_start_stop_manager.is_configured is False
|
||||
assert auto_start_stop_manager.is_auto_stopped is False
|
||||
assert auto_start_stop_manager.auto_start_stop_enable is False
|
||||
assert auto_start_stop_manager.name == "the name"
|
||||
|
||||
assert len(auto_start_stop_manager._active_listener) == 0
|
||||
|
||||
custom_attributes = {}
|
||||
auto_start_stop_manager.add_custom_attributes(custom_attributes)
|
||||
assert custom_attributes["is_auto_start_stop_configured"] is False
|
||||
# assert custom_attributes["auto_start_stop_enable"] is False
|
||||
# assert custom_attributes["auto_start_stop_level"] == AUTO_START_STOP_LEVEL_NONE
|
||||
# assert custom_attributes["auto_start_stop_dtmin"] is None
|
||||
# assert custom_attributes["auto_start_stop_accumulated_error"] is None
|
||||
# assert custom_attributes["auto_start_stop_accumulated_error_threshold"] is None
|
||||
# assert custom_attributes["auto_start_stop_last_switch_date"] is None
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"use_auto_start_stop_feature, level, is_configured",
|
||||
[
|
||||
# fmt: off
|
||||
( True, AUTO_START_STOP_LEVEL_NONE, True),
|
||||
( True, AUTO_START_STOP_LEVEL_SLOW, True),
|
||||
( True, AUTO_START_STOP_LEVEL_MEDIUM, True),
|
||||
( True, AUTO_START_STOP_LEVEL_FAST, True),
|
||||
# Level is missing , will be set to None
|
||||
( True, None, True),
|
||||
( False, AUTO_START_STOP_LEVEL_NONE, False),
|
||||
( False, AUTO_START_STOP_LEVEL_SLOW, False),
|
||||
( False, AUTO_START_STOP_LEVEL_MEDIUM, False),
|
||||
( False, AUTO_START_STOP_LEVEL_FAST, False),
|
||||
# Level is missing , will be set to None
|
||||
( False, None, False),
|
||||
# fmt: on
|
||||
],
|
||||
)
|
||||
async def test_auto_start_stop_feature_manager_post_init(
|
||||
hass: HomeAssistant, use_auto_start_stop_feature, level, is_configured
|
||||
):
|
||||
"""Test the FeatureMotionManager class direclty"""
|
||||
|
||||
fake_vtherm = MagicMock(spec=BaseThermostat)
|
||||
type(fake_vtherm).name = PropertyMock(return_value="the name")
|
||||
|
||||
# 1. creation
|
||||
auto_start_stop_manager = FeatureAutoStartStopManager(fake_vtherm, hass)
|
||||
assert auto_start_stop_manager is not None
|
||||
|
||||
# 2. post_init
|
||||
auto_start_stop_manager.post_init(
|
||||
{
|
||||
CONF_USE_AUTO_START_STOP_FEATURE: use_auto_start_stop_feature,
|
||||
CONF_AUTO_START_STOP_LEVEL: level,
|
||||
}
|
||||
)
|
||||
|
||||
assert auto_start_stop_manager.is_configured is is_configured
|
||||
assert (
|
||||
auto_start_stop_manager.auto_start_stop_level == level
|
||||
if level and is_configured
|
||||
else AUTO_START_STOP_LEVEL_NONE
|
||||
)
|
||||
assert auto_start_stop_manager.auto_start_stop_enable is False
|
||||
assert auto_start_stop_manager._auto_start_stop_algo is not None
|
||||
|
||||
custom_attributes = {}
|
||||
auto_start_stop_manager.add_custom_attributes(custom_attributes)
|
||||
assert custom_attributes["is_auto_start_stop_configured"] is is_configured
|
||||
|
||||
if auto_start_stop_manager.is_configured:
|
||||
assert custom_attributes["auto_start_stop_enable"] is False
|
||||
assert (
|
||||
custom_attributes["auto_start_stop_level"] == level
|
||||
if level and is_configured
|
||||
else AUTO_START_STOP_LEVEL_NONE
|
||||
)
|
||||
assert (
|
||||
custom_attributes["auto_start_stop_dtmin"]
|
||||
== auto_start_stop_manager._auto_start_stop_algo.dt_min
|
||||
)
|
||||
assert (
|
||||
custom_attributes["auto_start_stop_accumulated_error"]
|
||||
== auto_start_stop_manager._auto_start_stop_algo.accumulated_error
|
||||
)
|
||||
assert (
|
||||
custom_attributes["auto_start_stop_accumulated_error_threshold"]
|
||||
== auto_start_stop_manager._auto_start_stop_algo.accumulated_error_threshold
|
||||
)
|
||||
assert (
|
||||
custom_attributes["auto_start_stop_last_switch_date"]
|
||||
== auto_start_stop_manager._auto_start_stop_algo.last_switch_date
|
||||
)
|
||||
@@ -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
|
||||
@@ -57,8 +57,8 @@ async def test_security_binary_sensors(
|
||||
CONF_TPI_COEF_INT: 0.3,
|
||||
CONF_TPI_COEF_EXT: 0.01,
|
||||
CONF_MINIMAL_ACTIVATION_DELAY: 30,
|
||||
CONF_SECURITY_DELAY_MIN: 5,
|
||||
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
|
||||
CONF_SAFETY_DELAY_MIN: 5,
|
||||
CONF_SAFETY_MIN_ON_PERCENT: 0.3,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -84,17 +84,17 @@ async def test_security_binary_sensors(
|
||||
# Set temperature in the past
|
||||
event_timestamp = now - timedelta(minutes=6)
|
||||
|
||||
# set temperature to 15 so that on_percent will be > security_min_on_percent (0.2)
|
||||
# set temperature to 15 so that on_percent will be > safety_min_on_percent (0.2)
|
||||
await send_temperature_change_event(entity, 15, event_timestamp)
|
||||
|
||||
assert entity.security_state is True
|
||||
assert entity.safety_state is STATE_ON
|
||||
# Simulate the event reception
|
||||
await security_binary_sensor.async_my_climate_changed()
|
||||
assert security_binary_sensor.state == STATE_ON
|
||||
|
||||
# set temperature now
|
||||
await send_temperature_change_event(entity, 15, now)
|
||||
assert entity.security_state is False
|
||||
assert entity.safety_state is not STATE_ON
|
||||
# Simulate the event reception
|
||||
await security_binary_sensor.async_my_climate_changed()
|
||||
assert security_binary_sensor.state == STATE_OFF
|
||||
@@ -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,
|
||||
@@ -134,17 +138,15 @@ async def test_overpowering_binary_sensors(
|
||||
CONF_TPI_COEF_INT: 0.3,
|
||||
CONF_TPI_COEF_EXT: 0.01,
|
||||
CONF_MINIMAL_ACTIVATION_DELAY: 30,
|
||||
CONF_SECURITY_DELAY_MIN: 5,
|
||||
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
|
||||
CONF_POWER_SENSOR: "sensor.mock_power_sensor",
|
||||
CONF_MAX_POWER_SENSOR: "sensor.mock_power_max_sensor",
|
||||
CONF_SAFETY_DELAY_MIN: 5,
|
||||
CONF_SAFETY_MIN_ON_PERCENT: 0.3,
|
||||
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.check_overpowering() is False
|
||||
assert entity.overpowering_state is None
|
||||
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.check_overpowering() is True
|
||||
assert entity.overpowering_state is True
|
||||
# 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.check_overpowering() is False
|
||||
assert entity.overpowering_state is False
|
||||
# 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])
|
||||
@@ -218,8 +240,8 @@ async def test_window_binary_sensors(
|
||||
CONF_TPI_COEF_INT: 0.3,
|
||||
CONF_TPI_COEF_EXT: 0.01,
|
||||
CONF_MINIMAL_ACTIVATION_DELAY: 30,
|
||||
CONF_SECURITY_DELAY_MIN: 5,
|
||||
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
|
||||
CONF_SAFETY_DELAY_MIN: 5,
|
||||
CONF_SAFETY_MIN_ON_PERCENT: 0.3,
|
||||
CONF_WINDOW_SENSOR: "binary_sensor.mock_window_sensor",
|
||||
CONF_WINDOW_DELAY: 0, # important to not been obliged to wait
|
||||
},
|
||||
@@ -241,7 +263,7 @@ async def test_window_binary_sensors(
|
||||
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 entity.window_state is STATE_OFF
|
||||
assert entity.window_state is STATE_UNKNOWN
|
||||
|
||||
await window_binary_sensor.async_my_climate_changed()
|
||||
assert window_binary_sensor.state is STATE_OFF
|
||||
@@ -306,10 +328,12 @@ async def test_motion_binary_sensors(
|
||||
CONF_TPI_COEF_INT: 0.3,
|
||||
CONF_TPI_COEF_EXT: 0.01,
|
||||
CONF_MINIMAL_ACTIVATION_DELAY: 30,
|
||||
CONF_SECURITY_DELAY_MIN: 5,
|
||||
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
|
||||
CONF_SAFETY_DELAY_MIN: 5,
|
||||
CONF_SAFETY_MIN_ON_PERCENT: 0.3,
|
||||
CONF_MOTION_SENSOR: "binary_sensor.mock_motion_sensor",
|
||||
CONF_MOTION_DELAY: 0, # important to not been obliged to wait
|
||||
CONF_MOTION_PRESET: PRESET_BOOST,
|
||||
CONF_NO_MOTION_PRESET: PRESET_ECO,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -329,7 +353,7 @@ async def test_motion_binary_sensors(
|
||||
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 entity.motion_state is None
|
||||
assert entity.motion_state is STATE_UNKNOWN
|
||||
|
||||
await motion_binary_sensor.async_my_climate_changed()
|
||||
assert motion_binary_sensor.state is STATE_OFF
|
||||
@@ -397,8 +421,8 @@ async def test_presence_binary_sensors(
|
||||
CONF_TPI_COEF_INT: 0.3,
|
||||
CONF_TPI_COEF_EXT: 0.01,
|
||||
CONF_MINIMAL_ACTIVATION_DELAY: 30,
|
||||
CONF_SECURITY_DELAY_MIN: 5,
|
||||
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
|
||||
CONF_SAFETY_DELAY_MIN: 5,
|
||||
CONF_SAFETY_MIN_ON_PERCENT: 0.3,
|
||||
CONF_PRESENCE_SENSOR: "binary_sensor.mock_presence_sensor",
|
||||
},
|
||||
)
|
||||
@@ -419,7 +443,7 @@ async def test_presence_binary_sensors(
|
||||
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 entity.presence_state is None
|
||||
assert entity.presence_state is STATE_UNKNOWN
|
||||
|
||||
await presence_binary_sensor.async_my_climate_changed()
|
||||
assert presence_binary_sensor.state is STATE_OFF
|
||||
@@ -480,8 +504,8 @@ async def test_binary_sensors_over_climate_minimal(
|
||||
CONF_USE_PRESENCE_FEATURE: False,
|
||||
CONF_CLIMATE: "climate.mock_climate",
|
||||
CONF_MINIMAL_ACTIVATION_DELAY: 30,
|
||||
CONF_SECURITY_DELAY_MIN: 5,
|
||||
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
|
||||
CONF_SAFETY_DELAY_MIN: 5,
|
||||
CONF_SAFETY_MIN_ON_PERCENT: 0.3,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ async def test_bug_63(
|
||||
skip_turn_on_off_heater,
|
||||
skip_send_event,
|
||||
):
|
||||
"""Test that it should be possible to set the security_default_on_percent to 0"""
|
||||
"""Test that it should be possible to set the safety_default_on_percent to 0"""
|
||||
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
@@ -63,9 +63,9 @@ async def test_bug_63(
|
||||
CONF_TPI_COEF_INT: 0.3,
|
||||
CONF_TPI_COEF_EXT: 0.01,
|
||||
CONF_MINIMAL_ACTIVATION_DELAY: 30,
|
||||
CONF_SECURITY_DELAY_MIN: 5,
|
||||
CONF_SECURITY_MIN_ON_PERCENT: 0.0, # !! here
|
||||
CONF_SECURITY_DEFAULT_ON_PERCENT: 0.0, # !! here
|
||||
CONF_SAFETY_DELAY_MIN: 5,
|
||||
CONF_SAFETY_MIN_ON_PERCENT: 0.0, # !! here
|
||||
CONF_SAFETY_DEFAULT_ON_PERCENT: 0.0, # !! here
|
||||
CONF_DEVICE_POWER: 200,
|
||||
},
|
||||
)
|
||||
@@ -75,8 +75,8 @@ async def test_bug_63(
|
||||
)
|
||||
assert entity
|
||||
|
||||
assert entity._security_min_on_percent == 0
|
||||
assert entity._security_default_on_percent == 0
|
||||
assert entity.safety_manager.safety_min_on_percent == 0
|
||||
assert entity.safety_manager.safety_default_on_percent == 0
|
||||
|
||||
|
||||
# Waiting for answer in https://github.com/jmcollin78/versatile_thermostat/issues/64
|
||||
@@ -89,7 +89,7 @@ async def test_bug_64(
|
||||
skip_turn_on_off_heater,
|
||||
skip_send_event,
|
||||
):
|
||||
"""Test that it should be possible to set the security_default_on_percent to 0"""
|
||||
"""Test that it should be possible to set the safety_default_on_percent to 0"""
|
||||
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
@@ -115,9 +115,9 @@ async def test_bug_64(
|
||||
CONF_TPI_COEF_INT: 0.3,
|
||||
CONF_TPI_COEF_EXT: 0.01,
|
||||
CONF_MINIMAL_ACTIVATION_DELAY: 30,
|
||||
CONF_SECURITY_DELAY_MIN: 5,
|
||||
CONF_SECURITY_MIN_ON_PERCENT: 0.5,
|
||||
CONF_SECURITY_DEFAULT_ON_PERCENT: 0.1, # !! here
|
||||
CONF_SAFETY_DELAY_MIN: 5,
|
||||
CONF_SAFETY_MIN_ON_PERCENT: 0.5,
|
||||
CONF_SAFETY_DEFAULT_ON_PERCENT: 0.1, # !! here
|
||||
CONF_DEVICE_POWER: 200,
|
||||
},
|
||||
)
|
||||
@@ -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,
|
||||
@@ -299,57 +304,67 @@ async def test_bug_407(hass: HomeAssistant, skip_hass_states_is_state):
|
||||
CONF_TPI_COEF_INT: 0.3,
|
||||
CONF_TPI_COEF_EXT: 0.01,
|
||||
CONF_MINIMAL_ACTIVATION_DELAY: 30,
|
||||
CONF_SECURITY_DELAY_MIN: 5,
|
||||
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
|
||||
CONF_POWER_SENSOR: "sensor.mock_power_sensor",
|
||||
CONF_MAX_POWER_SENSOR: "sensor.mock_power_max_sensor",
|
||||
CONF_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"
|
||||
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)
|
||||
assert entity.hvac_mode is HVACMode.HEAT
|
||||
assert entity.preset_mode is PRESET_COMFORT
|
||||
assert entity.overpowering_state is None
|
||||
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.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.overpowering_state is False
|
||||
assert entity.power_manager.overpowering_state is STATE_OFF
|
||||
assert entity.is_device_active is True
|
||||
|
||||
# 2. An already active heater that switch preset will not switch to overpowering
|
||||
@@ -359,36 +374,57 @@ 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.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.overpowering_state is False
|
||||
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. if heater is stopped (is_device_active==False) and power is over max, then overpowering should be started
|
||||
side_effects.add_or_update_side_effect("sensor.the_power_sensor", State("sensor.the_power_sensor", 150))
|
||||
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(),
|
||||
):
|
||||
now = now + timedelta(seconds=30)
|
||||
VersatileThermostatAPI.get_vtherm_api()._set_now(now)
|
||||
|
||||
# change preset to Boost
|
||||
await entity.async_set_preset_mode(PRESET_COMFORT)
|
||||
# waits that the heater starts
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
assert await entity.check_overpowering() is True
|
||||
# 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_POWER
|
||||
assert entity.overpowering_state is True
|
||||
assert entity.preset_mode is PRESET_COMFORT
|
||||
assert entity.power_manager.overpowering_state is STATE_OFF
|
||||
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@@ -445,8 +481,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 +490,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
|
||||
|
||||
@@ -510,8 +544,8 @@ async def test_bug_465(hass: HomeAssistant, skip_hass_states_is_state):
|
||||
CONF_PRESENCE_SENSOR: "binary_sensor.presence_sensor",
|
||||
CONF_CLIMATE: "climate.mock_climate",
|
||||
CONF_MINIMAL_ACTIVATION_DELAY: 30,
|
||||
CONF_SECURITY_DELAY_MIN: 5,
|
||||
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
|
||||
CONF_SAFETY_DELAY_MIN: 5,
|
||||
CONF_SAFETY_MIN_ON_PERCENT: 0.3,
|
||||
CONF_AUTO_FAN_MODE: CONF_AUTO_FAN_TURBO,
|
||||
CONF_AC_MODE: True,
|
||||
},
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
""" Test the central_configuration """
|
||||
import asyncio
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from unittest.mock import patch, call
|
||||
|
||||
@@ -29,6 +29,8 @@ from custom_components.versatile_thermostat.binary_sensor import (
|
||||
CentralBoilerBinarySensor,
|
||||
)
|
||||
|
||||
from custom_components.versatile_thermostat.sensor import NbActiveDeviceForBoilerSensor
|
||||
|
||||
from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import
|
||||
from .const import * # pylint: disable=wildcard-import, unused-wildcard-import
|
||||
|
||||
@@ -103,15 +105,15 @@ async def test_update_central_boiler_state_simple(
|
||||
CONF_USE_MOTION_FEATURE: False,
|
||||
CONF_USE_POWER_FEATURE: False,
|
||||
CONF_USE_PRESENCE_FEATURE: False,
|
||||
CONF_HEATER: switch1.entity_id,
|
||||
CONF_UNDERLYING_LIST: [switch1.entity_id],
|
||||
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
|
||||
CONF_INVERSE_SWITCH: False,
|
||||
CONF_TPI_COEF_INT: 0.3,
|
||||
CONF_TPI_COEF_EXT: 0.01,
|
||||
CONF_MINIMAL_ACTIVATION_DELAY: 30,
|
||||
CONF_SECURITY_DELAY_MIN: 5,
|
||||
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
|
||||
CONF_SECURITY_DEFAULT_ON_PERCENT: 0.1,
|
||||
CONF_SAFETY_DELAY_MIN: 5,
|
||||
CONF_SAFETY_MIN_ON_PERCENT: 0.3,
|
||||
CONF_SAFETY_DEFAULT_ON_PERCENT: 0.1,
|
||||
CONF_USE_MAIN_CENTRAL_CONFIG: True,
|
||||
CONF_USE_TPI_CENTRAL_CONFIG: True,
|
||||
CONF_USE_PRESETS_CENTRAL_CONFIG: True,
|
||||
@@ -147,6 +149,13 @@ async def test_update_central_boiler_state_simple(
|
||||
assert boiler_binary_sensor is not None
|
||||
assert boiler_binary_sensor.state == STATE_OFF
|
||||
|
||||
nb_device_active_sensor: NbActiveDeviceForBoilerSensor = search_entity(
|
||||
hass, "sensor.nb_device_active_for_boiler", "sensor"
|
||||
)
|
||||
assert nb_device_active_sensor is not None
|
||||
assert nb_device_active_sensor.state == 0
|
||||
assert nb_device_active_sensor.active_device_ids == []
|
||||
|
||||
# 1. start a heater
|
||||
with patch(
|
||||
"homeassistant.core.ServiceRegistry.async_call"
|
||||
@@ -195,6 +204,9 @@ async def test_update_central_boiler_state_simple(
|
||||
assert api.nb_active_device_for_boiler == 1
|
||||
assert boiler_binary_sensor.state == STATE_ON
|
||||
|
||||
assert nb_device_active_sensor.state == 1
|
||||
assert nb_device_active_sensor.active_device_ids == ["switch.switch1"]
|
||||
|
||||
# 2. stop a heater
|
||||
with patch(
|
||||
"homeassistant.core.ServiceRegistry.async_call"
|
||||
@@ -235,6 +247,9 @@ async def test_update_central_boiler_state_simple(
|
||||
assert api.nb_active_device_for_boiler == 0
|
||||
assert boiler_binary_sensor.state == STATE_OFF
|
||||
|
||||
assert nb_device_active_sensor.state == 0
|
||||
assert nb_device_active_sensor.active_device_ids == []
|
||||
|
||||
entity.remove_thermostat()
|
||||
|
||||
|
||||
@@ -272,18 +287,20 @@ async def test_update_central_boiler_state_multiple(
|
||||
CONF_USE_MOTION_FEATURE: False,
|
||||
CONF_USE_POWER_FEATURE: False,
|
||||
CONF_USE_PRESENCE_FEATURE: False,
|
||||
CONF_HEATER: switch1.entity_id,
|
||||
CONF_HEATER_2: switch2.entity_id,
|
||||
CONF_HEATER_3: switch3.entity_id,
|
||||
CONF_HEATER_4: switch4.entity_id,
|
||||
CONF_UNDERLYING_LIST: [
|
||||
switch1.entity_id,
|
||||
switch2.entity_id,
|
||||
switch3.entity_id,
|
||||
switch4.entity_id,
|
||||
],
|
||||
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
|
||||
CONF_INVERSE_SWITCH: False,
|
||||
CONF_TPI_COEF_INT: 0.3,
|
||||
CONF_TPI_COEF_EXT: 0.01,
|
||||
CONF_MINIMAL_ACTIVATION_DELAY: 30,
|
||||
CONF_SECURITY_DELAY_MIN: 5,
|
||||
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
|
||||
CONF_SECURITY_DEFAULT_ON_PERCENT: 0.1,
|
||||
CONF_SAFETY_DELAY_MIN: 5,
|
||||
CONF_SAFETY_MIN_ON_PERCENT: 0.3,
|
||||
CONF_SAFETY_DEFAULT_ON_PERCENT: 0.1,
|
||||
CONF_USE_MAIN_CENTRAL_CONFIG: True,
|
||||
CONF_USE_TPI_CENTRAL_CONFIG: True,
|
||||
CONF_USE_PRESETS_CENTRAL_CONFIG: True,
|
||||
@@ -302,10 +319,18 @@ async def test_update_central_boiler_state_multiple(
|
||||
assert entity.underlying_entities[1].entity_id == "switch.switch2"
|
||||
assert entity.underlying_entities[2].entity_id == "switch.switch3"
|
||||
assert entity.underlying_entities[3].entity_id == "switch.switch4"
|
||||
assert entity.nb_device_actives == 0
|
||||
assert entity.device_actives == []
|
||||
|
||||
assert api.nb_active_device_for_boiler_threshold == 1
|
||||
assert api.nb_active_device_for_boiler == 0
|
||||
|
||||
nb_device_active_sensor: NbActiveDeviceForBoilerSensor = search_entity(
|
||||
hass, "sensor.nb_device_active_for_boiler", "sensor"
|
||||
)
|
||||
assert nb_device_active_sensor is not None
|
||||
assert nb_device_active_sensor.state == 0
|
||||
assert nb_device_active_sensor.active_device_ids == []
|
||||
|
||||
# Force the VTherm to heat
|
||||
await entity.async_set_hvac_mode(HVACMode.HEAT)
|
||||
await entity.async_set_preset_mode(PRESET_BOOST)
|
||||
@@ -338,7 +363,7 @@ async def test_update_central_boiler_state_multiple(
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
assert entity.hvac_action == HVACAction.HEATING
|
||||
assert entity.nb_device_actives == 1
|
||||
assert entity.device_actives == ["switch.switch1"]
|
||||
|
||||
assert mock_service_call.call_count == 1
|
||||
# No switch of the boiler
|
||||
@@ -356,6 +381,9 @@ async def test_update_central_boiler_state_multiple(
|
||||
assert api.nb_active_device_for_boiler == 1
|
||||
assert boiler_binary_sensor.state == STATE_OFF
|
||||
|
||||
assert nb_device_active_sensor.state == 1
|
||||
assert nb_device_active_sensor.active_device_ids == ["switch.switch1"]
|
||||
|
||||
# 2. start a 2nd heater
|
||||
with patch(
|
||||
"homeassistant.core.ServiceRegistry.async_call"
|
||||
@@ -368,7 +396,7 @@ async def test_update_central_boiler_state_multiple(
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
assert entity.hvac_action == HVACAction.HEATING
|
||||
assert entity.nb_device_actives == 2
|
||||
assert entity.device_actives == ["switch.switch1", "switch.switch2"]
|
||||
|
||||
# Only the first heater is started by the algo
|
||||
assert mock_service_call.call_count == 1
|
||||
@@ -388,6 +416,12 @@ async def test_update_central_boiler_state_multiple(
|
||||
assert api.nb_active_device_for_boiler == 2
|
||||
assert boiler_binary_sensor.state == STATE_OFF
|
||||
|
||||
assert nb_device_active_sensor.state == 2
|
||||
assert nb_device_active_sensor.active_device_ids == [
|
||||
"switch.switch1",
|
||||
"switch.switch2",
|
||||
]
|
||||
|
||||
# 3. start a 3rd heater
|
||||
with patch(
|
||||
"homeassistant.core.ServiceRegistry.async_call"
|
||||
@@ -436,6 +470,13 @@ async def test_update_central_boiler_state_multiple(
|
||||
assert api.nb_active_device_for_boiler == 3
|
||||
assert boiler_binary_sensor.state == STATE_ON
|
||||
|
||||
assert nb_device_active_sensor.state == 3
|
||||
assert nb_device_active_sensor.active_device_ids == [
|
||||
"switch.switch1",
|
||||
"switch.switch2",
|
||||
"switch.switch3",
|
||||
]
|
||||
|
||||
# 4. start a 4th heater
|
||||
with patch(
|
||||
"homeassistant.core.ServiceRegistry.async_call"
|
||||
@@ -466,6 +507,14 @@ async def test_update_central_boiler_state_multiple(
|
||||
assert api.nb_active_device_for_boiler == 4
|
||||
assert boiler_binary_sensor.state == STATE_ON
|
||||
|
||||
assert nb_device_active_sensor.state == 4
|
||||
assert nb_device_active_sensor.active_device_ids == [
|
||||
"switch.switch1",
|
||||
"switch.switch2",
|
||||
"switch.switch3",
|
||||
"switch.switch4",
|
||||
]
|
||||
|
||||
# 5. stop a heater
|
||||
with patch(
|
||||
"homeassistant.core.ServiceRegistry.async_call"
|
||||
@@ -484,6 +533,13 @@ async def test_update_central_boiler_state_multiple(
|
||||
assert api.nb_active_device_for_boiler == 3
|
||||
assert boiler_binary_sensor.state == STATE_ON
|
||||
|
||||
assert nb_device_active_sensor.state == 3
|
||||
assert nb_device_active_sensor.active_device_ids == [
|
||||
"switch.switch2",
|
||||
"switch.switch3",
|
||||
"switch.switch4",
|
||||
]
|
||||
|
||||
# 6. stop a 2nd heater
|
||||
with patch(
|
||||
"homeassistant.core.ServiceRegistry.async_call"
|
||||
@@ -524,6 +580,12 @@ async def test_update_central_boiler_state_multiple(
|
||||
assert api.nb_active_device_for_boiler == 2
|
||||
assert boiler_binary_sensor.state == STATE_OFF
|
||||
|
||||
assert nb_device_active_sensor.state == 2
|
||||
assert nb_device_active_sensor.active_device_ids == [
|
||||
"switch.switch2",
|
||||
"switch.switch3",
|
||||
]
|
||||
|
||||
entity.remove_thermostat()
|
||||
|
||||
|
||||
@@ -558,15 +620,15 @@ async def test_update_central_boiler_state_simple_valve(
|
||||
CONF_USE_MOTION_FEATURE: False,
|
||||
CONF_USE_POWER_FEATURE: False,
|
||||
CONF_USE_PRESENCE_FEATURE: False,
|
||||
CONF_VALVE: valve1.entity_id,
|
||||
CONF_UNDERLYING_LIST: [valve1.entity_id],
|
||||
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
|
||||
CONF_INVERSE_SWITCH: False,
|
||||
CONF_TPI_COEF_INT: 0.3,
|
||||
CONF_TPI_COEF_EXT: 0.01,
|
||||
CONF_MINIMAL_ACTIVATION_DELAY: 30,
|
||||
CONF_SECURITY_DELAY_MIN: 5,
|
||||
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
|
||||
CONF_SECURITY_DEFAULT_ON_PERCENT: 0.1,
|
||||
CONF_SAFETY_DELAY_MIN: 5,
|
||||
CONF_SAFETY_MIN_ON_PERCENT: 0.3,
|
||||
CONF_SAFETY_DEFAULT_ON_PERCENT: 0.1,
|
||||
CONF_USE_MAIN_CENTRAL_CONFIG: True,
|
||||
CONF_USE_TPI_CENTRAL_CONFIG: True,
|
||||
CONF_USE_PRESETS_CENTRAL_CONFIG: True,
|
||||
@@ -594,7 +656,7 @@ async def test_update_central_boiler_state_simple_valve(
|
||||
now: datetime = datetime.now(tz=tz)
|
||||
|
||||
assert entity.hvac_mode == HVACMode.HEAT
|
||||
assert entity.nb_device_actives == 0
|
||||
assert entity.device_actives == []
|
||||
|
||||
boiler_binary_sensor: CentralBoilerBinarySensor = search_entity(
|
||||
hass, "binary_sensor.central_boiler", "binary_sensor"
|
||||
@@ -602,6 +664,13 @@ async def test_update_central_boiler_state_simple_valve(
|
||||
assert boiler_binary_sensor is not None
|
||||
assert boiler_binary_sensor.state == STATE_OFF
|
||||
|
||||
nb_device_active_sensor: NbActiveDeviceForBoilerSensor = search_entity(
|
||||
hass, "sensor.nb_device_active_for_boiler", "sensor"
|
||||
)
|
||||
assert nb_device_active_sensor is not None
|
||||
assert nb_device_active_sensor.state == 0
|
||||
assert nb_device_active_sensor.active_device_ids == []
|
||||
|
||||
# 1. start a valve
|
||||
with patch(
|
||||
"homeassistant.core.ServiceRegistry.async_call"
|
||||
@@ -616,7 +685,7 @@ async def test_update_central_boiler_state_simple_valve(
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
assert entity.hvac_action == HVACAction.HEATING
|
||||
assert entity.nb_device_actives == 1
|
||||
assert entity.device_actives == ["number.valve1"]
|
||||
|
||||
assert mock_service_call.call_count >= 1
|
||||
mock_service_call.assert_has_calls(
|
||||
@@ -644,6 +713,11 @@ async def test_update_central_boiler_state_simple_valve(
|
||||
assert api.nb_active_device_for_boiler == 1
|
||||
assert boiler_binary_sensor.state == STATE_ON
|
||||
|
||||
assert nb_device_active_sensor.state == 1
|
||||
assert nb_device_active_sensor.active_device_ids == [
|
||||
"number.valve1",
|
||||
]
|
||||
|
||||
# 2. stop a heater
|
||||
with patch(
|
||||
"homeassistant.core.ServiceRegistry.async_call"
|
||||
@@ -658,7 +732,7 @@ async def test_update_central_boiler_state_simple_valve(
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
assert entity.hvac_action == HVACAction.IDLE
|
||||
assert entity.nb_device_actives == 0
|
||||
assert entity.device_actives == []
|
||||
|
||||
assert mock_service_call.call_count >= 1
|
||||
mock_service_call.assert_has_calls(
|
||||
@@ -687,6 +761,9 @@ async def test_update_central_boiler_state_simple_valve(
|
||||
assert api.nb_active_device_for_boiler == 0
|
||||
assert boiler_binary_sensor.state == STATE_OFF
|
||||
|
||||
assert nb_device_active_sensor.state == 0
|
||||
assert nb_device_active_sensor.active_device_ids == []
|
||||
|
||||
entity.remove_thermostat()
|
||||
|
||||
|
||||
@@ -721,11 +798,11 @@ async def test_update_central_boiler_state_simple_climate(
|
||||
CONF_USE_MOTION_FEATURE: False,
|
||||
CONF_USE_POWER_FEATURE: False,
|
||||
CONF_USE_PRESENCE_FEATURE: False,
|
||||
CONF_CLIMATE: climate1.entity_id,
|
||||
CONF_UNDERLYING_LIST: [climate1.entity_id],
|
||||
CONF_MINIMAL_ACTIVATION_DELAY: 30,
|
||||
CONF_SECURITY_DELAY_MIN: 5,
|
||||
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
|
||||
CONF_SECURITY_DEFAULT_ON_PERCENT: 0.1,
|
||||
CONF_SAFETY_DELAY_MIN: 5,
|
||||
CONF_SAFETY_MIN_ON_PERCENT: 0.3,
|
||||
CONF_SAFETY_DEFAULT_ON_PERCENT: 0.1,
|
||||
CONF_USE_MAIN_CENTRAL_CONFIG: True,
|
||||
CONF_USE_PRESETS_CENTRAL_CONFIG: True,
|
||||
CONF_USE_ADVANCED_CENTRAL_CONFIG: True,
|
||||
@@ -748,6 +825,13 @@ async def test_update_central_boiler_state_simple_climate(
|
||||
assert api.nb_active_device_for_boiler_threshold == 1
|
||||
assert api.nb_active_device_for_boiler == 0
|
||||
|
||||
nb_device_active_sensor: NbActiveDeviceForBoilerSensor = search_entity(
|
||||
hass, "sensor.nb_device_active_for_boiler", "sensor"
|
||||
)
|
||||
assert nb_device_active_sensor is not None
|
||||
assert nb_device_active_sensor.state == 0
|
||||
assert nb_device_active_sensor.active_device_ids == []
|
||||
|
||||
# Force the VTherm to heat
|
||||
await entity.async_set_hvac_mode(HVACMode.HEAT)
|
||||
await entity.async_set_preset_mode(PRESET_BOOST)
|
||||
@@ -756,7 +840,7 @@ async def test_update_central_boiler_state_simple_climate(
|
||||
now: datetime = datetime.now(tz=tz)
|
||||
|
||||
assert entity.hvac_mode == HVACMode.HEAT
|
||||
assert entity.nb_device_actives == 0
|
||||
assert entity.device_actives == []
|
||||
|
||||
boiler_binary_sensor: CentralBoilerBinarySensor = search_entity(
|
||||
hass, "binary_sensor.central_boiler", "binary_sensor"
|
||||
@@ -779,7 +863,7 @@ async def test_update_central_boiler_state_simple_climate(
|
||||
await asyncio.sleep(0.5)
|
||||
|
||||
assert entity.hvac_action == HVACAction.HEATING
|
||||
assert entity.nb_device_actives == 1
|
||||
assert entity.device_actives == ["climate.climate1"]
|
||||
|
||||
assert mock_service_call.call_count >= 1
|
||||
mock_service_call.assert_has_calls(
|
||||
@@ -807,6 +891,11 @@ async def test_update_central_boiler_state_simple_climate(
|
||||
assert api.nb_active_device_for_boiler == 1
|
||||
assert boiler_binary_sensor.state == STATE_ON
|
||||
|
||||
assert nb_device_active_sensor.state == 1
|
||||
assert nb_device_active_sensor.active_device_ids == [
|
||||
"climate.climate1",
|
||||
]
|
||||
|
||||
# 2. stop a climate
|
||||
with patch(
|
||||
"homeassistant.core.ServiceRegistry.async_call"
|
||||
@@ -821,7 +910,7 @@ async def test_update_central_boiler_state_simple_climate(
|
||||
await asyncio.sleep(0.5)
|
||||
|
||||
assert entity.hvac_action == HVACAction.IDLE
|
||||
assert entity.nb_device_actives == 0
|
||||
assert entity.device_actives == []
|
||||
|
||||
assert mock_service_call.call_count >= 1
|
||||
mock_service_call.assert_has_calls(
|
||||
@@ -850,6 +939,277 @@ async def test_update_central_boiler_state_simple_climate(
|
||||
assert api.nb_active_device_for_boiler == 0
|
||||
assert boiler_binary_sensor.state == STATE_OFF
|
||||
|
||||
assert nb_device_active_sensor.state == 0
|
||||
assert nb_device_active_sensor.active_device_ids == []
|
||||
|
||||
entity.remove_thermostat()
|
||||
|
||||
|
||||
async def test_update_central_boiler_state_simple_climate_valve_regulation(
|
||||
hass: HomeAssistant,
|
||||
# skip_hass_states_is_state,
|
||||
# skip_hass_states_get,
|
||||
init_central_config_with_boiler_fixture,
|
||||
):
|
||||
"""Test that the central boiler state behavior with a climate with valve regulation"""
|
||||
|
||||
api = VersatileThermostatAPI.get_vtherm_api(hass)
|
||||
|
||||
climate1 = MockClimate(hass, "climate1", "theClimate1")
|
||||
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
title="TheOverClimateMockName",
|
||||
unique_id="uniqueId",
|
||||
data={
|
||||
CONF_NAME: "TheOverClimateMockName",
|
||||
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_CLIMATE,
|
||||
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
|
||||
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
|
||||
CONF_CYCLE_MIN: 5,
|
||||
CONF_TEMP_MIN: 8,
|
||||
CONF_TEMP_MAX: 18,
|
||||
"frost_temp": 10,
|
||||
"eco_temp": 17,
|
||||
"comfort_temp": 18,
|
||||
"boost_temp": 21,
|
||||
CONF_USE_WINDOW_FEATURE: False,
|
||||
CONF_USE_MOTION_FEATURE: False,
|
||||
CONF_USE_POWER_FEATURE: False,
|
||||
CONF_USE_PRESENCE_FEATURE: False,
|
||||
CONF_UNDERLYING_LIST: [climate1.entity_id],
|
||||
CONF_OPENING_DEGREE_LIST: ["number.mock_opening_degree"],
|
||||
CONF_CLOSING_DEGREE_LIST: [],
|
||||
CONF_OFFSET_CALIBRATION_LIST: [],
|
||||
CONF_AUTO_REGULATION_MODE: CONF_AUTO_REGULATION_VALVE,
|
||||
CONF_AUTO_REGULATION_DTEMP: 0,
|
||||
CONF_AUTO_REGULATION_PERIOD_MIN: 0,
|
||||
CONF_TPI_COEF_INT: 0.3,
|
||||
CONF_TPI_COEF_EXT: 0.1,
|
||||
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
|
||||
CONF_AUTO_FAN_MODE: CONF_AUTO_FAN_HIGH,
|
||||
CONF_AUTO_REGULATION_USE_DEVICE_TEMP: False,
|
||||
CONF_MINIMAL_ACTIVATION_DELAY: 30,
|
||||
CONF_SAFETY_DELAY_MIN: 5,
|
||||
CONF_SAFETY_MIN_ON_PERCENT: 0.3,
|
||||
CONF_SAFETY_DEFAULT_ON_PERCENT: 0.1,
|
||||
CONF_USE_MAIN_CENTRAL_CONFIG: True,
|
||||
CONF_USE_PRESETS_CENTRAL_CONFIG: True,
|
||||
CONF_USE_ADVANCED_CENTRAL_CONFIG: True,
|
||||
CONF_USED_BY_CENTRAL_BOILER: True,
|
||||
},
|
||||
)
|
||||
|
||||
open_degree_entity = MockNumber(hass, "mock_opening_degree", "Opening degree")
|
||||
open_degree_entity.set_native_value(0)
|
||||
|
||||
# mock_get_state will be called for each OPENING/CLOSING/OFFSET_CALIBRATION list
|
||||
mock_get_state_side_effect = SideEffects(
|
||||
{
|
||||
open_degree_entity.entity_id: State(
|
||||
open_degree_entity.entity_id,
|
||||
open_degree_entity.state,
|
||||
{"min": 0, "max": 100},
|
||||
),
|
||||
"number.mock_closing_degree": State(
|
||||
"number.mock_closing_degree", "0", {"min": 0, "max": 100}
|
||||
),
|
||||
"number.mock_offset_calibration": State(
|
||||
"number.mock_offset_calibration", "0", {"min": -12, "max": 12}
|
||||
),
|
||||
},
|
||||
State("unknown.entity_id", "unknown"),
|
||||
)
|
||||
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
|
||||
return_value=climate1,
|
||||
), patch(
|
||||
"homeassistant.core.StateMachine.get",
|
||||
side_effect=mock_get_state_side_effect.get_side_effects(),
|
||||
):
|
||||
entity: ThermostatOverClimate = await create_thermostat(
|
||||
hass, entry, "climate.theoverclimatemockname"
|
||||
)
|
||||
assert entity
|
||||
assert entity.name == "TheOverClimateMockName"
|
||||
assert entity.is_over_climate
|
||||
assert entity.underlying_entities[0].entity_id == "climate.climate1"
|
||||
|
||||
assert api.nb_active_device_for_boiler_threshold == 1
|
||||
assert api.nb_active_device_for_boiler == 0
|
||||
|
||||
nb_device_active_sensor: NbActiveDeviceForBoilerSensor = search_entity(
|
||||
hass, "sensor.nb_device_active_for_boiler", "sensor"
|
||||
)
|
||||
assert nb_device_active_sensor is not None
|
||||
assert nb_device_active_sensor.state == 0
|
||||
assert nb_device_active_sensor.active_device_ids == []
|
||||
|
||||
# Force the VTherm to heat
|
||||
tz = get_tz(hass) # pylint: disable=invalid-name
|
||||
now: datetime = datetime.now(tz=tz)
|
||||
entity._set_now(now)
|
||||
|
||||
await send_temperature_change_event(entity, 30, now)
|
||||
await send_ext_temperature_change_event(entity, 30, now)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await entity.async_set_hvac_mode(HVACMode.HEAT)
|
||||
await entity.async_set_preset_mode(PRESET_BOOST)
|
||||
|
||||
# the VTherm should not heat now
|
||||
assert entity.hvac_mode == HVACMode.HEAT
|
||||
assert entity.hvac_action == HVACAction.OFF
|
||||
assert entity.activable_underlying_entities[0]._percent_open == 0
|
||||
assert entity.device_actives == []
|
||||
|
||||
boiler_binary_sensor: CentralBoilerBinarySensor = search_entity(
|
||||
hass, "binary_sensor.central_boiler", "binary_sensor"
|
||||
)
|
||||
assert boiler_binary_sensor is not None
|
||||
assert boiler_binary_sensor.state == STATE_OFF
|
||||
|
||||
# 1. start a climate
|
||||
open_degree_entity.set_native_value(100)
|
||||
mock_get_state_side_effect = SideEffects(
|
||||
{
|
||||
open_degree_entity.entity_id: State(
|
||||
open_degree_entity.entity_id,
|
||||
open_degree_entity.state,
|
||||
{"min": 0, "max": 100},
|
||||
),
|
||||
"number.mock_closing_degree": State(
|
||||
"number.mock_closing_degree", "0", {"min": 0, "max": 100}
|
||||
),
|
||||
"number.mock_offset_calibration": State(
|
||||
"number.mock_offset_calibration", "0", {"min": -12, "max": 12}
|
||||
),
|
||||
},
|
||||
State("unknown.entity_id", "unknown"),
|
||||
)
|
||||
|
||||
with patch(
|
||||
"homeassistant.core.ServiceRegistry.async_call"
|
||||
) as mock_service_call, patch(
|
||||
"custom_components.versatile_thermostat.binary_sensor.send_vtherm_event"
|
||||
) as mock_send_event, patch(
|
||||
"homeassistant.core.StateMachine.get",
|
||||
side_effect=mock_get_state_side_effect.get_side_effects(),
|
||||
):
|
||||
now = now + timedelta(minutes=1)
|
||||
entity._set_now(now)
|
||||
|
||||
await send_temperature_change_event(entity, 10, now)
|
||||
# we have to simulate the climate also else the test don't work
|
||||
climate1.set_hvac_mode(HVACMode.HEAT)
|
||||
climate1.set_hvac_action(HVACAction.HEATING)
|
||||
climate1.async_write_ha_state()
|
||||
open_degree_entity.set_native_value(100)
|
||||
# Wait for state event propagation
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert entity.hvac_action == HVACAction.HEATING
|
||||
assert entity.device_actives == ["number.mock_opening_degree"]
|
||||
|
||||
assert api.nb_active_device_for_boiler == 1
|
||||
assert boiler_binary_sensor.state == STATE_ON
|
||||
|
||||
assert nb_device_active_sensor.state == 1
|
||||
assert nb_device_active_sensor.active_device_ids == [
|
||||
"number.mock_opening_degree",
|
||||
]
|
||||
|
||||
assert mock_service_call.call_count >= 1
|
||||
mock_service_call.assert_has_calls(
|
||||
[
|
||||
call.service_call(
|
||||
"switch",
|
||||
"turn_on",
|
||||
service_data={},
|
||||
target={"entity_id": "switch.pompe_chaudiere"},
|
||||
),
|
||||
]
|
||||
)
|
||||
assert mock_send_event.call_count >= 1
|
||||
mock_send_event.assert_has_calls(
|
||||
[
|
||||
call.send_vtherm_event(
|
||||
hass=hass,
|
||||
event_type=EventType.CENTRAL_BOILER_EVENT,
|
||||
entity=api.central_boiler_entity,
|
||||
data={"central_boiler": True},
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
# 2. stop a climate
|
||||
open_degree_entity.set_native_value(0)
|
||||
mock_get_state_side_effect = SideEffects(
|
||||
{
|
||||
open_degree_entity.entity_id: State(
|
||||
open_degree_entity.entity_id,
|
||||
open_degree_entity.state,
|
||||
{"min": 0, "max": 100},
|
||||
),
|
||||
"number.mock_closing_degree": State(
|
||||
"number.mock_closing_degree", "0", {"min": 0, "max": 100}
|
||||
),
|
||||
"number.mock_offset_calibration": State(
|
||||
"number.mock_offset_calibration", "0", {"min": -12, "max": 12}
|
||||
),
|
||||
},
|
||||
State("unknown.entity_id", "unknown"),
|
||||
)
|
||||
with patch(
|
||||
"homeassistant.core.ServiceRegistry.async_call"
|
||||
) as mock_service_call, patch(
|
||||
"custom_components.versatile_thermostat.binary_sensor.send_vtherm_event"
|
||||
) as mock_send_event, patch(
|
||||
"homeassistant.core.StateMachine.get",
|
||||
side_effect=mock_get_state_side_effect.get_side_effects(),
|
||||
):
|
||||
await send_temperature_change_event(entity, 25, now)
|
||||
climate1.set_hvac_mode(HVACMode.HEAT)
|
||||
climate1.set_hvac_action(HVACAction.IDLE)
|
||||
climate1.async_write_ha_state()
|
||||
open_degree_entity.set_native_value(0)
|
||||
# Wait for state event propagation
|
||||
await asyncio.sleep(0.5)
|
||||
|
||||
assert entity.hvac_action == HVACAction.OFF
|
||||
assert entity.device_actives == []
|
||||
|
||||
assert mock_service_call.call_count >= 1
|
||||
mock_service_call.assert_has_calls(
|
||||
[
|
||||
call(
|
||||
"switch",
|
||||
"turn_off",
|
||||
service_data={},
|
||||
target={"entity_id": "switch.pompe_chaudiere"},
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
assert mock_send_event.call_count >= 1
|
||||
mock_send_event.assert_has_calls(
|
||||
[
|
||||
call.send_vtherm_event(
|
||||
hass=hass,
|
||||
event_type=EventType.CENTRAL_BOILER_EVENT,
|
||||
entity=api.central_boiler_entity,
|
||||
data={"central_boiler": False},
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
assert api.nb_active_device_for_boiler == 0
|
||||
assert boiler_binary_sensor.state == STATE_OFF
|
||||
|
||||
assert nb_device_active_sensor.state == 0
|
||||
assert nb_device_active_sensor.active_device_ids == []
|
||||
|
||||
entity.remove_thermostat()
|
||||
|
||||
|
||||
@@ -895,9 +1255,9 @@ async def test_bug_339(
|
||||
CONF_USE_PRESENCE_FEATURE: False,
|
||||
CONF_CLIMATE: climate1.entity_id,
|
||||
CONF_MINIMAL_ACTIVATION_DELAY: 30,
|
||||
CONF_SECURITY_DELAY_MIN: 5,
|
||||
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
|
||||
CONF_SECURITY_DEFAULT_ON_PERCENT: 0.1,
|
||||
CONF_SAFETY_DELAY_MIN: 5,
|
||||
CONF_SAFETY_MIN_ON_PERCENT: 0.3,
|
||||
CONF_SAFETY_DEFAULT_ON_PERCENT: 0.1,
|
||||
CONF_USE_MAIN_CENTRAL_CONFIG: True,
|
||||
CONF_USE_PRESETS_CENTRAL_CONFIG: True,
|
||||
CONF_USE_ADVANCED_CENTRAL_CONFIG: True,
|
||||
|
||||
@@ -67,9 +67,9 @@ async def test_add_a_central_config(hass: HomeAssistant, skip_hass_states_is_sta
|
||||
CONF_MAX_POWER_SENSOR: "sensor.mock_central_max_power_sensor",
|
||||
CONF_PRESET_POWER: 14,
|
||||
CONF_MINIMAL_ACTIVATION_DELAY: 11,
|
||||
CONF_SECURITY_DELAY_MIN: 61,
|
||||
CONF_SECURITY_MIN_ON_PERCENT: 0.5,
|
||||
CONF_SECURITY_DEFAULT_ON_PERCENT: 0.2,
|
||||
CONF_SAFETY_DELAY_MIN: 61,
|
||||
CONF_SAFETY_MIN_ON_PERCENT: 0.5,
|
||||
CONF_SAFETY_DEFAULT_ON_PERCENT: 0.2,
|
||||
CONF_USE_CENTRAL_BOILER_FEATURE: False,
|
||||
},
|
||||
)
|
||||
@@ -135,9 +135,9 @@ async def test_minimal_over_switch_wo_central_config(
|
||||
CONF_TPI_COEF_INT: 0.3,
|
||||
CONF_TPI_COEF_EXT: 0.01,
|
||||
CONF_MINIMAL_ACTIVATION_DELAY: 30,
|
||||
CONF_SECURITY_DELAY_MIN: 5,
|
||||
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
|
||||
CONF_SECURITY_DEFAULT_ON_PERCENT: 0.1,
|
||||
CONF_SAFETY_DELAY_MIN: 5,
|
||||
CONF_SAFETY_MIN_ON_PERCENT: 0.3,
|
||||
CONF_SAFETY_DEFAULT_ON_PERCENT: 0.1,
|
||||
# CONF_WINDOW_AUTO_OPEN_THRESHOLD: 0.1,
|
||||
# CONF_WINDOW_AUTO_CLOSE_THRESHOLD: 0.1,
|
||||
# CONF_WINDOW_AUTO_MAX_DURATION: 0, # Should be 0 for test
|
||||
@@ -167,16 +167,16 @@ async def test_minimal_over_switch_wo_central_config(
|
||||
assert entity.max_temp == 18
|
||||
assert entity.target_temperature_step == 0.3
|
||||
assert entity.preset_modes == ["none", "frost", "eco", "comfort", "boost"]
|
||||
assert entity.is_window_auto_enabled is False
|
||||
assert entity.window_manager.is_window_auto_configured is False
|
||||
assert entity.nb_underlying_entities == 1
|
||||
assert entity.underlying_entity_id(0) == "switch.mock_switch"
|
||||
assert entity.proportional_algorithm is not None
|
||||
assert entity.proportional_algorithm._tpi_coef_int == 0.3
|
||||
assert entity.proportional_algorithm._tpi_coef_ext == 0.01
|
||||
assert entity.proportional_algorithm._minimal_activation_delay == 30
|
||||
assert entity._security_delay_min == 5
|
||||
assert entity._security_min_on_percent == 0.3
|
||||
assert entity._security_default_on_percent == 0.1
|
||||
assert entity.safety_manager.safety_delay_min == 5
|
||||
assert entity.safety_manager.safety_min_on_percent == 0.3
|
||||
assert entity.safety_manager.safety_default_on_percent == 0.1
|
||||
assert entity.is_inversed
|
||||
|
||||
entity.remove_thermostat()
|
||||
@@ -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,27 +214,19 @@ 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,
|
||||
CONF_TPI_COEF_EXT: 0.01,
|
||||
CONF_MINIMAL_ACTIVATION_DELAY: 30,
|
||||
CONF_SECURITY_DELAY_MIN: 5,
|
||||
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
|
||||
CONF_SECURITY_DEFAULT_ON_PERCENT: 0.1,
|
||||
CONF_SAFETY_DELAY_MIN: 5,
|
||||
CONF_SAFETY_MIN_ON_PERCENT: 0.3,
|
||||
CONF_SAFETY_DEFAULT_ON_PERCENT: 0.1,
|
||||
CONF_WINDOW_SENSOR: "binary_sensor.mock_window_sensor",
|
||||
CONF_WINDOW_DELAY: 30,
|
||||
CONF_WINDOW_AUTO_OPEN_THRESHOLD: 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"
|
||||
@@ -274,28 +276,45 @@ async def test_full_over_switch_wo_central_config(
|
||||
assert entity.proportional_algorithm._tpi_coef_int == 0.3
|
||||
assert entity.proportional_algorithm._tpi_coef_ext == 0.01
|
||||
assert entity.proportional_algorithm._minimal_activation_delay == 30
|
||||
assert entity._security_delay_min == 5
|
||||
assert entity._security_min_on_percent == 0.3
|
||||
assert entity._security_default_on_percent == 0.1
|
||||
assert entity.safety_manager.safety_delay_min == 5
|
||||
assert entity.safety_manager.safety_min_on_percent == 0.3
|
||||
assert entity.safety_manager.safety_default_on_percent == 0.1
|
||||
assert entity.is_inversed is False
|
||||
|
||||
assert entity.is_window_auto_enabled is False # we have an entity_id
|
||||
assert entity._window_sensor_entity_id == "binary_sensor.mock_window_sensor"
|
||||
assert entity._window_delay_sec == 30
|
||||
assert entity._window_auto_close_threshold == 0.1
|
||||
assert entity._window_auto_open_threshold == 3
|
||||
assert entity._window_auto_max_duration == 5
|
||||
assert (
|
||||
entity.window_manager.is_window_auto_configured is False
|
||||
) # we have an entity_id
|
||||
assert (
|
||||
entity.window_manager._window_sensor_entity_id
|
||||
== "binary_sensor.mock_window_sensor"
|
||||
)
|
||||
assert entity.window_manager.window_delay_sec == 30
|
||||
assert entity.window_manager.window_auto_close_threshold == 0.1
|
||||
assert entity.window_manager.window_auto_open_threshold == 3
|
||||
assert entity.window_manager.window_auto_max_duration == 5
|
||||
|
||||
assert entity._motion_sensor_entity_id == "binary_sensor.mock_motion_sensor"
|
||||
assert entity._motion_delay_sec == 10
|
||||
assert entity._motion_off_delay_sec == 29
|
||||
assert entity._motion_preset == "comfort"
|
||||
assert entity._no_motion_preset == "eco"
|
||||
assert (
|
||||
entity.motion_manager.motion_sensor_entity_id
|
||||
== "binary_sensor.mock_motion_sensor"
|
||||
)
|
||||
assert entity.motion_manager.motion_delay_sec == 10
|
||||
assert entity.motion_manager.motion_off_delay_sec == 29
|
||||
assert entity.motion_manager.motion_preset == "comfort"
|
||||
assert entity.motion_manager.no_motion_preset == "eco"
|
||||
|
||||
assert entity._power_sensor_entity_id == "sensor.mock_power_sensor"
|
||||
assert entity._max_power_sensor_entity_id == "sensor.mock_max_power_sensor"
|
||||
assert (
|
||||
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 entity._presence_sensor_entity_id == "binary_sensor.mock_presence_sensor"
|
||||
assert (
|
||||
entity._presence_manager.presence_sensor_entity_id
|
||||
== "binary_sensor.mock_presence_sensor"
|
||||
)
|
||||
|
||||
entity.remove_thermostat()
|
||||
|
||||
@@ -303,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
|
||||
@@ -320,23 +339,19 @@ 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,
|
||||
CONF_TPI_COEF_EXT: 0.01,
|
||||
CONF_MINIMAL_ACTIVATION_DELAY: 30,
|
||||
CONF_SECURITY_DELAY_MIN: 5,
|
||||
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
|
||||
CONF_SECURITY_DEFAULT_ON_PERCENT: 0.1,
|
||||
CONF_SAFETY_DELAY_MIN: 5,
|
||||
CONF_SAFETY_MIN_ON_PERCENT: 0.3,
|
||||
CONF_SAFETY_DEFAULT_ON_PERCENT: 0.1,
|
||||
CONF_WINDOW_SENSOR: "binary_sensor.mock_window_sensor",
|
||||
CONF_WINDOW_DELAY: 30,
|
||||
CONF_WINDOW_AUTO_OPEN_THRESHOLD: 3,
|
||||
@@ -347,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,
|
||||
@@ -387,29 +400,44 @@ async def test_full_over_switch_with_central_config(
|
||||
assert entity.proportional_algorithm._tpi_coef_int == 0.5
|
||||
assert entity.proportional_algorithm._tpi_coef_ext == 0.02
|
||||
assert entity.proportional_algorithm._minimal_activation_delay == 11
|
||||
assert entity._security_delay_min == 61
|
||||
assert entity._security_min_on_percent == 0.5
|
||||
assert entity._security_default_on_percent == 0.2
|
||||
assert entity.safety_manager.safety_delay_min == 61
|
||||
assert entity.safety_manager.safety_min_on_percent == 0.5
|
||||
assert entity.safety_manager.safety_default_on_percent == 0.2
|
||||
assert entity.is_inversed is False
|
||||
|
||||
# We have an entity so window auto is not enabled
|
||||
assert entity.is_window_auto_enabled is False
|
||||
assert entity._window_sensor_entity_id == "binary_sensor.mock_window_sensor"
|
||||
assert entity._window_delay_sec == 15
|
||||
assert entity._window_auto_close_threshold == 1
|
||||
assert entity._window_auto_open_threshold == 4
|
||||
assert entity._window_auto_max_duration == 31
|
||||
assert entity.window_manager.is_window_auto_configured is False
|
||||
assert (
|
||||
entity.window_manager._window_sensor_entity_id
|
||||
== "binary_sensor.mock_window_sensor"
|
||||
)
|
||||
assert entity.window_manager.window_delay_sec == 15
|
||||
assert entity.window_manager.window_auto_close_threshold == 1
|
||||
assert entity.window_manager.window_auto_open_threshold == 4
|
||||
assert entity.window_manager.window_auto_max_duration == 31
|
||||
|
||||
assert entity._motion_sensor_entity_id == "binary_sensor.mock_motion_sensor"
|
||||
assert entity._motion_delay_sec == 31
|
||||
assert entity._motion_off_delay_sec == 301
|
||||
assert entity._motion_preset == "boost"
|
||||
assert entity._no_motion_preset == "frost"
|
||||
assert (
|
||||
entity.motion_manager.motion_sensor_entity_id
|
||||
== "binary_sensor.mock_motion_sensor"
|
||||
)
|
||||
assert entity.motion_manager.motion_delay_sec == 31
|
||||
assert entity.motion_manager.motion_off_delay_sec == 301
|
||||
assert entity.motion_manager.motion_preset == "boost"
|
||||
assert entity.motion_manager.no_motion_preset == "frost"
|
||||
|
||||
assert entity._power_sensor_entity_id == "sensor.mock_power_sensor"
|
||||
assert entity._max_power_sensor_entity_id == "sensor.mock_max_power_sensor"
|
||||
assert (
|
||||
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 entity._presence_sensor_entity_id == "binary_sensor.mock_presence_sensor"
|
||||
assert (
|
||||
entity._presence_manager.presence_sensor_entity_id
|
||||
== "binary_sensor.mock_presence_sensor"
|
||||
)
|
||||
|
||||
entity.remove_thermostat()
|
||||
|
||||
@@ -424,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(
|
||||
@@ -469,7 +497,7 @@ async def test_migration_of_central_config(
|
||||
central_config_entry = MockConfigEntry(
|
||||
version=CONFIG_VERSION,
|
||||
# An old minor version
|
||||
minor_version=1,
|
||||
minor_version=0,
|
||||
domain=DOMAIN,
|
||||
title="TheCentralConfigMockName",
|
||||
unique_id="centralConfigUniqueId",
|
||||
@@ -483,9 +511,9 @@ async def test_migration_of_central_config(
|
||||
CONF_TPI_COEF_INT: 0.5,
|
||||
CONF_TPI_COEF_EXT: 0.02,
|
||||
CONF_MINIMAL_ACTIVATION_DELAY: 11,
|
||||
CONF_SECURITY_DELAY_MIN: 61,
|
||||
CONF_SECURITY_MIN_ON_PERCENT: 0.5,
|
||||
CONF_SECURITY_DEFAULT_ON_PERCENT: 0.2,
|
||||
CONF_SAFETY_DELAY_MIN: 61,
|
||||
CONF_SAFETY_MIN_ON_PERCENT: 0.5,
|
||||
CONF_SAFETY_DEFAULT_ON_PERCENT: 0.2,
|
||||
# The old central_boiler parameter
|
||||
"add_central_boiler_control": True,
|
||||
CONF_CENTRAL_BOILER_ACTIVATION_SRV: "switch.pompe_chaudiere/switch.turn_on",
|
||||
|
||||
@@ -56,9 +56,9 @@ async def test_config_with_central_mode_true(
|
||||
CONF_TPI_COEF_INT: 0.3,
|
||||
CONF_TPI_COEF_EXT: 0.01,
|
||||
CONF_MINIMAL_ACTIVATION_DELAY: 30,
|
||||
CONF_SECURITY_DELAY_MIN: 5,
|
||||
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
|
||||
CONF_SECURITY_DEFAULT_ON_PERCENT: 0.1,
|
||||
CONF_SAFETY_DELAY_MIN: 5,
|
||||
CONF_SAFETY_MIN_ON_PERCENT: 0.3,
|
||||
CONF_SAFETY_DEFAULT_ON_PERCENT: 0.1,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -103,9 +103,9 @@ async def test_config_with_central_mode_false(
|
||||
CONF_USE_PRESENCE_FEATURE: False,
|
||||
CONF_CLIMATE: "climate.mock_climate",
|
||||
CONF_MINIMAL_ACTIVATION_DELAY: 30,
|
||||
CONF_SECURITY_DELAY_MIN: 5,
|
||||
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
|
||||
CONF_SECURITY_DEFAULT_ON_PERCENT: 0.1,
|
||||
CONF_SAFETY_DELAY_MIN: 5,
|
||||
CONF_SAFETY_MIN_ON_PERCENT: 0.3,
|
||||
CONF_SAFETY_DEFAULT_ON_PERCENT: 0.1,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -153,9 +153,9 @@ async def test_config_with_central_mode_none(
|
||||
CONF_TPI_COEF_INT: 0.3,
|
||||
CONF_TPI_COEF_EXT: 0.01,
|
||||
CONF_MINIMAL_ACTIVATION_DELAY: 30,
|
||||
CONF_SECURITY_DELAY_MIN: 5,
|
||||
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
|
||||
CONF_SECURITY_DEFAULT_ON_PERCENT: 0.1,
|
||||
CONF_SAFETY_DELAY_MIN: 5,
|
||||
CONF_SAFETY_MIN_ON_PERCENT: 0.3,
|
||||
CONF_SAFETY_DEFAULT_ON_PERCENT: 0.1,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -205,9 +205,9 @@ async def test_switch_change_central_mode_true(
|
||||
CONF_TPI_COEF_INT: 0.3,
|
||||
CONF_TPI_COEF_EXT: 0.01,
|
||||
CONF_MINIMAL_ACTIVATION_DELAY: 30,
|
||||
CONF_SECURITY_DELAY_MIN: 5,
|
||||
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
|
||||
CONF_SECURITY_DEFAULT_ON_PERCENT: 0.1,
|
||||
CONF_SAFETY_DELAY_MIN: 5,
|
||||
CONF_SAFETY_MIN_ON_PERCENT: 0.3,
|
||||
CONF_SAFETY_DEFAULT_ON_PERCENT: 0.1,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -347,9 +347,9 @@ async def test_switch_ac_change_central_mode_true(
|
||||
CONF_TPI_COEF_INT: 0.3,
|
||||
CONF_TPI_COEF_EXT: 0.01,
|
||||
CONF_MINIMAL_ACTIVATION_DELAY: 30,
|
||||
CONF_SECURITY_DELAY_MIN: 5,
|
||||
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
|
||||
CONF_SECURITY_DEFAULT_ON_PERCENT: 0.1,
|
||||
CONF_SAFETY_DELAY_MIN: 5,
|
||||
CONF_SAFETY_MIN_ON_PERCENT: 0.3,
|
||||
CONF_SAFETY_DEFAULT_ON_PERCENT: 0.1,
|
||||
CONF_AC_MODE: True,
|
||||
},
|
||||
)
|
||||
@@ -482,9 +482,9 @@ async def test_climate_ac_change_central_mode_false(
|
||||
CONF_USE_PRESENCE_FEATURE: False,
|
||||
CONF_CLIMATE: "climate.mock_climate",
|
||||
CONF_MINIMAL_ACTIVATION_DELAY: 30,
|
||||
CONF_SECURITY_DELAY_MIN: 5,
|
||||
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
|
||||
CONF_SECURITY_DEFAULT_ON_PERCENT: 0.1,
|
||||
CONF_SAFETY_DELAY_MIN: 5,
|
||||
CONF_SAFETY_MIN_ON_PERCENT: 0.3,
|
||||
CONF_SAFETY_DEFAULT_ON_PERCENT: 0.1,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -624,9 +624,9 @@ async def test_climate_ac_only_change_central_mode_true(
|
||||
CONF_USE_PRESENCE_FEATURE: False,
|
||||
CONF_CLIMATE: "climate.mock_climate",
|
||||
CONF_MINIMAL_ACTIVATION_DELAY: 30,
|
||||
CONF_SECURITY_DELAY_MIN: 5,
|
||||
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
|
||||
CONF_SECURITY_DEFAULT_ON_PERCENT: 0.1,
|
||||
CONF_SAFETY_DELAY_MIN: 5,
|
||||
CONF_SAFETY_MIN_ON_PERCENT: 0.3,
|
||||
CONF_SAFETY_DEFAULT_ON_PERCENT: 0.1,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -778,9 +778,9 @@ async def test_switch_change_central_mode_true_with_window(
|
||||
CONF_TPI_COEF_INT: 0.3,
|
||||
CONF_TPI_COEF_EXT: 0.01,
|
||||
CONF_MINIMAL_ACTIVATION_DELAY: 30,
|
||||
CONF_SECURITY_DELAY_MIN: 5,
|
||||
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
|
||||
CONF_SECURITY_DEFAULT_ON_PERCENT: 0.1,
|
||||
CONF_SAFETY_DELAY_MIN: 5,
|
||||
CONF_SAFETY_MIN_ON_PERCENT: 0.3,
|
||||
CONF_SAFETY_DEFAULT_ON_PERCENT: 0.1,
|
||||
CONF_WINDOW_SENSOR: "binary_sensor.window_sensor",
|
||||
CONF_WINDOW_DELAY: 0, # To be not obliged to wait
|
||||
CONF_MOTION_SENSOR: "input_boolean.motion_sensor",
|
||||
@@ -816,7 +816,7 @@ async def test_switch_change_central_mode_true_with_window(
|
||||
|
||||
assert entity.hvac_mode == HVACMode.HEAT
|
||||
assert entity.preset_mode == PRESET_ACTIVITY
|
||||
assert entity.window_state is STATE_OFF
|
||||
assert entity.window_state is STATE_UNKNOWN
|
||||
|
||||
# 2 Open the window
|
||||
with patch(
|
||||
@@ -935,9 +935,9 @@ async def test_switch_change_central_mode_true_with_cool_only_and_window(
|
||||
CONF_TPI_COEF_INT: 0.3,
|
||||
CONF_TPI_COEF_EXT: 0.01,
|
||||
CONF_MINIMAL_ACTIVATION_DELAY: 30,
|
||||
CONF_SECURITY_DELAY_MIN: 5,
|
||||
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
|
||||
CONF_SECURITY_DEFAULT_ON_PERCENT: 0.1,
|
||||
CONF_SAFETY_DELAY_MIN: 5,
|
||||
CONF_SAFETY_MIN_ON_PERCENT: 0.3,
|
||||
CONF_SAFETY_DEFAULT_ON_PERCENT: 0.1,
|
||||
CONF_WINDOW_SENSOR: "binary_sensor.window_sensor",
|
||||
CONF_WINDOW_DELAY: 0, # To be not obliged to wait
|
||||
CONF_MOTION_SENSOR: "input_boolean.motion_sensor",
|
||||
@@ -973,7 +973,7 @@ async def test_switch_change_central_mode_true_with_cool_only_and_window(
|
||||
|
||||
assert entity.hvac_mode == HVACMode.HEAT
|
||||
assert entity.preset_mode == PRESET_ACTIVITY
|
||||
assert entity.window_state is STATE_OFF
|
||||
assert entity.window_state is STATE_UNKNOWN
|
||||
|
||||
# 2 Change central_mode to COOL_ONLY
|
||||
with patch("homeassistant.core.ServiceRegistry.async_call"):
|
||||
|
||||
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
|
||||
@@ -470,9 +470,9 @@ async def test_user_config_flow_over_climate(
|
||||
result["flow_id"],
|
||||
user_input={
|
||||
CONF_MINIMAL_ACTIVATION_DELAY: 10,
|
||||
CONF_SECURITY_DELAY_MIN: 5,
|
||||
CONF_SECURITY_MIN_ON_PERCENT: 0.4,
|
||||
CONF_SECURITY_DEFAULT_ON_PERCENT: 0.3,
|
||||
CONF_SAFETY_DELAY_MIN: 5,
|
||||
CONF_SAFETY_MIN_ON_PERCENT: 0.4,
|
||||
CONF_SAFETY_DEFAULT_ON_PERCENT: 0.3,
|
||||
},
|
||||
)
|
||||
assert result["type"] == FlowResultType.MENU
|
||||
@@ -496,9 +496,9 @@ async def test_user_config_flow_over_climate(
|
||||
"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_SECURITY_DELAY_MIN: 5,
|
||||
CONF_SECURITY_MIN_ON_PERCENT: 0.4,
|
||||
CONF_SECURITY_DEFAULT_ON_PERCENT: 0.3,
|
||||
CONF_SAFETY_DELAY_MIN: 5,
|
||||
CONF_SAFETY_MIN_ON_PERCENT: 0.4,
|
||||
CONF_SAFETY_DEFAULT_ON_PERCENT: 0.3,
|
||||
} | MOCK_DEFAULT_FEATURE_CONFIG | {
|
||||
CONF_USE_MAIN_CENTRAL_CONFIG: False,
|
||||
CONF_USE_PRESETS_CENTRAL_CONFIG: False,
|
||||
@@ -1077,9 +1077,9 @@ async def test_user_config_flow_over_climate_auto_start_stop(
|
||||
result["flow_id"],
|
||||
user_input={
|
||||
CONF_MINIMAL_ACTIVATION_DELAY: 10,
|
||||
CONF_SECURITY_DELAY_MIN: 5,
|
||||
CONF_SECURITY_MIN_ON_PERCENT: 0.4,
|
||||
CONF_SECURITY_DEFAULT_ON_PERCENT: 0.3,
|
||||
CONF_SAFETY_DELAY_MIN: 5,
|
||||
CONF_SAFETY_MIN_ON_PERCENT: 0.4,
|
||||
CONF_SAFETY_DEFAULT_ON_PERCENT: 0.3,
|
||||
},
|
||||
)
|
||||
assert result["type"] == FlowResultType.MENU
|
||||
@@ -1104,9 +1104,9 @@ async def test_user_config_flow_over_climate_auto_start_stop(
|
||||
"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_SECURITY_DELAY_MIN: 5,
|
||||
CONF_SECURITY_MIN_ON_PERCENT: 0.4,
|
||||
CONF_SECURITY_DEFAULT_ON_PERCENT: 0.3,
|
||||
CONF_SAFETY_DELAY_MIN: 5,
|
||||
CONF_SAFETY_MIN_ON_PERCENT: 0.4,
|
||||
CONF_SAFETY_DEFAULT_ON_PERCENT: 0.3,
|
||||
} | MOCK_DEFAULT_FEATURE_CONFIG | {
|
||||
CONF_USE_MAIN_CENTRAL_CONFIG: False,
|
||||
CONF_USE_TPI_CENTRAL_CONFIG: False,
|
||||
@@ -1274,9 +1274,9 @@ async def test_user_config_flow_over_switch_bug_552_tpi(
|
||||
result["flow_id"],
|
||||
user_input={
|
||||
CONF_MINIMAL_ACTIVATION_DELAY: 10,
|
||||
CONF_SECURITY_DELAY_MIN: 5,
|
||||
CONF_SECURITY_MIN_ON_PERCENT: 0.4,
|
||||
CONF_SECURITY_DEFAULT_ON_PERCENT: 0.3,
|
||||
CONF_SAFETY_DELAY_MIN: 5,
|
||||
CONF_SAFETY_MIN_ON_PERCENT: 0.4,
|
||||
CONF_SAFETY_DEFAULT_ON_PERCENT: 0.3,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -1359,9 +1359,9 @@ async def test_user_config_flow_over_switch_bug_552_tpi(
|
||||
CONF_TEMP_MAX: 30,
|
||||
CONF_STEP_TEMPERATURE: 0.5,
|
||||
CONF_MINIMAL_ACTIVATION_DELAY: 10,
|
||||
CONF_SECURITY_DELAY_MIN: 5,
|
||||
CONF_SECURITY_MIN_ON_PERCENT: 0.4,
|
||||
CONF_SECURITY_DEFAULT_ON_PERCENT: 0.3,
|
||||
CONF_SAFETY_DELAY_MIN: 5,
|
||||
CONF_SAFETY_MIN_ON_PERCENT: 0.4,
|
||||
CONF_SAFETY_DEFAULT_ON_PERCENT: 0.3,
|
||||
CONF_USE_MAIN_CENTRAL_CONFIG: False,
|
||||
CONF_USE_TPI_CENTRAL_CONFIG: False,
|
||||
CONF_USE_PRESETS_CENTRAL_CONFIG: False,
|
||||
@@ -1388,8 +1388,7 @@ async def test_user_config_flow_over_switch_bug_552_tpi(
|
||||
|
||||
|
||||
# @pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
# @pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
# @pytest.mark.skip
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_user_config_flow_over_climate_valve(
|
||||
hass: HomeAssistant, skip_hass_states_get
|
||||
): # pylint: disable=unused-argument
|
||||
@@ -1581,6 +1580,7 @@ async def test_user_config_flow_over_climate_valve(
|
||||
CONF_OFFSET_CALIBRATION_LIST: ["number.offset_calibration1"],
|
||||
CONF_OPENING_DEGREE_LIST: ["number.opening_degree1"],
|
||||
CONF_CLOSING_DEGREE_LIST: ["number.closing_degree1"],
|
||||
CONF_MIN_OPENING_DEGREES: "10, 20,0",
|
||||
},
|
||||
)
|
||||
assert result["type"] == FlowResultType.FORM
|
||||
@@ -1619,6 +1619,7 @@ async def test_user_config_flow_over_climate_valve(
|
||||
"number.opening_degree2",
|
||||
],
|
||||
CONF_CLOSING_DEGREE_LIST: [],
|
||||
CONF_MIN_OPENING_DEGREES: "10, 20,0",
|
||||
},
|
||||
)
|
||||
assert result["type"] == FlowResultType.MENU
|
||||
@@ -1656,9 +1657,9 @@ async def test_user_config_flow_over_climate_valve(
|
||||
result["flow_id"],
|
||||
user_input={
|
||||
CONF_MINIMAL_ACTIVATION_DELAY: 10,
|
||||
CONF_SECURITY_DELAY_MIN: 5,
|
||||
CONF_SECURITY_MIN_ON_PERCENT: 0.4,
|
||||
CONF_SECURITY_DEFAULT_ON_PERCENT: 0.3,
|
||||
CONF_SAFETY_DELAY_MIN: 5,
|
||||
CONF_SAFETY_MIN_ON_PERCENT: 0.4,
|
||||
CONF_SAFETY_DEFAULT_ON_PERCENT: 0.3,
|
||||
},
|
||||
)
|
||||
assert result["type"] == FlowResultType.MENU
|
||||
@@ -1684,9 +1685,9 @@ async def test_user_config_flow_over_climate_valve(
|
||||
"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_SECURITY_DELAY_MIN: 5,
|
||||
CONF_SECURITY_MIN_ON_PERCENT: 0.4,
|
||||
CONF_SECURITY_DEFAULT_ON_PERCENT: 0.3,
|
||||
CONF_SAFETY_DELAY_MIN: 5,
|
||||
CONF_SAFETY_MIN_ON_PERCENT: 0.4,
|
||||
CONF_SAFETY_DEFAULT_ON_PERCENT: 0.3,
|
||||
} | MOCK_DEFAULT_FEATURE_CONFIG | {
|
||||
CONF_USE_MAIN_CENTRAL_CONFIG: False,
|
||||
CONF_USE_PRESETS_CENTRAL_CONFIG: False,
|
||||
@@ -1715,6 +1716,7 @@ async def test_user_config_flow_over_climate_valve(
|
||||
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
|
||||
CONF_TPI_COEF_INT: 0.3,
|
||||
CONF_TPI_COEF_EXT: 0.1,
|
||||
CONF_MIN_OPENING_DEGREES: "10, 20,0",
|
||||
}
|
||||
assert result["result"]
|
||||
assert result["result"].domain == DOMAIN
|
||||
|
||||
@@ -42,8 +42,8 @@ async def test_inverted_switch(hass: HomeAssistant, skip_hass_states_is_state):
|
||||
CONF_TPI_COEF_INT: 0.3,
|
||||
CONF_TPI_COEF_EXT: 0.01,
|
||||
CONF_MINIMAL_ACTIVATION_DELAY: 30,
|
||||
CONF_SECURITY_DELAY_MIN: 5,
|
||||
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
|
||||
CONF_SAFETY_DELAY_MIN: 5,
|
||||
CONF_SAFETY_MIN_ON_PERCENT: 0.3,
|
||||
CONF_WINDOW_AUTO_OPEN_THRESHOLD: 0.1,
|
||||
CONF_WINDOW_AUTO_CLOSE_THRESHOLD: 0.1,
|
||||
CONF_WINDOW_AUTO_MAX_DURATION: 0, # Should be 0 for test
|
||||
|
||||
@@ -25,6 +25,12 @@ async def test_last_seen_feature(hass: HomeAssistant, skip_hass_states_is_state)
|
||||
"""
|
||||
|
||||
tz = get_tz(hass) # pylint: disable=invalid-name
|
||||
temps = {
|
||||
"frost": 7,
|
||||
"eco": 17,
|
||||
"comfort": 18,
|
||||
"boost": 19,
|
||||
}
|
||||
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
@@ -39,22 +45,18 @@ async def test_last_seen_feature(hass: HomeAssistant, skip_hass_states_is_state)
|
||||
"cycle_min": 5,
|
||||
"temp_min": 15,
|
||||
"temp_max": 30,
|
||||
"frost_temp": 7,
|
||||
"eco_temp": 17,
|
||||
"comfort_temp": 18,
|
||||
"boost_temp": 19,
|
||||
"use_window_feature": False,
|
||||
"use_motion_feature": False,
|
||||
"use_power_feature": False,
|
||||
"use_presence_feature": False,
|
||||
"heater_entity_id": "switch.mock_switch",
|
||||
CONF_UNDERLYING_LIST: ["switch.mock_switch"],
|
||||
"proportional_function": "tpi",
|
||||
"tpi_coef_int": 0.3,
|
||||
"tpi_coef_ext": 0.01,
|
||||
"minimal_activation_delay": 30,
|
||||
"security_delay_min": 5, # 5 minutes
|
||||
"security_min_on_percent": 0.2,
|
||||
"security_default_on_percent": 0.1,
|
||||
CONF_SAFETY_DELAY_MIN: 5, # 5 minutes
|
||||
CONF_SAFETY_MIN_ON_PERCENT: 0.2,
|
||||
CONF_SAFETY_DEFAULT_ON_PERCENT: 0.1,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -65,8 +67,10 @@ async def test_last_seen_feature(hass: HomeAssistant, skip_hass_states_is_state)
|
||||
)
|
||||
assert entity
|
||||
|
||||
assert entity._security_state is False
|
||||
assert entity.preset_mode is not PRESET_SECURITY
|
||||
await set_all_climate_preset_temp(hass, entity, temps, "theoverswitchmockname")
|
||||
|
||||
assert entity.safety_manager.is_safety_detected is False
|
||||
assert entity.preset_mode is not PRESET_SAFETY
|
||||
assert entity._last_ext_temperature_measure is not None
|
||||
assert entity._last_temperature_measure is not None
|
||||
assert (entity._last_temperature_measure.astimezone(tz) - now).total_seconds() < 1
|
||||
@@ -84,6 +88,8 @@ async def test_last_seen_feature(hass: HomeAssistant, skip_hass_states_is_state)
|
||||
await entity.async_set_hvac_mode(HVACMode.HEAT)
|
||||
assert entity.hvac_mode == HVACMode.HEAT
|
||||
|
||||
last_change_time_from_vtherm = entity._last_change_time_from_vtherm
|
||||
|
||||
# 2. activate security feature when date is expired
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
|
||||
@@ -92,15 +98,15 @@ async def test_last_seen_feature(hass: HomeAssistant, skip_hass_states_is_state)
|
||||
) as mock_heater_on:
|
||||
event_timestamp = now - timedelta(minutes=6)
|
||||
|
||||
# set temperature to 15 so that on_percent will be > security_min_on_percent (0.2)
|
||||
# set temperature to 15 so that on_percent will be > safety_min_on_percent (0.2)
|
||||
await send_temperature_change_event(entity, 15, event_timestamp)
|
||||
assert entity.security_state is True
|
||||
assert entity.preset_mode == PRESET_SECURITY
|
||||
assert entity.safety_state is STATE_ON
|
||||
assert entity.preset_mode == PRESET_SAFETY
|
||||
|
||||
assert mock_send_event.call_count == 3
|
||||
mock_send_event.assert_has_calls(
|
||||
[
|
||||
call.send_event(EventType.PRESET_EVENT, {"preset": PRESET_SECURITY}),
|
||||
call.send_event(EventType.PRESET_EVENT, {"preset": PRESET_SAFETY}),
|
||||
call.send_event(
|
||||
EventType.TEMPERATURE_EVENT,
|
||||
{
|
||||
@@ -128,9 +134,13 @@ async def test_last_seen_feature(hass: HomeAssistant, skip_hass_states_is_state)
|
||||
|
||||
assert mock_heater_on.call_count == 1
|
||||
|
||||
assert entity._last_change_time_from_vtherm == last_change_time_from_vtherm
|
||||
|
||||
# 3. change the last seen sensor
|
||||
event_timestamp = now - timedelta(minutes=4)
|
||||
await send_last_seen_temperature_change_event(entity, event_timestamp)
|
||||
assert entity.security_state is False
|
||||
assert entity.safety_state is not STATE_ON
|
||||
assert entity.preset_mode is PRESET_COMFORT
|
||||
assert entity._last_temperature_measure == event_timestamp
|
||||
|
||||
assert entity._last_change_time_from_vtherm == last_change_time_from_vtherm
|
||||
|
||||
@@ -1,22 +1,288 @@
|
||||
# pylint: disable=wildcard-import, unused-wildcard-import, protected-access, unused-argument, line-too-long, unused-variable
|
||||
# pylint: disable=wildcard-import, unused-wildcard-import, protected-access, unused-argument, line-too-long, unused-variable, too-many-lines
|
||||
|
||||
""" Test the Window management """
|
||||
from datetime import datetime, timedelta
|
||||
import logging
|
||||
from unittest.mock import patch
|
||||
from unittest.mock import patch, call, AsyncMock, MagicMock, PropertyMock
|
||||
|
||||
from custom_components.versatile_thermostat.base_thermostat import BaseThermostat
|
||||
from custom_components.versatile_thermostat.feature_motion_manager import (
|
||||
FeatureMotionManager,
|
||||
)
|
||||
|
||||
from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import
|
||||
|
||||
logging.getLogger().setLevel(logging.DEBUG)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"current_state, new_state, temp, nb_call, motion_state, is_motion_detected, preset_refresh, changed",
|
||||
[
|
||||
(STATE_OFF, STATE_ON, 21, 1, STATE_ON, True, PRESET_BOOST, True),
|
||||
# motion is ON. So is_motion_detected is true and preset is BOOST
|
||||
(STATE_OFF, STATE_ON, 21, 1, STATE_ON, True, PRESET_BOOST, True),
|
||||
# current_state is ON and motion is OFF. So is_motion_detected is false and preset is ECO
|
||||
(STATE_ON, STATE_OFF, 17, 1, STATE_OFF, False, PRESET_ECO, True),
|
||||
],
|
||||
)
|
||||
async def test_motion_feature_manager_refresh(
|
||||
hass: HomeAssistant,
|
||||
current_state,
|
||||
new_state, # new state of motion event
|
||||
temp,
|
||||
nb_call,
|
||||
motion_state,
|
||||
is_motion_detected,
|
||||
preset_refresh,
|
||||
changed,
|
||||
):
|
||||
"""Test the FeatureMotionManager class direclty"""
|
||||
|
||||
fake_vtherm = MagicMock(spec=BaseThermostat)
|
||||
type(fake_vtherm).name = PropertyMock(return_value="the name")
|
||||
type(fake_vtherm).preset_mode = PropertyMock(return_value=PRESET_ACTIVITY)
|
||||
|
||||
# 1. creation
|
||||
motion_manager = FeatureMotionManager(fake_vtherm, hass)
|
||||
|
||||
assert motion_manager is not None
|
||||
assert motion_manager.is_configured is False
|
||||
assert motion_manager.is_motion_detected is False
|
||||
assert motion_manager.motion_state == STATE_UNAVAILABLE
|
||||
assert motion_manager.name == "the name"
|
||||
|
||||
assert len(motion_manager._active_listener) == 0
|
||||
|
||||
custom_attributes = {}
|
||||
motion_manager.add_custom_attributes(custom_attributes)
|
||||
assert custom_attributes["motion_sensor_entity_id"] is None
|
||||
assert custom_attributes["motion_state"] == STATE_UNAVAILABLE
|
||||
assert custom_attributes["is_motion_configured"] is False
|
||||
assert custom_attributes["motion_preset"] is None
|
||||
assert custom_attributes["no_motion_preset"] is None
|
||||
assert custom_attributes["motion_delay_sec"] == 0
|
||||
assert custom_attributes["motion_off_delay_sec"] == 0
|
||||
|
||||
# 2. post_init
|
||||
motion_manager.post_init(
|
||||
{
|
||||
CONF_MOTION_SENSOR: "sensor.the_motion_sensor",
|
||||
CONF_USE_MOTION_FEATURE: True,
|
||||
CONF_MOTION_DELAY: 10,
|
||||
CONF_MOTION_OFF_DELAY: 30,
|
||||
CONF_MOTION_PRESET: PRESET_BOOST,
|
||||
CONF_NO_MOTION_PRESET: PRESET_ECO,
|
||||
}
|
||||
)
|
||||
|
||||
assert motion_manager.is_configured is True
|
||||
assert motion_manager.motion_state == STATE_UNKNOWN
|
||||
assert motion_manager.is_motion_detected is False
|
||||
|
||||
custom_attributes = {}
|
||||
motion_manager.add_custom_attributes(custom_attributes)
|
||||
assert custom_attributes["motion_sensor_entity_id"] == "sensor.the_motion_sensor"
|
||||
assert custom_attributes["motion_state"] == STATE_UNKNOWN
|
||||
assert custom_attributes["is_motion_configured"] is True
|
||||
assert custom_attributes["motion_preset"] is PRESET_BOOST
|
||||
assert custom_attributes["no_motion_preset"] is PRESET_ECO
|
||||
assert custom_attributes["motion_delay_sec"] == 10
|
||||
assert custom_attributes["motion_off_delay_sec"] == 30
|
||||
|
||||
# 3. 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
|
||||
|
||||
assert len(motion_manager._active_listener) == 1
|
||||
|
||||
# 4. test refresh with the parametrized
|
||||
# fmt:off
|
||||
with patch("homeassistant.core.StateMachine.get", return_value=State("sensor.the_motion_sensor", new_state)) as mock_get_state:
|
||||
# fmt:on
|
||||
# Configurer les méthodes mockées
|
||||
fake_vtherm.find_preset_temp.return_value = temp
|
||||
fake_vtherm.change_target_temperature = AsyncMock()
|
||||
fake_vtherm.async_control_heating = AsyncMock()
|
||||
fake_vtherm.recalculate = MagicMock()
|
||||
|
||||
# force old state for the test
|
||||
motion_manager._motion_state = current_state
|
||||
|
||||
ret = await motion_manager.refresh_state()
|
||||
assert ret == changed
|
||||
assert motion_manager.is_configured is True
|
||||
# in the refresh there is no delay
|
||||
assert motion_manager.motion_state == new_state
|
||||
assert motion_manager.is_motion_detected is is_motion_detected
|
||||
|
||||
assert mock_get_state.call_count == 1
|
||||
|
||||
assert fake_vtherm.find_preset_temp.call_count == nb_call
|
||||
|
||||
if nb_call == 1:
|
||||
fake_vtherm.find_preset_temp.assert_has_calls(
|
||||
[
|
||||
call.find_preset_temp(preset_refresh),
|
||||
]
|
||||
)
|
||||
|
||||
assert fake_vtherm.change_target_temperature.call_count == nb_call
|
||||
fake_vtherm.change_target_temperature.assert_has_calls(
|
||||
[
|
||||
call.find_preset_temp(temp),
|
||||
]
|
||||
)
|
||||
|
||||
# We do not call control_heating at startup
|
||||
assert fake_vtherm.recalculate.call_count == 0
|
||||
assert fake_vtherm.async_control_heating.call_count == 0
|
||||
|
||||
fake_vtherm.reset_mock()
|
||||
|
||||
# 5. Check custom_attributes
|
||||
custom_attributes = {}
|
||||
motion_manager.add_custom_attributes(custom_attributes)
|
||||
assert custom_attributes["motion_sensor_entity_id"] == "sensor.the_motion_sensor"
|
||||
assert custom_attributes["motion_state"] == new_state
|
||||
assert custom_attributes["is_motion_configured"] is True
|
||||
assert custom_attributes["motion_preset"] is PRESET_BOOST
|
||||
assert custom_attributes["no_motion_preset"] is PRESET_ECO
|
||||
assert custom_attributes["motion_delay_sec"] == 10
|
||||
assert custom_attributes["motion_off_delay_sec"] == 30
|
||||
|
||||
motion_manager.stop_listening()
|
||||
await hass.async_block_till_done()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"current_state, long_enough, new_state, temp, nb_call, motion_state, is_motion_detected, preset_event, changed",
|
||||
[
|
||||
(STATE_OFF, True, STATE_ON, 21, 1, STATE_ON, True, PRESET_BOOST, True),
|
||||
# motion is ON but for not enough time but sensor is on at the end. So is_motion_detected is true and preset is BOOST
|
||||
(STATE_OFF, False, STATE_ON, 21, 1, STATE_ON, True, PRESET_BOOST, True),
|
||||
# motion is OFF for enough time. So is_motion_detected is false and preset is ECO
|
||||
(STATE_ON, True, STATE_OFF, 17, 1, STATE_OFF, False, PRESET_ECO, True),
|
||||
# motion is OFF for not enough time. So is_motion_detected is false and preset is ECO
|
||||
(STATE_ON, False, STATE_OFF, 21, 1, STATE_ON, True, PRESET_BOOST, True),
|
||||
],
|
||||
)
|
||||
async def test_motion_feature_manager_event(
|
||||
hass: HomeAssistant,
|
||||
current_state,
|
||||
long_enough,
|
||||
new_state, # new state of motion event
|
||||
temp,
|
||||
nb_call,
|
||||
motion_state,
|
||||
is_motion_detected,
|
||||
preset_event,
|
||||
changed,
|
||||
):
|
||||
"""Test the FeatureMotionManager class direclty"""
|
||||
|
||||
fake_vtherm = MagicMock(spec=BaseThermostat)
|
||||
type(fake_vtherm).name = PropertyMock(return_value="the name")
|
||||
type(fake_vtherm).preset_mode = PropertyMock(return_value=PRESET_ACTIVITY)
|
||||
|
||||
# 1. iniitialization creation, post_init, start_listening
|
||||
motion_manager = FeatureMotionManager(fake_vtherm, hass)
|
||||
motion_manager.post_init(
|
||||
{
|
||||
CONF_MOTION_SENSOR: "sensor.the_motion_sensor",
|
||||
CONF_USE_MOTION_FEATURE: True,
|
||||
CONF_MOTION_DELAY: 10,
|
||||
CONF_MOTION_OFF_DELAY: 30,
|
||||
CONF_MOTION_PRESET: PRESET_BOOST,
|
||||
CONF_NO_MOTION_PRESET: PRESET_ECO,
|
||||
}
|
||||
)
|
||||
await motion_manager.start_listening()
|
||||
|
||||
# 2. test _motion_sensor_changed with the parametrized
|
||||
# fmt: off
|
||||
with patch("homeassistant.helpers.condition.state", return_value=long_enough), \
|
||||
patch("homeassistant.core.StateMachine.get", return_value=State("sensor.the_motion_sensor", new_state)):
|
||||
# fmt: on
|
||||
fake_vtherm.find_preset_temp.return_value = temp
|
||||
fake_vtherm.change_target_temperature = AsyncMock()
|
||||
fake_vtherm.async_control_heating = AsyncMock()
|
||||
fake_vtherm.recalculate = MagicMock()
|
||||
|
||||
# force old state for the test
|
||||
motion_manager._motion_state = current_state
|
||||
|
||||
delay = await motion_manager._motion_sensor_changed(
|
||||
event=Event(
|
||||
event_type=EVENT_STATE_CHANGED,
|
||||
data={
|
||||
"entity_id": "sensor.the_motion_sensor",
|
||||
"new_state": State("sensor.the_motion_sensor", new_state),
|
||||
"old_state": State("sensor.the_motion_sensor", STATE_UNAVAILABLE),
|
||||
}))
|
||||
assert delay is not None
|
||||
|
||||
await delay(None)
|
||||
assert motion_manager.is_configured is True
|
||||
assert motion_manager.motion_state == motion_state
|
||||
assert motion_manager.is_motion_detected is is_motion_detected
|
||||
|
||||
assert fake_vtherm.find_preset_temp.call_count == nb_call
|
||||
|
||||
if nb_call == 1:
|
||||
fake_vtherm.find_preset_temp.assert_has_calls(
|
||||
[
|
||||
call.find_preset_temp(preset_event),
|
||||
]
|
||||
)
|
||||
|
||||
assert fake_vtherm.change_target_temperature.call_count == nb_call
|
||||
fake_vtherm.change_target_temperature.assert_has_calls(
|
||||
[
|
||||
call.find_preset_temp(temp),
|
||||
]
|
||||
)
|
||||
|
||||
assert fake_vtherm.recalculate.call_count == 1
|
||||
assert fake_vtherm.async_control_heating.call_count == 1
|
||||
fake_vtherm.async_control_heating.assert_has_calls([
|
||||
call.async_control_heating(force=True)
|
||||
])
|
||||
|
||||
fake_vtherm.reset_mock()
|
||||
|
||||
# 3. Check custom_attributes
|
||||
custom_attributes = {}
|
||||
motion_manager.add_custom_attributes(custom_attributes)
|
||||
assert custom_attributes["motion_sensor_entity_id"] == "sensor.the_motion_sensor"
|
||||
assert custom_attributes["motion_state"] == motion_state
|
||||
assert custom_attributes["is_motion_configured"] is True
|
||||
assert custom_attributes["motion_preset"] is PRESET_BOOST
|
||||
assert custom_attributes["no_motion_preset"] is PRESET_ECO
|
||||
assert custom_attributes["motion_delay_sec"] == 10
|
||||
assert custom_attributes["motion_off_delay_sec"] == 30
|
||||
|
||||
motion_manager.stop_listening()
|
||||
await hass.async_block_till_done()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_movement_management_time_not_enough(
|
||||
async def test_motion_management_time_not_enough(
|
||||
hass: HomeAssistant, skip_hass_states_is_state
|
||||
):
|
||||
"""Test the Presence management when time is not enough"""
|
||||
temps = {
|
||||
"frost": 10,
|
||||
"eco": 17,
|
||||
"comfort": 18,
|
||||
"boost": 19,
|
||||
"frost_away": 10,
|
||||
"eco_away": 17,
|
||||
"comfort_away": 18,
|
||||
"boost_away": 19,
|
||||
}
|
||||
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
@@ -30,23 +296,17 @@ async def test_movement_management_time_not_enough(
|
||||
CONF_CYCLE_MIN: 5,
|
||||
CONF_TEMP_MIN: 15,
|
||||
CONF_TEMP_MAX: 30,
|
||||
"eco_temp": 17,
|
||||
"comfort_temp": 18,
|
||||
"boost_temp": 19,
|
||||
"eco_away_temp": 17,
|
||||
"comfort_away_temp": 18,
|
||||
"boost_away_temp": 19,
|
||||
CONF_USE_WINDOW_FEATURE: False,
|
||||
CONF_USE_MOTION_FEATURE: True,
|
||||
CONF_USE_POWER_FEATURE: False,
|
||||
CONF_USE_PRESENCE_FEATURE: True,
|
||||
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_SECURITY_DELAY_MIN: 10,
|
||||
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
|
||||
CONF_SAFETY_DELAY_MIN: 10,
|
||||
CONF_SAFETY_MIN_ON_PERCENT: 0.3,
|
||||
CONF_MOTION_SENSOR: "binary_sensor.mock_motion_sensor",
|
||||
CONF_MOTION_DELAY: 10, # important to not been obliged to wait
|
||||
CONF_MOTION_OFF_DELAY: 30,
|
||||
@@ -60,11 +320,12 @@ async def test_movement_management_time_not_enough(
|
||||
hass, entry, "climate.theoverswitchmockname"
|
||||
)
|
||||
assert entity
|
||||
await set_all_climate_preset_temp(hass, entity, temps, "theoverswitchmockname")
|
||||
|
||||
tz = get_tz(hass) # pylint: disable=invalid-name
|
||||
now: datetime = datetime.now(tz=tz)
|
||||
|
||||
# start heating, in boost mode, when someone is present. We block the control_heating to avoid running a cycle
|
||||
# 1. start heating, in boost mode, when someone is present. We block the control_heating to avoid running a cycle
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.async_control_heating"
|
||||
):
|
||||
@@ -75,17 +336,17 @@ async def test_movement_management_time_not_enough(
|
||||
assert entity.preset_mode is PRESET_ACTIVITY
|
||||
# because no motion is detected yet
|
||||
assert entity.target_temperature == 18
|
||||
assert entity.motion_state is None
|
||||
assert entity.presence_state is None
|
||||
assert entity.motion_state is STATE_UNKNOWN
|
||||
assert entity.presence_state is STATE_UNKNOWN
|
||||
|
||||
event_timestamp = now - timedelta(minutes=5)
|
||||
await send_temperature_change_event(entity, 18, event_timestamp)
|
||||
await send_ext_temperature_change_event(entity, 10, event_timestamp)
|
||||
|
||||
await send_presence_change_event(entity, True, False, event_timestamp)
|
||||
assert entity.presence_state == "on"
|
||||
assert entity.presence_state == STATE_ON
|
||||
|
||||
# starts detecting motion with time not enough
|
||||
# 2. starts detecting motion with time not enough
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
|
||||
) as mock_send_event, patch(
|
||||
@@ -104,7 +365,9 @@ async def test_movement_management_time_not_enough(
|
||||
),
|
||||
):
|
||||
event_timestamp = now - timedelta(minutes=4)
|
||||
try_condition = await send_motion_change_event(entity, True, False, event_timestamp)
|
||||
try_condition = await send_motion_change_event(
|
||||
entity, True, False, event_timestamp
|
||||
)
|
||||
|
||||
# Will return False -> we will stay on movement False
|
||||
await try_condition(None)
|
||||
@@ -137,7 +400,9 @@ async def test_movement_management_time_not_enough(
|
||||
"homeassistant.helpers.condition.state", return_value=True
|
||||
) as mock_condition:
|
||||
event_timestamp = now - timedelta(minutes=3)
|
||||
try_condition = await send_motion_change_event(entity, True, False, event_timestamp)
|
||||
try_condition = await send_motion_change_event(
|
||||
entity, True, False, event_timestamp
|
||||
)
|
||||
|
||||
# Will return True -> we will switch to movement On
|
||||
await try_condition(None)
|
||||
@@ -168,7 +433,9 @@ async def test_movement_management_time_not_enough(
|
||||
),
|
||||
):
|
||||
event_timestamp = now - timedelta(minutes=2)
|
||||
try_condition = await send_motion_change_event(entity, False, True, event_timestamp)
|
||||
try_condition = await send_motion_change_event(
|
||||
entity, False, True, event_timestamp
|
||||
)
|
||||
|
||||
# Will return False -> we will stay to movement On
|
||||
await try_condition(None)
|
||||
@@ -200,7 +467,9 @@ async def test_movement_management_time_not_enough(
|
||||
"homeassistant.helpers.condition.state", return_value=True
|
||||
) as mock_condition:
|
||||
event_timestamp = now - timedelta(minutes=1)
|
||||
try_condition = await send_motion_change_event(entity, False, True, event_timestamp)
|
||||
try_condition = await send_motion_change_event(
|
||||
entity, False, True, event_timestamp
|
||||
)
|
||||
|
||||
# Will return True -> we will switch to movement Off
|
||||
await try_condition(None)
|
||||
@@ -221,7 +490,7 @@ async def test_movement_management_time_not_enough(
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_movement_management_time_enough_and_presence(
|
||||
async def test_motion_management_time_enough_and_presence(
|
||||
hass: HomeAssistant, skip_hass_states_is_state
|
||||
):
|
||||
"""Test the Motion management when time is not enough"""
|
||||
@@ -253,8 +522,8 @@ async def test_movement_management_time_enough_and_presence(
|
||||
CONF_TPI_COEF_INT: 0.3,
|
||||
CONF_TPI_COEF_EXT: 0.01,
|
||||
CONF_MINIMAL_ACTIVATION_DELAY: 30,
|
||||
CONF_SECURITY_DELAY_MIN: 5,
|
||||
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
|
||||
CONF_SAFETY_DELAY_MIN: 5,
|
||||
CONF_SAFETY_MIN_ON_PERCENT: 0.3,
|
||||
CONF_MOTION_SENSOR: "binary_sensor.mock_motion_sensor",
|
||||
CONF_MOTION_DELAY: 0, # important to not been obliged to wait
|
||||
CONF_MOTION_PRESET: "boost",
|
||||
@@ -282,8 +551,8 @@ async def test_movement_management_time_enough_and_presence(
|
||||
assert entity.preset_mode is PRESET_ACTIVITY
|
||||
# because no motion is detected yet
|
||||
assert entity.target_temperature == 18
|
||||
assert entity.motion_state is None
|
||||
assert entity.presence_state is None
|
||||
assert entity.motion_state is STATE_UNKNOWN
|
||||
assert entity.presence_state is STATE_UNKNOWN
|
||||
|
||||
event_timestamp = now - timedelta(minutes=4)
|
||||
await send_temperature_change_event(entity, 18, event_timestamp)
|
||||
@@ -312,9 +581,8 @@ async def test_movement_management_time_enough_and_presence(
|
||||
assert entity.preset_mode is PRESET_ACTIVITY
|
||||
# because motion is detected yet -> switch to Boost mode
|
||||
assert entity.target_temperature == 19
|
||||
assert entity.motion_state == "on"
|
||||
assert entity.presence_state == "on"
|
||||
|
||||
assert entity.motion_state == STATE_ON
|
||||
assert entity.presence_state == STATE_ON
|
||||
assert mock_send_event.call_count == 0
|
||||
# Change is confirmed. Heater should be started
|
||||
assert mock_heater_on.call_count == 1
|
||||
@@ -341,8 +609,8 @@ async def test_movement_management_time_enough_and_presence(
|
||||
assert entity.preset_mode is PRESET_ACTIVITY
|
||||
# because no motion is detected yet
|
||||
assert entity.target_temperature == 18
|
||||
assert entity.motion_state == "off"
|
||||
assert entity.presence_state == "on"
|
||||
assert entity.motion_state == STATE_OFF
|
||||
assert entity.presence_state == STATE_ON
|
||||
|
||||
assert mock_send_event.call_count == 0
|
||||
assert mock_heater_on.call_count == 0
|
||||
@@ -353,7 +621,7 @@ async def test_movement_management_time_enough_and_presence(
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_movement_management_time_enoughand_not_presence(
|
||||
async def test_motion_management_time_enough_and_not_presence(
|
||||
hass: HomeAssistant, skip_hass_states_is_state
|
||||
):
|
||||
"""Test the Presence management when time is not enough"""
|
||||
@@ -385,8 +653,8 @@ async def test_movement_management_time_enoughand_not_presence(
|
||||
CONF_TPI_COEF_INT: 0.3,
|
||||
CONF_TPI_COEF_EXT: 0.01,
|
||||
CONF_MINIMAL_ACTIVATION_DELAY: 30,
|
||||
CONF_SECURITY_DELAY_MIN: 5,
|
||||
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
|
||||
CONF_SAFETY_DELAY_MIN: 5,
|
||||
CONF_SAFETY_MIN_ON_PERCENT: 0.3,
|
||||
CONF_MOTION_SENSOR: "binary_sensor.mock_motion_sensor",
|
||||
CONF_MOTION_DELAY: 0, # important to not been obliged to wait
|
||||
CONF_MOTION_PRESET: "boost",
|
||||
@@ -414,15 +682,15 @@ async def test_movement_management_time_enoughand_not_presence(
|
||||
assert entity.preset_mode is PRESET_ACTIVITY
|
||||
# because no motion is detected yet and presence is unknown
|
||||
assert entity.target_temperature == 18
|
||||
assert entity.motion_state is None
|
||||
assert entity.presence_state is None
|
||||
assert entity.motion_state is STATE_UNKNOWN
|
||||
assert entity.presence_state is STATE_UNKNOWN
|
||||
|
||||
event_timestamp = now - timedelta(minutes=4)
|
||||
await send_temperature_change_event(entity, 18, event_timestamp)
|
||||
await send_ext_temperature_change_event(entity, 10, event_timestamp)
|
||||
|
||||
await send_presence_change_event(entity, False, True, event_timestamp)
|
||||
assert entity.presence_state == "off"
|
||||
assert entity.presence_state == STATE_OFF
|
||||
|
||||
# starts detecting motion
|
||||
with patch(
|
||||
@@ -444,8 +712,8 @@ async def test_movement_management_time_enoughand_not_presence(
|
||||
assert entity.preset_mode is PRESET_ACTIVITY
|
||||
# because motion is detected yet -> switch to Boost away mode
|
||||
assert entity.target_temperature == 19.1
|
||||
assert entity.motion_state == "on"
|
||||
assert entity.presence_state == "off"
|
||||
assert entity.motion_state == STATE_ON
|
||||
assert entity.presence_state == STATE_OFF
|
||||
|
||||
assert mock_send_event.call_count == 0
|
||||
# Change is confirmed. Heater should be started
|
||||
@@ -473,9 +741,8 @@ async def test_movement_management_time_enoughand_not_presence(
|
||||
assert entity.preset_mode is PRESET_ACTIVITY
|
||||
# because no motion is detected yet
|
||||
assert entity.target_temperature == 18.1
|
||||
assert entity.motion_state == "off"
|
||||
assert entity.presence_state == "off"
|
||||
|
||||
assert entity.motion_state == STATE_OFF
|
||||
assert entity.presence_state == STATE_OFF
|
||||
assert mock_send_event.call_count == 0
|
||||
# 18.1 starts heating with a low on_percent
|
||||
assert mock_heater_on.call_count == 1
|
||||
@@ -486,7 +753,7 @@ async def test_movement_management_time_enoughand_not_presence(
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_movement_management_with_stop_during_condition(
|
||||
async def test_motion_management_with_stop_during_condition(
|
||||
hass: HomeAssistant, skip_hass_states_is_state
|
||||
):
|
||||
"""Test the Motion management when the movement sensor switch to off and then to on during the test condition"""
|
||||
@@ -518,8 +785,8 @@ async def test_movement_management_with_stop_during_condition(
|
||||
CONF_TPI_COEF_INT: 0.3,
|
||||
CONF_TPI_COEF_EXT: 0.01,
|
||||
CONF_MINIMAL_ACTIVATION_DELAY: 30,
|
||||
CONF_SECURITY_DELAY_MIN: 5,
|
||||
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
|
||||
CONF_SAFETY_DELAY_MIN: 5,
|
||||
CONF_SAFETY_MIN_ON_PERCENT: 0.3,
|
||||
CONF_MOTION_SENSOR: "binary_sensor.mock_motion_sensor",
|
||||
CONF_MOTION_DELAY: 10,
|
||||
CONF_MOTION_OFF_DELAY: 30,
|
||||
@@ -548,15 +815,15 @@ async def test_movement_management_with_stop_during_condition(
|
||||
assert entity.preset_mode is PRESET_ACTIVITY
|
||||
# because no motion is detected yet
|
||||
assert entity.target_temperature == 18
|
||||
assert entity.motion_state is None
|
||||
assert entity.presence_state is None
|
||||
assert entity.motion_state is STATE_UNKNOWN
|
||||
assert entity.presence_state is STATE_UNKNOWN
|
||||
|
||||
event_timestamp = now - timedelta(minutes=6)
|
||||
await send_temperature_change_event(entity, 18, event_timestamp)
|
||||
await send_ext_temperature_change_event(entity, 10, event_timestamp)
|
||||
|
||||
await send_presence_change_event(entity, False, True, event_timestamp)
|
||||
assert entity.presence_state == "off"
|
||||
assert entity.presence_state == STATE_OFF
|
||||
|
||||
# starts detecting motion
|
||||
with patch(
|
||||
@@ -582,9 +849,8 @@ async def test_movement_management_with_stop_during_condition(
|
||||
assert entity.preset_mode is PRESET_ACTIVITY
|
||||
# because motion is detected yet -> switch to Boost mode
|
||||
assert entity.target_temperature == 18
|
||||
assert entity.motion_state is None
|
||||
assert entity.presence_state == "off"
|
||||
|
||||
assert entity.motion_state is STATE_UNKNOWN
|
||||
assert entity.presence_state == STATE_OFF
|
||||
# Send a stop detection
|
||||
event_timestamp = now - timedelta(minutes=4)
|
||||
try_condition = await send_motion_change_event(
|
||||
@@ -595,8 +861,8 @@ async def test_movement_management_with_stop_during_condition(
|
||||
assert entity.hvac_mode is HVACMode.HEAT
|
||||
assert entity.preset_mode is PRESET_ACTIVITY
|
||||
assert entity.target_temperature == 18
|
||||
assert entity.motion_state is None
|
||||
assert entity.presence_state == "off"
|
||||
assert entity.motion_state is STATE_UNKNOWN
|
||||
assert entity.presence_state == STATE_OFF
|
||||
|
||||
# Resend a start detection
|
||||
event_timestamp = now - timedelta(minutes=3)
|
||||
@@ -611,19 +877,19 @@ async def test_movement_management_with_stop_during_condition(
|
||||
assert entity.preset_mode is PRESET_ACTIVITY
|
||||
# still no motion detected
|
||||
assert entity.target_temperature == 18
|
||||
assert entity.motion_state is None
|
||||
assert entity.presence_state == "off"
|
||||
assert entity.motion_state is STATE_UNKNOWN
|
||||
assert entity.presence_state == STATE_OFF
|
||||
|
||||
await try_condition1(None)
|
||||
# We should have switch this time
|
||||
assert entity.target_temperature == 19 # Boost
|
||||
assert entity.motion_state == "on" # switch to movement on
|
||||
assert entity.presence_state == "off" # Non change
|
||||
assert entity.motion_state == STATE_ON # switch to movement on
|
||||
assert entity.presence_state == STATE_OFF # Non change
|
||||
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_movement_management_with_stop_during_condition_last_state_on(
|
||||
async def test_motion_management_with_stop_during_condition_last_state_on(
|
||||
hass: HomeAssistant, skip_hass_states_is_state
|
||||
):
|
||||
"""Test the Motion management when the movement sensor switch to off and then to on during the test condition"""
|
||||
@@ -655,8 +921,8 @@ async def test_movement_management_with_stop_during_condition_last_state_on(
|
||||
CONF_TPI_COEF_INT: 0.3,
|
||||
CONF_TPI_COEF_EXT: 0.01,
|
||||
CONF_MINIMAL_ACTIVATION_DELAY: 30,
|
||||
CONF_SECURITY_DELAY_MIN: 5,
|
||||
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
|
||||
CONF_SAFETY_DELAY_MIN: 5,
|
||||
CONF_SAFETY_MIN_ON_PERCENT: 0.3,
|
||||
CONF_MOTION_SENSOR: "binary_sensor.mock_motion_sensor",
|
||||
CONF_MOTION_DELAY: 10,
|
||||
CONF_MOTION_OFF_DELAY: 30,
|
||||
@@ -684,7 +950,7 @@ async def test_movement_management_with_stop_during_condition_last_state_on(
|
||||
assert entity.preset_mode is PRESET_ACTIVITY
|
||||
# because no motion is detected yet
|
||||
assert entity.target_temperature == 18
|
||||
assert entity.motion_state is None
|
||||
assert entity.motion_state is STATE_UNKNOWN
|
||||
|
||||
event_timestamp = now - timedelta(minutes=6)
|
||||
await send_temperature_change_event(entity, 18, event_timestamp)
|
||||
@@ -45,8 +45,8 @@ async def test_one_switch_cycle(
|
||||
CONF_USE_PRESENCE_FEATURE: False,
|
||||
CONF_HEATER: "switch.mock_switch1",
|
||||
CONF_MINIMAL_ACTIVATION_DELAY: 30,
|
||||
CONF_SECURITY_DELAY_MIN: 5,
|
||||
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
|
||||
CONF_SAFETY_DELAY_MIN: 5,
|
||||
CONF_SAFETY_MIN_ON_PERCENT: 0.3,
|
||||
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
|
||||
CONF_TPI_COEF_INT: 0.3,
|
||||
CONF_TPI_COEF_EXT: 0.01,
|
||||
@@ -69,7 +69,7 @@ async def test_one_switch_cycle(
|
||||
assert entity.hvac_mode is HVACMode.HEAT
|
||||
assert entity.preset_mode is PRESET_BOOST
|
||||
assert entity.target_temperature == 19
|
||||
assert entity.window_state is STATE_OFF
|
||||
assert entity.window_state is STATE_UNAVAILABLE
|
||||
|
||||
event_timestamp = now - timedelta(minutes=4)
|
||||
await send_temperature_change_event(entity, 15, event_timestamp)
|
||||
@@ -262,8 +262,8 @@ async def test_multiple_switchs(
|
||||
CONF_HEATER_4: "switch.mock_switch4",
|
||||
CONF_HEATER_KEEP_ALIVE: 0,
|
||||
CONF_MINIMAL_ACTIVATION_DELAY: 30,
|
||||
CONF_SECURITY_DELAY_MIN: 5,
|
||||
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
|
||||
CONF_SAFETY_DELAY_MIN: 5,
|
||||
CONF_SAFETY_MIN_ON_PERCENT: 0.3,
|
||||
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
|
||||
CONF_TPI_COEF_INT: 0.3,
|
||||
CONF_TPI_COEF_EXT: 0.01,
|
||||
@@ -289,7 +289,7 @@ async def test_multiple_switchs(
|
||||
assert entity.hvac_mode is HVACMode.HEAT
|
||||
assert entity.preset_mode is PRESET_BOOST
|
||||
assert entity.target_temperature == 19
|
||||
assert entity.window_state is STATE_OFF
|
||||
assert entity.window_state is STATE_UNAVAILABLE
|
||||
|
||||
event_timestamp = now - timedelta(minutes=4)
|
||||
await send_temperature_change_event(entity, 15, event_timestamp)
|
||||
@@ -402,8 +402,8 @@ async def test_multiple_climates(
|
||||
CONF_CLIMATE_3: "switch.mock_climate3",
|
||||
CONF_CLIMATE_4: "switch.mock_climate4",
|
||||
CONF_MINIMAL_ACTIVATION_DELAY: 30,
|
||||
CONF_SECURITY_DELAY_MIN: 5,
|
||||
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
|
||||
CONF_SAFETY_DELAY_MIN: 5,
|
||||
CONF_SAFETY_MIN_ON_PERCENT: 0.3,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -426,7 +426,7 @@ async def test_multiple_climates(
|
||||
assert entity.hvac_mode is HVACMode.HEAT
|
||||
assert entity.preset_mode is PRESET_BOOST
|
||||
assert entity.target_temperature == 19
|
||||
assert entity.window_state is STATE_OFF
|
||||
assert entity.window_state is STATE_UNAVAILABLE
|
||||
|
||||
event_timestamp = now - timedelta(minutes=4)
|
||||
await send_temperature_change_event(entity, 15, event_timestamp)
|
||||
@@ -451,7 +451,7 @@ async def test_multiple_climates(
|
||||
assert entity.hvac_mode is HVACMode.OFF
|
||||
assert entity.preset_mode is PRESET_BOOST
|
||||
assert entity.target_temperature == 19
|
||||
assert entity.window_state is STATE_OFF
|
||||
assert entity.window_state is STATE_UNAVAILABLE
|
||||
|
||||
event_timestamp = now - timedelta(minutes=4)
|
||||
await send_temperature_change_event(entity, 15, event_timestamp)
|
||||
@@ -503,8 +503,8 @@ async def test_multiple_climates_underlying_changes(
|
||||
CONF_CLIMATE_3: "switch.mock_climate3",
|
||||
CONF_CLIMATE_4: "switch.mock_climate4",
|
||||
CONF_MINIMAL_ACTIVATION_DELAY: 30,
|
||||
CONF_SECURITY_DELAY_MIN: 5,
|
||||
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
|
||||
CONF_SAFETY_DELAY_MIN: 5,
|
||||
CONF_SAFETY_MIN_ON_PERCENT: 0.3,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -527,7 +527,7 @@ async def test_multiple_climates_underlying_changes(
|
||||
assert entity.hvac_mode is HVACMode.HEAT
|
||||
assert entity.preset_mode is PRESET_BOOST
|
||||
assert entity.target_temperature == 19
|
||||
assert entity.window_state is STATE_OFF
|
||||
assert entity.window_state is STATE_UNAVAILABLE
|
||||
|
||||
event_timestamp = now - timedelta(minutes=4)
|
||||
await send_temperature_change_event(entity, 15, event_timestamp)
|
||||
@@ -648,8 +648,8 @@ async def test_multiple_climates_underlying_changes_not_aligned(
|
||||
CONF_CLIMATE_3: "switch.mock_climate3",
|
||||
CONF_CLIMATE_4: "switch.mock_climate4",
|
||||
CONF_MINIMAL_ACTIVATION_DELAY: 30,
|
||||
CONF_SECURITY_DELAY_MIN: 5,
|
||||
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
|
||||
CONF_SAFETY_DELAY_MIN: 5,
|
||||
CONF_SAFETY_MIN_ON_PERCENT: 0.3,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -672,7 +672,7 @@ async def test_multiple_climates_underlying_changes_not_aligned(
|
||||
assert entity.hvac_mode is HVACMode.HEAT
|
||||
assert entity.preset_mode is PRESET_BOOST
|
||||
assert entity.target_temperature == 19
|
||||
assert entity.window_state is STATE_OFF
|
||||
assert entity.window_state is STATE_UNAVAILABLE
|
||||
|
||||
event_timestamp = now - timedelta(minutes=4)
|
||||
await send_temperature_change_event(entity, 15, event_timestamp)
|
||||
@@ -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,33 +741,30 @@ 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_SECURITY_DELAY_MIN: 5,
|
||||
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
|
||||
CONF_SAFETY_DELAY_MIN: 5,
|
||||
CONF_SAFETY_MIN_ON_PERCENT: 0.3,
|
||||
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,84 +773,119 @@ 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
|
||||
assert entity.preset_mode is PRESET_BOOST
|
||||
assert entity.overpowering_state is None
|
||||
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.check_overpowering() is False
|
||||
# All configuration is complete and power is < power_max
|
||||
assert entity.preset_mode is PRESET_BOOST
|
||||
assert entity.overpowering_state is False
|
||||
# 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.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.overpowering_state is True
|
||||
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_power_max": 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.overpowering_state is True
|
||||
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.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.overpowering_state is False
|
||||
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
|
||||
|
||||
@@ -62,8 +62,8 @@ async def test_bug_56(
|
||||
CONF_USE_PRESENCE_FEATURE: False,
|
||||
CONF_CLIMATE: "climate.mock_climate",
|
||||
CONF_MINIMAL_ACTIVATION_DELAY: 30,
|
||||
CONF_SECURITY_DELAY_MIN: 5,
|
||||
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
|
||||
CONF_SAFETY_DELAY_MIN: 5,
|
||||
CONF_SAFETY_MIN_ON_PERCENT: 0.3,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -151,7 +151,7 @@ async def test_bug_82(
|
||||
PRESET_BOOST,
|
||||
]
|
||||
assert entity.preset_mode is PRESET_NONE
|
||||
assert entity._security_state is False
|
||||
assert entity.safety_manager.is_safety_detected is False
|
||||
|
||||
# should have been called with EventType.PRESET_EVENT and EventType.HVAC_MODE_EVENT
|
||||
assert mock_send_event.call_count == 2
|
||||
@@ -191,10 +191,10 @@ async def test_bug_82(
|
||||
):
|
||||
event_timestamp = now - timedelta(minutes=6)
|
||||
|
||||
# set temperature to 15 so that on_percent will be > security_min_on_percent (0.2)
|
||||
# set temperature to 15 so that on_percent will be > safety_min_on_percent (0.2)
|
||||
await send_temperature_change_event(entity, 15, event_timestamp)
|
||||
# Should stay False
|
||||
assert entity.security_state is False
|
||||
assert entity.safety_state is not STATE_ON
|
||||
assert entity.preset_mode == "none"
|
||||
assert entity._saved_preset_mode == "none"
|
||||
|
||||
@@ -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"
|
||||
@@ -323,7 +330,7 @@ async def test_underlying_change_follow(
|
||||
assert entity.target_temperature == entity.min_temp + 1
|
||||
assert entity.preset_mode is PRESET_NONE
|
||||
|
||||
# 4. Change the target temp with < 1 value. The value should not be taken
|
||||
# 4. Change the target temp with < 0.1 (step) value. The value should not be taken
|
||||
# Wait 11 sec
|
||||
event_timestamp = now + timedelta(seconds=11)
|
||||
await send_climate_change_event_with_temperature(
|
||||
@@ -333,7 +340,7 @@ async def test_underlying_change_follow(
|
||||
HVACAction.OFF,
|
||||
HVACAction.OFF,
|
||||
event_timestamp,
|
||||
entity.min_temp + 1.5,
|
||||
entity.min_temp + 1.09,
|
||||
True,
|
||||
"climate.mock_climate", # the underlying climate entity id
|
||||
)
|
||||
@@ -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,
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -641,8 +653,8 @@ async def test_bug_524(hass: HomeAssistant, skip_hass_states_is_state):
|
||||
CONF_PRESENCE_SENSOR: "binary_sensor.presence_sensor",
|
||||
CONF_CLIMATE: "climate.mock_climate",
|
||||
CONF_MINIMAL_ACTIVATION_DELAY: 30,
|
||||
CONF_SECURITY_DELAY_MIN: 5,
|
||||
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
|
||||
CONF_SAFETY_DELAY_MIN: 5,
|
||||
CONF_SAFETY_MIN_ON_PERCENT: 0.3,
|
||||
CONF_AUTO_FAN_MODE: CONF_AUTO_FAN_TURBO,
|
||||
CONF_AC_MODE: True,
|
||||
},
|
||||
@@ -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
|
||||
|
||||
@@ -904,8 +923,8 @@ async def test_manual_hvac_off_should_take_the_lead_over_window(
|
||||
CONF_PRESENCE_SENSOR: "binary_sensor.presence_sensor",
|
||||
CONF_CLIMATE: "climate.mock_climate",
|
||||
CONF_MINIMAL_ACTIVATION_DELAY: 30,
|
||||
CONF_SECURITY_DELAY_MIN: 5,
|
||||
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
|
||||
CONF_SAFETY_DELAY_MIN: 5,
|
||||
CONF_SAFETY_MIN_ON_PERCENT: 0.3,
|
||||
CONF_AUTO_FAN_MODE: CONF_AUTO_FAN_TURBO,
|
||||
CONF_AC_MODE: True,
|
||||
CONF_AUTO_START_STOP_LEVEL: AUTO_START_STOP_LEVEL_FAST,
|
||||
@@ -938,7 +957,10 @@ async def test_manual_hvac_off_should_take_the_lead_over_window(
|
||||
== AUTO_START_STOP_LEVEL_FAST
|
||||
)
|
||||
|
||||
assert vtherm.auto_start_stop_level == AUTO_START_STOP_LEVEL_FAST
|
||||
assert (
|
||||
vtherm.auto_start_stop_manager.auto_start_stop_level
|
||||
== AUTO_START_STOP_LEVEL_FAST
|
||||
)
|
||||
enable_entity = search_entity(
|
||||
hass, "switch.overclimate_enable_auto_start_stop", SWITCH_DOMAIN
|
||||
)
|
||||
@@ -949,7 +971,7 @@ async def test_manual_hvac_off_should_take_the_lead_over_window(
|
||||
now: datetime = datetime.now(tz=tz)
|
||||
|
||||
# 1. Set mode to Heat and preset to Comfort and close the window
|
||||
send_window_change_event(vtherm, False, False, now, False)
|
||||
await send_window_change_event(vtherm, False, False, now, False)
|
||||
await send_presence_change_event(vtherm, True, False, now)
|
||||
await send_temperature_change_event(vtherm, 18, now, True)
|
||||
await vtherm.async_set_hvac_mode(HVACMode.HEAT)
|
||||
@@ -960,7 +982,7 @@ async def test_manual_hvac_off_should_take_the_lead_over_window(
|
||||
# VTherm should be heating
|
||||
assert vtherm.hvac_mode == HVACMode.HEAT
|
||||
# VTherm window_state should be off
|
||||
assert vtherm.window_state == STATE_OFF
|
||||
assert vtherm.window_state == STATE_UNKNOWN # Cause try_condition is not called
|
||||
|
||||
# 2. Open the window and wait for the delay
|
||||
now = now + timedelta(minutes=2)
|
||||
@@ -1078,8 +1100,8 @@ async def test_manual_hvac_off_should_take_the_lead_over_auto_start_stop(
|
||||
CONF_PRESENCE_SENSOR: "binary_sensor.presence_sensor",
|
||||
CONF_CLIMATE: "climate.mock_climate",
|
||||
CONF_MINIMAL_ACTIVATION_DELAY: 30,
|
||||
CONF_SECURITY_DELAY_MIN: 5,
|
||||
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
|
||||
CONF_SAFETY_DELAY_MIN: 5,
|
||||
CONF_SAFETY_MIN_ON_PERCENT: 0.3,
|
||||
CONF_AUTO_FAN_MODE: CONF_AUTO_FAN_TURBO,
|
||||
CONF_AC_MODE: True,
|
||||
CONF_AUTO_START_STOP_LEVEL: AUTO_START_STOP_LEVEL_FAST,
|
||||
@@ -1112,7 +1134,10 @@ async def test_manual_hvac_off_should_take_the_lead_over_auto_start_stop(
|
||||
== AUTO_START_STOP_LEVEL_FAST
|
||||
)
|
||||
|
||||
assert vtherm.auto_start_stop_level == AUTO_START_STOP_LEVEL_FAST
|
||||
assert (
|
||||
vtherm.auto_start_stop_manager.auto_start_stop_level
|
||||
== AUTO_START_STOP_LEVEL_FAST
|
||||
)
|
||||
enable_entity = search_entity(
|
||||
hass, "switch.overclimate_enable_auto_start_stop", SWITCH_DOMAIN
|
||||
)
|
||||
@@ -1123,7 +1148,7 @@ async def test_manual_hvac_off_should_take_the_lead_over_auto_start_stop(
|
||||
now: datetime = datetime.now(tz=tz)
|
||||
|
||||
# 1. Set mode to Heat and preset to Comfort
|
||||
send_window_change_event(vtherm, False, False, now, False)
|
||||
await send_window_change_event(vtherm, False, False, now, False)
|
||||
await send_presence_change_event(vtherm, True, False, now)
|
||||
await send_temperature_change_event(vtherm, 18, now, True)
|
||||
await vtherm.async_set_hvac_mode(HVACMode.HEAT)
|
||||
@@ -1138,7 +1163,7 @@ async def test_manual_hvac_off_should_take_the_lead_over_auto_start_stop(
|
||||
now = now + timedelta(minutes=5)
|
||||
vtherm._set_now(now)
|
||||
# reset accumulated error (only for testing)
|
||||
vtherm._auto_start_stop_algo._accumulated_error = 0
|
||||
vtherm.auto_start_stop_manager._auto_start_stop_algo._accumulated_error = 0
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
|
||||
) as mock_send_event:
|
||||
|
||||
@@ -18,8 +18,8 @@ from .const import *
|
||||
logging.getLogger().setLevel(logging.DEBUG)
|
||||
|
||||
|
||||
# @pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
# @pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
# this test fails if run in // with the next because the underlying_valve_regulation is mixed. Don't know why
|
||||
# @pytest.mark.skip
|
||||
async def test_over_climate_valve_mono(hass: HomeAssistant, skip_hass_states_get):
|
||||
@@ -111,10 +111,10 @@ async def test_over_climate_valve_mono(hass: HomeAssistant, skip_hass_states_get
|
||||
PRESET_BOOST,
|
||||
]
|
||||
assert vtherm.preset_mode is PRESET_NONE
|
||||
assert vtherm._security_state is False
|
||||
assert vtherm._window_state is None
|
||||
assert vtherm._motion_state is None
|
||||
assert vtherm._presence_state is None
|
||||
assert vtherm.safety_manager.is_safety_detected is False
|
||||
assert vtherm.window_state is STATE_UNAVAILABLE
|
||||
assert vtherm.motion_state is STATE_UNAVAILABLE
|
||||
assert vtherm.presence_state is STATE_UNAVAILABLE
|
||||
|
||||
assert vtherm.is_device_active is False
|
||||
assert vtherm.valve_open_percent == 0
|
||||
@@ -138,13 +138,13 @@ async def test_over_climate_valve_mono(hass: HomeAssistant, skip_hass_states_get
|
||||
assert mock_service_call.call_count == 3
|
||||
mock_service_call.assert_has_calls(
|
||||
[
|
||||
call(domain='number', service='set_value', service_data={'value': 0}, target={'entity_id': 'number.mock_opening_degree'}),
|
||||
call(domain='number', service='set_value', service_data={'value': 100}, target={'entity_id': 'number.mock_closing_degree'}),
|
||||
call("climate","set_temperature",{
|
||||
"entity_id": "climate.mock_climate",
|
||||
"temperature": 15, # temp-min
|
||||
},
|
||||
),
|
||||
call(domain='number', service='set_value', service_data={'value': 0}, target={'entity_id': 'number.mock_opening_degree'}),
|
||||
call(domain='number', service='set_value', service_data={'value': 100}, target={'entity_id': 'number.mock_closing_degree'}),
|
||||
# we have no current_temperature yet
|
||||
# call(domain='number', service='set_value', service_data={'value': 12}, target={'entity_id': 'number.mock_offset_calibration'}),
|
||||
]
|
||||
@@ -300,6 +300,7 @@ async def test_over_climate_valve_mono(hass: HomeAssistant, skip_hass_states_get
|
||||
await hass.async_block_till_done()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_over_climate_valve_multi_presence(
|
||||
hass: HomeAssistant, skip_hass_states_get
|
||||
):
|
||||
@@ -482,3 +483,186 @@ async def test_over_climate_valve_multi_presence(
|
||||
)
|
||||
|
||||
assert vtherm.nb_device_actives == 0
|
||||
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_over_climate_valve_multi_min_opening_degrees(
|
||||
hass: HomeAssistant, skip_hass_states_get
|
||||
):
|
||||
"""Test the normal full start of a thermostat in thermostat_over_climate type
|
||||
with valve_regulation and min_opening_degreess set"""
|
||||
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
title="TheOverClimateMockName",
|
||||
unique_id="uniqueId",
|
||||
data={
|
||||
CONF_NAME: "TheOverClimateMockName",
|
||||
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
|
||||
CONF_CYCLE_MIN: 5,
|
||||
CONF_DEVICE_POWER: 1,
|
||||
CONF_USE_MAIN_CENTRAL_CONFIG: False,
|
||||
CONF_USE_CENTRAL_MODE: False,
|
||||
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_CLIMATE,
|
||||
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
|
||||
CONF_TEMP_MIN: 15,
|
||||
CONF_TEMP_MAX: 30,
|
||||
CONF_STEP_TEMPERATURE: 0.1,
|
||||
CONF_UNDERLYING_LIST: ["climate.mock_climate1", "climate.mock_climate2"],
|
||||
CONF_AC_MODE: False,
|
||||
CONF_AUTO_REGULATION_MODE: CONF_AUTO_REGULATION_VALVE,
|
||||
CONF_AUTO_REGULATION_DTEMP: 0.01,
|
||||
CONF_AUTO_REGULATION_PERIOD_MIN: 0,
|
||||
CONF_AUTO_FAN_MODE: CONF_AUTO_FAN_HIGH,
|
||||
CONF_AUTO_REGULATION_USE_DEVICE_TEMP: False,
|
||||
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
|
||||
CONF_TPI_COEF_INT: 0.3,
|
||||
CONF_TPI_COEF_EXT: 0.1,
|
||||
CONF_OPENING_DEGREE_LIST: [
|
||||
"number.mock_opening_degree1",
|
||||
"number.mock_opening_degree2",
|
||||
],
|
||||
CONF_CLOSING_DEGREE_LIST: [
|
||||
"number.mock_closing_degree1",
|
||||
"number.mock_closing_degree2",
|
||||
],
|
||||
CONF_OFFSET_CALIBRATION_LIST: [
|
||||
"number.mock_offset_calibration1",
|
||||
"number.mock_offset_calibration2",
|
||||
],
|
||||
CONF_USE_PRESENCE_FEATURE: False,
|
||||
CONF_USE_WINDOW_FEATURE: False,
|
||||
CONF_USE_MOTION_FEATURE: False,
|
||||
CONF_USE_POWER_FEATURE: False,
|
||||
CONF_MIN_OPENING_DEGREES: "60,70",
|
||||
}
|
||||
| MOCK_DEFAULT_CENTRAL_CONFIG
|
||||
| MOCK_ADVANCED_CONFIG,
|
||||
)
|
||||
|
||||
fake_underlying_climate1 = MockClimate(
|
||||
hass, "mockUniqueId1", "MockClimateName1", {}
|
||||
)
|
||||
fake_underlying_climate2 = MockClimate(
|
||||
hass, "mockUniqueId2", "MockClimateName2", {}
|
||||
)
|
||||
|
||||
# mock_get_state will be called for each OPENING/CLOSING/OFFSET_CALIBRATION list
|
||||
mock_get_state_side_effect = SideEffects(
|
||||
{
|
||||
# Valve 1 is open
|
||||
"number.mock_opening_degree1": State(
|
||||
"number.mock_opening_degree1", "10", {"min": 0, "max": 100}
|
||||
),
|
||||
"number.mock_closing_degree1": State(
|
||||
"number.mock_closing_degree1", "90", {"min": 0, "max": 100}
|
||||
),
|
||||
"number.mock_offset_calibration1": State(
|
||||
"number.mock_offset_calibration1", "0", {"min": -12, "max": 12}
|
||||
),
|
||||
# Valve 2 is closed
|
||||
"number.mock_opening_degree2": State(
|
||||
"number.mock_opening_degree2", "0", {"min": 0, "max": 100}
|
||||
),
|
||||
"number.mock_closing_degree2": State(
|
||||
"number.mock_closing_degree2", "100", {"min": 0, "max": 100}
|
||||
),
|
||||
"number.mock_offset_calibration2": State(
|
||||
"number.mock_offset_calibration2", "10", {"min": -12, "max": 12}
|
||||
),
|
||||
},
|
||||
State("unknown.entity_id", "unknown"),
|
||||
)
|
||||
|
||||
# 1. initialize the VTherm
|
||||
tz = get_tz(hass) # pylint: disable=invalid-name
|
||||
now: datetime = datetime.now(tz=tz)
|
||||
|
||||
# fmt: off
|
||||
with patch("custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event") as mock_send_event, \
|
||||
patch("custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate", side_effect=[fake_underlying_climate1, fake_underlying_climate2]) as mock_find_climate, \
|
||||
patch("homeassistant.core.ServiceRegistry.async_call") as mock_service_call,\
|
||||
patch("homeassistant.core.StateMachine.get", side_effect=mock_get_state_side_effect.get_side_effects()) as mock_get_state:
|
||||
# fmt: on
|
||||
|
||||
vtherm: ThermostatOverClimateValve = await create_thermostat(hass, entry, "climate.theoverclimatemockname")
|
||||
assert vtherm
|
||||
assert isinstance(vtherm, ThermostatOverClimateValve)
|
||||
|
||||
assert vtherm.name == "TheOverClimateMockName"
|
||||
assert vtherm.is_over_climate is True
|
||||
assert vtherm.have_valve_regulation is True
|
||||
|
||||
vtherm._set_now(now)
|
||||
|
||||
# initialize the temps
|
||||
await set_all_climate_preset_temp(hass, vtherm, default_temperatures, "theoverclimatemockname")
|
||||
|
||||
await send_temperature_change_event(vtherm, 20, now, True)
|
||||
await send_ext_temperature_change_event(vtherm, 20, now, True)
|
||||
await send_presence_change_event(vtherm, False, True, now)
|
||||
|
||||
await vtherm.async_set_preset_mode(PRESET_COMFORT)
|
||||
await vtherm.async_set_hvac_mode(HVACMode.HEAT)
|
||||
|
||||
assert vtherm.target_temperature == 19
|
||||
assert vtherm.nb_device_actives == 0
|
||||
|
||||
# 2: set temperature -> should activate the valve and change target
|
||||
# fmt: off
|
||||
with patch("custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event") as mock_send_event, \
|
||||
patch("homeassistant.core.ServiceRegistry.async_call") as mock_service_call,\
|
||||
patch("homeassistant.core.StateMachine.get", side_effect=mock_get_state_side_effect.get_side_effects()) as mock_get_state:
|
||||
# fmt: on
|
||||
now = now + timedelta(minutes=3)
|
||||
vtherm._set_now(now)
|
||||
|
||||
await send_temperature_change_event(vtherm, 18, now, True)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert vtherm.is_device_active is True
|
||||
assert vtherm.valve_open_percent == 20
|
||||
|
||||
# the underlying set temperature call and the call to the valve
|
||||
assert mock_service_call.call_count == 6
|
||||
mock_service_call.assert_has_calls([
|
||||
# min is 60
|
||||
call(domain='number', service='set_value', service_data={'value': 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': 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'})
|
||||
]
|
||||
)
|
||||
|
||||
assert vtherm.nb_device_actives >= 2 # should be 2 but when run in // with the first test it give 3
|
||||
|
||||
# 3: set high temperature -> should deactivate the valve and change target
|
||||
# fmt: off
|
||||
with patch("custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event") as mock_send_event, \
|
||||
patch("homeassistant.core.ServiceRegistry.async_call") as mock_service_call,\
|
||||
patch("homeassistant.core.StateMachine.get", side_effect=mock_get_state_side_effect.get_side_effects()) as mock_get_state:
|
||||
# fmt: on
|
||||
now = now + timedelta(minutes=3)
|
||||
vtherm._set_now(now)
|
||||
|
||||
await send_temperature_change_event(vtherm, 22, now, True)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert vtherm.is_device_active is False
|
||||
assert vtherm.valve_open_percent == 0
|
||||
|
||||
# the underlying set temperature call and the call to the valve
|
||||
assert mock_service_call.call_count == 6
|
||||
mock_service_call.assert_has_calls([
|
||||
call(domain='number', service='set_value', service_data={'value': 0}, target={'entity_id': 'number.mock_opening_degree1'}),
|
||||
call(domain='number', service='set_value', service_data={'value': 100}, target={'entity_id': 'number.mock_closing_degree1'}),
|
||||
call(domain='number', service='set_value', service_data={'value': 7.0}, target={'entity_id': 'number.mock_offset_calibration1'}),
|
||||
call(domain='number', service='set_value', service_data={'value': 0}, target={'entity_id': 'number.mock_opening_degree2'}),
|
||||
call(domain='number', service='set_value', service_data={'value': 100}, target={'entity_id': 'number.mock_closing_degree2'}),
|
||||
call(domain='number', service='set_value', service_data={'value': 12}, target={'entity_id': 'number.mock_offset_calibration2'})
|
||||
]
|
||||
)
|
||||
|
||||
assert vtherm.nb_device_actives == 0
|
||||
|
||||
@@ -1,24 +1,338 @@
|
||||
# pylint: disable=protected-access, unused-argument, line-too-long
|
||||
""" Test the Power management """
|
||||
from unittest.mock import patch, call
|
||||
from unittest.mock import patch, call, AsyncMock, MagicMock, PropertyMock
|
||||
from datetime import datetime, timedelta
|
||||
import logging
|
||||
|
||||
from custom_components.versatile_thermostat.thermostat_switch import (
|
||||
ThermostatOverSwitch,
|
||||
)
|
||||
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
|
||||
|
||||
logging.getLogger().setLevel(logging.DEBUG)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"is_over_climate, is_device_active, power, max_power, check_power_available",
|
||||
[
|
||||
# don't switch to overpower (power is enough)
|
||||
(False, False, 1000, 3000, True),
|
||||
# switch to overpower (power is not enough)
|
||||
(False, False, 2000, 3000, False),
|
||||
# don't switch to overpower (power is not enough but device is already on)
|
||||
(False, True, 2000, 3000, True),
|
||||
# Same with a over_climate
|
||||
# don't switch to overpower (power is enough)
|
||||
(True, False, 1000, 3000, True),
|
||||
# switch to overpower (power is not enough)
|
||||
(True, False, 2000, 3000, False),
|
||||
# don't switch to overpower (power is not enough but device is already on)
|
||||
(True, True, 2000, 3000, True),
|
||||
# Leave overpowering state
|
||||
# switch to not overpower (power is enough)
|
||||
(False, False, 1000, 3000, True),
|
||||
# don't switch to overpower (power is still not enough)
|
||||
(False, False, 2000, 3000, False),
|
||||
# keep overpower (power is not enough but device is already on)
|
||||
(False, True, 3000, 3000, False),
|
||||
],
|
||||
)
|
||||
async def test_power_feature_manager(
|
||||
hass: HomeAssistant,
|
||||
is_over_climate,
|
||||
is_device_active,
|
||||
power,
|
||||
max_power,
|
||||
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)
|
||||
|
||||
assert power_manager is not None
|
||||
assert power_manager.is_configured is False
|
||||
assert power_manager.overpowering_state == STATE_UNAVAILABLE
|
||||
assert power_manager.name == "the name"
|
||||
|
||||
assert len(power_manager._active_listener) == 0
|
||||
|
||||
custom_attributes = {}
|
||||
power_manager.add_custom_attributes(custom_attributes)
|
||||
assert custom_attributes["power_sensor_entity_id"] is None
|
||||
assert custom_attributes["max_power_sensor_entity_id"] is None
|
||||
assert custom_attributes["overpowering_state"] == STATE_UNAVAILABLE
|
||||
assert custom_attributes["is_power_configured"] is False
|
||||
assert custom_attributes["device_power"] is 0
|
||||
assert custom_attributes["power_temp"] is None
|
||||
assert custom_attributes["current_power"] is None
|
||||
assert custom_attributes["current_max_power"] is None
|
||||
|
||||
# 2. 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
|
||||
|
||||
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"] == STATE_UNKNOWN
|
||||
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"] is None
|
||||
assert custom_attributes["current_max_power"] is None
|
||||
|
||||
# 3. 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) == 0 # no more listening
|
||||
|
||||
# 4. test refresh and check_overpowering with the parametrized
|
||||
# fmt:off
|
||||
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"
|
||||
type(fake_vtherm).hvac_mode = PropertyMock(return_value=HVACMode.HEAT)
|
||||
type(fake_vtherm).is_device_active = PropertyMock(return_value=is_device_active)
|
||||
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)
|
||||
|
||||
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()
|
||||
fake_vtherm.save_preset_mode = MagicMock()
|
||||
fake_vtherm.restore_preset_mode = AsyncMock()
|
||||
fake_vtherm.async_underlying_entity_turn_off = AsyncMock()
|
||||
fake_vtherm.async_set_preset_mode_internal = AsyncMock()
|
||||
fake_vtherm.send_event = MagicMock()
|
||||
fake_vtherm.update_custom_attributes = MagicMock()
|
||||
|
||||
|
||||
# Call set_overpowering
|
||||
await power_manager.set_overpowering(is_overpowering, 1234)
|
||||
|
||||
assert power_manager.overpowering_state == new_overpowering_state
|
||||
|
||||
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
|
||||
|
||||
if current_overpowering_state == STATE_ON:
|
||||
assert fake_vtherm.update_custom_attributes.call_count == 1
|
||||
assert fake_vtherm.restore_preset_mode.call_count == 1
|
||||
if is_over_climate:
|
||||
assert fake_vtherm.restore_hvac_mode.call_count == 1
|
||||
else:
|
||||
assert fake_vtherm.restore_hvac_mode.call_count == 0
|
||||
else:
|
||||
assert fake_vtherm.update_custom_attributes.call_count == 0
|
||||
|
||||
if msg_sent:
|
||||
fake_vtherm.send_event.assert_has_calls(
|
||||
[
|
||||
call.fake_vtherm.send_event(
|
||||
EventType.POWER_EVENT,
|
||||
{
|
||||
"type": "end",
|
||||
"current_power": 1000,
|
||||
"device_power": 1234,
|
||||
"current_max_power": 2000,
|
||||
},
|
||||
),
|
||||
]
|
||||
)
|
||||
# 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
|
||||
|
||||
if current_overpowering_state == STATE_OFF:
|
||||
assert fake_vtherm.save_preset_mode.call_count == 1
|
||||
assert fake_vtherm.async_underlying_entity_turn_off.call_count == 1
|
||||
assert fake_vtherm.async_set_preset_mode_internal.call_count == 1
|
||||
assert fake_vtherm.send_event.call_count == 1
|
||||
assert fake_vtherm.update_custom_attributes.call_count == 1
|
||||
else:
|
||||
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 == 0
|
||||
assert fake_vtherm.update_custom_attributes.call_count == 0
|
||||
assert fake_vtherm.restore_hvac_mode.call_count == 0
|
||||
assert fake_vtherm.restore_preset_mode.call_count == 0
|
||||
|
||||
if msg_sent:
|
||||
fake_vtherm.send_event.assert_has_calls(
|
||||
[
|
||||
call.fake_vtherm.send_event(
|
||||
EventType.POWER_EVENT,
|
||||
{
|
||||
"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
|
||||
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"] == 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"] == 1000
|
||||
assert custom_attributes["current_max_power"] == 2000
|
||||
|
||||
power_manager.stop_listening()
|
||||
await hass.async_block_till_done()
|
||||
|
||||
|
||||
@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",
|
||||
@@ -31,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_SECURITY_DELAY_MIN: 5,
|
||||
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
|
||||
CONF_POWER_SENSOR: "sensor.mock_power_sensor",
|
||||
CONF_MAX_POWER_SENSOR: "sensor.mock_power_max_sensor",
|
||||
CONF_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"
|
||||
hass, entry, "climate.theoverswitchmockname", temps
|
||||
)
|
||||
assert entity
|
||||
|
||||
@@ -63,37 +372,56 @@ async def test_power_management_hvac_off(
|
||||
await entity.async_set_preset_mode(PRESET_BOOST)
|
||||
assert entity.preset_mode is PRESET_BOOST
|
||||
assert entity.target_temperature == 19
|
||||
assert entity.overpowering_state is None
|
||||
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.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.overpowering_state is None
|
||||
# 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.check_overpowering() is False
|
||||
# All configuration is complete and power is < power_max
|
||||
assert entity.preset_mode is PRESET_BOOST
|
||||
assert entity.overpowering_state is False
|
||||
# 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.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.overpowering_state is True
|
||||
assert entity.power_manager.overpowering_state is STATE_UNKNOWN
|
||||
|
||||
assert mock_send_event.call_count == 0
|
||||
assert mock_heater_on.call_count == 0
|
||||
@@ -102,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",
|
||||
@@ -117,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_SECURITY_DELAY_MIN: 5,
|
||||
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
|
||||
CONF_POWER_SENSOR: "sensor.mock_power_sensor",
|
||||
CONF_MAX_POWER_SENSOR: "sensor.mock_power_max_sensor",
|
||||
CONF_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"
|
||||
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
|
||||
|
||||
@@ -150,31 +484,55 @@ async def test_power_management_hvac_on(hass: HomeAssistant, skip_hass_states_is
|
||||
await entity.async_set_preset_mode(PRESET_BOOST)
|
||||
assert entity.hvac_mode is HVACMode.HEAT
|
||||
assert entity.preset_mode is PRESET_BOOST
|
||||
assert entity.overpowering_state is None
|
||||
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.check_overpowering() is False
|
||||
# All configuration is complete and power is < power_max
|
||||
assert entity.preset_mode is PRESET_BOOST
|
||||
assert entity.overpowering_state is False
|
||||
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.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.overpowering_state is True
|
||||
assert entity.power_manager.overpowering_state is STATE_ON
|
||||
assert entity.target_temperature == 12
|
||||
|
||||
assert mock_send_event.call_count == 2
|
||||
@@ -187,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_power_max": 149,
|
||||
"current_max_power": 49,
|
||||
"current_power_consumption": 100.0,
|
||||
},
|
||||
),
|
||||
@@ -197,19 +555,23 @@ 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.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.overpowering_state is False
|
||||
assert entity.power_manager.overpowering_state is STATE_OFF
|
||||
assert entity.target_temperature == 19
|
||||
|
||||
assert mock_send_event.call_count == 2
|
||||
@@ -222,7 +584,7 @@ async def test_power_management_hvac_on(hass: HomeAssistant, skip_hass_states_is
|
||||
"type": "end",
|
||||
"current_power": 48,
|
||||
"device_power": 100,
|
||||
"current_power_max": 149,
|
||||
"current_max_power": 149,
|
||||
},
|
||||
),
|
||||
],
|
||||
@@ -236,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",
|
||||
@@ -252,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_SECURITY_DELAY_MIN: 5,
|
||||
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
|
||||
CONF_POWER_SENSOR: "sensor.mock_power_sensor",
|
||||
CONF_MAX_POWER_SENSOR: "sensor.mock_power_max_sensor",
|
||||
CONF_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"
|
||||
hass, entry, "climate.theoverswitchmockname", temps
|
||||
)
|
||||
assert entity
|
||||
|
||||
@@ -297,13 +659,15 @@ 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
|
||||
assert entity.current_temperature == 15
|
||||
assert tpi_algo.on_percent == 1
|
||||
|
||||
assert entity.device_power == 100.0
|
||||
assert entity.power_manager.device_power == 100.0
|
||||
|
||||
assert mock_send_event.call_count == 2
|
||||
assert mock_heater_on.call_count == 1
|
||||
@@ -324,7 +688,7 @@ async def test_power_management_energy_over_switch(
|
||||
) as mock_heater_off:
|
||||
await send_temperature_change_event(entity, 18, datetime.now())
|
||||
assert tpi_algo.on_percent == 0.3
|
||||
assert entity.mean_cycle_power == 30.0
|
||||
assert entity.power_manager.mean_cycle_power == 30.0
|
||||
|
||||
assert mock_send_event.call_count == 0
|
||||
assert mock_heater_on.call_count == 0
|
||||
@@ -346,7 +710,7 @@ async def test_power_management_energy_over_switch(
|
||||
) as mock_heater_off:
|
||||
await send_temperature_change_event(entity, 20, datetime.now())
|
||||
assert tpi_algo.on_percent == 0.0
|
||||
assert entity.mean_cycle_power == 0.0
|
||||
assert entity.power_manager.mean_cycle_power == 0.0
|
||||
|
||||
assert mock_send_event.call_count == 0
|
||||
assert mock_heater_on.call_count == 0
|
||||
@@ -368,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",
|
||||
@@ -385,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_SECURITY_DELAY_MIN: 5,
|
||||
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
|
||||
CONF_POWER_SENSOR: "sensor.mock_power_sensor",
|
||||
CONF_MAX_POWER_SENSOR: "sensor.mock_power_max_sensor",
|
||||
CONF_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.theoverclimatemockname"
|
||||
hass, entry, "climate.theoverclimatemockname", temps
|
||||
)
|
||||
assert entity
|
||||
assert entity.is_over_climate
|
||||
@@ -421,7 +786,7 @@ async def test_power_management_energy_over_climate(
|
||||
assert entity.current_temperature == 15
|
||||
|
||||
# Not initialised yet
|
||||
assert entity.mean_cycle_power is None
|
||||
assert entity.power_manager.mean_cycle_power is None
|
||||
assert entity._underlying_climate_start_hvac_action_date is None
|
||||
|
||||
# Send a climate_change event with HVACAction=HEATING
|
||||
|
||||
178
tests/test_presence.py
Normal file
178
tests/test_presence.py
Normal file
@@ -0,0 +1,178 @@
|
||||
# pylint: disable=wildcard-import, unused-wildcard-import, protected-access, unused-argument, line-too-long
|
||||
|
||||
""" Test the Security featrure """
|
||||
import logging
|
||||
from unittest.mock import patch, call, AsyncMock, MagicMock, PropertyMock
|
||||
|
||||
# from datetime import timedelta, datetime
|
||||
|
||||
from custom_components.versatile_thermostat.base_thermostat import BaseThermostat
|
||||
from custom_components.versatile_thermostat.feature_presence_manager import (
|
||||
FeaturePresenceManager,
|
||||
)
|
||||
|
||||
from .commons import *
|
||||
|
||||
logging.getLogger().setLevel(logging.DEBUG)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"temp, absence, state, nb_call, presence_state, changed",
|
||||
[
|
||||
(19, False, STATE_ON, 1, STATE_ON, True),
|
||||
(17, True, STATE_OFF, 1, STATE_OFF, True),
|
||||
(19, False, STATE_HOME, 1, STATE_ON, True),
|
||||
(17, True, STATE_NOT_HOME, 1, STATE_OFF, True),
|
||||
(17, False, STATE_UNAVAILABLE, 0, STATE_UNKNOWN, False),
|
||||
(17, False, STATE_UNKNOWN, 0, STATE_UNKNOWN, False),
|
||||
(17, False, "wrong state", 0, STATE_UNKNOWN, False),
|
||||
],
|
||||
)
|
||||
async def test_presence_feature_manager(
|
||||
hass: HomeAssistant, temp, absence, state, nb_call, presence_state, changed
|
||||
):
|
||||
"""Test the FeaturePresenceManager class direclty"""
|
||||
|
||||
fake_vtherm = MagicMock(spec=BaseThermostat)
|
||||
type(fake_vtherm).name = PropertyMock(return_value="the name")
|
||||
type(fake_vtherm).preset_mode = PropertyMock(return_value=PRESET_COMFORT)
|
||||
|
||||
# 1. creation
|
||||
presence_manager = FeaturePresenceManager(fake_vtherm, hass)
|
||||
|
||||
assert presence_manager is not None
|
||||
assert presence_manager.is_configured is False
|
||||
assert presence_manager.is_absence_detected is False
|
||||
assert presence_manager.presence_state == STATE_UNAVAILABLE
|
||||
assert presence_manager.name == "the name"
|
||||
|
||||
assert len(presence_manager._active_listener) == 0
|
||||
|
||||
custom_attributes = {}
|
||||
presence_manager.add_custom_attributes(custom_attributes)
|
||||
assert custom_attributes["presence_sensor_entity_id"] is None
|
||||
assert custom_attributes["presence_state"] == STATE_UNAVAILABLE
|
||||
assert custom_attributes["is_presence_configured"] is False
|
||||
|
||||
# 2. post_init
|
||||
presence_manager.post_init(
|
||||
{
|
||||
CONF_PRESENCE_SENSOR: "sensor.the_presence_sensor",
|
||||
CONF_USE_PRESENCE_FEATURE: True,
|
||||
}
|
||||
)
|
||||
|
||||
assert presence_manager.is_configured is True
|
||||
assert presence_manager.presence_state == STATE_UNKNOWN
|
||||
assert presence_manager.is_absence_detected is False
|
||||
|
||||
custom_attributes = {}
|
||||
presence_manager.add_custom_attributes(custom_attributes)
|
||||
assert (
|
||||
custom_attributes["presence_sensor_entity_id"] == "sensor.the_presence_sensor"
|
||||
)
|
||||
assert custom_attributes["presence_state"] == STATE_UNKNOWN
|
||||
assert custom_attributes["is_presence_configured"] is True
|
||||
|
||||
# 3. 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
|
||||
|
||||
assert len(presence_manager._active_listener) == 1
|
||||
|
||||
# 4. test refresh with the parametrized
|
||||
# fmt:off
|
||||
with patch("homeassistant.core.StateMachine.get", return_value=State("sensor.the_presence_sensor", state)) as mock_get_state:
|
||||
# fmt:on
|
||||
# Configurer les méthodes mockées
|
||||
fake_vtherm.find_preset_temp.return_value = temp
|
||||
fake_vtherm.change_target_temperature = AsyncMock()
|
||||
fake_vtherm.async_control_heating = AsyncMock()
|
||||
|
||||
ret = await presence_manager.refresh_state()
|
||||
assert ret == changed
|
||||
assert presence_manager.is_configured is True
|
||||
assert presence_manager.presence_state == presence_state
|
||||
assert presence_manager.is_absence_detected is absence
|
||||
|
||||
assert mock_get_state.call_count == 1
|
||||
|
||||
assert fake_vtherm.find_preset_temp.call_count == nb_call
|
||||
|
||||
if nb_call == 1:
|
||||
fake_vtherm.find_preset_temp.assert_has_calls(
|
||||
[
|
||||
call.find_preset_temp(PRESET_COMFORT),
|
||||
]
|
||||
)
|
||||
|
||||
assert fake_vtherm.change_target_temperature.call_count == nb_call
|
||||
fake_vtherm.change_target_temperature.assert_has_calls(
|
||||
[
|
||||
call.find_preset_temp(temp),
|
||||
]
|
||||
)
|
||||
|
||||
assert fake_vtherm.async_control_heating.call_count == 0
|
||||
|
||||
fake_vtherm.reset_mock()
|
||||
|
||||
# 5. Check custom_attributes
|
||||
custom_attributes = {}
|
||||
presence_manager.add_custom_attributes(custom_attributes)
|
||||
assert custom_attributes["presence_sensor_entity_id"] == "sensor.the_presence_sensor"
|
||||
assert custom_attributes["presence_state"] == presence_state
|
||||
assert custom_attributes["is_presence_configured"] is True
|
||||
|
||||
# 6. test _presence_sensor_changed with the parametrized
|
||||
fake_vtherm.find_preset_temp.return_value = temp
|
||||
fake_vtherm.change_target_temperature = AsyncMock()
|
||||
fake_vtherm.async_control_heating = AsyncMock()
|
||||
|
||||
await presence_manager._presence_sensor_changed(
|
||||
event=Event(
|
||||
event_type=EVENT_STATE_CHANGED,
|
||||
data={
|
||||
"entity_id": "sensor.the_presence_sensor",
|
||||
"new_state": State("sensor.the_presence_sensor", state),
|
||||
"old_state": State("sensor.the_presence_sensor", STATE_UNAVAILABLE),
|
||||
}))
|
||||
assert ret == changed
|
||||
assert presence_manager.is_configured is True
|
||||
assert presence_manager.presence_state == presence_state
|
||||
assert presence_manager.is_absence_detected is absence
|
||||
|
||||
assert fake_vtherm.find_preset_temp.call_count == nb_call
|
||||
|
||||
if nb_call == 1:
|
||||
fake_vtherm.find_preset_temp.assert_has_calls(
|
||||
[
|
||||
call.find_preset_temp(PRESET_COMFORT),
|
||||
]
|
||||
)
|
||||
|
||||
assert fake_vtherm.change_target_temperature.call_count == nb_call
|
||||
fake_vtherm.change_target_temperature.assert_has_calls(
|
||||
[
|
||||
call.find_preset_temp(temp),
|
||||
]
|
||||
)
|
||||
|
||||
assert fake_vtherm.async_control_heating.call_count == 1
|
||||
fake_vtherm.async_control_heating.assert_has_calls([
|
||||
call.async_control_heating(force=True)
|
||||
])
|
||||
|
||||
fake_vtherm.reset_mock()
|
||||
|
||||
# 7. Check custom_attributes
|
||||
custom_attributes = {}
|
||||
presence_manager.add_custom_attributes(custom_attributes)
|
||||
assert custom_attributes["presence_sensor_entity_id"] == "sensor.the_presence_sensor"
|
||||
assert custom_attributes["presence_state"] == presence_state
|
||||
assert custom_attributes["is_presence_configured"] is True
|
||||
|
||||
presence_manager.stop_listening()
|
||||
await hass.async_block_till_done()
|
||||
@@ -1,7 +1,7 @@
|
||||
# pylint: disable=wildcard-import, unused-wildcard-import, protected-access, unused-argument, line-too-long
|
||||
|
||||
""" Test the Security featrure """
|
||||
from unittest.mock import patch, call
|
||||
from unittest.mock import patch, call, PropertyMock, MagicMock
|
||||
from datetime import timedelta, datetime
|
||||
import logging
|
||||
|
||||
@@ -11,12 +11,103 @@ from custom_components.versatile_thermostat.thermostat_climate import (
|
||||
from custom_components.versatile_thermostat.thermostat_switch import (
|
||||
ThermostatOverSwitch,
|
||||
)
|
||||
from custom_components.versatile_thermostat.feature_safety_manager import (
|
||||
FeatureSafetyManager,
|
||||
)
|
||||
from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import
|
||||
|
||||
|
||||
logging.getLogger().setLevel(logging.DEBUG)
|
||||
|
||||
|
||||
async def test_safety_feature_manager_create(
|
||||
hass: HomeAssistant,
|
||||
):
|
||||
"""Test the FeatureMotionManager class direclty"""
|
||||
|
||||
fake_vtherm = MagicMock(spec=BaseThermostat)
|
||||
type(fake_vtherm).name = PropertyMock(return_value="the name")
|
||||
|
||||
# 1. creation
|
||||
safety_manager = FeatureSafetyManager(fake_vtherm, hass)
|
||||
|
||||
assert safety_manager is not None
|
||||
assert safety_manager.is_configured is False
|
||||
assert safety_manager.is_safety_detected is False
|
||||
assert safety_manager.safety_state is STATE_UNAVAILABLE
|
||||
assert safety_manager.name == "the name"
|
||||
|
||||
assert len(safety_manager._active_listener) == 0
|
||||
|
||||
custom_attributes = {}
|
||||
safety_manager.add_custom_attributes(custom_attributes)
|
||||
assert custom_attributes["is_safety_configured"] is False
|
||||
assert custom_attributes["safety_state"] is STATE_UNAVAILABLE
|
||||
assert custom_attributes.get("safety_delay_min", None) is None
|
||||
assert custom_attributes.get("safety_min_on_percent", None) is None
|
||||
assert custom_attributes.get("safety_default_on_percent", None) is None
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"safety_delay_min, safety_min_on_percent, safety_default_on_percent, is_configured, state",
|
||||
[
|
||||
# fmt: off
|
||||
( 10, 11, 12, True, STATE_UNKNOWN),
|
||||
( None, 11, 12, False, STATE_UNAVAILABLE),
|
||||
( 10, None, 12, True, STATE_UNKNOWN),
|
||||
( 10, 11, None, True, STATE_UNKNOWN),
|
||||
( 10, None, None, True, STATE_UNKNOWN),
|
||||
( None, None, None, False, STATE_UNAVAILABLE),
|
||||
# fmt: on
|
||||
],
|
||||
)
|
||||
async def test_safety_feature_manager_post_init(
|
||||
hass: HomeAssistant,
|
||||
safety_delay_min,
|
||||
safety_min_on_percent,
|
||||
safety_default_on_percent,
|
||||
is_configured,
|
||||
state,
|
||||
):
|
||||
"""Test the FeatureSafetyManager class direclty"""
|
||||
|
||||
fake_vtherm = MagicMock(spec=BaseThermostat)
|
||||
type(fake_vtherm).name = PropertyMock(return_value="the name")
|
||||
|
||||
# 1. creation
|
||||
safety_manager = FeatureSafetyManager(fake_vtherm, hass)
|
||||
assert safety_manager is not None
|
||||
|
||||
# 2. post_init
|
||||
safety_manager.post_init(
|
||||
{
|
||||
CONF_SAFETY_DELAY_MIN: safety_delay_min,
|
||||
CONF_SAFETY_MIN_ON_PERCENT: safety_min_on_percent,
|
||||
CONF_SAFETY_DEFAULT_ON_PERCENT: safety_default_on_percent,
|
||||
}
|
||||
)
|
||||
|
||||
assert safety_manager.is_configured is is_configured
|
||||
assert safety_manager.safety_state is state
|
||||
|
||||
custom_attributes = {}
|
||||
safety_manager.add_custom_attributes(custom_attributes)
|
||||
assert custom_attributes["is_safety_configured"] is is_configured
|
||||
assert custom_attributes["safety_state"] is state
|
||||
|
||||
if safety_manager.is_configured:
|
||||
assert custom_attributes.get("safety_delay_min", None) == safety_delay_min
|
||||
assert (
|
||||
custom_attributes.get("safety_min_on_percent", None)
|
||||
== safety_min_on_percent
|
||||
or DEFAULT_SAFETY_MIN_ON_PERCENT
|
||||
)
|
||||
assert (
|
||||
custom_attributes.get("safety_default_on_percent", None)
|
||||
== safety_default_on_percent
|
||||
or DEFAULT_SAFETY_DEFAULT_ON_PERCENT
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_security_feature(hass: HomeAssistant, skip_hass_states_is_state):
|
||||
@@ -24,17 +115,26 @@ async def test_security_feature(hass: HomeAssistant, skip_hass_states_is_state):
|
||||
1. creates a thermostat and check that security is off
|
||||
2. activate security feature when date is expired
|
||||
3. change the preset to boost
|
||||
4. check that security is still on
|
||||
5. resolve the date issue
|
||||
6. check that security is off and preset is changed to boost
|
||||
"""
|
||||
|
||||
tz = get_tz(hass) # pylint: disable=invalid-name
|
||||
|
||||
temps = {
|
||||
"frost": 7,
|
||||
"eco": 17,
|
||||
"comfort": 18,
|
||||
"boost": 19,
|
||||
}
|
||||
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
title="TheOverSwitchMockName",
|
||||
unique_id="uniqueId",
|
||||
# With migration
|
||||
version=CONFIG_VERSION,
|
||||
minor_version=0,
|
||||
data={
|
||||
"name": "TheOverSwitchMockName",
|
||||
"thermostat_type": "thermostat_over_switch",
|
||||
@@ -43,15 +143,11 @@ async def test_security_feature(hass: HomeAssistant, skip_hass_states_is_state):
|
||||
"cycle_min": 5,
|
||||
"temp_min": 15,
|
||||
"temp_max": 30,
|
||||
"frost_temp": 7,
|
||||
"eco_temp": 17,
|
||||
"comfort_temp": 18,
|
||||
"boost_temp": 19,
|
||||
"use_window_feature": False,
|
||||
"use_motion_feature": False,
|
||||
"use_power_feature": False,
|
||||
"use_presence_feature": False,
|
||||
"heater_entity_id": "switch.mock_switch",
|
||||
CONF_UNDERLYING_LIST: ["switch.mock_switch"],
|
||||
"proportional_function": "tpi",
|
||||
"tpi_coef_int": 0.3,
|
||||
"tpi_coef_ext": 0.01,
|
||||
@@ -69,8 +165,11 @@ async def test_security_feature(hass: HomeAssistant, skip_hass_states_is_state):
|
||||
)
|
||||
assert entity
|
||||
|
||||
assert entity._security_state is False
|
||||
assert entity.preset_mode is not PRESET_SECURITY
|
||||
await set_all_climate_preset_temp(hass, entity, temps, "theoverswitchmockname")
|
||||
|
||||
assert entity.safety_manager.safety_state is not STATE_ON
|
||||
assert entity.safety_manager.is_safety_detected is False
|
||||
assert entity.preset_mode is not PRESET_SAFETY
|
||||
assert entity.preset_modes == [
|
||||
PRESET_NONE,
|
||||
PRESET_FROST_PROTECTION,
|
||||
@@ -105,8 +204,8 @@ async def test_security_feature(hass: HomeAssistant, skip_hass_states_is_state):
|
||||
|
||||
# set temperature to 15 so that on_percent will be > security_min_on_percent (0.2)
|
||||
await send_temperature_change_event(entity, 15, event_timestamp)
|
||||
assert entity.security_state is True
|
||||
assert entity.preset_mode == PRESET_SECURITY
|
||||
assert entity.safety_state is STATE_ON
|
||||
assert entity.preset_mode == PRESET_SAFETY
|
||||
assert entity._saved_preset_mode == PRESET_COMFORT
|
||||
assert entity._prop_algorithm.on_percent == 0.1
|
||||
assert entity._prop_algorithm.calculated_on_percent == 0.9
|
||||
@@ -114,7 +213,7 @@ async def test_security_feature(hass: HomeAssistant, skip_hass_states_is_state):
|
||||
assert mock_send_event.call_count == 3
|
||||
mock_send_event.assert_has_calls(
|
||||
[
|
||||
call.send_event(EventType.PRESET_EVENT, {"preset": PRESET_SECURITY}),
|
||||
call.send_event(EventType.PRESET_EVENT, {"preset": PRESET_SAFETY}),
|
||||
call.send_event(
|
||||
EventType.TEMPERATURE_EVENT,
|
||||
{
|
||||
@@ -151,11 +250,13 @@ async def test_security_feature(hass: HomeAssistant, skip_hass_states_is_state):
|
||||
await entity.async_set_preset_mode(PRESET_BOOST)
|
||||
|
||||
# 4. check that security is still on
|
||||
assert entity._security_state is True
|
||||
assert entity.safety_manager.safety_state is STATE_ON
|
||||
assert entity.safety_manager.is_safety_detected is True
|
||||
|
||||
assert entity._prop_algorithm.on_percent == 0.1
|
||||
assert entity._prop_algorithm.calculated_on_percent == 0.9
|
||||
assert entity._saved_preset_mode == PRESET_BOOST
|
||||
assert entity.preset_mode is PRESET_SECURITY
|
||||
assert entity.preset_mode is PRESET_SAFETY
|
||||
|
||||
# 5. resolve the datetime issue
|
||||
with patch(
|
||||
@@ -168,7 +269,9 @@ async def test_security_feature(hass: HomeAssistant, skip_hass_states_is_state):
|
||||
# set temperature to 15 so that on_percent will be > security_min_on_percent (0.2)
|
||||
await send_temperature_change_event(entity, 15.2, event_timestamp)
|
||||
|
||||
assert entity._security_state is False
|
||||
assert entity.safety_manager.safety_state is not STATE_ON
|
||||
assert entity.safety_manager.is_safety_detected is False
|
||||
|
||||
assert entity.preset_mode == PRESET_BOOST
|
||||
assert entity._saved_preset_mode == PRESET_BOOST
|
||||
assert entity._prop_algorithm.on_percent == 1.0
|
||||
@@ -215,6 +318,12 @@ async def test_security_feature_back_on_percent(
|
||||
"""
|
||||
|
||||
tz = get_tz(hass) # pylint: disable=invalid-name
|
||||
temps = {
|
||||
"eco": 17,
|
||||
"comfort": 18,
|
||||
"boost": 19,
|
||||
"frost": 10,
|
||||
}
|
||||
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
@@ -228,21 +337,18 @@ async def test_security_feature_back_on_percent(
|
||||
"cycle_min": 5,
|
||||
"temp_min": 15,
|
||||
"temp_max": 30,
|
||||
"eco_temp": 17,
|
||||
"comfort_temp": 18,
|
||||
"boost_temp": 19,
|
||||
"use_window_feature": False,
|
||||
"use_motion_feature": False,
|
||||
"use_power_feature": False,
|
||||
"use_presence_feature": False,
|
||||
"heater_entity_id": "switch.mock_switch",
|
||||
CONF_UNDERLYING_LIST: ["switch.mock_switch"],
|
||||
"proportional_function": "tpi",
|
||||
"tpi_coef_int": 0.3,
|
||||
"tpi_coef_ext": 0.01,
|
||||
"minimal_activation_delay": 30,
|
||||
"security_delay_min": 5, # 5 minutes
|
||||
"security_min_on_percent": 0.2,
|
||||
"security_default_on_percent": 0.1,
|
||||
"safety_delay_min": 5, # 5 minutes
|
||||
"safety_min_on_percent": 0.2,
|
||||
"safety_default_on_percent": 0.1,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -253,8 +359,12 @@ async def test_security_feature_back_on_percent(
|
||||
)
|
||||
assert entity
|
||||
|
||||
assert entity._security_state is False
|
||||
assert entity.preset_mode is not PRESET_SECURITY
|
||||
await set_all_climate_preset_temp(hass, entity, temps, "theoverswitchmockname")
|
||||
|
||||
assert entity.safety_manager.safety_state is not STATE_ON
|
||||
assert entity.safety_manager.is_safety_detected is False
|
||||
|
||||
assert entity.preset_mode is not PRESET_SAFETY
|
||||
assert entity._last_ext_temperature_measure is not None
|
||||
assert entity._last_temperature_measure is not None
|
||||
assert (entity._last_temperature_measure.astimezone(tz) - now).total_seconds() < 1
|
||||
@@ -285,7 +395,7 @@ async def test_security_feature_back_on_percent(
|
||||
await send_temperature_change_event(entity, 17, event_timestamp)
|
||||
assert entity._prop_algorithm.calculated_on_percent == 0.6
|
||||
assert entity.preset_mode == PRESET_BOOST
|
||||
assert entity.security_state is False
|
||||
assert entity.safety_state is not STATE_ON
|
||||
assert mock_send_event.call_count == 0
|
||||
|
||||
# 3. Set safety mode with a preset change
|
||||
@@ -302,14 +412,14 @@ async def test_security_feature_back_on_percent(
|
||||
|
||||
assert entity._prop_algorithm.calculated_on_percent == 0.6
|
||||
|
||||
assert entity.security_state is True
|
||||
assert entity.preset_mode == PRESET_SECURITY
|
||||
assert entity.safety_state is STATE_ON
|
||||
assert entity.preset_mode == PRESET_SAFETY
|
||||
assert entity._saved_preset_mode == PRESET_BOOST
|
||||
|
||||
assert mock_send_event.call_count == 3
|
||||
mock_send_event.assert_has_calls(
|
||||
[
|
||||
call.send_event(EventType.PRESET_EVENT, {"preset": PRESET_SECURITY}),
|
||||
call.send_event(EventType.PRESET_EVENT, {"preset": PRESET_SAFETY}),
|
||||
call.send_event(
|
||||
EventType.TEMPERATURE_EVENT,
|
||||
{
|
||||
@@ -343,8 +453,8 @@ async def test_security_feature_back_on_percent(
|
||||
entity._set_now(event_timestamp) # pylint: disable=protected-access
|
||||
|
||||
await entity.async_set_preset_mode(PRESET_ECO)
|
||||
assert entity.security_state is True
|
||||
assert entity.preset_mode == PRESET_SECURITY
|
||||
assert entity.safety_state is STATE_ON
|
||||
assert entity.preset_mode == PRESET_SAFETY
|
||||
|
||||
# 5. resolve the datetime issue
|
||||
with patch(
|
||||
@@ -359,7 +469,9 @@ async def test_security_feature_back_on_percent(
|
||||
# set temperature to 18.9 so that on_percent will be > security_min_on_percent (0.2)
|
||||
await send_temperature_change_event(entity, 18.92, event_timestamp)
|
||||
|
||||
assert entity._security_state is False
|
||||
assert entity.safety_manager.safety_state is not STATE_ON
|
||||
assert entity.safety_manager.is_safety_detected is False
|
||||
|
||||
assert entity.preset_mode == PRESET_ECO
|
||||
assert entity._saved_preset_mode == PRESET_ECO
|
||||
assert entity._prop_algorithm.on_percent == 0.0
|
||||
@@ -452,7 +564,8 @@ async def test_security_over_climate(
|
||||
PRESET_BOOST,
|
||||
]
|
||||
assert entity.preset_mode is PRESET_NONE
|
||||
assert entity._security_state is False
|
||||
assert entity.safety_manager.safety_state is not STATE_ON
|
||||
assert entity.safety_manager.is_safety_detected is False
|
||||
|
||||
# should have been called with EventType.PRESET_EVENT and EventType.HVAC_MODE_EVENT
|
||||
assert mock_send_event.call_count == 2
|
||||
@@ -506,6 +619,56 @@ async def test_security_over_climate(
|
||||
|
||||
await send_temperature_change_event(entity, 15, event_timestamp)
|
||||
# Should stay False because a climate is never in safety mode
|
||||
assert entity.security_state is False
|
||||
assert entity.safety_state is not STATE_ON
|
||||
assert entity.preset_mode == "none"
|
||||
assert entity._saved_preset_mode == "none"
|
||||
|
||||
|
||||
async def test_migration_security_safety(
|
||||
hass: HomeAssistant,
|
||||
skip_hass_states_is_state,
|
||||
):
|
||||
"""Tests the migration of security parameters to safety in English"""
|
||||
central_config_entry = MockConfigEntry(
|
||||
# Current is 2.1
|
||||
version=CONFIG_VERSION,
|
||||
# An old minor version
|
||||
minor_version=0,
|
||||
domain=DOMAIN,
|
||||
title="TheCentralConfigMockName",
|
||||
unique_id="centralConfigUniqueId",
|
||||
data={
|
||||
CONF_NAME: "migrationName",
|
||||
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_SWITCH,
|
||||
CONF_UNDERLYING_LIST: ["switch.under1"],
|
||||
"security_delay_min": 61,
|
||||
"security_min_on_percent": 0.5,
|
||||
"security_default_on_percent": 0.2,
|
||||
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
|
||||
CONF_CYCLE_MIN: 5,
|
||||
CONF_DEVICE_POWER: 1,
|
||||
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
|
||||
CONF_TPI_COEF_INT: 0.3,
|
||||
CONF_TPI_COEF_EXT: 0.1,
|
||||
CONF_USE_MAIN_CENTRAL_CONFIG: False,
|
||||
CONF_USE_WINDOW_FEATURE: False,
|
||||
CONF_USE_MOTION_FEATURE: False,
|
||||
CONF_USE_POWER_FEATURE: False,
|
||||
CONF_USE_PRESENCE_FEATURE: False,
|
||||
CONF_MINIMAL_ACTIVATION_DELAY: 10,
|
||||
},
|
||||
)
|
||||
|
||||
central_config_entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(central_config_entry.entry_id)
|
||||
assert central_config_entry.state is ConfigEntryState.LOADED
|
||||
|
||||
entity: ThermostatOverSwitch = search_entity(
|
||||
hass, "climate.migrationname", "climate"
|
||||
)
|
||||
|
||||
assert entity is not None
|
||||
|
||||
assert entity.safety_manager.safety_min_on_percent == 0.5
|
||||
assert entity.safety_manager.safety_default_on_percent == 0.2
|
||||
assert entity.safety_manager.safety_delay_min == 61
|
||||
@@ -62,8 +62,8 @@ async def test_sensors_over_switch(
|
||||
CONF_TPI_COEF_INT: 0.3,
|
||||
CONF_TPI_COEF_EXT: 0.01,
|
||||
CONF_MINIMAL_ACTIVATION_DELAY: 30,
|
||||
CONF_SECURITY_DELAY_MIN: 5,
|
||||
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
|
||||
CONF_SAFETY_DELAY_MIN: 5,
|
||||
CONF_SAFETY_MIN_ON_PERCENT: 0.3,
|
||||
CONF_DEVICE_POWER: 200,
|
||||
},
|
||||
)
|
||||
@@ -222,10 +222,8 @@ async def test_sensors_over_climate(
|
||||
CONF_USE_PRESENCE_FEATURE: False,
|
||||
CONF_CLIMATE: "climate.mock_climate",
|
||||
CONF_MINIMAL_ACTIVATION_DELAY: 30,
|
||||
CONF_SECURITY_DELAY_MIN: 5,
|
||||
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
|
||||
CONF_POWER_SENSOR: "sensor.mock_power_sensor",
|
||||
CONF_MAX_POWER_SENSOR: "sensor.mock_power_max_sensor",
|
||||
CONF_SAFETY_DELAY_MIN: 5,
|
||||
CONF_SAFETY_MIN_ON_PERCENT: 0.3,
|
||||
CONF_DEVICE_POWER: 1.5,
|
||||
CONF_PRESET_POWER: 12,
|
||||
},
|
||||
@@ -360,8 +358,8 @@ async def test_sensors_over_climate_minimal(
|
||||
CONF_USE_PRESENCE_FEATURE: False,
|
||||
CONF_CLIMATE: "climate.mock_climate",
|
||||
CONF_MINIMAL_ACTIVATION_DELAY: 30,
|
||||
CONF_SECURITY_DELAY_MIN: 5,
|
||||
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
|
||||
CONF_SAFETY_DELAY_MIN: 5,
|
||||
CONF_SAFETY_MIN_ON_PERCENT: 0.3,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -53,10 +53,10 @@ async def test_over_switch_full_start(hass: HomeAssistant, skip_hass_states_is_s
|
||||
PRESET_ACTIVITY,
|
||||
]
|
||||
assert entity.preset_mode is PRESET_NONE
|
||||
assert entity._security_state is False
|
||||
assert entity._window_state is None
|
||||
assert entity._motion_state is None
|
||||
assert entity._presence_state is None
|
||||
assert entity.safety_manager.is_safety_detected is False
|
||||
assert entity.window_state is STATE_UNKNOWN
|
||||
assert entity.motion_state is STATE_UNKNOWN
|
||||
assert entity.presence_state is STATE_UNKNOWN
|
||||
assert entity._prop_algorithm is not None
|
||||
assert entity.have_valve_regulation is False
|
||||
|
||||
@@ -112,10 +112,10 @@ async def test_over_climate_full_start(hass: HomeAssistant, skip_hass_states_is_
|
||||
PRESET_BOOST,
|
||||
]
|
||||
assert entity.preset_mode is PRESET_NONE
|
||||
assert entity._security_state is False
|
||||
assert entity._window_state is None
|
||||
assert entity._motion_state is None
|
||||
assert entity._presence_state is None
|
||||
assert entity.safety_manager.is_safety_detected is False
|
||||
assert entity.window_state is STATE_UNAVAILABLE
|
||||
assert entity.motion_state is STATE_UNAVAILABLE
|
||||
assert entity.presence_state is STATE_UNAVAILABLE
|
||||
assert entity.have_valve_regulation is False
|
||||
|
||||
# should have been called with EventType.PRESET_EVENT and EventType.HVAC_MODE_EVENT
|
||||
@@ -151,18 +151,6 @@ async def test_over_4switch_full_start(hass: HomeAssistant, skip_hass_states_is_
|
||||
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
|
||||
) as mock_send_event:
|
||||
entity = await create_thermostat(hass, entry, "climate.theover4switchmockname")
|
||||
# entry.add_to_hass(hass)
|
||||
# await hass.config_entries.async_setup(entry.entry_id)
|
||||
# assert entry.state is ConfigEntryState.LOADED
|
||||
#
|
||||
# def find_my_entity(entity_id) -> ClimateEntity:
|
||||
# """Find my new entity"""
|
||||
# component: EntityComponent[ClimateEntity] = hass.data[CLIMATE_DOMAIN]
|
||||
# for entity in component.entities:
|
||||
# if entity.entity_id == entity_id:
|
||||
# return entity
|
||||
#
|
||||
# entity: BaseThermostat = find_my_entity("climate.theover4switchmockname")
|
||||
|
||||
assert entity
|
||||
|
||||
@@ -180,10 +168,10 @@ async def test_over_4switch_full_start(hass: HomeAssistant, skip_hass_states_is_
|
||||
PRESET_ACTIVITY,
|
||||
]
|
||||
assert entity.preset_mode is PRESET_NONE
|
||||
assert entity._security_state is False
|
||||
assert entity._window_state is None
|
||||
assert entity._motion_state is None
|
||||
assert entity._presence_state is None
|
||||
assert entity.safety_manager.is_safety_detected is False
|
||||
assert entity.window_state is STATE_UNKNOWN
|
||||
assert entity.motion_state is STATE_UNKNOWN
|
||||
assert entity.presence_state is STATE_UNKNOWN
|
||||
assert entity._prop_algorithm is not None
|
||||
|
||||
assert entity.nb_underlying_entities == 4
|
||||
@@ -242,7 +230,7 @@ async def test_over_switch_deactivate_preset(
|
||||
CONF_HEATER_3: None,
|
||||
CONF_HEATER_4: None,
|
||||
CONF_HEATER_KEEP_ALIVE: 0,
|
||||
CONF_SECURITY_DELAY_MIN: 10,
|
||||
CONF_SAFETY_DELAY_MIN: 10,
|
||||
CONF_MINIMAL_ACTIVATION_DELAY: 10,
|
||||
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
|
||||
CONF_TPI_COEF_INT: 0.6,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user