Compare commits
5 Commits
main
...
3.5.3.beta
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
00d0659eef | ||
|
|
0b22abefa0 | ||
|
|
a6ad8e7927 | ||
|
|
23ee8f3d7f | ||
|
|
03723375e2 |
4
.bashrc
4
.bashrc
@@ -1,4 +1,6 @@
|
||||
|
||||
echo "Sourcing .bashrc"
|
||||
alias ll='ls -l'
|
||||
# source venv/bin/activate
|
||||
export HA='/home/vscode/core'
|
||||
cd $HA
|
||||
source venv/bin/activate
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
FROM mcr.microsoft.com/devcontainers/python:1-3.13
|
||||
RUN apt update && apt install -y ffmpeg libturbojpeg0-dev
|
||||
@@ -1,31 +1,11 @@
|
||||
default_config:
|
||||
|
||||
recorder:
|
||||
auto_purge: true
|
||||
purge_keep_days: 1
|
||||
commit_interval: 5
|
||||
include:
|
||||
domains:
|
||||
- input_boolean
|
||||
- input_number
|
||||
- input_select
|
||||
- switch
|
||||
- climate
|
||||
- sensor
|
||||
- binary_sensor
|
||||
- number
|
||||
- select
|
||||
- versatile_thermostat
|
||||
|
||||
logger:
|
||||
default: warning
|
||||
default: info
|
||||
logs:
|
||||
custom_components.versatile_thermostat: 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
|
||||
custom_components.versatile_thermostat.underlyings: debug
|
||||
custom_components.versatile_thermostat.climate: debug
|
||||
|
||||
# If you need to debug uncommment the line below (doc: https://www.home-assistant.io/integrations/debugpy/)
|
||||
debugpy:
|
||||
@@ -33,21 +13,6 @@ 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
|
||||
@@ -79,61 +44,6 @@ input_number:
|
||||
step: 10
|
||||
icon: mdi:flash
|
||||
unit_of_measurement: kW
|
||||
fake_valve1:
|
||||
name: The valve 1
|
||||
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
|
||||
fake_offset_calibration1:
|
||||
name: Sonoff offset calibration 1
|
||||
min: -12
|
||||
max: 12
|
||||
icon: mdi:tune
|
||||
unit_of_measurement: °C
|
||||
mode: box
|
||||
fake_opening_degree1:
|
||||
name: Sonoff Opening degree 1
|
||||
min: 0
|
||||
max: 100
|
||||
icon: mdi:valve-open
|
||||
unit_of_measurement: "%"
|
||||
mode: box
|
||||
fake_closing_degree1:
|
||||
name: Sonoff Closing degree 1
|
||||
min: 0
|
||||
max: 100
|
||||
icon: mdi:valve-closed
|
||||
unit_of_measurement: "%"
|
||||
mode: box
|
||||
fake_offset_calibration2:
|
||||
name: Sonoff offset calibration 2
|
||||
min: -12
|
||||
max: 12
|
||||
icon: mdi:tune
|
||||
unit_of_measurement: °C
|
||||
mode: box
|
||||
fake_opening_degree2:
|
||||
name: Sonoff Opening degree 2
|
||||
min: 0
|
||||
max: 100
|
||||
icon: mdi:valve-open
|
||||
unit_of_measurement: "%"
|
||||
mode: box
|
||||
fake_closing_degree2:
|
||||
name: Sonoff Closing degree 2
|
||||
min: 0
|
||||
max: 100
|
||||
icon: mdi:valve-closed
|
||||
unit_of_measurement: "%"
|
||||
mode: box
|
||||
|
||||
input_boolean:
|
||||
# input_boolean to simulate the windows entity. Only for development environment.
|
||||
@@ -150,9 +60,6 @@ input_boolean:
|
||||
fake_heater_switch1:
|
||||
name: Heater 1
|
||||
icon: mdi:radiator
|
||||
fake_heater_ac1:
|
||||
name: Air contionner 1
|
||||
icon: mdi:air-conditioner
|
||||
fake_heater_4switch1:
|
||||
name: Heater (multiswitch1)
|
||||
icon: mdi:radiator
|
||||
@@ -185,15 +92,6 @@ input_boolean:
|
||||
fake_presence_sensor1:
|
||||
name: Presence Sensor 1
|
||||
icon: mdi:home
|
||||
fake_valve_sonoff_trvzb1:
|
||||
name: Valve Sonoff TRVZB1
|
||||
icon: mdi:valve
|
||||
fake_valve_sonoff_trvzb2:
|
||||
name: Valve Sonoff TRVZB2
|
||||
icon: mdi:valve
|
||||
fake_inversed_heater:
|
||||
name: Inversed Heater
|
||||
icon: mdi:radiator-off
|
||||
|
||||
climate:
|
||||
- platform: generic_thermostat
|
||||
@@ -204,7 +102,6 @@ climate:
|
||||
name: Underlying thermostat2
|
||||
heater: input_boolean.fake_heater_switch3
|
||||
target_sensor: input_number.fake_temperature_sensor1
|
||||
ac_mode: false
|
||||
- platform: generic_thermostat
|
||||
name: Underlying thermostat3
|
||||
heater: input_boolean.fake_heater_switch3
|
||||
@@ -217,48 +114,31 @@ climate:
|
||||
name: Underlying thermostat 4-1
|
||||
heater: input_boolean.fake_heater_4climate1
|
||||
target_sensor: input_number.fake_temperature_sensor1
|
||||
ac_mode: false
|
||||
- platform: generic_thermostat
|
||||
name: Underlying thermostat 4-2
|
||||
heater: input_boolean.fake_heater_4climate2
|
||||
target_sensor: input_number.fake_temperature_sensor1
|
||||
ac_mode: false
|
||||
- platform: generic_thermostat
|
||||
name: Underlying thermostat 4-3
|
||||
heater: input_boolean.fake_heater_4climate3
|
||||
target_sensor: input_number.fake_temperature_sensor1
|
||||
ac_mode: false
|
||||
- platform: generic_thermostat
|
||||
name: Underlying thermostat 4-4
|
||||
heater: input_boolean.fake_heater_4climate4
|
||||
target_sensor: input_number.fake_temperature_sensor1
|
||||
ac_mode: false
|
||||
- platform: generic_thermostat
|
||||
name: Underlying thermostat9
|
||||
heater: input_boolean.fake_heater_switch3
|
||||
target_sensor: input_number.fake_temperature_sensor1
|
||||
- platform: generic_thermostat
|
||||
name: Underlying Sonoff TRVZB1
|
||||
heater: input_boolean.fake_valve_sonoff_trvzb1
|
||||
target_sensor: input_number.fake_temperature_sensor1
|
||||
ac_mode: false
|
||||
- platform: generic_thermostat
|
||||
name: Underlying Sonoff TRVZB2
|
||||
heater: input_boolean.fake_valve_sonoff_trvzb2
|
||||
target_sensor: input_number.fake_temperature_sensor1
|
||||
ac_mode: false
|
||||
- platform: generic_thermostat
|
||||
name: Underlying switch climate
|
||||
heater: input_boolean.fake_heater_switch2
|
||||
target_sensor: input_number.fake_temperature_sensor1
|
||||
ac_mode: false
|
||||
|
||||
input_datetime:
|
||||
fake_last_seen:
|
||||
name: Last seen temp sensor
|
||||
icon: mdi:update
|
||||
has_date: true
|
||||
has_time: true
|
||||
recorder:
|
||||
include:
|
||||
domains:
|
||||
- input_boolean
|
||||
- input_number
|
||||
- switch
|
||||
- climate
|
||||
- sensor
|
||||
|
||||
template:
|
||||
- binary_sensor:
|
||||
@@ -305,37 +185,20 @@ switch:
|
||||
friendly_name: "Pilote chauffage SDB RDC"
|
||||
value_template: "{{ is_state_attr('switch_seche_serviettes_sdb_rdc', 'sensor_state', 'on') }}"
|
||||
turn_on:
|
||||
action: select.select_option
|
||||
service: select.select_option
|
||||
data:
|
||||
option: comfort
|
||||
target:
|
||||
entity_id: select.seche_serviettes_sdb_rdc_cable_outlet_mode
|
||||
|
||||
turn_off:
|
||||
action: select.select_option
|
||||
service: select.select_option
|
||||
data:
|
||||
option: comfort-2
|
||||
target:
|
||||
entity_id: select.seche_serviettes_sdb_rdc_cable_outlet_mode
|
||||
- platform: template
|
||||
switches:
|
||||
fake_inversed_switch:
|
||||
friendly_name: "A fake inversed switch"
|
||||
value_template: "{{ is_state('input_boolean.fake_inversed_heater', 'on') }}"
|
||||
turn_on:
|
||||
action: input_boolean.turn_on
|
||||
data: {}
|
||||
target:
|
||||
entity_id: input_boolean.fake_inversed_heater
|
||||
turn_off:
|
||||
action: input_boolean.turn_off
|
||||
data: {}
|
||||
target:
|
||||
entity_id: input_boolean.fake_inversed_heater
|
||||
|
||||
frontend:
|
||||
extra_module_url:
|
||||
- /config/www/community/versatile-thermostat-ui-card/versatile-thermostat-ui-card.js
|
||||
themes:
|
||||
versatile_thermostat_theme:
|
||||
state-binary_sensor-safety-on-color: "#FF0B0B"
|
||||
@@ -343,4 +206,3 @@ frontend:
|
||||
state-binary_sensor-window-on-color: "rgb(156, 39, 176)"
|
||||
state-binary_sensor-motion-on-color: "rgb(156, 39, 176)"
|
||||
state-binary_sensor-presence-on-color: "lightgreen"
|
||||
state-binary_sensor-running-on-color: "orange"
|
||||
|
||||
@@ -1,70 +1,44 @@
|
||||
// See https://aka.ms/vscode-remote/devcontainer.json for format details.
|
||||
// "image": "ghcr.io/ludeeus/devcontainer/integration:latest",
|
||||
{
|
||||
"build": {
|
||||
"dockerfile": "Dockerfile"
|
||||
},
|
||||
"name": "Versatile Thermostat integration",
|
||||
"appPort": ["8123:8123"],
|
||||
// "postCreateCommand": "container install",
|
||||
"postCreateCommand": "./container dev-setup",
|
||||
|
||||
"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",
|
||||
"ferrierbenjamin.fold-unfold-all-icone",
|
||||
"LittleFoxTeam.vscode-python-test-adapter",
|
||||
"donjayamanne.githistory",
|
||||
"waderyan.gitblame",
|
||||
"keesschollaart.vscode-home-assistant",
|
||||
"vscode.markdown-math",
|
||||
"yzhang.markdown-all-in-one",
|
||||
"github.vscode-github-actions",
|
||||
"azuretools.vscode-docker",
|
||||
"huizhou.githd",
|
||||
"github.copilot",
|
||||
"github.copilot-chat"
|
||||
],
|
||||
"settings": {
|
||||
"files.eol": "\n",
|
||||
"editor.tabSize": 4,
|
||||
"terminal.integrated.profiles.linux": {
|
||||
"bash": {
|
||||
"path": "bash",
|
||||
"args": []
|
||||
}
|
||||
},
|
||||
"terminal.integrated.defaultProfile.linux": "bash",
|
||||
// "terminal.integrated.shell.linux": "/bin/bash",
|
||||
"python.pythonPath": "/usr/bin/python3",
|
||||
"python.analysis.autoSearchPaths": true,
|
||||
"pylint.lintOnChange": false,
|
||||
"python.formatting.provider": "black",
|
||||
"python.formatting.blackArgs": ["--line-length", "180"],
|
||||
"python.formatting.blackPath": "/usr/local/py-utils/bin/black",
|
||||
"editor.formatOnPaste": false,
|
||||
"editor.formatOnSave": true,
|
||||
"editor.formatOnType": true,
|
||||
"files.trimTrailingWhitespace": true
|
||||
// "python.experiments.optOutFrom": ["pythonTestAdapter"],
|
||||
// "python.analysis.logLevel": "Trace"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"image": "mcr.microsoft.com/vscode/devcontainers/python:0-3.10",
|
||||
"name": "Versatile Thermostat integration",
|
||||
"context": "..",
|
||||
"appPort": [
|
||||
"9123:8123"
|
||||
],
|
||||
// "postCreateCommand": "container install",
|
||||
"postCreateCommand": "./container install",
|
||||
"extensions": [
|
||||
"ms-python.python",
|
||||
"github.vscode-pull-request-github",
|
||||
"ryanluker.vscode-coverage-gutters",
|
||||
"ms-python.vscode-pylance"
|
||||
],
|
||||
"mounts": [
|
||||
"source=/Users/jmcollin/SugarSync/Projets/home-assistant/core,target=/home/vscode/core,type=bind,consistency=cached",
|
||||
"source=${localWorkspaceFolder}/.devcontainer/configuration.yaml,target=/home/vscode/core/config/configuration.yaml,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,
|
||||
"terminal.integrated.profiles.linux": {
|
||||
"Bash Profile": {
|
||||
"path": "bash",
|
||||
"args": []
|
||||
}
|
||||
},
|
||||
"terminal.integrated.defaultProfile.linux": "Bash Profile",
|
||||
// "terminal.integrated.shell.linux": "/bin/bash",
|
||||
"python.pythonPath": "/usr/bin/python3",
|
||||
"python.analysis.autoSearchPaths": true,
|
||||
"python.linting.pylintEnabled": true,
|
||||
"python.linting.enabled": true,
|
||||
"python.formatting.provider": "black",
|
||||
"editor.formatOnPaste": false,
|
||||
"editor.formatOnSave": true,
|
||||
"editor.formatOnType": true,
|
||||
"files.trimTrailingWhitespace": true
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
[pytest]
|
||||
asyncio_default_fixture_loop_scope = function
|
||||
1
.github/ISSUE_TEMPLATE/config.yml
vendored
1
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -1 +0,0 @@
|
||||
blank_issues_enabled: false
|
||||
6
.github/ISSUE_TEMPLATE/feature_request.md
vendored
6
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -4,12 +4,6 @@ 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 [...]
|
||||
|
||||
|
||||
124
.github/ISSUE_TEMPLATE/issue.md
vendored
124
.github/ISSUE_TEMPLATE/issue.md
vendored
@@ -4,146 +4,36 @@ about: Create a report to help us improve
|
||||
|
||||
---
|
||||
|
||||
# Read this carefully
|
||||
|
||||
> Please read carefully this instructions and fill this form before writing an issue. It helps me to help you.
|
||||
> If you choose to not follow this template, you accept to have no answer from the author. The tag on the issue 'Template not respected' means you don't respect this template. Potentially, you will not have a relevant answer.
|
||||
|
||||
<!-- This template will allow the maintainer to be efficient and post the more accurante response as possible. There is many types / modes / configuration possible, so the analysis can be very tricky. If don't follow this template, your issue could be rejected without any message. Please help me to help you. -->
|
||||
|
||||
<!-- 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] 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 that don't follow this template could be closed
|
||||
- Issues without debug logging will be closed.
|
||||
- Issues without configuration will 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 manifest.json file.
|
||||
If you are unsure about the version check the const.py 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. 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:
|
||||
```yaml
|
||||
hvac_modes:
|
||||
- heat
|
||||
- 'off'
|
||||
min_temp: 7
|
||||
max_temp: 35
|
||||
preset_modes:
|
||||
- none
|
||||
- eco
|
||||
- comfort
|
||||
- boost
|
||||
- activity
|
||||
current_temperature: 18.9
|
||||
temperature: 22
|
||||
hvac_action: 'off'
|
||||
preset_mode: security
|
||||
hvac_mode: 'off'
|
||||
type: null
|
||||
eco_temp: 17
|
||||
boost_temp: 20
|
||||
comfort_temp: 19
|
||||
eco_away_temp: 16.1
|
||||
boost_away_temp: 16.3
|
||||
comfort_away_temp: 16.2
|
||||
power_temp: 13
|
||||
ext_current_temperature: 11.6
|
||||
ac_mode: false
|
||||
current_power: 450
|
||||
current_power_max: 910
|
||||
saved_preset_mode: none
|
||||
saved_target_temp: 22
|
||||
saved_hvac_mode: heat
|
||||
window_state: 'on'
|
||||
motion_state: 'off'
|
||||
overpowering_state: false
|
||||
presence_state: 'on'
|
||||
window_auto_state: false
|
||||
is_window_bypass: false
|
||||
safety_delay_min: 2
|
||||
safety_min_on_percent: 0.5
|
||||
safety_default_on_percent: 0.1
|
||||
last_temperature_datetime: '2023-11-05T00:48:54.873157+01:00'
|
||||
last_ext_temperature_datetime: '2023-11-05T00:48:53.240122+01:00'
|
||||
security_state: true
|
||||
minimal_activation_delay_sec: 1
|
||||
device_power: 300
|
||||
mean_cycle_power: 30
|
||||
total_energy: 137.5
|
||||
last_update_datetime: '2023-11-05T00:51:54.901140+01:00'
|
||||
timezone: Europe/Paris
|
||||
window_sensor_entity_id: input_boolean.fake_window_sensor1
|
||||
window_delay_sec: 20
|
||||
window_auto_open_threshold: null
|
||||
window_auto_close_threshold: null
|
||||
window_auto_max_duration: null
|
||||
motion_sensor_entity_id: input_boolean.fake_motion_sensor1
|
||||
presence_sensor_entity_id: input_boolean.fake_presence_sensor1
|
||||
power_sensor_entity_id: input_number.fake_current_power
|
||||
max_power_sensor_entity_id: input_number.fake_current_power_max
|
||||
is_over_switch: true
|
||||
underlying_switch_0: input_boolean.fake_heater_switch1
|
||||
underlying_switch_1: null
|
||||
underlying_switch_2: null
|
||||
underlying_switch_3: null
|
||||
on_percent: 0.1
|
||||
on_time_sec: 6
|
||||
off_time_sec: 54
|
||||
cycle_min: 1
|
||||
function: tpi
|
||||
tpi_coef_int: 0.6
|
||||
tpi_coef_ext: 0.01
|
||||
friendly_name: Thermostat switch 1
|
||||
supported_features: 17
|
||||
|
||||
Add your logs here.
|
||||
|
||||
```
|
||||
|
||||
<!-- 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.
|
||||
|
||||
I'm trying to:
|
||||
<!-- compleete the description -->
|
||||
|
||||
And I expect:
|
||||
<!-- complete the expectations -->
|
||||
|
||||
But I observe this ....
|
||||
<!-- complete what you observe and why you think it is erroneous. -->
|
||||
|
||||
I read the documentation on the README.md file and I don't find any relevant information about this issue.
|
||||
|
||||
|
||||
## Debug log
|
||||
|
||||
<!-- To enable debug logs check this https://www.home-assistant.io/components/logger/
|
||||
Add the following configuration into your `configuration.yaml` (or `logger.yaml` if you have one) to enable logs: -->
|
||||
|
||||
```yaml
|
||||
logger:
|
||||
default: info
|
||||
logs:
|
||||
custom_components.versatile_thermostat: info
|
||||
```
|
||||
|
||||
<!-- You can also switch to debug mode but be careful, in debug mode, the logs are verbose.
|
||||
Please copy/paste the releveant logs (around the failure) below: -->
|
||||
<!-- To enable debug logs check this https://www.home-assistant.io/components/logger/ -->
|
||||
|
||||
```text
|
||||
|
||||
|
||||
6
.github/workflows/pull.yml
vendored
6
.github/workflows/pull.yml
vendored
@@ -31,8 +31,6 @@ jobs:
|
||||
- run: black .
|
||||
|
||||
tests:
|
||||
# Tests don't run in Gitlab ci environment
|
||||
if: 0
|
||||
runs-on: "ubuntu-latest"
|
||||
name: Run tests
|
||||
steps:
|
||||
@@ -43,10 +41,10 @@ jobs:
|
||||
with:
|
||||
python-version: "3.8"
|
||||
- name: Install requirements
|
||||
run: cd custom_components/versatile_thermostat && python3 -m pip install -r requirements_test.txt
|
||||
run: python3 -m pip install -r requirements_test.txt
|
||||
- name: Run tests
|
||||
run: |
|
||||
cd custom_components/versatile_thermostat && pytest \
|
||||
pytest \
|
||||
-qq \
|
||||
--timeout=9 \
|
||||
--durations=10 \
|
||||
|
||||
49
.github/workflows/testus.yaml
vendored
49
.github/workflows/testus.yaml
vendored
@@ -1,49 +0,0 @@
|
||||
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
|
||||
11
.gitignore
vendored
11
.gitignore
vendored
@@ -103,13 +103,4 @@ dist
|
||||
# TernJS port file
|
||||
.tern-port
|
||||
|
||||
# init file required for unittest
|
||||
custom_components/__init__.py
|
||||
__pycache__
|
||||
|
||||
config/**
|
||||
custom_components/hacs
|
||||
custom_components/localtuya
|
||||
|
||||
.coverage
|
||||
htmlcov
|
||||
__pycache__
|
||||
47
.vscode/launch.json
vendored
47
.vscode/launch.json
vendored
@@ -1,14 +1,39 @@
|
||||
{
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
// Example of attaching to local debug server
|
||||
"name": "Python: Attach Local",
|
||||
"type": "python",
|
||||
"request": "attach",
|
||||
"port": 5678,
|
||||
"host": "localhost",
|
||||
"justMyCode": false,
|
||||
"pathMappings": [
|
||||
// {
|
||||
// "localRoot": "${workspaceFolder}",
|
||||
// "remoteRoot": "."
|
||||
//},
|
||||
{
|
||||
"name": "Home Assistant (debug)",
|
||||
"type": "debugpy",
|
||||
"request": "launch",
|
||||
"module": "homeassistant",
|
||||
"justMyCode": false,
|
||||
"args": ["--debug", "-c", "config"]
|
||||
"localRoot": "${workspaceFolder}/../core",
|
||||
"remoteRoot": "/home/vscode/core"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
// Example of attaching to my production server
|
||||
"name": "Python: Attach Remote",
|
||||
"type": "python",
|
||||
"request": "attach",
|
||||
"port": 5678,
|
||||
"host": "homeassistant.local",
|
||||
"pathMappings": [
|
||||
{
|
||||
"localRoot": "${workspaceFolder}",
|
||||
"remoteRoot": "/usr/src/homeassistant"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
16
.vscode/settings.json
vendored
16
.vscode/settings.json
vendored
@@ -1,18 +1,12 @@
|
||||
{
|
||||
"[python]": {
|
||||
"editor.defaultFormatter": "ms-python.black-formatter",
|
||||
"editor.formatOnSave": true,
|
||||
"editor.formatOnSaveMode": "modifications"
|
||||
},
|
||||
"python.linting.pylintEnabled": true,
|
||||
"python.linting.enabled": true,
|
||||
"python.pythonPath": "/usr/local/bin/python",
|
||||
"files.associations": {
|
||||
"*.yaml": "home-assistant"
|
||||
},
|
||||
"python.testing.pytestArgs": [],
|
||||
"python.testing.unittestEnabled": false,
|
||||
"python.testing.pytestEnabled": true,
|
||||
"python.analysis.extraPaths": [
|
||||
// "/home/vscode/core",
|
||||
"/workspaces/versatile_thermostat/custom_components/versatile_thermostat",
|
||||
"/home/vscode/.local/lib/python3.12/site-packages/homeassistant"
|
||||
"/home/vscode/core",
|
||||
"/workspaces/versatile_thermostat"
|
||||
]
|
||||
}
|
||||
10
.vscode/tasks.json
vendored
10
.vscode/tasks.json
vendored
@@ -2,23 +2,17 @@
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"label": "Run Home Assistant on port 8123",
|
||||
"label": "Run Home Assistant on port 9123",
|
||||
"type": "shell",
|
||||
"command": "./container start",
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "Restart Home Assistant on port 8123",
|
||||
"label": "Restart Home Assistant on port 9123",
|
||||
"type": "shell",
|
||||
"command": "./container restart",
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "Start coverage",
|
||||
"type": "shell",
|
||||
"command": "./container coverage",
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "Home Assistant translations update",
|
||||
"type": "shell",
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
# Consignes de contribution
|
||||
|
||||
Contribuer à ce projet doit être aussi simple et transparent que possible, que ce soit :
|
||||
|
||||
- Signaler un bug
|
||||
- Discuter de l'état actuel du code
|
||||
- Soumettre un correctif
|
||||
- Proposer de nouvelles fonctionnalités
|
||||
|
||||
## Github est utilisé pour tout
|
||||
|
||||
Github est utilisé pour héberger du code, pour suivre les problèmes et les demandes de fonctionnalités, ainsi que pour accepter les demandes d'extraction.
|
||||
|
||||
Les demandes d'extraction sont le meilleur moyen de proposer des modifications à la base de code.
|
||||
|
||||
1. Fourchez le dépôt et créez votre branche à partir de `master`.
|
||||
2. Si vous avez modifié quelque chose, mettez à jour la documentation.
|
||||
3. Assurez-vous que votre code peluche (en utilisant du noir).
|
||||
4. Testez votre contribution.
|
||||
5. Émettez cette pull request !
|
||||
|
||||
## Toutes les contributions que vous ferez seront sous la licence logicielle MIT
|
||||
|
||||
En bref, lorsque vous soumettez des modifications de code, vos soumissions sont considérées comme étant sous la même [licence MIT](http://choosealicense.com/licenses/mit/) qui couvre le projet. N'hésitez pas à contacter les mainteneurs si cela vous préoccupe.
|
||||
|
||||
## Signaler les bogues en utilisant les [issues] de Github (../../issues)
|
||||
|
||||
Les problèmes GitHub sont utilisés pour suivre les bogues publics.
|
||||
Signalez un bogue en [ouvrant un nouveau problème](../../issues/new/choose) ; C'est si facile!
|
||||
|
||||
## Rédiger des rapports de bogue avec des détails, un arrière-plan et un exemple de code
|
||||
|
||||
Les **rapports de bogues géniaux** ont tendance à avoir :
|
||||
|
||||
- Un résumé rapide et/ou un historique
|
||||
- Étapes à reproduire
|
||||
- Être spécifique!
|
||||
- Donnez un exemple de code si vous le pouvez.
|
||||
- Ce à quoi vous vous attendiez arriverait
|
||||
- Que se passe-t-il réellement
|
||||
- Notes (y compris éventuellement pourquoi vous pensez que cela pourrait se produire, ou des choses que vous avez essayées qui n'ont pas fonctionné)
|
||||
|
||||
Les gens *adorent* les rapports de bogues approfondis. Je ne plaisante même pas.
|
||||
|
||||
## Utilisez un style de codage cohérent
|
||||
|
||||
Utilisez [black](https://github.com/ambv/black) pour vous assurer que le code suit le style.
|
||||
|
||||
## Testez votre modification de code
|
||||
|
||||
Ce composant personnalisé est basé sur les meilleures pratiques décrites ici [modèle d'intégration_blueprint](https://github.com/custom-components/integration_blueprint).
|
||||
|
||||
Il est livré avec un environnement de développement dans un conteneur, facile à lancer
|
||||
si vous utilisez Visual Studio Code. Avec ce conteneur, vous aurez un stand alone
|
||||
Instance de Home Assistant en cours d'exécution et déjà configurée avec le inclus
|
||||
[`.devcontainer/configuration.yaml`](./.devcontainer/configuration.yaml)
|
||||
déposer.
|
||||
|
||||
## Licence
|
||||
|
||||
En contribuant, vous acceptez que vos contributions soient autorisées sous sa licence MIT.
|
||||
@@ -1,61 +0,0 @@
|
||||
# Contribution guidelines
|
||||
|
||||
Contributing to this project should be as easy and transparent as possible, whether it's:
|
||||
|
||||
- Reporting a bug
|
||||
- Discussing the current state of the code
|
||||
- Submitting a fix
|
||||
- Proposing new features
|
||||
|
||||
## Github is used for everything
|
||||
|
||||
Github is used to host code, to track issues and feature requests, as well as accept pull requests.
|
||||
|
||||
Pull requests are the best way to propose changes to the codebase.
|
||||
|
||||
1. Fork the repo and create your branch from `master`.
|
||||
2. If you've changed something, update the documentation.
|
||||
3. Make sure your code lints (using black).
|
||||
4. Test you contribution.
|
||||
5. Issue that pull request!
|
||||
|
||||
## Any contributions you make will be under the MIT Software License
|
||||
|
||||
In short, when you submit code changes, your submissions are understood to be under the same [MIT License](http://choosealicense.com/licenses/mit/) that covers the project. Feel free to contact the maintainers if that's a concern.
|
||||
|
||||
## Report bugs using Github's [issues](../../issues)
|
||||
|
||||
GitHub issues are used to track public bugs.
|
||||
Report a bug by [opening a new issue](../../issues/new/choose); it's that easy!
|
||||
|
||||
## Write bug reports with detail, background, and sample code
|
||||
|
||||
**Great Bug Reports** tend to have:
|
||||
|
||||
- A quick summary and/or background
|
||||
- Steps to reproduce
|
||||
- Be specific!
|
||||
- Give sample code if you can.
|
||||
- What you expected would happen
|
||||
- What actually happens
|
||||
- Notes (possibly including why you think this might be happening, or stuff you tried that didn't work)
|
||||
|
||||
People *love* thorough bug reports. I'm not even kidding.
|
||||
|
||||
## Use a Consistent Coding Style
|
||||
|
||||
Use [black](https://github.com/ambv/black) to make sure the code follows the style.
|
||||
|
||||
## Test your code modification
|
||||
|
||||
This custom component is based on best practices described here [integration_blueprint template](https://github.com/custom-components/integration_blueprint).
|
||||
|
||||
It comes with development environment in a container, easy to launch
|
||||
if you use Visual Studio Code. With this container you will have a stand alone
|
||||
Home Assistant instance running and already configured with the included
|
||||
[`.devcontainer/configuration.yaml`](./.devcontainer/configuration.yaml)
|
||||
file.
|
||||
|
||||
## License
|
||||
|
||||
By contributing, you agree that your contributions will be licensed under its MIT License.
|
||||
808
README-fr.md
808
README-fr.md
@@ -4,101 +4,759 @@
|
||||
[![hacs][hacs_badge]][hacs]
|
||||
[![BuyMeCoffee][buymecoffeebadge]][buymecoffee]
|
||||
|
||||
[En](README.md)|[Fr](README-fr.md)
|
||||

|
||||

|
||||
|
||||
>  Cette intégration de thermostat vise à simplifier considérablement vos automatisations autour de la gestion du chauffage. Parce que tous les événements autour du chauffage classiques sont gérés nativement par le thermostat (personne à la maison ?, activité détectée dans une pièce ?, fenêtre ouverte ?, délestage de puissance ?), vous n'avez pas à vous encombrer de scripts et d'automatismes compliqués pour gérer vos thermostats. ;-).
|
||||
>  Cette intégration de thermostat vise à simplifier considérablement vos automatisations autour de la gestion du chauffage. Parce que tous les événements autour du chauffage classiques sont gérés nativement par le thermostat (personne à la maison ?, activité détectée dans une pièce ?, fenêtre ouverte ?, délestage de courant ?), vous n'avez pas à vous encombrer de scripts et d'automatismes compliqués pour gérer vos climats. ;-).
|
||||
|
||||
Ce composant personnalisé pour Home Assistant est une mise à niveau et une réécriture complète du composant "Awesome thermostat" (voir [Github](https://github.com/dadge/awesome_thermostat)) avec l'ajout de fonctionnalités.
|
||||
|
||||
# Quoi de neuf ?
|
||||

|
||||
> * **Release 7.2**:
|
||||
>
|
||||
> - Prise en compte native des équipements pilotable via une entité de type `select` (ou `input_select`) ou `climate` pour des _VTherm_ de type `over_switch`. Cette évolution rend obsolète, la création de switch virtuels pour l'intégration des Nodon ou Heaty ou eCosy ... etc. Plus d'informations [ici](documentation/fr/over-switch.md#la-personnalisation-des-commandes).
|
||||
>
|
||||
> - Lien vers la documentation : cette version 7.2 expérimente des liens vers la documentation depuis les pages de configuration. Le lien est accessible via l'icone [](https://github.com/jmcollin78/versatile_thermostat/blob/main/documentation/fr/over-switch.md#configuration). Elle est expérimentée sur certaines pages de la configuration.
|
||||
>
|
||||
> - Ajout d'un chapitre dans la documentation nommé 'Démarrage rapide' permettant de mettre en oeuvre rapidement un _VTherm_ en fonction de votre équipement. La page est [ici](documentation/quick-start.md)
|
||||
- [Merci pour la bière buymecoffee](#merci-pour-la-bière-buymecoffee)
|
||||
- [Quand l'utiliser et ne pas l'utiliser](#quand-lutiliser-et-ne-pas-lutiliser)
|
||||
- [Pourquoi une nouvelle implémentation du thermostat ?](#pourquoi-une-nouvelle-implémentation-du-thermostat-)
|
||||
- [Comment installer cet incroyable Thermostat Versatile ?](#comment-installer-cet-incroyable-thermostat-versatile-)
|
||||
- [HACS installation (recommendé)](#hacs-installation-recommendé)
|
||||
- [Installation manuelle](#installation-manuelle)
|
||||
- [Configuration](#configuration)
|
||||
- [Choix des attributs de base](#choix-des-attributs-de-base)
|
||||
- [Sélectionnez des entités pilotées](#sélectionnez-des-entités-pilotées)
|
||||
- [Configurez les coefficients de l'algorithme TPI](#configurez-les-coefficients-de-lalgorithme-tpi)
|
||||
- [Configurer la température préréglée](#configurer-la-température-préréglée)
|
||||
- [Configurer les portes/fenêtres en allumant/éteignant les thermostats](#configurer-les-portesfenêtres-en-allumantéteignant-les-thermostats)
|
||||
- [Le mode capteur](#le-mode-capteur)
|
||||
- [Le mode auto](#le-mode-auto)
|
||||
- [Configurer le mode d'activité ou la détection de mouvement](#configurer-le-mode-dactivité-ou-la-détection-de-mouvement)
|
||||
- [Configurer la gestion de la puissance](#configurer-la-gestion-de-la-puissance)
|
||||
- [Configurer la présence ou l'occupation](#configurer-la-présence-ou-loccupation)
|
||||
- [Configuration avancée](#configuration-avancée)
|
||||
- [Exemples de réglage](#exemples-de-réglage)
|
||||
- [Chauffage électrique](#chauffage-électrique)
|
||||
- [Chauffage central (chauffage gaz ou fuel)](#chauffage-central-chauffage-gaz-ou-fuel)
|
||||
- [Le capteur de température alimenté par batterie](#le-capteur-de-température-alimenté-par-batterie)
|
||||
- [Capteur de température réactif (sur secteur)](#capteur-de-température-réactif-sur-secteur)
|
||||
- [Mes presets](#mes-presets)
|
||||
- [Algorithme](#algorithme)
|
||||
- [Algorithme TPI](#algorithme-tpi)
|
||||
- [Capteurs](#capteurs)
|
||||
- [Services](#services)
|
||||
- [Forcer la présence/occupation](#forcer-la-présenceoccupation)
|
||||
- [Modifier la température des préréglages](#modifier-la-température-des-préréglages)
|
||||
- [Modifier les paramètres de sécurité](#modifier-les-paramètres-de-sécurité)
|
||||
- [Notifications](#notifications)
|
||||
- [Attributs personnalisés](#attributs-personnalisés)
|
||||
- [Quelques résultats](#quelques-résultats)
|
||||
- [Encore mieux](#encore-mieux)
|
||||
- [Encore mieux avec le composant Scheduler !](#encore-mieux-avec-le-composant-scheduler-)
|
||||
- [Encore bien mieux avec la custom:simple-thermostat front integration](#encore-bien-mieux-avec-la-customsimple-thermostat-front-integration)
|
||||
- [Toujours mieux avec Apex-chart pour régler votre thermostat](#toujours-mieux-avec-apex-chart-pour-régler-votre-thermostat)
|
||||
- [Et toujours de mieux en mieux avec l'AappDaemon NOTIFIER pour notifier les évènements](#et-toujours-de-mieux-en-mieux-avec-laappdaemon-notifier-pour-notifier-les-évènements)
|
||||
- [Les contributions sont les bienvenues !](#les-contributions-sont-les-bienvenues)
|
||||
|
||||
|
||||
# 🍻 Merci pour les bières [buymecoffee](https://www.buymeacoffee.com/jmcollin78) 🍻
|
||||
Un grand merci à tous mes fournisseurs de bières pour leurs dons et leurs encouragments. Ca me fait très plaisir et ça m'encourage à continuer ! Si cette intégration vous a fait économiser, payez moi une p'tite bière en retour, j'en vous en serais très reconnaissant !
|
||||
|
||||
# Glossaire
|
||||
|
||||
_VTherm_ : Versatile Thermostat dans la suite de ce document
|
||||
|
||||
_TRV_ : tête thermostatique équipée d'une vanne. La vanne s'ouvre ou se ferme permettant le passage de l'eau chaude
|
||||
|
||||
_AC_ : Air conditionné. Un équipement est AC si il fait du froid. Les températures sont alors inversées : Eco est plus chaud que Confort qui est plus chaud que Boost. Les algorithmes tiennent compte de cette information.
|
||||
|
||||
_EMA_ : Exponential Moving Average. Utilisé pour lisser les mesures de températures de capteur. Elle correspond à une moyenne glissante de la température de la pièce. Elle est utilisée pour calculer la pente de la courbe de température (slope) qui serait trop instable sur la courbe brute.
|
||||
|
||||
_slope_ : la pente de la courbe de température. Elle est mesurée en °(C ou K)/h. Elle est positive si la température augmente et négative si elle diminue. Cette pente est calculée sur l'_EMA_
|
||||
|
||||
_PAC_ : Pompe à chaleur
|
||||
|
||||
_HA_ : Home Assistant
|
||||
|
||||
_sous-jacent_ : l'équipement controlé par _VTherm_
|
||||
Ce composant personnalisé pour Home Assistant est une mise à niveau et est une réécriture complète du composant "Awesome thermostat" (voir [Github](https://github.com/dadge/awesome_thermostat)) avec l'ajout de fonctionnalités.
|
||||
|
||||
|
||||
# Documentation
|
||||
>  _*Nouveautés*_
|
||||
> * **Release 3.5**: Plusieurs thermostats sont possibles en "thermostat over climate" mode [#113](https://github.com/jmcollin78/versatile_thermostat/issues/113)
|
||||
> * **Release 3.4**: bug fix et exposition des preset temperatures pour le mode AC [#103](https://github.com/jmcollin78/versatile_thermostat/issues/103)
|
||||
> * **Release 3.3**: ajout du mode Air Conditionné (AC). Cette fonction vous permet d'utiliser le mode AC de votre thermostat sous-jacent. Pour l'utiliser, vous devez cocher l'option "Uitliser le mode AC" et définir les valeurs de température pour les presets et pour les presets en cas d'absence
|
||||
> * **Release 3.2** : ajout de la possibilité de commander plusieurs switch à partir du même thermostat. Dans ce mode, les switchs sont déclenchés avec un délai pour minimiser la puissance nécessaire à un instant (on minimise les périodes de recouvrement). Voir [Configuration](#sélectionnez-des-entités-pilotées)
|
||||
> * **Release 3.1** : ajout d'une détection de fenêtres/portes ouvertes par chute de température. Cette nouvelle fonction permet de stopper automatiquement un radiateur lorsque la température chute brutalement. Voir [Le mode auto](#le-mode-auto)
|
||||
> * **Release majeure 3.0** : ajout d'un équipement thermostat et de capteurs (binaires et non binaires) associés. Beaucoup plus proche de la philosphie Home Assistant, vous avez maintenant un accès direct à l'énergie consommée par le radiateur piloté par le thermostat et à plein d'autres capteurs qui seront utiles dans vos automatisations et dashboard.
|
||||
> * **release 2.3** : ajout de la mesure de puissance et d'énergie du radiateur piloté par le thermostat.
|
||||
> * **release 2.2** : ajout de fonction de sécurité permettant de ne pas laisser éternellement en chauffe un radiateur en cas de panne du thermomètre
|
||||
> * **release majeure 2.0** : ajout du thermostat "over climate" permettant de transformer n'importe quel thermostat en Versatile Thermostat et lui ajouter toutes les fonctions de ce dernier.
|
||||
|
||||
La documentation est maintenant découpée en plusieurs pages pour faciliter la lecture et la recherche d'informations :
|
||||
1. [présentation](documentation/fr/presentation.md),
|
||||
2. [Installation](documentation/fr/installation.md),
|
||||
3. [Démarrage rapide](documentation/fr/quick-start.md)
|
||||
4. [choisir un type de VTherm](documentation/fr/creation.md),
|
||||
5. [les attributs de base](documentation/fr/base-attributes.md)
|
||||
6. [configurer un VTherm sur un `switch`](documentation/fr/over-switch.md)
|
||||
7. [configurer un VTherm sur un `climate`](documentation/fr/over-climate.md)
|
||||
8. [configurer un VTherm sur une vanne](documentation/fr/over-valve.md)
|
||||
9. [les pré-régages (preset)](documentation/fr/feature-presets.md)
|
||||
10. [la gestion des ouvertures](documentation/fr/feature-window.md)
|
||||
11. [la gestion de la présence](documentation/fr/feature-presence.md)
|
||||
12. [la gestion de mouvement](documentation/fr/feature-motion.md)
|
||||
13. [la gestion de la puissance](documentation/fr/feature-power.md)
|
||||
14. [l'auto start and stop](documentation/fr/feature-auto-start-stop.md)
|
||||
15. [la contrôle centralisé de tous vos VTherms](documentation/fr/feature-central-mode.md)
|
||||
16. [la commande du chauffage central](documentation/fr/feature-central-boiler.md)
|
||||
17. [aspects avancés, mode sécurité](documentation/fr/feature-advanced.md)
|
||||
18. [l'auto-régulation](documentation/fr/self-regulation.md)
|
||||
19. [exemples de réglages](documentation/fr/tuning-examples.md)
|
||||
20. [les différents algorithmes](documentation/fr/algorithms.md)
|
||||
21. [documentation de référence](documentation/fr/reference.md)
|
||||
22. [exemple de réglages](documentation/fr/tuning-examples.md)
|
||||
23. [dépannage](documentation/fr/troubleshooting.md)
|
||||
24. [notes de version](documentation/fr/releases.md)
|
||||
# Merci pour la bière [buymecoffee](https://www.buymeacoffee.com/jmcollin78)
|
||||
Un grand merci à @salabur, @pvince83 and @bergoglio pour les bières. Ca fait très plaisir.
|
||||
|
||||
|
||||
# Quand l'utiliser et ne pas l'utiliser
|
||||
Ce thermostat peut piloter 2 types d'équipement:
|
||||
1. un radiateur qui ne fonctionne qu'en mode marche/arrêt (nommé ```thermostat_over_switch```). La configuration minimale nécessaire pour utiliser ce type thermostat est :
|
||||
a. un équipement comme un radiateur (un ```switch``` ou équivalent),
|
||||
b. une sonde de température pour la pièce (ou un input_number),
|
||||
c. un capteur de température externe (pensez à l'intégration météo si vous n'en avez pas)
|
||||
2. un autre thermostat qui a ses propres modes de fonctionnement (nommé ```thermostat_over_climate```). Pour ce type de thermostat la configuration minimale nécessite :
|
||||
a. un équipement comme une climatisation qui est pilotée par sa propre entity de type ```climate```,
|
||||
b. une sonde de température pour la pièce (ou un input_number),
|
||||
c. un capteur de température externe (pensez à l'intégration météo si vous n'en avez pas)
|
||||
|
||||
Le type ```thermostat_over_climate``` permet d'ajouter à votre équipement existant toutes les fonctionnalités fournies par VersatileThermostat. L'entité climate VersatileThermostat pilotera votre entité climate, en la coupant si les fenêtres sont ouvertes, la passant en mode Eco si personne n'est présent, etc. Cf. [ici](#pourquoi-une-nouvelle-implémentation-du-thermostat). Pour ce type de thermostat, les cycles éventuels de chauffe sont pilotés par l'entité climate sous-jacente et pas par le Versatile Thermostat lui-même.
|
||||
|
||||
Parce que cette intégration vise à commander le radiateur en tenant compte du préréglage configuré (preset) et de la température ambiante, ces informations sont obligatoires.
|
||||
|
||||
# Pourquoi une nouvelle implémentation du thermostat ?
|
||||
|
||||
Ce composant nommé __Versatile thermostat__ gère les cas d'utilisation suivants :
|
||||
- Configuration via l'interface graphique d'intégration standard (à l'aide du flux Config Entry),
|
||||
- Utilisations complètes du **mode préréglages**,
|
||||
- Désactiver le mode préréglé lorsque la température est **définie manuellement** sur un thermostat,
|
||||
- Éteindre/allumer un thermostat lorsqu'une **porte ou des fenêtres sont ouvertes/fermées** après un certain délai,
|
||||
- Changer de preset lorsqu'une **activité est détectée** ou non dans une pièce pendant un temps défini,
|
||||
- Utiliser un algorithme **TPI (Time Proportional Interval)** grâce à l'algorithme [[Argonaute](https://forum.hacf.fr/u/argonaute/summary)] ,
|
||||
- Ajouter une **gestion de délestage** ou une régulation pour ne pas dépasser une puissance totale définie. Lorsque la puissance maximale est dépassée, un préréglage caché de « puissance » est défini sur l'entité climatique. Lorsque la puissance passe en dessous du maximum, le préréglage précédent est restauré.
|
||||
- La **gestion de la présence à domicile**. Cette fonctionnalité vous permet de modifier dynamiquement la température du préréglage en tenant compte d'un capteur de présence de votre maison.
|
||||
- Des **services pour interagir avec le thermostat** à partir d'autres intégrations : vous pouvez forcer la présence / la non-présence à l'aide d'un service, et vous pouvez modifier dynamiquement la température des préréglages et changer les paramètres de sécurité.
|
||||
- Ajouter des capteurs pour voir les états internes du thermostat.
|
||||
|
||||
# Comment installer cet incroyable Thermostat Versatile ?
|
||||
|
||||
## HACS installation (recommendé)
|
||||
|
||||
1. Installez [HACS](https://hacs.xyz/). De cette façon, vous obtenez automatiquement les mises à jour.
|
||||
2. Ajoutez ce repository Github en tant que repository personnalisé dans les paramètres HACS.
|
||||
3. recherchez et installez "Versatile Thermostat" dans HACS et cliquez sur "installer".
|
||||
4. Redémarrez Home Assistant.
|
||||
5. Ensuite, vous pouvez ajouter une intégration de Versatile Thermostat dans la page d'intégration. Vous ajoutez autant de thermostats dont vous avez besoin (généralement un par radiateur qui doit être géré ou par pompe dans le cas d'un chauffage centralisé)
|
||||
|
||||
|
||||
## Installation manuelle
|
||||
|
||||
1. À l'aide de l'outil de votre choix, ouvrez le répertoire (dossier) de votre configuration HA (où vous trouverez `configuration.yaml`).
|
||||
2. Si vous n'avez pas de répertoire (dossier) `custom_components`, vous devez le créer.
|
||||
3. Dans le répertoire (dossier) `custom_components`, créez un nouveau dossier appelé `versatile_thermostat`.
|
||||
4. Téléchargez _tous_ les fichiers du répertoire `custom_components/versatile_thermostat/` (dossier) dans ce référentiel.
|
||||
5. Placez les fichiers que vous avez téléchargés dans le nouveau répertoire (dossier) que vous avez créé.
|
||||
6. Redémarrez l'assistant domestique
|
||||
7. Configurer la nouvelle intégration du Versatile Thermostat
|
||||
|
||||
# Configuration
|
||||
|
||||
Note: aucune configuration dans configuration.yaml n'est nécessaire car toute la configuration est effectuée via l'interface graphique standard lors de l'ajout de l'intégration.
|
||||
|
||||
Cliquez sur le bouton Ajouter une intégration dans la page d'intégration
|
||||
|
||||

|
||||
|
||||
La configuration peut être modifiée via la même interface. Sélectionnez simplement le thermostat à modifier, appuyez sur "Configurer" et vous pourrez modifier certains paramètres ou la configuration.
|
||||
|
||||
Suivez ensuite les étapes de configuration comme suit :
|
||||
|
||||
## Choix des attributs de base
|
||||
|
||||

|
||||
|
||||
Donnez les principaux attributs obligatoires :
|
||||
1. un nom (sera le nom de l'intégration et aussi le nom de l'entité climate)
|
||||
2. le type de thermostat ```thermostat_over_switch``` pour piloter un radiateur commandé par un switch ou ```thermostat_over_climate``` pour piloter un autre thermostat. Cf. [ci-dessus](#pourquoi-une-nouvelle-implémentation-du-thermostat)
|
||||
4. un identifiant d'entité de capteur de température qui donne la température de la pièce dans laquelle le radiateur est installé,
|
||||
5. une entité capteur de température donnant la température extérieure. Si vous n'avez pas de capteur externe, vous pouvez utiliser l'intégration météo locale
|
||||
6. une durée de cycle en minutes. A chaque cycle, le radiateur s'allumera puis s'éteindra pendant une durée calculée afin d'atteindre la température ciblée (voir [preset](#configure-the-preset-temperature) ci-dessous),
|
||||
7. les températures minimales et maximales du thermostat,
|
||||
8. une puissance de l'équipement ce qui va activer les capteurs de puissance et énergie consommée par l'appareil,
|
||||
9. la liste des fonctionnalités qui seront utilisées pour ce thermostat. En fonction de vos choix, les écrans de configuration suivants s'afficheront ou pas.
|
||||
|
||||
>  _*Notes*_
|
||||
1. avec le type ```thermostat_over_swutch```, les calculs sont effectués à chaque cycle. Donc en cas de changement de conditions, il faudra attendre le prochain cycle pour voir un changement. Pour cette raison, le cycle ne doit pas être trop long. **5 min est une bonne valeur**,
|
||||
2. si le cycle est trop court, le radiateur ne pourra jamais atteindre la température cible en effet pour le radiateur à accumulation et il sera sollicité inutilement
|
||||
|
||||
## Sélectionnez des entités pilotées
|
||||
En fonction de votre choix sur le type de thermostat, vous devrez choisir une ou plusieurs entités de type switch ou une entité de type climate. Seules les entités compatibles sont présentées.
|
||||
|
||||
Pour un thermostat de type ```thermostat_over_switch```:
|
||||

|
||||
L'algorithme à utiliser est aujourd'hui limité à TPI est disponible. Voir [algorithme](#algorithme).
|
||||
Si plusieurs entités de type sont configurées, la thermostat décale les activations afin de minimiser le nombre de switch actif à un instant t. Ca permet une meilleure répartition de la puissance puisque chaque radiateur va s'allumer à son tour.
|
||||
Exemple de déclenchement synchronisé :
|
||||

|
||||
|
||||
|
||||
Pour un thermostat de type ```thermostat_over_climate```:
|
||||

|
||||
|
||||
## Configurez les coefficients de l'algorithme TPI
|
||||
|
||||
Si vous avez choisi un thermostat de type ```thermostat_over_switch``` vous arriverez sur cette page :
|
||||
|
||||

|
||||
|
||||
Vous devez donner :
|
||||
1. le coefficient coef_int de l'algorithme TPI,
|
||||
2. le coefficient coef_ext de l'algorithme TPI
|
||||
|
||||
|
||||
Pour plus d'informations sur l'algorithme TPI et son réglage, veuillez vous référer à [algorithm](#algorithm).
|
||||
|
||||
## Configurer la température préréglée
|
||||
Cliquez sur 'Valider' sur la page précédente et vous y arriverez :
|
||||
|
||||

|
||||
|
||||
Le mode préréglé (preset) vous permet de préconfigurer la température ciblée. Utilisé en conjonction avec Scheduler (voir [scheduler](#even-better-with-scheduler-component) vous aurez un moyen puissant et simple d'optimiser la température par rapport à la consommation électrique de votre maison. Les préréglages gérés sont les suivants :
|
||||
- **Eco** : l'appareil est en mode d'économie d'énergie
|
||||
- **Confort** : l'appareil est en mode confort
|
||||
- **Boost** : l'appareil tourne toutes les vannes à fond
|
||||
|
||||
**Aucun** est toujours ajouté dans la liste des modes, car c'est un moyen de ne pas utiliser les preset mais une **température manuelle** à la place.
|
||||
|
||||
>  _*Notes*_
|
||||
1. En modifiant manuellement la température cible, réglez le préréglage sur Aucun (pas de préréglage). De cette façon, vous pouvez toujours définir une température cible même si aucun préréglage n'est disponible.
|
||||
2. Le préréglage standard ``Away`` est un préréglage caché qui n'est pas directement sélectionnable. Versatile Thermostat utilise la gestion de présence ou la gestion de mouvement pour régler automatiquement et dynamiquement la température cible en fonction d'une présence dans le logement ou d'une activité dans la pièce. Voir [gestion de la présence](#configure-the-presence-management).
|
||||
3. Si vous utilisez la gestion du délestage, vous verrez un préréglage caché nommé ``power``. Le préréglage de l'élément chauffant est réglé sur « puissance » lorsque des conditions de surpuissance sont rencontrées et que le délestage est actif pour cet élément chauffant. Voir [gestion de l'alimentation](#configure-the-power-management).
|
||||
4. si vous utilisez la configuration avancée, vous verrez le préréglage défini sur ``sécurité`` si la température n'a pas pu être récupérée après un certain délai
|
||||
5. Si vous ne souhaitez pas utiliser le préréglage, indiquez 0 comme température. Le préréglage sera alors ignoré et ne s'affichera pas dans le composant front
|
||||
|
||||
## Configurer les portes/fenêtres en allumant/éteignant les thermostats
|
||||
Vous devez avoir choisi la fonctionnalité ```Avec détection des ouvertures``` dans la première page pour arriver sur cette page.
|
||||
La détecttion des ouvertures peut se faire de 2 manières:
|
||||
1. soit avec un capteur placé sur l'ouverture (mode capteur),
|
||||
2. soit en détectant une chute brutale de température (mode auto)
|
||||
|
||||
### Le mode capteur
|
||||
En mode capteur, vous devez renseigner les informations suivantes:
|
||||

|
||||
|
||||
1. un identifiant d'entité d'un **capteur de fenêtre/porte**. Cela devrait être un binary_sensor ou un input_boolean. L'état de l'entité doit être 'on' lorsque la fenêtre est ouverte ou 'off' lorsqu'elle est fermée
|
||||
2. un **délai en secondes** avant tout changement. Cela permet d'ouvrir rapidement une fenêtre sans arrêter le chauffage.
|
||||
|
||||
|
||||
### Le mode auto
|
||||
En mode auto, la configuration est la suivante:
|
||||

|
||||
|
||||
1. un seuil de détection en degré par minute. Lorsque la température chute au delà de ce seuil, le thermostat s'éteindra. Plus cette valeur est faible et plus la détection sera rapide (en contre-partie d'un risque de faux positif),
|
||||
2. un seuil de fin de détection en degré par minute. Lorsque la chute de température repassera au-dessus cette valeur, le thermostat se remettra dans le mode précédent (mode et preset),
|
||||
3. une durée maximale de détection. Au delà de cette durée, le thermostat se remettra dans son mode et preset précédent même si la température continue de chuter.
|
||||
|
||||
Pour régler les seuils il est conseillé de commencer avec les valeurs de référence et d'ajuster les seuils de détection. Quelques essais m'ont donné les valeurs suivantes (pour un bureau):
|
||||
- seuil de détection : 0,05 °C/min
|
||||
- seuil de non détection: 0 °C/min
|
||||
- durée max : 60 min.
|
||||
|
||||
Un nouveau capteur "slope" a été ajouté pour tous les thermostats. Il donne la pente de la courbe de température en °C/min (ou °K/min). Cette pente est lissée et filtrée pour éviter les valeurs abérrantes des thermomètres qui viendraient pertuber la mesure.
|
||||

|
||||
|
||||
Pour bien régler il est conseillé d'affocher sur un même graphique historique la courbe de température et la pente de la courbe (le "slope") :
|
||||

|
||||
|
||||
Et c'est tout ! votre thermostat s'éteindra lorsque les fenêtres seront ouvertes et se rallumera lorsqu'il sera fermé.
|
||||
|
||||
>  _*Notes*_
|
||||
1. Si vous souhaitez utiliser **plusieurs capteurs de porte/fenêtre** pour automatiser votre thermostat, créez simplement un groupe avec le comportement habituel (https://www.home-assistant.io/integrations/binary_sensor.group/)
|
||||
2. Si vous n'avez pas de capteur de fenêtre/porte dans votre chambre, laissez simplement l'identifiant de l'entité du capteur vide,
|
||||
3. **Un seul mode est permis**. On ne peut pas configurer un thermostat avec un capteur et une détection automatique. Les 2 modes risquant de se contredire, il n'est pas possible d'avoir les 2 modes en même temps,
|
||||
4. Il est déconseillé d'utiliser le mode automatique pour un équipement soumis à des variations de température fréquentes et normales (couloirs, zones ouvertes, ...)
|
||||
|
||||
## Configurer le mode d'activité ou la détection de mouvement
|
||||
Si vous avez choisi la fonctionnalité ```Avec détection de mouvement```, cliquez sur 'Valider' sur la page précédente et vous y arriverez :
|
||||
|
||||

|
||||
|
||||
Nous allons maintenant voir comment configurer le nouveau mode Activité.
|
||||
Ce dont nous avons besoin:
|
||||
- un **capteur de mouvement**. ID d'entité d'un capteur de mouvement. Les états du capteur de mouvement doivent être « on » (mouvement détecté) ou « off » (aucun mouvement détecté)
|
||||
- une durée de **délai de mouvement** (en secondes) définissant combien de temps nous attendons la confirmation du mouvement avant de considérer le mouvement
|
||||
- un **préréglage de "mouvement" **. Nous utiliserons la température de ce préréglage lorsqu'une activité sera détectée.
|
||||
- un **préréglage "pas de mouvement"**. Nous utiliserons la température de ce deuxième préréglage lorsqu'aucune activité n'est détectée.
|
||||
|
||||
Alors imaginons que nous voulions avoir le comportement suivant :
|
||||
- nous avons une pièce avec un thermostat réglé en mode activité, le mode "mouvement" choisi est confort (21.5C), le mode "pas de mouvement" choisi est Eco (18.5C) et la temporisation du mouvement est de 5 min.
|
||||
- la pièce est vide depuis un moment (aucune activité détectée), la température de cette pièce est de 18,5 C
|
||||
- quelqu'un entre dans la pièce, une activité est détectée la température est fixée à 21,5 C
|
||||
- la personne quitte la chambre, au bout de 5 min la température est ramenée à 18,5 C
|
||||
|
||||
Pour que cela fonctionne, le thermostat climatique doit être en mode préréglé « Activité ».
|
||||
|
||||
>  _*Notes*_
|
||||
1. Sachez que comme pour les autres modes prédéfinis, ``Activity`` ne sera proposé que s'il est correctement configuré. En d'autres termes, les 4 clés de configuration doivent être définies si vous souhaitez voir l'activité dans l'interface de l'assistant domestique
|
||||
|
||||
## Configurer la gestion de la puissance
|
||||
|
||||
Si vous avez choisi la fonctionnalité ```Avec détection de la puissance```, cliquez sur 'Valider' sur la page précédente et vous arriverez ici :
|
||||
|
||||

|
||||
|
||||
Cette fonction vous permet de réguler la consommation électrique de vos radiateurs. Connue sous le nom de délestage, cette fonction vous permet de limiter la consommation électrique de votre appareil de chauffage si des conditions de surpuissance sont détectées. Donnez un **capteur à la consommation électrique actuelle de votre maison**, un **capteur à la puissance max** qu'il ne faut pas dépasser, la **consommation électrique de votre chauffage** (en étape 1 de la configuration) et l'algorithme ne démarrera pas un radiateur si la puissance maximale sera dépassée après le démarrage du radiateur.
|
||||
|
||||
Notez que toutes les valeurs de puissance doivent avoir les mêmes unités (kW ou W par exemple).
|
||||
Cela vous permet de modifier la puissance maximale au fil du temps à l'aide d'un planificateur ou de ce que vous voulez.
|
||||
|
||||
>  _*Notes*_
|
||||
1. En cas de délestage, le radiateur est réglé sur le préréglage nommé ```power```. Il s'agit d'un préréglage caché, vous ne pouvez pas le sélectionner manuellement.
|
||||
2. Je l'utilise pour éviter de dépasser la limite de mon contrat d'électricité lorsqu'un véhicule électrique est en charge. Cela crée une sorte d'autorégulation.
|
||||
3. Gardez toujours une marge, car la puissance max peut être brièvement dépassée en attendant le calcul du prochain cycle typiquement ou par des équipements non régulés.
|
||||
4. Si vous ne souhaitez pas utiliser cette fonctionnalité, laissez simplement l'identifiant des entités vide
|
||||
|
||||
## Configurer la présence ou l'occupation
|
||||
Si sélectionnée en première page, cette fonction vous permet de modifier dynamiquement la température de tous les préréglages du thermostat configurés lorsque personne n'est à la maison ou lorsque quelqu'un rentre à la maison. Pour cela, vous devez configurer la température qui sera utilisée pour chaque préréglage lorsque la présence est désactivée. Lorsque le capteur de présence s'éteint, ces températures seront utilisées. Lorsqu'il se rallume, la température "normale" configurée pour le préréglage est utilisée. Voir [gestion des préréglages](#configure-the-preset-temperature).
|
||||
Pour configurer la présence remplissez ce formulaire :
|
||||
|
||||

|
||||
|
||||
Pour cela, vous devez configurer :
|
||||
1. Un **capteur d'occupation** dont l'état doit être 'on' ou 'home' si quelqu'un est présent ou 'off' ou 'not_home' sinon,
|
||||
2. La **température utilisée en Eco** prédéfinie en cas d'absence,
|
||||
3. La **température utilisée en Confort** préréglée en cas d'absence,
|
||||
4. La **température utilisée en Boost** préréglée en cas d'absence
|
||||
|
||||
>  _*Notes*_
|
||||
1. le changement de température est immédiat et se répercute sur le volet avant. Le calcul prendra en compte la nouvelle température cible au prochain calcul du cycle,
|
||||
2. vous pouvez utiliser le capteur direct person.xxxx ou un groupe de capteurs de Home Assistant. Le capteur de présence gère les états ``on`` ou ``home`` comme présents et les états ``off`` ou ``not_home`` comme absents.
|
||||
|
||||
## Configuration avancée
|
||||
Ces paramètres permettent d'affiner le réglage du thermostat.
|
||||
Le formulaire de configuration avancée est le suivant :
|
||||
|
||||

|
||||
|
||||
Le premier délai (minimal_activation_delay_sec) en secondes est le délai minimum acceptable pour allumer le chauffage. Lorsque le calcul donne un délai de mise sous tension inférieur à cette valeur, le chauffage reste éteint.
|
||||
|
||||
Le deuxième délai (``security_delay_min``) est le délai maximal entre deux mesures de température avant de régler le préréglage sur ``security``. Si le capteur de température ne donne plus de mesures de température, le thermostat et le radiateur passeront en mode ``security`` après ce délai. Ceci est utile pour éviter une surchauffe si la batterie de votre capteur de température est trop faible.
|
||||
|
||||
Le troisième paramétre (``security_min_on_percent``) est la valeur minimal de ``on_percent`` en dessous de laquelle le préréglage sécurité ne sera pas activé. Ce paramètre permet de ne pas mettre en sécurité un thermostat, si le radiateur piloté ne chauffe pas suffisament.
|
||||
Mettre ce paramètre à ``0.00`` déclenchera le préréglage sécurité quelque soit la dernière consigne de chauffage, à l'inverse ``1.00`` ne déclenchera jamais le préréglage sécurité ( ce qui revient à désactiver la fonction).
|
||||
|
||||
Le quatrième param§tre (``security_default_on_percent``) est la valeur de ``on_percent`` qui sera utilisée lorsque le thermostat passe en mode ``security``. Si vous mettez ``0`` alors le thermostat sera coupé lorsqu'il passe en mode ``security``, mettre 0,2% par exemple permet de garder un peu de chauffage (20% dans ce cas), même en mode ``security``. Ca évite de retrouver son logement totalement gelé lors d'une panne de thermomètre.
|
||||
|
||||
Voir [exemple de réglages](#examples-tuning) pour avoir des exemples de réglage communs
|
||||
|
||||
>  _*Notes*_
|
||||
1. Lorsque le capteur de température viendra à la vie et renverra les températures, le préréglage sera restauré à sa valeur précédente,
|
||||
3. Attention, deux températures sont nécessaires : la température interne et la température externe et chacune doit donner la température, sinon le thermostat sera en préréglage "security",
|
||||
4. Un service est disponible qui permet de régler les 3 paramètres de sécurité. Ca peut servir à adapter la fonction de sécurité à votre usage,
|
||||
5. Pour un usage naturel, le ``security_default_on_percent`` doit être inférieur à ``security_min_on_percent``,
|
||||
6. Lorsqu'un thermostat de type ``thermostat_over_climate`` passe en mode ``security`` il est éteint. Les paramètres ``security_min_on_percent`` et ``security_default_on_percent`` ne sont alors pas utilisés.
|
||||
|
||||
# Exemples de réglage
|
||||
|
||||
## Chauffage électrique
|
||||
- cycle : entre 5 et 10 minutes,
|
||||
- minimal_activation_delay_sec : 30 secondes
|
||||
|
||||
## Chauffage central (chauffage gaz ou fuel)
|
||||
- cycle : entre 30 et 60 min,
|
||||
- minimal_activation_delay_sec : 300 secondes (à cause du temps de réponse)
|
||||
|
||||
## Le capteur de température alimenté par batterie
|
||||
- security_delay_min : 60 min (parce que ces capteurs sont paresseux)
|
||||
- security_min_on_percent : 0,5 (50% - on passe en preset ``security`` si le radiateur chauffait plus de 50% du temps)
|
||||
- security_default_on_percent : 0,1 (10% - en preset ``security``, on garde un fond de chauffe de 20% du temps)
|
||||
|
||||
Il faut comprendre ces réglages comme suit :
|
||||
|
||||
> Si le thermomètre n'envoie plus la température pendant 1 heure et que le pourcentage de chauffe (``on_percent``) était supérieur à 50 %, alors on ramène ce pourcentage de chauffe à 10 %.
|
||||
|
||||
A vous d'adapter ces réglages à votre cas !
|
||||
|
||||
Ce qui est important c'est de ne pas prendre trop de risque avec ces paramètres : supposez que vous êtes absent pour une longue période, que les piles de votre thermomètre arrivent en fin de vie, votre radiateur va chauffer 10% du temps pendant toute la durée de la panne.
|
||||
|
||||
Versatile Thermostat vous permet d'être notifié lorsqu'un évènement de ce type survient. Mettez en place, les alertes qui vont bien dès l'utilisation de ce thermostat. Cf. (#notifications)
|
||||
|
||||
## Capteur de température réactif (sur secteur)
|
||||
- security_delay_min : 15 min
|
||||
- security_min_on_percent : 0,7 (70% - on passe en preset ``security`` si le radiateur chauffait plus de 70% du temps)
|
||||
- security_default_on_percent : 0,25 (25% - en preset ``security``, on garde un fond de chauffe de 25% du temps)
|
||||
|
||||
## Mes presets
|
||||
Ceci est juste un exemple de la façon dont j'utilise le préréglage. A vous de vous adapter à votre configuration mais cela peut être utile pour comprendre son fonctionnement.
|
||||
``Éco`` : 17 °C
|
||||
``Confort`` : 19 °C
|
||||
``Boost`` : 20 °C
|
||||
|
||||
Lorsque la présence est désactivée :
|
||||
``Éco`` : 16,5 °C
|
||||
``Confort`` : 17 °C
|
||||
``Boost`` : 18 °C
|
||||
|
||||
Le détecteur de mouvement de mon bureau est configuré pour utiliser ``Boost`` lorsqu'un mouvement est détecté et ``Eco`` sinon.
|
||||
|
||||
# Algorithme
|
||||
Cette intégration utilise un algorithme proportionnel. Un algorithme proportionnel est utile pour éviter l'oscillation autour de la température cible. Cet algorithme est basé sur un cycle qui alterne le chauffage et l'arrêt du chauffage. La proportion de chauffage par rapport à l'absence de chauffage est déterminée par la différence entre la température et la température cible. Plus grande est la différence et plus grande est la proportion de chauffage à l'intérieur du cycle.
|
||||
|
||||
Cet algorithme fait converger la température et arrête d'osciller.
|
||||
|
||||
## Algorithme TPI
|
||||
L'algorithme TPI consiste à calculer à chaque cycle un pourcentage d'état On vs Off pour le radiateur en utilisant la température cible, la température actuelle dans la pièce et la température extérieure actuelle.
|
||||
|
||||
Le pourcentage est calculé avec cette formule :
|
||||
|
||||
on_percent = coef_int * (température cible - température actuelle) + coef_ext * (température cible - température extérieure)
|
||||
Ensuite, faites 0 <= on_percent <= 1
|
||||
|
||||
Les valeurs par défaut pour coef_int et coef_ext sont respectivement : ``0.6`` et ``0.01``. Ces valeurs par défaut conviennent à une pièce standard bien isolée.
|
||||
|
||||
Pour régler ces coefficients, gardez à l'esprit que :
|
||||
1. **si la température cible n'est pas atteinte** après une situation stable, vous devez augmenter le ``coef_ext`` (le ``on_percent`` est trop bas),
|
||||
2. **si la température cible est dépassée** après une situation stable, vous devez diminuer le ``coef_ext`` (le ``on_percent`` est trop haut),
|
||||
3. **si l'atteinte de la température cible est trop lente**, vous pouvez augmenter le ``coef_int`` pour donner plus de puissance au réchauffeur,
|
||||
4. **si l'atteinte de la température cible est trop rapide et que des oscillations apparaissent** autour de la cible, vous pouvez diminuer le ``coef_int`` pour donner moins de puissance au radiateur
|
||||
|
||||
Voir quelques situations à [examples](#some-results).
|
||||
|
||||
# Capteurs
|
||||
|
||||
Avec le thermostat sont disponibles des capteurs qui permettent de visualiser les alertes et l'état interne du thermostat. Ils sont disponibles dans les entités de l'appareil associé au thermostat :
|
||||
|
||||

|
||||
|
||||
Dans l'ordre, il y a :
|
||||
1. l'entité principale climate de commande du thermostat,
|
||||
2. l'énergie consommée par le thermostat (valeur qui s'incrémente en permanence),
|
||||
3. l'heure de réception de la dernière température extérieure,
|
||||
4. l'heure de réception de la dernière température intérieure,
|
||||
5. la puissance moyenne de l'appareil sur le cycle (pour les TPI seulement),
|
||||
6. le temps passé à l'état éteint dans le cycle (TPI seulement),
|
||||
7. le temps passé à l'état allumé dans le cycle (TPI seulement),
|
||||
8. l'état de délestage,
|
||||
9. le pourcentage de puissance sur le cycle (TPI seulement),
|
||||
10. l'état de présence (si la gestion de la présence est configurée),
|
||||
11. l'état de sécurité,
|
||||
12. l'état de l'ouverture (si la gestion des ouvertures est configurée),
|
||||
13. l'état du mouvement (si la gestion du mouvements est configurée)
|
||||
|
||||
Pour colorer les capteurs, ajouter ces lignes et personnalisez les au besoin, dans votre configuration.yaml :
|
||||
|
||||
```
|
||||
frontend:
|
||||
themes:
|
||||
versatile_thermostat_theme:
|
||||
state-binary_sensor-safety-on-color: "#FF0B0B"
|
||||
state-binary_sensor-power-on-color: "#FF0B0B"
|
||||
state-binary_sensor-window-on-color: "rgb(156, 39, 176)"
|
||||
state-binary_sensor-motion-on-color: "rgb(156, 39, 176)"
|
||||
state-binary_sensor-presence-on-color: "lightgreen"
|
||||
```
|
||||
et choisissez le thème ```versatile_thermostat_theme``` dans la configuration du panel. Vous obtiendrez quelque-chose qui va ressembler à ça :
|
||||
|
||||

|
||||
|
||||
# Services
|
||||
|
||||
Cette implémentation personnalisée offre des services spécifiques pour faciliter l'intégration avec d'autres composants Home Assistant.
|
||||
|
||||
## Forcer la présence/occupation
|
||||
Ce service permet de forcer l'état de présence indépendamment du capteur de présence. Cela peut être utile si vous souhaitez gérer la présence via un service et non via un capteur. Par exemple, vous pouvez utiliser votre réveil pour forcer l'absence lorsqu'il est allumé.
|
||||
|
||||
Le code pour appeler ce service est le suivant :
|
||||
```
|
||||
service : thermostat_polyvalent.set_presence
|
||||
Les données:
|
||||
présence : "off"
|
||||
cible:
|
||||
entity_id : climate.my_thermostat
|
||||
```
|
||||
|
||||
## Modifier la température des préréglages
|
||||
Ce service est utile si vous souhaitez modifier dynamiquement la température préréglée. Au lieu de changer de préréglage, certains cas d'utilisation doivent modifier la température du préréglage. Ainsi, vous pouvez garder le Programmateur inchangé pour gérer le préréglage et ajuster la température du préréglage.
|
||||
Si le préréglage modifié est actuellement sélectionné, la modification de la température cible est immédiate et sera prise en compte au prochain cycle de calcul.
|
||||
|
||||
Vous pouvez modifier l'une ou les deux températures (lorsqu'elles sont présentes ou absentes) de chaque préréglage.
|
||||
|
||||
Utilisez le code suivant pour régler la température du préréglage :
|
||||
```
|
||||
service : thermostat_polyvalent.set_preset_temperature
|
||||
date:
|
||||
preset : boost
|
||||
temperature : 17,8
|
||||
temperature_away : 15
|
||||
target:
|
||||
entity_id : climate.my_thermostat
|
||||
```
|
||||
|
||||
>  _*Notes*_
|
||||
- après un redémarrage, les préréglages sont réinitialisés à la température configurée. Si vous souhaitez que votre changement soit permanent, vous devez modifier le préréglage de la température dans la configuration de l'intégration.
|
||||
|
||||
## Modifier les paramètres de sécurité
|
||||
Ce service permet de modifier dynamiquement les paramètres de sécurité décrits ici [Configuration avancée](#configuration-avancée).
|
||||
Si le thermostat est en mode ``security`` les nouveaux paramètres sont appliqués immédiatement.
|
||||
|
||||
Pour changer les paramètres de sécurité utilisez le code suivant :
|
||||
```
|
||||
service : thermostat_polyvalent.set_security
|
||||
date:
|
||||
min_on_percent: "0.5"
|
||||
default_on_percent: "0.1"
|
||||
delay_min: 60
|
||||
target:
|
||||
entity_id : climate.my_thermostat
|
||||
```
|
||||
|
||||
# Notifications
|
||||
Les évènements marquant du thermostat sont notifiés par l'intermédiaire du bus de message.
|
||||
Les évènements notifiés sont les suivants:
|
||||
|
||||
- ``versatile_thermostat_security_event`` : un thermostat entre ou sort du preset ``security``
|
||||
- ``versatile_thermostat_power_event`` : un thermostat entre ou sort du preset ``power``
|
||||
- ``versatile_thermostat_temperature_event`` : une ou les deux mesures de température d'un thermostat n'ont pas été mis à jour depuis plus de `security_delay_min`` minutes
|
||||
- ``versatile_thermostat_hvac_mode_event`` : le thermostat est allumé ou éteint. Cet évènement est aussi diffusé au démarrage du thermostat
|
||||
- ``versatile_thermostat_preset_event`` : un nouveau preset est sélectionné sur le thermostat. Cet évènement est aussi diffusé au démarrage du thermostat
|
||||
|
||||
Si vous avez bien suivi, lorsqu'un thermostat passe en mode sécurité, 3 évènements sont déclenchés :
|
||||
1. ``versatile_thermostat_temperature_event`` pour indiquer qu'un thermomètre ne répond plus,
|
||||
2. ``versatile_thermostat_preset_event`` pour indiquer le passage en preset ```security```,
|
||||
3. ``versatile_thermostat_hvac_mode_event`` pour indiquer l'extinction éventuelle du thermostat
|
||||
|
||||
Chaque évènement porte les valeurs clés de l'évènement (températures, preset courant, puissance courante, ...) ainsi que les états du thermostat.
|
||||
|
||||
Vous pouvez très facilement capter ses évènements dans une automatisation par exemple pour notifier les utilisateurs.
|
||||
|
||||
# Attributs personnalisés
|
||||
|
||||
Pour régler l'algorithme, vous avez accès à tout le contexte vu et calculé par le thermostat via des attributs dédiés. Vous pouvez voir (et utiliser) ces attributs dans l'IHM "Outils de développement / états" de HA. Entrez votre thermostat et vous verrez quelque chose comme ceci :
|
||||

|
||||
|
||||
Les attributs personnalisés sont les suivants :
|
||||
|
||||
| Attribut | Signification |
|
||||
| ----------| --------|
|
||||
| ``hvac_modes`` | La liste des modes supportés par le thermostat |
|
||||
| ``temp_min`` | La température minimale |
|
||||
| ``temp_max`` | La température maximale |
|
||||
| ``preset_modes`` | Les préréglages visibles pour ce thermostat. Les préréglages cachés ne sont pas affichés ici |
|
||||
| ``temperature_actuelle`` | La température actuelle telle que rapportée par le capteur |
|
||||
| ``temperature`` | La température cible |
|
||||
| ``action_hvac`` | L'action en cours d'exécution par le réchauffeur. Peut être inactif, chauffage |
|
||||
| ``preset_mode`` | Le préréglage actuellement sélectionné. Peut être l'un des 'preset_modes' ou un préréglage caché comme power |
|
||||
| ``[eco/confort/boost]_temp`` | La température configurée pour le préréglage xxx |
|
||||
| ``[eco/confort/boost]_away_temp`` | La température configurée pour le préréglage xxx lorsque la présence est désactivée ou not_home |
|
||||
| ``temp_power`` | La température utilisée lors de la détection de la perte |
|
||||
| ``on_percent`` | (déprécié) Le pourcentage sur calculé par l'algorithme TPI |
|
||||
| ``on_time_sec`` | (déprécié) La période On en sec. Doit être ```on_percent * cycle_min``` |
|
||||
| ``off_time_sec`` | (déprécié) La période d'arrêt en sec. Doit être ```(1 - on_percent) * cycle_min``` |
|
||||
| ``cycle_min`` | Le cycle de calcul en minutes |
|
||||
| ``function`` | L'algorithme utilisé pour le calcul du cycle |
|
||||
| ``tpi_coef_int`` | Le ``coef_int`` de l'algorithme TPI |
|
||||
| ``tpi_coef_ext`` | Le ``coef_ext`` de l'algorithme TPI |
|
||||
| ``saved_preset_mode`` | Le dernier preset utilisé avant le basculement automatique du preset |
|
||||
| ``saved_target_temp`` | La dernière température utilisée avant la commutation automatique |
|
||||
| ``window_state`` | (déprécié) Le dernier état connu du capteur de fenêtre. Aucun si la fenêtre n'est pas configurée |
|
||||
| ``motion_state`` | (déprécié) Le dernier état connu du capteur de mouvement. Aucun si le mouvement n'est pas configuré |
|
||||
| ``overpowering_state`` | (déprécié) Le dernier état connu du capteur surpuissant. Aucun si la gestion de l'alimentation n'est pas configurée |
|
||||
| ``presence_state`` | (déprécié) Le dernier état connu du capteur de présence. Aucun si la gestion de présence n'est pas configurée |
|
||||
| ``security_delay_min`` | Le délai avant de régler le mode de sécurité lorsque le capteur de température est éteint |
|
||||
| ``security_min_on_percent`` | Pourcentage de chauffe en dessous duquel le thermostat ne passera pas en sécurité |
|
||||
| ``security_default_on_percent`` | Pourcentage de chauffe utilisé lorsque le thermostat est en sécurité |
|
||||
| ``last_temperature_datetime`` | (déprécié) La date et l'heure au format ISO8866 de la dernière réception de température interne |
|
||||
| ``last_ext_temperature_datetime`` | (déprécié) La date et l'heure au format ISO8866 de la dernière réception de température extérieure |
|
||||
| ``security_state`` | (déprécié) L'état de sécurité. vrai ou faux |
|
||||
| ``minimal_activation_delay_sec`` | Le délai d'activation minimal en secondes |
|
||||
| ``last_update_datetime`` | La date et l'heure au format ISO8866 de cet état |
|
||||
| ``friendly_name`` | Le nom du thermostat |
|
||||
| ``supported_features`` | Une combinaison de toutes les fonctionnalités prises en charge par ce thermostat. Voir la documentation officielle sur l'intégration climatique pour plus d'informations |
|
||||
|
||||
# Quelques résultats
|
||||
|
||||
**Stabilité de la température autour de la cible configurée par preset:**
|
||||
**Convergence de la température vers la cible configurée par preset:**
|
||||

|
||||
|
||||

|
||||
[Cycle de marche/arrêt calculé par l'intégration :](https://)
|
||||

|
||||
|
||||
Cycle de marche/arrêt calculé par l'intégration (`over_climate`):
|
||||

|
||||
|
||||
**Régulation avec un `over_switch`**
|
||||
|
||||

|
||||
**Coef_int trop élevé (oscillations autour de la cible)**
|
||||

|
||||
|
||||
**Évolution du calcul de l'algorithme**
|
||||

|
||||
Voir le code de ce composant [[ci-dessous](#even-better-with-apex-chart-to-tune-your-thermostat)]
|
||||
|
||||
**Regulation forte en `over_climate`**
|
||||
|
||||

|
||||
|
||||
**Regulation avec contrôle direct de la vanne en `over_climate`**
|
||||
|
||||

|
||||
**Thermostat finement réglé**
|
||||
Merci [impuR_Shozz](https://forum.hacf.fr/u/impur_shozz/summary) !
|
||||
On peut voir une stabilité autour de la température cible (consigne) et lorsqu'à cible le on_percent (puissance) est proche de 0,3 ce qui semble une très bonne valeur.
|
||||
|
||||

|
||||
|
||||
Enjoy !
|
||||
|
||||
# Encore mieux
|
||||
|
||||
## Encore mieux avec le composant Scheduler !
|
||||
|
||||
Afin de profiter de toute la puissance du Versatile Thermostat, je vous invite à l'utiliser avec https://github.com/nielsfaber/scheduler-component
|
||||
En effet, le composant scheduler propose une gestion de la base climatique sur les modes prédéfinis. Cette fonctionnalité a un intérêt limité avec le thermostat générique mais elle devient très puissante avec le thermostat Awesome :
|
||||
|
||||
À partir d'ici, je suppose que vous avez installé Awesome Thermostat et Scheduler Component.
|
||||
|
||||
Dans Scheduler, ajoutez un planning :
|
||||
|
||||

|
||||
|
||||
Choisissez le groupe "climat", choisissez une (ou plusieurs) entité(s), sélectionnez "MAKE SCHEME" et cliquez sur suivant :
|
||||
(il est possible de choisir "SET PRESET", mais je préfère utiliser "MAKE SCHEME")
|
||||
|
||||

|
||||
|
||||
Définissez votre schéma de mode et enregistrez :
|
||||
|
||||
|
||||

|
||||
|
||||
Dans cet exemple, j'ai réglé le mode ECO pendant la nuit et le jour lorsqu'il n'y a personne à la maison BOOST le matin et CONFORT le soir.
|
||||
|
||||
|
||||
J'espère que cet exemple vous aidera, n'hésitez pas à me faire part de vos retours !
|
||||
|
||||
## Encore bien mieux avec la custom:simple-thermostat front integration
|
||||
Le ``custom:simple-thermostat`` [ici](https://github.com/nervetattoo/simple-thermostat) est une excellente intégration qui permet une certaine personnalisation qui s'adapte bien à ce thermostat.
|
||||
Vous pouvez avoir quelque chose comme ça très facilement 
|
||||
Exemple de configuration :
|
||||
|
||||
```
|
||||
type: custom:simple-thermostat
|
||||
entity: climate.thermostat_sam2
|
||||
layout:
|
||||
step: row
|
||||
label:
|
||||
temperature: T°
|
||||
state: Etat
|
||||
hide:
|
||||
state: false
|
||||
control:
|
||||
hvac:
|
||||
_name: Mode
|
||||
preset:
|
||||
_name: Preset
|
||||
sensors:
|
||||
- entity: sensor.total_puissance_radiateur_sam2
|
||||
icon: mdi:lightning-bolt-outline
|
||||
header:
|
||||
toggle:
|
||||
entity: input_boolean.etat_ouverture_porte_sam
|
||||
name: Porte sam
|
||||
```
|
||||
|
||||
Vous pouvez personnaliser ce composant à l'aide du composant HACS card-mod pour ajuster les couleurs des alertes. Exemple pour afficher en rouge les alertes sécurité et délestage :
|
||||
|
||||
```
|
||||
card_mod:
|
||||
style: |
|
||||
{% if is_state('binary_sensor.thermostat_chambre_security_state', 'on') %}
|
||||
ha-card .body .sensor-heading ha-icon[icon="mdi:alert-outline"] {
|
||||
color: red;
|
||||
}
|
||||
{% endif %}
|
||||
{% if is_state('binary_sensor.thermostat_chambre_overpowering_state', 'on') %}
|
||||
ha-card .body .sensor-heading ha-icon[icon="mdi:flash"] {
|
||||
color: red;
|
||||
}
|
||||
{% endif %}
|
||||
```
|
||||

|
||||
|
||||
## Toujours mieux avec Apex-chart pour régler votre thermostat
|
||||
Vous pouvez obtenir une courbe comme celle présentée dans [some results](#some-results) avec une sorte de configuration de graphique Apex uniquement en utilisant les attributs personnalisés du thermostat décrits [ici](#custom-attributes) :
|
||||
|
||||
```
|
||||
type: custom:apexcharts-card
|
||||
header:
|
||||
show: true
|
||||
title: Tuning chauffage
|
||||
show_states: true
|
||||
colorize_states: true
|
||||
update_interval: 60sec
|
||||
graph_span: 4h
|
||||
yaxis:
|
||||
- id: left
|
||||
show: true
|
||||
decimals: 2
|
||||
- id: right
|
||||
decimals: 2
|
||||
show: true
|
||||
opposite: true
|
||||
series:
|
||||
- entity: climate.thermostat_mythermostat
|
||||
attribute: temperature
|
||||
type: line
|
||||
name: Target temp
|
||||
curve: smooth
|
||||
yaxis_id: left
|
||||
- entity: climate.thermostat_mythermostat
|
||||
attribute: current_temperature
|
||||
name: Current temp
|
||||
curve: smooth
|
||||
yaxis_id: left
|
||||
- entity: climate.thermostat_mythermostat
|
||||
attribute: on_percent
|
||||
name: Power percent
|
||||
curve: stepline
|
||||
yaxis_id: right
|
||||
```
|
||||
|
||||
## Et toujours de mieux en mieux avec l'AappDaemon NOTIFIER pour notifier les évènements
|
||||
Cette automatisation utilise l'excellente App Daemon nommée NOTIFIER développée par Horizon Domotique que vous trouverez en démonstration [ici](https://www.youtube.com/watch?v=chJylIK0ASo&ab_channel=HorizonDomotique) et le code est [ici](https://github.com/jlpouffier/home-assistant-config/blob/master/appdaemon/apps/notifier.py). Elle permet de notifier les utilisateurs du logement lorsqu'un des évènements touchant à la sécurité survient sur un des Versatile Thermostats.
|
||||
|
||||
C'est un excellent exemple de l'utilisation des notifications décrites ici [notification](#notifications).
|
||||
|
||||
```
|
||||
alias: Surveillance Mode Sécurité chauffage
|
||||
description: Envoi une notification si un thermostat passe en mode sécurité ou power
|
||||
trigger:
|
||||
- platform: event
|
||||
event_type: versatile_thermostat_security_event
|
||||
id: versatile_thermostat_security_event
|
||||
- platform: event
|
||||
event_type: versatile_thermostat_power_event
|
||||
id: versatile_thermostat_power_event
|
||||
- platform: event
|
||||
event_type: versatile_thermostat_temperature_event
|
||||
id: versatile_thermostat_temperature_event
|
||||
condition: []
|
||||
action:
|
||||
- choose:
|
||||
- conditions:
|
||||
- condition: trigger
|
||||
id: versatile_thermostat_security_event
|
||||
sequence:
|
||||
- event: NOTIFIER
|
||||
event_data:
|
||||
action: send_to_jmc
|
||||
title: >-
|
||||
Radiateur {{ trigger.event.data.name }} - {{
|
||||
trigger.event.data.type }} Sécurité
|
||||
message: >-
|
||||
Le radiateur {{ trigger.event.data.name }} est passé en {{
|
||||
trigger.event.data.type }} sécurité car le thermomètre ne répond
|
||||
plus.\n{{ trigger.event.data }}
|
||||
callback:
|
||||
- title: Stopper chauffage
|
||||
event: stopper_chauffage
|
||||
image_url: /media/local/alerte-securite.jpg
|
||||
click_url: /lovelace-chauffage/4
|
||||
icon: mdi:radiator-off
|
||||
tag: radiateur_security_alerte
|
||||
persistent: true
|
||||
- conditions:
|
||||
- condition: trigger
|
||||
id: versatile_thermostat_power_event
|
||||
sequence:
|
||||
- event: NOTIFIER
|
||||
event_data:
|
||||
action: send_to_jmc
|
||||
title: >-
|
||||
Radiateur {{ trigger.event.data.name }} - {{
|
||||
trigger.event.data.type }} Délestage
|
||||
message: >-
|
||||
Le radiateur {{ trigger.event.data.name }} est passé en {{
|
||||
trigger.event.data.type }} délestage car la puissance max est
|
||||
dépassée.\n{{ trigger.event.data }}
|
||||
callback:
|
||||
- title: Stopper chauffage
|
||||
event: stopper_chauffage
|
||||
image_url: /media/local/alerte-delestage.jpg
|
||||
click_url: /lovelace-chauffage/4
|
||||
icon: mdi:radiator-off
|
||||
tag: radiateur_power_alerte
|
||||
persistent: true
|
||||
- conditions:
|
||||
- condition: trigger
|
||||
id: versatile_thermostat_temperature_event
|
||||
sequence:
|
||||
- event: NOTIFIER
|
||||
event_data:
|
||||
action: send_to_jmc
|
||||
title: >-
|
||||
Le thermomètre du radiateur {{ trigger.event.data.name }} ne
|
||||
répond plus
|
||||
message: >-
|
||||
Le thermomètre du radiateur {{ trigger.event.data.name }} ne
|
||||
répond plus depuis longtemps.\n{{ trigger.event.data }}
|
||||
image_url: /media/local/thermometre-alerte.jpg
|
||||
click_url: /lovelace-chauffage/4
|
||||
icon: mdi:radiator-disabled
|
||||
tag: radiateur_thermometre_alerte
|
||||
persistent: true
|
||||
mode: queued
|
||||
max: 30
|
||||
```
|
||||
|
||||
|
||||
# Les contributions sont les bienvenues !
|
||||
|
||||
Si vous souhaitez contribuer, veuillez lire les [directives de contribution](CONTRIBUTING.md)
|
||||
|
||||
771
README.md
771
README.md
@@ -4,100 +4,745 @@
|
||||
[![hacs][hacs_badge]][hacs]
|
||||
[![BuyMeCoffee][buymecoffeebadge]][buymecoffee]
|
||||
|
||||
[En](README.md)|[Fr](README-fr.md)
|
||||

|
||||

|
||||
|
||||
>  This thermostat integration aims to greatly simplify your heating management automations. Since all typical heating events (nobody home?, activity detected in a room?, window open?, power load shedding?), are natively managed by the thermostat, you don’t need to deal with complicated scripts and automations to manage your thermostats. ;-).
|
||||
>  This thermostat integration aims to drastically simplify your automations around climate management. Because all classical events in climate are natively handled by the thermostat (nobody at home ?, activity detected in a room ?, window open ?, power shedding ?), you don't have to build over complicated scripts and automations to manage your climates ;-).
|
||||
|
||||
This custom component for Home Assistant is an upgrade and a complete rewrite of the "Awesome thermostat" component (see [Github](https://github.com/dadge/awesome_thermostat)) with added features.
|
||||
- [Thanks for the beer buymecoffee](#thanks-for-the-beer-buymecoffee)
|
||||
- [When to use / not use](#when-to-use--not-use)
|
||||
- [Why another thermostat implementation ?](#why-another-thermostat-implementation-)
|
||||
- [How to install this incredible Versatile Thermostat ?](#how-to-install-this-incredible-versatile-thermostat-)
|
||||
- [HACS installation (recommended)](#hacs-installation-recommended)
|
||||
- [Manual installation](#manual-installation)
|
||||
- [Configuration](#configuration)
|
||||
- [Minimal configuration update](#minimal-configuration-update)
|
||||
- [Select the driven entity](#select-the-driven-entity)
|
||||
- [Configure the TPI algorithm coefficients](#configure-the-tpi-algorithm-coefficients)
|
||||
- [Configure the preset temperature](#configure-the-preset-temperature)
|
||||
- [Configure the doors/windows turning on/off the thermostats](#configure-the-doorswindows-turning-onoff-the-thermostats)
|
||||
- [The sensor mode](#the-sensor-mode)
|
||||
- [Auto mode](#auto-mode)
|
||||
- [Configure the activity mode or motion detection](#configure-the-activity-mode-or-motion-detection)
|
||||
- [Configure the power management](#configure-the-power-management)
|
||||
- [Configure the presence or occupancy](#configure-the-presence-or-occupancy)
|
||||
- [Advanced configuration](#advanced-configuration)
|
||||
- [Examples tuning](#examples-tuning)
|
||||
- [Electrical heater](#electrical-heater)
|
||||
- [Central heating (gaz or fuel heating system)](#central-heating-gaz-or-fuel-heating-system)
|
||||
- [Temperature sensor will battery](#temperature-sensor-will-battery)
|
||||
- [Reactive temperature sensor (on mains)](#reactive-temperature-sensor-on-mains)
|
||||
- [My preset configuration](#my-preset-configuration)
|
||||
- [Algorithm](#algorithm)
|
||||
- [TPI algorithm](#tpi-algorithm)
|
||||
- [Sensors](#sensors)
|
||||
- [Services](#services)
|
||||
- [Force the presence / occupancy](#force-the-presence--occupancy)
|
||||
- [Change the temperature of presets](#change-the-temperature-of-presets)
|
||||
- [Change security settings](#change-security-settings)
|
||||
- [Notifications](#notifications)
|
||||
- [Custom attributes](#custom-attributes)
|
||||
- [Some results](#some-results)
|
||||
- [Even better](#even-better)
|
||||
- [Even Better with Scheduler Component !](#even-better-with-scheduler-component-)
|
||||
- [Even-even better with custom:simple-thermostat front integration](#even-even-better-with-customsimple-thermostat-front-integration)
|
||||
- [Even better with Apex-chart to tune your Thermostat](#even-better-with-apex-chart-to-tune-your-thermostat)
|
||||
- [And always better and better with the NOTIFIER daemon app to notify events](#and-always-better-and-better-with-the-notifier-daemon-app-to-notify-events)
|
||||
- [Contributions are welcome!](#contributions-are-welcome)
|
||||
|
||||
# What's new?
|
||||

|
||||
# What's New?
|
||||

|
||||
> * **Release 7.2**:
|
||||
>
|
||||
> - Native support for devices controlled via a `select` (or `input_select`) or `climate` entity for _VTherm_ of type `over_switch`. This update makes the creation of virtual switches obsolete for integrating Nodon, Heaty, eCosy, etc. More information [here](documentation/en/over-switch.md#command-customization).
|
||||
>
|
||||
> - Documentation links: Version 7.2 introduces experimental links to the documentation from the configuration pages. The link is accessible via the icon [](https://github.com/jmcollin78/versatile_thermostat/blob/main/documentation/en/over-switch.md#configuration). This feature is currently tested on some configuration pages.
|
||||
|
||||
# 🍻 Thanks for the beers [buymecoffee](https://www.buymeacoffee.com/jmcollin78) 🍻
|
||||
A big thank you to all my beer sponsors for their donations and encouragements. It means a lot to me and motivates me to keep going! If this integration has saved you money, buy me a beer in return; I would greatly appreciate it!
|
||||
This custom component for Home Assistant is an upgrade and is a complete rewrite of the component "Awesome thermostat" (see [Github](https://github.com/dadge/awesome_thermostat)) with addition of features.
|
||||
|
||||
# Glossary
|
||||
> _*News*_
|
||||
> * **Release 3.5**: Multiple thermostats when using "thermostat over another thermostat" mode [#113](https://github.com/jmcollin78/versatile_thermostat/issues/113)
|
||||
> * **Release 3.4**: bug fixes and expose preset temperatures for AC mode [#103](https://github.com/jmcollin78/versatile_thermostat/issues/103)
|
||||
> * **Release 3.3**: add the Air Conditionned mode (AC). This feature allow to use the eventual AC mode of your underlying climate entity. You have to check the "Use AC mode" checkbox in configuration and give preset temperature value for AC mode and AC mode when absent if absence is configured
|
||||
> * **Release 3.2**: add the ability to control multiple switches from the same thermostat. In this mode, the switches are triggered with a delay to minimize the power required at one time (we minimize the recovery periods). See [Configuration](#select-the-driven-entity)
|
||||
> * **Release 3.1**: added detection of open windows/doors by temperature drop. This new function makes it possible to automatically stop a radiator when the temperature drops suddenly. See [Auto mode](#auto-mode)
|
||||
> * **Major release 3.0**: addition of thermostat equipment and associated sensors (binary and non-binary). Much closer to the Home Assistant philosophy, you now have direct access to the energy consumed by the radiator controlled by the thermostat and many other sensors that will be useful in your automations and dashboard.
|
||||
> * **release 2.3**: addition of the power and energy measurement of the radiator controlled by the thermostat.
|
||||
> * **release 2.2**: addition of a safety function allowing a radiator not to be left heating forever in the event of a thermometer failure
|
||||
> * **major release 2.0**: addition of the "over climate" thermostat allowing you to transform any thermostat into a Versatile Thermostat and add all the functions of the latter.
|
||||
|
||||
_VTherm_: Versatile Thermostat as referred to in this document
|
||||
# Thanks for the beer [buymecoffee](https://www.buymeacoffee.com/jmcollin78)
|
||||
Many thanks to @salabur, @pvince83 and @bergoglio for the beers. It's very pleasing.
|
||||
|
||||
_TRV_: Thermostatic Radiator Valve equipped with a valve. The valve opens or closes to allow hot water to pass.
|
||||
|
||||
_AC_: Air Conditioning. An AC device cools instead of heats. Temperatures are reversed: Eco is warmer than Comfort, which is warmer than Boost. The algorithms take this information into account.
|
||||
# When to use / not use
|
||||
This thermostat can control 2 types of equipment:
|
||||
1. a heater that only works in on/off mode (named ```thermostat_over_switch```). The minimum configuration required to use this type of thermostat is:
|
||||
- an equipment such as a radiator (a ```switch``` or equivalent),
|
||||
- a temperature probe for the room (or an input_number),
|
||||
- an external temperature sensor (think about weather integration if you don't have one)
|
||||
2. another thermostat that has its own operating modes (named ```thermostat_over_climate```). For this type of thermostat, the minimum configuration requires:
|
||||
- an equipment such as air conditioning which is controlled by its own ```climate``` type entity,
|
||||
- a temperature probe for the room (or an input_number),
|
||||
- an external temperature sensor (think about weather integration if you don't have one)
|
||||
|
||||
_EMA_: Exponential Moving Average. Used to smooth sensor temperature measurements. It represents a moving average of the room's temperature and is used to calculate the slope of the temperature curve, which would be too unstable on the raw data.
|
||||
The ```thermostat_over_climate``` type allows you to add all the functionality provided by VersatileThermostat to your existing equipment. The climate VersatileThermostat entity will control your existing climate entity, turning it off if the windows are open, switching it to Eco mode if no one is present, etc. See [here](#why-a-new-implementation-of-the-thermostat). For this type of thermostat, any heating cycles are controlled by the underlying climate entity and not by the Versatile Thermostat itself.
|
||||
|
||||
_slope_: The slope of the temperature curve, measured in ° (C or K)/h. It is positive when the temperature increases and negative when it decreases. This slope is calculated based on the _EMA_.
|
||||
# Why another thermostat implementation ?
|
||||
|
||||
_PAC_: Heat pump
|
||||
|
||||
_HA_: Home Assistant
|
||||
This component named __Versatile thermostat__ manage the following use cases :
|
||||
- Configuration through standard integration GUI (using Config Entry flow),
|
||||
- Full uses of **presets mode**,
|
||||
- Unset the preset mode when the temperature is **manually defined** on a thermostat,
|
||||
- Turn off/on a thermostat when a **door or windows is opened/closed** after a certain delay,
|
||||
- Change preset when an **activity is detected** or not in a room for a defined time,
|
||||
- Use a **TPI (Time Proportional Interval) algorithm** thank's to [[Argonaute](https://forum.hacf.fr/u/argonaute/summary)] algorithm ,
|
||||
- Add **power shedding management** or regulation to avoid exceeding a defined total power. When max power is exceeded, a hidden 'power' preset is set on the climate entity. When power goes below the max, the previous preset is restored.
|
||||
- Add **home presence management**. This feature allows you to dynamically change the temperature of preset considering a occupancy sensor of your home.
|
||||
- Add **services to interact with the thermostat** from others integration: you can force the presence / un-presence using a service, and you can dynamically change the temperature of the presets and change dynamically the security parameters.
|
||||
- Add sensors to see the internal states of the thermostat
|
||||
|
||||
_underlying_: the device controlled by _VTherm_
|
||||
# How to install this incredible Versatile Thermostat ?
|
||||
|
||||
# Documentation
|
||||
## HACS installation (recommended)
|
||||
|
||||
The documentation is now divided into several pages for easier reading and searching:
|
||||
1. [Introduction](documentation/en/presentation.md),
|
||||
2. [Installation](documentation/en/installation.md),
|
||||
3. [Démarrage rapide](documentation/en/quick-start.md)
|
||||
4. [Choosing a VTherm type](documentation/en/creation.md),
|
||||
5. [Basic attributes](documentation/en/base-attributes.md)
|
||||
6. [Configuring a VTherm on a `switch`](documentation/en/over-switch.md)
|
||||
7. [Configuring a VTherm on a `climate`](documentation/en/over-climate.md)
|
||||
8. [Configuring a VTherm on a valve](documentation/en/over-valve.md)
|
||||
9. [Presets](documentation/en/feature-presets.md)
|
||||
10. [Window management](documentation/en/feature-window.md)
|
||||
11. [Presence management](documentation/en/feature-presence.md)
|
||||
12. [Motion management](documentation/en/feature-motion.md)
|
||||
13. [Power management](documentation/en/feature-power.md)
|
||||
14. [Auto start and stop](documentation/en/feature-auto-start-stop.md)
|
||||
15. [Centralized control of all VTherms](documentation/en/feature-central-mode.md)
|
||||
16. [Central heating control](documentation/en/feature-central-boiler.md)
|
||||
17. [Advanced aspects, security mode](documentation/en/feature-advanced.md)
|
||||
18. [Self-regulation](documentation/en/self-regulation.md)
|
||||
19. [Tuning examples](documentation/en/tuning-examples.md)
|
||||
20. [Algorithms](documentation/en/algorithms.md)
|
||||
21. [Reference documentation](documentation/en/reference.md)
|
||||
22. [Tuning examples](documentation/en/tuning-examples.md)
|
||||
23. [Troubleshooting](documentation/en/troubleshooting.md)
|
||||
24. [Release notes](documentation/en/releases.md)
|
||||
1. Install [HACS](https://hacs.xyz/). That way you get updates automatically.
|
||||
2. Add this Github repository as custom repository in HACS settings.
|
||||
3. search and install "Versatile Thermostat" in HACS and click `install`.
|
||||
4. Restart Home Assistant,
|
||||
5. Then you can add an Versatile Thermostat integration in the integration page. You add as many Versatile Thermostat that you need (typically one per heater that should be managed)
|
||||
|
||||
## Manual installation
|
||||
|
||||
1. Using the tool of choice open the directory (folder) for your HA configuration (where you find `configuration.yaml`).
|
||||
2. If you do not have a `custom_components` directory (folder) there, you need to create it.
|
||||
3. In the `custom_components` directory (folder) create a new folder called `versatile_thermostat`.
|
||||
4. Download _all_ the files from the `custom_components/versatile_thermostat/` directory (folder) in this repository.
|
||||
5. Place the files you downloaded in the new directory (folder) you created.
|
||||
6. Restart Home Assistant
|
||||
7. Configure new Versatile Thermostat integration
|
||||
|
||||
|
||||
# Configuration
|
||||
|
||||
Note: no configuration in configuration.yaml is needed because all configuration is done through the standard GUI when adding the integration.
|
||||
|
||||
Click on Add integration button in the integration page
|
||||

|
||||
|
||||
The configuration can be change through the same interface. Simply select the thermostat to change, hit "Configure" and you will be able to change some parameters or configuration.
|
||||
|
||||
Then follow the configurations steps as follow:
|
||||
|
||||
## Minimal configuration update
|
||||

|
||||
|
||||
Give the main mandatory attributes:
|
||||
1. a name (will be the name of the integration and also the name of the climate entity)
|
||||
2. the type of thermostat ```thermostat_over_switch``` to control a radiator controlled by a switch or ```thermostat_over_climate``` to control another thermostat. Cf. [above](#why-a-new-thermostat-implementation)
|
||||
4. a temperature sensor entity identifier which gives the temperature of the room in which the radiator is installed,
|
||||
5. a temperature sensor entity giving the outside temperature. If you don't have an external sensor, you can use local weather integration
|
||||
6. a cycle duration in minutes. On each cycle, the heater will cycle on and then off for a calculated time to reach the target temperature (see [preset](#configure-the-preset-temperature) below),
|
||||
7. minimum and maximum thermostat temperatures,
|
||||
8. the power of the l'équipement which will activate the power and energy sensors of the device,
|
||||
9. the list of features that will be used for this thermostat. Depending on your choices, the following configuration screens will appear or not.
|
||||
|
||||
>  _*Notes*_
|
||||
1. With the ```thermostat_over_switch``` type, calculation are done at each cycle. So in case of conditions change, you will have to wait for the next cycle to see a change. For this reason, the cycle should not be too long. **5 min is a good value**,
|
||||
2. if the cycle is too short, the heater could never reach the target temperature indeed for heater with accumulation features and it will be unnecessary solicited
|
||||
|
||||
## Select the driven entity
|
||||
Depending on your choice on the type of thermostat, you will have to choose a switch type entity or a climate type entity. Only compatible entities are shown.
|
||||
|
||||
For a ```thermostat_over_switch``` thermostat:
|
||||

|
||||
The algorithm to be used today is limited to TPI is available. See [algorithm](#algorithm)
|
||||
If several type entities are configured, the thermostat staggers the activations in order to minimize the number of active switches at a time t. This allows a better distribution of power since each radiator will turn on in turn.
|
||||
Example of synchronized triggering:
|
||||

|
||||
|
||||
For a ```thermostat_over_climate``` thermostat:
|
||||

|
||||
|
||||
## Configure the TPI algorithm coefficients
|
||||
Click on 'Validate' on the previous page and you will get there:
|
||||

|
||||
|
||||
For more informations on the TPI algorithm and tuned please refer to [algorithm](#algorithm).
|
||||
|
||||
## Configure the preset temperature
|
||||
Click on 'Validate' on the previous page and you will get there:
|
||||

|
||||
|
||||
The preset mode allows you to pre-configurate targeted temperature. Used in conjonction with Scheduler (see [scheduler](#even-better-with-scheduler-component) you will have a powerfull and simple way to optimize the temperature vs electrical consumption of your hous. Preset handled are the following :
|
||||
- **Eco** : device is running an energy-saving mode
|
||||
- **Comfort** : device is in comfort mode
|
||||
- **Boost** : device turn all valve full up
|
||||
|
||||
**None** is always added in the list of modes, as it is a way to not use the presets modes but a **manual temperature** instead.
|
||||
|
||||
>  _*Notes*_
|
||||
1. Changing manually the target temperature, set the preset to None (no preset). This way you can always set a target temperature even if no preset are available.
|
||||
2. standard ``Away`` preset is a hidden preset which is not directly selectable. Versatile Thermostat uses the presence management or movement management to set automatically and dynamically the target temperature depending on a presence in the home or an activity in the room. See [presence management](#configure-the-presence-management).
|
||||
3. if you uses the power shedding management, you will see a hidden preset named ``power``. The heater preset is set to ``power`` when overpowering conditions are encountered and shedding is active for this heater. See [power management](#configure-the-power-management).
|
||||
4. if you uses the advanced configuration you will see the preset set to ``security`` if the temperature could not be retrieved after a certain delay
|
||||
5. ff you don't want to use the preseet, give 0 as temperature. The preset will then been ignored and will not displayed in the front component
|
||||
|
||||
## Configure the doors/windows turning on/off the thermostats
|
||||
You must have chosen the ```With opening detection``` feature on the first page to arrive on this page.
|
||||
The detection of openings can be done in 2 ways:
|
||||
1. either with a sensor placed on the opening (sensor mode),
|
||||
2. either by detecting a sudden drop in temperature (auto mode)
|
||||
|
||||
### The sensor mode
|
||||
In sensor mode, you must fill in the following information:
|
||||

|
||||
|
||||
1. an entity ID of a **window/door sensor**. It should be a binary_sensor or an input_boolean. The state of the entity must be 'on' when the window is open or 'off' when it is closed
|
||||
2. a **delay in seconds** before any change. This allows a window to be opened quickly without stopping the heating.
|
||||
|
||||
### Auto mode
|
||||
In auto mode, the configuration is as follows:
|
||||

|
||||
|
||||
1. a detection threshold in degrees per minute. When the temperature drops below this threshold, the thermostat will turn off. The lower this value, the faster the detection will be (in return for a risk of false positives),
|
||||
2. an end of detection threshold in degrees per minute. When the temperature drop goes above this value, the thermostat will go back to the previous mode (mode and preset),
|
||||
3. maximum detection time. Beyond this time, the thermostat will return to its previous mode and preset even if the temperature continues to drop.
|
||||
|
||||
To set the thresholds it is advisable to start with the reference values and adjust the detection thresholds. A few tries gave me the following values (for a desktop):
|
||||
- detection threshold: 0.05°C/min
|
||||
- non-detection threshold: 0 °C/min
|
||||
- maximum duration: 60 min.
|
||||
|
||||
A new "slope" sensor has been added for all thermostats. It gives the slope of the temperature curve in °C/min (or °K/min). This slope is smoothed and filtered to avoid aberrant values from the thermometers which would interfere with the measurement.
|
||||

|
||||
|
||||
To properly adjust it is advisable to display on the same historical graph the temperature curve and the slope of the curve (the "slope"):
|
||||

|
||||
|
||||
And that's all ! your thermostat will turn off when the windows are open and turn back on when closed.
|
||||
|
||||
>  _*Notes*_
|
||||
1. If you want to use **multiple door/window sensors** to automate your thermostat, just create a group with the usual behavior (https://www.home-assistant.io/integrations/binary_sensor.group/)
|
||||
2. If you don't have a window/door sensor in your room, just leave the sensor entity id blank,
|
||||
3. **Only one mode is allowed**. You cannot configure a thermostat with a sensor and automatic detection. The 2 modes may contradict each other, it is not possible to have the 2 modes at the same time,
|
||||
4. It is not recommended to use the automatic mode for equipment subject to frequent and normal temperature variations (corridors, open areas, ...)
|
||||
|
||||
## Configure the activity mode or motion detection
|
||||
If you choose the ```Motion management``` feature, lick on 'Validate' on the previous page and you will get there:
|
||||

|
||||
|
||||
We will now see how to configure the new Activity mode.
|
||||
What we need:
|
||||
- a **motion sensor**. The entity id of a motion sensor. Motion sensor states should be 'on' (motion detected) or 'off' (no motion detected)
|
||||
- a **motion delay** (in seconds) duration defining how long we wait for motion confirmation before considering the motion
|
||||
- a **target "motion" preset**. We will used the temperature of this preset when an activity is detected.
|
||||
- a **target "no motion" preset**. We will used the temperature of this second preset when no activity is detected.
|
||||
|
||||
So imagine we want to have the following behavior :
|
||||
- we have room with a thermostat set in activity mode, the "motion" mode chosen is comfort (21.5C), the "no motion" mode chosen is Eco (18.5 C) and the motion delay is 5 min.
|
||||
- the room is empty for a while (no activity detected), the temperature of this room is 18.5 C
|
||||
- somebody enters into the room, an activity is detected the temperature is set to 21.5 C
|
||||
- the person leaves the room, after 5 min the temperature is set back to 18.5 C
|
||||
|
||||
For this to work, the climate thermostat should be in ``Activity`` preset mode.
|
||||
|
||||
>  _*Notes*_
|
||||
1. Be aware that as for the others preset modes, ``Activity`` will only be proposed if it's correctly configure. In other words, the 4 configuration keys have to be set if you want to see Activity in home assistant Interface
|
||||
|
||||
## Configure the power management
|
||||
|
||||
If you choose the ```Power management``` feature, click on 'Validate' on the previous page and you will get there:
|
||||

|
||||
|
||||
This feature allows you to regulate the power consumption of your radiators. Known as shedding, this feature allows you to limit the electrical power consumption of your heater if overpowering conditions are detected. Give a **sensor to the current power consumption of your house**, a **sensor to the max power** that should not be exceeded, the **power consumption of your heater** (in the first step of the configuration) and the algorithm will not start a radiator if the max power will be exceeded after radiator starts.
|
||||
|
||||
|
||||
Note that all power values should have the same units (kW or W for example).
|
||||
This allows you to change the max power along time using a Scheduler or whatever you like.
|
||||
|
||||
>  _*Notes*_
|
||||
1. When shedding is encountered, the heater is set to the preset named ``power``. This is a hidden preset, you cannot select it manually.
|
||||
2. I use this to avoid exceeded the limit of my electrical power contract when an electrical vehicle is charging. This makes a kind of auto-regulation.
|
||||
3. Always keep a margin, because max power can be briefly exceeded while waiting for the next cycle calculation typically or by not regulated equipement.
|
||||
4. If you don't want to use this feature, just leave the entities id empty
|
||||
|
||||
## Configure the presence or occupancy
|
||||
If you choose the ```Presence management``` feature, this feature allows you to dynamically changes the temperature of all configured Versatile thermostat's presets when nobody is at home or when someone comes back home. For this, you have to configure the temperature that will be used for each preset when presence is off. When the occupancy sensor turns to off, those tempoeratures will be used. When it turns on again the "normal" temperature configured for the preset is used. See [preset management](#configure-the-preset-temperature).
|
||||
To configure presence fills this form:
|
||||
|
||||

|
||||
|
||||
For this you need to configure:
|
||||
1. A **occupancy sensor** which state should be 'on' or 'home' if someone is present or 'off' or 'not_home' else,
|
||||
2. The **temperature used in Eco** preset when absent,
|
||||
3. The **temperature used in Comfort** preset when absent,
|
||||
4. The **temperature used in Boost** preset when absent
|
||||
|
||||
>  _*Notes*_
|
||||
1. the switch of temperature is immediate and is reflected on the front component. The calculation will take the new target temperature into account at the next cycle calculation,
|
||||
2. you can use direct person.xxxx sensor or group of sensors of Home Assistant. The presence sensor handles ``on`` or ``home`` states as present and ``off`` or ``not_home`` state as absent.
|
||||
|
||||
## Advanced configuration
|
||||
Those parameters allows to fine tune the thermostat.
|
||||
The advanced configuration form is the following:
|
||||
|
||||

|
||||
|
||||
The first delay (minimal_activation_delay_sec) in sec in the minimum delay acceptable for turning on the heater. When calculation gives a power on delay below this value, the heater will stays off.
|
||||
|
||||
The second delay (security_delay_min) is the maximal delay between two temperature measure before setting the preset to ``security`` and turning off the thermostat. If the temperature sensor is no more giving temperature measures, the thermostat and heater will turns off after this delay and the preset of the thermostat will be set to ``security``. This is useful to avoid overheating is the battery of your temperature sensor is too low.
|
||||
|
||||
The third parameter (``security_min_on_percent``) is the minimum value of ``on_percent`` below which the security preset will not be activated. This parameter makes it possible not to put a thermostat in safety, if the controlled radiator does not heat sufficiently.
|
||||
Setting this parameter to ``0.00`` will trigger the security preset regardless of the last heating setpoint, conversely ``1.00`` will never trigger the security preset (which amounts to disabling the function).
|
||||
|
||||
The fourth parameter (``security_default_on_percent``) is the ``on_percent`` value that will be used when the thermostat enters ``security`` mode. If you put ``0`` then the thermostat will be cut off when it goes into ``security`` mode, putting 0.2% for example allows you to keep a little heating (20% in this case), even in mode ``security``. It avoids finding your home totally frozen during a thermometer failure.
|
||||
|
||||
See [example tuning](#examples-tuning) for common tuning examples
|
||||
|
||||
> _*Notes*_
|
||||
1. When the temperature sensor comes to life and returns the temperatures, the preset will be restored to its previous value,
|
||||
3. Attention, two temperatures are needed: internal temperature and external temperature and each must give the temperature, otherwise the thermostat will be in "security" preset,
|
||||
4. A service is available that allows you to set the 3 security parameters. This can be used to adapt the security function to your use.
|
||||
5. For natural usage, the ``security_default_on_percent`` should be less than ``security_min_on_percent``,
|
||||
6. When a ``thermostat_over_climate`` type thermostat goes into ``security`` mode it is turned off. The ``security_min_on_percent`` and ``security_default_on_percent`` parameters are then not used.
|
||||
|
||||
# Examples tuning
|
||||
|
||||
## Electrical heater
|
||||
- cycle: between 5 and 10 minutes,
|
||||
- minimal_activation_delay_sec: 30 seconds
|
||||
|
||||
## Central heating (gaz or fuel heating system)
|
||||
- cycle: between 30 and 60 min,
|
||||
- minimal_activation_delay_sec: 300 seconds (because of the response time)
|
||||
|
||||
## Temperature sensor will battery
|
||||
- security_delay_min: 60 min (because these sensors are lazy)
|
||||
- security_min_on_percent: 0.5 (50% - we go to the ``security`` preset if the radiator was heating more than 50% of the time)
|
||||
- security_default_on_percent: 0.1 (10% - in preset ``security``, we keep a heating background 20% of the time)
|
||||
|
||||
These settings should be understood as follows:
|
||||
|
||||
> If the thermometer no longer sends the temperature for 1 hour and the heating percentage (``on_percent``) was greater than 50%, then this heating percentage is reduced to 10%.
|
||||
|
||||
It's up to you to adapt these settings to your case!
|
||||
|
||||
What is important is not to take too many risks with these parameters: suppose you are away for a long period, that the batteries of your thermometer reach the end of their life, your radiator will heat up 10% of the time for the whole the duration of the outage.
|
||||
|
||||
Versatile Thermostat allows you to be notified when an event of this type occurs. Set up the alerts that go well as soon as you use this thermostat. See (#notifications)
|
||||
|
||||
## Reactive temperature sensor (on mains)
|
||||
- security_delay_min: 15min
|
||||
- security_min_on_percent: 0.7 (70% - we go to the ``security`` preset if the radiator was heating more than 70% of the time)
|
||||
- security_default_on_percent: 0.25 (25% - in preset ``security``, we keep a heating background 25% of the time)
|
||||
|
||||
## My preset configuration
|
||||
This is just an example of how I use the preset. It up to you to adapt to your configuration but it can be useful to understand how it works.
|
||||
``Eco``: 17 °C
|
||||
``Comfort``: 19 °C
|
||||
``Boost``: 20 °C
|
||||
|
||||
When presence if off:
|
||||
``Eco``: 16.5 °C
|
||||
``Comfort``: 17 °C
|
||||
``Boost``: 18 °C
|
||||
|
||||
Motion detector in my office is set to use ``Boost`` when motion is detected and ``Eco`` if not.
|
||||
|
||||
# Algorithm
|
||||
This integration uses a proportional algorithm. A Proportional algorithm is useful to avoid the oscillation around the target temperature. This algorithm is based on a cycle which alternate heating and stop heating. The proportion of heating vs not heating is determined by the difference between the temperature and the target temperature. Bigger the difference is and bigger is the proportion of heating inside the cycle.
|
||||
|
||||
This algorithm make the temperature converge and stop oscillating.
|
||||
|
||||
## TPI algorithm
|
||||
The TPI algorithm consist in the calculation at each cycle of a percentage of On state vs Off state for the heater using the target temperature, the current temperature in the room and the current external temperature.
|
||||
|
||||
The percentage is calculated with this formula:
|
||||
|
||||
on_percent = coef_int * (target temperature - current temperature) + coef_ext * (target temperature - external temperature)
|
||||
Then make 0 <= on_percent <= 1
|
||||
|
||||
Defaults values for coef_int and coef_ext are respectively: ``0.6`` and ``0.01``. Those defaults values are suitable for a standard well isolated room.
|
||||
|
||||
To tune those coefficients keep in mind that:
|
||||
1. **if target temperature is not reach** after stable situation, you have to augment the ``coef_ext`` (the ``on_percent`` is too low),
|
||||
2. **if target temperature is exceeded** after stable situation, you have to decrease the ``coef_ext`` (the ``on_percent`` is too high),
|
||||
3. **if reaching the target temperature is too slow**, you can increase the ``coef_int`` to give more power to the heater,
|
||||
4. **if reaching the target temperature is too fast and some oscillations appears** around the target, you can decrease the ``coef_int`` to give less power to the heater
|
||||
|
||||
See some situations at [examples](#some-results).
|
||||
|
||||
# Sensors
|
||||
|
||||
With the thermostat are available sensors that allow you to view the alerts and the internal status of the thermostat. They are available in the entities of the device associated with the thermostat:
|
||||
|
||||

|
||||
|
||||
In order, there are:
|
||||
1. the main climate thermostat command entity,
|
||||
2. the energy consumed by the thermostat (value which continuously increases),
|
||||
3. the time of receipt of the last outside temperature,
|
||||
4. the time of receipt of the last indoor temperature,
|
||||
5. the average power of the device over the cycle (for TPIs only),
|
||||
6. the time spent in the off state in the cycle (TPI only),
|
||||
7. the time spent in the on state in the cycle (TPI only),
|
||||
8. load shedding status,
|
||||
9. cycle power percentage (TPI only),
|
||||
10. presence status (if presence management is configured),
|
||||
11. security status,
|
||||
12. opening status (if opening management is configured),
|
||||
13. motion status (if motion management is configured)
|
||||
|
||||
To color the sensors, add these lines and customize them as needed, in your configuration.yaml:
|
||||
|
||||
```
|
||||
frontend:
|
||||
themes:
|
||||
versatile_thermostat_theme:
|
||||
state-binary_sensor-safety-on-color: "#FF0B0B"
|
||||
state-binary_sensor-power-on-color: "#FF0B0B"
|
||||
state-binary_sensor-window-on-color: "rgb(156, 39, 176)"
|
||||
state-binary_sensor-motion-on-color: "rgb(156, 39, 176)"
|
||||
state-binary_sensor-presence-on-color: "lightgreen"
|
||||
```
|
||||
and choose the ```versatile_thermostat_theme``` theme in the panel configuration. You will get something that will look like this:
|
||||
|
||||

|
||||
|
||||
# Services
|
||||
|
||||
This custom implementation offers some specific services to facilitate integration with others Home Assisstant components.
|
||||
|
||||
## Force the presence / occupancy
|
||||
This service allows you to force the presence status independantly of the presence sensor. This can be useful if you want to manage the presence through a service and not through a sensor. For example, you could use your alarm to force the absence when it is switched on.
|
||||
|
||||
The code to call this service is the following:
|
||||
```
|
||||
service: versatile_thermostat.set_presence
|
||||
data:
|
||||
presence: "off"
|
||||
target:
|
||||
entity_id: climate.my_thermostat
|
||||
```
|
||||
|
||||
## Change the temperature of presets
|
||||
This services is useful if you want to dynamically change the preset temperature. Instead of changing preset, some use-case need to change the temperature of the preset. So you can keep the Scheduler unchanged to manage the preset and adjust the temperature of the preset.
|
||||
If the changed preset is currently selectionned, the modification of the target temperature is immediate and will be taken into account at the next calculation cycle.
|
||||
|
||||
You can change the one or the both temperature (when present or when absent) of each preset.
|
||||
|
||||
Use the following code the set the temperature of the preset:
|
||||
```
|
||||
service: versatile_thermostat.set_preset_temperature
|
||||
data:
|
||||
preset: boost
|
||||
temperature: 17.8
|
||||
temperature_away: 15
|
||||
target:
|
||||
entity_id: climate.my_thermostat
|
||||
```
|
||||
|
||||
>  _*Notes*_
|
||||
- after a restart the preset are resetted to the configured temperature. If you want your change to be permanent you should modify the temperature preset into the confguration of the integration.
|
||||
|
||||
## Change security settings
|
||||
This service is used to dynamically modify the security parameters described here [Advanced configuration](#configuration-avanced).
|
||||
If the thermostat is in ``security`` mode the new settings are applied immediately.
|
||||
|
||||
To change the security settings use the following code:
|
||||
```
|
||||
service : thermostat_polyvalent.set_security
|
||||
date:
|
||||
min_on_percent: "0.5"
|
||||
default_on_percent: "0.1"
|
||||
delay_min: 60
|
||||
target:
|
||||
entity_id : climate.my_thermostat
|
||||
```
|
||||
|
||||
# Notifications
|
||||
Significant thermostat events are notified via the message bus.
|
||||
The notified events are as follows:
|
||||
|
||||
- ``versatile_thermostat_security_event``: a thermostat enters or exits the ``security`` preset
|
||||
- ``versatile_thermostat_power_event``: a thermostat enters or exits the ``power`` preset
|
||||
- ``versatile_thermostat_temperature_event``: one or both temperature measurements of a thermostat have not been updated for more than ``security_delay_min`` minutes
|
||||
- ``versatile_thermostat_hvac_mode_event``: the thermostat is on or off. This event is also broadcast when the thermostat starts up
|
||||
- ``versatile_thermostat_preset_event``: a new preset is selected on the thermostat. This event is also broadcast when the thermostat starts up
|
||||
|
||||
If you have followed correctly, when a thermostat goes into safety mode, 3 events are triggered:
|
||||
1. ``versatile_thermostat_temperature_event`` to indicate that a thermometer has become unresponsive,
|
||||
2. ``versatile_thermostat_preset_event`` to indicate the switch to ```security``` preset,
|
||||
3. ``versatile_thermostat_hvac_mode_event`` to indicate the possible extinction of the thermostat
|
||||
|
||||
Each event carries the key values of the event (temperatures, current preset, current power, etc.) as well as the states of the thermostat.
|
||||
|
||||
You can very easily capture its events in an automation, for example to notify users.
|
||||
|
||||
# Custom attributes
|
||||
|
||||
To tune the algorithm you have access to all context seen and calculted by the thermostat through dedicated attributes. You can see (and use) those attributes in the "Development tools / states" HMI of HA. Enter your thermostat and you will see something like this:
|
||||

|
||||
|
||||
Custom attributes are the following:
|
||||
|
||||
| Attribute | Meaning |
|
||||
| ----------| --------|
|
||||
| ``hvac_modes`` | The list of modes supported by the thermostat |
|
||||
| ``min_temp`` | The minimal temperature |
|
||||
| ``max_temp`` | The maximal temperature |
|
||||
| ``preset_modes`` | The presets visible for this thermostat. Hidden presets are not showed here |
|
||||
| ``current_temperature`` | The current temperature as reported by the sensor |
|
||||
| ``temperature`` | The target temperature |
|
||||
| ``hvac_action`` | The action currently running by the heater. Can be idle, heating |
|
||||
| ``preset_mode`` | The currently selected preset. Can be one of the 'preset_modes' or a hidden preset like power |
|
||||
| ``[eco/comfort/boost]_temp`` | The temperature configured for the preset xxx |
|
||||
| ``[eco/comfort/boost]_away_temp`` | The temperature configured for the preset xxx when presence is off or not_home |
|
||||
| ``power_temp`` | The temperature used when shedding is detected |
|
||||
| ``on_percent`` | (deprecated) The percentage on calculated by the TPI algorithm |
|
||||
| ``on_time_sec`` | (deprecated) The On period in sec. Should be ```on_percent * cycle_min``` |
|
||||
| ``off_time_sec`` | (deprecated) The Off period in sec. Should be ```(1 - on_percent) * cycle_min``` |
|
||||
| ``cycle_min`` | The calculation cycle in minutes |
|
||||
| ``function`` | The algorithm used for cycle calculation |
|
||||
| ``tpi_coef_int`` | The ``coef_int`` of the TPI algorithm |
|
||||
| ``tpi_coef_ext`` | The ``coef_ext`` of the TPI algorithm |
|
||||
| ``saved_preset_mode`` | The last preset used before automatic switch of the preset |
|
||||
| ``saved_target_temp`` | The last temperature used before automatic switching |
|
||||
| ``window_state`` | (deprecated) The last known state of the window sensor. None if window is not configured |
|
||||
| ``motion_state`` | (deprecated) The last known state of the motion sensor. None if motion is not configured |
|
||||
| ``overpowering_state`` | (deprecated) The last known state of the overpowering sensor. None if power management is not configured |
|
||||
| ``presence_state`` | (deprecated) The last known state of the presence sensor. None if presence management is not configured |
|
||||
| ``security_delay_min`` | The delay before setting the security mode when temperature sensor are off |
|
||||
| ``security_min_on_percent`` | The minimal on_percent below which security preset won't be trigger |
|
||||
| ``security_default_on_percent`` | The on_percent used when thermostat is in ``security`` |
|
||||
| ``last_temperature_datetime`` | (deprecated) The date and time in ISO8866 format of the last internal temperature reception |
|
||||
| ``last_ext_temperature_datetime`` | (deprecated) The date and time in ISO8866 format of the last external temperature reception |
|
||||
| ``security_state`` | (deprecated) The security state. true or false |
|
||||
| ``minimal_activation_delay_sec`` | The minimal activation delay in seconds |
|
||||
| ``last_update_datetime`` | The date and time in ISO8866 format of this state |
|
||||
| ``friendly_name`` | The name of the thermostat |
|
||||
| ``supported_features`` | A combination of all features supported by this thermostat. See official climate integration documentation for more informations |
|
||||
|
||||
# Some results
|
||||
|
||||
**Temperature stability around the target configured by preset:**
|
||||
**Convergence of temperature to target configured by preset:**
|
||||

|
||||
|
||||

|
||||
[Cycle of on/off calculated by the integration:](https://)
|
||||

|
||||
|
||||
On/off cycles calculated by the integration (`over_climate`):
|
||||

|
||||
**Coef_int too high (oscillations around the target)**
|
||||

|
||||
|
||||
**Regulation with an `over_switch`**
|
||||
**Algorithm calculation evolution**
|
||||

|
||||
See the code of this component [[below](#even-better-with-apex-chart-to-tune-your-thermostat)]
|
||||
|
||||

|
||||
**Fine tuned thermostat**
|
||||
Thank's [impuR_Shozz](https://forum.hacf.fr/u/impur_shozz/summary) !
|
||||
We can see stability around the target temperature (consigne) and when at target the on_percent (puissance) is near 0.3 which seems a very good value.
|
||||
|
||||
See the component's code [[below](#even-better-with-apex-chart-to-tune-your-thermostat)]
|
||||

|
||||
|
||||
**Strong regulation in `over_climate`**
|
||||
Enjoy !
|
||||
|
||||

|
||||
# Even better
|
||||
|
||||
**Regulation with direct valve control in `over_climate`**
|
||||
## Even Better with Scheduler Component !
|
||||
|
||||

|
||||
In order to enjoy the full power of Versatile Thermostat, I invite you to use it with https://github.com/nielsfaber/scheduler-component
|
||||
Indeed, the scheduler component porpose a management of the climate base on the preset modes. This feature has limited interest with the generic thermostat but it becomes highly powerfull with Awesome thermostat :
|
||||
|
||||
Enjoy!
|
||||
Starting here, I assume you have installed Awesome Thermostat and Scheduler Component.
|
||||
|
||||
In Scheduler, add a schedule :
|
||||
|
||||

|
||||
|
||||
Choose "climate" group, choose one (or multiple) entity/ies, select "MAKE SCHEME" and click next :
|
||||
(it is possible to choose "SET PRESET", but I prefer to use "MAKE SCHEME")
|
||||
|
||||

|
||||
|
||||
Set your mode scheme and save :
|
||||
|
||||
|
||||

|
||||
|
||||
In this example I set ECO mode during the night and the day when nobody's at home BOOST in the morning and COMFORT in the evening.
|
||||
|
||||
|
||||
I hope this example helps you, don't hesitate to give me your feedbacks !
|
||||
|
||||
## Even-even better with custom:simple-thermostat front integration
|
||||
The ``custom:simple-thermostat`` [here](https://github.com/nervetattoo/simple-thermostat) is a great integration which allow some customisation which fits well with this thermostat.
|
||||
You can have something like that very easily 
|
||||
Example configuration:
|
||||
|
||||
```
|
||||
type: custom:simple-thermostat
|
||||
entity: climate.thermostat_sam2
|
||||
layout:
|
||||
step: row
|
||||
label:
|
||||
temperature: T°
|
||||
state: Etat
|
||||
hide:
|
||||
state: false
|
||||
control:
|
||||
hvac:
|
||||
_name: Mode
|
||||
preset:
|
||||
_name: Preset
|
||||
sensors:
|
||||
- entity: sensor.total_puissance_radiateur_sam2
|
||||
icon: mdi:lightning-bolt-outline
|
||||
header:
|
||||
toggle:
|
||||
entity: input_boolean.etat_ouverture_porte_sam
|
||||
name: Porte sam
|
||||
```
|
||||
You can customize this component using the HACS card-mod component to adjust the alert colors. Example for displaying safety and load shedding alerts in red:
|
||||
|
||||
```
|
||||
card_mod:
|
||||
style: |
|
||||
{% if is_state('binary_sensor.thermostat_chambre_security_state', 'on') %}
|
||||
ha-card .body .sensor-heading ha-icon[icon="mdi:alert-outline"] {
|
||||
color: red;
|
||||
}
|
||||
{% endif %}
|
||||
{% if is_state('binary_sensor.thermostat_chambre_overpowering_state', 'on') %}
|
||||
ha-card .body .sensor-heading ha-icon[icon="mdi:flash"] {
|
||||
color: red;
|
||||
}
|
||||
{% endif %}
|
||||
```
|
||||

|
||||
|
||||
## Even better with Apex-chart to tune your Thermostat
|
||||
You can get curve like presented in [some results](#some-results) with kind of Apex-chart configuration only using the custom attributes of the thermostat described [here](#custom-attributes):
|
||||
|
||||
```
|
||||
type: custom:apexcharts-card
|
||||
header:
|
||||
show: true
|
||||
title: Tuning chauffage
|
||||
show_states: true
|
||||
colorize_states: true
|
||||
update_interval: 60sec
|
||||
graph_span: 4h
|
||||
yaxis:
|
||||
- id: left
|
||||
show: true
|
||||
decimals: 2
|
||||
- id: right
|
||||
decimals: 2
|
||||
show: true
|
||||
opposite: true
|
||||
series:
|
||||
- entity: climate.thermostat_mythermostat
|
||||
attribute: temperature
|
||||
type: line
|
||||
name: Target temp
|
||||
curve: smooth
|
||||
yaxis_id: left
|
||||
- entity: climate.thermostat_mythermostat
|
||||
attribute: current_temperature
|
||||
name: Current temp
|
||||
curve: smooth
|
||||
yaxis_id: left
|
||||
- entity: climate.thermostat_mythermostat
|
||||
attribute: on_percent
|
||||
name: Power percent
|
||||
curve: stepline
|
||||
yaxis_id: right
|
||||
```
|
||||
|
||||
## And always better and better with the NOTIFIER daemon app to notify events
|
||||
This automation uses the excellent App Daemon named NOTIFIER developed by Horizon Domotique that you will find in demonstration [here](https://www.youtube.com/watch?v=chJylIK0ASo&ab_channel=HorizonDomotique) and the code is [here](https ://github.com/jlpouffier/home-assistant-config/blob/master/appdaemon/apps/notifier.py). It allows you to notify the users of the accommodation when one of the events affecting safety occurs on one of the Versatile Thermostats.
|
||||
|
||||
This is a great example of using the notifications described here [notification](#notifications).
|
||||
|
||||
```
|
||||
alias: Surveillance Mode Sécurité chauffage
|
||||
description: Envoi une notification si un thermostat passe en mode sécurité ou power
|
||||
trigger:
|
||||
- platform: event
|
||||
event_type: versatile_thermostat_security_event
|
||||
id: versatile_thermostat_security_event
|
||||
- platform: event
|
||||
event_type: versatile_thermostat_power_event
|
||||
id: versatile_thermostat_power_event
|
||||
- platform: event
|
||||
event_type: versatile_thermostat_temperature_event
|
||||
id: versatile_thermostat_temperature_event
|
||||
condition: []
|
||||
action:
|
||||
- choose:
|
||||
- conditions:
|
||||
- condition: trigger
|
||||
id: versatile_thermostat_security_event
|
||||
sequence:
|
||||
- event: NOTIFIER
|
||||
event_data:
|
||||
action: send_to_jmc
|
||||
title: >-
|
||||
Radiateur {{ trigger.event.data.name }} - {{
|
||||
trigger.event.data.type }} Sécurité
|
||||
message: >-
|
||||
Le radiateur {{ trigger.event.data.name }} est passé en {{
|
||||
trigger.event.data.type }} sécurité car le thermomètre ne répond
|
||||
plus.\n{{ trigger.event.data }}
|
||||
callback:
|
||||
- title: Stopper chauffage
|
||||
event: stopper_chauffage
|
||||
image_url: /media/local/alerte-securite.jpg
|
||||
click_url: /lovelace-chauffage/4
|
||||
icon: mdi:radiator-off
|
||||
tag: radiateur_security_alerte
|
||||
persistent: true
|
||||
- conditions:
|
||||
- condition: trigger
|
||||
id: versatile_thermostat_power_event
|
||||
sequence:
|
||||
- event: NOTIFIER
|
||||
event_data:
|
||||
action: send_to_jmc
|
||||
title: >-
|
||||
Radiateur {{ trigger.event.data.name }} - {{
|
||||
trigger.event.data.type }} Délestage
|
||||
message: >-
|
||||
Le radiateur {{ trigger.event.data.name }} est passé en {{
|
||||
trigger.event.data.type }} délestage car la puissance max est
|
||||
dépassée.\n{{ trigger.event.data }}
|
||||
callback:
|
||||
- title: Stopper chauffage
|
||||
event: stopper_chauffage
|
||||
image_url: /media/local/alerte-delestage.jpg
|
||||
click_url: /lovelace-chauffage/4
|
||||
icon: mdi:radiator-off
|
||||
tag: radiateur_power_alerte
|
||||
persistent: true
|
||||
- conditions:
|
||||
- condition: trigger
|
||||
id: versatile_thermostat_temperature_event
|
||||
sequence:
|
||||
- event: NOTIFIER
|
||||
event_data:
|
||||
action: send_to_jmc
|
||||
title: >-
|
||||
Le thermomètre du radiateur {{ trigger.event.data.name }} ne
|
||||
répond plus
|
||||
message: >-
|
||||
Le thermomètre du radiateur {{ trigger.event.data.name }} ne
|
||||
répond plus depuis longtemps.\n{{ trigger.event.data }}
|
||||
image_url: /media/local/thermometre-alerte.jpg
|
||||
click_url: /lovelace-chauffage/4
|
||||
icon: mdi:radiator-disabled
|
||||
tag: radiateur_thermometre_alerte
|
||||
persistent: true
|
||||
mode: queued
|
||||
max: 30
|
||||
```
|
||||
|
||||
# Contributions are welcome!
|
||||
|
||||
If you wish to contribute, please read the [contribution guidelines](CONTRIBUTING.md).
|
||||
If you want to contribute to this please read the [Contribution guidelines](CONTRIBUTING.md)
|
||||
|
||||
***
|
||||
|
||||
|
||||
77
container
77
container
@@ -4,52 +4,35 @@
|
||||
|
||||
. .bashrc
|
||||
|
||||
function get_dev() {
|
||||
pip install -r requirements_dev.txt
|
||||
pip install -r requirements_test.txt
|
||||
if [ -d /home/vscode/core ]; then
|
||||
sudo chown -R vscode: /home/vscode/core
|
||||
fi
|
||||
}
|
||||
cd $HA
|
||||
|
||||
echo "arguments are: "$1
|
||||
echo "arguments are: "$*
|
||||
# Post installation of container
|
||||
command=$1
|
||||
if [ "$command" == "install" ]; then
|
||||
echo "Running container post installation"
|
||||
script/setup
|
||||
fi
|
||||
|
||||
case $1 in
|
||||
start)
|
||||
echo "Running container start"
|
||||
./scripts/starts_ha.sh
|
||||
;;
|
||||
dev-setup)
|
||||
get_dev
|
||||
;;
|
||||
install)
|
||||
echo "Running container post installation"
|
||||
script/setup
|
||||
;;
|
||||
translations)
|
||||
echo "Running container start"
|
||||
cd $HA
|
||||
python3 -m script.translations develop
|
||||
;;
|
||||
hassfest)
|
||||
echo "Running container start"
|
||||
python3 -m script.hassfest
|
||||
# python -m script.hassfest --requirements --action validate --integration-path config/custom_components/versatile_thermostat/
|
||||
;;
|
||||
restart)
|
||||
echo "Killing existing container"
|
||||
pkill hass
|
||||
echo "Restarting existing container"
|
||||
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
|
||||
if [ "$command" == "start" ]; then
|
||||
echo "Running container start"
|
||||
hass -c ./config --debug
|
||||
fi
|
||||
|
||||
if [ "$command" == "translations" ]; then
|
||||
echo "Running container start"
|
||||
python3 -m script.translations develop
|
||||
fi
|
||||
|
||||
if [ "$command" == "hassfest" ]; then
|
||||
echo "Running container start"
|
||||
python3 -m script.hassfest
|
||||
# python -m script.hassfest --requirements --action validate --integration-path config/custom_components/versatile_thermostat/
|
||||
fi
|
||||
|
||||
if [ "$command" == "restart" ]; then
|
||||
echo "Killing existing container"
|
||||
pkill hass
|
||||
echo "Killing existing container"
|
||||
hass -c ./config
|
||||
fi
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
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,168 +1,19 @@
|
||||
"""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.const import SERVICE_RELOAD, EVENT_HOMEASSISTANT_STARTED
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry, ConfigType
|
||||
from homeassistant.core import HomeAssistant, CoreState, callback
|
||||
from homeassistant.helpers.service import async_register_admin_service
|
||||
from .climate import VersatileThermostat
|
||||
|
||||
from .base_thermostat import BaseThermostat
|
||||
|
||||
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_SAFETY_DELAY_MIN,
|
||||
CONF_SAFETY_MIN_ON_PERCENT,
|
||||
CONF_SAFETY_DEFAULT_ON_PERCENT,
|
||||
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,
|
||||
CONF_MAX_ON_PERCENT,
|
||||
)
|
||||
|
||||
from .vtherm_api import VersatileThermostatAPI
|
||||
from .const import DOMAIN, PLATFORMS
|
||||
|
||||
_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),
|
||||
vol.Optional(CONF_MAX_ON_PERCENT): vol.Coerce(float),
|
||||
}
|
||||
),
|
||||
},
|
||||
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."""
|
||||
@@ -173,7 +24,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
entry.data,
|
||||
)
|
||||
|
||||
api: VersatileThermostatAPI = VersatileThermostatAPI.get_vtherm_api(hass)
|
||||
# hass.data.setdefault(DOMAIN, {})
|
||||
|
||||
api: VersatileThermostatAPI = hass.data.get(DOMAIN)
|
||||
if api is None:
|
||||
api = VersatileThermostatAPI(hass)
|
||||
|
||||
api.add_entry(entry)
|
||||
|
||||
@@ -181,139 +36,74 @@ 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."""
|
||||
|
||||
_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)
|
||||
await hass.config_entries.async_reload(entry.entry_id)
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
api: VersatileThermostatAPI = VersatileThermostatAPI.get_vtherm_api(hass)
|
||||
api: VersatileThermostatAPI = hass.data.get(DOMAIN)
|
||||
|
||||
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/%s", config_entry.version, config_entry.minor_version
|
||||
)
|
||||
_LOGGER.debug("Migrating from version %s", config_entry.version)
|
||||
|
||||
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
|
||||
)
|
||||
if config_entry.version == 1:
|
||||
new = {**config_entry.data}
|
||||
# TO DO: modify Config Entry data if there will be something to migrate
|
||||
|
||||
thermostat_type = config_entry.data.get(CONF_THERMOSTAT_TYPE)
|
||||
config_entry.version = 2
|
||||
hass.config_entries.async_update_entry(config_entry, data=new)
|
||||
|
||||
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)
|
||||
|
||||
# Migration 2.0 to 2.1 -> rename security parameters into safety
|
||||
|
||||
if config_entry.version == CONFIG_VERSION and config_entry.minor_version == 0:
|
||||
for key in [
|
||||
"security_delay_min",
|
||||
"security_min_on_percent",
|
||||
"security_default_on_percent",
|
||||
]:
|
||||
new_key = key.replace("security_", "safety_")
|
||||
old_value = config_entry.data.get(key, None)
|
||||
if old_value is not None:
|
||||
new[new_key] = old_value
|
||||
new.pop(key, None)
|
||||
|
||||
hass.config_entries.async_update_entry(
|
||||
config_entry,
|
||||
data=new,
|
||||
version=CONFIG_VERSION,
|
||||
minor_version=CONFIG_MINOR_VERSION,
|
||||
)
|
||||
_LOGGER.info("Migration to version %s successful", config_entry.version)
|
||||
_LOGGER.info("Migration to version %s successful", config_entry.version)
|
||||
|
||||
return True
|
||||
|
||||
@@ -1,266 +0,0 @@
|
||||
# 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
|
||||
_last_switch_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._last_calculation_date = None
|
||||
self._last_switch_date = None
|
||||
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
|
||||
|
||||
# Calculate the number of minute from last_switch
|
||||
nb_minutes_since_last_switch = 999
|
||||
if self._last_switch_date is not None:
|
||||
nb_minutes_since_last_switch = (
|
||||
now - self._last_switch_date
|
||||
).total_seconds() / 60
|
||||
|
||||
# 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
|
||||
and nb_minutes_since_last_switch >= self._dt
|
||||
):
|
||||
_LOGGER.info(
|
||||
"%s - We need to stop, there is no need for heating for a long time.",
|
||||
self,
|
||||
)
|
||||
self._last_switch_date = now
|
||||
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
|
||||
and nb_minutes_since_last_switch >= self._dt
|
||||
):
|
||||
_LOGGER.info(
|
||||
"%s - We need to stop, there is no need for cooling for a long time.",
|
||||
self,
|
||||
)
|
||||
self._last_switch_date = now
|
||||
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
|
||||
and nb_minutes_since_last_switch >= self._dt
|
||||
):
|
||||
_LOGGER.info(
|
||||
"%s - We need to start, because it will be time to heat",
|
||||
self,
|
||||
)
|
||||
self._last_switch_date = now
|
||||
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
|
||||
and nb_minutes_since_last_switch >= self._dt
|
||||
):
|
||||
_LOGGER.info(
|
||||
"%s - We need to start, because it will be time to cool",
|
||||
self,
|
||||
)
|
||||
self._last_switch_date = now
|
||||
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
|
||||
|
||||
@property
|
||||
def last_switch_date(self) -> datetime | None:
|
||||
"""Get the last of the last switch"""
|
||||
return self._last_switch_date
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"AutoStartStopDetectionAlgorithm-{self._vtherm_name}"
|
||||
@@ -1,119 +0,0 @@
|
||||
""" A base class for all VTherm entities"""
|
||||
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
from homeassistant.core import HomeAssistant, callback, Event
|
||||
from homeassistant.components.climate import ClimateEntity
|
||||
from homeassistant.components.climate.const import DOMAIN as CLIMATE_DOMAIN
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.device_registry import DeviceInfo, DeviceEntryType
|
||||
from homeassistant.helpers.event import async_track_state_change_event, async_call_later
|
||||
|
||||
|
||||
from .const import DOMAIN, DEVICE_MANUFACTURER
|
||||
|
||||
from .base_thermostat import BaseThermostat
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class VersatileThermostatBaseEntity(Entity):
|
||||
"""A base class for all entities"""
|
||||
|
||||
_my_climate: BaseThermostat
|
||||
hass: HomeAssistant
|
||||
_config_id: str
|
||||
_device_name: str
|
||||
|
||||
def __init__(self, hass: HomeAssistant, config_id, device_name) -> None:
|
||||
"""The CTOR"""
|
||||
self.hass = hass
|
||||
self._config_id = config_id
|
||||
self._device_name = device_name
|
||||
self._my_climate = None
|
||||
self._cancel_call = None
|
||||
self._attr_has_entity_name = True
|
||||
|
||||
@property
|
||||
def should_poll(self) -> bool:
|
||||
"""Do not poll for those entities"""
|
||||
return False
|
||||
|
||||
@property
|
||||
def my_climate(self) -> BaseThermostat | None:
|
||||
"""Returns my climate if found"""
|
||||
if not self._my_climate:
|
||||
self._my_climate = self.find_my_versatile_thermostat()
|
||||
if self._my_climate:
|
||||
# Only the first time
|
||||
self.my_climate_is_initialized()
|
||||
return self._my_climate
|
||||
|
||||
@property
|
||||
def device_info(self) -> DeviceInfo:
|
||||
"""Return the device info."""
|
||||
return DeviceInfo(
|
||||
entry_type=DeviceEntryType.SERVICE,
|
||||
identifiers={(DOMAIN, self._config_id)},
|
||||
name=self._device_name,
|
||||
manufacturer=DEVICE_MANUFACTURER,
|
||||
model=DOMAIN,
|
||||
)
|
||||
|
||||
def find_my_versatile_thermostat(self) -> BaseThermostat:
|
||||
"""Find the underlying climate entity"""
|
||||
try:
|
||||
component: EntityComponent[ClimateEntity] = self.hass.data[CLIMATE_DOMAIN]
|
||||
for entity in component.entities:
|
||||
# _LOGGER.debug("Device_info is %s", entity.device_info)
|
||||
if entity.device_info == self.device_info:
|
||||
_LOGGER.debug("Found %s!", entity)
|
||||
return entity
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
return None
|
||||
|
||||
@callback
|
||||
async def async_added_to_hass(self):
|
||||
"""Listen to my climate state change"""
|
||||
|
||||
# Check delay condition
|
||||
async def try_find_climate(_):
|
||||
_LOGGER.debug(
|
||||
"%s - Calling VersatileThermostatBaseEntity.async_added_to_hass", self
|
||||
)
|
||||
mcl = self.my_climate
|
||||
if mcl:
|
||||
if self._cancel_call:
|
||||
self._cancel_call()
|
||||
self._cancel_call = None
|
||||
self.async_on_remove(
|
||||
async_track_state_change_event(
|
||||
self.hass,
|
||||
[mcl.entity_id],
|
||||
self.async_my_climate_changed,
|
||||
)
|
||||
)
|
||||
else:
|
||||
_LOGGER.debug("%s - no entity to listen. Try later", self)
|
||||
self._cancel_call = async_call_later(
|
||||
self.hass, timedelta(seconds=1), try_find_climate
|
||||
)
|
||||
|
||||
await try_find_climate(None)
|
||||
|
||||
@callback
|
||||
def my_climate_is_initialized(self):
|
||||
"""Called when the associated climate is initialized"""
|
||||
return
|
||||
|
||||
@callback
|
||||
async def async_my_climate_changed(
|
||||
self, event: Event
|
||||
): # pylint: disable=unused-argument
|
||||
"""Called when my climate have change
|
||||
This method aims to be overridden to take the status change
|
||||
"""
|
||||
return
|
||||
@@ -1,62 +0,0 @@
|
||||
""" Implements a base Feature Manager for Versatile Thermostat """
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.core import CALLBACK_TYPE, HomeAssistant
|
||||
|
||||
from .const import * # pylint: disable=wildcard-import, unused-wildcard-import
|
||||
from .commons import ConfigData
|
||||
|
||||
from .config_schema import * # pylint: disable=wildcard-import, unused-wildcard-import
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class BaseFeatureManager:
|
||||
"""A base class for all feature"""
|
||||
|
||||
def __init__(self, vtherm: Any, hass: HomeAssistant, name: str = None):
|
||||
"""Init of a featureManager"""
|
||||
self._vtherm = vtherm
|
||||
self._name = vtherm.name if vtherm else name
|
||||
self._active_listener: list[CALLBACK_TYPE] = []
|
||||
self._hass = hass
|
||||
|
||||
def post_init(self, entry_infos: ConfigData):
|
||||
"""Initialize the attributes of the FeatureManager"""
|
||||
raise NotImplementedError()
|
||||
|
||||
async def start_listening(self):
|
||||
"""Start listening the underlying entity"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def stop_listening(self) -> bool:
|
||||
"""stop listening to the sensor"""
|
||||
while self._active_listener:
|
||||
self._active_listener.pop()()
|
||||
|
||||
self._active_listener = []
|
||||
|
||||
async def refresh_state(self):
|
||||
"""Refresh the state and return True if a change have been made"""
|
||||
return False
|
||||
|
||||
def add_listener(self, func: CALLBACK_TYPE) -> None:
|
||||
"""Add a listener to the list of active listener"""
|
||||
self._active_listener.append(func)
|
||||
|
||||
@property
|
||||
def is_configured(self) -> bool:
|
||||
"""True if the FeatureManager is fully configured"""
|
||||
raise NotImplementedError()
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
"""The name"""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def hass(self) -> HomeAssistant:
|
||||
"""The HA instance"""
|
||||
return self._hass
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,20 +1,9 @@
|
||||
""" Implements the VersatileThermostat binary sensors component """
|
||||
# pylint: disable=unused-argument, line-too-long
|
||||
|
||||
import logging
|
||||
|
||||
from homeassistant.core import (
|
||||
HomeAssistant,
|
||||
callback,
|
||||
Event,
|
||||
# CoreState,
|
||||
HomeAssistantError,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback, Event
|
||||
|
||||
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.const import STATE_ON
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorEntity,
|
||||
@@ -24,25 +13,13 @@ from homeassistant.config_entries import ConfigEntry
|
||||
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from .vtherm_api import VersatileThermostatAPI
|
||||
from .commons import check_and_extract_service_configuration
|
||||
from .base_entity import VersatileThermostatBaseEntity
|
||||
from .commons import VersatileThermostatBaseEntity
|
||||
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__)
|
||||
@@ -60,45 +37,26 @@ 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 = None
|
||||
entities = [SecurityBinarySensor(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 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)
|
||||
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, # pylint: disable=unused-argument
|
||||
entry_infos,
|
||||
) -> None:
|
||||
def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None:
|
||||
"""Initialize the SecurityState Binary sensor"""
|
||||
super().__init__(hass, unique_id, name)
|
||||
super().__init__(hass, unique_id, entry_infos.get(CONF_NAME))
|
||||
self._attr_name = "Security state"
|
||||
self._attr_unique_id = f"{self._device_name}_security_state"
|
||||
self._attr_is_on = False
|
||||
@@ -106,10 +64,10 @@ class SecurityBinarySensor(VersatileThermostatBaseEntity, BinarySensorEntity):
|
||||
@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)
|
||||
_LOGGER.debug("%s - climate state change", self._attr_unique_id)
|
||||
|
||||
old_state = self._attr_is_on
|
||||
self._attr_is_on = self.my_climate.safety_manager.is_safety_detected
|
||||
self._attr_is_on = self.my_climate.security_state is True
|
||||
if old_state != self._attr_is_on:
|
||||
self.async_write_ha_state()
|
||||
return
|
||||
@@ -129,13 +87,7 @@ class SecurityBinarySensor(VersatileThermostatBaseEntity, BinarySensorEntity):
|
||||
class OverpoweringBinarySensor(VersatileThermostatBaseEntity, BinarySensorEntity):
|
||||
"""Representation of a BinarySensor which exposes the overpowering state"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
unique_id,
|
||||
name, # pylint: disable=unused-argument
|
||||
entry_infos,
|
||||
) -> None:
|
||||
def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None:
|
||||
"""Initialize the OverpoweringState Binary sensor"""
|
||||
super().__init__(hass, unique_id, entry_infos.get(CONF_NAME))
|
||||
self._attr_name = "Overpowering state"
|
||||
@@ -145,10 +97,10 @@ class OverpoweringBinarySensor(VersatileThermostatBaseEntity, BinarySensorEntity
|
||||
@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)
|
||||
_LOGGER.debug("%s - climate state change", self._attr_unique_id)
|
||||
|
||||
old_state = self._attr_is_on
|
||||
self._attr_is_on = self.my_climate.overpowering_state is STATE_ON
|
||||
self._attr_is_on = self.my_climate.overpowering_state is True
|
||||
if old_state != self._attr_is_on:
|
||||
self.async_write_ha_state()
|
||||
return
|
||||
@@ -168,13 +120,7 @@ class OverpoweringBinarySensor(VersatileThermostatBaseEntity, BinarySensorEntity
|
||||
class WindowBinarySensor(VersatileThermostatBaseEntity, BinarySensorEntity):
|
||||
"""Representation of a BinarySensor which exposes the window state"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
unique_id,
|
||||
name, # pylint: disable=unused-argument
|
||||
entry_infos,
|
||||
) -> None:
|
||||
def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None:
|
||||
"""Initialize the WindowState Binary sensor"""
|
||||
super().__init__(hass, unique_id, entry_infos.get(CONF_NAME))
|
||||
self._attr_name = "Window state"
|
||||
@@ -184,20 +130,15 @@ class WindowBinarySensor(VersatileThermostatBaseEntity, BinarySensorEntity):
|
||||
@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)
|
||||
_LOGGER.debug("%s - climate state change", self._attr_unique_id)
|
||||
|
||||
old_state = self._attr_is_on
|
||||
# Issue 120 - only take defined presence value
|
||||
if self.my_climate.window_state in [
|
||||
STATE_ON,
|
||||
STATE_OFF,
|
||||
] or self.my_climate.window_auto_state in [STATE_ON, STATE_OFF]:
|
||||
self._attr_is_on = (
|
||||
self.my_climate.window_state == STATE_ON
|
||||
or self.my_climate.window_auto_state == STATE_ON
|
||||
)
|
||||
if old_state != self._attr_is_on:
|
||||
self.async_write_ha_state()
|
||||
self._attr_is_on = (
|
||||
self.my_climate.window_state == STATE_ON
|
||||
or self.my_climate.window_auto_state == STATE_ON
|
||||
)
|
||||
if old_state != self._attr_is_on:
|
||||
self.async_write_ha_state()
|
||||
return
|
||||
|
||||
@property
|
||||
@@ -218,13 +159,7 @@ class WindowBinarySensor(VersatileThermostatBaseEntity, BinarySensorEntity):
|
||||
class MotionBinarySensor(VersatileThermostatBaseEntity, BinarySensorEntity):
|
||||
"""Representation of a BinarySensor which exposes the motion state"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
unique_id,
|
||||
name, # pylint: disable=unused-argument
|
||||
entry_infos,
|
||||
) -> None:
|
||||
def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None:
|
||||
"""Initialize the MotionState Binary sensor"""
|
||||
super().__init__(hass, unique_id, entry_infos.get(CONF_NAME))
|
||||
self._attr_name = "Motion state"
|
||||
@@ -234,13 +169,11 @@ class MotionBinarySensor(VersatileThermostatBaseEntity, BinarySensorEntity):
|
||||
@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)
|
||||
_LOGGER.debug("%s - climate state change", self._attr_unique_id)
|
||||
old_state = self._attr_is_on
|
||||
# Issue 120 - only take defined presence value
|
||||
if self.my_climate.motion_state in [STATE_ON, STATE_OFF]:
|
||||
self._attr_is_on = self.my_climate.motion_state == STATE_ON
|
||||
if old_state != self._attr_is_on:
|
||||
self.async_write_ha_state()
|
||||
self._attr_is_on = self.my_climate.motion_state == STATE_ON
|
||||
if old_state != self._attr_is_on:
|
||||
self.async_write_ha_state()
|
||||
return
|
||||
|
||||
@property
|
||||
@@ -258,13 +191,7 @@ class MotionBinarySensor(VersatileThermostatBaseEntity, BinarySensorEntity):
|
||||
class PresenceBinarySensor(VersatileThermostatBaseEntity, BinarySensorEntity):
|
||||
"""Representation of a BinarySensor which exposes the presence state"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
unique_id,
|
||||
name, # pylint: disable=unused-argument
|
||||
entry_infos,
|
||||
) -> None:
|
||||
def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None:
|
||||
"""Initialize the PresenceState Binary sensor"""
|
||||
super().__init__(hass, unique_id, entry_infos.get(CONF_NAME))
|
||||
self._attr_name = "Presence state"
|
||||
@@ -275,13 +202,11 @@ class PresenceBinarySensor(VersatileThermostatBaseEntity, BinarySensorEntity):
|
||||
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)
|
||||
_LOGGER.debug("%s - climate state change", self._attr_unique_id)
|
||||
old_state = self._attr_is_on
|
||||
# Issue 120 - only take defined presence value
|
||||
if self.my_climate.presence_state in [STATE_ON, STATE_OFF]:
|
||||
self._attr_is_on = self.my_climate.presence_state == STATE_ON
|
||||
if old_state != self._attr_is_on:
|
||||
self.async_write_ha_state()
|
||||
self._attr_is_on = self.my_climate.presence_state == STATE_ON
|
||||
if old_state != self._attr_is_on:
|
||||
self.async_write_ha_state()
|
||||
return
|
||||
|
||||
@property
|
||||
@@ -294,201 +219,3 @@ class PresenceBinarySensor(VersatileThermostatBaseEntity, BinarySensorEntity):
|
||||
return "mdi:home-account"
|
||||
else:
|
||||
return "mdi:nature-people"
|
||||
|
||||
|
||||
class WindowByPassBinarySensor(VersatileThermostatBaseEntity, BinarySensorEntity):
|
||||
"""Representation of a BinarySensor which exposes the Window ByPass state"""
|
||||
|
||||
def __init__(
|
||||
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"
|
||||
self._attr_unique_id = f"{self._device_name}_window_bypass_state"
|
||||
self._attr_is_on = False
|
||||
|
||||
@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)
|
||||
old_state = self._attr_is_on
|
||||
if self.my_climate.is_window_bypass in [True, False]:
|
||||
self._attr_is_on = self.my_climate.is_window_bypass
|
||||
if old_state != self._attr_is_on:
|
||||
self.async_write_ha_state()
|
||||
return
|
||||
|
||||
@property
|
||||
def device_class(self) -> BinarySensorDeviceClass | None:
|
||||
return BinarySensorDeviceClass.RUNNING
|
||||
|
||||
@property
|
||||
def icon(self) -> str | None:
|
||||
if self._attr_is_on:
|
||||
return "mdi:window-shutter-cog"
|
||||
else:
|
||||
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,324 +0,0 @@
|
||||
""" Implements a central Power Feature Manager for Versatile Thermostat """
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
from functools import cmp_to_key
|
||||
|
||||
from datetime import timedelta
|
||||
|
||||
from homeassistant.const import STATE_OFF
|
||||
from homeassistant.core import HomeAssistant, Event, callback
|
||||
from homeassistant.helpers.event import (
|
||||
async_track_state_change_event,
|
||||
EventStateChangedData,
|
||||
async_call_later,
|
||||
)
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
from homeassistant.components.climate import (
|
||||
ClimateEntity,
|
||||
DOMAIN as CLIMATE_DOMAIN,
|
||||
)
|
||||
|
||||
|
||||
from .const import * # pylint: disable=wildcard-import, unused-wildcard-import
|
||||
from .commons import ConfigData
|
||||
from .base_manager import BaseFeatureManager
|
||||
|
||||
# circular dependency
|
||||
# from .base_thermostat import BaseThermostat
|
||||
|
||||
MIN_DTEMP_SECS = 20
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CentralFeaturePowerManager(BaseFeatureManager):
|
||||
"""A central Power feature manager"""
|
||||
|
||||
def __init__(self, hass: HomeAssistant, vtherm_api: Any):
|
||||
"""Init of a featureManager"""
|
||||
super().__init__(None, hass, "centralPowerManager")
|
||||
self._hass: HomeAssistant = hass
|
||||
self._vtherm_api = vtherm_api # no type due to circular reference
|
||||
self._is_configured: bool = False
|
||||
self._power_sensor_entity_id: str = None
|
||||
self._max_power_sensor_entity_id: str = None
|
||||
self._current_power: float = None
|
||||
self._current_max_power: float = None
|
||||
self._power_temp: float = None
|
||||
self._cancel_calculate_shedding_call = None
|
||||
self._started_vtherm_total_power: float = None
|
||||
# Not used now
|
||||
self._last_shedding_date = None
|
||||
|
||||
def post_init(self, entry_infos: ConfigData):
|
||||
"""Gets the configuration parameters"""
|
||||
central_config = self._vtherm_api.find_central_configuration()
|
||||
if not central_config:
|
||||
_LOGGER.info("No central configuration is found. Power management will be deactivated")
|
||||
return
|
||||
|
||||
self._power_sensor_entity_id = entry_infos.get(CONF_POWER_SENSOR)
|
||||
self._max_power_sensor_entity_id = entry_infos.get(CONF_MAX_POWER_SENSOR)
|
||||
self._power_temp = entry_infos.get(CONF_PRESET_POWER)
|
||||
|
||||
self._is_configured = False
|
||||
self._current_power = None
|
||||
self._current_max_power = None
|
||||
if (
|
||||
entry_infos.get(CONF_USE_POWER_FEATURE, False)
|
||||
and self._max_power_sensor_entity_id
|
||||
and self._power_sensor_entity_id
|
||||
and self._power_temp
|
||||
):
|
||||
self._is_configured = True
|
||||
self._started_vtherm_total_power = 0
|
||||
else:
|
||||
_LOGGER.info("Power management is not fully configured and will be deactivated")
|
||||
|
||||
async def start_listening(self):
|
||||
"""Start listening the power sensor"""
|
||||
if not self._is_configured:
|
||||
return
|
||||
|
||||
self.stop_listening()
|
||||
|
||||
self.add_listener(
|
||||
async_track_state_change_event(
|
||||
self.hass,
|
||||
[self._power_sensor_entity_id],
|
||||
self._power_sensor_changed,
|
||||
)
|
||||
)
|
||||
|
||||
self.add_listener(
|
||||
async_track_state_change_event(
|
||||
self.hass,
|
||||
[self._max_power_sensor_entity_id],
|
||||
self._max_power_sensor_changed,
|
||||
)
|
||||
)
|
||||
|
||||
@callback
|
||||
async def _power_sensor_changed(self, event: Event[EventStateChangedData]):
|
||||
"""Handle power changes."""
|
||||
_LOGGER.debug("Receive new Power event")
|
||||
_LOGGER.debug(event)
|
||||
|
||||
self._started_vtherm_total_power = 0
|
||||
await self.refresh_state()
|
||||
|
||||
@callback
|
||||
async def _max_power_sensor_changed(self, event: Event[EventStateChangedData]):
|
||||
"""Handle power max changes."""
|
||||
_LOGGER.debug("Receive new Power Max event")
|
||||
_LOGGER.debug(event)
|
||||
await self.refresh_state()
|
||||
|
||||
@overrides
|
||||
async def refresh_state(self) -> bool:
|
||||
"""Tries to get the last state from sensor
|
||||
Returns True if a change has been made"""
|
||||
|
||||
async def _calculate_shedding_internal(_):
|
||||
_LOGGER.debug("Do the shedding calculation")
|
||||
await self.calculate_shedding()
|
||||
if self._cancel_calculate_shedding_call:
|
||||
self._cancel_calculate_shedding_call()
|
||||
self._cancel_calculate_shedding_call = None
|
||||
|
||||
if not self._is_configured:
|
||||
return False
|
||||
|
||||
# Retrieve current power
|
||||
new_power = get_safe_float(self._hass, self._power_sensor_entity_id)
|
||||
power_changed = new_power is not None and self._current_power != new_power
|
||||
if power_changed:
|
||||
self._current_power = new_power
|
||||
_LOGGER.debug("New current power has been retrieved: %.3f", self._current_power)
|
||||
|
||||
# Retrieve max power
|
||||
new_max_power = get_safe_float(self._hass, self._max_power_sensor_entity_id)
|
||||
max_power_changed = new_max_power is not None and self._current_max_power != new_max_power
|
||||
if max_power_changed:
|
||||
self._current_max_power = new_max_power
|
||||
_LOGGER.debug("New current max power has been retrieved: %.3f", self._current_max_power)
|
||||
|
||||
# Schedule shedding calculation if there's any change
|
||||
if power_changed or max_power_changed:
|
||||
if not self._cancel_calculate_shedding_call:
|
||||
self._cancel_calculate_shedding_call = async_call_later(self.hass, timedelta(seconds=MIN_DTEMP_SECS), _calculate_shedding_internal)
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
# For testing purpose only, do an immediate shedding calculation
|
||||
async def _do_immediate_shedding(self):
|
||||
"""Do an immmediate shedding calculation if a timer was programmed.
|
||||
Else, do nothing"""
|
||||
if self._cancel_calculate_shedding_call:
|
||||
self._cancel_calculate_shedding_call()
|
||||
self._cancel_calculate_shedding_call = None
|
||||
await self.calculate_shedding()
|
||||
|
||||
async def calculate_shedding(self):
|
||||
"""Do the shedding calculation and set/unset VTherm into overpowering state"""
|
||||
if not self.is_configured or self.current_max_power is None or self.current_power is None:
|
||||
return
|
||||
|
||||
_LOGGER.debug("-------- Start of calculate_shedding")
|
||||
# Find all VTherms
|
||||
available_power = self.current_max_power - self.current_power
|
||||
vtherms_sorted = self.find_all_vtherm_with_power_management_sorted_by_dtemp()
|
||||
|
||||
# shedding only
|
||||
if available_power < 0:
|
||||
_LOGGER.debug(
|
||||
"The available power is is < 0 (%s). Set overpowering only for list: %s",
|
||||
available_power,
|
||||
vtherms_sorted,
|
||||
)
|
||||
# we will set overpowering for the nearest target temp first
|
||||
total_power_gain = 0
|
||||
|
||||
for vtherm in vtherms_sorted:
|
||||
if vtherm.is_device_active and not vtherm.power_manager.is_overpowering_detected:
|
||||
device_power = vtherm.power_manager.device_power
|
||||
total_power_gain += device_power
|
||||
_LOGGER.info("vtherm %s should be in overpowering state (device_power=%.2f)", vtherm.name, device_power)
|
||||
await vtherm.power_manager.set_overpowering(True, device_power)
|
||||
|
||||
_LOGGER.debug("after vtherm %s total_power_gain=%s, available_power=%s", vtherm.name, total_power_gain, available_power)
|
||||
if total_power_gain >= -available_power:
|
||||
_LOGGER.debug("We have found enough vtherm to set to overpowering")
|
||||
break
|
||||
# unshedding only
|
||||
else:
|
||||
vtherms_sorted.reverse()
|
||||
_LOGGER.debug("The available power is is > 0 (%s). Do a complete shedding/un-shedding calculation for list: %s", available_power, vtherms_sorted)
|
||||
|
||||
total_power_added = 0
|
||||
|
||||
for vtherm in vtherms_sorted:
|
||||
# We want to do always unshedding in order to initialize the state
|
||||
# so we cannot use is_overpowering_detected which test also UNKNOWN and UNAVAILABLE
|
||||
if vtherm.power_manager.overpowering_state == STATE_OFF:
|
||||
continue
|
||||
|
||||
power_consumption_max = device_power = vtherm.power_manager.device_power
|
||||
# calculate the power_consumption_max
|
||||
if vtherm.on_percent is not None:
|
||||
power_consumption_max = max(
|
||||
device_power / vtherm.nb_underlying_entities,
|
||||
device_power * vtherm.on_percent,
|
||||
)
|
||||
|
||||
_LOGGER.debug("vtherm %s power_consumption_max is %s (device_power=%s, overclimate=%s)", vtherm.name, power_consumption_max, device_power, vtherm.is_over_climate)
|
||||
|
||||
# or not ... is for initializing the overpowering state if not already done
|
||||
if total_power_added + power_consumption_max < available_power or not vtherm.power_manager.is_overpowering_detected:
|
||||
# we count the unshedding only if the VTherm was in shedding
|
||||
if vtherm.power_manager.is_overpowering_detected:
|
||||
_LOGGER.info("vtherm %s should not be in overpowering state (power_consumption_max=%.2f)", vtherm.name, power_consumption_max)
|
||||
total_power_added += power_consumption_max
|
||||
|
||||
await vtherm.power_manager.set_overpowering(False)
|
||||
|
||||
if total_power_added >= available_power:
|
||||
_LOGGER.debug("We have found enough vtherm to set to non-overpowering")
|
||||
break
|
||||
|
||||
_LOGGER.debug("after vtherm %s total_power_added=%s, available_power=%s", vtherm.name, total_power_added, available_power)
|
||||
|
||||
self._last_shedding_date = self._vtherm_api.now
|
||||
_LOGGER.debug("-------- End of calculate_shedding")
|
||||
|
||||
def get_climate_components_entities(self) -> list:
|
||||
"""Get all VTherms entitites"""
|
||||
vtherms = []
|
||||
component: EntityComponent[ClimateEntity] = self._hass.data.get(
|
||||
CLIMATE_DOMAIN, None
|
||||
)
|
||||
if component:
|
||||
for entity in component.entities:
|
||||
# A little hack to test if the climate is a VTherm. Cannot use isinstance
|
||||
# due to circular dependency of BaseThermostat
|
||||
if (
|
||||
entity.device_info
|
||||
and entity.device_info.get("model", None) == DOMAIN
|
||||
):
|
||||
vtherms.append(entity)
|
||||
return vtherms
|
||||
|
||||
def find_all_vtherm_with_power_management_sorted_by_dtemp(
|
||||
self,
|
||||
) -> list:
|
||||
"""Returns all the VTherms with power management activated"""
|
||||
entities = self.get_climate_components_entities()
|
||||
vtherms = [
|
||||
vtherm
|
||||
for vtherm in entities
|
||||
if vtherm.power_manager.is_configured and vtherm.is_on
|
||||
]
|
||||
|
||||
# sort the result with the min temp difference first. A and B should be BaseThermostat class
|
||||
def cmp_temps(a, b) -> int:
|
||||
diff_a = float("inf")
|
||||
diff_b = float("inf")
|
||||
a_target = a.target_temperature if not a.power_manager.is_overpowering_detected else a.saved_target_temp
|
||||
b_target = b.target_temperature if not b.power_manager.is_overpowering_detected else b.saved_target_temp
|
||||
if a.current_temperature is not None and a_target is not None:
|
||||
diff_a = a_target - a.current_temperature
|
||||
if b.current_temperature is not None and b_target is not None:
|
||||
diff_b = b_target - b.current_temperature
|
||||
|
||||
if diff_a == diff_b:
|
||||
return 0
|
||||
return 1 if diff_a > diff_b else -1
|
||||
|
||||
vtherms.sort(key=cmp_to_key(cmp_temps))
|
||||
return vtherms
|
||||
|
||||
def add_started_vtherm_total_power(self, started_power: float):
|
||||
"""Add the power into the _started_vtherm_total_power which holds all VTherm started after
|
||||
the last power measurement"""
|
||||
self._started_vtherm_total_power += started_power
|
||||
_LOGGER.debug("%s - started_vtherm_total_power is now %s", self, self._started_vtherm_total_power)
|
||||
|
||||
@property
|
||||
def is_configured(self) -> bool:
|
||||
"""True if the FeatureManager is fully configured"""
|
||||
return self._is_configured
|
||||
|
||||
@property
|
||||
def current_power(self) -> float | None:
|
||||
"""Return the current power from sensor"""
|
||||
return self._current_power
|
||||
|
||||
@property
|
||||
def current_max_power(self) -> float | None:
|
||||
"""Return the current power from sensor"""
|
||||
return self._current_max_power
|
||||
|
||||
@property
|
||||
def power_temperature(self) -> float | None:
|
||||
"""Return the power temperature"""
|
||||
return self._power_temp
|
||||
|
||||
@property
|
||||
def power_sensor_entity_id(self) -> float | None:
|
||||
"""Return the power sensor entity id"""
|
||||
return self._power_sensor_entity_id
|
||||
|
||||
@property
|
||||
def max_power_sensor_entity_id(self) -> float | None:
|
||||
"""Return the max power sensor entity id"""
|
||||
return self._max_power_sensor_entity_id
|
||||
|
||||
@property
|
||||
def started_vtherm_total_power(self) -> float | None:
|
||||
"""Return the started_vtherm_total_power"""
|
||||
return self._started_vtherm_total_power
|
||||
|
||||
def __str__(self):
|
||||
return "CentralPowerManager"
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,152 +1,105 @@
|
||||
""" Some usefull commons class """
|
||||
|
||||
# pylint: disable=line-too-long
|
||||
|
||||
import logging
|
||||
import warnings
|
||||
from types import MappingProxyType
|
||||
from typing import Any, TypeVar
|
||||
from datetime import timedelta
|
||||
from homeassistant.core import HomeAssistant, callback, Event
|
||||
from homeassistant.components.climate import ClimateEntity, DOMAIN as CLIMATE_DOMAIN
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.device_registry import DeviceInfo, DeviceEntryType
|
||||
from homeassistant.helpers.event import async_track_state_change_event, async_call_later
|
||||
|
||||
from .const import ServiceConfigurationError
|
||||
from .underlyings import UnderlyingEntity
|
||||
|
||||
ConfigData = MappingProxyType[str, Any]
|
||||
T = TypeVar("T", bound=UnderlyingEntity)
|
||||
from .climate import VersatileThermostat
|
||||
from .const import DOMAIN, DEVICE_MANUFACTURER
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
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)
|
||||
class VersatileThermostatBaseEntity(Entity):
|
||||
"""A base class for all entities"""
|
||||
|
||||
print(nombre_arrondi1) # Output: 3.3
|
||||
print(nombre_arrondi2) # Output: 4.6
|
||||
"""
|
||||
assert x > 0
|
||||
return round(n * (1 / x)) / (1 / x)
|
||||
_my_climate: VersatileThermostat
|
||||
hass: HomeAssistant
|
||||
_config_id: str
|
||||
_device_name: str
|
||||
|
||||
def __init__(self, hass: HomeAssistant, config_id, device_name) -> None:
|
||||
"""The CTOR"""
|
||||
self.hass = hass
|
||||
self._config_id = config_id
|
||||
self._device_name = device_name
|
||||
self._my_climate = None
|
||||
self._cancel_call = None
|
||||
self._attr_has_entity_name = True
|
||||
|
||||
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"
|
||||
}
|
||||
@property
|
||||
def should_poll(self) -> bool:
|
||||
"""Do not poll for those entities"""
|
||||
return False
|
||||
|
||||
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": { },
|
||||
}
|
||||
@property
|
||||
def my_climate(self) -> VersatileThermostat | None:
|
||||
"""Returns my climate if found"""
|
||||
if not self._my_climate:
|
||||
self._my_climate = self.find_my_versatile_thermostat()
|
||||
return self._my_climate
|
||||
|
||||
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."
|
||||
@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,
|
||||
)
|
||||
|
||||
ret.update(
|
||||
{
|
||||
"service_domain": service_infos[0].strip(),
|
||||
"service_name": service_infos[1].strip(),
|
||||
}
|
||||
)
|
||||
def find_my_versatile_thermostat(self) -> VersatileThermostat:
|
||||
"""Find the underlying climate entity"""
|
||||
try:
|
||||
component: EntityComponent[ClimateEntity] = self.hass.data[CLIMATE_DOMAIN]
|
||||
for entity in component.entities:
|
||||
# _LOGGER.debug("Device_info is %s", entity.device_info)
|
||||
if entity.device_info == self.device_info:
|
||||
_LOGGER.debug("Found %s!", entity)
|
||||
return entity
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
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."
|
||||
)
|
||||
return None
|
||||
|
||||
ret.update(
|
||||
{
|
||||
"entity_domain": entity_infos[0].strip(),
|
||||
"entity_name": entity_infos[1].strip(),
|
||||
"entity_id": entity_id.strip(),
|
||||
}
|
||||
)
|
||||
@callback
|
||||
async def async_added_to_hass(self):
|
||||
"""Listen to my climate state change"""
|
||||
|
||||
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."
|
||||
# Check delay condition
|
||||
async def try_find_climate(_):
|
||||
_LOGGER.debug(
|
||||
"%s - Calling VersatileThermostatBaseEntity.async_added_to_hass", self
|
||||
)
|
||||
mcl = self.my_climate
|
||||
if mcl:
|
||||
if self._cancel_call:
|
||||
self._cancel_call()
|
||||
self._cancel_call = None
|
||||
self.async_on_remove(
|
||||
async_track_state_change_event(
|
||||
self.hass,
|
||||
[mcl.entity_id],
|
||||
self.async_my_climate_changed,
|
||||
)
|
||||
)
|
||||
else:
|
||||
_LOGGER.warning("%s - no entity to listen. Try later", self)
|
||||
self._cancel_call = async_call_later(
|
||||
self.hass, timedelta(seconds=1), try_find_climate
|
||||
)
|
||||
|
||||
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": {}})
|
||||
await try_find_climate(None)
|
||||
|
||||
_LOGGER.debug(
|
||||
"check_and_extract_service_configuration(%s) gives '%s'", service_config, ret
|
||||
)
|
||||
return ret
|
||||
|
||||
|
||||
def deprecated(message):
|
||||
"""A decorator to indicate that the method/attribut is deprecated"""
|
||||
|
||||
def decorator(func):
|
||||
def wrapper(*args, **kwargs):
|
||||
warnings.warn(
|
||||
f"{func.__name__} is deprecated: {message}",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
return func(*args, **kwargs)
|
||||
|
||||
return wrapper
|
||||
|
||||
return decorator
|
||||
@callback
|
||||
async def async_my_climate_changed(self, event: Event):
|
||||
"""Called when my climate have change
|
||||
This method aims to be overriden to take the status change
|
||||
"""
|
||||
return
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,394 +0,0 @@
|
||||
""" 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.select import (
|
||||
DOMAIN as SELECT_DOMAIN,
|
||||
)
|
||||
|
||||
from homeassistant.components.input_select import (
|
||||
DOMAIN as INPUT_SELECT_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, SELECT_DOMAIN, INPUT_SELECT_DOMAIN, CLIMATE_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,
|
||||
vol.Optional("on_command_text"): vol.In([]),
|
||||
vol.Optional(CONF_VSWITCH_ON_CMD_LIST): selector.TextSelector(selector.TextSelectorConfig(type=selector.TextSelectorType.TEXT, multiple=True)),
|
||||
vol.Optional("off_command_text"): vol.In([]),
|
||||
vol.Optional(CONF_VSWITCH_OFF_CMD_LIST): selector.TextSelector(selector.TextSelectorConfig(type=selector.TextSelectorType.TEXT, multiple=True)),
|
||||
}
|
||||
)
|
||||
|
||||
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_VALVE_REGULATION = vol.Schema( # pylint: disable=invalid-name
|
||||
{
|
||||
vol.Required(CONF_OPENING_DEGREE_LIST): selector.EntitySelector(
|
||||
selector.EntitySelectorConfig(
|
||||
domain=[NUMBER_DOMAIN, INPUT_NUMBER_DOMAIN], multiple=True
|
||||
),
|
||||
),
|
||||
vol.Optional(CONF_OFFSET_CALIBRATION_LIST): selector.EntitySelector(
|
||||
selector.EntitySelectorConfig(
|
||||
domain=[NUMBER_DOMAIN, INPUT_NUMBER_DOMAIN], multiple=True
|
||||
),
|
||||
),
|
||||
vol.Optional(CONF_CLOSING_DEGREE_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_MIN_OPENING_DEGREES, default=""): str,
|
||||
}
|
||||
)
|
||||
|
||||
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): selector.NumberSelector(selector.NumberSelectorConfig(min=0.0, max=1.0, step=0.01, mode=selector.NumberSelectorMode.BOX)),
|
||||
vol.Required(CONF_TPI_COEF_EXT, default=0.01): selector.NumberSelector(selector.NumberSelectorConfig(min=0.0, max=1.0, step=0.001, mode=selector.NumberSelectorMode.BOX)),
|
||||
}
|
||||
)
|
||||
|
||||
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_OFF_DELAY, default=30): cv.positive_int,
|
||||
vol.Optional(CONF_WINDOW_AUTO_OPEN_THRESHOLD, default=3): vol.Coerce(float),
|
||||
vol.Optional(CONF_WINDOW_AUTO_CLOSE_THRESHOLD, default=0): vol.Coerce(float),
|
||||
vol.Optional(CONF_WINDOW_AUTO_MAX_DURATION, default=30): cv.positive_int,
|
||||
vol.Optional(CONF_WINDOW_ACTION, default=CONF_WINDOW_TURN_OFF): selector.SelectSelector(
|
||||
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_OFF_DELAY, default=30): cv.positive_int,
|
||||
vol.Optional(CONF_WINDOW_ACTION, default=CONF_WINDOW_TURN_OFF): selector.SelectSelector(
|
||||
selector.SelectSelectorConfig(
|
||||
options=CONF_WINDOW_ACTIONS,
|
||||
translation_key="window_action",
|
||||
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_NON_CENTRAL_POWER_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name
|
||||
{
|
||||
vol.Optional(CONF_PRESET_POWER, default="13"): vol.Coerce(float),
|
||||
}
|
||||
)
|
||||
|
||||
STEP_POWER_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name
|
||||
{
|
||||
vol.Required(CONF_USE_POWER_CENTRAL_CONFIG, default=True): cv.boolean,
|
||||
}
|
||||
)
|
||||
|
||||
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_MINIMAL_DEACTIVATION_DELAY, default=0): cv.positive_int,
|
||||
vol.Required(CONF_SAFETY_DELAY_MIN, default=60): cv.positive_int,
|
||||
vol.Required(
|
||||
CONF_SAFETY_MIN_ON_PERCENT,
|
||||
default=DEFAULT_SAFETY_MIN_ON_PERCENT,
|
||||
): vol.Coerce(float),
|
||||
vol.Required(
|
||||
CONF_SAFETY_DEFAULT_ON_PERCENT,
|
||||
default=DEFAULT_SAFETY_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,14 +1,6 @@
|
||||
# pylint: disable=line-too-long
|
||||
"""Constants for the Versatile Thermostat integration."""
|
||||
|
||||
import logging
|
||||
import math
|
||||
from typing import Literal
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from enum import Enum
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.const import CONF_NAME, Platform
|
||||
|
||||
from homeassistant.components.climate import (
|
||||
@@ -19,51 +11,34 @@ from homeassistant.components.climate import (
|
||||
ClimateEntityFeature,
|
||||
)
|
||||
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from .prop_algorithm import (
|
||||
PROPORTIONAL_FUNCTION_TPI,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONFIG_VERSION = 2
|
||||
CONFIG_MINOR_VERSION = 1
|
||||
|
||||
PRESET_TEMP_SUFFIX = "_temp"
|
||||
PRESET_AC_SUFFIX = "_ac"
|
||||
PRESET_ECO_AC = PRESET_ECO + PRESET_AC_SUFFIX
|
||||
PRESET_COMFORT_AC = PRESET_COMFORT + PRESET_AC_SUFFIX
|
||||
PRESET_BOOST_AC = PRESET_BOOST + PRESET_AC_SUFFIX
|
||||
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
|
||||
from .prop_algorithm import (
|
||||
PROPORTIONAL_FUNCTION_TPI,
|
||||
)
|
||||
|
||||
DEVICE_MANUFACTURER = "JMCOLLIN"
|
||||
DEVICE_MODEL = "Versatile Thermostat"
|
||||
|
||||
PRESET_POWER = "power"
|
||||
PRESET_SAFETY = "security"
|
||||
PRESET_FROST_PROTECTION = "frost"
|
||||
PRESET_SECURITY = "security"
|
||||
|
||||
HIDDEN_PRESETS = [PRESET_POWER, PRESET_SAFETY]
|
||||
HIDDEN_PRESETS = [PRESET_POWER, PRESET_SECURITY]
|
||||
|
||||
DOMAIN = "versatile_thermostat"
|
||||
|
||||
# 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,
|
||||
]
|
||||
PLATFORMS: list[Platform] = [Platform.CLIMATE, Platform.BINARY_SENSOR, Platform.SENSOR]
|
||||
|
||||
CONF_UNDERLYING_LIST = "underlying_entity_ids"
|
||||
CONF_HEATER_KEEP_ALIVE = "heater_keep_alive"
|
||||
CONF_HEATER = "heater_entity_id"
|
||||
CONF_HEATER_2 = "heater_entity2_id"
|
||||
CONF_HEATER_3 = "heater_entity3_id"
|
||||
CONF_HEATER_4 = "heater_entity4_id"
|
||||
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"
|
||||
@@ -73,9 +48,7 @@ CONF_DEVICE_POWER = "device_power"
|
||||
CONF_CYCLE_MIN = "cycle_min"
|
||||
CONF_PROP_FUNCTION = "proportional_function"
|
||||
CONF_WINDOW_DELAY = "window_delay"
|
||||
CONF_WINDOW_OFF_DELAY = "window_off_delay"
|
||||
CONF_MOTION_DELAY = "motion_delay"
|
||||
CONF_MOTION_OFF_DELAY = "motion_off_delay"
|
||||
CONF_MOTION_PRESET = "motion_preset"
|
||||
CONF_NO_MOTION_PRESET = "no_motion_preset"
|
||||
CONF_TPI_COEF_INT = "tpi_coef_int"
|
||||
@@ -83,131 +56,30 @@ CONF_TPI_COEF_EXT = "tpi_coef_ext"
|
||||
CONF_PRESENCE_SENSOR = "presence_sensor_entity_id"
|
||||
CONF_PRESET_POWER = "power_temp"
|
||||
CONF_MINIMAL_ACTIVATION_DELAY = "minimal_activation_delay"
|
||||
CONF_MINIMAL_DEACTIVATION_DELAY = "minimal_deactivation_delay"
|
||||
CONF_TEMP_MIN = "temp_min"
|
||||
CONF_TEMP_MAX = "temp_max"
|
||||
CONF_SAFETY_DELAY_MIN = "safety_delay_min"
|
||||
CONF_SAFETY_MIN_ON_PERCENT = "safety_min_on_percent"
|
||||
CONF_SAFETY_DEFAULT_ON_PERCENT = "safety_default_on_percent"
|
||||
CONF_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_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_VALVE = "auto_regulation_valve"
|
||||
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"
|
||||
CONF_OFFSET_CALIBRATION_LIST = "offset_calibration_entity_ids"
|
||||
CONF_OPENING_DEGREE_LIST = "opening_degree_entity_ids"
|
||||
CONF_CLOSING_DEGREE_LIST = "closing_degree_entity_ids"
|
||||
CONF_MIN_OPENING_DEGREES = "min_opening_degrees"
|
||||
|
||||
CONF_VSWITCH_ON_CMD_LIST = "vswitch_on_command"
|
||||
CONF_VSWITCH_OFF_CMD_LIST = "vswitch_off_command"
|
||||
|
||||
# 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"
|
||||
|
||||
# Global params into configuration.yaml
|
||||
CONF_SHORT_EMA_PARAMS = "short_ema_params"
|
||||
CONF_SAFETY_MODE = "safety_mode"
|
||||
CONF_MAX_ON_PERCENT = "max_on_percent"
|
||||
|
||||
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_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_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_PRESETS = {
|
||||
p: f"{p}{PRESET_TEMP_SUFFIX}"
|
||||
p: f"{p}_temp"
|
||||
for p in (
|
||||
PRESET_FROST_PROTECTION,
|
||||
PRESET_ECO,
|
||||
PRESET_COMFORT,
|
||||
PRESET_BOOST,
|
||||
@@ -215,9 +87,8 @@ CONF_PRESETS = {
|
||||
}
|
||||
|
||||
CONF_PRESETS_WITH_AC = {
|
||||
p: f"{p}{PRESET_TEMP_SUFFIX}"
|
||||
p: f"{p}_temp"
|
||||
for p in (
|
||||
PRESET_FROST_PROTECTION,
|
||||
PRESET_ECO,
|
||||
PRESET_COMFORT,
|
||||
PRESET_BOOST,
|
||||
@@ -231,9 +102,8 @@ CONF_PRESETS_WITH_AC = {
|
||||
PRESET_AWAY_SUFFIX = "_away"
|
||||
|
||||
CONF_PRESETS_AWAY = {
|
||||
p: f"{p}{PRESET_TEMP_SUFFIX}"
|
||||
p: f"{p}_temp"
|
||||
for p in (
|
||||
PRESET_FROST_PROTECTION + PRESET_AWAY_SUFFIX,
|
||||
PRESET_ECO + PRESET_AWAY_SUFFIX,
|
||||
PRESET_COMFORT + PRESET_AWAY_SUFFIX,
|
||||
PRESET_BOOST + PRESET_AWAY_SUFFIX,
|
||||
@@ -241,9 +111,8 @@ CONF_PRESETS_AWAY = {
|
||||
}
|
||||
|
||||
CONF_PRESETS_AWAY_WITH_AC = {
|
||||
p: f"{p}{PRESET_TEMP_SUFFIX}"
|
||||
p: f"{p}_temp"
|
||||
for p in (
|
||||
PRESET_FROST_PROTECTION + PRESET_AWAY_SUFFIX,
|
||||
PRESET_ECO + PRESET_AWAY_SUFFIX,
|
||||
PRESET_COMFORT + PRESET_AWAY_SUFFIX,
|
||||
PRESET_BOOST + PRESET_AWAY_SUFFIX,
|
||||
@@ -253,12 +122,7 @@ CONF_PRESETS_AWAY_WITH_AC = {
|
||||
)
|
||||
}
|
||||
|
||||
CONF_PRESETS_SELECTIONABLE = [
|
||||
PRESET_FROST_PROTECTION,
|
||||
PRESET_ECO,
|
||||
PRESET_COMFORT,
|
||||
PRESET_BOOST,
|
||||
]
|
||||
CONF_PRESETS_SELECTIONABLE = [PRESET_ECO, PRESET_COMFORT, PRESET_BOOST]
|
||||
|
||||
CONF_PRESETS_VALUES = list(CONF_PRESETS.values())
|
||||
CONF_PRESETS_AWAY_VALUES = list(CONF_PRESETS_AWAY.values())
|
||||
@@ -268,14 +132,16 @@ CONF_PRESETS_AWAY_WITH_AC_VALUES = list(CONF_PRESETS_AWAY_WITH_AC.values())
|
||||
ALL_CONF = (
|
||||
[
|
||||
CONF_NAME,
|
||||
CONF_HEATER_KEEP_ALIVE,
|
||||
CONF_HEATER,
|
||||
CONF_HEATER_2,
|
||||
CONF_HEATER_3,
|
||||
CONF_HEATER_4,
|
||||
CONF_TEMP_SENSOR,
|
||||
CONF_EXTERNAL_TEMP_SENSOR,
|
||||
CONF_POWER_SENSOR,
|
||||
CONF_MAX_POWER_SENSOR,
|
||||
CONF_WINDOW_SENSOR,
|
||||
CONF_WINDOW_DELAY,
|
||||
CONF_WINDOW_OFF_DELAY,
|
||||
CONF_WINDOW_AUTO_OPEN_THRESHOLD,
|
||||
CONF_WINDOW_AUTO_CLOSE_THRESHOLD,
|
||||
CONF_WINDOW_AUTO_MAX_DURATION,
|
||||
@@ -290,41 +156,23 @@ ALL_CONF = (
|
||||
CONF_TPI_COEF_EXT,
|
||||
CONF_PRESENCE_SENSOR,
|
||||
CONF_MINIMAL_ACTIVATION_DELAY,
|
||||
CONF_MINIMAL_DEACTIVATION_DELAY,
|
||||
CONF_TEMP_MIN,
|
||||
CONF_TEMP_MAX,
|
||||
CONF_SAFETY_DELAY_MIN,
|
||||
CONF_SAFETY_MIN_ON_PERCENT,
|
||||
CONF_SAFETY_DEFAULT_ON_PERCENT,
|
||||
CONF_SECURITY_DELAY_MIN,
|
||||
CONF_SECURITY_MIN_ON_PERCENT,
|
||||
CONF_SECURITY_DEFAULT_ON_PERCENT,
|
||||
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_AUTO_REGULATION_MODE,
|
||||
CONF_AUTO_REGULATION_DTEMP,
|
||||
CONF_AUTO_REGULATION_PERIOD_MIN,
|
||||
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
|
||||
@@ -336,151 +184,20 @@ CONF_FUNCTIONS = [
|
||||
PROPORTIONAL_FUNCTION_TPI,
|
||||
]
|
||||
|
||||
CONF_AUTO_REGULATION_MODES = [
|
||||
CONF_AUTO_REGULATION_NONE,
|
||||
CONF_AUTO_REGULATION_VALVE,
|
||||
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_TYPES = [
|
||||
CONF_THERMOSTAT_CENTRAL_CONFIG,
|
||||
CONF_THERMOSTAT_SWITCH,
|
||||
CONF_THERMOSTAT_CLIMATE,
|
||||
CONF_THERMOSTAT_VALVE,
|
||||
]
|
||||
|
||||
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
|
||||
)
|
||||
SUPPORT_FLAGS = ClimateEntityFeature.TARGET_TEMPERATURE
|
||||
|
||||
SERVICE_SET_PRESENCE = "set_presence"
|
||||
SERVICE_SET_PRESET_TEMPERATURE = "set_preset_temperature"
|
||||
SERVICE_SET_SAFETY = "set_safety"
|
||||
SERVICE_SET_WINDOW_BYPASS = "set_window_bypass"
|
||||
SERVICE_SET_AUTO_REGULATION_MODE = "set_auto_regulation_mode"
|
||||
SERVICE_SET_AUTO_FAN_MODE = "set_auto_fan_mode"
|
||||
SERVICE_SET_SECURITY = "set_security"
|
||||
|
||||
DEFAULT_SAFETY_MIN_ON_PERCENT = 0.5
|
||||
DEFAULT_SAFETY_DEFAULT_ON_PERCENT = 0.1
|
||||
DEFAULT_SECURITY_MIN_ON_PERCENT = 0.5
|
||||
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
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
class RegulationParamStrong:
|
||||
"""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 = 8
|
||||
stabilization_threshold: float = 0.1
|
||||
accumulated_error_threshold: float = 80
|
||||
|
||||
|
||||
class EventType(Enum):
|
||||
"""The event type that can be sent"""
|
||||
@@ -489,53 +206,8 @@ 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)
|
||||
|
||||
|
||||
def get_safe_float(hass, entity_id: str):
|
||||
"""Get a safe float state value for an entity.
|
||||
Return None if entity is not available"""
|
||||
if (
|
||||
entity_id is None
|
||||
or not (state := hass.states.get(entity_id))
|
||||
or state.state is None
|
||||
or state.state == "None"
|
||||
or state.state == "unknown"
|
||||
or state.state == "unavailable"
|
||||
):
|
||||
return None
|
||||
float_val = float(state.state)
|
||||
return None if math.isinf(float_val) or not math.isfinite(float_val) else float_val
|
||||
|
||||
|
||||
def get_tz(hass: HomeAssistant):
|
||||
"""Get the current timezone"""
|
||||
|
||||
return dt_util.get_time_zone(hass.config.time_zone)
|
||||
|
||||
|
||||
class NowClass:
|
||||
"""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.
|
||||
"""
|
||||
return datetime.now(get_tz(hass))
|
||||
|
||||
|
||||
class UnknownEntity(HomeAssistantError):
|
||||
@@ -544,41 +216,3 @@ class UnknownEntity(HomeAssistantError):
|
||||
|
||||
class WindowOpenDetectionMethod(HomeAssistantError):
|
||||
"""Error to indicate there is an error in the window open detection method given."""
|
||||
|
||||
|
||||
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 ValveRegulationNbEntitiesIncorrect(HomeAssistantError):
|
||||
"""Error to indicate there is an error in the configuration of the TRV with valve regulation.
|
||||
The number of specific entities is incorrect."""
|
||||
|
||||
|
||||
class ValveRegulationMinOpeningDegreesIncorrect(HomeAssistantError):
|
||||
"""Error to indicate that the minimal opening degrees is not a list of int separated by coma"""
|
||||
|
||||
|
||||
class VirtualSwitchConfigurationIncorrect(HomeAssistantError):
|
||||
"""Error when a virtual switch is not configured correctly"""
|
||||
|
||||
|
||||
class overrides: # pylint: disable=invalid-name
|
||||
"""An annotation to inform overrides"""
|
||||
|
||||
def __init__(self, func):
|
||||
self.func = func
|
||||
|
||||
def __get__(self, instance, owner):
|
||||
return self.func.__get__(instance, owner)
|
||||
|
||||
def __call__(self, *args, **kwargs):
|
||||
raise RuntimeError(f"Method {self.func.__name__} should have been overridden")
|
||||
|
||||
@@ -1,92 +0,0 @@
|
||||
# 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)
|
||||
@@ -1,240 +0,0 @@
|
||||
""" Implements the Auto-start/stop Feature Manager """
|
||||
|
||||
# pylint: disable=line-too-long
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.core import (
|
||||
HomeAssistant,
|
||||
)
|
||||
from homeassistant.components.climate import HVACMode
|
||||
|
||||
from .const import * # pylint: disable=wildcard-import, unused-wildcard-import
|
||||
from .commons import ConfigData
|
||||
|
||||
from .base_manager import BaseFeatureManager
|
||||
|
||||
from .auto_start_stop_algorithm import (
|
||||
AutoStartStopDetectionAlgorithm,
|
||||
AUTO_START_STOP_ACTION_OFF,
|
||||
AUTO_START_STOP_ACTION_ON,
|
||||
)
|
||||
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class FeatureAutoStartStopManager(BaseFeatureManager):
|
||||
"""The implementation of the AutoStartStop feature"""
|
||||
|
||||
unrecorded_attributes = frozenset(
|
||||
{
|
||||
"auto_start_stop_level",
|
||||
"auto_start_stop_dtmin",
|
||||
"auto_start_stop_enable",
|
||||
"auto_start_stop_accumulated_error",
|
||||
"auto_start_stop_accumulated_error_threshold",
|
||||
"auto_start_stop_last_switch_date",
|
||||
}
|
||||
)
|
||||
|
||||
def __init__(self, vtherm: Any, hass: HomeAssistant):
|
||||
"""Init of a featureManager"""
|
||||
super().__init__(vtherm, hass)
|
||||
|
||||
self._auto_start_stop_level: TYPE_AUTO_START_STOP_LEVELS = (
|
||||
AUTO_START_STOP_LEVEL_NONE
|
||||
)
|
||||
self._auto_start_stop_algo: AutoStartStopDetectionAlgorithm | None = None
|
||||
self._is_configured: bool = False
|
||||
self._is_auto_start_stop_enabled: bool = False
|
||||
|
||||
@overrides
|
||||
def post_init(self, entry_infos: ConfigData):
|
||||
"""Reinit of the manager"""
|
||||
|
||||
use_auto_start_stop = entry_infos.get(CONF_USE_AUTO_START_STOP_FEATURE, False)
|
||||
if use_auto_start_stop:
|
||||
self._auto_start_stop_level = (
|
||||
entry_infos.get(CONF_AUTO_START_STOP_LEVEL, None)
|
||||
or AUTO_START_STOP_LEVEL_NONE
|
||||
)
|
||||
self._is_configured = True
|
||||
else:
|
||||
self._auto_start_stop_level = AUTO_START_STOP_LEVEL_NONE
|
||||
self._is_configured = False
|
||||
|
||||
# Instanciate the auto start stop algo
|
||||
self._auto_start_stop_algo = AutoStartStopDetectionAlgorithm(
|
||||
self._auto_start_stop_level, self.name
|
||||
)
|
||||
|
||||
# Fix an eventual incoherent state
|
||||
if self._vtherm.is_on and self._vtherm.hvac_off_reason == HVAC_OFF_REASON_AUTO_START_STOP:
|
||||
self._vtherm.hvac_off_reason = None
|
||||
|
||||
@overrides
|
||||
async def start_listening(self):
|
||||
"""Start listening the underlying entity"""
|
||||
|
||||
@overrides
|
||||
def stop_listening(self):
|
||||
"""Stop listening and remove the eventual timer still running"""
|
||||
|
||||
@overrides
|
||||
async def refresh_state(self) -> bool:
|
||||
"""Check the auto-start-stop and an eventual action
|
||||
Return False if we should stop the control_heating method"""
|
||||
|
||||
if not self._is_configured or not self._is_auto_start_stop_enabled:
|
||||
_LOGGER.debug("%s - auto start/stop is disabled (or not configured)", self)
|
||||
return True
|
||||
|
||||
slope = (
|
||||
self._vtherm.last_temperature_slope or 0
|
||||
) / 60 # to have the slope in °/min
|
||||
action = self._auto_start_stop_algo.calculate_action(
|
||||
self._vtherm.hvac_mode,
|
||||
self._vtherm.saved_hvac_mode,
|
||||
self._vtherm.target_temperature,
|
||||
self._vtherm.current_temperature,
|
||||
slope,
|
||||
self._vtherm.now,
|
||||
)
|
||||
_LOGGER.debug("%s - auto_start_stop action is %s", self, action)
|
||||
if action == AUTO_START_STOP_ACTION_OFF and self._vtherm.is_on:
|
||||
_LOGGER.info(
|
||||
"%s - Turning OFF the Vtherm due to auto-start-stop conditions",
|
||||
self,
|
||||
)
|
||||
self._vtherm.set_hvac_off_reason(HVAC_OFF_REASON_AUTO_START_STOP)
|
||||
await self._vtherm.async_turn_off()
|
||||
|
||||
# Send an event
|
||||
self._vtherm.send_event(
|
||||
event_type=EventType.AUTO_START_STOP_EVENT,
|
||||
data={
|
||||
"type": "stop",
|
||||
"name": self.name,
|
||||
"cause": "Auto stop conditions reached",
|
||||
"hvac_mode": self._vtherm.hvac_mode,
|
||||
"saved_hvac_mode": self._vtherm.saved_hvac_mode,
|
||||
"target_temperature": self._vtherm.target_temperature,
|
||||
"current_temperature": self._vtherm.current_temperature,
|
||||
"temperature_slope": round(slope, 3),
|
||||
"accumulated_error": self._auto_start_stop_algo.accumulated_error,
|
||||
"accumulated_error_threshold": self._auto_start_stop_algo.accumulated_error_threshold,
|
||||
},
|
||||
)
|
||||
|
||||
# Stop here
|
||||
return False
|
||||
elif (
|
||||
action == AUTO_START_STOP_ACTION_ON
|
||||
and self._vtherm.hvac_off_reason == HVAC_OFF_REASON_AUTO_START_STOP
|
||||
):
|
||||
_LOGGER.info(
|
||||
"%s - Turning ON the Vtherm due to auto-start-stop conditions", self
|
||||
)
|
||||
await self._vtherm.async_turn_on()
|
||||
|
||||
# Send an event
|
||||
self._vtherm.send_event(
|
||||
event_type=EventType.AUTO_START_STOP_EVENT,
|
||||
data={
|
||||
"type": "start",
|
||||
"name": self.name,
|
||||
"cause": "Auto start conditions reached",
|
||||
"hvac_mode": self._vtherm.hvac_mode,
|
||||
"saved_hvac_mode": self._vtherm.saved_hvac_mode,
|
||||
"target_temperature": self._vtherm.target_temperature,
|
||||
"current_temperature": self._vtherm.current_temperature,
|
||||
"temperature_slope": round(slope, 3),
|
||||
"accumulated_error": self._auto_start_stop_algo.accumulated_error,
|
||||
"accumulated_error_threshold": self._auto_start_stop_algo.accumulated_error_threshold,
|
||||
},
|
||||
)
|
||||
|
||||
self._vtherm.update_custom_attributes()
|
||||
|
||||
return True
|
||||
|
||||
def set_auto_start_stop_enable(self, is_enabled: bool):
|
||||
"""Enable/Disable the auto-start/stop feature"""
|
||||
self._is_auto_start_stop_enabled = is_enabled
|
||||
if (
|
||||
self._vtherm.hvac_mode == HVACMode.OFF
|
||||
and self._vtherm.hvac_off_reason == HVAC_OFF_REASON_AUTO_START_STOP
|
||||
):
|
||||
_LOGGER.debug(
|
||||
"%s - the vtherm is off cause auto-start/stop and enable have been set to false -> starts the VTherm"
|
||||
)
|
||||
self.hass.create_task(self._vtherm.async_turn_on())
|
||||
|
||||
# Send an event
|
||||
self._vtherm.send_event(
|
||||
event_type=EventType.AUTO_START_STOP_EVENT,
|
||||
data={
|
||||
"type": "start",
|
||||
"name": self.name,
|
||||
"cause": "Auto start stop disabled",
|
||||
"hvac_mode": self._vtherm.hvac_mode,
|
||||
"saved_hvac_mode": self._vtherm.saved_hvac_mode,
|
||||
"target_temperature": self._vtherm.target_temperature,
|
||||
"current_temperature": self._vtherm.current_temperature,
|
||||
"temperature_slope": round(
|
||||
self._vtherm.last_temperature_slope or 0, 3
|
||||
),
|
||||
"accumulated_error": self._auto_start_stop_algo.accumulated_error,
|
||||
"accumulated_error_threshold": self._auto_start_stop_algo.accumulated_error_threshold,
|
||||
},
|
||||
)
|
||||
|
||||
self._vtherm.update_custom_attributes()
|
||||
|
||||
def add_custom_attributes(self, extra_state_attributes: dict[str, Any]):
|
||||
"""Add some custom attributes"""
|
||||
extra_state_attributes.update(
|
||||
{
|
||||
"is_auto_start_stop_configured": self.is_configured,
|
||||
}
|
||||
)
|
||||
if self.is_configured:
|
||||
extra_state_attributes.update(
|
||||
{
|
||||
"auto_start_stop_enable": self.auto_start_stop_enable,
|
||||
"auto_start_stop_level": self._auto_start_stop_algo.level,
|
||||
"auto_start_stop_dtmin": self._auto_start_stop_algo.dt_min,
|
||||
"auto_start_stop_accumulated_error": self._auto_start_stop_algo.accumulated_error,
|
||||
"auto_start_stop_accumulated_error_threshold": self._auto_start_stop_algo.accumulated_error_threshold,
|
||||
"auto_start_stop_last_switch_date": self._auto_start_stop_algo.last_switch_date,
|
||||
}
|
||||
)
|
||||
|
||||
@overrides
|
||||
@property
|
||||
def is_configured(self) -> bool:
|
||||
"""Return True of the aiuto-start/stop feature is configured"""
|
||||
return self._is_configured
|
||||
|
||||
@property
|
||||
def auto_start_stop_level(self) -> TYPE_AUTO_START_STOP_LEVELS:
|
||||
"""Return the auto start/stop level."""
|
||||
return self._auto_start_stop_level
|
||||
|
||||
@property
|
||||
def auto_start_stop_enable(self) -> bool:
|
||||
"""Returns the auto_start_stop_enable"""
|
||||
return self._is_auto_start_stop_enabled
|
||||
|
||||
@property
|
||||
def is_auto_stopped(self) -> bool:
|
||||
"""Returns the is vtherm is stopped and reason is AUTO_START_STOP"""
|
||||
return (
|
||||
self._vtherm.hvac_mode == HVACMode.OFF
|
||||
and self._vtherm.hvac_off_reason == HVAC_OFF_REASON_AUTO_START_STOP
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return f"AutoStartStopManager-{self.name}"
|
||||
@@ -1,343 +0,0 @@
|
||||
""" Implements the Motion Feature Manager """
|
||||
|
||||
# pylint: disable=line-too-long
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
from datetime import timedelta
|
||||
|
||||
from homeassistant.const import (
|
||||
STATE_ON,
|
||||
STATE_OFF,
|
||||
STATE_UNAVAILABLE,
|
||||
STATE_UNKNOWN,
|
||||
)
|
||||
from homeassistant.core import (
|
||||
HomeAssistant,
|
||||
callback,
|
||||
Event,
|
||||
)
|
||||
from homeassistant.helpers.event import (
|
||||
async_track_state_change_event,
|
||||
EventStateChangedData,
|
||||
async_call_later,
|
||||
)
|
||||
|
||||
from homeassistant.components.climate import (
|
||||
PRESET_ACTIVITY,
|
||||
)
|
||||
|
||||
from homeassistant.exceptions import ConditionError
|
||||
from homeassistant.helpers import condition
|
||||
|
||||
from .const import * # pylint: disable=wildcard-import, unused-wildcard-import
|
||||
from .commons import ConfigData
|
||||
|
||||
from .base_manager import BaseFeatureManager
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class FeatureMotionManager(BaseFeatureManager):
|
||||
"""The implementation of the Motion feature"""
|
||||
|
||||
unrecorded_attributes = frozenset(
|
||||
{
|
||||
"motion_sensor_entity_id",
|
||||
"is_motion_configured",
|
||||
"motion_delay_sec",
|
||||
"motion_off_delay_sec",
|
||||
"motion_preset",
|
||||
"no_motion_preset",
|
||||
}
|
||||
)
|
||||
|
||||
def __init__(self, vtherm: Any, hass: HomeAssistant):
|
||||
"""Init of a featureManager"""
|
||||
super().__init__(vtherm, hass)
|
||||
self._motion_state: str = STATE_UNAVAILABLE
|
||||
self._motion_sensor_entity_id: str = None
|
||||
self._motion_delay_sec: int | None = 0
|
||||
self._motion_off_delay_sec: int | None = 0
|
||||
self._motion_preset: str | None = None
|
||||
self._no_motion_preset: str | None = None
|
||||
self._is_configured: bool = False
|
||||
self._motion_call_cancel: callable = None
|
||||
|
||||
@overrides
|
||||
def post_init(self, entry_infos: ConfigData):
|
||||
"""Reinit of the manager"""
|
||||
self.dearm_motion_timer()
|
||||
|
||||
self._motion_sensor_entity_id = entry_infos.get(CONF_MOTION_SENSOR, None)
|
||||
self._motion_delay_sec = entry_infos.get(CONF_MOTION_DELAY, 0)
|
||||
self._motion_off_delay_sec = entry_infos.get(CONF_MOTION_OFF_DELAY, None)
|
||||
if not self._motion_off_delay_sec:
|
||||
self._motion_off_delay_sec = self._motion_delay_sec
|
||||
|
||||
self._motion_preset = entry_infos.get(CONF_MOTION_PRESET)
|
||||
self._no_motion_preset = entry_infos.get(CONF_NO_MOTION_PRESET)
|
||||
if (
|
||||
self._motion_sensor_entity_id is not None
|
||||
and self._motion_preset is not None
|
||||
and self._no_motion_preset is not None
|
||||
):
|
||||
self._is_configured = True
|
||||
self._motion_state = STATE_UNKNOWN
|
||||
|
||||
@overrides
|
||||
async def start_listening(self):
|
||||
"""Start listening the underlying entity"""
|
||||
if self._is_configured:
|
||||
self.stop_listening()
|
||||
self.add_listener(
|
||||
async_track_state_change_event(
|
||||
self.hass,
|
||||
[self._motion_sensor_entity_id],
|
||||
self._motion_sensor_changed,
|
||||
)
|
||||
)
|
||||
|
||||
@overrides
|
||||
def stop_listening(self):
|
||||
"""Stop listening and remove the eventual timer still running"""
|
||||
self.dearm_motion_timer()
|
||||
super().stop_listening()
|
||||
|
||||
def dearm_motion_timer(self):
|
||||
"""Dearm the eventual motion time running"""
|
||||
if self._motion_call_cancel:
|
||||
self._motion_call_cancel()
|
||||
self._motion_call_cancel = None
|
||||
|
||||
@overrides
|
||||
async def refresh_state(self) -> bool:
|
||||
"""Tries to get the last state from sensor
|
||||
Returns True if a change has been made"""
|
||||
ret = False
|
||||
if self._is_configured:
|
||||
|
||||
motion_state = self.hass.states.get(self._motion_sensor_entity_id)
|
||||
if motion_state and motion_state.state not in (
|
||||
STATE_UNAVAILABLE,
|
||||
STATE_UNKNOWN,
|
||||
):
|
||||
_LOGGER.debug(
|
||||
"%s - Motion state have been retrieved: %s",
|
||||
self,
|
||||
self._motion_state,
|
||||
)
|
||||
# recalculate the right target_temp in activity mode
|
||||
ret = await self.update_motion_state(motion_state.state, False)
|
||||
|
||||
return ret
|
||||
|
||||
@callback
|
||||
async def _motion_sensor_changed(self, event: Event[EventStateChangedData]):
|
||||
"""Handle motion sensor changes."""
|
||||
new_state = event.data.get("new_state")
|
||||
_LOGGER.info(
|
||||
"%s - Motion changed. Event.new_state is %s, _attr_preset_mode=%s, activity=%s",
|
||||
self,
|
||||
new_state,
|
||||
self._vtherm.preset_mode,
|
||||
PRESET_ACTIVITY,
|
||||
)
|
||||
|
||||
if new_state is None or new_state.state not in (STATE_OFF, STATE_ON):
|
||||
return
|
||||
|
||||
# Check delay condition
|
||||
async def try_motion_condition(_):
|
||||
self.dearm_motion_timer()
|
||||
|
||||
try:
|
||||
delay = (
|
||||
self._motion_delay_sec
|
||||
if new_state.state == STATE_ON
|
||||
else self._motion_off_delay_sec
|
||||
)
|
||||
long_enough = condition.state(
|
||||
self.hass,
|
||||
self._motion_sensor_entity_id,
|
||||
new_state.state,
|
||||
timedelta(seconds=delay),
|
||||
)
|
||||
except ConditionError:
|
||||
long_enough = False
|
||||
|
||||
if not long_enough:
|
||||
_LOGGER.debug(
|
||||
"Motion delay condition is not satisfied (the sensor have change its state during the delay). Check motion sensor state"
|
||||
)
|
||||
# Get sensor current state
|
||||
motion_state = self.hass.states.get(self._motion_sensor_entity_id)
|
||||
_LOGGER.debug(
|
||||
"%s - motion_state=%s, new_state.state=%s",
|
||||
self,
|
||||
motion_state.state,
|
||||
new_state.state,
|
||||
)
|
||||
if (
|
||||
motion_state.state == new_state.state
|
||||
and new_state.state == STATE_ON
|
||||
):
|
||||
_LOGGER.debug(
|
||||
"%s - the motion sensor is finally 'on' after the delay", self
|
||||
)
|
||||
long_enough = True
|
||||
else:
|
||||
long_enough = False
|
||||
|
||||
if long_enough:
|
||||
_LOGGER.debug("%s - Motion delay condition is satisfied", self)
|
||||
await self.update_motion_state(new_state.state)
|
||||
else:
|
||||
await self.update_motion_state(
|
||||
STATE_ON if new_state.state == STATE_OFF else STATE_OFF
|
||||
)
|
||||
|
||||
im_on = self._motion_state == STATE_ON
|
||||
delay_running = self._motion_call_cancel is not None
|
||||
event_on = new_state.state == STATE_ON
|
||||
|
||||
def arm():
|
||||
"""Arm the timer"""
|
||||
delay = (
|
||||
self._motion_delay_sec
|
||||
if new_state.state == STATE_ON
|
||||
else self._motion_off_delay_sec
|
||||
)
|
||||
self._motion_call_cancel = async_call_later(
|
||||
self.hass, timedelta(seconds=delay), try_motion_condition
|
||||
)
|
||||
|
||||
# if I'm off
|
||||
if not im_on:
|
||||
if event_on and not delay_running:
|
||||
_LOGGER.debug(
|
||||
"%s - Arm delay cause i'm off and event is on and no delay is running",
|
||||
self,
|
||||
)
|
||||
arm()
|
||||
return try_motion_condition
|
||||
# Ignore the event
|
||||
_LOGGER.debug("%s - Event ignored cause i'm already off", self)
|
||||
return None
|
||||
else: # I'm On
|
||||
if not event_on and not delay_running:
|
||||
_LOGGER.info("%s - Arm delay cause i'm on and event is off", self)
|
||||
arm()
|
||||
return try_motion_condition
|
||||
if event_on and delay_running:
|
||||
_LOGGER.debug(
|
||||
"%s - Desarm off delay cause i'm on and event is on and a delay is running",
|
||||
self,
|
||||
)
|
||||
self.dearm_motion_timer()
|
||||
return None
|
||||
# Ignore the event
|
||||
_LOGGER.debug("%s - Event ignored cause i'm already on", self)
|
||||
return None
|
||||
|
||||
async def update_motion_state(
|
||||
self, new_state: str = None, recalculate: bool = True
|
||||
) -> bool:
|
||||
"""Update the value of the motion sensor and update the VTherm state accordingly
|
||||
Return true if a change has been made"""
|
||||
|
||||
_LOGGER.info("%s - Updating motion state. New state is %s", self, new_state)
|
||||
old_motion_state = self._motion_state
|
||||
if new_state is not None:
|
||||
self._motion_state = STATE_ON if new_state == STATE_ON else STATE_OFF
|
||||
|
||||
if self._vtherm.preset_mode == PRESET_ACTIVITY:
|
||||
new_preset = self.get_current_motion_preset()
|
||||
_LOGGER.info(
|
||||
"%s - Motion condition have changes. New preset temp will be %s",
|
||||
self,
|
||||
new_preset,
|
||||
)
|
||||
# We do not change the preset which is kept to ACTIVITY but only the target_temperature
|
||||
# We take the motion into account
|
||||
new_temp = self._vtherm.find_preset_temp(new_preset)
|
||||
old_temp = self._vtherm.target_temperature
|
||||
if new_temp != old_temp:
|
||||
await self._vtherm.change_target_temperature(new_temp)
|
||||
|
||||
if new_temp != old_temp and recalculate:
|
||||
self._vtherm.recalculate()
|
||||
await self._vtherm.async_control_heating(force=True)
|
||||
|
||||
return old_motion_state != self._motion_state
|
||||
|
||||
def get_current_motion_preset(self) -> str:
|
||||
"""Calculate and return the current motion preset"""
|
||||
return (
|
||||
self._motion_preset
|
||||
if self._motion_state == STATE_ON
|
||||
else self._no_motion_preset
|
||||
)
|
||||
|
||||
def add_custom_attributes(self, extra_state_attributes: dict[str, Any]):
|
||||
"""Add some custom attributes"""
|
||||
extra_state_attributes.update(
|
||||
{
|
||||
"motion_sensor_entity_id": self._motion_sensor_entity_id,
|
||||
"motion_state": self._motion_state,
|
||||
"is_motion_configured": self._is_configured,
|
||||
"motion_delay_sec": self._motion_delay_sec,
|
||||
"motion_off_delay_sec": self._motion_off_delay_sec,
|
||||
"motion_preset": self._motion_preset,
|
||||
"no_motion_preset": self._no_motion_preset,
|
||||
}
|
||||
)
|
||||
|
||||
@overrides
|
||||
@property
|
||||
def is_configured(self) -> bool:
|
||||
"""Return True of the motion is configured"""
|
||||
return self._is_configured
|
||||
|
||||
@property
|
||||
def motion_state(self) -> str | None:
|
||||
"""Return the current motion state STATE_ON or STATE_OFF
|
||||
or STATE_UNAVAILABLE if not configured"""
|
||||
if not self._is_configured:
|
||||
return STATE_UNAVAILABLE
|
||||
return self._motion_state
|
||||
|
||||
@property
|
||||
def is_motion_detected(self) -> bool:
|
||||
"""Return true if the motion is configured and motion sensor is OFF"""
|
||||
return self._is_configured and self._motion_state in [
|
||||
STATE_ON,
|
||||
]
|
||||
|
||||
@property
|
||||
def motion_sensor_entity_id(self) -> bool:
|
||||
"""Return true if the motion is configured and motion sensor is OFF"""
|
||||
return self._motion_sensor_entity_id
|
||||
|
||||
@property
|
||||
def motion_delay_sec(self) -> bool:
|
||||
"""Return the motion delay"""
|
||||
return self._motion_delay_sec
|
||||
|
||||
@property
|
||||
def motion_off_delay_sec(self) -> bool:
|
||||
"""Return motion delay off"""
|
||||
return self._motion_off_delay_sec
|
||||
|
||||
@property
|
||||
def motion_preset(self) -> bool:
|
||||
"""Return motion preset"""
|
||||
return self._motion_preset
|
||||
|
||||
@property
|
||||
def no_motion_preset(self) -> bool:
|
||||
"""Return no motion preset"""
|
||||
return self._no_motion_preset
|
||||
|
||||
def __str__(self):
|
||||
return f"MotionManager-{self.name}"
|
||||
@@ -1,268 +0,0 @@
|
||||
""" Implements the Power Feature Manager """
|
||||
|
||||
# pylint: disable=line-too-long
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.const import (
|
||||
STATE_ON,
|
||||
STATE_OFF,
|
||||
STATE_UNAVAILABLE,
|
||||
STATE_UNKNOWN,
|
||||
)
|
||||
|
||||
from homeassistant.core import (
|
||||
HomeAssistant,
|
||||
)
|
||||
|
||||
from .const import * # pylint: disable=wildcard-import, unused-wildcard-import
|
||||
from .commons import ConfigData
|
||||
|
||||
from .base_manager import BaseFeatureManager
|
||||
from .vtherm_api import VersatileThermostatAPI
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class FeaturePowerManager(BaseFeatureManager):
|
||||
"""The implementation of the Power feature"""
|
||||
|
||||
unrecorded_attributes = frozenset(
|
||||
{
|
||||
"power_sensor_entity_id",
|
||||
"max_power_sensor_entity_id",
|
||||
"is_power_configured",
|
||||
"device_power",
|
||||
"power_temp",
|
||||
"current_power",
|
||||
"current_max_power",
|
||||
}
|
||||
)
|
||||
|
||||
def __init__(self, vtherm: Any, hass: HomeAssistant):
|
||||
"""Init of a featureManager"""
|
||||
super().__init__(vtherm, hass)
|
||||
self._power_temp = None
|
||||
self._overpowering_state = STATE_UNAVAILABLE
|
||||
self._is_configured: bool = False
|
||||
self._device_power: float = 0
|
||||
self._use_power_feature: bool = False
|
||||
|
||||
@overrides
|
||||
def post_init(self, entry_infos: ConfigData):
|
||||
"""Reinit of the manager"""
|
||||
|
||||
# Power management
|
||||
self._power_temp = entry_infos.get(CONF_PRESET_POWER)
|
||||
|
||||
self._device_power = entry_infos.get(CONF_DEVICE_POWER) or 0
|
||||
self._use_power_feature = entry_infos.get(CONF_USE_POWER_FEATURE, False)
|
||||
self._is_configured = False
|
||||
|
||||
@overrides
|
||||
async def start_listening(self):
|
||||
"""Start listening the underlying entity. There is nothing to listen"""
|
||||
central_power_configuration = (
|
||||
VersatileThermostatAPI.get_vtherm_api().central_power_manager.is_configured
|
||||
)
|
||||
|
||||
if self._use_power_feature and self._device_power and central_power_configuration:
|
||||
self._is_configured = True
|
||||
# Try to restore _overpowering_state from previous state
|
||||
old_state = await self._vtherm.async_get_last_state()
|
||||
self._overpowering_state = STATE_ON if old_state and old_state.attributes and old_state.attributes.get("overpowering_state") == STATE_ON else STATE_UNKNOWN
|
||||
else:
|
||||
if self._use_power_feature:
|
||||
if not central_power_configuration:
|
||||
_LOGGER.warning(
|
||||
"%s - Power management is not fully configured. You have to configure the central configuration power",
|
||||
self,
|
||||
)
|
||||
else:
|
||||
_LOGGER.warning(
|
||||
"%s - Power management is not fully configured. You have to configure the power feature of the VTherm",
|
||||
self,
|
||||
)
|
||||
|
||||
def add_custom_attributes(self, extra_state_attributes: dict[str, Any]):
|
||||
"""Add some custom attributes"""
|
||||
vtherm_api = VersatileThermostatAPI.get_vtherm_api()
|
||||
extra_state_attributes.update(
|
||||
{
|
||||
"power_sensor_entity_id": vtherm_api.central_power_manager.power_sensor_entity_id,
|
||||
"max_power_sensor_entity_id": vtherm_api.central_power_manager.max_power_sensor_entity_id,
|
||||
"overpowering_state": self._overpowering_state,
|
||||
"is_power_configured": self._is_configured,
|
||||
"device_power": self._device_power,
|
||||
"power_temp": self._power_temp,
|
||||
"current_power": vtherm_api.central_power_manager.current_power,
|
||||
"current_max_power": vtherm_api.central_power_manager.current_max_power,
|
||||
"mean_cycle_power": self.mean_cycle_power,
|
||||
}
|
||||
)
|
||||
|
||||
async def check_power_available(self) -> bool:
|
||||
"""Check if the Vtherm can be started considering overpowering.
|
||||
Returns True if no overpowering conditions are found.
|
||||
If True the vtherm power is written into the temporay vtherm started
|
||||
"""
|
||||
|
||||
vtherm_api = VersatileThermostatAPI.get_vtherm_api()
|
||||
if (
|
||||
not self._is_configured
|
||||
or not vtherm_api.central_power_manager.is_configured
|
||||
):
|
||||
return True
|
||||
|
||||
current_power = vtherm_api.central_power_manager.current_power
|
||||
current_max_power = vtherm_api.central_power_manager.current_max_power
|
||||
started_vtherm_total_power = vtherm_api.central_power_manager.started_vtherm_total_power
|
||||
if (
|
||||
current_power is None
|
||||
or current_max_power is None
|
||||
or self._device_power is None
|
||||
):
|
||||
_LOGGER.warning(
|
||||
"%s - power not valued. check_power_available not available", self
|
||||
)
|
||||
return True
|
||||
|
||||
_LOGGER.debug(
|
||||
"%s - overpowering check: power=%.3f, max_power=%.3f heater power=%.3f",
|
||||
self,
|
||||
current_power,
|
||||
current_max_power,
|
||||
self._device_power,
|
||||
)
|
||||
|
||||
# issue 407 - power_consumption_max is power we need to add. If already active we don't need to add more power
|
||||
if self._vtherm.is_device_active:
|
||||
power_consumption_max = 0
|
||||
else:
|
||||
if self._vtherm.is_over_climate:
|
||||
power_consumption_max = self._device_power
|
||||
else:
|
||||
power_consumption_max = max(
|
||||
self._device_power / self._vtherm.nb_underlying_entities,
|
||||
self._device_power * self._vtherm.proportional_algorithm.on_percent,
|
||||
)
|
||||
|
||||
ret = (current_power + started_vtherm_total_power + power_consumption_max) < current_max_power
|
||||
if not ret:
|
||||
_LOGGER.info(
|
||||
"%s - there is not enough power available power=%.3f, max_power=%.3f heater power=%.3f",
|
||||
self,
|
||||
current_power,
|
||||
current_max_power,
|
||||
self._device_power,
|
||||
)
|
||||
else:
|
||||
# Adds the current_power_max to the started vtherm total power
|
||||
vtherm_api.central_power_manager.add_started_vtherm_total_power(power_consumption_max)
|
||||
|
||||
return ret
|
||||
|
||||
async def set_overpowering(self, overpowering: bool, power_consumption_max=0):
|
||||
"""Force the overpowering state for the VTherm"""
|
||||
|
||||
vtherm_api = VersatileThermostatAPI.get_vtherm_api()
|
||||
current_power = vtherm_api.central_power_manager.current_power
|
||||
current_max_power = vtherm_api.central_power_manager.current_max_power
|
||||
|
||||
if overpowering and not self.is_overpowering_detected:
|
||||
_LOGGER.warning(
|
||||
"%s - overpowering is detected. Heater preset will be set to 'power'",
|
||||
self,
|
||||
)
|
||||
|
||||
self._overpowering_state = STATE_ON
|
||||
|
||||
if self._vtherm.is_over_climate:
|
||||
self._vtherm.save_hvac_mode()
|
||||
|
||||
self._vtherm.save_preset_mode()
|
||||
await self._vtherm.async_underlying_entity_turn_off()
|
||||
await self._vtherm.async_set_preset_mode_internal(PRESET_POWER, force=True)
|
||||
self._vtherm.send_event(
|
||||
EventType.POWER_EVENT,
|
||||
{
|
||||
"type": "start",
|
||||
"current_power": current_power,
|
||||
"device_power": self._device_power,
|
||||
"current_max_power": current_max_power,
|
||||
"current_power_consumption": power_consumption_max,
|
||||
},
|
||||
)
|
||||
elif not overpowering and self.is_overpowering_detected:
|
||||
_LOGGER.warning(
|
||||
"%s - end of overpowering is detected. Heater preset will be restored to '%s'",
|
||||
self,
|
||||
self._vtherm._saved_preset_mode, # pylint: disable=protected-access
|
||||
)
|
||||
self._overpowering_state = STATE_OFF
|
||||
|
||||
# restore state
|
||||
if self._vtherm.is_over_climate:
|
||||
await self._vtherm.restore_hvac_mode()
|
||||
await self._vtherm.restore_preset_mode()
|
||||
# restart cycle
|
||||
await self._vtherm.async_control_heating(force=True)
|
||||
self._vtherm.send_event(
|
||||
EventType.POWER_EVENT,
|
||||
{
|
||||
"type": "end",
|
||||
"current_power": current_power,
|
||||
"device_power": self._device_power,
|
||||
"current_max_power": current_max_power,
|
||||
},
|
||||
)
|
||||
elif not overpowering and self._overpowering_state != STATE_OFF:
|
||||
# just set to not overpowering the state which was not set
|
||||
self._overpowering_state = STATE_OFF
|
||||
else:
|
||||
# Nothing to do (already in the right state)
|
||||
return
|
||||
self._vtherm.update_custom_attributes()
|
||||
|
||||
@overrides
|
||||
@property
|
||||
def is_configured(self) -> bool:
|
||||
"""Return True of the presence is configured"""
|
||||
return self._is_configured
|
||||
|
||||
@property
|
||||
def overpowering_state(self) -> str | None:
|
||||
"""Return the current overpowering state STATE_ON or STATE_OFF
|
||||
or STATE_UNAVAILABLE if not configured"""
|
||||
if not self._is_configured:
|
||||
return STATE_UNAVAILABLE
|
||||
return self._overpowering_state
|
||||
|
||||
@property
|
||||
def is_overpowering_detected(self) -> str | None:
|
||||
"""Return True if the Vtherm is in overpowering state"""
|
||||
return self._overpowering_state == STATE_ON
|
||||
|
||||
@property
|
||||
def power_temperature(self) -> bool:
|
||||
"""Return the power temperature"""
|
||||
return self._power_temp
|
||||
|
||||
@property
|
||||
def device_power(self) -> bool:
|
||||
"""Return the device power"""
|
||||
return self._device_power
|
||||
|
||||
@property
|
||||
def mean_cycle_power(self) -> float | None:
|
||||
"""Returns the mean power consumption during the cycle"""
|
||||
if not self._device_power or not self._vtherm.proportional_algorithm:
|
||||
return None
|
||||
|
||||
return float(
|
||||
self._device_power * self._vtherm.proportional_algorithm.on_percent
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return f"PowerManager-{self.name}"
|
||||
@@ -1,200 +0,0 @@
|
||||
""" Implements the Presence Feature Manager """
|
||||
|
||||
# pylint: disable=line-too-long
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.const import (
|
||||
STATE_ON,
|
||||
STATE_OFF,
|
||||
STATE_HOME,
|
||||
STATE_NOT_HOME,
|
||||
STATE_UNAVAILABLE,
|
||||
STATE_UNKNOWN,
|
||||
)
|
||||
from homeassistant.core import (
|
||||
HomeAssistant,
|
||||
callback,
|
||||
Event,
|
||||
)
|
||||
from homeassistant.helpers.event import (
|
||||
async_track_state_change_event,
|
||||
EventStateChangedData,
|
||||
)
|
||||
|
||||
from homeassistant.components.climate import PRESET_ACTIVITY, PRESET_BOOST, PRESET_COMFORT, PRESET_ECO
|
||||
|
||||
from .const import * # pylint: disable=wildcard-import, unused-wildcard-import
|
||||
from .commons import ConfigData
|
||||
|
||||
from .base_manager import BaseFeatureManager
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class FeaturePresenceManager(BaseFeatureManager):
|
||||
"""The implementation of the Presence feature"""
|
||||
|
||||
unrecorded_attributes = frozenset(
|
||||
{
|
||||
"presence_sensor_entity_id",
|
||||
"is_presence_configured",
|
||||
}
|
||||
)
|
||||
|
||||
def __init__(self, vtherm: Any, hass: HomeAssistant):
|
||||
"""Init of a featureManager"""
|
||||
super().__init__(vtherm, hass)
|
||||
self._presence_state: str = STATE_UNAVAILABLE
|
||||
self._presence_sensor_entity_id: str = None
|
||||
self._is_configured: bool = False
|
||||
|
||||
@overrides
|
||||
def post_init(self, entry_infos: ConfigData):
|
||||
"""Reinit of the manager"""
|
||||
self._presence_sensor_entity_id = entry_infos.get(CONF_PRESENCE_SENSOR)
|
||||
if (
|
||||
entry_infos.get(CONF_USE_PRESENCE_FEATURE, False)
|
||||
and self._presence_sensor_entity_id is not None
|
||||
):
|
||||
self._is_configured = True
|
||||
self._presence_state = STATE_UNKNOWN
|
||||
|
||||
@overrides
|
||||
async def start_listening(self):
|
||||
"""Start listening the underlying entity"""
|
||||
if self._is_configured:
|
||||
self.stop_listening()
|
||||
self.add_listener(
|
||||
async_track_state_change_event(
|
||||
self.hass,
|
||||
[self._presence_sensor_entity_id],
|
||||
self._presence_sensor_changed,
|
||||
)
|
||||
)
|
||||
|
||||
@overrides
|
||||
async def refresh_state(self) -> bool:
|
||||
"""Tries to get the last state from sensor
|
||||
Returns True if a change has been made"""
|
||||
ret = False
|
||||
if self._is_configured:
|
||||
# try to acquire presence entity state
|
||||
presence_state = self.hass.states.get(self._presence_sensor_entity_id)
|
||||
if presence_state and presence_state.state not in (
|
||||
STATE_UNAVAILABLE,
|
||||
STATE_UNKNOWN,
|
||||
):
|
||||
ret = await self.update_presence(presence_state.state)
|
||||
_LOGGER.debug(
|
||||
"%s - Presence have been retrieved: %s",
|
||||
self,
|
||||
presence_state.state,
|
||||
)
|
||||
return ret
|
||||
|
||||
@callback
|
||||
async def _presence_sensor_changed(self, event: Event[EventStateChangedData]):
|
||||
"""Handle presence changes."""
|
||||
new_state = event.data.get("new_state")
|
||||
_LOGGER.info(
|
||||
"%s - Presence changed. Event.new_state is %s, _attr_preset_mode=%s, activity=%s",
|
||||
self,
|
||||
new_state,
|
||||
self._vtherm.preset_mode,
|
||||
PRESET_ACTIVITY,
|
||||
)
|
||||
if new_state is None:
|
||||
return
|
||||
|
||||
if await self.update_presence(new_state.state):
|
||||
await self._vtherm.async_control_heating(force=True)
|
||||
|
||||
async def update_presence(self, new_state: str) -> bool:
|
||||
"""Update the value of the presence sensor and update the VTherm state accordingly
|
||||
Return true if a change has been made"""
|
||||
|
||||
_LOGGER.info("%s - Updating presence. New state is %s", self, new_state)
|
||||
old_presence_state = self._presence_state
|
||||
self._presence_state = (
|
||||
STATE_ON if new_state in (STATE_ON, STATE_HOME) else STATE_OFF
|
||||
)
|
||||
if self._vtherm.preset_mode in HIDDEN_PRESETS or self._is_configured is False:
|
||||
_LOGGER.info(
|
||||
"%s - Ignoring presence change cause in Power or Security preset or presence not configured",
|
||||
self,
|
||||
)
|
||||
return old_presence_state != self._presence_state
|
||||
|
||||
if new_state is None or new_state not in (
|
||||
STATE_OFF,
|
||||
STATE_ON,
|
||||
STATE_HOME,
|
||||
STATE_NOT_HOME,
|
||||
):
|
||||
self._presence_state = STATE_UNKNOWN
|
||||
return old_presence_state != self._presence_state
|
||||
|
||||
if self._vtherm.preset_mode not in [
|
||||
PRESET_BOOST,
|
||||
PRESET_COMFORT,
|
||||
PRESET_ECO,
|
||||
PRESET_ACTIVITY,
|
||||
PRESET_FROST_PROTECTION,
|
||||
]:
|
||||
return old_presence_state != self._presence_state
|
||||
|
||||
new_temp = self._vtherm.find_preset_temp(self._vtherm.preset_mode)
|
||||
if new_temp is not None:
|
||||
_LOGGER.debug(
|
||||
"%s - presence change in temperature mode new_temp will be: %.2f",
|
||||
self,
|
||||
new_temp,
|
||||
)
|
||||
await self._vtherm.change_target_temperature(new_temp)
|
||||
self._vtherm.recalculate()
|
||||
|
||||
return True
|
||||
|
||||
return old_presence_state != self._presence_state
|
||||
|
||||
def add_custom_attributes(self, extra_state_attributes: dict[str, Any]):
|
||||
"""Add some custom attributes"""
|
||||
extra_state_attributes.update(
|
||||
{
|
||||
"presence_sensor_entity_id": self._presence_sensor_entity_id,
|
||||
"presence_state": self._presence_state,
|
||||
"is_presence_configured": self._is_configured,
|
||||
}
|
||||
)
|
||||
|
||||
@overrides
|
||||
@property
|
||||
def is_configured(self) -> bool:
|
||||
"""Return True of the presence is configured"""
|
||||
return self._is_configured
|
||||
|
||||
@property
|
||||
def presence_state(self) -> str | None:
|
||||
"""Return the current presence state STATE_ON or STATE_OFF
|
||||
or STATE_UNAVAILABLE if not configured"""
|
||||
if not self._is_configured:
|
||||
return STATE_UNAVAILABLE
|
||||
return self._presence_state
|
||||
|
||||
@property
|
||||
def is_absence_detected(self) -> bool:
|
||||
"""Return true if the presence is configured and presence sensor is OFF"""
|
||||
return self._is_configured and self._presence_state in [
|
||||
STATE_NOT_HOME,
|
||||
STATE_OFF,
|
||||
]
|
||||
|
||||
@property
|
||||
def presence_sensor_entity_id(self) -> bool:
|
||||
"""Return true if the presence is configured and presence sensor is OFF"""
|
||||
return self._presence_sensor_entity_id
|
||||
|
||||
def __str__(self):
|
||||
return f"PresenceManager-{self.name}"
|
||||
@@ -1,322 +0,0 @@
|
||||
# pylint: disable=line-too-long
|
||||
|
||||
""" Implements the Safety as a Feature Manager"""
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.const import (
|
||||
STATE_ON,
|
||||
STATE_OFF,
|
||||
STATE_UNAVAILABLE,
|
||||
STATE_UNKNOWN,
|
||||
)
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.components.climate import HVACMode, HVACAction
|
||||
|
||||
from .const import * # pylint: disable=wildcard-import, unused-wildcard-import
|
||||
from .commons import ConfigData
|
||||
|
||||
from .base_manager import BaseFeatureManager
|
||||
from .vtherm_api import VersatileThermostatAPI
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class FeatureSafetyManager(BaseFeatureManager):
|
||||
"""The implementation of the Safety feature"""
|
||||
|
||||
unrecorded_attributes = frozenset(
|
||||
{
|
||||
"safety_delay_min",
|
||||
"safety_min_on_percent",
|
||||
"safety_default_on_percent",
|
||||
"is_safety_configured",
|
||||
}
|
||||
)
|
||||
|
||||
def __init__(self, vtherm: Any, hass: HomeAssistant):
|
||||
"""Init of a featureManager"""
|
||||
super().__init__(vtherm, hass)
|
||||
|
||||
self._is_configured: bool = False
|
||||
self._safety_delay_min = None
|
||||
self._safety_min_on_percent = None
|
||||
self._safety_default_on_percent = None
|
||||
self._safety_state = STATE_UNAVAILABLE
|
||||
|
||||
@overrides
|
||||
def post_init(self, entry_infos: ConfigData):
|
||||
"""Reinit of the manager"""
|
||||
self._safety_delay_min = entry_infos.get(CONF_SAFETY_DELAY_MIN)
|
||||
self._safety_min_on_percent = (
|
||||
entry_infos.get(CONF_SAFETY_MIN_ON_PERCENT)
|
||||
if entry_infos.get(CONF_SAFETY_MIN_ON_PERCENT) is not None
|
||||
else DEFAULT_SAFETY_MIN_ON_PERCENT
|
||||
)
|
||||
self._safety_default_on_percent = (
|
||||
entry_infos.get(CONF_SAFETY_DEFAULT_ON_PERCENT)
|
||||
if entry_infos.get(CONF_SAFETY_DEFAULT_ON_PERCENT) is not None
|
||||
else DEFAULT_SAFETY_DEFAULT_ON_PERCENT
|
||||
)
|
||||
|
||||
if (
|
||||
self._safety_delay_min is not None
|
||||
and self._safety_default_on_percent is not None
|
||||
and self._safety_default_on_percent is not None
|
||||
):
|
||||
self._safety_state = STATE_UNKNOWN
|
||||
self._is_configured = True
|
||||
|
||||
@overrides
|
||||
async def start_listening(self):
|
||||
"""Start listening the underlying entity"""
|
||||
|
||||
@overrides
|
||||
def stop_listening(self):
|
||||
"""Stop listening and remove the eventual timer still running"""
|
||||
|
||||
@overrides
|
||||
async def refresh_state(self) -> bool:
|
||||
"""Check the safety and an eventual action
|
||||
Return True is safety should be active"""
|
||||
|
||||
if not self._is_configured:
|
||||
_LOGGER.debug("%s - safety is disabled (or not configured)", self)
|
||||
return False
|
||||
|
||||
now = self._vtherm.now
|
||||
current_tz = dt_util.get_time_zone(self._hass.config.time_zone)
|
||||
|
||||
is_safety_detected = self.is_safety_detected
|
||||
|
||||
delta_temp = (
|
||||
now - self._vtherm.last_temperature_measure.replace(tzinfo=current_tz)
|
||||
).total_seconds() / 60.0
|
||||
delta_ext_temp = (
|
||||
now - self._vtherm.last_ext_temperature_measure.replace(tzinfo=current_tz)
|
||||
).total_seconds() / 60.0
|
||||
|
||||
mode_cond = self._vtherm.hvac_mode != HVACMode.OFF
|
||||
|
||||
api: VersatileThermostatAPI = VersatileThermostatAPI.get_vtherm_api()
|
||||
is_outdoor_checked = (
|
||||
not api.safety_mode
|
||||
or api.safety_mode.get("check_outdoor_sensor") is not False
|
||||
)
|
||||
|
||||
temp_cond: bool = delta_temp > self._safety_delay_min or (
|
||||
is_outdoor_checked and delta_ext_temp > self._safety_delay_min
|
||||
)
|
||||
climate_cond: bool = (
|
||||
self._vtherm.is_over_climate
|
||||
and self._vtherm.hvac_action
|
||||
not in [
|
||||
HVACAction.COOLING,
|
||||
HVACAction.IDLE,
|
||||
]
|
||||
)
|
||||
switch_cond: bool = (
|
||||
not self._vtherm.is_over_climate
|
||||
and self._vtherm.proportional_algorithm is not None
|
||||
and self._vtherm.proportional_algorithm.calculated_on_percent
|
||||
>= self._safety_min_on_percent
|
||||
)
|
||||
|
||||
_LOGGER.debug(
|
||||
"%s - checking safety delta_temp=%.1f delta_ext_temp=%.1f mod_cond=%s temp_cond=%s climate_cond=%s switch_cond=%s",
|
||||
self,
|
||||
delta_temp,
|
||||
delta_ext_temp,
|
||||
mode_cond,
|
||||
temp_cond,
|
||||
climate_cond,
|
||||
switch_cond,
|
||||
)
|
||||
|
||||
# Issue 99 - a climate is regulated by the device itself and not by VTherm. So a VTherm should never be in safety !
|
||||
should_climate_be_in_security = False # temp_cond and climate_cond
|
||||
should_switch_be_in_security = temp_cond and switch_cond
|
||||
should_be_in_security = (
|
||||
should_climate_be_in_security or should_switch_be_in_security
|
||||
)
|
||||
|
||||
should_start_security = (
|
||||
mode_cond and not is_safety_detected and should_be_in_security
|
||||
)
|
||||
# attr_preset_mode is not necessary normaly. It is just here to be sure
|
||||
should_stop_security = (
|
||||
is_safety_detected
|
||||
and not should_be_in_security
|
||||
and self._vtherm.preset_mode == PRESET_SAFETY
|
||||
)
|
||||
|
||||
# Logging and event
|
||||
if should_start_security:
|
||||
if should_climate_be_in_security:
|
||||
_LOGGER.warning(
|
||||
"%s - No temperature received for more than %.1f minutes (dt=%.1f, dext=%.1f) and underlying climate is %s. Setting it into safety mode",
|
||||
self,
|
||||
self._safety_delay_min,
|
||||
delta_temp,
|
||||
delta_ext_temp,
|
||||
self.hvac_action,
|
||||
)
|
||||
elif should_switch_be_in_security:
|
||||
_LOGGER.warning(
|
||||
"%s - No temperature received for more than %.1f minutes (dt=%.1f, dext=%.1f) and on_percent (%.2f %%) is over defined value (%.2f %%). Set it into safety mode",
|
||||
self,
|
||||
self._safety_delay_min,
|
||||
delta_temp,
|
||||
delta_ext_temp,
|
||||
self._vtherm.proportional_algorithm.on_percent * 100,
|
||||
self._safety_min_on_percent * 100,
|
||||
)
|
||||
|
||||
self._vtherm.send_event(
|
||||
EventType.TEMPERATURE_EVENT,
|
||||
{
|
||||
"last_temperature_measure": self._vtherm.last_temperature_measure.replace(
|
||||
tzinfo=current_tz
|
||||
).isoformat(),
|
||||
"last_ext_temperature_measure": self._vtherm.last_ext_temperature_measure.replace(
|
||||
tzinfo=current_tz
|
||||
).isoformat(),
|
||||
"current_temp": self._vtherm.current_temperature,
|
||||
"current_ext_temp": self._vtherm.current_outdoor_temperature,
|
||||
"target_temp": self._vtherm.target_temperature,
|
||||
},
|
||||
)
|
||||
|
||||
# Start safety mode
|
||||
if should_start_security:
|
||||
self._safety_state = STATE_ON
|
||||
self._vtherm.save_hvac_mode()
|
||||
self._vtherm.save_preset_mode()
|
||||
if self._vtherm.proportional_algorithm:
|
||||
self._vtherm.proportional_algorithm.set_safety(
|
||||
self._safety_default_on_percent
|
||||
)
|
||||
await self._vtherm.async_set_preset_mode_internal(PRESET_SAFETY)
|
||||
# Turn off the underlying climate or heater if safety default on_percent is 0
|
||||
if self._vtherm.is_over_climate or self._safety_default_on_percent <= 0.0:
|
||||
await self._vtherm.async_set_hvac_mode(HVACMode.OFF, False)
|
||||
|
||||
self._vtherm.send_event(
|
||||
EventType.SECURITY_EVENT,
|
||||
{
|
||||
"type": "start",
|
||||
"last_temperature_measure": self._vtherm.last_temperature_measure.replace(
|
||||
tzinfo=current_tz
|
||||
).isoformat(),
|
||||
"last_ext_temperature_measure": self._vtherm.last_ext_temperature_measure.replace(
|
||||
tzinfo=current_tz
|
||||
).isoformat(),
|
||||
"current_temp": self._vtherm.current_temperature,
|
||||
"current_ext_temp": self._vtherm.current_outdoor_temperature,
|
||||
"target_temp": self._vtherm.target_temperature,
|
||||
},
|
||||
)
|
||||
|
||||
# Stop safety mode
|
||||
elif should_stop_security:
|
||||
_LOGGER.warning(
|
||||
"%s - End of safety mode. restoring hvac_mode to %s and preset_mode to %s",
|
||||
self,
|
||||
self._vtherm.saved_hvac_mode,
|
||||
self._vtherm.saved_preset_mode,
|
||||
)
|
||||
self._safety_state = STATE_OFF
|
||||
if self._vtherm.proportional_algorithm:
|
||||
self._vtherm.proportional_algorithm.unset_safety()
|
||||
# Restore hvac_mode if previously saved
|
||||
if self._vtherm.is_over_climate or self._safety_default_on_percent <= 0.0:
|
||||
await self._vtherm.restore_hvac_mode(False)
|
||||
await self._vtherm.restore_preset_mode()
|
||||
self._vtherm.send_event(
|
||||
EventType.SECURITY_EVENT,
|
||||
{
|
||||
"type": "end",
|
||||
"last_temperature_measure": self._vtherm.last_temperature_measure.replace(
|
||||
tzinfo=current_tz
|
||||
).isoformat(),
|
||||
"last_ext_temperature_measure": self._vtherm.last_ext_temperature_measure.replace(
|
||||
tzinfo=current_tz
|
||||
).isoformat(),
|
||||
"current_temp": self._vtherm.current_temperature,
|
||||
"current_ext_temp": self._vtherm.current_outdoor_temperature,
|
||||
"target_temp": self._vtherm.target_temperature,
|
||||
},
|
||||
)
|
||||
|
||||
# Initialize the safety_state if not already done
|
||||
elif not should_be_in_security and self._safety_state in [STATE_UNKNOWN]:
|
||||
self._safety_state = STATE_OFF
|
||||
|
||||
return should_be_in_security
|
||||
|
||||
def add_custom_attributes(self, extra_state_attributes: dict[str, Any]):
|
||||
"""Add some custom attributes"""
|
||||
|
||||
extra_state_attributes.update(
|
||||
{
|
||||
"is_safety_configured": self._is_configured,
|
||||
"safety_state": self._safety_state,
|
||||
}
|
||||
)
|
||||
|
||||
if self._is_configured:
|
||||
extra_state_attributes.update(
|
||||
{
|
||||
"safety_delay_min": self._safety_delay_min,
|
||||
"safety_min_on_percent": self._safety_min_on_percent,
|
||||
"safety_default_on_percent": self._safety_default_on_percent,
|
||||
}
|
||||
)
|
||||
|
||||
@overrides
|
||||
@property
|
||||
def is_configured(self) -> bool:
|
||||
"""Return True of the safety feature is configured"""
|
||||
return self._is_configured
|
||||
|
||||
def set_safety_delay_min(self, safety_delay_min):
|
||||
"""Set the delay min"""
|
||||
self._safety_delay_min = safety_delay_min
|
||||
|
||||
def set_safety_min_on_percent(self, safety_min_on_percent):
|
||||
"""Set the min on percent"""
|
||||
self._safety_min_on_percent = safety_min_on_percent
|
||||
|
||||
def set_safety_default_on_percent(self, safety_default_on_percent):
|
||||
"""Set the default on_percent"""
|
||||
self._safety_default_on_percent = safety_default_on_percent
|
||||
|
||||
@property
|
||||
def is_safety_detected(self) -> bool:
|
||||
"""Returns the is vtherm is in safety mode"""
|
||||
return self._safety_state == STATE_ON
|
||||
|
||||
@property
|
||||
def safety_state(self) -> str:
|
||||
"""Returns the safety state: STATE_ON, STATE_OFF, STATE_UNKWNON, STATE_UNAVAILABLE"""
|
||||
return self._safety_state
|
||||
|
||||
@property
|
||||
def safety_delay_min(self) -> bool:
|
||||
"""Returns the safety delay min"""
|
||||
return self._safety_delay_min
|
||||
|
||||
@property
|
||||
def safety_min_on_percent(self) -> bool:
|
||||
"""Returns the safety min on percent"""
|
||||
return self._safety_min_on_percent
|
||||
|
||||
@property
|
||||
def safety_default_on_percent(self) -> bool:
|
||||
"""Returns the safety safety_default_on_percent"""
|
||||
return self._safety_default_on_percent
|
||||
|
||||
def __str__(self):
|
||||
return f"SafetyManager-{self.name}"
|
||||
@@ -1,560 +0,0 @@
|
||||
""" Implements the Window Feature Manager """
|
||||
|
||||
# pylint: disable=line-too-long
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
from datetime import timedelta
|
||||
|
||||
from homeassistant.const import (
|
||||
STATE_ON,
|
||||
STATE_OFF,
|
||||
STATE_UNAVAILABLE,
|
||||
STATE_UNKNOWN,
|
||||
)
|
||||
from homeassistant.core import (
|
||||
HomeAssistant,
|
||||
callback,
|
||||
Event,
|
||||
)
|
||||
from homeassistant.helpers.event import (
|
||||
async_track_state_change_event,
|
||||
EventStateChangedData,
|
||||
async_call_later,
|
||||
)
|
||||
|
||||
from homeassistant.components.climate import HVACMode
|
||||
|
||||
from homeassistant.exceptions import ConditionError
|
||||
from homeassistant.helpers import condition
|
||||
|
||||
from .const import * # pylint: disable=wildcard-import, unused-wildcard-import
|
||||
from .commons import ConfigData
|
||||
|
||||
from .base_manager import BaseFeatureManager
|
||||
from .open_window_algorithm import WindowOpenDetectionAlgorithm
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class FeatureWindowManager(BaseFeatureManager):
|
||||
"""The implementation of the Window feature"""
|
||||
|
||||
unrecorded_attributes = frozenset(
|
||||
{
|
||||
"window_sensor_entity_id",
|
||||
"is_window_configured",
|
||||
"window_delay_sec",
|
||||
"window_off_delay_sec",
|
||||
"window_auto_configured",
|
||||
"window_auto_open_threshold",
|
||||
"window_auto_close_threshold",
|
||||
"window_auto_max_duration",
|
||||
"window_action",
|
||||
}
|
||||
)
|
||||
|
||||
def __init__(self, vtherm: Any, hass: HomeAssistant):
|
||||
"""Init of a featureManager"""
|
||||
super().__init__(vtherm, hass)
|
||||
self._window_sensor_entity_id: str = None
|
||||
self._window_state: str = STATE_UNAVAILABLE
|
||||
self._window_auto_open_threshold: float = 0
|
||||
self._window_auto_close_threshold: float = 0
|
||||
self._window_auto_max_duration: int = 0
|
||||
self._window_auto_state: bool = False
|
||||
self._window_auto_algo: WindowOpenDetectionAlgorithm = None
|
||||
self._is_window_bypass: bool = False
|
||||
self._window_action: str = None
|
||||
self._window_delay_sec: int | None = 0
|
||||
self._window_off_delay_sec: int | None = 0
|
||||
self._is_configured: bool = False
|
||||
self._is_window_auto_configured: bool = False
|
||||
self._window_call_cancel: callable = None
|
||||
|
||||
@overrides
|
||||
def post_init(self, entry_infos: ConfigData):
|
||||
"""Reinit of the manager"""
|
||||
self.dearm_window_timer()
|
||||
|
||||
self._window_auto_state = STATE_UNAVAILABLE
|
||||
self._window_state = STATE_UNAVAILABLE
|
||||
|
||||
self._window_sensor_entity_id = entry_infos.get(CONF_WINDOW_SENSOR)
|
||||
self._window_delay_sec = entry_infos.get(CONF_WINDOW_DELAY)
|
||||
# default is the WINDOW_ON delay if not configured
|
||||
self._window_off_delay_sec = entry_infos.get(CONF_WINDOW_OFF_DELAY, self._window_delay_sec)
|
||||
|
||||
self._window_action = (
|
||||
entry_infos.get(CONF_WINDOW_ACTION) or CONF_WINDOW_TURN_OFF
|
||||
)
|
||||
|
||||
self._window_auto_open_threshold = entry_infos.get(
|
||||
CONF_WINDOW_AUTO_OPEN_THRESHOLD
|
||||
)
|
||||
self._window_auto_close_threshold = entry_infos.get(
|
||||
CONF_WINDOW_AUTO_CLOSE_THRESHOLD
|
||||
)
|
||||
self._window_auto_max_duration = entry_infos.get(CONF_WINDOW_AUTO_MAX_DURATION)
|
||||
|
||||
use_window_feature = entry_infos.get(CONF_USE_WINDOW_FEATURE, False)
|
||||
|
||||
if ( # pylint: disable=too-many-boolean-expressions
|
||||
use_window_feature
|
||||
and self._window_sensor_entity_id is None
|
||||
and self._window_auto_open_threshold is not None
|
||||
and self._window_auto_open_threshold > 0.0
|
||||
and self._window_auto_close_threshold is not None
|
||||
and self._window_auto_max_duration is not None
|
||||
and self._window_auto_max_duration > 0
|
||||
and self._window_action is not None
|
||||
):
|
||||
self._is_window_auto_configured = True
|
||||
self._window_auto_state = STATE_UNKNOWN
|
||||
|
||||
self._window_auto_algo = WindowOpenDetectionAlgorithm(
|
||||
alert_threshold=self._window_auto_open_threshold,
|
||||
end_alert_threshold=self._window_auto_close_threshold,
|
||||
)
|
||||
|
||||
if self._is_window_auto_configured or (
|
||||
use_window_feature
|
||||
and self._window_sensor_entity_id is not None
|
||||
and self._window_delay_sec is not None
|
||||
and self._window_action is not None
|
||||
):
|
||||
self._is_configured = True
|
||||
self._window_state = STATE_UNKNOWN
|
||||
|
||||
@overrides
|
||||
async def start_listening(self):
|
||||
"""Start listening the underlying entity"""
|
||||
|
||||
#Try to get last window bypass state
|
||||
old_state = await self._vtherm.async_get_last_state()
|
||||
self._is_window_bypass = True if old_state and old_state.attributes and old_state.attributes.get("is_window_bypass") == True else False
|
||||
|
||||
if self._is_configured:
|
||||
self.stop_listening()
|
||||
if self._window_sensor_entity_id:
|
||||
self.add_listener(
|
||||
async_track_state_change_event(
|
||||
self.hass,
|
||||
[self._window_sensor_entity_id],
|
||||
self._window_sensor_changed,
|
||||
)
|
||||
)
|
||||
|
||||
@overrides
|
||||
def stop_listening(self):
|
||||
"""Stop listening and remove the eventual timer still running"""
|
||||
self.dearm_window_timer()
|
||||
super().stop_listening()
|
||||
|
||||
def dearm_window_timer(self):
|
||||
"""Dearm the eventual motion time running"""
|
||||
if self._window_call_cancel:
|
||||
self._window_call_cancel()
|
||||
self._window_call_cancel = None
|
||||
|
||||
@overrides
|
||||
async def refresh_state(self) -> bool:
|
||||
"""Tries to get the last state from sensor
|
||||
Returns True if a change has been made"""
|
||||
ret = False
|
||||
if self._is_configured and self._window_sensor_entity_id is not None:
|
||||
|
||||
window_state = self.hass.states.get(self._window_sensor_entity_id)
|
||||
if window_state and window_state.state not in (
|
||||
STATE_UNAVAILABLE,
|
||||
STATE_UNKNOWN,
|
||||
):
|
||||
_LOGGER.debug(
|
||||
"%s - Window state have been retrieved: %s",
|
||||
self,
|
||||
self._window_state,
|
||||
)
|
||||
# recalculate the right target_temp in activity mode
|
||||
ret = await self.update_window_state(window_state.state)
|
||||
|
||||
return ret
|
||||
|
||||
@callback
|
||||
async def _window_sensor_changed(self, event: Event[EventStateChangedData]):
|
||||
"""Handle window sensor changes."""
|
||||
new_state = event.data.get("new_state")
|
||||
old_state = event.data.get("old_state")
|
||||
_LOGGER.info(
|
||||
"%s - Window changed. Event.new_state is %s, _hvac_mode=%s, _saved_hvac_mode=%s",
|
||||
self,
|
||||
new_state,
|
||||
self._vtherm.hvac_mode,
|
||||
self._vtherm.saved_hvac_mode,
|
||||
)
|
||||
|
||||
# Check delay condition
|
||||
async def try_window_condition(_):
|
||||
try:
|
||||
long_enough = condition.state(
|
||||
self._hass,
|
||||
self._window_sensor_entity_id,
|
||||
new_state.state,
|
||||
timedelta(seconds=delay),
|
||||
)
|
||||
except ConditionError:
|
||||
long_enough = False
|
||||
|
||||
if not long_enough:
|
||||
_LOGGER.debug(
|
||||
"Window delay condition is not satisfied. Ignore window event"
|
||||
)
|
||||
self._window_state = old_state.state or STATE_OFF
|
||||
return
|
||||
|
||||
_LOGGER.debug("%s - Window delay condition is satisfied", self)
|
||||
|
||||
if self._window_state == new_state.state:
|
||||
_LOGGER.debug("%s - no change in window state. Forget the event")
|
||||
return
|
||||
|
||||
_LOGGER.debug("%s - Window ByPass is : %s", self, self._is_window_bypass)
|
||||
if self._is_window_bypass:
|
||||
_LOGGER.info(
|
||||
"%s - Window ByPass is activated. Ignore window event", self
|
||||
)
|
||||
# We change tne state but we don't apply the change
|
||||
self._window_state = new_state.state
|
||||
else:
|
||||
await self.update_window_state(new_state.state)
|
||||
|
||||
self._vtherm.update_custom_attributes()
|
||||
|
||||
delay = self._window_delay_sec if new_state.state == STATE_ON else self._window_off_delay_sec
|
||||
if new_state is None or old_state is None or new_state.state == old_state.state:
|
||||
return try_window_condition
|
||||
|
||||
self.dearm_window_timer()
|
||||
self._window_call_cancel = async_call_later(self.hass, timedelta(seconds=delay), try_window_condition)
|
||||
# For testing purpose we need to access the inner function
|
||||
return try_window_condition
|
||||
|
||||
async def update_window_state(self, new_state: str = None) -> bool:
|
||||
"""Change the window detection state.
|
||||
new_state is on if an open window have been detected or off else
|
||||
return True if the state have changed
|
||||
"""
|
||||
|
||||
if self._window_state == new_state:
|
||||
return False
|
||||
|
||||
if new_state != STATE_ON:
|
||||
_LOGGER.info(
|
||||
"%s - Window is closed. Restoring hvac_mode '%s' if stopped by window detection or temperature %s",
|
||||
self,
|
||||
self._vtherm.saved_hvac_mode,
|
||||
self._vtherm.saved_target_temp,
|
||||
)
|
||||
|
||||
if self._window_action in [
|
||||
CONF_WINDOW_FROST_TEMP,
|
||||
CONF_WINDOW_ECO_TEMP,
|
||||
]:
|
||||
await self._vtherm.restore_target_temp()
|
||||
|
||||
# default to TURN_OFF
|
||||
elif self._window_action in [CONF_WINDOW_TURN_OFF]:
|
||||
if (
|
||||
self._vtherm.last_central_mode != CENTRAL_MODE_STOPPED
|
||||
and self._vtherm.hvac_off_reason == HVAC_OFF_REASON_WINDOW_DETECTION
|
||||
):
|
||||
self._vtherm.set_hvac_off_reason(None)
|
||||
await self._vtherm.restore_hvac_mode(True)
|
||||
elif self._window_action in [CONF_WINDOW_FAN_ONLY]:
|
||||
if self._vtherm.last_central_mode != CENTRAL_MODE_STOPPED:
|
||||
self._vtherm.set_hvac_off_reason(None)
|
||||
await self._vtherm.restore_hvac_mode(True)
|
||||
else:
|
||||
_LOGGER.error(
|
||||
"%s - undefined window_action %s. Please open a bug in the github of this project with this log",
|
||||
self,
|
||||
self._window_action,
|
||||
)
|
||||
return False
|
||||
else:
|
||||
_LOGGER.info(
|
||||
"%s - Window is open. Apply window action %s", self, self._window_action
|
||||
)
|
||||
if self._window_action == CONF_WINDOW_TURN_OFF and not self._vtherm.is_on:
|
||||
_LOGGER.debug(
|
||||
"%s is already off. Forget turning off VTherm due to window detection"
|
||||
)
|
||||
self._window_state = new_state
|
||||
return False
|
||||
|
||||
# self._window_state = new_state
|
||||
if self._vtherm.last_central_mode in [CENTRAL_MODE_AUTO, None]:
|
||||
if self._window_action in [
|
||||
CONF_WINDOW_TURN_OFF,
|
||||
CONF_WINDOW_FAN_ONLY,
|
||||
]:
|
||||
self._vtherm.save_hvac_mode()
|
||||
elif self._window_action in [
|
||||
CONF_WINDOW_FROST_TEMP,
|
||||
CONF_WINDOW_ECO_TEMP,
|
||||
]:
|
||||
self._vtherm.save_target_temp()
|
||||
|
||||
if (
|
||||
self._window_action == CONF_WINDOW_FAN_ONLY
|
||||
and HVACMode.FAN_ONLY in self._vtherm.hvac_modes
|
||||
):
|
||||
await self._vtherm.async_set_hvac_mode(HVACMode.FAN_ONLY)
|
||||
elif (
|
||||
self._window_action == CONF_WINDOW_FROST_TEMP
|
||||
and self._vtherm.is_preset_configured(PRESET_FROST_PROTECTION)
|
||||
):
|
||||
await self._vtherm.change_target_temperature(
|
||||
self._vtherm.find_preset_temp(PRESET_FROST_PROTECTION)
|
||||
)
|
||||
elif (
|
||||
self._window_action == CONF_WINDOW_ECO_TEMP
|
||||
and self._vtherm.is_preset_configured(PRESET_ECO)
|
||||
):
|
||||
await self._vtherm.change_target_temperature(
|
||||
self._vtherm.find_preset_temp(PRESET_ECO)
|
||||
)
|
||||
else: # default is to turn_off
|
||||
self._vtherm.set_hvac_off_reason(HVAC_OFF_REASON_WINDOW_DETECTION)
|
||||
await self._vtherm.async_set_hvac_mode(HVACMode.OFF)
|
||||
|
||||
self._window_state = new_state
|
||||
return True
|
||||
|
||||
async def manage_window_auto(self, in_cycle=False) -> callable:
|
||||
"""The management of the window auto feature
|
||||
Returns the dearm function used to deactivate the window auto"""
|
||||
|
||||
async def dearm_window_auto(_):
|
||||
"""Callback that will be called after end of WINDOW_AUTO_MAX_DURATION"""
|
||||
_LOGGER.info("Unset window auto because MAX_DURATION is exceeded")
|
||||
await deactivate_window_auto(auto=True)
|
||||
|
||||
async def deactivate_window_auto(auto=False):
|
||||
"""Deactivation of the Window auto state"""
|
||||
_LOGGER.warning(
|
||||
"%s - End auto detection of open window slope=%.3f", self, slope
|
||||
)
|
||||
# Send an event
|
||||
cause = "max duration expiration" if auto else "end of slope alert"
|
||||
self._vtherm.send_event(
|
||||
EventType.WINDOW_AUTO_EVENT,
|
||||
{"type": "end", "cause": cause, "curve_slope": slope},
|
||||
)
|
||||
# Set attributes
|
||||
self._window_auto_state = STATE_OFF
|
||||
await self.update_window_state(self._window_auto_state)
|
||||
# await self.restore_hvac_mode(True)
|
||||
|
||||
self.dearm_window_timer()
|
||||
|
||||
if not self._window_auto_algo:
|
||||
return None
|
||||
|
||||
if in_cycle:
|
||||
slope = self._window_auto_algo.check_age_last_measurement(
|
||||
temperature=self._vtherm.ema_temperature,
|
||||
datetime_now=self._vtherm.now,
|
||||
)
|
||||
else:
|
||||
slope = self._window_auto_algo.add_temp_measurement(
|
||||
temperature=self._vtherm.ema_temperature,
|
||||
datetime_measure=self._vtherm.last_temperature_measure,
|
||||
)
|
||||
|
||||
_LOGGER.debug(
|
||||
"%s - Window auto is on, check the alert. last slope is %.3f",
|
||||
self,
|
||||
slope if slope is not None else 0.0,
|
||||
)
|
||||
|
||||
if self.is_window_bypass or not self._is_window_auto_configured:
|
||||
_LOGGER.debug(
|
||||
"%s - Window auto event is ignored because bypass is ON or window auto detection is disabled",
|
||||
self,
|
||||
)
|
||||
return None
|
||||
|
||||
if (
|
||||
self._window_auto_algo.is_window_open_detected()
|
||||
and self._window_auto_state in [STATE_UNKNOWN, STATE_OFF]
|
||||
and self._vtherm.hvac_mode != HVACMode.OFF
|
||||
):
|
||||
if (
|
||||
self._vtherm.proportional_algorithm
|
||||
and self._vtherm.proportional_algorithm.on_percent <= 0.0
|
||||
):
|
||||
_LOGGER.info(
|
||||
"%s - Start auto detection of open window slope=%.3f but no heating detected (on_percent<=0). Forget the event",
|
||||
self,
|
||||
slope,
|
||||
)
|
||||
return dearm_window_auto
|
||||
|
||||
_LOGGER.warning(
|
||||
"%s - Start auto detection of open window slope=%.3f", self, slope
|
||||
)
|
||||
|
||||
# Send an event
|
||||
self._vtherm.send_event(
|
||||
EventType.WINDOW_AUTO_EVENT,
|
||||
{"type": "start", "cause": "slope alert", "curve_slope": slope},
|
||||
)
|
||||
# Set attributes
|
||||
self._window_auto_state = STATE_ON
|
||||
await self.update_window_state(self._window_auto_state)
|
||||
|
||||
# Arm the end trigger
|
||||
self.dearm_window_timer()
|
||||
self._window_call_cancel = async_call_later(
|
||||
self.hass,
|
||||
timedelta(minutes=self._window_auto_max_duration),
|
||||
dearm_window_auto,
|
||||
)
|
||||
|
||||
elif (
|
||||
self._window_auto_algo.is_window_close_detected()
|
||||
and self._window_auto_state == STATE_ON
|
||||
):
|
||||
await deactivate_window_auto(False)
|
||||
|
||||
# For testing purpose we need to return the inner function
|
||||
return dearm_window_auto
|
||||
|
||||
def add_custom_attributes(self, extra_state_attributes: dict[str, Any]):
|
||||
"""Add some custom attributes"""
|
||||
extra_state_attributes.update(
|
||||
{
|
||||
"window_state": self.window_state,
|
||||
"window_auto_state": self.window_auto_state,
|
||||
"window_action": self.window_action,
|
||||
"is_window_bypass": self._is_window_bypass,
|
||||
"window_sensor_entity_id": self._window_sensor_entity_id,
|
||||
"window_delay_sec": self._window_delay_sec,
|
||||
"window_off_delay_sec": self._window_off_delay_sec,
|
||||
"is_window_configured": self._is_configured,
|
||||
"is_window_auto_configured": self._is_window_auto_configured,
|
||||
"window_auto_open_threshold": self._window_auto_open_threshold,
|
||||
"window_auto_close_threshold": self._window_auto_close_threshold,
|
||||
"window_auto_max_duration": self._window_auto_max_duration,
|
||||
}
|
||||
)
|
||||
|
||||
async def set_window_bypass(self, window_bypass: bool) -> bool:
|
||||
"""Set the window bypass flag
|
||||
Return True if state have been changed"""
|
||||
self._is_window_bypass = window_bypass
|
||||
|
||||
if self._window_state == STATE_ON:
|
||||
if not self._is_window_bypass:
|
||||
_LOGGER.info(
|
||||
"%s - Last window state was open & ByPass is now off. Set hvac_mode to '%s'",
|
||||
self,
|
||||
HVACMode.OFF,
|
||||
)
|
||||
self._vtherm.save_hvac_mode()
|
||||
await self._vtherm.async_set_hvac_mode(HVACMode.OFF)
|
||||
|
||||
if self._is_window_bypass:
|
||||
_LOGGER.info(
|
||||
"%s - Last window state was open & ByPass is now on. Set hvac_mode to last available mode",
|
||||
self,
|
||||
)
|
||||
await self._vtherm.restore_hvac_mode(True)
|
||||
return True
|
||||
return False
|
||||
|
||||
@overrides
|
||||
@property
|
||||
def is_configured(self) -> bool:
|
||||
"""Return True of the window feature is configured"""
|
||||
return self._is_configured
|
||||
|
||||
@property
|
||||
def is_window_auto_configured(self) -> bool:
|
||||
"""Return True of the window automatic detection is configured"""
|
||||
return self._is_window_auto_configured
|
||||
|
||||
@property
|
||||
def window_state(self) -> str | None:
|
||||
"""Return the current window state STATE_ON or STATE_OFF
|
||||
or STATE_UNAVAILABLE if not configured"""
|
||||
if not self._is_configured:
|
||||
return STATE_UNAVAILABLE
|
||||
return self._window_state
|
||||
|
||||
@property
|
||||
def window_auto_state(self) -> str | None:
|
||||
"""Return the current window auto state STATE_ON or STATE_OFF
|
||||
or STATE_UNAVAILABLE if not configured"""
|
||||
if not self._is_configured:
|
||||
return STATE_UNAVAILABLE
|
||||
return self._window_auto_state
|
||||
|
||||
@property
|
||||
def is_window_bypass(self) -> str | None:
|
||||
"""Return True if the window bypass is activated"""
|
||||
if not self._is_configured:
|
||||
return False
|
||||
return self._is_window_bypass
|
||||
|
||||
@property
|
||||
def is_window_detected(self) -> bool:
|
||||
"""Return true if the window is configured and open and bypass is not ON"""
|
||||
return self._is_configured and (
|
||||
self._window_state == STATE_ON or self._window_auto_state == STATE_ON
|
||||
) and not self._is_window_bypass
|
||||
|
||||
@property
|
||||
def window_sensor_entity_id(self) -> bool:
|
||||
"""Return true if the presence is configured and presence sensor is OFF"""
|
||||
return self._window_sensor_entity_id
|
||||
|
||||
@property
|
||||
def window_delay_sec(self) -> bool:
|
||||
"""Return the window on delay"""
|
||||
return self._window_delay_sec
|
||||
|
||||
@property
|
||||
def window_off_delay_sec(self) -> bool:
|
||||
"""Return the window off delay"""
|
||||
return self._window_off_delay_sec
|
||||
|
||||
@property
|
||||
def window_action(self) -> bool:
|
||||
"""Return the window action"""
|
||||
return self._window_action
|
||||
|
||||
@property
|
||||
def window_auto_open_threshold(self) -> bool:
|
||||
"""Return the window_auto_open_threshold"""
|
||||
return self._window_auto_open_threshold
|
||||
|
||||
@property
|
||||
def window_auto_close_threshold(self) -> bool:
|
||||
"""Return the window_auto_close_threshold"""
|
||||
return self._window_auto_close_threshold
|
||||
|
||||
@property
|
||||
def window_auto_max_duration(self) -> bool:
|
||||
"""Return the window_auto_max_duration"""
|
||||
return self._window_auto_max_duration
|
||||
|
||||
@property
|
||||
def last_slope(self) -> float:
|
||||
"""Return the last slope (in °C/hour)"""
|
||||
if not self._window_auto_algo:
|
||||
return None
|
||||
return self._window_auto_algo.last_slope
|
||||
|
||||
def __str__(self):
|
||||
return f"WindowManager-{self.name}"
|
||||
@@ -1,18 +0,0 @@
|
||||
{
|
||||
"entity": {
|
||||
"climate": {
|
||||
"versatile_thermostat": {
|
||||
"state_attributes": {
|
||||
"preset_mode": {
|
||||
"state": {
|
||||
"shedding": "mdi:power-plug-off",
|
||||
"safety": "mdi:shield-alert",
|
||||
"none": "mdi:knob",
|
||||
"frost": "mdi:snowflake"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,138 +0,0 @@
|
||||
"""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": "7.2.1",
|
||||
"version": "3.0.0",
|
||||
"zeroconf": []
|
||||
}
|
||||
@@ -1,521 +0,0 @@
|
||||
# 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 .base_entity 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"""
|
||||
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,4 +1,3 @@
|
||||
# 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
|
||||
@@ -13,44 +12,28 @@ from datetime import datetime
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
# To filter bad values
|
||||
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
|
||||
MIN_DELTA_T_SEC = 10 # two temp mesure should be > 10 sec
|
||||
MAX_SLOPE_VALUE = 2 # slope cannot be > 2 or < -2 -> else this is an aberrant point
|
||||
|
||||
|
||||
class WindowOpenDetectionAlgorithm:
|
||||
"""The class that implements the algorithm listed above"""
|
||||
|
||||
_alert_threshold: float
|
||||
_end_alert_threshold: float
|
||||
_last_slope: float
|
||||
_last_datetime: datetime
|
||||
_last_temperature: float
|
||||
|
||||
def __init__(self, alert_threshold, end_alert_threshold) -> None:
|
||||
"""Initalize a new algorithm with the both threshold"""
|
||||
self._alert_threshold: float = alert_threshold
|
||||
self._end_alert_threshold: float = end_alert_threshold
|
||||
self._last_slope: float | None = None
|
||||
self._last_datetime: datetime = None
|
||||
self._last_temperature: float | None = None
|
||||
self._nb_point: int = 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
|
||||
self._alert_threshold = alert_threshold
|
||||
self._end_alert_threshold = end_alert_threshold
|
||||
self._last_slope = None
|
||||
self._last_datetime = None
|
||||
|
||||
def add_temp_measurement(
|
||||
self, temperature: float, datetime_measure: datetime, store_date: bool = True
|
||||
self, temperature: float, datetime_measure: datetime
|
||||
) -> float:
|
||||
"""Add a new temperature measurement
|
||||
returns the last slope
|
||||
@@ -59,7 +42,6 @@ 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(
|
||||
@@ -79,10 +61,8 @@ class WindowOpenDetectionAlgorithm:
|
||||
)
|
||||
return lspe
|
||||
|
||||
delta_t_hour = delta_t / 60.0
|
||||
|
||||
delta_temp = float(temperature - self._last_temperature)
|
||||
new_slope = delta_temp / delta_t_hour
|
||||
new_slope = delta_temp / delta_t
|
||||
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",
|
||||
@@ -92,28 +72,21 @@ class WindowOpenDetectionAlgorithm:
|
||||
return lspe
|
||||
|
||||
if self._last_slope is None:
|
||||
self._last_slope = round(new_slope, 2)
|
||||
self._last_slope = new_slope
|
||||
else:
|
||||
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_slope = (0.5 * self._last_slope) + (0.5 * new_slope)
|
||||
|
||||
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 nb_point=%s",
|
||||
"delta_t=%.3f delta_temp=%.3f new_slope=%.3f last_slope=%s slope=%.3f",
|
||||
delta_t,
|
||||
delta_temp,
|
||||
new_slope,
|
||||
lspe,
|
||||
self._last_slope,
|
||||
self._nb_point,
|
||||
)
|
||||
|
||||
return self._last_slope
|
||||
|
||||
def is_window_open_detected(self) -> bool:
|
||||
@@ -121,20 +94,22 @@ class WindowOpenDetectionAlgorithm:
|
||||
if self._alert_threshold is None:
|
||||
return False
|
||||
|
||||
if self._nb_point < MIN_NB_POINT or self._last_slope is None:
|
||||
return False
|
||||
|
||||
return self._last_slope < -self._alert_threshold
|
||||
return (
|
||||
self._last_slope < -self._alert_threshold
|
||||
if self._last_slope is not None
|
||||
else False
|
||||
)
|
||||
|
||||
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
|
||||
|
||||
if self._nb_point < MIN_NB_POINT or self._last_slope is None:
|
||||
return False
|
||||
|
||||
return self._last_slope >= self._end_alert_threshold
|
||||
return (
|
||||
self._last_slope >= self._end_alert_threshold
|
||||
if self._last_slope is not None
|
||||
else False
|
||||
)
|
||||
|
||||
@property
|
||||
def last_slope(self) -> float:
|
||||
|
||||
@@ -1,109 +0,0 @@
|
||||
# pylint: disable=line-too-long
|
||||
""" The PI algorithm implementation """
|
||||
|
||||
import logging
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class PITemperatureRegulator:
|
||||
"""A class implementing a PI Algorithm
|
||||
PI algorithms calculate a target temperature by adding an offset which is calculating as follow:
|
||||
- offset = kp * error + ki * accumulated_error
|
||||
|
||||
To use it you must:
|
||||
- instanciate the class and gives the algorithm parameters: kp, ki, offset_max, stabilization_threshold, accumulated_error_threshold
|
||||
- call calculate_regulated_temperature with the internal and external temperature
|
||||
- call set_target_temp when the target temperature change.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
target_temp: float,
|
||||
kp: float,
|
||||
ki: float,
|
||||
k_ext: float,
|
||||
offset_max: float,
|
||||
stabilization_threshold: float,
|
||||
accumulated_error_threshold: float,
|
||||
):
|
||||
self.target_temp: float = target_temp
|
||||
self.kp: float = kp # proportionnel gain
|
||||
self.ki: float = ki # integral gain
|
||||
self.k_ext: float = k_ext # exterior gain
|
||||
self.offset_max: float = offset_max
|
||||
self.stabilization_threshold: float = stabilization_threshold
|
||||
self.accumulated_error: float = 0
|
||||
self.accumulated_error_threshold: float = accumulated_error_threshold
|
||||
|
||||
def reset_accumulated_error(self):
|
||||
"""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
|
||||
# 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, room_temp: float, external_temp: float
|
||||
): # pylint: disable=unused-argument
|
||||
"""Calculate a new target_temp given some temperature"""
|
||||
if room_temp is None:
|
||||
_LOGGER.warning(
|
||||
"Temporarily skipping the self-regulation algorithm while the configured sensor for room temperature is unavailable"
|
||||
)
|
||||
return self.target_temp
|
||||
if external_temp is None:
|
||||
_LOGGER.warning(
|
||||
"Temporarily skipping the self-regulation algorithm while the configured sensor for outdoor temperature is unavailable"
|
||||
)
|
||||
return self.target_temp
|
||||
|
||||
# Calculate the error factor (P)
|
||||
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
|
||||
self.accumulated_error = min(
|
||||
self.accumulated_error_threshold,
|
||||
max(-self.accumulated_error_threshold, self.accumulated_error),
|
||||
)
|
||||
|
||||
# Calculate the offset (proportionnel + intégral)
|
||||
offset = self.kp * error + self.ki * self.accumulated_error
|
||||
|
||||
# Calculate the exterior offset
|
||||
offset_ext = self.k_ext * (room_temp - external_temp)
|
||||
|
||||
# Capping of offset
|
||||
total_offset = offset + offset_ext
|
||||
total_offset = min(self.offset_max, max(-self.offset_max, total_offset))
|
||||
|
||||
result = round(self.target_temp + total_offset, 1)
|
||||
|
||||
_LOGGER.debug(
|
||||
"PITemperatureRegulator - Error: %.2f accumulated_error: %.2f offset: %.2f offset_ext: %.2f target_tem: %.1f regulatedTemp: %.1f",
|
||||
error,
|
||||
self.accumulated_error,
|
||||
offset,
|
||||
offset_ext,
|
||||
self.target_temp,
|
||||
result,
|
||||
)
|
||||
|
||||
return result
|
||||
@@ -1,9 +1,6 @@
|
||||
""" 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"
|
||||
@@ -15,11 +12,6 @@ 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"""
|
||||
|
||||
@@ -30,94 +22,42 @@ class PropAlgorithm:
|
||||
tpi_coef_ext,
|
||||
cycle_min: int,
|
||||
minimal_activation_delay: int,
|
||||
minimal_deactivation_delay: int,
|
||||
vtherm_entity_id: str = None,
|
||||
max_on_percent: float = None,
|
||||
) -> None:
|
||||
"""Initialisation of the Proportional Algorithm"""
|
||||
_LOGGER.debug(
|
||||
"%s - Creation new PropAlgorithm function_type: %s, tpi_coef_int: %s, tpi_coef_ext: %s, cycle_min:%d, minimal_activation_delay:%d, minimal_deactivation_delay:%d", # pylint: disable=line-too-long
|
||||
vtherm_entity_id,
|
||||
"Creation new PropAlgorithm function_type: %s, tpi_coef_int: %s, tpi_coef_ext: %s, cycle_min:%d, minimal_activation_delay:%d",
|
||||
function_type,
|
||||
tpi_coef_int,
|
||||
tpi_coef_ext,
|
||||
cycle_min,
|
||||
minimal_activation_delay,
|
||||
minimal_deactivation_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 not is_number(minimal_deactivation_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, minimal_deactivation_delay is %s",
|
||||
vtherm_entity_id,
|
||||
function_type,
|
||||
vtherm_entity_id,
|
||||
tpi_coef_int,
|
||||
tpi_coef_ext,
|
||||
cycle_min,
|
||||
minimal_activation_delay,
|
||||
minimal_deactivation_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
|
||||
self._cycle_min = cycle_min
|
||||
self._minimal_activation_delay = minimal_activation_delay
|
||||
self._minimal_deactivation_delay = minimal_deactivation_delay
|
||||
self._on_percent = 0
|
||||
self._calculated_on_percent = 0
|
||||
self._on_time_sec = 0
|
||||
self._off_time_sec = self._cycle_min * 60
|
||||
self._security = False
|
||||
self._default_on_percent = 0
|
||||
self._max_on_percent = max_on_percent
|
||||
|
||||
def calculate(
|
||||
self,
|
||||
target_temp: float | None,
|
||||
current_temp: float | None,
|
||||
ext_current_temp: float | None,
|
||||
hvac_mode: HVACMode,
|
||||
self, target_temp: float, current_temp: float, ext_current_temp: float
|
||||
):
|
||||
"""Do the calculation of the duration"""
|
||||
if target_temp is None or current_temp is None:
|
||||
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,
|
||||
_LOGGER.warning(
|
||||
"Proportional algorithm: calculation is not possible cause target_temp or current_temp is null. Heating will be disabled" # pylint: disable=line-too-long
|
||||
)
|
||||
self._calculated_on_percent = 0
|
||||
else:
|
||||
if hvac_mode == HVACMode.COOL:
|
||||
delta_temp = current_temp - target_temp
|
||||
delta_ext_temp = (
|
||||
ext_current_temp - target_temp
|
||||
if ext_current_temp is not None
|
||||
else 0
|
||||
)
|
||||
else:
|
||||
delta_temp = target_temp - current_temp
|
||||
delta_ext_temp = (
|
||||
target_temp - ext_current_temp
|
||||
if ext_current_temp is not None
|
||||
else 0
|
||||
)
|
||||
delta_temp = target_temp - current_temp
|
||||
delta_ext_temp = (
|
||||
target_temp - ext_current_temp if ext_current_temp is not None else 0
|
||||
)
|
||||
|
||||
if self._function == PROPORTIONAL_FUNCTION_TPI:
|
||||
self._calculated_on_percent = (
|
||||
@@ -126,8 +66,7 @@ class PropAlgorithm:
|
||||
)
|
||||
else:
|
||||
_LOGGER.warning(
|
||||
"%s - Proportional algorithm: unknown %s function. Heating will be disabled",
|
||||
self._vtherm_entity_id,
|
||||
"Proportional algorithm: unknown %s function. Heating will be disabled",
|
||||
self._function,
|
||||
)
|
||||
self._calculated_on_percent = 0
|
||||
@@ -135,8 +74,7 @@ class PropAlgorithm:
|
||||
self._calculate_internal()
|
||||
|
||||
_LOGGER.debug(
|
||||
"%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,
|
||||
"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
|
||||
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,
|
||||
@@ -155,12 +93,11 @@ class PropAlgorithm:
|
||||
self._calculated_on_percent = 0
|
||||
|
||||
if self._security:
|
||||
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,
|
||||
_LOGGER.debug(
|
||||
"Security is On using the default_on_percent %f",
|
||||
self._default_on_percent,
|
||||
)
|
||||
self._on_percent = self._default_on_percent
|
||||
else:
|
||||
_LOGGER.debug(
|
||||
"Security is Off using the calculated_on_percent %f",
|
||||
@@ -168,23 +105,19 @@ class PropAlgorithm:
|
||||
)
|
||||
self._on_percent = self._calculated_on_percent
|
||||
|
||||
if self._max_on_percent is not None and self._on_percent > self._max_on_percent:
|
||||
_LOGGER.debug(
|
||||
"%s - Heating period clamped to %s (instead of %s) due to max_on_percent setting.",
|
||||
self._vtherm_entity_id,
|
||||
self._max_on_percent,
|
||||
self._on_percent,
|
||||
)
|
||||
self._on_percent = self._max_on_percent
|
||||
|
||||
self._on_time_sec = self._on_percent * self._cycle_min * 60
|
||||
|
||||
# Do not heat for less than xx sec
|
||||
if self._on_time_sec < self._minimal_activation_delay:
|
||||
if self._on_time_sec > 0:
|
||||
_LOGGER.info(
|
||||
"%s - No heating period due to heating period too small (%f < %f)",
|
||||
self._vtherm_entity_id,
|
||||
"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)",
|
||||
self._on_time_sec,
|
||||
self._minimal_activation_delay,
|
||||
)
|
||||
@@ -192,46 +125,28 @@ class PropAlgorithm:
|
||||
|
||||
self._off_time_sec = self._cycle_min * 60 - self._on_time_sec
|
||||
|
||||
# Do not stop heating when off time less than xx sec
|
||||
if self._off_time_sec < self._minimal_deactivation_delay:
|
||||
if self._off_time_sec > 0:
|
||||
_LOGGER.info(
|
||||
"%s - Force 100%% heating cycle since the off duration (%f) is shorter than minimal_deactivation_delay (%f)",
|
||||
self._vtherm_entity_id,
|
||||
self._off_time_sec,
|
||||
self._minimal_deactivation_delay,
|
||||
)
|
||||
self._on_time_sec = self._cycle_min * 60
|
||||
self._off_time_sec = 0
|
||||
|
||||
def set_safety(self, default_on_percent: float):
|
||||
"""Set a default value for on_percent (used for safety mode)"""
|
||||
_LOGGER.info(
|
||||
"%s - Proportional Algo - set security to ON", self._vtherm_entity_id
|
||||
)
|
||||
def set_security(self, default_on_percent: float):
|
||||
"""Set a default value for on_percent (used for security mode)"""
|
||||
self._security = True
|
||||
self._default_on_percent = default_on_percent
|
||||
self._calculate_internal()
|
||||
|
||||
def unset_safety(self):
|
||||
"""Unset the safety mode"""
|
||||
_LOGGER.info(
|
||||
"%s - Proportional Algo - set security to OFF", self._vtherm_entity_id
|
||||
)
|
||||
def unset_security(self):
|
||||
"""Unset the security mode"""
|
||||
self._security = False
|
||||
self._calculate_internal()
|
||||
|
||||
@property
|
||||
def on_percent(self) -> float:
|
||||
"""Returns the percentage the heater must be ON
|
||||
In safety mode this value is overriden with the _default_on_percent
|
||||
In security 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 safety mode
|
||||
Calculated means NOT overriden even in security mode
|
||||
(1 means the heater will be always on, 0 never on)""" # pylint: disable=line-too-long
|
||||
return round(self._calculated_on_percent, 2)
|
||||
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
homeassistant==2023.10.1
|
||||
ffmpeg
|
||||
@@ -0,0 +1,4 @@
|
||||
-r requirements_dev.txt
|
||||
# aiodiscover
|
||||
ulid_transform
|
||||
pytest-homeassistant-custom-component
|
||||
@@ -1,138 +0,0 @@
|
||||
# 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}"
|
||||
@@ -1,17 +1,10 @@
|
||||
# pylint: disable=unused-argument
|
||||
""" Implements the VersatileThermostat sensors component """
|
||||
import logging
|
||||
import math
|
||||
|
||||
from homeassistant.core import HomeAssistant, callback, Event, CoreState, State
|
||||
from homeassistant.core import HomeAssistant, callback, Event
|
||||
|
||||
from homeassistant.const import (
|
||||
UnitOfTime,
|
||||
UnitOfPower,
|
||||
UnitOfEnergy,
|
||||
PERCENTAGE,
|
||||
EVENT_HOMEASSISTANT_START,
|
||||
)
|
||||
from homeassistant.const import UnitOfTime, UnitOfPower, UnitOfEnergy, PERCENTAGE
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
SensorEntity,
|
||||
@@ -21,37 +14,15 @@ 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,
|
||||
)
|
||||
|
||||
|
||||
from .base_thermostat import BaseThermostat
|
||||
from .vtherm_api import VersatileThermostatAPI
|
||||
from .base_entity import VersatileThermostatBaseEntity
|
||||
from .commons import VersatileThermostatBaseEntity
|
||||
from .const import (
|
||||
DOMAIN,
|
||||
DEVICE_MANUFACTURER,
|
||||
CONF_NAME,
|
||||
CONF_DEVICE_POWER,
|
||||
CONF_PROP_FUNCTION,
|
||||
PROPORTIONAL_FUNCTION_TPI,
|
||||
CONF_THERMOSTAT_SWITCH,
|
||||
CONF_THERMOSTAT_VALVE,
|
||||
CONF_THERMOSTAT_CLIMATE,
|
||||
CONF_THERMOSTAT_TYPE,
|
||||
CONF_THERMOSTAT_CENTRAL_CONFIG,
|
||||
CONF_USE_CENTRAL_BOILER_FEATURE,
|
||||
CONF_AUTO_REGULATION_VALVE,
|
||||
CONF_AUTO_REGULATION_MODE,
|
||||
overrides,
|
||||
)
|
||||
|
||||
THRESHOLD_WATT_KILO = 100
|
||||
@@ -71,54 +42,23 @@ async def async_setup_entry(
|
||||
|
||||
unique_id = entry.entry_id
|
||||
name = entry.data.get(CONF_NAME)
|
||||
vt_type = entry.data.get(CONF_THERMOSTAT_TYPE)
|
||||
have_valve_regulation = (
|
||||
entry.data.get(CONF_AUTO_REGULATION_MODE) == CONF_AUTO_REGULATION_VALVE
|
||||
)
|
||||
|
||||
entities = None
|
||||
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) == CONF_THERMOSTAT_SWITCH:
|
||||
entities.append(MeanPowerSensor(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_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_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_VALVE
|
||||
or have_valve_regulation
|
||||
):
|
||||
entities.append(ValveOpenPercentSensor(hass, unique_id, name, entry.data))
|
||||
|
||||
if (
|
||||
entry.data.get(CONF_THERMOSTAT_TYPE) == CONF_THERMOSTAT_CLIMATE
|
||||
and not have_valve_regulation
|
||||
):
|
||||
entities.append(
|
||||
RegulatedTemperatureSensor(hass, unique_id, name, entry.data)
|
||||
)
|
||||
|
||||
if entities:
|
||||
async_add_entities(entities, True)
|
||||
async_add_entities(entities, True)
|
||||
|
||||
|
||||
class EnergySensor(VersatileThermostatBaseEntity, SensorEntity):
|
||||
@@ -133,17 +73,17 @@ class EnergySensor(VersatileThermostatBaseEntity, SensorEntity):
|
||||
@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)
|
||||
_LOGGER.debug("%s - climate state change", self._attr_unique_id)
|
||||
|
||||
energy = self.my_climate.total_energy
|
||||
if energy is None:
|
||||
return
|
||||
|
||||
if math.isnan(energy) or math.isinf(energy):
|
||||
if math.isnan(self.my_climate.total_energy) or math.isinf(
|
||||
self.my_climate.total_energy
|
||||
):
|
||||
raise ValueError(f"Sensor has illegal state {self.my_climate.total_energy}")
|
||||
|
||||
old_state = self._attr_native_value
|
||||
self._attr_native_value = round(energy, self.suggested_display_precision)
|
||||
self._attr_native_value = round(
|
||||
self.my_climate.total_energy, self.suggested_display_precision
|
||||
)
|
||||
if old_state != self._attr_native_value:
|
||||
self.async_write_ha_state()
|
||||
return
|
||||
@@ -165,7 +105,7 @@ class EnergySensor(VersatileThermostatBaseEntity, SensorEntity):
|
||||
if not self.my_climate:
|
||||
return None
|
||||
|
||||
if self.my_climate.power_manager.device_power > THRESHOLD_WATT_KILO:
|
||||
if self.my_climate.device_power > THRESHOLD_WATT_KILO:
|
||||
return UnitOfEnergy.WATT_HOUR
|
||||
else:
|
||||
return UnitOfEnergy.KILO_WATT_HOUR
|
||||
@@ -188,19 +128,18 @@ class MeanPowerSensor(VersatileThermostatBaseEntity, SensorEntity):
|
||||
@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)
|
||||
_LOGGER.debug("%s - climate state change", self._attr_unique_id)
|
||||
|
||||
if math.isnan(
|
||||
float(self.my_climate.power_manager.mean_cycle_power)
|
||||
) or math.isinf(self.my_climate.power_manager.mean_cycle_power):
|
||||
if math.isnan(float(self.my_climate.mean_cycle_power)) or math.isinf(
|
||||
self.my_climate.mean_cycle_power
|
||||
):
|
||||
raise ValueError(
|
||||
f"Sensor has illegal state {self.my_climate.power_manager.mean_cycle_power}"
|
||||
f"Sensor has illegal state {self.my_climate.mean_cycle_power}"
|
||||
)
|
||||
|
||||
old_state = self._attr_native_value
|
||||
self._attr_native_value = round(
|
||||
self.my_climate.power_manager.mean_cycle_power,
|
||||
self.suggested_display_precision,
|
||||
self.my_climate.mean_cycle_power, self.suggested_display_precision
|
||||
)
|
||||
if old_state != self._attr_native_value:
|
||||
self.async_write_ha_state()
|
||||
@@ -223,7 +162,7 @@ class MeanPowerSensor(VersatileThermostatBaseEntity, SensorEntity):
|
||||
if not self.my_climate:
|
||||
return None
|
||||
|
||||
if self.my_climate.power_manager.device_power > THRESHOLD_WATT_KILO:
|
||||
if self.my_climate.device_power > THRESHOLD_WATT_KILO:
|
||||
return UnitOfPower.WATT
|
||||
else:
|
||||
return UnitOfPower.KILO_WATT
|
||||
@@ -246,16 +185,13 @@ class OnPercentSensor(VersatileThermostatBaseEntity, SensorEntity):
|
||||
@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)
|
||||
_LOGGER.debug("%s - climate state change", self._attr_unique_id)
|
||||
|
||||
on_percent = (
|
||||
float(self.my_climate.proportional_algorithm.on_percent)
|
||||
if self.my_climate and self.my_climate.proportional_algorithm
|
||||
else None
|
||||
)
|
||||
if on_percent is None:
|
||||
return
|
||||
|
||||
if math.isnan(on_percent) or math.isinf(on_percent):
|
||||
raise ValueError(f"Sensor has illegal state {on_percent}")
|
||||
|
||||
@@ -289,52 +225,6 @@ class OnPercentSensor(VersatileThermostatBaseEntity, SensorEntity):
|
||||
return 1
|
||||
|
||||
|
||||
class ValveOpenPercentSensor(VersatileThermostatBaseEntity, SensorEntity):
|
||||
"""Representation of a on percent sensor which exposes the on_percent in a cycle"""
|
||||
|
||||
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 = "Valve open percent"
|
||||
self._attr_unique_id = f"{self._device_name}_valve_open_percent"
|
||||
|
||||
@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)
|
||||
|
||||
if not self.my_climate or not hasattr(self.my_climate, "valve_open_percent"):
|
||||
_LOGGER.warning("%s - my_climate not found or no valve_open_percent property found. This could be normal at startup. Ignore the underlying device change.", self)
|
||||
return
|
||||
|
||||
old_state = self._attr_native_value
|
||||
self._attr_native_value = self.my_climate.valve_open_percent
|
||||
if old_state != self._attr_native_value:
|
||||
self.async_write_ha_state()
|
||||
return
|
||||
|
||||
@property
|
||||
def icon(self) -> str | None:
|
||||
return "mdi:pipe-valve"
|
||||
|
||||
@property
|
||||
def device_class(self) -> SensorDeviceClass | None:
|
||||
return SensorDeviceClass.POWER_FACTOR
|
||||
|
||||
@property
|
||||
def state_class(self) -> SensorStateClass | None:
|
||||
return SensorStateClass.MEASUREMENT
|
||||
|
||||
@property
|
||||
def native_unit_of_measurement(self) -> str | None:
|
||||
return PERCENTAGE
|
||||
|
||||
@property
|
||||
def suggested_display_precision(self) -> int | None:
|
||||
"""Return the suggested number of decimal digits for display."""
|
||||
return 0
|
||||
|
||||
|
||||
class OnTimeSensor(VersatileThermostatBaseEntity, SensorEntity):
|
||||
"""Representation of a on time sensor which exposes the on_time_sec in a cycle"""
|
||||
|
||||
@@ -347,17 +237,13 @@ class OnTimeSensor(VersatileThermostatBaseEntity, SensorEntity):
|
||||
@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)
|
||||
_LOGGER.debug("%s - climate state change", self._attr_unique_id)
|
||||
|
||||
on_time = (
|
||||
float(self.my_climate.proportional_algorithm.on_time_sec)
|
||||
if self.my_climate and self.my_climate.proportional_algorithm
|
||||
else None
|
||||
)
|
||||
|
||||
if on_time is None:
|
||||
return
|
||||
|
||||
if math.isnan(on_time) or math.isinf(on_time):
|
||||
raise ValueError(f"Sensor has illegal state {on_time}")
|
||||
|
||||
@@ -396,16 +282,13 @@ class OffTimeSensor(VersatileThermostatBaseEntity, SensorEntity):
|
||||
@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)
|
||||
_LOGGER.debug("%s - climate state change", self._attr_unique_id)
|
||||
|
||||
off_time = (
|
||||
float(self.my_climate.proportional_algorithm.off_time_sec)
|
||||
if self.my_climate and self.my_climate.proportional_algorithm
|
||||
else None
|
||||
)
|
||||
if off_time is None:
|
||||
return
|
||||
|
||||
if math.isnan(off_time) or math.isinf(off_time):
|
||||
raise ValueError(f"Sensor has illegal state {off_time}")
|
||||
|
||||
@@ -444,10 +327,10 @@ class LastTemperatureSensor(VersatileThermostatBaseEntity, SensorEntity):
|
||||
@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)
|
||||
_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_measure
|
||||
self._attr_native_value = self.my_climate.last_temperature_mesure
|
||||
if old_state != self._attr_native_value:
|
||||
self.async_write_ha_state()
|
||||
return
|
||||
@@ -473,10 +356,10 @@ class LastExtTemperatureSensor(VersatileThermostatBaseEntity, SensorEntity):
|
||||
@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)
|
||||
_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_measure
|
||||
self._attr_native_value = self.my_climate.last_ext_temperature_mesure
|
||||
if old_state != self._attr_native_value:
|
||||
self.async_write_ha_state()
|
||||
return
|
||||
@@ -502,7 +385,7 @@ class TemperatureSlopeSensor(VersatileThermostatBaseEntity, SensorEntity):
|
||||
@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)
|
||||
_LOGGER.debug("%s - climate state change", self._attr_unique_id)
|
||||
|
||||
last_slope = self.my_climate.last_temperature_slope
|
||||
if last_slope is None:
|
||||
@@ -535,286 +418,9 @@ class TemperatureSlopeSensor(VersatileThermostatBaseEntity, SensorEntity):
|
||||
if not self.my_climate:
|
||||
return None
|
||||
|
||||
return self.my_climate.temperature_unit + "/hour"
|
||||
return self.my_climate.temperature_unit + "/min"
|
||||
|
||||
@property
|
||||
def suggested_display_precision(self) -> int | None:
|
||||
"""Return the suggested number of decimal digits for display."""
|
||||
return 2
|
||||
|
||||
|
||||
class RegulatedTemperatureSensor(VersatileThermostatBaseEntity, SensorEntity):
|
||||
"""Representation of a Energy sensor which exposes the energy"""
|
||||
|
||||
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 = "Regulated temperature"
|
||||
self._attr_unique_id = f"{self._device_name}_regulated_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_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(new_temp, self.suggested_display_precision)
|
||||
if old_state != self._attr_native_value:
|
||||
self.async_write_ha_state()
|
||||
return
|
||||
|
||||
@property
|
||||
def icon(self) -> str | None:
|
||||
return "mdi:thermometer-auto"
|
||||
|
||||
@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 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"""
|
||||
|
||||
_entity_component_unrecorded_attributes = SensorEntity._entity_component_unrecorded_attributes.union( # pylint: disable=protected-access
|
||||
frozenset({"active_device_ids"})
|
||||
)
|
||||
|
||||
def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None:
|
||||
"""Initialize the energy sensor"""
|
||||
self._hass = hass
|
||||
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 = []
|
||||
self._attr_active_device_ids = [] # Holds the entity ids of active devices
|
||||
|
||||
@property
|
||||
def extra_state_attributes(self) -> dict:
|
||||
"""Return additional attributes for the sensor."""
|
||||
return {
|
||||
"active_device_ids": self._attr_active_device_ids,
|
||||
}
|
||||
|
||||
@property
|
||||
def icon(self) -> str | None:
|
||||
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.activable_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 control the central boiler are %s",
|
||||
self,
|
||||
underlying_entities_id,
|
||||
)
|
||||
self.async_on_remove(listener_cancel)
|
||||
else:
|
||||
_LOGGER.debug("%s - no VTherm could control the central boiler", self)
|
||||
|
||||
await self.calculate_nb_active_devices(None)
|
||||
|
||||
async def calculate_nb_active_devices(self, event: Event):
|
||||
"""Calculate the number of active VTherm that have an
|
||||
influence on the central boiler and update the list of active device names."""
|
||||
|
||||
# _LOGGER.debug("%s- calculate_nb_active_devices - the event is %s ", self, event)
|
||||
|
||||
if event is not None:
|
||||
new_state: State = event.data.get("new_state")
|
||||
# _LOGGER.debug(
|
||||
# "%s - calculate_nb_active_devices new_state is %s", self, new_state
|
||||
# )
|
||||
if not new_state:
|
||||
return
|
||||
|
||||
old_state: State = event.data.get("old_state")
|
||||
|
||||
# For underlying climate, we need to observe also the hvac_action if available
|
||||
new_hvac_action = new_state.attributes.get("hvac_action")
|
||||
old_hvac_action = (
|
||||
old_state.attributes.get("hvac_action")
|
||||
if old_state is not None
|
||||
else None
|
||||
)
|
||||
|
||||
# Filter events that are not interested for us
|
||||
if (
|
||||
old_state is not None
|
||||
and new_state.state == old_state.state
|
||||
and new_hvac_action == old_hvac_action
|
||||
# issue 698 - force recalculation when underlying climate doesn't have any hvac_action
|
||||
and new_hvac_action is not None
|
||||
):
|
||||
# A false state change
|
||||
return
|
||||
|
||||
_LOGGER.debug(
|
||||
"%s - calculating the number of active underlying device for boiler activation. change change from %s to %s",
|
||||
self,
|
||||
old_state,
|
||||
new_state,
|
||||
)
|
||||
else:
|
||||
_LOGGER.debug(
|
||||
"%s - calculating the number of active underlying device for boiler activation. First time calculation",
|
||||
self,
|
||||
)
|
||||
|
||||
nb_active = 0
|
||||
active_device_ids = []
|
||||
|
||||
for entity in self._entities:
|
||||
device_actives = entity.device_actives
|
||||
_LOGGER.debug(
|
||||
"After examining the hvac_action of %s, device_actives is %s",
|
||||
entity.name,
|
||||
device_actives,
|
||||
)
|
||||
|
||||
nb_active += len(device_actives)
|
||||
active_device_ids.extend(device_actives)
|
||||
|
||||
self._attr_native_value = nb_active
|
||||
self._attr_active_device_ids = active_device_ids
|
||||
|
||||
self.async_write_ha_state()
|
||||
|
||||
@property
|
||||
def active_device_ids(self) -> list:
|
||||
"""Get the list of active device id"""
|
||||
return self._attr_active_device_ids
|
||||
|
||||
def __str__(self):
|
||||
return f"VersatileThermostat-{self.name}"
|
||||
|
||||
@@ -43,7 +43,6 @@ set_preset_temperature:
|
||||
- "eco"
|
||||
- "comfort"
|
||||
- "boost"
|
||||
- "frost"
|
||||
- "eco_ac"
|
||||
- "comfort_ac"
|
||||
- "boost_ac"
|
||||
@@ -76,9 +75,9 @@ set_preset_temperature:
|
||||
unit_of_measurement: °
|
||||
mode: slider
|
||||
|
||||
set_safety:
|
||||
name: Set safety
|
||||
description: Change the safety parameters
|
||||
set_security:
|
||||
name: Set security
|
||||
description: Change the security parameters
|
||||
target:
|
||||
entity:
|
||||
integration: versatile_thermostat
|
||||
@@ -97,7 +96,7 @@ set_safety:
|
||||
mode: box
|
||||
min_on_percent:
|
||||
name: Minimal on_percent
|
||||
description: Minimal heating percent value for safety preset activation
|
||||
description: Minimal heating percent value for security preset activation
|
||||
required: false
|
||||
advanced: false
|
||||
example: "0.5"
|
||||
@@ -110,8 +109,8 @@ set_safety:
|
||||
unit_of_measurement: "%"
|
||||
mode: slider
|
||||
default_on_percent:
|
||||
name: on_percent used in safety mode
|
||||
description: The default heating percent value in safety preset
|
||||
name: on_percent used in security mode
|
||||
description: The default heating percent value in security preset
|
||||
required: false
|
||||
advanced: false
|
||||
example: "0.1"
|
||||
@@ -123,64 +122,3 @@ set_safety:
|
||||
step: 0.05
|
||||
unit_of_measurement: "%"
|
||||
mode: slider
|
||||
|
||||
set_window_bypass:
|
||||
name: Set Window ByPass
|
||||
description: Bypass the window state to enable heating with window open.
|
||||
target:
|
||||
entity:
|
||||
integration: versatile_thermostat
|
||||
fields:
|
||||
window_bypass:
|
||||
name: Window ByPass
|
||||
description: ByPass value
|
||||
required: true
|
||||
advanced: false
|
||||
default: true
|
||||
selector:
|
||||
boolean:
|
||||
|
||||
set_auto_regulation_mode:
|
||||
name: Set Auto Regulation mode
|
||||
description: Change the mode of self-regulation (only for VTherm over climate)
|
||||
target:
|
||||
entity:
|
||||
integration: versatile_thermostat
|
||||
fields:
|
||||
auto_regulation_mode:
|
||||
name: Auto regulation mode
|
||||
description: Possible values
|
||||
required: true
|
||||
advanced: false
|
||||
default: true
|
||||
selector:
|
||||
select:
|
||||
options:
|
||||
- "None"
|
||||
- "Light"
|
||||
- "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,252 +4,142 @@
|
||||
"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",
|
||||
"valve_regulation": "Valve regulation configuration",
|
||||
"finalize": "All done",
|
||||
"configuration_not_complete": "Configuration not complete"
|
||||
}
|
||||
},
|
||||
"main": {
|
||||
"title": "Add new Versatile Thermostat",
|
||||
"description": "Main mandatory attributes [](https://github.com/jmcollin78/versatile_thermostat/blob/main/documentation/en/base-attributes.md)",
|
||||
"description": "Main mandatory attributes",
|
||||
"data": {
|
||||
"name": "Name",
|
||||
"thermostat_type": "Thermostat type",
|
||||
"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",
|
||||
"temperature_sensor_entity_id": "Temperature sensor entity id",
|
||||
"external_temperature_sensor_entity_id": "External temperature sensor entity id",
|
||||
"cycle_min": "Cycle duration (minutes)",
|
||||
"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": {
|
||||
"temp_min": "Minimal temperature allowed",
|
||||
"temp_max": "Maximal temperature allowed",
|
||||
"device_power": "Device power (kW)",
|
||||
"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_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"
|
||||
"use_presence_feature": "Use presence detection"
|
||||
}
|
||||
},
|
||||
"type": {
|
||||
"title": "Linked entities",
|
||||
"description": "Linked entities attributes [](https://github.com/jmcollin78/versatile_thermostat/blob/main/documentation/en/over-switch.md#configuration)",
|
||||
"description": "Linked entities attributes",
|
||||
"data": {
|
||||
"underlying_entity_ids": "The device(s) to be controlled",
|
||||
"heater_keep_alive": "Switch keep-alive interval in seconds",
|
||||
"heater_entity_id": "Heater switch",
|
||||
"heater_entity2_id": "2nd heater switch",
|
||||
"heater_entity3_id": "3rd heater switch",
|
||||
"heater_entity4_id": "4th heater switch",
|
||||
"proportional_function": "Algorithm",
|
||||
"ac_mode": "AC mode",
|
||||
"auto_regulation_mode": "Self-regulation",
|
||||
"auto_regulation_dtemp": "Regulation threshold",
|
||||
"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",
|
||||
"on_command_text": "Turn on command customization",
|
||||
"vswitch_on_command": "Optional turn on commands",
|
||||
"off_command_text": "Turn off command customization",
|
||||
"vswitch_off_command": "Optional turn off commands"
|
||||
"climate_entity_id": "Underlying climate",
|
||||
"climate_entity2_id": "2nd underlying climate",
|
||||
"climate_entity3_id": "3rd underlying climate",
|
||||
"climate_entity4_id": "4th underlying climate",
|
||||
"ac_mode": "AC mode"
|
||||
},
|
||||
"data_description": {
|
||||
"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.",
|
||||
"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",
|
||||
"proportional_function": "Algorithm to use (TPI is the only one for now)",
|
||||
"ac_mode": "Use the Air Conditioning (AC) mode",
|
||||
"auto_regulation_mode": "Auto adjustment of the target temperature",
|
||||
"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",
|
||||
"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",
|
||||
"on_command_text": "For underlying of type `select` or `climate` you have to customize the commands."
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"tpi": {
|
||||
"title": "TPI",
|
||||
"description": "Time Proportional Integral attributes [](https://github.com/jmcollin78/versatile_thermostat/blob/main/documentation/en/algorithms.md#lalgorithme-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",
|
||||
"use_tpi_central_config": "Check to use the central TPI configuration. Uncheck to use a specific TPI configuration for this VTherm"
|
||||
"tpi_coef_ext": "Coefficient to use for external temperature delta"
|
||||
}
|
||||
},
|
||||
"presets": {
|
||||
"title": "Presets",
|
||||
"description": "Select if the thermostat will use central preset - deselect for the thermostat to have its own presets [](https://github.com/jmcollin78/versatile_thermostat/blob/main/documentation/en/feature-presets.md)",
|
||||
"description": "For each presets, give the target temperature (0 to ignore preset)",
|
||||
"data": {
|
||||
"use_presets_central_config": "Use central presets configuration"
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"window": {
|
||||
"title": "Window management",
|
||||
"description": "Open window management.\nYou can also configure automatic window open detection based on temperature decrease [](https://github.com/jmcollin78/versatile_thermostat/blob/main/documentation/en/feature-window.md)",
|
||||
"description": "Open window management.\nLeave corresponding entity_id empty if not used\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 'on' delay (seconds)",
|
||||
"window_off_delay": "Window sensor 'off' delay (seconds)",
|
||||
"window_auto_open_threshold": "Temperature decrease threshold for automatic window open detection (in °/hours)",
|
||||
"window_auto_close_threshold": "Temperature increase threshold for end of automatic detection (in °/hours)",
|
||||
"window_auto_max_duration": "Maximum duration of automatic window open detection (in min)",
|
||||
"use_window_central_config": "Use central window configuration",
|
||||
"window_action": "Action"
|
||||
"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)"
|
||||
},
|
||||
"data_description": {
|
||||
"window_sensor_entity_id": "Leave empty if no window sensor should be used and to use the automatic detection",
|
||||
"window_delay": "The delay in seconds before sensor 'on' detection is taken into account",
|
||||
"window_off_delay": "The delay in seconds before sensor 'off' detection is taken into account. Leave it empty to use the same value as window on delay",
|
||||
"window_auto_open_threshold": "Recommended value: between 3 and 10. Leave empty if automatic window open detection is not used",
|
||||
"window_auto_close_threshold": "Recommended value: 0. Leave empty if automatic window open detection is not used",
|
||||
"window_auto_max_duration": "Recommended value: 60 (one hour). Leave empty if automatic window open detection is not used",
|
||||
"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"
|
||||
"window_sensor_entity_id": "Leave empty if no window sensor should be use",
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"motion": {
|
||||
"title": "Motion management",
|
||||
"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 [](https://github.com/jmcollin78/versatile_thermostat/blob/main/documentation/en/feature-motion.md)",
|
||||
"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",
|
||||
"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",
|
||||
"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 delay (seconds)",
|
||||
"motion_off_delay": "Motion deactivation delay (seconds)",
|
||||
"motion_delay": "Motion delay (seconds)",
|
||||
"motion_preset": "Preset to use when 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"
|
||||
"no_motion_preset": "Preset to use when no motion is detected"
|
||||
}
|
||||
},
|
||||
"power": {
|
||||
"title": "Power management",
|
||||
"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) [](https://github.com/jmcollin78/versatile_thermostat/blob/main/documentation/en/feature-power.md)",
|
||||
"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.",
|
||||
"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",
|
||||
"use_power_central_config": "Check to use the central power configuration. Uncheck to use a specific power configuration for this VTherm"
|
||||
"power_temp": "Temperature for Power shedding"
|
||||
}
|
||||
},
|
||||
"presence": {
|
||||
"title": "Presence management",
|
||||
"description": "Presence management attributes.\nGives the a presence sensor of your home (true is someone is present) and give the corresponding temperature preset setting [](https://github.com/jmcollin78/versatile_thermostat/blob/main/documentation/en/feature-presence.md)",
|
||||
"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.",
|
||||
"data": {
|
||||
"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"
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"advanced": {
|
||||
"title": "Advanced parameters",
|
||||
"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 [](https://github.com/jmcollin78/versatile_thermostat/blob/main/documentation/en/feature-advanced.md)",
|
||||
"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.",
|
||||
"data": {
|
||||
"minimal_activation_delay": "Minimum activation delay",
|
||||
"minimal_deactivation_delay": "Minimum deactivation delay",
|
||||
"safety_delay_min": "Safety delay (in minutes)",
|
||||
"safety_min_on_percent": "Minimum power percent to enable safety mode",
|
||||
"safety_default_on_percent": "Power percent to use in safety mode",
|
||||
"use_advanced_central_config": "Use central advanced configuration"
|
||||
"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"
|
||||
},
|
||||
"data_description": {
|
||||
"minimal_activation_delay": "Delay in seconds under which the equipment will not be activated",
|
||||
"minimal_deactivation_delay": "Delay in seconds under which the equipment will be kept active",
|
||||
"safety_delay_min": "Maximum allowed delay in minutes between two temperature measurements. Above this delay the thermostat will turn to a safety off state",
|
||||
"safety_min_on_percent": "Minimum heating percent value for safety preset activation. Below this amount of power percent the thermostat won't go into safety preset",
|
||||
"safety_default_on_percent": "The default heating power percent value in safety preset. Set to 0 to switch off heater in safety preset",
|
||||
"use_advanced_central_config": "Check to use the central advanced configuration. Uncheck to use a specific advanced configuration for this VTherm"
|
||||
}
|
||||
},
|
||||
"central_boiler": {
|
||||
"title": "Control of the central boiler",
|
||||
"description": "Enter the services to call to turn on/off the central boiler. Leave blank if no service call is to be made (in this case, you will have to manage the turning on/off of your central boiler yourself). The service called must be formatted as follows: `entity_id/service_name[/attribute:value]` (/attribute:value is optional)\nFor example:\n- to turn on a switch: `switch.controle_chaudiere/switch.turn_on`\n- to turn off a switch: `switch.controle_chaudiere/switch.turn_off`\n- to program the boiler to 25° and thus force its ignition: `climate.thermostat_chaudiere/climate.set_temperature/temperature:25`\n- to send 10° to the boiler and thus force its extinction: `climate.thermostat_chaudiere/climate.set_temperature/temperature:10` [](https://github.com/jmcollin78/versatile_thermostat/blob/main/documentation/fr/feature-central-boiler.md)",
|
||||
"data": {
|
||||
"central_boiler_activation_service": "Command to turn-on",
|
||||
"central_boiler_deactivation_service": "Command to turn-off"
|
||||
},
|
||||
"data_description": {
|
||||
"central_boiler_activation_service": "Command to turn-on the central boiler formatted like entity_id/service_name[/attribut:valeur]",
|
||||
"central_boiler_deactivation_service": "Command to turn-off the central boiler formatted like entity_id/service_name[/attribut:valeur]"
|
||||
}
|
||||
},
|
||||
"valve_regulation": {
|
||||
"title": "Self-regulation with valve",
|
||||
"description": "Configuration for self-regulation with direct control of the valve [](https://github.com/jmcollin78/versatile_thermostat/blob/main/documentation/fr/self-regulation.md#auto-régulation-par-contrôle-direct-de-la-vanne)",
|
||||
"data": {
|
||||
"offset_calibration_entity_ids": "Offset calibration entities",
|
||||
"opening_degree_entity_ids": "Opening degree entities",
|
||||
"closing_degree_entity_ids": "Closing degree entities",
|
||||
"proportional_function": "Algorithm",
|
||||
"min_opening_degrees": "Min opening degrees"
|
||||
},
|
||||
"data_description": {
|
||||
"offset_calibration_entity_ids": "The list of the 'offset calibration' entities. Set it if your TRV has the entity for better regulation. There should be one per underlying climate entities",
|
||||
"opening_degree_entity_ids": "The list of the 'opening degree' entities. There should be one per underlying climate entities",
|
||||
"closing_degree_entity_ids": "The list of the 'closing degree' entities. Set it if your TRV has the entity for better regulation. There should be one per underlying climate entities",
|
||||
"proportional_function": "Algorithm to use (TPI is the only one for now)",
|
||||
"min_opening_degrees": "Opening degree minimum value for each underlying device, comma separated. Default to 0. Example: 20, 25, 30"
|
||||
"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"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"unknown": "Unexpected error",
|
||||
"unknown_entity": "Unknown entity id",
|
||||
"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."
|
||||
"window_open_detection_method": "Only one window open detection method should be used. Use sensor or automatic detection through temperature threshold but not both"
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "Device is already configured"
|
||||
@@ -259,254 +149,142 @@
|
||||
"flow_title": "Versatile Thermostat configuration",
|
||||
"step": {
|
||||
"user": {
|
||||
"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",
|
||||
"valve_regulation": "Valve regulation configuration",
|
||||
"finalize": "All done",
|
||||
"configuration_not_complete": "Configuration not complete"
|
||||
}
|
||||
},
|
||||
"main": {
|
||||
"title": "Main - {name}",
|
||||
"description": "Main mandatory attributes [](https://github.com/jmcollin78/versatile_thermostat/blob/main/documentation/en/base-attributes.md)",
|
||||
"title": "Add new Versatile Thermostat",
|
||||
"description": "Main mandatory attributes",
|
||||
"data": {
|
||||
"name": "Name",
|
||||
"thermostat_type": "Thermostat type",
|
||||
"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",
|
||||
"temperature_sensor_entity_id": "Temperature sensor entity id",
|
||||
"external_temperature_sensor_entity_id": "External temperature sensor entity id",
|
||||
"cycle_min": "Cycle duration (minutes)",
|
||||
"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": {
|
||||
"temp_min": "Minimal temperature allowed",
|
||||
"temp_max": "Maximal temperature allowed",
|
||||
"device_power": "Device power (kW)",
|
||||
"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_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"
|
||||
"use_presence_feature": "Use presence detection"
|
||||
}
|
||||
},
|
||||
"type": {
|
||||
"title": "Entities - {name}",
|
||||
"description": "Linked entities attributes [](https://github.com/jmcollin78/versatile_thermostat/blob/main/documentation/en/over-switch.md#configuration)",
|
||||
"title": "Linked entities",
|
||||
"description": "Linked entities attributes",
|
||||
"data": {
|
||||
"underlying_entity_ids": "The device(s) to be controlled",
|
||||
"heater_keep_alive": "Switch keep-alive interval in seconds",
|
||||
"heater_entity_id": "Heater switch",
|
||||
"heater_entity2_id": "2nd Heater switch",
|
||||
"heater_entity3_id": "3rd Heater switch",
|
||||
"heater_entity4_id": "4th Heater switch",
|
||||
"proportional_function": "Algorithm",
|
||||
"ac_mode": "AC mode",
|
||||
"auto_regulation_mode": "Self-regulation",
|
||||
"auto_regulation_dtemp": "Regulation threshold",
|
||||
"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",
|
||||
"on_command_text": "Turn on command customization",
|
||||
"vswitch_on_command": "Optional turn on commands",
|
||||
"off_command_text": "Turn off command customization",
|
||||
"vswitch_off_command": "Optional turn off commands"
|
||||
"climate_entity_id": "Underlying thermostat",
|
||||
"climate_entity2_id": "2nd underlying climate",
|
||||
"climate_entity3_id": "3rd underlying climate",
|
||||
"climate_entity4_id": "4th underlying climate",
|
||||
"ac_mode": "AC mode"
|
||||
},
|
||||
"data_description": {
|
||||
"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.",
|
||||
"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",
|
||||
"proportional_function": "Algorithm to use (TPI is the only one for now)",
|
||||
"ac_mode": "Use the Air Conditioning (AC) mode",
|
||||
"auto_regulation_mode": "Auto adjustment of the target temperature",
|
||||
"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",
|
||||
"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",
|
||||
"on_command_text": "For underlying of type `select` or `climate` you have to customize the commands."
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"tpi": {
|
||||
"title": "TPI - {name}",
|
||||
"description": "Time Proportional Integral attributes [](https://github.com/jmcollin78/versatile_thermostat/blob/main/documentation/en/algorithms.md#lalgorithme-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",
|
||||
"use_tpi_central_config": "Check to use the central TPI configuration. Uncheck to use a specific TPI configuration for this VTherm"
|
||||
"tpi_coef_ext": "Coefficient to use for external temperature delta"
|
||||
}
|
||||
},
|
||||
"presets": {
|
||||
"title": "Presets - {name}",
|
||||
"description": "Check if the thermostat will use central presets. Uncheck and the thermostat will have its own preset entities [](https://github.com/jmcollin78/versatile_thermostat/blob/main/documentation/en/feature-presets.md)",
|
||||
"title": "Presets",
|
||||
"description": "For each presets, give the target temperature (0 to ignore preset)",
|
||||
"data": {
|
||||
"use_presets_central_config": "Use central presets configuration"
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"window": {
|
||||
"title": "Window - {name}",
|
||||
"description": "Open window management.\nYou can also configure automatic window open detection based on temperature decrease [](https://github.com/jmcollin78/versatile_thermostat/blob/main/documentation/en/feature-window.md)",
|
||||
"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",
|
||||
"data": {
|
||||
"window_sensor_entity_id": "Window sensor entity id",
|
||||
"window_delay": "Window sensor 'on' delay (seconds)",
|
||||
"window_off_delay": "Window sensor 'off' delay (seconds)",
|
||||
"window_auto_open_threshold": "Temperature decrease threshold for automatic window open detection (in °/hours)",
|
||||
"window_auto_close_threshold": "Temperature increase threshold for end of automatic detection (in °/hours)",
|
||||
"window_auto_max_duration": "Maximum duration of automatic window open detection (in min)",
|
||||
"use_window_central_config": "Use central window configuration",
|
||||
"window_action": "Action"
|
||||
"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)"
|
||||
},
|
||||
"data_description": {
|
||||
"window_sensor_entity_id": "Leave empty if no window sensor should be used and to use the automatic detection",
|
||||
"window_delay": "The delay in seconds before sensor 'on' detection is taken into account",
|
||||
"window_off_delay": "The delay in seconds before sensor 'off' detection is taken into account. Leave it empty to use the same value as window on delay",
|
||||
"window_auto_close_threshold": "Recommended value: 0. Leave empty if automatic window open detection is not used",
|
||||
"window_auto_max_duration": "Recommended value: 60 (one hour). Leave empty if automatic window open detection is not used",
|
||||
"use_window_central_config": "Check to use the central window configuration. Uncheck to use a specific window configuration for this VTherm",
|
||||
"window_action": "Action to do if window is deteted as open"
|
||||
"window_sensor_entity_id": "Leave empty if no window sensor should be use",
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"motion": {
|
||||
"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 [](https://github.com/jmcollin78/versatile_thermostat/blob/main/documentation/en/feature-motion.md)",
|
||||
"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",
|
||||
"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",
|
||||
"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 delay (seconds)",
|
||||
"motion_off_delay": "Motion deactivation delay (seconds)",
|
||||
"motion_delay": "Motion delay (seconds)",
|
||||
"motion_preset": "Preset to use when 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"
|
||||
"no_motion_preset": "Preset to use when no motion is detected"
|
||||
}
|
||||
},
|
||||
"power": {
|
||||
"title": "Power - {name}",
|
||||
"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) [](https://github.com/jmcollin78/versatile_thermostat/blob/main/documentation/en/feature-power.md)",
|
||||
"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.",
|
||||
"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",
|
||||
"use_power_central_config": "Check to use the central power configuration. Uncheck to use a specific power configuration for this VTherm"
|
||||
"power_temp": "Temperature for Power shedding"
|
||||
}
|
||||
},
|
||||
"presence": {
|
||||
"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 [](https://github.com/jmcollin78/versatile_thermostat/blob/main/documentation/en/feature-presence.md)",
|
||||
"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.",
|
||||
"data": {
|
||||
"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"
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"advanced": {
|
||||
"title": "Advanced - {name}",
|
||||
"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 [](https://github.com/jmcollin78/versatile_thermostat/blob/main/documentation/en/feature-advanced.md)",
|
||||
"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.",
|
||||
"data": {
|
||||
"minimal_activation_delay": "Minimum activation delay",
|
||||
"minimal_deactivation_delay": "Minimum deactivation delay",
|
||||
"safety_delay_min": "Safety delay (in minutes)",
|
||||
"safety_min_on_percent": "Minimum power percent to enable safety mode",
|
||||
"safety_default_on_percent": "Power percent to use in safety mode",
|
||||
"use_advanced_central_config": "Use central advanced configuration"
|
||||
"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"
|
||||
},
|
||||
"data_description": {
|
||||
"minimal_activation_delay": "Delay in seconds under which the equipment will not be activated",
|
||||
"minimal_deactivation_delay": "Delay in seconds under which the equipment will be kept active",
|
||||
"safety_delay_min": "Maximum allowed delay in minutes between two temperature measurements. Above this delay the thermostat will turn to a safety off state",
|
||||
"safety_min_on_percent": "Minimum heating percent value for safety preset activation. Below this amount of power percent the thermostat won't go into safety preset",
|
||||
"safety_default_on_percent": "The default heating power percent value in safety preset. Set to 0 to switch off heater in safety preset",
|
||||
"use_advanced_central_config": "Check to use the central advanced configuration. Uncheck to use a specific advanced configuration for this VTherm"
|
||||
}
|
||||
},
|
||||
"central_boiler": {
|
||||
"title": "Control of the central boiler - {name}",
|
||||
"description": "Enter the services to call to turn on/off the central boiler. Leave blank if no service call is to be made (in this case, you will have to manage the turning on/off of your central boiler yourself). The service called must be formatted as follows: `entity_id/service_name[/attribute:value]` (/attribute:value is optional)\nFor example:\n- to turn on a switch: `switch.controle_chaudiere/switch.turn_on`\n- to turn off a switch: `switch.controle_chaudiere/switch.turn_off`\n- to program the boiler to 25° and thus force its ignition: `climate.thermostat_chaudiere/climate.set_temperature/temperature:25`\n- to send 10° to the boiler and thus force its extinction: `climate.thermostat_chaudiere/climate.set_temperature/temperature:10` [](https://github.com/jmcollin78/versatile_thermostat/blob/main/documentation/fr/feature-central-boiler.md)",
|
||||
"data": {
|
||||
"central_boiler_activation_service": "Command to turn-on",
|
||||
"central_boiler_deactivation_service": "Command to turn-off"
|
||||
},
|
||||
"data_description": {
|
||||
"central_boiler_activation_service": "Command to turn-on the central boiler formatted like entity_id/service_name[/attribut:valeur]",
|
||||
"central_boiler_deactivation_service": "Command to turn-off the central boiler formatted like entity_id/service_name[/attribut:valeur]"
|
||||
}
|
||||
},
|
||||
"valve_regulation": {
|
||||
"title": "Self-regulation with valve - {name}",
|
||||
"description": "Configuration for self-regulation with direct control of the valve [](https://github.com/jmcollin78/versatile_thermostat/blob/main/documentation/fr/self-regulation.md#auto-régulation-par-contrôle-direct-de-la-vanne)",
|
||||
"data": {
|
||||
"offset_calibration_entity_ids": "Offset calibration entities",
|
||||
"opening_degree_entity_ids": "Opening degree entities",
|
||||
"closing_degree_entity_ids": "Closing degree entities",
|
||||
"proportional_function": "Algorithm",
|
||||
"min_opening_degrees": "Min opening degrees"
|
||||
},
|
||||
"data_description": {
|
||||
"offset_calibration_entity_ids": "The list of the 'offset calibration' entities. Set it if your TRV has the entity for better regulation. There should be one per underlying climate entities",
|
||||
"opening_degree_entity_ids": "The list of the 'opening degree' entities. There should be one per underlying climate entities",
|
||||
"closing_degree_entity_ids": "The list of the 'closing degree' entities. Set it if your TRV has the entity for better regulation. There should be one per underlying climate entities",
|
||||
"proportional_function": "Algorithm to use (TPI is the only one for now)",
|
||||
"min_opening_degrees": "Opening degree minimum value for each underlying device, comma separated. Default to 0. Example: 20, 25, 30"
|
||||
"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"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"unknown": "Unexpected error",
|
||||
"unknown_entity": "Unknown entity id",
|
||||
"window_open_detection_method": "Only one window open detection method should be used. Use either window sensor or automatic detection through temperature threshold but not both",
|
||||
"no_central_config": "You cannot check 'use central configuration' because no central configuration was found. You need to create a Versatile Thermostat of type 'Central Configuration' to use it.",
|
||||
"service_configuration_format": "The format of the service configuration is wrong",
|
||||
"valve_regulation_nb_entities_incorrect": "The number of valve entities for valve regulation should be equal to the number of underlyings",
|
||||
"min_opening_degrees_format": "A comma separated list of positive integer is expected. Example: 20, 25, 30"
|
||||
"window_open_detection_method": "Only one window open detection method should be used. Use sensor or automatic detection through temperature threshold but not both"
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "Device is already configured"
|
||||
@@ -515,54 +293,8 @@
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"auto_regulation_mode": {
|
||||
"options": {
|
||||
"auto_regulation_slow": "Slow",
|
||||
"auto_regulation_strong": "Strong",
|
||||
"auto_regulation_medium": "Medium",
|
||||
"auto_regulation_light": "Light",
|
||||
"auto_regulation_expert": "Expert",
|
||||
"auto_regulation_none": "No auto-regulation",
|
||||
"auto_regulation_valve": "Direct control of valve"
|
||||
}
|
||||
},
|
||||
"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"
|
||||
"thermostat_over_climate": "Thermostat over another thermostat"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -573,60 +305,12 @@
|
||||
"preset_mode": {
|
||||
"state": {
|
||||
"power": "Shedding",
|
||||
"safety": "Safety",
|
||||
"none": "Manual",
|
||||
"frost": "Frost"
|
||||
"security": "Security",
|
||||
"none": "Manual"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,173 +0,0 @@
|
||||
## 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 .base_entity 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)
|
||||
|
||||
entities = []
|
||||
if vt_type == CONF_THERMOSTAT_CLIMATE:
|
||||
entities.append(FollowUnderlyingTemperatureChange(hass, unique_id, name, entry))
|
||||
|
||||
if auto_start_stop_feature is True:
|
||||
# Creates a switch to enable the auto-start/stop
|
||||
enable_entity = AutoStartStopEnable(hass, unique_id, name, entry)
|
||||
entities.append(enable_entity)
|
||||
|
||||
async_add_entities(entities, 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
|
||||
and self.my_climate.auto_start_stop_manager is not None
|
||||
):
|
||||
self.my_climate.auto_start_stop_manager.set_auto_start_stop_enable(
|
||||
self._attr_is_on
|
||||
)
|
||||
|
||||
@callback
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""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()
|
||||
|
||||
|
||||
class FollowUnderlyingTemperatureChange(
|
||||
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 = "Follow underlying temp change"
|
||||
self._attr_unique_id = f"{self._device_name}_follow_underlying_temp_change"
|
||||
self._attr_is_on = False
|
||||
|
||||
@property
|
||||
def icon(self) -> str | None:
|
||||
"""The icon"""
|
||||
return "mdi:content-copy"
|
||||
|
||||
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 = False
|
||||
|
||||
self.update_my_state_and_vtherm()
|
||||
|
||||
def update_my_state_and_vtherm(self):
|
||||
"""Update the follow flag in my VTherm"""
|
||||
self.async_write_ha_state()
|
||||
if self.my_climate is not None:
|
||||
self.my_climate.set_follow_underlying_temp_change(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()
|
||||
539
custom_components/versatile_thermostat/tests/commons.py
Normal file
539
custom_components/versatile_thermostat/tests/commons.py
Normal file
@@ -0,0 +1,539 @@
|
||||
""" Some common resources """
|
||||
import asyncio
|
||||
import logging
|
||||
from unittest.mock import patch, MagicMock
|
||||
import pytest # pylint: disable=unused-import
|
||||
|
||||
from homeassistant.core import HomeAssistant, Event, EVENT_STATE_CHANGED, State
|
||||
from homeassistant.const import UnitOfTemperature, STATE_ON, STATE_OFF
|
||||
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.util import dt as dt_util
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.components.climate import (
|
||||
ClimateEntity,
|
||||
DOMAIN as CLIMATE_DOMAIN,
|
||||
HVACMode,
|
||||
HVACAction,
|
||||
ClimateEntityFeature,
|
||||
)
|
||||
|
||||
from pytest_homeassistant_custom_component.common import MockConfigEntry
|
||||
|
||||
from ..climate import VersatileThermostat
|
||||
from ..const import * # pylint: disable=wildcard-import, unused-wildcard-import
|
||||
from ..underlyings import * # pylint: disable=wildcard-import, unused-wildcard-import
|
||||
|
||||
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_TYPE_CONFIG,
|
||||
MOCK_TH_OVER_4SWITCH_TYPE_CONFIG,
|
||||
MOCK_TH_OVER_CLIMATE_TYPE_CONFIG,
|
||||
MOCK_TH_OVER_SWITCH_TPI_CONFIG,
|
||||
MOCK_PRESETS_CONFIG,
|
||||
MOCK_WINDOW_CONFIG,
|
||||
MOCK_MOTION_CONFIG,
|
||||
MOCK_POWER_CONFIG,
|
||||
MOCK_PRESENCE_CONFIG,
|
||||
MOCK_ADVANCED_CONFIG,
|
||||
# MOCK_DEFAULT_FEATURE_CONFIG,
|
||||
PRESET_BOOST,
|
||||
PRESET_COMFORT,
|
||||
PRESET_NONE,
|
||||
PRESET_ECO,
|
||||
PRESET_ACTIVITY,
|
||||
)
|
||||
|
||||
FULL_SWITCH_CONFIG = (
|
||||
MOCK_TH_OVER_SWITCH_USER_CONFIG
|
||||
| MOCK_TH_OVER_SWITCH_TYPE_CONFIG
|
||||
| MOCK_TH_OVER_SWITCH_TPI_CONFIG
|
||||
| MOCK_PRESETS_CONFIG
|
||||
| MOCK_WINDOW_CONFIG
|
||||
| MOCK_MOTION_CONFIG
|
||||
| MOCK_POWER_CONFIG
|
||||
| MOCK_PRESENCE_CONFIG
|
||||
| MOCK_ADVANCED_CONFIG
|
||||
)
|
||||
|
||||
PARTIAL_CLIMATE_CONFIG = (
|
||||
MOCK_TH_OVER_CLIMATE_USER_CONFIG
|
||||
| MOCK_TH_OVER_CLIMATE_TYPE_CONFIG
|
||||
| MOCK_PRESETS_CONFIG
|
||||
| MOCK_ADVANCED_CONFIG
|
||||
)
|
||||
|
||||
FULL_4SWITCH_CONFIG = (
|
||||
MOCK_TH_OVER_4SWITCH_USER_CONFIG
|
||||
| MOCK_TH_OVER_4SWITCH_TYPE_CONFIG
|
||||
| MOCK_TH_OVER_SWITCH_TPI_CONFIG
|
||||
| MOCK_PRESETS_CONFIG
|
||||
| MOCK_WINDOW_CONFIG
|
||||
| MOCK_MOTION_CONFIG
|
||||
| MOCK_POWER_CONFIG
|
||||
| MOCK_PRESENCE_CONFIG
|
||||
| MOCK_ADVANCED_CONFIG
|
||||
)
|
||||
|
||||
_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) -> None:
|
||||
"""Initialize the thermostat."""
|
||||
|
||||
super().__init__()
|
||||
|
||||
self.hass = hass
|
||||
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_mode = hvac_mode
|
||||
self._attr_hvac_modes = [HVACMode.OFF, HVACMode.COOL, HVACMode.HEAT]
|
||||
self._attr_temperature_unit = UnitOfTemperature.CELSIUS
|
||||
self._attr_target_temperature = 20
|
||||
self._attr_current_temperature = 15
|
||||
|
||||
def set_temperature(self, temperature):
|
||||
""" Set the target temperature"""
|
||||
self._attr_target_temperature = temperature
|
||||
self.async_write_ha_state()
|
||||
|
||||
def async_set_hvac_mode(self, hvac_mode):
|
||||
""" The hvac mode"""
|
||||
self._attr_hvac_mode = hvac_mode
|
||||
self.async_write_ha_state()
|
||||
|
||||
class MockUnavailableClimate(ClimateEntity):
|
||||
"""A Mock Climate class used for Underlying climate mode"""
|
||||
|
||||
def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None:
|
||||
"""Initialize the thermostat."""
|
||||
|
||||
super().__init__()
|
||||
|
||||
self._hass = hass
|
||||
self._attr_extra_state_attributes = {}
|
||||
self._unique_id = unique_id
|
||||
self._name = name
|
||||
self._attr_hvac_action = None
|
||||
self._attr_hvac_mode = None
|
||||
self._attr_hvac_modes = [HVACMode.OFF, HVACMode.COOL, HVACMode.HEAT]
|
||||
self._attr_temperature_unit = UnitOfTemperature.CELSIUS
|
||||
|
||||
class MagicMockClimate(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
|
||||
|
||||
|
||||
async def create_thermostat(
|
||||
hass: HomeAssistant, entry: MockConfigEntry, entity_id: str
|
||||
) -> VersatileThermostat:
|
||||
"""Creates and return a TPI Thermostat"""
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.climate.VersatileThermostat.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
|
||||
|
||||
return search_entity(hass, entity_id, CLIMATE_DOMAIN)
|
||||
|
||||
|
||||
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:
|
||||
if entity.entity_id == entity_id:
|
||||
return entity
|
||||
return None
|
||||
|
||||
|
||||
async def send_temperature_change_event(
|
||||
entity: VersatileThermostat, new_temp, date, sleep=True
|
||||
):
|
||||
"""Sending a new temperature event simulating a change on temperature sensor"""
|
||||
_LOGGER.info(
|
||||
"------- Testu: sending send_temperature_change_event, new_temp=%.2f date=%s on %s",
|
||||
new_temp,
|
||||
date,
|
||||
entity,
|
||||
)
|
||||
temp_event = Event(
|
||||
EVENT_STATE_CHANGED,
|
||||
{
|
||||
"new_state": State(
|
||||
entity_id=entity.entity_id,
|
||||
state=new_temp,
|
||||
last_changed=date,
|
||||
last_updated=date,
|
||||
)
|
||||
},
|
||||
)
|
||||
await entity._async_temperature_changed(temp_event)
|
||||
if sleep:
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
|
||||
async def send_ext_temperature_change_event(
|
||||
entity: VersatileThermostat, new_temp, date, sleep=True
|
||||
):
|
||||
"""Sending a new external temperature event simulating a change on temperature sensor"""
|
||||
_LOGGER.info(
|
||||
"------- Testu: sending send_temperature_change_event, new_temp=%.2f date=%s on %s",
|
||||
new_temp,
|
||||
date,
|
||||
entity,
|
||||
)
|
||||
temp_event = Event(
|
||||
EVENT_STATE_CHANGED,
|
||||
{
|
||||
"new_state": State(
|
||||
entity_id=entity.entity_id,
|
||||
state=new_temp,
|
||||
last_changed=date,
|
||||
last_updated=date,
|
||||
)
|
||||
},
|
||||
)
|
||||
await entity._async_ext_temperature_changed(temp_event)
|
||||
if sleep:
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
|
||||
async def send_power_change_event(
|
||||
entity: VersatileThermostat, 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",
|
||||
new_power,
|
||||
date,
|
||||
entity,
|
||||
)
|
||||
power_event = Event(
|
||||
EVENT_STATE_CHANGED,
|
||||
{
|
||||
"new_state": State(
|
||||
entity_id=entity.entity_id,
|
||||
state=new_power,
|
||||
last_changed=date,
|
||||
last_updated=date,
|
||||
)
|
||||
},
|
||||
)
|
||||
await entity._async_power_changed(power_event)
|
||||
if sleep:
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
|
||||
async def send_max_power_change_event(
|
||||
entity: VersatileThermostat, new_power_max, date, sleep=True
|
||||
):
|
||||
"""Sending a new power max event simulating a change on power max sensor"""
|
||||
_LOGGER.info(
|
||||
"------- Testu: sending send_temperature_change_event, new_power_max=%.2f date=%s on %s",
|
||||
new_power_max,
|
||||
date,
|
||||
entity,
|
||||
)
|
||||
power_event = Event(
|
||||
EVENT_STATE_CHANGED,
|
||||
{
|
||||
"new_state": State(
|
||||
entity_id=entity.entity_id,
|
||||
state=new_power_max,
|
||||
last_changed=date,
|
||||
last_updated=date,
|
||||
)
|
||||
},
|
||||
)
|
||||
await entity._async_max_power_changed(power_event)
|
||||
if sleep:
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
|
||||
async def send_window_change_event(
|
||||
entity: VersatileThermostat, new_state: bool, old_state: bool, date, sleep=True
|
||||
):
|
||||
"""Sending a new window event simulating a change on the window state"""
|
||||
_LOGGER.info(
|
||||
"------- Testu: sending send_temperature_change_event, new_state=%s old_state=%s date=%s on %s",
|
||||
new_state,
|
||||
old_state,
|
||||
date,
|
||||
entity,
|
||||
)
|
||||
window_event = Event(
|
||||
EVENT_STATE_CHANGED,
|
||||
{
|
||||
"new_state": State(
|
||||
entity_id=entity.entity_id,
|
||||
state=STATE_ON if new_state else STATE_OFF,
|
||||
last_changed=date,
|
||||
last_updated=date,
|
||||
),
|
||||
"old_state": State(
|
||||
entity_id=entity.entity_id,
|
||||
state=STATE_ON if old_state else STATE_OFF,
|
||||
last_changed=date,
|
||||
last_updated=date,
|
||||
),
|
||||
},
|
||||
)
|
||||
ret = await entity._async_windows_changed(window_event)
|
||||
if sleep:
|
||||
await asyncio.sleep(0.1)
|
||||
return ret
|
||||
|
||||
|
||||
async def send_motion_change_event(
|
||||
entity: VersatileThermostat, new_state: bool, old_state: bool, date, sleep=True
|
||||
):
|
||||
"""Sending a new motion event simulating a change on the window state"""
|
||||
_LOGGER.info(
|
||||
"------- Testu: sending send_temperature_change_event, new_state=%s old_state=%s date=%s on %s",
|
||||
new_state,
|
||||
old_state,
|
||||
date,
|
||||
entity,
|
||||
)
|
||||
motion_event = Event(
|
||||
EVENT_STATE_CHANGED,
|
||||
{
|
||||
"new_state": State(
|
||||
entity_id=entity.entity_id,
|
||||
state=STATE_ON if new_state else STATE_OFF,
|
||||
last_changed=date,
|
||||
last_updated=date,
|
||||
),
|
||||
"old_state": State(
|
||||
entity_id=entity.entity_id,
|
||||
state=STATE_ON if old_state else STATE_OFF,
|
||||
last_changed=date,
|
||||
last_updated=date,
|
||||
),
|
||||
},
|
||||
)
|
||||
ret = await entity._async_motion_changed(motion_event)
|
||||
if sleep:
|
||||
await asyncio.sleep(0.1)
|
||||
return ret
|
||||
|
||||
|
||||
async def send_presence_change_event(
|
||||
entity: VersatileThermostat, new_state: bool, old_state: bool, date, sleep=True
|
||||
):
|
||||
"""Sending a new presence event simulating a change on the window state"""
|
||||
_LOGGER.info(
|
||||
"------- Testu: sending send_temperature_change_event, new_state=%s old_state=%s date=%s on %s",
|
||||
new_state,
|
||||
old_state,
|
||||
date,
|
||||
entity,
|
||||
)
|
||||
presence_event = Event(
|
||||
EVENT_STATE_CHANGED,
|
||||
{
|
||||
"new_state": State(
|
||||
entity_id=entity.entity_id,
|
||||
state=STATE_ON if new_state else STATE_OFF,
|
||||
last_changed=date,
|
||||
last_updated=date,
|
||||
),
|
||||
"old_state": State(
|
||||
entity_id=entity.entity_id,
|
||||
state=STATE_ON if old_state else STATE_OFF,
|
||||
last_changed=date,
|
||||
last_updated=date,
|
||||
),
|
||||
},
|
||||
)
|
||||
ret = await entity._async_presence_changed(presence_event)
|
||||
if sleep:
|
||||
await asyncio.sleep(0.1)
|
||||
return ret
|
||||
|
||||
|
||||
def get_tz(hass: HomeAssistant):
|
||||
"""Get the current timezone"""
|
||||
|
||||
return dt_util.get_time_zone(hass.config.time_zone)
|
||||
|
||||
|
||||
async def send_climate_change_event(
|
||||
entity: VersatileThermostat,
|
||||
new_hvac_mode: HVACMode,
|
||||
old_hvac_mode: HVACMode,
|
||||
new_hvac_action: HVACAction,
|
||||
old_hvac_action: HVACAction,
|
||||
date,
|
||||
sleep=True,
|
||||
):
|
||||
"""Sending a new climate event simulating a change on the underlying climate state"""
|
||||
_LOGGER.info(
|
||||
"------- Testu: sending send_temperature_change_event, new_hvac_mode=%s old_hvac_mode=%s new_hvac_action=%s old_hvac_action=%s date=%s on %s",
|
||||
new_hvac_mode,
|
||||
old_hvac_mode,
|
||||
new_hvac_action,
|
||||
old_hvac_action,
|
||||
date,
|
||||
entity,
|
||||
)
|
||||
climate_event = Event(
|
||||
EVENT_STATE_CHANGED,
|
||||
{
|
||||
"new_state": State(
|
||||
entity_id=entity.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,
|
||||
state=old_hvac_mode,
|
||||
attributes={"hvac_action": old_hvac_action},
|
||||
last_changed=date,
|
||||
last_updated=date,
|
||||
),
|
||||
},
|
||||
)
|
||||
ret = await entity._async_climate_changed(climate_event)
|
||||
if sleep:
|
||||
await asyncio.sleep(0.1)
|
||||
return ret
|
||||
|
||||
async def send_climate_change_event_with_temperature(
|
||||
entity: VersatileThermostat,
|
||||
new_hvac_mode: HVACMode,
|
||||
old_hvac_mode: HVACMode,
|
||||
new_hvac_action: HVACAction,
|
||||
old_hvac_action: HVACAction,
|
||||
date,
|
||||
temperature,
|
||||
sleep=True,
|
||||
):
|
||||
"""Sending a new climate event simulating a change on the underlying climate state"""
|
||||
_LOGGER.info(
|
||||
"------- Testu: sending send_temperature_change_event, new_hvac_mode=%s old_hvac_mode=%s new_hvac_action=%s old_hvac_action=%s date=%s temperature=%s on %s",
|
||||
new_hvac_mode,
|
||||
old_hvac_mode,
|
||||
new_hvac_action,
|
||||
old_hvac_action,
|
||||
date,
|
||||
temperature,
|
||||
entity,
|
||||
)
|
||||
climate_event = Event(
|
||||
EVENT_STATE_CHANGED,
|
||||
{
|
||||
"new_state": State(
|
||||
entity_id=entity.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,
|
||||
state=old_hvac_mode,
|
||||
attributes={"hvac_action": old_hvac_action},
|
||||
last_changed=date,
|
||||
last_updated=date,
|
||||
),
|
||||
},
|
||||
)
|
||||
ret = await entity._async_climate_changed(climate_event)
|
||||
if sleep:
|
||||
await asyncio.sleep(0.1)
|
||||
return ret
|
||||
|
||||
|
||||
def cancel_switchs_cycles(entity: VersatileThermostat):
|
||||
"""This method will cancel all running cycle on all underlying switch entity"""
|
||||
if entity._is_over_climate:
|
||||
return
|
||||
for under in entity._underlyings:
|
||||
under._cancel_cycle()
|
||||
@@ -1,6 +1,4 @@
|
||||
"""Global fixtures for integration_blueprint integration."""
|
||||
# pylint: disable=line-too-long
|
||||
|
||||
# Fixtures allow you to replace functions with a Mock object. You can perform
|
||||
# many options via the Mock to reflect a particular behavior from the original
|
||||
# function that you want to see without going through the function's actual logic.
|
||||
@@ -19,8 +17,6 @@
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
# https://github.com/miketheman/pytest-socket/pull/275
|
||||
from pytest_socket import socket_allow_hosts
|
||||
|
||||
from homeassistant.core import StateMachine
|
||||
|
||||
@@ -28,37 +24,17 @@ from custom_components.versatile_thermostat.config_flow import (
|
||||
VersatileThermostatBaseConfigFlow,
|
||||
)
|
||||
|
||||
from custom_components.versatile_thermostat.const import (
|
||||
CONF_POWER_SENSOR,
|
||||
CONF_MAX_POWER_SENSOR,
|
||||
CONF_USE_POWER_FEATURE,
|
||||
CONF_PRESET_POWER,
|
||||
from custom_components.versatile_thermostat.climate import (
|
||||
VersatileThermostat,
|
||||
)
|
||||
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,
|
||||
)
|
||||
|
||||
# ...
|
||||
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
|
||||
|
||||
|
||||
# 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):
|
||||
"""Enable all integration in tests"""
|
||||
yield
|
||||
|
||||
@@ -106,7 +82,7 @@ def skip_hass_states_get_fixture():
|
||||
def skip_control_heating_fixture():
|
||||
"""Skip the control_heating of VersatileThermostat"""
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.async_control_heating"
|
||||
"custom_components.versatile_thermostat.climate.VersatileThermostat._async_control_heating"
|
||||
):
|
||||
yield
|
||||
|
||||
@@ -129,54 +105,6 @@ def skip_hass_states_is_state_fixture():
|
||||
|
||||
@pytest.fixture(name="skip_send_event")
|
||||
def skip_send_event_fixture():
|
||||
"""Skip the send_event in BaseThermostat"""
|
||||
with patch.object(BaseThermostat, "send_event"):
|
||||
"""Skip the send_event in VersatileThermostat"""
|
||||
with patch.object(VersatileThermostat, "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
|
||||
|
||||
|
||||
@pytest.fixture(name="init_central_power_manager")
|
||||
async def init_central_power_manager_fixture(
|
||||
hass, init_central_config
|
||||
): # pylint: disable=unused-argument
|
||||
"""Initialize the central power_manager"""
|
||||
vtherm_api: VersatileThermostatAPI = VersatileThermostatAPI.get_vtherm_api(hass)
|
||||
|
||||
# 1. creation / init
|
||||
vtherm_api.central_power_manager.post_init(
|
||||
{
|
||||
CONF_POWER_SENSOR: "sensor.the_power_sensor",
|
||||
CONF_MAX_POWER_SENSOR: "sensor.the_max_power_sensor",
|
||||
CONF_USE_POWER_FEATURE: True,
|
||||
CONF_PRESET_POWER: 13,
|
||||
}
|
||||
)
|
||||
assert vtherm_api.central_power_manager.is_configured
|
||||
|
||||
yield
|
||||
168
custom_components/versatile_thermostat/tests/const.py
Normal file
168
custom_components/versatile_thermostat/tests/const.py
Normal file
@@ -0,0 +1,168 @@
|
||||
""" The commons const for all tests """
|
||||
from homeassistant.components.climate.const import ( # pylint: disable=unused-import
|
||||
PRESET_BOOST,
|
||||
PRESET_COMFORT,
|
||||
PRESET_ECO,
|
||||
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_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,
|
||||
)
|
||||
|
||||
MOCK_TH_OVER_SWITCH_USER_CONFIG = {
|
||||
CONF_NAME: "TheOverSwitchMockName",
|
||||
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_SWITCH,
|
||||
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
|
||||
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
|
||||
CONF_CYCLE_MIN: 5,
|
||||
CONF_TEMP_MIN: 15,
|
||||
CONF_TEMP_MAX: 30,
|
||||
CONF_DEVICE_POWER: 1,
|
||||
CONF_USE_WINDOW_FEATURE: True,
|
||||
CONF_USE_MOTION_FEATURE: True,
|
||||
CONF_USE_POWER_FEATURE: True,
|
||||
CONF_USE_PRESENCE_FEATURE: True,
|
||||
}
|
||||
|
||||
MOCK_TH_OVER_4SWITCH_USER_CONFIG = {
|
||||
CONF_NAME: "TheOver4SwitchMockName",
|
||||
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,
|
||||
CONF_DEVICE_POWER: 1,
|
||||
CONF_USE_WINDOW_FEATURE: True,
|
||||
CONF_USE_MOTION_FEATURE: True,
|
||||
CONF_USE_POWER_FEATURE: True,
|
||||
CONF_USE_PRESENCE_FEATURE: True,
|
||||
}
|
||||
|
||||
MOCK_TH_OVER_CLIMATE_USER_CONFIG = {
|
||||
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,
|
||||
CONF_DEVICE_POWER: 1,
|
||||
# Keep default values which are False
|
||||
}
|
||||
|
||||
MOCK_TH_OVER_SWITCH_TYPE_CONFIG = {
|
||||
CONF_HEATER: "switch.mock_switch",
|
||||
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
|
||||
}
|
||||
|
||||
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_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
|
||||
}
|
||||
|
||||
MOCK_TH_OVER_SWITCH_TPI_CONFIG = {
|
||||
CONF_TPI_COEF_INT: 0.3,
|
||||
CONF_TPI_COEF_EXT: 0.1,
|
||||
}
|
||||
|
||||
MOCK_TH_OVER_CLIMATE_TYPE_CONFIG = {
|
||||
CONF_CLIMATE: "climate.mock_climate",
|
||||
CONF_AC_MODE: False,
|
||||
}
|
||||
|
||||
MOCK_PRESETS_CONFIG = {
|
||||
PRESET_ECO + "_temp": 16,
|
||||
PRESET_COMFORT + "_temp": 17,
|
||||
PRESET_BOOST + "_temp": 18,
|
||||
}
|
||||
|
||||
MOCK_WINDOW_CONFIG = {
|
||||
CONF_WINDOW_SENSOR: "binary_sensor.window_sensor",
|
||||
CONF_WINDOW_DELAY: 10,
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
|
||||
MOCK_MOTION_CONFIG = {
|
||||
CONF_MOTION_SENSOR: "input_boolean.motion_sensor",
|
||||
CONF_MOTION_DELAY: 10,
|
||||
CONF_MOTION_PRESET: PRESET_COMFORT,
|
||||
CONF_NO_MOTION_PRESET: PRESET_ECO,
|
||||
}
|
||||
|
||||
MOCK_POWER_CONFIG = {
|
||||
CONF_POWER_SENSOR: "sensor.power_sensor",
|
||||
CONF_MAX_POWER_SENSOR: "sensor.power_max_sensor",
|
||||
CONF_PRESET_POWER: 10,
|
||||
}
|
||||
|
||||
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_ADVANCED_CONFIG = {
|
||||
CONF_MINIMAL_ACTIVATION_DELAY: 10,
|
||||
CONF_SECURITY_DELAY_MIN: 5,
|
||||
CONF_SECURITY_MIN_ON_PERCENT: 0.4,
|
||||
CONF_SECURITY_DEFAULT_ON_PERCENT: 0.3,
|
||||
}
|
||||
|
||||
MOCK_DEFAULT_FEATURE_CONFIG = {
|
||||
CONF_USE_WINDOW_FEATURE: False,
|
||||
CONF_USE_MOTION_FEATURE: False,
|
||||
CONF_USE_POWER_FEATURE: False,
|
||||
CONF_USE_PRESENCE_FEATURE: False,
|
||||
}
|
||||
@@ -1,7 +1,5 @@
|
||||
# pylint: disable=wildcard-import, unused-wildcard-import, unused-argument, line-too-long, protected-access
|
||||
|
||||
""" Test the normal start of a Thermostat """
|
||||
from unittest.mock import patch, PropertyMock
|
||||
from unittest.mock import patch
|
||||
from datetime import timedelta, datetime
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
@@ -11,8 +9,9 @@ from homeassistant.components.binary_sensor import BinarySensorDeviceClass
|
||||
|
||||
from pytest_homeassistant_custom_component.common import MockConfigEntry
|
||||
|
||||
from custom_components.versatile_thermostat.base_thermostat import BaseThermostat
|
||||
from custom_components.versatile_thermostat.binary_sensor import (
|
||||
from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import
|
||||
from ..climate import VersatileThermostat
|
||||
from ..binary_sensor import (
|
||||
SecurityBinarySensor,
|
||||
OverpoweringBinarySensor,
|
||||
WindowBinarySensor,
|
||||
@@ -20,7 +19,7 @@ from custom_components.versatile_thermostat.binary_sensor import (
|
||||
PresenceBinarySensor,
|
||||
)
|
||||
|
||||
from .commons import *
|
||||
from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import
|
||||
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@@ -30,7 +29,7 @@ async def test_security_binary_sensors(
|
||||
skip_hass_states_is_state,
|
||||
skip_turn_on_off_heater,
|
||||
skip_send_event,
|
||||
): # pylint: disable=unused-argument
|
||||
):
|
||||
"""Test the security binary sensors in thermostat type"""
|
||||
|
||||
entry = MockConfigEntry(
|
||||
@@ -57,13 +56,12 @@ async def test_security_binary_sensors(
|
||||
CONF_TPI_COEF_INT: 0.3,
|
||||
CONF_TPI_COEF_EXT: 0.01,
|
||||
CONF_MINIMAL_ACTIVATION_DELAY: 30,
|
||||
CONF_MINIMAL_DEACTIVATION_DELAY: 30,
|
||||
CONF_SAFETY_DELAY_MIN: 5,
|
||||
CONF_SAFETY_MIN_ON_PERCENT: 0.3,
|
||||
CONF_SECURITY_DELAY_MIN: 5,
|
||||
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
|
||||
},
|
||||
)
|
||||
|
||||
entity: BaseThermostat = await create_thermostat (
|
||||
entity: VersatileThermostat = await create_thermostat(
|
||||
hass, entry, "climate.theoverswitchmockname"
|
||||
)
|
||||
assert entity
|
||||
@@ -85,17 +83,17 @@ async def test_security_binary_sensors(
|
||||
# Set temperature in the past
|
||||
event_timestamp = now - timedelta(minutes=6)
|
||||
|
||||
# set temperature to 15 so that on_percent will be > safety_min_on_percent (0.2)
|
||||
# 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.safety_state is STATE_ON
|
||||
assert entity.security_state is True
|
||||
# Simulate the event reception
|
||||
await security_binary_sensor.async_my_climate_changed()
|
||||
assert security_binary_sensor.state == STATE_ON
|
||||
|
||||
# set temperature now
|
||||
await send_temperature_change_event(entity, 15, now)
|
||||
assert entity.safety_state is not STATE_ON
|
||||
assert entity.security_state is False
|
||||
# Simulate the event reception
|
||||
await security_binary_sensor.async_my_climate_changed()
|
||||
assert security_binary_sensor.state == STATE_OFF
|
||||
@@ -108,16 +106,9 @@ async def test_overpowering_binary_sensors(
|
||||
skip_hass_states_is_state,
|
||||
skip_turn_on_off_heater,
|
||||
skip_send_event,
|
||||
init_central_power_manager,
|
||||
):
|
||||
"""Test the overpowering binary sensors in thermostat type"""
|
||||
|
||||
temps = {
|
||||
"eco": 17,
|
||||
"comfort": 18,
|
||||
"boost": 19,
|
||||
}
|
||||
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
title="TheOverSwitchMockName",
|
||||
@@ -130,6 +121,9 @@ async def test_overpowering_binary_sensors(
|
||||
CONF_CYCLE_MIN: 5,
|
||||
CONF_TEMP_MIN: 15,
|
||||
CONF_TEMP_MAX: 30,
|
||||
"eco_temp": 17,
|
||||
"comfort_temp": 18,
|
||||
"boost_temp": 19,
|
||||
CONF_USE_WINDOW_FEATURE: False,
|
||||
CONF_USE_MOTION_FEATURE: False,
|
||||
CONF_USE_POWER_FEATURE: True,
|
||||
@@ -139,16 +133,17 @@ async def test_overpowering_binary_sensors(
|
||||
CONF_TPI_COEF_INT: 0.3,
|
||||
CONF_TPI_COEF_EXT: 0.01,
|
||||
CONF_MINIMAL_ACTIVATION_DELAY: 30,
|
||||
CONF_MINIMAL_DEACTIVATION_DELAY: 0,
|
||||
CONF_SAFETY_DELAY_MIN: 5,
|
||||
CONF_SAFETY_MIN_ON_PERCENT: 0.3,
|
||||
CONF_SECURITY_DELAY_MIN: 5,
|
||||
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
|
||||
CONF_POWER_SENSOR: "sensor.mock_power_sensor",
|
||||
CONF_MAX_POWER_SENSOR: "sensor.mock_power_max_sensor",
|
||||
CONF_DEVICE_POWER: 100,
|
||||
CONF_PRESET_POWER: 12,
|
||||
},
|
||||
)
|
||||
|
||||
entity: BaseThermostat = await create_thermostat(
|
||||
hass, entry, "climate.theoverswitchmockname", temps
|
||||
entity: VersatileThermostat = await create_thermostat(
|
||||
hass, entry, "climate.theoverswitchmockname"
|
||||
)
|
||||
assert entity
|
||||
|
||||
@@ -157,55 +152,35 @@ async def test_overpowering_binary_sensors(
|
||||
)
|
||||
assert overpowering_binary_sensor
|
||||
|
||||
now: datetime = NowClass.get_now(hass)
|
||||
VersatileThermostatAPI.get_vtherm_api()._set_now(now)
|
||||
now: datetime = datetime.now(tz=get_tz(hass))
|
||||
|
||||
# Overpowering should be not set because poer have not been received
|
||||
await entity.async_set_preset_mode(PRESET_COMFORT)
|
||||
await entity.async_set_hvac_mode(HVACMode.HEAT)
|
||||
await send_temperature_change_event(entity, 15, now)
|
||||
assert entity.power_manager.is_overpowering_detected is False
|
||||
assert entity.power_manager.overpowering_state is STATE_UNKNOWN
|
||||
assert await entity.check_overpowering() is False
|
||||
assert entity.overpowering_state is None
|
||||
|
||||
await overpowering_binary_sensor.async_my_climate_changed()
|
||||
assert overpowering_binary_sensor.state is STATE_OFF
|
||||
assert overpowering_binary_sensor.device_class == BinarySensorDeviceClass.POWER
|
||||
|
||||
# Send power mesurement
|
||||
side_effects = SideEffects(
|
||||
{
|
||||
"sensor.the_power_sensor": State("sensor.the_power_sensor", 150),
|
||||
"sensor.the_max_power_sensor": State("sensor.the_max_power_sensor", 100),
|
||||
},
|
||||
State("unknown.entity_id", "unknown"),
|
||||
)
|
||||
# fmt:off
|
||||
with patch("homeassistant.core.StateMachine.get", side_effect=side_effects.get_side_effects()), \
|
||||
patch("custom_components.versatile_thermostat.thermostat_switch.ThermostatOverSwitch.is_device_active", new_callable=PropertyMock, return_value=True):
|
||||
# fmt: on
|
||||
await send_power_change_event(entity, 150, now)
|
||||
await send_max_power_change_event(entity, 100, now)
|
||||
await send_power_change_event(entity, 100, now)
|
||||
await send_max_power_change_event(entity, 150, now)
|
||||
assert await entity.check_overpowering() is True
|
||||
assert entity.overpowering_state is True
|
||||
|
||||
assert entity.power_manager.is_overpowering_detected is True
|
||||
assert entity.power_manager.overpowering_state is STATE_ON
|
||||
|
||||
# Simulate the event reception
|
||||
await overpowering_binary_sensor.async_my_climate_changed()
|
||||
assert overpowering_binary_sensor.state == STATE_ON
|
||||
# Simulate the event reception
|
||||
await overpowering_binary_sensor.async_my_climate_changed()
|
||||
assert overpowering_binary_sensor.state == STATE_ON
|
||||
|
||||
# set max power to a low value
|
||||
side_effects.add_or_update_side_effect("sensor.the_max_power_sensor", State("sensor.the_max_power_sensor", 251))
|
||||
# fmt:off
|
||||
with patch("homeassistant.core.StateMachine.get", side_effect=side_effects.get_side_effects()):
|
||||
# fmt: on
|
||||
now = now + timedelta(seconds=30)
|
||||
VersatileThermostatAPI.get_vtherm_api()._set_now(now)
|
||||
await send_max_power_change_event(entity, 251, now)
|
||||
assert entity.power_manager.is_overpowering_detected is False
|
||||
assert entity.power_manager.overpowering_state is STATE_OFF
|
||||
# Simulate the event reception
|
||||
await overpowering_binary_sensor.async_my_climate_changed()
|
||||
assert overpowering_binary_sensor.state == STATE_OFF
|
||||
await send_max_power_change_event(entity, 201, now)
|
||||
assert await entity.check_overpowering() is False
|
||||
assert entity.overpowering_state is False
|
||||
# Simulate the event reception
|
||||
await overpowering_binary_sensor.async_my_climate_changed()
|
||||
assert overpowering_binary_sensor.state == STATE_OFF
|
||||
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@@ -242,15 +217,14 @@ async def test_window_binary_sensors(
|
||||
CONF_TPI_COEF_INT: 0.3,
|
||||
CONF_TPI_COEF_EXT: 0.01,
|
||||
CONF_MINIMAL_ACTIVATION_DELAY: 30,
|
||||
CONF_MINIMAL_DEACTIVATION_DELAY: 0,
|
||||
CONF_SAFETY_DELAY_MIN: 5,
|
||||
CONF_SAFETY_MIN_ON_PERCENT: 0.3,
|
||||
CONF_SECURITY_DELAY_MIN: 5,
|
||||
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
|
||||
CONF_WINDOW_SENSOR: "binary_sensor.mock_window_sensor",
|
||||
CONF_WINDOW_DELAY: 0, # important to not been obliged to wait
|
||||
},
|
||||
)
|
||||
|
||||
entity: BaseThermostat = await create_thermostat(
|
||||
entity: VersatileThermostat = await create_thermostat(
|
||||
hass, entry, "climate.theoverswitchmockname"
|
||||
)
|
||||
assert entity
|
||||
@@ -266,7 +240,7 @@ async def test_window_binary_sensors(
|
||||
await entity.async_set_preset_mode(PRESET_COMFORT)
|
||||
await entity.async_set_hvac_mode(HVACMode.HEAT)
|
||||
await send_temperature_change_event(entity, 15, now)
|
||||
assert entity.window_state is STATE_UNKNOWN
|
||||
assert entity.window_state is None
|
||||
|
||||
await window_binary_sensor.async_my_climate_changed()
|
||||
assert window_binary_sensor.state is STATE_OFF
|
||||
@@ -331,17 +305,14 @@ async def test_motion_binary_sensors(
|
||||
CONF_TPI_COEF_INT: 0.3,
|
||||
CONF_TPI_COEF_EXT: 0.01,
|
||||
CONF_MINIMAL_ACTIVATION_DELAY: 30,
|
||||
CONF_MINIMAL_DEACTIVATION_DELAY: 0,
|
||||
CONF_SAFETY_DELAY_MIN: 5,
|
||||
CONF_SAFETY_MIN_ON_PERCENT: 0.3,
|
||||
CONF_SECURITY_DELAY_MIN: 5,
|
||||
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
|
||||
CONF_MOTION_SENSOR: "binary_sensor.mock_motion_sensor",
|
||||
CONF_MOTION_DELAY: 0, # important to not been obliged to wait
|
||||
CONF_MOTION_PRESET: PRESET_BOOST,
|
||||
CONF_NO_MOTION_PRESET: PRESET_ECO,
|
||||
},
|
||||
)
|
||||
|
||||
entity: BaseThermostat = await create_thermostat(
|
||||
entity: VersatileThermostat = await create_thermostat(
|
||||
hass, entry, "climate.theoverswitchmockname"
|
||||
)
|
||||
assert entity
|
||||
@@ -357,7 +328,7 @@ async def test_motion_binary_sensors(
|
||||
await entity.async_set_preset_mode(PRESET_COMFORT)
|
||||
await entity.async_set_hvac_mode(HVACMode.HEAT)
|
||||
await send_temperature_change_event(entity, 15, now)
|
||||
assert entity.motion_state is STATE_UNKNOWN
|
||||
assert entity.motion_state is None
|
||||
|
||||
await motion_binary_sensor.async_my_climate_changed()
|
||||
assert motion_binary_sensor.state is STATE_OFF
|
||||
@@ -425,14 +396,13 @@ async def test_presence_binary_sensors(
|
||||
CONF_TPI_COEF_INT: 0.3,
|
||||
CONF_TPI_COEF_EXT: 0.01,
|
||||
CONF_MINIMAL_ACTIVATION_DELAY: 30,
|
||||
CONF_MINIMAL_DEACTIVATION_DELAY: 0,
|
||||
CONF_SAFETY_DELAY_MIN: 5,
|
||||
CONF_SAFETY_MIN_ON_PERCENT: 0.3,
|
||||
CONF_SECURITY_DELAY_MIN: 5,
|
||||
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
|
||||
CONF_PRESENCE_SENSOR: "binary_sensor.mock_presence_sensor",
|
||||
},
|
||||
)
|
||||
|
||||
entity: BaseThermostat = await create_thermostat(
|
||||
entity: VersatileThermostat = await create_thermostat(
|
||||
hass, entry, "climate.theoverswitchmockname"
|
||||
)
|
||||
assert entity
|
||||
@@ -448,7 +418,7 @@ async def test_presence_binary_sensors(
|
||||
await entity.async_set_preset_mode(PRESET_COMFORT)
|
||||
await entity.async_set_hvac_mode(HVACMode.HEAT)
|
||||
await send_temperature_change_event(entity, 15, now)
|
||||
assert entity.presence_state is STATE_UNKNOWN
|
||||
assert entity.presence_state is None
|
||||
|
||||
await presence_binary_sensor.async_my_climate_changed()
|
||||
assert presence_binary_sensor.state is STATE_OFF
|
||||
@@ -509,13 +479,12 @@ async def test_binary_sensors_over_climate_minimal(
|
||||
CONF_USE_PRESENCE_FEATURE: False,
|
||||
CONF_CLIMATE: "climate.mock_climate",
|
||||
CONF_MINIMAL_ACTIVATION_DELAY: 30,
|
||||
CONF_MINIMAL_DEACTIVATION_DELAY: 0,
|
||||
CONF_SAFETY_DELAY_MIN: 5,
|
||||
CONF_SAFETY_MIN_ON_PERCENT: 0.3,
|
||||
CONF_SECURITY_DELAY_MIN: 5,
|
||||
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
|
||||
},
|
||||
)
|
||||
|
||||
entity: BaseThermostat = await create_thermostat(
|
||||
entity: VersatileThermostat = await create_thermostat(
|
||||
hass, entry, "climate.theoverclimatemockname"
|
||||
)
|
||||
assert entity
|
||||
@@ -545,4 +514,3 @@ async def test_binary_sensors_over_climate_minimal(
|
||||
hass, "binary_sensor.theoverclimatemockname_presence_state", "binary_sensor"
|
||||
)
|
||||
assert presence_binary_sensor is None
|
||||
|
||||
540
custom_components/versatile_thermostat/tests/test_bugs.py
Normal file
540
custom_components/versatile_thermostat/tests/test_bugs.py
Normal file
@@ -0,0 +1,540 @@
|
||||
""" Test the Window management """
|
||||
from unittest.mock import patch, call
|
||||
from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
import logging
|
||||
|
||||
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: VersatileThermostat = 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:
|
||||
await entity._async_control_heating()
|
||||
# an exception should be send
|
||||
assert False
|
||||
except UnknownEntity:
|
||||
pass
|
||||
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(
|
||||
hass: HomeAssistant,
|
||||
skip_hass_states_is_state,
|
||||
skip_turn_on_off_heater,
|
||||
skip_send_event,
|
||||
):
|
||||
"""Test that it should be possible to set the security_default_on_percent to 0"""
|
||||
|
||||
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,
|
||||
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.0, # !! here
|
||||
CONF_SECURITY_DEFAULT_ON_PERCENT: 0.0, # !! here
|
||||
CONF_DEVICE_POWER: 200,
|
||||
},
|
||||
)
|
||||
|
||||
entity: VersatileThermostat = await create_thermostat(
|
||||
hass, entry, "climate.theoverswitchmockname"
|
||||
)
|
||||
assert entity
|
||||
|
||||
assert entity._security_min_on_percent == 0
|
||||
assert entity._security_default_on_percent == 0
|
||||
|
||||
|
||||
# Waiting for answer in https://github.com/jmcollin78/versatile_thermostat/issues/64
|
||||
# Repro case not evident
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_bug_64(
|
||||
hass: HomeAssistant,
|
||||
skip_hass_states_is_state,
|
||||
skip_turn_on_off_heater,
|
||||
skip_send_event,
|
||||
):
|
||||
"""Test that it should be possible to set the security_default_on_percent to 0"""
|
||||
|
||||
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,
|
||||
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.5,
|
||||
CONF_SECURITY_DEFAULT_ON_PERCENT: 0.1, # !! here
|
||||
CONF_DEVICE_POWER: 200,
|
||||
},
|
||||
)
|
||||
|
||||
entity: VersatileThermostat = await create_thermostat(
|
||||
hass, entry, "climate.theoverswitchmockname"
|
||||
)
|
||||
assert entity
|
||||
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_bug_66(
|
||||
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"""
|
||||
|
||||
tz = get_tz(hass) # pylint: disable=invalid-name
|
||||
now: datetime = datetime.now(tz=tz)
|
||||
|
||||
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,
|
||||
CONF_USE_WINDOW_FEATURE: True,
|
||||
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.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
|
||||
},
|
||||
)
|
||||
|
||||
entity: VersatileThermostat = 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)
|
||||
|
||||
assert entity.hvac_mode is HVACMode.HEAT
|
||||
assert entity.preset_mode is PRESET_BOOST
|
||||
assert entity.target_temperature == 19
|
||||
assert entity.window_state is None
|
||||
|
||||
# Open the window and let the thermostat shut down
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.climate.VersatileThermostat.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=True,
|
||||
):
|
||||
await send_temperature_change_event(entity, 15, now)
|
||||
try_window_condition = await send_window_change_event(
|
||||
entity, True, False, now, False
|
||||
)
|
||||
|
||||
# simulate the call to try_window_condition
|
||||
await try_window_condition(None)
|
||||
|
||||
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
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.climate.VersatileThermostat.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(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
|
||||
return_value=False,
|
||||
):
|
||||
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)
|
||||
|
||||
# window state should not have change
|
||||
assert entity.window_state == STATE_ON
|
||||
|
||||
# Reopen immediatly with sufficient time
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.climate.VersatileThermostat.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.climate.VersatileThermostat.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 entity.hvac_mode is HVACMode.HEAT
|
||||
assert entity.preset_mode is PRESET_BOOST
|
||||
|
||||
|
||||
@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", {})
|
||||
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.climate.VersatileThermostat.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.climate.VersatileThermostat.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)
|
||||
# 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_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)
|
||||
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.climate.VersatileThermostat.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 the underlying is heating. In real life the underlying should be shut-off
|
||||
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
|
||||
await entity.async_set_preset_mode(PRESET_COMFORT)
|
||||
assert entity.preset_mode == PRESET_COMFORT
|
||||
|
||||
# 2. Change the target temp of underlying thermostat
|
||||
await send_climate_change_event_with_temperature(entity, HVACMode.HEAT, HVACMode.HEAT, HVACAction.OFF, HVACAction.OFF, now, 12.75)
|
||||
# Should have been switched to Manual preset
|
||||
assert entity.target_temperature == 12.75
|
||||
assert entity.preset_mode is PRESET_NONE
|
||||
|
||||
|
||||
|
||||
473
custom_components/versatile_thermostat/tests/test_config_flow.py
Normal file
473
custom_components/versatile_thermostat/tests/test_config_flow.py
Normal file
@@ -0,0 +1,473 @@
|
||||
""" Test the Versatile Thermostat config flow """
|
||||
|
||||
from homeassistant import data_entry_flow
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.config_entries import SOURCE_USER, ConfigEntry
|
||||
|
||||
from custom_components.versatile_thermostat.const import DOMAIN
|
||||
|
||||
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_show_form(hass: HomeAssistant) -> None:
|
||||
"""Test that the form is served with no input"""
|
||||
# Init the API
|
||||
# hass.data["custom_components"] = None
|
||||
# loader.async_get_custom_components(hass)
|
||||
# VersatileThermostatAPI(hass)
|
||||
|
||||
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
|
||||
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_user_config_flow_over_switch(hass: HomeAssistant, skip_hass_states_get):
|
||||
"""Test the config flow with all thermostat_over_switch features"""
|
||||
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=MOCK_TH_OVER_SWITCH_USER_CONFIG
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["step_id"] == "type"
|
||||
assert result["errors"] == {}
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input=MOCK_TH_OVER_SWITCH_TYPE_CONFIG
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["step_id"] == "tpi"
|
||||
assert result["errors"] == {}
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input=MOCK_TH_OVER_SWITCH_TPI_CONFIG
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["step_id"] == "presets"
|
||||
assert result["errors"] == {}
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input=MOCK_PRESETS_CONFIG
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["step_id"] == "window"
|
||||
assert result["errors"] == {}
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input=MOCK_WINDOW_CONFIG
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["step_id"] == "motion"
|
||||
assert result["errors"] == {}
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input=MOCK_MOTION_CONFIG
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["step_id"] == "power"
|
||||
assert result["errors"] == {}
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input=MOCK_POWER_CONFIG
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["step_id"] == "presence"
|
||||
assert result["errors"] == {}
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input=MOCK_PRESENCE_CONFIG
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["step_id"] == "advanced"
|
||||
assert result["errors"] == {}
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input=MOCK_ADVANCED_CONFIG
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
|
||||
assert (
|
||||
result["data"]
|
||||
== MOCK_TH_OVER_SWITCH_USER_CONFIG
|
||||
| MOCK_TH_OVER_SWITCH_TYPE_CONFIG
|
||||
| MOCK_TH_OVER_SWITCH_TPI_CONFIG
|
||||
| MOCK_PRESETS_CONFIG
|
||||
| MOCK_WINDOW_CONFIG
|
||||
| MOCK_MOTION_CONFIG
|
||||
| MOCK_POWER_CONFIG
|
||||
| MOCK_PRESENCE_CONFIG
|
||||
| MOCK_ADVANCED_CONFIG
|
||||
)
|
||||
assert result["result"]
|
||||
assert result["result"].domain == DOMAIN
|
||||
assert result["result"].version == 1
|
||||
assert result["result"].title == "TheOverSwitchMockName"
|
||||
assert isinstance(result["result"], ConfigEntry)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_user_config_flow_over_climate(hass: HomeAssistant, skip_hass_states_get):
|
||||
"""Test the config flow with all thermostat_over_climate features and no additional features"""
|
||||
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=MOCK_TH_OVER_CLIMATE_USER_CONFIG
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["step_id"] == "type"
|
||||
assert result["errors"] == {}
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input=MOCK_TH_OVER_CLIMATE_TYPE_CONFIG
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["step_id"] == "presets"
|
||||
assert result["errors"] == {}
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input=MOCK_PRESETS_CONFIG
|
||||
)
|
||||
|
||||
# assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
# assert result["step_id"] == "window"
|
||||
# assert result["errors"] == {}
|
||||
|
||||
# result = await hass.config_entries.flow.async_configure(
|
||||
# result["flow_id"], user_input=MOCK_WINDOW_CONFIG
|
||||
# )
|
||||
|
||||
# assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
# assert result["step_id"] == "motion"
|
||||
# assert result["errors"] == {}
|
||||
|
||||
# result = await hass.config_entries.flow.async_configure(
|
||||
# result["flow_id"], user_input=MOCK_MOTION_CONFIG
|
||||
# )
|
||||
|
||||
# assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
# assert result["step_id"] == "power"
|
||||
# assert result["errors"] == {}
|
||||
|
||||
# result = await hass.config_entries.flow.async_configure(
|
||||
# result["flow_id"], user_input=MOCK_POWER_CONFIG
|
||||
# )
|
||||
|
||||
# assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
# assert result["step_id"] == "presence"
|
||||
# assert result["errors"] == {}
|
||||
|
||||
# result = await hass.config_entries.flow.async_configure(
|
||||
# result["flow_id"], user_input=MOCK_PRESENCE_CONFIG
|
||||
# )
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["step_id"] == "advanced"
|
||||
assert result["errors"] == {}
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input=MOCK_ADVANCED_CONFIG
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
|
||||
assert (
|
||||
result["data"]
|
||||
== MOCK_TH_OVER_CLIMATE_USER_CONFIG
|
||||
| MOCK_TH_OVER_CLIMATE_TYPE_CONFIG
|
||||
| MOCK_PRESETS_CONFIG
|
||||
| MOCK_ADVANCED_CONFIG
|
||||
| MOCK_DEFAULT_FEATURE_CONFIG
|
||||
)
|
||||
assert result["result"]
|
||||
assert result["result"].domain == DOMAIN
|
||||
assert result["result"].version == 1
|
||||
assert result["result"].title == "TheOverClimateMockName"
|
||||
assert isinstance(result["result"], ConfigEntry)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_user_config_flow_window_auto_ok(
|
||||
hass: HomeAssistant, skip_hass_states_get, skip_control_heating
|
||||
):
|
||||
"""Test the config flow with only window auto feature"""
|
||||
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_NAME: "TheOverSwitchMockName",
|
||||
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_SWITCH,
|
||||
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
|
||||
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
|
||||
CONF_CYCLE_MIN: 5,
|
||||
CONF_TEMP_MIN: 15,
|
||||
CONF_TEMP_MAX: 30,
|
||||
CONF_DEVICE_POWER: 1,
|
||||
CONF_USE_WINDOW_FEATURE: True,
|
||||
CONF_USE_MOTION_FEATURE: False,
|
||||
CONF_USE_POWER_FEATURE: False,
|
||||
CONF_USE_PRESENCE_FEATURE: False,
|
||||
},
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["step_id"] == "type"
|
||||
assert result["errors"] == {}
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input=MOCK_TH_OVER_SWITCH_TYPE_CONFIG
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["step_id"] == "tpi"
|
||||
assert result["errors"] == {}
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input=MOCK_TH_OVER_SWITCH_TPI_CONFIG
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["step_id"] == "presets"
|
||||
assert result["errors"] == {}
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input=MOCK_PRESETS_CONFIG
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["step_id"] == "window"
|
||||
assert result["errors"] == {}
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input=MOCK_WINDOW_AUTO_CONFIG,
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["step_id"] == "advanced"
|
||||
assert result["errors"] == {}
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input=MOCK_ADVANCED_CONFIG
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
|
||||
assert (
|
||||
result["data"]
|
||||
== MOCK_TH_OVER_SWITCH_USER_CONFIG
|
||||
| {
|
||||
CONF_USE_MOTION_FEATURE: False,
|
||||
CONF_USE_POWER_FEATURE: False,
|
||||
CONF_USE_PRESENCE_FEATURE: False,
|
||||
CONF_WINDOW_DELAY: 30, # the default value is added
|
||||
CONF_USE_POWER_FEATURE: False,
|
||||
CONF_USE_PRESENCE_FEATURE: False,
|
||||
}
|
||||
| MOCK_TH_OVER_SWITCH_TYPE_CONFIG
|
||||
| MOCK_TH_OVER_SWITCH_TPI_CONFIG
|
||||
| MOCK_PRESETS_CONFIG
|
||||
| MOCK_WINDOW_AUTO_CONFIG
|
||||
| MOCK_ADVANCED_CONFIG
|
||||
)
|
||||
assert result["result"]
|
||||
assert result["result"].domain == DOMAIN
|
||||
assert result["result"].version == 1
|
||||
assert result["result"].title == "TheOverSwitchMockName"
|
||||
assert isinstance(result["result"], ConfigEntry)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_user_config_flow_window_auto_ko(
|
||||
hass: HomeAssistant, skip_hass_states_get
|
||||
):
|
||||
"""Test the config flow with window auto and window features -> not allowed"""
|
||||
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_NAME: "TheOverSwitchMockName",
|
||||
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_SWITCH,
|
||||
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
|
||||
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
|
||||
CONF_CYCLE_MIN: 5,
|
||||
CONF_TEMP_MIN: 15,
|
||||
CONF_TEMP_MAX: 30,
|
||||
CONF_DEVICE_POWER: 1,
|
||||
CONF_USE_WINDOW_FEATURE: True,
|
||||
CONF_USE_MOTION_FEATURE: False,
|
||||
CONF_USE_POWER_FEATURE: False,
|
||||
CONF_USE_PRESENCE_FEATURE: False,
|
||||
},
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["step_id"] == "type"
|
||||
assert result["errors"] == {}
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input=MOCK_TH_OVER_SWITCH_TYPE_CONFIG
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["step_id"] == "tpi"
|
||||
assert result["errors"] == {}
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input=MOCK_TH_OVER_SWITCH_TPI_CONFIG
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["step_id"] == "presets"
|
||||
assert result["errors"] == {}
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input=MOCK_PRESETS_CONFIG
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["step_id"] == "window"
|
||||
assert result["errors"] == {}
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input=MOCK_WINDOW_AUTO_CONFIG | MOCK_WINDOW_CONFIG,
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
# We should stay on window with an error
|
||||
assert result["step_id"] == "window"
|
||||
assert result["errors"] == {
|
||||
"window_sensor_entity_id": "window_open_detection_method"
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_user_config_flow_over_4_switches(
|
||||
hass: HomeAssistant, skip_hass_states_get, skip_control_heating
|
||||
):
|
||||
"""Test the config flow with 4 switchs thermostat_over_switch features"""
|
||||
|
||||
SOURCE_CONFIG = { # pylint: disable=wildcard-import, invalid-name
|
||||
CONF_NAME: "TheOver4SwitchMockName",
|
||||
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_SWITCH,
|
||||
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
|
||||
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
|
||||
CONF_CYCLE_MIN: 5,
|
||||
CONF_TEMP_MIN: 15,
|
||||
CONF_TEMP_MAX: 30,
|
||||
CONF_DEVICE_POWER: 1,
|
||||
CONF_USE_WINDOW_FEATURE: False,
|
||||
CONF_USE_MOTION_FEATURE: False,
|
||||
CONF_USE_POWER_FEATURE: False,
|
||||
CONF_USE_PRESENCE_FEATURE: False,
|
||||
}
|
||||
|
||||
TYPE_CONFIG = { # pylint: disable=wildcard-import, invalid-name
|
||||
CONF_HEATER: "switch.mock_switch1",
|
||||
CONF_HEATER_2: "switch.mock_switch2",
|
||||
CONF_HEATER_3: "switch.mock_switch3",
|
||||
CONF_HEATER_4: "switch.mock_switch4",
|
||||
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
|
||||
}
|
||||
|
||||
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=SOURCE_CONFIG,
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["step_id"] == "type"
|
||||
assert result["errors"] == {}
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input=TYPE_CONFIG,
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["step_id"] == "tpi"
|
||||
assert result["errors"] == {}
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input=MOCK_TH_OVER_SWITCH_TPI_CONFIG
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["step_id"] == "presets"
|
||||
assert result["errors"] == {}
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input=MOCK_PRESETS_CONFIG
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["step_id"] == "advanced"
|
||||
assert result["errors"] == {}
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input=MOCK_ADVANCED_CONFIG
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
|
||||
assert (
|
||||
result["data"]
|
||||
== SOURCE_CONFIG
|
||||
| TYPE_CONFIG
|
||||
| MOCK_TH_OVER_SWITCH_TPI_CONFIG
|
||||
| MOCK_PRESETS_CONFIG
|
||||
| MOCK_ADVANCED_CONFIG
|
||||
)
|
||||
assert result["result"]
|
||||
assert result["result"].domain == DOMAIN
|
||||
assert result["result"].version == 1
|
||||
assert result["result"].title == "TheOver4SwitchMockName"
|
||||
assert isinstance(result["result"], ConfigEntry)
|
||||
407
custom_components/versatile_thermostat/tests/test_movement.py
Normal file
407
custom_components/versatile_thermostat/tests/test_movement.py
Normal file
@@ -0,0 +1,407 @@
|
||||
""" Test the Window management """
|
||||
import asyncio
|
||||
from unittest.mock import patch, call, PropertyMock
|
||||
from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
import logging
|
||||
|
||||
logging.getLogger().setLevel(logging.DEBUG)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_movement_management_time_not_enough(
|
||||
hass: HomeAssistant, skip_hass_states_is_state
|
||||
):
|
||||
"""Test the Presence management when time is not enough"""
|
||||
|
||||
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: True,
|
||||
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: 0, # important to not been obliged to wait
|
||||
CONF_MOTION_PRESET: "boost",
|
||||
CONF_NO_MOTION_PRESET: "comfort",
|
||||
CONF_PRESENCE_SENSOR: "binary_sensor.mock_presence_sensor",
|
||||
},
|
||||
)
|
||||
|
||||
entity: VersatileThermostat = await create_thermostat(
|
||||
hass, entry, "climate.theoverswitchmockname"
|
||||
)
|
||||
assert entity
|
||||
|
||||
tz = get_tz(hass) # pylint: disable=invalid-name
|
||||
now: datetime = datetime.now(tz=tz)
|
||||
|
||||
# start heating, in boost mode. We block the control_heating to avoid running a cycle
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.climate.VersatileThermostat._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
|
||||
assert entity.presence_state is None
|
||||
|
||||
event_timestamp = now - timedelta(minutes=4)
|
||||
await send_temperature_change_event(entity, 18, event_timestamp)
|
||||
await send_ext_temperature_change_event(entity, 10, event_timestamp)
|
||||
|
||||
await send_presence_change_event(entity, True, False, event_timestamp)
|
||||
assert entity.presence_state is "on"
|
||||
|
||||
# starts detecting motion
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
|
||||
) as mock_send_event, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
|
||||
) as mock_heater_on, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
|
||||
) as mock_heater_off, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
|
||||
return_value=True,
|
||||
), patch(
|
||||
"homeassistant.helpers.condition.state", return_value=False
|
||||
):
|
||||
event_timestamp = now - timedelta(minutes=3)
|
||||
await send_motion_change_event(entity, True, False, event_timestamp)
|
||||
|
||||
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
|
||||
assert entity.presence_state is "on"
|
||||
|
||||
assert mock_send_event.call_count == 0
|
||||
# Change is not confirmed
|
||||
assert mock_heater_on.call_count == 0
|
||||
assert mock_heater_off.call_count == 0
|
||||
assert mock_send_event.call_count == 0
|
||||
|
||||
# stop detecting motion with confirmation of stop
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
|
||||
) as mock_send_event, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
|
||||
) as mock_heater_on, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
|
||||
) as mock_heater_off, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
|
||||
return_value=True,
|
||||
) as mock_device_active, patch(
|
||||
"homeassistant.helpers.condition.state", return_value=True
|
||||
) as mock_condition:
|
||||
event_timestamp = now - timedelta(minutes=2)
|
||||
await send_motion_change_event(entity, False, True, event_timestamp)
|
||||
|
||||
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 mock_send_event.call_count == 0
|
||||
# Change is not confirmed
|
||||
assert mock_heater_on.call_count == 0
|
||||
# Because device is active
|
||||
assert mock_heater_off.call_count == 1
|
||||
assert mock_send_event.call_count == 0
|
||||
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
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"""
|
||||
|
||||
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: True,
|
||||
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: 0, # important to not been obliged to wait
|
||||
CONF_MOTION_PRESET: "boost",
|
||||
CONF_NO_MOTION_PRESET: "comfort",
|
||||
CONF_PRESENCE_SENSOR: "binary_sensor.mock_presence_sensor",
|
||||
},
|
||||
)
|
||||
|
||||
entity: VersatileThermostat = await create_thermostat(
|
||||
hass, entry, "climate.theoverswitchmockname"
|
||||
)
|
||||
assert entity
|
||||
|
||||
tz = get_tz(hass) # pylint: disable=invalid-name
|
||||
now: datetime = datetime.now(tz=tz)
|
||||
|
||||
# start heating, in boost mode. We block the control_heating to avoid running a cycle
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.climate.VersatileThermostat._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
|
||||
assert entity.presence_state is None
|
||||
|
||||
event_timestamp = now - timedelta(minutes=4)
|
||||
await send_temperature_change_event(entity, 18, event_timestamp)
|
||||
await send_ext_temperature_change_event(entity, 10, event_timestamp)
|
||||
|
||||
await send_presence_change_event(entity, True, False, event_timestamp)
|
||||
assert entity.presence_state is "on"
|
||||
|
||||
# starts detecting motion
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
|
||||
) as mock_send_event, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
|
||||
) as mock_heater_on, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
|
||||
) as mock_heater_off, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
|
||||
return_value=False,
|
||||
), patch(
|
||||
"homeassistant.helpers.condition.state", return_value=True
|
||||
):
|
||||
event_timestamp = now - timedelta(minutes=3)
|
||||
await send_motion_change_event(entity, True, False, event_timestamp)
|
||||
|
||||
assert entity.hvac_mode is HVACMode.HEAT
|
||||
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 mock_send_event.call_count == 0
|
||||
# Change is confirmed. Heater should be started
|
||||
assert mock_heater_on.call_count == 1
|
||||
assert mock_heater_off.call_count == 0
|
||||
assert mock_send_event.call_count == 0
|
||||
|
||||
# stop detecting motion with confirmation of stop
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
|
||||
) as mock_send_event, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
|
||||
) as mock_heater_on, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
|
||||
) as mock_heater_off, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
|
||||
return_value=True,
|
||||
), patch(
|
||||
"homeassistant.helpers.condition.state", return_value=True
|
||||
):
|
||||
event_timestamp = now - timedelta(minutes=2)
|
||||
await send_motion_change_event(entity, False, True, event_timestamp)
|
||||
|
||||
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 mock_send_event.call_count == 0
|
||||
assert mock_heater_on.call_count == 0
|
||||
# Because heating is no more necessary
|
||||
assert mock_heater_off.call_count == 1
|
||||
assert mock_send_event.call_count == 0
|
||||
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_movement_management_time_enoughand_not_presence(
|
||||
hass: HomeAssistant, skip_hass_states_is_state
|
||||
):
|
||||
"""Test the Presence management when time is not enough"""
|
||||
|
||||
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.1,
|
||||
"comfort_away_temp": 18.1,
|
||||
"boost_away_temp": 19.1,
|
||||
CONF_USE_WINDOW_FEATURE: False,
|
||||
CONF_USE_MOTION_FEATURE: True,
|
||||
CONF_USE_POWER_FEATURE: False,
|
||||
CONF_USE_PRESENCE_FEATURE: True,
|
||||
CONF_HEATER: "switch.mock_switch",
|
||||
CONF_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: 0, # important to not been obliged to wait
|
||||
CONF_MOTION_PRESET: "boost",
|
||||
CONF_NO_MOTION_PRESET: "comfort",
|
||||
CONF_PRESENCE_SENSOR: "binary_sensor.mock_presence_sensor",
|
||||
},
|
||||
)
|
||||
|
||||
entity: VersatileThermostat = await create_thermostat(
|
||||
hass, entry, "climate.theoverswitchmockname"
|
||||
)
|
||||
assert entity
|
||||
|
||||
tz = get_tz(hass) # pylint: disable=invalid-name
|
||||
now: datetime = datetime.now(tz=tz)
|
||||
|
||||
# start heating, in boost mode. We block the control_heating to avoid running a cycle
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.climate.VersatileThermostat._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 and presence is unknown
|
||||
assert entity.target_temperature == 18
|
||||
assert entity.motion_state is None
|
||||
assert entity.presence_state is None
|
||||
|
||||
event_timestamp = now - timedelta(minutes=4)
|
||||
await send_temperature_change_event(entity, 18, event_timestamp)
|
||||
await send_ext_temperature_change_event(entity, 10, event_timestamp)
|
||||
|
||||
await send_presence_change_event(entity, False, True, event_timestamp)
|
||||
assert entity.presence_state is "off"
|
||||
|
||||
# starts detecting motion
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
|
||||
) as mock_send_event, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
|
||||
) as mock_heater_on, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
|
||||
) as mock_heater_off, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
|
||||
return_value=False,
|
||||
), patch(
|
||||
"homeassistant.helpers.condition.state", return_value=True
|
||||
):
|
||||
event_timestamp = now - timedelta(minutes=3)
|
||||
await send_motion_change_event(entity, True, False, event_timestamp)
|
||||
|
||||
assert entity.hvac_mode is HVACMode.HEAT
|
||||
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 mock_send_event.call_count == 0
|
||||
# Change is confirmed. Heater should be started
|
||||
assert mock_heater_on.call_count == 1
|
||||
assert mock_heater_off.call_count == 0
|
||||
assert mock_send_event.call_count == 0
|
||||
|
||||
# stop detecting motion with confirmation of stop
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
|
||||
) as mock_send_event, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
|
||||
) as mock_heater_on, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
|
||||
) as mock_heater_off, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
|
||||
return_value=True,
|
||||
), patch(
|
||||
"homeassistant.helpers.condition.state", return_value=True
|
||||
):
|
||||
event_timestamp = now - timedelta(minutes=2)
|
||||
await send_motion_change_event(entity, False, True, event_timestamp)
|
||||
|
||||
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.1
|
||||
assert entity.motion_state is "off"
|
||||
assert entity.presence_state is "off"
|
||||
|
||||
assert mock_send_event.call_count == 0
|
||||
# 18.1 starts heating with a low on_percent
|
||||
assert mock_heater_on.call_count == 1
|
||||
assert entity.proportional_algorithm.on_percent == 0.11
|
||||
assert mock_heater_off.call_count == 0
|
||||
assert mock_send_event.call_count == 0
|
||||
@@ -0,0 +1,558 @@
|
||||
""" Test the Multiple switch management """
|
||||
import asyncio
|
||||
from unittest.mock import patch, call, ANY
|
||||
from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
import logging
|
||||
|
||||
logging.getLogger().setLevel(logging.DEBUG)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_one_switch_cycle(
|
||||
hass: HomeAssistant,
|
||||
skip_hass_states_is_state,
|
||||
skip_send_event,
|
||||
):
|
||||
"""Test that when multiple switch are configured the activation is distributed"""
|
||||
|
||||
tz = get_tz(hass) # pylint: disable=invalid-name
|
||||
now: datetime = datetime.now(tz=tz)
|
||||
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
title="TheOver4SwitchMockName",
|
||||
unique_id="uniqueId",
|
||||
data={
|
||||
CONF_NAME: "TheOver4SwitchMockName",
|
||||
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,
|
||||
"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_HEATER: "switch.mock_switch1",
|
||||
CONF_MINIMAL_ACTIVATION_DELAY: 30,
|
||||
CONF_SECURITY_DELAY_MIN: 5,
|
||||
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
|
||||
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
|
||||
CONF_TPI_COEF_INT: 0.3,
|
||||
CONF_TPI_COEF_EXT: 0.01,
|
||||
},
|
||||
)
|
||||
|
||||
entity: VersatileThermostat = await create_thermostat(
|
||||
hass, entry, "climate.theover4switchmockname"
|
||||
)
|
||||
assert entity
|
||||
assert entity.is_over_climate is False
|
||||
|
||||
# start heating, in boost mode. We block the control_heating to avoid running a cycle
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.climate.VersatileThermostat._async_control_heating"
|
||||
):
|
||||
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 None
|
||||
|
||||
event_timestamp = now - timedelta(minutes=4)
|
||||
await send_temperature_change_event(entity, 15, event_timestamp)
|
||||
|
||||
# Checks that all heaters are off
|
||||
with patch(
|
||||
"homeassistant.core.StateMachine.is_state", return_value=False
|
||||
) as mock_is_state:
|
||||
assert entity._is_device_active is False
|
||||
|
||||
# Should be call for the Switch
|
||||
assert mock_is_state.call_count == 1
|
||||
|
||||
# Set temperature to a low level
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
|
||||
) as mock_send_event, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
|
||||
) as mock_heater_on, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
|
||||
) as mock_heater_off, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
|
||||
return_value=False,
|
||||
) as mock_device_active, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.call_later",
|
||||
return_value=None,
|
||||
) as mock_call_later:
|
||||
await send_ext_temperature_change_event(entity, 5, event_timestamp)
|
||||
|
||||
# No special event
|
||||
assert mock_send_event.call_count == 0
|
||||
assert mock_heater_off.call_count == 0
|
||||
|
||||
# The first heater should be on but because call_later is mocked heater_on is not called
|
||||
# assert mock_heater_on.call_count == 1
|
||||
assert mock_heater_on.call_count == 0
|
||||
# There is no check if active
|
||||
assert mock_device_active.call_count == 0
|
||||
|
||||
# 4 calls dispatched along the cycle
|
||||
assert mock_call_later.call_count == 1
|
||||
mock_call_later.assert_has_calls(
|
||||
[
|
||||
call.call_later(hass, 0.0, ANY),
|
||||
]
|
||||
)
|
||||
|
||||
# Set a temperature at middle level
|
||||
event_timestamp = now - timedelta(minutes=4)
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
|
||||
) as mock_send_event, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
|
||||
) as mock_heater_on, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
|
||||
) as mock_heater_off, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
|
||||
return_value=False,
|
||||
) as mock_device_active:
|
||||
await send_temperature_change_event(entity, 18, event_timestamp)
|
||||
|
||||
# No special event
|
||||
assert mock_send_event.call_count == 0
|
||||
assert mock_heater_off.call_count == 0
|
||||
|
||||
# The first heater should be turned on but is already on but because above we mock call_later the heater is not on. But this time it will be really on
|
||||
assert mock_heater_on.call_count == 1
|
||||
|
||||
# Set another temperature at middle level
|
||||
event_timestamp = now - timedelta(minutes=3)
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
|
||||
) as mock_send_event, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
|
||||
) as mock_heater_on, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
|
||||
) as mock_heater_off, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
|
||||
return_value=True,
|
||||
) as mock_device_active:
|
||||
await send_temperature_change_event(entity, 18.1, event_timestamp)
|
||||
|
||||
# No special event
|
||||
assert mock_send_event.call_count == 0
|
||||
assert mock_heater_off.call_count == 0
|
||||
|
||||
# The heater is already on cycle. So we wait that the cycle ends and no heater action is done
|
||||
assert mock_heater_on.call_count == 0
|
||||
# assert entity.underlying_entity(0)._should_relaunch_control_heating is True
|
||||
|
||||
# Simulate the relaunch
|
||||
await entity.underlying_entity(0)._turn_on_later(None)
|
||||
# wait restart
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
assert mock_heater_on.call_count == 1
|
||||
# TODO normal ? assert entity.underlying_entity(0)._should_relaunch_control_heating is False
|
||||
|
||||
# Simulate the end of heater on cycle
|
||||
event_timestamp = now - timedelta(minutes=3)
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
|
||||
) as mock_send_event, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
|
||||
) as mock_heater_on, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
|
||||
) as mock_heater_off, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
|
||||
return_value=True,
|
||||
) as mock_device_active:
|
||||
await entity.underlying_entity(0)._turn_off_later(None)
|
||||
|
||||
# No special event
|
||||
assert mock_send_event.call_count == 0
|
||||
assert mock_heater_on.call_count == 0
|
||||
# The heater should be turned off this time
|
||||
assert mock_heater_off.call_count == 1
|
||||
# assert entity.underlying_entity(0)._should_relaunch_control_heating is False
|
||||
|
||||
# Simulate the start of heater on cycle
|
||||
event_timestamp = now - timedelta(minutes=3)
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
|
||||
) as mock_send_event, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
|
||||
) as mock_heater_on, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
|
||||
) as mock_heater_off, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
|
||||
return_value=True,
|
||||
) as mock_device_active:
|
||||
await entity.underlying_entity(0)._turn_on_later(None)
|
||||
|
||||
# No special event
|
||||
assert mock_send_event.call_count == 0
|
||||
assert mock_heater_on.call_count == 1
|
||||
# The heater should be turned off this time
|
||||
assert mock_heater_off.call_count == 0
|
||||
# assert entity.underlying_entity(0)._should_relaunch_control_heating is False
|
||||
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_multiple_switchs(
|
||||
hass: HomeAssistant,
|
||||
skip_hass_states_is_state,
|
||||
skip_send_event,
|
||||
):
|
||||
"""Test that when multiple switch are configured the activation is distributed"""
|
||||
|
||||
tz = get_tz(hass) # pylint: disable=invalid-name
|
||||
now: datetime = datetime.now(tz=tz)
|
||||
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
title="TheOver4SwitchMockName",
|
||||
unique_id="uniqueId",
|
||||
data={
|
||||
CONF_NAME: "TheOver4SwitchMockName",
|
||||
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,
|
||||
"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_HEATER: "switch.mock_switch1",
|
||||
CONF_HEATER_2: "switch.mock_switch2",
|
||||
CONF_HEATER_3: "switch.mock_switch3",
|
||||
CONF_HEATER_4: "switch.mock_switch4",
|
||||
CONF_MINIMAL_ACTIVATION_DELAY: 30,
|
||||
CONF_SECURITY_DELAY_MIN: 5,
|
||||
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
|
||||
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
|
||||
CONF_TPI_COEF_INT: 0.3,
|
||||
CONF_TPI_COEF_EXT: 0.01,
|
||||
},
|
||||
)
|
||||
|
||||
entity: VersatileThermostat = await create_thermostat(
|
||||
hass, entry, "climate.theover4switchmockname"
|
||||
)
|
||||
assert entity
|
||||
assert entity.is_over_climate is False
|
||||
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.climate.VersatileThermostat._async_control_heating"
|
||||
), patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.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 None
|
||||
|
||||
event_timestamp = now - timedelta(minutes=4)
|
||||
await send_temperature_change_event(entity, 15, event_timestamp)
|
||||
|
||||
# Checks that all climates are off
|
||||
assert entity._is_device_active is False
|
||||
|
||||
# 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),
|
||||
]
|
||||
)
|
||||
|
||||
# Set temperature to a low level
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
|
||||
) as mock_send_event, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
|
||||
) as mock_heater_on, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
|
||||
) as mock_heater_off, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
|
||||
return_value=False,
|
||||
) as mock_device_active, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.call_later",
|
||||
return_value=None,
|
||||
) as mock_call_later:
|
||||
await send_ext_temperature_change_event(entity, 5, event_timestamp)
|
||||
|
||||
# No special event
|
||||
assert mock_send_event.call_count == 0
|
||||
assert mock_heater_off.call_count == 0
|
||||
|
||||
# The first heater should be on but because call_later is mocked heater_on is not called
|
||||
# assert mock_heater_on.call_count == 1
|
||||
assert mock_heater_on.call_count == 0
|
||||
# There is no check if active
|
||||
assert mock_device_active.call_count == 0
|
||||
|
||||
# 4 calls dispatched along the cycle
|
||||
assert mock_call_later.call_count == 4
|
||||
mock_call_later.assert_has_calls(
|
||||
[
|
||||
call.call_later(hass, 0.0, ANY),
|
||||
call.call_later(hass, 120.0, ANY),
|
||||
call.call_later(hass, 240.0, ANY),
|
||||
call.call_later(hass, 360.0, ANY),
|
||||
]
|
||||
)
|
||||
|
||||
# Set a temperature at middle level
|
||||
event_timestamp = now - timedelta(minutes=4)
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
|
||||
) as mock_send_event, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
|
||||
) as mock_heater_on, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
|
||||
) as mock_heater_off, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
|
||||
return_value=False,
|
||||
) as mock_device_active:
|
||||
await send_temperature_change_event(entity, 18, event_timestamp)
|
||||
|
||||
# No special event
|
||||
assert mock_send_event.call_count == 0
|
||||
assert mock_heater_off.call_count == 0
|
||||
|
||||
# The first heater should be turned on but is already on but because call_later is mocked, it is only turned on here
|
||||
assert mock_heater_on.call_count == 1
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_multiple_climates(
|
||||
hass: HomeAssistant,
|
||||
skip_hass_states_is_state,
|
||||
skip_send_event,
|
||||
):
|
||||
"""Test that when multiple climates are configured the activation and deactivation is propagated to all climates"""
|
||||
|
||||
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_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: VersatileThermostat = 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.climate.VersatileThermostat._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 None
|
||||
|
||||
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),
|
||||
]
|
||||
)
|
||||
assert entity._is_device_active is False
|
||||
|
||||
# Stop heating, in boost mode. We block the control_heating to avoid running a cycle
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.climate.VersatileThermostat._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.OFF)
|
||||
|
||||
assert entity.hvac_mode is HVACMode.OFF
|
||||
assert entity.preset_mode is PRESET_BOOST
|
||||
assert entity.target_temperature == 19
|
||||
assert entity.window_state is None
|
||||
|
||||
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.OFF),
|
||||
]
|
||||
)
|
||||
assert entity._is_device_active is False
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_multiple_climates_underlying_changes(
|
||||
hass: HomeAssistant,
|
||||
skip_hass_states_is_state,
|
||||
skip_send_event,
|
||||
):
|
||||
"""Test that when multiple switch are configured the activation of one underlying climate activate the others"""
|
||||
|
||||
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_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: VersatileThermostat = 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.climate.VersatileThermostat._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 None
|
||||
|
||||
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),
|
||||
]
|
||||
)
|
||||
assert entity._is_device_active is False
|
||||
|
||||
# Stop heating on one underlying climate
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.climate.VersatileThermostat._async_control_heating"
|
||||
), patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.set_hvac_mode"
|
||||
) as mock_underlying_set_hvac_mode:
|
||||
await send_climate_change_event(entity, HVACMode.OFF, HVACMode.HEAT, HVACAction.OFF, HVACAction.HEATING, now)
|
||||
|
||||
# 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.OFF),
|
||||
]
|
||||
)
|
||||
assert entity.hvac_mode == HVACMode.OFF
|
||||
assert entity._is_device_active is False
|
||||
|
||||
# Start heating on one underlying climate
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.climate.VersatileThermostat._async_control_heating"
|
||||
), patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.set_hvac_mode"
|
||||
) as mock_underlying_set_hvac_mode, patch(
|
||||
# notice that there is no need of return_value=HVACAction.IDLE because this is not a function but a property
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.hvac_action", HVACAction.IDLE
|
||||
) as mock_underlying_get_hvac_action:
|
||||
await send_climate_change_event(entity, HVACMode.HEAT, HVACMode.OFF, HVACAction.IDLE, HVACAction.OFF, now)
|
||||
|
||||
# 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),
|
||||
]
|
||||
)
|
||||
assert entity.hvac_mode == HVACMode.HEAT
|
||||
assert entity.hvac_action == HVACAction.IDLE
|
||||
assert entity._is_device_active is False
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
""" Test the OpenWindow algorithm """
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import
|
||||
from ..open_window_algorithm import WindowOpenDetectionAlgorithm
|
||||
|
||||
|
||||
async def test_open_window_algo(
|
||||
hass: HomeAssistant,
|
||||
skip_hass_states_is_state,
|
||||
):
|
||||
"""Tests the Algo"""
|
||||
|
||||
the_algo = WindowOpenDetectionAlgorithm(1.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)
|
||||
last_slope = the_algo.add_temp_measurement(
|
||||
temperature=10, datetime_measure=event_timestamp
|
||||
)
|
||||
|
||||
# We need at least 2 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)
|
||||
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 True
|
||||
assert the_algo.is_window_open_detected() is False
|
||||
|
||||
event_timestamp = now - timedelta(minutes=3)
|
||||
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 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=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 the_algo.is_window_close_detected() is True
|
||||
assert the_algo.is_window_open_detected() is False
|
||||
|
||||
|
||||
async def test_open_window_algo_wrong(
|
||||
hass: HomeAssistant,
|
||||
skip_hass_states_is_state,
|
||||
):
|
||||
"""Tests the Algo with wrong date"""
|
||||
the_algo = WindowOpenDetectionAlgorithm(1.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)
|
||||
last_slope = the_algo.add_temp_measurement(
|
||||
temperature=10, datetime_measure=event_timestamp
|
||||
)
|
||||
|
||||
# We need at least 2 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
|
||||
|
||||
# The next datetime_measurement cannot be in the past
|
||||
event_timestamp = now - timedelta(minutes=6)
|
||||
last_slope = the_algo.add_temp_measurement(
|
||||
temperature=18, datetime_measure=event_timestamp
|
||||
)
|
||||
|
||||
# No slope because same temperature
|
||||
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
|
||||
455
custom_components/versatile_thermostat/tests/test_power.py
Normal file
455
custom_components/versatile_thermostat/tests/test_power.py
Normal file
@@ -0,0 +1,455 @@
|
||||
""" Test the Power management """
|
||||
from unittest.mock import patch, call
|
||||
from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from homeassistant.const import UnitOfTemperature
|
||||
|
||||
import logging
|
||||
|
||||
logging.getLogger().setLevel(logging.DEBUG)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_power_management_hvac_off(
|
||||
hass: HomeAssistant, skip_hass_states_is_state
|
||||
):
|
||||
"""Test the Power management"""
|
||||
|
||||
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,
|
||||
CONF_USE_WINDOW_FEATURE: False,
|
||||
CONF_USE_MOTION_FEATURE: False,
|
||||
CONF_USE_POWER_FEATURE: True,
|
||||
CONF_USE_PRESENCE_FEATURE: False,
|
||||
CONF_HEATER: "switch.mock_switch",
|
||||
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
|
||||
CONF_TPI_COEF_INT: 0.3,
|
||||
CONF_TPI_COEF_EXT: 0.01,
|
||||
CONF_MINIMAL_ACTIVATION_DELAY: 30,
|
||||
CONF_SECURITY_DELAY_MIN: 5,
|
||||
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
|
||||
CONF_POWER_SENSOR: "sensor.mock_power_sensor",
|
||||
CONF_MAX_POWER_SENSOR: "sensor.mock_power_max_sensor",
|
||||
CONF_DEVICE_POWER: 100,
|
||||
CONF_PRESET_POWER: 12,
|
||||
},
|
||||
)
|
||||
|
||||
entity: VersatileThermostat = await create_thermostat(
|
||||
hass, entry, "climate.theoverswitchmockname"
|
||||
)
|
||||
assert entity
|
||||
|
||||
tpi_algo = entity._prop_algorithm
|
||||
assert tpi_algo
|
||||
|
||||
await entity.async_set_preset_mode(PRESET_BOOST)
|
||||
assert entity.preset_mode is PRESET_BOOST
|
||||
assert entity.target_temperature == 19
|
||||
assert entity.overpowering_state is None
|
||||
assert entity.hvac_mode == HVACMode.OFF
|
||||
|
||||
# Send power mesurement
|
||||
await send_power_change_event(entity, 50, datetime.now())
|
||||
assert await entity.check_overpowering() is False
|
||||
|
||||
# All configuration is not complete
|
||||
assert entity.preset_mode is PRESET_BOOST
|
||||
assert entity.overpowering_state is None
|
||||
|
||||
# Send power max mesurement
|
||||
await send_max_power_change_event(entity, 300, datetime.now())
|
||||
assert await entity.check_overpowering() is False
|
||||
# All configuration is complete and power is < power_max
|
||||
assert entity.preset_mode is PRESET_BOOST
|
||||
assert entity.overpowering_state is False
|
||||
|
||||
# Send power max mesurement too low but HVACMode is off
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
|
||||
) as mock_send_event, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
|
||||
) as mock_heater_on, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
|
||||
) as mock_heater_off:
|
||||
await send_max_power_change_event(entity, 149, datetime.now())
|
||||
assert await entity.check_overpowering() is True
|
||||
# All configuration is complete and power is > power_max but we stay in Boost cause thermostat if Off
|
||||
assert entity.preset_mode is PRESET_BOOST
|
||||
assert entity.overpowering_state is True
|
||||
|
||||
assert mock_send_event.call_count == 0
|
||||
assert mock_heater_on.call_count == 0
|
||||
assert mock_heater_off.call_count == 0
|
||||
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_power_management_hvac_on(hass: HomeAssistant, skip_hass_states_is_state):
|
||||
"""Test the Power management"""
|
||||
|
||||
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,
|
||||
CONF_USE_WINDOW_FEATURE: False,
|
||||
CONF_USE_MOTION_FEATURE: False,
|
||||
CONF_USE_POWER_FEATURE: True,
|
||||
CONF_USE_PRESENCE_FEATURE: False,
|
||||
CONF_HEATER: "switch.mock_switch",
|
||||
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
|
||||
CONF_TPI_COEF_INT: 0.3,
|
||||
CONF_TPI_COEF_EXT: 0.01,
|
||||
CONF_MINIMAL_ACTIVATION_DELAY: 30,
|
||||
CONF_SECURITY_DELAY_MIN: 5,
|
||||
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
|
||||
CONF_POWER_SENSOR: "sensor.mock_power_sensor",
|
||||
CONF_MAX_POWER_SENSOR: "sensor.mock_power_max_sensor",
|
||||
CONF_DEVICE_POWER: 100,
|
||||
CONF_PRESET_POWER: 12,
|
||||
},
|
||||
)
|
||||
|
||||
entity: VersatileThermostat = await create_thermostat(
|
||||
hass, entry, "climate.theoverswitchmockname"
|
||||
)
|
||||
assert entity
|
||||
|
||||
tpi_algo = entity._prop_algorithm
|
||||
assert tpi_algo
|
||||
|
||||
await entity.async_set_hvac_mode(HVACMode.HEAT)
|
||||
await entity.async_set_preset_mode(PRESET_BOOST)
|
||||
assert entity.hvac_mode is HVACMode.HEAT
|
||||
assert entity.preset_mode is PRESET_BOOST
|
||||
assert entity.overpowering_state is None
|
||||
assert entity.target_temperature == 19
|
||||
|
||||
# Send power mesurement
|
||||
await send_power_change_event(entity, 50, datetime.now())
|
||||
# Send power max mesurement
|
||||
await send_max_power_change_event(entity, 300, datetime.now())
|
||||
assert await entity.check_overpowering() is False
|
||||
# All configuration is complete and power is < power_max
|
||||
assert entity.preset_mode is PRESET_BOOST
|
||||
assert entity.overpowering_state is False
|
||||
|
||||
# Send power max mesurement too low and HVACMode is on
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
|
||||
) as mock_send_event, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
|
||||
) as mock_heater_on, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
|
||||
) as mock_heater_off:
|
||||
await send_max_power_change_event(entity, 149, datetime.now())
|
||||
assert await entity.check_overpowering() is True
|
||||
# All configuration is complete and power is > power_max we switch to POWER preset
|
||||
assert entity.preset_mode is PRESET_POWER
|
||||
assert entity.overpowering_state is True
|
||||
assert entity.target_temperature == 12
|
||||
|
||||
assert mock_send_event.call_count == 2
|
||||
mock_send_event.assert_has_calls(
|
||||
[
|
||||
call.send_event(EventType.PRESET_EVENT, {"preset": PRESET_POWER}),
|
||||
call.send_event(
|
||||
EventType.POWER_EVENT,
|
||||
{
|
||||
"type": "start",
|
||||
"current_power": 50,
|
||||
"device_power": 100,
|
||||
"current_power_max": 149,
|
||||
},
|
||||
),
|
||||
],
|
||||
any_order=True,
|
||||
)
|
||||
assert mock_heater_on.call_count == 0
|
||||
assert mock_heater_off.call_count == 1
|
||||
|
||||
# Send power mesurement low to unseet power preset
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
|
||||
) as mock_send_event, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
|
||||
) as mock_heater_on, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
|
||||
) as mock_heater_off:
|
||||
await send_power_change_event(entity, 48, datetime.now())
|
||||
assert await entity.check_overpowering() is False
|
||||
# All configuration is complete and power is < power_max, we restore previous preset
|
||||
assert entity.preset_mode is PRESET_BOOST
|
||||
assert entity.overpowering_state is False
|
||||
assert entity.target_temperature == 19
|
||||
|
||||
assert mock_send_event.call_count == 2
|
||||
mock_send_event.assert_has_calls(
|
||||
[
|
||||
call.send_event(EventType.PRESET_EVENT, {"preset": PRESET_BOOST}),
|
||||
call.send_event(
|
||||
EventType.POWER_EVENT,
|
||||
{
|
||||
"type": "end",
|
||||
"current_power": 48,
|
||||
"device_power": 100,
|
||||
"current_power_max": 149,
|
||||
},
|
||||
),
|
||||
],
|
||||
any_order=True,
|
||||
)
|
||||
# No current temperature is set so the heater wont be turned on
|
||||
assert mock_heater_on.call_count == 0
|
||||
assert mock_heater_off.call_count == 0
|
||||
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_power_management_energy_over_switch(
|
||||
hass: HomeAssistant, skip_hass_states_is_state
|
||||
):
|
||||
"""Test the Power management energy mesurement"""
|
||||
|
||||
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,
|
||||
CONF_USE_WINDOW_FEATURE: False,
|
||||
CONF_USE_MOTION_FEATURE: False,
|
||||
CONF_USE_POWER_FEATURE: True,
|
||||
CONF_USE_PRESENCE_FEATURE: False,
|
||||
CONF_HEATER: "switch.mock_switch",
|
||||
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
|
||||
CONF_TPI_COEF_INT: 0.3,
|
||||
CONF_TPI_COEF_EXT: 0.01,
|
||||
CONF_MINIMAL_ACTIVATION_DELAY: 30,
|
||||
CONF_SECURITY_DELAY_MIN: 5,
|
||||
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
|
||||
CONF_POWER_SENSOR: "sensor.mock_power_sensor",
|
||||
CONF_MAX_POWER_SENSOR: "sensor.mock_power_max_sensor",
|
||||
CONF_DEVICE_POWER: 100,
|
||||
CONF_PRESET_POWER: 12,
|
||||
},
|
||||
)
|
||||
|
||||
entity: VersatileThermostat = await create_thermostat(
|
||||
hass, entry, "climate.theoverswitchmockname"
|
||||
)
|
||||
assert entity
|
||||
|
||||
tpi_algo = entity._prop_algorithm
|
||||
assert tpi_algo
|
||||
|
||||
assert entity.total_energy == 0
|
||||
|
||||
# set temperature to 15 so that on_percent will be set
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
|
||||
) as mock_send_event, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
|
||||
) as mock_heater_on, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
|
||||
) as mock_heater_off:
|
||||
await entity.async_set_hvac_mode(HVACMode.HEAT)
|
||||
await entity.async_set_preset_mode(PRESET_BOOST)
|
||||
await send_temperature_change_event(entity, 15, datetime.now())
|
||||
|
||||
assert entity.hvac_mode is HVACMode.HEAT
|
||||
assert entity.preset_mode is PRESET_BOOST
|
||||
assert entity.target_temperature == 19
|
||||
assert entity.current_temperature == 15
|
||||
assert tpi_algo.on_percent == 1
|
||||
|
||||
assert entity.mean_cycle_power == 100.0
|
||||
|
||||
assert mock_send_event.call_count == 2
|
||||
assert mock_heater_on.call_count == 1
|
||||
assert mock_heater_off.call_count == 0
|
||||
|
||||
entity.incremente_energy()
|
||||
assert entity.total_energy == 100 * 5 / 60.0
|
||||
entity.incremente_energy()
|
||||
assert entity.total_energy == 2 * 100 * 5 / 60.0
|
||||
|
||||
# change temperature to a higher value
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
|
||||
) as mock_send_event, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
|
||||
) as mock_heater_on, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
|
||||
) as mock_heater_off:
|
||||
await send_temperature_change_event(entity, 18, datetime.now())
|
||||
assert tpi_algo.on_percent == 0.3
|
||||
assert entity.mean_cycle_power == 30.0
|
||||
|
||||
assert mock_send_event.call_count == 0
|
||||
assert mock_heater_on.call_count == 0
|
||||
assert mock_heater_off.call_count == 0
|
||||
|
||||
entity.incremente_energy()
|
||||
assert round(entity.total_energy, 2) == round((2.0 + 0.3) * 100 * 5 / 60.0, 2)
|
||||
|
||||
entity.incremente_energy()
|
||||
assert round(entity.total_energy, 2) == round((2.0 + 0.6) * 100 * 5 / 60.0, 2)
|
||||
|
||||
# change temperature to a much higher value so that heater will be shut down
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
|
||||
) as mock_send_event, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
|
||||
) as mock_heater_on, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
|
||||
) as mock_heater_off:
|
||||
await send_temperature_change_event(entity, 20, datetime.now())
|
||||
assert tpi_algo.on_percent == 0.0
|
||||
assert entity.mean_cycle_power == 0.0
|
||||
|
||||
assert mock_send_event.call_count == 0
|
||||
assert mock_heater_on.call_count == 0
|
||||
assert mock_heater_off.call_count == 0
|
||||
|
||||
entity.incremente_energy()
|
||||
# No change on energy
|
||||
assert round(entity.total_energy, 2) == round((2.0 + 0.6) * 100 * 5 / 60.0, 2)
|
||||
|
||||
# Still no change
|
||||
entity.incremente_energy()
|
||||
assert round(entity.total_energy, 2) == round((2.0 + 0.6) * 100 * 5 / 60.0, 2)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_power_management_energy_over_climate(
|
||||
hass: HomeAssistant, skip_hass_states_is_state
|
||||
):
|
||||
"""Test the Power management for a over_climate thermostat"""
|
||||
|
||||
the_mock_underlying = MagicMockClimate()
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
|
||||
return_value=the_mock_underlying,
|
||||
):
|
||||
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: True,
|
||||
CONF_USE_PRESENCE_FEATURE: False,
|
||||
CONF_CLIMATE: "climate.mock_climate",
|
||||
CONF_MINIMAL_ACTIVATION_DELAY: 30,
|
||||
CONF_SECURITY_DELAY_MIN: 5,
|
||||
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
|
||||
CONF_POWER_SENSOR: "sensor.mock_power_sensor",
|
||||
CONF_MAX_POWER_SENSOR: "sensor.mock_power_max_sensor",
|
||||
CONF_DEVICE_POWER: 100,
|
||||
CONF_PRESET_POWER: 12,
|
||||
},
|
||||
)
|
||||
|
||||
entity: VersatileThermostat = await create_thermostat(
|
||||
hass, entry, "climate.theoverclimatemockname"
|
||||
)
|
||||
assert entity
|
||||
assert entity.is_over_climate
|
||||
|
||||
now = datetime.now(tz=get_tz(hass))
|
||||
await send_temperature_change_event(entity, 15, now)
|
||||
await entity.async_set_hvac_mode(HVACMode.HEAT)
|
||||
await entity.async_set_preset_mode(PRESET_BOOST)
|
||||
|
||||
assert entity.hvac_mode is HVACMode.HEAT
|
||||
assert entity.hvac_action is HVACAction.IDLE
|
||||
assert entity.preset_mode is PRESET_BOOST
|
||||
assert entity.target_temperature == 19
|
||||
assert entity.current_temperature == 15
|
||||
|
||||
# Not initialised yet
|
||||
assert entity.mean_cycle_power is None
|
||||
assert entity._underlying_climate_start_hvac_action_date is None
|
||||
|
||||
# Send a climate_change event with HVACAction=HEATING
|
||||
event_timestamp = now - timedelta(minutes=3)
|
||||
await send_climate_change_event(
|
||||
entity,
|
||||
new_hvac_mode=HVACMode.HEAT,
|
||||
old_hvac_mode=HVACMode.HEAT,
|
||||
new_hvac_action=HVACAction.HEATING,
|
||||
old_hvac_action=HVACAction.OFF,
|
||||
date=event_timestamp,
|
||||
)
|
||||
# We have the start event and not the end event
|
||||
assert (entity._underlying_climate_start_hvac_action_date - now).total_seconds() < 1
|
||||
|
||||
entity.incremente_energy()
|
||||
assert entity.total_energy == 0
|
||||
|
||||
# Send a climate_change event with HVACAction=IDLE (end of heating)
|
||||
await send_climate_change_event(
|
||||
entity,
|
||||
new_hvac_mode=HVACMode.HEAT,
|
||||
old_hvac_mode=HVACMode.HEAT,
|
||||
new_hvac_action=HVACAction.IDLE,
|
||||
old_hvac_action=HVACAction.HEATING,
|
||||
date=now,
|
||||
)
|
||||
# We have the end event -> we should have some power and on_percent
|
||||
assert entity._underlying_climate_start_hvac_action_date is None
|
||||
|
||||
# 3 minutes at 100 W
|
||||
assert entity.total_energy == 100 * 3.0 / 60
|
||||
|
||||
# Test the re-increment
|
||||
entity.incremente_energy()
|
||||
assert entity.total_energy == 2 * 100 * 3.0 / 60
|
||||
305
custom_components/versatile_thermostat/tests/test_security.py
Normal file
305
custom_components/versatile_thermostat/tests/test_security.py
Normal file
@@ -0,0 +1,305 @@
|
||||
""" Test the Security featrure """
|
||||
from unittest.mock import patch, call
|
||||
|
||||
from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import
|
||||
|
||||
from datetime import timedelta, datetime
|
||||
import logging
|
||||
|
||||
logging.getLogger().setLevel(logging.DEBUG)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_security_feature(hass: HomeAssistant, skip_hass_states_is_state):
|
||||
"""Test the security feature and https://github.com/jmcollin78/versatile_thermostat/issues/49:
|
||||
1. creates a thermostat and check that security is off
|
||||
2. activate security feature when date is expired
|
||||
3. change the preset to boost
|
||||
4. check that security is still on
|
||||
5. resolve the date issue
|
||||
6. check that security is off and preset is changed to boost
|
||||
"""
|
||||
|
||||
tz = get_tz(hass) # pylint: disable=invalid-name
|
||||
|
||||
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: VersatileThermostat = 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.preset_modes == [
|
||||
PRESET_NONE,
|
||||
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_mesure.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.climate.VersatileThermostat.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 entity._saved_preset_mode == PRESET_COMFORT
|
||||
assert entity._prop_algorithm.on_percent == 0.1
|
||||
assert entity._prop_algorithm.calculated_on_percent == 0.9
|
||||
|
||||
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_mesure": event_timestamp.isoformat(),
|
||||
"last_ext_temperature_mesure": entity._last_ext_temperature_mesure.isoformat(),
|
||||
"current_temp": 15,
|
||||
"current_ext_temp": None,
|
||||
"target_temp": 18,
|
||||
},
|
||||
),
|
||||
call.send_event(
|
||||
EventType.SECURITY_EVENT,
|
||||
{
|
||||
"type": "start",
|
||||
"last_temperature_mesure": event_timestamp.isoformat(),
|
||||
"last_ext_temperature_mesure": entity._last_ext_temperature_mesure.isoformat(),
|
||||
"current_temp": 15,
|
||||
"current_ext_temp": None,
|
||||
"target_temp": 18,
|
||||
},
|
||||
),
|
||||
],
|
||||
any_order=True,
|
||||
)
|
||||
|
||||
assert mock_heater_on.call_count == 1
|
||||
|
||||
# 3. Change the preset to Boost (we should stay in SECURITY)
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
|
||||
) as mock_send_event, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
|
||||
) as mock_heater_on:
|
||||
await entity.async_set_preset_mode(PRESET_BOOST)
|
||||
|
||||
# 4. check that security is still on
|
||||
assert entity._security_state is True
|
||||
assert entity._prop_algorithm.on_percent == 0.1
|
||||
assert entity._prop_algorithm.calculated_on_percent == 0.9
|
||||
assert entity._saved_preset_mode == PRESET_BOOST
|
||||
assert entity.preset_mode is PRESET_SECURITY
|
||||
|
||||
# 5. resolve the datetime issue
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
|
||||
) as mock_send_event, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
|
||||
) as mock_heater_on:
|
||||
event_timestamp = datetime.now()
|
||||
|
||||
# set temperature to 15 so that on_percent will be > security_min_on_percent (0.2)
|
||||
await send_temperature_change_event(entity, 15.2, event_timestamp)
|
||||
|
||||
assert entity._security_state is False
|
||||
assert entity.preset_mode == PRESET_BOOST
|
||||
assert entity._saved_preset_mode == PRESET_BOOST
|
||||
assert entity._prop_algorithm.on_percent == 1.0
|
||||
assert entity._prop_algorithm.calculated_on_percent == 1.0
|
||||
|
||||
assert mock_send_event.call_count == 2
|
||||
mock_send_event.assert_has_calls(
|
||||
[
|
||||
call.send_event(EventType.PRESET_EVENT, {"preset": PRESET_BOOST}),
|
||||
call.send_event(
|
||||
EventType.SECURITY_EVENT,
|
||||
{
|
||||
"type": "end",
|
||||
"last_temperature_mesure": event_timestamp.astimezone(
|
||||
tz
|
||||
).isoformat(),
|
||||
"last_ext_temperature_mesure": entity._last_ext_temperature_mesure.astimezone(
|
||||
tz
|
||||
).isoformat(),
|
||||
"current_temp": 15.2,
|
||||
"current_ext_temp": None,
|
||||
"target_temp": 19,
|
||||
},
|
||||
),
|
||||
],
|
||||
any_order=True,
|
||||
)
|
||||
|
||||
# Heater is now on
|
||||
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_over_climate(
|
||||
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 = MockClimate(hass, "mockUniqueId", "MockClimateName", {}, HVACMode.HEAT)
|
||||
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.climate.VersatileThermostat.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
|
||||
|
||||
# Because the underlying is HEATING. In real life the underlying will be shut-off
|
||||
assert entity.hvac_action is HVACAction.HEATING
|
||||
assert entity.hvac_mode is HVACMode.OFF
|
||||
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(
|
||||
[
|
||||
# At startup
|
||||
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
|
||||
|
||||
# One call more
|
||||
assert mock_send_event.call_count == 3
|
||||
mock_send_event.assert_has_calls(
|
||||
[
|
||||
call.send_event(
|
||||
EventType.HVAC_MODE_EVENT,
|
||||
{"hvac_mode": HVACMode.HEAT},
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
# 2. activate security feature when date is expired
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.climate.VersatileThermostat.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)
|
||||
|
||||
await send_temperature_change_event(entity, 15, event_timestamp)
|
||||
# Should stay False because a climate is never in security mode
|
||||
assert entity.security_state is False
|
||||
assert entity.preset_mode == 'none'
|
||||
assert entity._saved_preset_mode == 'none'
|
||||
@@ -1,5 +1,3 @@
|
||||
# pylint: disable=wildcard-import, unused-wildcard-import, protected-access, unused-argument, line-too-long
|
||||
|
||||
""" Test the normal start of a Thermostat """
|
||||
from datetime import timedelta, datetime
|
||||
|
||||
@@ -14,8 +12,8 @@ from homeassistant.const import UnitOfTime, UnitOfPower, UnitOfEnergy, PERCENTAG
|
||||
|
||||
from pytest_homeassistant_custom_component.common import MockConfigEntry
|
||||
|
||||
from custom_components.versatile_thermostat.base_thermostat import BaseThermostat
|
||||
from custom_components.versatile_thermostat.sensor import (
|
||||
from ..climate import VersatileThermostat
|
||||
from ..sensor import (
|
||||
EnergySensor,
|
||||
MeanPowerSensor,
|
||||
OnPercentSensor,
|
||||
@@ -62,14 +60,13 @@ async def test_sensors_over_switch(
|
||||
CONF_TPI_COEF_INT: 0.3,
|
||||
CONF_TPI_COEF_EXT: 0.01,
|
||||
CONF_MINIMAL_ACTIVATION_DELAY: 30,
|
||||
CONF_MINIMAL_DEACTIVATION_DELAY: 0,
|
||||
CONF_SAFETY_DELAY_MIN: 5,
|
||||
CONF_SAFETY_MIN_ON_PERCENT: 0.3,
|
||||
CONF_SECURITY_DELAY_MIN: 5,
|
||||
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
|
||||
CONF_DEVICE_POWER: 200,
|
||||
},
|
||||
)
|
||||
|
||||
entity: BaseThermostat = await create_thermostat(
|
||||
entity: VersatileThermostat = await create_thermostat(
|
||||
hass, entry, "climate.theoverswitchmockname"
|
||||
)
|
||||
assert entity
|
||||
@@ -141,7 +138,7 @@ async def test_sensors_over_switch(
|
||||
entity.incremente_energy()
|
||||
|
||||
await energy_sensor.async_my_climate_changed()
|
||||
assert energy_sensor.state == round(16.667, 2)
|
||||
assert energy_sensor.state == 16.667
|
||||
assert energy_sensor.device_class == SensorDeviceClass.ENERGY
|
||||
assert energy_sensor.state_class == SensorStateClass.TOTAL_INCREASING
|
||||
# because device_power is 200
|
||||
@@ -223,15 +220,16 @@ async def test_sensors_over_climate(
|
||||
CONF_USE_PRESENCE_FEATURE: False,
|
||||
CONF_CLIMATE: "climate.mock_climate",
|
||||
CONF_MINIMAL_ACTIVATION_DELAY: 30,
|
||||
CONF_MINIMAL_DEACTIVATION_DELAY: 0,
|
||||
CONF_SAFETY_DELAY_MIN: 5,
|
||||
CONF_SAFETY_MIN_ON_PERCENT: 0.3,
|
||||
CONF_SECURITY_DELAY_MIN: 5,
|
||||
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
|
||||
CONF_POWER_SENSOR: "sensor.mock_power_sensor",
|
||||
CONF_MAX_POWER_SENSOR: "sensor.mock_power_max_sensor",
|
||||
CONF_DEVICE_POWER: 1.5,
|
||||
CONF_PRESET_POWER: 12,
|
||||
},
|
||||
)
|
||||
|
||||
entity: BaseThermostat = await create_thermostat(
|
||||
entity: VersatileThermostat = await create_thermostat(
|
||||
hass, entry, "climate.theoverclimatemockname"
|
||||
)
|
||||
assert entity
|
||||
@@ -283,7 +281,6 @@ 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)
|
||||
@@ -294,7 +291,6 @@ 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
|
||||
@@ -360,13 +356,12 @@ async def test_sensors_over_climate_minimal(
|
||||
CONF_USE_PRESENCE_FEATURE: False,
|
||||
CONF_CLIMATE: "climate.mock_climate",
|
||||
CONF_MINIMAL_ACTIVATION_DELAY: 30,
|
||||
CONF_MINIMAL_DEACTIVATION_DELAY: 0,
|
||||
CONF_SAFETY_DELAY_MIN: 5,
|
||||
CONF_SAFETY_MIN_ON_PERCENT: 0.3,
|
||||
CONF_SECURITY_DELAY_MIN: 5,
|
||||
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
|
||||
},
|
||||
)
|
||||
|
||||
entity: BaseThermostat = await create_thermostat(
|
||||
entity: VersatileThermostat = await create_thermostat(
|
||||
hass, entry, "climate.theoverclimatemockname"
|
||||
)
|
||||
assert entity
|
||||
@@ -1,20 +1,16 @@
|
||||
# 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 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 ..climate import VersatileThermostat
|
||||
|
||||
from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import
|
||||
|
||||
@@ -32,33 +28,41 @@ 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"
|
||||
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
|
||||
) as mock_send_event:
|
||||
entity = await create_thermostat(hass, entry, "climate.theoverswitchmockname")
|
||||
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: VersatileThermostat = find_my_entity("climate.theoverswitchmockname")
|
||||
|
||||
assert entity
|
||||
assert isinstance(entity, ThermostatOverSwitch)
|
||||
|
||||
assert entity.name == "TheOverSwitchMockName"
|
||||
assert entity.is_over_climate is False
|
||||
assert entity._is_over_climate is False
|
||||
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,
|
||||
PRESET_ACTIVITY,
|
||||
]
|
||||
assert entity.preset_mode is PRESET_NONE
|
||||
assert entity.safety_manager.is_safety_detected is False
|
||||
assert entity.window_state is STATE_UNKNOWN
|
||||
assert entity.motion_state is STATE_UNKNOWN
|
||||
assert entity.presence_state is STATE_UNKNOWN
|
||||
assert entity._security_state is False
|
||||
assert entity._window_state is None
|
||||
assert entity._motion_state is None
|
||||
assert entity._presence_state is None
|
||||
assert entity._prop_algorithm is not None
|
||||
assert entity.have_valve_regulation is False
|
||||
|
||||
# should have been called with EventType.PRESET_EVENT and EventType.HVAC_MODE_EVENT
|
||||
assert mock_send_event.call_count == 2
|
||||
@@ -89,34 +93,42 @@ async def test_over_climate_full_start(hass: HomeAssistant, skip_hass_states_is_
|
||||
fake_underlying_climate = MockClimate(hass, "mockUniqueId", "MockClimateName", {})
|
||||
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
|
||||
"custom_components.versatile_thermostat.climate.VersatileThermostat.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")
|
||||
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)
|
||||
|
||||
assert entity.name == "TheOverClimateMockName"
|
||||
assert entity.is_over_climate is True
|
||||
assert entity._is_over_climate is True
|
||||
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,
|
||||
]
|
||||
assert entity.preset_mode is PRESET_NONE
|
||||
assert entity.safety_manager.is_safety_detected is False
|
||||
assert entity.window_state is STATE_UNAVAILABLE
|
||||
assert entity.motion_state is STATE_UNAVAILABLE
|
||||
assert entity.presence_state is STATE_UNAVAILABLE
|
||||
assert entity.have_valve_regulation is False
|
||||
assert entity._security_state is False
|
||||
assert entity._window_state is None
|
||||
assert entity._motion_state is None
|
||||
assert entity._presence_state is None
|
||||
|
||||
# should have been called with EventType.PRESET_EVENT and EventType.HVAC_MODE_EVENT
|
||||
assert mock_send_event.call_count == 2
|
||||
@@ -148,30 +160,40 @@ async def test_over_4switch_full_start(hass: HomeAssistant, skip_hass_states_is_
|
||||
)
|
||||
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
|
||||
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
|
||||
) as mock_send_event:
|
||||
entity = await create_thermostat(hass, entry, "climate.theover4switchmockname")
|
||||
entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(entry.entry_id)
|
||||
assert entry.state is ConfigEntryState.LOADED
|
||||
|
||||
def find_my_entity(entity_id) -> ClimateEntity:
|
||||
"""Find my new entity"""
|
||||
component: EntityComponent[ClimateEntity] = hass.data[CLIMATE_DOMAIN]
|
||||
for entity in component.entities:
|
||||
if entity.entity_id == entity_id:
|
||||
return entity
|
||||
|
||||
entity: VersatileThermostat = find_my_entity("climate.theover4switchmockname")
|
||||
|
||||
assert entity
|
||||
|
||||
assert entity.name == "TheOver4SwitchMockName"
|
||||
assert entity.is_over_switch
|
||||
assert entity._is_over_climate is False
|
||||
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,
|
||||
PRESET_ACTIVITY,
|
||||
]
|
||||
assert entity.preset_mode is PRESET_NONE
|
||||
assert entity.safety_manager.is_safety_detected is False
|
||||
assert entity.window_state is STATE_UNKNOWN
|
||||
assert entity.motion_state is STATE_UNKNOWN
|
||||
assert entity.presence_state is STATE_UNKNOWN
|
||||
assert entity._security_state is False
|
||||
assert entity._window_state is None
|
||||
assert entity._motion_state is None
|
||||
assert entity._presence_state is None
|
||||
assert entity._prop_algorithm is not None
|
||||
|
||||
assert entity.nb_underlying_entities == 4
|
||||
@@ -196,70 +218,3 @@ 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_SAFETY_DELAY_MIN: 10,
|
||||
CONF_MINIMAL_ACTIVATION_DELAY: 10,
|
||||
CONF_MINIMAL_DEACTIVATION_DELAY: 0,
|
||||
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
|
||||
85
custom_components/versatile_thermostat/tests/test_tpi.py
Normal file
85
custom_components/versatile_thermostat/tests/test_tpi.py
Normal file
@@ -0,0 +1,85 @@
|
||||
""" Test the TPI algorithm """
|
||||
|
||||
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_tpi_calculation(hass: HomeAssistant, skip_hass_states_is_state):
|
||||
"""Test the TPI calculation"""
|
||||
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
title="TheOverSwitchMockName",
|
||||
unique_id="uniqueId",
|
||||
data={
|
||||
CONF_NAME: "TheOverSwitchMockName",
|
||||
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_SWITCH,
|
||||
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
|
||||
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
|
||||
CONF_CYCLE_MIN: 5,
|
||||
CONF_TEMP_MIN: 15,
|
||||
CONF_TEMP_MAX: 30,
|
||||
CONF_USE_WINDOW_FEATURE: False,
|
||||
CONF_USE_MOTION_FEATURE: False,
|
||||
CONF_USE_POWER_FEATURE: 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_DEVICE_POWER: 100,
|
||||
},
|
||||
)
|
||||
|
||||
entity: VersatileThermostat = await create_thermostat(
|
||||
hass, entry, "climate.theoverswitchmockname"
|
||||
)
|
||||
assert entity
|
||||
|
||||
tpi_algo = entity._prop_algorithm
|
||||
assert tpi_algo
|
||||
|
||||
tpi_algo.calculate(15, 10, 7)
|
||||
assert tpi_algo.on_percent == 1
|
||||
assert tpi_algo.calculated_on_percent == 1
|
||||
assert tpi_algo.on_time_sec == 300
|
||||
assert tpi_algo.off_time_sec == 0
|
||||
assert entity.mean_cycle_power is None # no device power configured
|
||||
|
||||
tpi_algo.calculate(15, 14, 5)
|
||||
assert tpi_algo.on_percent == 0.4
|
||||
assert tpi_algo.calculated_on_percent == 0.4
|
||||
assert tpi_algo.on_time_sec == 120
|
||||
assert tpi_algo.off_time_sec == 180
|
||||
|
||||
tpi_algo.set_security(0.1)
|
||||
tpi_algo.calculate(15, 14, 5)
|
||||
assert tpi_algo.on_percent == 0.1
|
||||
assert tpi_algo.calculated_on_percent == 0.4
|
||||
assert tpi_algo.on_time_sec == 30 # >= minimal_activation_delay (=30)
|
||||
assert tpi_algo.off_time_sec == 270
|
||||
|
||||
tpi_algo.unset_security()
|
||||
tpi_algo.calculate(15, 14, 5)
|
||||
assert tpi_algo.on_percent == 0.4
|
||||
assert tpi_algo.calculated_on_percent == 0.4
|
||||
assert tpi_algo.on_time_sec == 120
|
||||
assert tpi_algo.off_time_sec == 180
|
||||
|
||||
# Test minimal activation delay
|
||||
tpi_algo.calculate(15, 14.7, 15)
|
||||
assert tpi_algo.on_percent == 0.09
|
||||
assert tpi_algo.calculated_on_percent == 0.09
|
||||
assert tpi_algo.on_time_sec == 0
|
||||
assert tpi_algo.off_time_sec == 300
|
||||
|
||||
tpi_algo.set_security(0.09)
|
||||
tpi_algo.calculate(15, 14.7, 15)
|
||||
assert tpi_algo.on_percent == 0.09
|
||||
assert tpi_algo.calculated_on_percent == 0.09
|
||||
assert tpi_algo.on_time_sec == 0
|
||||
assert tpi_algo.off_time_sec == 300
|
||||
673
custom_components/versatile_thermostat/tests/test_window.py
Normal file
673
custom_components/versatile_thermostat/tests/test_window.py
Normal file
@@ -0,0 +1,673 @@
|
||||
""" Test the Window management """
|
||||
import asyncio
|
||||
from unittest.mock import patch, call, PropertyMock
|
||||
from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
import logging
|
||||
|
||||
logging.getLogger().setLevel(logging.DEBUG)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_window_management_time_not_enough(
|
||||
hass: HomeAssistant, skip_hass_states_is_state
|
||||
):
|
||||
"""Test the Window management when time is not enough"""
|
||||
|
||||
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,
|
||||
CONF_USE_WINDOW_FEATURE: True,
|
||||
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_WINDOW_SENSOR: "binary_sensor.mock_window_sensor",
|
||||
CONF_WINDOW_DELAY: 0, # important to not been obliged to wait
|
||||
},
|
||||
)
|
||||
|
||||
entity: VersatileThermostat = await create_thermostat(
|
||||
hass, entry, "climate.theoverswitchmockname"
|
||||
)
|
||||
assert entity
|
||||
|
||||
tpi_algo = entity._prop_algorithm
|
||||
assert tpi_algo
|
||||
|
||||
await entity.async_set_hvac_mode(HVACMode.HEAT)
|
||||
await entity.async_set_preset_mode(PRESET_BOOST)
|
||||
assert entity.hvac_mode is HVACMode.HEAT
|
||||
assert entity.preset_mode is PRESET_BOOST
|
||||
assert entity.overpowering_state is None
|
||||
assert entity.target_temperature == 19
|
||||
|
||||
assert entity.window_state is None
|
||||
|
||||
# Open the window, but condition of time is not satisfied and check the thermostat don't turns off
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.climate.VersatileThermostat.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:
|
||||
await send_temperature_change_event(entity, 15, datetime.now())
|
||||
await send_window_change_event(entity, True, False, datetime.now())
|
||||
# simulate the call to try_window_condition. No need due to 0 WINDOW_DELAY and sleep after event is sent
|
||||
# await try_window_condition(None)
|
||||
|
||||
assert mock_send_event.call_count == 0
|
||||
assert mock_heater_on.call_count == 1
|
||||
assert mock_heater_off.call_count == 0
|
||||
assert mock_condition.call_count == 1
|
||||
|
||||
assert entity.window_state == STATE_OFF
|
||||
|
||||
# Close the window
|
||||
try_window_condition = await send_window_change_event(
|
||||
entity, False, False, datetime.now()
|
||||
)
|
||||
# simulate the call to try_window_condition
|
||||
await try_window_condition(None)
|
||||
assert entity.window_state == STATE_OFF
|
||||
|
||||
entity.remove_thermostat()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_window_management_time_enough(
|
||||
hass: HomeAssistant, skip_hass_states_is_state
|
||||
):
|
||||
"""Test the Window management when time is enough"""
|
||||
|
||||
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,
|
||||
CONF_USE_WINDOW_FEATURE: True,
|
||||
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_WINDOW_SENSOR: "binary_sensor.mock_window_sensor",
|
||||
CONF_WINDOW_DELAY: 0, # important to not been obliged to wait
|
||||
},
|
||||
)
|
||||
|
||||
entity: VersatileThermostat = await create_thermostat(
|
||||
hass, entry, "climate.theoverswitchmockname"
|
||||
)
|
||||
assert entity
|
||||
|
||||
tpi_algo = entity._prop_algorithm
|
||||
assert tpi_algo
|
||||
|
||||
await entity.async_set_hvac_mode(HVACMode.HEAT)
|
||||
await entity.async_set_preset_mode(PRESET_BOOST)
|
||||
assert entity.hvac_mode is HVACMode.HEAT
|
||||
assert entity.preset_mode is PRESET_BOOST
|
||||
assert entity.overpowering_state is None
|
||||
assert entity.target_temperature == 19
|
||||
|
||||
assert entity.window_state is None
|
||||
|
||||
# change temperature to force turning on the heater
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
|
||||
) as mock_send_event, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
|
||||
) as mock_heater_on, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
|
||||
) as mock_heater_off, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
|
||||
return_value=False,
|
||||
):
|
||||
await send_temperature_change_event(entity, 15, datetime.now())
|
||||
|
||||
# Heater shoud turn-on
|
||||
assert mock_heater_on.call_count >= 1
|
||||
assert mock_heater_off.call_count == 0
|
||||
assert mock_send_event.call_count == 0
|
||||
|
||||
# Open the window, condition of time is satisfied, check the thermostat and heater turns off
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.climate.VersatileThermostat.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=True,
|
||||
):
|
||||
await send_window_change_event(entity, True, False, datetime.now())
|
||||
|
||||
assert mock_send_event.call_count == 1
|
||||
mock_send_event.assert_has_calls(
|
||||
[call.send_event(EventType.HVAC_MODE_EVENT, {"hvac_mode": HVACMode.OFF})]
|
||||
)
|
||||
|
||||
# Heater should not be on
|
||||
assert mock_heater_on.call_count == 0
|
||||
# One call in set_hvac_mode turn_off and one call in the control_heating for security
|
||||
assert mock_heater_off.call_count == 2
|
||||
assert mock_condition.call_count == 1
|
||||
assert entity.hvac_mode is HVACMode.OFF
|
||||
assert entity.window_state == STATE_ON
|
||||
|
||||
# Close the window
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.climate.VersatileThermostat.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_function = await send_window_change_event(
|
||||
entity, False, True, datetime.now(), sleep=False
|
||||
)
|
||||
|
||||
await try_function(None)
|
||||
|
||||
# Wait for initial delay of heater
|
||||
await asyncio.sleep(0.3)
|
||||
|
||||
assert entity.window_state == STATE_OFF
|
||||
assert mock_heater_on.call_count == 1
|
||||
assert mock_send_event.call_count == 1
|
||||
mock_send_event.assert_has_calls(
|
||||
[
|
||||
call.send_event(
|
||||
EventType.HVAC_MODE_EVENT, {"hvac_mode": HVACMode.HEAT}
|
||||
),
|
||||
],
|
||||
any_order=False,
|
||||
)
|
||||
assert entity.preset_mode is PRESET_BOOST
|
||||
|
||||
# Clean the entity
|
||||
entity.remove_thermostat()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_window_auto_fast(hass: HomeAssistant, skip_hass_states_is_state):
|
||||
"""Test the Power management"""
|
||||
|
||||
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": 21,
|
||||
CONF_USE_WINDOW_FEATURE: True,
|
||||
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_WINDOW_AUTO_OPEN_THRESHOLD: 0.1,
|
||||
CONF_WINDOW_AUTO_CLOSE_THRESHOLD: 0.1,
|
||||
CONF_WINDOW_AUTO_MAX_DURATION: 10, # Should be 0 for test
|
||||
},
|
||||
)
|
||||
|
||||
entity: VersatileThermostat = await create_thermostat(
|
||||
hass, entry, "climate.theoverswitchmockname"
|
||||
)
|
||||
assert entity
|
||||
|
||||
tz = get_tz(hass) # pylint: disable=invalid-name
|
||||
now = datetime.now(tz)
|
||||
|
||||
tpi_algo = entity._prop_algorithm
|
||||
assert tpi_algo
|
||||
|
||||
await entity.async_set_hvac_mode(HVACMode.HEAT)
|
||||
await entity.async_set_preset_mode(PRESET_BOOST)
|
||||
assert entity.hvac_mode is HVACMode.HEAT
|
||||
assert entity.preset_mode is PRESET_BOOST
|
||||
assert entity.overpowering_state is None
|
||||
assert entity.target_temperature == 21
|
||||
|
||||
assert entity.window_state is None
|
||||
|
||||
# Make the temperature down
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
|
||||
) as mock_send_event, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
|
||||
) as mock_heater_on, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
|
||||
) as mock_heater_off, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
|
||||
return_value=True,
|
||||
):
|
||||
event_timestamp = now - timedelta(minutes=4)
|
||||
await send_temperature_change_event(entity, 19, event_timestamp)
|
||||
|
||||
# The heater turns on
|
||||
assert mock_send_event.call_count == 0
|
||||
assert mock_heater_on.call_count == 1
|
||||
assert entity.last_temperature_slope is None
|
||||
assert entity._window_auto_algo.is_window_open_detected() is False
|
||||
assert entity._window_auto_algo.is_window_close_detected() is False
|
||||
assert entity.hvac_mode is HVACMode.HEAT
|
||||
|
||||
# send one degre down in one minute
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
|
||||
) as mock_send_event, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
|
||||
) as mock_heater_on, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
|
||||
) as mock_heater_off, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
|
||||
return_value=True,
|
||||
):
|
||||
event_timestamp = now - timedelta(minutes=3)
|
||||
await send_temperature_change_event(entity, 18, event_timestamp)
|
||||
|
||||
# The heater turns on
|
||||
assert mock_send_event.call_count == 2
|
||||
assert mock_heater_on.call_count == 0
|
||||
assert mock_heater_off.call_count >= 1
|
||||
assert entity.last_temperature_slope == -1
|
||||
assert entity._window_auto_algo.is_window_open_detected() is True
|
||||
assert entity._window_auto_algo.is_window_close_detected() is False
|
||||
assert entity.window_auto_state == STATE_ON
|
||||
assert entity.hvac_mode is HVACMode.OFF
|
||||
|
||||
mock_send_event.assert_has_calls(
|
||||
[
|
||||
call.send_event(EventType.HVAC_MODE_EVENT, {"hvac_mode": HVACMode.OFF}),
|
||||
call.send_event(
|
||||
EventType.WINDOW_AUTO_EVENT,
|
||||
{"type": "start", "cause": "slope alert", "curve_slope": -1.0},
|
||||
),
|
||||
],
|
||||
any_order=True,
|
||||
)
|
||||
|
||||
# send another 0.1 degre in one minute -> no change
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
|
||||
) as mock_send_event, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
|
||||
) as mock_heater_on, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
|
||||
) as mock_heater_off, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
|
||||
new_callable=PropertyMock,
|
||||
return_value=False,
|
||||
):
|
||||
event_timestamp = now - timedelta(minutes=2)
|
||||
await send_temperature_change_event(entity, 17.9, event_timestamp)
|
||||
|
||||
# The heater turns on
|
||||
assert mock_send_event.call_count == 0
|
||||
assert mock_heater_on.call_count == 0
|
||||
assert mock_heater_off.call_count == 0
|
||||
assert round(entity.last_temperature_slope, 3) == -0.1 * 0.5 - 1 * 0.5
|
||||
assert entity._window_auto_algo.is_window_open_detected() is True
|
||||
assert entity._window_auto_algo.is_window_close_detected() is False
|
||||
assert entity.window_auto_state == STATE_ON
|
||||
assert entity.hvac_mode is HVACMode.OFF
|
||||
|
||||
# send another plus 1.1 degre in one minute -> restore state
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
|
||||
) as mock_send_event, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
|
||||
) as mock_heater_on, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
|
||||
) as mock_heater_off, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
|
||||
new_callable=PropertyMock,
|
||||
return_value=False,
|
||||
):
|
||||
event_timestamp = now - timedelta(minutes=1)
|
||||
await send_temperature_change_event(entity, 19, event_timestamp)
|
||||
|
||||
# The heater turns on
|
||||
assert mock_send_event.call_count == 2
|
||||
mock_send_event.assert_has_calls(
|
||||
[
|
||||
call.send_event(
|
||||
EventType.HVAC_MODE_EVENT, {"hvac_mode": HVACMode.HEAT}
|
||||
),
|
||||
call.send_event(
|
||||
EventType.WINDOW_AUTO_EVENT,
|
||||
{
|
||||
"type": "end",
|
||||
"cause": "end of slope alert",
|
||||
"curve_slope": 0.27500000000000036,
|
||||
},
|
||||
),
|
||||
],
|
||||
any_order=True,
|
||||
)
|
||||
assert mock_heater_on.call_count == 1
|
||||
assert mock_heater_off.call_count == 0
|
||||
assert round(entity.last_temperature_slope, 3) == 0.275
|
||||
assert entity._window_auto_algo.is_window_open_detected() is False
|
||||
assert entity._window_auto_algo.is_window_close_detected() is True
|
||||
assert entity.window_auto_state == STATE_OFF
|
||||
assert entity.hvac_mode is HVACMode.HEAT
|
||||
|
||||
# Clean the entity
|
||||
entity.remove_thermostat()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_window_auto_auto_stop(hass: HomeAssistant, skip_hass_states_is_state):
|
||||
"""Test the Power management"""
|
||||
|
||||
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": 21,
|
||||
CONF_USE_WINDOW_FEATURE: True,
|
||||
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_WINDOW_AUTO_OPEN_THRESHOLD: 0.1,
|
||||
CONF_WINDOW_AUTO_CLOSE_THRESHOLD: 0.1,
|
||||
CONF_WINDOW_AUTO_MAX_DURATION: 0, # Should be 0 for test
|
||||
},
|
||||
)
|
||||
|
||||
entity: VersatileThermostat = await create_thermostat(
|
||||
hass, entry, "climate.theoverswitchmockname"
|
||||
)
|
||||
assert entity
|
||||
|
||||
tz = get_tz(hass) # pylint: disable=invalid-name
|
||||
now = datetime.now(tz)
|
||||
|
||||
tpi_algo = entity._prop_algorithm
|
||||
assert tpi_algo
|
||||
|
||||
await entity.async_set_hvac_mode(HVACMode.HEAT)
|
||||
await entity.async_set_preset_mode(PRESET_BOOST)
|
||||
assert entity.hvac_mode is HVACMode.HEAT
|
||||
assert entity.preset_mode is PRESET_BOOST
|
||||
assert entity.overpowering_state is None
|
||||
assert entity.target_temperature == 21
|
||||
|
||||
assert entity.window_state is None
|
||||
|
||||
# Make the temperature down
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
|
||||
) as mock_send_event, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
|
||||
) as mock_heater_on, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
|
||||
) as mock_heater_off, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
|
||||
return_value=True,
|
||||
):
|
||||
event_timestamp = now - timedelta(minutes=4)
|
||||
await send_temperature_change_event(entity, 19, event_timestamp)
|
||||
|
||||
# The heater turns on
|
||||
assert mock_heater_on.call_count == 1
|
||||
assert entity.last_temperature_slope is None
|
||||
assert entity._window_auto_algo.is_window_open_detected() is False
|
||||
assert entity._window_auto_algo.is_window_close_detected() is False
|
||||
assert entity.hvac_mode is HVACMode.HEAT
|
||||
|
||||
# send one degre down in one minute
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
|
||||
) as mock_send_event, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
|
||||
) as mock_heater_on, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
|
||||
) as mock_heater_off, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
|
||||
return_value=True,
|
||||
):
|
||||
event_timestamp = now - timedelta(minutes=3)
|
||||
await send_temperature_change_event(entity, 18, event_timestamp, sleep=False)
|
||||
|
||||
assert mock_send_event.call_count == 2
|
||||
# The heater turns off
|
||||
mock_send_event.assert_has_calls(
|
||||
[
|
||||
call.send_event(EventType.HVAC_MODE_EVENT, {"hvac_mode": HVACMode.OFF}),
|
||||
call.send_event(
|
||||
EventType.WINDOW_AUTO_EVENT,
|
||||
{
|
||||
"type": "start",
|
||||
"cause": "slope alert",
|
||||
"curve_slope": -1.0,
|
||||
},
|
||||
),
|
||||
],
|
||||
any_order=True,
|
||||
)
|
||||
assert mock_heater_on.call_count == 0
|
||||
assert mock_heater_off.call_count >= 1
|
||||
assert entity.last_temperature_slope == -1
|
||||
assert entity._window_auto_algo.is_window_open_detected() is True
|
||||
assert entity._window_auto_algo.is_window_close_detected() is False
|
||||
assert entity.window_auto_state == STATE_ON
|
||||
assert entity.hvac_mode is HVACMode.OFF
|
||||
|
||||
# Waits for automatic disable
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
|
||||
) as mock_send_event, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
|
||||
) as mock_heater_on, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
|
||||
) as mock_heater_off, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
|
||||
return_value=False,
|
||||
):
|
||||
await asyncio.sleep(0.3)
|
||||
|
||||
assert mock_heater_on.call_count == 1
|
||||
assert mock_heater_off.call_count == 0
|
||||
assert round(entity.last_temperature_slope, 3) == -1
|
||||
# Because the algorithm is not aware of the expiration, for the algo we are still in alert
|
||||
assert entity._window_auto_algo.is_window_open_detected() is True
|
||||
assert entity._window_auto_algo.is_window_close_detected() is False
|
||||
assert entity.window_auto_state == STATE_OFF
|
||||
assert entity.hvac_mode is HVACMode.HEAT
|
||||
assert entity.preset_mode is PRESET_BOOST
|
||||
|
||||
# Clean the entity
|
||||
entity.remove_thermostat()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_window_auto_no_on_percent(
|
||||
hass: HomeAssistant, skip_hass_states_is_state
|
||||
):
|
||||
"""Test the Power management"""
|
||||
|
||||
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": 21,
|
||||
CONF_USE_WINDOW_FEATURE: True,
|
||||
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_WINDOW_AUTO_OPEN_THRESHOLD: 0.1,
|
||||
CONF_WINDOW_AUTO_CLOSE_THRESHOLD: 0.1,
|
||||
CONF_WINDOW_AUTO_MAX_DURATION: 0, # Should be 0 for test
|
||||
},
|
||||
)
|
||||
|
||||
entity: VersatileThermostat = await create_thermostat(
|
||||
hass, entry, "climate.theoverswitchmockname"
|
||||
)
|
||||
assert entity
|
||||
|
||||
tz = get_tz(hass) # pylint: disable=invalid-name
|
||||
now = datetime.now(tz)
|
||||
|
||||
tpi_algo = entity._prop_algorithm
|
||||
assert tpi_algo
|
||||
|
||||
await entity.async_set_hvac_mode(HVACMode.HEAT)
|
||||
await entity.async_set_preset_mode(PRESET_BOOST)
|
||||
assert entity.hvac_mode is HVACMode.HEAT
|
||||
assert entity.preset_mode is PRESET_BOOST
|
||||
assert entity.overpowering_state is None
|
||||
assert entity.target_temperature == 21
|
||||
|
||||
assert entity.window_state is None
|
||||
|
||||
# Make the temperature down
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
|
||||
) as mock_send_event, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
|
||||
) as mock_heater_on, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
|
||||
) as mock_heater_off, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
|
||||
return_value=True,
|
||||
):
|
||||
event_timestamp = now - timedelta(minutes=4)
|
||||
await send_temperature_change_event(entity, 21.5, event_timestamp)
|
||||
|
||||
# The heater turns on
|
||||
assert mock_heater_on.call_count == 0
|
||||
assert entity.last_temperature_slope is None
|
||||
assert entity._window_auto_algo.is_window_open_detected() is False
|
||||
assert entity._window_auto_algo.is_window_close_detected() is False
|
||||
assert entity.hvac_mode is HVACMode.HEAT
|
||||
assert entity.proportional_algorithm.on_percent == 0.0
|
||||
|
||||
# send one degre down in one minute
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
|
||||
) as mock_send_event, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
|
||||
) as mock_heater_on, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
|
||||
) as mock_heater_off, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
|
||||
return_value=True,
|
||||
):
|
||||
event_timestamp = now - timedelta(minutes=3)
|
||||
await send_temperature_change_event(entity, 20, event_timestamp)
|
||||
|
||||
# The heater turns on but no alert because the heater was not heating
|
||||
assert mock_send_event.call_count == 0
|
||||
assert mock_heater_on.call_count == 1
|
||||
assert mock_heater_off.call_count == 0
|
||||
assert entity.last_temperature_slope == -1.5
|
||||
assert entity._window_auto_algo.is_window_open_detected() is True
|
||||
assert entity._window_auto_algo.is_window_close_detected() is False
|
||||
assert entity.window_auto_state == STATE_OFF
|
||||
assert entity.hvac_mode is HVACMode.HEAT
|
||||
|
||||
# Clean the entity
|
||||
entity.remove_thermostat()
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,312 +0,0 @@
|
||||
# pylint: disable=line-too-long, too-many-lines, abstract-method
|
||||
""" A climate with a direct valve regulation class """
|
||||
|
||||
import logging
|
||||
from datetime import datetime
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.components.climate import HVACMode, HVACAction
|
||||
|
||||
from .underlyings import UnderlyingValveRegulation
|
||||
|
||||
# from .commons import NowClass, round_to_nearest
|
||||
from .base_thermostat import ConfigData
|
||||
from .thermostat_climate import ThermostatOverClimate
|
||||
from .prop_algorithm import PropAlgorithm
|
||||
|
||||
from .const import * # pylint: disable=wildcard-import, unused-wildcard-import
|
||||
|
||||
# from .vtherm_api import VersatileThermostatAPI
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ThermostatOverClimateValve(ThermostatOverClimate):
|
||||
"""This class represent a VTherm over a climate with a direct valve regulation"""
|
||||
|
||||
_entity_component_unrecorded_attributes = ThermostatOverClimate._entity_component_unrecorded_attributes.union( # pylint: disable=protected-access
|
||||
frozenset(
|
||||
{
|
||||
"is_over_climate",
|
||||
"have_valve_regulation",
|
||||
"underlying_entities",
|
||||
"on_time_sec",
|
||||
"off_time_sec",
|
||||
"cycle_min",
|
||||
"function",
|
||||
"tpi_coef_int",
|
||||
"tpi_coef_ext",
|
||||
"power_percent",
|
||||
"min_opening_degrees",
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self, hass: HomeAssistant, unique_id: str, name: str, entry_infos: ConfigData
|
||||
):
|
||||
"""Initialize the ThermostatOverClimateValve class"""
|
||||
_LOGGER.debug("%s - creating a ThermostatOverClimateValve VTherm", name)
|
||||
self._underlyings_valve_regulation: list[UnderlyingValveRegulation] = []
|
||||
self._valve_open_percent: int | None = None
|
||||
self._last_calculation_timestamp: datetime | None = None
|
||||
self._auto_regulation_dpercent: float | None = None
|
||||
self._auto_regulation_period_min: int | None = None
|
||||
self._min_opening_degress: list[int] = []
|
||||
|
||||
super().__init__(hass, unique_id, name, entry_infos)
|
||||
|
||||
@overrides
|
||||
def post_init(self, config_entry: ConfigData):
|
||||
"""Initialize the Thermostat and underlyings
|
||||
Beware that the underlyings list contains the climate which represent the TRV
|
||||
but also the UnderlyingValveRegulation which reprensent the valve"""
|
||||
|
||||
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
|
||||
)
|
||||
|
||||
# Initialization of the TPI algo
|
||||
self._prop_algorithm = PropAlgorithm(
|
||||
self._proportional_function,
|
||||
self._tpi_coef_int,
|
||||
self._tpi_coef_ext,
|
||||
self._cycle_min,
|
||||
self._minimal_activation_delay,
|
||||
self._minimal_deactivation_delay,
|
||||
self.name,
|
||||
)
|
||||
|
||||
offset_list = config_entry.get(CONF_OFFSET_CALIBRATION_LIST, [])
|
||||
opening_list = config_entry.get(CONF_OPENING_DEGREE_LIST)
|
||||
closing_list = config_entry.get(CONF_CLOSING_DEGREE_LIST, [])
|
||||
|
||||
self._min_opening_degrees = config_entry.get(CONF_MIN_OPENING_DEGREES, None)
|
||||
min_opening_degrees_list = []
|
||||
if self._min_opening_degrees:
|
||||
min_opening_degrees_list = [
|
||||
int(x.strip()) for x in self._min_opening_degrees.split(",")
|
||||
]
|
||||
|
||||
for idx, _ in enumerate(config_entry.get(CONF_UNDERLYING_LIST)):
|
||||
offset = offset_list[idx] if idx < len(offset_list) else None
|
||||
# number of opening should equal number of underlying
|
||||
opening = opening_list[idx]
|
||||
closing = closing_list[idx] if idx < len(closing_list) else None
|
||||
under = UnderlyingValveRegulation(
|
||||
hass=self._hass,
|
||||
thermostat=self,
|
||||
offset_calibration_entity_id=offset,
|
||||
opening_degree_entity_id=opening,
|
||||
closing_degree_entity_id=closing,
|
||||
climate_underlying=self._underlyings[idx],
|
||||
min_opening_degree=(
|
||||
min_opening_degrees_list[idx]
|
||||
if idx < len(min_opening_degrees_list)
|
||||
else 0
|
||||
),
|
||||
)
|
||||
self._underlyings_valve_regulation.append(under)
|
||||
|
||||
@overrides
|
||||
def update_custom_attributes(self):
|
||||
"""Custom attributes"""
|
||||
super().update_custom_attributes()
|
||||
|
||||
self._attr_extra_state_attributes["have_valve_regulation"] = (
|
||||
self.have_valve_regulation
|
||||
)
|
||||
|
||||
self._attr_extra_state_attributes["underlyings_valve_regulation"] = [
|
||||
underlying.valve_entity_ids
|
||||
for underlying in self._underlyings_valve_regulation
|
||||
]
|
||||
|
||||
self._attr_extra_state_attributes["on_percent"] = (
|
||||
self._prop_algorithm.on_percent
|
||||
)
|
||||
self._attr_extra_state_attributes["power_percent"] = self.power_percent
|
||||
self._attr_extra_state_attributes["on_time_sec"] = (
|
||||
self._prop_algorithm.on_time_sec
|
||||
)
|
||||
self._attr_extra_state_attributes["off_time_sec"] = (
|
||||
self._prop_algorithm.off_time_sec
|
||||
)
|
||||
self._attr_extra_state_attributes["cycle_min"] = self._cycle_min
|
||||
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["min_opening_degrees"] = (
|
||||
self._min_opening_degrees
|
||||
)
|
||||
|
||||
self._attr_extra_state_attributes["valve_open_percent"] = (
|
||||
self.valve_open_percent
|
||||
)
|
||||
|
||||
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(
|
||||
"%s - Calling update_custom_attributes: %s",
|
||||
self,
|
||||
self._attr_extra_state_attributes,
|
||||
)
|
||||
|
||||
@overrides
|
||||
def recalculate(self):
|
||||
"""A utility function to force the calculation of a the algo and
|
||||
update the custom attributes and write the state
|
||||
"""
|
||||
_LOGGER.debug("%s - recalculate the open percent", self)
|
||||
|
||||
# TODO this is exactly the same method as the thermostat_valve recalculate. Put that in common
|
||||
|
||||
# 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 or HVACMode.OFF,
|
||||
)
|
||||
|
||||
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 self._valve_open_percent is not None
|
||||
else 0
|
||||
)
|
||||
if (
|
||||
self._last_calculation_timestamp is not None
|
||||
and 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._last_calculation_timestamp is not None
|
||||
and 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
|
||||
|
||||
self._last_calculation_timestamp = now
|
||||
|
||||
super().recalculate()
|
||||
|
||||
async def _send_regulated_temperature(self, force=False):
|
||||
"""Sends the regulated temperature to all underlying"""
|
||||
if self.target_temperature is None:
|
||||
return
|
||||
|
||||
for under in self._underlyings:
|
||||
if self.target_temperature != under.last_sent_temperature:
|
||||
await under.set_temperature(
|
||||
self.target_temperature,
|
||||
self._attr_max_temp,
|
||||
self._attr_min_temp,
|
||||
)
|
||||
|
||||
self._last_regulation_change = self.now
|
||||
self.reset_last_change_time_from_vtherm()
|
||||
|
||||
_LOGGER.debug(
|
||||
"%s - last_regulation_change is now: %s and last_change_from_vtherm is now: %s", self, self._last_regulation_change, self._last_change_time_from_vtherm
|
||||
) # pylint: disable=protected-access
|
||||
|
||||
for under in self._underlyings_valve_regulation:
|
||||
await under.set_valve_open_percent()
|
||||
|
||||
@property
|
||||
def have_valve_regulation(self) -> bool:
|
||||
"""True if the Thermostat is regulated by valve"""
|
||||
return True
|
||||
|
||||
@property
|
||||
def valve_open_percent(self) -> int:
|
||||
"""Gives the percentage of valve needed"""
|
||||
if self._hvac_mode == HVACMode.OFF or self._valve_open_percent is None:
|
||||
return 0
|
||||
else:
|
||||
return self._valve_open_percent
|
||||
|
||||
@property
|
||||
def hvac_action(self) -> HVACAction | None:
|
||||
"""Returns the current hvac_action by checking all hvac_action of the _underlyings_valve_regulation"""
|
||||
|
||||
return self.calculate_hvac_action(self._underlyings_valve_regulation)
|
||||
|
||||
@property
|
||||
def is_device_active(self) -> bool:
|
||||
"""A hack to overrides the state from underlyings"""
|
||||
return self.valve_open_percent > 0
|
||||
|
||||
@property
|
||||
def device_actives(self) -> int:
|
||||
"""Calculate the number of active devices"""
|
||||
if self.is_device_active:
|
||||
return [
|
||||
under.opening_degree_entity_id
|
||||
for under in self._underlyings_valve_regulation
|
||||
]
|
||||
else:
|
||||
return []
|
||||
|
||||
@property
|
||||
def activable_underlying_entities(self) -> list | None:
|
||||
"""Returns the activable underlying entities for controling the central boiler"""
|
||||
return self._underlyings_valve_regulation
|
||||
|
||||
@overrides
|
||||
async def service_set_auto_regulation_mode(self, auto_regulation_mode: str):
|
||||
"""This should not be possible in valve regulation mode"""
|
||||
return
|
||||
@@ -1,232 +0,0 @@
|
||||
# pylint: disable=line-too-long, abstract-method
|
||||
|
||||
""" A climate over switch classe """
|
||||
import logging
|
||||
from homeassistant.core import Event, callback
|
||||
from homeassistant.helpers.event import (
|
||||
async_track_state_change_event,
|
||||
EventStateChangedData,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.components.climate import HVACMode
|
||||
|
||||
from .const import (
|
||||
CONF_UNDERLYING_LIST,
|
||||
CONF_HEATER_KEEP_ALIVE,
|
||||
CONF_INVERSE_SWITCH,
|
||||
CONF_VSWITCH_ON_CMD_LIST,
|
||||
CONF_VSWITCH_OFF_CMD_LIST,
|
||||
overrides,
|
||||
)
|
||||
|
||||
from .base_thermostat import BaseThermostat, ConfigData
|
||||
from .underlyings import UnderlyingSwitch
|
||||
from .prop_algorithm import PropAlgorithm
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
class ThermostatOverSwitch(BaseThermostat[UnderlyingSwitch]):
|
||||
"""Representation of a base class for a Versatile Thermostat over a switch."""
|
||||
|
||||
_entity_component_unrecorded_attributes = BaseThermostat._entity_component_unrecorded_attributes.union( # pylint: disable=protected-access
|
||||
frozenset(
|
||||
{
|
||||
"is_over_switch",
|
||||
"is_inversed",
|
||||
"underlying_entities",
|
||||
"on_time_sec",
|
||||
"off_time_sec",
|
||||
"cycle_min",
|
||||
"function",
|
||||
"tpi_coef_int",
|
||||
"tpi_coef_ext",
|
||||
"power_percent",
|
||||
"calculated_on_percent",
|
||||
"vswitch_on_commands",
|
||||
"vswitch_off_commands",
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
def __init__(self, hass: HomeAssistant, unique_id, name, config_entry) -> None:
|
||||
"""Initialize the thermostat over switch."""
|
||||
self._is_inversed: bool | None = None
|
||||
self._lst_vswitch_on: list[str] = []
|
||||
self._lst_vswitch_off: list[str] = []
|
||||
super().__init__(hass, unique_id, name, config_entry)
|
||||
|
||||
@property
|
||||
def is_over_switch(self) -> bool:
|
||||
"""True if the Thermostat is over_switch"""
|
||||
return True
|
||||
|
||||
@property
|
||||
def is_inversed(self) -> bool:
|
||||
"""True if the switch is inversed (for pilot wire and diode)"""
|
||||
return self._is_inversed is True
|
||||
|
||||
@overrides
|
||||
def post_init(self, config_entry: ConfigData):
|
||||
"""Initialize the Thermostat"""
|
||||
|
||||
super().post_init(config_entry)
|
||||
|
||||
self._prop_algorithm = PropAlgorithm(
|
||||
self._proportional_function,
|
||||
self._tpi_coef_int,
|
||||
self._tpi_coef_ext,
|
||||
self._cycle_min,
|
||||
self._minimal_activation_delay,
|
||||
self._minimal_deactivation_delay,
|
||||
self.name,
|
||||
max_on_percent=self._max_on_percent,
|
||||
)
|
||||
|
||||
self._is_inversed = config_entry.get(CONF_INVERSE_SWITCH) is True
|
||||
|
||||
lst_switches = config_entry.get(CONF_UNDERLYING_LIST)
|
||||
self._lst_vswitch_on = config_entry.get(CONF_VSWITCH_ON_CMD_LIST, [])
|
||||
self._lst_vswitch_off = config_entry.get(CONF_VSWITCH_OFF_CMD_LIST, [])
|
||||
|
||||
delta_cycle = self._cycle_min * 60 / len(lst_switches)
|
||||
for idx, switch in enumerate(lst_switches):
|
||||
vswitch_on = self._lst_vswitch_on[idx] if idx < len(self._lst_vswitch_on) else None
|
||||
vswitch_off = self._lst_vswitch_off[idx] if idx < len(self._lst_vswitch_off) else None
|
||||
self._underlyings.append(
|
||||
UnderlyingSwitch(
|
||||
hass=self._hass,
|
||||
thermostat=self,
|
||||
switch_entity_id=switch,
|
||||
initial_delay_sec=idx * delta_cycle,
|
||||
keep_alive_sec=config_entry.get(CONF_HEATER_KEEP_ALIVE, 0),
|
||||
vswitch_on=vswitch_on,
|
||||
vswitch_off=vswitch_off,
|
||||
)
|
||||
)
|
||||
|
||||
self._should_relaunch_control_heating = False
|
||||
|
||||
@overrides
|
||||
async def async_added_to_hass(self):
|
||||
"""Run when entity about to be added."""
|
||||
_LOGGER.debug("Calling async_added_to_hass")
|
||||
|
||||
await super().async_added_to_hass()
|
||||
|
||||
# Add listener to all underlying entities
|
||||
for switch in self._underlyings:
|
||||
self.async_on_remove(
|
||||
async_track_state_change_event(
|
||||
self.hass, [switch.entity_id], self._async_switch_changed
|
||||
)
|
||||
)
|
||||
switch.startup()
|
||||
|
||||
self.hass.create_task(self.async_control_heating())
|
||||
|
||||
@overrides
|
||||
def update_custom_attributes(self):
|
||||
"""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["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"
|
||||
] = self._prop_algorithm.on_percent
|
||||
self._attr_extra_state_attributes["power_percent"] = self.power_percent
|
||||
self._attr_extra_state_attributes[
|
||||
"on_time_sec"
|
||||
] = self._prop_algorithm.on_time_sec
|
||||
self._attr_extra_state_attributes[
|
||||
"off_time_sec"
|
||||
] = self._prop_algorithm.off_time_sec
|
||||
self._attr_extra_state_attributes["cycle_min"] = self._cycle_min
|
||||
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[
|
||||
"calculated_on_percent"
|
||||
] = self._prop_algorithm.calculated_on_percent
|
||||
|
||||
self._attr_extra_state_attributes["vswitch_on_commands"] = self._lst_vswitch_on
|
||||
self._attr_extra_state_attributes["vswitch_off_commands"] = self._lst_vswitch_off
|
||||
|
||||
self.async_write_ha_state()
|
||||
_LOGGER.debug(
|
||||
"%s - Calling update_custom_attributes: %s",
|
||||
self,
|
||||
self._attr_extra_state_attributes,
|
||||
)
|
||||
|
||||
@overrides
|
||||
def recalculate(self):
|
||||
"""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)
|
||||
self._prop_algorithm.calculate(
|
||||
self._target_temp,
|
||||
self._cur_temp,
|
||||
self._cur_ext_temp,
|
||||
self._hvac_mode or HVACMode.OFF,
|
||||
)
|
||||
self.update_custom_attributes()
|
||||
# already done bu update_custom_attributes
|
||||
# self.async_write_ha_state()
|
||||
|
||||
@overrides
|
||||
def incremente_energy(self):
|
||||
"""increment the energy counter if device is active"""
|
||||
if self.hvac_mode == HVACMode.OFF:
|
||||
return
|
||||
|
||||
added_energy = 0
|
||||
if not self.is_over_climate and self.power_manager.mean_cycle_power is not None:
|
||||
added_energy = (
|
||||
self.power_manager.mean_cycle_power * float(self._cycle_min) / 60.0
|
||||
)
|
||||
|
||||
if self._total_energy is None:
|
||||
self._total_energy = added_energy
|
||||
_LOGGER.debug(
|
||||
"%s - incremente_energy set energy is %s",
|
||||
self,
|
||||
self._total_energy,
|
||||
)
|
||||
else:
|
||||
self._total_energy += added_energy
|
||||
_LOGGER.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,
|
||||
added_energy,
|
||||
self._total_energy,
|
||||
)
|
||||
|
||||
@callback
|
||||
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")
|
||||
if new_state is None:
|
||||
return
|
||||
if old_state is None:
|
||||
self.hass.create_task(self._check_initial_state())
|
||||
|
||||
self.async_write_ha_state()
|
||||
self.update_custom_attributes()
|
||||
@@ -1,296 +0,0 @@
|
||||
# pylint: disable=line-too-long, abstract-method
|
||||
""" A climate over switch classe """
|
||||
import logging
|
||||
from datetime import timedelta, datetime
|
||||
|
||||
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, ConfigData
|
||||
from .prop_algorithm import PropAlgorithm
|
||||
|
||||
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__)
|
||||
|
||||
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( # 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",
|
||||
"calculated_on_percent",
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
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"""
|
||||
return True
|
||||
|
||||
@property
|
||||
def valve_open_percent(self) -> int:
|
||||
"""Gives the percentage of valve needed"""
|
||||
if self._hvac_mode == HVACMode.OFF:
|
||||
return 0
|
||||
else:
|
||||
return self._valve_open_percent
|
||||
|
||||
@overrides
|
||||
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
|
||||
)
|
||||
|
||||
self._prop_algorithm = PropAlgorithm(
|
||||
self._proportional_function,
|
||||
self._tpi_coef_int,
|
||||
self._tpi_coef_ext,
|
||||
self._cycle_min,
|
||||
self._minimal_activation_delay,
|
||||
self._minimal_deactivation_delay,
|
||||
self.name,
|
||||
max_on_percent=self._max_on_percent,
|
||||
)
|
||||
|
||||
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)
|
||||
)
|
||||
|
||||
self._should_relaunch_control_heating = False
|
||||
|
||||
@overrides
|
||||
async def async_added_to_hass(self):
|
||||
"""Run when entity about to be added."""
|
||||
_LOGGER.debug("Calling async_added_to_hass")
|
||||
|
||||
await super().async_added_to_hass()
|
||||
|
||||
# Add listener to all underlying entities
|
||||
for valve in self._underlyings:
|
||||
self.async_on_remove(
|
||||
async_track_state_change_event(
|
||||
self.hass, [valve.entity_id], self._async_valve_changed
|
||||
)
|
||||
)
|
||||
|
||||
# Start the control_heating
|
||||
# starts a cycle
|
||||
self.async_on_remove(
|
||||
async_track_time_interval(
|
||||
self.hass,
|
||||
self.async_control_heating,
|
||||
interval=timedelta(minutes=self._cycle_min),
|
||||
)
|
||||
)
|
||||
|
||||
@callback
|
||||
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
|
||||
)
|
||||
|
||||
@overrides
|
||||
def update_custom_attributes(self):
|
||||
"""Custom attributes"""
|
||||
super().update_custom_attributes()
|
||||
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_entities"] = [
|
||||
underlying.entity_id for underlying in self._underlyings
|
||||
]
|
||||
|
||||
self._attr_extra_state_attributes[
|
||||
"on_percent"
|
||||
] = self._prop_algorithm.on_percent
|
||||
self._attr_extra_state_attributes[
|
||||
"on_time_sec"
|
||||
] = self._prop_algorithm.on_time_sec
|
||||
self._attr_extra_state_attributes[
|
||||
"off_time_sec"
|
||||
] = self._prop_algorithm.off_time_sec
|
||||
self._attr_extra_state_attributes["cycle_min"] = self._cycle_min
|
||||
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._attr_extra_state_attributes[
|
||||
"calculated_on_percent"
|
||||
] = self._prop_algorithm.calculated_on_percent
|
||||
|
||||
self.async_write_ha_state()
|
||||
_LOGGER.debug(
|
||||
"%s - Calling update_custom_attributes: %s",
|
||||
self,
|
||||
self._attr_extra_state_attributes,
|
||||
)
|
||||
|
||||
@overrides
|
||||
def recalculate(self):
|
||||
"""A utility function to force the calculation of a the algo and
|
||||
update the custom attributes and write the state
|
||||
"""
|
||||
_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 or HVACMode.OFF,
|
||||
)
|
||||
|
||||
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
|
||||
|
||||
# is one in start_cycle now
|
||||
# for under in self._underlyings:
|
||||
# under.set_valve_open_percent()
|
||||
|
||||
self._last_calculation_timestamp = now
|
||||
|
||||
self.update_custom_attributes()
|
||||
# already done in update_custom_attributes
|
||||
# self.async_write_ha_state()
|
||||
|
||||
@overrides
|
||||
def incremente_energy(self):
|
||||
"""increment the energy counter if device is active"""
|
||||
if self.hvac_mode == HVACMode.OFF:
|
||||
return
|
||||
|
||||
added_energy = 0
|
||||
if not self.is_over_climate and self.power_manager.mean_cycle_power is not None:
|
||||
added_energy = (
|
||||
self.power_manager.mean_cycle_power * float(self._cycle_min) / 60.0
|
||||
)
|
||||
|
||||
if self._total_energy is None:
|
||||
self._total_energy = added_energy
|
||||
_LOGGER.debug(
|
||||
"%s - incremente_energy set energy is %s",
|
||||
self,
|
||||
self._total_energy,
|
||||
)
|
||||
else:
|
||||
self._total_energy += added_energy
|
||||
_LOGGER.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,
|
||||
)
|
||||
@@ -1,392 +0,0 @@
|
||||
{
|
||||
"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": "Ελάχιστη καθυστέρηση ενεργοποίησης",
|
||||
"safety_delay_min": "Καθυστέρηση ασφαλείας (σε λεπτά)",
|
||||
"safety_min_on_percent": "Ελάχιστο ποσοστό ισχύος για ενεργοποίηση λειτουργίας ασφαλείας",
|
||||
"safety_default_on_percent": "Ποσοστό ισχύος για χρήση σε λειτουργία ασφαλείας"
|
||||
},
|
||||
"data_description": {
|
||||
"minimal_activation_delay": "Καθυστέρηση σε δευτερόλεπτα κάτω από την οποία η συσκευή δεν θα ενεργοποιηθεί",
|
||||
"safety_delay_min": "Μέγιστη επιτρεπτή καθυστέρηση σε λεπτά μεταξύ δύο μετρήσεων θερμοκρασίας. Πέρα από αυτή την καθυστέρηση, ο θερμοστάτης θα μεταβεί σε κατάσταση ασφαλείας",
|
||||
"safety_min_on_percent": "Ελάχιστη τιμή ποσοστού θέρμανσης για την ενεργοποίηση του προεπιλεγμένου ασφάλειας. Κάτω από αυτό το ποσοστό ισχύος το θερμοστάτη δεν θα πάει στο προεπιλεγμένο ασφάλειας.",
|
||||
"safety_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": "Ελάχιστη καθυστέρηση ενεργοποίησης",
|
||||
"safety_delay_min": "Καθυστέρηση ασφαλείας (σε λεπτά)",
|
||||
"safety_min_on_percent": "Ελάχιστο ποσοστό ισχύος για τη λειτουργία ασφαλείας",
|
||||
"safety_default_on_percent": "Ποσοστό ισχύος που θα χρησιμοποιηθεί στη λειτουργία ασφαλείας"
|
||||
},
|
||||
"data_description": {
|
||||
"minimal_activation_delay": "Καθυστέρηση σε δευτερόλεπτα κάτω από την οποία ο εξοπλισμός δεν θα ενεργοποιηθεί",
|
||||
"safety_delay_min": "Μέγιστη επιτρεπόμενη καθυστέρηση σε λεπτά μεταξύ δύο μετρήσεων θερμοκρασίας. Πάνω από αυτή την καθυστέρηση, ο θερμοστάτης θα μεταβεί σε κατάσταση ασφαλείας",
|
||||
"safety_min_on_percent": "Ελάχιστη τιμή ποσοστού θέρμανσης για ενεργοποίηση του προεπιλεγμένου ασφαλείας. Κάτω από αυτό το ποσοστό ισχύος, ο θερμοστάτης δεν θα μεταβεί στο προεπιλεγμένο ασφαλείας",
|
||||
"safety_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,252 +4,142 @@
|
||||
"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",
|
||||
"valve_regulation": "Valve regulation configuration",
|
||||
"finalize": "All done",
|
||||
"configuration_not_complete": "Configuration not complete"
|
||||
}
|
||||
},
|
||||
"main": {
|
||||
"title": "Add new Versatile Thermostat",
|
||||
"description": "Main mandatory attributes [](https://github.com/jmcollin78/versatile_thermostat/blob/main/documentation/en/base-attributes.md)",
|
||||
"description": "Main mandatory attributes",
|
||||
"data": {
|
||||
"name": "Name",
|
||||
"thermostat_type": "Thermostat type",
|
||||
"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",
|
||||
"temperature_sensor_entity_id": "Temperature sensor entity id",
|
||||
"external_temperature_sensor_entity_id": "External temperature sensor entity id",
|
||||
"cycle_min": "Cycle duration (minutes)",
|
||||
"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": {
|
||||
"temp_min": "Minimal temperature allowed",
|
||||
"temp_max": "Maximal temperature allowed",
|
||||
"device_power": "Device power (kW)",
|
||||
"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_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"
|
||||
"use_presence_feature": "Use presence detection"
|
||||
}
|
||||
},
|
||||
"type": {
|
||||
"title": "Linked entities",
|
||||
"description": "Linked entities attributes [](https://github.com/jmcollin78/versatile_thermostat/blob/main/documentation/en/over-switch.md#configuration)",
|
||||
"description": "Linked entities attributes",
|
||||
"data": {
|
||||
"underlying_entity_ids": "The device(s) to be controlled",
|
||||
"heater_keep_alive": "Switch keep-alive interval in seconds",
|
||||
"heater_entity_id": "Heater switch",
|
||||
"heater_entity2_id": "2nd heater switch",
|
||||
"heater_entity3_id": "3rd heater switch",
|
||||
"heater_entity4_id": "4th heater switch",
|
||||
"proportional_function": "Algorithm",
|
||||
"ac_mode": "AC mode",
|
||||
"auto_regulation_mode": "Self-regulation",
|
||||
"auto_regulation_dtemp": "Regulation threshold",
|
||||
"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",
|
||||
"on_command_text": "Turn on command customization",
|
||||
"vswitch_on_command": "Optional turn on commands",
|
||||
"off_command_text": "Turn off command customization",
|
||||
"vswitch_off_command": "Optional turn off commands"
|
||||
"climate_entity_id": "Underlying climate",
|
||||
"climate_entity2_id": "2nd underlying climate",
|
||||
"climate_entity3_id": "3rd underlying climate",
|
||||
"climate_entity4_id": "4th underlying climate",
|
||||
"ac_mode": "AC mode"
|
||||
},
|
||||
"data_description": {
|
||||
"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.",
|
||||
"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",
|
||||
"proportional_function": "Algorithm to use (TPI is the only one for now)",
|
||||
"ac_mode": "Use the Air Conditioning (AC) mode",
|
||||
"auto_regulation_mode": "Auto adjustment of the target temperature",
|
||||
"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",
|
||||
"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",
|
||||
"on_command_text": "For underlying of type `select` or `climate` you have to customize the commands."
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"tpi": {
|
||||
"title": "TPI",
|
||||
"description": "Time Proportional Integral attributes [](https://github.com/jmcollin78/versatile_thermostat/blob/main/documentation/en/algorithms.md#lalgorithme-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",
|
||||
"use_tpi_central_config": "Check to use the central TPI configuration. Uncheck to use a specific TPI configuration for this VTherm"
|
||||
"tpi_coef_ext": "Coefficient to use for external temperature delta"
|
||||
}
|
||||
},
|
||||
"presets": {
|
||||
"title": "Presets",
|
||||
"description": "Select if the thermostat will use central preset - deselect for the thermostat to have its own presets [](https://github.com/jmcollin78/versatile_thermostat/blob/main/documentation/en/feature-presets.md)",
|
||||
"description": "For each presets, give the target temperature (0 to ignore preset)",
|
||||
"data": {
|
||||
"use_presets_central_config": "Use central presets configuration"
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"window": {
|
||||
"title": "Window management",
|
||||
"description": "Open window management.\nYou can also configure automatic window open detection based on temperature decrease [](https://github.com/jmcollin78/versatile_thermostat/blob/main/documentation/en/feature-window.md)",
|
||||
"description": "Open window management.\nLeave corresponding entity_id empty if not used\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 'on' delay (seconds)",
|
||||
"window_off_delay": "Window sensor 'off' delay (seconds)",
|
||||
"window_auto_open_threshold": "Temperature decrease threshold for automatic window open detection (in °/hours)",
|
||||
"window_auto_close_threshold": "Temperature increase threshold for end of automatic detection (in °/hours)",
|
||||
"window_auto_max_duration": "Maximum duration of automatic window open detection (in min)",
|
||||
"use_window_central_config": "Use central window configuration",
|
||||
"window_action": "Action"
|
||||
"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)"
|
||||
},
|
||||
"data_description": {
|
||||
"window_sensor_entity_id": "Leave empty if no window sensor should be used and to use the automatic detection",
|
||||
"window_delay": "The delay in seconds before sensor 'on' detection is taken into account",
|
||||
"window_off_delay": "The delay in seconds before sensor 'off' detection is taken into account. Leave it empty to use the same value as window on delay",
|
||||
"window_auto_open_threshold": "Recommended value: between 3 and 10. Leave empty if automatic window open detection is not used",
|
||||
"window_auto_close_threshold": "Recommended value: 0. Leave empty if automatic window open detection is not used",
|
||||
"window_auto_max_duration": "Recommended value: 60 (one hour). Leave empty if automatic window open detection is not used",
|
||||
"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"
|
||||
"window_sensor_entity_id": "Leave empty if no window sensor should be use",
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"motion": {
|
||||
"title": "Motion management",
|
||||
"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 [](https://github.com/jmcollin78/versatile_thermostat/blob/main/documentation/en/feature-motion.md)",
|
||||
"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",
|
||||
"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",
|
||||
"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 delay (seconds)",
|
||||
"motion_off_delay": "Motion deactivation delay (seconds)",
|
||||
"motion_delay": "Motion delay (seconds)",
|
||||
"motion_preset": "Preset to use when 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"
|
||||
"no_motion_preset": "Preset to use when no motion is detected"
|
||||
}
|
||||
},
|
||||
"power": {
|
||||
"title": "Power management",
|
||||
"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) [](https://github.com/jmcollin78/versatile_thermostat/blob/main/documentation/en/feature-power.md)",
|
||||
"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.",
|
||||
"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",
|
||||
"use_power_central_config": "Check to use the central power configuration. Uncheck to use a specific power configuration for this VTherm"
|
||||
"power_temp": "Temperature for Power shedding"
|
||||
}
|
||||
},
|
||||
"presence": {
|
||||
"title": "Presence management",
|
||||
"description": "Presence management attributes.\nGives the a presence sensor of your home (true is someone is present) and give the corresponding temperature preset setting [](https://github.com/jmcollin78/versatile_thermostat/blob/main/documentation/en/feature-presence.md)",
|
||||
"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.",
|
||||
"data": {
|
||||
"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"
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"advanced": {
|
||||
"title": "Advanced parameters",
|
||||
"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 [](https://github.com/jmcollin78/versatile_thermostat/blob/main/documentation/en/feature-advanced.md)",
|
||||
"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.",
|
||||
"data": {
|
||||
"minimal_activation_delay": "Minimum activation delay",
|
||||
"minimal_deactivation_delay": "Minimal deactivation delay",
|
||||
"safety_delay_min": "Safety delay (in minutes)",
|
||||
"safety_min_on_percent": "Minimum power percent to enable safety mode",
|
||||
"safety_default_on_percent": "Power percent to use in safety mode",
|
||||
"use_advanced_central_config": "Use central advanced configuration"
|
||||
"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"
|
||||
},
|
||||
"data_description": {
|
||||
"minimal_activation_delay": "Delay in seconds under which the equipment will not be activated",
|
||||
"minimal_deactivation_delay": "Delay in seconds under which the equipment will be kept active",
|
||||
"safety_delay_min": "Maximum allowed delay in minutes between two temperature measurements. Above this delay the thermostat will turn to a safety off state",
|
||||
"safety_min_on_percent": "Minimum heating percent value for safety preset activation. Below this amount of power percent the thermostat won't go into safety preset",
|
||||
"safety_default_on_percent": "The default heating power percent value in safety preset. Set to 0 to switch off heater in safety preset",
|
||||
"use_advanced_central_config": "Check to use the central advanced configuration. Uncheck to use a specific advanced configuration for this VTherm"
|
||||
}
|
||||
},
|
||||
"central_boiler": {
|
||||
"title": "Control of the central boiler",
|
||||
"description": "Enter the services to call to turn on/off the central boiler. Leave blank if no service call is to be made (in this case, you will have to manage the turning on/off of your central boiler yourself). The service called must be formatted as follows: `entity_id/service_name[/attribute:value]` (/attribute:value is optional)\nFor example:\n- to turn on a switch: `switch.controle_chaudiere/switch.turn_on`\n- to turn off a switch: `switch.controle_chaudiere/switch.turn_off`\n- to program the boiler to 25° and thus force its ignition: `climate.thermostat_chaudiere/climate.set_temperature/temperature:25`\n- to send 10° to the boiler and thus force its extinction: `climate.thermostat_chaudiere/climate.set_temperature/temperature:10` [](https://github.com/jmcollin78/versatile_thermostat/blob/main/documentation/fr/feature-central-boiler.md)",
|
||||
"data": {
|
||||
"central_boiler_activation_service": "Command to turn-on",
|
||||
"central_boiler_deactivation_service": "Command to turn-off"
|
||||
},
|
||||
"data_description": {
|
||||
"central_boiler_activation_service": "Command to turn-on the central boiler formatted like entity_id/service_name[/attribut:valeur]",
|
||||
"central_boiler_deactivation_service": "Command to turn-off the central boiler formatted like entity_id/service_name[/attribut:valeur]"
|
||||
}
|
||||
},
|
||||
"valve_regulation": {
|
||||
"title": "Self-regulation with valve",
|
||||
"description": "Configuration for self-regulation with direct control of the valve [](https://github.com/jmcollin78/versatile_thermostat/blob/main/documentation/fr/self-regulation.md#auto-régulation-par-contrôle-direct-de-la-vanne)",
|
||||
"data": {
|
||||
"offset_calibration_entity_ids": "Offset calibration entities",
|
||||
"opening_degree_entity_ids": "Opening degree entities",
|
||||
"closing_degree_entity_ids": "Closing degree entities",
|
||||
"proportional_function": "Algorithm",
|
||||
"min_opening_degrees": "Min opening degrees"
|
||||
},
|
||||
"data_description": {
|
||||
"offset_calibration_entity_ids": "The list of the 'offset calibration' entities. Set it if your TRV has the entity for better regulation. There should be one per underlying climate entities",
|
||||
"opening_degree_entity_ids": "The list of the 'opening degree' entities. There should be one per underlying climate entities",
|
||||
"closing_degree_entity_ids": "The list of the 'closing degree' entities. Set it if your TRV has the entity for better regulation. There should be one per underlying climate entities",
|
||||
"proportional_function": "Algorithm to use (TPI is the only one for now)",
|
||||
"min_opening_degrees": "Opening degree minimum value for each underlying device, comma separated. Default to 0. Example: 20, 25, 30"
|
||||
"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"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"unknown": "Unexpected error",
|
||||
"unknown_entity": "Unknown entity id",
|
||||
"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."
|
||||
"window_open_detection_method": "Only one window open detection method should be used. Use sensor or automatic detection through temperature threshold but not both"
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "Device is already configured"
|
||||
@@ -259,252 +149,142 @@
|
||||
"flow_title": "Versatile Thermostat configuration",
|
||||
"step": {
|
||||
"user": {
|
||||
"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",
|
||||
"valve_regulation": "Valve regulation configuration",
|
||||
"finalize": "All done",
|
||||
"configuration_not_complete": "Configuration not complete"
|
||||
}
|
||||
},
|
||||
"main": {
|
||||
"title": "Main - {name}",
|
||||
"description": "Main mandatory attributes [](https://github.com/jmcollin78/versatile_thermostat/blob/main/documentation/en/base-attributes.md)",
|
||||
"title": "Add new Versatile Thermostat",
|
||||
"description": "Main mandatory attributes",
|
||||
"data": {
|
||||
"name": "Name",
|
||||
"thermostat_type": "Thermostat type",
|
||||
"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",
|
||||
"temperature_sensor_entity_id": "Temperature sensor entity id",
|
||||
"external_temperature_sensor_entity_id": "External temperature sensor entity id",
|
||||
"cycle_min": "Cycle duration (minutes)",
|
||||
"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": {
|
||||
"temp_min": "Minimal temperature allowed",
|
||||
"temp_max": "Maximal temperature allowed",
|
||||
"device_power": "Device power (kW)",
|
||||
"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_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"
|
||||
"use_presence_feature": "Use presence detection"
|
||||
}
|
||||
},
|
||||
"type": {
|
||||
"title": "Entities - {name}",
|
||||
"description": "Linked entities attributes [](https://github.com/jmcollin78/versatile_thermostat/blob/main/documentation/en/over-switch.md#configuration)",
|
||||
"title": "Linked entities",
|
||||
"description": "Linked entities attributes",
|
||||
"data": {
|
||||
"underlying_entity_ids": "The device(s) to be controlled",
|
||||
"heater_keep_alive": "Switch keep-alive interval in seconds",
|
||||
"heater_entity_id": "Heater switch",
|
||||
"heater_entity2_id": "2nd Heater switch",
|
||||
"heater_entity3_id": "3rd Heater switch",
|
||||
"heater_entity4_id": "4th Heater switch",
|
||||
"proportional_function": "Algorithm",
|
||||
"ac_mode": "AC mode",
|
||||
"auto_regulation_mode": "Self-regulation",
|
||||
"auto_regulation_dtemp": "Regulation threshold",
|
||||
"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",
|
||||
"on_command_text": "Turn on command customization",
|
||||
"vswitch_on_command": "Optional turn on commands",
|
||||
"off_command_text": "Turn off command customization",
|
||||
"vswitch_off_command": "Optional turn off commands"
|
||||
"climate_entity_id": "Underlying thermostat",
|
||||
"climate_entity2_id": "2nd underlying climate",
|
||||
"climate_entity3_id": "3rd underlying climate",
|
||||
"climate_entity4_id": "4th underlying climate",
|
||||
"ac_mode": "AC mode"
|
||||
},
|
||||
"data_description": {
|
||||
"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.",
|
||||
"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",
|
||||
"proportional_function": "Algorithm to use (TPI is the only one for now)",
|
||||
"ac_mode": "Use the Air Conditioning (AC) mode",
|
||||
"auto_regulation_mode": "Auto adjustment of the target temperature",
|
||||
"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",
|
||||
"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",
|
||||
"on_command_text": "For underlying of type `select` or `climate` you have to customize the commands."
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"tpi": {
|
||||
"title": "TPI - {name}",
|
||||
"description": "Time Proportional Integral attributes [](https://github.com/jmcollin78/versatile_thermostat/blob/main/documentation/en/algorithms.md#lalgorithme-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",
|
||||
"use_tpi_central_config": "Check to use the central TPI configuration. Uncheck to use a specific TPI configuration for this VTherm"
|
||||
"tpi_coef_ext": "Coefficient to use for external temperature delta"
|
||||
}
|
||||
},
|
||||
"presets": {
|
||||
"title": "Presets - {name}",
|
||||
"description": "Check if the thermostat will use central presets. Uncheck and the thermostat will have its own preset entities [](https://github.com/jmcollin78/versatile_thermostat/blob/main/documentation/en/feature-presets.md)",
|
||||
"title": "Presets",
|
||||
"description": "For each presets, give the target temperature (0 to ignore preset)",
|
||||
"data": {
|
||||
"use_presets_central_config": "Use central presets configuration"
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"window": {
|
||||
"title": "Window - {name}",
|
||||
"description": "Open window management.\nYou can also configure automatic window open detection based on temperature decrease [](https://github.com/jmcollin78/versatile_thermostat/blob/main/documentation/en/feature-window.md)",
|
||||
"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",
|
||||
"data": {
|
||||
"window_sensor_entity_id": "Window sensor entity id",
|
||||
"window_delay": "Window sensor 'on' delay (seconds)",
|
||||
"window_off_delay": "Window sensor 'off' delay (seconds)",
|
||||
"window_auto_open_threshold": "Temperature decrease threshold for automatic window open detection (in °/hours)",
|
||||
"window_auto_close_threshold": "Temperature increase threshold for end of automatic detection (in °/hours)",
|
||||
"window_auto_max_duration": "Maximum duration of automatic window open detection (in min)",
|
||||
"use_window_central_config": "Use central window configuration",
|
||||
"window_action": "Action"
|
||||
"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)"
|
||||
},
|
||||
"data_description": {
|
||||
"window_sensor_entity_id": "Leave empty if no window sensor should be used and to use the automatic detection",
|
||||
"window_delay": "The delay in seconds before sensor 'on' detection is taken into account",
|
||||
"window_off_delay": "The delay in seconds before sensor 'off' detection is taken into account. Leave it empty to use the same value as window on delay",
|
||||
"window_auto_close_threshold": "Recommended value: 0. Leave empty if automatic window open detection is not used",
|
||||
"window_auto_max_duration": "Recommended value: 60 (one hour). Leave empty if automatic window open detection is not used",
|
||||
"use_window_central_config": "Check to use the central window configuration. Uncheck to use a specific window configuration for this VTherm",
|
||||
"window_action": "Action to do if window is deteted as open"
|
||||
"window_sensor_entity_id": "Leave empty if no window sensor should be use",
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"motion": {
|
||||
"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 [](https://github.com/jmcollin78/versatile_thermostat/blob/main/documentation/en/feature-motion.md)",
|
||||
"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",
|
||||
"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",
|
||||
"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 delay (seconds)",
|
||||
"motion_off_delay": "Motion deactivation delay (seconds)",
|
||||
"motion_delay": "Motion delay (seconds)",
|
||||
"motion_preset": "Preset to use when 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"
|
||||
"no_motion_preset": "Preset to use when no motion is detected"
|
||||
}
|
||||
},
|
||||
"power": {
|
||||
"title": "Power - {name}",
|
||||
"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) [](https://github.com/jmcollin78/versatile_thermostat/blob/main/documentation/en/feature-power.md)",
|
||||
"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.",
|
||||
"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",
|
||||
"use_power_central_config": "Check to use the central power configuration. Uncheck to use a specific power configuration for this VTherm"
|
||||
"power_temp": "Temperature for Power shedding"
|
||||
}
|
||||
},
|
||||
"presence": {
|
||||
"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 [](https://github.com/jmcollin78/versatile_thermostat/blob/main/documentation/en/feature-presence.md)",
|
||||
"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.",
|
||||
"data": {
|
||||
"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"
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"advanced": {
|
||||
"title": "Advanced - {name}",
|
||||
"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 [](https://github.com/jmcollin78/versatile_thermostat/blob/main/documentation/en/feature-advanced.md)",
|
||||
"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.",
|
||||
"data": {
|
||||
"minimal_activation_delay": "Minimum activation delay",
|
||||
"safety_delay_min": "Safety delay (in minutes)",
|
||||
"safety_min_on_percent": "Minimum power percent to enable safety mode",
|
||||
"safety_default_on_percent": "Power percent to use in safety mode",
|
||||
"use_advanced_central_config": "Use central advanced configuration"
|
||||
"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"
|
||||
},
|
||||
"data_description": {
|
||||
"minimal_activation_delay": "Delay in seconds under which the equipment will not be activated",
|
||||
"safety_delay_min": "Maximum allowed delay in minutes between two temperature measurements. Above this delay the thermostat will turn to a safety off state",
|
||||
"safety_min_on_percent": "Minimum heating percent value for safety preset activation. Below this amount of power percent the thermostat won't go into safety preset",
|
||||
"safety_default_on_percent": "The default heating power percent value in safety preset. Set to 0 to switch off heater in safety preset",
|
||||
"use_advanced_central_config": "Check to use the central advanced configuration. Uncheck to use a specific advanced configuration for this VTherm"
|
||||
}
|
||||
},
|
||||
"central_boiler": {
|
||||
"title": "Control of the central boiler - {name}",
|
||||
"description": "Enter the services to call to turn on/off the central boiler. Leave blank if no service call is to be made (in this case, you will have to manage the turning on/off of your central boiler yourself). The service called must be formatted as follows: `entity_id/service_name[/attribute:value]` (/attribute:value is optional)\nFor example:\n- to turn on a switch: `switch.controle_chaudiere/switch.turn_on`\n- to turn off a switch: `switch.controle_chaudiere/switch.turn_off`\n- to program the boiler to 25° and thus force its ignition: `climate.thermostat_chaudiere/climate.set_temperature/temperature:25`\n- to send 10° to the boiler and thus force its extinction: `climate.thermostat_chaudiere/climate.set_temperature/temperature:10` [](https://github.com/jmcollin78/versatile_thermostat/blob/main/documentation/fr/feature-central-boiler.md)",
|
||||
"data": {
|
||||
"central_boiler_activation_service": "Command to turn-on",
|
||||
"central_boiler_deactivation_service": "Command to turn-off"
|
||||
},
|
||||
"data_description": {
|
||||
"central_boiler_activation_service": "Command to turn-on the central boiler formatted like entity_id/service_name[/attribut:valeur]",
|
||||
"central_boiler_deactivation_service": "Command to turn-off the central boiler formatted like entity_id/service_name[/attribut:valeur]"
|
||||
}
|
||||
},
|
||||
"valve_regulation": {
|
||||
"title": "Self-regulation with valve - {name}",
|
||||
"description": "Configuration for self-regulation with direct control of the valve [](https://github.com/jmcollin78/versatile_thermostat/blob/main/documentation/fr/self-regulation.md#auto-régulation-par-contrôle-direct-de-la-vanne)",
|
||||
"data": {
|
||||
"offset_calibration_entity_ids": "Offset calibration entities",
|
||||
"opening_degree_entity_ids": "Opening degree entities",
|
||||
"closing_degree_entity_ids": "Closing degree entities",
|
||||
"proportional_function": "Algorithm",
|
||||
"min_opening_degrees": "Min opening degrees"
|
||||
},
|
||||
"data_description": {
|
||||
"offset_calibration_entity_ids": "The list of the 'offset calibration' entities. Set it if your TRV has the entity for better regulation. There should be one per underlying climate entities",
|
||||
"opening_degree_entity_ids": "The list of the 'opening degree' entities. There should be one per underlying climate entities",
|
||||
"closing_degree_entity_ids": "The list of the 'closing degree' entities. Set it if your TRV has the entity for better regulation. There should be one per underlying climate entities",
|
||||
"proportional_function": "Algorithm to use (TPI is the only one for now)",
|
||||
"min_opening_degrees": "Opening degree minimum value for each underlying device, comma separated. Default to 0. Example: 20, 25, 30"
|
||||
"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"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"unknown": "Unexpected error",
|
||||
"unknown_entity": "Unknown entity id",
|
||||
"window_open_detection_method": "Only one window open detection method should be used. Use either window sensor or automatic detection through temperature threshold but not both",
|
||||
"no_central_config": "You cannot check 'use central configuration' because no central configuration was found. You need to create a Versatile Thermostat of type 'Central Configuration' to use it.",
|
||||
"service_configuration_format": "The format of the service configuration is wrong",
|
||||
"valve_regulation_nb_entities_incorrect": "The number of valve entities for valve regulation should be equal to the number of underlyings",
|
||||
"min_opening_degrees_format": "A comma separated list of positive integer is expected. Example: 20, 25, 30"
|
||||
"window_open_detection_method": "Only one window open detection method should be used. Use sensor or automatic detection through temperature threshold but not both"
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "Device is already configured"
|
||||
@@ -513,54 +293,8 @@
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"auto_regulation_mode": {
|
||||
"options": {
|
||||
"auto_regulation_slow": "Slow",
|
||||
"auto_regulation_strong": "Strong",
|
||||
"auto_regulation_medium": "Medium",
|
||||
"auto_regulation_light": "Light",
|
||||
"auto_regulation_expert": "Expert",
|
||||
"auto_regulation_none": "No auto-regulation",
|
||||
"auto_regulation_valve": "Direct control of valve"
|
||||
}
|
||||
},
|
||||
"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"
|
||||
"thermostat_over_climate": "Thermostat over another thermostat"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -571,60 +305,12 @@
|
||||
"preset_mode": {
|
||||
"state": {
|
||||
"power": "Shedding",
|
||||
"safety": "Safety",
|
||||
"none": "Manual",
|
||||
"frost": "Frost"
|
||||
"security": "Security",
|
||||
"none": "Manual"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"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,253 +4,141 @@
|
||||
"flow_title": "Versatile Thermostat configuration",
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "Type du nouveau Versatile Thermostat",
|
||||
"description": "Choisissez le type de thermostat que vous voulez créer [](https://github.com/jmcollin78/versatile_thermostat/blob/main/documentation/fr/creation.md)",
|
||||
"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",
|
||||
"valve_regulation": "Configuration de la regulation par vanne",
|
||||
"finalize": "Finaliser la création",
|
||||
"configuration_not_complete": "Configuration incomplète"
|
||||
}
|
||||
},
|
||||
"main": {
|
||||
"title": "Ajout d'un nouveau thermostat",
|
||||
"description": "Principaux attributs obligatoires [](https://github.com/jmcollin78/versatile_thermostat/blob/main/documentation/fr/base-attributes.md)",
|
||||
"description": "Principaux attributs obligatoires",
|
||||
"data": {
|
||||
"name": "Nom",
|
||||
"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",
|
||||
"temperature_sensor_entity_id": "Température sensor entity id",
|
||||
"external_temperature_sensor_entity_id": "Temperature exterieure sensor entity id",
|
||||
"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_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"
|
||||
"use_presence_feature": "Avec détection de présence"
|
||||
}
|
||||
},
|
||||
"type": {
|
||||
"title": "Entité(s) liée(s)",
|
||||
"description": "Attributs de(s) l'entité(s) liée(s) [](https://github.com/jmcollin78/versatile_thermostat/blob/main/documentation/fr/over-switch.md#configuration)",
|
||||
"description": "Attributs de(s) l'entité(s) liée(s)",
|
||||
"data": {
|
||||
"underlying_entity_ids": "Les équipements à controller",
|
||||
"heater_keep_alive": "keep-alive (sec)",
|
||||
"heater_entity_id": "1er radiateur",
|
||||
"heater_entity2_id": "2ème radiateur",
|
||||
"heater_entity3_id": "3ème radiateur",
|
||||
"heater_entity4_id": "4ème radiateur",
|
||||
"proportional_function": "Algorithme",
|
||||
"ac_mode": "AC mode ?",
|
||||
"auto_regulation_mode": "Auto-régulation",
|
||||
"auto_regulation_dtemp": "Seuil de régulation",
|
||||
"auto_regulation_periode_min": "Période minimale de régulation",
|
||||
"auto_regulation_use_device_temp": "Compenser la température interne du sous-jacent",
|
||||
"inverse_switch_command": "Inverser la commande",
|
||||
"auto_fan_mode": " Auto ventilation mode",
|
||||
"on_command_text": "Personnalisation des commandes d'allumage",
|
||||
"vswitch_on_command": "Commande d'allumage (optionnel)",
|
||||
"off_command_text": "Personnalisation des commandes d'extinction",
|
||||
"vswitch_off_command": "Commande d'extinction (optionnel)"
|
||||
"climate_entity_id": "Thermostat sous-jacent",
|
||||
"climate_entity2_id": "2ème thermostat sous-jacent",
|
||||
"climate_entity3_id": "3ème thermostat sous-jacent",
|
||||
"climate_entity4_id": "4ème thermostat sous-jacent",
|
||||
"ac_mode": "AC mode ?"
|
||||
},
|
||||
"data_description": {
|
||||
"underlying_entity_ids": "La liste des équipements qui seront controlés par ce VTherm",
|
||||
"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.",
|
||||
"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",
|
||||
"proportional_function": "Algorithme à utiliser (Seul TPI est disponible pour l'instant)",
|
||||
"ac_mode": "Utilisation du mode Air Conditionné (AC)",
|
||||
"auto_regulation_mode": "Utilisation de l'auto-régulation faite par VTherm",
|
||||
"auto_regulation_dtemp": "Le seuil en ° (ou % pour les vannes) 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",
|
||||
"auto_regulation_use_device_temp": "Compenser la temperature interne du sous-jacent pour accélérer l'auto-régulation",
|
||||
"inverse_switch_command": "Inverse la commande du switch pour une installation avec fil pilote et diode",
|
||||
"auto_fan_mode": "Active la ventilation automatiquement en cas d'écart important",
|
||||
"on_command_text": "Pour les sous-jacents de type `select` ou `climate` vous devez personnaliser les commandes."
|
||||
"climate_entity_id": "Entity id du thermostat sous-jacent",
|
||||
"climate_entity2_id": "Entity id du 2ème thermostat sous-jacent",
|
||||
"climate_entity3_id": "Entity id du 3ème thermostat sous-jacent",
|
||||
"climate_entity4_id": "Entity id du 4ème thermostat sous-jacent",
|
||||
"ac_mode": "Utilisation du mode Air Conditionné (AC)"
|
||||
}
|
||||
},
|
||||
"tpi": {
|
||||
"title": "TPI",
|
||||
"description": "Attributs de l'algo Time Proportional Integral [](https://github.com/jmcollin78/versatile_thermostat/blob/main/documentation/fr/algorithms.md#lalgorithme-tpi)",
|
||||
"description": "Attributs de l'algo Time Proportional Integral",
|
||||
"data": {
|
||||
"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"
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"presets": {
|
||||
"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 [](https://github.com/jmcollin78/versatile_thermostat/blob/main/documentation/fr/feature-presets.md)",
|
||||
"title": "Presets",
|
||||
"description": "Pour chaque preset, donnez la température cible (0 pour ignorer le preset)",
|
||||
"data": {
|
||||
"use_presets_central_config": "Utiliser la configuration des pré-réglages centrale"
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"window": {
|
||||
"title": "Gestion d'une ouverture",
|
||||
"description": "Coupe le radiateur si l'ouverture est ouverte.\nLaissez l'id d'entité vide pour utiliser la détection automatique [](https://github.com/jmcollin78/versatile_thermostat/blob/main/documentation/fr/feature-window.md)",
|
||||
"description": "Coupe le radiateur si l'ouverture est ouverte.\nLaissez l'entity id vide si non utilisé.",
|
||||
"data": {
|
||||
"window_sensor_entity_id": "Détecteur d'ouverture (entity id)",
|
||||
"window_delay": "Délai de prise en compte à l'ouverture (secondes)",
|
||||
"window_off_delay": "Délai de prise compte à la fermeture (secondes)",
|
||||
"window_auto_open_threshold": "Seuil haut de chute de température pour la détection automatique (en °/heure)",
|
||||
"window_auto_close_threshold": "Seuil bas de chute de température pour la fin de détection automatique (en °/heure)",
|
||||
"window_auto_max_duration": "Durée maximum d'une extinction automatique (en min)",
|
||||
"use_window_central_config": "Utiliser la configuration centrale des ouvertures",
|
||||
"window_action": "Action"
|
||||
"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)"
|
||||
},
|
||||
"data_description": {
|
||||
"window_sensor_entity_id": "Laissez vide si vous n'avez de détecteur et pour utiliser la détection automatique",
|
||||
"window_delay": "Le délai (en secondes) avant que le changement du détecteur soit pris en compte lors de la détection d'une ouverture",
|
||||
"window_off_delay": "Le délai (en secondes) avant que le changement du détecteur soit pris en compte lors de la détection d'une fermeture. Laissez vide pour utiliser le même délai à l'ouveture et à la fermeture",
|
||||
"window_auto_open_threshold": "Valeur recommandée: entre 3 et 10. Laissez vide si vous n'utilisez pas la détection automatique",
|
||||
"window_sensor_entity_id": "Laissez vide si vous n'avez de détecteur",
|
||||
"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_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",
|
||||
"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"
|
||||
"window_auto_max_duration": "Valeur recommandée: 60 (1 heure). Laissez vide si vous n'utilisez pas la détection automatique"
|
||||
}
|
||||
},
|
||||
"motion": {
|
||||
"title": "Gestion de la détection de mouvement",
|
||||
"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 [](https://github.com/jmcollin78/versatile_thermostat/blob/main/documentation/fr/feature-motion.md)",
|
||||
"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.",
|
||||
"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 sans mouvement",
|
||||
"use_motion_central_config": "Utiliser la condfiguration centrale du mouvement"
|
||||
},
|
||||
"data_description": {
|
||||
"motion_sensor_entity_id": "Id d'entité du détecteur de mouvement",
|
||||
"motion_delay": "Délai avant activation lorsqu'un mouvement est détecté (secondes)",
|
||||
"motion_off_delai": "Délai avant désactivation lorsqu'aucun mouvement n'est détecté (secondes)",
|
||||
"motion_sensor_entity_id": "Détecteur de mouvement entity id",
|
||||
"motion_delay": "Délai avant changement (seconds)",
|
||||
"motion_preset": "Preset à utiliser si 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"
|
||||
"no_motion_preset": "Preset à utiliser si pas de mouvement détecté"
|
||||
}
|
||||
},
|
||||
"power": {
|
||||
"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) [](https://github.com/jmcollin78/versatile_thermostat/blob/main/documentation/fr/feature-power.md)",
|
||||
"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).",
|
||||
"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",
|
||||
"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"
|
||||
"power_temp": "Température si délestaqe"
|
||||
}
|
||||
},
|
||||
"presence": {
|
||||
"title": "Gestion de la présenc",
|
||||
"description": "Donnez un capteur de présence (true si quelqu'un est présent) et les températures cibles à utiliser en cas d'absence [](https://github.com/jmcollin78/versatile_thermostat/blob/main/documentation/fr/feature-presence.md)",
|
||||
"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.",
|
||||
"data": {
|
||||
"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"
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"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 [](https://github.com/jmcollin78/versatile_thermostat/blob/main/documentation/fr/feature-advanced.md)",
|
||||
"description": "Configuration des paramètres avancés. Laissez les valeurs par défaut si vous ne savez pas ce que vous faites.\nCes paramètres peuvent induire des mauvais comportements du thermostat.",
|
||||
"data": {
|
||||
"minimal_activation_delay": "Délai minimal d'activation",
|
||||
"minimal_deactivation_delay": "Délai de désactivation minimal",
|
||||
"safety_delay_min": "Délai maximal entre 2 mesures de températures",
|
||||
"safety_min_on_percent": "Pourcentage minimal de puissance",
|
||||
"safety_default_on_percent": "Pourcentage de puissance a utiliser en mode securité",
|
||||
"use_advanced_central_config": "Utiliser la configuration centrale avancée"
|
||||
"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é"
|
||||
},
|
||||
"data_description": {
|
||||
"minimal_activation_delay": "Délai en secondes en-dessous duquel l'équipement ne sera pas activé",
|
||||
"minimal_deactivation_delay": "Délai en secondes en-dessous duquel l'équipement se laissé actif",
|
||||
"safety_delay_min": "Délai maximal autorisé en minutes entre 2 mesures de températures. Au-dessus de ce délai, le thermostat se mettra en position de sécurité",
|
||||
"safety_min_on_percent": "Seuil minimal de pourcentage de chauffage en-dessous duquel le préréglage sécurité ne sera jamais activé",
|
||||
"safety_default_on_percent": "Valeur par défaut pour le pourcentage de chauffage en mode sécurité. Mettre 0 pour éteindre le radiateur en mode sécurité",
|
||||
"use_advanced_central_config": "Cochez pour utiliser la configuration centrale avancée. Décochez et saisissez les attributs pour utiliser une configuration spécifique avancée"
|
||||
}
|
||||
},
|
||||
"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` [](https://github.com/jmcollin78/versatile_thermostat/blob/main/documentation/fr/feature-central-boiler.md)",
|
||||
"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]"
|
||||
}
|
||||
},
|
||||
"valve_regulation": {
|
||||
"title": "Auto-régulation par vanne",
|
||||
"description": "Configuration de l'auto-régulation par controle direct de la vanne [](https://github.com/jmcollin78/versatile_thermostat/blob/main/documentation/fr/self-regulation.md#auto-régulation-par-contrôle-direct-de-la-vanne)",
|
||||
"data": {
|
||||
"offset_calibration_entity_ids": "Entités de 'calibrage du décalage''",
|
||||
"opening_degree_entity_ids": "Entités 'ouverture de vanne'",
|
||||
"closing_degree_entity_ids": "Entités 'fermeture de la vanne'",
|
||||
"proportional_function": "Algorithme",
|
||||
"min_opening_degrees": "Ouvertures minimales"
|
||||
},
|
||||
"data_description": {
|
||||
"offset_calibration_entity_ids": "La liste des entités 'calibrage du décalage' (offset calibration). Configurez le si votre TRV possède cette fonction pour une meilleure régulation. Il doit y en avoir une par entité climate sous-jacente",
|
||||
"opening_degree_entity_ids": "La liste des entités 'ouverture de vanne'. Il doit y en avoir une par entité climate sous-jacente",
|
||||
"closing_degree_entity_ids": "La liste des entités 'fermeture de la vanne'. Configurez le si votre TRV possède cette fonction pour une meilleure régulation. Il doit y en avoir une par entité climate sous-jacente",
|
||||
"proportional_function": "Algorithme à utiliser (seulement TPI est disponible)",
|
||||
"min_opening_degrees": "Une liste séparée par des virgules de minimum d'ouverture. Valeur par défaut : 0. Exemple : 20, 25, 30"
|
||||
"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é"
|
||||
}
|
||||
}
|
||||
},
|
||||
"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.",
|
||||
"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."
|
||||
"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."
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "Le device est déjà configuré"
|
||||
@@ -260,251 +148,143 @@
|
||||
"flow_title": "Versatile Thermostat configuration",
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "Type - {name}",
|
||||
"description": "Choisissez le type de thermostat que vous voulez créer [](https://github.com/jmcollin78/versatile_thermostat/blob/main/documentation/fr/creation.md)",
|
||||
"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",
|
||||
"valve_regulation": "Configuration de la regulation par vanne",
|
||||
"finalize": "Finaliser les modifications",
|
||||
"configuration_not_complete": "Configuration incomplète"
|
||||
}
|
||||
},
|
||||
"main": {
|
||||
"title": "Attributs - {name}",
|
||||
"description": "Principaux attributs obligatoires [](https://github.com/jmcollin78/versatile_thermostat/blob/main/documentation/fr/base-attributes.md)",
|
||||
"title": "Ajout d'un nouveau thermostat",
|
||||
"description": "Principaux attributs obligatoires",
|
||||
"data": {
|
||||
"name": "Nom",
|
||||
"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",
|
||||
"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",
|
||||
"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_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"
|
||||
"use_presence_feature": "Avec détection de présence"
|
||||
}
|
||||
},
|
||||
"type": {
|
||||
"title": "Entité(s) liée(s) - {name}",
|
||||
"description": "Attributs de(s) l'entité(s) liée(s) [](https://github.com/jmcollin78/versatile_thermostat/blob/main/documentation/fr/over-switch.md#configuration)",
|
||||
"title": "Entité(s) liée(s)",
|
||||
"description": "Attributs de(s) l'entité(s) liée(s)",
|
||||
"data": {
|
||||
"underlying_entity_ids": "Les équipements à controller",
|
||||
"heater_keep_alive": "keep-alive (sec)",
|
||||
"heater_entity_id": "1er radiateur",
|
||||
"heater_entity2_id": "2ème radiateur",
|
||||
"heater_entity3_id": "3ème radiateur",
|
||||
"heater_entity4_id": "4ème radiateur",
|
||||
"proportional_function": "Algorithme",
|
||||
"ac_mode": "AC mode ?",
|
||||
"auto_regulation_mode": "Auto-régulation",
|
||||
"auto_regulation_dtemp": "Seuil de régulation",
|
||||
"auto_regulation_periode_min": "Période minimale de régulation",
|
||||
"auto_regulation_use_device_temp": "Compenser la température interne du sous-jacent",
|
||||
"inverse_switch_command": "Inverser la commande",
|
||||
"auto_fan_mode": " Auto ventilation mode",
|
||||
"on_command_text": "Personnalisation des commandes d'allumage",
|
||||
"vswitch_on_command": "Commande d'allumage (optionnel)",
|
||||
"off_command_text": "Personnalisation des commandes d'extinction",
|
||||
"vswitch_off_command": "Commande d'extinction (optionnel)"
|
||||
"climate_entity_id": "Thermostat sous-jacent",
|
||||
"climate_entity2_id": "2ème thermostat sous-jacent",
|
||||
"climate_entity3_id": "3ème thermostat sous-jacent",
|
||||
"climate_entity4_id": "4ème thermostat sous-jacent",
|
||||
"ac_mode": "AC mode ?"
|
||||
},
|
||||
"data_description": {
|
||||
"underlying_entity_ids": "La liste des équipements qui seront controlés par ce VTherm",
|
||||
"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.",
|
||||
"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",
|
||||
"proportional_function": "Algorithme à utiliser (Seul TPI est disponible pour l'instant)",
|
||||
"ac_mode": "Utilisation du mode Air Conditionné (AC)",
|
||||
"auto_regulation_mode": "Utilisation de l'auto-régulation faite par VTherm",
|
||||
"auto_regulation_dtemp": "Le seuil en ° (ou % pour les vannes) 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",
|
||||
"auto_regulation_use_device_temp": "Compenser la temperature interne du sous-jacent pour accélérer l'auto-régulation",
|
||||
"inverse_switch_command": "Inverse la commande du switch pour une installation avec fil pilote et diode",
|
||||
"auto_fan_mode": "Active la ventilation automatiquement en cas d'écart important",
|
||||
"on_command_text": "Pour les sous-jacents de type `select` ou `climate`"
|
||||
"climate_entity_id": "Entity id du thermostat sous-jacent",
|
||||
"climate_entity2_id": "Entity id du 2ème thermostat sous-jacent",
|
||||
"climate_entity3_id": "Entity id du 3ème thermostat sous-jacent",
|
||||
"climate_entity4_id": "Entity id du 4ème thermostat sous-jacent",
|
||||
"ac_mode": "Utilisation du mode Air Conditionné (AC)"
|
||||
}
|
||||
},
|
||||
"tpi": {
|
||||
"title": "TPI - {name}",
|
||||
"description": "Attributs de l'algo Time Proportional Integral [](https://github.com/jmcollin78/versatile_thermostat/blob/main/documentation/fr/algorithms.md#lalgorithme-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"
|
||||
}
|
||||
},
|
||||
"presets": {
|
||||
"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 [](https://github.com/jmcollin78/versatile_thermostat/blob/main/documentation/fr/feature-presets.md)",
|
||||
"title": "Presets",
|
||||
"description": "Pour chaque preset, donnez la température cible (0 pour ignorer le preset)",
|
||||
"data": {
|
||||
"use_presets_central_config": "Utiliser la configuration des pré-réglages centrale"
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"window": {
|
||||
"title": "Ouverture - {name}",
|
||||
"description": "Coupe le radiateur si l'ouverture est ouverte.\nLaissez l'id d'entité vide pour utiliser la détection automatique [](https://github.com/jmcollin78/versatile_thermostat/blob/main/documentation/fr/feature-window.md)",
|
||||
"title": "Gestion d'une ouverture",
|
||||
"description": "Coupe le radiateur si l'ouverture est ouverte.\nLaissez l'entity id vide si non utilisé.",
|
||||
"data": {
|
||||
"window_sensor_entity_id": "Détecteur d'ouverture (entity id)",
|
||||
"window_delay": "Délai avant extinction (secondes)",
|
||||
"window_off_delay": "Délai de prise compte à la fermeture (secondes)",
|
||||
"window_auto_open_threshold": "Seuil haut de chute de température pour la détection automatique (en °/heure)",
|
||||
"window_auto_close_threshold": "Seuil bas de chute de température pour la fin de détection automatique (en °/heure)",
|
||||
"window_auto_max_duration": "Durée maximum d'une extinction automatique (en min)",
|
||||
"use_window_central_config": "Utiliser la configuration centrale des ouvertures",
|
||||
"window_action": "Action"
|
||||
"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)"
|
||||
},
|
||||
"data_description": {
|
||||
"window_sensor_entity_id": "Laissez vide si vous n'avez de détecteur et pour utiliser la détection automatique",
|
||||
"window_sensor_entity_id": "Laissez vide si vous n'avez de détecteur",
|
||||
"window_delay": "Le délai (en secondes) avant que le changement du détecteur soit pris en compte",
|
||||
"window_off_delay": "Le délai (en secondes) avant que le changement du détecteur soit pris en compte lors de la détection d'une fermeture. Laissez vide pour utiliser le même délai à l'ouveture et à la fermeture",
|
||||
"window_auto_open_threshold": "Valeur recommandée: entre 3 et 10. Laissez vide si vous n'utilisez pas la détection automatique",
|
||||
"window_auto_open_threshold": "Valeur recommandée: entre 0.05 et 0.1. 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",
|
||||
"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"
|
||||
"window_auto_max_duration": "Valeur recommandée: 60 (1 heure). Laissez vide si vous n'utilisez pas la détection automatique"
|
||||
}
|
||||
},
|
||||
"motion": {
|
||||
"title": "Mouvement - {name}",
|
||||
"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 [](https://github.com/jmcollin78/versatile_thermostat/blob/main/documentation/fr/feature-motion.md)",
|
||||
"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.",
|
||||
"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 sans mouvement",
|
||||
"use_motion_central_config": "Utiliser la condfiguration centrale du mouvement"
|
||||
},
|
||||
"data_description": {
|
||||
"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_sensor_entity_id": "Détecteur de mouvement entity id",
|
||||
"motion_delay": "Délai avant changement (seconds)",
|
||||
"motion_preset": "Preset à utiliser si 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"
|
||||
"no_motion_preset": "Preset à utiliser si pas de mouvement détecté"
|
||||
}
|
||||
},
|
||||
"power": {
|
||||
"title": "Puissance - {name}",
|
||||
"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) [](https://github.com/jmcollin78/versatile_thermostat/blob/main/documentation/fr/feature-power.md)",
|
||||
"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).",
|
||||
"data": {
|
||||
"power_sensor_entity_id": "Puissance totale",
|
||||
"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",
|
||||
"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"
|
||||
"power_temp": "Température si délestaqe"
|
||||
}
|
||||
},
|
||||
"presence": {
|
||||
"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'absence [](https://github.com/jmcollin78/versatile_thermostat/blob/main/documentation/fr/feature-presence.md)",
|
||||
"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.",
|
||||
"data": {
|
||||
"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"
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"advanced": {
|
||||
"title": "Avancés - {name}",
|
||||
"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 [](https://github.com/jmcollin78/versatile_thermostat/blob/main/documentation/fr/feature-advanced.md)",
|
||||
"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.",
|
||||
"data": {
|
||||
"minimal_activation_delay": "Délai minimal d'activation",
|
||||
"minimal_deactivation_delay": "Délai de désactivation minimal",
|
||||
"safety_delay_min": "Délai maximal entre 2 mesures de températures",
|
||||
"safety_min_on_percent": "Pourcentage minimal de puissance",
|
||||
"safety_default_on_percent": "Pourcentage de puissance a utiliser en mode securité",
|
||||
"use_advanced_central_config": "Utiliser la configuration centrale avancée"
|
||||
"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é"
|
||||
},
|
||||
"data_description": {
|
||||
"minimal_activation_delay": "Délai en secondes en-dessous duquel l'équipement ne sera pas activé",
|
||||
"minimal_deactivation_delay": "Délai en secondes en-dessous duquel l'équipement se laissé actif",
|
||||
"safety_delay_min": "Délai maximal autorisé en minutes entre 2 mesures de températures. Au-dessus de ce délai, le thermostat se mettra en position de sécurité",
|
||||
"safety_min_on_percent": "Seuil minimal de pourcentage de chauffage en-dessous duquel le préréglage sécurité ne sera jamais activé",
|
||||
"safety_default_on_percent": "Valeur par défaut pour le pourcentage de chauffage en mode sécurité. Mettre 0 pour éteindre le radiateur en mode sécurité",
|
||||
"use_advanced_central_config": "Cochez pour utiliser la configuration centrale avancée. Décochez et saisissez les attributs pour utiliser une configuration spécifique avancée"
|
||||
}
|
||||
},
|
||||
"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` [](https://github.com/jmcollin78/versatile_thermostat/blob/main/documentation/fr/feature-central-boiler.md)",
|
||||
"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]"
|
||||
}
|
||||
},
|
||||
"valve_regulation": {
|
||||
"title": "Auto-régulation par vanne - {name}",
|
||||
"description": "Configuration de l'auto-régulation par controle direct de la vanne [](https://github.com/jmcollin78/versatile_thermostat/blob/main/documentation/fr/self-regulation.md#auto-régulation-par-contrôle-direct-de-la-vanne)",
|
||||
"data": {
|
||||
"offset_calibration_entity_ids": "Entités de 'calibrage du décalage''",
|
||||
"opening_degree_entity_ids": "Entités 'ouverture de vanne'",
|
||||
"closing_degree_entity_ids": "Entités 'fermeture de la vanne'",
|
||||
"proportional_function": "Algorithme",
|
||||
"min_opening_degrees": "Ouvertures minimales"
|
||||
},
|
||||
"data_description": {
|
||||
"offset_calibration_entity_ids": "La liste des entités 'calibrage du décalage' (offset calibration). Configurez le si votre TRV possède cette fonction pour une meilleure régulation. Il doit y en avoir une par entité climate sous-jacente",
|
||||
"opening_degree_entity_ids": "La liste des entités 'ouverture de vanne'. Il doit y en avoir une par entité climate sous-jacente",
|
||||
"closing_degree_entity_ids": "La liste des entités 'fermeture de la vanne'. Configurez le si votre TRV possède cette fonction pour une meilleure régulation. Il doit y en avoir une par entité climate sous-jacente",
|
||||
"proportional_function": "Algorithme à utiliser (seulement TPI est disponible)",
|
||||
"min_opening_degrees": "Une liste séparée par des virgules de minimum d'ouverture. Valeur par défaut : 0. Exemple : 20, 25, 30"
|
||||
"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é"
|
||||
}
|
||||
}
|
||||
},
|
||||
"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.",
|
||||
"no_central_config": "Vous ne pouvez pas cocher 'Utiliser la configuration centrale' car aucune configuration centrale n'a été trouvée. Vous devez créer un Versatile Thermostat de type 'Central Configuration' pour pouvoir l'utiliser.",
|
||||
"service_configuration_format": "Mauvais format de la configuration du service",
|
||||
"valve_regulation_nb_entities_incorrect": "Le nombre d'entités pour la régulation par vanne doit être égal au nombre d'entité sous-jacentes",
|
||||
"min_opening_degrees_format": "Une liste d'entiers positifs séparés par des ',' est attendu. Exemple : 20, 25, 30",
|
||||
"vswitch_configuration_incorrect": "La configuration de la personnalisation des commandes est incorrecte. Elle est obligatoire pour les sous-jacents non switch et le format doit être 'service_name[/attribut:valeur]'. Plus d'informations dans le README."
|
||||
"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."
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "Le device est déjà configuré"
|
||||
@@ -513,54 +293,8 @@
|
||||
"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 vanne"
|
||||
}
|
||||
},
|
||||
"auto_regulation_mode": {
|
||||
"options": {
|
||||
"auto_regulation_slow": "Lente",
|
||||
"auto_regulation_strong": "Forte",
|
||||
"auto_regulation_medium": "Moyenne",
|
||||
"auto_regulation_light": "Légère",
|
||||
"auto_regulation_expert": "Expert",
|
||||
"auto_regulation_none": "Aucune",
|
||||
"auto_regulation_valve": "Contrôle direct de la vanne"
|
||||
}
|
||||
},
|
||||
"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"
|
||||
"thermostat_over_climate": "Thermostat sur un autre thermostat"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -571,60 +305,12 @@
|
||||
"preset_mode": {
|
||||
"state": {
|
||||
"power": "Délestage",
|
||||
"safety": "Sécurité",
|
||||
"none": "Manuel",
|
||||
"frost": "Hors Gel"
|
||||
"security": "Sécurité",
|
||||
"none": "Manuel"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"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,40 +29,24 @@
|
||||
"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",
|
||||
"climate_entity3_id": "Terzo termostato",
|
||||
"climate_entity4_id": "Quarto termostato",
|
||||
"ac_mode": "AC mode ?",
|
||||
"valve_entity_id": "Prima valvola",
|
||||
"valve_entity2_id": "Seconda valvolao",
|
||||
"valve_entity3_id": "Terza valvola",
|
||||
"valve_entity4_id": "Quarta valvola",
|
||||
"auto_regulation_mode": "Autoregolamentazione",
|
||||
"inverse_switch_command": "Comando inverso",
|
||||
"auto_fan_mode": "Auto fan mode"
|
||||
"climate_entity_id": "Termostato sottostante",
|
||||
"climate_entity2_id": "Secundo termostato sottostante",
|
||||
"climate_entity3_id": "Terzo termostato sottostante",
|
||||
"climate_entity4_id": "Quarto termostato sottostante",
|
||||
"ac_mode": "AC 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",
|
||||
"climate_entity3_id": "Entity id del terzo termostato",
|
||||
"climate_entity4_id": "Entity id del quarto termostato",
|
||||
"ac_mode": "Utilizzare la modalità AC (Air Conditioned) ?",
|
||||
"valve_entity_id": "Entity id della prima valvola",
|
||||
"valve_entity2_id": "Entity id della seconda valvola",
|
||||
"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",
|
||||
"auto_fan_mode": "Automatically activate fan when huge heating/cooling is necessary"
|
||||
"climate_entity_id": "Entity id del termostato sottostante",
|
||||
"climate_entity2_id": "Entity id del secundo termostato sottostante",
|
||||
"climate_entity3_id": "Entity id del terzo termostato sottostante",
|
||||
"climate_entity4_id": "Entity id del quarto termostato sottostante",
|
||||
"ac_mode": "Utilizzare la modalità AC (Air Conditioned) ?"
|
||||
}
|
||||
},
|
||||
"tpi": {
|
||||
@@ -80,7 +64,6 @@
|
||||
"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)"
|
||||
@@ -92,14 +75,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 °/ora)",
|
||||
"window_auto_close_threshold": "Soglia di aumento della temperatura per la fine del rilevamento automatico (in °/ora)",
|
||||
"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_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 3 e 10. Lasciare vuoto se il rilevamento automatico della finestra aperta non è utilizzato",
|
||||
"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 (un'ora). Lasciare vuoto se il rilevamento automatico della finestra aperta non è utilizzato"
|
||||
}
|
||||
@@ -110,7 +93,6 @@
|
||||
"data": {
|
||||
"motion_sensor_entity_id": "Entity id sensore di movimento",
|
||||
"motion_delay": "Ritardo in secondi prima che il rilevamento del sensore sia preso in considerazione",
|
||||
"motion_off_delay": "Ritardo in secondi di disattivazione prima che del sensore sia preso in considerazione",
|
||||
"motion_preset": "Preset da utilizzare quando viene rilevato il movimento",
|
||||
"no_motion_preset": "Preset da utilizzare quando non viene rilevato il movimento"
|
||||
}
|
||||
@@ -132,7 +114,6 @@
|
||||
"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)"
|
||||
@@ -143,15 +124,15 @@
|
||||
"description": "Configurazione avanzata dei parametri. Lasciare i valori predefiniti se non conoscete cosa state modificando.\nQuesti parametri possono determinare una pessima gestione della temperatura e della potenza.",
|
||||
"data": {
|
||||
"minimal_activation_delay": "Ritardo minimo di accensione",
|
||||
"safety_delay_min": "Ritardo di sicurezza (in minuti)",
|
||||
"safety_min_on_percent": "Percentuale minima di potenza per la modalità di sicurezza",
|
||||
"safety_default_on_percent": "Percentuale di potenza per la modalità di sicurezza"
|
||||
"security_delay_min": "Ritardo di sicurezza (in minuti)",
|
||||
"security_min_on_percent": "Percentuale minima di potenza per la modalità di sicurezza",
|
||||
"security_default_on_percent": "Percentuale di potenza per la modalità di sicurezza"
|
||||
},
|
||||
"data_description": {
|
||||
"minimal_activation_delay": "Ritardo in secondi al di sotto del quale l'apparecchiatura non verrà attivata",
|
||||
"safety_delay_min": "Ritardo massimo consentito in minuti tra due misure di temperatura. Al di sopra di questo ritardo, il termostato passerà allo stato di sicurezza",
|
||||
"safety_min_on_percent": "Soglia percentuale minima di riscaldamento al di sotto della quale il preset di sicurezza non verrà mai attivato",
|
||||
"safety_default_on_percent": "Valore percentuale predefinito della potenza di riscaldamento nella modalità di sicurezza. Impostare a 0 per spegnere il riscaldatore nella modalità di sicurezza"
|
||||
"security_delay_min": "Ritardo massimo consentito in minuti tra due misure di temperatura. Al di sopra di questo ritardo, il termostato passerà allo stato di sicurezza",
|
||||
"security_min_on_percent": "Soglia percentuale minima di riscaldamento al di sotto della quale il preset di sicurezza non verrà mai attivato",
|
||||
"security_default_on_percent": "Valore percentuale predefinito della potenza di riscaldamento nella modalità di sicurezza. Impostare a 0 per spegnere il riscaldatore nella modalità di sicurezza"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -187,46 +168,30 @@
|
||||
},
|
||||
"type": {
|
||||
"title": "Entità collegate",
|
||||
"description": "Parametri entità collegate",
|
||||
"description": "Attributi delle entità collegate",
|
||||
"data": {
|
||||
"heater_entity_id": "Primo riscaldatore",
|
||||
"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",
|
||||
"heater_entity_id": "Interruttore riscaldatore",
|
||||
"heater_entity2_id": "Secondo interruttore riscaldatore",
|
||||
"heater_entity3_id": "Terzo interruttore riscaldatore",
|
||||
"heater_entity4_id": "Quarto interruttore riscaldatore",
|
||||
"proportional_function": "Algoritmo",
|
||||
"climate_entity_id": "Primo termostato",
|
||||
"climate_entity2_id": "Secondo termostato",
|
||||
"climate_entity3_id": "Terzo termostato",
|
||||
"climate_entity4_id": "Quarto termostato",
|
||||
"ac_mode": "AC mode ?",
|
||||
"valve_entity_id": "Prima valvola",
|
||||
"valve_entity2_id": "Seconda valvola",
|
||||
"valve_entity3_id": "Terza valvola",
|
||||
"valve_entity4_id": "Quarta valvola",
|
||||
"auto_regulation_mode": "Autoregolamentazione",
|
||||
"inverse_switch_command": "Comando inverso",
|
||||
"auto_fan_mode": "Auto fan mode"
|
||||
"climate_entity_id": "Termostato sottostante",
|
||||
"climate_entity2_id": "Secundo termostato sottostante",
|
||||
"climate_entity3_id": "Terzo termostato sottostante",
|
||||
"climate_entity4_id": "Quarto termostato sottostante",
|
||||
"ac_mode": "AC 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",
|
||||
"climate_entity3_id": "Entity id del terzo termostato",
|
||||
"climate_entity4_id": "Entity id del quarto termostato",
|
||||
"ac_mode": "Utilizzare la modalità AC (Air Conditioned) ?",
|
||||
"valve_entity_id": "Entity id della prima valvola",
|
||||
"valve_entity2_id": "Entity id della seconda valvola",
|
||||
"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",
|
||||
"auto_fan_mode": "Automatically activate fan when huge heating/cooling is necessary"
|
||||
"climate_entity_id": "Entity id del termostato sottostante",
|
||||
"climate_entity2_id": "Entity id del secundo termostato sottostante",
|
||||
"climate_entity3_id": "Entity id del terzo termostato sottostante",
|
||||
"climate_entity4_id": "Entity id del quarto termostato sottostante",
|
||||
"ac_mode": "Utilizzare la modalità AC (Air Conditioned) ?"
|
||||
}
|
||||
},
|
||||
"tpi": {
|
||||
@@ -244,7 +209,6 @@
|
||||
"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)"
|
||||
@@ -256,14 +220,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 °/ora)",
|
||||
"window_auto_close_threshold": "Soglia di aumento della temperatura per la fine del rilevamento automatico (in °/ora)",
|
||||
"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_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 3 e 10. Lasciare vuoto se il rilevamento automatico della finestra aperta non è utilizzato",
|
||||
"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 (un'ora). Lasciare vuoto se il rilevamento automatico della finestra aperta non è utilizzato"
|
||||
}
|
||||
@@ -274,7 +238,6 @@
|
||||
"data": {
|
||||
"motion_sensor_entity_id": "Entity id sensore di movimento",
|
||||
"motion_delay": "Ritardo in secondi prima che il rilevamento del sensore sia preso in considerazione",
|
||||
"motion_off_delay": "Ritardo in secondi di disattivazione prima che del sensore sia preso in considerazione",
|
||||
"motion_preset": "Preset da utilizzare quando viene rilevato il movimento",
|
||||
"no_motion_preset": "Preset da utilizzare quando non viene rilevato il movimento"
|
||||
}
|
||||
@@ -296,7 +259,6 @@
|
||||
"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)"
|
||||
@@ -307,15 +269,15 @@
|
||||
"description": "Configurazione avanzata dei parametri. Lasciare i valori predefiniti se non conoscete cosa state modificando.\nQuesti parametri possono determinare una pessima gestione della temperatura e della potenza.",
|
||||
"data": {
|
||||
"minimal_activation_delay": "Ritardo minimo di accensione",
|
||||
"safety_delay_min": "Ritardo di sicurezza (in minuti)",
|
||||
"safety_min_on_percent": "Percentuale minima di potenza per la modalità di sicurezza",
|
||||
"safety_default_on_percent": "Percentuale di potenza per la modalità di sicurezza"
|
||||
"security_delay_min": "Ritardo di sicurezza (in minuti)",
|
||||
"security_min_on_percent": "Percentuale minima di potenza per la modalità di sicurezza",
|
||||
"security_default_on_percent": "Percentuale di potenza per la modalità di sicurezza"
|
||||
},
|
||||
"data_description": {
|
||||
"minimal_activation_delay": "Ritardo in secondi al di sotto del quale l'apparecchiatura non verrà attivata",
|
||||
"safety_delay_min": "Ritardo massimo consentito in minuti tra due misure di temperatura. Al di sopra di questo ritardo, il termostato passerà allo stato di sicurezza",
|
||||
"safety_min_on_percent": "Soglia percentuale minima di riscaldamento al di sotto della quale il preset di sicurezza non verrà mai attivato",
|
||||
"safety_default_on_percent": "Valore percentuale predefinito della potenza di riscaldamento nella modalità di sicurezza. Impostare a 0 per spegnere il riscaldatore nella modalità di sicurezza"
|
||||
"security_delay_min": "Ritardo massimo consentito in minuti tra due misure di temperatura. Al di sopra di questo ritardo, il termostato passerà allo stato di sicurezza",
|
||||
"security_min_on_percent": "Soglia percentuale minima di riscaldamento al di sotto della quale il preset di sicurezza non verrà mai attivato",
|
||||
"security_default_on_percent": "Valore percentuale predefinito della potenza di riscaldamento nella modalità di sicurezza. Impostare a 0 per spegnere il riscaldatore nella modalità di sicurezza"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -332,27 +294,7 @@
|
||||
"thermostat_type": {
|
||||
"options": {
|
||||
"thermostat_over_switch": "Termostato su un interruttore",
|
||||
"thermostat_over_climate": "Termostato su un climatizzatore",
|
||||
"thermostat_over_valve": "Termostato su una valvola"
|
||||
}
|
||||
},
|
||||
"auto_regulation_mode": {
|
||||
"options": {
|
||||
"auto_regulation_slow": "Lento",
|
||||
"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"
|
||||
"thermostat_over_climate": "Termostato sopra un altro termostato"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -364,8 +306,7 @@
|
||||
"state": {
|
||||
"power": "Ripartizione",
|
||||
"security": "Sicurezza",
|
||||
"none": "Manuale",
|
||||
"frost": "Gelo"
|
||||
"none": "Manuale"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,581 +0,0 @@
|
||||
{
|
||||
"title": "Všestranná konfigurácia termostatu",
|
||||
"config": {
|
||||
"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_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",
|
||||
"description": "Atribúty prepojených entít",
|
||||
"data": {
|
||||
"heater_entity_id": "1. 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": "1. základná klíma",
|
||||
"climate_entity2_id": "2. základná klíma",
|
||||
"climate_entity3_id": "3. základná klíma",
|
||||
"climate_entity4_id": "4. základná klíma",
|
||||
"ac_mode": "AC režim",
|
||||
"valve_entity_id": "1. ventil číslo",
|
||||
"valve_entity2_id": "2. ventil číslo",
|
||||
"valve_entity3_id": "3. ventil číslo",
|
||||
"valve_entity4_id": "4. ventil číslo",
|
||||
"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",
|
||||
"climate_entity3_id": "3. základné identifikačné číslo klimatickej entity",
|
||||
"climate_entity4_id": "4. základné identifikačné číslo klimatickej entity",
|
||||
"ac_mode": "Použite režim klimatizácie (AC)",
|
||||
"valve_entity_id": "1. ventil číslo entity id",
|
||||
"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": "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",
|
||||
"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",
|
||||
"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": {
|
||||
"use_presets_central_config": "Použite konfiguráciu centrálnych predvolieb"
|
||||
}
|
||||
},
|
||||
"window": {
|
||||
"title": "Správa okien",
|
||||
"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 °/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 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",
|
||||
"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",
|
||||
"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",
|
||||
"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",
|
||||
"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",
|
||||
"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",
|
||||
"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": "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": {
|
||||
"title": "Pokročilé parametre",
|
||||
"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",
|
||||
"safety_delay_min": "Bezpečnostné oneskorenie (v minútach)",
|
||||
"safety_min_on_percent": "Minimálne percento výkonu na aktiváciu bezpečnostného režimu",
|
||||
"safety_default_on_percent": "Percento výkonu na použitie v bezpečnostnom režime",
|
||||
"use_advanced_central_config": "Použite centrálnu rozšírenú konfiguráciu"
|
||||
},
|
||||
"data_description": {
|
||||
"minimal_activation_delay": "Oneskorenie v sekundách, pri ktorom sa zariadenie neaktivuje",
|
||||
"safety_delay_min": "Maximálne povolené oneskorenie v minútach medzi dvoma meraniami teploty. Po uplynutí tohto oneskorenia sa termostat prepne do bezpečnostného vypnutého stavu",
|
||||
"safety_min_on_percent": "Minimálna percentuálna hodnota ohrevu pre aktiváciu prednastavenej bezpečnosti. Pod týmto percentom výkonu termostat neprejde do prednastavenia zabezpečenia",
|
||||
"safety_default_on_percent": "Predvolená percentuálna hodnota vykurovacieho výkonu v bezpečnostnej predvoľbe. Nastavte na 0, ak chcete vypnúť ohrievač v zabezpečenom stave",
|
||||
"use_advanced_central_config": "Začiarknite, ak chcete použiť centrálnu rozšírenú konfiguráciu. Zrušte začiarknutie, ak chcete použiť špecifickú rozšírenú konfiguráciu pre tento VTherm"
|
||||
}
|
||||
}
|
||||
},
|
||||
"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",
|
||||
"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é"
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"flow_title": "Všestranná konfigurácia termostatu",
|
||||
"step": {
|
||||
"user": {
|
||||
"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_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 - {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",
|
||||
"climate_entity3_id": "3. základná klíma",
|
||||
"climate_entity4_id": "4. základná klíma",
|
||||
"ac_mode": "AC režim",
|
||||
"valve_entity_id": "1. ventil číslo",
|
||||
"valve_entity2_id": "2. ventil číslo",
|
||||
"valve_entity3_id": "3. ventil číslo",
|
||||
"valve_entity4_id": "4. ventil číslo",
|
||||
"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",
|
||||
"climate_entity3_id": "3. základný identifikátor klimatickej entity",
|
||||
"climate_entity4_id": "4. základný identifikátor klimatickej entity",
|
||||
"ac_mode": "Použite režim klimatizácie (AC)",
|
||||
"valve_entity_id": "1. ventil číslo entity id",
|
||||
"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": "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 - {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",
|
||||
"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 - {name}",
|
||||
"description": "Pre každú predvoľbu zadajte cieľovú teplotu (0, ak chcete predvoľbu ignorovať)",
|
||||
"data": {
|
||||
"use_presets_central_config": "Použite konfiguráciu centrálnych predvolieb"
|
||||
}
|
||||
},
|
||||
"window": {
|
||||
"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 °/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 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",
|
||||
"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 - {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",
|
||||
"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",
|
||||
"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 - {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",
|
||||
"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": "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": "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 - {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",
|
||||
"safety_delay_min": "Bezpečnostné oneskorenie (v minútach)",
|
||||
"safety_min_on_percent": "Minimálne percento výkonu pre bezpečnostný režim",
|
||||
"safety_default_on_percent": "Percento výkonu na použitie v bezpečnostnom režime",
|
||||
"use_advanced_central_config": "Použite centrálnu rozšírenú konfiguráciu"
|
||||
},
|
||||
"data_description": {
|
||||
"minimal_activation_delay": "Oneskorenie v sekundách, pri ktorom sa zariadenie neaktivuje",
|
||||
"safety_delay_min": "Maximálne povolené oneskorenie v minútach medzi dvoma meraniami teploty. Po uplynutí tohto oneskorenia sa termostat prepne do bezpečnostného vypnutého stavu",
|
||||
"safety_min_on_percent": "Minimálna percentuálna hodnota ohrevu pre aktiváciu prednastavenej bezpečnosti. Pod týmto percentom výkonu termostat neprejde do prednastavenia zabezpečenia",
|
||||
"safety_default_on_percent": "Predvolená percentuálna hodnota vykurovacieho výkonu v bezpečnostnej predvoľbe. Nastavte na 0, ak chcete vypnúť ohrievač v zabezpečenom stave",
|
||||
"use_advanced_central_config": "Začiarknite, ak chcete použiť centrálnu rozšírenú konfiguráciu. Zrušte začiarknutie, ak chcete použiť špecifickú rozšírenú konfiguráciu pre tento VTherm"
|
||||
}
|
||||
}
|
||||
},
|
||||
"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",
|
||||
"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é"
|
||||
}
|
||||
},
|
||||
"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": "Termostat nad ventilom"
|
||||
}
|
||||
},
|
||||
"auto_regulation_mode": {
|
||||
"options": {
|
||||
"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"
|
||||
}
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"climate": {
|
||||
"versatile_thermostat": {
|
||||
"state_attributes": {
|
||||
"preset_mode": {
|
||||
"state": {
|
||||
"power": "Vyradenie",
|
||||
"security": "Zabezpečenie",
|
||||
"none": "Manuálne"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,320 +0,0 @@
|
||||
""" The API of Versatile Thermostat"""
|
||||
|
||||
import logging
|
||||
from datetime import datetime
|
||||
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,
|
||||
CONF_MAX_ON_PERCENT,
|
||||
NowClass,
|
||||
)
|
||||
|
||||
from .central_feature_power_manager import CentralFeaturePowerManager
|
||||
|
||||
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()
|
||||
self._max_on_percent = None
|
||||
self._central_power_manager = CentralFeaturePowerManager(
|
||||
VersatileThermostatAPI._hass, self
|
||||
)
|
||||
|
||||
# the current time (for testing purpose)
|
||||
self._now = None
|
||||
|
||||
def find_central_configuration(self):
|
||||
"""Search for a central configuration"""
|
||||
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)
|
||||
|
||||
self._max_on_percent = config.get(CONF_MAX_ON_PERCENT)
|
||||
if self._max_on_percent:
|
||||
_LOGGER.debug(
|
||||
"We have found max_on_percent setting %s", self._max_on_percent
|
||||
)
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
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:
|
||||
# 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())
|
||||
|
||||
# start listening for the central power manager if not only one vtherm reload
|
||||
if not entry_id:
|
||||
await self.central_power_manager.start_listening()
|
||||
|
||||
async def init_vtherm_preset_with_central(self):
|
||||
"""Init all VTherm presets when the VTherm uses central temperature"""
|
||||
# Initialization of all preset for all VTherm
|
||||
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 max_on_percent(self):
|
||||
"""Get the max_open_percent params"""
|
||||
return self._max_on_percent
|
||||
|
||||
@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
|
||||
|
||||
@property
|
||||
def central_power_manager(self) -> any:
|
||||
"""Returns the central power manager"""
|
||||
return self._central_power_manager
|
||||
|
||||
# For testing purpose
|
||||
def _set_now(self, now: datetime):
|
||||
"""Set the now timestamp. This is only for tests purpose"""
|
||||
self._now = now
|
||||
|
||||
@property
|
||||
def now(self) -> datetime:
|
||||
"""Get now. The local datetime or the overloaded _set_now date"""
|
||||
return self._now if self._now is not None else NowClass.get_now(self._hass)
|
||||
@@ -1,211 +0,0 @@
|
||||
# Some Essential Add-Ons
|
||||
|
||||
- [Some Essential Add-Ons](#some-essential-add-ons)
|
||||
- [the Versatile Thermostat UI Card](#the-versatile-thermostat-ui-card)
|
||||
- [the Scheduler Component!](#the-scheduler-component)
|
||||
- [Regulation curves with Plotly to Fine-Tune Your Thermostat](#regulation-curves-with-plotly-to-fine-tune-your-thermostat)
|
||||
- [Event notification with the AppDaemon NOTIFIER](#event-notification-with-the-appdaemon-notifier)
|
||||
|
||||
## the Versatile Thermostat UI Card
|
||||
A dedicated card for the Versatile Thermostat has been developed (based on Better Thermostat). It is available here: [Versatile Thermostat UI Card](https://github.com/jmcollin78/versatile-thermostat-ui-card) and offers a modern view of all the VTherm statuses:
|
||||
|
||||

|
||||
|
||||
## the Scheduler Component!
|
||||
|
||||
To make the most out of the Versatile Thermostat, I recommend using it with the [Scheduler Component](https://github.com/nielsfaber/scheduler-component). The scheduler component provides climate scheduling based on predefined modes. While this feature is somewhat limited with the generic thermostat, it becomes very powerful when paired with the Versatile Thermostat.
|
||||
|
||||
Assuming you have installed both the Versatile Thermostat and the Scheduler Component, here’s an example:
|
||||
|
||||
In Scheduler, add a schedule:
|
||||
|
||||

|
||||
|
||||
Choose the "Climate" group, select one (or more) entity, pick "MAKE SCHEME," and click next:
|
||||
(You can also choose "SET PRESET," but I prefer "MAKE SCHEME.")
|
||||
|
||||

|
||||
|
||||
Define your mode scheme and save:
|
||||
|
||||

|
||||
|
||||
In this example, I set ECO mode during the night and when no one is home during the day, BOOST in the morning, and COMFORT in the evening.
|
||||
|
||||
I hope this example helps; feel free to share your feedback!
|
||||
|
||||
## Regulation curves with Plotly to Fine-Tune Your Thermostat
|
||||
You can obtain a curve similar to the one shown in [some results](#some-results) using a Plotly graph configuration by leveraging the thermostat's custom attributes described [here](#custom-attributes):
|
||||
|
||||
Replace the values between `[[ ]]` with your own.
|
||||
<details>
|
||||
|
||||
```yaml
|
||||
- type: custom:plotly-graph
|
||||
entities:
|
||||
- entity: '[[climate]]'
|
||||
attribute: temperature
|
||||
yaxis: y1
|
||||
name: Consigne
|
||||
- entity: '[[climate]]'
|
||||
attribute: current_temperature
|
||||
yaxis: y1
|
||||
name: T°
|
||||
- entity: '[[climate]]'
|
||||
attribute: ema_temp
|
||||
yaxis: y1
|
||||
name: Ema
|
||||
- entity: '[[climate]]'
|
||||
attribute: on_percent
|
||||
yaxis: y2
|
||||
name: Power percent
|
||||
fill: tozeroy
|
||||
fillcolor: rgba(200, 10, 10, 0.3)
|
||||
line:
|
||||
color: rgba(200, 10, 10, 0.9)
|
||||
- entity: '[[slope]]'
|
||||
name: Slope
|
||||
fill: tozeroy
|
||||
yaxis: y9
|
||||
fillcolor: rgba(100, 100, 100, 0.3)
|
||||
line:
|
||||
color: rgba(100, 100, 100, 0.9)
|
||||
hours_to_show: 4
|
||||
refresh_interval: 10
|
||||
height: 800
|
||||
config:
|
||||
scrollZoom: true
|
||||
layout:
|
||||
margin:
|
||||
r: 50
|
||||
legend:
|
||||
x: 0
|
||||
'y': 1.2
|
||||
groupclick: togglegroup
|
||||
title:
|
||||
side: top right
|
||||
yaxis:
|
||||
visible: true
|
||||
position: 0
|
||||
yaxis2:
|
||||
visible: true
|
||||
position: 0
|
||||
fixedrange: true
|
||||
range:
|
||||
- 0
|
||||
- 1
|
||||
yaxis9:
|
||||
visible: true
|
||||
fixedrange: false
|
||||
range:
|
||||
- -2
|
||||
- 2
|
||||
position: 1
|
||||
xaxis:
|
||||
rangeselector:
|
||||
'y': 1.1
|
||||
x: 0.7
|
||||
buttons:
|
||||
- count: 1
|
||||
step: hour
|
||||
- count: 12
|
||||
step: hour
|
||||
- count: 1
|
||||
step: day
|
||||
- count: 7
|
||||
step: day
|
||||
```
|
||||
</details>
|
||||
|
||||
Example of curves obtained with Plotly:
|
||||
|
||||

|
||||
|
||||
## Event notification with the AppDaemon NOTIFIER
|
||||
This automation leverages the excellent AppDaemon app named NOTIFIER, developed by Horizon Domotique, demonstrated [here](https://www.youtube.com/watch?v=chJylIK0ASo&ab_channel=HorizonDomotique), and the code is available [here](https://github.com/jlpouffier/home-assistant-config/blob/master/appdaemon/apps/notifier.py). It allows users to be notified of security-related events occurring on any Versatile Thermostat.
|
||||
|
||||
This is a great example of using the notifications described here: [notification](#notifications).
|
||||
<details>
|
||||
|
||||
```yaml
|
||||
alias: Surveillance Mode Sécurité chauffage
|
||||
description: Envoi une notification si un thermostat passe en mode sécurité ou power
|
||||
trigger:
|
||||
- platform: event
|
||||
event_type: versatile_thermostat_security_event
|
||||
id: versatile_thermostat_security_event
|
||||
- platform: event
|
||||
event_type: versatile_thermostat_power_event
|
||||
id: versatile_thermostat_power_event
|
||||
- platform: event
|
||||
event_type: versatile_thermostat_temperature_event
|
||||
id: versatile_thermostat_temperature_event
|
||||
condition: []
|
||||
action:
|
||||
- choose:
|
||||
- conditions:
|
||||
- condition: trigger
|
||||
id: versatile_thermostat_security_event
|
||||
sequence:
|
||||
- event: NOTIFIER
|
||||
event_data:
|
||||
action: send_to_jmc
|
||||
title: >-
|
||||
Radiateur {{ trigger.event.data.name }} - {{
|
||||
trigger.event.data.type }} Sécurité
|
||||
message: >-
|
||||
Le radiateur {{ trigger.event.data.name }} est passé en {{
|
||||
trigger.event.data.type }} sécurité car le thermomètre ne répond
|
||||
plus.\n{{ trigger.event.data }}
|
||||
callback:
|
||||
- title: Stopper chauffage
|
||||
event: stopper_chauffage
|
||||
image_url: /media/local/alerte-securite.jpg
|
||||
click_url: /lovelace-chauffage/4
|
||||
icon: mdi:radiator-off
|
||||
tag: radiateur_security_alerte
|
||||
persistent: true
|
||||
- conditions:
|
||||
- condition: trigger
|
||||
id: versatile_thermostat_power_event
|
||||
sequence:
|
||||
- event: NOTIFIER
|
||||
event_data:
|
||||
action: send_to_jmc
|
||||
title: >-
|
||||
Radiateur {{ trigger.event.data.name }} - {{
|
||||
trigger.event.data.type }} Délestage
|
||||
message: >-
|
||||
Le radiateur {{ trigger.event.data.name }} est passé en {{
|
||||
trigger.event.data.type }} délestage car la puissance max est
|
||||
dépassée.\n{{ trigger.event.data }}
|
||||
callback:
|
||||
- title: Stopper chauffage
|
||||
event: stopper_chauffage
|
||||
image_url: /media/local/alerte-delestage.jpg
|
||||
click_url: /lovelace-chauffage/4
|
||||
icon: mdi:radiator-off
|
||||
tag: radiateur_power_alerte
|
||||
persistent: true
|
||||
- conditions:
|
||||
- condition: trigger
|
||||
id: versatile_thermostat_temperature_event
|
||||
sequence:
|
||||
- event: NOTIFIER
|
||||
event_data:
|
||||
action: send_to_jmc
|
||||
title: >-
|
||||
Le thermomètre du radiateur {{ trigger.event.data.name }} ne
|
||||
répond plus
|
||||
message: >-
|
||||
Le thermomètre du radiateur {{ trigger.event.data.name }} ne
|
||||
répond plus depuis longtemps.\n{{ trigger.event.data }}
|
||||
image_url: /media/local/thermometre-alerte.jpg
|
||||
click_url: /lovelace-chauffage/4
|
||||
icon: mdi:radiator-disabled
|
||||
tag: radiateur_thermometre_alerte
|
||||
persistent: true
|
||||
mode: queued
|
||||
max: 30
|
||||
```
|
||||
</details>
|
||||
@@ -1,67 +0,0 @@
|
||||
# The Different Algorithms Used
|
||||
|
||||
- [The Different Algorithms Used](#the-different-algorithms-used)
|
||||
- [The TPI Algorithm](#the-tpi-algorithm)
|
||||
- [Configuring the TPI Algorithm Coefficients](#configuring-the-tpi-algorithm-coefficients)
|
||||
- [Principle](#principle)
|
||||
- [The Self-Regulation Algorithm (Without Valve Control)](#the-self-regulation-algorithm-without-valve-control)
|
||||
- [The Auto-Start/Stop Function Algorithm](#the-auto-startstop-function-algorithm)
|
||||
|
||||
## The TPI Algorithm
|
||||
|
||||
### Configuring the TPI Algorithm Coefficients
|
||||
|
||||
If you have selected a thermostat of type `over_switch`, `over_valve`, or `over_climate` with self-regulation in `Direct Valve Control` mode and choose the "TPI" option in the menu, you will land on this page:
|
||||
|
||||

|
||||
|
||||
You need to provide:
|
||||
1. the coefficient `coef_int` for the TPI algorithm,
|
||||
2. the coefficient `coef_ext` for the TPI algorithm.
|
||||
|
||||
### Principle
|
||||
|
||||
The TPI algorithm calculates the On vs Off percentage for the radiator at each cycle, using the target temperature, the current room temperature, and the current outdoor temperature. This algorithm is only applicable for Versatile Thermostats operating in `over_switch` and `over_valve` modes.
|
||||
|
||||
The percentage is calculated using this formula:
|
||||
|
||||
on_percent = coef_int * (target_temperature - current_temperature) + coef_ext * (target_temperature - outdoor_temperature)
|
||||
Then, the algorithm ensures that 0 <= on_percent <= 1.
|
||||
|
||||
The default values for `coef_int` and `coef_ext` are `0.6` and `0.01`, respectively. These default values are suitable for a standard well-insulated room.
|
||||
|
||||
When adjusting these coefficients, keep the following in mind:
|
||||
1. **If the target temperature is not reached** after stabilization, increase `coef_ext` (the `on_percent` is too low),
|
||||
2. **If the target temperature is exceeded** after stabilization, decrease `coef_ext` (the `on_percent` is too high),
|
||||
3. **If reaching the target temperature is too slow**, increase `coef_int` to provide more power to the heater,
|
||||
4. **If reaching the target temperature is too fast and oscillations occur** around the target, decrease `coef_int` to provide less power to the radiator.
|
||||
|
||||
In `over_valve` mode, the `on_percent` value is converted to a percentage (0 to 100%) and directly controls the valve's opening level.
|
||||
|
||||
## The Self-Regulation Algorithm (Without Valve Control)
|
||||
|
||||
The self-regulation algorithm can be summarized as follows:
|
||||
|
||||
1. Initialize the target temperature as the VTherm setpoint,
|
||||
2. If self-regulation is enabled:
|
||||
1. Calculate the regulated temperature (valid for a VTherm),
|
||||
2. Use this temperature as the target,
|
||||
3. For each underlying device of the VTherm:
|
||||
1. If "Use Internal Temperature" is checked:
|
||||
1. Calculate the compensation (`trv_internal_temp - room_temp`),
|
||||
2. Add the offset to the target temperature,
|
||||
3. Send the target temperature (= regulated_temp + (internal_temp - room_temp)) to the underlying device.
|
||||
|
||||
## The Auto-Start/Stop Function Algorithm
|
||||
|
||||
The algorithm used in the auto-start/stop function operates as follows:
|
||||
1. If "Enable Auto-Start/Stop" is off, stop here.
|
||||
2. If VTherm is on and in Heating mode, when `error_accumulated` < `-error_threshold` -> turn off and save HVAC mode.
|
||||
3. If VTherm is on and in Cooling mode, when `error_accumulated` > `error_threshold` -> turn off and save HVAC mode.
|
||||
4. If VTherm is off and the saved HVAC mode is Heating, and `current_temperature + slope * dt <= target_temperature`, turn on and set the HVAC mode to the saved mode.
|
||||
5. If VTherm is off and the saved HVAC mode is Cooling, and `current_temperature + slope * dt >= target_temperature`, turn on and set the HVAC mode to the saved mode.
|
||||
6. `error_threshold` is set to `10 (° * min)` for slow detection, `5` for medium, and `2` for fast.
|
||||
|
||||
`dt` is set to `30 min` for slow, `15 min` for medium, and `7 min` for fast detection levels.
|
||||
|
||||
The function is detailed [here](https://github.com/jmcollin78/versatile_thermostat/issues/585).
|
||||
@@ -1,45 +0,0 @@
|
||||
- [Choosing Basic Attributes](#choosing-basic-attributes)
|
||||
- [Choosing the features to Use](#choosing-the-features-to-use)
|
||||
|
||||
# Choosing Basic Attributes
|
||||
|
||||
Select the "Main Attributes" menu.
|
||||
|
||||

|
||||
|
||||
Provide the mandatory main attributes. These attributes are common to all VTherms:
|
||||
1. A name (this will be both the integration's name and the `climate` entity name),
|
||||
2. An entity ID of a temperature sensor that provides the room temperature where the radiator is installed,
|
||||
3. An optional sensor entity providing the last seen date and time of the sensor (`last_seen`). If available, specify it here. It helps prevent safety shutdowns when the temperature is stable, and the sensor stops reporting for a long time (see [here](troubleshooting.md#why-does-my-versatile-thermostat-go-into-safety-mode)),
|
||||
4. A cycle duration in minutes. At each cycle:
|
||||
1. For `over_switch`: VTherm will turn the radiator on/off, modulating the proportion of time it is on,
|
||||
2. For `over_valve`: VTherm will calculate a new valve opening level and send it if it has changed,
|
||||
3. For `over_climate`: The cycle performs basic controls and recalculates the self-regulation coefficients. The cycle may result in a new setpoint sent to underlying devices or a valve opening adjustment in the case of a controllable TRV.
|
||||
5. The equipment's power, which will activate power and energy consumption sensors for the device. If multiple devices are linked to the same VTherm, specify the total maximum power of all devices here. The power unit is not important here. What is important is that all _VTherm_ and all power sensors have the same unit (see: Power shedding feature),
|
||||
6. The option to use additional parameters from centralized configuration:
|
||||
1. Outdoor temperature sensor,
|
||||
2. Minimum/maximum temperature and temperature step size,
|
||||
7. The option to control the thermostat centrally. See [centralized control](#centralized-control),
|
||||
8. A checkbox if this VTherm is used to trigger a central boiler.
|
||||
|
||||
>  _*Notes*_
|
||||
> 1. With the `over_switch` and `over_valve` types, calculations are performed at each cycle. In case of changing conditions, you will need to wait for the next cycle to see a change. For this reason, the cycle should not be too long. **5 minutes is a good value**, but it should be adjusted to your heating type. The greater the inertia, the longer the cycle should be. See [Tuning examples](tuning-examples.md).
|
||||
> 2. If the cycle is too short, the radiator may never reach the target temperature. For example, with a storage heater, it will be unnecessarily activated.
|
||||
|
||||
# Choosing the features to Use
|
||||
|
||||
Select the "Features" menu.
|
||||
|
||||

|
||||
|
||||
Choose the features you want to use for this VTherm:
|
||||
1. **Opening detection** (doors, windows) stops heating when an opening is detected. (see [managing openings](feature-window.md)),
|
||||
2. **Motion detection**: VTherm can adjust the target temperature when motion is detected in the room. (see [motion detection](feature-motion.md)),
|
||||
3. **Power management**: VTherm can stop a device if the power consumption in your home exceeds a threshold. (see [load-shedding management](feature-power.md)),
|
||||
4. **Presence detection**: If you have a sensor indicating presence or absence in your home, you can use it to change the target temperature. See [presence management](feature-presence.md). Note the difference between this function and motion detection: presence is typically used at the home level, while motion detection is more room-specific.
|
||||
5. **Automatic start/stop**: For `over_climate` VTherms only. This function stops a device when VTherm detects it will not be needed for a while. It uses the temperature curve to predict when the device will be needed again and turns it back on at that time. See [automatic start/stop management](feature-auto-start-stop.md).
|
||||
|
||||
>  _*Notes*_
|
||||
> 1. The list of available functions adapts to your VTherm type.
|
||||
> 2. When you enable a function, a new menu entry is added to configure it.
|
||||
> 3. You cannot validate the creation of a VTherm if all parameters for all enabled functions have not been configured.
|
||||
@@ -1,71 +0,0 @@
|
||||
# Choosing a VTherm
|
||||
|
||||
- [Choosing a VTherm](#choosing-a-vtherm)
|
||||
- [Creating a New Versatile Thermostat](#creating-a-new-versatile-thermostat)
|
||||
- [Choosing a VTherm Type](#choosing-a-vtherm-type)
|
||||
- [Centralized configuration](#centralized-configuration)
|
||||
- [VTherm over a switch](#vtherm-over-a-switch)
|
||||
- [VTherm over another thermostat](#vtherm-over-another-thermostat)
|
||||
- [VTherm over a valve](#vtherm-over-a-valve)
|
||||
- [Making the right choice](#making-the-right-choice)
|
||||
- [Reference Article](#reference-article)
|
||||
|
||||
>  _*Notes*_
|
||||
>
|
||||
> There are three ways to work with VTherms:
|
||||
> 1. Each Versatile Thermostat is fully configured independently. Choose this option if you do not want any centralized configuration or management.
|
||||
> 2. Some aspects are configured centrally. For example, you can define the minimum/maximum temperatures, open window detection parameters, etc., at a single central instance. For each VTherm you configure, you can then choose to use the central configuration or override it with custom parameters.
|
||||
> 3. In addition to centralized configuration, all VTherms can be controlled by a single `select` entity called `central_mode`. This feature allows you to stop/start/set frost protection/etc. for all VTherms at once. For each VTherm, you can specify if it is affected by this `central_mode`.
|
||||
|
||||
## Creating a New Versatile Thermostat
|
||||
|
||||
Click on "Add Integration" on the integration page (or click 'Add device' in the integration page)
|
||||
|
||||

|
||||
|
||||
then:
|
||||
|
||||

|
||||
|
||||
The configuration can be modified via the same interface. Simply select the thermostat to modify, press "Configure," and you will be able to change some parameters or settings.
|
||||
|
||||
Follow the configuration steps by selecting the menu option to configure.
|
||||
|
||||
# Choosing a VTherm Type
|
||||
|
||||
## Centralized configuration
|
||||
This option allows you to configure certain repetitive aspects for all VTherms at once, such as:
|
||||
1. Parameters for different algorithms (TPI, open window detection, motion detection, power sensors for your home, presence detection). These parameters apply across all VTherms. You only need to enter them once in `Centralized Configuration`. This configuration does not create a VTherm itself but centralizes parameters that would be tedious to re-enter for each VTherm. Note that you can override these parameters on individual VTherms to specialize them if needed.
|
||||
2. Configuration for controlling a central heating system,
|
||||
3. Certain advanced parameters, such as safety settings.
|
||||
|
||||
## VTherm over a switch
|
||||
This VTherm type controls a switch that turns a radiator on or off. The switch can be a physical switch directly controlling a radiator (often electric) or a virtual switch that can perform any action when turned on or off. The latter type can, for example, control pilot wire switches or DIY pilot wire solutions with diodes. VTherm modulates the proportion of time the radiator is on (`on_percent`) to achieve the desired temperature. If it is cold, it turns on more frequently (up to 100%); if it is warm, it reduces the on time.
|
||||
|
||||
The underlying entities for this type are `switches` or `input_booleans`.
|
||||
|
||||
## VTherm over another thermostat
|
||||
When your device is controlled by a `climate` entity in Home Assistant and you only have access to this, you should use this VTherm type. In this case, VTherm simply adjusts the target temperature of the underlying `climate` entity.
|
||||
|
||||
This type also includes advanced self-regulation features to adjust the setpoint sent to the underlying device, helping to achieve the target temperature faster and mitigating poor internal regulation. For example, if the device's internal thermometer is too close to the heating element, it may incorrectly assume the room is warm while the setpoint is far from being achieved in other areas.
|
||||
|
||||
Since version 6.8, this VTherm type can also regulate directly by controlling the valve. Ideal for controllable TRVs, as Sonoff TRVZB, this type is recommended if you have such devices.
|
||||
|
||||
The underlying entities for this VTherm type are exclusively `climate`.
|
||||
|
||||
## VTherm over a valve
|
||||
If the only entity available to regulate your radiator's temperature is a `number` entity, you should use the `over_valve` type. VTherm adjusts the valve opening based on the difference between the target temperature and the actual room temperature (and the outdoor temperature, if available).
|
||||
|
||||
This type can be used for TRVs without an associated `climate` entity or other DIY solutions exposing a `number` entity.
|
||||
|
||||
# Making the right choice
|
||||
>  _*How to Choose the Type*_
|
||||
> Choosing the correct type is crucial. It cannot be changed later via the configuration interface. To make the right choice, consider the following questions:
|
||||
> 1. **What type of equipment will I control?** Follow this order of preference:
|
||||
> 1. If you have a controllable thermostatic valve (TRV) in Home Assistant through a `number` entity (e.g., a Shelly TRV), choose the `over_valve` type. This is the most direct type and ensures the best regulation.
|
||||
> 2. If you have an electric radiator (with or without a pilot wire) controlled by a `switch` entity to turn it on/off, then the `over_switch` type is preferable. Regulation will be managed by the Versatile Thermostat based on the temperature measured by your thermometer at its placement location.
|
||||
> 3. In all other cases, use the `over_climate` mode. You retain your original `climate` entity, and the Versatile Thermostat "only" controls the on/off state and target temperature of your original thermostat. Regulation is handled by your original thermostat in this case. This mode is particularly suited for all-in-one reversible air conditioning systems exposed as a `climate` entity in Home Assistant. Advanced self-regulation can achieve the setpoint faster by forcing the setpoint or directly controlling the valve when possible.
|
||||
> 2. **What type of regulation do I want?** If the controlled equipment has its own built-in regulation mechanism (e.g., HVAC systems, certain TRVs) and it works well, choose `over_climate`. For TRVs with a controllable valve in Home Assistant, the `over_climate` type with `Direct Valve Control` self-regulation is the best choice.
|
||||
|
||||
# Reference Article
|
||||
For more information on these concepts, refer to this article (in French): https://www.hacf.fr/optimisation-versatile-thermostat/#optimiser-vos-vtherm
|
||||
@@ -1,57 +0,0 @@
|
||||
# Advanced Configuration
|
||||
|
||||
- [Advanced Configuration](#advanced-configuration)
|
||||
- [Advanced Settings](#advanced-settings)
|
||||
- [Minimum Activation Delay](#minimum-activation-delay)
|
||||
- [Safety Mode](#safety-mode)
|
||||
|
||||
These settings refine the thermostat's operation, particularly the safety mechanism for a _VTherm_. Missing temperature sensors (room or outdoor) can pose a risk to your home. For instance, if the temperature sensor gets stuck at 10°C, the `over_climate` or `over_valve` _VTherm_ types will command maximum heating of the underlying devices, which could lead to room overheating or even property damage, at worst resulting in a fire hazard.
|
||||
|
||||
To prevent this, _VTherm_ ensures that thermometers report values regularly. If they don't, the _VTherm_ switches to a special mode called Safety Mode. This mode ensures minimal heating to prevent the opposite risk: a completely unheated home in the middle of winter, for example.
|
||||
|
||||
The challenge lies in that some thermometers—especially battery-operated ones—only send temperature updates when the value changes. It is entirely possible to receive no temperature updates for hours without the thermometer failing. The parameters below allow fine-tuning of the thresholds for activating Safety Mode.
|
||||
|
||||
If your thermometer has a `last seen` attribute indicating the last contact time, you can specify it in the _VTherm_'s main attributes to greatly reduce false Safety Mode activations. See [configuration](base-attributes.md#choosing-base-attributes) and [troubleshooting](troubleshooting.md#why-does-my-versatile-thermostat-switch-to-safety-mode).
|
||||
|
||||
For `over_climate` _VTherms_, which self-regulate, Safety Mode is disabled. In this case, there is no danger, only the risk of an incorrect temperature.
|
||||
|
||||
## Advanced Settings
|
||||
|
||||
The advanced configuration form looks like this:
|
||||
|
||||

|
||||
|
||||
### Minimum Activation Delay
|
||||
|
||||
The first delay (`minimal_activation_delay_sec`) in seconds is the minimum acceptable delay to turn on the heating. If the calculated activation time is shorter than this value, the heating remains off. This parameter only applies to _VTherm_ with cyclic triggering `over_switch`. If the activation time is too short, rapid switching will not allow the device to heat up properly.
|
||||
|
||||
### Minimum Deactivation Delay
|
||||
|
||||
The delay (`minimal_deactivation_delay_sec`) in seconds is the minimum acceptable delay to turn off the heating. If the calculated deactivation time is shorter than this value, the heating remains on.
|
||||
|
||||
### Safety Mode
|
||||
|
||||
The second delay (`safety_delay_min`) is the maximum time between two temperature measurements before the _VTherm_ switches to Safety Mode.
|
||||
|
||||
The third parameter (`safety_min_on_percent`) is the minimum `on_percent` below which Safety Mode will not be activated. This setting prevents activating Safety Mode if the controlled radiator does not heat sufficiently. In this case, there is no physical risk to the home, only the risk of overheating or underheating.
|
||||
Setting this parameter to `0.00` will trigger Safety Mode regardless of the last heating setting, whereas `1.00` will never trigger Safety Mode (effectively disabling the feature). This can be useful to adapt the safety mechanism to your specific needs.
|
||||
|
||||
The fourth parameter (`safety_default_on_percent`) defines the `on_percent` used when the thermostat switches to `security` mode. Setting it to `0` will turn off the thermostat in Safety Mode, while setting it to a value like `0.2` (20%) ensures some heating remains, avoiding a completely frozen home in case of a thermometer failure.
|
||||
|
||||
It is possible to disable Safety Mode triggered by missing data from the outdoor thermometer. Since the outdoor thermometer usually has a minor impact on regulation (depending on your configuration), it might not be critical if it's unavailable. To do this, add the following lines to your `configuration.yaml`:
|
||||
|
||||
```yaml
|
||||
versatile_thermostat:
|
||||
...
|
||||
safety_mode:
|
||||
check_outdoor_sensor: false
|
||||
```
|
||||
|
||||
By default, the outdoor thermometer can trigger Safety Mode if it stops sending data. Remember that Home Assistant must be restarted for these changes to take effect. This setting applies to all _VTherms_ sharing the outdoor thermometer.
|
||||
|
||||
>  _*Notes*_
|
||||
> 1. When the temperature sensor resumes reporting, the preset will be restored to its previous value.
|
||||
> 2. Two temperature sources are required: the indoor and outdoor temperatures. Both must report values, or the thermostat will switch to "security" preset.
|
||||
> 3. An action is available to adjust the three safety parameters. This can help adapt Safety Mode to your needs.
|
||||
> 4. For normal use, `safety_default_on_percent` should be lower than `safety_min_on_percent`.
|
||||
> 5. If you use the Versatile Thermostat UI card (see [here](additions.md#better-with-the-versatile-thermostat-ui-card)), a _VTherm_ in Safety Mode is indicated by a gray overlay showing the faulty thermometer and the time since its last value update: .
|
||||
@@ -1,41 +0,0 @@
|
||||
# Auto-start / Auto-stop
|
||||
|
||||
- [Auto-start / Auto-stop](#auto-start--auto-stop)
|
||||
- [Configure Auto-start/stop](#configure-auto-startstop)
|
||||
- [Usage](#usage)
|
||||
|
||||
This feature allows _VTherm_ to stop an appliance that doesn't need to be on and restart it when conditions require it. This function includes three settings that control how quickly the appliance is stopped and restarted.
|
||||
Exclusively reserved for _VTherm_ of type `over_climate`, it applies to the following use case:
|
||||
1. Your appliance is permanently powered on and consumes electricity even when heating (or cooling) is not needed. This is often the case with heat pumps (_PAC_) that consume power even in standby mode.
|
||||
2. The temperature conditions are such that heating (or cooling) is not needed for a long period: the setpoint is higher (or lower) than the room temperature.
|
||||
3. The temperature rises (or falls), remains stable, or falls (or rises) slowly.
|
||||
|
||||
In such cases, it is preferable to ask the appliance to turn off to avoid unnecessary power consumption in standby mode.
|
||||
|
||||
## Configure Auto-start/stop
|
||||
|
||||
To use this feature, you need to:
|
||||
1. Add the `With auto-start and stop` function in the 'Functions' menu.
|
||||
2. Set the detection level in the 'Auto-start/stop' option that appears when the function is activated. Choose the detection level between 'Slow', 'Medium', and 'Fast'. With the 'Fast' setting, stops and restarts will occur more frequently.
|
||||
|
||||

|
||||
|
||||
The 'Slow' setting allows about 30 minutes between a stop and a restart,
|
||||
The 'Medium' setting sets the threshold to about 15 minutes, and the 'Fast' setting puts it at 7 minutes.
|
||||
|
||||
Note that these are not absolute settings since the algorithm takes into account the slope of the room temperature curve to respond accordingly. It is still possible that a restart occurs shortly after a stop if the temperature drops significantly.
|
||||
|
||||
## Usage
|
||||
|
||||
Once the function is configured, you will now have a new `switch` type entity that allows you to enable or disable auto-start/stop without modifying the configuration. This entity is available on the _VTherm_ device and is named `switch.<name>_enable_auto_start_stop`.
|
||||
|
||||

|
||||
|
||||
Check the box to allow auto-start and auto-stop, and leave it unchecked to disable the feature.
|
||||
|
||||
Note: The auto-start/stop function will only turn a _VTherm_ back on if it was turned off by this function. This prevents unwanted or unexpected activations. Naturally, the off state is preserved even after a Home Assistant restart.
|
||||
|
||||
>  _*Notes*_
|
||||
> 1. The detection algorithm is described [here](algorithms.md#auto-startstop-algorithm).
|
||||
> 2. Some appliances (boilers, underfloor heating, _PAC_, etc.) may not like being started/stopped too frequently. If that's the case, it might be better to disable the function when you know the appliance will be used. For example, I disable this feature during the day when presence is detected because I know my _PAC_ will turn on often. I enable auto-start/stop at night or when no one is home, as the setpoint is lowered and it rarely triggers.
|
||||
> 3. If you use the Versatile Thermostat UI card (see [here](additions.md#better-with-the-versatile-thermostat-ui-card)), a checkbox is directly visible on the card to disable auto-start/stop, and a _VTherm_ stopped by auto-start/stop is indicated by the icon: .
|
||||
@@ -1,123 +0,0 @@
|
||||
# Le contrôle d'une chaudière centrale# Controlling a Central Boiler
|
||||
|
||||
- [Le contrôle d'une chaudière centrale# Controlling a Central Boiler](#le-contrôle-dune-chaudière-centrale-controlling-a-central-boiler)
|
||||
- [Principle](#principle)
|
||||
- [Configuration](#configuration)
|
||||
- [How to Find the Right Action?](#how-to-find-the-right-action)
|
||||
- [Events](#events)
|
||||
- [Warning](#warning)
|
||||
|
||||
You can control a centralized boiler. As long as it's possible to trigger or stop the boiler from Home Assistant, Versatile Thermostat will be able to control it directly.
|
||||
|
||||
## Principle
|
||||
The basic principle is as follows:
|
||||
1. A new entity of type `binary_sensor`, named by default `binary_sensor.central_boiler`, is added.
|
||||
2. In the configuration of the _VTherms_, you specify whether the _VTherm_ should control the boiler. In a heterogeneous installation, some _VTherms_ should control the boiler, and others should not. Therefore, you need to indicate in each _VTherm_ configuration whether it controls the boiler.
|
||||
3. The `binary_sensor.central_boiler` listens for state changes in the equipment of the _VTherms_ marked as controlling the boiler.
|
||||
4. When the number of devices controlled by the _VTherm_ requesting heating (i.e., when its `hvac_action` changes to `Heating`) exceeds a configurable threshold, the `binary_sensor.central_boiler` turns `on`, and **if an activation service has been configured, that service is called**.
|
||||
5. If the number of devices requesting heating drops below the threshold, the `binary_sensor.central_boiler` turns `off`, and **if a deactivation service has been configured, that service is called**.
|
||||
6. You have access to two entities:
|
||||
- A `number` type entity, named by default `number.boiler_activation_threshold`, which gives the activation threshold. This threshold is the number of devices (radiators) requesting heating.
|
||||
- A `sensor` type entity, named by default `sensor.nb_device_active_for_boiler`, which shows the number of devices requesting heating. For example, a _VTherm_ with 4 valves, 3 of which request heating, will make this sensor show 3. Only the devices from _VTherms_ marked to control the central boiler are counted.
|
||||
|
||||
You therefore always have the information to manage and adjust the triggering of the boiler.
|
||||
|
||||
All these entities are linked to the central configuration service:
|
||||
|
||||

|
||||
|
||||
## Configuration
|
||||
To configure this feature, you need a centralized configuration (see [Configuration](#configuration)) and check the 'Add Central Boiler' box:
|
||||
|
||||

|
||||
|
||||
On the next page, you can provide the configuration for the actions (e.g., services) to be called when the boiler is turned on/off:
|
||||
|
||||

|
||||
|
||||
The actions (e.g., services) are configured as described on the page:
|
||||
1. The general format is `entity_id/service_id[/attribute:value]` (where `/attribute:value` is optional).
|
||||
2. `entity_id` is the name of the entity controlling the boiler in the form `domain.entity_name`. For example: `switch.chaudiere` for a boiler controlled by a switch, or `climate.chaudière` for a boiler controlled by a thermostat, or any other entity that allows boiler control (there is no limitation). You can also toggle inputs (`helpers`) such as `input_boolean` or `input_number`.
|
||||
3. `service_id` is the name of the service to be called in the form `domain.service_name`. For example: `switch.turn_on`, `switch.turn_off`, `climate.set_temperature`, `climate.set_hvac_mode` are valid examples.
|
||||
4. Some services require a parameter. This could be the 'HVAC Mode' for `climate.set_hvac_mode` or the target temperature for `climate.set_temperature`. This parameter should be configured in the format `attribute:value` at the end of the string.
|
||||
|
||||
Examples (to adjust to your case):
|
||||
- `climate.chaudiere/climate.set_hvac_mode/hvac_mode:heat`: to turn the boiler thermostat on in heating mode.
|
||||
- `climate.chaudiere/climate.set_hvac_mode/hvac_mode:off`: to turn off the boiler thermostat.
|
||||
- `switch.pompe_chaudiere/switch.turn_on`: to turn on the switch powering the boiler pump.
|
||||
- `switch.pompe_chaudiere/switch.turn_off`: to turn off the switch powering the boiler pump.
|
||||
- ...
|
||||
|
||||
### How to Find the Right Action?
|
||||
To find the correct action to use, it's best to go to "Developer Tools / Services", search for the action to call, the entity to control, and any required parameters.
|
||||
Click 'Call Service'. If your boiler turns on, you have the correct configuration. Then switch to YAML mode and copy the parameters.
|
||||
|
||||
Example:
|
||||
|
||||
In "Developer Tools / Actions":
|
||||
|
||||

|
||||
|
||||
In YAML mode:
|
||||
|
||||

|
||||
|
||||
The service to configure will then be: `climate.sonoff/climate.set_hvac_mode/hvac_mode:heat` (note the removal of spaces in `hvac_mode:heat`).
|
||||
|
||||
Do the same for the off service, and you’re ready to go.
|
||||
|
||||
## Events
|
||||
|
||||
Each successful boiler activation or deactivation sends an event from Versatile Thermostat. This can be captured by an automation, for example, to notify you of the change.
|
||||
The events look like this:
|
||||
|
||||
An activation event:
|
||||
```yaml
|
||||
event_type: versatile_thermostat_central_boiler_event
|
||||
data:
|
||||
central_boiler: true
|
||||
entity_id: binary_sensor.central_boiler
|
||||
name: Central boiler
|
||||
state_attributes: null
|
||||
origin: LOCAL
|
||||
time_fired: "2024-01-14T11:33:52.342026+00:00"
|
||||
context:
|
||||
id: 01HM3VZRJP3WYYWPNSDAFARW1T
|
||||
parent_id: null
|
||||
user_id: null
|
||||
```yaml
|
||||
event_type: versatile_thermostat_central_boiler_event
|
||||
data:
|
||||
central_boiler: true
|
||||
entity_id: binary_sensor.central_boiler
|
||||
name: Central boiler
|
||||
state_attributes: null
|
||||
origin: LOCAL
|
||||
time_fired: "2024-01-14T11:33:52.342026+00:00"
|
||||
context:
|
||||
id: 01HM3VZRJP3WYYWPNSDAFARW1T
|
||||
parent_id: null
|
||||
user_id: null
|
||||
```
|
||||
|
||||
Un évènement d'extinction :
|
||||
```yaml
|
||||
event_type: versatile_thermostat_central_boiler_event
|
||||
data:
|
||||
central_boiler: false
|
||||
entity_id: binary_sensor.central_boiler
|
||||
name: Central boiler
|
||||
state_attributes: null
|
||||
origin: LOCAL
|
||||
time_fired: "2024-01-14T11:43:52.342026+00:00"
|
||||
context:
|
||||
id: 01HM3VZRJP3WYYWPNSDAFBRW1T
|
||||
parent_id: null
|
||||
user_id: null
|
||||
```
|
||||
|
||||
## Warning
|
||||
|
||||
>  _*Notes*_
|
||||
>
|
||||
> Software or home automation control of a central boiler may pose risks to its proper operation. Before using these functions, ensure that your boiler has proper safety features and that they are functioning correctly. For example, turning on a boiler with all valves closed can create excessive pressure.
|
||||
@@ -1,31 +0,0 @@
|
||||
# Centralized Control
|
||||
|
||||
- [Centralized Control](#centralized-control)
|
||||
- [Configuration of Centralized Control](#configuration-of-centralized-control)
|
||||
- [Usage](#usage)
|
||||
|
||||
This feature allows you to control all your _VTherms_ from a single control point.
|
||||
A typical use case is when you leave for an extended period and want to set all your _VTherms_ to frost protection, and when you return, you want to set them back to their initial state.
|
||||
|
||||
Centralized control is done from a special _VTherm_ called centralized configuration. See [here](creation.md#centralized-configuration) for more information.
|
||||
|
||||
## Configuration of Centralized Control
|
||||
|
||||
If you have set up a centralized configuration, you will have a new entity named `select.central_mode` that allows you to control all _VTherms_ with a single action.
|
||||
|
||||

|
||||
|
||||
This entity appears as a list of choices containing the following options:
|
||||
1. `Auto`: the 'normal' mode where each _VTherm_ operates autonomously,
|
||||
2. `Stopped`: all _VTherms_ are turned off (`hvac_off`),
|
||||
3. `Heat only`: all _VTherms_ are set to heating mode if supported, otherwise they are stopped,
|
||||
4. `Cool only`: all _VTherms_ are set to cooling mode if supported, otherwise they are stopped,
|
||||
5. `Frost protection`: all _VTherms_ are set to frost protection mode if supported, otherwise they are stopped.
|
||||
|
||||
## Usage
|
||||
|
||||
For a _VTherm_ to be controllable centrally, its configuration attribute named `use_central_mode` must be true. This attribute is available in the configuration page `Main Attributes`.
|
||||
|
||||

|
||||
|
||||
This means you can control all _VTherms_ (those explicitly designated) with a single control.
|
||||
@@ -1,40 +0,0 @@
|
||||
# Motion or Activity Detection
|
||||
|
||||
- [Motion or Activity Detection](#motion-or-activity-detection)
|
||||
- [Configure Activity Mode or Motion Detection](#configure-activity-mode-or-motion-detection)
|
||||
- [Usage](#usage)
|
||||
|
||||
This feature allows you to change presets when motion is detected in a room. If you don't want to heat your office when the room is occupied and only when the room is occupied, you need a motion (or presence) sensor in the room and configure this feature.
|
||||
|
||||
This function is often confused with the presence feature. They are complementary but not interchangeable. The 'motion' function is local to a room equipped with a motion sensor, while the 'presence' function is designed to be global to the entire home.
|
||||
|
||||
## Configure Activity Mode or Motion Detection
|
||||
|
||||
If you have chosen the `With motion detection` feature:
|
||||
|
||||

|
||||
|
||||
What we need:
|
||||
- a **motion sensor**. Entity ID of a motion sensor. The states of the motion sensor must be "on" (motion detected) or "off" (no motion detected),
|
||||
- a **detection delay** (in seconds) defining how long we wait for confirmation of the motion before considering the motion. This parameter can be **greater than your motion sensor's delay**, otherwise, the detection will happen with every motion detected by the sensor,
|
||||
- an **inactivity delay** (in seconds) defining how long we wait for confirmation of no motion before no longer considering the motion,
|
||||
- a **"motion" preset**. We will use the temperature of this preset when activity is detected,
|
||||
- a **"no motion" preset**. We will use the temperature of this second preset when no activity is detected.
|
||||
|
||||
## Usage
|
||||
|
||||
To tell a _VTherm_ that it should listen to the motion sensor, you must set it to the special 'Activity' preset. If you have installed the Versatile Thermostat UI card (see [here](additions.md#much-better-with-the-versatile-thermostat-ui-card)), this preset is displayed as follows: .
|
||||
|
||||
You can then, upon request, set a _VTherm_ to motion detection mode.
|
||||
|
||||
The behavior will be as follows:
|
||||
- we have a room with a thermostat set to activity mode, the "motion" mode chosen is comfort (21.5°C), the "no motion" mode chosen is Eco (18.5°C), and the motion delay is 30 seconds on detection and 5 minutes on the end of detection.
|
||||
- the room has been empty for a while (no activity detected), the setpoint temperature in this room is 18.5°.
|
||||
- someone enters the room, and activity is detected if the motion is present for at least 30 seconds. The temperature then goes up to 21.5°.
|
||||
- if the motion is present for less than 30 seconds (quick passage), the temperature stays at 18.5°.
|
||||
- imagine the temperature has gone up to 21.5°, when the person leaves the room, after 5 minutes the temperature is returned to 18.5°.
|
||||
- if the person returns before the 5 minutes, the temperature stays at 21.5°.
|
||||
|
||||
>  _*Notes*_
|
||||
> 1. As with other presets, `Activity` will only be offered if it is correctly configured. In other words, all 4 configuration keys must be set.
|
||||
> 2. If you are using the Versatile Thermostat UI card (see [here](additions.md#much-better-with-the-versatile-thermostat-ui-card)), motion detection is represented as follows: .
|
||||
@@ -1,51 +0,0 @@
|
||||
# Power Management - Load Shedding
|
||||
|
||||
- [Power Management - Load Shedding](#power-management---load-shedding)
|
||||
- [Example Use Case:](#example-use-case)
|
||||
- [Configuring Power Management](#configuring-power-management)
|
||||
|
||||
This feature allows you to regulate the electrical consumption of your heaters. Known as load shedding, it lets you limit the electrical consumption of your heating equipment if overconsumption conditions are detected.
|
||||
You will need a **sensor for the total instantaneous power consumption** of your home and a **sensor for the maximum allowed power**.
|
||||
|
||||
The behavior of this feature is as follows:
|
||||
1. When a new measurement of the home's power consumption or the maximum allowed power is received,
|
||||
2. If the maximum power is exceeded, the central command will shed the load of all active devices starting with those closest to the setpoint. This continues until enough _VTherms_ are shed,
|
||||
3. If there is available power reserve and some _VTherms_ are shed, the central command will re-enable as many devices as possible, starting with those furthest from the setpoint (at the time they were shed).
|
||||
4. When a _VTherm_ starts, a check is performed to determine if the declared power is available. If not, the _VTherm_ is put into shed mode.
|
||||
|
||||
**WARNING:** This is **not a safety feature** but an optimization function to manage consumption at the expense of some heating degradation. Overconsumption is still possible depending on the frequency of your consumption sensor updates and the actual power used by your equipment. Always maintain a safety margin.
|
||||
|
||||
### Example Use Case:
|
||||
1. You have an electric meter limited to 11 kW,
|
||||
2. You occasionally charge an electric vehicle at 5 kW,
|
||||
3. This leaves 6 kW for everything else, including heating,
|
||||
4. You have 1 kW of other active devices,
|
||||
5. You declare a sensor (`input_number`) for the maximum allowed power at 9 kW (= 11 kW - reserved power for other devices - safety margin).
|
||||
|
||||
If the vehicle is charging, the total consumed power is 6 kW (5 + 1), and a _VTherm_ will only turn on if its declared power is a maximum of 3 kW (9 kW - 6 kW).
|
||||
If the vehicle is charging and another _VTherm_ of 2 kW is on, the total consumed power is 8 kW (5 + 1 + 2), and a _VTherm_ will only turn on if its declared power is a maximum of 1 kW (9 kW - 8 kW). Otherwise, it will skip its turn (cycle).
|
||||
If the vehicle is not charging, the total consumed power is 1 kW, and a _VTherm_ will only turn on if its declared power is a maximum of 8 kW (9 kW - 1 kW).
|
||||
|
||||
## Configuring Power Management
|
||||
|
||||
In the centralized configuration, if you have selected the `With power detection` feature, configure it as follows:
|
||||
|
||||

|
||||
|
||||
1. The entity ID of the **sensor for total instantaneous power consumption** of your home,
|
||||
2. The entity ID of the **sensor for maximum allowed power**,
|
||||
3. The temperature to apply if load shedding is activated.
|
||||
|
||||
Ensure that all power values use the same units (e.g., kW or W).
|
||||
Having a **sensor for maximum allowed power** allows you to modify the maximum power dynamically using a scheduler or automation.
|
||||
|
||||
Note that due to centralized load-shedding, it is not possible to override the consumption and maximum consumption sensors on individual _VTherms_. This configuration must be done in the centralized settings. See [Centralized Configuration](./creation.md#centralized-configuration).
|
||||
|
||||
>  _*Notes*_
|
||||
>
|
||||
> 1. During load shedding, the heater is set to the preset named `power`. This is a hidden preset that cannot be manually selected.
|
||||
> 2. Always maintain a margin, as the maximum power can briefly be exceeded while waiting for the next cycle's calculation or due to uncontrolled devices.
|
||||
> 3. If you do not wish to use this feature, uncheck it in the 'Features' menu.
|
||||
> 4. If a single _VTherm_ controls multiple devices, the **declared heating power consumption** should correspond to the total power of all devices.
|
||||
> 5. If you use the Versatile Thermostat UI card (see [here](additions.md#better-with-the-versatile-thermostat-ui-card)), load shedding is represented as follows: .
|
||||
> 6. There may be a delay of up to 20 seconds between receiving a new value from the power consumption sensor and triggering load shedding for _VTherms_. This delay prevents overloading Home Assistant if your consumption updates are very frequent.
|
||||
@@ -1,24 +0,0 @@
|
||||
# Presence / Absence Management
|
||||
|
||||
- [Presence / Absence Management](#presence--absence-management)
|
||||
- [Configure Presence (or Absence)](#configure-presence-or-absence)
|
||||
|
||||
## Configure Presence (or Absence)
|
||||
|
||||
If this feature is selected, it allows you to dynamically adjust the preset temperatures of the thermostat when presence (or absence) is detected. To do this, you need to configure the temperature to be used for each preset when presence is disabled. When the presence sensor turns off, these temperatures will be applied. When it turns back on, the "normal" temperature configured for the preset will be used. See [preset management](feature-presets.md).
|
||||
|
||||
To configure presence, fill out this form:
|
||||
|
||||

|
||||
|
||||
For this, you simply need to configure an **occupancy sensor** whose state must be 'on' or 'home' if someone is present, or 'off' or 'not_home' otherwise.
|
||||
|
||||
Temperatures are configured in the entities of the device corresponding to your _VTherm_ (Settings/Integration/Versatile Thermostat/the vtherm).
|
||||
|
||||
WARNING: People groups do not work as a presence sensor. They are not recognized as a presence sensor. You need to use a template as described here [Using a People Group as a Presence Sensor](troubleshooting.md#using-a-people-group-as-a-presence-sensor).
|
||||
|
||||
>  _*Notes*_
|
||||
>
|
||||
> 1. The temperature change is immediate and is reflected on the front panel. The calculation will consider the new target temperature at the next cycle calculation.
|
||||
> 2. You can use the direct person.xxxx sensor or a Home Assistant sensor group. The presence sensor handles the states `on` or `home` as present and `off` or `not_home` as absent.
|
||||
> 3. To pre-heat your home when everyone is absent, you can add an `input_boolean` entity to your people group. If you set this `input_boolean` to 'On', the presence sensor will be forced to 'On' and the presets with presence will be used. You can also set this `input_boolean` to 'On' via an automation, for example, when you leave a zone to start preheating your home.
|
||||
@@ -1,30 +0,0 @@
|
||||
# Presets (Pre-configured Settings)
|
||||
|
||||
- [Presets (Pre-configured Settings)](#presets-pre-configured-settings)
|
||||
- [Configure Pre-configured Temperatures](#configure-pre-configured-temperatures)
|
||||
|
||||
## Configure Pre-configured Temperatures
|
||||
|
||||
The preset mode allows you to pre-configure the target temperature. Used in conjunction with Scheduler (see [scheduler](additions.md#the-scheduler-component)), you'll have a powerful and simple way to optimize the temperature relative to the electricity consumption in your home. The managed presets are as follows:
|
||||
- **Eco**: the device is in energy-saving mode
|
||||
- **Comfort**: the device is in comfort mode
|
||||
- **Boost**: the device fully opens all valves
|
||||
|
||||
If the AC mode is used, you can also configure temperatures when the equipment is in air conditioning mode.
|
||||
|
||||
**None** is always added to the list of modes, as it is a way to not use presets but instead set a **manual temperature**.
|
||||
|
||||
The presets are configured directly from the _VTherm_ entities or the central configuration if you're using centralized control. Upon creating the _VTherm_, you will have different entities that will allow you to set the temperatures for each preset:
|
||||
|
||||
.
|
||||
|
||||
The list of entities varies depending on your feature choices:
|
||||
1. If the 'presence detection' function is activated, you will have the presets with an "absence" version prefixed with _abs_.
|
||||
2. If you have selected the _AC_ option, you will also have presets for 'air conditioning' prefixed with _clim_.
|
||||
|
||||
>  _*Notes*_
|
||||
>
|
||||
> 1. When you manually change the target temperature, the preset switches to None (no preset).
|
||||
> 2. The standard preset `Away` is a hidden preset that cannot be directly selected. Versatile Thermostat uses presence management or motion detection to automatically and dynamically adjust the target temperature based on presence in the home or activity in the room. See [presence management](feature-presence.md).
|
||||
> 3. If you're using load shedding management, you will see a hidden preset named `power`. The heating element's preset is set to "power" when overload conditions are met and load shedding is active for that heating element. See [power management](feature-power.md).
|
||||
> 4. If you're using advanced configuration, you will see the preset set to `safety` if the temperature could not be retrieved after a certain delay. See [Safety Mode](feature-advanced.md#safety-mode).
|
||||
@@ -1,64 +0,0 @@
|
||||
# Door/Window Open Detection
|
||||
|
||||
- [Door/Window Open Detection](#doorwindow-open-detection)
|
||||
- [Sensor Mode](#sensor-mode)
|
||||
- [Auto Mode](#auto-mode)
|
||||
|
||||
You must have selected the `With Open Detection` feature on the first page to reach this page.
|
||||
Open detection can be done in two ways:
|
||||
1. By using a sensor placed on the opening (sensor mode),
|
||||
2. By detecting a sudden temperature drop (auto mode)
|
||||
|
||||
## Sensor Mode
|
||||
To switch to sensor mode, you need to provide an entity of type `binary_sensor` or `input_boolean`.
|
||||
In this mode, you need to fill in the following information:
|
||||
|
||||

|
||||
|
||||
1. A **delay in seconds** before any change. This allows you to open a window quickly without stopping the heating.
|
||||
2. The action to take when the opening is detected as open. The possible actions are:
|
||||
1. _Turn off_: the _VTherm_ will be turned off.
|
||||
2. _Fan only_: heating or cooling will be turned off, but the equipment will continue to ventilate (for compatible equipment).
|
||||
3. _Frost protection_: the "Frost Protection" preset temperature will be selected on the _VTherm_ without changing the current preset (see notes below).
|
||||
4. _Eco_: the "Eco" preset temperature will be applied to the _VTherm_ without changing the current preset (see notes below).
|
||||
|
||||
When the detector switches to open:
|
||||
1. _VTherm_ waits for the specified delay.
|
||||
2. If the window is still open after the delay, the _VTherm_ state (Heating / Cooling / ..., current preset, current target temperature) is saved and the action is performed.
|
||||
|
||||
Similarly, when the detector switches to closed:
|
||||
1. _VTherm_ waits for the specified delay.
|
||||
2. If the window is still closed after the delay, the state before the window opening is restored.
|
||||
|
||||
## Auto Mode
|
||||
In auto mode, the configuration is as follows:
|
||||
|
||||

|
||||
|
||||
1. A **delay in seconds** before any change. This allows you to open a window quickly without stopping the heating.
|
||||
2. A detection threshold in degrees per hour. When the temperature drops beyond this threshold, the thermostat will turn off. The lower this value, the faster the detection (with a higher risk of false positives).
|
||||
3. A threshold for ending detection in degrees per hour. When the temperature drop exceeds this value, the thermostat will return to the previous mode (mode and preset).
|
||||
4. A maximum detection duration. Beyond this duration, the thermostat will return to its previous mode and preset even if the temperature continues to drop.
|
||||
5. The action to take when the opening is detected as open. The actions are the same as in sensor mode described above.
|
||||
|
||||
To adjust the thresholds, it is recommended to start with the reference values and adjust the detection thresholds. Some tests gave me the following values (for an office):
|
||||
- Detection threshold: 3°C/hour
|
||||
- No detection threshold: 0°C/hour
|
||||
- Max duration: 30 min.
|
||||
|
||||
A new sensor called "slope" has been added for all thermostats. It provides the slope of the temperature curve in °C/hour (or °K/hour). This slope is smoothed and filtered to avoid aberrant thermometer values that could interfere with the measurement.
|
||||
|
||||

|
||||
|
||||
To adjust it properly, it is recommended to display both the temperature curve and the slope of the curve ("slope") on the same historical graph:
|
||||
|
||||

|
||||
|
||||
>  _*Notes*_
|
||||
>
|
||||
> 1. If you want to use **multiple door/window sensors** to automate your thermostat, simply create a group with the usual behavior (https://www.home-assistant.io/integrations/binary_sensor.group/)
|
||||
> 2. If you don't have a door/window sensor in your room, simply leave the sensor entity ID empty.
|
||||
> 3. **Only one mode is allowed**. You cannot configure a thermostat with both a sensor and auto detection. The two modes might contradict each other, so both modes cannot be active at the same time.
|
||||
> 4. It is not recommended to use auto mode for equipment subjected to frequent and normal temperature variations (hallways, open areas, etc.).
|
||||
> 5. To avoid interfering with your current preset settings, the actions _Frost protection_ and _Eco_ change the target temperature without changing the preset. So, you may notice a discrepancy between the selected preset and the setpoint.
|
||||
> 6. If you use the Versatile Thermostat UI card (see [here](additions.md#even-better-with-the-versatile-thermostat-ui-card)), open detection is represented as follows: .
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 67 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 1.5 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 4.2 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 1.3 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 144 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 16 KiB |
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user