Compare commits
152 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2d72efe447 | ||
|
|
95af6eba97 | ||
|
|
06dc537767 | ||
|
|
2d79d961dc | ||
|
|
027bf8386b | ||
|
|
a0e548ef71 | ||
|
|
132519b471 | ||
|
|
e6c330fc9d | ||
|
|
968e8286ea | ||
|
|
0f60c070ab | ||
|
|
810430f7b1 | ||
|
|
b4860c2b8d | ||
|
|
60bd522a97 | ||
|
|
fc39cf5f40 | ||
|
|
f6fb7487d5 | ||
|
|
0f585be0c9 | ||
|
|
492c95aff5 | ||
|
|
a530051bbd | ||
|
|
4ef82af8ce | ||
|
|
2ea5cf471b | ||
|
|
f6afaf2715 | ||
|
|
f29b2f9b81 | ||
|
|
de9b95903e | ||
|
|
d112273c58 | ||
|
|
73a9ca4e53 | ||
|
|
1334bdbd8f | ||
|
|
646ef47f6f | ||
|
|
c344c43185 | ||
|
|
062f8a617d | ||
|
|
70f91f3cbe | ||
|
|
668053b352 | ||
|
|
6ff9ff1ee5 | ||
|
|
3f95ed74f4 | ||
|
|
6e42904ddf | ||
|
|
4c1fc396fb | ||
|
|
d6ec7a86be | ||
|
|
a3f8715fe5 | ||
|
|
a1a9a8bbab | ||
|
|
d5c5869276 | ||
|
|
c4b03f8c1e | ||
|
|
ac206a949f | ||
|
|
4bccb746b8 | ||
|
|
e999705286 | ||
|
|
b4873bfd27 | ||
|
|
b00dc09c80 | ||
|
|
da6d6cbce6 | ||
|
|
864e904e21 | ||
|
|
0ee4fe355d | ||
|
|
53dce224cd | ||
|
|
2fd60074c7 | ||
|
|
549423b313 | ||
|
|
6bd1b1137e | ||
|
|
189418e69a | ||
|
|
4ab932f44e | ||
|
|
e1ff23fb30 | ||
|
|
7b657ffabf | ||
|
|
acd22d1fc4 | ||
|
|
d6f33d5796 | ||
|
|
c1ebb46ac6 | ||
|
|
eee4a9c4e3 | ||
|
|
2a3d3ff877 | ||
|
|
a9c368d64c | ||
|
|
1595ff32a2 | ||
|
|
c49545d9e3 | ||
|
|
f4598a407e | ||
|
|
d96fe4bec7 | ||
|
|
a9b87b3aee | ||
|
|
c512cb6f74 | ||
|
|
9269240fe3 | ||
|
|
91ba2387b2 | ||
|
|
162efb4709 | ||
|
|
6a97622226 | ||
|
|
5db7a49e75 | ||
|
|
d7cdf79561 | ||
|
|
07ac7beb7d | ||
|
|
7ded723c8b | ||
|
|
5369111f2d | ||
|
|
4a7ae81c8f | ||
|
|
dbf2bc6982 | ||
|
|
2430e7dd8c | ||
|
|
030069bb97 | ||
|
|
9fb9d89f17 | ||
|
|
12025c0610 | ||
|
|
a9595a5cf8 | ||
|
|
047c847f3c | ||
|
|
91e39f885f | ||
|
|
dce8fa2ed6 | ||
|
|
a440b35815 | ||
|
|
e52666b9d9 | ||
|
|
d9fe2bbd55 | ||
|
|
0a50d0fd4e | ||
|
|
c60f23a9ca | ||
|
|
557657a01c | ||
|
|
1f13eb4f37 | ||
|
|
4f349d6f6f | ||
|
|
76382ebb35 | ||
|
|
90f9a0e1e3 | ||
|
|
ed977b53cd | ||
|
|
5d453393f8 | ||
|
|
d2f2ab7804 | ||
|
|
b0b6d0478d | ||
|
|
f8a2c9baa9 | ||
|
|
8cbd81012c | ||
|
|
26844593b1 | ||
|
|
c12a91a5ff | ||
|
|
3da271b671 | ||
|
|
e8bb465b43 | ||
|
|
d7ec6770c4 | ||
|
|
51428aa875 | ||
|
|
6ea6fe8542 | ||
|
|
a18d10fa3f | ||
|
|
7d4ee40b4d | ||
|
|
1aaf9c8c8e | ||
|
|
ae93a8b97c | ||
|
|
cbe98ae20c | ||
|
|
bfcc854c3e | ||
|
|
683aa050f3 | ||
|
|
7476e7fa64 | ||
|
|
c222feda1a | ||
|
|
d05df021ab | ||
|
|
27a267139f | ||
|
|
707f40d406 | ||
|
|
a01f5770d9 | ||
|
|
04d0b28f1d | ||
|
|
30c3418f1b | ||
|
|
efb8ce257d | ||
|
|
8f934a3298 | ||
|
|
5a468fe2b9 | ||
|
|
fa248a3cfd | ||
|
|
a7480e15c4 | ||
|
|
76416d28d2 | ||
|
|
2bbd7ed8d6 | ||
|
|
7851df84ec | ||
|
|
f7c4e20de3 | ||
|
|
4d2888b220 | ||
|
|
d2829bb951 | ||
|
|
cd50c9b6e8 | ||
|
|
b6f52bcc1b | ||
|
|
5df77a1f74 | ||
|
|
fad1c4136a | ||
|
|
23f9c7c52f | ||
|
|
e5076db96c | ||
|
|
475cb67cf8 | ||
|
|
d5c7b2e571 | ||
|
|
12092a7412 | ||
|
|
b63283c0fe | ||
|
|
7eac10ab3c | ||
|
|
856f47ce03 | ||
|
|
f1595f93da | ||
|
|
a5c548bbee | ||
|
|
1375b3c53a | ||
|
|
72ede4a03f |
2
.devcontainer/Dockerfile
Normal file
@@ -0,0 +1,2 @@
|
||||
FROM mcr.microsoft.com/devcontainers/python:1-3.12
|
||||
RUN apt update && apt install -y ffmpeg
|
||||
@@ -1,11 +1,30 @@
|
||||
default_config:
|
||||
|
||||
recorder:
|
||||
auto_purge: true
|
||||
purge_keep_days: 1
|
||||
commit_interval: 5
|
||||
include:
|
||||
domains:
|
||||
- input_boolean
|
||||
- input_number
|
||||
- switch
|
||||
- climate
|
||||
- sensor
|
||||
- binary_sensor
|
||||
- number
|
||||
- input_select
|
||||
- versatile_thermostat
|
||||
|
||||
logger:
|
||||
default: info
|
||||
default: warning
|
||||
logs:
|
||||
custom_components.versatile_thermostat: debug
|
||||
custom_components.versatile_thermostat.underlyings: debug
|
||||
custom_components.versatile_thermostat.climate: debug
|
||||
# custom_components.versatile_thermostat.underlyings: info
|
||||
# custom_components.versatile_thermostat.climate: info
|
||||
# custom_components.versatile_thermostat.base_thermostat: debug
|
||||
custom_components.versatile_thermostat.sensor: info
|
||||
custom_components.versatile_thermostat.binary_sensor: info
|
||||
|
||||
# If you need to debug uncommment the line below (doc: https://www.home-assistant.io/integrations/debugpy/)
|
||||
debugpy:
|
||||
@@ -13,6 +32,21 @@ debugpy:
|
||||
wait: false
|
||||
port: 5678
|
||||
|
||||
versatile_thermostat:
|
||||
auto_regulation_expert:
|
||||
kp: 0.4
|
||||
ki: 0.08
|
||||
k_ext: 0.0
|
||||
offset_max: 5
|
||||
stabilization_threshold: 0.1
|
||||
accumulated_error_threshold: 50
|
||||
short_ema_params:
|
||||
max_alpha: 0.6
|
||||
halflife_sec: 301
|
||||
precision: 3
|
||||
safety_mode:
|
||||
check_outdoor_sensor: false
|
||||
|
||||
input_number:
|
||||
fake_temperature_sensor1:
|
||||
name: Temperature
|
||||
@@ -46,10 +80,17 @@ input_number:
|
||||
unit_of_measurement: kW
|
||||
fake_valve1:
|
||||
name: The valve 1
|
||||
min: 0
|
||||
max: 100
|
||||
min: 10
|
||||
max: 90
|
||||
icon: mdi:pipe-valve
|
||||
unit_of_measurement: percentage
|
||||
fake_boiler_temperature:
|
||||
name: Central thermostat temp
|
||||
min: 0
|
||||
max: 30
|
||||
icon: mdi:thermostat
|
||||
unit_of_measurement: °C
|
||||
mode: box
|
||||
|
||||
input_boolean:
|
||||
# input_boolean to simulate the windows entity. Only for development environment.
|
||||
@@ -144,14 +185,12 @@ climate:
|
||||
heater: input_boolean.fake_heater_switch3
|
||||
target_sensor: input_number.fake_temperature_sensor1
|
||||
|
||||
recorder:
|
||||
include:
|
||||
domains:
|
||||
- input_boolean
|
||||
- input_number
|
||||
- switch
|
||||
- climate
|
||||
- sensor
|
||||
input_datetime:
|
||||
fake_last_seen:
|
||||
name: Last seen temp sensor
|
||||
icon: mdi:update
|
||||
has_date: true
|
||||
has_time: true
|
||||
|
||||
template:
|
||||
- binary_sensor:
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
// See https://aka.ms/vscode-remote/devcontainer.json for format details.
|
||||
// "image": "ghcr.io/ludeeus/devcontainer/integration:latest",
|
||||
{
|
||||
"image": "mcr.microsoft.com/devcontainers/python:1-3.11-bullseye",
|
||||
"build": {
|
||||
"dockerfile": "Dockerfile"
|
||||
},
|
||||
"name": "Versatile Thermostat integration",
|
||||
"appPort": [
|
||||
"8123:8123"
|
||||
@@ -9,26 +11,35 @@
|
||||
// "postCreateCommand": "container install",
|
||||
"postCreateCommand": "./container dev-setup",
|
||||
|
||||
"mounts": [
|
||||
"source=/Users/jmcollin/.ssh,target=/home/vscode/.ssh,type=bind,consistency=cached",
|
||||
// uncomment this to get the versatile-thermostat-ui-card
|
||||
"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"
|
||||
],
|
||||
"mounts": [
|
||||
"source=${localEnv:HOME}/.ssh,target=/home/vscode/.ssh,type=bind,consistency=cached",
|
||||
// uncomment this to get the versatile-thermostat-ui-card
|
||||
"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",
|
||||
"ms-python.black-formatter",
|
||||
"ms-python.pylint",
|
||||
"ferrierbenjamin.fold-unfold-all-icone"
|
||||
"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"
|
||||
],
|
||||
// "mounts": [
|
||||
// "source=${localWorkspaceFolder}/.devcontainer/configuration.yaml,target=${localWorkspaceFolder}/config/www/community/,type=bind,consistency=cached",
|
||||
// "source=${localWorkspaceFolder}/custom_components,target=/home/vscode/core/config/custom_components,type=bind,consistency=cached"
|
||||
// ],
|
||||
"settings": {
|
||||
"files.eol": "\n",
|
||||
"editor.tabSize": 4,
|
||||
@@ -48,10 +59,10 @@
|
||||
"editor.formatOnPaste": false,
|
||||
"editor.formatOnSave": true,
|
||||
"editor.formatOnType": true,
|
||||
"files.trimTrailingWhitespace": true,
|
||||
"python.experiments.optOutFrom": ["pythonTestAdapter"],
|
||||
"python.analysis.logLevel": "Trace"
|
||||
"files.trimTrailingWhitespace": true
|
||||
// "python.experiments.optOutFrom": ["pythonTestAdapter"],
|
||||
// "python.analysis.logLevel": "Trace"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
6
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -4,6 +4,12 @@ about: Suggest an idea for this project
|
||||
|
||||
---
|
||||
|
||||
**Consider the alternative to create a free discusssion before making a feature request**
|
||||
Discussions forum is [here](https://github.com/jmcollin78/versatile_thermostat/discussions).
|
||||
You should check that a discussion relative to the same issue have not been already answered in the forum.
|
||||
|
||||
Please also check in the [closed issues](https://github.com/jmcollin78/versatile_thermostat/issues) for a similar case.
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
|
||||
15
.github/ISSUE_TEMPLATE/issue.md
vendored
@@ -4,25 +4,31 @@ about: Create a report to help us improve
|
||||
|
||||
---
|
||||
|
||||
> Please read carefuly this instructions and fill this form before writing an issue. It helps me to help you.
|
||||
|
||||
<!-- This template will allow the maintainer to be efficient and post the more accurante response as possible. There is many types / modes / configuration possible, so the analysis can be very tricky. If don't follow this template, your issue could be rejected without any message. Please help me to help you. -->
|
||||
|
||||
<!-- Before you open a new issue, search through the existing issues to see if others have had the same problem.
|
||||
|
||||
If you have a simple question or you are not sure this is an issue, don't open an issue but open a new discussion [here](https://github.com/jmcollin78/versatile_thermostat/discussions).
|
||||
|
||||
Check also in the [Troubleshooting](#troubleshooting) paragrah of the README if the aswer is not already given.
|
||||
|
||||
Issues not containing the minimum requirements will be closed:
|
||||
|
||||
- Issues without a description (using the header is not good enough) will be closed.
|
||||
- Issues without configuration will be closed
|
||||
- Issues that don't follow this template could be closed
|
||||
|
||||
-->
|
||||
|
||||
## Version of the custom_component
|
||||
<!-- If you are not using the newest version, download and try that before opening an issue
|
||||
If you are unsure about the version check the const.py file.
|
||||
If you are unsure about the version check the manifest.json file.
|
||||
-->
|
||||
|
||||
## Configuration
|
||||
|
||||
<!-- Copy / paste the attributes of the VTherm here. You can go to Development Tool / States, find and select your VTherm and the copy/paste the attributes.
|
||||
<!-- Copy / paste the attributes of the VTherm here. You can go to Development Tool / States, find and select your VTherm and the copy/paste the attributes. Surround these attributes by a yaml formatting ```yaml <put the attributes> .... ```
|
||||
Without these attribute support is impossible due to the number of configuration attributes the VTherm have (more than 60). -->
|
||||
|
||||
My VTherm attributes are the following:
|
||||
@@ -103,6 +109,9 @@ supported_features: 17
|
||||
|
||||
<!-- Please do not send an image but a copy / paste of the attributes in yaml format. -->
|
||||
|
||||
## If it is releveant to regulation performance or optimisation some curves are needed
|
||||
To have a great curves demonstrating what you think is a problem, please install and configure what is described here: [Even better with Plotly to tune your Thermostat](#even-better-with-plotly-to-tune-your-thermostat)
|
||||
|
||||
## Describe the bug
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
|
||||
49
.github/workflows/testus.yaml
vendored
Normal file
@@ -0,0 +1,49 @@
|
||||
name: Run Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
testu:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: 3.12
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip3 install -r requirements_test.txt
|
||||
|
||||
- name: Run Tests
|
||||
run: |
|
||||
pytest \
|
||||
-qq \
|
||||
--timeout=9 \
|
||||
--durations=10 \
|
||||
-n auto \
|
||||
-o console_output_style=count \
|
||||
-p no:sugar \
|
||||
tests
|
||||
|
||||
- name: Coverage
|
||||
run: |
|
||||
coverage run -m pytest tests/
|
||||
coverage report
|
||||
|
||||
- name: Generate HTML Coverage Report
|
||||
run: coverage html
|
||||
# - name: Deploy to GitHub Pages
|
||||
# uses: peaceiris/actions-gh-pages@v3
|
||||
# with:
|
||||
# github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
# publish_dir: ./htmlcov
|
||||
4
.gitignore
vendored
@@ -109,3 +109,7 @@ __pycache__
|
||||
|
||||
config/**
|
||||
custom_components/hacs
|
||||
custom_components/localtuya
|
||||
|
||||
.coverage
|
||||
htmlcov
|
||||
30
.vscode/launch.json
vendored
@@ -1,18 +1,14 @@
|
||||
{
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Home Assistant (debug)",
|
||||
"type": "python",
|
||||
"request": "launch",
|
||||
"module": "homeassistant",
|
||||
"justMyCode": false,
|
||||
"args": [
|
||||
"--debug",
|
||||
"-c",
|
||||
"config"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Home Assistant (debug)",
|
||||
"type": "debugpy",
|
||||
"request": "launch",
|
||||
"module": "homeassistant",
|
||||
"justMyCode": false,
|
||||
"args": ["--debug", "-c", "config"]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
14
.vscode/settings.json
vendored
@@ -1,20 +1,18 @@
|
||||
{
|
||||
"[python]": {
|
||||
"editor.defaultFormatter": "ms-python.black-formatter",
|
||||
"editor.formatOnSave": true
|
||||
"editor.formatOnSave": true,
|
||||
"editor.formatOnSaveMode": "modifications"
|
||||
},
|
||||
"pylint.lintOnChange": false,
|
||||
"files.associations": {
|
||||
"*.yaml": "home-assistant"
|
||||
},
|
||||
"python.testing.pytestArgs": [
|
||||
"tests"
|
||||
],
|
||||
"python.testing.pytestArgs": [],
|
||||
"python.testing.unittestEnabled": false,
|
||||
"python.testing.pytestEnabled": true,
|
||||
"python.analysis.extraPaths": [
|
||||
// "/home/vscode/core",
|
||||
"/workspaces/versatile_thermostat/custom_components/versatile_thermostat"
|
||||
],
|
||||
"python.formatting.provider": "none"
|
||||
"/workspaces/versatile_thermostat/custom_components/versatile_thermostat",
|
||||
"/home/vscode/.local/lib/python3.12/site-packages/homeassistant"
|
||||
]
|
||||
}
|
||||
6
.vscode/tasks.json
vendored
@@ -13,6 +13,12 @@
|
||||
"command": "./container restart",
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "Start coverage",
|
||||
"type": "shell",
|
||||
"command": "./container coverage",
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "Home Assistant translations update",
|
||||
"type": "shell",
|
||||
|
||||
1176
README-fr.md
@@ -43,4 +43,13 @@ case $1 in
|
||||
pwd
|
||||
./scripts/starts_ha.sh
|
||||
;;
|
||||
coverage)
|
||||
rm -rf htmlcov/*
|
||||
echo "Starting coverage tests"
|
||||
coverage run -m pytest tests/
|
||||
echo "Starting coverage report"
|
||||
coverage report
|
||||
echo "Starting coverage html"
|
||||
coverage html
|
||||
;;
|
||||
esac
|
||||
|
||||
7
copy-to-forum.txt
Normal file
@@ -0,0 +1,7 @@
|
||||
Before copying to forum you need to replace relative images by this command into VSCode:
|
||||
|
||||
Search :
|
||||
\(images/(.*).png\)
|
||||
|
||||
Replace with:
|
||||
(https://github.com/jmcollin78/versatile_thermostat/blob/main/images/$1.png?raw=true)
|
||||
@@ -1,19 +1,163 @@
|
||||
"""The Versatile Thermostat integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Dict
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
import voluptuous as vol
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.const import SERVICE_RELOAD, EVENT_HOMEASSISTANT_STARTED
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry, ConfigType
|
||||
from homeassistant.core import HomeAssistant, CoreState, callback
|
||||
from homeassistant.helpers.service import async_register_admin_service
|
||||
|
||||
from .base_thermostat import BaseThermostat
|
||||
|
||||
from .const import DOMAIN, PLATFORMS
|
||||
from .const import (
|
||||
DOMAIN,
|
||||
PLATFORMS,
|
||||
CONFIG_VERSION,
|
||||
CONFIG_MINOR_VERSION,
|
||||
CONF_AUTO_REGULATION_LIGHT,
|
||||
CONF_AUTO_REGULATION_MEDIUM,
|
||||
CONF_AUTO_REGULATION_STRONG,
|
||||
CONF_AUTO_REGULATION_SLOW,
|
||||
CONF_AUTO_REGULATION_EXPERT,
|
||||
CONF_SHORT_EMA_PARAMS,
|
||||
CONF_SAFETY_MODE,
|
||||
CONF_THERMOSTAT_CENTRAL_CONFIG,
|
||||
CONF_THERMOSTAT_TYPE,
|
||||
CONF_USE_WINDOW_FEATURE,
|
||||
CONF_USE_MOTION_FEATURE,
|
||||
CONF_USE_PRESENCE_FEATURE,
|
||||
CONF_USE_POWER_FEATURE,
|
||||
CONF_USE_CENTRAL_BOILER_FEATURE,
|
||||
CONF_POWER_SENSOR,
|
||||
CONF_PRESENCE_SENSOR,
|
||||
CONF_UNDERLYING_LIST,
|
||||
CONF_HEATER,
|
||||
CONF_HEATER_2,
|
||||
CONF_HEATER_3,
|
||||
CONF_HEATER_4,
|
||||
CONF_CLIMATE,
|
||||
CONF_CLIMATE_2,
|
||||
CONF_CLIMATE_3,
|
||||
CONF_CLIMATE_4,
|
||||
CONF_VALVE,
|
||||
CONF_VALVE_2,
|
||||
CONF_VALVE_3,
|
||||
CONF_VALVE_4,
|
||||
CONF_THERMOSTAT_SWITCH,
|
||||
CONF_THERMOSTAT_CLIMATE,
|
||||
CONF_THERMOSTAT_VALVE,
|
||||
)
|
||||
|
||||
from .vtherm_api import VersatileThermostatAPI
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
SELF_REGULATION_PARAM_SCHEMA = {
|
||||
vol.Required("kp"): vol.Coerce(float),
|
||||
vol.Required("ki"): vol.Coerce(float),
|
||||
vol.Required("k_ext"): vol.Coerce(float),
|
||||
vol.Required("offset_max"): vol.Coerce(float),
|
||||
vol.Required("stabilization_threshold"): vol.Coerce(float),
|
||||
vol.Required("accumulated_error_threshold"): vol.Coerce(float),
|
||||
}
|
||||
|
||||
EMA_PARAM_SCHEMA = {
|
||||
vol.Required("max_alpha"): vol.Coerce(float),
|
||||
vol.Required("halflife_sec"): vol.Coerce(float),
|
||||
vol.Required("precision"): cv.positive_int,
|
||||
}
|
||||
|
||||
SAFETY_MODE_PARAM_SCHEMA = {
|
||||
vol.Required("check_outdoor_sensor"): bool,
|
||||
}
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema(
|
||||
{
|
||||
DOMAIN: vol.Schema(
|
||||
{
|
||||
CONF_AUTO_REGULATION_EXPERT: vol.Schema(SELF_REGULATION_PARAM_SCHEMA),
|
||||
CONF_SHORT_EMA_PARAMS: vol.Schema(EMA_PARAM_SCHEMA),
|
||||
CONF_SAFETY_MODE: vol.Schema(SAFETY_MODE_PARAM_SCHEMA),
|
||||
}
|
||||
),
|
||||
},
|
||||
extra=vol.ALLOW_EXTRA,
|
||||
)
|
||||
|
||||
|
||||
async def async_setup(
|
||||
hass: HomeAssistant, config: ConfigType
|
||||
): # pylint: disable=unused-argument
|
||||
"""Initialisation de l'intégration"""
|
||||
_LOGGER.info(
|
||||
"Initializing %s integration with config: %s",
|
||||
DOMAIN,
|
||||
config.get(DOMAIN),
|
||||
)
|
||||
|
||||
async def _handle_reload(_):
|
||||
"""The reload callback"""
|
||||
await reload_all_vtherm(hass)
|
||||
|
||||
hass.data.setdefault(DOMAIN, {})
|
||||
|
||||
api: VersatileThermostatAPI = VersatileThermostatAPI.get_vtherm_api(hass)
|
||||
# L'argument config contient votre fichier configuration.yaml
|
||||
vtherm_config = config.get(DOMAIN)
|
||||
if vtherm_config is not None:
|
||||
api.set_global_config(vtherm_config)
|
||||
else:
|
||||
_LOGGER.info("No global config from configuration.yaml available")
|
||||
|
||||
# Listen HA starts to initialize all links between
|
||||
@callback
|
||||
async def _async_startup_internal(*_):
|
||||
_LOGGER.info(
|
||||
"VersatileThermostat - HA is started, initialize all links between VTherm entities"
|
||||
)
|
||||
await api.init_vtherm_links()
|
||||
await api.notify_central_mode_change()
|
||||
await api.reload_central_boiler_entities_list()
|
||||
|
||||
if hass.state == CoreState.running:
|
||||
await _async_startup_internal()
|
||||
else:
|
||||
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STARTED, _async_startup_internal)
|
||||
|
||||
async_register_admin_service(
|
||||
hass,
|
||||
DOMAIN,
|
||||
SERVICE_RELOAD,
|
||||
_handle_reload,
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def reload_all_vtherm(hass):
|
||||
"""Handle reload service call."""
|
||||
_LOGGER.info("Service %s.reload called: reloading integration", DOMAIN)
|
||||
|
||||
current_entries = hass.config_entries.async_entries(DOMAIN)
|
||||
|
||||
reload_tasks = [
|
||||
hass.config_entries.async_reload(entry.entry_id) for entry in current_entries
|
||||
]
|
||||
|
||||
await asyncio.gather(*reload_tasks)
|
||||
api: VersatileThermostatAPI = VersatileThermostatAPI.get_vtherm_api(hass)
|
||||
if api:
|
||||
await api.reload_central_boiler_entities_list()
|
||||
await api.init_vtherm_links()
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up Versatile Thermostat from a config entry."""
|
||||
@@ -24,11 +168,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
entry.data,
|
||||
)
|
||||
|
||||
# hass.data.setdefault(DOMAIN, {})
|
||||
|
||||
api: VersatileThermostatAPI = hass.data.get(DOMAIN)
|
||||
if api is None:
|
||||
api = VersatileThermostatAPI(hass)
|
||||
api: VersatileThermostatAPI = VersatileThermostatAPI.get_vtherm_api(hass)
|
||||
|
||||
api.add_entry(entry)
|
||||
|
||||
@@ -36,74 +176,125 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
if hass.state == CoreState.running:
|
||||
await api.reload_central_boiler_entities_list()
|
||||
await api.init_vtherm_links(entry.entry_id)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||
"""Update listener."""
|
||||
await hass.config_entries.async_reload(entry.entry_id)
|
||||
|
||||
_LOGGER.debug(
|
||||
"Calling update_listener entry: entry_id='%s', value='%s'",
|
||||
entry.entry_id,
|
||||
entry.data,
|
||||
)
|
||||
|
||||
if entry.data.get(CONF_THERMOSTAT_TYPE) == CONF_THERMOSTAT_CENTRAL_CONFIG:
|
||||
await reload_all_vtherm(hass)
|
||||
else:
|
||||
await hass.config_entries.async_reload(entry.entry_id)
|
||||
# Reload the central boiler list of entities
|
||||
api: VersatileThermostatAPI = VersatileThermostatAPI.get_vtherm_api(hass)
|
||||
if api is not None:
|
||||
await api.reload_central_boiler_entities_list()
|
||||
await api.init_vtherm_links(entry.entry_id)
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
api: VersatileThermostatAPI = hass.data.get(DOMAIN)
|
||||
api: VersatileThermostatAPI = VersatileThermostatAPI.get_vtherm_api(hass)
|
||||
|
||||
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
|
||||
if api:
|
||||
api.remove_entry(entry)
|
||||
await api.reload_central_boiler_entities_list()
|
||||
|
||||
return unload_ok
|
||||
|
||||
|
||||
class VersatileThermostatAPI(dict):
|
||||
"""The VersatileThermostatAPI"""
|
||||
|
||||
_hass: HomeAssistant
|
||||
# _entries: Dict(str, ConfigEntry)
|
||||
|
||||
def __init__(self, hass: HomeAssistant) -> None:
|
||||
_LOGGER.debug("building a VersatileThermostatAPI")
|
||||
super().__init__()
|
||||
self._hass = hass
|
||||
# self._entries = dict()
|
||||
# Add the API in hass.data
|
||||
self._hass.data[DOMAIN] = self
|
||||
|
||||
def add_entry(self, entry: ConfigEntry):
|
||||
"""Add a new entry"""
|
||||
_LOGGER.debug("Add the entry %s", entry.entry_id)
|
||||
# self._entries[entry.entry_id] = entry
|
||||
# Add the entry in hass.data
|
||||
self._hass.data[DOMAIN][entry.entry_id] = entry
|
||||
|
||||
def remove_entry(self, entry: ConfigEntry):
|
||||
"""Remove an entry"""
|
||||
_LOGGER.debug("Remove the entry %s", entry.entry_id)
|
||||
# self._entries.pop(entry.entry_id)
|
||||
self._hass.data[DOMAIN].pop(entry.entry_id)
|
||||
# If not more entries are preset, remove the API
|
||||
if len(self) == 0:
|
||||
_LOGGER.debug("No more entries-> Remove the API from DOMAIN")
|
||||
self._hass.data.pop(DOMAIN)
|
||||
|
||||
@property
|
||||
def hass(self):
|
||||
"""Get the HomeAssistant object"""
|
||||
return self._hass
|
||||
|
||||
|
||||
# Example migration function
|
||||
async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry):
|
||||
"""Migrate old entry."""
|
||||
_LOGGER.debug("Migrating from version %s", config_entry.version)
|
||||
_LOGGER.debug(
|
||||
"Migrating from version %s/%s", config_entry.version, config_entry.minor_version
|
||||
)
|
||||
|
||||
if config_entry.version == 1:
|
||||
if (
|
||||
config_entry.version != CONFIG_VERSION
|
||||
or config_entry.minor_version != CONFIG_MINOR_VERSION
|
||||
):
|
||||
_LOGGER.debug(
|
||||
"Migration to %s/%s is needed", CONFIG_VERSION, CONFIG_MINOR_VERSION
|
||||
)
|
||||
new = {**config_entry.data}
|
||||
# TO DO: modify Config Entry data if there will be something to migrate
|
||||
|
||||
config_entry.version = 2
|
||||
hass.config_entries.async_update_entry(config_entry, data=new)
|
||||
thermostat_type = config_entry.data.get(CONF_THERMOSTAT_TYPE)
|
||||
|
||||
_LOGGER.info("Migration to version %s successful", config_entry.version)
|
||||
if thermostat_type == CONF_THERMOSTAT_CENTRAL_CONFIG:
|
||||
new[CONF_USE_WINDOW_FEATURE] = True
|
||||
new[CONF_USE_MOTION_FEATURE] = True
|
||||
new[CONF_USE_POWER_FEATURE] = new.get(CONF_POWER_SENSOR, None) is not None
|
||||
new[CONF_USE_PRESENCE_FEATURE] = (
|
||||
new.get(CONF_PRESENCE_SENSOR, None) is not None
|
||||
)
|
||||
|
||||
new[CONF_USE_CENTRAL_BOILER_FEATURE] = new.get(
|
||||
"add_central_boiler_control", False
|
||||
) or new.get(CONF_USE_CENTRAL_BOILER_FEATURE, False)
|
||||
|
||||
if config_entry.data.get(CONF_UNDERLYING_LIST, None) is None:
|
||||
underlying_list = []
|
||||
if thermostat_type == CONF_THERMOSTAT_SWITCH:
|
||||
underlying_list = [
|
||||
config_entry.data.get(CONF_HEATER, None),
|
||||
config_entry.data.get(CONF_HEATER_2, None),
|
||||
config_entry.data.get(CONF_HEATER_3, None),
|
||||
config_entry.data.get(CONF_HEATER_4, None),
|
||||
]
|
||||
elif thermostat_type == CONF_THERMOSTAT_CLIMATE:
|
||||
underlying_list = [
|
||||
config_entry.data.get(CONF_CLIMATE, None),
|
||||
config_entry.data.get(CONF_CLIMATE_2, None),
|
||||
config_entry.data.get(CONF_CLIMATE_3, None),
|
||||
config_entry.data.get(CONF_CLIMATE_4, None),
|
||||
]
|
||||
elif thermostat_type == CONF_THERMOSTAT_VALVE:
|
||||
underlying_list = [
|
||||
config_entry.data.get(CONF_VALVE, None),
|
||||
config_entry.data.get(CONF_VALVE_2, None),
|
||||
config_entry.data.get(CONF_VALVE_3, None),
|
||||
config_entry.data.get(CONF_VALVE_4, None),
|
||||
]
|
||||
|
||||
new[CONF_UNDERLYING_LIST] = [
|
||||
entity for entity in underlying_list if entity is not None
|
||||
]
|
||||
|
||||
for key in [
|
||||
CONF_HEATER,
|
||||
CONF_HEATER_2,
|
||||
CONF_HEATER_3,
|
||||
CONF_HEATER_4,
|
||||
CONF_CLIMATE,
|
||||
CONF_CLIMATE_2,
|
||||
CONF_CLIMATE_3,
|
||||
CONF_CLIMATE_4,
|
||||
CONF_VALVE,
|
||||
CONF_VALVE_2,
|
||||
CONF_VALVE_3,
|
||||
CONF_VALVE_4,
|
||||
]:
|
||||
new.pop(key, None)
|
||||
|
||||
hass.config_entries.async_update_entry(
|
||||
config_entry,
|
||||
data=new,
|
||||
version=CONFIG_VERSION,
|
||||
minor_version=CONFIG_MINOR_VERSION,
|
||||
)
|
||||
_LOGGER.info("Migration to version %s successful", config_entry.version)
|
||||
|
||||
return True
|
||||
|
||||
@@ -0,0 +1,239 @@
|
||||
# pylint: disable=line-too-long
|
||||
""" This file implements the Auto start/stop algorithm as described here: https://github.com/jmcollin78/versatile_thermostat/issues/585
|
||||
"""
|
||||
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from typing import Literal
|
||||
|
||||
from homeassistant.components.climate import HVACMode
|
||||
|
||||
from .const import (
|
||||
AUTO_START_STOP_LEVEL_NONE,
|
||||
AUTO_START_STOP_LEVEL_FAST,
|
||||
AUTO_START_STOP_LEVEL_MEDIUM,
|
||||
AUTO_START_STOP_LEVEL_SLOW,
|
||||
TYPE_AUTO_START_STOP_LEVELS,
|
||||
)
|
||||
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
# Some constant to make algorithm depending of level
|
||||
DT_MIN = {
|
||||
AUTO_START_STOP_LEVEL_NONE: 0, # Not used
|
||||
AUTO_START_STOP_LEVEL_SLOW: 30,
|
||||
AUTO_START_STOP_LEVEL_MEDIUM: 15,
|
||||
AUTO_START_STOP_LEVEL_FAST: 7,
|
||||
}
|
||||
|
||||
# the measurement cycle (2 min)
|
||||
CYCLE_SEC = 120
|
||||
|
||||
# A temp hysteresis to avoid rapid OFF/ON
|
||||
TEMP_HYSTERESIS = 0.5
|
||||
|
||||
ERROR_THRESHOLD = {
|
||||
AUTO_START_STOP_LEVEL_NONE: 0, # Not used
|
||||
AUTO_START_STOP_LEVEL_SLOW: 10, # 10 cycle above 1° or 5 cycle above 2°, ...
|
||||
AUTO_START_STOP_LEVEL_MEDIUM: 5, # 5 cycle above 1° or 3 cycle above 2°, ..., 1 cycle above 5°
|
||||
AUTO_START_STOP_LEVEL_FAST: 2, # 2 cycle above 1° or 1 cycle above 2°
|
||||
}
|
||||
|
||||
AUTO_START_STOP_ACTION_OFF = "turnOff"
|
||||
AUTO_START_STOP_ACTION_ON = "turnOn"
|
||||
AUTO_START_STOP_ACTION_NOTHING = "nothing"
|
||||
AUTO_START_STOP_ACTIONS = Literal[ # pylint: disable=invalid-name
|
||||
AUTO_START_STOP_ACTION_OFF,
|
||||
AUTO_START_STOP_ACTION_ON,
|
||||
AUTO_START_STOP_ACTION_NOTHING,
|
||||
]
|
||||
|
||||
class AutoStartStopDetectionAlgorithm:
|
||||
"""The class that implements the algorithm listed above"""
|
||||
|
||||
_dt: float | None = None
|
||||
_level: str = AUTO_START_STOP_LEVEL_NONE
|
||||
_accumulated_error: float = 0
|
||||
_error_threshold: float | None = None
|
||||
_last_calculation_date: datetime | None = None
|
||||
|
||||
def __init__(self, level: TYPE_AUTO_START_STOP_LEVELS, vtherm_name) -> None:
|
||||
"""Initalize a new algorithm with the right constants"""
|
||||
self._vtherm_name = vtherm_name
|
||||
self._init_level(level)
|
||||
|
||||
def _init_level(self, level: TYPE_AUTO_START_STOP_LEVELS):
|
||||
"""Initialize a new level"""
|
||||
if level == self._level:
|
||||
return
|
||||
|
||||
self._level = level
|
||||
if self._level != AUTO_START_STOP_LEVEL_NONE:
|
||||
self._dt = DT_MIN[level]
|
||||
self._error_threshold = ERROR_THRESHOLD[level]
|
||||
# reset accumulated error if we change the level
|
||||
self._accumulated_error = 0
|
||||
|
||||
def calculate_action(
|
||||
self,
|
||||
hvac_mode: HVACMode | None,
|
||||
saved_hvac_mode: HVACMode | None,
|
||||
target_temp: float,
|
||||
current_temp: float,
|
||||
slope_min: float | None,
|
||||
now: datetime,
|
||||
) -> AUTO_START_STOP_ACTIONS:
|
||||
"""Calculate an eventual action to do depending of the value in parameter"""
|
||||
if self._level == AUTO_START_STOP_LEVEL_NONE:
|
||||
_LOGGER.debug(
|
||||
"%s - auto-start/stop is disabled",
|
||||
self,
|
||||
)
|
||||
return AUTO_START_STOP_ACTION_NOTHING
|
||||
|
||||
_LOGGER.debug(
|
||||
"%s - calculate_action: hvac_mode=%s, saved_hvac_mode=%s, target_temp=%s, current_temp=%s, slope_min=%s at %s",
|
||||
self,
|
||||
hvac_mode,
|
||||
saved_hvac_mode,
|
||||
target_temp,
|
||||
current_temp,
|
||||
slope_min,
|
||||
now,
|
||||
)
|
||||
|
||||
if hvac_mode is None or target_temp is None or current_temp is None:
|
||||
_LOGGER.debug(
|
||||
"%s - No all mandatory parameters are set. Disable auto-start/stop",
|
||||
self,
|
||||
)
|
||||
return AUTO_START_STOP_ACTION_NOTHING
|
||||
|
||||
# Calculate the error factor (P)
|
||||
error = target_temp - current_temp
|
||||
|
||||
# reduce the error considering the dt between the last measurement
|
||||
if self._last_calculation_date is not None:
|
||||
dtmin = (now - self._last_calculation_date).total_seconds() / CYCLE_SEC
|
||||
# ignore two calls too near (< 24 sec)
|
||||
if dtmin <= 0.2:
|
||||
_LOGGER.debug(
|
||||
"%s - new calculation of auto_start_stop (%s) is too near of the last one (%s). Forget it",
|
||||
self,
|
||||
now,
|
||||
self._last_calculation_date,
|
||||
)
|
||||
return AUTO_START_STOP_ACTION_NOTHING
|
||||
error = error * dtmin
|
||||
|
||||
# If the error have change its sign, reset smoothly the accumulated error
|
||||
if error * self._accumulated_error < 0:
|
||||
self._accumulated_error = self._accumulated_error / 2.0
|
||||
|
||||
self._accumulated_error += error
|
||||
|
||||
# Capping of the error
|
||||
self._accumulated_error = min(
|
||||
self._error_threshold,
|
||||
max(-self._error_threshold, self._accumulated_error),
|
||||
)
|
||||
|
||||
self._last_calculation_date = now
|
||||
|
||||
temp_at_dt = current_temp + slope_min * self._dt
|
||||
|
||||
# Check to turn-off
|
||||
# When we hit the threshold, that mean we can turn off
|
||||
if hvac_mode == HVACMode.HEAT:
|
||||
if (
|
||||
self._accumulated_error <= -self._error_threshold
|
||||
and temp_at_dt >= target_temp + TEMP_HYSTERESIS
|
||||
):
|
||||
_LOGGER.info(
|
||||
"%s - We need to stop, there is no need for heating for a long time.",
|
||||
self,
|
||||
)
|
||||
return AUTO_START_STOP_ACTION_OFF
|
||||
else:
|
||||
_LOGGER.debug("%s - nothing to do, we are heating", self)
|
||||
return AUTO_START_STOP_ACTION_NOTHING
|
||||
|
||||
if hvac_mode == HVACMode.COOL:
|
||||
if (
|
||||
self._accumulated_error >= self._error_threshold
|
||||
and temp_at_dt <= target_temp - TEMP_HYSTERESIS
|
||||
):
|
||||
_LOGGER.info(
|
||||
"%s - We need to stop, there is no need for cooling for a long time.",
|
||||
self,
|
||||
)
|
||||
return AUTO_START_STOP_ACTION_OFF
|
||||
else:
|
||||
_LOGGER.debug(
|
||||
"%s - nothing to do, we are cooling",
|
||||
self,
|
||||
)
|
||||
return AUTO_START_STOP_ACTION_NOTHING
|
||||
|
||||
# check to turn on
|
||||
if hvac_mode == HVACMode.OFF and saved_hvac_mode == HVACMode.HEAT:
|
||||
if temp_at_dt <= target_temp - TEMP_HYSTERESIS:
|
||||
_LOGGER.info(
|
||||
"%s - We need to start, because it will be time to heat",
|
||||
self,
|
||||
)
|
||||
return AUTO_START_STOP_ACTION_ON
|
||||
else:
|
||||
_LOGGER.debug(
|
||||
"%s - nothing to do, we don't need to heat soon",
|
||||
self,
|
||||
)
|
||||
return AUTO_START_STOP_ACTION_NOTHING
|
||||
|
||||
if hvac_mode == HVACMode.OFF and saved_hvac_mode == HVACMode.COOL:
|
||||
if temp_at_dt >= target_temp + TEMP_HYSTERESIS:
|
||||
_LOGGER.info(
|
||||
"%s - We need to start, because it will be time to cool",
|
||||
self,
|
||||
)
|
||||
return AUTO_START_STOP_ACTION_ON
|
||||
else:
|
||||
_LOGGER.debug(
|
||||
"%s - nothing to do, we don't need to cool soon",
|
||||
self,
|
||||
)
|
||||
return AUTO_START_STOP_ACTION_NOTHING
|
||||
|
||||
_LOGGER.debug(
|
||||
"%s - nothing to do, no conditions applied",
|
||||
self,
|
||||
)
|
||||
return AUTO_START_STOP_ACTION_NOTHING
|
||||
|
||||
def set_level(self, level: TYPE_AUTO_START_STOP_LEVELS):
|
||||
"""Set a new level"""
|
||||
self._init_level(level)
|
||||
|
||||
@property
|
||||
def dt_min(self) -> float:
|
||||
"""Get the dt value"""
|
||||
return self._dt
|
||||
|
||||
@property
|
||||
def accumulated_error(self) -> float:
|
||||
"""Get the accumulated error value"""
|
||||
return self._accumulated_error
|
||||
|
||||
@property
|
||||
def accumulated_error_threshold(self) -> float:
|
||||
"""Get the accumulated error threshold value"""
|
||||
return self._error_threshold
|
||||
|
||||
@property
|
||||
def level(self) -> TYPE_AUTO_START_STOP_LEVELS:
|
||||
"""Get the level value"""
|
||||
return self._level
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"AutoStartStopDetectionAlgorithm-{self._vtherm_name}"
|
||||
@@ -1,9 +1,20 @@
|
||||
""" Implements the VersatileThermostat binary sensors component """
|
||||
# pylint: disable=unused-argument, line-too-long
|
||||
|
||||
import logging
|
||||
|
||||
from homeassistant.core import HomeAssistant, callback, Event
|
||||
from homeassistant.core import (
|
||||
HomeAssistant,
|
||||
callback,
|
||||
Event,
|
||||
# CoreState,
|
||||
HomeAssistantError,
|
||||
)
|
||||
|
||||
from homeassistant.const import STATE_ON, STATE_OFF
|
||||
from homeassistant.const import STATE_ON, STATE_OFF # , EVENT_HOMEASSISTANT_START
|
||||
|
||||
from homeassistant.helpers.device_registry import DeviceInfo, DeviceEntryType
|
||||
from homeassistant.helpers.event import async_track_state_change_event
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorEntity,
|
||||
@@ -13,13 +24,27 @@ from homeassistant.config_entries import ConfigEntry
|
||||
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from .commons import VersatileThermostatBaseEntity
|
||||
from .vtherm_api import VersatileThermostatAPI
|
||||
from .commons import (
|
||||
VersatileThermostatBaseEntity,
|
||||
check_and_extract_service_configuration,
|
||||
)
|
||||
from .const import (
|
||||
DOMAIN,
|
||||
DEVICE_MANUFACTURER,
|
||||
CONF_NAME,
|
||||
CONF_USE_POWER_FEATURE,
|
||||
CONF_USE_PRESENCE_FEATURE,
|
||||
CONF_USE_MOTION_FEATURE,
|
||||
CONF_USE_WINDOW_FEATURE,
|
||||
CONF_THERMOSTAT_TYPE,
|
||||
CONF_THERMOSTAT_CENTRAL_CONFIG,
|
||||
CONF_USE_CENTRAL_BOILER_FEATURE,
|
||||
CONF_CENTRAL_BOILER_ACTIVATION_SRV,
|
||||
CONF_CENTRAL_BOILER_DEACTIVATION_SRV,
|
||||
overrides,
|
||||
EventType,
|
||||
send_vtherm_event,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -37,28 +62,45 @@ async def async_setup_entry(
|
||||
|
||||
unique_id = entry.entry_id
|
||||
name = entry.data.get(CONF_NAME)
|
||||
vt_type = entry.data.get(CONF_THERMOSTAT_TYPE)
|
||||
|
||||
entities = [SecurityBinarySensor(hass, unique_id, name, entry.data),WindowByPassBinarySensor(hass, unique_id, name, entry.data)]
|
||||
if entry.data.get(CONF_USE_MOTION_FEATURE):
|
||||
entities.append(MotionBinarySensor(hass, unique_id, name, entry.data))
|
||||
if entry.data.get(CONF_USE_WINDOW_FEATURE):
|
||||
entities.append(WindowBinarySensor(hass, unique_id, name, entry.data))
|
||||
if entry.data.get(CONF_USE_PRESENCE_FEATURE):
|
||||
entities.append(PresenceBinarySensor(hass, unique_id, name, entry.data))
|
||||
if entry.data.get(CONF_USE_POWER_FEATURE):
|
||||
entities.append(OverpoweringBinarySensor(hass, unique_id, name, entry.data))
|
||||
entities = None
|
||||
|
||||
async_add_entities(entities, True)
|
||||
if vt_type == CONF_THERMOSTAT_CENTRAL_CONFIG:
|
||||
if entry.data.get(CONF_USE_CENTRAL_BOILER_FEATURE):
|
||||
entities = [
|
||||
CentralBoilerBinarySensor(hass, unique_id, name, entry.data),
|
||||
]
|
||||
else:
|
||||
entities = [
|
||||
SecurityBinarySensor(hass, unique_id, name, entry.data),
|
||||
WindowByPassBinarySensor(hass, unique_id, name, entry.data),
|
||||
]
|
||||
if entry.data.get(CONF_USE_MOTION_FEATURE):
|
||||
entities.append(MotionBinarySensor(hass, unique_id, name, entry.data))
|
||||
if entry.data.get(CONF_USE_WINDOW_FEATURE):
|
||||
entities.append(WindowBinarySensor(hass, unique_id, name, entry.data))
|
||||
if entry.data.get(CONF_USE_PRESENCE_FEATURE):
|
||||
entities.append(PresenceBinarySensor(hass, unique_id, name, entry.data))
|
||||
if entry.data.get(CONF_USE_POWER_FEATURE):
|
||||
entities.append(OverpoweringBinarySensor(hass, unique_id, name, entry.data))
|
||||
|
||||
if entities:
|
||||
async_add_entities(entities, True)
|
||||
|
||||
|
||||
class SecurityBinarySensor(VersatileThermostatBaseEntity, BinarySensorEntity):
|
||||
"""Representation of a BinarySensor which exposes the security state"""
|
||||
|
||||
def __init__(
|
||||
self, hass: HomeAssistant, unique_id, name, entry_infos
|
||||
) -> None: # pylint: disable=unused-argument
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
unique_id,
|
||||
name, # pylint: disable=unused-argument
|
||||
entry_infos,
|
||||
) -> None:
|
||||
"""Initialize the SecurityState Binary sensor"""
|
||||
super().__init__(hass, unique_id, entry_infos.get(CONF_NAME))
|
||||
super().__init__(hass, unique_id, name)
|
||||
self._attr_name = "Security state"
|
||||
self._attr_unique_id = f"{self._device_name}_security_state"
|
||||
self._attr_is_on = False
|
||||
@@ -90,8 +132,12 @@ class OverpoweringBinarySensor(VersatileThermostatBaseEntity, BinarySensorEntity
|
||||
"""Representation of a BinarySensor which exposes the overpowering state"""
|
||||
|
||||
def __init__(
|
||||
self, hass: HomeAssistant, unique_id, name, entry_infos
|
||||
) -> None: # pylint: disable=unused-argument
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
unique_id,
|
||||
name, # pylint: disable=unused-argument
|
||||
entry_infos,
|
||||
) -> None:
|
||||
"""Initialize the OverpoweringState Binary sensor"""
|
||||
super().__init__(hass, unique_id, entry_infos.get(CONF_NAME))
|
||||
self._attr_name = "Overpowering state"
|
||||
@@ -125,8 +171,12 @@ class WindowBinarySensor(VersatileThermostatBaseEntity, BinarySensorEntity):
|
||||
"""Representation of a BinarySensor which exposes the window state"""
|
||||
|
||||
def __init__(
|
||||
self, hass: HomeAssistant, unique_id, name, entry_infos
|
||||
) -> None: # pylint: disable=unused-argument
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
unique_id,
|
||||
name, # pylint: disable=unused-argument
|
||||
entry_infos,
|
||||
) -> None:
|
||||
"""Initialize the WindowState Binary sensor"""
|
||||
super().__init__(hass, unique_id, entry_infos.get(CONF_NAME))
|
||||
self._attr_name = "Window state"
|
||||
@@ -171,8 +221,12 @@ class MotionBinarySensor(VersatileThermostatBaseEntity, BinarySensorEntity):
|
||||
"""Representation of a BinarySensor which exposes the motion state"""
|
||||
|
||||
def __init__(
|
||||
self, hass: HomeAssistant, unique_id, name, entry_infos
|
||||
) -> None: # pylint: disable=unused-argument
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
unique_id,
|
||||
name, # pylint: disable=unused-argument
|
||||
entry_infos,
|
||||
) -> None:
|
||||
"""Initialize the MotionState Binary sensor"""
|
||||
super().__init__(hass, unique_id, entry_infos.get(CONF_NAME))
|
||||
self._attr_name = "Motion state"
|
||||
@@ -207,8 +261,12 @@ class PresenceBinarySensor(VersatileThermostatBaseEntity, BinarySensorEntity):
|
||||
"""Representation of a BinarySensor which exposes the presence state"""
|
||||
|
||||
def __init__(
|
||||
self, hass: HomeAssistant, unique_id, name, entry_infos
|
||||
) -> None: # pylint: disable=unused-argument
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
unique_id,
|
||||
name, # pylint: disable=unused-argument
|
||||
entry_infos,
|
||||
) -> None:
|
||||
"""Initialize the PresenceState Binary sensor"""
|
||||
super().__init__(hass, unique_id, entry_infos.get(CONF_NAME))
|
||||
self._attr_name = "Presence state"
|
||||
@@ -239,13 +297,17 @@ class PresenceBinarySensor(VersatileThermostatBaseEntity, BinarySensorEntity):
|
||||
else:
|
||||
return "mdi:nature-people"
|
||||
|
||||
#PR - Adding Window ByPass
|
||||
|
||||
class WindowByPassBinarySensor(VersatileThermostatBaseEntity, BinarySensorEntity):
|
||||
"""Representation of a BinarySensor which exposes the Window ByPass state"""
|
||||
|
||||
def __init__(
|
||||
self, hass: HomeAssistant, unique_id, name, entry_infos
|
||||
) -> None: # pylint: disable=unused-argument
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
unique_id,
|
||||
name, # pylint: disable=unused-argument
|
||||
entry_infos,
|
||||
) -> None:
|
||||
"""Initialize the WindowByPass Binary sensor"""
|
||||
super().__init__(hass, unique_id, entry_infos.get(CONF_NAME))
|
||||
self._attr_name = "Window bypass"
|
||||
@@ -272,4 +334,163 @@ class WindowByPassBinarySensor(VersatileThermostatBaseEntity, BinarySensorEntity
|
||||
if self._attr_is_on:
|
||||
return "mdi:window-shutter-cog"
|
||||
else:
|
||||
return "mdi:window-shutter-auto"
|
||||
return "mdi:window-shutter-auto"
|
||||
|
||||
|
||||
class CentralBoilerBinarySensor(BinarySensorEntity):
|
||||
"""Representation of a BinarySensor which exposes the Central Boiler state"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
unique_id,
|
||||
name, # pylint: disable=unused-argument
|
||||
entry_infos,
|
||||
) -> None:
|
||||
"""Initialize the CentralBoiler Binary sensor"""
|
||||
self._config_id = unique_id
|
||||
self._attr_name = "Central boiler"
|
||||
self._attr_unique_id = "central_boiler_state"
|
||||
self._attr_is_on = False
|
||||
self._device_name = entry_infos.get(CONF_NAME)
|
||||
self._entities = []
|
||||
self._hass = hass
|
||||
self._service_activate = check_and_extract_service_configuration(
|
||||
entry_infos.get(CONF_CENTRAL_BOILER_ACTIVATION_SRV)
|
||||
)
|
||||
self._service_deactivate = check_and_extract_service_configuration(
|
||||
entry_infos.get(CONF_CENTRAL_BOILER_DEACTIVATION_SRV)
|
||||
)
|
||||
|
||||
@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,
|
||||
)
|
||||
|
||||
@property
|
||||
def device_class(self) -> BinarySensorDeviceClass | None:
|
||||
return BinarySensorDeviceClass.RUNNING
|
||||
|
||||
@property
|
||||
def icon(self) -> str | None:
|
||||
if self._attr_is_on:
|
||||
return "mdi:water-boiler"
|
||||
else:
|
||||
return "mdi:water-boiler-off"
|
||||
|
||||
@overrides
|
||||
async def async_added_to_hass(self) -> None:
|
||||
await super().async_added_to_hass()
|
||||
|
||||
api: VersatileThermostatAPI = VersatileThermostatAPI.get_vtherm_api(self._hass)
|
||||
api.register_central_boiler(self)
|
||||
|
||||
# Should be not more needed and replaced by vtherm_api.init_vtherm_links
|
||||
# @callback
|
||||
# async def _async_startup_internal(*_):
|
||||
# _LOGGER.debug("%s - Calling async_startup_internal", self)
|
||||
# await self.listen_nb_active_vtherm_entity()
|
||||
#
|
||||
# if self.hass.state == CoreState.running:
|
||||
# await _async_startup_internal()
|
||||
# else:
|
||||
# self.hass.bus.async_listen_once(
|
||||
# EVENT_HOMEASSISTANT_START, _async_startup_internal
|
||||
# )
|
||||
|
||||
async def listen_nb_active_vtherm_entity(self):
|
||||
"""Initialize the listening of state change of VTherms"""
|
||||
|
||||
# Listen to all VTherm state change
|
||||
api: VersatileThermostatAPI = VersatileThermostatAPI.get_vtherm_api(self._hass)
|
||||
|
||||
if (
|
||||
api.nb_active_device_for_boiler_entity
|
||||
and api.nb_active_device_for_boiler_threshold_entity
|
||||
):
|
||||
listener_cancel = async_track_state_change_event(
|
||||
self._hass,
|
||||
[
|
||||
api.nb_active_device_for_boiler_entity.entity_id,
|
||||
api.nb_active_device_for_boiler_threshold_entity.entity_id,
|
||||
],
|
||||
self.calculate_central_boiler_state,
|
||||
)
|
||||
_LOGGER.debug(
|
||||
"%s - entity to get the nb of active VTherm is %s",
|
||||
self,
|
||||
api.nb_active_device_for_boiler_entity.entity_id,
|
||||
)
|
||||
self.async_on_remove(listener_cancel)
|
||||
else:
|
||||
_LOGGER.debug("%s - no VTherm could controls the central boiler", self)
|
||||
|
||||
await self.calculate_central_boiler_state(None)
|
||||
|
||||
async def calculate_central_boiler_state(self, _):
|
||||
"""Calculate the central boiler state depending on all VTherm that
|
||||
controls this central boiler"""
|
||||
|
||||
_LOGGER.debug("%s - calculating the new central boiler state", self)
|
||||
api: VersatileThermostatAPI = VersatileThermostatAPI.get_vtherm_api(self._hass)
|
||||
if (
|
||||
api.nb_active_device_for_boiler is None
|
||||
or api.nb_active_device_for_boiler_threshold is None
|
||||
):
|
||||
_LOGGER.warning(
|
||||
"%s - the entities to calculate the boiler state are not initialized. Boiler state cannot be calculated",
|
||||
self,
|
||||
)
|
||||
return False
|
||||
|
||||
active = (
|
||||
api.nb_active_device_for_boiler >= api.nb_active_device_for_boiler_threshold
|
||||
)
|
||||
|
||||
if self._attr_is_on != active:
|
||||
try:
|
||||
if active:
|
||||
await self.call_service(self._service_activate)
|
||||
_LOGGER.info("%s - central boiler have been turned on", self)
|
||||
else:
|
||||
await self.call_service(self._service_deactivate)
|
||||
_LOGGER.info("%s - central boiler have been turned off", self)
|
||||
self._attr_is_on = active
|
||||
send_vtherm_event(
|
||||
hass=self._hass,
|
||||
event_type=EventType.CENTRAL_BOILER_EVENT,
|
||||
entity=self,
|
||||
data={"central_boiler": active},
|
||||
)
|
||||
self.async_write_ha_state()
|
||||
except HomeAssistantError as err:
|
||||
_LOGGER.error(
|
||||
"%s - Impossible to activate/deactivat boiler due to error %s."
|
||||
"Central boiler will not being controled by VTherm."
|
||||
"Please check your service configuration. Cf. README.",
|
||||
self,
|
||||
err,
|
||||
)
|
||||
|
||||
async def call_service(self, service_config: dict):
|
||||
"""Make a call to a service if correctly configured"""
|
||||
if not service_config:
|
||||
return
|
||||
|
||||
await self._hass.services.async_call(
|
||||
service_config["service_domain"],
|
||||
service_config["service_name"],
|
||||
service_data=service_config["data"],
|
||||
target={
|
||||
"entity_id": service_config["entity_id"],
|
||||
},
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return f"VersatileThermostat-{self.name}"
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
# pylint: disable=line-too-long
|
||||
# pylint: disable=invalid-name
|
||||
""" Implements the VersatileThermostat climate component """
|
||||
|
||||
import logging
|
||||
|
||||
|
||||
@@ -15,7 +14,13 @@ from homeassistant.helpers.reload import async_setup_reload_service
|
||||
|
||||
from homeassistant.helpers import entity_platform
|
||||
|
||||
from homeassistant.const import CONF_NAME, STATE_ON, STATE_OFF, STATE_HOME, STATE_NOT_HOME
|
||||
from homeassistant.const import (
|
||||
CONF_NAME,
|
||||
STATE_ON,
|
||||
STATE_OFF,
|
||||
STATE_HOME,
|
||||
STATE_NOT_HOME,
|
||||
)
|
||||
|
||||
from .const import (
|
||||
DOMAIN,
|
||||
@@ -26,10 +31,12 @@ from .const import (
|
||||
SERVICE_SET_SECURITY,
|
||||
SERVICE_SET_WINDOW_BYPASS,
|
||||
SERVICE_SET_AUTO_REGULATION_MODE,
|
||||
SERVICE_SET_AUTO_FAN_MODE,
|
||||
CONF_THERMOSTAT_TYPE,
|
||||
CONF_THERMOSTAT_SWITCH,
|
||||
CONF_THERMOSTAT_CLIMATE,
|
||||
CONF_THERMOSTAT_VALVE
|
||||
CONF_THERMOSTAT_VALVE,
|
||||
CONF_THERMOSTAT_CENTRAL_CONFIG,
|
||||
)
|
||||
|
||||
from .thermostat_switch import ThermostatOverSwitch
|
||||
@@ -38,9 +45,6 @@ from .thermostat_valve import ThermostatOverValve
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
# _LOGGER.setLevel(logging.DEBUG)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
@@ -57,13 +61,24 @@ async def async_setup_entry(
|
||||
name = entry.data.get(CONF_NAME)
|
||||
vt_type = entry.data.get(CONF_THERMOSTAT_TYPE)
|
||||
|
||||
if vt_type == CONF_THERMOSTAT_CENTRAL_CONFIG:
|
||||
return
|
||||
|
||||
# Instantiate the right base class
|
||||
entity = None
|
||||
if vt_type == CONF_THERMOSTAT_SWITCH:
|
||||
entity = ThermostatOverSwitch(hass, unique_id, name, entry.data)
|
||||
elif vt_type == CONF_THERMOSTAT_CLIMATE:
|
||||
entity = ThermostatOverClimate(hass, unique_id, name, entry.data)
|
||||
elif vt_type == CONF_THERMOSTAT_VALVE:
|
||||
entity = ThermostatOverValve(hass, unique_id, name, entry.data)
|
||||
else:
|
||||
_LOGGER.error(
|
||||
"Cannot create Versatile Thermostat name=%s of type %s which is unknown",
|
||||
name,
|
||||
vt_type,
|
||||
)
|
||||
return
|
||||
|
||||
async_add_entities([entity], True)
|
||||
|
||||
@@ -102,8 +117,7 @@ async def async_setup_entry(
|
||||
platform.async_register_entity_service(
|
||||
SERVICE_SET_WINDOW_BYPASS,
|
||||
{
|
||||
vol.Required("window_bypass"): vol.In([True, False]
|
||||
),
|
||||
vol.Required("window_bypass"): vol.In([True, False]),
|
||||
},
|
||||
"service_set_window_bypass_state",
|
||||
)
|
||||
@@ -111,7 +125,19 @@ async def async_setup_entry(
|
||||
platform.async_register_entity_service(
|
||||
SERVICE_SET_AUTO_REGULATION_MODE,
|
||||
{
|
||||
vol.Required("auto_regulation_mode"): vol.In(["None", "Light", "Medium", "Strong", "Slow"]),
|
||||
vol.Required("auto_regulation_mode"): vol.In(
|
||||
["None", "Light", "Medium", "Strong", "Slow"]
|
||||
),
|
||||
},
|
||||
"service_set_auto_regulation_mode",
|
||||
)
|
||||
|
||||
platform.async_register_entity_service(
|
||||
SERVICE_SET_AUTO_FAN_MODE,
|
||||
{
|
||||
vol.Required("auto_fan_mode"): vol.In(
|
||||
["None", "Low", "Medium", "High", "Turbo"]
|
||||
),
|
||||
},
|
||||
"service_set_auto_fan_mode",
|
||||
)
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
""" Some usefull commons class """
|
||||
|
||||
# pylint: disable=line-too-long
|
||||
|
||||
import logging
|
||||
from datetime import timedelta, datetime
|
||||
from homeassistant.core import HomeAssistant, callback, Event
|
||||
@@ -10,7 +13,7 @@ from homeassistant.helpers.event import async_track_state_change_event, async_ca
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from .base_thermostat import BaseThermostat
|
||||
from .const import DOMAIN, DEVICE_MANUFACTURER
|
||||
from .const import DOMAIN, DEVICE_MANUFACTURER, ServiceConfigurationError
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -19,32 +22,138 @@ def get_tz(hass: HomeAssistant):
|
||||
|
||||
return dt_util.get_time_zone(hass.config.time_zone)
|
||||
|
||||
|
||||
class NowClass:
|
||||
""" For testing purpose only"""
|
||||
"""For testing purpose only"""
|
||||
|
||||
@staticmethod
|
||||
def get_now(hass: HomeAssistant) -> datetime:
|
||||
""" A test function to get the now.
|
||||
For testing purpose this method can be overriden to get a specific
|
||||
timestamp.
|
||||
"""A test function to get the now.
|
||||
For testing purpose this method can be overriden to get a specific
|
||||
timestamp.
|
||||
"""
|
||||
return datetime.now( get_tz(hass))
|
||||
return datetime.now(get_tz(hass))
|
||||
|
||||
def round_to_nearest(n:float, x: float)->float:
|
||||
""" Round a number to the nearest x (which should be decimal but not null)
|
||||
Example:
|
||||
nombre1 = 3.2
|
||||
nombre2 = 4.7
|
||||
x = 0.3
|
||||
|
||||
nombre_arrondi1 = round_to_nearest(nombre1, x)
|
||||
nombre_arrondi2 = round_to_nearest(nombre2, x)
|
||||
def round_to_nearest(n: float, x: float) -> float:
|
||||
"""Round a number to the nearest x (which should be decimal but not null)
|
||||
Example:
|
||||
nombre1 = 3.2
|
||||
nombre2 = 4.7
|
||||
x = 0.3
|
||||
|
||||
print(nombre_arrondi1) # Output: 3.3
|
||||
print(nombre_arrondi2) # Output: 4.6
|
||||
nombre_arrondi1 = round_to_nearest(nombre1, x)
|
||||
nombre_arrondi2 = round_to_nearest(nombre2, x)
|
||||
|
||||
print(nombre_arrondi1) # Output: 3.3
|
||||
print(nombre_arrondi2) # Output: 4.6
|
||||
"""
|
||||
assert x > 0
|
||||
return round(n * (1/x)) / (1/x)
|
||||
return round(n * (1 / x)) / (1 / x)
|
||||
|
||||
|
||||
def check_and_extract_service_configuration(service_config) -> dict:
|
||||
"""Raise a ServiceConfigurationError. In return you have a dict formatted like follows.
|
||||
Example if you call with 'climate.central_boiler/climate.set_temperature/temperature:10':
|
||||
{
|
||||
"service_domain": "climate",
|
||||
"service_name": "set_temperature",
|
||||
"entity_id": "climate.central_boiler",
|
||||
"entity_domain": "climate",
|
||||
"entity_name": "central_boiler",
|
||||
"data": {
|
||||
"temperature": "10"
|
||||
},
|
||||
"attribute_name": "temperature",
|
||||
"attribute_value: "10"
|
||||
}
|
||||
|
||||
For this example 'switch.central_boiler/switch.turn_off' you will have this:
|
||||
{
|
||||
"service_domain": "switch",
|
||||
"service_name": "turn_off",
|
||||
"entity_id": "switch.central_boiler",
|
||||
"entity_domain": "switch",
|
||||
"entity_name": "central_boiler",
|
||||
"data": { },
|
||||
}
|
||||
|
||||
All values are striped (white space are removed) and are string
|
||||
"""
|
||||
|
||||
ret = {}
|
||||
|
||||
if service_config is None:
|
||||
return ret
|
||||
|
||||
parties = service_config.split("/")
|
||||
if len(parties) < 2:
|
||||
raise ServiceConfigurationError(
|
||||
f"Incorrect service configuration. Service {service_config} should be formatted with: 'entity_name/service_name[/data]'. See README for more information."
|
||||
)
|
||||
entity_id = parties[0]
|
||||
service_name = parties[1]
|
||||
|
||||
service_infos = service_name.split(".")
|
||||
if len(service_infos) != 2:
|
||||
raise ServiceConfigurationError(
|
||||
f"Incorrect service configuration. The service {service_config} should be formatted like: 'domain.service_name' (ex: 'switch.turn_on'). See README for more information."
|
||||
)
|
||||
|
||||
ret.update(
|
||||
{
|
||||
"service_domain": service_infos[0].strip(),
|
||||
"service_name": service_infos[1].strip(),
|
||||
}
|
||||
)
|
||||
|
||||
entity_infos = entity_id.split(".")
|
||||
if len(entity_infos) != 2:
|
||||
raise ServiceConfigurationError(
|
||||
f"Incorrect service configuration. The entity_id {entity_id} should be formatted like: 'domain.entity_name' (ex: 'switch.central_boiler_switch'). See README for more information."
|
||||
)
|
||||
|
||||
ret.update(
|
||||
{
|
||||
"entity_domain": entity_infos[0].strip(),
|
||||
"entity_name": entity_infos[1].strip(),
|
||||
"entity_id": entity_id.strip(),
|
||||
}
|
||||
)
|
||||
|
||||
if len(parties) == 3:
|
||||
data = parties[2]
|
||||
if len(data) > 0:
|
||||
data_infos = None
|
||||
data_infos = data.split(":")
|
||||
if (
|
||||
len(data_infos) != 2
|
||||
or len(data_infos[0]) <= 0
|
||||
or len(data_infos[1]) <= 0
|
||||
):
|
||||
raise ServiceConfigurationError(
|
||||
f"Incorrect service configuration. The data {data} should be formatted like: 'attribute:value' (ex: 'value:25'). See README for more information."
|
||||
)
|
||||
|
||||
ret.update(
|
||||
{
|
||||
"attribute_name": data_infos[0].strip(),
|
||||
"attribute_value": data_infos[1].strip(),
|
||||
"data": {data_infos[0].strip(): data_infos[1].strip()},
|
||||
}
|
||||
)
|
||||
else:
|
||||
raise ServiceConfigurationError(
|
||||
f"Incorrect service configuration. The data {data} should be formatted like: 'attribute:value' (ex: 'value:25'). See README for more information."
|
||||
)
|
||||
else:
|
||||
ret.update({"data": {}})
|
||||
|
||||
_LOGGER.debug(
|
||||
"check_and_extract_service_configuration(%s) gives '%s'", service_config, ret
|
||||
)
|
||||
return ret
|
||||
|
||||
|
||||
class VersatileThermostatBaseEntity(Entity):
|
||||
"""A base class for all entities"""
|
||||
@@ -73,6 +182,9 @@ class VersatileThermostatBaseEntity(Entity):
|
||||
"""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
|
||||
@@ -122,7 +234,7 @@ class VersatileThermostatBaseEntity(Entity):
|
||||
)
|
||||
)
|
||||
else:
|
||||
_LOGGER.warning("%s - no entity to listen. Try later", self)
|
||||
_LOGGER.debug("%s - no entity to listen. Try later", self)
|
||||
self._cancel_call = async_call_later(
|
||||
self.hass, timedelta(seconds=1), try_find_climate
|
||||
)
|
||||
@@ -130,7 +242,14 @@ class VersatileThermostatBaseEntity(Entity):
|
||||
await try_find_climate(None)
|
||||
|
||||
@callback
|
||||
async def async_my_climate_changed(self, event: Event): # pylint: disable=unused-argument
|
||||
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
|
||||
"""
|
||||
|
||||
353
custom_components/versatile_thermostat/config_schema.py
Normal file
@@ -0,0 +1,353 @@
|
||||
""" All the schemas for ConfigFlow validation"""
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers import selector
|
||||
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
|
||||
from homeassistant.components.number import DOMAIN as NUMBER_DOMAIN
|
||||
from homeassistant.components.climate import DOMAIN as CLIMATE_DOMAIN
|
||||
from homeassistant.components.input_boolean import (
|
||||
DOMAIN as INPUT_BOOLEAN_DOMAIN,
|
||||
)
|
||||
|
||||
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
|
||||
from homeassistant.components.input_number import (
|
||||
DOMAIN as INPUT_NUMBER_DOMAIN,
|
||||
)
|
||||
|
||||
from homeassistant.components.input_datetime import (
|
||||
DOMAIN as INPUT_DATETIME_DOMAIN,
|
||||
)
|
||||
|
||||
from homeassistant.components.person import DOMAIN as PERSON_DOMAIN
|
||||
from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN
|
||||
|
||||
|
||||
from .const import * # pylint: disable=wildcard-import, unused-wildcard-import
|
||||
|
||||
STEP_USER_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name
|
||||
{
|
||||
vol.Required(
|
||||
CONF_THERMOSTAT_TYPE, default=CONF_THERMOSTAT_SWITCH
|
||||
): selector.SelectSelector(
|
||||
selector.SelectSelectorConfig(
|
||||
options=CONF_THERMOSTAT_TYPES,
|
||||
translation_key="thermostat_type",
|
||||
mode="list",
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
STEP_MAIN_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name
|
||||
{
|
||||
vol.Required(CONF_NAME): cv.string,
|
||||
vol.Required(CONF_TEMP_SENSOR): selector.EntitySelector(
|
||||
selector.EntitySelectorConfig(domain=[SENSOR_DOMAIN, INPUT_NUMBER_DOMAIN]),
|
||||
),
|
||||
vol.Optional(CONF_LAST_SEEN_TEMP_SENSOR): selector.EntitySelector(
|
||||
selector.EntitySelectorConfig(
|
||||
domain=[SENSOR_DOMAIN, INPUT_DATETIME_DOMAIN]
|
||||
),
|
||||
),
|
||||
vol.Required(CONF_CYCLE_MIN, default=5): cv.positive_int,
|
||||
vol.Optional(CONF_DEVICE_POWER, default="1"): vol.Coerce(float),
|
||||
vol.Required(CONF_USE_MAIN_CENTRAL_CONFIG, default=True): cv.boolean,
|
||||
vol.Optional(CONF_USE_CENTRAL_MODE, default=True): cv.boolean,
|
||||
vol.Required(CONF_USED_BY_CENTRAL_BOILER, default=False): cv.boolean,
|
||||
}
|
||||
)
|
||||
|
||||
STEP_FEATURES_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name
|
||||
{
|
||||
vol.Optional(CONF_USE_WINDOW_FEATURE, default=False): cv.boolean,
|
||||
vol.Optional(CONF_USE_MOTION_FEATURE, default=False): cv.boolean,
|
||||
vol.Optional(CONF_USE_POWER_FEATURE, default=False): cv.boolean,
|
||||
vol.Optional(CONF_USE_PRESENCE_FEATURE, default=False): cv.boolean,
|
||||
}
|
||||
)
|
||||
|
||||
STEP_CLIMATE_FEATURES_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name
|
||||
{
|
||||
vol.Optional(CONF_USE_WINDOW_FEATURE, default=False): cv.boolean,
|
||||
vol.Optional(CONF_USE_MOTION_FEATURE, default=False): cv.boolean,
|
||||
vol.Optional(CONF_USE_POWER_FEATURE, default=False): cv.boolean,
|
||||
vol.Optional(CONF_USE_PRESENCE_FEATURE, default=False): cv.boolean,
|
||||
vol.Optional(CONF_USE_AUTO_START_STOP_FEATURE, default=False): cv.boolean,
|
||||
}
|
||||
)
|
||||
|
||||
STEP_CENTRAL_FEATURES_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name
|
||||
{
|
||||
vol.Optional(CONF_USE_WINDOW_FEATURE, default=False): cv.boolean,
|
||||
vol.Optional(CONF_USE_MOTION_FEATURE, default=False): cv.boolean,
|
||||
vol.Optional(CONF_USE_POWER_FEATURE, default=False): cv.boolean,
|
||||
vol.Optional(CONF_USE_PRESENCE_FEATURE, default=False): cv.boolean,
|
||||
vol.Optional(CONF_USE_CENTRAL_BOILER_FEATURE, default=False): cv.boolean,
|
||||
}
|
||||
)
|
||||
|
||||
STEP_CENTRAL_MAIN_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name
|
||||
{
|
||||
vol.Required(CONF_EXTERNAL_TEMP_SENSOR): selector.EntitySelector(
|
||||
selector.EntitySelectorConfig(domain=[SENSOR_DOMAIN, INPUT_NUMBER_DOMAIN]),
|
||||
),
|
||||
vol.Required(CONF_TEMP_MIN, default=7): vol.Coerce(float),
|
||||
vol.Required(CONF_TEMP_MAX, default=35): vol.Coerce(float),
|
||||
vol.Required(CONF_STEP_TEMPERATURE, default=0.1): vol.Coerce(float),
|
||||
}
|
||||
)
|
||||
|
||||
STEP_CENTRAL_SPEC_MAIN_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name
|
||||
{
|
||||
vol.Required(CONF_EXTERNAL_TEMP_SENSOR): selector.EntitySelector(
|
||||
selector.EntitySelectorConfig(domain=[SENSOR_DOMAIN, INPUT_NUMBER_DOMAIN]),
|
||||
),
|
||||
vol.Required(CONF_TEMP_MIN, default=7): vol.Coerce(float),
|
||||
vol.Required(CONF_TEMP_MAX, default=35): vol.Coerce(float),
|
||||
vol.Required(CONF_STEP_TEMPERATURE, default=0.1): vol.Coerce(float),
|
||||
}
|
||||
)
|
||||
|
||||
STEP_CENTRAL_BOILER_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_CENTRAL_BOILER_ACTIVATION_SRV, default=""): str,
|
||||
vol.Optional(CONF_CENTRAL_BOILER_DEACTIVATION_SRV, default=""): str,
|
||||
}
|
||||
)
|
||||
|
||||
STEP_THERMOSTAT_SWITCH = vol.Schema( # pylint: disable=invalid-name
|
||||
{
|
||||
vol.Required(CONF_UNDERLYING_LIST): selector.EntitySelector(
|
||||
selector.EntitySelectorConfig(
|
||||
domain=[SWITCH_DOMAIN, INPUT_BOOLEAN_DOMAIN], multiple=True
|
||||
),
|
||||
),
|
||||
vol.Optional(CONF_HEATER_KEEP_ALIVE): cv.positive_int,
|
||||
vol.Required(CONF_PROP_FUNCTION, default=PROPORTIONAL_FUNCTION_TPI): vol.In(
|
||||
[
|
||||
PROPORTIONAL_FUNCTION_TPI,
|
||||
]
|
||||
),
|
||||
vol.Optional(CONF_AC_MODE, default=False): cv.boolean,
|
||||
vol.Optional(CONF_INVERSE_SWITCH, default=False): cv.boolean,
|
||||
}
|
||||
)
|
||||
|
||||
STEP_THERMOSTAT_CLIMATE = vol.Schema( # pylint: disable=invalid-name
|
||||
{
|
||||
vol.Required(CONF_UNDERLYING_LIST): selector.EntitySelector(
|
||||
selector.EntitySelectorConfig(domain=CLIMATE_DOMAIN, multiple=True),
|
||||
),
|
||||
vol.Optional(CONF_AC_MODE, default=False): cv.boolean,
|
||||
vol.Optional(
|
||||
CONF_AUTO_REGULATION_MODE, default=CONF_AUTO_REGULATION_NONE
|
||||
): selector.SelectSelector(
|
||||
selector.SelectSelectorConfig(
|
||||
options=CONF_AUTO_REGULATION_MODES,
|
||||
translation_key="auto_regulation_mode",
|
||||
mode="dropdown",
|
||||
)
|
||||
),
|
||||
vol.Optional(CONF_AUTO_REGULATION_DTEMP, default=0.5): vol.Coerce(float),
|
||||
vol.Optional(CONF_AUTO_REGULATION_PERIOD_MIN, default=5): cv.positive_int,
|
||||
vol.Optional(
|
||||
CONF_AUTO_FAN_MODE, default=CONF_AUTO_FAN_HIGH
|
||||
): selector.SelectSelector(
|
||||
selector.SelectSelectorConfig(
|
||||
options=CONF_AUTO_FAN_MODES,
|
||||
translation_key="auto_fan_mode",
|
||||
mode="dropdown",
|
||||
)
|
||||
),
|
||||
vol.Optional(CONF_AUTO_REGULATION_USE_DEVICE_TEMP, default=False): cv.boolean,
|
||||
}
|
||||
)
|
||||
|
||||
STEP_THERMOSTAT_VALVE = vol.Schema( # pylint: disable=invalid-name
|
||||
{
|
||||
vol.Required(CONF_UNDERLYING_LIST): selector.EntitySelector(
|
||||
selector.EntitySelectorConfig(
|
||||
domain=[NUMBER_DOMAIN, INPUT_NUMBER_DOMAIN], multiple=True
|
||||
),
|
||||
),
|
||||
vol.Required(CONF_PROP_FUNCTION, default=PROPORTIONAL_FUNCTION_TPI): vol.In(
|
||||
[
|
||||
PROPORTIONAL_FUNCTION_TPI,
|
||||
]
|
||||
),
|
||||
vol.Optional(CONF_AC_MODE, default=False): cv.boolean,
|
||||
vol.Optional(CONF_AUTO_REGULATION_DTEMP, default=10): vol.Coerce(float),
|
||||
vol.Optional(CONF_AUTO_REGULATION_PERIOD_MIN, default=5): cv.positive_int,
|
||||
}
|
||||
)
|
||||
|
||||
STEP_AUTO_START_STOP = vol.Schema( # pylint: disable=invalid-name
|
||||
{
|
||||
vol.Optional(
|
||||
CONF_AUTO_START_STOP_LEVEL, default=AUTO_START_STOP_LEVEL_NONE
|
||||
): selector.SelectSelector(
|
||||
selector.SelectSelectorConfig(
|
||||
options=CONF_AUTO_START_STOP_LEVELS,
|
||||
translation_key="auto_start_stop",
|
||||
mode="dropdown",
|
||||
)
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
STEP_TPI_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name
|
||||
{
|
||||
vol.Required(CONF_USE_TPI_CENTRAL_CONFIG, default=True): cv.boolean,
|
||||
}
|
||||
)
|
||||
|
||||
STEP_CENTRAL_TPI_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name
|
||||
{
|
||||
vol.Required(CONF_TPI_COEF_INT, default=0.6): vol.Coerce(float),
|
||||
vol.Required(CONF_TPI_COEF_EXT, default=0.01): vol.Coerce(float),
|
||||
}
|
||||
)
|
||||
|
||||
STEP_PRESETS_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name
|
||||
{
|
||||
vol.Required(CONF_USE_PRESETS_CENTRAL_CONFIG, default=True): cv.boolean,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
STEP_WINDOW_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name
|
||||
{
|
||||
vol.Optional(CONF_WINDOW_SENSOR): selector.EntitySelector(
|
||||
selector.EntitySelectorConfig(
|
||||
domain=[BINARY_SENSOR_DOMAIN, INPUT_BOOLEAN_DOMAIN]
|
||||
),
|
||||
),
|
||||
vol.Required(CONF_USE_WINDOW_CENTRAL_CONFIG, default=True): cv.boolean,
|
||||
}
|
||||
)
|
||||
|
||||
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_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(
|
||||
selector.SelectSelectorConfig(
|
||||
options=CONF_WINDOW_ACTIONS,
|
||||
translation_key="window_action",
|
||||
mode="dropdown",
|
||||
)
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
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(
|
||||
selector.SelectSelectorConfig(
|
||||
options=CONF_WINDOW_ACTIONS,
|
||||
translation_key="window_action",
|
||||
mode="dropdown",
|
||||
)
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
STEP_MOTION_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name
|
||||
{
|
||||
vol.Required(CONF_MOTION_SENSOR): selector.EntitySelector(
|
||||
selector.EntitySelectorConfig(
|
||||
domain=[BINARY_SENSOR_DOMAIN, INPUT_BOOLEAN_DOMAIN]
|
||||
),
|
||||
),
|
||||
vol.Required(CONF_USE_MOTION_CENTRAL_CONFIG, default=True): cv.boolean,
|
||||
}
|
||||
)
|
||||
|
||||
STEP_CENTRAL_MOTION_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name
|
||||
{
|
||||
vol.Optional(CONF_MOTION_DELAY, default=30): cv.positive_int,
|
||||
vol.Optional(CONF_MOTION_OFF_DELAY, default=300): cv.positive_int,
|
||||
vol.Optional(CONF_MOTION_PRESET, default="comfort"): selector.SelectSelector(
|
||||
selector.SelectSelectorConfig(
|
||||
options=CONF_PRESETS_SELECTIONABLE,
|
||||
translation_key="presets",
|
||||
mode="dropdown",
|
||||
)
|
||||
),
|
||||
vol.Optional(CONF_NO_MOTION_PRESET, default="eco"): selector.SelectSelector(
|
||||
selector.SelectSelectorConfig(
|
||||
options=CONF_PRESETS_SELECTIONABLE,
|
||||
translation_key="presets",
|
||||
mode="dropdown",
|
||||
)
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
STEP_CENTRAL_POWER_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name
|
||||
{
|
||||
vol.Required(CONF_POWER_SENSOR): selector.EntitySelector(
|
||||
selector.EntitySelectorConfig(domain=[SENSOR_DOMAIN, INPUT_NUMBER_DOMAIN]),
|
||||
),
|
||||
vol.Required(CONF_MAX_POWER_SENSOR): selector.EntitySelector(
|
||||
selector.EntitySelectorConfig(domain=[SENSOR_DOMAIN, INPUT_NUMBER_DOMAIN]),
|
||||
),
|
||||
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,
|
||||
}
|
||||
)
|
||||
|
||||
STEP_CENTRAL_PRESENCE_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name
|
||||
{
|
||||
vol.Required(CONF_PRESENCE_SENSOR): selector.EntitySelector(
|
||||
selector.EntitySelectorConfig(
|
||||
domain=[
|
||||
PERSON_DOMAIN,
|
||||
BINARY_SENSOR_DOMAIN,
|
||||
INPUT_BOOLEAN_DOMAIN,
|
||||
]
|
||||
),
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
STEP_PRESENCE_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name
|
||||
{
|
||||
vol.Required(CONF_USE_PRESENCE_CENTRAL_CONFIG, default=True): cv.boolean,
|
||||
}
|
||||
)
|
||||
|
||||
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_SECURITY_MIN_ON_PERCENT,
|
||||
default=DEFAULT_SECURITY_MIN_ON_PERCENT,
|
||||
): vol.Coerce(float),
|
||||
vol.Required(
|
||||
CONF_SECURITY_DEFAULT_ON_PERCENT,
|
||||
default=DEFAULT_SECURITY_DEFAULT_ON_PERCENT,
|
||||
): vol.Coerce(float),
|
||||
}
|
||||
)
|
||||
|
||||
STEP_ADVANCED_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name
|
||||
{
|
||||
vol.Required(CONF_USE_ADVANCED_CENTRAL_CONFIG, default=True): cv.boolean,
|
||||
}
|
||||
)
|
||||
@@ -1,6 +1,9 @@
|
||||
# pylint: disable=line-too-long
|
||||
"""Constants for the Versatile Thermostat integration."""
|
||||
|
||||
import logging
|
||||
from typing import Literal
|
||||
|
||||
from enum import Enum
|
||||
from homeassistant.const import CONF_NAME, Platform
|
||||
|
||||
@@ -17,6 +20,13 @@ from homeassistant.exceptions import HomeAssistantError
|
||||
from .prop_algorithm import (
|
||||
PROPORTIONAL_FUNCTION_TPI,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONFIG_VERSION = 2
|
||||
CONFIG_MINOR_VERSION = 0
|
||||
|
||||
PRESET_TEMP_SUFFIX = "_temp"
|
||||
PRESET_AC_SUFFIX = "_ac"
|
||||
PRESET_ECO_AC = PRESET_ECO + PRESET_AC_SUFFIX
|
||||
PRESET_COMFORT_AC = PRESET_COMFORT + PRESET_AC_SUFFIX
|
||||
@@ -28,18 +38,27 @@ DEVICE_MODEL = "Versatile Thermostat"
|
||||
|
||||
PRESET_POWER = "power"
|
||||
PRESET_SECURITY = "security"
|
||||
PRESET_FROST_PROTECTION = "frost"
|
||||
|
||||
HIDDEN_PRESETS = [PRESET_POWER, PRESET_SECURITY]
|
||||
|
||||
DOMAIN = "versatile_thermostat"
|
||||
|
||||
PLATFORMS: list[Platform] = [Platform.CLIMATE, Platform.BINARY_SENSOR, Platform.SENSOR]
|
||||
# The order is important.
|
||||
PLATFORMS: list[Platform] = [
|
||||
Platform.SELECT,
|
||||
Platform.CLIMATE,
|
||||
Platform.SENSOR,
|
||||
# Number should be after CLIMATE
|
||||
Platform.NUMBER,
|
||||
Platform.BINARY_SENSOR,
|
||||
Platform.SWITCH,
|
||||
]
|
||||
|
||||
CONF_HEATER = "heater_entity_id"
|
||||
CONF_HEATER_2 = "heater_entity2_id"
|
||||
CONF_HEATER_3 = "heater_entity3_id"
|
||||
CONF_HEATER_4 = "heater_entity4_id"
|
||||
CONF_UNDERLYING_LIST = "underlying_entity_ids"
|
||||
CONF_HEATER_KEEP_ALIVE = "heater_keep_alive"
|
||||
CONF_TEMP_SENSOR = "temperature_sensor_entity_id"
|
||||
CONF_LAST_SEEN_TEMP_SENSOR = "last_seen_temperature_sensor_entity_id"
|
||||
CONF_EXTERNAL_TEMP_SENSOR = "external_temperature_sensor_entity_id"
|
||||
CONF_POWER_SENSOR = "power_sensor_entity_id"
|
||||
CONF_MAX_POWER_SENSOR = "max_power_sensor_entity_id"
|
||||
@@ -64,38 +83,115 @@ 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_THERMOSTAT_TYPE = "thermostat_type"
|
||||
CONF_THERMOSTAT_CENTRAL_CONFIG = "thermostat_central_config"
|
||||
CONF_THERMOSTAT_SWITCH = "thermostat_over_switch"
|
||||
CONF_THERMOSTAT_CLIMATE = "thermostat_over_climate"
|
||||
CONF_THERMOSTAT_VALVE = "thermostat_over_valve"
|
||||
CONF_CLIMATE = "climate_entity_id"
|
||||
CONF_CLIMATE_2 = "climate_entity2_id"
|
||||
CONF_CLIMATE_3 = "climate_entity3_id"
|
||||
CONF_CLIMATE_4 = "climate_entity4_id"
|
||||
CONF_USE_WINDOW_FEATURE = "use_window_feature"
|
||||
CONF_USE_MOTION_FEATURE = "use_motion_feature"
|
||||
CONF_USE_PRESENCE_FEATURE = "use_presence_feature"
|
||||
CONF_USE_POWER_FEATURE = "use_power_feature"
|
||||
CONF_USE_CENTRAL_BOILER_FEATURE = "use_central_boiler_feature"
|
||||
CONF_USE_AUTO_START_STOP_FEATURE = "use_auto_start_stop_feature"
|
||||
CONF_AC_MODE = "ac_mode"
|
||||
CONF_WINDOW_AUTO_OPEN_THRESHOLD = "window_auto_open_threshold"
|
||||
CONF_WINDOW_AUTO_CLOSE_THRESHOLD = "window_auto_close_threshold"
|
||||
CONF_WINDOW_AUTO_MAX_DURATION = "window_auto_max_duration"
|
||||
CONF_AUTO_REGULATION_MODE = "auto_regulation_mode"
|
||||
CONF_AUTO_REGULATION_NONE = "auto_regulation_none"
|
||||
CONF_AUTO_REGULATION_SLOW = "auto_regulation_slow"
|
||||
CONF_AUTO_REGULATION_LIGHT = "auto_regulation_light"
|
||||
CONF_AUTO_REGULATION_MEDIUM = "auto_regulation_medium"
|
||||
CONF_AUTO_REGULATION_STRONG = "auto_regulation_strong"
|
||||
CONF_AUTO_REGULATION_EXPERT = "auto_regulation_expert"
|
||||
CONF_AUTO_REGULATION_DTEMP = "auto_regulation_dtemp"
|
||||
CONF_AUTO_REGULATION_PERIOD_MIN = "auto_regulation_periode_min"
|
||||
CONF_AUTO_REGULATION_USE_DEVICE_TEMP = "auto_regulation_use_device_temp"
|
||||
CONF_INVERSE_SWITCH = "inverse_switch_command"
|
||||
CONF_AUTO_FAN_MODE = "auto_fan_mode"
|
||||
CONF_AUTO_FAN_NONE = "auto_fan_none"
|
||||
CONF_AUTO_FAN_LOW = "auto_fan_low"
|
||||
CONF_AUTO_FAN_MEDIUM = "auto_fan_medium"
|
||||
CONF_AUTO_FAN_HIGH = "auto_fan_high"
|
||||
CONF_AUTO_FAN_TURBO = "auto_fan_turbo"
|
||||
CONF_STEP_TEMPERATURE = "step_temperature"
|
||||
|
||||
# Deprecated
|
||||
CONF_HEATER = "heater_entity_id"
|
||||
CONF_HEATER_2 = "heater_entity2_id"
|
||||
CONF_HEATER_3 = "heater_entity3_id"
|
||||
CONF_HEATER_4 = "heater_entity4_id"
|
||||
CONF_CLIMATE = "climate_entity_id"
|
||||
CONF_CLIMATE_2 = "climate_entity2_id"
|
||||
CONF_CLIMATE_3 = "climate_entity3_id"
|
||||
CONF_CLIMATE_4 = "climate_entity4_id"
|
||||
CONF_VALVE = "valve_entity_id"
|
||||
CONF_VALVE_2 = "valve_entity2_id"
|
||||
CONF_VALVE_3 = "valve_entity3_id"
|
||||
CONF_VALVE_4 = "valve_entity4_id"
|
||||
CONF_AUTO_REGULATION_MODE= "auto_regulation_mode"
|
||||
CONF_AUTO_REGULATION_NONE= "auto_regulation_none"
|
||||
CONF_AUTO_REGULATION_SLOW= "auto_regulation_slow"
|
||||
CONF_AUTO_REGULATION_LIGHT= "auto_regulation_light"
|
||||
CONF_AUTO_REGULATION_MEDIUM= "auto_regulation_medium"
|
||||
CONF_AUTO_REGULATION_STRONG= "auto_regulation_strong"
|
||||
CONF_AUTO_REGULATION_DTEMP="auto_regulation_dtemp"
|
||||
CONF_AUTO_REGULATION_PERIOD_MIN="auto_regulation_periode_min"
|
||||
CONF_INVERSE_SWITCH="inverse_switch_command"
|
||||
|
||||
# Global params into configuration.yaml
|
||||
CONF_SHORT_EMA_PARAMS = "short_ema_params"
|
||||
CONF_SAFETY_MODE = "safety_mode"
|
||||
|
||||
CONF_USE_MAIN_CENTRAL_CONFIG = "use_main_central_config"
|
||||
CONF_USE_TPI_CENTRAL_CONFIG = "use_tpi_central_config"
|
||||
CONF_USE_WINDOW_CENTRAL_CONFIG = "use_window_central_config"
|
||||
CONF_USE_MOTION_CENTRAL_CONFIG = "use_motion_central_config"
|
||||
CONF_USE_POWER_CENTRAL_CONFIG = "use_power_central_config"
|
||||
CONF_USE_PRESENCE_CENTRAL_CONFIG = "use_presence_central_config"
|
||||
CONF_USE_PRESETS_CENTRAL_CONFIG = "use_presets_central_config"
|
||||
CONF_USE_ADVANCED_CENTRAL_CONFIG = "use_advanced_central_config"
|
||||
|
||||
CONF_USE_CENTRAL_MODE = "use_central_mode"
|
||||
|
||||
CONF_CENTRAL_BOILER_ACTIVATION_SRV = "central_boiler_activation_service"
|
||||
CONF_CENTRAL_BOILER_DEACTIVATION_SRV = "central_boiler_deactivation_service"
|
||||
|
||||
CONF_USED_BY_CENTRAL_BOILER = "used_by_controls_central_boiler"
|
||||
CONF_WINDOW_ACTION = "window_action"
|
||||
|
||||
CONF_AUTO_START_STOP_LEVEL = "auto_start_stop_level"
|
||||
AUTO_START_STOP_LEVEL_NONE = "auto_start_stop_none"
|
||||
AUTO_START_STOP_LEVEL_SLOW = "auto_start_stop_slow"
|
||||
AUTO_START_STOP_LEVEL_MEDIUM = "auto_start_stop_medium"
|
||||
AUTO_START_STOP_LEVEL_FAST = "auto_start_stop_fast"
|
||||
CONF_AUTO_START_STOP_LEVELS = [
|
||||
AUTO_START_STOP_LEVEL_NONE,
|
||||
AUTO_START_STOP_LEVEL_SLOW,
|
||||
AUTO_START_STOP_LEVEL_MEDIUM,
|
||||
AUTO_START_STOP_LEVEL_FAST,
|
||||
]
|
||||
|
||||
# For explicit typing purpose only
|
||||
TYPE_AUTO_START_STOP_LEVELS = Literal[ # pylint: disable=invalid-name
|
||||
AUTO_START_STOP_LEVEL_FAST,
|
||||
AUTO_START_STOP_LEVEL_MEDIUM,
|
||||
AUTO_START_STOP_LEVEL_SLOW,
|
||||
AUTO_START_STOP_LEVEL_NONE,
|
||||
]
|
||||
|
||||
HVAC_OFF_REASON_NAME = "hvac_off_reason"
|
||||
HVAC_OFF_REASON_MANUAL = "manual"
|
||||
HVAC_OFF_REASON_AUTO_START_STOP = "auto_start_stop"
|
||||
HVAC_OFF_REASON_WINDOW_DETECTION = "window_detection"
|
||||
HVAC_OFF_REASONS = Literal[ # pylint: disable=invalid-name
|
||||
HVAC_OFF_REASON_MANUAL,
|
||||
HVAC_OFF_REASON_AUTO_START_STOP,
|
||||
HVAC_OFF_REASON_WINDOW_DETECTION,
|
||||
]
|
||||
|
||||
DEFAULT_SHORT_EMA_PARAMS = {
|
||||
"max_alpha": 0.5,
|
||||
# In sec
|
||||
"halflife_sec": 300,
|
||||
"precision": 2,
|
||||
}
|
||||
|
||||
CONF_PRESETS = {
|
||||
p: f"{p}_temp"
|
||||
p: f"{p}{PRESET_TEMP_SUFFIX}"
|
||||
for p in (
|
||||
PRESET_FROST_PROTECTION,
|
||||
PRESET_ECO,
|
||||
PRESET_COMFORT,
|
||||
PRESET_BOOST,
|
||||
@@ -103,8 +199,9 @@ CONF_PRESETS = {
|
||||
}
|
||||
|
||||
CONF_PRESETS_WITH_AC = {
|
||||
p: f"{p}_temp"
|
||||
p: f"{p}{PRESET_TEMP_SUFFIX}"
|
||||
for p in (
|
||||
PRESET_FROST_PROTECTION,
|
||||
PRESET_ECO,
|
||||
PRESET_COMFORT,
|
||||
PRESET_BOOST,
|
||||
@@ -118,8 +215,9 @@ CONF_PRESETS_WITH_AC = {
|
||||
PRESET_AWAY_SUFFIX = "_away"
|
||||
|
||||
CONF_PRESETS_AWAY = {
|
||||
p: f"{p}_temp"
|
||||
p: f"{p}{PRESET_TEMP_SUFFIX}"
|
||||
for p in (
|
||||
PRESET_FROST_PROTECTION + PRESET_AWAY_SUFFIX,
|
||||
PRESET_ECO + PRESET_AWAY_SUFFIX,
|
||||
PRESET_COMFORT + PRESET_AWAY_SUFFIX,
|
||||
PRESET_BOOST + PRESET_AWAY_SUFFIX,
|
||||
@@ -127,8 +225,9 @@ CONF_PRESETS_AWAY = {
|
||||
}
|
||||
|
||||
CONF_PRESETS_AWAY_WITH_AC = {
|
||||
p: f"{p}_temp"
|
||||
p: f"{p}{PRESET_TEMP_SUFFIX}"
|
||||
for p in (
|
||||
PRESET_FROST_PROTECTION + PRESET_AWAY_SUFFIX,
|
||||
PRESET_ECO + PRESET_AWAY_SUFFIX,
|
||||
PRESET_COMFORT + PRESET_AWAY_SUFFIX,
|
||||
PRESET_BOOST + PRESET_AWAY_SUFFIX,
|
||||
@@ -138,7 +237,12 @@ CONF_PRESETS_AWAY_WITH_AC = {
|
||||
)
|
||||
}
|
||||
|
||||
CONF_PRESETS_SELECTIONABLE = [PRESET_ECO, PRESET_COMFORT, PRESET_BOOST]
|
||||
CONF_PRESETS_SELECTIONABLE = [
|
||||
PRESET_FROST_PROTECTION,
|
||||
PRESET_ECO,
|
||||
PRESET_COMFORT,
|
||||
PRESET_BOOST,
|
||||
]
|
||||
|
||||
CONF_PRESETS_VALUES = list(CONF_PRESETS.values())
|
||||
CONF_PRESETS_AWAY_VALUES = list(CONF_PRESETS_AWAY.values())
|
||||
@@ -148,10 +252,7 @@ CONF_PRESETS_AWAY_WITH_AC_VALUES = list(CONF_PRESETS_AWAY_WITH_AC.values())
|
||||
ALL_CONF = (
|
||||
[
|
||||
CONF_NAME,
|
||||
CONF_HEATER,
|
||||
CONF_HEATER_2,
|
||||
CONF_HEATER_3,
|
||||
CONF_HEATER_4,
|
||||
CONF_HEATER_KEEP_ALIVE,
|
||||
CONF_TEMP_SENSOR,
|
||||
CONF_EXTERNAL_TEMP_SENSOR,
|
||||
CONF_POWER_SENSOR,
|
||||
@@ -180,23 +281,32 @@ ALL_CONF = (
|
||||
CONF_THERMOSTAT_TYPE,
|
||||
CONF_THERMOSTAT_SWITCH,
|
||||
CONF_THERMOSTAT_CLIMATE,
|
||||
CONF_CLIMATE,
|
||||
CONF_CLIMATE_2,
|
||||
CONF_CLIMATE_3,
|
||||
CONF_CLIMATE_4,
|
||||
CONF_USE_WINDOW_FEATURE,
|
||||
CONF_USE_MOTION_FEATURE,
|
||||
CONF_USE_PRESENCE_FEATURE,
|
||||
CONF_USE_POWER_FEATURE,
|
||||
CONF_USE_CENTRAL_BOILER_FEATURE,
|
||||
CONF_AC_MODE,
|
||||
CONF_VALVE,
|
||||
CONF_VALVE_2,
|
||||
CONF_VALVE_3,
|
||||
CONF_VALVE_4,
|
||||
CONF_AUTO_REGULATION_MODE,
|
||||
CONF_AUTO_REGULATION_DTEMP,
|
||||
CONF_AUTO_REGULATION_PERIOD_MIN,
|
||||
CONF_INVERSE_SWITCH
|
||||
CONF_AUTO_REGULATION_USE_DEVICE_TEMP,
|
||||
CONF_INVERSE_SWITCH,
|
||||
CONF_AUTO_FAN_MODE,
|
||||
CONF_USE_MAIN_CENTRAL_CONFIG,
|
||||
CONF_USE_TPI_CENTRAL_CONFIG,
|
||||
CONF_USE_PRESETS_CENTRAL_CONFIG,
|
||||
CONF_USE_WINDOW_CENTRAL_CONFIG,
|
||||
CONF_USE_MOTION_CENTRAL_CONFIG,
|
||||
CONF_USE_POWER_CENTRAL_CONFIG,
|
||||
CONF_USE_PRESENCE_CENTRAL_CONFIG,
|
||||
CONF_USE_ADVANCED_CENTRAL_CONFIG,
|
||||
CONF_USE_CENTRAL_MODE,
|
||||
CONF_USED_BY_CENTRAL_BOILER,
|
||||
CONF_CENTRAL_BOILER_ACTIVATION_SRV,
|
||||
CONF_CENTRAL_BOILER_DEACTIVATION_SRV,
|
||||
CONF_WINDOW_ACTION,
|
||||
CONF_STEP_TEMPERATURE,
|
||||
]
|
||||
+ CONF_PRESETS_VALUES
|
||||
+ CONF_PRESETS_AWAY_VALUES
|
||||
@@ -208,17 +318,54 @@ CONF_FUNCTIONS = [
|
||||
PROPORTIONAL_FUNCTION_TPI,
|
||||
]
|
||||
|
||||
CONF_AUTO_REGULATION_MODES = [CONF_AUTO_REGULATION_NONE, CONF_AUTO_REGULATION_LIGHT, CONF_AUTO_REGULATION_MEDIUM, CONF_AUTO_REGULATION_STRONG, CONF_AUTO_REGULATION_SLOW]
|
||||
CONF_AUTO_REGULATION_MODES = [
|
||||
CONF_AUTO_REGULATION_NONE,
|
||||
CONF_AUTO_REGULATION_LIGHT,
|
||||
CONF_AUTO_REGULATION_MEDIUM,
|
||||
CONF_AUTO_REGULATION_STRONG,
|
||||
CONF_AUTO_REGULATION_SLOW,
|
||||
CONF_AUTO_REGULATION_EXPERT,
|
||||
]
|
||||
|
||||
CONF_THERMOSTAT_TYPES = [CONF_THERMOSTAT_SWITCH, CONF_THERMOSTAT_CLIMATE, CONF_THERMOSTAT_VALVE]
|
||||
CONF_THERMOSTAT_TYPES = [
|
||||
CONF_THERMOSTAT_CENTRAL_CONFIG,
|
||||
CONF_THERMOSTAT_SWITCH,
|
||||
CONF_THERMOSTAT_CLIMATE,
|
||||
CONF_THERMOSTAT_VALVE,
|
||||
]
|
||||
|
||||
SUPPORT_FLAGS = ClimateEntityFeature.TARGET_TEMPERATURE
|
||||
CONF_AUTO_FAN_MODES = [
|
||||
CONF_AUTO_FAN_NONE,
|
||||
CONF_AUTO_FAN_LOW,
|
||||
CONF_AUTO_FAN_MEDIUM,
|
||||
CONF_AUTO_FAN_HIGH,
|
||||
CONF_AUTO_FAN_TURBO,
|
||||
]
|
||||
|
||||
CONF_WINDOW_TURN_OFF = "window_turn_off"
|
||||
CONF_WINDOW_FAN_ONLY = "window_fan_only"
|
||||
CONF_WINDOW_FROST_TEMP = "window_frost_temp"
|
||||
CONF_WINDOW_ECO_TEMP = "window_eco_temp"
|
||||
|
||||
CONF_WINDOW_ACTIONS = [
|
||||
CONF_WINDOW_TURN_OFF,
|
||||
CONF_WINDOW_FAN_ONLY,
|
||||
CONF_WINDOW_FROST_TEMP,
|
||||
CONF_WINDOW_ECO_TEMP,
|
||||
]
|
||||
|
||||
SUPPORT_FLAGS = (
|
||||
ClimateEntityFeature.TARGET_TEMPERATURE
|
||||
| ClimateEntityFeature.TURN_OFF
|
||||
| ClimateEntityFeature.TURN_ON
|
||||
)
|
||||
|
||||
SERVICE_SET_PRESENCE = "set_presence"
|
||||
SERVICE_SET_PRESET_TEMPERATURE = "set_preset_temperature"
|
||||
SERVICE_SET_SECURITY = "set_security"
|
||||
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
|
||||
@@ -226,54 +373,95 @@ DEFAULT_SECURITY_DEFAULT_ON_PERCENT = 0.1
|
||||
ATTR_TOTAL_ENERGY = "total_energy"
|
||||
ATTR_MEAN_POWER_CYCLE = "mean_cycle_power"
|
||||
|
||||
AUTO_FAN_DTEMP_THRESHOLD = 2
|
||||
AUTO_FAN_DEACTIVATED_MODES = ["mute", "auto", "low"]
|
||||
|
||||
CENTRAL_CONFIG_NAME = "Central configuration"
|
||||
|
||||
CENTRAL_MODE_AUTO = "Auto"
|
||||
CENTRAL_MODE_STOPPED = "Stopped"
|
||||
CENTRAL_MODE_HEAT_ONLY = "Heat only"
|
||||
CENTRAL_MODE_COOL_ONLY = "Cool only"
|
||||
CENTRAL_MODE_FROST_PROTECTION = "Frost protection"
|
||||
CENTRAL_MODES = [
|
||||
CENTRAL_MODE_AUTO,
|
||||
CENTRAL_MODE_STOPPED,
|
||||
CENTRAL_MODE_HEAT_ONLY,
|
||||
CENTRAL_MODE_COOL_ONLY,
|
||||
CENTRAL_MODE_FROST_PROTECTION,
|
||||
]
|
||||
|
||||
|
||||
# A special regulation parameter suggested by @Maia here: https://github.com/jmcollin78/versatile_thermostat/discussions/154
|
||||
class RegulationParamSlow:
|
||||
""" Light parameters for slow latency regulation"""
|
||||
kp:float = 0.2 # 20% of the current internal regulation offset are caused by the current difference of target temperature and room temperature
|
||||
ki:float = 0.8 / 288.0 # 80% of the current internal regulation offset are caused by the average offset of the past 24 hours
|
||||
k_ext:float = 1.0 / 25.0 # this will add 1°C to the offset when it's 25°C colder outdoor than indoor
|
||||
offset_max:float = 2.0 # limit to a final offset of -2°C to +2°C
|
||||
stabilization_threshold:float = 0.0 # this needs to be disabled as otherwise the long term accumulated error will always be reset when the temp briefly crosses from/to below/above the target
|
||||
accumulated_error_threshold:float = 2.0 * 288 # this allows up to 2°C long term offset in both directions
|
||||
"""Light parameters for slow latency regulation"""
|
||||
|
||||
kp: float = (
|
||||
0.2 # 20% of the current internal regulation offset are caused by the current difference of target temperature and room temperature
|
||||
)
|
||||
ki: float = (
|
||||
0.8 / 288.0
|
||||
) # 80% of the current internal regulation offset are caused by the average offset of the past 24 hours
|
||||
k_ext: float = (
|
||||
1.0 / 25.0
|
||||
) # this will add 1°C to the offset when it's 25°C colder outdoor than indoor
|
||||
offset_max: float = 2.0 # limit to a final offset of -2°C to +2°C
|
||||
stabilization_threshold: float = (
|
||||
0.0 # this needs to be disabled as otherwise the long term accumulated error will always be reset when the temp briefly crosses from/to below/above the target
|
||||
)
|
||||
accumulated_error_threshold: float = (
|
||||
2.0 * 288
|
||||
) # this allows up to 2°C long term offset in both directions
|
||||
|
||||
|
||||
class RegulationParamLight:
|
||||
""" Light parameters for regulation"""
|
||||
kp:float = 0.2
|
||||
ki:float = 0.05
|
||||
k_ext:float = 0.05
|
||||
offset_max:float = 1.5
|
||||
stabilization_threshold:float = 0.1
|
||||
accumulated_error_threshold:float = 10
|
||||
"""Light parameters for regulation"""
|
||||
|
||||
kp: float = 0.2
|
||||
ki: float = 0.05
|
||||
k_ext: float = 0.05
|
||||
offset_max: float = 1.5
|
||||
stabilization_threshold: float = 0.1
|
||||
accumulated_error_threshold: float = 10
|
||||
|
||||
|
||||
class RegulationParamMedium:
|
||||
""" Light parameters for regulation"""
|
||||
kp:float = 0.3
|
||||
ki:float = 0.05
|
||||
k_ext:float = 0.1
|
||||
offset_max:float = 2
|
||||
stabilization_threshold:float = 0.1
|
||||
accumulated_error_threshold:float = 20
|
||||
"""Light parameters for regulation"""
|
||||
|
||||
kp: float = 0.3
|
||||
ki: float = 0.05
|
||||
k_ext: float = 0.1
|
||||
offset_max: float = 2
|
||||
stabilization_threshold: float = 0.1
|
||||
accumulated_error_threshold: float = 20
|
||||
|
||||
|
||||
class RegulationParamStrong:
|
||||
""" Medium parameters for regulation"""
|
||||
kp:float = 0.4
|
||||
ki:float = 0.08
|
||||
k_ext:float = 0.1
|
||||
offset_max:float = 3
|
||||
stabilization_threshold:float = 0.1
|
||||
accumulated_error_threshold:float = 25
|
||||
"""Strong parameters for regulation
|
||||
A set of parameters which doesn't take into account the external temp
|
||||
and concentrate to internal temp error + accumulated error.
|
||||
This should work for cold external conditions which else generates
|
||||
high external_offset"""
|
||||
|
||||
kp: float = 0.4
|
||||
ki: float = 0.08
|
||||
k_ext: float = 0.0
|
||||
offset_max: float = 5
|
||||
stabilization_threshold: float = 0.1
|
||||
accumulated_error_threshold: float = 50
|
||||
|
||||
|
||||
# Not used now
|
||||
class RegulationParamVeryStrong:
|
||||
""" Strong parameters for regulation"""
|
||||
kp:float = 0.6
|
||||
ki:float = 0.1
|
||||
k_ext:float = 0.2
|
||||
offset_max:float = 4
|
||||
stabilization_threshold:float = 0.1
|
||||
accumulated_error_threshold:float = 30
|
||||
"""Strong parameters for regulation"""
|
||||
|
||||
kp: float = 0.6
|
||||
ki: float = 0.1
|
||||
k_ext: float = 0.2
|
||||
offset_max: float = 4
|
||||
stabilization_threshold: float = 0.1
|
||||
accumulated_error_threshold: float = 30
|
||||
|
||||
|
||||
class EventType(Enum):
|
||||
"""The event type that can be sent"""
|
||||
@@ -282,8 +470,19 @@ class EventType(Enum):
|
||||
POWER_EVENT: str = "versatile_thermostat_power_event"
|
||||
TEMPERATURE_EVENT: str = "versatile_thermostat_temperature_event"
|
||||
HVAC_MODE_EVENT: str = "versatile_thermostat_hvac_mode_event"
|
||||
CENTRAL_BOILER_EVENT: str = "versatile_thermostat_central_boiler_event"
|
||||
PRESET_EVENT: str = "versatile_thermostat_preset_event"
|
||||
WINDOW_AUTO_EVENT: str = "versatile_thermostat_window_auto_event"
|
||||
AUTO_START_STOP_EVENT: str = "versatile_thermostat_auto_start_stop_event"
|
||||
|
||||
|
||||
def send_vtherm_event(hass, event_type: EventType, entity, data: dict):
|
||||
"""Send an event"""
|
||||
_LOGGER.info("%s - Sending event %s with data: %s", entity, event_type, data)
|
||||
data["entity_id"] = entity.entity_id
|
||||
data["name"] = entity.name
|
||||
data["state_attributes"] = entity.state_attributes
|
||||
hass.bus.fire(event_type.value, data)
|
||||
|
||||
|
||||
class UnknownEntity(HomeAssistantError):
|
||||
@@ -293,8 +492,22 @@ class UnknownEntity(HomeAssistantError):
|
||||
class WindowOpenDetectionMethod(HomeAssistantError):
|
||||
"""Error to indicate there is an error in the window open detection method given."""
|
||||
|
||||
class overrides: # pylint: disable=invalid-name
|
||||
""" An annotation to inform overrides """
|
||||
|
||||
class NoCentralConfig(HomeAssistantError):
|
||||
"""Error to indicate that we try to use a central configuration but no VTherm of type CENTRAL CONFIGURATION has been found"""
|
||||
|
||||
|
||||
class ServiceConfigurationError(HomeAssistantError):
|
||||
"""Error in the service configuration to control the central boiler"""
|
||||
|
||||
|
||||
class ConfigurationNotCompleteError(HomeAssistantError):
|
||||
"""Error the configuration is not complete"""
|
||||
|
||||
|
||||
class overrides: # pylint: disable=invalid-name
|
||||
"""An annotation to inform overrides"""
|
||||
|
||||
def __init__(self, func):
|
||||
self.func = func
|
||||
|
||||
|
||||
92
custom_components/versatile_thermostat/ema.py
Normal file
@@ -0,0 +1,92 @@
|
||||
# pylint: disable=line-too-long
|
||||
"""The Estimated Mobile Average calculation used for temperature slope
|
||||
and maybe some others feature"""
|
||||
|
||||
import logging
|
||||
import math
|
||||
from datetime import datetime, tzinfo
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
MIN_TIME_DECAY_SEC = 0
|
||||
|
||||
# MAX_ALPHA:
|
||||
# As for the EMA calculation of irregular time series, I've seen that it might be useful to
|
||||
# have an upper limit for alpha in case the last measurement was too long ago.
|
||||
# For example when using a half life of 10 minutes a measurement that is 60 minutes ago
|
||||
# (if there's nothing inbetween) would contribute to the smoothed value with 1,5%,
|
||||
# giving the current measurement 98,5% relevance. It could be wise to limit the alpha to e.g. 4x the half life (=0.9375).
|
||||
|
||||
|
||||
class ExponentialMovingAverage:
|
||||
"""A class that will do the Estimated Mobile Average calculation"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
vterm_name: str,
|
||||
halflife: float,
|
||||
timezone: tzinfo,
|
||||
precision: int = 3,
|
||||
max_alpha: float = 0.5,
|
||||
):
|
||||
"""The halflife is the duration in secondes of a normal cycle"""
|
||||
self._halflife: float = halflife
|
||||
self._timezone = timezone
|
||||
self._current_ema: float = None
|
||||
self._last_timestamp: datetime = datetime.now(self._timezone)
|
||||
self._name = vterm_name
|
||||
self._precision = precision
|
||||
self._max_alpha = max_alpha
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"EMA-{self._name}"
|
||||
|
||||
def calculate_ema(self, measurement: float, timestamp: datetime) -> float | None:
|
||||
"""Calculate the new EMA from a new measurement measured at timestamp
|
||||
Return the EMA or None if all parameters are not initialized now
|
||||
"""
|
||||
|
||||
if measurement is None or timestamp is None:
|
||||
_LOGGER.warning(
|
||||
"%s - Cannot calculate EMA: measurement and timestamp are mandatory. This message can be normal at startup but should not persist",
|
||||
self,
|
||||
)
|
||||
return measurement
|
||||
|
||||
if self._current_ema is None:
|
||||
_LOGGER.debug(
|
||||
"%s - First init of the EMA",
|
||||
self,
|
||||
)
|
||||
self._current_ema = measurement
|
||||
self._last_timestamp = timestamp
|
||||
return self._current_ema
|
||||
|
||||
time_decay = (timestamp - self._last_timestamp).total_seconds()
|
||||
if time_decay < MIN_TIME_DECAY_SEC:
|
||||
_LOGGER.debug(
|
||||
"%s - time_decay %s is too small (< %s). Forget the measurement",
|
||||
self,
|
||||
time_decay,
|
||||
MIN_TIME_DECAY_SEC,
|
||||
)
|
||||
return self._current_ema
|
||||
|
||||
alpha = 1 - math.exp(math.log(0.5) * time_decay / self._halflife)
|
||||
# capping alpha to avoid gap if last measurement was long time ago
|
||||
alpha = min(alpha, self._max_alpha)
|
||||
new_ema = alpha * measurement + (1 - alpha) * self._current_ema
|
||||
|
||||
self._last_timestamp = timestamp
|
||||
self._current_ema = new_ema
|
||||
_LOGGER.debug(
|
||||
"%s - timestamp=%s alpha=%.2f measurement=%.2f current_ema=%.2f new_ema=%.2f",
|
||||
self,
|
||||
timestamp,
|
||||
alpha,
|
||||
measurement,
|
||||
self._current_ema,
|
||||
new_ema,
|
||||
)
|
||||
|
||||
return round(self._current_ema, self._precision)
|
||||
18
custom_components/versatile_thermostat/icons.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"entity": {
|
||||
"climate": {
|
||||
"versatile_thermostat": {
|
||||
"state_attributes": {
|
||||
"preset_mode": {
|
||||
"state": {
|
||||
"shedding": "mdi:power-plug-off",
|
||||
"safety": "mdi:shield-alert",
|
||||
"none": "mdi:knob",
|
||||
"frost": "mdi:snowflake"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
138
custom_components/versatile_thermostat/keep_alive.py
Normal file
@@ -0,0 +1,138 @@
|
||||
"""Building blocks for the heater switch keep-alive feature.
|
||||
|
||||
The heater switch keep-alive feature consists of regularly refreshing the state
|
||||
of directly controlled switches at a configurable interval (regularly turning the
|
||||
switch 'on' or 'off' again even if it is already turned 'on' or 'off'), just like
|
||||
the keep_alive setting of Home Assistant's Generic Thermostat integration:
|
||||
https://www.home-assistant.io/integrations/generic_thermostat/
|
||||
"""
|
||||
|
||||
import logging
|
||||
from collections.abc import Awaitable, Callable
|
||||
from datetime import timedelta, datetime
|
||||
from time import monotonic
|
||||
|
||||
from homeassistant.core import HomeAssistant, CALLBACK_TYPE
|
||||
from homeassistant.helpers.event import async_track_time_interval
|
||||
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class BackoffTimer:
|
||||
"""Exponential backoff timer with a non-blocking polling-style implementation.
|
||||
|
||||
Usage example:
|
||||
timer = BackoffTimer(multiplier=1.5, upper_limit_sec=600)
|
||||
while some_condition:
|
||||
if timer.is_ready():
|
||||
do_something()
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
multiplier=2.0,
|
||||
lower_limit_sec=30,
|
||||
upper_limit_sec=86400,
|
||||
initially_ready=True,
|
||||
):
|
||||
"""Initialize a BackoffTimer instance.
|
||||
|
||||
Args:
|
||||
multiplier (int, optional): Period multiplier applied when is_ready() is True.
|
||||
lower_limit_sec (int, optional): Initial backoff period in seconds.
|
||||
upper_limit_sec (int, optional): Maximum backoff period in seconds.
|
||||
initially_ready (bool, optional): Whether is_ready() should return True the
|
||||
first time it is called, or after a call to reset().
|
||||
"""
|
||||
self._multiplier = multiplier
|
||||
self._lower_limit_sec = lower_limit_sec
|
||||
self._upper_limit_sec = upper_limit_sec
|
||||
self._initially_ready = initially_ready
|
||||
|
||||
self._timestamp = 0
|
||||
self._period_sec = self._lower_limit_sec
|
||||
|
||||
@property
|
||||
def in_progress(self) -> bool:
|
||||
"""Whether the backoff timer is in progress (True after a call to is_ready())."""
|
||||
return bool(self._timestamp)
|
||||
|
||||
def reset(self):
|
||||
"""Reset a BackoffTimer instance."""
|
||||
self._timestamp = 0
|
||||
self._period_sec = self._lower_limit_sec
|
||||
|
||||
def is_ready(self) -> bool:
|
||||
"""Check whether an exponentially increasing period of time has passed.
|
||||
|
||||
Whenever is_ready() returns True, the timer period is multiplied so that
|
||||
it takes longer until is_ready() returns True again.
|
||||
Returns:
|
||||
bool: True if enough time has passed since one of the following events,
|
||||
in relation to an instance of this class:
|
||||
- The last time when this method returned True, if it ever did.
|
||||
- Or else, when this method was first called after a call to reset().
|
||||
- Or else, when this method was first called.
|
||||
False otherwise.
|
||||
"""
|
||||
now = monotonic()
|
||||
if self._timestamp == 0:
|
||||
self._timestamp = now
|
||||
return self._initially_ready
|
||||
elif now - self._timestamp >= self._period_sec:
|
||||
self._timestamp = now
|
||||
self._period_sec = max(
|
||||
self._lower_limit_sec,
|
||||
min(self._upper_limit_sec, self._period_sec * self._multiplier),
|
||||
)
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
class IntervalCaller:
|
||||
"""Repeatedly call a given async action function at a given regular interval.
|
||||
|
||||
Convenience wrapper around Home Assistant's `async_track_time_interval` function.
|
||||
"""
|
||||
|
||||
def __init__(self, hass: HomeAssistant, interval_sec: float) -> None:
|
||||
self._hass = hass
|
||||
self._interval_sec = interval_sec
|
||||
self._remove_handle: CALLBACK_TYPE | None = None
|
||||
self.backoff_timer = BackoffTimer()
|
||||
|
||||
@property
|
||||
def interval_sec(self) -> float:
|
||||
"""Return the calling interval in seconds."""
|
||||
return self._interval_sec
|
||||
|
||||
def cancel(self):
|
||||
"""Cancel the regular calls to the action function."""
|
||||
if self._remove_handle:
|
||||
self._remove_handle()
|
||||
self._remove_handle = None
|
||||
|
||||
def set_async_action(self, action: Callable[[], Awaitable[None]]):
|
||||
"""Set the async action function to be called at regular intervals."""
|
||||
if not self._interval_sec:
|
||||
return
|
||||
self.cancel()
|
||||
|
||||
async def callback(_time: datetime):
|
||||
try:
|
||||
_LOGGER.debug(
|
||||
"Calling keep-alive action '%s' (%ss interval)",
|
||||
action.__name__,
|
||||
self._interval_sec,
|
||||
)
|
||||
await action()
|
||||
except Exception as e: # pylint: disable=broad-exception-caught
|
||||
_LOGGER.error(e)
|
||||
self.cancel()
|
||||
|
||||
self._remove_handle = async_track_time_interval(
|
||||
self._hass, callback, timedelta(seconds=self._interval_sec)
|
||||
)
|
||||
@@ -14,6 +14,6 @@
|
||||
"quality_scale": "silver",
|
||||
"requirements": [],
|
||||
"ssdp": [],
|
||||
"version": "4.0.0",
|
||||
"version": "6.6.0",
|
||||
"zeroconf": []
|
||||
}
|
||||
524
custom_components/versatile_thermostat/number.py
Normal file
@@ -0,0 +1,524 @@
|
||||
# pylint: disable=unused-argument
|
||||
|
||||
""" Implements the VersatileThermostat select component """
|
||||
import logging
|
||||
|
||||
# from homeassistant.const import EVENT_HOMEASSISTANT_START
|
||||
from homeassistant.core import HomeAssistant, CoreState # , callback
|
||||
|
||||
from homeassistant.components.number import (
|
||||
NumberEntity,
|
||||
NumberMode,
|
||||
NumberDeviceClass,
|
||||
DOMAIN as NUMBER_DOMAIN,
|
||||
DEFAULT_MAX_VALUE,
|
||||
DEFAULT_MIN_VALUE,
|
||||
DEFAULT_STEP,
|
||||
)
|
||||
from homeassistant.components.climate import (
|
||||
PRESET_BOOST,
|
||||
PRESET_COMFORT,
|
||||
PRESET_ECO,
|
||||
)
|
||||
|
||||
from homeassistant.helpers.device_registry import DeviceInfo, DeviceEntryType
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.helpers.restore_state import RestoreEntity
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.util import slugify
|
||||
|
||||
from .vtherm_api import VersatileThermostatAPI
|
||||
from .commons import VersatileThermostatBaseEntity
|
||||
|
||||
from .const import (
|
||||
DOMAIN,
|
||||
DEVICE_MANUFACTURER,
|
||||
CONF_NAME,
|
||||
CONF_THERMOSTAT_TYPE,
|
||||
CONF_THERMOSTAT_CENTRAL_CONFIG,
|
||||
CONF_TEMP_MIN,
|
||||
CONF_TEMP_MAX,
|
||||
CONF_STEP_TEMPERATURE,
|
||||
CONF_AC_MODE,
|
||||
PRESET_FROST_PROTECTION,
|
||||
PRESET_ECO_AC,
|
||||
PRESET_COMFORT_AC,
|
||||
PRESET_BOOST_AC,
|
||||
PRESET_AWAY_SUFFIX,
|
||||
PRESET_TEMP_SUFFIX,
|
||||
CONF_PRESETS_VALUES,
|
||||
CONF_PRESETS_WITH_AC_VALUES,
|
||||
CONF_PRESETS_AWAY_VALUES,
|
||||
CONF_PRESETS_AWAY_WITH_AC_VALUES,
|
||||
CONF_USE_PRESETS_CENTRAL_CONFIG,
|
||||
CONF_USE_PRESENCE_CENTRAL_CONFIG,
|
||||
CONF_USE_PRESENCE_FEATURE,
|
||||
CONF_USE_CENTRAL_BOILER_FEATURE,
|
||||
overrides,
|
||||
CONF_USE_MAIN_CENTRAL_CONFIG,
|
||||
)
|
||||
|
||||
PRESET_ICON_MAPPING = {
|
||||
PRESET_FROST_PROTECTION + PRESET_TEMP_SUFFIX: "mdi:snowflake-thermometer",
|
||||
PRESET_ECO + PRESET_TEMP_SUFFIX: "mdi:leaf",
|
||||
PRESET_COMFORT + PRESET_TEMP_SUFFIX: "mdi:sofa",
|
||||
PRESET_BOOST + PRESET_TEMP_SUFFIX: "mdi:rocket-launch",
|
||||
PRESET_ECO_AC + PRESET_TEMP_SUFFIX: "mdi:leaf-circle-outline",
|
||||
PRESET_COMFORT_AC + PRESET_TEMP_SUFFIX: "mdi:sofa-outline",
|
||||
PRESET_BOOST_AC + PRESET_TEMP_SUFFIX: "mdi:rocket-launch-outline",
|
||||
PRESET_FROST_PROTECTION
|
||||
+ PRESET_AWAY_SUFFIX
|
||||
+ PRESET_TEMP_SUFFIX: "mdi:snowflake-thermometer",
|
||||
PRESET_ECO + PRESET_AWAY_SUFFIX + PRESET_TEMP_SUFFIX: "mdi:leaf",
|
||||
PRESET_COMFORT + PRESET_AWAY_SUFFIX + PRESET_TEMP_SUFFIX: "mdi:sofa",
|
||||
PRESET_BOOST + PRESET_AWAY_SUFFIX + PRESET_TEMP_SUFFIX: "mdi:rocket-launch",
|
||||
PRESET_ECO_AC + PRESET_AWAY_SUFFIX + PRESET_TEMP_SUFFIX: "mdi:leaf-circle-outline",
|
||||
PRESET_COMFORT_AC + PRESET_AWAY_SUFFIX + PRESET_TEMP_SUFFIX: "mdi:sofa-outline",
|
||||
PRESET_BOOST_AC
|
||||
+ PRESET_AWAY_SUFFIX
|
||||
+ PRESET_TEMP_SUFFIX: "mdi:rocket-launch-outline",
|
||||
}
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the VersatileThermostat selects with config flow."""
|
||||
_LOGGER.debug(
|
||||
"Calling async_setup_entry entry=%s, data=%s", entry.entry_id, entry.data
|
||||
)
|
||||
|
||||
unique_id = entry.entry_id
|
||||
name = entry.data.get(CONF_NAME)
|
||||
vt_type = entry.data.get(CONF_THERMOSTAT_TYPE)
|
||||
# is_central_boiler = entry.data.get(CONF_USE_CENTRAL_BOILER_FEATURE)
|
||||
|
||||
entities = []
|
||||
|
||||
if vt_type != CONF_THERMOSTAT_CENTRAL_CONFIG:
|
||||
# Creates non central temperature entities
|
||||
if not entry.data.get(CONF_USE_PRESETS_CENTRAL_CONFIG, False):
|
||||
if entry.data.get(CONF_AC_MODE, False):
|
||||
for preset in CONF_PRESETS_WITH_AC_VALUES:
|
||||
_LOGGER.debug(
|
||||
"%s - configuring Number non central, AC, non AWAY for preset %s",
|
||||
name,
|
||||
preset,
|
||||
)
|
||||
entities.append(
|
||||
TemperatureNumber(
|
||||
hass, unique_id, name, preset, True, False, entry.data
|
||||
)
|
||||
)
|
||||
else:
|
||||
for preset in CONF_PRESETS_VALUES:
|
||||
_LOGGER.debug(
|
||||
"%s - configuring Number non central, non AC, non AWAY for preset %s",
|
||||
name,
|
||||
preset,
|
||||
)
|
||||
entities.append(
|
||||
TemperatureNumber(
|
||||
hass, unique_id, name, preset, False, False, entry.data
|
||||
)
|
||||
)
|
||||
|
||||
if entry.data.get(
|
||||
CONF_USE_PRESENCE_FEATURE, False
|
||||
) is True and not entry.data.get(CONF_USE_PRESENCE_CENTRAL_CONFIG, False):
|
||||
if entry.data.get(CONF_AC_MODE, False):
|
||||
for preset in CONF_PRESETS_AWAY_WITH_AC_VALUES:
|
||||
_LOGGER.debug(
|
||||
"%s - configuring Number non central, AC, AWAY for preset %s",
|
||||
name,
|
||||
preset,
|
||||
)
|
||||
entities.append(
|
||||
TemperatureNumber(
|
||||
hass, unique_id, name, preset, True, True, entry.data
|
||||
)
|
||||
)
|
||||
else:
|
||||
for preset in CONF_PRESETS_AWAY_VALUES:
|
||||
_LOGGER.debug(
|
||||
"%s - configuring Number non central, non AC, AWAY for preset %s",
|
||||
name,
|
||||
preset,
|
||||
)
|
||||
entities.append(
|
||||
TemperatureNumber(
|
||||
hass, unique_id, name, preset, False, True, entry.data
|
||||
)
|
||||
)
|
||||
|
||||
# For central config only
|
||||
else:
|
||||
if entry.data.get(CONF_USE_CENTRAL_BOILER_FEATURE):
|
||||
entities.append(
|
||||
ActivateBoilerThresholdNumber(hass, unique_id, name, entry.data)
|
||||
)
|
||||
for preset in CONF_PRESETS_WITH_AC_VALUES:
|
||||
_LOGGER.debug(
|
||||
"%s - configuring Number central, AC, non AWAY for preset %s",
|
||||
name,
|
||||
preset,
|
||||
)
|
||||
entities.append(
|
||||
CentralConfigTemperatureNumber(
|
||||
hass, unique_id, name, preset, True, False, entry.data
|
||||
)
|
||||
)
|
||||
|
||||
for preset in CONF_PRESETS_AWAY_WITH_AC_VALUES:
|
||||
_LOGGER.debug(
|
||||
"%s - configuring Number central, AC, AWAY for preset %s", name, preset
|
||||
)
|
||||
entities.append(
|
||||
CentralConfigTemperatureNumber(
|
||||
hass, unique_id, name, preset, True, True, entry.data
|
||||
)
|
||||
)
|
||||
|
||||
if len(entities) > 0:
|
||||
async_add_entities(entities, True)
|
||||
|
||||
|
||||
class ActivateBoilerThresholdNumber(
|
||||
NumberEntity, RestoreEntity
|
||||
): # pylint: disable=abstract-method
|
||||
"""Representation of the threshold of the number of VTherm
|
||||
which should be active to activate the boiler"""
|
||||
|
||||
def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None:
|
||||
"""Initialize the energy sensor"""
|
||||
self._hass = hass
|
||||
self._config_id = unique_id
|
||||
self._device_name = entry_infos.get(CONF_NAME)
|
||||
self._attr_name = "Boiler Activation threshold"
|
||||
self._attr_unique_id = "boiler_activation_threshold"
|
||||
self._attr_value = self._attr_native_value = 1 # default value
|
||||
self._attr_native_min_value = 1
|
||||
self._attr_native_max_value = 9
|
||||
self._attr_step = 1 # default value
|
||||
self._attr_mode = NumberMode.AUTO
|
||||
|
||||
@property
|
||||
def icon(self) -> str | None:
|
||||
if isinstance(self._attr_native_value, int):
|
||||
val = int(self._attr_native_value)
|
||||
return f"mdi:numeric-{val}-box-outline"
|
||||
else:
|
||||
return "mdi:numeric-0-box-outline"
|
||||
|
||||
@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,
|
||||
)
|
||||
|
||||
@overrides
|
||||
async def async_added_to_hass(self) -> None:
|
||||
await super().async_added_to_hass()
|
||||
|
||||
api: VersatileThermostatAPI = VersatileThermostatAPI.get_vtherm_api(self._hass)
|
||||
api.register_central_boiler_activation_number_threshold(self)
|
||||
|
||||
old_state: CoreState = await self.async_get_last_state()
|
||||
_LOGGER.debug(
|
||||
"%s - Calling async_added_to_hass old_state is %s", self, old_state
|
||||
)
|
||||
if old_state is not None:
|
||||
self._attr_value = self._attr_native_value = int(float(old_state.state))
|
||||
|
||||
@overrides
|
||||
def set_native_value(self, value: float) -> None:
|
||||
"""Change the value"""
|
||||
int_value = int(value)
|
||||
old_value = int(self._attr_native_value)
|
||||
|
||||
if int_value == old_value:
|
||||
return
|
||||
|
||||
self._attr_value = self._attr_native_value = int_value
|
||||
|
||||
def __str__(self):
|
||||
return f"VersatileThermostat-{self.name}"
|
||||
|
||||
|
||||
class CentralConfigTemperatureNumber(
|
||||
NumberEntity, RestoreEntity
|
||||
): # pylint: disable=abstract-method
|
||||
"""Representation of one temperature number"""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
unique_id,
|
||||
name,
|
||||
preset_name,
|
||||
is_ac,
|
||||
is_away,
|
||||
entry_infos,
|
||||
) -> None:
|
||||
"""Initialize the temperature with entry_infos if available. Else
|
||||
the restoration will do the trick."""
|
||||
|
||||
self._config_id = unique_id
|
||||
self._device_name = name
|
||||
# self._attr_name = name
|
||||
|
||||
self._attr_translation_key = preset_name
|
||||
self.entity_id = f"{NUMBER_DOMAIN}.{slugify(name)}_preset_{preset_name}"
|
||||
self._attr_unique_id = f"central_configuration_preset_{preset_name}"
|
||||
self._attr_device_class = NumberDeviceClass.TEMPERATURE
|
||||
self._attr_native_unit_of_measurement = hass.config.units.temperature_unit
|
||||
|
||||
self._attr_native_step = entry_infos.get(CONF_STEP_TEMPERATURE, 0.5)
|
||||
self._attr_native_min_value = entry_infos.get(CONF_TEMP_MIN)
|
||||
self._attr_native_max_value = entry_infos.get(CONF_TEMP_MAX)
|
||||
|
||||
# Initialize the values if included into the entry_infos. This will do
|
||||
# the temperature migration. Else the temperature will be restored from
|
||||
# previous value
|
||||
# TODO remove this after the next major release and just keep the init min/max
|
||||
temp = None
|
||||
if (temp := entry_infos.get(preset_name, None)) is not None:
|
||||
self._attr_value = self._attr_native_value = temp
|
||||
else:
|
||||
if entry_infos.get(CONF_AC_MODE) is True:
|
||||
self._attr_native_value = self._attr_native_max_value
|
||||
else:
|
||||
self._attr_native_value = self._attr_native_min_value
|
||||
|
||||
self._attr_mode = NumberMode.BOX
|
||||
self._preset_name = preset_name
|
||||
self._is_away = is_away
|
||||
self._is_ac = is_ac
|
||||
|
||||
@property
|
||||
def icon(self) -> str | None:
|
||||
return PRESET_ICON_MAPPING[self._preset_name]
|
||||
|
||||
@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,
|
||||
)
|
||||
|
||||
@overrides
|
||||
async def async_added_to_hass(self) -> None:
|
||||
await super().async_added_to_hass()
|
||||
|
||||
# register the temp entity for this device and preset
|
||||
api: VersatileThermostatAPI = VersatileThermostatAPI.get_vtherm_api(self.hass)
|
||||
api.register_temperature_number(self._config_id, self._preset_name, self)
|
||||
|
||||
# Restore value from previous one if exists
|
||||
old_state: CoreState = await self.async_get_last_state()
|
||||
_LOGGER.debug(
|
||||
"%s - Calling async_added_to_hass old_state is %s", self, old_state
|
||||
)
|
||||
try:
|
||||
if old_state is not None and ((value := float(old_state.state)) > 0):
|
||||
self._attr_value = self._attr_native_value = value
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
@overrides
|
||||
async def async_set_native_value(self, value: float) -> None:
|
||||
"""The value have change from the Number Entity in UI"""
|
||||
float_value = float(value)
|
||||
old_value = (
|
||||
None if self._attr_native_value is None else float(self._attr_native_value)
|
||||
)
|
||||
|
||||
if float_value == old_value:
|
||||
return
|
||||
|
||||
self._attr_value = self._attr_native_value = float_value
|
||||
|
||||
# persist the value
|
||||
self.async_write_ha_state()
|
||||
|
||||
# We have to reload all VTherm for which uses the central configuration
|
||||
api: VersatileThermostatAPI = VersatileThermostatAPI.get_vtherm_api(self.hass)
|
||||
# Update the VTherms which have temperature in central config
|
||||
self.hass.create_task(api.init_vtherm_preset_with_central())
|
||||
|
||||
def __str__(self):
|
||||
return f"VersatileThermostat-{self.name}"
|
||||
|
||||
@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
|
||||
|
||||
|
||||
class TemperatureNumber( # pylint: disable=abstract-method
|
||||
VersatileThermostatBaseEntity, NumberEntity, RestoreEntity
|
||||
):
|
||||
"""Representation of one temperature number"""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
unique_id,
|
||||
name,
|
||||
preset_name,
|
||||
is_ac,
|
||||
is_away,
|
||||
entry_infos,
|
||||
) -> None:
|
||||
"""Initialize the temperature with entry_infos if available. Else
|
||||
the restoration will do the trick."""
|
||||
super().__init__(hass, unique_id, name)
|
||||
|
||||
self._attr_translation_key = preset_name
|
||||
self.entity_id = f"{NUMBER_DOMAIN}.{slugify(name)}_preset_{preset_name}"
|
||||
|
||||
self._attr_unique_id = f"{self._device_name}_preset_{preset_name}"
|
||||
self._attr_device_class = NumberDeviceClass.TEMPERATURE
|
||||
self._attr_native_unit_of_measurement = hass.config.units.temperature_unit
|
||||
|
||||
self._has_central_main_attributes = entry_infos.get(
|
||||
CONF_USE_MAIN_CENTRAL_CONFIG, False
|
||||
)
|
||||
|
||||
self.init_min_max_step(entry_infos)
|
||||
|
||||
# Initialize the values if included into the entry_infos. This will do
|
||||
# the temperature migration.
|
||||
temp = None
|
||||
if (temp := entry_infos.get(preset_name, None)) is not None:
|
||||
self._attr_value = self._attr_native_value = temp
|
||||
else:
|
||||
if entry_infos.get(CONF_AC_MODE) is True:
|
||||
self._attr_native_value = self._attr_native_max_value
|
||||
else:
|
||||
self._attr_native_value = self._attr_native_min_value
|
||||
|
||||
self._attr_mode = NumberMode.BOX
|
||||
self._preset_name = preset_name
|
||||
self._canonical_preset_name = preset_name.replace(
|
||||
PRESET_TEMP_SUFFIX, ""
|
||||
).replace(PRESET_AWAY_SUFFIX, "")
|
||||
self._is_away = is_away
|
||||
self._is_ac = is_ac
|
||||
|
||||
@property
|
||||
def icon(self) -> str | None:
|
||||
return PRESET_ICON_MAPPING[self._preset_name]
|
||||
|
||||
@overrides
|
||||
async def async_added_to_hass(self) -> None:
|
||||
await super().async_added_to_hass()
|
||||
|
||||
# register the temp entity for this device and preset
|
||||
api: VersatileThermostatAPI = VersatileThermostatAPI.get_vtherm_api(self.hass)
|
||||
api.register_temperature_number(self._config_id, self._preset_name, self)
|
||||
|
||||
old_state: CoreState = await self.async_get_last_state()
|
||||
_LOGGER.debug(
|
||||
"%s - Calling async_added_to_hass old_state is %s", self, old_state
|
||||
)
|
||||
try:
|
||||
if old_state is not None and ((value := float(old_state.state)) > 0):
|
||||
self._attr_value = self._attr_native_value = value
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
@overrides
|
||||
def my_climate_is_initialized(self):
|
||||
"""Called when the associated climate is initialized"""
|
||||
self._attr_native_step = self.my_climate.target_temperature_step
|
||||
self._attr_native_min_value = self.my_climate.min_temp
|
||||
self._attr_native_max_value = self.my_climate.max_temp
|
||||
return
|
||||
|
||||
@overrides
|
||||
async def async_set_native_value(self, value: float) -> None:
|
||||
"""Change the value"""
|
||||
|
||||
if self.my_climate is None:
|
||||
_LOGGER.warning(
|
||||
"%s - cannot change temperature because VTherm is not initialized", self
|
||||
)
|
||||
return
|
||||
|
||||
float_value = float(value)
|
||||
old_value = (
|
||||
None if self._attr_native_value is None else float(self._attr_native_value)
|
||||
)
|
||||
|
||||
if float_value == old_value:
|
||||
return
|
||||
|
||||
self._attr_value = self._attr_native_value = float_value
|
||||
self.async_write_ha_state()
|
||||
|
||||
# Update the VTherm temp
|
||||
self.hass.create_task(
|
||||
self.my_climate.service_set_preset_temperature(
|
||||
self._canonical_preset_name,
|
||||
self._attr_native_value if not self._is_away else None,
|
||||
self._attr_native_value if self._is_away else None,
|
||||
)
|
||||
)
|
||||
|
||||
# We set the min, max and step from central config if relevant because it is possible
|
||||
# that central config was not loaded at startup
|
||||
self.init_min_max_step()
|
||||
|
||||
def __str__(self):
|
||||
return f"VersatileThermostat-{self.name}"
|
||||
|
||||
@property
|
||||
def native_unit_of_measurement(self) -> str | None:
|
||||
"""The unit of measurement"""
|
||||
if not self.my_climate:
|
||||
return self.hass.config.units.temperature_unit
|
||||
return self.my_climate.temperature_unit
|
||||
|
||||
def init_min_max_step(self, entry_infos=None):
|
||||
"""Initialize min, max and step value from config or from central config"""
|
||||
if self._has_central_main_attributes:
|
||||
vthermapi: VersatileThermostatAPI = VersatileThermostatAPI.get_vtherm_api()
|
||||
central_config = vthermapi.find_central_configuration()
|
||||
if central_config:
|
||||
self._attr_native_step = central_config.data.get(CONF_STEP_TEMPERATURE)
|
||||
self._attr_native_min_value = central_config.data.get(CONF_TEMP_MIN)
|
||||
self._attr_native_max_value = central_config.data.get(CONF_TEMP_MAX)
|
||||
|
||||
return
|
||||
|
||||
if entry_infos:
|
||||
self._attr_native_step = entry_infos.get(
|
||||
CONF_STEP_TEMPERATURE, DEFAULT_STEP
|
||||
)
|
||||
self._attr_native_min_value = entry_infos.get(
|
||||
CONF_TEMP_MIN, DEFAULT_MIN_VALUE
|
||||
)
|
||||
self._attr_native_max_value = entry_infos.get(
|
||||
CONF_TEMP_MAX, DEFAULT_MAX_VALUE
|
||||
)
|
||||
@@ -1,3 +1,4 @@
|
||||
# pylint: disable=line-too-long
|
||||
""" This file implements the Open Window by temperature algorithm
|
||||
This algo works the following way:
|
||||
- each time a new temperature is measured
|
||||
@@ -12,8 +13,14 @@ from datetime import datetime
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
# To filter bad values
|
||||
MIN_DELTA_T_SEC = 30 # two temp mesure should be > 10 sec
|
||||
MAX_SLOPE_VALUE = 2 # slope cannot be > 2 or < -2 -> else this is an aberrant point
|
||||
MIN_DELTA_T_SEC = 0 # two temp mesure should be > 0 sec
|
||||
MAX_SLOPE_VALUE = (
|
||||
120 # slope cannot be > 2°/min or < -2°/min -> else this is an aberrant point
|
||||
)
|
||||
|
||||
MAX_DURATION_MIN = 30 # a fake data point is added in the cycle if last measurement was older than 30 min
|
||||
|
||||
MIN_NB_POINT = 4 # do not calculate slope until we have enough point
|
||||
|
||||
|
||||
class WindowOpenDetectionAlgorithm:
|
||||
@@ -24,6 +31,7 @@ class WindowOpenDetectionAlgorithm:
|
||||
_last_slope: float
|
||||
_last_datetime: datetime
|
||||
_last_temperature: float
|
||||
_nb_point: int
|
||||
|
||||
def __init__(self, alert_threshold, end_alert_threshold) -> None:
|
||||
"""Initalize a new algorithm with the both threshold"""
|
||||
@@ -31,9 +39,24 @@ class WindowOpenDetectionAlgorithm:
|
||||
self._end_alert_threshold = end_alert_threshold
|
||||
self._last_slope = None
|
||||
self._last_datetime = None
|
||||
self._nb_point = 0
|
||||
|
||||
def check_age_last_measurement(self, temperature, datetime_now) -> float:
|
||||
""" " Check if last measurement is old and add
|
||||
a fake measurement point if this is the case
|
||||
"""
|
||||
if self._last_datetime is None:
|
||||
return self.add_temp_measurement(temperature, datetime_now)
|
||||
|
||||
delta_t_sec = float((datetime_now - self._last_datetime).total_seconds()) / 60.0
|
||||
if delta_t_sec >= MAX_DURATION_MIN:
|
||||
return self.add_temp_measurement(temperature, datetime_now, False)
|
||||
else:
|
||||
# do nothing
|
||||
return self._last_slope
|
||||
|
||||
def add_temp_measurement(
|
||||
self, temperature: float, datetime_measure: datetime
|
||||
self, temperature: float, datetime_measure: datetime, store_date: bool = True
|
||||
) -> float:
|
||||
"""Add a new temperature measurement
|
||||
returns the last slope
|
||||
@@ -42,6 +65,7 @@ class WindowOpenDetectionAlgorithm:
|
||||
_LOGGER.debug("First initialisation")
|
||||
self._last_datetime = datetime_measure
|
||||
self._last_temperature = temperature
|
||||
self._nb_point = self._nb_point + 1
|
||||
return None
|
||||
|
||||
_LOGGER.debug(
|
||||
@@ -61,8 +85,10 @@ class WindowOpenDetectionAlgorithm:
|
||||
)
|
||||
return lspe
|
||||
|
||||
delta_t_hour = delta_t / 60.0
|
||||
|
||||
delta_temp = float(temperature - self._last_temperature)
|
||||
new_slope = delta_temp / delta_t
|
||||
new_slope = delta_temp / delta_t_hour
|
||||
if new_slope > MAX_SLOPE_VALUE or new_slope < -MAX_SLOPE_VALUE:
|
||||
_LOGGER.debug(
|
||||
"New_slope is abs(%.2f) > %.2f which should be not possible. We don't consider this value",
|
||||
@@ -72,21 +98,28 @@ class WindowOpenDetectionAlgorithm:
|
||||
return lspe
|
||||
|
||||
if self._last_slope is None:
|
||||
self._last_slope = new_slope
|
||||
self._last_slope = round(new_slope, 2)
|
||||
else:
|
||||
self._last_slope = (0.5 * self._last_slope) + (0.5 * new_slope)
|
||||
self._last_slope = round((0.2 * self._last_slope) + (0.8 * new_slope), 2)
|
||||
|
||||
# if we are in cycle check and so adding a fake datapoint, we don't store the event datetime
|
||||
# so that, when we will receive a real temperature point we will not calculate a wrong slope
|
||||
if store_date:
|
||||
self._last_datetime = datetime_measure
|
||||
|
||||
self._last_datetime = datetime_measure
|
||||
self._last_temperature = temperature
|
||||
|
||||
self._nb_point = self._nb_point + 1
|
||||
_LOGGER.debug(
|
||||
"delta_t=%.3f delta_temp=%.3f new_slope=%.3f last_slope=%s slope=%.3f",
|
||||
"delta_t=%.3f delta_temp=%.3f new_slope=%.3f last_slope=%s slope=%.3f nb_point=%s",
|
||||
delta_t,
|
||||
delta_temp,
|
||||
new_slope,
|
||||
lspe,
|
||||
self._last_slope,
|
||||
self._nb_point,
|
||||
)
|
||||
|
||||
return self._last_slope
|
||||
|
||||
def is_window_open_detected(self) -> bool:
|
||||
@@ -94,22 +127,20 @@ class WindowOpenDetectionAlgorithm:
|
||||
if self._alert_threshold is None:
|
||||
return False
|
||||
|
||||
return (
|
||||
self._last_slope < -self._alert_threshold
|
||||
if self._last_slope is not None
|
||||
else False
|
||||
)
|
||||
if self._nb_point < MIN_NB_POINT or self._last_slope is None:
|
||||
return False
|
||||
|
||||
return self._last_slope < -self._alert_threshold
|
||||
|
||||
def is_window_close_detected(self) -> bool:
|
||||
"""True if the last calculated slope is above (cause negative) the _end_alert_threshold"""
|
||||
if self._end_alert_threshold is None:
|
||||
return False
|
||||
|
||||
return (
|
||||
self._last_slope >= self._end_alert_threshold
|
||||
if self._last_slope is not None
|
||||
else False
|
||||
)
|
||||
if self._nb_point < MIN_NB_POINT or self._last_slope is None:
|
||||
return False
|
||||
|
||||
return self._last_slope >= self._end_alert_threshold
|
||||
|
||||
@property
|
||||
def last_slope(self) -> float:
|
||||
|
||||
@@ -40,17 +40,23 @@ class PITemperatureRegulator:
|
||||
"""Reset the accumulated error"""
|
||||
self.accumulated_error = 0
|
||||
|
||||
def set_accumulated_error(self, accumulated_error):
|
||||
"""Allow to persist and restore the accumulated_error"""
|
||||
self.accumulated_error = accumulated_error
|
||||
|
||||
def set_target_temp(self, target_temp):
|
||||
"""Set the new target_temp"""
|
||||
self.target_temp = target_temp
|
||||
# Do not reset the accumulated error
|
||||
# self.accumulated_error = 0
|
||||
# Discussion #191. After a target change we should reset the accumulated error which is certainly wrong now.
|
||||
# Discussion #384. Finally don't reset the accumulated error but smoothly reset it if the sign is inversed
|
||||
# if self.accumulated_error < 0:
|
||||
# self.accumulated_error = 0
|
||||
|
||||
def calculate_regulated_temperature(
|
||||
self, internal_temp: float, external_temp: float
|
||||
self, room_temp: float, external_temp: float
|
||||
): # pylint: disable=unused-argument
|
||||
"""Calculate a new target_temp given some temperature"""
|
||||
if internal_temp is None:
|
||||
if room_temp is None:
|
||||
_LOGGER.warning(
|
||||
"Temporarily skipping the self-regulation algorithm while the configured sensor for room temperature is unavailable"
|
||||
)
|
||||
@@ -62,9 +68,14 @@ class PITemperatureRegulator:
|
||||
return self.target_temp
|
||||
|
||||
# Calculate the error factor (P)
|
||||
error = self.target_temp - internal_temp
|
||||
error = self.target_temp - room_temp
|
||||
|
||||
# Calculate the sum of error (I)
|
||||
# Discussion #384. Finally don't reset the accumulated error but smoothly reset it if the sign is inversed
|
||||
# If the error have change its sign, reset smoothly the accumulated error
|
||||
if error * self.accumulated_error < 0:
|
||||
self.accumulated_error = self.accumulated_error / 2.0
|
||||
|
||||
self.accumulated_error += error
|
||||
|
||||
# Capping of the error
|
||||
@@ -77,18 +88,12 @@ class PITemperatureRegulator:
|
||||
offset = self.kp * error + self.ki * self.accumulated_error
|
||||
|
||||
# Calculate the exterior offset
|
||||
# For Maia tests - use the internal_temp vs external_temp and not target_temp - external_temp
|
||||
offset_ext = self.k_ext * (internal_temp - external_temp)
|
||||
offset_ext = self.k_ext * (room_temp - external_temp)
|
||||
|
||||
# Capping of offset_ext
|
||||
# Capping of offset
|
||||
total_offset = offset + offset_ext
|
||||
total_offset = min(self.offset_max, max(-self.offset_max, total_offset))
|
||||
|
||||
# If temperature is near the target_temp, reset the accumulated_error
|
||||
if abs(error) < self.stabilization_threshold:
|
||||
_LOGGER.debug("Stabilisation")
|
||||
self.accumulated_error = 0
|
||||
|
||||
result = round(self.target_temp + total_offset, 1)
|
||||
|
||||
_LOGGER.debug(
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
""" The TPI calculation module """
|
||||
# pylint: disable='line-too-long'
|
||||
import logging
|
||||
|
||||
from homeassistant.components.climate import HVACMode
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
PROPORTIONAL_FUNCTION_ATAN = "atan"
|
||||
@@ -12,6 +15,11 @@ PROPORTIONAL_MIN_DURATION_SEC = 10
|
||||
FUNCTION_TYPE = [PROPORTIONAL_FUNCTION_ATAN, PROPORTIONAL_FUNCTION_LINEAR]
|
||||
|
||||
|
||||
def is_number(value):
|
||||
"""check if value is a number"""
|
||||
return isinstance(value, (int, float))
|
||||
|
||||
|
||||
class PropAlgorithm:
|
||||
"""This class aims to do all calculation of the Proportional alogorithm"""
|
||||
|
||||
@@ -22,16 +30,43 @@ class PropAlgorithm:
|
||||
tpi_coef_ext,
|
||||
cycle_min: int,
|
||||
minimal_activation_delay: int,
|
||||
vtherm_entity_id: str = None,
|
||||
) -> None:
|
||||
"""Initialisation of the Proportional Algorithm"""
|
||||
_LOGGER.debug(
|
||||
"Creation new PropAlgorithm function_type: %s, tpi_coef_int: %s, tpi_coef_ext: %s, cycle_min:%d, minimal_activation_delay:%d",
|
||||
"%s - Creation new PropAlgorithm function_type: %s, tpi_coef_int: %s, tpi_coef_ext: %s, cycle_min:%d, minimal_activation_delay:%d", # pylint: disable=line-too-long
|
||||
vtherm_entity_id,
|
||||
function_type,
|
||||
tpi_coef_int,
|
||||
tpi_coef_ext,
|
||||
cycle_min,
|
||||
minimal_activation_delay,
|
||||
)
|
||||
|
||||
# Issue 506 - check parameters
|
||||
if (
|
||||
vtherm_entity_id is None
|
||||
or not is_number(tpi_coef_int)
|
||||
or not is_number(tpi_coef_ext)
|
||||
or not is_number(cycle_min)
|
||||
or not is_number(minimal_activation_delay)
|
||||
or function_type != PROPORTIONAL_FUNCTION_TPI
|
||||
):
|
||||
_LOGGER.error(
|
||||
"%s - configuration is wrong. function_type=%s, entity_id is %s, tpi_coef_int is %s, tpi_coef_ext is %s, cycle_min is %s, minimal_activation_delay is %s",
|
||||
vtherm_entity_id,
|
||||
function_type,
|
||||
vtherm_entity_id,
|
||||
tpi_coef_int,
|
||||
tpi_coef_ext,
|
||||
cycle_min,
|
||||
minimal_activation_delay,
|
||||
)
|
||||
raise TypeError(
|
||||
"TPI parameters are not set correctly. VTherm will not work as expected. Please reconfigure it correctly. See previous log for values"
|
||||
)
|
||||
|
||||
self._vtherm_entity_id = vtherm_entity_id
|
||||
self._function = function_type
|
||||
self._tpi_coef_int = tpi_coef_int
|
||||
self._tpi_coef_ext = tpi_coef_ext
|
||||
@@ -46,24 +81,28 @@ class PropAlgorithm:
|
||||
|
||||
def calculate(
|
||||
self,
|
||||
target_temp: float,
|
||||
current_temp: float,
|
||||
ext_current_temp: float,
|
||||
cooling=False,
|
||||
target_temp: float | None,
|
||||
current_temp: float | None,
|
||||
ext_current_temp: float | None,
|
||||
hvac_mode: HVACMode,
|
||||
):
|
||||
"""Do the calculation of the duration"""
|
||||
if target_temp is None or current_temp is None:
|
||||
_LOGGER.warning(
|
||||
"Proportional algorithm: calculation is not possible cause target_temp or current_temp is null. Heating/cooling will be disabled" # pylint: disable=line-too-long
|
||||
log = _LOGGER.debug if hvac_mode == HVACMode.OFF else _LOGGER.warning
|
||||
log(
|
||||
"%s - Proportional algorithm: calculation is not possible cause target_temp (%s) or current_temp (%s) is null. Heating/cooling will be disabled. This could be normal at startup", # pylint: disable=line-too-long
|
||||
self._vtherm_entity_id,
|
||||
target_temp,
|
||||
current_temp,
|
||||
)
|
||||
self._calculated_on_percent = 0
|
||||
else:
|
||||
if cooling:
|
||||
if hvac_mode == HVACMode.COOL:
|
||||
delta_temp = current_temp - target_temp
|
||||
delta_ext_temp = (
|
||||
ext_current_temp
|
||||
ext_current_temp - target_temp
|
||||
if ext_current_temp is not None
|
||||
else 0 - target_temp
|
||||
else 0
|
||||
)
|
||||
else:
|
||||
delta_temp = target_temp - current_temp
|
||||
@@ -80,7 +119,8 @@ class PropAlgorithm:
|
||||
)
|
||||
else:
|
||||
_LOGGER.warning(
|
||||
"Proportional algorithm: unknown %s function. Heating will be disabled",
|
||||
"%s - Proportional algorithm: unknown %s function. Heating will be disabled",
|
||||
self._vtherm_entity_id,
|
||||
self._function,
|
||||
)
|
||||
self._calculated_on_percent = 0
|
||||
@@ -88,7 +128,8 @@ class PropAlgorithm:
|
||||
self._calculate_internal()
|
||||
|
||||
_LOGGER.debug(
|
||||
"heating percent calculated for current_temp %.1f, ext_current_temp %.1f and target_temp %.1f is %.2f, on_time is %d (sec), off_time is %d (sec)", # pylint: disable=line-too-long
|
||||
"%s - heating percent calculated for current_temp %.1f, ext_current_temp %.1f and target_temp %.1f is %.2f, on_time is %d (sec), off_time is %d (sec)", # pylint: disable=line-too-long
|
||||
self._vtherm_entity_id,
|
||||
current_temp if current_temp else -9999.0,
|
||||
ext_current_temp if ext_current_temp else -9999.0,
|
||||
target_temp if target_temp else -9999.0,
|
||||
@@ -107,11 +148,12 @@ class PropAlgorithm:
|
||||
self._calculated_on_percent = 0
|
||||
|
||||
if self._security:
|
||||
_LOGGER.debug(
|
||||
"Security is On using the default_on_percent %f",
|
||||
self._default_on_percent,
|
||||
)
|
||||
self._on_percent = self._default_on_percent
|
||||
_LOGGER.info(
|
||||
"%s - Security is On using the default_on_percent %f",
|
||||
self._vtherm_entity_id,
|
||||
self._on_percent,
|
||||
)
|
||||
else:
|
||||
_LOGGER.debug(
|
||||
"Security is Off using the calculated_on_percent %f",
|
||||
@@ -125,13 +167,8 @@ class PropAlgorithm:
|
||||
if self._on_time_sec < self._minimal_activation_delay:
|
||||
if self._on_time_sec > 0:
|
||||
_LOGGER.info(
|
||||
"No heating period due to heating period too small (%f < %f)",
|
||||
self._on_time_sec,
|
||||
self._minimal_activation_delay,
|
||||
)
|
||||
else:
|
||||
_LOGGER.debug(
|
||||
"No heating period due to heating period too small (%f < %f)",
|
||||
"%s - No heating period due to heating period too small (%f < %f)",
|
||||
self._vtherm_entity_id,
|
||||
self._on_time_sec,
|
||||
self._minimal_activation_delay,
|
||||
)
|
||||
@@ -140,27 +177,33 @@ class PropAlgorithm:
|
||||
self._off_time_sec = self._cycle_min * 60 - self._on_time_sec
|
||||
|
||||
def set_security(self, default_on_percent: float):
|
||||
"""Set a default value for on_percent (used for security mode)"""
|
||||
"""Set a default value for on_percent (used for safety mode)"""
|
||||
_LOGGER.info(
|
||||
"%s - Proportional Algo - set security to ON", self._vtherm_entity_id
|
||||
)
|
||||
self._security = True
|
||||
self._default_on_percent = default_on_percent
|
||||
self._calculate_internal()
|
||||
|
||||
def unset_security(self):
|
||||
"""Unset the security mode"""
|
||||
"""Unset the safety mode"""
|
||||
_LOGGER.info(
|
||||
"%s - Proportional Algo - set security to OFF", self._vtherm_entity_id
|
||||
)
|
||||
self._security = False
|
||||
self._calculate_internal()
|
||||
|
||||
@property
|
||||
def on_percent(self) -> float:
|
||||
"""Returns the percentage the heater must be ON
|
||||
In security mode this value is overriden with the _default_on_percent
|
||||
In safety mode this value is overriden with the _default_on_percent
|
||||
(1 means the heater will be always on, 0 never on)""" # pylint: disable=line-too-long
|
||||
return round(self._on_percent, 2)
|
||||
|
||||
@property
|
||||
def calculated_on_percent(self) -> float:
|
||||
"""Returns the calculated percentage the heater must be ON
|
||||
Calculated means NOT overriden even in security mode
|
||||
Calculated means NOT overriden even in safety mode
|
||||
(1 means the heater will be always on, 0 never on)""" # pylint: disable=line-too-long
|
||||
return round(self._calculated_on_percent, 2)
|
||||
|
||||
|
||||
138
custom_components/versatile_thermostat/select.py
Normal file
@@ -0,0 +1,138 @@
|
||||
# pylint: disable=unused-argument
|
||||
|
||||
""" Implements the VersatileThermostat select component """
|
||||
import logging
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from homeassistant.components.select import SelectEntity
|
||||
from homeassistant.helpers.device_registry import DeviceInfo, DeviceEntryType
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.helpers.restore_state import RestoreEntity
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from custom_components.versatile_thermostat.base_thermostat import (
|
||||
ConfigData,
|
||||
)
|
||||
|
||||
from custom_components.versatile_thermostat.vtherm_api import VersatileThermostatAPI
|
||||
|
||||
from .const import (
|
||||
DOMAIN,
|
||||
DEVICE_MANUFACTURER,
|
||||
CONF_NAME,
|
||||
CONF_THERMOSTAT_TYPE,
|
||||
CONF_THERMOSTAT_CENTRAL_CONFIG,
|
||||
CENTRAL_MODE_AUTO,
|
||||
CENTRAL_MODES,
|
||||
overrides,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the VersatileThermostat selects with config flow."""
|
||||
_LOGGER.debug(
|
||||
"Calling async_setup_entry entry=%s, data=%s", entry.entry_id, entry.data
|
||||
)
|
||||
|
||||
unique_id = entry.entry_id
|
||||
name = entry.data.get(CONF_NAME)
|
||||
vt_type = entry.data.get(CONF_THERMOSTAT_TYPE)
|
||||
|
||||
if vt_type != CONF_THERMOSTAT_CENTRAL_CONFIG:
|
||||
return
|
||||
|
||||
entities = [
|
||||
CentralModeSelect(hass, unique_id, name, entry.data),
|
||||
]
|
||||
|
||||
async_add_entities(entities, True)
|
||||
|
||||
|
||||
class CentralModeSelect(SelectEntity, RestoreEntity):
|
||||
"""Representation of the central mode choice"""
|
||||
|
||||
def __init__(
|
||||
self, hass: HomeAssistant, unique_id: str, name: str, entry_infos: ConfigData
|
||||
):
|
||||
"""Initialize the energy sensor"""
|
||||
self._config_id = unique_id
|
||||
self._device_name = entry_infos.get(CONF_NAME)
|
||||
self._attr_name = "Central Mode"
|
||||
self._attr_unique_id = "central_mode"
|
||||
self._attr_options = CENTRAL_MODES
|
||||
self._attr_current_option = CENTRAL_MODE_AUTO
|
||||
|
||||
@property
|
||||
def icon(self) -> str:
|
||||
return "mdi:form-select"
|
||||
|
||||
@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,
|
||||
)
|
||||
|
||||
@overrides
|
||||
async def async_added_to_hass(self) -> None:
|
||||
await super().async_added_to_hass()
|
||||
|
||||
old_state = await self.async_get_last_state()
|
||||
_LOGGER.debug(
|
||||
"%s - Calling async_added_to_hass old_state is %s", self, old_state
|
||||
)
|
||||
if old_state is not None:
|
||||
self._attr_current_option = old_state.state
|
||||
|
||||
api: VersatileThermostatAPI = VersatileThermostatAPI.get_vtherm_api(self.hass)
|
||||
api.register_central_mode_select(self)
|
||||
|
||||
# @callback
|
||||
# async def _async_startup_internal(*_):
|
||||
# _LOGGER.debug("%s - Calling async_startup_internal", self)
|
||||
# await self.notify_central_mode_change()
|
||||
#
|
||||
# if self.hass.state == CoreState.running:
|
||||
# await _async_startup_internal()
|
||||
# else:
|
||||
# self.hass.bus.async_listen_once(
|
||||
# EVENT_HOMEASSISTANT_START, _async_startup_internal
|
||||
# )
|
||||
|
||||
@overrides
|
||||
async def async_select_option(self, option: str) -> None:
|
||||
"""Change the selected option."""
|
||||
old_option = self._attr_current_option
|
||||
|
||||
if option == old_option:
|
||||
return
|
||||
|
||||
if option in CENTRAL_MODES:
|
||||
self._attr_current_option = option
|
||||
await self.notify_central_mode_change(old_central_mode=old_option)
|
||||
|
||||
@overrides
|
||||
def select_option(self, option: str) -> None:
|
||||
"""Change the selected option"""
|
||||
# Update the VTherms which have temperature in central config
|
||||
self.hass.create_task(self.async_select_option(option))
|
||||
|
||||
async def notify_central_mode_change(self, old_central_mode: str | None = None):
|
||||
"""Notify all VTherm that the central_mode have change"""
|
||||
api: VersatileThermostatAPI = VersatileThermostatAPI.get_vtherm_api(self.hass)
|
||||
# Update all VTherm states
|
||||
await api.notify_central_mode_change(old_central_mode)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"VersatileThermostat-{self.name}"
|
||||
@@ -3,9 +3,15 @@
|
||||
import logging
|
||||
import math
|
||||
|
||||
from homeassistant.core import HomeAssistant, callback, Event
|
||||
from homeassistant.core import HomeAssistant, callback, Event, CoreState
|
||||
|
||||
from homeassistant.const import UnitOfTime, UnitOfPower, UnitOfEnergy, PERCENTAGE
|
||||
from homeassistant.const import (
|
||||
UnitOfTime,
|
||||
UnitOfPower,
|
||||
UnitOfEnergy,
|
||||
PERCENTAGE,
|
||||
EVENT_HOMEASSISTANT_START,
|
||||
)
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
SensorEntity,
|
||||
@@ -16,9 +22,24 @@ from homeassistant.components.sensor import (
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.device_registry import DeviceInfo, DeviceEntryType
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
from homeassistant.helpers.event import async_track_state_change_event
|
||||
|
||||
from homeassistant.components.climate import (
|
||||
ClimateEntity,
|
||||
DOMAIN as CLIMATE_DOMAIN,
|
||||
HVACAction,
|
||||
HVACMode,
|
||||
)
|
||||
|
||||
|
||||
from .base_thermostat import BaseThermostat
|
||||
from .vtherm_api import VersatileThermostatAPI
|
||||
from .commons import VersatileThermostatBaseEntity
|
||||
from .const import (
|
||||
DOMAIN,
|
||||
DEVICE_MANUFACTURER,
|
||||
CONF_NAME,
|
||||
CONF_DEVICE_POWER,
|
||||
CONF_PROP_FUNCTION,
|
||||
@@ -27,6 +48,9 @@ from .const import (
|
||||
CONF_THERMOSTAT_VALVE,
|
||||
CONF_THERMOSTAT_CLIMATE,
|
||||
CONF_THERMOSTAT_TYPE,
|
||||
CONF_THERMOSTAT_CENTRAL_CONFIG,
|
||||
CONF_USE_CENTRAL_BOILER_FEATURE,
|
||||
overrides,
|
||||
)
|
||||
|
||||
THRESHOLD_WATT_KILO = 100
|
||||
@@ -46,32 +70,45 @@ async def async_setup_entry(
|
||||
|
||||
unique_id = entry.entry_id
|
||||
name = entry.data.get(CONF_NAME)
|
||||
vt_type = entry.data.get(CONF_THERMOSTAT_TYPE)
|
||||
|
||||
entities = [
|
||||
LastTemperatureSensor(hass, unique_id, name, entry.data),
|
||||
LastExtTemperatureSensor(hass, unique_id, name, entry.data),
|
||||
TemperatureSlopeSensor(hass, unique_id, name, entry.data),
|
||||
]
|
||||
if entry.data.get(CONF_DEVICE_POWER):
|
||||
entities.append(EnergySensor(hass, unique_id, name, entry.data))
|
||||
if entry.data.get(CONF_THERMOSTAT_TYPE) in [
|
||||
CONF_THERMOSTAT_SWITCH,
|
||||
CONF_THERMOSTAT_VALVE,
|
||||
]:
|
||||
entities.append(MeanPowerSensor(hass, unique_id, name, entry.data))
|
||||
entities = None
|
||||
|
||||
if entry.data.get(CONF_PROP_FUNCTION) == PROPORTIONAL_FUNCTION_TPI:
|
||||
entities.append(OnPercentSensor(hass, unique_id, name, entry.data))
|
||||
entities.append(OnTimeSensor(hass, unique_id, name, entry.data))
|
||||
entities.append(OffTimeSensor(hass, unique_id, name, entry.data))
|
||||
if vt_type == CONF_THERMOSTAT_CENTRAL_CONFIG:
|
||||
if entry.data.get(CONF_USE_CENTRAL_BOILER_FEATURE):
|
||||
entities = [
|
||||
NbActiveDeviceForBoilerSensor(hass, unique_id, name, entry.data)
|
||||
]
|
||||
else:
|
||||
entities = [
|
||||
LastTemperatureSensor(hass, unique_id, name, entry.data),
|
||||
LastExtTemperatureSensor(hass, unique_id, name, entry.data),
|
||||
TemperatureSlopeSensor(hass, unique_id, name, entry.data),
|
||||
EMATemperatureSensor(hass, unique_id, name, entry.data),
|
||||
]
|
||||
if entry.data.get(CONF_DEVICE_POWER):
|
||||
entities.append(EnergySensor(hass, unique_id, name, entry.data))
|
||||
if entry.data.get(CONF_THERMOSTAT_TYPE) in [
|
||||
CONF_THERMOSTAT_SWITCH,
|
||||
CONF_THERMOSTAT_VALVE,
|
||||
]:
|
||||
entities.append(MeanPowerSensor(hass, unique_id, name, entry.data))
|
||||
|
||||
if entry.data.get(CONF_THERMOSTAT_TYPE) == CONF_THERMOSTAT_VALVE:
|
||||
entities.append(ValveOpenPercentSensor(hass, unique_id, name, entry.data))
|
||||
if entry.data.get(CONF_PROP_FUNCTION) == PROPORTIONAL_FUNCTION_TPI:
|
||||
entities.append(OnPercentSensor(hass, unique_id, name, entry.data))
|
||||
entities.append(OnTimeSensor(hass, unique_id, name, entry.data))
|
||||
entities.append(OffTimeSensor(hass, unique_id, name, entry.data))
|
||||
|
||||
if entry.data.get(CONF_THERMOSTAT_TYPE) == CONF_THERMOSTAT_CLIMATE:
|
||||
entities.append(RegulatedTemperatureSensor(hass, unique_id, name, entry.data))
|
||||
if entry.data.get(CONF_THERMOSTAT_TYPE) == CONF_THERMOSTAT_VALVE:
|
||||
entities.append(ValveOpenPercentSensor(hass, unique_id, name, entry.data))
|
||||
|
||||
async_add_entities(entities, True)
|
||||
if entry.data.get(CONF_THERMOSTAT_TYPE) == CONF_THERMOSTAT_CLIMATE:
|
||||
entities.append(
|
||||
RegulatedTemperatureSensor(hass, unique_id, name, entry.data)
|
||||
)
|
||||
|
||||
if entities:
|
||||
async_add_entities(entities, True)
|
||||
|
||||
|
||||
class EnergySensor(VersatileThermostatBaseEntity, SensorEntity):
|
||||
@@ -88,15 +125,15 @@ class EnergySensor(VersatileThermostatBaseEntity, SensorEntity):
|
||||
"""Called when my climate have change"""
|
||||
_LOGGER.debug("%s - climate state change", self._attr_unique_id)
|
||||
|
||||
if math.isnan(self.my_climate.total_energy) or math.isinf(
|
||||
self.my_climate.total_energy
|
||||
):
|
||||
energy = self.my_climate.total_energy
|
||||
if energy is None:
|
||||
return
|
||||
|
||||
if math.isnan(energy) or math.isinf(energy):
|
||||
raise ValueError(f"Sensor has illegal state {self.my_climate.total_energy}")
|
||||
|
||||
old_state = self._attr_native_value
|
||||
self._attr_native_value = round(
|
||||
self.my_climate.total_energy, self.suggested_display_precision
|
||||
)
|
||||
self._attr_native_value = round(energy, self.suggested_display_precision)
|
||||
if old_state != self._attr_native_value:
|
||||
self.async_write_ha_state()
|
||||
return
|
||||
@@ -247,7 +284,7 @@ class ValveOpenPercentSensor(VersatileThermostatBaseEntity, SensorEntity):
|
||||
def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None:
|
||||
"""Initialize the energy sensor"""
|
||||
super().__init__(hass, unique_id, entry_infos.get(CONF_NAME))
|
||||
self._attr_name = "Vave open percent"
|
||||
self._attr_name = "Valve open percent"
|
||||
self._attr_unique_id = f"{self._device_name}_valve_open_percent"
|
||||
|
||||
@callback
|
||||
@@ -395,7 +432,7 @@ class LastTemperatureSensor(VersatileThermostatBaseEntity, SensorEntity):
|
||||
_LOGGER.debug("%s - climate state change", self._attr_unique_id)
|
||||
|
||||
old_state = self._attr_native_value
|
||||
self._attr_native_value = self.my_climate.last_temperature_mesure
|
||||
self._attr_native_value = self.my_climate.last_temperature_measure
|
||||
if old_state != self._attr_native_value:
|
||||
self.async_write_ha_state()
|
||||
return
|
||||
@@ -424,7 +461,7 @@ class LastExtTemperatureSensor(VersatileThermostatBaseEntity, SensorEntity):
|
||||
_LOGGER.debug("%s - climate state change", self._attr_unique_id)
|
||||
|
||||
old_state = self._attr_native_value
|
||||
self._attr_native_value = self.my_climate.last_ext_temperature_mesure
|
||||
self._attr_native_value = self.my_climate.last_ext_temperature_measure
|
||||
if old_state != self._attr_native_value:
|
||||
self.async_write_ha_state()
|
||||
return
|
||||
@@ -483,7 +520,7 @@ class TemperatureSlopeSensor(VersatileThermostatBaseEntity, SensorEntity):
|
||||
if not self.my_climate:
|
||||
return None
|
||||
|
||||
return self.my_climate.temperature_unit + "/min"
|
||||
return self.my_climate.temperature_unit + "/hour"
|
||||
|
||||
@property
|
||||
def suggested_display_precision(self) -> int | None:
|
||||
@@ -505,17 +542,15 @@ class RegulatedTemperatureSensor(VersatileThermostatBaseEntity, SensorEntity):
|
||||
"""Called when my climate have change"""
|
||||
_LOGGER.debug("%s - climate state change", self._attr_unique_id)
|
||||
|
||||
if math.isnan(self.my_climate.regulated_target_temp) or math.isinf(
|
||||
self.my_climate.regulated_target_temp
|
||||
):
|
||||
raise ValueError(
|
||||
f"Sensor has illegal state {self.my_climate.regulated_target_temp}"
|
||||
)
|
||||
new_temp = self.my_climate.regulated_target_temp
|
||||
if new_temp is None:
|
||||
return
|
||||
|
||||
if math.isnan(new_temp) or math.isinf(new_temp):
|
||||
raise ValueError(f"Sensor has illegal state {new_temp}")
|
||||
|
||||
old_state = self._attr_native_value
|
||||
self._attr_native_value = round(
|
||||
self.my_climate.regulated_target_temp, self.suggested_display_precision
|
||||
)
|
||||
self._attr_native_value = round(new_temp, self.suggested_display_precision)
|
||||
if old_state != self._attr_native_value:
|
||||
self.async_write_ha_state()
|
||||
return
|
||||
@@ -535,10 +570,174 @@ class RegulatedTemperatureSensor(VersatileThermostatBaseEntity, SensorEntity):
|
||||
@property
|
||||
def native_unit_of_measurement(self) -> str | None:
|
||||
if not self.my_climate:
|
||||
return UnitOfTemperature.CELSIUS
|
||||
return self.hass.config.units.temperature_unit
|
||||
return self.my_climate.temperature_unit
|
||||
|
||||
@property
|
||||
def suggested_display_precision(self) -> int | None:
|
||||
"""Return the suggested number of decimal digits for display."""
|
||||
return 1
|
||||
|
||||
|
||||
class EMATemperatureSensor(VersatileThermostatBaseEntity, SensorEntity):
|
||||
"""Representation of a Exponential Moving Average temp"""
|
||||
|
||||
def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None:
|
||||
"""Initialize the regulated temperature sensor"""
|
||||
super().__init__(hass, unique_id, entry_infos.get(CONF_NAME))
|
||||
self._attr_name = "EMA temperature"
|
||||
self._attr_unique_id = f"{self._device_name}_ema_temperature"
|
||||
|
||||
@callback
|
||||
async def async_my_climate_changed(self, event: Event = None):
|
||||
"""Called when my climate have change"""
|
||||
_LOGGER.debug("%s - climate state change", self._attr_unique_id)
|
||||
|
||||
new_ema = self.my_climate.ema_temperature
|
||||
if new_ema is None:
|
||||
return
|
||||
|
||||
if math.isnan(new_ema) or math.isinf(new_ema):
|
||||
raise ValueError(f"Sensor has illegal state {new_ema}")
|
||||
|
||||
old_state = self._attr_native_value
|
||||
self._attr_native_value = new_ema
|
||||
if old_state != self._attr_native_value:
|
||||
self.async_write_ha_state()
|
||||
return
|
||||
|
||||
@property
|
||||
def icon(self) -> str | None:
|
||||
return "mdi:thermometer-lines"
|
||||
|
||||
@property
|
||||
def device_class(self) -> SensorDeviceClass | None:
|
||||
return SensorDeviceClass.TEMPERATURE
|
||||
|
||||
@property
|
||||
def state_class(self) -> SensorStateClass | None:
|
||||
return SensorStateClass.MEASUREMENT
|
||||
|
||||
@property
|
||||
def native_unit_of_measurement(self) -> str | None:
|
||||
if not self.my_climate:
|
||||
return self.hass.config.units.temperature_unit
|
||||
return self.my_climate.temperature_unit
|
||||
|
||||
@property
|
||||
def suggested_display_precision(self) -> int | None:
|
||||
"""Return the suggested number of decimal digits for display."""
|
||||
return 2
|
||||
|
||||
|
||||
class NbActiveDeviceForBoilerSensor(SensorEntity):
|
||||
"""Representation of the threshold of the number of VTherm
|
||||
which should be active to activate the boiler"""
|
||||
|
||||
def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None:
|
||||
"""Initialize the energy sensor"""
|
||||
self._hass = hass
|
||||
self._config_id = unique_id
|
||||
self._device_name = entry_infos.get(CONF_NAME)
|
||||
self._attr_name = "Nb device active for boiler"
|
||||
self._attr_unique_id = "nb_device_active_boiler"
|
||||
self._attr_value = self._attr_native_value = None # default value
|
||||
self._entities = []
|
||||
|
||||
@property
|
||||
def icon(self) -> str | None:
|
||||
return "mdi:heat-wave"
|
||||
|
||||
@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,
|
||||
)
|
||||
|
||||
@property
|
||||
def state_class(self) -> SensorStateClass | None:
|
||||
return SensorStateClass.MEASUREMENT
|
||||
|
||||
@property
|
||||
def suggested_display_precision(self) -> int | None:
|
||||
"""Return the suggested number of decimal digits for display."""
|
||||
return 0
|
||||
|
||||
@overrides
|
||||
async def async_added_to_hass(self) -> None:
|
||||
await super().async_added_to_hass()
|
||||
|
||||
api: VersatileThermostatAPI = VersatileThermostatAPI.get_vtherm_api(self._hass)
|
||||
api.register_nb_device_active_boiler(self)
|
||||
|
||||
@callback
|
||||
async def _async_startup_internal(*_):
|
||||
_LOGGER.debug("%s - Calling async_startup_internal", self)
|
||||
await self.listen_vtherms_entities()
|
||||
|
||||
if self.hass.state == CoreState.running:
|
||||
await _async_startup_internal()
|
||||
else:
|
||||
self.hass.bus.async_listen_once(
|
||||
EVENT_HOMEASSISTANT_START, _async_startup_internal
|
||||
)
|
||||
|
||||
async def listen_vtherms_entities(self):
|
||||
"""Initialize the listening of state change of VTherms"""
|
||||
|
||||
# Listen to all VTherm state change
|
||||
self._entities = []
|
||||
underlying_entities_id = []
|
||||
|
||||
component: EntityComponent[ClimateEntity] = self.hass.data[CLIMATE_DOMAIN]
|
||||
for entity in component.entities:
|
||||
if isinstance(entity, BaseThermostat) and entity.is_used_by_central_boiler:
|
||||
self._entities.append(entity)
|
||||
for under in entity.underlying_entities:
|
||||
underlying_entities_id.append(under.entity_id)
|
||||
if len(underlying_entities_id) > 0:
|
||||
# Arme l'écoute de la première entité
|
||||
listener_cancel = async_track_state_change_event(
|
||||
self._hass,
|
||||
underlying_entities_id,
|
||||
self.calculate_nb_active_devices,
|
||||
)
|
||||
_LOGGER.info(
|
||||
"%s - the underlyings that could controls 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)
|
||||
|
||||
await self.calculate_nb_active_devices(None)
|
||||
|
||||
async def calculate_nb_active_devices(self, _):
|
||||
"""Calculate the number of active VTherm that have an
|
||||
influence on central boiler"""
|
||||
|
||||
_LOGGER.debug("%s - calculating the number of active VTherm", self)
|
||||
nb_active = 0
|
||||
for entity in self._entities:
|
||||
_LOGGER.debug(
|
||||
"Examining the hvac_action of %s",
|
||||
entity.name,
|
||||
)
|
||||
if (
|
||||
entity.hvac_mode in [HVACMode.HEAT, HVACMode.AUTO]
|
||||
and entity.hvac_action == HVACAction.HEATING
|
||||
):
|
||||
for under in entity.underlying_entities:
|
||||
nb_active += 1 if under.is_device_active else 0
|
||||
|
||||
self._attr_native_value = nb_active
|
||||
self.async_write_ha_state()
|
||||
|
||||
def __str__(self):
|
||||
return f"VersatileThermostat-{self.name}"
|
||||
|
||||
@@ -43,6 +43,7 @@ set_preset_temperature:
|
||||
- "eco"
|
||||
- "comfort"
|
||||
- "boost"
|
||||
- "frost"
|
||||
- "eco_ac"
|
||||
- "comfort_ac"
|
||||
- "boost_ac"
|
||||
@@ -76,8 +77,8 @@ set_preset_temperature:
|
||||
mode: slider
|
||||
|
||||
set_security:
|
||||
name: Set security
|
||||
description: Change the security parameters
|
||||
name: Set safety
|
||||
description: Change the safety parameters
|
||||
target:
|
||||
entity:
|
||||
integration: versatile_thermostat
|
||||
@@ -96,7 +97,7 @@ set_security:
|
||||
mode: box
|
||||
min_on_percent:
|
||||
name: Minimal on_percent
|
||||
description: Minimal heating percent value for security preset activation
|
||||
description: Minimal heating percent value for safety preset activation
|
||||
required: false
|
||||
advanced: false
|
||||
example: "0.5"
|
||||
@@ -109,8 +110,8 @@ set_security:
|
||||
unit_of_measurement: "%"
|
||||
mode: slider
|
||||
default_on_percent:
|
||||
name: on_percent used in security mode
|
||||
description: The default heating percent value in security preset
|
||||
name: on_percent used in safety mode
|
||||
description: The default heating percent value in safety preset
|
||||
required: false
|
||||
advanced: false
|
||||
example: "0.1"
|
||||
@@ -160,3 +161,26 @@ set_auto_regulation_mode:
|
||||
- "Medium"
|
||||
- "Strong"
|
||||
- "Slow"
|
||||
- "Expert"
|
||||
|
||||
set_auto_fan_mode:
|
||||
name: Set Auto Fan mode
|
||||
description: Change the mode of auto-fan (only for VTherm over climate)
|
||||
target:
|
||||
entity:
|
||||
integration: versatile_thermostat
|
||||
fields:
|
||||
auto_fan_mode:
|
||||
name: Auto fan mode
|
||||
description: Possible values
|
||||
required: true
|
||||
advanced: false
|
||||
default: true
|
||||
selector:
|
||||
select:
|
||||
options:
|
||||
- "None"
|
||||
- "Low"
|
||||
- "Medium"
|
||||
- "High"
|
||||
- "Turbo"
|
||||
|
||||
@@ -4,166 +4,212 @@
|
||||
"flow_title": "Versatile Thermostat configuration",
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "Type of Versatile Thermostat",
|
||||
"data": {
|
||||
"thermostat_type": "Thermostat type"
|
||||
},
|
||||
"data_description": {
|
||||
"thermostat_type": "Only one central configuration type is possible"
|
||||
}
|
||||
},
|
||||
"menu": {
|
||||
"title": "Menu",
|
||||
"description": "Configure your thermostat. You will be able to finalize the configuration when all required parameters are entered.",
|
||||
"menu_options": {
|
||||
"main": "Main attributes",
|
||||
"central_boiler": "Central boiler",
|
||||
"type": "Underlyings",
|
||||
"tpi": "TPI parameters",
|
||||
"features": "Features",
|
||||
"presets": "Presets",
|
||||
"window": "Window detection",
|
||||
"motion": "Motion detection",
|
||||
"power": "Power management",
|
||||
"presence": "Presence detection",
|
||||
"advanced": "Advanced parameters",
|
||||
"auto_start_stop": "Auto start and stop",
|
||||
"finalize": "All done",
|
||||
"configuration_not_complete": "Configuration not complete"
|
||||
}
|
||||
},
|
||||
"main": {
|
||||
"title": "Add new Versatile Thermostat",
|
||||
"description": "Main mandatory attributes",
|
||||
"data": {
|
||||
"name": "Name",
|
||||
"thermostat_type": "Thermostat type",
|
||||
"temperature_sensor_entity_id": "Temperature sensor entity id",
|
||||
"external_temperature_sensor_entity_id": "External temperature sensor entity id",
|
||||
"temperature_sensor_entity_id": "Room temperature",
|
||||
"last_seen_temperature_sensor_entity_id": "Last seen room temperature datetime",
|
||||
"external_temperature_sensor_entity_id": "Outdoor temperature sensor entity id",
|
||||
"cycle_min": "Cycle duration (minutes)",
|
||||
"temp_min": "Minimal temperature allowed",
|
||||
"temp_max": "Maximal temperature allowed",
|
||||
"temp_min": "Minimum temperature allowed",
|
||||
"temp_max": "Maximum temperature allowed",
|
||||
"step_temperature": "Temperature step",
|
||||
"device_power": "Device power",
|
||||
"use_central_mode": "Enable the control by central entity (requires central config). Check to enable the control of the VTherm with the select central_mode entities.",
|
||||
"use_main_central_config": "Use additional central main configuration. Check to use the central main configuration (outdoor temperature, min, max, step, ...).",
|
||||
"used_by_controls_central_boiler": "Used by central boiler. Check if this VTherm should have control on the central boiler"
|
||||
},
|
||||
"data_description": {
|
||||
"temperature_sensor_entity_id": "Room temperature sensor entity id",
|
||||
"last_seen_temperature_sensor_entity_id": "Last seen room temperature sensor entity id. Should be datetime sensor",
|
||||
"external_temperature_sensor_entity_id": "Outdoor temperature sensor entity id. Not used if central configuration is selected"
|
||||
}
|
||||
},
|
||||
"features": {
|
||||
"title": "Features",
|
||||
"description": "Thermostat features",
|
||||
"data": {
|
||||
"use_window_feature": "Use window detection",
|
||||
"use_motion_feature": "Use motion detection",
|
||||
"use_power_feature": "Use power management",
|
||||
"use_presence_feature": "Use presence detection"
|
||||
"use_presence_feature": "Use presence detection",
|
||||
"use_central_boiler_feature": "Use a central boiler. Check to add a control to your central boiler. You will have to configure the VTherm which will have a control of the central boiler after seecting this checkbox to take effect. If one VTherm requires heating, the boiler will be turned on. If no VTherm requires heating, the boiler will be turned off. Commands for turning on/off the central boiler are given in the related configuration page",
|
||||
"use_auto_start_stop_feature": "Use the auto start and stop feature"
|
||||
}
|
||||
},
|
||||
"type": {
|
||||
"title": "Linked entities",
|
||||
"description": "Linked entities attributes",
|
||||
"data": {
|
||||
"heater_entity_id": "1rst heater switch",
|
||||
"heater_entity2_id": "2nd heater switch",
|
||||
"heater_entity3_id": "3rd heater switch",
|
||||
"heater_entity4_id": "4th heater switch",
|
||||
"underlying_entity_ids": "The device(s) to be controlled",
|
||||
"heater_keep_alive": "Switch keep-alive interval in seconds",
|
||||
"proportional_function": "Algorithm",
|
||||
"climate_entity_id": "1rst underlying climate",
|
||||
"climate_entity2_id": "2nd underlying climate",
|
||||
"climate_entity3_id": "3rd underlying climate",
|
||||
"climate_entity4_id": "4th underlying climate",
|
||||
"ac_mode": "AC mode",
|
||||
"valve_entity_id": "1rst valve number",
|
||||
"valve_entity2_id": "2nd valve number",
|
||||
"valve_entity3_id": "3rd valve number",
|
||||
"valve_entity4_id": "4th valve number",
|
||||
"auto_regulation_mode": "Self-regulation",
|
||||
"auto_regulation_dtemp": "Regulation threshold",
|
||||
"auto_regulation_periode_min": "Regulation minimal period",
|
||||
"inverse_switch_command": "Inverse switch command"
|
||||
"auto_regulation_periode_min": "Regulation minimum period",
|
||||
"auto_regulation_use_device_temp": "Use internal temperature of the underlying",
|
||||
"inverse_switch_command": "Inverse switch command",
|
||||
"auto_fan_mode": "Auto fan mode"
|
||||
},
|
||||
"data_description": {
|
||||
"heater_entity_id": "Mandatory heater entity id",
|
||||
"heater_entity2_id": "Optional 2nd Heater entity id. Leave empty if not used",
|
||||
"heater_entity3_id": "Optional 3rd Heater entity id. Leave empty if not used",
|
||||
"heater_entity4_id": "Optional 4th Heater entity id. Leave empty if not used",
|
||||
"underlying_entity_ids": "The device(s) to be controlled - 1 is required",
|
||||
"heater_keep_alive": "Optional heater switch state refresh interval. Leave empty if not required.",
|
||||
"proportional_function": "Algorithm to use (TPI is the only one for now)",
|
||||
"climate_entity_id": "Underlying climate entity id",
|
||||
"climate_entity2_id": "2nd underlying climate entity id",
|
||||
"climate_entity3_id": "3rd underlying climate entity id",
|
||||
"climate_entity4_id": "4th underlying climate entity id",
|
||||
"ac_mode": "Use the Air Conditioning (AC) mode",
|
||||
"valve_entity_id": "1rst valve number entity id",
|
||||
"valve_entity2_id": "2nd valve number entity id",
|
||||
"valve_entity3_id": "3rd valve number entity id",
|
||||
"valve_entity4_id": "4th valve number entity id",
|
||||
"auto_regulation_mode": "Auto adjustment of the target temperature",
|
||||
"auto_regulation_dtemp": "The threshold in ° under which the temperature change will not be send",
|
||||
"auto_regulation_dtemp": "The threshold in ° (or % for valve) under which the temperature change will not be sent",
|
||||
"auto_regulation_periode_min": "Duration in minutes between two regulation update",
|
||||
"inverse_switch_command": "For switch with pilot wire and diode you may need to inverse the command"
|
||||
"auto_regulation_use_device_temp": "Use the eventual internal temperature sensor of the underlying to speedup the self-regulation",
|
||||
"inverse_switch_command": "For switch with pilot wire and diode you may need to inverse the command",
|
||||
"auto_fan_mode": "Automatically activate fan when huge heating/cooling is necessary"
|
||||
}
|
||||
},
|
||||
"tpi": {
|
||||
"title": "TPI",
|
||||
"description": "Time Proportional Integral attributes",
|
||||
"data": {
|
||||
"tpi_coef_int": "coef_int",
|
||||
"tpi_coef_ext": "coef_ext",
|
||||
"use_tpi_central_config": "Use central TPI configuration"
|
||||
},
|
||||
"data_description": {
|
||||
"tpi_coef_int": "Coefficient to use for internal temperature delta",
|
||||
"tpi_coef_ext": "Coefficient to use for external temperature delta"
|
||||
"tpi_coef_ext": "Coefficient to use for external temperature delta",
|
||||
"use_tpi_central_config": "Check to use the central TPI configuration. Uncheck to use a specific TPI configuration for this VTherm"
|
||||
}
|
||||
},
|
||||
"presets": {
|
||||
"title": "Presets",
|
||||
"description": "For each presets, give the target temperature (0 to ignore preset)",
|
||||
"description": "Select if the thermostat will use central preset - deselect for the thermostat to have its own presets",
|
||||
"data": {
|
||||
"eco_temp": "Temperature in Eco preset",
|
||||
"comfort_temp": "Temperature in Comfort preset",
|
||||
"boost_temp": "Temperature in Boost preset",
|
||||
"eco_ac_temp": "Temperature in Eco preset for AC mode",
|
||||
"comfort_ac_temp": "Temperature in Comfort preset for AC mode",
|
||||
"boost_ac_temp": "Temperature in Boost preset for AC mode"
|
||||
"use_presets_central_config": "Use central presets configuration"
|
||||
}
|
||||
},
|
||||
"window": {
|
||||
"title": "Window management",
|
||||
"description": "Open window management.\nLeave corresponding entity_id empty if not used\nYou can also configure automatic window open detection based on temperature decrease",
|
||||
"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_auto_open_threshold": "Temperature decrease threshold for automatic window open detection (in °/min)",
|
||||
"window_auto_close_threshold": "Temperature increase threshold for end of automatic detection (in °/min)",
|
||||
"window_auto_max_duration": "Maximum duration of automatic window open detection (in min)"
|
||||
"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)",
|
||||
"use_window_central_config": "Use central window configuration",
|
||||
"window_action": "Action"
|
||||
},
|
||||
"data_description": {
|
||||
"window_sensor_entity_id": "Leave empty if no window sensor should be use",
|
||||
"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 0.05 and 0.1. Leave empty if automatic window open detection is not use",
|
||||
"window_auto_close_threshold": "Recommended value: 0. Leave empty if automatic window open detection is not use",
|
||||
"window_auto_max_duration": "Recommended value: 60 (one hour). Leave empty if automatic window open detection is not use"
|
||||
"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",
|
||||
"use_window_central_config": "Select to use the central window configuration. Deselect to use a specific window configuration for this VTherm",
|
||||
"window_action": "Action to perform if window is deteted as open"
|
||||
}
|
||||
},
|
||||
"motion": {
|
||||
"title": "Motion management",
|
||||
"description": "Motion sensor management. Preset can switch automatically depending of a motion detection\nLeave corresponding entity_id empty if not used.\nmotion_preset and no_motion_preset should be set to the corresponding preset name",
|
||||
"description": "Motion sensor management. Preset can switch automatically depending on motion detection\nmotion_preset and no_motion_preset should be set to the corresponding preset name",
|
||||
"data": {
|
||||
"motion_sensor_entity_id": "Motion sensor entity id",
|
||||
"motion_delay": "Activation delay",
|
||||
"motion_off_delay": "Deactivation delay",
|
||||
"motion_preset": "Motion preset",
|
||||
"no_motion_preset": "No motion preset"
|
||||
"no_motion_preset": "No motion preset",
|
||||
"use_motion_central_config": "Use central motion configuration"
|
||||
},
|
||||
"data_description": {
|
||||
"motion_sensor_entity_id": "The entity id of the motion sensor",
|
||||
"motion_delay": "Motion activation activation delay (seconds)",
|
||||
"motion_delay": "Motion activation delay (seconds)",
|
||||
"motion_off_delay": "Motion deactivation delay (seconds)",
|
||||
"motion_preset": "Preset to use when motion is detected",
|
||||
"no_motion_preset": "Preset to use when no motion is detected"
|
||||
"no_motion_preset": "Preset to use when no motion is detected",
|
||||
"use_motion_central_config": "Check to use the central motion configuration. Uncheck to use a specific motion configuration for this VTherm"
|
||||
}
|
||||
},
|
||||
"power": {
|
||||
"title": "Power management",
|
||||
"description": "Power management attributes.\nGives the power and max power sensor of your home.\nThen specify the power consumption of the heater when on.\nAll sensors and device power should have the same unit (kW or W).\nLeave corresponding entity_id empty if not used.",
|
||||
"description": "Power management attributes.\nGives the power and max power sensor of your home.\nSpecify the power consumption of the heater when on.\nAll sensors and device power should use the same unit (kW or W).",
|
||||
"data": {
|
||||
"power_sensor_entity_id": "Power",
|
||||
"max_power_sensor_entity_id": "Max power",
|
||||
"power_temp": "Shedding temperature",
|
||||
"use_power_central_config": "Use central power configuration"
|
||||
},
|
||||
"data_description": {
|
||||
"power_sensor_entity_id": "Power sensor entity id",
|
||||
"max_power_sensor_entity_id": "Max power sensor entity id",
|
||||
"power_temp": "Temperature for Power shedding"
|
||||
"power_temp": "Temperature for Power shedding",
|
||||
"use_power_central_config": "Check to use the central power configuration. Uncheck to use a specific power configuration for this VTherm"
|
||||
}
|
||||
},
|
||||
"presence": {
|
||||
"title": "Presence management",
|
||||
"description": "Presence management attributes.\nGives the a presence sensor of your home (true is someone is present).\nThen specify either the preset to use when presence sensor is false or the offset in temperature to apply.\nIf preset is given, the offset will not be used.\nLeave corresponding entity_id empty if not used.",
|
||||
"description": "Presence management attributes.\nGives the a presence sensor of your home (true is someone is present) and give the corresponding temperature preset setting.",
|
||||
"data": {
|
||||
"presence_sensor_entity_id": "Presence sensor entity id",
|
||||
"eco_away_temp": "Temperature in Eco preset when no presence",
|
||||
"comfort_away_temp": "Temperature in Comfort preset when no presence",
|
||||
"boost_away_temp": "Temperature in Boost preset when no presence",
|
||||
"eco_ac_away_temp": "Temperature in Eco preset when no presence in AC mode",
|
||||
"comfort_ac_away_temp": "Temperature in Comfort preset when no presence in AC mode",
|
||||
"boost_ac_away_temp": "Temperature in Boost preset when no presence in AC mode"
|
||||
"presence_sensor_entity_id": "Presence sensor",
|
||||
"use_presence_central_config": "Use central presence temperature configuration. Deselect to use specific temperature entities"
|
||||
},
|
||||
"data_description": {
|
||||
"presence_sensor_entity_id": "Presence sensor entity id"
|
||||
}
|
||||
},
|
||||
"advanced": {
|
||||
"title": "Advanced parameters",
|
||||
"description": "Configuration of advanced parameters. Leave the default values if you don't know what you are doing.\nThis parameters can lead to a very bad temperature or power regulation.",
|
||||
"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": "Minimal activation delay",
|
||||
"security_delay_min": "Security delay (in minutes)",
|
||||
"security_min_on_percent": "Minimal power percent to enable security mode",
|
||||
"security_default_on_percent": "Power percent to use in security mode"
|
||||
"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",
|
||||
"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 mesures. Above this delay, the thermostat will turn to a security off state",
|
||||
"security_min_on_percent": "Minimal heating percent value for security preset activation. Below this amount of power percent the thermostat won't go into security preset",
|
||||
"security_default_on_percent": "The default heating power percent value in security preset. Set to 0 to switch off heater in security present"
|
||||
"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",
|
||||
"use_advanced_central_config": "Check to use the central advanced configuration. Uncheck to use a specific advanced configuration for this VTherm"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"unknown": "Unexpected error",
|
||||
"unknown_entity": "Unknown entity id",
|
||||
"window_open_detection_method": "Only one window open detection method should be used. Use sensor or automatic detection through temperature threshold but not both"
|
||||
"window_open_detection_method": "Only one window open detection method should be used. Use either window sensor or automatic detection through temperature threshold but not both",
|
||||
"no_central_config": "You cannot select 'use central configuration' because no central configuration was found. You need to create a Versatile Thermostat of type 'Central Configuration' to use it."
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "Device is already configured"
|
||||
@@ -173,166 +219,213 @@
|
||||
"flow_title": "Versatile Thermostat configuration",
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "Add new Versatile Thermostat",
|
||||
"title": "Type - {name}",
|
||||
"data": {
|
||||
"thermostat_type": "Thermostat type"
|
||||
},
|
||||
"data_description": {
|
||||
"thermostat_type": "Only one central configuration type is possible"
|
||||
}
|
||||
},
|
||||
"menu": {
|
||||
"title": "Menu",
|
||||
"description": "Configure your thermostat. You will be able to finalize the configuration when all required parameters are entered.",
|
||||
"menu_options": {
|
||||
"main": "Main attributes",
|
||||
"central_boiler": "Central boiler",
|
||||
"type": "Underlyings",
|
||||
"tpi": "TPI parameters",
|
||||
"features": "Features",
|
||||
"presets": "Presets",
|
||||
"window": "Window detection",
|
||||
"motion": "Motion detection",
|
||||
"power": "Power management",
|
||||
"presence": "Presence detection",
|
||||
"advanced": "Advanced parameters",
|
||||
"auto_start_stop": "Auto start and stop",
|
||||
"finalize": "All done",
|
||||
"configuration_not_complete": "Configuration not complete"
|
||||
}
|
||||
},
|
||||
"main": {
|
||||
"title": "Main - {name}",
|
||||
"description": "Main mandatory attributes",
|
||||
"data": {
|
||||
"name": "Name",
|
||||
"thermostat_type": "Thermostat type",
|
||||
"temperature_sensor_entity_id": "Temperature sensor entity id",
|
||||
"external_temperature_sensor_entity_id": "External temperature sensor entity id",
|
||||
"temperature_sensor_entity_id": "Room temperature",
|
||||
"last_seen_temperature_sensor_entity_id": "Last seen room temperature datetime",
|
||||
"external_temperature_sensor_entity_id": "Outdoor temperature sensor entity id",
|
||||
"cycle_min": "Cycle duration (minutes)",
|
||||
"temp_min": "Minimal temperature allowed",
|
||||
"temp_max": "Maximal temperature allowed",
|
||||
"device_power": "Device power (kW)",
|
||||
"temp_min": "Minimum temperature allowed",
|
||||
"temp_max": "Maximum temperature allowed",
|
||||
"step_temperature": "Temperature step",
|
||||
"device_power": "Device power",
|
||||
"use_central_mode": "Enable the control by central entity (requires central config). Check to enable the control of the VTherm with the select central_mode entities.",
|
||||
"use_main_central_config": "Use additional central main configuration. Check to use the central main configuration (outdoor temperature, min, max, step, ...).",
|
||||
"used_by_controls_central_boiler": "Used by central boiler. Check if this VTherm should have control on the central boiler"
|
||||
},
|
||||
"data_description": {
|
||||
"temperature_sensor_entity_id": "Room temperature sensor entity id",
|
||||
"last_seen_temperature_sensor_entity_id": "Last seen room temperature sensor entity id. Should be datetime sensor",
|
||||
"external_temperature_sensor_entity_id": "Outdoor temperature sensor entity id. Not used if central configuration is selected"
|
||||
}
|
||||
},
|
||||
"features": {
|
||||
"title": "Features - {name}",
|
||||
"description": "Thermostat features",
|
||||
"data": {
|
||||
"use_window_feature": "Use window detection",
|
||||
"use_motion_feature": "Use motion detection",
|
||||
"use_power_feature": "Use power management",
|
||||
"use_presence_feature": "Use presence detection"
|
||||
"use_presence_feature": "Use presence detection",
|
||||
"use_central_boiler_feature": "Use a central boiler. Check to add a control to your central boiler. You will have to configure the VTherm which will have a control of the central boiler after seecting this checkbox to take effect. If one VTherm requires heating, the boiler will be turned on. If no VTherm requires heating, the boiler will be turned off. Commands for turning on/off the central boiler are given in the related configuration page",
|
||||
"use_auto_start_stop_feature": "Use the auto start and stop feature"
|
||||
}
|
||||
},
|
||||
"type": {
|
||||
"title": "Linked entities",
|
||||
"title": "Entities - {name}",
|
||||
"description": "Linked entities attributes",
|
||||
"data": {
|
||||
"heater_entity_id": "1rst heater switch",
|
||||
"heater_entity2_id": "2nd heater switch",
|
||||
"heater_entity3_id": "3rd heater switch",
|
||||
"heater_entity4_id": "4th heater switch",
|
||||
"underlying_entity_ids": "The device(s) to be controlled",
|
||||
"heater_keep_alive": "Switch keep-alive interval in seconds",
|
||||
"proportional_function": "Algorithm",
|
||||
"climate_entity_id": "1rst underlying climate",
|
||||
"climate_entity2_id": "2nd underlying climate",
|
||||
"climate_entity3_id": "3rd underlying climate",
|
||||
"climate_entity4_id": "4th underlying climate",
|
||||
"ac_mode": "AC mode",
|
||||
"valve_entity_id": "1rst valve number",
|
||||
"valve_entity2_id": "2nd valve number",
|
||||
"valve_entity3_id": "3rd valve number",
|
||||
"valve_entity4_id": "4th valve number",
|
||||
"auto_regulation_mode": "Self-regulation",
|
||||
"auto_regulation_dtemp": "Regulation threshold",
|
||||
"auto_regulation_periode_min": "Regulation minimal period",
|
||||
"inverse_switch_command": "Inverse switch command"
|
||||
"auto_regulation_periode_min": "Regulation minimum period",
|
||||
"auto_regulation_use_device_temp": "Use internal temperature of the underlying",
|
||||
"inverse_switch_command": "Inverse switch command",
|
||||
"auto_fan_mode": "Auto fan mode"
|
||||
},
|
||||
"data_description": {
|
||||
"heater_entity_id": "Mandatory heater entity id",
|
||||
"heater_entity2_id": "Optional 2nd Heater entity id. Leave empty if not used",
|
||||
"heater_entity3_id": "Optional 3rd Heater entity id. Leave empty if not used",
|
||||
"heater_entity4_id": "Optional 4th Heater entity id. Leave empty if not used",
|
||||
"underlying_entity_ids": "The device(s) to be controlled - 1 is required",
|
||||
"heater_keep_alive": "Optional heater switch state refresh interval. Leave empty if not required.",
|
||||
"proportional_function": "Algorithm to use (TPI is the only one for now)",
|
||||
"climate_entity_id": "Underlying climate entity id",
|
||||
"climate_entity2_id": "2nd underlying climate entity id",
|
||||
"climate_entity3_id": "3rd underlying climate entity id",
|
||||
"climate_entity4_id": "4th underlying climate entity id",
|
||||
"ac_mode": "Use the Air Conditioning (AC) mode",
|
||||
"valve_entity_id": "1rst valve number entity id",
|
||||
"valve_entity2_id": "2nd valve number entity id",
|
||||
"valve_entity3_id": "3rd valve number entity id",
|
||||
"valve_entity4_id": "4th valve number entity id",
|
||||
"auto_regulation_mode": "Auto adjustment of the target temperature",
|
||||
"auto_regulation_dtemp": "The threshold in ° under which the temperature change will not be send",
|
||||
"auto_regulation_dtemp": "The threshold in ° (or % for valve) under which the temperature change will not be sent",
|
||||
"auto_regulation_periode_min": "Duration in minutes between two regulation update",
|
||||
"inverse_switch_command": "For switch with pilot wire and diode you may need to inverse the command"
|
||||
"auto_regulation_use_device_temp": "Use the eventual internal temperature sensor of the underlying to speedup the self-regulation",
|
||||
"inverse_switch_command": "For switch with pilot wire and diode you may need to invert the command",
|
||||
"auto_fan_mode": "Automatically activate fan when huge heating/cooling is necessary"
|
||||
}
|
||||
},
|
||||
"tpi": {
|
||||
"title": "TPI",
|
||||
"title": "TPI - {name}",
|
||||
"description": "Time Proportional Integral attributes",
|
||||
"data": {
|
||||
"tpi_coef_int": "coef_int",
|
||||
"tpi_coef_ext": "coef_ext",
|
||||
"use_tpi_central_config": "Use central TPI configuration"
|
||||
},
|
||||
"data_description": {
|
||||
"tpi_coef_int": "Coefficient to use for internal temperature delta",
|
||||
"tpi_coef_ext": "Coefficient to use for external temperature delta"
|
||||
"tpi_coef_ext": "Coefficient to use for external temperature delta",
|
||||
"use_tpi_central_config": "Check to use the central TPI configuration. Uncheck to use a specific TPI configuration for this VTherm"
|
||||
}
|
||||
},
|
||||
"presets": {
|
||||
"title": "Presets",
|
||||
"description": "For each presets, give the target temperature (0 to ignore preset)",
|
||||
"title": "Presets - {name}",
|
||||
"description": "Check if the thermostat will use central presets. Uncheck and the thermostat will have its own preset entities",
|
||||
"data": {
|
||||
"eco_temp": "Temperature in Eco preset",
|
||||
"comfort_temp": "Temperature in Comfort preset",
|
||||
"boost_temp": "Temperature in Boost preset",
|
||||
"eco_ac_temp": "Temperature in Eco preset for AC mode",
|
||||
"comfort_ac_temp": "Temperature in Comfort preset for AC mode",
|
||||
"boost_ac_temp": "Temperature in Boost preset for AC mode"
|
||||
"use_presets_central_config": "Use central presets configuration"
|
||||
}
|
||||
},
|
||||
"window": {
|
||||
"title": "Window management",
|
||||
"description": "Open window management.\nLeave corresponding entity_id empty if not used\nYou can also configure automatic window open detection based on temperature decrease",
|
||||
"title": "Window - {name}",
|
||||
"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_auto_open_threshold": "Temperature decrease threshold for automatic window open detection (in °/min)",
|
||||
"window_auto_close_threshold": "Temperature increase threshold for end of automatic detection (in °/min)",
|
||||
"window_auto_max_duration": "Maximum duration of automatic window open detection (in min)"
|
||||
"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)",
|
||||
"use_window_central_config": "Use central window configuration",
|
||||
"window_action": "Action"
|
||||
},
|
||||
"data_description": {
|
||||
"window_sensor_entity_id": "Leave empty if no window sensor should be use",
|
||||
"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 0.05 and 0.1. Leave empty if automatic window open detection is not use",
|
||||
"window_auto_close_threshold": "Recommended value: 0. Leave empty if automatic window open detection is not use",
|
||||
"window_auto_max_duration": "Recommended value: 60 (one hour). Leave empty if automatic window open detection is not use"
|
||||
"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",
|
||||
"use_window_central_config": "Check to use the central window configuration. Uncheck to use a specific window configuration for this VTherm",
|
||||
"window_action": "Action to do if window is deteted as open"
|
||||
}
|
||||
},
|
||||
"motion": {
|
||||
"title": "Motion management",
|
||||
"description": "Motion sensor management. Preset can switch automatically depending of a motion detection\nLeave corresponding entity_id empty if not used.\nmotion_preset and no_motion_preset should be set to the corresponding preset name",
|
||||
"title": "Motion - {name}",
|
||||
"description": "Motion management. Preset can switch automatically depending of a motion detection\nmotion_preset and no_motion_preset should be set to the corresponding preset name",
|
||||
"data": {
|
||||
"motion_sensor_entity_id": "Motion sensor entity id",
|
||||
"motion_delay": "Activation delay",
|
||||
"motion_off_delay": "Deactivation delay",
|
||||
"motion_preset": "Motion preset",
|
||||
"no_motion_preset": "No motion preset"
|
||||
"no_motion_preset": "No motion preset",
|
||||
"use_motion_central_config": "Use central motion configuration"
|
||||
},
|
||||
"data_description": {
|
||||
"motion_sensor_entity_id": "The entity id of the motion sensor",
|
||||
"motion_delay": "Motion activation activation delay (seconds)",
|
||||
"motion_delay": "Motion activation delay (seconds)",
|
||||
"motion_off_delay": "Motion deactivation delay (seconds)",
|
||||
"motion_preset": "Preset to use when motion is detected",
|
||||
"no_motion_preset": "Preset to use when no motion is detected"
|
||||
"no_motion_preset": "Preset to use when no motion is detected",
|
||||
"use_motion_central_config": "Check to use the central motion configuration. Uncheck to use a specific motion configuration for this VTherm"
|
||||
}
|
||||
},
|
||||
"power": {
|
||||
"title": "Power management",
|
||||
"description": "Power management attributes.\nGives the power and max power sensor of your home.\nThen specify the power consumption of the heater when on.\nAll sensors and device power should have the same unit (kW or W).\nLeave corresponding entity_id empty if not used.",
|
||||
"title": "Power - {name}",
|
||||
"description": "Power management attributes.\nGives the power and max power sensor of your home.\nThen specify the power consumption of the heater when on.\nAll sensors and device power should have the same unit (kW or W).",
|
||||
"data": {
|
||||
"power_sensor_entity_id": "Power",
|
||||
"max_power_sensor_entity_id": "Max power",
|
||||
"power_temp": "Shedding temperature",
|
||||
"use_power_central_config": "Use central power configuration"
|
||||
},
|
||||
"data_description": {
|
||||
"power_sensor_entity_id": "Power sensor entity id",
|
||||
"max_power_sensor_entity_id": "Max power sensor entity id",
|
||||
"power_temp": "Temperature for Power shedding"
|
||||
"power_temp": "Temperature for Power shedding",
|
||||
"use_power_central_config": "Check to use the central power configuration. Uncheck to use a specific power configuration for this VTherm"
|
||||
}
|
||||
},
|
||||
"presence": {
|
||||
"title": "Presence management",
|
||||
"description": "Presence management attributes.\nGives the a presence sensor of your home (true is someone is present).\nThen specify either the preset to use when presence sensor is false or the offset in temperature to apply.\nIf preset is given, the offset will not be used.\nLeave corresponding entity_id empty if not used.",
|
||||
"title": "Presence - {name}",
|
||||
"description": "Presence management attributes.\nGives the a presence sensor of your home (true is someone is present) and give the corresponding temperature preset setting.",
|
||||
"data": {
|
||||
"presence_sensor_entity_id": "Presence sensor entity id (true is present)",
|
||||
"eco_away_temp": "Temperature in Eco preset when no presence",
|
||||
"comfort_away_temp": "Temperature in Comfort preset when no presence",
|
||||
"boost_away_temp": "Temperature in Boost preset when no presence",
|
||||
"eco_ac_away_temp": "Temperature in Eco preset when no presence in AC mode",
|
||||
"comfort_ac_away_temp": "Temperature in Comfort preset when no presence in AC mode",
|
||||
"boost_ac_away_temp": "Temperature in Boost preset when no presence in AC mode"
|
||||
"presence_sensor_entity_id": "Presence sensor",
|
||||
"use_presence_central_config": "Use central presence temperature configuration. Uncheck to use specific temperature entities"
|
||||
},
|
||||
"data_description": {
|
||||
"presence_sensor_entity_id": "Presence sensor entity id"
|
||||
}
|
||||
},
|
||||
"advanced": {
|
||||
"title": "Advanced parameters",
|
||||
"description": "Configuration of advanced parameters. Leave the default values if you don't know what you are doing.\nThis parameters can lead to a very bad temperature or power regulation.",
|
||||
"title": "Advanced - {name}",
|
||||
"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": "Minimal activation delay",
|
||||
"security_delay_min": "Security delay (in minutes)",
|
||||
"security_min_on_percent": "Minimal power percent for security mode",
|
||||
"security_default_on_percent": "Power percent to use in security mode"
|
||||
"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",
|
||||
"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 mesures. Above this delay, the thermostat will turn to a security off state",
|
||||
"security_min_on_percent": "Minimal heating percent value for security preset activation. Below this amount of power percent the thermostat won't go into security preset",
|
||||
"security_default_on_percent": "The default heating power percent value in security preset. Set to 0 to switch off heater in security present"
|
||||
"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",
|
||||
"use_advanced_central_config": "Check to use the central advanced configuration. Uncheck to use a specific advanced configuration for this VTherm"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"unknown": "Unexpected error",
|
||||
"unknown_entity": "Unknown entity id",
|
||||
"window_open_detection_method": "Only one window open detection method should be used. Use sensor or automatic detection through temperature threshold but not both"
|
||||
"window_open_detection_method": "Only one window open detection method should be used. Use either window sensor or automatic detection through temperature threshold but not both",
|
||||
"no_central_config": "You cannot check 'use central configuration' because no central configuration was found. You need to create a Versatile Thermostat of type 'Central Configuration' to use it.",
|
||||
"service_configuration_format": "The format of the service configuration is wrong"
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "Device is already configured"
|
||||
@@ -341,6 +434,7 @@
|
||||
"selector": {
|
||||
"thermostat_type": {
|
||||
"options": {
|
||||
"thermostat_central_config": "Central configuration",
|
||||
"thermostat_over_switch": "Thermostat over a switch",
|
||||
"thermostat_over_climate": "Thermostat over a climate",
|
||||
"thermostat_over_valve": "Thermostat over a valve"
|
||||
@@ -352,8 +446,42 @@
|
||||
"auto_regulation_strong": "Strong",
|
||||
"auto_regulation_medium": "Medium",
|
||||
"auto_regulation_light": "Light",
|
||||
"auto_regulation_expert": "Expert",
|
||||
"auto_regulation_none": "No auto-regulation"
|
||||
}
|
||||
},
|
||||
"auto_fan_mode": {
|
||||
"options": {
|
||||
"auto_fan_none": "No auto fan",
|
||||
"auto_fan_low": "Low",
|
||||
"auto_fan_medium": "Medium",
|
||||
"auto_fan_high": "High",
|
||||
"auto_fan_turbo": "Turbo"
|
||||
}
|
||||
},
|
||||
"window_action": {
|
||||
"options": {
|
||||
"window_turn_off": "Turn off",
|
||||
"window_fan_only": "Fan only",
|
||||
"window_frost_temp": "Frost protect",
|
||||
"window_eco_temp": "Eco"
|
||||
}
|
||||
},
|
||||
"presets": {
|
||||
"options": {
|
||||
"frost": "Frost protect",
|
||||
"eco": "Eco",
|
||||
"comfort": "Comfort",
|
||||
"boost": "Boost"
|
||||
}
|
||||
},
|
||||
"auto_start_stop": {
|
||||
"options": {
|
||||
"auto_start_stop_none": "No auto start/stop",
|
||||
"auto_start_stop_slow": "Slow detection",
|
||||
"auto_start_stop_medium": "Medium detection",
|
||||
"auto_start_stop_fast": "Fast detection"
|
||||
}
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
@@ -363,12 +491,60 @@
|
||||
"preset_mode": {
|
||||
"state": {
|
||||
"power": "Shedding",
|
||||
"security": "Security",
|
||||
"none": "Manual"
|
||||
"security": "Safety",
|
||||
"none": "Manual",
|
||||
"frost": "Frost"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"number": {
|
||||
"frost_temp": {
|
||||
"name": "Frost"
|
||||
},
|
||||
"eco_temp": {
|
||||
"name": "Eco"
|
||||
},
|
||||
"comfort_temp": {
|
||||
"name": "Comfort"
|
||||
},
|
||||
"boost_temp": {
|
||||
"name": "Boost"
|
||||
},
|
||||
"frost_ac_temp": {
|
||||
"name": "Frost ac"
|
||||
},
|
||||
"eco_ac_temp": {
|
||||
"name": "Eco ac"
|
||||
},
|
||||
"comfort_ac_temp": {
|
||||
"name": "Comfort ac"
|
||||
},
|
||||
"boost_ac_temp": {
|
||||
"name": "Boost ac"
|
||||
},
|
||||
"frost_away_temp": {
|
||||
"name": "Frost away"
|
||||
},
|
||||
"eco_away_temp": {
|
||||
"name": "Eco away"
|
||||
},
|
||||
"comfort_away_temp": {
|
||||
"name": "Comfort away"
|
||||
},
|
||||
"boost_away_temp": {
|
||||
"name": "Boost away"
|
||||
},
|
||||
"eco_ac_away_temp": {
|
||||
"name": "Eco ac away"
|
||||
},
|
||||
"comfort_ac_away_temp": {
|
||||
"name": "Comfort ac away"
|
||||
},
|
||||
"boost_ac_away_temp": {
|
||||
"name": "Boost ac away"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
102
custom_components/versatile_thermostat/switch.py
Normal file
@@ -0,0 +1,102 @@
|
||||
## pylint: disable=unused-argument
|
||||
|
||||
""" Implements the VersatileThermostat select component """
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
|
||||
from homeassistant.components.switch import SwitchEntity
|
||||
|
||||
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 .const import * # pylint: disable=unused-wildcard-import,wildcard-import
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the VersatileThermostat switches with config flow."""
|
||||
_LOGGER.debug(
|
||||
"Calling async_setup_entry entry=%s, data=%s", entry.entry_id, entry.data
|
||||
)
|
||||
|
||||
unique_id = entry.entry_id
|
||||
name = entry.data.get(CONF_NAME)
|
||||
vt_type = entry.data.get(CONF_THERMOSTAT_TYPE)
|
||||
auto_start_stop_feature = entry.data.get(CONF_USE_AUTO_START_STOP_FEATURE)
|
||||
|
||||
if vt_type == CONF_THERMOSTAT_CLIMATE and auto_start_stop_feature is True:
|
||||
# Creates a switch to enable the auto-start/stop
|
||||
enable_entity = AutoStartStopEnable(hass, unique_id, name, entry)
|
||||
async_add_entities([enable_entity], True)
|
||||
|
||||
|
||||
class AutoStartStopEnable(VersatileThermostatBaseEntity, SwitchEntity, RestoreEntity):
|
||||
"""The that enables the ManagedDevice optimisation with"""
|
||||
|
||||
def __init__(
|
||||
self, hass: HomeAssistant, unique_id: str, name: str, entry_infos: ConfigEntry
|
||||
):
|
||||
super().__init__(hass, unique_id, name)
|
||||
self._attr_name = "Enable auto start/stop"
|
||||
self._attr_unique_id = f"{self._device_name}_enable_auto_start_stop"
|
||||
self._default_value = (
|
||||
entry_infos.data.get(CONF_AUTO_START_STOP_LEVEL)
|
||||
!= AUTO_START_STOP_LEVEL_NONE
|
||||
)
|
||||
self._attr_is_on = self._default_value
|
||||
|
||||
@property
|
||||
def icon(self) -> str | None:
|
||||
"""The icon"""
|
||||
return "mdi:power-sleep"
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
await super().async_added_to_hass()
|
||||
|
||||
# Récupérer le dernier état sauvegardé de l'entité
|
||||
last_state = await self.async_get_last_state()
|
||||
|
||||
# Si l'état précédent existe, vous pouvez l'utiliser
|
||||
if last_state is not None:
|
||||
self._attr_is_on = last_state.state == "on"
|
||||
else:
|
||||
# If no previous state set it to false by default
|
||||
self._attr_is_on = self._default_value
|
||||
|
||||
self.update_my_state_and_vtherm()
|
||||
|
||||
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)
|
||||
|
||||
@callback
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn the entity on."""
|
||||
self.turn_on()
|
||||
|
||||
@callback
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn the entity off."""
|
||||
self.turn_off()
|
||||
|
||||
@overrides
|
||||
def turn_off(self, **kwargs: Any):
|
||||
self._attr_is_on = False
|
||||
self.update_my_state_and_vtherm()
|
||||
|
||||
@overrides
|
||||
def turn_on(self, **kwargs: Any):
|
||||
self._attr_is_on = True
|
||||
self.update_my_state_and_vtherm()
|
||||
@@ -2,27 +2,30 @@
|
||||
|
||||
""" A climate over switch classe """
|
||||
import logging
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.event import async_track_state_change_event
|
||||
from homeassistant.core import Event, callback
|
||||
from homeassistant.helpers.event import (
|
||||
async_track_state_change_event,
|
||||
EventStateChangedData,
|
||||
)
|
||||
from homeassistant.components.climate import HVACMode
|
||||
|
||||
from .const import (
|
||||
CONF_HEATER,
|
||||
CONF_HEATER_2,
|
||||
CONF_HEATER_3,
|
||||
CONF_HEATER_4,
|
||||
CONF_UNDERLYING_LIST,
|
||||
CONF_HEATER_KEEP_ALIVE,
|
||||
CONF_INVERSE_SWITCH,
|
||||
overrides,
|
||||
)
|
||||
|
||||
from .base_thermostat import BaseThermostat
|
||||
from .base_thermostat import BaseThermostat, ConfigData
|
||||
from .underlyings import UnderlyingSwitch
|
||||
from .prop_algorithm import PropAlgorithm
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
_LOGGER_ENERGY = logging.getLogger(
|
||||
"custom_components.versatile_thermostat.energy_debug"
|
||||
)
|
||||
|
||||
|
||||
class ThermostatOverSwitch(BaseThermostat):
|
||||
class ThermostatOverSwitch(BaseThermostat[UnderlyingSwitch]):
|
||||
"""Representation of a base class for a Versatile Thermostat over a switch."""
|
||||
|
||||
_entity_component_unrecorded_attributes = (
|
||||
@@ -31,10 +34,7 @@ class ThermostatOverSwitch(BaseThermostat):
|
||||
{
|
||||
"is_over_switch",
|
||||
"is_inversed",
|
||||
"underlying_switch_0",
|
||||
"underlying_switch_1",
|
||||
"underlying_switch_2",
|
||||
"underlying_switch_3",
|
||||
"underlying_entities",
|
||||
"on_time_sec",
|
||||
"off_time_sec",
|
||||
"cycle_min",
|
||||
@@ -48,10 +48,10 @@ class ThermostatOverSwitch(BaseThermostat):
|
||||
)
|
||||
|
||||
# useless for now
|
||||
# def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None:
|
||||
# def __init__(self, hass: HomeAssistant, unique_id, name, config_entry) -> None:
|
||||
# """Initialize the thermostat over switch."""
|
||||
# super().__init__(hass, unique_id, name, entry_infos)
|
||||
_is_inversed: bool = None
|
||||
# super().__init__(hass, unique_id, name, config_entry)
|
||||
_is_inversed: bool | None = None
|
||||
|
||||
@property
|
||||
def is_over_switch(self) -> bool:
|
||||
@@ -72,10 +72,10 @@ class ThermostatOverSwitch(BaseThermostat):
|
||||
return None
|
||||
|
||||
@overrides
|
||||
def post_init(self, entry_infos):
|
||||
def post_init(self, config_entry: ConfigData):
|
||||
"""Initialize the Thermostat"""
|
||||
|
||||
super().post_init(entry_infos)
|
||||
super().post_init(config_entry)
|
||||
|
||||
self._prop_algorithm = PropAlgorithm(
|
||||
self._proportional_function,
|
||||
@@ -83,15 +83,10 @@ class ThermostatOverSwitch(BaseThermostat):
|
||||
self._tpi_coef_ext,
|
||||
self._cycle_min,
|
||||
self._minimal_activation_delay,
|
||||
self.name,
|
||||
)
|
||||
|
||||
lst_switches = [entry_infos.get(CONF_HEATER)]
|
||||
if entry_infos.get(CONF_HEATER_2):
|
||||
lst_switches.append(entry_infos.get(CONF_HEATER_2))
|
||||
if entry_infos.get(CONF_HEATER_3):
|
||||
lst_switches.append(entry_infos.get(CONF_HEATER_3))
|
||||
if entry_infos.get(CONF_HEATER_4):
|
||||
lst_switches.append(entry_infos.get(CONF_HEATER_4))
|
||||
lst_switches = config_entry.get(CONF_UNDERLYING_LIST)
|
||||
|
||||
delta_cycle = self._cycle_min * 60 / len(lst_switches)
|
||||
for idx, switch in enumerate(lst_switches):
|
||||
@@ -101,10 +96,11 @@ class ThermostatOverSwitch(BaseThermostat):
|
||||
thermostat=self,
|
||||
switch_entity_id=switch,
|
||||
initial_delay_sec=idx * delta_cycle,
|
||||
keep_alive_sec=config_entry.get(CONF_HEATER_KEEP_ALIVE, 0),
|
||||
)
|
||||
)
|
||||
|
||||
self._is_inversed = entry_infos.get(CONF_INVERSE_SWITCH) is True
|
||||
self._is_inversed = config_entry.get(CONF_INVERSE_SWITCH) is True
|
||||
self._should_relaunch_control_heating = False
|
||||
|
||||
@overrides
|
||||
@@ -121,6 +117,7 @@ class ThermostatOverSwitch(BaseThermostat):
|
||||
self.hass, [switch.entity_id], self._async_switch_changed
|
||||
)
|
||||
)
|
||||
switch.startup()
|
||||
|
||||
self.hass.create_task(self.async_control_heating())
|
||||
|
||||
@@ -129,20 +126,14 @@ class ThermostatOverSwitch(BaseThermostat):
|
||||
"""Custom attributes"""
|
||||
super().update_custom_attributes()
|
||||
|
||||
under0: UnderlyingSwitch = self._underlyings[0]
|
||||
self._attr_extra_state_attributes["is_over_switch"] = self.is_over_switch
|
||||
self._attr_extra_state_attributes["is_inversed"] = self.is_inversed
|
||||
self._attr_extra_state_attributes["underlying_switch_0"] = self._underlyings[
|
||||
0
|
||||
].entity_id
|
||||
self._attr_extra_state_attributes["underlying_switch_1"] = (
|
||||
self._underlyings[1].entity_id if len(self._underlyings) > 1 else None
|
||||
)
|
||||
self._attr_extra_state_attributes["underlying_switch_2"] = (
|
||||
self._underlyings[2].entity_id if len(self._underlyings) > 2 else None
|
||||
)
|
||||
self._attr_extra_state_attributes["underlying_switch_3"] = (
|
||||
self._underlyings[3].entity_id if len(self._underlyings) > 3 else None
|
||||
)
|
||||
self._attr_extra_state_attributes["keep_alive_sec"] = under0.keep_alive_sec
|
||||
|
||||
self._attr_extra_state_attributes["underlying_entities"] = [
|
||||
underlying.entity_id for underlying in self._underlyings
|
||||
]
|
||||
|
||||
self._attr_extra_state_attributes[
|
||||
"on_percent"
|
||||
@@ -176,10 +167,11 @@ class ThermostatOverSwitch(BaseThermostat):
|
||||
self._target_temp,
|
||||
self._cur_temp,
|
||||
self._cur_ext_temp,
|
||||
self._hvac_mode == HVACMode.COOL,
|
||||
self._hvac_mode or HVACMode.OFF,
|
||||
)
|
||||
self.update_custom_attributes()
|
||||
self.async_write_ha_state()
|
||||
# already done bu update_custom_attributes
|
||||
# self.async_write_ha_state()
|
||||
|
||||
@overrides
|
||||
def incremente_energy(self):
|
||||
@@ -191,7 +183,23 @@ class ThermostatOverSwitch(BaseThermostat):
|
||||
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
|
||||
|
||||
self._total_energy += added_energy
|
||||
if self._total_energy is None:
|
||||
self._total_energy = added_energy
|
||||
_LOGGER_ENERGY.debug(
|
||||
"%s - incremente_energy set energy is %s",
|
||||
self,
|
||||
self._total_energy,
|
||||
)
|
||||
else:
|
||||
self._total_energy += added_energy
|
||||
_LOGGER_ENERGY.debug(
|
||||
"%s - incremente_energy increment energy is %s",
|
||||
self,
|
||||
self._total_energy,
|
||||
)
|
||||
|
||||
self.update_custom_attributes()
|
||||
|
||||
_LOGGER.debug(
|
||||
"%s - added energy is %.3f . Total energy is now: %.3f",
|
||||
self,
|
||||
@@ -200,7 +208,7 @@ class ThermostatOverSwitch(BaseThermostat):
|
||||
)
|
||||
|
||||
@callback
|
||||
def _async_switch_changed(self, event):
|
||||
def _async_switch_changed(self, event: Event[EventStateChangedData]):
|
||||
"""Handle heater switch state changes."""
|
||||
new_state = event.data.get("new_state")
|
||||
old_state = event.data.get("old_state")
|
||||
@@ -208,5 +216,6 @@ class ThermostatOverSwitch(BaseThermostat):
|
||||
return
|
||||
if old_state is None:
|
||||
self.hass.create_task(self._check_initial_state())
|
||||
|
||||
self.async_write_ha_state()
|
||||
self.update_custom_attributes()
|
||||
|
||||
@@ -1,77 +1,111 @@
|
||||
# pylint: disable=line-too-long
|
||||
""" A climate over switch classe """
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
from datetime import timedelta, datetime
|
||||
|
||||
from homeassistant.helpers.event import async_track_state_change_event, async_track_time_interval
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.event import (
|
||||
async_track_state_change_event,
|
||||
async_track_time_interval,
|
||||
EventStateChangedData,
|
||||
)
|
||||
from homeassistant.core import Event, HomeAssistant, callback
|
||||
from homeassistant.components.climate import HVACMode
|
||||
|
||||
from .base_thermostat import BaseThermostat
|
||||
from .base_thermostat import BaseThermostat, ConfigData
|
||||
from .prop_algorithm import PropAlgorithm
|
||||
|
||||
from .const import CONF_VALVE, CONF_VALVE_2, CONF_VALVE_3, CONF_VALVE_4, overrides
|
||||
from .const import (
|
||||
CONF_UNDERLYING_LIST,
|
||||
# This is not really self-regulation but regulation here
|
||||
CONF_AUTO_REGULATION_DTEMP,
|
||||
CONF_AUTO_REGULATION_PERIOD_MIN,
|
||||
overrides,
|
||||
)
|
||||
|
||||
from .underlyings import UnderlyingValve
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
_LOGGER_ENERGY = logging.getLogger(
|
||||
"custom_components.versatile_thermostat.energy_debug"
|
||||
)
|
||||
|
||||
class ThermostatOverValve(BaseThermostat):
|
||||
class ThermostatOverValve(BaseThermostat[UnderlyingValve]): # pylint: disable=abstract-method
|
||||
"""Representation of a class for a Versatile Thermostat over a Valve"""
|
||||
|
||||
_entity_component_unrecorded_attributes = BaseThermostat._entity_component_unrecorded_attributes.union(frozenset(
|
||||
{
|
||||
"is_over_valve", "underlying_valve_0", "underlying_valve_1",
|
||||
"underlying_valve_2", "underlying_valve_3", "on_time_sec", "off_time_sec",
|
||||
"cycle_min", "function", "tpi_coef_int", "tpi_coef_ext"
|
||||
}))
|
||||
_entity_component_unrecorded_attributes = BaseThermostat._entity_component_unrecorded_attributes.union( # pylint: disable=protected-access
|
||||
frozenset(
|
||||
{
|
||||
"is_over_valve",
|
||||
"underlying_entities",
|
||||
"on_time_sec",
|
||||
"off_time_sec",
|
||||
"cycle_min",
|
||||
"function",
|
||||
"tpi_coef_int",
|
||||
"tpi_coef_ext",
|
||||
"auto_regulation_dpercent",
|
||||
"auto_regulation_period_min",
|
||||
"last_calculation_timestamp",
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
# Useless for now
|
||||
# def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None:
|
||||
# """Initialize the thermostat over switch."""
|
||||
# super().__init__(hass, unique_id, name, entry_infos)
|
||||
def __init__(
|
||||
self, hass: HomeAssistant, unique_id: str, name: str, config_entry: ConfigData
|
||||
):
|
||||
"""Initialize the thermostat over switch."""
|
||||
self._valve_open_percent: int = 0
|
||||
self._last_calculation_timestamp: datetime | None = None
|
||||
self._auto_regulation_dpercent: float | None = None
|
||||
self._auto_regulation_period_min: int | None = None
|
||||
|
||||
# Call to super must be done after initialization because it calls post_init at the end
|
||||
super().__init__(hass, unique_id, name, config_entry)
|
||||
|
||||
@property
|
||||
def is_over_valve(self) -> bool:
|
||||
""" True if the Thermostat is over_valve"""
|
||||
"""True if the Thermostat is over_valve"""
|
||||
return True
|
||||
|
||||
@property
|
||||
def valve_open_percent(self) -> int:
|
||||
""" Gives the percentage of valve needed"""
|
||||
"""Gives the percentage of valve needed"""
|
||||
if self._hvac_mode == HVACMode.OFF:
|
||||
return 0
|
||||
else:
|
||||
return round(max(0, min(self.proportional_algorithm.on_percent, 1)) * 100)
|
||||
return self._valve_open_percent
|
||||
|
||||
@overrides
|
||||
def post_init(self, entry_infos):
|
||||
""" Initialize the Thermostat"""
|
||||
def post_init(self, config_entry: ConfigData):
|
||||
"""Initialize the Thermostat"""
|
||||
|
||||
super().post_init(config_entry)
|
||||
|
||||
self._auto_regulation_dpercent = (
|
||||
config_entry.get(CONF_AUTO_REGULATION_DTEMP)
|
||||
if config_entry.get(CONF_AUTO_REGULATION_DTEMP) is not None
|
||||
else 0.0
|
||||
)
|
||||
self._auto_regulation_period_min = (
|
||||
config_entry.get(CONF_AUTO_REGULATION_PERIOD_MIN)
|
||||
if config_entry.get(CONF_AUTO_REGULATION_PERIOD_MIN) is not None
|
||||
else 0
|
||||
)
|
||||
|
||||
super().post_init(entry_infos)
|
||||
self._prop_algorithm = PropAlgorithm(
|
||||
self._proportional_function,
|
||||
self._tpi_coef_int,
|
||||
self._tpi_coef_ext,
|
||||
self._cycle_min,
|
||||
self._minimal_activation_delay,
|
||||
self.name,
|
||||
)
|
||||
|
||||
lst_valves = [entry_infos.get(CONF_VALVE)]
|
||||
if entry_infos.get(CONF_VALVE_2):
|
||||
lst_valves.append(entry_infos.get(CONF_VALVE_2))
|
||||
if entry_infos.get(CONF_VALVE_3):
|
||||
lst_valves.append(entry_infos.get(CONF_VALVE_3))
|
||||
if entry_infos.get(CONF_VALVE_4):
|
||||
lst_valves.append(entry_infos.get(CONF_VALVE_4))
|
||||
lst_valves = config_entry.get(CONF_UNDERLYING_LIST)
|
||||
|
||||
for _, valve in enumerate(lst_valves):
|
||||
self._underlyings.append(
|
||||
UnderlyingValve(
|
||||
hass=self._hass,
|
||||
thermostat=self,
|
||||
valve_entity_id=valve
|
||||
)
|
||||
UnderlyingValve(hass=self._hass, thermostat=self, valve_entity_id=valve)
|
||||
)
|
||||
|
||||
self._should_relaunch_control_heating = False
|
||||
@@ -89,7 +123,7 @@ class ThermostatOverValve(BaseThermostat):
|
||||
async_track_state_change_event(
|
||||
self.hass, [valve.entity_id], self._async_valve_changed
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
# Start the control_heating
|
||||
# starts a cycle
|
||||
@@ -102,34 +136,31 @@ class ThermostatOverValve(BaseThermostat):
|
||||
)
|
||||
|
||||
@callback
|
||||
async def _async_valve_changed(self, event):
|
||||
async def _async_valve_changed(self, event: Event[EventStateChangedData]):
|
||||
"""Handle unerdlying valve state changes.
|
||||
This method just log the change. It changes nothing to avoid loops.
|
||||
"""
|
||||
new_state = event.data.get("new_state")
|
||||
_LOGGER.debug("%s - _async_valve_changed new_state is %s", self, new_state.state)
|
||||
_LOGGER.debug(
|
||||
"%s - _async_valve_changed new_state is %s", self, new_state.state
|
||||
)
|
||||
|
||||
@overrides
|
||||
def update_custom_attributes(self):
|
||||
""" Custom attributes """
|
||||
"""Custom attributes"""
|
||||
super().update_custom_attributes()
|
||||
self._attr_extra_state_attributes["valve_open_percent"] = self.valve_open_percent
|
||||
self._attr_extra_state_attributes[
|
||||
"valve_open_percent"
|
||||
] = self.valve_open_percent
|
||||
self._attr_extra_state_attributes["is_over_valve"] = self.is_over_valve
|
||||
self._attr_extra_state_attributes["underlying_valve_0"] = (
|
||||
self._underlyings[0].entity_id)
|
||||
self._attr_extra_state_attributes["underlying_valve_1"] = (
|
||||
self._underlyings[1].entity_id if len(self._underlyings) > 1 else None
|
||||
)
|
||||
self._attr_extra_state_attributes["underlying_valve_2"] = (
|
||||
self._underlyings[2].entity_id if len(self._underlyings) > 2 else None
|
||||
)
|
||||
self._attr_extra_state_attributes["underlying_valve_3"] = (
|
||||
self._underlyings[3].entity_id if len(self._underlyings) > 3 else None
|
||||
)
|
||||
|
||||
self._attr_extra_state_attributes["underlying_entities"] = [
|
||||
underlying.entity_id for underlying in self._underlyings
|
||||
]
|
||||
|
||||
self._attr_extra_state_attributes[
|
||||
"on_percent"
|
||||
] = self._prop_algorithm.on_percent
|
||||
"on_percent"
|
||||
] = self._prop_algorithm.on_percent
|
||||
self._attr_extra_state_attributes[
|
||||
"on_time_sec"
|
||||
] = self._prop_algorithm.on_time_sec
|
||||
@@ -140,6 +171,17 @@ class ThermostatOverValve(BaseThermostat):
|
||||
self._attr_extra_state_attributes["function"] = self._proportional_function
|
||||
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[
|
||||
"auto_regulation_dpercent"
|
||||
] = self._auto_regulation_dpercent
|
||||
self._attr_extra_state_attributes[
|
||||
"auto_regulation_period_min"
|
||||
] = self._auto_regulation_period_min
|
||||
self._attr_extra_state_attributes["last_calculation_timestamp"] = (
|
||||
self._last_calculation_timestamp.astimezone(self._current_tz).isoformat()
|
||||
if self._last_calculation_timestamp
|
||||
else None
|
||||
)
|
||||
|
||||
self.async_write_ha_state()
|
||||
_LOGGER.debug(
|
||||
@@ -153,21 +195,65 @@ class ThermostatOverValve(BaseThermostat):
|
||||
"""A utility function to force the calculation of a the algo and
|
||||
update the custom attributes and write the state
|
||||
"""
|
||||
_LOGGER.debug("%s - recalculate all", self)
|
||||
_LOGGER.debug("%s - recalculate the open percent", self)
|
||||
|
||||
# For testing purpose. Should call _set_now() before
|
||||
now = self.now
|
||||
|
||||
if self._last_calculation_timestamp is not None:
|
||||
period = (now - self._last_calculation_timestamp).total_seconds() / 60
|
||||
if period < self._auto_regulation_period_min:
|
||||
_LOGGER.info(
|
||||
"%s - do not calculate TPI because regulation_period (%d) is not exceeded",
|
||||
self,
|
||||
period,
|
||||
)
|
||||
return
|
||||
|
||||
self._prop_algorithm.calculate(
|
||||
self._target_temp,
|
||||
self._cur_temp,
|
||||
self._cur_ext_temp,
|
||||
self._hvac_mode == HVACMode.COOL,
|
||||
self._hvac_mode or HVACMode.OFF,
|
||||
)
|
||||
|
||||
for under in self._underlyings:
|
||||
under.set_valve_open_percent(
|
||||
self._prop_algorithm.on_percent
|
||||
new_valve_percent = round(
|
||||
max(0, min(self.proportional_algorithm.on_percent, 1)) * 100
|
||||
)
|
||||
|
||||
# Issue 533 - don't filter with dtemp if valve should be close. Else it will never close
|
||||
if new_valve_percent < self._auto_regulation_dpercent:
|
||||
new_valve_percent = 0
|
||||
|
||||
dpercent = new_valve_percent - self.valve_open_percent
|
||||
if (
|
||||
new_valve_percent > 0
|
||||
and -1 * self._auto_regulation_dpercent
|
||||
<= dpercent
|
||||
< self._auto_regulation_dpercent
|
||||
):
|
||||
_LOGGER.debug(
|
||||
"%s - do not calculate TPI because regulation_dpercent (%.1f) is not exceeded",
|
||||
self,
|
||||
dpercent,
|
||||
)
|
||||
|
||||
return
|
||||
|
||||
if self._valve_open_percent == new_valve_percent:
|
||||
_LOGGER.debug("%s - no change in valve_open_percent.", self)
|
||||
return
|
||||
|
||||
self._valve_open_percent = new_valve_percent
|
||||
|
||||
for under in self._underlyings:
|
||||
under.set_valve_open_percent()
|
||||
|
||||
self._last_calculation_timestamp = now
|
||||
|
||||
self.update_custom_attributes()
|
||||
self.async_write_ha_state()
|
||||
# already done in update_custom_attributes
|
||||
# self.async_write_ha_state()
|
||||
|
||||
@overrides
|
||||
def incremente_energy(self):
|
||||
@@ -179,10 +265,26 @@ class ThermostatOverValve(BaseThermostat):
|
||||
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
|
||||
|
||||
self._total_energy += added_energy
|
||||
if self._total_energy is None:
|
||||
self._total_energy = added_energy
|
||||
_LOGGER_ENERGY.debug(
|
||||
"%s - incremente_energy set energy is %s",
|
||||
self,
|
||||
self._total_energy,
|
||||
)
|
||||
else:
|
||||
self._total_energy += added_energy
|
||||
_LOGGER_ENERGY.debug(
|
||||
"%s - get_my_previous_state increment energy is %s",
|
||||
self,
|
||||
self._total_energy,
|
||||
)
|
||||
|
||||
self.update_custom_attributes()
|
||||
|
||||
_LOGGER.debug(
|
||||
"%s - added energy is %.3f . Total energy is now: %.3f",
|
||||
self,
|
||||
added_energy,
|
||||
self._total_energy,
|
||||
)
|
||||
)
|
||||
|
||||
392
custom_components/versatile_thermostat/translations/el.json
Normal file
@@ -0,0 +1,392 @@
|
||||
{
|
||||
"title": "Διαμόρφωση Ευέλικτου Θερμοστάτη",
|
||||
"config": {
|
||||
"flow_title": "Διαμόρφωση Ευέλικτου Θερμοστάτη",
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "Προσθήκη νέου Ευέλικτου Θερμοστάτη",
|
||||
"description": "Κύρια υποχρεωτικά χαρακτηριστικά",
|
||||
"data": {
|
||||
"name": "Όνομα",
|
||||
"thermostat_type": "Τύπος Θερμοστάτη",
|
||||
"temperature_sensor_entity_id": "Ταυτότητα οντότητας αισθητήρα θερμοκρασίας",
|
||||
"external_temperature_sensor_entity_id": "Ταυτότητα οντότητας εξωτερικού αισθητήρα θερμοκρασίας",
|
||||
"cycle_min": "Διάρκεια κύκλου (λεπτά)",
|
||||
"temp_min": "Ελάχιστη επιτρεπτή θερμοκρασία",
|
||||
"temp_max": "Μέγιστη επιτρεπτή θερμοκρασία",
|
||||
"device_power": "Ισχύς συσκευής",
|
||||
"use_window_feature": "Χρήση ανίχνευσης παραθύρου",
|
||||
"use_motion_feature": "Χρήση ανίχνευσης κίνησης",
|
||||
"use_power_feature": "Χρήση διαχείρισης ισχύος",
|
||||
"use_presence_feature": "Χρήση ανίχνευσης παρουσίας"
|
||||
}
|
||||
},
|
||||
"type": {
|
||||
"title": "Συνδεδεμένες οντότητες",
|
||||
"description": "Χαρακτηριστικά συνδεδεμένων οντοτήτων",
|
||||
"data": {
|
||||
"heater_entity_id": "1ος διακόπτης θερμαντήρα",
|
||||
"heater_entity2_id": "2ος διακόπτης θερμαντήρα",
|
||||
"heater_entity3_id": "3ος διακόπτης θερμαντήρα",
|
||||
"heater_entity4_id": "4ος διακόπτης θερμαντήρα",
|
||||
"proportional_function": "Αλγόριθμος",
|
||||
"climate_entity_id": "1η υποκείμενη κλιματική οντότητα",
|
||||
"climate_entity2_id": "2η υποκείμενη κλιματική οντότητα",
|
||||
"climate_entity3_id": "3η υποκείμενη κλιματική οντότητα",
|
||||
"climate_entity4_id": "4η υποκείμενη κλιματική οντότητα",
|
||||
"ac_mode": "Λειτουργία AC",
|
||||
"valve_entity_id": "1ος αριθμός βαλβίδας",
|
||||
"valve_entity2_id": "2ος αριθμός βαλβίδας",
|
||||
"valve_entity3_id": "3ος αριθμός βαλβίδας",
|
||||
"valve_entity4_id": "4ος αριθμός βαλβίδας",
|
||||
"auto_regulation_mode": "Αυτόματη ρύθμιση",
|
||||
"auto_regulation_dtemp": "Όριο ρύθμισης",
|
||||
"auto_regulation_periode_min": "Ελάχιστη περίοδος ρύθμισης",
|
||||
"inverse_switch_command": "Αντίστροφη εντολή διακόπτη",
|
||||
"auto_fan_mode": "Auto fan mode"
|
||||
},
|
||||
"data_description": {
|
||||
"heater_entity_id": "Υποχρεωτική ταυτότητα οντότητας θερμαντήρα",
|
||||
"heater_entity2_id": "Προαιρετική 2η ταυτότητα οντότητας θερμαντήρα. Αφήστε κενό αν δεν χρησιμοποιείται",
|
||||
"heater_entity3_id": "Προαιρετική 3η ταυτότητα οντότητας θερμαντήρα. Αφήστε κενό αν δεν χρησιμοποιείται",
|
||||
"heater_entity4_id": "Προαιρετική 4η ταυτότητα οντότητας θερμαντήρα. Αφήστε κενό αν δεν χρησιμοποιείται",
|
||||
"proportional_function": "Αλγόριθμος προς χρήση (TPI είναι ο μόνος για τώρα)",
|
||||
"climate_entity_id": "Ταυτότητα υποκείμενης κλιματικής οντότητας",
|
||||
"climate_entity2_id": "2η ταυτότητα υποκείμενης κλιματικής οντότητας",
|
||||
"climate_entity3_id": "3η ταυτότητα υποκείμενης κλιματικής οντότητας",
|
||||
"climate_entity4_id": "4η ταυτότητα υποκείμενης κλιματικής οντότητας",
|
||||
"ac_mode": "Χρήση της λειτουργίας Κλιματισμού (AC)",
|
||||
"valve_entity_id": "1η ταυτότητα αριθμού βαλβίδας",
|
||||
"valve_entity2_id": "2η ταυτότητα αριθμού βαλβίδας",
|
||||
"valve_entity3_id": "3η ταυτότητα αριθμού βαλβίδας",
|
||||
"valve_entity4_id": "4η ταυτότητα αριθμού βαλβίδας",
|
||||
"auto_regulation_mode": "Αυτόματη προσαρμογή της στοχευμένης θερμοκρασίας",
|
||||
"auto_regulation_dtemp": "Το όριο σε ° κάτω από το οποίο η αλλαγή θερμοκρασίας δεν θα αποστέλλεται",
|
||||
"auto_regulation_periode_min": "Διάρκεια σε λεπτά μεταξύ δύο ενημερώσεων ρύθμισης",
|
||||
"inverse_switch_command": "Για διακόπτη με πιλοτικό καλώδιο και δίοδο μπορεί να χρειαστεί να αντιστρέψετε την εντολή",
|
||||
"auto_fan_mode": "Automatically activate fan when huge heating/cooling is necessary"
|
||||
}
|
||||
},
|
||||
"tpi": {
|
||||
"title": "TPI",
|
||||
"description": "Χαρακτηριστικά Χρονικά Αναλογικού Ολοκληρωτικού (TPI)",
|
||||
"data": {
|
||||
"tpi_coef_int": "Συντελεστής για χρήση στη διαφορά εσωτερικής θερμοκρασίας",
|
||||
"tpi_coef_ext": "Συντελεστής για χρήση στη διαφορά εξωτερικής θερμοκρασίας"
|
||||
}
|
||||
},
|
||||
"presets": {
|
||||
"title": "Προκαθορισμένα",
|
||||
"description": "Για κάθε προκαθορισμένο, δώστε την επιθυμητή θερμοκρασία (0 για να αγνοηθεί το προκαθορισμένο)",
|
||||
"data": {
|
||||
"eco_temp": "Θερμοκρασία στο προκαθορισμένο Eco",
|
||||
"comfort_temp": "Θερμοκρασία στο προκαθορισμένο Comfort",
|
||||
"boost_temp": "Θερμοκρασία στο προκαθορισμένο Boost",
|
||||
"frost_temp": "Θερμοκρασία στο προκαθορισμένο Frost protection",
|
||||
"eco_ac_temp": "Θερμοκρασία στο προκαθορισμένο Eco για λειτουργία AC",
|
||||
"comfort_ac_temp": "Θερμοκρασία στο προκαθορισμένο Comfort για λειτουργία AC",
|
||||
"boost_ac_temp": "Θερμοκρασία στο προκαθορισμένο Boost για λειτουργία AC"
|
||||
}
|
||||
},
|
||||
"window": {
|
||||
"title": "Διαχείριση Παραθύρων",
|
||||
"description": "Ανοίξτε τη διαχείριση παραθύρων.\nΑφήστε το αντίστοιχο entity_id κενό αν δεν χρησιμοποιείται\nΜπορείτε επίσης να ρυθμίσετε αυτόματη ανίχνευση ανοίγματος παραθύρου με βάση τη μείωση της θερμοκρασίας",
|
||||
"data": {
|
||||
"window_sensor_entity_id": "Ταυτότητα οντότητας αισθητήρα παραθύρου",
|
||||
"window_delay": "Καθυστέρηση αισθητήρα παραθύρου (δευτερόλεπτα)",
|
||||
"window_auto_open_threshold": "Κατώφλι μείωσης θερμοκρασίας για αυτόματη ανίχνευση ανοίγματος παραθύρου (σε °/λεπτό)",
|
||||
"window_auto_close_threshold": "Κατώφλι αύξησης θερμοκρασίας για τέλος αυτόματης ανίχνευσης (σε °/λεπτό)",
|
||||
"window_auto_max_duration": "Μέγιστη διάρκεια αυτόματης ανίχνευσης ανοίγματος παραθύρου (σε λεπτά)"
|
||||
},
|
||||
"data_description": {
|
||||
"window_sensor_entity_id": "Αφήστε κενό αν δεν πρέπει να χρησιμοποιηθεί αισθητήρας παραθύρου",
|
||||
"window_delay": "Η καθυστέρηση σε δευτερόλεπτα πριν ληφθεί υπόψη η ανίχνευση του αισθητήρα",
|
||||
"window_auto_open_threshold": "Συνιστώμενη τιμή: μεταξύ 0.05 και 0.1. Αφήστε κενό αν δεν χρησιμοποιείται αυτόματη ανίχνευση ανοίγματος παραθύρου",
|
||||
"window_auto_close_threshold": "Συνιστώμενη τιμή: 0. Αφήστε κενό αν δεν χρησιμοποιείται αυτόματη ανίχνευση ανοίγματος παραθύρου",
|
||||
"window_auto_max_duration": "Συνιστώμενη τιμή: 60 (μία ώρα). Αφήστε κενό αν δεν χρησιμοποιείται αυτόματη ανίχνευση ανοίγματος παραθύρου"
|
||||
}
|
||||
},
|
||||
"motion": {
|
||||
"title": "Διαχείριση Κίνησης",
|
||||
"description": "Διαχείριση αισθητήρα κίνησης. Το προκαθορισμένο μπορεί να αλλάζει αυτόματα ανάλογα με ανίχνευση κίνησης\nΑφήστε το αντίστοιχο entity_id κενό αν δεν χρησιμοποιείται.\nΟι επιλογές motion_preset και no_motion_preset πρέπει να οριστούν στο αντίστοιχο όνομα προκαθορισμένου",
|
||||
"data": {
|
||||
"motion_sensor_entity_id": "Ταυτότητα οντότητας αισθητήρα κίνησης",
|
||||
"motion_delay": "Καθυστέρηση ενεργοποίησης",
|
||||
"motion_off_delay": "Καθυστέρηση απενεργοποίησης",
|
||||
"motion_preset": "Προκαθορισμένο κίνησης",
|
||||
"no_motion_preset": "Προκαθορισμένο χωρίς κίνηση"
|
||||
},
|
||||
"data_description": {
|
||||
"motion_sensor_entity_id": "Η ταυτότητα οντότητας του αισθητήρα κίνησης",
|
||||
"motion_delay": "Καθυστέρηση ενεργοποίησης κίνησης (δευτερόλεπτα)",
|
||||
"motion_off_delay": "Καθυστέρηση απενεργοποίησης κίνησης (δευτερόλεπτα)",
|
||||
"motion_preset": "Το προκαθορισμένο που θα χρησιμοποιηθεί όταν ανιχνευθεί κίνηση",
|
||||
"no_motion_preset": "Το προκαθορισμένο που θα χρησιμοποιηθεί όταν δεν ανιχνευθεί κίνηση"
|
||||
}
|
||||
},
|
||||
"power": {
|
||||
"title": "Διαχείριση Ενέργειας",
|
||||
"description": "Χαρακτηριστικά διαχείρισης ενέργειας.\nΔίνει τον αισθητήρα ενέργειας και τον μέγιστο αισθητήρα ενέργειας του σπιτιού σας.\nΣτη συνέχεια καθορίστε την κατανάλωση ενέργειας του θερμαντήρα όταν είναι ενεργοποιημένος.\nΌλοι οι αισθητήρες και η ισχύς της συσκευής πρέπει να έχουν την ίδια μονάδα (kW ή W).\nΑφήστε το αντίστοιχο entity_id κενό αν δεν χρησιμοποιείται.",
|
||||
"data": {
|
||||
"power_sensor_entity_id": "Ταυτότητα οντότητας αισθητήρα ενέργειας",
|
||||
"max_power_sensor_entity_id": "Ταυτότητα οντότητας αισθητήρα μέγιστης ενέργειας",
|
||||
"power_temp": "Θερμοκρασία για Αποβολή Ενέργειας"
|
||||
}
|
||||
},
|
||||
"presence": {
|
||||
"title": "Διαχείριση Παρουσίας",
|
||||
"description": "Χαρακτηριστικά διαχείρισης παρουσίας.\nΔίνει έναν αισθητήρα παρουσίας του σπιτιού σας (αληθές αν κάποιος είναι παρών).\nΣτη συνέχεια καθορίστε είτε το προκαθορισμένο που θα χρησιμοποιηθεί όταν ο αισθητήρας παρουσίας είναι ψευδής ή την απόκλιση στη θερμοκρασία που θα εφαρμοστεί.\nΑν δοθεί προκαθορισμένο, η απόκλιση δεν θα χρησιμοποιηθεί.\nΑφήστε το αντίστοιχο entity_id κενό αν δεν χρησιμοποιείται.",
|
||||
"data": {
|
||||
"presence_sensor_entity_id": "Ταυτότητα οντότητας αισθητήρα παρουσίας",
|
||||
"eco_away_temp": "Θερμοκρασία στο προκαθορισμένο Eco όταν δεν υπάρχει παρουσία",
|
||||
"comfort_away_temp": "Θερμοκρασία στο προκαθορισμένο Comfort όταν δεν υπάρχει παρουσία",
|
||||
"boost_away_temp": "Θερμοκρασία στο προκαθορισμένο Boost όταν δεν υπάρχει παρουσία",
|
||||
"frost_away_temp": "Θερμοκρασία στο προκαθορισμένο Frost protection όταν δεν υπάρχει παρουσία",
|
||||
"eco_ac_away_temp": "Θερμοκρασία στο προκαθορισμένο Eco όταν δεν υπάρχει παρουσία σε λειτουργία AC",
|
||||
"comfort_ac_away_temp": "Θερμοκρασία στο προκαθορισμένο Comfort όταν δεν υπάρχει παρουσία σε λειτουργία AC",
|
||||
"boost_ac_away_temp": "Θερμοκρασία στο προκαθορισμένο Boost όταν δεν υπάρχει παρουσία σε λειτουργία AC"
|
||||
}
|
||||
},
|
||||
"advanced": {
|
||||
"title": "Προχωρημένες Παράμετροι",
|
||||
"description": "Διαμόρφωση των προχωρημένων παραμέτρων. Αφήστε τις προεπιλεγμένες τιμές αν δεν γνωρίζετε τι κάνετε.\nΑυτές οι παράμετροι μπορούν να οδηγήσουν σε πολύ κακή ρύθμιση θερμοκρασίας ή ενέργειας.",
|
||||
"data": {
|
||||
"minimal_activation_delay": "Ελάχιστη καθυστέρηση ενεργοποίησης",
|
||||
"security_delay_min": "Καθυστέρηση ασφαλείας (σε λεπτά)",
|
||||
"security_min_on_percent": "Ελάχιστο ποσοστό ισχύος για ενεργοποίηση λειτουργίας ασφαλείας",
|
||||
"security_default_on_percent": "Ποσοστό ισχύος για χρήση σε λειτουργία ασφαλείας"
|
||||
},
|
||||
"data_description": {
|
||||
"minimal_activation_delay": "Καθυστέρηση σε δευτερόλεπτα κάτω από την οποία η συσκευή δεν θα ενεργοποιηθεί",
|
||||
"security_delay_min": "Μέγιστη επιτρεπτή καθυστέρηση σε λεπτά μεταξύ δύο μετρήσεων θερμοκρασίας. Πέρα από αυτή την καθυστέρηση, ο θερμοστάτης θα μεταβεί σε κατάσταση ασφαλείας",
|
||||
"security_min_on_percent": "Ελάχιστη τιμή ποσοστού θέρμανσης για την ενεργοποίηση του προεπιλεγμένου ασφάλειας. Κάτω από αυτό το ποσοστό ισχύος το θερμοστάτη δεν θα πάει στο προεπιλεγμένο ασφάλειας.",
|
||||
"security_default_on_percent": "Η προεπιλεγμένη τιμή ποσοστού ισχύος θέρμανσης στο προεπιλεγμένο ασφάλειας. Ορίστε σε 0 για να απενεργοποιήσετε τη θερμάστρα στο παρόν ασφάλειας."
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"unknown": "Απρόσμενο σφάλμα",
|
||||
"unknown_entity": "Άγνωστο αναγνωριστικό οντότητας",
|
||||
"window_open_detection_method": "Πρέπει να χρησιμοποιείται μόνο μία μέθοδος ανίχνευσης ανοιχτού παραθύρου. Χρησιμοποιήστε αισθητήρα ή αυτόματη ανίχνευση μέσω του κατωφλίου θερμοκρασίας, αλλά όχι και τα δύο"
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "Η συσκευή έχει ήδη ρυθμιστεί"
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"flow_title": "Διαμόρφωση Ευέλικτου Θερμοστάτη",
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "Προσθήκη νέου Ευέλικτου Θερμοστάτη",
|
||||
"description": "Κύρια υποχρεωτικά χαρακτηριστικά",
|
||||
"data": {
|
||||
"name": "Όνομα",
|
||||
"thermostat_type": "Τύπος θερμοστάτη",
|
||||
"temperature_sensor_entity_id": "Ταυτότητα οντότητας αισθητήρα θερμοκρασίας",
|
||||
"external_temperature_sensor_entity_id": "Ταυτότητα οντότητας εξωτερικού αισθητήρα θερμοκρασίας",
|
||||
"cycle_min": "Διάρκεια κύκλου (λεπτά)",
|
||||
"temp_min": "Ελάχιστη επιτρεπόμενη θερμοκρασία",
|
||||
"temp_max": "Μέγιστη επιτρεπόμενη θερμοκρασία",
|
||||
"device_power": "Ισχύς συσκευής (kW)",
|
||||
"use_window_feature": "Χρήση ανίχνευσης παραθύρου",
|
||||
"use_motion_feature": "Χρήση ανίχνευσης κίνησης",
|
||||
"use_power_feature": "Χρήση διαχείρισης ενέργειας",
|
||||
"use_presence_feature": "Χρήση ανίχνευσης παρουσίας"
|
||||
}
|
||||
},
|
||||
"type": {
|
||||
"title": "Συνδεδεμένες οντότητες",
|
||||
"description": "Χαρακτηριστικά συνδεδεμένων οντοτήτων",
|
||||
"data": {
|
||||
"heater_entity_id": "1ος διακόπτης θερμαντήρα",
|
||||
"heater_entity2_id": "2ος διακόπτης θερμαντήρα",
|
||||
"heater_entity3_id": "3ος διακόπτης θερμαντήρα",
|
||||
"heater_entity4_id": "4ος διακόπτης θερμαντήρα",
|
||||
"proportional_function": "Αλγόριθμος",
|
||||
"climate_entity_id": "1η υποκείμενη κλιματική οντότητα",
|
||||
"climate_entity2_id": "2η υποκείμενη κλιματική οντότητα",
|
||||
"climate_entity3_id": "3η υποκείμενη κλιματική οντότητα",
|
||||
"climate_entity4_id": "4η υποκείμενη κλιματική οντότητα",
|
||||
"ac_mode": "Λειτουργία AC",
|
||||
"valve_entity_id": "1ος αριθμός βαλβίδας",
|
||||
"valve_entity2_id": "2ος αριθμός βαλβίδας",
|
||||
"valve_entity3_id": "3ος αριθμός βαλβίδας",
|
||||
"valve_entity4_id": "4ος αριθμός βαλβίδας",
|
||||
"auto_regulation_mode": "Αυτορύθμιση",
|
||||
"auto_regulation_dtemp": "Όριο ρύθμισης",
|
||||
"auto_regulation_periode_min": "Ελάχιστη περίοδος ρύθμισης",
|
||||
"inverse_switch_command": "Αντίστροφη εντολή διακόπτη",
|
||||
"auto_fan_mode": "Auto fan mode"
|
||||
},
|
||||
"data_description": {
|
||||
"heater_entity_id": "Υποχρεωτική ταυτότητα οντότητας θερμαντήρα",
|
||||
"heater_entity2_id": "Προαιρετική ταυτότητα οντότητας 2ου θερμαντήρα. Αφήστε το κενό αν δεν χρησιμοποιείται",
|
||||
"heater_entity3_id": "Προαιρετική ταυτότητα οντότητας 3ου θερμαντήρα. Αφήστε το κενό αν δεν χρησιμοποιείται",
|
||||
"heater_entity4_id": "Προαιρετική ταυτότητα οντότητας 4ου θερμαντήρα. Αφήστε το κενό αν δεν χρησιμοποιείται",
|
||||
"proportional_function": "Αλγόριθμος που θα χρησιμοποιηθεί (TPI είναι ο μόνος για τώρα)",
|
||||
"climate_entity_id": "Ταυτότητα οντότητας υποκείμενου κλίματος",
|
||||
"climate_entity2_id": "Ταυτότητα οντότητας 2ου υποκείμενου κλίματος",
|
||||
"climate_entity3_id": "Ταυτότητα οντότητας 3ου υποκείμενου κλίματος",
|
||||
"climate_entity4_id": "Ταυτότητα οντότητας 4ου υποκείμενου κλίματος",
|
||||
"ac_mode": "Χρήση της λειτουργίας Κλιματισμού (AC)",
|
||||
"valve_entity_id": "Ταυτότητα οντότητας 1ης βαλβίδας",
|
||||
"valve_entity2_id": "Ταυτότητα οντότητας 2ης βαλβίδας",
|
||||
"valve_entity3_id": "Ταυτότητα οντότητας 3ης βαλβίδας",
|
||||
"valve_entity4_id": "Ταυτότητα οντότητας 4ης βαλβίδας",
|
||||
"auto_regulation_mode": "Αυτόματη ρύθμιση της στοχευόμενης θερμοκρασίας",
|
||||
"auto_regulation_dtemp": "Το κατώφλι σε °C κάτω από το οποίο η αλλαγή της θερμοκρασίας δεν θα αποστέλλεται",
|
||||
"auto_regulation_periode_min": "Διάρκεια σε λεπτά μεταξύ δύο ενημερώσεων ρύθμισης",
|
||||
"inverse_switch_command": "Για διακόπτες με πιλοτικό καλώδιο και δίοδο μπορεί να χρειαστεί να αντιστραφεί η εντολή",
|
||||
"auto_fan_mode": "Automatically activate fan when huge heating/cooling is necessary"
|
||||
}
|
||||
},
|
||||
"tpi": {
|
||||
"title": "TPI",
|
||||
"description": "Χαρακτηριστικά Χρονικού Αναλογικού Ολοκληρωτικού (TPI)",
|
||||
"data": {
|
||||
"tpi_coef_int": "Συντελεστής που θα χρησιμοποιηθεί για την εσωτερική διαφορά θερμοκρασίας",
|
||||
"tpi_coef_ext": "Συντελεστής που θα χρησιμοποιηθεί για την εξωτερική διαφορά θερμοκρασίας"
|
||||
}
|
||||
},
|
||||
"presets": {
|
||||
"title": "Προεπιλογές",
|
||||
"description": "Για κάθε προεπιλογή, δώστε τη στοχευόμενη θερμοκρασία (0 για να αγνοηθεί η προεπιλογή)",
|
||||
"data": {
|
||||
"eco_temp": "Θερμοκρασία στην οικονομική προεπιλογή",
|
||||
"comfort_temp": "Θερμοκρασία στην άνετη προεπιλογή",
|
||||
"boost_temp": "Θερμοκρασία στην ενισχυμένη προεπιλογή",
|
||||
"frost_temp": "Θερμοκρασία στο προκαθορισμένο Frost protection",
|
||||
"eco_ac_temp": "Θερμοκρασία στην οικονομική προεπιλογή για τη λειτουργία AC",
|
||||
"comfort_ac_temp": "Θερμοκρασία στην άνετη προεπιλογή για τη λειτουργία AC",
|
||||
"boost_ac_temp": "Θερμοκρασία στην ενισχυμένη προεπιλογή για τη λειτουργία AC"
|
||||
}
|
||||
},
|
||||
"window": {
|
||||
"title": "Διαχείριση παραθύρου",
|
||||
"description": "Διαχείριση ανοιχτού παραθύρου.\nΑφήστε την αντίστοιχη ταυτότητα οντότητας κενή αν δεν χρησιμοποιείται\nΜπορείτε επίσης να διαμορφώσετε την αυτόματη ανίχνευση ανοίγματος παραθύρου βάσει της μείωσης της θερμοκρασίας",
|
||||
"data": {
|
||||
"window_sensor_entity_id": "Ταυτότητα οντότητας αισθητήρα παραθύρου",
|
||||
"window_delay": "Καθυστέρηση αισθητήρα παραθύρου (δευτερόλεπτα)",
|
||||
"window_auto_open_threshold": "Όριο μείωσης θερμοκρασίας για αυτόματη ανίχνευση ανοίγματος παραθύρου (σε °/λεπτό)",
|
||||
"window_auto_close_threshold": "Όριο αύξησης θερμοκρασίας για τέλος αυτόματης ανίχνευσης (σε °/λεπτό)",
|
||||
"window_auto_max_duration": "Μέγιστη διάρκεια αυτόματης ανίχνευσης ανοίγματος παραθύρου (σε λεπτά)"
|
||||
},
|
||||
"data_description": {
|
||||
"window_sensor_entity_id": "Αφήστε κενό αν δεν πρέπει να χρησιμοποιηθεί αισθητήρας παραθύρου",
|
||||
"window_delay": "Η καθυστέρηση σε δευτερόλεπτα πριν ληφθεί υπόψη η ανίχνευση αισθητήρα",
|
||||
"window_auto_open_threshold": "Συνιστώμενη τιμή: μεταξύ 0.05 και 0.1. Αφήστε κενό αν δεν χρησιμοποιείται αυτόματη ανίχνευση ανοίγματος παραθύρου",
|
||||
"window_auto_close_threshold": "Συνιστώμενη τιμή: 0. Αφήστε κενό αν δεν χρησιμοποιείται αυτόματη ανίχνευση ανοίγματος παραθύρου",
|
||||
"window_auto_max_duration": "Συνιστώμενη τιμή: 60 (μία ώρα). Αφήστε κενό αν δεν χρησιμοποιείται αυτόματη ανίχνευση ανοίγματος παραθύρου"
|
||||
}
|
||||
},
|
||||
"motion": {
|
||||
"title": "Διαχείριση κίνησης",
|
||||
"description": "Διαχείριση αισθητήρα κίνησης. Ο προεπιλεγμένος τρόπος μπορεί να αλλάξει αυτόματα ανάλογα με την ανίχνευση κίνησης\nΑφήστε το αντίστοιχο entity_id κενό αν δεν χρησιμοποιείται.\nΤα motion_preset και no_motion_preset πρέπει να οριστούν στο αντίστοιχο όνομα προεπιλογής",
|
||||
"data": {
|
||||
"motion_sensor_entity_id": "Ανιχνευτής κίνησης entity id",
|
||||
"motion_delay": "Καθυστέρηση ενεργοποίησης",
|
||||
"motion_off_delay": "Καθυστέρηση απενεργοποίησης",
|
||||
"motion_preset": "Προεπιλογή κίνησης",
|
||||
"no_motion_preset": "Προεπιλογή χωρίς κίνηση"
|
||||
},
|
||||
"data_description": {
|
||||
"motion_sensor_entity_id": "Το entity id του ανιχνευτή κίνησης",
|
||||
"motion_delay": "Καθυστέρηση ενεργοποίησης κίνησης (δευτερόλεπτα)",
|
||||
"motion_off_delay": "Καθυστέρηση απενεργοποίησης κίνησης (δευτερόλεπτα)",
|
||||
"motion_preset": "Η προεπιλογή που χρησιμοποιείται όταν ανιχνεύεται κίνηση",
|
||||
"no_motion_preset": "Η προεπιλογή που χρησιμοποιείται όταν δεν ανιχνεύεται κίνηση"
|
||||
}
|
||||
},
|
||||
"power": {
|
||||
"title": "Διαχείριση Ενέργειας",
|
||||
"description": "Χαρακτηριστικά διαχείρισης ενέργειας.\nΠαρέχει τον αισθητήρα ισχύος και τον μέγιστο αισθητήρα ισχύος του σπιτιού σας.\nΣτη συνέχεια καθορίστε την κατανάλωση ενέργειας του θερμαντήρα όταν είναι ενεργοποιημένος.\nΌλοι οι αισθητήρες και η ισχύς της συσκευής πρέπει να έχουν την ίδια μονάδα (kW ή W).\nΑφήστε το αντίστοιχο entity_id κενό εάν δεν χρησιμοποιείται.",
|
||||
"data": {
|
||||
"power_sensor_entity_id": "Ταυτότητα οντότητας αισθητήρα ισχύος",
|
||||
"max_power_sensor_entity_id": "Ταυτότητα οντότητας αισθητήρα μέγιστης ισχύος",
|
||||
"power_temp": "Θερμοκρασία για Μείωση Ισχύος"
|
||||
}
|
||||
},
|
||||
"presence": {
|
||||
"title": "Διαχείριση Παρουσίας",
|
||||
"description": "Χαρακτηριστικά διαχείρισης παρουσίας.\nΠαρέχει έναν αισθητήρα παρουσίας του σπιτιού σας (αληθές εάν κάποιος είναι παρών).\nΣτη συνέχεια καθορίστε είτε το προεπιλεγμένο πρόγραμμα που θα χρησιμοποιηθεί όταν ο αισθητήρας παρουσίας είναι ψευδής είτε την θερμοκρασιακή διαφορά που θα εφαρμοστεί.\nΕάν δίνεται προεπιλογή, η διαφορά δεν θα χρησιμοποιηθεί.\nΑφήστε το αντίστοιχο entity_id κενό εάν δεν χρησιμοποιείται.",
|
||||
"data": {
|
||||
"presence_sensor_entity_id": "Ταυτότητα οντότητας αισθητήρα παρουσίας (αληθές είναι παρών)",
|
||||
"eco_away_temp": "Θερμοκρασία στο πρόγραμμα Eco όταν δεν υπάρχει παρουσία",
|
||||
"comfort_away_temp": "Θερμοκρασία στο πρόγραμμα Comfort όταν δεν υπάρχει παρουσία",
|
||||
"boost_away_temp": "Θερμοκρασία στο πρόγραμμα Boost όταν δεν υπάρχει παρουσία",
|
||||
"frost_away_temp": "Θερμοκρασία στο προκαθορισμένο Frost protection όταν δεν υπάρχει παρουσία",
|
||||
"eco_ac_away_temp": "Θερμοκρασία στο πρόγραμμα Eco όταν δεν υπάρχει παρουσία σε λειτουργία AC",
|
||||
"comfort_ac_away_temp": "Θερμοκρασία στο πρόγραμμα Comfort όταν δεν υπάρχει παρουσία σε λειτουργία AC",
|
||||
"boost_ac_away_temp": "Θερμοκρασία στο πρόγραμμα Boost όταν δεν υπάρχει παρουσία σε λειτουργία AC"
|
||||
}
|
||||
},
|
||||
"advanced": {
|
||||
"title": "Προηγμένες Παράμετροι",
|
||||
"description": "Διαμόρφωση των προηγμένων παραμέτρων. Αφήστε τις προεπιλεγμένες τιμές εάν δεν γνωρίζετε τι κάνετε.\nΑυτές οι παράμετροι μπορούν να οδηγήσουν σε πολύ κακή ρύθμιση θερμοκρασίας ή ενέργειας.",
|
||||
"data": {
|
||||
"minimal_activation_delay": "Ελάχιστη καθυστέρηση ενεργοποίησης",
|
||||
"security_delay_min": "Καθυστέρηση ασφαλείας (σε λεπτά)",
|
||||
"security_min_on_percent": "Ελάχιστο ποσοστό ισχύος για τη λειτουργία ασφαλείας",
|
||||
"security_default_on_percent": "Ποσοστό ισχύος που θα χρησιμοποιηθεί στη λειτουργία ασφαλείας"
|
||||
},
|
||||
"data_description": {
|
||||
"minimal_activation_delay": "Καθυστέρηση σε δευτερόλεπτα κάτω από την οποία ο εξοπλισμός δεν θα ενεργοποιηθεί",
|
||||
"security_delay_min": "Μέγιστη επιτρεπόμενη καθυστέρηση σε λεπτά μεταξύ δύο μετρήσεων θερμοκρασίας. Πάνω από αυτή την καθυστέρηση, ο θερμοστάτης θα μεταβεί σε κατάσταση ασφαλείας",
|
||||
"security_min_on_percent": "Ελάχιστη τιμή ποσοστού θέρμανσης για ενεργοποίηση του προεπιλεγμένου ασφαλείας. Κάτω από αυτό το ποσοστό ισχύος, ο θερμοστάτης δεν θα μεταβεί στο προεπιλεγμένο ασφαλείας",
|
||||
"security_default_on_percent": "Η προεπιλεγμένη τιμή ποσοστού ισχύος θέρμανσης στο προεπιλεγμένο ασφαλείας. Ορίστε σε 0 για να απενεργοποιήσετε τη θερμάστρα στο παρόν ασφαλείας"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"unknown": "Απροσδόκητο λάθος",
|
||||
"unknown_entity": "Άγνωστο αναγνωριστικό οντότητας",
|
||||
"window_open_detection_method": "Πρέπει να χρησιμοποιηθεί μόνο μία μέθοδος ανίχνευσης ανοιχτού παραθύρου. Χρησιμοποιήστε αισθητήρα ή αυτόματη ανίχνευση μέσω κατωφλίου θερμοκρασίας αλλά όχι και τα δύο"
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "Η συσκευή έχει ήδη ρυθμιστεί"
|
||||
}
|
||||
},
|
||||
"selector": {
|
||||
"thermostat_type": {
|
||||
"options": {
|
||||
"thermostat_over_switch": "Θερμοστάτης πάνω σε διακόπτη",
|
||||
"thermostat_over_climate": "Θερμοστάτης πάνω σε κλίμα",
|
||||
"thermostat_over_valve": "Θερμοστάτης πάνω σε βαλβίδα"
|
||||
}
|
||||
},
|
||||
"auto_regulation_mode": {
|
||||
"options": {
|
||||
"auto_regulation_slow": "Αργή",
|
||||
"auto_regulation_strong": "Δυνατή",
|
||||
"auto_regulation_medium": "Μέτρια",
|
||||
"auto_regulation_light": "Ελαφριά",
|
||||
"auto_regulation_expert": "Εμπειρογνώμων",
|
||||
"auto_regulation_none": "Χωρίς αυτόματη ρύθμιση"
|
||||
}
|
||||
},
|
||||
"auto_fan_mode": {
|
||||
"options": {
|
||||
"auto_fan_none": "No auto fan",
|
||||
"auto_fan_low": "Low",
|
||||
"auto_fan_medium": "Medium",
|
||||
"auto_fan_high": "High",
|
||||
"auto_fan_turbo": "Turbo"
|
||||
}
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"climate": {
|
||||
"versatile_thermostat": {
|
||||
"state_attributes": {
|
||||
"preset_mode": {
|
||||
"state": {
|
||||
"power": "Μείωση",
|
||||
"security": "Ασφάλεια",
|
||||
"none": "Χειροκίνητο"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,166 +4,212 @@
|
||||
"flow_title": "Versatile Thermostat configuration",
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "Type of Versatile Thermostat",
|
||||
"data": {
|
||||
"thermostat_type": "Thermostat type"
|
||||
},
|
||||
"data_description": {
|
||||
"thermostat_type": "Only one central configuration type is possible"
|
||||
}
|
||||
},
|
||||
"menu": {
|
||||
"title": "Menu",
|
||||
"description": "Configure your thermostat. You will be able to finalize the configuration when all required parameters are entered.",
|
||||
"menu_options": {
|
||||
"main": "Main attributes",
|
||||
"central_boiler": "Central boiler",
|
||||
"type": "Underlyings",
|
||||
"tpi": "TPI parameters",
|
||||
"features": "Features",
|
||||
"presets": "Presets",
|
||||
"window": "Window detection",
|
||||
"motion": "Motion detection",
|
||||
"power": "Power management",
|
||||
"presence": "Presence detection",
|
||||
"advanced": "Advanced parameters",
|
||||
"auto_start_stop": "Auto start and stop",
|
||||
"finalize": "All done",
|
||||
"configuration_not_complete": "Configuration not complete"
|
||||
}
|
||||
},
|
||||
"main": {
|
||||
"title": "Add new Versatile Thermostat",
|
||||
"description": "Main mandatory attributes",
|
||||
"data": {
|
||||
"name": "Name",
|
||||
"thermostat_type": "Thermostat type",
|
||||
"temperature_sensor_entity_id": "Temperature sensor entity id",
|
||||
"external_temperature_sensor_entity_id": "External temperature sensor entity id",
|
||||
"temperature_sensor_entity_id": "Room temperature",
|
||||
"last_seen_temperature_sensor_entity_id": "Last seen room temperature datetime",
|
||||
"external_temperature_sensor_entity_id": "Outdoor temperature sensor entity id",
|
||||
"cycle_min": "Cycle duration (minutes)",
|
||||
"temp_min": "Minimal temperature allowed",
|
||||
"temp_max": "Maximal temperature allowed",
|
||||
"temp_min": "Minimum temperature allowed",
|
||||
"temp_max": "Maximum temperature allowed",
|
||||
"step_temperature": "Temperature step",
|
||||
"device_power": "Device power",
|
||||
"use_central_mode": "Enable the control by central entity (requires central config). Check to enable the control of the VTherm with the select central_mode entities.",
|
||||
"use_main_central_config": "Use additional central main configuration. Check to use the central main configuration (outdoor temperature, min, max, step, ...).",
|
||||
"used_by_controls_central_boiler": "Used by central boiler. Check if this VTherm should have control on the central boiler"
|
||||
},
|
||||
"data_description": {
|
||||
"temperature_sensor_entity_id": "Room temperature sensor entity id",
|
||||
"last_seen_temperature_sensor_entity_id": "Last seen room temperature sensor entity id. Should be datetime sensor",
|
||||
"external_temperature_sensor_entity_id": "Outdoor temperature sensor entity id. Not used if central configuration is selected"
|
||||
}
|
||||
},
|
||||
"features": {
|
||||
"title": "Features",
|
||||
"description": "Thermostat features",
|
||||
"data": {
|
||||
"use_window_feature": "Use window detection",
|
||||
"use_motion_feature": "Use motion detection",
|
||||
"use_power_feature": "Use power management",
|
||||
"use_presence_feature": "Use presence detection"
|
||||
"use_presence_feature": "Use presence detection",
|
||||
"use_central_boiler_feature": "Use a central boiler. Check to add a control to your central boiler. You will have to configure the VTherm which will have a control of the central boiler after selecting this checkbox to take effect. If one VTherm requires heating, the boiler will be turned on. If no VTherm requires heating, the boiler will be turned off. Commands for turning on/off the central boiler are given in the related configuration page",
|
||||
"use_auto_start_stop_feature": "Use the auto start and stop feature"
|
||||
}
|
||||
},
|
||||
"type": {
|
||||
"title": "Linked entities",
|
||||
"description": "Linked entities attributes",
|
||||
"data": {
|
||||
"heater_entity_id": "1rst heater switch",
|
||||
"heater_entity2_id": "2nd heater switch",
|
||||
"heater_entity3_id": "3rd heater switch",
|
||||
"heater_entity4_id": "4th heater switch",
|
||||
"underlying_entity_ids": "The device(s) to be controlled",
|
||||
"heater_keep_alive": "Switch keep-alive interval in seconds",
|
||||
"proportional_function": "Algorithm",
|
||||
"climate_entity_id": "1rst underlying climate",
|
||||
"climate_entity2_id": "2nd underlying climate",
|
||||
"climate_entity3_id": "3rd underlying climate",
|
||||
"climate_entity4_id": "4th underlying climate",
|
||||
"ac_mode": "AC mode",
|
||||
"valve_entity_id": "1rst valve number",
|
||||
"valve_entity2_id": "2nd valve number",
|
||||
"valve_entity3_id": "3rd valve number",
|
||||
"valve_entity4_id": "4th valve number",
|
||||
"auto_regulation_mode": "Self-regulation",
|
||||
"auto_regulation_dtemp": "Regulation threshold",
|
||||
"auto_regulation_periode_min": "Regulation minimal period",
|
||||
"inverse_switch_command": "Inverse switch command"
|
||||
"auto_regulation_periode_min": "Regulation minimum period",
|
||||
"auto_regulation_use_device_temp": "Use internal temperature of the underlying",
|
||||
"inverse_switch_command": "Inverse switch command",
|
||||
"auto_fan_mode": "Auto fan mode"
|
||||
},
|
||||
"data_description": {
|
||||
"heater_entity_id": "Mandatory heater entity id",
|
||||
"heater_entity2_id": "Optional 2nd Heater entity id. Leave empty if not used",
|
||||
"heater_entity3_id": "Optional 3rd Heater entity id. Leave empty if not used",
|
||||
"heater_entity4_id": "Optional 4th Heater entity id. Leave empty if not used",
|
||||
"underlying_entity_ids": "The device(s) to be controlled - 1 is required",
|
||||
"heater_keep_alive": "Optional heater switch state refresh interval. Leave empty if not required.",
|
||||
"proportional_function": "Algorithm to use (TPI is the only one for now)",
|
||||
"climate_entity_id": "Underlying climate entity id",
|
||||
"climate_entity2_id": "2nd underlying climate entity id",
|
||||
"climate_entity3_id": "3rd underlying climate entity id",
|
||||
"climate_entity4_id": "4th underlying climate entity id",
|
||||
"ac_mode": "Use the Air Conditioning (AC) mode",
|
||||
"valve_entity_id": "1rst valve number entity id",
|
||||
"valve_entity2_id": "2nd valve number entity id",
|
||||
"valve_entity3_id": "3rd valve number entity id",
|
||||
"valve_entity4_id": "4th valve number entity id",
|
||||
"auto_regulation_mode": "Auto adjustment of the target temperature",
|
||||
"auto_regulation_dtemp": "The threshold in ° under which the temperature change will not be send",
|
||||
"auto_regulation_dtemp": "The threshold in ° (or % for valve) under which the temperature change will not be sent",
|
||||
"auto_regulation_periode_min": "Duration in minutes between two regulation update",
|
||||
"inverse_switch_command": "For switch with pilot wire and diode you may need to inverse the command"
|
||||
"auto_regulation_use_device_temp": "Use the eventual internal temperature sensor of the underlying to speedup the self-regulation",
|
||||
"inverse_switch_command": "For switch with pilot wire and diode you may need to inverse the command",
|
||||
"auto_fan_mode": "Automatically activate fan when huge heating/cooling is necessary"
|
||||
}
|
||||
},
|
||||
"tpi": {
|
||||
"title": "TPI",
|
||||
"description": "Time Proportional Integral attributes",
|
||||
"data": {
|
||||
"tpi_coef_int": "coef_int",
|
||||
"tpi_coef_ext": "coef_ext",
|
||||
"use_tpi_central_config": "Use central TPI configuration"
|
||||
},
|
||||
"data_description": {
|
||||
"tpi_coef_int": "Coefficient to use for internal temperature delta",
|
||||
"tpi_coef_ext": "Coefficient to use for external temperature delta"
|
||||
"tpi_coef_ext": "Coefficient to use for external temperature delta",
|
||||
"use_tpi_central_config": "Check to use the central TPI configuration. Uncheck to use a specific TPI configuration for this VTherm"
|
||||
}
|
||||
},
|
||||
"presets": {
|
||||
"title": "Presets",
|
||||
"description": "For each presets, give the target temperature (0 to ignore preset)",
|
||||
"description": "Select if the thermostat will use central preset - deselect for the thermostat to have its own presets",
|
||||
"data": {
|
||||
"eco_temp": "Temperature in Eco preset",
|
||||
"comfort_temp": "Temperature in Comfort preset",
|
||||
"boost_temp": "Temperature in Boost preset",
|
||||
"eco_ac_temp": "Temperature in Eco preset for AC mode",
|
||||
"comfort_ac_temp": "Temperature in Comfort preset for AC mode",
|
||||
"boost_ac_temp": "Temperature in Boost preset for AC mode"
|
||||
"use_presets_central_config": "Use central presets configuration"
|
||||
}
|
||||
},
|
||||
"window": {
|
||||
"title": "Window management",
|
||||
"description": "Open window management.\nLeave corresponding entity_id empty if not used\nYou can also configure automatic window open detection based on temperature decrease",
|
||||
"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_auto_open_threshold": "Temperature decrease threshold for automatic window open detection (in °/min)",
|
||||
"window_auto_close_threshold": "Temperature increase threshold for end of automatic detection (in °/min)",
|
||||
"window_auto_max_duration": "Maximum duration of automatic window open detection (in min)"
|
||||
"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)",
|
||||
"use_window_central_config": "Use central window configuration",
|
||||
"window_action": "Action"
|
||||
},
|
||||
"data_description": {
|
||||
"window_sensor_entity_id": "Leave empty if no window sensor should be use",
|
||||
"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 0.05 and 0.1. Leave empty if automatic window open detection is not use",
|
||||
"window_auto_close_threshold": "Recommended value: 0. Leave empty if automatic window open detection is not use",
|
||||
"window_auto_max_duration": "Recommended value: 60 (one hour). Leave empty if automatic window open detection is not use"
|
||||
"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",
|
||||
"use_window_central_config": "Select to use the central window configuration. Deselect to use a specific window configuration for this VTherm",
|
||||
"window_action": "Action to perform if window is deteted as open"
|
||||
}
|
||||
},
|
||||
"motion": {
|
||||
"title": "Motion management",
|
||||
"description": "Motion sensor management. Preset can switch automatically depending of a motion detection\nLeave corresponding entity_id empty if not used.\nmotion_preset and no_motion_preset should be set to the corresponding preset name",
|
||||
"description": "Motion sensor management. Preset can switch automatically depending on motion detection\nmotion_preset and no_motion_preset should be set to the corresponding preset name",
|
||||
"data": {
|
||||
"motion_sensor_entity_id": "Motion sensor entity id",
|
||||
"motion_delay": "Activation delay",
|
||||
"motion_off_delay": "Deactivation delay",
|
||||
"motion_preset": "Motion preset",
|
||||
"no_motion_preset": "No motion preset"
|
||||
"no_motion_preset": "No motion preset",
|
||||
"use_motion_central_config": "Use central motion configuration"
|
||||
},
|
||||
"data_description": {
|
||||
"motion_sensor_entity_id": "The entity id of the motion sensor",
|
||||
"motion_delay": "Motion activation activation delay (seconds)",
|
||||
"motion_delay": "Motion activation delay (seconds)",
|
||||
"motion_off_delay": "Motion deactivation delay (seconds)",
|
||||
"motion_preset": "Preset to use when motion is detected",
|
||||
"no_motion_preset": "Preset to use when no motion is detected"
|
||||
"no_motion_preset": "Preset to use when no motion is detected",
|
||||
"use_motion_central_config": "Check to use the central motion configuration. Uncheck to use a specific motion configuration for this VTherm"
|
||||
}
|
||||
},
|
||||
"power": {
|
||||
"title": "Power management",
|
||||
"description": "Power management attributes.\nGives the power and max power sensor of your home.\nThen specify the power consumption of the heater when on.\nAll sensors and device power should have the same unit (kW or W).\nLeave corresponding entity_id empty if not used.",
|
||||
"description": "Power management attributes.\nGives the power and max power sensor of your home.\nSpecify the power consumption of the heater when on.\nAll sensors and device power should use the same unit (kW or W).",
|
||||
"data": {
|
||||
"power_sensor_entity_id": "Power",
|
||||
"max_power_sensor_entity_id": "Max power",
|
||||
"power_temp": "Shedding temperature",
|
||||
"use_power_central_config": "Use central power configuration"
|
||||
},
|
||||
"data_description": {
|
||||
"power_sensor_entity_id": "Power sensor entity id",
|
||||
"max_power_sensor_entity_id": "Max power sensor entity id",
|
||||
"power_temp": "Temperature for Power shedding"
|
||||
"power_temp": "Temperature for Power shedding",
|
||||
"use_power_central_config": "Check to use the central power configuration. Uncheck to use a specific power configuration for this VTherm"
|
||||
}
|
||||
},
|
||||
"presence": {
|
||||
"title": "Presence management",
|
||||
"description": "Presence management attributes.\nGives the a presence sensor of your home (true is someone is present).\nThen specify either the preset to use when presence sensor is false or the offset in temperature to apply.\nIf preset is given, the offset will not be used.\nLeave corresponding entity_id empty if not used.",
|
||||
"description": "Presence management attributes.\nGives the a presence sensor of your home (true is someone is present) and give the corresponding temperature preset setting.",
|
||||
"data": {
|
||||
"presence_sensor_entity_id": "Presence sensor entity id",
|
||||
"eco_away_temp": "Temperature in Eco preset when no presence",
|
||||
"comfort_away_temp": "Temperature in Comfort preset when no presence",
|
||||
"boost_away_temp": "Temperature in Boost preset when no presence",
|
||||
"eco_ac_away_temp": "Temperature in Eco preset when no presence in AC mode",
|
||||
"comfort_ac_away_temp": "Temperature in Comfort preset when no presence in AC mode",
|
||||
"boost_ac_away_temp": "Temperature in Boost preset when no presence in AC mode"
|
||||
"presence_sensor_entity_id": "Presence sensor",
|
||||
"use_presence_central_config": "Use central presence temperature configuration. Deselect to use specific temperature entities"
|
||||
},
|
||||
"data_description": {
|
||||
"presence_sensor_entity_id": "Presence sensor entity id"
|
||||
}
|
||||
},
|
||||
"advanced": {
|
||||
"title": "Advanced parameters",
|
||||
"description": "Configuration of advanced parameters. Leave the default values if you don't know what you are doing.\nThis parameters can lead to a very bad temperature or power regulation.",
|
||||
"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": "Minimal activation delay",
|
||||
"security_delay_min": "Security delay (in minutes)",
|
||||
"security_min_on_percent": "Minimal power percent to enable security mode",
|
||||
"security_default_on_percent": "Power percent to use in security mode"
|
||||
"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",
|
||||
"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 mesures. Above this delay, the thermostat will turn to a security off state",
|
||||
"security_min_on_percent": "Minimal heating percent value for security preset activation. Below this amount of power percent the thermostat won't go into security preset",
|
||||
"security_default_on_percent": "The default heating power percent value in security preset. Set to 0 to switch off heater in security present"
|
||||
"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",
|
||||
"use_advanced_central_config": "Check to use the central advanced configuration. Uncheck to use a specific advanced configuration for this VTherm"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"unknown": "Unexpected error",
|
||||
"unknown_entity": "Unknown entity id",
|
||||
"window_open_detection_method": "Only one window open detection method should be used. Use sensor or automatic detection through temperature threshold but not both"
|
||||
"window_open_detection_method": "Only one window open detection method should be used. Use either window sensor or automatic detection through temperature threshold but not both",
|
||||
"no_central_config": "You cannot select 'use central configuration' because no central configuration was found. You need to create a Versatile Thermostat of type 'Central Configuration' to use it."
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "Device is already configured"
|
||||
@@ -173,166 +219,213 @@
|
||||
"flow_title": "Versatile Thermostat configuration",
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "Add new Versatile Thermostat",
|
||||
"title": "Type - {name}",
|
||||
"data": {
|
||||
"thermostat_type": "Thermostat type"
|
||||
},
|
||||
"data_description": {
|
||||
"thermostat_type": "Only one central configuration type is possible"
|
||||
}
|
||||
},
|
||||
"menu": {
|
||||
"title": "Menu",
|
||||
"description": "Configure your thermostat. You will be able to finalize the configuration when all required parameters are entered.",
|
||||
"menu_options": {
|
||||
"main": "Main attributes",
|
||||
"central_boiler": "Central boiler",
|
||||
"type": "Underlyings",
|
||||
"tpi": "TPI parameters",
|
||||
"features": "Features",
|
||||
"presets": "Presets",
|
||||
"window": "Window detection",
|
||||
"motion": "Motion detection",
|
||||
"power": "Power management",
|
||||
"presence": "Presence detection",
|
||||
"advanced": "Advanced parameters",
|
||||
"auto_start_stop": "Auto start and stop",
|
||||
"finalize": "All done",
|
||||
"configuration_not_complete": "Configuration not complete"
|
||||
}
|
||||
},
|
||||
"main": {
|
||||
"title": "Main - {name}",
|
||||
"description": "Main mandatory attributes",
|
||||
"data": {
|
||||
"name": "Name",
|
||||
"thermostat_type": "Thermostat type",
|
||||
"temperature_sensor_entity_id": "Temperature sensor entity id",
|
||||
"external_temperature_sensor_entity_id": "External temperature sensor entity id",
|
||||
"temperature_sensor_entity_id": "Room temperature",
|
||||
"last_seen_temperature_sensor_entity_id": "Last seen room temperature datetime",
|
||||
"external_temperature_sensor_entity_id": "Outdoor temperature sensor entity id",
|
||||
"cycle_min": "Cycle duration (minutes)",
|
||||
"temp_min": "Minimal temperature allowed",
|
||||
"temp_max": "Maximal temperature allowed",
|
||||
"device_power": "Device power (kW)",
|
||||
"temp_min": "Minimum temperature allowed",
|
||||
"temp_max": "Maximum temperature allowed",
|
||||
"step_temperature": "Temperature step",
|
||||
"device_power": "Device power",
|
||||
"use_central_mode": "Enable the control by central entity (requires central config). Check to enable the control of the VTherm with the select central_mode entities.",
|
||||
"use_main_central_config": "Use additional central main configuration. Check to use the central main configuration (outdoor temperature, min, max, step, ...).",
|
||||
"used_by_controls_central_boiler": "Used by central boiler. Check if this VTherm should have control on the central boiler"
|
||||
},
|
||||
"data_description": {
|
||||
"temperature_sensor_entity_id": "Room temperature sensor entity id",
|
||||
"last_seen_temperature_sensor_entity_id": "Last seen room temperature sensor entity id. Should be datetime sensor",
|
||||
"external_temperature_sensor_entity_id": "Outdoor temperature sensor entity id. Not used if central configuration is selected"
|
||||
}
|
||||
},
|
||||
"features": {
|
||||
"title": "Features - {name}",
|
||||
"description": "Thermostat features",
|
||||
"data": {
|
||||
"use_window_feature": "Use window detection",
|
||||
"use_motion_feature": "Use motion detection",
|
||||
"use_power_feature": "Use power management",
|
||||
"use_presence_feature": "Use presence detection"
|
||||
"use_presence_feature": "Use presence detection",
|
||||
"use_central_boiler_feature": "Use a central boiler. Check to add a control to your central boiler. You will have to configure the VTherm which will have a control of the central boiler after seecting this checkbox to take effect. If one VTherm requires heating, the boiler will be turned on. If no VTherm requires heating, the boiler will be turned off. Commands for turning on/off the central boiler are given in the related configuration page",
|
||||
"use_auto_start_stop_feature": "Use the auto start and stop feature"
|
||||
}
|
||||
},
|
||||
"type": {
|
||||
"title": "Linked entities",
|
||||
"title": "Entities - {name}",
|
||||
"description": "Linked entities attributes",
|
||||
"data": {
|
||||
"heater_entity_id": "1rst heater switch",
|
||||
"heater_entity2_id": "2nd heater switch",
|
||||
"heater_entity3_id": "3rd heater switch",
|
||||
"heater_entity4_id": "4th heater switch",
|
||||
"underlying_entity_ids": "The device(s) to be controlled",
|
||||
"heater_keep_alive": "Switch keep-alive interval in seconds",
|
||||
"proportional_function": "Algorithm",
|
||||
"climate_entity_id": "1rst underlying climate",
|
||||
"climate_entity2_id": "2nd underlying climate",
|
||||
"climate_entity3_id": "3rd underlying climate",
|
||||
"climate_entity4_id": "4th underlying climate",
|
||||
"ac_mode": "AC mode",
|
||||
"valve_entity_id": "1rst valve number",
|
||||
"valve_entity2_id": "2nd valve number",
|
||||
"valve_entity3_id": "3rd valve number",
|
||||
"valve_entity4_id": "4th valve number",
|
||||
"auto_regulation_mode": "Self-regulation",
|
||||
"auto_regulation_dtemp": "Regulation threshold",
|
||||
"auto_regulation_periode_min": "Regulation minimal period",
|
||||
"inverse_switch_command": "Inverse switch command"
|
||||
"auto_regulation_periode_min": "Regulation minimum period",
|
||||
"auto_regulation_use_device_temp": "Use internal temperature of the underlying",
|
||||
"inverse_switch_command": "Inverse switch command",
|
||||
"auto_fan_mode": "Auto fan mode"
|
||||
},
|
||||
"data_description": {
|
||||
"heater_entity_id": "Mandatory heater entity id",
|
||||
"heater_entity2_id": "Optional 2nd Heater entity id. Leave empty if not used",
|
||||
"heater_entity3_id": "Optional 3rd Heater entity id. Leave empty if not used",
|
||||
"heater_entity4_id": "Optional 4th Heater entity id. Leave empty if not used",
|
||||
"underlying_entity_ids": "The device(s) to be controlled - 1 is required",
|
||||
"heater_keep_alive": "Optional heater switch state refresh interval. Leave empty if not required.",
|
||||
"proportional_function": "Algorithm to use (TPI is the only one for now)",
|
||||
"climate_entity_id": "Underlying climate entity id",
|
||||
"climate_entity2_id": "2nd underlying climate entity id",
|
||||
"climate_entity3_id": "3rd underlying climate entity id",
|
||||
"climate_entity4_id": "4th underlying climate entity id",
|
||||
"ac_mode": "Use the Air Conditioning (AC) mode",
|
||||
"valve_entity_id": "1rst valve number entity id",
|
||||
"valve_entity2_id": "2nd valve number entity id",
|
||||
"valve_entity3_id": "3rd valve number entity id",
|
||||
"valve_entity4_id": "4th valve number entity id",
|
||||
"auto_regulation_mode": "Auto adjustment of the target temperature",
|
||||
"auto_regulation_dtemp": "The threshold in ° under which the temperature change will not be send",
|
||||
"auto_regulation_dtemp": "The threshold in ° (or % for valve) under which the temperature change will not be sent",
|
||||
"auto_regulation_periode_min": "Duration in minutes between two regulation update",
|
||||
"inverse_switch_command": "For switch with pilot wire and diode you may need to inverse the command"
|
||||
"auto_regulation_use_device_temp": "Use the eventual internal temperature sensor of the underlying to speedup the self-regulation",
|
||||
"inverse_switch_command": "For switch with pilot wire and diode you may need to invert the command",
|
||||
"auto_fan_mode": "Automatically activate fan when huge heating/cooling is necessary"
|
||||
}
|
||||
},
|
||||
"tpi": {
|
||||
"title": "TPI",
|
||||
"title": "TPI - {name}",
|
||||
"description": "Time Proportional Integral attributes",
|
||||
"data": {
|
||||
"tpi_coef_int": "coef_int",
|
||||
"tpi_coef_ext": "coef_ext",
|
||||
"use_tpi_central_config": "Use central TPI configuration"
|
||||
},
|
||||
"data_description": {
|
||||
"tpi_coef_int": "Coefficient to use for internal temperature delta",
|
||||
"tpi_coef_ext": "Coefficient to use for external temperature delta"
|
||||
"tpi_coef_ext": "Coefficient to use for external temperature delta",
|
||||
"use_tpi_central_config": "Check to use the central TPI configuration. Uncheck to use a specific TPI configuration for this VTherm"
|
||||
}
|
||||
},
|
||||
"presets": {
|
||||
"title": "Presets",
|
||||
"description": "For each presets, give the target temperature (0 to ignore preset)",
|
||||
"title": "Presets - {name}",
|
||||
"description": "Check if the thermostat will use central presets. Uncheck and the thermostat will have its own preset entities",
|
||||
"data": {
|
||||
"eco_temp": "Temperature in Eco preset",
|
||||
"comfort_temp": "Temperature in Comfort preset",
|
||||
"boost_temp": "Temperature in Boost preset",
|
||||
"eco_ac_temp": "Temperature in Eco preset for AC mode",
|
||||
"comfort_ac_temp": "Temperature in Comfort preset for AC mode",
|
||||
"boost_ac_temp": "Temperature in Boost preset for AC mode"
|
||||
"use_presets_central_config": "Use central presets configuration"
|
||||
}
|
||||
},
|
||||
"window": {
|
||||
"title": "Window management",
|
||||
"description": "Open window management.\nLeave corresponding entity_id empty if not used\nYou can also configure automatic window open detection based on temperature decrease",
|
||||
"title": "Window - {name}",
|
||||
"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_auto_open_threshold": "Temperature decrease threshold for automatic window open detection (in °/min)",
|
||||
"window_auto_close_threshold": "Temperature increase threshold for end of automatic detection (in °/min)",
|
||||
"window_auto_max_duration": "Maximum duration of automatic window open detection (in min)"
|
||||
"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)",
|
||||
"use_window_central_config": "Use central window configuration",
|
||||
"window_action": "Action"
|
||||
},
|
||||
"data_description": {
|
||||
"window_sensor_entity_id": "Leave empty if no window sensor should be use",
|
||||
"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 0.05 and 0.1. Leave empty if automatic window open detection is not use",
|
||||
"window_auto_close_threshold": "Recommended value: 0. Leave empty if automatic window open detection is not use",
|
||||
"window_auto_max_duration": "Recommended value: 60 (one hour). Leave empty if automatic window open detection is not use"
|
||||
"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",
|
||||
"use_window_central_config": "Check to use the central window configuration. Uncheck to use a specific window configuration for this VTherm",
|
||||
"window_action": "Action to do if window is deteted as open"
|
||||
}
|
||||
},
|
||||
"motion": {
|
||||
"title": "Motion management",
|
||||
"description": "Motion sensor management. Preset can switch automatically depending of a motion detection\nLeave corresponding entity_id empty if not used.\nmotion_preset and no_motion_preset should be set to the corresponding preset name",
|
||||
"title": "Motion - {name}",
|
||||
"description": "Motion management. Preset can switch automatically depending of a motion detection\nmotion_preset and no_motion_preset should be set to the corresponding preset name",
|
||||
"data": {
|
||||
"motion_sensor_entity_id": "Motion sensor entity id",
|
||||
"motion_delay": "Activation delay",
|
||||
"motion_off_delay": "Deactivation delay",
|
||||
"motion_preset": "Motion preset",
|
||||
"no_motion_preset": "No motion preset"
|
||||
"no_motion_preset": "No motion preset",
|
||||
"use_motion_central_config": "Use central motion configuration"
|
||||
},
|
||||
"data_description": {
|
||||
"motion_sensor_entity_id": "The entity id of the motion sensor",
|
||||
"motion_delay": "Motion activation activation delay (seconds)",
|
||||
"motion_delay": "Motion activation delay (seconds)",
|
||||
"motion_off_delay": "Motion deactivation delay (seconds)",
|
||||
"motion_preset": "Preset to use when motion is detected",
|
||||
"no_motion_preset": "Preset to use when no motion is detected"
|
||||
"no_motion_preset": "Preset to use when no motion is detected",
|
||||
"use_motion_central_config": "Check to use the central motion configuration. Uncheck to use a specific motion configuration for this VTherm"
|
||||
}
|
||||
},
|
||||
"power": {
|
||||
"title": "Power management",
|
||||
"description": "Power management attributes.\nGives the power and max power sensor of your home.\nThen specify the power consumption of the heater when on.\nAll sensors and device power should have the same unit (kW or W).\nLeave corresponding entity_id empty if not used.",
|
||||
"title": "Power - {name}",
|
||||
"description": "Power management attributes.\nGives the power and max power sensor of your home.\nThen specify the power consumption of the heater when on.\nAll sensors and device power should have the same unit (kW or W).",
|
||||
"data": {
|
||||
"power_sensor_entity_id": "Power",
|
||||
"max_power_sensor_entity_id": "Max power",
|
||||
"power_temp": "Shedding temperature",
|
||||
"use_power_central_config": "Use central power configuration"
|
||||
},
|
||||
"data_description": {
|
||||
"power_sensor_entity_id": "Power sensor entity id",
|
||||
"max_power_sensor_entity_id": "Max power sensor entity id",
|
||||
"power_temp": "Temperature for Power shedding"
|
||||
"power_temp": "Temperature for Power shedding",
|
||||
"use_power_central_config": "Check to use the central power configuration. Uncheck to use a specific power configuration for this VTherm"
|
||||
}
|
||||
},
|
||||
"presence": {
|
||||
"title": "Presence management",
|
||||
"description": "Presence management attributes.\nGives the a presence sensor of your home (true is someone is present).\nThen specify either the preset to use when presence sensor is false or the offset in temperature to apply.\nIf preset is given, the offset will not be used.\nLeave corresponding entity_id empty if not used.",
|
||||
"title": "Presence - {name}",
|
||||
"description": "Presence management attributes.\nGives the a presence sensor of your home (true is someone is present) and give the corresponding temperature preset setting.",
|
||||
"data": {
|
||||
"presence_sensor_entity_id": "Presence sensor entity id (true is present)",
|
||||
"eco_away_temp": "Temperature in Eco preset when no presence",
|
||||
"comfort_away_temp": "Temperature in Comfort preset when no presence",
|
||||
"boost_away_temp": "Temperature in Boost preset when no presence",
|
||||
"eco_ac_away_temp": "Temperature in Eco preset when no presence in AC mode",
|
||||
"comfort_ac_away_temp": "Temperature in Comfort preset when no presence in AC mode",
|
||||
"boost_ac_away_temp": "Temperature in Boost preset when no presence in AC mode"
|
||||
"presence_sensor_entity_id": "Presence sensor",
|
||||
"use_presence_central_config": "Use central presence temperature configuration. Uncheck to use specific temperature entities"
|
||||
},
|
||||
"data_description": {
|
||||
"presence_sensor_entity_id": "Presence sensor entity id"
|
||||
}
|
||||
},
|
||||
"advanced": {
|
||||
"title": "Advanced parameters",
|
||||
"description": "Configuration of advanced parameters. Leave the default values if you don't know what you are doing.\nThis parameters can lead to a very bad temperature or power regulation.",
|
||||
"title": "Advanced - {name}",
|
||||
"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": "Minimal activation delay",
|
||||
"security_delay_min": "Security delay (in minutes)",
|
||||
"security_min_on_percent": "Minimal power percent for security mode",
|
||||
"security_default_on_percent": "Power percent to use in security mode"
|
||||
"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",
|
||||
"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 mesures. Above this delay, the thermostat will turn to a security off state",
|
||||
"security_min_on_percent": "Minimal heating percent value for security preset activation. Below this amount of power percent the thermostat won't go into security preset",
|
||||
"security_default_on_percent": "The default heating power percent value in security preset. Set to 0 to switch off heater in security present"
|
||||
"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",
|
||||
"use_advanced_central_config": "Check to use the central advanced configuration. Uncheck to use a specific advanced configuration for this VTherm"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"unknown": "Unexpected error",
|
||||
"unknown_entity": "Unknown entity id",
|
||||
"window_open_detection_method": "Only one window open detection method should be used. Use sensor or automatic detection through temperature threshold but not both"
|
||||
"window_open_detection_method": "Only one window open detection method should be used. Use either window sensor or automatic detection through temperature threshold but not both",
|
||||
"no_central_config": "You cannot check 'use central configuration' because no central configuration was found. You need to create a Versatile Thermostat of type 'Central Configuration' to use it.",
|
||||
"service_configuration_format": "The format of the service configuration is wrong"
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "Device is already configured"
|
||||
@@ -341,6 +434,7 @@
|
||||
"selector": {
|
||||
"thermostat_type": {
|
||||
"options": {
|
||||
"thermostat_central_config": "Central configuration",
|
||||
"thermostat_over_switch": "Thermostat over a switch",
|
||||
"thermostat_over_climate": "Thermostat over a climate",
|
||||
"thermostat_over_valve": "Thermostat over a valve"
|
||||
@@ -352,8 +446,42 @@
|
||||
"auto_regulation_strong": "Strong",
|
||||
"auto_regulation_medium": "Medium",
|
||||
"auto_regulation_light": "Light",
|
||||
"auto_regulation_expert": "Expert",
|
||||
"auto_regulation_none": "No auto-regulation"
|
||||
}
|
||||
},
|
||||
"auto_fan_mode": {
|
||||
"options": {
|
||||
"auto_fan_none": "No auto fan",
|
||||
"auto_fan_low": "Low",
|
||||
"auto_fan_medium": "Medium",
|
||||
"auto_fan_high": "High",
|
||||
"auto_fan_turbo": "Turbo"
|
||||
}
|
||||
},
|
||||
"window_action": {
|
||||
"options": {
|
||||
"window_turn_off": "Turn off",
|
||||
"window_fan_only": "Fan only",
|
||||
"window_frost_temp": "Frost protect",
|
||||
"window_eco_temp": "Eco"
|
||||
}
|
||||
},
|
||||
"presets": {
|
||||
"options": {
|
||||
"frost": "Frost protect",
|
||||
"eco": "Eco",
|
||||
"comfort": "Comfort",
|
||||
"boost": "Boost"
|
||||
}
|
||||
},
|
||||
"auto_start_stop": {
|
||||
"options": {
|
||||
"auto_start_stop_none": "No auto start/stop",
|
||||
"auto_start_stop_slow": "Slow detection",
|
||||
"auto_start_stop_medium": "Medium detection",
|
||||
"auto_start_stop_fast": "Fast detection"
|
||||
}
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
@@ -363,12 +491,60 @@
|
||||
"preset_mode": {
|
||||
"state": {
|
||||
"power": "Shedding",
|
||||
"security": "Security",
|
||||
"none": "Manual"
|
||||
"security": "Safety",
|
||||
"none": "Manual",
|
||||
"frost": "Frost"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"number": {
|
||||
"frost_temp": {
|
||||
"name": "Frost"
|
||||
},
|
||||
"eco_temp": {
|
||||
"name": "Eco"
|
||||
},
|
||||
"comfort_temp": {
|
||||
"name": "Comfort"
|
||||
},
|
||||
"boost_temp": {
|
||||
"name": "Boost"
|
||||
},
|
||||
"frost_ac_temp": {
|
||||
"name": "Frost ac"
|
||||
},
|
||||
"eco_ac_temp": {
|
||||
"name": "Eco ac"
|
||||
},
|
||||
"comfort_ac_temp": {
|
||||
"name": "Comfort ac"
|
||||
},
|
||||
"boost_ac_temp": {
|
||||
"name": "Boost ac"
|
||||
},
|
||||
"frost_away_temp": {
|
||||
"name": "Frost away"
|
||||
},
|
||||
"eco_away_temp": {
|
||||
"name": "Eco away"
|
||||
},
|
||||
"comfort_away_temp": {
|
||||
"name": "Comfort away"
|
||||
},
|
||||
"boost_away_temp": {
|
||||
"name": "Boost away"
|
||||
},
|
||||
"eco_ac_away_temp": {
|
||||
"name": "Eco ac away"
|
||||
},
|
||||
"comfort_ac_away_temp": {
|
||||
"name": "Comfort ac away"
|
||||
},
|
||||
"boost_ac_away_temp": {
|
||||
"name": "Boost ac away"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,21 +4,68 @@
|
||||
"flow_title": "Versatile Thermostat configuration",
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "Type du nouveau Versatile Thermostat",
|
||||
"data": {
|
||||
"thermostat_type": "Type de thermostat"
|
||||
},
|
||||
"data_description": {
|
||||
"thermostat_type": "Un seul thermostat de type Configuration centrale est possible."
|
||||
}
|
||||
},
|
||||
"menu": {
|
||||
"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",
|
||||
"central_boiler": "Chauffage central",
|
||||
"type": "Sous-jacents",
|
||||
"tpi": "Paramètres TPI",
|
||||
"features": "Fonctions",
|
||||
"presets": "Pre-réglages",
|
||||
"window": "Détection d'ouverture",
|
||||
"motion": "Détection de mouvement",
|
||||
"power": "Gestion de la puissance",
|
||||
"presence": "Détection de présence",
|
||||
"advanced": "Paramètres avancés",
|
||||
"auto_start_stop": "Allumage/extinction automatique",
|
||||
"finalize": "Finaliser la création",
|
||||
"configuration_not_complete": "Configuration incomplète"
|
||||
}
|
||||
},
|
||||
"main": {
|
||||
"title": "Ajout d'un nouveau thermostat",
|
||||
"description": "Principaux attributs obligatoires",
|
||||
"data": {
|
||||
"name": "Nom",
|
||||
"thermostat_type": "Type de thermostat",
|
||||
"temperature_sensor_entity_id": "Température sensor entity id",
|
||||
"external_temperature_sensor_entity_id": "Température exterieure sensor entity id",
|
||||
"temperature_sensor_entity_id": "Capteur de température",
|
||||
"last_seen_temperature_sensor_entity_id": "Dernière vue capteur de température",
|
||||
"external_temperature_sensor_entity_id": "Capteur de température exterieure",
|
||||
"cycle_min": "Durée du cycle (minutes)",
|
||||
"temp_min": "Température minimale permise",
|
||||
"temp_max": "Température maximale permise",
|
||||
"step_temperature": "Pas de température",
|
||||
"device_power": "Puissance de l'équipement",
|
||||
"use_central_mode": "Autoriser le controle par une entity centrale ('nécessite une config. centrale`). Cochez pour autoriser le contrôle du VTherm par la liste déroulante 'central_mode' de l'entité configuration centrale.",
|
||||
"use_main_central_config": "Utiliser la configuration centrale supplémentaire. Cochez pour utiliser la configuration centrale supplémentaire (température externe, min, max, pas, ...)",
|
||||
"used_by_controls_central_boiler": "Utilisé par la chaudière centrale. Cochez si ce VTherm doit contrôler la chaudière centrale."
|
||||
},
|
||||
"data_description": {
|
||||
"temperature_sensor_entity_id": "Id d'entité du capteur de température",
|
||||
"last_seen_temperature_sensor_entity_id": "Id d'entité du capteur donnant la date et heure de dernière vue capteur de température. L'état doit être au format date heure (ex: 2024-03-31T17:07:03+00:00)",
|
||||
"external_temperature_sensor_entity_id": "Entity id du capteur de température extérieure. Non utilisé si une configuration centrale est définie"
|
||||
}
|
||||
},
|
||||
"features": {
|
||||
"title": "Fonctions",
|
||||
"description": "Fonctions du thermostat à utiliser",
|
||||
"data": {
|
||||
"use_window_feature": "Avec détection des ouvertures",
|
||||
"use_motion_feature": "Avec détection de mouvement",
|
||||
"use_power_feature": "Avec gestion de la puissance",
|
||||
"use_presence_feature": "Avec détection de présence"
|
||||
"use_presence_feature": "Avec détection de présence",
|
||||
"use_central_boiler_feature": "Ajouter une chaudière centrale. Cochez pour ajouter un controle sur une chaudière centrale. Vous devrez ensuite configurer les VTherms qui commande la chaudière centrale pour que cette option prenne effet. Si au moins un des VTherm a besoin de chauffer, la chaudière centrale sera activée. Si aucun VTherm n'a besoin de chauffer, elle sera éteinte. Les commandes pour allumer/éteindre la chaudière centrale sont données dans la page de configuration suivante.",
|
||||
"use_auto_start_stop_feature": "Avec démarrage et extinction automatique"
|
||||
}
|
||||
},
|
||||
"type": {
|
||||
@@ -29,6 +76,7 @@
|
||||
"heater_entity2_id": "2ème radiateur",
|
||||
"heater_entity3_id": "3ème radiateur",
|
||||
"heater_entity4_id": "4ème radiateur",
|
||||
"heater_keep_alive": "keep-alive (sec)",
|
||||
"proportional_function": "Algorithme",
|
||||
"climate_entity_id": "Thermostat sous-jacent",
|
||||
"climate_entity2_id": "2ème thermostat sous-jacent",
|
||||
@@ -42,13 +90,16 @@
|
||||
"auto_regulation_mode": "Auto-régulation",
|
||||
"auto_regulation_dtemp": "Seuil de régulation",
|
||||
"auto_regulation_periode_min": "Période minimale de régulation",
|
||||
"inverse_switch_command": "Inverser la commande"
|
||||
"auto_regulation_use_device_temp": "Utiliser la température interne du sous-jacent",
|
||||
"inverse_switch_command": "Inverser la commande",
|
||||
"auto_fan_mode": " Auto ventilation mode"
|
||||
},
|
||||
"data_description": {
|
||||
"heater_entity_id": "Entity id du 1er radiateur obligatoire",
|
||||
"heater_entity2_id": "Optionnel entity id du 2ème radiateur",
|
||||
"heater_entity3_id": "Optionnel entity id du 3ème radiateur",
|
||||
"heater_entity4_id": "Optionnel entity id du 4ème radiateur",
|
||||
"heater_keep_alive": "Intervalle de rafraichissement du switch en secondes. Laisser vide pour désactiver. À n'utiliser que pour les switchs qui le nécessite.",
|
||||
"proportional_function": "Algorithme à utiliser (Seul TPI est disponible pour l'instant)",
|
||||
"climate_entity_id": "Entity id du thermostat sous-jacent",
|
||||
"climate_entity2_id": "Entity id du 2ème thermostat sous-jacent",
|
||||
@@ -60,87 +111,101 @@
|
||||
"valve_entity3_id": "Entity id de la 3ème valve",
|
||||
"valve_entity4_id": "Entity id de la 4ème valve",
|
||||
"auto_regulation_mode": "Ajustement automatique de la température cible",
|
||||
"auto_regulation_dtemp": "Le seuil en ° au-dessous duquel la régulation ne sera pas envoyée",
|
||||
"auto_regulation_dtemp": "Le seuil en ° (ou % pour les valves) en-dessous duquel la régulation ne sera pas envoyée",
|
||||
"auto_regulation_periode_min": "La durée en minutes entre deux mise à jour faites par la régulation",
|
||||
"inverse_switch_command": "Inverse la commande du switch pour une installation avec fil pilote et diode"
|
||||
"auto_regulation_use_device_temp": "Utiliser la temperature interne du sous-jacent pour accélérer l'auto-régulation",
|
||||
"inverse_switch_command": "Inverse la commande du switch pour une installation avec fil pilote et diode",
|
||||
"auto_fan_mode": "Active la ventilation automatiquement en cas d'écart important"
|
||||
}
|
||||
},
|
||||
"tpi": {
|
||||
"title": "TPI",
|
||||
"description": "Attributs de l'algo Time Proportional Integral",
|
||||
"data": {
|
||||
"tpi_coef_int": "coeff_int : Coefficient à utiliser pour le delta de température interne",
|
||||
"tpi_coef_ext": "coeff_ext : Coefficient à utiliser pour le delta de température externe"
|
||||
"tpi_coef_int": "coeff_int",
|
||||
"tpi_coef_ext": "coeff_ext",
|
||||
"use_tpi_central_config": "Utiliser la configuration TPI centrale"
|
||||
},
|
||||
"data_description": {
|
||||
"tpi_coef_int": "Coefficient à utiliser pour le delta de température interne",
|
||||
"tpi_coef_ext": "Coefficient à utiliser pour le delta de température externe",
|
||||
"use_tpi_central_config": "Cochez pour utiliser la configuration TPI centrale. Décochez et saisissez les attributs pour utiliser une configuration TPI spécifique"
|
||||
}
|
||||
},
|
||||
"presets": {
|
||||
"title": "Presets",
|
||||
"description": "Pour chaque preset, donnez la température cible (0 pour ignorer le preset)",
|
||||
"title": "Pre-réglages",
|
||||
"description": "Cochez pour que ce thermostat utilise les pré-réglages de la configuration centrale. Décochez pour utiliser des entités de température spécifiques",
|
||||
"data": {
|
||||
"eco_temp": "Température en preset Eco",
|
||||
"comfort_temp": "Température en preset Comfort",
|
||||
"boost_temp": "Température en preset Boost",
|
||||
"eco_ac_temp": "Température en preset Eco en mode AC",
|
||||
"comfort_ac_temp": "Température en preset Comfort en mode AC",
|
||||
"boost_ac_temp": "Température en preset Boost en mode AC"
|
||||
"use_presets_central_config": "Utiliser la configuration des pré-réglages centrale"
|
||||
}
|
||||
},
|
||||
"window": {
|
||||
"title": "Gestion d'une ouverture",
|
||||
"description": "Coupe le radiateur si l'ouverture est ouverte.\nLaissez l'entity id vide si non utilisé.",
|
||||
"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_auto_open_threshold": "Seuil haut de chute de température pour la détection automatique (en °/min)",
|
||||
"window_auto_close_threshold": "Seuil bas de chute de température pour la fin de détection automatique (en °/min)",
|
||||
"window_auto_max_duration": "Durée maximum d'une extinction automatique (en min)"
|
||||
"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)",
|
||||
"use_window_central_config": "Utiliser la configuration centrale des ouvertures",
|
||||
"window_action": "Action"
|
||||
},
|
||||
"data_description": {
|
||||
"window_sensor_entity_id": "Laissez vide si vous n'avez de détecteur",
|
||||
"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_auto_open_threshold": "Valeur recommandée: entre 0.05 et 0.1. Laissez vide si vous n'utilisez pas la détection automatique",
|
||||
"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"
|
||||
"window_auto_max_duration": "Valeur recommandée: 60 (1 heure). Laissez vide si vous n'utilisez pas la détection automatique",
|
||||
"use_window_central_config": "Cochez pour utiliser la configuration centrale des ouvertures. Décochez et saisissez les attributs pour utiliser une configuration spécifique des ouvertures",
|
||||
"window_action": "Action a effectuer si la fenêtre est détectée comme ouverte"
|
||||
}
|
||||
},
|
||||
"motion": {
|
||||
"title": "Gestion de la détection de mouvement",
|
||||
"description": "Le preset s'ajuste automatiquement si un mouvement est détecté\nLaissez l'entity id vide si non utilisé.\n'Preset mouvement' et 'Preset sans mouvement' doivent être choisis avec les preset à utiliser.",
|
||||
"description": "Le preset s'ajuste automatiquement si un mouvement est détecté\n'Preset mouvement' et 'Preset sans mouvement' doivent être choisis avec les preset à utiliser.",
|
||||
"data": {
|
||||
"motion_sensor_entity_id": "Détecteur de mouvement",
|
||||
"motion_delay": "Délai d'activation",
|
||||
"motion_off_delay": "Délai de désactivation",
|
||||
"motion_preset": "Preset si mouvement",
|
||||
"no_motion_preset": "Preset si pas de mouvement"
|
||||
"no_motion_preset": "Preset sans mouvement",
|
||||
"use_motion_central_config": "Utiliser la condfiguration centrale du mouvement"
|
||||
},
|
||||
"data_description": {
|
||||
"motion_sensor_entity_id": "Détecteur de mouvement entity id",
|
||||
"motion_sensor_entity_id": "Id d'entité du détecteur de mouvement",
|
||||
"motion_delay": "Délai avant activation lorsqu'un mouvement est détecté (secondss)",
|
||||
"motion_off_delai": "Délai avant désactivation lorsqu'aucun mouvement n'est détecté (secondes)",
|
||||
"motion_preset": "Preset à utiliser si mouvement détecté",
|
||||
"no_motion_preset": "Preset à utiliser si pas de mouvement détecté"
|
||||
"no_motion_preset": "Preset à utiliser si pas de mouvement détecté",
|
||||
"use_motion_central_config": "Cochez pour utiliser la configuration centrale du mouvement. Décochez et saisissez les attributs pour utiliser une configuration spécifique du mouvement"
|
||||
}
|
||||
},
|
||||
"power": {
|
||||
"title": "Gestion de l'énergie",
|
||||
"title": "Gestion de la puissance",
|
||||
"description": "Sélectionne automatiquement le preset 'power' si la puissance consommée est supérieure à un maximum.\nDonnez les entity id des capteurs qui mesurent la puissance totale et la puissance max autorisée.\nEnsuite donnez la puissance de l'équipement.\nTous les capteurs et la puissance consommée par l'équipement doivent avoir la même unité de mesure (kW ou W).",
|
||||
"data": {
|
||||
"power_sensor_entity_id": "Capteur de puissance totale (entity id)",
|
||||
"max_power_sensor_entity_id": "Capteur de puissance Max (entity id)",
|
||||
"power_temp": "Température si délestaqe"
|
||||
"power_temp": "Température si délestaqe",
|
||||
"use_power_central_config": "Utiliser la configuration centrale de la puissance"
|
||||
},
|
||||
"data_description": {
|
||||
"power_sensor_entity_id": "Entity id du capteur de puissance totale du logement",
|
||||
"max_power_sensor_entity_id": "Entity id du capteur de puissance Max autorisée avant délestage",
|
||||
"power_temp": "Température cible si délestaqe",
|
||||
"use_power_central_config": "Cochez pour utiliser la configuration centrale de la puissance. Décochez et saisissez les attributs pour utiliser une configuration spécifique de la puissance"
|
||||
}
|
||||
},
|
||||
"presence": {
|
||||
"title": "Gestion de la présence",
|
||||
"description": "Donnez un capteur de présence (true si quelqu'un est présent).\nEnsuite spécifiez soit un preset à utiliser, soit un offset de température à appliquer lorsque personne n'est présent.\nSi le préset est utilisé, l'offset ne sera pas pris en compte.\nLaissez l'entity id vide si la gestion de la présence est non utilisée.",
|
||||
"description": "Donnez un capteur de présence (true si quelqu'un est présent) et les températures cibles à utiliser en cas d'abs.",
|
||||
"data": {
|
||||
"presence_sensor_entity_id": "Capteur de présence entity id (true si quelqu'un est présent)",
|
||||
"eco_away_temp": "Température en preset Eco en cas d'absence",
|
||||
"comfort_away_temp": "Température en preset Comfort en cas d'absence",
|
||||
"boost_away_temp": "Température en preset Boost en cas d'absence",
|
||||
"eco_ac_away_temp": "Température en preset Eco en cas d'absence en mode AC",
|
||||
"comfort_ac_away_temp": "Température en preset Comfort en cas d'absence en mode AC",
|
||||
"boost_ac_away_temp": "Température en preset Boost en cas d'absence en mode AC"
|
||||
"presence_sensor_entity_id": "Capteur de présence",
|
||||
"use_presence_central_config": "Utiliser la configuration centrale des températures en cas d'absence. Décochez pour avoir des entités de température dédiées"
|
||||
},
|
||||
"data_description": {
|
||||
"presence_sensor_entity_id": "Id d'entité du capteur de présence"
|
||||
}
|
||||
},
|
||||
"advanced": {
|
||||
@@ -150,20 +215,35 @@
|
||||
"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é"
|
||||
"security_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é"
|
||||
"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é",
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"central_boiler": {
|
||||
"title": "Contrôle de la chaudière centrale",
|
||||
"description": "Donnez les services à appeler pour allumer/éteindre la chaudière centrale. Laissez vide, si aucun appel de service ne doit être effectué (dans ce cas, vous devrez gérer vous même l'allumage/extinction de votre chaudière centrale). Le service a appelé doit être formatté comme suit: `entity_id/service_name[/attribut:valeur]` (/attribut:valeur est facultatif)\nPar exemple:\n- pour allumer un switch: `switch.controle_chaudiere/switch.turn_on`\n- pour éteindre un switch: `switch.controle_chaudiere/switch.turn_off`\n- pour programmer la chaudière sur 25° et ainsi forcer son allumage: `climate.thermostat_chaudiere/climate.set_temperature/temperature:25`\n- pour envoyer 10° à la chaudière et ainsi forcer son extinction: `climate.thermostat_chaudiere/climate.set_temperature/temperature:10`",
|
||||
"data": {
|
||||
"central_boiler_activation_service": "Commande pour allumer",
|
||||
"central_boiler_deactivation_service": "Commande pour éteindre"
|
||||
},
|
||||
"data_description": {
|
||||
"central_boiler_activation_service": "Commande à éxecuter pour allumer la chaudière centrale au format entity_id/service_name[/attribut:valeur]",
|
||||
"central_boiler_deactivation_service": "Commande à éxecuter pour étiendre la chaudière centrale au format entity_id/service_name[/attribut:valeur]"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"unknown": "Erreur inattendue",
|
||||
"unknown_entity": "entity id inconnu",
|
||||
"window_open_detection_method": "Une seule méthode de détection des ouvertures ouvertes doit être utilisée. Utilisez le détecteur d'ouverture ou les seuils de température mais pas les deux."
|
||||
"window_open_detection_method": "Une seule méthode de détection des ouvertures ouvertes doit être utilisée. Utilisez le détecteur d'ouverture ou les seuils de température mais pas les deux.",
|
||||
"no_central_config": "Vous ne pouvez pas cocher 'Utiliser la configuration centrale' car aucune configuration centrale n'a été trouvée. Vous devez créer un Versatile Thermostat de type 'Central Configuration' pour pouvoir l'utiliser."
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "Le device est déjà configuré"
|
||||
@@ -173,32 +253,79 @@
|
||||
"flow_title": "Versatile Thermostat configuration",
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "Ajout d'un nouveau thermostat",
|
||||
"title": "Type - {name}",
|
||||
"data": {
|
||||
"thermostat_type": "Type de thermostat"
|
||||
},
|
||||
"data_description": {
|
||||
"thermostat_type": "Un seul thermostat de type Configuration centrale est possible."
|
||||
}
|
||||
},
|
||||
"menu": {
|
||||
"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",
|
||||
"central_boiler": "Chauffage central",
|
||||
"type": "Sous-jacents",
|
||||
"tpi": "Paramètres TPI",
|
||||
"features": "Fonctions",
|
||||
"presets": "Pre-réglages",
|
||||
"window": "Détection d'ouvertures",
|
||||
"motion": "Détection de mouvement",
|
||||
"power": "Gestion de la puissance",
|
||||
"presence": "Détection de présence",
|
||||
"advanced": "Paramètres avancés",
|
||||
"auto_start_stop": "Allumage/extinction automatique",
|
||||
"finalize": "Finaliser les modifications",
|
||||
"configuration_not_complete": "Configuration incomplète"
|
||||
}
|
||||
},
|
||||
"main": {
|
||||
"title": "Attributs - {name}",
|
||||
"description": "Principaux attributs obligatoires",
|
||||
"data": {
|
||||
"name": "Nom",
|
||||
"thermostat_over_switch": "Thermostat sur un switch",
|
||||
"thermostat_over_climate": "Thermostat sur un autre thermostat",
|
||||
"temperature_sensor_entity_id": "Température sensor entity id",
|
||||
"external_temperature_sensor_entity_id": "Temperature exterieure sensor entity id",
|
||||
"thermostat_type": "Type de thermostat",
|
||||
"temperature_sensor_entity_id": "Capteur de température",
|
||||
"last_seen_temperature_sensor_entity_id": "Dernière vue capteur de température",
|
||||
"external_temperature_sensor_entity_id": "Capteur de température exterieure",
|
||||
"cycle_min": "Durée du cycle (minutes)",
|
||||
"temp_min": "Température minimale permise",
|
||||
"temp_max": "Température maximale permise",
|
||||
"step_temperature": "Pas de température",
|
||||
"device_power": "Puissance de l'équipement",
|
||||
"use_central_mode": "Autoriser le controle par une entity centrale ('nécessite une config. centrale`). Cochez pour autoriser le contrôle du VTherm par la liste déroulante 'central_mode' de l'entité configuration centrale.",
|
||||
"use_main_central_config": "Utiliser la configuration centrale supplémentaire. Cochez pour utiliser la configuration centrale supplémentaire (température externe, min, max, pas, ...)",
|
||||
"used_by_controls_central_boiler": "Utilisé par la chaudière centrale. Cochez si ce VTherm doit contrôler la chaudière centrale."
|
||||
},
|
||||
"data_description": {
|
||||
"temperature_sensor_entity_id": "Id d'entité du capteur de température",
|
||||
"last_seen_temperature_sensor_entity_id": "Id d'entité du capteur donnant la date et heure de dernière vue capteur de température. L'état doit être au format date heure (ex: 2024-03-31T17:07:03+00:00)",
|
||||
"external_temperature_sensor_entity_id": "Entity id du capteur de température extérieure. Non utilisé si une configuration centrale est définie"
|
||||
}
|
||||
},
|
||||
"features": {
|
||||
"title": "Fonctions - {name}",
|
||||
"description": "Fonctions du thermostat à utiliser",
|
||||
"data": {
|
||||
"use_window_feature": "Avec détection des ouvertures",
|
||||
"use_motion_feature": "Avec détection de mouvement",
|
||||
"use_power_feature": "Avec gestion de la puissance",
|
||||
"use_presence_feature": "Avec détection de présence"
|
||||
"use_presence_feature": "Avec détection de présence",
|
||||
"use_central_boiler_feature": "Ajouter une chaudière centrale. Cochez pour ajouter un controle sur une chaudière centrale. Vous devrez ensuite configurer les VTherms qui commande la chaudière centrale pour que cette option prenne effet. Si au moins un des VTherm a besoin de chauffer, la chaudière centrale sera activée. Si aucun VTherm n'a besoin de chauffer, elle sera éteinte. Les commandes pour allumer/éteindre la chaudière centrale sont données dans la page de configuration suivante.",
|
||||
"use_auto_start_stop_feature": "Avec démarrage et extinction automatique"
|
||||
}
|
||||
},
|
||||
"type": {
|
||||
"title": "Entité(s) liée(s)",
|
||||
"title": "Entités - {name}",
|
||||
"description": "Attributs de(s) l'entité(s) liée(s)",
|
||||
"data": {
|
||||
"heater_entity_id": "1er radiateur",
|
||||
"heater_entity2_id": "2ème radiateur",
|
||||
"heater_entity3_id": "3ème radiateur",
|
||||
"heater_entity4_id": "4ème radiateur",
|
||||
"heater_keep_alive": "Keep-alive (sec)",
|
||||
"proportional_function": "Algorithme",
|
||||
"climate_entity_id": "Thermostat sous-jacent",
|
||||
"climate_entity2_id": "2ème thermostat sous-jacent",
|
||||
@@ -212,13 +339,16 @@
|
||||
"auto_regulation_mode": "Auto-regulation",
|
||||
"auto_regulation_dtemp": "Seuil de régulation",
|
||||
"auto_regulation_periode_min": "Période minimale de régulation",
|
||||
"inverse_switch_command": "Inverser la commande"
|
||||
"auto_regulation_use_device_temp": "Utiliser la température interne du sous-jacent",
|
||||
"inverse_switch_command": "Inverser la commande",
|
||||
"auto_fan_mode": "Auto fan mode"
|
||||
},
|
||||
"data_description": {
|
||||
"heater_entity_id": "Entity id du 1er radiateur obligatoire",
|
||||
"heater_entity2_id": "Optionnel entity id du 2ème radiateur",
|
||||
"heater_entity3_id": "Optionnel entity id du 3ème radiateur",
|
||||
"heater_entity4_id": "Optionnel entity id du 4ème radiateur",
|
||||
"heater_keep_alive": "Intervalle de rafraichissement du switch en secondes. Laisser vide pour désactiver. À n'utiliser que pour les switchs qui le nécessite.",
|
||||
"proportional_function": "Algorithme à utiliser (Seul TPI est disponible pour l'instant)",
|
||||
"climate_entity_id": "Entity id du thermostat sous-jacent",
|
||||
"climate_entity2_id": "Entity id du 2ème thermostat sous-jacent",
|
||||
@@ -230,13 +360,15 @@
|
||||
"valve_entity3_id": "Entity id de la 3ème valve",
|
||||
"valve_entity4_id": "Entity id de la 4ème valve",
|
||||
"auto_regulation_mode": "Ajustement automatique de la consigne",
|
||||
"auto_regulation_dtemp": "Le seuil en ° au-dessous duquel la régulation ne sera pas envoyée",
|
||||
"auto_regulation_dtemp": "Le seuil en ° (ou % pour les valves) en-dessous duquel la régulation ne sera pas envoyée",
|
||||
"auto_regulation_periode_min": "La durée en minutes entre deux mise à jour faites par la régulation",
|
||||
"inverse_switch_command": "Inverse la commande du switch pour une installation avec fil pilote et diode"
|
||||
"auto_regulation_use_device_temp": "Utiliser la temperature interne du sous-jacent pour accélérer l'auto-régulation",
|
||||
"inverse_switch_command": "Inverse la commande du switch pour une installation avec fil pilote et diode",
|
||||
"auto_fan_mode": "Active la ventilation automatiquement en cas d'écart important"
|
||||
}
|
||||
},
|
||||
"tpi": {
|
||||
"title": "TPI",
|
||||
"title": "TPI - {name}",
|
||||
"description": "Attributs de l'algo Time Proportional Integral",
|
||||
"data": {
|
||||
"tpi_coef_int": "coeff_int : Coefficient à utiliser pour le delta de température interne",
|
||||
@@ -244,96 +376,118 @@
|
||||
}
|
||||
},
|
||||
"presets": {
|
||||
"title": "Presets",
|
||||
"description": "Pour chaque preset, donnez la température cible (0 pour ignorer le preset)",
|
||||
"title": "Pre-réglages - {name}",
|
||||
"description": "Cochez pour que ce thermostat utilise les pré-réglages de la configuration centrale. Décochez pour utiliser des entités de température spécifiques",
|
||||
"data": {
|
||||
"eco_temp": "Température en preset Eco",
|
||||
"comfort_temp": "Température en preset Comfort",
|
||||
"boost_temp": "Température en preset Boost",
|
||||
"eco_ac_temp": "Température en preset Eco en mode AC",
|
||||
"comfort_ac_temp": "Température en preset Comfort en mode AC",
|
||||
"boost_ac_temp": "Température en preset Boost en mode AC"
|
||||
"use_presets_central_config": "Utiliser la configuration des pré-réglages centrale"
|
||||
}
|
||||
},
|
||||
"window": {
|
||||
"title": "Gestion d'une ouverture",
|
||||
"description": "Coupe le radiateur si l'ouverture est ouverte.\nLaissez l'entity id vide si non utilisé.",
|
||||
"title": "Ouverture - {name}",
|
||||
"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_auto_open_threshold": "seuil haut de chute de température pour la détection automatique (en °/min)",
|
||||
"window_auto_close_threshold": "Seuil bas de chute de température pour la fin de détection automatique (en °/min)",
|
||||
"window_auto_max_duration": "Durée maximum d'une extinction automatique (en min)"
|
||||
"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)",
|
||||
"use_window_central_config": "Utiliser la configuration centrale des ouvertures",
|
||||
"window_action": "Action"
|
||||
},
|
||||
"data_description": {
|
||||
"window_sensor_entity_id": "Laissez vide si vous n'avez de détecteur",
|
||||
"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_auto_open_threshold": "Valeur recommandée: entre 0.05 et 0.1. Laissez vide si vous n'utilisez pas la détection automatique",
|
||||
"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"
|
||||
"window_auto_max_duration": "Valeur recommandée: 60 (1 heure). Laissez vide si vous n'utilisez pas la détection automatique",
|
||||
"use_window_central_config": "Cochez pour utiliser la configuration centrale des ouvertures. Décochez et saisissez les attributs pour utiliser une configuration spécifique des ouvertures",
|
||||
"window_action": "Action a effectuer si la fenêtre est détectée comme ouverte"
|
||||
}
|
||||
},
|
||||
"motion": {
|
||||
"title": "Gestion de la détection de mouvement",
|
||||
"description": "Le preset s'ajuste automatiquement si un mouvement est détecté\nLaissez l'entity id vide si non utilisé.\n'Preset mouvement' et 'Preset sans mouvement' doivent être choisis avec les preset à utiliser.",
|
||||
"title": "Mouvement - {name}",
|
||||
"description": "Gestion du mouvement. Le preset s'ajuste automatiquement si un mouvement est détecté\n'Preset mouvement' et 'Preset sans mouvement' doivent être choisis avec les preset à utiliser.",
|
||||
"data": {
|
||||
"motion_sensor_entity_id": "Détecteur de mouvement",
|
||||
"motion_delay": "Délai d'activation",
|
||||
"motion_off_delay": "Délai de désactivation",
|
||||
"motion_preset": "Preset si mouvement",
|
||||
"no_motion_preset": "Preset si pas de mouvement"
|
||||
"no_motion_preset": "Preset sans mouvement",
|
||||
"use_motion_central_config": "Utiliser la condfiguration centrale du mouvement"
|
||||
},
|
||||
"data_description": {
|
||||
"motion_sensor_entity_id": "Détecteur de mouvement entity id",
|
||||
"motion_sensor_entity_id": "Id d'entité du détecteur de mouvement",
|
||||
"motion_delay": "Délai avant activation lorsqu'un mouvement est détecté (secondss)",
|
||||
"motion_off_delai": "Délai avant désactivation lorsqu'aucun mouvement n'est détecté (secondes)",
|
||||
"motion_preset": "Preset à utiliser si mouvement détecté",
|
||||
"no_motion_preset": "Preset à utiliser si pas de mouvement détecté"
|
||||
"no_motion_preset": "Preset à utiliser si pas de mouvement détecté",
|
||||
"use_motion_central_config": "Cochez pour utiliser la configuration centrale du mouvement. Décochez et saisissez les attributs pour utiliser une configuration spécifique du mouvement"
|
||||
}
|
||||
},
|
||||
"power": {
|
||||
"title": "Gestion de l'énergie",
|
||||
"description": "Sélectionne automatiquement le preset 'power' si la puissance consommée est supérieure à un maximum.\nDonnez les entity id des capteurs qui mesurent la puissance totale et la puissance max autorisée.\nEnsuite donnez la puissance de l'équipement.\nTous les capteurs et la puissance consommée par l'équipement doivent avoir la même unité de mesure (kW ou W).",
|
||||
"title": "Puissance - {name}",
|
||||
"description": "Gestion de la puissance. Sélectionne automatiquement le preset 'power' si la puissance consommée est supérieure à un maximum. Tous les capteurs et la puissance consommée par l'équipement doivent avoir la même unité de mesure (kW ou W).",
|
||||
"data": {
|
||||
"power_sensor_entity_id": "Capteur de puissance totale (entity id)",
|
||||
"power_sensor_entity_id": "Puissance totale",
|
||||
"max_power_sensor_entity_id": "Capteur de puissance Max (entity id)",
|
||||
"power_temp": "Température si délestaqe"
|
||||
"power_temp": "Température si délestaqe",
|
||||
"use_power_central_config": "Utiliser la configuration centrale de la puissance"
|
||||
},
|
||||
"data_description": {
|
||||
"power_sensor_entity_id": "Entity id du capteur de puissance totale du logement",
|
||||
"max_power_sensor_entity_id": "Entity id du capteur de puissance Max autorisée avant délestage",
|
||||
"power_temp": "Température cible si délestaqe",
|
||||
"use_power_central_config": "Cochez pour utiliser la configuration centrale de la puissance. Décochez et saisissez les attributs pour utiliser une configuration spécifique de la puissance"
|
||||
}
|
||||
},
|
||||
"presence": {
|
||||
"title": "Gestion de la présence",
|
||||
"description": "Donnez un capteur de présence (true si quelqu'un est présent).\nEnsuite spécifiez soit un preset à utiliser, soit un offset de température à appliquer lorsque personne n'est présent.\nSi le préset est utilisé, l'offset ne sera pas pris en compte.\nLaissez l'entity id vide si la gestion de la présence est non utilisée.",
|
||||
"title": "Présence - {name}",
|
||||
"description": "Donnez un capteur de présence (true si quelqu'un est présent) et les températures cibles à utiliser en cas d'abs.",
|
||||
"data": {
|
||||
"presence_sensor_entity_id": "Capteur de présence entity id (true si quelqu'un est présent)",
|
||||
"eco_away_temp": "Température en preset Eco en cas d'absence",
|
||||
"comfort_away_temp": "Température en preset Comfort en cas d'absence",
|
||||
"boost_away_temp": "Température en preset Boost en cas d'absence",
|
||||
"eco_ac_away_temp": "Température en preset Eco en cas d'absence en mode AC",
|
||||
"comfort_ac_away_temp": "Température en preset Comfort en cas d'absence en mode AC",
|
||||
"boost_ac_away_temp": "Température en preset Boost en cas d'absence en mode AC"
|
||||
"presence_sensor_entity_id": "Capteur de présence",
|
||||
"use_presence_central_config": "Utiliser la configuration centrale des températures en cas d'absence. Décochez pour avoir des entités de température dédiées"
|
||||
},
|
||||
"data_description": {
|
||||
"presence_sensor_entity_id": "Id d'entité du capteur de présence"
|
||||
}
|
||||
},
|
||||
"advanced": {
|
||||
"title": "Parameters avancés",
|
||||
"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.",
|
||||
"title": "Avancés - {name}",
|
||||
"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é"
|
||||
"security_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é"
|
||||
"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é",
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"central_boiler": {
|
||||
"title": "Contrôle de la chaudière centrale - {name}",
|
||||
"description": "Donnez les services à appeler pour allumer/éteindre la chaudière centrale. Laissez vide, si aucun appel de service ne doit être effectué (dans ce cas, vous devrez gérer vous même l'allumage/extinction de votre chaudière centrale). Le service a appelé doit être formatté comme suit: `entity_id/service_name[/attribut:valeur]` (/attribut:valeur est facultatif)\nPar exemple:\n- pour allumer un switch: `switch.controle_chaudiere/switch.turn_on`\n- pour éteindre un switch: `switch.controle_chaudiere/switch.turn_off`\n- pour programmer la chaudière sur 25° et ainsi forcer son allumage: `climate.thermostat_chaudiere/climate.set_temperature/temperature:25`\n- pour envoyer 10° à la chaudière et ainsi forcer son extinction: `climate.thermostat_chaudiere/climate.set_temperature/temperature:10`",
|
||||
"data": {
|
||||
"central_boiler_activation_service": "Commande pour allumer",
|
||||
"central_boiler_deactivation_service": "Commande pour éteindre"
|
||||
},
|
||||
"data_description": {
|
||||
"central_boiler_activation_service": "Commande à éxecuter pour allumer la chaudière centrale au format entity_id/service_name[/attribut:valeur]",
|
||||
"central_boiler_deactivation_service": "Commande à éxecuter pour étiendre la chaudière centrale au format entity_id/service_name[/attribut:valeur]"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"unknown": "Erreur inattendue",
|
||||
"unknown_entity": "entity id inconnu",
|
||||
"window_open_detection_method": "Une seule méthode de détection des ouvertures ouvertes doit être utilisée. Utilisez le détecteur d'ouverture ou les seuils de température mais pas les deux."
|
||||
"window_open_detection_method": "Une seule méthode de détection des ouvertures ouvertes doit être utilisée. Utilisez le détecteur d'ouverture ou les seuils de température mais pas les deux.",
|
||||
"no_central_config": "Vous ne pouvez pas cocher 'Utiliser la configuration centrale' car aucune configuration centrale n'a été trouvée. Vous devez créer un Versatile Thermostat de type 'Central Configuration' pour pouvoir l'utiliser.",
|
||||
"service_configuration_format": "Mauvais format de la configuration du service"
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "Le device est déjà configuré"
|
||||
@@ -342,6 +496,7 @@
|
||||
"selector": {
|
||||
"thermostat_type": {
|
||||
"options": {
|
||||
"thermostat_central_config": "Configuration centrale",
|
||||
"thermostat_over_switch": "Thermostat sur un switch",
|
||||
"thermostat_over_climate": "Thermostat sur un autre thermostat",
|
||||
"thermostat_over_valve": "Thermostat sur une valve"
|
||||
@@ -353,8 +508,42 @@
|
||||
"auto_regulation_strong": "Forte",
|
||||
"auto_regulation_medium": "Moyenne",
|
||||
"auto_regulation_light": "Légère",
|
||||
"auto_regulation_expert": "Expert",
|
||||
"auto_regulation_none": "Aucune"
|
||||
}
|
||||
},
|
||||
"auto_fan_mode": {
|
||||
"options": {
|
||||
"auto_fan_none": "Pas d'auto fan",
|
||||
"auto_fan_low": "Faible",
|
||||
"auto_fan_medium": "Moyenne",
|
||||
"auto_fan_high": "Forte",
|
||||
"auto_fan_turbo": "Turbo"
|
||||
}
|
||||
},
|
||||
"window_action": {
|
||||
"options": {
|
||||
"window_turn_off": "Eteindre",
|
||||
"window_fan_only": "Ventilateur seul",
|
||||
"window_frost_temp": "Hors gel",
|
||||
"window_eco_temp": "Eco"
|
||||
}
|
||||
},
|
||||
"presets": {
|
||||
"options": {
|
||||
"frost": "Hors-gel",
|
||||
"eco": "Eco",
|
||||
"comfort": "Confort",
|
||||
"boost": "Renforcé (boost)"
|
||||
}
|
||||
},
|
||||
"auto_start_stop": {
|
||||
"options": {
|
||||
"auto_start_stop_none": "No auto start/stop",
|
||||
"auto_start_stop_slow": "Slow detection",
|
||||
"auto_start_stop_medium": "Medium detection",
|
||||
"auto_start_stop_fast": "Fast detection"
|
||||
}
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
@@ -365,11 +554,59 @@
|
||||
"state": {
|
||||
"power": "Délestage",
|
||||
"security": "Sécurité",
|
||||
"none": "Manuel"
|
||||
"none": "Manuel",
|
||||
"frost": "Hors Gel"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"number": {
|
||||
"frost_temp": {
|
||||
"name": "Hors gel "
|
||||
},
|
||||
"eco_temp": {
|
||||
"name": "Eco"
|
||||
},
|
||||
"comfort_temp": {
|
||||
"name": "Confort"
|
||||
},
|
||||
"boost_temp": {
|
||||
"name": "Boost"
|
||||
},
|
||||
"frost_ac_temp": {
|
||||
"name": "Hors gel clim"
|
||||
},
|
||||
"eco_ac_temp": {
|
||||
"name": "Eco clim"
|
||||
},
|
||||
"comfort_ac_temp": {
|
||||
"name": "Confort clim"
|
||||
},
|
||||
"boost_ac_temp": {
|
||||
"name": "Boost clim"
|
||||
},
|
||||
"frost_away_temp": {
|
||||
"name": "Hors gel abs"
|
||||
},
|
||||
"eco_away_temp": {
|
||||
"name": "Eco abs"
|
||||
},
|
||||
"comfort_away_temp": {
|
||||
"name": "Confort abs"
|
||||
},
|
||||
"boost_away_temp": {
|
||||
"name": "Boost abs"
|
||||
},
|
||||
"eco_ac_away_temp": {
|
||||
"name": "Eco clim abs"
|
||||
},
|
||||
"comfort_ac_away_temp": {
|
||||
"name": "Confort clim abs"
|
||||
},
|
||||
"boost_ac_away_temp": {
|
||||
"name": "Boost clim abs"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -29,6 +29,7 @@
|
||||
"heater_entity2_id": "Secondo riscaldatore",
|
||||
"heater_entity3_id": "Terzo riscaldatore",
|
||||
"heater_entity4_id": "Quarto riscaldatore",
|
||||
"heater_keep_alive": "Intervallo keep-alive dell'interruttore in secondi",
|
||||
"proportional_function": "Algoritmo",
|
||||
"climate_entity_id": "Primo termostato",
|
||||
"climate_entity2_id": "Secondo termostato",
|
||||
@@ -40,13 +41,15 @@
|
||||
"valve_entity3_id": "Terza valvola",
|
||||
"valve_entity4_id": "Quarta valvola",
|
||||
"auto_regulation_mode": "Autoregolamentazione",
|
||||
"inverse_switch_command": "Comando inverso"
|
||||
"inverse_switch_command": "Comando inverso",
|
||||
"auto_fan_mode": "Auto fan mode"
|
||||
},
|
||||
"data_description": {
|
||||
"heater_entity_id": "Entity id obbligatoria del primo riscaldatore",
|
||||
"heater_entity2_id": "Entity id del secondo riscaldatore facoltativo. Lasciare vuoto se non utilizzato",
|
||||
"heater_entity3_id": "Entity id del terzo riscaldatore facoltativo. Lasciare vuoto se non utilizzato",
|
||||
"heater_entity4_id": "Entity id del quarto riscaldatore facoltativo. Lasciare vuoto se non utilizzato",
|
||||
"heater_keep_alive": "Frequenza di aggiornamento dell'interruttore (facoltativo). Lasciare vuoto se non richiesto.",
|
||||
"proportional_function": "Algoritmo da utilizzare (il TPI per adesso è l'unico)",
|
||||
"climate_entity_id": "Entity id del primo termostato",
|
||||
"climate_entity2_id": "Entity id del secondo termostato",
|
||||
@@ -58,7 +61,8 @@
|
||||
"valve_entity3_id": "Entity id della terza valvola",
|
||||
"valve_entity4_id": "Entity id della quarta valvola",
|
||||
"auto_regulation_mode": "Regolazione automatica della temperatura target",
|
||||
"inverse_switch_command": "Inverte il controllo dell'interruttore per un'installazione con filo pilota e diodo"
|
||||
"inverse_switch_command": "Inverte il controllo dell'interruttore per un'installazione con filo pilota e diodo",
|
||||
"auto_fan_mode": "Automatically activate fan when huge heating/cooling is necessary"
|
||||
}
|
||||
},
|
||||
"tpi": {
|
||||
@@ -76,6 +80,7 @@
|
||||
"eco_temp": "Temperatura nel preset Eco",
|
||||
"comfort_temp": "Temperatura nel preset Comfort",
|
||||
"boost_temp": "Temperatura nel preset Boost",
|
||||
"frost_temp": "Temperatura nel preset Frost protection",
|
||||
"eco_ac_temp": "Temperatura nel preset Eco (AC mode)",
|
||||
"comfort_ac_temp": "Temperatura nel preset Comfort (AC mode)",
|
||||
"boost_ac_temp": "Temperatura nel preset Boost (AC mode)"
|
||||
@@ -87,14 +92,14 @@
|
||||
"data": {
|
||||
"window_sensor_entity_id": "Entity id sensore finestra",
|
||||
"window_delay": "Ritardo sensore finestra (secondi)",
|
||||
"window_auto_open_threshold": "Soglia di diminuzione della temperatura per il rilevamento automatico della finestra aperta (in °/min)",
|
||||
"window_auto_close_threshold": "Soglia di aumento della temperatura per la fine del rilevamento automatico (in °/min)",
|
||||
"window_auto_open_threshold": "Soglia di diminuzione della temperatura per il rilevamento automatico della finestra aperta (in °/ora)",
|
||||
"window_auto_close_threshold": "Soglia di aumento della temperatura per la fine del rilevamento automatico (in °/ora)",
|
||||
"window_auto_max_duration": "Durata massima del rilevamento automatico della finestra aperta (in min)"
|
||||
},
|
||||
"data_description": {
|
||||
"window_sensor_entity_id": "Lasciare vuoto se non deve essere utilizzato alcun sensore finestra",
|
||||
"window_delay": "Ritardo in secondi prima che il rilevamento del sensore sia preso in considerazione",
|
||||
"window_auto_open_threshold": "Valore consigliato: tra 0.05 e 0.1. Lasciare vuoto se il rilevamento automatico della finestra aperta non è utilizzato",
|
||||
"window_auto_open_threshold": "Valore consigliato: tra 3 e 10. Lasciare vuoto se il rilevamento automatico della finestra aperta non è utilizzato",
|
||||
"window_auto_close_threshold": "Valore consigliato: 0. Lasciare vuoto se il rilevamento automatico della finestra aperta non è utilizzato",
|
||||
"window_auto_max_duration": "Valore consigliato: 60 (un'ora). Lasciare vuoto se il rilevamento automatico della finestra aperta non è utilizzato"
|
||||
}
|
||||
@@ -127,6 +132,7 @@
|
||||
"eco_away_temp": "Temperatura al preset Eco in caso d'assenza",
|
||||
"comfort_away_temp": "Temperatura al preset Comfort in caso d'assenza",
|
||||
"boost_away_temp": "Temperatura al preset Boost in caso d'assenza",
|
||||
"frost_away_temp": "Temperatura al preset Frost protection in caso d'assenza",
|
||||
"eco_ac_away_temp": "Temperatura al preset Eco in caso d'assenza (AC mode)",
|
||||
"comfort_ac_away_temp": "Temperatura al preset Comfort in caso d'assenza (AC mode)",
|
||||
"boost_ac_away_temp": "Temperatura al preset Boost in caso d'assenza (AC mode)"
|
||||
@@ -187,6 +193,7 @@
|
||||
"heater_entity2_id": "Secondo riscaldatore",
|
||||
"heater_entity3_id": "Terzo riscaldatore",
|
||||
"heater_entity4_id": "Quarto riscaldatore",
|
||||
"heater_keep_alive": "Intervallo keep-alive dell'interruttore in secondi",
|
||||
"proportional_function": "Algoritmo",
|
||||
"climate_entity_id": "Primo termostato",
|
||||
"climate_entity2_id": "Secondo termostato",
|
||||
@@ -198,13 +205,15 @@
|
||||
"valve_entity3_id": "Terza valvola",
|
||||
"valve_entity4_id": "Quarta valvola",
|
||||
"auto_regulation_mode": "Autoregolamentazione",
|
||||
"inverse_switch_command": "Comando inverso"
|
||||
"inverse_switch_command": "Comando inverso",
|
||||
"auto_fan_mode": "Auto fan mode"
|
||||
},
|
||||
"data_description": {
|
||||
"heater_entity_id": "Entity id obbligatoria del primo riscaldatore",
|
||||
"heater_entity2_id": "Entity id del secondo riscaldatore facoltativo. Lasciare vuoto se non utilizzato",
|
||||
"heater_entity3_id": "Entity id del terzo riscaldatore facoltativo. Lasciare vuoto se non utilizzato",
|
||||
"heater_entity4_id": "Entity id del quarto riscaldatore facoltativo. Lasciare vuoto se non utilizzato",
|
||||
"heater_keep_alive": "Frequenza di aggiornamento dell'interruttore (facoltativo). Lasciare vuoto se non richiesto.",
|
||||
"proportional_function": "Algoritmo da utilizzare (il TPI per adesso è l'unico)",
|
||||
"climate_entity_id": "Entity id del primo termostato",
|
||||
"climate_entity2_id": "Entity id del secondo termostato",
|
||||
@@ -216,7 +225,8 @@
|
||||
"valve_entity3_id": "Entity id della terza valvola",
|
||||
"valve_entity4_id": "Entity id della quarta valvola",
|
||||
"auto_regulation_mode": "Autoregolamentazione",
|
||||
"inverse_switch_command": "Inverte il controllo dell'interruttore per un'installazione con filo pilota e diodo"
|
||||
"inverse_switch_command": "Inverte il controllo dell'interruttore per un'installazione con filo pilota e diodo",
|
||||
"auto_fan_mode": "Automatically activate fan when huge heating/cooling is necessary"
|
||||
}
|
||||
},
|
||||
"tpi": {
|
||||
@@ -234,6 +244,7 @@
|
||||
"eco_temp": "Temperatura nel preset Eco",
|
||||
"comfort_temp": "Temperatura nel preset Comfort",
|
||||
"boost_temp": "Temperatura nel preset Boost",
|
||||
"frost_temp": "Temperatura nel preset Frost protection",
|
||||
"eco_ac_temp": "Temperatura nel preset Eco (AC mode)",
|
||||
"comfort_ac_temp": "Temperatura nel preset Comfort (AC mode)",
|
||||
"boost_ac_temp": "Temperatura nel preset Boost (AC mode)"
|
||||
@@ -245,16 +256,16 @@
|
||||
"data": {
|
||||
"window_sensor_entity_id": "Entity id sensore finestra",
|
||||
"window_delay": "Ritardo sensore finestra (secondi)",
|
||||
"window_auto_open_threshold": "Soglia di diminuzione della temperatura per il rilevamento automatico della finestra aperta (in °/min)",
|
||||
"window_auto_close_threshold": "Soglia di aumento della temperatura per la fine del rilevamento automatico (in °/min)",
|
||||
"window_auto_open_threshold": "Soglia di diminuzione della temperatura per il rilevamento automatico della finestra aperta (in °/ora)",
|
||||
"window_auto_close_threshold": "Soglia di aumento della temperatura per la fine del rilevamento automatico (in °/ora)",
|
||||
"window_auto_max_duration": "Durata massima del rilevamento automatico della finestra aperta (in min)"
|
||||
},
|
||||
"data_description": {
|
||||
"window_sensor_entity_id": "Lasciare vuoto se non deve essere utilizzato alcun sensore finestra",
|
||||
"window_delay": "Ritardo in secondi prima che il rilevamento del sensore sia preso in considerazione",
|
||||
"window_auto_open_threshold": "Valore consigliato: tra 0.05 e 0.1 - Lasciare vuoto se il rilevamento automatico della finestra aperta non è utilizzato",
|
||||
"window_auto_close_threshold": "Valore consigliato: 0 - Lasciare vuoto se il rilevamento automatico della finestra aperta non è utilizzato",
|
||||
"window_auto_max_duration": "Valore consigliato: 60 minuti. Lasciare vuoto se il rilevamento automatico della finestra aperta non è utilizzato"
|
||||
"window_auto_open_threshold": "Valore consigliato: tra 3 e 10. Lasciare vuoto se il rilevamento automatico della finestra aperta non è utilizzato",
|
||||
"window_auto_close_threshold": "Valore consigliato: 0. Lasciare vuoto se il rilevamento automatico della finestra aperta non è utilizzato",
|
||||
"window_auto_max_duration": "Valore consigliato: 60 (un'ora). Lasciare vuoto se il rilevamento automatico della finestra aperta non è utilizzato"
|
||||
}
|
||||
},
|
||||
"motion": {
|
||||
@@ -285,6 +296,7 @@
|
||||
"eco_away_temp": "Temperatura al preset Eco in caso d'assenza",
|
||||
"comfort_away_temp": "Temperatura al preset Comfort in caso d'assenza",
|
||||
"boost_away_temp": "Temperatura al preset Boost in caso d'assenza",
|
||||
"frost_away_temp": "Temperatura al preset Frost protection in caso d'assenza",
|
||||
"eco_ac_away_temp": "Temperatura al preset Eco in caso d'assenza (AC mode)",
|
||||
"comfort_ac_away_temp": "Temperatura al preset Comfort in caso d'assenza (AC mode)",
|
||||
"boost_ac_away_temp": "Temperatura al preset Boost in caso d'assenza (AC mode)"
|
||||
@@ -330,8 +342,18 @@
|
||||
"auto_regulation_strong": "Forte",
|
||||
"auto_regulation_medium": "Media",
|
||||
"auto_regulation_light": "Leggera",
|
||||
"auto_regulation_expert": "Esperto",
|
||||
"auto_regulation_none": "Nessuna autoregolamentazione"
|
||||
}
|
||||
},
|
||||
"auto_fan_mode": {
|
||||
"options": {
|
||||
"auto_fan_none": "Nessune autofan",
|
||||
"auto_fan_low": "Leggera",
|
||||
"auto_fan_medium": "Media",
|
||||
"auto_fan_high": "Forte",
|
||||
"auto_fan_turbo": "Turbo"
|
||||
}
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
@@ -342,7 +364,8 @@
|
||||
"state": {
|
||||
"power": "Ripartizione",
|
||||
"security": "Sicurezza",
|
||||
"none": "Manuale"
|
||||
"none": "Manuale",
|
||||
"frost": "Gelo"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,21 +4,66 @@
|
||||
"flow_title": "Všestranná konfigurácia termostatu",
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "Typ všestranného termostatu",
|
||||
"data": {
|
||||
"thermostat_type": "Typ termostatu"
|
||||
},
|
||||
"data_description": {
|
||||
"thermostat_type": "Len jeden centrálny typ konfigurácie je možný"
|
||||
}
|
||||
},
|
||||
"menu": {
|
||||
"title": "Menu",
|
||||
"description": "Nakonfigurujte si termostat. Po zadaní všetkých požadovaných parametrov budete môcť dokončiť konfiguráciu.",
|
||||
"menu_options": {
|
||||
"main": "Hlavné atribúty",
|
||||
"central_boiler": "Centrálny kotol",
|
||||
"type": "Podklady",
|
||||
"tpi": "TPI parametre",
|
||||
"features": "Vlastnosti",
|
||||
"presets": "Predvoľby",
|
||||
"window": "Detekcia okien",
|
||||
"motion": "Detekcia pohybu",
|
||||
"power": "Správa napájania",
|
||||
"presence": "Detekcia prítomnosti",
|
||||
"advanced": "Pokročilé parametre",
|
||||
"finalize": "Všetko hotové",
|
||||
"configuration_not_complete": "Konfigurácia nie je dokončená"
|
||||
}
|
||||
},
|
||||
"main": {
|
||||
"title": "Pridajte nový všestranný termostat",
|
||||
"description": "Hlavné povinné atribúty",
|
||||
"data": {
|
||||
"name": "Názov",
|
||||
"thermostat_type": "Termostat typ",
|
||||
"temperature_sensor_entity_id": "ID entity snímača teploty",
|
||||
"last_seen_temperature_sensor_entity_id": "Dátum posledného zobrazenia izbovej teploty",
|
||||
"external_temperature_sensor_entity_id": "ID entity externého snímača teploty",
|
||||
"cycle_min": "Trvanie cyklu (minúty)",
|
||||
"temp_min": "Minimálna povolená teplota",
|
||||
"temp_max": "Maximálna povolená teplota",
|
||||
"step_temperature": "Krok teploty",
|
||||
"device_power": "Napájanie zariadenia",
|
||||
"use_central_mode": "Povoliť ovládanie centrálnou entitou (potrebná centrálna konfigurácia)",
|
||||
"use_main_central_config": "Použite dodatočnú centrálnu hlavnú konfiguráciu. Začiarknite, ak chcete použiť centrálnu hlavnú konfiguráciu (vonkajšia teplota, min, max, krok, ...).",
|
||||
"used_by_controls_central_boiler": "Používa sa centrálnym kotlom. Skontrolujte, či má mať tento VTherm ovládanie na centrálnom kotli"
|
||||
},
|
||||
"data_description": {
|
||||
"temperature_sensor_entity_id": "ID entity snímača izbovej teploty",
|
||||
"last_seen_temperature_sensor_entity_id": "Naposledy videný snímač izbovej teploty ID entity. Mal by to byť snímač dátumu a času",
|
||||
"external_temperature_sensor_entity_id": "ID entity snímača vonkajšej teploty. Nepoužíva sa, ak je zvolená centrálna konfigurácia"
|
||||
}
|
||||
},
|
||||
"features": {
|
||||
"title": "Vlastnosti",
|
||||
"description": "Vlastnosti termostatu",
|
||||
"data": {
|
||||
"use_window_feature": "Použite detekciu okien",
|
||||
"use_motion_feature": "Použite detekciu pohybu",
|
||||
"use_power_feature": "Použite správu napájania",
|
||||
"use_presence_feature": "Použite detekciu prítomnosti"
|
||||
"use_presence_feature": "Použite detekciu prítomnosti",
|
||||
"use_central_boiler_feature": "Použite centrálny kotol. Začiarknutím tohto políčka pridáte ovládanie do centrálneho kotla. Po zaškrtnutí tohto políčka budete musieť nakonfigurovať VTherm, ktorý bude mať ovládanie centrálneho kotla, aby sa prejavilo. Ak jeden VTherm vyžaduje ohrev, kotol sa zapne. Ak žiadny VTherm nevyžaduje ohrev, kotol sa vypne. Príkazy na zapnutie/vypnutie centrálneho kotla sú uvedené na príslušnej konfiguračnej stránke"
|
||||
}
|
||||
},
|
||||
"type": {
|
||||
@@ -29,6 +74,7 @@
|
||||
"heater_entity2_id": "2. spínač ohrievača",
|
||||
"heater_entity3_id": "3. spínač ohrievača",
|
||||
"heater_entity4_id": "4. spínač ohrievača",
|
||||
"heater_keep_alive": "Prepnite interval udržiavania v sekundách",
|
||||
"proportional_function": "Algoritmus",
|
||||
"climate_entity_id": "1. základná klíma",
|
||||
"climate_entity2_id": "2. základná klíma",
|
||||
@@ -39,16 +85,19 @@
|
||||
"valve_entity2_id": "2. ventil číslo",
|
||||
"valve_entity3_id": "3. ventil číslo",
|
||||
"valve_entity4_id": "4. ventil číslo",
|
||||
"auto_regulation_mode": "Self-regulation",
|
||||
"auto_regulation_dtemp": "Regulation threshold",
|
||||
"auto_regulation_periode_min": "Regulation minimal period",
|
||||
"inverse_switch_command": "Inverse switch command"
|
||||
"auto_regulation_mode": "Samoregulácia",
|
||||
"auto_regulation_dtemp": "Regulačný prah",
|
||||
"auto_regulation_periode_min": "Regulačné minimálne obdobie",
|
||||
"auto_regulation_use_device_temp": "Použite vnútornú teplotu podkladu",
|
||||
"inverse_switch_command": "Inverzný prepínací príkaz",
|
||||
"auto_fan_mode": "Režim automatického ventilátora"
|
||||
},
|
||||
"data_description": {
|
||||
"heater_entity_id": "ID entity povinného ohrievača",
|
||||
"heater_entity2_id": "Voliteľné ID entity 2. ohrievača. Ak sa nepoužíva, nechajte prázdne",
|
||||
"heater_entity3_id": "Voliteľné ID entity 3. ohrievača. Ak sa nepoužíva, nechajte prázdne",
|
||||
"heater_entity4_id": "Voliteľné ID entity 4. ohrievača. Ak sa nepoužíva, nechajte prázdne",
|
||||
"heater_keep_alive": "Voliteľný interval obnovy stavu spínača ohrievača. Ak to nie je potrebné, nechajte prázdne.",
|
||||
"proportional_function": "Algoritmus, ktorý sa má použiť (TPI je zatiaľ jediný)",
|
||||
"climate_entity_id": "ID základnej klimatickej entity",
|
||||
"climate_entity2_id": "2. základné identifikačné číslo klimatickej entity",
|
||||
@@ -59,10 +108,12 @@
|
||||
"valve_entity2_id": "2. ventil číslo entity id",
|
||||
"valve_entity3_id": "3. ventil číslo entity id",
|
||||
"valve_entity4_id": "4. ventil číslo entity id",
|
||||
"auto_regulation_mode": "Auto adjustment of the target temperature",
|
||||
"auto_regulation_dtemp": "The threshold in ° under which the temperature change will not be send",
|
||||
"auto_regulation_periode_min": "Duration in minutes between two regulation update",
|
||||
"inverse_switch_command": "For switch with pilot wire and diode you may need to inverse the command"
|
||||
"auto_regulation_mode": "Automatické nastavenie cieľovej teploty",
|
||||
"auto_regulation_dtemp": "Hranica v °, pod ktorou sa zmena teploty neodošle",
|
||||
"auto_regulation_periode_min": "Trvanie v minútach medzi dvoma aktualizáciami predpisov",
|
||||
"auto_regulation_use_device_temp": "Na urýchlenie samoregulácie použite prípadný vnútorný snímač teploty podkladu",
|
||||
"inverse_switch_command": "V prípade spínača s pilotným vodičom a diódou možno budete musieť príkaz invertovať",
|
||||
"auto_fan_mode": "Automaticky aktivujte ventilátor, keď je potrebné veľké vykurovanie/chladenie"
|
||||
}
|
||||
},
|
||||
"tpi": {
|
||||
@@ -70,19 +121,20 @@
|
||||
"description": "Časovo proporcionálne integrálne atribúty",
|
||||
"data": {
|
||||
"tpi_coef_int": "Koeficient na použitie pre vnútornú teplotnú deltu",
|
||||
"tpi_coef_ext": "Koeficient na použitie pre deltu vonkajšej teploty"
|
||||
"tpi_coef_ext": "Koeficient na použitie pre deltu vonkajšej teploty",
|
||||
"use_tpi_central_config": "Použite centrálnu konfiguráciu TPI"
|
||||
},
|
||||
"data_description": {
|
||||
"tpi_coef_int": "Koeficient na použitie pre vnútornú teplotnú deltu",
|
||||
"tpi_coef_ext": "Koeficient na použitie pre deltu vonkajšej teploty",
|
||||
"use_tpi_central_config": "Začiarknite, ak chcete použiť centrálnu konfiguráciu TPI. Zrušte začiarknutie, ak chcete použiť špecifickú konfiguráciu TPI pre tento VTherm"
|
||||
}
|
||||
},
|
||||
"presets": {
|
||||
"title": "Predvoľby",
|
||||
"description": "Pre každú predvoľbu zadajte cieľovú teplotu (0, ak chcete predvoľbu ignorovať)",
|
||||
"data": {
|
||||
"eco_temp": "Teplota v predvoľbe Eco",
|
||||
"comfort_temp": "Prednastavená teplota v komfortnom režime",
|
||||
"boost_temp": "Teplota v prednastavení Boost",
|
||||
"eco_ac_temp": "Teplota v režime Eco prednastavená pre režim AC",
|
||||
"comfort_ac_temp": "Teplota v režime Comfort je prednastavená pre režim AC",
|
||||
"boost_ac_temp": "Prednastavená teplota v režime Boost pre režim AC"
|
||||
"use_presets_central_config": "Použite konfiguráciu centrálnych predvolieb"
|
||||
}
|
||||
},
|
||||
"window": {
|
||||
@@ -91,16 +143,20 @@
|
||||
"data": {
|
||||
"window_sensor_entity_id": "ID entity snímača okna",
|
||||
"window_delay": "Oneskorenie snímača okna (sekundy)",
|
||||
"window_auto_open_threshold": "Prah poklesu teploty pre automatickú detekciu otvoreného okna (v °/min)",
|
||||
"window_auto_close_threshold": "Prahová hodnota zvýšenia teploty pre koniec automatickej detekcie (v °/min)",
|
||||
"window_auto_max_duration": "Maximálne trvanie automatickej detekcie otvoreného okna (v min)"
|
||||
"window_auto_open_threshold": "Prah poklesu teploty pre automatickú detekciu otvoreného okna (v °/hodina)",
|
||||
"window_auto_close_threshold": "Prahová hodnota zvýšenia teploty pre koniec automatickej detekcie (v °/hodina)",
|
||||
"window_auto_max_duration": "Maximálne trvanie automatickej detekcie otvoreného okna (v min)",
|
||||
"use_window_central_config": "Použite centrálnu konfiguráciu okna",
|
||||
"window_action": "Akcia"
|
||||
},
|
||||
"data_description": {
|
||||
"window_sensor_entity_id": "Nechajte prázdne, ak nemáte použiť žiadny okenný senzor",
|
||||
"window_delay": "Zohľadňuje sa oneskorenie v sekundách pred detekciou snímača",
|
||||
"window_auto_open_threshold": "Odporúčaná hodnota: medzi 0,05 a 0,1. Ak sa nepoužíva automatická detekcia otvoreného okna, nechajte prázdne",
|
||||
"window_auto_open_threshold": "Odporúčaná hodnota: medzi 3 a 10. Ak sa nepoužíva automatická detekcia otvoreného okna, nechajte prázdne",
|
||||
"window_auto_close_threshold": "Odporúčaná hodnota: 0. Ak sa nepoužíva automatická detekcia otvoreného okna, nechajte prázdne",
|
||||
"window_auto_max_duration": "Odporúčaná hodnota: 60 (jedna hodina). Ak sa nepoužíva automatická detekcia otvoreného okna, nechajte prázdne"
|
||||
"window_auto_max_duration": "Odporúčaná hodnota: 60 (jedna hodina). Ak sa nepoužíva automatická detekcia otvoreného okna, nechajte prázdne",
|
||||
"use_window_central_config": "Začiarknite, ak chcete použiť konfiguráciu centrálneho okna. Zrušte začiarknutie, ak chcete použiť špecifickú konfiguráciu okna pre tento VTherm",
|
||||
"window_action": "Akcia, ktorá sa má vykonať, ak sa okno zistí ako otvorené"
|
||||
}
|
||||
},
|
||||
"motion": {
|
||||
@@ -111,14 +167,16 @@
|
||||
"motion_delay": "Oneskorenie aktivácie",
|
||||
"motion_off_delay": "Oneskorenie deaktivácie",
|
||||
"motion_preset": "Prednastavený pohyb",
|
||||
"no_motion_preset": "Žiadna predvoľba pohybu"
|
||||
"no_motion_preset": "Žiadna predvoľba pohybu",
|
||||
"use_motion_central_config": "Použite centrálnu konfiguráciu pohybu"
|
||||
},
|
||||
"data_description": {
|
||||
"motion_sensor_entity_id": "ID entity snímača pohybu",
|
||||
"motion_delay": "Oneskorenie aktivácie pohybu (sekundy)",
|
||||
"motion_off_delay": "Oneskorenie deaktivácie pohybu (sekundy)",
|
||||
"motion_preset": "Prednastavené na použitie pri detekcii pohybu",
|
||||
"no_motion_preset": "Prednastavené na použitie, keď nie je detekovaný žiadny pohyb"
|
||||
"no_motion_preset": "Prednastavené na použitie, keď nie je detekovaný žiadny pohyb",
|
||||
"use_motion_central_config": "Začiarknite, ak chcete použiť konfiguráciu centrálneho pohybu. Zrušte začiarknutie, ak chcete použiť špecifickú konfiguráciu pohybu pre tento VTherm"
|
||||
}
|
||||
},
|
||||
"power": {
|
||||
@@ -127,20 +185,25 @@
|
||||
"data": {
|
||||
"power_sensor_entity_id": "ID entity snímača výkonu",
|
||||
"max_power_sensor_entity_id": "ID entity snímača maximálneho výkonu",
|
||||
"power_temp": "Teplota pre zníženie výkonu"
|
||||
"power_temp": "Teplota pre zníženie výkonu",
|
||||
"use_power_central_config": "Použite centrálnu konfiguráciu napájania"
|
||||
},
|
||||
"data_description": {
|
||||
"power_sensor_entity_id": "ID entity snímača výkonu",
|
||||
"max_power_sensor_entity_id": "ID entity snímača maximálneho výkonu",
|
||||
"power_temp": "Teplota pre zníženie výkonu",
|
||||
"use_power_central_config": "Začiarknite, ak chcete použiť konfiguráciu centrálneho napájania. Zrušte začiarknutie, ak chcete použiť špecifickú konfiguráciu napájania pre tento VTherm"
|
||||
}
|
||||
},
|
||||
"presence": {
|
||||
"title": "Riadenie prítomnosti",
|
||||
"description": "Atribúty správy prítomnosti.\nPoskytuje senzor prítomnosti vášho domova (pravda, ak je niekto prítomný).\nPotom zadajte buď predvoľbu, ktorá sa má použiť, keď je senzor prítomnosti nepravdivý, alebo posun teploty, ktorý sa má použiť.\nAk je zadaná predvoľba, posun sa nepoužije.\nAk sa nepoužije, ponechajte zodpovedajúce entity_id prázdne.",
|
||||
"data": {
|
||||
"presence_sensor_entity_id": "ID entity senzora prítomnosti",
|
||||
"eco_away_temp": "Teplota v prednastavenej Eco, keď nie je žiadna prítomnosť",
|
||||
"comfort_away_temp": "Teplota v režime Comfort je prednastavená, keď nie je prítomný",
|
||||
"boost_away_temp": "Prednastavená teplota v režime Boost, keď nie je prítomný",
|
||||
"eco_ac_away_temp": "Teplota v prednastavenej Eco, keď nie je prítomná v režime AC",
|
||||
"comfort_ac_away_temp": "Teplota v režime Comfort je prednastavená, keď nie je prítomný v režime AC",
|
||||
"boost_ac_away_temp": "Teplota v prednastavenom Boost, keď nie je prítomný v režime AC"
|
||||
"presence_sensor_entity_id": "Senzora prítomnosti",
|
||||
"use_presence_central_config": "Použite konfiguráciu centrálnej prítomnosti teploty. Ak chcete použiť špecifické teplotné entity, zrušte výber"
|
||||
},
|
||||
"data_description": {
|
||||
"presence_sensor_entity_id": "ID entity senzora prítomnosti"
|
||||
}
|
||||
},
|
||||
"advanced": {
|
||||
@@ -150,20 +213,23 @@
|
||||
"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"
|
||||
"security_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"
|
||||
"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",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"unknown": "Neočakávaná chyba",
|
||||
"unknown_entity": "Neznáme ID entity",
|
||||
"window_open_detection_method": "Mala by sa použiť iba jedna metóda detekcie otvoreného okna. Použite senzor alebo automatickú detekciu cez teplotný prah, ale nie oboje"
|
||||
"window_open_detection_method": "Mala by sa použiť iba jedna metóda detekcie otvoreného okna. Použite senzor alebo automatickú detekciu cez teplotný prah, ale nie oboje",
|
||||
"no_central_config": "Nemôžete zaškrtnúť „použiť centrálnu konfiguráciu“, pretože sa nenašla žiadna centrálna konfigurácia. Aby ste ho mohli používať, musíte si vytvoriť všestranný termostat typu „Central Configuration“."
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "Zariadenie je už nakonfigurované"
|
||||
@@ -173,31 +239,77 @@
|
||||
"flow_title": "Všestranná konfigurácia termostatu",
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "Pridajte nový všestranný termostat",
|
||||
"title": "Typ - {name}",
|
||||
"data": {
|
||||
"thermostat_type": "Typ termostatu"
|
||||
},
|
||||
"data_description": {
|
||||
"thermostat_type": "Je možný len jeden centrálny typ konfigurácie"
|
||||
}
|
||||
},
|
||||
"menu": {
|
||||
"title": "Menu",
|
||||
"description": "Nakonfigurujte si termostat. Po zadaní všetkých požadovaných parametrov budete môcť dokončiť konfiguráciu.",
|
||||
"menu_options": {
|
||||
"main": "Hlavné atribúty",
|
||||
"central_boiler": "Centrálny kotol",
|
||||
"type": "Podklady",
|
||||
"tpi": "TPI parametre",
|
||||
"features": "Vlastnosti",
|
||||
"presets": "Predvoľby",
|
||||
"window": "Detekcia okien",
|
||||
"motion": "Detekcia pohybu",
|
||||
"power": "Správa napájania",
|
||||
"presence": "Detekcia prítomnosti",
|
||||
"advanced": "Pokročilé parametre",
|
||||
"finalize": "Všetko hotové",
|
||||
"configuration_not_complete": "Konfigurácia nie je dokončená"
|
||||
}
|
||||
},
|
||||
"main": {
|
||||
"title": "Hlavný - {name}",
|
||||
"description": "Hlavné povinné atribúty",
|
||||
"data": {
|
||||
"name": "Názov",
|
||||
"thermostat_type": "Termostat typ",
|
||||
"temperature_sensor_entity_id": "ID entity snímača teploty",
|
||||
"last_seen_temperature_sensor_entity_id": "Dátum posledného zobrazenia izbovej teploty",
|
||||
"external_temperature_sensor_entity_id": "ID entity externého snímača teploty",
|
||||
"cycle_min": "Trvanie cyklu (minúty)",
|
||||
"temp_min": "Minimálna povolená teplota",
|
||||
"temp_max": "Maximálna povolená teplota",
|
||||
"step_temperature": "Krok teploty",
|
||||
"device_power": "Výkon zariadenia (kW)",
|
||||
"use_central_mode": "Povoliť ovládanie centrálnou entitou (vyžaduje centrálnu konfiguráciu). Zaškrtnutím povolíte ovládanie VTherm pomocou vybraných entít central_mode.",
|
||||
"use_main_central_config": "Použite dodatočnú centrálnu hlavnú konfiguráciu. Začiarknite, ak chcete použiť centrálnu hlavnú konfiguráciu (vonkajšia teplota, min, max, krok, ...).",
|
||||
"used_by_controls_central_boiler": "Používa sa centrálnym kotlom. Skontrolujte, či má mať tento VTherm ovládanie na centrálnom kotli"
|
||||
},
|
||||
"data_description": {
|
||||
"temperature_sensor_entity_id": "ID entity snímača izbovej teploty",
|
||||
"last_seen_temperature_sensor_entity_id": "Naposledy videný snímač izbovej teploty ID entity. Mal by to byť snímač dátumu a času",
|
||||
"external_temperature_sensor_entity_id": "ID entity snímača vonkajšej teploty. Nepoužíva sa, ak je zvolená centrálna konfigurácia"
|
||||
}
|
||||
},
|
||||
"features": {
|
||||
"title": "Vlastnosti - {name}",
|
||||
"description": "Vlastnosti termostatu",
|
||||
"data": {
|
||||
"use_window_feature": "Použite detekciu okien",
|
||||
"use_motion_feature": "Použite detekciu pohybu",
|
||||
"use_power_feature": "Použite správu napájania",
|
||||
"use_presence_feature": "Použite detekciu prítomnosti"
|
||||
"use_presence_feature": "Použite detekciu prítomnosti",
|
||||
"use_central_boiler_feature": "Použite centrálny kotol. Začiarknutím tohto políčka pridáte ovládanie do centrálneho kotla. Po zaškrtnutí tohto políčka budete musieť nakonfigurovať VTherm, ktorý bude mať ovládanie centrálneho kotla, aby sa prejavilo. Ak jeden VTherm vyžaduje ohrev, kotol sa zapne. Ak žiadny VTherm nevyžaduje ohrev, kotol sa vypne. Príkazy na zapnutie/vypnutie centrálneho kotla sú uvedené na príslušnej konfiguračnej stránke"
|
||||
}
|
||||
},
|
||||
"type": {
|
||||
"title": "Prepojené entity",
|
||||
"title": "Prepojené entity - {name}",
|
||||
"description": "Atribúty prepojených entít",
|
||||
"data": {
|
||||
"heater_entity_id": "Spínač ohrievača",
|
||||
"heater_entity2_id": "2. spínač ohrievača",
|
||||
"heater_entity3_id": "3. spínač ohrievača",
|
||||
"heater_entity4_id": "4. spínač ohrievača",
|
||||
"heater_keep_alive": "Prepnite interval udržiavania v sekundách",
|
||||
"proportional_function": "Algoritmus",
|
||||
"climate_entity_id": "Základná klíma",
|
||||
"climate_entity2_id": "2. základná klíma",
|
||||
@@ -208,16 +320,19 @@
|
||||
"valve_entity2_id": "2. ventil číslo",
|
||||
"valve_entity3_id": "3. ventil číslo",
|
||||
"valve_entity4_id": "4. ventil číslo",
|
||||
"auto_regulation_mode": "Self-regulation",
|
||||
"auto_regulation_dtemp": "Regulation threshold",
|
||||
"auto_regulation_periode_min": "Regulation minimal period",
|
||||
"inverse_switch_command": "Inverse switch command"
|
||||
"auto_regulation_mode": "Samoregulácia",
|
||||
"auto_regulation_dtemp": "Regulačný prah",
|
||||
"auto_regulation_periode_min": "Regulačné minimálne obdobie",
|
||||
"auto_regulation_use_device_temp": "Použite vnútornú teplotu podkladu",
|
||||
"inverse_switch_command": "Inverzný prepínací príkaz",
|
||||
"auto_fan_mode": "Režim automatického ventilátora"
|
||||
},
|
||||
"data_description": {
|
||||
"heater_entity_id": "ID entity povinného ohrievača",
|
||||
"heater_entity2_id": "Voliteľné ID entity 2. ohrievača. Ak sa nepoužíva, nechajte prázdne",
|
||||
"heater_entity3_id": "Voliteľné ID entity 3. ohrievača. Ak sa nepoužíva, nechajte prázdne",
|
||||
"heater_entity4_id": "Voliteľné ID entity 4. ohrievača. Ak sa nepoužíva, nechajte prázdne",
|
||||
"heater_keep_alive": "Voliteľný interval obnovy stavu spínača ohrievača. Ak to nie je potrebné, nechajte prázdne.",
|
||||
"proportional_function": "Algoritmus, ktorý sa má použiť (TPI je zatiaľ jediný)",
|
||||
"climate_entity_id": "ID základnej klimatickej entity",
|
||||
"climate_entity2_id": "2. základný identifikátor klimatickej entity",
|
||||
@@ -228,111 +343,129 @@
|
||||
"valve_entity2_id": "2. ventil číslo entity id",
|
||||
"valve_entity3_id": "3. ventil číslo entity id",
|
||||
"valve_entity4_id": "4. ventil číslo entity id",
|
||||
"auto_regulation_mode": "Auto adjustment of the target temperature",
|
||||
"auto_regulation_dtemp": "The threshold in ° under which the temperature change will not be send",
|
||||
"auto_regulation_periode_min": "Duration in minutes between two regulation update",
|
||||
"inverse_switch_command": "For switch with pilot wire and diode you may need to inverse the command"
|
||||
"auto_regulation_mode": "Automatické nastavenie cieľovej teploty",
|
||||
"auto_regulation_dtemp": "Hranica v °, pod ktorou sa zmena teploty neodošle",
|
||||
"auto_regulation_periode_min": "Trvanie v minútach medzi dvoma aktualizáciami predpisov",
|
||||
"auto_regulation_use_device_temp": "Na urýchlenie samoregulácie použite prípadný vnútorný snímač teploty podkladu",
|
||||
"inverse_switch_command": "V prípade spínača s pilotným vodičom a diódou možno budete musieť príkaz invertovať",
|
||||
"auto_fan_mode": "Automaticky aktivujte ventilátor, keď je potrebné veľké vykurovanie/chladenie"
|
||||
}
|
||||
},
|
||||
"tpi": {
|
||||
"title": "TPI",
|
||||
"title": "TPI - {name}",
|
||||
"description": "Časovo proporcionálne integrálne atribúty",
|
||||
"data": {
|
||||
"tpi_coef_int": "Koeficient na použitie pre vnútornú teplotnú deltu",
|
||||
"tpi_coef_ext": "Koeficient na použitie pre vonkajšiu teplotnú deltu"
|
||||
"tpi_coef_ext": "Koeficient na použitie pre vonkajšiu teplotnú deltu",
|
||||
"use_tpi_central_config": "Použite centrálnu konfiguráciu TPI"
|
||||
},
|
||||
"data_description": {
|
||||
"tpi_coef_int": "Koeficient na použitie pre vnútornú teplotnú deltu",
|
||||
"tpi_coef_ext": "Koeficient na použitie pre deltu vonkajšej teploty",
|
||||
"use_tpi_central_config": "Začiarknite, ak chcete použiť centrálnu konfiguráciu TPI. Zrušte začiarknutie, ak chcete použiť špecifickú konfiguráciu TPI pre tento VTherm"
|
||||
}
|
||||
},
|
||||
"presets": {
|
||||
"title": "Predvoľby",
|
||||
"title": "Predvoľby - {name}",
|
||||
"description": "Pre každú predvoľbu zadajte cieľovú teplotu (0, ak chcete predvoľbu ignorovať)",
|
||||
"data": {
|
||||
"eco_temp": "Teplota v predvoľbe Eco",
|
||||
"comfort_temp": "Prednastavená teplota v komfortnom režime",
|
||||
"boost_temp": "Teplota v prednastavení Boost",
|
||||
"eco_ac_temp": "Teplota v režime Eco prednastavená pre režim AC",
|
||||
"comfort_ac_temp": "Teplota v režime Comfort je prednastavená pre režim AC",
|
||||
"boost_ac_temp": "Prednastavená teplota v režime Boost pre režim AC"
|
||||
"use_presets_central_config": "Použite konfiguráciu centrálnych predvolieb"
|
||||
}
|
||||
},
|
||||
"window": {
|
||||
"title": "Správa okien",
|
||||
"title": "Správa okien - {name}",
|
||||
"description": "Otvoriť správu okien.\nAk sa príslušné entity_id nepoužíva, ponechajte prázdne\nMôžete tiež nakonfigurovať automatickú detekciu otvoreného okna na základe poklesu teploty",
|
||||
"data": {
|
||||
"window_sensor_entity_id": "ID entity snímača okna",
|
||||
"window_delay": "Oneskorenie snímača okna (sekundy)",
|
||||
"window_auto_open_threshold": "Prah poklesu teploty pre automatickú detekciu otvoreného okna (v °/min)",
|
||||
"window_auto_close_threshold": "Prahová hodnota zvýšenia teploty pre koniec automatickej detekcie (v °/min)",
|
||||
"window_auto_max_duration": "Maximálne trvanie automatickej detekcie otvoreného okna (v min)"
|
||||
"window_auto_open_threshold": "Prah poklesu teploty pre automatickú detekciu otvoreného okna (v °/hodina)",
|
||||
"window_auto_close_threshold": "Prahová hodnota zvýšenia teploty pre koniec automatickej detekcie (v °/hodina)",
|
||||
"window_auto_max_duration": "Maximálne trvanie automatickej detekcie otvoreného okna (v min)",
|
||||
"use_window_central_config": "Použite centrálnu konfiguráciu okna",
|
||||
"window_action": "Akcia"
|
||||
},
|
||||
"data_description": {
|
||||
"window_sensor_entity_id": "Nechajte prázdne, ak nemáte použiť žiadny okenný senzor",
|
||||
"window_delay": "Zohľadňuje sa oneskorenie v sekundách pred detekciou snímača",
|
||||
"window_auto_open_threshold": "Odporúčaná hodnota: medzi 0,05 a 0,1. Ak sa nepoužíva automatická detekcia otvoreného okna, nechajte prázdne",
|
||||
"window_auto_open_threshold": "Odporúčaná hodnota: medzi 3 a 10. Ak sa nepoužíva automatická detekcia otvoreného okna, nechajte prázdne",
|
||||
"window_auto_close_threshold": "Odporúčaná hodnota: 0. Ak sa nepoužíva automatická detekcia otvoreného okna, nechajte prázdne",
|
||||
"window_auto_max_duration": "Odporúčaná hodnota: 60 (jedna hodina). Ak sa nepoužíva automatická detekcia otvoreného okna, nechajte prázdne"
|
||||
"window_auto_max_duration": "Odporúčaná hodnota: 60 (jedna hodina). Ak sa nepoužíva automatická detekcia otvoreného okna, nechajte prázdne",
|
||||
"use_window_central_config": "Začiarknite, ak chcete použiť konfiguráciu centrálneho okna. Zrušte začiarknutie, ak chcete použiť špecifickú konfiguráciu okna pre tento VTherm",
|
||||
"window_action": "Akcia, ktorá sa má vykonať, ak sa okno zistí ako otvorené"
|
||||
}
|
||||
},
|
||||
"motion": {
|
||||
"title": "Riadenie pohybu",
|
||||
"title": "Riadenie pohybu - {name}",
|
||||
"description": "Správa snímača pohybu. Predvoľba sa môže automaticky prepínať v závislosti od detekcie pohybu\nAk sa nepoužíva, ponechajte zodpovedajúce entity_id prázdne.\nmotion_preset a no_motion_preset by mali byť nastavené na zodpovedajúci názov predvoľby",
|
||||
"data": {
|
||||
"motion_sensor_entity_id": "ID entity snímača pohybu",
|
||||
"motion_delay": "Oneskorenie aktivácie",
|
||||
"motion_off_delay": "Oneskorenie deaktivácie",
|
||||
"motion_preset": "Prednastavený pohyb",
|
||||
"no_motion_preset": "Žiadna predvoľba pohybu"
|
||||
"no_motion_preset": "Žiadna predvoľba pohybu",
|
||||
"use_motion_central_config": "Použite centrálnu konfiguráciu pohybu"
|
||||
},
|
||||
"data_description": {
|
||||
"motion_sensor_entity_id": "ID entity snímača pohybu",
|
||||
"motion_delay": "Oneskorenie aktivácie pohybu (sekundy)",
|
||||
"motion_off_delay": "Oneskorenie deaktivácie pohybu (sekundy)",
|
||||
"motion_preset": "Prednastavené na použitie pri detekcii pohybu",
|
||||
"no_motion_preset": "Prednastavené na použitie, keď nie je detekovaný žiadny pohyb"
|
||||
"no_motion_preset": "Prednastavené na použitie, keď nie je detekovaný žiadny pohyb",
|
||||
"use_motion_central_config": "Začiarknite, ak chcete použiť konfiguráciu centrálneho pohybu. Zrušte začiarknutie, ak chcete použiť špecifickú konfiguráciu pohybu pre tento VTherm"
|
||||
}
|
||||
},
|
||||
"power": {
|
||||
"title": "Správa napájania",
|
||||
"title": "Správa napájania - {name}",
|
||||
"description": "Atribúty správy napájania.\nPoskytuje senzor výkonu a maximálneho výkonu vášho domova.\nPotom zadajte spotrebu energie ohrievača, keď je zapnutý.\nVšetky senzory a výkon zariadenia by mali mať rovnakú jednotku (kW alebo W).\nPonechajte zodpovedajúce entity_id prázdne ak sa nepoužíva.",
|
||||
"data": {
|
||||
"power_sensor_entity_id": "ID entity snímača výkonu",
|
||||
"max_power_sensor_entity_id": "ID entity snímača maximálneho výkonu",
|
||||
"power_temp": "Teplota pre zníženie výkonu"
|
||||
"power_temp": "Teplota pre zníženie výkonu",
|
||||
"use_power_central_config": "Použite centrálnu konfiguráciu napájania"
|
||||
},
|
||||
"data_description": {
|
||||
"power_sensor_entity_id": "ID entity snímača výkonu",
|
||||
"max_power_sensor_entity_id": "ID entity snímača maximálneho výkonu",
|
||||
"power_temp": "Teplota pre zníženie výkonu",
|
||||
"use_power_central_config": "Začiarknite, ak chcete použiť konfiguráciu centrálneho napájania. Zrušte začiarknutie, ak chcete použiť špecifickú konfiguráciu napájania pre tento VTherm"
|
||||
}
|
||||
},
|
||||
"presence": {
|
||||
"title": "Riadenie prítomnosti",
|
||||
"description": "Atribúty správy prítomnosti.\nPoskytuje senzor prítomnosti vášho domova (pravda, ak je niekto prítomný).\nPotom zadajte buď predvoľbu, ktorá sa má použiť, keď je senzor prítomnosti nepravdivý, alebo posun teploty, ktorý sa má použiť.\nAk je zadaná predvoľba, posun sa nepoužije.\nAk sa nepoužije, ponechajte zodpovedajúce entity_id prázdne.",
|
||||
"title": "Prítommnosť - {name}",
|
||||
"description": "Atribúty riadenia prítomnosti.\nPoskytuje senzor prítomnosti vášho domova (pravda, je niekto prítomný) a poskytuje zodpovedajúce prednastavené nastavenie teploty.",
|
||||
"data": {
|
||||
"presence_sensor_entity_id": "ID entity senzora prítomnosti (pravda je prítomná)",
|
||||
"eco_away_temp": "Teplota v prednastavenej Eco, keď nie je žiadna prítomnosť",
|
||||
"comfort_away_temp": "Teplota v režime Comfort je prednastavená, keď nie je prítomný",
|
||||
"boost_away_temp": "Prednastavená teplota v režime Boost, keď nie je prítomný",
|
||||
"eco_ac_away_temp": "Teplota v prednastavenej Eco, keď nie je prítomná v režime AC",
|
||||
"comfort_ac_away_temp": "Teplota v režime Comfort je prednastavená, keď nie je prítomný v režime AC",
|
||||
"boost_ac_away_temp": "Teplota v prednastavenom Boost, keď nie je prítomný v režime AC"
|
||||
"presence_sensor_entity_id": "Senzor prítomnosti",
|
||||
"use_presence_central_config": "Použite konfiguráciu centrálnej prítomnosti teploty. Ak chcete použiť špecifické entity teploty, zrušte začiarknutie"
|
||||
},
|
||||
"data_description": {
|
||||
"presence_sensor_entity_id": "ID entity senzora prítomnosti"
|
||||
}
|
||||
},
|
||||
"advanced": {
|
||||
"title": "Pokročilé parametre",
|
||||
"title": "Pokročilé parametre - {name}",
|
||||
"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"
|
||||
"security_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"
|
||||
"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",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"unknown": "Neočakávaná chyba",
|
||||
"unknown_entity": "Neznáme ID entity",
|
||||
"window_open_detection_method": "Mala by sa použiť iba jedna metóda detekcie otvoreného okna. Použite senzor alebo automatickú detekciu cez teplotný prah, ale nie oboje"
|
||||
"window_open_detection_method": "Mala by sa použiť iba jedna metóda detekcie otvoreného okna. Použite senzor alebo automatickú detekciu cez teplotný prah, ale nie oboje",
|
||||
"no_central_config": "Nemôžete zaškrtnúť „použiť centrálnu konfiguráciu“, pretože sa nenašla žiadna centrálna konfigurácia. Aby ste ho mohli používať, musíte si vytvoriť všestranný termostat typu „Central Configuration“.",
|
||||
"service_configuration_format": "Formát konfigurácie služby je nesprávny"
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "Zariadenie je už nakonfigurované"
|
||||
@@ -341,18 +474,45 @@
|
||||
"selector": {
|
||||
"thermostat_type": {
|
||||
"options": {
|
||||
"thermostat_central_config": "Centrálna konfigurácia",
|
||||
"thermostat_over_switch": "Termostat nad spínačom",
|
||||
"thermostat_over_climate": "Termostat nad iným termostatom",
|
||||
"thermostat_over_valve": "Thermostat over a valve"
|
||||
"thermostat_over_valve": "Termostat nad ventilom"
|
||||
}
|
||||
},
|
||||
"auto_regulation_mode": {
|
||||
"options": {
|
||||
"auto_regulation_slow": "Slow",
|
||||
"auto_regulation_strong": "Strong",
|
||||
"auto_regulation_medium": "Medium",
|
||||
"auto_regulation_light": "Light",
|
||||
"auto_regulation_none": "No auto-regulation"
|
||||
"auto_regulation_slow": "Pomalé",
|
||||
"auto_regulation_strong": "Silné",
|
||||
"auto_regulation_medium": "Stredné",
|
||||
"auto_regulation_light": "Jemné",
|
||||
"auto_regulation_expert": "Expertné",
|
||||
"auto_regulation_none": "Nie auto-regulácia"
|
||||
}
|
||||
},
|
||||
"auto_fan_mode": {
|
||||
"options": {
|
||||
"auto_fan_none": "Žiadny automatický ventilátor",
|
||||
"auto_fan_low": "Nízky",
|
||||
"auto_fan_medium": "Stredný",
|
||||
"auto_fan_high": "Vysoký",
|
||||
"auto_fan_turbo": "Turbo"
|
||||
}
|
||||
},
|
||||
"window_action": {
|
||||
"options": {
|
||||
"window_turn_off": "Vypnúť",
|
||||
"window_fan_only": "Len ventilátor",
|
||||
"window_frost_temp": "Ochrana pred mrazom",
|
||||
"window_eco_temp": "Eco"
|
||||
}
|
||||
},
|
||||
"presets": {
|
||||
"options": {
|
||||
"frost": "Ochrana proti mrazu",
|
||||
"eco": "Eco",
|
||||
"comfort": "Komfort",
|
||||
"boost": "Boost"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -369,6 +529,53 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"number": {
|
||||
"frost_temp": {
|
||||
"name": "Mráz"
|
||||
},
|
||||
"eco_temp": {
|
||||
"name": "Eco"
|
||||
},
|
||||
"comfort_temp": {
|
||||
"name": "Komfort"
|
||||
},
|
||||
"boost_temp": {
|
||||
"name": "Boost"
|
||||
},
|
||||
"frost_ac_temp": {
|
||||
"name": "Mráz ac"
|
||||
},
|
||||
"eco_ac_temp": {
|
||||
"name": "Eco ac"
|
||||
},
|
||||
"comfort_ac_temp": {
|
||||
"name": "Komfort ac"
|
||||
},
|
||||
"boost_ac_temp": {
|
||||
"name": "Boost ac"
|
||||
},
|
||||
"frost_away_temp": {
|
||||
"name": "Mráz mimo"
|
||||
},
|
||||
"eco_away_temp": {
|
||||
"name": "Eko mimo"
|
||||
},
|
||||
"comfort_away_temp": {
|
||||
"name": "Komfort mimo"
|
||||
},
|
||||
"boost_away_temp": {
|
||||
"name": "Boost mimo"
|
||||
},
|
||||
"eco_ac_away_temp": {
|
||||
"name": "Eco ac mimo"
|
||||
},
|
||||
"comfort_ac_away_temp": {
|
||||
"name": "Komfort ac mimo"
|
||||
},
|
||||
"boost_ac_away_temp": {
|
||||
"name": "Boost ac mimo"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,8 @@ import logging
|
||||
from typing import Any
|
||||
from enum import StrEnum
|
||||
|
||||
from homeassistant.const import ATTR_ENTITY_ID, STATE_ON, UnitOfTemperature
|
||||
from homeassistant.const import ATTR_ENTITY_ID, STATE_ON, STATE_UNAVAILABLE
|
||||
from homeassistant.core import State
|
||||
|
||||
from homeassistant.exceptions import ServiceNotFound
|
||||
|
||||
@@ -29,8 +30,10 @@ from homeassistant.components.number import SERVICE_SET_VALUE
|
||||
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
from homeassistant.helpers.event import async_call_later
|
||||
from homeassistant.util.unit_conversion import TemperatureConverter
|
||||
|
||||
from .const import UnknownEntity, overrides
|
||||
from .keep_alive import IntervalCaller
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -111,18 +114,18 @@ class UnderlyingEntity:
|
||||
# This should be the correct way to handle turn_off and turn_on but this breaks the unit test
|
||||
# will an not understandable error: TypeError: object MagicMock can't be used in 'await' expression
|
||||
async def turn_off(self):
|
||||
""" Turn off the underlying equipement.
|
||||
Need to be overriden"""
|
||||
"""Turn off the underlying equipement.
|
||||
Need to be overriden"""
|
||||
return NotImplementedError
|
||||
|
||||
async def turn_on(self):
|
||||
""" Turn off the underlying equipement.
|
||||
Need to be overriden"""
|
||||
"""Turn off the underlying equipement.
|
||||
Need to be overriden"""
|
||||
return NotImplementedError
|
||||
|
||||
@property
|
||||
def is_inversed(self):
|
||||
""" Tells if the switch command should be inversed"""
|
||||
"""Tells if the switch command should be inversed"""
|
||||
return False
|
||||
|
||||
def remove_entity(self):
|
||||
@@ -132,16 +135,17 @@ class UnderlyingEntity:
|
||||
async def check_initial_state(self, hvac_mode: HVACMode):
|
||||
"""Prevent the underlying to be on but thermostat is off"""
|
||||
if hvac_mode == HVACMode.OFF and self.is_device_active:
|
||||
_LOGGER.warning(
|
||||
_LOGGER.info(
|
||||
"%s - The hvac mode is OFF, but the underlying device is ON. Turning off device %s",
|
||||
self,
|
||||
self._entity_id,
|
||||
)
|
||||
await self.set_hvac_mode(hvac_mode)
|
||||
elif hvac_mode != HVACMode.OFF and not self.is_device_active:
|
||||
_LOGGER.warning(
|
||||
"%s - The hvac mode is ON, but the underlying device is not ON. Turning on device %s",
|
||||
_LOGGER.info(
|
||||
"%s - The hvac mode is %s, but the underlying device is not ON. Turning on device %s if needed",
|
||||
self,
|
||||
hvac_mode,
|
||||
self._entity_id,
|
||||
)
|
||||
await self.set_hvac_mode(hvac_mode)
|
||||
@@ -164,7 +168,11 @@ class UnderlyingEntity:
|
||||
"""Starting cycle for switch"""
|
||||
|
||||
def _cancel_cycle(self):
|
||||
""" Stops an eventual cycle """
|
||||
"""Stops an eventual cycle"""
|
||||
|
||||
def cap_sent_value(self, value) -> float:
|
||||
"""capping of the value send to the underlying eqt"""
|
||||
return value
|
||||
|
||||
|
||||
class UnderlyingSwitch(UnderlyingEntity):
|
||||
@@ -181,6 +189,7 @@ class UnderlyingSwitch(UnderlyingEntity):
|
||||
thermostat: Any,
|
||||
switch_entity_id: str,
|
||||
initial_delay_sec: int,
|
||||
keep_alive_sec: float,
|
||||
) -> None:
|
||||
"""Initialize the underlying switch"""
|
||||
|
||||
@@ -196,6 +205,7 @@ class UnderlyingSwitch(UnderlyingEntity):
|
||||
self._on_time_sec = 0
|
||||
self._off_time_sec = 0
|
||||
self._hvac_mode = None
|
||||
self._keep_alive = IntervalCaller(hass, keep_alive_sec)
|
||||
|
||||
@property
|
||||
def initial_delay_sec(self):
|
||||
@@ -205,9 +215,19 @@ class UnderlyingSwitch(UnderlyingEntity):
|
||||
@overrides
|
||||
@property
|
||||
def is_inversed(self):
|
||||
""" Tells if the switch command should be inversed"""
|
||||
"""Tells if the switch command should be inversed"""
|
||||
return self._thermostat.is_inversed
|
||||
|
||||
@property
|
||||
def keep_alive_sec(self) -> float:
|
||||
"""Return the switch keep-alive interval in seconds."""
|
||||
return self._keep_alive.interval_sec
|
||||
|
||||
@overrides
|
||||
def startup(self):
|
||||
super().startup()
|
||||
self._keep_alive.set_async_action(self._keep_alive_callback)
|
||||
|
||||
# @overrides this breaks some unit tests TypeError: object MagicMock can't be used in 'await' expression
|
||||
async def set_hvac_mode(self, hvac_mode: HVACMode) -> bool:
|
||||
"""Set the HVACmode. Returns true if something have change"""
|
||||
@@ -227,41 +247,71 @@ class UnderlyingSwitch(UnderlyingEntity):
|
||||
def is_device_active(self):
|
||||
"""If the toggleable device is currently active."""
|
||||
real_state = self._hass.states.is_state(self._entity_id, STATE_ON)
|
||||
return (self.is_inversed and not real_state) or (not self.is_inversed and real_state)
|
||||
return (self.is_inversed and not real_state) or (
|
||||
not self.is_inversed and real_state
|
||||
)
|
||||
|
||||
async def _keep_alive_callback(self):
|
||||
"""Keep alive: Turn on if already turned on, turn off if already turned off."""
|
||||
timer = self._keep_alive.backoff_timer
|
||||
state: State | None = self._hass.states.get(self._entity_id)
|
||||
# Normal, expected state.state values are "on" and "off". An absent
|
||||
# underlying MQTT switch was observed to produce either state == None
|
||||
# or state.state == STATE_UNAVAILABLE ("unavailable").
|
||||
if state is None or state.state == STATE_UNAVAILABLE:
|
||||
if timer.is_ready():
|
||||
_LOGGER.warning(
|
||||
"Entity %s is not available (state: %s). Will keep trying "
|
||||
"keep alive calls, but won't log this condition every time.",
|
||||
self._entity_id,
|
||||
state.state if state else "None",
|
||||
)
|
||||
else:
|
||||
if timer.in_progress:
|
||||
timer.reset()
|
||||
_LOGGER.warning(
|
||||
"Entity %s has recovered (state: %s).",
|
||||
self._entity_id,
|
||||
state.state,
|
||||
)
|
||||
await (self.turn_on() if self.is_device_active else self.turn_off())
|
||||
|
||||
# @overrides this breaks some unit tests TypeError: object MagicMock can't be used in 'await' expression
|
||||
async def turn_off(self):
|
||||
"""Turn heater toggleable device off."""
|
||||
self._keep_alive.cancel() # Cancel early to avoid a turn_on/turn_off race condition
|
||||
_LOGGER.debug("%s - Stopping underlying entity %s", self, self._entity_id)
|
||||
command = SERVICE_TURN_OFF if not self.is_inversed else SERVICE_TURN_ON
|
||||
domain = self._entity_id.split('.')[0]
|
||||
domain = self._entity_id.split(".")[0]
|
||||
# This may fails if called after shutdown
|
||||
try:
|
||||
data = {ATTR_ENTITY_ID: self._entity_id}
|
||||
await self._hass.services.async_call(
|
||||
domain,
|
||||
command,
|
||||
data,
|
||||
)
|
||||
try:
|
||||
data = {ATTR_ENTITY_ID: self._entity_id}
|
||||
await self._hass.services.async_call(domain, command, data)
|
||||
self._keep_alive.set_async_action(self._keep_alive_callback)
|
||||
except Exception:
|
||||
self._keep_alive.cancel()
|
||||
raise
|
||||
except ServiceNotFound as err:
|
||||
_LOGGER.error(err)
|
||||
|
||||
async def turn_on(self):
|
||||
"""Turn heater toggleable device on."""
|
||||
self._keep_alive.cancel() # Cancel early to avoid a turn_on/turn_off race condition
|
||||
_LOGGER.debug("%s - Starting underlying entity %s", self, self._entity_id)
|
||||
command = SERVICE_TURN_ON if not self.is_inversed else SERVICE_TURN_OFF
|
||||
domain = self._entity_id.split('.')[0]
|
||||
domain = self._entity_id.split(".")[0]
|
||||
try:
|
||||
data = {ATTR_ENTITY_ID: self._entity_id}
|
||||
await self._hass.services.async_call(
|
||||
domain,
|
||||
command,
|
||||
data,
|
||||
)
|
||||
try:
|
||||
data = {ATTR_ENTITY_ID: self._entity_id}
|
||||
await self._hass.services.async_call(domain, command, data)
|
||||
self._keep_alive.set_async_action(self._keep_alive_callback)
|
||||
except Exception:
|
||||
self._keep_alive.cancel()
|
||||
raise
|
||||
except ServiceNotFound as err:
|
||||
_LOGGER.error(err)
|
||||
|
||||
|
||||
@overrides
|
||||
async def start_cycle(
|
||||
self,
|
||||
@@ -348,8 +398,8 @@ class UnderlyingSwitch(UnderlyingEntity):
|
||||
if await self._thermostat.check_overpowering():
|
||||
_LOGGER.debug("%s - End of cycle (3)", self)
|
||||
return
|
||||
# Security mode could have change the on_time percent
|
||||
await self._thermostat.check_security()
|
||||
# safety mode could have change the on_time percent
|
||||
await self._thermostat.check_safety()
|
||||
time = self._on_time_sec
|
||||
|
||||
action_label = "start"
|
||||
@@ -415,6 +465,7 @@ class UnderlyingSwitch(UnderlyingEntity):
|
||||
def remove_entity(self):
|
||||
"""Remove the entity after stopping its cycle"""
|
||||
self._cancel_cycle()
|
||||
self._keep_alive.cancel()
|
||||
|
||||
|
||||
class UnderlyingClimate(UnderlyingEntity):
|
||||
@@ -437,6 +488,7 @@ class UnderlyingClimate(UnderlyingEntity):
|
||||
entity_id=climate_entity_id,
|
||||
)
|
||||
self._underlying_climate = None
|
||||
self._last_sent_temperature = None
|
||||
|
||||
def find_underlying_climate(self) -> ClimateEntity:
|
||||
"""Find the underlying climate entity"""
|
||||
@@ -457,8 +509,8 @@ class UnderlyingClimate(UnderlyingEntity):
|
||||
self._underlying_climate,
|
||||
)
|
||||
else:
|
||||
_LOGGER.error(
|
||||
"%s - Cannot find the underlying climate entity: %s. Thermostat will not be operational",
|
||||
_LOGGER.info(
|
||||
"%s - Cannot find the underlying climate entity: %s. Thermostat will not be operational. Will try later.",
|
||||
self,
|
||||
self.entity_id,
|
||||
)
|
||||
@@ -477,6 +529,14 @@ class UnderlyingClimate(UnderlyingEntity):
|
||||
if not self.is_initialized:
|
||||
return False
|
||||
|
||||
if self._underlying_climate.hvac_mode == hvac_mode:
|
||||
_LOGGER.debug(
|
||||
"%s - hvac_mode is already is requested state %s. Do not send any command",
|
||||
self,
|
||||
self._underlying_climate.hvac_mode,
|
||||
)
|
||||
return False
|
||||
|
||||
data = {ATTR_ENTITY_ID: self._entity_id, "hvac_mode": hvac_mode}
|
||||
await self._hass.services.async_call(
|
||||
CLIMATE_DOMAIN,
|
||||
@@ -490,9 +550,10 @@ class UnderlyingClimate(UnderlyingEntity):
|
||||
def is_device_active(self):
|
||||
"""If the toggleable device is currently active."""
|
||||
if self.is_initialized:
|
||||
return self._underlying_climate.hvac_mode != HVACMode.OFF and self._underlying_climate.hvac_action not in [
|
||||
return self.hvac_mode != HVACMode.OFF and self.hvac_action not in [
|
||||
HVACAction.IDLE,
|
||||
HVACAction.OFF,
|
||||
None,
|
||||
]
|
||||
else:
|
||||
return None
|
||||
@@ -548,12 +609,25 @@ class UnderlyingClimate(UnderlyingEntity):
|
||||
"""Set the target temperature"""
|
||||
if not self.is_initialized:
|
||||
return
|
||||
data = {
|
||||
ATTR_ENTITY_ID: self._entity_id,
|
||||
"temperature": temperature,
|
||||
"target_temp_high": max_temp,
|
||||
"target_temp_low": min_temp,
|
||||
}
|
||||
|
||||
# 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,
|
||||
}
|
||||
|
||||
await self._hass.services.async_call(
|
||||
CLIMATE_DOMAIN,
|
||||
@@ -561,12 +635,48 @@ class UnderlyingClimate(UnderlyingEntity):
|
||||
data,
|
||||
)
|
||||
|
||||
self._last_sent_temperature = target_temp
|
||||
|
||||
@property
|
||||
def last_sent_temperature(self) -> float | None:
|
||||
"""Get the last send temperature. None if no temperature have been sent yet"""
|
||||
return self._last_sent_temperature
|
||||
|
||||
@property
|
||||
def hvac_action(self) -> HVACAction | None:
|
||||
"""Get the hvac action of the underlying"""
|
||||
if not self.is_initialized:
|
||||
return None
|
||||
return self._underlying_climate.hvac_action
|
||||
|
||||
hvac_action = self._underlying_climate.hvac_action
|
||||
if hvac_action is None:
|
||||
target = (
|
||||
self.underlying_target_temperature
|
||||
or self._thermostat.target_temperature
|
||||
)
|
||||
current = (
|
||||
self.underlying_current_temperature
|
||||
or self._thermostat.current_temperature
|
||||
)
|
||||
hvac_mode = self.hvac_mode
|
||||
|
||||
_LOGGER.debug(
|
||||
"%s - hvac_action simulation target=%s, current=%s, hvac_mode=%s",
|
||||
self,
|
||||
target,
|
||||
current,
|
||||
hvac_mode,
|
||||
)
|
||||
hvac_action = HVACAction.IDLE
|
||||
if target is not None and current is not None:
|
||||
dtemp = target - current
|
||||
|
||||
if hvac_mode == HVACMode.COOL and dtemp < 0:
|
||||
hvac_action = HVACAction.COOLING
|
||||
elif hvac_mode in [HVACMode.HEAT, HVACMode.HEAT_COOL] and dtemp > 0:
|
||||
hvac_action = HVACAction.HEATING
|
||||
|
||||
return hvac_action
|
||||
|
||||
@property
|
||||
def hvac_mode(self) -> HVACMode | None:
|
||||
@@ -621,7 +731,7 @@ class UnderlyingClimate(UnderlyingEntity):
|
||||
def temperature_unit(self) -> str:
|
||||
"""Get the temperature_unit"""
|
||||
if not self.is_initialized:
|
||||
return UnitOfTemperature.CELSIUS
|
||||
return self._hass.config.units.temperature_unit
|
||||
return self._underlying_climate.temperature_unit
|
||||
|
||||
@property
|
||||
@@ -645,6 +755,35 @@ class UnderlyingClimate(UnderlyingEntity):
|
||||
return 15
|
||||
return self._underlying_climate.target_temperature_low
|
||||
|
||||
@property
|
||||
def underlying_target_temperature(self) -> float:
|
||||
"""Get the target_temperature"""
|
||||
if not self.is_initialized:
|
||||
return None
|
||||
|
||||
if not hasattr(self._underlying_climate, "target_temperature"):
|
||||
return None
|
||||
else:
|
||||
return self._underlying_climate.target_temperature
|
||||
|
||||
# return self._hass.states.get(self._entity_id).attributes.get(
|
||||
# "target_temperature"
|
||||
# )
|
||||
|
||||
@property
|
||||
def underlying_current_temperature(self) -> float | None:
|
||||
"""Get the underlying current_temperature if it exists
|
||||
and if initialized"""
|
||||
if not self.is_initialized:
|
||||
return None
|
||||
|
||||
if not hasattr(self._underlying_climate, "current_temperature"):
|
||||
return None
|
||||
else:
|
||||
return self._underlying_climate.current_temperature
|
||||
|
||||
# return self._hass.states.get(self._entity_id).attributes.get("current_temperature")
|
||||
|
||||
@property
|
||||
def is_aux_heat(self) -> bool:
|
||||
"""Get the is_aux_heat"""
|
||||
@@ -664,6 +803,44 @@ class UnderlyingClimate(UnderlyingEntity):
|
||||
return None
|
||||
return self._underlying_climate.turn_aux_heat_off()
|
||||
|
||||
@overrides
|
||||
def cap_sent_value(self, value) -> float:
|
||||
"""Try to adapt the target temp value to the min_temp / max_temp found
|
||||
in the underlying entity (if any)"""
|
||||
|
||||
if not self.is_initialized:
|
||||
return value
|
||||
|
||||
# Gets the min_temp and max_temp
|
||||
if (
|
||||
self._underlying_climate.min_temp is not None
|
||||
and self._underlying_climate is not None
|
||||
):
|
||||
min_val = TemperatureConverter.convert(
|
||||
self._underlying_climate.min_temp, self._underlying_climate.temperature_unit, self._hass.config.units.temperature_unit
|
||||
)
|
||||
max_val = TemperatureConverter.convert(
|
||||
self._underlying_climate.max_temp, self._underlying_climate.temperature_unit, self._hass.config.units.temperature_unit
|
||||
)
|
||||
|
||||
new_value = max(min_val, min(value, max_val))
|
||||
else:
|
||||
_LOGGER.debug("%s - no min and max attributes on underlying", self)
|
||||
new_value = value
|
||||
|
||||
if new_value != value:
|
||||
_LOGGER.info(
|
||||
"%s - Target temp have been updated due min, max of the underlying entity. new_value=%.0f value=%.0f min=%.0f max=%.0f",
|
||||
self,
|
||||
new_value,
|
||||
value,
|
||||
min_val,
|
||||
max_val,
|
||||
)
|
||||
|
||||
return new_value
|
||||
|
||||
|
||||
class UnderlyingValve(UnderlyingEntity):
|
||||
"""Represent a underlying switch"""
|
||||
|
||||
@@ -672,10 +849,7 @@ class UnderlyingValve(UnderlyingEntity):
|
||||
_percent_open: int
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
thermostat: Any,
|
||||
valve_entity_id: str
|
||||
self, hass: HomeAssistant, thermostat: Any, valve_entity_id: str
|
||||
) -> None:
|
||||
"""Initialize the underlying switch"""
|
||||
|
||||
@@ -689,37 +863,48 @@ class UnderlyingValve(UnderlyingEntity):
|
||||
self._should_relaunch_control_heating = False
|
||||
self._hvac_mode = None
|
||||
self._percent_open = self._thermostat.valve_open_percent
|
||||
self._valve_entity_id = valve_entity_id
|
||||
|
||||
async def send_percent_open(self):
|
||||
""" Send the percent open to the underlying valve """
|
||||
"""Send the percent open to the underlying valve"""
|
||||
# This may fails if called after shutdown
|
||||
try:
|
||||
data = { ATTR_ENTITY_ID: self._entity_id, "value": self._percent_open }
|
||||
domain = self._entity_id.split('.')[0]
|
||||
data = {"value": self._percent_open}
|
||||
target = {ATTR_ENTITY_ID: self._entity_id}
|
||||
domain = self._entity_id.split(".")[0]
|
||||
await self._hass.services.async_call(
|
||||
domain,
|
||||
SERVICE_SET_VALUE,
|
||||
data,
|
||||
domain=domain,
|
||||
service=SERVICE_SET_VALUE,
|
||||
service_data=data,
|
||||
target=target,
|
||||
)
|
||||
except ServiceNotFound as err:
|
||||
_LOGGER.error(err)
|
||||
# This could happens in unit test if input_number domain is not yet loaded
|
||||
# raise err
|
||||
|
||||
async def turn_off(self):
|
||||
"""Turn heater toggleable device off."""
|
||||
_LOGGER.debug("%s - Stopping underlying valve entity %s", self, self._entity_id)
|
||||
self._percent_open = 0
|
||||
if self.is_device_active:
|
||||
# Issue 341
|
||||
is_active = self.is_device_active
|
||||
self._percent_open = self.cap_sent_value(0)
|
||||
if is_active:
|
||||
await self.send_percent_open()
|
||||
|
||||
async def turn_on(self):
|
||||
"""Nothing to do for Valve because it cannot be turned off"""
|
||||
"""Nothing to do for Valve because it cannot be turned on"""
|
||||
self.set_valve_open_percent()
|
||||
|
||||
async def set_hvac_mode(self, hvac_mode: HVACMode) -> bool:
|
||||
"""Set the HVACmode. Returns true if something have change"""
|
||||
|
||||
if hvac_mode == HVACMode.OFF:
|
||||
if hvac_mode == HVACMode.OFF and self.is_device_active:
|
||||
await self.turn_off()
|
||||
|
||||
if hvac_mode != HVACMode.OFF and not self.is_device_active:
|
||||
await self.turn_on()
|
||||
|
||||
if self._hvac_mode != hvac_mode:
|
||||
self._hvac_mode = hvac_mode
|
||||
return True
|
||||
@@ -734,7 +919,7 @@ class UnderlyingValve(UnderlyingEntity):
|
||||
# To test if real device is open but this is causing some side effect
|
||||
# because the activation can be deferred -
|
||||
# or float(self._hass.states.get(self._entity_id).state) > 0
|
||||
except Exception: # pylint: disable=broad-exception-caught
|
||||
except Exception: # pylint: disable=broad-exception-caught
|
||||
return False
|
||||
|
||||
@overrides
|
||||
@@ -748,11 +933,45 @@ class UnderlyingValve(UnderlyingEntity):
|
||||
):
|
||||
"""We use this function to change the on_percent"""
|
||||
if force:
|
||||
await self.send_percent_open()
|
||||
# self._percent_open = self.cap_sent_value(self._percent_open)
|
||||
# await self.send_percent_open()
|
||||
# avoid to send 2 times the same value at startup
|
||||
self.set_valve_open_percent()
|
||||
|
||||
def set_valve_open_percent(self, percent):
|
||||
""" Update the valve open percent """
|
||||
caped_val = self._thermostat.valve_open_percent
|
||||
@overrides
|
||||
def cap_sent_value(self, value) -> float:
|
||||
"""Try to adapt the open_percent value to the min / max found
|
||||
in the underlying entity (if any)"""
|
||||
|
||||
# Gets the last number state
|
||||
valve_state: State = self._hass.states.get(self._valve_entity_id)
|
||||
if valve_state is None:
|
||||
return value
|
||||
|
||||
if "min" in valve_state.attributes and "max" in valve_state.attributes:
|
||||
min_val = valve_state.attributes["min"]
|
||||
max_val = valve_state.attributes["max"]
|
||||
|
||||
new_value = round(max(min_val, min(value / 100 * max_val, max_val)))
|
||||
else:
|
||||
_LOGGER.debug("%s - no min and max attributes on underlying", self)
|
||||
new_value = value
|
||||
|
||||
if new_value != value:
|
||||
_LOGGER.info(
|
||||
"%s - Valve open percent have been updated due min, max of the underlying entity. new_value=%.0f value=%.0f min=%.0f max=%.0f",
|
||||
self,
|
||||
new_value,
|
||||
value,
|
||||
min_val,
|
||||
max_val,
|
||||
)
|
||||
|
||||
return new_value
|
||||
|
||||
def set_valve_open_percent(self):
|
||||
"""Update the valve open percent"""
|
||||
caped_val = self.cap_sent_value(self._thermostat.valve_open_percent)
|
||||
if self._percent_open == caped_val:
|
||||
# No changes
|
||||
return
|
||||
@@ -760,7 +979,9 @@ class UnderlyingValve(UnderlyingEntity):
|
||||
self._percent_open = caped_val
|
||||
# Send the new command to valve via a service call
|
||||
|
||||
_LOGGER.info("%s - Setting valve ouverture percent to %s", self, self._percent_open)
|
||||
_LOGGER.info(
|
||||
"%s - Setting valve ouverture percent to %s", self, self._percent_open
|
||||
)
|
||||
# Send the change to the valve, in background
|
||||
self._hass.create_task(self.send_percent_open())
|
||||
|
||||
|
||||
289
custom_components/versatile_thermostat/vtherm_api.py
Normal file
@@ -0,0 +1,289 @@
|
||||
""" The API of Versatile Thermostat"""
|
||||
|
||||
import logging
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
from homeassistant.components.climate import ClimateEntity, DOMAIN as CLIMATE_DOMAIN
|
||||
from homeassistant.components.number import NumberEntity
|
||||
|
||||
from .const import (
|
||||
DOMAIN,
|
||||
CONF_AUTO_REGULATION_EXPERT,
|
||||
CONF_SHORT_EMA_PARAMS,
|
||||
CONF_SAFETY_MODE,
|
||||
CONF_THERMOSTAT_TYPE,
|
||||
CONF_THERMOSTAT_CENTRAL_CONFIG,
|
||||
)
|
||||
|
||||
VTHERM_API_NAME = "vtherm_api"
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class VersatileThermostatAPI(dict):
|
||||
"""The VersatileThermostatAPI"""
|
||||
|
||||
_hass: HomeAssistant = None
|
||||
|
||||
@classmethod
|
||||
def get_vtherm_api(cls, hass=None):
|
||||
"""Get the eventual VTherm API class instance or
|
||||
instantiate it if it doesn't exists"""
|
||||
if hass is not None:
|
||||
VersatileThermostatAPI._hass = hass
|
||||
|
||||
if VersatileThermostatAPI._hass is None:
|
||||
return None
|
||||
|
||||
domain = VersatileThermostatAPI._hass.data.get(DOMAIN)
|
||||
if not domain:
|
||||
VersatileThermostatAPI._hass.data.setdefault(DOMAIN, {})
|
||||
|
||||
ret = VersatileThermostatAPI._hass.data.get(DOMAIN).get(VTHERM_API_NAME)
|
||||
if ret is None:
|
||||
ret = VersatileThermostatAPI()
|
||||
VersatileThermostatAPI._hass.data[DOMAIN][VTHERM_API_NAME] = ret
|
||||
return ret
|
||||
|
||||
def __init__(self) -> None:
|
||||
_LOGGER.debug("building a VersatileThermostatAPI")
|
||||
super().__init__()
|
||||
self._expert_params = None
|
||||
self._short_ema_params = None
|
||||
self._safety_mode = None
|
||||
self._central_boiler_entity = None
|
||||
self._threshold_number_entity = None
|
||||
self._nb_active_number_entity = None
|
||||
self._central_configuration = None
|
||||
self._central_mode_select = None
|
||||
# A dict that will store all Number entities which holds the temperature
|
||||
self._number_temperatures = dict()
|
||||
|
||||
def find_central_configuration(self):
|
||||
"""Search for a central configuration"""
|
||||
if not self._central_configuration:
|
||||
for (
|
||||
config_entry
|
||||
) in VersatileThermostatAPI._hass.config_entries.async_entries(DOMAIN):
|
||||
if (
|
||||
config_entry.data.get(CONF_THERMOSTAT_TYPE)
|
||||
== CONF_THERMOSTAT_CENTRAL_CONFIG
|
||||
):
|
||||
self._central_configuration = config_entry
|
||||
break
|
||||
# return self._central_configuration
|
||||
return self._central_configuration
|
||||
|
||||
def add_entry(self, entry: ConfigEntry):
|
||||
"""Add a new entry"""
|
||||
_LOGGER.debug("Add the entry %s", entry.entry_id)
|
||||
# Add the entry in hass.data
|
||||
VersatileThermostatAPI._hass.data[DOMAIN][entry.entry_id] = entry
|
||||
|
||||
def remove_entry(self, entry: ConfigEntry):
|
||||
"""Remove an entry"""
|
||||
_LOGGER.debug("Remove the entry %s", entry.entry_id)
|
||||
VersatileThermostatAPI._hass.data[DOMAIN].pop(entry.entry_id)
|
||||
# If not more entries are preset, remove the API
|
||||
if len(self) == 0:
|
||||
_LOGGER.debug("No more entries-> Remove the API from DOMAIN")
|
||||
VersatileThermostatAPI._hass.data.pop(DOMAIN)
|
||||
|
||||
def set_global_config(self, config):
|
||||
"""Read the global configuration from configuration.yaml file"""
|
||||
_LOGGER.info("Read global config from configuration.yaml")
|
||||
|
||||
self._expert_params = config.get(CONF_AUTO_REGULATION_EXPERT)
|
||||
if self._expert_params:
|
||||
_LOGGER.debug("We have found expert params %s", self._expert_params)
|
||||
|
||||
self._short_ema_params = config.get(CONF_SHORT_EMA_PARAMS)
|
||||
if self._short_ema_params:
|
||||
_LOGGER.debug("We have found short ema params %s", self._short_ema_params)
|
||||
|
||||
self._safety_mode = config.get(CONF_SAFETY_MODE)
|
||||
if self._safety_mode:
|
||||
_LOGGER.debug("We have found safet_mode params %s", self._safety_mode)
|
||||
|
||||
def register_central_boiler(self, central_boiler_entity):
|
||||
"""Register the central boiler entity. This is used by the CentralBoilerBinarySensor
|
||||
class to register itself at creation"""
|
||||
self._central_boiler_entity = central_boiler_entity
|
||||
|
||||
def register_central_boiler_activation_number_threshold(
|
||||
self, threshold_number_entity
|
||||
):
|
||||
"""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,
|
||||
config_id: str,
|
||||
preset_name: str,
|
||||
number_entity: NumberEntity,
|
||||
):
|
||||
"""Register the NumberEntity for a particular device / preset."""
|
||||
# Search for device_name into the _number_temperatures dict
|
||||
if not self._number_temperatures.get(config_id):
|
||||
self._number_temperatures[config_id] = dict()
|
||||
|
||||
self._number_temperatures.get(config_id)[preset_name] = number_entity
|
||||
|
||||
def get_temperature_number_value(self, config_id, preset_name) -> float | None:
|
||||
"""Returns the value of a previously registred NumberEntity which represent
|
||||
a temperature. If no NumberEntity was previously registred, then returns None"""
|
||||
entities = self._number_temperatures.get(config_id, None)
|
||||
if entities:
|
||||
entity = entities.get(preset_name, None)
|
||||
if entity:
|
||||
return entity.state
|
||||
return None
|
||||
|
||||
async def init_vtherm_links(self, entry_id=None):
|
||||
"""Initialize all VTherms entities links
|
||||
This method is called when HA is fully started (and all entities should be initialized)
|
||||
Or when we need to reload all VTherm links (with Number temp entities, central boiler, ...)
|
||||
If entry_id is set, only the VTherm of this entry will be reloaded
|
||||
"""
|
||||
await self.reload_central_boiler_binary_listener()
|
||||
await self.reload_central_boiler_entities_list()
|
||||
# Initialization of all preset for all VTherm
|
||||
component: EntityComponent[ClimateEntity] = self._hass.data.get(
|
||||
CLIMATE_DOMAIN, None
|
||||
)
|
||||
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 (
|
||||
entity.device_info
|
||||
and entity.device_info.get("model", None) == DOMAIN
|
||||
):
|
||||
if entry_id is None or entry_id == entity.unique_id:
|
||||
await entity.async_startup(self.find_central_configuration())
|
||||
|
||||
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
|
||||
component: EntityComponent[ClimateEntity] = self._hass.data.get(
|
||||
CLIMATE_DOMAIN, None
|
||||
)
|
||||
if component:
|
||||
for entity in component.entities:
|
||||
if (
|
||||
entity.device_info
|
||||
and entity.device_info.get("model", None) == DOMAIN
|
||||
and entity.use_central_config_temperature
|
||||
):
|
||||
await entity.init_presets(self.find_central_configuration())
|
||||
|
||||
async def reload_central_boiler_binary_listener(self):
|
||||
"""Reloads the BinarySensor entity which listen to the number of
|
||||
active devices and the thresholds entities"""
|
||||
if self._central_boiler_entity:
|
||||
await self._central_boiler_entity.listen_nb_active_vtherm_entity()
|
||||
|
||||
async def reload_central_boiler_entities_list(self):
|
||||
"""Reload the central boiler list of entities if a central boiler is used"""
|
||||
if self._nb_active_number_entity is not None:
|
||||
await self._nb_active_number_entity.listen_vtherms_entities()
|
||||
|
||||
def register_central_mode_select(self, central_mode_select):
|
||||
"""Register the select entity which holds the central_mode"""
|
||||
self._central_mode_select = central_mode_select
|
||||
|
||||
async def notify_central_mode_change(self, old_central_mode: str | None = None):
|
||||
"""Notify all VTherm that the central_mode have change"""
|
||||
if self._central_mode_select is None:
|
||||
return
|
||||
|
||||
# Update all VTherm states
|
||||
component: EntityComponent[ClimateEntity] = self.hass.data[CLIMATE_DOMAIN]
|
||||
for entity in component.entities:
|
||||
if entity.device_info and entity.device_info.get("model", None) == DOMAIN:
|
||||
_LOGGER.debug(
|
||||
"Changing the central_mode. We have find %s to update",
|
||||
entity.name,
|
||||
)
|
||||
await entity.check_central_mode(
|
||||
self._central_mode_select.state, old_central_mode
|
||||
)
|
||||
|
||||
@property
|
||||
def self_regulation_expert(self):
|
||||
"""Get the self regulation params"""
|
||||
return self._expert_params
|
||||
|
||||
@property
|
||||
def short_ema_params(self):
|
||||
"""Get the short EMA params in expert mode"""
|
||||
return self._short_ema_params
|
||||
|
||||
@property
|
||||
def safety_mode(self):
|
||||
"""Get the safety_mode params"""
|
||||
return self._safety_mode
|
||||
|
||||
@property
|
||||
def central_boiler_entity(self):
|
||||
"""Get the central boiler binary_sensor entity"""
|
||||
return self._central_boiler_entity
|
||||
|
||||
@property
|
||||
def nb_active_device_for_boiler(self):
|
||||
"""Returns the number of active VTherm which have an
|
||||
influence on boiler"""
|
||||
if self._nb_active_number_entity is None:
|
||||
return None
|
||||
else:
|
||||
return self._nb_active_number_entity.native_value
|
||||
|
||||
@property
|
||||
def nb_active_device_for_boiler_entity(self):
|
||||
"""Returns the number of active VTherm entity which have an
|
||||
influence on boiler"""
|
||||
return self._nb_active_number_entity
|
||||
|
||||
@property
|
||||
def nb_active_device_for_boiler_threshold_entity(self):
|
||||
"""Returns the number of active VTherm entity which have an
|
||||
influence on boiler"""
|
||||
return self._threshold_number_entity
|
||||
|
||||
@property
|
||||
def nb_active_device_for_boiler_threshold(self):
|
||||
"""Returns the number of active VTherm entity which have an
|
||||
influence on boiler"""
|
||||
if self._threshold_number_entity is None:
|
||||
return None
|
||||
return int(self._threshold_number_entity.native_value)
|
||||
|
||||
@property
|
||||
def central_mode(self) -> str | None:
|
||||
"""Get the current central mode or None"""
|
||||
if self._central_mode_select:
|
||||
return self._central_mode_select.state
|
||||
else:
|
||||
return None
|
||||
|
||||
@property
|
||||
def hass(self):
|
||||
"""Get the HomeAssistant object"""
|
||||
return VersatileThermostatAPI._hass
|
||||
@@ -3,5 +3,5 @@
|
||||
"content_in_root": false,
|
||||
"render_readme": true,
|
||||
"hide_default_branch": false,
|
||||
"homeassistant": "2023.11.0"
|
||||
"homeassistant": "2024.10.4"
|
||||
}
|
||||
BIN
images/central_mode.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
images/config-central-boiler-1.png
Normal file
|
After Width: | Height: | Size: 26 KiB |
BIN
images/config-central-boiler-2.png
Normal file
|
After Width: | Height: | Size: 85 KiB |
BIN
images/config-complete.png
Normal file
|
After Width: | Height: | Size: 3.1 KiB |
BIN
images/config-features.png
Normal file
|
After Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 50 KiB |
|
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 45 KiB |
BIN
images/config-main0.png
Normal file
|
After Width: | Height: | Size: 28 KiB |
BIN
images/config-menu-all-options.png
Normal file
|
After Width: | Height: | Size: 40 KiB |
BIN
images/config-menu.png
Normal file
|
After Width: | Height: | Size: 28 KiB |
BIN
images/config-not-complete.png
Normal file
|
After Width: | Height: | Size: 3.6 KiB |
BIN
images/config-terminate.png
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
images/config-use-internal-temp.png
Normal file
|
After Width: | Height: | Size: 4.8 KiB |
BIN
images/dev-tools-turnon-boiler-1.png
Normal file
|
After Width: | Height: | Size: 47 KiB |
BIN
images/dev-tools-turnon-boiler-2.png
Normal file
|
After Width: | Height: | Size: 31 KiB |
BIN
images/en/config-linked-entity.png
Normal file
|
After Width: | Height: | Size: 50 KiB |
BIN
images/entitites-central-boiler.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
images/plotly-curves.png
Normal file
|
After Width: | Height: | Size: 50 KiB |
BIN
images/security-mode-symptome1.png
Normal file
|
After Width: | Height: | Size: 58 KiB |
BIN
images/security-mode-symptome2.png
Normal file
|
After Width: | Height: | Size: 85 KiB |
BIN
images/temp-entities-1.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
images/temp-entities-2.png
Normal file
|
After Width: | Height: | Size: 46 KiB |
0
pyproject.toml
Normal file
7
pyrightconfig.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"include": [
|
||||
"custom_components/versatile_thermostat/**",
|
||||
"homeassistant/**"
|
||||
],
|
||||
"reportShadowedImports": false
|
||||
}
|
||||
@@ -1,2 +1 @@
|
||||
homeassistant==2023.10.3
|
||||
ffmpeg
|
||||
homeassistant==2024.10.4
|
||||
|
||||
@@ -2,5 +2,7 @@
|
||||
-r requirements_dev.txt
|
||||
aiodiscover
|
||||
ulid_transform
|
||||
pytest
|
||||
coverage
|
||||
pytest-asyncio
|
||||
pytest-homeassistant-custom-component
|
||||
@@ -25,5 +25,9 @@ fi
|
||||
## without resulting to symlinks.
|
||||
export PYTHONPATH="${PYTHONPATH}:${PWD}/custom_components"
|
||||
|
||||
## Link custom_components into config
|
||||
# rm -f ${PWD}/config/custom_components
|
||||
# ln -s ${PWD}/custom_components ${PWD}/config/
|
||||
|
||||
# Start Home Assistant
|
||||
hass --config "${PWD}/config" --debug
|
||||
531
tests/commons.py
@@ -1,9 +1,9 @@
|
||||
# pylint: disable=wildcard-import, unused-wildcard-import, protected-access, unused-argument, line-too-long
|
||||
# pylint: disable=wildcard-import, unused-wildcard-import, protected-access, unused-argument, line-too-long, abstract-method, too-many-lines, redefined-builtin
|
||||
|
||||
""" Some common resources """
|
||||
import asyncio
|
||||
import logging
|
||||
from unittest.mock import patch, MagicMock
|
||||
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
|
||||
@@ -19,21 +19,37 @@ from homeassistant.components.climate import (
|
||||
ClimateEntityFeature,
|
||||
)
|
||||
|
||||
from homeassistant.components.switch import (
|
||||
SwitchEntity,
|
||||
)
|
||||
|
||||
from homeassistant.components.number import NumberEntity, DOMAIN as NUMBER_DOMAIN
|
||||
|
||||
from pytest_homeassistant_custom_component.common import MockConfigEntry
|
||||
|
||||
from custom_components.versatile_thermostat.base_thermostat import BaseThermostat
|
||||
from custom_components.versatile_thermostat.const import * # pylint: disable=wildcard-import, unused-wildcard-import
|
||||
from custom_components.versatile_thermostat.underlyings import * # pylint: disable=wildcard-import, unused-wildcard-import
|
||||
from custom_components.versatile_thermostat.commons import get_tz, NowClass # pylint: disable=unused-import
|
||||
from custom_components.versatile_thermostat.commons import ( # pylint: disable=unused-import
|
||||
get_tz,
|
||||
NowClass,
|
||||
)
|
||||
|
||||
from custom_components.versatile_thermostat.vtherm_api import VersatileThermostatAPI
|
||||
|
||||
from .const import ( # pylint: disable=unused-import
|
||||
MOCK_TH_OVER_SWITCH_USER_CONFIG,
|
||||
MOCK_TH_OVER_4SWITCH_USER_CONFIG,
|
||||
MOCK_TH_OVER_CLIMATE_USER_CONFIG,
|
||||
MOCK_TH_OVER_SWITCH_MAIN_CONFIG,
|
||||
MOCK_TH_OVER_SWITCH_CENTRAL_MAIN_CONFIG,
|
||||
MOCK_TH_OVER_SWITCH_TYPE_CONFIG,
|
||||
MOCK_TH_OVER_SWITCH_AC_TYPE_CONFIG,
|
||||
MOCK_TH_OVER_4SWITCH_TYPE_CONFIG,
|
||||
MOCK_TH_OVER_CLIMATE_MAIN_CONFIG,
|
||||
MOCK_TH_OVER_CLIMATE_CENTRAL_MAIN_CONFIG,
|
||||
MOCK_TH_OVER_CLIMATE_TYPE_CONFIG,
|
||||
MOCK_TH_OVER_CLIMATE_TYPE_USE_DEVICE_TEMP_CONFIG,
|
||||
MOCK_TH_OVER_CLIMATE_TYPE_AC_CONFIG,
|
||||
MOCK_TH_OVER_CLIMATE_TYPE_NOT_REGULATED_CONFIG,
|
||||
MOCK_TH_OVER_SWITCH_TPI_CONFIG,
|
||||
@@ -51,13 +67,24 @@ from .const import ( # pylint: disable=unused-import
|
||||
PRESET_NONE,
|
||||
PRESET_ECO,
|
||||
PRESET_ACTIVITY,
|
||||
overrides,
|
||||
)
|
||||
|
||||
MOCK_FULL_FEATURES = {
|
||||
CONF_USE_WINDOW_FEATURE: True,
|
||||
CONF_USE_MOTION_FEATURE: True,
|
||||
CONF_USE_POWER_FEATURE: True,
|
||||
CONF_USE_PRESENCE_FEATURE: True,
|
||||
}
|
||||
|
||||
FULL_SWITCH_CONFIG = (
|
||||
MOCK_TH_OVER_SWITCH_USER_CONFIG
|
||||
| MOCK_TH_OVER_SWITCH_MAIN_CONFIG
|
||||
| MOCK_TH_OVER_SWITCH_CENTRAL_MAIN_CONFIG
|
||||
| MOCK_TH_OVER_SWITCH_TYPE_CONFIG
|
||||
| MOCK_TH_OVER_SWITCH_TPI_CONFIG
|
||||
| MOCK_PRESETS_CONFIG
|
||||
| MOCK_FULL_FEATURES
|
||||
| MOCK_WINDOW_CONFIG
|
||||
| MOCK_MOTION_CONFIG
|
||||
| MOCK_POWER_CONFIG
|
||||
@@ -67,9 +94,12 @@ FULL_SWITCH_CONFIG = (
|
||||
|
||||
FULL_SWITCH_AC_CONFIG = (
|
||||
MOCK_TH_OVER_SWITCH_USER_CONFIG
|
||||
| MOCK_TH_OVER_SWITCH_MAIN_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
|
||||
| MOCK_POWER_CONFIG
|
||||
@@ -77,16 +107,28 @@ FULL_SWITCH_AC_CONFIG = (
|
||||
| MOCK_ADVANCED_CONFIG
|
||||
)
|
||||
|
||||
|
||||
PARTIAL_CLIMATE_CONFIG = (
|
||||
MOCK_TH_OVER_CLIMATE_USER_CONFIG
|
||||
| MOCK_TH_OVER_CLIMATE_MAIN_CONFIG
|
||||
| MOCK_TH_OVER_CLIMATE_CENTRAL_MAIN_CONFIG
|
||||
| MOCK_TH_OVER_CLIMATE_TYPE_CONFIG
|
||||
| MOCK_PRESETS_CONFIG
|
||||
| MOCK_ADVANCED_CONFIG
|
||||
)
|
||||
|
||||
PARTIAL_CLIMATE_CONFIG_USE_DEVICE_TEMP = (
|
||||
MOCK_TH_OVER_CLIMATE_USER_CONFIG
|
||||
| MOCK_TH_OVER_CLIMATE_MAIN_CONFIG
|
||||
| MOCK_TH_OVER_CLIMATE_CENTRAL_MAIN_CONFIG
|
||||
| MOCK_TH_OVER_CLIMATE_TYPE_USE_DEVICE_TEMP_CONFIG
|
||||
| MOCK_PRESETS_CONFIG
|
||||
| MOCK_ADVANCED_CONFIG
|
||||
)
|
||||
|
||||
PARTIAL_CLIMATE_NOT_REGULATED_CONFIG = (
|
||||
MOCK_TH_OVER_CLIMATE_USER_CONFIG
|
||||
| MOCK_TH_OVER_CLIMATE_MAIN_CONFIG
|
||||
| MOCK_TH_OVER_CLIMATE_CENTRAL_MAIN_CONFIG
|
||||
| MOCK_TH_OVER_CLIMATE_TYPE_NOT_REGULATED_CONFIG
|
||||
| MOCK_PRESETS_CONFIG
|
||||
| MOCK_ADVANCED_CONFIG
|
||||
@@ -95,6 +137,8 @@ PARTIAL_CLIMATE_NOT_REGULATED_CONFIG = (
|
||||
PARTIAL_CLIMATE_AC_CONFIG = (
|
||||
MOCK_TH_OVER_CLIMATE_USER_CONFIG
|
||||
| MOCK_TH_OVER_CLIMATE_TYPE_AC_CONFIG
|
||||
| MOCK_TH_OVER_CLIMATE_MAIN_CONFIG
|
||||
| MOCK_TH_OVER_CLIMATE_CENTRAL_MAIN_CONFIG
|
||||
| MOCK_PRESETS_CONFIG
|
||||
| MOCK_ADVANCED_CONFIG
|
||||
)
|
||||
@@ -111,53 +155,191 @@ FULL_4SWITCH_CONFIG = (
|
||||
| MOCK_ADVANCED_CONFIG
|
||||
)
|
||||
|
||||
FULL_CENTRAL_CONFIG = {
|
||||
CONF_NAME: CENTRAL_CONFIG_NAME,
|
||||
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_CENTRAL_CONFIG,
|
||||
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
|
||||
CONF_TEMP_MIN: 15,
|
||||
CONF_TEMP_MAX: 30,
|
||||
CONF_STEP_TEMPERATURE: 0.1,
|
||||
CONF_TPI_COEF_INT: 0.5,
|
||||
CONF_TPI_COEF_EXT: 0.02,
|
||||
"frost_temp": 10,
|
||||
"eco_temp": 17.1,
|
||||
"comfort_temp": 0,
|
||||
"boost_temp": 19.1,
|
||||
"eco_ac_temp": 25.1,
|
||||
"comfort_ac_temp": 23.1,
|
||||
"boost_ac_temp": 21.1,
|
||||
"frost_away_temp": 15.1,
|
||||
"eco_away_temp": 15.2,
|
||||
"comfort_away_temp": 0,
|
||||
"boost_away_temp": 15.4,
|
||||
"eco_ac_away_temp": 30.5,
|
||||
"comfort_ac_away_temp": 0,
|
||||
"boost_ac_away_temp": 30.7,
|
||||
CONF_WINDOW_DELAY: 15,
|
||||
CONF_WINDOW_AUTO_OPEN_THRESHOLD: 4,
|
||||
CONF_WINDOW_AUTO_CLOSE_THRESHOLD: 1,
|
||||
CONF_WINDOW_AUTO_MAX_DURATION: 31,
|
||||
CONF_MOTION_DELAY: 31,
|
||||
CONF_MOTION_OFF_DELAY: 301,
|
||||
CONF_MOTION_PRESET: "boost",
|
||||
CONF_NO_MOTION_PRESET: "frost",
|
||||
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_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_USE_CENTRAL_BOILER_FEATURE: False,
|
||||
}
|
||||
|
||||
FULL_CENTRAL_CONFIG_WITH_BOILER = {
|
||||
CONF_NAME: CENTRAL_CONFIG_NAME,
|
||||
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_CENTRAL_CONFIG,
|
||||
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
|
||||
CONF_TEMP_MIN: 15,
|
||||
CONF_TEMP_MAX: 30,
|
||||
CONF_STEP_TEMPERATURE: 0.1,
|
||||
CONF_TPI_COEF_INT: 0.5,
|
||||
CONF_TPI_COEF_EXT: 0.02,
|
||||
"frost_temp": 10,
|
||||
"eco_temp": 17.1,
|
||||
"comfort_temp": 0,
|
||||
"boost_temp": 19.1,
|
||||
"eco_ac_temp": 25.1,
|
||||
"comfort_ac_temp": 23.1,
|
||||
"boost_ac_temp": 21.1,
|
||||
"frost_away_temp": 15.1,
|
||||
"eco_away_temp": 15.2,
|
||||
"comfort_away_temp": 0,
|
||||
"boost_away_temp": 15.4,
|
||||
"eco_ac_away_temp": 30.5,
|
||||
"comfort_ac_away_temp": 0,
|
||||
"boost_ac_away_temp": 30.7,
|
||||
CONF_WINDOW_DELAY: 15,
|
||||
CONF_WINDOW_AUTO_OPEN_THRESHOLD: 4,
|
||||
CONF_WINDOW_AUTO_CLOSE_THRESHOLD: 1,
|
||||
CONF_WINDOW_AUTO_MAX_DURATION: 31,
|
||||
CONF_MOTION_DELAY: 31,
|
||||
CONF_MOTION_OFF_DELAY: 301,
|
||||
CONF_MOTION_PRESET: "boost",
|
||||
CONF_NO_MOTION_PRESET: "frost",
|
||||
CONF_POWER_SENSOR: "sensor.mock_power_sensor",
|
||||
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_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",
|
||||
}
|
||||
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class MockClimate(ClimateEntity):
|
||||
"""A Mock Climate class used for Underlying climate mode"""
|
||||
|
||||
def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos, hvac_mode:HVACMode = HVACMode.OFF, hvac_action:HVACAction = HVACAction.OFF) -> None: # pylint: disable=unused-argument
|
||||
def __init__( # pylint: disable=unused-argument, dangerous-default-value
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
unique_id,
|
||||
name,
|
||||
entry_infos={},
|
||||
hvac_mode: HVACMode = HVACMode.OFF,
|
||||
hvac_action: HVACAction = HVACAction.OFF,
|
||||
fan_modes: list[str] = None,
|
||||
hvac_modes: list[str] = None,
|
||||
) -> None:
|
||||
"""Initialize the thermostat."""
|
||||
|
||||
super().__init__()
|
||||
|
||||
self.hass = hass
|
||||
self.platform = 'climate'
|
||||
self.entity_id= self.platform+'.'+unique_id
|
||||
self.platform = "climate"
|
||||
self.entity_id = self.platform + "." + unique_id
|
||||
self._attr_extra_state_attributes = {}
|
||||
self._unique_id = unique_id
|
||||
self._name = name
|
||||
self._attr_hvac_action = HVACAction.OFF if hvac_mode == HVACMode.OFF else HVACAction.HEATING
|
||||
self._attr_hvac_action = (
|
||||
HVACAction.OFF if hvac_mode == HVACMode.OFF else HVACAction.HEATING
|
||||
)
|
||||
self._attr_hvac_mode = hvac_mode
|
||||
self._attr_hvac_modes = [HVACMode.OFF, HVACMode.COOL, HVACMode.HEAT]
|
||||
self._attr_hvac_modes = (
|
||||
hvac_modes
|
||||
if hvac_modes is not None
|
||||
else [HVACMode.OFF, HVACMode.COOL, HVACMode.HEAT]
|
||||
)
|
||||
self._attr_temperature_unit = UnitOfTemperature.CELSIUS
|
||||
self._attr_target_temperature = 20
|
||||
self._attr_current_temperature = 15
|
||||
self._attr_hvac_action = hvac_action
|
||||
self._attr_target_temperature_step = 0.2
|
||||
self._fan_modes = fan_modes if fan_modes else None
|
||||
self._attr_fan_mode = None
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
"""The name"""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def hvac_action(self):
|
||||
"""The hvac action of the mock climate"""
|
||||
return self._attr_hvac_action
|
||||
|
||||
@property
|
||||
def fan_modes(self) -> list[str] | None:
|
||||
"""The list of fan_modes"""
|
||||
return self._fan_modes
|
||||
|
||||
def set_fan_mode(self, fan_mode):
|
||||
"""Set the fan mode"""
|
||||
self._attr_fan_mode = fan_mode
|
||||
|
||||
@property
|
||||
def supported_features(self) -> int:
|
||||
"""The supported feature of this climate entity"""
|
||||
ret = ClimateEntityFeature.TARGET_TEMPERATURE
|
||||
if self._fan_modes:
|
||||
ret = ret | ClimateEntityFeature.FAN_MODE
|
||||
return ret
|
||||
|
||||
def set_temperature(self, **kwargs):
|
||||
""" Set the target temperature"""
|
||||
"""Set the target temperature"""
|
||||
temperature = kwargs.get(ATTR_TEMPERATURE)
|
||||
self._attr_target_temperature = temperature
|
||||
|
||||
async def async_set_hvac_mode(self, hvac_mode):
|
||||
""" The hvac mode"""
|
||||
"""The hvac mode"""
|
||||
self._attr_hvac_mode = hvac_mode
|
||||
|
||||
@property
|
||||
def hvac_action(self):
|
||||
""" The hvac action of the mock climate"""
|
||||
return self._attr_hvac_action
|
||||
def set_hvac_mode(self, hvac_mode):
|
||||
"""The hvac mode"""
|
||||
self._attr_hvac_mode = hvac_mode
|
||||
|
||||
def set_hvac_action(self, hvac_action: HVACAction):
|
||||
""" Set the HVACaction """
|
||||
"""Set the HVACaction"""
|
||||
self._attr_hvac_action = hvac_action
|
||||
|
||||
def set_current_temperature(self, current_temperature):
|
||||
"""Set the current_temperature"""
|
||||
self._attr_current_temperature = current_temperature
|
||||
|
||||
|
||||
class MockUnavailableClimate(ClimateEntity):
|
||||
"""A Mock Climate class used for Underlying climate mode"""
|
||||
|
||||
def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None: # pylint: disable=unused-argument
|
||||
def __init__(
|
||||
self, hass: HomeAssistant, unique_id, name, entry_infos
|
||||
) -> None: # pylint: disable=unused-argument
|
||||
"""Initialize the thermostat."""
|
||||
|
||||
super().__init__()
|
||||
@@ -170,6 +352,8 @@ class MockUnavailableClimate(ClimateEntity):
|
||||
self._attr_hvac_mode = None
|
||||
self._attr_hvac_modes = [HVACMode.OFF, HVACMode.COOL, HVACMode.HEAT]
|
||||
self._attr_temperature_unit = UnitOfTemperature.CELSIUS
|
||||
self._attr_fan_mode = None
|
||||
|
||||
|
||||
class MagicMockClimate(MagicMock):
|
||||
"""A Magic Mock class for a underlying climate entity"""
|
||||
@@ -242,37 +426,221 @@ class MagicMockClimate(MagicMock):
|
||||
def supported_features(self): # pylint: disable=missing-function-docstring
|
||||
return ClimateEntityFeature.TARGET_TEMPERATURE
|
||||
|
||||
@property
|
||||
def min_temp(self): # pylint: disable=missing-function-docstring
|
||||
return 15
|
||||
|
||||
@property
|
||||
def max_temp(self): # pylint: disable=missing-function-docstring
|
||||
return 19
|
||||
|
||||
|
||||
class MagicMockClimateWithTemperatureRange(MagicMock):
|
||||
"""A Magic Mock class for a underlying climate entity"""
|
||||
|
||||
@property
|
||||
def temperature_unit(self): # pylint: disable=missing-function-docstring
|
||||
return UnitOfTemperature.CELSIUS
|
||||
|
||||
@property
|
||||
def hvac_mode(self): # pylint: disable=missing-function-docstring
|
||||
return HVACMode.HEAT
|
||||
|
||||
@property
|
||||
def hvac_action(self): # pylint: disable=missing-function-docstring
|
||||
return HVACAction.IDLE
|
||||
|
||||
@property
|
||||
def target_temperature(self): # pylint: disable=missing-function-docstring
|
||||
return 15
|
||||
|
||||
@property
|
||||
def current_temperature(self): # pylint: disable=missing-function-docstring
|
||||
return 14
|
||||
|
||||
@property
|
||||
def target_temperature_step( # pylint: disable=missing-function-docstring
|
||||
self,
|
||||
) -> float | None:
|
||||
return 0.5
|
||||
|
||||
@property
|
||||
def target_temperature_high( # pylint: disable=missing-function-docstring
|
||||
self,
|
||||
) -> float | None:
|
||||
return 35
|
||||
|
||||
@property
|
||||
def target_temperature_low( # pylint: disable=missing-function-docstring
|
||||
self,
|
||||
) -> float | None:
|
||||
return 7
|
||||
|
||||
@property
|
||||
def hvac_modes( # pylint: disable=missing-function-docstring
|
||||
self,
|
||||
) -> list[str] | None:
|
||||
return [HVACMode.HEAT, HVACMode.OFF, HVACMode.COOL]
|
||||
|
||||
@property
|
||||
def fan_modes( # pylint: disable=missing-function-docstring
|
||||
self,
|
||||
) -> list[str] | None:
|
||||
return None
|
||||
|
||||
@property
|
||||
def swing_modes( # pylint: disable=missing-function-docstring
|
||||
self,
|
||||
) -> list[str] | None:
|
||||
return None
|
||||
|
||||
@property
|
||||
def fan_mode(self) -> str | None: # pylint: disable=missing-function-docstring
|
||||
return None
|
||||
|
||||
@property
|
||||
def swing_mode(self) -> str | None: # pylint: disable=missing-function-docstring
|
||||
return None
|
||||
|
||||
@property
|
||||
def supported_features(self): # pylint: disable=missing-function-docstring
|
||||
return ClimateEntityFeature.TARGET_TEMPERATURE_RANGE
|
||||
|
||||
@property
|
||||
def min_temp(self): # pylint: disable=missing-function-docstring
|
||||
return 10
|
||||
|
||||
@property
|
||||
def max_temp(self): # pylint: disable=missing-function-docstring
|
||||
return 31
|
||||
|
||||
|
||||
class MockSwitch(SwitchEntity):
|
||||
"""A fake switch to be used instead real switch"""
|
||||
|
||||
def __init__( # pylint: disable=unused-argument, dangerous-default-value
|
||||
self, hass: HomeAssistant, unique_id, name, entry_infos={}
|
||||
):
|
||||
"""Init the switch"""
|
||||
super().__init__()
|
||||
|
||||
self.hass = hass
|
||||
self.platform = "switch"
|
||||
self.entity_id = self.platform + "." + unique_id
|
||||
self._name = name
|
||||
self._attr_is_on = False
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
"""The name"""
|
||||
return self._name
|
||||
|
||||
@overrides
|
||||
def turn_on(self, **kwargs: Any):
|
||||
"""Turns the switch on and notify the state change"""
|
||||
self._attr_is_on = True
|
||||
# self.async_write_ha_state()
|
||||
|
||||
@overrides
|
||||
def turn_off(self, **kwargs: Any):
|
||||
"""Turns the switch on and notify the state change"""
|
||||
self._attr_is_on = False
|
||||
# self.async_write_ha_state()
|
||||
|
||||
|
||||
class MockNumber(NumberEntity):
|
||||
"""A fake switch to be used instead real switch"""
|
||||
|
||||
def __init__( # pylint: disable=unused-argument, dangerous-default-value
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
unique_id,
|
||||
name,
|
||||
min=0,
|
||||
max=100,
|
||||
step=1,
|
||||
entry_infos={},
|
||||
):
|
||||
"""Init the switch"""
|
||||
super().__init__()
|
||||
|
||||
self.hass = hass
|
||||
self.platform = "number"
|
||||
self.entity_id = self.platform + "." + unique_id
|
||||
self._name = name
|
||||
self._attr_native_value = 0
|
||||
self._attr_native_min_value = min
|
||||
self._attr_native_max_value = max
|
||||
self._attr_step = step
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
"""The name"""
|
||||
return self._name
|
||||
|
||||
@overrides
|
||||
def set_native_value(self, value: float):
|
||||
"""Change the value"""
|
||||
self._attr_native_value = value
|
||||
|
||||
|
||||
async def create_thermostat(
|
||||
hass: HomeAssistant, entry: MockConfigEntry, entity_id: str
|
||||
) -> BaseThermostat:
|
||||
"""Creates and return a TPI Thermostat"""
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
|
||||
):
|
||||
entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(entry.entry_id)
|
||||
assert entry.state is ConfigEntryState.LOADED
|
||||
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
|
||||
# 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)
|
||||
|
||||
return search_entity(hass, entity_id, CLIMATE_DOMAIN)
|
||||
return entity
|
||||
|
||||
|
||||
async def create_central_config( # pylint: disable=dangerous-default-value
|
||||
hass: HomeAssistant, entry: MockConfigEntry = FULL_CENTRAL_CONFIG
|
||||
):
|
||||
"""Creates a Central Configuration from entry given in argument"""
|
||||
central_config_entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
title="TheCentralConfigMockName",
|
||||
unique_id="centralConfigUniqueId",
|
||||
data=entry,
|
||||
)
|
||||
|
||||
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
|
||||
|
||||
# Test that VTherm API find the CentralConfig
|
||||
api = VersatileThermostatAPI.get_vtherm_api(hass)
|
||||
central_configuration = api.find_central_configuration()
|
||||
assert central_configuration is not None
|
||||
|
||||
return central_configuration
|
||||
|
||||
|
||||
def search_entity(hass: HomeAssistant, entity_id, domain) -> Entity:
|
||||
"""Search and return the entity in the domain"""
|
||||
component = hass.data[domain]
|
||||
for entity in component.entities:
|
||||
_LOGGER.debug("Found %s entity: %s", domain, entity.entity_id)
|
||||
if entity.entity_id == entity_id:
|
||||
return entity
|
||||
return None
|
||||
|
||||
|
||||
def count_entities(hass: HomeAssistant, entity_id, domain) -> Entity:
|
||||
"""Search and return the entity in the domain"""
|
||||
component = hass.data[domain]
|
||||
return len(list(component.entities)) if component.entities else 0
|
||||
|
||||
|
||||
async def send_temperature_change_event(
|
||||
entity: BaseThermostat, new_temp, date, sleep=True
|
||||
):
|
||||
@@ -294,7 +662,34 @@ async def send_temperature_change_event(
|
||||
)
|
||||
},
|
||||
)
|
||||
await entity._async_temperature_changed(temp_event)
|
||||
dearm_window_auto = await entity._async_temperature_changed(temp_event)
|
||||
if sleep:
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
return dearm_window_auto
|
||||
|
||||
|
||||
async def send_last_seen_temperature_change_event(
|
||||
entity: BaseThermostat, date, sleep=True
|
||||
):
|
||||
"""Sending a new last seen event simulating a change on last seen temperature sensor"""
|
||||
_LOGGER.info(
|
||||
"------- Testu: sending send_last_seen_temperature_change_event, date=%s on %s",
|
||||
date,
|
||||
entity,
|
||||
)
|
||||
last_seen_event = Event(
|
||||
EVENT_STATE_CHANGED,
|
||||
{
|
||||
"new_state": State(
|
||||
entity_id=entity.entity_id,
|
||||
state=date,
|
||||
last_changed=date,
|
||||
last_updated=date,
|
||||
)
|
||||
},
|
||||
)
|
||||
await entity._async_last_seen_temperature_changed(last_seen_event)
|
||||
if sleep:
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
@@ -325,9 +720,7 @@ async def send_ext_temperature_change_event(
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
|
||||
async def send_power_change_event(
|
||||
entity: BaseThermostat, new_power, date, sleep=True
|
||||
):
|
||||
async def send_power_change_event(entity: BaseThermostat, new_power, date, sleep=True):
|
||||
"""Sending a new power event simulating a change on power sensor"""
|
||||
_LOGGER.info(
|
||||
"------- Testu: sending send_temperature_change_event, new_power=%.2f date=%s on %s",
|
||||
@@ -478,6 +871,7 @@ async def send_presence_change_event(
|
||||
await asyncio.sleep(0.1)
|
||||
return ret
|
||||
|
||||
|
||||
async def send_climate_change_event(
|
||||
entity: BaseThermostat,
|
||||
new_hvac_mode: HVACMode,
|
||||
@@ -486,6 +880,7 @@ async def send_climate_change_event(
|
||||
old_hvac_action: HVACAction,
|
||||
date,
|
||||
sleep=True,
|
||||
underlying_entity_id: str = None,
|
||||
):
|
||||
"""Sending a new climate event simulating a change on the underlying climate state"""
|
||||
_LOGGER.info(
|
||||
@@ -497,18 +892,23 @@ async def send_climate_change_event(
|
||||
date,
|
||||
entity,
|
||||
)
|
||||
|
||||
send_from_entity_id = (
|
||||
underlying_entity_id if underlying_entity_id is not None else entity.entity_id
|
||||
)
|
||||
|
||||
climate_event = Event(
|
||||
EVENT_STATE_CHANGED,
|
||||
{
|
||||
"new_state": State(
|
||||
entity_id=entity.entity_id,
|
||||
entity_id=send_from_entity_id,
|
||||
state=new_hvac_mode,
|
||||
attributes={"hvac_action": new_hvac_action},
|
||||
last_changed=date,
|
||||
last_updated=date,
|
||||
),
|
||||
"old_state": State(
|
||||
entity_id=entity.entity_id,
|
||||
entity_id=send_from_entity_id,
|
||||
state=old_hvac_mode,
|
||||
attributes={"hvac_action": old_hvac_action},
|
||||
last_changed=date,
|
||||
@@ -521,6 +921,7 @@ async def send_climate_change_event(
|
||||
await asyncio.sleep(0.1)
|
||||
return ret
|
||||
|
||||
|
||||
async def send_climate_change_event_with_temperature(
|
||||
entity: BaseThermostat,
|
||||
new_hvac_mode: HVACMode,
|
||||
@@ -530,6 +931,7 @@ async def send_climate_change_event_with_temperature(
|
||||
date,
|
||||
temperature,
|
||||
sleep=True,
|
||||
underlying_entity_id=None,
|
||||
):
|
||||
"""Sending a new climate event simulating a change on the underlying climate state"""
|
||||
_LOGGER.info(
|
||||
@@ -542,18 +944,21 @@ async def send_climate_change_event_with_temperature(
|
||||
temperature,
|
||||
entity,
|
||||
)
|
||||
if not underlying_entity_id:
|
||||
underlying_entity_id = entity.entity_id
|
||||
|
||||
climate_event = Event(
|
||||
EVENT_STATE_CHANGED,
|
||||
{
|
||||
"new_state": State(
|
||||
entity_id=entity.entity_id,
|
||||
entity_id=underlying_entity_id,
|
||||
state=new_hvac_mode,
|
||||
attributes={"hvac_action": new_hvac_action, "temperature": temperature},
|
||||
last_changed=date,
|
||||
last_updated=date,
|
||||
),
|
||||
"old_state": State(
|
||||
entity_id=entity.entity_id,
|
||||
entity_id=underlying_entity_id,
|
||||
state=old_hvac_mode,
|
||||
attributes={"hvac_action": old_hvac_action},
|
||||
last_changed=date,
|
||||
@@ -573,3 +978,53 @@ def cancel_switchs_cycles(entity: BaseThermostat):
|
||||
return
|
||||
for under in entity._underlyings:
|
||||
under._cancel_cycle()
|
||||
|
||||
|
||||
async def set_climate_preset_temp(
|
||||
entity: BaseThermostat, temp_number_name: str, temp: float
|
||||
):
|
||||
"""Set a preset value in the temp Number entity"""
|
||||
number_entity_id = (
|
||||
NUMBER_DOMAIN
|
||||
+ "."
|
||||
+ entity.entity_id.split(".")[1]
|
||||
+ "_preset_"
|
||||
+ temp_number_name
|
||||
+ PRESET_TEMP_SUFFIX
|
||||
)
|
||||
|
||||
temp_entity = search_entity(
|
||||
entity.hass,
|
||||
number_entity_id,
|
||||
NUMBER_DOMAIN,
|
||||
)
|
||||
if temp_entity:
|
||||
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'",
|
||||
number_entity_id,
|
||||
)
|
||||
|
||||
|
||||
async def set_all_climate_preset_temp(
|
||||
hass, vtherm: BaseThermostat, temps: dict, number_entity_base_name: str
|
||||
):
|
||||
"""Initialize all temp of preset for a VTherm entity"""
|
||||
# We initialize
|
||||
for preset_name, value in temps.items():
|
||||
|
||||
await set_climate_preset_temp(vtherm, preset_name, value)
|
||||
|
||||
# Search the number entity to control it is correctly set
|
||||
number_entity_name = (
|
||||
f"number.{number_entity_base_name}_preset_{preset_name}{PRESET_TEMP_SUFFIX}"
|
||||
)
|
||||
temp_entity: NumberEntity = search_entity(
|
||||
hass,
|
||||
number_entity_name,
|
||||
NUMBER_DOMAIN,
|
||||
)
|
||||
assert temp_entity
|
||||
# Because set_value is not implemented in Number class (really don't understand why...)
|
||||
assert temp_entity.state == value
|
||||
|
||||
@@ -26,15 +26,47 @@ from custom_components.versatile_thermostat.config_flow import (
|
||||
VersatileThermostatBaseConfigFlow,
|
||||
)
|
||||
|
||||
from custom_components.versatile_thermostat.vtherm_api import VersatileThermostatAPI
|
||||
from custom_components.versatile_thermostat.base_thermostat import BaseThermostat
|
||||
|
||||
from .commons import (
|
||||
create_central_config,
|
||||
FULL_CENTRAL_CONFIG,
|
||||
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"""
|
||||
socket_allow_hosts(
|
||||
allowed=["localhost", "127.0.0.1", "::1"], allow_unix_socket=True
|
||||
)
|
||||
|
||||
|
||||
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)
|
||||
def auto_enable_custom_integrations(enable_custom_integrations): # pylint: disable=unused-argument
|
||||
def auto_enable_custom_integrations(
|
||||
enable_custom_integrations,
|
||||
): # pylint: disable=unused-argument
|
||||
"""Enable all integration in tests"""
|
||||
yield
|
||||
|
||||
@@ -108,3 +140,30 @@ def skip_send_event_fixture():
|
||||
"""Skip the send_event in BaseThermostat"""
|
||||
with patch.object(BaseThermostat, "send_event"):
|
||||
yield
|
||||
|
||||
|
||||
@pytest.fixture(name="init_vtherm_api")
|
||||
def init_vtherm_api_fixture(hass):
|
||||
"""Initialize the VTherm API"""
|
||||
VersatileThermostatAPI.get_vtherm_api(hass)
|
||||
yield
|
||||
|
||||
|
||||
@pytest.fixture(name="init_central_config")
|
||||
async def init_central_config_fixture(
|
||||
hass, init_vtherm_api
|
||||
): # pylint: disable=unused-argument
|
||||
"""Initialize the VTherm API"""
|
||||
await create_central_config(hass, FULL_CENTRAL_CONFIG)
|
||||
|
||||
yield
|
||||
|
||||
|
||||
@pytest.fixture(name="init_central_config_with_boiler_fixture")
|
||||
async def init_central_config_with_boiler_fixture(
|
||||
hass, init_vtherm_api
|
||||
): # pylint: disable=unused-argument
|
||||
"""Initialize the VTherm API"""
|
||||
await create_central_config(hass, FULL_CENTRAL_CONFIG_WITH_BOILER)
|
||||
|
||||
yield
|
||||
|
||||
187
tests/const.py
@@ -1,4 +1,5 @@
|
||||
""" The commons const for all tests """
|
||||
|
||||
from homeassistant.components.climate.const import ( # pylint: disable=unused-import
|
||||
PRESET_BOOST,
|
||||
PRESET_COMFORT,
|
||||
@@ -6,70 +7,23 @@ from homeassistant.components.climate.const import ( # pylint: disable=unused-i
|
||||
PRESET_NONE,
|
||||
PRESET_ACTIVITY,
|
||||
)
|
||||
from custom_components.versatile_thermostat.const import (
|
||||
CONF_NAME,
|
||||
CONF_HEATER,
|
||||
CONF_HEATER_2,
|
||||
CONF_HEATER_3,
|
||||
CONF_HEATER_4,
|
||||
CONF_THERMOSTAT_CLIMATE,
|
||||
CONF_THERMOSTAT_SWITCH,
|
||||
CONF_THERMOSTAT_TYPE,
|
||||
CONF_AC_MODE,
|
||||
CONF_TEMP_SENSOR,
|
||||
CONF_EXTERNAL_TEMP_SENSOR,
|
||||
CONF_CYCLE_MIN,
|
||||
CONF_TEMP_MAX,
|
||||
CONF_TEMP_MIN,
|
||||
CONF_PROP_FUNCTION,
|
||||
PROPORTIONAL_FUNCTION_TPI,
|
||||
CONF_TPI_COEF_INT,
|
||||
CONF_TPI_COEF_EXT,
|
||||
CONF_MINIMAL_ACTIVATION_DELAY,
|
||||
CONF_SECURITY_DELAY_MIN,
|
||||
CONF_SECURITY_MIN_ON_PERCENT,
|
||||
CONF_SECURITY_DEFAULT_ON_PERCENT,
|
||||
CONF_USE_WINDOW_FEATURE,
|
||||
CONF_USE_MOTION_FEATURE,
|
||||
CONF_USE_POWER_FEATURE,
|
||||
CONF_USE_PRESENCE_FEATURE,
|
||||
CONF_WINDOW_SENSOR,
|
||||
CONF_WINDOW_DELAY,
|
||||
CONF_WINDOW_AUTO_OPEN_THRESHOLD,
|
||||
CONF_WINDOW_AUTO_CLOSE_THRESHOLD,
|
||||
CONF_WINDOW_AUTO_MAX_DURATION,
|
||||
CONF_MOTION_SENSOR,
|
||||
CONF_MOTION_DELAY,
|
||||
CONF_MOTION_OFF_DELAY,
|
||||
CONF_MOTION_PRESET,
|
||||
CONF_NO_MOTION_PRESET,
|
||||
CONF_POWER_SENSOR,
|
||||
CONF_MAX_POWER_SENSOR,
|
||||
CONF_DEVICE_POWER,
|
||||
CONF_PRESET_POWER,
|
||||
CONF_PRESENCE_SENSOR,
|
||||
PRESET_AWAY_SUFFIX,
|
||||
CONF_CLIMATE,
|
||||
CONF_AUTO_REGULATION_MODE,
|
||||
CONF_AUTO_REGULATION_STRONG,
|
||||
CONF_AUTO_REGULATION_NONE,
|
||||
CONF_AUTO_REGULATION_DTEMP,
|
||||
CONF_AUTO_REGULATION_PERIOD_MIN,
|
||||
CONF_INVERSE_SWITCH
|
||||
)
|
||||
|
||||
from custom_components.versatile_thermostat.const import * # pylint: disable=wildcard-import, unused-wildcard-import
|
||||
|
||||
MOCK_TH_OVER_SWITCH_USER_CONFIG = {
|
||||
CONF_NAME: "TheOverSwitchMockName",
|
||||
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_SWITCH,
|
||||
}
|
||||
|
||||
MOCK_TH_OVER_SWITCH_MAIN_CONFIG = {
|
||||
CONF_NAME: "TheOverSwitchMockName",
|
||||
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
|
||||
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
|
||||
CONF_CYCLE_MIN: 5,
|
||||
CONF_TEMP_MIN: 15,
|
||||
CONF_TEMP_MAX: 30,
|
||||
CONF_DEVICE_POWER: 1,
|
||||
CONF_USE_WINDOW_FEATURE: True,
|
||||
CONF_USE_MOTION_FEATURE: True,
|
||||
CONF_USE_POWER_FEATURE: True,
|
||||
CONF_USE_PRESENCE_FEATURE: True,
|
||||
# CONF_USE_WINDOW_FEATURE: True,
|
||||
# CONF_USE_MOTION_FEATURE: True,
|
||||
# CONF_USE_POWER_FEATURE: True,
|
||||
# CONF_USE_PRESENCE_FEATURE: True,
|
||||
CONF_USE_MAIN_CENTRAL_CONFIG: True,
|
||||
}
|
||||
|
||||
MOCK_TH_OVER_4SWITCH_USER_CONFIG = {
|
||||
@@ -80,6 +34,7 @@ MOCK_TH_OVER_4SWITCH_USER_CONFIG = {
|
||||
CONF_CYCLE_MIN: 8,
|
||||
CONF_TEMP_MIN: 15,
|
||||
CONF_TEMP_MAX: 30,
|
||||
CONF_STEP_TEMPERATURE: 0.1,
|
||||
CONF_DEVICE_POWER: 1,
|
||||
CONF_USE_WINDOW_FEATURE: True,
|
||||
CONF_USE_MOTION_FEATURE: True,
|
||||
@@ -88,39 +43,57 @@ MOCK_TH_OVER_4SWITCH_USER_CONFIG = {
|
||||
}
|
||||
|
||||
MOCK_TH_OVER_CLIMATE_USER_CONFIG = {
|
||||
CONF_NAME: "TheOverClimateMockName",
|
||||
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_CLIMATE,
|
||||
}
|
||||
|
||||
|
||||
MOCK_TH_OVER_CLIMATE_MAIN_CONFIG = {
|
||||
CONF_NAME: "TheOverClimateMockName",
|
||||
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
|
||||
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
|
||||
CONF_CYCLE_MIN: 5,
|
||||
CONF_DEVICE_POWER: 1,
|
||||
CONF_USE_MAIN_CENTRAL_CONFIG: False,
|
||||
CONF_USE_CENTRAL_MODE: True,
|
||||
# Keep default values which are False
|
||||
}
|
||||
|
||||
MOCK_TH_OVER_CLIMATE_CENTRAL_MAIN_CONFIG = {
|
||||
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
|
||||
CONF_TEMP_MIN: 15,
|
||||
CONF_TEMP_MAX: 30,
|
||||
CONF_DEVICE_POWER: 1
|
||||
CONF_STEP_TEMPERATURE: 0.1,
|
||||
# Keep default values which are False
|
||||
}
|
||||
|
||||
MOCK_TH_OVER_SWITCH_CENTRAL_MAIN_CONFIG = {
|
||||
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
|
||||
CONF_TEMP_MIN: 15,
|
||||
CONF_TEMP_MAX: 30,
|
||||
CONF_STEP_TEMPERATURE: 0.1,
|
||||
# Keep default values which are False
|
||||
}
|
||||
|
||||
MOCK_TH_OVER_SWITCH_TYPE_CONFIG = {
|
||||
CONF_HEATER: "switch.mock_switch",
|
||||
CONF_UNDERLYING_LIST: ["switch.mock_switch"],
|
||||
CONF_HEATER_KEEP_ALIVE: 0,
|
||||
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
|
||||
CONF_AC_MODE: False,
|
||||
CONF_INVERSE_SWITCH: False
|
||||
CONF_INVERSE_SWITCH: False,
|
||||
}
|
||||
|
||||
MOCK_TH_OVER_SWITCH_AC_TYPE_CONFIG = {
|
||||
CONF_HEATER: "switch.mock_air_conditioner",
|
||||
CONF_UNDERLYING_LIST: ["switch.mock_air_conditioner"],
|
||||
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
|
||||
CONF_AC_MODE: True,
|
||||
CONF_INVERSE_SWITCH: False
|
||||
CONF_INVERSE_SWITCH: False,
|
||||
}
|
||||
|
||||
MOCK_TH_OVER_4SWITCH_TYPE_CONFIG = {
|
||||
CONF_HEATER: "switch.mock_4switch0",
|
||||
CONF_HEATER_2: "switch.mock_4switch1",
|
||||
CONF_HEATER_3: "switch.mock_4switch2",
|
||||
CONF_HEATER_4: "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,
|
||||
CONF_INVERSE_SWITCH: False
|
||||
CONF_INVERSE_SWITCH: False,
|
||||
}
|
||||
|
||||
MOCK_TH_OVER_SWITCH_TPI_CONFIG = {
|
||||
@@ -129,44 +102,65 @@ MOCK_TH_OVER_SWITCH_TPI_CONFIG = {
|
||||
}
|
||||
|
||||
MOCK_TH_OVER_CLIMATE_TYPE_CONFIG = {
|
||||
CONF_CLIMATE: "climate.mock_climate",
|
||||
CONF_UNDERLYING_LIST: ["climate.mock_climate"],
|
||||
CONF_AC_MODE: False,
|
||||
CONF_AUTO_REGULATION_MODE: CONF_AUTO_REGULATION_STRONG,
|
||||
CONF_AUTO_REGULATION_DTEMP: 0.5,
|
||||
CONF_AUTO_REGULATION_PERIOD_MIN: 2
|
||||
CONF_AUTO_REGULATION_PERIOD_MIN: 2,
|
||||
CONF_AUTO_FAN_MODE: CONF_AUTO_FAN_HIGH,
|
||||
CONF_AUTO_REGULATION_USE_DEVICE_TEMP: False,
|
||||
}
|
||||
|
||||
MOCK_TH_OVER_CLIMATE_TYPE_USE_DEVICE_TEMP_CONFIG = {
|
||||
CONF_UNDERLYING_LIST: ["climate.mock_climate"],
|
||||
CONF_AC_MODE: False,
|
||||
CONF_AUTO_REGULATION_MODE: CONF_AUTO_REGULATION_STRONG,
|
||||
CONF_AUTO_REGULATION_DTEMP: 0.1,
|
||||
CONF_AUTO_REGULATION_PERIOD_MIN: 2,
|
||||
CONF_AUTO_FAN_MODE: CONF_AUTO_FAN_HIGH,
|
||||
CONF_AUTO_REGULATION_USE_DEVICE_TEMP: True,
|
||||
}
|
||||
|
||||
MOCK_TH_OVER_CLIMATE_TYPE_NOT_REGULATED_CONFIG = {
|
||||
CONF_CLIMATE: "climate.mock_climate",
|
||||
CONF_UNDERLYING_LIST: ["climate.mock_climate"],
|
||||
CONF_AC_MODE: False,
|
||||
CONF_AUTO_REGULATION_MODE: CONF_AUTO_REGULATION_NONE
|
||||
CONF_AUTO_REGULATION_MODE: CONF_AUTO_REGULATION_NONE,
|
||||
}
|
||||
|
||||
MOCK_TH_OVER_CLIMATE_TYPE_AC_CONFIG = {
|
||||
CONF_CLIMATE: "climate.mock_climate",
|
||||
CONF_UNDERLYING_LIST: ["climate.mock_climate"],
|
||||
CONF_AC_MODE: True,
|
||||
CONF_AUTO_REGULATION_MODE: CONF_AUTO_REGULATION_STRONG,
|
||||
CONF_AUTO_REGULATION_DTEMP: 0.5,
|
||||
CONF_AUTO_REGULATION_PERIOD_MIN: 1
|
||||
CONF_AUTO_REGULATION_PERIOD_MIN: 1,
|
||||
}
|
||||
|
||||
# TODO remove this later
|
||||
MOCK_PRESETS_CONFIG = {
|
||||
PRESET_ECO + "_temp": 16,
|
||||
PRESET_COMFORT + "_temp": 17,
|
||||
PRESET_BOOST + "_temp": 18,
|
||||
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_ECO + "_temp": 17,
|
||||
PRESET_COMFORT + "_temp": 19,
|
||||
PRESET_BOOST + "_temp": 20,
|
||||
PRESET_ECO + "_ac_temp": 25,
|
||||
PRESET_COMFORT + "_ac_temp": 23,
|
||||
PRESET_BOOST + "_ac_temp": 21,
|
||||
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
|
||||
CONF_WINDOW_DELAY: 10,
|
||||
}
|
||||
|
||||
MOCK_WINDOW_DELAY_CONFIG = {
|
||||
CONF_WINDOW_DELAY: 10,
|
||||
}
|
||||
|
||||
@@ -174,6 +168,7 @@ MOCK_WINDOW_AUTO_CONFIG = {
|
||||
CONF_WINDOW_AUTO_OPEN_THRESHOLD: 1.0,
|
||||
CONF_WINDOW_AUTO_CLOSE_THRESHOLD: 0.0,
|
||||
CONF_WINDOW_AUTO_MAX_DURATION: 5.0,
|
||||
CONF_WINDOW_ACTION: CONF_WINDOW_FAN_ONLY,
|
||||
}
|
||||
|
||||
MOCK_MOTION_CONFIG = {
|
||||
@@ -192,19 +187,10 @@ MOCK_POWER_CONFIG = {
|
||||
|
||||
MOCK_PRESENCE_CONFIG = {
|
||||
CONF_PRESENCE_SENSOR: "person.presence_sensor",
|
||||
PRESET_ECO + PRESET_AWAY_SUFFIX + "_temp": 16,
|
||||
PRESET_COMFORT + PRESET_AWAY_SUFFIX + "_temp": 17,
|
||||
PRESET_BOOST + PRESET_AWAY_SUFFIX + "_temp": 18,
|
||||
}
|
||||
|
||||
MOCK_PRESENCE_AC_CONFIG = {
|
||||
CONF_PRESENCE_SENSOR: "person.presence_sensor",
|
||||
PRESET_ECO + PRESET_AWAY_SUFFIX + "_temp": 16,
|
||||
PRESET_COMFORT + PRESET_AWAY_SUFFIX + "_temp": 17,
|
||||
PRESET_BOOST + PRESET_AWAY_SUFFIX + "_temp": 18,
|
||||
PRESET_ECO + "_ac" + PRESET_AWAY_SUFFIX + "_temp": 27,
|
||||
PRESET_COMFORT + "_ac" + PRESET_AWAY_SUFFIX + "_temp": 26,
|
||||
PRESET_BOOST + "_ac" + PRESET_AWAY_SUFFIX + "_temp": 25,
|
||||
}
|
||||
|
||||
MOCK_ADVANCED_CONFIG = {
|
||||
@@ -220,3 +206,14 @@ MOCK_DEFAULT_FEATURE_CONFIG = {
|
||||
CONF_USE_POWER_FEATURE: False,
|
||||
CONF_USE_PRESENCE_FEATURE: False,
|
||||
}
|
||||
|
||||
MOCK_DEFAULT_CENTRAL_CONFIG = {
|
||||
CONF_USE_MAIN_CENTRAL_CONFIG: False,
|
||||
CONF_USE_TPI_CENTRAL_CONFIG: False,
|
||||
CONF_USE_PRESETS_CENTRAL_CONFIG: False,
|
||||
CONF_USE_WINDOW_CENTRAL_CONFIG: False,
|
||||
CONF_USE_MOTION_CENTRAL_CONFIG: False,
|
||||
CONF_USE_POWER_CENTRAL_CONFIG: False,
|
||||
CONF_USE_PRESENCE_CENTRAL_CONFIG: False,
|
||||
CONF_USE_ADVANCED_CENTRAL_CONFIG: False,
|
||||
}
|
||||
|
||||
347
tests/test_auto_fan_mode.py
Normal file
@@ -0,0 +1,347 @@
|
||||
# pylint: disable=wildcard-import, unused-wildcard-import, protected-access, unused-argument, line-too-long
|
||||
|
||||
""" Test the auto fan mode of a over_climate thermostat """
|
||||
from unittest.mock import patch, call
|
||||
|
||||
from datetime import datetime # , timedelta
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
# from homeassistant.components.climate import HVACAction, HVACMode
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
|
||||
# from homeassistant.helpers.entity_component import EntityComponent
|
||||
# from homeassistant.components.climate import ClimateEntity, DOMAIN as CLIMATE_DOMAIN
|
||||
|
||||
from pytest_homeassistant_custom_component.common import MockConfigEntry
|
||||
|
||||
# from custom_components.versatile_thermostat.base_thermostat import BaseThermostat
|
||||
from custom_components.versatile_thermostat.thermostat_climate import (
|
||||
ThermostatOverClimate,
|
||||
)
|
||||
|
||||
from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import
|
||||
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_over_climate_auto_fan_mode_turbo(
|
||||
hass: HomeAssistant, skip_hass_states_is_state, skip_send_event
|
||||
):
|
||||
"""Test the init of an over climate thermostat with auto_fan_mode = Turbo which exists"""
|
||||
|
||||
fan_modes = ["low", "medium", "high", "boost", "mute", "auto", "turbo"]
|
||||
|
||||
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: 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: 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_AUTO_FAN_MODE: CONF_AUTO_FAN_TURBO,
|
||||
},
|
||||
)
|
||||
|
||||
fake_underlying_climate = MockClimate(
|
||||
hass=hass,
|
||||
unique_id="mockUniqueId",
|
||||
name="MockClimateName",
|
||||
fan_modes=fan_modes,
|
||||
)
|
||||
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
|
||||
return_value=fake_underlying_climate,
|
||||
):
|
||||
entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(entry.entry_id)
|
||||
assert entry.state is ConfigEntryState.LOADED
|
||||
|
||||
entity: ThermostatOverClimate = search_entity(
|
||||
hass, "climate.theoverclimatemockname", "climate"
|
||||
)
|
||||
|
||||
assert entity
|
||||
assert isinstance(entity, ThermostatOverClimate)
|
||||
|
||||
assert entity.name == "TheOverClimateMockName"
|
||||
assert entity.is_over_climate is True
|
||||
assert entity.fan_modes == fan_modes
|
||||
assert entity._auto_fan_mode == "auto_fan_turbo"
|
||||
assert entity._auto_activated_fan_mode == "turbo"
|
||||
assert entity._auto_deactivated_fan_mode == "mute"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_over_climate_auto_fan_mode_not_turbo(
|
||||
hass: HomeAssistant, skip_hass_states_is_state, skip_send_event
|
||||
):
|
||||
"""Test the init of an over climate thermostat with auto_fan_mode = Turbo which doesn't exists"""
|
||||
|
||||
fan_modes = ["low", "medium", "high", "boost", "auto"]
|
||||
|
||||
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: 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: 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_AUTO_FAN_MODE: CONF_AUTO_FAN_TURBO,
|
||||
},
|
||||
)
|
||||
|
||||
fake_underlying_climate = MockClimate(
|
||||
hass=hass,
|
||||
unique_id="mockUniqueId",
|
||||
name="MockClimateName",
|
||||
fan_modes=fan_modes,
|
||||
)
|
||||
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
|
||||
return_value=fake_underlying_climate,
|
||||
):
|
||||
entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(entry.entry_id)
|
||||
assert entry.state is ConfigEntryState.LOADED
|
||||
|
||||
entity: ThermostatOverClimate = search_entity(
|
||||
hass, "climate.theoverclimatemockname", "climate"
|
||||
)
|
||||
|
||||
assert entity
|
||||
assert isinstance(entity, ThermostatOverClimate)
|
||||
|
||||
assert entity.name == "TheOverClimateMockName"
|
||||
assert entity.is_over_climate is True
|
||||
assert entity.fan_modes == fan_modes
|
||||
assert entity._auto_fan_mode == "auto_fan_turbo"
|
||||
# Turbo doesn't exists -> fallback to high
|
||||
assert entity._auto_activated_fan_mode == "high"
|
||||
# Mute doesn't exists -> fallback to auto
|
||||
assert entity._auto_deactivated_fan_mode == "auto"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_over_climate_auto_fan_mode_turbo_activation(
|
||||
hass: HomeAssistant, skip_hass_states_is_state, skip_send_event
|
||||
):
|
||||
"""Test the init of an over climate thermostat with auto_fan_mode = Turbo which exists"""
|
||||
|
||||
fan_modes = ["low", "medium", "high", "boost", "mute", "auto", "turbo"]
|
||||
|
||||
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: 15,
|
||||
CONF_TEMP_MAX: 30,
|
||||
"eco_temp": 17,
|
||||
"comfort_temp": 18,
|
||||
"boost_temp": 19,
|
||||
"eco_ac_temp": 25,
|
||||
"comfort_ac_temp": 23,
|
||||
"boost_ac_temp": 21,
|
||||
CONF_USE_WINDOW_FEATURE: False,
|
||||
CONF_USE_MOTION_FEATURE: False,
|
||||
CONF_USE_POWER_FEATURE: 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_AUTO_FAN_MODE: CONF_AUTO_FAN_TURBO,
|
||||
CONF_AC_MODE: True,
|
||||
},
|
||||
)
|
||||
|
||||
tz = get_tz(hass) # pylint: disable=invalid-name
|
||||
now: datetime = datetime.now(tz=tz)
|
||||
|
||||
fake_underlying_climate = MockClimate(
|
||||
hass=hass,
|
||||
unique_id="mockUniqueId",
|
||||
name="MockClimateName",
|
||||
fan_modes=fan_modes,
|
||||
)
|
||||
|
||||
# 1. Init fan mode
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
|
||||
return_value=fake_underlying_climate,
|
||||
):
|
||||
entity = await create_thermostat(hass, entry, "climate.theoverclimatemockname")
|
||||
# entry.add_to_hass(hass)
|
||||
# await hass.config_entries.async_setup(entry.entry_id)
|
||||
# assert entry.state is ConfigEntryState.LOADED
|
||||
#
|
||||
# entity: ThermostatOverClimate = search_entity(
|
||||
# hass, "climate.theoverclimatemockname", "climate"
|
||||
# )
|
||||
|
||||
assert entity
|
||||
assert isinstance(entity, ThermostatOverClimate)
|
||||
|
||||
assert entity.name == "TheOverClimateMockName"
|
||||
assert entity.is_over_climate is True
|
||||
assert entity.fan_modes == fan_modes
|
||||
assert entity.fan_mode is None
|
||||
assert entity._auto_fan_mode == "auto_fan_turbo"
|
||||
assert entity._auto_activated_fan_mode == "turbo"
|
||||
assert entity._auto_deactivated_fan_mode == "mute"
|
||||
|
||||
# 2. Turn on and set temperature cold
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.set_fan_mode"
|
||||
) as mock_send_fan_mode:
|
||||
# Force preset mode
|
||||
await entity.async_set_hvac_mode(HVACMode.HEAT)
|
||||
assert entity.hvac_mode == HVACMode.HEAT
|
||||
await entity.async_set_preset_mode(PRESET_COMFORT)
|
||||
assert entity.preset_mode == PRESET_COMFORT
|
||||
assert entity.target_temperature == 18
|
||||
|
||||
# Change the current temperature to 16 which is 2° under
|
||||
await send_temperature_change_event(entity, 16, now, True)
|
||||
fake_underlying_climate.set_fan_mode("turbo")
|
||||
|
||||
assert mock_send_fan_mode.call_count == 1
|
||||
mock_send_fan_mode.assert_has_calls([call.set_fan_mode("turbo")])
|
||||
|
||||
assert entity.fan_mode == "turbo"
|
||||
|
||||
# 3. Set another low temperature
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.set_fan_mode"
|
||||
) as mock_send_fan_mode:
|
||||
fake_underlying_climate.set_fan_mode("turbo")
|
||||
|
||||
# Change the current temperature to 17 which is 1° under
|
||||
await send_temperature_change_event(entity, 15, now, True)
|
||||
|
||||
# Nothing is send cause we are already in turbo fan mode
|
||||
assert mock_send_fan_mode.call_count == 0
|
||||
|
||||
assert entity.fan_mode == "turbo"
|
||||
|
||||
# 4. Set temperature not so cold
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.set_fan_mode"
|
||||
) as mock_send_fan_mode:
|
||||
# Change the current temperature to 17 which is 1° under
|
||||
await send_temperature_change_event(entity, 17, now, True)
|
||||
fake_underlying_climate.set_fan_mode("mute")
|
||||
|
||||
assert mock_send_fan_mode.call_count == 1
|
||||
mock_send_fan_mode.assert_has_calls([call.set_fan_mode("mute")])
|
||||
|
||||
assert entity.fan_mode == "mute"
|
||||
|
||||
# 5. Set temperature not so cold another time
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.set_fan_mode"
|
||||
) as mock_send_fan_mode:
|
||||
fake_underlying_climate.set_fan_mode("mute")
|
||||
|
||||
# Change the current temperature to 17 which is 1° under
|
||||
await send_temperature_change_event(entity, 17.1, now, True)
|
||||
|
||||
assert mock_send_fan_mode.call_count == 0
|
||||
assert entity.fan_mode == "mute"
|
||||
|
||||
# 6. Set temperature very high above the target
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.set_fan_mode"
|
||||
) as mock_send_fan_mode:
|
||||
fake_underlying_climate.set_fan_mode("mute")
|
||||
|
||||
# Change the current temperature to 17 which is 1° under
|
||||
await send_temperature_change_event(entity, 21, now, True)
|
||||
|
||||
assert mock_send_fan_mode.call_count == 0
|
||||
assert entity.fan_mode == "mute"
|
||||
|
||||
# 7. In AC mode, set temperature very high under the target
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.set_fan_mode"
|
||||
) as mock_send_fan_mode:
|
||||
await entity.async_set_hvac_mode(HVACMode.COOL)
|
||||
assert entity.hvac_mode == HVACMode.COOL
|
||||
assert entity.preset_mode == PRESET_COMFORT
|
||||
assert entity.target_temperature == 23
|
||||
|
||||
assert entity.current_temperature == 21
|
||||
|
||||
fake_underlying_climate.set_fan_mode("mute")
|
||||
|
||||
# Change the current temperature to 17 which is 1° under
|
||||
await send_temperature_change_event(entity, 20, now, True)
|
||||
|
||||
assert mock_send_fan_mode.call_count == 0
|
||||
assert entity.fan_mode == "mute"
|
||||
|
||||
# 8. In AC mode, set temperature not so high above the target
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.set_fan_mode"
|
||||
) as mock_send_fan_mode:
|
||||
assert entity.target_temperature == 23
|
||||
await send_temperature_change_event(entity, 24, now, True)
|
||||
assert entity.current_temperature == 24
|
||||
fake_underlying_climate.set_fan_mode("mute")
|
||||
|
||||
assert mock_send_fan_mode.call_count == 0
|
||||
assert entity.fan_mode == "mute"
|
||||
|
||||
# 8. In AC mode, set temperature high above the target
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.set_fan_mode"
|
||||
) as mock_send_fan_mode:
|
||||
assert entity.target_temperature == 23
|
||||
await send_temperature_change_event(entity, 25.1, now, True)
|
||||
assert entity.current_temperature == 25.1
|
||||
fake_underlying_climate.set_fan_mode("turbo")
|
||||
|
||||
assert mock_send_fan_mode.call_count == 1
|
||||
mock_send_fan_mode.assert_has_calls([call.set_fan_mode("turbo")])
|
||||
assert entity.fan_mode == "turbo"
|
||||
@@ -1,7 +1,7 @@
|
||||
# pylint: disable=wildcard-import, unused-wildcard-import, protected-access, unused-argument, line-too-long
|
||||
|
||||
""" Test the normal start of a Thermostat """
|
||||
from unittest.mock import patch #, call
|
||||
from unittest.mock import patch, call
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
@@ -14,13 +14,18 @@ from homeassistant.components.climate import ClimateEntity, DOMAIN as CLIMATE_DO
|
||||
from pytest_homeassistant_custom_component.common import MockConfigEntry
|
||||
|
||||
# from custom_components.versatile_thermostat.base_thermostat import BaseThermostat
|
||||
from custom_components.versatile_thermostat.thermostat_climate import ThermostatOverClimate
|
||||
from custom_components.versatile_thermostat.thermostat_climate import (
|
||||
ThermostatOverClimate,
|
||||
)
|
||||
|
||||
from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import
|
||||
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_over_climate_regulation(hass: HomeAssistant, skip_hass_states_is_state, skip_send_event):
|
||||
async def test_over_climate_regulation(
|
||||
hass: HomeAssistant, skip_hass_states_is_state, skip_send_event
|
||||
):
|
||||
"""Test the regulation of an over climate thermostat"""
|
||||
|
||||
entry = MockConfigEntry(
|
||||
@@ -41,23 +46,13 @@ async def test_over_climate_regulation(hass: HomeAssistant, skip_hass_states_is_
|
||||
event_timestamp = now - timedelta(minutes=10)
|
||||
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.commons.NowClass.get_now", return_value=event_timestamp
|
||||
"custom_components.versatile_thermostat.commons.NowClass.get_now",
|
||||
return_value=event_timestamp,
|
||||
), patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
|
||||
return_value=fake_underlying_climate,
|
||||
):
|
||||
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:ThermostatOverClimate = find_my_entity("climate.theoverclimatemockname")
|
||||
entity = await create_thermostat(hass, entry, "climate.theoverclimatemockname")
|
||||
|
||||
assert entity
|
||||
assert isinstance(entity, ThermostatOverClimate)
|
||||
@@ -65,11 +60,13 @@ async def test_over_climate_regulation(hass: HomeAssistant, skip_hass_states_is_
|
||||
assert entity.name == "TheOverClimateMockName"
|
||||
assert entity.is_over_climate is True
|
||||
assert entity.is_regulated is True
|
||||
assert entity.hvac_action is HVACAction.OFF
|
||||
assert entity.auto_regulation_use_device_temp is False
|
||||
assert entity.hvac_mode is HVACMode.OFF
|
||||
assert entity.hvac_action is HVACAction.OFF
|
||||
assert entity.target_temperature == entity.min_temp
|
||||
assert entity.preset_modes == [
|
||||
PRESET_NONE,
|
||||
PRESET_FROST_PROTECTION,
|
||||
PRESET_ECO,
|
||||
PRESET_COMFORT,
|
||||
PRESET_BOOST,
|
||||
@@ -90,36 +87,43 @@ async def test_over_climate_regulation(hass: HomeAssistant, skip_hass_states_is_
|
||||
# set manual target temp (at now - 7) -> the regulation should occurs
|
||||
event_timestamp = now - timedelta(minutes=7)
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.commons.NowClass.get_now", return_value=event_timestamp
|
||||
"custom_components.versatile_thermostat.commons.NowClass.get_now",
|
||||
return_value=event_timestamp,
|
||||
):
|
||||
await entity.async_set_temperature(temperature=18)
|
||||
|
||||
fake_underlying_climate.set_hvac_action(HVACAction.HEATING) # simulate under heating
|
||||
fake_underlying_climate.set_hvac_action(
|
||||
HVACAction.HEATING
|
||||
) # simulate under heating
|
||||
assert entity.hvac_action == HVACAction.HEATING
|
||||
assert entity.preset_mode == PRESET_NONE # Manual mode
|
||||
assert entity.preset_mode == PRESET_NONE # Manual mode
|
||||
|
||||
# the regulated temperature should be greater
|
||||
assert entity.regulated_target_temp > entity.target_temperature
|
||||
# In medium we could go up to +3 degre
|
||||
# normally the calcul gives 18 + 2.2 but we round the result to the nearest 0.5 which is 2.0
|
||||
assert entity.regulated_target_temp == 18+2.0
|
||||
assert entity.regulated_target_temp == 18 + 1.5
|
||||
assert entity.hvac_action == HVACAction.HEATING
|
||||
|
||||
# change temperature so that the regulated temperature should slow down
|
||||
event_timestamp = now - timedelta(minutes=5)
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.commons.NowClass.get_now", return_value=event_timestamp
|
||||
"custom_components.versatile_thermostat.commons.NowClass.get_now",
|
||||
return_value=event_timestamp,
|
||||
):
|
||||
await send_temperature_change_event(entity, 23, event_timestamp)
|
||||
await send_ext_temperature_change_event(entity, 19, event_timestamp)
|
||||
|
||||
# the regulated temperature should be under
|
||||
assert entity.regulated_target_temp < entity.target_temperature
|
||||
assert entity.regulated_target_temp == 18-0.5 # normally 0.6 but round_to_nearest gives 0.5
|
||||
assert entity.regulated_target_temp == 18 - 2.5
|
||||
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_over_climate_regulation_ac_mode(hass: HomeAssistant, skip_hass_states_is_state, skip_send_event):
|
||||
async def test_over_climate_regulation_ac_mode(
|
||||
hass: HomeAssistant, skip_hass_states_is_state, skip_send_event
|
||||
):
|
||||
"""Test the regulation of an over climate thermostat"""
|
||||
|
||||
entry = MockConfigEntry(
|
||||
@@ -140,23 +144,13 @@ async def test_over_climate_regulation_ac_mode(hass: HomeAssistant, skip_hass_st
|
||||
event_timestamp = now - timedelta(minutes=10)
|
||||
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.commons.NowClass.get_now", return_value=event_timestamp
|
||||
"custom_components.versatile_thermostat.commons.NowClass.get_now",
|
||||
return_value=event_timestamp,
|
||||
), patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
|
||||
return_value=fake_underlying_climate,
|
||||
):
|
||||
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:ThermostatOverClimate = find_my_entity("climate.theoverclimatemockname")
|
||||
entity = await create_thermostat(hass, entry, "climate.theoverclimatemockname")
|
||||
|
||||
assert entity
|
||||
assert isinstance(entity, ThermostatOverClimate)
|
||||
@@ -169,6 +163,7 @@ async def test_over_climate_regulation_ac_mode(hass: HomeAssistant, skip_hass_st
|
||||
assert entity.target_temperature == entity.max_temp
|
||||
assert entity.preset_modes == [
|
||||
PRESET_NONE,
|
||||
PRESET_FROST_PROTECTION,
|
||||
PRESET_ECO,
|
||||
PRESET_COMFORT,
|
||||
PRESET_BOOST,
|
||||
@@ -185,53 +180,66 @@ async def test_over_climate_regulation_ac_mode(hass: HomeAssistant, skip_hass_st
|
||||
await send_temperature_change_event(entity, 30, event_timestamp)
|
||||
await send_ext_temperature_change_event(entity, 35, event_timestamp)
|
||||
|
||||
|
||||
# set manual target temp
|
||||
event_timestamp = now - timedelta(minutes=7)
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.commons.NowClass.get_now", return_value=event_timestamp
|
||||
"custom_components.versatile_thermostat.commons.NowClass.get_now",
|
||||
return_value=event_timestamp,
|
||||
):
|
||||
await entity.async_set_temperature(temperature=25)
|
||||
|
||||
fake_underlying_climate.set_hvac_action(HVACAction.COOLING) # simulate under heating
|
||||
fake_underlying_climate.set_hvac_action(
|
||||
HVACAction.COOLING
|
||||
) # simulate under heating
|
||||
assert entity.hvac_action == HVACAction.COOLING
|
||||
assert entity.preset_mode == PRESET_NONE # Manual mode
|
||||
assert entity.preset_mode == PRESET_NONE # Manual mode
|
||||
|
||||
# the regulated temperature should be lower
|
||||
assert entity.regulated_target_temp < entity.target_temperature
|
||||
assert entity.regulated_target_temp == 25-3 # In medium we could go up to -3 degre
|
||||
assert (
|
||||
entity.regulated_target_temp == 25 - 2.5
|
||||
) # In medium we could go up to -3 degre
|
||||
assert entity.hvac_action == HVACAction.COOLING
|
||||
|
||||
# change temperature so that the regulated temperature should slow down
|
||||
event_timestamp = now - timedelta(minutes=5)
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.commons.NowClass.get_now", return_value=event_timestamp
|
||||
"custom_components.versatile_thermostat.commons.NowClass.get_now",
|
||||
return_value=event_timestamp,
|
||||
):
|
||||
await send_temperature_change_event(entity, 26, event_timestamp)
|
||||
await send_ext_temperature_change_event(entity, 35, event_timestamp)
|
||||
|
||||
# the regulated temperature should be under
|
||||
assert entity.regulated_target_temp < entity.target_temperature
|
||||
assert entity.regulated_target_temp == 25-2 # +2.3 without round_to_nearest
|
||||
assert (
|
||||
entity.regulated_target_temp == 25 - 1
|
||||
) # +2.3 without round_to_nearest
|
||||
|
||||
# change temperature so that the regulated temperature should slow down
|
||||
event_timestamp = now - timedelta(minutes=3)
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.commons.NowClass.get_now", return_value=event_timestamp
|
||||
"custom_components.versatile_thermostat.commons.NowClass.get_now",
|
||||
return_value=event_timestamp,
|
||||
):
|
||||
await send_temperature_change_event(entity, 18, event_timestamp)
|
||||
await send_ext_temperature_change_event(entity, 25, event_timestamp)
|
||||
|
||||
# the regulated temperature should be greater
|
||||
assert entity.regulated_target_temp > entity.target_temperature
|
||||
assert entity.regulated_target_temp == 25+0.5 # +0.4 without round_to_nearest
|
||||
assert (
|
||||
entity.regulated_target_temp == 25 + 3
|
||||
) # +0.4 without round_to_nearest
|
||||
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_over_climate_regulation_limitations(hass: HomeAssistant, skip_hass_states_is_state, skip_send_event):
|
||||
async def test_over_climate_regulation_limitations(
|
||||
hass: HomeAssistant, skip_hass_states_is_state, skip_send_event
|
||||
):
|
||||
"""Test the limitations of the regulation of an over climate thermostat:
|
||||
1. test the period_min parameter: do not send regulation event too frequently
|
||||
2. test the dtemp parameter: do not send regulation event if offset temp is lower than dtemp
|
||||
1. test the period_min parameter: do not send regulation event too frequently
|
||||
2. test the dtemp parameter: do not send regulation event if offset temp is lower than dtemp
|
||||
"""
|
||||
|
||||
entry = MockConfigEntry(
|
||||
@@ -252,7 +260,8 @@ async def test_over_climate_regulation_limitations(hass: HomeAssistant, skip_has
|
||||
event_timestamp = now - timedelta(minutes=20)
|
||||
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.commons.NowClass.get_now", return_value=event_timestamp
|
||||
"custom_components.versatile_thermostat.commons.NowClass.get_now",
|
||||
return_value=event_timestamp,
|
||||
), patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
|
||||
return_value=fake_underlying_climate,
|
||||
@@ -268,7 +277,7 @@ async def test_over_climate_regulation_limitations(hass: HomeAssistant, skip_has
|
||||
if entity.entity_id == entity_id:
|
||||
return entity
|
||||
|
||||
entity:ThermostatOverClimate = find_my_entity("climate.theoverclimatemockname")
|
||||
entity: ThermostatOverClimate = find_my_entity("climate.theoverclimatemockname")
|
||||
|
||||
assert entity
|
||||
assert isinstance(entity, ThermostatOverClimate)
|
||||
@@ -289,46 +298,334 @@ async def test_over_climate_regulation_limitations(hass: HomeAssistant, skip_has
|
||||
# set manual target temp (at now - 19) -> the regulation should be ignored because too early
|
||||
event_timestamp = now - timedelta(minutes=19)
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.commons.NowClass.get_now", return_value=event_timestamp
|
||||
"custom_components.versatile_thermostat.commons.NowClass.get_now",
|
||||
return_value=event_timestamp,
|
||||
):
|
||||
await entity.async_set_temperature(temperature=18)
|
||||
|
||||
fake_underlying_climate.set_hvac_action(HVACAction.HEATING) # simulate under heating
|
||||
fake_underlying_climate.set_hvac_action(
|
||||
HVACAction.HEATING
|
||||
) # simulate under heating
|
||||
assert entity.hvac_action == HVACAction.HEATING
|
||||
|
||||
# the regulated temperature will change because when we set temp manually it is forced
|
||||
assert entity.regulated_target_temp == 20.
|
||||
assert entity.regulated_target_temp == 19.5
|
||||
|
||||
# set manual target temp (at now - 18) -> the regulation should be taken into account
|
||||
event_timestamp = now - timedelta(minutes=18)
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.commons.NowClass.get_now", return_value=event_timestamp
|
||||
"custom_components.versatile_thermostat.commons.NowClass.get_now",
|
||||
return_value=event_timestamp,
|
||||
):
|
||||
await entity.async_set_temperature(temperature=17)
|
||||
assert entity.regulated_target_temp > entity.target_temperature
|
||||
assert entity.regulated_target_temp == 18+1 # In strong we could go up to +3 degre. 0.7 without round_to_nearest
|
||||
assert (
|
||||
entity.regulated_target_temp == 18 + 0
|
||||
) # In strong we could go up to +3 degre. 0.7 without round_to_nearest
|
||||
old_regulated_temp = entity.regulated_target_temp
|
||||
|
||||
# change temperature so that dtemp < 0.5 and time is > period_min (+ 3min)
|
||||
event_timestamp = now - timedelta(minutes=15)
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.commons.NowClass.get_now", return_value=event_timestamp
|
||||
"custom_components.versatile_thermostat.commons.NowClass.get_now",
|
||||
return_value=event_timestamp,
|
||||
):
|
||||
await send_temperature_change_event(entity, 16, event_timestamp)
|
||||
await send_ext_temperature_change_event(entity, 10, event_timestamp)
|
||||
|
||||
# the regulated temperature should be under
|
||||
assert entity.regulated_target_temp == old_regulated_temp
|
||||
assert entity.regulated_target_temp <= old_regulated_temp
|
||||
|
||||
# change temperature so that dtemp > 0.5 and time is > period_min (+ 3min)
|
||||
event_timestamp = now - timedelta(minutes=12)
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.commons.NowClass.get_now", return_value=event_timestamp
|
||||
"custom_components.versatile_thermostat.commons.NowClass.get_now",
|
||||
return_value=event_timestamp,
|
||||
):
|
||||
await send_temperature_change_event(entity, 18, event_timestamp)
|
||||
await send_temperature_change_event(entity, 15, event_timestamp)
|
||||
await send_ext_temperature_change_event(entity, 12, event_timestamp)
|
||||
|
||||
# the regulated should have been done
|
||||
assert entity.regulated_target_temp != old_regulated_temp
|
||||
assert entity.regulated_target_temp >= entity.target_temperature
|
||||
assert (
|
||||
entity.regulated_target_temp == 17 + 1.5
|
||||
) # 0.7 without round_to_nearest
|
||||
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
# Disable this test which is not working when run in // of others.
|
||||
# I couldn't find out why
|
||||
@pytest.mark.skip
|
||||
async def test_over_climate_regulation_use_device_temp(
|
||||
hass: HomeAssistant, skip_hass_states_is_state, skip_send_event
|
||||
):
|
||||
"""Test the regulation of an over climate thermostat"""
|
||||
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
title="TheOverClimateMockName",
|
||||
unique_id="uniqueId",
|
||||
# This is include a medium regulation
|
||||
data=PARTIAL_CLIMATE_CONFIG_USE_DEVICE_TEMP | {CONF_AUTO_REGULATION_DTEMP: 0.5},
|
||||
)
|
||||
|
||||
tz = get_tz(hass) # pylint: disable=invalid-name
|
||||
now: datetime = datetime.now(tz=tz)
|
||||
|
||||
fake_underlying_climate = MockClimate(hass, "mockUniqueId", "MockClimateName", {})
|
||||
|
||||
assert fake_underlying_climate.current_temperature == 15
|
||||
|
||||
# Creates the regulated VTherm over climate
|
||||
# change temperature so that the heating will start
|
||||
event_timestamp = now - timedelta(minutes=10)
|
||||
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.commons.NowClass.get_now",
|
||||
return_value=event_timestamp,
|
||||
), patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
|
||||
return_value=fake_underlying_climate,
|
||||
):
|
||||
entity: ThermostatOverClimate = await create_thermostat(
|
||||
hass, entry, "climate.theoverclimatemockname"
|
||||
)
|
||||
assert entity
|
||||
assert isinstance(entity, ThermostatOverClimate)
|
||||
|
||||
assert entity.name == "TheOverClimateMockName"
|
||||
assert entity.is_over_climate is True
|
||||
assert entity.is_regulated is True
|
||||
assert entity.auto_regulation_use_device_temp is True
|
||||
|
||||
# 1. Activate the heating by changing HVACMode and temperature
|
||||
# Select a hvacmode, presence and preset
|
||||
await entity.async_set_hvac_mode(HVACMode.HEAT)
|
||||
assert entity.hvac_mode is HVACMode.HEAT
|
||||
assert entity.regulated_target_temp == entity.min_temp
|
||||
|
||||
await send_temperature_change_event(entity, 18, event_timestamp)
|
||||
await send_ext_temperature_change_event(entity, 10, event_timestamp)
|
||||
|
||||
# 2. set manual target temp (at now - 7) -> no regulation should occurs
|
||||
# room temp is 18
|
||||
# target is 16
|
||||
# internal heater temp is 15
|
||||
fake_underlying_climate.set_current_temperature(15)
|
||||
event_timestamp = now - timedelta(minutes=7)
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.commons.NowClass.get_now",
|
||||
return_value=event_timestamp,
|
||||
), patch("homeassistant.core.ServiceRegistry.async_call") as mock_service_call:
|
||||
await entity.async_set_temperature(temperature=16)
|
||||
|
||||
fake_underlying_climate.set_hvac_action(
|
||||
HVACAction.HEATING
|
||||
) # simulate under heating
|
||||
assert entity.hvac_action == HVACAction.HEATING
|
||||
assert entity.preset_mode == PRESET_NONE # Manual mode
|
||||
|
||||
# the regulated temperature should be higher
|
||||
assert entity.regulated_target_temp < entity.target_temperature
|
||||
# The calcul is the following: 16 + (16 - 18) x 0.4 (strong) + 0 x ki - 1 (device offset)
|
||||
assert (
|
||||
entity.regulated_target_temp == 15
|
||||
) # round(16 + (16 - 18) * 0.4 + 0 * 0.08)
|
||||
assert entity.hvac_action == HVACAction.HEATING
|
||||
|
||||
mock_service_call.assert_has_calls(
|
||||
[
|
||||
call.service_call(
|
||||
"climate",
|
||||
"set_temperature",
|
||||
{
|
||||
"entity_id": "climate.mock_climate",
|
||||
# because device offset is -3 but not used because target is reach
|
||||
"temperature": 15.0,
|
||||
"target_temp_high": 30,
|
||||
"target_temp_low": 15,
|
||||
},
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
# 3. change temperature so that the regulated temperature should slow down
|
||||
# HVACMODE.HEAT
|
||||
# room temp is 15
|
||||
# target is 18
|
||||
# internal heater temp is 20
|
||||
fake_underlying_climate.set_current_temperature(20.1)
|
||||
await entity.async_set_temperature(temperature=18)
|
||||
await send_ext_temperature_change_event(entity, 9, event_timestamp)
|
||||
|
||||
event_timestamp = now - timedelta(minutes=5)
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.commons.NowClass.get_now",
|
||||
return_value=event_timestamp,
|
||||
), patch("homeassistant.core.ServiceRegistry.async_call") as mock_service_call:
|
||||
await send_temperature_change_event(entity, 15, event_timestamp)
|
||||
|
||||
# the regulated temperature should be under (device offset is -2)
|
||||
assert entity.regulated_target_temp > entity.target_temperature
|
||||
assert entity.regulated_target_temp == 17 + 1.5 # 0.7 without round_to_nearest
|
||||
assert entity.regulated_target_temp == 19.5 # round(18 + 1.4, 0.5)
|
||||
|
||||
mock_service_call.assert_has_calls(
|
||||
[
|
||||
call.service_call(
|
||||
"climate",
|
||||
"set_temperature",
|
||||
{
|
||||
"entity_id": "climate.mock_climate",
|
||||
"temperature": 24.5, # round(19.5 + 5, 0.5)
|
||||
"target_temp_high": 30,
|
||||
"target_temp_low": 15,
|
||||
},
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
# 4. In cool mode
|
||||
# room temp is 25
|
||||
# target is 23
|
||||
# internal heater temp is 27
|
||||
await entity.async_set_hvac_mode(HVACMode.COOL)
|
||||
await entity.async_set_temperature(temperature=23)
|
||||
fake_underlying_climate.set_current_temperature(26.9)
|
||||
await send_ext_temperature_change_event(entity, 30, event_timestamp)
|
||||
|
||||
event_timestamp = now - timedelta(minutes=3)
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.commons.NowClass.get_now",
|
||||
return_value=event_timestamp,
|
||||
), patch("homeassistant.core.ServiceRegistry.async_call") as mock_service_call:
|
||||
await send_temperature_change_event(entity, 25, event_timestamp)
|
||||
|
||||
# the regulated temperature should be upper (device offset is +1.9)
|
||||
assert entity.regulated_target_temp < entity.target_temperature
|
||||
assert entity.regulated_target_temp == 22.5
|
||||
|
||||
mock_service_call.assert_has_calls(
|
||||
[
|
||||
call.service_call(
|
||||
"climate",
|
||||
"set_temperature",
|
||||
{
|
||||
"entity_id": "climate.mock_climate",
|
||||
"temperature": 24.5, # round(22.5 + 1.9° of offset)
|
||||
"target_temp_high": 30,
|
||||
"target_temp_low": 15,
|
||||
},
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_over_climate_regulation_dtemp_null(
|
||||
hass: HomeAssistant, skip_hass_states_is_state, skip_send_event
|
||||
):
|
||||
"""Test the regulation of an over climate thermostat with no Dtemp limitation"""
|
||||
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
title="TheOverClimateMockName",
|
||||
unique_id="uniqueId",
|
||||
# This is include a medium regulation
|
||||
data=PARTIAL_CLIMATE_AC_CONFIG | {CONF_AUTO_REGULATION_DTEMP: 0, CONF_STEP_TEMPERATURE: 0.1},
|
||||
)
|
||||
|
||||
tz = get_tz(hass) # pylint: disable=invalid-name
|
||||
now: datetime = datetime.now(tz=tz)
|
||||
fake_underlying_climate = MockClimate(hass, "mockUniqueId", "MockClimateName", {})
|
||||
|
||||
# Creates the regulated VTherm over climate
|
||||
# change temperature so that the heating will start
|
||||
event_timestamp = now - timedelta(minutes=20)
|
||||
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.commons.NowClass.get_now",
|
||||
return_value=event_timestamp,
|
||||
), patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
|
||||
return_value=fake_underlying_climate,
|
||||
):
|
||||
entity = await create_thermostat(hass, entry, "climate.theoverclimatemockname")
|
||||
|
||||
assert entity
|
||||
assert isinstance(entity, ThermostatOverClimate)
|
||||
|
||||
assert entity.name == "TheOverClimateMockName"
|
||||
assert entity.is_over_climate is True
|
||||
assert entity.is_regulated is True
|
||||
|
||||
# Activate the heating by changing HVACMode and temperature
|
||||
# Select a hvacmode, presence and preset
|
||||
await entity.async_set_hvac_mode(HVACMode.HEAT)
|
||||
assert entity.hvac_mode is HVACMode.HEAT
|
||||
assert entity.hvac_action == HVACAction.OFF
|
||||
|
||||
# change temperature so that the heating will start
|
||||
await send_temperature_change_event(entity, 15, event_timestamp)
|
||||
await send_ext_temperature_change_event(entity, 10, event_timestamp)
|
||||
|
||||
# set manual target temp
|
||||
event_timestamp = now - timedelta(minutes=17)
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.commons.NowClass.get_now",
|
||||
return_value=event_timestamp,
|
||||
):
|
||||
await entity.async_set_temperature(temperature=20)
|
||||
|
||||
fake_underlying_climate.set_hvac_action(
|
||||
HVACAction.HEATING
|
||||
) # simulate under cooling
|
||||
assert entity.hvac_action == HVACAction.HEATING
|
||||
assert entity.preset_mode == PRESET_NONE # Manual mode
|
||||
|
||||
# the regulated temperature should be lower
|
||||
assert entity.regulated_target_temp > entity.target_temperature
|
||||
assert (
|
||||
entity.regulated_target_temp == 20 + 2.4
|
||||
) # In medium we could go up to +3 degre
|
||||
assert entity.hvac_action == HVACAction.HEATING
|
||||
|
||||
# change temperature so that the regulated temperature should slow down
|
||||
event_timestamp = now - timedelta(minutes=15)
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.commons.NowClass.get_now",
|
||||
return_value=event_timestamp,
|
||||
):
|
||||
await send_temperature_change_event(entity, 19, event_timestamp)
|
||||
await send_ext_temperature_change_event(entity, 10, event_timestamp)
|
||||
|
||||
# the regulated temperature should be greater
|
||||
assert entity.regulated_target_temp > entity.target_temperature
|
||||
assert entity.regulated_target_temp == 20 + 0.9
|
||||
|
||||
# change temperature so that the regulated temperature should slow down
|
||||
event_timestamp = now - timedelta(minutes=13)
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.commons.NowClass.get_now",
|
||||
return_value=event_timestamp,
|
||||
):
|
||||
await send_temperature_change_event(entity, 20, event_timestamp)
|
||||
await send_ext_temperature_change_event(entity, 10, event_timestamp)
|
||||
|
||||
# the regulated temperature should be greater
|
||||
assert entity.regulated_target_temp > entity.target_temperature
|
||||
assert entity.regulated_target_temp == 20 + 0.5
|
||||
|
||||
old_regulated_temp = entity.regulated_target_temp
|
||||
# Test if a small temperature change is taken into account : change temperature so that dtemp < 0.5 and time is > period_min (+ 3min)
|
||||
event_timestamp = now - timedelta(minutes=10)
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.commons.NowClass.get_now",
|
||||
return_value=event_timestamp,
|
||||
):
|
||||
await send_temperature_change_event(entity, 19.6, event_timestamp)
|
||||
await send_ext_temperature_change_event(entity, 10, event_timestamp)
|
||||
|
||||
# the regulated temperature should be greater. This does not work if dtemp is not null
|
||||
assert entity.regulated_target_temp > old_regulated_temp
|
||||
|
||||
1465
tests/test_auto_start_stop.py
Normal file
@@ -1,92 +1,34 @@
|
||||
# pylint: disable=wildcard-import, unused-wildcard-import, protected-access, unused-argument, line-too-long
|
||||
# pylint: disable=wildcard-import, unused-wildcard-import, protected-access, unused-argument, line-too-long, too-many-lines
|
||||
|
||||
""" Test the Window management """
|
||||
from unittest.mock import patch, call
|
||||
import asyncio
|
||||
|
||||
from unittest.mock import patch, call, PropertyMock
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
import logging
|
||||
|
||||
from homeassistant.const import STATE_ON, STATE_OFF
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.components.climate import (
|
||||
SERVICE_SET_TEMPERATURE,
|
||||
)
|
||||
|
||||
from custom_components.versatile_thermostat.config_flow import (
|
||||
VersatileThermostatBaseConfigFlow,
|
||||
)
|
||||
from custom_components.versatile_thermostat.thermostat_climate import (
|
||||
ThermostatOverClimate,
|
||||
)
|
||||
from custom_components.versatile_thermostat.thermostat_switch import (
|
||||
ThermostatOverSwitch,
|
||||
)
|
||||
|
||||
from .commons import *
|
||||
|
||||
logging.getLogger().setLevel(logging.DEBUG)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_bug_56(
|
||||
hass: HomeAssistant,
|
||||
skip_hass_states_is_state,
|
||||
skip_turn_on_off_heater,
|
||||
skip_send_event,
|
||||
):
|
||||
"""Test that in over_climate mode there is no error when underlying climate is not available"""
|
||||
|
||||
the_mock_underlying = MagicMockClimate()
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
|
||||
return_value=None, # dont find the underlying climate
|
||||
):
|
||||
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: 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: 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,
|
||||
},
|
||||
)
|
||||
|
||||
entity: BaseThermostat = await create_thermostat(
|
||||
hass, entry, "climate.theoverclimatemockname"
|
||||
)
|
||||
assert entity
|
||||
# cause the underlying climate was not found
|
||||
assert entity.is_over_climate is True
|
||||
assert entity.underlying_entity(0)._underlying_climate is None
|
||||
|
||||
# Should not failed
|
||||
entity.update_custom_attributes()
|
||||
|
||||
# try to call async_control_heating
|
||||
try:
|
||||
ret = await entity.async_control_heating()
|
||||
# an exception should be send
|
||||
assert ret is False
|
||||
except Exception: # pylint: disable=broad-exception-caught
|
||||
assert False
|
||||
|
||||
# This time the underlying will be found
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
|
||||
return_value=the_mock_underlying, # dont find the underlying climate
|
||||
):
|
||||
# try to call async_control_heating
|
||||
try:
|
||||
await entity.async_control_heating()
|
||||
except UnknownEntity:
|
||||
assert False
|
||||
except Exception: # pylint: disable=broad-exception-caught
|
||||
assert False
|
||||
|
||||
# Should not failed
|
||||
entity.update_custom_attributes()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_bug_63(
|
||||
@@ -188,17 +130,159 @@ async def test_bug_64(
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_bug_66(
|
||||
async def test_bug_272(
|
||||
hass: HomeAssistant,
|
||||
skip_hass_states_is_state,
|
||||
skip_turn_on_off_heater,
|
||||
skip_send_event,
|
||||
):
|
||||
"""Test that it should be possible to open/close the window rapidly without side effect"""
|
||||
"""Test that it not possible to set the target temperature under the min_temp setting"""
|
||||
|
||||
tz = get_tz(hass) # pylint: disable=invalid-name
|
||||
now: datetime = datetime.now(tz=tz)
|
||||
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
title="TheOverClimateMockName",
|
||||
unique_id="uniqueId",
|
||||
# default value are min 15°, max 30°, step 0.1
|
||||
data=PARTIAL_CLIMATE_CONFIG, # 5 minutes security delay
|
||||
)
|
||||
|
||||
# Min_temp is 15 and max_temp is 19
|
||||
fake_underlying_climate = MagicMockClimate()
|
||||
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
|
||||
), patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
|
||||
return_value=fake_underlying_climate,
|
||||
), patch(
|
||||
"homeassistant.core.ServiceRegistry.async_call"
|
||||
) as mock_service_call:
|
||||
entity = await create_thermostat(hass, entry, "climate.theoverclimatemockname")
|
||||
# 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 = find_my_entity("climate.theoverclimatemockname")
|
||||
|
||||
assert entity
|
||||
|
||||
assert entity.name == "TheOverClimateMockName"
|
||||
assert entity.is_over_climate is True
|
||||
assert entity.hvac_mode is HVACMode.OFF
|
||||
# The VTherm value and not the underlying value
|
||||
assert entity.target_temperature_step == 0.1
|
||||
assert entity.target_temperature == entity.min_temp
|
||||
assert entity.is_regulated is True
|
||||
|
||||
assert mock_service_call.call_count == 0
|
||||
|
||||
# Set the hvac_mode to HEAT
|
||||
await entity.async_set_hvac_mode(HVACMode.HEAT)
|
||||
|
||||
# In the accepted interval
|
||||
await entity.async_set_temperature(temperature=17.5)
|
||||
|
||||
# MagicMock climate is already HEAT by default. So there is no SET_HAVC_MODE call
|
||||
assert mock_service_call.call_count == 1
|
||||
mock_service_call.assert_has_calls(
|
||||
[
|
||||
# call.async_call(
|
||||
# "climate",
|
||||
# SERVICE_SET_HVAC_MODE,
|
||||
# {"entity_id": "climate.mock_climate", "hvac_mode": HVACMode.HEAT},
|
||||
# ),
|
||||
call.async_call(
|
||||
"climate",
|
||||
SERVICE_SET_TEMPERATURE,
|
||||
{
|
||||
"entity_id": "climate.mock_climate",
|
||||
"temperature": 17.5,
|
||||
# "target_temp_high": 30,
|
||||
# "target_temp_low": 15,
|
||||
},
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
tz = get_tz(hass) # pylint: disable=invalid-name
|
||||
now: datetime = datetime.now(tz=tz)
|
||||
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
|
||||
), patch("homeassistant.core.ServiceRegistry.async_call") as mock_service_call:
|
||||
# Set room temperature to something very cold
|
||||
event_timestamp = now + timedelta(minutes=1)
|
||||
|
||||
await send_temperature_change_event(entity, 13, event_timestamp)
|
||||
await send_ext_temperature_change_event(entity, 9, event_timestamp)
|
||||
|
||||
# Not in the accepted interval (15-19)
|
||||
await entity.async_set_temperature(temperature=10)
|
||||
assert mock_service_call.call_count == 1
|
||||
mock_service_call.assert_has_calls(
|
||||
[
|
||||
call.async_call(
|
||||
"climate",
|
||||
SERVICE_SET_TEMPERATURE,
|
||||
{
|
||||
"entity_id": "climate.mock_climate",
|
||||
"temperature": 15, # the minimum acceptable
|
||||
# "target_temp_high": 30,
|
||||
# "target_temp_low": 15,
|
||||
},
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
|
||||
), patch("homeassistant.core.ServiceRegistry.async_call") as mock_service_call:
|
||||
# Set room temperature to something very cold
|
||||
event_timestamp = now + timedelta(minutes=1)
|
||||
|
||||
await send_temperature_change_event(entity, 13, event_timestamp)
|
||||
await send_ext_temperature_change_event(entity, 9, event_timestamp)
|
||||
|
||||
# In the accepted interval
|
||||
await entity.async_set_temperature(temperature=20.8)
|
||||
assert mock_service_call.call_count == 1
|
||||
mock_service_call.assert_has_calls(
|
||||
[
|
||||
call.async_call(
|
||||
"climate",
|
||||
SERVICE_SET_TEMPERATURE,
|
||||
{
|
||||
"entity_id": "climate.mock_climate",
|
||||
"temperature": 19, # the maximum acceptable
|
||||
# "target_temp_high": 30,
|
||||
# "target_temp_low": 15,
|
||||
},
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
@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):
|
||||
"""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,
|
||||
3. expected: no shredding should occur because the heater was already active,
|
||||
4. constated: the heater goes into shredding.
|
||||
|
||||
"""
|
||||
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
title="TheOverSwitchMockName",
|
||||
@@ -214,9 +298,9 @@ async def test_bug_66(
|
||||
"eco_temp": 17,
|
||||
"comfort_temp": 18,
|
||||
"boost_temp": 19,
|
||||
CONF_USE_WINDOW_FEATURE: True,
|
||||
CONF_USE_WINDOW_FEATURE: False,
|
||||
CONF_USE_MOTION_FEATURE: False,
|
||||
CONF_USE_POWER_FEATURE: False,
|
||||
CONF_USE_POWER_FEATURE: True,
|
||||
CONF_USE_PRESENCE_FEATURE: False,
|
||||
CONF_HEATER: "switch.mock_switch",
|
||||
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
|
||||
@@ -224,322 +308,308 @@ async def test_bug_66(
|
||||
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_DEVICE_POWER: 200,
|
||||
CONF_WINDOW_SENSOR: "binary_sensor.mock_window_sensor",
|
||||
CONF_WINDOW_DELAY: 0, # important to not been obliged to wait
|
||||
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
|
||||
CONF_POWER_SENSOR: "sensor.mock_power_sensor",
|
||||
CONF_MAX_POWER_SENSOR: "sensor.mock_power_max_sensor",
|
||||
CONF_DEVICE_POWER: 100,
|
||||
CONF_PRESET_POWER: 12,
|
||||
},
|
||||
)
|
||||
|
||||
entity: BaseThermostat = await create_thermostat(
|
||||
entity: ThermostatOverSwitch = await create_thermostat(
|
||||
hass, entry, "climate.theoverswitchmockname"
|
||||
)
|
||||
assert entity
|
||||
|
||||
await entity.async_set_hvac_mode(HVACMode.HEAT)
|
||||
await entity.async_set_preset_mode(PRESET_BOOST)
|
||||
tpi_algo = entity._prop_algorithm
|
||||
assert tpi_algo
|
||||
|
||||
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
|
||||
tz = get_tz(hass) # pylint: disable=invalid-name
|
||||
now: datetime = datetime.now(tz=tz)
|
||||
|
||||
# Open the window and let the thermostat shut down
|
||||
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
|
||||
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(
|
||||
"homeassistant.helpers.condition.state", return_value=True
|
||||
) as mock_condition, 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,
|
||||
):
|
||||
await send_temperature_change_event(entity, 15, now)
|
||||
try_window_condition = await send_window_change_event(
|
||||
entity, True, False, now, False
|
||||
)
|
||||
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.target_temperature == 18
|
||||
# waits that the heater starts
|
||||
await asyncio.sleep(0.1)
|
||||
assert mock_service_call.call_count >= 1
|
||||
assert entity.is_device_active is True
|
||||
|
||||
# simulate the call to try_window_condition
|
||||
await try_window_condition(None)
|
||||
# Send power max mesurement
|
||||
await send_max_power_change_event(entity, 110, datetime.now())
|
||||
# Send power mesurement (theheater is already in the power measurement)
|
||||
await send_power_change_event(entity, 100, datetime.now())
|
||||
# No overpowering yet
|
||||
assert await entity.check_overpowering() 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.is_device_active is True
|
||||
|
||||
assert mock_send_event.call_count == 1
|
||||
assert mock_heater_on.call_count == 1
|
||||
assert mock_heater_off.call_count >= 1
|
||||
assert mock_condition.call_count == 1
|
||||
|
||||
assert entity.window_state == STATE_ON
|
||||
|
||||
# Close the window but too shortly
|
||||
# 2. An already active heater that switch preset will not switch to 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, patch(
|
||||
"homeassistant.helpers.condition.state", return_value=False
|
||||
) as mock_condition, patch(
|
||||
"homeassistant.core.ServiceRegistry.async_call"
|
||||
) as mock_service_call, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
|
||||
return_value=False,
|
||||
new_callable=PropertyMock,
|
||||
return_value=True,
|
||||
):
|
||||
event_timestamp = now + timedelta(minutes=1)
|
||||
try_window_condition = await send_window_change_event(
|
||||
entity, False, True, event_timestamp
|
||||
)
|
||||
# simulate the call to try_window_condition
|
||||
await try_window_condition(None)
|
||||
# change preset to Boost
|
||||
await entity.async_set_preset_mode(PRESET_BOOST)
|
||||
# waits that the heater starts
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
# window state should not have change
|
||||
assert entity.window_state == STATE_ON
|
||||
|
||||
# Reopen immediatly with sufficient time
|
||||
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(
|
||||
"homeassistant.helpers.condition.state", return_value=True
|
||||
) as mock_condition, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
|
||||
return_value=False,
|
||||
):
|
||||
try_window_condition = await send_window_change_event(
|
||||
entity, True, False, event_timestamp
|
||||
)
|
||||
# simulate the call to try_window_condition
|
||||
await try_window_condition(None)
|
||||
|
||||
# still no change
|
||||
assert entity.window_state == STATE_ON
|
||||
assert entity.hvac_mode == HVACMode.OFF
|
||||
|
||||
# Close the window but with sufficient time this time
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
|
||||
) as mock_send_event, patch(
|
||||
"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(
|
||||
"homeassistant.helpers.condition.state", return_value=True
|
||||
) as mock_condition, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
|
||||
return_value=False,
|
||||
):
|
||||
event_timestamp = now + timedelta(minutes=2)
|
||||
try_window_condition = await send_window_change_event(
|
||||
entity, False, True, event_timestamp
|
||||
)
|
||||
# simulate the call to try_window_condition
|
||||
await try_window_condition(None)
|
||||
|
||||
# window state should be Off this time and old state should have been restored
|
||||
assert entity.window_state == STATE_OFF
|
||||
assert await entity.check_overpowering() is False
|
||||
assert entity.hvac_mode is HVACMode.HEAT
|
||||
assert entity.preset_mode is PRESET_BOOST
|
||||
assert entity.overpowering_state is False
|
||||
assert entity.target_temperature == 19
|
||||
assert mock_service_call.call_count >= 1
|
||||
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_bug_82(
|
||||
hass: HomeAssistant,
|
||||
skip_hass_states_is_state,
|
||||
skip_turn_on_off_heater,
|
||||
skip_send_event,
|
||||
):
|
||||
"""Test that when a underlying climate is not available the VTherm doesn't go into security mode"""
|
||||
|
||||
tz = get_tz(hass) # pylint: disable=invalid-name
|
||||
now: datetime = datetime.now(tz=tz)
|
||||
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
title="TheOverClimateMockName",
|
||||
unique_id="uniqueId",
|
||||
data=PARTIAL_CLIMATE_CONFIG, # 5 minutes security delay
|
||||
)
|
||||
|
||||
fake_underlying_climate = MockUnavailableClimate(hass, "mockUniqueId", "MockClimateName", {})
|
||||
|
||||
# 3. if heater is stopped (is_device_active==False), then overpowering should be started
|
||||
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",
|
||||
return_value=fake_underlying_climate,
|
||||
) as mock_find_climate:
|
||||
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 = find_my_entity("climate.theoverclimatemockname")
|
||||
|
||||
assert entity
|
||||
|
||||
assert entity.name == "TheOverClimateMockName"
|
||||
assert entity.is_over_climate is True
|
||||
# assert entity.hvac_action is HVACAction.OFF
|
||||
assert entity.hvac_mode is HVACMode.OFF
|
||||
# assert entity.hvac_mode is None
|
||||
assert entity.target_temperature == entity.min_temp
|
||||
assert entity.preset_modes == [
|
||||
PRESET_NONE,
|
||||
PRESET_ECO,
|
||||
PRESET_COMFORT,
|
||||
PRESET_BOOST,
|
||||
]
|
||||
assert entity.preset_mode is PRESET_NONE
|
||||
assert entity._security_state is False
|
||||
|
||||
# should have been called with EventType.PRESET_EVENT and EventType.HVAC_MODE_EVENT
|
||||
assert mock_send_event.call_count == 2
|
||||
mock_send_event.assert_has_calls(
|
||||
[
|
||||
call.send_event(EventType.PRESET_EVENT, {"preset": PRESET_NONE}),
|
||||
call.send_event(
|
||||
EventType.HVAC_MODE_EVENT,
|
||||
{"hvac_mode": HVACMode.OFF},
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
assert mock_find_climate.call_count == 1
|
||||
assert mock_find_climate.mock_calls[0] == call()
|
||||
mock_find_climate.assert_has_calls([call.find_underlying_entity()])
|
||||
|
||||
# Force security mode
|
||||
assert entity._last_ext_temperature_mesure is not None
|
||||
assert entity._last_temperature_mesure is not None
|
||||
assert (entity._last_temperature_mesure.astimezone(tz) - now).total_seconds() < 1
|
||||
assert (
|
||||
entity._last_ext_temperature_mesure.astimezone(tz) - now
|
||||
).total_seconds() < 1
|
||||
|
||||
# Tries to turns on the Thermostat
|
||||
await entity.async_set_hvac_mode(HVACMode.HEAT)
|
||||
assert entity.hvac_mode == HVACMode.HEAT
|
||||
|
||||
# 2. activate security feature when date is expired
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
|
||||
) as mock_send_event, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
|
||||
):
|
||||
event_timestamp = now - timedelta(minutes=6)
|
||||
|
||||
# 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)
|
||||
# Should stay False
|
||||
assert entity.security_state is False
|
||||
assert entity.preset_mode == 'none'
|
||||
assert entity._saved_preset_mode == 'none'
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_bug_101(
|
||||
hass: HomeAssistant,
|
||||
skip_hass_states_is_state,
|
||||
skip_turn_on_off_heater,
|
||||
skip_send_event,
|
||||
):
|
||||
"""Test that when a underlying climate target temp is changed, the VTherm change its own temperature target and switch to manual"""
|
||||
|
||||
tz = get_tz(hass) # pylint: disable=invalid-name
|
||||
now: datetime = datetime.now(tz=tz)
|
||||
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
title="TheOverClimateMockName",
|
||||
unique_id="uniqueId",
|
||||
data=PARTIAL_CLIMATE_NOT_REGULATED_CONFIG, # 5 minutes security delay
|
||||
)
|
||||
|
||||
# Underlying is in HEAT mode but should be shutdown at startup
|
||||
fake_underlying_climate = MockClimate(hass, "mockUniqueId", "MockClimateName", {}, HVACMode.HEAT, HVACAction.HEATING)
|
||||
|
||||
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",
|
||||
return_value=fake_underlying_climate,
|
||||
) as mock_find_climate, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.set_hvac_mode"
|
||||
) as mock_underlying_set_hvac_mode:
|
||||
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 = find_my_entity("climate.theoverclimatemockname")
|
||||
|
||||
assert entity
|
||||
|
||||
assert entity.name == "TheOverClimateMockName"
|
||||
assert entity.is_over_climate is True
|
||||
assert entity.hvac_mode is HVACMode.OFF
|
||||
# because in MockClimate HVACAction is HEATING if hvac_mode is not set
|
||||
assert entity.hvac_action is HVACAction.HEATING
|
||||
# Underlying should have been shutdown
|
||||
assert mock_underlying_set_hvac_mode.call_count == 1
|
||||
mock_underlying_set_hvac_mode.assert_has_calls(
|
||||
[
|
||||
call.set_hvac_mode(HVACMode.OFF),
|
||||
]
|
||||
)
|
||||
|
||||
assert entity.target_temperature == entity.min_temp
|
||||
assert entity.preset_mode is PRESET_NONE
|
||||
|
||||
# should have been called with EventType.PRESET_EVENT and EventType.HVAC_MODE_EVENT
|
||||
assert mock_send_event.call_count == 2
|
||||
mock_send_event.assert_has_calls(
|
||||
[
|
||||
call.send_event(EventType.PRESET_EVENT, {"preset": PRESET_NONE}),
|
||||
call.send_event(
|
||||
EventType.HVAC_MODE_EVENT,
|
||||
{"hvac_mode": HVACMode.OFF},
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
assert mock_find_climate.call_count == 1
|
||||
assert mock_find_climate.mock_calls[0] == call()
|
||||
mock_find_climate.assert_has_calls([call.find_underlying_entity()])
|
||||
|
||||
# Force preset mode
|
||||
await entity.async_set_hvac_mode(HVACMode.HEAT)
|
||||
assert entity.hvac_mode == HVACMode.HEAT
|
||||
"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,
|
||||
):
|
||||
# change preset to Boost
|
||||
await entity.async_set_preset_mode(PRESET_COMFORT)
|
||||
assert entity.preset_mode == PRESET_COMFORT
|
||||
# waits that the heater starts
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
# 2. Change the target temp of underlying thermostat at now -> the event will be disgarded because to fast (to avoid loop cf issue 121)
|
||||
await send_climate_change_event_with_temperature(entity, HVACMode.HEAT, HVACMode.HEAT, HVACAction.OFF, HVACAction.OFF, now, 12.75)
|
||||
# Should NOT have been switched to Manual preset
|
||||
assert entity.target_temperature == 17
|
||||
assert entity.preset_mode is PRESET_COMFORT
|
||||
assert await entity.check_overpowering() is True
|
||||
assert entity.hvac_mode is HVACMode.HEAT
|
||||
assert entity.preset_mode is PRESET_POWER
|
||||
assert entity.overpowering_state is True
|
||||
|
||||
# 2. Change the target temp of underlying thermostat at 11 sec later -> the event will be taken
|
||||
# Wait 11 sec
|
||||
event_timestamp = now + timedelta(seconds=11)
|
||||
assert entity.is_regulated is False
|
||||
await send_climate_change_event_with_temperature(entity, HVACMode.HEAT, HVACMode.HEAT, HVACAction.OFF, HVACAction.OFF, event_timestamp, 12.75)
|
||||
assert entity.target_temperature == 12.75
|
||||
assert entity.preset_mode is PRESET_NONE
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_bug_500_1(hass: HomeAssistant, init_vtherm_api) -> None:
|
||||
"""Test that the form is served with no input"""
|
||||
|
||||
config = {
|
||||
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_SWITCH,
|
||||
CONF_USE_WINDOW_CENTRAL_CONFIG: True,
|
||||
CONF_USE_POWER_CENTRAL_CONFIG: True,
|
||||
CONF_USE_PRESENCE_CENTRAL_CONFIG: True,
|
||||
CONF_USE_MOTION_FEATURE: True,
|
||||
CONF_MOTION_SENSOR: "sensor.theMotionSensor",
|
||||
}
|
||||
|
||||
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_PRESENCE_FEATURE] is True
|
||||
assert flow._infos[CONF_USE_MOTION_FEATURE] is True
|
||||
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_bug_500_2(hass: HomeAssistant, init_vtherm_api) -> None:
|
||||
"""Test that the form is served with no input"""
|
||||
|
||||
config = {
|
||||
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_SWITCH,
|
||||
CONF_USE_WINDOW_CENTRAL_CONFIG: False,
|
||||
CONF_USE_POWER_CENTRAL_CONFIG: False,
|
||||
CONF_USE_PRESENCE_CENTRAL_CONFIG: False,
|
||||
CONF_USE_MOTION_FEATURE: False,
|
||||
}
|
||||
|
||||
flow = VersatileThermostatBaseConfigFlow(config)
|
||||
|
||||
assert flow._infos[CONF_USE_WINDOW_FEATURE] is False
|
||||
assert flow._infos[CONF_USE_POWER_FEATURE] is False
|
||||
assert flow._infos[CONF_USE_PRESENCE_FEATURE] is False
|
||||
assert flow._infos[CONF_USE_MOTION_FEATURE] is False
|
||||
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_bug_500_3(hass: HomeAssistant, init_vtherm_api) -> None:
|
||||
"""Test that the form is served with no input"""
|
||||
|
||||
config = {
|
||||
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_SWITCH,
|
||||
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
|
||||
CONF_MOTION_SENSOR: "sensor.theMotionSensor",
|
||||
}
|
||||
|
||||
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_PRESENCE_FEATURE] is True
|
||||
assert flow._infos[CONF_USE_MOTION_FEATURE] is True
|
||||
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_bug_465(hass: HomeAssistant, skip_hass_states_is_state):
|
||||
"""Test store and restore hvac_mode on toggle hvac state"""
|
||||
|
||||
# vtherm_api: VersatileThermostatAPI = VersatileThermostatAPI.get_vtherm_api(hass)
|
||||
|
||||
# The temperatures to set
|
||||
temps = {
|
||||
"frost": 7.0,
|
||||
"eco": 17.0,
|
||||
"comfort": 19.0,
|
||||
"boost": 21.0,
|
||||
"eco_ac": 27.0,
|
||||
"comfort_ac": 25.0,
|
||||
"boost_ac": 23.0,
|
||||
"frost_away": 7.1,
|
||||
"eco_away": 17.1,
|
||||
"comfort_away": 19.1,
|
||||
"boost_away": 21.1,
|
||||
"eco_ac_away": 27.1,
|
||||
"comfort_ac_away": 25.1,
|
||||
"boost_ac_away": 23.1,
|
||||
}
|
||||
|
||||
# 0. initialisation
|
||||
|
||||
config_entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
title="TheOverClimateMockName",
|
||||
unique_id="overClimateUniqueId",
|
||||
data={
|
||||
CONF_NAME: "overClimate",
|
||||
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
|
||||
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_CLIMATE,
|
||||
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
|
||||
CONF_CYCLE_MIN: 5,
|
||||
CONF_TEMP_MIN: 15,
|
||||
CONF_TEMP_MAX: 30,
|
||||
CONF_USE_WINDOW_FEATURE: True,
|
||||
CONF_WINDOW_SENSOR: "binary_sensor.window_sensor",
|
||||
CONF_WINDOW_ACTION: CONF_WINDOW_TURN_OFF,
|
||||
CONF_WINDOW_DELAY: 1,
|
||||
CONF_USE_MOTION_FEATURE: False,
|
||||
CONF_USE_POWER_FEATURE: False,
|
||||
CONF_USE_PRESENCE_FEATURE: True,
|
||||
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_AUTO_FAN_MODE: CONF_AUTO_FAN_TURBO,
|
||||
CONF_AC_MODE: True,
|
||||
},
|
||||
)
|
||||
|
||||
fake_underlying_climate = MockClimate(
|
||||
hass=hass,
|
||||
unique_id="mock_climate",
|
||||
name="mock_climate",
|
||||
hvac_modes=[HVACMode.OFF, HVACMode.COOL, HVACMode.HEAT, HVACMode.FAN_ONLY],
|
||||
)
|
||||
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
|
||||
return_value=fake_underlying_climate,
|
||||
):
|
||||
vtherm: ThermostatOverClimate = await create_thermostat(
|
||||
hass, config_entry, "climate.overclimate"
|
||||
)
|
||||
assert vtherm is not None
|
||||
|
||||
await set_all_climate_preset_temp(hass, vtherm, temps, "overclimate")
|
||||
|
||||
now: datetime = datetime.now(tz=get_tz(hass))
|
||||
|
||||
# 1. Set mode to Heat and preset to Comfort
|
||||
await send_presence_change_event(vtherm, True, False, datetime.now())
|
||||
await vtherm.async_set_hvac_mode(HVACMode.HEAT)
|
||||
await vtherm.async_set_preset_mode(PRESET_BOOST)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert vtherm.target_temperature == 21.0
|
||||
|
||||
# 2. Toggle the VTherm state
|
||||
await vtherm.async_toggle()
|
||||
await hass.async_block_till_done()
|
||||
assert vtherm.hvac_mode == HVACMode.OFF
|
||||
|
||||
# 3. (re)Toggle the VTherm state
|
||||
await vtherm.async_toggle()
|
||||
await hass.async_block_till_done()
|
||||
assert vtherm.hvac_mode == HVACMode.HEAT
|
||||
|
||||
# 4. Toggle from COOL
|
||||
await vtherm.async_set_hvac_mode(HVACMode.COOL)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert vtherm.target_temperature == 23.0
|
||||
|
||||
# 5. Toggle the VTherm state
|
||||
await vtherm.async_toggle()
|
||||
await hass.async_block_till_done()
|
||||
assert vtherm.hvac_mode == HVACMode.OFF
|
||||
|
||||
# 6. (re)Toggle the VTherm state
|
||||
await vtherm.async_toggle()
|
||||
await hass.async_block_till_done()
|
||||
assert vtherm.hvac_mode == HVACMode.COOL
|
||||
|
||||
###
|
||||
# Same test with an open window and initial state is COOL
|
||||
#
|
||||
# 7. open the window
|
||||
with patch("homeassistant.helpers.condition.state", return_value=True):
|
||||
try_window_condition = await send_window_change_event(
|
||||
vtherm, True, False, now, False
|
||||
)
|
||||
await try_window_condition(None)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert vtherm.window_state is STATE_ON
|
||||
assert vtherm.hvac_mode == HVACMode.OFF
|
||||
|
||||
# 8. call toggle -> we should stay in OFF (command is ignored)
|
||||
await vtherm.async_toggle()
|
||||
await hass.async_block_till_done()
|
||||
assert vtherm.hvac_mode == HVACMode.OFF
|
||||
|
||||
# 9. Close the window (we should come back to Cool this time)
|
||||
now = now + timedelta(minutes=2)
|
||||
with patch("homeassistant.helpers.condition.state", return_value=True):
|
||||
try_window_condition = await send_window_change_event(
|
||||
vtherm, False, True, now, False
|
||||
)
|
||||
await try_window_condition(None)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert vtherm.window_state is STATE_OFF
|
||||
assert vtherm.hvac_mode == HVACMode.COOL
|
||||
|
||||
# 9. call toggle -> we should come back in OFF
|
||||
await vtherm.async_toggle()
|
||||
await hass.async_block_till_done()
|
||||
assert vtherm.hvac_mode == HVACMode.OFF
|
||||
|
||||
921
tests/test_central_boiler.py
Normal file
@@ -0,0 +1,921 @@
|
||||
# pylint: disable=wildcard-import, unused-wildcard-import, protected-access, unused-argument, line-too-long
|
||||
|
||||
""" Test the central_configuration """
|
||||
import asyncio
|
||||
from datetime import datetime
|
||||
|
||||
from unittest.mock import patch, call
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
|
||||
from pytest_homeassistant_custom_component.common import MockConfigEntry
|
||||
|
||||
from custom_components.versatile_thermostat.thermostat_switch import (
|
||||
ThermostatOverSwitch,
|
||||
)
|
||||
|
||||
from custom_components.versatile_thermostat.thermostat_valve import (
|
||||
ThermostatOverValve,
|
||||
)
|
||||
|
||||
from custom_components.versatile_thermostat.thermostat_climate import (
|
||||
ThermostatOverClimate,
|
||||
)
|
||||
|
||||
from custom_components.versatile_thermostat.vtherm_api import VersatileThermostatAPI
|
||||
from custom_components.versatile_thermostat.binary_sensor import (
|
||||
CentralBoilerBinarySensor,
|
||||
)
|
||||
|
||||
from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import
|
||||
from .const import * # pylint: disable=wildcard-import, unused-wildcard-import
|
||||
|
||||
|
||||
# @pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
# @pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_add_a_central_config_with_boiler(
|
||||
hass: HomeAssistant,
|
||||
skip_hass_states_is_state,
|
||||
):
|
||||
"""Tests the clean_central_config_doubon of base_thermostat"""
|
||||
central_config_entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
title="TheCentralConfigMockName",
|
||||
unique_id="centralConfigUniqueId",
|
||||
data=FULL_CENTRAL_CONFIG_WITH_BOILER,
|
||||
)
|
||||
|
||||
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: ThermostatOverClimate = search_entity(
|
||||
hass, "climate.thecentralconfigmockname", "climate"
|
||||
)
|
||||
|
||||
assert entity is None
|
||||
|
||||
assert count_entities(hass, "climate.thecentralconfigmockname", "climate") == 0
|
||||
|
||||
# Test that VTherm API find the CentralConfig
|
||||
api = VersatileThermostatAPI.get_vtherm_api(hass)
|
||||
central_configuration = api.find_central_configuration()
|
||||
assert central_configuration is not None
|
||||
|
||||
# Test that VTherm API have any central boiler entities
|
||||
assert api.nb_active_device_for_boiler_entity is not None
|
||||
assert api.nb_active_device_for_boiler == 0
|
||||
|
||||
assert api.nb_active_device_for_boiler_threshold_entity is not None
|
||||
assert api.nb_active_device_for_boiler_threshold == 1 # the default value is 1
|
||||
|
||||
|
||||
async def test_update_central_boiler_state_simple(
|
||||
hass: HomeAssistant,
|
||||
# skip_hass_states_is_state,
|
||||
init_central_config_with_boiler_fixture,
|
||||
):
|
||||
"""Test that the central boiler state behavoir"""
|
||||
|
||||
api = VersatileThermostatAPI.get_vtherm_api(hass)
|
||||
|
||||
switch1 = MockSwitch(hass, "switch1", "theSwitch1")
|
||||
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
title="TheOverSwitchMockName",
|
||||
unique_id="uniqueId",
|
||||
data={
|
||||
CONF_NAME: "TheOverSwitchMockName",
|
||||
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_SWITCH,
|
||||
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
|
||||
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
|
||||
CONF_CYCLE_MIN: 5,
|
||||
CONF_TEMP_MIN: 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_HEATER: 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_USE_MAIN_CENTRAL_CONFIG: True,
|
||||
CONF_USE_TPI_CENTRAL_CONFIG: True,
|
||||
CONF_USE_PRESETS_CENTRAL_CONFIG: True,
|
||||
CONF_USE_ADVANCED_CENTRAL_CONFIG: True,
|
||||
CONF_USED_BY_CENTRAL_BOILER: True,
|
||||
},
|
||||
)
|
||||
|
||||
entity: ThermostatOverSwitch = await create_thermostat(
|
||||
hass, entry, "climate.theoverswitchmockname"
|
||||
)
|
||||
assert entity
|
||||
assert entity.name == "TheOverSwitchMockName"
|
||||
assert entity.is_over_switch
|
||||
assert entity.underlying_entities[0].entity_id == "switch.switch1"
|
||||
|
||||
assert api.nb_active_device_for_boiler_threshold == 1
|
||||
assert api.nb_active_device_for_boiler == 0
|
||||
|
||||
# Force the VTherm to heat
|
||||
await entity.async_set_hvac_mode(HVACMode.HEAT)
|
||||
await entity.async_set_preset_mode(PRESET_BOOST)
|
||||
|
||||
tz = get_tz(hass) # pylint: disable=invalid-name
|
||||
now: datetime = datetime.now(tz=tz)
|
||||
await send_temperature_change_event(entity, 10, now)
|
||||
|
||||
assert entity.hvac_mode == HVACMode.HEAT
|
||||
|
||||
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 heater
|
||||
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:
|
||||
await switch1.async_turn_on()
|
||||
switch1.async_write_ha_state()
|
||||
# Wait for state event propagation
|
||||
await asyncio.sleep(1)
|
||||
|
||||
assert entity.hvac_action == HVACAction.HEATING
|
||||
|
||||
assert mock_service_call.call_count == 2
|
||||
|
||||
# Sometimes this test fails
|
||||
mock_service_call.assert_has_calls(
|
||||
[
|
||||
call.service_call(
|
||||
"switch",
|
||||
"turn_on",
|
||||
{"entity_id": "switch.switch1"},
|
||||
),
|
||||
call(
|
||||
"switch",
|
||||
"turn_on",
|
||||
service_data={},
|
||||
target={"entity_id": "switch.pompe_chaudiere"},
|
||||
),
|
||||
],
|
||||
any_order=True,
|
||||
)
|
||||
|
||||
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},
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
assert api.nb_active_device_for_boiler == 1
|
||||
assert boiler_binary_sensor.state == STATE_ON
|
||||
|
||||
# 2. stop a heater
|
||||
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:
|
||||
await switch1.async_turn_off()
|
||||
switch1.async_write_ha_state()
|
||||
# Wait for state event propagation
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
assert entity.hvac_action == HVACAction.IDLE
|
||||
|
||||
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
|
||||
|
||||
entity.remove_thermostat()
|
||||
|
||||
|
||||
async def test_update_central_boiler_state_multiple(
|
||||
hass: HomeAssistant,
|
||||
# skip_hass_states_is_state,
|
||||
init_central_config_with_boiler_fixture,
|
||||
):
|
||||
"""Test that the central boiler state behavoir"""
|
||||
|
||||
api = VersatileThermostatAPI.get_vtherm_api(hass)
|
||||
|
||||
switch1 = MockSwitch(hass, "switch1", "theSwitch1")
|
||||
switch2 = MockSwitch(hass, "switch2", "theSwitch2")
|
||||
switch3 = MockSwitch(hass, "switch3", "theSwitch3")
|
||||
switch4 = MockSwitch(hass, "switch4", "theSwitch4")
|
||||
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
title="TheOverSwitchMockName",
|
||||
unique_id="uniqueId",
|
||||
data={
|
||||
CONF_NAME: "TheOverSwitchMockName",
|
||||
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_SWITCH,
|
||||
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
|
||||
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
|
||||
CONF_CYCLE_MIN: 5,
|
||||
CONF_TEMP_MIN: 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_HEATER: switch1.entity_id,
|
||||
CONF_HEATER_2: switch2.entity_id,
|
||||
CONF_HEATER_3: switch3.entity_id,
|
||||
CONF_HEATER_4: 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_USE_MAIN_CENTRAL_CONFIG: True,
|
||||
CONF_USE_TPI_CENTRAL_CONFIG: True,
|
||||
CONF_USE_PRESETS_CENTRAL_CONFIG: True,
|
||||
CONF_USE_ADVANCED_CENTRAL_CONFIG: True,
|
||||
CONF_USED_BY_CENTRAL_BOILER: True,
|
||||
},
|
||||
)
|
||||
|
||||
entity: ThermostatOverSwitch = await create_thermostat(
|
||||
hass, entry, "climate.theoverswitchmockname"
|
||||
)
|
||||
assert entity
|
||||
assert entity.name == "TheOverSwitchMockName"
|
||||
assert entity.is_over_switch
|
||||
assert entity.underlying_entities[0].entity_id == "switch.switch1"
|
||||
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 api.nb_active_device_for_boiler_threshold == 1
|
||||
assert api.nb_active_device_for_boiler == 0
|
||||
# Force the VTherm to heat
|
||||
await entity.async_set_hvac_mode(HVACMode.HEAT)
|
||||
await entity.async_set_preset_mode(PRESET_BOOST)
|
||||
|
||||
tz = get_tz(hass) # pylint: disable=invalid-name
|
||||
now: datetime = datetime.now(tz=tz)
|
||||
await send_temperature_change_event(entity, 10, now)
|
||||
|
||||
assert entity.hvac_mode == HVACMode.HEAT
|
||||
|
||||
# 0. set threshold to 3
|
||||
api.nb_active_device_for_boiler_threshold_entity.set_native_value(3)
|
||||
assert api.nb_active_device_for_boiler_threshold == 3
|
||||
|
||||
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 first heater
|
||||
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:
|
||||
await switch1.async_turn_on()
|
||||
switch1.async_write_ha_state()
|
||||
# Wait for state event propagation
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
assert entity.hvac_action == HVACAction.HEATING
|
||||
|
||||
assert mock_service_call.call_count == 1
|
||||
# No switch of the boiler
|
||||
mock_service_call.assert_has_calls(
|
||||
[
|
||||
call.service_call(
|
||||
"switch",
|
||||
"turn_on",
|
||||
{"entity_id": "switch.switch1"},
|
||||
),
|
||||
]
|
||||
)
|
||||
assert mock_send_event.call_count == 0
|
||||
|
||||
assert api.nb_active_device_for_boiler == 1
|
||||
assert boiler_binary_sensor.state == STATE_OFF
|
||||
|
||||
# 2. start a 2nd heater
|
||||
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:
|
||||
await switch2.async_turn_on()
|
||||
switch2.async_write_ha_state()
|
||||
# Wait for state event propagation
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
assert entity.hvac_action == HVACAction.HEATING
|
||||
|
||||
# Only the first heater is started by the algo
|
||||
assert mock_service_call.call_count == 1
|
||||
# No switch of the boiler. Caution: each time a underlying heater state change itself,
|
||||
# the cycle restarts. So it is always the first heater that is started
|
||||
mock_service_call.assert_has_calls(
|
||||
[
|
||||
call.service_call(
|
||||
"switch",
|
||||
"turn_on",
|
||||
{"entity_id": "switch.switch1"},
|
||||
),
|
||||
]
|
||||
)
|
||||
assert mock_send_event.call_count == 0
|
||||
|
||||
assert api.nb_active_device_for_boiler == 2
|
||||
assert boiler_binary_sensor.state == STATE_OFF
|
||||
|
||||
# 3. start a 3rd heater
|
||||
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:
|
||||
await switch3.async_turn_on()
|
||||
switch3.async_write_ha_state()
|
||||
# Wait for state event propagation
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
assert entity.hvac_action == HVACAction.HEATING
|
||||
|
||||
# Only the first heater is started by the algo
|
||||
assert mock_service_call.call_count == 2
|
||||
# No switch of the boiler. Caution: each time a underlying heater state change itself,
|
||||
# the cycle restarts. So it is always the first heater that is started
|
||||
mock_service_call.assert_has_calls(
|
||||
[
|
||||
call.service_call(
|
||||
"switch",
|
||||
"turn_on",
|
||||
service_data={},
|
||||
target={"entity_id": "switch.pompe_chaudiere"},
|
||||
),
|
||||
call.service_call(
|
||||
"switch",
|
||||
"turn_on",
|
||||
{"entity_id": "switch.switch1"},
|
||||
),
|
||||
],
|
||||
any_order=True,
|
||||
)
|
||||
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},
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
assert api.nb_active_device_for_boiler == 3
|
||||
assert boiler_binary_sensor.state == STATE_ON
|
||||
|
||||
# 4. start a 4th heater
|
||||
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:
|
||||
await switch4.async_turn_on()
|
||||
switch4.async_write_ha_state()
|
||||
# Wait for state event propagation
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
assert entity.hvac_action == HVACAction.HEATING
|
||||
|
||||
# Only the first heater is started by the algo
|
||||
assert mock_service_call.call_count == 1
|
||||
# No switch of the boiler. Caution: each time a underlying heater state change itself,
|
||||
# the cycle restarts. So it is always the first heater that is started
|
||||
mock_service_call.assert_has_calls(
|
||||
[
|
||||
call.service_call(
|
||||
"switch",
|
||||
"turn_on",
|
||||
{"entity_id": "switch.switch1"},
|
||||
),
|
||||
]
|
||||
)
|
||||
assert mock_send_event.call_count == 0
|
||||
assert api.nb_active_device_for_boiler == 4
|
||||
assert boiler_binary_sensor.state == STATE_ON
|
||||
|
||||
# 5. stop a heater
|
||||
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:
|
||||
await switch1.async_turn_off()
|
||||
switch1.async_write_ha_state()
|
||||
# Wait for state event propagation
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
assert entity.hvac_action == HVACAction.HEATING
|
||||
|
||||
assert mock_service_call.call_count == 0
|
||||
assert mock_send_event.call_count == 0
|
||||
assert api.nb_active_device_for_boiler == 3
|
||||
assert boiler_binary_sensor.state == STATE_ON
|
||||
|
||||
# 6. stop a 2nd heater
|
||||
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:
|
||||
await switch4.async_turn_off()
|
||||
switch4.async_write_ha_state()
|
||||
# Wait for state event propagation
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
assert entity.hvac_action == HVACAction.HEATING
|
||||
|
||||
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 == 2
|
||||
assert boiler_binary_sensor.state == STATE_OFF
|
||||
|
||||
entity.remove_thermostat()
|
||||
|
||||
|
||||
async def test_update_central_boiler_state_simple_valve(
|
||||
hass: HomeAssistant,
|
||||
# skip_hass_states_is_state,
|
||||
init_central_config_with_boiler_fixture,
|
||||
):
|
||||
"""Test that the central boiler state behavoir"""
|
||||
|
||||
api = VersatileThermostatAPI.get_vtherm_api(hass)
|
||||
|
||||
valve1 = MockNumber(hass, "valve1", "theValve1")
|
||||
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
title="TheOverValveMockName",
|
||||
unique_id="uniqueId",
|
||||
data={
|
||||
CONF_NAME: "TheOverValveMockName",
|
||||
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_VALVE,
|
||||
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_VALVE: 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_USE_MAIN_CENTRAL_CONFIG: True,
|
||||
CONF_USE_TPI_CENTRAL_CONFIG: True,
|
||||
CONF_USE_PRESETS_CENTRAL_CONFIG: True,
|
||||
CONF_USE_ADVANCED_CENTRAL_CONFIG: True,
|
||||
CONF_USED_BY_CENTRAL_BOILER: True,
|
||||
},
|
||||
)
|
||||
|
||||
entity: ThermostatOverValve = await create_thermostat(
|
||||
hass, entry, "climate.theovervalvemockname"
|
||||
)
|
||||
assert entity
|
||||
assert entity.name == "TheOverValveMockName"
|
||||
assert entity.is_over_valve
|
||||
assert entity.underlying_entities[0].entity_id == "number.valve1"
|
||||
|
||||
assert api.nb_active_device_for_boiler_threshold == 1
|
||||
assert api.nb_active_device_for_boiler == 0
|
||||
|
||||
# Force the VTherm to heat
|
||||
await entity.async_set_hvac_mode(HVACMode.HEAT)
|
||||
await entity.async_set_preset_mode(PRESET_BOOST)
|
||||
|
||||
tz = get_tz(hass) # pylint: disable=invalid-name
|
||||
now: datetime = datetime.now(tz=tz)
|
||||
|
||||
assert entity.hvac_mode == HVACMode.HEAT
|
||||
|
||||
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 valve
|
||||
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:
|
||||
await send_temperature_change_event(entity, 10, now)
|
||||
# we have to simulate the valve also else the test don't work
|
||||
valve1.set_native_value(10)
|
||||
valve1.async_write_ha_state()
|
||||
# Wait for state event propagation
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
assert entity.hvac_action == HVACAction.HEATING
|
||||
|
||||
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},
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
assert api.nb_active_device_for_boiler == 1
|
||||
assert boiler_binary_sensor.state == STATE_ON
|
||||
|
||||
# 2. stop a heater
|
||||
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:
|
||||
await send_temperature_change_event(entity, 25, now)
|
||||
# Change the valve value to 0
|
||||
valve1.set_native_value(0)
|
||||
valve1.async_write_ha_state()
|
||||
# Wait for state event propagation
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
assert entity.hvac_action == HVACAction.IDLE
|
||||
|
||||
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
|
||||
|
||||
entity.remove_thermostat()
|
||||
|
||||
|
||||
async def test_update_central_boiler_state_simple_climate(
|
||||
hass: HomeAssistant,
|
||||
# skip_hass_states_is_state,
|
||||
init_central_config_with_boiler_fixture,
|
||||
):
|
||||
"""Test that the central boiler state behavoir"""
|
||||
|
||||
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_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_USE_MAIN_CENTRAL_CONFIG: True,
|
||||
CONF_USE_PRESETS_CENTRAL_CONFIG: True,
|
||||
CONF_USE_ADVANCED_CENTRAL_CONFIG: True,
|
||||
CONF_USED_BY_CENTRAL_BOILER: True,
|
||||
},
|
||||
)
|
||||
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
|
||||
return_value=climate1,
|
||||
):
|
||||
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
|
||||
|
||||
# Force the VTherm to heat
|
||||
await entity.async_set_hvac_mode(HVACMode.HEAT)
|
||||
await entity.async_set_preset_mode(PRESET_BOOST)
|
||||
|
||||
tz = get_tz(hass) # pylint: disable=invalid-name
|
||||
now: datetime = datetime.now(tz=tz)
|
||||
|
||||
assert entity.hvac_mode == HVACMode.HEAT
|
||||
|
||||
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
|
||||
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:
|
||||
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()
|
||||
# Wait for state event propagation
|
||||
await asyncio.sleep(0.5)
|
||||
|
||||
assert entity.hvac_action == HVACAction.HEATING
|
||||
|
||||
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},
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
assert api.nb_active_device_for_boiler == 1
|
||||
assert boiler_binary_sensor.state == STATE_ON
|
||||
|
||||
# 2. stop a climate
|
||||
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:
|
||||
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()
|
||||
# Wait for state event propagation
|
||||
await asyncio.sleep(0.5)
|
||||
|
||||
assert entity.hvac_action == HVACAction.IDLE
|
||||
|
||||
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
|
||||
|
||||
entity.remove_thermostat()
|
||||
|
||||
|
||||
async def test_bug_339(
|
||||
hass: HomeAssistant,
|
||||
# skip_hass_states_is_state,
|
||||
init_central_config_with_boiler_fixture,
|
||||
):
|
||||
"""Test that the counter of active Vtherm in central boiler is
|
||||
correctly updated with underlying is in auto and device is active
|
||||
"""
|
||||
|
||||
api = VersatileThermostatAPI.get_vtherm_api(hass)
|
||||
|
||||
climate1 = MockClimate(
|
||||
hass=hass,
|
||||
unique_id="climate1",
|
||||
name="theClimate1",
|
||||
hvac_mode=HVACMode.AUTO,
|
||||
hvac_modes=[HVACMode.AUTO, HVACMode.OFF, HVACMode.HEAT, HVACMode.COOL],
|
||||
hvac_action=HVACAction.HEATING,
|
||||
)
|
||||
|
||||
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_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_USE_MAIN_CENTRAL_CONFIG: True,
|
||||
CONF_USE_PRESETS_CENTRAL_CONFIG: True,
|
||||
CONF_USE_ADVANCED_CENTRAL_CONFIG: True,
|
||||
CONF_USED_BY_CENTRAL_BOILER: True,
|
||||
},
|
||||
)
|
||||
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
|
||||
return_value=climate1,
|
||||
):
|
||||
entity: ThermostatOverValve = 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
|
||||
|
||||
await entity.async_set_hvac_mode(HVACMode.AUTO)
|
||||
# Simulate a state change in underelying
|
||||
await api.nb_active_device_for_boiler_entity.calculate_nb_active_devices(None)
|
||||
|
||||
# The VTherm should be active
|
||||
assert entity.underlying_entity(0).is_device_active is True
|
||||
assert entity.is_device_active is True
|
||||
assert api.nb_active_device_for_boiler == 1
|
||||
|
||||
entity.remove_thermostat()
|
||||
517
tests/test_central_config.py
Normal file
@@ -0,0 +1,517 @@
|
||||
# pylint: disable=wildcard-import, unused-wildcard-import, protected-access, unused-argument, line-too-long
|
||||
|
||||
""" Test the central_configuration """
|
||||
from unittest.mock import patch # , call
|
||||
|
||||
# from datetime import datetime # , timedelta
|
||||
from homeassistant import data_entry_flow
|
||||
from homeassistant.data_entry_flow import FlowResultType
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from homeassistant.config_entries import SOURCE_USER
|
||||
from pytest_homeassistant_custom_component.common import MockConfigEntry
|
||||
|
||||
from custom_components.versatile_thermostat.thermostat_switch import (
|
||||
ThermostatOverSwitch,
|
||||
)
|
||||
|
||||
from custom_components.versatile_thermostat.vtherm_api import VersatileThermostatAPI
|
||||
|
||||
from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import
|
||||
from .const import * # pylint: disable=wildcard-import, unused-wildcard-import
|
||||
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_add_a_central_config(hass: HomeAssistant, skip_hass_states_is_state):
|
||||
"""Tests the clean_central_config_doubon of base_thermostat"""
|
||||
central_config_entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
title="TheCentralConfigMockName",
|
||||
unique_id="centralConfigUniqueId",
|
||||
data={
|
||||
CONF_NAME: CENTRAL_CONFIG_NAME,
|
||||
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_CENTRAL_CONFIG,
|
||||
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_central_ext_temp_sensor",
|
||||
CONF_TEMP_MIN: 15,
|
||||
CONF_TEMP_MAX: 30,
|
||||
CONF_TPI_COEF_INT: 0.5,
|
||||
CONF_TPI_COEF_EXT: 0.02,
|
||||
"frost_temp": 10,
|
||||
"eco_temp": 17.1,
|
||||
"comfort_temp": 18.1,
|
||||
"boost_temp": 19.1,
|
||||
"eco_ac_temp": 25.1,
|
||||
"comfort_ac_temp": 23.1,
|
||||
"boost_ac_temp": 21.1,
|
||||
"frost_away_temp": 15.1,
|
||||
"eco_away_temp": 15.2,
|
||||
"comfort_away_temp": 15.3,
|
||||
"boost_away_temp": 15.4,
|
||||
"eco_ac_away_temp": 30.5,
|
||||
"comfort_ac_away_temp": 30.6,
|
||||
"boost_ac_away_temp": 30.7,
|
||||
CONF_WINDOW_DELAY: 15,
|
||||
CONF_WINDOW_AUTO_OPEN_THRESHOLD: 4,
|
||||
CONF_WINDOW_AUTO_CLOSE_THRESHOLD: 1,
|
||||
CONF_WINDOW_AUTO_MAX_DURATION: 31,
|
||||
CONF_MOTION_DELAY: 31,
|
||||
CONF_MOTION_OFF_DELAY: 301,
|
||||
CONF_MOTION_PRESET: "boost",
|
||||
CONF_NO_MOTION_PRESET: "frost",
|
||||
CONF_POWER_SENSOR: "sensor.mock_central_power_sensor",
|
||||
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_USE_CENTRAL_BOILER_FEATURE: False,
|
||||
},
|
||||
)
|
||||
|
||||
entity = await create_thermostat(
|
||||
hass, central_config_entry, "climate.thecentralconfigmockname"
|
||||
)
|
||||
# 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: ThermostatOverClimate = search_entity(
|
||||
# hass, "climate.thecentralconfigmockname", "climate"
|
||||
# )
|
||||
|
||||
assert entity is None
|
||||
|
||||
assert count_entities(hass, "climate.thecentralconfigmockname", "climate") == 0
|
||||
|
||||
# Test that VTherm API find the CentralConfig
|
||||
api = VersatileThermostatAPI.get_vtherm_api(hass)
|
||||
central_configuration = api.find_central_configuration()
|
||||
assert central_configuration is not None
|
||||
|
||||
# Test that VTherm API doesn't have any central boiler entities
|
||||
assert api.nb_active_device_for_boiler_entity is None
|
||||
assert api.nb_active_device_for_boiler is None
|
||||
|
||||
assert api.nb_active_device_for_boiler_threshold_entity is None
|
||||
assert api.nb_active_device_for_boiler_threshold is None
|
||||
|
||||
|
||||
# @pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_minimal_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"""
|
||||
# Add a Switch VTherm
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
title="TheOverSwitchMockName",
|
||||
unique_id="uniqueId",
|
||||
data={
|
||||
CONF_NAME: "TheOverSwitchMockName",
|
||||
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_SWITCH,
|
||||
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
|
||||
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
|
||||
CONF_CYCLE_MIN: 5,
|
||||
CONF_TEMP_MIN: 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: False,
|
||||
CONF_USE_MOTION_FEATURE: False,
|
||||
CONF_USE_POWER_FEATURE: False,
|
||||
CONF_USE_PRESENCE_FEATURE: False,
|
||||
CONF_HEATER: "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_SECURITY_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
|
||||
CONF_INVERSE_SWITCH: True,
|
||||
# CONF_USE_MAIN_CENTRAL_CONFIG: False,
|
||||
# CONF_USE_TPI_CENTRAL_CONFIG: False,
|
||||
# CONF_USE_WINDOW_CENTRAL_CONFIG: False,
|
||||
# CONF_USE_MOTION_CENTRAL_CONFIG: False,
|
||||
# CONF_USE_POWER_CENTRAL_CONFIG: False,
|
||||
# CONF_USE_PRESENCE_CENTRAL_CONFIG: False,
|
||||
# CONF_USE_PRESETS_CENTRAL_CONFIG: False,
|
||||
# CONF_USE_ADVANCED_CENTRAL_CONFIG: False,
|
||||
},
|
||||
)
|
||||
|
||||
with patch("homeassistant.core.ServiceRegistry.async_call"):
|
||||
entity: ThermostatOverSwitch = await create_thermostat(
|
||||
hass, entry, "climate.theoverswitchmockname"
|
||||
)
|
||||
assert entity
|
||||
assert entity.name == "TheOverSwitchMockName"
|
||||
assert entity.is_over_switch
|
||||
assert entity._temp_sensor_entity_id == "sensor.mock_temp_sensor"
|
||||
assert entity._ext_temp_sensor_entity_id == "sensor.mock_ext_temp_sensor"
|
||||
assert entity._cycle_min == 5
|
||||
assert entity.min_temp == 8
|
||||
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.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.is_inversed
|
||||
|
||||
entity.remove_thermostat()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
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"""
|
||||
# Add a Switch VTherm
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
title="TheOverSwitchMockName",
|
||||
unique_id="uniqueId",
|
||||
data={
|
||||
CONF_NAME: "TheOverSwitchMockName",
|
||||
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_SWITCH,
|
||||
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
|
||||
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
|
||||
CONF_CYCLE_MIN: 5,
|
||||
CONF_TEMP_MIN: 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_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_WINDOW_SENSOR: "binary_sensor.mock_window_sensor",
|
||||
CONF_WINDOW_DELAY: 30,
|
||||
CONF_WINDOW_AUTO_OPEN_THRESHOLD: 3,
|
||||
CONF_WINDOW_AUTO_CLOSE_THRESHOLD: 0.1,
|
||||
CONF_WINDOW_AUTO_MAX_DURATION: 5,
|
||||
CONF_MOTION_DELAY: 10,
|
||||
CONF_MOTION_OFF_DELAY: 29,
|
||||
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,
|
||||
CONF_USE_WINDOW_CENTRAL_CONFIG: False,
|
||||
CONF_USE_MOTION_CENTRAL_CONFIG: False,
|
||||
CONF_USE_POWER_CENTRAL_CONFIG: False,
|
||||
CONF_USE_PRESENCE_CENTRAL_CONFIG: False,
|
||||
CONF_USE_PRESETS_CENTRAL_CONFIG: False,
|
||||
CONF_USE_ADVANCED_CENTRAL_CONFIG: False,
|
||||
},
|
||||
)
|
||||
|
||||
with patch("homeassistant.core.ServiceRegistry.async_call"):
|
||||
entity: ThermostatOverSwitch = await create_thermostat(
|
||||
hass, entry, "climate.theoverswitchmockname"
|
||||
)
|
||||
assert entity
|
||||
assert entity.name == "TheOverSwitchMockName"
|
||||
assert entity.is_over_switch
|
||||
assert entity._temp_sensor_entity_id == "sensor.mock_temp_sensor"
|
||||
assert entity._ext_temp_sensor_entity_id == "sensor.mock_ext_temp_sensor"
|
||||
assert entity._cycle_min == 5
|
||||
assert entity.min_temp == 8
|
||||
assert entity.max_temp == 18
|
||||
assert entity.target_temperature_step == 0.3
|
||||
assert entity.preset_modes == [
|
||||
"none",
|
||||
"frost",
|
||||
"eco",
|
||||
"comfort",
|
||||
"boost",
|
||||
"activity",
|
||||
]
|
||||
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.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._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._power_sensor_entity_id == "sensor.mock_power_sensor"
|
||||
assert entity._max_power_sensor_entity_id == "sensor.mock_max_power_sensor"
|
||||
|
||||
assert entity._presence_sensor_entity_id == "binary_sensor.mock_presence_sensor"
|
||||
|
||||
entity.remove_thermostat()
|
||||
|
||||
|
||||
@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
|
||||
):
|
||||
"""Tests that a VTherm with central_configuration is working with the central_config attributes"""
|
||||
# Add a Switch VTherm
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
title="TheOverSwitchMockName",
|
||||
unique_id="uniqueId",
|
||||
data={
|
||||
CONF_NAME: "TheOverSwitchMockName",
|
||||
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_SWITCH,
|
||||
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
|
||||
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
|
||||
CONF_CYCLE_MIN: 5,
|
||||
CONF_TEMP_MIN: 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_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_WINDOW_SENSOR: "binary_sensor.mock_window_sensor",
|
||||
CONF_WINDOW_DELAY: 30,
|
||||
CONF_WINDOW_AUTO_OPEN_THRESHOLD: 3,
|
||||
CONF_WINDOW_AUTO_CLOSE_THRESHOLD: 0.1,
|
||||
CONF_WINDOW_AUTO_MAX_DURATION: 5,
|
||||
CONF_MOTION_DELAY: 10,
|
||||
CONF_MOTION_OFF_DELAY: 29,
|
||||
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,
|
||||
CONF_USE_WINDOW_CENTRAL_CONFIG: True,
|
||||
CONF_USE_MOTION_CENTRAL_CONFIG: True,
|
||||
CONF_USE_POWER_CENTRAL_CONFIG: True,
|
||||
CONF_USE_PRESENCE_CENTRAL_CONFIG: True,
|
||||
CONF_USE_PRESETS_CENTRAL_CONFIG: True,
|
||||
CONF_USE_ADVANCED_CENTRAL_CONFIG: True,
|
||||
},
|
||||
)
|
||||
|
||||
with patch("homeassistant.core.ServiceRegistry.async_call"):
|
||||
entity: ThermostatOverSwitch = await create_thermostat(
|
||||
hass, entry, "climate.theoverswitchmockname"
|
||||
)
|
||||
assert entity
|
||||
assert entity.name == "TheOverSwitchMockName"
|
||||
assert entity.is_over_switch
|
||||
assert entity._temp_sensor_entity_id == "sensor.mock_temp_sensor"
|
||||
assert entity._ext_temp_sensor_entity_id == "sensor.mock_ext_temp_sensor"
|
||||
assert entity._cycle_min == 5
|
||||
assert entity.min_temp == 15
|
||||
assert entity.max_temp == 30
|
||||
assert entity.target_temperature_step == 0.1
|
||||
assert entity.preset_modes == [
|
||||
"none",
|
||||
"frost",
|
||||
"eco",
|
||||
"boost",
|
||||
"activity",
|
||||
]
|
||||
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.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.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._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._power_sensor_entity_id == "sensor.mock_power_sensor"
|
||||
assert entity._max_power_sensor_entity_id == "sensor.mock_max_power_sensor"
|
||||
|
||||
assert entity._presence_sensor_entity_id == "binary_sensor.mock_presence_sensor"
|
||||
|
||||
entity.remove_thermostat()
|
||||
|
||||
|
||||
# @pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
# @pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_over_switch_with_central_config_but_no_central_config(
|
||||
hass: HomeAssistant, skip_hass_states_get, init_vtherm_api
|
||||
):
|
||||
"""Tests that a VTherm with a central_configuration flag but no central config. Should lead to an error"""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["step_id"] == SOURCE_USER
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={
|
||||
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_SWITCH,
|
||||
},
|
||||
)
|
||||
|
||||
assert result["type"] == FlowResultType.MENU
|
||||
assert result["step_id"] == "menu"
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input={"next_step_id": "main"}
|
||||
)
|
||||
|
||||
assert result["type"] == FlowResultType.FORM
|
||||
assert result["step_id"] == "main"
|
||||
assert result["errors"] == {}
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={
|
||||
CONF_NAME: "TheOverSwitchMockName",
|
||||
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
|
||||
CONF_CYCLE_MIN: 5,
|
||||
CONF_DEVICE_POWER: 1,
|
||||
CONF_USE_MAIN_CENTRAL_CONFIG: True,
|
||||
},
|
||||
)
|
||||
|
||||
assert result["type"] == FlowResultType.FORM
|
||||
# in case of error we stays in main
|
||||
assert result["step_id"] == "main"
|
||||
assert result["errors"] == {"use_main_central_config": "no_central_config"}
|
||||
|
||||
|
||||
async def test_migration_of_central_config(
|
||||
hass: HomeAssistant,
|
||||
skip_hass_states_is_state,
|
||||
):
|
||||
"""Tests the clean_central_config_doubon of base_thermostat"""
|
||||
central_config_entry = MockConfigEntry(
|
||||
version=CONFIG_VERSION,
|
||||
# An old minor version
|
||||
minor_version=1,
|
||||
domain=DOMAIN,
|
||||
title="TheCentralConfigMockName",
|
||||
unique_id="centralConfigUniqueId",
|
||||
data={
|
||||
CONF_NAME: CENTRAL_CONFIG_NAME,
|
||||
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_CENTRAL_CONFIG,
|
||||
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
|
||||
CONF_TEMP_MIN: 15,
|
||||
CONF_TEMP_MAX: 30,
|
||||
CONF_STEP_TEMPERATURE: 0.1,
|
||||
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,
|
||||
# The old central_boiler parameter
|
||||
"add_central_boiler_control": True,
|
||||
CONF_CENTRAL_BOILER_ACTIVATION_SRV: "switch.pompe_chaudiere/switch.turn_on",
|
||||
CONF_CENTRAL_BOILER_DEACTIVATION_SRV: "switch.pompe_chaudiere/switch.turn_off",
|
||||
},
|
||||
)
|
||||
|
||||
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: ThermostatOverClimate = search_entity(
|
||||
hass, "climate.thecentralconfigmockname", "climate"
|
||||
)
|
||||
|
||||
assert entity is None
|
||||
|
||||
assert count_entities(hass, "climate.thecentralconfigmockname", "climate") == 0
|
||||
|
||||
# Test that VTherm API find the CentralConfig
|
||||
api = VersatileThermostatAPI.get_vtherm_api(hass)
|
||||
central_configuration = api.find_central_configuration()
|
||||
assert central_configuration is not None
|
||||
|
||||
assert central_config_entry.data.get(CONF_USE_CENTRAL_BOILER_FEATURE) is True
|
||||
|
||||
# Test that VTherm API have any central boiler entities
|
||||
# It should have been migrated and initialized
|
||||
assert api.nb_active_device_for_boiler_entity is not None
|
||||
assert api.nb_active_device_for_boiler == 0
|
||||
|
||||
assert api.nb_active_device_for_boiler_threshold_entity is not None
|
||||
assert api.nb_active_device_for_boiler_threshold == 1 # the default value is 1
|
||||
1058
tests/test_central_mode.py
Normal file
54
tests/test_ema.py
Normal file
@@ -0,0 +1,54 @@
|
||||
# pylint: disable=line-too-long
|
||||
""" Tests de EMA calculation"""
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from custom_components.versatile_thermostat.ema import ExponentialMovingAverage
|
||||
|
||||
from .commons import get_tz
|
||||
|
||||
|
||||
def test_ema_basics(hass: HomeAssistant):
|
||||
"""Test the EMA calculation with basic features"""
|
||||
|
||||
tz = get_tz(hass) # pylint: disable=invalid-name
|
||||
now: datetime = datetime.now(tz=tz)
|
||||
|
||||
the_ema = ExponentialMovingAverage(
|
||||
"test",
|
||||
# 5 minutes
|
||||
300,
|
||||
# Needed for time calculation
|
||||
get_tz(hass),
|
||||
1,
|
||||
)
|
||||
|
||||
assert the_ema
|
||||
|
||||
current_timestamp = now
|
||||
# First initialization
|
||||
assert the_ema.calculate_ema(20, current_timestamp) == 20
|
||||
|
||||
current_timestamp = current_timestamp + timedelta(minutes=1)
|
||||
# One minute later, same temperature. EMA temperature should not have change
|
||||
assert the_ema.calculate_ema(20, current_timestamp) == 20
|
||||
|
||||
# Too short measurement should be ignored
|
||||
assert the_ema.calculate_ema(2000, current_timestamp) == 20
|
||||
|
||||
current_timestamp = current_timestamp + timedelta(seconds=4)
|
||||
assert the_ema.calculate_ema(20, current_timestamp) == 20
|
||||
|
||||
# a new normal measurement 5 minutes later
|
||||
current_timestamp = current_timestamp + timedelta(minutes=5)
|
||||
ema = the_ema.calculate_ema(25, current_timestamp)
|
||||
assert ema > 20
|
||||
assert ema == 22.5
|
||||
|
||||
# a big change in a short time does have a limited effect
|
||||
current_timestamp = current_timestamp + timedelta(seconds=5)
|
||||
ema = the_ema.calculate_ema(30, current_timestamp)
|
||||
assert ema > 22.5
|
||||
assert ema < 23
|
||||
assert ema == 22.6
|
||||
@@ -1,15 +1,18 @@
|
||||
# pylint: disable=unused-argument, line-too-long, protected-access
|
||||
""" Test the Window management """
|
||||
import asyncio
|
||||
# import asyncio
|
||||
import logging
|
||||
from unittest.mock import patch, call
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from custom_components.versatile_thermostat.thermostat_switch import ThermostatOverSwitch
|
||||
from custom_components.versatile_thermostat.thermostat_switch import (
|
||||
ThermostatOverSwitch,
|
||||
)
|
||||
from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import
|
||||
|
||||
logging.getLogger().setLevel(logging.DEBUG)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_inverted_switch(hass: HomeAssistant, skip_hass_states_is_state):
|
||||
@@ -44,14 +47,14 @@ async def test_inverted_switch(hass: HomeAssistant, skip_hass_states_is_state):
|
||||
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
|
||||
CONF_INVERSE_SWITCH: True
|
||||
CONF_INVERSE_SWITCH: True,
|
||||
},
|
||||
)
|
||||
|
||||
with patch(
|
||||
"homeassistant.core.ServiceRegistry.async_call"
|
||||
) as mock_service_call, patch(
|
||||
"homeassistant.core.StateMachine.is_state", return_value=True # switch is On
|
||||
"homeassistant.core.StateMachine.is_state", return_value=True # switch is On
|
||||
):
|
||||
entity: ThermostatOverSwitch = await create_thermostat(
|
||||
hass, entry, "climate.theoverswitchmockname"
|
||||
@@ -80,7 +83,7 @@ async def test_inverted_switch(hass: HomeAssistant, skip_hass_states_is_state):
|
||||
), patch(
|
||||
"homeassistant.core.ServiceRegistry.async_call"
|
||||
) as mock_service_call, patch(
|
||||
"homeassistant.core.StateMachine.is_state", return_value=True # switch is Off
|
||||
"homeassistant.core.StateMachine.is_state", return_value=True # switch is Off
|
||||
):
|
||||
event_timestamp = now - timedelta(minutes=4)
|
||||
await send_temperature_change_event(entity, 19, event_timestamp)
|
||||
@@ -90,9 +93,13 @@ async def test_inverted_switch(hass: HomeAssistant, skip_hass_states_is_state):
|
||||
# not updated cause mocked assert entity.is_device_active is True
|
||||
|
||||
assert mock_service_call.call_count == 1
|
||||
mock_service_call.assert_has_calls([
|
||||
call.async_call('switch', SERVICE_TURN_OFF, {'entity_id': 'switch.mock_switch'}),
|
||||
])
|
||||
mock_service_call.assert_has_calls(
|
||||
[
|
||||
call.async_call(
|
||||
"switch", SERVICE_TURN_OFF, {"entity_id": "switch.mock_switch"}
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
# 2. Make the temperature up to deactivate the switch
|
||||
with patch(
|
||||
@@ -100,7 +107,8 @@ async def test_inverted_switch(hass: HomeAssistant, skip_hass_states_is_state):
|
||||
), patch(
|
||||
"homeassistant.core.ServiceRegistry.async_call"
|
||||
) as mock_service_call, patch(
|
||||
"homeassistant.core.StateMachine.is_state", return_value=False # switch is On -> it should turned off
|
||||
"homeassistant.core.StateMachine.is_state",
|
||||
return_value=False, # switch is On -> it should turned off
|
||||
):
|
||||
event_timestamp = now - timedelta(minutes=3)
|
||||
await send_temperature_change_event(entity, 25, event_timestamp)
|
||||
@@ -114,11 +122,13 @@ async def test_inverted_switch(hass: HomeAssistant, skip_hass_states_is_state):
|
||||
await entity._underlyings[0].turn_off()
|
||||
|
||||
assert mock_service_call.call_count == 1
|
||||
mock_service_call.assert_has_calls([
|
||||
call.async_call('switch', SERVICE_TURN_ON, {'entity_id': 'switch.mock_switch'}),
|
||||
])
|
||||
|
||||
|
||||
mock_service_call.assert_has_calls(
|
||||
[
|
||||
call.async_call(
|
||||
"switch", SERVICE_TURN_ON, {"entity_id": "switch.mock_switch"}
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
# Clean the entity
|
||||
entity.remove_thermostat()
|
||||
|
||||
136
tests/test_last_seen.py
Normal file
@@ -0,0 +1,136 @@
|
||||
# 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 datetime import timedelta, datetime
|
||||
import logging
|
||||
|
||||
from custom_components.versatile_thermostat.thermostat_switch import (
|
||||
ThermostatOverSwitch,
|
||||
)
|
||||
from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import
|
||||
|
||||
|
||||
logging.getLogger().setLevel(logging.DEBUG)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_last_seen_feature(hass: HomeAssistant, skip_hass_states_is_state):
|
||||
"""Test the last_ssen feature
|
||||
1. creates a thermostat and check that security is off
|
||||
2. activate security feature when date is expired
|
||||
3. change the last seen sensor
|
||||
4. check that security is off
|
||||
"""
|
||||
|
||||
tz = get_tz(hass) # pylint: disable=invalid-name
|
||||
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
title="TheOverSwitchMockName",
|
||||
unique_id="uniqueId",
|
||||
data={
|
||||
"name": "TheOverSwitchMockName",
|
||||
"thermostat_type": "thermostat_over_switch",
|
||||
"temperature_sensor_entity_id": "sensor.mock_temp_sensor",
|
||||
"last_seen_temperature_sensor_entity_id": "sensor.mock_last_seen_temp_sensor",
|
||||
"external_temperature_sensor_entity_id": "sensor.mock_ext_temp_sensor",
|
||||
"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",
|
||||
"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,
|
||||
},
|
||||
)
|
||||
|
||||
# 1. creates a thermostat and check that security is off
|
||||
now: datetime = datetime.now(tz=tz)
|
||||
entity: ThermostatOverSwitch = await create_thermostat(
|
||||
hass, entry, "climate.theoverswitchmockname"
|
||||
)
|
||||
assert entity
|
||||
|
||||
assert entity._security_state is False
|
||||
assert entity.preset_mode is not PRESET_SECURITY
|
||||
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
|
||||
assert (
|
||||
entity._last_ext_temperature_measure.astimezone(tz) - now
|
||||
).total_seconds() < 1
|
||||
|
||||
# set a preset
|
||||
assert entity.preset_mode is PRESET_NONE
|
||||
await entity.async_set_preset_mode(PRESET_COMFORT)
|
||||
assert entity.preset_mode is PRESET_COMFORT
|
||||
|
||||
# Turn On the thermostat
|
||||
assert entity.hvac_mode == HVACMode.OFF
|
||||
await entity.async_set_hvac_mode(HVACMode.HEAT)
|
||||
assert entity.hvac_mode == HVACMode.HEAT
|
||||
|
||||
# 2. activate security feature when date is expired
|
||||
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:
|
||||
event_timestamp = now - timedelta(minutes=6)
|
||||
|
||||
# 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 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.TEMPERATURE_EVENT,
|
||||
{
|
||||
"last_temperature_measure": event_timestamp.isoformat(),
|
||||
"last_ext_temperature_measure": entity._last_ext_temperature_measure.isoformat(),
|
||||
"current_temp": 15,
|
||||
"current_ext_temp": None,
|
||||
"target_temp": 18,
|
||||
},
|
||||
),
|
||||
call.send_event(
|
||||
EventType.SECURITY_EVENT,
|
||||
{
|
||||
"type": "start",
|
||||
"last_temperature_measure": event_timestamp.isoformat(),
|
||||
"last_ext_temperature_measure": entity._last_ext_temperature_measure.isoformat(),
|
||||
"current_temp": 15,
|
||||
"current_ext_temp": None,
|
||||
"target_temp": 18,
|
||||
},
|
||||
),
|
||||
],
|
||||
any_order=True,
|
||||
)
|
||||
|
||||
assert mock_heater_on.call_count == 1
|
||||
|
||||
# 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.preset_mode is PRESET_COMFORT
|
||||
assert entity._last_temperature_measure == event_timestamp
|
||||
@@ -83,7 +83,7 @@ async def test_movement_management_time_not_enough(
|
||||
await send_ext_temperature_change_event(entity, 10, event_timestamp)
|
||||
|
||||
await send_presence_change_event(entity, True, False, event_timestamp)
|
||||
assert entity.presence_state is "on"
|
||||
assert entity.presence_state == "on"
|
||||
|
||||
# starts detecting motion with time not enough
|
||||
with patch(
|
||||
@@ -97,7 +97,12 @@ async def test_movement_management_time_not_enough(
|
||||
return_value=False,
|
||||
), patch(
|
||||
"homeassistant.helpers.condition.state", return_value=False
|
||||
) as mock_condition:
|
||||
) as mock_condition, patch(
|
||||
"homeassistant.core.StateMachine.get",
|
||||
return_value=State(
|
||||
entity_id="binary_sensor.mock_motion_sensor", state=STATE_OFF
|
||||
),
|
||||
):
|
||||
event_timestamp = now - timedelta(minutes=4)
|
||||
try_condition = await send_motion_change_event(entity, True, False, event_timestamp)
|
||||
|
||||
@@ -109,8 +114,8 @@ async def test_movement_management_time_not_enough(
|
||||
# because no motion is detected yet
|
||||
assert entity.target_temperature == 18
|
||||
# state is not changed if time is not enough
|
||||
assert entity.motion_state is None
|
||||
assert entity.presence_state is "on"
|
||||
assert entity.motion_state is STATE_OFF
|
||||
assert entity.presence_state == STATE_ON
|
||||
|
||||
assert mock_send_event.call_count == 0
|
||||
# Change is not confirmed
|
||||
@@ -141,8 +146,8 @@ async def test_movement_management_time_not_enough(
|
||||
assert entity.preset_mode is PRESET_ACTIVITY
|
||||
# because motion is detected yet
|
||||
assert entity.target_temperature == 19
|
||||
assert entity.motion_state is "on"
|
||||
assert entity.presence_state is "on"
|
||||
assert entity.motion_state == STATE_ON
|
||||
assert entity.presence_state == STATE_ON
|
||||
|
||||
# stop detecting motion with off delay too low
|
||||
with patch(
|
||||
@@ -156,19 +161,24 @@ async def test_movement_management_time_not_enough(
|
||||
return_value=True,
|
||||
) as mock_device_active, patch(
|
||||
"homeassistant.helpers.condition.state", return_value=False
|
||||
) as mock_condition:
|
||||
) as mock_condition, patch(
|
||||
"homeassistant.core.StateMachine.get",
|
||||
return_value=State(
|
||||
entity_id="binary_sensor.mock_motion_sensor", state=STATE_OFF
|
||||
),
|
||||
):
|
||||
event_timestamp = now - timedelta(minutes=2)
|
||||
try_condition = await send_motion_change_event(entity, False, True, event_timestamp)
|
||||
|
||||
# Will return False -> we will stay to movement On
|
||||
# Will return False -> we will stay to movement On
|
||||
await try_condition(None)
|
||||
|
||||
assert entity.hvac_mode is HVACMode.HEAT
|
||||
assert entity.preset_mode is PRESET_ACTIVITY
|
||||
# because no motion is detected yet
|
||||
assert entity.target_temperature == 19
|
||||
assert entity.motion_state is "on"
|
||||
assert entity.presence_state is "on"
|
||||
assert entity.motion_state == STATE_ON
|
||||
assert entity.presence_state == STATE_ON
|
||||
|
||||
assert mock_send_event.call_count == 0
|
||||
# The heater must heat now
|
||||
@@ -192,15 +202,15 @@ async def test_movement_management_time_not_enough(
|
||||
event_timestamp = now - timedelta(minutes=1)
|
||||
try_condition = await send_motion_change_event(entity, False, True, event_timestamp)
|
||||
|
||||
# Will return True -> we will switch to movement Off
|
||||
# Will return True -> we will switch to movement Off
|
||||
await try_condition(None)
|
||||
|
||||
assert entity.hvac_mode is HVACMode.HEAT
|
||||
assert entity.preset_mode is PRESET_ACTIVITY
|
||||
# because no motion is detected yet
|
||||
assert entity.target_temperature == 18
|
||||
assert entity.motion_state is "off"
|
||||
assert entity.presence_state is "on"
|
||||
assert entity.motion_state == STATE_OFF
|
||||
assert entity.presence_state == STATE_ON
|
||||
|
||||
assert mock_send_event.call_count == 0
|
||||
# The heater must stop heating now
|
||||
@@ -214,7 +224,7 @@ async def test_movement_management_time_not_enough(
|
||||
async def test_movement_management_time_enough_and_presence(
|
||||
hass: HomeAssistant, skip_hass_states_is_state
|
||||
):
|
||||
"""Test the Presence management when time is not enough"""
|
||||
"""Test the Motion management when time is not enough"""
|
||||
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
@@ -280,7 +290,7 @@ async def test_movement_management_time_enough_and_presence(
|
||||
await send_ext_temperature_change_event(entity, 10, event_timestamp)
|
||||
|
||||
await send_presence_change_event(entity, True, False, event_timestamp)
|
||||
assert entity.presence_state is "on"
|
||||
assert entity.presence_state == "on"
|
||||
|
||||
# starts detecting motion
|
||||
with patch(
|
||||
@@ -302,8 +312,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 is "on"
|
||||
assert entity.presence_state is "on"
|
||||
assert entity.motion_state == "on"
|
||||
assert entity.presence_state == "on"
|
||||
|
||||
assert mock_send_event.call_count == 0
|
||||
# Change is confirmed. Heater should be started
|
||||
@@ -331,8 +341,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 "off"
|
||||
assert entity.presence_state is "on"
|
||||
assert entity.motion_state == "off"
|
||||
assert entity.presence_state == "on"
|
||||
|
||||
assert mock_send_event.call_count == 0
|
||||
assert mock_heater_on.call_count == 0
|
||||
@@ -412,7 +422,7 @@ async def test_movement_management_time_enoughand_not_presence(
|
||||
await send_ext_temperature_change_event(entity, 10, event_timestamp)
|
||||
|
||||
await send_presence_change_event(entity, False, True, event_timestamp)
|
||||
assert entity.presence_state is "off"
|
||||
assert entity.presence_state == "off"
|
||||
|
||||
# starts detecting motion
|
||||
with patch(
|
||||
@@ -434,8 +444,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 is "on"
|
||||
assert entity.presence_state is "off"
|
||||
assert entity.motion_state == "on"
|
||||
assert entity.presence_state == "off"
|
||||
|
||||
assert mock_send_event.call_count == 0
|
||||
# Change is confirmed. Heater should be started
|
||||
@@ -463,8 +473,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 is "off"
|
||||
assert entity.presence_state is "off"
|
||||
assert entity.motion_state == "off"
|
||||
assert entity.presence_state == "off"
|
||||
|
||||
assert mock_send_event.call_count == 0
|
||||
# 18.1 starts heating with a low on_percent
|
||||
@@ -479,7 +489,7 @@ async def test_movement_management_time_enoughand_not_presence(
|
||||
async def test_movement_management_with_stop_during_condition(
|
||||
hass: HomeAssistant, skip_hass_states_is_state
|
||||
):
|
||||
"""Test the Presence management when the movement sensor switch to off and then to on during the test condition"""
|
||||
"""Test the Motion management when the movement sensor switch to off and then to on during the test condition"""
|
||||
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
@@ -546,7 +556,7 @@ async def test_movement_management_with_stop_during_condition(
|
||||
await send_ext_temperature_change_event(entity, 10, event_timestamp)
|
||||
|
||||
await send_presence_change_event(entity, False, True, event_timestamp)
|
||||
assert entity.presence_state is "off"
|
||||
assert entity.presence_state == "off"
|
||||
|
||||
# starts detecting motion
|
||||
with patch(
|
||||
@@ -558,9 +568,13 @@ async def test_movement_management_with_stop_during_condition(
|
||||
) as mock_heater_off, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
|
||||
return_value=True,
|
||||
), patch("homeassistant.helpers.condition.state", return_value=True): # Not needed for this test
|
||||
), patch(
|
||||
"homeassistant.helpers.condition.state", return_value=True
|
||||
): # Not needed for this test
|
||||
event_timestamp = now - timedelta(minutes=5)
|
||||
try_condition1 = await send_motion_change_event(entity, True, False, event_timestamp)
|
||||
try_condition1 = await send_motion_change_event(
|
||||
entity, True, False, event_timestamp
|
||||
)
|
||||
|
||||
assert try_condition1 is not None
|
||||
|
||||
@@ -569,33 +583,155 @@ async def test_movement_management_with_stop_during_condition(
|
||||
# because motion is detected yet -> switch to Boost mode
|
||||
assert entity.target_temperature == 18
|
||||
assert entity.motion_state is None
|
||||
assert entity.presence_state is "off"
|
||||
assert entity.presence_state == "off"
|
||||
|
||||
# Send a stop detection
|
||||
event_timestamp = now - timedelta(minutes=4)
|
||||
try_condition = await send_motion_change_event(entity, False, True, event_timestamp)
|
||||
assert try_condition is None # The timer should not have been stopped
|
||||
try_condition = await send_motion_change_event(
|
||||
entity, False, True, event_timestamp
|
||||
)
|
||||
assert try_condition is None # The timer should not have been stopped
|
||||
|
||||
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 is "off"
|
||||
assert entity.presence_state == "off"
|
||||
|
||||
# Resend a start detection
|
||||
event_timestamp = now - timedelta(minutes=3)
|
||||
try_condition = await send_motion_change_event(entity, True, False, event_timestamp)
|
||||
assert try_condition is None # The timer should not have been restarted (we keep the first one)
|
||||
try_condition = await send_motion_change_event(
|
||||
entity, True, False, event_timestamp
|
||||
)
|
||||
assert (
|
||||
try_condition is None
|
||||
) # The timer should not have been restarted (we keep the first one)
|
||||
|
||||
assert entity.hvac_mode is HVACMode.HEAT
|
||||
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 is "off"
|
||||
assert entity.presence_state == "off"
|
||||
|
||||
await try_condition1(None)
|
||||
# We should have switch this time
|
||||
assert entity.target_temperature == 19 # Boost
|
||||
assert entity.motion_state is "on" # switch to movement on
|
||||
assert entity.presence_state is "off" # Non change
|
||||
assert entity.target_temperature == 19 # Boost
|
||||
assert entity.motion_state == "on" # switch to movement on
|
||||
assert entity.presence_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(
|
||||
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"""
|
||||
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
title="TheOverSwitchMockName",
|
||||
unique_id="uniqueId",
|
||||
data={
|
||||
CONF_NAME: "TheOverSwitchMockName",
|
||||
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_SWITCH,
|
||||
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
|
||||
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
|
||||
CONF_CYCLE_MIN: 5,
|
||||
CONF_TEMP_MIN: 15,
|
||||
CONF_TEMP_MAX: 30,
|
||||
"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: False,
|
||||
CONF_HEATER: "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_MOTION_SENSOR: "binary_sensor.mock_motion_sensor",
|
||||
CONF_MOTION_DELAY: 10,
|
||||
CONF_MOTION_OFF_DELAY: 30,
|
||||
CONF_MOTION_PRESET: "boost",
|
||||
CONF_NO_MOTION_PRESET: "comfort",
|
||||
},
|
||||
)
|
||||
|
||||
entity: BaseThermostat = await create_thermostat(
|
||||
hass, entry, "climate.theoverswitchmockname"
|
||||
)
|
||||
assert entity
|
||||
|
||||
tz = get_tz(hass) # pylint: disable=invalid-name
|
||||
now: datetime = datetime.now(tz=tz)
|
||||
|
||||
# 0. start heating, in boost mode. We block the control_heating to avoid running a cycle
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.async_control_heating"
|
||||
):
|
||||
await entity.async_set_hvac_mode(HVACMode.HEAT)
|
||||
await entity.async_set_preset_mode(PRESET_ACTIVITY)
|
||||
|
||||
assert entity.hvac_mode is HVACMode.HEAT
|
||||
assert entity.preset_mode is PRESET_ACTIVITY
|
||||
# because no motion is detected yet
|
||||
assert entity.target_temperature == 18
|
||||
assert entity.motion_state is None
|
||||
|
||||
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)
|
||||
|
||||
# 1. starts detecting motion but the sensor is off
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
|
||||
return_value=True,
|
||||
), patch("homeassistant.helpers.condition.state", return_value=False), patch(
|
||||
"homeassistant.core.StateMachine.get",
|
||||
return_value=State(
|
||||
entity_id="binary_sensor.mock_motion_sensor", state=STATE_OFF
|
||||
),
|
||||
):
|
||||
event_timestamp = now - timedelta(minutes=5)
|
||||
try_condition1 = await send_motion_change_event(
|
||||
entity, True, False, event_timestamp
|
||||
)
|
||||
|
||||
assert try_condition1 is not None
|
||||
|
||||
await try_condition1(None)
|
||||
|
||||
# because no motion is detected yet -> condition.state is False and sensor is not active
|
||||
assert entity.target_temperature == 18
|
||||
assert entity.motion_state is STATE_OFF
|
||||
|
||||
# 2. starts detecting motion but the sensor is on
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
|
||||
return_value=True,
|
||||
), patch("homeassistant.helpers.condition.state", return_value=False), patch(
|
||||
"homeassistant.core.StateMachine.get",
|
||||
return_value=State(
|
||||
entity_id="binary_sensor.mock_motion_sensor", state=STATE_ON
|
||||
),
|
||||
):
|
||||
event_timestamp = now - timedelta(minutes=5)
|
||||
try_condition1 = await send_motion_change_event(
|
||||
entity, True, False, event_timestamp
|
||||
)
|
||||
|
||||
assert try_condition1 is not None
|
||||
|
||||
await try_condition1(None)
|
||||
|
||||
# because no motion is detected yet -> condition.state is False and sensor is not active
|
||||
assert entity.target_temperature == 19
|
||||
assert entity.motion_state is STATE_ON
|
||||
|
||||
@@ -260,6 +260,7 @@ async def test_multiple_switchs(
|
||||
CONF_HEATER_2: "switch.mock_switch2",
|
||||
CONF_HEATER_3: "switch.mock_switch3",
|
||||
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,
|
||||
@@ -386,6 +387,7 @@ async def test_multiple_climates(
|
||||
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: 8,
|
||||
CONF_TEMP_MIN: 15,
|
||||
CONF_TEMP_MAX: 30,
|
||||
"eco_temp": 17,
|
||||
@@ -471,7 +473,7 @@ async def test_multiple_climates_underlying_changes(
|
||||
skip_hass_states_is_state,
|
||||
skip_send_event,
|
||||
): # pylint: disable=unused-argument
|
||||
"""Test that when multiple switch are configured the activation of one underlying
|
||||
"""Test that when multiple climate are configured the activation of one underlying
|
||||
climate activate the others"""
|
||||
|
||||
tz = get_tz(hass) # pylint: disable=invalid-name
|
||||
@@ -486,6 +488,7 @@ async def test_multiple_climates_underlying_changes(
|
||||
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: 8,
|
||||
CONF_TEMP_MIN: 15,
|
||||
CONF_TEMP_MAX: 30,
|
||||
"eco_temp": 17,
|
||||
@@ -539,11 +542,15 @@ async def test_multiple_climates_underlying_changes(
|
||||
assert entity.is_device_active is False # pylint: disable=protected-access
|
||||
|
||||
# Stop heating on one underlying climate
|
||||
# All underlying supposed to be aligned with the hvac_mode now
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.async_control_heating"
|
||||
), patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.set_hvac_mode"
|
||||
) as mock_underlying_set_hvac_mode:
|
||||
) as mock_underlying_set_hvac_mode, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.hvac_mode",
|
||||
HVACMode.HEAT,
|
||||
):
|
||||
# Wait 11 sec so that the event will not be discarded
|
||||
event_timestamp = now + timedelta(seconds=11)
|
||||
await send_climate_change_event(
|
||||
@@ -553,6 +560,7 @@ async def test_multiple_climates_underlying_changes(
|
||||
HVACAction.OFF,
|
||||
HVACAction.HEATING,
|
||||
event_timestamp,
|
||||
underlying_entity_id="switch.mock_climate3",
|
||||
)
|
||||
|
||||
# Should be call for all Switch
|
||||
@@ -575,6 +583,9 @@ async def test_multiple_climates_underlying_changes(
|
||||
# a function but a property
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.hvac_action",
|
||||
HVACAction.IDLE,
|
||||
), patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.hvac_mode",
|
||||
HVACMode.OFF,
|
||||
):
|
||||
# Wait 11 sec so that the event will not be discarded
|
||||
event_timestamp = now + timedelta(seconds=11)
|
||||
@@ -585,6 +596,7 @@ async def test_multiple_climates_underlying_changes(
|
||||
HVACAction.IDLE,
|
||||
HVACAction.OFF,
|
||||
event_timestamp,
|
||||
underlying_entity_id="switch.mock_climate3",
|
||||
)
|
||||
|
||||
# Should be call for all Switch
|
||||
@@ -599,6 +611,113 @@ async def test_multiple_climates_underlying_changes(
|
||||
assert entity.is_device_active is False # pylint: disable=protected-access
|
||||
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_multiple_climates_underlying_changes_not_aligned(
|
||||
hass: HomeAssistant,
|
||||
skip_hass_states_is_state,
|
||||
skip_send_event,
|
||||
): # pylint: disable=unused-argument
|
||||
"""Test that when multiple climate are configured the activation of one underlying
|
||||
climate don't activate the others if their havc_mode are not aligned"""
|
||||
|
||||
tz = get_tz(hass) # pylint: disable=invalid-name
|
||||
now: datetime = datetime.now(tz=tz)
|
||||
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
title="TheOver4ClimateMockName",
|
||||
unique_id="uniqueId",
|
||||
data={
|
||||
CONF_NAME: "TheOver4ClimateMockName",
|
||||
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: 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: False,
|
||||
CONF_USE_PRESENCE_FEATURE: False,
|
||||
CONF_CLIMATE: "switch.mock_climate1",
|
||||
CONF_CLIMATE_2: "switch.mock_climate2",
|
||||
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,
|
||||
},
|
||||
)
|
||||
|
||||
entity: BaseThermostat = await create_thermostat(
|
||||
hass, entry, "climate.theover4climatemockname"
|
||||
)
|
||||
assert entity
|
||||
assert entity.is_over_climate is True
|
||||
assert entity.nb_underlying_entities == 4
|
||||
|
||||
# start heating, in boost mode. We block the control_heating to avoid running a cycle
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.async_control_heating"
|
||||
), patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.set_hvac_mode"
|
||||
) as mock_underlying_set_hvac_mode:
|
||||
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.target_temperature == 19
|
||||
assert entity.window_state is STATE_OFF
|
||||
|
||||
event_timestamp = now - timedelta(minutes=4)
|
||||
await send_temperature_change_event(entity, 15, event_timestamp)
|
||||
|
||||
# Should be call for all Switch
|
||||
assert mock_underlying_set_hvac_mode.call_count == 4
|
||||
mock_underlying_set_hvac_mode.assert_has_calls(
|
||||
[
|
||||
call.set_hvac_mode(HVACMode.HEAT),
|
||||
]
|
||||
)
|
||||
|
||||
# Stop heating on one underlying climate
|
||||
# All underlying supposed to be aligned with the hvac_mode now
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.async_control_heating"
|
||||
), patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.set_hvac_mode"
|
||||
) as mock_underlying_set_hvac_mode, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.hvac_mode",
|
||||
HVACMode.COOL,
|
||||
):
|
||||
# Wait 11 sec so that the event will not be discarded
|
||||
event_timestamp = now + timedelta(seconds=11)
|
||||
await send_climate_change_event(
|
||||
entity,
|
||||
HVACMode.OFF,
|
||||
HVACMode.HEAT,
|
||||
HVACAction.OFF,
|
||||
HVACAction.HEATING,
|
||||
event_timestamp,
|
||||
underlying_entity_id="switch.mock_climate3",
|
||||
)
|
||||
|
||||
# Should be call for all Switch
|
||||
assert mock_underlying_set_hvac_mode.call_count == 0
|
||||
# mock_underlying_set_hvac_mode.assert_has_calls(
|
||||
# [
|
||||
# call.set_hvac_mode(HVACMode.OFF),
|
||||
# ]
|
||||
# )
|
||||
# No change
|
||||
assert entity.hvac_mode == HVACMode.HEAT
|
||||
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_multiple_switch_power_management(
|
||||
@@ -629,6 +748,7 @@ async def test_multiple_switch_power_management(
|
||||
CONF_HEATER_2: "switch.mock_switch2",
|
||||
CONF_HEATER_3: "switch.mock_switch3",
|
||||
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,
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
""" Test the OpenWindow algorithm """
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
from custom_components.versatile_thermostat.open_window_algorithm import WindowOpenDetectionAlgorithm
|
||||
from custom_components.versatile_thermostat.open_window_algorithm import (
|
||||
WindowOpenDetectionAlgorithm,
|
||||
)
|
||||
|
||||
from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import
|
||||
|
||||
@@ -13,24 +15,34 @@ async def test_open_window_algo(
|
||||
):
|
||||
"""Tests the Algo"""
|
||||
|
||||
the_algo = WindowOpenDetectionAlgorithm(1.0, 0.0)
|
||||
the_algo = WindowOpenDetectionAlgorithm(60.0, 0.0)
|
||||
assert the_algo.last_slope is None
|
||||
|
||||
tz = get_tz(hass) # pylint: disable=invalid-name
|
||||
now = datetime.now(tz)
|
||||
|
||||
event_timestamp = now - timedelta(minutes=5)
|
||||
event_timestamp = now - timedelta(minutes=10)
|
||||
last_slope = the_algo.add_temp_measurement(
|
||||
temperature=10, datetime_measure=event_timestamp
|
||||
)
|
||||
|
||||
# We need at least 2 measurement
|
||||
# We need at least 4 measurement
|
||||
assert last_slope is None
|
||||
assert the_algo.last_slope is None
|
||||
assert the_algo.is_window_close_detected() is False
|
||||
assert the_algo.is_window_open_detected() is False
|
||||
|
||||
event_timestamp = now - timedelta(minutes=4)
|
||||
event_timestamp = now - timedelta(minutes=9)
|
||||
last_slope = the_algo.add_temp_measurement(
|
||||
temperature=10, datetime_measure=event_timestamp
|
||||
)
|
||||
|
||||
event_timestamp = now - timedelta(minutes=8)
|
||||
last_slope = the_algo.add_temp_measurement(
|
||||
temperature=10, datetime_measure=event_timestamp
|
||||
)
|
||||
|
||||
event_timestamp = now - timedelta(minutes=7)
|
||||
last_slope = the_algo.add_temp_measurement(
|
||||
temperature=10, datetime_measure=event_timestamp
|
||||
)
|
||||
@@ -41,62 +53,62 @@ async def test_open_window_algo(
|
||||
assert the_algo.is_window_close_detected() is True
|
||||
assert the_algo.is_window_open_detected() is False
|
||||
|
||||
event_timestamp = now - timedelta(minutes=3)
|
||||
event_timestamp = now - timedelta(minutes=6)
|
||||
last_slope = the_algo.add_temp_measurement(
|
||||
temperature=9, datetime_measure=event_timestamp
|
||||
)
|
||||
|
||||
# A slope is calculated
|
||||
assert last_slope == -0.5
|
||||
assert the_algo.last_slope == -0.5
|
||||
assert last_slope == -48.0
|
||||
assert the_algo.last_slope == -48.0
|
||||
assert the_algo.is_window_close_detected() is False
|
||||
assert the_algo.is_window_open_detected() is False
|
||||
|
||||
# A new temperature with 2 degre less in one minute (value will be rejected)
|
||||
event_timestamp = now - timedelta(minutes=5)
|
||||
last_slope = the_algo.add_temp_measurement(
|
||||
temperature=7, datetime_measure=event_timestamp
|
||||
)
|
||||
|
||||
# A slope is calculated
|
||||
assert last_slope == (-48.0 * 0.2 - 120.0 * 0.8)
|
||||
assert the_algo.last_slope == -105.6
|
||||
assert the_algo.is_window_close_detected() is False
|
||||
assert the_algo.is_window_open_detected() is True
|
||||
|
||||
# A new temperature with 1 degre less
|
||||
event_timestamp = now - timedelta(minutes=4)
|
||||
last_slope = the_algo.add_temp_measurement(
|
||||
temperature=6, datetime_measure=event_timestamp
|
||||
)
|
||||
|
||||
# A slope is calculated
|
||||
assert last_slope == -105.6 * 0.2 - 60.0 * 0.8
|
||||
assert the_algo.last_slope == -69.12
|
||||
assert the_algo.is_window_close_detected() is False
|
||||
assert the_algo.is_window_open_detected() is True
|
||||
|
||||
# A new temperature with 0 degre less
|
||||
event_timestamp = now - timedelta(minutes=3)
|
||||
last_slope = the_algo.add_temp_measurement(
|
||||
temperature=6, datetime_measure=event_timestamp
|
||||
)
|
||||
|
||||
# A slope is calculated
|
||||
assert last_slope == round(-69.12 * 0.2 - 0.0 * 0.8, 2)
|
||||
assert the_algo.last_slope == -13.82
|
||||
assert the_algo.is_window_close_detected() is False
|
||||
assert the_algo.is_window_open_detected() is False
|
||||
|
||||
# A new temperature with 1 degre more
|
||||
event_timestamp = now - timedelta(minutes=2)
|
||||
last_slope = the_algo.add_temp_measurement(
|
||||
temperature=7, datetime_measure=event_timestamp
|
||||
)
|
||||
|
||||
# A slope is calculated
|
||||
assert last_slope == -0.5 / 2.0 - 2.0 / 2.0
|
||||
assert the_algo.last_slope == -1.25
|
||||
assert the_algo.is_window_close_detected() is False
|
||||
assert the_algo.is_window_open_detected() is True
|
||||
|
||||
# A new temperature with 1 degre less
|
||||
event_timestamp = now - timedelta(minutes=1)
|
||||
last_slope = the_algo.add_temp_measurement(
|
||||
temperature=6, datetime_measure=event_timestamp
|
||||
)
|
||||
|
||||
# A slope is calculated
|
||||
assert last_slope == -1.25 / 2 - 1.0 / 2.0
|
||||
assert the_algo.last_slope == -1.125
|
||||
assert the_algo.is_window_close_detected() is False
|
||||
assert the_algo.is_window_open_detected() is True
|
||||
|
||||
# A new temperature with 0 degre less
|
||||
event_timestamp = now - timedelta(minutes=0)
|
||||
last_slope = the_algo.add_temp_measurement(
|
||||
temperature=6, datetime_measure=event_timestamp
|
||||
)
|
||||
|
||||
# A slope is calculated
|
||||
assert last_slope == -1.125 / 2
|
||||
assert the_algo.last_slope == -1.125 / 2
|
||||
assert the_algo.is_window_close_detected() is False
|
||||
assert the_algo.is_window_open_detected() is False
|
||||
|
||||
# A new temperature with 1 degre more
|
||||
event_timestamp = now + timedelta(minutes=1)
|
||||
last_slope = the_algo.add_temp_measurement(
|
||||
temperature=7, datetime_measure=event_timestamp
|
||||
)
|
||||
|
||||
# A slope is calculated
|
||||
assert last_slope == -1.125 / 4 + 0.5
|
||||
assert the_algo.last_slope == 0.21875
|
||||
assert last_slope == round(-13.82 * 0.2 + 60.0 * 0.8, 2)
|
||||
assert the_algo.last_slope == 45.24
|
||||
assert the_algo.is_window_close_detected() is True
|
||||
assert the_algo.is_window_open_detected() is False
|
||||
|
||||
@@ -106,7 +118,7 @@ async def test_open_window_algo_wrong(
|
||||
skip_hass_states_is_state,
|
||||
):
|
||||
"""Tests the Algo with wrong date"""
|
||||
the_algo = WindowOpenDetectionAlgorithm(1.0, 0.0)
|
||||
the_algo = WindowOpenDetectionAlgorithm(60.0, 0.0)
|
||||
assert the_algo.last_slope is None
|
||||
|
||||
tz = get_tz(hass) # pylint: disable=invalid-name
|
||||
@@ -134,3 +146,95 @@ async def test_open_window_algo_wrong(
|
||||
assert the_algo.last_slope is None
|
||||
assert the_algo.is_window_close_detected() is False
|
||||
assert the_algo.is_window_open_detected() is False
|
||||
|
||||
|
||||
async def test_open_window_algo_fake_point(
|
||||
hass: HomeAssistant,
|
||||
skip_hass_states_is_state,
|
||||
):
|
||||
"""Tests the Algo with adding fake point"""
|
||||
|
||||
the_algo = WindowOpenDetectionAlgorithm(3.0, 0.1)
|
||||
assert the_algo.last_slope is None
|
||||
|
||||
tz = get_tz(hass) # pylint: disable=invalid-name
|
||||
now = datetime.now(tz)
|
||||
|
||||
event_timestamp = now
|
||||
last_slope = the_algo.check_age_last_measurement(
|
||||
temperature=10, datetime_now=event_timestamp
|
||||
)
|
||||
|
||||
# We need at least 4 measurement
|
||||
assert last_slope is None
|
||||
assert the_algo.last_slope is None
|
||||
assert the_algo.is_window_close_detected() is False
|
||||
assert the_algo.is_window_open_detected() is False
|
||||
|
||||
event_timestamp = now + timedelta(minutes=1)
|
||||
last_slope = the_algo.add_temp_measurement(
|
||||
temperature=10, datetime_measure=event_timestamp
|
||||
)
|
||||
|
||||
event_timestamp = now + timedelta(minutes=2)
|
||||
last_slope = the_algo.add_temp_measurement(
|
||||
temperature=10, datetime_measure=event_timestamp
|
||||
)
|
||||
|
||||
event_timestamp = now + timedelta(minutes=3)
|
||||
last_slope = the_algo.add_temp_measurement(
|
||||
temperature=10, datetime_measure=event_timestamp
|
||||
)
|
||||
|
||||
# No slope because same temperature
|
||||
assert last_slope == 0
|
||||
assert the_algo.last_slope == 0
|
||||
assert the_algo.is_window_close_detected() is False
|
||||
assert the_algo.is_window_open_detected() is False
|
||||
|
||||
event_timestamp = now + timedelta(minutes=4)
|
||||
last_slope = the_algo.add_temp_measurement(
|
||||
temperature=9, datetime_measure=event_timestamp
|
||||
)
|
||||
|
||||
# A slope is calculated
|
||||
assert last_slope == -48.0
|
||||
assert the_algo.last_slope == -48.0
|
||||
assert the_algo.is_window_close_detected() is False
|
||||
assert the_algo.is_window_open_detected() is True # One degre in one minute
|
||||
|
||||
# 1 Add a fake point one minute later
|
||||
event_timestamp = now + timedelta(minutes=5)
|
||||
last_slope = the_algo.check_age_last_measurement(
|
||||
temperature=8, datetime_now=event_timestamp
|
||||
)
|
||||
|
||||
# The slope not have change (fake point is ignored)
|
||||
assert last_slope == -48.0
|
||||
assert the_algo.last_slope == -48.0
|
||||
assert the_algo.is_window_close_detected() is False
|
||||
assert the_algo.is_window_open_detected() is True # One degre in one minute
|
||||
|
||||
# 2 Add a fake point 31 minute later -> +2 degres in 32 minutes
|
||||
event_timestamp = event_timestamp + timedelta(minutes=31)
|
||||
last_slope = the_algo.check_age_last_measurement(
|
||||
temperature=10, datetime_now=event_timestamp
|
||||
)
|
||||
|
||||
# The slope should have change (fake point is added)
|
||||
assert last_slope == -8.1
|
||||
assert the_algo.last_slope == -8.1
|
||||
assert the_algo.is_window_close_detected() is False
|
||||
assert the_algo.is_window_open_detected() is True
|
||||
|
||||
# 3 Add a 2nd fake point 30 minute later -> +3 degres in 30 minutes
|
||||
event_timestamp = event_timestamp + timedelta(minutes=31)
|
||||
last_slope = the_algo.check_age_last_measurement(
|
||||
temperature=13, datetime_now=event_timestamp
|
||||
)
|
||||
|
||||
# The slope should have change (fake point is added)
|
||||
assert last_slope == 0.67
|
||||
assert the_algo.last_slope == 0.67
|
||||
assert the_algo.is_window_close_detected() is True
|
||||
assert the_algo.is_window_open_detected() is False
|
||||
|
||||
633
tests/test_overclimate.py
Normal file
@@ -0,0 +1,633 @@
|
||||
# pylint: disable=wildcard-import, unused-wildcard-import, protected-access, unused-argument, line-too-long, too-many-lines
|
||||
|
||||
""" Test the Window management """
|
||||
from unittest.mock import patch, call
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
import logging
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.components.climate import (
|
||||
SERVICE_SET_TEMPERATURE,
|
||||
)
|
||||
|
||||
from custom_components.versatile_thermostat.thermostat_climate import (
|
||||
ThermostatOverClimate,
|
||||
)
|
||||
|
||||
from .commons import *
|
||||
|
||||
logging.getLogger().setLevel(logging.DEBUG)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_bug_56(
|
||||
hass: HomeAssistant,
|
||||
skip_hass_states_is_state,
|
||||
skip_turn_on_off_heater,
|
||||
skip_send_event,
|
||||
):
|
||||
"""Test that in over_climate mode there is no error when underlying climate is not available"""
|
||||
|
||||
the_mock_underlying = MagicMockClimate()
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
|
||||
return_value=None, # dont find the underlying climate
|
||||
):
|
||||
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: 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: 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,
|
||||
},
|
||||
)
|
||||
|
||||
entity: BaseThermostat = await create_thermostat(
|
||||
hass, entry, "climate.theoverclimatemockname"
|
||||
)
|
||||
assert entity
|
||||
# cause the underlying climate was not found
|
||||
assert entity.is_over_climate is True
|
||||
assert entity.underlying_entity(0)._underlying_climate is None
|
||||
|
||||
# Should not failed
|
||||
entity.update_custom_attributes()
|
||||
|
||||
# try to call async_control_heating
|
||||
try:
|
||||
ret = await entity.async_control_heating()
|
||||
# an exception should be send
|
||||
assert ret is False
|
||||
except Exception: # pylint: disable=broad-exception-caught
|
||||
assert False
|
||||
|
||||
# This time the underlying will be found
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
|
||||
return_value=the_mock_underlying, # dont find the underlying climate
|
||||
):
|
||||
# try to call async_control_heating
|
||||
try:
|
||||
await entity.async_control_heating()
|
||||
except UnknownEntity:
|
||||
assert False
|
||||
except Exception: # pylint: disable=broad-exception-caught
|
||||
assert False
|
||||
|
||||
# Should not failed
|
||||
entity.update_custom_attributes()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_bug_82(
|
||||
hass: HomeAssistant,
|
||||
skip_hass_states_is_state,
|
||||
skip_turn_on_off_heater,
|
||||
skip_send_event,
|
||||
):
|
||||
"""Test that when a underlying climate is not available the VTherm doesn't go into safety mode"""
|
||||
|
||||
tz = get_tz(hass) # pylint: disable=invalid-name
|
||||
now: datetime = datetime.now(tz=tz)
|
||||
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
title="TheOverClimateMockName",
|
||||
unique_id="uniqueId",
|
||||
data=PARTIAL_CLIMATE_CONFIG, # 5 minutes security delay
|
||||
)
|
||||
|
||||
fake_underlying_climate = MockUnavailableClimate(
|
||||
hass, "mockUniqueId", "MockClimateName", {}
|
||||
)
|
||||
|
||||
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",
|
||||
return_value=fake_underlying_climate,
|
||||
) as mock_find_climate:
|
||||
entity = await create_thermostat(hass, entry, "climate.theoverclimatemockname")
|
||||
|
||||
assert entity
|
||||
|
||||
assert entity.name == "TheOverClimateMockName"
|
||||
assert entity.is_over_climate is True
|
||||
# assert entity.hvac_action is HVACAction.OFF
|
||||
assert entity.hvac_mode is HVACMode.OFF
|
||||
# assert entity.hvac_mode is None
|
||||
assert entity.target_temperature == entity.min_temp
|
||||
assert entity.preset_modes == [
|
||||
PRESET_NONE,
|
||||
PRESET_FROST_PROTECTION,
|
||||
PRESET_ECO,
|
||||
PRESET_COMFORT,
|
||||
PRESET_BOOST,
|
||||
]
|
||||
assert entity.preset_mode is PRESET_NONE
|
||||
assert entity._security_state is False
|
||||
|
||||
# should have been called with EventType.PRESET_EVENT and EventType.HVAC_MODE_EVENT
|
||||
assert mock_send_event.call_count == 2
|
||||
mock_send_event.assert_has_calls(
|
||||
[
|
||||
call.send_event(EventType.PRESET_EVENT, {"preset": PRESET_NONE}),
|
||||
call.send_event(
|
||||
EventType.HVAC_MODE_EVENT,
|
||||
{"hvac_mode": HVACMode.OFF},
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
assert mock_find_climate.call_count == 1
|
||||
assert mock_find_climate.mock_calls[0] == call()
|
||||
mock_find_climate.assert_has_calls([call.find_underlying_entity()])
|
||||
|
||||
# Force safety mode
|
||||
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
|
||||
assert (
|
||||
entity._last_ext_temperature_measure.astimezone(tz) - now
|
||||
).total_seconds() < 1
|
||||
|
||||
# Tries to turns on the Thermostat
|
||||
await entity.async_set_hvac_mode(HVACMode.HEAT)
|
||||
assert entity.hvac_mode == HVACMode.HEAT
|
||||
|
||||
# 2. activate security feature when date is expired
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
|
||||
) as mock_send_event, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
|
||||
):
|
||||
event_timestamp = now - timedelta(minutes=6)
|
||||
|
||||
# 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)
|
||||
# Should stay False
|
||||
assert entity.security_state is False
|
||||
assert entity.preset_mode == "none"
|
||||
assert entity._saved_preset_mode == "none"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_bug_101(
|
||||
hass: HomeAssistant,
|
||||
skip_hass_states_is_state,
|
||||
skip_turn_on_off_heater,
|
||||
skip_send_event,
|
||||
):
|
||||
"""Test that when a underlying climate target temp is changed, the VTherm change its own temperature target and switch to manual"""
|
||||
|
||||
tz = get_tz(hass) # pylint: disable=invalid-name
|
||||
now: datetime = datetime.now(tz=tz)
|
||||
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
title="TheOverClimateMockName",
|
||||
unique_id="uniqueId",
|
||||
data=PARTIAL_CLIMATE_NOT_REGULATED_CONFIG, # 5 minutes security delay
|
||||
)
|
||||
|
||||
# Underlying is in HEAT mode but should be shutdown at startup
|
||||
fake_underlying_climate = MockClimate(
|
||||
hass, "mockUniqueId", "MockClimateName", {}, HVACMode.HEAT, HVACAction.HEATING
|
||||
)
|
||||
|
||||
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",
|
||||
return_value=fake_underlying_climate,
|
||||
) 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")
|
||||
|
||||
assert entity
|
||||
|
||||
assert entity.name == "TheOverClimateMockName"
|
||||
assert entity.is_over_climate is True
|
||||
assert entity.hvac_mode is HVACMode.OFF
|
||||
# because in MockClimate HVACAction is HEATING if hvac_mode is not set
|
||||
assert entity.hvac_action is HVACAction.HEATING
|
||||
# Underlying should have been shutdown
|
||||
assert mock_underlying_set_hvac_mode.call_count == 1
|
||||
mock_underlying_set_hvac_mode.assert_has_calls(
|
||||
[
|
||||
call.set_hvac_mode(HVACMode.OFF),
|
||||
]
|
||||
)
|
||||
|
||||
assert entity.target_temperature == entity.min_temp
|
||||
assert entity.preset_mode is PRESET_NONE
|
||||
|
||||
# should have been called with EventType.PRESET_EVENT and EventType.HVAC_MODE_EVENT
|
||||
assert mock_send_event.call_count == 2
|
||||
mock_send_event.assert_has_calls(
|
||||
[
|
||||
call.send_event(EventType.PRESET_EVENT, {"preset": PRESET_NONE}),
|
||||
call.send_event(
|
||||
EventType.HVAC_MODE_EVENT,
|
||||
{"hvac_mode": HVACMode.OFF},
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
assert mock_find_climate.call_count == 1
|
||||
assert mock_find_climate.mock_calls[0] == call()
|
||||
mock_find_climate.assert_has_calls([call.find_underlying_entity()])
|
||||
|
||||
# 1. Force preset mode
|
||||
await entity.async_set_hvac_mode(HVACMode.HEAT)
|
||||
assert entity.hvac_mode == HVACMode.HEAT
|
||||
await entity.async_set_preset_mode(PRESET_COMFORT)
|
||||
assert entity.preset_mode == PRESET_COMFORT
|
||||
|
||||
# 2. Change the target temp of underlying thermostat at now -> the event will be disgarded because to fast (to avoid loop cf issue 121)
|
||||
await send_climate_change_event_with_temperature(
|
||||
entity,
|
||||
HVACMode.HEAT,
|
||||
HVACMode.HEAT,
|
||||
HVACAction.OFF,
|
||||
HVACAction.OFF,
|
||||
now,
|
||||
entity.min_temp + 1,
|
||||
True,
|
||||
"climate.mock_climate", # the underlying climate entity id
|
||||
)
|
||||
# Should NOT have been switched to Manual preset
|
||||
assert entity.target_temperature == 17
|
||||
assert entity.preset_mode is PRESET_COMFORT
|
||||
|
||||
# 3. Change the target temp of underlying thermostat at 11 sec later -> the event will be taken
|
||||
# Wait 11 sec
|
||||
event_timestamp = now + timedelta(seconds=11)
|
||||
assert entity.is_regulated is False
|
||||
await send_climate_change_event_with_temperature(
|
||||
entity,
|
||||
HVACMode.HEAT,
|
||||
HVACMode.HEAT,
|
||||
HVACAction.OFF,
|
||||
HVACAction.OFF,
|
||||
event_timestamp,
|
||||
entity.min_temp + 1,
|
||||
True,
|
||||
"climate.mock_climate", # the underlying climate entity id
|
||||
)
|
||||
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
|
||||
# Wait 11 sec
|
||||
event_timestamp = now + timedelta(seconds=11)
|
||||
await send_climate_change_event_with_temperature(
|
||||
entity,
|
||||
HVACMode.HEAT,
|
||||
HVACMode.HEAT,
|
||||
HVACAction.OFF,
|
||||
HVACAction.OFF,
|
||||
event_timestamp,
|
||||
entity.min_temp + 1.5,
|
||||
True,
|
||||
"climate.mock_climate", # the underlying climate entity id
|
||||
)
|
||||
assert entity.target_temperature == entity.min_temp + 1
|
||||
assert entity.preset_mode is PRESET_NONE
|
||||
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_bug_508(
|
||||
hass: HomeAssistant,
|
||||
skip_hass_states_is_state,
|
||||
skip_turn_on_off_heater,
|
||||
skip_send_event,
|
||||
):
|
||||
"""Test that it not possible to set the target temperature under the min_temp setting"""
|
||||
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
title="TheOverClimateMockName",
|
||||
unique_id="uniqueId",
|
||||
# default value are min 15°, max 31°, step 0.1
|
||||
data=PARTIAL_CLIMATE_CONFIG, # 5 minutes security delay
|
||||
)
|
||||
|
||||
# Min_temp is 10 and max_temp is 31 and features contains TARGET_TEMPERATURE_RANGE
|
||||
fake_underlying_climate = MagicMockClimateWithTemperatureRange()
|
||||
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
|
||||
), patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
|
||||
return_value=fake_underlying_climate,
|
||||
), patch(
|
||||
"homeassistant.core.ServiceRegistry.async_call"
|
||||
) as mock_service_call:
|
||||
entity = await create_thermostat(hass, entry, "climate.theoverclimatemockname")
|
||||
|
||||
assert entity
|
||||
|
||||
assert entity.name == "TheOverClimateMockName"
|
||||
assert entity.is_over_climate is True
|
||||
assert entity.hvac_mode is HVACMode.OFF
|
||||
# The VTherm value and not the underlying value
|
||||
assert entity.target_temperature_step == 0.1
|
||||
assert entity.target_temperature == entity.min_temp
|
||||
assert entity.is_regulated is True
|
||||
|
||||
assert mock_service_call.call_count == 0
|
||||
|
||||
# Set the hvac_mode to HEAT
|
||||
await entity.async_set_hvac_mode(HVACMode.HEAT)
|
||||
|
||||
# Not In the accepted interval -> should be converted into 10 (the min) and send with target_temp_high and target_temp_low
|
||||
await entity.async_set_temperature(temperature=8.5)
|
||||
|
||||
# MagicMock climate is already HEAT by default. So there is no SET_HAVC_MODE call
|
||||
assert mock_service_call.call_count == 1
|
||||
mock_service_call.assert_has_calls(
|
||||
[
|
||||
call.async_call(
|
||||
"climate",
|
||||
SERVICE_SET_TEMPERATURE,
|
||||
{
|
||||
"entity_id": "climate.mock_climate",
|
||||
# "temperature": 17.5,
|
||||
"target_temp_high": 10,
|
||||
"target_temp_low": 10,
|
||||
"temperature": 10,
|
||||
},
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
with patch("homeassistant.core.ServiceRegistry.async_call") as mock_service_call:
|
||||
# Not In the accepted interval -> should be converted into 10 (the min) and send with target_temp_high and target_temp_low
|
||||
await entity.async_set_temperature(temperature=32)
|
||||
|
||||
# MagicMock climate is already HEAT by default. So there is no SET_HAVC_MODE call
|
||||
assert mock_service_call.call_count == 1
|
||||
mock_service_call.assert_has_calls(
|
||||
[
|
||||
call.async_call(
|
||||
"climate",
|
||||
SERVICE_SET_TEMPERATURE,
|
||||
{
|
||||
"entity_id": "climate.mock_climate",
|
||||
"target_temp_high": 31,
|
||||
"target_temp_low": 31,
|
||||
"temperature": 31,
|
||||
},
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_bug_524(hass: HomeAssistant, skip_hass_states_is_state):
|
||||
"""Test when switching from Cool to Heat the new temperature in Heat mode should be used"""
|
||||
|
||||
# vtherm_api: VersatileThermostatAPI = VersatileThermostatAPI.get_vtherm_api(hass)
|
||||
|
||||
# The temperatures to set
|
||||
temps = {
|
||||
"frost": 7.0,
|
||||
"eco": 17.0,
|
||||
"comfort": 19.0,
|
||||
"boost": 21.0,
|
||||
"eco_ac": 27.0,
|
||||
"comfort_ac": 25.0,
|
||||
"boost_ac": 23.0,
|
||||
"frost_away": 7.1,
|
||||
"eco_away": 17.1,
|
||||
"comfort_away": 19.1,
|
||||
"boost_away": 21.1,
|
||||
"eco_ac_away": 27.1,
|
||||
"comfort_ac_away": 25.1,
|
||||
"boost_ac_away": 23.1,
|
||||
}
|
||||
|
||||
config_entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
title="TheOverClimateMockName",
|
||||
unique_id="overClimateUniqueId",
|
||||
data={
|
||||
CONF_NAME: "overClimate",
|
||||
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
|
||||
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_CLIMATE,
|
||||
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
|
||||
CONF_CYCLE_MIN: 5,
|
||||
CONF_TEMP_MIN: 15,
|
||||
CONF_TEMP_MAX: 30,
|
||||
CONF_USE_WINDOW_FEATURE: False,
|
||||
CONF_USE_MOTION_FEATURE: False,
|
||||
CONF_USE_POWER_FEATURE: False,
|
||||
CONF_USE_PRESENCE_FEATURE: True,
|
||||
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_AUTO_FAN_MODE: CONF_AUTO_FAN_TURBO,
|
||||
CONF_AC_MODE: True,
|
||||
},
|
||||
# | temps,
|
||||
)
|
||||
|
||||
fake_underlying_climate = MockClimate(
|
||||
hass=hass,
|
||||
unique_id="mock_climate",
|
||||
name="mock_climate",
|
||||
hvac_modes=[HVACMode.OFF, HVACMode.COOL, HVACMode.HEAT, HVACMode.FAN_ONLY],
|
||||
)
|
||||
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
|
||||
return_value=fake_underlying_climate,
|
||||
):
|
||||
vtherm: ThermostatOverClimate = await create_thermostat(
|
||||
hass, config_entry, "climate.overclimate"
|
||||
)
|
||||
|
||||
assert vtherm is not None
|
||||
|
||||
# We search for NumberEntities
|
||||
for preset_name, value in temps.items():
|
||||
|
||||
await set_climate_preset_temp(vtherm, preset_name, value)
|
||||
|
||||
temp_entity: NumberEntity = search_entity(
|
||||
hass,
|
||||
"number.overclimate_preset_" + preset_name + PRESET_TEMP_SUFFIX,
|
||||
NUMBER_DOMAIN,
|
||||
)
|
||||
assert temp_entity
|
||||
# Because set_value is not implemented in Number class (really don't understand why...)
|
||||
assert temp_entity.state == value
|
||||
|
||||
# 1. Set mode to Heat and preset to Comfort
|
||||
await send_presence_change_event(vtherm, True, False, datetime.now())
|
||||
await vtherm.async_set_hvac_mode(HVACMode.HEAT)
|
||||
await vtherm.async_set_preset_mode(PRESET_COMFORT)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert vtherm.target_temperature == 19.0
|
||||
|
||||
# 2. Only change the HVAC_MODE (and keep preset to comfort)
|
||||
await vtherm.async_set_hvac_mode(HVACMode.COOL)
|
||||
await hass.async_block_till_done()
|
||||
assert vtherm.target_temperature == 25.0
|
||||
|
||||
# 3. Only change the HVAC_MODE (and keep preset to comfort)
|
||||
await vtherm.async_set_hvac_mode(HVACMode.HEAT)
|
||||
await hass.async_block_till_done()
|
||||
assert vtherm.target_temperature == 19.0
|
||||
|
||||
# 4. Change presence to off
|
||||
await send_presence_change_event(vtherm, False, True, datetime.now())
|
||||
await hass.async_block_till_done()
|
||||
assert vtherm.target_temperature == 19.1
|
||||
|
||||
# 5. Change hvac_mode to AC
|
||||
await vtherm.async_set_hvac_mode(HVACMode.COOL)
|
||||
await hass.async_block_till_done()
|
||||
assert vtherm.target_temperature == 25.1
|
||||
|
||||
# 6. Change presence to on
|
||||
await send_presence_change_event(vtherm, True, False, datetime.now())
|
||||
await hass.async_block_till_done()
|
||||
assert vtherm.target_temperature == 25
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_ignore_temp_outside_minmax_range(
|
||||
hass: HomeAssistant,
|
||||
skip_hass_states_is_state,
|
||||
skip_turn_on_off_heater,
|
||||
skip_send_event,
|
||||
):
|
||||
"""Test that when a underlying climate target temp is changed, the VTherm ignores the target temp if it is outside the min/max range"""
|
||||
|
||||
tz = get_tz(hass) # pylint: disable=invalid-name
|
||||
now: datetime = datetime.now(tz=tz)
|
||||
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
title="TheOverClimateMockName",
|
||||
unique_id="uniqueId",
|
||||
data=PARTIAL_CLIMATE_NOT_REGULATED_CONFIG, # 5 minutes security delay
|
||||
)
|
||||
|
||||
# Underlying is in HEAT mode but should be shutdown at startup
|
||||
fake_underlying_climate = MockClimate(
|
||||
hass, "mockUniqueId", "MockClimateName", {}, HVACMode.HEAT, HVACAction.HEATING
|
||||
)
|
||||
|
||||
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",
|
||||
return_value=fake_underlying_climate,
|
||||
) 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")
|
||||
|
||||
assert entity
|
||||
|
||||
assert entity.name == "TheOverClimateMockName"
|
||||
assert entity.is_over_climate is True
|
||||
assert entity.hvac_mode is HVACMode.OFF
|
||||
# because in MockClimate HVACAction is HEATING if hvac_mode is not set
|
||||
assert entity.hvac_action is HVACAction.HEATING
|
||||
# Underlying should have been shutdown
|
||||
assert mock_underlying_set_hvac_mode.call_count == 1
|
||||
mock_underlying_set_hvac_mode.assert_has_calls(
|
||||
[
|
||||
call.set_hvac_mode(HVACMode.OFF),
|
||||
]
|
||||
)
|
||||
|
||||
assert entity.target_temperature == entity.min_temp
|
||||
assert entity.preset_mode is PRESET_NONE
|
||||
|
||||
# should have been called with EventType.PRESET_EVENT and EventType.HVAC_MODE_EVENT
|
||||
assert mock_send_event.call_count == 2
|
||||
mock_send_event.assert_has_calls(
|
||||
[
|
||||
call.send_event(EventType.PRESET_EVENT, {"preset": PRESET_NONE}),
|
||||
call.send_event(
|
||||
EventType.HVAC_MODE_EVENT,
|
||||
{"hvac_mode": HVACMode.OFF},
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
assert mock_find_climate.call_count == 1
|
||||
assert mock_find_climate.mock_calls[0] == call()
|
||||
mock_find_climate.assert_has_calls([call.find_underlying_entity()])
|
||||
|
||||
# 1. Force preset mode
|
||||
await entity.async_set_hvac_mode(HVACMode.HEAT)
|
||||
assert entity.hvac_mode == HVACMode.HEAT
|
||||
await entity.async_set_preset_mode(PRESET_COMFORT)
|
||||
assert entity.preset_mode == PRESET_COMFORT
|
||||
|
||||
# 1. Try to set the target temperature to a below min_temp -> should be ignored
|
||||
# Wait 11 sec
|
||||
event_timestamp = now + timedelta(seconds=11)
|
||||
assert entity.is_regulated is False
|
||||
await send_climate_change_event_with_temperature(
|
||||
entity,
|
||||
HVACMode.HEAT,
|
||||
HVACMode.HEAT,
|
||||
HVACAction.OFF,
|
||||
HVACAction.OFF,
|
||||
event_timestamp,
|
||||
entity.min_temp - 1,
|
||||
True,
|
||||
"climate.mock_climate", # the underlying climate entity id
|
||||
)
|
||||
assert entity.target_temperature == 17
|
||||
|
||||
# 2. Try to set the target temperature to a above max_temp -> should be ignored
|
||||
event_timestamp = event_timestamp + timedelta(seconds=11)
|
||||
assert entity.is_regulated is False
|
||||
await send_climate_change_event_with_temperature(
|
||||
entity,
|
||||
HVACMode.HEAT,
|
||||
HVACMode.HEAT,
|
||||
HVACAction.OFF,
|
||||
HVACAction.OFF,
|
||||
event_timestamp,
|
||||
entity.max_temp + 1,
|
||||
True,
|
||||
"climate.mock_climate", # the underlying climate entity id
|
||||
)
|
||||
assert entity.target_temperature == 17
|
||||
149
tests/test_pi.py
@@ -3,10 +3,19 @@
|
||||
|
||||
from custom_components.versatile_thermostat.pi_algorithm import PITemperatureRegulator
|
||||
|
||||
def test_pi_algorithm_basics():
|
||||
""" Test the PI algorithm """
|
||||
|
||||
the_algo = PITemperatureRegulator(target_temp=20, kp=0.2, ki=0.05, k_ext=0.1, offset_max=2, stabilization_threshold=0.1, accumulated_error_threshold=20)
|
||||
def test_pi_algorithm_basics():
|
||||
"""Test the PI algorithm"""
|
||||
|
||||
the_algo = PITemperatureRegulator(
|
||||
target_temp=20,
|
||||
kp=0.2,
|
||||
ki=0.05,
|
||||
k_ext=0.1,
|
||||
offset_max=2,
|
||||
stabilization_threshold=0.1,
|
||||
accumulated_error_threshold=20,
|
||||
)
|
||||
|
||||
assert the_algo
|
||||
|
||||
@@ -19,62 +28,79 @@ def test_pi_algorithm_basics():
|
||||
the_algo.reset_accumulated_error()
|
||||
|
||||
# Test the accumulator threshold effect and offset_max
|
||||
assert the_algo.calculate_regulated_temperature(10, 10) == 22 # +2
|
||||
assert the_algo.calculate_regulated_temperature(10, 10) == 22 # +2
|
||||
assert the_algo.calculate_regulated_temperature(10, 10) == 22
|
||||
assert the_algo.calculate_regulated_temperature(10, 10) == 22
|
||||
# Will keep infinitly 22.0
|
||||
|
||||
# to reset the accumulated error
|
||||
the_algo.reset_accumulated_error()
|
||||
assert the_algo.calculate_regulated_temperature(18, 10) == 21.3 # +1.5
|
||||
assert the_algo.calculate_regulated_temperature(18, 10) == 21.3 # +1.5
|
||||
assert the_algo.calculate_regulated_temperature(18.1, 10) == 21.4 # +1.6
|
||||
assert the_algo.calculate_regulated_temperature(18.3, 10) == 21.4 # +1.6
|
||||
assert the_algo.calculate_regulated_temperature(18.5, 10) == 21.5 # +1.7
|
||||
assert the_algo.calculate_regulated_temperature(18.7, 10) == 21.6 # +1.7
|
||||
assert the_algo.calculate_regulated_temperature(19, 10) == 21.6 # +1.7
|
||||
assert the_algo.calculate_regulated_temperature(20, 10) == 21.5 # +1.5
|
||||
assert the_algo.calculate_regulated_temperature(21, 10) == 20.9 # +0.8
|
||||
assert the_algo.calculate_regulated_temperature(21, 10) == 20.8 # +0.7
|
||||
assert the_algo.calculate_regulated_temperature(20, 10) == 20.9 # +0.7
|
||||
assert the_algo.calculate_regulated_temperature(19, 10) == 21.6 # +1.7
|
||||
assert the_algo.calculate_regulated_temperature(20, 10) == 21.5 # +1.5
|
||||
assert the_algo.calculate_regulated_temperature(21, 10) == 21.1 # error change sign
|
||||
assert the_algo.calculate_regulated_temperature(21, 10) == 20.9
|
||||
assert the_algo.calculate_regulated_temperature(20, 10) == 21.0
|
||||
|
||||
# Test temperature external
|
||||
assert the_algo.calculate_regulated_temperature(20, 12) == 20.8 # +0.8
|
||||
assert the_algo.calculate_regulated_temperature(20, 15) == 20.5 # +0.5
|
||||
assert the_algo.calculate_regulated_temperature(20, 18) == 20.2 # +0.2
|
||||
assert the_algo.calculate_regulated_temperature(20, 20) == 20.0 # =
|
||||
assert the_algo.calculate_regulated_temperature(20, 12) == 20.8
|
||||
assert the_algo.calculate_regulated_temperature(20, 15) == 20.5
|
||||
assert the_algo.calculate_regulated_temperature(20, 18) == 20.2 # +0.2
|
||||
assert the_algo.calculate_regulated_temperature(20, 20) == 20 # =
|
||||
|
||||
|
||||
def test_pi_algorithm_light():
|
||||
""" Test the PI algorithm """
|
||||
"""Test the PI algorithm"""
|
||||
|
||||
the_algo = PITemperatureRegulator(target_temp=20, kp=0.2, ki=0.05, k_ext=0.1, offset_max=2, stabilization_threshold=0.1, accumulated_error_threshold=20)
|
||||
the_algo = PITemperatureRegulator(
|
||||
target_temp=20,
|
||||
kp=0.2,
|
||||
ki=0.05,
|
||||
k_ext=0.1,
|
||||
offset_max=2,
|
||||
stabilization_threshold=0.1,
|
||||
accumulated_error_threshold=20,
|
||||
)
|
||||
|
||||
assert the_algo
|
||||
|
||||
# to reset the accumulated erro
|
||||
the_algo.set_target_temp(20)
|
||||
|
||||
assert the_algo.calculate_regulated_temperature(18, 10) == 21.3 # +1.5
|
||||
assert the_algo.calculate_regulated_temperature(18, 10) == 21.3 # +1.5
|
||||
assert the_algo.calculate_regulated_temperature(18.1, 10) == 21.4 # +1.6
|
||||
assert the_algo.calculate_regulated_temperature(18.3, 10) == 21.4 # +1.6
|
||||
assert the_algo.calculate_regulated_temperature(18.5, 10) == 21.5 # +1.7
|
||||
assert the_algo.calculate_regulated_temperature(18.7, 10) == 21.6 # +1.7
|
||||
assert the_algo.calculate_regulated_temperature(19, 10) == 21.6 # +1.7
|
||||
assert the_algo.calculate_regulated_temperature(20, 10) == 21.5 # +1.5
|
||||
assert the_algo.calculate_regulated_temperature(21, 10) == 20.9 # +0.8
|
||||
assert the_algo.calculate_regulated_temperature(21, 10) == 20.8 # +0.7
|
||||
assert the_algo.calculate_regulated_temperature(20, 10) == 20.9 # +0.7
|
||||
assert the_algo.calculate_regulated_temperature(19, 10) == 21.6 # +1.7
|
||||
assert the_algo.calculate_regulated_temperature(20, 10) == 21.5 # +1.5
|
||||
assert the_algo.calculate_regulated_temperature(21, 10) == 21.1 # Error sign change
|
||||
assert the_algo.calculate_regulated_temperature(21, 10) == 20.9
|
||||
assert the_algo.calculate_regulated_temperature(20, 10) == 21
|
||||
|
||||
# Test temperature external
|
||||
assert the_algo.calculate_regulated_temperature(20, 12) == 20.8 # +0.8
|
||||
assert the_algo.calculate_regulated_temperature(20, 15) == 20.5 # +0.5
|
||||
assert the_algo.calculate_regulated_temperature(20, 18) == 20.2 # +0.2
|
||||
assert the_algo.calculate_regulated_temperature(20, 20) == 20.0 # =
|
||||
assert the_algo.calculate_regulated_temperature(20, 12) == 20.8 # +0.8
|
||||
assert the_algo.calculate_regulated_temperature(20, 15) == 20.5 # +0.5
|
||||
assert the_algo.calculate_regulated_temperature(20, 18) == 20.2 # +0.2
|
||||
assert the_algo.calculate_regulated_temperature(20, 20) == 20.0 # =
|
||||
|
||||
|
||||
def test_pi_algorithm_medium():
|
||||
""" Test the PI algorithm """
|
||||
"""Test the PI algorithm"""
|
||||
|
||||
the_algo = PITemperatureRegulator(target_temp=20, kp=0.5, ki=0.1, k_ext=0.1, offset_max=3, stabilization_threshold=0.1, accumulated_error_threshold=30)
|
||||
the_algo = PITemperatureRegulator(
|
||||
target_temp=20,
|
||||
kp=0.5,
|
||||
ki=0.1,
|
||||
k_ext=0.1,
|
||||
offset_max=3,
|
||||
stabilization_threshold=0.1,
|
||||
accumulated_error_threshold=30,
|
||||
)
|
||||
|
||||
assert the_algo
|
||||
|
||||
@@ -88,20 +114,20 @@ def test_pi_algorithm_medium():
|
||||
assert the_algo.calculate_regulated_temperature(18.7, 10) == 22.4
|
||||
assert the_algo.calculate_regulated_temperature(19, 10) == 22.3
|
||||
assert the_algo.calculate_regulated_temperature(20, 10) == 21.9
|
||||
assert the_algo.calculate_regulated_temperature(21, 10) == 20.5
|
||||
assert the_algo.calculate_regulated_temperature(21, 10) == 20.4
|
||||
assert the_algo.calculate_regulated_temperature(20, 10) == 20.8
|
||||
assert the_algo.calculate_regulated_temperature(21, 10) == 21.0 # error sign change
|
||||
assert the_algo.calculate_regulated_temperature(21, 10) == 20.7
|
||||
assert the_algo.calculate_regulated_temperature(20, 10) == 21.1
|
||||
|
||||
# Test temperature external
|
||||
assert the_algo.calculate_regulated_temperature(20, 8) == 21.2
|
||||
assert the_algo.calculate_regulated_temperature(20, 6) == 21.4
|
||||
assert the_algo.calculate_regulated_temperature(20, 4) == 21.6
|
||||
assert the_algo.calculate_regulated_temperature(20, 2) == 21.8
|
||||
assert the_algo.calculate_regulated_temperature(20, 0) == 22.0
|
||||
assert the_algo.calculate_regulated_temperature(20, -2) == 22.2
|
||||
assert the_algo.calculate_regulated_temperature(20, -4) == 22.4
|
||||
assert the_algo.calculate_regulated_temperature(20, -6) == 22.6
|
||||
assert the_algo.calculate_regulated_temperature(20, -8) == 22.8
|
||||
assert the_algo.calculate_regulated_temperature(20, 8) == 21.3
|
||||
assert the_algo.calculate_regulated_temperature(20, 6) == 21.5
|
||||
assert the_algo.calculate_regulated_temperature(20, 4) == 21.7
|
||||
assert the_algo.calculate_regulated_temperature(20, 2) == 21.9
|
||||
assert the_algo.calculate_regulated_temperature(20, 0) == 22.1
|
||||
assert the_algo.calculate_regulated_temperature(20, -2) == 22.3
|
||||
assert the_algo.calculate_regulated_temperature(20, -4) == 22.5
|
||||
assert the_algo.calculate_regulated_temperature(20, -6) == 22.7
|
||||
assert the_algo.calculate_regulated_temperature(20, -8) == 22.9
|
||||
|
||||
# to reset the accumulated erro
|
||||
the_algo.set_target_temp(20)
|
||||
@@ -121,10 +147,19 @@ def test_pi_algorithm_medium():
|
||||
assert the_algo.calculate_regulated_temperature(19, 5) == 23
|
||||
assert the_algo.calculate_regulated_temperature(19, 5) == 23
|
||||
|
||||
def test_pi_algorithm_strong():
|
||||
""" Test the PI algorithm """
|
||||
|
||||
the_algo = PITemperatureRegulator(target_temp=20, kp=0.6, ki=0.2, k_ext=0.2, offset_max=4, stabilization_threshold=0.1, accumulated_error_threshold=40)
|
||||
def test_pi_algorithm_strong():
|
||||
"""Test the PI algorithm"""
|
||||
|
||||
the_algo = PITemperatureRegulator(
|
||||
target_temp=20,
|
||||
kp=0.6,
|
||||
ki=0.2,
|
||||
k_ext=0.2,
|
||||
offset_max=4,
|
||||
stabilization_threshold=0.1,
|
||||
accumulated_error_threshold=40,
|
||||
)
|
||||
|
||||
assert the_algo
|
||||
|
||||
@@ -138,22 +173,22 @@ def test_pi_algorithm_strong():
|
||||
assert the_algo.calculate_regulated_temperature(18.7, 10) == 24
|
||||
assert the_algo.calculate_regulated_temperature(19, 10) == 24
|
||||
assert the_algo.calculate_regulated_temperature(20, 10) == 23.9
|
||||
assert the_algo.calculate_regulated_temperature(21, 10) == 21.4
|
||||
assert the_algo.calculate_regulated_temperature(21, 10) == 21.2
|
||||
assert the_algo.calculate_regulated_temperature(21, 10) == 21
|
||||
assert the_algo.calculate_regulated_temperature(21, 10) == 20.8
|
||||
assert the_algo.calculate_regulated_temperature(21, 10) == 20.6
|
||||
assert the_algo.calculate_regulated_temperature(21, 10) == 20.4
|
||||
assert the_algo.calculate_regulated_temperature(21, 10) == 20.2
|
||||
assert the_algo.calculate_regulated_temperature(21, 10) == 22.3 # error sign change
|
||||
assert the_algo.calculate_regulated_temperature(21, 10) == 21.8
|
||||
assert the_algo.calculate_regulated_temperature(21, 10) == 21.5
|
||||
assert the_algo.calculate_regulated_temperature(21, 10) == 21.3
|
||||
assert the_algo.calculate_regulated_temperature(21, 10) == 21.1
|
||||
assert the_algo.calculate_regulated_temperature(21, 10) == 20.9
|
||||
assert the_algo.calculate_regulated_temperature(21, 10) == 20.7
|
||||
|
||||
# Test temperature external
|
||||
assert the_algo.calculate_regulated_temperature(20, 8) == 21.0
|
||||
assert the_algo.calculate_regulated_temperature(20, 6) == 22.8
|
||||
assert the_algo.calculate_regulated_temperature(20, 4) == 23.2
|
||||
assert the_algo.calculate_regulated_temperature(20, 2) == 23.6
|
||||
assert the_algo.calculate_regulated_temperature(20, 0) == 24
|
||||
assert the_algo.calculate_regulated_temperature(20, -2) == 24
|
||||
assert the_algo.calculate_regulated_temperature(20, -4) == 24
|
||||
assert the_algo.calculate_regulated_temperature(20, 8) == 21.5
|
||||
assert the_algo.calculate_regulated_temperature(20, 6) == 21.9
|
||||
assert the_algo.calculate_regulated_temperature(20, 4) == 22.3
|
||||
assert the_algo.calculate_regulated_temperature(20, 2) == 22.7
|
||||
assert the_algo.calculate_regulated_temperature(20, 0) == 23.1
|
||||
assert the_algo.calculate_regulated_temperature(20, -2) == 23.5
|
||||
assert the_algo.calculate_regulated_temperature(20, -4) == 23.9
|
||||
assert the_algo.calculate_regulated_temperature(20, -6) == 24
|
||||
assert the_algo.calculate_regulated_temperature(20, -8) == 24
|
||||
|
||||
|
||||
@@ -433,6 +433,7 @@ async def test_power_management_energy_over_climate(
|
||||
new_hvac_action=HVACAction.HEATING,
|
||||
old_hvac_action=HVACAction.OFF,
|
||||
date=event_timestamp,
|
||||
underlying_entity_id="climate.mock_climate",
|
||||
)
|
||||
# We have the start event and not the end event
|
||||
assert (entity._underlying_climate_start_hvac_action_date - now).total_seconds() < 1
|
||||
@@ -448,6 +449,7 @@ async def test_power_management_energy_over_climate(
|
||||
new_hvac_action=HVACAction.IDLE,
|
||||
old_hvac_action=HVACAction.HEATING,
|
||||
date=now,
|
||||
underlying_entity_id="climate.mock_climate",
|
||||
)
|
||||
# We have the end event -> we should have some power and on_percent
|
||||
assert entity._underlying_climate_start_hvac_action_date is None
|
||||
|
||||
@@ -5,8 +5,12 @@ from unittest.mock import patch, call
|
||||
from datetime import timedelta, datetime
|
||||
import logging
|
||||
|
||||
from custom_components.versatile_thermostat.thermostat_climate import ThermostatOverClimate
|
||||
from custom_components.versatile_thermostat.thermostat_switch import ThermostatOverSwitch
|
||||
from custom_components.versatile_thermostat.thermostat_climate import (
|
||||
ThermostatOverClimate,
|
||||
)
|
||||
from custom_components.versatile_thermostat.thermostat_switch import (
|
||||
ThermostatOverSwitch,
|
||||
)
|
||||
from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import
|
||||
|
||||
|
||||
@@ -39,6 +43,7 @@ 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,
|
||||
@@ -68,15 +73,16 @@ async def test_security_feature(hass: HomeAssistant, skip_hass_states_is_state):
|
||||
assert entity.preset_mode is not PRESET_SECURITY
|
||||
assert entity.preset_modes == [
|
||||
PRESET_NONE,
|
||||
PRESET_FROST_PROTECTION,
|
||||
PRESET_ECO,
|
||||
PRESET_COMFORT,
|
||||
PRESET_BOOST,
|
||||
]
|
||||
assert entity._last_ext_temperature_mesure is not None
|
||||
assert entity._last_temperature_mesure is not None
|
||||
assert (entity._last_temperature_mesure.astimezone(tz) - now).total_seconds() < 1
|
||||
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
|
||||
assert (
|
||||
entity._last_ext_temperature_mesure.astimezone(tz) - now
|
||||
entity._last_ext_temperature_measure.astimezone(tz) - now
|
||||
).total_seconds() < 1
|
||||
|
||||
# set a preset
|
||||
@@ -112,8 +118,8 @@ async def test_security_feature(hass: HomeAssistant, skip_hass_states_is_state):
|
||||
call.send_event(
|
||||
EventType.TEMPERATURE_EVENT,
|
||||
{
|
||||
"last_temperature_mesure": event_timestamp.isoformat(),
|
||||
"last_ext_temperature_mesure": entity._last_ext_temperature_mesure.isoformat(),
|
||||
"last_temperature_measure": event_timestamp.isoformat(),
|
||||
"last_ext_temperature_measure": entity._last_ext_temperature_measure.isoformat(),
|
||||
"current_temp": 15,
|
||||
"current_ext_temp": None,
|
||||
"target_temp": 18,
|
||||
@@ -123,8 +129,8 @@ async def test_security_feature(hass: HomeAssistant, skip_hass_states_is_state):
|
||||
EventType.SECURITY_EVENT,
|
||||
{
|
||||
"type": "start",
|
||||
"last_temperature_mesure": event_timestamp.isoformat(),
|
||||
"last_ext_temperature_mesure": entity._last_ext_temperature_mesure.isoformat(),
|
||||
"last_temperature_measure": event_timestamp.isoformat(),
|
||||
"last_ext_temperature_measure": entity._last_ext_temperature_measure.isoformat(),
|
||||
"current_temp": 15,
|
||||
"current_ext_temp": None,
|
||||
"target_temp": 18,
|
||||
@@ -176,10 +182,10 @@ async def test_security_feature(hass: HomeAssistant, skip_hass_states_is_state):
|
||||
EventType.SECURITY_EVENT,
|
||||
{
|
||||
"type": "end",
|
||||
"last_temperature_mesure": event_timestamp.astimezone(
|
||||
"last_temperature_measure": event_timestamp.astimezone(
|
||||
tz
|
||||
).isoformat(),
|
||||
"last_ext_temperature_mesure": entity._last_ext_temperature_mesure.astimezone(
|
||||
"last_ext_temperature_measure": entity._last_ext_temperature_measure.astimezone(
|
||||
tz
|
||||
).isoformat(),
|
||||
"current_temp": 15.2,
|
||||
@@ -195,6 +201,197 @@ async def test_security_feature(hass: HomeAssistant, skip_hass_states_is_state):
|
||||
assert mock_heater_on.call_count == 1
|
||||
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_security_feature_back_on_percent(
|
||||
hass: HomeAssistant, skip_hass_states_is_state
|
||||
):
|
||||
"""Test the security feature and https://github.com/jmcollin78/versatile_thermostat/issues/49:
|
||||
1. creates a thermostat and check that security is off, preset Boost
|
||||
2. change temperature so that on_percent is high
|
||||
3. send next timestamp date so that security is on WITH A Eco preset that makes a on_percent low
|
||||
4. this shoud resolve the date issue
|
||||
4. check that security is off and preset is Boost
|
||||
"""
|
||||
|
||||
tz = get_tz(hass) # pylint: disable=invalid-name
|
||||
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
title="TheOverSwitchMockName",
|
||||
unique_id="uniqueId",
|
||||
data={
|
||||
"name": "TheOverSwitchMockName",
|
||||
"thermostat_type": "thermostat_over_switch",
|
||||
"temperature_sensor_entity_id": "sensor.mock_temp_sensor",
|
||||
"external_temperature_sensor_entity_id": "sensor.mock_ext_temp_sensor",
|
||||
"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",
|
||||
"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,
|
||||
},
|
||||
)
|
||||
|
||||
# 1. creates a thermostat and check that security is off
|
||||
now: datetime = datetime.now(tz=tz)
|
||||
entity: ThermostatOverSwitch = await create_thermostat(
|
||||
hass, entry, "climate.theoverswitchmockname"
|
||||
)
|
||||
assert entity
|
||||
|
||||
assert entity._security_state is False
|
||||
assert entity.preset_mode is not PRESET_SECURITY
|
||||
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
|
||||
assert (
|
||||
entity._last_ext_temperature_measure.astimezone(tz) - now
|
||||
).total_seconds() < 1
|
||||
|
||||
# set a preset
|
||||
assert entity.preset_mode is PRESET_NONE
|
||||
await entity.async_set_preset_mode(PRESET_BOOST)
|
||||
assert entity.preset_mode is PRESET_BOOST
|
||||
|
||||
# Turn On the thermostat
|
||||
assert entity.hvac_mode == HVACMode.OFF
|
||||
await entity.async_set_hvac_mode(HVACMode.HEAT)
|
||||
assert entity.hvac_mode == HVACMode.HEAT
|
||||
|
||||
# 2. activate on_percent
|
||||
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:
|
||||
event_timestamp = now + timedelta(minutes=1)
|
||||
entity._set_now(event_timestamp) # pylint: disable=protected-access
|
||||
|
||||
# set temperature to 17 so that on_percent will be > security_min_on_percent (0.2)
|
||||
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 mock_send_event.call_count == 0
|
||||
|
||||
# 3. Set safety mode with a preset change
|
||||
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:
|
||||
# 6 min between two mesure
|
||||
event_timestamp = event_timestamp + timedelta(minutes=6)
|
||||
entity._set_now(event_timestamp) # pylint: disable=protected-access
|
||||
|
||||
await send_temperature_change_event(entity, 17, event_timestamp)
|
||||
|
||||
assert entity._prop_algorithm.calculated_on_percent == 0.6
|
||||
|
||||
assert entity.security_state is True
|
||||
assert entity.preset_mode == PRESET_SECURITY
|
||||
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.TEMPERATURE_EVENT,
|
||||
{
|
||||
"last_temperature_measure": event_timestamp.isoformat(),
|
||||
"last_ext_temperature_measure": entity._last_ext_temperature_measure.isoformat(),
|
||||
"current_temp": 17,
|
||||
"current_ext_temp": None,
|
||||
"target_temp": 19,
|
||||
},
|
||||
),
|
||||
call.send_event(
|
||||
EventType.SECURITY_EVENT,
|
||||
{
|
||||
"type": "start",
|
||||
"last_temperature_measure": event_timestamp.isoformat(),
|
||||
"last_ext_temperature_measure": entity._last_ext_temperature_measure.isoformat(),
|
||||
"current_temp": 17,
|
||||
"current_ext_temp": None,
|
||||
"target_temp": 19,
|
||||
},
|
||||
),
|
||||
],
|
||||
any_order=True,
|
||||
)
|
||||
|
||||
# heating have been started on the previous call
|
||||
assert mock_heater_on.call_count == 0
|
||||
|
||||
# 4. change preset so that on_percent will be low
|
||||
event_timestamp = event_timestamp + timedelta(minutes=1)
|
||||
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
|
||||
|
||||
# 5. resolve the datetime issue
|
||||
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:
|
||||
# +2 min between two mesure
|
||||
event_timestamp = event_timestamp + timedelta(minutes=2)
|
||||
entity._set_now(event_timestamp) # pylint: disable=protected-access
|
||||
|
||||
# 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.preset_mode == PRESET_ECO
|
||||
assert entity._saved_preset_mode == PRESET_ECO
|
||||
assert entity._prop_algorithm.on_percent == 0.0
|
||||
assert entity._prop_algorithm.calculated_on_percent == 0.0
|
||||
|
||||
assert mock_send_event.call_count == 2
|
||||
mock_send_event.assert_has_calls(
|
||||
[
|
||||
call.send_event(EventType.PRESET_EVENT, {"preset": PRESET_ECO}),
|
||||
call.send_event(
|
||||
EventType.SECURITY_EVENT,
|
||||
{
|
||||
"type": "end",
|
||||
"last_temperature_measure": event_timestamp.astimezone(
|
||||
tz
|
||||
).isoformat(),
|
||||
"last_ext_temperature_measure": entity._last_ext_temperature_measure.astimezone(
|
||||
tz
|
||||
).isoformat(),
|
||||
"current_temp": 18.92,
|
||||
"current_ext_temp": None,
|
||||
"target_temp": 17,
|
||||
},
|
||||
),
|
||||
],
|
||||
any_order=True,
|
||||
)
|
||||
|
||||
# Heater is stays off
|
||||
assert mock_heater_on.call_count == 0
|
||||
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_security_over_climate(
|
||||
@@ -203,7 +400,7 @@ async def test_security_over_climate(
|
||||
skip_turn_on_off_heater,
|
||||
skip_send_event,
|
||||
):
|
||||
"""Test that when a underlying climate is not available the VTherm doesn't go into security mode"""
|
||||
"""Test that when a underlying climate is not available the VTherm doesn't go into safety mode"""
|
||||
|
||||
tz = get_tz(hass) # pylint: disable=invalid-name
|
||||
now: datetime = datetime.now(tz=tz)
|
||||
@@ -212,10 +409,12 @@ async def test_security_over_climate(
|
||||
domain=DOMAIN,
|
||||
title="TheOverClimateMockName",
|
||||
unique_id="uniqueId",
|
||||
data=PARTIAL_CLIMATE_CONFIG, # 5 minutes security delay
|
||||
data=PARTIAL_CLIMATE_CONFIG, # 5 minutes security delay
|
||||
)
|
||||
|
||||
fake_underlying_climate = MockClimate(hass, "mockUniqueId", "MockClimateName", {}, HVACMode.HEAT, HVACAction.HEATING)
|
||||
fake_underlying_climate = MockClimate(
|
||||
hass, "mockUniqueId", "MockClimateName", {}, HVACMode.HEAT, HVACAction.HEATING
|
||||
)
|
||||
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
|
||||
@@ -247,6 +446,7 @@ async def test_security_over_climate(
|
||||
assert entity.target_temperature == entity.min_temp
|
||||
assert entity.preset_modes == [
|
||||
PRESET_NONE,
|
||||
PRESET_FROST_PROTECTION,
|
||||
PRESET_ECO,
|
||||
PRESET_COMFORT,
|
||||
PRESET_BOOST,
|
||||
@@ -271,12 +471,14 @@ async def test_security_over_climate(
|
||||
assert mock_find_climate.mock_calls[0] == call()
|
||||
mock_find_climate.assert_has_calls([call.find_underlying_entity()])
|
||||
|
||||
# Force security mode
|
||||
assert entity._last_ext_temperature_mesure is not None
|
||||
assert entity._last_temperature_mesure is not None
|
||||
assert (entity._last_temperature_mesure.astimezone(tz) - now).total_seconds() < 1
|
||||
# Force safety mode
|
||||
assert entity._last_ext_temperature_measure is not None
|
||||
assert entity._last_temperature_measure is not None
|
||||
assert (
|
||||
entity._last_ext_temperature_mesure.astimezone(tz) - now
|
||||
entity._last_temperature_measure.astimezone(tz) - now
|
||||
).total_seconds() < 1
|
||||
assert (
|
||||
entity._last_ext_temperature_measure.astimezone(tz) - now
|
||||
).total_seconds() < 1
|
||||
|
||||
# Tries to turns on the Thermostat
|
||||
@@ -303,7 +505,7 @@ async def test_security_over_climate(
|
||||
event_timestamp = now - timedelta(minutes=6)
|
||||
|
||||
await send_temperature_change_event(entity, 15, event_timestamp)
|
||||
# Should stay False because a climate is never in security mode
|
||||
# Should stay False because a climate is never in safety mode
|
||||
assert entity.security_state is False
|
||||
assert entity.preset_mode == 'none'
|
||||
assert entity._saved_preset_mode == 'none'
|
||||
assert entity.preset_mode == "none"
|
||||
assert entity._saved_preset_mode == "none"
|
||||
|
||||
@@ -283,6 +283,7 @@ async def test_sensors_over_climate(
|
||||
new_hvac_action=HVACAction.HEATING,
|
||||
old_hvac_action=HVACAction.OFF,
|
||||
date=event_timestamp,
|
||||
underlying_entity_id="climate.mock_climate",
|
||||
)
|
||||
|
||||
# Send a climate_change event with HVACAction=IDLE (end of heating)
|
||||
@@ -293,6 +294,7 @@ async def test_sensors_over_climate(
|
||||
new_hvac_action=HVACAction.IDLE,
|
||||
old_hvac_action=HVACAction.HEATING,
|
||||
date=now,
|
||||
underlying_entity_id="climate.mock_climate",
|
||||
)
|
||||
|
||||
# 60 minutes heating with 1.5 kW heating -> 1.5 kWh
|
||||
|
||||
@@ -5,16 +5,16 @@ from unittest.mock import patch, call
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.components.climate import HVACAction, HVACMode
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
from homeassistant.components.climate import ClimateEntity, DOMAIN as CLIMATE_DOMAIN
|
||||
|
||||
from pytest_homeassistant_custom_component.common import MockConfigEntry
|
||||
|
||||
from custom_components.versatile_thermostat.base_thermostat import BaseThermostat
|
||||
from custom_components.versatile_thermostat.thermostat_climate import ThermostatOverClimate
|
||||
from custom_components.versatile_thermostat.thermostat_switch import ThermostatOverSwitch
|
||||
from custom_components.versatile_thermostat.thermostat_climate import (
|
||||
ThermostatOverClimate,
|
||||
)
|
||||
from custom_components.versatile_thermostat.thermostat_switch import (
|
||||
ThermostatOverSwitch,
|
||||
)
|
||||
|
||||
from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import
|
||||
|
||||
@@ -34,18 +34,7 @@ async def test_over_switch_full_start(hass: HomeAssistant, skip_hass_states_is_s
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
|
||||
) as mock_send_event:
|
||||
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.theoverswitchmockname")
|
||||
entity = await create_thermostat(hass, entry, "climate.theoverswitchmockname")
|
||||
|
||||
assert entity
|
||||
assert isinstance(entity, ThermostatOverSwitch)
|
||||
@@ -57,6 +46,7 @@ async def test_over_switch_full_start(hass: HomeAssistant, skip_hass_states_is_s
|
||||
assert entity.target_temperature == entity.min_temp
|
||||
assert entity.preset_modes == [
|
||||
PRESET_NONE,
|
||||
PRESET_FROST_PROTECTION,
|
||||
PRESET_ECO,
|
||||
PRESET_COMFORT,
|
||||
PRESET_BOOST,
|
||||
@@ -103,18 +93,19 @@ async def test_over_climate_full_start(hass: HomeAssistant, skip_hass_states_is_
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
|
||||
return_value=fake_underlying_climate,
|
||||
) as mock_find_climate:
|
||||
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 = find_my_entity("climate.theoverclimatemockname")
|
||||
entity = await create_thermostat(hass, entry, "climate.theoverclimatemockname")
|
||||
# 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 = find_my_entity("climate.theoverclimatemockname")
|
||||
|
||||
assert entity
|
||||
assert isinstance(entity, ThermostatOverClimate)
|
||||
@@ -126,6 +117,7 @@ async def test_over_climate_full_start(hass: HomeAssistant, skip_hass_states_is_
|
||||
assert entity.target_temperature == entity.min_temp
|
||||
assert entity.preset_modes == [
|
||||
PRESET_NONE,
|
||||
PRESET_FROST_PROTECTION,
|
||||
PRESET_ECO,
|
||||
PRESET_COMFORT,
|
||||
PRESET_BOOST,
|
||||
@@ -168,28 +160,30 @@ async def test_over_4switch_full_start(hass: HomeAssistant, skip_hass_states_is_
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
|
||||
) as mock_send_event:
|
||||
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")
|
||||
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
|
||||
|
||||
assert entity.name == "TheOver4SwitchMockName"
|
||||
assert entity.is_over_climate is False
|
||||
assert entity.is_over_switch
|
||||
assert entity.hvac_action is HVACAction.OFF
|
||||
assert entity.hvac_mode is HVACMode.OFF
|
||||
assert entity.target_temperature == entity.min_temp
|
||||
assert entity.preset_modes == [
|
||||
PRESET_NONE,
|
||||
PRESET_FROST_PROTECTION,
|
||||
PRESET_ECO,
|
||||
PRESET_COMFORT,
|
||||
PRESET_BOOST,
|
||||
@@ -224,3 +218,69 @@ async def test_over_4switch_full_start(hass: HomeAssistant, skip_hass_states_is_
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_over_switch_deactivate_preset(
|
||||
hass: HomeAssistant, skip_hass_states_is_state
|
||||
):
|
||||
"""Test the normal full start of a thermostat in thermostat_over_switch type"""
|
||||
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
title="TheOverSwitchMockName",
|
||||
unique_id="uniqueId",
|
||||
data={
|
||||
CONF_NAME: "TheOverSwitchMockName",
|
||||
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_SWITCH,
|
||||
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
|
||||
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
|
||||
CONF_CYCLE_MIN: 8,
|
||||
CONF_TEMP_MIN: 15,
|
||||
CONF_TEMP_MAX: 30,
|
||||
"frost_temp": 0,
|
||||
"eco_temp": 17,
|
||||
"comfort_temp": 0,
|
||||
"boost_temp": 19,
|
||||
CONF_USE_WINDOW_FEATURE: False,
|
||||
CONF_USE_MOTION_FEATURE: False,
|
||||
CONF_USE_POWER_FEATURE: False,
|
||||
CONF_USE_PRESENCE_FEATURE: False,
|
||||
CONF_HEATER: "switch.mock_switch1",
|
||||
CONF_HEATER_2: None,
|
||||
CONF_HEATER_3: None,
|
||||
CONF_HEATER_4: None,
|
||||
CONF_HEATER_KEEP_ALIVE: 0,
|
||||
CONF_SECURITY_DELAY_MIN: 10,
|
||||
CONF_MINIMAL_ACTIVATION_DELAY: 10,
|
||||
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
|
||||
CONF_TPI_COEF_INT: 0.6,
|
||||
CONF_TPI_COEF_EXT: 0.01,
|
||||
},
|
||||
)
|
||||
|
||||
entity: BaseThermostat = await create_thermostat(
|
||||
hass, entry, "climate.theoverswitchmockname"
|
||||
)
|
||||
assert entity
|
||||
assert isinstance(entity, ThermostatOverSwitch)
|
||||
|
||||
assert entity.preset_modes == [
|
||||
PRESET_NONE,
|
||||
# PRESET_FROST_PROTECTION,
|
||||
PRESET_ECO,
|
||||
# PRESET_COMFORT,
|
||||
PRESET_BOOST,
|
||||
]
|
||||
assert entity.preset_mode is PRESET_NONE
|
||||
|
||||
# try to set the COMFORT Preset which is absent
|
||||
try:
|
||||
await entity.async_set_preset_mode(PRESET_COMFORT)
|
||||
except ValueError as err:
|
||||
print(err)
|
||||
else:
|
||||
assert False
|
||||
finally:
|
||||
assert entity.preset_mode is PRESET_NONE
|
||||
|
||||
@@ -12,13 +12,18 @@ from homeassistant.components.climate import ClimateEntity, DOMAIN as CLIMATE_DO
|
||||
from pytest_homeassistant_custom_component.common import MockConfigEntry
|
||||
|
||||
from custom_components.versatile_thermostat.base_thermostat import BaseThermostat
|
||||
from custom_components.versatile_thermostat.thermostat_switch import ThermostatOverSwitch
|
||||
from custom_components.versatile_thermostat.thermostat_switch import (
|
||||
ThermostatOverSwitch,
|
||||
)
|
||||
|
||||
from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import
|
||||
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_over_switch_ac_full_start(hass: HomeAssistant, skip_hass_states_is_state): # pylint: disable=unused-argument
|
||||
async def test_over_switch_ac_full_start(
|
||||
hass: HomeAssistant, skip_hass_states_is_state
|
||||
): # pylint: disable=unused-argument
|
||||
"""Test the normal full start of a thermostat in thermostat_over_switch type"""
|
||||
|
||||
entry = MockConfigEntry(
|
||||
@@ -51,8 +56,25 @@ async def test_over_switch_ac_full_start(hass: HomeAssistant, skip_hass_states_i
|
||||
assert entity
|
||||
assert isinstance(entity, ThermostatOverSwitch)
|
||||
|
||||
# Initialise the preset temp
|
||||
await set_climate_preset_temp(
|
||||
entity, PRESET_FROST_PROTECTION + PRESET_AWAY_SUFFIX, 7
|
||||
)
|
||||
await set_climate_preset_temp(entity, PRESET_ECO + PRESET_AWAY_SUFFIX, 16)
|
||||
await set_climate_preset_temp(entity, PRESET_COMFORT + PRESET_AWAY_SUFFIX, 17)
|
||||
await set_climate_preset_temp(entity, PRESET_BOOST + PRESET_AWAY_SUFFIX, 18)
|
||||
await set_climate_preset_temp(
|
||||
entity, PRESET_ECO + PRESET_AC_SUFFIX + PRESET_AWAY_SUFFIX, 27
|
||||
)
|
||||
await set_climate_preset_temp(
|
||||
entity, PRESET_COMFORT + PRESET_AC_SUFFIX + PRESET_AWAY_SUFFIX, 26
|
||||
)
|
||||
await set_climate_preset_temp(
|
||||
entity, PRESET_BOOST + PRESET_AC_SUFFIX + PRESET_AWAY_SUFFIX, 25
|
||||
)
|
||||
|
||||
assert entity.name == "TheOverSwitchMockName"
|
||||
assert entity.is_over_climate is False # pylint: disable=protected-access
|
||||
assert entity.is_over_climate is False # pylint: disable=protected-access
|
||||
assert entity.ac_mode is True
|
||||
assert entity.hvac_action is HVACAction.OFF
|
||||
assert entity.hvac_mode is HVACMode.OFF
|
||||
@@ -60,17 +82,18 @@ async def test_over_switch_ac_full_start(hass: HomeAssistant, skip_hass_states_i
|
||||
assert entity.target_temperature == entity.max_temp
|
||||
assert entity.preset_modes == [
|
||||
PRESET_NONE,
|
||||
PRESET_FROST_PROTECTION,
|
||||
PRESET_ECO,
|
||||
PRESET_COMFORT,
|
||||
PRESET_BOOST,
|
||||
PRESET_ACTIVITY,
|
||||
]
|
||||
assert entity.preset_mode is PRESET_NONE
|
||||
assert entity._security_state is False # pylint: disable=protected-access
|
||||
assert entity._window_state is None # pylint: disable=protected-access
|
||||
assert entity._motion_state is None # pylint: disable=protected-access
|
||||
assert entity._presence_state is None # pylint: disable=protected-access
|
||||
assert entity._prop_algorithm is not None # pylint: disable=protected-access
|
||||
assert entity._security_state is False # pylint: disable=protected-access
|
||||
assert entity._window_state is None # pylint: disable=protected-access
|
||||
assert entity._motion_state is None # pylint: disable=protected-access
|
||||
assert entity._presence_state is None # pylint: disable=protected-access
|
||||
assert entity._prop_algorithm is not None # pylint: disable=protected-access
|
||||
|
||||
# should have been called with EventType.PRESET_EVENT and EventType.HVAC_MODE_EVENT
|
||||
assert mock_send_event.call_count == 2
|
||||
@@ -91,7 +114,7 @@ async def test_over_switch_ac_full_start(hass: HomeAssistant, skip_hass_states_i
|
||||
|
||||
event_timestamp = now - timedelta(minutes=4)
|
||||
await send_presence_change_event(entity, True, False, event_timestamp)
|
||||
assert entity._presence_state == STATE_ON # pylint: disable=protected-access
|
||||
assert entity._presence_state == STATE_ON # pylint: disable=protected-access
|
||||
|
||||
await entity.async_set_hvac_mode(HVACMode.COOL)
|
||||
assert entity.hvac_mode is HVACMode.COOL
|
||||
@@ -108,45 +131,54 @@ async def test_over_switch_ac_full_start(hass: HomeAssistant, skip_hass_states_i
|
||||
# Unset the presence
|
||||
event_timestamp = now - timedelta(minutes=3)
|
||||
await send_presence_change_event(entity, False, True, event_timestamp)
|
||||
assert entity._presence_state == STATE_OFF # pylint: disable=protected-access
|
||||
assert entity.target_temperature == 27 # eco_ac_away
|
||||
assert entity._presence_state == STATE_OFF # pylint: disable=protected-access
|
||||
assert entity.target_temperature == 27 # eco_ac_away
|
||||
|
||||
# Open a window
|
||||
with patch(
|
||||
"homeassistant.helpers.condition.state", return_value=True
|
||||
):
|
||||
with patch("homeassistant.helpers.condition.state", return_value=True):
|
||||
event_timestamp = now - timedelta(minutes=2)
|
||||
try_condition = await send_window_change_event(entity, True, False, event_timestamp)
|
||||
try_condition = await send_window_change_event(
|
||||
entity, True, False, event_timestamp
|
||||
)
|
||||
|
||||
# Confirme the window event
|
||||
await try_condition(None)
|
||||
|
||||
assert entity.hvac_mode is HVACMode.OFF
|
||||
assert entity.hvac_action is HVACAction.OFF
|
||||
assert entity.target_temperature == 27 # eco_ac_away
|
||||
assert entity.target_temperature == 27 # eco_ac_away (no change)
|
||||
|
||||
# Close a window
|
||||
with patch(
|
||||
"homeassistant.helpers.condition.state", return_value=True
|
||||
):
|
||||
with patch("homeassistant.helpers.condition.state", return_value=True):
|
||||
event_timestamp = now - timedelta(minutes=2)
|
||||
try_condition = await send_window_change_event(entity, False, True, event_timestamp)
|
||||
try_condition = await send_window_change_event(
|
||||
entity, False, True, event_timestamp
|
||||
)
|
||||
|
||||
# Confirme the window event
|
||||
await try_condition(None)
|
||||
|
||||
assert entity.hvac_mode is HVACMode.COOL
|
||||
assert (entity.hvac_action is HVACAction.OFF or entity.hvac_action is HVACAction.IDLE)
|
||||
assert entity.target_temperature == 27 # eco_ac_away
|
||||
assert (
|
||||
entity.hvac_action is HVACAction.OFF
|
||||
or entity.hvac_action is HVACAction.IDLE
|
||||
)
|
||||
assert entity.target_temperature == 27 # eco_ac_away
|
||||
|
||||
await entity.async_set_hvac_mode(HVACMode.HEAT)
|
||||
assert entity.hvac_mode is HVACMode.HEAT
|
||||
|
||||
# switch to comfort
|
||||
await entity.async_set_preset_mode(PRESET_COMFORT)
|
||||
assert entity.preset_mode is PRESET_COMFORT
|
||||
assert entity.target_temperature == 26
|
||||
assert entity.target_temperature == 17
|
||||
|
||||
# switch to Eco
|
||||
await entity.async_set_preset_mode(PRESET_ECO)
|
||||
assert entity.preset_mode is PRESET_ECO
|
||||
assert entity.target_temperature == 27
|
||||
assert entity.target_temperature == 16
|
||||
|
||||
# switch to boost
|
||||
await entity.async_set_preset_mode(PRESET_BOOST)
|
||||
assert entity.preset_mode is PRESET_BOOST
|
||||
assert entity.target_temperature == 18
|
||||
|
||||
383
tests/test_switch_keep_alive.py
Normal file
@@ -0,0 +1,383 @@
|
||||
"""Test the switch keep-alive feature."""
|
||||
import logging
|
||||
from collections.abc import AsyncGenerator, Callable, Awaitable
|
||||
from dataclasses import dataclass
|
||||
from unittest.mock import ANY, _Call, call, patch
|
||||
from datetime import datetime, timedelta
|
||||
from typing import cast
|
||||
|
||||
from custom_components.versatile_thermostat.keep_alive import BackoffTimer
|
||||
from custom_components.versatile_thermostat.thermostat_switch import (
|
||||
ThermostatOverSwitch,
|
||||
)
|
||||
from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import
|
||||
|
||||
logging.getLogger().setLevel(logging.DEBUG)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def config_entry() -> MockConfigEntry:
|
||||
"""Return common test data"""
|
||||
return MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
title="TheOverSwitchMockName",
|
||||
unique_id="uniqueId",
|
||||
data={
|
||||
CONF_NAME: "TheOverSwitchMockName",
|
||||
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_SWITCH,
|
||||
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
|
||||
CONF_CYCLE_MIN: 5,
|
||||
CONF_TEMP_MIN: 15,
|
||||
CONF_TEMP_MAX: 30,
|
||||
CONF_USE_WINDOW_FEATURE: False,
|
||||
CONF_USE_MOTION_FEATURE: False,
|
||||
CONF_USE_POWER_FEATURE: False,
|
||||
CONF_USE_PRESENCE_FEATURE: False,
|
||||
CONF_HEATER: "switch.mock_switch",
|
||||
CONF_HEATER_KEEP_ALIVE: 1,
|
||||
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.1,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class CommonMocks:
|
||||
"""Common mocked objects used by most test cases"""
|
||||
|
||||
config_entry: MockConfigEntry
|
||||
hass: HomeAssistant
|
||||
thermostat: ThermostatOverSwitch
|
||||
mock_is_state: MagicMock
|
||||
mock_get_state: MagicMock
|
||||
mock_service_call: MagicMock
|
||||
mock_async_track_time_interval: MagicMock
|
||||
mock_send_event: MagicMock
|
||||
|
||||
|
||||
# pylint: disable=redefined-outer-name, line-too-long, protected-access
|
||||
@pytest.fixture
|
||||
async def common_mocks(
|
||||
config_entry: MockConfigEntry,
|
||||
hass: HomeAssistant,
|
||||
) -> AsyncGenerator[CommonMocks, None]:
|
||||
"""Create and destroy a ThermostatOverSwitch as a test fixture"""
|
||||
# fmt: off
|
||||
with patch("homeassistant.core.ServiceRegistry.async_call") as mock_service_call, \
|
||||
patch("homeassistant.core.StateMachine.is_state", return_value=False) as mock_is_state, \
|
||||
patch("custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event") as mock_send_event, \
|
||||
patch("custom_components.versatile_thermostat.keep_alive.async_track_time_interval") as mock_async_track_time_interval:
|
||||
# fmt: on
|
||||
thermostat = cast(ThermostatOverSwitch, await create_thermostat(
|
||||
hass, config_entry, "climate.theoverswitchmockname"
|
||||
))
|
||||
with patch("homeassistant.core.StateMachine.get") as mock_get_state:
|
||||
mock_get_state.return_value.state = "off"
|
||||
yield CommonMocks(
|
||||
config_entry=config_entry,
|
||||
hass=hass,
|
||||
thermostat=thermostat,
|
||||
mock_is_state=mock_is_state,
|
||||
mock_get_state=mock_get_state,
|
||||
mock_service_call=mock_service_call,
|
||||
mock_async_track_time_interval=mock_async_track_time_interval,
|
||||
mock_send_event=mock_send_event,
|
||||
)
|
||||
# Clean the entity
|
||||
thermostat.remove_thermostat()
|
||||
|
||||
|
||||
class TestKeepAlive:
|
||||
"""Tests for the switch keep-alive feature"""
|
||||
|
||||
# pylint: disable=attribute-defined-outside-init
|
||||
def setup_method(self):
|
||||
"""Initialise test case data before the execution of each test case method."""
|
||||
self._prev_service_calls: list[_Call] = []
|
||||
self._prev_atti_call_count = 0 # atti: async_time_track_interval
|
||||
self._prev_atti_callback: Callable[[datetime], Awaitable[None]] | None = None
|
||||
|
||||
def _assert_service_call(
|
||||
self, cm: CommonMocks, expected_additional_calls: list[_Call]
|
||||
):
|
||||
"""Assert that hass.services.async_call() was called with the expected arguments,
|
||||
cumulatively over the course of long test cases."""
|
||||
self._prev_service_calls.extend(expected_additional_calls)
|
||||
cm.mock_service_call.assert_has_calls(self._prev_service_calls)
|
||||
|
||||
def _assert_async_mock_track_time_interval(
|
||||
self, cm: CommonMocks, expected_additional_calls: int
|
||||
):
|
||||
"""Assert that async_track_time_interval() was called the expected number of times
|
||||
with the expected arguments, cumulatively over the course of long test cases."""
|
||||
self._prev_atti_call_count += expected_additional_calls
|
||||
assert (
|
||||
cm.mock_async_track_time_interval.call_count == self._prev_atti_call_count
|
||||
)
|
||||
interval = timedelta(seconds=cm.config_entry.data[CONF_HEATER_KEEP_ALIVE])
|
||||
cm.mock_async_track_time_interval.assert_called_with(cm.hass, ANY, interval)
|
||||
keep_alive_callback = cm.mock_async_track_time_interval.call_args.args[1]
|
||||
assert callable(keep_alive_callback)
|
||||
self._prev_atti_callback = keep_alive_callback
|
||||
|
||||
async def _assert_multipe_keep_alive_callback_calls(
|
||||
self, cm: CommonMocks, n_calls: int
|
||||
):
|
||||
"""Call the keep-alive callback a few times as if `async_track_time_interval()` had
|
||||
done it, and assert that this triggers further calls to `async_track_time_interval()`.
|
||||
"""
|
||||
old_callback = self._prev_atti_callback
|
||||
assert (
|
||||
old_callback
|
||||
), "The keep-alive callback should have been called before, but it wasn't."
|
||||
interval = timedelta(seconds=cm.config_entry.data[CONF_HEATER_KEEP_ALIVE])
|
||||
for _ in range(n_calls):
|
||||
await old_callback(datetime.fromtimestamp(0))
|
||||
self._prev_atti_call_count += 1
|
||||
assert (
|
||||
cm.mock_async_track_time_interval.call_count
|
||||
== self._prev_atti_call_count
|
||||
)
|
||||
cm.mock_async_track_time_interval.assert_called_with(cm.hass, ANY, interval)
|
||||
new_callback = cm.mock_async_track_time_interval.call_args.args[1]
|
||||
assert new_callback is not old_callback
|
||||
assert new_callback.__qualname__ == old_callback.__qualname__
|
||||
old_callback = new_callback
|
||||
|
||||
self._prev_atti_callback = old_callback
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_switch_keep_alive_startup(self, common_mocks: CommonMocks):
|
||||
"""Test that switch keep-alive service calls are made at startup time."""
|
||||
|
||||
thermostat = common_mocks.thermostat
|
||||
await thermostat.async_set_hvac_mode(HVACMode.HEAT)
|
||||
assert thermostat.hvac_mode is HVACMode.HEAT
|
||||
assert thermostat.target_temperature == 15
|
||||
assert thermostat.is_device_active is False
|
||||
|
||||
# When the keep-alive feature is enabled, regular calls to the switch
|
||||
# turn_on / turn_off methods are _scheduled_ at start up.
|
||||
self._assert_async_mock_track_time_interval(common_mocks, 1)
|
||||
|
||||
# Those keep-alive calls are scheduled but until the callback is called,
|
||||
# no service calls are made to the SERVICE_TURN_OFF home assistant service.
|
||||
self._assert_service_call(common_mocks, [])
|
||||
|
||||
# Call the keep-alive callback a few times (as if `async_track_time_interval`
|
||||
# had done it) and assert that the callback function is replaced each time.
|
||||
await self._assert_multipe_keep_alive_callback_calls(common_mocks, 2)
|
||||
|
||||
# Every time the keep-alive callback is called, the home assistant switch
|
||||
# turn on/off service should be called too.
|
||||
self._assert_service_call(
|
||||
common_mocks,
|
||||
[
|
||||
call("switch", SERVICE_TURN_OFF, {"entity_id": "switch.mock_switch"}),
|
||||
call("switch", SERVICE_TURN_OFF, {"entity_id": "switch.mock_switch"}),
|
||||
],
|
||||
)
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_switch_keep_alive(self, common_mocks: CommonMocks):
|
||||
"""Test that switch keep-alive service calls are made during thermostat operation."""
|
||||
|
||||
hass = common_mocks.hass
|
||||
thermostat = common_mocks.thermostat
|
||||
|
||||
await thermostat.async_set_hvac_mode(HVACMode.HEAT)
|
||||
assert thermostat.hvac_mode is HVACMode.HEAT
|
||||
assert thermostat.target_temperature == 15
|
||||
assert thermostat.is_device_active is False
|
||||
|
||||
tz = get_tz(hass) # pylint: disable=invalid-name
|
||||
now = datetime.now(tz)
|
||||
event_timestamp = now - timedelta(minutes=4)
|
||||
|
||||
# 1. Decrease the temperature to activate the heater switch
|
||||
|
||||
await send_temperature_change_event(thermostat, 14, event_timestamp)
|
||||
|
||||
# async_track_time_interval() should have been called twice: once at startup
|
||||
# while the switch was turned off, and once when the switch was turned on.
|
||||
self._assert_async_mock_track_time_interval(common_mocks, 2)
|
||||
|
||||
# The keep-alive callback hasn't been called yet, so the only service
|
||||
# call so far is to SERVICE_TURN_ON as a result of the switch turn_on()
|
||||
# method being called when the target temperature increased.
|
||||
self._assert_service_call(
|
||||
common_mocks,
|
||||
[call("switch", SERVICE_TURN_ON, {"entity_id": "switch.mock_switch"})],
|
||||
)
|
||||
common_mocks.mock_is_state.return_value = True
|
||||
|
||||
# Call the keep-alive callback a few times (as if `async_track_time_interval`
|
||||
# had done it) and assert that the callback function is replaced each time.
|
||||
await self._assert_multipe_keep_alive_callback_calls(common_mocks, 2)
|
||||
|
||||
# Every time the keep-alive callback is called, the home assistant switch
|
||||
# turn on/off service should be called too.
|
||||
self._assert_service_call(
|
||||
common_mocks,
|
||||
[
|
||||
call("switch", SERVICE_TURN_ON, {"entity_id": "switch.mock_switch"}),
|
||||
call("switch", SERVICE_TURN_ON, {"entity_id": "switch.mock_switch"}),
|
||||
],
|
||||
)
|
||||
|
||||
# 2. Increase the temperature to deactivate the heater switch
|
||||
|
||||
await send_temperature_change_event(thermostat, 20, event_timestamp)
|
||||
|
||||
# Simulate the end of the TPI heating cycle
|
||||
await thermostat._underlyings[0].turn_off() # pylint: disable=protected-access
|
||||
|
||||
# turn_off() should have triggered a call to `async_track_time_interval()`
|
||||
self._assert_async_mock_track_time_interval(common_mocks, 1)
|
||||
|
||||
# turn_off() should have triggered a call to the SERVICE_TURN_OFF service.
|
||||
self._assert_service_call(
|
||||
common_mocks,
|
||||
[call("switch", SERVICE_TURN_OFF, {"entity_id": "switch.mock_switch"})],
|
||||
)
|
||||
common_mocks.mock_is_state.return_value = False
|
||||
|
||||
# Call the keep-alive callback a few times (as if `async_track_time_interval`
|
||||
# had done it) and assert that the callback function is replaced each time.
|
||||
await self._assert_multipe_keep_alive_callback_calls(common_mocks, 2)
|
||||
|
||||
# Every time the keep-alive callback is called, the home assistant switch
|
||||
# turn on/off service should be called too.
|
||||
self._assert_service_call(
|
||||
common_mocks,
|
||||
[
|
||||
call("switch", SERVICE_TURN_OFF, {"entity_id": "switch.mock_switch"}),
|
||||
call("switch", SERVICE_TURN_OFF, {"entity_id": "switch.mock_switch"}),
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
class TestBackoffTimer:
|
||||
"""Test the keep_alive.BackoffTimer helper class."""
|
||||
|
||||
def test_exponential_period_increase(self):
|
||||
"""Test that consecutive calls to is_ready() produce increasing wait periods."""
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.keep_alive.monotonic"
|
||||
) as mock_monotonic:
|
||||
timer = BackoffTimer(
|
||||
multiplier=2,
|
||||
lower_limit_sec=30,
|
||||
upper_limit_sec=86400,
|
||||
initially_ready=True,
|
||||
)
|
||||
mock_monotonic.return_value = 100
|
||||
assert timer.is_ready()
|
||||
mock_monotonic.return_value = 129
|
||||
assert not timer.is_ready()
|
||||
mock_monotonic.return_value = 130
|
||||
assert timer.is_ready()
|
||||
mock_monotonic.return_value = 188
|
||||
assert not timer.is_ready()
|
||||
mock_monotonic.return_value = 189
|
||||
assert not timer.is_ready()
|
||||
mock_monotonic.return_value = 190
|
||||
assert timer.is_ready()
|
||||
mock_monotonic.return_value = 309
|
||||
assert not timer.is_ready()
|
||||
|
||||
def test_the_upper_limit_option(self):
|
||||
"""Test the timer.in_progress property and the effect of timer.reset()."""
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.keep_alive.monotonic"
|
||||
) as mock_monotonic:
|
||||
timer = BackoffTimer(
|
||||
multiplier=2,
|
||||
lower_limit_sec=30,
|
||||
upper_limit_sec=50,
|
||||
initially_ready=True,
|
||||
)
|
||||
mock_monotonic.return_value = 100
|
||||
assert timer.is_ready()
|
||||
mock_monotonic.return_value = 129
|
||||
assert not timer.is_ready()
|
||||
mock_monotonic.return_value = 130
|
||||
assert timer.is_ready()
|
||||
mock_monotonic.return_value = 178
|
||||
assert not timer.is_ready()
|
||||
mock_monotonic.return_value = 179
|
||||
assert not timer.is_ready()
|
||||
mock_monotonic.return_value = 180
|
||||
assert timer.is_ready()
|
||||
mock_monotonic.return_value = 229
|
||||
assert not timer.is_ready()
|
||||
mock_monotonic.return_value = 230
|
||||
assert timer.is_ready()
|
||||
|
||||
def test_the_lower_limit_option(self):
|
||||
"""Test the timer.in_progress property and the effect of timer.reset()."""
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.keep_alive.monotonic"
|
||||
) as mock_monotonic:
|
||||
timer = BackoffTimer(
|
||||
multiplier=0.5,
|
||||
lower_limit_sec=30,
|
||||
upper_limit_sec=50,
|
||||
initially_ready=True,
|
||||
)
|
||||
mock_monotonic.return_value = 100
|
||||
assert timer.is_ready()
|
||||
mock_monotonic.return_value = 129
|
||||
assert not timer.is_ready()
|
||||
mock_monotonic.return_value = 130
|
||||
assert timer.is_ready()
|
||||
mock_monotonic.return_value = 158
|
||||
assert not timer.is_ready()
|
||||
mock_monotonic.return_value = 159
|
||||
assert not timer.is_ready()
|
||||
mock_monotonic.return_value = 160
|
||||
assert timer.is_ready()
|
||||
|
||||
def test_initial_is_ready_result(self):
|
||||
"""Test that the first call to is_ready() produces the initially_ready option value."""
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.keep_alive.monotonic"
|
||||
) as mock_monotonic:
|
||||
for initial in [True, False]:
|
||||
timer = BackoffTimer(
|
||||
multiplier=2,
|
||||
lower_limit_sec=30,
|
||||
upper_limit_sec=86400,
|
||||
initially_ready=initial,
|
||||
)
|
||||
mock_monotonic.return_value = 100
|
||||
assert timer.is_ready() == initial
|
||||
assert not timer.is_ready()
|
||||
|
||||
def test_in_progress_and_reset(self):
|
||||
"""Test the timer.in_progress property and the effect of timer.reset()."""
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.keep_alive.monotonic"
|
||||
) as mock_monotonic:
|
||||
timer = BackoffTimer(
|
||||
multiplier=2,
|
||||
lower_limit_sec=30,
|
||||
upper_limit_sec=86400,
|
||||
initially_ready=True,
|
||||
)
|
||||
mock_monotonic.return_value = 100
|
||||
assert not timer.in_progress
|
||||
assert timer.is_ready()
|
||||
assert timer.in_progress
|
||||
assert not timer.is_ready()
|
||||
timer.reset()
|
||||
assert not timer.in_progress
|
||||
assert timer.is_ready()
|
||||
assert timer.in_progress
|
||||
assert not timer.is_ready()
|
||||