Compare commits

..

90 Commits

Author SHA1 Message Date
Jean-Marc Collin
062f8a617d Fix #533 (#542)
Clean some pylint hints
Avoid 2 times open percentage send at startup

Co-authored-by: Jean-Marc Collin <jean-marc.collin-extern@renault.com>
2024-10-12 10:46:13 +02:00
Jean-Marc Collin
70f91f3cbe Issue #524 switch from cool to heat don't change the target temp (#529)
* Preparation tests ok

* Fixed

---------

Co-authored-by: Jean-Marc Collin <jean-marc.collin-extern@renault.com>
2024-10-08 17:06:16 +02:00
Jean-Marc Collin
668053b352 Fix unit_test 2024-10-07 05:22:57 +00:00
Jean-Marc Collin
6ff9ff1ee5 Fix variables in error log 2024-10-07 04:52:33 +00:00
Jean-Marc Collin
3f95ed74f4 FIX TypeError: '>' not supported between instances of 'float' and 'NoneType' error message 2024-10-06 09:04:47 +00:00
Jean-Marc Collin
6e42904ddf Issue #518 - Fix ThermostatOverClimate object has no attribute __attr_preset_modes 2024-10-06 08:58:58 +00:00
Jean-Marc Collin
4c1fc396fb Issue #500 - check feature is use central config is checked (#513)
Co-authored-by: Jean-Marc Collin <jean-marc.collin-extern@renault.com>
2024-09-29 11:34:39 +02:00
Jean-Marc Collin
d6ec7a86be issue #506 - Add some check to verify tpi algorithm parameters are correctly set. (#512)
Co-authored-by: Jean-Marc Collin <jean-marc.collin-extern@renault.com>
2024-09-29 09:54:39 +02:00
Jean-Marc Collin
a3f8715fe5 HA 2024.9.3 and issue 508 (#510)
* HA 2024.9.3 and issue 508

* Fix strings trailing spaces

---------

Co-authored-by: Jean-Marc Collin <jean-marc.collin-extern@renault.com>
2024-09-28 19:36:46 +02:00
domozer
a1a9a8bbab Update README-fr.md (#487)
Remplacement de "Awesome Thermostat" par "Versatile Thermostat" dans la section "Encore mieux avec le composant Scheduler !"

Idem pour le fichier README.md
2024-07-23 19:24:56 +02:00
Jean-Marc Collin
d5c5869276 Update settings 2024-07-17 06:50:01 +00:00
Jean-Marc Collin
c4b03f8c1e Update manifest.json 2024-07-07 16:49:22 +02:00
Paulo Ferreira de Castro
ac206a949f Fix Home Assistant deprecation warnings (EventType, helpers.service) (#484)
* Type hints: Replace deprecated helpers.typing.EventType with core.Event

* Replace deprecated use of hass.helpers.service.async_register_admin_service
2024-07-07 16:47:30 +02:00
Jean-Marc Collin
4bccb746b8 Release 6.2.8 2024-07-02 05:18:29 +00:00
Jean-Marc Collin
e999705286 Issue 474 - TPI in AC mode is wrong 2024-07-02 05:17:14 +00:00
Jean-Marc Collin
b4873bfd27 FIX issue_479 (#480)
Co-authored-by: Jean-Marc Collin <jean-marc.collin-extern@renault.com>
2024-07-02 07:04:47 +02:00
Jean-Marc Collin
b00dc09c80 Update manifest.json 2024-06-30 09:05:14 +02:00
Jean-Marc Collin
da6d6cbce6 Release 6.2.6 2024-06-30 09:04:51 +02:00
Paulo Ferreira de Castro
864e904e21 Reduce keep-alive warning logs when the controlled entity becomes unavailable (#476) 2024-06-28 09:53:40 +02:00
cddu33
0ee4fe355d issue with ac and mouvement detection (#471)
* Update base_thermostat.py

issue with ac and mouvement detection

* issue ac with detection mouvement

modif class find_preset_temps for preset activity
2024-06-17 10:18:03 +02:00
Maxwell Gonsalves
53dce224cd Change VTherm temperature unit to HA's preferred unit. (#461)
* Change VTherm temperature unit to HA's preferred unit.

* fix pytest issue

* update current_temperature, explicitly convert temps (fixes pytest error)
2024-06-10 18:46:45 +02:00
Jean-Marc Collin
2fd60074c7 Beers ! 2024-05-28 08:47:14 +02:00
jkreiss-coexya
549423b313 Enhance temperature regulation when working with internal device temperature (#453)
* [feature/autoregulation-send-for-underlyingtemp] Do not forget regulation send when using underlying device temperature for offset

* [feature/autoregulation-send-for-underlyingtemp] Add unit test for dtemp = 0

* [feature/autoregulation-send-for-underlyingtemp] Test with dtemp lower than 0.5

* [feature/autoregulation-send-for-underlyingtemp] Comments
2024-05-13 08:27:52 +02:00
Matt Bush
6bd1b1137e [Draft] Use user's preferred temperature unit instead of hardcoding celsius (#460)
* Use user's preferred temperature unit instead of hardcoding celsius

* Fix warnings about using is instead of == in tests
2024-05-13 07:47:04 +02:00
Jean-Marc Collin
189418e69a Beers 2024-05-11 10:47:33 +02:00
Jean-Marc Collin
4ab932f44e Update README-fr.md
Beers !
2024-05-11 10:46:59 +02:00
misa1515
e1ff23fb30 Update sk.json (#454) 2024-04-29 07:15:26 +02:00
Jean-Marc Collin
7b657ffabf Issue 444 ha 2024.04.3 (#452)
* HA 2024.4.3 and release

* [#444] - fix initial temp values with standalone presets

---------

Co-authored-by: Jean-Marc Collin <jean-marc.collin-extern@renault.com>
2024-04-23 07:58:22 +02:00
Jean-Marc Collin
acd22d1fc4 HA 2024.4.3 and release (#447)
Co-authored-by: Jean-Marc Collin <jean-marc.collin-extern@renault.com>
2024-04-16 08:54:59 +02:00
Jean-Marc Collin
d6f33d5796 [#438] - manage total_energy none until restored or calculated (#446)
Co-authored-by: Jean-Marc Collin <jean-marc.collin-extern@renault.com>
2024-04-16 07:14:38 +02:00
Jean-Marc Collin
c1ebb46ac6 [#358] - Block preset_mode change on central_mode status (#445)
Co-authored-by: Jean-Marc Collin <jean-marc.collin-extern@renault.com>
2024-04-15 08:06:07 +02:00
Jean-Marc Collin
eee4a9c4e3 Beers ! 2024-04-08 05:33:56 +00:00
Jean-Marc Collin
2a3d3ff877 [#429] - VTherm doesn't respect target temperature (#435)
Co-authored-by: Jean-Marc Collin <jean-marc.collin-extern@renault.com>
2024-04-01 12:34:18 +02:00
Jean-Marc Collin
a9c368d64c Release 6.2.0 2024-03-31 18:48:47 +00:00
Jean-Marc Collin
1595ff32a2 [#432] - Use valve number max value instead of 100 (#434)
Co-authored-by: Jean-Marc Collin <jean-marc.collin-extern@renault.com>
2024-03-31 20:47:04 +02:00
Jean-Marc Collin
c49545d9e3 [#398] - Add last_seen sensor to update temperature last datetime (#433)
Co-authored-by: Jean-Marc Collin <jean-marc.collin-extern@renault.com>
2024-03-31 20:25:34 +02:00
Jean-Marc Collin
f4598a407e Ajout incompatibilité Airwell 2024-03-31 16:57:32 +00:00
Jean-Marc Collin
d96fe4bec7 Add Nodon module virtuak switch example 2024-03-30 07:51:22 +00:00
Jean-Marc Collin
a9b87b3aee Release 6.1.0 2024-03-26 20:10:29 +00:00
Jean-Marc Collin
c512cb6f74 [#428] - Refacto start versatile_thermostat (#430)
Co-authored-by: Jean-Marc Collin <jean-marc.collin-extern@renault.com>
2024-03-26 21:06:25 +01:00
Jean-Marc Collin
9269240fe3 Release 2024-03-25 07:43:09 +00:00
Jean-Marc Collin
91ba2387b2 Persistence of boiler srv attribute 2024-03-25 07:25:42 +00:00
Jean-Marc Collin
162efb4709 [#425] - Boiler management entities are generated independently of this option selection (#426)
Co-authored-by: Jean-Marc Collin <jean-marc.collin-extern@renault.com>
2024-03-25 08:00:08 +01:00
Paulo Ferreira de Castro
6a97622226 Expose the keep_alive_sec attribute in HASS Developer Tools - States (#381)
* Typing: Make BaseThermostat generic on the UnderlyingEntity type

* Typing: Change the type of IntervalCaller._interval_sec from int to float

This makes the IntervalCaller class more reusable.

* Keep-alive: Expose UnderlyingSwitch.keep_alive_sec as a HASS Dev Tools attribute

Also improve a keep-alive log message.
2024-03-23 11:49:09 +01:00
Jean-Marc Collin
5db7a49e75 [#339] - AUTO mode not counted for active boiler (#424)
Co-authored-by: Jean-Marc Collin <jean-marc.collin-extern@renault.com>
2024-03-23 09:07:55 +01:00
Jean-Marc Collin
d7cdf79561 [#407] - shredding if heating (#423)
Co-authored-by: Jean-Marc Collin <jean-marc.collin-extern@renault.com>
2024-03-23 08:30:17 +01:00
Jean-Marc Collin
07ac7beb7d [#419] - Turnon don't work anymore (#422)
Co-authored-by: Jean-Marc Collin <jean-marc.collin-extern@renault.com>
2024-03-23 07:21:18 +01:00
Jean-Marc Collin
7ded723c8b [#420] - Message d'erreur dans home assistant (#421)
Co-authored-by: Jean-Marc Collin <jean-marc.collin-extern@renault.com>
2024-03-23 07:17:55 +01:00
EPicLURcher
5369111f2d English translations (#417)
* Update strings.json

* Update en.json

* Update strings.json

* Update strings.json

* Update en.json
2024-03-20 23:11:50 +01:00
Jean-Marc Collin
4a7ae81c8f Release 2024-03-20 18:17:28 +00:00
Jean-Marc Collin
dbf2bc6982 [#412] - Fixe Unisue IDs error at startup (#416)
Co-authored-by: Jean-Marc Collin <jean-marc.collin-extern@renault.com>
2024-03-20 19:16:38 +01:00
Jean-Marc Collin
2430e7dd8c [#388] - remove climate warning (#415)
Co-authored-by: Jean-Marc Collin <jean-marc.collin-extern@renault.com>
2024-03-20 18:57:49 +01:00
Jean-Marc Collin
030069bb97 Fix README 2024-03-16 17:19:41 +00:00
Jean-Marc Collin
9fb9d89f17 Make the temperature preset as persistent entities / redesign of the configuration menu (#409)
* HA 2024.2.b4

* Add temp entities initialization

* Python12 env rebuild

* Init temperature number for central configuration + testus ok

* With calculation of VTherm temp entities + test ok

* FIX some testus. Some others are still KO

* Beers

* Update central config Number temp entity

* Many but not all testus ok

* All testus ok

* With central config temp change ok

* Cleaning and fixing Issues

* Validation tests ok

* With new menu. Testus KO

* All developped and tests ok

* Fix central_config menu

* Documentation and release

* Fix testus KO

* Add log into migration for testu

---------

Co-authored-by: Jean-Marc Collin <jean-marc.collin-extern@renault.com>
2024-03-16 18:10:13 +01:00
Jean-Marc Collin
12025c0610 Add Dockerfile plugin 2024-02-16 15:12:09 +00:00
Jean-Marc Collin
a9595a5cf8 Beers ! 2024-02-16 08:35:02 +00:00
Jean-Marc Collin
047c847f3c Fix rounding regulated + offset (#384)
Co-authored-by: Jean-Marc Collin <jean-marc.collin-extern@renault.com>
2024-02-16 08:46:11 +01:00
Paulo Ferreira de Castro
91e39f885f Improvements to the development environment (#383)
* Update Home Assistant dev version in requirements_dev.txt

* Avoid "Error starting FFmpeg" error in VSCode dev container logs

* Add "editor.formatOnSaveMode": "modifications" to .vscode/settings.json
2024-02-16 07:30:37 +01:00
Paulo Ferreira de Castro
dce8fa2ed6 Make the switch keep-alive callback conditional on the entity state (#382) 2024-02-16 07:23:30 +01:00
Paulo Ferreira de Castro
a440b35815 Prevent disabled heating warning loop while HVACMode is OFF (#374) 2024-02-04 20:58:22 +01:00
Jean-Marc Collin
e52666b9d9 Add logs in troubleshooting 2024-02-04 09:28:37 +00:00
Jean-Marc Collin
d9fe2bbd55 Documentation and release 2024-02-04 08:44:56 +00:00
Jean-Marc Collin
0a50d0fd4e Issue 348 self regulation use internal temp (#373)
* Add config option

* With all features + testu ok

* Change algo using underlying internal temp

* Algo fixes

* Bières !

* Change reset cumulated_error formula

* FIX pi algo test with accumulated_error reset

---------

Co-authored-by: Jean-Marc Collin <jean-marc.collin-extern@renault.com>
2024-02-04 08:57:04 +01:00
Jean-Marc Collin
c60f23a9ca Add debug log for keep-alive 2024-02-04 06:50:41 +00:00
Jean-Marc Collin
557657a01c Change Fr keep-alive text 2024-02-04 06:45:12 +00:00
Paulo Ferreira de Castro
1f13eb4f37 Implement a keep-alive feature for directly controlled heater switches (#345)
* Add keep_alive feature for directly controlled switches

* Add test cases for the switch keep-alive feature

* Add documentation (readme) and translations for the keep-alive feature
2024-01-30 07:47:17 +01:00
Paulo Ferreira de Castro
4f349d6f6f README*.md: Replace absolute image URLs with relative URLs (#367) 2024-01-27 09:41:46 +01:00
Jean-Marc Collin
76382ebb35 issue #325 - restore self-regulation errors after restart (#366)
* issue #325 - creates regulation_algo in post_init only

* Remove github pages deployment

---------

Co-authored-by: Jean-Marc Collin <jean-marc.collin-extern@renault.com>
2024-01-26 18:38:36 +01:00
Paulo Ferreira de Castro
90f9a0e1e3 Change log level of "Window auto event is ignored" message from info to debug (#350) 2024-01-26 14:13:56 +01:00
Paulo Ferreira de Castro
ed977b53cd Add more type hints in the thermostat classes and selected files (#364) 2024-01-26 10:51:25 +01:00
Jean-Marc Collin
5d453393f8 Change incompatilibity 2024-01-24 07:14:06 +00:00
Frederic Seiler
d2f2ab7804 Typo fix (#362) 2024-01-24 07:37:02 +01:00
Jean-Marc Collin
b0b6d0478d Incompatibility with Sonoff TRVZB 2024-01-24 06:32:26 +00:00
Jean-Marc Collin
f8a2c9baa9 FIX default value for regulation valve 2024-01-21 18:43:05 +00:00
Jean-Marc Collin
8cbd81012c Issue 338 limit regulation over valve to avoid drowning battery of the TRV (#356)
* With testus ok

* Clean TPI algo

* Commenet failed testu

* Documentation

---------

Co-authored-by: Jean-Marc Collin <jean-marc.collin-extern@renault.com>
2024-01-21 19:36:44 +01:00
Jean-Marc Collin
26844593b1 Add step temperature - Issue #311 (#355)
* Add step temperature in config

* All testus ok

* Keep the step of the VTherm and not the step of the underlying

* Release 5.4.0

---------

Co-authored-by: Jean-Marc Collin <jean-marc.collin-extern@renault.com>
2024-01-21 12:36:14 +01:00
Jean-Marc Collin
c12a91a5ff Feature 234 add central boiler helper (#352)
* Creation of the central boiler config + binary_sensor entity

* Fonctional before testu. Miss the service call

* Full featured but without testu

* Documentation and release.

* Add events in README

* FIX #341 - when window state change, open_valve_percent should be resend

* Issue #343 - disable safety mode for outdoor thermometer

* Issue #255 - Specify window action on window open detection

* Add en and string translation

* central boiler - add entites to fine tune the boiler start

* With testu ok

* Add testus for valve and climate

* Add testus in pipelines

* With pip 3

* With more pytest options

* Ass coverage tests

* Add coverage report in github

* Release 5.3.0

---------

Co-authored-by: Jean-Marc Collin <jean-marc.collin-extern@renault.com>
2024-01-21 00:31:16 +01:00
John Kozyrakis
3da271b671 Set the last regulation timestamp only when regulation is sent to thermostats (#351)
Fixes a small issue where `_last_regulation_change` is being set to `now` even though the new temperature is not sent to the thermostats (because `abs(dtemp) < self._auto_regulation_dtemp:`)
2024-01-20 06:46:23 +01:00
Jean-Marc Collin
e8bb465b43 Try to fix issue #334 - loop when underlying is late to update 2024-01-13 11:30:11 +00:00
Jean-Marc Collin
d7ec6770c4 Update version manifest.json 2024-01-12 12:20:35 +01:00
Jean-Marc Collin
51428aa875 Issue #324 - (re) 2024-01-09 20:25:10 +00:00
Jean-Marc Collin
6ea6fe8542 Issue #324 - don't use window auto detection is sensor is given 2024-01-09 20:24:45 +00:00
misa1515
a18d10fa3f Update sk.json (#322)
* Update sk.json

* Update sk.json
2024-01-09 08:14:02 +01:00
Jean-Marc Collin
7d4ee40b4d Update TOC 2024-01-06 09:41:02 +00:00
Jean-Marc Collin
1aaf9c8c8e Add troubleshoot "heaters hets when target is overseeded" 2024-01-06 09:38:40 +00:00
Jean-Marc Collin
ae93a8b97c Issue template improvement. 2024-01-05 17:28:57 +00:00
Jean-Marc Collin
cbe98ae20c Issue #313 - improve description with central configuration 2024-01-05 07:56:44 +00:00
Jean-Marc Collin
bfcc854c3e Issue #314 - rename central_mode 2024-01-05 06:47:48 +00:00
Jean-Marc Collin
683aa050f3 Issue #314 - part 1 (update documentation) 2024-01-05 06:37:25 +00:00
Jean-Marc Collin
7476e7fa64 Feature 158 central mode (#309)
* Normal algo working and testu ok

* Fix interaction with window

* FIX complex scenario

* pylint warning

* Release

* Issue #306

* Issue #306

---------

Co-authored-by: Jean-Marc Collin <jean-marc.collin-extern@renault.com>
2024-01-03 17:52:34 +01:00
86 changed files with 11583 additions and 2190 deletions

2
.devcontainer/Dockerfile Normal file
View File

@@ -0,0 +1,2 @@
FROM mcr.microsoft.com/devcontainers/python:1-3.12
RUN apt update && apt install -y ffmpeg

View File

@@ -1,11 +1,14 @@
default_config: default_config:
logger: logger:
default: info default: warning
logs: logs:
custom_components.versatile_thermostat: debug custom_components.versatile_thermostat: debug
custom_components.versatile_thermostat.underlyings: debug # custom_components.versatile_thermostat.underlyings: info
custom_components.versatile_thermostat.climate: debug # custom_components.versatile_thermostat.climate: info
# custom_components.versatile_thermostat.base_thermostat: debug
custom_components.versatile_thermostat.sensor: info
custom_components.versatile_thermostat.binary_sensor: info
# If you need to debug uncommment the line below (doc: https://www.home-assistant.io/integrations/debugpy/) # If you need to debug uncommment the line below (doc: https://www.home-assistant.io/integrations/debugpy/)
debugpy: debugpy:
@@ -25,6 +28,8 @@ versatile_thermostat:
max_alpha: 0.6 max_alpha: 0.6
halflife_sec: 301 halflife_sec: 301
precision: 3 precision: 3
safety_mode:
check_outdoor_sensor: false
input_number: input_number:
fake_temperature_sensor1: fake_temperature_sensor1:
@@ -63,6 +68,13 @@ input_number:
max: 90 max: 90
icon: mdi:pipe-valve icon: mdi:pipe-valve
unit_of_measurement: percentage unit_of_measurement: percentage
fake_boiler_temperature:
name: Central thermostat temp
min: 0
max: 30
icon: mdi:thermostat
unit_of_measurement: °C
mode: box
input_boolean: input_boolean:
# input_boolean to simulate the windows entity. Only for development environment. # input_boolean to simulate the windows entity. Only for development environment.
@@ -157,7 +169,15 @@ climate:
heater: input_boolean.fake_heater_switch3 heater: input_boolean.fake_heater_switch3
target_sensor: input_number.fake_temperature_sensor1 target_sensor: input_number.fake_temperature_sensor1
input_datetime:
fake_last_seen:
name: Last seen temp sensor
icon: mdi:update
has_date: true
has_time: true
recorder: recorder:
commit_interval: 0
include: include:
domains: domains:
- input_boolean - input_boolean
@@ -165,6 +185,10 @@ recorder:
- switch - switch
- climate - climate
- sensor - sensor
- binary_sensor
- number
- input_select
- versatile_thermostat
template: template:
- binary_sensor: - binary_sensor:

View File

@@ -1,7 +1,9 @@
// See https://aka.ms/vscode-remote/devcontainer.json for format details. // See https://aka.ms/vscode-remote/devcontainer.json for format details.
// "image": "ghcr.io/ludeeus/devcontainer/integration:latest", // "image": "ghcr.io/ludeeus/devcontainer/integration:latest",
{ {
"image": "mcr.microsoft.com/devcontainers/python:1-3.11-bullseye", "build": {
"dockerfile": "Dockerfile"
},
"name": "Versatile Thermostat integration", "name": "Versatile Thermostat integration",
"appPort": [ "appPort": [
"8123:8123" "8123:8123"
@@ -9,28 +11,34 @@
// "postCreateCommand": "container install", // "postCreateCommand": "container install",
"postCreateCommand": "./container dev-setup", "postCreateCommand": "./container dev-setup",
"mounts": [ "mounts": [
"source=/Users/jmcollin/.ssh,target=/home/vscode/.ssh,type=bind,consistency=cached", "source=${localEnv:HOME}/.ssh,target=/home/vscode/.ssh,type=bind,consistency=cached",
// uncomment this to get the versatile-thermostat-ui-card // 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" "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": { "customizations": {
"vscode": { "vscode": {
"extensions": [ "extensions": [
"ms-python.python", "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", "github.vscode-pull-request-github",
"ryanluker.vscode-coverage-gutters", "ryanluker.vscode-coverage-gutters",
"ms-python.black-formatter",
"ms-python.pylint",
"ferrierbenjamin.fold-unfold-all-icone", "ferrierbenjamin.fold-unfold-all-icone",
"ms-python.isort",
"LittleFoxTeam.vscode-python-test-adapter", "LittleFoxTeam.vscode-python-test-adapter",
"donjayamanne.githistory", "donjayamanne.githistory",
"waderyan.gitblame", "waderyan.gitblame",
"keesschollaart.vscode-home-assistant", "keesschollaart.vscode-home-assistant",
"vscode.markdown-math", "vscode.markdown-math",
"yzhang.markdown-all-in-one" "yzhang.markdown-all-in-one",
"github.vscode-github-actions",
"azuretools.vscode-docker"
], ],
"settings": { "settings": {
"files.eol": "\n", "files.eol": "\n",
@@ -51,10 +59,10 @@
"editor.formatOnPaste": false, "editor.formatOnPaste": false,
"editor.formatOnSave": true, "editor.formatOnSave": true,
"editor.formatOnType": true, "editor.formatOnType": true,
"files.trimTrailingWhitespace": true, "files.trimTrailingWhitespace": true
"python.experiments.optOutFrom": ["pythonTestAdapter"], // "python.experiments.optOutFrom": ["pythonTestAdapter"],
"python.analysis.logLevel": "Trace" // "python.analysis.logLevel": "Trace"
} }
} }
} }
} }

View File

@@ -4,6 +4,8 @@ about: Create a report to help us improve
--- ---
<!-- 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. <!-- 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). 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).
@@ -13,7 +15,7 @@ Check also in the [Troubleshooting](#troubleshooting) paragrah of the README if
Issues not containing the minimum requirements will be closed: Issues not containing the minimum requirements will be closed:
- Issues without a description (using the header is not good enough) will be closed. - Issues without a description (using the header is not good enough) will be closed.
- Issues without configuration will be closed - Issues that don't follow this template could be closed
--> -->

49
.github/workflows/testus.yaml vendored Normal file
View File

@@ -0,0 +1,49 @@
name: Run Tests
on:
push:
branches:
- main
pull_request:
jobs:
testu:
runs-on: ubuntu-latest
steps:
- name: Checkout Repository
uses: actions/checkout@v2
- name: Set up Python
uses: actions/setup-python@v2
with:
python-version: 3.12
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip3 install -r requirements_test.txt
- name: Run Tests
run: |
pytest \
-qq \
--timeout=9 \
--durations=10 \
-n auto \
-o console_output_style=count \
-p no:sugar \
tests
- name: Coverage
run: |
coverage run -m pytest tests/
coverage report
- name: Generate HTML Coverage Report
run: coverage html
# - name: Deploy to GitHub Pages
# uses: peaceiris/actions-gh-pages@v3
# with:
# github_token: ${{ secrets.GITHUB_TOKEN }}
# publish_dir: ./htmlcov

3
.gitignore vendored
View File

@@ -110,3 +110,6 @@ __pycache__
config/** config/**
custom_components/hacs custom_components/hacs
custom_components/localtuya custom_components/localtuya
.coverage
htmlcov

30
.vscode/launch.json vendored
View File

@@ -1,18 +1,14 @@
{ {
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0", "version": "0.2.0",
"configurations": [ "configurations": [
{ {
"name": "Home Assistant (debug)", "name": "Home Assistant (debug)",
"type": "python", "type": "debugpy",
"request": "launch", "request": "launch",
"module": "homeassistant", "module": "homeassistant",
"justMyCode": false, "justMyCode": false,
"args": [ "args": ["--debug", "-c", "config"]
"--debug", }
"-c", ]
"config" }
]
}
]
}

13
.vscode/settings.json vendored
View File

@@ -1,21 +1,18 @@
{ {
"[python]": { "[python]": {
"editor.defaultFormatter": "ms-python.black-formatter", "editor.defaultFormatter": "ms-python.black-formatter",
"editor.formatOnSave": true "editor.formatOnSave": true,
"editor.formatOnSaveMode": "modifications"
}, },
"pylint.lintOnChange": false,
"files.associations": { "files.associations": {
"*.yaml": "home-assistant" "*.yaml": "home-assistant"
}, },
"python.testing.pytestArgs": [ "python.testing.pytestArgs": [],
"tests"
],
"python.testing.unittestEnabled": false, "python.testing.unittestEnabled": false,
"python.testing.pytestEnabled": true, "python.testing.pytestEnabled": true,
"python.analysis.extraPaths": [ "python.analysis.extraPaths": [
// "/home/vscode/core", // "/home/vscode/core",
"/workspaces/versatile_thermostat/custom_components/versatile_thermostat", "/workspaces/versatile_thermostat/custom_components/versatile_thermostat",
"/home/vscode/.local/lib/python3.11/site-packages/homeassistant" "/home/vscode/.local/lib/python3.12/site-packages/homeassistant"
], ]
"python.formatting.provider": "none"
} }

6
.vscode/tasks.json vendored
View File

@@ -13,6 +13,12 @@
"command": "./container restart", "command": "./container restart",
"problemMatcher": [] "problemMatcher": []
}, },
{
"label": "Start coverage",
"type": "shell",
"command": "./container coverage",
"problemMatcher": []
},
{ {
"label": "Home Assistant translations update", "label": "Home Assistant translations update",
"type": "shell", "type": "shell",

View File

@@ -4,11 +4,15 @@
[![hacs][hacs_badge]][hacs] [![hacs][hacs_badge]][hacs]
[![BuyMeCoffee][buymecoffeebadge]][buymecoffee] [![BuyMeCoffee][buymecoffeebadge]][buymecoffee]
![Tip](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/icon.png?raw=true) ![Tip](images/icon.png)
> ![Tip](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/tips.png?raw=true) 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. ;-). > ![Tip](images/tips.png) 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. ;-).
- [Changements majeurs dans la version 5.0](#changements-majeurs-dans-la-version-50) - [Changements dans la version 6.0](#changements-dans-la-version-60)
- [Entités de température pour les pre-réglages](#entités-de-température-pour-les-pre-réglages)
- [Dans le cas d'une configuration centrale](#dans-le-cas-dune-configuration-centrale)
- [Refonte du menu de configuration](#refonte-du-menu-de-configuration)
- [Les options de menu 'Configuration incomplète' et 'Finaliser'](#les-options-de-menu-configuration-incomplète-et-finaliser)
- [Merci pour la bière buymecoffee](#merci-pour-la-bière-buymecoffee) - [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) - [Quand l'utiliser et ne pas l'utiliser](#quand-lutiliser-et-ne-pas-lutiliser)
- [Incompatibilités](#incompatibilités) - [Incompatibilités](#incompatibilités)
@@ -17,23 +21,32 @@
- [HACS installation (recommendé)](#hacs-installation-recommendé) - [HACS installation (recommendé)](#hacs-installation-recommendé)
- [Installation manuelle](#installation-manuelle) - [Installation manuelle](#installation-manuelle)
- [Configuration](#configuration) - [Configuration](#configuration)
- [Création d'un nouveau Versatile Thermostat](#création-dun-nouveau-versatile-thermostat)
- [Choix des attributs de base](#choix-des-attributs-de-base) - [Choix des attributs de base](#choix-des-attributs-de-base)
- [Sélectionnez des entités pilotées](#sélectionnez-des-entités-pilotées) - [Sélectionnez des entités pilotées (sous-jacents)](#sélectionnez-des-entités-pilotées-sous-jacents)
- [Pour un thermostat de type ```thermostat_over_switch```](#pour-un-thermostat-de-type-thermostat_over_switch) - [Pour un thermostat de type ```thermostat_over_switch```](#pour-un-thermostat-de-type-thermostat_over_switch)
- [Pour un thermostat de type ```thermostat_over_climate```:](#pour-un-thermostat-de-type-thermostat_over_climate) - [Pour un thermostat de type ```thermostat_over_climate```:](#pour-un-thermostat-de-type-thermostat_over_climate)
- [L'auto-régulation](#lauto-régulation) - [L'auto-régulation](#lauto-régulation)
- [L'auto-régulation en mode Expert](#lauto-régulation-en-mode-expert) - [L'auto-régulation en mode Expert](#lauto-régulation-en-mode-expert)
- [Compensation de la température interne](#compensation-de-la-température-interne)
- [Synthèse de l'algorithme d'auto-régulation](#synthèse-de-lalgorithme-dauto-régulation)
- [Le mode auto-fan](#le-mode-auto-fan) - [Le mode auto-fan](#le-mode-auto-fan)
- [Pour un thermostat de type ```thermostat_over_valve```:](#pour-un-thermostat-de-type-thermostat_over_valve) - [Pour un thermostat de type ```thermostat_over_valve```:](#pour-un-thermostat-de-type-thermostat_over_valve)
- [Configurez les coefficients de l'algorithme TPI](#configurez-les-coefficients-de-lalgorithme-tpi) - [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 températures préréglées](#configurer-les-températures-préréglées)
- [Configurer les portes/fenêtres en allumant/éteignant les thermostats](#configurer-les-portesfenêtres-en-allumantéteignant-les-thermostats) - [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 capteur](#le-mode-capteur)
- [Le mode auto](#le-mode-auto) - [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 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 gestion de la puissance](#configurer-la-gestion-de-la-puissance)
- [Configurer la présence ou l'occupation](#configurer-la-présence-ou-loccupation) - [Configurer la présence (ou l'absence)](#configurer-la-présence-ou-labsence)
- [Configuration avancée](#configuration-avancée) - [Configuration avancée](#configuration-avancée)
- [Le contrôle centralisé](#le-contrôle-centralisé)
- [Le contrôle d'une chaudière centrale](#le-contrôle-dune-chaudière-centrale)
- [Configuration](#configuration-1)
- [Comment trouver le bon service ?](#comment-trouver-le-bon-service-)
- [Les évènements](#les-évènements)
- [Avertissement](#avertissement)
- [Synthèse des paramètres](#synthèse-des-paramètres) - [Synthèse des paramètres](#synthèse-des-paramètres)
- [Exemples de réglage](#exemples-de-réglage) - [Exemples de réglage](#exemples-de-réglage)
- [Chauffage électrique](#chauffage-électrique) - [Chauffage électrique](#chauffage-électrique)
@@ -62,26 +75,43 @@
- [Dépannages](#dépannages) - [Dépannages](#dépannages)
- [Utilisation d'un Heatzy](#utilisation-dun-heatzy) - [Utilisation d'un Heatzy](#utilisation-dun-heatzy)
- [Utilisation d'un radiateur avec un fil pilote](#utilisation-dun-radiateur-avec-un-fil-pilote) - [Utilisation d'un radiateur avec un fil pilote](#utilisation-dun-radiateur-avec-un-fil-pilote)
- [Utilisation d'un radiateur avec un fil pilote](#utilisation-dun-radiateur-avec-un-fil-pilote-1)
- [Seul le premier radiateur chauffe](#seul-le-premier-radiateur-chauffe) - [Seul le premier radiateur chauffe](#seul-le-premier-radiateur-chauffe)
- [Le radiateur chauffe alors que la température de consigne est dépassée ou ne chauffe pas alors que la température de la pièce est bien en-dessous de la consigne](#le-radiateur-chauffe-alors-que-la-température-de-consigne-est-dépassée-ou-ne-chauffe-pas-alors-que-la-température-de-la-pièce-est-bien-en-dessous-de-la-consigne)
- [Type `over_switch` ou `over_valve`](#type-over_switch-ou-over_valve)
- [Type `over_climate`](#type-over_climate)
- [Régler les paramètres de détection d'ouverture de fenêtre en mode auto](#régler-les-paramètres-de-détection-douverture-de-fenêtre-en-mode-auto) - [Régler les paramètres de détection d'ouverture de fenêtre en mode auto](#régler-les-paramètres-de-détection-douverture-de-fenêtre-en-mode-auto)
- [Pourquoi mon Versatile Thermostat se met en Securite ?](#pourquoi-mon-versatile-thermostat-se-met-en-securite-) - [Pourquoi mon Versatile Thermostat se met en Securite ?](#pourquoi-mon-versatile-thermostat-se-met-en-securite-)
- [Comment détecter le mode sécurité ?](#comment-détecter-le-mode-sécurité-) - [Comment détecter le mode sécurité ?](#comment-détecter-le-mode-sécurité-)
- [Comment être averti lorsque cela se produit ?](#comment-être-averti-lorsque-cela-se-produit-) - [Comment être averti lorsque cela se produit ?](#comment-être-averti-lorsque-cela-se-produit-)
- [Comment réparer ?](#comment-réparer-) - [Comment réparer ?](#comment-réparer-)
- [Utilisation d'un groupe de personnes comme capteur de présence](#utilisation-dun-groupe-de-personnes-comme-capteur-de-présence) - [Utilisation d'un groupe de personnes comme capteur de présence](#utilisation-dun-groupe-de-personnes-comme-capteur-de-présence)
- [Activer les logs du Versatile Thermostat](#activer-les-logs-du-versatile-thermostat)
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. 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.
> ![Nouveau](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/new-icon.png?raw=true) _*Nouveautés*_ > ![Nouveau](images/new-icon.png) _*Historique des dernières versions*_
> * **Release 6.0** :
> - Ajout d'entités du domaine Number permettant de configurer les températures des presets [354](https://github.com/jmcollin78/versatile_thermostat/issues/354)
> - Refonte complète du menu de configuration pour supprimer les températures et utililsation d'un menu au lieu d'un tunnel de configuration [354](https://github.com/jmcollin78/versatile_thermostat/issues/354)
> * **Release 5.4** :
> - Ajout du pas de température [#311](https://github.com/jmcollin78/versatile_thermostat/issues/311),
> - ajout de seuils de régulation pour les `over_valve` pour éviter de trop vider la batterie des TRV [#338](https://github.com/jmcollin78/versatile_thermostat/issues/338),
> - ajout d'une option permettant d'utiliser la température interne d'un TRV pour forcer l' auto-régulation [#348](https://github.com/jmcollin78/versatile_thermostat/issues/348),
> - ajout d'une fonction de keep-alive pour les VTherm `over_switch` [#345](https://github.com/jmcollin78/versatile_thermostat/issues/345)
> * **Release 5.3** : Ajout d'une fonction de pilotage d'une chaudière centrale [#234](https://github.com/jmcollin78/versatile_thermostat/issues/234) - plus d'infos ici: [Le contrôle d'une chaudière centrale](#le-contrôle-dune-chaudière-centrale). Ajout de la possibilité de désactiver le mode sécurité pour le thermomètre extérieur [#343](https://github.com/jmcollin78/versatile_thermostat/issues/343)
> * **Release 5.2** : Ajout d'un `central_mode` permettant de piloter tous les VTherms de façon centralisée [#158](https://github.com/jmcollin78/versatile_thermostat/issues/158).
> * **Release 5.1** : Limitation des valeurs envoyées aux valves et au température envoyées au climate sous-jacent.
> * **Release 5.0** : Ajout d'une configuration centrale permettant de mettre en commun les attributs qui peuvent l'être [#239](https://github.com/jmcollin78/versatile_thermostat/issues/239). > * **Release 5.0** : Ajout d'une configuration centrale permettant de mettre en commun les attributs qui peuvent l'être [#239](https://github.com/jmcollin78/versatile_thermostat/issues/239).
<details>
<summary>Autres versions</summary>
> * **Release 4.3** : Ajout d'un mode auto-fan pour le type `over_climate` permettant d'activer la ventilation si l'écart de température est important [#223](https://github.com/jmcollin78/versatile_thermostat/issues/223). > * **Release 4.3** : Ajout d'un mode auto-fan pour le type `over_climate` permettant d'activer la ventilation si l'écart de température est important [#223](https://github.com/jmcollin78/versatile_thermostat/issues/223).
> * **Release 4.2** : Le calcul de la pente de la courbe de température se fait maintenant en °/heure et non plus en °/min [#242](https://github.com/jmcollin78/versatile_thermostat/issues/242). Correction de la détection automatique des ouvertures par l'ajout d'un lissage de la courbe de température . > * **Release 4.2** : Le calcul de la pente de la courbe de température se fait maintenant en °/heure et non plus en °/min [#242](https://github.com/jmcollin78/versatile_thermostat/issues/242). Correction de la détection automatique des ouvertures par l'ajout d'un lissage de la courbe de température .
> * **Release 4.1** : Ajout d'un mode de régulation **Expert** dans lequel l'utilisateur peut spécifier ses propres paramètres d'auto-régulation au lieu d'utiliser les pre-programmés [#194](https://github.com/jmcollin78/versatile_thermostat/issues/194). > * **Release 4.1** : Ajout d'un mode de régulation **Expert** dans lequel l'utilisateur peut spécifier ses propres paramètres d'auto-régulation au lieu d'utiliser les pre-programmés [#194](https://github.com/jmcollin78/versatile_thermostat/issues/194).
> * **Release 4.0** : Ajout de la prise en charge de la **Versatile Thermostat UI Card**. Voir [Versatile Thermostat UI Card](https://github.com/jmcollin78/versatile-thermostat-ui-card). Ajout d'un mode de régulation **Slow** pour les appareils de chauffage à latence lente [#168](https://github.com/jmcollin78/versatile_thermostat/issues/168). Changement de la façon dont **la puissance est calculée** dans le cas de VTherm avec des équipements multi-sous-jacents [#146](https://github.com/jmcollin78/versatile_thermostat/issues/146). Ajout de la prise en charge de AC et Heat pour VTherm via un interrupteur également [#144](https://github.com/jmcollin78/versatile_thermostat/pull/144) > * **Release 4.0** : Ajout de la prise en charge de la **Versatile Thermostat UI Card**. Voir [Versatile Thermostat UI Card](https://github.com/jmcollin78/versatile-thermostat-ui-card). Ajout d'un mode de régulation **Slow** pour les appareils de chauffage à latence lente [#168](https://github.com/jmcollin78/versatile_thermostat/issues/168). Changement de la façon dont **la puissance est calculée** dans le cas de VTherm avec des équipements multi-sous-jacents [#146](https://github.com/jmcollin78/versatile_thermostat/issues/146). Ajout de la prise en charge de AC et Heat pour VTherm via un interrupteur également [#144](https://github.com/jmcollin78/versatile_thermostat/pull/144)
<details>
<summary>Autres versions</summary>
> * **Release 3.8**: Ajout d'une **fonction d'auto-régulation** pour les thermostats `over climate` dont la régulation est faite par le climate sous-jacent. Cf. [L'auto-régulation](#lauto-régulation) et [#129](https://github.com/jmcollin78/versatile_thermostat/issues/129). Ajout de la **possibilité d'inverser la commande** pour un thermostat `over switch` pour adresser les installations avec fil pilote et diode [#124](https://github.com/jmcollin78/versatile_thermostat/issues/124). > * **Release 3.8**: Ajout d'une **fonction d'auto-régulation** pour les thermostats `over climate` dont la régulation est faite par le climate sous-jacent. Cf. [L'auto-régulation](#lauto-régulation) et [#129](https://github.com/jmcollin78/versatile_thermostat/issues/129). Ajout de la **possibilité d'inverser la commande** pour un thermostat `over switch` pour adresser les installations avec fil pilote et diode [#124](https://github.com/jmcollin78/versatile_thermostat/issues/124).
> * **Release 3.7**: Ajout du type de **Versatile Thermostat `over valve`** pour piloter une vanne TRV directement ou tout autre équipement type gradateur pour le chauffage. La régulation se fait alors directement en agissant sur le pourcentage d'ouverture de l'entité sous-jacente : 0 la vanne est coupée, 100 : la vanne est ouverte à fond. Cf. [#131](https://github.com/jmcollin78/versatile_thermostat/issues/131). Ajout d'une fonction permettant le bypass de la détection d'ouverture [#138](https://github.com/jmcollin78/versatile_thermostat/issues/138). Ajout de la langue Slovaque > * **Release 3.7**: Ajout du type de **Versatile Thermostat `over valve`** pour piloter une vanne TRV directement ou tout autre équipement type gradateur pour le chauffage. La régulation se fait alors directement en agissant sur le pourcentage d'ouverture de l'entité sous-jacente : 0 la vanne est coupée, 100 : la vanne est ouverte à fond. Cf. [#131](https://github.com/jmcollin78/versatile_thermostat/issues/131). Ajout d'une fonction permettant le bypass de la détection d'ouverture [#138](https://github.com/jmcollin78/versatile_thermostat/issues/138). Ajout de la langue Slovaque
> * **Release 3.6**: Ajout du paramètre `motion_off_delay` pour améliorer la gestion de des mouvements [#116](https://github.com/jmcollin78/versatile_thermostat/issues/116), [#128](https://github.com/jmcollin78/versatile_thermostat/issues/128). Ajout du mode AC (air conditionné) pour un VTherm over switch. Préparation du projet Github pour faciliter les contributions [#127](https://github.com/jmcollin78/versatile_thermostat/issues/127) > * **Release 3.6**: Ajout du paramètre `motion_off_delay` pour améliorer la gestion de des mouvements [#116](https://github.com/jmcollin78/versatile_thermostat/issues/116), [#128](https://github.com/jmcollin78/versatile_thermostat/issues/128). Ajout du mode AC (air conditionné) pour un VTherm over switch. Préparation du projet Github pour faciliter les contributions [#127](https://github.com/jmcollin78/versatile_thermostat/issues/127)
@@ -96,8 +126,76 @@ Ce composant personnalisé pour Home Assistant est une mise à niveau et est une
> * **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. > * **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.
</details> </details>
# Changements majeurs dans la version 5.0 # Changements dans la version 6.0
![Nouveau](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/new-icon.png?raw=true)
## Entités de température pour les pre-réglages
Les températures des presets sont maintenant directement acessibles sous la forme d'entités reliés au VTherm.
Exemple :
![Entités température](images/temp-entities-1.png)
Les entités Boost, Confort, Eco et Hors-gel permettent de régler directement les températures de ces présets sans avoir à reconfigurer le VTHerm dans les écrans de configuration.
Ces modifications sont persistentent à un redémarrage et sont prises en compte immédiatement par le VTherm.
En fonction des fonctions activées, la liste des températures peut être plus ou moins complète :
1. Si la gestion de présence est activée, les presets en cas d'absence sont créés. Ils sont suffixés par 'abs' pour absence,
2. Si la gestion de la climatisation (Mode AC) est activé, les presets en mode clim sont créés. Ils sont suffixés par 'clim' pour climatisation. Seul le preset Hors gel n'a pas d'équivalent en mode clim,
3. Les différentes combinaison absent et clim peuvent être créés en fonction de la configuration du VTherm
Si un VTherm utilise les preset de la configuration centrale, ces entités ne sont pas créées, car les températures des presets sont gérés par la configuration centrale.
### Dans le cas d'une configuration centrale
Si vous avez configuré une configuration centrale, celle-ci possède aussi ses propres presets qui répondent au même règles qu'énoncées ci-dessus.
Exemple d'une configuration centrale avec gestion de présence et mode AC (climatisation) :
![Entités température](images/temp-entities-2.png)
Dans le cas d'un changement d'une température de la configuration centrale, tous les VTherm qui utilisent ce preset sont immédiatement mis à jour.
## Refonte du menu de configuration
Le menu de configuration a été totalement revu. Il s'adapte dynamiquement aux choix de l'utilisateur et permet d'accéder directement aux réglages de la fonction voulue sans avoir à dérouler tous le tunnel de configuration.
Pour créer un nouveau VTherm, il faudra d'abord choisir le type de VTherm :
![Choix VTherm](images/config-main0.png)
Puis, vous accédez maintenant au menu de configuration suivant :
![VTherm menu](images/config-menu.png)
Chaque partie à configurer est accessible directement, sans avoir à dérouler tout le tunnel de configuration comme précédemment.
Vous noterez l'option de menu nommée `Fonctions` qui permet de choisir quelles fonctions vont être implémentées pour ce VTherm :
![VTherm fonctions](images/config-features.png)
En fonction de vos choix, le menu principal s'adaptera pour ajouter les options nécessaires.
Exemple de menu avec toutes les fonctions cochées :
![VTherm menu](images/config-menu-all-options.png)
Vous pouvez constater que les options 'Détection des ouvertures', 'Détection de mouvement', 'Gestion de la puissance' et 'Gestion de présence' ont été ajoutées. Vous pouvez alors les configurer.
### Les options de menu 'Configuration incomplète' et 'Finaliser'
La dernière option du menu est spéciale. Elle permet de valider la création du VTherm lorsque toutes les fonctions ont été correctement configurées.
Si l'une options n'est pas correctement configurée, la dernière option est la suivante :
![Configuration incomplète](images/config-not-complete.png)
Sa sélection ne fait rien mais vous empêche de finaliser la création (resp. la modification) du VTherm.
**Vous devez alors chercher dans les options laquelle manque**.
Une fois que toute la configuration est valide, la dernière option se transforme en :
![Configuration complète](images/config-complete.png)
Cliquez sur cette option pour créér (resp. modifier) le VTherm :
![Configuration terminée](images/config-terminate.png)
<details>
<summary>Changements dans la version 5.0</summary>
Vous pouvez maintenant définir une configuration centrale qui va vous permettre de mettre en commun sur tous vos VTherms (ou seulement une partie), certains attributs. Pour utiliser cette possibilité, vous devez : Vous pouvez maintenant définir une configuration centrale qui va vous permettre de mettre en commun sur tous vos VTherms (ou seulement une partie), certains attributs. Pour utiliser cette possibilité, vous devez :
1. Créer un VTherm de type "Configuration Centrale", 1. Créer un VTherm de type "Configuration Centrale",
@@ -111,10 +209,10 @@ Lors d'un changement sur la configuration centrale, tous les VTherms seront rech
En conséquence toute la phase de paramètrage d'un VTherm a été profondemment modifiée pour pouvoir utiliser la configuration centrale ou surcharger les valeurs de la configuration centrale par des valeurs propre au VTherm en cours de configuration. En conséquence toute la phase de paramètrage d'un VTherm a été profondemment modifiée pour pouvoir utiliser la configuration centrale ou surcharger les valeurs de la configuration centrale par des valeurs propre au VTherm en cours de configuration.
**Note :** les copies d'écran de la configuration d'un VTherm n'ont pas été mises à jour. </details>
# Merci pour la bière [buymecoffee](https://www.buymeacoffee.com/jmcollin78) # Merci pour la bière [buymecoffee](https://www.buymeacoffee.com/jmcollin78)
Un grand merci à @salabur, @pvince83, @bergoglio, @EPicLURcher, @ecolorado66, @Kriss1670, @maia, @f.maymil, @moutte69, @Jerome, @Gunnar M, @Greg.o, @John Burgess, @abyssmal, @capinfo26, @Helge, @MattG @Mexx62, @Someone, @Lajull pour les bières. Ca fait très plaisir et ça m'encourage à continuer ! Un grand merci à @salabur, @pvince83, @bergoglio, @EPicLURcher, @ecolorado66, @Kriss1670, @maia, @f.maymil, @moutte69, @Jerome, @Gunnar M, @Greg.o, @John Burgess, @abyssmal, @capinfo26, @Helge, @MattG @Mexx62, @Someone, @Lajull, @giopeco, @fredericselier, @philpagan, @studiogriffanti, @Edwin, @Sebbou, @Gerard R., @John Burgess, @Sylvoliv, @cdenfert, @stephane.l, @jms92100 pour les bières. Ca fait très plaisir et ça m'encourage à continuer !
# Quand l'utiliser et ne pas l'utiliser # Quand l'utiliser et ne pas l'utiliser
@@ -137,6 +235,8 @@ Certains thermostat de type TRV sont réputés incompatibles avec le Versatile T
2. Les thermostats « Homematic » (et éventuellement Homematic IP) sont connus pour rencontrer des problèmes avec le Versatile Thermostat en raison des limitations du protocole RF sous-jacent. Ce problème se produit particulièrement lorsque vous essayez de contrôler plusieurs thermostats Homematic à la fois dans une seule instance de VTherm. Afin de réduire la charge du cycle de service, vous pouvez par ex. regroupez les thermostats avec des procédures spécifiques à Homematic (par exemple en utilisant un thermostat mural) et laissez Versatile Thermostat contrôler uniquement le thermostat mural directement. Une autre option consiste à contrôler un seul thermostat et à propager les changements de mode CVC et de température par un automatisme, 2. Les thermostats « Homematic » (et éventuellement Homematic IP) sont connus pour rencontrer des problèmes avec le Versatile Thermostat en raison des limitations du protocole RF sous-jacent. Ce problème se produit particulièrement lorsque vous essayez de contrôler plusieurs thermostats Homematic à la fois dans une seule instance de VTherm. Afin de réduire la charge du cycle de service, vous pouvez par ex. regroupez les thermostats avec des procédures spécifiques à Homematic (par exemple en utilisant un thermostat mural) et laissez Versatile Thermostat contrôler uniquement le thermostat mural directement. Une autre option consiste à contrôler un seul thermostat et à propager les changements de mode CVC et de température par un automatisme,
3. les thermostats de type Heatzy qui ne supportent pas les commandes de type set_temperature 3. les thermostats de type Heatzy qui ne supportent pas les commandes de type set_temperature
4. les thermostats de type Rointe ont tendance a se réveiller tout seul. Le reste fonctionne normalement. 4. les thermostats de type Rointe ont tendance a se réveiller tout seul. Le reste fonctionne normalement.
5. les TRV de type Aqara SRTS-A01 et MOES TV01-ZB qui n'ont pas le retour d'état `hvac_action` permettant de savoir si elle chauffe ou pas. Donc les retours d'état sont faussés, le reste à l'air fonctionnel.
6. La clim Airwell avec l'intégration "Midea AC LAN". Si 2 commandes de VTherm sont trop rapprochées, la clim s'arrête d'elle même.
# Pourquoi une nouvelle implémentation du thermostat ? # Pourquoi une nouvelle implémentation du thermostat ?
@@ -150,7 +250,9 @@ Ce composant nommé __Versatile thermostat__ gère les cas d'utilisation suivant
- 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é. - 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. - 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é. - 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. - Ajouter des capteurs pour voir les états internes du thermostat,
- Contrôle centralisé de tous les Versatile Thermostat pour les stopper tous, les passer tous en hors-gel, les forcer en mode Chauffage (l'hiver), les forcer en mode Climatisation (l'été).
- Contrôle d'une chaudière centrale et des VTherm qui doivent contrôler cette chaudière.
# Comment installer cet incroyable Thermostat Versatile ? # Comment installer cet incroyable Thermostat Versatile ?
@@ -175,19 +277,42 @@ Ce composant nommé __Versatile thermostat__ gère les cas d'utilisation suivant
# Configuration # 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. -- VTherm = Versatile Thermostat dans la suite de ce document --
> ![Astuce](images/tips.png) _*Notes*_
>
> Trois façons de configurer les VTherms sont disponibles :
> 1. Chaque Versatile Thermostat est entièrement configurée de manière indépendante. Choisissez cette option si vous ne souhaitez avoir aucune configuration ou gestion centrale.
> 2. Certains aspects sont configurés de manière centralisée. Cela permet par ex. définir la température min/max, la détection de fenêtre ouverte,… au niveau d'une instance centrale et unique. Pour chaque VTherm que vous configurez, vous pouvez alors choisir d'utiliser la configuration centrale ou de la remplacer par des paramètres personnalisés.
> 3. En plus de cette configuration centralisée, tous les VTherm peuvent être contrôlées par une seule entité de type `select`. Cette fonction est nommé `central_mode`. Cela permet de stopper / démarrer / mettre en hors gel / etc tous les VTherms en une seule fois. Pour chaque VTherm, l'utilisateur indique si il est concerné par ce `central_mode`.
<details>
<summary>Création d'un nouveau Versatile Thermostat</summary>
## Création d'un nouveau Versatile Thermostat
Cliquez sur le bouton Ajouter une intégration dans la page d'intégration Cliquez sur le bouton Ajouter une intégration dans la page d'intégration
![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/add-an-integration.png?raw=true) ![image](images/add-an-integration.png)
puis
![image](images/config-main0.png)
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. 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 : Suivez ensuite les étapes de configuration en sélectionnant dans le menu l'option à configurer.
</details>
<details>
<summary>Choix des attributs de base</summary>
## Choix des attributs de base ## Choix des attributs de base
![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/config-main.png?raw=true) Choisisez le menu "Principaux attributs".
![image](images/config-main.png)
Donnez les principaux attributs obligatoires : Donnez les principaux attributs obligatoires :
1. un nom (sera le nom de l'intégration et aussi le nom de l'entité climate) 1. un nom (sera le nom de l'intégration et aussi le nom de l'entité climate)
@@ -197,16 +322,22 @@ Donnez les principaux attributs obligatoires :
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). En mode ```over_climate```, le cycle ne sert qu'à faire des controles de base mais ne régule pas directement la température. C'est le ```climate``` sous-jacent qui le fait, 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). En mode ```over_climate```, le cycle ne sert qu'à faire des controles de base mais ne régule pas directement la température. C'est le ```climate``` sous-jacent qui le fait,
7. les températures minimales et maximales du thermostat, 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, 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. 9. la possibilité de controler le thermostat de façon centralisée. Cf [controle centralisé](#le-contrôle-centralisé),
10. 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.
> ![Astuce](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/tips.png?raw=true) _*Notes*_ > ![Astuce](images/tips.png) _*Notes*_
> 1. avec les types ```over_switch``` et ```over_valve```, 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**, > 1. avec les types ```over_switch``` et ```over_valve```, 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. Pour le radiateur à accumulation par exemple il sera sollicité inutilement. > 2. si le cycle est trop court, le radiateur ne pourra jamais atteindre la température cible. Pour le radiateur à accumulation par exemple il sera sollicité inutilement.
</details>
<details>
<summary>Sélectionnez des entités pilotées (sous-jacents)</summary>
## Sélectionnez des entités pilotées (sous-jacents)
## 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`, `climate` ou `number`. Seules les entités compatibles avec le type sont présentées. En fonction de votre choix sur le type de thermostat, vous devrez choisir une ou plusieurs entités de type `switch`, `climate` ou `number`. Seules les entités compatibles avec le type sont présentées.
> ![Astuce](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/tips.png?raw=true) _*Comment choisir le type*_ > ![Astuce](images/tips.png) _*Comment choisir le type*_
> Le choix du type est important. Même si il toujours possible de le modifier ensuite via l'IHM de configuration, il est préférable de se poser les quelques questions suivantes : > Le choix du type est important. Même si il toujours possible de le modifier ensuite via l'IHM de configuration, il est préférable de se poser les quelques questions suivantes :
> 1. **quel type d'équipement je vais piloter ?** Dans l'ordre voici ce qu'il faut faire : > 1. **quel type d'équipement je vais piloter ?** Dans l'ordre voici ce qu'il faut faire :
> 1. si vous avez une vanne thermostatique (TRV) commandable dans Home Assistant via une entité de type ```number``` (par exemple une _Shelly TRV_), choisissez le type `over_valve`. C'est le type le plus direct et qui assure la meilleure régulation, > 1. si vous avez une vanne thermostatique (TRV) commandable dans Home Assistant via une entité de type ```number``` (par exemple une _Shelly TRV_), choisissez le type `over_valve`. C'est le type le plus direct et qui assure la meilleure régulation,
@@ -215,18 +346,21 @@ En fonction de votre choix sur le type de thermostat, vous devrez choisir une ou
> 2. **quelle type de régulation je veux ?** Si l'équipement piloté possède son propre mécanisme de régulation (clim, certaine vanne TRV) et que cette régulation fonctionne bien, optez pour un ```over_climate``` > 2. **quelle type de régulation je veux ?** Si l'équipement piloté possède son propre mécanisme de régulation (clim, certaine vanne TRV) et que cette régulation fonctionne bien, optez pour un ```over_climate```
### Pour un thermostat de type ```thermostat_over_switch``` ### Pour un thermostat de type ```thermostat_over_switch```
![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/config-linked-entity.png?raw=true) ![image](images/config-linked-entity.png)
Certains équipements nécessitent d'être périodiquement sollicités pour empêcher un arrêt de sécurité. Connu sous le nom de "keep-alive" cette fonction est activable en entrant un nombre de secondes non nul dans le champ d'intervalle keep-alive du thermostat. Pour désactiver la fonction ou en cas de doute, laissez-le vide ou entrez zéro (valeur par défaut).
L'algorithme à utiliser est aujourd'hui limité à TPI est disponible. Voir [algorithme](#algorithme). 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. 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é : Exemple de déclenchement synchronisé :
![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/multi-switch-activation.png?raw=true) ![image](images/multi-switch-activation.png)
Il est possible de choisir un thermostat over switch qui commande une climatisation en cochant la case "AC Mode". Dans ce cas, seul le mode refroidissement sera visible. Il est possible de choisir un thermostat over switch qui commande une climatisation en cochant la case "AC Mode". Dans ce cas, seul le mode refroidissement sera visible.
Si votre équipement est commandé par un fil pilote avec un diode, vous aurez certainement besoin de cocher la case "Inverser la case". Elle permet de mettre le switch à On lorsqu'on doit étiendre l'équipement et à Off lorsqu'on doit l'allumer. Si votre équipement est commandé par un fil pilote avec un diode, vous aurez certainement besoin de cocher la case "Inverser la case". Elle permet de mettre le switch à On lorsqu'on doit étiendre l'équipement et à Off lorsqu'on doit l'allumer.
### Pour un thermostat de type ```thermostat_over_climate```: ### Pour un thermostat de type ```thermostat_over_climate```:
![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/config-linked-entity2.png?raw=true) ![image](images/config-linked-entity2.png)
Il est possible de choisir un thermostat over climate qui commande une climatisation réversible en cochant la case "AC Mode". Dans ce cas, selon l'équipement commandé vous aurez accès au chauffage et/ou au réfroidissement. Il est possible de choisir un thermostat over climate qui commande une climatisation réversible en cochant la case "AC Mode". Dans ce cas, selon l'équipement commandé vous aurez accès au chauffage et/ou au réfroidissement.
@@ -249,7 +383,7 @@ La fonction d'auto-régulation se paramètre avec :
Ces trois paramètres permettent de moduler la régulation et éviter de multiplier les envois de régulation. Certains équipements comme les TRV, les chaudières n'aiment pas qu'on change la consigne de température trop souvent. Ces trois paramètres permettent de moduler la régulation et éviter de multiplier les envois de régulation. Certains équipements comme les TRV, les chaudières n'aiment pas qu'on change la consigne de température trop souvent.
> ![Astuce](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/tips.png?raw=true) _*Conseil de mise en place*_ > ![Astuce](images/tips.png) _*Conseil de mise en place*_
> 1. Ne démarrez pas tout de suite l'auto-régulation. Regardez comment se passe la régulation naturelle de votre équipement. Si vous constatez que la température de consigne n'est pas atteinte ou qu'elle met trop de temps à être atteinte, démarrez la régulation, > 1. Ne démarrez pas tout de suite l'auto-régulation. Regardez comment se passe la régulation naturelle de votre équipement. Si vous constatez que la température de consigne n'est pas atteinte ou qu'elle met trop de temps à être atteinte, démarrez la régulation,
> 2. D'abord commencez par une légère auto-régulation et gardez les deux paramètres avec leur valeurs par défaut. Attendez quelques jours et vérifiez si la situation s'est améliorée, > 2. D'abord commencez par une légère auto-régulation et gardez les deux paramètres avec leur valeurs par défaut. Attendez quelques jours et vérifiez si la situation s'est améliorée,
> 3. Si ce n'est pas suffisant, passez en auto-régulation Medium, attendez une stabilisation, > 3. Si ce n'est pas suffisant, passez en auto-régulation Medium, attendez une stabilisation,
@@ -336,6 +470,37 @@ et bien sur, configurer le mode auto-régulation du VTherm en mode Expert. Tous
Pour que les modifications soient prises en compte, il faut soit **relancer totalement Home Assistant** soit juste l'intégration Versatile Thermostat (Outils de dev / Yaml / rechargement de la configuration / Versatile Thermostat). Pour que les modifications soient prises en compte, il faut soit **relancer totalement Home Assistant** soit juste l'intégration Versatile Thermostat (Outils de dev / Yaml / rechargement de la configuration / Versatile Thermostat).
#### Compensation de la température interne
Quelque fois, il arrive que le thermomètre interne du sous-jacent (TRV, climatisation, ...) soit tellement faux que l' auto-régulation ne suffise pas à réguler.
Cela arrive lorsque le thermomètre interne est trop près de la source de chaleur. La température interne monte alors beaucoup plus vite que la température de la pièce, ce qui génère des défauts dans la régulation.
Exemple :
1. la température de la pièce est 18°, la consigne est à 20°,
2. la température interne de l'équipement est de 22°,
3. si VTherm envoie 21° comme consigne (= 20° + 1° d'auto-regulation), alors l'équipement ne chauffera pas car sa température interne (22°) est au-dessus de la consigne (21°)
Pour palier à ça, une nouvelle option facultative a été ajoutée en version 5.4 : ![Utilisation de la température interne](images/config-use-internal-temp.png)
Lorsqu'elle est activée, cette fonction ajoutera l'écart entre la température interne et la température de la pièce à la consigne pour forcer le chauffage.
Dans l'exemple ci-dessus, l'écart est de +4° (22° - 18°), donc VTherm enverra 25° (21°+4°) à l'équipement le forçant ainsi à chauffer.
Cet écart est calculé pour chaque sous-jacent car chacun à sa propre température interne. Pensez à un VTherm qui serait relié à 3 TRV chacun avec sa température interne par exemple.
On obtient alors une auto-régulation bien plus efficace qui évite l'eccueil des gros écarts de température interne défaillante.
#### Synthèse de l'algorithme d'auto-régulation
L'algorithme d'auto-régulation peut être synthétisé comme suit:
1. initialiser la température cible comme la consigne du VTherm,
1. Si l'auto-régulation est activée,
1. calcule de la température régulée (valable pour un VTherm),
2. prendre cette température comme cible,
2. Pour chaque sous-jacent du VTherm,
1. Si "utiliser la température interne" est cochée,
1. calcule de l'écart (trv internal temp - room temp),
2. ajout de l'écart à la température cible,
3. envoie de la température cible ( = temp regulee + (temp interne - temp pièce)) au sous-jacent
#### Le mode auto-fan #### Le mode auto-fan
Ce mode introduit en 4.3 permet de forcer l'usage de la ventilation si l'écart de température est important. En effet, en activant la ventilation, la répartition se fait plus rapidement ce qui permet de gagner du temps dans l'atteinte de la température cible. Ce mode introduit en 4.3 permet de forcer l'usage de la ventilation si l'écart de température est important. En effet, en activant la ventilation, la répartition se fait plus rapidement ce qui permet de gagner du temps dans l'atteinte de la température cible.
@@ -346,17 +511,21 @@ Si votre équipement ne comprend pas le mode Turbo, le mode Forte` sera utilisé
Une fois l'écart de température redevenu faible, la ventilation se mettra dans un mode "normal" qui dépend de votre équipement à savoir (dans l'ordre) : `Silence (mute)`, `Auto (auto)`, `Faible (low)`. La première valeur qui est possible pour votre équipement sera choisie. Une fois l'écart de température redevenu faible, la ventilation se mettra dans un mode "normal" qui dépend de votre équipement à savoir (dans l'ordre) : `Silence (mute)`, `Auto (auto)`, `Faible (low)`. La première valeur qui est possible pour votre équipement sera choisie.
### Pour un thermostat de type ```thermostat_over_valve```: ### Pour un thermostat de type ```thermostat_over_valve```:
![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/config-linked-entity3.png?raw=true) ![image](images/config-linked-entity3.png)
Vous pouvez choisir jusqu'à entité du domaine ```number``` ou ```ìnput_number``` qui vont commander les vannes. Vous pouvez choisir jusqu'à entité du domaine ```number``` ou ```ìnput_number``` qui vont commander les vannes.
L'algorithme à utiliser est aujourd'hui limité à TPI est disponible. Voir [algorithme](#algorithme). L'algorithme à utiliser est aujourd'hui limité à TPI est disponible. Voir [algorithme](#algorithme).
Il est possible de choisir un thermostat over valve qui commande une climatisation en cochant la case "AC Mode". Dans ce cas, seul le mode refroidissement sera visible. Il est possible de choisir un thermostat over valve qui commande une climatisation en cochant la case "AC Mode". Dans ce cas, seul le mode refroidissement sera visible.
</details>
<details>
<summary>Configurez les coefficients de l'algorithme TPI</summary>
## Configurez les coefficients de l'algorithme TPI ## Configurez les coefficients de l'algorithme TPI
Si vous avez choisi un thermostat de type ```over_switch``` ou ```over_valve``` vous arriverez sur cette page : Si vous avez choisi un thermostat de type ```over_switch``` ou ```over_valve``` et que vous sélectionnez l'option "TPI" vous menu, vous arriverez sur cette page :
![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/config-tpi.png?raw=true) ![image](images/config-tpi.png)
Vous devez donner : Vous devez donner :
1. le coefficient coef_int de l'algorithme TPI, 1. le coefficient coef_int de l'algorithme TPI,
@@ -364,11 +533,12 @@ Vous devez donner :
Pour plus d'informations sur l'algorithme TPI et son réglage, veuillez vous référer à [algorithm](#algorithm). Pour plus d'informations sur l'algorithme TPI et son réglage, veuillez vous référer à [algorithm](#algorithm).
</details>
## Configurer la température préréglée <details>
Cliquez sur 'Valider' sur la page précédente et vous y arriverez : <summary>Configurer les températures préréglées</summary>
![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/config-presets.png?raw=true) ## Configurer les températures préréglées
Le mode préréglé (preset) vous permet de préconfigurer la température ciblée. Utilisé en conjonction avec Scheduler (voir [scheduler](#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 : 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 - **Eco** : l'appareil est en mode d'économie d'énergie
@@ -379,14 +549,21 @@ Le mode préréglé (preset) vous permet de préconfigurer la température cibl
**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. **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.
> ![Astuce](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/tips.png?raw=true) _*Notes*_ Les pré-réglages se font (depuis v6.0) directement depuis les entités du VTherm ou de la configuration centrale si vous utilisez la configuration centrale.
> ![Astuce](images/tips.png) _*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. > 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). > 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). > 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 > 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 > 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
</details>
<details>
<summary>Configurer les portes/fenêtres en allumant/éteignant les thermostats</summary>
## Configurer les portes/fenêtres en allumant/éteignant les thermostats ## 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. 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: La détecttion des ouvertures peut se faire de 2 manières:
1. soit avec un capteur placé sur l'ouverture (mode capteur), 1. soit avec un capteur placé sur l'ouverture (mode capteur),
@@ -394,7 +571,7 @@ La détecttion des ouvertures peut se faire de 2 manières:
### Le mode capteur ### Le mode capteur
En mode capteur, vous devez renseigner les informations suivantes: En mode capteur, vous devez renseigner les informations suivantes:
![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/config-window-sensor.png?raw=true) ![image](images/config-window-sensor.png)
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 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. 2. un **délai en secondes** avant tout changement. Cela permet d'ouvrir rapidement une fenêtre sans arrêter le chauffage.
@@ -402,7 +579,7 @@ En mode capteur, vous devez renseigner les informations suivantes:
### Le mode auto ### Le mode auto
En mode auto, la configuration est la suivante: En mode auto, la configuration est la suivante:
![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/config-window-auto.png?raw=true) ![image](images/config-window-auto.png)
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), 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), 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),
@@ -414,23 +591,28 @@ Pour régler les seuils il est conseillé de commencer avec les valeurs de réf
- durée max : 60 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. 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.
![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/temperature-slope.png?raw=true) ![image](images/temperature-slope.png)
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") : 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") :
![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/window-auto-tuning.png?raw=true) ![image](images/window-auto-tuning.png)
Et c'est tout ! votre thermostat s'éteindra lorsque les fenêtres seront ouvertes et se rallumera lorsqu'il sera fermé. Et c'est tout ! votre thermostat s'éteindra lorsque les fenêtres seront ouvertes et se rallumera lorsqu'il sera fermé.
> ![Astuce](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/tips.png?raw=true) _*Notes*_ > ![Astuce](images/tips.png) _*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/) > 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, > 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, > 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, ...) > 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, ...)
</details>
<details>
<summary>Configurer le mode d'activité ou la détection de mouvement</summary>
## Configurer le mode d'activité ou la détection de mouvement ## 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 : Si vous avez choisi la fonctionnalité ```Avec détection de mouvement```, cliquez sur 'Valider' sur la page précédente et vous y arriverez :
![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/config-motion.png?raw=true) ![image](images/config-motion.png)
Nous allons maintenant voir comment configurer le nouveau mode Activité. Nous allons maintenant voir comment configurer le nouveau mode Activité.
Ce dont nous avons besoin: Ce dont nous avons besoin:
@@ -450,32 +632,41 @@ Alors imaginons que nous voulions avoir le comportement suivant :
Pour que cela fonctionne, le thermostat doit être en mode préréglé « Activité ». Pour que cela fonctionne, le thermostat doit être en mode préréglé « Activité ».
> ![Astuce](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/tips.png?raw=true) _*Notes*_ > ![Astuce](images/tips.png) _*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 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
</details>
<details>
<summary>Configurer la gestion de la puissance</summary>
## Configurer la gestion de la puissance ## 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 : 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 :
![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/config-power.png?raw=true) ![image](images/config-power.png)
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 totale des équipements du VTherm** (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. 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 totale des équipements du VTherm** (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). 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. Cela vous permet de modifier la puissance maximale au fil du temps à l'aide d'un planificateur ou de ce que vous voulez.
> ![Astuce](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/tips.png?raw=true) _*Notes*_ > ![Astuce](images/tips.png) _*Notes*_
> 1. En cas de délestage, le radiateur est réglé sur le préréglage nommé ```power```. Il s'agit d'un préréglage caché, vous ne pouvez pas le sélectionner manuellement. > 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. > 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. > 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 > 4. Si vous ne souhaitez pas utiliser cette fonctionnalité, laissez simplement l'identifiant des entités vide
> 5. Si vous controlez plusieurs radiateurs, la **consommation électrique de votre chauffage** renseigné doit correspondre à la somme des puissances. > 5. Si vous controlez plusieurs radiateurs, la **consommation électrique de votre chauffage** renseigné doit correspondre à la somme des puissances.
</details>
<details>
<summary>Configurer la présence (ou l'absence)</summary>
## Configurer la présence (ou l'absence)
## 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). 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 configurer la présence remplissez ce formulaire :
![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/config-presence.png?raw=true) ![image](images/config-presence.png)
Pour cela, vous devez configurer : 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, 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,
@@ -487,15 +678,20 @@ Si le mode AC est utilisé, vous pourrez aussi configurer les températures lors
ATTENTION : les groupes de personnes ne fonctionnent pas en tant que capteur de présence. Ils ne sont pas reconnus comme un capteur de présence. Vous devez utiliser, un template comme décrit ici [Utilisation d'un groupe de personnes comme capteur de présence](#utilisation-dun-groupe-de-personnes-comme-capteur-de-présence). ATTENTION : les groupes de personnes ne fonctionnent pas en tant que capteur de présence. Ils ne sont pas reconnus comme un capteur de présence. Vous devez utiliser, un template comme décrit ici [Utilisation d'un groupe de personnes comme capteur de présence](#utilisation-dun-groupe-de-personnes-comme-capteur-de-présence).
> ![Astuce](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/tips.png?raw=true) _*Notes*_ > ![Astuce](images/tips.png) _*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, > 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. > 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.
</details>
<details>
<summary>Configuration avancée</summary>
## Configuration avancée ## Configuration avancée
Ces paramètres permettent d'affiner le réglage du thermostat. Ces paramètres permettent d'affiner le réglage du thermostat.
Le formulaire de configuration avancée est le suivant : Le formulaire de configuration avancée est le suivant :
![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/config-advanced.png?raw=true) ![image](images/config-advanced.png)
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 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.
@@ -506,14 +702,152 @@ Mettre ce paramètre à ``0.00`` déclenchera le préréglage sécurité quelque
Le quatrième param§tre (``security_default_on_percent``) est la valeur de ``on_percent`` qui sera utilisée lorsque le thermostat passe en mode ``security``. Si vous mettez ``0`` alors le thermostat sera coupé lorsqu'il passe en mode ``security``, mettre 0,2% par exemple permet de garder un peu de chauffage (20% dans ce cas), même en mode ``security``. Ca évite de retrouver son logement totalement gelé lors d'une panne de thermomètre. Le quatrième param§tre (``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.
Depuis la version 5.3 il est possible de désactiver la mise en sécurité suite à une absence de données du thermomètre extérieure. En effet, celui-ci ayant la plupart du temps un impact faible sur la régulation (dépendant de votre paramètrage), il est possible qu'il soit absent sans mettre en danger le logement. Pour cela, il faut ajouter les lignes suivantes dans votre `configuration.yaml` :
```
versatile_thermostat:
...
safety_mode:
check_outdoor_sensor: false
```
Par défaut, le thermomètre extérieur peut déclencher une mise en sécurité si il n'envoit plus de valeur.
Voir [exemple de réglages](#examples-tuning) pour avoir des exemples de réglage communs Voir [exemple de réglages](#examples-tuning) pour avoir des exemples de réglage communs
> ![Astuce](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/tips.png?raw=true) _*Notes*_ > ![Astuce](images/tips.png) _*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, > 1. Lorsque le capteur de température viendra à la vie et renverra les températures, le préréglage sera restauré à sa valeur précédente,
> 2. Attention, deux températures sont nécessaires : la température interne et la température externe et chacune doit donner la température, sinon le thermostat sera en préréglage "security", > 2. Attention, deux températures sont nécessaires : la température interne et la température externe et chacune doit donner la température, sinon le thermostat sera en préréglage "security",
> 3. 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, > 3. 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,
> 4. Pour un usage naturel, le ``security_default_on_percent`` doit être inférieur à ``security_min_on_percent``, > 4. Pour un usage naturel, le ``security_default_on_percent`` doit être inférieur à ``security_min_on_percent``,
> 5. Les thermostats de type ``thermostat_over_climate`` ne sont pas concernés par le mode security. > 5. Les thermostats de type ``thermostat_over_climate`` ne sont pas concernés par le mode security.
</details>
<details>
<summary>Le contrôle centralisé</summary>
## Le contrôle centralisé
Depuis la release 5.2, si vous avez défini une configuration centralisée, vous avez une nouvelle entité nommée `select.central_mode` qui permet de piloter tous les VTherms avec une seule action. Pour qu'un VTherm soit contrôlable de façon centralisée, il faut que son attribut de configuration nommé `use_central_mode` soit vrai.
Cette entité se présente sous la forme d'une liste de choix qui contient les choix suivants :
1. `Auto` : le mode 'normal' dans lequel chaque VTherm se comporte comme dans les versions précédentes,
2. `Stooped` : tous les VTherms sont mis à l'arrêt (`hvac_off`),
3. `Heat only` : tous les VTherms sont mis en mode chauffage lorsque ce mode est supporté par le VTherm, sinon il est stoppé,
3. `Cool only` : tous les VTherms sont mis en mode climatisation lorsque ce mode est supporté par le VTherm, sinon il est stoppé,
4. `Frost protection` : tous les VTherms sont mis en preset hors-gel lorsque ce preset est supporté par le VTherm, sinon il est stoppé.
Il est donc possible de contrôler tous les VTherms (que ceux que l'on désigne explicitement) avec un seul contrôle.
Exemple de rendu :
![central_mode](images/central_mode.png)
</details>
<details>
<summary>Le contrôle d'une chaudière centrale</summary>
## Le contrôle d'une chaudière centrale
Depuis la release 5.3, vous avez la possibilité de contrôler une chaudière centralisée. A partir du moment où il est possible de déclencher ou stopper cette chaudière depuis Home Assistant, alors Versatile Thermostat va pouvoir la commander directement.
Le principe mis en place est globalement le suivant :
1. une nouvelle entité de type `binary_sensor` et nommée par défaut `binary_sensor.central_boiler` est ajoutée,
2. dans la configuration des VTherms vous indiquez si le VTherm doit contrôler la chaudière. En effet, dans une installation hétérogène, certains VTherm doivent commander la chaudière et d'autres non. Vous devez donc indiquer dans chaque configuration de VTherm si il contrôle la chaudière ou pas,
3. le `binary_sensor.central_boiler` écoute les changements d'états des équipements des VTherm marqués comme contrôlant la chaudière,
4. dès que le nombre d'équipements pilotés par le VTherm demandant du chauffage (ie son `hvac_action` passe à `Heating`) dépasse un seuil paramétrable, alors le `binary_sensor.central_boiler` passe à `on` et **si un service d'activation a été configuré, alors ce service est appelé**,
5. si le nombre d'équipements nécessitant du chauffage repasse en dessous du seuil, alors le `binary_sensor.central_boiler` passe à `off` et si **un service de désactivation a été configuré, alors ce service est appelé**,
6. vous avez accès à deux entités :
- une de type `number` nommé par défaut `number.boiler_activation_threshold`, donne le seuil de déclenchement. Ce seuil est en nombre d'équipements (radiateurs) qui demande du chauffage.
- une de type `sensor` nommé par défaut `sensor.nb_device_active_for_boiler`, donne le nombre d'équipements qui demande du chauffage. Par exemple, un VTherm ayant 4 vannes dont 3 demandes du chauffage fera passé ce capteur à 3. Seuls les équipements des VTherms qui sont marqués pour contrôler la chaudière centrale sont comptabilisés.
Vous avez donc en permanence, les informations qui permettent de piloter et régler le déclenchement de la chaudière.
Toutes ces entités sont rattachés au service de configuration centrale :
![Les entités pilotant la chaudière](images/entitites-central-boiler.png)
### Configuration
Pour configurer cette fonction, vous devez avoir une configuration centralisée (cf. [Configuration](#configuration)) et cochez la case 'Ajouter une chuadière centrale' :
![Ajout d'une chaudière centrale](images/config-central-boiler-1.png)
Sur la page suivante vous pouvez donner la configuration des services à appeler lors de l'allumage / extinction de la chaudière :
![Ajout d'une chaudière centrale](images/config-central-boiler-2.png)
Les services se configurent comme indiqués dans la page :
1. le format général est `entity_id/service_id[/attribut:valeur]` (où `/attribut:valeur` est facultatif),
2. `entity_id` est le nom de l'entité qui commande la chaudière sous la forme `domain.entity_name`. Par exemple: `switch.chaudiere` pour les chaudière commandée par un switch ou `climate.chaudière` pour une chaudière commandée par un thermostat ou tout autre entité qui permet le contrôle de la chaudière (il n'y a pas de limitation). On peut aussi commuter des entrées (`helpers`) comme des `input_boolean` ou `input_number`.
3. `service_id` est le nom du service à appeler sous la forme `domain.service_name`. Par exemple: `switch.turn_on`, `switch.turn_off`, `climate.set_temperature`, `climate.set_hvac_mode` sont des exemples valides.
4. pour certain service vous aurez besoin d'un paramètre. Cela peut être le 'Mode CVC' `climate.set_hvac_mode` ou la température cible pour `climate.set_temperature`. Ce paramètre doit être configuré sous la forme `attribut:valeur` en fin de chaine.
Exemples (à ajuster à votre cas) :
- `climate.chaudiere/climate.set_hvac_mode/hvac_mode:heat` : pour allumer le thermostat de la chaudière en mode chauffage,
- `climate.chaudiere/climate.set_hvac_mode/hvac_mode:off` : pour stopper le thermostat de la chaudière,
- `switch.pompe_chaudiere/switch.turn_on` : pour allumer le swicth qui alimente la pompe de la chaudière,
- `switch.pompe_chaudiere/switch.turn_off` : pour allumer le swicth qui alimente la pompe de la chaudière,
- ...
### Comment trouver le bon service ?
Pour trouver le services a utiliser, le mieux est d'aller dans "Outils de développement / Services", chercher le service a appelé, l'entité à commander et l'éventuel paramètre à donner.
Cliquez sur 'Appeler le service'. Si votre chaudière s'allume vous avez la bonne configuration. Passez alors en mode Yaml et recopiez les paramètres.
Exemple:
Sous "Outils de développement / Service" :
![Configuration du service](images/dev-tools-turnon-boiler-1.png)
En mode yaml :
![Configuration du service](images/dev-tools-turnon-boiler-2.png)
Le service à configurer est alors le suivant: `climate.empty_thermostast/climate.set_hvac_mode/hvac_mode:heat` (notez la suppression du blanc dans `hvac_mode:heat`)
Faite alors de même pour le service d'extinction et vous êtes parés.
### Les évènements
A chaque allumage ou extinction réussie de la chaudière un évènement est envoyé par Versatile Thermostat. Il peut avantageusement être capté par une automatisation, par exemple pour notifier un changement.
Les évènements ressemblent à ça :
Un évènement d'allumage :
```
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 :
```
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
```
### Avertissement
> ![Astuce](images/tips.png) _*Notes*_
> Le contrôle par du logiciel ou du matériel de type domotique d'une chaudière centrale peut induire des risques pour son bon fonctionnement. Assurez-vous avant d'utiliser ces fonctions, que votre chaudière possède bien des fonctions de sécurité et que celles-ci fonctionnent. Allumer une chaudière si tous les robinets sont fermés peut générer de la sur-pression par exemple.
</details>
<details>
<summary>Synthèse des paramètres</summary>
## Synthèse des paramètres ## Synthèse des paramètres
@@ -527,6 +861,7 @@ Voir [exemple de réglages](#examples-tuning) pour avoir des exemples de réglag
| ``temp_min`` | Température minimale permise | X | X | X | X | | ``temp_min`` | Température minimale permise | X | X | X | X |
| ``temp_max`` | Température maximale permise | X | X | X | X | | ``temp_max`` | Température maximale permise | X | X | X | X |
| ``device_power`` | Puissance de l'équipement | X | X | X | - | | ``device_power`` | Puissance de l'équipement | X | X | X | - |
| ``use_central_mode`` | Autorisation du contrôle centralisé | X | X | X | - |
| ``use_window_feature`` | Avec détection des ouvertures | X | X | X | - | | ``use_window_feature`` | Avec détection des ouvertures | X | X | X | - |
| ``use_motion_feature`` | Avec détection de mouvement | X | X | X | - | | ``use_motion_feature`` | Avec détection de mouvement | X | X | X | - |
| ``use_power_feature`` | Avec gestion de la puissance | X | X | X | - | | ``use_power_feature`` | Avec gestion de la puissance | X | X | X | - |
@@ -535,6 +870,7 @@ Voir [exemple de réglages](#examples-tuning) pour avoir des exemples de réglag
| ``heater_entity2_id`` | 2ème radiateur | X | - | - | - | | ``heater_entity2_id`` | 2ème radiateur | X | - | - | - |
| ``heater_entity3_id`` | 3ème radiateur | X | - | - | - | | ``heater_entity3_id`` | 3ème radiateur | X | - | - | - |
| ``heater_entity4_id`` | 4ème radiateur | X | - | - | - | | ``heater_entity4_id`` | 4ème radiateur | X | - | - | - |
| ``heater_keep_alive`` | Intervalle de rafraichissement du switch | X | - | - | - |
| ``proportional_function`` | Algorithme | X | - | - | - | | ``proportional_function`` | Algorithme | X | - | - | - |
| ``climate_entity1_id`` | Thermostat sous-jacent | - | X | - | - | | ``climate_entity1_id`` | Thermostat sous-jacent | - | X | - | - |
| ``climate_entity2_id`` | 2ème thermostat sous-jacent | - | X | - | - | | ``climate_entity2_id`` | 2ème thermostat sous-jacent | - | X | - | - |
@@ -547,13 +883,7 @@ Voir [exemple de réglages](#examples-tuning) pour avoir des exemples de réglag
| ``ac_mode`` | utilisation de l'air conditionné (AC) ? | X | X | X | - | | ``ac_mode`` | utilisation de l'air conditionné (AC) ? | X | X | X | - |
| ``tpi_coef_int`` | Coefficient à utiliser pour le delta de température interne | X | - | X | X | | ``tpi_coef_int`` | Coefficient à utiliser pour le delta de température interne | X | - | X | X |
| ``tpi_coef_ext`` | Coefficient à utiliser pour le delta de température externe | X | - | X | X | | ``tpi_coef_ext`` | Coefficient à utiliser pour le delta de température externe | X | - | X | X |
| ``frost_tp`` | Température en preset Hors-gel | X | X | X | X | | ``frost_temp`` | Température en preset Hors-gel | X | X | X | X |
| ``eco_temp`` | Température en preset Eco | X | X | X | X |
| ``comfort_temp`` | Température en preset Confort | X | X | X | X |
| ``boost_temp`` | Température en preset Boost | X | X | X | X |
| ``eco_ac_temp`` | Température en preset Eco en mode AC | X | X | X | X |
| ``comfort_ac_temp`` | Température en preset Confort en mode AC | X | X | X | X |
| ``boost_ac_temp`` | Température en preset Boost en mode AC | X | X | X | X |
| ``window_sensor_entity_id`` | Détecteur d'ouverture (entity id) | X | X | X | - | | ``window_sensor_entity_id`` | Détecteur d'ouverture (entity id) | X | X | X | - |
| ``window_delay`` | Délai avant extinction (secondes) | X | X | X | X | | ``window_delay`` | Délai avant extinction (secondes) | X | X | X | X |
| ``window_auto_open_threshold`` | Seuil haut de chute de température pour la détection automatique (en °/min) | X | X | X | X | | ``window_auto_open_threshold`` | Seuil haut de chute de température pour la détection automatique (en °/min) | X | X | X | X |
@@ -568,13 +898,6 @@ Voir [exemple de réglages](#examples-tuning) pour avoir des exemples de réglag
| ``max_power_sensor_entity_id`` | Capteur de puissance Max (entity id) | X | X | X | X | | ``max_power_sensor_entity_id`` | Capteur de puissance Max (entity id) | X | X | X | X |
| ``power_temp`` | Température si délestaqe | X | X | X | X | | ``power_temp`` | Température si délestaqe | X | X | X | X |
| ``presence_sensor_entity_id`` | Capteur de présence entity id (true si quelqu'un est présent) | X | X | X | - | | ``presence_sensor_entity_id`` | Capteur de présence entity id (true si quelqu'un est présent) | X | X | X | - |
| ``frost_ay_temp`` | Température en preset Hors-gel en cas d'absence | X | X | X | X |
| ``eco_away_temp`` | Température en preset Eco en cas d'absence | X | X | X | X |
| ``comfort_away_temp`` | Température en preset Comfort en cas d'absence | X | X | X | X |
| ``boost_away_temp`` | Température en preset Boost en cas d'absence | X | X | X | X |
| ``eco_ac_away_temp`` | Température en preset Eco en cas d'absence en mode AC | X | X | X | X |
| ``comfort_ac_away_temp`` | Température en preset Comfort en cas d'absence en mode AC | X | X | X | X |
| ``boost_ac_away_temp`` | Température en preset Boost en cas d'absence en mode AC | X | X | X | X |
| ``minimal_activation_delay`` | Délai minimal d'activation | X | - | - | X | | ``minimal_activation_delay`` | Délai minimal d'activation | X | - | - | X |
| ``security_delay_min`` | Délai maximal entre 2 mesures de températures | X | - | X | X | | ``security_delay_min`` | Délai maximal entre 2 mesures de températures | X | - | X | X |
| ``security_min_on_percent`` | Pourcentage minimal de puissance pour passer en mode sécurité | X | - | X | X | | ``security_min_on_percent`` | Pourcentage minimal de puissance pour passer en mode sécurité | X | - | X | X |
@@ -582,7 +905,13 @@ Voir [exemple de réglages](#examples-tuning) pour avoir des exemples de réglag
| ``auto_regulation_dtemp`` | La seuil d'auto-régulation | - | X | - | - | | ``auto_regulation_dtemp`` | La seuil d'auto-régulation | - | X | - | - |
| ``auto_regulation_period_min`` | La période minimale d'auto-régulation | - | X | - | - | | ``auto_regulation_period_min`` | La période minimale d'auto-régulation | - | X | - | - |
| ``inverse_switch_command`` | Inverse la commande du switch (pour switch avec fil pilote) | X | - | - | - | | ``inverse_switch_command`` | Inverse la commande du switch (pour switch avec fil pilote) | X | - | - | - |
| ``auto_fan_mode` | Mode de ventilation automatique | - | X | - | - | | ``auto_fan_mode`` | Mode de ventilation automatique | - | X | - | - |
| ``auto_regulation_use_device_temp`` | Utilisation de la température interne du sous-jacent | - | X | - | - |
| ``use_central_boiler_feature`` | Ajout du controle d'une chaudière centrale | - | - | - | X |
| ``central_boiler_activation_service`` | Service d'activation de la chaudière | - | - | - | X |
| ``central_boiler_deactivation_service`` | Service de desactivation de la chaudière | - | - | - | X |
| ``used_by_controls_central_boiler`` | Indique si le VTherm contrôle la chaudière centrale | X | X | X | - |
</details>
# Exemples de réglage # Exemples de réglage
@@ -658,7 +987,7 @@ Voir quelques situations à [examples](#some-results).
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 : 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 :
![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/thermostat-sensors.png?raw=true) ![image](images/thermostat-sensors.png)
Dans l'ordre, il y a : Dans l'ordre, il y a :
1. l'entité principale climate de commande du thermostat, 1. l'entité principale climate de commande du thermostat,
@@ -691,7 +1020,7 @@ frontend:
``` ```
et choisissez le thème ```versatile_thermostat_theme``` dans la configuration du panel. Vous obtiendrez quelque-chose qui va ressembler à ça : et choisissez le thème ```versatile_thermostat_theme``` dans la configuration du panel. Vous obtiendrez quelque-chose qui va ressembler à ça :
![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/colored-thermostat-sensors.png?raw=true) ![image](images/colored-thermostat-sensors.png)
# Services # Services
@@ -737,7 +1066,7 @@ target:
entity_id: climate.my_thermostat entity_id: climate.my_thermostat
``` ```
> ![Astuce](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/tips.png?raw=true) _*Notes*_ > ![Astuce](images/tips.png) _*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. - 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é ## Modifier les paramètres de sécurité
@@ -778,6 +1107,7 @@ Les évènements notifiés sont les suivants:
- ``versatile_thermostat_temperature_event`` : une ou les deux mesures de température d'un thermostat n'ont pas été mis à jour depuis plus de `security_delay_min`` minutes - ``versatile_thermostat_temperature_event`` : une ou les deux mesures de température d'un thermostat n'ont pas été mis à jour depuis plus de `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_hvac_mode_event`` : le thermostat est allumé ou éteint. Cet évènement est aussi diffusé au démarrage du thermostat
- ``versatile_thermostat_preset_event`` : un nouveau preset est sélectionné sur le thermostat. Cet évènement est aussi diffusé au démarrage du thermostat - ``versatile_thermostat_preset_event`` : un nouveau preset est sélectionné sur le thermostat. Cet évènement est aussi diffusé au démarrage du thermostat
- ``versatile_thermostat_central_boiler_event`` : un évènement indiquant un changement dans l'état de la chaudière.
Si vous avez bien suivi, lorsqu'un thermostat passe en mode sécurité, 3 évènements sont déclenchés : 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, 1. ``versatile_thermostat_temperature_event`` pour indiquer qu'un thermomètre ne répond plus,
@@ -791,7 +1121,7 @@ Vous pouvez très facilement capter ses évènements dans une automatisation par
# Attributs personnalisés # 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 : 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 :
![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/dev-tools-climate.png?raw=true) ![image](images/dev-tools-climate.png)
Les attributs personnalisés sont les suivants : Les attributs personnalisés sont les suivants :
@@ -835,27 +1165,30 @@ Les attributs personnalisés sont les suivants :
| ``valve_open_percent`` | Le pourcentage d'ouverture de la vanne | | ``valve_open_percent`` | Le pourcentage d'ouverture de la vanne |
| ``regulated_target_temperature`` | La température de consigne calculée par l'auto-régulation | | ``regulated_target_temperature`` | La température de consigne calculée par l'auto-régulation |
| ``is_inversed`` | True si la commande est inversée (fil pilote avec diode) | | ``is_inversed`` | True si la commande est inversée (fil pilote avec diode) |
| ``is_controlled_by_central_mode`` | True si le VTherm peut être controlé de façon centrale |
| ``last_central_mode`` | Le dernier mode central utilisé (None si le VTherm n'est pas controlé en central) |
| ``is_used_by_central_boiler`` | Indique si le VTherm peut contrôler la chaudière centrale |
# Quelques résultats # Quelques résultats
**Convergence de la température vers la cible configurée par preset:** **Convergence de la température vers la cible configurée par preset:**
![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/results-1.png?raw=true) ![image](images/results-1.png)
[Cycle de marche/arrêt calculé par l'intégration :](https://) [Cycle de marche/arrêt calculé par l'intégration :](https://)
![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/results-2.png?raw=true) ![image](images/results-2.png)
**Coef_int trop élevé (oscillations autour de la cible)** **Coef_int trop élevé (oscillations autour de la cible)**
![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/results-3.png?raw=true) ![image](images/results-3.png)
**Évolution du calcul de l'algorithme** **Évolution du calcul de l'algorithme**
![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/results-4.png?raw=true) ![image](images/results-4.png)
Voir le code de ce composant [[ci-dessous](#even-better-with-apex-chart-to-tune-your-thermostat)] Voir le code de ce composant [[ci-dessous](#even-better-with-apex-chart-to-tune-your-thermostat)]
**Thermostat finement réglé** **Thermostat finement réglé**
Merci [impuR_Shozz](https://forum.hacf.fr/u/impur_shozz/summary) ! 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. 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.
![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/results-fine-tuned.png?raw=true) ![image](images/results-fine-tuned.png)
Enjoy ! Enjoy !
@@ -869,9 +1202,9 @@ Une carte spéciale pour le Versatile Thermostat a été développée (sur la ba
## Encore mieux avec le composant Scheduler ! ## 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 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 : 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 Versatile Thermostat :
À partir d'ici, je suppose que vous avez installé Awesome Thermostat et Scheduler Component. À partir d'ici, je suppose que vous avez installé Versatile Thermostat et Scheduler Component.
Dans Scheduler, ajoutez un planning : Dans Scheduler, ajoutez un planning :
@@ -894,7 +1227,7 @@ J'espère que cet exemple vous aidera, n'hésitez pas à me faire part de vos re
## Encore bien mieux avec la custom:simple-thermostat front integration ## 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. 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 ![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/simple-thermostat.png?raw=true) Vous pouvez avoir quelque chose comme ça très facilement ![image](images/simple-thermostat.png)
Exemple de configuration : Exemple de configuration :
``` ```
@@ -937,7 +1270,7 @@ Vous pouvez personnaliser ce composant à l'aide du composant HACS card-mod pour
} }
{% endif %} {% endif %}
``` ```
![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/custom-css-thermostat.png?raw=true) ![image](images/custom-css-thermostat.png)
## Toujours mieux avec Plotly pour régler votre thermostat ## Toujours mieux avec Plotly 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 Plotly uniquement en utilisant les attributs personnalisés du thermostat décrits [ici](#custom-attributes) : Vous pouvez obtenir une courbe comme celle présentée dans [some results](#some-results) avec une sorte de configuration de graphique Plotly uniquement en utilisant les attributs personnalisés du thermostat décrits [ici](#custom-attributes) :
@@ -1008,6 +1341,10 @@ Remplacez les valeurs entre [[ ]] par les votres.
step: day step: day
``` ```
Exemple de courbes obtenues avec Plotly :
![image](images/plotly-curves.png)
## Et toujours de mieux en mieux avec l'AappDaemon NOTIFIER pour notifier les évènements ## 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. 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.
@@ -1102,7 +1439,11 @@ Si vous souhaitez contribuer, veuillez lire les [directives de contribution](CON
# Dépannages # Dépannages
<details>
<summary>Utilisation d'un Heatzy</summary>
## Utilisation d'un Heatzy ## Utilisation d'un Heatzy
L'utilisation d'un Heatzy est possible à la condition d'utiliser un switch virtuel sur ce modèle : L'utilisation d'un Heatzy est possible à la condition d'utiliser un switch virtuel sur ce modèle :
``` ```
- platform: template - platform: template
@@ -1131,6 +1472,10 @@ L'utilisation d'un Heatzy est possible à la condition d'utiliser un switch virt
preset_mode: "eco" preset_mode: "eco"
``` ```
Merci à @gael pour cet exemple. Merci à @gael pour cet exemple.
</details>
<details>
<summary>Utilisation d'un radiateur avec un fil pilote</summary>
## Utilisation d'un radiateur avec un fil pilote ## Utilisation d'un radiateur avec un fil pilote
Comme pour le Heatzy ci-dessus vous pouvez utiliser un switch virtuel qui va changer le preset de votre radiateur en fonction de l'état d'allumage du VTherm. Comme pour le Heatzy ci-dessus vous pouvez utiliser un switch virtuel qui va changer le preset de votre radiateur en fonction de l'état d'allumage du VTherm.
@@ -1152,10 +1497,74 @@ Exemple :
icon_template: "{% if is_state('switch.radiateur_soan', 'on') %}mdi:radiator-disabled{% else %}mdi:radiator{% endif %}" icon_template: "{% if is_state('switch.radiateur_soan', 'on') %}mdi:radiator-disabled{% else %}mdi:radiator{% endif %}"
``` ```
</details>
<details>
<summary>Utilisation d'un radiateur avec un module Nodon</summary>
## Utilisation d'un radiateur avec un fil pilote
Comme pour le Heatzy ci-dessus vous pouvez utiliser un switch virtuel qui va changer le preset de votre radiateur en fonction de l'état d'allumage du VTherm.
Exemple :
```
- platform: template
switches:
chauffage_chb_parents:
unique_id: chauffage_chb_parents
friendly_name: Chauffage chambre parents
value_template: "{{ is_state('select.fp_chb_parents_pilot_wire_mode', 'comfort') }}"
icon_template: >-
{% if is_state('select.fp_chb_parents_pilot_wire_mode', 'comfort') %}
mdi:radiator
{% elif is_state('select.fp_chb_parents_pilot_wire_mode', 'frost_protection') %}
mdi:snowflake
{% else %}
mdi:radiator-disabled
{% endif %}
turn_on:
service: select.select_option
target:
entity_id: select.fp_chb_parents_pilot_wire_mode
data:
option: comfort
turn_off:
service: select.select_option
target:
entity_id: select.fp_chb_parents_pilot_wire_mode
data:
option: eco
```
</details>
<details>
<summary>Seul le premier radiateur chauffe</summary>
## Seul le premier radiateur chauffe ## Seul le premier radiateur chauffe
En mode `over_switch` si plusieurs radiateurs sont configurés pour un même VTherm, l'alllumage va se faire de façon séquentiel pour lisser au plus possible les pics de consommation. En mode `over_switch` si plusieurs radiateurs sont configurés pour un même VTherm, l'alllumage va se faire de façon séquentiel pour lisser au plus possible les pics de consommation.
Cela est tout à fait normal et voulu. C'est décrit ici : [Pour un thermostat de type ```thermostat_over_switch```](#pour-un-thermostat-de-type-thermostat_over_switch) Cela est tout à fait normal et voulu. C'est décrit ici : [Pour un thermostat de type ```thermostat_over_switch```](#pour-un-thermostat-de-type-thermostat_over_switch)
</details>
<details>
<summary>Le radiateur chauffe alors que la température de consigne est dépassée ou ne chauffe pas alors que la température de la pièce est bien en-dessous de la consigne</summary>
## Le radiateur chauffe alors que la température de consigne est dépassée ou ne chauffe pas alors que la température de la pièce est bien en-dessous de la consigne
### Type `over_switch` ou `over_valve`
Avec un VTherm de type `over_switch` ou `over_valve`, ce défaut montre juste que les paramètres de l'algorithme TPI sont mal réglés. Voir [Algorithme TPI](#algorithme-tpi) pour optimiser les réglages.
### Type `over_climate`
Avec un VTherm de type `over_climate`, la régulation est faite par le `climate` sous-jacent directement et VTherm se contente de lui transmettre les consignes. Donc si le radiateur chauffe alors que la température de consigne est dépassée, c'est certainement que sa mesure de température interne est biaisée. Ca arrive très souvent avec les TRV et les clims réversibles qui ont un capteur de température interne, soit trop près de l'élément de chauffe (donc trop froid l'hiver).
Exemple de discussion autour de ces sujets: [#348](https://github.com/jmcollin78/versatile_thermostat/issues/348), [#316](https://github.com/jmcollin78/versatile_thermostat/issues/316), [#312](https://github.com/jmcollin78/versatile_thermostat/discussions/312), [#278](https://github.com/jmcollin78/versatile_thermostat/discussions/278)
Pour s'en sortir, VTherm est équipé d'une fonction nommée auto-régulation qui permet d'adapter la consigne envoyée au sous-jacent jusqu'à ce que la consigne soit respectée. Cette fonction permet de compenser le biais de mesure des thermomètres internes. Si le biais est important la régulation doit être importante. Voir [L'auto-régulation](#lauto-régulation) pour configurer l'auto-régulation.
</details>
<details>
<summary>Régler les paramètres de détection d'ouverture de fenêtre en mode auto</summary>
## Régler les paramètres de détection d'ouverture de fenêtre en mode auto ## Régler les paramètres de détection d'ouverture de fenêtre en mode auto
Si vous n'arrivez pas à régler la fonction de détection des ouvertures en mode auto (cf. [auto](#le-mode-auto)), vous pouvez essayer de modifier les paramètres de l'algorithme de lissage de la température. Si vous n'arrivez pas à régler la fonction de détection des ouvertures en mode auto (cf. [auto](#le-mode-auto)), vous pouvez essayer de modifier les paramètres de l'algorithme de lissage de la température.
@@ -1175,6 +1584,10 @@ versatile_thermostat:
``` ```
Ces paramètres sont sensibles et assez difficiles à régler. Merci de ne les utiliser que si vous savez ce que vous faites et que vos mesures de température ne sont pas déjà lisses. Ces paramètres sont sensibles et assez difficiles à régler. Merci de ne les utiliser que si vous savez ce que vous faites et que vos mesures de température ne sont pas déjà lisses.
</details>
<details>
<summary>Pourquoi mon Versatile Thermostat se met en Securite ?</summary>
## Pourquoi mon Versatile Thermostat se met en Securite ? ## Pourquoi mon Versatile Thermostat se met en Securite ?
Le mode sécurité n'est possible que sur les VTherm `over_switch` et `over_valve`. Il survient lorsqu'un des 2 thermomètres qui donne la température de la pièce ou la température extérieure n'a pas envoyé de valeur depuis plus de `security_delay_min` minutes et que le radiateur chauffait à au moins `security_min_on_percent`. Le mode sécurité n'est possible que sur les VTherm `over_switch` et `over_valve`. Il survient lorsqu'un des 2 thermomètres qui donne la température de la pièce ou la température extérieure n'a pas envoyé de valeur depuis plus de `security_delay_min` minutes et que le radiateur chauffait à au moins `security_min_on_percent`.
@@ -1187,11 +1600,11 @@ Tous ces paramètres se règlent dans la dernière page de la configuration du V
Le premier symptôme est une température anormalement basse avec un temps de chauffe faible à chaque cycle et régulier. Le premier symptôme est une température anormalement basse avec un temps de chauffe faible à chaque cycle et régulier.
Exemple: Exemple:
[security mode](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/security-mode-symptome1.png?raw=true) [security mode](images/security-mode-symptome1.png)
Si vous avez installé la carte [Versatile Thermostat UI Card](https://github.com/jmcollin78/versatile-thermostat-ui-card), le VTherm en question aura cette forme là : Si vous avez installé la carte [Versatile Thermostat UI Card](https://github.com/jmcollin78/versatile-thermostat-ui-card), le VTherm en question aura cette forme là :
[security mode UI Card](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/security-mode-symptome2.png?raw=true) [security mode UI Card](images/security-mode-symptome2.png)
Vous pouvez aussi vérifier dans les attributs du VTherm les dates de réception des différentes dates. **Les attributs sont disponibles dans les Outils de développement / Etats**. Vous pouvez aussi vérifier dans les attributs du VTherm les dates de réception des différentes dates. **Les attributs sont disponibles dans les Outils de développement / Etats**.
@@ -1222,8 +1635,13 @@ Cela va dépendre de la cause du problème :
2. Si le paramètre `security_delay_min` est trop petit, cela rsique de générer beaucoup de fausses alertes. Une valeur correcte est de l'ordre de 60 min, surtout si vous avez des capteurs de température à pile. 2. Si le paramètre `security_delay_min` est trop petit, cela rsique de générer beaucoup de fausses alertes. Une valeur correcte est de l'ordre de 60 min, surtout si vous avez des capteurs de température à pile.
3. Certains capteurs de température, n'envoie pas de mesure si la température n'a pas changée. Donc en cas de température très stable pendant longtemps, le mode sécurité peut se déclencher. Ce n'est pas très grave puisqu'il s'enlève dès que le VTherm reçoit à nouveau une température. Sur certain thermomètre (TuYA par exemple), on peut forcer le délai max entre 2 mesures. Il conviendra de mettre un délai max < `security_delay_min`, 3. Certains capteurs de température, n'envoie pas de mesure si la température n'a pas changée. Donc en cas de température très stable pendant longtemps, le mode sécurité peut se déclencher. Ce n'est pas très grave puisqu'il s'enlève dès que le VTherm reçoit à nouveau une température. Sur certain thermomètre (TuYA par exemple), on peut forcer le délai max entre 2 mesures. Il conviendra de mettre un délai max < `security_delay_min`,
4. Dès que la température sera a nouveau reçue le mode sécurité s'enlèvera et les valeurs précédentes de preset, température cible et mode seront restaurées. 4. Dès que la température sera a nouveau reçue le mode sécurité s'enlèvera et les valeurs précédentes de preset, température cible et mode seront restaurées.
</details>
<details>
<summary>Utilisation d'un groupe de personnes comme capteur de présence</summary>
## Utilisation d'un groupe de personnes comme capteur de présence ## Utilisation d'un groupe de personnes comme capteur de présence
Malheureusement, les groupes de personnes ne sont pas reconnus comme des capteurs de présence. On ne peut donc pas les utiliser directement dans VTherm. Malheureusement, les groupes de personnes ne sont pas reconnus comme des capteurs de présence. On ne peut donc pas les utiliser directement dans VTherm.
Le contournement est de créer un template de binary_sensor avec le code suivant : Le contournement est de créer un template de binary_sensor avec le code suivant :
@@ -1244,6 +1662,21 @@ Fichier `configuration.yaml`:
template: !include templates.yaml template: !include templates.yaml
... ...
``` ```
</details>
<details>
<summary>Activer les logs du Versatile Thermostat</summary>
## Activer les logs du Versatile Thermostat
Des fois, vous aurez besoin d'activer les logs pour afiner les analyses. Pour cela, éditer le fichier `logger.yaml` de votre configuration et configurer les logs comme suit :
```
default: xxxx
logs:
custom_components.versatile_thermostat: info
```
Vous devez recharger la configuration yaml (Outils de dev / Yaml / Toute la configuration Yaml) ou redémarrer Home Assistant pour que ce changement soit pris en compte.
</details>
*** ***

596
README.md
View File

@@ -4,11 +4,16 @@
[![hacs][hacs_badge]][hacs] [![hacs][hacs_badge]][hacs]
[![BuyMeCoffee][buymecoffeebadge]][buymecoffee] [![BuyMeCoffee][buymecoffeebadge]][buymecoffee]
![Tip](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/icon.png?raw=true) ![Tip](images/icon.png)
> ![Tip](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/tips.png?raw=true) 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 ;-). > ![Tip](images/tips.png) 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 ;-).
- [Major changes in version 5.0](#major-changes-in-version-50) - [Changes in version 6.0](#changes-in-version-60)
- [Temperature entities for presets](#temperature-entities-for-presets)
- [In the case of a central configuration](#in-the-case-of-a-central-configuration)
- [Redesign of the configuration menu](#redesign-of-the-configuration-menu)
- [The 'Incomplete configuration' and 'Finalize' menu options](#the-incomplete-configuration-and-finalize-menu-options)
- [Changements dans la version 5.0](#changements-dans-la-version-50)
- [Thanks for the beer buymecoffee](#thanks-for-the-beer-buymecoffee) - [Thanks for the beer buymecoffee](#thanks-for-the-beer-buymecoffee)
- [When to use / not use](#when-to-use--not-use) - [When to use / not use](#when-to-use--not-use)
- [Incompatibilities](#incompatibilities) - [Incompatibilities](#incompatibilities)
@@ -17,12 +22,15 @@
- [HACS installation (recommended)](#hacs-installation-recommended) - [HACS installation (recommended)](#hacs-installation-recommended)
- [Manual installation](#manual-installation) - [Manual installation](#manual-installation)
- [Configuration](#configuration) - [Configuration](#configuration)
- [Creation of a new Versatile Thermostat](#creation-of-a-new-versatile-thermostat)
- [Minimal configuration update](#minimal-configuration-update) - [Minimal configuration update](#minimal-configuration-update)
- [Select the driven entity](#select-the-driven-entity) - [Select the driven entity](#select-the-driven-entity)
- [For a ```thermostat_over_switch``` type thermostat](#for-a-thermostat_over_switch-type-thermostat) - [For a ```thermostat_over_switch``` type thermostat](#for-a-thermostat_over_switch-type-thermostat)
- [For a thermostat of type ```thermostat_over_climate```:](#for-a-thermostat-of-type-thermostat_over_climate) - [For a thermostat of type ```thermostat_over_climate```:](#for-a-thermostat-of-type-thermostat_over_climate)
- [Self-regulation](#self-regulation) - [Self-regulation](#self-regulation)
- [Self-regulation in Expert mode](#self-regulation-in-expert-mode) - [Self-regulation in Expert mode](#self-regulation-in-expert-mode)
- [Internal temperature compensation](#internal-temperature-compensation)
- [synthesis of the self-regulation algorithm](#synthesis-of-the-self-regulation-algorithm)
- [Auto-fan mode](#auto-fan-mode) - [Auto-fan mode](#auto-fan-mode)
- [For a thermostat of type ```thermostat_over_valve```:](#for-a-thermostat-of-type-thermostat_over_valve) - [For a thermostat of type ```thermostat_over_valve```:](#for-a-thermostat-of-type-thermostat_over_valve)
- [Configure the TPI algorithm coefficients](#configure-the-tpi-algorithm-coefficients) - [Configure the TPI algorithm coefficients](#configure-the-tpi-algorithm-coefficients)
@@ -34,8 +42,14 @@
- [Configure the power management](#configure-the-power-management) - [Configure the power management](#configure-the-power-management)
- [Configure presence or occupancy](#configure-presence-or-occupancy) - [Configure presence or occupancy](#configure-presence-or-occupancy)
- [Advanced configuration](#advanced-configuration) - [Advanced configuration](#advanced-configuration)
- [Parameters synthesis](#parameters-synthesis) - [Centralized control](#centralized-control)
- [Examples tuning](#examples-tuning) - [Control of a central boiler](#control-of-a-central-boiler)
- [Setup](#setup)
- [How to find the right service?](#how-to-find-the-right-service)
- [The events](#the-events)
- [Warning](#warning)
- [Parameter summary](#parameter-summary)
- [Tuning examples](#tuning-examples)
- [Electrical heater](#electrical-heater) - [Electrical heater](#electrical-heater)
- [Central heating (gaz or fuel heating system)](#central-heating-gaz-or-fuel-heating-system) - [Central heating (gaz or fuel heating system)](#central-heating-gaz-or-fuel-heating-system)
- [Temperature sensor will battery](#temperature-sensor-will-battery) - [Temperature sensor will battery](#temperature-sensor-will-battery)
@@ -62,28 +76,44 @@
- [Troubleshooting](#troubleshooting) - [Troubleshooting](#troubleshooting)
- [Using a Heatzy](#using-a-heatzy) - [Using a Heatzy](#using-a-heatzy)
- [Using a Heatsink with a Pilot Wire](#using-a-heatsink-with-a-pilot-wire) - [Using a Heatsink with a Pilot Wire](#using-a-heatsink-with-a-pilot-wire)
- [Using a heater with a Nodon module](#using-a-heater-with-a-nodon-module)
- [Only the first radiator heats](#only-the-first-radiator-heats) - [Only the first radiator heats](#only-the-first-radiator-heats)
- [The radiator heats up even though the setpoint temperature is exceeded or does not heat up even though the room temperature is well below the setpoint](#the-radiator-heats-up-even-though-the-setpoint-temperature-is-exceeded-or-does-not-heat-up-even-though-the-room-temperature-is-well-below-the-setpoint)
- [Type `over_switch` or `over_valve`](#type-over_switch-or-over_valve)
- [Type `over_climate`](#type-over_climate)
- [Adjust window opening detection parameters in auto mode](#adjust-window-opening-detection-parameters-in-auto-mode) - [Adjust window opening detection parameters in auto mode](#adjust-window-opening-detection-parameters-in-auto-mode)
- [Why does my Versatile Thermostat go into Safety?](#why-does-my-versatile-thermostat-go-into-safety) - [Why does my Versatile Thermostat go into Safety?](#why-does-my-versatile-thermostat-go-into-safety)
- [How to detect safety mode?](#how-to-detect-safety-mode) - [How to detect safety mode?](#how-to-detect-safety-mode)
- [How can I be notified when this happens?](#how-can-i-be-notified-when-this-happens) - [How can I be notified when this happens?](#how-can-i-be-notified-when-this-happens)
- [How to repair?](#how-to-repair) - [How to repair?](#how-to-repair)
- [Using a group of people as a presence sensor](#using-a-group-of-people-as-a-presence-sensor) - [Using a group of people as a presence sensor](#using-a-group-of-people-as-a-presence-sensor)
- [Enable Versatile Thermostat logs](#enable-versatile-thermostat-logs)
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. 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.
>![New](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/new-icon.png?raw=true) _*News*_ >![New](images/new-icon.png) _*Latest releases*_
> * **Release 6.0**:
> - Added entities from the Number domain to configure preset temperatures [354](https://github.com/jmcollin78/versatile_thermostat/issues/354)
> - Complete redesign of the configuration menu to remove temperatures and use a menu instead of a configuration tunnel [354](https://github.com/jmcollin78/versatile_thermostat/issues/354)
> * **Release 5.4**:
> - Added temperature step [#311](https://github.com/jmcollin78/versatile_thermostat/issues/311),
> - addition of regulation thresholds for the `over_valve` to avoid draining the TRV battery too much [#338](https://github.com/jmcollin78/versatile_thermostat/issues/338),
> - added an option allowing the internal temperature of a TRV to be used to force self-regulation [#348](https://github.com/jmcollin78/versatile_thermostat/issues/348),
> - added a keep-alive function for VTherm `over_switch` [#345](https://github.com/jmcollin78/versatile_thermostat/issues/345)
> * **Release 5.3**: Added a central boiler control function [#234](https://github.com/jmcollin78/versatile_thermostat/issues/234) - more information here: [Controlling a central boiler](#controlling-a-central-boiler). Added the ability to disable security mode for outdoor thermometer [#343](https://github.com/jmcollin78/versatile_thermostat/issues/343)
> * **Release 5.2**: Added a `central_mode` allowing all VTherms to be controlled centrally [#158](https://github.com/jmcollin78/versatile_thermostat/issues/158).
> * **Release 5.1**: Limitation of the values sent to the valves and the temperature sent to the underlying climate.
> * **Release 5.0**: Added a central configuration allowing the sharing of attributes that can be shared [#239](https://github.com/jmcollin78/versatile_thermostat/issues/239). > * **Release 5.0**: Added a central configuration allowing the sharing of attributes that can be shared [#239](https://github.com/jmcollin78/versatile_thermostat/issues/239).
<details>
<summary>Others releases</summary>
> * **Release 4.3**: Added an auto-fan mode for the `over_climate` type allowing ventilation to be activated if the temperature difference is significant [#223](https://github.com/jmcollin78/versatile_thermostat/issues/223). > * **Release 4.3**: Added an auto-fan mode for the `over_climate` type allowing ventilation to be activated if the temperature difference is significant [#223](https://github.com/jmcollin78/versatile_thermostat/issues/223).
> * **Release 4.2**: The calculation of the slope of the temperature curve is now done in °/hour and no longer in °/min [#242](https://github.com/jmcollin78/versatile_thermostat/issues/242). Correction of automatic detection of openings by adding smoothing of the temperature curve. > * **Release 4.2**: The calculation of the slope of the temperature curve is now done in °/hour and no longer in °/min [#242](https://github.com/jmcollin78/versatile_thermostat/issues/242). Correction of automatic detection of openings by adding smoothing of the temperature curve.
> * **Release 4.1**: Added an **Expert** regulation mode in which the user can specify their own auto-regulation parameters instead of using the pre-programmed ones [#194]( https://github.com/jmcollin78/versatile_thermostat/issues/194). > * **Release 4.1**: Added an **Expert** regulation mode in which the user can specify their own auto-regulation parameters instead of using the pre-programmed ones [#194]( https://github.com/jmcollin78/versatile_thermostat/issues/194).
> * **Release 4.0**: Added the support of **Versatile Thermostat UI Card**. See [Versatile Thermostat UI Card](https://github.com/jmcollin78/versatile-thermostat-ui-card). Added a **Slow** regulation mode for slow latency heating devices [#168](https://github.com/jmcollin78/versatile_thermostat/issues/168). Change the way **the power is calculated** in case of VTherm with multi-underlying equipements [#146](https://github.com/jmcollin78/versatile_thermostat/issues/146). Added the support of AC and Heat for VTherm over switch alse [#144](https://github.com/jmcollin78/versatile_thermostat/pull/144) > * **Release 4.0**: Added the support of **Versatile Thermostat UI Card**. See [Versatile Thermostat UI Card](https://github.com/jmcollin78/versatile-thermostat-ui-card). Added a **Slow** regulation mode for slow latency heating devices [#168](https://github.com/jmcollin78/versatile_thermostat/issues/168). Change the way **the power is calculated** in case of VTherm with multi-underlying equipements [#146](https://github.com/jmcollin78/versatile_thermostat/issues/146). Added the support of AC and Heat for VTherm over switch alse [#144](https://github.com/jmcollin78/versatile_thermostat/pull/144)
> * **Release 3.8**: Added a **self-regulation function** for `over climate` thermostats whose regulation is done by the underlying climate. See [Self-regulation](#self-regulation) and [#129](https://github.com/jmcollin78/versatile_thermostat/issues/129). Added the possibility of **inverting the command** for an `over switch` thermostat to address installations with pilot wire and diode [#124](https://github.com/jmcollin78/versatile_thermostat/issues/124). > * **Release 3.8**: Added a **self-regulation function** for `over climate` thermostats whose regulation is done by the underlying climate. See [Self-regulation](#self-regulation) and [#129](https://github.com/jmcollin78/versatile_thermostat/issues/129). Added the possibility of **inverting the command** for an `over switch` thermostat to address installations with pilot wire and diode [#124](https://github.com/jmcollin78/versatile_thermostat/issues/124).
> * **Release 3.7**: Addition of the **Versatile Thermostat type `over valve`** to control a TRV valve directly or any other dimmer type equipment for heating. Regulation is then done directly by acting on the opening percentage of the underlying entity: 0 the valve is cut off, 100: the valve is fully opened. See [#131](https://github.com/jmcollin78/versatile_thermostat/issues/131). Added a function allowing the bypass of opening detection [#138](https://github.com/jmcollin78/versatile_thermostat/issues/138). Added Slovak language > * **Release 3.7**: Addition of the **Versatile Thermostat type `over valve`** to control a TRV valve directly or any other dimmer type equipment for heating. Regulation is then done directly by acting on the opening percentage of the underlying entity: 0 the valve is cut off, 100: the valve is fully opened. See [#131](https://github.com/jmcollin78/versatile_thermostat/issues/131). Added a function allowing the bypass of opening detection [#138](https://github.com/jmcollin78/versatile_thermostat/issues/138). Added Slovak language
<details>
<summary>Others releases</summary>
> * **Release 3.6**: Added the `motion_off_delay` parameter to improve motion management [#116](https://github.com/jmcollin78/versatile_thermostat/issues/116), [#128](https://github.com/jmcollin78/versatile_thermostat/issues/128). Added AC (air conditioning) mode for a VTherm over switch. Preparing the Github project to facilitate contributions [#127](https://github.com/jmcollin78/versatile_thermostat/issues/127) > * **Release 3.6**: Added the `motion_off_delay` parameter to improve motion management [#116](https://github.com/jmcollin78/versatile_thermostat/issues/116), [#128](https://github.com/jmcollin78/versatile_thermostat/issues/128). Added AC (air conditioning) mode for a VTherm over switch. Preparing the Github project to facilitate contributions [#127](https://github.com/jmcollin78/versatile_thermostat/issues/127)
> * **Release 3.5**: Multiple thermostats when using "thermostat over another thermostat" mode [#113](https://github.com/jmcollin78/versatile_thermostat/issues/113) > * **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.4**: bug fixes and expose preset temperatures for AC mode [#103](https://github.com/jmcollin78/versatile_thermostat/issues/103)
@@ -96,8 +126,78 @@ This custom component for Home Assistant is an upgrade and is a complete rewrite
> * **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. > * **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.
</details> </details>
# Major changes in version 5.0 # Changes in version 6.0
![New](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/new-icon.png?raw=true)
## Temperature entities for presets
Preset temperatures are now directly accessible in the form of entities linked to VTherm.
Example :
![Temperature entities](images/temp-entities-1.png)
The Boost, Comfort, Eco and Frost Protection entities allow you to directly adjust the temperatures of these presets without having to reconfigure the VTHerm in the configuration screens.
These modifications persist after a restart and are taken into account immediately by VTherm.
Depending on the functions activated, the list of temperatures may be more or less complete:
1. If presence management is activated, absence presets are created. They are suffixed with 'abs' for absence,
2. If air conditioning management (AC Mode) is activated, air conditioning mode presets are created. They are suffixed with 'clim' for air conditioning. Only the Frost protection preset has no equivalent in air conditioning mode,
3. The different absent and air conditioning combinations can be created depending on the configuration of the VTherm
If a VTherm uses the presets of the central configuration, these entities are not created, because the temperatures of the presets are managed by the central configuration.
### In the case of a central configuration
If you have configured a central configuration, this also has its own presets which meet the same rules as stated above.
Example of a central configuration with presence management and AC (air conditioning) mode:
![Temperature entities](images/temp-entities-2.png)
In the case of a change of a central configuration temperature, all VTherms that use this preset are immediately updated.
## Redesign of the configuration menu
The configuration menu has been completely revised. It dynamically adapts to the user's choices and allows direct access to the settings of the desired function without having to go through the entire configuration tunnel.
To create a new VTherm, you will first need to choose the type of VTherm:
![VTherm choice](images/config-main0.png)
Then, you now access the following configuration menu:
![VTherm menu](images/config-menu.png)
Each part to be configured is directly accessible, without having to go through the entire configuration tunnel as before.
You will note the menu option named `Functions` which allows you to choose which functions will be implemented for this VTherm:
![VTherm features](images/config-features.png)
Depending on your choices, the main menu will adapt to add the necessary options.
Example of menu with all functions checked:
![VTherm menu](images/config-menu-all-options.png)
You can see that the 'Opening detection', 'Motion detection', 'Power management' and 'Presence management' options have been added. You can then configure them.
### The 'Incomplete configuration' and 'Finalize' menu options
The last menu option is special. It allows you to validate the creation of the VTherm when all the functions have been correctly configured.
If one option is not configured correctly, the last option is:
![Incomplete configuration](images/config-not-complete.png)
Its selection does nothing but prevents you from finalizing the creation (resp. modification) of the VTherm.
**You must then search in the options which one is missing**.
Once all configuration is valid, the last option changes to:
![Complete configuration](images/config-complete.png)
Click on this option to create (resp. modify) the VTherm:
![Configuration Complete](images/config-terminate.png)
<details>
<summary>Changements dans la version 5.0</summary>
# Changements dans la version 5.0
You can now define a central configuration which will allow you to share certain attributes on all your VTherms (or only part of them). To use this possibility, you must: You can now define a central configuration which will allow you to share certain attributes on all your VTherms (or only part of them). To use this possibility, you must:
1. Create a VTherm of type “Central Configuration”, 1. Create a VTherm of type “Central Configuration”,
@@ -110,11 +210,10 @@ The configurable attributes in the central configuration are listed here: [Param
When changing the central configuration, all VTherms will be reloaded to take these changes into account. When changing the central configuration, all VTherms will be reloaded to take these changes into account.
Consequently, the entire configuration phase of a VTherm has been profoundly modified to be able to use the central configuration or overload the values of the central configuration with values specific to the VTherm being configured. Consequently, the entire configuration phase of a VTherm has been profoundly modified to be able to use the central configuration or overload the values of the central configuration with values specific to the VTherm being configured.
</details>
**Note:** the VTherm configuration screenshots have not been updated.
# Thanks for the beer [buymecoffee](https://www.buymeacoffee.com/jmcollin78) # Thanks for the beer [buymecoffee](https://www.buymeacoffee.com/jmcollin78)
Many thanks to @salabur, @pvince83, @bergoglio, @EPicLURcher, @ecolorado66, @Kriss1670, @maia, @f.maymil, @moutte69, @Jerome, @Gunnar M, @Greg.o, @John Burgess, @abyssmal, @capinfo26, @Helge, @MattG, @MattG, @Mexx62, @Someone, @Lajull for the beers. It's very nice and encourages me to continue! Many thanks to @salabur, @pvince83, @bergoglio, @EPicLURcher, @ecolorado66, @Kriss1670, @maia, @f.maymil, @moutte69, @Jerome, @Gunnar M, @Greg.o, @John Burgess, @abyssmal, @capinfo26, @Helge, @MattG, @MattG, @Mexx62, @Someone, @Lajull, @giopeco, @fredericselier, @philpagan, @studiogriffanti, @Edwin, @Sebbou, @Gerard R., @John Burgess, @Sylvoliv, @cdenfert, @stephane.l, @jms92100 for the beers. It's very nice and encourages me to continue!
# When to use / not use # When to use / not use
This thermostat can control 3 types of equipment: This thermostat can control 3 types of equipment:
@@ -137,6 +236,8 @@ Some TRV type thermostats are known to be incompatible with the Versatile Thermo
2. "Homematic" (and possible Homematic IP) thermostats are known to have problems with Versatile Thermostats because of limitations of the underlying RF protocol. This problem especially occurs when trying to control several Homematic thermostats at once in one Versatile Thermostat instance. In order to reduce duty cycle load, you may e.g. group thermostats with Homematic-specific procedures (e.g. using a wall thermostat) and let Versatile Thermostat only control the wall thermostat directly. Another option is to control only one thermostat and propagate the changes in HVAC mode and temperature by an automation. 2. "Homematic" (and possible Homematic IP) thermostats are known to have problems with Versatile Thermostats because of limitations of the underlying RF protocol. This problem especially occurs when trying to control several Homematic thermostats at once in one Versatile Thermostat instance. In order to reduce duty cycle load, you may e.g. group thermostats with Homematic-specific procedures (e.g. using a wall thermostat) and let Versatile Thermostat only control the wall thermostat directly. Another option is to control only one thermostat and propagate the changes in HVAC mode and temperature by an automation.
3. Thermostat of type Heatzy which doesn't supports the set_temperature command. 3. Thermostat of type Heatzy which doesn't supports the set_temperature command.
4. Thermostats of type Rointe tends to awake alone even if VTherm turns it off. Others functions works fine. 4. Thermostats of type Rointe tends to awake alone even if VTherm turns it off. Others functions works fine.
5. TRV of type Aqara SRTS-A01 and MOES TV01-ZB which doesn't have the return state `hvac_action` allowing to know if it is heating or not. So return states are not available. Others features, seems to work normally.
6. The Airwell with the "Midea AC LAN" integration. If two orders are too close, the device shut off.
# Why another thermostat implementation ? # Why another thermostat implementation ?
@@ -150,7 +251,9 @@ This component named __Versatile thermostat__ manage the following use cases :
- 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 **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 **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 safety parameters. - 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 safety parameters.
- Add sensors to see the internal states of the thermostat - Add sensors to see the internal states of the thermostat,
- Centralized control of all Versatile Thermostats to stop them all, switch them all to frost protection, force them into Heating mode (winter), force them into Cooling mode (summer).
- Control of a central boiler and the VTherms which must control this boiler.
# How to install this incredible Versatile Thermostat ? # How to install this incredible Versatile Thermostat ?
@@ -175,17 +278,39 @@ This component named __Versatile thermostat__ manage the following use cases :
# Configuration # Configuration
Note: no configuration in configuration.yaml is needed because all configuration is done through the standard GUI when adding the integration. -- VTherm = Versatile Thermostat in the remainder of this document --
> ![Tip](images/tips.png) _*Notes*_
>
> Three ways to configure VTherms are available:
> 1. Each Versatile Thermostat is completely independently configured. Choose this option if you do not want to have any central configuration or management.
> 2. Some aspects are configured centrally. This allows e.g. define the min/max temperature, open window detection, etc. at the level of a single central instance. For each VTherm you configure, you can then choose to use the central configuration or override it with custom settings.
> 3. In addition to this centralized configuration, all VTherms can be controlled by a single entity of type `select`. This function is named `central_mode`. This allows you to stop / start / freeze / etc. all VTherms at once. For each VTherm, the user indicates whether he is affected by this `central_mode`.
<details>
<summary>Creation of a new Versatile Thermostat</summary>
## Creation of a new Versatile Thermostat
Click on Add integration button in the integration page Click on Add integration button in the integration page
![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/add-an-integration.png?raw=true) ![image](images/add-an-integration.png)
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. 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: Then choose the type of VTherm you want to create:
![image](images/config-main0.png)
</details>
<details>
<summary>Minimal configuration update</summary>
## Minimal configuration update ## Minimal configuration update
![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/config-main.png?raw=true)
Then choose the “Main attributes” menu.
![image](images/config-main.png)
Give the main mandatory attributes: Give the main mandatory attributes:
1. a name (will be the name of the integration and also the name of the climate entity) 1. a name (will be the name of the integration and also the name of the climate entity)
@@ -195,16 +320,22 @@ Give the main mandatory attributes:
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). In ```over_climate``` mode, the cycle is only used to carry out basic controls but does not directly regulate the temperature. It's the underlying climate that does it, 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). In ```over_climate``` mode, the cycle is only used to carry out basic controls but does not directly regulate the temperature. It's the underlying climate that does it,
7. minimum and maximum thermostat temperatures, 7. minimum and maximum thermostat temperatures,
8. the power of the l'équipement which will activate the power and energy sensors of the device, 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. 9. the possibility of controlling the thermostat centrally. Cf [centralized control](#centralized-control),
10. the list of features that will be used for this thermostat. Depending on your choices, the following configuration screens will appear or not.
> ![Tip](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/tips.png?raw=true) _*Notes*_ > ![Tip](images/tips.png) _*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**, > 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. For the storage radiator for example it will be used unnecessarily. > 2. if the cycle is too short, the heater could never reach the target temperature. For the storage radiator for example it will be used unnecessarily.
</details>
<details>
<summary>Select the driven entity</summary>
## Select the driven entity ## Select the driven entity
Depending on your choice of thermostat type, you will need to choose one or more `switch`, `climate` or `number` type entities. Only entities compatible with the type are presented. Depending on your choice of thermostat type, you will need to choose one or more `switch`, `climate` or `number` type entities. Only entities compatible with the type are presented.
> ![Tip](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/tips.png?raw=true) _*How to choose the type*_ > ![Tip](images/tips.png) _*How to choose the type*_
> The choice of type is important. Even if it is always possible to modify it afterwards via the configuration HMI, it is preferable to ask yourself the following few questions: > The choice of type is important. Even if it is always possible to modify it afterwards via the configuration HMI, it is preferable to ask yourself the following few questions:
> 1. **what type of equipment am I going to pilot?** In order, here is what to do: > 1. **what type of equipment am I going to pilot?** In order, here is what to do:
> 1. if you have a thermostatic valve (TRV) that can be controlled in Home Assistant via a ```number``` type entity (for example a _Shelly TRV_), choose the `over_valve` type. It is the most direct type and which ensures the best regulation, > 1. if you have a thermostatic valve (TRV) that can be controlled in Home Assistant via a ```number``` type entity (for example a _Shelly TRV_), choose the `over_valve` type. It is the most direct type and which ensures the best regulation,
@@ -214,18 +345,21 @@ Depending on your choice of thermostat type, you will need to choose one or more
It is possible to choose an over switch thermostat which controls air conditioning by checking the "AC Mode" box. In this case, only the cooling mode will be visible. It is possible to choose an over switch thermostat which controls air conditioning by checking the "AC Mode" box. In this case, only the cooling mode will be visible.
### For a ```thermostat_over_switch``` type thermostat ### For a ```thermostat_over_switch``` type thermostat
![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/config-linked-entity.png?raw=true) ![image](images/en/config-linked-entity.png)
Some heater switches require regular "keep-alive messages" to prevent them from triggering a failsafe switch off. This feature can be enabled through the switch keep-alive interval configuration field.
The algorithm to use is currently limited to TPI is available. See [algorithm](#algorithm). The algorithm to use is currently limited to TPI is available. See [algorithm](#algorithm).
If several type entities are configured, the thermostat shifts the activations in order to minimize the number of switches active at a time t. This allows for better power distribution since each radiator will turn on in turn. If several type entities are configured, the thermostat shifts the activations in order to minimize the number of switches active at a time t. This allows for better power distribution since each radiator will turn on in turn.
Example of synchronized triggering: Example of synchronized triggering:
![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/multi-switch-activation.png?raw=true) ![image](images/multi-switch-activation.png)
It is possible to choose an over switch thermostat which controls air conditioning by checking the "AC Mode" box. In this case, only the cooling mode will be visible. It is possible to choose an over switch thermostat which controls air conditioning by checking the "AC Mode" box. In this case, only the cooling mode will be visible.
If your equipment is controlled by a pilot wire with a diode, you will certainly need to check the "Invert Check" box. It allows you to set the switch to On when you need to turn the equipment off and to Off when you need to turn it on. If your equipment is controlled by a pilot wire with a diode, you will certainly need to check the "Invert Check" box. It allows you to set the switch to On when you need to turn the equipment off and to Off when you need to turn it on.
### For a thermostat of type ```thermostat_over_climate```: ### For a thermostat of type ```thermostat_over_climate```:
![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/config-linked-entity2.png?raw=true) ![image](images/config-linked-entity2.png)
It is possible to choose an over climate thermostat which controls reversible air conditioning by checking the “AC Mode” box. In this case, depending on the equipment ordered, you will have access to heating and/or cooling. It is possible to choose an over climate thermostat which controls reversible air conditioning by checking the “AC Mode” box. In this case, depending on the equipment ordered, you will have access to heating and/or cooling.
@@ -248,7 +382,7 @@ The self-regulation function is configured with:
These three parameters make it possible to modulate the regulation and avoid multiplying the regulation sendings. Some equipment such as TRVs and boilers do not like the temperature setpoint to be changed too often. These three parameters make it possible to modulate the regulation and avoid multiplying the regulation sendings. Some equipment such as TRVs and boilers do not like the temperature setpoint to be changed too often.
> ![Tip](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/tips.png?raw=true) _*Implementation tip*_ > ![Tip](images/tips.png) _*Implementation tip*_
> 1. Do not start self-regulation straight away. Watch how the natural regulation of your equipment works. If you notice that the set temperature is not reached or that it is taking too long to be reached, start the regulation, > 1. Do not start self-regulation straight away. Watch how the natural regulation of your equipment works. If you notice that the set temperature is not reached or that it is taking too long to be reached, start the regulation,
> 2. First start with a slight self-regulation and keep both parameters at their default values. Wait a few days and check if the situation has improved, > 2. First start with a slight self-regulation and keep both parameters at their default values. Wait a few days and check if the situation has improved,
> 3. If this is not sufficient, switch to Medium self-regulation, wait for stabilization, > 3. If this is not sufficient, switch to Medium self-regulation, wait for stabilization,
@@ -333,6 +467,36 @@ and of course, configure the VTherm's self-regulation mode in **Expert** mode. A
For the changes to be taken into account, you must either **completely restart Home Assistant** or just the **Versatile Thermostat integration** (Dev tools / Yaml / reloading the configuration / Versatile Thermostat). For the changes to be taken into account, you must either **completely restart Home Assistant** or just the **Versatile Thermostat integration** (Dev tools / Yaml / reloading the configuration / Versatile Thermostat).
#### Internal temperature compensation
Sometimes, it happens that the internal thermometer of the underlying (TRV, air conditioning, etc.) is so wrong that self-regulation is not enough to regulate.
This happens when the internal thermometer is too close to the heat source. The internal temperature then rises much faster than the room temperature, which generates faults in the regulation.
Example :
1. the room temperature is 18°, the setpoint is 20°,
2. the internal temperature of the equipment is 22°,
3. if VTherm sends 21° as setpoint (= 20° + 1° auto-regulation), then the equipment will not heat because its internal temperature (22°) is above the setpoint (21°)
To overcome this, a new optional option was added in version 5.4: ![Use of internal temperature](images/config-use-internal-temp.png)
When enabled, this function will add the difference between the internal temperature and the room temperature to the setpoint to force heating.
In the example above, the difference is +4° (22° - 18°), so VTherm will send 25° (21°+4°) to the equipment forcing it to heat up.
This difference is calculated for each underlying because each has its own internal temperature. Think of a VTherm which would be connected to 3 TRVs each with its internal temperature for example.
We then obtain much more effective self-regulation which avoids the pitfall of large variations in faulty internal temperature.
#### synthesis of the self-regulation algorithm
The self-regulation algorithm can be summarized as follows:
1. initialize the target temperature as the VTherm setpoint,
1. If self-regulation is activated,
1. calculates the regulated temperature (valid for a VTherm),
2. take this temperature as a target,
2. For each underlying of the VTherm,
1. If "use internal temperature" is checked,
1. calculates the offset (trv internal temp - room temp),
2. Adding the offset to the target temperature,
3. sends the target temperature (= regulated temp + (internal temp - room temp)) to the underlying
#### Auto-fan mode #### Auto-fan mode
This mode introduced in 4.3 makes it possible to force the use of ventilation if the temperature difference is significant. In fact, by activating ventilation, distribution occurs more quickly, which saves time in reaching the target temperature. This mode introduced in 4.3 makes it possible to force the use of ventilation if the temperature difference is significant. In fact, by activating ventilation, distribution occurs more quickly, which saves time in reaching the target temperature.
You can choose which ventilation you want to activate between the following settings: Low, Medium, High, Turbo. You can choose which ventilation you want to activate between the following settings: Low, Medium, High, Turbo.
@@ -342,21 +506,30 @@ If your equipment does not include Turbo mode, Forte` mode will be used as a rep
Once the temperature difference becomes low again, the ventilation will go into a "normal" mode which depends on your equipment, namely (in order): `Silence (mute)`, `Auto (auto)`, `Low (low)`. The first value that is possible for your equipment will be chosen. Once the temperature difference becomes low again, the ventilation will go into a "normal" mode which depends on your equipment, namely (in order): `Silence (mute)`, `Auto (auto)`, `Low (low)`. The first value that is possible for your equipment will be chosen.
### For a thermostat of type ```thermostat_over_valve```: ### For a thermostat of type ```thermostat_over_valve```:
![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/config-linked-entity3.png?raw=true) ![image](images/config-linked-entity3.png)
You can choose up to domain entity ```number``` or ```ìnput_number``` which will control the valves. You can choose up to domain entity ```number``` or ```ìnput_number``` which will control the valves.
The algorithm to use is currently limited to TPI is available. See [algorithm](#algorithm). The algorithm to use is currently limited to TPI is available. See [algorithm](#algorithm).
It is possible to choose an over valve thermostat which controls air conditioning by checking the "AC Mode" box. In this case, only the cooling mode will be visible. It is possible to choose an over valve thermostat which controls air conditioning by checking the "AC Mode" box. In this case, only the cooling mode will be visible.
</details>
<details>
<summary>Configure the TPI algorithm coefficients</summary>
## Configure the TPI algorithm coefficients ## Configure the TPI algorithm coefficients
click on 'Validate' on the previous page, and if you choose a ```over_switch``` or ```over_valve``` thermostat and you will get there:
![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/config-tpi.png?raw=true) Ff you choose a ```over_switch``` or ```over_valve``` thermostat and select the "TPI" menu option, you will get there:
![image](images/config-tpi.png)
For more informations on the TPI algorithm and tuned please refer to [algorithm](#algorithm). For more informations on the TPI algorithm and tuned please refer to [algorithm](#algorithm).
</details>
<details>
<summary>Configure the preset temperature</summary>
## Configure the preset temperature ## Configure the preset temperature
Click on 'Validate' on the previous page and you will get there:
![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/config-presets.png?raw=true)
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 : 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 - **Eco** : device is running an energy-saving mode
@@ -367,14 +540,22 @@ The preset mode allows you to pre-configurate targeted temperature. Used in conj
**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. **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.
> ![Tip](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/tips.png?raw=true) _*Notes*_ The pre-settings are made (since v6.0) directly from the VTherm entities or from the central configuration if you use the central configuration.
> ![Tip](images/tips.png) _*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. > 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). > 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). > 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 ``safety`` if the temperature could not be retrieved after a certain delay > 4. if you uses the advanced configuration you will see the preset set to ``safety`` 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 > 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
</details>
<details>
<summary>Configure the doors/windows turning on/off the thermostats</summary>
## Configure the doors/windows turning on/off the thermostats ## 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. 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: The detection of openings can be done in 2 ways:
1. either with a sensor placed on the opening (sensor mode), 1. either with a sensor placed on the opening (sensor mode),
@@ -382,14 +563,14 @@ The detection of openings can be done in 2 ways:
### The sensor mode ### The sensor mode
In sensor mode, you must fill in the following information: In sensor mode, you must fill in the following information:
![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/config-window-sensor.png?raw=true) ![image](images/config-window-sensor.png)
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 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. 2. a **delay in seconds** before any change. This allows a window to be opened quickly without stopping the heating.
### Auto mode ### Auto mode
In auto mode, the configuration is as follows: In auto mode, the configuration is as follows:
![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/config-window-auto.png?raw=true) ![image](images/config-window-auto.png)
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), 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), 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),
@@ -401,22 +582,27 @@ To set the thresholds it is advisable to start with the reference values a
- maximum duration: 60 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. 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.
![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/temperature-slope.png?raw=true) ![image](images/temperature-slope.png)
To properly adjust it is advisable to display on the same historical graph the temperature curve and the slope of the curve (the "slope"): To properly adjust it is advisable to display on the same historical graph the temperature curve and the slope of the curve (the "slope"):
![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/window-auto-tuning.png?raw=true) ![image](images/window-auto-tuning.png)
And that's all ! your thermostat will turn off when the windows are open and turn back on when closed. And that's all ! your thermostat will turn off when the windows are open and turn back on when closed.
> ![Tip](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/tips.png?raw=true) _*Notes*_ > ![Tip](images/tips.png) _*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/) > 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, > 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, > 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, ...) > 4. It is not recommended to use the automatic mode for equipment subject to frequent and normal temperature variations (corridors, open areas, ...)
</details>
<details>
<summary>Configure the activity mode or motion detection</summary>
## Configure the activity mode or motion detection ## 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: If you choose the ```Motion management``` feature, lick on 'Validate' on the previous page and you will get there:
![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/config-motion.png?raw=true) ![image](images/config-motion.png)
We will now see how to configure the new Activity mode. We will now see how to configure the new Activity mode.
What we need: What we need:
@@ -435,13 +621,18 @@ What we need:
For this to work, the climate thermostat should be in ``Activity`` preset mode. For this to work, the climate thermostat should be in ``Activity`` preset mode.
> ![Tip](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/tips.png?raw=true) _*Notes*_ > ![Tip](images/tips.png) _*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 > 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
</details>
<details>
<summary>Configure the power management</summary>
## Configure the power management ## Configure the power management
If you choose the ```Power management``` feature, click on 'Validate' on the previous page and you will get there: If you choose the ```Power management``` feature, click on 'Validate' on the previous page and you will get there:
![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/config-power.png?raw=true) ![image](images/config-power.png)
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 heaters linked to the VTherm** (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. 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 heaters linked to the VTherm** (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.
@@ -449,18 +640,23 @@ This feature allows you to regulate the power consumption of your radiators. Kno
Note that all power values should have the same units (kW or W for example). 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. This allows you to change the max power along time using a Scheduler or whatever you like.
> ![Tip](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/tips.png?raw=true) _*Notes*_ > ![Tip](images/tips.png) _*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. > 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. > 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. > 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 > 4. If you don't want to use this feature, just leave the entities id empty
> 5. If you control several heaters, the **power consumption of your heater** setup should be the sum of the power. > 5. If you control several heaters, the **power consumption of your heater** setup should be the sum of the power.
</details>
<details>
<summary>Configure presence or occupancy</summary>
## Configure presence or occupancy ## Configure presence or occupancy
If selected on the first page, this feature allows you to dynamically change the temperature of all configured thermostat presets when no one is home or when someone comes home. To do this, you must configure the temperature that will be used for each preset when presence is disabled. When the presence sensor turns off, these temperatures will be used. When it turns back on, the "normal" temperature configured for the preset is used. See [preset management](#configure-the-preset-temperature). If selected on the first page, this feature allows you to dynamically change the temperature of all configured thermostat presets when no one is home or when someone comes home. To do this, you must configure the temperature that will be used for each preset when presence is disabled. When the presence sensor turns off, these temperatures will be used. When it turns back on, the "normal" temperature configured for the preset is used. See [preset management](#configure-the-preset-temperature).
To configure presence, complete this form: To configure presence, complete this form:
![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/config-presence.png?raw=true) ![image](images/config-presence.png)
To do this, you must configure: To do this, you must configure:
1. An **occupancy sensor** whose state must be 'on' or 'home' if someone is present or 'off' or 'not_home' otherwise, 1. An **occupancy sensor** whose state must be 'on' or 'home' if someone is present or 'off' or 'not_home' otherwise,
@@ -472,15 +668,21 @@ If AC mode is used, you will also be able to configure temperatures when the equ
ATTENTION: groups of people do not function as a presence sensor. They are not recognized as a presence sensor. You must use a template as described here [Using a group of people as a presence sensor](#using-a-group-of-people-as-a-presence-sensor). ATTENTION: groups of people do not function as a presence sensor. They are not recognized as a presence sensor. You must use a template as described here [Using a group of people as a presence sensor](#using-a-group-of-people-as-a-presence-sensor).
> ![Tip](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/tips.png?raw=true) _*Notes*_ > ![Tip](images/tips.png) _*Notes*_
> 1. the change in temperature is immediate and is reflected on the front shutter. The calculation will take into account the new target temperature at the next calculation of the cycle, > 1. the change in temperature is immediate and is reflected on the front shutter. The calculation will take into account the new target temperature at the next calculation of the cycle,
> 2. you can use the person.xxxx direct sensor or a group of Home Assistant sensors. The presence sensor manages the ``on`` or ``home`` states as present and the ``off`` or ``not_home`` states as absent. > 2. you can use the person.xxxx direct sensor or a group of Home Assistant sensors. The presence sensor manages the ``on`` or ``home`` states as present and the ``off`` or ``not_home`` states as absent.
</details>
<details>
<summary>Advanced configuration</summary>
## Advanced configuration ## Advanced configuration
Those parameters allows to fine tune the thermostat. Those parameters allows to fine tune the thermostat.
The advanced configuration form is the following: The advanced configuration form is the following:
![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/config-advanced.png?raw=true) ![image](images/config-advanced.png)
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 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.
@@ -491,18 +693,159 @@ Setting this parameter to ``0.00`` will trigger the safety preset regardless of
The fourth parameter (``security_default_on_percent``) is the ``on_percent`` value that will be used when the thermostat enters ``safety`` mode. If you put ``0`` then the thermostat will be cut off when it goes into ``safety`` mode, putting 0.2% for example allows you to keep a little heating (20% in this case), even in mode ``safety``. It avoids finding your home totally frozen during a thermometer failure. The fourth parameter (``security_default_on_percent``) is the ``on_percent`` value that will be used when the thermostat enters ``safety`` mode. If you put ``0`` then the thermostat will be cut off when it goes into ``safety`` mode, putting 0.2% for example allows you to keep a little heating (20% in this case), even in mode ``safety``. It avoids finding your home totally frozen during a thermometer failure.
Since version 5.3 it is possible to deactivate the safety device following a lack of data from the outdoor thermometer. Indeed, this most of the time having a low impact on regulation (depending on your settings), it is possible that it is absent without endangering the home. To do this, you must add the following lines to your `configuration.yaml`:
```
versatile_thermostat:
...
safety_mode:
check_outdoor_sensor: false
```
By default, the outdoor thermometer can trigger a trip if it no longer sends a value.
See [example tuning](#examples-tuning) for common tuning examples See [example tuning](#examples-tuning) for common tuning examples
>![Tip](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/tips.png?raw=true) _*Notes*_ >![Tip](images/tips.png) _*Notes*_
> 1. When the temperature sensor comes to life and returns the temperatures, the preset will be restored to its previous value, > 1. When the temperature sensor comes to life and returns the temperatures, the preset will be restored to its previous value,
> 2. Attention, two temperatures are needed: internal temperature and external temperature and each must give the temperature, otherwise the thermostat will be in "safety" preset, > 2. Attention, two temperatures are needed: internal temperature and external temperature and each must give the temperature, otherwise the thermostat will be in "safety" preset,
> 3. A service is available that allows you to set the 3 safety parameters. This can be used to adapt the safety function to your use. > 3. A service is available that allows you to set the 3 safety parameters. This can be used to adapt the safety function to your use.
> 4. For natural usage, the ``security_default_on_percent`` should be less than ``security_min_on_percent``, > 4. For natural usage, the ``security_default_on_percent`` should be less than ``security_min_on_percent``,
> 5. Thermostat of type ``thermostat_over_climate`` are not concerned by the safety feature. > 5. Thermostat of type ``thermostat_over_climate`` are not concerned by the safety feature.
## Parameters synthesis </details>
| Paramètre | Libellé | "over switch" | "over climate" | "over valve" | "central configuration" | <details>
<summary>Centralized control</summary>
## Centralized control
Since release 5.2, if you have defined a centralized configuration, you have a new entity named `select.central_mode` which allows you to control all VTherms with a single action. For a VTherm to be centrally controllable, its configuration attribute named `use_central_mode` must be true.
This entity is presented in the form of a list of choices which contains the following choices:
1. `Auto`: the 'normal' mode in which each VTherm behaves as in previous versions,
2. `Stooped`: all VTherms are turned off (`hvac_off`),
3. `Heat only`: all VTherms are put in heating mode when this mode is supported by the VTherm, otherwise it is stopped,
3. `Cool only`: all VTherms are put in cooling mode when this mode is supported by the VTherm, otherwise it is stopped,
4. `Frost protection`: all VTherms are put in frost protection preset when this preset is supported by the VTherm, otherwise it is stopped.
It is therefore possible to control all VTherms (only those explicitly designated) with a single control.
Example rendering:
![central_mode](images/central_mode.png)
</details>
<details>
<summary>Control of a central boiler</summary>
## Control of a central boiler
Since release 5.3, you have the possibility of controlling a centralized boiler. From the moment it is possible to start or stop this boiler from Home Assistant, then Versatile Thermostat will be able to control it directly.
The principle put in place is generally as follows:
1. a new entity of type `binary_sensor` and named by default `binary_sensor.central_boiler` is added,
2. in the VTherms configuration you indicate whether the VTherm should control the boiler. Indeed, in a heterogeneous installation, some VTherm must control the boiler and others not. You must therefore indicate in each VTherm configuration whether it controls the boiler or not,
3. the `binary_sensor.central_boiler` listens for changes in state of VTherm equipment marked as controlling the boiler,
4. as soon as the number of devices controlled by the VTherm requesting heating (ie its `hvac_action` goes to `Heating`) exceeds a configurable threshold, then the `binary_sensor.central_boiler` goes to `on` and **if a activation service has been configured, then this service is called**,
5. if the number of devices requiring heating falls below the threshold again, then the `binary_sensor.central_boiler` goes to `off` and if **a deactivation service has been configured, then this service is called**,
6. you have access to two entities:
- one of type `number` named by default `number.boiler_activation_threshold`, gives the trigger threshold. This threshold is in number of equipment (radiators) which requires heating.
- one of type `sensor` named by default `sensor.nb_device_active_for_boiler`, gives the number of devices requiring heating. For example, a VTherm having 4 valves including 3 heating requests will increase this sensor to 3. Only VTherm equipment that is marked to control the central boiler is counted.
You therefore always have the information which allows you to control and adjust the activation of the boiler.
All these entities are attached to the central configuration service:
![The entities controlling the boiler](images/entitites-central-boiler.png)
### Setup
To configure this function, you must have a centralized configuration (see [Configuration](#configuration)) and check the 'Add a central boiler' box:
![Adding a central boiler](images/config-central-boiler-1.png)
On the following page you can configure the services to be called when switching the boiler on/off:
![Adding a central boiler](images/config-central-boiler-2.png)
The services are configured as indicated 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 that controls the boiler in the form `domain.entity_name`. For example: `switch.boiler` for boilers controlled by a switch or `climate.boiler` for a boiler controlled by a thermostat or any other entity which allows control of the boiler (there is no limitation). We can also switch inputs (`helpers`) like `input_boolean` or `input_number`.
3. `service_id` is the name of the service to call 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. For some service you will need a parameter. This can be the 'HVAC Mode' `climate.set_hvac_mode` or the target temperature for `climate.set_temperature`. This parameter must be configured in the form `attribute:value` at the end of the string.
Examples (to be adjusted to your case):
- `climate.chaudiere/climate.set_hvac_mode/hvac_mode:heat`: to turn on the boiler thermostat in heating mode,
- `climate.chaudiere/climate.set_hvac_mode/hvac_mode:off`: to stop the boiler thermostat,
- `switch.pompe_chaudiere/switch.turn_on`: to turn on the switch which powers the boiler pump,
- `switch.pompe_chaudiere/switch.turn_off`: to turn on the switch which powers the boiler pump,
- ...
### How to find the right service?
To find the service to use, the best is to go to "Development tools / Services", look for the service called, the entity to order and the possible parameter to give.
Click on 'Call Service'. If your boiler lights up you have the correct configuration. Then switch to Yaml mode and copy the parameters.
Example:
Under "Development Tools / Service":
![Service configuration](images/dev-tools-turnon-boiler-1.png)
In yaml mode:
![Service configuration](images/dev-tools-turnon-boiler-2.png)
The service to configure is then the following: `climate.empty_thermostast/climate.set_hvac_mode/hvac_mode:heat` (note the removal of the blank in `hvac_mode:heat`)
Then do the same for the extinguishing service and you are all set.
### The events
Each time the boiler is successfully switched on or off, an event is sent by Versatile Thermostat. It can advantageously be captured by automation, for example to notify a change.
The events look like this:
An ignition event:
```
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
```
An extinction event:
```
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
> ![Tip](images/tips.png) _*Notes*_
> Controlling a central boiler using software or hardware such as home automation can pose risks to its proper functioning. Before using these functions, make sure that your boiler has safety functions and that they are working. Turning on a boiler if all the taps are closed can generate excess pressure, for example.
</details>
<details>
<summary>Parameter summary</summary>
## Parameter summary
| Parameter | Description | "over switch" | "over climate" | "over valve" | "central configuration" |
| ----------------------------------------- | ----------------------------------------------------------------------------- | ------------- | ------------------- | ------------ | ----------------------- | | ----------------------------------------- | ----------------------------------------------------------------------------- | ------------- | ------------------- | ------------ | ----------------------- |
| ``name`` | Name | X | X | X | - | | ``name`` | Name | X | X | X | - |
| ``thermostat_type`` | Thermostat type | X | X | X | - | | ``thermostat_type`` | Thermostat type | X | X | X | - |
@@ -512,6 +855,7 @@ See [example tuning](#examples-tuning) for common tuning examples
| ``temp_min`` | Minimal temperature allowed | X | X | X | X | | ``temp_min`` | Minimal temperature allowed | X | X | X | X |
| ``temp_max`` | Maximal temperature allowed | X | X | X | X | | ``temp_max`` | Maximal temperature allowed | X | X | X | X |
| ``device_power`` | Total device power | X | X | X | - | | ``device_power`` | Total device power | X | X | X | - |
| ``use_central_mode`` | Allow the centralized control | X | X | X | - |
| ``use_window_feature`` | Use window detection | X | X | X | - | | ``use_window_feature`` | Use window detection | X | X | X | - |
| ``use_motion_feature`` | Use motion detection | X | X | X | - | | ``use_motion_feature`` | Use motion detection | X | X | X | - |
| ``use_power_feature`` | Use power management | X | X | X | - | | ``use_power_feature`` | Use power management | X | X | X | - |
@@ -520,6 +864,7 @@ See [example tuning](#examples-tuning) for common tuning examples
| ``heater_entity2_id`` | 2nd heater switch | X | - | - | - | | ``heater_entity2_id`` | 2nd heater switch | X | - | - | - |
| ``heater_entity3_id`` | 3rd heater switch | X | - | - | - | | ``heater_entity3_id`` | 3rd heater switch | X | - | - | - |
| ``heater_entity4_id`` | 4th heater switch | X | - | - | - | | ``heater_entity4_id`` | 4th heater switch | X | - | - | - |
| ``heater_keep_alive`` | Switch keep-alive interval | X | - | - | - |
| ``proportional_function`` | Algorithm | X | - | X | - | | ``proportional_function`` | Algorithm | X | - | X | - |
| ``climate_entity1_id`` | 1rst underlying climate | - | X | - | - | | ``climate_entity1_id`` | 1rst underlying climate | - | X | - | - |
| ``climate_entity2_id`` | 2nd underlying climate | - | X | - | - | | ``climate_entity2_id`` | 2nd underlying climate | - | X | - | - |
@@ -532,13 +877,6 @@ See [example tuning](#examples-tuning) for common tuning examples
| ``ac_mode`` | Use the Air Conditioning (AC) mode | X | X | X | - | | ``ac_mode`` | Use the Air Conditioning (AC) mode | X | X | X | - |
| ``tpi_coef_int`` | Coefficient to use for internal temperature delta | X | - | X | X | | ``tpi_coef_int`` | Coefficient to use for internal temperature delta | X | - | X | X |
| ``tpi_coef_ext`` | Coefficient to use for external temperature delta | X | - | X | X | | ``tpi_coef_ext`` | Coefficient to use for external temperature delta | X | - | X | X |
| ``frost_temp`` | Temperature in frost protection preset | X | X | X | X |
| ``eco_temp`` | Temperature in Eco preset | X | X | X | X |
| ``comfort_temp`` | Temperature in Comfort preset | X | X | X | X |
| ``boost_temp`` | Temperature in Boost preset | X | X | X | X |
| ``eco_ac_temp`` | Temperature in Eco preset for AC mode | X | X | X | X |
| ``comfort_ac_temp`` | Temperature in Comfort preset for AC mode | X | X | X | X |
| ``boost_ac_temp`` | Temperature in Boost preset for AC mode | X | X | X | X |
| ``window_sensor_entity_id`` | Window sensor entity id | X | X | X | - | | ``window_sensor_entity_id`` | Window sensor entity id | X | X | X | - |
| ``window_delay`` | Window sensor delay (seconds) | X | X | X | X | | ``window_delay`` | Window sensor delay (seconds) | X | X | X | X |
| ``window_auto_open_threshold`` | Temperature decrease threshold for automatic window open detection (in °/min) | X | X | X | X | | ``window_auto_open_threshold`` | Temperature decrease threshold for automatic window open detection (in °/min) | X | X | X | X |
@@ -553,13 +891,6 @@ See [example tuning](#examples-tuning) for common tuning examples
| ``max_power_sensor_entity_id`` | Max power sensor entity id | X | X | X | X | | ``max_power_sensor_entity_id`` | Max power sensor entity id | X | X | X | X |
| ``power_temp`` | Temperature for Power shedding | X | X | X | X | | ``power_temp`` | Temperature for Power shedding | X | X | X | X |
| ``presence_sensor_entity_id`` | Presence sensor entity id | X | X | X | X | | ``presence_sensor_entity_id`` | Presence sensor entity id | X | X | X | X |
| ``frost_away_temp`` | Temperature in Frost protection preset when no presence | X | X | X | X |
| ``eco_away_temp`` | Temperature in Eco preset when no presence | X | X | X | X |
| ``comfort_away_temp`` | Temperature in Comfort preset when no presence | X | X | X | X |
| ``boost_away_temp`` | Temperature in Boost preset when no presence | X | X | X | X |
| ``eco_ac_away_temp`` | Temperature in Eco preset when no presence in AC mode | X | X | X | X |
| ``comfort_ac_away_temp`` | Temperature in Comfort preset when no presence in AC mode | X | X | X | X |
| ``boost_ac_away_temp`` | Temperature in Boost preset when no presence in AC mode | X | X | X | X |
| ``minimal_activation_delay`` | Minimal activation delay | X | - | X | X | | ``minimal_activation_delay`` | Minimal activation delay | X | - | X | X |
| ``security_delay_min`` | Safety delay (in minutes) | X | - | X | X | | ``security_delay_min`` | Safety delay (in minutes) | X | - | X | X |
| ``security_min_on_percent`` | Minimal power percent to enable safety mode | X | - | X | X | | ``security_min_on_percent`` | Minimal power percent to enable safety mode | X | - | X | X |
@@ -568,9 +899,15 @@ See [example tuning](#examples-tuning) for common tuning examples
| ``auto_regulation_dtemp`` | La seuil d'auto-régulation | - | X | - | - | | ``auto_regulation_dtemp`` | La seuil d'auto-régulation | - | X | - | - |
| ``auto_regulation_period_min`` | La période minimale d'auto-régulation | - | X | - | - | | ``auto_regulation_period_min`` | La période minimale d'auto-régulation | - | X | - | - |
| ``inverse_switch_command`` | Inverse the switch command (for pilot wire switch) | X | - | - | - | | ``inverse_switch_command`` | Inverse the switch command (for pilot wire switch) | X | - | - | - |
| ``auto_fan_mode` | Auto fan mode | - | X | - | - | | ``auto_fan_mode`` | Auto fan mode | - | X | - | - |
| ``auto_regulation_use_device_temp`` | Use the internal temperature of the underlying device | - | X | - | - |
| ``use_central_boiler_feature`` | Add the control of a central boiler | - | - | - | X |
| ``central_boiler_activation_service`` | Activation service of the boiler | - | - | - | X |
| ``central_boiler_deactivation_service`` | Deactivaiton service of the boiler | - | - | - | X |
| ``used_by_controls_central_boiler`` | Indicate if the VTherm control the central boiler | X | X | X | - |
</details>
# Examples tuning # Tuning examples
## Electrical heater ## Electrical heater
- cycle: between 5 and 10 minutes, - cycle: between 5 and 10 minutes,
@@ -644,7 +981,7 @@ See some situations at [examples](#some-results).
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: 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:
![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/thermostat-sensors.png?raw=true) ![image](images/thermostat-sensors.png)
In order, there are: In order, there are:
1. the main climate thermostat command entity, 1. the main climate thermostat command entity,
@@ -677,7 +1014,7 @@ frontend:
``` ```
and choose the ```versatile_thermostat_theme``` theme in the panel configuration. You will get something that will look like this: and choose the ```versatile_thermostat_theme``` theme in the panel configuration. You will get something that will look like this:
![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/colored-thermostat-sensors.png?raw=true) ![image](images/colored-thermostat-sensors.png)
# Services # Services
@@ -723,7 +1060,7 @@ target:
entity_id: climate.my_thermostat entity_id: climate.my_thermostat
``` ```
> ![Tip](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/tips.png?raw=true) _*Notes*_ > ![Tip](images/tips.png) _*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. - 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 safety settings ## Change safety settings
@@ -762,6 +1099,7 @@ The notified events are as follows:
- ``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_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_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 - ``versatile_thermostat_preset_event``: a new preset is selected on the thermostat. This event is also broadcast when the thermostat starts up
- ``versatile_thermostat_central_boiler_event``: an event indicating a change in the state of the central boiler.
If you have followed correctly, when a thermostat goes into safety mode, 3 events are triggered: 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, 1. ``versatile_thermostat_temperature_event`` to indicate that a thermometer has become unresponsive,
@@ -775,7 +1113,7 @@ You can very easily capture its events in an automation, for example to notify u
# Custom attributes # 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: 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:
![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/dev-tools-climate.png?raw=true) ![image](images/dev-tools-climate.png)
Custom attributes are the following: Custom attributes are the following:
@@ -819,27 +1157,30 @@ Custom attributes are the following:
| ``valve_open_percent`` | The opening percentage of the valve | | ``valve_open_percent`` | The opening percentage of the valve |
| ``regulated_target_temperature`` | The self-regulated target temperature calculated | | ``regulated_target_temperature`` | The self-regulated target temperature calculated |
| ``is_inversed`` | True if the command is inversed (pilot wire with diode) | | ``is_inversed`` | True if the command is inversed (pilot wire with diode) |
| ``is_controlled_by_central_mode`` | True if the VTherm can be centrally controlled |
| ``last_central_mode`` | The last central mode used (None if the VTherm is not centrally controlled) |
| ``is_used_by_central_boiler`` | Indicate if the VTherm can control the central boiler |
# Some results # Some results
**Convergence of temperature to target configured by preset:** **Convergence of temperature to target configured by preset:**
![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/results-1.png?raw=true) ![image](images/results-1.png)
[Cycle of on/off calculated by the integration:](https://) [Cycle of on/off calculated by the integration:](https://)
![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/results-2.png?raw=true) ![image](images/results-2.png)
**Coef_int too high (oscillations around the target)** **Coef_int too high (oscillations around the target)**
![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/results-3.png?raw=true) ![image](images/results-3.png)
**Algorithm calculation evolution** **Algorithm calculation evolution**
![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/results-4.png?raw=true) ![image](images/results-4.png)
See the code of this component [[below](#even-better-with-apex-chart-to-tune-your-thermostat)] See the code of this component [[below](#even-better-with-apex-chart-to-tune-your-thermostat)]
**Fine tuned thermostat** **Fine tuned thermostat**
Thank's [impuR_Shozz](https://forum.hacf.fr/u/impur_shozz/summary) ! 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. 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.
![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/results-fine-tuned.png?raw=true) ![image](images/results-fine-tuned.png)
Enjoy ! Enjoy !
@@ -878,7 +1219,7 @@ I hope this example helps you, don't hesitate to give me your feedbacks !
## Even-even better with custom:simple-thermostat front integration ## 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. 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 ![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/simple-thermostat.png?raw=true) You can have something like that very easily ![image](images/simple-thermostat.png)
Example configuration: Example configuration:
``` ```
@@ -920,7 +1261,7 @@ You can customize this component using the HACS card-mod component to adjust the
} }
{% endif %} {% endif %}
``` ```
![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/custom-css-thermostat.png?raw=true) ![image](images/custom-css-thermostat.png)
## Even better with Plotly to tune your Thermostat ## Even better with Plotly to tune your Thermostat
You can get curve like presented in [some results](#some-results) with kind of Plotly configuration only using the custom attributes of the thermostat described [here](#custom-attributes): You can get curve like presented in [some results](#some-results) with kind of Plotly configuration only using the custom attributes of the thermostat described [here](#custom-attributes):
@@ -991,6 +1332,11 @@ Replace values in [[ ]] by yours.
step: day step: day
``` ```
Example of graph obtained with Plotly :
![image](images/plotly-curves.png)
## 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
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 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.
@@ -1084,7 +1430,11 @@ If you want to contribute to this please read the [Contribution guidelines](CONT
# Troubleshooting # Troubleshooting
<details>
<summary>Using a Heatzy</summary>
## Using a Heatzy ## Using a Heatzy
The use of a Heatzy is possible provided you use a virtual switch on this model: The use of a Heatzy is possible provided you use a virtual switch on this model:
``` ```
- platform:template - platform:template
@@ -1114,6 +1464,11 @@ The use of a Heatzy is possible provided you use a virtual switch on this model:
``` ```
Thanks to @gael for this example. Thanks to @gael for this example.
</details>
<details>
<summary>Using a Heatsink with a Pilot Wire</summary>
## Using a Heatsink with a Pilot Wire ## Using a Heatsink with a Pilot Wire
As with the Heatzy above you can use a virtual switch which will change the preset of your radiator depending on the ignition state of the VTherm. As with the Heatzy above you can use a virtual switch which will change the preset of your radiator depending on the ignition state of the VTherm.
Example : Example :
@@ -1133,10 +1488,72 @@ Example :
entity_id: switch.radiateur_soan entity_id: switch.radiateur_soan
icon_template: "{% if is_state('switch.radiateur_soan', 'on') %}mdi:radiator-disabled{% else %}mdi:radiator{% endif %}" icon_template: "{% if is_state('switch.radiateur_soan', 'on') %}mdi:radiator-disabled{% else %}mdi:radiator{% endif %}"
``` ```
</details>
<details>
<summary>Using a heater with a Nodon</summary>
## Using a heater with a Nodon module
As for the heatzy module above you can use a virtual switch which will change the preset of your heater depending of the state of the VTherm.
Example :
```
- platform: template
switches:
chauffage_chb_parents:
unique_id: chauffage_chb_parents
friendly_name: Chauffage chambre parents
value_template: "{{ is_state('select.fp_chb_parents_pilot_wire_mode', 'comfort') }}"
icon_template: >-
{% if is_state('select.fp_chb_parents_pilot_wire_mode', 'comfort') %}
mdi:radiator
{% elif is_state('select.fp_chb_parents_pilot_wire_mode', 'frost_protection') %}
mdi:snowflake
{% else %}
mdi:radiator-disabled
{% endif %}
turn_on:
service: select.select_option
target:
entity_id: select.fp_chb_parents_pilot_wire_mode
data:
option: comfort
turn_off:
service: select.select_option
target:
entity_id: select.fp_chb_parents_pilot_wire_mode
data:
option: eco
```
</details>
<details>
<summary>Only the first radiator heats</summary>
## Only the first radiator heats ## Only the first radiator heats
In `over_switch` mode if several radiators are configured for the same VTherm, switching on will be done sequentially to smooth out consumption peaks as much as possible. In `over_switch` mode if several radiators are configured for the same VTherm, switching on will be done sequentially to smooth out consumption peaks as much as possible.
This is completely normal and desired. It is described here: [For a thermostat of type ``thermostat_over_switch```](#for-a-thermostat-of-type-thermostat_over_switch) This is completely normal and desired. It is described here: [For a thermostat of type ``thermostat_over_switch```](#for-a-thermostat-of-type-thermostat_over_switch)v
</details>
<details>
<summary>The radiator heats up even though the setpoint temperature is exceeded or does not heat up even though the room temperature is well below the setpoint</summary>
## The radiator heats up even though the setpoint temperature is exceeded or does not heat up even though the room temperature is well below the setpoint
### Type `over_switch` or `over_valve`
With a VTherm of type `over_switch` or `over_valve`, this fault just shows that the parameters of the TPI algorithm are incorrectly set. See [TPI Algorithm](#tpi-algorithm) to optimize the settings.
### Type `over_climate`
With an `over_climate` type VTherm, the regulation is done by the underlying `climate` directly and VTherm simply transmits the instructions to it. So if the radiator heats up when the set temperature is exceeded, it is certainly because its internal temperature measurement is biased. This happens very often with TRVs and reversible air conditioning units which have an internal temperature sensor, or too close to the heating element (therefore too cold in winter).
Example of discussion around these topics: [#348](https://github.com/jmcollin78/versatile_thermostat/issues/348), [#316](https://github.com/jmcollin78/versatile_thermostat/issues/316), [#312](https://github.com/jmcollin78/versatile_thermostat/discussions/312 ), [#278](https://github.com/jmcollin78/versatile_thermostat/discussions/278)
To get around this, VTherm is equipped with a function called self-regulation which allows the instruction sent to the underlying to be adapted until the target temperature is respected. This function compensates for the measurement bias of internal thermometers. If the bias is important the regulation must be important. See [Self-regulation](#self-regulation) to configure self-regulation.
</details>
<details>
<summary>Adjust window opening detection parameters in auto mode</summary>
## Adjust window opening detection parameters in auto mode ## Adjust window opening detection parameters in auto mode
@@ -1157,8 +1574,13 @@ versatile_thermostat:
``` ```
These parameters are sensitive and quite difficult to adjust. Please only use them if you know what you are doing and your temperature measurements are not already smooth. These parameters are sensitive and quite difficult to adjust. Please only use them if you know what you are doing and your temperature measurements are not already smooth.
</details>
<details>
<summary>Why does my Versatile Thermostat go into Safety?</summary>
## Why does my Versatile Thermostat go into Safety? ## Why does my Versatile Thermostat go into Safety?
Safety mode is only possible on VTherm `over_switch` and `over_valve`. It occurs when one of the 2 thermometers which gives the room temperature or the outside temperature has not sent a value for more than `security_delay_min` minutes and the radiator was heating at least `security_min_on_percent`. Safety mode is only possible on VTherm `over_switch` and `over_valve`. It occurs when one of the 2 thermometers which gives the room temperature or the outside temperature has not sent a value for more than `security_delay_min` minutes and the radiator was heating at least `security_min_on_percent`.
As the algorithm is based on temperature measurements, if they are no longer received by the VTherm, there is a risk of overheating and fire. To avoid this, when the conditions mentioned above are detected, heating is limited to the `security_default_on_percent` parameter. This value must therefore be reasonably low. It helps prevent a fire while avoiding completely cutting off the radiator (risk of freezing). As the algorithm is based on temperature measurements, if they are no longer received by the VTherm, there is a risk of overheating and fire. To avoid this, when the conditions mentioned above are detected, heating is limited to the `security_default_on_percent` parameter. This value must therefore be reasonably low. It helps prevent a fire while avoiding completely cutting off the radiator (risk of freezing).
@@ -1169,11 +1591,11 @@ All these parameters are adjusted on the last page of the VTherm configuration:
The first symptom is an abnormally low temperature with a slow and regular heating time in each cycle. The first symptom is an abnormally low temperature with a slow and regular heating time in each cycle.
Example: Example:
[safety mode](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/security-mode-symptome1.png?raw=true) [safety mode](images/security-mode-symptome1.png)
If you installed the [Versatile Thermostat UI Card](https://github.com/jmcollin78/versatile-thermostat-ui-card), the VTherm in question will have this shape: If you installed the [Versatile Thermostat UI Card](https://github.com/jmcollin78/versatile-thermostat-ui-card), the VTherm in question will have this shape:
[safety mode UI Card](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/security-mode-symptome2.png?raw=true) [safety mode UI Card](images/security-mode-symptome2.png)
You can also check in the VTherm attributes the dates of receipt of the different dates. Attributes are available in Development Tools / Reports. You can also check in the VTherm attributes the dates of receipt of the different dates. Attributes are available in Development Tools / Reports.
@@ -1204,8 +1626,13 @@ This will depend on the cause of the problem:
2. If the `security_delay_min` parameter is too small, it risks generating a lot of false alerts. A correct value is around 60 min, especially if you have battery-powered temperature sensors. 2. If the `security_delay_min` parameter is too small, it risks generating a lot of false alerts. A correct value is around 60 min, especially if you have battery-powered temperature sensors.
3. Some temperature sensors do not send a measurement if the temperature has not changed. So in the event of a very stable temperature for a long time, the safety mode may be triggered. This is not very serious since it is removed as soon as the VTherm receives a temperature again. On certain thermometers (TuYA for example), you can force the maximum delay between 2 measurements. It will be appropriate to set a max delay < `security_delay_min`, 3. Some temperature sensors do not send a measurement if the temperature has not changed. So in the event of a very stable temperature for a long time, the safety mode may be triggered. This is not very serious since it is removed as soon as the VTherm receives a temperature again. On certain thermometers (TuYA for example), you can force the maximum delay between 2 measurements. It will be appropriate to set a max delay < `security_delay_min`,
4. As soon as the temperature is received again the safety mode will be removed and the previous values of preset, target temperature and mode will be restored. 4. As soon as the temperature is received again the safety mode will be removed and the previous values of preset, target temperature and mode will be restored.
</details>
<details>
<summary>Using a group of people as a presence sensor</summary>
## Using a group of people as a presence sensor ## Using a group of people as a presence sensor
Unfortunately, groups of people are not recognized as presence sensors. We cannot therefore use them directly in VTherm. Unfortunately, groups of people are not recognized as presence sensors. We cannot therefore use them directly in VTherm.
The workaround is to create a binary_sensor template with the following code: The workaround is to create a binary_sensor template with the following code:
@@ -1226,6 +1653,21 @@ You will note in this example, the use of an input_boolean named force_presence
template: !include templates.yaml template: !include templates.yaml
... ...
``` ```
</details>
<details>
<summary>Enable Versatile Thermostat logs</summary>
## Enable Versatile Thermostat logs
Sometimes you will need to enable logs to refine the analyses. To do this, edit the `logger.yaml` file of your configuration and configure the logs as follows:
```
default: xxxx
logs:
custom_components.versatile_thermostat: info
```
You must reload the yaml configuration (Dev Tools / Yaml / All Yaml configuration) or restart Home Assistant for this change to take effect.
</details>
*** ***

View File

@@ -43,4 +43,13 @@ case $1 in
pwd pwd
./scripts/starts_ha.sh ./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 esac

7
copy-to-forum.txt Normal file
View File

@@ -0,0 +1,7 @@
Before copying to forum you need to replace relative images by this command into VSCode:
Search :
\(images/(.*).png\)
Replace with:
(https://github.com/jmcollin78/versatile_thermostat/blob/main/images/$1.png?raw=true)

View File

@@ -1,4 +1,5 @@
"""The Versatile Thermostat integration.""" """The Versatile Thermostat integration."""
from __future__ import annotations from __future__ import annotations
from typing import Dict from typing import Dict
@@ -8,24 +9,35 @@ import logging
import voluptuous as vol import voluptuous as vol
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.const import SERVICE_RELOAD from homeassistant.const import SERVICE_RELOAD, EVENT_HOMEASSISTANT_STARTED
from homeassistant.config_entries import ConfigEntry, ConfigType from homeassistant.config_entries import ConfigEntry, ConfigType
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant, CoreState, callback
from homeassistant.helpers.service import async_register_admin_service
from .base_thermostat import BaseThermostat from .base_thermostat import BaseThermostat
from .const import ( from .const import (
DOMAIN, DOMAIN,
PLATFORMS, PLATFORMS,
CONFIG_VERSION,
CONFIG_MINOR_VERSION,
CONF_AUTO_REGULATION_LIGHT, CONF_AUTO_REGULATION_LIGHT,
CONF_AUTO_REGULATION_MEDIUM, CONF_AUTO_REGULATION_MEDIUM,
CONF_AUTO_REGULATION_STRONG, CONF_AUTO_REGULATION_STRONG,
CONF_AUTO_REGULATION_SLOW, CONF_AUTO_REGULATION_SLOW,
CONF_AUTO_REGULATION_EXPERT, CONF_AUTO_REGULATION_EXPERT,
CONF_SHORT_EMA_PARAMS, CONF_SHORT_EMA_PARAMS,
CONF_SAFETY_MODE,
CONF_THERMOSTAT_CENTRAL_CONFIG, CONF_THERMOSTAT_CENTRAL_CONFIG,
CONF_THERMOSTAT_TYPE, 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,
) )
from .vtherm_api import VersatileThermostatAPI from .vtherm_api import VersatileThermostatAPI
@@ -47,12 +59,17 @@ EMA_PARAM_SCHEMA = {
vol.Required("precision"): cv.positive_int, vol.Required("precision"): cv.positive_int,
} }
SAFETY_MODE_PARAM_SCHEMA = {
vol.Required("check_outdoor_sensor"): bool,
}
CONFIG_SCHEMA = vol.Schema( CONFIG_SCHEMA = vol.Schema(
{ {
DOMAIN: vol.Schema( DOMAIN: vol.Schema(
{ {
CONF_AUTO_REGULATION_EXPERT: vol.Schema(SELF_REGULATION_PARAM_SCHEMA), CONF_AUTO_REGULATION_EXPERT: vol.Schema(SELF_REGULATION_PARAM_SCHEMA),
CONF_SHORT_EMA_PARAMS: vol.Schema(EMA_PARAM_SCHEMA), CONF_SHORT_EMA_PARAMS: vol.Schema(EMA_PARAM_SCHEMA),
CONF_SAFETY_MODE: vol.Schema(SAFETY_MODE_PARAM_SCHEMA),
} }
), ),
}, },
@@ -76,16 +93,31 @@ async def async_setup(
hass.data.setdefault(DOMAIN, {}) hass.data.setdefault(DOMAIN, {})
api: VersatileThermostatAPI = VersatileThermostatAPI.get_vtherm_api(hass)
# L'argument config contient votre fichier configuration.yaml # L'argument config contient votre fichier configuration.yaml
vtherm_config = config.get(DOMAIN) vtherm_config = config.get(DOMAIN)
if vtherm_config is not None: if vtherm_config is not None:
api: VersatileThermostatAPI = VersatileThermostatAPI.get_vtherm_api(hass)
api.set_global_config(vtherm_config) api.set_global_config(vtherm_config)
else: else:
_LOGGER.info("No global config from configuration.yaml available") _LOGGER.info("No global config from configuration.yaml available")
hass.helpers.service.async_register_admin_service( # 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, DOMAIN,
SERVICE_RELOAD, SERVICE_RELOAD,
_handle_reload, _handle_reload,
@@ -105,6 +137,10 @@ async def reload_all_vtherm(hass):
] ]
await asyncio.gather(*reload_tasks) 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: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
@@ -124,6 +160,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) 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()
return True return True
@@ -133,6 +173,11 @@ async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:
await reload_all_vtherm(hass) await reload_all_vtherm(hass)
else: else:
await hass.config_entries.async_reload(entry.entry_id) 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()
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
@@ -142,6 +187,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
if api: if api:
api.remove_entry(entry) api.remove_entry(entry)
await api.reload_central_boiler_entities_list()
return unload_ok return unload_ok
@@ -149,15 +195,40 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
# Example migration function # Example migration function
async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry): async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry):
"""Migrate old entry.""" """Migrate old entry."""
_LOGGER.debug("Migrating from version %s", config_entry.version) _LOGGER.debug(
"Migrating from version %s/%s", config_entry.version, config_entry.minor_version
)
if config_entry.version == 1: if (
config_entry.version != CONFIG_VERSION
or config_entry.minor_version != CONFIG_MINOR_VERSION
):
_LOGGER.debug(
"Migration to %s/%s is needed", CONFIG_VERSION, CONFIG_MINOR_VERSION
)
new = {**config_entry.data} new = {**config_entry.data}
# TO DO: modify Config Entry data if there will be something to migrate
config_entry.version = 2 if (
hass.config_entries.async_update_entry(config_entry, data=new) config_entry.data.get(CONF_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
)
_LOGGER.info("Migration to version %s successful", config_entry.version) new[CONF_USE_CENTRAL_BOILER_FEATURE] = new.get(
"add_central_boiler_control", False
) or new.get(CONF_USE_CENTRAL_BOILER_FEATURE, False)
hass.config_entries.async_update_entry(
config_entry,
data=new,
version=CONFIG_VERSION,
minor_version=CONFIG_MINOR_VERSION,
)
_LOGGER.info("Migration to version %s successful", config_entry.version)
return True return True

File diff suppressed because it is too large Load Diff

View File

@@ -1,9 +1,20 @@
""" Implements the VersatileThermostat binary sensors component """ """ Implements the VersatileThermostat binary sensors component """
# pylint: disable=unused-argument, line-too-long
import logging import logging
from homeassistant.core import HomeAssistant, callback, Event from homeassistant.core import (
HomeAssistant,
callback,
Event,
# CoreState,
HomeAssistantError,
)
from homeassistant.const import STATE_ON, STATE_OFF from homeassistant.const import STATE_ON, STATE_OFF # , EVENT_HOMEASSISTANT_START
from homeassistant.helpers.device_registry import DeviceInfo, DeviceEntryType
from homeassistant.helpers.event import async_track_state_change_event
from homeassistant.components.binary_sensor import ( from homeassistant.components.binary_sensor import (
BinarySensorEntity, BinarySensorEntity,
@@ -13,8 +24,14 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .commons import VersatileThermostatBaseEntity from .vtherm_api import VersatileThermostatAPI
from .commons import (
VersatileThermostatBaseEntity,
check_and_extract_service_configuration,
)
from .const import ( from .const import (
DOMAIN,
DEVICE_MANUFACTURER,
CONF_NAME, CONF_NAME,
CONF_USE_POWER_FEATURE, CONF_USE_POWER_FEATURE,
CONF_USE_PRESENCE_FEATURE, CONF_USE_PRESENCE_FEATURE,
@@ -22,6 +39,12 @@ from .const import (
CONF_USE_WINDOW_FEATURE, CONF_USE_WINDOW_FEATURE,
CONF_THERMOSTAT_TYPE, CONF_THERMOSTAT_TYPE,
CONF_THERMOSTAT_CENTRAL_CONFIG, 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__) _LOGGER = logging.getLogger(__name__)
@@ -41,23 +64,29 @@ async def async_setup_entry(
name = entry.data.get(CONF_NAME) name = entry.data.get(CONF_NAME)
vt_type = entry.data.get(CONF_THERMOSTAT_TYPE) vt_type = entry.data.get(CONF_THERMOSTAT_TYPE)
entities = None
if vt_type == CONF_THERMOSTAT_CENTRAL_CONFIG: if vt_type == CONF_THERMOSTAT_CENTRAL_CONFIG:
return 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))
entities = [ if entities:
SecurityBinarySensor(hass, unique_id, name, entry.data), async_add_entities(entities, True)
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))
async_add_entities(entities, True)
class SecurityBinarySensor(VersatileThermostatBaseEntity, BinarySensorEntity): class SecurityBinarySensor(VersatileThermostatBaseEntity, BinarySensorEntity):
@@ -269,7 +298,6 @@ class PresenceBinarySensor(VersatileThermostatBaseEntity, BinarySensorEntity):
return "mdi:nature-people" return "mdi:nature-people"
# PR - Adding Window ByPass
class WindowByPassBinarySensor(VersatileThermostatBaseEntity, BinarySensorEntity): class WindowByPassBinarySensor(VersatileThermostatBaseEntity, BinarySensorEntity):
"""Representation of a BinarySensor which exposes the Window ByPass state""" """Representation of a BinarySensor which exposes the Window ByPass state"""
@@ -307,3 +335,162 @@ class WindowByPassBinarySensor(VersatileThermostatBaseEntity, BinarySensorEntity
return "mdi:window-shutter-cog" return "mdi:window-shutter-cog"
else: else:
return "mdi:window-shutter-auto" return "mdi:window-shutter-auto"
class CentralBoilerBinarySensor(BinarySensorEntity):
"""Representation of a BinarySensor which exposes the Central Boiler state"""
def __init__(
self,
hass: HomeAssistant,
unique_id,
name, # pylint: disable=unused-argument
entry_infos,
) -> None:
"""Initialize the CentralBoiler Binary sensor"""
self._config_id = unique_id
self._attr_name = "Central boiler"
self._attr_unique_id = "central_boiler_state"
self._attr_is_on = False
self._device_name = entry_infos.get(CONF_NAME)
self._entities = []
self._hass = hass
self._service_activate = check_and_extract_service_configuration(
entry_infos.get(CONF_CENTRAL_BOILER_ACTIVATION_SRV)
)
self._service_deactivate = check_and_extract_service_configuration(
entry_infos.get(CONF_CENTRAL_BOILER_DEACTIVATION_SRV)
)
@property
def device_info(self) -> DeviceInfo:
"""Return the device info."""
return DeviceInfo(
entry_type=DeviceEntryType.SERVICE,
identifiers={(DOMAIN, self._config_id)},
name=self._device_name,
manufacturer=DEVICE_MANUFACTURER,
model=DOMAIN,
)
@property
def device_class(self) -> BinarySensorDeviceClass | None:
return BinarySensorDeviceClass.RUNNING
@property
def icon(self) -> str | None:
if self._attr_is_on:
return "mdi:water-boiler"
else:
return "mdi:water-boiler-off"
@overrides
async def async_added_to_hass(self) -> None:
await super().async_added_to_hass()
api: VersatileThermostatAPI = VersatileThermostatAPI.get_vtherm_api(self._hass)
api.register_central_boiler(self)
# Should be not more needed and replaced by vtherm_api.init_vtherm_links
# @callback
# async def _async_startup_internal(*_):
# _LOGGER.debug("%s - Calling async_startup_internal", self)
# await self.listen_nb_active_vtherm_entity()
#
# if self.hass.state == CoreState.running:
# await _async_startup_internal()
# else:
# self.hass.bus.async_listen_once(
# EVENT_HOMEASSISTANT_START, _async_startup_internal
# )
async def listen_nb_active_vtherm_entity(self):
"""Initialize the listening of state change of VTherms"""
# Listen to all VTherm state change
api: VersatileThermostatAPI = VersatileThermostatAPI.get_vtherm_api(self._hass)
if (
api.nb_active_device_for_boiler_entity
and api.nb_active_device_for_boiler_threshold_entity
):
listener_cancel = async_track_state_change_event(
self._hass,
[
api.nb_active_device_for_boiler_entity.entity_id,
api.nb_active_device_for_boiler_threshold_entity.entity_id,
],
self.calculate_central_boiler_state,
)
_LOGGER.debug(
"%s - entity to get the nb of active VTherm is %s",
self,
api.nb_active_device_for_boiler_entity.entity_id,
)
self.async_on_remove(listener_cancel)
else:
_LOGGER.debug("%s - no VTherm could controls the central boiler", self)
await self.calculate_central_boiler_state(None)
async def calculate_central_boiler_state(self, _):
"""Calculate the central boiler state depending on all VTherm that
controls this central boiler"""
_LOGGER.debug("%s - calculating the new central boiler state", self)
api: VersatileThermostatAPI = VersatileThermostatAPI.get_vtherm_api(self._hass)
if (
api.nb_active_device_for_boiler is None
or api.nb_active_device_for_boiler_threshold is None
):
_LOGGER.warning(
"%s - the entities to calculate the boiler state are not initialized. Boiler state cannot be calculated",
self,
)
return False
active = (
api.nb_active_device_for_boiler >= api.nb_active_device_for_boiler_threshold
)
if self._attr_is_on != active:
try:
if active:
await self.call_service(self._service_activate)
_LOGGER.info("%s - central boiler have been turned on", self)
else:
await self.call_service(self._service_deactivate)
_LOGGER.info("%s - central boiler have been turned off", self)
self._attr_is_on = active
send_vtherm_event(
hass=self._hass,
event_type=EventType.CENTRAL_BOILER_EVENT,
entity=self,
data={"central_boiler": active},
)
self.async_write_ha_state()
except HomeAssistantError as err:
_LOGGER.error(
"%s - Impossible to activate/deactivat boiler due to error %s."
"Central boiler will not being controled by VTherm."
"Please check your service configuration. Cf. README.",
self,
err,
)
async def call_service(self, service_config: dict):
"""Make a call to a service if correctly configured"""
if not service_config:
return
await self._hass.services.async_call(
service_config["service_domain"],
service_config["service_name"],
service_data=service_config["data"],
target={
"entity_id": service_config["entity_id"],
},
)
def __str__(self):
return f"VersatileThermostat-{self.name}"

View File

@@ -1,4 +1,5 @@
""" Implements the VersatileThermostat climate component """ """ Implements the VersatileThermostat climate component """
import logging import logging
@@ -44,9 +45,6 @@ from .thermostat_valve import ThermostatOverValve
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
# _LOGGER.setLevel(logging.DEBUG)
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, hass: HomeAssistant,
entry: ConfigEntry, entry: ConfigEntry,
@@ -74,6 +72,13 @@ async def async_setup_entry(
entity = ThermostatOverClimate(hass, unique_id, name, entry.data) entity = ThermostatOverClimate(hass, unique_id, name, entry.data)
elif vt_type == CONF_THERMOSTAT_VALVE: elif vt_type == CONF_THERMOSTAT_VALVE:
entity = ThermostatOverValve(hass, unique_id, name, entry.data) entity = ThermostatOverValve(hass, unique_id, name, entry.data)
else:
_LOGGER.error(
"Cannot create Versatile Thermostat name=%s of type %s which is unknown",
name,
vt_type,
)
return
async_add_entities([entity], True) async_add_entities([entity], True)

View File

@@ -1,4 +1,7 @@
""" Some usefull commons class """ """ Some usefull commons class """
# pylint: disable=line-too-long
import logging import logging
from datetime import timedelta, datetime from datetime import timedelta, datetime
from homeassistant.core import HomeAssistant, callback, Event from homeassistant.core import HomeAssistant, callback, Event
@@ -10,41 +13,148 @@ from homeassistant.helpers.event import async_track_state_change_event, async_ca
from homeassistant.util import dt as dt_util from homeassistant.util import dt as dt_util
from .base_thermostat import BaseThermostat from .base_thermostat import BaseThermostat
from .const import DOMAIN, DEVICE_MANUFACTURER from .const import DOMAIN, DEVICE_MANUFACTURER, ServiceConfigurationError
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
def get_tz(hass: HomeAssistant): def get_tz(hass: HomeAssistant):
"""Get the current timezone""" """Get the current timezone"""
return dt_util.get_time_zone(hass.config.time_zone) return dt_util.get_time_zone(hass.config.time_zone)
class NowClass: class NowClass:
""" For testing purpose only""" """For testing purpose only"""
@staticmethod @staticmethod
def get_now(hass: HomeAssistant) -> datetime: def get_now(hass: HomeAssistant) -> datetime:
""" A test function to get the now. """A test function to get the now.
For testing purpose this method can be overriden to get a specific For testing purpose this method can be overriden to get a specific
timestamp. timestamp.
""" """
return datetime.now( get_tz(hass)) return datetime.now(get_tz(hass))
def round_to_nearest(n:float, x: float)->float:
""" Round a number to the nearest x (which should be decimal but not null)
Example:
nombre1 = 3.2
nombre2 = 4.7
x = 0.3
nombre_arrondi1 = round_to_nearest(nombre1, x) def round_to_nearest(n: float, x: float) -> float:
nombre_arrondi2 = round_to_nearest(nombre2, x) """Round a number to the nearest x (which should be decimal but not null)
Example:
nombre1 = 3.2
nombre2 = 4.7
x = 0.3
print(nombre_arrondi1) # Output: 3.3 nombre_arrondi1 = round_to_nearest(nombre1, x)
print(nombre_arrondi2) # Output: 4.6 nombre_arrondi2 = round_to_nearest(nombre2, x)
print(nombre_arrondi1) # Output: 3.3
print(nombre_arrondi2) # Output: 4.6
""" """
assert x > 0 assert x > 0
return round(n * (1/x)) / (1/x) return round(n * (1 / x)) / (1 / x)
def check_and_extract_service_configuration(service_config) -> dict:
"""Raise a ServiceConfigurationError. In return you have a dict formatted like follows.
Example if you call with 'climate.central_boiler/climate.set_temperature/temperature:10':
{
"service_domain": "climate",
"service_name": "set_temperature",
"entity_id": "climate.central_boiler",
"entity_domain": "climate",
"entity_name": "central_boiler",
"data": {
"temperature": "10"
},
"attribute_name": "temperature",
"attribute_value: "10"
}
For this example 'switch.central_boiler/switch.turn_off' you will have this:
{
"service_domain": "switch",
"service_name": "turn_off",
"entity_id": "switch.central_boiler",
"entity_domain": "switch",
"entity_name": "central_boiler",
"data": { },
}
All values are striped (white space are removed) and are string
"""
ret = {}
if service_config is None:
return ret
parties = service_config.split("/")
if len(parties) < 2:
raise ServiceConfigurationError(
f"Incorrect service configuration. Service {service_config} should be formatted with: 'entity_name/service_name[/data]'. See README for more information."
)
entity_id = parties[0]
service_name = parties[1]
service_infos = service_name.split(".")
if len(service_infos) != 2:
raise ServiceConfigurationError(
f"Incorrect service configuration. The service {service_config} should be formatted like: 'domain.service_name' (ex: 'switch.turn_on'). See README for more information."
)
ret.update(
{
"service_domain": service_infos[0].strip(),
"service_name": service_infos[1].strip(),
}
)
entity_infos = entity_id.split(".")
if len(entity_infos) != 2:
raise ServiceConfigurationError(
f"Incorrect service configuration. The entity_id {entity_id} should be formatted like: 'domain.entity_name' (ex: 'switch.central_boiler_switch'). See README for more information."
)
ret.update(
{
"entity_domain": entity_infos[0].strip(),
"entity_name": entity_infos[1].strip(),
"entity_id": entity_id.strip(),
}
)
if len(parties) == 3:
data = parties[2]
if len(data) > 0:
data_infos = None
data_infos = data.split(":")
if (
len(data_infos) != 2
or len(data_infos[0]) <= 0
or len(data_infos[1]) <= 0
):
raise ServiceConfigurationError(
f"Incorrect service configuration. The data {data} should be formatted like: 'attribute:value' (ex: 'value:25'). See README for more information."
)
ret.update(
{
"attribute_name": data_infos[0].strip(),
"attribute_value": data_infos[1].strip(),
"data": {data_infos[0].strip(): data_infos[1].strip()},
}
)
else:
raise ServiceConfigurationError(
f"Incorrect service configuration. The data {data} should be formatted like: 'attribute:value' (ex: 'value:25'). See README for more information."
)
else:
ret.update({"data": {}})
_LOGGER.debug(
"check_and_extract_service_configuration(%s) gives '%s'", service_config, ret
)
return ret
class VersatileThermostatBaseEntity(Entity): class VersatileThermostatBaseEntity(Entity):
"""A base class for all entities""" """A base class for all entities"""
@@ -73,6 +183,9 @@ class VersatileThermostatBaseEntity(Entity):
"""Returns my climate if found""" """Returns my climate if found"""
if not self._my_climate: if not self._my_climate:
self._my_climate = self.find_my_versatile_thermostat() 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 return self._my_climate
@property @property
@@ -122,7 +235,7 @@ class VersatileThermostatBaseEntity(Entity):
) )
) )
else: else:
_LOGGER.warning("%s - no entity to listen. Try later", self) _LOGGER.debug("%s - no entity to listen. Try later", self)
self._cancel_call = async_call_later( self._cancel_call = async_call_later(
self.hass, timedelta(seconds=1), try_find_climate self.hass, timedelta(seconds=1), try_find_climate
) )
@@ -130,7 +243,14 @@ class VersatileThermostatBaseEntity(Entity):
await try_find_climate(None) await try_find_climate(None)
@callback @callback
async def async_my_climate_changed(self, event: Event): # pylint: disable=unused-argument def my_climate_is_initialized(self):
"""Called when the associated climate is initialized"""
return
@callback
async def async_my_climate_changed(
self, event: Event
): # pylint: disable=unused-argument
"""Called when my climate have change """Called when my climate have change
This method aims to be overriden to take the status change This method aims to be overriden to take the status change
""" """

View File

@@ -23,6 +23,7 @@ from homeassistant.data_entry_flow import FlowHandler, FlowResult
from .const import * # pylint: disable=wildcard-import, unused-wildcard-import from .const import * # pylint: disable=wildcard-import, unused-wildcard-import
from .config_schema import * # pylint: disable=wildcard-import, unused-wildcard-import from .config_schema import * # pylint: disable=wildcard-import, unused-wildcard-import
from .vtherm_api import VersatileThermostatAPI from .vtherm_api import VersatileThermostatAPI
from .commons import check_and_extract_service_configuration
COMES_FROM = "comes_from" COMES_FROM = "comes_from"
@@ -73,7 +74,9 @@ def add_suggested_values_to_schema(
class VersatileThermostatBaseConfigFlow(FlowHandler): class VersatileThermostatBaseConfigFlow(FlowHandler):
"""The base Config flow class. Used to put some code in commons.""" """The base Config flow class. Used to put some code in commons."""
VERSION = 1 VERSION = CONFIG_VERSION
MINOR_VERSION = CONFIG_MINOR_VERSION
_infos: dict _infos: dict
_placeholders = { _placeholders = {
CONF_NAME: "", CONF_NAME: "",
@@ -94,23 +97,35 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
self._init_feature_flags(infos) self._init_feature_flags(infos)
self._init_central_config_flags(infos) self._init_central_config_flags(infos)
def _init_feature_flags(self, infos): def _init_feature_flags(self, _):
"""Fix features selection depending to infos""" """Fix features selection depending to infos"""
is_empty: bool = not bool(infos) is_central_config = (
self._infos.get(CONF_THERMOSTAT_TYPE) == CONF_THERMOSTAT_CENTRAL_CONFIG
)
self._infos[CONF_USE_WINDOW_FEATURE] = ( self._infos[CONF_USE_WINDOW_FEATURE] = (
is_empty self._infos.get(CONF_USE_WINDOW_CENTRAL_CONFIG)
or self._infos.get(CONF_WINDOW_SENSOR) is not None or self._infos.get(CONF_WINDOW_SENSOR) is not None
or self._infos.get(CONF_WINDOW_AUTO_OPEN_THRESHOLD) is not None or self._infos.get(CONF_WINDOW_AUTO_OPEN_THRESHOLD) is not None
) )
self._infos[CONF_USE_MOTION_FEATURE] = ( self._infos[CONF_USE_MOTION_FEATURE] = self._infos.get(
is_empty or self._infos.get(CONF_MOTION_SENSOR) is not None CONF_USE_MOTION_FEATURE
) ) and (self._infos.get(CONF_MOTION_SENSOR) is not None or is_central_config)
self._infos[CONF_USE_POWER_FEATURE] = is_empty or (
self._infos[CONF_USE_POWER_FEATURE] = self._infos.get(
CONF_USE_POWER_CENTRAL_CONFIG
) or (
self._infos.get(CONF_POWER_SENSOR) is not None self._infos.get(CONF_POWER_SENSOR) is not None
and self._infos.get(CONF_MAX_POWER_SENSOR) is not None and self._infos.get(CONF_MAX_POWER_SENSOR) is not None
) )
self._infos[CONF_USE_PRESENCE_FEATURE] = ( self._infos[CONF_USE_PRESENCE_FEATURE] = (
is_empty or self._infos.get(CONF_PRESENCE_SENSOR) is not None self._infos.get(CONF_USE_PRESENCE_CENTRAL_CONFIG)
or self._infos.get(CONF_PRESENCE_SENSOR) is not None
)
self._infos[CONF_USE_CENTRAL_BOILER_FEATURE] = (
self._infos.get(CONF_CENTRAL_BOILER_ACTIVATION_SRV) is not None
and self._infos.get(CONF_CENTRAL_BOILER_DEACTIVATION_SRV) is not None
) )
def _init_central_config_flags(self, infos): def _init_central_config_flags(self, infos):
@@ -127,7 +142,10 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
CONF_USE_ADVANCED_CENTRAL_CONFIG, CONF_USE_ADVANCED_CENTRAL_CONFIG,
): ):
if not is_empty: if not is_empty:
self._infos[config] = self._infos.get(config) is True current_config = self._infos.get(config, None)
self._infos[config] = current_config is True or (
current_config is None and self._central_config is not None
)
else: else:
self._infos[config] = self._central_config is not None self._infos[config] = self._central_config is not None
@@ -191,6 +209,111 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
) )
raise NoCentralConfig(conf) raise NoCentralConfig(conf)
# Check the service for central boiler format
if self._infos.get(CONF_USE_CENTRAL_BOILER_FEATURE):
for conf in [
CONF_CENTRAL_BOILER_ACTIVATION_SRV,
CONF_CENTRAL_BOILER_DEACTIVATION_SRV,
]:
try:
check_and_extract_service_configuration(data.get(conf))
except ServiceConfigurationError as err:
raise ServiceConfigurationError(conf) from err
def check_config_complete(self, infos) -> bool:
"""True if the config is now complete (ie all mandatory attributes are set)"""
is_central_config = (
infos.get(CONF_THERMOSTAT_TYPE) == CONF_THERMOSTAT_CENTRAL_CONFIG
)
if is_central_config:
if (
infos.get(CONF_NAME) is None
or infos.get(CONF_EXTERNAL_TEMP_SENSOR) is None
):
return False
if infos.get(CONF_USE_POWER_FEATURE, False) is True and (
infos.get(CONF_POWER_SENSOR, None) is None
or infos.get(CONF_MAX_POWER_SENSOR, None) is None
):
return False
if (
infos.get(CONF_USE_PRESENCE_FEATURE, False) is True
and infos.get(CONF_PRESENCE_SENSOR, None) is None
):
return False
if self._infos[CONF_USE_CENTRAL_BOILER_FEATURE] and (
not self._infos.get(CONF_CENTRAL_BOILER_ACTIVATION_SRV, False)
or len(self._infos.get(CONF_CENTRAL_BOILER_ACTIVATION_SRV)) <= 0
or not self._infos.get(CONF_CENTRAL_BOILER_DEACTIVATION_SRV, False)
or len(self._infos.get(CONF_CENTRAL_BOILER_DEACTIVATION_SRV)) <= 0
):
return False
else:
if (
infos.get(CONF_NAME) is None
or infos.get(CONF_TEMP_SENSOR) is None
or infos.get(CONF_CYCLE_MIN) is None
):
return False
if (
infos.get(CONF_USE_MAIN_CENTRAL_CONFIG, False) is False
and infos.get(CONF_EXTERNAL_TEMP_SENSOR) is None
):
return False
if (
infos.get(CONF_THERMOSTAT_TYPE) == CONF_THERMOSTAT_SWITCH
and infos.get(CONF_HEATER, None) is None
):
return False
if (
infos.get(CONF_THERMOSTAT_TYPE) == CONF_THERMOSTAT_CLIMATE
and infos.get(CONF_CLIMATE, None) is None
):
return False
if (
infos.get(CONF_THERMOSTAT_TYPE) == CONF_THERMOSTAT_VALVE
and infos.get(CONF_VALVE, None) is None
):
return False
if (
infos.get(CONF_USE_MOTION_FEATURE, False) is True
and infos.get(CONF_MOTION_SENSOR, None) is None
):
return False
if (
infos.get(CONF_USE_POWER_FEATURE, False) is True
and infos.get(CONF_USE_POWER_CENTRAL_CONFIG, False) is False
and (
infos.get(CONF_POWER_SENSOR, None) is None
or infos.get(CONF_MAX_POWER_SENSOR, None) is None
)
):
return False
if (
infos.get(CONF_USE_PRESENCE_FEATURE, False) is True
and infos.get(CONF_USE_PRESENCE_CENTRAL_CONFIG, False) is False
and infos.get(CONF_PRESENCE_SENSOR, None) is None
):
return False
if (
infos.get(CONF_USE_ADVANCED_CENTRAL_CONFIG, False) is False
and infos.get(CONF_MINIMAL_ACTIVATION_DELAY, -1) == -1
):
return False
return True
def merge_user_input(self, data_schema: vol.Schema, user_input: dict): def merge_user_input(self, data_schema: vol.Schema, user_input: dict):
"""For each schema entry not in user_input, set or remove values in infos""" """For each schema entry not in user_input, set or remove values in infos"""
self._infos.update(user_input) self._infos.update(user_input)
@@ -225,11 +348,17 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
errors[str(err)] = "window_open_detection_method" errors[str(err)] = "window_open_detection_method"
except NoCentralConfig as err: except NoCentralConfig as err:
errors[str(err)] = "no_central_config" errors[str(err)] = "no_central_config"
except ServiceConfigurationError as err:
errors[str(err)] = "service_configuration_format"
except ConfigurationNotCompleteError as err:
errors["base"] = "configuration_not_complete"
except Exception: # pylint: disable=broad-except except Exception: # pylint: disable=broad-except
_LOGGER.exception("Unexpected exception") _LOGGER.exception("Unexpected exception")
errors["base"] = "unknown" errors["base"] = "unknown"
else: else:
self.merge_user_input(data_schema, user_input) self.merge_user_input(data_schema, user_input)
# Add default values for central config flags
self._init_central_config_flags(self._infos)
_LOGGER.debug("_info is now: %s", self._infos) _LOGGER.debug("_info is now: %s", self._infos)
return await next_step_function() return await next_step_function()
@@ -250,27 +379,92 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
_LOGGER.debug("Into ConfigFlow.async_step_user user_input=%s", user_input) _LOGGER.debug("Into ConfigFlow.async_step_user user_input=%s", user_input)
return await self.generic_step( return await self.generic_step(
"user", STEP_USER_DATA_SCHEMA, user_input, self.async_step_main "user", STEP_USER_DATA_SCHEMA, user_input, self.async_step_menu
)
async def async_step_configuration_not_complete(
self, user_input: dict | None = None
) -> FlowResult:
"""A fake step to handle the incomplete configuration flow"""
return await self.async_step_menu(user_input)
async def async_step_menu(self, user_input: dict | None = None) -> FlowResult:
"""Handle the flow steps"""
_LOGGER.debug("Into ConfigFlow.async_step_menu user_input=%s", user_input)
is_central_config = (
self._infos[CONF_THERMOSTAT_TYPE] == CONF_THERMOSTAT_CENTRAL_CONFIG
)
menu_options = ["main", "features"]
if not is_central_config:
menu_options.append("type")
if (
self._infos.get(CONF_PROP_FUNCTION) == PROPORTIONAL_FUNCTION_TPI
or is_central_config
):
menu_options.append("tpi")
if self._infos[CONF_THERMOSTAT_TYPE] in [
CONF_THERMOSTAT_SWITCH,
CONF_THERMOSTAT_VALVE,
CONF_THERMOSTAT_CLIMATE,
]:
menu_options.append("presets")
if (
is_central_config
and self._infos.get(CONF_USE_CENTRAL_BOILER_FEATURE) is True
):
menu_options.append("central_boiler")
if self._infos[CONF_USE_WINDOW_FEATURE] is True:
menu_options.append("window")
if self._infos[CONF_USE_MOTION_FEATURE] is True:
menu_options.append("motion")
if self._infos[CONF_USE_POWER_FEATURE] is True:
menu_options.append("power")
if self._infos[CONF_USE_PRESENCE_FEATURE] is True:
menu_options.append("presence")
menu_options.append("advanced")
if self.check_config_complete(self._infos):
menu_options.append("finalize")
else:
_LOGGER.info("The configuration is not terminated")
menu_options.append("configuration_not_complete")
return self.async_show_menu(
step_id="menu",
menu_options=menu_options,
description_placeholders=self._placeholders,
) )
async def async_step_main(self, user_input: dict | None = None) -> FlowResult: async def async_step_main(self, user_input: dict | None = None) -> FlowResult:
"""Handle the flow steps""" """Handle the flow steps"""
_LOGGER.debug("Into ConfigFlow.async_step_main user_input=%s", user_input) _LOGGER.debug("Into ConfigFlow.async_step_main user_input=%s", user_input)
schema = STEP_MAIN_DATA_SCHEMA next_step = self.async_step_menu
next_step = self.async_step_type
if self._infos[CONF_THERMOSTAT_TYPE] == CONF_THERMOSTAT_CENTRAL_CONFIG: if self._infos[CONF_THERMOSTAT_TYPE] == CONF_THERMOSTAT_CENTRAL_CONFIG:
self._infos[CONF_NAME] = CENTRAL_CONFIG_NAME self._infos[CONF_NAME] = CENTRAL_CONFIG_NAME
schema = STEP_CENTRAL_MAIN_DATA_SCHEMA schema = STEP_CENTRAL_MAIN_DATA_SCHEMA
next_step = self.async_step_tpi else:
elif user_input and user_input.get(CONF_USE_MAIN_CENTRAL_CONFIG) is False:
next_step = self.async_step_spec_main
schema = STEP_MAIN_DATA_SCHEMA schema = STEP_MAIN_DATA_SCHEMA
# If we come from async_step_spec_main
elif self._infos.get(COMES_FROM) == "async_step_spec_main": if (
next_step = self.async_step_type user_input
schema = STEP_CENTRAL_MAIN_DATA_SCHEMA and user_input.get(CONF_USE_MAIN_CENTRAL_CONFIG, False) is False
):
if user_input and self._infos.get(COMES_FROM) == "async_step_spec_main":
schema = STEP_CENTRAL_MAIN_DATA_SCHEMA
del self._infos[COMES_FROM]
else:
next_step = self.async_step_spec_main
return await self.generic_step("main", schema, user_input, next_step) return await self.generic_step("main", schema, user_input, next_step)
@@ -278,50 +472,84 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
"""Handle the specific main flow steps""" """Handle the specific main flow steps"""
_LOGGER.debug("Into ConfigFlow.async_step_spec_main user_input=%s", user_input) _LOGGER.debug("Into ConfigFlow.async_step_spec_main user_input=%s", user_input)
schema = STEP_CENTRAL_MAIN_DATA_SCHEMA if self._infos[CONF_THERMOSTAT_TYPE] == CONF_THERMOSTAT_CENTRAL_CONFIG:
next_step = self.async_step_type schema = STEP_CENTRAL_MAIN_DATA_SCHEMA
else:
schema = STEP_CENTRAL_SPEC_MAIN_DATA_SCHEMA
next_step = self.async_step_menu
self._infos[COMES_FROM] = "async_step_spec_main" self._infos[COMES_FROM] = "async_step_spec_main"
# This will return to async_step_main (to keep the "main" step) # This will return to async_step_main (to keep the "main" step)
return await self.generic_step("main", schema, user_input, next_step) return await self.generic_step("main", schema, user_input, next_step)
async def async_step_central_boiler(
self, user_input: dict | None = None
) -> FlowResult:
"""Handle the central boiler flow steps"""
_LOGGER.debug(
"Into ConfigFlow.async_step_central_boiler user_input=%s", user_input
)
schema = STEP_CENTRAL_BOILER_SCHEMA
next_step = self.async_step_menu
return await self.generic_step("central_boiler", schema, user_input, next_step)
async def async_step_type(self, user_input: dict | None = None) -> FlowResult: async def async_step_type(self, user_input: dict | None = None) -> FlowResult:
"""Handle the Type flow steps""" """Handle the Type flow steps"""
_LOGGER.debug("Into ConfigFlow.async_step_type user_input=%s", user_input) _LOGGER.debug("Into ConfigFlow.async_step_type user_input=%s", user_input)
if self._infos[CONF_THERMOSTAT_TYPE] == CONF_THERMOSTAT_SWITCH: if self._infos[CONF_THERMOSTAT_TYPE] == CONF_THERMOSTAT_SWITCH:
return await self.generic_step( return await self.generic_step(
"type", STEP_THERMOSTAT_SWITCH, user_input, self.async_step_tpi "type", STEP_THERMOSTAT_SWITCH, user_input, self.async_step_menu
) )
elif self._infos[CONF_THERMOSTAT_TYPE] == CONF_THERMOSTAT_VALVE: elif self._infos[CONF_THERMOSTAT_TYPE] == CONF_THERMOSTAT_VALVE:
return await self.generic_step( return await self.generic_step(
"type", STEP_THERMOSTAT_VALVE, user_input, self.async_step_tpi "type", STEP_THERMOSTAT_VALVE, user_input, self.async_step_menu
) )
else: else:
return await self.generic_step( return await self.generic_step(
"type", "type",
STEP_THERMOSTAT_CLIMATE, STEP_THERMOSTAT_CLIMATE,
user_input, user_input,
self.async_step_presets, self.async_step_menu,
) )
async def async_step_features(self, user_input: dict | None = None) -> FlowResult:
"""Handle the Type flow steps"""
_LOGGER.debug("Into ConfigFlow.async_step_features user_input=%s", user_input)
return await self.generic_step(
"features",
(
STEP_CENTRAL_FEATURES_DATA_SCHEMA
if self._infos[CONF_THERMOSTAT_TYPE] == CONF_THERMOSTAT_CENTRAL_CONFIG
else STEP_FEATURES_DATA_SCHEMA
),
user_input,
self.async_step_menu,
)
async def async_step_tpi(self, user_input: dict | None = None) -> FlowResult: async def async_step_tpi(self, user_input: dict | None = None) -> FlowResult:
"""Handle the TPI flow steps""" """Handle the TPI flow steps"""
_LOGGER.debug("Into ConfigFlow.async_step_tpi user_input=%s", user_input) _LOGGER.debug("Into ConfigFlow.async_step_tpi user_input=%s", user_input)
schema = STEP_TPI_DATA_SCHEMA next_step = self.async_step_menu
next_step = (
self.async_step_spec_tpi
if user_input and user_input.get(CONF_USE_TPI_CENTRAL_CONFIG) is False
else self.async_step_presets
)
if self._infos[CONF_THERMOSTAT_TYPE] == CONF_THERMOSTAT_CENTRAL_CONFIG: if self._infos[CONF_THERMOSTAT_TYPE] == CONF_THERMOSTAT_CENTRAL_CONFIG:
schema = STEP_CENTRAL_TPI_DATA_SCHEMA schema = STEP_CENTRAL_TPI_DATA_SCHEMA
next_step = self.async_step_presets else:
elif self._infos.get(COMES_FROM) == "async_step_spec_tpi": schema = STEP_TPI_DATA_SCHEMA
schema = STEP_CENTRAL_TPI_DATA_SCHEMA
if (
user_input
and user_input.get(CONF_USE_TPI_CENTRAL_CONFIG, False) is False
):
if user_input and self._infos.get(COMES_FROM) == "async_step_spec_tpi":
schema = STEP_CENTRAL_TPI_DATA_SCHEMA
del self._infos[COMES_FROM]
else:
next_step = self.async_step_spec_tpi
return await self.generic_step("tpi", schema, user_input, next_step) return await self.generic_step("tpi", schema, user_input, next_step)
@@ -331,7 +559,7 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
schema = STEP_CENTRAL_TPI_DATA_SCHEMA schema = STEP_CENTRAL_TPI_DATA_SCHEMA
self._infos[COMES_FROM] = "async_step_spec_tpi" self._infos[COMES_FROM] = "async_step_spec_tpi"
next_step = self.async_step_presets next_step = self.async_step_menu
return await self.generic_step("tpi", schema, user_input, next_step) return await self.generic_step("tpi", schema, user_input, next_step)
@@ -339,82 +567,41 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
"""Handle the presets flow steps""" """Handle the presets flow steps"""
_LOGGER.debug("Into ConfigFlow.async_step_presets user_input=%s", user_input) _LOGGER.debug("Into ConfigFlow.async_step_presets user_input=%s", user_input)
if self._infos.get(CONF_AC_MODE) is True: next_step = self.async_step_menu # advanced
schema_ac_or_not = STEP_CENTRAL_PRESETS_WITH_AC_DATA_SCHEMA
else:
schema_ac_or_not = STEP_CENTRAL_PRESETS_DATA_SCHEMA
next_step = self.async_step_advanced
schema = STEP_PRESETS_DATA_SCHEMA schema = STEP_PRESETS_DATA_SCHEMA
if self._infos[CONF_USE_WINDOW_FEATURE]:
next_step = self.async_step_window
elif self._infos[CONF_USE_MOTION_FEATURE]:
next_step = self.async_step_motion
elif self._infos[CONF_USE_POWER_FEATURE]:
next_step = self.async_step_power
elif self._infos[CONF_USE_PRESENCE_FEATURE]:
next_step = self.async_step_presence
# In Central config -> display the presets_with_ac and goto windows # In Central config -> display the next step immedialty
if self._infos[CONF_THERMOSTAT_TYPE] == CONF_THERMOSTAT_CENTRAL_CONFIG: if self._infos[CONF_THERMOSTAT_TYPE] == CONF_THERMOSTAT_CENTRAL_CONFIG:
schema = STEP_CENTRAL_PRESETS_WITH_AC_DATA_SCHEMA # Call directly the next step, we have nothing to display here
next_step = self.async_step_window return await self.async_step_window() # = self.async_step_window
# If comes from async_step_spec_presets
elif self._infos.get(COMES_FROM) == "async_step_spec_presets":
schema = schema_ac_or_not
elif user_input and user_input.get(CONF_USE_PRESETS_CENTRAL_CONFIG) is False:
next_step = self.async_step_spec_presets
schema = STEP_PRESETS_DATA_SCHEMA
return await self.generic_step("presets", schema, user_input, next_step) return await self.generic_step("presets", schema, user_input, next_step)
async def async_step_spec_presets(
self, user_input: dict | None = None
) -> FlowResult:
"""Handle the specific presets flow steps"""
_LOGGER.debug(
"Into ConfigFlow.async_step_spec_presets user_input=%s", user_input
)
if self._infos.get(CONF_AC_MODE) is True:
schema = STEP_CENTRAL_PRESETS_WITH_AC_DATA_SCHEMA
else:
schema = STEP_CENTRAL_PRESETS_DATA_SCHEMA
self._infos[COMES_FROM] = "async_step_spec_presets"
next_step = self.async_step_window
# This will return to async_step_main (to keep the "main" step)
return await self.generic_step("presets", schema, user_input, next_step)
async def async_step_window(self, user_input: dict | None = None) -> FlowResult: async def async_step_window(self, user_input: dict | None = None) -> FlowResult:
"""Handle the window sensor flow steps""" """Handle the window sensor flow steps"""
_LOGGER.debug("Into ConfigFlow.async_step_window user_input=%s", user_input) _LOGGER.debug("Into ConfigFlow.async_step_window user_input=%s", user_input)
schema = STEP_WINDOW_DATA_SCHEMA next_step = self.async_step_menu
next_step = self.async_step_advanced
if self._infos[CONF_USE_MOTION_FEATURE]:
next_step = self.async_step_motion
elif self._infos[CONF_USE_POWER_FEATURE]:
next_step = self.async_step_power
elif self._infos[CONF_USE_PRESENCE_FEATURE]:
next_step = self.async_step_presence
# In Central config -> display the presets_with_ac and goto windows
if self._infos[CONF_THERMOSTAT_TYPE] == CONF_THERMOSTAT_CENTRAL_CONFIG: if self._infos[CONF_THERMOSTAT_TYPE] == CONF_THERMOSTAT_CENTRAL_CONFIG:
schema = STEP_CENTRAL_WINDOW_DATA_SCHEMA schema = STEP_CENTRAL_WINDOW_DATA_SCHEMA
next_step = self.async_step_motion else:
# If comes from async_step_spec_window schema = STEP_WINDOW_DATA_SCHEMA
elif self._infos.get(COMES_FROM) == "async_step_spec_window":
# If we have a window sensor don't display the auto window parameters if (
if self._infos.get(CONF_WINDOW_SENSOR) is not None: user_input
schema = STEP_CENTRAL_WINDOW_WO_AUTO_DATA_SCHEMA and user_input.get(CONF_USE_WINDOW_CENTRAL_CONFIG, False) is False
else: ):
schema = STEP_CENTRAL_WINDOW_DATA_SCHEMA if (
elif user_input and user_input.get(CONF_USE_WINDOW_CENTRAL_CONFIG) is False: user_input
next_step = self.async_step_spec_window and self._infos.get(COMES_FROM) == "async_step_spec_window"
):
if self._infos.get(CONF_WINDOW_SENSOR) is not None:
schema = STEP_CENTRAL_WINDOW_WO_AUTO_DATA_SCHEMA
else:
schema = STEP_CENTRAL_WINDOW_DATA_SCHEMA
del self._infos[COMES_FROM]
else:
next_step = self.async_step_spec_window
return await self.generic_step("window", schema, user_input, next_step) return await self.generic_step("window", schema, user_input, next_step)
@@ -441,23 +628,24 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
"""Handle the window and motion sensor flow steps""" """Handle the window and motion sensor flow steps"""
_LOGGER.debug("Into ConfigFlow.async_step_motion user_input=%s", user_input) _LOGGER.debug("Into ConfigFlow.async_step_motion user_input=%s", user_input)
schema = STEP_MOTION_DATA_SCHEMA next_step = self.async_step_menu
next_step = self.async_step_advanced
if self._infos[CONF_USE_POWER_FEATURE]:
next_step = self.async_step_power
elif self._infos[CONF_USE_PRESENCE_FEATURE]:
next_step = self.async_step_presence
# In Central config -> display the presets_with_ac and goto windows
if self._infos[CONF_THERMOSTAT_TYPE] == CONF_THERMOSTAT_CENTRAL_CONFIG: if self._infos[CONF_THERMOSTAT_TYPE] == CONF_THERMOSTAT_CENTRAL_CONFIG:
schema = STEP_CENTRAL_MOTION_DATA_SCHEMA schema = STEP_CENTRAL_MOTION_DATA_SCHEMA
next_step = self.async_step_power else:
# If comes from async_step_spec_motion schema = STEP_MOTION_DATA_SCHEMA
elif self._infos.get(COMES_FROM) == "async_step_spec_motion":
schema = STEP_CENTRAL_MOTION_DATA_SCHEMA if (
elif user_input and user_input.get(CONF_USE_MOTION_CENTRAL_CONFIG) is False: user_input
next_step = self.async_step_spec_motion and user_input.get(CONF_USE_MOTION_CENTRAL_CONFIG, False) is False
):
if (
user_input
and self._infos.get(COMES_FROM) == "async_step_spec_motion"
):
schema = STEP_CENTRAL_MOTION_DATA_SCHEMA
del self._infos[COMES_FROM]
else:
next_step = self.async_step_spec_motion
return await self.generic_step("motion", schema, user_input, next_step) return await self.generic_step("motion", schema, user_input, next_step)
@@ -473,7 +661,7 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
self._infos[COMES_FROM] = "async_step_spec_motion" self._infos[COMES_FROM] = "async_step_spec_motion"
next_step = self.async_step_power next_step = self.async_step_menu
# This will return to async_step_main (to keep the "main" step) # This will return to async_step_main (to keep the "main" step)
return await self.generic_step("motion", schema, user_input, next_step) return await self.generic_step("motion", schema, user_input, next_step)
@@ -482,21 +670,24 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
"""Handle the power management flow steps""" """Handle the power management flow steps"""
_LOGGER.debug("Into ConfigFlow.async_step_power user_input=%s", user_input) _LOGGER.debug("Into ConfigFlow.async_step_power user_input=%s", user_input)
schema = STEP_POWER_DATA_SCHEMA next_step = self.async_step_menu
next_step = self.async_step_advanced
if self._infos[CONF_USE_PRESENCE_FEATURE]:
next_step = self.async_step_presence
# In Central config -> display the presets_with_ac and goto windows
if self._infos[CONF_THERMOSTAT_TYPE] == CONF_THERMOSTAT_CENTRAL_CONFIG: if self._infos[CONF_THERMOSTAT_TYPE] == CONF_THERMOSTAT_CENTRAL_CONFIG:
schema = STEP_CENTRAL_POWER_DATA_SCHEMA schema = STEP_CENTRAL_POWER_DATA_SCHEMA
next_step = self.async_step_presence else:
# If comes from async_step_spec_motion schema = STEP_POWER_DATA_SCHEMA
elif self._infos.get(COMES_FROM) == "async_step_spec_power":
schema = STEP_CENTRAL_POWER_DATA_SCHEMA if (
elif user_input and user_input.get(CONF_USE_POWER_CENTRAL_CONFIG) is False: user_input
next_step = self.async_step_spec_power and user_input.get(CONF_USE_POWER_CENTRAL_CONFIG, False) is False
):
if (
user_input
and self._infos.get(COMES_FROM) == "async_step_spec_power"
):
schema = STEP_CENTRAL_POWER_DATA_SCHEMA
del self._infos[COMES_FROM]
else:
next_step = self.async_step_spec_power
return await self.generic_step("power", schema, user_input, next_step) return await self.generic_step("power", schema, user_input, next_step)
@@ -508,7 +699,7 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
self._infos[COMES_FROM] = "async_step_spec_power" self._infos[COMES_FROM] = "async_step_spec_power"
next_step = self.async_step_presence next_step = self.async_step_menu
# This will return to async_step_power (to keep the "power" step) # This will return to async_step_power (to keep the "power" step)
return await self.generic_step("power", schema, user_input, next_step) return await self.generic_step("power", schema, user_input, next_step)
@@ -517,25 +708,31 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
"""Handle the presence management flow steps""" """Handle the presence management flow steps"""
_LOGGER.debug("Into ConfigFlow.async_step_presence user_input=%s", user_input) _LOGGER.debug("Into ConfigFlow.async_step_presence user_input=%s", user_input)
schema = STEP_PRESENCE_DATA_SCHEMA next_step = self.async_step_menu
next_step = self.async_step_advanced
# In Central config -> display the presets_with_ac and goto windows
if self._infos[CONF_THERMOSTAT_TYPE] == CONF_THERMOSTAT_CENTRAL_CONFIG: if self._infos[CONF_THERMOSTAT_TYPE] == CONF_THERMOSTAT_CENTRAL_CONFIG:
schema = STEP_CENTRAL_PRESENCE_DATA_SCHEMA schema = STEP_CENTRAL_PRESENCE_DATA_SCHEMA
next_step = self.async_step_advanced else:
# If comes from async_step_spec_presence schema = STEP_PRESENCE_DATA_SCHEMA
elif self._infos.get(COMES_FROM) == "async_step_spec_presence":
schema = STEP_CENTRAL_PRESENCE_DATA_SCHEMA if (
elif user_input and user_input.get(CONF_USE_PRESENCE_CENTRAL_CONFIG) is False: user_input
next_step = self.async_step_spec_presence and user_input.get(CONF_USE_PRESENCE_CENTRAL_CONFIG, False) is False
):
if (
user_input
and self._infos.get(COMES_FROM) == "async_step_spec_presence"
):
schema = STEP_CENTRAL_PRESENCE_DATA_SCHEMA
del self._infos[COMES_FROM]
else:
next_step = self.async_step_spec_presence
return await self.generic_step("presence", schema, user_input, next_step) return await self.generic_step("presence", schema, user_input, next_step)
async def async_step_spec_presence( async def async_step_spec_presence(
self, user_input: dict | None = None self, user_input: dict | None = None
) -> FlowResult: ) -> FlowResult:
"""Handle the specific preseence flow steps""" """Handle the specific power flow steps"""
_LOGGER.debug( _LOGGER.debug(
"Into ConfigFlow.async_step_spec_presence user_input=%s", user_input "Into ConfigFlow.async_step_spec_presence user_input=%s", user_input
) )
@@ -544,26 +741,33 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
self._infos[COMES_FROM] = "async_step_spec_presence" self._infos[COMES_FROM] = "async_step_spec_presence"
next_step = self.async_step_advanced next_step = self.async_step_menu
# This will return to async_step_presence (to keep the "presence" step) # This will return to async_step_power (to keep the "power" step)
return await self.generic_step("presence", schema, user_input, next_step) return await self.generic_step("presence", schema, user_input, next_step)
async def async_step_advanced(self, user_input: dict | None = None) -> FlowResult: async def async_step_advanced(self, user_input: dict | None = None) -> FlowResult:
"""Handle the advanced parameter flow steps""" """Handle the advanced parameter flow steps"""
_LOGGER.debug("Into ConfigFlow.async_step_advanced user_input=%s", user_input) _LOGGER.debug("Into ConfigFlow.async_step_advanced user_input=%s", user_input)
schema = STEP_ADVANCED_DATA_SCHEMA next_step = self.async_step_menu
next_step = self.async_finalize
# In Central config -> display the presets_with_ac and goto windows
if self._infos[CONF_THERMOSTAT_TYPE] == CONF_THERMOSTAT_CENTRAL_CONFIG: if self._infos[CONF_THERMOSTAT_TYPE] == CONF_THERMOSTAT_CENTRAL_CONFIG:
schema = STEP_CENTRAL_ADVANCED_DATA_SCHEMA schema = STEP_CENTRAL_ADVANCED_DATA_SCHEMA
# If comes from async_step_spec_presence else:
elif self._infos.get(COMES_FROM) == "async_step_spec_advanced": schema = STEP_ADVANCED_DATA_SCHEMA
schema = STEP_CENTRAL_ADVANCED_DATA_SCHEMA
elif user_input and user_input.get(CONF_USE_ADVANCED_CENTRAL_CONFIG) is False: if (
next_step = self.async_step_spec_advanced user_input
and user_input.get(CONF_USE_ADVANCED_CENTRAL_CONFIG, False) is False
):
if (
user_input
and self._infos.get(COMES_FROM) == "async_step_spec_advanced"
):
schema = STEP_CENTRAL_ADVANCED_DATA_SCHEMA
del self._infos[COMES_FROM]
else:
next_step = self.async_step_spec_advanced
return await self.generic_step("advanced", schema, user_input, next_step) return await self.generic_step("advanced", schema, user_input, next_step)
@@ -584,22 +788,12 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
# This will return to async_step_presence (to keep the "presence" step) # This will return to async_step_presence (to keep the "presence" step)
return await self.generic_step("advanced", schema, user_input, next_step) return await self.generic_step("advanced", schema, user_input, next_step)
async def async_finalize(self): async def async_step_finalize(self, _):
"""Should be implemented by Leaf classes""" """Should be implemented by Leaf classes"""
raise HomeAssistantError( raise HomeAssistantError(
"async_finalize not implemented on VersatileThermostat sub-class" "async_finalize not implemented on VersatileThermostat sub-class"
) )
# Not used but can be useful in the future
# def find_all_climates(self) -> list(str):
# """Find all climate known by HA"""
# component: EntityComponent[ClimateEntity] = self.hass.data[CLIMATE_DOMAIN]
# ret: list(str) = list()
# for entity in component.entities:
# ret.append(entity.entity_id)
# _LOGGER.debug("Found all climate entities: %s", ret)
# return ret
class VersatileThermostatConfigFlow( class VersatileThermostatConfigFlow(
VersatileThermostatBaseConfigFlow, HAConfigFlow, domain=DOMAIN VersatileThermostatBaseConfigFlow, HAConfigFlow, domain=DOMAIN
@@ -617,7 +811,7 @@ class VersatileThermostatConfigFlow(
"""Get options flow for this handler""" """Get options flow for this handler"""
return VersatileThermostatOptionsFlowHandler(config_entry) return VersatileThermostatOptionsFlowHandler(config_entry)
async def async_finalize(self): async def async_step_finalize(self, _):
"""Finalization of the ConfigEntry creation""" """Finalization of the ConfigEntry creation"""
_LOGGER.debug("ConfigFlow.async_finalize") _LOGGER.debug("ConfigFlow.async_finalize")
# Removes temporary value # Removes temporary value
@@ -652,155 +846,9 @@ class VersatileThermostatOptionsFlowHandler(
CONF_NAME: self._infos[CONF_NAME], CONF_NAME: self._infos[CONF_NAME],
} }
return await self.async_step_main(user_input) return await self.async_step_menu(user_input)
# async def async_step_main(self, user_input: dict | None = None) -> FlowResult: async def async_step_finalize(self, _):
# """Handle the flow steps"""
# _LOGGER.debug(
# "Into OptionsFlowHandler.async_step_user user_input=%s", user_input
# )
# return await self.generic_step(
# "user", STEP_USER_DATA_SCHEMA, user_input, self.async_step_type
# )
# async def async_step_type(self, user_input: dict | None = None) -> FlowResult:
# """Handle the flow steps"""
# _LOGGER.debug(
# "Into OptionsFlowHandler.async_step_user user_input=%s", user_input
# )
# if self._infos[CONF_THERMOSTAT_TYPE] == CONF_THERMOSTAT_SWITCH:
# return await self.generic_step(
# "type", STEP_THERMOSTAT_SWITCH, user_input, self.async_step_tpi
# )
# elif self._infos[CONF_THERMOSTAT_TYPE] == CONF_THERMOSTAT_VALVE:
# return await self.generic_step(
# "type", STEP_THERMOSTAT_VALVE, user_input, self.async_step_tpi
# )
# else:
# return await self.generic_step(
# "type",
# STEP_THERMOSTAT_CLIMATE,
# user_input,
# self.async_step_presets,
# )
# async def async_step_tpi(self, user_input: dict | None = None) -> FlowResult:
# """Handle the tpi flow steps"""
# _LOGGER.debug(
# "Into OptionsFlowHandler.async_step_tpi user_input=%s", user_input
# )
# return await self.generic_step(
# "tpi", STEP_TPI_DATA_SCHEMA, user_input, self.async_step_presets
# )
# async def async_step_presets(self, user_input: dict | None = None) -> FlowResult:
# """Handle the presets flow steps"""
# _LOGGER.debug(
# "Into OptionsFlowHandler.async_step_presets user_input=%s", user_input
# )
# next_step = self.async_step_advanced
# if self._infos[CONF_USE_WINDOW_FEATURE]:
# next_step = self.async_step_window
# elif self._infos[CONF_USE_MOTION_FEATURE]:
# next_step = self.async_step_motion
# elif self._infos[CONF_USE_POWER_FEATURE]:
# next_step = self.async_step_power
# elif self._infos[CONF_USE_PRESENCE_FEATURE]:
# next_step = self.async_step_presence
# if self._infos.get(CONF_AC_MODE) is True:
# schema = STEP_PRESETS_WITH_AC_DATA_SCHEMA
# else:
# schema = STEP_PRESETS_DATA_SCHEMA
# return await self.generic_step("presets", schema, user_input, next_step)
# async def async_step_window(self, user_input: dict | None = None) -> FlowResult:
# """Handle the window sensor flow steps"""
# _LOGGER.debug(
# "Into OptionsFlowHandler.async_step_window user_input=%s", user_input
# )
# next_step = self.async_step_advanced
# if self._infos[CONF_USE_MOTION_FEATURE]:
# next_step = self.async_step_motion
# elif self._infos[CONF_USE_POWER_FEATURE]:
# next_step = self.async_step_power
# elif self._infos[CONF_USE_PRESENCE_FEATURE]:
# next_step = self.async_step_presence
# return await self.generic_step(
# "window", STEP_WINDOW_DATA_SCHEMA, user_input, next_step
# )
# async def async_step_motion(self, user_input: dict | None = None) -> FlowResult:
# """Handle the window and motion sensor flow steps"""
# _LOGGER.debug(
# "Into OptionsFlowHandler.async_step_motion user_input=%s", user_input
# )
# next_step = self.async_step_advanced
# if self._infos[CONF_USE_POWER_FEATURE]:
# next_step = self.async_step_power
# elif self._infos[CONF_USE_PRESENCE_FEATURE]:
# next_step = self.async_step_presence
# return await self.generic_step(
# "motion", STEP_MOTION_DATA_SCHEMA, user_input, next_step
# )
# async def async_step_power(self, user_input: dict | None = None) -> FlowResult:
# """Handle the power management flow steps"""
# _LOGGER.debug(
# "Into OptionsFlowHandler.async_step_power user_input=%s", user_input
# )
# next_step = self.async_step_advanced
# if self._infos[CONF_USE_PRESENCE_FEATURE]:
# next_step = self.async_step_presence
# return await self.generic_step(
# "power",
# STEP_POWER_DATA_SCHEMA,
# user_input,
# next_step,
# )
# async def async_step_presence(self, user_input: dict | None = None) -> FlowResult:
# """Handle the presence management flow steps"""
# _LOGGER.debug(
# "Into OptionsFlowHandler.async_step_presence user_input=%s", user_input
# )
# if self._infos.get(CONF_AC_MODE) is True:
# schema = STEP_PRESENCE_WITH_AC_DATA_SCHEMA
# else:
# schema = STEP_PRESENCE_DATA_SCHEMA
# return await self.generic_step(
# "presence",
# schema,
# user_input,
# self.async_step_advanced,
# )
# async def async_step_advanced(self, user_input: dict | None = None) -> FlowResult:
# """Handle the advanced flow steps"""
# _LOGGER.debug(
# "Into OptionsFlowHandler.async_step_advanced user_input=%s", user_input
# )
# return await self.generic_step(
# "advanced",
# STEP_ADVANCED_DATA_SCHEMA,
# user_input,
# self.async_end,
# )
async def async_finalize(self):
"""Finalization of the ConfigEntry creation""" """Finalization of the ConfigEntry creation"""
if not self._infos[CONF_USE_WINDOW_FEATURE]: if not self._infos[CONF_USE_WINDOW_FEATURE]:
self._infos[CONF_USE_WINDOW_CENTRAL_CONFIG] = False self._infos[CONF_USE_WINDOW_CENTRAL_CONFIG] = False
@@ -818,6 +866,9 @@ class VersatileThermostatOptionsFlowHandler(
if not self._infos[CONF_USE_PRESENCE_FEATURE]: if not self._infos[CONF_USE_PRESENCE_FEATURE]:
self._infos[CONF_USE_PRESENCE_CENTRAL_CONFIG] = False self._infos[CONF_USE_PRESENCE_CENTRAL_CONFIG] = False
self._infos[CONF_PRESENCE_SENSOR] = None self._infos[CONF_PRESENCE_SENSOR] = None
if not self._infos[CONF_USE_CENTRAL_BOILER_FEATURE]:
self._infos[CONF_CENTRAL_BOILER_ACTIVATION_SRV] = None
self._infos[CONF_CENTRAL_BOILER_DEACTIVATION_SRV] = None
_LOGGER.info( _LOGGER.info(
"Recreating entry %s due to configuration change. New config is now: %s", "Recreating entry %s due to configuration change. New config is now: %s",

View File

@@ -16,6 +16,10 @@ from homeassistant.components.input_number import (
DOMAIN as INPUT_NUMBER_DOMAIN, DOMAIN as INPUT_NUMBER_DOMAIN,
) )
from homeassistant.components.input_datetime import (
DOMAIN as INPUT_DATETIME_DOMAIN,
)
from homeassistant.components.person import DOMAIN as PERSON_DOMAIN from homeassistant.components.person import DOMAIN as PERSON_DOMAIN
from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN
@@ -28,7 +32,9 @@ STEP_USER_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name
CONF_THERMOSTAT_TYPE, default=CONF_THERMOSTAT_SWITCH CONF_THERMOSTAT_TYPE, default=CONF_THERMOSTAT_SWITCH
): selector.SelectSelector( ): selector.SelectSelector(
selector.SelectSelectorConfig( selector.SelectSelectorConfig(
options=CONF_THERMOSTAT_TYPES, translation_key="thermostat_type" options=CONF_THERMOSTAT_TYPES,
translation_key="thermostat_type",
mode="list",
) )
) )
} }
@@ -40,14 +46,35 @@ STEP_MAIN_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name
vol.Required(CONF_TEMP_SENSOR): selector.EntitySelector( vol.Required(CONF_TEMP_SENSOR): selector.EntitySelector(
selector.EntitySelectorConfig(domain=[SENSOR_DOMAIN, INPUT_NUMBER_DOMAIN]), 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.Required(CONF_CYCLE_MIN, default=5): cv.positive_int,
vol.Optional(CONF_DEVICE_POWER, default="1"): vol.Coerce(float), 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.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_WINDOW_FEATURE, default=False): cv.boolean,
vol.Optional(CONF_USE_MOTION_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_POWER_FEATURE, default=False): cv.boolean,
vol.Optional(CONF_USE_PRESENCE_FEATURE, default=False): cv.boolean, vol.Optional(CONF_USE_PRESENCE_FEATURE, default=False): cv.boolean,
vol.Required(CONF_USE_MAIN_CENTRAL_CONFIG, default=True): 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,
} }
) )
@@ -58,6 +85,25 @@ STEP_CENTRAL_MAIN_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name
), ),
vol.Required(CONF_TEMP_MIN, default=7): vol.Coerce(float), vol.Required(CONF_TEMP_MIN, default=7): vol.Coerce(float),
vol.Required(CONF_TEMP_MAX, default=35): 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,
} }
) )
@@ -75,6 +121,7 @@ STEP_THERMOSTAT_SWITCH = vol.Schema( # pylint: disable=invalid-name
vol.Optional(CONF_HEATER_4): selector.EntitySelector( vol.Optional(CONF_HEATER_4): selector.EntitySelector(
selector.EntitySelectorConfig(domain=[SWITCH_DOMAIN, INPUT_BOOLEAN_DOMAIN]), selector.EntitySelectorConfig(domain=[SWITCH_DOMAIN, INPUT_BOOLEAN_DOMAIN]),
), ),
vol.Optional(CONF_HEATER_KEEP_ALIVE): cv.positive_int,
vol.Required(CONF_PROP_FUNCTION, default=PROPORTIONAL_FUNCTION_TPI): vol.In( vol.Required(CONF_PROP_FUNCTION, default=PROPORTIONAL_FUNCTION_TPI): vol.In(
[ [
PROPORTIONAL_FUNCTION_TPI, PROPORTIONAL_FUNCTION_TPI,
@@ -106,6 +153,7 @@ STEP_THERMOSTAT_CLIMATE = vol.Schema( # pylint: disable=invalid-name
selector.SelectSelectorConfig( selector.SelectSelectorConfig(
options=CONF_AUTO_REGULATION_MODES, options=CONF_AUTO_REGULATION_MODES,
translation_key="auto_regulation_mode", translation_key="auto_regulation_mode",
mode="dropdown",
) )
), ),
vol.Optional(CONF_AUTO_REGULATION_DTEMP, default=0.5): vol.Coerce(float), vol.Optional(CONF_AUTO_REGULATION_DTEMP, default=0.5): vol.Coerce(float),
@@ -116,8 +164,10 @@ STEP_THERMOSTAT_CLIMATE = vol.Schema( # pylint: disable=invalid-name
selector.SelectSelectorConfig( selector.SelectSelectorConfig(
options=CONF_AUTO_FAN_MODES, options=CONF_AUTO_FAN_MODES,
translation_key="auto_fan_mode", translation_key="auto_fan_mode",
mode="dropdown",
) )
), ),
vol.Optional(CONF_AUTO_REGULATION_USE_DEVICE_TEMP, default=False): cv.boolean,
} }
) )
@@ -141,6 +191,8 @@ STEP_THERMOSTAT_VALVE = vol.Schema( # pylint: disable=invalid-name
] ]
), ),
vol.Optional(CONF_AC_MODE, default=False): cv.boolean, 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,
} }
) )
@@ -163,18 +215,6 @@ STEP_PRESETS_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name
} }
) )
STEP_CENTRAL_PRESETS_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name
{vol.Optional(v, default=0): vol.Coerce(float) for (k, v) in CONF_PRESETS.items()}
)
STEP_CENTRAL_PRESETS_WITH_AC_DATA_SCHEMA = (
vol.Schema( # pylint: disable=invalid-name # pylint: disable=invalid-name
{
vol.Optional(v, default=0): vol.Coerce(float)
for (k, v) in CONF_PRESETS_WITH_AC.items()
}
)
)
STEP_WINDOW_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name STEP_WINDOW_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name
{ {
@@ -193,18 +233,36 @@ STEP_CENTRAL_WINDOW_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name
vol.Optional(CONF_WINDOW_AUTO_OPEN_THRESHOLD, default=3): vol.Coerce(float), 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_CLOSE_THRESHOLD, default=0): vol.Coerce(float),
vol.Optional(CONF_WINDOW_AUTO_MAX_DURATION, default=30): cv.positive_int, 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 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_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 STEP_MOTION_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name
{ {
vol.Optional(CONF_MOTION_SENSOR): selector.EntitySelector( vol.Required(CONF_MOTION_SENSOR): selector.EntitySelector(
selector.EntitySelectorConfig( selector.EntitySelectorConfig(
domain=[BINARY_SENSOR_DOMAIN, INPUT_BOOLEAN_DOMAIN] domain=[BINARY_SENSOR_DOMAIN, INPUT_BOOLEAN_DOMAIN]
), ),
@@ -217,21 +275,29 @@ 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_DELAY, default=30): cv.positive_int,
vol.Optional(CONF_MOTION_OFF_DELAY, default=300): cv.positive_int, vol.Optional(CONF_MOTION_OFF_DELAY, default=300): cv.positive_int,
vol.Optional(CONF_MOTION_PRESET, default="comfort"): vol.In( vol.Optional(CONF_MOTION_PRESET, default="comfort"): selector.SelectSelector(
CONF_PRESETS_SELECTIONABLE selector.SelectSelectorConfig(
options=CONF_PRESETS_SELECTIONABLE,
translation_key="presets",
mode="dropdown",
)
), ),
vol.Optional(CONF_NO_MOTION_PRESET, default="eco"): vol.In( vol.Optional(CONF_NO_MOTION_PRESET, default="eco"): selector.SelectSelector(
CONF_PRESETS_SELECTIONABLE selector.SelectSelectorConfig(
options=CONF_PRESETS_SELECTIONABLE,
translation_key="presets",
mode="dropdown",
)
), ),
} }
) )
STEP_CENTRAL_POWER_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name STEP_CENTRAL_POWER_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name
{ {
vol.Optional(CONF_POWER_SENSOR): selector.EntitySelector( vol.Required(CONF_POWER_SENSOR): selector.EntitySelector(
selector.EntitySelectorConfig(domain=[SENSOR_DOMAIN, INPUT_NUMBER_DOMAIN]), selector.EntitySelectorConfig(domain=[SENSOR_DOMAIN, INPUT_NUMBER_DOMAIN]),
), ),
vol.Optional(CONF_MAX_POWER_SENSOR): selector.EntitySelector( vol.Required(CONF_MAX_POWER_SENSOR): selector.EntitySelector(
selector.EntitySelectorConfig(domain=[SENSOR_DOMAIN, INPUT_NUMBER_DOMAIN]), selector.EntitySelectorConfig(domain=[SENSOR_DOMAIN, INPUT_NUMBER_DOMAIN]),
), ),
vol.Optional(CONF_PRESET_POWER, default="13"): vol.Coerce(float), vol.Optional(CONF_PRESET_POWER, default="13"): vol.Coerce(float),
@@ -246,19 +312,7 @@ STEP_POWER_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name
STEP_CENTRAL_PRESENCE_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name STEP_CENTRAL_PRESENCE_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name
{ {
vol.Optional(v, default=17): vol.Coerce(float) vol.Required(CONF_PRESENCE_SENSOR): selector.EntitySelector(
for (k, v) in CONF_PRESETS_AWAY.items()
}
)
STEP_CENTRAL_PRESENCE_WITH_AC_DATA_SCHEMA = { # pylint: disable=invalid-name
vol.Optional(v, default=17): vol.Coerce(float)
for (k, v) in CONF_PRESETS_AWAY_WITH_AC.items()
}
STEP_PRESENCE_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name
{
vol.Optional(CONF_PRESENCE_SENSOR): selector.EntitySelector(
selector.EntitySelectorConfig( selector.EntitySelectorConfig(
domain=[ domain=[
PERSON_DOMAIN, PERSON_DOMAIN,
@@ -266,7 +320,12 @@ STEP_PRESENCE_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name
INPUT_BOOLEAN_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, vol.Required(CONF_USE_PRESENCE_CENTRAL_CONFIG, default=True): cv.boolean,
} }
) )

View File

@@ -1,6 +1,8 @@
# pylint: disable=line-too-long # pylint: disable=line-too-long
"""Constants for the Versatile Thermostat integration.""" """Constants for the Versatile Thermostat integration."""
import logging
from enum import Enum from enum import Enum
from homeassistant.const import CONF_NAME, Platform from homeassistant.const import CONF_NAME, Platform
@@ -18,6 +20,12 @@ from .prop_algorithm import (
PROPORTIONAL_FUNCTION_TPI, PROPORTIONAL_FUNCTION_TPI,
) )
_LOGGER = logging.getLogger(__name__)
CONFIG_VERSION = 1
CONFIG_MINOR_VERSION = 2
PRESET_TEMP_SUFFIX = "_temp"
PRESET_AC_SUFFIX = "_ac" PRESET_AC_SUFFIX = "_ac"
PRESET_ECO_AC = PRESET_ECO + PRESET_AC_SUFFIX PRESET_ECO_AC = PRESET_ECO + PRESET_AC_SUFFIX
PRESET_COMFORT_AC = PRESET_COMFORT + PRESET_AC_SUFFIX PRESET_COMFORT_AC = PRESET_COMFORT + PRESET_AC_SUFFIX
@@ -35,18 +43,23 @@ HIDDEN_PRESETS = [PRESET_POWER, PRESET_SECURITY]
DOMAIN = "versatile_thermostat" DOMAIN = "versatile_thermostat"
# The order is important.
PLATFORMS: list[Platform] = [ PLATFORMS: list[Platform] = [
Platform.CLIMATE,
Platform.BINARY_SENSOR,
Platform.SENSOR,
Platform.SELECT, Platform.SELECT,
Platform.CLIMATE,
Platform.SENSOR,
# Number should be after CLIMATE
Platform.NUMBER,
Platform.BINARY_SENSOR,
] ]
CONF_HEATER = "heater_entity_id" CONF_HEATER = "heater_entity_id"
CONF_HEATER_2 = "heater_entity2_id" CONF_HEATER_2 = "heater_entity2_id"
CONF_HEATER_3 = "heater_entity3_id" CONF_HEATER_3 = "heater_entity3_id"
CONF_HEATER_4 = "heater_entity4_id" CONF_HEATER_4 = "heater_entity4_id"
CONF_HEATER_KEEP_ALIVE = "heater_keep_alive"
CONF_TEMP_SENSOR = "temperature_sensor_entity_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_EXTERNAL_TEMP_SENSOR = "external_temperature_sensor_entity_id"
CONF_POWER_SENSOR = "power_sensor_entity_id" CONF_POWER_SENSOR = "power_sensor_entity_id"
CONF_MAX_POWER_SENSOR = "max_power_sensor_entity_id" CONF_MAX_POWER_SENSOR = "max_power_sensor_entity_id"
@@ -83,6 +96,7 @@ CONF_USE_WINDOW_FEATURE = "use_window_feature"
CONF_USE_MOTION_FEATURE = "use_motion_feature" CONF_USE_MOTION_FEATURE = "use_motion_feature"
CONF_USE_PRESENCE_FEATURE = "use_presence_feature" CONF_USE_PRESENCE_FEATURE = "use_presence_feature"
CONF_USE_POWER_FEATURE = "use_power_feature" CONF_USE_POWER_FEATURE = "use_power_feature"
CONF_USE_CENTRAL_BOILER_FEATURE = "use_central_boiler_feature"
CONF_AC_MODE = "ac_mode" CONF_AC_MODE = "ac_mode"
CONF_WINDOW_AUTO_OPEN_THRESHOLD = "window_auto_open_threshold" CONF_WINDOW_AUTO_OPEN_THRESHOLD = "window_auto_open_threshold"
CONF_WINDOW_AUTO_CLOSE_THRESHOLD = "window_auto_close_threshold" CONF_WINDOW_AUTO_CLOSE_THRESHOLD = "window_auto_close_threshold"
@@ -100,14 +114,19 @@ CONF_AUTO_REGULATION_STRONG = "auto_regulation_strong"
CONF_AUTO_REGULATION_EXPERT = "auto_regulation_expert" CONF_AUTO_REGULATION_EXPERT = "auto_regulation_expert"
CONF_AUTO_REGULATION_DTEMP = "auto_regulation_dtemp" CONF_AUTO_REGULATION_DTEMP = "auto_regulation_dtemp"
CONF_AUTO_REGULATION_PERIOD_MIN = "auto_regulation_periode_min" 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_INVERSE_SWITCH = "inverse_switch_command"
CONF_SHORT_EMA_PARAMS = "short_ema_params"
CONF_AUTO_FAN_MODE = "auto_fan_mode" CONF_AUTO_FAN_MODE = "auto_fan_mode"
CONF_AUTO_FAN_NONE = "auto_fan_none" CONF_AUTO_FAN_NONE = "auto_fan_none"
CONF_AUTO_FAN_LOW = "auto_fan_low" CONF_AUTO_FAN_LOW = "auto_fan_low"
CONF_AUTO_FAN_MEDIUM = "auto_fan_medium" CONF_AUTO_FAN_MEDIUM = "auto_fan_medium"
CONF_AUTO_FAN_HIGH = "auto_fan_high" CONF_AUTO_FAN_HIGH = "auto_fan_high"
CONF_AUTO_FAN_TURBO = "auto_fan_turbo" CONF_AUTO_FAN_TURBO = "auto_fan_turbo"
CONF_STEP_TEMPERATURE = "step_temperature"
# Global params into configuration.yaml
CONF_SHORT_EMA_PARAMS = "short_ema_params"
CONF_SAFETY_MODE = "safety_mode"
CONF_USE_MAIN_CENTRAL_CONFIG = "use_main_central_config" CONF_USE_MAIN_CENTRAL_CONFIG = "use_main_central_config"
CONF_USE_TPI_CENTRAL_CONFIG = "use_tpi_central_config" CONF_USE_TPI_CENTRAL_CONFIG = "use_tpi_central_config"
@@ -120,6 +139,12 @@ CONF_USE_ADVANCED_CENTRAL_CONFIG = "use_advanced_central_config"
CONF_USE_CENTRAL_MODE = "use_central_mode" 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"
DEFAULT_SHORT_EMA_PARAMS = { DEFAULT_SHORT_EMA_PARAMS = {
"max_alpha": 0.5, "max_alpha": 0.5,
# In sec # In sec
@@ -128,7 +153,7 @@ DEFAULT_SHORT_EMA_PARAMS = {
} }
CONF_PRESETS = { CONF_PRESETS = {
p: f"{p}_temp" p: f"{p}{PRESET_TEMP_SUFFIX}"
for p in ( for p in (
PRESET_FROST_PROTECTION, PRESET_FROST_PROTECTION,
PRESET_ECO, PRESET_ECO,
@@ -138,7 +163,7 @@ CONF_PRESETS = {
} }
CONF_PRESETS_WITH_AC = { CONF_PRESETS_WITH_AC = {
p: f"{p}_temp" p: f"{p}{PRESET_TEMP_SUFFIX}"
for p in ( for p in (
PRESET_FROST_PROTECTION, PRESET_FROST_PROTECTION,
PRESET_ECO, PRESET_ECO,
@@ -154,7 +179,7 @@ CONF_PRESETS_WITH_AC = {
PRESET_AWAY_SUFFIX = "_away" PRESET_AWAY_SUFFIX = "_away"
CONF_PRESETS_AWAY = { CONF_PRESETS_AWAY = {
p: f"{p}_temp" p: f"{p}{PRESET_TEMP_SUFFIX}"
for p in ( for p in (
PRESET_FROST_PROTECTION + PRESET_AWAY_SUFFIX, PRESET_FROST_PROTECTION + PRESET_AWAY_SUFFIX,
PRESET_ECO + PRESET_AWAY_SUFFIX, PRESET_ECO + PRESET_AWAY_SUFFIX,
@@ -164,7 +189,7 @@ CONF_PRESETS_AWAY = {
} }
CONF_PRESETS_AWAY_WITH_AC = { CONF_PRESETS_AWAY_WITH_AC = {
p: f"{p}_temp" p: f"{p}{PRESET_TEMP_SUFFIX}"
for p in ( for p in (
PRESET_FROST_PROTECTION + PRESET_AWAY_SUFFIX, PRESET_FROST_PROTECTION + PRESET_AWAY_SUFFIX,
PRESET_ECO + PRESET_AWAY_SUFFIX, PRESET_ECO + PRESET_AWAY_SUFFIX,
@@ -195,6 +220,7 @@ ALL_CONF = (
CONF_HEATER_2, CONF_HEATER_2,
CONF_HEATER_3, CONF_HEATER_3,
CONF_HEATER_4, CONF_HEATER_4,
CONF_HEATER_KEEP_ALIVE,
CONF_TEMP_SENSOR, CONF_TEMP_SENSOR,
CONF_EXTERNAL_TEMP_SENSOR, CONF_EXTERNAL_TEMP_SENSOR,
CONF_POWER_SENSOR, CONF_POWER_SENSOR,
@@ -231,6 +257,7 @@ ALL_CONF = (
CONF_USE_MOTION_FEATURE, CONF_USE_MOTION_FEATURE,
CONF_USE_PRESENCE_FEATURE, CONF_USE_PRESENCE_FEATURE,
CONF_USE_POWER_FEATURE, CONF_USE_POWER_FEATURE,
CONF_USE_CENTRAL_BOILER_FEATURE,
CONF_AC_MODE, CONF_AC_MODE,
CONF_VALVE, CONF_VALVE,
CONF_VALVE_2, CONF_VALVE_2,
@@ -239,6 +266,7 @@ ALL_CONF = (
CONF_AUTO_REGULATION_MODE, CONF_AUTO_REGULATION_MODE,
CONF_AUTO_REGULATION_DTEMP, CONF_AUTO_REGULATION_DTEMP,
CONF_AUTO_REGULATION_PERIOD_MIN, CONF_AUTO_REGULATION_PERIOD_MIN,
CONF_AUTO_REGULATION_USE_DEVICE_TEMP,
CONF_INVERSE_SWITCH, CONF_INVERSE_SWITCH,
CONF_AUTO_FAN_MODE, CONF_AUTO_FAN_MODE,
CONF_USE_MAIN_CENTRAL_CONFIG, CONF_USE_MAIN_CENTRAL_CONFIG,
@@ -250,6 +278,11 @@ ALL_CONF = (
CONF_USE_PRESENCE_CENTRAL_CONFIG, CONF_USE_PRESENCE_CENTRAL_CONFIG,
CONF_USE_ADVANCED_CENTRAL_CONFIG, CONF_USE_ADVANCED_CENTRAL_CONFIG,
CONF_USE_CENTRAL_MODE, 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_VALUES
+ CONF_PRESETS_AWAY_VALUES + CONF_PRESETS_AWAY_VALUES
@@ -285,7 +318,19 @@ CONF_AUTO_FAN_MODES = [
CONF_AUTO_FAN_TURBO, CONF_AUTO_FAN_TURBO,
] ]
SUPPORT_FLAGS = ClimateEntityFeature.TARGET_TEMPERATURE CONF_WINDOW_TURN_OFF = "window_turn_off"
CONF_WINDOW_FAN_ONLY = "window_fan_only"
CONF_WINDOW_FROST_TEMP = "window_frost_temp"
CONF_WINDOW_ECO_TEMP = "window_eco_temp"
CONF_WINDOW_ACTIONS = [
CONF_WINDOW_TURN_OFF,
CONF_WINDOW_FAN_ONLY,
CONF_WINDOW_FROST_TEMP,
CONF_WINDOW_ECO_TEMP,
]
SUPPORT_FLAGS = ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.TURN_OFF | ClimateEntityFeature.TURN_ON
SERVICE_SET_PRESENCE = "set_presence" SERVICE_SET_PRESENCE = "set_presence"
SERVICE_SET_PRESET_TEMPERATURE = "set_preset_temperature" SERVICE_SET_PRESET_TEMPERATURE = "set_preset_temperature"
@@ -323,7 +368,9 @@ CENTRAL_MODES = [
class RegulationParamSlow: class RegulationParamSlow:
"""Light parameters for slow latency regulation""" """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 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 = ( ki: float = (
0.8 / 288.0 0.8 / 288.0
) # 80% of the current internal regulation offset are caused by the average offset of the past 24 hours ) # 80% of the current internal regulation offset are caused by the average offset of the past 24 hours
@@ -331,7 +378,9 @@ class RegulationParamSlow:
1.0 / 25.0 1.0 / 25.0
) # this will add 1°C to the offset when it's 25°C colder outdoor than indoor ) # 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 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 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 = ( accumulated_error_threshold: float = (
2.0 * 288 2.0 * 288
) # this allows up to 2°C long term offset in both directions ) # this allows up to 2°C long term offset in both directions
@@ -393,10 +442,20 @@ class EventType(Enum):
POWER_EVENT: str = "versatile_thermostat_power_event" POWER_EVENT: str = "versatile_thermostat_power_event"
TEMPERATURE_EVENT: str = "versatile_thermostat_temperature_event" TEMPERATURE_EVENT: str = "versatile_thermostat_temperature_event"
HVAC_MODE_EVENT: str = "versatile_thermostat_hvac_mode_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" PRESET_EVENT: str = "versatile_thermostat_preset_event"
WINDOW_AUTO_EVENT: str = "versatile_thermostat_window_auto_event" WINDOW_AUTO_EVENT: str = "versatile_thermostat_window_auto_event"
def send_vtherm_event(hass, event_type: EventType, entity, data: dict):
"""Send an event"""
_LOGGER.info("%s - Sending event %s with data: %s", entity, event_type, data)
data["entity_id"] = entity.entity_id
data["name"] = entity.name
data["state_attributes"] = entity.state_attributes
hass.bus.fire(event_type.value, data)
class UnknownEntity(HomeAssistantError): class UnknownEntity(HomeAssistantError):
"""Error to indicate there is an unknown entity_id given.""" """Error to indicate there is an unknown entity_id given."""
@@ -409,6 +468,14 @@ class NoCentralConfig(HomeAssistantError):
"""Error to indicate that we try to use a central configuration but no VTherm of type CENTRAL CONFIGURATION has been found""" """Error to indicate that we try to use a central configuration but no VTherm of type CENTRAL CONFIGURATION has been found"""
class ServiceConfigurationError(HomeAssistantError):
"""Error in the service configuration to control the central boiler"""
class ConfigurationNotCompleteError(HomeAssistantError):
"""Error the configuration is not complete"""
class overrides: # pylint: disable=invalid-name class overrides: # pylint: disable=invalid-name
"""An annotation to inform overrides""" """An annotation to inform overrides"""

View File

@@ -0,0 +1,138 @@
"""Building blocks for the heater switch keep-alive feature.
The heater switch keep-alive feature consists of regularly refreshing the state
of directly controlled switches at a configurable interval (regularly turning the
switch 'on' or 'off' again even if it is already turned 'on' or 'off'), just like
the keep_alive setting of Home Assistant's Generic Thermostat integration:
https://www.home-assistant.io/integrations/generic_thermostat/
"""
import logging
from collections.abc import Awaitable, Callable
from datetime import timedelta, datetime
from time import monotonic
from homeassistant.core import HomeAssistant, CALLBACK_TYPE
from homeassistant.helpers.event import async_track_time_interval
_LOGGER = logging.getLogger(__name__)
class BackoffTimer:
"""Exponential backoff timer with a non-blocking polling-style implementation.
Usage example:
timer = BackoffTimer(multiplier=1.5, upper_limit_sec=600)
while some_condition:
if timer.is_ready():
do_something()
"""
def __init__(
self,
*,
multiplier=2.0,
lower_limit_sec=30,
upper_limit_sec=86400,
initially_ready=True,
):
"""Initialize a BackoffTimer instance.
Args:
multiplier (int, optional): Period multiplier applied when is_ready() is True.
lower_limit_sec (int, optional): Initial backoff period in seconds.
upper_limit_sec (int, optional): Maximum backoff period in seconds.
initially_ready (bool, optional): Whether is_ready() should return True the
first time it is called, or after a call to reset().
"""
self._multiplier = multiplier
self._lower_limit_sec = lower_limit_sec
self._upper_limit_sec = upper_limit_sec
self._initially_ready = initially_ready
self._timestamp = 0
self._period_sec = self._lower_limit_sec
@property
def in_progress(self) -> bool:
"""Whether the backoff timer is in progress (True after a call to is_ready())."""
return bool(self._timestamp)
def reset(self):
"""Reset a BackoffTimer instance."""
self._timestamp = 0
self._period_sec = self._lower_limit_sec
def is_ready(self) -> bool:
"""Check whether an exponentially increasing period of time has passed.
Whenever is_ready() returns True, the timer period is multiplied so that
it takes longer until is_ready() returns True again.
Returns:
bool: True if enough time has passed since one of the following events,
in relation to an instance of this class:
- The last time when this method returned True, if it ever did.
- Or else, when this method was first called after a call to reset().
- Or else, when this method was first called.
False otherwise.
"""
now = monotonic()
if self._timestamp == 0:
self._timestamp = now
return self._initially_ready
elif now - self._timestamp >= self._period_sec:
self._timestamp = now
self._period_sec = max(
self._lower_limit_sec,
min(self._upper_limit_sec, self._period_sec * self._multiplier),
)
return True
return False
class IntervalCaller:
"""Repeatedly call a given async action function at a given regular interval.
Convenience wrapper around Home Assistant's `async_track_time_interval` function.
"""
def __init__(self, hass: HomeAssistant, interval_sec: float) -> None:
self._hass = hass
self._interval_sec = interval_sec
self._remove_handle: CALLBACK_TYPE | None = None
self.backoff_timer = BackoffTimer()
@property
def interval_sec(self) -> float:
"""Return the calling interval in seconds."""
return self._interval_sec
def cancel(self):
"""Cancel the regular calls to the action function."""
if self._remove_handle:
self._remove_handle()
self._remove_handle = None
def set_async_action(self, action: Callable[[], Awaitable[None]]):
"""Set the async action function to be called at regular intervals."""
if not self._interval_sec:
return
self.cancel()
async def callback(_time: datetime):
try:
_LOGGER.debug(
"Calling keep-alive action '%s' (%ss interval)",
action.__name__,
self._interval_sec,
)
await action()
except Exception as e: # pylint: disable=broad-exception-caught
_LOGGER.error(e)
self.cancel()
self._remove_handle = async_track_time_interval(
self._hass, callback, timedelta(seconds=self._interval_sec)
)

View File

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

View File

@@ -0,0 +1,524 @@
# pylint: disable=unused-argument
""" Implements the VersatileThermostat select component """
import logging
# from homeassistant.const import EVENT_HOMEASSISTANT_START
from homeassistant.core import HomeAssistant, CoreState # , callback
from homeassistant.components.number import (
NumberEntity,
NumberMode,
NumberDeviceClass,
DOMAIN as NUMBER_DOMAIN,
DEFAULT_MAX_VALUE,
DEFAULT_MIN_VALUE,
DEFAULT_STEP,
)
from homeassistant.components.climate import (
PRESET_BOOST,
PRESET_COMFORT,
PRESET_ECO,
)
from homeassistant.helpers.device_registry import DeviceInfo, DeviceEntryType
from homeassistant.config_entries import ConfigEntry
from homeassistant.helpers.restore_state import RestoreEntity
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.util import slugify
from .vtherm_api import VersatileThermostatAPI
from .commons import VersatileThermostatBaseEntity
from .const import (
DOMAIN,
DEVICE_MANUFACTURER,
CONF_NAME,
CONF_THERMOSTAT_TYPE,
CONF_THERMOSTAT_CENTRAL_CONFIG,
CONF_TEMP_MIN,
CONF_TEMP_MAX,
CONF_STEP_TEMPERATURE,
CONF_AC_MODE,
PRESET_FROST_PROTECTION,
PRESET_ECO_AC,
PRESET_COMFORT_AC,
PRESET_BOOST_AC,
PRESET_AWAY_SUFFIX,
PRESET_TEMP_SUFFIX,
CONF_PRESETS_VALUES,
CONF_PRESETS_WITH_AC_VALUES,
CONF_PRESETS_AWAY_VALUES,
CONF_PRESETS_AWAY_WITH_AC_VALUES,
CONF_USE_PRESETS_CENTRAL_CONFIG,
CONF_USE_PRESENCE_CENTRAL_CONFIG,
CONF_USE_PRESENCE_FEATURE,
CONF_USE_CENTRAL_BOILER_FEATURE,
overrides,
CONF_USE_MAIN_CENTRAL_CONFIG,
)
PRESET_ICON_MAPPING = {
PRESET_FROST_PROTECTION + PRESET_TEMP_SUFFIX: "mdi:snowflake-thermometer",
PRESET_ECO + PRESET_TEMP_SUFFIX: "mdi:leaf",
PRESET_COMFORT + PRESET_TEMP_SUFFIX: "mdi:sofa",
PRESET_BOOST + PRESET_TEMP_SUFFIX: "mdi:rocket-launch",
PRESET_ECO_AC + PRESET_TEMP_SUFFIX: "mdi:leaf-circle-outline",
PRESET_COMFORT_AC + PRESET_TEMP_SUFFIX: "mdi:sofa-outline",
PRESET_BOOST_AC + PRESET_TEMP_SUFFIX: "mdi:rocket-launch-outline",
PRESET_FROST_PROTECTION
+ PRESET_AWAY_SUFFIX
+ PRESET_TEMP_SUFFIX: "mdi:snowflake-thermometer",
PRESET_ECO + PRESET_AWAY_SUFFIX + PRESET_TEMP_SUFFIX: "mdi:leaf",
PRESET_COMFORT + PRESET_AWAY_SUFFIX + PRESET_TEMP_SUFFIX: "mdi:sofa",
PRESET_BOOST + PRESET_AWAY_SUFFIX + PRESET_TEMP_SUFFIX: "mdi:rocket-launch",
PRESET_ECO_AC + PRESET_AWAY_SUFFIX + PRESET_TEMP_SUFFIX: "mdi:leaf-circle-outline",
PRESET_COMFORT_AC + PRESET_AWAY_SUFFIX + PRESET_TEMP_SUFFIX: "mdi:sofa-outline",
PRESET_BOOST_AC
+ PRESET_AWAY_SUFFIX
+ PRESET_TEMP_SUFFIX: "mdi:rocket-launch-outline",
}
_LOGGER = logging.getLogger(__name__)
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the VersatileThermostat selects with config flow."""
_LOGGER.debug(
"Calling async_setup_entry entry=%s, data=%s", entry.entry_id, entry.data
)
unique_id = entry.entry_id
name = entry.data.get(CONF_NAME)
vt_type = entry.data.get(CONF_THERMOSTAT_TYPE)
# is_central_boiler = entry.data.get(CONF_USE_CENTRAL_BOILER_FEATURE)
entities = []
if vt_type != CONF_THERMOSTAT_CENTRAL_CONFIG:
# Creates non central temperature entities
if not entry.data.get(CONF_USE_PRESETS_CENTRAL_CONFIG, False):
if entry.data.get(CONF_AC_MODE, False):
for preset in CONF_PRESETS_WITH_AC_VALUES:
_LOGGER.debug(
"%s - configuring Number non central, AC, non AWAY for preset %s",
name,
preset,
)
entities.append(
TemperatureNumber(
hass, unique_id, name, preset, True, False, entry.data
)
)
else:
for preset in CONF_PRESETS_VALUES:
_LOGGER.debug(
"%s - configuring Number non central, non AC, non AWAY for preset %s",
name,
preset,
)
entities.append(
TemperatureNumber(
hass, unique_id, name, preset, False, False, entry.data
)
)
if entry.data.get(
CONF_USE_PRESENCE_FEATURE, False
) is True and not entry.data.get(CONF_USE_PRESENCE_CENTRAL_CONFIG, False):
if entry.data.get(CONF_AC_MODE, False):
for preset in CONF_PRESETS_AWAY_WITH_AC_VALUES:
_LOGGER.debug(
"%s - configuring Number non central, AC, AWAY for preset %s",
name,
preset,
)
entities.append(
TemperatureNumber(
hass, unique_id, name, preset, True, True, entry.data
)
)
else:
for preset in CONF_PRESETS_AWAY_VALUES:
_LOGGER.debug(
"%s - configuring Number non central, non AC, AWAY for preset %s",
name,
preset,
)
entities.append(
TemperatureNumber(
hass, unique_id, name, preset, False, True, entry.data
)
)
# For central config only
else:
if entry.data.get(CONF_USE_CENTRAL_BOILER_FEATURE):
entities.append(
ActivateBoilerThresholdNumber(hass, unique_id, name, entry.data)
)
for preset in CONF_PRESETS_WITH_AC_VALUES:
_LOGGER.debug(
"%s - configuring Number central, AC, non AWAY for preset %s",
name,
preset,
)
entities.append(
CentralConfigTemperatureNumber(
hass, unique_id, name, preset, True, False, entry.data
)
)
for preset in CONF_PRESETS_AWAY_WITH_AC_VALUES:
_LOGGER.debug(
"%s - configuring Number central, AC, AWAY for preset %s", name, preset
)
entities.append(
CentralConfigTemperatureNumber(
hass, unique_id, name, preset, True, True, entry.data
)
)
if len(entities) > 0:
async_add_entities(entities, True)
class ActivateBoilerThresholdNumber(
NumberEntity, RestoreEntity
): # pylint: disable=abstract-method
"""Representation of the threshold of the number of VTherm
which should be active to activate the boiler"""
def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None:
"""Initialize the energy sensor"""
self._hass = hass
self._config_id = unique_id
self._device_name = entry_infos.get(CONF_NAME)
self._attr_name = "Boiler Activation threshold"
self._attr_unique_id = "boiler_activation_threshold"
self._attr_value = self._attr_native_value = 1 # default value
self._attr_native_min_value = 1
self._attr_native_max_value = 9
self._attr_step = 1 # default value
self._attr_mode = NumberMode.AUTO
@property
def icon(self) -> str | None:
if isinstance(self._attr_native_value, int):
val = int(self._attr_native_value)
return f"mdi:numeric-{val}-box-outline"
else:
return "mdi:numeric-0-box-outline"
@property
def device_info(self) -> DeviceInfo:
"""Return the device info."""
return DeviceInfo(
entry_type=DeviceEntryType.SERVICE,
identifiers={(DOMAIN, self._config_id)},
name=self._device_name,
manufacturer=DEVICE_MANUFACTURER,
model=DOMAIN,
)
@overrides
async def async_added_to_hass(self) -> None:
await super().async_added_to_hass()
api: VersatileThermostatAPI = VersatileThermostatAPI.get_vtherm_api(self._hass)
api.register_central_boiler_activation_number_threshold(self)
old_state: CoreState = await self.async_get_last_state()
_LOGGER.debug(
"%s - Calling async_added_to_hass old_state is %s", self, old_state
)
if old_state is not None:
self._attr_value = self._attr_native_value = int(float(old_state.state))
@overrides
def set_native_value(self, value: float) -> None:
"""Change the value"""
int_value = int(value)
old_value = int(self._attr_native_value)
if int_value == old_value:
return
self._attr_value = self._attr_native_value = int_value
def __str__(self):
return f"VersatileThermostat-{self.name}"
class CentralConfigTemperatureNumber(
NumberEntity, RestoreEntity
): # pylint: disable=abstract-method
"""Representation of one temperature number"""
_attr_has_entity_name = True
def __init__(
self,
hass: HomeAssistant,
unique_id,
name,
preset_name,
is_ac,
is_away,
entry_infos,
) -> None:
"""Initialize the temperature with entry_infos if available. Else
the restoration will do the trick."""
self._config_id = unique_id
self._device_name = name
# self._attr_name = name
self._attr_translation_key = preset_name
self.entity_id = f"{NUMBER_DOMAIN}.{slugify(name)}_preset_{preset_name}"
self._attr_unique_id = f"central_configuration_preset_{preset_name}"
self._attr_device_class = NumberDeviceClass.TEMPERATURE
self._attr_native_unit_of_measurement = hass.config.units.temperature_unit
self._attr_native_step = entry_infos.get(CONF_STEP_TEMPERATURE, 0.5)
self._attr_native_min_value = entry_infos.get(CONF_TEMP_MIN)
self._attr_native_max_value = entry_infos.get(CONF_TEMP_MAX)
# Initialize the values if included into the entry_infos. This will do
# the temperature migration. Else the temperature will be restored from
# previous value
# TODO remove this after the next major release and just keep the init min/max
temp = None
if (temp := entry_infos.get(preset_name, None)) is not None:
self._attr_value = self._attr_native_value = temp
else:
if entry_infos.get(CONF_AC_MODE) is True:
self._attr_native_value = self._attr_native_max_value
else:
self._attr_native_value = self._attr_native_min_value
self._attr_mode = NumberMode.BOX
self._preset_name = preset_name
self._is_away = is_away
self._is_ac = is_ac
@property
def icon(self) -> str | None:
return PRESET_ICON_MAPPING[self._preset_name]
@property
def device_info(self) -> DeviceInfo:
"""Return the device info."""
return DeviceInfo(
entry_type=DeviceEntryType.SERVICE,
identifiers={(DOMAIN, self._config_id)},
name=self._device_name,
manufacturer=DEVICE_MANUFACTURER,
model=DOMAIN,
)
@overrides
async def async_added_to_hass(self) -> None:
await super().async_added_to_hass()
# register the temp entity for this device and preset
api: VersatileThermostatAPI = VersatileThermostatAPI.get_vtherm_api(self.hass)
api.register_temperature_number(self._config_id, self._preset_name, self)
# Restore value from previous one if exists
old_state: CoreState = await self.async_get_last_state()
_LOGGER.debug(
"%s - Calling async_added_to_hass old_state is %s", self, old_state
)
try:
if old_state is not None and ((value := float(old_state.state)) > 0):
self._attr_value = self._attr_native_value = value
except ValueError:
pass
@overrides
async def async_set_native_value(self, value: float) -> None:
"""The value have change from the Number Entity in UI"""
float_value = float(value)
old_value = (
None if self._attr_native_value is None else float(self._attr_native_value)
)
if float_value == old_value:
return
self._attr_value = self._attr_native_value = float_value
# persist the value
self.async_write_ha_state()
# We have to reload all VTherm for which uses the central configuration
api: VersatileThermostatAPI = VersatileThermostatAPI.get_vtherm_api(self.hass)
# Update the VTherms which have temperature in central config
self.hass.create_task(api.init_vtherm_preset_with_central())
def __str__(self):
return f"VersatileThermostat-{self.name}"
@property
def native_unit_of_measurement(self) -> str | None:
"""The unit of measurement"""
# TODO Kelvin ? It seems not because all internal values are stored in
# ° Celsius but only the render in front can be in °K depending on the
# user configuration.
return self.hass.config.units.temperature_unit
class TemperatureNumber( # pylint: disable=abstract-method
VersatileThermostatBaseEntity, NumberEntity, RestoreEntity
):
"""Representation of one temperature number"""
_attr_has_entity_name = True
def __init__(
self,
hass: HomeAssistant,
unique_id,
name,
preset_name,
is_ac,
is_away,
entry_infos,
) -> None:
"""Initialize the temperature with entry_infos if available. Else
the restoration will do the trick."""
super().__init__(hass, unique_id, name)
self._attr_translation_key = preset_name
self.entity_id = f"{NUMBER_DOMAIN}.{slugify(name)}_preset_{preset_name}"
self._attr_unique_id = f"{self._device_name}_preset_{preset_name}"
self._attr_device_class = NumberDeviceClass.TEMPERATURE
self._attr_native_unit_of_measurement = hass.config.units.temperature_unit
self._has_central_main_attributes = entry_infos.get(
CONF_USE_MAIN_CENTRAL_CONFIG, False
)
self.init_min_max_step(entry_infos)
# Initialize the values if included into the entry_infos. This will do
# the temperature migration.
temp = None
if (temp := entry_infos.get(preset_name, None)) is not None:
self._attr_value = self._attr_native_value = temp
else:
if entry_infos.get(CONF_AC_MODE) is True:
self._attr_native_value = self._attr_native_max_value
else:
self._attr_native_value = self._attr_native_min_value
self._attr_mode = NumberMode.BOX
self._preset_name = preset_name
self._canonical_preset_name = preset_name.replace(
PRESET_TEMP_SUFFIX, ""
).replace(PRESET_AWAY_SUFFIX, "")
self._is_away = is_away
self._is_ac = is_ac
@property
def icon(self) -> str | None:
return PRESET_ICON_MAPPING[self._preset_name]
@overrides
async def async_added_to_hass(self) -> None:
await super().async_added_to_hass()
# register the temp entity for this device and preset
api: VersatileThermostatAPI = VersatileThermostatAPI.get_vtherm_api(self.hass)
api.register_temperature_number(self._config_id, self._preset_name, self)
old_state: CoreState = await self.async_get_last_state()
_LOGGER.debug(
"%s - Calling async_added_to_hass old_state is %s", self, old_state
)
try:
if old_state is not None and ((value := float(old_state.state)) > 0):
self._attr_value = self._attr_native_value = value
except ValueError:
pass
@overrides
def my_climate_is_initialized(self):
"""Called when the associated climate is initialized"""
self._attr_native_step = self.my_climate.target_temperature_step
self._attr_native_min_value = self.my_climate.min_temp
self._attr_native_max_value = self.my_climate.max_temp
return
@overrides
async def async_set_native_value(self, value: float) -> None:
"""Change the value"""
if self.my_climate is None:
_LOGGER.warning(
"%s - cannot change temperature because VTherm is not initialized", self
)
return
float_value = float(value)
old_value = (
None if self._attr_native_value is None else float(self._attr_native_value)
)
if float_value == old_value:
return
self._attr_value = self._attr_native_value = float_value
self.async_write_ha_state()
# Update the VTherm temp
self.hass.create_task(
self.my_climate.service_set_preset_temperature(
self._canonical_preset_name,
self._attr_native_value if not self._is_away else None,
self._attr_native_value if self._is_away else None,
)
)
# We set the min, max and step from central config if relevant because it is possible
# that central config was not loaded at startup
self.init_min_max_step()
def __str__(self):
return f"VersatileThermostat-{self.name}"
@property
def native_unit_of_measurement(self) -> str | None:
"""The unit of measurement"""
if not self.my_climate:
return self.hass.config.units.temperature_unit
return self.my_climate.temperature_unit
def init_min_max_step(self, entry_infos=None):
"""Initialize min, max and step value from config or from central config"""
if self._has_central_main_attributes:
vthermapi: VersatileThermostatAPI = VersatileThermostatAPI.get_vtherm_api()
central_config = vthermapi.find_central_configuration()
if central_config:
self._attr_native_step = central_config.data.get(CONF_STEP_TEMPERATURE)
self._attr_native_min_value = central_config.data.get(CONF_TEMP_MIN)
self._attr_native_max_value = central_config.data.get(CONF_TEMP_MAX)
return
if entry_infos:
self._attr_native_step = entry_infos.get(
CONF_STEP_TEMPERATURE, DEFAULT_STEP
)
self._attr_native_min_value = entry_infos.get(
CONF_TEMP_MIN, DEFAULT_MIN_VALUE
)
self._attr_native_max_value = entry_infos.get(
CONF_TEMP_MAX, DEFAULT_MAX_VALUE
)

View File

@@ -47,16 +47,16 @@ class PITemperatureRegulator:
def set_target_temp(self, target_temp): def set_target_temp(self, target_temp):
"""Set the new target_temp""" """Set the new target_temp"""
self.target_temp = target_temp self.target_temp = target_temp
# Do not reset the accumulated error
# Discussion #191. After a target change we should reset the accumulated error which is certainly wrong now. # Discussion #191. After a target change we should reset the accumulated error which is certainly wrong now.
if self.accumulated_error < 0: # Discussion #384. Finally don't reset the accumulated error but smoothly reset it if the sign is inversed
self.accumulated_error = 0 # if self.accumulated_error < 0:
# self.accumulated_error = 0
def calculate_regulated_temperature( def calculate_regulated_temperature(
self, internal_temp: float, external_temp: float self, room_temp: float, external_temp: float
): # pylint: disable=unused-argument ): # pylint: disable=unused-argument
"""Calculate a new target_temp given some temperature""" """Calculate a new target_temp given some temperature"""
if internal_temp is None: if room_temp is None:
_LOGGER.warning( _LOGGER.warning(
"Temporarily skipping the self-regulation algorithm while the configured sensor for room temperature is unavailable" "Temporarily skipping the self-regulation algorithm while the configured sensor for room temperature is unavailable"
) )
@@ -68,9 +68,14 @@ class PITemperatureRegulator:
return self.target_temp return self.target_temp
# Calculate the error factor (P) # Calculate the error factor (P)
error = self.target_temp - internal_temp error = self.target_temp - room_temp
# Calculate the sum of error (I) # 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 self.accumulated_error += error
# Capping of the error # Capping of the error
@@ -83,19 +88,12 @@ class PITemperatureRegulator:
offset = self.kp * error + self.ki * self.accumulated_error offset = self.kp * error + self.ki * self.accumulated_error
# Calculate the exterior offset # Calculate the exterior offset
# For Maia tests - use the internal_temp vs external_temp and not target_temp - external_temp offset_ext = self.k_ext * (room_temp - external_temp)
offset_ext = self.k_ext * (internal_temp - external_temp)
# Capping of offset_ext # Capping of offset
total_offset = offset + offset_ext total_offset = offset + offset_ext
total_offset = min(self.offset_max, max(-self.offset_max, total_offset)) total_offset = min(self.offset_max, max(-self.offset_max, total_offset))
# If temperature is near the target_temp, reset the accumulated_error
# Issue #199 - don't reset the accumulation error
# if abs(error) < self.stabilization_threshold:
# _LOGGER.debug("Stabilisation")
# self.accumulated_error = 0
result = round(self.target_temp + total_offset, 1) result = round(self.target_temp + total_offset, 1)
_LOGGER.debug( _LOGGER.debug(

View File

@@ -1,6 +1,9 @@
""" The TPI calculation module """ """ The TPI calculation module """
# pylint: disable='line-too-long'
import logging import logging
from homeassistant.components.climate import HVACMode
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
PROPORTIONAL_FUNCTION_ATAN = "atan" PROPORTIONAL_FUNCTION_ATAN = "atan"
@@ -12,6 +15,11 @@ PROPORTIONAL_MIN_DURATION_SEC = 10
FUNCTION_TYPE = [PROPORTIONAL_FUNCTION_ATAN, PROPORTIONAL_FUNCTION_LINEAR] 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: class PropAlgorithm:
"""This class aims to do all calculation of the Proportional alogorithm""" """This class aims to do all calculation of the Proportional alogorithm"""
@@ -22,16 +30,43 @@ class PropAlgorithm:
tpi_coef_ext, tpi_coef_ext,
cycle_min: int, cycle_min: int,
minimal_activation_delay: int, minimal_activation_delay: int,
vtherm_entity_id: str = None,
) -> None: ) -> None:
"""Initialisation of the Proportional Algorithm""" """Initialisation of the Proportional Algorithm"""
_LOGGER.debug( _LOGGER.debug(
"Creation new PropAlgorithm function_type: %s, tpi_coef_int: %s, tpi_coef_ext: %s, cycle_min:%d, minimal_activation_delay:%d", # pylint: disable=line-too-long "%s - Creation new PropAlgorithm function_type: %s, tpi_coef_int: %s, tpi_coef_ext: %s, cycle_min:%d, minimal_activation_delay:%d", # pylint: disable=line-too-long
vtherm_entity_id,
function_type, function_type,
tpi_coef_int, tpi_coef_int,
tpi_coef_ext, tpi_coef_ext,
cycle_min, cycle_min,
minimal_activation_delay, minimal_activation_delay,
) )
# Issue 506 - check parameters
if (
vtherm_entity_id is None
or not is_number(tpi_coef_int)
or not is_number(tpi_coef_ext)
or not is_number(cycle_min)
or not is_number(minimal_activation_delay)
or function_type != PROPORTIONAL_FUNCTION_TPI
):
_LOGGER.error(
"%s - configuration is wrong. function_type=%s, entity_id is %s, tpi_coef_int is %s, tpi_coef_ext is %s, cycle_min is %s, minimal_activation_delay is %s",
vtherm_entity_id,
function_type,
vtherm_entity_id,
tpi_coef_int,
tpi_coef_ext,
cycle_min,
minimal_activation_delay,
)
raise TypeError(
"TPI parameters are not set correctly. VTherm will not work as expected. Please reconfigure it correctly. See previous log for values"
)
self._vtherm_entity_id = vtherm_entity_id
self._function = function_type self._function = function_type
self._tpi_coef_int = tpi_coef_int self._tpi_coef_int = tpi_coef_int
self._tpi_coef_ext = tpi_coef_ext self._tpi_coef_ext = tpi_coef_ext
@@ -46,24 +81,28 @@ class PropAlgorithm:
def calculate( def calculate(
self, self,
target_temp: float, target_temp: float | None,
current_temp: float, current_temp: float | None,
ext_current_temp: float, ext_current_temp: float | None,
cooling=False, hvac_mode: HVACMode,
): ):
"""Do the calculation of the duration""" """Do the calculation of the duration"""
if target_temp is None or current_temp is None: if target_temp is None or current_temp is None:
_LOGGER.warning( log = _LOGGER.debug if hvac_mode == HVACMode.OFF else _LOGGER.warning
"Proportional algorithm: calculation is not possible cause target_temp or current_temp is null. Heating/cooling will be disabled" # pylint: disable=line-too-long log(
"%s - Proportional algorithm: calculation is not possible cause target_temp (%s) or current_temp (%s) is null. Heating/cooling will be disabled. This could be normal at startup", # pylint: disable=line-too-long
self._vtherm_entity_id,
target_temp,
current_temp,
) )
self._calculated_on_percent = 0 self._calculated_on_percent = 0
else: else:
if cooling: if hvac_mode == HVACMode.COOL:
delta_temp = current_temp - target_temp delta_temp = current_temp - target_temp
delta_ext_temp = ( delta_ext_temp = (
ext_current_temp ext_current_temp - target_temp
if ext_current_temp is not None if ext_current_temp is not None
else 0 - target_temp else 0
) )
else: else:
delta_temp = target_temp - current_temp delta_temp = target_temp - current_temp
@@ -80,7 +119,8 @@ class PropAlgorithm:
) )
else: else:
_LOGGER.warning( _LOGGER.warning(
"Proportional algorithm: unknown %s function. Heating will be disabled", "%s - Proportional algorithm: unknown %s function. Heating will be disabled",
self._vtherm_entity_id,
self._function, self._function,
) )
self._calculated_on_percent = 0 self._calculated_on_percent = 0
@@ -88,7 +128,8 @@ class PropAlgorithm:
self._calculate_internal() self._calculate_internal()
_LOGGER.debug( _LOGGER.debug(
"heating percent calculated for current_temp %.1f, ext_current_temp %.1f and target_temp %.1f is %.2f, on_time is %d (sec), off_time is %d (sec)", # pylint: disable=line-too-long "%s - heating percent calculated for current_temp %.1f, ext_current_temp %.1f and target_temp %.1f is %.2f, on_time is %d (sec), off_time is %d (sec)", # pylint: disable=line-too-long
self._vtherm_entity_id,
current_temp if current_temp else -9999.0, current_temp if current_temp else -9999.0,
ext_current_temp if ext_current_temp else -9999.0, ext_current_temp if ext_current_temp else -9999.0,
target_temp if target_temp else -9999.0, target_temp if target_temp else -9999.0,
@@ -107,11 +148,12 @@ class PropAlgorithm:
self._calculated_on_percent = 0 self._calculated_on_percent = 0
if self._security: if self._security:
_LOGGER.debug(
"Security is On using the default_on_percent %f",
self._default_on_percent,
)
self._on_percent = self._default_on_percent self._on_percent = self._default_on_percent
_LOGGER.info(
"%s - Security is On using the default_on_percent %f",
self._vtherm_entity_id,
self._on_percent,
)
else: else:
_LOGGER.debug( _LOGGER.debug(
"Security is Off using the calculated_on_percent %f", "Security is Off using the calculated_on_percent %f",
@@ -125,13 +167,8 @@ class PropAlgorithm:
if self._on_time_sec < self._minimal_activation_delay: if self._on_time_sec < self._minimal_activation_delay:
if self._on_time_sec > 0: if self._on_time_sec > 0:
_LOGGER.info( _LOGGER.info(
"No heating period due to heating period too small (%f < %f)", "%s - No heating period due to heating period too small (%f < %f)",
self._on_time_sec, self._vtherm_entity_id,
self._minimal_activation_delay,
)
else:
_LOGGER.debug(
"No heating period due to heating period too small (%f < %f)",
self._on_time_sec, self._on_time_sec,
self._minimal_activation_delay, self._minimal_activation_delay,
) )
@@ -140,27 +177,33 @@ class PropAlgorithm:
self._off_time_sec = self._cycle_min * 60 - self._on_time_sec self._off_time_sec = self._cycle_min * 60 - self._on_time_sec
def set_security(self, default_on_percent: float): def set_security(self, default_on_percent: float):
"""Set a default value for on_percent (used for security mode)""" """Set a default value for on_percent (used for safety mode)"""
_LOGGER.info(
"%s - Proportional Algo - set security to ON", self._vtherm_entity_id
)
self._security = True self._security = True
self._default_on_percent = default_on_percent self._default_on_percent = default_on_percent
self._calculate_internal() self._calculate_internal()
def unset_security(self): def unset_security(self):
"""Unset the security mode""" """Unset the safety mode"""
_LOGGER.info(
"%s - Proportional Algo - set security to OFF", self._vtherm_entity_id
)
self._security = False self._security = False
self._calculate_internal() self._calculate_internal()
@property @property
def on_percent(self) -> float: def on_percent(self) -> float:
"""Returns the percentage the heater must be ON """Returns the percentage the heater must be ON
In security mode this value is overriden with the _default_on_percent In safety mode this value is overriden with the _default_on_percent
(1 means the heater will be always on, 0 never on)""" # pylint: disable=line-too-long (1 means the heater will be always on, 0 never on)""" # pylint: disable=line-too-long
return round(self._on_percent, 2) return round(self._on_percent, 2)
@property @property
def calculated_on_percent(self) -> float: def calculated_on_percent(self) -> float:
"""Returns the calculated percentage the heater must be ON """Returns the calculated percentage the heater must be ON
Calculated means NOT overriden even in security mode Calculated means NOT overriden even in safety mode
(1 means the heater will be always on, 0 never on)""" # pylint: disable=line-too-long (1 means the heater will be always on, 0 never on)""" # pylint: disable=line-too-long
return round(self._calculated_on_percent, 2) return round(self._calculated_on_percent, 2)

View File

@@ -3,19 +3,20 @@
""" Implements the VersatileThermostat select component """ """ Implements the VersatileThermostat select component """
import logging import logging
from homeassistant.const import EVENT_HOMEASSISTANT_START from homeassistant.core import HomeAssistant
from homeassistant.core import HomeAssistant, CoreState, callback
from homeassistant.components.climate import ClimateEntity, DOMAIN as CLIMATE_DOMAIN
from homeassistant.components.select import SelectEntity from homeassistant.components.select import SelectEntity
from homeassistant.helpers.device_registry import DeviceInfo, DeviceEntryType from homeassistant.helpers.device_registry import DeviceInfo, DeviceEntryType
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.restore_state import RestoreEntity
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.entity_component import EntityComponent
from custom_components.versatile_thermostat.base_thermostat import (
ConfigData,
)
from custom_components.versatile_thermostat.vtherm_api import VersatileThermostatAPI
from custom_components.versatile_thermostat.base_thermostat import BaseThermostat
from .const import ( from .const import (
DOMAIN, DOMAIN,
DEVICE_MANUFACTURER, DEVICE_MANUFACTURER,
@@ -55,9 +56,11 @@ async def async_setup_entry(
class CentralModeSelect(SelectEntity, RestoreEntity): class CentralModeSelect(SelectEntity, RestoreEntity):
"""Representation of a Energy sensor which exposes the energy""" """Representation of the central mode choice"""
def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None: def __init__(
self, hass: HomeAssistant, unique_id: str, name: str, entry_infos: ConfigData
):
"""Initialize the energy sensor""" """Initialize the energy sensor"""
self._config_id = unique_id self._config_id = unique_id
self._device_name = entry_infos.get(CONF_NAME) self._device_name = entry_infos.get(CONF_NAME)
@@ -67,7 +70,7 @@ class CentralModeSelect(SelectEntity, RestoreEntity):
self._attr_current_option = CENTRAL_MODE_AUTO self._attr_current_option = CENTRAL_MODE_AUTO
@property @property
def icon(self) -> str | None: def icon(self) -> str:
return "mdi:form-select" return "mdi:form-select"
@property @property
@@ -90,19 +93,22 @@ class CentralModeSelect(SelectEntity, RestoreEntity):
"%s - Calling async_added_to_hass old_state is %s", self, old_state "%s - Calling async_added_to_hass old_state is %s", self, old_state
) )
if old_state is not None: if old_state is not None:
self.async_select_option = old_state.state self._attr_current_option = old_state.state
@callback api: VersatileThermostatAPI = VersatileThermostatAPI.get_vtherm_api(self.hass)
async def _async_startup_internal(*_): api.register_central_mode_select(self)
_LOGGER.debug("%s - Calling async_startup_internal", self)
await self.notify_central_mode_change()
if self.hass.state == CoreState.running: # @callback
await _async_startup_internal() # async def _async_startup_internal(*_):
else: # _LOGGER.debug("%s - Calling async_startup_internal", self)
self.hass.bus.async_listen_once( # await self.notify_central_mode_change()
EVENT_HOMEASSISTANT_START, _async_startup_internal #
) # if self.hass.state == CoreState.running:
# await _async_startup_internal()
# else:
# self.hass.bus.async_listen_once(
# EVENT_HOMEASSISTANT_START, _async_startup_internal
# )
@overrides @overrides
async def async_select_option(self, option: str) -> None: async def async_select_option(self, option: str) -> None:
@@ -116,19 +122,17 @@ class CentralModeSelect(SelectEntity, RestoreEntity):
self._attr_current_option = option self._attr_current_option = option
await self.notify_central_mode_change(old_central_mode=old_option) await self.notify_central_mode_change(old_central_mode=old_option)
async def notify_central_mode_change(self, old_central_mode=None): @overrides
"""Notify all VTherm that the central_mode have change""" def select_option(self, option: str) -> None:
# Update all VTherm states """Change the selected option"""
component: EntityComponent[ClimateEntity] = self.hass.data[CLIMATE_DOMAIN] # Update the VTherms which have temperature in central config
for entity in component.entities: self.hass.create_task(self.async_select_option(option))
if isinstance(entity, BaseThermostat):
_LOGGER.debug(
"Changing the central_mode. We have find %s to update",
entity.name,
)
await entity.check_central_mode(
self._attr_current_option, old_central_mode
)
def __str__(self): 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}" return f"VersatileThermostat-{self.name}"

View File

@@ -3,9 +3,15 @@
import logging import logging
import math import math
from homeassistant.core import HomeAssistant, callback, Event from homeassistant.core import HomeAssistant, callback, Event, CoreState
from homeassistant.const import UnitOfTime, UnitOfPower, UnitOfEnergy, PERCENTAGE from homeassistant.const import (
UnitOfTime,
UnitOfPower,
UnitOfEnergy,
PERCENTAGE,
EVENT_HOMEASSISTANT_START,
)
from homeassistant.components.sensor import ( from homeassistant.components.sensor import (
SensorEntity, SensorEntity,
@@ -16,9 +22,24 @@ from homeassistant.components.sensor import (
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.device_registry import DeviceInfo, DeviceEntryType
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.event import async_track_state_change_event
from homeassistant.components.climate import (
ClimateEntity,
DOMAIN as CLIMATE_DOMAIN,
HVACAction,
HVACMode,
)
from .base_thermostat import BaseThermostat
from .vtherm_api import VersatileThermostatAPI
from .commons import VersatileThermostatBaseEntity from .commons import VersatileThermostatBaseEntity
from .const import ( from .const import (
DOMAIN,
DEVICE_MANUFACTURER,
CONF_NAME, CONF_NAME,
CONF_DEVICE_POWER, CONF_DEVICE_POWER,
CONF_PROP_FUNCTION, CONF_PROP_FUNCTION,
@@ -28,6 +49,8 @@ from .const import (
CONF_THERMOSTAT_CLIMATE, CONF_THERMOSTAT_CLIMATE,
CONF_THERMOSTAT_TYPE, CONF_THERMOSTAT_TYPE,
CONF_THERMOSTAT_CENTRAL_CONFIG, CONF_THERMOSTAT_CENTRAL_CONFIG,
CONF_USE_CENTRAL_BOILER_FEATURE,
overrides,
) )
THRESHOLD_WATT_KILO = 100 THRESHOLD_WATT_KILO = 100
@@ -49,35 +72,43 @@ async def async_setup_entry(
name = entry.data.get(CONF_NAME) name = entry.data.get(CONF_NAME)
vt_type = entry.data.get(CONF_THERMOSTAT_TYPE) vt_type = entry.data.get(CONF_THERMOSTAT_TYPE)
entities = None
if vt_type == CONF_THERMOSTAT_CENTRAL_CONFIG: if vt_type == CONF_THERMOSTAT_CENTRAL_CONFIG:
return 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))
entities = [ if entry.data.get(CONF_PROP_FUNCTION) == PROPORTIONAL_FUNCTION_TPI:
LastTemperatureSensor(hass, unique_id, name, entry.data), entities.append(OnPercentSensor(hass, unique_id, name, entry.data))
LastExtTemperatureSensor(hass, unique_id, name, entry.data), entities.append(OnTimeSensor(hass, unique_id, name, entry.data))
TemperatureSlopeSensor(hass, unique_id, name, entry.data), entities.append(OffTimeSensor(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: if entry.data.get(CONF_THERMOSTAT_TYPE) == CONF_THERMOSTAT_VALVE:
entities.append(OnPercentSensor(hass, unique_id, name, entry.data)) entities.append(ValveOpenPercentSensor(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: if entry.data.get(CONF_THERMOSTAT_TYPE) == CONF_THERMOSTAT_CLIMATE:
entities.append(ValveOpenPercentSensor(hass, unique_id, name, entry.data)) entities.append(
RegulatedTemperatureSensor(hass, unique_id, name, entry.data)
)
if entry.data.get(CONF_THERMOSTAT_TYPE) == CONF_THERMOSTAT_CLIMATE: if entities:
entities.append(RegulatedTemperatureSensor(hass, unique_id, name, entry.data)) async_add_entities(entities, True)
async_add_entities(entities, True)
class EnergySensor(VersatileThermostatBaseEntity, SensorEntity): class EnergySensor(VersatileThermostatBaseEntity, SensorEntity):
@@ -94,15 +125,15 @@ class EnergySensor(VersatileThermostatBaseEntity, SensorEntity):
"""Called when my climate have change""" """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(self.my_climate.total_energy) or math.isinf( energy = self.my_climate.total_energy
self.my_climate.total_energy if energy is None:
): return
if math.isnan(energy) or math.isinf(energy):
raise ValueError(f"Sensor has illegal state {self.my_climate.total_energy}") raise ValueError(f"Sensor has illegal state {self.my_climate.total_energy}")
old_state = self._attr_native_value old_state = self._attr_native_value
self._attr_native_value = round( self._attr_native_value = round(energy, self.suggested_display_precision)
self.my_climate.total_energy, self.suggested_display_precision
)
if old_state != self._attr_native_value: if old_state != self._attr_native_value:
self.async_write_ha_state() self.async_write_ha_state()
return return
@@ -253,7 +284,7 @@ class ValveOpenPercentSensor(VersatileThermostatBaseEntity, SensorEntity):
def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None: def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None:
"""Initialize the energy sensor""" """Initialize the energy sensor"""
super().__init__(hass, unique_id, entry_infos.get(CONF_NAME)) super().__init__(hass, unique_id, entry_infos.get(CONF_NAME))
self._attr_name = "Vave open percent" self._attr_name = "Valve open percent"
self._attr_unique_id = f"{self._device_name}_valve_open_percent" self._attr_unique_id = f"{self._device_name}_valve_open_percent"
@callback @callback
@@ -539,7 +570,7 @@ class RegulatedTemperatureSensor(VersatileThermostatBaseEntity, SensorEntity):
@property @property
def native_unit_of_measurement(self) -> str | None: def native_unit_of_measurement(self) -> str | None:
if not self.my_climate: if not self.my_climate:
return UnitOfTemperature.CELSIUS return self.hass.config.units.temperature_unit
return self.my_climate.temperature_unit return self.my_climate.temperature_unit
@property @property
@@ -590,10 +621,123 @@ class EMATemperatureSensor(VersatileThermostatBaseEntity, SensorEntity):
@property @property
def native_unit_of_measurement(self) -> str | None: def native_unit_of_measurement(self) -> str | None:
if not self.my_climate: if not self.my_climate:
return UnitOfTemperature.CELSIUS return self.hass.config.units.temperature_unit
return self.my_climate.temperature_unit return self.my_climate.temperature_unit
@property @property
def suggested_display_precision(self) -> int | None: def suggested_display_precision(self) -> int | None:
"""Return the suggested number of decimal digits for display.""" """Return the suggested number of decimal digits for display."""
return 2 return 2
class NbActiveDeviceForBoilerSensor(SensorEntity):
"""Representation of the threshold of the number of VTherm
which should be active to activate the boiler"""
def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None:
"""Initialize the energy sensor"""
self._hass = hass
self._config_id = unique_id
self._device_name = entry_infos.get(CONF_NAME)
self._attr_name = "Nb device active for boiler"
self._attr_unique_id = "nb_device_active_boiler"
self._attr_value = self._attr_native_value = None # default value
self._entities = []
@property
def icon(self) -> str | None:
return "mdi:heat-wave"
@property
def device_info(self) -> DeviceInfo:
"""Return the device info."""
return DeviceInfo(
entry_type=DeviceEntryType.SERVICE,
identifiers={(DOMAIN, self._config_id)},
name=self._device_name,
manufacturer=DEVICE_MANUFACTURER,
model=DOMAIN,
)
@property
def state_class(self) -> SensorStateClass | None:
return SensorStateClass.MEASUREMENT
@property
def suggested_display_precision(self) -> int | None:
"""Return the suggested number of decimal digits for display."""
return 0
@overrides
async def async_added_to_hass(self) -> None:
await super().async_added_to_hass()
api: VersatileThermostatAPI = VersatileThermostatAPI.get_vtherm_api(self._hass)
api.register_nb_device_active_boiler(self)
@callback
async def _async_startup_internal(*_):
_LOGGER.debug("%s - Calling async_startup_internal", self)
await self.listen_vtherms_entities()
if self.hass.state == CoreState.running:
await _async_startup_internal()
else:
self.hass.bus.async_listen_once(
EVENT_HOMEASSISTANT_START, _async_startup_internal
)
async def listen_vtherms_entities(self):
"""Initialize the listening of state change of VTherms"""
# Listen to all VTherm state change
self._entities = []
underlying_entities_id = []
component: EntityComponent[ClimateEntity] = self.hass.data[CLIMATE_DOMAIN]
for entity in component.entities:
if isinstance(entity, BaseThermostat) and entity.is_used_by_central_boiler:
self._entities.append(entity)
for under in entity.underlying_entities:
underlying_entities_id.append(under.entity_id)
if len(underlying_entities_id) > 0:
# Arme l'écoute de la première entité
listener_cancel = async_track_state_change_event(
self._hass,
underlying_entities_id,
self.calculate_nb_active_devices,
)
_LOGGER.info(
"%s - the underlyings that could controls the central boiler are %s",
self,
underlying_entities_id,
)
self.async_on_remove(listener_cancel)
else:
_LOGGER.debug("%s - no VTherm could controls the central boiler", self)
await self.calculate_nb_active_devices(None)
async def calculate_nb_active_devices(self, _):
"""Calculate the number of active VTherm that have an
influence on central boiler"""
_LOGGER.debug("%s - calculating the number of active VTherm", self)
nb_active = 0
for entity in self._entities:
_LOGGER.debug(
"Examining the hvac_action of %s",
entity.name,
)
if (
entity.hvac_mode in [HVACMode.HEAT, HVACMode.AUTO]
and entity.hvac_action == HVACAction.HEATING
):
for under in entity.underlying_entities:
nb_active += 1 if under.is_device_active else 0
self._attr_native_value = nb_active
self.async_write_ha_state()
def __str__(self):
return f"VersatileThermostat-{self.name}"

View File

@@ -12,29 +12,58 @@
"thermostat_type": "Only one central configuration type is possible" "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",
"finalize": "All done",
"configuration_not_complete": "Configuration not complete"
}
},
"main": { "main": {
"title": "Add new Versatile Thermostat", "title": "Add new Versatile Thermostat",
"description": "Main mandatory attributes", "description": "Main mandatory attributes",
"data": { "data": {
"name": "Name", "name": "Name",
"thermostat_type": "Thermostat type", "thermostat_type": "Thermostat type",
"temperature_sensor_entity_id": "Temperature sensor entity id", "temperature_sensor_entity_id": "Room temperature",
"last_seen_temperature_sensor_entity_id": "Last seen room temperature datetime",
"external_temperature_sensor_entity_id": "Outdoor temperature sensor entity id", "external_temperature_sensor_entity_id": "Outdoor temperature sensor entity id",
"cycle_min": "Cycle duration (minutes)", "cycle_min": "Cycle duration (minutes)",
"temp_min": "Minimal temperature allowed", "temp_min": "Minimum temperature allowed",
"temp_max": "Maximal temperature allowed", "temp_max": "Maximum temperature allowed",
"step_temperature": "Temperature step",
"device_power": "Device power", "device_power": "Device power",
"use_central_mode": "Enable the control by central mode ('central_mode')", "use_central_mode": "Enable the control by central entity (requires central config). Check to enable the control of the VTherm with the select central_mode entities.",
"use_main_central_config": "Use additional central main configuration. Check to use the central main configuration (outdoor temperature, min, max, step, ...).",
"used_by_controls_central_boiler": "Used by central boiler. Check if this VTherm should have control on the central boiler"
},
"data_description": {
"temperature_sensor_entity_id": "Room temperature sensor entity id",
"last_seen_temperature_sensor_entity_id": "Last seen room temperature sensor entity id. Should be datetime sensor",
"external_temperature_sensor_entity_id": "Outdoor temperature sensor entity id. Not used if central configuration is selected"
}
},
"features": {
"title": "Features",
"description": "Thermostat features",
"data": {
"use_window_feature": "Use window detection", "use_window_feature": "Use window detection",
"use_motion_feature": "Use motion detection", "use_motion_feature": "Use motion detection",
"use_power_feature": "Use power management", "use_power_feature": "Use power management",
"use_presence_feature": "Use presence detection", "use_presence_feature": "Use presence detection",
"use_main_central_config": "Use central main configuration" "use_central_boiler_feature": "Use a central boiler. Check to add a control to your central boiler. You will have to configure the VTherm which will have a control of the central boiler after seecting this checkbox to take effect. If one VTherm requires heating, the boiler will be turned on. If no VTherm requires heating, the boiler will be turned off. Commands for turning on/off the central boiler are given in the related configuration page"
},
"data_description": {
"use_central_mode": "Check to enable the control of the VTherm with the select central_mode entities",
"use_main_central_config": "Check to use the central main configuration. Uncheck to use a specific main configuration for this VTherm",
"external_temperature_sensor_entity_id": "Outdoor temperature sensor entity id. Not used if central configuration is selected"
} }
}, },
"type": { "type": {
@@ -45,6 +74,7 @@
"heater_entity2_id": "2nd heater switch", "heater_entity2_id": "2nd heater switch",
"heater_entity3_id": "3rd heater switch", "heater_entity3_id": "3rd heater switch",
"heater_entity4_id": "4th heater switch", "heater_entity4_id": "4th heater switch",
"heater_keep_alive": "Switch keep-alive interval in seconds",
"proportional_function": "Algorithm", "proportional_function": "Algorithm",
"climate_entity_id": "1st underlying climate", "climate_entity_id": "1st underlying climate",
"climate_entity2_id": "2nd underlying climate", "climate_entity2_id": "2nd underlying climate",
@@ -57,15 +87,17 @@
"valve_entity4_id": "4th valve number", "valve_entity4_id": "4th valve number",
"auto_regulation_mode": "Self-regulation", "auto_regulation_mode": "Self-regulation",
"auto_regulation_dtemp": "Regulation threshold", "auto_regulation_dtemp": "Regulation threshold",
"auto_regulation_periode_min": "Regulation minimal period", "auto_regulation_periode_min": "Regulation minimum period",
"auto_regulation_use_device_temp": "Use internal temperature of the underlying",
"inverse_switch_command": "Inverse switch command", "inverse_switch_command": "Inverse switch command",
"auto_fan_mode": " Auto fan mode" "auto_fan_mode": "Auto fan mode"
}, },
"data_description": { "data_description": {
"heater_entity_id": "Mandatory heater entity id", "heater_entity_id": "Mandatory heater entity id",
"heater_entity2_id": "Optional 2nd Heater entity id. Leave empty if not used", "heater_entity2_id": "Optional 2nd Heater entity id. Leave empty if not required",
"heater_entity3_id": "Optional 3rd Heater entity id. Leave empty if not used", "heater_entity3_id": "Optional 3rd Heater entity id. Leave empty if not required",
"heater_entity4_id": "Optional 4th Heater entity id. Leave empty if not used", "heater_entity4_id": "Optional 4th Heater entity id. Leave empty if not required",
"heater_keep_alive": "Optional heater switch state refresh interval. Leave empty if not required.",
"proportional_function": "Algorithm to use (TPI is the only one for now)", "proportional_function": "Algorithm to use (TPI is the only one for now)",
"climate_entity_id": "Underlying climate entity id", "climate_entity_id": "Underlying climate entity id",
"climate_entity2_id": "2nd underlying climate entity id", "climate_entity2_id": "2nd underlying climate entity id",
@@ -77,10 +109,11 @@
"valve_entity3_id": "3rd valve number entity id", "valve_entity3_id": "3rd valve number entity id",
"valve_entity4_id": "4th valve number entity id", "valve_entity4_id": "4th valve number entity id",
"auto_regulation_mode": "Auto adjustment of the target temperature", "auto_regulation_mode": "Auto adjustment of the target temperature",
"auto_regulation_dtemp": "The threshold in ° under which the temperature change will not be sent", "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_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", "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" "auto_fan_mode": "Automatically activate fan when huge heating/cooling is necessary"
} }
}, },
"tpi": { "tpi": {
@@ -99,51 +132,36 @@
}, },
"presets": { "presets": {
"title": "Presets", "title": "Presets",
"description": "For each preset set the target temperature (0 to ignore preset)", "description": "Select if the thermostat will use central preset - deselect for the thermostat to have its own presets",
"data": { "data": {
"eco_temp": "Eco preset",
"comfort_temp": "Comfort preset",
"boost_temp": "Boost preset",
"frost_temp": "Frost protection preset",
"eco_ac_temp": "Eco preset for AC mode",
"comfort_ac_temp": "Comfort preset for AC mode",
"boost_ac_temp": "Boost preset for AC mode",
"use_presets_central_config": "Use central presets configuration" "use_presets_central_config": "Use central presets configuration"
},
"data_description": {
"eco_temp": "Temperature in Eco preset",
"comfort_temp": "Temperature in Comfort preset",
"boost_temp": "Temperature in Boost preset",
"frost_temp": "Temperature in Frost protection preset",
"eco_ac_temp": "Temperature in Eco preset for AC mode",
"comfort_ac_temp": "Temperature in Comfort preset for AC mode",
"boost_ac_temp": "Temperature in Boost preset for AC mode",
"use_presets_central_config": "Check to use the central presets configuration. Uncheck to use a specific presets configuration for this VTherm"
} }
}, },
"window": { "window": {
"title": "Window management", "title": "Window management",
"description": "Open window management.\nLeave corresponding entity_id empty if not used\nYou can also configure automatic window open detection based on temperature decrease", "description": "Open window management.\nYou can also configure automatic window open detection based on temperature decrease",
"data": { "data": {
"window_sensor_entity_id": "Window sensor entity id", "window_sensor_entity_id": "Window sensor entity id",
"window_delay": "Window sensor delay (seconds)", "window_delay": "Window sensor delay (seconds)",
"window_auto_open_threshold": "Temperature decrease threshold for automatic window open detection (in °/hours)", "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_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)", "window_auto_max_duration": "Maximum duration of automatic window open detection (in min)",
"use_window_central_config": "Use central window configuration" "use_window_central_config": "Use central window configuration",
"window_action": "Action"
}, },
"data_description": { "data_description": {
"window_sensor_entity_id": "Leave empty if no window sensor should be used", "window_sensor_entity_id": "Leave empty if no window sensor should be used and to use the automatic detection",
"window_delay": "The delay in seconds before sensor detection is taken into account", "window_delay": "The delay in seconds before sensor detection is taken into account",
"window_auto_open_threshold": "Recommended value: between 3 and 10. Leave empty if automatic window open detection is not used", "window_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_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", "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" "use_window_central_config": "Select to use the central window configuration. Deselect to use a specific window configuration for this VTherm",
"window_action": "Action to perform if window is deteted as open"
} }
}, },
"motion": { "motion": {
"title": "Motion management", "title": "Motion management",
"description": "Motion sensor management. Preset can switch automatically depending on motion detection\nLeave corresponding entity_id empty if not used.\nmotion_preset and no_motion_preset should be set to the corresponding preset name", "description": "Motion sensor management. Preset can switch automatically depending on motion detection\nmotion_preset and no_motion_preset should be set to the corresponding preset name",
"data": { "data": {
"motion_sensor_entity_id": "Motion sensor entity id", "motion_sensor_entity_id": "Motion sensor entity id",
"motion_delay": "Activation delay", "motion_delay": "Activation delay",
@@ -163,7 +181,7 @@
}, },
"power": { "power": {
"title": "Power management", "title": "Power management",
"description": "Power management attributes.\nGives the power and max power sensor of your home.\nThen specify the power consumption of the heater when on.\nAll sensors and device power should have the same unit (kW or W).\nLeave corresponding entity_id empty if not used.", "description": "Power management attributes.\nGives the power and max power sensor of your home.\nSpecify the power consumption of the heater when on.\nAll sensors and device power should use the same unit (kW or W).",
"data": { "data": {
"power_sensor_entity_id": "Power", "power_sensor_entity_id": "Power",
"max_power_sensor_entity_id": "Max power", "max_power_sensor_entity_id": "Max power",
@@ -179,44 +197,29 @@
}, },
"presence": { "presence": {
"title": "Presence management", "title": "Presence management",
"description": "Presence management attributes.\nGives the a presence sensor of your home (true is someone is present).\nThen specify either the preset to use when presence sensor is false or the offset in temperature to apply.\nIf preset is given, the offset will not be used.\nLeave corresponding entity_id empty if not used.", "description": "Presence management attributes.\nGives the a presence sensor of your home (true is someone is present) and give the corresponding temperature preset setting.",
"data": { "data": {
"presence_sensor_entity_id": "Presence sensor", "presence_sensor_entity_id": "Presence sensor",
"eco_away_temp": "Eco preset", "use_presence_central_config": "Use central presence temperature configuration. Deselect to use specific temperature entities"
"comfort_away_temp": "Comfort preset",
"boost_away_temp": "Boost preset",
"frost_away_temp": "Frost protection preset",
"eco_ac_away_temp": "Eco preset in AC mode",
"comfort_ac_away_temp": "Comfort preset in AC mode",
"boost_ac_away_temp": "Boost pres et in AC mode",
"use_presence_central_config": "Use central presence configuration"
}, },
"data_description": { "data_description": {
"presence_sensor_entity_id": "Presence sensor entity id", "presence_sensor_entity_id": "Presence sensor entity id"
"eco_away_temp": "Temperature in Eco preset when no presence",
"comfort_away_temp": "Temperature in Comfort preset when no presence",
"boost_away_temp": "Temperature in Boost preset when no presence",
"frost_away_temp": "Temperature in Frost protection 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",
"use_presence_central_config": "Check to use the central presence configuration. Uncheck to use a specific presence configuration for this VTherm"
} }
}, },
"advanced": { "advanced": {
"title": "Advanced parameters", "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.", "description": "Configuration of advanced parameters. Leave the default values if you don't know what you are doing.\nThese parameters can lead to very poor temperature control or bad power regulation.",
"data": { "data": {
"minimal_activation_delay": "Minimal activation delay", "minimal_activation_delay": "Minimum activation delay",
"security_delay_min": "Safety delay (in minutes)", "security_delay_min": "Safety delay (in minutes)",
"security_min_on_percent": "Minimal power percent to enable safety mode", "security_min_on_percent": "Minimum power percent to enable safety mode",
"security_default_on_percent": "Power percent to use in safety mode", "security_default_on_percent": "Power percent to use in safety mode",
"use_advanced_central_config": "Use central advanced configuration" "use_advanced_central_config": "Use central advanced configuration"
}, },
"data_description": { "data_description": {
"minimal_activation_delay": "Delay in seconds under which the equipment will not be activated", "minimal_activation_delay": "Delay in seconds under which the equipment will not be activated",
"security_delay_min": "Maximum allowed delay in minutes between two temperature measurements. Above this delay the thermostat will turn to a safety off state", "security_delay_min": "Maximum allowed delay in minutes between two temperature measurements. Above this delay the thermostat will turn to a safety off state",
"security_min_on_percent": "Minimal heating percent value for safety preset activation. Below this amount of power percent the thermostat won't go into safety preset", "security_min_on_percent": "Minimum heating percent value for safety preset activation. Below this amount of power percent the thermostat won't go into safety preset",
"security_default_on_percent": "The default heating power percent value in safety preset. Set to 0 to switch off heater in safety preset", "security_default_on_percent": "The default heating power percent value in safety preset. Set to 0 to switch off heater in safety preset",
"use_advanced_central_config": "Check to use the central advanced configuration. Uncheck to use a specific advanced configuration for this VTherm" "use_advanced_central_config": "Check to use the central advanced configuration. Uncheck to use a specific advanced configuration for this VTherm"
} }
@@ -226,7 +229,7 @@
"unknown": "Unexpected error", "unknown": "Unexpected error",
"unknown_entity": "Unknown entity id", "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", "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." "no_central_config": "You cannot select 'use central configuration' because no central configuration was found. You need to create a Versatile Thermostat of type 'Central Configuration' to use it."
}, },
"abort": { "abort": {
"already_configured": "Device is already configured" "already_configured": "Device is already configured"
@@ -244,29 +247,58 @@
"thermostat_type": "Only one central configuration type is possible" "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",
"finalize": "All done",
"configuration_not_complete": "Configuration not complete"
}
},
"main": { "main": {
"title": "Main - {name}", "title": "Main - {name}",
"description": "Main mandatory attributes", "description": "Main mandatory attributes",
"data": { "data": {
"name": "Name", "name": "Name",
"thermostat_type": "Thermostat type", "thermostat_type": "Thermostat type",
"temperature_sensor_entity_id": "Room temperature sensor entity id", "temperature_sensor_entity_id": "Room temperature",
"last_seen_temperature_sensor_entity_id": "Last seen room temperature datetime",
"external_temperature_sensor_entity_id": "Outdoor temperature sensor entity id", "external_temperature_sensor_entity_id": "Outdoor temperature sensor entity id",
"cycle_min": "Cycle duration (minutes)", "cycle_min": "Cycle duration (minutes)",
"temp_min": "Minimal temperature allowed", "temp_min": "Minimum temperature allowed",
"temp_max": "Maximal temperature allowed", "temp_max": "Maximum temperature allowed",
"step_temperature": "Temperature step",
"device_power": "Device power", "device_power": "Device power",
"use_central_mode": "Enable the control by central mode ('central_mode')", "use_central_mode": "Enable the control by central entity (requires central config). Check to enable the control of the VTherm with the select central_mode entities.",
"use_main_central_config": "Use additional central main configuration. Check to use the central main configuration (outdoor temperature, min, max, step, ...).",
"used_by_controls_central_boiler": "Used by central boiler. Check if this VTherm should have control on the central boiler"
},
"data_description": {
"temperature_sensor_entity_id": "Room temperature sensor entity id",
"last_seen_temperature_sensor_entity_id": "Last seen room temperature sensor entity id. Should be datetime sensor",
"external_temperature_sensor_entity_id": "Outdoor temperature sensor entity id. Not used if central configuration is selected"
}
},
"features": {
"title": "Features - {name}",
"description": "Thermostat features",
"data": {
"use_window_feature": "Use window detection", "use_window_feature": "Use window detection",
"use_motion_feature": "Use motion detection", "use_motion_feature": "Use motion detection",
"use_power_feature": "Use power management", "use_power_feature": "Use power management",
"use_presence_feature": "Use presence detection", "use_presence_feature": "Use presence detection",
"use_main_central_config": "Use central main configuration" "use_central_boiler_feature": "Use a central boiler. Check to add a control to your central boiler. You will have to configure the VTherm which will have a control of the central boiler after seecting this checkbox to take effect. If one VTherm requires heating, the boiler will be turned on. If no VTherm requires heating, the boiler will be turned off. Commands for turning on/off the central boiler are given in the related configuration page"
},
"data_description": {
"use_central_mode": "Check to enable the control of the VTherm with the select central_mode entities",
"use_main_central_config": "Check to use the central main configuration. Uncheck to use a specific configuration for this VTherm",
"external_temperature_sensor_entity_id": "Outdoor temperature sensor entity id. Not used if central configuration is selected"
} }
}, },
"type": { "type": {
@@ -277,6 +309,7 @@
"heater_entity2_id": "2nd heater switch", "heater_entity2_id": "2nd heater switch",
"heater_entity3_id": "3rd heater switch", "heater_entity3_id": "3rd heater switch",
"heater_entity4_id": "4th heater switch", "heater_entity4_id": "4th heater switch",
"heater_keep_alive": "Switch keep-alive interval in seconds",
"proportional_function": "Algorithm", "proportional_function": "Algorithm",
"climate_entity_id": "1st underlying climate", "climate_entity_id": "1st underlying climate",
"climate_entity2_id": "2nd underlying climate", "climate_entity2_id": "2nd underlying climate",
@@ -289,15 +322,17 @@
"valve_entity4_id": "4th valve number", "valve_entity4_id": "4th valve number",
"auto_regulation_mode": "Self-regulation", "auto_regulation_mode": "Self-regulation",
"auto_regulation_dtemp": "Regulation threshold", "auto_regulation_dtemp": "Regulation threshold",
"auto_regulation_periode_min": "Regulation minimal period", "auto_regulation_periode_min": "Regulation minimum period",
"auto_regulation_use_device_temp": "Use internal temperature of the underlying",
"inverse_switch_command": "Inverse switch command", "inverse_switch_command": "Inverse switch command",
"auto_fan_mode": " Auto fan mode" "auto_fan_mode": "Auto fan mode"
}, },
"data_description": { "data_description": {
"heater_entity_id": "Mandatory heater entity id", "heater_entity_id": "Mandatory heater entity id",
"heater_entity2_id": "Optional 2nd Heater entity id. Leave empty if not used", "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_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", "heater_entity4_id": "Optional 4th Heater entity id. Leave empty if not used",
"heater_keep_alive": "Optional heater switch state refresh interval. Leave empty if not required.",
"proportional_function": "Algorithm to use (TPI is the only one for now)", "proportional_function": "Algorithm to use (TPI is the only one for now)",
"climate_entity_id": "Underlying climate entity id", "climate_entity_id": "Underlying climate entity id",
"climate_entity2_id": "2nd underlying climate entity id", "climate_entity2_id": "2nd underlying climate entity id",
@@ -309,10 +344,11 @@
"valve_entity3_id": "3rd valve number entity id", "valve_entity3_id": "3rd valve number entity id",
"valve_entity4_id": "4th valve number entity id", "valve_entity4_id": "4th valve number entity id",
"auto_regulation_mode": "Auto adjustment of the target temperature", "auto_regulation_mode": "Auto adjustment of the target temperature",
"auto_regulation_dtemp": "The threshold in ° under which the temperature change will not be sent", "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_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 invert the command", "inverse_switch_command": "For switch with pilot wire and diode you may need to invert the command",
"auto_fan_mode": " Automatically activate fan when huge heating/cooling is necessary" "auto_fan_mode": "Automatically activate fan when huge heating/cooling is necessary"
} }
}, },
"tpi": { "tpi": {
@@ -331,26 +367,9 @@
}, },
"presets": { "presets": {
"title": "Presets - {name}", "title": "Presets - {name}",
"description": "For each preset set the target temperature (0 to ignore preset)", "description": "Check if the thermostat will use central presets. Uncheck and the thermostat will have its own preset entities",
"data": { "data": {
"eco_temp": "Eco preset",
"comfort_temp": "Comfort preset",
"boost_temp": "Boost preset",
"frost_temp": "Frost protection preset",
"eco_ac_temp": "Eco preset for AC mode",
"comfort_ac_temp": "Comfort preset for AC mode",
"boost_ac_temp": "Boost preset for AC mode",
"use_presets_central_config": "Use central presets configuration" "use_presets_central_config": "Use central presets configuration"
},
"data_description": {
"eco_temp": "Temperature in Eco preset",
"comfort_temp": "Temperature in Comfort preset",
"boost_temp": "Temperature in Boost preset",
"frost_temp": "Temperature in Frost protection preset",
"eco_ac_temp": "Temperature in Eco preset for AC mode",
"comfort_ac_temp": "Temperature in Comfort preset for AC mode",
"boost_ac_temp": "Temperature in Boost preset for AC mode",
"use_presets_central_config": "Check to use the central presets configuration. Uncheck to use a specific presets configuration for this VTherm"
} }
}, },
"window": { "window": {
@@ -362,20 +381,22 @@
"window_auto_open_threshold": "Temperature decrease threshold for automatic window open detection (in °/hours)", "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_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)", "window_auto_max_duration": "Maximum duration of automatic window open detection (in min)",
"use_window_central_config": "Use central window configuration" "use_window_central_config": "Use central window configuration",
"window_action": "Action"
}, },
"data_description": { "data_description": {
"window_sensor_entity_id": "Leave empty if no window sensor should be used", "window_sensor_entity_id": "Leave empty if no window sensor should be used and to use the automatic detection",
"window_delay": "The delay in seconds before sensor detection is taken into account", "window_delay": "The delay in seconds before sensor detection is taken into account",
"window_auto_open_threshold": "Recommended value: between 3 and 10. Leave empty if automatic window open detection is not used", "window_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_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", "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" "use_window_central_config": "Check to use the central window configuration. Uncheck to use a specific window configuration for this VTherm",
"window_action": "Action to do if window is deteted as open"
} }
}, },
"motion": { "motion": {
"title": "Motion - {name}", "title": "Motion - {name}",
"description": "Motion management. Preset can switch automatically depending of a motion detection\nLeave corresponding entity_id empty if not used.\nmotion_preset and no_motion_preset should be set to the corresponding preset name", "description": "Motion management. Preset can switch automatically depending of a motion detection\nmotion_preset and no_motion_preset should be set to the corresponding preset name",
"data": { "data": {
"motion_sensor_entity_id": "Motion sensor entity id", "motion_sensor_entity_id": "Motion sensor entity id",
"motion_delay": "Activation delay", "motion_delay": "Activation delay",
@@ -395,7 +416,7 @@
}, },
"power": { "power": {
"title": "Power - {name}", "title": "Power - {name}",
"description": "Power management attributes.\nGives the power and max power sensor of your home.\nThen specify the power consumption of the heater when on.\nAll sensors and device power should have the same unit (kW or W).\nLeave corresponding entity_id empty if not used.", "description": "Power management attributes.\nGives the power and max power sensor of your home.\nThen specify the power consumption of the heater when on.\nAll sensors and device power should have the same unit (kW or W).",
"data": { "data": {
"power_sensor_entity_id": "Power", "power_sensor_entity_id": "Power",
"max_power_sensor_entity_id": "Max power", "max_power_sensor_entity_id": "Max power",
@@ -411,44 +432,29 @@
}, },
"presence": { "presence": {
"title": "Presence - {name}", "title": "Presence - {name}",
"description": "Presence management attributes.\nGives the a presence sensor of your home (true is someone is present).\nThen specify either the preset to use when presence sensor is false or the offset in temperature to apply.\nIf preset is given, the offset will not be used.\nLeave corresponding entity_id empty if not used.", "description": "Presence management attributes.\nGives the a presence sensor of your home (true is someone is present) and give the corresponding temperature preset setting.",
"data": { "data": {
"presence_sensor_entity_id": "Presence sensor", "presence_sensor_entity_id": "Presence sensor",
"eco_away_temp": "Eco away preset", "use_presence_central_config": "Use central presence temperature configuration. Uncheck to use specific temperature entities"
"comfort_away_temp": "Comfort away preset",
"boost_away_temp": "Boost away preset",
"frost_away_temp": "Frost protection preset",
"eco_ac_away_temp": "Eco away preset in AC mode",
"comfort_ac_away_temp": "Comfort away preset in AC mode",
"boost_ac_away_temp": "Boost away preset in AC mode",
"use_presence_central_config": "Use central presence configuration"
}, },
"data_description": { "data_description": {
"presence_sensor_entity_id": "Presence sensor entity id", "presence_sensor_entity_id": "Presence sensor entity id"
"eco_away_temp": "Temperature in Eco preset when no presence",
"comfort_away_temp": "Temperature in Comfort preset when no presence",
"boost_away_temp": "Temperature in Boost preset when no presence",
"frost_away_temp": "Temperature in Frost protection 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",
"use_presence_central_config": "Check to use the central presence configuration. Uncheck to use a specific presence configuration for this VTherm"
} }
}, },
"advanced": { "advanced": {
"title": "Advanced - {name}", "title": "Advanced - {name}",
"description": "Advanced parameters. Leave the default values if you don't know what you are doing.\nThese parameters can lead to very poor temperature control or bad power regulation.", "description": "Advanced parameters. Leave the default values if you don't know what you are doing.\nThese parameters can lead to very poor temperature control or bad power regulation.",
"data": { "data": {
"minimal_activation_delay": "Minimal activation delay", "minimal_activation_delay": "Minimum activation delay",
"security_delay_min": "Safety delay (in minutes)", "security_delay_min": "Safety delay (in minutes)",
"security_min_on_percent": "Minimal power percent to enable safety mode", "security_min_on_percent": "Minimum power percent to enable safety mode",
"security_default_on_percent": "Power percent to use in safety mode", "security_default_on_percent": "Power percent to use in safety mode",
"use_advanced_central_config": "Use central advanced configuration" "use_advanced_central_config": "Use central advanced configuration"
}, },
"data_description": { "data_description": {
"minimal_activation_delay": "Delay in seconds under which the equipment will not be activated", "minimal_activation_delay": "Delay in seconds under which the equipment will not be activated",
"security_delay_min": "Maximum allowed delay in minutes between two temperature measurements. Above this delay the thermostat will turn to a safety off state", "security_delay_min": "Maximum allowed delay in minutes between two temperature measurements. Above this delay the thermostat will turn to a safety off state",
"security_min_on_percent": "Minimal heating percent value for safety preset activation. Below this amount of power percent the thermostat won't go into safety preset", "security_min_on_percent": "Minimum heating percent value for safety preset activation. Below this amount of power percent the thermostat won't go into safety preset",
"security_default_on_percent": "The default heating power percent value in safety preset. Set to 0 to switch off heater in safety preset", "security_default_on_percent": "The default heating power percent value in safety preset. Set to 0 to switch off heater in safety preset",
"use_advanced_central_config": "Check to use the central advanced configuration. Uncheck to use a specific advanced configuration for this VTherm" "use_advanced_central_config": "Check to use the central advanced configuration. Uncheck to use a specific advanced configuration for this VTherm"
} }
@@ -458,7 +464,8 @@
"unknown": "Unexpected error", "unknown": "Unexpected error",
"unknown_entity": "Unknown entity id", "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", "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." "no_central_config": "You cannot check 'use central configuration' because no central configuration was found. You need to create a Versatile Thermostat of type 'Central Configuration' to use it.",
"service_configuration_format": "The format of the service configuration is wrong"
}, },
"abort": { "abort": {
"already_configured": "Device is already configured" "already_configured": "Device is already configured"
@@ -491,6 +498,22 @@
"auto_fan_high": "High", "auto_fan_high": "High",
"auto_fan_turbo": "Turbo" "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"
}
} }
}, },
"entity": { "entity": {
@@ -506,6 +529,53 @@
} }
} }
} }
},
"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"
}
} }
} }
} }

View File

@@ -3,12 +3,12 @@
import logging import logging
from datetime import timedelta, datetime from datetime import timedelta, datetime
from homeassistant.core import HomeAssistant, callback from homeassistant.core import Event, HomeAssistant, State, callback
from homeassistant.helpers.event import ( from homeassistant.helpers.event import (
async_track_state_change_event, async_track_state_change_event,
async_track_time_interval, async_track_time_interval,
EventStateChangedData,
) )
from homeassistant.components.climate import ( from homeassistant.components.climate import (
HVACAction, HVACAction,
HVACMode, HVACMode,
@@ -16,7 +16,7 @@ from homeassistant.components.climate import (
) )
from .commons import NowClass, round_to_nearest from .commons import NowClass, round_to_nearest
from .base_thermostat import BaseThermostat from .base_thermostat import BaseThermostat, ConfigData
from .pi_algorithm import PITemperatureRegulator from .pi_algorithm import PITemperatureRegulator
from .const import ( from .const import (
@@ -35,6 +35,7 @@ from .const import (
CONF_AUTO_REGULATION_EXPERT, CONF_AUTO_REGULATION_EXPERT,
CONF_AUTO_REGULATION_DTEMP, CONF_AUTO_REGULATION_DTEMP,
CONF_AUTO_REGULATION_PERIOD_MIN, CONF_AUTO_REGULATION_PERIOD_MIN,
CONF_AUTO_REGULATION_USE_DEVICE_TEMP,
CONF_AUTO_FAN_MODE, CONF_AUTO_FAN_MODE,
CONF_AUTO_FAN_NONE, CONF_AUTO_FAN_NONE,
CONF_AUTO_FAN_LOW, CONF_AUTO_FAN_LOW,
@@ -56,22 +57,22 @@ from .underlyings import UnderlyingClimate
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
class ThermostatOverClimate(BaseThermostat): class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
"""Representation of a base class for a Versatile Thermostat over a climate""" """Representation of a base class for a Versatile Thermostat over a climate"""
_auto_regulation_mode: str = None _auto_regulation_mode: str | None = None
_regulation_algo = None _regulation_algo = None
_regulated_target_temp: float = None _regulated_target_temp: float | None = None
_auto_regulation_dtemp: float = None _auto_regulation_dtemp: float | None = None
_auto_regulation_period_min: int = None _auto_regulation_period_min: int | None = None
_last_regulation_change: datetime = None _last_regulation_change: datetime | None = None
# The fan mode configured in configEntry # The fan mode configured in configEntry
_auto_fan_mode: str = None _auto_fan_mode: str | None = None
# The current fan mode (could be change by service call) # The current fan mode (could be change by service call)
_current_auto_fan_mode: str = None _current_auto_fan_mode: str | None = None
# The fan_mode name depending of the current_mode # The fan_mode name depending of the current_mode
_auto_activated_fan_mode: str = None _auto_activated_fan_mode: str | None = None
_auto_deactivated_fan_mode: str = None _auto_deactivated_fan_mode: str | None = None
_entity_component_unrecorded_attributes = ( _entity_component_unrecorded_attributes = (
BaseThermostat._entity_component_unrecorded_attributes.union( BaseThermostat._entity_component_unrecorded_attributes.union(
@@ -89,12 +90,15 @@ class ThermostatOverClimate(BaseThermostat):
"current_auto_fan_mode", "current_auto_fan_mode",
"auto_activated_fan_mode", "auto_activated_fan_mode",
"auto_deactivated_fan_mode", "auto_deactivated_fan_mode",
"auto_regulation_use_device_temp",
} }
) )
) )
) )
def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None: def __init__(
self, hass: HomeAssistant, unique_id: str, name: str, entry_infos: ConfigData
):
"""Initialize the thermostat over switch.""" """Initialize the thermostat over switch."""
# super.__init__ calls post_init at the end. So it must be called after regulation initialization # super.__init__ calls post_init at the end. So it must be called after regulation initialization
super().__init__(hass, unique_id, name, entry_infos) super().__init__(hass, unique_id, name, entry_infos)
@@ -127,7 +131,7 @@ class ThermostatOverClimate(BaseThermostat):
return HVACAction.OFF return HVACAction.OFF
@overrides @overrides
async def _async_internal_set_temperature(self, temperature): async def _async_internal_set_temperature(self, temperature: float):
"""Set the target temperature and the target temperature of underlying climate if any""" """Set the target temperature and the target temperature of underlying climate if any"""
await super()._async_internal_set_temperature(temperature) await super()._async_internal_set_temperature(temperature)
@@ -138,7 +142,17 @@ class ThermostatOverClimate(BaseThermostat):
"""Sends the regulated temperature to all underlying""" """Sends the regulated temperature to all underlying"""
if self.hvac_mode == HVACMode.OFF: if self.hvac_mode == HVACMode.OFF:
_LOGGER.debug("%s - don't send regulated temperature cause VTherm is off ") _LOGGER.debug(
"%s - don't send regulated temperature cause VTherm is off ", self
)
return
if self.target_temperature is None:
_LOGGER.warning(
"%s - don't send regulated temperature cause VTherm target_temp (%s) is None. This should be a temporary warning message.",
self,
self.target_temperature,
)
return return
_LOGGER.info( _LOGGER.info(
@@ -162,14 +176,20 @@ class ThermostatOverClimate(BaseThermostat):
self._regulated_target_temp = self.target_temperature self._regulated_target_temp = self.target_temperature
_LOGGER.info("%s - regulation calculation will be done", self) _LOGGER.info("%s - regulation calculation will be done", self)
self._last_regulation_change = now
new_regulated_temp = round_to_nearest( # use _attr_target_temperature_step to round value if _auto_regulation_dtemp is equal to 0
self._regulation_algo.calculate_regulated_temperature( regulation_step = self._auto_regulation_dtemp if self._auto_regulation_dtemp else self._attr_target_temperature_step
self.current_temperature, self._cur_ext_temp _LOGGER.debug("%s - usage regulation_step: %.2f ", self, regulation_step)
),
self._auto_regulation_dtemp, if self.current_temperature is not None:
) new_regulated_temp = round_to_nearest(
self._regulation_algo.calculate_regulated_temperature(
self.current_temperature, self._cur_ext_temp
),
regulation_step,
)
else:
new_regulated_temp = self.target_temperature
dtemp = new_regulated_temp - self._regulated_target_temp dtemp = new_regulated_temp - self._regulated_target_temp
if not force and abs(dtemp) < self._auto_regulation_dtemp: if not force and abs(dtemp) < self._auto_regulation_dtemp:
@@ -188,9 +208,46 @@ class ThermostatOverClimate(BaseThermostat):
new_regulated_temp, new_regulated_temp,
) )
self._last_regulation_change = now
for under in self._underlyings: for under in self._underlyings:
# issue 348 - use device temperature if configured as offset
offset_temp = 0
device_temp = 0
if (
# current_temperature is set
self.current_temperature is not None
# regulation can use the device_temp
and self.auto_regulation_use_device_temp
# and we have access to the device temp
and (device_temp := under.underlying_current_temperature) is not None
# and target is not reach (ie we need regulation)
and (
(
self.hvac_mode == HVACMode.COOL
and self.target_temperature < self.current_temperature
)
or (
self.hvac_mode == HVACMode.HEAT
and self.target_temperature > self.current_temperature
)
)
):
offset_temp = device_temp - self.current_temperature
target_temp = round_to_nearest(self.regulated_target_temp + offset_temp, regulation_step)
_LOGGER.debug(
"%s - The device offset temp for regulation is %.2f - internal temp is %.2f. New target is %.2f",
self,
offset_temp,
device_temp,
target_temp,
)
await under.set_temperature( await under.set_temperature(
self.regulated_target_temp, self._attr_max_temp, self._attr_min_temp target_temp,
self._attr_max_temp,
self._attr_min_temp,
) )
async def _send_auto_fan_mode(self): async def _send_auto_fan_mode(self):
@@ -239,7 +296,7 @@ class ThermostatOverClimate(BaseThermostat):
await self.async_set_fan_mode(self._auto_deactivated_fan_mode) await self.async_set_fan_mode(self._auto_deactivated_fan_mode)
@overrides @overrides
def post_init(self, config_entry): def post_init(self, config_entry: ConfigData):
"""Initialize the Thermostat""" """Initialize the Thermostat"""
super().post_init(config_entry) super().post_init(config_entry)
@@ -281,7 +338,11 @@ class ThermostatOverClimate(BaseThermostat):
else CONF_AUTO_FAN_NONE else CONF_AUTO_FAN_NONE
) )
def choose_auto_regulation_mode(self, auto_regulation_mode): self._auto_regulation_use_device_temp = config_entry.get(
CONF_AUTO_REGULATION_USE_DEVICE_TEMP, False
)
def choose_auto_regulation_mode(self, auto_regulation_mode: str):
"""Choose or change the regulation mode""" """Choose or change the regulation mode"""
self._auto_regulation_mode = auto_regulation_mode self._auto_regulation_mode = auto_regulation_mode
if self._auto_regulation_mode == CONF_AUTO_REGULATION_LIGHT: if self._auto_regulation_mode == CONF_AUTO_REGULATION_LIGHT:
@@ -357,7 +418,7 @@ class ThermostatOverClimate(BaseThermostat):
self.target_temperature, 0, 0, 0, 0, 0.1, 0 self.target_temperature, 0, 0, 0, 0, 0.1, 0
) )
def choose_auto_fan_mode(self, auto_fan_mode): def choose_auto_fan_mode(self, auto_fan_mode: str):
"""Choose the correct fan mode depending of the underlying capacities and the configuration""" """Choose the correct fan mode depending of the underlying capacities and the configuration"""
self._current_auto_fan_mode = auto_fan_mode self._current_auto_fan_mode = auto_fan_mode
@@ -369,7 +430,7 @@ class ThermostatOverClimate(BaseThermostat):
self._auto_activated_fan_mode = self._auto_deactivated_fan_mode = None self._auto_activated_fan_mode = self._auto_deactivated_fan_mode = None
return return
def find_fan_mode(fan_modes, fan_mode) -> str: def find_fan_mode(fan_modes: list[str], fan_mode: str) -> str | None:
"""Return the fan_mode if it exist of None if not""" """Return the fan_mode if it exist of None if not"""
try: try:
return fan_mode if fan_modes.index(fan_mode) >= 0 else None return fan_mode if fan_modes.index(fan_mode) >= 0 else None
@@ -427,10 +488,11 @@ class ThermostatOverClimate(BaseThermostat):
) )
# init auto_regulation_mode # init auto_regulation_mode
self.choose_auto_regulation_mode(self._auto_regulation_mode) # Issue 325 - do only once (in post_init and not here)
# self.choose_auto_regulation_mode(self._auto_regulation_mode)
@overrides @overrides
def restore_specific_previous_state(self, old_state): def restore_specific_previous_state(self, old_state: State):
"""Restore my specific attributes from previous state""" """Restore my specific attributes from previous state"""
old_error = old_state.attributes.get("regulation_accumulated_error") old_error = old_state.attributes.get("regulation_accumulated_error")
if old_error: if old_error:
@@ -447,9 +509,9 @@ class ThermostatOverClimate(BaseThermostat):
super().update_custom_attributes() super().update_custom_attributes()
self._attr_extra_state_attributes["is_over_climate"] = self.is_over_climate self._attr_extra_state_attributes["is_over_climate"] = self.is_over_climate
self._attr_extra_state_attributes[ self._attr_extra_state_attributes["start_hvac_action_date"] = (
"start_hvac_action_date" self._underlying_climate_start_hvac_action_date
] = self._underlying_climate_start_hvac_action_date )
self._attr_extra_state_attributes["underlying_climate_0"] = self._underlyings[ self._attr_extra_state_attributes["underlying_climate_0"] = self._underlyings[
0 0
].entity_id ].entity_id
@@ -465,28 +527,32 @@ class ThermostatOverClimate(BaseThermostat):
if self.is_regulated: if self.is_regulated:
self._attr_extra_state_attributes["is_regulated"] = self.is_regulated self._attr_extra_state_attributes["is_regulated"] = self.is_regulated
self._attr_extra_state_attributes[ self._attr_extra_state_attributes["regulated_target_temperature"] = (
"regulated_target_temperature" self._regulated_target_temp
] = self._regulated_target_temp )
self._attr_extra_state_attributes[ self._attr_extra_state_attributes["auto_regulation_mode"] = (
"auto_regulation_mode" self.auto_regulation_mode
] = self.auto_regulation_mode )
self._attr_extra_state_attributes[ self._attr_extra_state_attributes["regulation_accumulated_error"] = (
"regulation_accumulated_error" self._regulation_algo.accumulated_error
] = self._regulation_algo.accumulated_error )
self._attr_extra_state_attributes["auto_fan_mode"] = self.auto_fan_mode self._attr_extra_state_attributes["auto_fan_mode"] = self.auto_fan_mode
self._attr_extra_state_attributes[ self._attr_extra_state_attributes["current_auto_fan_mode"] = (
"current_auto_fan_mode" self._current_auto_fan_mode
] = self._current_auto_fan_mode )
self._attr_extra_state_attributes[ self._attr_extra_state_attributes["auto_activated_fan_mode"] = (
"auto_activated_fan_mode" self._auto_activated_fan_mode
] = self._auto_activated_fan_mode )
self._attr_extra_state_attributes[ self._attr_extra_state_attributes["auto_deactivated_fan_mode"] = (
"auto_deactivated_fan_mode" self._auto_deactivated_fan_mode
] = self._auto_deactivated_fan_mode )
self._attr_extra_state_attributes["auto_regulation_use_device_temp"] = (
self.auto_regulation_use_device_temp
)
self.async_write_ha_state() self.async_write_ha_state()
_LOGGER.debug( _LOGGER.debug(
@@ -526,10 +592,18 @@ class ThermostatOverClimate(BaseThermostat):
return return
added_energy = 0 added_energy = 0
if self.is_over_climate and self._underlying_climate_delta_t is not None: if (
self.is_over_climate
and self._underlying_climate_delta_t is not None
and self._device_power
):
added_energy = self._device_power * self._underlying_climate_delta_t added_energy = self._device_power * self._underlying_climate_delta_t
self._total_energy += added_energy if self._total_energy is None:
self._total_energy = added_energy
else:
self._total_energy += added_energy
_LOGGER.debug( _LOGGER.debug(
"%s - added energy is %.3f . Total energy is now: %.3f", "%s - added energy is %.3f . Total energy is now: %.3f",
self, self,
@@ -538,7 +612,7 @@ class ThermostatOverClimate(BaseThermostat):
) )
@callback @callback
async def _async_climate_changed(self, event): async def _async_climate_changed(self, event: Event[EventStateChangedData]):
"""Handle unerdlying climate state changes. """Handle unerdlying climate state changes.
This method takes the underlying values and update the VTherm with them. This method takes the underlying values and update the VTherm with them.
To avoid loops (issues #121 #101 #95 #99), we discard the event if it is received To avoid loops (issues #121 #101 #95 #99), we discard the event if it is received
@@ -548,7 +622,7 @@ class ThermostatOverClimate(BaseThermostat):
which is important for feedaback and which cannot generates loops. which is important for feedaback and which cannot generates loops.
""" """
async def end_climate_changed(changes): async def end_climate_changed(changes: bool):
"""To end the event management""" """To end the event management"""
if changes: if changes:
self.async_write_ha_state() self.async_write_ha_state()
@@ -601,8 +675,9 @@ class ThermostatOverClimate(BaseThermostat):
# new_hvac_mode = HVACMode.OFF # new_hvac_mode = HVACMode.OFF
_LOGGER.info( _LOGGER.info(
"%s - Underlying climate changed. Event.new_hvac_mode is %s, current_hvac_mode=%s, new_hvac_action=%s, old_hvac_action=%s", "%s - Underlying climate %s changed. Event.new_hvac_mode is %s, current_hvac_mode=%s, new_hvac_action=%s, old_hvac_action=%s",
self, self,
new_state.entity_id,
new_hvac_mode, new_hvac_mode,
self._hvac_mode, self._hvac_mode,
new_hvac_action, new_hvac_action,
@@ -658,7 +733,7 @@ class ThermostatOverClimate(BaseThermostat):
) )
changes = True changes = True
# Issue #120 - Some TRV are chaning target temperature a very long time (6 sec) after the change. # Issue #120 - Some TRV are changing target temperature a very long time (6 sec) after the change.
# In that case a loop is possible if a user change multiple times during this 6 sec. # In that case a loop is possible if a user change multiple times during this 6 sec.
if new_state_date_updated and self._last_change_time: if new_state_date_updated and self._last_change_time:
delta = (new_state_date_updated - self._last_change_time).total_seconds() delta = (new_state_date_updated - self._last_change_time).total_seconds()
@@ -684,12 +759,31 @@ class ThermostatOverClimate(BaseThermostat):
] ]
and self._hvac_mode != new_hvac_mode and self._hvac_mode != new_hvac_mode
): ):
changes = True
self._hvac_mode = new_hvac_mode
# Update all underlyings state # Update all underlyings state
# Issue #334 - if all underlyings are not aligned with the same hvac_mode don't change the underlying and wait they are aligned
if self.is_over_climate: if self.is_over_climate:
for under in self._underlyings:
if (
under.entity_id != new_state.entity_id
and under.hvac_mode != self._hvac_mode
):
_LOGGER.info(
"%s - the underlying's hvac_mode %s is not aligned with VTherm hvac_mode %s. So we don't diffuse the change to all other underlyings to avoid loops",
under,
under.hvac_mode,
self._hvac_mode,
)
return
_LOGGER.debug(
"%s - All underlyings have the same hvac_mode, so VTherm will send the new hvac mode %s",
self,
new_hvac_mode,
)
for under in self._underlyings: for under in self._underlyings:
await under.set_hvac_mode(new_hvac_mode) await under.set_hvac_mode(new_hvac_mode)
changes = True
self._hvac_mode = new_hvac_mode
# A quick win to known if it has change by using the self._attr_fan_mode and not only underlying[0].fan_mode # A quick win to known if it has change by using the self._attr_fan_mode and not only underlying[0].fan_mode
if new_fan_mode != self._attr_fan_mode: if new_fan_mode != self._attr_fan_mode:
@@ -721,7 +815,7 @@ class ThermostatOverClimate(BaseThermostat):
await end_climate_changed(changes) await end_climate_changed(changes)
@overrides @overrides
async def async_control_heating(self, force=False, _=None): async def async_control_heating(self, force=False, _=None) -> bool:
"""The main function used to run the calculation at each cycle""" """The main function used to run the calculation at each cycle"""
ret = await super().async_control_heating(force, _) ret = await super().async_control_heating(force, _)
@@ -733,27 +827,32 @@ class ThermostatOverClimate(BaseThermostat):
return ret return ret
@property @property
def auto_regulation_mode(self): def auto_regulation_mode(self) -> str | None:
"""Get the regulation mode""" """Get the regulation mode"""
return self._auto_regulation_mode return self._auto_regulation_mode
@property @property
def auto_fan_mode(self): def auto_fan_mode(self) -> str | None:
"""Get the auto fan mode""" """Get the auto fan mode"""
return self._auto_fan_mode return self._auto_fan_mode
@property @property
def regulated_target_temp(self): def auto_regulation_use_device_temp(self) -> bool | None:
"""Returns the value of parameter auto_regulation_use_device_temp"""
return self._auto_regulation_use_device_temp
@property
def regulated_target_temp(self) -> float | None:
"""Get the regulated target temperature""" """Get the regulated target temperature"""
return self._regulated_target_temp return self._regulated_target_temp
@property @property
def is_regulated(self): def is_regulated(self) -> bool:
"""Check if the ThermostatOverClimate is regulated""" """Check if the ThermostatOverClimate is regulated"""
return self.auto_regulation_mode != CONF_AUTO_REGULATION_NONE return self.auto_regulation_mode != CONF_AUTO_REGULATION_NONE
@property @property
def hvac_modes(self): def hvac_modes(self) -> list[HVACMode]:
"""List of available operation modes.""" """List of available operation modes."""
if self.underlying_entity(0): if self.underlying_entity(0):
return self.underlying_entity(0).hvac_modes return self.underlying_entity(0).hvac_modes
@@ -813,10 +912,7 @@ class ThermostatOverClimate(BaseThermostat):
@property @property
def temperature_unit(self) -> str: def temperature_unit(self) -> str:
"""Return the unit of measurement.""" """Return the unit of measurement."""
if self.underlying_entity(0): return self.hass.config.units.temperature_unit
return self.underlying_entity(0).temperature_unit
return self._unit
@property @property
def supported_features(self): def supported_features(self):
@@ -826,13 +922,14 @@ class ThermostatOverClimate(BaseThermostat):
return self._support_flags return self._support_flags
@property # We keep the step configured for the VTherm and not the step of the underlying
def target_temperature_step(self) -> float | None: # @property
"""Return the supported step of target temperature.""" # def target_temperature_step(self) -> float | None:
if self.underlying_entity(0): # """Return the supported step of target temperature."""
return self.underlying_entity(0).target_temperature_step # if self.underlying_entity(0):
# return self.underlying_entity(0).target_temperature_step
return None #
# return None
@property @property
def target_temperature_high(self) -> float | None: def target_temperature_high(self) -> float | None:
@@ -919,7 +1016,7 @@ class ThermostatOverClimate(BaseThermostat):
await under.async_turn_aux_heat_off() await under.async_turn_aux_heat_off()
@overrides @overrides
async def async_set_fan_mode(self, fan_mode): async def async_set_fan_mode(self, fan_mode: str):
"""Set new target fan mode.""" """Set new target fan mode."""
_LOGGER.info("%s - Set fan mode: %s", self, fan_mode) _LOGGER.info("%s - Set fan mode: %s", self, fan_mode)
if fan_mode is None: if fan_mode is None:
@@ -952,7 +1049,7 @@ class ThermostatOverClimate(BaseThermostat):
self._swing_mode = swing_mode self._swing_mode = swing_mode
self.async_write_ha_state() self.async_write_ha_state()
async def service_set_auto_regulation_mode(self, auto_regulation_mode): async def service_set_auto_regulation_mode(self, auto_regulation_mode: str):
"""Called by a service call: """Called by a service call:
service: versatile_thermostat.set_auto_regulation_mode service: versatile_thermostat.set_auto_regulation_mode
data: data:
@@ -981,7 +1078,7 @@ class ThermostatOverClimate(BaseThermostat):
await self._send_regulated_temperature() await self._send_regulated_temperature()
self.update_custom_attributes() self.update_custom_attributes()
async def service_set_auto_fan_mode(self, auto_fan_mode): async def service_set_auto_fan_mode(self, auto_fan_mode: str):
"""Called by a service call: """Called by a service call:
service: versatile_thermostat.set_auto_fan_mode service: versatile_thermostat.set_auto_fan_mode
data: data:

View File

@@ -2,8 +2,11 @@
""" A climate over switch classe """ """ A climate over switch classe """
import logging import logging
from homeassistant.core import callback from homeassistant.core import Event, callback
from homeassistant.helpers.event import async_track_state_change_event from homeassistant.helpers.event import (
async_track_state_change_event,
EventStateChangedData,
)
from homeassistant.components.climate import HVACMode from homeassistant.components.climate import HVACMode
from .const import ( from .const import (
@@ -11,18 +14,19 @@ from .const import (
CONF_HEATER_2, CONF_HEATER_2,
CONF_HEATER_3, CONF_HEATER_3,
CONF_HEATER_4, CONF_HEATER_4,
CONF_HEATER_KEEP_ALIVE,
CONF_INVERSE_SWITCH, CONF_INVERSE_SWITCH,
overrides, overrides,
) )
from .base_thermostat import BaseThermostat from .base_thermostat import BaseThermostat, ConfigData
from .underlyings import UnderlyingSwitch from .underlyings import UnderlyingSwitch
from .prop_algorithm import PropAlgorithm from .prop_algorithm import PropAlgorithm
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
class ThermostatOverSwitch(BaseThermostat): class ThermostatOverSwitch(BaseThermostat[UnderlyingSwitch]):
"""Representation of a base class for a Versatile Thermostat over a switch.""" """Representation of a base class for a Versatile Thermostat over a switch."""
_entity_component_unrecorded_attributes = ( _entity_component_unrecorded_attributes = (
@@ -51,7 +55,7 @@ class ThermostatOverSwitch(BaseThermostat):
# def __init__(self, hass: HomeAssistant, unique_id, name, config_entry) -> None: # def __init__(self, hass: HomeAssistant, unique_id, name, config_entry) -> None:
# """Initialize the thermostat over switch.""" # """Initialize the thermostat over switch."""
# super().__init__(hass, unique_id, name, config_entry) # super().__init__(hass, unique_id, name, config_entry)
_is_inversed: bool = None _is_inversed: bool | None = None
@property @property
def is_over_switch(self) -> bool: def is_over_switch(self) -> bool:
@@ -72,7 +76,7 @@ class ThermostatOverSwitch(BaseThermostat):
return None return None
@overrides @overrides
def post_init(self, config_entry): def post_init(self, config_entry: ConfigData):
"""Initialize the Thermostat""" """Initialize the Thermostat"""
super().post_init(config_entry) super().post_init(config_entry)
@@ -83,6 +87,7 @@ class ThermostatOverSwitch(BaseThermostat):
self._tpi_coef_ext, self._tpi_coef_ext,
self._cycle_min, self._cycle_min,
self._minimal_activation_delay, self._minimal_activation_delay,
self.name,
) )
lst_switches = [config_entry.get(CONF_HEATER)] lst_switches = [config_entry.get(CONF_HEATER)]
@@ -101,6 +106,7 @@ class ThermostatOverSwitch(BaseThermostat):
thermostat=self, thermostat=self,
switch_entity_id=switch, switch_entity_id=switch,
initial_delay_sec=idx * delta_cycle, initial_delay_sec=idx * delta_cycle,
keep_alive_sec=config_entry.get(CONF_HEATER_KEEP_ALIVE, 0),
) )
) )
@@ -121,6 +127,7 @@ class ThermostatOverSwitch(BaseThermostat):
self.hass, [switch.entity_id], self._async_switch_changed self.hass, [switch.entity_id], self._async_switch_changed
) )
) )
switch.startup()
self.hass.create_task(self.async_control_heating()) self.hass.create_task(self.async_control_heating())
@@ -129,11 +136,11 @@ class ThermostatOverSwitch(BaseThermostat):
"""Custom attributes""" """Custom attributes"""
super().update_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_over_switch"] = self.is_over_switch
self._attr_extra_state_attributes["is_inversed"] = self.is_inversed self._attr_extra_state_attributes["is_inversed"] = self.is_inversed
self._attr_extra_state_attributes["underlying_switch_0"] = self._underlyings[ self._attr_extra_state_attributes["keep_alive_sec"] = under0.keep_alive_sec
0 self._attr_extra_state_attributes["underlying_switch_0"] = under0.entity_id
].entity_id
self._attr_extra_state_attributes["underlying_switch_1"] = ( self._attr_extra_state_attributes["underlying_switch_1"] = (
self._underlyings[1].entity_id if len(self._underlyings) > 1 else None self._underlyings[1].entity_id if len(self._underlyings) > 1 else None
) )
@@ -176,7 +183,7 @@ class ThermostatOverSwitch(BaseThermostat):
self._target_temp, self._target_temp,
self._cur_temp, self._cur_temp,
self._cur_ext_temp, self._cur_ext_temp,
self._hvac_mode == HVACMode.COOL, self._hvac_mode or HVACMode.OFF,
) )
self.update_custom_attributes() self.update_custom_attributes()
self.async_write_ha_state() self.async_write_ha_state()
@@ -191,7 +198,11 @@ class ThermostatOverSwitch(BaseThermostat):
if not self.is_over_climate and self.mean_cycle_power is not None: if not self.is_over_climate and self.mean_cycle_power is not None:
added_energy = self.mean_cycle_power * float(self._cycle_min) / 60.0 added_energy = self.mean_cycle_power * float(self._cycle_min) / 60.0
self._total_energy += added_energy if self._total_energy is None:
self._total_energy = added_energy
else:
self._total_energy += added_energy
_LOGGER.debug( _LOGGER.debug(
"%s - added energy is %.3f . Total energy is now: %.3f", "%s - added energy is %.3f . Total energy is now: %.3f",
self, self,
@@ -200,7 +211,7 @@ class ThermostatOverSwitch(BaseThermostat):
) )
@callback @callback
def _async_switch_changed(self, event): def _async_switch_changed(self, event: Event[EventStateChangedData]):
"""Handle heater switch state changes.""" """Handle heater switch state changes."""
new_state = event.data.get("new_state") new_state = event.data.get("new_state")
old_state = event.data.get("old_state") old_state = event.data.get("old_state")
@@ -208,5 +219,6 @@ class ThermostatOverSwitch(BaseThermostat):
return return
if old_state is None: if old_state is None:
self.hass.create_task(self._check_initial_state()) self.hass.create_task(self._check_initial_state())
self.async_write_ha_state() self.async_write_ha_state()
self.update_custom_attributes() self.update_custom_attributes()

View File

@@ -1,52 +1,70 @@
# pylint: disable=line-too-long # pylint: disable=line-too-long
""" A climate over switch classe """ """ A climate over switch classe """
import logging import logging
from datetime import timedelta from datetime import timedelta, datetime
from homeassistant.helpers.event import ( from homeassistant.helpers.event import (
async_track_state_change_event, async_track_state_change_event,
async_track_time_interval, async_track_time_interval,
EventStateChangedData,
) )
from homeassistant.core import callback from homeassistant.core import Event, HomeAssistant, callback
from homeassistant.components.climate import HVACMode from homeassistant.components.climate import HVACMode
from .base_thermostat import BaseThermostat from .base_thermostat import BaseThermostat, ConfigData
from .prop_algorithm import PropAlgorithm from .prop_algorithm import PropAlgorithm
from .const import CONF_VALVE, CONF_VALVE_2, CONF_VALVE_3, CONF_VALVE_4, overrides from .const import (
CONF_VALVE,
CONF_VALVE_2,
CONF_VALVE_3,
CONF_VALVE_4,
# This is not really self-regulation but regulation here
CONF_AUTO_REGULATION_DTEMP,
CONF_AUTO_REGULATION_PERIOD_MIN,
overrides,
)
from .underlyings import UnderlyingValve from .underlyings import UnderlyingValve
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
class ThermostatOverValve(BaseThermostat): class ThermostatOverValve(BaseThermostat[UnderlyingValve]): # pylint: disable=abstract-method
"""Representation of a class for a Versatile Thermostat over a Valve""" """Representation of a class for a Versatile Thermostat over a Valve"""
_entity_component_unrecorded_attributes = ( _entity_component_unrecorded_attributes = BaseThermostat._entity_component_unrecorded_attributes.union( # pylint: disable=protected-access
BaseThermostat._entity_component_unrecorded_attributes.union( frozenset(
frozenset( {
{ "is_over_valve",
"is_over_valve", "underlying_valve_0",
"underlying_valve_0", "underlying_valve_1",
"underlying_valve_1", "underlying_valve_2",
"underlying_valve_2", "underlying_valve_3",
"underlying_valve_3", "on_time_sec",
"on_time_sec", "off_time_sec",
"off_time_sec", "cycle_min",
"cycle_min", "function",
"function", "tpi_coef_int",
"tpi_coef_int", "tpi_coef_ext",
"tpi_coef_ext", "auto_regulation_dpercent",
} "auto_regulation_period_min",
) "last_calculation_timestamp",
}
) )
) )
# Useless for now def __init__(
# def __init__(self, hass: HomeAssistant, unique_id, name, config_entry) -> None: self, hass: HomeAssistant, unique_id: str, name: str, config_entry: ConfigData
# """Initialize the thermostat over switch.""" ):
# super().__init__(hass, unique_id, name, config_entry) """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 @property
def is_over_valve(self) -> bool: def is_over_valve(self) -> bool:
@@ -59,19 +77,32 @@ class ThermostatOverValve(BaseThermostat):
if self._hvac_mode == HVACMode.OFF: if self._hvac_mode == HVACMode.OFF:
return 0 return 0
else: else:
return round(max(0, min(self.proportional_algorithm.on_percent, 1)) * 100) return self._valve_open_percent
@overrides @overrides
def post_init(self, config_entry): def post_init(self, config_entry: ConfigData):
"""Initialize the Thermostat""" """Initialize the Thermostat"""
super().post_init(config_entry) 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._prop_algorithm = PropAlgorithm(
self._proportional_function, self._proportional_function,
self._tpi_coef_int, self._tpi_coef_int,
self._tpi_coef_ext, self._tpi_coef_ext,
self._cycle_min, self._cycle_min,
self._minimal_activation_delay, self._minimal_activation_delay,
self.name,
) )
lst_valves = [config_entry.get(CONF_VALVE)] lst_valves = [config_entry.get(CONF_VALVE)]
@@ -115,7 +146,7 @@ class ThermostatOverValve(BaseThermostat):
) )
@callback @callback
async def _async_valve_changed(self, event): async def _async_valve_changed(self, event: Event[EventStateChangedData]):
"""Handle unerdlying valve state changes. """Handle unerdlying valve state changes.
This method just log the change. It changes nothing to avoid loops. This method just log the change. It changes nothing to avoid loops.
""" """
@@ -158,6 +189,17 @@ class ThermostatOverValve(BaseThermostat):
self._attr_extra_state_attributes["function"] = self._proportional_function 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_int"] = self._tpi_coef_int
self._attr_extra_state_attributes["tpi_coef_ext"] = self._tpi_coef_ext self._attr_extra_state_attributes["tpi_coef_ext"] = self._tpi_coef_ext
self._attr_extra_state_attributes[
"auto_regulation_dpercent"
] = self._auto_regulation_dpercent
self._attr_extra_state_attributes[
"auto_regulation_period_min"
] = self._auto_regulation_period_min
self._attr_extra_state_attributes["last_calculation_timestamp"] = (
self._last_calculation_timestamp.astimezone(self._current_tz).isoformat()
if self._last_calculation_timestamp
else None
)
self.async_write_ha_state() self.async_write_ha_state()
_LOGGER.debug( _LOGGER.debug(
@@ -171,17 +213,62 @@ class ThermostatOverValve(BaseThermostat):
"""A utility function to force the calculation of a the algo and """A utility function to force the calculation of a the algo and
update the custom attributes and write the state update the custom attributes and write the state
""" """
_LOGGER.debug("%s - recalculate all", self) _LOGGER.debug("%s - recalculate the open percent", self)
# For testing purpose. Should call _set_now() before
now = self.now
if self._last_calculation_timestamp is not None:
period = (now - self._last_calculation_timestamp).total_seconds() / 60
if period < self._auto_regulation_period_min:
_LOGGER.info(
"%s - do not calculate TPI because regulation_period (%d) is not exceeded",
self,
period,
)
return
self._prop_algorithm.calculate( self._prop_algorithm.calculate(
self._target_temp, self._target_temp,
self._cur_temp, self._cur_temp,
self._cur_ext_temp, self._cur_ext_temp,
self._hvac_mode == HVACMode.COOL, 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
for under in self._underlyings: for under in self._underlyings:
under.set_valve_open_percent() under.set_valve_open_percent()
self._last_calculation_timestamp = now
self.update_custom_attributes() self.update_custom_attributes()
self.async_write_ha_state() self.async_write_ha_state()
@@ -195,7 +282,11 @@ class ThermostatOverValve(BaseThermostat):
if not self.is_over_climate and self.mean_cycle_power is not None: if not self.is_over_climate and self.mean_cycle_power is not None:
added_energy = self.mean_cycle_power * float(self._cycle_min) / 60.0 added_energy = self.mean_cycle_power * float(self._cycle_min) / 60.0
self._total_energy += added_energy if self._total_energy is None:
self._total_energy = added_energy
else:
self._total_energy += added_energy
_LOGGER.debug( _LOGGER.debug(
"%s - added energy is %.3f . Total energy is now: %.3f", "%s - added energy is %.3f . Total energy is now: %.3f",
self, self,

View File

@@ -43,7 +43,7 @@
"auto_regulation_dtemp": "Όριο ρύθμισης", "auto_regulation_dtemp": "Όριο ρύθμισης",
"auto_regulation_periode_min": "Ελάχιστη περίοδος ρύθμισης", "auto_regulation_periode_min": "Ελάχιστη περίοδος ρύθμισης",
"inverse_switch_command": "Αντίστροφη εντολή διακόπτη", "inverse_switch_command": "Αντίστροφη εντολή διακόπτη",
"auto_fan_mode": " Auto fan mode" "auto_fan_mode": "Auto fan mode"
}, },
"data_description": { "data_description": {
"heater_entity_id": "Υποχρεωτική ταυτότητα οντότητας θερμαντήρα", "heater_entity_id": "Υποχρεωτική ταυτότητα οντότητας θερμαντήρα",
@@ -64,7 +64,7 @@
"auto_regulation_dtemp": "Το όριο σε ° κάτω από το οποίο η αλλαγή θερμοκρασίας δεν θα αποστέλλεται", "auto_regulation_dtemp": "Το όριο σε ° κάτω από το οποίο η αλλαγή θερμοκρασίας δεν θα αποστέλλεται",
"auto_regulation_periode_min": "Διάρκεια σε λεπτά μεταξύ δύο ενημερώσεων ρύθμισης", "auto_regulation_periode_min": "Διάρκεια σε λεπτά μεταξύ δύο ενημερώσεων ρύθμισης",
"inverse_switch_command": "Για διακόπτη με πιλοτικό καλώδιο και δίοδο μπορεί να χρειαστεί να αντιστρέψετε την εντολή", "inverse_switch_command": "Για διακόπτη με πιλοτικό καλώδιο και δίοδο μπορεί να χρειαστεί να αντιστρέψετε την εντολή",
"auto_fan_mode": " Automatically activate fan when huge heating/cooling is necessary" "auto_fan_mode": "Automatically activate fan when huge heating/cooling is necessary"
} }
}, },
"tpi": { "tpi": {
@@ -216,7 +216,7 @@
"auto_regulation_dtemp": "Όριο ρύθμισης", "auto_regulation_dtemp": "Όριο ρύθμισης",
"auto_regulation_periode_min": "Ελάχιστη περίοδος ρύθμισης", "auto_regulation_periode_min": "Ελάχιστη περίοδος ρύθμισης",
"inverse_switch_command": "Αντίστροφη εντολή διακόπτη", "inverse_switch_command": "Αντίστροφη εντολή διακόπτη",
"auto_fan_mode": " Auto fan mode" "auto_fan_mode": "Auto fan mode"
}, },
"data_description": { "data_description": {
"heater_entity_id": "Υποχρεωτική ταυτότητα οντότητας θερμαντήρα", "heater_entity_id": "Υποχρεωτική ταυτότητα οντότητας θερμαντήρα",
@@ -237,7 +237,7 @@
"auto_regulation_dtemp": "Το κατώφλι σε °C κάτω από το οποίο η αλλαγή της θερμοκρασίας δεν θα αποστέλλεται", "auto_regulation_dtemp": "Το κατώφλι σε °C κάτω από το οποίο η αλλαγή της θερμοκρασίας δεν θα αποστέλλεται",
"auto_regulation_periode_min": "Διάρκεια σε λεπτά μεταξύ δύο ενημερώσεων ρύθμισης", "auto_regulation_periode_min": "Διάρκεια σε λεπτά μεταξύ δύο ενημερώσεων ρύθμισης",
"inverse_switch_command": "Για διακόπτες με πιλοτικό καλώδιο και δίοδο μπορεί να χρειαστεί να αντιστραφεί η εντολή", "inverse_switch_command": "Για διακόπτες με πιλοτικό καλώδιο και δίοδο μπορεί να χρειαστεί να αντιστραφεί η εντολή",
"auto_fan_mode": " Automatically activate fan when huge heating/cooling is necessary" "auto_fan_mode": "Automatically activate fan when huge heating/cooling is necessary"
} }
}, },
"tpi": { "tpi": {

View File

@@ -12,29 +12,58 @@
"thermostat_type": "Only one central configuration type is possible" "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",
"finalize": "All done",
"configuration_not_complete": "Configuration not complete"
}
},
"main": { "main": {
"title": "Add new Versatile Thermostat", "title": "Add new Versatile Thermostat",
"description": "Main mandatory attributes", "description": "Main mandatory attributes",
"data": { "data": {
"name": "Name", "name": "Name",
"thermostat_type": "Thermostat type", "thermostat_type": "Thermostat type",
"temperature_sensor_entity_id": "Temperature sensor entity id", "temperature_sensor_entity_id": "Room temperature",
"last_seen_temperature_sensor_entity_id": "Last seen room temperature datetime",
"external_temperature_sensor_entity_id": "Outdoor temperature sensor entity id", "external_temperature_sensor_entity_id": "Outdoor temperature sensor entity id",
"cycle_min": "Cycle duration (minutes)", "cycle_min": "Cycle duration (minutes)",
"temp_min": "Minimal temperature allowed", "temp_min": "Minimum temperature allowed",
"temp_max": "Maximal temperature allowed", "temp_max": "Maximum temperature allowed",
"step_temperature": "Temperature step",
"device_power": "Device power", "device_power": "Device power",
"use_central_mode": "Enable the control by central mode ('central_mode')", "use_central_mode": "Enable the control by central entity (requires central config). Check to enable the control of the VTherm with the select central_mode entities.",
"use_main_central_config": "Use additional central main configuration. Check to use the central main configuration (outdoor temperature, min, max, step, ...).",
"used_by_controls_central_boiler": "Used by central boiler. Check if this VTherm should have control on the central boiler"
},
"data_description": {
"temperature_sensor_entity_id": "Room temperature sensor entity id",
"last_seen_temperature_sensor_entity_id": "Last seen room temperature sensor entity id. Should be datetime sensor",
"external_temperature_sensor_entity_id": "Outdoor temperature sensor entity id. Not used if central configuration is selected"
}
},
"features": {
"title": "Features",
"description": "Thermostat features",
"data": {
"use_window_feature": "Use window detection", "use_window_feature": "Use window detection",
"use_motion_feature": "Use motion detection", "use_motion_feature": "Use motion detection",
"use_power_feature": "Use power management", "use_power_feature": "Use power management",
"use_presence_feature": "Use presence detection", "use_presence_feature": "Use presence detection",
"use_main_central_config": "Use central main configuration" "use_central_boiler_feature": "Use a central boiler. Check to add a control to your central boiler. You will have to configure the VTherm which will have a control of the central boiler after seecting this checkbox to take effect. If one VTherm requires heating, the boiler will be turned on. If no VTherm requires heating, the boiler will be turned off. Commands for turning on/off the central boiler are given in the related configuration page"
},
"data_description": {
"use_central_mode": "Check to enable the control of the VTherm with the select central_mode entities",
"use_main_central_config": "Check to use the central main configuration. Uncheck to use a specific main configuration for this VTherm",
"external_temperature_sensor_entity_id": "Outdoor temperature sensor entity id. Not used if central configuration is selected"
} }
}, },
"type": { "type": {
@@ -45,6 +74,7 @@
"heater_entity2_id": "2nd heater switch", "heater_entity2_id": "2nd heater switch",
"heater_entity3_id": "3rd heater switch", "heater_entity3_id": "3rd heater switch",
"heater_entity4_id": "4th heater switch", "heater_entity4_id": "4th heater switch",
"heater_keep_alive": "Switch keep-alive interval in seconds",
"proportional_function": "Algorithm", "proportional_function": "Algorithm",
"climate_entity_id": "1st underlying climate", "climate_entity_id": "1st underlying climate",
"climate_entity2_id": "2nd underlying climate", "climate_entity2_id": "2nd underlying climate",
@@ -57,15 +87,17 @@
"valve_entity4_id": "4th valve number", "valve_entity4_id": "4th valve number",
"auto_regulation_mode": "Self-regulation", "auto_regulation_mode": "Self-regulation",
"auto_regulation_dtemp": "Regulation threshold", "auto_regulation_dtemp": "Regulation threshold",
"auto_regulation_periode_min": "Regulation minimal period", "auto_regulation_periode_min": "Regulation minimum period",
"auto_regulation_use_device_temp": "Use internal temperature of the underlying",
"inverse_switch_command": "Inverse switch command", "inverse_switch_command": "Inverse switch command",
"auto_fan_mode": " Auto fan mode" "auto_fan_mode": "Auto fan mode"
}, },
"data_description": { "data_description": {
"heater_entity_id": "Mandatory heater entity id", "heater_entity_id": "Mandatory heater entity id",
"heater_entity2_id": "Optional 2nd Heater entity id. Leave empty if not used", "heater_entity2_id": "Optional 2nd Heater entity id. Leave empty if not required",
"heater_entity3_id": "Optional 3rd Heater entity id. Leave empty if not used", "heater_entity3_id": "Optional 3rd Heater entity id. Leave empty if not required",
"heater_entity4_id": "Optional 4th Heater entity id. Leave empty if not used", "heater_entity4_id": "Optional 4th Heater entity id. Leave empty if not required",
"heater_keep_alive": "Optional heater switch state refresh interval. Leave empty if not required.",
"proportional_function": "Algorithm to use (TPI is the only one for now)", "proportional_function": "Algorithm to use (TPI is the only one for now)",
"climate_entity_id": "Underlying climate entity id", "climate_entity_id": "Underlying climate entity id",
"climate_entity2_id": "2nd underlying climate entity id", "climate_entity2_id": "2nd underlying climate entity id",
@@ -77,10 +109,11 @@
"valve_entity3_id": "3rd valve number entity id", "valve_entity3_id": "3rd valve number entity id",
"valve_entity4_id": "4th valve number entity id", "valve_entity4_id": "4th valve number entity id",
"auto_regulation_mode": "Auto adjustment of the target temperature", "auto_regulation_mode": "Auto adjustment of the target temperature",
"auto_regulation_dtemp": "The threshold in ° under which the temperature change will not be sent", "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_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", "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" "auto_fan_mode": "Automatically activate fan when huge heating/cooling is necessary"
} }
}, },
"tpi": { "tpi": {
@@ -99,51 +132,36 @@
}, },
"presets": { "presets": {
"title": "Presets", "title": "Presets",
"description": "For each preset set the target temperature (0 to ignore preset)", "description": "Select if the thermostat will use central preset - deselect for the thermostat to have its own presets",
"data": { "data": {
"eco_temp": "Eco preset",
"comfort_temp": "Comfort preset",
"boost_temp": "Boost preset",
"frost_temp": "Frost protection preset",
"eco_ac_temp": "Eco preset for AC mode",
"comfort_ac_temp": "Comfort preset for AC mode",
"boost_ac_temp": "Boost preset for AC mode",
"use_presets_central_config": "Use central presets configuration" "use_presets_central_config": "Use central presets configuration"
},
"data_description": {
"eco_temp": "Temperature in Eco preset",
"comfort_temp": "Temperature in Comfort preset",
"boost_temp": "Temperature in Boost preset",
"frost_temp": "Temperature in Frost protection preset",
"eco_ac_temp": "Temperature in Eco preset for AC mode",
"comfort_ac_temp": "Temperature in Comfort preset for AC mode",
"boost_ac_temp": "Temperature in Boost preset for AC mode",
"use_presets_central_config": "Check to use the central presets configuration. Uncheck to use a specific presets configuration for this VTherm"
} }
}, },
"window": { "window": {
"title": "Window management", "title": "Window management",
"description": "Open window management.\nLeave corresponding entity_id empty if not used\nYou can also configure automatic window open detection based on temperature decrease", "description": "Open window management.\nYou can also configure automatic window open detection based on temperature decrease",
"data": { "data": {
"window_sensor_entity_id": "Window sensor entity id", "window_sensor_entity_id": "Window sensor entity id",
"window_delay": "Window sensor delay (seconds)", "window_delay": "Window sensor delay (seconds)",
"window_auto_open_threshold": "Temperature decrease threshold for automatic window open detection (in °/hours)", "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_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)", "window_auto_max_duration": "Maximum duration of automatic window open detection (in min)",
"use_window_central_config": "Use central window configuration" "use_window_central_config": "Use central window configuration",
"window_action": "Action"
}, },
"data_description": { "data_description": {
"window_sensor_entity_id": "Leave empty if no window sensor should be used", "window_sensor_entity_id": "Leave empty if no window sensor should be used and to use the automatic detection",
"window_delay": "The delay in seconds before sensor detection is taken into account", "window_delay": "The delay in seconds before sensor detection is taken into account",
"window_auto_open_threshold": "Recommended value: between 3 and 10. Leave empty if automatic window open detection is not used", "window_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_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", "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" "use_window_central_config": "Select to use the central window configuration. Deselect to use a specific window configuration for this VTherm",
"window_action": "Action to perform if window is deteted as open"
} }
}, },
"motion": { "motion": {
"title": "Motion management", "title": "Motion management",
"description": "Motion sensor management. Preset can switch automatically depending on motion detection\nLeave corresponding entity_id empty if not used.\nmotion_preset and no_motion_preset should be set to the corresponding preset name", "description": "Motion sensor management. Preset can switch automatically depending on motion detection\nmotion_preset and no_motion_preset should be set to the corresponding preset name",
"data": { "data": {
"motion_sensor_entity_id": "Motion sensor entity id", "motion_sensor_entity_id": "Motion sensor entity id",
"motion_delay": "Activation delay", "motion_delay": "Activation delay",
@@ -163,7 +181,7 @@
}, },
"power": { "power": {
"title": "Power management", "title": "Power management",
"description": "Power management attributes.\nGives the power and max power sensor of your home.\nThen specify the power consumption of the heater when on.\nAll sensors and device power should have the same unit (kW or W).\nLeave corresponding entity_id empty if not used.", "description": "Power management attributes.\nGives the power and max power sensor of your home.\nSpecify the power consumption of the heater when on.\nAll sensors and device power should use the same unit (kW or W).",
"data": { "data": {
"power_sensor_entity_id": "Power", "power_sensor_entity_id": "Power",
"max_power_sensor_entity_id": "Max power", "max_power_sensor_entity_id": "Max power",
@@ -179,44 +197,29 @@
}, },
"presence": { "presence": {
"title": "Presence management", "title": "Presence management",
"description": "Presence management attributes.\nGives the a presence sensor of your home (true is someone is present).\nThen specify either the preset to use when presence sensor is false or the offset in temperature to apply.\nIf preset is given, the offset will not be used.\nLeave corresponding entity_id empty if not used.", "description": "Presence management attributes.\nGives the a presence sensor of your home (true is someone is present) and give the corresponding temperature preset setting.",
"data": { "data": {
"presence_sensor_entity_id": "Presence sensor", "presence_sensor_entity_id": "Presence sensor",
"eco_away_temp": "Eco preset", "use_presence_central_config": "Use central presence temperature configuration. Deselect to use specific temperature entities"
"comfort_away_temp": "Comfort preset",
"boost_away_temp": "Boost preset",
"frost_away_temp": "Frost protection preset",
"eco_ac_away_temp": "Eco preset in AC mode",
"comfort_ac_away_temp": "Comfort preset in AC mode",
"boost_ac_away_temp": "Boost pres et in AC mode",
"use_presence_central_config": "Use central presence configuration"
}, },
"data_description": { "data_description": {
"presence_sensor_entity_id": "Presence sensor entity id", "presence_sensor_entity_id": "Presence sensor entity id"
"eco_away_temp": "Temperature in Eco preset when no presence",
"comfort_away_temp": "Temperature in Comfort preset when no presence",
"boost_away_temp": "Temperature in Boost preset when no presence",
"frost_away_temp": "Temperature in Frost protection 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",
"use_presence_central_config": "Check to use the central presence configuration. Uncheck to use a specific presence configuration for this VTherm"
} }
}, },
"advanced": { "advanced": {
"title": "Advanced parameters", "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.", "description": "Configuration of advanced parameters. Leave the default values if you don't know what you are doing.\nThese parameters can lead to very poor temperature control or bad power regulation.",
"data": { "data": {
"minimal_activation_delay": "Minimal activation delay", "minimal_activation_delay": "Minimum activation delay",
"security_delay_min": "Safety delay (in minutes)", "security_delay_min": "Safety delay (in minutes)",
"security_min_on_percent": "Minimal power percent to enable safety mode", "security_min_on_percent": "Minimum power percent to enable safety mode",
"security_default_on_percent": "Power percent to use in safety mode", "security_default_on_percent": "Power percent to use in safety mode",
"use_advanced_central_config": "Use central advanced configuration" "use_advanced_central_config": "Use central advanced configuration"
}, },
"data_description": { "data_description": {
"minimal_activation_delay": "Delay in seconds under which the equipment will not be activated", "minimal_activation_delay": "Delay in seconds under which the equipment will not be activated",
"security_delay_min": "Maximum allowed delay in minutes between two temperature measurements. Above this delay the thermostat will turn to a safety off state", "security_delay_min": "Maximum allowed delay in minutes between two temperature measurements. Above this delay the thermostat will turn to a safety off state",
"security_min_on_percent": "Minimal heating percent value for safety preset activation. Below this amount of power percent the thermostat won't go into safety preset", "security_min_on_percent": "Minimum heating percent value for safety preset activation. Below this amount of power percent the thermostat won't go into safety preset",
"security_default_on_percent": "The default heating power percent value in safety preset. Set to 0 to switch off heater in safety preset", "security_default_on_percent": "The default heating power percent value in safety preset. Set to 0 to switch off heater in safety preset",
"use_advanced_central_config": "Check to use the central advanced configuration. Uncheck to use a specific advanced configuration for this VTherm" "use_advanced_central_config": "Check to use the central advanced configuration. Uncheck to use a specific advanced configuration for this VTherm"
} }
@@ -226,7 +229,7 @@
"unknown": "Unexpected error", "unknown": "Unexpected error",
"unknown_entity": "Unknown entity id", "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", "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." "no_central_config": "You cannot select 'use central configuration' because no central configuration was found. You need to create a Versatile Thermostat of type 'Central Configuration' to use it."
}, },
"abort": { "abort": {
"already_configured": "Device is already configured" "already_configured": "Device is already configured"
@@ -244,29 +247,58 @@
"thermostat_type": "Only one central configuration type is possible" "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",
"finalize": "All done",
"configuration_not_complete": "Configuration not complete"
}
},
"main": { "main": {
"title": "Main - {name}", "title": "Main - {name}",
"description": "Main mandatory attributes", "description": "Main mandatory attributes",
"data": { "data": {
"name": "Name", "name": "Name",
"thermostat_type": "Thermostat type", "thermostat_type": "Thermostat type",
"temperature_sensor_entity_id": "Room temperature sensor entity id", "temperature_sensor_entity_id": "Room temperature",
"last_seen_temperature_sensor_entity_id": "Last seen room temperature datetime",
"external_temperature_sensor_entity_id": "Outdoor temperature sensor entity id", "external_temperature_sensor_entity_id": "Outdoor temperature sensor entity id",
"cycle_min": "Cycle duration (minutes)", "cycle_min": "Cycle duration (minutes)",
"temp_min": "Minimal temperature allowed", "temp_min": "Minimum temperature allowed",
"temp_max": "Maximal temperature allowed", "temp_max": "Maximum temperature allowed",
"step_temperature": "Temperature step",
"device_power": "Device power", "device_power": "Device power",
"use_central_mode": "Enable the control by central mode ('central_mode')", "use_central_mode": "Enable the control by central entity (requires central config). Check to enable the control of the VTherm with the select central_mode entities.",
"use_main_central_config": "Use additional central main configuration. Check to use the central main configuration (outdoor temperature, min, max, step, ...).",
"used_by_controls_central_boiler": "Used by central boiler. Check if this VTherm should have control on the central boiler"
},
"data_description": {
"temperature_sensor_entity_id": "Room temperature sensor entity id",
"last_seen_temperature_sensor_entity_id": "Last seen room temperature sensor entity id. Should be datetime sensor",
"external_temperature_sensor_entity_id": "Outdoor temperature sensor entity id. Not used if central configuration is selected"
}
},
"features": {
"title": "Features - {name}",
"description": "Thermostat features",
"data": {
"use_window_feature": "Use window detection", "use_window_feature": "Use window detection",
"use_motion_feature": "Use motion detection", "use_motion_feature": "Use motion detection",
"use_power_feature": "Use power management", "use_power_feature": "Use power management",
"use_presence_feature": "Use presence detection", "use_presence_feature": "Use presence detection",
"use_main_central_config": "Use central main configuration" "use_central_boiler_feature": "Use a central boiler. Check to add a control to your central boiler. You will have to configure the VTherm which will have a control of the central boiler after seecting this checkbox to take effect. If one VTherm requires heating, the boiler will be turned on. If no VTherm requires heating, the boiler will be turned off. Commands for turning on/off the central boiler are given in the related configuration page"
},
"data_description": {
"use_central_mode": "Check to enable the control of the VTherm with the select central_mode entities",
"use_main_central_config": "Check to use the central main configuration. Uncheck to use a specific configuration for this VTherm",
"external_temperature_sensor_entity_id": "Outdoor temperature sensor entity id. Not used if central configuration is selected"
} }
}, },
"type": { "type": {
@@ -277,6 +309,7 @@
"heater_entity2_id": "2nd heater switch", "heater_entity2_id": "2nd heater switch",
"heater_entity3_id": "3rd heater switch", "heater_entity3_id": "3rd heater switch",
"heater_entity4_id": "4th heater switch", "heater_entity4_id": "4th heater switch",
"heater_keep_alive": "Switch keep-alive interval in seconds",
"proportional_function": "Algorithm", "proportional_function": "Algorithm",
"climate_entity_id": "1st underlying climate", "climate_entity_id": "1st underlying climate",
"climate_entity2_id": "2nd underlying climate", "climate_entity2_id": "2nd underlying climate",
@@ -289,15 +322,17 @@
"valve_entity4_id": "4th valve number", "valve_entity4_id": "4th valve number",
"auto_regulation_mode": "Self-regulation", "auto_regulation_mode": "Self-regulation",
"auto_regulation_dtemp": "Regulation threshold", "auto_regulation_dtemp": "Regulation threshold",
"auto_regulation_periode_min": "Regulation minimal period", "auto_regulation_periode_min": "Regulation minimum period",
"auto_regulation_use_device_temp": "Use internal temperature of the underlying",
"inverse_switch_command": "Inverse switch command", "inverse_switch_command": "Inverse switch command",
"auto_fan_mode": " Auto fan mode" "auto_fan_mode": "Auto fan mode"
}, },
"data_description": { "data_description": {
"heater_entity_id": "Mandatory heater entity id", "heater_entity_id": "Mandatory heater entity id",
"heater_entity2_id": "Optional 2nd Heater entity id. Leave empty if not used", "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_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", "heater_entity4_id": "Optional 4th Heater entity id. Leave empty if not used",
"heater_keep_alive": "Optional heater switch state refresh interval. Leave empty if not required.",
"proportional_function": "Algorithm to use (TPI is the only one for now)", "proportional_function": "Algorithm to use (TPI is the only one for now)",
"climate_entity_id": "Underlying climate entity id", "climate_entity_id": "Underlying climate entity id",
"climate_entity2_id": "2nd underlying climate entity id", "climate_entity2_id": "2nd underlying climate entity id",
@@ -309,10 +344,11 @@
"valve_entity3_id": "3rd valve number entity id", "valve_entity3_id": "3rd valve number entity id",
"valve_entity4_id": "4th valve number entity id", "valve_entity4_id": "4th valve number entity id",
"auto_regulation_mode": "Auto adjustment of the target temperature", "auto_regulation_mode": "Auto adjustment of the target temperature",
"auto_regulation_dtemp": "The threshold in ° under which the temperature change will not be sent", "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_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 invert the command", "inverse_switch_command": "For switch with pilot wire and diode you may need to invert the command",
"auto_fan_mode": " Automatically activate fan when huge heating/cooling is necessary" "auto_fan_mode": "Automatically activate fan when huge heating/cooling is necessary"
} }
}, },
"tpi": { "tpi": {
@@ -331,26 +367,9 @@
}, },
"presets": { "presets": {
"title": "Presets - {name}", "title": "Presets - {name}",
"description": "For each preset set the target temperature (0 to ignore preset)", "description": "Check if the thermostat will use central presets. Uncheck and the thermostat will have its own preset entities",
"data": { "data": {
"eco_temp": "Eco preset",
"comfort_temp": "Comfort preset",
"boost_temp": "Boost preset",
"frost_temp": "Frost protection preset",
"eco_ac_temp": "Eco preset for AC mode",
"comfort_ac_temp": "Comfort preset for AC mode",
"boost_ac_temp": "Boost preset for AC mode",
"use_presets_central_config": "Use central presets configuration" "use_presets_central_config": "Use central presets configuration"
},
"data_description": {
"eco_temp": "Temperature in Eco preset",
"comfort_temp": "Temperature in Comfort preset",
"boost_temp": "Temperature in Boost preset",
"frost_temp": "Temperature in Frost protection preset",
"eco_ac_temp": "Temperature in Eco preset for AC mode",
"comfort_ac_temp": "Temperature in Comfort preset for AC mode",
"boost_ac_temp": "Temperature in Boost preset for AC mode",
"use_presets_central_config": "Check to use the central presets configuration. Uncheck to use a specific presets configuration for this VTherm"
} }
}, },
"window": { "window": {
@@ -362,20 +381,22 @@
"window_auto_open_threshold": "Temperature decrease threshold for automatic window open detection (in °/hours)", "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_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)", "window_auto_max_duration": "Maximum duration of automatic window open detection (in min)",
"use_window_central_config": "Use central window configuration" "use_window_central_config": "Use central window configuration",
"window_action": "Action"
}, },
"data_description": { "data_description": {
"window_sensor_entity_id": "Leave empty if no window sensor should be used", "window_sensor_entity_id": "Leave empty if no window sensor should be used and to use the automatic detection",
"window_delay": "The delay in seconds before sensor detection is taken into account", "window_delay": "The delay in seconds before sensor detection is taken into account",
"window_auto_open_threshold": "Recommended value: between 3 and 10. Leave empty if automatic window open detection is not used", "window_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_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", "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" "use_window_central_config": "Check to use the central window configuration. Uncheck to use a specific window configuration for this VTherm",
"window_action": "Action to do if window is deteted as open"
} }
}, },
"motion": { "motion": {
"title": "Motion - {name}", "title": "Motion - {name}",
"description": "Motion management. Preset can switch automatically depending of a motion detection\nLeave corresponding entity_id empty if not used.\nmotion_preset and no_motion_preset should be set to the corresponding preset name", "description": "Motion management. Preset can switch automatically depending of a motion detection\nmotion_preset and no_motion_preset should be set to the corresponding preset name",
"data": { "data": {
"motion_sensor_entity_id": "Motion sensor entity id", "motion_sensor_entity_id": "Motion sensor entity id",
"motion_delay": "Activation delay", "motion_delay": "Activation delay",
@@ -395,7 +416,7 @@
}, },
"power": { "power": {
"title": "Power - {name}", "title": "Power - {name}",
"description": "Power management attributes.\nGives the power and max power sensor of your home.\nThen specify the power consumption of the heater when on.\nAll sensors and device power should have the same unit (kW or W).\nLeave corresponding entity_id empty if not used.", "description": "Power management attributes.\nGives the power and max power sensor of your home.\nThen specify the power consumption of the heater when on.\nAll sensors and device power should have the same unit (kW or W).",
"data": { "data": {
"power_sensor_entity_id": "Power", "power_sensor_entity_id": "Power",
"max_power_sensor_entity_id": "Max power", "max_power_sensor_entity_id": "Max power",
@@ -411,44 +432,29 @@
}, },
"presence": { "presence": {
"title": "Presence - {name}", "title": "Presence - {name}",
"description": "Presence management attributes.\nGives the a presence sensor of your home (true is someone is present).\nThen specify either the preset to use when presence sensor is false or the offset in temperature to apply.\nIf preset is given, the offset will not be used.\nLeave corresponding entity_id empty if not used.", "description": "Presence management attributes.\nGives the a presence sensor of your home (true is someone is present) and give the corresponding temperature preset setting.",
"data": { "data": {
"presence_sensor_entity_id": "Presence sensor", "presence_sensor_entity_id": "Presence sensor",
"eco_away_temp": "Eco away preset", "use_presence_central_config": "Use central presence temperature configuration. Uncheck to use specific temperature entities"
"comfort_away_temp": "Comfort away preset",
"boost_away_temp": "Boost away preset",
"frost_away_temp": "Frost protection preset",
"eco_ac_away_temp": "Eco away preset in AC mode",
"comfort_ac_away_temp": "Comfort away preset in AC mode",
"boost_ac_away_temp": "Boost away preset in AC mode",
"use_presence_central_config": "Use central presence configuration"
}, },
"data_description": { "data_description": {
"presence_sensor_entity_id": "Presence sensor entity id", "presence_sensor_entity_id": "Presence sensor entity id"
"eco_away_temp": "Temperature in Eco preset when no presence",
"comfort_away_temp": "Temperature in Comfort preset when no presence",
"boost_away_temp": "Temperature in Boost preset when no presence",
"frost_away_temp": "Temperature in Frost protection 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",
"use_presence_central_config": "Check to use the central presence configuration. Uncheck to use a specific presence configuration for this VTherm"
} }
}, },
"advanced": { "advanced": {
"title": "Advanced - {name}", "title": "Advanced - {name}",
"description": "Advanced parameters. Leave the default values if you don't know what you are doing.\nThese parameters can lead to very poor temperature control or bad power regulation.", "description": "Advanced parameters. Leave the default values if you don't know what you are doing.\nThese parameters can lead to very poor temperature control or bad power regulation.",
"data": { "data": {
"minimal_activation_delay": "Minimal activation delay", "minimal_activation_delay": "Minimum activation delay",
"security_delay_min": "Safety delay (in minutes)", "security_delay_min": "Safety delay (in minutes)",
"security_min_on_percent": "Minimal power percent to enable safety mode", "security_min_on_percent": "Minimum power percent to enable safety mode",
"security_default_on_percent": "Power percent to use in safety mode", "security_default_on_percent": "Power percent to use in safety mode",
"use_advanced_central_config": "Use central advanced configuration" "use_advanced_central_config": "Use central advanced configuration"
}, },
"data_description": { "data_description": {
"minimal_activation_delay": "Delay in seconds under which the equipment will not be activated", "minimal_activation_delay": "Delay in seconds under which the equipment will not be activated",
"security_delay_min": "Maximum allowed delay in minutes between two temperature measurements. Above this delay the thermostat will turn to a safety off state", "security_delay_min": "Maximum allowed delay in minutes between two temperature measurements. Above this delay the thermostat will turn to a safety off state",
"security_min_on_percent": "Minimal heating percent value for safety preset activation. Below this amount of power percent the thermostat won't go into safety preset", "security_min_on_percent": "Minimum heating percent value for safety preset activation. Below this amount of power percent the thermostat won't go into safety preset",
"security_default_on_percent": "The default heating power percent value in safety preset. Set to 0 to switch off heater in safety preset", "security_default_on_percent": "The default heating power percent value in safety preset. Set to 0 to switch off heater in safety preset",
"use_advanced_central_config": "Check to use the central advanced configuration. Uncheck to use a specific advanced configuration for this VTherm" "use_advanced_central_config": "Check to use the central advanced configuration. Uncheck to use a specific advanced configuration for this VTherm"
} }
@@ -458,7 +464,8 @@
"unknown": "Unexpected error", "unknown": "Unexpected error",
"unknown_entity": "Unknown entity id", "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", "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." "no_central_config": "You cannot check 'use central configuration' because no central configuration was found. You need to create a Versatile Thermostat of type 'Central Configuration' to use it.",
"service_configuration_format": "The format of the service configuration is wrong"
}, },
"abort": { "abort": {
"already_configured": "Device is already configured" "already_configured": "Device is already configured"
@@ -491,6 +498,22 @@
"auto_fan_high": "High", "auto_fan_high": "High",
"auto_fan_turbo": "Turbo" "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"
}
} }
}, },
"entity": { "entity": {
@@ -506,6 +529,53 @@
} }
} }
} }
},
"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"
}
} }
} }
} }

View File

@@ -12,29 +12,58 @@
"thermostat_type": "Un seul thermostat de type Configuration centrale est possible." "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",
"finalize": "Finaliser la création",
"configuration_not_complete": "Configuration incomplète"
}
},
"main": { "main": {
"title": "Ajout d'un nouveau thermostat", "title": "Ajout d'un nouveau thermostat",
"description": "Principaux attributs obligatoires", "description": "Principaux attributs obligatoires",
"data": { "data": {
"name": "Nom", "name": "Nom",
"thermostat_type": "Type de thermostat", "thermostat_type": "Type de thermostat",
"temperature_sensor_entity_id": "Température sensor entity id", "temperature_sensor_entity_id": "Capteur de température",
"external_temperature_sensor_entity_id": "Température exterieure sensor entity id", "last_seen_temperature_sensor_entity_id": "Dernière vue capteur de température",
"external_temperature_sensor_entity_id": "Capteur de température exterieure",
"cycle_min": "Durée du cycle (minutes)", "cycle_min": "Durée du cycle (minutes)",
"temp_min": "Température minimale permise", "temp_min": "Température minimale permise",
"temp_max": "Température maximale permise", "temp_max": "Température maximale permise",
"step_temperature": "Pas de température",
"device_power": "Puissance de l'équipement", "device_power": "Puissance de l'équipement",
"use_central_mode": "Autoriser le controle par le mode central ('central_mode`)", "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_window_feature": "Avec détection des ouvertures",
"use_motion_feature": "Avec détection de mouvement", "use_motion_feature": "Avec détection de mouvement",
"use_power_feature": "Avec gestion de la puissance", "use_power_feature": "Avec gestion de la puissance",
"use_presence_feature": "Avec détection de présence", "use_presence_feature": "Avec détection de présence",
"use_main_central_config": "Utiliser la configuration centrale principale" "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."
},
"data_description": {
"use_central_mode": "Cochez pour autoriser le contrôle du VTherm par la liste déroulante 'central_mode' de l'entité configuration centrale",
"external_temperature_sensor_entity_id": "Entity id du capteur de température extérieure. N'est pas utilisé si la configuration centrale est utilisée",
"use_main_central_config": "Cochez pour utiliser la configuration centrale principale. Décochez et saisissez les attributs pour utiliser une configuration spécifique principale"
} }
}, },
"type": { "type": {
@@ -45,6 +74,7 @@
"heater_entity2_id": "2ème radiateur", "heater_entity2_id": "2ème radiateur",
"heater_entity3_id": "3ème radiateur", "heater_entity3_id": "3ème radiateur",
"heater_entity4_id": "4ème radiateur", "heater_entity4_id": "4ème radiateur",
"heater_keep_alive": "keep-alive (sec)",
"proportional_function": "Algorithme", "proportional_function": "Algorithme",
"climate_entity_id": "Thermostat sous-jacent", "climate_entity_id": "Thermostat sous-jacent",
"climate_entity2_id": "2ème thermostat sous-jacent", "climate_entity2_id": "2ème thermostat sous-jacent",
@@ -58,6 +88,7 @@
"auto_regulation_mode": "Auto-régulation", "auto_regulation_mode": "Auto-régulation",
"auto_regulation_dtemp": "Seuil de régulation", "auto_regulation_dtemp": "Seuil de régulation",
"auto_regulation_periode_min": "Période minimale de régulation", "auto_regulation_periode_min": "Période minimale de régulation",
"auto_regulation_use_device_temp": "Utiliser la température interne du sous-jacent",
"inverse_switch_command": "Inverser la commande", "inverse_switch_command": "Inverser la commande",
"auto_fan_mode": " Auto ventilation mode" "auto_fan_mode": " Auto ventilation mode"
}, },
@@ -66,6 +97,7 @@
"heater_entity2_id": "Optionnel entity id du 2ème radiateur", "heater_entity2_id": "Optionnel entity id du 2ème radiateur",
"heater_entity3_id": "Optionnel entity id du 3ème radiateur", "heater_entity3_id": "Optionnel entity id du 3ème radiateur",
"heater_entity4_id": "Optionnel entity id du 4ème radiateur", "heater_entity4_id": "Optionnel entity id du 4ème radiateur",
"heater_keep_alive": "Intervalle de rafraichissement du switch en secondes. Laisser vide pour désactiver. À n'utiliser que pour les switchs qui le nécessite.",
"proportional_function": "Algorithme à utiliser (Seul TPI est disponible pour l'instant)", "proportional_function": "Algorithme à utiliser (Seul TPI est disponible pour l'instant)",
"climate_entity_id": "Entity id du thermostat sous-jacent", "climate_entity_id": "Entity id du thermostat sous-jacent",
"climate_entity2_id": "Entity id du 2ème thermostat sous-jacent", "climate_entity2_id": "Entity id du 2ème thermostat sous-jacent",
@@ -77,8 +109,9 @@
"valve_entity3_id": "Entity id de la 3ème valve", "valve_entity3_id": "Entity id de la 3ème valve",
"valve_entity4_id": "Entity id de la 4ème valve", "valve_entity4_id": "Entity id de la 4ème valve",
"auto_regulation_mode": "Ajustement automatique de la température cible", "auto_regulation_mode": "Ajustement automatique de la température cible",
"auto_regulation_dtemp": "Le seuil en ° au-dessous duquel la régulation ne sera pas envoyée", "auto_regulation_dtemp": "Le seuil en ° (ou % pour les valves) en-dessous duquel la régulation ne sera pas envoyée",
"auto_regulation_periode_min": "La durée en minutes entre deux mise à jour faites par la régulation", "auto_regulation_periode_min": "La durée en minutes entre deux mise à jour faites par la régulation",
"auto_regulation_use_device_temp": "Utiliser la temperature interne du sous-jacent pour accélérer l'auto-régulation",
"inverse_switch_command": "Inverse la commande du switch pour une installation avec fil pilote et diode", "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" "auto_fan_mode": "Active la ventilation automatiquement en cas d'écart important"
} }
@@ -98,62 +131,47 @@
} }
}, },
"presets": { "presets": {
"title": "Presets", "title": "Pre-réglages",
"description": "Pour chaque preset, donnez la température cible (0 pour ignorer le preset)", "description": "Cochez pour que ce thermostat utilise les pré-réglages de la configuration centrale. Décochez pour utiliser des entités de température spécifiques",
"data": { "data": {
"eco_temp": "Preset Eco", "use_presets_central_config": "Utiliser la configuration des pré-réglages centrale"
"comfort_temp": "Preset Comfort",
"boost_temp": "Preset Boost",
"frost_temp": "Preset Hors-gel",
"eco_ac_temp": "Preset Eco en mode AC",
"comfort_ac_temp": "Preset Comfort en mode AC",
"boost_ac_temp": "Preset Boost en mode AC",
"use_presets_central_config": "Utiliser la configuration des presets centrale"
},
"data_description": {
"eco_temp": "Température en preset Eco",
"comfort_temp": "Température en preset Comfort",
"boost_temp": "Température en preset Boost",
"frost_temp": "Température en preset Hors-gel",
"eco_ac_temp": "Température en preset Eco en mode AC",
"comfort_ac_temp": "Température en preset Comfort en mode AC",
"boost_ac_temp": "Température en preset Boost en mode AC",
"use_presets_central_config": "Cochez pour utiliser la configuration des presets centrale. Décochez et saisissez les attributs pour utiliser une configuration des presets spécifique"
} }
}, },
"window": { "window": {
"title": "Gestion d'une ouverture", "title": "Gestion d'une ouverture",
"description": "Coupe le radiateur si l'ouverture est ouverte.\nLaissez l'entity id vide si non utilisé.", "description": "Coupe le radiateur si l'ouverture est ouverte.\nLaissez l'id d'entité vide pour utiliser la détection automatique.",
"data": { "data": {
"window_sensor_entity_id": "Détecteur d'ouverture (entity id)", "window_sensor_entity_id": "Détecteur d'ouverture (entity id)",
"window_delay": "Délai avant extinction (secondes)", "window_delay": "Délai avant extinction (secondes)",
"window_auto_open_threshold": "Seuil haut de chute de température pour la détection automatique (en °/heure)", "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_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)", "window_auto_max_duration": "Durée maximum d'une extinction automatique (en min)",
"use_window_central_config": "Utiliser la configuration centrale des ouvertures" "use_window_central_config": "Utiliser la configuration centrale des ouvertures",
"window_action": "Action"
}, },
"data_description": { "data_description": {
"window_sensor_entity_id": "Laissez vide si vous n'avez de détecteur", "window_sensor_entity_id": "Laissez vide si vous n'avez de détecteur et pour utiliser la détection automatique",
"window_delay": "Le délai (en secondes) avant que le changement du détecteur soit pris en compte", "window_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 3 et 10. Laissez vide si vous n'utilisez pas la détection automatique", "window_auto_open_threshold": "Valeur recommandée: entre 3 et 10. Laissez vide si vous n'utilisez pas la détection automatique",
"window_auto_close_threshold": "Valeur recommandée: 0. Laissez vide si vous n'utilisez pas la détection automatique", "window_auto_close_threshold": "Valeur recommandée: 0. Laissez vide si vous n'utilisez pas la détection automatique",
"window_auto_max_duration": "Valeur recommandée: 60 (1 heure). Laissez vide si vous n'utilisez pas la détection automatique", "window_auto_max_duration": "Valeur recommandée: 60 (1 heure). Laissez vide si vous n'utilisez pas la détection automatique",
"use_window_central_config": "Cochez pour utiliser la configuration centrale des ouvertures. Décochez et saisissez les attributs pour utiliser une configuration spécifique des ouvertures" "use_window_central_config": "Cochez pour utiliser la configuration centrale des ouvertures. Décochez et saisissez les attributs pour utiliser une configuration spécifique des ouvertures",
"window_action": "Action a effectuer si la fenêtre est détectée comme ouverte"
} }
}, },
"motion": { "motion": {
"title": "Gestion de la détection de mouvement", "title": "Gestion de la détection de mouvement",
"description": "Le preset s'ajuste automatiquement si un mouvement est détecté\nLaissez l'entity id vide si non utilisé.\n'Preset mouvement' et 'Preset sans mouvement' doivent être choisis avec les preset à utiliser.", "description": "Le preset s'ajuste automatiquement si un mouvement est détecté\n'Preset mouvement' et 'Preset sans mouvement' doivent être choisis avec les preset à utiliser.",
"data": { "data": {
"motion_sensor_entity_id": "Détecteur de mouvement", "motion_sensor_entity_id": "Détecteur de mouvement",
"motion_delay": "Délai d'activation", "motion_delay": "Délai d'activation",
"motion_off_delay": "Délai de désactivation", "motion_off_delay": "Délai de désactivation",
"motion_preset": "Preset si mouvement", "motion_preset": "Preset si mouvement",
"no_motion_preset": "Preset si pas de mouvement", "no_motion_preset": "Preset sans mouvement",
"use_motion_central_config": "Utiliser la condfiguration centrale du mouvement" "use_motion_central_config": "Utiliser la condfiguration centrale du mouvement"
}, },
"data_description": { "data_description": {
"motion_sensor_entity_id": "Détecteur de mouvement entity id", "motion_sensor_entity_id": "Id d'entité du détecteur de mouvement",
"motion_delay": "Délai avant activation lorsqu'un mouvement est détecté (secondss)", "motion_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_off_delai": "Délai avant désactivation lorsqu'aucun mouvement n'est détecté (secondes)",
"motion_preset": "Preset à utiliser si mouvement détecté", "motion_preset": "Preset à utiliser si mouvement détecté",
@@ -179,28 +197,13 @@
}, },
"presence": { "presence": {
"title": "Gestion de la présence", "title": "Gestion de la présence",
"description": "Donnez un capteur de présence (true si quelqu'un est présent).\nEnsuite spécifiez soit un preset à utiliser, soit un offset de température à appliquer lorsque personne n'est présent.\nSi le préset est utilisé, l'offset ne sera pas pris en compte.\nLaissez l'entity id vide si la gestion de la présence est non utilisée.", "description": "Donnez un capteur de présence (true si quelqu'un est présent) et les températures cibles à utiliser en cas d'abs.",
"data": { "data": {
"presence_sensor_entity_id": "Capteur de présence", "presence_sensor_entity_id": "Capteur de présence",
"eco_away_temp": "preset Eco", "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"
"comfort_away_temp": "preset Comfort",
"boost_away_temp": "preset Boost",
"frost_away_temp": "preset Hors-gel",
"eco_ac_away_temp": "preset Eco en mode AC",
"comfort_ac_away_temp": "preset Comfort en mode AC",
"boost_ac_away_temp": "preset Boost en mode AC",
"use_presence_central_config": "Utiliser la configuration centrale de la présence"
}, },
"data_description": { "data_description": {
"presence_sensor_entity_id": "Capteur de présence entity id (true si quelqu'un est présent)", "presence_sensor_entity_id": "Id d'entité du capteur de présence"
"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",
"frost_away_temp": "Température en preset Hors-gel 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",
"use_presence_central_config": "Cochez pour utiliser la configuration centrale de la présence. Décochez et saisissez les attributs pour utiliser une configuration spécifique de la présence"
} }
}, },
"advanced": { "advanced": {
@@ -220,6 +223,18 @@
"security_default_on_percent": "Valeur par défaut pour le pourcentage de chauffage en mode sécurité. Mettre 0 pour éteindre le radiateur en mode sécurité", "security_default_on_percent": "Valeur par défaut pour le pourcentage de chauffage en mode sécurité. Mettre 0 pour éteindre le radiateur en mode sécurité",
"use_advanced_central_config": "Cochez pour utiliser la configuration centrale avancée. Décochez et saisissez les attributs pour utiliser une configuration spécifique avancée" "use_advanced_central_config": "Cochez pour utiliser la configuration centrale avancée. Décochez et saisissez les attributs pour utiliser une configuration spécifique avancée"
} }
},
"central_boiler": {
"title": "Contrôle de la chaudière centrale",
"description": "Donnez les services à appeler pour allumer/éteindre la chaudière centrale. Laissez vide, si aucun appel de service ne doit être effectué (dans ce cas, vous devrez gérer vous même l'allumage/extinction de votre chaudière centrale). Le service a appelé doit être formatté comme suit: `entity_id/service_name[/attribut:valeur]` (/attribut:valeur est facultatif)\nPar exemple:\n- pour allumer un switch: `switch.controle_chaudiere/switch.turn_on`\n- pour éteindre un switch: `switch.controle_chaudiere/switch.turn_off`\n- pour programmer la chaudière sur 25° et ainsi forcer son allumage: `climate.thermostat_chaudiere/climate.set_temperature/temperature:25`\n- pour envoyer 10° à la chaudière et ainsi forcer son extinction: `climate.thermostat_chaudiere/climate.set_temperature/temperature:10`",
"data": {
"central_boiler_activation_service": "Commande pour allumer",
"central_boiler_deactivation_service": "Commande pour éteindre"
},
"data_description": {
"central_boiler_activation_service": "Commande à éxecuter pour allumer la chaudière centrale au format entity_id/service_name[/attribut:valeur]",
"central_boiler_deactivation_service": "Commande à éxecuter pour étiendre la chaudière centrale au format entity_id/service_name[/attribut:valeur]"
}
} }
}, },
"error": { "error": {
@@ -244,29 +259,58 @@
"thermostat_type": "Un seul thermostat de type Configuration centrale est possible." "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",
"finalize": "Finaliser les modifications",
"configuration_not_complete": "Configuration incomplète"
}
},
"main": { "main": {
"title": "Attributs - {name}", "title": "Attributs - {name}",
"description": "Principaux attributs obligatoires", "description": "Principaux attributs obligatoires",
"data": { "data": {
"name": "Nom", "name": "Nom",
"thermostat_type": "Type de thermostat", "thermostat_type": "Type de thermostat",
"temperature_sensor_entity_id": "Température sensor entity id", "temperature_sensor_entity_id": "Capteur de température",
"external_temperature_sensor_entity_id": "Température exterieure sensor entity id", "last_seen_temperature_sensor_entity_id": "Dernière vue capteur de température",
"external_temperature_sensor_entity_id": "Capteur de température exterieure",
"cycle_min": "Durée du cycle (minutes)", "cycle_min": "Durée du cycle (minutes)",
"temp_min": "Température minimale permise", "temp_min": "Température minimale permise",
"temp_max": "Température maximale permise", "temp_max": "Température maximale permise",
"step_temperature": "Pas de température",
"device_power": "Puissance de l'équipement", "device_power": "Puissance de l'équipement",
"use_central_mode": "Autoriser le controle par le mode central ('central_mode`)", "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_window_feature": "Avec détection des ouvertures",
"use_motion_feature": "Avec détection de mouvement", "use_motion_feature": "Avec détection de mouvement",
"use_power_feature": "Avec gestion de la puissance", "use_power_feature": "Avec gestion de la puissance",
"use_presence_feature": "Avec détection de présence", "use_presence_feature": "Avec détection de présence",
"use_main_central_config": "Utiliser la configuration centrale" "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."
},
"data_description": {
"use_central_mode": "Cochez pour autoriser le contrôle du VTherm par la liste déroulante 'central_mode' de l'entité configuration centrale",
"use_main_central_config": "Cochez pour utiliser la configuration centrale. Décochez et saisissez les attributs pour utiliser une configuration spécifique",
"external_temperature_sensor_entity_id": "Entity id du capteur de température extérieure. N'est pas utilisé si la configuration centrale est utilisée"
} }
}, },
"type": { "type": {
@@ -277,6 +321,7 @@
"heater_entity2_id": "2ème radiateur", "heater_entity2_id": "2ème radiateur",
"heater_entity3_id": "3ème radiateur", "heater_entity3_id": "3ème radiateur",
"heater_entity4_id": "4ème radiateur", "heater_entity4_id": "4ème radiateur",
"heater_keep_alive": "Keep-alive (sec)",
"proportional_function": "Algorithme", "proportional_function": "Algorithme",
"climate_entity_id": "Thermostat sous-jacent", "climate_entity_id": "Thermostat sous-jacent",
"climate_entity2_id": "2ème thermostat sous-jacent", "climate_entity2_id": "2ème thermostat sous-jacent",
@@ -290,14 +335,16 @@
"auto_regulation_mode": "Auto-regulation", "auto_regulation_mode": "Auto-regulation",
"auto_regulation_dtemp": "Seuil de régulation", "auto_regulation_dtemp": "Seuil de régulation",
"auto_regulation_periode_min": "Période minimale de régulation", "auto_regulation_periode_min": "Période minimale de régulation",
"auto_regulation_use_device_temp": "Utiliser la température interne du sous-jacent",
"inverse_switch_command": "Inverser la commande", "inverse_switch_command": "Inverser la commande",
"auto_fan_mode": " Auto fan mode" "auto_fan_mode": "Auto fan mode"
}, },
"data_description": { "data_description": {
"heater_entity_id": "Entity id du 1er radiateur obligatoire", "heater_entity_id": "Entity id du 1er radiateur obligatoire",
"heater_entity2_id": "Optionnel entity id du 2ème radiateur", "heater_entity2_id": "Optionnel entity id du 2ème radiateur",
"heater_entity3_id": "Optionnel entity id du 3ème radiateur", "heater_entity3_id": "Optionnel entity id du 3ème radiateur",
"heater_entity4_id": "Optionnel entity id du 4ème radiateur", "heater_entity4_id": "Optionnel entity id du 4ème radiateur",
"heater_keep_alive": "Intervalle de rafraichissement du switch en secondes. Laisser vide pour désactiver. À n'utiliser que pour les switchs qui le nécessite.",
"proportional_function": "Algorithme à utiliser (Seul TPI est disponible pour l'instant)", "proportional_function": "Algorithme à utiliser (Seul TPI est disponible pour l'instant)",
"climate_entity_id": "Entity id du thermostat sous-jacent", "climate_entity_id": "Entity id du thermostat sous-jacent",
"climate_entity2_id": "Entity id du 2ème thermostat sous-jacent", "climate_entity2_id": "Entity id du 2ème thermostat sous-jacent",
@@ -309,8 +356,9 @@
"valve_entity3_id": "Entity id de la 3ème valve", "valve_entity3_id": "Entity id de la 3ème valve",
"valve_entity4_id": "Entity id de la 4ème valve", "valve_entity4_id": "Entity id de la 4ème valve",
"auto_regulation_mode": "Ajustement automatique de la consigne", "auto_regulation_mode": "Ajustement automatique de la consigne",
"auto_regulation_dtemp": "Le seuil en ° au-dessous duquel la régulation ne sera pas envoyée", "auto_regulation_dtemp": "Le seuil en ° (ou % pour les valves) en-dessous duquel la régulation ne sera pas envoyée",
"auto_regulation_periode_min": "La durée en minutes entre deux mise à jour faites par la régulation", "auto_regulation_periode_min": "La durée en minutes entre deux mise à jour faites par la régulation",
"auto_regulation_use_device_temp": "Utiliser la temperature interne du sous-jacent pour accélérer l'auto-régulation",
"inverse_switch_command": "Inverse la commande du switch pour une installation avec fil pilote et diode", "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" "auto_fan_mode": "Active la ventilation automatiquement en cas d'écart important"
} }
@@ -325,46 +373,31 @@
}, },
"presets": { "presets": {
"title": "Pre-réglages - {name}", "title": "Pre-réglages - {name}",
"description": "Réglage des presets. Donnez la température cible (0 pour ignorer le preset)", "description": "Cochez pour que ce thermostat utilise les pré-réglages de la configuration centrale. Décochez pour utiliser des entités de température spécifiques",
"data": { "data": {
"eco_temp": "Preset Eco", "use_presets_central_config": "Utiliser la configuration des pré-réglages centrale"
"comfort_temp": "Preset Comfort",
"boost_temp": "Preset Boost",
"frost_temp": "Preset Hors-gel",
"eco_ac_temp": "Preset Eco en mode AC",
"comfort_ac_temp": "Preset Comfort en mode AC",
"boost_ac_temp": "Preset Boost en mode AC",
"use_presets_central_config": "Utiliser la configuration centrale des presets"
},
"data_description": {
"eco_temp": "Température en preset Eco",
"comfort_temp": "Température en preset Comfort",
"boost_temp": "Température en preset Boost",
"frost_temp": "Température en preset Hors-gel",
"eco_ac_temp": "Température en preset Eco en mode AC",
"comfort_ac_temp": "Température en preset Comfort en mode AC",
"boost_ac_temp": "Température en preset Boost en mode AC",
"use_presets_central_config": "Cochez pour utiliser la configuration centrale des presets. Décochez et saisissez les attributs pour utiliser une configuration des presets spécifique"
} }
}, },
"window": { "window": {
"title": "Ouverture - {name}", "title": "Ouverture - {name}",
"description": "Gestion des ouvertures. Coupe le radiateur si l'ouverture est ouverte.", "description": "Coupe le radiateur si l'ouverture est ouverte.\nLaissez l'id d'entité vide pour utiliser la détection automatique.",
"data": { "data": {
"window_sensor_entity_id": "Détecteur d'ouverture (entity id)", "window_sensor_entity_id": "Détecteur d'ouverture (entity id)",
"window_delay": "Délai avant extinction (secondes)", "window_delay": "Délai avant extinction (secondes)",
"window_auto_open_threshold": "Seuil haut de chute de température pour la détection automatique (en °/heure)", "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_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)", "window_auto_max_duration": "Durée maximum d'une extinction automatique (en min)",
"use_window_central_config": "Utiliser la configuration centrale des ouvertures" "use_window_central_config": "Utiliser la configuration centrale des ouvertures",
"window_action": "Action"
}, },
"data_description": { "data_description": {
"window_sensor_entity_id": "Laissez vide si vous n'avez de détecteur", "window_sensor_entity_id": "Laissez vide si vous n'avez de détecteur et pour utiliser la détection automatique",
"window_delay": "Le délai (en secondes) avant que le changement du détecteur soit pris en compte", "window_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 3 et 10. Laissez vide si vous n'utilisez pas la détection automatique", "window_auto_open_threshold": "Valeur recommandée: entre 3 et 10. Laissez vide si vous n'utilisez pas la détection automatique",
"window_auto_close_threshold": "Valeur recommandée: 0. Laissez vide si vous n'utilisez pas la détection automatique", "window_auto_close_threshold": "Valeur recommandée: 0. Laissez vide si vous n'utilisez pas la détection automatique",
"window_auto_max_duration": "Valeur recommandée: 60 (1 heure). Laissez vide si vous n'utilisez pas la détection automatique", "window_auto_max_duration": "Valeur recommandée: 60 (1 heure). Laissez vide si vous n'utilisez pas la détection automatique",
"use_window_central_config": "Cochez pour utiliser la configuration centrale des ouvertures. Décochez et saisissez les attributs pour utiliser une configuration spécifique des ouvertures" "use_window_central_config": "Cochez pour utiliser la configuration centrale des ouvertures. Décochez et saisissez les attributs pour utiliser une configuration spécifique des ouvertures",
"window_action": "Action a effectuer si la fenêtre est détectée comme ouverte"
} }
}, },
"motion": { "motion": {
@@ -375,11 +408,11 @@
"motion_delay": "Délai d'activation", "motion_delay": "Délai d'activation",
"motion_off_delay": "Délai de désactivation", "motion_off_delay": "Délai de désactivation",
"motion_preset": "Preset si mouvement", "motion_preset": "Preset si mouvement",
"no_motion_preset": "Preset si pas de mouvement", "no_motion_preset": "Preset sans mouvement",
"use_motion_central_config": "Utiliser la condfiguration centrale du mouvement" "use_motion_central_config": "Utiliser la condfiguration centrale du mouvement"
}, },
"data_description": { "data_description": {
"motion_sensor_entity_id": "Détecteur de mouvement entity id", "motion_sensor_entity_id": "Id d'entité du détecteur de mouvement",
"motion_delay": "Délai avant activation lorsqu'un mouvement est détecté (secondss)", "motion_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_off_delai": "Délai avant désactivation lorsqu'aucun mouvement n'est détecté (secondes)",
"motion_preset": "Preset à utiliser si mouvement détecté", "motion_preset": "Preset à utiliser si mouvement détecté",
@@ -405,28 +438,13 @@
}, },
"presence": { "presence": {
"title": "Présence - {name}", "title": "Présence - {name}",
"description": "Gestion de la présence. Le capteur de présence doit être true ou home si quelqu'un est présent.", "description": "Donnez un capteur de présence (true si quelqu'un est présent) et les températures cibles à utiliser en cas d'abs.",
"data": { "data": {
"presence_sensor_entity_id": "Capteur de présence", "presence_sensor_entity_id": "Capteur de présence",
"eco_away_temp": "preset Eco", "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"
"comfort_away_temp": "preset Comfort",
"boost_away_temp": "preset Boost",
"frost_away_temp": "preset Hors-gel",
"eco_ac_away_temp": "preset Eco en mode AC",
"comfort_ac_away_temp": "preset Comfort en mode AC",
"boost_ac_away_temp": "preset Boost en mode AC",
"use_presence_central_config": "Utiliser la configuration centrale de la présence"
}, },
"data_description": { "data_description": {
"presence_sensor_entity_id": "Capteur de présence entity id (true si quelqu'un est présent)", "presence_sensor_entity_id": "Id d'entité du capteur de présence"
"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",
"frost_away_temp": "Température en preset Hors-gel 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",
"use_presence_central_config": "Cochez pour utiliser la configuration centrale de la présence. Décochez et saisissez les attributs pour utiliser une configuration spécifique de la présence"
} }
}, },
"advanced": { "advanced": {
@@ -446,13 +464,26 @@
"security_default_on_percent": "Valeur par défaut pour le pourcentage de chauffage en mode sécurité. Mettre 0 pour éteindre le radiateur en mode sécurité", "security_default_on_percent": "Valeur par défaut pour le pourcentage de chauffage en mode sécurité. Mettre 0 pour éteindre le radiateur en mode sécurité",
"use_advanced_central_config": "Cochez pour utiliser la configuration centrale avancée. Décochez et saisissez les attributs pour utiliser une configuration spécifique avancée" "use_advanced_central_config": "Cochez pour utiliser la configuration centrale avancée. Décochez et saisissez les attributs pour utiliser une configuration spécifique avancée"
} }
},
"central_boiler": {
"title": "Contrôle de la chaudière centrale - {name}",
"description": "Donnez les services à appeler pour allumer/éteindre la chaudière centrale. Laissez vide, si aucun appel de service ne doit être effectué (dans ce cas, vous devrez gérer vous même l'allumage/extinction de votre chaudière centrale). Le service a appelé doit être formatté comme suit: `entity_id/service_name[/attribut:valeur]` (/attribut:valeur est facultatif)\nPar exemple:\n- pour allumer un switch: `switch.controle_chaudiere/switch.turn_on`\n- pour éteindre un switch: `switch.controle_chaudiere/switch.turn_off`\n- pour programmer la chaudière sur 25° et ainsi forcer son allumage: `climate.thermostat_chaudiere/climate.set_temperature/temperature:25`\n- pour envoyer 10° à la chaudière et ainsi forcer son extinction: `climate.thermostat_chaudiere/climate.set_temperature/temperature:10`",
"data": {
"central_boiler_activation_service": "Commande pour allumer",
"central_boiler_deactivation_service": "Commande pour éteindre"
},
"data_description": {
"central_boiler_activation_service": "Commande à éxecuter pour allumer la chaudière centrale au format entity_id/service_name[/attribut:valeur]",
"central_boiler_deactivation_service": "Commande à éxecuter pour étiendre la chaudière centrale au format entity_id/service_name[/attribut:valeur]"
}
} }
}, },
"error": { "error": {
"unknown": "Erreur inattendue", "unknown": "Erreur inattendue",
"unknown_entity": "entity id inconnu", "unknown_entity": "entity id inconnu",
"window_open_detection_method": "Une seule méthode de détection des ouvertures ouvertes doit être utilisée. Utilisez le détecteur d'ouverture ou les seuils de température mais pas les deux.", "window_open_detection_method": "Une seule méthode de détection des ouvertures ouvertes doit être utilisée. Utilisez le détecteur d'ouverture ou les seuils de température mais pas les deux.",
"no_central_config": "Vous ne pouvez pas cocher 'Utiliser la configuration centrale' car aucune configuration centrale n'a été trouvée. Vous devez créer un Versatile Thermostat de type 'Central Configuration' pour pouvoir l'utiliser." "no_central_config": "Vous ne pouvez pas cocher 'Utiliser la configuration centrale' car aucune configuration centrale n'a été trouvée. Vous devez créer un Versatile Thermostat de type 'Central Configuration' pour pouvoir l'utiliser.",
"service_configuration_format": "Mauvais format de la configuration du service"
}, },
"abort": { "abort": {
"already_configured": "Le device est déjà configuré" "already_configured": "Le device est déjà configuré"
@@ -485,6 +516,22 @@
"auto_fan_high": "Forte", "auto_fan_high": "Forte",
"auto_fan_turbo": "Turbo" "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)"
}
} }
}, },
"entity": { "entity": {
@@ -500,6 +547,53 @@
} }
} }
} }
},
"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"
}
} }
} }
} }

View File

@@ -29,6 +29,7 @@
"heater_entity2_id": "Secondo riscaldatore", "heater_entity2_id": "Secondo riscaldatore",
"heater_entity3_id": "Terzo riscaldatore", "heater_entity3_id": "Terzo riscaldatore",
"heater_entity4_id": "Quarto riscaldatore", "heater_entity4_id": "Quarto riscaldatore",
"heater_keep_alive": "Intervallo keep-alive dell'interruttore in secondi",
"proportional_function": "Algoritmo", "proportional_function": "Algoritmo",
"climate_entity_id": "Primo termostato", "climate_entity_id": "Primo termostato",
"climate_entity2_id": "Secondo termostato", "climate_entity2_id": "Secondo termostato",
@@ -41,13 +42,14 @@
"valve_entity4_id": "Quarta valvola", "valve_entity4_id": "Quarta valvola",
"auto_regulation_mode": "Autoregolamentazione", "auto_regulation_mode": "Autoregolamentazione",
"inverse_switch_command": "Comando inverso", "inverse_switch_command": "Comando inverso",
"auto_fan_mode": " Auto fan mode" "auto_fan_mode": "Auto fan mode"
}, },
"data_description": { "data_description": {
"heater_entity_id": "Entity id obbligatoria del primo riscaldatore", "heater_entity_id": "Entity id obbligatoria del primo riscaldatore",
"heater_entity2_id": "Entity id del secondo riscaldatore facoltativo. Lasciare vuoto se non utilizzato", "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_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_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)", "proportional_function": "Algoritmo da utilizzare (il TPI per adesso è l'unico)",
"climate_entity_id": "Entity id del primo termostato", "climate_entity_id": "Entity id del primo termostato",
"climate_entity2_id": "Entity id del secondo termostato", "climate_entity2_id": "Entity id del secondo termostato",
@@ -60,7 +62,7 @@
"valve_entity4_id": "Entity id della quarta valvola", "valve_entity4_id": "Entity id della quarta valvola",
"auto_regulation_mode": "Regolazione automatica della temperatura target", "auto_regulation_mode": "Regolazione automatica della temperatura target",
"inverse_switch_command": "Inverte il controllo dell'interruttore per un'installazione con filo pilota e diodo", "inverse_switch_command": "Inverte il controllo dell'interruttore per un'installazione con filo pilota e diodo",
"auto_fan_mode": " Automatically activate fan when huge heating/cooling is necessary" "auto_fan_mode": "Automatically activate fan when huge heating/cooling is necessary"
} }
}, },
"tpi": { "tpi": {
@@ -191,6 +193,7 @@
"heater_entity2_id": "Secondo riscaldatore", "heater_entity2_id": "Secondo riscaldatore",
"heater_entity3_id": "Terzo riscaldatore", "heater_entity3_id": "Terzo riscaldatore",
"heater_entity4_id": "Quarto riscaldatore", "heater_entity4_id": "Quarto riscaldatore",
"heater_keep_alive": "Intervallo keep-alive dell'interruttore in secondi",
"proportional_function": "Algoritmo", "proportional_function": "Algoritmo",
"climate_entity_id": "Primo termostato", "climate_entity_id": "Primo termostato",
"climate_entity2_id": "Secondo termostato", "climate_entity2_id": "Secondo termostato",
@@ -203,13 +206,14 @@
"valve_entity4_id": "Quarta valvola", "valve_entity4_id": "Quarta valvola",
"auto_regulation_mode": "Autoregolamentazione", "auto_regulation_mode": "Autoregolamentazione",
"inverse_switch_command": "Comando inverso", "inverse_switch_command": "Comando inverso",
"auto_fan_mode": " Auto fan mode" "auto_fan_mode": "Auto fan mode"
}, },
"data_description": { "data_description": {
"heater_entity_id": "Entity id obbligatoria del primo riscaldatore", "heater_entity_id": "Entity id obbligatoria del primo riscaldatore",
"heater_entity2_id": "Entity id del secondo riscaldatore facoltativo. Lasciare vuoto se non utilizzato", "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_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_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)", "proportional_function": "Algoritmo da utilizzare (il TPI per adesso è l'unico)",
"climate_entity_id": "Entity id del primo termostato", "climate_entity_id": "Entity id del primo termostato",
"climate_entity2_id": "Entity id del secondo termostato", "climate_entity2_id": "Entity id del secondo termostato",
@@ -222,7 +226,7 @@
"valve_entity4_id": "Entity id della quarta valvola", "valve_entity4_id": "Entity id della quarta valvola",
"auto_regulation_mode": "Autoregolamentazione", "auto_regulation_mode": "Autoregolamentazione",
"inverse_switch_command": "Inverte il controllo dell'interruttore per un'installazione con filo pilota e diodo", "inverse_switch_command": "Inverte il controllo dell'interruttore per un'installazione con filo pilota e diodo",
"auto_fan_mode": " Automatically activate fan when huge heating/cooling is necessary" "auto_fan_mode": "Automatically activate fan when huge heating/cooling is necessary"
} }
}, },
"tpi": { "tpi": {

View File

@@ -4,21 +4,66 @@
"flow_title": "Všestranná konfigurácia termostatu", "flow_title": "Všestranná konfigurácia termostatu",
"step": { "step": {
"user": { "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", "title": "Pridajte nový všestranný termostat",
"description": "Hlavné povinné atribúty", "description": "Hlavné povinné atribúty",
"data": { "data": {
"name": "Názov", "name": "Názov",
"thermostat_type": "Termostat typ", "thermostat_type": "Termostat typ",
"temperature_sensor_entity_id": "ID entity snímača teploty", "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", "external_temperature_sensor_entity_id": "ID entity externého snímača teploty",
"cycle_min": "Trvanie cyklu (minúty)", "cycle_min": "Trvanie cyklu (minúty)",
"temp_min": "Minimálna povolená teplota", "temp_min": "Minimálna povolená teplota",
"temp_max": "Maximálna povolená teplota", "temp_max": "Maximálna povolená teplota",
"step_temperature": "Krok teploty",
"device_power": "Napájanie zariadenia", "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_window_feature": "Použite detekciu okien",
"use_motion_feature": "Použite detekciu pohybu", "use_motion_feature": "Použite detekciu pohybu",
"use_power_feature": "Použite správu napájania", "use_power_feature": "Použite správu napájania",
"use_presence_feature": "Použite detekciu prítomnosti" "use_presence_feature": "Použite detekciu prítomnosti",
"use_central_boiler_feature": "Použite centrálny kotol. Začiarknutím tohto políčka pridáte ovládanie do centrálneho kotla. Po zaškrtnutí tohto políčka budete musieť nakonfigurovať VTherm, ktorý bude mať ovládanie centrálneho kotla, aby sa prejavilo. Ak jeden VTherm vyžaduje ohrev, kotol sa zapne. Ak žiadny VTherm nevyžaduje ohrev, kotol sa vypne. Príkazy na zapnutie/vypnutie centrálneho kotla sú uvedené na príslušnej konfiguračnej stránke"
} }
}, },
"type": { "type": {
@@ -29,6 +74,7 @@
"heater_entity2_id": "2. spínač ohrievača", "heater_entity2_id": "2. spínač ohrievača",
"heater_entity3_id": "3. spínač ohrievača", "heater_entity3_id": "3. spínač ohrievača",
"heater_entity4_id": "4. 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", "proportional_function": "Algoritmus",
"climate_entity_id": "1. základná klíma", "climate_entity_id": "1. základná klíma",
"climate_entity2_id": "2. základná klíma", "climate_entity2_id": "2. základná klíma",
@@ -39,17 +85,19 @@
"valve_entity2_id": "2. ventil číslo", "valve_entity2_id": "2. ventil číslo",
"valve_entity3_id": "3. ventil číslo", "valve_entity3_id": "3. ventil číslo",
"valve_entity4_id": "4. ventil číslo", "valve_entity4_id": "4. ventil číslo",
"auto_regulation_mode": "Self-regulation", "auto_regulation_mode": "Samoregulácia",
"auto_regulation_dtemp": "Regulation threshold", "auto_regulation_dtemp": "Regulačný prah",
"auto_regulation_periode_min": "Regulation minimal period", "auto_regulation_periode_min": "Regulačné minimálne obdobie",
"inverse_switch_command": "Inverse switch command", "auto_regulation_use_device_temp": "Použite vnútornú teplotu podkladu",
"auto_fan_mode": " Auto fan mode" "inverse_switch_command": "Inverzný prepínací príkaz",
"auto_fan_mode": "Režim automatického ventilátora"
}, },
"data_description": { "data_description": {
"heater_entity_id": "ID entity povinného ohrievača", "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_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_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_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ý)", "proportional_function": "Algoritmus, ktorý sa má použiť (TPI je zatiaľ jediný)",
"climate_entity_id": "ID základnej klimatickej entity", "climate_entity_id": "ID základnej klimatickej entity",
"climate_entity2_id": "2. základné identifikačné číslo klimatickej entity", "climate_entity2_id": "2. základné identifikačné číslo klimatickej entity",
@@ -60,11 +108,12 @@
"valve_entity2_id": "2. ventil číslo entity id", "valve_entity2_id": "2. ventil číslo entity id",
"valve_entity3_id": "3. ventil číslo entity id", "valve_entity3_id": "3. ventil číslo entity id",
"valve_entity4_id": "4. ventil číslo entity id", "valve_entity4_id": "4. ventil číslo entity id",
"auto_regulation_mode": "Auto adjustment of the target temperature", "auto_regulation_mode": "Automatické nastavenie cieľovej teploty",
"auto_regulation_dtemp": "The threshold in ° under which the temperature change will not be send", "auto_regulation_dtemp": "Hranica v °, pod ktorou sa zmena teploty neodošle",
"auto_regulation_periode_min": "Duration in minutes between two regulation update", "auto_regulation_periode_min": "Trvanie v minútach medzi dvoma aktualizáciami predpisov",
"inverse_switch_command": "For switch with pilot wire and diode you may need to inverse the command", "auto_regulation_use_device_temp": "Na urýchlenie samoregulácie použite prípadný vnútorný snímač teploty podkladu",
"auto_fan_mode": " Automatically activate fan when huge heating/cooling is necessary" "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": { "tpi": {
@@ -72,20 +121,20 @@
"description": "Časovo proporcionálne integrálne atribúty", "description": "Časovo proporcionálne integrálne atribúty",
"data": { "data": {
"tpi_coef_int": "Koeficient na použitie pre vnútornú teplotnú deltu", "tpi_coef_int": "Koeficient na použitie pre vnútornú teplotnú deltu",
"tpi_coef_ext": "Koeficient na použitie pre deltu vonkajšej teploty" "tpi_coef_ext": "Koeficient na použitie pre deltu vonkajšej teploty",
"use_tpi_central_config": "Použite centrálnu konfiguráciu TPI"
},
"data_description": {
"tpi_coef_int": "Koeficient na použitie pre vnútornú teplotnú deltu",
"tpi_coef_ext": "Koeficient na použitie pre deltu vonkajšej teploty",
"use_tpi_central_config": "Začiarknite, ak chcete použiť centrálnu konfiguráciu TPI. Zrušte začiarknutie, ak chcete použiť špecifickú konfiguráciu TPI pre tento VTherm"
} }
}, },
"presets": { "presets": {
"title": "Predvoľby", "title": "Predvoľby",
"description": "Pre každú predvoľbu zadajte cieľovú teplotu (0, ak chcete predvoľbu ignorovať)", "description": "Pre každú predvoľbu zadajte cieľovú teplotu (0, ak chcete predvoľbu ignorovať)",
"data": { "data": {
"eco_temp": "Teplota v predvoľbe Eco", "use_presets_central_config": "Použite konfiguráciu centrálnych predvolieb"
"comfort_temp": "Prednastavená teplota v komfortnom režime",
"boost_temp": "Teplota v prednastavení Boost",
"frost_temp": "Teplota v prednastavení Frost protection",
"eco_ac_temp": "Teplota v režime Eco prednastavená pre režim AC",
"comfort_ac_temp": "Teplota v režime Comfort je prednastavená pre režim AC",
"boost_ac_temp": "Prednastavená teplota v režime Boost pre režim AC"
} }
}, },
"window": { "window": {
@@ -96,14 +145,18 @@
"window_delay": "Oneskorenie snímača okna (sekundy)", "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_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_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)" "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": { "data_description": {
"window_sensor_entity_id": "Nechajte prázdne, ak nemáte použiť žiadny okenný senzor", "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_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_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_close_threshold": "Odporúčaná hodnota: 0. Ak sa nepoužíva automatická detekcia otvoreného okna, nechajte prázdne",
"window_auto_max_duration": "Odporúčaná hodnota: 60 (jedna hodina). Ak sa nepoužíva automatická detekcia otvoreného okna, nechajte prázdne" "window_auto_max_duration": "Odporúčaná hodnota: 60 (jedna hodina). Ak sa nepoužíva automatická detekcia otvoreného okna, nechajte prázdne",
"use_window_central_config": "Začiarknite, ak chcete použiť konfiguráciu centrálneho okna. Zrušte začiarknutie, ak chcete použiť špecifickú konfiguráciu okna pre tento VTherm",
"window_action": "Akcia, ktorá sa má vykonať, ak sa okno zistí ako otvorené"
} }
}, },
"motion": { "motion": {
@@ -114,14 +167,16 @@
"motion_delay": "Oneskorenie aktivácie", "motion_delay": "Oneskorenie aktivácie",
"motion_off_delay": "Oneskorenie deaktivácie", "motion_off_delay": "Oneskorenie deaktivácie",
"motion_preset": "Prednastavený pohyb", "motion_preset": "Prednastavený pohyb",
"no_motion_preset": "Žiadna predvoľba pohybu" "no_motion_preset": "Žiadna predvoľba pohybu",
"use_motion_central_config": "Použite centrálnu konfiguráciu pohybu"
}, },
"data_description": { "data_description": {
"motion_sensor_entity_id": "ID entity snímača pohybu", "motion_sensor_entity_id": "ID entity snímača pohybu",
"motion_delay": "Oneskorenie aktivácie pohybu (sekundy)", "motion_delay": "Oneskorenie aktivácie pohybu (sekundy)",
"motion_off_delay": "Oneskorenie deaktivácie pohybu (sekundy)", "motion_off_delay": "Oneskorenie deaktivácie pohybu (sekundy)",
"motion_preset": "Prednastavené na použitie pri detekcii pohybu", "motion_preset": "Prednastavené na použitie pri detekcii pohybu",
"no_motion_preset": "Prednastavené na použitie, keď nie je detekovaný žiadny pohyb" "no_motion_preset": "Prednastavené na použitie, keď nie je detekovaný žiadny pohyb",
"use_motion_central_config": "Začiarknite, ak chcete použiť konfiguráciu centrálneho pohybu. Zrušte začiarknutie, ak chcete použiť špecifickú konfiguráciu pohybu pre tento VTherm"
} }
}, },
"power": { "power": {
@@ -130,21 +185,25 @@
"data": { "data": {
"power_sensor_entity_id": "ID entity snímača výkonu", "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", "max_power_sensor_entity_id": "ID entity snímača maximálneho výkonu",
"power_temp": "Teplota pre zníženie výkonu" "power_temp": "Teplota pre zníženie výkonu",
"use_power_central_config": "Použite centrálnu konfiguráciu napájania"
},
"data_description": {
"power_sensor_entity_id": "ID entity snímača výkonu",
"max_power_sensor_entity_id": "ID entity snímača maximálneho výkonu",
"power_temp": "Teplota pre zníženie výkonu",
"use_power_central_config": "Začiarknite, ak chcete použiť konfiguráciu centrálneho napájania. Zrušte začiarknutie, ak chcete použiť špecifickú konfiguráciu napájania pre tento VTherm"
} }
}, },
"presence": { "presence": {
"title": "Riadenie prítomnosti", "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.", "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": { "data": {
"presence_sensor_entity_id": "ID entity senzora prítomnosti", "presence_sensor_entity_id": "Senzora prítomnosti",
"eco_away_temp": "Teplota v prednastavenej Eco, keď nie je žiadna prítomnosť", "use_presence_central_config": "Použite konfiguráciu centrálnej prítomnosti teploty. Ak chcete použiť špecifické teplotné entity, zrušte výber"
"comfort_away_temp": "Teplota v režime Comfort je prednastavená, keď nie je prítomný", },
"boost_away_temp": "Prednastavená teplota v režime Boost, keď nie je prítomný", "data_description": {
"frost_away_temp": "Prednastavená teplota v režime Frost protection, keď nie je prítomný", "presence_sensor_entity_id": "ID entity senzora prítomnosti"
"eco_ac_away_temp": "Teplota v prednastavenej Eco, keď nie je prítomná v režime AC",
"comfort_ac_away_temp": "Teplota v režime Comfort je prednastavená, keď nie je prítomný v režime AC",
"boost_ac_away_temp": "Teplota v prednastavenom Boost, keď nie je prítomný v režime AC"
} }
}, },
"advanced": { "advanced": {
@@ -154,20 +213,23 @@
"minimal_activation_delay": "Minimálne oneskorenie aktivácie", "minimal_activation_delay": "Minimálne oneskorenie aktivácie",
"security_delay_min": "Bezpečnostné oneskorenie (v minútach)", "security_delay_min": "Bezpečnostné oneskorenie (v minútach)",
"security_min_on_percent": "Minimálne percento výkonu na aktiváciu bezpečnostného režimu", "security_min_on_percent": "Minimálne percento výkonu na aktiváciu bezpečnostného režimu",
"security_default_on_percent": "Percento výkonu na použitie v bezpečnostnom režime" "security_default_on_percent": "Percento výkonu na použitie v bezpečnostnom režime",
"use_advanced_central_config": "Použite centrálnu rozšírenú konfiguráciu"
}, },
"data_description": { "data_description": {
"minimal_activation_delay": "Oneskorenie v sekundách, pri ktorom sa zariadenie neaktivuje", "minimal_activation_delay": "Oneskorenie v sekundách, pri ktorom sa zariadenie neaktivuje",
"security_delay_min": "Maximálne povolené oneskorenie v minútach medzi dvoma meraniami teploty. Po uplynutí tohto oneskorenia sa termostat prepne do bezpečnostného vypnutého stavu", "security_delay_min": "Maximálne povolené oneskorenie v minútach medzi dvoma meraniami teploty. Po uplynutí tohto oneskorenia sa termostat prepne do bezpečnostného vypnutého stavu",
"security_min_on_percent": "Minimálna percentuálna hodnota ohrevu pre aktiváciu prednastavenej bezpečnosti. Pod týmto percentom výkonu termostat neprejde do prednastavenia zabezpečenia", "security_min_on_percent": "Minimálna percentuálna hodnota ohrevu pre aktiváciu prednastavenej bezpečnosti. Pod týmto percentom výkonu termostat neprejde do prednastavenia zabezpečenia",
"security_default_on_percent": "Predvolená percentuálna hodnota vykurovacieho výkonu v bezpečnostnej predvoľbe. Nastavte na 0, ak chcete vypnúť ohrievač v zabezpečenom stave" "security_default_on_percent": "Predvolená percentuálna hodnota vykurovacieho výkonu v bezpečnostnej predvoľbe. Nastavte na 0, ak chcete vypnúť ohrievač v zabezpečenom stave",
"use_advanced_central_config": "Začiarknite, ak chcete použiť centrálnu rozšírenú konfiguráciu. Zrušte začiarknutie, ak chcete použiť špecifickú rozšírenú konfiguráciu pre tento VTherm"
} }
} }
}, },
"error": { "error": {
"unknown": "Neočakávaná chyba", "unknown": "Neočakávaná chyba",
"unknown_entity": "Neznáme ID entity", "unknown_entity": "Neznáme ID entity",
"window_open_detection_method": "Mala by sa použiť iba jedna metóda detekcie otvoreného okna. Použite senzor alebo automatickú detekciu cez teplotný prah, ale nie oboje" "window_open_detection_method": "Mala by sa použiť iba jedna metóda detekcie otvoreného okna. Použite senzor alebo automatickú detekciu cez teplotný prah, ale nie oboje",
"no_central_config": "Nemôžete zaškrtnúť „použiť centrálnu konfiguráciu“, pretože sa nenašla žiadna centrálna konfigurácia. Aby ste ho mohli používať, musíte si vytvoriť všestranný termostat typu „Central Configuration“."
}, },
"abort": { "abort": {
"already_configured": "Zariadenie je už nakonfigurované" "already_configured": "Zariadenie je už nakonfigurované"
@@ -177,31 +239,77 @@
"flow_title": "Všestranná konfigurácia termostatu", "flow_title": "Všestranná konfigurácia termostatu",
"step": { "step": {
"user": { "user": {
"title": "Pridajte nový všestranný termostat", "title": "Typ - {name}",
"data": {
"thermostat_type": "Typ termostatu"
},
"data_description": {
"thermostat_type": "Je možný len jeden centrálny typ konfigurácie"
}
},
"menu": {
"title": "Menu",
"description": "Nakonfigurujte si termostat. Po zadaní všetkých požadovaných parametrov budete môcť dokončiť konfiguráciu.",
"menu_options": {
"main": "Hlavné atribúty",
"central_boiler": "Centrálny kotol",
"type": "Podklady",
"tpi": "TPI parametre",
"features": "Vlastnosti",
"presets": "Predvoľby",
"window": "Detekcia okien",
"motion": "Detekcia pohybu",
"power": "Správa napájania",
"presence": "Detekcia prítomnosti",
"advanced": "Pokročilé parametre",
"finalize": "Všetko hotové",
"configuration_not_complete": "Konfigurácia nie je dokončená"
}
},
"main": {
"title": "Hlavný - {name}",
"description": "Hlavné povinné atribúty", "description": "Hlavné povinné atribúty",
"data": { "data": {
"name": "Názov", "name": "Názov",
"thermostat_type": "Termostat typ", "thermostat_type": "Termostat typ",
"temperature_sensor_entity_id": "ID entity snímača teploty", "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", "external_temperature_sensor_entity_id": "ID entity externého snímača teploty",
"cycle_min": "Trvanie cyklu (minúty)", "cycle_min": "Trvanie cyklu (minúty)",
"temp_min": "Minimálna povolená teplota", "temp_min": "Minimálna povolená teplota",
"temp_max": "Maximálna povolená teplota", "temp_max": "Maximálna povolená teplota",
"step_temperature": "Krok teploty",
"device_power": "Výkon zariadenia (kW)", "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_window_feature": "Použite detekciu okien",
"use_motion_feature": "Použite detekciu pohybu", "use_motion_feature": "Použite detekciu pohybu",
"use_power_feature": "Použite správu napájania", "use_power_feature": "Použite správu napájania",
"use_presence_feature": "Použite detekciu prítomnosti" "use_presence_feature": "Použite detekciu prítomnosti",
"use_central_boiler_feature": "Použite centrálny kotol. Začiarknutím tohto políčka pridáte ovládanie do centrálneho kotla. Po zaškrtnutí tohto políčka budete musieť nakonfigurovať VTherm, ktorý bude mať ovládanie centrálneho kotla, aby sa prejavilo. Ak jeden VTherm vyžaduje ohrev, kotol sa zapne. Ak žiadny VTherm nevyžaduje ohrev, kotol sa vypne. Príkazy na zapnutie/vypnutie centrálneho kotla sú uvedené na príslušnej konfiguračnej stránke"
} }
}, },
"type": { "type": {
"title": "Prepojené entity", "title": "Prepojené entity - {name}",
"description": "Atribúty prepojených entít", "description": "Atribúty prepojených entít",
"data": { "data": {
"heater_entity_id": "Spínač ohrievača", "heater_entity_id": "Spínač ohrievača",
"heater_entity2_id": "2. spínač ohrievača", "heater_entity2_id": "2. spínač ohrievača",
"heater_entity3_id": "3. spínač ohrievača", "heater_entity3_id": "3. spínač ohrievača",
"heater_entity4_id": "4. 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", "proportional_function": "Algoritmus",
"climate_entity_id": "Základná klíma", "climate_entity_id": "Základná klíma",
"climate_entity2_id": "2. základná klíma", "climate_entity2_id": "2. základná klíma",
@@ -212,17 +320,19 @@
"valve_entity2_id": "2. ventil číslo", "valve_entity2_id": "2. ventil číslo",
"valve_entity3_id": "3. ventil číslo", "valve_entity3_id": "3. ventil číslo",
"valve_entity4_id": "4. ventil číslo", "valve_entity4_id": "4. ventil číslo",
"auto_regulation_mode": "Self-regulation", "auto_regulation_mode": "Samoregulácia",
"auto_regulation_dtemp": "Regulation threshold", "auto_regulation_dtemp": "Regulačný prah",
"auto_regulation_periode_min": "Regulation minimal period", "auto_regulation_periode_min": "Regulačné minimálne obdobie",
"inverse_switch_command": "Inverse switch command", "auto_regulation_use_device_temp": "Použite vnútornú teplotu podkladu",
"auto_fan_mode": " Auto fan mode" "inverse_switch_command": "Inverzný prepínací príkaz",
"auto_fan_mode": "Režim automatického ventilátora"
}, },
"data_description": { "data_description": {
"heater_entity_id": "ID entity povinného ohrievača", "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_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_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_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ý)", "proportional_function": "Algoritmus, ktorý sa má použiť (TPI je zatiaľ jediný)",
"climate_entity_id": "ID základnej klimatickej entity", "climate_entity_id": "ID základnej klimatickej entity",
"climate_entity2_id": "2. základný identifikátor klimatickej entity", "climate_entity2_id": "2. základný identifikátor klimatickej entity",
@@ -233,114 +343,129 @@
"valve_entity2_id": "2. ventil číslo entity id", "valve_entity2_id": "2. ventil číslo entity id",
"valve_entity3_id": "3. ventil číslo entity id", "valve_entity3_id": "3. ventil číslo entity id",
"valve_entity4_id": "4. ventil číslo entity id", "valve_entity4_id": "4. ventil číslo entity id",
"auto_regulation_mode": "Auto adjustment of the target temperature", "auto_regulation_mode": "Automatické nastavenie cieľovej teploty",
"auto_regulation_dtemp": "The threshold in ° under which the temperature change will not be send", "auto_regulation_dtemp": "Hranica v °, pod ktorou sa zmena teploty neodošle",
"auto_regulation_periode_min": "Duration in minutes between two regulation update", "auto_regulation_periode_min": "Trvanie v minútach medzi dvoma aktualizáciami predpisov",
"inverse_switch_command": "For switch with pilot wire and diode you may need to inverse the command", "auto_regulation_use_device_temp": "Na urýchlenie samoregulácie použite prípadný vnútorný snímač teploty podkladu",
"auto_fan_mode": " Automatically activate fan when huge heating/cooling is necessary" "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": { "tpi": {
"title": "TPI", "title": "TPI - {name}",
"description": "Časovo proporcionálne integrálne atribúty", "description": "Časovo proporcionálne integrálne atribúty",
"data": { "data": {
"tpi_coef_int": "Koeficient na použitie pre vnútornú teplotnú deltu", "tpi_coef_int": "Koeficient na použitie pre vnútornú teplotnú deltu",
"tpi_coef_ext": "Koeficient na použitie pre vonkajšiu teplotnú deltu" "tpi_coef_ext": "Koeficient na použitie pre vonkajšiu teplotnú deltu",
"use_tpi_central_config": "Použite centrálnu konfiguráciu TPI"
},
"data_description": {
"tpi_coef_int": "Koeficient na použitie pre vnútornú teplotnú deltu",
"tpi_coef_ext": "Koeficient na použitie pre deltu vonkajšej teploty",
"use_tpi_central_config": "Začiarknite, ak chcete použiť centrálnu konfiguráciu TPI. Zrušte začiarknutie, ak chcete použiť špecifickú konfiguráciu TPI pre tento VTherm"
} }
}, },
"presets": { "presets": {
"title": "Predvoľby", "title": "Predvoľby - {name}",
"description": "Pre každú predvoľbu zadajte cieľovú teplotu (0, ak chcete predvoľbu ignorovať)", "description": "Pre každú predvoľbu zadajte cieľovú teplotu (0, ak chcete predvoľbu ignorovať)",
"data": { "data": {
"eco_temp": "Teplota v predvoľbe Eco", "use_presets_central_config": "Použite konfiguráciu centrálnych predvolieb"
"comfort_temp": "Prednastavená teplota v komfortnom režime",
"boost_temp": "Teplota v prednastavení Boost",
"frost_temp": "Teplota v prednastavení Frost protection",
"eco_ac_temp": "Teplota v režime Eco prednastavená pre režim AC",
"comfort_ac_temp": "Teplota v režime Comfort je prednastavená pre režim AC",
"boost_ac_temp": "Prednastavená teplota v režime Boost pre režim AC"
} }
}, },
"window": { "window": {
"title": "Správa okien", "title": "Správa okien - {name}",
"description": "Otvoriť správu okien.\nAk sa príslušné entity_id nepoužíva, ponechajte prázdne\nMôžete tiež nakonfigurovať automatickú detekciu otvoreného okna na základe poklesu teploty", "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": { "data": {
"window_sensor_entity_id": "ID entity snímača okna", "window_sensor_entity_id": "ID entity snímača okna",
"window_delay": "Oneskorenie snímača okna (sekundy)", "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_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_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)" "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": { "data_description": {
"window_sensor_entity_id": "Nechajte prázdne, ak nemáte použiť žiadny okenný senzor", "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_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_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_close_threshold": "Odporúčaná hodnota: 0. Ak sa nepoužíva automatická detekcia otvoreného okna, nechajte prázdne",
"window_auto_max_duration": "Odporúčaná hodnota: 60 (jedna hodina). Ak sa nepoužíva automatická detekcia otvoreného okna, nechajte prázdne" "window_auto_max_duration": "Odporúčaná hodnota: 60 (jedna hodina). Ak sa nepoužíva automatická detekcia otvoreného okna, nechajte prázdne",
"use_window_central_config": "Začiarknite, ak chcete použiť konfiguráciu centrálneho okna. Zrušte začiarknutie, ak chcete použiť špecifickú konfiguráciu okna pre tento VTherm",
"window_action": "Akcia, ktorá sa má vykonať, ak sa okno zistí ako otvorené"
} }
}, },
"motion": { "motion": {
"title": "Riadenie pohybu", "title": "Riadenie pohybu - {name}",
"description": "Správa snímača pohybu. Predvoľba sa môže automaticky prepínať v závislosti od detekcie pohybu\nAk sa nepoužíva, ponechajte zodpovedajúce entity_id prázdne.\nmotion_preset a no_motion_preset by mali byť nastavené na zodpovedajúci názov predvoľby", "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": { "data": {
"motion_sensor_entity_id": "ID entity snímača pohybu", "motion_sensor_entity_id": "ID entity snímača pohybu",
"motion_delay": "Oneskorenie aktivácie", "motion_delay": "Oneskorenie aktivácie",
"motion_off_delay": "Oneskorenie deaktivácie", "motion_off_delay": "Oneskorenie deaktivácie",
"motion_preset": "Prednastavený pohyb", "motion_preset": "Prednastavený pohyb",
"no_motion_preset": "Žiadna predvoľba pohybu" "no_motion_preset": "Žiadna predvoľba pohybu",
"use_motion_central_config": "Použite centrálnu konfiguráciu pohybu"
}, },
"data_description": { "data_description": {
"motion_sensor_entity_id": "ID entity snímača pohybu", "motion_sensor_entity_id": "ID entity snímača pohybu",
"motion_delay": "Oneskorenie aktivácie pohybu (sekundy)", "motion_delay": "Oneskorenie aktivácie pohybu (sekundy)",
"motion_off_delay": "Oneskorenie deaktivácie pohybu (sekundy)", "motion_off_delay": "Oneskorenie deaktivácie pohybu (sekundy)",
"motion_preset": "Prednastavené na použitie pri detekcii pohybu", "motion_preset": "Prednastavené na použitie pri detekcii pohybu",
"no_motion_preset": "Prednastavené na použitie, keď nie je detekovaný žiadny pohyb" "no_motion_preset": "Prednastavené na použitie, keď nie je detekovaný žiadny pohyb",
"use_motion_central_config": "Začiarknite, ak chcete použiť konfiguráciu centrálneho pohybu. Zrušte začiarknutie, ak chcete použiť špecifickú konfiguráciu pohybu pre tento VTherm"
} }
}, },
"power": { "power": {
"title": "Správa napájania", "title": "Správa napájania - {name}",
"description": "Atribúty správy napájania.\nPoskytuje senzor výkonu a maximálneho výkonu vášho domova.\nPotom zadajte spotrebu energie ohrievača, keď je zapnutý.\nVšetky senzory a výkon zariadenia by mali mať rovnakú jednotku (kW alebo W).\nPonechajte zodpovedajúce entity_id prázdne ak sa nepoužíva.", "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": { "data": {
"power_sensor_entity_id": "ID entity snímača výkonu", "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", "max_power_sensor_entity_id": "ID entity snímača maximálneho výkonu",
"power_temp": "Teplota pre zníženie výkonu" "power_temp": "Teplota pre zníženie výkonu",
"use_power_central_config": "Použite centrálnu konfiguráciu napájania"
},
"data_description": {
"power_sensor_entity_id": "ID entity snímača výkonu",
"max_power_sensor_entity_id": "ID entity snímača maximálneho výkonu",
"power_temp": "Teplota pre zníženie výkonu",
"use_power_central_config": "Začiarknite, ak chcete použiť konfiguráciu centrálneho napájania. Zrušte začiarknutie, ak chcete použiť špecifickú konfiguráciu napájania pre tento VTherm"
} }
}, },
"presence": { "presence": {
"title": "Riadenie prítomnosti", "title": "Prítommnosť - {name}",
"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.", "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": { "data": {
"presence_sensor_entity_id": "ID entity senzora prítomnosti (pravda je prítomná)", "presence_sensor_entity_id": "Senzor prítomnosti",
"eco_away_temp": "Teplota v prednastavenej Eco, keď nie je žiadna prítomnosť", "use_presence_central_config": "Použite konfiguráciu centrálnej prítomnosti teploty. Ak chcete použiť špecifické entity teploty, zrušte začiarknutie"
"comfort_away_temp": "Teplota v režime Comfort je prednastavená, keď nie je prítomný", },
"boost_away_temp": "Prednastavená teplota v režime Boost, keď nie je prítomný", "data_description": {
"frost_away_temp": "Prednastavená teplota v režime Frost protection, keď nie je prítomný", "presence_sensor_entity_id": "ID entity senzora prítomnosti"
"eco_ac_away_temp": "Teplota v prednastavenej Eco, keď nie je prítomná v režime AC",
"comfort_ac_away_temp": "Teplota v režime Comfort je prednastavená, keď nie je prítomný v režime AC",
"boost_ac_away_temp": "Teplota v prednastavenom Boost, keď nie je prítomný v režime AC"
} }
}, },
"advanced": { "advanced": {
"title": "Pokročilé parametre", "title": "Pokročilé parametre - {name}",
"description": "Konfigurácia pokročilých parametrov. Ak neviete, čo robíte, ponechajte predvolené hodnoty.\nTento parameter môže viesť k veľmi zlej regulácii teploty alebo výkonu.", "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": { "data": {
"minimal_activation_delay": "Minimálne oneskorenie aktivácie", "minimal_activation_delay": "Minimálne oneskorenie aktivácie",
"security_delay_min": "Bezpečnostné oneskorenie (v minútach)", "security_delay_min": "Bezpečnostné oneskorenie (v minútach)",
"security_min_on_percent": "Minimálne percento výkonu pre bezpečnostný režim", "security_min_on_percent": "Minimálne percento výkonu pre bezpečnostný režim",
"security_default_on_percent": "Percento výkonu na použitie v bezpečnostnom režime" "security_default_on_percent": "Percento výkonu na použitie v bezpečnostnom režime",
"use_advanced_central_config": "Použite centrálnu rozšírenú konfiguráciu"
}, },
"data_description": { "data_description": {
"minimal_activation_delay": "Oneskorenie v sekundách, pri ktorom sa zariadenie neaktivuje", "minimal_activation_delay": "Oneskorenie v sekundách, pri ktorom sa zariadenie neaktivuje",
"security_delay_min": "Maximálne povolené oneskorenie v minútach medzi dvoma meraniami teploty. Po uplynutí tohto oneskorenia sa termostat prepne do bezpečnostného vypnutého stavu", "security_delay_min": "Maximálne povolené oneskorenie v minútach medzi dvoma meraniami teploty. Po uplynutí tohto oneskorenia sa termostat prepne do bezpečnostného vypnutého stavu",
"security_min_on_percent": "Minimálna percentuálna hodnota ohrevu pre aktiváciu prednastavenej bezpečnosti. Pod týmto percentom výkonu termostat neprejde do prednastavenia zabezpečenia", "security_min_on_percent": "Minimálna percentuálna hodnota ohrevu pre aktiváciu prednastavenej bezpečnosti. Pod týmto percentom výkonu termostat neprejde do prednastavenia zabezpečenia",
"security_default_on_percent": "Predvolená percentuálna hodnota vykurovacieho výkonu v bezpečnostnej predvoľbe. Nastavte na 0, ak chcete vypnúť ohrievač v zabezpečenom stave" "security_default_on_percent": "Predvolená percentuálna hodnota vykurovacieho výkonu v bezpečnostnej predvoľbe. Nastavte na 0, ak chcete vypnúť ohrievač v zabezpečenom stave",
"use_advanced_central_config": "Začiarknite, ak chcete použiť centrálnu rozšírenú konfiguráciu. Zrušte začiarknutie, ak chcete použiť špecifickú rozšírenú konfiguráciu pre tento VTherm"
} }
} }
}, },
"error": { "error": {
"unknown": "Neočakávaná chyba", "unknown": "Neočakávaná chyba",
"unknown_entity": "Neznáme ID entity", "unknown_entity": "Neznáme ID entity",
"window_open_detection_method": "Mala by sa použiť iba jedna metóda detekcie otvoreného okna. Použite senzor alebo automatickú detekciu cez teplotný prah, ale nie oboje" "window_open_detection_method": "Mala by sa použiť iba jedna metóda detekcie otvoreného okna. Použite senzor alebo automatickú detekciu cez teplotný prah, ale nie oboje",
"no_central_config": "Nemôžete zaškrtnúť „použiť centrálnu konfiguráciu“, pretože sa nenašla žiadna centrálna konfigurácia. Aby ste ho mohli používať, musíte si vytvoriť všestranný termostat typu „Central Configuration“.",
"service_configuration_format": "Formát konfigurácie služby je nesprávny"
}, },
"abort": { "abort": {
"already_configured": "Zariadenie je už nakonfigurované" "already_configured": "Zariadenie je už nakonfigurované"
@@ -349,29 +474,46 @@
"selector": { "selector": {
"thermostat_type": { "thermostat_type": {
"options": { "options": {
"thermostat_central_config": "Centrálna konfigurácia",
"thermostat_over_switch": "Termostat nad spínačom", "thermostat_over_switch": "Termostat nad spínačom",
"thermostat_over_climate": "Termostat nad iným termostatom", "thermostat_over_climate": "Termostat nad iným termostatom",
"thermostat_over_valve": "Thermostat over a valve" "thermostat_over_valve": "Termostat nad ventilom"
} }
}, },
"auto_regulation_mode": { "auto_regulation_mode": {
"options": { "options": {
"auto_regulation_slow": "Slow", "auto_regulation_slow": "Pomalé",
"auto_regulation_strong": "Strong", "auto_regulation_strong": "Silné",
"auto_regulation_medium": "Medium", "auto_regulation_medium": "Stredné",
"auto_regulation_light": "Light", "auto_regulation_light": "Jemné",
"auto_regulation_expert": "Expert", "auto_regulation_expert": "Expert",
"auto_regulation_none": "No auto-regulation" "auto_regulation_none": "Nie auto-regulácia"
} }
}, },
"auto_fan_mode": { "auto_fan_mode": {
"options": { "options": {
"auto_fan_none": "No auto-fan", "auto_fan_none": "Žiadny automatický ventilátor",
"auto_fan_low": "Low", "auto_fan_low": "Nízky",
"auto_fan_medium": "Medium", "auto_fan_medium": "Stredný",
"auto_fan_high": "High", "auto_fan_high": "Vysoký",
"auto_fan_turbo": "Turbo" "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": { "entity": {
@@ -387,6 +529,53 @@
} }
} }
} }
},
"number": {
"frost_temp": {
"name": "Mráz"
},
"eco_temp": {
"name": "Eco"
},
"comfort_temp": {
"name": "Komfort"
},
"boost_temp": {
"name": "Boost"
},
"frost_ac_temp": {
"name": "Mráz ac"
},
"eco_ac_temp": {
"name": "Eco ac"
},
"comfort_ac_temp": {
"name": "Komfort ac"
},
"boost_ac_temp": {
"name": "Boost ac"
},
"frost_away_temp": {
"name": "Mráz mimo"
},
"eco_away_temp": {
"name": "Eko mimo"
},
"comfort_away_temp": {
"name": "Komfort mimo"
},
"boost_away_temp": {
"name": "Boost mimo"
},
"eco_ac_away_temp": {
"name": "Eco ac mimo"
},
"comfort_ac_away_temp": {
"name": "Komfort ac mimo"
},
"boost_ac_away_temp": {
"name": "Boost ac mimo"
}
} }
} }
} }

View File

@@ -5,7 +5,7 @@ import logging
from typing import Any from typing import Any
from enum import StrEnum from enum import StrEnum
from homeassistant.const import ATTR_ENTITY_ID, STATE_ON, UnitOfTemperature from homeassistant.const import ATTR_ENTITY_ID, STATE_ON, STATE_UNAVAILABLE
from homeassistant.core import State from homeassistant.core import State
from homeassistant.exceptions import ServiceNotFound from homeassistant.exceptions import ServiceNotFound
@@ -30,8 +30,10 @@ from homeassistant.components.number import SERVICE_SET_VALUE
from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.event import async_call_later from homeassistant.helpers.event import async_call_later
from homeassistant.util.unit_conversion import TemperatureConverter
from .const import UnknownEntity, overrides from .const import UnknownEntity, overrides
from .keep_alive import IntervalCaller
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@@ -133,16 +135,17 @@ class UnderlyingEntity:
async def check_initial_state(self, hvac_mode: HVACMode): async def check_initial_state(self, hvac_mode: HVACMode):
"""Prevent the underlying to be on but thermostat is off""" """Prevent the underlying to be on but thermostat is off"""
if hvac_mode == HVACMode.OFF and self.is_device_active: if hvac_mode == HVACMode.OFF and self.is_device_active:
_LOGGER.warning( _LOGGER.info(
"%s - The hvac mode is OFF, but the underlying device is ON. Turning off device %s", "%s - The hvac mode is OFF, but the underlying device is ON. Turning off device %s",
self, self,
self._entity_id, self._entity_id,
) )
await self.set_hvac_mode(hvac_mode) await self.set_hvac_mode(hvac_mode)
elif hvac_mode != HVACMode.OFF and not self.is_device_active: elif hvac_mode != HVACMode.OFF and not self.is_device_active:
_LOGGER.warning( _LOGGER.info(
"%s - The hvac mode is ON, but the underlying device is not ON. Turning on device %s", "%s - The hvac mode is %s, but the underlying device is not ON. Turning on device %s if needed",
self, self,
hvac_mode,
self._entity_id, self._entity_id,
) )
await self.set_hvac_mode(hvac_mode) await self.set_hvac_mode(hvac_mode)
@@ -186,6 +189,7 @@ class UnderlyingSwitch(UnderlyingEntity):
thermostat: Any, thermostat: Any,
switch_entity_id: str, switch_entity_id: str,
initial_delay_sec: int, initial_delay_sec: int,
keep_alive_sec: float,
) -> None: ) -> None:
"""Initialize the underlying switch""" """Initialize the underlying switch"""
@@ -201,6 +205,7 @@ class UnderlyingSwitch(UnderlyingEntity):
self._on_time_sec = 0 self._on_time_sec = 0
self._off_time_sec = 0 self._off_time_sec = 0
self._hvac_mode = None self._hvac_mode = None
self._keep_alive = IntervalCaller(hass, keep_alive_sec)
@property @property
def initial_delay_sec(self): def initial_delay_sec(self):
@@ -213,6 +218,16 @@ class UnderlyingSwitch(UnderlyingEntity):
"""Tells if the switch command should be inversed""" """Tells if the switch command should be inversed"""
return self._thermostat.is_inversed return self._thermostat.is_inversed
@property
def keep_alive_sec(self) -> float:
"""Return the switch keep-alive interval in seconds."""
return self._keep_alive.interval_sec
@overrides
def startup(self):
super().startup()
self._keep_alive.set_async_action(self._keep_alive_callback)
# @overrides this breaks some unit tests TypeError: object MagicMock can't be used in 'await' expression # @overrides this breaks some unit tests TypeError: object MagicMock can't be used in 'await' expression
async def set_hvac_mode(self, hvac_mode: HVACMode) -> bool: async def set_hvac_mode(self, hvac_mode: HVACMode) -> bool:
"""Set the HVACmode. Returns true if something have change""" """Set the HVACmode. Returns true if something have change"""
@@ -236,35 +251,64 @@ class UnderlyingSwitch(UnderlyingEntity):
not self.is_inversed and real_state not self.is_inversed and real_state
) )
async def _keep_alive_callback(self):
"""Keep alive: Turn on if already turned on, turn off if already turned off."""
timer = self._keep_alive.backoff_timer
state: State | None = self._hass.states.get(self._entity_id)
# Normal, expected state.state values are "on" and "off". An absent
# underlying MQTT switch was observed to produce either state == None
# or state.state == STATE_UNAVAILABLE ("unavailable").
if state is None or state.state == STATE_UNAVAILABLE:
if timer.is_ready():
_LOGGER.warning(
"Entity %s is not available (state: %s). Will keep trying "
"keep alive calls, but won't log this condition every time.",
self._entity_id,
state.state if state else "None",
)
else:
if timer.in_progress:
timer.reset()
_LOGGER.warning(
"Entity %s has recovered (state: %s).",
self._entity_id,
state.state,
)
await (self.turn_on() if self.is_device_active else self.turn_off())
# @overrides this breaks some unit tests TypeError: object MagicMock can't be used in 'await' expression # @overrides this breaks some unit tests TypeError: object MagicMock can't be used in 'await' expression
async def turn_off(self): async def turn_off(self):
"""Turn heater toggleable device off.""" """Turn heater toggleable device off."""
self._keep_alive.cancel() # Cancel early to avoid a turn_on/turn_off race condition
_LOGGER.debug("%s - Stopping underlying entity %s", self, self._entity_id) _LOGGER.debug("%s - Stopping underlying entity %s", self, self._entity_id)
command = SERVICE_TURN_OFF if not self.is_inversed else SERVICE_TURN_ON command = SERVICE_TURN_OFF if not self.is_inversed else SERVICE_TURN_ON
domain = self._entity_id.split(".")[0] domain = self._entity_id.split(".")[0]
# This may fails if called after shutdown # This may fails if called after shutdown
try: try:
data = {ATTR_ENTITY_ID: self._entity_id} try:
await self._hass.services.async_call( data = {ATTR_ENTITY_ID: self._entity_id}
domain, await self._hass.services.async_call(domain, command, data)
command, self._keep_alive.set_async_action(self._keep_alive_callback)
data, except Exception:
) self._keep_alive.cancel()
raise
except ServiceNotFound as err: except ServiceNotFound as err:
_LOGGER.error(err) _LOGGER.error(err)
async def turn_on(self): async def turn_on(self):
"""Turn heater toggleable device on.""" """Turn heater toggleable device on."""
self._keep_alive.cancel() # Cancel early to avoid a turn_on/turn_off race condition
_LOGGER.debug("%s - Starting underlying entity %s", self, self._entity_id) _LOGGER.debug("%s - Starting underlying entity %s", self, self._entity_id)
command = SERVICE_TURN_ON if not self.is_inversed else SERVICE_TURN_OFF command = SERVICE_TURN_ON if not self.is_inversed else SERVICE_TURN_OFF
domain = self._entity_id.split(".")[0] domain = self._entity_id.split(".")[0]
try: try:
data = {ATTR_ENTITY_ID: self._entity_id} try:
await self._hass.services.async_call( data = {ATTR_ENTITY_ID: self._entity_id}
domain, await self._hass.services.async_call(domain, command, data)
command, self._keep_alive.set_async_action(self._keep_alive_callback)
data, except Exception:
) self._keep_alive.cancel()
raise
except ServiceNotFound as err: except ServiceNotFound as err:
_LOGGER.error(err) _LOGGER.error(err)
@@ -354,8 +398,8 @@ class UnderlyingSwitch(UnderlyingEntity):
if await self._thermostat.check_overpowering(): if await self._thermostat.check_overpowering():
_LOGGER.debug("%s - End of cycle (3)", self) _LOGGER.debug("%s - End of cycle (3)", self)
return return
# Security mode could have change the on_time percent # safety mode could have change the on_time percent
await self._thermostat.check_security() await self._thermostat.check_safety()
time = self._on_time_sec time = self._on_time_sec
action_label = "start" action_label = "start"
@@ -421,6 +465,7 @@ class UnderlyingSwitch(UnderlyingEntity):
def remove_entity(self): def remove_entity(self):
"""Remove the entity after stopping its cycle""" """Remove the entity after stopping its cycle"""
self._cancel_cycle() self._cancel_cycle()
self._keep_alive.cancel()
class UnderlyingClimate(UnderlyingEntity): class UnderlyingClimate(UnderlyingEntity):
@@ -463,8 +508,8 @@ class UnderlyingClimate(UnderlyingEntity):
self._underlying_climate, self._underlying_climate,
) )
else: else:
_LOGGER.error( _LOGGER.info(
"%s - Cannot find the underlying climate entity: %s. Thermostat will not be operational", "%s - Cannot find the underlying climate entity: %s. Thermostat will not be operational. Will try later.",
self, self,
self.entity_id, self.entity_id,
) )
@@ -483,6 +528,14 @@ class UnderlyingClimate(UnderlyingEntity):
if not self.is_initialized: if not self.is_initialized:
return False return False
if self._underlying_climate.hvac_mode == hvac_mode:
_LOGGER.debug(
"%s - hvac_mode is already is requested state %s. Do not send any command",
self,
self._underlying_climate.hvac_mode,
)
return False
data = {ATTR_ENTITY_ID: self._entity_id, "hvac_mode": hvac_mode} data = {ATTR_ENTITY_ID: self._entity_id, "hvac_mode": hvac_mode}
await self._hass.services.async_call( await self._hass.services.async_call(
CLIMATE_DOMAIN, CLIMATE_DOMAIN,
@@ -558,12 +611,23 @@ class UnderlyingClimate(UnderlyingEntity):
"""Set the target temperature""" """Set the target temperature"""
if not self.is_initialized: if not self.is_initialized:
return return
data = {
ATTR_ENTITY_ID: self._entity_id, # Issue 508 we have to take care of service set_temperature or set_range
"temperature": self.cap_sent_value(temperature), target_temp = self.cap_sent_value(temperature)
"target_temp_high": max_temp, if (
"target_temp_low": min_temp, ClimateEntityFeature.TARGET_TEMPERATURE_RANGE
} in self._underlying_climate.supported_features
):
data = {
ATTR_ENTITY_ID: self._entity_id,
"target_temp_high": target_temp,
"target_temp_low": target_temp,
}
else:
data = {
ATTR_ENTITY_ID: self._entity_id,
"temperature": target_temp,
}
await self._hass.services.async_call( await self._hass.services.async_call(
CLIMATE_DOMAIN, CLIMATE_DOMAIN,
@@ -631,7 +695,7 @@ class UnderlyingClimate(UnderlyingEntity):
def temperature_unit(self) -> str: def temperature_unit(self) -> str:
"""Get the temperature_unit""" """Get the temperature_unit"""
if not self.is_initialized: if not self.is_initialized:
return UnitOfTemperature.CELSIUS return self._hass.config.units.temperature_unit
return self._underlying_climate.temperature_unit return self._underlying_climate.temperature_unit
@property @property
@@ -662,6 +726,18 @@ class UnderlyingClimate(UnderlyingEntity):
return False return False
return self._underlying_climate.is_aux_heat return self._underlying_climate.is_aux_heat
@property
def underlying_current_temperature(self) -> float | None:
"""Get the underlying current_temperature if it exists
and if initialized"""
if not self.is_initialized:
return None
if not hasattr(self._underlying_climate, "current_temperature"):
return None
return self._hass.states.get(self._entity_id).attributes.get("current_temperature")
def turn_aux_heat_on(self) -> None: def turn_aux_heat_on(self) -> None:
"""Turn auxiliary heater on.""" """Turn auxiliary heater on."""
if not self.is_initialized: if not self.is_initialized:
@@ -687,8 +763,12 @@ class UnderlyingClimate(UnderlyingEntity):
self._underlying_climate.min_temp is not None self._underlying_climate.min_temp is not None
and self._underlying_climate is not None and self._underlying_climate is not None
): ):
min_val = self._underlying_climate.min_temp min_val = TemperatureConverter.convert(
max_val = self._underlying_climate.max_temp self._underlying_climate.min_temp, self._underlying_climate.temperature_unit, self._hass.config.units.temperature_unit
)
max_val = TemperatureConverter.convert(
self._underlying_climate.max_temp, self._underlying_climate.temperature_unit, self._hass.config.units.temperature_unit
)
new_value = max(min_val, min(value, max_val)) new_value = max(min_val, min(value, max_val))
else: else:
@@ -736,32 +816,42 @@ class UnderlyingValve(UnderlyingEntity):
"""Send the percent open to the underlying valve""" """Send the percent open to the underlying valve"""
# This may fails if called after shutdown # This may fails if called after shutdown
try: try:
data = {ATTR_ENTITY_ID: self._entity_id, "value": self._percent_open} data = {"value": self._percent_open}
target = {ATTR_ENTITY_ID: self._entity_id}
domain = self._entity_id.split(".")[0] domain = self._entity_id.split(".")[0]
await self._hass.services.async_call( await self._hass.services.async_call(
domain, domain=domain,
SERVICE_SET_VALUE, service=SERVICE_SET_VALUE,
data, service_data=data,
target=target,
) )
except ServiceNotFound as err: except ServiceNotFound as err:
_LOGGER.error(err) _LOGGER.error(err)
# This could happens in unit test if input_number domain is not yet loaded
# raise err
async def turn_off(self): async def turn_off(self):
"""Turn heater toggleable device off.""" """Turn heater toggleable device off."""
_LOGGER.debug("%s - Stopping underlying valve entity %s", self, self._entity_id) _LOGGER.debug("%s - Stopping underlying valve entity %s", self, self._entity_id)
self._percent_open = 0 # Issue 341
if self.is_device_active: is_active = self.is_device_active
self._percent_open = self.cap_sent_value(0)
if is_active:
await self.send_percent_open() await self.send_percent_open()
async def turn_on(self): async def turn_on(self):
"""Nothing to do for Valve because it cannot be turned off""" """Nothing to do for Valve because it cannot be turned on"""
self.set_valve_open_percent()
async def set_hvac_mode(self, hvac_mode: HVACMode) -> bool: async def set_hvac_mode(self, hvac_mode: HVACMode) -> bool:
"""Set the HVACmode. Returns true if something have change""" """Set the HVACmode. Returns true if something have change"""
if hvac_mode == HVACMode.OFF: if hvac_mode == HVACMode.OFF and self.is_device_active:
await self.turn_off() await self.turn_off()
if hvac_mode != HVACMode.OFF and not self.is_device_active:
await self.turn_on()
if self._hvac_mode != hvac_mode: if self._hvac_mode != hvac_mode:
self._hvac_mode = hvac_mode self._hvac_mode = hvac_mode
return True return True
@@ -790,8 +880,10 @@ class UnderlyingValve(UnderlyingEntity):
): ):
"""We use this function to change the on_percent""" """We use this function to change the on_percent"""
if force: if force:
self._percent_open = self.cap_sent_value(self._percent_open) # self._percent_open = self.cap_sent_value(self._percent_open)
await self.send_percent_open() # await self.send_percent_open()
# avoid to send 2 times the same value at startup
self.set_valve_open_percent()
@overrides @overrides
def cap_sent_value(self, value) -> float: def cap_sent_value(self, value) -> float:
@@ -807,7 +899,7 @@ class UnderlyingValve(UnderlyingEntity):
min_val = valve_state.attributes["min"] min_val = valve_state.attributes["min"]
max_val = valve_state.attributes["max"] max_val = valve_state.attributes["max"]
new_value = round(max(min_val, min(value, max_val))) new_value = round(max(min_val, min(value / 100 * max_val, max_val)))
else: else:
_LOGGER.debug("%s - no min and max attributes on underlying", self) _LOGGER.debug("%s - no min and max attributes on underlying", self)
new_value = value new_value = value

View File

@@ -1,12 +1,18 @@
""" The API of Versatile Thermostat""" """ The API of Versatile Thermostat"""
import logging import logging
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.config_entries import ConfigEntry 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 ( from .const import (
DOMAIN, DOMAIN,
CONF_AUTO_REGULATION_EXPERT, CONF_AUTO_REGULATION_EXPERT,
CONF_SHORT_EMA_PARAMS, CONF_SHORT_EMA_PARAMS,
CONF_SAFETY_MODE,
CONF_THERMOSTAT_TYPE, CONF_THERMOSTAT_TYPE,
CONF_THERMOSTAT_CENTRAL_CONFIG, CONF_THERMOSTAT_CENTRAL_CONFIG,
) )
@@ -46,19 +52,29 @@ class VersatileThermostatAPI(dict):
super().__init__() super().__init__()
self._expert_params = None self._expert_params = None
self._short_ema_params = None self._short_ema_params = None
self._safety_mode = None
self._central_boiler_entity = None
self._threshold_number_entity = None
self._nb_active_number_entity = None
self._central_configuration = None
self._central_mode_select = None
# A dict that will store all Number entities which holds the temperature
self._number_temperatures = dict()
def find_central_configuration(self): def find_central_configuration(self):
"""Search for a central configuration""" """Search for a central configuration"""
for config_entry in VersatileThermostatAPI._hass.config_entries.async_entries( if not self._central_configuration:
DOMAIN for (
): config_entry
if ( ) in VersatileThermostatAPI._hass.config_entries.async_entries(DOMAIN):
config_entry.data.get(CONF_THERMOSTAT_TYPE) if (
== CONF_THERMOSTAT_CENTRAL_CONFIG config_entry.data.get(CONF_THERMOSTAT_TYPE)
): == CONF_THERMOSTAT_CENTRAL_CONFIG
central_config = config_entry ):
return central_config self._central_configuration = config_entry
return None break
# return self._central_configuration
return self._central_configuration
def add_entry(self, entry: ConfigEntry): def add_entry(self, entry: ConfigEntry):
"""Add a new entry""" """Add a new entry"""
@@ -87,6 +103,127 @@ class VersatileThermostatAPI(dict):
if self._short_ema_params: if self._short_ema_params:
_LOGGER.debug("We have found short ema params %s", self._short_ema_params) _LOGGER.debug("We have found short ema params %s", self._short_ema_params)
self._safety_mode = config.get(CONF_SAFETY_MODE)
if self._safety_mode:
_LOGGER.debug("We have found safet_mode params %s", self._safety_mode)
def register_central_boiler(self, central_boiler_entity):
"""Register the central boiler entity. This is used by the CentralBoilerBinarySensor
class to register itself at creation"""
self._central_boiler_entity = central_boiler_entity
def register_central_boiler_activation_number_threshold(
self, threshold_number_entity
):
"""register the two number entities needed for boiler activation"""
self._threshold_number_entity = threshold_number_entity
# If sensor and threshold number are initialized, reload the listener
# if self._nb_active_number_entity and self._central_boiler_entity:
# self._hass.async_add_job(self.reload_central_boiler_binary_listener)
def register_nb_device_active_boiler(self, nb_active_number_entity):
"""register the two number entities needed for boiler activation"""
self._nb_active_number_entity = nb_active_number_entity
# if self._threshold_number_entity and self._central_boiler_entity:
# self._hass.async_add_job(self.reload_central_boiler_binary_listener)
def register_temperature_number(
self,
config_id: str,
preset_name: str,
number_entity: NumberEntity,
):
"""Register the NumberEntity for a particular device / preset."""
# Search for device_name into the _number_temperatures dict
if not self._number_temperatures.get(config_id):
self._number_temperatures[config_id] = dict()
self._number_temperatures.get(config_id)[preset_name] = number_entity
def get_temperature_number_value(self, config_id, preset_name) -> float | None:
"""Returns the value of a previously registred NumberEntity which represent
a temperature. If no NumberEntity was previously registred, then returns None"""
entities = self._number_temperatures.get(config_id, None)
if entities:
entity = entities.get(preset_name, None)
if entity:
return entity.state
return None
async def init_vtherm_links(self):
"""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, ...)
"""
await self.reload_central_boiler_binary_listener()
await self.reload_central_boiler_entities_list()
# Initialization of all preset for all VTherm
component: EntityComponent[ClimateEntity] = self._hass.data.get(
CLIMATE_DOMAIN, None
)
if component:
for entity in component.entities:
# if hasattr(entity, "init_presets"):
# if (
# only_use_central is False
# or entity.use_central_config_temperature
# ):
# await entity.init_presets(self.find_central_configuration())
# A little hack to test if the climate is a VTherm. Cannot use isinstance due to circular dependency of BaseThermostat
if (
entity.device_info
and entity.device_info.get("model", None) == DOMAIN
):
await entity.async_startup(self.find_central_configuration())
async def init_vtherm_preset_with_central(self):
"""Init all VTherm presets when the VTherm uses central temperature"""
# Initialization of all preset for all VTherm
component: EntityComponent[ClimateEntity] = self._hass.data.get(
CLIMATE_DOMAIN, None
)
if component:
for entity in component.entities:
if (
entity.device_info
and entity.device_info.get("model", None) == DOMAIN
and entity.use_central_config_temperature
):
await entity.init_presets(self.find_central_configuration())
async def reload_central_boiler_binary_listener(self):
"""Reloads the BinarySensor entity which listen to the number of
active devices and the thresholds entities"""
if self._central_boiler_entity:
await self._central_boiler_entity.listen_nb_active_vtherm_entity()
async def reload_central_boiler_entities_list(self):
"""Reload the central boiler list of entities if a central boiler is used"""
if self._nb_active_number_entity is not None:
await self._nb_active_number_entity.listen_vtherms_entities()
def register_central_mode_select(self, central_mode_select):
"""Register the select entity which holds the central_mode"""
self._central_mode_select = central_mode_select
async def notify_central_mode_change(self, old_central_mode: str | None = None):
"""Notify all VTherm that the central_mode have change"""
if self._central_mode_select is None:
return
# Update all VTherm states
component: EntityComponent[ClimateEntity] = self.hass.data[CLIMATE_DOMAIN]
for entity in component.entities:
if entity.device_info and entity.device_info.get("model", None) == DOMAIN:
_LOGGER.debug(
"Changing the central_mode. We have find %s to update",
entity.name,
)
await entity.check_central_mode(
self._central_mode_select.state, old_central_mode
)
@property @property
def self_regulation_expert(self): def self_regulation_expert(self):
"""Get the self regulation params""" """Get the self regulation params"""
@@ -94,9 +231,56 @@ class VersatileThermostatAPI(dict):
@property @property
def short_ema_params(self): def short_ema_params(self):
"""Get the self regulation params""" """Get the short EMA params in expert mode"""
return self._short_ema_params return self._short_ema_params
@property
def safety_mode(self):
"""Get the safety_mode params"""
return self._safety_mode
@property
def central_boiler_entity(self):
"""Get the central boiler binary_sensor entity"""
return self._central_boiler_entity
@property
def nb_active_device_for_boiler(self):
"""Returns the number of active VTherm which have an
influence on boiler"""
if self._nb_active_number_entity is None:
return None
else:
return self._nb_active_number_entity.native_value
@property
def nb_active_device_for_boiler_entity(self):
"""Returns the number of active VTherm entity which have an
influence on boiler"""
return self._nb_active_number_entity
@property
def nb_active_device_for_boiler_threshold_entity(self):
"""Returns the number of active VTherm entity which have an
influence on boiler"""
return self._threshold_number_entity
@property
def nb_active_device_for_boiler_threshold(self):
"""Returns the number of active VTherm entity which have an
influence on boiler"""
if self._threshold_number_entity is None:
return None
return int(self._threshold_number_entity.native_value)
@property
def central_mode(self) -> str | None:
"""Get the current central mode or None"""
if self._central_mode_select:
return self._central_mode_select.state
else:
return None
@property @property
def hass(self): def hass(self):
"""Get the HomeAssistant object""" """Get the HomeAssistant object"""

View File

@@ -3,5 +3,5 @@
"content_in_root": false, "content_in_root": false,
"render_readme": true, "render_readme": true,
"hide_default_branch": false, "hide_default_branch": false,
"homeassistant": "2023.12.1" "homeassistant": "2024.9.3"
} }

BIN
images/central_mode.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

BIN
images/config-complete.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

BIN
images/config-features.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 45 KiB

BIN
images/config-main0.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

BIN
images/config-menu.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

BIN
images/config-terminate.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

BIN
images/plotly-curves.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

BIN
images/temp-entities-1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

BIN
images/temp-entities-2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

0
pyproject.toml Normal file
View File

View File

@@ -1,2 +1 @@
homeassistant==2023.12.1 homeassistant==2024.9.3
ffmpeg

View File

@@ -2,5 +2,7 @@
-r requirements_dev.txt -r requirements_dev.txt
aiodiscover aiodiscover
ulid_transform ulid_transform
pytest
coverage
pytest-asyncio pytest-asyncio
pytest-homeassistant-custom-component pytest-homeassistant-custom-component

View File

@@ -25,5 +25,9 @@ fi
## without resulting to symlinks. ## without resulting to symlinks.
export PYTHONPATH="${PYTHONPATH}:${PWD}/custom_components" export PYTHONPATH="${PYTHONPATH}:${PWD}/custom_components"
## Link custom_components into config
# rm -f ${PWD}/config/custom_components
# ln -s ${PWD}/custom_components ${PWD}/config/
# Start Home Assistant # Start Home Assistant
hass --config "${PWD}/config" --debug hass --config "${PWD}/config" --debug

View File

@@ -1,9 +1,9 @@
# pylint: disable=wildcard-import, unused-wildcard-import, protected-access, unused-argument, line-too-long # pylint: disable=wildcard-import, unused-wildcard-import, protected-access, unused-argument, line-too-long, abstract-method
""" Some common resources """ """ Some common resources """
import asyncio import asyncio
import logging import logging
from unittest.mock import patch, MagicMock from unittest.mock import patch, MagicMock # pylint: disable=unused-import
import pytest # pylint: disable=unused-import import pytest # pylint: disable=unused-import
from homeassistant.core import HomeAssistant, Event, EVENT_STATE_CHANGED, State from homeassistant.core import HomeAssistant, Event, EVENT_STATE_CHANGED, State
@@ -19,6 +19,12 @@ from homeassistant.components.climate import (
ClimateEntityFeature, ClimateEntityFeature,
) )
from homeassistant.components.switch import (
SwitchEntity,
)
from homeassistant.components.number import NumberEntity, DOMAIN as NUMBER_DOMAIN
from pytest_homeassistant_custom_component.common import MockConfigEntry from pytest_homeassistant_custom_component.common import MockConfigEntry
from custom_components.versatile_thermostat.base_thermostat import BaseThermostat from custom_components.versatile_thermostat.base_thermostat import BaseThermostat
@@ -43,6 +49,7 @@ from .const import ( # pylint: disable=unused-import
MOCK_TH_OVER_CLIMATE_MAIN_CONFIG, MOCK_TH_OVER_CLIMATE_MAIN_CONFIG,
MOCK_TH_OVER_CLIMATE_CENTRAL_MAIN_CONFIG, MOCK_TH_OVER_CLIMATE_CENTRAL_MAIN_CONFIG,
MOCK_TH_OVER_CLIMATE_TYPE_CONFIG, MOCK_TH_OVER_CLIMATE_TYPE_CONFIG,
MOCK_TH_OVER_CLIMATE_TYPE_USE_DEVICE_TEMP_CONFIG,
MOCK_TH_OVER_CLIMATE_TYPE_AC_CONFIG, MOCK_TH_OVER_CLIMATE_TYPE_AC_CONFIG,
MOCK_TH_OVER_CLIMATE_TYPE_NOT_REGULATED_CONFIG, MOCK_TH_OVER_CLIMATE_TYPE_NOT_REGULATED_CONFIG,
MOCK_TH_OVER_SWITCH_TPI_CONFIG, MOCK_TH_OVER_SWITCH_TPI_CONFIG,
@@ -60,8 +67,15 @@ from .const import ( # pylint: disable=unused-import
PRESET_NONE, PRESET_NONE,
PRESET_ECO, PRESET_ECO,
PRESET_ACTIVITY, PRESET_ACTIVITY,
overrides,
) )
MOCK_FULL_FEATURES = {
CONF_USE_WINDOW_FEATURE: True,
CONF_USE_MOTION_FEATURE: True,
CONF_USE_POWER_FEATURE: True,
CONF_USE_PRESENCE_FEATURE: True,
}
FULL_SWITCH_CONFIG = ( FULL_SWITCH_CONFIG = (
MOCK_TH_OVER_SWITCH_USER_CONFIG MOCK_TH_OVER_SWITCH_USER_CONFIG
@@ -70,6 +84,7 @@ FULL_SWITCH_CONFIG = (
| MOCK_TH_OVER_SWITCH_TYPE_CONFIG | MOCK_TH_OVER_SWITCH_TYPE_CONFIG
| MOCK_TH_OVER_SWITCH_TPI_CONFIG | MOCK_TH_OVER_SWITCH_TPI_CONFIG
| MOCK_PRESETS_CONFIG | MOCK_PRESETS_CONFIG
| MOCK_FULL_FEATURES
| MOCK_WINDOW_CONFIG | MOCK_WINDOW_CONFIG
| MOCK_MOTION_CONFIG | MOCK_MOTION_CONFIG
| MOCK_POWER_CONFIG | MOCK_POWER_CONFIG
@@ -84,6 +99,7 @@ FULL_SWITCH_AC_CONFIG = (
| MOCK_TH_OVER_SWITCH_AC_TYPE_CONFIG | MOCK_TH_OVER_SWITCH_AC_TYPE_CONFIG
| MOCK_TH_OVER_SWITCH_TPI_CONFIG | MOCK_TH_OVER_SWITCH_TPI_CONFIG
| MOCK_PRESETS_AC_CONFIG | MOCK_PRESETS_AC_CONFIG
| MOCK_FULL_FEATURES
| MOCK_WINDOW_CONFIG | MOCK_WINDOW_CONFIG
| MOCK_MOTION_CONFIG | MOCK_MOTION_CONFIG
| MOCK_POWER_CONFIG | MOCK_POWER_CONFIG
@@ -91,7 +107,6 @@ FULL_SWITCH_AC_CONFIG = (
| MOCK_ADVANCED_CONFIG | MOCK_ADVANCED_CONFIG
) )
PARTIAL_CLIMATE_CONFIG = ( PARTIAL_CLIMATE_CONFIG = (
MOCK_TH_OVER_CLIMATE_USER_CONFIG MOCK_TH_OVER_CLIMATE_USER_CONFIG
| MOCK_TH_OVER_CLIMATE_MAIN_CONFIG | MOCK_TH_OVER_CLIMATE_MAIN_CONFIG
@@ -101,6 +116,15 @@ PARTIAL_CLIMATE_CONFIG = (
| MOCK_ADVANCED_CONFIG | MOCK_ADVANCED_CONFIG
) )
PARTIAL_CLIMATE_CONFIG_USE_DEVICE_TEMP = (
MOCK_TH_OVER_CLIMATE_USER_CONFIG
| MOCK_TH_OVER_CLIMATE_MAIN_CONFIG
| MOCK_TH_OVER_CLIMATE_CENTRAL_MAIN_CONFIG
| MOCK_TH_OVER_CLIMATE_TYPE_USE_DEVICE_TEMP_CONFIG
| MOCK_PRESETS_CONFIG
| MOCK_ADVANCED_CONFIG
)
PARTIAL_CLIMATE_NOT_REGULATED_CONFIG = ( PARTIAL_CLIMATE_NOT_REGULATED_CONFIG = (
MOCK_TH_OVER_CLIMATE_USER_CONFIG MOCK_TH_OVER_CLIMATE_USER_CONFIG
| MOCK_TH_OVER_CLIMATE_MAIN_CONFIG | MOCK_TH_OVER_CLIMATE_MAIN_CONFIG
@@ -137,6 +161,49 @@ FULL_CENTRAL_CONFIG = {
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor", CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
CONF_TEMP_MIN: 15, CONF_TEMP_MIN: 15,
CONF_TEMP_MAX: 30, CONF_TEMP_MAX: 30,
CONF_STEP_TEMPERATURE: 0.1,
CONF_TPI_COEF_INT: 0.5,
CONF_TPI_COEF_EXT: 0.02,
"frost_temp": 10,
"eco_temp": 17.1,
"comfort_temp": 0,
"boost_temp": 19.1,
"eco_ac_temp": 25.1,
"comfort_ac_temp": 23.1,
"boost_ac_temp": 21.1,
"frost_away_temp": 15.1,
"eco_away_temp": 15.2,
"comfort_away_temp": 0,
"boost_away_temp": 15.4,
"eco_ac_away_temp": 30.5,
"comfort_ac_away_temp": 0,
"boost_ac_away_temp": 30.7,
CONF_WINDOW_DELAY: 15,
CONF_WINDOW_AUTO_OPEN_THRESHOLD: 4,
CONF_WINDOW_AUTO_CLOSE_THRESHOLD: 1,
CONF_WINDOW_AUTO_MAX_DURATION: 31,
CONF_MOTION_DELAY: 31,
CONF_MOTION_OFF_DELAY: 301,
CONF_MOTION_PRESET: "boost",
CONF_NO_MOTION_PRESET: "frost",
CONF_POWER_SENSOR: "sensor.mock_power_sensor",
CONF_MAX_POWER_SENSOR: "sensor.mock_max_power_sensor",
CONF_PRESENCE_SENSOR: "binary_sensor.mock_presence_sensor",
CONF_PRESET_POWER: 14,
CONF_MINIMAL_ACTIVATION_DELAY: 11,
CONF_SECURITY_DELAY_MIN: 61,
CONF_SECURITY_MIN_ON_PERCENT: 0.5,
CONF_SECURITY_DEFAULT_ON_PERCENT: 0.2,
CONF_USE_CENTRAL_BOILER_FEATURE: False,
}
FULL_CENTRAL_CONFIG_WITH_BOILER = {
CONF_NAME: CENTRAL_CONFIG_NAME,
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_CENTRAL_CONFIG,
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
CONF_TEMP_MIN: 15,
CONF_TEMP_MAX: 30,
CONF_STEP_TEMPERATURE: 0.1,
CONF_TPI_COEF_INT: 0.5, CONF_TPI_COEF_INT: 0.5,
CONF_TPI_COEF_EXT: 0.02, CONF_TPI_COEF_EXT: 0.02,
"frost_temp": 10, "frost_temp": 10,
@@ -168,8 +235,12 @@ FULL_CENTRAL_CONFIG = {
CONF_SECURITY_DELAY_MIN: 61, CONF_SECURITY_DELAY_MIN: 61,
CONF_SECURITY_MIN_ON_PERCENT: 0.5, CONF_SECURITY_MIN_ON_PERCENT: 0.5,
CONF_SECURITY_DEFAULT_ON_PERCENT: 0.2, CONF_SECURITY_DEFAULT_ON_PERCENT: 0.2,
CONF_USE_CENTRAL_BOILER_FEATURE: True,
CONF_CENTRAL_BOILER_ACTIVATION_SRV: "switch.pompe_chaudiere/switch.turn_on",
CONF_CENTRAL_BOILER_DEACTIVATION_SRV: "switch.pompe_chaudiere/switch.turn_off",
} }
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@@ -210,9 +281,15 @@ class MockClimate(ClimateEntity):
self._attr_target_temperature = 20 self._attr_target_temperature = 20
self._attr_current_temperature = 15 self._attr_current_temperature = 15
self._attr_hvac_action = hvac_action self._attr_hvac_action = hvac_action
self._attr_target_temperature_step = 0.2
self._fan_modes = fan_modes if fan_modes else None self._fan_modes = fan_modes if fan_modes else None
self._attr_fan_mode = None self._attr_fan_mode = None
@property
def name(self) -> str:
"""The name"""
return self._name
@property @property
def hvac_action(self): def hvac_action(self):
"""The hvac action of the mock climate""" """The hvac action of the mock climate"""
@@ -244,10 +321,18 @@ class MockClimate(ClimateEntity):
"""The hvac mode""" """The hvac mode"""
self._attr_hvac_mode = hvac_mode self._attr_hvac_mode = hvac_mode
def set_hvac_mode(self, hvac_mode):
"""The hvac mode"""
self._attr_hvac_mode = hvac_mode
def set_hvac_action(self, hvac_action: HVACAction): def set_hvac_action(self, hvac_action: HVACAction):
"""Set the HVACaction""" """Set the HVACaction"""
self._attr_hvac_action = hvac_action self._attr_hvac_action = hvac_action
def set_current_temperature(self, current_temperature):
"""Set the current_temperature"""
self._attr_current_temperature = current_temperature
class MockUnavailableClimate(ClimateEntity): class MockUnavailableClimate(ClimateEntity):
"""A Mock Climate class used for Underlying climate mode""" """A Mock Climate class used for Underlying climate mode"""
@@ -350,25 +435,171 @@ class MagicMockClimate(MagicMock):
return 19 return 19
class MagicMockClimateWithTemperatureRange(MagicMock):
"""A Magic Mock class for a underlying climate entity"""
@property
def temperature_unit(self): # pylint: disable=missing-function-docstring
return UnitOfTemperature.CELSIUS
@property
def hvac_mode(self): # pylint: disable=missing-function-docstring
return HVACMode.HEAT
@property
def hvac_action(self): # pylint: disable=missing-function-docstring
return HVACAction.IDLE
@property
def target_temperature(self): # pylint: disable=missing-function-docstring
return 15
@property
def current_temperature(self): # pylint: disable=missing-function-docstring
return 14
@property
def target_temperature_step( # pylint: disable=missing-function-docstring
self,
) -> float | None:
return 0.5
@property
def target_temperature_high( # pylint: disable=missing-function-docstring
self,
) -> float | None:
return 35
@property
def target_temperature_low( # pylint: disable=missing-function-docstring
self,
) -> float | None:
return 7
@property
def hvac_modes( # pylint: disable=missing-function-docstring
self,
) -> list[str] | None:
return [HVACMode.HEAT, HVACMode.OFF, HVACMode.COOL]
@property
def fan_modes( # pylint: disable=missing-function-docstring
self,
) -> list[str] | None:
return None
@property
def swing_modes( # pylint: disable=missing-function-docstring
self,
) -> list[str] | None:
return None
@property
def fan_mode(self) -> str | None: # pylint: disable=missing-function-docstring
return None
@property
def swing_mode(self) -> str | None: # pylint: disable=missing-function-docstring
return None
@property
def supported_features(self): # pylint: disable=missing-function-docstring
return ClimateEntityFeature.TARGET_TEMPERATURE_RANGE
@property
def min_temp(self): # pylint: disable=missing-function-docstring
return 10
@property
def max_temp(self): # pylint: disable=missing-function-docstring
return 31
class MockSwitch(SwitchEntity):
"""A fake switch to be used instead real switch"""
def __init__( # pylint: disable=unused-argument, dangerous-default-value
self, hass: HomeAssistant, unique_id, name, entry_infos={}
):
"""Init the switch"""
super().__init__()
self.hass = hass
self.platform = "switch"
self.entity_id = self.platform + "." + unique_id
self._name = name
self._attr_is_on = False
@property
def name(self) -> str:
"""The name"""
return self._name
@overrides
def turn_on(self, **kwargs: Any):
"""Turns the switch on and notify the state change"""
self._attr_is_on = True
# self.async_write_ha_state()
@overrides
def turn_off(self, **kwargs: Any):
"""Turns the switch on and notify the state change"""
self._attr_is_on = False
# self.async_write_ha_state()
class MockNumber(NumberEntity):
"""A fake switch to be used instead real switch"""
def __init__( # pylint: disable=unused-argument, dangerous-default-value
self,
hass: HomeAssistant,
unique_id,
name,
min=0,
max=100,
step=1,
entry_infos={},
):
"""Init the switch"""
super().__init__()
self.hass = hass
self.platform = "number"
self.entity_id = self.platform + "." + unique_id
self._name = name
self._attr_native_value = 0
self._attr_native_min_value = min
self._attr_native_max_value = max
self._attr_step = step
@property
def name(self) -> str:
"""The name"""
return self._name
@overrides
def set_native_value(self, value: float):
"""Change the value"""
self._attr_native_value = value
async def create_thermostat( async def create_thermostat(
hass: HomeAssistant, entry: MockConfigEntry, entity_id: str hass: HomeAssistant, entry: MockConfigEntry, entity_id: str
) -> BaseThermostat: ) -> BaseThermostat:
"""Creates and return a TPI Thermostat""" """Creates and return a TPI Thermostat"""
with patch( entry.add_to_hass(hass)
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event" await hass.config_entries.async_setup(entry.entry_id)
): assert entry.state is ConfigEntryState.LOADED
entry.add_to_hass(hass)
await hass.config_entries.async_setup(entry.entry_id)
assert entry.state is ConfigEntryState.LOADED
# def find_my_entity(entity_id) -> ClimateEntity: # We should reload the VTherm links
# """Find my new entity""" # vtherm_api: VersatileThermostatAPI = VersatileThermostatAPI.get_vtherm_api()
# component: EntityComponent[ClimateEntity] = hass.data[CLIMATE_DOMAIN] # central_config = vtherm_api.find_central_configuration()
# for entity in component.entities: entity = search_entity(hass, entity_id, CLIMATE_DOMAIN)
# if entity.entity_id == entity_id: # if entity and hasattr(entity, "init_presets")::
# return entity # await entity.init_presets(central_config)
return search_entity(hass, entity_id, CLIMATE_DOMAIN) return entity
async def create_central_config( # pylint: disable=dangerous-default-value async def create_central_config( # pylint: disable=dangerous-default-value
@@ -391,11 +622,14 @@ async def create_central_config( # pylint: disable=dangerous-default-value
central_configuration = api.find_central_configuration() central_configuration = api.find_central_configuration()
assert central_configuration is not None assert central_configuration is not None
return central_configuration
def search_entity(hass: HomeAssistant, entity_id, domain) -> Entity: def search_entity(hass: HomeAssistant, entity_id, domain) -> Entity:
"""Search and return the entity in the domain""" """Search and return the entity in the domain"""
component = hass.data[domain] component = hass.data[domain]
for entity in component.entities: for entity in component.entities:
_LOGGER.debug("Found %s entity: %s", domain, entity.entity_id)
if entity.entity_id == entity_id: if entity.entity_id == entity_id:
return entity return entity
return None return None
@@ -428,7 +662,34 @@ async def send_temperature_change_event(
) )
}, },
) )
await entity._async_temperature_changed(temp_event) dearm_window_auto = await entity._async_temperature_changed(temp_event)
if sleep:
await asyncio.sleep(0.1)
return dearm_window_auto
async def send_last_seen_temperature_change_event(
entity: BaseThermostat, date, sleep=True
):
"""Sending a new last seen event simulating a change on last seen temperature sensor"""
_LOGGER.info(
"------- Testu: sending send_last_seen_temperature_change_event, date=%s on %s",
date,
entity,
)
last_seen_event = Event(
EVENT_STATE_CHANGED,
{
"new_state": State(
entity_id=entity.entity_id,
state=date,
last_changed=date,
last_updated=date,
)
},
)
await entity._async_last_seen_temperature_changed(last_seen_event)
if sleep: if sleep:
await asyncio.sleep(0.1) await asyncio.sleep(0.1)
@@ -619,6 +880,7 @@ async def send_climate_change_event(
old_hvac_action: HVACAction, old_hvac_action: HVACAction,
date, date,
sleep=True, sleep=True,
underlying_entity_id: str = None,
): ):
"""Sending a new climate event simulating a change on the underlying climate state""" """Sending a new climate event simulating a change on the underlying climate state"""
_LOGGER.info( _LOGGER.info(
@@ -630,18 +892,23 @@ async def send_climate_change_event(
date, date,
entity, entity,
) )
send_from_entity_id = (
underlying_entity_id if underlying_entity_id is not None else entity.entity_id
)
climate_event = Event( climate_event = Event(
EVENT_STATE_CHANGED, EVENT_STATE_CHANGED,
{ {
"new_state": State( "new_state": State(
entity_id=entity.entity_id, entity_id=send_from_entity_id,
state=new_hvac_mode, state=new_hvac_mode,
attributes={"hvac_action": new_hvac_action}, attributes={"hvac_action": new_hvac_action},
last_changed=date, last_changed=date,
last_updated=date, last_updated=date,
), ),
"old_state": State( "old_state": State(
entity_id=entity.entity_id, entity_id=send_from_entity_id,
state=old_hvac_mode, state=old_hvac_mode,
attributes={"hvac_action": old_hvac_action}, attributes={"hvac_action": old_hvac_action},
last_changed=date, last_changed=date,
@@ -707,3 +974,53 @@ def cancel_switchs_cycles(entity: BaseThermostat):
return return
for under in entity._underlyings: for under in entity._underlyings:
under._cancel_cycle() under._cancel_cycle()
async def set_climate_preset_temp(
entity: BaseThermostat, temp_number_name: str, temp: float
):
"""Set a preset value in the temp Number entity"""
number_entity_id = (
NUMBER_DOMAIN
+ "."
+ entity.entity_id.split(".")[1]
+ "_preset_"
+ temp_number_name
+ PRESET_TEMP_SUFFIX
)
temp_entity = search_entity(
entity.hass,
number_entity_id,
NUMBER_DOMAIN,
)
if temp_entity:
await temp_entity.async_set_native_value(temp)
else:
_LOGGER.warning(
"commons tests set_cliamte_preset_temp: cannot find number entity with entity_id '%s'",
number_entity_id,
)
async def set_all_climate_preset_temp(
hass, vtherm: BaseThermostat, temps: dict, number_entity_base_name: str
):
"""Initialize all temp of preset for a VTherm entity"""
# We initialize
for preset_name, value in temps.items():
await set_climate_preset_temp(vtherm, preset_name, value)
# Search the number entity to control it is correctly set
number_entity_name = (
f"number.{number_entity_base_name}_preset_{preset_name}{PRESET_TEMP_SUFFIX}"
)
temp_entity: NumberEntity = search_entity(
hass,
number_entity_name,
NUMBER_DOMAIN,
)
assert temp_entity
# Because set_value is not implemented in Number class (really don't understand why...)
assert temp_entity.state == value

View File

@@ -29,10 +29,37 @@ from custom_components.versatile_thermostat.config_flow import (
from custom_components.versatile_thermostat.vtherm_api import VersatileThermostatAPI from custom_components.versatile_thermostat.vtherm_api import VersatileThermostatAPI
from custom_components.versatile_thermostat.base_thermostat import BaseThermostat from custom_components.versatile_thermostat.base_thermostat import BaseThermostat
from .commons import create_central_config from .commons import (
create_central_config,
FULL_CENTRAL_CONFIG,
FULL_CENTRAL_CONFIG_WITH_BOILER,
)
# https://github.com/miketheman/pytest-socket/pull/275
from pytest_socket import socket_allow_hosts
# ...
# ...
def pytest_runtest_setup():
"""setup tests"""
socket_allow_hosts(
allowed=["localhost", "127.0.0.1", "::1"], allow_unix_socket=True
)
pytest_plugins = "pytest_homeassistant_custom_component" # pylint: disable=invalid-name pytest_plugins = "pytest_homeassistant_custom_component" # pylint: disable=invalid-name
# Permet d'exclure certains test en mode d'ex
# sequential = pytest.mark.sequential
# This fixture allow to execute some tests first and not in //
# @pytest.fixture
# def order():
# return 1
#
# This fixture enables loading custom integrations in all tests. # This fixture enables loading custom integrations in all tests.
# Remove to enable selective use of this fixture # Remove to enable selective use of this fixture
@@ -127,6 +154,16 @@ async def init_central_config_fixture(
hass, init_vtherm_api hass, init_vtherm_api
): # pylint: disable=unused-argument ): # pylint: disable=unused-argument
"""Initialize the VTherm API""" """Initialize the VTherm API"""
await create_central_config(hass) 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 yield

View File

@@ -1,4 +1,5 @@
""" The commons const for all tests """ """ The commons const for all tests """
from homeassistant.components.climate.const import ( # pylint: disable=unused-import from homeassistant.components.climate.const import ( # pylint: disable=unused-import
PRESET_BOOST, PRESET_BOOST,
PRESET_COMFORT, PRESET_COMFORT,
@@ -18,10 +19,10 @@ MOCK_TH_OVER_SWITCH_MAIN_CONFIG = {
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor", CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
CONF_CYCLE_MIN: 5, CONF_CYCLE_MIN: 5,
CONF_DEVICE_POWER: 1, CONF_DEVICE_POWER: 1,
CONF_USE_WINDOW_FEATURE: True, # CONF_USE_WINDOW_FEATURE: True,
CONF_USE_MOTION_FEATURE: True, # CONF_USE_MOTION_FEATURE: True,
CONF_USE_POWER_FEATURE: True, # CONF_USE_POWER_FEATURE: True,
CONF_USE_PRESENCE_FEATURE: True, # CONF_USE_PRESENCE_FEATURE: True,
CONF_USE_MAIN_CENTRAL_CONFIG: True, CONF_USE_MAIN_CENTRAL_CONFIG: True,
} }
@@ -33,6 +34,7 @@ MOCK_TH_OVER_4SWITCH_USER_CONFIG = {
CONF_CYCLE_MIN: 8, CONF_CYCLE_MIN: 8,
CONF_TEMP_MIN: 15, CONF_TEMP_MIN: 15,
CONF_TEMP_MAX: 30, CONF_TEMP_MAX: 30,
CONF_STEP_TEMPERATURE: 0.1,
CONF_DEVICE_POWER: 1, CONF_DEVICE_POWER: 1,
CONF_USE_WINDOW_FEATURE: True, CONF_USE_WINDOW_FEATURE: True,
CONF_USE_MOTION_FEATURE: True, CONF_USE_MOTION_FEATURE: True,
@@ -51,7 +53,7 @@ MOCK_TH_OVER_CLIMATE_MAIN_CONFIG = {
CONF_CYCLE_MIN: 5, CONF_CYCLE_MIN: 5,
CONF_DEVICE_POWER: 1, CONF_DEVICE_POWER: 1,
CONF_USE_MAIN_CENTRAL_CONFIG: False, CONF_USE_MAIN_CENTRAL_CONFIG: False,
CONF_USE_CENTRAL_MODE: True CONF_USE_CENTRAL_MODE: True,
# Keep default values which are False # Keep default values which are False
} }
@@ -59,6 +61,7 @@ MOCK_TH_OVER_CLIMATE_CENTRAL_MAIN_CONFIG = {
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor", CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
CONF_TEMP_MIN: 15, CONF_TEMP_MIN: 15,
CONF_TEMP_MAX: 30, CONF_TEMP_MAX: 30,
CONF_STEP_TEMPERATURE: 0.1,
# Keep default values which are False # Keep default values which are False
} }
@@ -66,11 +69,13 @@ MOCK_TH_OVER_SWITCH_CENTRAL_MAIN_CONFIG = {
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor", CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
CONF_TEMP_MIN: 15, CONF_TEMP_MIN: 15,
CONF_TEMP_MAX: 30, CONF_TEMP_MAX: 30,
CONF_STEP_TEMPERATURE: 0.1,
# Keep default values which are False # Keep default values which are False
} }
MOCK_TH_OVER_SWITCH_TYPE_CONFIG = { MOCK_TH_OVER_SWITCH_TYPE_CONFIG = {
CONF_HEATER: "switch.mock_switch", CONF_HEATER: "switch.mock_switch",
CONF_HEATER_KEEP_ALIVE: 0,
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI, CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
CONF_AC_MODE: False, CONF_AC_MODE: False,
CONF_INVERSE_SWITCH: False, CONF_INVERSE_SWITCH: False,
@@ -88,6 +93,7 @@ MOCK_TH_OVER_4SWITCH_TYPE_CONFIG = {
CONF_HEATER_2: "switch.mock_4switch1", CONF_HEATER_2: "switch.mock_4switch1",
CONF_HEATER_3: "switch.mock_4switch2", CONF_HEATER_3: "switch.mock_4switch2",
CONF_HEATER_4: "switch.mock_4switch3", CONF_HEATER_4: "switch.mock_4switch3",
CONF_HEATER_KEEP_ALIVE: 0,
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI, CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
CONF_AC_MODE: False, CONF_AC_MODE: False,
CONF_INVERSE_SWITCH: False, CONF_INVERSE_SWITCH: False,
@@ -105,6 +111,17 @@ MOCK_TH_OVER_CLIMATE_TYPE_CONFIG = {
CONF_AUTO_REGULATION_DTEMP: 0.5, CONF_AUTO_REGULATION_DTEMP: 0.5,
CONF_AUTO_REGULATION_PERIOD_MIN: 2, CONF_AUTO_REGULATION_PERIOD_MIN: 2,
CONF_AUTO_FAN_MODE: CONF_AUTO_FAN_HIGH, CONF_AUTO_FAN_MODE: CONF_AUTO_FAN_HIGH,
CONF_AUTO_REGULATION_USE_DEVICE_TEMP: False,
}
MOCK_TH_OVER_CLIMATE_TYPE_USE_DEVICE_TEMP_CONFIG = {
CONF_CLIMATE: "climate.mock_climate",
CONF_AC_MODE: False,
CONF_AUTO_REGULATION_MODE: CONF_AUTO_REGULATION_STRONG,
CONF_AUTO_REGULATION_DTEMP: 0.1,
CONF_AUTO_REGULATION_PERIOD_MIN: 2,
CONF_AUTO_FAN_MODE: CONF_AUTO_FAN_HIGH,
CONF_AUTO_REGULATION_USE_DEVICE_TEMP: True,
} }
MOCK_TH_OVER_CLIMATE_TYPE_NOT_REGULATED_CONFIG = { MOCK_TH_OVER_CLIMATE_TYPE_NOT_REGULATED_CONFIG = {
@@ -121,21 +138,23 @@ MOCK_TH_OVER_CLIMATE_TYPE_AC_CONFIG = {
CONF_AUTO_REGULATION_PERIOD_MIN: 1, CONF_AUTO_REGULATION_PERIOD_MIN: 1,
} }
# TODO remove this later
MOCK_PRESETS_CONFIG = { MOCK_PRESETS_CONFIG = {
PRESET_FROST_PROTECTION + "_temp": 7, PRESET_FROST_PROTECTION + PRESET_TEMP_SUFFIX: 7,
PRESET_ECO + "_temp": 16, PRESET_ECO + PRESET_TEMP_SUFFIX: 16,
PRESET_COMFORT + "_temp": 17, PRESET_COMFORT + PRESET_TEMP_SUFFIX: 17,
PRESET_BOOST + "_temp": 18, PRESET_BOOST + PRESET_TEMP_SUFFIX: 18,
} }
# TODO remove this later
MOCK_PRESETS_AC_CONFIG = { MOCK_PRESETS_AC_CONFIG = {
PRESET_FROST_PROTECTION + "_temp": 7, PRESET_FROST_PROTECTION + PRESET_TEMP_SUFFIX: 7,
PRESET_ECO + "_temp": 17, PRESET_ECO + PRESET_TEMP_SUFFIX: 17,
PRESET_COMFORT + "_temp": 19, PRESET_COMFORT + PRESET_TEMP_SUFFIX: 19,
PRESET_BOOST + "_temp": 20, PRESET_BOOST + PRESET_TEMP_SUFFIX: 20,
PRESET_ECO + "_ac_temp": 25, PRESET_ECO + PRESET_AC_SUFFIX + PRESET_TEMP_SUFFIX: 25,
PRESET_COMFORT + "_ac_temp": 23, PRESET_COMFORT + PRESET_AC_SUFFIX + PRESET_TEMP_SUFFIX: 23,
PRESET_BOOST + "_ac_temp": 21, PRESET_BOOST + PRESET_AC_SUFFIX + PRESET_TEMP_SUFFIX: 21,
} }
MOCK_WINDOW_CONFIG = { MOCK_WINDOW_CONFIG = {
@@ -152,6 +171,7 @@ MOCK_WINDOW_AUTO_CONFIG = {
CONF_WINDOW_AUTO_OPEN_THRESHOLD: 1.0, CONF_WINDOW_AUTO_OPEN_THRESHOLD: 1.0,
CONF_WINDOW_AUTO_CLOSE_THRESHOLD: 0.0, CONF_WINDOW_AUTO_CLOSE_THRESHOLD: 0.0,
CONF_WINDOW_AUTO_MAX_DURATION: 5.0, CONF_WINDOW_AUTO_MAX_DURATION: 5.0,
CONF_WINDOW_ACTION: CONF_WINDOW_FAN_ONLY,
} }
MOCK_MOTION_CONFIG = { MOCK_MOTION_CONFIG = {
@@ -170,20 +190,10 @@ MOCK_POWER_CONFIG = {
MOCK_PRESENCE_CONFIG = { MOCK_PRESENCE_CONFIG = {
CONF_PRESENCE_SENSOR: "person.presence_sensor", CONF_PRESENCE_SENSOR: "person.presence_sensor",
PRESET_ECO + PRESET_AWAY_SUFFIX + "_temp": 16,
PRESET_COMFORT + PRESET_AWAY_SUFFIX + "_temp": 17,
PRESET_BOOST + PRESET_AWAY_SUFFIX + "_temp": 18,
} }
MOCK_PRESENCE_AC_CONFIG = { MOCK_PRESENCE_AC_CONFIG = {
CONF_PRESENCE_SENSOR: "person.presence_sensor", CONF_PRESENCE_SENSOR: "person.presence_sensor",
PRESET_FROST_PROTECTION + PRESET_AWAY_SUFFIX + "_temp": 7,
PRESET_ECO + PRESET_AWAY_SUFFIX + "_temp": 16,
PRESET_COMFORT + PRESET_AWAY_SUFFIX + "_temp": 17,
PRESET_BOOST + PRESET_AWAY_SUFFIX + "_temp": 18,
PRESET_ECO + "_ac" + PRESET_AWAY_SUFFIX + "_temp": 27,
PRESET_COMFORT + "_ac" + PRESET_AWAY_SUFFIX + "_temp": 26,
PRESET_BOOST + "_ac" + PRESET_AWAY_SUFFIX + "_temp": 25,
} }
MOCK_ADVANCED_CONFIG = { MOCK_ADVANCED_CONFIG = {

View File

@@ -211,13 +211,14 @@ async def test_over_climate_auto_fan_mode_turbo_activation(
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate", "custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
return_value=fake_underlying_climate, return_value=fake_underlying_climate,
): ):
entry.add_to_hass(hass) entity = await create_thermostat(hass, entry, "climate.theoverclimatemockname")
await hass.config_entries.async_setup(entry.entry_id) # entry.add_to_hass(hass)
assert entry.state is ConfigEntryState.LOADED # await hass.config_entries.async_setup(entry.entry_id)
# assert entry.state is ConfigEntryState.LOADED
entity: ThermostatOverClimate = search_entity( #
hass, "climate.theoverclimatemockname", "climate" # entity: ThermostatOverClimate = search_entity(
) # hass, "climate.theoverclimatemockname", "climate"
# )
assert entity assert entity
assert isinstance(entity, ThermostatOverClimate) assert isinstance(entity, ThermostatOverClimate)

View File

@@ -1,7 +1,7 @@
# pylint: disable=wildcard-import, unused-wildcard-import, protected-access, unused-argument, line-too-long # pylint: disable=wildcard-import, unused-wildcard-import, protected-access, unused-argument, line-too-long
""" Test the normal start of a Thermostat """ """ Test the normal start of a Thermostat """
from unittest.mock import patch # , call from unittest.mock import patch, call
from datetime import datetime, timedelta from datetime import datetime, timedelta
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
@@ -52,18 +52,7 @@ async def test_over_climate_regulation(
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate", "custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
return_value=fake_underlying_climate, return_value=fake_underlying_climate,
): ):
entry.add_to_hass(hass) entity = await create_thermostat(hass, entry, "climate.theoverclimatemockname")
await hass.config_entries.async_setup(entry.entry_id)
assert entry.state is ConfigEntryState.LOADED
def find_my_entity(entity_id) -> ClimateEntity:
"""Find my new entity"""
component: EntityComponent[ClimateEntity] = hass.data[CLIMATE_DOMAIN]
for entity in component.entities:
if entity.entity_id == entity_id:
return entity
entity: ThermostatOverClimate = find_my_entity("climate.theoverclimatemockname")
assert entity assert entity
assert isinstance(entity, ThermostatOverClimate) assert isinstance(entity, ThermostatOverClimate)
@@ -71,6 +60,7 @@ async def test_over_climate_regulation(
assert entity.name == "TheOverClimateMockName" assert entity.name == "TheOverClimateMockName"
assert entity.is_over_climate is True assert entity.is_over_climate is True
assert entity.is_regulated is True assert entity.is_regulated is True
assert entity.auto_regulation_use_device_temp is False
assert entity.hvac_mode is HVACMode.OFF assert entity.hvac_mode is HVACMode.OFF
assert entity.hvac_action is HVACAction.OFF assert entity.hvac_action is HVACAction.OFF
assert entity.target_temperature == entity.min_temp assert entity.target_temperature == entity.min_temp
@@ -126,9 +116,7 @@ async def test_over_climate_regulation(
# the regulated temperature should be under # the regulated temperature should be under
assert entity.regulated_target_temp < entity.target_temperature assert entity.regulated_target_temp < entity.target_temperature
assert ( assert entity.regulated_target_temp == 18 - 2.5
entity.regulated_target_temp == 18 - 2
) # normally 0.6 but round_to_nearest gives 0.5
@pytest.mark.parametrize("expected_lingering_tasks", [True]) @pytest.mark.parametrize("expected_lingering_tasks", [True])
@@ -162,18 +150,7 @@ async def test_over_climate_regulation_ac_mode(
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate", "custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
return_value=fake_underlying_climate, return_value=fake_underlying_climate,
): ):
entry.add_to_hass(hass) entity = await create_thermostat(hass, entry, "climate.theoverclimatemockname")
await hass.config_entries.async_setup(entry.entry_id)
assert entry.state is ConfigEntryState.LOADED
def find_my_entity(entity_id) -> ClimateEntity:
"""Find my new entity"""
component: EntityComponent[ClimateEntity] = hass.data[CLIMATE_DOMAIN]
for entity in component.entities:
if entity.entity_id == entity_id:
return entity
entity: ThermostatOverClimate = find_my_entity("climate.theoverclimatemockname")
assert entity assert entity
assert isinstance(entity, ThermostatOverClimate) assert isinstance(entity, ThermostatOverClimate)
@@ -374,3 +351,281 @@ async def test_over_climate_regulation_limitations(
assert ( assert (
entity.regulated_target_temp == 17 + 1.5 entity.regulated_target_temp == 17 + 1.5
) # 0.7 without round_to_nearest ) # 0.7 without round_to_nearest
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
# Disable this test which is not working when run in // of others.
# I couldn't find out why
@pytest.mark.skip
async def test_over_climate_regulation_use_device_temp(
hass: HomeAssistant, skip_hass_states_is_state, skip_send_event
):
"""Test the regulation of an over climate thermostat"""
entry = MockConfigEntry(
domain=DOMAIN,
title="TheOverClimateMockName",
unique_id="uniqueId",
# This is include a medium regulation
data=PARTIAL_CLIMATE_CONFIG_USE_DEVICE_TEMP | {CONF_AUTO_REGULATION_DTEMP: 0.5},
)
tz = get_tz(hass) # pylint: disable=invalid-name
now: datetime = datetime.now(tz=tz)
fake_underlying_climate = MockClimate(hass, "mockUniqueId", "MockClimateName", {})
assert fake_underlying_climate.current_temperature == 15
# Creates the regulated VTherm over climate
# change temperature so that the heating will start
event_timestamp = now - timedelta(minutes=10)
with patch(
"custom_components.versatile_thermostat.commons.NowClass.get_now",
return_value=event_timestamp,
), patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
return_value=fake_underlying_climate,
):
entity: ThermostatOverClimate = await create_thermostat(
hass, entry, "climate.theoverclimatemockname"
)
assert entity
assert isinstance(entity, ThermostatOverClimate)
assert entity.name == "TheOverClimateMockName"
assert entity.is_over_climate is True
assert entity.is_regulated is True
assert entity.auto_regulation_use_device_temp is True
# 1. Activate the heating by changing HVACMode and temperature
# Select a hvacmode, presence and preset
await entity.async_set_hvac_mode(HVACMode.HEAT)
assert entity.hvac_mode is HVACMode.HEAT
assert entity.regulated_target_temp == entity.min_temp
await send_temperature_change_event(entity, 18, event_timestamp)
await send_ext_temperature_change_event(entity, 10, event_timestamp)
# 2. set manual target temp (at now - 7) -> no regulation should occurs
# room temp is 18
# target is 16
# internal heater temp is 15
fake_underlying_climate.set_current_temperature(15)
event_timestamp = now - timedelta(minutes=7)
with patch(
"custom_components.versatile_thermostat.commons.NowClass.get_now",
return_value=event_timestamp,
), patch("homeassistant.core.ServiceRegistry.async_call") as mock_service_call:
await entity.async_set_temperature(temperature=16)
fake_underlying_climate.set_hvac_action(
HVACAction.HEATING
) # simulate under heating
assert entity.hvac_action == HVACAction.HEATING
assert entity.preset_mode == PRESET_NONE # Manual mode
# the regulated temperature should be higher
assert entity.regulated_target_temp < entity.target_temperature
# The calcul is the following: 16 + (16 - 18) x 0.4 (strong) + 0 x ki - 1 (device offset)
assert (
entity.regulated_target_temp == 15
) # round(16 + (16 - 18) * 0.4 + 0 * 0.08)
assert entity.hvac_action == HVACAction.HEATING
mock_service_call.assert_has_calls(
[
call.service_call(
"climate",
"set_temperature",
{
"entity_id": "climate.mock_climate",
# because device offset is -3 but not used because target is reach
"temperature": 15.0,
"target_temp_high": 30,
"target_temp_low": 15,
},
),
]
)
# 3. change temperature so that the regulated temperature should slow down
# HVACMODE.HEAT
# room temp is 15
# target is 18
# internal heater temp is 20
fake_underlying_climate.set_current_temperature(20.1)
await entity.async_set_temperature(temperature=18)
await send_ext_temperature_change_event(entity, 9, event_timestamp)
event_timestamp = now - timedelta(minutes=5)
with patch(
"custom_components.versatile_thermostat.commons.NowClass.get_now",
return_value=event_timestamp,
), patch("homeassistant.core.ServiceRegistry.async_call") as mock_service_call:
await send_temperature_change_event(entity, 15, event_timestamp)
# the regulated temperature should be under (device offset is -2)
assert entity.regulated_target_temp > entity.target_temperature
assert entity.regulated_target_temp == 19.5 # round(18 + 1.4, 0.5)
mock_service_call.assert_has_calls(
[
call.service_call(
"climate",
"set_temperature",
{
"entity_id": "climate.mock_climate",
"temperature": 24.5, # round(19.5 + 5, 0.5)
"target_temp_high": 30,
"target_temp_low": 15,
},
),
]
)
# 4. In cool mode
# room temp is 25
# target is 23
# internal heater temp is 27
await entity.async_set_hvac_mode(HVACMode.COOL)
await entity.async_set_temperature(temperature=23)
fake_underlying_climate.set_current_temperature(26.9)
await send_ext_temperature_change_event(entity, 30, event_timestamp)
event_timestamp = now - timedelta(minutes=3)
with patch(
"custom_components.versatile_thermostat.commons.NowClass.get_now",
return_value=event_timestamp,
), patch("homeassistant.core.ServiceRegistry.async_call") as mock_service_call:
await send_temperature_change_event(entity, 25, event_timestamp)
# the regulated temperature should be upper (device offset is +1.9)
assert entity.regulated_target_temp < entity.target_temperature
assert entity.regulated_target_temp == 22.5
mock_service_call.assert_has_calls(
[
call.service_call(
"climate",
"set_temperature",
{
"entity_id": "climate.mock_climate",
"temperature": 24.5, # round(22.5 + 1.9° of offset)
"target_temp_high": 30,
"target_temp_low": 15,
},
),
]
)
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_over_climate_regulation_dtemp_null(
hass: HomeAssistant, skip_hass_states_is_state, skip_send_event
):
"""Test the regulation of an over climate thermostat with no Dtemp limitation"""
entry = MockConfigEntry(
domain=DOMAIN,
title="TheOverClimateMockName",
unique_id="uniqueId",
# This is include a medium regulation
data=PARTIAL_CLIMATE_AC_CONFIG | {CONF_AUTO_REGULATION_DTEMP: 0, CONF_STEP_TEMPERATURE: 0.1},
)
tz = get_tz(hass) # pylint: disable=invalid-name
now: datetime = datetime.now(tz=tz)
fake_underlying_climate = MockClimate(hass, "mockUniqueId", "MockClimateName", {})
# Creates the regulated VTherm over climate
# change temperature so that the heating will start
event_timestamp = now - timedelta(minutes=20)
with patch(
"custom_components.versatile_thermostat.commons.NowClass.get_now",
return_value=event_timestamp,
), patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
return_value=fake_underlying_climate,
):
entity = await create_thermostat(hass, entry, "climate.theoverclimatemockname")
assert entity
assert isinstance(entity, ThermostatOverClimate)
assert entity.name == "TheOverClimateMockName"
assert entity.is_over_climate is True
assert entity.is_regulated is True
# Activate the heating by changing HVACMode and temperature
# Select a hvacmode, presence and preset
await entity.async_set_hvac_mode(HVACMode.HEAT)
assert entity.hvac_mode is HVACMode.HEAT
assert entity.hvac_action == HVACAction.OFF
# change temperature so that the heating will start
await send_temperature_change_event(entity, 15, event_timestamp)
await send_ext_temperature_change_event(entity, 10, event_timestamp)
# set manual target temp
event_timestamp = now - timedelta(minutes=17)
with patch(
"custom_components.versatile_thermostat.commons.NowClass.get_now",
return_value=event_timestamp,
):
await entity.async_set_temperature(temperature=20)
fake_underlying_climate.set_hvac_action(
HVACAction.HEATING
) # simulate under cooling
assert entity.hvac_action == HVACAction.HEATING
assert entity.preset_mode == PRESET_NONE # Manual mode
# the regulated temperature should be lower
assert entity.regulated_target_temp > entity.target_temperature
assert (
entity.regulated_target_temp == 20 + 2.4
) # In medium we could go up to +3 degre
assert entity.hvac_action == HVACAction.HEATING
# change temperature so that the regulated temperature should slow down
event_timestamp = now - timedelta(minutes=15)
with patch(
"custom_components.versatile_thermostat.commons.NowClass.get_now",
return_value=event_timestamp,
):
await send_temperature_change_event(entity, 19, event_timestamp)
await send_ext_temperature_change_event(entity, 10, event_timestamp)
# the regulated temperature should be greater
assert entity.regulated_target_temp > entity.target_temperature
assert entity.regulated_target_temp == 20 + 0.9
# change temperature so that the regulated temperature should slow down
event_timestamp = now - timedelta(minutes=13)
with patch(
"custom_components.versatile_thermostat.commons.NowClass.get_now",
return_value=event_timestamp,
):
await send_temperature_change_event(entity, 20, event_timestamp)
await send_ext_temperature_change_event(entity, 10, event_timestamp)
# the regulated temperature should be greater
assert entity.regulated_target_temp > entity.target_temperature
assert entity.regulated_target_temp == 20 + 0.5
old_regulated_temp = entity.regulated_target_temp
# Test if a small temperature change is taken into account : change temperature so that dtemp < 0.5 and time is > period_min (+ 3min)
event_timestamp = now - timedelta(minutes=10)
with patch(
"custom_components.versatile_thermostat.commons.NowClass.get_now",
return_value=event_timestamp,
):
await send_temperature_change_event(entity, 19.6, event_timestamp)
await send_ext_temperature_change_event(entity, 10, event_timestamp)
# the regulated temperature should be greater. This does not work if dtemp is not null
assert entity.regulated_target_temp > old_regulated_temp

View File

@@ -1,15 +1,29 @@
# pylint: disable=wildcard-import, unused-wildcard-import, protected-access, unused-argument, line-too-long # pylint: disable=wildcard-import, unused-wildcard-import, protected-access, unused-argument, line-too-long
""" Test the Window management """ """ Test the Window management """
from unittest.mock import patch, call import asyncio
from unittest.mock import patch, call, PropertyMock
from datetime import datetime, timedelta from datetime import datetime, timedelta
import logging import logging
from homeassistant.core import HomeAssistant, State
from homeassistant.components.climate import ( from homeassistant.components.climate import (
SERVICE_SET_TEMPERATURE, SERVICE_SET_TEMPERATURE,
) )
from custom_components.versatile_thermostat.config_flow import (
VersatileThermostatBaseConfigFlow,
)
from custom_components.versatile_thermostat.thermostat_valve import ThermostatOverValve
from custom_components.versatile_thermostat.thermostat_climate import (
ThermostatOverClimate,
)
from custom_components.versatile_thermostat.thermostat_switch import (
ThermostatOverSwitch,
)
from .commons import * from .commons import *
logging.getLogger().setLevel(logging.DEBUG) logging.getLogger().setLevel(logging.DEBUG)
@@ -358,7 +372,7 @@ async def test_bug_82(
skip_turn_on_off_heater, skip_turn_on_off_heater,
skip_send_event, skip_send_event,
): ):
"""Test that when a underlying climate is not available the VTherm doesn't go into security mode""" """Test that when a underlying climate is not available the VTherm doesn't go into safety mode"""
tz = get_tz(hass) # pylint: disable=invalid-name tz = get_tz(hass) # pylint: disable=invalid-name
now: datetime = datetime.now(tz=tz) now: datetime = datetime.now(tz=tz)
@@ -380,18 +394,19 @@ async def test_bug_82(
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate", "custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
return_value=fake_underlying_climate, return_value=fake_underlying_climate,
) as mock_find_climate: ) as mock_find_climate:
entry.add_to_hass(hass) entity = await create_thermostat(hass, entry, "climate.theoverclimatemockname")
await hass.config_entries.async_setup(entry.entry_id) # entry.add_to_hass(hass)
assert entry.state is ConfigEntryState.LOADED # 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""" # def find_my_entity(entity_id) -> ClimateEntity:
component: EntityComponent[ClimateEntity] = hass.data[CLIMATE_DOMAIN] # """Find my new entity"""
for entity in component.entities: # component: EntityComponent[ClimateEntity] = hass.data[CLIMATE_DOMAIN]
if entity.entity_id == entity_id: # for entity in component.entities:
return entity # if entity.entity_id == entity_id:
# return entity
entity = find_my_entity("climate.theoverclimatemockname") #
# entity = find_my_entity("climate.theoverclimatemockname")
assert entity assert entity
@@ -427,7 +442,7 @@ async def test_bug_82(
assert mock_find_climate.mock_calls[0] == call() assert mock_find_climate.mock_calls[0] == call()
mock_find_climate.assert_has_calls([call.find_underlying_entity()]) mock_find_climate.assert_has_calls([call.find_underlying_entity()])
# Force security mode # Force safety mode
assert entity._last_ext_temperature_measure is not None assert entity._last_ext_temperature_measure is not None
assert entity._last_temperature_measure is not None assert entity._last_temperature_measure is not None
assert ( assert (
@@ -490,18 +505,19 @@ async def test_bug_101(
) as mock_find_climate, patch( ) as mock_find_climate, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.set_hvac_mode" "custom_components.versatile_thermostat.underlyings.UnderlyingClimate.set_hvac_mode"
) as mock_underlying_set_hvac_mode: ) as mock_underlying_set_hvac_mode:
entry.add_to_hass(hass) entity = await create_thermostat(hass, entry, "climate.theoverclimatemockname")
await hass.config_entries.async_setup(entry.entry_id) # entry.add_to_hass(hass)
assert entry.state is ConfigEntryState.LOADED # 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""" # def find_my_entity(entity_id) -> ClimateEntity:
component: EntityComponent[ClimateEntity] = hass.data[CLIMATE_DOMAIN] # """Find my new entity"""
for entity in component.entities: # component: EntityComponent[ClimateEntity] = hass.data[CLIMATE_DOMAIN]
if entity.entity_id == entity_id: # for entity in component.entities:
return entity # if entity.entity_id == entity_id:
# return entity
entity = find_my_entity("climate.theoverclimatemockname") #
# entity = find_my_entity("climate.theoverclimatemockname")
assert entity assert entity
@@ -591,6 +607,7 @@ async def test_bug_272(
domain=DOMAIN, domain=DOMAIN,
title="TheOverClimateMockName", title="TheOverClimateMockName",
unique_id="uniqueId", unique_id="uniqueId",
# default value are min 15°, max 30°, step 0.1
data=PARTIAL_CLIMATE_CONFIG, # 5 minutes security delay data=PARTIAL_CLIMATE_CONFIG, # 5 minutes security delay
) )
@@ -605,24 +622,27 @@ async def test_bug_272(
), patch( ), patch(
"homeassistant.core.ServiceRegistry.async_call" "homeassistant.core.ServiceRegistry.async_call"
) as mock_service_call: ) as mock_service_call:
entry.add_to_hass(hass) entity = await create_thermostat(hass, entry, "climate.theoverclimatemockname")
await hass.config_entries.async_setup(entry.entry_id) # entry.add_to_hass(hass)
assert entry.state is ConfigEntryState.LOADED # 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""" # def find_my_entity(entity_id) -> ClimateEntity:
component: EntityComponent[ClimateEntity] = hass.data[CLIMATE_DOMAIN] # """Find my new entity"""
for entity in component.entities: # component: EntityComponent[ClimateEntity] = hass.data[CLIMATE_DOMAIN]
if entity.entity_id == entity_id: # for entity in component.entities:
return entity # if entity.entity_id == entity_id:
# return entity
entity = find_my_entity("climate.theoverclimatemockname") #
# entity = find_my_entity("climate.theoverclimatemockname")
assert entity assert entity
assert entity.name == "TheOverClimateMockName" assert entity.name == "TheOverClimateMockName"
assert entity.is_over_climate is True assert entity.is_over_climate is True
assert entity.hvac_mode is HVACMode.OFF assert entity.hvac_mode is HVACMode.OFF
# The VTherm value and not the underlying value
assert entity.target_temperature_step == 0.1
assert entity.target_temperature == entity.min_temp assert entity.target_temperature == entity.min_temp
assert entity.is_regulated is True assert entity.is_regulated is True
@@ -633,22 +653,24 @@ async def test_bug_272(
# In the accepted interval # In the accepted interval
await entity.async_set_temperature(temperature=17.5) await entity.async_set_temperature(temperature=17.5)
assert mock_service_call.call_count == 2
# MagicMock climate is already HEAT by default. So there is no SET_HAVC_MODE call
assert mock_service_call.call_count == 1
mock_service_call.assert_has_calls( mock_service_call.assert_has_calls(
[ [
call.async_call( # call.async_call(
"climate", # "climate",
SERVICE_SET_HVAC_MODE, # SERVICE_SET_HVAC_MODE,
{"entity_id": "climate.mock_climate", "hvac_mode": HVACMode.HEAT}, # {"entity_id": "climate.mock_climate", "hvac_mode": HVACMode.HEAT},
), # ),
call.async_call( call.async_call(
"climate", "climate",
SERVICE_SET_TEMPERATURE, SERVICE_SET_TEMPERATURE,
{ {
"entity_id": "climate.mock_climate", "entity_id": "climate.mock_climate",
"temperature": 17.5, "temperature": 17.5,
"target_temp_high": 30, # "target_temp_high": 30,
"target_temp_low": 15, # "target_temp_low": 15,
}, },
), ),
] ]
@@ -677,8 +699,8 @@ async def test_bug_272(
{ {
"entity_id": "climate.mock_climate", "entity_id": "climate.mock_climate",
"temperature": 15, # the minimum acceptable "temperature": 15, # the minimum acceptable
"target_temp_high": 30, # "target_temp_high": 30,
"target_temp_low": 15, # "target_temp_low": 15,
}, },
), ),
] ]
@@ -704,9 +726,651 @@ async def test_bug_272(
{ {
"entity_id": "climate.mock_climate", "entity_id": "climate.mock_climate",
"temperature": 19, # the maximum acceptable "temperature": 19, # the maximum acceptable
"target_temp_high": 30, # "target_temp_high": 30,
"target_temp_low": 15, # "target_temp_low": 15,
}, },
), ),
] ]
) )
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_bug_407(hass: HomeAssistant, skip_hass_states_is_state):
"""Test the followin case in power management:
1. a heater is active (heating). So the power consumption takes the heater power into account. We suppose the power consumption is near the threshold,
2. the user switch preset let's say from Comfort to Boost,
3. expected: no shredding should occur because the heater was already active,
4. constated: the heater goes into shredding.
"""
entry = MockConfigEntry(
domain=DOMAIN,
title="TheOverSwitchMockName",
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: ThermostatOverSwitch = await create_thermostat(
hass, entry, "climate.theoverswitchmockname"
)
assert entity
tpi_algo = entity._prop_algorithm
assert tpi_algo
tz = get_tz(hass) # pylint: disable=invalid-name
now: datetime = datetime.now(tz=tz)
await send_temperature_change_event(entity, 16, now)
await send_ext_temperature_change_event(entity, 10, now)
# 1. An already active heater will not switch to overpowering
with patch(
"homeassistant.core.ServiceRegistry.async_call"
) as mock_service_call, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
new_callable=PropertyMock,
return_value=True,
):
await entity.async_set_hvac_mode(HVACMode.HEAT)
await entity.async_set_preset_mode(PRESET_COMFORT)
assert entity.hvac_mode is HVACMode.HEAT
assert entity.preset_mode is PRESET_COMFORT
assert entity.overpowering_state is None
assert entity.target_temperature == 18
# waits that the heater starts
await asyncio.sleep(0.1)
assert mock_service_call.call_count >= 1
assert entity.is_device_active is True
# Send power max mesurement
await send_max_power_change_event(entity, 110, datetime.now())
# Send power mesurement (theheater is already in the power measurement)
await send_power_change_event(entity, 100, datetime.now())
# No overpowering yet
assert await entity.check_overpowering() is False
# All configuration is complete and power is < power_max
assert entity.preset_mode is PRESET_COMFORT
assert entity.overpowering_state is False
assert entity.is_device_active is True
# 2. An already active heater that switch preset will not switch to overpowering
with patch(
"homeassistant.core.ServiceRegistry.async_call"
) as mock_service_call, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
new_callable=PropertyMock,
return_value=True,
):
# change preset to Boost
await entity.async_set_preset_mode(PRESET_BOOST)
# waits that the heater starts
await asyncio.sleep(0.1)
assert await entity.check_overpowering() is False
assert entity.hvac_mode is HVACMode.HEAT
assert entity.preset_mode is PRESET_BOOST
assert entity.overpowering_state is False
assert entity.target_temperature == 19
assert mock_service_call.call_count >= 1
# 3. if heater is stopped (is_device_active==False), then overpowering should be started
with patch(
"homeassistant.core.ServiceRegistry.async_call"
) as mock_service_call, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
new_callable=PropertyMock,
return_value=False,
):
# change preset to Boost
await entity.async_set_preset_mode(PRESET_COMFORT)
# waits that the heater starts
await asyncio.sleep(0.1)
assert await entity.check_overpowering() is True
assert entity.hvac_mode is HVACMode.HEAT
assert entity.preset_mode is PRESET_POWER
assert entity.overpowering_state is True
async def test_bug_339(
hass: HomeAssistant,
# skip_hass_states_is_state,
init_central_config_with_boiler_fixture,
):
"""Test that the counter of active Vtherm in central boiler is
correctly updated with underlying is in auto and device is active
"""
api = VersatileThermostatAPI.get_vtherm_api(hass)
climate1 = MockClimate(
hass=hass,
unique_id="climate1",
name="theClimate1",
hvac_mode=HVACMode.AUTO,
hvac_modes=[HVACMode.AUTO, HVACMode.OFF, HVACMode.HEAT, HVACMode.COOL],
hvac_action=HVACAction.HEATING,
)
entry = MockConfigEntry(
domain=DOMAIN,
title="TheOverClimateMockName",
unique_id="uniqueId",
data={
CONF_NAME: "TheOverClimateMockName",
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_CLIMATE,
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
CONF_CYCLE_MIN: 5,
CONF_TEMP_MIN: 8,
CONF_TEMP_MAX: 18,
"frost_temp": 10,
"eco_temp": 17,
"comfort_temp": 18,
"boost_temp": 21,
CONF_USE_WINDOW_FEATURE: False,
CONF_USE_MOTION_FEATURE: False,
CONF_USE_POWER_FEATURE: False,
CONF_USE_PRESENCE_FEATURE: False,
CONF_CLIMATE: climate1.entity_id,
CONF_MINIMAL_ACTIVATION_DELAY: 30,
CONF_SECURITY_DELAY_MIN: 5,
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
CONF_SECURITY_DEFAULT_ON_PERCENT: 0.1,
CONF_USE_MAIN_CENTRAL_CONFIG: True,
CONF_USE_PRESETS_CENTRAL_CONFIG: True,
CONF_USE_ADVANCED_CENTRAL_CONFIG: True,
CONF_USED_BY_CENTRAL_BOILER: True,
},
)
with patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
return_value=climate1,
):
entity: ThermostatOverValve = await create_thermostat(
hass, entry, "climate.theoverclimatemockname"
)
assert entity
assert entity.name == "TheOverClimateMockName"
assert entity.is_over_climate
assert entity.underlying_entities[0].entity_id == "climate.climate1"
assert api.nb_active_device_for_boiler_threshold == 1
await entity.async_set_hvac_mode(HVACMode.AUTO)
# Simulate a state change in underelying
await api.nb_active_device_for_boiler_entity.calculate_nb_active_devices(None)
# The VTherm should be active
assert entity.underlying_entity(0).is_device_active is True
assert entity.is_device_active is True
assert api.nb_active_device_for_boiler == 1
entity.remove_thermostat()
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_bug_508(
hass: HomeAssistant,
skip_hass_states_is_state,
skip_turn_on_off_heater,
skip_send_event,
):
"""Test that it not possible to set the target temperature under the min_temp setting"""
tz = get_tz(hass) # pylint: disable=invalid-name
now: datetime = datetime.now(tz=tz)
entry = MockConfigEntry(
domain=DOMAIN,
title="TheOverClimateMockName",
unique_id="uniqueId",
# default value are min 15°, max 31°, step 0.1
data=PARTIAL_CLIMATE_CONFIG, # 5 minutes security delay
)
# Min_temp is 10 and max_temp is 31 and features contains TARGET_TEMPERATURE_RANGE
fake_underlying_climate = MagicMockClimateWithTemperatureRange()
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
), patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
return_value=fake_underlying_climate,
), patch(
"homeassistant.core.ServiceRegistry.async_call"
) as mock_service_call:
entity = await create_thermostat(hass, entry, "climate.theoverclimatemockname")
assert entity
assert entity.name == "TheOverClimateMockName"
assert entity.is_over_climate is True
assert entity.hvac_mode is HVACMode.OFF
# The VTherm value and not the underlying value
assert entity.target_temperature_step == 0.1
assert entity.target_temperature == entity.min_temp
assert entity.is_regulated is True
assert mock_service_call.call_count == 0
# Set the hvac_mode to HEAT
await entity.async_set_hvac_mode(HVACMode.HEAT)
# Not In the accepted interval -> should be converted into 10 (the min) and send with target_temp_high and target_temp_low
await entity.async_set_temperature(temperature=8.5)
# MagicMock climate is already HEAT by default. So there is no SET_HAVC_MODE call
assert mock_service_call.call_count == 1
mock_service_call.assert_has_calls(
[
call.async_call(
"climate",
SERVICE_SET_TEMPERATURE,
{
"entity_id": "climate.mock_climate",
# "temperature": 17.5,
"target_temp_high": 10,
"target_temp_low": 10,
},
),
]
)
with patch("homeassistant.core.ServiceRegistry.async_call") as mock_service_call:
# Not In the accepted interval -> should be converted into 10 (the min) and send with target_temp_high and target_temp_low
await entity.async_set_temperature(temperature=32)
# MagicMock climate is already HEAT by default. So there is no SET_HAVC_MODE call
assert mock_service_call.call_count == 1
mock_service_call.assert_has_calls(
[
call.async_call(
"climate",
SERVICE_SET_TEMPERATURE,
{
"entity_id": "climate.mock_climate",
"target_temp_high": 31,
"target_temp_low": 31,
},
),
]
)
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_bug_500_1(hass: HomeAssistant, init_vtherm_api) -> None:
"""Test that the form is served with no input"""
config = {
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_SWITCH,
CONF_USE_WINDOW_CENTRAL_CONFIG: True,
CONF_USE_POWER_CENTRAL_CONFIG: True,
CONF_USE_PRESENCE_CENTRAL_CONFIG: True,
CONF_USE_MOTION_FEATURE: True,
CONF_MOTION_SENSOR: "sensor.theMotionSensor",
}
flow = VersatileThermostatBaseConfigFlow(config)
assert flow._infos[CONF_USE_WINDOW_FEATURE] is True
assert flow._infos[CONF_USE_POWER_FEATURE] is True
assert flow._infos[CONF_USE_PRESENCE_FEATURE] is True
assert flow._infos[CONF_USE_MOTION_FEATURE] is True
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_bug_500_2(hass: HomeAssistant, init_vtherm_api) -> None:
"""Test that the form is served with no input"""
config = {
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_SWITCH,
CONF_USE_WINDOW_CENTRAL_CONFIG: False,
CONF_USE_POWER_CENTRAL_CONFIG: False,
CONF_USE_PRESENCE_CENTRAL_CONFIG: False,
CONF_USE_MOTION_FEATURE: False,
}
flow = VersatileThermostatBaseConfigFlow(config)
assert flow._infos[CONF_USE_WINDOW_FEATURE] is False
assert flow._infos[CONF_USE_POWER_FEATURE] is False
assert flow._infos[CONF_USE_PRESENCE_FEATURE] is False
assert flow._infos[CONF_USE_MOTION_FEATURE] is False
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_bug_500_3(hass: HomeAssistant, init_vtherm_api) -> None:
"""Test that the form is served with no input"""
config = {
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_SWITCH,
CONF_USE_WINDOW_CENTRAL_CONFIG: False,
CONF_WINDOW_SENSOR: "sensor.theWindowSensor",
CONF_USE_POWER_CENTRAL_CONFIG: False,
CONF_POWER_SENSOR: "sensor.thePowerSensor",
CONF_MAX_POWER_SENSOR: "sensor.theMaxPowerSensor",
CONF_USE_PRESENCE_CENTRAL_CONFIG: False,
CONF_PRESENCE_SENSOR: "sensor.thePresenceSensor",
CONF_USE_MOTION_FEATURE: True, # motion sensor need to be checked AND a motion sensor set
CONF_MOTION_SENSOR: "sensor.theMotionSensor",
}
flow = VersatileThermostatBaseConfigFlow(config)
assert flow._infos[CONF_USE_WINDOW_FEATURE] is True
assert flow._infos[CONF_USE_POWER_FEATURE] is True
assert flow._infos[CONF_USE_PRESENCE_FEATURE] is True
assert flow._infos[CONF_USE_MOTION_FEATURE] is True
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_bug_524(hass: HomeAssistant, skip_hass_states_is_state):
"""Test when switching from Cool to Heat the new temperature in Heat mode should be used"""
vtherm_api: VersatileThermostatAPI = VersatileThermostatAPI.get_vtherm_api(hass)
# The temperatures to set
temps = {
"frost": 7.0,
"eco": 17.0,
"comfort": 19.0,
"boost": 21.0,
"eco_ac": 27.0,
"comfort_ac": 25.0,
"boost_ac": 23.0,
"frost_away": 7.1,
"eco_away": 17.1,
"comfort_away": 19.1,
"boost_away": 21.1,
"eco_ac_away": 27.1,
"comfort_ac_away": 25.1,
"boost_ac_away": 23.1,
}
config_entry = MockConfigEntry(
domain=DOMAIN,
title="TheOverClimateMockName",
unique_id="overClimateUniqueId",
data={
CONF_NAME: "overClimate",
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_CLIMATE,
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
CONF_CYCLE_MIN: 5,
CONF_TEMP_MIN: 15,
CONF_TEMP_MAX: 30,
CONF_USE_WINDOW_FEATURE: False,
CONF_USE_MOTION_FEATURE: False,
CONF_USE_POWER_FEATURE: False,
CONF_USE_PRESENCE_FEATURE: True,
CONF_PRESENCE_SENSOR: "binary_sensor.presence_sensor",
CONF_CLIMATE: "climate.mock_climate",
CONF_MINIMAL_ACTIVATION_DELAY: 30,
CONF_SECURITY_DELAY_MIN: 5,
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
CONF_AUTO_FAN_MODE: CONF_AUTO_FAN_TURBO,
CONF_AC_MODE: True,
},
# | temps,
)
fake_underlying_climate = MockClimate(
hass=hass,
unique_id="mock_climate",
name="mock_climate",
hvac_modes=[HVACMode.OFF, HVACMode.COOL, HVACMode.HEAT, HVACMode.FAN_ONLY],
)
with patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
return_value=fake_underlying_climate,
):
vtherm: ThermostatOverClimate = await create_thermostat(
hass, config_entry, "climate.overclimate"
)
assert vtherm is not None
# We search for NumberEntities
for preset_name, value in temps.items():
await set_climate_preset_temp(vtherm, preset_name, value)
temp_entity: NumberEntity = search_entity(
hass,
"number.overclimate_preset_" + preset_name + PRESET_TEMP_SUFFIX,
NUMBER_DOMAIN,
)
assert temp_entity
# Because set_value is not implemented in Number class (really don't understand why...)
assert temp_entity.state == value
# 1. Set mode to Heat and preset to Comfort
await send_presence_change_event(vtherm, True, False, datetime.now())
await vtherm.async_set_hvac_mode(HVACMode.HEAT)
await vtherm.async_set_preset_mode(PRESET_COMFORT)
await hass.async_block_till_done()
assert vtherm.target_temperature == 19.0
# 2. Only change the HVAC_MODE (and keep preset to comfort)
await vtherm.async_set_hvac_mode(HVACMode.COOL)
await hass.async_block_till_done()
assert vtherm.target_temperature == 25.0
# 3. Only change the HVAC_MODE (and keep preset to comfort)
await vtherm.async_set_hvac_mode(HVACMode.HEAT)
await hass.async_block_till_done()
assert vtherm.target_temperature == 19.0
# 4. Change presence to off
await send_presence_change_event(vtherm, False, True, datetime.now())
await hass.async_block_till_done()
assert vtherm.target_temperature == 19.1
# 5. Change hvac_mode to AC
await vtherm.async_set_hvac_mode(HVACMode.COOL)
await hass.async_block_till_done()
assert vtherm.target_temperature == 25.1
# 6. Change presence to on
await send_presence_change_event(vtherm, True, False, datetime.now())
await hass.async_block_till_done()
assert vtherm.target_temperature == 25
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_bug_533(hass: HomeAssistant, skip_hass_states_is_state):
"""Test that with an over_valve and _auto_regulation_dpercent is set that the valve could close totally"""
vtherm_api: VersatileThermostatAPI = VersatileThermostatAPI.get_vtherm_api(hass)
# The temperatures to set
temps = {
"frost": 7.0,
"eco": 17.0,
"comfort": 19.0,
"boost": 21.0,
}
config_entry = MockConfigEntry(
domain=DOMAIN,
title="TheOverValveMockName",
unique_id="overValveUniqueId",
data={
CONF_NAME: "overValve",
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_VALVE,
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
CONF_TPI_COEF_INT: 0.5,
CONF_TPI_COEF_EXT: 0,
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_VALVE: "number.mock_valve",
CONF_AUTO_REGULATION_DTEMP: 10, # This parameter makes the bug
CONF_MINIMAL_ACTIVATION_DELAY: 30,
CONF_SECURITY_DELAY_MIN: 60,
},
# | temps,
)
# Not used because number is not registred so we can use directly the underlying number
# fake_underlying_number = MockNumber(
# hass=hass, unique_id="mock_number", name="mock_number"
# )
vtherm: ThermostatOverClimate = await create_thermostat(
hass, config_entry, "climate.overvalve"
)
assert vtherm is not None
tz = get_tz(hass) # pylint: disable=invalid-name
now: datetime = datetime.now(tz=tz)
# Set all temps and check they are correctly initialized
await set_all_climate_preset_temp(hass, vtherm, temps, "overvalve")
await send_temperature_change_event(vtherm, 15, now)
await send_ext_temperature_change_event(vtherm, 15, now)
# 1. Set mode to Heat and preset to Comfort
await vtherm.async_set_hvac_mode(HVACMode.HEAT)
with patch(
"homeassistant.core.StateMachine.get",
return_value=State(
entity_id="number.mock_valve",
state="100",
attributes={"min": 0, "max": 100},
),
), patch("homeassistant.core.ServiceRegistry.async_call") as mock_service_call:
await vtherm.async_set_preset_mode(PRESET_COMFORT)
await hass.async_block_till_done()
assert vtherm.target_temperature == 19.0
assert mock_service_call.call_count == 1
mock_service_call.assert_has_calls(
[
call.async_call(
domain="number",
service="set_value",
service_data={"value": 100},
target={"entity_id": "number.mock_valve"},
),
]
)
# 2. set current temperature to 18 -> still 50% open, so there is a call
now = now + timedelta(minutes=1)
with patch(
"homeassistant.core.StateMachine.get",
return_value=State(
entity_id="number.mock_valve",
state="100",
attributes={"min": 0, "max": 100},
),
), patch("homeassistant.core.ServiceRegistry.async_call") as mock_service_call:
await send_temperature_change_event(vtherm, 18, now)
await hass.async_block_till_done()
assert mock_service_call.call_count == 1
mock_service_call.assert_has_calls(
[
call.async_call(
domain="number",
service="set_value",
service_data={"value": 50},
target={"entity_id": "number.mock_valve"},
),
]
)
# 3. set current temperature to 18.8 -> still 10% open, so there is one call
now = now + timedelta(minutes=1)
with patch(
"homeassistant.core.StateMachine.get",
return_value=State(
entity_id="number.mock_valve",
state="50",
attributes={"min": 0, "max": 100},
),
), patch("homeassistant.core.ServiceRegistry.async_call") as mock_service_call:
await send_temperature_change_event(vtherm, 18.8, now)
await hass.async_block_till_done()
assert mock_service_call.call_count == 1
mock_service_call.assert_has_calls(
[
call.async_call(
domain="number",
service="set_value",
service_data={"value": 10},
target={"entity_id": "number.mock_valve"},
),
]
)
# 4. set current temperature to 19 -> should have 0% open and one call to set the 0
now = now + timedelta(minutes=1)
with patch(
"homeassistant.core.StateMachine.get",
return_value=State(
entity_id="number.mock_valve",
state="10", # the previous value
attributes={"min": 0, "max": 100},
),
), patch("homeassistant.core.ServiceRegistry.async_call") as mock_service_call:
await send_temperature_change_event(vtherm, 19, now)
await hass.async_block_till_done()
assert mock_service_call.call_count == 1
mock_service_call.assert_has_calls(
[
call.async_call(
domain="number",
service="set_value",
service_data={"value": 0},
target={"entity_id": "number.mock_valve"},
),
]
)

View File

@@ -0,0 +1,844 @@
# pylint: disable=wildcard-import, unused-wildcard-import, protected-access, unused-argument, line-too-long
""" Test the central_configuration """
import asyncio
from datetime import datetime
from unittest.mock import patch, call
from homeassistant.core import HomeAssistant
from homeassistant.config_entries import ConfigEntryState
from pytest_homeassistant_custom_component.common import MockConfigEntry
from custom_components.versatile_thermostat.thermostat_switch import (
ThermostatOverSwitch,
)
from custom_components.versatile_thermostat.thermostat_valve import (
ThermostatOverValve,
)
from custom_components.versatile_thermostat.thermostat_climate import (
ThermostatOverClimate,
)
from custom_components.versatile_thermostat.vtherm_api import VersatileThermostatAPI
from custom_components.versatile_thermostat.binary_sensor import (
CentralBoilerBinarySensor,
)
from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import
from .const import * # pylint: disable=wildcard-import, unused-wildcard-import
# @pytest.mark.parametrize("expected_lingering_tasks", [True])
# @pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_add_a_central_config_with_boiler(
hass: HomeAssistant,
skip_hass_states_is_state,
):
"""Tests the clean_central_config_doubon of base_thermostat"""
central_config_entry = MockConfigEntry(
domain=DOMAIN,
title="TheCentralConfigMockName",
unique_id="centralConfigUniqueId",
data=FULL_CENTRAL_CONFIG_WITH_BOILER,
)
central_config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(central_config_entry.entry_id)
assert central_config_entry.state is ConfigEntryState.LOADED
entity: ThermostatOverClimate = search_entity(
hass, "climate.thecentralconfigmockname", "climate"
)
assert entity is None
assert count_entities(hass, "climate.thecentralconfigmockname", "climate") == 0
# Test that VTherm API find the CentralConfig
api = VersatileThermostatAPI.get_vtherm_api(hass)
central_configuration = api.find_central_configuration()
assert central_configuration is not None
# Test that VTherm API have any central boiler entities
assert api.nb_active_device_for_boiler_entity is not None
assert api.nb_active_device_for_boiler == 0
assert api.nb_active_device_for_boiler_threshold_entity is not None
assert api.nb_active_device_for_boiler_threshold == 1 # the default value is 1
async def test_update_central_boiler_state_simple(
hass: HomeAssistant,
# skip_hass_states_is_state,
init_central_config_with_boiler_fixture,
):
"""Test that the central boiler state behavoir"""
api = VersatileThermostatAPI.get_vtherm_api(hass)
switch1 = MockSwitch(hass, "switch1", "theSwitch1")
entry = MockConfigEntry(
domain=DOMAIN,
title="TheOverSwitchMockName",
unique_id="uniqueId",
data={
CONF_NAME: "TheOverSwitchMockName",
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_SWITCH,
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
CONF_CYCLE_MIN: 5,
CONF_TEMP_MIN: 8,
CONF_TEMP_MAX: 18,
"frost_temp": 10,
"eco_temp": 17,
"comfort_temp": 18,
"boost_temp": 21,
CONF_USE_WINDOW_FEATURE: False,
CONF_USE_MOTION_FEATURE: False,
CONF_USE_POWER_FEATURE: False,
CONF_USE_PRESENCE_FEATURE: False,
CONF_HEATER: switch1.entity_id,
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
CONF_INVERSE_SWITCH: False,
CONF_TPI_COEF_INT: 0.3,
CONF_TPI_COEF_EXT: 0.01,
CONF_MINIMAL_ACTIVATION_DELAY: 30,
CONF_SECURITY_DELAY_MIN: 5,
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
CONF_SECURITY_DEFAULT_ON_PERCENT: 0.1,
CONF_USE_MAIN_CENTRAL_CONFIG: True,
CONF_USE_TPI_CENTRAL_CONFIG: True,
CONF_USE_PRESETS_CENTRAL_CONFIG: True,
CONF_USE_ADVANCED_CENTRAL_CONFIG: True,
CONF_USED_BY_CENTRAL_BOILER: True,
},
)
entity: ThermostatOverSwitch = await create_thermostat(
hass, entry, "climate.theoverswitchmockname"
)
assert entity
assert entity.name == "TheOverSwitchMockName"
assert entity.is_over_switch
assert entity.underlying_entities[0].entity_id == "switch.switch1"
assert api.nb_active_device_for_boiler_threshold == 1
assert api.nb_active_device_for_boiler == 0
# Force the VTherm to heat
await entity.async_set_hvac_mode(HVACMode.HEAT)
await entity.async_set_preset_mode(PRESET_BOOST)
tz = get_tz(hass) # pylint: disable=invalid-name
now: datetime = datetime.now(tz=tz)
await send_temperature_change_event(entity, 10, now)
assert entity.hvac_mode == HVACMode.HEAT
boiler_binary_sensor: CentralBoilerBinarySensor = search_entity(
hass, "binary_sensor.central_boiler", "binary_sensor"
)
assert boiler_binary_sensor is not None
assert boiler_binary_sensor.state == STATE_OFF
# 1. start a heater
with patch(
"homeassistant.core.ServiceRegistry.async_call"
) as mock_service_call, patch(
"custom_components.versatile_thermostat.binary_sensor.send_vtherm_event"
) as mock_send_event:
await switch1.async_turn_on()
switch1.async_write_ha_state()
# Wait for state event propagation
await asyncio.sleep(1)
assert entity.hvac_action == HVACAction.HEATING
assert mock_service_call.call_count == 2
# Sometimes this test fails
mock_service_call.assert_has_calls(
[
call.service_call(
"switch",
"turn_on",
{"entity_id": "switch.switch1"},
),
call(
"switch",
"turn_on",
service_data={},
target={"entity_id": "switch.pompe_chaudiere"},
),
],
any_order=True,
)
assert mock_send_event.call_count >= 1
mock_send_event.assert_has_calls(
[
call.send_vtherm_event(
hass=hass,
event_type=EventType.CENTRAL_BOILER_EVENT,
entity=api.central_boiler_entity,
data={"central_boiler": True},
)
]
)
assert api.nb_active_device_for_boiler == 1
assert boiler_binary_sensor.state == STATE_ON
# 2. stop a heater
with patch(
"homeassistant.core.ServiceRegistry.async_call"
) as mock_service_call, patch(
"custom_components.versatile_thermostat.binary_sensor.send_vtherm_event"
) as mock_send_event:
await switch1.async_turn_off()
switch1.async_write_ha_state()
# Wait for state event propagation
await asyncio.sleep(0.1)
assert entity.hvac_action == HVACAction.IDLE
assert mock_service_call.call_count >= 1
mock_service_call.assert_has_calls(
[
call(
"switch",
"turn_off",
service_data={},
target={"entity_id": "switch.pompe_chaudiere"},
)
]
)
assert mock_send_event.call_count >= 1
mock_send_event.assert_has_calls(
[
call.send_vtherm_event(
hass=hass,
event_type=EventType.CENTRAL_BOILER_EVENT,
entity=api.central_boiler_entity,
data={"central_boiler": False},
)
]
)
assert api.nb_active_device_for_boiler == 0
assert boiler_binary_sensor.state == STATE_OFF
entity.remove_thermostat()
async def test_update_central_boiler_state_multiple(
hass: HomeAssistant,
# skip_hass_states_is_state,
init_central_config_with_boiler_fixture,
):
"""Test that the central boiler state behavoir"""
api = VersatileThermostatAPI.get_vtherm_api(hass)
switch1 = MockSwitch(hass, "switch1", "theSwitch1")
switch2 = MockSwitch(hass, "switch2", "theSwitch2")
switch3 = MockSwitch(hass, "switch3", "theSwitch3")
switch4 = MockSwitch(hass, "switch4", "theSwitch4")
entry = MockConfigEntry(
domain=DOMAIN,
title="TheOverSwitchMockName",
unique_id="uniqueId",
data={
CONF_NAME: "TheOverSwitchMockName",
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_SWITCH,
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
CONF_CYCLE_MIN: 5,
CONF_TEMP_MIN: 8,
CONF_TEMP_MAX: 18,
"frost_temp": 10,
"eco_temp": 17,
"comfort_temp": 18,
"boost_temp": 21,
CONF_USE_WINDOW_FEATURE: False,
CONF_USE_MOTION_FEATURE: False,
CONF_USE_POWER_FEATURE: False,
CONF_USE_PRESENCE_FEATURE: False,
CONF_HEATER: switch1.entity_id,
CONF_HEATER_2: switch2.entity_id,
CONF_HEATER_3: switch3.entity_id,
CONF_HEATER_4: switch4.entity_id,
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
CONF_INVERSE_SWITCH: False,
CONF_TPI_COEF_INT: 0.3,
CONF_TPI_COEF_EXT: 0.01,
CONF_MINIMAL_ACTIVATION_DELAY: 30,
CONF_SECURITY_DELAY_MIN: 5,
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
CONF_SECURITY_DEFAULT_ON_PERCENT: 0.1,
CONF_USE_MAIN_CENTRAL_CONFIG: True,
CONF_USE_TPI_CENTRAL_CONFIG: True,
CONF_USE_PRESETS_CENTRAL_CONFIG: True,
CONF_USE_ADVANCED_CENTRAL_CONFIG: True,
CONF_USED_BY_CENTRAL_BOILER: True,
},
)
entity: ThermostatOverSwitch = await create_thermostat(
hass, entry, "climate.theoverswitchmockname"
)
assert entity
assert entity.name == "TheOverSwitchMockName"
assert entity.is_over_switch
assert entity.underlying_entities[0].entity_id == "switch.switch1"
assert entity.underlying_entities[1].entity_id == "switch.switch2"
assert entity.underlying_entities[2].entity_id == "switch.switch3"
assert entity.underlying_entities[3].entity_id == "switch.switch4"
assert api.nb_active_device_for_boiler_threshold == 1
assert api.nb_active_device_for_boiler == 0
# Force the VTherm to heat
await entity.async_set_hvac_mode(HVACMode.HEAT)
await entity.async_set_preset_mode(PRESET_BOOST)
tz = get_tz(hass) # pylint: disable=invalid-name
now: datetime = datetime.now(tz=tz)
await send_temperature_change_event(entity, 10, now)
assert entity.hvac_mode == HVACMode.HEAT
# 0. set threshold to 3
api.nb_active_device_for_boiler_threshold_entity.set_native_value(3)
assert api.nb_active_device_for_boiler_threshold == 3
boiler_binary_sensor: CentralBoilerBinarySensor = search_entity(
hass, "binary_sensor.central_boiler", "binary_sensor"
)
assert boiler_binary_sensor is not None
assert boiler_binary_sensor.state == STATE_OFF
# 1. start a first heater
with patch(
"homeassistant.core.ServiceRegistry.async_call"
) as mock_service_call, patch(
"custom_components.versatile_thermostat.binary_sensor.send_vtherm_event"
) as mock_send_event:
await switch1.async_turn_on()
switch1.async_write_ha_state()
# Wait for state event propagation
await asyncio.sleep(0.1)
assert entity.hvac_action == HVACAction.HEATING
assert mock_service_call.call_count == 1
# No switch of the boiler
mock_service_call.assert_has_calls(
[
call.service_call(
"switch",
"turn_on",
{"entity_id": "switch.switch1"},
),
]
)
assert mock_send_event.call_count == 0
assert api.nb_active_device_for_boiler == 1
assert boiler_binary_sensor.state == STATE_OFF
# 2. start a 2nd heater
with patch(
"homeassistant.core.ServiceRegistry.async_call"
) as mock_service_call, patch(
"custom_components.versatile_thermostat.binary_sensor.send_vtherm_event"
) as mock_send_event:
await switch2.async_turn_on()
switch2.async_write_ha_state()
# Wait for state event propagation
await asyncio.sleep(0.1)
assert entity.hvac_action == HVACAction.HEATING
# Only the first heater is started by the algo
assert mock_service_call.call_count == 1
# No switch of the boiler. Caution: each time a underlying heater state change itself,
# the cycle restarts. So it is always the first heater that is started
mock_service_call.assert_has_calls(
[
call.service_call(
"switch",
"turn_on",
{"entity_id": "switch.switch1"},
),
]
)
assert mock_send_event.call_count == 0
assert api.nb_active_device_for_boiler == 2
assert boiler_binary_sensor.state == STATE_OFF
# 3. start a 3rd heater
with patch(
"homeassistant.core.ServiceRegistry.async_call"
) as mock_service_call, patch(
"custom_components.versatile_thermostat.binary_sensor.send_vtherm_event"
) as mock_send_event:
await switch3.async_turn_on()
switch3.async_write_ha_state()
# Wait for state event propagation
await asyncio.sleep(0.1)
assert entity.hvac_action == HVACAction.HEATING
# Only the first heater is started by the algo
assert mock_service_call.call_count == 2
# No switch of the boiler. Caution: each time a underlying heater state change itself,
# the cycle restarts. So it is always the first heater that is started
mock_service_call.assert_has_calls(
[
call.service_call(
"switch",
"turn_on",
service_data={},
target={"entity_id": "switch.pompe_chaudiere"},
),
call.service_call(
"switch",
"turn_on",
{"entity_id": "switch.switch1"},
),
],
any_order=True,
)
assert mock_send_event.call_count == 1
mock_send_event.assert_has_calls(
[
call.send_vtherm_event(
hass=hass,
event_type=EventType.CENTRAL_BOILER_EVENT,
entity=api.central_boiler_entity,
data={"central_boiler": True},
)
]
)
assert api.nb_active_device_for_boiler == 3
assert boiler_binary_sensor.state == STATE_ON
# 4. start a 4th heater
with patch(
"homeassistant.core.ServiceRegistry.async_call"
) as mock_service_call, patch(
"custom_components.versatile_thermostat.binary_sensor.send_vtherm_event"
) as mock_send_event:
await switch4.async_turn_on()
switch4.async_write_ha_state()
# Wait for state event propagation
await asyncio.sleep(0.1)
assert entity.hvac_action == HVACAction.HEATING
# Only the first heater is started by the algo
assert mock_service_call.call_count == 1
# No switch of the boiler. Caution: each time a underlying heater state change itself,
# the cycle restarts. So it is always the first heater that is started
mock_service_call.assert_has_calls(
[
call.service_call(
"switch",
"turn_on",
{"entity_id": "switch.switch1"},
),
]
)
assert mock_send_event.call_count == 0
assert api.nb_active_device_for_boiler == 4
assert boiler_binary_sensor.state == STATE_ON
# 5. stop a heater
with patch(
"homeassistant.core.ServiceRegistry.async_call"
) as mock_service_call, patch(
"custom_components.versatile_thermostat.binary_sensor.send_vtherm_event"
) as mock_send_event:
await switch1.async_turn_off()
switch1.async_write_ha_state()
# Wait for state event propagation
await asyncio.sleep(0.1)
assert entity.hvac_action == HVACAction.HEATING
assert mock_service_call.call_count == 0
assert mock_send_event.call_count == 0
assert api.nb_active_device_for_boiler == 3
assert boiler_binary_sensor.state == STATE_ON
# 6. stop a 2nd heater
with patch(
"homeassistant.core.ServiceRegistry.async_call"
) as mock_service_call, patch(
"custom_components.versatile_thermostat.binary_sensor.send_vtherm_event"
) as mock_send_event:
await switch4.async_turn_off()
switch4.async_write_ha_state()
# Wait for state event propagation
await asyncio.sleep(0.1)
assert entity.hvac_action == HVACAction.HEATING
assert mock_service_call.call_count >= 1
mock_service_call.assert_has_calls(
[
call(
"switch",
"turn_off",
service_data={},
target={"entity_id": "switch.pompe_chaudiere"},
)
]
)
assert mock_send_event.call_count >= 1
mock_send_event.assert_has_calls(
[
call.send_vtherm_event(
hass=hass,
event_type=EventType.CENTRAL_BOILER_EVENT,
entity=api.central_boiler_entity,
data={"central_boiler": False},
)
]
)
assert api.nb_active_device_for_boiler == 2
assert boiler_binary_sensor.state == STATE_OFF
entity.remove_thermostat()
async def test_update_central_boiler_state_simple_valve(
hass: HomeAssistant,
# skip_hass_states_is_state,
init_central_config_with_boiler_fixture,
):
"""Test that the central boiler state behavoir"""
api = VersatileThermostatAPI.get_vtherm_api(hass)
valve1 = MockNumber(hass, "valve1", "theValve1")
entry = MockConfigEntry(
domain=DOMAIN,
title="TheOverValveMockName",
unique_id="uniqueId",
data={
CONF_NAME: "TheOverValveMockName",
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_VALVE,
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
CONF_CYCLE_MIN: 5,
CONF_TEMP_MIN: 8,
CONF_TEMP_MAX: 18,
"frost_temp": 10,
"eco_temp": 17,
"comfort_temp": 18,
"boost_temp": 21,
CONF_USE_WINDOW_FEATURE: False,
CONF_USE_MOTION_FEATURE: False,
CONF_USE_POWER_FEATURE: False,
CONF_USE_PRESENCE_FEATURE: False,
CONF_VALVE: valve1.entity_id,
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
CONF_INVERSE_SWITCH: False,
CONF_TPI_COEF_INT: 0.3,
CONF_TPI_COEF_EXT: 0.01,
CONF_MINIMAL_ACTIVATION_DELAY: 30,
CONF_SECURITY_DELAY_MIN: 5,
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
CONF_SECURITY_DEFAULT_ON_PERCENT: 0.1,
CONF_USE_MAIN_CENTRAL_CONFIG: True,
CONF_USE_TPI_CENTRAL_CONFIG: True,
CONF_USE_PRESETS_CENTRAL_CONFIG: True,
CONF_USE_ADVANCED_CENTRAL_CONFIG: True,
CONF_USED_BY_CENTRAL_BOILER: True,
},
)
entity: ThermostatOverValve = await create_thermostat(
hass, entry, "climate.theovervalvemockname"
)
assert entity
assert entity.name == "TheOverValveMockName"
assert entity.is_over_valve
assert entity.underlying_entities[0].entity_id == "number.valve1"
assert api.nb_active_device_for_boiler_threshold == 1
assert api.nb_active_device_for_boiler == 0
# Force the VTherm to heat
await entity.async_set_hvac_mode(HVACMode.HEAT)
await entity.async_set_preset_mode(PRESET_BOOST)
tz = get_tz(hass) # pylint: disable=invalid-name
now: datetime = datetime.now(tz=tz)
assert entity.hvac_mode == HVACMode.HEAT
boiler_binary_sensor: CentralBoilerBinarySensor = search_entity(
hass, "binary_sensor.central_boiler", "binary_sensor"
)
assert boiler_binary_sensor is not None
assert boiler_binary_sensor.state == STATE_OFF
# 1. start a valve
with patch(
"homeassistant.core.ServiceRegistry.async_call"
) as mock_service_call, patch(
"custom_components.versatile_thermostat.binary_sensor.send_vtherm_event"
) as mock_send_event:
await send_temperature_change_event(entity, 10, now)
# we have to simulate the valve also else the test don't work
valve1.set_native_value(10)
valve1.async_write_ha_state()
# Wait for state event propagation
await asyncio.sleep(0.1)
assert entity.hvac_action == HVACAction.HEATING
assert mock_service_call.call_count >= 1
mock_service_call.assert_has_calls(
[
call.service_call(
"switch",
"turn_on",
service_data={},
target={"entity_id": "switch.pompe_chaudiere"},
),
]
)
assert mock_send_event.call_count >= 1
mock_send_event.assert_has_calls(
[
call.send_vtherm_event(
hass=hass,
event_type=EventType.CENTRAL_BOILER_EVENT,
entity=api.central_boiler_entity,
data={"central_boiler": True},
)
]
)
assert api.nb_active_device_for_boiler == 1
assert boiler_binary_sensor.state == STATE_ON
# 2. stop a heater
with patch(
"homeassistant.core.ServiceRegistry.async_call"
) as mock_service_call, patch(
"custom_components.versatile_thermostat.binary_sensor.send_vtherm_event"
) as mock_send_event:
await send_temperature_change_event(entity, 25, now)
# Change the valve value to 0
valve1.set_native_value(0)
valve1.async_write_ha_state()
# Wait for state event propagation
await asyncio.sleep(0.1)
assert entity.hvac_action == HVACAction.IDLE
assert mock_service_call.call_count >= 1
mock_service_call.assert_has_calls(
[
call(
"switch",
"turn_off",
service_data={},
target={"entity_id": "switch.pompe_chaudiere"},
)
]
)
assert mock_send_event.call_count >= 1
mock_send_event.assert_has_calls(
[
call.send_vtherm_event(
hass=hass,
event_type=EventType.CENTRAL_BOILER_EVENT,
entity=api.central_boiler_entity,
data={"central_boiler": False},
)
]
)
assert api.nb_active_device_for_boiler == 0
assert boiler_binary_sensor.state == STATE_OFF
entity.remove_thermostat()
async def test_update_central_boiler_state_simple_climate(
hass: HomeAssistant,
# skip_hass_states_is_state,
init_central_config_with_boiler_fixture,
):
"""Test that the central boiler state behavoir"""
api = VersatileThermostatAPI.get_vtherm_api(hass)
climate1 = MockClimate(hass, "climate1", "theClimate1")
entry = MockConfigEntry(
domain=DOMAIN,
title="TheOverClimateMockName",
unique_id="uniqueId",
data={
CONF_NAME: "TheOverClimateMockName",
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_CLIMATE,
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
CONF_CYCLE_MIN: 5,
CONF_TEMP_MIN: 8,
CONF_TEMP_MAX: 18,
"frost_temp": 10,
"eco_temp": 17,
"comfort_temp": 18,
"boost_temp": 21,
CONF_USE_WINDOW_FEATURE: False,
CONF_USE_MOTION_FEATURE: False,
CONF_USE_POWER_FEATURE: False,
CONF_USE_PRESENCE_FEATURE: False,
CONF_CLIMATE: climate1.entity_id,
CONF_MINIMAL_ACTIVATION_DELAY: 30,
CONF_SECURITY_DELAY_MIN: 5,
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
CONF_SECURITY_DEFAULT_ON_PERCENT: 0.1,
CONF_USE_MAIN_CENTRAL_CONFIG: True,
CONF_USE_PRESETS_CENTRAL_CONFIG: True,
CONF_USE_ADVANCED_CENTRAL_CONFIG: True,
CONF_USED_BY_CENTRAL_BOILER: True,
},
)
with patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
return_value=climate1,
):
entity: ThermostatOverClimate = await create_thermostat(
hass, entry, "climate.theoverclimatemockname"
)
assert entity
assert entity.name == "TheOverClimateMockName"
assert entity.is_over_climate
assert entity.underlying_entities[0].entity_id == "climate.climate1"
assert api.nb_active_device_for_boiler_threshold == 1
assert api.nb_active_device_for_boiler == 0
# Force the VTherm to heat
await entity.async_set_hvac_mode(HVACMode.HEAT)
await entity.async_set_preset_mode(PRESET_BOOST)
tz = get_tz(hass) # pylint: disable=invalid-name
now: datetime = datetime.now(tz=tz)
assert entity.hvac_mode == HVACMode.HEAT
boiler_binary_sensor: CentralBoilerBinarySensor = search_entity(
hass, "binary_sensor.central_boiler", "binary_sensor"
)
assert boiler_binary_sensor is not None
assert boiler_binary_sensor.state == STATE_OFF
# 1. start a climate
with patch(
"homeassistant.core.ServiceRegistry.async_call"
) as mock_service_call, patch(
"custom_components.versatile_thermostat.binary_sensor.send_vtherm_event"
) as mock_send_event:
await send_temperature_change_event(entity, 10, now)
# we have to simulate the climate also else the test don't work
climate1.set_hvac_mode(HVACMode.HEAT)
climate1.set_hvac_action(HVACAction.HEATING)
climate1.async_write_ha_state()
# Wait for state event propagation
await asyncio.sleep(0.5)
assert entity.hvac_action == HVACAction.HEATING
assert mock_service_call.call_count >= 1
mock_service_call.assert_has_calls(
[
call.service_call(
"switch",
"turn_on",
service_data={},
target={"entity_id": "switch.pompe_chaudiere"},
),
]
)
assert mock_send_event.call_count >= 1
mock_send_event.assert_has_calls(
[
call.send_vtherm_event(
hass=hass,
event_type=EventType.CENTRAL_BOILER_EVENT,
entity=api.central_boiler_entity,
data={"central_boiler": True},
)
]
)
assert api.nb_active_device_for_boiler == 1
assert boiler_binary_sensor.state == STATE_ON
# 2. stop a climate
with patch(
"homeassistant.core.ServiceRegistry.async_call"
) as mock_service_call, patch(
"custom_components.versatile_thermostat.binary_sensor.send_vtherm_event"
) as mock_send_event:
await send_temperature_change_event(entity, 25, now)
climate1.set_hvac_mode(HVACMode.HEAT)
climate1.set_hvac_action(HVACAction.IDLE)
climate1.async_write_ha_state()
# Wait for state event propagation
await asyncio.sleep(0.5)
assert entity.hvac_action == HVACAction.IDLE
assert mock_service_call.call_count >= 1
mock_service_call.assert_has_calls(
[
call(
"switch",
"turn_off",
service_data={},
target={"entity_id": "switch.pompe_chaudiere"},
)
]
)
assert mock_send_event.call_count >= 1
mock_send_event.assert_has_calls(
[
call.send_vtherm_event(
hass=hass,
event_type=EventType.CENTRAL_BOILER_EVENT,
entity=api.central_boiler_entity,
data={"central_boiler": False},
)
]
)
assert api.nb_active_device_for_boiler == 0
assert boiler_binary_sensor.state == STATE_OFF
entity.remove_thermostat()

View File

@@ -4,23 +4,13 @@
from unittest.mock import patch # , call from unittest.mock import patch # , call
# from datetime import datetime # , timedelta # from datetime import datetime # , timedelta
from homeassistant import data_entry_flow from homeassistant import data_entry_flow
from homeassistant.data_entry_flow import FlowResultType
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
# from homeassistant.components.climate import HVACAction, HVACMode from homeassistant.config_entries import SOURCE_USER
from homeassistant.config_entries import ConfigEntryState, SOURCE_USER
# 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 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 ( from custom_components.versatile_thermostat.thermostat_switch import (
ThermostatOverSwitch, ThermostatOverSwitch,
) )
@@ -76,16 +66,20 @@ async def test_add_a_central_config(hass: HomeAssistant, skip_hass_states_is_sta
CONF_SECURITY_DELAY_MIN: 61, CONF_SECURITY_DELAY_MIN: 61,
CONF_SECURITY_MIN_ON_PERCENT: 0.5, CONF_SECURITY_MIN_ON_PERCENT: 0.5,
CONF_SECURITY_DEFAULT_ON_PERCENT: 0.2, CONF_SECURITY_DEFAULT_ON_PERCENT: 0.2,
CONF_USE_CENTRAL_BOILER_FEATURE: False,
}, },
) )
central_config_entry.add_to_hass(hass) entity = await create_thermostat(
await hass.config_entries.async_setup(central_config_entry.entry_id) hass, central_config_entry, "climate.thecentralconfigmockname"
assert central_config_entry.state is ConfigEntryState.LOADED
entity: ThermostatOverClimate = search_entity(
hass, "climate.thecentralconfigmockname", "climate"
) )
# central_config_entry.add_to_hass(hass)
# await hass.config_entries.async_setup(central_config_entry.entry_id)
# assert central_config_entry.state is ConfigEntryState.LOADED
#
# entity: ThermostatOverClimate = search_entity(
# hass, "climate.thecentralconfigmockname", "climate"
# )
assert entity is None assert entity is None
@@ -96,8 +90,15 @@ async def test_add_a_central_config(hass: HomeAssistant, skip_hass_states_is_sta
central_configuration = api.find_central_configuration() central_configuration = api.find_central_configuration()
assert central_configuration is not None assert central_configuration is not None
# Test that VTherm API doesn't have any central boiler entities
assert api.nb_active_device_for_boiler_entity is None
assert api.nb_active_device_for_boiler is None
@pytest.mark.parametrize("expected_lingering_tasks", [True]) assert api.nb_active_device_for_boiler_threshold_entity is None
assert api.nb_active_device_for_boiler_threshold is None
# @pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True]) @pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_minimal_over_switch_wo_central_config( async def test_minimal_over_switch_wo_central_config(
hass: HomeAssistant, skip_hass_states_is_state, init_vtherm_api hass: HomeAssistant, skip_hass_states_is_state, init_vtherm_api
@@ -116,6 +117,7 @@ async def test_minimal_over_switch_wo_central_config(
CONF_CYCLE_MIN: 5, CONF_CYCLE_MIN: 5,
CONF_TEMP_MIN: 8, CONF_TEMP_MIN: 8,
CONF_TEMP_MAX: 18, CONF_TEMP_MAX: 18,
CONF_STEP_TEMPERATURE: 0.3,
"frost_temp": 10, "frost_temp": 10,
"eco_temp": 17, "eco_temp": 17,
"comfort_temp": 18, "comfort_temp": 18,
@@ -157,8 +159,9 @@ async def test_minimal_over_switch_wo_central_config(
assert entity._temp_sensor_entity_id == "sensor.mock_temp_sensor" assert entity._temp_sensor_entity_id == "sensor.mock_temp_sensor"
assert entity._ext_temp_sensor_entity_id == "sensor.mock_ext_temp_sensor" assert entity._ext_temp_sensor_entity_id == "sensor.mock_ext_temp_sensor"
assert entity._cycle_min == 5 assert entity._cycle_min == 5
assert entity._attr_min_temp == 8 assert entity.min_temp == 8
assert entity._attr_max_temp == 18 assert entity.max_temp == 18
assert entity.target_temperature_step == 0.3
assert entity.preset_modes == ["none", "frost", "eco", "comfort", "boost"] assert entity.preset_modes == ["none", "frost", "eco", "comfort", "boost"]
assert entity.is_window_auto_enabled is False assert entity.is_window_auto_enabled is False
assert entity.nb_underlying_entities == 1 assert entity.nb_underlying_entities == 1
@@ -172,6 +175,8 @@ async def test_minimal_over_switch_wo_central_config(
assert entity._security_default_on_percent == 0.1 assert entity._security_default_on_percent == 0.1
assert entity.is_inversed assert entity.is_inversed
entity.remove_thermostat()
@pytest.mark.parametrize("expected_lingering_tasks", [True]) @pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True]) @pytest.mark.parametrize("expected_lingering_timers", [True])
@@ -192,6 +197,7 @@ async def test_full_over_switch_wo_central_config(
CONF_CYCLE_MIN: 5, CONF_CYCLE_MIN: 5,
CONF_TEMP_MIN: 8, CONF_TEMP_MIN: 8,
CONF_TEMP_MAX: 18, CONF_TEMP_MAX: 18,
CONF_STEP_TEMPERATURE: 0.3,
"frost_temp": 10, "frost_temp": 10,
"eco_temp": 17, "eco_temp": 17,
"comfort_temp": 18, "comfort_temp": 18,
@@ -247,8 +253,9 @@ async def test_full_over_switch_wo_central_config(
assert entity._temp_sensor_entity_id == "sensor.mock_temp_sensor" assert entity._temp_sensor_entity_id == "sensor.mock_temp_sensor"
assert entity._ext_temp_sensor_entity_id == "sensor.mock_ext_temp_sensor" assert entity._ext_temp_sensor_entity_id == "sensor.mock_ext_temp_sensor"
assert entity._cycle_min == 5 assert entity._cycle_min == 5
assert entity._attr_min_temp == 8 assert entity.min_temp == 8
assert entity._attr_max_temp == 18 assert entity.max_temp == 18
assert entity.target_temperature_step == 0.3
assert entity.preset_modes == [ assert entity.preset_modes == [
"none", "none",
"frost", "frost",
@@ -268,7 +275,7 @@ async def test_full_over_switch_wo_central_config(
assert entity._security_default_on_percent == 0.1 assert entity._security_default_on_percent == 0.1
assert entity.is_inversed is False assert entity.is_inversed is False
assert entity.is_window_auto_enabled is True assert entity.is_window_auto_enabled is False # we have an entity_id
assert entity._window_sensor_entity_id == "binary_sensor.mock_window_sensor" assert entity._window_sensor_entity_id == "binary_sensor.mock_window_sensor"
assert entity._window_delay_sec == 30 assert entity._window_delay_sec == 30
assert entity._window_auto_close_threshold == 0.1 assert entity._window_auto_close_threshold == 0.1
@@ -286,6 +293,8 @@ async def test_full_over_switch_wo_central_config(
assert entity._presence_sensor_entity_id == "binary_sensor.mock_presence_sensor" assert entity._presence_sensor_entity_id == "binary_sensor.mock_presence_sensor"
entity.remove_thermostat()
@pytest.mark.parametrize("expected_lingering_tasks", [True]) @pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True]) @pytest.mark.parametrize("expected_lingering_timers", [True])
@@ -306,6 +315,7 @@ async def test_full_over_switch_with_central_config(
CONF_CYCLE_MIN: 5, CONF_CYCLE_MIN: 5,
CONF_TEMP_MIN: 8, CONF_TEMP_MIN: 8,
CONF_TEMP_MAX: 18, CONF_TEMP_MAX: 18,
CONF_STEP_TEMPERATURE: 0.3,
"frost_temp": 10, "frost_temp": 10,
"eco_temp": 17, "eco_temp": 17,
"comfort_temp": 18, "comfort_temp": 18,
@@ -357,8 +367,9 @@ async def test_full_over_switch_with_central_config(
assert entity._temp_sensor_entity_id == "sensor.mock_temp_sensor" assert entity._temp_sensor_entity_id == "sensor.mock_temp_sensor"
assert entity._ext_temp_sensor_entity_id == "sensor.mock_ext_temp_sensor" assert entity._ext_temp_sensor_entity_id == "sensor.mock_ext_temp_sensor"
assert entity._cycle_min == 5 assert entity._cycle_min == 5
assert entity._attr_min_temp == 15 assert entity.min_temp == 15
assert entity._attr_max_temp == 30 assert entity.max_temp == 30
assert entity.target_temperature_step == 0.1
assert entity.preset_modes == [ assert entity.preset_modes == [
"none", "none",
"frost", "frost",
@@ -377,7 +388,8 @@ async def test_full_over_switch_with_central_config(
assert entity._security_default_on_percent == 0.2 assert entity._security_default_on_percent == 0.2
assert entity.is_inversed is False assert entity.is_inversed is False
assert entity.is_window_auto_enabled is True # We have an entity so window auto is not enabled
assert entity.is_window_auto_enabled is False
assert entity._window_sensor_entity_id == "binary_sensor.mock_window_sensor" assert entity._window_sensor_entity_id == "binary_sensor.mock_window_sensor"
assert entity._window_delay_sec == 15 assert entity._window_delay_sec == 15
assert entity._window_auto_close_threshold == 1 assert entity._window_auto_close_threshold == 1
@@ -395,9 +407,11 @@ async def test_full_over_switch_with_central_config(
assert entity._presence_sensor_entity_id == "binary_sensor.mock_presence_sensor" assert entity._presence_sensor_entity_id == "binary_sensor.mock_presence_sensor"
entity.remove_thermostat()
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True]) # @pytest.mark.parametrize("expected_lingering_tasks", [True])
# @pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_over_switch_with_central_config_but_no_central_config( async def test_over_switch_with_central_config_but_no_central_config(
hass: HomeAssistant, skip_hass_states_get, init_vtherm_api hass: HomeAssistant, skip_hass_states_get, init_vtherm_api
): ):
@@ -416,7 +430,13 @@ async def test_over_switch_with_central_config_but_no_central_config(
}, },
) )
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["type"] == FlowResultType.MENU
assert result["step_id"] == "menu"
result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input={"next_step_id": "main"}
)
assert result["type"] == FlowResultType.FORM
assert result["step_id"] == "main" assert result["step_id"] == "main"
assert result["errors"] == {} assert result["errors"] == {}
@@ -427,15 +447,71 @@ async def test_over_switch_with_central_config_but_no_central_config(
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor", CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
CONF_CYCLE_MIN: 5, CONF_CYCLE_MIN: 5,
CONF_DEVICE_POWER: 1, CONF_DEVICE_POWER: 1,
CONF_USE_WINDOW_FEATURE: True,
CONF_USE_MOTION_FEATURE: False,
CONF_USE_POWER_FEATURE: False,
CONF_USE_PRESENCE_FEATURE: False,
CONF_USE_MAIN_CENTRAL_CONFIG: True, CONF_USE_MAIN_CENTRAL_CONFIG: True,
}, },
) )
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["type"] == FlowResultType.FORM
# in case of error we stays in main # in case of error we stays in main
assert result["step_id"] == "main" assert result["step_id"] == "main"
assert result["errors"] == {"use_main_central_config": "no_central_config"} assert result["errors"] == {"use_main_central_config": "no_central_config"}
async def test_migration_of_central_config(
hass: HomeAssistant,
skip_hass_states_is_state,
):
"""Tests the clean_central_config_doubon of base_thermostat"""
central_config_entry = MockConfigEntry(
version=CONFIG_VERSION,
# An old minor version
minor_version=1,
domain=DOMAIN,
title="TheCentralConfigMockName",
unique_id="centralConfigUniqueId",
data={
CONF_NAME: CENTRAL_CONFIG_NAME,
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_CENTRAL_CONFIG,
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
CONF_TEMP_MIN: 15,
CONF_TEMP_MAX: 30,
CONF_STEP_TEMPERATURE: 0.1,
CONF_TPI_COEF_INT: 0.5,
CONF_TPI_COEF_EXT: 0.02,
CONF_MINIMAL_ACTIVATION_DELAY: 11,
CONF_SECURITY_DELAY_MIN: 61,
CONF_SECURITY_MIN_ON_PERCENT: 0.5,
CONF_SECURITY_DEFAULT_ON_PERCENT: 0.2,
# The old central_boiler parameter
"add_central_boiler_control": True,
CONF_CENTRAL_BOILER_ACTIVATION_SRV: "switch.pompe_chaudiere/switch.turn_on",
CONF_CENTRAL_BOILER_DEACTIVATION_SRV: "switch.pompe_chaudiere/switch.turn_off",
},
)
central_config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(central_config_entry.entry_id)
assert central_config_entry.state is ConfigEntryState.LOADED
entity: ThermostatOverClimate = search_entity(
hass, "climate.thecentralconfigmockname", "climate"
)
assert entity is None
assert count_entities(hass, "climate.thecentralconfigmockname", "climate") == 0
# Test that VTherm API find the CentralConfig
api = VersatileThermostatAPI.get_vtherm_api(hass)
central_configuration = api.find_central_configuration()
assert central_configuration is not None
assert central_config_entry.data.get(CONF_USE_CENTRAL_BOILER_FEATURE) is True
# Test that VTherm API have any central boiler entities
# It should have been migrated and initialized
assert api.nb_active_device_for_boiler_entity is not None
assert api.nb_active_device_for_boiler == 0
assert api.nb_active_device_for_boiler_threshold_entity is not None
assert api.nb_active_device_for_boiler_threshold == 1 # the default value is 1

View File

@@ -1,9 +1,9 @@
# pylint: disable=wildcard-import, unused-wildcard-import, protected-access, unused-argument, line-too-long # pylint: disable=wildcard-import, unused-wildcard-import, protected-access, unused-argument, line-too-long, too-many-lines
""" Test the central_configuration """ """ Test the central_configuration """
from unittest.mock import patch # , call from unittest.mock import patch, call
# from datetime import datetime # , timedelta from datetime import datetime, timedelta
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
@@ -23,7 +23,7 @@ from .const import * # pylint: disable=wildcard-import, unused-wildcard-import
# @pytest.mark.parametrize("expected_lingering_tasks", [True]) # @pytest.mark.parametrize("expected_lingering_tasks", [True])
# @pytest.mark.parametrize("expected_lingering_timers", [True]) @pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_config_with_central_mode_true( async def test_config_with_central_mode_true(
hass: HomeAssistant, skip_hass_states_is_state hass: HomeAssistant, skip_hass_states_is_state
): ):
@@ -170,6 +170,8 @@ async def test_config_with_central_mode_none(
assert entity.last_central_mode is None # cause no central config exists assert entity.last_central_mode is None # cause no central config exists
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_switch_change_central_mode_true( async def test_switch_change_central_mode_true(
hass: HomeAssistant, skip_hass_states_is_state, init_central_config hass: HomeAssistant, skip_hass_states_is_state, init_central_config
): ):
@@ -310,6 +312,8 @@ async def test_switch_change_central_mode_true(
assert entity.preset_mode == PRESET_COMFORT assert entity.preset_mode == PRESET_COMFORT
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_switch_ac_change_central_mode_true( async def test_switch_ac_change_central_mode_true(
hass: HomeAssistant, skip_hass_states_is_state, init_central_config hass: HomeAssistant, skip_hass_states_is_state, init_central_config
): ):
@@ -444,6 +448,8 @@ async def test_switch_ac_change_central_mode_true(
assert entity.preset_mode == PRESET_COMFORT assert entity.preset_mode == PRESET_COMFORT
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_climate_ac_change_central_mode_false( async def test_climate_ac_change_central_mode_false(
hass: HomeAssistant, skip_hass_states_is_state, init_central_config hass: HomeAssistant, skip_hass_states_is_state, init_central_config
): ):
@@ -577,6 +583,8 @@ async def test_climate_ac_change_central_mode_false(
assert entity.preset_mode == PRESET_COMFORT assert entity.preset_mode == PRESET_COMFORT
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_climate_ac_only_change_central_mode_true( async def test_climate_ac_only_change_central_mode_true(
hass: HomeAssistant, skip_hass_states_is_state, init_central_config hass: HomeAssistant, skip_hass_states_is_state, init_central_config
): ):
@@ -732,3 +740,313 @@ async def test_climate_ac_only_change_central_mode_true(
# No change # No change
assert entity.hvac_mode == HVACMode.COOL assert entity.hvac_mode == HVACMode.COOL
assert entity.preset_mode == PRESET_ECO assert entity.preset_mode == PRESET_ECO
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_switch_change_central_mode_true_with_window(
hass: HomeAssistant, skip_hass_states_is_state, init_central_config
):
"""test that changes with over_switch config with central_mode True are
taken into account"""
# Add a Switch VTherm
entry = MockConfigEntry(
domain=DOMAIN,
title="TheOverSwitchMockName",
unique_id="uniqueId",
data={
CONF_NAME: "TheOverSwitchMockName",
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_SWITCH,
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
CONF_USE_CENTRAL_MODE: True,
CONF_CYCLE_MIN: 5,
CONF_TEMP_MIN: 8,
CONF_TEMP_MAX: 18,
"frost_temp": 10,
"eco_temp": 17,
"comfort_temp": 18,
"boost_temp": 21,
CONF_USE_WINDOW_FEATURE: True,
CONF_USE_MOTION_FEATURE: True,
CONF_USE_POWER_FEATURE: False,
CONF_USE_PRESENCE_FEATURE: False,
CONF_HEATER: "switch.mock_switch",
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
CONF_TPI_COEF_INT: 0.3,
CONF_TPI_COEF_EXT: 0.01,
CONF_MINIMAL_ACTIVATION_DELAY: 30,
CONF_SECURITY_DELAY_MIN: 5,
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
CONF_SECURITY_DEFAULT_ON_PERCENT: 0.1,
CONF_WINDOW_SENSOR: "binary_sensor.window_sensor",
CONF_WINDOW_DELAY: 0, # To be not obliged to wait
CONF_MOTION_SENSOR: "input_boolean.motion_sensor",
CONF_MOTION_DELAY: 10,
CONF_MOTION_OFF_DELAY: 30,
CONF_MOTION_PRESET: PRESET_BOOST,
CONF_NO_MOTION_PRESET: PRESET_ECO,
},
)
tz = get_tz(hass) # pylint: disable=invalid-name
event_timestamp = datetime.now(tz)
# 1 initialize entity and find select entity
with patch("homeassistant.core.ServiceRegistry.async_call"):
entity: ThermostatOverSwitch = await create_thermostat(
hass, entry, "climate.theoverswitchmockname"
)
assert entity
assert entity.is_controlled_by_central_mode
assert entity.last_central_mode is None
# Find the select entity
select_entity = search_entity(hass, "select.central_mode", SELECT_DOMAIN)
assert select_entity
assert select_entity.current_option == CENTRAL_MODE_AUTO
assert select_entity.options == CENTRAL_MODES
# start entity
await entity.async_set_hvac_mode(HVACMode.HEAT)
await entity.async_set_preset_mode(PRESET_ACTIVITY)
assert entity.hvac_mode == HVACMode.HEAT
assert entity.preset_mode == PRESET_ACTIVITY
assert entity.window_state is STATE_OFF
# 2 Open the window
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event, patch(
"homeassistant.helpers.condition.state", return_value=True
):
event_timestamp = event_timestamp + timedelta(minutes=1)
try_function = await send_window_change_event(
entity, True, False, event_timestamp
)
await try_function(None)
assert mock_send_event.call_count == 1
mock_send_event.assert_has_calls(
[call.send_event(EventType.HVAC_MODE_EVENT, {"hvac_mode": HVACMode.OFF})]
)
assert entity.hvac_mode == HVACMode.OFF
assert entity.preset_mode == PRESET_ACTIVITY
assert entity._saved_hvac_mode == HVACMode.HEAT
assert entity._saved_preset_mode == PRESET_ACTIVITY
assert entity.window_state is STATE_ON
# 3 Change central_mode to STOPPED
with patch("homeassistant.core.ServiceRegistry.async_call"):
event_timestamp = event_timestamp + timedelta(minutes=1)
entity._set_now(event_timestamp)
await select_entity.async_select_option(CENTRAL_MODE_STOPPED)
assert entity.last_central_mode is CENTRAL_MODE_STOPPED
# No change
assert entity.hvac_mode == HVACMode.OFF
assert entity.preset_mode == PRESET_ACTIVITY
assert entity._saved_hvac_mode == HVACMode.HEAT
assert entity._saved_preset_mode == PRESET_ACTIVITY
# 4 Close the window
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event, patch(
"homeassistant.helpers.condition.state", return_value=True
):
event_timestamp = event_timestamp + timedelta(minutes=1)
try_function = await send_window_change_event(
entity, False, True, event_timestamp
)
await try_function(None)
# No hvac_mode change cause the central mode is STOPPED
assert mock_send_event.call_count == 0
# We should stay off because central is STOPPED
assert entity.hvac_mode == HVACMode.OFF
assert entity.preset_mode == PRESET_ACTIVITY
assert entity._saved_hvac_mode == HVACMode.HEAT
assert entity._saved_preset_mode == PRESET_ACTIVITY
assert entity.window_state is STATE_OFF
# 5 Back to Auto
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event:
event_timestamp = event_timestamp + timedelta(minutes=1)
await select_entity.async_select_option(CENTRAL_MODE_AUTO)
assert mock_send_event.call_count == 1
mock_send_event.assert_has_calls(
[call.send_event(EventType.HVAC_MODE_EVENT, {"hvac_mode": HVACMode.HEAT})]
)
# We should switch back to HEAT
assert entity.hvac_mode == HVACMode.HEAT
assert entity.preset_mode == PRESET_ACTIVITY
assert entity._saved_hvac_mode == HVACMode.HEAT
assert entity._saved_preset_mode == PRESET_ACTIVITY
assert entity.window_state is STATE_OFF
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_switch_change_central_mode_true_with_cool_only_and_window(
hass: HomeAssistant, skip_hass_states_is_state, init_central_config
):
"""test that changes with over_switch config with central_mode True are
taken into account"""
# Add a Switch VTherm
entry = MockConfigEntry(
domain=DOMAIN,
title="TheOverSwitchMockName",
unique_id="uniqueId",
data={
CONF_NAME: "TheOverSwitchMockName",
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_SWITCH,
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
CONF_USE_CENTRAL_MODE: True,
CONF_CYCLE_MIN: 5,
CONF_TEMP_MIN: 8,
CONF_TEMP_MAX: 18,
"frost_temp": 10,
"eco_temp": 17,
"comfort_temp": 18,
"boost_temp": 21,
CONF_USE_WINDOW_FEATURE: True,
CONF_USE_MOTION_FEATURE: True,
CONF_USE_POWER_FEATURE: False,
CONF_USE_PRESENCE_FEATURE: False,
CONF_HEATER: "switch.mock_switch",
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
CONF_TPI_COEF_INT: 0.3,
CONF_TPI_COEF_EXT: 0.01,
CONF_MINIMAL_ACTIVATION_DELAY: 30,
CONF_SECURITY_DELAY_MIN: 5,
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
CONF_SECURITY_DEFAULT_ON_PERCENT: 0.1,
CONF_WINDOW_SENSOR: "binary_sensor.window_sensor",
CONF_WINDOW_DELAY: 0, # To be not obliged to wait
CONF_MOTION_SENSOR: "input_boolean.motion_sensor",
CONF_MOTION_DELAY: 10,
CONF_MOTION_OFF_DELAY: 30,
CONF_MOTION_PRESET: PRESET_BOOST,
CONF_NO_MOTION_PRESET: PRESET_ECO,
},
)
tz = get_tz(hass) # pylint: disable=invalid-name
event_timestamp = datetime.now(tz)
# 1 initialize entity and find select entity
with patch("homeassistant.core.ServiceRegistry.async_call"):
entity: ThermostatOverSwitch = await create_thermostat(
hass, entry, "climate.theoverswitchmockname"
)
assert entity
assert entity.is_controlled_by_central_mode
assert entity.last_central_mode is None
# Find the select entity
select_entity = search_entity(hass, "select.central_mode", SELECT_DOMAIN)
assert select_entity
assert select_entity.current_option == CENTRAL_MODE_AUTO
assert select_entity.options == CENTRAL_MODES
# start entity
await entity.async_set_hvac_mode(HVACMode.HEAT)
await entity.async_set_preset_mode(PRESET_ACTIVITY)
assert entity.hvac_mode == HVACMode.HEAT
assert entity.preset_mode == PRESET_ACTIVITY
assert entity.window_state is STATE_OFF
# 2 Change central_mode to COOL_ONLY
with patch("homeassistant.core.ServiceRegistry.async_call"):
event_timestamp = event_timestamp + timedelta(minutes=1)
entity._set_now(event_timestamp)
await select_entity.async_select_option(CENTRAL_MODE_COOL_ONLY)
assert entity.last_central_mode is CENTRAL_MODE_COOL_ONLY
await entity.async_set_hvac_mode(HVACMode.OFF)
await entity.async_set_preset_mode(PRESET_ACTIVITY)
assert entity._saved_hvac_mode == HVACMode.HEAT
assert entity._saved_preset_mode == PRESET_ACTIVITY
# 3 Open the window
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event, patch(
"homeassistant.helpers.condition.state", return_value=True
):
event_timestamp = event_timestamp + timedelta(minutes=1)
try_function = await send_window_change_event(
entity, True, False, event_timestamp
)
await try_function(None)
assert mock_send_event.call_count == 1
mock_send_event.assert_has_calls(
[call.send_event(EventType.HVAC_MODE_EVENT, {"hvac_mode": HVACMode.OFF})]
)
assert entity.hvac_mode == HVACMode.OFF
assert entity.preset_mode == PRESET_ACTIVITY
assert entity._saved_hvac_mode == HVACMode.HEAT
assert entity._saved_preset_mode == PRESET_ACTIVITY
assert entity.window_state is STATE_ON
# 4 Change central_mode to AUTO
with patch("homeassistant.core.ServiceRegistry.async_call"):
event_timestamp = event_timestamp + timedelta(minutes=1)
entity._set_now(event_timestamp)
await select_entity.async_select_option(CENTRAL_MODE_AUTO)
assert entity.last_central_mode is CENTRAL_MODE_AUTO
# No change
assert entity.hvac_mode == HVACMode.OFF
assert entity.preset_mode == PRESET_ACTIVITY
assert entity._saved_hvac_mode == HVACMode.HEAT
assert entity._saved_preset_mode == PRESET_ACTIVITY
# 5 Close the window
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event, patch(
"homeassistant.helpers.condition.state", return_value=True
):
event_timestamp = event_timestamp + timedelta(minutes=1)
try_function = await send_window_change_event(
entity, False, True, event_timestamp
)
await try_function(None)
# hvac_mode change to HEAT
assert mock_send_event.call_count == 1
mock_send_event.assert_has_calls(
[call.send_event(EventType.HVAC_MODE_EVENT, {"hvac_mode": HVACMode.HEAT})]
)
# We should stay off because central is STOPPED
assert entity.hvac_mode == HVACMode.HEAT
assert entity.preset_mode == PRESET_ACTIVITY
assert entity._saved_hvac_mode == HVACMode.HEAT
assert entity._saved_preset_mode == PRESET_ACTIVITY
assert entity.window_state is STATE_OFF

View File

@@ -2,6 +2,7 @@
""" Test the Versatile Thermostat config flow """ """ Test the Versatile Thermostat config flow """
from homeassistant import data_entry_flow from homeassistant import data_entry_flow
from homeassistant.data_entry_flow import FlowResultType
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.config_entries import SOURCE_USER, ConfigEntry from homeassistant.config_entries import SOURCE_USER, ConfigEntry
@@ -19,14 +20,14 @@ async def test_show_form(hass: HomeAssistant, init_vtherm_api) -> None:
DOMAIN, context={"source": SOURCE_USER} DOMAIN, context={"source": SOURCE_USER}
) )
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["type"] == FlowResultType.FORM
assert result["step_id"] == SOURCE_USER assert result["step_id"] == SOURCE_USER
@pytest.mark.parametrize("expected_lingering_tasks", [True]) @pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True]) @pytest.mark.parametrize("expected_lingering_timers", [True])
# Disable this test which don't work anymore (kill the pytest !) # Disable this test which don't work anymore (kill the pytest !)
@pytest.mark.skip # @pytest.mark.skip
async def test_user_config_flow_over_switch( async def test_user_config_flow_over_switch(
hass: HomeAssistant, skip_hass_states_get, init_central_config hass: HomeAssistant, skip_hass_states_get, init_central_config
): # pylint: disable=unused-argument ): # pylint: disable=unused-argument
@@ -34,49 +35,147 @@ async def test_user_config_flow_over_switch(
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER} DOMAIN, context={"source": SOURCE_USER}
) )
assert result["type"] == FlowResultType.FORM
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == SOURCE_USER assert result["step_id"] == SOURCE_USER
result = await hass.config_entries.flow.async_configure( result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input=MOCK_TH_OVER_SWITCH_USER_CONFIG result["flow_id"],
user_input={
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_SWITCH,
},
) )
assert result["type"] == FlowResultType.MENU
assert result["step_id"] == "menu"
assert result["menu_options"] == [
"main",
"features",
"type",
"presets",
"advanced",
"configuration_not_complete",
]
assert result.get("errors") is None
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input={"next_step_id": "main"}
)
assert result["type"] == FlowResultType.FORM
assert result["step_id"] == "main" assert result["step_id"] == "main"
assert result["errors"] == {} assert result.get("errors") == {}
result = await hass.config_entries.flow.async_configure( result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input=MOCK_TH_OVER_SWITCH_MAIN_CONFIG result["flow_id"],
user_input={
CONF_NAME: "TheOverSwitchMockName",
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
CONF_CYCLE_MIN: 5,
CONF_DEVICE_POWER: 1,
CONF_USE_MAIN_CENTRAL_CONFIG: True,
},
) )
assert result["type"] == FlowResultType.MENU
assert result["step_id"] == "menu"
assert result.get("errors") is None
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input={"next_step_id": "type"}
)
assert result["type"] == FlowResultType.FORM
assert result["step_id"] == "type" assert result["step_id"] == "type"
assert result["errors"] == {} assert result.get("errors") == {}
result = await hass.config_entries.flow.async_configure( result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input=MOCK_TH_OVER_SWITCH_TYPE_CONFIG result["flow_id"],
user_input={
CONF_HEATER: "switch.mock_switch",
CONF_HEATER_KEEP_ALIVE: 0,
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
CONF_AC_MODE: False,
CONF_INVERSE_SWITCH: False,
},
) )
assert result["type"] == FlowResultType.MENU
assert result["step_id"] == "menu"
assert result["menu_options"] == [
"main",
"features",
"type",
"tpi",
"presets",
"advanced",
"finalize", # because by default all options are "use central config"
]
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result.get("errors") is None
result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input={"next_step_id": "tpi"}
)
assert result["type"] == FlowResultType.FORM
assert result["step_id"] == "tpi" assert result["step_id"] == "tpi"
assert result["errors"] == {} assert result.get("errors") == {}
result = await hass.config_entries.flow.async_configure( result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input={CONF_USE_TPI_CENTRAL_CONFIG: True} result["flow_id"], user_input={CONF_USE_TPI_CENTRAL_CONFIG: True}
) )
assert result["type"] == FlowResultType.MENU
assert result["step_id"] == "menu"
assert result.get("errors") is None
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input={"next_step_id": "presets"}
)
assert result["type"] == FlowResultType.FORM
assert result["step_id"] == "presets" assert result["step_id"] == "presets"
assert result["errors"] == {} assert result.get("errors") == {}
result = await hass.config_entries.flow.async_configure( result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input={CONF_USE_PRESETS_CENTRAL_CONFIG: True} result["flow_id"], user_input={CONF_USE_PRESETS_CENTRAL_CONFIG: True}
) )
assert result["type"] == FlowResultType.MENU
assert result["step_id"] == "menu"
assert result.get("errors") is None
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input={"next_step_id": "features"}
)
assert result["type"] == FlowResultType.FORM
assert result["step_id"] == "features"
assert result.get("errors") == {}
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
user_input={
CONF_USE_MOTION_FEATURE: True,
CONF_USE_POWER_FEATURE: True,
CONF_USE_PRESENCE_FEATURE: True,
CONF_USE_WINDOW_FEATURE: True,
},
)
assert result["type"] == FlowResultType.MENU
assert result["step_id"] == "menu"
assert result.get("errors") is None
assert result["menu_options"] == [
"main",
"features",
"type",
"tpi",
"presets",
"window",
"motion",
"power",
"presence",
"advanced",
"configuration_not_complete",
# "finalize" : because for motion we need an motion sensor
]
result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input={"next_step_id": "window"}
)
assert result["type"] == FlowResultType.FORM
assert result["step_id"] == "window" assert result["step_id"] == "window"
assert result["errors"] == {} assert result.get("errors") == {}
result = await hass.config_entries.flow.async_configure( result = await hass.config_entries.flow.async_configure(
result["flow_id"], result["flow_id"],
@@ -85,10 +184,16 @@ async def test_user_config_flow_over_switch(
CONF_USE_WINDOW_CENTRAL_CONFIG: True, CONF_USE_WINDOW_CENTRAL_CONFIG: True,
}, },
) )
assert result["type"] == FlowResultType.MENU
assert result["step_id"] == "menu"
assert result.get("errors") is None
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input={"next_step_id": "motion"}
)
assert result["type"] == FlowResultType.FORM
assert result["step_id"] == "motion" assert result["step_id"] == "motion"
assert result["errors"] == {} assert result.get("errors") == {}
result = await hass.config_entries.flow.async_configure( result = await hass.config_entries.flow.async_configure(
result["flow_id"], result["flow_id"],
@@ -98,42 +203,74 @@ async def test_user_config_flow_over_switch(
}, },
) )
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input={"next_step_id": "power"}
)
assert result["type"] == FlowResultType.FORM
assert result["step_id"] == "power" assert result["step_id"] == "power"
assert result["errors"] == {} assert result.get("errors") == {}
result = await hass.config_entries.flow.async_configure( result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input={CONF_USE_POWER_CENTRAL_CONFIG: True} result["flow_id"], user_input={CONF_USE_POWER_CENTRAL_CONFIG: True}
) )
assert result["type"] == FlowResultType.MENU
assert result["step_id"] == "menu"
assert result["menu_options"] == [
"main",
"features",
"type",
"tpi",
"presets",
"window",
"motion",
"power",
"presence",
"advanced",
"finalize",
]
assert result.get("errors") is None
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input={"next_step_id": "presence"}
)
assert result["type"] == FlowResultType.FORM
assert result["step_id"] == "presence" assert result["step_id"] == "presence"
assert result["errors"] == {} assert result.get("errors") == {}
result = await hass.config_entries.flow.async_configure( result = await hass.config_entries.flow.async_configure(
result["flow_id"], result["flow_id"],
user_input={ user_input={
CONF_PRESENCE_SENSOR: "person.presence_sensor",
CONF_USE_PRESENCE_CENTRAL_CONFIG: True, CONF_USE_PRESENCE_CENTRAL_CONFIG: True,
}, },
) )
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["type"] == FlowResultType.MENU
assert result["step_id"] == "menu"
assert result.get("errors") is None
result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input={"next_step_id": "advanced"}
)
assert result["type"] == FlowResultType.FORM
assert result["step_id"] == "advanced" assert result["step_id"] == "advanced"
assert result["errors"] == {} assert result.get("errors") == {}
result = await hass.config_entries.flow.async_configure( result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input={CONF_USE_ADVANCED_CENTRAL_CONFIG: True} result["flow_id"], user_input={CONF_USE_ADVANCED_CENTRAL_CONFIG: True}
) )
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input={"next_step_id": "finalize"}
)
assert result["type"] == FlowResultType.CREATE_ENTRY
assert result.get("errors") is None
assert result["data"] == ( assert result["data"] == (
MOCK_TH_OVER_SWITCH_USER_CONFIG MOCK_TH_OVER_SWITCH_USER_CONFIG
| MOCK_TH_OVER_SWITCH_MAIN_CONFIG | MOCK_TH_OVER_SWITCH_MAIN_CONFIG
| MOCK_TH_OVER_SWITCH_TYPE_CONFIG | MOCK_TH_OVER_SWITCH_TYPE_CONFIG
| {CONF_WINDOW_SENSOR: "binary_sensor.window_sensor"} | {CONF_WINDOW_SENSOR: "binary_sensor.window_sensor"}
| {CONF_MOTION_SENSOR: "input_boolean.motion_sensor"} | {CONF_MOTION_SENSOR: "input_boolean.motion_sensor"}
| {CONF_PRESENCE_SENSOR: "person.presence_sensor"} # | {CONF_PRESENCE_SENSOR: "person.presence_sensor"} now in central config
| { | {
CONF_USE_MAIN_CENTRAL_CONFIG: True, CONF_USE_MAIN_CENTRAL_CONFIG: True,
CONF_USE_TPI_CENTRAL_CONFIG: True, CONF_USE_TPI_CENTRAL_CONFIG: True,
@@ -143,6 +280,13 @@ async def test_user_config_flow_over_switch(
CONF_USE_POWER_CENTRAL_CONFIG: True, CONF_USE_POWER_CENTRAL_CONFIG: True,
CONF_USE_PRESENCE_CENTRAL_CONFIG: True, CONF_USE_PRESENCE_CENTRAL_CONFIG: True,
CONF_USE_ADVANCED_CENTRAL_CONFIG: True, CONF_USE_ADVANCED_CENTRAL_CONFIG: True,
CONF_USE_CENTRAL_MODE: True,
CONF_USED_BY_CENTRAL_BOILER: False,
CONF_USE_WINDOW_FEATURE: True,
CONF_USE_MOTION_FEATURE: True,
CONF_USE_POWER_FEATURE: True,
CONF_USE_PRESENCE_FEATURE: True,
CONF_USE_CENTRAL_BOILER_FEATURE: False,
} }
) )
assert result["result"] assert result["result"]
@@ -154,91 +298,215 @@ async def test_user_config_flow_over_switch(
@pytest.mark.parametrize("expected_lingering_tasks", [True]) @pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True]) @pytest.mark.parametrize("expected_lingering_timers", [True])
# TODO this test fails when run in // but works alone
@pytest.mark.skip
async def test_user_config_flow_over_climate( async def test_user_config_flow_over_climate(
hass: HomeAssistant, skip_hass_states_get hass: HomeAssistant, skip_hass_states_get
): # pylint: disable=unused-argument ): # pylint: disable=unused-argument
"""Test the config flow with all thermostat_over_climate features and no additional features""" """Test the config flow with all thermostat_over_switch features and never use central config.
await create_central_config(hass) We don't use any features"""
# await create_central_config(hass)
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER} DOMAIN, context={"source": SOURCE_USER}
) )
assert result["type"] == FlowResultType.FORM
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == SOURCE_USER assert result["step_id"] == SOURCE_USER
result = await hass.config_entries.flow.async_configure( result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input=MOCK_TH_OVER_CLIMATE_USER_CONFIG result["flow_id"],
user_input={
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_CLIMATE,
},
) )
assert result["type"] == FlowResultType.MENU
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "menu"
assert result["step_id"] == "main" assert result["menu_options"] == [
assert result["errors"] == {} "main",
"features",
"type",
"presets",
"advanced",
"configuration_not_complete",
]
assert result.get("errors") is None
result = await hass.config_entries.flow.async_configure( result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input=MOCK_TH_OVER_CLIMATE_MAIN_CONFIG result["flow_id"], user_input={"next_step_id": "main"}
) )
assert result["type"] == FlowResultType.FORM
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "main" assert result["step_id"] == "main"
assert result["errors"] == {} assert result.get("errors") == {}
result = await hass.config_entries.flow.async_configure( result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input=MOCK_TH_OVER_CLIMATE_CENTRAL_MAIN_CONFIG result["flow_id"],
user_input={
CONF_NAME: "TheOverClimateMockName",
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
CONF_CYCLE_MIN: 5,
CONF_DEVICE_POWER: 1,
CONF_USE_MAIN_CENTRAL_CONFIG: False,
CONF_USE_CENTRAL_MODE: True,
# Keep default values which are False
},
) )
assert result["type"] == FlowResultType.FORM
assert result["step_id"] == "main"
assert result.get("errors") == {}
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM result = await hass.config_entries.flow.async_configure(
result["flow_id"],
user_input={
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
CONF_TEMP_MIN: 15,
CONF_TEMP_MAX: 30,
CONF_STEP_TEMPERATURE: 0.1,
# Keep default values which are False
},
)
assert result["type"] == FlowResultType.MENU
assert result["step_id"] == "menu"
assert result.get("errors") is None
result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input={"next_step_id": "type"}
)
assert result["type"] == FlowResultType.FORM
assert result["step_id"] == "type" assert result["step_id"] == "type"
assert result["errors"] == {} assert result.get("errors") == {}
result = await hass.config_entries.flow.async_configure( result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input=MOCK_TH_OVER_CLIMATE_TYPE_CONFIG result["flow_id"],
user_input={
CONF_CLIMATE: "climate.mock_climate",
CONF_AC_MODE: False,
CONF_AUTO_REGULATION_MODE: CONF_AUTO_REGULATION_STRONG,
CONF_AUTO_REGULATION_DTEMP: 0.5,
CONF_AUTO_REGULATION_PERIOD_MIN: 2,
CONF_AUTO_FAN_MODE: CONF_AUTO_FAN_HIGH,
CONF_AUTO_REGULATION_USE_DEVICE_TEMP: False,
},
) )
assert result["type"] == FlowResultType.MENU
assert result["step_id"] == "menu"
assert result["menu_options"] == [
"main",
"features",
"type",
"presets",
"advanced",
"configuration_not_complete",
# "finalize", # because we need Advanced default parameters
]
assert result.get("errors") is None
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input={"next_step_id": "presets"}
)
assert result["type"] == FlowResultType.FORM
assert result["step_id"] == "presets" assert result["step_id"] == "presets"
assert result["errors"] == {} assert result.get("errors") == {}
result = await hass.config_entries.flow.async_configure( result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input={CONF_USE_PRESETS_CENTRAL_CONFIG: False} result["flow_id"], user_input={CONF_USE_PRESETS_CENTRAL_CONFIG: False}
) )
assert result["type"] == FlowResultType.MENU
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "menu"
assert result["step_id"] == "presets" assert result.get("errors") is None
assert result["errors"] == {}
result = await hass.config_entries.flow.async_configure( result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input=MOCK_PRESETS_CONFIG result["flow_id"], user_input={"next_step_id": "features"}
) )
assert result["type"] == FlowResultType.FORM
assert result["step_id"] == "features"
assert result.get("errors") == {}
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM result = await hass.config_entries.flow.async_configure(
result["flow_id"],
user_input={
CONF_USE_MOTION_FEATURE: False,
CONF_USE_POWER_FEATURE: False,
CONF_USE_PRESENCE_FEATURE: False,
CONF_USE_WINDOW_FEATURE: False,
},
)
assert result["type"] == FlowResultType.MENU
assert result["step_id"] == "menu"
assert result.get("errors") is None
assert result["menu_options"] == [
"main",
"features",
"type",
"presets",
"advanced",
"configuration_not_complete",
# "finalize", finalize is not present waiting for advanced configuration
]
result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input={"next_step_id": "advanced"}
)
assert result["type"] == FlowResultType.FORM
assert result["step_id"] == "advanced" assert result["step_id"] == "advanced"
assert result["errors"] == {} assert result.get("errors") == {}
result = await hass.config_entries.flow.async_configure( result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input={CONF_USE_ADVANCED_CENTRAL_CONFIG: False} result["flow_id"],
user_input={CONF_USE_ADVANCED_CENTRAL_CONFIG: False},
) )
assert result["type"] == FlowResultType.FORM
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "advanced" assert result["step_id"] == "advanced"
assert result["errors"] == {} assert result.get("errors") == {}
result = await hass.config_entries.flow.async_configure( result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input=MOCK_ADVANCED_CONFIG result["flow_id"],
user_input={
CONF_MINIMAL_ACTIVATION_DELAY: 10,
CONF_SECURITY_DELAY_MIN: 5,
CONF_SECURITY_MIN_ON_PERCENT: 0.4,
CONF_SECURITY_DEFAULT_ON_PERCENT: 0.3,
},
) )
assert result["type"] == FlowResultType.MENU
assert result["step_id"] == "menu"
assert result.get("errors") is None
assert result["menu_options"] == [
"main",
"features",
"type",
"presets",
"advanced",
"finalize", # Now finalize is present
]
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input={"next_step_id": "finalize"}
)
assert result["type"] == FlowResultType.CREATE_ENTRY
assert result.get("errors") is None
assert result[ assert result[
"data" "data"
] == MOCK_TH_OVER_CLIMATE_USER_CONFIG | MOCK_TH_OVER_CLIMATE_MAIN_CONFIG | MOCK_TH_OVER_CLIMATE_CENTRAL_MAIN_CONFIG | MOCK_TH_OVER_CLIMATE_TYPE_CONFIG | MOCK_PRESETS_CONFIG | MOCK_ADVANCED_CONFIG | MOCK_DEFAULT_FEATURE_CONFIG | { ] == MOCK_TH_OVER_CLIMATE_USER_CONFIG | MOCK_TH_OVER_CLIMATE_MAIN_CONFIG | MOCK_TH_OVER_CLIMATE_CENTRAL_MAIN_CONFIG | MOCK_TH_OVER_CLIMATE_TYPE_CONFIG | {
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_MAIN_CENTRAL_CONFIG: False, CONF_USE_MAIN_CENTRAL_CONFIG: False,
CONF_USE_TPI_CENTRAL_CONFIG: False, CONF_USE_TPI_CENTRAL_CONFIG: False,
CONF_USE_PRESETS_CENTRAL_CONFIG: False, CONF_USE_PRESETS_CENTRAL_CONFIG: False,
CONF_USE_MOTION_FEATURE: False,
CONF_USE_POWER_FEATURE: False,
CONF_USE_PRESENCE_FEATURE: False,
CONF_USE_WINDOW_FEATURE: False,
CONF_USE_CENTRAL_BOILER_FEATURE: False,
CONF_USE_TPI_CENTRAL_CONFIG: False,
CONF_USE_WINDOW_CENTRAL_CONFIG: False, CONF_USE_WINDOW_CENTRAL_CONFIG: False,
CONF_USE_MOTION_CENTRAL_CONFIG: False, CONF_USE_MOTION_CENTRAL_CONFIG: False,
CONF_USE_POWER_CENTRAL_CONFIG: False, CONF_USE_POWER_CENTRAL_CONFIG: False,
CONF_USE_PRESENCE_CENTRAL_CONFIG: False, CONF_USE_PRESENCE_CENTRAL_CONFIG: False,
CONF_USE_ADVANCED_CENTRAL_CONFIG: False, CONF_USE_ADVANCED_CENTRAL_CONFIG: False,
CONF_USED_BY_CENTRAL_BOILER: False,
} }
assert result["result"] assert result["result"]
assert result["result"].domain == DOMAIN assert result["result"].domain == DOMAIN
@@ -249,6 +517,8 @@ async def test_user_config_flow_over_climate(
@pytest.mark.parametrize("expected_lingering_tasks", [True]) @pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True]) @pytest.mark.parametrize("expected_lingering_timers", [True])
# TODO reimplement this
@pytest.mark.skip
async def test_user_config_flow_window_auto_ok( async def test_user_config_flow_window_auto_ok(
hass: HomeAssistant, hass: HomeAssistant,
skip_hass_states_get, skip_hass_states_get,
@@ -261,7 +531,7 @@ async def test_user_config_flow_window_auto_ok(
DOMAIN, context={"source": SOURCE_USER} DOMAIN, context={"source": SOURCE_USER}
) )
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["type"] == FlowResultType.MENU
assert result["step_id"] == SOURCE_USER assert result["step_id"] == SOURCE_USER
result = await hass.config_entries.flow.async_configure( result = await hass.config_entries.flow.async_configure(
@@ -271,9 +541,9 @@ async def test_user_config_flow_window_auto_ok(
}, },
) )
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["type"] == FlowResultType.MENU
assert result["step_id"] == "main" assert result["step_id"] == "main"
assert result["errors"] == {} assert result.get("errors") is None
result = await hass.config_entries.flow.async_configure( result = await hass.config_entries.flow.async_configure(
result["flow_id"], result["flow_id"],
@@ -287,62 +557,63 @@ async def test_user_config_flow_window_auto_ok(
CONF_USE_POWER_FEATURE: False, CONF_USE_POWER_FEATURE: False,
CONF_USE_PRESENCE_FEATURE: False, CONF_USE_PRESENCE_FEATURE: False,
CONF_USE_MAIN_CENTRAL_CONFIG: True, CONF_USE_MAIN_CENTRAL_CONFIG: True,
CONF_USED_BY_CENTRAL_BOILER: True,
}, },
) )
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["type"] == FlowResultType.MENU
assert result["step_id"] == "type" assert result["step_id"] == "type"
assert result["errors"] == {} assert result.get("errors") is None
result = await hass.config_entries.flow.async_configure( result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input=MOCK_TH_OVER_SWITCH_TYPE_CONFIG result["flow_id"], user_input=MOCK_TH_OVER_SWITCH_TYPE_CONFIG
) )
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["type"] == FlowResultType.MENU
assert result["step_id"] == "tpi" assert result["step_id"] == "tpi"
assert result["errors"] == {} assert result.get("errors") is None
result = await hass.config_entries.flow.async_configure( result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input={CONF_USE_TPI_CENTRAL_CONFIG: False} result["flow_id"], user_input={CONF_USE_TPI_CENTRAL_CONFIG: False}
) )
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["type"] == FlowResultType.MENU
assert result["step_id"] == "tpi" assert result["step_id"] == "tpi"
assert result["errors"] == {} assert result.get("errors") is None
result = await hass.config_entries.flow.async_configure( result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input=MOCK_TH_OVER_SWITCH_TPI_CONFIG result["flow_id"], user_input=MOCK_TH_OVER_SWITCH_TPI_CONFIG
) )
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["type"] == FlowResultType.MENU
assert result["step_id"] == "presets" assert result["step_id"] == "presets"
assert result["errors"] == {} assert result.get("errors") is None
result = await hass.config_entries.flow.async_configure( result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input={CONF_USE_PRESETS_CENTRAL_CONFIG: True} result["flow_id"], user_input={CONF_USE_PRESETS_CENTRAL_CONFIG: True}
) )
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["type"] == FlowResultType.MENU
assert result["step_id"] == "window" assert result["step_id"] == "window"
assert result["errors"] == {} assert result.get("errors") is None
result = await hass.config_entries.flow.async_configure( result = await hass.config_entries.flow.async_configure(
result["flow_id"], result["flow_id"],
user_input={CONF_USE_WINDOW_CENTRAL_CONFIG: False}, user_input={CONF_USE_WINDOW_CENTRAL_CONFIG: False},
) )
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["type"] == FlowResultType.MENU
assert result["step_id"] == "window" assert result["step_id"] == "window"
assert result["errors"] == {} assert result.get("errors") is None
result = await hass.config_entries.flow.async_configure( result = await hass.config_entries.flow.async_configure(
result["flow_id"], result["flow_id"],
user_input=MOCK_WINDOW_AUTO_CONFIG, user_input=MOCK_WINDOW_AUTO_CONFIG,
) )
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["type"] == FlowResultType.MENU
assert result["step_id"] == "advanced" assert result["step_id"] == "advanced"
assert result["errors"] == {} assert result.get("errors") is None
result = await hass.config_entries.flow.async_configure( result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input={CONF_USE_ADVANCED_CENTRAL_CONFIG: True} result["flow_id"], user_input={CONF_USE_ADVANCED_CENTRAL_CONFIG: True}
@@ -373,6 +644,7 @@ async def test_user_config_flow_window_auto_ok(
CONF_USE_POWER_CENTRAL_CONFIG: False, CONF_USE_POWER_CENTRAL_CONFIG: False,
CONF_USE_PRESENCE_CENTRAL_CONFIG: False, CONF_USE_PRESENCE_CENTRAL_CONFIG: False,
CONF_USE_ADVANCED_CENTRAL_CONFIG: True, CONF_USE_ADVANCED_CENTRAL_CONFIG: True,
CONF_USED_BY_CENTRAL_BOILER: True,
} }
assert result["result"] assert result["result"]
assert result["result"].domain == DOMAIN assert result["result"].domain == DOMAIN
@@ -383,6 +655,8 @@ async def test_user_config_flow_window_auto_ok(
@pytest.mark.parametrize("expected_lingering_tasks", [True]) @pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True]) @pytest.mark.parametrize("expected_lingering_timers", [True])
# TODO reimplement this
@pytest.mark.skip
async def test_user_config_flow_window_auto_ko( async def test_user_config_flow_window_auto_ko(
hass: HomeAssistant, skip_hass_states_get # pylint: disable=unused-argument hass: HomeAssistant, skip_hass_states_get # pylint: disable=unused-argument
): ):
@@ -394,7 +668,7 @@ async def test_user_config_flow_window_auto_ko(
DOMAIN, context={"source": SOURCE_USER} DOMAIN, context={"source": SOURCE_USER}
) )
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["type"] == FlowResultType.MENU
assert result["step_id"] == SOURCE_USER assert result["step_id"] == SOURCE_USER
result = await hass.config_entries.flow.async_configure( result = await hass.config_entries.flow.async_configure(
@@ -404,9 +678,9 @@ async def test_user_config_flow_window_auto_ko(
}, },
) )
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["type"] == FlowResultType.MENU
assert result["step_id"] == "main" assert result["step_id"] == "main"
assert result["errors"] == {} assert result.get("errors") is None
result = await hass.config_entries.flow.async_configure( result = await hass.config_entries.flow.async_configure(
result["flow_id"], result["flow_id"],
@@ -423,41 +697,41 @@ async def test_user_config_flow_window_auto_ko(
}, },
) )
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["type"] == FlowResultType.MENU
assert result["step_id"] == "type" assert result["step_id"] == "type"
assert result["errors"] == {} assert result.get("errors") is None
result = await hass.config_entries.flow.async_configure( result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input=MOCK_TH_OVER_SWITCH_TYPE_CONFIG result["flow_id"], user_input=MOCK_TH_OVER_SWITCH_TYPE_CONFIG
) )
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["type"] == FlowResultType.MENU
assert result["step_id"] == "tpi" assert result["step_id"] == "tpi"
assert result["errors"] == {} assert result.get("errors") is None
result = await hass.config_entries.flow.async_configure( result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input={CONF_USE_TPI_CENTRAL_CONFIG: False} result["flow_id"], user_input={CONF_USE_TPI_CENTRAL_CONFIG: False}
) )
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["type"] == FlowResultType.MENU
assert result["step_id"] == "tpi" assert result["step_id"] == "tpi"
assert result["errors"] == {} assert result.get("errors") is None
result = await hass.config_entries.flow.async_configure( result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input=MOCK_TH_OVER_SWITCH_TPI_CONFIG result["flow_id"], user_input=MOCK_TH_OVER_SWITCH_TPI_CONFIG
) )
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["type"] == FlowResultType.MENU
assert result["step_id"] == "presets" assert result["step_id"] == "presets"
assert result["errors"] == {} assert result.get("errors") is None
result = await hass.config_entries.flow.async_configure( result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input={CONF_USE_PRESETS_CENTRAL_CONFIG: True} result["flow_id"], user_input={CONF_USE_PRESETS_CENTRAL_CONFIG: True}
) )
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["type"] == FlowResultType.MENU
assert result["step_id"] == "window" assert result["step_id"] == "window"
assert result["errors"] == {} assert result.get("errors") is None
result = await hass.config_entries.flow.async_configure( result = await hass.config_entries.flow.async_configure(
result["flow_id"], result["flow_id"],
@@ -467,9 +741,9 @@ async def test_user_config_flow_window_auto_ko(
}, },
) )
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["type"] == FlowResultType.MENU
assert result["step_id"] == "window" assert result["step_id"] == "window"
assert result["errors"] == {} assert result.get("errors") is None
result = await hass.config_entries.flow.async_configure( result = await hass.config_entries.flow.async_configure(
result["flow_id"], result["flow_id"],
@@ -478,9 +752,9 @@ async def test_user_config_flow_window_auto_ko(
# Since issue #280 we cannot have the error because we only display the # Since issue #280 we cannot have the error because we only display the
# MOCK_WINDOW_DELAY_CONFIG form if we have a sensor configured # MOCK_WINDOW_DELAY_CONFIG form if we have a sensor configured
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["type"] == FlowResultType.MENU
# We should stay on window with an error # We should stay on window with an error
assert result["errors"] == {} assert result.get("errors") is None
# "window_sensor_entity_id": "window_open_detection_method" # "window_sensor_entity_id": "window_open_detection_method"
# } # }
assert result["step_id"] == "advanced" assert result["step_id"] == "advanced"
@@ -488,6 +762,8 @@ async def test_user_config_flow_window_auto_ko(
@pytest.mark.parametrize("expected_lingering_tasks", [True]) @pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True]) @pytest.mark.parametrize("expected_lingering_timers", [True])
# TODO reimplement this
@pytest.mark.skip
async def test_user_config_flow_over_4_switches( async def test_user_config_flow_over_4_switches(
hass: HomeAssistant, hass: HomeAssistant,
skip_hass_states_get, skip_hass_states_get,
@@ -497,11 +773,11 @@ async def test_user_config_flow_over_4_switches(
await create_central_config(hass) await create_central_config(hass)
SOURCE_CONFIG = { SOURCE_CONFIG = { # pylint: disable=invalid-name
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_SWITCH, CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_SWITCH,
} }
MAIN_CONFIG = { # pylint: disable=wildcard-import, invalid-name MAIN_CONFIG = { # pylint: disable=invalid-name
CONF_NAME: "TheOver4SwitchMockName", CONF_NAME: "TheOver4SwitchMockName",
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor", CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
CONF_CYCLE_MIN: 5, CONF_CYCLE_MIN: 5,
@@ -512,13 +788,15 @@ async def test_user_config_flow_over_4_switches(
CONF_USE_PRESENCE_FEATURE: False, CONF_USE_PRESENCE_FEATURE: False,
CONF_USE_MAIN_CENTRAL_CONFIG: True, CONF_USE_MAIN_CENTRAL_CONFIG: True,
CONF_USE_CENTRAL_MODE: False, CONF_USE_CENTRAL_MODE: False,
CONF_USED_BY_CENTRAL_BOILER: False,
} }
TYPE_CONFIG = { # pylint: disable=wildcard-import, invalid-name TYPE_CONFIG = { # pylint: disable=invalid-name
CONF_HEATER: "switch.mock_switch1", CONF_HEATER: "switch.mock_switch1",
CONF_HEATER_2: "switch.mock_switch2", CONF_HEATER_2: "switch.mock_switch2",
CONF_HEATER_3: "switch.mock_switch3", CONF_HEATER_3: "switch.mock_switch3",
CONF_HEATER_4: "switch.mock_switch4", CONF_HEATER_4: "switch.mock_switch4",
CONF_HEATER_KEEP_ALIVE: 0,
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI, CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
CONF_AC_MODE: False, CONF_AC_MODE: False,
CONF_INVERSE_SWITCH: False, CONF_INVERSE_SWITCH: False,
@@ -528,7 +806,7 @@ async def test_user_config_flow_over_4_switches(
DOMAIN, context={"source": SOURCE_USER} DOMAIN, context={"source": SOURCE_USER}
) )
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["type"] == FlowResultType.MENU
assert result["step_id"] == SOURCE_USER assert result["step_id"] == SOURCE_USER
result = await hass.config_entries.flow.async_configure( result = await hass.config_entries.flow.async_configure(
@@ -536,43 +814,43 @@ async def test_user_config_flow_over_4_switches(
user_input=SOURCE_CONFIG, user_input=SOURCE_CONFIG,
) )
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["type"] == FlowResultType.MENU
assert result["step_id"] == "main" assert result["step_id"] == "main"
assert result["errors"] == {} assert result.get("errors") is None
result = await hass.config_entries.flow.async_configure( result = await hass.config_entries.flow.async_configure(
result["flow_id"], result["flow_id"],
user_input=MAIN_CONFIG, user_input=MAIN_CONFIG,
) )
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["type"] == FlowResultType.MENU
assert result["step_id"] == "type" assert result["step_id"] == "type"
assert result["errors"] == {} assert result.get("errors") is None
result = await hass.config_entries.flow.async_configure( result = await hass.config_entries.flow.async_configure(
result["flow_id"], result["flow_id"],
user_input=TYPE_CONFIG, user_input=TYPE_CONFIG,
) )
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["type"] == FlowResultType.MENU
assert result["step_id"] == "tpi" assert result["step_id"] == "tpi"
assert result["errors"] == {} assert result.get("errors") is None
result = await hass.config_entries.flow.async_configure( result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input={CONF_USE_TPI_CENTRAL_CONFIG: True} result["flow_id"], user_input={CONF_USE_TPI_CENTRAL_CONFIG: True}
) )
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["type"] == FlowResultType.MENU
assert result["step_id"] == "presets" assert result["step_id"] == "presets"
assert result["errors"] == {} assert result.get("errors") is None
result = await hass.config_entries.flow.async_configure( result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input={CONF_USE_PRESETS_CENTRAL_CONFIG: True} result["flow_id"], user_input={CONF_USE_PRESETS_CENTRAL_CONFIG: True}
) )
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["type"] == FlowResultType.MENU
assert result["step_id"] == "advanced" assert result["step_id"] == "advanced"
assert result["errors"] == {} assert result.get("errors") is None
result = await hass.config_entries.flow.async_configure( result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input={CONF_USE_ADVANCED_CENTRAL_CONFIG: True} result["flow_id"], user_input={CONF_USE_ADVANCED_CENTRAL_CONFIG: True}

136
tests/test_last_seen.py Normal file
View File

@@ -0,0 +1,136 @@
# pylint: disable=wildcard-import, unused-wildcard-import, protected-access, unused-argument, line-too-long
""" Test the Security featrure """
from unittest.mock import patch, call
from datetime import timedelta, datetime
import logging
from custom_components.versatile_thermostat.thermostat_switch import (
ThermostatOverSwitch,
)
from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import
logging.getLogger().setLevel(logging.DEBUG)
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_last_seen_feature(hass: HomeAssistant, skip_hass_states_is_state):
"""Test the last_ssen feature
1. creates a thermostat and check that security is off
2. activate security feature when date is expired
3. change the last seen sensor
4. check that security is off
"""
tz = get_tz(hass) # pylint: disable=invalid-name
entry = MockConfigEntry(
domain=DOMAIN,
title="TheOverSwitchMockName",
unique_id="uniqueId",
data={
"name": "TheOverSwitchMockName",
"thermostat_type": "thermostat_over_switch",
"temperature_sensor_entity_id": "sensor.mock_temp_sensor",
"last_seen_temperature_sensor_entity_id": "sensor.mock_last_seen_temp_sensor",
"external_temperature_sensor_entity_id": "sensor.mock_ext_temp_sensor",
"cycle_min": 5,
"temp_min": 15,
"temp_max": 30,
"frost_temp": 7,
"eco_temp": 17,
"comfort_temp": 18,
"boost_temp": 19,
"use_window_feature": False,
"use_motion_feature": False,
"use_power_feature": False,
"use_presence_feature": False,
"heater_entity_id": "switch.mock_switch",
"proportional_function": "tpi",
"tpi_coef_int": 0.3,
"tpi_coef_ext": 0.01,
"minimal_activation_delay": 30,
"security_delay_min": 5, # 5 minutes
"security_min_on_percent": 0.2,
"security_default_on_percent": 0.1,
},
)
# 1. creates a thermostat and check that security is off
now: datetime = datetime.now(tz=tz)
entity: ThermostatOverSwitch = await create_thermostat(
hass, entry, "climate.theoverswitchmockname"
)
assert entity
assert entity._security_state is False
assert entity.preset_mode is not PRESET_SECURITY
assert entity._last_ext_temperature_measure is not None
assert entity._last_temperature_measure is not None
assert (entity._last_temperature_measure.astimezone(tz) - now).total_seconds() < 1
assert (
entity._last_ext_temperature_measure.astimezone(tz) - now
).total_seconds() < 1
# set a preset
assert entity.preset_mode is PRESET_NONE
await entity.async_set_preset_mode(PRESET_COMFORT)
assert entity.preset_mode is PRESET_COMFORT
# Turn On the thermostat
assert entity.hvac_mode == HVACMode.OFF
await entity.async_set_hvac_mode(HVACMode.HEAT)
assert entity.hvac_mode == HVACMode.HEAT
# 2. activate security feature when date is expired
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on:
event_timestamp = now - timedelta(minutes=6)
# set temperature to 15 so that on_percent will be > security_min_on_percent (0.2)
await send_temperature_change_event(entity, 15, event_timestamp)
assert entity.security_state is True
assert entity.preset_mode == PRESET_SECURITY
assert mock_send_event.call_count == 3
mock_send_event.assert_has_calls(
[
call.send_event(EventType.PRESET_EVENT, {"preset": PRESET_SECURITY}),
call.send_event(
EventType.TEMPERATURE_EVENT,
{
"last_temperature_measure": event_timestamp.isoformat(),
"last_ext_temperature_measure": entity._last_ext_temperature_measure.isoformat(),
"current_temp": 15,
"current_ext_temp": None,
"target_temp": 18,
},
),
call.send_event(
EventType.SECURITY_EVENT,
{
"type": "start",
"last_temperature_measure": event_timestamp.isoformat(),
"last_ext_temperature_measure": entity._last_ext_temperature_measure.isoformat(),
"current_temp": 15,
"current_ext_temp": None,
"target_temp": 18,
},
),
],
any_order=True,
)
assert mock_heater_on.call_count == 1
# 3. change the last seen sensor
event_timestamp = now - timedelta(minutes=4)
await send_last_seen_temperature_change_event(entity, event_timestamp)
assert entity.security_state is False
assert entity.preset_mode is PRESET_COMFORT
assert entity._last_temperature_measure == event_timestamp

View File

@@ -83,7 +83,7 @@ async def test_movement_management_time_not_enough(
await send_ext_temperature_change_event(entity, 10, event_timestamp) await send_ext_temperature_change_event(entity, 10, event_timestamp)
await send_presence_change_event(entity, True, False, event_timestamp) await send_presence_change_event(entity, True, False, event_timestamp)
assert entity.presence_state is "on" assert entity.presence_state == "on"
# starts detecting motion with time not enough # starts detecting motion with time not enough
with patch( with patch(
@@ -110,7 +110,7 @@ async def test_movement_management_time_not_enough(
assert entity.target_temperature == 18 assert entity.target_temperature == 18
# state is not changed if time is not enough # state is not changed if time is not enough
assert entity.motion_state is None assert entity.motion_state is None
assert entity.presence_state is "on" assert entity.presence_state == "on"
assert mock_send_event.call_count == 0 assert mock_send_event.call_count == 0
# Change is not confirmed # Change is not confirmed
@@ -141,8 +141,8 @@ async def test_movement_management_time_not_enough(
assert entity.preset_mode is PRESET_ACTIVITY assert entity.preset_mode is PRESET_ACTIVITY
# because motion is detected yet # because motion is detected yet
assert entity.target_temperature == 19 assert entity.target_temperature == 19
assert entity.motion_state is "on" assert entity.motion_state == "on"
assert entity.presence_state is "on" assert entity.presence_state == "on"
# stop detecting motion with off delay too low # stop detecting motion with off delay too low
with patch( with patch(
@@ -167,8 +167,8 @@ async def test_movement_management_time_not_enough(
assert entity.preset_mode is PRESET_ACTIVITY assert entity.preset_mode is PRESET_ACTIVITY
# because no motion is detected yet # because no motion is detected yet
assert entity.target_temperature == 19 assert entity.target_temperature == 19
assert entity.motion_state is "on" assert entity.motion_state == "on"
assert entity.presence_state is "on" assert entity.presence_state == "on"
assert mock_send_event.call_count == 0 assert mock_send_event.call_count == 0
# The heater must heat now # The heater must heat now
@@ -199,8 +199,8 @@ async def test_movement_management_time_not_enough(
assert entity.preset_mode is PRESET_ACTIVITY assert entity.preset_mode is PRESET_ACTIVITY
# because no motion is detected yet # because no motion is detected yet
assert entity.target_temperature == 18 assert entity.target_temperature == 18
assert entity.motion_state is "off" assert entity.motion_state == "off"
assert entity.presence_state is "on" assert entity.presence_state == "on"
assert mock_send_event.call_count == 0 assert mock_send_event.call_count == 0
# The heater must stop heating now # The heater must stop heating now
@@ -280,7 +280,7 @@ async def test_movement_management_time_enough_and_presence(
await send_ext_temperature_change_event(entity, 10, event_timestamp) await send_ext_temperature_change_event(entity, 10, event_timestamp)
await send_presence_change_event(entity, True, False, event_timestamp) await send_presence_change_event(entity, True, False, event_timestamp)
assert entity.presence_state is "on" assert entity.presence_state == "on"
# starts detecting motion # starts detecting motion
with patch( with patch(
@@ -302,8 +302,8 @@ async def test_movement_management_time_enough_and_presence(
assert entity.preset_mode is PRESET_ACTIVITY assert entity.preset_mode is PRESET_ACTIVITY
# because motion is detected yet -> switch to Boost mode # because motion is detected yet -> switch to Boost mode
assert entity.target_temperature == 19 assert entity.target_temperature == 19
assert entity.motion_state is "on" assert entity.motion_state == "on"
assert entity.presence_state is "on" assert entity.presence_state == "on"
assert mock_send_event.call_count == 0 assert mock_send_event.call_count == 0
# Change is confirmed. Heater should be started # Change is confirmed. Heater should be started
@@ -331,8 +331,8 @@ async def test_movement_management_time_enough_and_presence(
assert entity.preset_mode is PRESET_ACTIVITY assert entity.preset_mode is PRESET_ACTIVITY
# because no motion is detected yet # because no motion is detected yet
assert entity.target_temperature == 18 assert entity.target_temperature == 18
assert entity.motion_state is "off" assert entity.motion_state == "off"
assert entity.presence_state is "on" assert entity.presence_state == "on"
assert mock_send_event.call_count == 0 assert mock_send_event.call_count == 0
assert mock_heater_on.call_count == 0 assert mock_heater_on.call_count == 0
@@ -412,7 +412,7 @@ async def test_movement_management_time_enoughand_not_presence(
await send_ext_temperature_change_event(entity, 10, event_timestamp) await send_ext_temperature_change_event(entity, 10, event_timestamp)
await send_presence_change_event(entity, False, True, event_timestamp) await send_presence_change_event(entity, False, True, event_timestamp)
assert entity.presence_state is "off" assert entity.presence_state == "off"
# starts detecting motion # starts detecting motion
with patch( with patch(
@@ -434,8 +434,8 @@ async def test_movement_management_time_enoughand_not_presence(
assert entity.preset_mode is PRESET_ACTIVITY assert entity.preset_mode is PRESET_ACTIVITY
# because motion is detected yet -> switch to Boost away mode # because motion is detected yet -> switch to Boost away mode
assert entity.target_temperature == 19.1 assert entity.target_temperature == 19.1
assert entity.motion_state is "on" assert entity.motion_state == "on"
assert entity.presence_state is "off" assert entity.presence_state == "off"
assert mock_send_event.call_count == 0 assert mock_send_event.call_count == 0
# Change is confirmed. Heater should be started # Change is confirmed. Heater should be started
@@ -463,8 +463,8 @@ async def test_movement_management_time_enoughand_not_presence(
assert entity.preset_mode is PRESET_ACTIVITY assert entity.preset_mode is PRESET_ACTIVITY
# because no motion is detected yet # because no motion is detected yet
assert entity.target_temperature == 18.1 assert entity.target_temperature == 18.1
assert entity.motion_state is "off" assert entity.motion_state == "off"
assert entity.presence_state is "off" assert entity.presence_state == "off"
assert mock_send_event.call_count == 0 assert mock_send_event.call_count == 0
# 18.1 starts heating with a low on_percent # 18.1 starts heating with a low on_percent
@@ -546,7 +546,7 @@ async def test_movement_management_with_stop_during_condition(
await send_ext_temperature_change_event(entity, 10, event_timestamp) await send_ext_temperature_change_event(entity, 10, event_timestamp)
await send_presence_change_event(entity, False, True, event_timestamp) await send_presence_change_event(entity, False, True, event_timestamp)
assert entity.presence_state is "off" assert entity.presence_state == "off"
# starts detecting motion # starts detecting motion
with patch( with patch(
@@ -569,7 +569,7 @@ async def test_movement_management_with_stop_during_condition(
# because motion is detected yet -> switch to Boost mode # because motion is detected yet -> switch to Boost mode
assert entity.target_temperature == 18 assert entity.target_temperature == 18
assert entity.motion_state is None assert entity.motion_state is None
assert entity.presence_state is "off" assert entity.presence_state == "off"
# Send a stop detection # Send a stop detection
event_timestamp = now - timedelta(minutes=4) event_timestamp = now - timedelta(minutes=4)
@@ -580,7 +580,7 @@ async def test_movement_management_with_stop_during_condition(
assert entity.preset_mode is PRESET_ACTIVITY assert entity.preset_mode is PRESET_ACTIVITY
assert entity.target_temperature == 18 assert entity.target_temperature == 18
assert entity.motion_state is None assert entity.motion_state is None
assert entity.presence_state is "off" assert entity.presence_state == "off"
# Resend a start detection # Resend a start detection
event_timestamp = now - timedelta(minutes=3) event_timestamp = now - timedelta(minutes=3)
@@ -592,10 +592,10 @@ async def test_movement_management_with_stop_during_condition(
# still no motion detected # still no motion detected
assert entity.target_temperature == 18 assert entity.target_temperature == 18
assert entity.motion_state is None assert entity.motion_state is None
assert entity.presence_state is "off" assert entity.presence_state == "off"
await try_condition1(None) await try_condition1(None)
# We should have switch this time # We should have switch this time
assert entity.target_temperature == 19 # Boost assert entity.target_temperature == 19 # Boost
assert entity.motion_state is "on" # switch to movement on assert entity.motion_state == "on" # switch to movement on
assert entity.presence_state is "off" # Non change assert entity.presence_state == "off" # Non change

View File

@@ -260,6 +260,7 @@ async def test_multiple_switchs(
CONF_HEATER_2: "switch.mock_switch2", CONF_HEATER_2: "switch.mock_switch2",
CONF_HEATER_3: "switch.mock_switch3", CONF_HEATER_3: "switch.mock_switch3",
CONF_HEATER_4: "switch.mock_switch4", CONF_HEATER_4: "switch.mock_switch4",
CONF_HEATER_KEEP_ALIVE: 0,
CONF_MINIMAL_ACTIVATION_DELAY: 30, CONF_MINIMAL_ACTIVATION_DELAY: 30,
CONF_SECURITY_DELAY_MIN: 5, CONF_SECURITY_DELAY_MIN: 5,
CONF_SECURITY_MIN_ON_PERCENT: 0.3, CONF_SECURITY_MIN_ON_PERCENT: 0.3,
@@ -472,7 +473,7 @@ async def test_multiple_climates_underlying_changes(
skip_hass_states_is_state, skip_hass_states_is_state,
skip_send_event, skip_send_event,
): # pylint: disable=unused-argument ): # pylint: disable=unused-argument
"""Test that when multiple switch are configured the activation of one underlying """Test that when multiple climate are configured the activation of one underlying
climate activate the others""" climate activate the others"""
tz = get_tz(hass) # pylint: disable=invalid-name tz = get_tz(hass) # pylint: disable=invalid-name
@@ -541,11 +542,15 @@ async def test_multiple_climates_underlying_changes(
assert entity.is_device_active is False # pylint: disable=protected-access assert entity.is_device_active is False # pylint: disable=protected-access
# Stop heating on one underlying climate # Stop heating on one underlying climate
# All underlying supposed to be aligned with the hvac_mode now
with patch( with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.async_control_heating" "custom_components.versatile_thermostat.base_thermostat.BaseThermostat.async_control_heating"
), patch( ), patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.set_hvac_mode" "custom_components.versatile_thermostat.underlyings.UnderlyingClimate.set_hvac_mode"
) as mock_underlying_set_hvac_mode: ) as mock_underlying_set_hvac_mode, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.hvac_mode",
HVACMode.HEAT,
):
# Wait 11 sec so that the event will not be discarded # Wait 11 sec so that the event will not be discarded
event_timestamp = now + timedelta(seconds=11) event_timestamp = now + timedelta(seconds=11)
await send_climate_change_event( await send_climate_change_event(
@@ -555,6 +560,7 @@ async def test_multiple_climates_underlying_changes(
HVACAction.OFF, HVACAction.OFF,
HVACAction.HEATING, HVACAction.HEATING,
event_timestamp, event_timestamp,
underlying_entity_id="switch.mock_climate3",
) )
# Should be call for all Switch # Should be call for all Switch
@@ -577,6 +583,9 @@ async def test_multiple_climates_underlying_changes(
# a function but a property # a function but a property
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.hvac_action", "custom_components.versatile_thermostat.underlyings.UnderlyingClimate.hvac_action",
HVACAction.IDLE, HVACAction.IDLE,
), patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.hvac_mode",
HVACMode.OFF,
): ):
# Wait 11 sec so that the event will not be discarded # Wait 11 sec so that the event will not be discarded
event_timestamp = now + timedelta(seconds=11) event_timestamp = now + timedelta(seconds=11)
@@ -601,6 +610,113 @@ async def test_multiple_climates_underlying_changes(
assert entity.is_device_active is False # pylint: disable=protected-access assert entity.is_device_active is False # pylint: disable=protected-access
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_multiple_climates_underlying_changes_not_aligned(
hass: HomeAssistant,
skip_hass_states_is_state,
skip_send_event,
): # pylint: disable=unused-argument
"""Test that when multiple climate are configured the activation of one underlying
climate don't activate the others if their havc_mode are not aligned"""
tz = get_tz(hass) # pylint: disable=invalid-name
now: datetime = datetime.now(tz=tz)
entry = MockConfigEntry(
domain=DOMAIN,
title="TheOver4ClimateMockName",
unique_id="uniqueId",
data={
CONF_NAME: "TheOver4ClimateMockName",
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_CLIMATE,
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
CONF_CYCLE_MIN: 8,
CONF_TEMP_MIN: 15,
CONF_TEMP_MAX: 30,
"eco_temp": 17,
"comfort_temp": 18,
"boost_temp": 19,
CONF_USE_WINDOW_FEATURE: False,
CONF_USE_MOTION_FEATURE: False,
CONF_USE_POWER_FEATURE: False,
CONF_USE_PRESENCE_FEATURE: False,
CONF_CLIMATE: "switch.mock_climate1",
CONF_CLIMATE_2: "switch.mock_climate2",
CONF_CLIMATE_3: "switch.mock_climate3",
CONF_CLIMATE_4: "switch.mock_climate4",
CONF_MINIMAL_ACTIVATION_DELAY: 30,
CONF_SECURITY_DELAY_MIN: 5,
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
},
)
entity: BaseThermostat = await create_thermostat(
hass, entry, "climate.theover4climatemockname"
)
assert entity
assert entity.is_over_climate is True
assert entity.nb_underlying_entities == 4
# start heating, in boost mode. We block the control_heating to avoid running a cycle
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.async_control_heating"
), patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.set_hvac_mode"
) as mock_underlying_set_hvac_mode:
await entity.async_set_hvac_mode(HVACMode.HEAT)
await entity.async_set_preset_mode(PRESET_BOOST)
assert entity.hvac_mode is HVACMode.HEAT
assert entity.preset_mode is PRESET_BOOST
assert entity.target_temperature == 19
assert entity.window_state is STATE_OFF
event_timestamp = now - timedelta(minutes=4)
await send_temperature_change_event(entity, 15, event_timestamp)
# Should be call for all Switch
assert mock_underlying_set_hvac_mode.call_count == 4
mock_underlying_set_hvac_mode.assert_has_calls(
[
call.set_hvac_mode(HVACMode.HEAT),
]
)
# Stop heating on one underlying climate
# All underlying supposed to be aligned with the hvac_mode now
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.async_control_heating"
), patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.set_hvac_mode"
) as mock_underlying_set_hvac_mode, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.hvac_mode",
HVACMode.COOL,
):
# Wait 11 sec so that the event will not be discarded
event_timestamp = now + timedelta(seconds=11)
await send_climate_change_event(
entity,
HVACMode.OFF,
HVACMode.HEAT,
HVACAction.OFF,
HVACAction.HEATING,
event_timestamp,
underlying_entity_id="switch.mock_climate3",
)
# Should be call for all Switch
assert mock_underlying_set_hvac_mode.call_count == 0
# mock_underlying_set_hvac_mode.assert_has_calls(
# [
# call.set_hvac_mode(HVACMode.OFF),
# ]
# )
# No change
assert entity.hvac_mode == HVACMode.HEAT
@pytest.mark.parametrize("expected_lingering_tasks", [True]) @pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True]) @pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_multiple_switch_power_management( async def test_multiple_switch_power_management(
@@ -631,6 +747,7 @@ async def test_multiple_switch_power_management(
CONF_HEATER_2: "switch.mock_switch2", CONF_HEATER_2: "switch.mock_switch2",
CONF_HEATER_3: "switch.mock_switch3", CONF_HEATER_3: "switch.mock_switch3",
CONF_HEATER_4: "switch.mock_switch4", CONF_HEATER_4: "switch.mock_switch4",
CONF_HEATER_KEEP_ALIVE: 0,
CONF_MINIMAL_ACTIVATION_DELAY: 30, CONF_MINIMAL_ACTIVATION_DELAY: 30,
CONF_SECURITY_DELAY_MIN: 5, CONF_SECURITY_DELAY_MIN: 5,
CONF_SECURITY_MIN_ON_PERCENT: 0.3, CONF_SECURITY_MIN_ON_PERCENT: 0.3,

View File

@@ -42,15 +42,15 @@ def test_pi_algorithm_basics():
assert the_algo.calculate_regulated_temperature(18.7, 10) == 21.6 # +1.7 assert the_algo.calculate_regulated_temperature(18.7, 10) == 21.6 # +1.7
assert the_algo.calculate_regulated_temperature(19, 10) == 21.6 # +1.7 assert the_algo.calculate_regulated_temperature(19, 10) == 21.6 # +1.7
assert the_algo.calculate_regulated_temperature(20, 10) == 21.5 # +1.5 assert the_algo.calculate_regulated_temperature(20, 10) == 21.5 # +1.5
assert the_algo.calculate_regulated_temperature(21, 10) == 21.3 # +0.8 assert the_algo.calculate_regulated_temperature(21, 10) == 21.1 # error change sign
assert the_algo.calculate_regulated_temperature(21, 10) == 21.3 # +0.7 assert the_algo.calculate_regulated_temperature(21, 10) == 20.9
assert the_algo.calculate_regulated_temperature(20, 10) == 21.4 # +0.7 assert the_algo.calculate_regulated_temperature(20, 10) == 21.0
# Test temperature external # Test temperature external
assert the_algo.calculate_regulated_temperature(20, 12) == 21.2 # +0.8 assert the_algo.calculate_regulated_temperature(20, 12) == 20.8
assert the_algo.calculate_regulated_temperature(20, 15) == 20.9 # +0.5 assert the_algo.calculate_regulated_temperature(20, 15) == 20.5
assert the_algo.calculate_regulated_temperature(20, 18) == 20.6 # +0.2 assert the_algo.calculate_regulated_temperature(20, 18) == 20.2 # +0.2
assert the_algo.calculate_regulated_temperature(20, 20) == 20.4 # = assert the_algo.calculate_regulated_temperature(20, 20) == 20 # =
def test_pi_algorithm_light(): def test_pi_algorithm_light():
@@ -78,15 +78,15 @@ def test_pi_algorithm_light():
assert the_algo.calculate_regulated_temperature(18.7, 10) == 21.6 # +1.7 assert the_algo.calculate_regulated_temperature(18.7, 10) == 21.6 # +1.7
assert the_algo.calculate_regulated_temperature(19, 10) == 21.6 # +1.7 assert the_algo.calculate_regulated_temperature(19, 10) == 21.6 # +1.7
assert the_algo.calculate_regulated_temperature(20, 10) == 21.5 # +1.5 assert the_algo.calculate_regulated_temperature(20, 10) == 21.5 # +1.5
assert the_algo.calculate_regulated_temperature(21, 10) == 21.3 # +0.8 assert the_algo.calculate_regulated_temperature(21, 10) == 21.1 # Error sign change
assert the_algo.calculate_regulated_temperature(21, 10) == 21.3 # +0.7 assert the_algo.calculate_regulated_temperature(21, 10) == 20.9
assert the_algo.calculate_regulated_temperature(20, 10) == 21.4 # +0.7 assert the_algo.calculate_regulated_temperature(20, 10) == 21
# Test temperature external # Test temperature external
assert the_algo.calculate_regulated_temperature(20, 12) == 21.2 # +0.8 assert the_algo.calculate_regulated_temperature(20, 12) == 20.8 # +0.8
assert the_algo.calculate_regulated_temperature(20, 15) == 20.9 # +0.5 assert the_algo.calculate_regulated_temperature(20, 15) == 20.5 # +0.5
assert the_algo.calculate_regulated_temperature(20, 18) == 20.6 # +0.2 assert the_algo.calculate_regulated_temperature(20, 18) == 20.2 # +0.2
assert the_algo.calculate_regulated_temperature(20, 20) == 20.4 # = assert the_algo.calculate_regulated_temperature(20, 20) == 20.0 # =
def test_pi_algorithm_medium(): def test_pi_algorithm_medium():
@@ -114,20 +114,20 @@ def test_pi_algorithm_medium():
assert the_algo.calculate_regulated_temperature(18.7, 10) == 22.4 assert the_algo.calculate_regulated_temperature(18.7, 10) == 22.4
assert the_algo.calculate_regulated_temperature(19, 10) == 22.3 assert the_algo.calculate_regulated_temperature(19, 10) == 22.3
assert the_algo.calculate_regulated_temperature(20, 10) == 21.9 assert the_algo.calculate_regulated_temperature(20, 10) == 21.9
assert the_algo.calculate_regulated_temperature(21, 10) == 21.4 assert the_algo.calculate_regulated_temperature(21, 10) == 21.0 # error sign change
assert the_algo.calculate_regulated_temperature(21, 10) == 21.3 assert the_algo.calculate_regulated_temperature(21, 10) == 20.7
assert the_algo.calculate_regulated_temperature(20, 10) == 21.7 assert the_algo.calculate_regulated_temperature(20, 10) == 21.1
# Test temperature external # Test temperature external
assert the_algo.calculate_regulated_temperature(20, 8) == 21.9 assert the_algo.calculate_regulated_temperature(20, 8) == 21.3
assert the_algo.calculate_regulated_temperature(20, 6) == 22.1 assert the_algo.calculate_regulated_temperature(20, 6) == 21.5
assert the_algo.calculate_regulated_temperature(20, 4) == 22.3 assert the_algo.calculate_regulated_temperature(20, 4) == 21.7
assert the_algo.calculate_regulated_temperature(20, 2) == 22.5 assert the_algo.calculate_regulated_temperature(20, 2) == 21.9
assert the_algo.calculate_regulated_temperature(20, 0) == 22.7 assert the_algo.calculate_regulated_temperature(20, 0) == 22.1
assert the_algo.calculate_regulated_temperature(20, -2) == 22.9 assert the_algo.calculate_regulated_temperature(20, -2) == 22.3
assert the_algo.calculate_regulated_temperature(20, -4) == 23.0 assert the_algo.calculate_regulated_temperature(20, -4) == 22.5
assert the_algo.calculate_regulated_temperature(20, -6) == 23.0 assert the_algo.calculate_regulated_temperature(20, -6) == 22.7
assert the_algo.calculate_regulated_temperature(20, -8) == 23.0 assert the_algo.calculate_regulated_temperature(20, -8) == 22.9
# to reset the accumulated erro # to reset the accumulated erro
the_algo.set_target_temp(20) the_algo.set_target_temp(20)
@@ -173,22 +173,22 @@ def test_pi_algorithm_strong():
assert the_algo.calculate_regulated_temperature(18.7, 10) == 24 assert the_algo.calculate_regulated_temperature(18.7, 10) == 24
assert the_algo.calculate_regulated_temperature(19, 10) == 24 assert the_algo.calculate_regulated_temperature(19, 10) == 24
assert the_algo.calculate_regulated_temperature(20, 10) == 23.9 assert the_algo.calculate_regulated_temperature(20, 10) == 23.9
assert the_algo.calculate_regulated_temperature(21, 10) == 23.3 assert the_algo.calculate_regulated_temperature(21, 10) == 22.3 # error sign change
assert the_algo.calculate_regulated_temperature(21, 10) == 23.1 assert the_algo.calculate_regulated_temperature(21, 10) == 21.8
assert the_algo.calculate_regulated_temperature(21, 10) == 22.9 assert the_algo.calculate_regulated_temperature(21, 10) == 21.5
assert the_algo.calculate_regulated_temperature(21, 10) == 22.7 assert the_algo.calculate_regulated_temperature(21, 10) == 21.3
assert the_algo.calculate_regulated_temperature(21, 10) == 22.5 assert the_algo.calculate_regulated_temperature(21, 10) == 21.1
assert the_algo.calculate_regulated_temperature(21, 10) == 22.3 assert the_algo.calculate_regulated_temperature(21, 10) == 20.9
assert the_algo.calculate_regulated_temperature(21, 10) == 22.1 assert the_algo.calculate_regulated_temperature(21, 10) == 20.7
# Test temperature external # Test temperature external
assert the_algo.calculate_regulated_temperature(20, 8) == 22.9 assert the_algo.calculate_regulated_temperature(20, 8) == 21.5
assert the_algo.calculate_regulated_temperature(20, 6) == 23.3 assert the_algo.calculate_regulated_temperature(20, 6) == 21.9
assert the_algo.calculate_regulated_temperature(20, 4) == 23.7 assert the_algo.calculate_regulated_temperature(20, 4) == 22.3
assert the_algo.calculate_regulated_temperature(20, 2) == 24 assert the_algo.calculate_regulated_temperature(20, 2) == 22.7
assert the_algo.calculate_regulated_temperature(20, 0) == 24 assert the_algo.calculate_regulated_temperature(20, 0) == 23.1
assert the_algo.calculate_regulated_temperature(20, -2) == 24 assert the_algo.calculate_regulated_temperature(20, -2) == 23.5
assert the_algo.calculate_regulated_temperature(20, -4) == 24 assert the_algo.calculate_regulated_temperature(20, -4) == 23.9
assert the_algo.calculate_regulated_temperature(20, -6) == 24 assert the_algo.calculate_regulated_temperature(20, -6) == 24
assert the_algo.calculate_regulated_temperature(20, -8) == 24 assert the_algo.calculate_regulated_temperature(20, -8) == 24

View File

@@ -288,7 +288,7 @@ async def test_security_feature_back_on_percent(
assert entity.security_state is False assert entity.security_state is False
assert mock_send_event.call_count == 0 assert mock_send_event.call_count == 0
# 3. Set security mode with a preset change # 3. Set safety mode with a preset change
with patch( with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event" "custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event, patch( ) as mock_send_event, patch(
@@ -400,7 +400,7 @@ async def test_security_over_climate(
skip_turn_on_off_heater, skip_turn_on_off_heater,
skip_send_event, skip_send_event,
): ):
"""Test that when a underlying climate is not available the VTherm doesn't go into security mode""" """Test that when a underlying climate is not available the VTherm doesn't go into safety mode"""
tz = get_tz(hass) # pylint: disable=invalid-name tz = get_tz(hass) # pylint: disable=invalid-name
now: datetime = datetime.now(tz=tz) now: datetime = datetime.now(tz=tz)
@@ -471,7 +471,7 @@ async def test_security_over_climate(
assert mock_find_climate.mock_calls[0] == call() assert mock_find_climate.mock_calls[0] == call()
mock_find_climate.assert_has_calls([call.find_underlying_entity()]) mock_find_climate.assert_has_calls([call.find_underlying_entity()])
# Force security mode # Force safety mode
assert entity._last_ext_temperature_measure is not None assert entity._last_ext_temperature_measure is not None
assert entity._last_temperature_measure is not None assert entity._last_temperature_measure is not None
assert ( assert (
@@ -505,7 +505,7 @@ async def test_security_over_climate(
event_timestamp = now - timedelta(minutes=6) event_timestamp = now - timedelta(minutes=6)
await send_temperature_change_event(entity, 15, event_timestamp) await send_temperature_change_event(entity, 15, event_timestamp)
# Should stay False because a climate is never in security mode # Should stay False because a climate is never in safety mode
assert entity.security_state is False assert entity.security_state is False
assert entity.preset_mode == "none" assert entity.preset_mode == "none"
assert entity._saved_preset_mode == "none" assert entity._saved_preset_mode == "none"

View File

@@ -5,10 +5,6 @@ from unittest.mock import patch, call
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.components.climate import HVACAction, HVACMode 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 pytest_homeassistant_custom_component.common import MockConfigEntry
@@ -38,18 +34,7 @@ async def test_over_switch_full_start(hass: HomeAssistant, skip_hass_states_is_s
with patch( with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event" "custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event: ) as mock_send_event:
entry.add_to_hass(hass) entity = await create_thermostat(hass, entry, "climate.theoverswitchmockname")
await hass.config_entries.async_setup(entry.entry_id)
assert entry.state is ConfigEntryState.LOADED
def find_my_entity(entity_id) -> ClimateEntity:
"""Find my new entity"""
component: EntityComponent[ClimateEntity] = hass.data[CLIMATE_DOMAIN]
for entity in component.entities:
if entity.entity_id == entity_id:
return entity
entity: BaseThermostat = find_my_entity("climate.theoverswitchmockname")
assert entity assert entity
assert isinstance(entity, ThermostatOverSwitch) assert isinstance(entity, ThermostatOverSwitch)
@@ -108,18 +93,19 @@ async def test_over_climate_full_start(hass: HomeAssistant, skip_hass_states_is_
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate", "custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
return_value=fake_underlying_climate, return_value=fake_underlying_climate,
) as mock_find_climate: ) as mock_find_climate:
entry.add_to_hass(hass) entity = await create_thermostat(hass, entry, "climate.theoverclimatemockname")
await hass.config_entries.async_setup(entry.entry_id) # entry.add_to_hass(hass)
assert entry.state is ConfigEntryState.LOADED # 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""" # def find_my_entity(entity_id) -> ClimateEntity:
component: EntityComponent[ClimateEntity] = hass.data[CLIMATE_DOMAIN] # """Find my new entity"""
for entity in component.entities: # component: EntityComponent[ClimateEntity] = hass.data[CLIMATE_DOMAIN]
if entity.entity_id == entity_id: # for entity in component.entities:
return entity # if entity.entity_id == entity_id:
# return entity
entity = find_my_entity("climate.theoverclimatemockname") #
# entity = find_my_entity("climate.theoverclimatemockname")
assert entity assert entity
assert isinstance(entity, ThermostatOverClimate) assert isinstance(entity, ThermostatOverClimate)
@@ -174,23 +160,24 @@ async def test_over_4switch_full_start(hass: HomeAssistant, skip_hass_states_is_
with patch( with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event" "custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event: ) as mock_send_event:
entry.add_to_hass(hass) entity = await create_thermostat(hass, entry, "climate.theover4switchmockname")
await hass.config_entries.async_setup(entry.entry_id) # entry.add_to_hass(hass)
assert entry.state is ConfigEntryState.LOADED # 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""" # def find_my_entity(entity_id) -> ClimateEntity:
component: EntityComponent[ClimateEntity] = hass.data[CLIMATE_DOMAIN] # """Find my new entity"""
for entity in component.entities: # component: EntityComponent[ClimateEntity] = hass.data[CLIMATE_DOMAIN]
if entity.entity_id == entity_id: # for entity in component.entities:
return entity # if entity.entity_id == entity_id:
# return entity
entity: BaseThermostat = find_my_entity("climate.theover4switchmockname") #
# entity: BaseThermostat = find_my_entity("climate.theover4switchmockname")
assert entity assert entity
assert entity.name == "TheOver4SwitchMockName" assert entity.name == "TheOver4SwitchMockName"
assert entity.is_over_climate is False assert entity.is_over_switch
assert entity.hvac_action is HVACAction.OFF assert entity.hvac_action is HVACAction.OFF
assert entity.hvac_mode is HVACMode.OFF assert entity.hvac_mode is HVACMode.OFF
assert entity.target_temperature == entity.min_temp assert entity.target_temperature == entity.min_temp
@@ -264,8 +251,12 @@ async def test_over_switch_deactivate_preset(
CONF_HEATER_2: None, CONF_HEATER_2: None,
CONF_HEATER_3: None, CONF_HEATER_3: None,
CONF_HEATER_4: None, CONF_HEATER_4: None,
CONF_HEATER_KEEP_ALIVE: 0,
CONF_SECURITY_DELAY_MIN: 10, CONF_SECURITY_DELAY_MIN: 10,
CONF_MINIMAL_ACTIVATION_DELAY: 10, CONF_MINIMAL_ACTIVATION_DELAY: 10,
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
CONF_TPI_COEF_INT: 0.6,
CONF_TPI_COEF_EXT: 0.01,
}, },
) )

View File

@@ -56,6 +56,23 @@ async def test_over_switch_ac_full_start(
assert entity assert entity
assert isinstance(entity, ThermostatOverSwitch) assert isinstance(entity, ThermostatOverSwitch)
# Initialise the preset temp
await set_climate_preset_temp(
entity, PRESET_FROST_PROTECTION + PRESET_AWAY_SUFFIX, 7
)
await set_climate_preset_temp(entity, PRESET_ECO + PRESET_AWAY_SUFFIX, 16)
await set_climate_preset_temp(entity, PRESET_COMFORT + PRESET_AWAY_SUFFIX, 17)
await set_climate_preset_temp(entity, PRESET_BOOST + PRESET_AWAY_SUFFIX, 18)
await set_climate_preset_temp(
entity, PRESET_ECO + PRESET_AC_SUFFIX + PRESET_AWAY_SUFFIX, 27
)
await set_climate_preset_temp(
entity, PRESET_COMFORT + PRESET_AC_SUFFIX + PRESET_AWAY_SUFFIX, 26
)
await set_climate_preset_temp(
entity, PRESET_BOOST + PRESET_AC_SUFFIX + PRESET_AWAY_SUFFIX, 25
)
assert entity.name == "TheOverSwitchMockName" assert entity.name == "TheOverSwitchMockName"
assert entity.is_over_climate is False # pylint: disable=protected-access assert entity.is_over_climate is False # pylint: disable=protected-access
assert entity.ac_mode is True assert entity.ac_mode is True

View File

@@ -0,0 +1,383 @@
"""Test the switch keep-alive feature."""
import logging
from collections.abc import AsyncGenerator, Callable, Awaitable
from dataclasses import dataclass
from unittest.mock import ANY, _Call, call, patch
from datetime import datetime, timedelta
from typing import cast
from custom_components.versatile_thermostat.keep_alive import BackoffTimer
from custom_components.versatile_thermostat.thermostat_switch import (
ThermostatOverSwitch,
)
from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import
logging.getLogger().setLevel(logging.DEBUG)
@pytest.fixture
def config_entry() -> MockConfigEntry:
"""Return common test data"""
return MockConfigEntry(
domain=DOMAIN,
title="TheOverSwitchMockName",
unique_id="uniqueId",
data={
CONF_NAME: "TheOverSwitchMockName",
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_SWITCH,
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
CONF_CYCLE_MIN: 5,
CONF_TEMP_MIN: 15,
CONF_TEMP_MAX: 30,
CONF_USE_WINDOW_FEATURE: False,
CONF_USE_MOTION_FEATURE: False,
CONF_USE_POWER_FEATURE: False,
CONF_USE_PRESENCE_FEATURE: False,
CONF_HEATER: "switch.mock_switch",
CONF_HEATER_KEEP_ALIVE: 1,
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
CONF_TPI_COEF_INT: 0.3,
CONF_TPI_COEF_EXT: 0.01,
CONF_MINIMAL_ACTIVATION_DELAY: 30,
CONF_SECURITY_DELAY_MIN: 5,
CONF_SECURITY_MIN_ON_PERCENT: 0.1,
},
)
@dataclass
class CommonMocks:
"""Common mocked objects used by most test cases"""
config_entry: MockConfigEntry
hass: HomeAssistant
thermostat: ThermostatOverSwitch
mock_is_state: MagicMock
mock_get_state: MagicMock
mock_service_call: MagicMock
mock_async_track_time_interval: MagicMock
mock_send_event: MagicMock
# pylint: disable=redefined-outer-name, line-too-long, protected-access
@pytest.fixture
async def common_mocks(
config_entry: MockConfigEntry,
hass: HomeAssistant,
) -> AsyncGenerator[CommonMocks, None]:
"""Create and destroy a ThermostatOverSwitch as a test fixture"""
# fmt: off
with patch("homeassistant.core.ServiceRegistry.async_call") as mock_service_call, \
patch("homeassistant.core.StateMachine.is_state", return_value=False) as mock_is_state, \
patch("custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event") as mock_send_event, \
patch("custom_components.versatile_thermostat.keep_alive.async_track_time_interval") as mock_async_track_time_interval:
# fmt: on
thermostat = cast(ThermostatOverSwitch, await create_thermostat(
hass, config_entry, "climate.theoverswitchmockname"
))
with patch("homeassistant.core.StateMachine.get") as mock_get_state:
mock_get_state.return_value.state = "off"
yield CommonMocks(
config_entry=config_entry,
hass=hass,
thermostat=thermostat,
mock_is_state=mock_is_state,
mock_get_state=mock_get_state,
mock_service_call=mock_service_call,
mock_async_track_time_interval=mock_async_track_time_interval,
mock_send_event=mock_send_event,
)
# Clean the entity
thermostat.remove_thermostat()
class TestKeepAlive:
"""Tests for the switch keep-alive feature"""
# pylint: disable=attribute-defined-outside-init
def setup_method(self):
"""Initialise test case data before the execution of each test case method."""
self._prev_service_calls: list[_Call] = []
self._prev_atti_call_count = 0 # atti: async_time_track_interval
self._prev_atti_callback: Callable[[datetime], Awaitable[None]] | None = None
def _assert_service_call(
self, cm: CommonMocks, expected_additional_calls: list[_Call]
):
"""Assert that hass.services.async_call() was called with the expected arguments,
cumulatively over the course of long test cases."""
self._prev_service_calls.extend(expected_additional_calls)
cm.mock_service_call.assert_has_calls(self._prev_service_calls)
def _assert_async_mock_track_time_interval(
self, cm: CommonMocks, expected_additional_calls: int
):
"""Assert that async_track_time_interval() was called the expected number of times
with the expected arguments, cumulatively over the course of long test cases."""
self._prev_atti_call_count += expected_additional_calls
assert (
cm.mock_async_track_time_interval.call_count == self._prev_atti_call_count
)
interval = timedelta(seconds=cm.config_entry.data[CONF_HEATER_KEEP_ALIVE])
cm.mock_async_track_time_interval.assert_called_with(cm.hass, ANY, interval)
keep_alive_callback = cm.mock_async_track_time_interval.call_args.args[1]
assert callable(keep_alive_callback)
self._prev_atti_callback = keep_alive_callback
async def _assert_multipe_keep_alive_callback_calls(
self, cm: CommonMocks, n_calls: int
):
"""Call the keep-alive callback a few times as if `async_track_time_interval()` had
done it, and assert that this triggers further calls to `async_track_time_interval()`.
"""
old_callback = self._prev_atti_callback
assert (
old_callback
), "The keep-alive callback should have been called before, but it wasn't."
interval = timedelta(seconds=cm.config_entry.data[CONF_HEATER_KEEP_ALIVE])
for _ in range(n_calls):
await old_callback(datetime.fromtimestamp(0))
self._prev_atti_call_count += 1
assert (
cm.mock_async_track_time_interval.call_count
== self._prev_atti_call_count
)
cm.mock_async_track_time_interval.assert_called_with(cm.hass, ANY, interval)
new_callback = cm.mock_async_track_time_interval.call_args.args[1]
assert new_callback is not old_callback
assert new_callback.__qualname__ == old_callback.__qualname__
old_callback = new_callback
self._prev_atti_callback = old_callback
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_switch_keep_alive_startup(self, common_mocks: CommonMocks):
"""Test that switch keep-alive service calls are made at startup time."""
thermostat = common_mocks.thermostat
await thermostat.async_set_hvac_mode(HVACMode.HEAT)
assert thermostat.hvac_mode is HVACMode.HEAT
assert thermostat.target_temperature == 15
assert thermostat.is_device_active is False
# When the keep-alive feature is enabled, regular calls to the switch
# turn_on / turn_off methods are _scheduled_ at start up.
self._assert_async_mock_track_time_interval(common_mocks, 1)
# Those keep-alive calls are scheduled but until the callback is called,
# no service calls are made to the SERVICE_TURN_OFF home assistant service.
self._assert_service_call(common_mocks, [])
# Call the keep-alive callback a few times (as if `async_track_time_interval`
# had done it) and assert that the callback function is replaced each time.
await self._assert_multipe_keep_alive_callback_calls(common_mocks, 2)
# Every time the keep-alive callback is called, the home assistant switch
# turn on/off service should be called too.
self._assert_service_call(
common_mocks,
[
call("switch", SERVICE_TURN_OFF, {"entity_id": "switch.mock_switch"}),
call("switch", SERVICE_TURN_OFF, {"entity_id": "switch.mock_switch"}),
],
)
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_switch_keep_alive(self, common_mocks: CommonMocks):
"""Test that switch keep-alive service calls are made during thermostat operation."""
hass = common_mocks.hass
thermostat = common_mocks.thermostat
await thermostat.async_set_hvac_mode(HVACMode.HEAT)
assert thermostat.hvac_mode is HVACMode.HEAT
assert thermostat.target_temperature == 15
assert thermostat.is_device_active is False
tz = get_tz(hass) # pylint: disable=invalid-name
now = datetime.now(tz)
event_timestamp = now - timedelta(minutes=4)
# 1. Decrease the temperature to activate the heater switch
await send_temperature_change_event(thermostat, 14, event_timestamp)
# async_track_time_interval() should have been called twice: once at startup
# while the switch was turned off, and once when the switch was turned on.
self._assert_async_mock_track_time_interval(common_mocks, 2)
# The keep-alive callback hasn't been called yet, so the only service
# call so far is to SERVICE_TURN_ON as a result of the switch turn_on()
# method being called when the target temperature increased.
self._assert_service_call(
common_mocks,
[call("switch", SERVICE_TURN_ON, {"entity_id": "switch.mock_switch"})],
)
common_mocks.mock_is_state.return_value = True
# Call the keep-alive callback a few times (as if `async_track_time_interval`
# had done it) and assert that the callback function is replaced each time.
await self._assert_multipe_keep_alive_callback_calls(common_mocks, 2)
# Every time the keep-alive callback is called, the home assistant switch
# turn on/off service should be called too.
self._assert_service_call(
common_mocks,
[
call("switch", SERVICE_TURN_ON, {"entity_id": "switch.mock_switch"}),
call("switch", SERVICE_TURN_ON, {"entity_id": "switch.mock_switch"}),
],
)
# 2. Increase the temperature to deactivate the heater switch
await send_temperature_change_event(thermostat, 20, event_timestamp)
# Simulate the end of the TPI heating cycle
await thermostat._underlyings[0].turn_off() # pylint: disable=protected-access
# turn_off() should have triggered a call to `async_track_time_interval()`
self._assert_async_mock_track_time_interval(common_mocks, 1)
# turn_off() should have triggered a call to the SERVICE_TURN_OFF service.
self._assert_service_call(
common_mocks,
[call("switch", SERVICE_TURN_OFF, {"entity_id": "switch.mock_switch"})],
)
common_mocks.mock_is_state.return_value = False
# Call the keep-alive callback a few times (as if `async_track_time_interval`
# had done it) and assert that the callback function is replaced each time.
await self._assert_multipe_keep_alive_callback_calls(common_mocks, 2)
# Every time the keep-alive callback is called, the home assistant switch
# turn on/off service should be called too.
self._assert_service_call(
common_mocks,
[
call("switch", SERVICE_TURN_OFF, {"entity_id": "switch.mock_switch"}),
call("switch", SERVICE_TURN_OFF, {"entity_id": "switch.mock_switch"}),
],
)
class TestBackoffTimer:
"""Test the keep_alive.BackoffTimer helper class."""
def test_exponential_period_increase(self):
"""Test that consecutive calls to is_ready() produce increasing wait periods."""
with patch(
"custom_components.versatile_thermostat.keep_alive.monotonic"
) as mock_monotonic:
timer = BackoffTimer(
multiplier=2,
lower_limit_sec=30,
upper_limit_sec=86400,
initially_ready=True,
)
mock_monotonic.return_value = 100
assert timer.is_ready()
mock_monotonic.return_value = 129
assert not timer.is_ready()
mock_monotonic.return_value = 130
assert timer.is_ready()
mock_monotonic.return_value = 188
assert not timer.is_ready()
mock_monotonic.return_value = 189
assert not timer.is_ready()
mock_monotonic.return_value = 190
assert timer.is_ready()
mock_monotonic.return_value = 309
assert not timer.is_ready()
def test_the_upper_limit_option(self):
"""Test the timer.in_progress property and the effect of timer.reset()."""
with patch(
"custom_components.versatile_thermostat.keep_alive.monotonic"
) as mock_monotonic:
timer = BackoffTimer(
multiplier=2,
lower_limit_sec=30,
upper_limit_sec=50,
initially_ready=True,
)
mock_monotonic.return_value = 100
assert timer.is_ready()
mock_monotonic.return_value = 129
assert not timer.is_ready()
mock_monotonic.return_value = 130
assert timer.is_ready()
mock_monotonic.return_value = 178
assert not timer.is_ready()
mock_monotonic.return_value = 179
assert not timer.is_ready()
mock_monotonic.return_value = 180
assert timer.is_ready()
mock_monotonic.return_value = 229
assert not timer.is_ready()
mock_monotonic.return_value = 230
assert timer.is_ready()
def test_the_lower_limit_option(self):
"""Test the timer.in_progress property and the effect of timer.reset()."""
with patch(
"custom_components.versatile_thermostat.keep_alive.monotonic"
) as mock_monotonic:
timer = BackoffTimer(
multiplier=0.5,
lower_limit_sec=30,
upper_limit_sec=50,
initially_ready=True,
)
mock_monotonic.return_value = 100
assert timer.is_ready()
mock_monotonic.return_value = 129
assert not timer.is_ready()
mock_monotonic.return_value = 130
assert timer.is_ready()
mock_monotonic.return_value = 158
assert not timer.is_ready()
mock_monotonic.return_value = 159
assert not timer.is_ready()
mock_monotonic.return_value = 160
assert timer.is_ready()
def test_initial_is_ready_result(self):
"""Test that the first call to is_ready() produces the initially_ready option value."""
with patch(
"custom_components.versatile_thermostat.keep_alive.monotonic"
) as mock_monotonic:
for initial in [True, False]:
timer = BackoffTimer(
multiplier=2,
lower_limit_sec=30,
upper_limit_sec=86400,
initially_ready=initial,
)
mock_monotonic.return_value = 100
assert timer.is_ready() == initial
assert not timer.is_ready()
def test_in_progress_and_reset(self):
"""Test the timer.in_progress property and the effect of timer.reset()."""
with patch(
"custom_components.versatile_thermostat.keep_alive.monotonic"
) as mock_monotonic:
timer = BackoffTimer(
multiplier=2,
lower_limit_sec=30,
upper_limit_sec=86400,
initially_ready=True,
)
mock_monotonic.return_value = 100
assert not timer.in_progress
assert timer.is_ready()
assert timer.in_progress
assert not timer.is_ready()
timer.reset()
assert not timer.in_progress
assert timer.is_ready()
assert timer.in_progress
assert not timer.is_ready()

1154
tests/test_temp_number.py Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,12 @@
""" Test the TPI algorithm """ """ Test the TPI algorithm """
from homeassistant.components.climate import HVACMode
from custom_components.versatile_thermostat.base_thermostat import BaseThermostat from custom_components.versatile_thermostat.base_thermostat import BaseThermostat
from custom_components.versatile_thermostat.prop_algorithm import (
PropAlgorithm,
PROPORTIONAL_FUNCTION_TPI,
)
from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import
@@ -42,53 +48,54 @@ async def test_tpi_calculation(
hass, entry, "climate.theoverswitchmockname" hass, entry, "climate.theoverswitchmockname"
) )
assert entity assert entity
assert entity._prop_algorithm # pylint: disable=protected-access
tpi_algo = entity._prop_algorithm # pylint: disable=protected-access tpi_algo: PropAlgorithm = entity._prop_algorithm # pylint: disable=protected-access
assert tpi_algo assert tpi_algo
tpi_algo.calculate(15, 10, 7) tpi_algo.calculate(15, 10, 7, HVACMode.HEAT)
assert tpi_algo.on_percent == 1 assert tpi_algo.on_percent == 1
assert tpi_algo.calculated_on_percent == 1 assert tpi_algo.calculated_on_percent == 1
assert tpi_algo.on_time_sec == 300 assert tpi_algo.on_time_sec == 300
assert tpi_algo.off_time_sec == 0 assert tpi_algo.off_time_sec == 0
assert entity.mean_cycle_power is None # no device power configured assert entity.mean_cycle_power is None # no device power configured
tpi_algo.calculate(15, 14, 5, False) tpi_algo.calculate(15, 14, 5, HVACMode.HEAT)
assert tpi_algo.on_percent == 0.4 assert tpi_algo.on_percent == 0.4
assert tpi_algo.calculated_on_percent == 0.4 assert tpi_algo.calculated_on_percent == 0.4
assert tpi_algo.on_time_sec == 120 assert tpi_algo.on_time_sec == 120
assert tpi_algo.off_time_sec == 180 assert tpi_algo.off_time_sec == 180
tpi_algo.set_security(0.1) tpi_algo.set_security(0.1)
tpi_algo.calculate(15, 14, 5, False) tpi_algo.calculate(15, 14, 5, HVACMode.HEAT)
assert tpi_algo.on_percent == 0.1 assert tpi_algo.on_percent == 0.1
assert tpi_algo.calculated_on_percent == 0.4 assert tpi_algo.calculated_on_percent == 0.4
assert tpi_algo.on_time_sec == 30 # >= minimal_activation_delay (=30) assert tpi_algo.on_time_sec == 30 # >= minimal_activation_delay (=30)
assert tpi_algo.off_time_sec == 270 assert tpi_algo.off_time_sec == 270
tpi_algo.unset_security() tpi_algo.unset_security()
tpi_algo.calculate(15, 14, 5, False) tpi_algo.calculate(15, 14, 5, HVACMode.HEAT)
assert tpi_algo.on_percent == 0.4 assert tpi_algo.on_percent == 0.4
assert tpi_algo.calculated_on_percent == 0.4 assert tpi_algo.calculated_on_percent == 0.4
assert tpi_algo.on_time_sec == 120 assert tpi_algo.on_time_sec == 120
assert tpi_algo.off_time_sec == 180 assert tpi_algo.off_time_sec == 180
# Test minimal activation delay # Test minimal activation delay
tpi_algo.calculate(15, 14.7, 15, False) tpi_algo.calculate(15, 14.7, 15, HVACMode.HEAT)
assert tpi_algo.on_percent == 0.09 assert tpi_algo.on_percent == 0.09
assert tpi_algo.calculated_on_percent == 0.09 assert tpi_algo.calculated_on_percent == 0.09
assert tpi_algo.on_time_sec == 0 assert tpi_algo.on_time_sec == 0
assert tpi_algo.off_time_sec == 300 assert tpi_algo.off_time_sec == 300
tpi_algo.set_security(0.09) tpi_algo.set_security(0.09)
tpi_algo.calculate(15, 14.7, 15, False) tpi_algo.calculate(15, 14.7, 15, HVACMode.HEAT)
assert tpi_algo.on_percent == 0.09 assert tpi_algo.on_percent == 0.09
assert tpi_algo.calculated_on_percent == 0.09 assert tpi_algo.calculated_on_percent == 0.09
assert tpi_algo.on_time_sec == 0 assert tpi_algo.on_time_sec == 0
assert tpi_algo.off_time_sec == 300 assert tpi_algo.off_time_sec == 300
tpi_algo.unset_security() tpi_algo.unset_security()
tpi_algo.calculate(25, 30, 35, True) tpi_algo.calculate(25, 30, 35, HVACMode.COOL)
assert tpi_algo.on_percent == 1 assert tpi_algo.on_percent == 1
assert tpi_algo.calculated_on_percent == 1 assert tpi_algo.calculated_on_percent == 1
assert tpi_algo.on_time_sec == 300 assert tpi_algo.on_time_sec == 300
@@ -96,9 +103,144 @@ async def test_tpi_calculation(
assert entity.mean_cycle_power is None # no device power configured assert entity.mean_cycle_power is None # no device power configured
tpi_algo.set_security(0.09) tpi_algo.set_security(0.09)
tpi_algo.calculate(25, 30, 35, True) tpi_algo.calculate(25, 30, 35, HVACMode.COOL)
assert tpi_algo.on_percent == 0.09 assert tpi_algo.on_percent == 0.09
assert tpi_algo.calculated_on_percent == 1 assert tpi_algo.calculated_on_percent == 1
assert tpi_algo.on_time_sec == 0 assert tpi_algo.on_time_sec == 0
assert tpi_algo.off_time_sec == 300 assert tpi_algo.off_time_sec == 300
assert entity.mean_cycle_power is None # no device power configured assert entity.mean_cycle_power is None # no device power configured
tpi_algo.unset_security()
# The calculated values for HVACMode.OFF are the same as for HVACMode.HEAT.
tpi_algo.calculate(15, 10, 7, HVACMode.OFF)
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
# If target_temp or current_temp are None, _calculated_on_percent is set to 0.
tpi_algo.calculate(15, None, 7, HVACMode.OFF)
assert tpi_algo.on_percent == 0
assert tpi_algo.calculated_on_percent == 0
assert tpi_algo.on_time_sec == 0
assert tpi_algo.off_time_sec == 300
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_wrong_tpi_parameters(
hass: HomeAssistant, skip_hass_states_is_state: None
): # pylint: disable=unused-argument
"""Test the wrong TPI parameters"""
# Nominal case
try:
algo = PropAlgorithm(
PROPORTIONAL_FUNCTION_TPI,
0.6,
0.01,
5,
1,
"entity_id",
)
# We should not be there
assert True
except TypeError as e:
# the normal case
assert False
# Test TPI function
try:
algo = PropAlgorithm(
"WRONG",
1,
0,
2,
3,
"entity_id",
)
# We should not be there
assert False
except TypeError as e:
# the normal case
pass
# Test coef_int
try:
algo = PropAlgorithm(
PROPORTIONAL_FUNCTION_TPI,
None,
0,
2,
3,
"entity_id",
)
# We should not be there
assert False
except TypeError as e:
# the normal case
pass
# Test coef_ext
try:
algo = PropAlgorithm(
PROPORTIONAL_FUNCTION_TPI,
0.6,
None,
2,
3,
"entity_id",
)
# We should not be there
assert False
except TypeError as e:
# the normal case
pass
# Test cycle_min
try:
algo = PropAlgorithm(
PROPORTIONAL_FUNCTION_TPI,
0.6,
0.00001,
None,
3,
"entity_id",
)
# We should not be there
assert False
except TypeError as e:
# the normal case
pass
# Test minimal_activation_delay
try:
algo = PropAlgorithm(
PROPORTIONAL_FUNCTION_TPI,
0.6,
0.00001,
0,
None,
"entity_id",
)
# We should not be there
assert False
except TypeError as e:
# the normal case
pass
# Test vtherm_entity_id
try:
algo = PropAlgorithm(
PROPORTIONAL_FUNCTION_TPI,
0.6,
0.00001,
0,
12,
None,
)
# We should not be there
assert False
except TypeError as e:
# the normal case
pass

View File

@@ -1,4 +1,4 @@
# pylint: disable=line-too-long # pylint: disable=line-too-long, disable=protected-access
""" Test the normal start of a Switch AC Thermostat """ """ Test the normal start of a Switch AC Thermostat """
from unittest.mock import patch, call from unittest.mock import patch, call
@@ -37,10 +37,10 @@ async def test_over_valve_full_start(
CONF_CYCLE_MIN: 5, CONF_CYCLE_MIN: 5,
CONF_TEMP_MIN: 15, CONF_TEMP_MIN: 15,
CONF_TEMP_MAX: 30, CONF_TEMP_MAX: 30,
PRESET_FROST_PROTECTION + "_temp": 7, PRESET_FROST_PROTECTION + PRESET_TEMP_SUFFIX: 7,
PRESET_ECO + "_temp": 17, PRESET_ECO + PRESET_TEMP_SUFFIX: 17,
PRESET_COMFORT + "_temp": 19, PRESET_COMFORT + PRESET_TEMP_SUFFIX: 19,
PRESET_BOOST + "_temp": 21, PRESET_BOOST + PRESET_TEMP_SUFFIX: 21,
CONF_USE_WINDOW_FEATURE: True, CONF_USE_WINDOW_FEATURE: True,
CONF_USE_MOTION_FEATURE: True, CONF_USE_MOTION_FEATURE: True,
CONF_USE_POWER_FEATURE: True, CONF_USE_POWER_FEATURE: True,
@@ -58,10 +58,10 @@ async def test_over_valve_full_start(
CONF_POWER_SENSOR: "sensor.power_sensor", CONF_POWER_SENSOR: "sensor.power_sensor",
CONF_MAX_POWER_SENSOR: "sensor.power_max_sensor", CONF_MAX_POWER_SENSOR: "sensor.power_max_sensor",
CONF_PRESENCE_SENSOR: "person.presence_sensor", CONF_PRESENCE_SENSOR: "person.presence_sensor",
PRESET_FROST_PROTECTION + PRESET_AWAY_SUFFIX + "_temp": 7, PRESET_FROST_PROTECTION + PRESET_AWAY_SUFFIX + PRESET_TEMP_SUFFIX: 7,
PRESET_ECO + PRESET_AWAY_SUFFIX + "_temp": 17.1, PRESET_ECO + PRESET_AWAY_SUFFIX + PRESET_TEMP_SUFFIX: 17.1,
PRESET_COMFORT + PRESET_AWAY_SUFFIX + "_temp": 17.2, PRESET_COMFORT + PRESET_AWAY_SUFFIX + PRESET_TEMP_SUFFIX: 17.2,
PRESET_BOOST + PRESET_AWAY_SUFFIX + "_temp": 17.3, PRESET_BOOST + PRESET_AWAY_SUFFIX + PRESET_TEMP_SUFFIX: 17.3,
CONF_PRESET_POWER: 10, CONF_PRESET_POWER: 10,
CONF_MINIMAL_ACTIVATION_DELAY: 30, CONF_MINIMAL_ACTIVATION_DELAY: 30,
CONF_SECURITY_DELAY_MIN: 5, CONF_SECURITY_DELAY_MIN: 5,
@@ -74,22 +74,12 @@ async def test_over_valve_full_start(
tz = get_tz(hass) # pylint: disable=invalid-name tz = get_tz(hass) # pylint: disable=invalid-name
now: datetime = datetime.now(tz=tz) now: datetime = datetime.now(tz=tz)
# 1. create the entity
with patch( with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event" "custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event: ) as mock_send_event:
entry.add_to_hass(hass)
await hass.config_entries.async_setup(entry.entry_id)
assert entry.state is ConfigEntryState.LOADED
def find_my_entity(entity_id) -> ClimateEntity: entity = await create_thermostat(hass, entry, "climate.theovervalvemockname")
"""Find my new entity"""
component: EntityComponent[ClimateEntity] = hass.data[CLIMATE_DOMAIN]
for entity in component.entities:
if entity.entity_id == entity_id:
return entity
# The name is in the CONF and not the title of the entry
entity: ThermostatOverValve = find_my_entity("climate.theovervalvemockname")
assert entity assert entity
assert isinstance(entity, ThermostatOverValve) assert isinstance(entity, ThermostatOverValve)
@@ -119,7 +109,7 @@ async def test_over_valve_full_start(
assert entity._prop_algorithm is not None # pylint: disable=protected-access assert entity._prop_algorithm is not None # pylint: disable=protected-access
# should have been called with EventType.PRESET_EVENT and EventType.HVAC_MODE_EVENT # should have been called with EventType.PRESET_EVENT and EventType.HVAC_MODE_EVENT
assert mock_send_event.call_count == 2 # assert mock_send_event.call_count == 2
mock_send_event.assert_has_calls( mock_send_event.assert_has_calls(
[ [
call.send_event(EventType.PRESET_EVENT, {"preset": PRESET_NONE}), call.send_event(EventType.PRESET_EVENT, {"preset": PRESET_NONE}),
@@ -130,7 +120,7 @@ async def test_over_valve_full_start(
] ]
) )
# Set the HVACMode to HEAT, with manual preset and target_temp to 18 before receiving temperature # 2. Set the HVACMode to HEAT, with manual preset and target_temp to 18 before receiving temperature
with patch( with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event" "custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event: ) as mock_send_event:
@@ -158,7 +148,7 @@ async def test_over_valve_full_start(
# Nothing have changed cause we don't have room and external temperature # Nothing have changed cause we don't have room and external temperature
assert mock_send_event.call_count == 1 assert mock_send_event.call_count == 1
# Set temperature and external temperature # 3. Set temperature and external temperature
with patch( with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event" "custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event, patch( ) as mock_send_event, patch(
@@ -181,14 +171,18 @@ async def test_over_valve_full_start(
mock_service_call.assert_has_calls( mock_service_call.assert_has_calls(
[ [
call.async_call( call.async_call(
"number", domain="number",
"set_value", service="set_value",
{"entity_id": "number.mock_valve", "value": 90}, service_data={"value": 90},
target={"entity_id": "number.mock_valve"},
# {"entity_id": "number.mock_valve", "value": 90},
), ),
call.async_call( call.async_call(
"number", domain="number",
"set_value", service="set_value",
{"entity_id": "number.mock_valve", "value": 98}, service_data={"value": 98},
target={"entity_id": "number.mock_valve"},
# {"entity_id": "number.mock_valve", "value": 98},
), ),
] ]
) )
@@ -196,15 +190,18 @@ async def test_over_valve_full_start(
assert mock_send_event.call_count == 0 assert mock_send_event.call_count == 0
# Change to preset Comfort # Change to preset Comfort
# Change presence to off
event_timestamp = now - timedelta(minutes=4)
await send_presence_change_event(entity, False, True, event_timestamp)
await entity.async_set_preset_mode(preset_mode=PRESET_COMFORT) await entity.async_set_preset_mode(preset_mode=PRESET_COMFORT)
assert entity.preset_mode == PRESET_COMFORT assert entity.preset_mode == PRESET_COMFORT
assert entity.target_temperature == 17.2 assert entity.target_temperature == 17.2 # Comfort with presence off
assert entity.valve_open_percent == 73 assert entity.valve_open_percent == 73
assert entity.is_device_active is True assert entity.is_device_active is True
assert entity.hvac_action == HVACAction.HEATING assert entity.hvac_action == HVACAction.HEATING
# Change presence to on # Change presence to on
event_timestamp = now - timedelta(minutes=4) event_timestamp = now - timedelta(minutes=3)
await send_presence_change_event(entity, True, False, event_timestamp) await send_presence_change_event(entity, True, False, event_timestamp)
assert entity.presence_state == STATE_ON # pylint: disable=protected-access assert entity.presence_state == STATE_ON # pylint: disable=protected-access
assert entity.preset_mode is PRESET_COMFORT assert entity.preset_mode is PRESET_COMFORT
@@ -213,7 +210,7 @@ async def test_over_valve_full_start(
assert entity.is_device_active is True assert entity.is_device_active is True
assert entity.hvac_action == HVACAction.HEATING assert entity.hvac_action == HVACAction.HEATING
# Change internal temperature # 4. Change internal temperature
expected_state = State( expected_state = State(
entity_id="number.mock_valve", state="0", attributes={"min": 10, "max": 50} entity_id="number.mock_valve", state="0", attributes={"min": 10, "max": 50}
) )
@@ -225,7 +222,7 @@ async def test_over_valve_full_start(
) as mock_service_call, patch( ) as mock_service_call, patch(
"homeassistant.core.StateMachine.get", return_value=expected_state "homeassistant.core.StateMachine.get", return_value=expected_state
): ):
event_timestamp = now - timedelta(minutes=3) event_timestamp = now - timedelta(minutes=2)
await send_temperature_change_event(entity, 20, datetime.now()) await send_temperature_change_event(entity, 20, datetime.now())
assert entity.valve_open_percent == 0 assert entity.valve_open_percent == 0
assert entity.is_device_active is True # Should be 0 but in fact 10 is send assert entity.is_device_active is True # Should be 0 but in fact 10 is send
@@ -238,9 +235,10 @@ async def test_over_valve_full_start(
mock_service_call.assert_has_calls( mock_service_call.assert_has_calls(
[ [
call.async_call( call.async_call(
"number", domain="number",
"set_value", service="set_value",
{"entity_id": "number.mock_valve", "value": 10}, service_data={"value": 10},
target={"entity_id": "number.mock_valve"},
) )
] ]
) )
@@ -251,20 +249,18 @@ async def test_over_valve_full_start(
mock_service_call.assert_has_calls( mock_service_call.assert_has_calls(
[ [
call.async_call( call.async_call(
"number", domain="number",
"set_value", service="set_value",
{ service_data={"value": 10},
"entity_id": "number.mock_valve", target={"entity_id": "number.mock_valve"}, # the min allowed value
"value": 10,
}, # the min allowed value
), ),
call.async_call( call.async_call(
"number", domain="number",
"set_value", service="set_value",
{ service_data={
"entity_id": "number.mock_valve", "value": 34
"value": 50, }, # 34 is 50 x open_percent (69%) and is the max allowed value
}, # the max allowed value target={"entity_id": "number.mock_valve"},
), ),
] ]
) )
@@ -275,7 +271,7 @@ async def test_over_valve_full_start(
assert entity.valve_open_percent == 7 assert entity.valve_open_percent == 7
# Unset the presence # Unset the presence
event_timestamp = now - timedelta(minutes=2) event_timestamp = now - timedelta(minutes=1)
await send_presence_change_event(entity, False, True, event_timestamp) await send_presence_change_event(entity, False, True, event_timestamp)
assert entity.presence_state == STATE_OFF # pylint: disable=protected-access assert entity.presence_state == STATE_OFF # pylint: disable=protected-access
assert entity.valve_open_percent == 10 assert entity.valve_open_percent == 10
@@ -283,9 +279,9 @@ async def test_over_valve_full_start(
assert entity.is_device_active is True assert entity.is_device_active is True
assert entity.hvac_action == HVACAction.HEATING assert entity.hvac_action == HVACAction.HEATING
# Test window open/close (with a normal min/max so that is_device_active is False when open_percent is 0) # 5. Test window open/close (with a normal min/max so that is_device_active is False when open_percent is 0)
expected_state = State( expected_state = State(
entity_id="number.mock_valve", state="0", attributes={"min": 0, "max": 99} entity_id="number.mock_valve", state="0", attributes={"min": 0, "max": 255}
) )
with patch( with patch(
@@ -321,9 +317,237 @@ async def test_over_valve_full_start(
await try_condition(None) await try_condition(None)
assert entity.hvac_mode is HVACMode.HEAT assert entity.hvac_mode is HVACMode.HEAT
assert ( assert entity.hvac_action is HVACAction.HEATING
entity.hvac_action is HVACAction.OFF
or entity.hvac_action is HVACAction.IDLE
)
assert entity.target_temperature == 17.1 # eco assert entity.target_temperature == 17.1 # eco
assert entity.valve_open_percent == 10 assert entity.valve_open_percent == 10
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_over_valve_regulation(
hass: HomeAssistant, skip_hass_states_is_state
): # pylint: disable=unused-argument
"""Test the normal full start of a thermostat in thermostat_over_switch type"""
entry = MockConfigEntry(
domain=DOMAIN,
title="TheOverValveMockName",
unique_id="uniqueId",
data={
CONF_NAME: "TheOverValveMockName",
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_VALVE,
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
CONF_VALVE: "number.mock_valve",
CONF_CYCLE_MIN: 5,
CONF_TEMP_MIN: 15,
CONF_TEMP_MAX: 30,
PRESET_FROST_PROTECTION + PRESET_TEMP_SUFFIX: 7,
PRESET_ECO + PRESET_TEMP_SUFFIX: 17,
PRESET_COMFORT + PRESET_TEMP_SUFFIX: 19,
PRESET_BOOST + PRESET_TEMP_SUFFIX: 21,
CONF_USE_WINDOW_FEATURE: False,
CONF_USE_MOTION_FEATURE: False,
CONF_USE_POWER_FEATURE: False,
CONF_USE_PRESENCE_FEATURE: False,
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: 60,
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
# only send new valve open percent if dtemp is > 30%
CONF_AUTO_REGULATION_DTEMP: 5,
# only send new valve open percent last mesure was more than 5 min ago
CONF_AUTO_REGULATION_PERIOD_MIN: 5,
},
)
tz = get_tz(hass) # pylint: disable=invalid-name
now: datetime = datetime.now(tz=tz)
# 1. prepare the Valve at now
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event:
entity: ThermostatOverValve = await create_thermostat(
hass, entry, "climate.theovervalvemockname"
)
assert entity
assert isinstance(entity, ThermostatOverValve)
assert entity.name == "TheOverValveMockName"
assert entity.is_over_valve is True
assert entity._auto_regulation_dpercent == 5
assert entity._auto_regulation_period_min == 5
assert entity.target_temperature == entity.min_temp
assert entity._prop_algorithm is not None
# 2. Set the HVACMode to HEAT, with manual preset and target_temp to 18 before receiving temperature
# at now +1
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event:
now = now + timedelta(minutes=1)
entity._set_now(now)
# Select a hvacmode, presence and preset
await entity.async_set_hvac_mode(HVACMode.HEAT)
assert entity.hvac_mode is HVACMode.HEAT
# No heating now
assert entity.valve_open_percent == 0
assert entity.hvac_action == HVACAction.IDLE
assert mock_send_event.call_count == 1
mock_send_event.assert_has_calls(
[
call.send_event(
EventType.HVAC_MODE_EVENT,
{"hvac_mode": HVACMode.HEAT},
),
]
)
# 3. Set the preset
# at now +1
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event:
now = now + timedelta(minutes=1)
entity._set_now(now)
# set preset
await entity.async_set_preset_mode(PRESET_BOOST)
assert entity.preset_mode == PRESET_BOOST
assert entity.target_temperature == 21
# the preset have changed
assert mock_send_event.call_count == 1
mock_send_event.assert_has_calls(
[
call.send_event(
EventType.PRESET_EVENT,
{"preset": PRESET_BOOST},
),
]
)
await entity.async_set_hvac_mode(HVACMode.HEAT)
assert entity.hvac_mode is HVACMode.HEAT
# Still no heating because we don't have temperature
assert entity.valve_open_percent == 0
assert entity.hvac_action == HVACAction.IDLE
# 4. Set temperature and external temperature
# at now + 1 (but the _last_calculation_timestamp is still not send)
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event, patch(
"homeassistant.core.ServiceRegistry.async_call"
) as mock_service_call, patch(
"homeassistant.core.StateMachine.get",
return_value=State(entity_id="number.mock_valve", state="90"),
):
# Change temperature
now = now + timedelta(minutes=1)
entity._set_now(now)
await send_temperature_change_event(entity, 18, now)
assert entity.valve_open_percent == 90
assert entity.is_device_active is True
assert entity.hvac_action == HVACAction.HEATING
assert mock_service_call.call_count == 1
mock_service_call.assert_has_calls(
[
call.async_call(
domain="number",
service="set_value",
service_data={"value": 90},
target={"entity_id": "number.mock_valve"},
),
]
)
assert mock_send_event.call_count == 0
# 5. Set external temperature
# at now + 1
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event, patch(
"homeassistant.core.ServiceRegistry.async_call"
) as mock_service_call, patch(
"homeassistant.core.StateMachine.get",
return_value=State(entity_id="number.mock_valve", state="90"),
):
# Change external temperature
now = now + timedelta(minutes=1)
entity._set_now(now)
await send_ext_temperature_change_event(entity, 10, now)
# Should not have change due to regulation (period_min !)
assert entity.valve_open_percent == 90
assert entity.is_device_active is True
assert entity.hvac_action == HVACAction.HEATING
assert mock_service_call.call_count == 0
assert mock_send_event.call_count == 0
# 6. Set temperature
# at now + 5 (to avoid the period_min threshold)
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event, patch(
"homeassistant.core.ServiceRegistry.async_call"
) as mock_service_call, patch(
"homeassistant.core.StateMachine.get",
return_value=State(entity_id="number.mock_valve", state="90"),
):
# Change external temperature
now = now + timedelta(minutes=5)
entity._set_now(now)
await send_ext_temperature_change_event(entity, 15, now)
# Should have change this time to 96
assert entity.valve_open_percent == 96
assert entity.is_device_active is True
assert entity.hvac_action == HVACAction.HEATING
assert mock_service_call.call_count == 1
mock_service_call.assert_has_calls(
[
call.async_call(
domain="number",
service="set_value",
service_data={"value": 96},
target={"entity_id": "number.mock_valve"},
),
]
)
assert mock_send_event.call_count == 0
# 7. Set small temperature update to test dtemp threshold
# at now + 5
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event, patch(
"homeassistant.core.ServiceRegistry.async_call"
) as mock_service_call, patch(
"homeassistant.core.StateMachine.get",
return_value=State(entity_id="number.mock_valve", state="96"),
):
# Change external temperature
now = now + timedelta(minutes=5)
entity._set_now(now)
# this generate a delta percent of -3
await send_temperature_change_event(entity, 18.1, now)
# Should not have due to dtemp
assert entity.valve_open_percent == 96
assert entity.is_device_active is True
assert entity.hvac_action == HVACAction.HEATING
assert mock_service_call.call_count == 0
assert mock_send_event.call_count == 0

File diff suppressed because it is too large Load Diff