Compare commits

..

116 Commits

Author SHA1 Message Date
Jean-Marc Collin
9102b09691 Calculate offset_calibration as room_temp - local_temp
Fix hvac_action calculation
2024-11-19 06:58:20 +00:00
Jean-Marc Collin
5b64a8fba0 Add #602 - implement a max_on_percent setting 2024-11-18 10:04:04 +00:00
Jean-Marc Collin
14bb7474ae Fix release name 2024-11-18 10:04:04 +00:00
Jean-Marc Collin
bc1c18d719 Release 2024-11-18 10:04:03 +00:00
Jean-Marc Collin
c935ee3af5 Fix Testus 2024-11-18 10:04:02 +00:00
Jean-Marc Collin
ce73f1275b Work in simuated environment 2024-11-18 10:04:01 +00:00
Jean-Marc Collin
cd08dca913 With 1rst implementation of VTherm TRVZB and underlying 2024-11-18 10:02:18 +00:00
Jean-Marc Collin
93079e974f Next (not finished) 2024-11-18 10:02:17 +00:00
Jean-Marc Collin
db38392dab Fix configuration 2024-11-18 10:02:16 +00:00
Jean-Marc Collin
ac705df862 With Sonoff configuration ok 2024-11-18 10:02:15 +00:00
ms5
289ccc7bb7 Implementing max_on_percent setting (#632)
* implementing max_on_percent setting

* remove % sign from log message

* README updated: created new export-mode section, moved self-regulation expert settings to new section, added new section about on-time clamping
2024-11-17 18:28:24 +01:00
Jean-Marc Collin
c1d1e8f1db Fix safety mode doc 2024-11-16 09:33:48 +00:00
Gernot Messow
71c35ecdc0 Fixed and extended unit test (#637) 2024-11-14 22:29:04 +01:00
Gernot Messow
4f8e45dda6 Just ignore illegal target temp, do not throw away all data (#635)
Co-authored-by: Gernot Messow <gmessow@insys-locks.de>
2024-11-14 21:54:15 +01:00
Jean-Marc Collin
d624c327b6 Issue #552 (#627)
Co-authored-by: Jean-Marc Collin <jean-marc.collin-extern@renault.com>
2024-11-13 19:14:22 +01:00
Jean-Marc Collin
b46a24f834 Issue #628 add follow underlying temp change entity (#630)
* First commit (no test)

* With tests ok

---------

Co-authored-by: Jean-Marc Collin <jean-marc.collin-extern@renault.com>
2024-11-13 19:14:03 +01:00
Jean-Marc Collin
d31376d55d Add Overkiz incompatilibity 2024-11-10 15:29:35 +00:00
Jean-Marc Collin
dbfd294ff3 Issue #496 - precision on safety parameters for over_climate 2024-11-10 10:03:33 +00:00
Jean-Marc Collin
e111bd0647 Removes most of the collapsible section in README. 2024-11-10 09:48:45 +00:00
Jean-Marc Collin
ba69319198 Issue #619 - manual hvac_off should be prioritized over window and auto-start/stop hvac_off (#622)
Co-authored-by: Jean-Marc Collin <jean-marc.collin-extern@renault.com>
2024-11-10 10:17:53 +01:00
Jean-Marc Collin
f9df925181 Issue #615 - VTherm switch to manual on its own (#618)
Co-authored-by: Jean-Marc Collin <jean-marc.collin-extern@renault.com>
2024-11-09 18:44:13 +01:00
Jean-Marc Collin
2d72efe447 Issue 600 energy can be negative after configuration (#614)
* Add logs to diagnose the case

* Issue #552 (#608)

Co-authored-by: Jean-Marc Collin <jean-marc.collin-extern@renault.com>

* Fix typo (#607)

* - Force writing state when entity is removed
- Fix bug with issue #552 on CONF_USE_CENTRAL_BOILER_FEATURE which should be proposed on a central configuration
- Improve reload of entity to avoid reloading all VTherm. Only the reconfigured one will be reloaded

---------

Co-authored-by: Jean-Marc Collin <jean-marc.collin-extern@renault.com>
Co-authored-by: Ludovic BOUÉ <lboue@users.noreply.github.com>
2024-11-07 21:57:08 +01:00
Ludovic BOUÉ
95af6eba97 Fix typo (#607) 2024-11-05 22:47:42 +01:00
Jean-Marc Collin
06dc537767 Issue #552 (#608)
Co-authored-by: Jean-Marc Collin <jean-marc.collin-extern@renault.com>
2024-11-05 22:39:26 +01:00
Joeri Colman
2d79d961dc Update en.json (#604)
fixed typo
2024-11-05 10:40:49 +01:00
Jean-Marc Collin
027bf8386b Add message into issue template. 2024-11-05 06:37:27 +00:00
Jean-Marc Collin
a0e548ef71 Release 2024-11-03 22:16:15 +00:00
Jean-Marc Collin
132519b471 Merge #590 and fix some tests 2024-11-03 22:12:56 +00:00
hilburn
e6c330fc9d Underlying config changes (#590)
* Changes config_flow to allow dynamic length list of underlying entities
Updates previously defined 4x entries to new config style
Changes to thermostat_X to load underlying entities from list
Changes to thermostat X to display underlying entities as a list - COULD BREAK EXISTING TEMPLATES

* Modifies tests to use the new list format

* Added English translation for UI

* Removed all references to individual entities in strings/en.json

* Fix merge mistake

---------

Co-authored-by: Jean-Marc Collin <jm.collin.78@gmail.com>
2024-11-03 22:52:19 +01:00
Jean-Marc Collin
968e8286ea Add some infos 2024-11-03 21:50:42 +00:00
hilburn
0f60c070ab Preset display tweaks (#599)
* Addded Frost Preset to translations
Added Icons for Shedding, Safety, Manual and Frost Presets

* Fixed French Translation
2024-11-03 11:50:37 +01:00
Jean-Marc Collin
810430f7b1 Update README.md
#597
2024-11-02 19:16:24 +01:00
Jean-Marc Collin
b4860c2b8d Issue 585 add auto start/stop feature (#594)
* Migrate to HA 2024.10.4

* Auto start/stop alog and testu + ConfigFlow

* With config flow ok

* Change algo

* All is fine

* Add change_preset test

* + comment

* FIX too much start/stop

* Change algo to take slop into account

* Allow calculation even if slope is None

* With enable + tests + hysteresis in calculation

* Add hvac_off_reason and test with window interaction

* Fix some tests

* Restore saved_state

* Release

---------

Co-authored-by: Jean-Marc Collin <jean-marc.collin-extern@renault.com>
2024-11-01 18:54:22 +01:00
Gernot Messow
60bd522a97 Filter out-of-range target temperature sent from underlying climate devices (#581)
* Filter out-of-range temperature from underlying climate

* Fixed broken test case, added new test case for range filtering
2024-10-27 09:21:08 +01:00
Jean-Marc Collin
fc39cf5f40 Maia suggestion to README 2024-10-26 11:27:16 +02:00
Jean-Marc Collin
f6fb7487d5 Issue #467 - Always apply offset compensation (#567)
Co-authored-by: Jean-Marc Collin <jean-marc.collin-extern@renault.com>
2024-10-16 19:33:55 +02:00
Jean-Marc Collin
0f585be0c9 issue #556 - enhance motion detection feature (2) 2024-10-16 05:08:57 +00:00
Jean-Marc Collin
492c95aff5 FIX issue #556 - enhance motion detection feature (#560)
Co-authored-by: Jean-Marc Collin <jean-marc.collin-extern@renault.com>
2024-10-14 20:14:51 +02:00
Jean-Marc Collin
a530051bbd FIX #518 TypeError: unsupported operand type(s) for -: 'int' and 'NoneType' (#559)
Co-authored-by: Jean-Marc Collin <jean-marc.collin-extern@renault.com>
2024-10-14 19:29:34 +02:00
Jean-Marc Collin
4ef82af8ce Merge branch 'issue_554-simulate-hvac-action' 2024-10-14 17:01:01 +00:00
Jean-Marc Collin
2ea5cf471b Cleaning 2024-10-14 16:58:18 +00:00
Jean-Marc Collin
f6afaf2715 with local tests ok. (#555)
Co-authored-by: Jean-Marc Collin <jean-marc.collin-extern@renault.com>
2024-10-14 09:02:50 +02:00
Jean-Marc Collin
f29b2f9b81 with local tests ok. 2024-10-14 07:01:27 +00:00
Jean-Marc Collin
de9b95903e Add testu 2024-10-14 04:56:12 +00:00
Jean-Marc Collin
d112273c58 Fix preset temp is sommetimes lost on over_climate 2024-10-14 04:43:19 +00:00
Jean-Marc Collin
73a9ca4e53 Issue #478 vtherm doesn't follow underlying (#548)
* Dispatch test_bugs into each own VTherm type tests

* Local tests ok

* With testus ok.

---------

Co-authored-by: Jean-Marc Collin <jean-marc.collin-extern@renault.com>
2024-10-13 11:30:04 +02:00
Jean-Marc Collin
1334bdbd8f FIX #465 and make this coherent with Windows open/close (#545)
Co-authored-by: Jean-Marc Collin <jean-marc.collin-extern@renault.com>
2024-10-12 17:32:05 +02:00
Jean-Marc Collin
646ef47f6f Fix issue #485 (#544)
Co-authored-by: Jean-Marc Collin <jean-marc.collin-extern@renault.com>
2024-10-12 12:50:12 +02:00
Jean-Marc Collin
c344c43185 FIX #518 (#543)
Co-authored-by: Jean-Marc Collin <jean-marc.collin-extern@renault.com>
2024-10-12 11:02:24 +02:00
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
80 changed files with 13233 additions and 3451 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,14 +1,30 @@
default_config:
# ffmeg
ffmpeg:
recorder:
auto_purge: true
purge_keep_days: 1
commit_interval: 5
include:
domains:
- input_boolean
- input_number
- switch
- climate
- sensor
- binary_sensor
- number
- input_select
- versatile_thermostat
logger:
default: info
default: warning
logs:
custom_components.versatile_thermostat: debug
custom_components.versatile_thermostat.underlyings: debug
custom_components.versatile_thermostat.climate: debug
# custom_components.versatile_thermostat.underlyings: info
# custom_components.versatile_thermostat.climate: info
# custom_components.versatile_thermostat.base_thermostat: debug
custom_components.versatile_thermostat.sensor: info
custom_components.versatile_thermostat.binary_sensor: info
# If you need to debug uncommment the line below (doc: https://www.home-assistant.io/integrations/debugpy/)
debugpy:
@@ -75,6 +91,48 @@ input_number:
icon: mdi:thermostat
unit_of_measurement: °C
mode: box
fake_offset_calibration1:
name: Sonoff offset calibration 1
min: -12
max: 12
icon: mdi:tune
unit_of_measurement: °C
mode: box
fake_opening_degree1:
name: Sonoff Opening degree 1
min: 0
max: 100
icon: mdi:valve-open
unit_of_measurement: "%"
mode: box
fake_closing_degree1:
name: Sonoff Closing degree 1
min: 0
max: 100
icon: mdi:valve-closed
unit_of_measurement: "%"
mode: box
fake_offset_calibration2:
name: Sonoff offset calibration 2
min: -12
max: 12
icon: mdi:tune
unit_of_measurement: °C
mode: box
fake_opening_degree2:
name: Sonoff Opening degree 2
min: 0
max: 100
icon: mdi:valve-open
unit_of_measurement: "%"
mode: box
fake_closing_degree2:
name: Sonoff Closing degree 2
min: 0
max: 100
icon: mdi:valve-closed
unit_of_measurement: "%"
mode: box
input_boolean:
# input_boolean to simulate the windows entity. Only for development environment.
@@ -126,6 +184,12 @@ input_boolean:
fake_presence_sensor1:
name: Presence Sensor 1
icon: mdi:home
fake_valve_sonoff_trvzb1:
name: Valve Sonoff TRVZB1
icon: mdi:valve
fake_valve_sonoff_trvzb2:
name: Valve Sonoff TRVZB2
icon: mdi:valve
climate:
- platform: generic_thermostat
@@ -136,6 +200,7 @@ climate:
name: Underlying thermostat2
heater: input_boolean.fake_heater_switch3
target_sensor: input_number.fake_temperature_sensor1
ac_mode: false
- platform: generic_thermostat
name: Underlying thermostat3
heater: input_boolean.fake_heater_switch3
@@ -168,17 +233,23 @@ climate:
name: Underlying thermostat9
heater: input_boolean.fake_heater_switch3
target_sensor: input_number.fake_temperature_sensor1
- platform: generic_thermostat
name: Underlying Sonoff TRVZB1
heater: input_boolean.fake_valve_sonoff_trvzb1
target_sensor: input_number.fake_temperature_sensor1
ac_mode: false
- platform: generic_thermostat
name: Underlying Sonoff TRVZB2
heater: input_boolean.fake_valve_sonoff_trvzb2
target_sensor: input_number.fake_temperature_sensor1
ac_mode: false
recorder:
commit_interval: 1
include:
domains:
- input_boolean
- input_number
- switch
- climate
- sensor
- binary_sensor
input_datetime:
fake_last_seen:
name: Last seen temp sensor
icon: mdi:update
has_date: true
has_time: true
template:
- binary_sensor:
@@ -225,14 +296,14 @@ switch:
friendly_name: "Pilote chauffage SDB RDC"
value_template: "{{ is_state_attr('switch_seche_serviettes_sdb_rdc', 'sensor_state', 'on') }}"
turn_on:
service: select.select_option
action: select.select_option
data:
option: comfort
target:
entity_id: select.seche_serviettes_sdb_rdc_cable_outlet_mode
turn_off:
service: select.select_option
action: select.select_option
data:
option: comfort-2
target:

View File

@@ -1,7 +1,9 @@
// See https://aka.ms/vscode-remote/devcontainer.json for format details.
// "image": "ghcr.io/ludeeus/devcontainer/integration:latest",
{
"image": "mcr.microsoft.com/devcontainers/python:1-3.11-bullseye",
"build": {
"dockerfile": "Dockerfile"
},
"name": "Versatile Thermostat integration",
"appPort": [
"8123:8123"
@@ -9,29 +11,34 @@
// "postCreateCommand": "container install",
"postCreateCommand": "./container dev-setup",
"mounts": [
"source=/Users/jmcollin/.ssh,target=/home/vscode/.ssh,type=bind,consistency=cached",
// uncomment this to get the versatile-thermostat-ui-card
"source=${localEnv:HOME}/SugarSync/Projets/home-assistant/versatile-thermostat-ui-card/dist,target=/workspaces/versatile_thermostat/config/www/community/versatile-thermostat-ui-card,type=bind,consistency=cached"
],
"mounts": [
"source=${localEnv:HOME}/.ssh,target=/home/vscode/.ssh,type=bind,consistency=cached",
// uncomment this to get the versatile-thermostat-ui-card
"source=${localEnv:HOME}/SugarSync/Projets/home-assistant/versatile-thermostat-ui-card/dist,target=/workspaces/versatile_thermostat/config/www/community/versatile-thermostat-ui-card,type=bind,consistency=cached"
],
"customizations": {
"vscode": {
"extensions": [
"ms-python.python",
"ms-python.pylint",
// Doesn't work (crash). Default in python is to use Jedi see Settings / Python / Default Language
// "ms-python.vscode-pylance",
"ms-python.isort",
"ms-python.black-formatter",
"visualstudioexptteam.vscodeintellicode",
"redhat.vscode-yaml",
"github.vscode-pull-request-github",
"ryanluker.vscode-coverage-gutters",
"ms-python.black-formatter",
"ms-python.pylint",
"ferrierbenjamin.fold-unfold-all-icone",
"ms-python.isort",
"LittleFoxTeam.vscode-python-test-adapter",
"donjayamanne.githistory",
"waderyan.gitblame",
"keesschollaart.vscode-home-assistant",
"vscode.markdown-math",
"yzhang.markdown-all-in-one",
"github.vscode-github-actions"
"github.vscode-github-actions",
"azuretools.vscode-docker"
],
"settings": {
"files.eol": "\n",
@@ -52,10 +59,10 @@
"editor.formatOnPaste": false,
"editor.formatOnSave": true,
"editor.formatOnType": true,
"files.trimTrailingWhitespace": true,
"python.experiments.optOutFrom": ["pythonTestAdapter"],
"python.analysis.logLevel": "Trace"
"files.trimTrailingWhitespace": true
// "python.experiments.optOutFrom": ["pythonTestAdapter"],
// "python.analysis.logLevel": "Trace"
}
}
}
}
}

View File

@@ -4,6 +4,8 @@ about: Create a report to help us improve
---
> Please read carefuly this instructions and fill this form before writing an issue. It helps me to help you.
<!-- This template will allow the maintainer to be efficient and post the more accurante response as possible. There is many types / modes / configuration possible, so the analysis can be very tricky. If don't follow this template, your issue could be rejected without any message. Please help me to help you. -->
<!-- Before you open a new issue, search through the existing issues to see if others have had the same problem.

View File

@@ -17,7 +17,7 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v2
with:
python-version: 3.11
python-version: 3.12
- name: Install dependencies
run: |

30
.vscode/launch.json vendored
View File

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

13
.vscode/settings.json vendored
View File

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

View File

@@ -4,11 +4,16 @@
[![hacs][hacs_badge]][hacs]
[![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)
- [Changements dans la version 5.0](#changements-dans-la-version-50)
- [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)
- [Incompatibilités](#incompatibilités)
@@ -19,21 +24,24 @@
- [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)
- [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_climate```:](#pour-un-thermostat-de-type-thermostat_over_climate)
- [L'auto-régulation](#lauto-régulation)
- [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 démarrage / arrêt automatique](#le-démarrage--arrêt-automatique)
- [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)
- [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)
- [Le mode capteur](#le-mode-capteur)
- [Le mode auto](#le-mode-auto)
- [Configurer le mode d'activité ou la détection de mouvement](#configurer-le-mode-dactivité-ou-la-détection-de-mouvement)
- [Configurer la gestion de la puissance](#configurer-la-gestion-de-la-puissance)
- [Configurer la présence ou l'occupation](#configurer-la-présence-ou-loccupation)
- [Configurer la présence (ou l'absence)](#configurer-la-présence-ou-labsence)
- [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)
@@ -69,6 +77,7 @@
- [Dépannages](#dépannages)
- [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-1)
- [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)
@@ -79,19 +88,31 @@
- [Comment être averti lorsque cela se produit ?](#comment-être-averti-lorsque-cela-se-produit-)
- [Comment réparer ?](#comment-réparer-)
- [Utilisation d'un groupe de personnes comme capteur de présence](#utilisation-dun-groupe-de-personnes-comme-capteur-de-présence)
- [Activer les logs du Versatile Thermostat](#activer-les-logs-du-versatile-thermostat)
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*_
> * **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)
> ![Nouveau](images/new-icon.png) _*Historique des dernières versions*_
> * **Release 6.5** :
> - Ajout d'une nouvelle fonction permettant l'arrêt et la relance automatique d'un VTherm `over_climate` [585](https://github.com/jmcollin78/versatile_thermostat/issues/585)
> - Amélioration de la gestion des ouvertures au démarrage. Permet de mémoriser et de recalculer l'état d'une ouverture au redémarage de Home Assistant [504](https://github.com/jmcollin78/versatile_thermostat/issues/504)
> * **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)
<details>
<summary>Autres versions</summary>
> * **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).
<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.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).
@@ -110,8 +131,81 @@ 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.
</details>
# Changements majeurs dans la version 5.0
![Nouveau](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/new-icon.png?raw=true)
<details>
<summary>Changements dans la version 6.0</summary>
# Changements dans la version 6.0
## 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>
<details>
<summary>Changements dans la version 5.0</summary>
# Changements dans la version 5.0
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",
@@ -125,10 +219,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.
**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)
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 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
@@ -151,7 +245,9 @@ 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,
3. les thermostats de type Heatzy qui ne supportent pas les commandes de type set_temperature
4. les thermostats de type Rointe ont tendance a se réveiller tout seul. Le reste fonctionne normalement.
5. les TRV de type Aqara SRTS-A01 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.
5. les TRV de type Aqara SRTS-A01 et MOES TV01-ZB qui n'ont pas le retour d'état `hvac_action` permettant de savoir si elle chauffe ou pas. Donc les retours d'état sont faussés, le reste à l'air fonctionnel.
6. La clim Airwell avec l'intégration "Midea AC LAN". Si 2 commandes de VTherm sont trop rapprochées, la clim s'arrête d'elle même.
7. Les climates basés sur l'intégration Overkiz ne fonctionnent pas. Il parait impossible d'éteindre ni même de changer la température sur ces systèmes.
# Pourquoi une nouvelle implémentation du thermostat ?
@@ -194,7 +290,7 @@ Ce composant nommé __Versatile thermostat__ gère les cas d'utilisation suivant
-- VTherm = Versatile Thermostat dans la suite de ce document --
> ![Astuce](/images/tips.png?raw=true) _*Notes*_
> ![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.
@@ -203,19 +299,24 @@ Ce composant nommé __Versatile thermostat__ gère les cas d'utilisation suivant
## Création d'un nouveau Versatile Thermostat
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.
Suivez ensuite les étapes de configuration comme suit :
Suivez ensuite les étapes de configuration en sélectionnant dans le menu l'option à configurer.
## Choix des attributs de base
![image](/images/config-main0.png?raw=true)
Choisisez le menu "Principaux attributs".
![image](/images/config-main.png?raw=true)
![image](images/config-main.png)
Donnez les principaux attributs obligatoires :
1. un nom (sera le nom de l'intégration et aussi le nom de l'entité climate)
@@ -228,14 +329,15 @@ Donnez les principaux attributs obligatoires :
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**,
> 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.
## Sélectionnez des entités pilotées
## Sélectionnez des entités pilotées (sous-jacents)
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 :
> 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,
@@ -244,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```
### 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).
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é :
![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.
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```:
![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.
@@ -278,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.
> ![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,
> 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,
@@ -365,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).
#### 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
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.
@@ -374,8 +510,19 @@ Il faut évidemment que votre équipement sous-jacent soit équipée d'une venti
Si votre équipement ne comprend pas le mode Turbo, le mode Forte` sera utilisé en remplacement.
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.
#### Le démarrage / arrêt automatique
Cette fonction a été introduite en 6.5.0. Elle permet d'autoriser VTherm a stopper un équipement qui n'a pas besoin d'être allumé et de le redémarrer lorsque les conditions le réclame. Cette fonction est munie de 3 réglages qui permettent d'arrêter / relancer plus ou moins rapidement l'équipement.
Pour l'utiliser, vous devez :
1. Ajouter la fonction `Avec démmarrage et extinction automatique` dans le menu 'Fonctions',
2. Paramétrer le niveau de détection dans l'option 'Allumage/extinction automatique' qui s'affiche lorsque la fonction a été activée. Vous choisissez le niveau de détection entre 'Lent', 'Moyen' et 'Rapide'. Les arrêts/relances seront plus nombreux avec le niveau 'Rapide'.
Une fois paramétré, vous aurez maintenant une nouvelle entité de type `switch` qui vous permet d'autoriser ou non l'arrêt/relance automatique sans toucher à la configuration. Cette entité est disponible sur l'appareil VTherm et se nomme `switch.<name>_enable_auto_start_stop`. Cochez la pour autoriser le démarrage et extinction automatique.
L'algorithme de détection est décrit [ici](https://github.com/jmcollin78/versatile_thermostat/issues/585).
### 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.
L'algorithme à utiliser est aujourd'hui limité à TPI est disponible. Voir [algorithme](#algorithme).
@@ -383,9 +530,9 @@ Il est possible de choisir un thermostat over valve qui commande une climatisati
## 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 :
1. le coefficient coef_int de l'algorithme TPI,
@@ -394,10 +541,7 @@ Vous devez donner :
Pour plus d'informations sur l'algorithme TPI et son réglage, veuillez vous référer à [algorithm](#algorithm).
## Configurer la température préréglée
Cliquez sur 'Valider' sur la page précédente et vous y arriverez :
![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 :
- **Eco** : l'appareil est en mode d'économie d'énergie
@@ -408,7 +552,9 @@ 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.
> ![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.
> 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).
@@ -416,6 +562,7 @@ Le mode préréglé (preset) vous permet de préconfigurer la température cibl
> 5. Si vous ne souhaitez pas utiliser le préréglage, indiquez 0 comme température. Le préréglage sera alors ignoré et ne s'affichera pas dans le composant front
## Configurer les portes/fenêtres en allumant/éteignant les thermostats
Vous devez avoir choisi la fonctionnalité ```Avec détection des ouvertures``` dans la première page pour arriver sur cette page.
La détecttion des ouvertures peut se faire de 2 manières:
1. soit avec un capteur placé sur l'ouverture (mode capteur),
@@ -423,7 +570,7 @@ La détecttion des ouvertures peut se faire de 2 manières:
### Le mode capteur
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
2. un **délai en secondes** avant tout changement. Cela permet d'ouvrir rapidement une fenêtre sans arrêter le chauffage.
@@ -431,7 +578,7 @@ En mode capteur, vous devez renseigner les informations suivantes:
### Le mode auto
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),
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),
@@ -443,23 +590,24 @@ Pour régler les seuils il est conseillé de commencer avec les valeurs de réf
- 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.
![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") :
![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é.
> ![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/)
> 2. Si vous n'avez pas de capteur de fenêtre/porte dans votre chambre, laissez simplement l'identifiant de l'entité du capteur vide,
> 3. **Un seul mode est permis**. On ne peut pas configurer un thermostat avec un capteur et une détection automatique. Les 2 modes risquant de se contredire, il n'est pas possible d'avoir les 2 modes en même temps,
> 4. Il est déconseillé d'utiliser le mode automatique pour un équipement soumis à des variations de température fréquentes et normales (couloirs, zones ouvertes, ...)
## Configurer le mode d'activité ou la détection de mouvement
Si vous avez choisi la fonctionnalité ```Avec détection de mouvement```, cliquez sur 'Valider' sur la page précédente et vous y arriverez :
![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é.
Ce dont nous avons besoin:
@@ -479,32 +627,33 @@ Alors imaginons que nous voulions avoir le comportement suivant :
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
## 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 :
![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.
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.
> ![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.
> 2. Je l'utilise pour éviter de dépasser la limite de mon contrat d'électricité lorsqu'un véhicule électrique est en charge. Cela crée une sorte d'autorégulation.
> 3. Gardez toujours une marge, car la puissance max peut être brièvement dépassée en attendant le calcul du prochain cycle typiquement ou par des équipements non régulés.
> 4. Si vous ne souhaitez pas utiliser cette fonctionnalité, laissez simplement l'identifiant des entités vide
> 5. Si vous controlez plusieurs radiateurs, la **consommation électrique de votre chauffage** renseigné doit correspondre à la somme des puissances.
## Configurer la présence ou l'occupation
## Configurer la présence (ou l'absence)
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 :
![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 :
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,
@@ -516,15 +665,16 @@ 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).
> ![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,
> 2. vous pouvez utiliser le capteur direct person.xxxx ou un groupe de capteurs de Home Assistant. Le capteur de présence gère les états ``on`` ou ``home`` comme présents et les états ``off`` ou ``not_home`` comme absents.
## Configuration avancée
Ces paramètres permettent d'affiner le réglage du thermostat.
Le formulaire de configuration avancée est le suivant :
![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.
@@ -535,6 +685,8 @@ 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.
Note: les paramètres `security_min_on_percent` et `security_default_on_percent` ne s'applique pas aux VTherms `over_climate`.
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:
@@ -546,7 +698,7 @@ Par défaut, le thermomètre extérieur peut déclencher une mise en sécurité
Voir [exemple de réglages](#examples-tuning) pour avoir des exemples de réglage communs
> ![Astuce](/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,
> 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,
@@ -554,6 +706,7 @@ Voir [exemple de réglages](#examples-tuning) pour avoir des exemples de réglag
> 5. Les thermostats de type ``thermostat_over_climate`` ne sont pas concernés par le mode security.
## 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 :
@@ -566,9 +719,10 @@ Cette entité se présente sous la forme d'une liste de choix qui contient les c
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?raw=true)
![central_mode](images/central_mode.png)
## 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 :
@@ -584,16 +738,16 @@ Le principe mis en place est globalement le suivant :
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?raw=true)
![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?raw=true)
![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?raw=true)
![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),
@@ -616,11 +770,11 @@ Exemple:
Sous "Outils de développement / Service" :
![Configuration du service](/images/dev-tools-turnon-boiler-1.png?raw=true)
![Configuration du service](images/dev-tools-turnon-boiler-1.png)
En mode yaml :
![Configuration du service](/images/dev-tools-turnon-boiler-2.png?raw=true)
![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`)
@@ -665,9 +819,12 @@ context:
### Avertissement
> ![Astuce](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/tips.png?raw=true) _*Notes*_
> ![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>
<summary>Synthèse des paramètres</summary>
## Synthèse des paramètres
| Paramètre | Libellé | "over switch" | "over climate" | "over valve" | "configuration centrale" |
@@ -689,6 +846,7 @@ context:
| ``heater_entity2_id`` | 2ème radiateur | X | - | - | - |
| ``heater_entity3_id`` | 3ème radiateur | X | - | - | - |
| ``heater_entity4_id`` | 4ème radiateur | X | - | - | - |
| ``heater_keep_alive`` | Intervalle de rafraichissement du switch | X | - | - | - |
| ``proportional_function`` | Algorithme | X | - | - | - |
| ``climate_entity1_id`` | Thermostat sous-jacent | - | X | - | - |
| ``climate_entity2_id`` | 2ème thermostat sous-jacent | - | X | - | - |
@@ -701,13 +859,7 @@ context:
| ``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_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 |
| ``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 |
| ``frost_temp`` | Température en preset Hors-gel | X | 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_auto_open_threshold`` | Seuil haut de chute de température pour la détection automatique (en °/min) | X | X | X | X |
@@ -722,13 +874,6 @@ context:
| ``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 |
| ``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 |
| ``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 |
@@ -737,10 +882,14 @@ context:
| ``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 | - | - | - |
| ``auto_fan_mode`` | Mode de ventilation automatique | - | X | - | - |
| ``add_central_boiler_control`` | Ajout du controle d'une chaudière centrale | - | - | - | 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 | - |
| ``use_auto_start_stop_feature`` | Indique si la fonction de démarrage/extinction automatique est activée | - | X | - | - |
| ``auto_start_stop_lvel`` | Le niveau de détection de l'auto start/stop | - | X | - | - |
</details>
# Exemples de réglage
@@ -816,7 +965,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 :
![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 :
1. l'entité principale climate de commande du thermostat,
@@ -849,7 +998,7 @@ frontend:
```
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
@@ -895,7 +1044,7 @@ target:
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.
## Modifier les paramètres de sécurité
@@ -950,7 +1099,7 @@ Vous pouvez très facilement capter ses évènements dans une automatisation par
# 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 :
![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 :
@@ -997,27 +1146,30 @@ Les attributs personnalisés sont les suivants :
| ``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 |
| ``auto_start_stop_enable`` | Indique si le VTherm est autorisé à s'auto démarrer/arrêter |
| ``auto_start_stop_level`` | Indique le niveau d'auto start/stop |
| ``hvac_off_reason`` | Indique la raison de l'arrêt (hvac_off) du VTherm. Ce peut être Window, Auto-start/stop ou Manuel |
# Quelques résultats
**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://)
![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)**
![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**
![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)]
**Thermostat finement réglé**
Merci [impuR_Shozz](https://forum.hacf.fr/u/impur_shozz/summary) !
On peut voir une stabilité autour de la température cible (consigne) et lorsqu'à cible le on_percent (puissance) est proche de 0,3 ce qui semble une très bonne valeur.
![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/results-fine-tuned.png?raw=true)
![image](images/results-fine-tuned.png)
Enjoy !
@@ -1031,9 +1183,9 @@ Une carte spéciale pour le Versatile Thermostat a été développée (sur la ba
## Encore mieux avec le composant Scheduler !
Afin de profiter de toute la puissance du Versatile Thermostat, je vous invite à l'utiliser avec https://github.com/nielsfaber/scheduler-component
En effet, le composant scheduler propose une gestion de la base climatique sur les modes prédéfinis. Cette fonctionnalité a un intérêt limité avec le thermostat générique mais elle devient très puissante avec le thermostat Awesome :
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 :
@@ -1056,7 +1208,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
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 :
```
@@ -1099,7 +1251,7 @@ Vous pouvez personnaliser ce composant à l'aide du composant HACS card-mod pour
}
{% 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
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) :
@@ -1172,7 +1324,7 @@ Remplacez les valeurs entre [[ ]] par les votres.
Exemple de courbes obtenues avec Plotly :
![image](/images/plotly-curves.png?raw=true)
![image](images/plotly-curves.png)
## 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.
@@ -1268,7 +1420,11 @@ Si vous souhaitez contribuer, veuillez lire les [directives de contribution](CON
# Dépannages
<details>
<summary>Utilisation d'un Heatzy</summary>
## Utilisation d'un Heatzy
L'utilisation d'un Heatzy est possible à la condition d'utiliser un switch virtuel sur ce modèle :
```
- platform: template
@@ -1297,6 +1453,10 @@ L'utilisation d'un Heatzy est possible à la condition d'utiliser un switch virt
preset_mode: "eco"
```
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
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.
@@ -1318,10 +1478,58 @@ Exemple :
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
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)
</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`
@@ -1333,7 +1541,10 @@ Avec un VTherm de type `over_climate`, la régulation est faite par le `climate`
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
@@ -1354,9 +1565,13 @@ 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.
</details>
<details>
<summary>Pourquoi mon Versatile Thermostat se met en Securite ?</summary>
## 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é est possible sur tous les types de VTherm . Il survient lorsqu'un des 2 thermomètres qui donne la température de la pièce ou la température extérieure n'a pas envoyé de valeur depuis plus de `security_delay_min` minutes et que le radiateur chauffait à au moins `security_min_on_percent`.
Comme l'algorithme est basé sur les mesures de température, si elles ne sont plus reçues par le VTherm, il y a un risque de surchauffe et d'incendie. Pour éviter ça, lorsque les conditions rappelées ci-dessus sont détectées, la chauffe est limité au paramètre `security_default_on_percent`. Cette valeur doit donc être raisonnablement faible (10% est une bonne valeur). Elle permet d'éviter un incendie tout en évitant de couper totalement le radiateur (risque de gel).
@@ -1366,11 +1581,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.
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à :
[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**.
@@ -1401,8 +1616,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.
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.
</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
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 :
@@ -1423,6 +1643,21 @@ Fichier `configuration.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>
***

943
README.md

File diff suppressed because it is too large Load Diff

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."""
from __future__ import annotations
from typing import Dict
@@ -8,16 +9,19 @@ import logging
import voluptuous as vol
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.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 .const import (
DOMAIN,
PLATFORMS,
CONFIG_VERSION,
CONFIG_MINOR_VERSION,
CONF_AUTO_REGULATION_LIGHT,
CONF_AUTO_REGULATION_MEDIUM,
CONF_AUTO_REGULATION_STRONG,
@@ -27,6 +31,30 @@ from .const import (
CONF_SAFETY_MODE,
CONF_THERMOSTAT_CENTRAL_CONFIG,
CONF_THERMOSTAT_TYPE,
CONF_USE_WINDOW_FEATURE,
CONF_USE_MOTION_FEATURE,
CONF_USE_PRESENCE_FEATURE,
CONF_USE_POWER_FEATURE,
CONF_USE_CENTRAL_BOILER_FEATURE,
CONF_POWER_SENSOR,
CONF_PRESENCE_SENSOR,
CONF_UNDERLYING_LIST,
CONF_HEATER,
CONF_HEATER_2,
CONF_HEATER_3,
CONF_HEATER_4,
CONF_CLIMATE,
CONF_CLIMATE_2,
CONF_CLIMATE_3,
CONF_CLIMATE_4,
CONF_VALVE,
CONF_VALVE_2,
CONF_VALVE_3,
CONF_VALVE_4,
CONF_THERMOSTAT_SWITCH,
CONF_THERMOSTAT_CLIMATE,
CONF_THERMOSTAT_VALVE,
CONF_MAX_ON_PERCENT,
)
from .vtherm_api import VersatileThermostatAPI
@@ -59,6 +87,7 @@ CONFIG_SCHEMA = vol.Schema(
CONF_AUTO_REGULATION_EXPERT: vol.Schema(SELF_REGULATION_PARAM_SCHEMA),
CONF_SHORT_EMA_PARAMS: vol.Schema(EMA_PARAM_SCHEMA),
CONF_SAFETY_MODE: vol.Schema(SAFETY_MODE_PARAM_SCHEMA),
vol.Optional(CONF_MAX_ON_PERCENT): vol.Coerce(float),
}
),
},
@@ -82,16 +111,31 @@ async def async_setup(
hass.data.setdefault(DOMAIN, {})
api: VersatileThermostatAPI = VersatileThermostatAPI.get_vtherm_api(hass)
# L'argument config contient votre fichier configuration.yaml
vtherm_config = config.get(DOMAIN)
if vtherm_config is not None:
api: VersatileThermostatAPI = VersatileThermostatAPI.get_vtherm_api(hass)
api.set_global_config(vtherm_config)
else:
_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,
SERVICE_RELOAD,
_handle_reload,
@@ -114,6 +158,7 @@ async def reload_all_vtherm(hass):
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:
@@ -133,13 +178,22 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
await api.reload_central_boiler_entities_list()
if hass.state == CoreState.running:
await api.reload_central_boiler_entities_list()
await api.init_vtherm_links(entry.entry_id)
return True
async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:
"""Update listener."""
_LOGGER.debug(
"Calling update_listener entry: entry_id='%s', value='%s'",
entry.entry_id,
entry.data,
)
if entry.data.get(CONF_THERMOSTAT_TYPE) == CONF_THERMOSTAT_CENTRAL_CONFIG:
await reload_all_vtherm(hass)
else:
@@ -148,6 +202,7 @@ async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:
api: VersatileThermostatAPI = VersatileThermostatAPI.get_vtherm_api(hass)
if api is not None:
await api.reload_central_boiler_entities_list()
await api.init_vtherm_links(entry.entry_id)
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
@@ -165,15 +220,83 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
# Example migration function
async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry):
"""Migrate old entry."""
_LOGGER.debug("Migrating from version %s", config_entry.version)
_LOGGER.debug(
"Migrating from version %s/%s", config_entry.version, config_entry.minor_version
)
if config_entry.version == 1:
if (
config_entry.version != CONFIG_VERSION
or config_entry.minor_version != CONFIG_MINOR_VERSION
):
_LOGGER.debug(
"Migration to %s/%s is needed", CONFIG_VERSION, CONFIG_MINOR_VERSION
)
new = {**config_entry.data}
# TO DO: modify Config Entry data if there will be something to migrate
config_entry.version = 2
hass.config_entries.async_update_entry(config_entry, data=new)
thermostat_type = config_entry.data.get(CONF_THERMOSTAT_TYPE)
_LOGGER.info("Migration to version %s successful", config_entry.version)
if thermostat_type == CONF_THERMOSTAT_CENTRAL_CONFIG:
new[CONF_USE_WINDOW_FEATURE] = True
new[CONF_USE_MOTION_FEATURE] = True
new[CONF_USE_POWER_FEATURE] = new.get(CONF_POWER_SENSOR, None) is not None
new[CONF_USE_PRESENCE_FEATURE] = (
new.get(CONF_PRESENCE_SENSOR, None) is not None
)
new[CONF_USE_CENTRAL_BOILER_FEATURE] = new.get(
"add_central_boiler_control", False
) or new.get(CONF_USE_CENTRAL_BOILER_FEATURE, False)
if config_entry.data.get(CONF_UNDERLYING_LIST, None) is None:
underlying_list = []
if thermostat_type == CONF_THERMOSTAT_SWITCH:
underlying_list = [
config_entry.data.get(CONF_HEATER, None),
config_entry.data.get(CONF_HEATER_2, None),
config_entry.data.get(CONF_HEATER_3, None),
config_entry.data.get(CONF_HEATER_4, None),
]
elif thermostat_type == CONF_THERMOSTAT_CLIMATE:
underlying_list = [
config_entry.data.get(CONF_CLIMATE, None),
config_entry.data.get(CONF_CLIMATE_2, None),
config_entry.data.get(CONF_CLIMATE_3, None),
config_entry.data.get(CONF_CLIMATE_4, None),
]
elif thermostat_type == CONF_THERMOSTAT_VALVE:
underlying_list = [
config_entry.data.get(CONF_VALVE, None),
config_entry.data.get(CONF_VALVE_2, None),
config_entry.data.get(CONF_VALVE_3, None),
config_entry.data.get(CONF_VALVE_4, None),
]
new[CONF_UNDERLYING_LIST] = [
entity for entity in underlying_list if entity is not None
]
for key in [
CONF_HEATER,
CONF_HEATER_2,
CONF_HEATER_3,
CONF_HEATER_4,
CONF_CLIMATE,
CONF_CLIMATE_2,
CONF_CLIMATE_3,
CONF_CLIMATE_4,
CONF_VALVE,
CONF_VALVE_2,
CONF_VALVE_3,
CONF_VALVE_4,
]:
new.pop(key, None)
hass.config_entries.async_update_entry(
config_entry,
data=new,
version=CONFIG_VERSION,
minor_version=CONFIG_MINOR_VERSION,
)
_LOGGER.info("Migration to version %s successful", config_entry.version)
return True

View File

@@ -0,0 +1,239 @@
# pylint: disable=line-too-long
""" This file implements the Auto start/stop algorithm as described here: https://github.com/jmcollin78/versatile_thermostat/issues/585
"""
import logging
from datetime import datetime
from typing import Literal
from homeassistant.components.climate import HVACMode
from .const import (
AUTO_START_STOP_LEVEL_NONE,
AUTO_START_STOP_LEVEL_FAST,
AUTO_START_STOP_LEVEL_MEDIUM,
AUTO_START_STOP_LEVEL_SLOW,
TYPE_AUTO_START_STOP_LEVELS,
)
_LOGGER = logging.getLogger(__name__)
# Some constant to make algorithm depending of level
DT_MIN = {
AUTO_START_STOP_LEVEL_NONE: 0, # Not used
AUTO_START_STOP_LEVEL_SLOW: 30,
AUTO_START_STOP_LEVEL_MEDIUM: 15,
AUTO_START_STOP_LEVEL_FAST: 7,
}
# the measurement cycle (2 min)
CYCLE_SEC = 120
# A temp hysteresis to avoid rapid OFF/ON
TEMP_HYSTERESIS = 0.5
ERROR_THRESHOLD = {
AUTO_START_STOP_LEVEL_NONE: 0, # Not used
AUTO_START_STOP_LEVEL_SLOW: 10, # 10 cycle above 1° or 5 cycle above 2°, ...
AUTO_START_STOP_LEVEL_MEDIUM: 5, # 5 cycle above 1° or 3 cycle above 2°, ..., 1 cycle above 5°
AUTO_START_STOP_LEVEL_FAST: 2, # 2 cycle above 1° or 1 cycle above 2°
}
AUTO_START_STOP_ACTION_OFF = "turnOff"
AUTO_START_STOP_ACTION_ON = "turnOn"
AUTO_START_STOP_ACTION_NOTHING = "nothing"
AUTO_START_STOP_ACTIONS = Literal[ # pylint: disable=invalid-name
AUTO_START_STOP_ACTION_OFF,
AUTO_START_STOP_ACTION_ON,
AUTO_START_STOP_ACTION_NOTHING,
]
class AutoStartStopDetectionAlgorithm:
"""The class that implements the algorithm listed above"""
_dt: float | None = None
_level: str = AUTO_START_STOP_LEVEL_NONE
_accumulated_error: float = 0
_error_threshold: float | None = None
_last_calculation_date: datetime | None = None
def __init__(self, level: TYPE_AUTO_START_STOP_LEVELS, vtherm_name) -> None:
"""Initalize a new algorithm with the right constants"""
self._vtherm_name = vtherm_name
self._init_level(level)
def _init_level(self, level: TYPE_AUTO_START_STOP_LEVELS):
"""Initialize a new level"""
if level == self._level:
return
self._level = level
if self._level != AUTO_START_STOP_LEVEL_NONE:
self._dt = DT_MIN[level]
self._error_threshold = ERROR_THRESHOLD[level]
# reset accumulated error if we change the level
self._accumulated_error = 0
def calculate_action(
self,
hvac_mode: HVACMode | None,
saved_hvac_mode: HVACMode | None,
target_temp: float,
current_temp: float,
slope_min: float | None,
now: datetime,
) -> AUTO_START_STOP_ACTIONS:
"""Calculate an eventual action to do depending of the value in parameter"""
if self._level == AUTO_START_STOP_LEVEL_NONE:
_LOGGER.debug(
"%s - auto-start/stop is disabled",
self,
)
return AUTO_START_STOP_ACTION_NOTHING
_LOGGER.debug(
"%s - calculate_action: hvac_mode=%s, saved_hvac_mode=%s, target_temp=%s, current_temp=%s, slope_min=%s at %s",
self,
hvac_mode,
saved_hvac_mode,
target_temp,
current_temp,
slope_min,
now,
)
if hvac_mode is None or target_temp is None or current_temp is None:
_LOGGER.debug(
"%s - No all mandatory parameters are set. Disable auto-start/stop",
self,
)
return AUTO_START_STOP_ACTION_NOTHING
# Calculate the error factor (P)
error = target_temp - current_temp
# reduce the error considering the dt between the last measurement
if self._last_calculation_date is not None:
dtmin = (now - self._last_calculation_date).total_seconds() / CYCLE_SEC
# ignore two calls too near (< 24 sec)
if dtmin <= 0.2:
_LOGGER.debug(
"%s - new calculation of auto_start_stop (%s) is too near of the last one (%s). Forget it",
self,
now,
self._last_calculation_date,
)
return AUTO_START_STOP_ACTION_NOTHING
error = error * dtmin
# If the error have change its sign, reset smoothly the accumulated error
if error * self._accumulated_error < 0:
self._accumulated_error = self._accumulated_error / 2.0
self._accumulated_error += error
# Capping of the error
self._accumulated_error = min(
self._error_threshold,
max(-self._error_threshold, self._accumulated_error),
)
self._last_calculation_date = now
temp_at_dt = current_temp + slope_min * self._dt
# Check to turn-off
# When we hit the threshold, that mean we can turn off
if hvac_mode == HVACMode.HEAT:
if (
self._accumulated_error <= -self._error_threshold
and temp_at_dt >= target_temp + TEMP_HYSTERESIS
):
_LOGGER.info(
"%s - We need to stop, there is no need for heating for a long time.",
self,
)
return AUTO_START_STOP_ACTION_OFF
else:
_LOGGER.debug("%s - nothing to do, we are heating", self)
return AUTO_START_STOP_ACTION_NOTHING
if hvac_mode == HVACMode.COOL:
if (
self._accumulated_error >= self._error_threshold
and temp_at_dt <= target_temp - TEMP_HYSTERESIS
):
_LOGGER.info(
"%s - We need to stop, there is no need for cooling for a long time.",
self,
)
return AUTO_START_STOP_ACTION_OFF
else:
_LOGGER.debug(
"%s - nothing to do, we are cooling",
self,
)
return AUTO_START_STOP_ACTION_NOTHING
# check to turn on
if hvac_mode == HVACMode.OFF and saved_hvac_mode == HVACMode.HEAT:
if temp_at_dt <= target_temp - TEMP_HYSTERESIS:
_LOGGER.info(
"%s - We need to start, because it will be time to heat",
self,
)
return AUTO_START_STOP_ACTION_ON
else:
_LOGGER.debug(
"%s - nothing to do, we don't need to heat soon",
self,
)
return AUTO_START_STOP_ACTION_NOTHING
if hvac_mode == HVACMode.OFF and saved_hvac_mode == HVACMode.COOL:
if temp_at_dt >= target_temp + TEMP_HYSTERESIS:
_LOGGER.info(
"%s - We need to start, because it will be time to cool",
self,
)
return AUTO_START_STOP_ACTION_ON
else:
_LOGGER.debug(
"%s - nothing to do, we don't need to cool soon",
self,
)
return AUTO_START_STOP_ACTION_NOTHING
_LOGGER.debug(
"%s - nothing to do, no conditions applied",
self,
)
return AUTO_START_STOP_ACTION_NOTHING
def set_level(self, level: TYPE_AUTO_START_STOP_LEVELS):
"""Set a new level"""
self._init_level(level)
@property
def dt_min(self) -> float:
"""Get the dt value"""
return self._dt
@property
def accumulated_error(self) -> float:
"""Get the accumulated error value"""
return self._accumulated_error
@property
def accumulated_error_threshold(self) -> float:
"""Get the accumulated error threshold value"""
return self._error_threshold
@property
def level(self) -> TYPE_AUTO_START_STOP_LEVELS:
"""Get the level value"""
return self._level
def __str__(self) -> str:
return f"AutoStartStopDetectionAlgorithm-{self._vtherm_name}"

File diff suppressed because it is too large Load Diff

View File

@@ -7,11 +7,11 @@ from homeassistant.core import (
HomeAssistant,
callback,
Event,
CoreState,
# CoreState,
HomeAssistantError,
)
from homeassistant.const import STATE_ON, STATE_OFF, EVENT_HOMEASSISTANT_START
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
@@ -39,6 +39,7 @@ from .const import (
CONF_USE_WINDOW_FEATURE,
CONF_THERMOSTAT_TYPE,
CONF_THERMOSTAT_CENTRAL_CONFIG,
CONF_USE_CENTRAL_BOILER_FEATURE,
CONF_CENTRAL_BOILER_ACTIVATION_SRV,
CONF_CENTRAL_BOILER_DEACTIVATION_SRV,
overrides,
@@ -63,10 +64,13 @@ async def async_setup_entry(
name = entry.data.get(CONF_NAME)
vt_type = entry.data.get(CONF_THERMOSTAT_TYPE)
entities = None
if vt_type == CONF_THERMOSTAT_CENTRAL_CONFIG:
entities = [
CentralBoilerBinarySensor(hass, unique_id, name, entry.data),
]
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),
@@ -81,7 +85,8 @@ async def async_setup_entry(
if entry.data.get(CONF_USE_POWER_FEATURE):
entities.append(OverpoweringBinarySensor(hass, unique_id, name, entry.data))
async_add_entities(entities, True)
if entities:
async_add_entities(entities, True)
class SecurityBinarySensor(VersatileThermostatBaseEntity, BinarySensorEntity):
@@ -95,7 +100,7 @@ class SecurityBinarySensor(VersatileThermostatBaseEntity, BinarySensorEntity):
entry_infos,
) -> None:
"""Initialize the SecurityState Binary sensor"""
super().__init__(hass, unique_id, entry_infos.get(CONF_NAME))
super().__init__(hass, unique_id, name)
self._attr_name = "Security state"
self._attr_unique_id = f"{self._device_name}_security_state"
self._attr_is_on = False
@@ -386,17 +391,18 @@ class CentralBoilerBinarySensor(BinarySensorEntity):
api: VersatileThermostatAPI = VersatileThermostatAPI.get_vtherm_api(self._hass)
api.register_central_boiler(self)
@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
)
# 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"""

View File

@@ -1,4 +1,5 @@
""" Implements the VersatileThermostat climate component """
import logging
@@ -36,17 +37,16 @@ from .const import (
CONF_THERMOSTAT_CLIMATE,
CONF_THERMOSTAT_VALVE,
CONF_THERMOSTAT_CENTRAL_CONFIG,
CONF_SONOFF_TRZB_MODE,
)
from .thermostat_switch import ThermostatOverSwitch
from .thermostat_climate import ThermostatOverClimate
from .thermostat_valve import ThermostatOverValve
from .thermostat_sonoff_trvzb import ThermostatOverSonoffTRVZB
_LOGGER = logging.getLogger(__name__)
# _LOGGER.setLevel(logging.DEBUG)
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
@@ -62,6 +62,7 @@ async def async_setup_entry(
unique_id = entry.entry_id
name = entry.data.get(CONF_NAME)
vt_type = entry.data.get(CONF_THERMOSTAT_TYPE)
is_sonoff_trvzb = entry.data.get(CONF_SONOFF_TRZB_MODE)
if vt_type == CONF_THERMOSTAT_CENTRAL_CONFIG:
return
@@ -71,9 +72,19 @@ async def async_setup_entry(
if vt_type == CONF_THERMOSTAT_SWITCH:
entity = ThermostatOverSwitch(hass, unique_id, name, entry.data)
elif vt_type == CONF_THERMOSTAT_CLIMATE:
entity = ThermostatOverClimate(hass, unique_id, name, entry.data)
if is_sonoff_trvzb is True:
entity = ThermostatOverSonoffTRVZB(hass, unique_id, name, entry.data)
else:
entity = ThermostatOverClimate(hass, unique_id, name, entry.data)
elif vt_type == CONF_THERMOSTAT_VALVE:
entity = ThermostatOverValve(hass, unique_id, name, entry.data)
else:
_LOGGER.error(
"Cannot create Versatile Thermostat name=%s of type %s which is unknown",
name,
vt_type,
)
return
async_add_entities([entity], True)

View File

@@ -1,4 +1,5 @@
""" Some usefull commons class """
# pylint: disable=line-too-long
import logging
@@ -16,7 +17,6 @@ from .const import DOMAIN, DEVICE_MANUFACTURER, ServiceConfigurationError
_LOGGER = logging.getLogger(__name__)
def get_tz(hass: HomeAssistant):
"""Get the current timezone"""
@@ -182,6 +182,9 @@ class VersatileThermostatBaseEntity(Entity):
"""Returns my climate if found"""
if not self._my_climate:
self._my_climate = self.find_my_versatile_thermostat()
if self._my_climate:
# Only the first time
self.my_climate_is_initialized()
return self._my_climate
@property
@@ -231,13 +234,18 @@ class VersatileThermostatBaseEntity(Entity):
)
)
else:
_LOGGER.warning("%s - no entity to listen. Try later", self)
_LOGGER.debug("%s - no entity to listen. Try later", self)
self._cancel_call = async_call_later(
self.hass, timedelta(seconds=1), try_find_climate
)
await try_find_climate(None)
@callback
def my_climate_is_initialized(self):
"""Called when the associated climate is initialized"""
return
@callback
async def async_my_climate_changed(
self, event: Event

File diff suppressed because it is too large Load Diff

View File

@@ -16,6 +16,10 @@ from homeassistant.components.input_number import (
DOMAIN as INPUT_NUMBER_DOMAIN,
)
from homeassistant.components.input_datetime import (
DOMAIN as INPUT_DATETIME_DOMAIN,
)
from homeassistant.components.person import DOMAIN as PERSON_DOMAIN
from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN
@@ -42,15 +46,45 @@ STEP_MAIN_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name
vol.Required(CONF_TEMP_SENSOR): selector.EntitySelector(
selector.EntitySelectorConfig(domain=[SENSOR_DOMAIN, INPUT_NUMBER_DOMAIN]),
),
vol.Optional(CONF_LAST_SEEN_TEMP_SENSOR): selector.EntitySelector(
selector.EntitySelectorConfig(
domain=[SENSOR_DOMAIN, INPUT_DATETIME_DOMAIN]
),
),
vol.Required(CONF_CYCLE_MIN, default=5): cv.positive_int,
vol.Optional(CONF_DEVICE_POWER, default="1"): vol.Coerce(float),
vol.Optional(CONF_USE_CENTRAL_MODE, default=True): cv.boolean,
vol.Required(CONF_USE_MAIN_CENTRAL_CONFIG, default=True): cv.boolean,
vol.Optional(CONF_USE_CENTRAL_MODE, default=True): cv.boolean,
vol.Required(CONF_USED_BY_CENTRAL_BOILER, default=False): cv.boolean,
}
)
STEP_FEATURES_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name
{
vol.Optional(CONF_USE_WINDOW_FEATURE, default=False): cv.boolean,
vol.Optional(CONF_USE_MOTION_FEATURE, default=False): cv.boolean,
vol.Optional(CONF_USE_POWER_FEATURE, default=False): cv.boolean,
vol.Optional(CONF_USE_PRESENCE_FEATURE, default=False): cv.boolean,
vol.Required(CONF_USED_BY_CENTRAL_BOILER, default=False): cv.boolean,
}
)
STEP_CLIMATE_FEATURES_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name
{
vol.Optional(CONF_USE_WINDOW_FEATURE, default=False): cv.boolean,
vol.Optional(CONF_USE_MOTION_FEATURE, default=False): cv.boolean,
vol.Optional(CONF_USE_POWER_FEATURE, default=False): cv.boolean,
vol.Optional(CONF_USE_PRESENCE_FEATURE, default=False): cv.boolean,
vol.Optional(CONF_USE_AUTO_START_STOP_FEATURE, default=False): cv.boolean,
}
)
STEP_CENTRAL_FEATURES_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name
{
vol.Optional(CONF_USE_WINDOW_FEATURE, default=False): cv.boolean,
vol.Optional(CONF_USE_MOTION_FEATURE, default=False): cv.boolean,
vol.Optional(CONF_USE_POWER_FEATURE, default=False): cv.boolean,
vol.Optional(CONF_USE_PRESENCE_FEATURE, default=False): cv.boolean,
vol.Optional(CONF_USE_CENTRAL_BOILER_FEATURE, default=False): cv.boolean,
}
)
@@ -62,7 +96,6 @@ 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_MAX, default=35): vol.Coerce(float),
vol.Required(CONF_STEP_TEMPERATURE, default=0.1): vol.Coerce(float),
vol.Required(CONF_ADD_CENTRAL_BOILER_CONTROL, default=False): cv.boolean,
}
)
@@ -86,18 +119,12 @@ STEP_CENTRAL_BOILER_SCHEMA = vol.Schema(
STEP_THERMOSTAT_SWITCH = vol.Schema( # pylint: disable=invalid-name
{
vol.Required(CONF_HEATER): selector.EntitySelector(
selector.EntitySelectorConfig(domain=[SWITCH_DOMAIN, INPUT_BOOLEAN_DOMAIN]),
),
vol.Optional(CONF_HEATER_2): selector.EntitySelector(
selector.EntitySelectorConfig(domain=[SWITCH_DOMAIN, INPUT_BOOLEAN_DOMAIN]),
),
vol.Optional(CONF_HEATER_3): selector.EntitySelector(
selector.EntitySelectorConfig(domain=[SWITCH_DOMAIN, INPUT_BOOLEAN_DOMAIN]),
),
vol.Optional(CONF_HEATER_4): selector.EntitySelector(
selector.EntitySelectorConfig(domain=[SWITCH_DOMAIN, INPUT_BOOLEAN_DOMAIN]),
vol.Required(CONF_UNDERLYING_LIST): selector.EntitySelector(
selector.EntitySelectorConfig(
domain=[SWITCH_DOMAIN, INPUT_BOOLEAN_DOMAIN], multiple=True
),
),
vol.Optional(CONF_HEATER_KEEP_ALIVE): cv.positive_int,
vol.Required(CONF_PROP_FUNCTION, default=PROPORTIONAL_FUNCTION_TPI): vol.In(
[
PROPORTIONAL_FUNCTION_TPI,
@@ -110,19 +137,11 @@ STEP_THERMOSTAT_SWITCH = vol.Schema( # pylint: disable=invalid-name
STEP_THERMOSTAT_CLIMATE = vol.Schema( # pylint: disable=invalid-name
{
vol.Required(CONF_CLIMATE): selector.EntitySelector(
selector.EntitySelectorConfig(domain=CLIMATE_DOMAIN),
),
vol.Optional(CONF_CLIMATE_2): selector.EntitySelector(
selector.EntitySelectorConfig(domain=CLIMATE_DOMAIN),
),
vol.Optional(CONF_CLIMATE_3): selector.EntitySelector(
selector.EntitySelectorConfig(domain=CLIMATE_DOMAIN),
),
vol.Optional(CONF_CLIMATE_4): selector.EntitySelector(
selector.EntitySelectorConfig(domain=CLIMATE_DOMAIN),
vol.Required(CONF_UNDERLYING_LIST): selector.EntitySelector(
selector.EntitySelectorConfig(domain=CLIMATE_DOMAIN, multiple=True),
),
vol.Optional(CONF_AC_MODE, default=False): cv.boolean,
vol.Optional(CONF_SONOFF_TRZB_MODE, default=False): cv.boolean,
vol.Optional(
CONF_AUTO_REGULATION_MODE, default=CONF_AUTO_REGULATION_NONE
): selector.SelectSelector(
@@ -143,22 +162,16 @@ STEP_THERMOSTAT_CLIMATE = vol.Schema( # pylint: disable=invalid-name
mode="dropdown",
)
),
vol.Optional(CONF_AUTO_REGULATION_USE_DEVICE_TEMP, default=False): cv.boolean,
}
)
STEP_THERMOSTAT_VALVE = vol.Schema( # pylint: disable=invalid-name
{
vol.Required(CONF_VALVE): selector.EntitySelector(
selector.EntitySelectorConfig(domain=[NUMBER_DOMAIN, INPUT_NUMBER_DOMAIN]),
),
vol.Optional(CONF_VALVE_2): selector.EntitySelector(
selector.EntitySelectorConfig(domain=[NUMBER_DOMAIN, INPUT_NUMBER_DOMAIN]),
),
vol.Optional(CONF_VALVE_3): selector.EntitySelector(
selector.EntitySelectorConfig(domain=[NUMBER_DOMAIN, INPUT_NUMBER_DOMAIN]),
),
vol.Optional(CONF_VALVE_4): selector.EntitySelector(
selector.EntitySelectorConfig(domain=[NUMBER_DOMAIN, INPUT_NUMBER_DOMAIN]),
vol.Required(CONF_UNDERLYING_LIST): selector.EntitySelector(
selector.EntitySelectorConfig(
domain=[NUMBER_DOMAIN, INPUT_NUMBER_DOMAIN], multiple=True
),
),
vol.Required(CONF_PROP_FUNCTION, default=PROPORTIONAL_FUNCTION_TPI): vol.In(
[
@@ -171,6 +184,45 @@ STEP_THERMOSTAT_VALVE = vol.Schema( # pylint: disable=invalid-name
}
)
STEP_AUTO_START_STOP = vol.Schema( # pylint: disable=invalid-name
{
vol.Optional(
CONF_AUTO_START_STOP_LEVEL, default=AUTO_START_STOP_LEVEL_NONE
): selector.SelectSelector(
selector.SelectSelectorConfig(
options=CONF_AUTO_START_STOP_LEVELS,
translation_key="auto_start_stop",
mode="dropdown",
)
),
}
)
STEP_SONOFF_TRVZB = vol.Schema( # pylint: disable=invalid-name
{
vol.Required(CONF_OFFSET_CALIBRATION_LIST): selector.EntitySelector(
selector.EntitySelectorConfig(
domain=[NUMBER_DOMAIN, INPUT_NUMBER_DOMAIN], multiple=True
),
),
vol.Required(CONF_OPENING_DEGREE_LIST): selector.EntitySelector(
selector.EntitySelectorConfig(
domain=[NUMBER_DOMAIN, INPUT_NUMBER_DOMAIN], multiple=True
),
),
vol.Required(CONF_CLOSING_DEGREE_LIST): selector.EntitySelector(
selector.EntitySelectorConfig(
domain=[NUMBER_DOMAIN, INPUT_NUMBER_DOMAIN], multiple=True
),
),
vol.Required(CONF_PROP_FUNCTION, default=PROPORTIONAL_FUNCTION_TPI): vol.In(
[
PROPORTIONAL_FUNCTION_TPI,
]
),
}
)
STEP_TPI_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name
{
vol.Required(CONF_USE_TPI_CENTRAL_CONFIG, default=True): cv.boolean,
@@ -190,18 +242,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
{
@@ -249,7 +289,7 @@ STEP_CENTRAL_WINDOW_WO_AUTO_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid
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(
domain=[BINARY_SENSOR_DOMAIN, INPUT_BOOLEAN_DOMAIN]
),
@@ -281,10 +321,10 @@ STEP_CENTRAL_MOTION_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]),
),
vol.Optional(CONF_MAX_POWER_SENSOR): selector.EntitySelector(
vol.Required(CONF_MAX_POWER_SENSOR): selector.EntitySelector(
selector.EntitySelectorConfig(domain=[SENSOR_DOMAIN, INPUT_NUMBER_DOMAIN]),
),
vol.Optional(CONF_PRESET_POWER, default="13"): vol.Coerce(float),
@@ -299,19 +339,7 @@ STEP_POWER_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)
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(
vol.Required(CONF_PRESENCE_SENSOR): selector.EntitySelector(
selector.EntitySelectorConfig(
domain=[
PERSON_DOMAIN,
@@ -319,7 +347,12 @@ STEP_PRESENCE_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name
INPUT_BOOLEAN_DOMAIN,
]
),
),
)
},
)
STEP_PRESENCE_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name
{
vol.Required(CONF_USE_PRESENCE_CENTRAL_CONFIG, default=True): cv.boolean,
}
)

View File

@@ -2,6 +2,7 @@
"""Constants for the Versatile Thermostat integration."""
import logging
from typing import Literal
from enum import Enum
from homeassistant.const import CONF_NAME, Platform
@@ -22,6 +23,10 @@ from .prop_algorithm import (
_LOGGER = logging.getLogger(__name__)
CONFIG_VERSION = 2
CONFIG_MINOR_VERSION = 0
PRESET_TEMP_SUFFIX = "_temp"
PRESET_AC_SUFFIX = "_ac"
PRESET_ECO_AC = PRESET_ECO + PRESET_AC_SUFFIX
PRESET_COMFORT_AC = PRESET_COMFORT + PRESET_AC_SUFFIX
@@ -39,19 +44,21 @@ HIDDEN_PRESETS = [PRESET_POWER, PRESET_SECURITY]
DOMAIN = "versatile_thermostat"
# The order is important.
PLATFORMS: list[Platform] = [
Platform.NUMBER,
Platform.SELECT,
Platform.CLIMATE,
Platform.SENSOR,
# Number should be after CLIMATE
Platform.NUMBER,
Platform.BINARY_SENSOR,
Platform.SWITCH,
]
CONF_HEATER = "heater_entity_id"
CONF_HEATER_2 = "heater_entity2_id"
CONF_HEATER_3 = "heater_entity3_id"
CONF_HEATER_4 = "heater_entity4_id"
CONF_UNDERLYING_LIST = "underlying_entity_ids"
CONF_HEATER_KEEP_ALIVE = "heater_keep_alive"
CONF_TEMP_SENSOR = "temperature_sensor_entity_id"
CONF_LAST_SEEN_TEMP_SENSOR = "last_seen_temperature_sensor_entity_id"
CONF_EXTERNAL_TEMP_SENSOR = "external_temperature_sensor_entity_id"
CONF_POWER_SENSOR = "power_sensor_entity_id"
CONF_MAX_POWER_SENSOR = "max_power_sensor_entity_id"
@@ -80,22 +87,17 @@ CONF_THERMOSTAT_CENTRAL_CONFIG = "thermostat_central_config"
CONF_THERMOSTAT_SWITCH = "thermostat_over_switch"
CONF_THERMOSTAT_CLIMATE = "thermostat_over_climate"
CONF_THERMOSTAT_VALVE = "thermostat_over_valve"
CONF_CLIMATE = "climate_entity_id"
CONF_CLIMATE_2 = "climate_entity2_id"
CONF_CLIMATE_3 = "climate_entity3_id"
CONF_CLIMATE_4 = "climate_entity4_id"
CONF_USE_WINDOW_FEATURE = "use_window_feature"
CONF_USE_MOTION_FEATURE = "use_motion_feature"
CONF_USE_PRESENCE_FEATURE = "use_presence_feature"
CONF_USE_POWER_FEATURE = "use_power_feature"
CONF_USE_CENTRAL_BOILER_FEATURE = "use_central_boiler_feature"
CONF_USE_AUTO_START_STOP_FEATURE = "use_auto_start_stop_feature"
CONF_AC_MODE = "ac_mode"
CONF_SONOFF_TRZB_MODE = "sonoff_trvzb_mode"
CONF_WINDOW_AUTO_OPEN_THRESHOLD = "window_auto_open_threshold"
CONF_WINDOW_AUTO_CLOSE_THRESHOLD = "window_auto_close_threshold"
CONF_WINDOW_AUTO_MAX_DURATION = "window_auto_max_duration"
CONF_VALVE = "valve_entity_id"
CONF_VALVE_2 = "valve_entity2_id"
CONF_VALVE_3 = "valve_entity3_id"
CONF_VALVE_4 = "valve_entity4_id"
CONF_AUTO_REGULATION_MODE = "auto_regulation_mode"
CONF_AUTO_REGULATION_NONE = "auto_regulation_none"
CONF_AUTO_REGULATION_SLOW = "auto_regulation_slow"
@@ -105,6 +107,7 @@ CONF_AUTO_REGULATION_STRONG = "auto_regulation_strong"
CONF_AUTO_REGULATION_EXPERT = "auto_regulation_expert"
CONF_AUTO_REGULATION_DTEMP = "auto_regulation_dtemp"
CONF_AUTO_REGULATION_PERIOD_MIN = "auto_regulation_periode_min"
CONF_AUTO_REGULATION_USE_DEVICE_TEMP = "auto_regulation_use_device_temp"
CONF_INVERSE_SWITCH = "inverse_switch_command"
CONF_AUTO_FAN_MODE = "auto_fan_mode"
CONF_AUTO_FAN_NONE = "auto_fan_none"
@@ -113,10 +116,28 @@ CONF_AUTO_FAN_MEDIUM = "auto_fan_medium"
CONF_AUTO_FAN_HIGH = "auto_fan_high"
CONF_AUTO_FAN_TURBO = "auto_fan_turbo"
CONF_STEP_TEMPERATURE = "step_temperature"
CONF_OFFSET_CALIBRATION_LIST = "offset_calibration_entity_ids"
CONF_OPENING_DEGREE_LIST = "opening_degree_entity_ids"
CONF_CLOSING_DEGREE_LIST = "closing_degree_entity_ids"
# Deprecated
CONF_HEATER = "heater_entity_id"
CONF_HEATER_2 = "heater_entity2_id"
CONF_HEATER_3 = "heater_entity3_id"
CONF_HEATER_4 = "heater_entity4_id"
CONF_CLIMATE = "climate_entity_id"
CONF_CLIMATE_2 = "climate_entity2_id"
CONF_CLIMATE_3 = "climate_entity3_id"
CONF_CLIMATE_4 = "climate_entity4_id"
CONF_VALVE = "valve_entity_id"
CONF_VALVE_2 = "valve_entity2_id"
CONF_VALVE_3 = "valve_entity3_id"
CONF_VALVE_4 = "valve_entity4_id"
# Global params into configuration.yaml
CONF_SHORT_EMA_PARAMS = "short_ema_params"
CONF_SAFETY_MODE = "safety_mode"
CONF_MAX_ON_PERCENT = "max_on_percent"
CONF_USE_MAIN_CENTRAL_CONFIG = "use_main_central_config"
CONF_USE_TPI_CENTRAL_CONFIG = "use_tpi_central_config"
@@ -129,13 +150,42 @@ CONF_USE_ADVANCED_CENTRAL_CONFIG = "use_advanced_central_config"
CONF_USE_CENTRAL_MODE = "use_central_mode"
CONF_ADD_CENTRAL_BOILER_CONTROL = "add_central_boiler_control"
CONF_CENTRAL_BOILER_ACTIVATION_SRV = "central_boiler_activation_service"
CONF_CENTRAL_BOILER_DEACTIVATION_SRV = "central_boiler_deactivation_service"
CONF_USED_BY_CENTRAL_BOILER = "used_by_controls_central_boiler"
CONF_WINDOW_ACTION = "window_action"
CONF_AUTO_START_STOP_LEVEL = "auto_start_stop_level"
AUTO_START_STOP_LEVEL_NONE = "auto_start_stop_none"
AUTO_START_STOP_LEVEL_SLOW = "auto_start_stop_slow"
AUTO_START_STOP_LEVEL_MEDIUM = "auto_start_stop_medium"
AUTO_START_STOP_LEVEL_FAST = "auto_start_stop_fast"
CONF_AUTO_START_STOP_LEVELS = [
AUTO_START_STOP_LEVEL_NONE,
AUTO_START_STOP_LEVEL_SLOW,
AUTO_START_STOP_LEVEL_MEDIUM,
AUTO_START_STOP_LEVEL_FAST,
]
# For explicit typing purpose only
TYPE_AUTO_START_STOP_LEVELS = Literal[ # pylint: disable=invalid-name
AUTO_START_STOP_LEVEL_FAST,
AUTO_START_STOP_LEVEL_MEDIUM,
AUTO_START_STOP_LEVEL_SLOW,
AUTO_START_STOP_LEVEL_NONE,
]
HVAC_OFF_REASON_NAME = "hvac_off_reason"
HVAC_OFF_REASON_MANUAL = "manual"
HVAC_OFF_REASON_AUTO_START_STOP = "auto_start_stop"
HVAC_OFF_REASON_WINDOW_DETECTION = "window_detection"
HVAC_OFF_REASONS = Literal[ # pylint: disable=invalid-name
HVAC_OFF_REASON_MANUAL,
HVAC_OFF_REASON_AUTO_START_STOP,
HVAC_OFF_REASON_WINDOW_DETECTION,
]
DEFAULT_SHORT_EMA_PARAMS = {
"max_alpha": 0.5,
# In sec
@@ -144,7 +194,7 @@ DEFAULT_SHORT_EMA_PARAMS = {
}
CONF_PRESETS = {
p: f"{p}_temp"
p: f"{p}{PRESET_TEMP_SUFFIX}"
for p in (
PRESET_FROST_PROTECTION,
PRESET_ECO,
@@ -154,7 +204,7 @@ CONF_PRESETS = {
}
CONF_PRESETS_WITH_AC = {
p: f"{p}_temp"
p: f"{p}{PRESET_TEMP_SUFFIX}"
for p in (
PRESET_FROST_PROTECTION,
PRESET_ECO,
@@ -170,7 +220,7 @@ CONF_PRESETS_WITH_AC = {
PRESET_AWAY_SUFFIX = "_away"
CONF_PRESETS_AWAY = {
p: f"{p}_temp"
p: f"{p}{PRESET_TEMP_SUFFIX}"
for p in (
PRESET_FROST_PROTECTION + PRESET_AWAY_SUFFIX,
PRESET_ECO + PRESET_AWAY_SUFFIX,
@@ -180,7 +230,7 @@ CONF_PRESETS_AWAY = {
}
CONF_PRESETS_AWAY_WITH_AC = {
p: f"{p}_temp"
p: f"{p}{PRESET_TEMP_SUFFIX}"
for p in (
PRESET_FROST_PROTECTION + PRESET_AWAY_SUFFIX,
PRESET_ECO + PRESET_AWAY_SUFFIX,
@@ -207,10 +257,7 @@ CONF_PRESETS_AWAY_WITH_AC_VALUES = list(CONF_PRESETS_AWAY_WITH_AC.values())
ALL_CONF = (
[
CONF_NAME,
CONF_HEATER,
CONF_HEATER_2,
CONF_HEATER_3,
CONF_HEATER_4,
CONF_HEATER_KEEP_ALIVE,
CONF_TEMP_SENSOR,
CONF_EXTERNAL_TEMP_SENSOR,
CONF_POWER_SENSOR,
@@ -239,22 +286,17 @@ ALL_CONF = (
CONF_THERMOSTAT_TYPE,
CONF_THERMOSTAT_SWITCH,
CONF_THERMOSTAT_CLIMATE,
CONF_CLIMATE,
CONF_CLIMATE_2,
CONF_CLIMATE_3,
CONF_CLIMATE_4,
CONF_USE_WINDOW_FEATURE,
CONF_USE_MOTION_FEATURE,
CONF_USE_PRESENCE_FEATURE,
CONF_USE_POWER_FEATURE,
CONF_USE_CENTRAL_BOILER_FEATURE,
CONF_AC_MODE,
CONF_VALVE,
CONF_VALVE_2,
CONF_VALVE_3,
CONF_VALVE_4,
CONF_SONOFF_TRZB_MODE,
CONF_AUTO_REGULATION_MODE,
CONF_AUTO_REGULATION_DTEMP,
CONF_AUTO_REGULATION_PERIOD_MIN,
CONF_AUTO_REGULATION_USE_DEVICE_TEMP,
CONF_INVERSE_SWITCH,
CONF_AUTO_FAN_MODE,
CONF_USE_MAIN_CENTRAL_CONFIG,
@@ -266,7 +308,6 @@ ALL_CONF = (
CONF_USE_PRESENCE_CENTRAL_CONFIG,
CONF_USE_ADVANCED_CENTRAL_CONFIG,
CONF_USE_CENTRAL_MODE,
CONF_ADD_CENTRAL_BOILER_CONTROL,
CONF_USED_BY_CENTRAL_BOILER,
CONF_CENTRAL_BOILER_ACTIVATION_SRV,
CONF_CENTRAL_BOILER_DEACTIVATION_SRV,
@@ -319,7 +360,11 @@ CONF_WINDOW_ACTIONS = [
CONF_WINDOW_ECO_TEMP,
]
SUPPORT_FLAGS = ClimateEntityFeature.TARGET_TEMPERATURE
SUPPORT_FLAGS = (
ClimateEntityFeature.TARGET_TEMPERATURE
| ClimateEntityFeature.TURN_OFF
| ClimateEntityFeature.TURN_ON
)
SERVICE_SET_PRESENCE = "set_presence"
SERVICE_SET_PRESET_TEMPERATURE = "set_preset_temperature"
@@ -357,7 +402,9 @@ CENTRAL_MODES = [
class RegulationParamSlow:
"""Light parameters for slow latency regulation"""
kp: float = 0.2 # 20% of the current internal regulation offset are caused by the current difference of target temperature and room temperature
kp: float = (
0.2 # 20% of the current internal regulation offset are caused by the current difference of target temperature and room temperature
)
ki: float = (
0.8 / 288.0
) # 80% of the current internal regulation offset are caused by the average offset of the past 24 hours
@@ -365,7 +412,9 @@ class RegulationParamSlow:
1.0 / 25.0
) # this will add 1°C to the offset when it's 25°C colder outdoor than indoor
offset_max: float = 2.0 # limit to a final offset of -2°C to +2°C
stabilization_threshold: float = 0.0 # this needs to be disabled as otherwise the long term accumulated error will always be reset when the temp briefly crosses from/to below/above the target
stabilization_threshold: float = (
0.0 # this needs to be disabled as otherwise the long term accumulated error will always be reset when the temp briefly crosses from/to below/above the target
)
accumulated_error_threshold: float = (
2.0 * 288
) # this allows up to 2°C long term offset in both directions
@@ -415,9 +464,9 @@ class RegulationParamVeryStrong:
kp: float = 0.6
ki: float = 0.1
k_ext: float = 0.2
offset_max: float = 4
offset_max: float = 8
stabilization_threshold: float = 0.1
accumulated_error_threshold: float = 30
accumulated_error_threshold: float = 80
class EventType(Enum):
@@ -430,6 +479,7 @@ class EventType(Enum):
CENTRAL_BOILER_EVENT: str = "versatile_thermostat_central_boiler_event"
PRESET_EVENT: str = "versatile_thermostat_preset_event"
WINDOW_AUTO_EVENT: str = "versatile_thermostat_window_auto_event"
AUTO_START_STOP_EVENT: str = "versatile_thermostat_auto_start_stop_event"
def send_vtherm_event(hass, event_type: EventType, entity, data: dict):
@@ -457,6 +507,15 @@ class ServiceConfigurationError(HomeAssistantError):
"""Error in the service configuration to control the central boiler"""
class ConfigurationNotCompleteError(HomeAssistantError):
"""Error the configuration is not complete"""
class SonoffTRVZBNbEntitiesIncorrect(HomeAssistantError):
"""Error to indicate there is an error in the configuration of the Sonoff TRVZB.
The number of specific entities is incorrect."""
class overrides: # pylint: disable=invalid-name
"""An annotation to inform overrides"""

View File

@@ -0,0 +1,18 @@
{
"entity": {
"climate": {
"versatile_thermostat": {
"state_attributes": {
"preset_mode": {
"state": {
"shedding": "mdi:power-plug-off",
"safety": "mdi:shield-alert",
"none": "mdi:knob",
"frost": "mdi:snowflake"
}
}
}
}
}
}
}

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",
"requirements": [],
"ssdp": [],
"version": "5.4.0",
"version": "6.8.0",
"zeroconf": []
}

View File

@@ -6,24 +6,79 @@ import logging
# from homeassistant.const import EVENT_HOMEASSISTANT_START
from homeassistant.core import HomeAssistant, CoreState # , callback
from homeassistant.components.number import NumberEntity, NumberMode
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 custom_components.versatile_thermostat.vtherm_api import VersatileThermostatAPI
from .const import (
DOMAIN,
DEVICE_MANUFACTURER,
CONF_NAME,
CONF_THERMOSTAT_TYPE,
CONF_THERMOSTAT_CENTRAL_CONFIG,
CONF_ADD_CENTRAL_BOILER_CONTROL,
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__)
@@ -40,19 +95,101 @@ async def async_setup_entry(
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_ADD_CENTRAL_BOILER_CONTROL)
# is_central_boiler = entry.data.get(CONF_USE_CENTRAL_BOILER_FEATURE)
if vt_type != CONF_THERMOSTAT_CENTRAL_CONFIG or not is_central_boiler:
return
entities = []
entities = [
ActivateBoilerThresholdNumber(hass, unique_id, name, entry.data),
]
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
)
)
async_add_entities(entities, True)
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):
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"""
@@ -115,3 +252,273 @@ class ActivateBoilerThresholdNumber(NumberEntity, RestoreEntity):
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):
"""Set the new 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.
if self.accumulated_error < 0:
self.accumulated_error = 0
# Discussion #384. Finally don't reset the accumulated error but smoothly reset it if the sign is inversed
# if self.accumulated_error < 0:
# self.accumulated_error = 0
def calculate_regulated_temperature(
self, internal_temp: float, external_temp: float
self, room_temp: float, external_temp: float
): # pylint: disable=unused-argument
"""Calculate a new target_temp given some temperature"""
if internal_temp is None:
if room_temp is None:
_LOGGER.warning(
"Temporarily skipping the self-regulation algorithm while the configured sensor for room temperature is unavailable"
)
@@ -68,9 +68,14 @@ class PITemperatureRegulator:
return self.target_temp
# Calculate the error factor (P)
error = self.target_temp - internal_temp
error = self.target_temp - room_temp
# Calculate the sum of error (I)
# Discussion #384. Finally don't reset the accumulated error but smoothly reset it if the sign is inversed
# If the error have change its sign, reset smoothly the accumulated error
if error * self.accumulated_error < 0:
self.accumulated_error = self.accumulated_error / 2.0
self.accumulated_error += error
# Capping of the error
@@ -83,19 +88,12 @@ class PITemperatureRegulator:
offset = self.kp * error + self.ki * self.accumulated_error
# Calculate the exterior offset
# For Maia tests - use the internal_temp vs external_temp and not target_temp - external_temp
offset_ext = self.k_ext * (internal_temp - external_temp)
offset_ext = self.k_ext * (room_temp - external_temp)
# Capping of offset_ext
# Capping of offset
total_offset = offset + offset_ext
total_offset = min(self.offset_max, max(-self.offset_max, total_offset))
# If temperature is near the target_temp, reset the accumulated_error
# 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)
_LOGGER.debug(

View File

@@ -1,6 +1,9 @@
""" The TPI calculation module """
# pylint: disable='line-too-long'
import logging
from homeassistant.components.climate import HVACMode
_LOGGER = logging.getLogger(__name__)
PROPORTIONAL_FUNCTION_ATAN = "atan"
@@ -12,6 +15,11 @@ PROPORTIONAL_MIN_DURATION_SEC = 10
FUNCTION_TYPE = [PROPORTIONAL_FUNCTION_ATAN, PROPORTIONAL_FUNCTION_LINEAR]
def is_number(value):
"""check if value is a number"""
return isinstance(value, (int, float))
class PropAlgorithm:
"""This class aims to do all calculation of the Proportional alogorithm"""
@@ -22,16 +30,44 @@ class PropAlgorithm:
tpi_coef_ext,
cycle_min: int,
minimal_activation_delay: int,
vtherm_entity_id: str = None,
max_on_percent: float = None,
) -> None:
"""Initialisation of the Proportional Algorithm"""
_LOGGER.debug(
"Creation new PropAlgorithm function_type: %s, tpi_coef_int: %s, tpi_coef_ext: %s, cycle_min:%d, minimal_activation_delay:%d", # 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,
tpi_coef_int,
tpi_coef_ext,
cycle_min,
minimal_activation_delay,
)
# Issue 506 - check parameters
if (
vtherm_entity_id is None
or not is_number(tpi_coef_int)
or not is_number(tpi_coef_ext)
or not is_number(cycle_min)
or not is_number(minimal_activation_delay)
or function_type != PROPORTIONAL_FUNCTION_TPI
):
_LOGGER.error(
"%s - configuration is wrong. function_type=%s, entity_id is %s, tpi_coef_int is %s, tpi_coef_ext is %s, cycle_min is %s, minimal_activation_delay is %s",
vtherm_entity_id,
function_type,
vtherm_entity_id,
tpi_coef_int,
tpi_coef_ext,
cycle_min,
minimal_activation_delay,
)
raise TypeError(
"TPI parameters are not set correctly. VTherm will not work as expected. Please reconfigure it correctly. See previous log for values"
)
self._vtherm_entity_id = vtherm_entity_id
self._function = function_type
self._tpi_coef_int = tpi_coef_int
self._tpi_coef_ext = tpi_coef_ext
@@ -43,27 +79,32 @@ class PropAlgorithm:
self._off_time_sec = self._cycle_min * 60
self._security = False
self._default_on_percent = 0
self._max_on_percent = max_on_percent
def calculate(
self,
target_temp: float,
current_temp: float,
ext_current_temp: float,
cooling=False,
target_temp: float | None,
current_temp: float | None,
ext_current_temp: float | None,
hvac_mode: HVACMode,
):
"""Do the calculation of the duration"""
if target_temp is None or current_temp is None:
_LOGGER.warning(
"Proportional algorithm: calculation is not possible cause target_temp or current_temp is null. Heating/cooling will be disabled" # pylint: disable=line-too-long
log = _LOGGER.debug if hvac_mode == HVACMode.OFF else _LOGGER.warning
log(
"%s - Proportional algorithm: calculation is not possible cause target_temp (%s) or current_temp (%s) is null. Heating/cooling will be disabled. This could be normal at startup", # pylint: disable=line-too-long
self._vtherm_entity_id,
target_temp,
current_temp,
)
self._calculated_on_percent = 0
else:
if cooling:
if hvac_mode == HVACMode.COOL:
delta_temp = current_temp - target_temp
delta_ext_temp = (
ext_current_temp
ext_current_temp - target_temp
if ext_current_temp is not None
else 0 - target_temp
else 0
)
else:
delta_temp = target_temp - current_temp
@@ -80,7 +121,8 @@ class PropAlgorithm:
)
else:
_LOGGER.warning(
"Proportional algorithm: unknown %s function. Heating will be disabled",
"%s - Proportional algorithm: unknown %s function. Heating will be disabled",
self._vtherm_entity_id,
self._function,
)
self._calculated_on_percent = 0
@@ -88,7 +130,8 @@ class PropAlgorithm:
self._calculate_internal()
_LOGGER.debug(
"heating percent calculated for current_temp %.1f, ext_current_temp %.1f and target_temp %.1f is %.2f, on_time is %d (sec), off_time is %d (sec)", # pylint: disable=line-too-long
"%s - heating percent calculated for current_temp %.1f, ext_current_temp %.1f and target_temp %.1f is %.2f, on_time is %d (sec), off_time is %d (sec)", # pylint: disable=line-too-long
self._vtherm_entity_id,
current_temp if current_temp else -9999.0,
ext_current_temp if ext_current_temp else -9999.0,
target_temp if target_temp else -9999.0,
@@ -107,11 +150,12 @@ class PropAlgorithm:
self._calculated_on_percent = 0
if self._security:
_LOGGER.debug(
"Security is On using the default_on_percent %f",
self._default_on_percent,
)
self._on_percent = self._default_on_percent
_LOGGER.info(
"%s - Security is On using the default_on_percent %f",
self._vtherm_entity_id,
self._on_percent,
)
else:
_LOGGER.debug(
"Security is Off using the calculated_on_percent %f",
@@ -119,19 +163,23 @@ class PropAlgorithm:
)
self._on_percent = self._calculated_on_percent
if self._max_on_percent is not None and self._on_percent > self._max_on_percent:
_LOGGER.debug(
"%s - Heating period clamped to %s (instead of %s) due to max_on_percent setting.",
self._vtherm_entity_id,
self._max_on_percent,
self._on_percent,
)
self._on_percent = self._max_on_percent
self._on_time_sec = self._on_percent * self._cycle_min * 60
# Do not heat for less than xx sec
if self._on_time_sec < self._minimal_activation_delay:
if self._on_time_sec > 0:
_LOGGER.info(
"No heating period due to heating period too small (%f < %f)",
self._on_time_sec,
self._minimal_activation_delay,
)
else:
_LOGGER.debug(
"No heating period due to heating period too small (%f < %f)",
"%s - No heating period due to heating period too small (%f < %f)",
self._vtherm_entity_id,
self._on_time_sec,
self._minimal_activation_delay,
)
@@ -141,12 +189,18 @@ class PropAlgorithm:
def set_security(self, default_on_percent: float):
"""Set a default value for on_percent (used for safety mode)"""
_LOGGER.info(
"%s - Proportional Algo - set security to ON", self._vtherm_entity_id
)
self._security = True
self._default_on_percent = default_on_percent
self._calculate_internal()
def unset_security(self):
"""Unset the safety mode"""
_LOGGER.info(
"%s - Proportional Algo - set security to OFF", self._vtherm_entity_id
)
self._security = False
self._calculate_internal()

View File

@@ -3,21 +3,20 @@
""" Implements the VersatileThermostat select component """
import logging
from homeassistant.const import EVENT_HOMEASSISTANT_START
from homeassistant.core import HomeAssistant, CoreState, callback
from homeassistant.core import HomeAssistant
from homeassistant.components.climate import ClimateEntity, DOMAIN as CLIMATE_DOMAIN
from homeassistant.components.select import SelectEntity
from homeassistant.helpers.device_registry import DeviceInfo, DeviceEntryType
from homeassistant.config_entries import ConfigEntry
from homeassistant.helpers.restore_state import RestoreEntity
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.entity_component import EntityComponent
from custom_components.versatile_thermostat.base_thermostat import (
BaseThermostat,
ConfigData,
)
from custom_components.versatile_thermostat.vtherm_api import VersatileThermostatAPI
from .const import (
DOMAIN,
DEVICE_MANUFACTURER,
@@ -96,17 +95,20 @@ class CentralModeSelect(SelectEntity, RestoreEntity):
if old_state is not None:
self._attr_current_option = old_state.state
@callback
async def _async_startup_internal(*_):
_LOGGER.debug("%s - Calling async_startup_internal", self)
await self.notify_central_mode_change()
api: VersatileThermostatAPI = VersatileThermostatAPI.get_vtherm_api(self.hass)
api.register_central_mode_select(self)
if self.hass.state == CoreState.running:
await _async_startup_internal()
else:
self.hass.bus.async_listen_once(
EVENT_HOMEASSISTANT_START, _async_startup_internal
)
# @callback
# async def _async_startup_internal(*_):
# _LOGGER.debug("%s - Calling async_startup_internal", self)
# await self.notify_central_mode_change()
#
# if self.hass.state == CoreState.running:
# await _async_startup_internal()
# else:
# self.hass.bus.async_listen_once(
# EVENT_HOMEASSISTANT_START, _async_startup_internal
# )
@overrides
async def async_select_option(self, option: str) -> None:
@@ -120,19 +122,17 @@ class CentralModeSelect(SelectEntity, RestoreEntity):
self._attr_current_option = option
await self.notify_central_mode_change(old_central_mode=old_option)
@overrides
def select_option(self, option: str) -> None:
"""Change the selected option"""
# Update the VTherms which have temperature in central config
self.hass.create_task(self.async_select_option(option))
async def notify_central_mode_change(self, old_central_mode: str | None = None):
"""Notify all VTherm that the central_mode have change"""
api: VersatileThermostatAPI = VersatileThermostatAPI.get_vtherm_api(self.hass)
# Update all VTherm states
component: EntityComponent[ClimateEntity] = self.hass.data[CLIMATE_DOMAIN]
for entity in component.entities:
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
)
await api.notify_central_mode_change(old_central_mode)
def __str__(self) -> str:
return f"VersatileThermostat-{self.name}"

View File

@@ -17,7 +17,6 @@ from homeassistant.components.sensor import (
SensorEntity,
SensorDeviceClass,
SensorStateClass,
UnitOfTemperature,
)
from homeassistant.config_entries import ConfigEntry
@@ -49,7 +48,8 @@ from .const import (
CONF_THERMOSTAT_CLIMATE,
CONF_THERMOSTAT_TYPE,
CONF_THERMOSTAT_CENTRAL_CONFIG,
CONF_ADD_CENTRAL_BOILER_CONTROL,
CONF_USE_CENTRAL_BOILER_FEATURE,
CONF_SONOFF_TRZB_MODE,
overrides,
)
@@ -71,15 +71,15 @@ async def async_setup_entry(
unique_id = entry.entry_id
name = entry.data.get(CONF_NAME)
vt_type = entry.data.get(CONF_THERMOSTAT_TYPE)
is_sonoff_trvzb = entry.data.get(CONF_SONOFF_TRZB_MODE)
entities = None
if vt_type == CONF_THERMOSTAT_CENTRAL_CONFIG:
if entry.data.get(CONF_ADD_CENTRAL_BOILER_CONTROL):
if entry.data.get(CONF_USE_CENTRAL_BOILER_FEATURE):
entities = [
NbActiveDeviceForBoilerSensor(hass, unique_id, name, entry.data)
]
async_add_entities(entities, True)
else:
entities = [
LastTemperatureSensor(hass, unique_id, name, entry.data),
@@ -100,14 +100,21 @@ async def async_setup_entry(
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_VALVE
or is_sonoff_trvzb
):
entities.append(ValveOpenPercentSensor(hass, unique_id, name, entry.data))
if entry.data.get(CONF_THERMOSTAT_TYPE) == CONF_THERMOSTAT_CLIMATE:
if (
entry.data.get(CONF_THERMOSTAT_TYPE) == CONF_THERMOSTAT_CLIMATE
and not is_sonoff_trvzb
):
entities.append(
RegulatedTemperatureSensor(hass, unique_id, name, entry.data)
)
if entities:
async_add_entities(entities, True)
@@ -125,15 +132,15 @@ class EnergySensor(VersatileThermostatBaseEntity, SensorEntity):
"""Called when my climate have change"""
_LOGGER.debug("%s - climate state change", self._attr_unique_id)
if math.isnan(self.my_climate.total_energy) or math.isinf(
self.my_climate.total_energy
):
energy = self.my_climate.total_energy
if energy is None:
return
if math.isnan(energy) or math.isinf(energy):
raise ValueError(f"Sensor has illegal state {self.my_climate.total_energy}")
old_state = self._attr_native_value
self._attr_native_value = round(
self.my_climate.total_energy, self.suggested_display_precision
)
self._attr_native_value = round(energy, self.suggested_display_precision)
if old_state != self._attr_native_value:
self.async_write_ha_state()
return
@@ -570,7 +577,7 @@ class RegulatedTemperatureSensor(VersatileThermostatBaseEntity, SensorEntity):
@property
def native_unit_of_measurement(self) -> str | None:
if not self.my_climate:
return UnitOfTemperature.CELSIUS
return self.hass.config.units.temperature_unit
return self.my_climate.temperature_unit
@property
@@ -621,7 +628,7 @@ class EMATemperatureSensor(VersatileThermostatBaseEntity, SensorEntity):
@property
def native_unit_of_measurement(self) -> str | None:
if not self.my_climate:
return UnitOfTemperature.CELSIUS
return self.hass.config.units.temperature_unit
return self.my_climate.temperature_unit
@property
@@ -730,7 +737,7 @@ class NbActiveDeviceForBoilerSensor(SensorEntity):
entity.name,
)
if (
entity.hvac_mode == HVACMode.HEAT
entity.hvac_mode in [HVACMode.HEAT, HVACMode.AUTO]
and entity.hvac_action == HVACAction.HEATING
):
for under in entity.underlying_entities:

View File

@@ -12,76 +12,91 @@
"thermostat_type": "Only one central configuration type is possible"
}
},
"menu": {
"title": "Menu",
"description": "Configure your thermostat. You will be able to finalize the configuration when all required parameters are entered.",
"menu_options": {
"main": "Main attributes",
"central_boiler": "Central boiler",
"type": "Underlyings",
"tpi": "TPI parameters",
"features": "Features",
"presets": "Presets",
"window": "Window detection",
"motion": "Motion detection",
"power": "Power management",
"presence": "Presence detection",
"advanced": "Advanced parameters",
"auto_start_stop": "Auto start and stop",
"sonoff_trvzb": "Sonoff TRVZB configuration",
"finalize": "All done",
"configuration_not_complete": "Configuration not complete"
}
},
"main": {
"title": "Add new Versatile Thermostat",
"description": "Main mandatory attributes",
"data": {
"name": "Name",
"thermostat_type": "Thermostat type",
"temperature_sensor_entity_id": "Temperature sensor entity id",
"temperature_sensor_entity_id": "Room temperature",
"last_seen_temperature_sensor_entity_id": "Last seen room temperature datetime",
"external_temperature_sensor_entity_id": "Outdoor temperature sensor entity id",
"cycle_min": "Cycle duration (minutes)",
"temp_min": "Minimal temperature allowed",
"temp_max": "Maximal temperature allowed",
"temp_min": "Minimum temperature allowed",
"temp_max": "Maximum temperature allowed",
"step_temperature": "Temperature step",
"device_power": "Device power",
"use_central_mode": "Enable the control by central entity (need central config). Check to enable the control of the VTherm with the select central_mode entities.",
"use_central_mode": "Enable the control by central entity (requires central config). Check to enable the control of the VTherm with the select central_mode entities.",
"use_main_central_config": "Use additional central main configuration. Check to use the central main configuration (outdoor temperature, min, max, step, ...).",
"used_by_controls_central_boiler": "Used by central boiler. Check if this VTherm should have control on the central boiler"
},
"data_description": {
"temperature_sensor_entity_id": "Room temperature sensor entity id",
"last_seen_temperature_sensor_entity_id": "Last seen room temperature sensor entity id. Should be datetime sensor",
"external_temperature_sensor_entity_id": "Outdoor temperature sensor entity id. Not used if central configuration is selected"
}
},
"features": {
"title": "Features",
"description": "Thermostat features",
"data": {
"use_window_feature": "Use window detection",
"use_motion_feature": "Use motion detection",
"use_power_feature": "Use power management",
"use_presence_feature": "Use presence detection",
"use_main_central_config": "Use central main configuration. Check to use the central main configuration. Uncheck to use a specific main configuration for this VTherm",
"add_central_boiler_control": "Add 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 need heating, the boiler will be turned on. If no VTherm needs heating, the boiler will be turned off. Commands for turning on/off the central boiler are given in the next configuration page",
"used_by_controls_central_boiler": "Used by central boiler. Check if this VTherm should have control on the central boiler"
},
"data_description": {
"external_temperature_sensor_entity_id": "Outdoor temperature sensor entity id. Not used if central configuration is selected"
"use_central_boiler_feature": "Use a central boiler. Check to add a control to your central boiler. You will have to configure the VTherm which will have a control of the central boiler after seecting this checkbox to take effect. If one VTherm requires heating, the boiler will be turned on. If no VTherm requires heating, the boiler will be turned off. Commands for turning on/off the central boiler are given in the related configuration page",
"use_auto_start_stop_feature": "Use the auto start and stop feature"
}
},
"type": {
"title": "Linked entities",
"description": "Linked entities attributes",
"data": {
"heater_entity_id": "1st heater switch",
"heater_entity2_id": "2nd heater switch",
"heater_entity3_id": "3rd heater switch",
"heater_entity4_id": "4th heater switch",
"underlying_entity_ids": "The device(s) to be controlled",
"heater_keep_alive": "Switch keep-alive interval in seconds",
"proportional_function": "Algorithm",
"climate_entity_id": "1st underlying climate",
"climate_entity2_id": "2nd underlying climate",
"climate_entity3_id": "3rd underlying climate",
"climate_entity4_id": "4th underlying climate",
"ac_mode": "AC mode",
"valve_entity_id": "1st valve number",
"valve_entity2_id": "2nd valve number",
"valve_entity3_id": "3rd valve number",
"valve_entity4_id": "4th valve number",
"sonoff_trvzb_mode": "SONOFF TRVZB mode",
"auto_regulation_mode": "Self-regulation",
"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",
"auto_fan_mode": " Auto fan mode"
"auto_fan_mode": "Auto fan mode"
},
"data_description": {
"heater_entity_id": "Mandatory heater entity id",
"heater_entity2_id": "Optional 2nd Heater entity id. Leave empty if not used",
"heater_entity3_id": "Optional 3rd Heater entity id. Leave empty if not used",
"heater_entity4_id": "Optional 4th Heater entity id. Leave empty if not used",
"underlying_entity_ids": "The device(s) to be controlled - 1 is required",
"heater_keep_alive": "Optional heater switch state refresh interval. Leave empty if not required.",
"proportional_function": "Algorithm to use (TPI is the only one for now)",
"climate_entity_id": "Underlying climate entity id",
"climate_entity2_id": "2nd underlying climate entity id",
"climate_entity3_id": "3rd underlying climate entity id",
"climate_entity4_id": "4th underlying climate entity id",
"ac_mode": "Use the Air Conditioning (AC) mode",
"valve_entity_id": "1st valve number entity id",
"valve_entity2_id": "2nd valve number entity id",
"valve_entity3_id": "3rd valve number entity id",
"valve_entity4_id": "4th valve number entity id",
"sonoff_trvzb_mode": "The underlyings are SONOFF TRVZB. You have to configure some extra entities in the specific menu option 'Sonoff trvzb configuration'",
"auto_regulation_mode": "Auto adjustment of the target temperature",
"auto_regulation_dtemp": "The threshold in ° (or % for valve) under which the temperature change will not be sent",
"auto_regulation_periode_min": "Duration in minutes between two regulation update",
"auto_regulation_use_device_temp": "Use the eventual internal temperature sensor of the underlying to speedup the self-regulation",
"inverse_switch_command": "For switch with pilot wire and diode you may need to inverse the command",
"auto_fan_mode": " Automatically activate fan when huge heating/cooling is necessary"
"auto_fan_mode": "Automatically activate fan when huge heating/cooling is necessary"
}
},
"tpi": {
@@ -100,26 +115,9 @@
},
"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": {
"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"
},
"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": {
@@ -140,8 +138,8 @@
"window_auto_open_threshold": "Recommended value: between 3 and 10. Leave empty if automatic window open detection is not used",
"window_auto_close_threshold": "Recommended value: 0. Leave empty if automatic window open detection is not used",
"window_auto_max_duration": "Recommended value: 60 (one hour). Leave empty if automatic window open detection is not used",
"use_window_central_config": "Check to use the central window configuration. Uncheck to use a specific window configuration for this VTherm",
"window_action": "Action to do if window is deteted as open"
"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": {
@@ -166,7 +164,7 @@
},
"power": {
"title": "Power management",
"description": "Power management attributes.\nGives the power and max power sensor of your home.\nThen specify the power consumption of the heater when on.\nAll sensors and device power should have the same unit (kW or W).",
"description": "Power management attributes.\nGives the power and max power sensor of your home.\nSpecify the power consumption of the heater when on.\nAll sensors and device power should use the same unit (kW or W).",
"data": {
"power_sensor_entity_id": "Power",
"max_power_sensor_entity_id": "Max power",
@@ -185,51 +183,64 @@
"description": "Presence management attributes.\nGives the a presence sensor of your home (true is someone is present) and give the corresponding temperature preset setting.",
"data": {
"presence_sensor_entity_id": "Presence sensor",
"eco_away_temp": "Eco preset",
"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"
"use_presence_central_config": "Use central presence temperature configuration. Deselect to use specific temperature entities"
},
"data_description": {
"presence_sensor_entity_id": "Presence sensor entity id",
"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"
"presence_sensor_entity_id": "Presence sensor entity id"
}
},
"advanced": {
"title": "Advanced parameters",
"description": "Configuration of advanced parameters. Leave the default values if you don't know what you are doing.\nThese parameters can lead to very poor temperature control or bad power regulation.",
"data": {
"minimal_activation_delay": "Minimal activation delay",
"minimal_activation_delay": "Minimum activation delay",
"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",
"use_advanced_central_config": "Use central advanced configuration"
},
"data_description": {
"minimal_activation_delay": "Delay in seconds under which the equipment will not be activated",
"security_delay_min": "Maximum allowed delay in minutes between two temperature measurements. Above this delay the thermostat will turn to a safety off state",
"security_min_on_percent": "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",
"use_advanced_central_config": "Check to use the central advanced configuration. Uncheck to use a specific advanced configuration for this VTherm"
}
},
"central_boiler": {
"title": "Control of the central boiler",
"description": "Enter the services to call to turn on/off the central boiler. Leave blank if no service call is to be made (in this case, you will have to manage the turning on/off of your central boiler yourself). The service called must be formatted as follows: `entity_id/service_name[/attribute:value]` (/attribute:value is optional)\nFor example:\n- to turn on a switch: `switch.controle_chaudiere/switch.turn_on`\n- to turn off a switch: `switch.controle_chaudiere/switch.turn_off`\n- to program the boiler to 25° and thus force its ignition: `climate.thermostat_chaudiere/climate.set_temperature/temperature:25`\n- to send 10° to the boiler and thus force its extinction: `climate.thermostat_chaudiere/climate.set_temperature/temperature:10`",
"data": {
"central_boiler_activation_service": "Command to turn-on",
"central_boiler_deactivation_service": "Command to turn-off"
},
"data_description": {
"central_boiler_activation_service": "Command to turn-on the central boiler formatted like entity_id/service_name[/attribut:valeur]",
"central_boiler_deactivation_service": "Command to turn-off the central boiler formatted like entity_id/service_name[/attribut:valeur]"
}
},
"sonoff_trvzb": {
"title": "Sonoff TRVZB configuration",
"description": "Specific Sonoff TRVZB configuration",
"data": {
"offset_calibration_entity_ids": "Offset calibration entities",
"opening_degree_entity_ids": "Opening degree entities",
"closing_degree_entity_ids": "Closing degree entities",
"proportional_function": "Algorithm"
},
"data_description": {
"offset_calibration_entity_ids": "The list of the 'offset calibration' entities. There should be one per underlying climate entities",
"opening_degree_entity_ids": "The list of the 'opening degree' entities. There should be one per underlying climate entities",
"closing_degree_entity_ids": "The list of the 'closing degree' entities. There should be one per underlying climate entities",
"proportional_function": "Algorithm to use (TPI is the only one for now)"
}
}
},
"error": {
"unknown": "Unexpected error",
"unknown_entity": "Unknown entity id",
"window_open_detection_method": "Only one window open detection method should be used. Use either window sensor or automatic detection through temperature threshold but not both",
"no_central_config": "You cannot check 'use central configuration' because no central configuration was found. You need to create a Versatile Thermostat of type 'Central Configuration' to use it."
"no_central_config": "You cannot select 'use central configuration' because no central configuration was found. You need to create a Versatile Thermostat of type 'Central Configuration' to use it."
},
"abort": {
"already_configured": "Device is already configured"
@@ -247,76 +258,91 @@
"thermostat_type": "Only one central configuration type is possible"
}
},
"menu": {
"title": "Menu",
"description": "Configure your thermostat. You will be able to finalize the configuration when all required parameters are entered.",
"menu_options": {
"main": "Main attributes",
"central_boiler": "Central boiler",
"type": "Underlyings",
"tpi": "TPI parameters",
"features": "Features",
"presets": "Presets",
"window": "Window detection",
"motion": "Motion detection",
"power": "Power management",
"presence": "Presence detection",
"advanced": "Advanced parameters",
"auto_start_stop": "Auto start and stop",
"sonoff_trvzb": "Sonoff TRVZB configuration",
"finalize": "All done",
"configuration_not_complete": "Configuration not complete"
}
},
"main": {
"title": "Main - {name}",
"description": "Main mandatory attributes",
"data": {
"name": "Name",
"thermostat_type": "Thermostat type",
"temperature_sensor_entity_id": "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",
"cycle_min": "Cycle duration (minutes)",
"temp_min": "Minimal temperature allowed",
"temp_max": "Maximal temperature allowed",
"temp_min": "Minimum temperature allowed",
"temp_max": "Maximum temperature allowed",
"step_temperature": "Temperature step",
"device_power": "Device power",
"use_central_mode": "Enable the control by central entity (need central config). Check to enable the control of the VTherm with the select central_mode entities.",
"use_central_mode": "Enable the control by central entity (requires central config). Check to enable the control of the VTherm with the select central_mode entities.",
"use_main_central_config": "Use additional central main configuration. Check to use the central main configuration (outdoor temperature, min, max, step, ...).",
"used_by_controls_central_boiler": "Used by central boiler. Check if this VTherm should have control on the central boiler"
},
"data_description": {
"temperature_sensor_entity_id": "Room temperature sensor entity id",
"last_seen_temperature_sensor_entity_id": "Last seen room temperature sensor entity id. Should be datetime sensor",
"external_temperature_sensor_entity_id": "Outdoor temperature sensor entity id. Not used if central configuration is selected"
}
},
"features": {
"title": "Features - {name}",
"description": "Thermostat features",
"data": {
"use_window_feature": "Use window detection",
"use_motion_feature": "Use motion detection",
"use_power_feature": "Use power management",
"use_presence_feature": "Use presence detection",
"use_main_central_config": "Use central main configuration. Check to use the central main configuration. Uncheck to use a specific main configuration for this VTherm",
"add_central_boiler_control": "Add 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 need heating, the boiler will be turned on. If no VTherm needs heating, the boiler will be turned off. Commands for turning on/off the central boiler are given in the next configuration page",
"used_by_controls_central_boiler": "Used by central boiler. Check if this VTherm should have control on the central boiler"
},
"data_description": {
"external_temperature_sensor_entity_id": "Outdoor temperature sensor entity id. Not used if central configuration is selected"
"use_central_boiler_feature": "Use a central boiler. Check to add a control to your central boiler. You will have to configure the VTherm which will have a control of the central boiler after seecting this checkbox to take effect. If one VTherm requires heating, the boiler will be turned on. If no VTherm requires heating, the boiler will be turned off. Commands for turning on/off the central boiler are given in the related configuration page",
"use_auto_start_stop_feature": "Use the auto start and stop feature"
}
},
"type": {
"title": "Entities - {name}",
"description": "Linked entities attributes",
"data": {
"heater_entity_id": "1st heater switch",
"heater_entity2_id": "2nd heater switch",
"heater_entity3_id": "3rd heater switch",
"heater_entity4_id": "4th heater switch",
"underlying_entity_ids": "The device(s) to be controlled",
"heater_keep_alive": "Switch keep-alive interval in seconds",
"proportional_function": "Algorithm",
"climate_entity_id": "1st underlying climate",
"climate_entity2_id": "2nd underlying climate",
"climate_entity3_id": "3rd underlying climate",
"climate_entity4_id": "4th underlying climate",
"ac_mode": "AC mode",
"valve_entity_id": "1st valve number",
"valve_entity2_id": "2nd valve number",
"valve_entity3_id": "3rd valve number",
"valve_entity4_id": "4th valve number",
"sonoff_trvzb_mode": "SONOFF TRVZB mode",
"auto_regulation_mode": "Self-regulation",
"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",
"auto_fan_mode": " Auto fan mode"
"auto_fan_mode": "Auto fan mode"
},
"data_description": {
"heater_entity_id": "Mandatory heater entity id",
"heater_entity2_id": "Optional 2nd Heater entity id. Leave empty if not used",
"heater_entity3_id": "Optional 3rd Heater entity id. Leave empty if not used",
"heater_entity4_id": "Optional 4th Heater entity id. Leave empty if not used",
"underlying_entity_ids": "The device(s) to be controlled - 1 is required",
"heater_keep_alive": "Optional heater switch state refresh interval. Leave empty if not required.",
"proportional_function": "Algorithm to use (TPI is the only one for now)",
"climate_entity_id": "Underlying climate entity id",
"climate_entity2_id": "2nd underlying climate entity id",
"climate_entity3_id": "3rd underlying climate entity id",
"climate_entity4_id": "4th underlying climate entity id",
"ac_mode": "Use the Air Conditioning (AC) mode",
"valve_entity_id": "1st valve number entity id",
"valve_entity2_id": "2nd valve number entity id",
"valve_entity3_id": "3rd valve number entity id",
"valve_entity4_id": "4th valve number entity id",
"sonoff_trvzb_mode": "The underlyings are SONOFF TRVZB. You have to configure some extra entities in the specific menu option 'Sonoff trvzb configuration'",
"auto_regulation_mode": "Auto adjustment of the target temperature",
"auto_regulation_dtemp": "The threshold in ° (or % for valve) under which the temperature change will not be sent",
"auto_regulation_periode_min": "Duration in minutes between two regulation update",
"auto_regulation_use_device_temp": "Use the eventual internal temperature sensor of the underlying to speedup the self-regulation",
"inverse_switch_command": "For switch with pilot wire and diode you may need to 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": {
@@ -335,26 +361,9 @@
},
"presets": {
"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": {
"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"
},
"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": {
@@ -420,44 +429,57 @@
"description": "Presence management attributes.\nGives the a presence sensor of your home (true is someone is present) and give the corresponding temperature preset setting.",
"data": {
"presence_sensor_entity_id": "Presence sensor",
"eco_away_temp": "Eco away preset",
"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"
"use_presence_central_config": "Use central presence temperature configuration. Uncheck to use specific temperature entities"
},
"data_description": {
"presence_sensor_entity_id": "Presence sensor entity id",
"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"
"presence_sensor_entity_id": "Presence sensor entity id"
}
},
"advanced": {
"title": "Advanced - {name}",
"description": "Advanced parameters. Leave the default values if you don't know what you are doing.\nThese parameters can lead to very poor temperature control or bad power regulation.",
"data": {
"minimal_activation_delay": "Minimal activation delay",
"minimal_activation_delay": "Minimum activation delay",
"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",
"use_advanced_central_config": "Use central advanced configuration"
},
"data_description": {
"minimal_activation_delay": "Delay in seconds under which the equipment will not be activated",
"security_delay_min": "Maximum allowed delay in minutes between two temperature measurements. Above this delay the thermostat will turn to a safety off state",
"security_min_on_percent": "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",
"use_advanced_central_config": "Check to use the central advanced configuration. Uncheck to use a specific advanced configuration for this VTherm"
}
},
"central_boiler": {
"title": "Control of the central boiler - {name}",
"description": "Enter the services to call to turn on/off the central boiler. Leave blank if no service call is to be made (in this case, you will have to manage the turning on/off of your central boiler yourself). The service called must be formatted as follows: `entity_id/service_name[/attribute:value]` (/attribute:value is optional)\nFor example:\n- to turn on a switch: `switch.controle_chaudiere/switch.turn_on`\n- to turn off a switch: `switch.controle_chaudiere/switch.turn_off`\n- to program the boiler to 25° and thus force its ignition: `climate.thermostat_chaudiere/climate.set_temperature/temperature:25`\n- to send 10° to the boiler and thus force its extinction: `climate.thermostat_chaudiere/climate.set_temperature/temperature:10`",
"data": {
"central_boiler_activation_service": "Command to turn-on",
"central_boiler_deactivation_service": "Command to turn-off"
},
"data_description": {
"central_boiler_activation_service": "Command to turn-on the central boiler formatted like entity_id/service_name[/attribut:valeur]",
"central_boiler_deactivation_service": "Command to turn-off the central boiler formatted like entity_id/service_name[/attribut:valeur]"
}
},
"sonoff_trvzb": {
"title": "Sonoff TRVZB configuration - {name}",
"description": "Specific Sonoff TRVZB configuration",
"data": {
"offset_calibration_entity_ids": "Offset calibration entities",
"opening_degree_entity_ids": "Opening degree entities",
"closing_degree_entity_ids": "Closing degree entities",
"proportional_function": "Algorithm"
},
"data_description": {
"offset_calibration_entity_ids": "The list of the 'offset calibration' entities. There should be one per underlying climate entities",
"opening_degree_entity_ids": "The list of the 'opening degree' entities. There should be one per underlying climate entities",
"closing_degree_entity_ids": "The list of the 'closing degree' entities. There should be one per underlying climate entities",
"proportional_function": "Algorithm to use (TPI is the only one for now)"
}
}
},
"error": {
@@ -465,7 +487,8 @@
"unknown_entity": "Unknown entity id",
"window_open_detection_method": "Only one window open detection method should be used. Use either window sensor or automatic detection through temperature threshold but not both",
"no_central_config": "You cannot check 'use central configuration' because no central configuration was found. You need to create a Versatile Thermostat of type 'Central Configuration' to use it.",
"service_configuration_format": "The format of the service configuration is wrong"
"service_configuration_format": "The format of the service configuration is wrong",
"sonoff_trvzb_nb_entities_incorrect": "The number of specific entities for Sonoff TRVZB should be equal to the number of underlyings"
},
"abort": {
"already_configured": "Device is already configured"
@@ -514,6 +537,14 @@
"comfort": "Comfort",
"boost": "Boost"
}
},
"auto_start_stop": {
"options": {
"auto_start_stop_none": "No auto start/stop",
"auto_start_stop_slow": "Slow detection",
"auto_start_stop_medium": "Medium detection",
"auto_start_stop_fast": "Fast detection"
}
}
},
"entity": {
@@ -524,11 +555,59 @@
"state": {
"power": "Shedding",
"security": "Safety",
"none": "Manual"
"none": "Manual",
"frost": "Frost"
}
}
}
}
},
"number": {
"frost_temp": {
"name": "Frost"
},
"eco_temp": {
"name": "Eco"
},
"comfort_temp": {
"name": "Comfort"
},
"boost_temp": {
"name": "Boost"
},
"frost_ac_temp": {
"name": "Frost ac"
},
"eco_ac_temp": {
"name": "Eco ac"
},
"comfort_ac_temp": {
"name": "Comfort ac"
},
"boost_ac_temp": {
"name": "Boost ac"
},
"frost_away_temp": {
"name": "Frost away"
},
"eco_away_temp": {
"name": "Eco away"
},
"comfort_away_temp": {
"name": "Comfort away"
},
"boost_away_temp": {
"name": "Boost away"
},
"eco_ac_away_temp": {
"name": "Eco ac away"
},
"comfort_ac_away_temp": {
"name": "Comfort ac away"
},
"boost_ac_away_temp": {
"name": "Boost ac away"
}
}
}
}

View File

@@ -0,0 +1,168 @@
## pylint: disable=unused-argument
""" Implements the VersatileThermostat select component """
import logging
from typing import Any
from homeassistant.core import HomeAssistant, callback
from homeassistant.components.switch import SwitchEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.helpers.restore_state import RestoreEntity
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .commons import VersatileThermostatBaseEntity
from .const import * # pylint: disable=unused-wildcard-import,wildcard-import
_LOGGER = logging.getLogger(__name__)
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the VersatileThermostat switches with config flow."""
_LOGGER.debug(
"Calling async_setup_entry entry=%s, data=%s", entry.entry_id, entry.data
)
unique_id = entry.entry_id
name = entry.data.get(CONF_NAME)
vt_type = entry.data.get(CONF_THERMOSTAT_TYPE)
auto_start_stop_feature = entry.data.get(CONF_USE_AUTO_START_STOP_FEATURE)
entities = []
if vt_type == CONF_THERMOSTAT_CLIMATE:
entities.append(FollowUnderlyingTemperatureChange(hass, unique_id, name, entry))
if auto_start_stop_feature is True:
# Creates a switch to enable the auto-start/stop
enable_entity = AutoStartStopEnable(hass, unique_id, name, entry)
entities.append(enable_entity)
async_add_entities(entities, True)
class AutoStartStopEnable(VersatileThermostatBaseEntity, SwitchEntity, RestoreEntity):
"""The that enables the ManagedDevice optimisation with"""
def __init__(
self, hass: HomeAssistant, unique_id: str, name: str, entry_infos: ConfigEntry
):
super().__init__(hass, unique_id, name)
self._attr_name = "Enable auto start/stop"
self._attr_unique_id = f"{self._device_name}_enable_auto_start_stop"
self._default_value = (
entry_infos.data.get(CONF_AUTO_START_STOP_LEVEL)
!= AUTO_START_STOP_LEVEL_NONE
)
self._attr_is_on = self._default_value
@property
def icon(self) -> str | None:
"""The icon"""
return "mdi:power-sleep"
async def async_added_to_hass(self):
await super().async_added_to_hass()
# Récupérer le dernier état sauvegardé de l'entité
last_state = await self.async_get_last_state()
# Si l'état précédent existe, vous pouvez l'utiliser
if last_state is not None:
self._attr_is_on = last_state.state == "on"
else:
# If no previous state set it to false by default
self._attr_is_on = self._default_value
self.update_my_state_and_vtherm()
def update_my_state_and_vtherm(self):
"""Update the auto_start_stop_enable flag in my VTherm"""
self.async_write_ha_state()
if self.my_climate is not None:
self.my_climate.set_auto_start_stop_enable(self._attr_is_on)
@callback
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the entity on."""
self.turn_on()
@callback
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the entity off."""
self.turn_off()
@overrides
def turn_off(self, **kwargs: Any):
self._attr_is_on = False
self.update_my_state_and_vtherm()
@overrides
def turn_on(self, **kwargs: Any):
self._attr_is_on = True
self.update_my_state_and_vtherm()
class FollowUnderlyingTemperatureChange(
VersatileThermostatBaseEntity, SwitchEntity, RestoreEntity
):
"""The that enables the ManagedDevice optimisation with"""
def __init__(
self, hass: HomeAssistant, unique_id: str, name: str, entry_infos: ConfigEntry
):
super().__init__(hass, unique_id, name)
self._attr_name = "Follow underlying temp change"
self._attr_unique_id = f"{self._device_name}_follow_underlying_temp_change"
self._attr_is_on = False
@property
def icon(self) -> str | None:
"""The icon"""
return "mdi:content-copy"
async def async_added_to_hass(self):
await super().async_added_to_hass()
# Récupérer le dernier état sauvegardé de l'entité
last_state = await self.async_get_last_state()
# Si l'état précédent existe, vous pouvez l'utiliser
if last_state is not None:
self._attr_is_on = last_state.state == "on"
else:
# If no previous state set it to false by default
self._attr_is_on = False
self.update_my_state_and_vtherm()
def update_my_state_and_vtherm(self):
"""Update the follow flag in my VTherm"""
self.async_write_ha_state()
if self.my_climate is not None:
self.my_climate.set_follow_underlying_temp_change(self._attr_is_on)
@callback
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the entity on."""
self.turn_on()
@callback
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the entity off."""
self.turn_off()
@overrides
def turn_off(self, **kwargs: Any):
self._attr_is_on = False
self.update_my_state_and_vtherm()
@overrides
def turn_on(self, **kwargs: Any):
self._attr_is_on = True
self.update_my_state_and_vtherm()

View File

@@ -1,15 +1,15 @@
# pylint: disable=line-too-long, too-many-lines
""" A climate over switch classe """
# pylint: disable=line-too-long, too-many-lines, abstract-method
""" A climate over climate classe """
import logging
from datetime import timedelta, datetime
from homeassistant.core import HomeAssistant, State, callback
from homeassistant.const import STATE_ON
from homeassistant.core import Event, HomeAssistant, State, callback
from homeassistant.helpers.event import (
async_track_state_change_event,
async_track_time_interval,
EventStateChangedData,
)
from homeassistant.helpers.typing import EventType as HASSEventType
from homeassistant.components.climate import (
HVACAction,
HVACMode,
@@ -20,44 +20,26 @@ from .commons import NowClass, round_to_nearest
from .base_thermostat import BaseThermostat, ConfigData
from .pi_algorithm import PITemperatureRegulator
from .const import (
overrides,
DOMAIN,
CONF_CLIMATE,
CONF_CLIMATE_2,
CONF_CLIMATE_3,
CONF_CLIMATE_4,
CONF_AUTO_REGULATION_MODE,
CONF_AUTO_REGULATION_NONE,
CONF_AUTO_REGULATION_SLOW,
CONF_AUTO_REGULATION_LIGHT,
CONF_AUTO_REGULATION_MEDIUM,
CONF_AUTO_REGULATION_STRONG,
CONF_AUTO_REGULATION_EXPERT,
CONF_AUTO_REGULATION_DTEMP,
CONF_AUTO_REGULATION_PERIOD_MIN,
CONF_AUTO_FAN_MODE,
CONF_AUTO_FAN_NONE,
CONF_AUTO_FAN_LOW,
CONF_AUTO_FAN_MEDIUM,
CONF_AUTO_FAN_HIGH,
CONF_AUTO_FAN_TURBO,
RegulationParamSlow,
RegulationParamLight,
RegulationParamMedium,
RegulationParamStrong,
AUTO_FAN_DTEMP_THRESHOLD,
AUTO_FAN_DEACTIVATED_MODES,
UnknownEntity,
)
from .const import * # pylint: disable=wildcard-import, unused-wildcard-import
from .vtherm_api import VersatileThermostatAPI
from .underlyings import UnderlyingClimate
from .auto_start_stop_algorithm import (
AutoStartStopDetectionAlgorithm,
AUTO_START_STOP_ACTION_OFF,
AUTO_START_STOP_ACTION_ON,
)
_LOGGER = logging.getLogger(__name__)
HVAC_ACTION_ON = [ # pylint: disable=invalid-name
HVACAction.COOLING,
HVACAction.DRYING,
HVACAction.FAN,
HVACAction.HEATING,
]
class ThermostatOverClimate(BaseThermostat):
class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
"""Representation of a base class for a Versatile Thermostat over a climate"""
_auto_regulation_mode: str | None = None
@@ -73,25 +55,31 @@ class ThermostatOverClimate(BaseThermostat):
# The fan_mode name depending of the current_mode
_auto_activated_fan_mode: str | None = None
_auto_deactivated_fan_mode: str | None = None
_auto_start_stop_level: TYPE_AUTO_START_STOP_LEVELS = AUTO_START_STOP_LEVEL_NONE
_auto_start_stop_algo: AutoStartStopDetectionAlgorithm | None = None
_is_auto_start_stop_enabled: bool = False
_follow_underlying_temp_change: bool = False
_entity_component_unrecorded_attributes = (
BaseThermostat._entity_component_unrecorded_attributes.union(
frozenset(
{
"is_over_climate",
"start_hvac_action_date",
"underlying_climate_0",
"underlying_climate_1",
"underlying_climate_2",
"underlying_climate_3",
"regulation_accumulated_error",
"auto_regulation_mode",
"auto_fan_mode",
"current_auto_fan_mode",
"auto_activated_fan_mode",
"auto_deactivated_fan_mode",
}
)
_entity_component_unrecorded_attributes = BaseThermostat._entity_component_unrecorded_attributes.union( # pylint: disable=protected-access
frozenset(
{
"is_over_climate",
"start_hvac_action_date",
"underlying_entities",
"regulation_accumulated_error",
"auto_regulation_mode",
"auto_fan_mode",
"current_auto_fan_mode",
"auto_activated_fan_mode",
"auto_deactivated_fan_mode",
"auto_regulation_use_device_temp",
"auto_start_stop_level",
"auto_start_stop_dtmin",
"auto_start_stop_enable",
"auto_start_stop_accumulated_error",
"auto_start_stop_accumulated_error_threshold",
"follow_underlying_temp_change",
}
)
)
@@ -104,20 +92,72 @@ class ThermostatOverClimate(BaseThermostat):
self._regulated_target_temp = self.target_temperature
self._last_regulation_change = NowClass.get_now(hass)
@overrides
def post_init(self, config_entry: ConfigData):
"""Initialize the Thermostat"""
super().post_init(config_entry)
for climate in config_entry.get(CONF_UNDERLYING_LIST):
under = UnderlyingClimate(
hass=self._hass,
thermostat=self,
climate_entity_id=climate,
)
self._underlyings.append(under)
self.choose_auto_regulation_mode(
config_entry.get(CONF_AUTO_REGULATION_MODE)
if config_entry.get(CONF_AUTO_REGULATION_MODE) is not None
else CONF_AUTO_REGULATION_NONE
)
self._auto_regulation_dtemp = (
config_entry.get(CONF_AUTO_REGULATION_DTEMP)
if config_entry.get(CONF_AUTO_REGULATION_DTEMP) is not None
else 0.5
)
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 5
)
self._auto_fan_mode = (
config_entry.get(CONF_AUTO_FAN_MODE)
if config_entry.get(CONF_AUTO_FAN_MODE) is not None
else CONF_AUTO_FAN_NONE
)
self._auto_regulation_use_device_temp = config_entry.get(
CONF_AUTO_REGULATION_USE_DEVICE_TEMP, False
)
use_auto_start_stop = config_entry.get(CONF_USE_AUTO_START_STOP_FEATURE, False)
if use_auto_start_stop:
self._auto_start_stop_level = config_entry.get(
CONF_AUTO_START_STOP_LEVEL, AUTO_START_STOP_LEVEL_NONE
)
else:
self._auto_start_stop_level = AUTO_START_STOP_LEVEL_NONE
# Instanciate the auto start stop algo
self._auto_start_stop_algo = AutoStartStopDetectionAlgorithm(
self._auto_start_stop_level, self.name
)
@property
def is_over_climate(self) -> bool:
"""True if the Thermostat is over_climate"""
return True
@property
def hvac_action(self) -> HVACAction | None:
"""Returns the current hvac_action by checking all hvac_action of the underlyings"""
def calculate_hvac_action(self, under_list: list) -> HVACAction | None:
"""Calculate an hvac action based on the hvac_action of the list in argument"""
# if one not IDLE or OFF -> return it
# else if one IDLE -> IDLE
# else OFF
one_idle = False
for under in self._underlyings:
for under in under_list:
if (action := under.hvac_action) not in [
HVACAction.IDLE,
HVACAction.OFF,
@@ -129,6 +169,11 @@ class ThermostatOverClimate(BaseThermostat):
return HVACAction.IDLE
return HVACAction.OFF
@property
def hvac_action(self) -> HVACAction | None:
"""Returns the current hvac_action by checking all hvac_action of the underlyings"""
return self.calculate_hvac_action(self._underlyings)
@overrides
async def _async_internal_set_temperature(self, temperature: float):
"""Set the target temperature and the target temperature of underlying climate if any"""
@@ -141,7 +186,17 @@ class ThermostatOverClimate(BaseThermostat):
"""Sends the regulated temperature to all underlying"""
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
_LOGGER.info(
@@ -166,12 +221,19 @@ class ThermostatOverClimate(BaseThermostat):
_LOGGER.info("%s - regulation calculation will be done", self)
new_regulated_temp = round_to_nearest(
self._regulation_algo.calculate_regulated_temperature(
self.current_temperature, self._cur_ext_temp
),
self._auto_regulation_dtemp,
)
# use _attr_target_temperature_step to round value if _auto_regulation_dtemp is equal to 0
regulation_step = self._auto_regulation_dtemp if self._auto_regulation_dtemp else self._attr_target_temperature_step
_LOGGER.debug("%s - usage regulation_step: %.2f ", self, regulation_step)
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
if not force and abs(dtemp) < self._auto_regulation_dtemp:
@@ -192,8 +254,33 @@ class ThermostatOverClimate(BaseThermostat):
self._last_regulation_change = now
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
):
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(
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):
@@ -241,49 +328,6 @@ class ThermostatOverClimate(BaseThermostat):
)
await self.async_set_fan_mode(self._auto_deactivated_fan_mode)
@overrides
def post_init(self, config_entry: ConfigData):
"""Initialize the Thermostat"""
super().post_init(config_entry)
for climate in [
CONF_CLIMATE,
CONF_CLIMATE_2,
CONF_CLIMATE_3,
CONF_CLIMATE_4,
]:
if config_entry.get(climate):
self._underlyings.append(
UnderlyingClimate(
hass=self._hass,
thermostat=self,
climate_entity_id=config_entry.get(climate),
)
)
self.choose_auto_regulation_mode(
config_entry.get(CONF_AUTO_REGULATION_MODE)
if config_entry.get(CONF_AUTO_REGULATION_MODE) is not None
else CONF_AUTO_REGULATION_NONE
)
self._auto_regulation_dtemp = (
config_entry.get(CONF_AUTO_REGULATION_DTEMP)
if config_entry.get(CONF_AUTO_REGULATION_DTEMP) is not None
else 0.5
)
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 5
)
self._auto_fan_mode = (
config_entry.get(CONF_AUTO_FAN_MODE)
if config_entry.get(CONF_AUTO_FAN_MODE) is not None
else CONF_AUTO_FAN_NONE
)
def choose_auto_regulation_mode(self, auto_regulation_mode: str):
"""Choose or change the regulation mode"""
self._auto_regulation_mode = auto_regulation_mode
@@ -451,48 +495,67 @@ class ThermostatOverClimate(BaseThermostat):
super().update_custom_attributes()
self._attr_extra_state_attributes["is_over_climate"] = self.is_over_climate
self._attr_extra_state_attributes[
"start_hvac_action_date"
] = self._underlying_climate_start_hvac_action_date
self._attr_extra_state_attributes["underlying_climate_0"] = self._underlyings[
0
].entity_id
self._attr_extra_state_attributes["underlying_climate_1"] = (
self._underlyings[1].entity_id if len(self._underlyings) > 1 else None
)
self._attr_extra_state_attributes["underlying_climate_2"] = (
self._underlyings[2].entity_id if len(self._underlyings) > 2 else None
)
self._attr_extra_state_attributes["underlying_climate_3"] = (
self._underlyings[3].entity_id if len(self._underlyings) > 3 else None
self._attr_extra_state_attributes["start_hvac_action_date"] = (
self._underlying_climate_start_hvac_action_date
)
self._attr_extra_state_attributes["underlying_entities"] = [
underlying.entity_id for underlying in self._underlyings
]
if self.is_regulated:
self._attr_extra_state_attributes["is_regulated"] = self.is_regulated
self._attr_extra_state_attributes[
"regulated_target_temperature"
] = self._regulated_target_temp
self._attr_extra_state_attributes[
"auto_regulation_mode"
] = self.auto_regulation_mode
self._attr_extra_state_attributes[
"regulation_accumulated_error"
] = self._regulation_algo.accumulated_error
self._attr_extra_state_attributes["regulated_target_temperature"] = (
self._regulated_target_temp
)
self._attr_extra_state_attributes["auto_regulation_mode"] = (
self.auto_regulation_mode
)
self._attr_extra_state_attributes["regulation_accumulated_error"] = (
self._regulation_algo.accumulated_error
)
self._attr_extra_state_attributes["auto_fan_mode"] = self.auto_fan_mode
self._attr_extra_state_attributes[
"current_auto_fan_mode"
] = self._current_auto_fan_mode
self._attr_extra_state_attributes["current_auto_fan_mode"] = (
self._current_auto_fan_mode
)
self._attr_extra_state_attributes["auto_activated_fan_mode"] = (
self._auto_activated_fan_mode
)
self._attr_extra_state_attributes["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._attr_extra_state_attributes["auto_start_stop_enable"] = (
self.auto_start_stop_enable
)
self._attr_extra_state_attributes["auto_start_stop_level"] = (
self._auto_start_stop_algo.level
)
self._attr_extra_state_attributes["auto_start_stop_dtmin"] = (
self._auto_start_stop_algo.dt_min
)
self._attr_extra_state_attributes["auto_start_stop_accumulated_error"] = (
self._auto_start_stop_algo.accumulated_error
)
self._attr_extra_state_attributes[
"auto_activated_fan_mode"
] = self._auto_activated_fan_mode
"auto_start_stop_accumulated_error_threshold"
] = self._auto_start_stop_algo.accumulated_error_threshold
self._attr_extra_state_attributes[
"auto_deactivated_fan_mode"
] = self._auto_deactivated_fan_mode
self._attr_extra_state_attributes["follow_underlying_temp_change"] = (
self._follow_underlying_temp_change
)
self.async_write_ha_state()
_LOGGER.debug(
"%s - Calling update_custom_attributes: %s",
self,
@@ -537,7 +600,21 @@ class ThermostatOverClimate(BaseThermostat):
):
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
_LOGGER.debug(
"%s - incremente_energy set energy is %s",
self,
self._total_energy,
)
else:
self._total_energy += added_energy
_LOGGER.debug(
"%s - incremente_energy incremented energy is %s",
self,
self._total_energy,
)
_LOGGER.debug(
"%s - added energy is %.3f . Total energy is now: %.3f",
self,
@@ -546,7 +623,7 @@ class ThermostatOverClimate(BaseThermostat):
)
@callback
async def _async_climate_changed(self, event: HASSEventType[EventStateChangedData]):
async def _async_climate_changed(self, event: Event[EventStateChangedData]):
"""Handle unerdlying climate state changes.
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
@@ -559,7 +636,8 @@ class ThermostatOverClimate(BaseThermostat):
async def end_climate_changed(changes: bool):
"""To end the event management"""
if changes:
self.async_write_ha_state()
# already done by update_custom_attribute
# self.async_write_ha_state()
self.update_custom_attributes()
await self.async_control_heating()
@@ -568,6 +646,15 @@ class ThermostatOverClimate(BaseThermostat):
if not new_state:
return
# Find the underlying which have change
under = self.find_underlying_by_entity_id(new_state.entity_id)
if not under:
_LOGGER.warning(
"We have a receive an event from entity %s which is NOT one of our underlying entities. This is not normal and should be reported to the developper of the integration"
)
return
changes = False
new_hvac_mode = new_state.state
@@ -602,20 +689,68 @@ class ThermostatOverClimate(BaseThermostat):
new_state.last_updated if new_state and new_state.last_updated else None
)
new_target_temp = (
new_state.attributes.get("temperature")
if new_state and new_state.attributes
else None
)
last_sent_temperature = under.last_sent_temperature or 0
under_temp_diff = (
(new_target_temp - last_sent_temperature) if new_target_temp else 0
)
if -1 < under_temp_diff < 1:
under_temp_diff = 0
# Issue 99 - some AC turn hvac_mode=cool and hvac_action=idle when sending a HVACMode_OFF command
# Issue 114 - Remove this because hvac_mode is now managed by local _hvac_mode and use idle action as is
# if self._hvac_mode == HVACMode.OFF and new_hvac_action == HVACAction.IDLE:
# _LOGGER.debug("The underlying switch to idle instead of OFF. We will consider it as OFF")
# new_hvac_mode = HVACMode.OFF
# Forget event when the event holds no real changes
if (
new_hvac_mode == self._hvac_mode
and new_hvac_action == old_hvac_action
and under_temp_diff == 0
and (new_fan_mode is None or new_fan_mode == self._attr_fan_mode)
):
_LOGGER.debug(
"%s - a underlying state change event is received but no real change have been found. Forget the event",
self,
)
return
# Ignore new target temperature when out of range
if (
not new_target_temp is None
and not self._attr_min_temp is None
and not self._attr_max_temp is None
and not (self._attr_min_temp <= new_target_temp <= self._attr_max_temp)
):
_LOGGER.debug(
"%s - underlying sent a target temperature (%s) which is out of configured min/max range (%s / %s). The value will be ignored",
self,
new_target_temp,
self._attr_min_temp,
self._attr_max_temp,
)
new_target_temp = None
under_temp_diff = 0
# A real changes have to be managed
_LOGGER.info(
"%s - Underlying climate %s changed. Event.new_hvac_mode is %s, current_hvac_mode=%s, new_hvac_action=%s, old_hvac_action=%s",
"%s - Underlying climate %s have changed. new_hvac_mode is %s (vs %s), new_hvac_action=%s (vs %s), new_target_temp=%s (vs %s), new_fan_mode=%s (vs %s)",
self,
new_state.entity_id,
under.entity_id,
new_hvac_mode,
self._hvac_mode,
new_hvac_action,
old_hvac_action,
new_target_temp,
self.target_temperature,
new_fan_mode,
self._attr_fan_mode,
)
_LOGGER.debug(
@@ -629,12 +764,6 @@ class ThermostatOverClimate(BaseThermostat):
)
# Interpretation of hvac action
HVAC_ACTION_ON = [ # pylint: disable=invalid-name
HVACAction.COOLING,
HVACAction.DRYING,
HVACAction.FAN,
HVACAction.HEATING,
]
if old_hvac_action not in HVAC_ACTION_ON and new_hvac_action in HVAC_ACTION_ON:
self._underlying_climate_start_hvac_action_date = (
self.get_last_updated_date_or_now(new_state)
@@ -667,6 +796,7 @@ class ThermostatOverClimate(BaseThermostat):
)
changes = True
# Filter new state when received just after a change from VTherm
# Issue #120 - Some TRV are changing target temperature a very long time (6 sec) after the change.
# In that case a loop is possible if a user change multiple times during this 6 sec.
if new_state_date_updated and self._last_change_time:
@@ -679,6 +809,7 @@ class ThermostatOverClimate(BaseThermostat):
await end_climate_changed(changes)
return
# Update all underlyings hvac_mode state if it has change
if (
new_hvac_mode
in [
@@ -693,7 +824,6 @@ class ThermostatOverClimate(BaseThermostat):
]
and self._hvac_mode != new_hvac_mode
):
# 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:
for under in self._underlyings:
@@ -724,35 +854,122 @@ class ThermostatOverClimate(BaseThermostat):
self._attr_fan_mode = new_fan_mode
changes = True
if not changes:
# try to manage new target temperature set if state
# try to manage new target temperature set if state if no other changes have been found
# and if a target temperature have already been sent
if (
self._follow_underlying_temp_change
and not changes
and under.last_sent_temperature is not None
):
_LOGGER.debug(
"Do temperature check. temperature is %s, new_state.attributes is %s",
self.target_temperature,
new_state.attributes,
"Do temperature check. under.last_sent_temperature is %s, new_target_temp is %s",
under.last_sent_temperature,
new_target_temp,
)
if (
# we do not change target temperature on regulated VTherm
not self.is_regulated
and new_state.attributes
and (new_target_temp := new_state.attributes.get("temperature"))
and new_target_temp != self.target_temperature
):
# if the underlying have change its target temperature
if under_temp_diff != 0:
_LOGGER.info(
"%s - Target temp in underlying have change to %s",
"%s - Target temp in underlying have change to %s (vs %s)",
self,
new_target_temp,
under.last_sent_temperature,
)
await self.async_set_temperature(temperature=new_target_temp)
changes = True
else:
_LOGGER.debug(
"%s - Forget the eventual underlying temperature change there is no real change",
self,
)
await end_climate_changed(changes)
async def check_auto_start_stop(self):
"""Check the auto-start-stop and an eventual action
Return False if we should stop the control_heating method"""
slope = (self.last_temperature_slope or 0) / 60 # to have the slope in °/min
action = self._auto_start_stop_algo.calculate_action(
self.hvac_mode,
self._saved_hvac_mode,
self.target_temperature,
self.current_temperature,
slope,
self.now,
)
_LOGGER.debug("%s - auto_start_stop action is %s", self, action)
if action == AUTO_START_STOP_ACTION_OFF and self.is_on:
_LOGGER.info(
"%s - Turning OFF the Vtherm due to auto-start-stop conditions",
self,
)
self.set_hvac_off_reason(HVAC_OFF_REASON_AUTO_START_STOP)
await self.async_turn_off()
# Send an event
self.send_event(
event_type=EventType.AUTO_START_STOP_EVENT,
data={
"type": "stop",
"name": self.name,
"cause": "Auto stop conditions reached",
"hvac_mode": self.hvac_mode,
"saved_hvac_mode": self._saved_hvac_mode,
"target_temperature": self.target_temperature,
"current_temperature": self.current_temperature,
"temperature_slope": round(slope, 3),
"accumulated_error": self._auto_start_stop_algo.accumulated_error,
"accumulated_error_threshold": self._auto_start_stop_algo.accumulated_error_threshold,
},
)
# Stop here
return False
elif (
action == AUTO_START_STOP_ACTION_ON
and self.hvac_off_reason == HVAC_OFF_REASON_AUTO_START_STOP
):
_LOGGER.info(
"%s - Turning ON the Vtherm due to auto-start-stop conditions", self
)
await self.async_turn_on()
# Send an event
self.send_event(
event_type=EventType.AUTO_START_STOP_EVENT,
data={
"type": "start",
"name": self.name,
"cause": "Auto start conditions reached",
"hvac_mode": self.hvac_mode,
"saved_hvac_mode": self._saved_hvac_mode,
"target_temperature": self.target_temperature,
"current_temperature": self.current_temperature,
"temperature_slope": round(slope, 3),
"accumulated_error": self._auto_start_stop_algo.accumulated_error,
"accumulated_error_threshold": self._auto_start_stop_algo.accumulated_error_threshold,
},
)
self.update_custom_attributes()
return True
@overrides
async def async_control_heating(self, force=False, _=None) -> bool:
"""The main function used to run the calculation at each cycle"""
ret = await super().async_control_heating(force, _)
# Check if we need to auto start/stop the Vtherm
if self.auto_start_stop_enable:
continu = await self.check_auto_start_stop()
if not continu:
return ret
else:
_LOGGER.debug("%s - auto start/stop is disabled", self)
# Continue the normal async_control_heating
# Send the regulated temperature to the underlyings
await self._send_regulated_temperature()
if self._auto_fan_mode and self._auto_fan_mode != CONF_AUTO_FAN_NONE:
@@ -760,6 +977,16 @@ class ThermostatOverClimate(BaseThermostat):
return ret
def set_auto_start_stop_enable(self, is_enabled: bool):
"""Enable/Disable the auto-start/stop feature"""
self._is_auto_start_stop_enabled = is_enabled
self.update_custom_attributes()
def set_follow_underlying_temp_change(self, follow: bool):
"""Set the flaf follow the underlying temperature changes"""
self._follow_underlying_temp_change = follow
self.update_custom_attributes()
@property
def auto_regulation_mode(self) -> str | None:
"""Get the regulation mode"""
@@ -770,6 +997,11 @@ class ThermostatOverClimate(BaseThermostat):
"""Get the auto fan mode"""
return self._auto_fan_mode
@property
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"""
@@ -841,10 +1073,7 @@ class ThermostatOverClimate(BaseThermostat):
@property
def temperature_unit(self) -> str:
"""Return the unit of measurement."""
if self.underlying_entity(0):
return self.underlying_entity(0).temperature_unit
return self._unit
return self.hass.config.units.temperature_unit
@property
def supported_features(self):
@@ -885,6 +1114,14 @@ class ThermostatOverClimate(BaseThermostat):
return None
@property
def current_humidity(self) -> float | None:
"""Return the humidity."""
if self.underlying_entity(0):
return self.underlying_entity(0).current_humidity
return None
@property
def is_aux_heat(self) -> bool | None:
"""Return true if aux heater.
@@ -904,6 +1141,21 @@ class ThermostatOverClimate(BaseThermostat):
return False
return True
@property
def auto_start_stop_level(self) -> TYPE_AUTO_START_STOP_LEVELS:
"""Return the auto start/stop level."""
return self._auto_start_stop_level
@property
def auto_start_stop_enable(self) -> bool:
"""Returns the auto_start_stop_enable"""
return self._is_auto_start_stop_enabled
@property
def follow_underlying_temp_change(self) -> bool:
"""Get the follow underlying temp change flag"""
return self._follow_underlying_temp_change
@overrides
def init_underlyings(self):
"""Init the underlyings if not already done"""
@@ -1035,3 +1287,29 @@ class ThermostatOverClimate(BaseThermostat):
self.choose_auto_fan_mode(CONF_AUTO_FAN_TURBO)
self.update_custom_attributes()
@overrides
async def async_turn_off(self) -> None:
# if window is open, don't overwrite the saved_hvac_mode
if self.window_state != STATE_ON:
self.save_hvac_mode()
await self.async_set_hvac_mode(HVACMode.OFF)
@overrides
async def async_turn_on(self) -> None:
# don't turn_on if window is open
if self.window_state == STATE_ON:
_LOGGER.info(
"%s - refuse to turn on because window is open. We keep the save_hvac_mode",
self,
)
return
if self._saved_hvac_mode is not None: # pylint: disable=protected-access
await self.restore_hvac_mode(True)
else:
if self._ac_mode:
await self.async_set_hvac_mode(HVACMode.COOL)
else:
await self.async_set_hvac_mode(HVACMode.HEAT)

View File

@@ -0,0 +1,249 @@
# pylint: disable=line-too-long, too-many-lines, abstract-method
""" A climate over Sonoff TRVZB classe """
import logging
from datetime import datetime
from homeassistant.core import HomeAssistant
from homeassistant.components.climate import HVACMode, HVACAction
from .underlyings import UnderlyingSonoffTRVZB
# from .commons import NowClass, round_to_nearest
from .base_thermostat import ConfigData
from .thermostat_climate import ThermostatOverClimate
from .prop_algorithm import PropAlgorithm
from .const import * # pylint: disable=wildcard-import, unused-wildcard-import
# from .vtherm_api import VersatileThermostatAPI
_LOGGER = logging.getLogger(__name__)
class ThermostatOverSonoffTRVZB(ThermostatOverClimate):
"""This class represent a VTherm over a Sonoff TRVZB climate"""
_entity_component_unrecorded_attributes = ThermostatOverClimate._entity_component_unrecorded_attributes.union( # pylint: disable=protected-access
frozenset(
{
"is_over_climate",
"is_over_sonoff_trvzb",
"underlying_entities",
"on_time_sec",
"off_time_sec",
"cycle_min",
"function",
"tpi_coef_int",
"tpi_coef_ext",
"power_percent",
}
)
)
_underlyings_sonoff_trvzb: list[UnderlyingSonoffTRVZB] = []
_valve_open_percent: int = 0
_last_calculation_timestamp: datetime | None = None
_auto_regulation_dpercent: float | None = None
_auto_regulation_period_min: int | None = None
def __init__(
self, hass: HomeAssistant, unique_id: str, name: str, entry_infos: ConfigData
):
"""Initialize the ThermostatOverSonoffTRVZB class"""
_LOGGER.debug("%s - creating a ThermostatOverSonoffTRVZB VTherm", name)
super().__init__(hass, unique_id, name, entry_infos)
# 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
@overrides
def post_init(self, config_entry: ConfigData):
"""Initialize the Thermostat and underlyings
Beware that the underlyings list contains the climate which represent the Sonoff TRVZB
but also the UnderlyingSonoff which reprensent the valve"""
super().post_init(config_entry)
self._auto_regulation_dpercent = (
config_entry.get(CONF_AUTO_REGULATION_DTEMP)
if config_entry.get(CONF_AUTO_REGULATION_DTEMP) is not None
else 0.0
)
self._auto_regulation_period_min = (
config_entry.get(CONF_AUTO_REGULATION_PERIOD_MIN)
if config_entry.get(CONF_AUTO_REGULATION_PERIOD_MIN) is not None
else 0
)
# Initialization of the TPI algo
self._prop_algorithm = PropAlgorithm(
self._proportional_function,
self._tpi_coef_int,
self._tpi_coef_ext,
self._cycle_min,
self._minimal_activation_delay,
self.name,
)
for idx, _ in enumerate(config_entry.get(CONF_UNDERLYING_LIST)):
offset = config_entry.get(CONF_OFFSET_CALIBRATION_LIST)[idx]
opening = config_entry.get(CONF_OPENING_DEGREE_LIST)[idx]
closing = config_entry.get(CONF_CLOSING_DEGREE_LIST)[idx]
under = UnderlyingSonoffTRVZB(
hass=self._hass,
thermostat=self,
offset_calibration_entity_id=offset,
opening_degree_entity_id=opening,
closing_degree_entity_id=closing,
climate_underlying=self._underlyings[idx],
)
self._underlyings_sonoff_trvzb.append(under)
@overrides
def update_custom_attributes(self):
"""Custom attributes"""
super().update_custom_attributes()
self._attr_extra_state_attributes["is_over_sonoff_trvzb"] = (
self.is_over_sonoff_trvzb
)
self._attr_extra_state_attributes["underlying_sonoff_trvzb_entities"] = [
underlying.entity_id for underlying in self._underlyings_sonoff_trvzb
]
self._attr_extra_state_attributes["on_percent"] = (
self._prop_algorithm.on_percent
)
self._attr_extra_state_attributes["power_percent"] = self.power_percent
self._attr_extra_state_attributes["on_time_sec"] = (
self._prop_algorithm.on_time_sec
)
self._attr_extra_state_attributes["off_time_sec"] = (
self._prop_algorithm.off_time_sec
)
self._attr_extra_state_attributes["cycle_min"] = self._cycle_min
self._attr_extra_state_attributes["function"] = self._proportional_function
self._attr_extra_state_attributes["tpi_coef_int"] = self._tpi_coef_int
self._attr_extra_state_attributes["tpi_coef_ext"] = self._tpi_coef_ext
self._attr_extra_state_attributes["valve_open_percent"] = (
self.valve_open_percent
)
self._attr_extra_state_attributes["auto_regulation_dpercent"] = (
self._auto_regulation_dpercent
)
self._attr_extra_state_attributes["auto_regulation_period_min"] = (
self._auto_regulation_period_min
)
self._attr_extra_state_attributes["last_calculation_timestamp"] = (
self._last_calculation_timestamp.astimezone(self._current_tz).isoformat()
if self._last_calculation_timestamp
else None
)
self.async_write_ha_state()
_LOGGER.debug(
"%s - Calling update_custom_attributes: %s",
self,
self._attr_extra_state_attributes,
)
@overrides
def recalculate(self):
"""A utility function to force the calculation of a the algo and
update the custom attributes and write the state
"""
_LOGGER.debug("%s - recalculate the open percent", self)
# TODO this is exactly the same method as the thermostat_valve recalculate. Put that in common
# For testing purpose. Should call _set_now() before
now = self.now
if self._last_calculation_timestamp is not None:
period = (now - self._last_calculation_timestamp).total_seconds() / 60
if period < self._auto_regulation_period_min:
_LOGGER.info(
"%s - do not calculate TPI because regulation_period (%d) is not exceeded",
self,
period,
)
return
self._prop_algorithm.calculate(
self._target_temp,
self._cur_temp,
self._cur_ext_temp,
self._hvac_mode or HVACMode.OFF,
)
new_valve_percent = round(
max(0, min(self.proportional_algorithm.on_percent, 1)) * 100
)
# Issue 533 - don't filter with dtemp if valve should be close. Else it will never close
if new_valve_percent < self._auto_regulation_dpercent:
new_valve_percent = 0
dpercent = new_valve_percent - self.valve_open_percent
if (
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_sonoff_trvzb:
under.set_valve_open_percent()
self._last_calculation_timestamp = now
self.update_custom_attributes()
@property
def is_over_sonoff_trvzb(self) -> bool:
"""True if the Thermostat is over_sonoff_trvzb"""
return True
@property
def power_percent(self) -> float | None:
"""Get the current on_percent value"""
if self._prop_algorithm:
return round(self._prop_algorithm.on_percent * 100, 0)
else:
return None
# @property
# def hvac_modes(self) -> list[HVACMode]:
# """Get the hvac_modes"""
# return self._hvac_list
@property
def valve_open_percent(self) -> int:
"""Gives the percentage of valve needed"""
if self._hvac_mode == HVACMode.OFF:
return 0
else:
return self._valve_open_percent
@property
def hvac_action(self) -> HVACAction | None:
"""Returns the current hvac_action by checking all hvac_action of the _underlyings_sonoff_trvzb"""
return self.calculate_hvac_action(self._underlyings_sonoff_trvzb)

View File

@@ -1,20 +1,17 @@
# pylint: disable=line-too-long
# pylint: disable=line-too-long, abstract-method
""" A climate over switch classe """
import logging
from homeassistant.core import callback
from homeassistant.core import Event, callback
from homeassistant.helpers.event import (
async_track_state_change_event,
EventStateChangedData,
)
from homeassistant.helpers.typing import EventType as HASSEventType
from homeassistant.components.climate import HVACMode
from .const import (
CONF_HEATER,
CONF_HEATER_2,
CONF_HEATER_3,
CONF_HEATER_4,
CONF_UNDERLYING_LIST,
CONF_HEATER_KEEP_ALIVE,
CONF_INVERSE_SWITCH,
overrides,
)
@@ -25,8 +22,7 @@ from .prop_algorithm import PropAlgorithm
_LOGGER = logging.getLogger(__name__)
class ThermostatOverSwitch(BaseThermostat):
class ThermostatOverSwitch(BaseThermostat[UnderlyingSwitch]):
"""Representation of a base class for a Versatile Thermostat over a switch."""
_entity_component_unrecorded_attributes = (
@@ -35,10 +31,7 @@ class ThermostatOverSwitch(BaseThermostat):
{
"is_over_switch",
"is_inversed",
"underlying_switch_0",
"underlying_switch_1",
"underlying_switch_2",
"underlying_switch_3",
"underlying_entities",
"on_time_sec",
"off_time_sec",
"cycle_min",
@@ -46,6 +39,7 @@ class ThermostatOverSwitch(BaseThermostat):
"tpi_coef_int",
"tpi_coef_ext",
"power_percent",
"calculated_on_percent",
}
)
)
@@ -87,15 +81,11 @@ class ThermostatOverSwitch(BaseThermostat):
self._tpi_coef_ext,
self._cycle_min,
self._minimal_activation_delay,
self.name,
max_on_percent=self._max_on_percent,
)
lst_switches = [config_entry.get(CONF_HEATER)]
if config_entry.get(CONF_HEATER_2):
lst_switches.append(config_entry.get(CONF_HEATER_2))
if config_entry.get(CONF_HEATER_3):
lst_switches.append(config_entry.get(CONF_HEATER_3))
if config_entry.get(CONF_HEATER_4):
lst_switches.append(config_entry.get(CONF_HEATER_4))
lst_switches = config_entry.get(CONF_UNDERLYING_LIST)
delta_cycle = self._cycle_min * 60 / len(lst_switches)
for idx, switch in enumerate(lst_switches):
@@ -105,6 +95,7 @@ class ThermostatOverSwitch(BaseThermostat):
thermostat=self,
switch_entity_id=switch,
initial_delay_sec=idx * delta_cycle,
keep_alive_sec=config_entry.get(CONF_HEATER_KEEP_ALIVE, 0),
)
)
@@ -125,6 +116,7 @@ class ThermostatOverSwitch(BaseThermostat):
self.hass, [switch.entity_id], self._async_switch_changed
)
)
switch.startup()
self.hass.create_task(self.async_control_heating())
@@ -133,20 +125,14 @@ class ThermostatOverSwitch(BaseThermostat):
"""Custom attributes"""
super().update_custom_attributes()
under0: UnderlyingSwitch = self._underlyings[0]
self._attr_extra_state_attributes["is_over_switch"] = self.is_over_switch
self._attr_extra_state_attributes["is_inversed"] = self.is_inversed
self._attr_extra_state_attributes["underlying_switch_0"] = self._underlyings[
0
].entity_id
self._attr_extra_state_attributes["underlying_switch_1"] = (
self._underlyings[1].entity_id if len(self._underlyings) > 1 else None
)
self._attr_extra_state_attributes["underlying_switch_2"] = (
self._underlyings[2].entity_id if len(self._underlyings) > 2 else None
)
self._attr_extra_state_attributes["underlying_switch_3"] = (
self._underlyings[3].entity_id if len(self._underlyings) > 3 else None
)
self._attr_extra_state_attributes["keep_alive_sec"] = under0.keep_alive_sec
self._attr_extra_state_attributes["underlying_entities"] = [
underlying.entity_id for underlying in self._underlyings
]
self._attr_extra_state_attributes[
"on_percent"
@@ -162,6 +148,9 @@ class ThermostatOverSwitch(BaseThermostat):
self._attr_extra_state_attributes["function"] = self._proportional_function
self._attr_extra_state_attributes["tpi_coef_int"] = self._tpi_coef_int
self._attr_extra_state_attributes["tpi_coef_ext"] = self._tpi_coef_ext
self._attr_extra_state_attributes[
"calculated_on_percent"
] = self._prop_algorithm.calculated_on_percent
self.async_write_ha_state()
_LOGGER.debug(
@@ -180,10 +169,11 @@ class ThermostatOverSwitch(BaseThermostat):
self._target_temp,
self._cur_temp,
self._cur_ext_temp,
self._hvac_mode == HVACMode.COOL,
self._hvac_mode or HVACMode.OFF,
)
self.update_custom_attributes()
self.async_write_ha_state()
# already done bu update_custom_attributes
# self.async_write_ha_state()
@overrides
def incremente_energy(self):
@@ -195,7 +185,23 @@ class ThermostatOverSwitch(BaseThermostat):
if not self.is_over_climate and self.mean_cycle_power is not None:
added_energy = self.mean_cycle_power * float(self._cycle_min) / 60.0
self._total_energy += added_energy
if self._total_energy is None:
self._total_energy = added_energy
_LOGGER.debug(
"%s - incremente_energy set energy is %s",
self,
self._total_energy,
)
else:
self._total_energy += added_energy
_LOGGER.debug(
"%s - incremente_energy increment energy is %s",
self,
self._total_energy,
)
self.update_custom_attributes()
_LOGGER.debug(
"%s - added energy is %.3f . Total energy is now: %.3f",
self,
@@ -204,7 +210,7 @@ class ThermostatOverSwitch(BaseThermostat):
)
@callback
def _async_switch_changed(self, event: HASSEventType[EventStateChangedData]):
def _async_switch_changed(self, event: Event[EventStateChangedData]):
"""Handle heater switch state changes."""
new_state = event.data.get("new_state")
old_state = event.data.get("old_state")

View File

@@ -1,4 +1,4 @@
# pylint: disable=line-too-long
# pylint: disable=line-too-long, abstract-method
""" A climate over switch classe """
import logging
from datetime import timedelta, datetime
@@ -8,18 +8,14 @@ from homeassistant.helpers.event import (
async_track_time_interval,
EventStateChangedData,
)
from homeassistant.helpers.typing import EventType as HASSEventType
from homeassistant.core import HomeAssistant, callback
from homeassistant.core import Event, HomeAssistant, callback
from homeassistant.components.climate import HVACMode
from .base_thermostat import BaseThermostat, ConfigData
from .prop_algorithm import PropAlgorithm
from .const import (
CONF_VALVE,
CONF_VALVE_2,
CONF_VALVE_3,
CONF_VALVE_4,
CONF_UNDERLYING_LIST,
# This is not really self-regulation but regulation here
CONF_AUTO_REGULATION_DTEMP,
CONF_AUTO_REGULATION_PERIOD_MIN,
@@ -30,30 +26,25 @@ from .underlyings import UnderlyingValve
_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"""
_entity_component_unrecorded_attributes = (
BaseThermostat._entity_component_unrecorded_attributes.union(
frozenset(
{
"is_over_valve",
"underlying_valve_0",
"underlying_valve_1",
"underlying_valve_2",
"underlying_valve_3",
"on_time_sec",
"off_time_sec",
"cycle_min",
"function",
"tpi_coef_int",
"tpi_coef_ext",
"auto_regulation_dpercent",
"auto_regulation_period_min",
"last_calculation_timestamp",
}
)
_entity_component_unrecorded_attributes = BaseThermostat._entity_component_unrecorded_attributes.union( # pylint: disable=protected-access
frozenset(
{
"is_over_valve",
"underlying_entities",
"on_time_sec",
"off_time_sec",
"cycle_min",
"function",
"tpi_coef_int",
"tpi_coef_ext",
"auto_regulation_dpercent",
"auto_regulation_period_min",
"last_calculation_timestamp",
"calculated_on_percent",
}
)
)
@@ -105,15 +96,11 @@ class ThermostatOverValve(BaseThermostat):
self._tpi_coef_ext,
self._cycle_min,
self._minimal_activation_delay,
self.name,
max_on_percent=self._max_on_percent,
)
lst_valves = [config_entry.get(CONF_VALVE)]
if config_entry.get(CONF_VALVE_2):
lst_valves.append(config_entry.get(CONF_VALVE_2))
if config_entry.get(CONF_VALVE_3):
lst_valves.append(config_entry.get(CONF_VALVE_3))
if config_entry.get(CONF_VALVE_4):
lst_valves.append(config_entry.get(CONF_VALVE_4))
lst_valves = config_entry.get(CONF_UNDERLYING_LIST)
for _, valve in enumerate(lst_valves):
self._underlyings.append(
@@ -148,7 +135,7 @@ class ThermostatOverValve(BaseThermostat):
)
@callback
async def _async_valve_changed(self, event: HASSEventType[EventStateChangedData]):
async def _async_valve_changed(self, event: Event[EventStateChangedData]):
"""Handle unerdlying valve state changes.
This method just log the change. It changes nothing to avoid loops.
"""
@@ -165,18 +152,10 @@ class ThermostatOverValve(BaseThermostat):
"valve_open_percent"
] = self.valve_open_percent
self._attr_extra_state_attributes["is_over_valve"] = self.is_over_valve
self._attr_extra_state_attributes["underlying_valve_0"] = self._underlyings[
0
].entity_id
self._attr_extra_state_attributes["underlying_valve_1"] = (
self._underlyings[1].entity_id if len(self._underlyings) > 1 else None
)
self._attr_extra_state_attributes["underlying_valve_2"] = (
self._underlyings[2].entity_id if len(self._underlyings) > 2 else None
)
self._attr_extra_state_attributes["underlying_valve_3"] = (
self._underlyings[3].entity_id if len(self._underlyings) > 3 else None
)
self._attr_extra_state_attributes["underlying_entities"] = [
underlying.entity_id for underlying in self._underlyings
]
self._attr_extra_state_attributes[
"on_percent"
@@ -202,6 +181,9 @@ class ThermostatOverValve(BaseThermostat):
if self._last_calculation_timestamp
else None
)
self._attr_extra_state_attributes[
"calculated_on_percent"
] = self._prop_algorithm.calculated_on_percent
self.async_write_ha_state()
_LOGGER.debug(
@@ -234,17 +216,23 @@ class ThermostatOverValve(BaseThermostat):
self._target_temp,
self._cur_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 (
dpercent >= -1 * self._auto_regulation_dpercent
and dpercent < self._auto_regulation_dpercent
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",
@@ -266,7 +254,8 @@ class ThermostatOverValve(BaseThermostat):
self._last_calculation_timestamp = now
self.update_custom_attributes()
self.async_write_ha_state()
# already done in update_custom_attributes
# self.async_write_ha_state()
@overrides
def incremente_energy(self):
@@ -278,7 +267,23 @@ class ThermostatOverValve(BaseThermostat):
if not self.is_over_climate and self.mean_cycle_power is not None:
added_energy = self.mean_cycle_power * float(self._cycle_min) / 60.0
self._total_energy += added_energy
if self._total_energy is None:
self._total_energy = added_energy
_LOGGER.debug(
"%s - incremente_energy set energy is %s",
self,
self._total_energy,
)
else:
self._total_energy += added_energy
_LOGGER.debug(
"%s - get_my_previous_state increment energy is %s",
self,
self._total_energy,
)
self.update_custom_attributes()
_LOGGER.debug(
"%s - added energy is %.3f . Total energy is now: %.3f",
self,

View File

@@ -43,7 +43,7 @@
"auto_regulation_dtemp": "Όριο ρύθμισης",
"auto_regulation_periode_min": "Ελάχιστη περίοδος ρύθμισης",
"inverse_switch_command": "Αντίστροφη εντολή διακόπτη",
"auto_fan_mode": " Auto fan mode"
"auto_fan_mode": "Auto fan mode"
},
"data_description": {
"heater_entity_id": "Υποχρεωτική ταυτότητα οντότητας θερμαντήρα",
@@ -64,7 +64,7 @@
"auto_regulation_dtemp": "Το όριο σε ° κάτω από το οποίο η αλλαγή θερμοκρασίας δεν θα αποστέλλεται",
"auto_regulation_periode_min": "Διάρκεια σε λεπτά μεταξύ δύο ενημερώσεων ρύθμισης",
"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": {
@@ -216,7 +216,7 @@
"auto_regulation_dtemp": "Όριο ρύθμισης",
"auto_regulation_periode_min": "Ελάχιστη περίοδος ρύθμισης",
"inverse_switch_command": "Αντίστροφη εντολή διακόπτη",
"auto_fan_mode": " Auto fan mode"
"auto_fan_mode": "Auto fan mode"
},
"data_description": {
"heater_entity_id": "Υποχρεωτική ταυτότητα οντότητας θερμαντήρα",
@@ -237,7 +237,7 @@
"auto_regulation_dtemp": "Το κατώφλι σε °C κάτω από το οποίο η αλλαγή της θερμοκρασίας δεν θα αποστέλλεται",
"auto_regulation_periode_min": "Διάρκεια σε λεπτά μεταξύ δύο ενημερώσεων ρύθμισης",
"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": {

View File

@@ -12,76 +12,91 @@
"thermostat_type": "Only one central configuration type is possible"
}
},
"menu": {
"title": "Menu",
"description": "Configure your thermostat. You will be able to finalize the configuration when all required parameters are entered.",
"menu_options": {
"main": "Main attributes",
"central_boiler": "Central boiler",
"type": "Underlyings",
"tpi": "TPI parameters",
"features": "Features",
"presets": "Presets",
"window": "Window detection",
"motion": "Motion detection",
"power": "Power management",
"presence": "Presence detection",
"advanced": "Advanced parameters",
"auto_start_stop": "Auto start and stop",
"sonoff_trvzb": "Sonoff TRVZB configuration",
"finalize": "All done",
"configuration_not_complete": "Configuration not complete"
}
},
"main": {
"title": "Add new Versatile Thermostat",
"description": "Main mandatory attributes",
"data": {
"name": "Name",
"thermostat_type": "Thermostat type",
"temperature_sensor_entity_id": "Temperature sensor entity id",
"temperature_sensor_entity_id": "Room temperature",
"last_seen_temperature_sensor_entity_id": "Last seen room temperature datetime",
"external_temperature_sensor_entity_id": "Outdoor temperature sensor entity id",
"cycle_min": "Cycle duration (minutes)",
"temp_min": "Minimal temperature allowed",
"temp_max": "Maximal temperature allowed",
"temp_min": "Minimum temperature allowed",
"temp_max": "Maximum temperature allowed",
"step_temperature": "Temperature step",
"device_power": "Device power",
"use_central_mode": "Enable the control by central entity (need central config). Check to enable the control of the VTherm with the select central_mode entities.",
"use_central_mode": "Enable the control by central entity (requires central config). Check to enable the control of the VTherm with the select central_mode entities.",
"use_main_central_config": "Use additional central main configuration. Check to use the central main configuration (outdoor temperature, min, max, step, ...).",
"used_by_controls_central_boiler": "Used by central boiler. Check if this VTherm should have control on the central boiler"
},
"data_description": {
"temperature_sensor_entity_id": "Room temperature sensor entity id",
"last_seen_temperature_sensor_entity_id": "Last seen room temperature sensor entity id. Should be datetime sensor",
"external_temperature_sensor_entity_id": "Outdoor temperature sensor entity id. Not used if central configuration is selected"
}
},
"features": {
"title": "Features",
"description": "Thermostat features",
"data": {
"use_window_feature": "Use window detection",
"use_motion_feature": "Use motion detection",
"use_power_feature": "Use power management",
"use_presence_feature": "Use presence detection",
"use_main_central_config": "Use central main configuration. Check to use the central main configuration. Uncheck to use a specific main configuration for this VTherm",
"add_central_boiler_control": "Add 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 need heating, the boiler will be turned on. If no VTherm needs heating, the boiler will be turned off. Commands for turning on/off the central boiler are given in the next configuration page",
"used_by_controls_central_boiler": "Used by central boiler. Check if this VTherm should have control on the central boiler"
},
"data_description": {
"external_temperature_sensor_entity_id": "Outdoor temperature sensor entity id. Not used if central configuration is selected"
"use_central_boiler_feature": "Use a central boiler. Check to add a control to your central boiler. You will have to configure the VTherm which will have a control of the central boiler after seecting this checkbox to take effect. If one VTherm requires heating, the boiler will be turned on. If no VTherm requires heating, the boiler will be turned off. Commands for turning on/off the central boiler are given in the related configuration page",
"use_auto_start_stop_feature": "Use the auto start and stop feature"
}
},
"type": {
"title": "Linked entities",
"description": "Linked entities attributes",
"data": {
"heater_entity_id": "1st heater switch",
"heater_entity2_id": "2nd heater switch",
"heater_entity3_id": "3rd heater switch",
"heater_entity4_id": "4th heater switch",
"underlying_entity_ids": "The device(s) to be controlled",
"heater_keep_alive": "Switch keep-alive interval in seconds",
"proportional_function": "Algorithm",
"climate_entity_id": "1st underlying climate",
"climate_entity2_id": "2nd underlying climate",
"climate_entity3_id": "3rd underlying climate",
"climate_entity4_id": "4th underlying climate",
"ac_mode": "AC mode",
"valve_entity_id": "1st valve number",
"valve_entity2_id": "2nd valve number",
"valve_entity3_id": "3rd valve number",
"valve_entity4_id": "4th valve number",
"sonoff_trvzb_mode": "SONOFF TRVZB mode",
"auto_regulation_mode": "Self-regulation",
"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",
"auto_fan_mode": " Auto fan mode"
"auto_fan_mode": "Auto fan mode"
},
"data_description": {
"heater_entity_id": "Mandatory heater entity id",
"heater_entity2_id": "Optional 2nd Heater entity id. Leave empty if not used",
"heater_entity3_id": "Optional 3rd Heater entity id. Leave empty if not used",
"heater_entity4_id": "Optional 4th Heater entity id. Leave empty if not used",
"underlying_entity_ids": "The device(s) to be controlled - 1 is required",
"heater_keep_alive": "Optional heater switch state refresh interval. Leave empty if not required.",
"proportional_function": "Algorithm to use (TPI is the only one for now)",
"climate_entity_id": "Underlying climate entity id",
"climate_entity2_id": "2nd underlying climate entity id",
"climate_entity3_id": "3rd underlying climate entity id",
"climate_entity4_id": "4th underlying climate entity id",
"ac_mode": "Use the Air Conditioning (AC) mode",
"valve_entity_id": "1st valve number entity id",
"valve_entity2_id": "2nd valve number entity id",
"valve_entity3_id": "3rd valve number entity id",
"valve_entity4_id": "4th valve number entity id",
"sonoff_trvzb_mode": "The underlyings are SONOFF TRVZB. You have to configure some extra entities in the specific menu option 'Sonoff trvzb configuration'",
"auto_regulation_mode": "Auto adjustment of the target temperature",
"auto_regulation_dtemp": "The threshold in ° (or % for valve) under which the temperature change will not be sent",
"auto_regulation_periode_min": "Duration in minutes between two regulation update",
"auto_regulation_use_device_temp": "Use the eventual internal temperature sensor of the underlying to speedup the self-regulation",
"inverse_switch_command": "For switch with pilot wire and diode you may need to inverse the command",
"auto_fan_mode": " Automatically activate fan when huge heating/cooling is necessary"
"auto_fan_mode": "Automatically activate fan when huge heating/cooling is necessary"
}
},
"tpi": {
@@ -100,26 +115,9 @@
},
"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": {
"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"
},
"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": {
@@ -140,8 +138,8 @@
"window_auto_open_threshold": "Recommended value: between 3 and 10. Leave empty if automatic window open detection is not used",
"window_auto_close_threshold": "Recommended value: 0. Leave empty if automatic window open detection is not used",
"window_auto_max_duration": "Recommended value: 60 (one hour). Leave empty if automatic window open detection is not used",
"use_window_central_config": "Check to use the central window configuration. Uncheck to use a specific window configuration for this VTherm",
"window_action": "Action to do if window is deteted as open"
"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": {
@@ -166,7 +164,7 @@
},
"power": {
"title": "Power management",
"description": "Power management attributes.\nGives the power and max power sensor of your home.\nThen specify the power consumption of the heater when on.\nAll sensors and device power should have the same unit (kW or W).",
"description": "Power management attributes.\nGives the power and max power sensor of your home.\nSpecify the power consumption of the heater when on.\nAll sensors and device power should use the same unit (kW or W).",
"data": {
"power_sensor_entity_id": "Power",
"max_power_sensor_entity_id": "Max power",
@@ -185,51 +183,64 @@
"description": "Presence management attributes.\nGives the a presence sensor of your home (true is someone is present) and give the corresponding temperature preset setting.",
"data": {
"presence_sensor_entity_id": "Presence sensor",
"eco_away_temp": "Eco preset",
"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"
"use_presence_central_config": "Use central presence temperature configuration. Deselect to use specific temperature entities"
},
"data_description": {
"presence_sensor_entity_id": "Presence sensor entity id",
"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"
"presence_sensor_entity_id": "Presence sensor entity id"
}
},
"advanced": {
"title": "Advanced parameters",
"description": "Configuration of advanced parameters. Leave the default values if you don't know what you are doing.\nThese parameters can lead to very poor temperature control or bad power regulation.",
"data": {
"minimal_activation_delay": "Minimal activation delay",
"minimal_activation_delay": "Minimum activation delay",
"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",
"use_advanced_central_config": "Use central advanced configuration"
},
"data_description": {
"minimal_activation_delay": "Delay in seconds under which the equipment will not be activated",
"security_delay_min": "Maximum allowed delay in minutes between two temperature measurements. Above this delay the thermostat will turn to a safety off state",
"security_min_on_percent": "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",
"use_advanced_central_config": "Check to use the central advanced configuration. Uncheck to use a specific advanced configuration for this VTherm"
}
},
"central_boiler": {
"title": "Control of the central boiler",
"description": "Enter the services to call to turn on/off the central boiler. Leave blank if no service call is to be made (in this case, you will have to manage the turning on/off of your central boiler yourself). The service called must be formatted as follows: `entity_id/service_name[/attribute:value]` (/attribute:value is optional)\nFor example:\n- to turn on a switch: `switch.controle_chaudiere/switch.turn_on`\n- to turn off a switch: `switch.controle_chaudiere/switch.turn_off`\n- to program the boiler to 25° and thus force its ignition: `climate.thermostat_chaudiere/climate.set_temperature/temperature:25`\n- to send 10° to the boiler and thus force its extinction: `climate.thermostat_chaudiere/climate.set_temperature/temperature:10`",
"data": {
"central_boiler_activation_service": "Command to turn-on",
"central_boiler_deactivation_service": "Command to turn-off"
},
"data_description": {
"central_boiler_activation_service": "Command to turn-on the central boiler formatted like entity_id/service_name[/attribut:valeur]",
"central_boiler_deactivation_service": "Command to turn-off the central boiler formatted like entity_id/service_name[/attribut:valeur]"
}
},
"sonoff_trvzb": {
"title": "Sonoff TRVZB configuration",
"description": "Specific Sonoff TRVZB configuration",
"data": {
"offset_calibration_entity_ids": "Offset calibration entities",
"opening_degree_entity_ids": "Opening degree entities",
"closing_degree_entity_ids": "Closing degree entities",
"proportional_function": "Algorithm"
},
"data_description": {
"offset_calibration_entity_ids": "The list of the 'offset calibration' entities. There should be one per underlying climate entities",
"opening_degree_entity_ids": "The list of the 'opening degree' entities. There should be one per underlying climate entities",
"closing_degree_entity_ids": "The list of the 'closing degree' entities. There should be one per underlying climate entities",
"proportional_function": "Algorithm to use (TPI is the only one for now)"
}
}
},
"error": {
"unknown": "Unexpected error",
"unknown_entity": "Unknown entity id",
"window_open_detection_method": "Only one window open detection method should be used. Use either window sensor or automatic detection through temperature threshold but not both",
"no_central_config": "You cannot check 'use central configuration' because no central configuration was found. You need to create a Versatile Thermostat of type 'Central Configuration' to use it."
"no_central_config": "You cannot select 'use central configuration' because no central configuration was found. You need to create a Versatile Thermostat of type 'Central Configuration' to use it."
},
"abort": {
"already_configured": "Device is already configured"
@@ -247,76 +258,91 @@
"thermostat_type": "Only one central configuration type is possible"
}
},
"menu": {
"title": "Menu",
"description": "Configure your thermostat. You will be able to finalize the configuration when all required parameters are entered.",
"menu_options": {
"main": "Main attributes",
"central_boiler": "Central boiler",
"type": "Underlyings",
"tpi": "TPI parameters",
"features": "Features",
"presets": "Presets",
"window": "Window detection",
"motion": "Motion detection",
"power": "Power management",
"presence": "Presence detection",
"advanced": "Advanced parameters",
"auto_start_stop": "Auto start and stop",
"sonoff_trvzb": "Sonoff TRVZB configuration",
"finalize": "All done",
"configuration_not_complete": "Configuration not complete"
}
},
"main": {
"title": "Main - {name}",
"description": "Main mandatory attributes",
"data": {
"name": "Name",
"thermostat_type": "Thermostat type",
"temperature_sensor_entity_id": "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",
"cycle_min": "Cycle duration (minutes)",
"temp_min": "Minimal temperature allowed",
"temp_max": "Maximal temperature allowed",
"temp_min": "Minimum temperature allowed",
"temp_max": "Maximum temperature allowed",
"step_temperature": "Temperature step",
"device_power": "Device power",
"use_central_mode": "Enable the control by central entity (need central config). Check to enable the control of the VTherm with the select central_mode entities.",
"use_central_mode": "Enable the control by central entity (requires central config). Check to enable the control of the VTherm with the select central_mode entities.",
"use_main_central_config": "Use additional central main configuration. Check to use the central main configuration (outdoor temperature, min, max, step, ...).",
"used_by_controls_central_boiler": "Used by central boiler. Check if this VTherm should have control on the central boiler"
},
"data_description": {
"temperature_sensor_entity_id": "Room temperature sensor entity id",
"last_seen_temperature_sensor_entity_id": "Last seen room temperature sensor entity id. Should be datetime sensor",
"external_temperature_sensor_entity_id": "Outdoor temperature sensor entity id. Not used if central configuration is selected"
}
},
"features": {
"title": "Features - {name}",
"description": "Thermostat features",
"data": {
"use_window_feature": "Use window detection",
"use_motion_feature": "Use motion detection",
"use_power_feature": "Use power management",
"use_presence_feature": "Use presence detection",
"use_main_central_config": "Use central main configuration. Check to use the central main configuration. Uncheck to use a specific main configuration for this VTherm",
"add_central_boiler_control": "Add 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 need heating, the boiler will be turned on. If no VTherm needs heating, the boiler will be turned off. Commands for turning on/off the central boiler are given in the next configuration page",
"used_by_controls_central_boiler": "Used by central boiler. Check if this VTherm should have control on the central boiler"
},
"data_description": {
"external_temperature_sensor_entity_id": "Outdoor temperature sensor entity id. Not used if central configuration is selected"
"use_central_boiler_feature": "Use a central boiler. Check to add a control to your central boiler. You will have to configure the VTherm which will have a control of the central boiler after seecting this checkbox to take effect. If one VTherm requires heating, the boiler will be turned on. If no VTherm requires heating, the boiler will be turned off. Commands for turning on/off the central boiler are given in the related configuration page",
"use_auto_start_stop_feature": "Use the auto start and stop feature"
}
},
"type": {
"title": "Entities - {name}",
"description": "Linked entities attributes",
"data": {
"heater_entity_id": "1st heater switch",
"heater_entity2_id": "2nd heater switch",
"heater_entity3_id": "3rd heater switch",
"heater_entity4_id": "4th heater switch",
"underlying_entity_ids": "The device(s) to be controlled",
"heater_keep_alive": "Switch keep-alive interval in seconds",
"proportional_function": "Algorithm",
"climate_entity_id": "1st underlying climate",
"climate_entity2_id": "2nd underlying climate",
"climate_entity3_id": "3rd underlying climate",
"climate_entity4_id": "4th underlying climate",
"ac_mode": "AC mode",
"valve_entity_id": "1st valve number",
"valve_entity2_id": "2nd valve number",
"valve_entity3_id": "3rd valve number",
"valve_entity4_id": "4th valve number",
"sonoff_trvzb_mode": "SONOFF TRVZB mode",
"auto_regulation_mode": "Self-regulation",
"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",
"auto_fan_mode": " Auto fan mode"
"auto_fan_mode": "Auto fan mode"
},
"data_description": {
"heater_entity_id": "Mandatory heater entity id",
"heater_entity2_id": "Optional 2nd Heater entity id. Leave empty if not used",
"heater_entity3_id": "Optional 3rd Heater entity id. Leave empty if not used",
"heater_entity4_id": "Optional 4th Heater entity id. Leave empty if not used",
"underlying_entity_ids": "The device(s) to be controlled - 1 is required",
"heater_keep_alive": "Optional heater switch state refresh interval. Leave empty if not required.",
"proportional_function": "Algorithm to use (TPI is the only one for now)",
"climate_entity_id": "Underlying climate entity id",
"climate_entity2_id": "2nd underlying climate entity id",
"climate_entity3_id": "3rd underlying climate entity id",
"climate_entity4_id": "4th underlying climate entity id",
"ac_mode": "Use the Air Conditioning (AC) mode",
"valve_entity_id": "1st valve number entity id",
"valve_entity2_id": "2nd valve number entity id",
"valve_entity3_id": "3rd valve number entity id",
"valve_entity4_id": "4th valve number entity id",
"sonoff_trvzb_mode": "The underlyings are SONOFF TRVZB. You have to configure some extra entities in the specific menu option 'Sonoff trvzb configuration'",
"auto_regulation_mode": "Auto adjustment of the target temperature",
"auto_regulation_dtemp": "The threshold in ° (or % for valve) under which the temperature change will not be sent",
"auto_regulation_periode_min": "Duration in minutes between two regulation update",
"auto_regulation_use_device_temp": "Use the eventual internal temperature sensor of the underlying to speedup the self-regulation",
"inverse_switch_command": "For switch with pilot wire and diode you may need to 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": {
@@ -335,26 +361,9 @@
},
"presets": {
"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": {
"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"
},
"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": {
@@ -420,44 +429,57 @@
"description": "Presence management attributes.\nGives the a presence sensor of your home (true is someone is present) and give the corresponding temperature preset setting.",
"data": {
"presence_sensor_entity_id": "Presence sensor",
"eco_away_temp": "Eco away preset",
"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"
"use_presence_central_config": "Use central presence temperature configuration. Uncheck to use specific temperature entities"
},
"data_description": {
"presence_sensor_entity_id": "Presence sensor entity id",
"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"
"presence_sensor_entity_id": "Presence sensor entity id"
}
},
"advanced": {
"title": "Advanced - {name}",
"description": "Advanced parameters. Leave the default values if you don't know what you are doing.\nThese parameters can lead to very poor temperature control or bad power regulation.",
"data": {
"minimal_activation_delay": "Minimal activation delay",
"minimal_activation_delay": "Minimum activation delay",
"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",
"use_advanced_central_config": "Use central advanced configuration"
},
"data_description": {
"minimal_activation_delay": "Delay in seconds under which the equipment will not be activated",
"security_delay_min": "Maximum allowed delay in minutes between two temperature measurements. Above this delay the thermostat will turn to a safety off state",
"security_min_on_percent": "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",
"use_advanced_central_config": "Check to use the central advanced configuration. Uncheck to use a specific advanced configuration for this VTherm"
}
},
"central_boiler": {
"title": "Control of the central boiler",
"description": "Enter the services to call to turn on/off the central boiler. Leave blank if no service call is to be made (in this case, you will have to manage the turning on/off of your central boiler yourself). The service called must be formatted as follows: `entity_id/service_name[/attribute:value]` (/attribute:value is optional)\nFor example:\n- to turn on a switch: `switch.controle_chaudiere/switch.turn_on`\n- to turn off a switch: `switch.controle_chaudiere/switch.turn_off`\n- to program the boiler to 25° and thus force its ignition: `climate.thermostat_chaudiere/climate.set_temperature/temperature:25`\n- to send 10° to the boiler and thus force its extinction: `climate.thermostat_chaudiere/climate.set_temperature/temperature:10`",
"data": {
"central_boiler_activation_service": "Command to turn-on",
"central_boiler_deactivation_service": "Command to turn-off"
},
"data_description": {
"central_boiler_activation_service": "Command to turn-on the central boiler formatted like entity_id/service_name[/attribut:valeur]",
"central_boiler_deactivation_service": "Command to turn-off the central boiler formatted like entity_id/service_name[/attribut:valeur]"
}
},
"sonoff_trvzb": {
"title": "Sonoff TRVZB configuration",
"description": "Specific Sonoff TRVZB configuration",
"data": {
"offset_calibration_entity_ids": "Offset calibration entities",
"opening_degree_entity_ids": "Opening degree entities",
"closing_degree_entity_ids": "Closing degree entities",
"proportional_function": "Algorithm"
},
"data_description": {
"offset_calibration_entity_ids": "The list of the 'offset calibration' entities. There should be one per underlying climate entities",
"opening_degree_entity_ids": "The list of the 'opening degree' entities. There should be one per underlying climate entities",
"closing_degree_entity_ids": "The list of the 'closing degree' entities. There should be one per underlying climate entities",
"proportional_function": "Algorithm to use (TPI is the only one for now)"
}
}
},
"error": {
@@ -465,7 +487,8 @@
"unknown_entity": "Unknown entity id",
"window_open_detection_method": "Only one window open detection method should be used. Use either window sensor or automatic detection through temperature threshold but not both",
"no_central_config": "You cannot check 'use central configuration' because no central configuration was found. You need to create a Versatile Thermostat of type 'Central Configuration' to use it.",
"service_configuration_format": "The format of the service configuration is wrong"
"service_configuration_format": "The format of the service configuration is wrong",
"sonoff_trvzb_nb_entities_incorrect": "The number of specific entities for Sonoff TRVZB should be equal to the number of underlyings"
},
"abort": {
"already_configured": "Device is already configured"
@@ -514,6 +537,14 @@
"comfort": "Comfort",
"boost": "Boost"
}
},
"auto_start_stop": {
"options": {
"auto_start_stop_none": "No auto start/stop",
"auto_start_stop_slow": "Slow detection",
"auto_start_stop_medium": "Medium detection",
"auto_start_stop_fast": "Fast detection"
}
}
},
"entity": {
@@ -524,11 +555,59 @@
"state": {
"power": "Shedding",
"security": "Safety",
"none": "Manual"
"none": "Manual",
"frost": "Frost"
}
}
}
}
},
"number": {
"frost_temp": {
"name": "Frost"
},
"eco_temp": {
"name": "Eco"
},
"comfort_temp": {
"name": "Comfort"
},
"boost_temp": {
"name": "Boost"
},
"frost_ac_temp": {
"name": "Frost ac"
},
"eco_ac_temp": {
"name": "Eco ac"
},
"comfort_ac_temp": {
"name": "Comfort ac"
},
"boost_ac_temp": {
"name": "Boost ac"
},
"frost_away_temp": {
"name": "Frost away"
},
"eco_away_temp": {
"name": "Eco away"
},
"comfort_away_temp": {
"name": "Comfort away"
},
"boost_away_temp": {
"name": "Boost away"
},
"eco_ac_away_temp": {
"name": "Eco ac away"
},
"comfort_ac_away_temp": {
"name": "Comfort ac away"
},
"boost_ac_away_temp": {
"name": "Boost ac away"
}
}
}
}

View File

@@ -12,74 +12,89 @@
"thermostat_type": "Un seul thermostat de type Configuration centrale est possible."
}
},
"menu": {
"title": "Menu",
"description": "Paramétrez votre thermostat. Vous pourrez finaliser la configuration quand tous les paramètres auront été saisis.",
"menu_options": {
"main": "Principaux Attributs",
"central_boiler": "Chauffage central",
"type": "Sous-jacents",
"tpi": "Paramètres TPI",
"features": "Fonctions",
"presets": "Pre-réglages",
"window": "Détection d'ouverture",
"motion": "Détection de mouvement",
"power": "Gestion de la puissance",
"presence": "Détection de présence",
"advanced": "Paramètres avancés",
"auto_start_stop": "Allumage/extinction automatique",
"sonoff_trvzb": "Configuration spécifique à Sonoff TRVZB",
"finalize": "Finaliser la création",
"configuration_not_complete": "Configuration incomplète"
}
},
"main": {
"title": "Ajout d'un nouveau thermostat",
"description": "Principaux attributs obligatoires",
"data": {
"name": "Nom",
"thermostat_type": "Type de thermostat",
"temperature_sensor_entity_id": "Température sensor entity id",
"external_temperature_sensor_entity_id": "Température exterieure sensor entity id",
"temperature_sensor_entity_id": "Capteur de température",
"last_seen_temperature_sensor_entity_id": "Dernière vue capteur de température",
"external_temperature_sensor_entity_id": "Capteur de température exterieure",
"cycle_min": "Durée du cycle (minutes)",
"temp_min": "Température minimale permise",
"temp_max": "Température maximale permise",
"step_temperature": "Pas de température",
"device_power": "Puissance de l'équipement",
"use_central_mode": "Autoriser le controle par une entity centrale ('nécessite une config. centrale`). Cochez pour autoriser le contrôle du VTherm par la liste déroulante 'central_mode' de l'entité configuration centrale.",
"use_main_central_config": "Utiliser la configuration centrale supplémentaire. Cochez pour utiliser la configuration centrale supplémentaire (température externe, min, max, pas, ...)",
"used_by_controls_central_boiler": "Utilisé par la chaudière centrale. Cochez si ce VTherm doit contrôler la chaudière centrale."
},
"data_description": {
"temperature_sensor_entity_id": "Id d'entité du capteur de température",
"last_seen_temperature_sensor_entity_id": "Id d'entité du capteur donnant la date et heure de dernière vue capteur de température. L'état doit être au format date heure (ex: 2024-03-31T17:07:03+00:00)",
"external_temperature_sensor_entity_id": "Entity id du capteur de température extérieure. Non utilisé si une configuration centrale est définie"
}
},
"features": {
"title": "Fonctions",
"description": "Fonctions du thermostat à utiliser",
"data": {
"use_window_feature": "Avec détection des ouvertures",
"use_motion_feature": "Avec détection de mouvement",
"use_power_feature": "Avec gestion de la puissance",
"use_presence_feature": "Avec détection de présence",
"use_main_central_config": "Utiliser la configuration centrale. Cochez pour utiliser la configuration centrale. Décochez et saisissez les attributs pour utiliser une configuration spécifique.",
"add_central_boiler_control": "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.",
"used_by_controls_central_boiler": "Utilisé par la chaudière centrale. Cochez si ce VTherm doit contrôler la chaudière centrale."
},
"data_description": {
"external_temperature_sensor_entity_id": "Entity id du capteur de température extérieure."
"use_central_boiler_feature": "Ajouter une chaudière centrale. Cochez pour ajouter un controle sur une chaudière centrale. Vous devrez ensuite configurer les VTherms qui commande la chaudière centrale pour que cette option prenne effet. Si au moins un des VTherm a besoin de chauffer, la chaudière centrale sera activée. Si aucun VTherm n'a besoin de chauffer, elle sera éteinte. Les commandes pour allumer/éteindre la chaudière centrale sont données dans la page de configuration suivante.",
"use_auto_start_stop_feature": "Avec démarrage et extinction automatique"
}
},
"type": {
"title": "Entité(s) liée(s)",
"description": "Attributs de(s) l'entité(s) liée(s)",
"data": {
"heater_entity_id": "1er radiateur",
"heater_entity2_id": "2ème radiateur",
"heater_entity3_id": "3ème radiateur",
"heater_entity4_id": "4ème radiateur",
"underlying_entity_ids": "Les équipements à controller",
"heater_keep_alive": "keep-alive (sec)",
"proportional_function": "Algorithme",
"climate_entity_id": "Thermostat sous-jacent",
"climate_entity2_id": "2ème thermostat sous-jacent",
"climate_entity3_id": "3ème thermostat sous-jacent",
"climate_entity4_id": "4ème thermostat sous-jacent",
"ac_mode": "AC mode ?",
"valve_entity_id": "1ère valve number",
"valve_entity2_id": "2ème valve number",
"valve_entity3_id": "3ème valve number",
"valve_entity4_id": "4ème valve number",
"sonoff_trvzb_mode": "Mode Sonoff TRVZB",
"auto_regulation_mode": "Auto-régulation",
"auto_regulation_dtemp": "Seuil de régulation",
"auto_regulation_periode_min": "Période minimale de régulation",
"auto_regulation_use_device_temp": "Compenser la température interne du sous-jacent",
"inverse_switch_command": "Inverser la commande",
"auto_fan_mode": " Auto ventilation mode"
},
"data_description": {
"heater_entity_id": "Entity id du 1er radiateur obligatoire",
"heater_entity2_id": "Optionnel entity id du 2ème radiateur",
"heater_entity3_id": "Optionnel entity id du 3ème radiateur",
"heater_entity4_id": "Optionnel entity id du 4ème radiateur",
"underlying_entity_ids": "La liste des équipements qui seront controlés par ce VTherm",
"heater_keep_alive": "Intervalle de rafraichissement du switch en secondes. Laisser vide pour désactiver. À n'utiliser que pour les switchs qui le nécessite.",
"proportional_function": "Algorithme à utiliser (Seul TPI est disponible pour l'instant)",
"climate_entity_id": "Entity id du thermostat sous-jacent",
"climate_entity2_id": "Entity id du 2ème thermostat sous-jacent",
"climate_entity3_id": "Entity id du 3ème thermostat sous-jacent",
"climate_entity4_id": "Entity id du 4ème thermostat sous-jacent",
"ac_mode": "Utilisation du mode Air Conditionné (AC)",
"valve_entity_id": "Entity id de la 1ère valve",
"valve_entity2_id": "Entity id de la 2ème valve",
"valve_entity3_id": "Entity id de la 3ème valve",
"valve_entity4_id": "Entity id de la 4ème valve",
"sonoff_trvzb_mode": "Les équipements sont des Sonoff TRVZB. Vous devez configurer les entités dédiées dans le menu 'Configuration Sonoff TRVZB'",
"auto_regulation_mode": "Ajustement automatique de la température cible",
"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_use_device_temp": "Compenser la temperature interne du sous-jacent pour accélérer l'auto-régulation",
"inverse_switch_command": "Inverse la commande du switch pour une installation avec fil pilote et diode",
"auto_fan_mode": "Active la ventilation automatiquement en cas d'écart important"
}
@@ -99,27 +114,10 @@
}
},
"presets": {
"title": "Presets",
"description": "Pour chaque preset, donnez la température cible (0 pour ignorer le preset)",
"title": "Pre-réglages",
"description": "Cochez pour que ce thermostat utilise les pré-réglages de la configuration centrale. Décochez pour utiliser des entités de température spécifiques",
"data": {
"eco_temp": "Preset Eco",
"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"
"use_presets_central_config": "Utiliser la configuration des pré-réglages centrale"
}
},
"window": {
@@ -182,28 +180,13 @@
},
"presence": {
"title": "Gestion de la présence",
"description": "Donnez un capteur de présence (true si quelqu'un est présent) et les températures cibles à utiliser en cas d'absence.",
"description": "Donnez un capteur de présence (true si quelqu'un est présent) et les températures cibles à utiliser en cas d'abs.",
"data": {
"presence_sensor_entity_id": "Capteur de présence",
"eco_away_temp": "preset Eco",
"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"
"use_presence_central_config": "Utiliser la configuration centrale des températures en cas d'absence. Décochez pour avoir des entités de température dédiées"
},
"data_description": {
"presence_sensor_entity_id": "Id d'entité du capteur de présence",
"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"
"presence_sensor_entity_id": "Id d'entité du capteur de présence"
}
},
"advanced": {
@@ -235,6 +218,22 @@
"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]"
}
},
"sonoff_trvzb": {
"title": "Configuration Sonoff TRVZB",
"description": "Configuration spécifique des Sonoff TRVZB",
"data": {
"offset_calibration_entity_ids": "Entités de 'Offset calibration'",
"opening_degree_entity_ids": "Entités de 'Opening degree'",
"closing_degree_entity_ids": "Entités de 'Closing degree'",
"proportional_function": "Algorithme"
},
"data_description": {
"offset_calibration_entity_ids": "La liste des entités 'offset calibration' entities. Il doit y en avoir une par entité climate sous-jacente",
"opening_degree_entity_ids": "La liste des entités 'opening degree' entities. Il doit y en avoir une par entité climate sous-jacente",
"closing_degree_entity_ids": "La liste des entités 'closing degree' entities. Il doit y en avoir une par entité climate sous-jacente",
"proportional_function": "Algorithme à utiliser (seulement TPI est disponible)"
}
}
},
"error": {
@@ -259,74 +258,89 @@
"thermostat_type": "Un seul thermostat de type Configuration centrale est possible."
}
},
"menu": {
"title": "Menu - {name}",
"description": "Paramétrez votre thermostat. Vous pourrez finaliser la configuration quand tous les paramètres auront été saisis.",
"menu_options": {
"main": "Principaux Attributs",
"central_boiler": "Chauffage central",
"type": "Sous-jacents",
"tpi": "Paramètres TPI",
"features": "Fonctions",
"presets": "Pre-réglages",
"window": "Détection d'ouvertures",
"motion": "Détection de mouvement",
"power": "Gestion de la puissance",
"presence": "Détection de présence",
"advanced": "Paramètres avancés",
"auto_start_stop": "Allumage/extinction automatique",
"sonoff_trvzb": "Configuration spécifique à Sonoff TRVZB",
"finalize": "Finaliser les modifications",
"configuration_not_complete": "Configuration incomplète"
}
},
"main": {
"title": "Attributs - {name}",
"description": "Principaux attributs obligatoires",
"data": {
"name": "Nom",
"thermostat_type": "Type de thermostat",
"temperature_sensor_entity_id": "Température sensor entity id",
"external_temperature_sensor_entity_id": "Température exterieure sensor entity id",
"temperature_sensor_entity_id": "Capteur de température",
"last_seen_temperature_sensor_entity_id": "Dernière vue capteur de température",
"external_temperature_sensor_entity_id": "Capteur de température exterieure",
"cycle_min": "Durée du cycle (minutes)",
"temp_min": "Température minimale permise",
"temp_max": "Température maximale permise",
"step_temperature": "Pas de température",
"device_power": "Puissance de l'équipement",
"use_central_mode": "Autoriser le controle par une entity centrale ('nécessite une config. centrale`). Cochez pour autoriser le contrôle du VTherm par la liste déroulante 'central_mode' de l'entité configuration centrale.",
"use_main_central_config": "Utiliser la configuration centrale supplémentaire. Cochez pour utiliser la configuration centrale supplémentaire (température externe, min, max, pas, ...)",
"used_by_controls_central_boiler": "Utilisé par la chaudière centrale. Cochez si ce VTherm doit contrôler la chaudière centrale."
},
"data_description": {
"temperature_sensor_entity_id": "Id d'entité du capteur de température",
"last_seen_temperature_sensor_entity_id": "Id d'entité du capteur donnant la date et heure de dernière vue capteur de température. L'état doit être au format date heure (ex: 2024-03-31T17:07:03+00:00)",
"external_temperature_sensor_entity_id": "Entity id du capteur de température extérieure. Non utilisé si une configuration centrale est définie"
}
},
"features": {
"title": "Fonctions - {name}",
"description": "Fonctions du thermostat à utiliser",
"data": {
"use_window_feature": "Avec détection des ouvertures",
"use_motion_feature": "Avec détection de mouvement",
"use_power_feature": "Avec gestion de la puissance",
"use_presence_feature": "Avec détection de présence",
"use_main_central_config": "Utiliser la configuration centrale. Cochez pour utiliser la configuration centrale. Décochez et saisissez les attributs pour utiliser une configuration spécifique.",
"add_central_boiler_control": "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.",
"used_by_controls_central_boiler": "Utilisé par la chaudière centrale. Cochez si ce VTherm doit contrôler la chaudière centrale."
},
"data_description": {
"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_central_boiler_feature": "Ajouter une chaudière centrale. Cochez pour ajouter un controle sur une chaudière centrale. Vous devrez ensuite configurer les VTherms qui commande la chaudière centrale pour que cette option prenne effet. Si au moins un des VTherm a besoin de chauffer, la chaudière centrale sera activée. Si aucun VTherm n'a besoin de chauffer, elle sera éteinte. Les commandes pour allumer/éteindre la chaudière centrale sont données dans la page de configuration suivante.",
"use_auto_start_stop_feature": "Avec démarrage et extinction automatique"
}
},
"type": {
"title": "Entités - {name}",
"title": "Entité(s) liée(s) - {name}",
"description": "Attributs de(s) l'entité(s) liée(s)",
"data": {
"heater_entity_id": "1er radiateur",
"heater_entity2_id": "2ème radiateur",
"heater_entity3_id": "3ème radiateur",
"heater_entity4_id": "4ème radiateur",
"underlying_entity_ids": "Les équipements à controller",
"heater_keep_alive": "keep-alive (sec)",
"proportional_function": "Algorithme",
"climate_entity_id": "Thermostat sous-jacent",
"climate_entity2_id": "2ème thermostat sous-jacent",
"climate_entity3_id": "3ème thermostat sous-jacent",
"climate_entity4_id": "4ème thermostat sous-jacent",
"ac_mode": "AC mode ?",
"valve_entity_id": "1ère valve",
"valve_entity2_id": "2ème valve",
"valve_entity3_id": "3ème valve",
"valve_entity4_id": "4ème valve",
"auto_regulation_mode": "Auto-regulation",
"sonoff_trvzb_mode": "Mode Sonoff TRVZB",
"auto_regulation_mode": "Auto-régulation",
"auto_regulation_dtemp": "Seuil de régulation",
"auto_regulation_periode_min": "Période minimale de régulation",
"auto_regulation_use_device_temp": "Compenser la température interne du sous-jacent",
"inverse_switch_command": "Inverser la commande",
"auto_fan_mode": " Auto fan mode"
"auto_fan_mode": " Auto ventilation mode"
},
"data_description": {
"heater_entity_id": "Entity id du 1er radiateur obligatoire",
"heater_entity2_id": "Optionnel entity id du 2ème radiateur",
"heater_entity3_id": "Optionnel entity id du 3ème radiateur",
"heater_entity4_id": "Optionnel entity id du 4ème radiateur",
"underlying_entity_ids": "La liste des équipements qui seront controlés par ce VTherm",
"heater_keep_alive": "Intervalle de rafraichissement du switch en secondes. Laisser vide pour désactiver. À n'utiliser que pour les switchs qui le nécessite.",
"proportional_function": "Algorithme à utiliser (Seul TPI est disponible pour l'instant)",
"climate_entity_id": "Entity id du thermostat sous-jacent",
"climate_entity2_id": "Entity id du 2ème thermostat sous-jacent",
"climate_entity3_id": "Entity id du 3ème thermostat sous-jacent",
"climate_entity4_id": "Entity id du 4ème thermostat sous-jacent",
"ac_mode": "Utilisation du mode Air Conditionné (AC)",
"valve_entity_id": "Entity id de la 1ère valve",
"valve_entity2_id": "Entity id de la 2ème valve",
"valve_entity3_id": "Entity id de la 3ème valve",
"valve_entity4_id": "Entity id de la 4ème valve",
"auto_regulation_mode": "Ajustement automatique de la consigne",
"sonoff_trvzb_mode": "Les équipements sont des Sonoff TRVZB. Vous devez configurer les entités dédiées dans le menu 'Configuration Sonoff TRVZB'",
"auto_regulation_mode": "Ajustement automatique de la température cible",
"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_use_device_temp": "Compenser la temperature interne du sous-jacent pour accélérer l'auto-régulation",
"inverse_switch_command": "Inverse la commande du switch pour une installation avec fil pilote et diode",
"auto_fan_mode": "Active la ventilation automatiquement en cas d'écart important"
}
@@ -341,26 +355,9 @@
},
"presets": {
"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": {
"eco_temp": "Preset Eco",
"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"
"use_presets_central_config": "Utiliser la configuration des pré-réglages centrale"
}
},
"window": {
@@ -423,28 +420,13 @@
},
"presence": {
"title": "Présence - {name}",
"description": "Donnez un capteur de présence (true si quelqu'un est présent) et les températures cibles à utiliser en cas d'absence.",
"description": "Donnez un capteur de présence (true si quelqu'un est présent) et les températures cibles à utiliser en cas d'abs.",
"data": {
"presence_sensor_entity_id": "Capteur de présence",
"eco_away_temp": "preset Eco",
"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"
"use_presence_central_config": "Utiliser la configuration centrale des températures en cas d'absence. Décochez pour avoir des entités de température dédiées"
},
"data_description": {
"presence_sensor_entity_id": "Id d'entité du capteur de présence",
"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"
"presence_sensor_entity_id": "Id d'entité du capteur de présence"
}
},
"advanced": {
@@ -476,6 +458,22 @@
"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]"
}
},
"sonoff_trvzb": {
"title": "Configuration Sonoff TRVZB - {name}",
"description": "Configuration spécifique des Sonoff TRVZB",
"data": {
"offset_calibration_entity_ids": "Entités de 'Offset calibration'",
"opening_degree_entity_ids": "Entités de 'Opening degree'",
"closing_degree_entity_ids": "Entités de 'Closing degree'",
"proportional_function": "Algorithme"
},
"data_description": {
"offset_calibration_entity_ids": "La liste des entités 'offset calibration' entities. Il doit y en avoir une par entité climate sous-jacente",
"opening_degree_entity_ids": "La liste des entités 'opening degree' entities. Il doit y en avoir une par entité climate sous-jacente",
"closing_degree_entity_ids": "La liste des entités 'closing degree' entities. Il doit y en avoir une par entité climate sous-jacente",
"proportional_function": "Algorithme à utiliser (seulement TPI est disponible)"
}
}
},
"error": {
@@ -483,7 +481,8 @@
"unknown_entity": "entity id inconnu",
"window_open_detection_method": "Une seule méthode de détection des ouvertures ouvertes doit être utilisée. Utilisez le détecteur d'ouverture ou les seuils de température mais pas les deux.",
"no_central_config": "Vous ne pouvez pas cocher 'Utiliser la configuration centrale' car aucune configuration centrale n'a été trouvée. Vous devez créer un Versatile Thermostat de type 'Central Configuration' pour pouvoir l'utiliser.",
"service_configuration_format": "Mauvais format de la configuration du service"
"service_configuration_format": "Mauvais format de la configuration du service",
"sonoff_trvzb_nb_entities_incorrect": "Le nombre d'entités spécifiques au Sonoff TRVZB doit être égal au nombre d'entité sous-jacentes"
},
"abort": {
"already_configured": "Le device est déjà configuré"
@@ -532,6 +531,14 @@
"comfort": "Confort",
"boost": "Renforcé (boost)"
}
},
"auto_start_stop": {
"options": {
"auto_start_stop_none": "No auto start/stop",
"auto_start_stop_slow": "Slow detection",
"auto_start_stop_medium": "Medium detection",
"auto_start_stop_fast": "Fast detection"
}
}
},
"entity": {
@@ -542,11 +549,59 @@
"state": {
"power": "Délestage",
"security": "Sécurité",
"none": "Manuel"
"none": "Manuel",
"frost": "Hors Gel"
}
}
}
}
},
"number": {
"frost_temp": {
"name": "Hors gel "
},
"eco_temp": {
"name": "Eco"
},
"comfort_temp": {
"name": "Confort"
},
"boost_temp": {
"name": "Boost"
},
"frost_ac_temp": {
"name": "Hors gel clim"
},
"eco_ac_temp": {
"name": "Eco clim"
},
"comfort_ac_temp": {
"name": "Confort clim"
},
"boost_ac_temp": {
"name": "Boost clim"
},
"frost_away_temp": {
"name": "Hors gel abs"
},
"eco_away_temp": {
"name": "Eco abs"
},
"comfort_away_temp": {
"name": "Confort abs"
},
"boost_away_temp": {
"name": "Boost abs"
},
"eco_ac_away_temp": {
"name": "Eco clim abs"
},
"comfort_ac_away_temp": {
"name": "Confort clim abs"
},
"boost_ac_away_temp": {
"name": "Boost clim abs"
}
}
}
}

View File

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

View File

@@ -12,6 +12,25 @@
"thermostat_type": "Len jeden centrálny typ konfigurácie je možný"
}
},
"menu": {
"title": "Menu",
"description": "Nakonfigurujte si termostat. Po zadaní všetkých požadovaných parametrov budete môcť dokončiť konfiguráciu.",
"menu_options": {
"main": "Hlavné atribúty",
"central_boiler": "Centrálny kotol",
"type": "Podklady",
"tpi": "TPI parametre",
"features": "Vlastnosti",
"presets": "Predvoľby",
"window": "Detekcia okien",
"motion": "Detekcia pohybu",
"power": "Správa napájania",
"presence": "Detekcia prítomnosti",
"advanced": "Pokročilé parametre",
"finalize": "Všetko hotové",
"configuration_not_complete": "Konfigurácia nie je dokončená"
}
},
"main": {
"title": "Pridajte nový všestranný termostat",
"description": "Hlavné povinné atribúty",
@@ -19,22 +38,32 @@
"name": "Názov",
"thermostat_type": "Termostat typ",
"temperature_sensor_entity_id": "ID entity snímača teploty",
"last_seen_temperature_sensor_entity_id": "Dátum posledného zobrazenia izbovej teploty",
"external_temperature_sensor_entity_id": "ID entity externého snímača teploty",
"cycle_min": "Trvanie cyklu (minúty)",
"temp_min": "Minimálna povolená teplota",
"temp_max": "Maximálna povolená teplota",
"step_temperature": "Krok teploty",
"device_power": "Napájanie zariadenia",
"use_central_mode": "Povoliť ovládanie centrálnou entitou (potrebná centrálna konfigurácia)",
"use_main_central_config": "Použite dodatočnú centrálnu hlavnú konfiguráciu. Začiarknite, ak chcete použiť centrálnu hlavnú konfiguráciu (vonkajšia teplota, min, max, krok, ...).",
"used_by_controls_central_boiler": "Používa sa centrálnym kotlom. Skontrolujte, či má mať tento VTherm ovládanie na centrálnom kotli"
},
"data_description": {
"temperature_sensor_entity_id": "ID entity snímača izbovej teploty",
"last_seen_temperature_sensor_entity_id": "Naposledy videný snímač izbovej teploty ID entity. Mal by to byť snímač dátumu a času",
"external_temperature_sensor_entity_id": "ID entity snímača vonkajšej teploty. Nepoužíva sa, ak je zvolená centrálna konfigurácia"
}
},
"features": {
"title": "Vlastnosti",
"description": "Vlastnosti termostatu",
"data": {
"use_window_feature": "Použite detekciu okien",
"use_motion_feature": "Použite detekciu pohybu",
"use_power_feature": "Použite správu napájania",
"use_presence_feature": "Použite detekciu prítomnosti",
"use_main_central_config": "Použite centrálnu hlavnú konfiguráciu"
},
"data_description": {
"use_central_mode": "Zaškrtnutím povolíte ovládanie VTherm pomocou vybraných entít central_mode",
"use_main_central_config": "Začiarknite, ak chcete použiť centrálnu hlavnú konfiguráciu. Zrušte začiarknutie, ak chcete použiť špecifickú hlavnú konfiguráciu pre tento VTherm",
"external_temperature_sensor_entity_id": "ID entity snímača vonkajšej teploty. Nepoužíva sa, ak je zvolená centrálna konfigurácia"
"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": {
@@ -45,6 +74,7 @@
"heater_entity2_id": "2. spínač ohrievača",
"heater_entity3_id": "3. spínač ohrievača",
"heater_entity4_id": "4. spínač ohrievača",
"heater_keep_alive": "Prepnite interval udržiavania v sekundách",
"proportional_function": "Algoritmus",
"climate_entity_id": "1. základná klíma",
"climate_entity2_id": "2. základná klíma",
@@ -58,6 +88,7 @@
"auto_regulation_mode": "Samoregulácia",
"auto_regulation_dtemp": "Regulačný prah",
"auto_regulation_periode_min": "Regulačné minimálne obdobie",
"auto_regulation_use_device_temp": "Použite vnútornú teplotu podkladu",
"inverse_switch_command": "Inverzný prepínací príkaz",
"auto_fan_mode": "Režim automatického ventilátora"
},
@@ -66,6 +97,7 @@
"heater_entity2_id": "Voliteľné ID entity 2. ohrievača. Ak sa nepoužíva, nechajte prázdne",
"heater_entity3_id": "Voliteľné ID entity 3. ohrievača. Ak sa nepoužíva, nechajte prázdne",
"heater_entity4_id": "Voliteľné ID entity 4. ohrievača. Ak sa nepoužíva, nechajte prázdne",
"heater_keep_alive": "Voliteľný interval obnovy stavu spínača ohrievača. Ak to nie je potrebné, nechajte prázdne.",
"proportional_function": "Algoritmus, ktorý sa má použiť (TPI je zatiaľ jediný)",
"climate_entity_id": "ID základnej klimatickej entity",
"climate_entity2_id": "2. základné identifikačné číslo klimatickej entity",
@@ -79,6 +111,7 @@
"auto_regulation_mode": "Automatické nastavenie cieľovej teploty",
"auto_regulation_dtemp": "Hranica v °, pod ktorou sa zmena teploty neodošle",
"auto_regulation_periode_min": "Trvanie v minútach medzi dvoma aktualizáciami predpisov",
"auto_regulation_use_device_temp": "Na urýchlenie samoregulácie použite prípadný vnútorný snímač teploty podkladu",
"inverse_switch_command": "V prípade spínača s pilotným vodičom a diódou možno budete musieť príkaz invertovať",
"auto_fan_mode": "Automaticky aktivujte ventilátor, keď je potrebné veľké vykurovanie/chladenie"
}
@@ -101,24 +134,7 @@
"title": "Predvoľby",
"description": "Pre každú predvoľbu zadajte cieľovú teplotu (0, ak chcete predvoľbu ignorovať)",
"data": {
"eco_temp": "Teplota v predvoľbe Eco",
"comfort_temp": "Prednastavená teplota v komfortnom režime",
"boost_temp": "Teplota v prednastavení Boost",
"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",
"use_presets_central_config": "Použite konfiguráciu centrálnych predvolieb"
},
"data_description": {
"eco_temp": "Teplota v predvoľbe Eco",
"comfort_temp": "Prednastavená teplota v komfortnom režime",
"boost_temp": "Teplota v prednastavení Boost",
"frost_temp": "Teplota v prednastavenej ochrane proti mrazu",
"eco_ac_temp": "Teplota v režime Eco prednastavená pre režim AC",
"comfort_ac_temp": "Teplota v režime Comfort je prednastavená pre režim AC",
"boost_ac_temp": "Prednastavená teplota v režime Boost pre režim AC",
"use_presets_central_config": "Začiarknite, ak chcete použiť konfiguráciu centrálnych predvolieb. Zrušte začiarknutie, ak chcete použiť špecifickú konfiguráciu predvolieb pre tento VTherm"
}
},
"window": {
@@ -130,7 +146,8 @@
"window_auto_open_threshold": "Prah poklesu teploty pre automatickú detekciu otvoreného okna (v °/hodina)",
"window_auto_close_threshold": "Prahová hodnota zvýšenia teploty pre koniec automatickej detekcie (v °/hodina)",
"window_auto_max_duration": "Maximálne trvanie automatickej detekcie otvoreného okna (v min)",
"use_window_central_config": "Použite centrálnu konfiguráciu okna"
"use_window_central_config": "Použite centrálnu konfiguráciu okna",
"window_action": "Akcia"
},
"data_description": {
"window_sensor_entity_id": "Nechajte prázdne, ak nemáte použiť žiadny okenný senzor",
@@ -138,7 +155,8 @@
"window_auto_open_threshold": "Odporúčaná hodnota: medzi 3 a 10. Ak sa nepoužíva automatická detekcia otvoreného okna, nechajte prázdne",
"window_auto_close_threshold": "Odporúčaná hodnota: 0. Ak sa nepoužíva automatická detekcia otvoreného okna, nechajte prázdne",
"window_auto_max_duration": "Odporúčaná hodnota: 60 (jedna hodina). Ak sa nepoužíva automatická detekcia otvoreného okna, nechajte prázdne",
"use_window_central_config": "Začiarknite, ak chcete použiť konfiguráciu centrálneho okna. Zrušte začiarknutie, ak chcete použiť špecifickú konfiguráciu okna pre tento VTherm"
"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": {
@@ -181,26 +199,11 @@
"title": "Riadenie prítomnosti",
"description": "Atribúty správy prítomnosti.\nPoskytuje senzor prítomnosti vášho domova (pravda, ak je niekto prítomný).\nPotom zadajte buď predvoľbu, ktorá sa má použiť, keď je senzor prítomnosti nepravdivý, alebo posun teploty, ktorý sa má použiť.\nAk je zadaná predvoľba, posun sa nepoužije.\nAk sa nepoužije, ponechajte zodpovedajúce entity_id prázdne.",
"data": {
"presence_sensor_entity_id": "ID entity senzora prítomnosti",
"eco_away_temp": "Teplota v prednastavenej Eco, keď nie je žiadna prítomnosť",
"comfort_away_temp": "Teplota v režime Comfort je prednastavená, keď nie je prítomný",
"boost_away_temp": "Prednastavená teplota v režime Boost, keď nie je prítomný",
"frost_away_temp": "Prednastavená teplota v režime Frost protection, keď nie je prítomný",
"eco_ac_away_temp": "Teplota v prednastavenej Eco, keď nie je prítomná v režime AC",
"comfort_ac_away_temp": "Teplota v režime Comfort je prednastavená, keď nie je prítomný v režime AC",
"boost_ac_away_temp": "Teplota v prednastavenom Boost, keď nie je prítomný v režime AC",
"use_presence_central_config": "Použite centrálnu konfiguráciu prítomnosti"
"presence_sensor_entity_id": "Senzora prítomnosti",
"use_presence_central_config": "Použite konfiguráciu centrálnej prítomnosti teploty. Ak chcete použiť špecifické teplotné entity, zrušte výber"
},
"data_description": {
"presence_sensor_entity_id": "ID entity senzora prítomnosti",
"eco_away_temp": "Teplota v prednastavenej Eco, keď nie je žiadna prítomnosť",
"comfort_away_temp": "Teplota v režime Comfort je prednastavená, keď nie je prítomný",
"boost_away_temp": "Prednastavená teplota v režime Boost, keď nie je prítomný",
"frost_away_temp": "Teplota v Prednastavená ochrana pred mrazom, keď nie je prítomný",
"eco_ac_away_temp": "Teplota v prednastavenej Eco, keď nie je prítomná v režime AC",
"comfort_ac_away_temp": "Teplota v režime Comfort je prednastavená, keď nie je prítomný v režime AC",
"boost_ac_away_temp": "Teplota v prednastavenom Boost, keď nie je prítomný v režime AC",
"use_presence_central_config": "Začiarknite, ak chcete použiť konfiguráciu centrálnej prítomnosti. Zrušte začiarknutie, ak chcete použiť špecifickú konfiguráciu prítomnosti pre tento VTherm"
"presence_sensor_entity_id": "ID entity senzora prítomnosti"
}
},
"advanced": {
@@ -244,6 +247,25 @@
"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",
@@ -251,22 +273,32 @@
"name": "Názov",
"thermostat_type": "Termostat typ",
"temperature_sensor_entity_id": "ID entity snímača teploty",
"last_seen_temperature_sensor_entity_id": "Dátum posledného zobrazenia izbovej teploty",
"external_temperature_sensor_entity_id": "ID entity externého snímača teploty",
"cycle_min": "Trvanie cyklu (minúty)",
"temp_min": "Minimálna povolená teplota",
"temp_max": "Maximálna povolená teplota",
"step_temperature": "Krok teploty",
"device_power": "Výkon zariadenia (kW)",
"use_central_mode": "Povoliť ovládanie centrálnou entitou (potrebná centrálna konfigurácia)",
"use_central_mode": "Povoliť ovládanie centrálnou entitou (vyžaduje centrálnu konfiguráciu). Zaškrtnutím povolíte ovládanie VTherm pomocou vybraných entít central_mode.",
"use_main_central_config": "Použite dodatočnú centrálnu hlavnú konfiguráciu. Začiarknite, ak chcete použiť centrálnu hlavnú konfiguráciu (vonkajšia teplota, min, max, krok, ...).",
"used_by_controls_central_boiler": "Používa sa centrálnym kotlom. Skontrolujte, či má mať tento VTherm ovládanie na centrálnom kotli"
},
"data_description": {
"temperature_sensor_entity_id": "ID entity snímača izbovej teploty",
"last_seen_temperature_sensor_entity_id": "Naposledy videný snímač izbovej teploty ID entity. Mal by to byť snímač dátumu a času",
"external_temperature_sensor_entity_id": "ID entity snímača vonkajšej teploty. Nepoužíva sa, ak je zvolená centrálna konfigurácia"
}
},
"features": {
"title": "Vlastnosti - {name}",
"description": "Vlastnosti termostatu",
"data": {
"use_window_feature": "Použite detekciu okien",
"use_motion_feature": "Použite detekciu pohybu",
"use_power_feature": "Použite správu napájania",
"use_presence_feature": "Použite detekciu prítomnosti",
"use_main_central_config": "Použite centrálnu hlavnú konfiguráciu"
},
"data_description": {
"use_central_mode": "Zaškrtnutím povolíte ovládanie VTherm pomocou vybraných entít central_mode",
"use_main_central_config": "Začiarknite, ak chcete použiť centrálnu hlavnú konfiguráciu. Ak chcete použiť špecifickú konfiguráciu pre tento VTherm, zrušte začiarknutie",
"external_temperature_sensor_entity_id": "ID entity snímača vonkajšej teploty. Nepoužíva sa, ak je zvolená centrálna konfigurácia"
"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": {
@@ -277,6 +309,7 @@
"heater_entity2_id": "2. spínač ohrievača",
"heater_entity3_id": "3. spínač ohrievača",
"heater_entity4_id": "4. spínač ohrievača",
"heater_keep_alive": "Prepnite interval udržiavania v sekundách",
"proportional_function": "Algoritmus",
"climate_entity_id": "Základná klíma",
"climate_entity2_id": "2. základná klíma",
@@ -290,6 +323,7 @@
"auto_regulation_mode": "Samoregulácia",
"auto_regulation_dtemp": "Regulačný prah",
"auto_regulation_periode_min": "Regulačné minimálne obdobie",
"auto_regulation_use_device_temp": "Použite vnútornú teplotu podkladu",
"inverse_switch_command": "Inverzný prepínací príkaz",
"auto_fan_mode": "Režim automatického ventilátora"
},
@@ -298,6 +332,7 @@
"heater_entity2_id": "Voliteľné ID entity 2. ohrievača. Ak sa nepoužíva, nechajte prázdne",
"heater_entity3_id": "Voliteľné ID entity 3. ohrievača. Ak sa nepoužíva, nechajte prázdne",
"heater_entity4_id": "Voliteľné ID entity 4. ohrievača. Ak sa nepoužíva, nechajte prázdne",
"heater_keep_alive": "Voliteľný interval obnovy stavu spínača ohrievača. Ak to nie je potrebné, nechajte prázdne.",
"proportional_function": "Algoritmus, ktorý sa má použiť (TPI je zatiaľ jediný)",
"climate_entity_id": "ID základnej klimatickej entity",
"climate_entity2_id": "2. základný identifikátor klimatickej entity",
@@ -311,6 +346,7 @@
"auto_regulation_mode": "Automatické nastavenie cieľovej teploty",
"auto_regulation_dtemp": "Hranica v °, pod ktorou sa zmena teploty neodošle",
"auto_regulation_periode_min": "Trvanie v minútach medzi dvoma aktualizáciami predpisov",
"auto_regulation_use_device_temp": "Na urýchlenie samoregulácie použite prípadný vnútorný snímač teploty podkladu",
"inverse_switch_command": "V prípade spínača s pilotným vodičom a diódou možno budete musieť príkaz invertovať",
"auto_fan_mode": "Automaticky aktivujte ventilátor, keď je potrebné veľké vykurovanie/chladenie"
}
@@ -333,24 +369,7 @@
"title": "Predvoľby - {name}",
"description": "Pre každú predvoľbu zadajte cieľovú teplotu (0, ak chcete predvoľbu ignorovať)",
"data": {
"eco_temp": "Teplota v predvoľbe Eco",
"comfort_temp": "Prednastavená teplota v komfortnom režime",
"boost_temp": "Teplota v prednastavení Boost",
"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",
"use_presets_central_config": "Použite konfiguráciu centrálnych predvolieb"
},
"data_description": {
"eco_temp": "Teplota v predvoľbe Eco",
"comfort_temp": "Prednastavená teplota v komfortnom režime",
"boost_temp": "Teplota v prednastavení Boost",
"frost_temp": "Teplota v prednastavenej ochrane proti mrazu",
"eco_ac_temp": "Teplota v režime Eco prednastavená pre režim AC",
"comfort_ac_temp": "Teplota v režime Comfort je prednastavená pre režim AC",
"boost_ac_temp": "Prednastavená teplota v režime Boost pre režim AC",
"use_presets_central_config": "Začiarknite, ak chcete použiť konfiguráciu centrálnych predvolieb. Zrušte začiarknutie, ak chcete použiť špecifickú konfiguráciu predvolieb pre tento VTherm"
}
},
"window": {
@@ -362,7 +381,8 @@
"window_auto_open_threshold": "Prah poklesu teploty pre automatickú detekciu otvoreného okna (v °/hodina)",
"window_auto_close_threshold": "Prahová hodnota zvýšenia teploty pre koniec automatickej detekcie (v °/hodina)",
"window_auto_max_duration": "Maximálne trvanie automatickej detekcie otvoreného okna (v min)",
"use_window_central_config": "Použite centrálnu konfiguráciu okna"
"use_window_central_config": "Použite centrálnu konfiguráciu okna",
"window_action": "Akcia"
},
"data_description": {
"window_sensor_entity_id": "Nechajte prázdne, ak nemáte použiť žiadny okenný senzor",
@@ -370,7 +390,8 @@
"window_auto_open_threshold": "Odporúčaná hodnota: medzi 3 a 10. Ak sa nepoužíva automatická detekcia otvoreného okna, nechajte prázdne",
"window_auto_close_threshold": "Odporúčaná hodnota: 0. Ak sa nepoužíva automatická detekcia otvoreného okna, nechajte prázdne",
"window_auto_max_duration": "Odporúčaná hodnota: 60 (jedna hodina). Ak sa nepoužíva automatická detekcia otvoreného okna, nechajte prázdne",
"use_window_central_config": "Začiarknite, ak chcete použiť konfiguráciu centrálneho okna. Zrušte začiarknutie, ak chcete použiť špecifickú konfiguráciu okna pre tento VTherm"
"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": {
@@ -410,29 +431,14 @@
}
},
"presence": {
"title": "Riadenie prítomnosti",
"description": "Atribúty správy prítomnosti.\nPoskytuje senzor prítomnosti vášho domova (pravda, ak je niekto prítomný).\nPotom zadajte buď predvoľbu, ktorá sa má použiť, keď je senzor prítomnosti nepravdivý, alebo posun teploty, ktorý sa má použiť.\nAk je zadaná predvoľba, posun sa nepoužije.\nAk sa nepoužije, ponechajte zodpovedajúce entity_id prázdne.",
"title": "Prítommnosť - {name}",
"description": "Atribúty riadenia prítomnosti.\nPoskytuje senzor prítomnosti vášho domova (pravda, je niekto prítomný) a poskytuje zodpovedajúce prednastavené nastavenie teploty.",
"data": {
"presence_sensor_entity_id": "ID entity senzora prítomnosti (pravda je prítomná)",
"eco_away_temp": "Teplota v prednastavenej Eco, keď nie je žiadna prítomnosť",
"comfort_away_temp": "Teplota v režime Comfort je prednastavená, keď nie je prítomný",
"boost_away_temp": "Prednastavená teplota v režime Boost, keď nie je prítomný",
"frost_away_temp": "Prednastavená teplota v režime Frost protection, keď nie je prítomný",
"eco_ac_away_temp": "Teplota v prednastavenej Eco, keď nie je prítomná v režime AC",
"comfort_ac_away_temp": "Teplota v režime Comfort je prednastavená, keď nie je prítomný v režime AC",
"boost_ac_away_temp": "Teplota v prednastavenom Boost, keď nie je prítomný v režime AC",
"use_presence_central_config": "Použite centrálnu konfiguráciu prítomnosti"
"presence_sensor_entity_id": "Senzor prítomnosti",
"use_presence_central_config": "Použite konfiguráciu centrálnej prítomnosti teploty. Ak chcete použiť špecifické entity teploty, zrušte začiarknutie"
},
"data_description": {
"presence_sensor_entity_id": "ID entity senzora prítomnosti",
"eco_away_temp": "Teplota v prednastavenej Eco, keď nie je žiadna prítomnosť",
"comfort_away_temp": "Teplota v režime Comfort je prednastavená, keď nie je prítomný",
"boost_away_temp": "Prednastavená teplota v režime Boost, keď nie je prítomný",
"frost_away_temp": "Teplota v Prednastavená ochrana pred mrazom, keď nie je prítomný",
"eco_ac_away_temp": "Teplota v prednastavenej Eco, keď nie je prítomná v režime AC",
"comfort_ac_away_temp": "Teplota v režime Comfort je prednastavená, keď nie je prítomný v režime AC",
"boost_ac_away_temp": "Teplota v prednastavenom Boost, keď nie je prítomný v režime AC",
"use_presence_central_config": "Začiarknite, ak chcete použiť konfiguráciu centrálnej prítomnosti. Zrušte začiarknutie, ak chcete použiť špecifickú konfiguráciu prítomnosti pre tento VTherm"
"presence_sensor_entity_id": "ID entity senzora prítomnosti"
}
},
"advanced": {
@@ -458,7 +464,8 @@
"unknown": "Neočakávaná chyba",
"unknown_entity": "Neznáme ID entity",
"window_open_detection_method": "Mala by sa použiť iba jedna metóda detekcie otvoreného okna. Použite senzor alebo automatickú detekciu cez teplotný prah, ale nie oboje",
"no_central_config": "Nemôžete zaškrtnúť „použiť centrálnu konfiguráciu“, pretože sa nenašla žiadna centrálna konfigurácia. Aby ste ho mohli používať, musíte si vytvoriť všestranný termostat typu „Central Configuration“."
"no_central_config": "Nemôžete zaškrtnúť „použiť centrálnu konfiguráciu“, pretože sa nenašla žiadna centrálna konfigurácia. Aby ste ho mohli používať, musíte si vytvoriť všestranný termostat typu „Central Configuration“.",
"service_configuration_format": "Formát konfigurácie služby je nesprávny"
},
"abort": {
"already_configured": "Zariadenie je už nakonfigurované"
@@ -491,6 +498,22 @@
"auto_fan_high": "Vysoký",
"auto_fan_turbo": "Turbo"
}
},
"window_action": {
"options": {
"window_turn_off": "Vypnúť",
"window_fan_only": "Len ventilátor",
"window_frost_temp": "Ochrana pred mrazom",
"window_eco_temp": "Eco"
}
},
"presets": {
"options": {
"frost": "Ochrana proti mrazu",
"eco": "Eco",
"comfort": "Komfort",
"boost": "Boost"
}
}
},
"entity": {
@@ -506,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

@@ -1,11 +1,11 @@
# pylint: disable=unused-argument, line-too-long
# pylint: disable=unused-argument, line-too-long, too-many-lines
""" Underlying entities classes """
import logging
from typing import Any
from enum import StrEnum
from homeassistant.const import ATTR_ENTITY_ID, STATE_ON, UnitOfTemperature
from homeassistant.const import ATTR_ENTITY_ID, STATE_ON, STATE_UNAVAILABLE
from homeassistant.core import State
from homeassistant.exceptions import ServiceNotFound
@@ -30,8 +30,10 @@ from homeassistant.components.number import SERVICE_SET_VALUE
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.event import async_call_later
from homeassistant.util.unit_conversion import TemperatureConverter
from .const import UnknownEntity, overrides
from .keep_alive import IntervalCaller
_LOGGER = logging.getLogger(__name__)
@@ -51,6 +53,9 @@ class UnderlyingEntityType(StrEnum):
# a valve
VALVE = "valve"
# a Sonoff TRVZB
SONOFF_TRVZB = "sonoff_trvzb"
class UnderlyingEntity:
"""Represent a underlying device which could be a switch or a climate"""
@@ -60,6 +65,7 @@ class UnderlyingEntity:
_thermostat: Any
_entity_id: str
_type: UnderlyingEntityType
_hvac_mode: HVACMode | None
def __init__(
self,
@@ -73,6 +79,7 @@ class UnderlyingEntity:
self._thermostat = thermostat
self._type = entity_type
self._entity_id = entity_id
self._hvac_mode = None
def __str__(self):
return str(self._thermostat) + "-" + self._entity_id
@@ -98,13 +105,24 @@ class UnderlyingEntity:
async def set_hvac_mode(self, hvac_mode: HVACMode):
"""Set the HVACmode"""
self._hvac_mode = hvac_mode
return
@property
def hvac_mode(self) -> HVACMode | None:
"""Return the current hvac_mode"""
return self._hvac_mode
@property
def is_device_active(self) -> bool | None:
"""If the toggleable device is currently active."""
return None
@property
def hvac_action(self) -> HVACAction:
"""Calculate a hvac_action"""
return HVACAction.HEATING if self.is_device_active is True else HVACAction.OFF
async def set_temperature(self, temperature, max_temp, min_temp):
"""Set the target temperature"""
return
@@ -179,7 +197,6 @@ class UnderlyingSwitch(UnderlyingEntity):
_initialDelaySec: int
_on_time_sec: int
_off_time_sec: int
_hvac_mode: HVACMode
def __init__(
self,
@@ -187,6 +204,7 @@ class UnderlyingSwitch(UnderlyingEntity):
thermostat: Any,
switch_entity_id: str,
initial_delay_sec: int,
keep_alive_sec: float,
) -> None:
"""Initialize the underlying switch"""
@@ -201,7 +219,7 @@ class UnderlyingSwitch(UnderlyingEntity):
self._should_relaunch_control_heating = False
self._on_time_sec = 0
self._off_time_sec = 0
self._hvac_mode = None
self._keep_alive = IntervalCaller(hass, keep_alive_sec)
@property
def initial_delay_sec(self):
@@ -214,6 +232,16 @@ class UnderlyingSwitch(UnderlyingEntity):
"""Tells if the switch command should be inversed"""
return self._thermostat.is_inversed
@property
def keep_alive_sec(self) -> float:
"""Return the switch keep-alive interval in seconds."""
return self._keep_alive.interval_sec
@overrides
def startup(self):
super().startup()
self._keep_alive.set_async_action(self._keep_alive_callback)
# @overrides this breaks some unit tests TypeError: object MagicMock can't be used in 'await' expression
async def set_hvac_mode(self, hvac_mode: HVACMode) -> bool:
"""Set the HVACmode. Returns true if something have change"""
@@ -223,8 +251,8 @@ class UnderlyingSwitch(UnderlyingEntity):
await self.turn_off()
self._cancel_cycle()
if self._hvac_mode != hvac_mode:
self._hvac_mode = hvac_mode
if self.hvac_mode != hvac_mode:
super().set_hvac_mode(hvac_mode)
return True
else:
return False
@@ -237,35 +265,64 @@ class UnderlyingSwitch(UnderlyingEntity):
not self.is_inversed and real_state
)
async def _keep_alive_callback(self):
"""Keep alive: Turn on if already turned on, turn off if already turned off."""
timer = self._keep_alive.backoff_timer
state: State | None = self._hass.states.get(self._entity_id)
# Normal, expected state.state values are "on" and "off". An absent
# underlying MQTT switch was observed to produce either state == None
# or state.state == STATE_UNAVAILABLE ("unavailable").
if state is None or state.state == STATE_UNAVAILABLE:
if timer.is_ready():
_LOGGER.warning(
"Entity %s is not available (state: %s). Will keep trying "
"keep alive calls, but won't log this condition every time.",
self._entity_id,
state.state if state else "None",
)
else:
if timer.in_progress:
timer.reset()
_LOGGER.warning(
"Entity %s has recovered (state: %s).",
self._entity_id,
state.state,
)
await (self.turn_on() if self.is_device_active else self.turn_off())
# @overrides this breaks some unit tests TypeError: object MagicMock can't be used in 'await' expression
async def turn_off(self):
"""Turn heater toggleable device off."""
self._keep_alive.cancel() # Cancel early to avoid a turn_on/turn_off race condition
_LOGGER.debug("%s - Stopping underlying entity %s", self, self._entity_id)
command = SERVICE_TURN_OFF if not self.is_inversed else SERVICE_TURN_ON
domain = self._entity_id.split(".")[0]
# This may fails if called after shutdown
try:
data = {ATTR_ENTITY_ID: self._entity_id}
await self._hass.services.async_call(
domain,
command,
data,
)
try:
data = {ATTR_ENTITY_ID: self._entity_id}
await self._hass.services.async_call(domain, command, data)
self._keep_alive.set_async_action(self._keep_alive_callback)
except Exception:
self._keep_alive.cancel()
raise
except ServiceNotFound as err:
_LOGGER.error(err)
async def turn_on(self):
"""Turn heater toggleable device on."""
self._keep_alive.cancel() # Cancel early to avoid a turn_on/turn_off race condition
_LOGGER.debug("%s - Starting underlying entity %s", self, self._entity_id)
command = SERVICE_TURN_ON if not self.is_inversed else SERVICE_TURN_OFF
domain = self._entity_id.split(".")[0]
try:
data = {ATTR_ENTITY_ID: self._entity_id}
await self._hass.services.async_call(
domain,
command,
data,
)
try:
data = {ATTR_ENTITY_ID: self._entity_id}
await self._hass.services.async_call(domain, command, data)
self._keep_alive.set_async_action(self._keep_alive_callback)
except Exception:
self._keep_alive.cancel()
raise
except ServiceNotFound as err:
_LOGGER.error(err)
@@ -422,6 +479,7 @@ class UnderlyingSwitch(UnderlyingEntity):
def remove_entity(self):
"""Remove the entity after stopping its cycle"""
self._cancel_cycle()
self._keep_alive.cancel()
class UnderlyingClimate(UnderlyingEntity):
@@ -444,6 +502,7 @@ class UnderlyingClimate(UnderlyingEntity):
entity_id=climate_entity_id,
)
self._underlying_climate = None
self._last_sent_temperature = None
def find_underlying_climate(self) -> ClimateEntity:
"""Find the underlying climate entity"""
@@ -464,8 +523,8 @@ class UnderlyingClimate(UnderlyingEntity):
self._underlying_climate,
)
else:
_LOGGER.error(
"%s - Cannot find the underlying climate entity: %s. Thermostat will not be operational",
_LOGGER.info(
"%s - Cannot find the underlying climate entity: %s. Thermostat will not be operational. Will try later.",
self,
self.entity_id,
)
@@ -505,14 +564,11 @@ class UnderlyingClimate(UnderlyingEntity):
def is_device_active(self):
"""If the toggleable device is currently active."""
if self.is_initialized:
return (
self._underlying_climate.hvac_mode != HVACMode.OFF
and self._underlying_climate.hvac_action
not in [
HVACAction.IDLE,
HVACAction.OFF,
]
)
return self.hvac_mode != HVACMode.OFF and self.hvac_action not in [
HVACAction.IDLE,
HVACAction.OFF,
None,
]
else:
return None
@@ -567,12 +623,25 @@ class UnderlyingClimate(UnderlyingEntity):
"""Set the target temperature"""
if not self.is_initialized:
return
data = {
ATTR_ENTITY_ID: self._entity_id,
"temperature": self.cap_sent_value(temperature),
"target_temp_high": max_temp,
"target_temp_low": min_temp,
}
# Issue 508 we have to take care of service set_temperature or set_range
target_temp = self.cap_sent_value(temperature)
if (
ClimateEntityFeature.TARGET_TEMPERATURE_RANGE
in self._underlying_climate.supported_features
):
data = {
ATTR_ENTITY_ID: self._entity_id,
"target_temp_high": target_temp,
"target_temp_low": target_temp,
# issue 518 - we should send also the target temperature, even in TARGET RANGE
"temperature": target_temp,
}
else:
data = {
ATTR_ENTITY_ID: self._entity_id,
"temperature": target_temp,
}
await self._hass.services.async_call(
CLIMATE_DOMAIN,
@@ -580,12 +649,48 @@ class UnderlyingClimate(UnderlyingEntity):
data,
)
self._last_sent_temperature = target_temp
@property
def last_sent_temperature(self) -> float | None:
"""Get the last send temperature. None if no temperature have been sent yet"""
return self._last_sent_temperature
@property
def hvac_action(self) -> HVACAction | None:
"""Get the hvac action of the underlying"""
if not self.is_initialized:
return None
return self._underlying_climate.hvac_action
hvac_action = self._underlying_climate.hvac_action
if hvac_action is None:
target = (
self.underlying_target_temperature
or self._thermostat.target_temperature
)
current = (
self.underlying_current_temperature
or self._thermostat.current_temperature
)
hvac_mode = self.hvac_mode
_LOGGER.debug(
"%s - hvac_action simulation target=%s, current=%s, hvac_mode=%s",
self,
target,
current,
hvac_mode,
)
hvac_action = HVACAction.IDLE
if target is not None and current is not None:
dtemp = target - current
if hvac_mode == HVACMode.COOL and dtemp < 0:
hvac_action = HVACAction.COOLING
elif hvac_mode in [HVACMode.HEAT, HVACMode.HEAT_COOL] and dtemp > 0:
hvac_action = HVACAction.HEATING
return hvac_action
@property
def hvac_mode(self) -> HVACMode | None:
@@ -622,6 +727,13 @@ class UnderlyingClimate(UnderlyingEntity):
return []
return self._underlying_climate.hvac_modes
@property
def current_humidity(self) -> float | None:
"""Get the humidity"""
if not self.is_initialized:
return None
return self._underlying_climate.current_humidity
@property
def fan_modes(self) -> list[str]:
"""Get the fan_modes"""
@@ -640,7 +752,7 @@ class UnderlyingClimate(UnderlyingEntity):
def temperature_unit(self) -> str:
"""Get the temperature_unit"""
if not self.is_initialized:
return UnitOfTemperature.CELSIUS
return self._hass.config.units.temperature_unit
return self._underlying_climate.temperature_unit
@property
@@ -664,6 +776,35 @@ class UnderlyingClimate(UnderlyingEntity):
return 15
return self._underlying_climate.target_temperature_low
@property
def underlying_target_temperature(self) -> float:
"""Get the target_temperature"""
if not self.is_initialized:
return None
if not hasattr(self._underlying_climate, "target_temperature"):
return None
else:
return self._underlying_climate.target_temperature
# return self._hass.states.get(self._entity_id).attributes.get(
# "target_temperature"
# )
@property
def underlying_current_temperature(self) -> float | None:
"""Get the underlying current_temperature if it exists
and if initialized"""
if not self.is_initialized:
return None
if not hasattr(self._underlying_climate, "current_temperature"):
return None
else:
return self._underlying_climate.current_temperature
# return self._hass.states.get(self._entity_id).attributes.get("current_temperature")
@property
def is_aux_heat(self) -> bool:
"""Get the is_aux_heat"""
@@ -696,8 +837,12 @@ class UnderlyingClimate(UnderlyingEntity):
self._underlying_climate.min_temp is not None
and self._underlying_climate is not None
):
min_val = self._underlying_climate.min_temp
max_val = self._underlying_climate.max_temp
min_val = TemperatureConverter.convert(
self._underlying_climate.min_temp, self._underlying_climate.temperature_unit, self._hass.config.units.temperature_unit
)
max_val = TemperatureConverter.convert(
self._underlying_climate.max_temp, self._underlying_climate.temperature_unit, self._hass.config.units.temperature_unit
)
new_value = max(min_val, min(value, max_val))
else:
@@ -723,11 +868,12 @@ class UnderlyingValve(UnderlyingEntity):
_hvac_mode: HVACMode
# This is the percentage of opening int integer (from 0 to 100)
_percent_open: int
_last_sent_temperature = None
def __init__(
self, hass: HomeAssistant, thermostat: Any, valve_entity_id: str
) -> None:
"""Initialize the underlying switch"""
"""Initialize the underlying valve"""
super().__init__(
hass=hass,
@@ -741,19 +887,27 @@ class UnderlyingValve(UnderlyingEntity):
self._percent_open = self._thermostat.valve_open_percent
self._valve_entity_id = valve_entity_id
async def send_percent_open(self):
"""Send the percent open to the underlying valve"""
# This may fails if called after shutdown
async def _send_value_to_number(self, number_entity_id: str, value: int):
"""Send a value to a number entity"""
try:
data = {ATTR_ENTITY_ID: self._entity_id, "value": self._percent_open}
domain = self._entity_id.split(".")[0]
data = {"value": value}
target = {ATTR_ENTITY_ID: number_entity_id}
domain = number_entity_id.split(".")[0]
await self._hass.services.async_call(
domain,
SERVICE_SET_VALUE,
data,
domain=domain,
service=SERVICE_SET_VALUE,
service_data=data,
target=target,
)
except ServiceNotFound as err:
_LOGGER.error(err)
# This could happens in unit test if input_number domain is not yet loaded
# raise err
async def send_percent_open(self):
"""Send the percent open to the underlying valve"""
# This may fails if called after shutdown
return await self._send_value_to_number(self._entity_id, self._percent_open)
async def turn_off(self):
"""Turn heater toggleable device off."""
@@ -805,8 +959,10 @@ class UnderlyingValve(UnderlyingEntity):
):
"""We use this function to change the on_percent"""
if force:
self._percent_open = self.cap_sent_value(self._percent_open)
await self.send_percent_open()
# self._percent_open = self.cap_sent_value(self._percent_open)
# await self.send_percent_open()
# avoid to send 2 times the same value at startup
self.set_valve_open_percent()
@overrides
def cap_sent_value(self, value) -> float:
@@ -822,7 +978,7 @@ class UnderlyingValve(UnderlyingEntity):
min_val = valve_state.attributes["min"]
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:
_LOGGER.debug("%s - no min and max attributes on underlying", self)
new_value = value
@@ -858,3 +1014,111 @@ class UnderlyingValve(UnderlyingEntity):
def remove_entity(self):
"""Remove the entity after stopping its cycle"""
self._cancel_cycle()
class UnderlyingSonoffTRVZB(UnderlyingValve):
"""A specific underlying class for Sonoff TRVZB TRV"""
_offset_calibration_entity_id: str
_opening_degree_entity_id: str
_closing_degree_entity_id: str
def __init__(
self,
hass: HomeAssistant,
thermostat: Any,
offset_calibration_entity_id: str,
opening_degree_entity_id: str,
closing_degree_entity_id: str,
climate_underlying: UnderlyingClimate,
) -> None:
"""Initialize the underlying Sonoff TRV"""
super().__init__(hass, thermostat, opening_degree_entity_id)
self._offset_calibration_entity_id = offset_calibration_entity_id
self._opening_degree_entity_id = opening_degree_entity_id
self._closing_degree_entity_id = closing_degree_entity_id
self._climate_underlying = climate_underlying
self._is_min_max_initialized = False
self._max_opening_degree = None
self._min_offset_calibration = None
self._max_offset_calibration = None
async def send_percent_open(self):
"""Send the percent open to the underlying valve"""
if not self._is_min_max_initialized:
_LOGGER.debug(
"%s - initialize min offset_calibration and max open_degree", self
)
self._max_opening_degree = self._hass.states.get(
self._opening_degree_entity_id
).attributes.get("max")
self._min_offset_calibration = self._hass.states.get(
self._offset_calibration_entity_id
).attributes.get("min")
self._max_offset_calibration = self._hass.states.get(
self._offset_calibration_entity_id
).attributes.get("max")
self._is_min_max_initialized = (
self._max_opening_degree is not None
and self._min_offset_calibration is not None
)
if not self._is_min_max_initialized:
_LOGGER.warning(
"%s - impossible to initialize max_opening_degree or min_offset_calibration. Abort sending percent open to the valve. This could be a temporary message at startup."
)
return
# Send opening_degree
await super().send_percent_open()
# Send closing_degree.
await self._send_value_to_number(
self._closing_degree_entity_id,
closing_degree := self._max_opening_degree - self._percent_open,
)
# send offset_calibration to the difference between target temp and local temp
offset = 0
if (
local_temp := self._climate_underlying.underlying_current_temperature
) is not None and (
room_temp := self._thermostat.current_temperature
) is not None:
offset = min(
self._max_offset_calibration,
max(self._min_offset_calibration, room_temp - local_temp),
)
await self._send_value_to_number(self._offset_calibration_entity_id, offset)
_LOGGER.debug(
"%s - SonoffTRVZB - I have sent offset_calibration=%s opening_degree=%s closing_degree=%s",
self,
offset,
self._percent_open,
closing_degree,
)
@property
def offset_calibration_entity_id(self) -> str:
"""The offset_calibration_entity_id"""
return self._offset_calibration_entity_id
@property
def opening_degree_entity_id(self) -> str:
"""The offset_calibration_entity_id"""
return self._opening_degree_entity_id
@property
def closing_degree_entity_id(self) -> str:
"""The offset_calibration_entity_id"""
return self._closing_degree_entity_id
@property
def hvac_modes(self) -> list[HVACMode]:
"""Get the hvac_modes"""
if not self.is_initialized:
return []
return [HVACMode.OFF, HVACMode.HEAT]

View File

@@ -1,8 +1,13 @@
""" The API of Versatile Thermostat"""
import logging
from homeassistant.core import HomeAssistant
from homeassistant.config_entries import ConfigEntry
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.components.climate import ClimateEntity, DOMAIN as CLIMATE_DOMAIN
from homeassistant.components.number import NumberEntity
from .const import (
DOMAIN,
CONF_AUTO_REGULATION_EXPERT,
@@ -10,6 +15,7 @@ from .const import (
CONF_SAFETY_MODE,
CONF_THERMOSTAT_TYPE,
CONF_THERMOSTAT_CENTRAL_CONFIG,
CONF_MAX_ON_PERCENT,
)
VTHERM_API_NAME = "vtherm_api"
@@ -51,19 +57,26 @@ class VersatileThermostatAPI(dict):
self._central_boiler_entity = None
self._threshold_number_entity = None
self._nb_active_number_entity = None
self._central_configuration = None
self._central_mode_select = None
# A dict that will store all Number entities which holds the temperature
self._number_temperatures = dict()
self._max_on_percent = None
def find_central_configuration(self):
"""Search for a central configuration"""
for config_entry in VersatileThermostatAPI._hass.config_entries.async_entries(
DOMAIN
):
if (
config_entry.data.get(CONF_THERMOSTAT_TYPE)
== CONF_THERMOSTAT_CENTRAL_CONFIG
):
central_config = config_entry
return central_config
return None
if not self._central_configuration:
for (
config_entry
) in VersatileThermostatAPI._hass.config_entries.async_entries(DOMAIN):
if (
config_entry.data.get(CONF_THERMOSTAT_TYPE)
== CONF_THERMOSTAT_CENTRAL_CONFIG
):
self._central_configuration = config_entry
break
# return self._central_configuration
return self._central_configuration
def add_entry(self, entry: ConfigEntry):
"""Add a new entry"""
@@ -96,6 +109,12 @@ class VersatileThermostatAPI(dict):
if self._safety_mode:
_LOGGER.debug("We have found safet_mode params %s", self._safety_mode)
self._max_on_percent = config.get(CONF_MAX_ON_PERCENT)
if self._max_on_percent:
_LOGGER.debug(
"We have found max_on_percent setting %s", self._max_on_percent
)
def register_central_boiler(self, central_boiler_entity):
"""Register the central boiler entity. This is used by the CentralBoilerBinarySensor
class to register itself at creation"""
@@ -106,16 +125,116 @@ class VersatileThermostatAPI(dict):
):
"""register the two number entities needed for boiler activation"""
self._threshold_number_entity = threshold_number_entity
# If sensor and threshold number are initialized, reload the listener
# if self._nb_active_number_entity and self._central_boiler_entity:
# self._hass.async_add_job(self.reload_central_boiler_binary_listener)
def register_nb_device_active_boiler(self, nb_active_number_entity):
"""register the two number entities needed for boiler activation"""
self._nb_active_number_entity = nb_active_number_entity
# if self._threshold_number_entity and self._central_boiler_entity:
# self._hass.async_add_job(self.reload_central_boiler_binary_listener)
def register_temperature_number(
self,
config_id: str,
preset_name: str,
number_entity: NumberEntity,
):
"""Register the NumberEntity for a particular device / preset."""
# Search for device_name into the _number_temperatures dict
if not self._number_temperatures.get(config_id):
self._number_temperatures[config_id] = dict()
self._number_temperatures.get(config_id)[preset_name] = number_entity
def get_temperature_number_value(self, config_id, preset_name) -> float | None:
"""Returns the value of a previously registred NumberEntity which represent
a temperature. If no NumberEntity was previously registred, then returns None"""
entities = self._number_temperatures.get(config_id, None)
if entities:
entity = entities.get(preset_name, None)
if entity:
return entity.state
return None
async def init_vtherm_links(self, entry_id=None):
"""Initialize all VTherms entities links
This method is called when HA is fully started (and all entities should be initialized)
Or when we need to reload all VTherm links (with Number temp entities, central boiler, ...)
If entry_id is set, only the VTherm of this entry will be reloaded
"""
await self.reload_central_boiler_binary_listener()
await self.reload_central_boiler_entities_list()
# Initialization of all preset for all VTherm
component: EntityComponent[ClimateEntity] = self._hass.data.get(
CLIMATE_DOMAIN, None
)
if component:
for entity in component.entities:
# if hasattr(entity, "init_presets"):
# if (
# only_use_central is False
# or entity.use_central_config_temperature
# ):
# await entity.init_presets(self.find_central_configuration())
# A little hack to test if the climate is a VTherm. Cannot use isinstance
# due to circular dependency of BaseThermostat
if (
entity.device_info
and entity.device_info.get("model", None) == DOMAIN
):
if entry_id is None or entry_id == entity.unique_id:
await entity.async_startup(self.find_central_configuration())
async def init_vtherm_preset_with_central(self):
"""Init all VTherm presets when the VTherm uses central temperature"""
# Initialization of all preset for all VTherm
component: EntityComponent[ClimateEntity] = self._hass.data.get(
CLIMATE_DOMAIN, None
)
if component:
for entity in component.entities:
if (
entity.device_info
and entity.device_info.get("model", None) == DOMAIN
and entity.use_central_config_temperature
):
await entity.init_presets(self.find_central_configuration())
async def reload_central_boiler_binary_listener(self):
"""Reloads the BinarySensor entity which listen to the number of
active devices and the thresholds entities"""
if self._central_boiler_entity:
await self._central_boiler_entity.listen_nb_active_vtherm_entity()
async def reload_central_boiler_entities_list(self):
"""Reload the central boiler list of entities if a central boiler is used"""
if self._nb_active_number_entity is not None:
await self._nb_active_number_entity.listen_vtherms_entities()
def register_central_mode_select(self, central_mode_select):
"""Register the select entity which holds the central_mode"""
self._central_mode_select = central_mode_select
async def notify_central_mode_change(self, old_central_mode: str | None = None):
"""Notify all VTherm that the central_mode have change"""
if self._central_mode_select is None:
return
# Update all VTherm states
component: EntityComponent[ClimateEntity] = self.hass.data[CLIMATE_DOMAIN]
for entity in component.entities:
if entity.device_info and entity.device_info.get("model", None) == DOMAIN:
_LOGGER.debug(
"Changing the central_mode. We have find %s to update",
entity.name,
)
await entity.check_central_mode(
self._central_mode_select.state, old_central_mode
)
@property
def self_regulation_expert(self):
"""Get the self regulation params"""
@@ -131,6 +250,11 @@ class VersatileThermostatAPI(dict):
"""Get the safety_mode params"""
return self._safety_mode
@property
def max_on_percent(self):
"""Get the max_open_percent params"""
return self._max_on_percent
@property
def central_boiler_entity(self):
"""Get the central boiler binary_sensor entity"""
@@ -165,6 +289,14 @@ class VersatileThermostatAPI(dict):
return None
return int(self._threshold_number_entity.native_value)
@property
def central_mode(self) -> str | None:
"""Get the current central mode or None"""
if self._central_mode_select:
return self._central_mode_select.state
else:
return None
@property
def hass(self):
"""Get the HomeAssistant object"""

View File

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

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.

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: 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
ffmpeg
homeassistant==2024.10.4

View File

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

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, too-many-lines, redefined-builtin
""" Some common resources """
import asyncio
import logging
from unittest.mock import patch, MagicMock
from unittest.mock import patch, MagicMock # pylint: disable=unused-import
import pytest # pylint: disable=unused-import
from homeassistant.core import HomeAssistant, Event, EVENT_STATE_CHANGED, State
@@ -23,9 +23,7 @@ from homeassistant.components.switch import (
SwitchEntity,
)
from homeassistant.components.number import (
NumberEntity,
)
from homeassistant.components.number import NumberEntity, DOMAIN as NUMBER_DOMAIN
from pytest_homeassistant_custom_component.common import MockConfigEntry
@@ -51,6 +49,7 @@ from .const import ( # pylint: disable=unused-import
MOCK_TH_OVER_CLIMATE_MAIN_CONFIG,
MOCK_TH_OVER_CLIMATE_CENTRAL_MAIN_CONFIG,
MOCK_TH_OVER_CLIMATE_TYPE_CONFIG,
MOCK_TH_OVER_CLIMATE_TYPE_USE_DEVICE_TEMP_CONFIG,
MOCK_TH_OVER_CLIMATE_TYPE_AC_CONFIG,
MOCK_TH_OVER_CLIMATE_TYPE_NOT_REGULATED_CONFIG,
MOCK_TH_OVER_SWITCH_TPI_CONFIG,
@@ -71,6 +70,12 @@ from .const import ( # pylint: disable=unused-import
overrides,
)
MOCK_FULL_FEATURES = {
CONF_USE_WINDOW_FEATURE: True,
CONF_USE_MOTION_FEATURE: True,
CONF_USE_POWER_FEATURE: True,
CONF_USE_PRESENCE_FEATURE: True,
}
FULL_SWITCH_CONFIG = (
MOCK_TH_OVER_SWITCH_USER_CONFIG
@@ -79,6 +84,7 @@ FULL_SWITCH_CONFIG = (
| MOCK_TH_OVER_SWITCH_TYPE_CONFIG
| MOCK_TH_OVER_SWITCH_TPI_CONFIG
| MOCK_PRESETS_CONFIG
| MOCK_FULL_FEATURES
| MOCK_WINDOW_CONFIG
| MOCK_MOTION_CONFIG
| MOCK_POWER_CONFIG
@@ -93,6 +99,7 @@ FULL_SWITCH_AC_CONFIG = (
| MOCK_TH_OVER_SWITCH_AC_TYPE_CONFIG
| MOCK_TH_OVER_SWITCH_TPI_CONFIG
| MOCK_PRESETS_AC_CONFIG
| MOCK_FULL_FEATURES
| MOCK_WINDOW_CONFIG
| MOCK_MOTION_CONFIG
| MOCK_POWER_CONFIG
@@ -100,7 +107,6 @@ FULL_SWITCH_AC_CONFIG = (
| MOCK_ADVANCED_CONFIG
)
PARTIAL_CLIMATE_CONFIG = (
MOCK_TH_OVER_CLIMATE_USER_CONFIG
| MOCK_TH_OVER_CLIMATE_MAIN_CONFIG
@@ -110,6 +116,15 @@ PARTIAL_CLIMATE_CONFIG = (
| MOCK_ADVANCED_CONFIG
)
PARTIAL_CLIMATE_CONFIG_USE_DEVICE_TEMP = (
MOCK_TH_OVER_CLIMATE_USER_CONFIG
| MOCK_TH_OVER_CLIMATE_MAIN_CONFIG
| MOCK_TH_OVER_CLIMATE_CENTRAL_MAIN_CONFIG
| MOCK_TH_OVER_CLIMATE_TYPE_USE_DEVICE_TEMP_CONFIG
| MOCK_PRESETS_CONFIG
| MOCK_ADVANCED_CONFIG
)
PARTIAL_CLIMATE_NOT_REGULATED_CONFIG = (
MOCK_TH_OVER_CLIMATE_USER_CONFIG
| MOCK_TH_OVER_CLIMATE_MAIN_CONFIG
@@ -173,12 +188,13 @@ FULL_CENTRAL_CONFIG = {
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_ADD_CENTRAL_BOILER_CONTROL: False,
CONF_USE_CENTRAL_BOILER_FEATURE: False,
}
FULL_CENTRAL_CONFIG_WITH_BOILER = {
@@ -219,7 +235,7 @@ FULL_CENTRAL_CONFIG_WITH_BOILER = {
CONF_SECURITY_DELAY_MIN: 61,
CONF_SECURITY_MIN_ON_PERCENT: 0.5,
CONF_SECURITY_DEFAULT_ON_PERCENT: 0.2,
CONF_ADD_CENTRAL_BOILER_CONTROL: True,
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",
}
@@ -313,6 +329,10 @@ class MockClimate(ClimateEntity):
"""Set the HVACaction"""
self._attr_hvac_action = hvac_action
def set_current_temperature(self, current_temperature):
"""Set the current_temperature"""
self._attr_current_temperature = current_temperature
class MockUnavailableClimate(ClimateEntity):
"""A Mock Climate class used for Underlying climate mode"""
@@ -415,6 +435,86 @@ class MagicMockClimate(MagicMock):
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"""
@@ -452,7 +552,14 @@ 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, entry_infos={}
self,
hass: HomeAssistant,
unique_id,
name,
min=0,
max=100,
step=1,
entry_infos={},
):
"""Init the switch"""
super().__init__()
@@ -462,7 +569,9 @@ class MockNumber(NumberEntity):
self.entity_id = self.platform + "." + unique_id
self._name = name
self._attr_native_value = 0
self._attr_native_min_value = 0
self._attr_native_min_value = min
self._attr_native_max_value = max
self._attr_step = step
@property
def name(self) -> str:
@@ -479,14 +588,18 @@ async def create_thermostat(
hass: HomeAssistant, entry: MockConfigEntry, entity_id: str
) -> BaseThermostat:
"""Creates and return a TPI Thermostat"""
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
):
entry.add_to_hass(hass)
await hass.config_entries.async_setup(entry.entry_id)
assert entry.state is ConfigEntryState.LOADED
entry.add_to_hass(hass)
await hass.config_entries.async_setup(entry.entry_id)
assert entry.state is ConfigEntryState.LOADED
return search_entity(hass, entity_id, CLIMATE_DOMAIN)
# We should reload the VTherm links
# vtherm_api: VersatileThermostatAPI = VersatileThermostatAPI.get_vtherm_api()
# central_config = vtherm_api.find_central_configuration()
entity = search_entity(hass, entity_id, CLIMATE_DOMAIN)
# if entity and hasattr(entity, "init_presets")::
# await entity.init_presets(central_config)
return entity
async def create_central_config( # pylint: disable=dangerous-default-value
@@ -509,11 +622,14 @@ async def create_central_config( # pylint: disable=dangerous-default-value
central_configuration = api.find_central_configuration()
assert central_configuration is not None
return central_configuration
def search_entity(hass: HomeAssistant, entity_id, domain) -> Entity:
"""Search and return the entity in the domain"""
component = hass.data[domain]
for entity in component.entities:
_LOGGER.debug("Found %s entity: %s", domain, entity.entity_id)
if entity.entity_id == entity_id:
return entity
return None
@@ -553,6 +669,31 @@ async def send_temperature_change_event(
return dearm_window_auto
async def send_last_seen_temperature_change_event(
entity: BaseThermostat, date, sleep=True
):
"""Sending a new last seen event simulating a change on last seen temperature sensor"""
_LOGGER.info(
"------- Testu: sending send_last_seen_temperature_change_event, date=%s on %s",
date,
entity,
)
last_seen_event = Event(
EVENT_STATE_CHANGED,
{
"new_state": State(
entity_id=entity.entity_id,
state=date,
last_changed=date,
last_updated=date,
)
},
)
await entity._async_last_seen_temperature_changed(last_seen_event)
if sleep:
await asyncio.sleep(0.1)
async def send_ext_temperature_change_event(
entity: BaseThermostat, new_temp, date, sleep=True
):
@@ -790,6 +931,7 @@ async def send_climate_change_event_with_temperature(
date,
temperature,
sleep=True,
underlying_entity_id=None,
):
"""Sending a new climate event simulating a change on the underlying climate state"""
_LOGGER.info(
@@ -802,18 +944,21 @@ async def send_climate_change_event_with_temperature(
temperature,
entity,
)
if not underlying_entity_id:
underlying_entity_id = entity.entity_id
climate_event = Event(
EVENT_STATE_CHANGED,
{
"new_state": State(
entity_id=entity.entity_id,
entity_id=underlying_entity_id,
state=new_hvac_mode,
attributes={"hvac_action": new_hvac_action, "temperature": temperature},
last_changed=date,
last_updated=date,
),
"old_state": State(
entity_id=entity.entity_id,
entity_id=underlying_entity_id,
state=old_hvac_mode,
attributes={"hvac_action": old_hvac_action},
last_changed=date,
@@ -833,3 +978,53 @@ def cancel_switchs_cycles(entity: BaseThermostat):
return
for under in entity._underlyings:
under._cancel_cycle()
async def set_climate_preset_temp(
entity: BaseThermostat, temp_number_name: str, temp: float
):
"""Set a preset value in the temp Number entity"""
number_entity_id = (
NUMBER_DOMAIN
+ "."
+ entity.entity_id.split(".")[1]
+ "_preset_"
+ temp_number_name
+ PRESET_TEMP_SUFFIX
)
temp_entity = search_entity(
entity.hass,
number_entity_id,
NUMBER_DOMAIN,
)
if temp_entity:
await temp_entity.async_set_native_value(temp)
else:
_LOGGER.warning(
"commons tests set_cliamte_preset_temp: cannot find number entity with entity_id '%s'",
number_entity_id,
)
async def set_all_climate_preset_temp(
hass, vtherm: BaseThermostat, temps: dict, number_entity_base_name: str
):
"""Initialize all temp of preset for a VTherm entity"""
# We initialize
for preset_name, value in temps.items():
await set_climate_preset_temp(vtherm, preset_name, value)
# Search the number entity to control it is correctly set
number_entity_name = (
f"number.{number_entity_base_name}_preset_{preset_name}{PRESET_TEMP_SUFFIX}"
)
temp_entity: NumberEntity = search_entity(
hass,
number_entity_name,
NUMBER_DOMAIN,
)
assert temp_entity
# Because set_value is not implemented in Number class (really don't understand why...)
assert temp_entity.state == value

View File

@@ -35,8 +35,31 @@ from .commons import (
FULL_CENTRAL_CONFIG_WITH_BOILER,
)
# https://github.com/miketheman/pytest-socket/pull/275
from pytest_socket import socket_allow_hosts
# ...
# ...
def pytest_runtest_setup():
"""setup tests"""
socket_allow_hosts(
allowed=["localhost", "127.0.0.1", "::1"], allow_unix_socket=True
)
pytest_plugins = "pytest_homeassistant_custom_component" # pylint: disable=invalid-name
# Permet d'exclure certains test en mode d'ex
# sequential = pytest.mark.sequential
# This fixture allow to execute some tests first and not in //
# @pytest.fixture
# def order():
# return 1
#
# This fixture enables loading custom integrations in all tests.
# Remove to enable selective use of this fixture

View File

@@ -1,4 +1,5 @@
""" The commons const for all tests """
from homeassistant.components.climate.const import ( # pylint: disable=unused-import
PRESET_BOOST,
PRESET_COMFORT,
@@ -18,10 +19,10 @@ MOCK_TH_OVER_SWITCH_MAIN_CONFIG = {
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
CONF_CYCLE_MIN: 5,
CONF_DEVICE_POWER: 1,
CONF_USE_WINDOW_FEATURE: True,
CONF_USE_MOTION_FEATURE: True,
CONF_USE_POWER_FEATURE: True,
CONF_USE_PRESENCE_FEATURE: True,
# CONF_USE_WINDOW_FEATURE: True,
# CONF_USE_MOTION_FEATURE: True,
# CONF_USE_POWER_FEATURE: True,
# CONF_USE_PRESENCE_FEATURE: True,
CONF_USE_MAIN_CENTRAL_CONFIG: True,
}
@@ -52,7 +53,7 @@ MOCK_TH_OVER_CLIMATE_MAIN_CONFIG = {
CONF_CYCLE_MIN: 5,
CONF_DEVICE_POWER: 1,
CONF_USE_MAIN_CENTRAL_CONFIG: False,
CONF_USE_CENTRAL_MODE: True
CONF_USE_CENTRAL_MODE: True,
# Keep default values which are False
}
@@ -73,24 +74,23 @@ MOCK_TH_OVER_SWITCH_CENTRAL_MAIN_CONFIG = {
}
MOCK_TH_OVER_SWITCH_TYPE_CONFIG = {
CONF_HEATER: "switch.mock_switch",
CONF_UNDERLYING_LIST: ["switch.mock_switch"],
CONF_HEATER_KEEP_ALIVE: 0,
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
CONF_AC_MODE: False,
CONF_INVERSE_SWITCH: False,
}
MOCK_TH_OVER_SWITCH_AC_TYPE_CONFIG = {
CONF_HEATER: "switch.mock_air_conditioner",
CONF_UNDERLYING_LIST: ["switch.mock_air_conditioner"],
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
CONF_AC_MODE: True,
CONF_INVERSE_SWITCH: False,
}
MOCK_TH_OVER_4SWITCH_TYPE_CONFIG = {
CONF_HEATER: "switch.mock_4switch0",
CONF_HEATER_2: "switch.mock_4switch1",
CONF_HEATER_3: "switch.mock_4switch2",
CONF_HEATER_4: "switch.mock_4switch3",
CONF_UNDERLYING_LIST: ["switch.mock_4switch0", "switch.mock_4switch1","switch.mock_4switch2","switch.mock_4switch3"],
CONF_HEATER_KEEP_ALIVE: 0,
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
CONF_AC_MODE: False,
CONF_INVERSE_SWITCH: False,
@@ -102,43 +102,56 @@ MOCK_TH_OVER_SWITCH_TPI_CONFIG = {
}
MOCK_TH_OVER_CLIMATE_TYPE_CONFIG = {
CONF_CLIMATE: "climate.mock_climate",
CONF_UNDERLYING_LIST: ["climate.mock_climate"],
CONF_AC_MODE: False,
CONF_AUTO_REGULATION_MODE: CONF_AUTO_REGULATION_STRONG,
CONF_AUTO_REGULATION_DTEMP: 0.5,
CONF_AUTO_REGULATION_PERIOD_MIN: 2,
CONF_AUTO_FAN_MODE: CONF_AUTO_FAN_HIGH,
CONF_AUTO_REGULATION_USE_DEVICE_TEMP: False,
}
MOCK_TH_OVER_CLIMATE_TYPE_USE_DEVICE_TEMP_CONFIG = {
CONF_UNDERLYING_LIST: ["climate.mock_climate"],
CONF_AC_MODE: False,
CONF_AUTO_REGULATION_MODE: CONF_AUTO_REGULATION_STRONG,
CONF_AUTO_REGULATION_DTEMP: 0.1,
CONF_AUTO_REGULATION_PERIOD_MIN: 2,
CONF_AUTO_FAN_MODE: CONF_AUTO_FAN_HIGH,
CONF_AUTO_REGULATION_USE_DEVICE_TEMP: True,
}
MOCK_TH_OVER_CLIMATE_TYPE_NOT_REGULATED_CONFIG = {
CONF_CLIMATE: "climate.mock_climate",
CONF_UNDERLYING_LIST: ["climate.mock_climate"],
CONF_AC_MODE: False,
CONF_AUTO_REGULATION_MODE: CONF_AUTO_REGULATION_NONE,
}
MOCK_TH_OVER_CLIMATE_TYPE_AC_CONFIG = {
CONF_CLIMATE: "climate.mock_climate",
CONF_UNDERLYING_LIST: ["climate.mock_climate"],
CONF_AC_MODE: True,
CONF_AUTO_REGULATION_MODE: CONF_AUTO_REGULATION_STRONG,
CONF_AUTO_REGULATION_DTEMP: 0.5,
CONF_AUTO_REGULATION_PERIOD_MIN: 1,
}
# TODO remove this later
MOCK_PRESETS_CONFIG = {
PRESET_FROST_PROTECTION + "_temp": 7,
PRESET_ECO + "_temp": 16,
PRESET_COMFORT + "_temp": 17,
PRESET_BOOST + "_temp": 18,
PRESET_FROST_PROTECTION + PRESET_TEMP_SUFFIX: 7,
PRESET_ECO + PRESET_TEMP_SUFFIX: 16,
PRESET_COMFORT + PRESET_TEMP_SUFFIX: 17,
PRESET_BOOST + PRESET_TEMP_SUFFIX: 18,
}
# TODO remove this later
MOCK_PRESETS_AC_CONFIG = {
PRESET_FROST_PROTECTION + "_temp": 7,
PRESET_ECO + "_temp": 17,
PRESET_COMFORT + "_temp": 19,
PRESET_BOOST + "_temp": 20,
PRESET_ECO + "_ac_temp": 25,
PRESET_COMFORT + "_ac_temp": 23,
PRESET_BOOST + "_ac_temp": 21,
PRESET_FROST_PROTECTION + PRESET_TEMP_SUFFIX: 7,
PRESET_ECO + PRESET_TEMP_SUFFIX: 17,
PRESET_COMFORT + PRESET_TEMP_SUFFIX: 19,
PRESET_BOOST + PRESET_TEMP_SUFFIX: 20,
PRESET_ECO + PRESET_AC_SUFFIX + PRESET_TEMP_SUFFIX: 25,
PRESET_COMFORT + PRESET_AC_SUFFIX + PRESET_TEMP_SUFFIX: 23,
PRESET_BOOST + PRESET_AC_SUFFIX + PRESET_TEMP_SUFFIX: 21,
}
MOCK_WINDOW_CONFIG = {
@@ -174,20 +187,10 @@ MOCK_POWER_CONFIG = {
MOCK_PRESENCE_CONFIG = {
CONF_PRESENCE_SENSOR: "person.presence_sensor",
PRESET_ECO + PRESET_AWAY_SUFFIX + "_temp": 16,
PRESET_COMFORT + PRESET_AWAY_SUFFIX + "_temp": 17,
PRESET_BOOST + PRESET_AWAY_SUFFIX + "_temp": 18,
}
MOCK_PRESENCE_AC_CONFIG = {
CONF_PRESENCE_SENSOR: "person.presence_sensor",
PRESET_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 = {

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",
return_value=fake_underlying_climate,
):
entry.add_to_hass(hass)
await hass.config_entries.async_setup(entry.entry_id)
assert entry.state is ConfigEntryState.LOADED
entity: ThermostatOverClimate = search_entity(
hass, "climate.theoverclimatemockname", "climate"
)
entity = await create_thermostat(hass, entry, "climate.theoverclimatemockname")
# entry.add_to_hass(hass)
# await hass.config_entries.async_setup(entry.entry_id)
# assert entry.state is ConfigEntryState.LOADED
#
# entity: ThermostatOverClimate = search_entity(
# hass, "climate.theoverclimatemockname", "climate"
# )
assert entity
assert isinstance(entity, ThermostatOverClimate)

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -1,97 +1,34 @@
# pylint: disable=wildcard-import, unused-wildcard-import, protected-access, unused-argument, line-too-long
# pylint: disable=wildcard-import, unused-wildcard-import, protected-access, unused-argument, line-too-long, too-many-lines
""" Test the Window management """
from unittest.mock import patch, call
import asyncio
from unittest.mock import patch, call, PropertyMock
from datetime import datetime, timedelta
import logging
from homeassistant.const import STATE_ON, STATE_OFF
from homeassistant.core import HomeAssistant
from homeassistant.components.climate import (
SERVICE_SET_TEMPERATURE,
)
from custom_components.versatile_thermostat.config_flow import (
VersatileThermostatBaseConfigFlow,
)
from custom_components.versatile_thermostat.thermostat_climate import (
ThermostatOverClimate,
)
from custom_components.versatile_thermostat.thermostat_switch import (
ThermostatOverSwitch,
)
from .commons import *
logging.getLogger().setLevel(logging.DEBUG)
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_bug_56(
hass: HomeAssistant,
skip_hass_states_is_state,
skip_turn_on_off_heater,
skip_send_event,
):
"""Test that in over_climate mode there is no error when underlying climate is not available"""
the_mock_underlying = MagicMockClimate()
with patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
return_value=None, # dont find the underlying climate
):
entry = MockConfigEntry(
domain=DOMAIN,
title="TheOverClimateMockName",
unique_id="uniqueId",
data={
CONF_NAME: "TheOverClimateMockName",
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_CLIMATE,
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
CONF_CYCLE_MIN: 5,
CONF_TEMP_MIN: 15,
CONF_TEMP_MAX: 30,
"eco_temp": 17,
"comfort_temp": 18,
"boost_temp": 19,
CONF_USE_WINDOW_FEATURE: False,
CONF_USE_MOTION_FEATURE: False,
CONF_USE_POWER_FEATURE: False,
CONF_USE_PRESENCE_FEATURE: False,
CONF_CLIMATE: "climate.mock_climate",
CONF_MINIMAL_ACTIVATION_DELAY: 30,
CONF_SECURITY_DELAY_MIN: 5,
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
},
)
entity: BaseThermostat = await create_thermostat(
hass, entry, "climate.theoverclimatemockname"
)
assert entity
# cause the underlying climate was not found
assert entity.is_over_climate is True
assert entity.underlying_entity(0)._underlying_climate is None
# Should not failed
entity.update_custom_attributes()
# try to call async_control_heating
try:
ret = await entity.async_control_heating()
# an exception should be send
assert ret is False
except Exception: # pylint: disable=broad-exception-caught
assert False
# This time the underlying will be found
with patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
return_value=the_mock_underlying, # dont find the underlying climate
):
# try to call async_control_heating
try:
await entity.async_control_heating()
except UnknownEntity:
assert False
except Exception: # pylint: disable=broad-exception-caught
assert False
# Should not failed
entity.update_custom_attributes()
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_bug_63(
@@ -191,389 +128,6 @@ async def test_bug_64(
assert entity
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_bug_66(
hass: HomeAssistant,
skip_hass_states_is_state,
skip_turn_on_off_heater,
skip_send_event,
):
"""Test that it should be possible to open/close the window rapidly without side effect"""
tz = get_tz(hass) # pylint: disable=invalid-name
now: datetime = datetime.now(tz=tz)
entry = MockConfigEntry(
domain=DOMAIN,
title="TheOverSwitchMockName",
unique_id="uniqueId",
data={
CONF_NAME: "TheOverSwitchMockName",
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_SWITCH,
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
CONF_CYCLE_MIN: 5,
CONF_TEMP_MIN: 15,
CONF_TEMP_MAX: 30,
"eco_temp": 17,
"comfort_temp": 18,
"boost_temp": 19,
CONF_USE_WINDOW_FEATURE: True,
CONF_USE_MOTION_FEATURE: False,
CONF_USE_POWER_FEATURE: False,
CONF_USE_PRESENCE_FEATURE: False,
CONF_HEATER: "switch.mock_switch",
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
CONF_TPI_COEF_INT: 0.3,
CONF_TPI_COEF_EXT: 0.01,
CONF_MINIMAL_ACTIVATION_DELAY: 30,
CONF_SECURITY_DELAY_MIN: 5,
CONF_SECURITY_MIN_ON_PERCENT: 0.5,
CONF_SECURITY_DEFAULT_ON_PERCENT: 0.1, # !! here
CONF_DEVICE_POWER: 200,
CONF_WINDOW_SENSOR: "binary_sensor.mock_window_sensor",
CONF_WINDOW_DELAY: 0, # important to not been obliged to wait
},
)
entity: BaseThermostat = await create_thermostat(
hass, entry, "climate.theoverswitchmockname"
)
assert entity
await entity.async_set_hvac_mode(HVACMode.HEAT)
await entity.async_set_preset_mode(PRESET_BOOST)
assert entity.hvac_mode is HVACMode.HEAT
assert entity.preset_mode is PRESET_BOOST
assert entity.target_temperature == 19
assert entity.window_state is STATE_OFF
# Open the window and let the thermostat shut down
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off, patch(
"homeassistant.helpers.condition.state", return_value=True
) as mock_condition, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
return_value=True,
):
await send_temperature_change_event(entity, 15, now)
try_window_condition = await send_window_change_event(
entity, True, False, now, False
)
# simulate the call to try_window_condition
await try_window_condition(None)
assert mock_send_event.call_count == 1
assert mock_heater_on.call_count == 1
assert mock_heater_off.call_count >= 1
assert mock_condition.call_count == 1
assert entity.window_state == STATE_ON
# Close the window but too shortly
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off, patch(
"homeassistant.helpers.condition.state", return_value=False
) as mock_condition, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
return_value=False,
):
event_timestamp = now + timedelta(minutes=1)
try_window_condition = await send_window_change_event(
entity, False, True, event_timestamp
)
# simulate the call to try_window_condition
await try_window_condition(None)
# window state should not have change
assert entity.window_state == STATE_ON
# Reopen immediatly with sufficient time
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off, patch(
"homeassistant.helpers.condition.state", return_value=True
) as mock_condition, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
return_value=False,
):
try_window_condition = await send_window_change_event(
entity, True, False, event_timestamp
)
# simulate the call to try_window_condition
await try_window_condition(None)
# still no change
assert entity.window_state == STATE_ON
assert entity.hvac_mode == HVACMode.OFF
# Close the window but with sufficient time this time
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off, patch(
"homeassistant.helpers.condition.state", return_value=True
) as mock_condition, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
return_value=False,
):
event_timestamp = now + timedelta(minutes=2)
try_window_condition = await send_window_change_event(
entity, False, True, event_timestamp
)
# simulate the call to try_window_condition
await try_window_condition(None)
# window state should be Off this time and old state should have been restored
assert entity.window_state == STATE_OFF
assert entity.hvac_mode is HVACMode.HEAT
assert entity.preset_mode is PRESET_BOOST
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_bug_82(
hass: HomeAssistant,
skip_hass_states_is_state,
skip_turn_on_off_heater,
skip_send_event,
):
"""Test that when a underlying climate is not available the VTherm doesn't go into safety mode"""
tz = get_tz(hass) # pylint: disable=invalid-name
now: datetime = datetime.now(tz=tz)
entry = MockConfigEntry(
domain=DOMAIN,
title="TheOverClimateMockName",
unique_id="uniqueId",
data=PARTIAL_CLIMATE_CONFIG, # 5 minutes security delay
)
fake_underlying_climate = MockUnavailableClimate(
hass, "mockUniqueId", "MockClimateName", {}
)
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
return_value=fake_underlying_climate,
) as mock_find_climate:
entry.add_to_hass(hass)
await hass.config_entries.async_setup(entry.entry_id)
assert entry.state is ConfigEntryState.LOADED
def find_my_entity(entity_id) -> ClimateEntity:
"""Find my new entity"""
component: EntityComponent[ClimateEntity] = hass.data[CLIMATE_DOMAIN]
for entity in component.entities:
if entity.entity_id == entity_id:
return entity
entity = find_my_entity("climate.theoverclimatemockname")
assert entity
assert entity.name == "TheOverClimateMockName"
assert entity.is_over_climate is True
# assert entity.hvac_action is HVACAction.OFF
assert entity.hvac_mode is HVACMode.OFF
# assert entity.hvac_mode is None
assert entity.target_temperature == entity.min_temp
assert entity.preset_modes == [
PRESET_NONE,
PRESET_FROST_PROTECTION,
PRESET_ECO,
PRESET_COMFORT,
PRESET_BOOST,
]
assert entity.preset_mode is PRESET_NONE
assert entity._security_state is False
# should have been called with EventType.PRESET_EVENT and EventType.HVAC_MODE_EVENT
assert mock_send_event.call_count == 2
mock_send_event.assert_has_calls(
[
call.send_event(EventType.PRESET_EVENT, {"preset": PRESET_NONE}),
call.send_event(
EventType.HVAC_MODE_EVENT,
{"hvac_mode": HVACMode.OFF},
),
]
)
assert mock_find_climate.call_count == 1
assert mock_find_climate.mock_calls[0] == call()
mock_find_climate.assert_has_calls([call.find_underlying_entity()])
# Force safety mode
assert entity._last_ext_temperature_measure is not None
assert entity._last_temperature_measure is not None
assert (
entity._last_temperature_measure.astimezone(tz) - now
).total_seconds() < 1
assert (
entity._last_ext_temperature_measure.astimezone(tz) - now
).total_seconds() < 1
# Tries to turns on the Thermostat
await entity.async_set_hvac_mode(HVACMode.HEAT)
assert entity.hvac_mode == HVACMode.HEAT
# 2. activate security feature when date is expired
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
):
event_timestamp = now - timedelta(minutes=6)
# set temperature to 15 so that on_percent will be > security_min_on_percent (0.2)
await send_temperature_change_event(entity, 15, event_timestamp)
# Should stay False
assert entity.security_state is False
assert entity.preset_mode == "none"
assert entity._saved_preset_mode == "none"
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_bug_101(
hass: HomeAssistant,
skip_hass_states_is_state,
skip_turn_on_off_heater,
skip_send_event,
):
"""Test that when a underlying climate target temp is changed, the VTherm change its own temperature target and switch to manual"""
tz = get_tz(hass) # pylint: disable=invalid-name
now: datetime = datetime.now(tz=tz)
entry = MockConfigEntry(
domain=DOMAIN,
title="TheOverClimateMockName",
unique_id="uniqueId",
data=PARTIAL_CLIMATE_NOT_REGULATED_CONFIG, # 5 minutes security delay
)
# Underlying is in HEAT mode but should be shutdown at startup
fake_underlying_climate = MockClimate(
hass, "mockUniqueId", "MockClimateName", {}, HVACMode.HEAT, HVACAction.HEATING
)
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
return_value=fake_underlying_climate,
) as mock_find_climate, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.set_hvac_mode"
) as mock_underlying_set_hvac_mode:
entry.add_to_hass(hass)
await hass.config_entries.async_setup(entry.entry_id)
assert entry.state is ConfigEntryState.LOADED
def find_my_entity(entity_id) -> ClimateEntity:
"""Find my new entity"""
component: EntityComponent[ClimateEntity] = hass.data[CLIMATE_DOMAIN]
for entity in component.entities:
if entity.entity_id == entity_id:
return entity
entity = find_my_entity("climate.theoverclimatemockname")
assert entity
assert entity.name == "TheOverClimateMockName"
assert entity.is_over_climate is True
assert entity.hvac_mode is HVACMode.OFF
# because in MockClimate HVACAction is HEATING if hvac_mode is not set
assert entity.hvac_action is HVACAction.HEATING
# Underlying should have been shutdown
assert mock_underlying_set_hvac_mode.call_count == 1
mock_underlying_set_hvac_mode.assert_has_calls(
[
call.set_hvac_mode(HVACMode.OFF),
]
)
assert entity.target_temperature == entity.min_temp
assert entity.preset_mode is PRESET_NONE
# should have been called with EventType.PRESET_EVENT and EventType.HVAC_MODE_EVENT
assert mock_send_event.call_count == 2
mock_send_event.assert_has_calls(
[
call.send_event(EventType.PRESET_EVENT, {"preset": PRESET_NONE}),
call.send_event(
EventType.HVAC_MODE_EVENT,
{"hvac_mode": HVACMode.OFF},
),
]
)
assert mock_find_climate.call_count == 1
assert mock_find_climate.mock_calls[0] == call()
mock_find_climate.assert_has_calls([call.find_underlying_entity()])
# Force preset mode
await entity.async_set_hvac_mode(HVACMode.HEAT)
assert entity.hvac_mode == HVACMode.HEAT
await entity.async_set_preset_mode(PRESET_COMFORT)
assert entity.preset_mode == PRESET_COMFORT
# 2. Change the target temp of underlying thermostat at now -> the event will be disgarded because to fast (to avoid loop cf issue 121)
await send_climate_change_event_with_temperature(
entity,
HVACMode.HEAT,
HVACMode.HEAT,
HVACAction.OFF,
HVACAction.OFF,
now,
12.75,
)
# Should NOT have been switched to Manual preset
assert entity.target_temperature == 17
assert entity.preset_mode is PRESET_COMFORT
# 2. Change the target temp of underlying thermostat at 11 sec later -> the event will be taken
# Wait 11 sec
event_timestamp = now + timedelta(seconds=11)
assert entity.is_regulated is False
await send_climate_change_event_with_temperature(
entity,
HVACMode.HEAT,
HVACMode.HEAT,
HVACAction.OFF,
HVACAction.OFF,
event_timestamp,
12.75,
)
assert entity.target_temperature == 12.75
assert entity.preset_mode is PRESET_NONE
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_bug_272(
@@ -606,18 +160,19 @@ async def test_bug_272(
), patch(
"homeassistant.core.ServiceRegistry.async_call"
) as mock_service_call:
entry.add_to_hass(hass)
await hass.config_entries.async_setup(entry.entry_id)
assert entry.state is ConfigEntryState.LOADED
def find_my_entity(entity_id) -> ClimateEntity:
"""Find my new entity"""
component: EntityComponent[ClimateEntity] = hass.data[CLIMATE_DOMAIN]
for entity in component.entities:
if entity.entity_id == entity_id:
return entity
entity = find_my_entity("climate.theoverclimatemockname")
entity = await create_thermostat(hass, entry, "climate.theoverclimatemockname")
# entry.add_to_hass(hass)
# await hass.config_entries.async_setup(entry.entry_id)
# assert entry.state is ConfigEntryState.LOADED
#
# def find_my_entity(entity_id) -> ClimateEntity:
# """Find my new entity"""
# component: EntityComponent[ClimateEntity] = hass.data[CLIMATE_DOMAIN]
# for entity in component.entities:
# if entity.entity_id == entity_id:
# return entity
#
# entity = find_my_entity("climate.theoverclimatemockname")
assert entity
@@ -652,8 +207,8 @@ async def test_bug_272(
{
"entity_id": "climate.mock_climate",
"temperature": 17.5,
"target_temp_high": 30,
"target_temp_low": 15,
# "target_temp_high": 30,
# "target_temp_low": 15,
},
),
]
@@ -682,8 +237,8 @@ async def test_bug_272(
{
"entity_id": "climate.mock_climate",
"temperature": 15, # the minimum acceptable
"target_temp_high": 30,
"target_temp_low": 15,
# "target_temp_high": 30,
# "target_temp_low": 15,
},
),
]
@@ -709,9 +264,352 @@ async def test_bug_272(
{
"entity_id": "climate.mock_climate",
"temperature": 19, # the maximum acceptable
"target_temp_high": 30,
"target_temp_low": 15,
# "target_temp_high": 30,
# "target_temp_low": 15,
},
),
]
)
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_bug_407(hass: HomeAssistant, skip_hass_states_is_state):
"""Test the followin case in power management:
1. a heater is active (heating). So the power consumption takes the heater power into account. We suppose the power consumption is near the threshold,
2. the user switch preset let's say from Comfort to Boost,
3. expected: no shredding should occur because the heater was already active,
4. constated: the heater goes into shredding.
"""
entry = MockConfigEntry(
domain=DOMAIN,
title="TheOverSwitchMockName",
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
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_bug_500_1(hass: HomeAssistant, init_vtherm_api) -> None:
"""Test that the form is served with no input"""
config = {
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_SWITCH,
CONF_USE_WINDOW_CENTRAL_CONFIG: True,
CONF_USE_POWER_CENTRAL_CONFIG: True,
CONF_USE_PRESENCE_CENTRAL_CONFIG: True,
CONF_USE_MOTION_FEATURE: True,
CONF_MOTION_SENSOR: "sensor.theMotionSensor",
}
flow = VersatileThermostatBaseConfigFlow(config)
assert flow._infos[CONF_USE_WINDOW_FEATURE] is True
assert flow._infos[CONF_USE_POWER_FEATURE] is True
assert flow._infos[CONF_USE_PRESENCE_FEATURE] is True
assert flow._infos[CONF_USE_MOTION_FEATURE] is True
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_bug_500_2(hass: HomeAssistant, init_vtherm_api) -> None:
"""Test that the form is served with no input"""
config = {
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_SWITCH,
CONF_USE_WINDOW_CENTRAL_CONFIG: False,
CONF_USE_POWER_CENTRAL_CONFIG: False,
CONF_USE_PRESENCE_CENTRAL_CONFIG: False,
CONF_USE_MOTION_FEATURE: False,
}
flow = VersatileThermostatBaseConfigFlow(config)
assert flow._infos[CONF_USE_WINDOW_FEATURE] is False
assert flow._infos[CONF_USE_POWER_FEATURE] is False
assert flow._infos[CONF_USE_PRESENCE_FEATURE] is False
assert flow._infos[CONF_USE_MOTION_FEATURE] is False
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_bug_500_3(hass: HomeAssistant, init_vtherm_api) -> None:
"""Test that the form is served with no input"""
config = {
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_SWITCH,
CONF_USE_WINDOW_CENTRAL_CONFIG: False,
CONF_WINDOW_SENSOR: "sensor.theWindowSensor",
CONF_USE_POWER_CENTRAL_CONFIG: False,
CONF_POWER_SENSOR: "sensor.thePowerSensor",
CONF_MAX_POWER_SENSOR: "sensor.theMaxPowerSensor",
CONF_USE_PRESENCE_CENTRAL_CONFIG: False,
CONF_PRESENCE_SENSOR: "sensor.thePresenceSensor",
CONF_USE_MOTION_FEATURE: True, # motion sensor need to be checked AND a motion sensor set
CONF_MOTION_SENSOR: "sensor.theMotionSensor",
}
flow = VersatileThermostatBaseConfigFlow(config)
assert flow._infos[CONF_USE_WINDOW_FEATURE] is True
assert flow._infos[CONF_USE_POWER_FEATURE] is True
assert flow._infos[CONF_USE_PRESENCE_FEATURE] is True
assert flow._infos[CONF_USE_MOTION_FEATURE] is True
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_bug_465(hass: HomeAssistant, skip_hass_states_is_state):
"""Test store and restore hvac_mode on toggle hvac state"""
# vtherm_api: VersatileThermostatAPI = VersatileThermostatAPI.get_vtherm_api(hass)
# The temperatures to set
temps = {
"frost": 7.0,
"eco": 17.0,
"comfort": 19.0,
"boost": 21.0,
"eco_ac": 27.0,
"comfort_ac": 25.0,
"boost_ac": 23.0,
"frost_away": 7.1,
"eco_away": 17.1,
"comfort_away": 19.1,
"boost_away": 21.1,
"eco_ac_away": 27.1,
"comfort_ac_away": 25.1,
"boost_ac_away": 23.1,
}
# 0. initialisation
config_entry = MockConfigEntry(
domain=DOMAIN,
title="TheOverClimateMockName",
unique_id="overClimateUniqueId",
data={
CONF_NAME: "overClimate",
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_CLIMATE,
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
CONF_CYCLE_MIN: 5,
CONF_TEMP_MIN: 15,
CONF_TEMP_MAX: 30,
CONF_USE_WINDOW_FEATURE: True,
CONF_WINDOW_SENSOR: "binary_sensor.window_sensor",
CONF_WINDOW_ACTION: CONF_WINDOW_TURN_OFF,
CONF_WINDOW_DELAY: 1,
CONF_USE_MOTION_FEATURE: False,
CONF_USE_POWER_FEATURE: False,
CONF_USE_PRESENCE_FEATURE: True,
CONF_PRESENCE_SENSOR: "binary_sensor.presence_sensor",
CONF_CLIMATE: "climate.mock_climate",
CONF_MINIMAL_ACTIVATION_DELAY: 30,
CONF_SECURITY_DELAY_MIN: 5,
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
CONF_AUTO_FAN_MODE: CONF_AUTO_FAN_TURBO,
CONF_AC_MODE: True,
},
)
fake_underlying_climate = MockClimate(
hass=hass,
unique_id="mock_climate",
name="mock_climate",
hvac_modes=[HVACMode.OFF, HVACMode.COOL, HVACMode.HEAT, HVACMode.FAN_ONLY],
)
with patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
return_value=fake_underlying_climate,
):
vtherm: ThermostatOverClimate = await create_thermostat(
hass, config_entry, "climate.overclimate"
)
assert vtherm is not None
await set_all_climate_preset_temp(hass, vtherm, temps, "overclimate")
now: datetime = datetime.now(tz=get_tz(hass))
# 1. Set mode to Heat and preset to Comfort
await send_presence_change_event(vtherm, True, False, datetime.now())
await vtherm.async_set_hvac_mode(HVACMode.HEAT)
await vtherm.async_set_preset_mode(PRESET_BOOST)
await hass.async_block_till_done()
assert vtherm.target_temperature == 21.0
# 2. Toggle the VTherm state
await vtherm.async_toggle()
await hass.async_block_till_done()
assert vtherm.hvac_mode == HVACMode.OFF
# 3. (re)Toggle the VTherm state
await vtherm.async_toggle()
await hass.async_block_till_done()
assert vtherm.hvac_mode == HVACMode.HEAT
# 4. Toggle from COOL
await vtherm.async_set_hvac_mode(HVACMode.COOL)
await hass.async_block_till_done()
assert vtherm.target_temperature == 23.0
# 5. Toggle the VTherm state
await vtherm.async_toggle()
await hass.async_block_till_done()
assert vtherm.hvac_mode == HVACMode.OFF
# 6. (re)Toggle the VTherm state
await vtherm.async_toggle()
await hass.async_block_till_done()
assert vtherm.hvac_mode == HVACMode.COOL
###
# Same test with an open window and initial state is COOL
#
# 7. open the window
with patch("homeassistant.helpers.condition.state", return_value=True):
try_window_condition = await send_window_change_event(
vtherm, True, False, now, False
)
await try_window_condition(None)
await hass.async_block_till_done()
assert vtherm.window_state is STATE_ON
assert vtherm.hvac_mode == HVACMode.OFF
# 8. call toggle -> we should stay in OFF (command is ignored)
await vtherm.async_toggle()
await hass.async_block_till_done()
assert vtherm.hvac_mode == HVACMode.OFF
# 9. Close the window (we should come back to Cool this time)
now = now + timedelta(minutes=2)
with patch("homeassistant.helpers.condition.state", return_value=True):
try_window_condition = await send_window_change_event(
vtherm, False, True, now, False
)
await try_window_condition(None)
await hass.async_block_till_done()
assert vtherm.window_state is STATE_OFF
assert vtherm.hvac_mode == HVACMode.COOL
# 9. call toggle -> we should come back in OFF
await vtherm.async_toggle()
await hass.async_block_till_done()
assert vtherm.hvac_mode == HVACMode.OFF

View File

@@ -69,7 +69,7 @@ async def test_add_a_central_config_with_boiler(
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 is 1 # the default value is 1
assert api.nb_active_device_for_boiler_threshold == 1 # the default value is 1
async def test_update_central_boiler_state_simple(
@@ -156,23 +156,29 @@ async def test_update_central_boiler_state_simple(
await switch1.async_turn_on()
switch1.async_write_ha_state()
# Wait for state event propagation
await asyncio.sleep(0.1)
await asyncio.sleep(1)
assert entity.hvac_action == HVACAction.HEATING
assert mock_service_call.call_count >= 1
assert mock_service_call.call_count == 2
# Sometimes this test fails
# mock_service_call.assert_has_calls(
# [
# call.service_call(
# "switch",
# "turn_on",
# service_data={},
# target={"entity_id": "switch.pompe_chaudiere"},
# ),
# ]
# )
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(
@@ -725,7 +731,7 @@ async def test_update_central_boiler_state_simple_climate(
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
return_value=climate1,
):
entity: ThermostatOverValve = await create_thermostat(
entity: ThermostatOverClimate = await create_thermostat(
hass, entry, "climate.theoverclimatemockname"
)
assert entity
@@ -836,3 +842,80 @@ async def test_update_central_boiler_state_simple_climate(
assert boiler_binary_sensor.state == STATE_OFF
entity.remove_thermostat()
async def test_bug_339(
hass: HomeAssistant,
# skip_hass_states_is_state,
init_central_config_with_boiler_fixture,
):
"""Test that the counter of active Vtherm in central boiler is
correctly updated with underlying is in auto and device is active
"""
api = VersatileThermostatAPI.get_vtherm_api(hass)
climate1 = MockClimate(
hass=hass,
unique_id="climate1",
name="theClimate1",
hvac_mode=HVACMode.AUTO,
hvac_modes=[HVACMode.AUTO, HVACMode.OFF, HVACMode.HEAT, HVACMode.COOL],
hvac_action=HVACAction.HEATING,
)
entry = MockConfigEntry(
domain=DOMAIN,
title="TheOverClimateMockName",
unique_id="uniqueId",
data={
CONF_NAME: "TheOverClimateMockName",
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_CLIMATE,
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
CONF_CYCLE_MIN: 5,
CONF_TEMP_MIN: 8,
CONF_TEMP_MAX: 18,
"frost_temp": 10,
"eco_temp": 17,
"comfort_temp": 18,
"boost_temp": 21,
CONF_USE_WINDOW_FEATURE: False,
CONF_USE_MOTION_FEATURE: False,
CONF_USE_POWER_FEATURE: False,
CONF_USE_PRESENCE_FEATURE: False,
CONF_CLIMATE: climate1.entity_id,
CONF_MINIMAL_ACTIVATION_DELAY: 30,
CONF_SECURITY_DELAY_MIN: 5,
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
CONF_SECURITY_DEFAULT_ON_PERCENT: 0.1,
CONF_USE_MAIN_CENTRAL_CONFIG: True,
CONF_USE_PRESETS_CENTRAL_CONFIG: True,
CONF_USE_ADVANCED_CENTRAL_CONFIG: True,
CONF_USED_BY_CENTRAL_BOILER: True,
},
)
with patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
return_value=climate1,
):
entity: ThermostatOverValve = await create_thermostat(
hass, entry, "climate.theoverclimatemockname"
)
assert entity
assert entity.name == "TheOverClimateMockName"
assert entity.is_over_climate
assert entity.underlying_entities[0].entity_id == "climate.climate1"
assert api.nb_active_device_for_boiler_threshold == 1
await entity.async_set_hvac_mode(HVACMode.AUTO)
# Simulate a state change in underelying
await api.nb_active_device_for_boiler_entity.calculate_nb_active_devices(None)
# The VTherm should be active
assert entity.underlying_entity(0).is_device_active is True
assert entity.is_device_active is True
assert api.nb_active_device_for_boiler == 1
entity.remove_thermostat()

View File

@@ -4,23 +4,13 @@
from unittest.mock import patch # , call
# from datetime import datetime # , timedelta
from homeassistant import data_entry_flow
from homeassistant.data_entry_flow import FlowResultType
from homeassistant.core import HomeAssistant
# from homeassistant.components.climate import HVACAction, HVACMode
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 homeassistant.config_entries import SOURCE_USER
from pytest_homeassistant_custom_component.common import MockConfigEntry
# from custom_components.versatile_thermostat.base_thermostat import BaseThermostat
from custom_components.versatile_thermostat.thermostat_climate import (
ThermostatOverClimate,
)
from custom_components.versatile_thermostat.thermostat_switch import (
ThermostatOverSwitch,
)
@@ -31,8 +21,8 @@ from .commons import * # pylint: disable=wildcard-import, unused-wildcard-impor
from .const import * # pylint: disable=wildcard-import, unused-wildcard-import
# @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_add_a_central_config(hass: HomeAssistant, skip_hass_states_is_state):
"""Tests the clean_central_config_doubon of base_thermostat"""
central_config_entry = MockConfigEntry(
@@ -76,17 +66,20 @@ async def test_add_a_central_config(hass: HomeAssistant, skip_hass_states_is_sta
CONF_SECURITY_DELAY_MIN: 61,
CONF_SECURITY_MIN_ON_PERCENT: 0.5,
CONF_SECURITY_DEFAULT_ON_PERCENT: 0.2,
CONF_ADD_CENTRAL_BOILER_CONTROL: False,
CONF_USE_CENTRAL_BOILER_FEATURE: False,
},
)
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"
entity = await create_thermostat(
hass, central_config_entry, "climate.thecentralconfigmockname"
)
# central_config_entry.add_to_hass(hass)
# await hass.config_entries.async_setup(central_config_entry.entry_id)
# assert central_config_entry.state is ConfigEntryState.LOADED
#
# entity: ThermostatOverClimate = search_entity(
# hass, "climate.thecentralconfigmockname", "climate"
# )
assert entity is None
@@ -185,8 +178,8 @@ async def test_minimal_over_switch_wo_central_config(
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_full_over_switch_wo_central_config(
hass: HomeAssistant, skip_hass_states_is_state, init_vtherm_api
):
@@ -303,8 +296,8 @@ async def test_full_over_switch_wo_central_config(
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_full_over_switch_with_central_config(
hass: HomeAssistant, skip_hass_states_is_state, init_central_config
):
@@ -437,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["errors"] == {}
@@ -448,15 +447,71 @@ async def test_over_switch_with_central_config_but_no_central_config(
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
CONF_CYCLE_MIN: 5,
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,
},
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["type"] == FlowResultType.FORM
# in case of error we stays in main
assert result["step_id"] == "main"
assert result["errors"] == {"use_main_central_config": "no_central_config"}
async def test_migration_of_central_config(
hass: HomeAssistant,
skip_hass_states_is_state,
):
"""Tests the clean_central_config_doubon of base_thermostat"""
central_config_entry = MockConfigEntry(
version=CONFIG_VERSION,
# An old minor version
minor_version=1,
domain=DOMAIN,
title="TheCentralConfigMockName",
unique_id="centralConfigUniqueId",
data={
CONF_NAME: CENTRAL_CONFIG_NAME,
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_CENTRAL_CONFIG,
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
CONF_TEMP_MIN: 15,
CONF_TEMP_MAX: 30,
CONF_STEP_TEMPERATURE: 0.1,
CONF_TPI_COEF_INT: 0.5,
CONF_TPI_COEF_EXT: 0.02,
CONF_MINIMAL_ACTIVATION_DELAY: 11,
CONF_SECURITY_DELAY_MIN: 61,
CONF_SECURITY_MIN_ON_PERCENT: 0.5,
CONF_SECURITY_DEFAULT_ON_PERCENT: 0.2,
# The old central_boiler parameter
"add_central_boiler_control": True,
CONF_CENTRAL_BOILER_ACTIVATION_SRV: "switch.pompe_chaudiere/switch.turn_on",
CONF_CENTRAL_BOILER_DEACTIVATION_SRV: "switch.pompe_chaudiere/switch.turn_off",
},
)
central_config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(central_config_entry.entry_id)
assert central_config_entry.state is ConfigEntryState.LOADED
entity: ThermostatOverClimate = search_entity(
hass, "climate.thecentralconfigmockname", "climate"
)
assert entity is None
assert count_entities(hass, "climate.thecentralconfigmockname", "climate") == 0
# Test that VTherm API find the CentralConfig
api = VersatileThermostatAPI.get_vtherm_api(hass)
central_configuration = api.find_central_configuration()
assert central_configuration is not None
assert central_config_entry.data.get(CONF_USE_CENTRAL_BOILER_FEATURE) is True
# Test that VTherm API have any central boiler entities
# It should have been migrated and initialized
assert api.nb_active_device_for_boiler_entity is not None
assert api.nb_active_device_for_boiler == 0
assert api.nb_active_device_for_boiler_threshold_entity is not None
assert api.nb_active_device_for_boiler_threshold == 1 # the default value is 1

View File

@@ -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_timers", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_config_with_central_mode_true(
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
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_switch_change_central_mode_true(
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
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_switch_ac_change_central_mode_true(
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
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_climate_ac_change_central_mode_false(
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
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_climate_ac_only_change_central_mode_true(
hass: HomeAssistant, skip_hass_states_is_state, init_central_config
):
@@ -622,6 +630,7 @@ async def test_climate_ac_only_change_central_mode_true(
},
)
# 1. set hvac_mode to COOL and preet ECO
with patch("homeassistant.core.ServiceRegistry.async_call"), patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
return_value=fake_underlying_climate,
@@ -734,6 +743,8 @@ async def test_climate_ac_only_change_central_mode_true(
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
):
@@ -889,6 +900,8 @@ async def test_switch_change_central_mode_true_with_window(
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
):
@@ -970,7 +983,8 @@ async def test_switch_change_central_mode_true_with_cool_only_and_window(
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)
assert entity.hvac_mode is HVACMode.OFF
assert entity.hvac_off_reason == HVAC_OFF_REASON_MANUAL
await entity.async_set_preset_mode(PRESET_ACTIVITY)
assert entity._saved_hvac_mode == HVACMode.HEAT
assert entity._saved_preset_mode == PRESET_ACTIVITY
@@ -988,12 +1002,14 @@ async def test_switch_change_central_mode_true_with_cool_only_and_window(
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})]
)
# The VTherm is already off -> window detection is ignored
assert mock_send_event.call_count == 0
# 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.hvac_off_reason == HVAC_OFF_REASON_MANUAL
assert entity.preset_mode == PRESET_ACTIVITY
assert entity._saved_hvac_mode == HVACMode.HEAT
assert entity._saved_preset_mode == PRESET_ACTIVITY
@@ -1009,6 +1025,8 @@ async def test_switch_change_central_mode_true_with_cool_only_and_window(
assert entity.last_central_mode is CENTRAL_MODE_AUTO
# No change
assert entity.hvac_mode == HVACMode.OFF
# We have to a reason of WINDOW_DETECTION
assert entity.hvac_off_reason == HVAC_OFF_REASON_WINDOW_DETECTION
assert entity.preset_mode == PRESET_ACTIVITY
assert entity._saved_hvac_mode == HVACMode.HEAT
assert entity._saved_preset_mode == PRESET_ACTIVITY
@@ -1034,6 +1052,7 @@ async def test_switch_change_central_mode_true_with_cool_only_and_window(
# We should stay off because central is STOPPED
assert entity.hvac_mode == HVACMode.HEAT
assert entity.hvac_off_reason is None
assert entity.preset_mode == PRESET_ACTIVITY
assert entity._saved_hvac_mode == HVACMode.HEAT
assert entity._saved_preset_mode == PRESET_ACTIVITY

File diff suppressed because it is too large Load Diff

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_presence_change_event(entity, True, False, event_timestamp)
assert entity.presence_state is "on"
assert entity.presence_state == "on"
# starts detecting motion with time not enough
with patch(
@@ -97,7 +97,12 @@ async def test_movement_management_time_not_enough(
return_value=False,
), patch(
"homeassistant.helpers.condition.state", return_value=False
) as mock_condition:
) as mock_condition, patch(
"homeassistant.core.StateMachine.get",
return_value=State(
entity_id="binary_sensor.mock_motion_sensor", state=STATE_OFF
),
):
event_timestamp = now - timedelta(minutes=4)
try_condition = await send_motion_change_event(entity, True, False, event_timestamp)
@@ -109,8 +114,8 @@ async def test_movement_management_time_not_enough(
# because no motion is detected yet
assert entity.target_temperature == 18
# state is not changed if time is not enough
assert entity.motion_state is None
assert entity.presence_state is "on"
assert entity.motion_state is STATE_OFF
assert entity.presence_state == STATE_ON
assert mock_send_event.call_count == 0
# Change is not confirmed
@@ -141,8 +146,8 @@ async def test_movement_management_time_not_enough(
assert entity.preset_mode is PRESET_ACTIVITY
# because motion is detected yet
assert entity.target_temperature == 19
assert entity.motion_state is "on"
assert entity.presence_state is "on"
assert entity.motion_state == STATE_ON
assert entity.presence_state == STATE_ON
# stop detecting motion with off delay too low
with patch(
@@ -156,19 +161,24 @@ async def test_movement_management_time_not_enough(
return_value=True,
) as mock_device_active, patch(
"homeassistant.helpers.condition.state", return_value=False
) as mock_condition:
) as mock_condition, patch(
"homeassistant.core.StateMachine.get",
return_value=State(
entity_id="binary_sensor.mock_motion_sensor", state=STATE_OFF
),
):
event_timestamp = now - timedelta(minutes=2)
try_condition = await send_motion_change_event(entity, False, True, event_timestamp)
# Will return False -> we will stay to movement On
# Will return False -> we will stay to movement On
await try_condition(None)
assert entity.hvac_mode is HVACMode.HEAT
assert entity.preset_mode is PRESET_ACTIVITY
# because no motion is detected yet
assert entity.target_temperature == 19
assert entity.motion_state is "on"
assert entity.presence_state is "on"
assert entity.motion_state == STATE_ON
assert entity.presence_state == STATE_ON
assert mock_send_event.call_count == 0
# The heater must heat now
@@ -192,15 +202,15 @@ async def test_movement_management_time_not_enough(
event_timestamp = now - timedelta(minutes=1)
try_condition = await send_motion_change_event(entity, False, True, event_timestamp)
# Will return True -> we will switch to movement Off
# Will return True -> we will switch to movement Off
await try_condition(None)
assert entity.hvac_mode is HVACMode.HEAT
assert entity.preset_mode is PRESET_ACTIVITY
# because no motion is detected yet
assert entity.target_temperature == 18
assert entity.motion_state is "off"
assert entity.presence_state is "on"
assert entity.motion_state == STATE_OFF
assert entity.presence_state == STATE_ON
assert mock_send_event.call_count == 0
# The heater must stop heating now
@@ -214,7 +224,7 @@ async def test_movement_management_time_not_enough(
async def test_movement_management_time_enough_and_presence(
hass: HomeAssistant, skip_hass_states_is_state
):
"""Test the Presence management when time is not enough"""
"""Test the Motion management when time is not enough"""
entry = MockConfigEntry(
domain=DOMAIN,
@@ -280,7 +290,7 @@ async def test_movement_management_time_enough_and_presence(
await send_ext_temperature_change_event(entity, 10, event_timestamp)
await send_presence_change_event(entity, True, False, event_timestamp)
assert entity.presence_state is "on"
assert entity.presence_state == "on"
# starts detecting motion
with patch(
@@ -302,8 +312,8 @@ async def test_movement_management_time_enough_and_presence(
assert entity.preset_mode is PRESET_ACTIVITY
# because motion is detected yet -> switch to Boost mode
assert entity.target_temperature == 19
assert entity.motion_state is "on"
assert entity.presence_state is "on"
assert entity.motion_state == "on"
assert entity.presence_state == "on"
assert mock_send_event.call_count == 0
# Change is confirmed. Heater should be started
@@ -331,8 +341,8 @@ async def test_movement_management_time_enough_and_presence(
assert entity.preset_mode is PRESET_ACTIVITY
# because no motion is detected yet
assert entity.target_temperature == 18
assert entity.motion_state is "off"
assert entity.presence_state is "on"
assert entity.motion_state == "off"
assert entity.presence_state == "on"
assert mock_send_event.call_count == 0
assert mock_heater_on.call_count == 0
@@ -412,7 +422,7 @@ async def test_movement_management_time_enoughand_not_presence(
await send_ext_temperature_change_event(entity, 10, event_timestamp)
await send_presence_change_event(entity, False, True, event_timestamp)
assert entity.presence_state is "off"
assert entity.presence_state == "off"
# starts detecting motion
with patch(
@@ -434,8 +444,8 @@ async def test_movement_management_time_enoughand_not_presence(
assert entity.preset_mode is PRESET_ACTIVITY
# because motion is detected yet -> switch to Boost away mode
assert entity.target_temperature == 19.1
assert entity.motion_state is "on"
assert entity.presence_state is "off"
assert entity.motion_state == "on"
assert entity.presence_state == "off"
assert mock_send_event.call_count == 0
# Change is confirmed. Heater should be started
@@ -463,8 +473,8 @@ async def test_movement_management_time_enoughand_not_presence(
assert entity.preset_mode is PRESET_ACTIVITY
# because no motion is detected yet
assert entity.target_temperature == 18.1
assert entity.motion_state is "off"
assert entity.presence_state is "off"
assert entity.motion_state == "off"
assert entity.presence_state == "off"
assert mock_send_event.call_count == 0
# 18.1 starts heating with a low on_percent
@@ -479,7 +489,7 @@ async def test_movement_management_time_enoughand_not_presence(
async def test_movement_management_with_stop_during_condition(
hass: HomeAssistant, skip_hass_states_is_state
):
"""Test the Presence management when the movement sensor switch to off and then to on during the test condition"""
"""Test the Motion management when the movement sensor switch to off and then to on during the test condition"""
entry = MockConfigEntry(
domain=DOMAIN,
@@ -546,7 +556,7 @@ async def test_movement_management_with_stop_during_condition(
await send_ext_temperature_change_event(entity, 10, event_timestamp)
await send_presence_change_event(entity, False, True, event_timestamp)
assert entity.presence_state is "off"
assert entity.presence_state == "off"
# starts detecting motion
with patch(
@@ -558,9 +568,13 @@ async def test_movement_management_with_stop_during_condition(
) as mock_heater_off, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
return_value=True,
), patch("homeassistant.helpers.condition.state", return_value=True): # Not needed for this test
), patch(
"homeassistant.helpers.condition.state", return_value=True
): # Not needed for this test
event_timestamp = now - timedelta(minutes=5)
try_condition1 = await send_motion_change_event(entity, True, False, event_timestamp)
try_condition1 = await send_motion_change_event(
entity, True, False, event_timestamp
)
assert try_condition1 is not None
@@ -569,33 +583,155 @@ async def test_movement_management_with_stop_during_condition(
# because motion is detected yet -> switch to Boost mode
assert entity.target_temperature == 18
assert entity.motion_state is None
assert entity.presence_state is "off"
assert entity.presence_state == "off"
# Send a stop detection
event_timestamp = now - timedelta(minutes=4)
try_condition = await send_motion_change_event(entity, False, True, event_timestamp)
assert try_condition is None # The timer should not have been stopped
try_condition = await send_motion_change_event(
entity, False, True, event_timestamp
)
assert try_condition is None # The timer should not have been stopped
assert entity.hvac_mode is HVACMode.HEAT
assert entity.preset_mode is PRESET_ACTIVITY
assert entity.target_temperature == 18
assert entity.motion_state is None
assert entity.presence_state is "off"
assert entity.presence_state == "off"
# Resend a start detection
event_timestamp = now - timedelta(minutes=3)
try_condition = await send_motion_change_event(entity, True, False, event_timestamp)
assert try_condition is None # The timer should not have been restarted (we keep the first one)
try_condition = await send_motion_change_event(
entity, True, False, event_timestamp
)
assert (
try_condition is None
) # The timer should not have been restarted (we keep the first one)
assert entity.hvac_mode is HVACMode.HEAT
assert entity.preset_mode is PRESET_ACTIVITY
# still no motion detected
assert entity.target_temperature == 18
assert entity.motion_state is None
assert entity.presence_state is "off"
assert entity.presence_state == "off"
await try_condition1(None)
# We should have switch this time
assert entity.target_temperature == 19 # Boost
assert entity.motion_state is "on" # switch to movement on
assert entity.presence_state is "off" # Non change
assert entity.target_temperature == 19 # Boost
assert entity.motion_state == "on" # switch to movement on
assert entity.presence_state == "off" # Non change
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_movement_management_with_stop_during_condition_last_state_on(
hass: HomeAssistant, skip_hass_states_is_state
):
"""Test the Motion management when the movement sensor switch to off and then to on during the test condition"""
entry = MockConfigEntry(
domain=DOMAIN,
title="TheOverSwitchMockName",
unique_id="uniqueId",
data={
CONF_NAME: "TheOverSwitchMockName",
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_SWITCH,
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
CONF_CYCLE_MIN: 5,
CONF_TEMP_MIN: 15,
CONF_TEMP_MAX: 30,
"eco_temp": 17,
"comfort_temp": 18,
"boost_temp": 19,
"eco_away_temp": 17,
"comfort_away_temp": 18,
"boost_away_temp": 19,
CONF_USE_WINDOW_FEATURE: False,
CONF_USE_MOTION_FEATURE: True,
CONF_USE_POWER_FEATURE: False,
CONF_USE_PRESENCE_FEATURE: False,
CONF_HEATER: "switch.mock_switch",
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
CONF_TPI_COEF_INT: 0.3,
CONF_TPI_COEF_EXT: 0.01,
CONF_MINIMAL_ACTIVATION_DELAY: 30,
CONF_SECURITY_DELAY_MIN: 5,
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
CONF_MOTION_SENSOR: "binary_sensor.mock_motion_sensor",
CONF_MOTION_DELAY: 10,
CONF_MOTION_OFF_DELAY: 30,
CONF_MOTION_PRESET: "boost",
CONF_NO_MOTION_PRESET: "comfort",
},
)
entity: BaseThermostat = await create_thermostat(
hass, entry, "climate.theoverswitchmockname"
)
assert entity
tz = get_tz(hass) # pylint: disable=invalid-name
now: datetime = datetime.now(tz=tz)
# 0. start heating, in boost mode. We block the control_heating to avoid running a cycle
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.async_control_heating"
):
await entity.async_set_hvac_mode(HVACMode.HEAT)
await entity.async_set_preset_mode(PRESET_ACTIVITY)
assert entity.hvac_mode is HVACMode.HEAT
assert entity.preset_mode is PRESET_ACTIVITY
# because no motion is detected yet
assert entity.target_temperature == 18
assert entity.motion_state is None
event_timestamp = now - timedelta(minutes=6)
await send_temperature_change_event(entity, 18, event_timestamp)
await send_ext_temperature_change_event(entity, 10, event_timestamp)
# 1. starts detecting motion but the sensor is off
with patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
return_value=True,
), patch("homeassistant.helpers.condition.state", return_value=False), patch(
"homeassistant.core.StateMachine.get",
return_value=State(
entity_id="binary_sensor.mock_motion_sensor", state=STATE_OFF
),
):
event_timestamp = now - timedelta(minutes=5)
try_condition1 = await send_motion_change_event(
entity, True, False, event_timestamp
)
assert try_condition1 is not None
await try_condition1(None)
# because no motion is detected yet -> condition.state is False and sensor is not active
assert entity.target_temperature == 18
assert entity.motion_state is STATE_OFF
# 2. starts detecting motion but the sensor is on
with patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
return_value=True,
), patch("homeassistant.helpers.condition.state", return_value=False), patch(
"homeassistant.core.StateMachine.get",
return_value=State(
entity_id="binary_sensor.mock_motion_sensor", state=STATE_ON
),
):
event_timestamp = now - timedelta(minutes=5)
try_condition1 = await send_motion_change_event(
entity, True, False, event_timestamp
)
assert try_condition1 is not None
await try_condition1(None)
# because no motion is detected yet -> condition.state is False and sensor is not active
assert entity.target_temperature == 19
assert entity.motion_state is STATE_ON

View File

@@ -260,6 +260,7 @@ async def test_multiple_switchs(
CONF_HEATER_2: "switch.mock_switch2",
CONF_HEATER_3: "switch.mock_switch3",
CONF_HEATER_4: "switch.mock_switch4",
CONF_HEATER_KEEP_ALIVE: 0,
CONF_MINIMAL_ACTIVATION_DELAY: 30,
CONF_SECURITY_DELAY_MIN: 5,
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
@@ -595,6 +596,7 @@ async def test_multiple_climates_underlying_changes(
HVACAction.IDLE,
HVACAction.OFF,
event_timestamp,
underlying_entity_id="switch.mock_climate3",
)
# Should be call for all Switch
@@ -746,6 +748,7 @@ async def test_multiple_switch_power_management(
CONF_HEATER_2: "switch.mock_switch2",
CONF_HEATER_3: "switch.mock_switch3",
CONF_HEATER_4: "switch.mock_switch4",
CONF_HEATER_KEEP_ALIVE: 0,
CONF_MINIMAL_ACTIVATION_DELAY: 30,
CONF_SECURITY_DELAY_MIN: 5,
CONF_SECURITY_MIN_ON_PERCENT: 0.3,

1201
tests/test_overclimate.py Normal file

File diff suppressed because it is too large Load Diff

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

View File

@@ -433,6 +433,7 @@ async def test_power_management_energy_over_climate(
new_hvac_action=HVACAction.HEATING,
old_hvac_action=HVACAction.OFF,
date=event_timestamp,
underlying_entity_id="climate.mock_climate",
)
# We have the start event and not the end event
assert (entity._underlying_climate_start_hvac_action_date - now).total_seconds() < 1
@@ -448,6 +449,7 @@ async def test_power_management_energy_over_climate(
new_hvac_action=HVACAction.IDLE,
old_hvac_action=HVACAction.HEATING,
date=now,
underlying_entity_id="climate.mock_climate",
)
# We have the end event -> we should have some power and on_percent
assert entity._underlying_climate_start_hvac_action_date is None

View File

@@ -283,6 +283,7 @@ async def test_sensors_over_climate(
new_hvac_action=HVACAction.HEATING,
old_hvac_action=HVACAction.OFF,
date=event_timestamp,
underlying_entity_id="climate.mock_climate",
)
# Send a climate_change event with HVACAction=IDLE (end of heating)
@@ -293,6 +294,7 @@ async def test_sensors_over_climate(
new_hvac_action=HVACAction.IDLE,
old_hvac_action=HVACAction.HEATING,
date=now,
underlying_entity_id="climate.mock_climate",
)
# 60 minutes heating with 1.5 kW heating -> 1.5 kWh

View File

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

View File

@@ -56,6 +56,23 @@ async def test_over_switch_ac_full_start(
assert entity
assert isinstance(entity, ThermostatOverSwitch)
# Initialise the preset temp
await set_climate_preset_temp(
entity, PRESET_FROST_PROTECTION + PRESET_AWAY_SUFFIX, 7
)
await set_climate_preset_temp(entity, PRESET_ECO + PRESET_AWAY_SUFFIX, 16)
await set_climate_preset_temp(entity, PRESET_COMFORT + PRESET_AWAY_SUFFIX, 17)
await set_climate_preset_temp(entity, PRESET_BOOST + PRESET_AWAY_SUFFIX, 18)
await set_climate_preset_temp(
entity, PRESET_ECO + PRESET_AC_SUFFIX + PRESET_AWAY_SUFFIX, 27
)
await set_climate_preset_temp(
entity, PRESET_COMFORT + PRESET_AC_SUFFIX + PRESET_AWAY_SUFFIX, 26
)
await set_climate_preset_temp(
entity, PRESET_BOOST + PRESET_AC_SUFFIX + PRESET_AWAY_SUFFIX, 25
)
assert entity.name == "TheOverSwitchMockName"
assert entity.is_over_climate is False # pylint: disable=protected-access
assert entity.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 """
from homeassistant.components.climate import HVACMode
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
@@ -42,53 +48,54 @@ async def test_tpi_calculation(
hass, entry, "climate.theoverswitchmockname"
)
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
tpi_algo.calculate(15, 10, 7)
tpi_algo.calculate(15, 10, 7, HVACMode.HEAT)
assert tpi_algo.on_percent == 1
assert tpi_algo.calculated_on_percent == 1
assert tpi_algo.on_time_sec == 300
assert tpi_algo.off_time_sec == 0
assert entity.mean_cycle_power is None # no device power configured
tpi_algo.calculate(15, 14, 5, False)
tpi_algo.calculate(15, 14, 5, HVACMode.HEAT)
assert tpi_algo.on_percent == 0.4
assert tpi_algo.calculated_on_percent == 0.4
assert tpi_algo.on_time_sec == 120
assert tpi_algo.off_time_sec == 180
tpi_algo.set_security(0.1)
tpi_algo.calculate(15, 14, 5, False)
tpi_algo.calculate(15, 14, 5, HVACMode.HEAT)
assert tpi_algo.on_percent == 0.1
assert tpi_algo.calculated_on_percent == 0.4
assert tpi_algo.on_time_sec == 30 # >= minimal_activation_delay (=30)
assert tpi_algo.off_time_sec == 270
tpi_algo.unset_security()
tpi_algo.calculate(15, 14, 5, False)
tpi_algo.calculate(15, 14, 5, HVACMode.HEAT)
assert tpi_algo.on_percent == 0.4
assert tpi_algo.calculated_on_percent == 0.4
assert tpi_algo.on_time_sec == 120
assert tpi_algo.off_time_sec == 180
# Test minimal activation delay
tpi_algo.calculate(15, 14.7, 15, False)
tpi_algo.calculate(15, 14.7, 15, HVACMode.HEAT)
assert tpi_algo.on_percent == 0.09
assert tpi_algo.calculated_on_percent == 0.09
assert tpi_algo.on_time_sec == 0
assert tpi_algo.off_time_sec == 300
tpi_algo.set_security(0.09)
tpi_algo.calculate(15, 14.7, 15, False)
tpi_algo.calculate(15, 14.7, 15, HVACMode.HEAT)
assert tpi_algo.on_percent == 0.09
assert tpi_algo.calculated_on_percent == 0.09
assert tpi_algo.on_time_sec == 0
assert tpi_algo.off_time_sec == 300
tpi_algo.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.calculated_on_percent == 1
assert tpi_algo.on_time_sec == 300
@@ -96,9 +103,177 @@ async def test_tpi_calculation(
assert entity.mean_cycle_power is None # no device power configured
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.calculated_on_percent == 1
assert tpi_algo.on_time_sec == 0
assert tpi_algo.off_time_sec == 300
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
"""
Test the max_on_percent clamping calculations
"""
tpi_algo._max_on_percent = 0.8
# no clamping
tpi_algo.calculate(15, 14.7, 15, HVACMode.HEAT)
assert tpi_algo.on_percent == 0.09
assert tpi_algo.calculated_on_percent == 0.09
assert tpi_algo.on_time_sec == 0
assert tpi_algo.off_time_sec == 300
# no clamping (calculated_on_percent = 0.79)
tpi_algo.calculate(15, 12.5, 11, HVACMode.HEAT)
assert tpi_algo.on_percent == 0.79
assert tpi_algo.calculated_on_percent == 0.79
assert tpi_algo.on_time_sec == 237
assert tpi_algo.off_time_sec == 63
# clamping to 80% (calculated_on_percent = 1)
tpi_algo.calculate(15, 10, 7, HVACMode.HEAT)
assert tpi_algo.on_percent == 0.8 # should be clamped to 80%
assert tpi_algo.calculated_on_percent == 1 # calculated percentage should not be affected by clamping
assert tpi_algo.on_time_sec == 240 # capped at 80%
assert tpi_algo.off_time_sec == 60
# clamping to 80% (calculated_on_percent = 0.81)
tpi_algo.calculate(15, 12.5, 9, HVACMode.HEAT)
assert tpi_algo.on_percent == 0.80 # should be clamped to 80%
assert tpi_algo.calculated_on_percent == 0.81 # calculated percentage should not be affected by clamping
assert tpi_algo.on_time_sec == 240 # capped at 80%
assert tpi_algo.off_time_sec == 60
@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

@@ -6,10 +6,6 @@ from datetime import datetime, timedelta
from homeassistant.core import HomeAssistant, State
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
@@ -37,10 +33,10 @@ async def test_over_valve_full_start(
CONF_CYCLE_MIN: 5,
CONF_TEMP_MIN: 15,
CONF_TEMP_MAX: 30,
PRESET_FROST_PROTECTION + "_temp": 7,
PRESET_ECO + "_temp": 17,
PRESET_COMFORT + "_temp": 19,
PRESET_BOOST + "_temp": 21,
PRESET_FROST_PROTECTION + PRESET_TEMP_SUFFIX: 7,
PRESET_ECO + PRESET_TEMP_SUFFIX: 17,
PRESET_COMFORT + PRESET_TEMP_SUFFIX: 19,
PRESET_BOOST + PRESET_TEMP_SUFFIX: 21,
CONF_USE_WINDOW_FEATURE: True,
CONF_USE_MOTION_FEATURE: True,
CONF_USE_POWER_FEATURE: True,
@@ -58,10 +54,10 @@ async def test_over_valve_full_start(
CONF_POWER_SENSOR: "sensor.power_sensor",
CONF_MAX_POWER_SENSOR: "sensor.power_max_sensor",
CONF_PRESENCE_SENSOR: "person.presence_sensor",
PRESET_FROST_PROTECTION + PRESET_AWAY_SUFFIX + "_temp": 7,
PRESET_ECO + PRESET_AWAY_SUFFIX + "_temp": 17.1,
PRESET_COMFORT + PRESET_AWAY_SUFFIX + "_temp": 17.2,
PRESET_BOOST + PRESET_AWAY_SUFFIX + "_temp": 17.3,
PRESET_FROST_PROTECTION + PRESET_AWAY_SUFFIX + PRESET_TEMP_SUFFIX: 7,
PRESET_ECO + PRESET_AWAY_SUFFIX + PRESET_TEMP_SUFFIX: 17.1,
PRESET_COMFORT + PRESET_AWAY_SUFFIX + PRESET_TEMP_SUFFIX: 17.2,
PRESET_BOOST + PRESET_AWAY_SUFFIX + PRESET_TEMP_SUFFIX: 17.3,
CONF_PRESET_POWER: 10,
CONF_MINIMAL_ACTIVATION_DELAY: 30,
CONF_SECURITY_DELAY_MIN: 5,
@@ -74,22 +70,12 @@ async def test_over_valve_full_start(
tz = get_tz(hass) # pylint: disable=invalid-name
now: datetime = datetime.now(tz=tz)
# 1. create the entity
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event:
entry.add_to_hass(hass)
await hass.config_entries.async_setup(entry.entry_id)
assert entry.state is ConfigEntryState.LOADED
def find_my_entity(entity_id) -> ClimateEntity:
"""Find my new entity"""
component: EntityComponent[ClimateEntity] = hass.data[CLIMATE_DOMAIN]
for entity in component.entities:
if entity.entity_id == entity_id:
return entity
# The name is in the CONF and not the title of the entry
entity: ThermostatOverValve = find_my_entity("climate.theovervalvemockname")
entity = await create_thermostat(hass, entry, "climate.theovervalvemockname")
assert entity
assert isinstance(entity, ThermostatOverValve)
@@ -119,7 +105,7 @@ async def test_over_valve_full_start(
assert entity._prop_algorithm is not None # pylint: disable=protected-access
# should have been called with EventType.PRESET_EVENT and EventType.HVAC_MODE_EVENT
assert mock_send_event.call_count == 2
# assert mock_send_event.call_count == 2
mock_send_event.assert_has_calls(
[
call.send_event(EventType.PRESET_EVENT, {"preset": PRESET_NONE}),
@@ -130,7 +116,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(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event:
@@ -158,7 +144,7 @@ async def test_over_valve_full_start(
# Nothing have changed cause we don't have room and external temperature
assert mock_send_event.call_count == 1
# Set temperature and external temperature
# 3. Set temperature and external temperature
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event, patch(
@@ -181,14 +167,18 @@ async def test_over_valve_full_start(
mock_service_call.assert_has_calls(
[
call.async_call(
"number",
"set_value",
{"entity_id": "number.mock_valve", "value": 90},
domain="number",
service="set_value",
service_data={"value": 90},
target={"entity_id": "number.mock_valve"},
# {"entity_id": "number.mock_valve", "value": 90},
),
call.async_call(
"number",
"set_value",
{"entity_id": "number.mock_valve", "value": 98},
domain="number",
service="set_value",
service_data={"value": 98},
target={"entity_id": "number.mock_valve"},
# {"entity_id": "number.mock_valve", "value": 98},
),
]
)
@@ -196,15 +186,18 @@ async def test_over_valve_full_start(
assert mock_send_event.call_count == 0
# 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)
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.is_device_active is True
assert entity.hvac_action == HVACAction.HEATING
# 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)
assert entity.presence_state == STATE_ON # pylint: disable=protected-access
assert entity.preset_mode is PRESET_COMFORT
@@ -213,7 +206,7 @@ async def test_over_valve_full_start(
assert entity.is_device_active is True
assert entity.hvac_action == HVACAction.HEATING
# Change internal temperature
# 4. Change internal temperature
expected_state = State(
entity_id="number.mock_valve", state="0", attributes={"min": 10, "max": 50}
)
@@ -225,7 +218,7 @@ async def test_over_valve_full_start(
) as mock_service_call, patch(
"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())
assert entity.valve_open_percent == 0
assert entity.is_device_active is True # Should be 0 but in fact 10 is send
@@ -238,9 +231,10 @@ async def test_over_valve_full_start(
mock_service_call.assert_has_calls(
[
call.async_call(
"number",
"set_value",
{"entity_id": "number.mock_valve", "value": 10},
domain="number",
service="set_value",
service_data={"value": 10},
target={"entity_id": "number.mock_valve"},
)
]
)
@@ -251,20 +245,18 @@ async def test_over_valve_full_start(
mock_service_call.assert_has_calls(
[
call.async_call(
"number",
"set_value",
{
"entity_id": "number.mock_valve",
"value": 10,
}, # the min allowed value
domain="number",
service="set_value",
service_data={"value": 10},
target={"entity_id": "number.mock_valve"}, # the min allowed value
),
call.async_call(
"number",
"set_value",
{
"entity_id": "number.mock_valve",
"value": 50,
}, # the max allowed value
domain="number",
service="set_value",
service_data={
"value": 34
}, # 34 is 50 x open_percent (69%) and is the max allowed value
target={"entity_id": "number.mock_valve"},
),
]
)
@@ -275,7 +267,7 @@ async def test_over_valve_full_start(
assert entity.valve_open_percent == 7
# 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)
assert entity.presence_state == STATE_OFF # pylint: disable=protected-access
assert entity.valve_open_percent == 10
@@ -283,9 +275,9 @@ async def test_over_valve_full_start(
assert entity.is_device_active is True
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(
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(
@@ -345,10 +337,10 @@ async def test_over_valve_regulation(
CONF_CYCLE_MIN: 5,
CONF_TEMP_MIN: 15,
CONF_TEMP_MAX: 30,
PRESET_FROST_PROTECTION + "_temp": 7,
PRESET_ECO + "_temp": 17,
PRESET_COMFORT + "_temp": 19,
PRESET_BOOST + "_temp": 21,
PRESET_FROST_PROTECTION + PRESET_TEMP_SUFFIX: 7,
PRESET_ECO + PRESET_TEMP_SUFFIX: 17,
PRESET_COMFORT + PRESET_TEMP_SUFFIX: 19,
PRESET_BOOST + PRESET_TEMP_SUFFIX: 21,
CONF_USE_WINDOW_FEATURE: False,
CONF_USE_MOTION_FEATURE: False,
CONF_USE_POWER_FEATURE: False,
@@ -463,9 +455,10 @@ async def test_over_valve_regulation(
mock_service_call.assert_has_calls(
[
call.async_call(
"number",
"set_value",
{"entity_id": "number.mock_valve", "value": 90},
domain="number",
service="set_value",
service_data={"value": 90},
target={"entity_id": "number.mock_valve"},
),
]
)
@@ -521,9 +514,10 @@ async def test_over_valve_regulation(
mock_service_call.assert_has_calls(
[
call.async_call(
"number",
"set_value",
{"entity_id": "number.mock_valve", "value": 96},
domain="number",
service="set_value",
service_data={"value": 96},
target={"entity_id": "number.mock_valve"},
),
]
)
@@ -553,3 +547,168 @@ async def test_over_valve_regulation(
assert mock_service_call.call_count == 0
assert mock_send_event.call_count == 0
@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
): # pylint: disable=unused-argument
"""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: ThermostatOverValve = 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

@@ -168,6 +168,7 @@ async def test_window_management_time_enough(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
new_callable=PropertyMock,
return_value=False,
):
await send_temperature_change_event(entity, 15, datetime.now())
@@ -188,6 +189,7 @@ async def test_window_management_time_enough(
"homeassistant.helpers.condition.state", return_value=True
) as mock_condition, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
new_callable=PropertyMock,
return_value=True,
):
await send_window_change_event(entity, True, False, datetime.now())
@@ -203,6 +205,8 @@ async def test_window_management_time_enough(
assert mock_heater_off.call_count == 2
assert mock_condition.call_count == 1
assert entity.hvac_mode is HVACMode.OFF
assert entity._saved_hvac_mode is HVACMode.HEAT
assert entity.hvac_off_reason == HVAC_OFF_REASON_WINDOW_DETECTION
assert entity.window_state == STATE_ON
# Close the window
@@ -216,6 +220,7 @@ async def test_window_management_time_enough(
"homeassistant.helpers.condition.state", return_value=True
) as mock_condition, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
new_callable=PropertyMock,
return_value=False,
):
try_function = await send_window_change_event(
@@ -239,6 +244,9 @@ async def test_window_management_time_enough(
any_order=False,
)
assert entity.preset_mode is PRESET_BOOST
assert entity.hvac_mode is HVACMode.HEAT
assert entity._saved_hvac_mode is HVACMode.HEAT # No change
assert entity.hvac_off_reason == None
# Clean the entity
entity.remove_thermostat()
@@ -319,6 +327,7 @@ async def test_window_auto_fast(hass: HomeAssistant, skip_hass_states_is_state):
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
new_callable=PropertyMock,
return_value=True,
):
event_timestamp = event_timestamp + timedelta(minutes=1)
@@ -341,6 +350,7 @@ async def test_window_auto_fast(hass: HomeAssistant, skip_hass_states_is_state):
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
new_callable=PropertyMock,
return_value=True,
):
event_timestamp = event_timestamp + timedelta(minutes=1)
@@ -514,6 +524,7 @@ async def test_window_auto_fast_and_sensor(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
new_callable=PropertyMock,
return_value=True,
):
event_timestamp = event_timestamp + timedelta(minutes=1)
@@ -536,6 +547,7 @@ async def test_window_auto_fast_and_sensor(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
new_callable=PropertyMock,
return_value=True,
):
event_timestamp = event_timestamp + timedelta(minutes=1)
@@ -646,6 +658,7 @@ async def test_window_auto_auto_stop(hass: HomeAssistant, skip_hass_states_is_st
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.set_hvac_mode"
) as mock_set_hvac_mode, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
new_callable=PropertyMock,
return_value=True,
):
event_timestamp = event_timestamp + timedelta(minutes=1)
@@ -689,7 +702,8 @@ async def test_window_auto_auto_stop(hass: HomeAssistant, skip_hass_states_is_st
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.set_hvac_mode"
) as mock_set_hvac_mode, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
return_value=False,
new_callable=PropertyMock,
return_value=True,
):
# simulate the expiration of the delay
await dearm_window_auto(None)
@@ -783,6 +797,7 @@ async def test_window_auto_no_on_percent(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
new_callable=PropertyMock,
return_value=True,
):
event_timestamp = event_timestamp + timedelta(minutes=1)
@@ -805,6 +820,7 @@ async def test_window_auto_no_on_percent(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
new_callable=PropertyMock,
return_value=True,
):
event_timestamp = event_timestamp + timedelta(minutes=1)
@@ -890,6 +906,7 @@ async def test_window_bypass(hass: HomeAssistant, skip_hass_states_is_state):
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
new_callable=PropertyMock,
return_value=False,
):
await send_temperature_change_event(entity, 15, datetime.now())
@@ -916,6 +933,7 @@ async def test_window_bypass(hass: HomeAssistant, skip_hass_states_is_state):
"homeassistant.helpers.condition.state", return_value=True
) as mock_condition, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
new_callable=PropertyMock,
return_value=True,
):
await send_window_change_event(entity, True, False, datetime.now())
@@ -941,6 +959,7 @@ async def test_window_bypass(hass: HomeAssistant, skip_hass_states_is_state):
"homeassistant.helpers.condition.state", return_value=True
) as mock_condition, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
new_callable=PropertyMock,
return_value=False,
):
try_function = await send_window_change_event(
@@ -1038,6 +1057,7 @@ async def test_window_auto_bypass(hass: HomeAssistant, skip_hass_states_is_state
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
new_callable=PropertyMock,
return_value=True,
):
event_timestamp = event_timestamp + timedelta(minutes=1)
@@ -1062,6 +1082,7 @@ async def test_window_auto_bypass(hass: HomeAssistant, skip_hass_states_is_state
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
new_callable=PropertyMock,
return_value=True,
):
event_timestamp = event_timestamp + timedelta(minutes=1)
@@ -1145,6 +1166,7 @@ async def test_window_bypass_reactivate(hass: HomeAssistant, skip_hass_states_is
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
new_callable=PropertyMock,
return_value=False,
):
await send_temperature_change_event(entity, 15, datetime.now())
@@ -1165,6 +1187,7 @@ async def test_window_bypass_reactivate(hass: HomeAssistant, skip_hass_states_is
"homeassistant.helpers.condition.state", return_value=True
) as mock_condition, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
new_callable=PropertyMock,
return_value=True,
):
await send_window_change_event(entity, True, False, datetime.now())
@@ -1191,6 +1214,7 @@ async def test_window_bypass_reactivate(hass: HomeAssistant, skip_hass_states_is
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
new_callable=PropertyMock,
return_value=False,
):
await entity.service_set_window_bypass_state(True)
@@ -1320,6 +1344,7 @@ async def test_window_action_fan_only(hass: HomeAssistant, skip_hass_states_is_s
# The underlying should be in FAN_ONLY hvac_mode
assert entity.hvac_mode is HVACMode.FAN_ONLY
assert entity._saved_hvac_mode is HVACMode.HEAT
assert entity.hvac_off_reason is None # Hvac is not off
assert entity.preset_mode is PRESET_COMFORT
# 3. Close the window
@@ -1338,7 +1363,7 @@ async def test_window_action_fan_only(hass: HomeAssistant, skip_hass_states_is_s
await try_function(None)
# Wait for initial delay of heater
await asyncio.sleep(0.3)
await hass.async_block_till_done()
assert entity.window_state == STATE_OFF
assert mock_send_event.call_count == 1
@@ -1360,6 +1385,7 @@ async def test_window_action_fan_only(hass: HomeAssistant, skip_hass_states_is_s
)
assert entity.hvac_mode is HVACMode.HEAT
assert entity.preset_mode is PRESET_COMFORT
assert entity.hvac_off_reason is None
# Clean the entity
entity.remove_thermostat()
@@ -1588,6 +1614,7 @@ async def test_window_action_eco_temp(hass: HomeAssistant, skip_hass_states_is_s
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
new_callable=PropertyMock,
return_value=True,
):
event_timestamp = event_timestamp + timedelta(minutes=1)
@@ -1609,6 +1636,7 @@ async def test_window_action_eco_temp(hass: HomeAssistant, skip_hass_states_is_s
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
new_callable=PropertyMock,
return_value=True,
):
event_timestamp = event_timestamp + timedelta(minutes=1)
@@ -1783,6 +1811,7 @@ async def test_window_action_frost_temp(hass: HomeAssistant, skip_hass_states_is
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
new_callable=PropertyMock,
return_value=True,
):
event_timestamp = event_timestamp + timedelta(minutes=1)
@@ -1804,6 +1833,7 @@ async def test_window_action_frost_temp(hass: HomeAssistant, skip_hass_states_is
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
new_callable=PropertyMock,
return_value=True,
):
event_timestamp = event_timestamp + timedelta(minutes=1)
@@ -1902,3 +1932,162 @@ async def test_window_action_frost_temp(hass: HomeAssistant, skip_hass_states_is
# Clean the entity
entity.remove_thermostat()
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_bug_66(
hass: HomeAssistant,
skip_hass_states_is_state,
skip_turn_on_off_heater,
skip_send_event,
):
"""Test that it should be possible to open/close the window rapidly without side effect"""
tz = get_tz(hass) # pylint: disable=invalid-name
now: datetime = datetime.now(tz=tz)
entry = MockConfigEntry(
domain=DOMAIN,
title="TheOverSwitchMockName",
unique_id="uniqueId",
data={
CONF_NAME: "TheOverSwitchMockName",
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_SWITCH,
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
CONF_CYCLE_MIN: 5,
CONF_TEMP_MIN: 15,
CONF_TEMP_MAX: 30,
"eco_temp": 17,
"comfort_temp": 18,
"boost_temp": 19,
CONF_USE_WINDOW_FEATURE: True,
CONF_USE_MOTION_FEATURE: False,
CONF_USE_POWER_FEATURE: False,
CONF_USE_PRESENCE_FEATURE: False,
CONF_HEATER: "switch.mock_switch",
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
CONF_TPI_COEF_INT: 0.3,
CONF_TPI_COEF_EXT: 0.01,
CONF_MINIMAL_ACTIVATION_DELAY: 30,
CONF_SECURITY_DELAY_MIN: 5,
CONF_SECURITY_MIN_ON_PERCENT: 0.5,
CONF_SECURITY_DEFAULT_ON_PERCENT: 0.1, # !! here
CONF_DEVICE_POWER: 200,
CONF_WINDOW_SENSOR: "binary_sensor.mock_window_sensor",
CONF_WINDOW_DELAY: 0, # important to not been obliged to wait
},
)
entity: BaseThermostat = await create_thermostat(
hass, entry, "climate.theoverswitchmockname"
)
assert entity
await entity.async_set_hvac_mode(HVACMode.HEAT)
await entity.async_set_preset_mode(PRESET_BOOST)
assert entity.hvac_mode is HVACMode.HEAT
assert entity.preset_mode is PRESET_BOOST
assert entity.target_temperature == 19
assert entity.window_state is STATE_OFF
# Open the window and let the thermostat shut down
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off, patch(
"homeassistant.helpers.condition.state", return_value=True
) as mock_condition, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
return_value=True,
):
await send_temperature_change_event(entity, 15, now)
try_window_condition = await send_window_change_event(
entity, True, False, now, False
)
# simulate the call to try_window_condition
await try_window_condition(None)
assert mock_send_event.call_count == 1
assert mock_heater_on.call_count == 1
assert mock_heater_off.call_count >= 1
assert mock_condition.call_count == 1
assert entity.window_state == STATE_ON
# Close the window but too shortly
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off, patch(
"homeassistant.helpers.condition.state", return_value=False
) as mock_condition, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
return_value=False,
):
event_timestamp = now + timedelta(minutes=1)
try_window_condition = await send_window_change_event(
entity, False, True, event_timestamp
)
# simulate the call to try_window_condition
await try_window_condition(None)
# window state should not have change
assert entity.window_state == STATE_ON
# Reopen immediatly with sufficient time
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off, patch(
"homeassistant.helpers.condition.state", return_value=True
) as mock_condition, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
return_value=False,
):
try_window_condition = await send_window_change_event(
entity, True, False, event_timestamp
)
# simulate the call to try_window_condition
await try_window_condition(None)
# still no change
assert entity.window_state == STATE_ON
assert entity.hvac_mode == HVACMode.OFF
# Close the window but with sufficient time this time
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off, patch(
"homeassistant.helpers.condition.state", return_value=True
) as mock_condition, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
return_value=False,
):
event_timestamp = now + timedelta(minutes=2)
try_window_condition = await send_window_change_event(
entity, False, True, event_timestamp
)
# simulate the call to try_window_condition
await try_window_condition(None)
# window state should be Off this time and old state should have been restored
assert entity.window_state == STATE_OFF
assert entity.hvac_mode is HVACMode.HEAT
assert entity.preset_mode is PRESET_BOOST