Compare commits

...

142 Commits
2.2.0 ... 4.2.0

Author SHA1 Message Date
Jean-Marc Collin
fad1c4136a Issue #244 - make ema params configurable. Try to reproduce bug on Security mode 2023-12-02 09:13:12 +00:00
Jean-Marc Collin
23f9c7c52f Feature 181 & 242 - improve auto window detection (#243)
* Add ema calculation class, calculate an emo temperature, use the ema_temperature in auto_window dectection

* Removes circular dependency error

* Fix ema_temp unknown and remove slope smoothing

* 15 sec between two slope calculation

* Take Maia feedbacks on the algo.

* Maia comments: change MAX_ALPHA to 0.5, add slope calculation at each cycle.

* With EMA entity and slope calculation optimisations

* Change open_window_detection fake datapoint threshold

* Try auto window new algo

* Don't store datetime of fake datapoint

* Change auto window threshold in °/hour

---------

Co-authored-by: Jean-Marc Collin <jean-marc.collin-extern@renault.com>
2023-12-01 21:02:53 +01:00
Jean-Marc Collin
e5076db96c Add troubleshooting in README 2023-11-28 05:42:46 +00:00
Andrea Nicotra
475cb67cf8 fix #185 switching from HEAT to COOL or viceversa (#226) 2023-11-25 07:16:19 +01:00
Jean-Marc Collin
d5c7b2e571 FIX readme (outdoor temperature) 2023-11-24 07:29:29 +00:00
Vassilis Papanikolaou
12092a7412 Fix english language translation strings (#225)
Thank you @vassilis-papanikolaou
2023-11-23 22:30:42 +01:00
Vassilis Papanikolaou
b63283c0fe Add Greek language translations (#227) 2023-11-23 22:28:08 +01:00
Jean-Marc Collin
7eac10ab3c Test with reset of error when changing the target temperature. For @pbranly configuration 2023-11-18 11:50:51 +00:00
Jean-Marc Collin
856f47ce03 Use HA 2023.11.2, fix regulation unit tests 2023-11-17 23:53:48 +00:00
Jean-Marc Collin
f1595f93da Issue #199 - persist and don't reset the accumulation error 2023-11-17 18:11:55 +00:00
Jean-Marc Collin
a5c548bbee Fix issue #195 - Presence management don't work for person 2023-11-17 17:24:11 +00:00
Jean-Marc Collin
1375b3c53a Feature 194 - add auto-regulation Export mode (#197)
* Add config, read and create algo
* Tests ok

---------

Co-authored-by: Jean-Marc Collin <jean-marc.collin-extern@renault.com>
2023-11-17 07:40:07 +01:00
Jean-Marc Collin
72ede4a03f Test parameters for gunmalmg AC heatpump conditions 2023-11-14 06:41:20 +00:00
Jean-Marc Collin
96076bf7c2 Change the auto-regulation last calculation
Change auto-window detection to 30 sec of delay
2023-11-12 17:44:02 +00:00
Jean-Marc Collin
a3f7043f45 Add target_temperature_step management 2023-11-12 11:20:40 +00:00
Jean-Marc Collin
67c01b02ec Add default target temperature step 2023-11-12 10:37:44 +00:00
Jean-Marc Collin
ab1c6892df Issue #164 - multiple calls to regulation 2023-11-12 09:20:52 +00:00
Jean-Marc Collin
84c8ac4f59 Beers from @Gunnar M 2023-11-11 17:32:41 +00:00
Jean-Marc Collin
faab9648a7 Add Rointe incompatility 2023-11-11 16:41:03 +00:00
Jean-Marc Collin
a30ad38a53 Add logs for issue #164 2023-11-11 15:48:12 +00:00
Jean-Marc Collin
c0b186b8c1 Issue #181 - auto-window for over_climate doesn't work 2023-11-11 15:20:52 +00:00
Jean-Marc Collin
01e761aecd FIX is_device_active flag 2023-11-11 11:39:04 +00:00
Jean-Marc Collin
55a99054fa FIX overpowering is not always saved 2023-11-11 10:30:37 +00:00
Jean-Marc Collin
2c5078cd7f With update for UI card 2023-11-11 08:41:25 +00:00
Jean-Marc Collin
82348adef2 Add Heatzy incompatibility 2023-11-10 22:26:08 +00:00
Jean-Marc Collin
71aad211c6 Add power_percent in over_switch for UI 2023-11-07 00:09:34 +00:00
Jean-Marc Collin
a40f976fd1 Enhance messages when temp are not ready 2023-11-06 16:54:19 +00:00
Jean-Marc Collin
382f6f99c6 Issue #162 - overpowering mode after preset change 2023-11-06 16:43:59 +00:00
Jean-Marc Collin
95c4aa8ae9 Issue #174 - regression following PR#150 2023-11-06 16:13:35 +00:00
Jean-Marc Collin
a6a47fde53 Resolve devcontainers warnings 2023-11-06 15:58:09 +00:00
echopage
e08f51b4f2 Update it.json (#172)
Verifica e sostituzione terminologie errate
2023-11-06 16:17:19 +01:00
Jean-Marc Collin
cf2098bd88 Release 4.0.0 2023-11-06 12:53:28 +01:00
Jean-Marc Collin
0c8d80f378 Issue #168 - Adds slow auto-new_slow_regulation (#170)
Issue #169 - Adds support for Versatile Thermostat UI Card

Co-authored-by: Jean-Marc Collin <jean-marc.collin-extern@renault.com>
2023-11-06 12:31:45 +01:00
Jean-Marc Collin
69a05725c9 Update the template for issue. 2023-11-05 09:38:28 +00:00
adi90x
9abcd98f52 Mean power Update (#150)
* Mean power should not multiply power setup + Documentation

* Update test to use device_power
2023-11-03 23:31:41 +01:00
Andrea Nicotra
5e6b477174 switch from HEAT to COOL mode (#144)
* support both HEAT and COOL mode

* update unit test to support both HEAT and COOL mode
2023-11-02 00:40:48 +01:00
Jean-Marc Collin
89b6f0523b FIX is_device_active 2023-11-01 23:08:01 +00:00
Jean-Marc Collin
8282b69209 FIX issue #145 2023-11-01 12:43:39 +01:00
Jean-Marc Collin
ca56b58587 Documentation for wire pilot over_switch 2023-11-01 12:32:26 +01:00
Jean-Marc Collin
2ebeac30e6 Feature 124 add inversion pilot wire (#149)
* Add inverse switch command for ThermostatOverSwitch

---------

Co-authored-by: Jean-Marc Collin <jean-marc.collin-extern@renault.com>
2023-11-01 12:08:13 +01:00
Jean-Marc Collin
dd7d6c97b3 Feature #129 add auto regulation for over climate (#148)
* Algo implementation and tests
---------

Co-authored-by: Jean-Marc Collin <jean-marc.collin-extern@renault.com>
2023-11-01 00:52:31 +01:00
Jean-Marc Collin
923d374ce3 FIX save window state if bypass is activated 2023-10-30 09:15:07 +00:00
Jean-Marc Collin
3c988749b5 Release 3.7.0 2023-10-30 08:51:12 +00:00
Jean-Marc Collin
dc89e011c7 Window bypass - add testu and color theme for sensor 2023-10-30 08:22:06 +00:00
adi90x
2786a6e5ae Window ByPass (#143)
* Add Window ByPass
2023-10-30 07:47:51 +01:00
misa1515
88760dbec9 Create sk.json (#135)
* Create sk.json

* Update sk.json

* Update sk.json
2023-10-29 22:21:57 +01:00
Jean-Marc Collin
1cc47626c7 Refactorisation and complete #137 2023-10-29 09:20:06 +00:00
Jean-Marc Collin
4905c93a51 Removes Valve update trigger to avoid loops 2023-10-28 22:13:17 +00:00
Jean-Marc Collin
1b49d8566f Add valves keys missing 2023-10-28 21:23:42 +00:00
Jean-Marc Collin
15c419ae82 FIX bug (#141)
Co-authored-by: Jean-Marc Collin <jean-marc.collin-extern@renault.com>
2023-10-28 18:53:20 +02:00
Jean-Marc Collin
e1c4fa1a08 Add _entity_component_unrecorded_attributes to exclude attributes from record (#140)
Co-authored-by: Jean-Marc Collin <jean-marc.collin-extern@renault.com>
2023-10-28 18:31:15 +02:00
Jean-Marc Collin
17a0217678 Removes old docs 2023-10-28 11:30:49 +02:00
Jean-Marc Collin
1c5790d096 Feature 131 control trv valve (#139)
* Add configFlow and translations
* Refacto VersatileThermostat with BaseThermostat. Tests ok
* Add ThermostatValve. All tests ok

---------

Co-authored-by: Jean-Marc Collin <jean-marc.collin-extern@renault.com>
2023-10-28 11:30:14 +02:00
Jean-Marc Collin
bf4eee85d8 Add Homematic TRV into incompatible devices 2023-10-22 09:59:29 +02:00
Jean-Marc Collin
f1116b79cc Merge branch 'main' of github.com:jmcollin78/versatile_thermostat into main 2023-10-22 00:17:32 +02:00
Jean-Marc Collin
f37e896fe4 Update beer list. 2023-10-21 22:13:25 +00:00
Jean-Marc Collin
e0f1c968a7 Release 3.6.0 (#134)
* Replace tests at the right place. Add missing config files. Run tests

* FIX merge from #108 have lost some changes

* FIX entity_not found error

* Change port

* Add tests for over switch AC mode

* Issue #133 - Force temperature after off to on transition of over climate

* Build release 3.6.0

---------

Co-authored-by: Jean-Marc Collin <jean-marc.collin-extern@renault.com>
2023-10-21 23:51:50 +02:00
Andrea Nicotra
eb54f2826f move AC mode config under the right configuration step (#108)
* move AC mode config under the right configuration step
2023-10-21 08:31:45 +02:00
Jean-Marc Collin
043fd5f7aa Feature 128 activity preset (#132)
* Issue #128 - add motion_off_delay

* Fix movement detection

---------

Co-authored-by: Jean-Marc Collin <jean-marc.collin-extern@renault.com>
2023-10-21 08:28:19 +02:00
Jean-Marc Collin
7e4e407732 Issue #125 - remove thermometers for over climate mode. 2023-10-18 11:34:19 +02:00
Jean-Marc Collin
81900ceeea Issue #120 - presence sensor not updated 2023-10-15 19:04:46 +02:00
Jean-Marc Collin
66297c6044 FIX non catched error (see issue #120) 2023-10-15 18:39:13 +02:00
Jean-Marc Collin
f384225b0f Maj documentation + issue #115 2023-10-15 18:16:24 +02:00
Jean-Marc Collin
e6ecd100f6 Issue #119 - Set preset target temperature not updating in ac mode 2023-10-15 12:22:15 +02:00
Jean-Marc Collin
7ac49d7864 undo remove other packages. Test don't run into gitlab-ci environment 2023-10-15 10:43:59 +02:00
Jean-Marc Collin
40da04838d Bug #121 loop in over climate
* Issue #121 - loop when underlying is slow
* Issue #121 - try fix
* Release 3.5.3
* Fix tests step
2023-10-15 10:29:54 +02:00
Jean-Marc Collin
fcdd93b4ae Issue # 114 - add unit test for multi-climate VTherm 2023-10-08 13:08:34 +02:00
Jean-Marc Collin
56fdbf4fba FIX Issue #114 - Thermostat not reporting on / off properly (90 sec delay) 2023-10-08 11:05:04 +02:00
Jean-Marc Collin
ef994e300b Documentation 2023-10-08 01:29:25 +02:00
Jean-Marc Collin
72c4105bbd Issue #113 - Add multi thermostat over climate 2023-10-08 01:23:25 +02:00
Jean-Marc Collin
79eb4a0a0d Enhance documentation. 2023-10-07 18:44:39 +02:00
Jean-Marc Collin
b032198c66 Issue #103 - expose preset temp pour AC mode 2023-10-07 18:16:20 +02:00
Jean-Marc Collin
487c118b44 Issue #101 - Use target temperature of underlying if changed 2023-10-07 17:54:50 +02:00
Jean-Marc Collin
e29ff0568b Issue #82 - unavailable climate goes into security mode 2023-10-07 10:49:40 +02:00
Jean-Marc Collin
814e4d3b83 Last HA versions 2023-10-03 08:09:12 +02:00
felix schwenzel
abb6531f49 Update climate.py (#112)
without these parenthesis the `action` and therefore returned `hvac_action` seems to be `True` instead of i.e. `heating`
(did not see that when using an underlying homekit devices climate device, but when the underlying was a generic thermostat)
2023-10-03 08:06:52 +02:00
Jean-Marc Collin
f970c18eaf Issue #100: compatibility with HA 2023.9.0 2023-09-08 08:48:09 +02:00
Jean-Marc Collin
af51ef62e0 Issue #99 : over climate VTherm a regulated by the device itself and should not goes into security 2023-08-30 09:06:26 +02:00
Jean-Marc Collin
b38fbd9d78 Issue #99 - security mode toggling 100 times within 2 minutes 2023-08-27 18:06:43 +02:00
Jean-Marc Collin
6e8e72e343 FIX Service name Github error 2023-08-16 22:30:46 +02:00
Jean-Marc Collin
2bebe3e210 Issue #95 - the integration would switch ac on and off rapidly and lock up home assistant if outside temp is NaN 2023-08-05 20:02:54 +02:00
Jean-Marc Collin
aa3b87762d FIX AC climate stops even if already stopped 2023-07-30 21:25:20 +02:00
Jean-Marc Collin
f4cabbf2c0 FIX unit tests 2023-07-30 18:09:42 +02:00
Jean-Marc Collin
24b59e545b FIX cancel_timer await 2023-07-29 11:22:50 +02:00
Jean-Marc Collin
5997a26c73 FIX #90 WarCOzes remark 2023-07-23 09:08:56 +02:00
Jean-Marc Collin
fe4b9ced81 Documentation 2023-07-22 17:23:42 +02:00
Jean-Marc Collin
c4fc976007 Terminate Add AC mode and fix remove_thermostat sync 2023-07-22 17:05:54 +02:00
Jean-Marc Collin
31d862acab With config_flow ok 2023-07-22 12:21:54 +02:00
Jean-Marc Collin
9709a9eed0 Add AC mode configFlow 2023-07-22 10:47:10 +02:00
Jean-Marc Collin
61eae8c066 FIX #89 _is_aux_heat error 2023-06-25 12:27:00 +02:00
Jean-Marc Collin
e16daa3d53 Add hacs.yaml github workflow 2023-05-14 11:11:55 +02:00
Jean-Marc Collin
90a6c926e3 Resolve actions warning 2023-05-14 11:02:29 +02:00
Jean-Marc Collin
64ce3aa0ad Issue #81 - recursive loop when security should be set 2023-04-28 12:12:38 +02:00
Jean-Marc Collin
3f498ffbd3 Add BuyMeCoffee contributors (fr) 2023-04-26 08:23:03 +02:00
Jean-Marc Collin
3236be6c3b Update README.md
Add BuyMeCoffee contributors
2023-04-26 08:21:54 +02:00
Jean-Marc Collin
be86fd3ac0 Merge branch 'main' of github.com:jmcollin78/versatile_thermostat into main 2023-04-22 11:06:12 +02:00
Jean-Marc Collin
e35ba57bd7 Fix compilation warnings 2023-04-22 11:05:38 +02:00
Bergoglio
72d7803ffa Add files via upload (#79)
Thanks @Bergoglio !
2023-04-22 10:29:48 +02:00
Jean-Marc Collin
4dd7c62a42 Migration to 2023.4 and fix lingering tasks and timers tests errors 2023-04-22 09:24:57 +02:00
Jean-Marc Collin
429ff47269 Typo in README 2023-04-14 08:34:43 +02:00
Jean-Marc Collin
a17423d470 FIX #73, #72. thanks to Salabur 2023-04-14 08:24:01 +02:00
Jean-Marc Collin
81a467b8c3 Translations typos 2023-04-02 17:40:40 +02:00
Jean-Marc Collin
4e3ee0703b Typo 2023-04-02 17:33:59 +02:00
Jean-Marc Collin
539ec4a6bd Isseu #70. Build release. 2023-04-02 16:44:57 +02:00
Jean-Marc Collin
9b085f1264 FIX overpowering ko 2023-03-28 10:06:35 +02:00
Jean-Marc Collin
637367bd65 FIX circular call on over_climate 2023-03-28 09:33:50 +02:00
Jean-Marc Collin
bcc0a32b6a Release 3.2 beta1 - an error in multi-testu 2023-03-26 18:39:08 +02:00
Jean-Marc Collin
80fa977c15 With all testu developed and ok 2023-03-26 12:40:38 +02:00
Jean-Marc Collin
67d20dd083 Testu ok 2023-03-25 13:02:03 +01:00
Jean-Marc Collin
e2e8499bdb First commit not completed 2023-03-25 12:32:51 +01:00
Jean-Marc Collin
93cfd22744 Issue #66 When windows opens and closes rapidly, thermostat stays to OFF 2023-03-19 10:28:16 +01:00
Jean-Marc Collin
0671e008a1 Issue #63 - security parameters cannot be set to 0 2023-03-18 12:18:44 +01:00
Jean-Marc Collin
a7465fba2e Issue #56 - Exception when undeerlying thermostat is not found at startup 2023-03-18 11:40:28 +01:00
Jean-Marc Collin
c98197e99f Add buy me a coffe badge 2023-03-12 11:27:23 +01:00
Jean-Marc Collin
b091056032 Add buy me a coffee badge 2023-03-12 11:24:59 +01:00
Jean-Marc Collin
c9efea2ce0 Typo in Readme 2023-03-12 11:10:30 +01:00
Jean-Marc Collin
171ad20d85 Release 3.1.0 2023-03-12 11:06:17 +01:00
Jean-Marc Collin
63cf77abc9 [#28] Add a window open detection based on internal temperature change 2023-03-11 18:17:17 +01:00
Jean-Marc Collin
6e40a15262 Documentation for release 2023-03-05 10:36:58 +01:00
Jean-Marc Collin
974e5d26db Finish unit tests #48
Remove warnings #48
Add theme colors #27
2023-03-05 10:11:16 +01:00
Jean-Marc Collin
ae32f117a0 Add reload and default power and energy units 2023-03-04 11:23:20 +01:00
Jean-Marc Collin
eb8cb18c6f Update documentation 2023-03-04 09:47:50 +01:00
Jean-Marc Collin
ea7b6a0425 FIX error on startup when my_climate is not found 2023-03-03 22:03:08 +01:00
Jean-Marc Collin
7c8717553b Add duration cycle sensors 2023-03-03 18:28:33 +01:00
Jean-Marc Collin
f672fc807d Add sensors 2023-03-01 08:11:53 +01:00
Jean-Marc Collin
168568ac5d With all binary_sensor ok 2023-02-28 22:48:07 +01:00
Jean-Marc Collin
330c3323d1 Add binary_sensors and it's ok 2023-02-26 23:34:37 +01:00
Jean-Marc Collin
e63213d22a try to fix [MANIFEST] Manifest keys have been sorted: domain, name, then alphabetical order 2023-02-26 11:49:07 +01:00
Jean-Marc Collin
fb7ee1bdac try to FIX Manifest keys have been sorted 2023-02-26 11:44:33 +01:00
Jean-Marc Collin
ca86b310c4 Power of the heater should be accessible event if power management is not selected #53 2023-02-26 11:41:38 +01:00
Jean-Marc Collin
23074e6f46 Add energy for thermostat over climate 2023-02-26 11:22:20 +01:00
Jean-Marc Collin
718315c4fe FIX Documentation is wrong #55 2023-02-24 08:06:29 +01:00
Jean-Marc Collin
46278ca9a3 In certain case, temperature event are registred with an offset of one hour #52 2023-02-19 22:42:02 +01:00
Jean-Marc Collin
0b81a94d0f Add testus and change date timestamp. 2023-02-19 19:10:08 +01:00
Jean-Marc Collin
33590886c1 With energy calculation 2023-02-18 18:10:37 +01:00
Jean-Marc Collin
039b372a53 Add power tests 2023-02-18 11:49:37 +01:00
Jean-Marc Collin
a161540f10 Testus +1 2023-02-18 11:49:20 +01:00
Jean-Marc Collin
8bbcafdf4a Add the device power and device energy into attributes #25 2023-02-18 00:07:54 +01:00
Jean-Marc Collin
08d08e52de FIX A thermostat stays with security_default_on_percent when the preset change during security mode #49 2023-02-15 23:44:09 +01:00
Jean-Marc Collin
81b4f7e5f6 Testus 2 2023-02-12 23:52:04 +01:00
Jean-Marc Collin
7a917c6ff7 Testus 2023-02-12 18:50:56 +01:00
Jean-Marc Collin
20a9e2523e First config flow testu 2023-02-12 12:35:32 +01:00
85 changed files with 17726 additions and 2837 deletions

View File

@@ -1,6 +1,4 @@
echo "Sourcing .bashrc"
alias ll='ls -l'
export HA='/home/vscode/core'
cd $HA
source venv/bin/activate
# source venv/bin/activate

View File

@@ -1,140 +1,237 @@
default_config:
logger:
default: info
logs:
custom_components.versatile_thermostat: debug
default: info
logs:
custom_components.versatile_thermostat: debug
custom_components.versatile_thermostat.underlyings: debug
custom_components.versatile_thermostat.climate: debug
# If you need to debug uncommment the line below (doc: https://www.home-assistant.io/integrations/debugpy/)
debugpy:
start: true
wait: false
port: 5678
start: true
wait: false
port: 5678
versatile_thermostat:
auto_regulation_expert:
kp: 0.4
ki: 0.08
k_ext: 0.0
offset_max: 5
stabilization_threshold: 0.1
accumulated_error_threshold: 50
short_ema_params:
max_alpha: 0.6
halflife_sec: 301
precision: 3
input_number:
fake_temperature_sensor1:
name: Temperature
min: 0
max: 35
step: .1
icon: mdi:thermometer
unit_of_measurement: °C
fake_external_temperature_sensor1:
name: Ext Temperature
min: -10
max: 35
step: .1
icon: mdi:home-thermometer
unit_of_measurement: °C
fake_current_power:
name: Current power
min: 0
max: 1000
step: 10
icon: mdi:flash
unit_of_measurement: kW
fake_current_power_max:
name: Current power max threshold
min: 0
max: 1000
step: 10
icon: mdi:flash
unit_of_measurement: kW
fake_temperature_sensor1:
name: Temperature
min: 0
max: 35
step: .1
icon: mdi:thermometer
unit_of_measurement: °C
mode: box
fake_external_temperature_sensor1:
name: Ext Temperature
min: -10
max: 35
step: .1
icon: mdi:home-thermometer
unit_of_measurement: °C
mode: box
fake_current_power:
name: Current power
min: 0
max: 1000
step: 10
icon: mdi:flash
unit_of_measurement: kW
fake_current_power_max:
name: Current power max threshold
min: 0
max: 1000
step: 10
icon: mdi:flash
unit_of_measurement: kW
fake_valve1:
name: The valve 1
min: 0
max: 100
icon: mdi:pipe-valve
unit_of_measurement: percentage
input_boolean:
# input_boolean to simulate the windows entity. Only for development environment.
fake_window_sensor1:
name: Window 1
icon: mdi:window-closed-variant
# input_boolean to simulate the heater entity switch. Only for development environment.
fake_heater_switch1:
name: Heater 1 (Linear)
icon: mdi:radiator
fake_heater_switch2:
name: Heater (TPI with presence preset)
icon: mdi:radiator
fake_heater_switch3:
name: Heater (TPI with offset)
icon: mdi:radiator
# input_boolean to simulate the motion sensor entity. Only for development environment.
fake_motion_sensor1:
name: Motion Sensor 1
icon: mdi:run
# input_boolean to simulate the presence sensor entity. Only for development environment.
fake_presence_sensor1:
name: Presence Sensor 1
icon: mdi:home
# input_boolean to simulate the windows entity. Only for development environment.
fake_window_sensor1:
name: Window 1
icon: mdi:window-closed-variant
# input_boolean to simulate the heater entity switch. Only for development environment.
fake_heater_switch3:
name: Heater 3
icon: mdi:radiator
fake_heater_switch2:
name: Heater 2
icon: mdi:radiator
fake_heater_switch1:
name: Heater 1
icon: mdi:radiator
fake_heater_ac1:
name: Air contionner 1
icon: mdi:air-conditioner
fake_heater_4switch1:
name: Heater (multiswitch1)
icon: mdi:radiator
fake_heater_4switch2:
name: Heater (multiswitch2)
icon: mdi:radiator
fake_heater_4switch3:
name: Heater (multiswitch3)
icon: mdi:radiator
fake_heater_4switch4:
name: Heater (multiswitch4)
icon: mdi:radiator
fake_heater_4climate1:
name: Heater (multiclimate1)
icon: mdi:radiator
fake_heater_4climate2:
name: Heater (multiclimate2)
icon: mdi:radiator
fake_heater_4climate3:
name: Heater (multiclimate3)
icon: mdi:radiator
fake_heater_4climate4:
name: Heater (multiclimate4)
icon: mdi:radiator
# input_boolean to simulate the motion sensor entity. Only for development environment.
fake_motion_sensor1:
name: Motion Sensor 1
icon: mdi:run
# input_boolean to simulate the presence sensor entity. Only for development environment.
fake_presence_sensor1:
name: Presence Sensor 1
icon: mdi:home
climate:
- platform: generic_thermostat
name: Underlying thermostat1
heater: input_boolean.fake_heater_switch3
target_sensor: input_number.fake_temperature_sensor1
- platform: generic_thermostat
name: Underlying thermostat2
heater: input_boolean.fake_heater_switch3
target_sensor: input_number.fake_temperature_sensor1
- platform: generic_thermostat
name: Underlying thermostat3
heater: input_boolean.fake_heater_switch3
target_sensor: input_number.fake_temperature_sensor1
- platform: generic_thermostat
name: Underlying thermostat4
heater: input_boolean.fake_heater_switch3
target_sensor: input_number.fake_temperature_sensor1
- platform: generic_thermostat
name: Underlying thermostat5
heater: input_boolean.fake_heater_switch3
target_sensor: input_number.fake_temperature_sensor1
- platform: generic_thermostat
name: Underlying thermostat6
heater: input_boolean.fake_heater_switch3
target_sensor: input_number.fake_temperature_sensor1
- platform: generic_thermostat
name: Underlying thermostat7
heater: input_boolean.fake_heater_switch3
target_sensor: input_number.fake_temperature_sensor1
- platform: generic_thermostat
name: Underlying thermostat8
heater: input_boolean.fake_heater_switch3
target_sensor: input_number.fake_temperature_sensor1
- platform: generic_thermostat
name: Underlying thermostat9
heater: input_boolean.fake_heater_switch3
target_sensor: input_number.fake_temperature_sensor1
- platform: generic_thermostat
name: Underlying thermostat1
heater: input_boolean.fake_heater_switch3
target_sensor: input_number.fake_temperature_sensor1
- platform: generic_thermostat
name: Underlying thermostat2
heater: input_boolean.fake_heater_switch3
target_sensor: input_number.fake_temperature_sensor1
- platform: generic_thermostat
name: Underlying thermostat3
heater: input_boolean.fake_heater_switch3
target_sensor: input_number.fake_temperature_sensor1
- platform: generic_thermostat
name: Underlying thermostat4
heater: input_boolean.fake_heater_switch3
target_sensor: input_number.fake_temperature_sensor1
- platform: generic_thermostat
name: Underlying thermostat 4-1
heater: input_boolean.fake_heater_4climate1
target_sensor: input_number.fake_temperature_sensor1
ac_mode: false
- platform: generic_thermostat
name: Underlying thermostat 4-2
heater: input_boolean.fake_heater_4climate2
target_sensor: input_number.fake_temperature_sensor1
ac_mode: false
- platform: generic_thermostat
name: Underlying thermostat 4-3
heater: input_boolean.fake_heater_4climate3
target_sensor: input_number.fake_temperature_sensor1
ac_mode: false
- platform: generic_thermostat
name: Underlying thermostat 4-4
heater: input_boolean.fake_heater_4climate4
target_sensor: input_number.fake_temperature_sensor1
ac_mode: false
- platform: generic_thermostat
name: Underlying thermostat9
heater: input_boolean.fake_heater_switch3
target_sensor: input_number.fake_temperature_sensor1
recorder:
include:
domains:
- input_boolean
- input_number
- switch
- climate
- sensor
include:
domains:
- input_boolean
- input_number
- switch
- climate
- sensor
template:
- binary_sensor:
- name: maison_occupee
unique_id: maison_occupee
state: "{{is_state('person.jmc', 'home') }}"
device_class: occupancy
- binary_sensor:
- name: maison_occupee
unique_id: maison_occupee
state: "{{is_state('person.jmc', 'home') }}"
device_class: occupancy
- sensor:
- name: "Total énergie switch1"
unique_id: total_energie_switch1
unit_of_measurement: "kWh"
device_class: energy
state_class: total_increasing
state: >
{% set energy = state_attr('climate.thermostat_switch_1', 'total_energy') | float(default=-1) %}
{% if energy < 0 %}{{none}}{% else %}
{{ energy | round(2, default=0) }}
{% endif %}
- name: "Total énergie climate 2"
unique_id: total_energie_climate2
unit_of_measurement: "kWh"
device_class: energy
state_class: total_increasing
state: >
{% set energy = state_attr('climate.thermostat_climate_2', 'total_energy') | float(default=-1) %}
{% if energy < 0 %}{{none}}{% else %}
{{ energy | round(2, default=0) }}
{% endif %}
- name: "Total énergie chambre"
unique_id: total_energie_chambre
unit_of_measurement: "kWh"
device_class: energy
state_class: total_increasing
state: >
{% set energy = state_attr('climate.thermostat_chambre', 'total_energy') | float(default=-1) %}
{% if energy < 0 %}{{none}}{% else %}
{{ energy | round(2, default=0) }}
{% endif %}
switch:
- platform: template
switches:
pilote_sdb_rdc:
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
data:
option: comfort
target:
entity_id: select.seche_serviettes_sdb_rdc_cable_outlet_mode
- platform: template
switches:
pilote_sdb_rdc:
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
data:
option: comfort
target:
entity_id: select.seche_serviettes_sdb_rdc_cable_outlet_mode
turn_off:
service: select.select_option
data:
option: comfort-2
target:
entity_id: select.seche_serviettes_sdb_rdc_cable_outlet_mode
turn_off:
service: select.select_option
data:
option: comfort-2
target:
entity_id: select.seche_serviettes_sdb_rdc_cable_outlet_mode
frontend:
extra_module_url:
- /config/www/community/versatile-thermostat-ui-card/versatile-thermostat-ui-card.js
themes:
versatile_thermostat_theme:
state-binary_sensor-safety-on-color: "#FF0B0B"
state-binary_sensor-power-on-color: "#FF0B0B"
state-binary_sensor-window-on-color: "rgb(156, 39, 176)"
state-binary_sensor-motion-on-color: "rgb(156, 39, 176)"
state-binary_sensor-presence-on-color: "lightgreen"
state-binary_sensor-running-on-color: "orange"

View File

@@ -1,44 +1,59 @@
// See https://aka.ms/vscode-remote/devcontainer.json for format details.
// "image": "ghcr.io/ludeeus/devcontainer/integration:latest",
{
"image": "mcr.microsoft.com/vscode/devcontainers/python:0-3.10",
"image": "mcr.microsoft.com/devcontainers/python:1-3.11-bullseye",
"name": "Versatile Thermostat integration",
"context": "..",
"appPort": [
"9123:8123"
"8123:8123"
],
// "postCreateCommand": "container install",
"postCreateCommand": "./container install",
"extensions": [
"ms-python.python",
"github.vscode-pull-request-github",
"ryanluker.vscode-coverage-gutters",
"ms-python.vscode-pylance"
],
"postCreateCommand": "./container dev-setup",
"mounts": [
"source=/Users/jmcollin/SugarSync/Projets/home-assistant/core,target=/home/vscode/core,type=bind,consistency=cached",
"source=${localWorkspaceFolder}/.devcontainer/configuration.yaml,target=/home/vscode/core/config/configuration.yaml,type=bind,consistency=cached",
"source=${localWorkspaceFolder}/custom_components,target=/home/vscode/core/config/custom_components,type=bind,consistency=cached"
"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"
],
"settings": {
"files.eol": "\n",
"editor.tabSize": 4,
"terminal.integrated.profiles.linux": {
"Bash Profile": {
"path": "bash",
"args": []
"customizations": {
"vscode": {
"extensions": [
"ms-python.python",
"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"
],
// "mounts": [
// "source=${localWorkspaceFolder}/.devcontainer/configuration.yaml,target=${localWorkspaceFolder}/config/www/community/,type=bind,consistency=cached",
// "source=${localWorkspaceFolder}/custom_components,target=/home/vscode/core/config/custom_components,type=bind,consistency=cached"
// ],
"settings": {
"files.eol": "\n",
"editor.tabSize": 4,
"terminal.integrated.profiles.linux": {
"bash": {
"path": "bash",
"args": []
}
},
"terminal.integrated.defaultProfile.linux": "bash",
// "terminal.integrated.shell.linux": "/bin/bash",
"python.pythonPath": "/usr/bin/python3",
"python.analysis.autoSearchPaths": true,
"pylint.lintOnChange": false,
"python.formatting.provider": "black",
"python.formatting.blackPath": "/usr/local/py-utils/bin/black",
"editor.formatOnPaste": false,
"editor.formatOnSave": true,
"editor.formatOnType": true,
"files.trimTrailingWhitespace": true,
"python.experiments.optOutFrom": ["pythonTestAdapter"],
"python.analysis.logLevel": "Trace"
}
},
"terminal.integrated.defaultProfile.linux": "Bash Profile",
// "terminal.integrated.shell.linux": "/bin/bash",
"python.pythonPath": "/usr/bin/python3",
"python.analysis.autoSearchPaths": true,
"python.linting.pylintEnabled": true,
"python.linting.enabled": true,
"python.formatting.provider": "black",
"editor.formatOnPaste": false,
"editor.formatOnSave": true,
"editor.formatOnType": true,
"files.trimTrailingWhitespace": true
}
}
}

View File

@@ -6,10 +6,11 @@ about: Create a report to help us improve
<!-- Before you open a new issue, search through the existing issues to see if others have had the same problem.
If you have a simple question or you are not sure this is an issue, don't open an issue but open a new discussion [here](https://github.com/jmcollin78/versatile_thermostat/discussions).
Issues not containing the minimum requirements will be closed:
- Issues without a description (using the header is not good enough) will be closed.
- Issues without debug logging will be closed.
- Issues without configuration will be closed
-->
@@ -21,19 +22,116 @@ If you are unsure about the version check the const.py file.
## Configuration
<!-- Copy / paste the attributes of the VTherm here. You can go to Development Tool / States, find and select your VTherm and the copy/paste the attributes.
Without these attribute support is impossible due to the number of configuration attributes the VTherm have (more than 60). -->
My VTherm attributes are the following:
```yaml
Add your logs here.
hvac_modes:
- heat
- 'off'
min_temp: 7
max_temp: 35
preset_modes:
- none
- eco
- comfort
- boost
- activity
current_temperature: 18.9
temperature: 22
hvac_action: 'off'
preset_mode: security
hvac_mode: 'off'
type: null
eco_temp: 17
boost_temp: 20
comfort_temp: 19
eco_away_temp: 16.1
boost_away_temp: 16.3
comfort_away_temp: 16.2
power_temp: 13
ext_current_temperature: 11.6
ac_mode: false
current_power: 450
current_power_max: 910
saved_preset_mode: none
saved_target_temp: 22
saved_hvac_mode: heat
window_state: 'on'
motion_state: 'off'
overpowering_state: false
presence_state: 'on'
window_auto_state: false
window_bypass_state: false
security_delay_min: 2
security_min_on_percent: 0.5
security_default_on_percent: 0.1
last_temperature_datetime: '2023-11-05T00:48:54.873157+01:00'
last_ext_temperature_datetime: '2023-11-05T00:48:53.240122+01:00'
security_state: true
minimal_activation_delay_sec: 1
device_power: 300
mean_cycle_power: 30
total_energy: 137.5
last_update_datetime: '2023-11-05T00:51:54.901140+01:00'
timezone: Europe/Paris
window_sensor_entity_id: input_boolean.fake_window_sensor1
window_delay_sec: 20
window_auto_open_threshold: null
window_auto_close_threshold: null
window_auto_max_duration: null
motion_sensor_entity_id: input_boolean.fake_motion_sensor1
presence_sensor_entity_id: input_boolean.fake_presence_sensor1
power_sensor_entity_id: input_number.fake_current_power
max_power_sensor_entity_id: input_number.fake_current_power_max
is_over_switch: true
underlying_switch_0: input_boolean.fake_heater_switch1
underlying_switch_1: null
underlying_switch_2: null
underlying_switch_3: null
on_percent: 0.1
on_time_sec: 6
off_time_sec: 54
cycle_min: 1
function: tpi
tpi_coef_int: 0.6
tpi_coef_ext: 0.01
friendly_name: Thermostat switch 1
supported_features: 17
```
<!-- Please do not send an image but a copy / paste of the attributes in yaml format. -->
## Describe the bug
A clear and concise description of what the bug is.
I'm trying to:
<!-- compleete the description -->
And I expect:
<!-- complete the expectations -->
But I observe this ....
<!-- complete what you observe and why you think it is erroneous. -->
I read the documentation on the README.md file and I don't find any relevant information about this issue.
## Debug log
<!-- To enable debug logs check this https://www.home-assistant.io/components/logger/ -->
<!-- To enable debug logs check this https://www.home-assistant.io/components/logger/
Add the following configuration into your `configuration.yaml` (or `logger.yaml` if you have one) to enable logs: -->
```yaml
logger:
default: info
logs:
custom_components.versatile_thermostat: info
```
<!-- You can also switch to debug mode but be careful, in debug mode, the logs are verbose.
Please copy/paste the releveant logs (around the failure) below: -->
```text

View File

@@ -10,7 +10,7 @@ jobs:
runs-on: "ubuntu-latest"
name: Validate
steps:
- uses: "actions/checkout@v2"
- uses: "actions/checkout@v3.5.2"
- name: HACS validation
uses: "hacs/action@main"

17
.github/workflows/hacs.yml vendored Normal file
View File

@@ -0,0 +1,17 @@
name: HACS Action
on:
push:
pull_request:
schedule:
- cron: "0 0 * * *"
jobs:
hacs:
name: HACS Action
runs-on: "ubuntu-latest"
steps:
- name: HACS Action
uses: "hacs/action@main"
with:
category: "integration"

View File

@@ -8,7 +8,7 @@ jobs:
runs-on: "ubuntu-latest"
name: Validate
steps:
- uses: "actions/checkout@v2"
- uses: "actions/checkout@v3.5.2"
- name: HACS validation
uses: "hacs/action@main"
@@ -23,28 +23,30 @@ jobs:
runs-on: "ubuntu-latest"
name: Check style formatting
steps:
- uses: "actions/checkout@v2"
- uses: "actions/setup-python@v1"
- uses: "actions/checkout@v3.5.2"
- uses: "actions/setup-python@v4.6.0"
with:
python-version: "3.x"
- run: python3 -m pip install black
- run: black .
tests:
# Tests don't run in Gitlab ci environment
if: 0
runs-on: "ubuntu-latest"
name: Run tests
steps:
- name: Check out code from GitHub
uses: "actions/checkout@v2"
uses: "actions/checkout@v3.5.2"
- name: Setup Python
uses: "actions/setup-python@v1"
uses: "actions/setup-python@v4.6.0"
with:
python-version: "3.8"
- name: Install requirements
run: python3 -m pip install -r requirements_test.txt
run: cd custom_components/versatile_thermostat && python3 -m pip install -r requirements_test.txt
- name: Run tests
run: |
pytest \
cd custom_components/versatile_thermostat && pytest \
-qq \
--timeout=9 \
--durations=10 \

View File

@@ -11,7 +11,7 @@ jobs:
runs-on: "ubuntu-latest"
name: Validate
steps:
- uses: "actions/checkout@v2"
- uses: "actions/checkout@v3.5.2"
- name: HACS validation
uses: "hacs/action@main"
@@ -26,8 +26,8 @@ jobs:
runs-on: "ubuntu-latest"
name: Check style formatting
steps:
- uses: "actions/checkout@v2"
- uses: "actions/setup-python@v1"
- uses: "actions/checkout@v3.5.2"
- uses: "actions/setup-python@v4.6.0"
with:
python-version: "3.x"
- run: python3 -m pip install black

7
.gitignore vendored
View File

@@ -103,4 +103,9 @@ dist
# TernJS port file
.tern-port
__pycache__
# init file required for unittest
custom_components/__init__.py
__pycache__
config/**
custom_components/hacs

35
.vscode/launch.json vendored
View File

@@ -3,36 +3,15 @@
"version": "0.2.0",
"configurations": [
{
// Example of attaching to local debug server
"name": "Python: Attach Local",
"name": "Home Assistant (debug)",
"type": "python",
"request": "attach",
"port": 5678,
"host": "localhost",
"request": "launch",
"module": "homeassistant",
"justMyCode": false,
"pathMappings": [
// {
// "localRoot": "${workspaceFolder}",
// "remoteRoot": "."
//},
{
"localRoot": "${workspaceFolder}/../core",
"remoteRoot": "/home/vscode/core"
}
]
},
{
// Example of attaching to my production server
"name": "Python: Attach Remote",
"type": "python",
"request": "attach",
"port": 5678,
"host": "homeassistant.local",
"pathMappings": [
{
"localRoot": "${workspaceFolder}",
"remoteRoot": "/usr/src/homeassistant"
}
"args": [
"--debug",
"-c",
"config"
]
}
]

20
.vscode/settings.json vendored
View File

@@ -1,12 +1,20 @@
{
"python.linting.pylintEnabled": true,
"python.linting.enabled": true,
"python.pythonPath": "/usr/local/bin/python",
"[python]": {
"editor.defaultFormatter": "ms-python.black-formatter",
"editor.formatOnSave": true
},
"pylint.lintOnChange": false,
"files.associations": {
"*.yaml": "home-assistant"
},
"python.testing.pytestArgs": [
"tests"
],
"python.testing.unittestEnabled": false,
"python.testing.pytestEnabled": true,
"python.analysis.extraPaths": [
"/home/vscode/core",
"/workspaces/versatile_thermostat"
]
// "/home/vscode/core",
"/workspaces/versatile_thermostat/custom_components/versatile_thermostat"
],
"python.formatting.provider": "none"
}

4
.vscode/tasks.json vendored
View File

@@ -2,13 +2,13 @@
"version": "2.0.0",
"tasks": [
{
"label": "Run Home Assistant on port 9123",
"label": "Run Home Assistant on port 8123",
"type": "shell",
"command": "./container start",
"problemMatcher": []
},
{
"label": "Restart Home Assistant on port 9123",
"label": "Restart Home Assistant on port 8123",
"type": "shell",
"command": "./container restart",
"problemMatcher": []

61
CONTRIBUTING-fr.md Normal file
View File

@@ -0,0 +1,61 @@
# Consignes de contribution
Contribuer à ce projet doit être aussi simple et transparent que possible, que ce soit :
- Signaler un bug
- Discuter de l'état actuel du code
- Soumettre un correctif
- Proposer de nouvelles fonctionnalités
## Github est utilisé pour tout
Github est utilisé pour héberger du code, pour suivre les problèmes et les demandes de fonctionnalités, ainsi que pour accepter les demandes d'extraction.
Les demandes d'extraction sont le meilleur moyen de proposer des modifications à la base de code.
1. Fourchez le dépôt et créez votre branche à partir de `master`.
2. Si vous avez modifié quelque chose, mettez à jour la documentation.
3. Assurez-vous que votre code peluche (en utilisant du noir).
4. Testez votre contribution.
5. Émettez cette pull request !
## Toutes les contributions que vous ferez seront sous la licence logicielle MIT
En bref, lorsque vous soumettez des modifications de code, vos soumissions sont considérées comme étant sous la même [licence MIT](http://choosealicense.com/licenses/mit/) qui couvre le projet. N'hésitez pas à contacter les mainteneurs si cela vous préoccupe.
## Signaler les bogues en utilisant les [issues] de Github (../../issues)
Les problèmes GitHub sont utilisés pour suivre les bogues publics.
Signalez un bogue en [ouvrant un nouveau problème](../../issues/new/choose) ; C'est si facile!
## Rédiger des rapports de bogue avec des détails, un arrière-plan et un exemple de code
Les **rapports de bogues géniaux** ont tendance à avoir :
- Un résumé rapide et/ou un historique
- Étapes à reproduire
- Être spécifique!
- Donnez un exemple de code si vous le pouvez.
- Ce à quoi vous vous attendiez arriverait
- Que se passe-t-il réellement
- Notes (y compris éventuellement pourquoi vous pensez que cela pourrait se produire, ou des choses que vous avez essayées qui n'ont pas fonctionné)
Les gens *adorent* les rapports de bogues approfondis. Je ne plaisante même pas.
## Utilisez un style de codage cohérent
Utilisez [black](https://github.com/ambv/black) pour vous assurer que le code suit le style.
## Testez votre modification de code
Ce composant personnalisé est basé sur les meilleures pratiques décrites ici [modèle d'intégration_blueprint](https://github.com/custom-components/integration_blueprint).
Il est livré avec un environnement de développement dans un conteneur, facile à lancer
si vous utilisez Visual Studio Code. Avec ce conteneur, vous aurez un stand alone
Instance de Home Assistant en cours d'exécution et déjà configurée avec le inclus
[`.devcontainer/configuration.yaml`](./.devcontainer/configuration.yaml)
déposer.
## Licence
En contribuant, vous acceptez que vos contributions soient autorisées sous sa licence MIT.

61
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,61 @@
# Contribution guidelines
Contributing to this project should be as easy and transparent as possible, whether it's:
- Reporting a bug
- Discussing the current state of the code
- Submitting a fix
- Proposing new features
## Github is used for everything
Github is used to host code, to track issues and feature requests, as well as accept pull requests.
Pull requests are the best way to propose changes to the codebase.
1. Fork the repo and create your branch from `master`.
2. If you've changed something, update the documentation.
3. Make sure your code lints (using black).
4. Test you contribution.
5. Issue that pull request!
## Any contributions you make will be under the MIT Software License
In short, when you submit code changes, your submissions are understood to be under the same [MIT License](http://choosealicense.com/licenses/mit/) that covers the project. Feel free to contact the maintainers if that's a concern.
## Report bugs using Github's [issues](../../issues)
GitHub issues are used to track public bugs.
Report a bug by [opening a new issue](../../issues/new/choose); it's that easy!
## Write bug reports with detail, background, and sample code
**Great Bug Reports** tend to have:
- A quick summary and/or background
- Steps to reproduce
- Be specific!
- Give sample code if you can.
- What you expected would happen
- What actually happens
- Notes (possibly including why you think this might be happening, or stuff you tried that didn't work)
People *love* thorough bug reports. I'm not even kidding.
## Use a Consistent Coding Style
Use [black](https://github.com/ambv/black) to make sure the code follows the style.
## Test your code modification
This custom component is based on best practices described here [integration_blueprint template](https://github.com/custom-components/integration_blueprint).
It comes with development environment in a container, easy to launch
if you use Visual Studio Code. With this container you will have a stand alone
Home Assistant instance running and already configured with the included
[`.devcontainer/configuration.yaml`](./.devcontainer/configuration.yaml)
file.
## License
By contributing, you agree that your contributions will be licensed under its MIT License.

View File

@@ -2,26 +2,38 @@
[![GitHub Activity][commits-shield]][commits]
[![License][license-shield]](LICENSE)
[![hacs][hacs_badge]][hacs]
[![BuyMeCoffee][buymecoffeebadge]][buymecoffee]
![Tip](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/icon.png?raw=true)
> ![Tip](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/tips.png?raw=true?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](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. ;-).
- [Changements majeurs dans la version 4.0.0](#changements-majeurs-dans-la-version-400)
- [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)
- [Pourquoi une nouvelle implémentation du thermostat ?](#pourquoi-une-nouvelle-implémentation-du-thermostat-)
- [Comment installer cet incroyable Thermostat Versatile ?](#comment-installer-cet-incroyable-thermostat-versatile-)
- [HACS installation (recommendé)](#hacs-installation-recommendé)
- [Installation manuelle](#installation-manuelle)
- [Configuration](#configuration)
- [Choix des attributs de base](#choix-des-attributs-de-base)
- [Sélectionnez l'entité pilotée](#sélectionnez-lentité-pilotée)
- [Sélectionnez des entités pilotées](#sélectionnez-des-entités-pilotées)
- [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)
- [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 portes/fenêtres en allumant/éteignant les thermostats](#configurer-les-portesfenêtres-en-allumantéteignant-les-thermostats)
- [Le mode capteur](#le-mode-capteur)
- [Le mode auto](#le-mode-auto)
- [Configurer le mode d'activité ou la détection de mouvement](#configurer-le-mode-dactivité-ou-la-détection-de-mouvement)
- [Configurer la gestion de la puissance](#configurer-la-gestion-de-la-puissance)
- [Configurer la présence ou l'occupation](#configurer-la-présence-ou-loccupation)
- [Configuration avancée](#configuration-avancée)
- [Synthèse des paramètres](#synthèse-des-paramètres)
- [Exemples de réglage](#exemples-de-réglage)
- [Chauffage électrique](#chauffage-électrique)
- [Chauffage central (chauffage gaz ou fuel)](#chauffage-central-chauffage-gaz-ou-fuel)
@@ -30,42 +42,83 @@
- [Mes presets](#mes-presets)
- [Algorithme](#algorithme)
- [Algorithme TPI](#algorithme-tpi)
- [Capteurs](#capteurs)
- [Services](#services)
- [Forcer la présence/occupation](#forcer-la-présenceoccupation)
- [Modifier la température des préréglages](#modifier-la-température-des-préréglages)
- [Modifier les paramètres de sécurité](#modifier-les-paramètres-de-sécurité)
- [ByPass Window Check](#bypass-window-check)
- [Notifications](#notifications)
- [Attributs personnalisés](#attributs-personnalisés)
- [Quelques résultats](#quelques-résultats)
- [Encore mieux](#encore-mieux)
- [Bien mieux avec le Versatile Thermostat UI Card](#bien-mieux-avec-le-versatile-thermostat-ui-card)
- [Encore mieux avec le composant Scheduler !](#encore-mieux-avec-le-composant-scheduler-)
- [Encore bien mieux avec la custom:simple-thermostat front integration](#encore-bien-mieux-avec-la-customsimple-thermostat-front-integration)
- [Toujours mieux avec Apex-chart pour régler votre thermostat](#toujours-mieux-avec-apex-chart-pour-régler-votre-thermostat)
- [Et toujours de mieux en mieux avec l'AappDaemon NOTIFIER pour notifier les évènements](#et-toujours-de-mieux-en-mieux-avec-laappdaemon-notifier-pour-notifier-les-évènements)
- [Les contributions sont les bienvenues !](#les-contributions-sont-les-bienvenues)
_Composant développé à l'aide de l'incroyable modèle de développement [[blueprint](https://github.com/custom-components/integration_blueprint)]._
- [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)
- [Seul le premier radiateur chauffe](#seul-le-premier-radiateur-chauffe)
- [Régler les paramètres de détection d'ouverture de fenêtre en mode auto](#régler-les-paramètres-de-détection-douverture-de-fenêtre-en-mode-auto)
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 4.2** : Le calcul de la pente de la courbe de température se fait maintenant en °/heure et non plus en °/min [#242](https://github.com/jmcollin78/versatile_thermostat/issues/242). Correction de la détection automatique des ouvertures par l'ajout d'un lissage de la courbe de température .
> * **Release 4.1** : Ajout d'un mode de régulation **Expert** dans lequel l'utilisateur peut spécifier ses propres paramètres d'auto-régulation au lieu d'utiliser les pre-programmés [#194](https://github.com/jmcollin78/versatile_thermostat/issues/194).
> * **Release 4.0** : Ajout de la prise en charge de la **Versatile Thermostat UI Card**. Voir [Versatile Thermostat UI Card](https://github.com/jmcollin78/versatile-thermostat-ui-card). Ajout d'un mode de régulation **Slow** pour les appareils de chauffage à latence lente [#168](https://github.com/jmcollin78/versatile_thermostat/issues/168). Changement de la façon dont **la puissance est calculée** dans le cas de VTherm avec des équipements multi-sous-jacents [#146](https://github.com/jmcollin78/versatile_thermostat/issues/146). Ajout de la prise en charge de AC et Heat pour VTherm via un interrupteur également [#144](https://github.com/jmcollin78/versatile_thermostat/pull/144)
<details>
<summary>Autres versions</summary>
> * **Release 3.8**: Ajout d'une **fonction d'auto-régulation** pour les thermostats `over climate` dont la régulation est faite par le climate sous-jacent. Cf. [L'auto-régulation](#lauto-régulation) et [#129](https://github.com/jmcollin78/versatile_thermostat/issues/129). Ajout de la **possibilité d'inverser la commande** pour un thermostat `over switch` pour adresser les installations avec fil pilote et diode [#124](https://github.com/jmcollin78/versatile_thermostat/issues/124).
> * **Release 3.7**: Ajout du type de **Versatile Thermostat `over valve`** pour piloter une vanne TRV directement ou tout autre équipement type gradateur pour le chauffage. La régulation se fait alors directement en agissant sur le pourcentage d'ouverture de l'entité sous-jacente : 0 la vanne est coupée, 100 : la vanne est ouverte à fond. Cf. [#131](https://github.com/jmcollin78/versatile_thermostat/issues/131). Ajout d'une fonction permettant le bypass de la détection d'ouverture [#138](https://github.com/jmcollin78/versatile_thermostat/issues/138). Ajout de la langue Slovaque
> * **Release 3.6**: Ajout du paramètre `motion_off_delay` pour améliorer la gestion de des mouvements [#116](https://github.com/jmcollin78/versatile_thermostat/issues/116), [#128](https://github.com/jmcollin78/versatile_thermostat/issues/128). Ajout du mode AC (air conditionné) pour un VTherm over switch. Préparation du projet Github pour faciliter les contributions [#127](https://github.com/jmcollin78/versatile_thermostat/issues/127)
> * **Release 3.5**: Plusieurs thermostats sont possibles en "thermostat over climate" mode [#113](https://github.com/jmcollin78/versatile_thermostat/issues/113)
> * **Release 3.4**: bug fix et exposition des preset temperatures pour le mode AC [#103](https://github.com/jmcollin78/versatile_thermostat/issues/103)
> * **Release 3.3**: ajout du mode Air Conditionné (AC). Cette fonction vous permet d'utiliser le mode AC de votre thermostat sous-jacent. Pour l'utiliser, vous devez cocher l'option "Uitliser le mode AC" et définir les valeurs de température pour les presets et pour les presets en cas d'absence
> * **Release 3.2** : ajout de la possibilité de commander plusieurs switch à partir du même thermostat. Dans ce mode, les switchs sont déclenchés avec un délai pour minimiser la puissance nécessaire à un instant (on minimise les périodes de recouvrement). Voir [Configuration](#sélectionnez-des-entités-pilotées)
> * **Release 3.1** : ajout d'une détection de fenêtres/portes ouvertes par chute de température. Cette nouvelle fonction permet de stopper automatiquement un radiateur lorsque la température chute brutalement. Voir [Le mode auto](#le-mode-auto)
> * **Release majeure 3.0** : ajout d'un équipement thermostat et de capteurs (binaires et non binaires) associés. Beaucoup plus proche de la philosphie Home Assistant, vous avez maintenant un accès direct à l'énergie consommée par le radiateur piloté par le thermostat et à plein d'autres capteurs qui seront utiles dans vos automatisations et dashboard.
> * **release 2.3** : ajout de la mesure de puissance et d'énergie du radiateur piloté par le thermostat.
> * **release 2.2** : ajout de fonction de sécurité permettant de ne pas laisser éternellement en chauffe un radiateur en cas de panne du thermomètre
> * **release majeure 2.0** : ajout du thermostat "over climate" permettant de transformer n'importe quel thermostat en Versatile Thermostat et lui ajouter toutes les fonctions de ce dernier.
</details>
# Changements majeurs dans la version 4.0.0
1. La puissance de l'appareil doit maintenant être la puissance totale de tous les appareils controlée par le VTherm. Cela permet d'avoir des équipements hétérogènes de puissance différente. Dans le cas de plusieurs appareils contrôlés par un seul VTherm, vous devrez éditer et changer la valeur `device_power`. Vous devez configurer la puissance totale de tous les appareils.
2. Le seuil de détection automatique des ouvertures doit être spécifié en °/heure et pas plus en °/min. Pour conserver les mêmes paramètres il faut multiplier la valeur configurée par 60.
# 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 pour les bières. Ca fait très plaisir et ça m'encourage à continuer !
# Quand l'utiliser et ne pas l'utiliser
Ce thermostat peut piloter 2 types d'équipement:
Ce thermostat peut piloter 3 types d'équipements :
1. un radiateur qui ne fonctionne qu'en mode marche/arrêt (nommé ```thermostat_over_switch```). La configuration minimale nécessaire pour utiliser ce type thermostat est :
a. un équipement comme un radiateur (un ```switch``` ou équivalent),
b. une sonde de température pour la pièce (ou un input_number),
c. un capteur de température externe (pensez à l'intégration météo si vous n'en avez pas)
1. un équipement comme un radiateur (un ```switch``` ou équivalent),
2. une sonde de température pour la pièce (ou un input_number),
3. un capteur de température externe (pensez à l'intégration météo si vous n'en avez pas)
2. un autre thermostat qui a ses propres modes de fonctionnement (nommé ```thermostat_over_climate```). Pour ce type de thermostat la configuration minimale nécessite :
a. un équipement comme une climatisation qui est pilotée par sa propre entity de type ```climate```,
b. une sonde de température pour la pièce (ou un input_number),
c. un capteur de température externe (pensez à l'intégration météo si vous n'en avez pas)
1. un équipement - comme une climatisation, une valve thermostatique - qui est pilotée par sa propre entity de type ```climate```,
3. un équipement qui peut prendre une valeur de 0 à 100% (nommée ```thermostat_over_valve```). A 0 le chauffage est coupé, 100% il est ouvert à fond. Ce type permet de piloter une valve thermostatique (cf. valve Shelly) qui expose une entité de type `number.` permetttant de piloter directement l'ouverture de la vanne. Versatile Thermostat régule la température de la pièce en jouant sur le pourcentage d'ouverture, à l'aide des capteurs de température intérieur et extérieur en utilisant l'algorithme TPI décrit ci-dessous.
Le type ```thermostat_over_climate``` permet d'ajouter à votre équipement existant toutes les fonctionnalités fournies par VersatileThermostat. L'entité climate VersatileThermostat pilotera votre entité climate, en la coupant si les fenêtres sont ouvertes, la passant en mode Eco si personne n'est présent, etc. Cf. [ici](#pourquoi-une-nouvelle-implémentation-du-thermostat). Pour ce type de thermostat, les cycles éventuels de chauffe sont pilotés par l'entité climate sous-jacente et pas par le Versatile Thermostat lui-même.
Le type `over_climate` vous permet d'ajouter à votre équipement existant toutes les fonctionnalités apportées par VersatileThermostat. L'entité climate VersatileThermostat contrôlera votre entité climate sous-jacente, l'éteindra si les fenêtres sont ouvertes, la fera passer en mode Eco si personne n'est présent, etc. Voir [ici] (#pourquoi-un-nouveau-thermostat-implémentation). Pour ce type de thermostat, tous les cycles de chauffage sont contrôlés par l'entité climate sous-jacente et non par le thermostat polyvalent lui-même. Une fonction facultative d'auto-régulation permet au Versatile Thermostat d'ajuster la température donnée en consigne au sous-jacent afin d'atteindre la consigne.
Parce que cette intégration vise à commander le radiateur en tenant compte du préréglage configuré (preset) et de la température ambiante, ces informations sont obligatoires.
Les installations avec fil pilote et diode d'activation bénéficie d'une option qui permet d'inverser la commande on/off du radiateur sous-jacent. Pour cela, utilisez le type `over switch` et cochez l'option d'inversion de la commande.
## Incompatibilités
Certains thermostat de type TRV sont réputés incompatibles avec le Versatile Thermostat. C'est le cas des vannes suivantes :
1. les vannes POPP de Danfoss avec retour de température. Il est impossible d'éteindre cette vanne et elle s'auto-régule d'elle-même causant des conflits avec le VTherm,
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.
# Pourquoi une nouvelle implémentation du thermostat ?
Pour mon usage personnel, j'avais besoin d'ajouter quelques fonctionnalités et aussi de mettre à jour le comportement implémenté dans le composant précédent "Awesome thermostat".
Ce composant nommé __Versatile thermostat__ gère les cas d'utilisation suivants :
- Configuration via l'interface graphique d'intégration standard (à l'aide du flux Config Entry),
- Utilisations complètes du **mode préréglages**,
@@ -73,19 +126,20 @@ Ce composant nommé __Versatile thermostat__ gère les cas d'utilisation suivant
- Éteindre/allumer un thermostat lorsqu'une **porte ou des fenêtres sont ouvertes/fermées** après un certain délai,
- Changer de preset lorsqu'une **activité est détectée** ou non dans une pièce pendant un temps défini,
- Utiliser un algorithme **TPI (Time Proportional Interval)** grâce à l'algorithme [[Argonaute](https://forum.hacf.fr/u/argonaute/summary)] ,
- Ajoutez une **gestion de délestage** ou une régulation pour ne pas dépasser une puissance totale définie. Lorsque la puissance maximale est dépassée, un préréglage caché de « puissance » est défini sur l'entité climatique. Lorsque la puissance passe en dessous du maximum, le préréglage précédent est restauré.
- Ajouter la **gestion de la présence à domicile**. Cette fonctionnalité vous permet de modifier dynamiquement la température du préréglage en tenant compte d'un capteur de présence de votre maison.
- Ajoutez des **services pour interagir avec le thermostat** à partir d'autres intégrations : vous pouvez forcer la présence / la non-présence à l'aide d'un service, et vous pouvez modifier dynamiquement la température des préréglages et changer les paramètres de sécurité.
- Ajouter une **gestion de délestage** ou une régulation pour ne pas dépasser une puissance totale définie. Lorsque la puissance maximale est dépassée, un préréglage caché de « puissance » est défini sur l'entité climatique. Lorsque la puissance passe en dessous du maximum, le préréglage précédent est restauré.
- La **gestion de la présence à domicile**. Cette fonctionnalité vous permet de modifier dynamiquement la température du préréglage en tenant compte d'un capteur de présence de votre maison.
- Des **services pour interagir avec le thermostat** à partir d'autres intégrations : vous pouvez forcer la présence / la non-présence à l'aide d'un service, et vous pouvez modifier dynamiquement la température des préréglages et changer les paramètres de sécurité.
- Ajouter des capteurs pour voir les états internes du thermostat.
# Comment installer cet incroyable Thermostat Versatile ?
## HACS installation (recommendé)
1. Installez [HACS](https://hacs.xyz/). De cette façon, vous obtenez automatiquement les mises à jour.
2. Ajoutez ce repository Github en tant que repository personnalisé dans les paramètres HACS.
2. L'intégration Versatile Thermostat est maintenant proposée directement depuis l'interface HACF (onglet intégrations),
3. recherchez et installez "Versatile Thermostat" dans HACS et cliquez sur "installer".
4. Redémarrez Home Assistant.
5. Ensuite, vous pouvez ajouter une intégration de Versatile Thermostat dans la page d'intégration. Vous ajoutez autant de thermostats dont vous avez besoin (généralement un par radiateur qui doit être géré ou par pompe dans le cas d'un chauffage centralisé)
5. Ensuite, vous pouvez ajouter une intégration de Versatile Thermostat dans la page Paramètres / Intégrations. Vous ajoutez autant de thermostats dont vous avez besoin (généralement un par radiateur ou par groupe de radiateurs qui doivent être gérés ou par pompe dans le cas d'un chauffage centralisé)
## Installation manuelle
@@ -116,30 +170,161 @@ Suivez ensuite les étapes de configuration comme suit :
Donnez les principaux attributs obligatoires :
1. un nom (sera le nom de l'intégration et aussi le nom de l'entité climate)
2. le type de thermostat ```thermostat_over_switch``` pour piloter un radiateur commandé par un switch ou ```thermostat_over_climate``` pour piloter un autre thermostat. Cf. [ci-dessus](#pourquoi-une-nouvelle-implémentation-du-thermostat)
2. le type de thermostat ```thermostat_over_switch``` pour piloter un radiateur commandé par un switch ou ```thermostat_over_climate``` pour piloter un autre thermostat, ou ```thermostat_over_valve``` Cf. [ci-dessus](#pourquoi-une-nouvelle-implémentation-du-thermostat)
4. un identifiant d'entité de capteur de température qui donne la température de la pièce dans laquelle le radiateur est installé,
5. une entité capteur de température donnant la température extérieure. Si vous n'avez pas de capteur externe, vous pouvez utiliser l'intégration météo locale
6. une durée de cycle en minutes. A chaque cycle, le radiateur s'allumera puis s'éteindra pendant une durée calculée afin d'atteindre la température ciblée (voir [preset](#configure-the-preset-temperature) ci-dessous),
6. une durée de cycle en minutes. A chaque cycle, le radiateur s'allumera puis s'éteindra pendant une durée calculée afin d'atteindre la température ciblée (voir [preset](#configure-the-preset-temperature) ci-dessous). En mode ```over_climate```, le cycle ne sert qu'à faire des controles de base mais ne régule pas directement la température. C'est le ```climate``` sous-jacent qui le fait,
7. les températures minimales et maximales du thermostat,
8. 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.
8. une puissance de l'équipement ce qui va activer les capteurs de puissance et énergie consommée par l'appareil,
9. la liste des fonctionnalités qui seront utilisées pour ce thermostat. En fonction de vos choix, les écrans de configuration suivants s'afficheront ou pas.
> ![Astuce](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/tips.png?raw=true) _*Notes*_
1. avec le type ```thermostat_over_swutch```, les calculs sont effectués à chaque cycle. Donc en cas de changement de conditions, il faudra attendre le prochain cycle pour voir un changement. Pour cette raison, le cycle ne doit pas être trop long. **5 min est une bonne valeur**,
2. si le cycle est trop court, le radiateur ne pourra jamais atteindre la température cible en effet pour le radiateur à accumulation et il sera sollicité inutilement
> 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 l'entité pilotée
En fonction de votre choix sur le type de thermostat, vous devrez choisir une entité de type switch ou une entité de type climate. Seules les entités compatibles sont présentées.
## Sélectionnez des entités pilotées
En fonction de votre choix sur le type de thermostat, vous devrez choisir une ou plusieurs entités de type `switch`, `climate` ou `number`. Seules les entités compatibles avec le type sont présentées.
Pour un thermostat de type ```thermostat_over_switch```:
> ![Astuce](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/tips.png?raw=true) _*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,
> 2. si vous avez un radiateur électrique (avec ou sans fil pilote) et qu'une entité de type ```switch``` permet de l'allumer ou de l'éteindre, alors le type ```over_switch``` est préférable. La régulation sera faite par le Versatile Thermostat en fonction de la température mesuré par votre thermomètre, à l'endroit ou vous l'avez placé,
> 3. dans tous les autres cas, utilisez le mode ```over_climate```. Vous gardez votre entité ```climate``` d'origine et le Versatile Thermostat "ne fait que" piloter le on/off et la température cible de votre thermostat d'origine. La régulation est faite par votre thermostat d'origine dans ce cas. Ce mode est particulièrement adapté aux climatisations réversible tout-en-un dont l'exposition dans Home Assistant se limite à une entité de type ```climate```
> 2. **quelle type de régulation je veux ?** Si l'équipement piloté possède son propre mécanisme de régulation (clim, certaine vanne TRV) et que cette régulation fonctionne bien, optez pour un ```over_climate```
### Pour un thermostat de type ```thermostat_over_switch```
![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/config-linked-entity.png?raw=true)
L'algorithme à utiliser est aujourd'hui limité à TPI est disponible. Voir [algorithme](#algorithme)
L'algorithme à utiliser est aujourd'hui limité à TPI est disponible. Voir [algorithme](#algorithme).
Si plusieurs entités de type sont configurées, la thermostat décale les activations afin de minimiser le nombre de switch actif à un instant t. Ca permet une meilleure répartition de la puissance puisque chaque radiateur va s'allumer à son tour.
Exemple de déclenchement synchronisé :
![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/multi-switch-activation.png?raw=true)
Pour un thermostat de type ```thermostat_over_climate```:
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)
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.
#### L'auto-régulation
Depuis la release 3.8, vous avez la possibilité d'activer la fonction d'auto-régulation. Cette fonction autorise VersatileThermostat à adapter la consigne de température donnée au climate sous-jacent afin que la température de la pièce atteigne réellement la consigne.
Pour faire ça, le VersatileThermostat calcule un décalage basé sur les informations suivantes :
1. la différence actuelle entre la température réelle et la température de consigne, appelé erreur brute,
2. l'accumulation des erreurs passées,
3. la différence entre la température extérieure et la consigne
Ces trois informations sont combinées pour calculer le décalage qui sera ajouté à la consigne courante et envoyé au climate sous-jacent.
La fonction d'auto-régulation se paramètre avec :
1. une dégré de régulation :
1. Légère - pour des faibles besoin en auto-régulation. Dans ce mode, le décalage maximal sera de 1,5°,
2. Medium - pour une auto-régulation moyenne. Un décalage maximal de 2° est possible dans ce mode,
3. Forte - pour un fort besoin d'auto-régulation. Le décalage maximal est de 3° dans ce mode et l'auto-régulation réagira fortement aux changements de température.
2. Un seuil d'auto-régulation : valeur en dessous de laquelle une nouvelle régulation ne sera pas appliquée. Imaginons qu'à un instant t, le décalage soit de 2°. Si au prochain calcul, le décalage est de 2.4°, il sera pas appliqué. Il ne sera appliqué que la différence entre 2 décalages sera au moins égal à ce seuil,
3. Période minimal entre 2 auto-régulation : ce nombre, exprimé en minute, indique la durée entre 2 changements de régulation.
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*_
> 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,
> 4. Si ce n'est toujours pas suffisant, passez en auto-régulation Forte,
> 5. Si ce n'est toujours pas bon, il faudra passer en mode expert pour pouvoir régler les paramètres de régulation de façon fine. Voir en-dessous
L'auto-régulation consiste à forcer l'équipement a aller plus loin en lui forçant sa température de consigne régulièrement. Sa consommation peut donc être augmentée, ainsi que son usure.
#### L'auto-régulation en mode Expert
En mode **Expert** pouvez régler finement les paramètres de l'auto-régulation pour atteindre vos objeetifs et optimiser au mieux. L'algorithme calcule l'écart entre la consigne et la température réelle de la pièce. Cet écard est appelé erreur.
Les paramètres réglables sont les suivants :
1. `kp` : le facteur appliqué à l'erreur brute,
2. `ki` : le facteur appliqué à l'accumulation des erreurs,
3. `k_ext` : le facteur appliqué à la différence entre la température intérieure et la température externe,
4. `offset_max` : le maximum de correction (offset) que la régulation peut appliquer,
5. `stabilization_threshold` : un seuil de stabilisation qui lorsqu'il est atteint par l'erreur remet à 0, l'accumulation des erreurs,
6. `accumulated_error_threshold` : le maximum pour l'accumulation d'erreur.
Pour le tuning il faut tenir compte de ces observations :
1. `kp * erreur` va donner l'offset lié à l'erreur brute. Cet offset est directement proportionnel à l'erreur et sera à 0 lorsque la target sera atteinte,
2. l'accumulation de l'erreur permet de corriger le stabilisation de la courbe alors qu'il reste une erreur. L'erreur s'accumule et l'offset augmente donc progressivement ce qui devrait finir par stabiliser sur la température cible. Pour que ce paramètre fondamental est un effet il faut qu'il soit pas trop petit. Une valeur moyenne est 30
3. `ki * accumulated_error_threshold` va donner l'offset maximal lié à l'accumulation de l'erreur,
4. `k_ext` permet d'appliquer tout de suite (sans attendre une accumulation des erreurs) une correction lorsque la température extérieure est très différente de la température cible. Si la stabilisation se fait trop haut lorsqu'il les écarts de température sont importants, c'est que ce paramètre est trop fort. Il devrait pouvoir être annulé totalement pour laisser faire les 2 premiers offset
Les valeurs préprogrammées sont les suivantes :
Slow régulation :
kp: 0.2 # 20% of the current internal regulation offset are caused by the current difference of target temperature and room temperature
ki: 0.8 / 288.0 # 80% of the current internal regulation offset are caused by the average offset of the past 24 hours
k_ext: 1.0 / 25.0 # this will add 1°C to the offset when it's 25°C colder outdoor than indoor
offset_max: 2.0 # limit to a final offset of -2°C to +2°C
stabilization_threshold: 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: 2.0 * 288 # this allows up to 2°C long term offset in both directions
Light régulation :
kp: 0.2
ki: 0.05
k_ext: 0.05
offset_max: 1.5
stabilization_threshold: 0.1
accumulated_error_threshold: 10
Medium régulation :
kp: 0.3
ki: 0.05
k_ext: 0.1
offset_max: 2
stabilization_threshold: 0.1
accumulated_error_threshold: 20
Strong régulation :
"""Strong parameters for regulation
A set of parameters which doesn't take into account the external temp
and concentrate to internal temp error + accumulated error.
This should work for cold external conditions which else generates
high external_offset"""
kp: 0.4
ki: 0.08
k_ext: 0.0
offset_max: 5
stabilization_threshold: 0.1
accumulated_error_threshold: 50
Pour utiliser le mode Expert il vous faut déclarer les valeurs que vous souhaitez utiliser pour chacun de ces paramètres dans votre `configuration.yaml` sous la forme suivante :
```
versatile_thermostat:
auto_regulation_expert:
kp: 0.4
ki: 0.08
k_ext: 0.0
offset_max: 5
stabilization_threshold: 0.1
accumulated_error_threshold: 50
```
et bien sur, configurer le mode auto-régulation du VTherm en mode Expert. Tous les VTherm en mode **Expert** utiliseront ces mêmes paramètres.
Pour que les modifications soient prises en compte, il faut soit **relancer totalement Home Assistant** soit juste l'intégration Versatile Thermostat (Outils de dev / Yaml / rechargement de la configuration / Versatile Thermostat).
### Pour un thermostat de type ```thermostat_over_valve```:
![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/config-linked-entity3.png?raw=true)
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).
Il est possible de choisir un thermostat over valve qui commande une climatisation en cochant la case "AC Mode". Dans ce cas, seul le mode refroidissement sera visible.
## Configurez les coefficients de l'algorithme TPI
Si vous avez choisi un thermostat de type ```thermostat_over_switch``` vous arriverez sur cette page :
Si vous avez choisi un thermostat de type ```over_switch``` ou ```over_valve``` vous arriverez sur cette page :
![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/config-tpi.png?raw=true)
@@ -160,29 +345,57 @@ Le mode préréglé (preset) vous permet de préconfigurer la température cibl
- **Confort** : l'appareil est en mode confort
- **Boost** : l'appareil tourne toutes les vannes à fond
Si le mode AC est utilisé, vous pourrez aussi configurer les températures lorsque l'équipement en mode climatisation.
**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*_
1. En modifiant manuellement la température cible, réglez le préréglage sur Aucun (pas de préréglage). De cette façon, vous pouvez toujours définir une température cible même si aucun préréglage n'est disponible.
2. Le préréglage standard ``Away`` est un préréglage caché qui n'est pas directement sélectionnable. Versatile Thermostat utilise la gestion de présence ou la gestion de mouvement pour régler automatiquement et dynamiquement la température cible en fonction d'une présence dans le logement ou d'une activité dans la pièce. Voir [gestion de la présence](#configure-the-presence-management).
3. Si vous utilisez la gestion du délestage, vous verrez un préréglage caché nommé ``power``. Le préréglage de l'élément chauffant est réglé sur « puissance » lorsque des conditions de surpuissance sont rencontrées et que le délestage est actif pour cet élément chauffant. Voir [gestion de l'alimentation](#configure-the-power-management).
4. si vous utilisez la configuration avancée, vous verrez le préréglage défini sur ``sécurité`` si la température n'a pas pu être récupérée après un certain délai
5. Si vous ne souhaitez pas utiliser le préréglage, indiquez 0 comme température. Le préréglage sera alors ignoré et ne s'affichera pas dans le composant front
> 1. En modifiant manuellement la température cible, réglez le préréglage sur Aucun (pas de préréglage). De cette façon, vous pouvez toujours définir une température cible même si aucun préréglage n'est disponible.
> 2. Le préréglage standard ``Away`` est un préréglage caché qui n'est pas directement sélectionnable. Versatile Thermostat utilise la gestion de présence ou la gestion de mouvement pour régler automatiquement et dynamiquement la température cible en fonction d'une présence dans le logement ou d'une activité dans la pièce. Voir [gestion de la présence](#configure-the-presence-management).
> 3. Si vous utilisez la gestion du délestage, vous verrez un préréglage caché nommé ``power``. Le préréglage de l'élément chauffant est réglé sur « puissance » lorsque des conditions de surpuissance sont rencontrées et que le délestage est actif pour cet élément chauffant. Voir [gestion de l'alimentation](#configure-the-power-management).
> 4. si vous utilisez la configuration avancée, vous verrez le préréglage défini sur ``sécurité`` si la température n'a pas pu être récupérée après un certain délai
> 5. Si vous ne souhaitez pas utiliser le préréglage, indiquez 0 comme température. Le préréglage sera alors ignoré et ne s'affichera pas dans le composant front
## Configurer les portes/fenêtres en allumant/éteignant les thermostats
Si vous avez choisi la fonctionnalité ```Avec détection des ouvertures```, cliquez sur 'Valider' sur la page précédente et vous y arriverez :
Vous devez avoir choisi la fonctionnalité ```Avec détection des ouvertures``` dans la première page pour arriver sur cette page.
La détecttion des ouvertures peut se faire de 2 manières:
1. soit avec un capteur placé sur l'ouverture (mode capteur),
2. soit en détectant une chute brutale de température (mode auto)
![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/config-window.png?raw=true)
### 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)
Donnez les attributs suivants :
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.
Et c'est tout ! votre thermostat s'éteindra lorsque les fenêtres seront ouvertes et se rallumera lorsqu'il sera fermé après le délai.
### 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)
1. un seuil de détection en degré par minute. Lorsque la température chute au delà de ce seuil, le thermostat s'éteindra. Plus cette valeur est faible et plus la détection sera rapide (en contre-partie d'un risque de faux positif),
2. un seuil de fin de détection en degré par minute. Lorsque la chute de température repassera au-dessus cette valeur, le thermostat se remettra dans le mode précédent (mode et preset),
3. une durée maximale de détection. Au delà de cette durée, le thermostat se remettra dans son mode et preset précédent même si la température continue de chuter.
Pour régler les seuils il est conseillé de commencer avec les valeurs de référence et d'ajuster les seuils de détection. Quelques essais m'ont donné les valeurs suivantes (pour un bureau):
- seuil de détection : 0,05 °C/min
- seuil de non détection: 0 °C/min
- durée max : 60 min.
Un nouveau capteur "slope" a été ajouté pour tous les thermostats. Il donne la pente de la courbe de température en °C/min (ou °K/min). Cette pente est lissée et filtrée pour éviter les valeurs abérrantes des thermomètres qui viendraient pertuber la mesure.
![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/temperature-slope.png?raw=true)
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)
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*_
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
> 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 :
@@ -192,17 +405,20 @@ Si vous avez choisi la fonctionnalité ```Avec détection de mouvement```, cliqu
Nous allons maintenant voir comment configurer le nouveau mode Activité.
Ce dont nous avons besoin:
- un **capteur de mouvement**. ID d'entité d'un capteur de mouvement. Les états du capteur de mouvement doivent être « on » (mouvement détecté) ou « off » (aucun mouvement détecté)
- une durée de **délai de mouvement** (en secondes) définissant combien de temps nous attendons la confirmation du mouvement avant de considérer le mouvement
- une durée de **délai de mouvement** (en secondes) définissant combien de temps nous attendons la confirmation du mouvement avant de considérer le mouvement. Ce paramètre peut être supérieur à la temporision de votre détecteur de mouvement, sinon la détection se fera à chaque mouvement signalé par le détecteur,
- une durée de fin **délai de mouvement** (en secondes) définissant combien de temps nous attendons la confirmation d'une fin de mouvement avant de ne plus considérer le mouvement.
- un **préréglage de "mouvement" **. Nous utiliserons la température de ce préréglage lorsqu'une activité sera détectée.
- un **préréglage "pas de mouvement"**. Nous utiliserons la température de ce deuxième préréglage lorsqu'aucune activité n'est détectée.
Alors imaginons que nous voulions avoir le comportement suivant :
- nous avons une pièce avec un thermostat réglé en mode activité, le mode "mouvement" choisi est confort (21.5C), le mode "pas de mouvement" choisi est Eco (18.5C) et la temporisation du mouvement est de 5 min.
- la pièce est vide depuis un moment (aucune activité détectée), la température de cette pièce est de 18,5 C
- quelqu'un entre dans la pièce, une activité est détectée la température est fixée à 21,5 C
- la personne quitte la chambre, au bout de 5 min la température est ramenée à 18,5 C
- nous avons une pièce avec un thermostat réglé en mode activité, le mode "mouvement" choisi est confort (21,5°C), le mode "pas de mouvement" choisi est Eco (18.5°C) et la temporisation du mouvement est de 30 sec lors de la détection et de 5 minutes sur fin de détection.
- la pièce est vide depuis un moment (aucune activité détectée), la température de cette pièce est de 18,5°
- quelqu'un entre dans la pièce, une activité est détectée si le mouvement est présent pendant au moins 30 sec. La température passe alors à 21,5°
- si le mouvement est présent pendant moins de 30 sec (passage rapide), la température reste sur 18,5°,
- imaginons que la température soit passée sur 21,5°, lorsque la personne quitte la pièce, au bout de 5 min la température est ramenée à 18,5°.
- si la personne revient avant les 5 minutes, la température reste sur 21,5°
Pour que cela fonctionne, le thermostat climatique doit être en mode préréglé « Activité ».
Pour que cela fonctionne, le thermostat doit être en mode préréglé « Activité ».
> ![Astuce](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/tips.png?raw=true) _*Notes*_
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
@@ -213,16 +429,17 @@ Si vous avez choisi la fonctionnalité ```Avec détection de la puissance```, cl
![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/config-power.png?raw=true)
Cette fonction vous permet de réguler la consommation électrique de vos radiateurs. Connue sous le nom de délestage, cette fonction vous permet de limiter la consommation électrique de votre appareil de chauffage si des conditions de surpuissance sont détectées. Donnez un **capteur à la consommation électrique actuelle de votre maison**, un **capteur à la puissance max** qu'il ne faut pas dépasser, la **consommation électrique de votre chauffage** et l'algorithme ne démarrera pas un radiateur si la puissance maximale sera dépassée après le démarrage du radiateur.
Cette fonction vous permet de réguler la consommation électrique de vos radiateurs. Connue sous le nom de délestage, cette fonction vous permet de limiter la consommation électrique de votre appareil de chauffage si des conditions de surpuissance sont détectées. Donnez un **capteur à la consommation électrique actuelle de votre maison**, un **capteur à la puissance max** qu'il ne faut pas dépasser, la **consommation électrique de votre chauffage** (en étape 1 de la configuration) et l'algorithme ne démarrera pas un radiateur si la puissance maximale sera dépassée après le démarrage du radiateur.
Notez que toutes les valeurs de puissance doivent avoir les mêmes unités (kW ou W par exemple).
Cela vous permet de modifier la puissance maximale au fil du temps à l'aide d'un planificateur ou de ce que vous voulez.
> ![Astuce](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/tips.png?raw=true) _*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
> 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
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).
@@ -236,9 +453,11 @@ Pour cela, vous devez configurer :
3. La **température utilisée en Confort** préréglée en cas d'absence,
4. La **température utilisée en Boost** préréglée en cas d'absence
Si le mode AC est utilisé, vous pourrez aussi configurer les températures lorsque l'équipement en mode climatisation.
> ![Astuce](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/tips.png?raw=true) _*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.
> 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.
@@ -258,11 +477,77 @@ Le quatrième param§tre (``security_default_on_percent``) est la valeur de ``on
Voir [exemple de réglages](#examples-tuning) pour avoir des exemples de réglage communs
> ![Astuce](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/tips.png?raw=true) _*Notes*_
1. Lorsque le capteur de température viendra à la vie et renverra les températures, le préréglage sera restauré à sa valeur précédente,
3. Attention, deux températures sont nécessaires : la température interne et la température externe et chacune doit donner la température, sinon le thermostat sera en préréglage "security",
4. Un service est disponible qui permet de régler les 3 paramètres de sécurité. Ca peut servir à adapter la fonction de sécurité à votre usage,
5. Pour un usage naturel, le ``security_default_on_percent`` doit être inférieur à ``security_min_on_percent``,
6. Lorsqu'un thermostat de type ``thermostat_over_climate`` passe en mode ``security`` il est éteint. Les paramètres ``security_min_on_percent`` et ``security_default_on_percent`` ne sont alors pas utilisés.
> 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,
> 4. Pour un usage naturel, le ``security_default_on_percent`` doit être inférieur à ``security_min_on_percent``,
> 5. Les thermostats de type ``thermostat_over_climate`` ne sont pas concernés par le mode security.
## Synthèse des paramètres
| Paramètre | Libellé | "over switch" | "over climate" | over valve |
| - | - | - | - | - |
| ``name`` | Nom | X | X | X |
| ``thermostat_type`` | Type de thermostat | X | X | X |
| ``temperature_sensor_entity_id`` | Temperature sensor entity id | X | X (auto-regulation) | X |
| ``external_temperature_sensor_entity_id`` | Température de l'exterieur sensor entity id | X | X (auto-regulation) | X |
| ``cycle_min`` | Durée du cycle (minutes) | X | X | X |
| ``temp_min`` | Température minimale permise | X | X | X |
| ``temp_max`` | Température maximale permise | X | X | X |
| ``device_power`` | Puissance de l'équipement | X | X | X |
| ``use_window_feature`` | Avec détection des ouvertures | X | X | X |
| ``use_motion_feature`` | Avec détection de mouvement | X | X | X |
| ``use_power_feature`` | Avec gestion de la puissance | X | X | X |
| ``use_presence_feature`` | Avec détection de présence | X | X | X |
| ``heater_entity1_id`` | 1er radiateur | X | - | - |
| ``heater_entity2_id`` | 2ème radiateur | X | - | - |
| ``heater_entity3_id`` | 3ème radiateur | X | - | - |
| ``heater_entity4_id`` | 4ème radiateur | X | - | - |
| ``proportional_function`` | Algorithme | X | - | - |
| ``climate_entity1_id`` | Thermostat sous-jacent | - | X | - |
| ``climate_entity2_id`` | 2ème thermostat sous-jacent | - | X | - |
| ``climate_entity3_id`` | 3ème thermostat sous-jacent | - | X | - |
| ``climate_entity4_id`` | 4ème thermostat sous-jacent | - | X | - |
| ``valve_entity1_id`` | Vanne sous-jacente | - | - | X |
| ``valve_entity2_id`` | 2ème vanne sous-jacente | - | - | X |
| ``valve_entity3_id`` | 3ème vanne sous-jacente | - | - | X |
| ``valve_entity4_id`` | 4ème vanne sous-jacente | - | - | X |
| ``ac_mode`` | utilisation de l'air conditionné (AC) ? | X | X | X |
| ``tpi_coef_int`` | Coefficient à utiliser pour le delta de température interne | X | - | X |
| ``tpi_coef_ext`` | Coefficient à utiliser pour le delta de température externe | X | - | X |
| ``eco_temp`` | Température en preset Eco | X | X | X |
| ``comfort_temp`` | Température en preset Confort | X | X | X |
| ``boost_temp`` | Température en preset Boost | X | X | X |
| ``eco_ac_temp`` | Température en preset Eco en mode AC | X | X | X |
| ``comfort_ac_temp`` | Température en preset Confort en mode AC | X | X | X |
| ``boost_ac_temp`` | Température en preset Boost en mode AC | 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 |
| ``window_auto_open_threshold`` | Seuil haut de chute de température pour la détection automatique (en °/min) | X | X | X |
| ``window_auto_close_threshold`` | Seuil bas de chute de température pour la fin de détection automatique (en °/min) | X | X | X |
| ``window_auto_max_duration`` | Durée maximum d'une extinction automatique (en min) | X | X | X |
| ``motion_sensor_entity_id`` | Détecteur de mouvement entity id | X | X | X |
| ``motion_delay`` | Délai avant prise en compte du mouvement (seconds) | X | X | X |
| ``motion_off_delay`` | Délai avant prise en compte de la fin de mouvement (seconds) | X | X | X |
| ``motion_preset`` | Preset à utiliser si mouvement détecté | X | X | X |
| ``no_motion_preset`` | Preset à utiliser si pas de mouvement détecté | X | X | X |
| ``power_sensor_entity_id`` | Capteur de puissance totale (entity id) | X | X | X |
| ``max_power_sensor_entity_id`` | Capteur de puissance Max (entity id) | X | X | X |
| ``power_temp`` | Température si délestaqe | X | X | X |
| ``presence_sensor_entity_id`` | Capteur de présence entity id (true si quelqu'un est présent) | X | X | X |
| ``eco_away_temp`` | Température en preset Eco en cas d'absence | X | X | X |
| ``comfort_away_temp`` | Température en preset Comfort en cas d'absence | X | X | X |
| ``boost_away_temp`` | Température en preset Boost en cas d'absence | X | X | X |
| ``eco_ac_away_temp`` | Température en preset Eco en cas d'absence en mode AC | X | X | X |
| ``comfort_ac_away_temp`` | Température en preset Comfort en cas d'absence en mode AC | X | X | X |
| ``boost_ac_away_temp`` | Température en preset Boost en cas d'absence en mode AC | X | X | X |
| ``minimal_activation_delay`` | Délai minimal d'activation | X | - | - |
| ``security_delay_min`` | Délai maximal entre 2 mesures de températures | X | - | X |
| ``security_min_on_percent`` | Pourcentage minimal de puissance pour passer en mode sécurité | X | - | X |
| ``auto_regulation_mode`` | Le mode d'auto-régulation | - | X | - |
| ``auto_regulation_dtemp`` | La seuil d'auto-régulation | - | X | - |
| ``auto_regulation_period_min`` | La période minimale d'auto-régulation | - | X | - |
| ``inverse_switch_command`` | Inverse la commande du switch (pour switch avec fil pilote) | X | - | - |
# Exemples de réglage
@@ -313,23 +598,64 @@ Cette intégration utilise un algorithme proportionnel. Un algorithme proportion
Cet algorithme fait converger la température et arrête d'osciller.
## Algorithme TPI
L'algorithme TPI consiste à calculer à chaque cycle un pourcentage d'état On vs Off pour le radiateur en utilisant la température cible, la température actuelle dans la pièce et la température extérieure actuelle.
L'algorithme TPI consiste à calculer à chaque cycle un pourcentage d'état On vs Off pour le radiateur en utilisant la température cible, la température actuelle dans la pièce et la température extérieure actuelle. Cet algorithme n'est donc valable que pour les Versatile Thermostat qui régulent : `over_switch` et `over_valve`.
Le pourcentage est calculé avec cette formule :
on_percent = coef_int * (température cible - température actuelle) + coef_ext * (température cible - température extérieure)
Ensuite, faites 0 <= on_percent <= 1
Ensuite, l'algo fait en sorte que 0 <= on_percent <= 1
Les valeurs par défaut pour coef_int et coef_ext sont respectivement : ``0.6`` et ``0.01``. Ces valeurs par défaut conviennent à une pièce standard bien isolée.
Pour régler ces coefficients, gardez à l'esprit que :
1. **si la température cible n'est pas atteinte** après une situation stable, vous devez augmenter le ``coef_ext`` (le ``on_percent`` est trop élevé),
2. **si la température cible est dépassée** après une situation stable, vous devez diminuer le ``coef_ext`` (le ``on_percent`` est trop bas),
1. **si la température cible n'est pas atteinte** après une situation stable, vous devez augmenter le ``coef_ext`` (le ``on_percent`` est trop bas),
2. **si la température cible est dépassée** après une situation stable, vous devez diminuer le ``coef_ext`` (le ``on_percent`` est trop haut),
3. **si l'atteinte de la température cible est trop lente**, vous pouvez augmenter le ``coef_int`` pour donner plus de puissance au réchauffeur,
4. **si l'atteinte de la température cible est trop rapide et que des oscillations apparaissent** autour de la cible, vous pouvez diminuer le ``coef_int`` pour donner moins de puissance au radiateur
4. **si l'atteinte de la température cible est trop rapide et que des oscillations apparaissent** autour de la cible, vous pouvez diminuer le ``coef_int`` pour donner moins de puissance au radiateur.
En type `over_valve` le `on_percent` est ramené à une valeur entre 0 et 100% et sert directement à commander l'ouverture de la vanne.
Voir quelques situations à [examples](#some-results).
# Capteurs
Avec le thermostat sont disponibles des capteurs qui permettent de visualiser les alertes et l'état interne du thermostat. Ils sont disponibles dans les entités de l'appareil associé au thermostat :
![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/thermostat-sensors.png?raw=true)
Dans l'ordre, il y a :
1. l'entité principale climate de commande du thermostat,
2. l'énergie consommée par le thermostat (valeur qui s'incrémente en permanence),
3. l'heure de réception de la dernière température extérieure,
4. l'heure de réception de la dernière température intérieure,
5. la puissance moyenne de l'appareil sur le cycle (pour les TPI seulement),
6. le temps passé à l'état éteint dans le cycle (TPI seulement),
7. le temps passé à l'état allumé dans le cycle (TPI seulement),
8. l'état de délestage,
9. le pourcentage de puissance sur le cycle (TPI seulement),
10. l'état de présence (si la gestion de la présence est configurée),
11. l'état de sécurité,
12. l'état de l'ouverture (si la gestion des ouvertures est configurée),
13. l'état du mouvement (si la gestion du mouvements est configurée)
14. le pourcentage d'ouverture de la vanne (pour le type `over_valve`)
Pour colorer les capteurs, ajouter ces lignes et personnalisez les au besoin, dans votre configuration.yaml :
```
frontend:
themes:
versatile_thermostat_theme:
state-binary_sensor-safety-on-color: "#FF0B0B"
state-binary_sensor-power-on-color: "#FF0B0B"
state-binary_sensor-window-on-color: "rgb(156, 39, 176)"
state-binary_sensor-motion-on-color: "rgb(156, 39, 176)"
state-binary_sensor-presence-on-color: "lightgreen"
state-binary_sensor-running-on-color: "orange"
```
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)
# Services
Cette implémentation personnalisée offre des services spécifiques pour faciliter l'intégration avec d'autres composants Home Assistant.
@@ -339,7 +665,7 @@ Ce service permet de forcer l'état de présence indépendamment du capteur de p
Le code pour appeler ce service est le suivant :
```
service : thermostat_polyvalent.set_presence
service : versatile_thermostat.set_presence
Les données:
présence : "off"
cible:
@@ -354,7 +680,7 @@ Vous pouvez modifier l'une ou les deux températures (lorsqu'elles sont présent
Utilisez le code suivant pour régler la température du préréglage :
```
service : thermostat_polyvalent.set_preset_temperature
service : versatile_thermostat.set_preset_temperature
date:
preset : boost
temperature : 17,8
@@ -363,6 +689,17 @@ target:
entity_id : climate.my_thermostat
```
Ou pour changer le pré-réglage du mode Air Conditionné (AC) ajoutez un préfixe `_ac`` au nom du preset comme ceci :
```
service: versatile_thermostat.set_preset_temperature
data:
preset: boost_ac
temperature: 25
temperature_away: 30
target:
entity_id: climate.my_thermostat
```
> ![Astuce](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/tips.png?raw=true) _*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.
@@ -372,8 +709,8 @@ Si le thermostat est en mode ``security`` les nouveaux paramètres sont appliqu
Pour changer les paramètres de sécurité utilisez le code suivant :
```
service : thermostat_polyvalent.set_security
date:
service : versatile_thermostat.set_security
data:
min_on_percent: "0.5"
default_on_percent: "0.1"
delay_min: 60
@@ -381,9 +718,23 @@ target:
entity_id : climate.my_thermostat
```
## ByPass Window Check
Ce service permet d'activer ou non un bypass de la vérification des fenetres.
Il permet de continuer à chauffer même si la fenetre est detectée ouverte.
Mis à ``true`` les modifications de status de la fenetre n'auront plus d'effet sur le thermostat, remis à ``false`` cela s'assurera de désactiver le thermostat si la fenetre est toujours ouverte.
Pour changer le paramètre de bypass utilisez le code suivant :
```
service : versatile_thermostat.set_window_bypass
data:
window_bypass: true
target:
entity_id : climate.my_thermostat
```
# Notifications
Les évènements marquant du thermostat sont notifiés par l'intermédiaire du bus de message.
Les évènements notifiés sont les suivants:
Les évènements notifiés sont les suivants:
- ``versatile_thermostat_security_event`` : un thermostat entre ou sort du preset ``security``
- ``versatile_thermostat_power_event`` : un thermostat entre ou sort du preset ``power``
@@ -430,6 +781,7 @@ Les attributs personnalisés sont les suivants :
| ``saved_preset_mode`` | Le dernier preset utilisé avant le basculement automatique du preset |
| ``saved_target_temp`` | La dernière température utilisée avant la commutation automatique |
| ``window_state`` | Le dernier état connu du capteur de fenêtre. Aucun si la fenêtre n'est pas configurée |
| ``window_bypass_state`` | True si le bypass de la détection d'ouverture et activé |
| ``motion_state`` | Le dernier état connu du capteur de mouvement. Aucun si le mouvement n'est pas configuré |
| ``overpowering_state`` | Le dernier état connu du capteur surpuissant. Aucun si la gestion de l'alimentation n'est pas configurée |
| ``presence_state`` | Le dernier état connu du capteur de présence. Aucun si la gestion de présence n'est pas configurée |
@@ -443,6 +795,9 @@ Les attributs personnalisés sont les suivants :
| ``last_update_datetime`` | La date et l'heure au format ISO8866 de cet état |
| ``friendly_name`` | Le nom du thermostat |
| ``supported_features`` | Une combinaison de toutes les fonctionnalités prises en charge par ce thermostat. Voir la documentation officielle sur l'intégration climatique pour plus d'informations |
| ``valve_open_percent`` | Le pourcentage d'ouverture de la vanne |
| ``regulated_target_temperature`` | La température de consigne calculée par l'auto-régulation |
| ``is_inversed`` | True si la commande est inversée (fil pilote avec diode) |
# Quelques résultats
@@ -469,6 +824,11 @@ Enjoy !
# Encore mieux
## Bien mieux avec le Versatile Thermostat UI Card
Une carte spéciale pour le Versatile Thermostat a été développée (sur la base du Better Thermostat). Elle est dispo ici [Versatile Thermostat UI Card](https://github.com/jmcollin78/versatile-thermostat-ui-card) et propose une vision moderne de tous les status du VTherm :
![image](https://github.com/jmcollin78/versatile-thermostat-ui-card/blob/master/assets/1.png?raw=true)
## 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
@@ -524,6 +884,24 @@ Exemple de configuration :
name: Porte sam
```
Vous pouvez personnaliser ce composant à l'aide du composant HACS card-mod pour ajuster les couleurs des alertes. Exemple pour afficher en rouge les alertes sécurité et délestage :
```
card_mod:
style: |
{% if is_state('binary_sensor.thermostat_chambre_security_state', 'on') %}
ha-card .body .sensor-heading ha-icon[icon="mdi:alert-outline"] {
color: red;
}
{% endif %}
{% if is_state('binary_sensor.thermostat_chambre_overpowering_state', 'on') %}
ha-card .body .sensor-heading ha-icon[icon="mdi:flash"] {
color: red;
}
{% endif %}
```
![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/custom-css-thermostat.png?raw=true)
## Toujours mieux avec Apex-chart pour régler votre thermostat
Vous pouvez obtenir une courbe comme celle présentée dans [some results](#some-results) avec une sorte de configuration de graphique Apex uniquement en utilisant les attributs personnalisés du thermostat décrits [ici](#custom-attributes) :
@@ -556,11 +934,21 @@ series:
name: Current temp
curve: smooth
yaxis_id: left
- entity: climate.thermostat_mythermostat
- entity: climate.thermostat_mythermostat <--- for over_switch
attribute: on_percent
name: Power percent
curve: stepline
yaxis_id: right
- entity: climate.thermostat_mythermostat <--- for over_thermostast
attribute: regulated_target_temperature
name: Regulated temperature
curve: stepline
yaxis_id: left
- entity: climate.thermostat_mythermostat <--- for over_valve
attribute: valve_open_percent
name: Valve open percent
curve: stepline
yaxis_id: right
```
## Et toujours de mieux en mieux avec l'AappDaemon NOTIFIER pour notifier les évènements
@@ -655,10 +1043,87 @@ max: 30
Si vous souhaitez contribuer, veuillez lire les [directives de contribution](CONTRIBUTING.md)
# Dépannages
## Utilisation d'un Heatzy
L'utilisation d'un Heatzy est possible à la condition d'utiliser un switch virtuel sur ce modèle :
```
- platform: template
switches:
chauffage_sdb:
unique_id: chauffage_sdb
friendly_name: Chauffage salle de bain
value_template: "{{ is_state_attr('climate.salle_de_bain', 'preset_mode', 'comfort') }}"
icon_template: >-
{% if is_state_attr('climate.salle_de_bain', 'preset_mode', 'comfort') %}
mdi:radiator
{% elif is_state_attr('climate.salle_de_bain', 'preset_mode', 'away') %}
mdi:snowflake
{% else %}
mdi:radiator-disabled
{% endif %}
turn_on:
service: climate.set_preset_mode
entity_id: climate.salle_de_bain
data:
preset_mode: "comfort"
turn_off:
service: climate.set_preset_mode
entity_id: climate.salle_de_bain
data:
preset_mode: "eco"
```
Merci à @gael pour cet exemple.
## 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:
radiateur_soan:
friendly_name: radiateur_soan_inv
value_template: "{{ is_state('switch.radiateur_soan', 'off') }}"
turn_on:
service: switch.turn_off
data:
entity_id: switch.radiateur_soan
turn_off:
service: switch.turn_on
data:
entity_id: switch.radiateur_soan
icon_template: "{% if is_state('switch.radiateur_soan', 'on') %}mdi:radiator-disabled{% else %}mdi:radiator{% endif %}"
```
## 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)
## Régler les paramètres de détection d'ouverture de fenêtre en mode auto
Si vous n'arrivez pas à régler la fonction de détection des ouvertures en mode auto (cf. [auto](#le-mode-auto)), vous pouvez essayer de modifier les paramètres de l'algorithme de lissage de la température.
En effet, la détection automatique d'ouverture est basée sur le calcul de la pente de la température (slope). Pour éviter les artefacts due à un capteur de température imprécis, cette pente est calculée sur une température lissée avec un algorithme de lissage nommée Exponential Moving Average (Moyenne mobile exponentielle).
Cet algorithm possède 3 paramètres :
1. `lifecycle_sec` : la durée en secondes prise en compte pour le lissage. Plus elle est forte et plus le lissage sera important mais plus il y aura de délai de détection,
2. `max_alpha` : si deux mesures de température sont éloignées dans le temps, la deuxième aura un poid beaucoup fort. Le paramètre permet de limiter le poid d'une mesure qui arrive bien après la précédente. Cette valeur doit être comprise entre 0 et 1. Plus elle est faible et moins les valeurs éloignées sont prises en compte. La valeur par défaut est de 0,5. Cela fait que lorsqu'une nouvelle valeur de température ne pèsera jamais plus que la moitié de la moyenne mobile,
3. `precision` : le nombre de chiffre après la virgule conservée pour le calcul de la moyenne mobile.
Pour changer ses paramètres, il faut modifier le fichier `configuration.yaml` et ajouter la section suivante (les valeurs sont les valeurs par défaut):
```
versatile_thermostat:
short_ema_params:
max_alpha: 0.5
halflife_sec: 300
precision: 2
```
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.
***
[integration_blueprint]: https://github.com/custom-components/integration_blueprint
[versatile_thermostat]: https://github.com/jmcollin78/versatile_thermostat
[buymecoffee]: https://www.buymeacoffee.com/jmcollin78
[buymecoffeebadge]: https://img.shields.io/badge/Buy%20me%20a%20beer-%245-orange?style=for-the-badge&logo=buy-me-a-beer
[commits-shield]: https://img.shields.io/github/commit-activity/y/jmcollin78/versatile_thermostat.svg?style=for-the-badge
[commits]: https://github.com/jmcollin78/versatile_thermostat/commits/master
[hacs]: https://github.com/custom-components/hacs

591
README.md
View File

@@ -2,12 +2,16 @@
[![GitHub Activity][commits-shield]][commits]
[![License][license-shield]](LICENSE)
[![hacs][hacs_badge]][hacs]
[![BuyMeCoffee][buymecoffeebadge]][buymecoffee]
![Tip](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/icon.png?raw=true)
> ![Tip](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/tips.png?raw=true?raw=true) This thermostat integration aims to drastically simplify your automations around climate management. Because all classical events in climate are natively handled by the thermostat (nobody at home ?, activity detected in a room ?, window open ?, power shedding ?), you don't have to build over complicated scripts and automations to manage your climates ;-).
> ![Tip](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/tips.png?raw=true) This thermostat integration aims to drastically simplify your automations around climate management. Because all classical events in climate are natively handled by the thermostat (nobody at home ?, activity detected in a room ?, window open ?, power shedding ?), you don't have to build over complicated scripts and automations to manage your climates ;-).
- [Breaking changes in 4.0.0](#breaking-changes-in-400)
- [Thanks for the beer buymecoffee](#thanks-for-the-beer-buymecoffee)
- [When to use / not use](#when-to-use--not-use)
- [Incompatibilities](#incompatibilities)
- [Why another thermostat implementation ?](#why-another-thermostat-implementation-)
- [How to install this incredible Versatile Thermostat ?](#how-to-install-this-incredible-versatile-thermostat-)
- [HACS installation (recommended)](#hacs-installation-recommended)
@@ -15,13 +19,20 @@
- [Configuration](#configuration)
- [Minimal configuration update](#minimal-configuration-update)
- [Select the driven entity](#select-the-driven-entity)
- [For a ```thermostat_over_switch``` type thermostat](#for-a-thermostat_over_switch-type-thermostat)
- [For a thermostat of type ```thermostat_over_climate```:](#for-a-thermostat-of-type-thermostat_over_climate)
- [Self-regulation](#self-regulation)
- [Self-regulation in Expert mode](#self-regulation-in-expert-mode)
- [For a thermostat of type ```thermostat_over_valve```:](#for-a-thermostat-of-type-thermostat_over_valve)
- [Configure the TPI algorithm coefficients](#configure-the-tpi-algorithm-coefficients)
- [Configure the preset temperature](#configure-the-preset-temperature)
- [Configure the doors/windows turning on/off the thermostats](#configure-the-doorswindows-turning-onoff-the-thermostats)
- [The sensor mode](#the-sensor-mode)
- [Auto mode](#auto-mode)
- [Configure the activity mode or motion detection](#configure-the-activity-mode-or-motion-detection)
- [Configure the power management](#configure-the-power-management)
- [Configure the presence or occupancy](#configure-the-presence-or-occupancy)
- [Advanced configuration](#advanced-configuration)
- [Parameters synthesis](#parameters-synthesis)
- [Examples tuning](#examples-tuning)
- [Electrical heater](#electrical-heater)
- [Central heating (gaz or fuel heating system)](#central-heating-gaz-or-fuel-heating-system)
@@ -30,40 +41,83 @@
- [My preset configuration](#my-preset-configuration)
- [Algorithm](#algorithm)
- [TPI algorithm](#tpi-algorithm)
- [Sensors](#sensors)
- [Services](#services)
- [Force the presence / occupancy](#force-the-presence--occupancy)
- [Change the temperature of presets](#change-the-temperature-of-presets)
- [Change security settings](#change-security-settings)
- [ByPass Window Check](#bypass-window-check)
- [Notifications](#notifications)
- [Custom attributes](#custom-attributes)
- [Some results](#some-results)
- [Even better](#even-better)
- [Much better with the Veersatile Thermostat UI Card](#much-better-with-the-veersatile-thermostat-ui-card)
- [Even Better with Scheduler Component !](#even-better-with-scheduler-component-)
- [Even-even better with custom:simple-thermostat front integration](#even-even-better-with-customsimple-thermostat-front-integration)
- [Even better with Apex-chart to tune your Thermostat](#even-better-with-apex-chart-to-tune-your-thermostat)
- [And always better and better with the NOTIFIER daemon app to notify events](#and-always-better-and-better-with-the-notifier-daemon-app-to-notify-events)
- [Contributions are welcome!](#contributions-are-welcome)
- [Troubleshooting](#troubleshooting)
- [Using a Heatzy](#using-a-heatzy)
- [Using a Heatsink with a Pilot Wire](#using-a-heatsink-with-a-pilot-wire)
- [Only the first radiator heats](#only-the-first-radiator-heats)
- [Adjust window opening detection parameters in auto mode](#adjust-window-opening-detection-parameters-in-auto-mode)
_Component developed by using the amazing development template [[blueprint](https://github.com/custom-components/integration_blueprint)]._
This custom component for Home Assistant is an upgrade and is a complete rewrite of the component "Awesome thermostat" (see [Github](https://github.com/dadge/awesome_thermostat)) with addition of features.
# When to use / not use
This thermostat can control 2 types of equipment:
1. a heater that only works in on/off mode (named ```thermostat_over_switch```). The minimum configuration required to use this type of thermostat is:
has. equipment such as a radiator (a ```switch``` or equivalent),
b. a temperature probe for the room (or an input_number),
vs. an external temperature sensor (think about weather integration if you don't have one)
2. another thermostat that has its own operating modes (named ```thermostat_over_climate```). For this type of thermostat, the minimum configuration requires:
has. equipment such as air conditioning which is controlled by its own ```climate``` type entity,
b. a temperature probe for the room (or an input_number),
vs. an external temperature sensor (think about weather integration if you don't have one)
>![New](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/new-icon.png?raw=true) _*News*_
> * **Release 4.2**: The calculation of the slope of the temperature curve is now done in °/hour and no longer in °/min [#242](https://github.com/jmcollin78/versatile_thermostat/ issues/242). Correction of automatic detection of openings by adding smoothing of the temperature curve.
> * **Release 4.1**: Added an **Expert** regulation mode in which the user can specify their own auto-regulation parameters instead of using the pre-programmed ones [#194]( https://github.com/jmcollin78/versatile_thermostat/issues/194).
> * **Release 4.0**: Added the support of **Versatile Thermostat UI Card**. See [Versatile Thermostat UI Card](https://github.com/jmcollin78/versatile-thermostat-ui-card). Added a **Slow** regulation mode for slow latency heating devices [#168](https://github.com/jmcollin78/versatile_thermostat/issues/168). Change the way **the power is calculated** in case of VTherm with multi-underlying equipements [#146](https://github.com/jmcollin78/versatile_thermostat/issues/146). Added the support of AC and Heat for VTherm over switch alse [#144](https://github.com/jmcollin78/versatile_thermostat/pull/144)
> * **Release 3.8**: Added a **self-regulation function** for `over climate` thermostats whose regulation is done by the underlying climate. See [Self-regulation](#self-regulation) and [#129](https://github.com/jmcollin78/versatile_thermostat/issues/129). Added the possibility of **inverting the command** for an `over switch` thermostat to address installations with pilot wire and diode [#124](https://github.com/jmcollin78/versatile_thermostat/issues/124).
> * **Release 3.7**: Addition of the **Versatile Thermostat type `over valve`** to control a TRV valve directly or any other dimmer type equipment for heating. Regulation is then done directly by acting on the opening percentage of the underlying entity: 0 the valve is cut off, 100: the valve is fully opened. See [#131](https://github.com/jmcollin78/versatile_thermostat/issues/131). Added a function allowing the bypass of opening detection [#138](https://github.com/jmcollin78/versatile_thermostat/issues/138). Added Slovak language
<details>
<summary>Others releases</summary>
The ```thermostat_over_climate``` type allows you to add all the functionality provided by VersatileThermostat to your existing equipment. The climate VersatileThermostat entity will control your climate entity, turning it off if the windows are open, switching it to Eco mode if no one is present, etc. See [here](#why-a-new-implementation-of-the-thermostat). For this type of thermostat, any heating cycles are controlled by the underlying climate entity and not by the Versatile Thermostat itself.
> * **Release 3.6**: Added the `motion_off_delay` parameter to improve motion management [#116](https://github.com/jmcollin78/versatile_thermostat/issues/116), [#128](https://github.com/jmcollin78/versatile_thermostat/issues/128). Added AC (air conditioning) mode for a VTherm over switch. Preparing the Github project to facilitate contributions [#127](https://github.com/jmcollin78/versatile_thermostat/issues/127)
> * **Release 3.5**: Multiple thermostats when using "thermostat over another thermostat" mode [#113](https://github.com/jmcollin78/versatile_thermostat/issues/113)
> * **Release 3.4**: bug fixes and expose preset temperatures for AC mode [#103](https://github.com/jmcollin78/versatile_thermostat/issues/103)
> * **Release 3.3**: add the Air Conditionned mode (AC). This feature allow to use the eventual AC mode of your underlying climate entity. You have to check the "Use AC mode" checkbox in configuration and give preset temperature value for AC mode and AC mode when absent if absence is configured
> * **Release 3.2**: add the ability to control multiple switches from the same thermostat. In this mode, the switches are triggered with a delay to minimize the power required at one time (we minimize the recovery periods). See [Configuration](#select-the-driven-entity)
> * **Release 3.1**: added detection of open windows/doors by temperature drop. This new function makes it possible to automatically stop a radiator when the temperature drops suddenly. See [Auto mode](#auto-mode)
> * **Major release 3.0**: addition of thermostat equipment and associated sensors (binary and non-binary). Much closer to the Home Assistant philosophy, you now have direct access to the energy consumed by the radiator controlled by the thermostat and many other sensors that will be useful in your automations and dashboard.
> * **release 2.3**: addition of the power and energy measurement of the radiator controlled by the thermostat.
> * **release 2.2**: addition of a safety function allowing a radiator not to be left heating forever in the event of a thermometer failure
> * **major release 2.0**: addition of the "over climate" thermostat allowing you to transform any thermostat into a Versatile Thermostat and add all the functions of the latter.
</details>
# Breaking changes in 4.0.0
1. The power of the device should now be the total power of all controler devices by the VTherm. This allow to have eterogeneous equipment with different power. In case of multi-devices controlled by a single VTherm you will have to edit and change the `device_power` value. Set the total power of all devices.
2. The threshold for auto window auto detection should be specified in °/hour and no more in °/min. To keep the same parameters you have to multiply the configured value by 60.
# Thanks for the beer [buymecoffee](https://www.buymeacoffee.com/jmcollin78)
Many thanks to @salabur, @pvince83, @bergoglio, @EPicLURcher, @ecolorado66, @Kriss1670, @maia, @f.maymil, @moutte69, @Jerome, @Gunnar M, @Greg.o, @John Burgess for the beers. It's very nice and encourages me to continue!
# When to use / not use
This thermostat can control 3 types of equipment:
1. a radiator that only operates in on/off mode (called ``thermostat_over_switch```). The minimum configuration necessary to use this type thermostat is:
1. equipment such as a radiator (a ``switch``` or equivalent),
2. a temperature probe for the room (or an input_number),
3. an outdoor temperature sensor (consider weather integration if you don't have one)
2. another thermostat which has its own operating modes (named ``thermostat_over_climate```). For this type of thermostat the minimum configuration requires:
1. equipment - such as air conditioning, a thermostatic valve - which is controlled by its own ``climate'' type entity,
3. equipment which can take a value from 0 to 100% (called `thermostat_over_valve`). At 0 the heating is cut off, 100% it is fully opened. This type allows you to control a thermostatic valve (see Shelly valve) which exposes an entity of type `number.` allowing you to directly control the opening of the valve. Versatile Thermostat regulates the room temperature by adjusting the opening percentage, using the interior and exterior temperature sensors using the TPI algorithm described below.
The ```thermostat_over_climate``` type allows you to add all the functionality provided by VersatileThermostat to your existing equipment. The climate VersatileThermostat entity will control your existing climate entity, turning it off if the windows are open, switching it to Eco mode if no one is present, etc. See [here](#why-a-new-implementation-of-the-thermostat). For this type of thermostat, any heating cycles are controlled by the underlying climate entity and not by the Versatile Thermostat itself. An optional self-regulation function allows the Versatile Thermostat to adjust the temperature given as a setpoint to the underlying in order to reach the setpoint.
Installations with pilot wire and activation diode benefit from an option which allows the on/off control of the underlying radiator to be reversed. To do this, use the `over switch` type and check the command inversion option.
## Incompatibilities
Some TRV type thermostats are known to be incompatible with the Versatile Thermostat. This is the case for the following valves:
1. Danfoss POPP valves with temperature feedback. It is impossible to turn off this valve and it self-regulates, causing conflicts with the VTherm,
2. "Homematic" (and possible Homematic IP) thermostats are known to have problems with Versatile Thermostats because of limitations of the underlying RF protocol. This problem especially occurs when trying to control several Homematic thermostats at once in one Versatile Thermostat instance. In order to reduce duty cycle load, you may e.g. group thermostats with Homematic-specific procedures (e.g. using a wall thermostat) and let Versatile Thermostat only control the wall thermostat directly. Another option is to control only one thermostat and propagate the changes in HVAC mode and temperature by an automation.
3. Thermostat of type Heatzy which doesn't supports the set_temperature command.
4. Thermostats of type Rointe tends to awake alone even if VTherm turns it off. Others functions works fine.
# Why another thermostat implementation ?
For my personnal usage, I needed to add a couple of features and also to update the behavior that implemented in the previous component "Awesome thermostat".
This component named __Versatile thermostat__ manage the following use cases :
- Configuration through standard integration GUI (using Config Entry flow),
- Full uses of **presets mode**,
@@ -74,13 +128,14 @@ This component named __Versatile thermostat__ manage the following use cases :
- Add **power shedding management** or regulation to avoid exceeding a defined total power. When max power is exceeded, a hidden 'power' preset is set on the climate entity. When power goes below the max, the previous preset is restored.
- Add **home presence management**. This feature allows you to dynamically change the temperature of preset considering a occupancy sensor of your home.
- Add **services to interact with the thermostat** from others integration: you can force the presence / un-presence using a service, and you can dynamically change the temperature of the presets and change dynamically the security parameters.
- Add sensors to see the internal states of the thermostat
# How to install this incredible Versatile Thermostat ?
## HACS installation (recommended)
1. Install [HACS](https://hacs.xyz/). That way you get updates automatically.
2. Add this Github repository as custom repository in HACS settings.
2. The Versatile Thermostat integration is now offered directly from the HACF interface (integration tab).
3. search and install "Versatile Thermostat" in HACS and click `install`.
4. Restart Home Assistant,
5. Then you can add an Versatile Thermostat integration in the integration page. You add as many Versatile Thermostat that you need (typically one per heater that should be managed)
@@ -112,29 +167,159 @@ Then follow the configurations steps as follow:
Give the main mandatory attributes:
1. a name (will be the name of the integration and also the name of the climate entity)
2. the type of thermostat ```thermostat_over_switch``` to control a radiator controlled by a switch or ```thermostat_over_climate``` to control another thermostat. Cf. [above](#why-a-new-thermostat-implementation)
2. the thermostat type ```thermostat_over_switch``` to control a radiator controlled by a switch or ```thermostat_over_climate``` to control another thermostat, or ```thermostat_over_valve``` Cf. [above](# why-a-new-thermostat-implementation)
4. a temperature sensor entity identifier which gives the temperature of the room in which the radiator is installed,
5. a temperature sensor entity giving the outside temperature. If you don't have an external sensor, you can use local weather integration
6. a cycle duration in minutes. On each cycle, the heater will cycle on and then off for a calculated time to reach the target temperature (see [preset](#configure-the-preset-temperature) below),
6. a cycle duration in minutes. On each cycle, the heater will cycle on and then off for a calculated time to reach the target temperature (see [preset](#configure-the-preset-temperature) below). In ```over_climate``` mode, the cycle is only used to carry out basic controls but does not directly regulate the temperature. It's the underlying climate that does it,
7. minimum and maximum thermostat temperatures,
8. the list of features that will be used for this thermostat. Depending on your choices, the following configuration screens will appear or not.
8. the power of the l'équipement which will activate the power and energy sensors of the device,
9. the list of features that will be used for this thermostat. Depending on your choices, the following configuration screens will appear or not.
> ![Tip](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/tips.png?raw=true) _*Notes*_
1. With the ```thermostat_over_switch``` type, calculation are done at each cycle. So in case of conditions change, you will have to wait for the next cycle to see a change. For this reason, the cycle should not be too long. **5 min is a good value**,
2. if the cycle is too short, the heater could never reach the target temperature indeed for heater with accumulation features and it will be unnecessary solicited
> 1. With the ```thermostat_over_switch``` type, calculation are done at each cycle. So in case of conditions change, you will have to wait for the next cycle to see a change. For this reason, the cycle should not be too long. **5 min is a good value**,
> 2. if the cycle is too short, the heater could never reach the target temperature. For the storage radiator for example it will be used unnecessarily.
## Select the driven entity
Depending on your choice on the type of thermostat, you will have to choose a switch type entity or a climate type entity. Only compatible entities are shown.
Depending on your choice of thermostat type, you will need to choose one or more `switch`, `climate` or `number` type entities. Only entities compatible with the type are presented.
For a ```thermostat_over_switch``` thermostat:
> ![Tip](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/tips.png?raw=true) _*How to choose the type*_
> The choice of type is important. Even if it is always possible to modify it afterwards via the configuration HMI, it is preferable to ask yourself the following few questions:
> 1. **what type of equipment am I going to pilot?** In order, here is what to do:
> 1. if you have a thermostatic valve (TRV) that can be controlled in Home Assistant via a ```number``` type entity (for example a _Shelly TRV_), choose the `over_valve` type. It is the most direct type and which ensures the best regulation,
> 2. if you have an electric radiator (with or without pilot wire) and a ```switch``` type entity allows you to turn it on or off, then the ```over_switch``` type is preferable. Regulation will be done by the Versatile Thermostat according to the temperature measured by your thermometer, where you have placed it,
> 3. in all other cases, use the ```over_climate``` mode. You keep your original `climate` entity and the Versatile Thermostat "only" controls the on/off and the target temperature of your original thermostat. Regulation is done by your original thermostat in this case. This mode is particularly suitable for all-in-one reversible air conditioning systems whose exposure in Home Assistant is limited to a `climate` type entity.
> 2. **what type of regulation do I want?** If the controlled equipment has its own regulation mechanism (air conditioning, certain TRV valve) and this regulation works well, opt for an ``over_climate```
It is possible to choose an over switch thermostat which controls air conditioning by checking the "AC Mode" box. In this case, only the cooling mode will be visible.
### For a ```thermostat_over_switch``` type thermostat
![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/config-linked-entity.png?raw=true)
The algorithm to be used today is limited to TPI is available. See [algorithm](#algorithm)
The algorithm to use is currently limited to TPI is available. See [algorithm](#algorithm).
If several type entities are configured, the thermostat shifts the activations in order to minimize the number of switches active at a time t. This allows for better power distribution since each radiator will turn on in turn.
Example of synchronized triggering:
![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/multi-switch-activation.png?raw=true)
For a ```thermostat_over_climate``` thermostat:
It is possible to choose an over switch thermostat which controls air conditioning by checking the "AC Mode" box. In this case, only the cooling mode will be visible.
If your equipment is controlled by a pilot wire with a diode, you will certainly need to check the "Invert Check" box. It allows you to set the switch to On when you need to turn the equipment off and to Off when you need to turn it on.
### For a thermostat of type ```thermostat_over_climate```:
![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/config-linked-entity2.png?raw=true)
It is possible to choose an over climate thermostat which controls reversible air conditioning by checking the “AC Mode” box. In this case, depending on the equipment ordered, you will have access to heating and/or cooling.
#### Self-regulation
Since release 3.8, you have the possibility to activate the self-regulation function. This function allows VersatileThermostat to adapt the temperature setpoint given to the underlying climate so that the room temperature actually reaches the setpoint.
To do this, the VersatileThermostat calculates an offset based on the following information:
1. the current difference between the actual temperature and the set temperature,
2. the accumulation of past differences,
3. the difference between the outside temperature and the setpoint
These three pieces of information are combined to calculate the offset which will be added to the current setpoint and sent to the underlying climate.
The self-regulation function is configured with:
1. a degree of regulation:
1. Light - for low self-regulation needs. In this mode, the maximum offset will be 1.5°,
2. Medium - for average self-regulation. A maximum offset of 2° is possible in this mode,
3. Strong - for a strong need for self-regulation. The maximum offset is 3° in this mode and the auto-regulation will react strongly to temperature changes.
2. A self-regulation threshold: value below which new regulation will not be applied. Let us imagine that at a time t, the offset is 2°. If in the next calculation, the offset is 2.4°, it will not be applied. It will only be applied that the difference between 2 offsets will be at least equal to this threshold,
3. Minimum period between 2 self-regulation changes: this number, expressed in minutes, indicates the duration between 2 regulation changes.
These three parameters make it possible to modulate the regulation and avoid multiplying the regulation sendings. Some equipment such as TRVs and boilers do not like the temperature setpoint to be changed too often.
> ![Tip](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/tips.png?raw=true) _*Implementation tip*_
> 1. Do not start self-regulation straight away. Watch how the natural regulation of your equipment works. If you notice that the set temperature is not reached or that it is taking too long to be reached, start the regulation,
> 2. First start with a slight self-regulation and keep both parameters at their default values. Wait a few days and check if the situation has improved,
> 3. If this is not sufficient, switch to Medium self-regulation, wait for stabilization,
> 4. If this is still not sufficient, switch to Strong self-regulation,
> 5. If it is still not good, you will have to switch to expert mode to be able to finely adjust the regulation parameters. See below.
Self-regulation consists of forcing the equipment to go further by forcing its set temperature regularly. Its consumption can therefore be increased, as well as its wear.
#### Self-regulation in Expert mode
In **Expert** mode you can finely adjust the auto-regulation parameters to achieve your objectives and optimize as best as possible. The algorithm calculates the difference between the setpoint and the actual temperature of the room. This discrepancy is called error.
The adjustable parameters are as follows:
1. `kp`: the factor applied to the raw error,
2. `ki`: the factor applied to the accumulation of errors,
3. `k_ext`: the factor applied to the difference between the interior temperature and the exterior temperature,
4. `offset_max`: the maximum correction (offset) that the regulation can apply,
5. `stabilization_threshold`: a stabilization threshold which, when reached by the error, resets the accumulation of errors to 0,
6. `accumulated_error_threshold`: the maximum for error accumulation.
For tuning, these observations must be taken into account:
1. `kp * error` will give the offset linked to the raw error. This offset is directly proportional to the error and will be 0 when the target is reached,
2. the accumulation of the error makes it possible to correct the stabilization of the curve while there remains an error. The error accumulates and the offset therefore gradually increases which should eventually stabilize at the target temperature. For this fundamental parameter to have an effect it must not be too small. An average value is 30
3. `ki * accumulated_error_threshold` will give the maximum offset linked to the accumulation of the error,
4. `k_ext` allows a correction to be applied immediately (without waiting for errors to accumulate) when the outside temperature is very different from the target temperature. If the stabilization is done too high when the temperature differences are significant, it is because this parameter is too high. It should be possible to cancel completely to let the first 2 offsets take place
The pre-programmed values are as follows:
Slow régulation :
kp: 0.2 # 20% of the current internal regulation offset are caused by the current difference of target temperature and room temperature
ki: 0.8 / 288.0 # 80% of the current internal regulation offset are caused by the average offset of the past 24 hours
k_ext: 1.0 / 25.0 # this will add 1°C to the offset when it's 25°C colder outdoor than indoor
offset_max: 2.0 # limit to a final offset of -2°C to +2°C
stabilization_threshold: 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: 2.0 * 288 # this allows up to 2°C long term offset in both directions
Light régulation :
kp: 0.2
ki: 0.05
k_ext: 0.05
offset_max: 1.5
stabilization_threshold: 0.1
accumulated_error_threshold: 10
Medium régulation :
kp: 0.3
ki: 0.05
k_ext: 0.1
offset_max: 2
stabilization_threshold: 0.1
accumulated_error_threshold: 20
Strong régulation :
"""Strong parameters for regulation
A set of parameters which doesn't take into account the external temp
and concentrate to internal temp error + accumulated error.
This should work for cold external conditions which else generates
high external_offset"""
kp: 0.4
ki: 0.08
k_ext: 0.0
offset_max: 5
stabilization_threshold: 0.1
accumulated_error_threshold: 50
To use Expert mode you must declare the values you want to use for each of these parameters in your `configuration.yaml` in the following form:
```
versatile_thermostat:
auto_regulation_expert:
kp: 0.4
ki: 0.08
k_ext: 0.0
offset_max: 5
stabilization_threshold: 0.1
accumulated_error_threshold: 50
```
and of course, configure the VTherm's self-regulation mode in **Expert** mode. All VTherms in Expert mode will use these same settings.
For the changes to be taken into account, you must either **completely restart Home Assistant** or just the **Versatile Thermostat integration** (Dev tools / Yaml / reloading the configuration / Versatile Thermostat).
### For a thermostat of type ```thermostat_over_valve```:
![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/config-linked-entity3.png?raw=true)
You can choose up to domain entity ```number``` or ```ìnput_number``` which will control the valves.
The algorithm to use is currently limited to TPI is available. See [algorithm](#algorithm).
It is possible to choose an over valve thermostat which controls air conditioning by checking the "AC Mode" box. In this case, only the cooling mode will be visible.
## Configure the TPI algorithm coefficients
Click on 'Validate' on the previous page and you will get there:
click on 'Validate' on the previous page, and if you choose a ```over_switch``` or ```over_valve``` thermostat and you will get there:
![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/config-tpi.png?raw=true)
For more informations on the TPI algorithm and tuned please refer to [algorithm](#algorithm).
@@ -148,28 +333,56 @@ The preset mode allows you to pre-configurate targeted temperature. Used in conj
- **Comfort** : device is in comfort mode
- **Boost** : device turn all valve full up
If AC mode is used, you will also be able to configure temperatures when the equipment is in cooling mode.
**None** is always added in the list of modes, as it is a way to not use the presets modes but a **manual temperature** instead.
> ![Tip](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/tips.png?raw=true) _*Notes*_
1. Changing manually the target temperature, set the preset to None (no preset). This way you can always set a target temperature even if no preset are available.
2. standard ``Away`` preset is a hidden preset which is not directly selectable. Versatile Thermostat uses the presence management or movement management to set automatically and dynamically the target temperature depending on a presence in the home or an activity in the room. See [presence management](#configure-the-presence-management).
3. if you uses the power shedding management, you will see a hidden preset named ``power``. The heater preset is set to ``power`` when overpowering conditions are encountered and shedding is active for this heater. See [power management](#configure-the-power-management).
4. if you uses the advanced configuration you will see the preset set to ``security`` if the temperature could not be retrieved after a certain delay
5. ff you don't want to use the preseet, give 0 as temperature. The preset will then been ignored and will not displayed in the front component
> 1. Changing manually the target temperature, set the preset to None (no preset). This way you can always set a target temperature even if no preset are available.
> 2. standard ``Away`` preset is a hidden preset which is not directly selectable. Versatile Thermostat uses the presence management or movement management to set automatically and dynamically the target temperature depending on a presence in the home or an activity in the room. See [presence management](#configure-the-presence-management).
> 3. if you uses the power shedding management, you will see a hidden preset named ``power``. The heater preset is set to ``power`` when overpowering conditions are encountered and shedding is active for this heater. See [power management](#configure-the-power-management).
> 4. if you uses the advanced configuration you will see the preset set to ``security`` if the temperature could not be retrieved after a certain delay
> 5. ff you don't want to use the preseet, give 0 as temperature. The preset will then been ignored and will not displayed in the front component
## Configure the doors/windows turning on/off the thermostats
If you choose the ```Window management``` feature, click on 'Validate' on the previous page and you will get there:
![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/config-window.png?raw=true)
You must have chosen the ```With opening detection``` feature on the first page to arrive on this page.
The detection of openings can be done in 2 ways:
1. either with a sensor placed on the opening (sensor mode),
2. either by detecting a sudden drop in temperature (auto mode)
Give the following attributes:
1. an entity id of a **window/door sensor**. This should be a binary_sensor or a input_boolean. The state of the entity should be 'on' when the window is open or 'off' when closed
2. a **delay in seconds** before any change. This allow to quickly open a window without stopping the heater.
### The sensor mode
In sensor mode, you must fill in the following information:
![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/config-window-sensor.png?raw=true)
And that's it ! your thermostat will turn off when the windows is open and be turned back on when it's closed afer the delay.
1. an entity ID of a **window/door sensor**. It should be a binary_sensor or an input_boolean. The state of the entity must be 'on' when the window is open or 'off' when it is closed
2. a **delay in seconds** before any change. This allows a window to be opened quickly without stopping the heating.
> ![Tip](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/tips.png?raw=true) _*Notes*_
1. If you want to use **several door/windows sensors** to automatize your thermostat, just create a group with the regular behavior (https://www.home-assistant.io/integrations/binary_sensor.group/)
2. If you don't have any window/door sensor in your room, just leave the sensor entity id empty
### Auto mode
In auto mode, the configuration is as follows:
![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/config-window-auto.png?raw=true)
1. a detection threshold in degrees per minute. When the temperature drops below this threshold, the thermostat will turn off. The lower this value, the faster the detection will be (in return for a risk of false positives),
2. an end of detection threshold in degrees per minute. When the temperature drop goes above this value, the thermostat will go back to the previous mode (mode and preset),
3. maximum detection time. Beyond this time, the thermostat will return to its previous mode and preset even if the temperature continues to drop.
To set the thresholds it is advisable to start with the reference values and adjust the detection thresholds. A few tries gave me the following values (for a desktop):
- detection threshold: 0.05°C/min
- non-detection threshold: 0 °C/min
- maximum duration: 60 min.
A new "slope" sensor has been added for all thermostats. It gives the slope of the temperature curve in °C/min (or °K/min). This slope is smoothed and filtered to avoid aberrant values from the thermometers which would interfere with the measurement.
![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/temperature-slope.png?raw=true)
To properly adjust it is advisable to display on the same historical graph the temperature curve and the slope of the curve (the "slope"):
![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/window-auto-tuning.png?raw=true)
And that's all ! your thermostat will turn off when the windows are open and turn back on when closed.
> ![Tip](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/tips.png?raw=true) _*Notes*_
> 1. If you want to use **multiple door/window sensors** to automate your thermostat, just create a group with the usual behavior (https://www.home-assistant.io/integrations/binary_sensor.group/)
> 2. If you don't have a window/door sensor in your room, just leave the sensor entity id blank,
> 3. **Only one mode is allowed**. You cannot configure a thermostat with a sensor and automatic detection. The 2 modes may contradict each other, it is not possible to have the 2 modes at the same time,
> 4. It is not recommended to use the automatic mode for equipment subject to frequent and normal temperature variations (corridors, open areas, ...)
## Configure the activity mode or motion detection
If you choose the ```Motion management``` feature, lick on 'Validate' on the previous page and you will get there:
@@ -179,38 +392,40 @@ We will now see how to configure the new Activity mode.
What we need:
- a **motion sensor**. The entity id of a motion sensor. Motion sensor states should be 'on' (motion detected) or 'off' (no motion detected)
- a **motion delay** (in seconds) duration defining how long we wait for motion confirmation before considering the motion
- a **end of motion delay** (in seconds) duration defining how long we wait for end of motion confirmation before considering the end of motion
- a **target "motion" preset**. We will used the temperature of this preset when an activity is detected.
- a **target "no motion" preset**. We will used the temperature of this second preset when no activity is detected.
So imagine we want to have the following behavior :
- we have room with a thermostat set in activity mode, the "motion" mode chosen is comfort (21.5C), the "no motion" mode chosen is Eco (18.5 C) and the motion delay is 5 min.
- the room is empty for a while (no activity detected), the temperature of this room is 18.5 C
- somebody enters into the room, an activity is detected the temperature is set to 21.5 C
- the person leaves the room, after 5 min the temperature is set back to 18.5 C
- we have a room with a thermostat set to activity mode, the "movement" mode chosen is comfort (21.5°C), the "no movement" mode chosen is Eco (18.5°C) and the movement delay is 30 sec during detection and 5 minutes at the end of detection.
- the room has been empty for a while (no activity detected), the temperature of this room is 18.5°
- someone enters the room, activity is detected if movement is present for at least 30 seconds. The temperature then rises to 21.5°
- if the movement is present for less than 30 seconds (rapid passage), the temperature remains at 18.5°,
- imagine that the temperature has risen to 21.5°, when the person leaves the room, after 5 minutes the temperature is reduced to 18.5°.
- if the person returns before 5 minutes, the temperature remains at 21.5°
For this to work, the climate thermostat should be in ``Activity`` preset mode.
> ![Tip](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/tips.png?raw=true) _*Notes*_
1. Be aware that as for the others preset modes, ``Activity`` will only be proposed if it's correctly configure. In other words, the 4 configuration keys have to be set if you want to see Activity in home assistant Interface
> 1. Be aware that as for the others preset modes, ``Activity`` will only be proposed if it's correctly configure. In other words, the 4 configuration keys have to be set if you want to see Activity in home assistant Interface
## Configure the power management
If you choose the ```Power management``` feature, click on 'Validate' on the previous page and you will get there:
![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/config-power.png?raw=true)
This feature allows you to regulate the power consumption of your radiators. Known as shedding, this feature allows you to limit the electrical power consumption of your heater if overpowering conditions are detected. Give a **sensor to the current power consumption of your house**, a **sensor to the max power** that should not be exceeded, the **power consumption of your heater** and the algorithm will not start a radiator if the max power will be exceeded after radiator starts.
This feature allows you to regulate the power consumption of your radiators. Known as shedding, this feature allows you to limit the electrical power consumption of your heater if overpowering conditions are detected. Give a **sensor to the current power consumption of your house**, a **sensor to the max power** that should not be exceeded, the **power consumption of your heater** (in the first step of the configuration) and the algorithm will not start a radiator if the max power will be exceeded after radiator starts.
Note that all power values should have the same units (kW or W for example).
This allows you to change the max power along time using a Scheduler or whatever you like.
> ![Tip](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/tips.png?raw=true) _*Notes*_
1. When shedding is encountered, the heater is set to the preset named ``power``. This is a hidden preset, you cannot select it manually.
2. I use this to avoid exceeded the limit of my electrical power contract when an electrical vehicle is charging. This makes a kind of auto-regulation.
3. Always keep a margin, because max power can be briefly exceeded while waiting for the next cycle calculation typically or by not regulated equipement.
4. If you don't want to use this feature, just leave the entities id empty
> 1. When shedding is encountered, the heater is set to the preset named ``power``. This is a hidden preset, you cannot select it manually.
> 2. I use this to avoid exceeded the limit of my electrical power contract when an electrical vehicle is charging. This makes a kind of auto-regulation.
> 3. Always keep a margin, because max power can be briefly exceeded while waiting for the next cycle calculation typically or by not regulated equipement.
> 4. If you don't want to use this feature, just leave the entities id empty
> 5. If you control several heaters, the **power consumption of your heater** setup should be the sum of the power.
## Configure the presence or occupancy
If you choose the ```Presence management``` feature, this feature allows you to dynamically changes the temperature of all configured Versatile thermostat's presets when nobody is at home or when someone comes back home. For this, you have to configure the temperature that will be used for each preset when presence is off. When the occupancy sensor turns to off, those tempoeratures will be used. When it turns on again the "normal" temperature configured for the preset is used. See [preset management](#configure-the-preset-temperature).
To configure presence fills this form:
@@ -222,9 +437,11 @@ For this you need to configure:
3. The **temperature used in Comfort** preset when absent,
4. The **temperature used in Boost** preset when absent
Si le mode AC est utilisé, vous pourrez aussi configurer les températures lorsque l'équipement en mode climatisation.
> ![Tip](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/tips.png?raw=true) _*Notes*_
1. the switch of temperature is immediate and is reflected on the front component. The calculation will take the new target temperature into account at the next cycle calculation,
2. you can use direct person.xxxx sensor or group of sensors of Home Assistant. The presence sensor handles ``on`` or ``home`` states as present and ``off`` or ``not_home`` state as absent.
> 1. the switch of temperature is immediate and is reflected on the front component. The calculation will take the new target temperature into account at the next cycle calculation,
> 2. you can use direct person.xxxx sensor or group of sensors of Home Assistant. The presence sensor handles ``on`` or ``home`` states as present and ``off`` or ``not_home`` state as absent.
## Advanced configuration
Those parameters allows to fine tune the thermostat.
@@ -244,11 +461,78 @@ The fourth parameter (``security_default_on_percent``) is the ``on_percent`` val
See [example tuning](#examples-tuning) for common tuning examples
>![Tip](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/tips.png?raw=true) _*Notes*_
1. When the temperature sensor comes to life and returns the temperatures, the preset will be restored to its previous value,
3. Attention, two temperatures are needed: internal temperature and external temperature and each must give the temperature, otherwise the thermostat will be in "security" preset,
4. A service is available that allows you to set the 3 security parameters. This can be used to adapt the security function to your use.
5. For natural usage, the ``security_default_on_percent`` should be less than ``security_min_on_percent``,
6. When a ``thermostat_over_climate`` type thermostat goes into ``security`` mode it is turned off. The ``security_min_on_percent`` and ``security_default_on_percent`` parameters are then not used.
> 1. When the temperature sensor comes to life and returns the temperatures, the preset will be restored to its previous value,
> 2. Attention, two temperatures are needed: internal temperature and external temperature and each must give the temperature, otherwise the thermostat will be in "security" preset,
> 3. A service is available that allows you to set the 3 security parameters. This can be used to adapt the security function to your use.
> 4. For natural usage, the ``security_default_on_percent`` should be less than ``security_min_on_percent``,
> 5. Thermostat of type ``thermostat_over_climate`` are not concerned by the security feature.
## Parameters synthesis
| Paramètre | Libellé | "over switch" | "over climate" | "over valve" |
| ----------| --------| --- | --- | -- |
| ``name`` | Name | X | X | X |
| ``thermostat_type`` | Thermostat type | X | X | X |
| ``temperature_sensor_entity_id`` | Temperature sensor entity id | X | X (self-regulation) | X |
| ``external_temperature_sensor_entity_id`` | External temperature sensor entity id | X | X (self-regulation) | X |
| ``cycle_min`` | Cycle duration (minutes) | X | X | X |
| ``temp_min`` | Minimal temperature allowed | X | X | X |
| ``temp_max`` | Maximal temperature allowed | X | X | X |
| ``device_power`` | Device power | X | X | X |
| ``use_window_feature`` | Use window detection | X | X | X |
| ``use_motion_feature`` | Use motion detection | X | X | X |
| ``use_power_feature`` | Use power management | X | X | X |
| ``use_presence_feature`` | Use presence detection | X | X | X |
| ``heater_entity1_id`` | 1rst heater switch | X | - | - |
| ``heater_entity2_id`` | 2nd heater switch | X | - | - |
| ``heater_entity3_id`` | 3rd heater switch | X | - | - |
| ``heater_entity4_id`` | 4th heater switch | X | - | - |
| ``proportional_function`` | Algorithm | X | - | X |
| ``climate_entity1_id`` | 1rst underlying climate | - | X | - |
| ``climate_entity2_id`` | 2nd underlying climate | - | X | - |
| ``climate_entity3_id`` | 3rd underlying climate | - | X | - |
| ``climate_entity4_id`` | 4th underlying climate | - | X | - |
| ``valve_entity1_id`` | 1rst underlying valve | - | - | X |
| ``valve_entity2_id`` | 2nd underlying valve | - | - | X |
| ``valve_entity3_id`` | 3rd underlying valve | - | - | X |
| ``valve_entity4_id`` | 4th underlying valve | - | - | X |
| ``ac_mode`` | Use the Air Conditioning (AC) mode | X | X | X |
| ``tpi_coef_int`` | Coefficient to use for internal temperature delta | X | - | X |
| ``tpi_coef_ext`` | Coefficient to use for external temperature delta | X | - | X |
| ``eco_temp`` | Temperature in Eco preset | X | X | X |
| ``comfort_temp`` | Temperature in Comfort preset | X | X | X |
| ``boost_temp`` | Temperature in Boost preset | X | X | X |
| ``eco_ac_temp`` | Temperature in Eco preset for AC mode | X | X | X |
| ``comfort_ac_temp`` | Temperature in Comfort preset for AC mode | X | X | X |
| ``boost_ac_temp`` | Temperature in Boost preset for AC mode | X | X | X |
| ``window_sensor_entity_id`` | Window sensor entity id | X | X | X |
| ``window_delay`` | Window sensor delay (seconds) | X | X | X |
| ``window_auto_open_threshold`` | Temperature decrease threshold for automatic window open detection (in °/min) | X | X | X |
| ``window_auto_close_threshold`` | Temperature increase threshold for end of automatic detection (in °/min) | X | X | X |
| ``window_auto_max_duration`` | Maximum duration of automatic window open detection (in min) | X | X | X |
| ``motion_sensor_entity_id`` | Motion sensor entity id | X | X | X |
| ``motion_delay`` | Delay before considering the motion (seconds) | X | X | X |
| ``motion_off_delay`` | Delay before considering the end of motion (seconds) | X | X | X |
| ``motion_preset`` | Preset to use when motion is detected | X | X | X |
| ``no_motion_preset`` | Preset to use when no motion is detected | X | X | X |
| ``power_sensor_entity_id`` | Power sensor entity id | X | X | X |
| ``max_power_sensor_entity_id`` | Max power sensor entity id | X | X | X |
| ``power_temp`` | Temperature for Power shedding | X | X | X |
| ``presence_sensor_entity_id`` | Presence sensor entity id | X | X | X |
| ``eco_away_temp`` | Temperature in Eco preset when no presence | X | X | X |
| ``comfort_away_temp`` | Temperature in Comfort preset when no presence | X | X | X |
| ``boost_away_temp`` | Temperature in Boost preset when no presence | X | X | X |
| ``eco_ac_away_temp`` | Temperature in Eco preset when no presence in AC mode | X | X | X |
| ``comfort_ac_away_temp`` | Temperature in Comfort preset when no presence in AC mode | X | X | X |
| ``boost_ac_away_temp`` | Temperature in Boost preset when no presence in AC mode | X | X | X |
| ``minimal_activation_delay`` | Minimal activation delay | X | - | X |
| ``security_delay_min`` | Security delay (in minutes) | X | - | X |
| ``security_min_on_percent`` | Minimal power percent to enable security mode | X | - | X |
| ``security_default_on_percent`` | Power percent to use in security mode | X | - | X |
| ``auto_regulation_mode`` | Le mode d'auto-régulation | - | X | - |
| ``auto_regulation_dtemp`` | La seuil d'auto-régulation | - | X | - |
| ``auto_regulation_period_min`` | La période minimale d'auto-régulation | - | X | - |
| ``inverse_switch_command`` | Inverse the switch command (for pilot wire switch) | X | - | - |
# Examples tuning
@@ -299,7 +583,7 @@ This integration uses a proportional algorithm. A Proportional algorithm is usef
This algorithm make the temperature converge and stop oscillating.
## TPI algorithm
The TPI algorithm consist in the calculation at each cycle of a percentage of On state vs Off state for the heater using the target temperature, the current temperature in the room and the current external temperature.
The TPI algorithm consist in the calculation at each cycle of a percentage of On state vs Off state for the heater using the target temperature, the current temperature in the room and the current external temperature. This algorithm is therefore only valid for Versatile Thermostats which regulate: `over_switch` and `over_valve`.
The percentage is calculated with this formula:
@@ -309,13 +593,54 @@ The percentage is calculated with this formula:
Defaults values for coef_int and coef_ext are respectively: ``0.6`` and ``0.01``. Those defaults values are suitable for a standard well isolated room.
To tune those coefficients keep in mind that:
1. **if target temperature is not reach** after stable situation, you have to augment the ``coef_ext`` (the ``on_percent`` is too high),
2. **if target temperature is exceeded** after stable situation, you have to decrease the ``coef_ext`` (the ``on_percent`` is too low),
1. **if target temperature is not reach** after stable situation, you have to augment the ``coef_ext`` (the ``on_percent`` is too low),
2. **if target temperature is exceeded** after stable situation, you have to decrease the ``coef_ext`` (the ``on_percent`` is too high),
3. **if reaching the target temperature is too slow**, you can increase the ``coef_int`` to give more power to the heater,
4. **if reaching the target temperature is too fast and some oscillations appears** around the target, you can decrease the ``coef_int`` to give less power to the heater
In type `over_valve` the `on_percent` is reduced to a value between 0 and 100% and is used directly to control the opening of the valve.
See some situations at [examples](#some-results).
# Sensors
With the thermostat are available sensors that allow you to view the alerts and the internal status of the thermostat. They are available in the entities of the device associated with the thermostat:
![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/thermostat-sensors.png?raw=true)
In order, there are:
1. the main climate thermostat command entity,
2. the energy consumed by the thermostat (value which continuously increases),
3. the time of receipt of the last outside temperature,
4. the time of receipt of the last indoor temperature,
5. the average power of the device over the cycle (for TPIs only),
6. the time spent in the off state in the cycle (TPI only),
7. the time spent in the on state in the cycle (TPI only),
8. load shedding status,
9. cycle power percentage (TPI only),
10. presence status (if presence management is configured),
11. security status,
12. opening status (if opening management is configured),
13. motion status (if motion management is configured),
14. the valve opening percentage (for the `over_valve` type)
To color the sensors, add these lines and customize them as needed, in your configuration.yaml:
```
frontend:
themes:
versatile_thermostat_theme:
state-binary_sensor-safety-on-color: "#FF0B0B"
state-binary_sensor-power-on-color: "#FF0B0B"
state-binary_sensor-window-on-color: "rgb(156, 39, 176)"
state-binary_sensor-motion-on-color: "rgb(156, 39, 176)"
state-binary_sensor-presence-on-color: "lightgreen"
state-binary_sensor-running-on-color: "orange"
```
and choose the ```versatile_thermostat_theme``` theme in the panel configuration. You will get something that will look like this:
![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/colored-thermostat-sensors.png?raw=true)
# Services
This custom implementation offers some specific services to facilitate integration with others Home Assisstant components.
@@ -349,6 +674,17 @@ target:
entity_id: climate.my_thermostat
```
Or to change the preset of the AC mode, add _ac to the preset name like this:
```
service: versatile_thermostat.set_preset_temperature
data:
preset: boost_ac
temperature: 25
temperature_away: 30
target:
entity_id: climate.my_thermostat
```
> ![Tip](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/tips.png?raw=true) _*Notes*_
- after a restart the preset are resetted to the configured temperature. If you want your change to be permanent you should modify the temperature preset into the confguration of the integration.
@@ -359,13 +695,25 @@ If the thermostat is in ``security`` mode the new settings are applied immediate
To change the security settings use the following code:
```
service : thermostat_polyvalent.set_security
date:
data:
min_on_percent: "0.5"
default_on_percent: "0.1"
delay_min: 60
target:
entity_id : climate.my_thermostat
```
## ByPass Window Check
This service is used to bypass the window check implemented to stop thermostat when an open window is detected.
When set to ``true`` window event won't have any effect on the thermostat, when set back to ``false`` it will make sure to disable the thermostat if window is still open.
To change the bypass setting use the following code:
```
service : thermostat_polyvalent.set_window_bypass
data:
window_bypass: true
target:
entity_id : climate.my_thermostat
```
# Notifications
Significant thermostat events are notified via the message bus.
@@ -416,6 +764,7 @@ Custom attributes are the following:
| ``saved_preset_mode`` | The last preset used before automatic switch of the preset |
| ``saved_target_temp`` | The last temperature used before automatic switching |
| ``window_state`` | The last known state of the window sensor. None if window is not configured |
| ``window_bypass_state`` | True if the bypass of the window detection is activated |
| ``motion_state`` | The last known state of the motion sensor. None if motion is not configured |
| ``overpowering_state`` | The last known state of the overpowering sensor. None if power management is not configured |
| ``presence_state`` | The last known state of the presence sensor. None if presence management is not configured |
@@ -429,6 +778,9 @@ Custom attributes are the following:
| ``last_update_datetime`` | The date and time in ISO8866 format of this state |
| ``friendly_name`` | The name of the thermostat |
| ``supported_features`` | A combination of all features supported by this thermostat. See official climate integration documentation for more informations |
| ``valve_open_percent`` | The opening percentage of the valve |
| ``regulated_target_temperature`` | The self-regulated target temperature calculated |
| ``is_inversed`` | True if the command is inversed (pilot wire with diode) |
# Some results
@@ -455,6 +807,11 @@ Enjoy !
# Even better
## Much better with the Veersatile Thermostat UI Card
A special card for the Versatile Thermostat has been developed (based on the Better Thermostat). It is available here [Versatile Thermostat UI Card](https://github.com/jmcollin78/versatile-thermostat-ui-card) and offers a modern vision of all the VTherm statuses:
![image](https://github.com/jmcollin78/versatile-thermostat-ui-card/blob/master/assets/1.png?raw=true)
## Even Better with Scheduler Component !
In order to enjoy the full power of Versatile Thermostat, I invite you to use it with https://github.com/nielsfaber/scheduler-component
@@ -509,6 +866,23 @@ Example configuration:
entity: input_boolean.etat_ouverture_porte_sam
name: Porte sam
```
You can customize this component using the HACS card-mod component to adjust the alert colors. Example for displaying safety and load shedding alerts in red:
```
card_mod:
style: |
{% if is_state('binary_sensor.thermostat_chambre_security_state', 'on') %}
ha-card .body .sensor-heading ha-icon[icon="mdi:alert-outline"] {
color: red;
}
{% endif %}
{% if is_state('binary_sensor.thermostat_chambre_overpowering_state', 'on') %}
ha-card .body .sensor-heading ha-icon[icon="mdi:flash"] {
color: red;
}
{% endif %}
```
![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/custom-css-thermostat.png?raw=true)
## Even better with Apex-chart to tune your Thermostat
You can get curve like presented in [some results](#some-results) with kind of Apex-chart configuration only using the custom attributes of the thermostat described [here](#custom-attributes):
@@ -542,11 +916,21 @@ series:
name: Current temp
curve: smooth
yaxis_id: left
- entity: climate.thermostat_mythermostat
- entity: climate.thermostat_mythermostat <--- for over_switch
attribute: on_percent
name: Power percent
curve: stepline
yaxis_id: right
- entity: climate.thermostat_mythermostat <--- for over_thermostast
attribute: regulated_target_temperature
name: Regulated temperature
curve: stepline
yaxis_id: left
- entity: climate.thermostat_mythermostat <--- for over_valve
attribute: valve_open_percent
name: Valve open percent
curve: stepline
yaxis_id: right
```
## And always better and better with the NOTIFIER daemon app to notify events
@@ -640,10 +1024,87 @@ max: 30
If you want to contribute to this please read the [Contribution guidelines](CONTRIBUTING.md)
# Troubleshooting
## Using a Heatzy
The use of a Heatzy is possible provided you use a virtual switch on this model:
```
- platform:template
switches:
bathroom_heating:
unique_id: heating_bathroom
friendly_name: Bathroom heating
value_template: "{{ is_state_attr('climate.bathroom', 'preset_mode', 'comfort') }}"
icon_template: >-
{% if is_state_attr('climate.bathroom', 'preset_mode', 'comfort') %}
mdi:radiator
{% elif is_state_attr('climate.bathroom', 'preset_mode', 'away') %}
mdi:snowflake
{% else %}
mdi:radiator-disabled
{% endif %}
turn on:
service: climate.set_preset_mode
entity_id: climate.bathroom
data:
preset_mode: "comfort"
turn_off:
service: climate.set_preset_mode
entity_id: climate.bathroom
data:
preset_mode: "eco"
```
Thanks to @gael for this example.
## Using a Heatsink with a Pilot Wire
As with the Heatzy above you can use a virtual switch which will change the preset of your radiator depending on the ignition state of the VTherm.
Example :
```
- platform:template
switches:
radiator_soan:
friendly_name: radiator_soan_inv
value_template: "{{ is_state('switch.radiateur_soan', 'off') }}"
turn on:
service: switch.turn_off
data:
entity_id: switch.radiateur_soan
turn_off:
service: switch.turn_on
data:
entity_id: switch.radiateur_soan
icon_template: "{% if is_state('switch.radiateur_soan', 'on') %}mdi:radiator-disabled{% else %}mdi:radiator{% endif %}"
```
## Only the first radiator heats
In `over_switch` mode if several radiators are configured for the same VTherm, switching on will be done sequentially to smooth out consumption peaks as much as possible.
This is completely normal and desired. It is described here: [For a thermostat of type ``thermostat_over_switch```](#for-a-thermostat-of-type-thermostat_over_switch)
## Adjust window opening detection parameters in auto mode
If you cannot set the opening detection function in auto mode (see [auto](#auto-mode)), you can try modifying the parameters of the temperature smoothing algorithm.
In fact, automatic opening detection is based on the calculation of the temperature slope. To avoid artifacts due to an imprecise temperature sensor, this slope is calculated on a smoothed temperature with a smoothing algorithm called Exponential Moving Average.
This algorithm has 3 parameters:
1. `lifecycle_sec`: the duration in seconds taken into account for smoothing. The stronger it is, the greater the smoothing will be, but the longer there will be a detection delay,
2. `max_alpha`: if two temperature measurements are separated in time, the second will have a very strong weight. The parameter makes it possible to limit the weight of a measurement which arrives well after the previous one. This value must be between 0 and 1. The lower it is, the less distant values are taken into account. The default is 0.5. This means that when a new temperature value will never weigh more than half of the moving average,
3. `precision`: the number of digits after the decimal point retained for calculating the moving average.
To change its parameters, you must modify the `configuration.yaml` file and add the following section (the values are the default values):
```
versatile_thermostat:
short_ema_params:
max_alpha: 0.5
halflife_sec: 300
accuracy: 2
```
These parameters are sensitive and quite difficult to adjust. Please only use them if you know what you are doing and your temperature measurements are not already smooth.
***
[integration_blueprint]: https://github.com/custom-components/integration_blueprint
[versatile_thermostat]: https://github.com/jmcollin78/versatile_thermostat
[buymecoffee]: https://www.buymeacoffee.com/jmcollin78
[buymecoffeebadge]: https://img.shields.io/badge/Buy%20me%20a%20beer-%245-orange?style=for-the-badge&logo=buy-me-a-beer
[commits-shield]: https://img.shields.io/github/commit-activity/y/jmcollin78/versatile_thermostat.svg?style=for-the-badge
[commits]: https://github.com/jmcollin78/versatile_thermostat/commits/master
[hacs]: https://github.com/custom-components/hacs

View File

@@ -4,34 +4,43 @@
. .bashrc
cd $HA
function get_dev() {
pip install -r requirements_dev.txt
pip install -r requirements_test.txt
if [ -d /home/vscode/core ]; then
sudo chown -R vscode: /home/vscode/core
fi
}
echo "arguments are: "$*
# Post installation of container
command=$1
if [ "$command" == "install" ]; then
echo "Running container post installation"
script/setup
fi
echo "arguments are: "$1
if [ "$command" == "start" ]; then
echo "Running container start"
hass -c ./config --debug
fi
if [ "$command" == "translations" ]; then
echo "Running container start"
python3 -m script.translations develop
fi
if [ "$command" == "hassfest" ]; then
echo "Running container start"
python3 -m script.hassfest
fi
if [ "$command" == "restart" ]; then
echo "Killing existing container"
pkill hass
echo "Killing existing container"
hass -c ./config
fi
case $1 in
start)
echo "Running container start"
./scripts/starts_ha.sh
;;
dev-setup)
get_dev
;;
install)
echo "Running container post installation"
script/setup
;;
translations)
echo "Running container start"
cd $HA
python3 -m script.translations develop
;;
hassfest)
echo "Running container start"
python3 -m script.hassfest
# python -m script.hassfest --requirements --action validate --integration-path config/custom_components/versatile_thermostat/
;;
restart)
echo "Killing existing container"
pkill hass
echo "Restarting existing container"
pwd
./scripts/starts_ha.sh
;;
esac

View File

@@ -4,18 +4,79 @@ from __future__ import annotations
from typing import Dict
import logging
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform
from homeassistant.config_entries import ConfigEntry, ConfigType
from homeassistant.core import HomeAssistant
from .climate import VersatileThermostat
from .base_thermostat import BaseThermostat
from .const import DOMAIN
from .const import (
DOMAIN,
PLATFORMS,
CONF_AUTO_REGULATION_LIGHT,
CONF_AUTO_REGULATION_MEDIUM,
CONF_AUTO_REGULATION_STRONG,
CONF_AUTO_REGULATION_SLOW,
CONF_AUTO_REGULATION_EXPERT,
CONF_SHORT_EMA_PARAMS,
)
from .vtherm_api import VersatileThermostatAPI
_LOGGER = logging.getLogger(__name__)
PLATFORMS: list[Platform] = [Platform.CLIMATE]
SELF_REGULATION_PARAM_SCHEMA = {
vol.Required("kp"): vol.Coerce(float),
vol.Required("ki"): vol.Coerce(float),
vol.Required("k_ext"): vol.Coerce(float),
vol.Required("offset_max"): vol.Coerce(float),
vol.Required("stabilization_threshold"): vol.Coerce(float),
vol.Required("accumulated_error_threshold"): vol.Coerce(float),
}
EMA_PARAM_SCHEMA = {
vol.Required("max_alpha"): vol.Coerce(float),
vol.Required("halflife_sec"): vol.Coerce(float),
vol.Required("precision"): cv.positive_int,
}
CONFIG_SCHEMA = vol.Schema(
{
DOMAIN: vol.Schema(
{
CONF_AUTO_REGULATION_EXPERT: vol.Schema(SELF_REGULATION_PARAM_SCHEMA),
CONF_SHORT_EMA_PARAMS: vol.Schema(EMA_PARAM_SCHEMA),
}
),
},
extra=vol.ALLOW_EXTRA,
)
async def async_setup(
hass: HomeAssistant, config: ConfigType
): # pylint: disable=unused-argument
"""Initialisation de l'intégration"""
_LOGGER.info(
"Initializing %s integration with config: %s",
DOMAIN,
config.get(DOMAIN),
)
hass.data.setdefault(DOMAIN, {})
# 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")
return True
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
@@ -27,11 +88,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
entry.data,
)
# hass.data.setdefault(DOMAIN, {})
api: VersatileThermostatAPI = hass.data.get(DOMAIN)
if api is None:
api = VersatileThermostatAPI(hass)
api: VersatileThermostatAPI = VersatileThermostatAPI.get_vtherm_api(hass)
api.add_entry(entry)
@@ -49,7 +106,7 @@ async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
api: VersatileThermostatAPI = hass.data.get(DOMAIN)
api: VersatileThermostatAPI = VersatileThermostatAPI.get_vtherm_api(hass)
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
if api:
@@ -58,52 +115,14 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
return unload_ok
class VersatileThermostatAPI(Dict):
"""The VersatileThermostatAPI"""
_hass: HomeAssistant
# _entries: Dict(str, ConfigEntry)
def __init__(self, hass):
_LOGGER.debug("building a VersatileThermostatAPI")
super().__init__()
self._hass = hass
# self._entries = dict()
# Add the API in hass.data
self._hass.data[DOMAIN] = self
def add_entry(self, entry: ConfigEntry):
"""Add a new entry"""
_LOGGER.debug("Add the entry %s", entry.entry_id)
# self._entries[entry.entry_id] = entry
# Add the entry in hass.data
self._hass.data[DOMAIN][entry.entry_id] = entry
def remove_entry(self, entry: ConfigEntry):
"""Remove an entry"""
_LOGGER.debug("Remove the entry %s", entry.entry_id)
# self._entries.pop(entry.entry_id)
self._hass.data[DOMAIN].pop(entry.entry_id)
# If not more entries are preset, remove the API
if len(self) == 0:
_LOGGER.debug("No more entries-> Remove the API from DOMAIN")
self._hass.data.pop(DOMAIN)
@property
def hass(self):
"""Get the HomeAssistant object"""
return self._hass
# Example migration function
async def async_migrate_entry(hass, config_entry: ConfigEntry):
async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry):
"""Migrate old entry."""
_LOGGER.debug("Migrating from version %s", config_entry.version)
if config_entry.version == 1:
new = {**config_entry.data}
# TODO: modify 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)

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,275 @@
""" Implements the VersatileThermostat binary sensors component """
import logging
from homeassistant.core import HomeAssistant, callback, Event
from homeassistant.const import STATE_ON, STATE_OFF
from homeassistant.components.binary_sensor import (
BinarySensorEntity,
BinarySensorDeviceClass,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .commons import VersatileThermostatBaseEntity
from .const import (
CONF_NAME,
CONF_USE_POWER_FEATURE,
CONF_USE_PRESENCE_FEATURE,
CONF_USE_MOTION_FEATURE,
CONF_USE_WINDOW_FEATURE,
)
_LOGGER = logging.getLogger(__name__)
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the VersatileThermostat binary sensors 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)
entities = [SecurityBinarySensor(hass, unique_id, name, entry.data),WindowByPassBinarySensor(hass, unique_id, name, entry.data)]
if entry.data.get(CONF_USE_MOTION_FEATURE):
entities.append(MotionBinarySensor(hass, unique_id, name, entry.data))
if entry.data.get(CONF_USE_WINDOW_FEATURE):
entities.append(WindowBinarySensor(hass, unique_id, name, entry.data))
if entry.data.get(CONF_USE_PRESENCE_FEATURE):
entities.append(PresenceBinarySensor(hass, unique_id, name, entry.data))
if entry.data.get(CONF_USE_POWER_FEATURE):
entities.append(OverpoweringBinarySensor(hass, unique_id, name, entry.data))
async_add_entities(entities, True)
class SecurityBinarySensor(VersatileThermostatBaseEntity, BinarySensorEntity):
"""Representation of a BinarySensor which exposes the security state"""
def __init__(
self, hass: HomeAssistant, unique_id, name, entry_infos
) -> None: # pylint: disable=unused-argument
"""Initialize the SecurityState Binary sensor"""
super().__init__(hass, unique_id, entry_infos.get(CONF_NAME))
self._attr_name = "Security state"
self._attr_unique_id = f"{self._device_name}_security_state"
self._attr_is_on = False
@callback
async def async_my_climate_changed(self, event: Event = None):
"""Called when my climate have change"""
_LOGGER.debug("%s - climate state change", self._attr_unique_id)
old_state = self._attr_is_on
self._attr_is_on = self.my_climate.security_state is True
if old_state != self._attr_is_on:
self.async_write_ha_state()
return
@property
def device_class(self) -> BinarySensorDeviceClass | None:
return BinarySensorDeviceClass.SAFETY
@property
def icon(self) -> str | None:
if self._attr_is_on:
return "mdi:shield-alert"
else:
return "mdi:shield-check-outline"
class OverpoweringBinarySensor(VersatileThermostatBaseEntity, BinarySensorEntity):
"""Representation of a BinarySensor which exposes the overpowering state"""
def __init__(
self, hass: HomeAssistant, unique_id, name, entry_infos
) -> None: # pylint: disable=unused-argument
"""Initialize the OverpoweringState Binary sensor"""
super().__init__(hass, unique_id, entry_infos.get(CONF_NAME))
self._attr_name = "Overpowering state"
self._attr_unique_id = f"{self._device_name}_overpowering_state"
self._attr_is_on = False
@callback
async def async_my_climate_changed(self, event: Event = None):
"""Called when my climate have change"""
_LOGGER.debug("%s - climate state change", self._attr_unique_id)
old_state = self._attr_is_on
self._attr_is_on = self.my_climate.overpowering_state is True
if old_state != self._attr_is_on:
self.async_write_ha_state()
return
@property
def device_class(self) -> BinarySensorDeviceClass | None:
return BinarySensorDeviceClass.POWER
@property
def icon(self) -> str | None:
if self._attr_is_on:
return "mdi:flash-alert-outline"
else:
return "mdi:flash-outline"
class WindowBinarySensor(VersatileThermostatBaseEntity, BinarySensorEntity):
"""Representation of a BinarySensor which exposes the window state"""
def __init__(
self, hass: HomeAssistant, unique_id, name, entry_infos
) -> None: # pylint: disable=unused-argument
"""Initialize the WindowState Binary sensor"""
super().__init__(hass, unique_id, entry_infos.get(CONF_NAME))
self._attr_name = "Window state"
self._attr_unique_id = f"{self._device_name}_window_state"
self._attr_is_on = False
@callback
async def async_my_climate_changed(self, event: Event = None):
"""Called when my climate have change"""
_LOGGER.debug("%s - climate state change", self._attr_unique_id)
old_state = self._attr_is_on
# Issue 120 - only take defined presence value
if self.my_climate.window_state in [
STATE_ON,
STATE_OFF,
] or self.my_climate.window_auto_state in [STATE_ON, STATE_OFF]:
self._attr_is_on = (
self.my_climate.window_state == STATE_ON
or self.my_climate.window_auto_state == STATE_ON
)
if old_state != self._attr_is_on:
self.async_write_ha_state()
return
@property
def device_class(self) -> BinarySensorDeviceClass | None:
return BinarySensorDeviceClass.WINDOW
@property
def icon(self) -> str | None:
if self._attr_is_on:
if self.my_climate.window_state == STATE_ON:
return "mdi:window-open-variant"
else:
return "mdi:window-open"
else:
return "mdi:window-closed-variant"
class MotionBinarySensor(VersatileThermostatBaseEntity, BinarySensorEntity):
"""Representation of a BinarySensor which exposes the motion state"""
def __init__(
self, hass: HomeAssistant, unique_id, name, entry_infos
) -> None: # pylint: disable=unused-argument
"""Initialize the MotionState Binary sensor"""
super().__init__(hass, unique_id, entry_infos.get(CONF_NAME))
self._attr_name = "Motion state"
self._attr_unique_id = f"{self._device_name}_motion_state"
self._attr_is_on = False
@callback
async def async_my_climate_changed(self, event: Event = None):
"""Called when my climate have change"""
_LOGGER.debug("%s - climate state change", self._attr_unique_id)
old_state = self._attr_is_on
# Issue 120 - only take defined presence value
if self.my_climate.motion_state in [STATE_ON, STATE_OFF]:
self._attr_is_on = self.my_climate.motion_state == STATE_ON
if old_state != self._attr_is_on:
self.async_write_ha_state()
return
@property
def device_class(self) -> BinarySensorDeviceClass | None:
return BinarySensorDeviceClass.MOTION
@property
def icon(self) -> str | None:
if self._attr_is_on:
return "mdi:motion-sensor"
else:
return "mdi:motion-sensor-off"
class PresenceBinarySensor(VersatileThermostatBaseEntity, BinarySensorEntity):
"""Representation of a BinarySensor which exposes the presence state"""
def __init__(
self, hass: HomeAssistant, unique_id, name, entry_infos
) -> None: # pylint: disable=unused-argument
"""Initialize the PresenceState Binary sensor"""
super().__init__(hass, unique_id, entry_infos.get(CONF_NAME))
self._attr_name = "Presence state"
self._attr_unique_id = f"{self._device_name}_presence_state"
self._attr_is_on = False
@callback
async def async_my_climate_changed(self, event: Event = None):
"""Called when my climate have change"""
_LOGGER.debug("%s - climate state change", self._attr_unique_id)
old_state = self._attr_is_on
# Issue 120 - only take defined presence value
if self.my_climate.presence_state in [STATE_ON, STATE_OFF]:
self._attr_is_on = self.my_climate.presence_state == STATE_ON
if old_state != self._attr_is_on:
self.async_write_ha_state()
return
@property
def device_class(self) -> BinarySensorDeviceClass | None:
return BinarySensorDeviceClass.PRESENCE
@property
def icon(self) -> str | None:
if self._attr_is_on:
return "mdi:home-account"
else:
return "mdi:nature-people"
#PR - Adding Window ByPass
class WindowByPassBinarySensor(VersatileThermostatBaseEntity, BinarySensorEntity):
"""Representation of a BinarySensor which exposes the Window ByPass state"""
def __init__(
self, hass: HomeAssistant, unique_id, name, entry_infos
) -> None: # pylint: disable=unused-argument
"""Initialize the WindowByPass Binary sensor"""
super().__init__(hass, unique_id, entry_infos.get(CONF_NAME))
self._attr_name = "Window bypass"
self._attr_unique_id = f"{self._device_name}_window_bypass_state"
self._attr_is_on = False
@callback
async def async_my_climate_changed(self, event: Event = None):
"""Called when my climate have change"""
_LOGGER.debug("%s - climate state change", self._attr_unique_id)
old_state = self._attr_is_on
if self.my_climate.window_bypass_state in [True, False]:
self._attr_is_on = self.my_climate.window_bypass_state
if old_state != self._attr_is_on:
self.async_write_ha_state()
return
@property
def device_class(self) -> BinarySensorDeviceClass | None:
return BinarySensorDeviceClass.RUNNING
@property
def icon(self) -> str | None:
if self._attr_is_on:
return "mdi:window-shutter-cog"
else:
return "mdi:window-shutter-auto"

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,137 @@
""" Some usefull commons class """
import logging
from datetime import timedelta, datetime
from homeassistant.core import HomeAssistant, callback, Event
from homeassistant.components.climate import ClimateEntity, DOMAIN as CLIMATE_DOMAIN
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.device_registry import DeviceInfo, DeviceEntryType
from homeassistant.helpers.event import async_track_state_change_event, async_call_later
from homeassistant.util import dt as dt_util
from .base_thermostat import BaseThermostat
from .const import DOMAIN, DEVICE_MANUFACTURER
_LOGGER = logging.getLogger(__name__)
def get_tz(hass: HomeAssistant):
"""Get the current timezone"""
return dt_util.get_time_zone(hass.config.time_zone)
class NowClass:
""" For testing purpose only"""
@staticmethod
def get_now(hass: HomeAssistant) -> datetime:
""" A test function to get the now.
For testing purpose this method can be overriden to get a specific
timestamp.
"""
return datetime.now( get_tz(hass))
def round_to_nearest(n:float, x: float)->float:
""" Round a number to the nearest x (which should be decimal but not null)
Example:
nombre1 = 3.2
nombre2 = 4.7
x = 0.3
nombre_arrondi1 = round_to_nearest(nombre1, x)
nombre_arrondi2 = round_to_nearest(nombre2, x)
print(nombre_arrondi1) # Output: 3.3
print(nombre_arrondi2) # Output: 4.6
"""
assert x > 0
return round(n * (1/x)) / (1/x)
class VersatileThermostatBaseEntity(Entity):
"""A base class for all entities"""
_my_climate: BaseThermostat
hass: HomeAssistant
_config_id: str
_device_name: str
def __init__(self, hass: HomeAssistant, config_id, device_name) -> None:
"""The CTOR"""
self.hass = hass
self._config_id = config_id
self._device_name = device_name
self._my_climate = None
self._cancel_call = None
self._attr_has_entity_name = True
@property
def should_poll(self) -> bool:
"""Do not poll for those entities"""
return False
@property
def my_climate(self) -> BaseThermostat | None:
"""Returns my climate if found"""
if not self._my_climate:
self._my_climate = self.find_my_versatile_thermostat()
return self._my_climate
@property
def device_info(self) -> DeviceInfo:
"""Return the device info."""
return DeviceInfo(
entry_type=DeviceEntryType.SERVICE,
identifiers={(DOMAIN, self._config_id)},
name=self._device_name,
manufacturer=DEVICE_MANUFACTURER,
model=DOMAIN,
)
def find_my_versatile_thermostat(self) -> BaseThermostat:
"""Find the underlying climate entity"""
try:
component: EntityComponent[ClimateEntity] = self.hass.data[CLIMATE_DOMAIN]
for entity in component.entities:
# _LOGGER.debug("Device_info is %s", entity.device_info)
if entity.device_info == self.device_info:
_LOGGER.debug("Found %s!", entity)
return entity
except KeyError:
pass
return None
@callback
async def async_added_to_hass(self):
"""Listen to my climate state change"""
# Check delay condition
async def try_find_climate(_):
_LOGGER.debug(
"%s - Calling VersatileThermostatBaseEntity.async_added_to_hass", self
)
mcl = self.my_climate
if mcl:
if self._cancel_call:
self._cancel_call()
self._cancel_call = None
self.async_on_remove(
async_track_state_change_event(
self.hass,
[mcl.entity_id],
self.async_my_climate_changed,
)
)
else:
_LOGGER.warning("%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
async def async_my_climate_changed(self, event: Event): # pylint: disable=unused-argument
"""Called when my climate have change
This method aims to be overriden to take the status change
"""
return

View File

@@ -1,3 +1,5 @@
# pylint: disable=line-too-long, too-many-lines, invalid-name
"""Config flow for Versatile Thermostat integration."""
from __future__ import annotations
@@ -24,6 +26,7 @@ from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers import selector
from homeassistant.components.climate import ClimateEntity, DOMAIN as CLIMATE_DOMAIN
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
from homeassistant.components.number import DOMAIN as NUMBER_DOMAIN
from homeassistant.components.input_boolean import (
DOMAIN as INPUT_BOOLEAN_DOMAIN,
)
@@ -41,21 +44,30 @@ from .const import (
DOMAIN,
CONF_NAME,
CONF_HEATER,
CONF_HEATER_2,
CONF_HEATER_3,
CONF_HEATER_4,
CONF_TEMP_SENSOR,
CONF_EXTERNAL_TEMP_SENSOR,
CONF_POWER_SENSOR,
CONF_MAX_POWER_SENSOR,
CONF_WINDOW_SENSOR,
CONF_WINDOW_DELAY,
CONF_WINDOW_AUTO_MAX_DURATION,
CONF_WINDOW_AUTO_CLOSE_THRESHOLD,
CONF_WINDOW_AUTO_OPEN_THRESHOLD,
CONF_MOTION_SENSOR,
CONF_MOTION_DELAY,
CONF_MOTION_OFF_DELAY,
CONF_MOTION_PRESET,
CONF_NO_MOTION_PRESET,
CONF_DEVICE_POWER,
CONF_CYCLE_MIN,
CONF_PRESET_POWER,
CONF_PRESETS,
CONF_PRESETS_WITH_AC,
CONF_PRESETS_AWAY,
CONF_PRESETS_AWAY_WITH_AC,
CONF_PRESETS_SELECTIONABLE,
CONF_PROP_FUNCTION,
CONF_TPI_COEF_EXT,
@@ -73,12 +85,28 @@ from .const import (
CONF_THERMOSTAT_TYPE,
CONF_THERMOSTAT_SWITCH,
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_AC_MODE,
CONF_THERMOSTAT_TYPES,
CONF_THERMOSTAT_VALVE,
CONF_VALVE,
CONF_VALVE_2,
CONF_VALVE_3,
CONF_VALVE_4,
CONF_AUTO_REGULATION_MODES,
CONF_AUTO_REGULATION_MODE,
CONF_AUTO_REGULATION_NONE,
CONF_AUTO_REGULATION_DTEMP,
CONF_AUTO_REGULATION_PERIOD_MIN,
CONF_INVERSE_SWITCH,
UnknownEntity,
WindowOpenDetectionMethod,
)
_LOGGER = logging.getLogger(__name__)
@@ -125,35 +153,6 @@ def add_suggested_values_to_schema(
return vol.Schema(schema)
# def is_temperature_sensor(sensor: RegistryEntry):
# """Check if a registryEntry is a temperature sensor or assimilable to a temperature sensor"""
# if not sensor.entity_id.startswith(
# INPUT_NUMBER_DOMAIN
# ) and not sensor.entity_id.startswith(SENSOR_DOMAIN):
# return False
# return (
# sensor.device_class == TEMPERATURE
# or sensor.original_device_class == TEMPERATURE
# or sensor.unit_of_measurement in TEMPERATURE_UNITS
# )
#
#
# def is_power_sensor(sensor: RegistryEntry):
# """Check if a registryEntry is a power sensor or assimilable to a temperature sensor"""
# if not sensor.entity_id.startswith(
# INPUT_NUMBER_DOMAIN
# ) and not sensor.entity_id.startswith(SENSOR_DOMAIN):
# return False
# return (
# sensor.unit_of_measurement
# in [
# UnitOfPower.KILO_WATT,
# UnitOfPower.WATT,
# UnitOfPower.BTU_PER_HOUR,
# ]
# )
class VersatileThermostatBaseConfigFlow(FlowHandler):
"""The base Config flow class. Used to put some code in commons."""
@@ -167,7 +166,9 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
is_empty: bool = not bool(infos)
# Fix features selection depending to infos
self._infos[CONF_USE_WINDOW_FEATURE] = (
is_empty or self._infos.get(CONF_WINDOW_SENSOR) is not None
is_empty
or self._infos.get(CONF_WINDOW_SENSOR) is not None
or self._infos.get(CONF_WINDOW_AUTO_OPEN_THRESHOLD) is not None
)
self._infos[CONF_USE_MOTION_FEATURE] = (
is_empty or self._infos.get(CONF_MOTION_SENSOR) is not None
@@ -180,7 +181,7 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
is_empty or self._infos.get(CONF_PRESENCE_SENSOR) is not None
)
self.STEP_USER_DATA_SCHEMA = vol.Schema(
self.STEP_USER_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name
{
vol.Required(CONF_NAME): cv.string,
vol.Required(
@@ -195,15 +196,15 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
domain=[SENSOR_DOMAIN, INPUT_NUMBER_DOMAIN]
),
),
# vol.In(temp_sensors),
vol.Required(CONF_EXTERNAL_TEMP_SENSOR): selector.EntitySelector(
selector.EntitySelectorConfig(
domain=[SENSOR_DOMAIN, INPUT_NUMBER_DOMAIN]
),
), # vol.In(temp_sensors),
),
vol.Required(CONF_CYCLE_MIN, default=5): cv.positive_int,
vol.Required(CONF_TEMP_MIN, default=7): vol.Coerce(float),
vol.Required(CONF_TEMP_MAX, default=35): vol.Coerce(float),
vol.Optional(CONF_DEVICE_POWER, default="1"): vol.Coerce(float),
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,
@@ -211,13 +212,28 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
}
)
self.STEP_THERMOSTAT_SWITCH = vol.Schema(
self.STEP_THERMOSTAT_SWITCH = vol.Schema( # pylint: disable=invalid-name
{
vol.Required(CONF_HEATER): selector.EntitySelector(
selector.EntitySelectorConfig(
domain=[SWITCH_DOMAIN, INPUT_BOOLEAN_DOMAIN]
),
), # vol.In(switches),
),
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_PROP_FUNCTION, default=PROPORTIONAL_FUNCTION_TPI
): vol.In(
@@ -225,50 +241,118 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
PROPORTIONAL_FUNCTION_TPI,
]
),
vol.Optional(CONF_AC_MODE, default=False): cv.boolean,
vol.Optional(CONF_INVERSE_SWITCH, default=False): cv.boolean,
}
)
self.STEP_THERMOSTAT_CLIMATE = vol.Schema(
self.STEP_THERMOSTAT_CLIMATE = vol.Schema( # pylint: disable=invalid-name
{
vol.Required(CONF_CLIMATE): selector.EntitySelector(
selector.EntitySelectorConfig(domain=CLIMATE_DOMAIN),
), # vol.In(climates),
),
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.Optional(CONF_AC_MODE, default=False): cv.boolean,
vol.Optional(
CONF_AUTO_REGULATION_MODE, default=CONF_AUTO_REGULATION_NONE
): selector.SelectSelector(
selector.SelectSelectorConfig(
options=CONF_AUTO_REGULATION_MODES, translation_key="auto_regulation_mode"
)
),
vol.Optional(CONF_AUTO_REGULATION_DTEMP, default=0.5): vol.Coerce(float),
vol.Optional(CONF_AUTO_REGULATION_PERIOD_MIN, default=5): cv.positive_int
}
)
self.STEP_TPI_DATA_SCHEMA = vol.Schema(
self.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_PROP_FUNCTION, default=PROPORTIONAL_FUNCTION_TPI
): vol.In(
[
PROPORTIONAL_FUNCTION_TPI,
]
),
vol.Optional(CONF_AC_MODE, default=False): cv.boolean,
}
)
self.STEP_TPI_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name
{
vol.Required(CONF_TPI_COEF_INT, default=0.6): vol.Coerce(float),
vol.Required(CONF_TPI_COEF_EXT, default=0.01): vol.Coerce(float),
}
)
self.STEP_PRESETS_DATA_SCHEMA = vol.Schema(
self.STEP_PRESETS_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name
{
vol.Optional(v, default=0.0): vol.Coerce(float)
for (k, v) in CONF_PRESETS.items()
}
)
self.STEP_WINDOW_DATA_SCHEMA = vol.Schema(
self.STEP_PRESETS_WITH_AC_DATA_SCHEMA = ( # pylint: disable=invalid-name
vol.Schema( # pylint: disable=invalid-name
{
vol.Optional(v, default=0.0): vol.Coerce(float)
for (k, v) in CONF_PRESETS_WITH_AC.items()
}
)
)
self.STEP_WINDOW_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name
{
vol.Optional(CONF_WINDOW_SENSOR): selector.EntitySelector(
selector.EntitySelectorConfig(
domain=[BINARY_SENSOR_DOMAIN, INPUT_BOOLEAN_DOMAIN]
),
), # vol.In(window_sensors),
),
vol.Optional(CONF_WINDOW_DELAY, default=30): cv.positive_int,
vol.Optional(CONF_WINDOW_AUTO_OPEN_THRESHOLD): vol.Coerce(float),
vol.Optional(CONF_WINDOW_AUTO_CLOSE_THRESHOLD): vol.Coerce(float),
vol.Optional(CONF_WINDOW_AUTO_MAX_DURATION): cv.positive_int,
}
)
self.STEP_MOTION_DATA_SCHEMA = vol.Schema(
self.STEP_MOTION_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name
{
vol.Optional(CONF_MOTION_SENSOR): selector.EntitySelector(
selector.EntitySelectorConfig(
domain=[BINARY_SENSOR_DOMAIN, INPUT_BOOLEAN_DOMAIN]
),
), # vol.In(window_sensors),
),
vol.Optional(CONF_MOTION_DELAY, default=30): cv.positive_int,
vol.Optional(CONF_MOTION_OFF_DELAY, default=300): cv.positive_int,
vol.Optional(CONF_MOTION_PRESET, default="comfort"): vol.In(
CONF_PRESETS_SELECTIONABLE
),
@@ -278,24 +362,23 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
}
)
self.STEP_POWER_DATA_SCHEMA = vol.Schema(
self.STEP_POWER_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name
{
vol.Optional(CONF_POWER_SENSOR): selector.EntitySelector(
selector.EntitySelectorConfig(
domain=[SENSOR_DOMAIN, INPUT_NUMBER_DOMAIN]
),
), # vol.In(power_sensors),
),
vol.Optional(CONF_MAX_POWER_SENSOR): selector.EntitySelector(
selector.EntitySelectorConfig(
domain=[SENSOR_DOMAIN, INPUT_NUMBER_DOMAIN]
),
), # vol.In(power_sensors),
vol.Optional(CONF_DEVICE_POWER, default="1"): vol.Coerce(float),
),
vol.Optional(CONF_PRESET_POWER, default="13"): vol.Coerce(float),
}
)
self.STEP_PRESENCE_DATA_SCHEMA = vol.Schema(
self.STEP_PRESENCE_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name
{
vol.Optional(CONF_PRESENCE_SENSOR): selector.EntitySelector(
selector.EntitySelectorConfig(
@@ -305,7 +388,7 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
INPUT_BOOLEAN_DOMAIN,
]
),
), # vol.In(presence_sensors),
),
}
).extend(
{
@@ -314,7 +397,28 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
}
)
self.STEP_ADVANCED_DATA_SCHEMA = vol.Schema(
self.STEP_PRESENCE_WITH_AC_DATA_SCHEMA = ( # pylint: disable=invalid-name
vol.Schema(
{
vol.Optional(CONF_PRESENCE_SENSOR): selector.EntitySelector(
selector.EntitySelectorConfig(
domain=[
PERSON_DOMAIN,
BINARY_SENSOR_DOMAIN,
INPUT_BOOLEAN_DOMAIN,
]
),
),
}
).extend(
{
vol.Optional(v, default=17): vol.Coerce(float)
for (k, v) in CONF_PRESETS_AWAY_WITH_AC.items()
}
)
)
self.STEP_ADVANCED_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name
{
vol.Required(
CONF_MINIMAL_ACTIVATION_DELAY, default=10
@@ -331,7 +435,7 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
}
)
async def validate_input(self, data: dict) -> dict[str]:
async def validate_input(self, data: dict) -> None:
"""Validate the user input allows us to connect.
Data has the keys from STEP_*_DATA_SCHEMA with values provided by the user.
@@ -357,6 +461,19 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
)
raise UnknownEntity(conf)
# Check that only one window feature is used
ws = data.get(CONF_WINDOW_SENSOR) # pylint: disable=invalid-name
waot = data.get(CONF_WINDOW_AUTO_OPEN_THRESHOLD)
wact = data.get(CONF_WINDOW_AUTO_CLOSE_THRESHOLD)
wamd = data.get(CONF_WINDOW_AUTO_MAX_DURATION)
if ws is not None and (
waot is not None or wact is not None or wamd is not None
):
_LOGGER.error(
"Only one window detection method should be used. Use window_sensor or auto window open detection but not both"
)
raise WindowOpenDetectionMethod(CONF_WINDOW_SENSOR)
def merge_user_input(self, data_schema: vol.Schema, user_input: dict):
"""For each schema entry not in user_input, set or remove values in infos"""
self._infos.update(user_input)
@@ -387,6 +504,8 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
await self.validate_input(user_input)
except UnknownEntity as err:
errors[str(err)] = "unknown_entity"
except WindowOpenDetectionMethod as err:
errors[str(err)] = "window_open_detection_method"
except Exception: # pylint: disable=broad-except
_LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"
@@ -418,6 +537,10 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
return await self.generic_step(
"type", self.STEP_THERMOSTAT_SWITCH, user_input, self.async_step_tpi
)
elif self._infos[CONF_THERMOSTAT_TYPE] == CONF_THERMOSTAT_VALVE:
return await self.generic_step(
"type", self.STEP_THERMOSTAT_VALVE, user_input, self.async_step_tpi
)
else:
return await self.generic_step(
"type",
@@ -448,9 +571,12 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
elif self._infos[CONF_USE_PRESENCE_FEATURE]:
next_step = self.async_step_presence
return await self.generic_step(
"presets", self.STEP_PRESETS_DATA_SCHEMA, user_input, next_step
)
if self._infos.get(CONF_AC_MODE) is True:
schema = self.STEP_PRESETS_WITH_AC_DATA_SCHEMA
else:
schema = self.STEP_PRESETS_DATA_SCHEMA
return await self.generic_step("presets", schema, user_input, next_step)
async def async_step_window(self, user_input: dict | None = None) -> FlowResult:
"""Handle the window sensor flow steps"""
@@ -501,9 +627,14 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
"""Handle the presence management flow steps"""
_LOGGER.debug("Into ConfigFlow.async_step_presence user_input=%s", user_input)
if self._infos.get(CONF_AC_MODE) is True:
schema = self.STEP_PRESENCE_WITH_AC_DATA_SCHEMA
else:
schema = self.STEP_PRESENCE_DATA_SCHEMA
return await self.generic_step(
"presence",
self.STEP_PRESENCE_DATA_SCHEMA,
schema,
user_input,
self.async_step_advanced,
)
@@ -601,6 +732,10 @@ class VersatileThermostatOptionsFlowHandler(
return await self.generic_step(
"type", self.STEP_THERMOSTAT_SWITCH, user_input, self.async_step_tpi
)
elif self._infos[CONF_THERMOSTAT_TYPE] == CONF_THERMOSTAT_VALVE:
return await self.generic_step(
"type", self.STEP_THERMOSTAT_VALVE, user_input, self.async_step_tpi
)
else:
return await self.generic_step(
"type",
@@ -635,9 +770,12 @@ class VersatileThermostatOptionsFlowHandler(
elif self._infos[CONF_USE_PRESENCE_FEATURE]:
next_step = self.async_step_presence
return await self.generic_step(
"presets", self.STEP_PRESETS_DATA_SCHEMA, user_input, next_step
)
if self._infos.get(CONF_AC_MODE) is True:
schema = self.STEP_PRESETS_WITH_AC_DATA_SCHEMA
else:
schema = self.STEP_PRESETS_DATA_SCHEMA
return await self.generic_step("presets", schema, user_input, next_step)
async def async_step_window(self, user_input: dict | None = None) -> FlowResult:
"""Handle the window sensor flow steps"""
@@ -695,9 +833,14 @@ class VersatileThermostatOptionsFlowHandler(
"Into OptionsFlowHandler.async_step_presence user_input=%s", user_input
)
if self._infos.get(CONF_AC_MODE) is True:
schema = self.STEP_PRESENCE_WITH_AC_DATA_SCHEMA
else:
schema = self.STEP_PRESENCE_DATA_SCHEMA
return await self.generic_step(
"presence",
self.STEP_PRESENCE_DATA_SCHEMA,
schema,
user_input,
self.async_step_advanced,
)

View File

@@ -1,13 +1,15 @@
# pylint: disable=line-too-long
"""Constants for the Versatile Thermostat integration."""
from enum import Enum
from homeassistant.const import CONF_NAME
from homeassistant.components.climate.const import (
from homeassistant.const import CONF_NAME, Platform
from homeassistant.components.climate import (
# PRESET_ACTIVITY,
PRESET_BOOST,
PRESET_COMFORT,
PRESET_ECO,
SUPPORT_TARGET_TEMPERATURE,
ClimateEntityFeature,
)
from homeassistant.exceptions import HomeAssistantError
@@ -16,6 +18,15 @@ from .prop_algorithm import (
PROPORTIONAL_FUNCTION_TPI,
)
PRESET_AC_SUFFIX = "_ac"
PRESET_ECO_AC = PRESET_ECO + PRESET_AC_SUFFIX
PRESET_COMFORT_AC = PRESET_COMFORT + PRESET_AC_SUFFIX
PRESET_BOOST_AC = PRESET_BOOST + PRESET_AC_SUFFIX
DEVICE_MANUFACTURER = "JMCOLLIN"
DEVICE_MODEL = "Versatile Thermostat"
PRESET_POWER = "power"
PRESET_SECURITY = "security"
@@ -23,7 +34,12 @@ HIDDEN_PRESETS = [PRESET_POWER, PRESET_SECURITY]
DOMAIN = "versatile_thermostat"
PLATFORMS: list[Platform] = [Platform.CLIMATE, Platform.BINARY_SENSOR, Platform.SENSOR]
CONF_HEATER = "heater_entity_id"
CONF_HEATER_2 = "heater_entity2_id"
CONF_HEATER_3 = "heater_entity3_id"
CONF_HEATER_4 = "heater_entity4_id"
CONF_TEMP_SENSOR = "temperature_sensor_entity_id"
CONF_EXTERNAL_TEMP_SENSOR = "external_temperature_sensor_entity_id"
CONF_POWER_SENSOR = "power_sensor_entity_id"
@@ -35,6 +51,7 @@ CONF_CYCLE_MIN = "cycle_min"
CONF_PROP_FUNCTION = "proportional_function"
CONF_WINDOW_DELAY = "window_delay"
CONF_MOTION_DELAY = "motion_delay"
CONF_MOTION_OFF_DELAY = "motion_off_delay"
CONF_MOTION_PRESET = "motion_preset"
CONF_NO_MOTION_PRESET = "no_motion_preset"
CONF_TPI_COEF_INT = "tpi_coef_int"
@@ -50,11 +67,41 @@ CONF_SECURITY_DEFAULT_ON_PERCENT = "security_default_on_percent"
CONF_THERMOSTAT_TYPE = "thermostat_type"
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_AC_MODE = "ac_mode"
CONF_WINDOW_AUTO_OPEN_THRESHOLD = "window_auto_open_threshold"
CONF_WINDOW_AUTO_CLOSE_THRESHOLD = "window_auto_close_threshold"
CONF_WINDOW_AUTO_MAX_DURATION = "window_auto_max_duration"
CONF_VALVE = "valve_entity_id"
CONF_VALVE_2 = "valve_entity2_id"
CONF_VALVE_3 = "valve_entity3_id"
CONF_VALVE_4 = "valve_entity4_id"
CONF_AUTO_REGULATION_MODE = "auto_regulation_mode"
CONF_AUTO_REGULATION_NONE = "auto_regulation_none"
CONF_AUTO_REGULATION_SLOW = "auto_regulation_slow"
CONF_AUTO_REGULATION_LIGHT = "auto_regulation_light"
CONF_AUTO_REGULATION_MEDIUM = "auto_regulation_medium"
CONF_AUTO_REGULATION_STRONG = "auto_regulation_strong"
CONF_AUTO_REGULATION_EXPERT = "auto_regulation_expert"
CONF_AUTO_REGULATION_DTEMP = "auto_regulation_dtemp"
CONF_AUTO_REGULATION_PERIOD_MIN = "auto_regulation_periode_min"
CONF_INVERSE_SWITCH = "inverse_switch_command"
CONF_SHORT_EMA_PARAMS = "short_ema_params"
DEFAULT_SHORT_EMA_PARAMS = {
"max_alpha": 0.5,
# In sec
"halflife_sec": 300,
"precision": 2,
}
CONF_PRESETS = {
p: f"{p}_temp"
@@ -65,14 +112,39 @@ CONF_PRESETS = {
)
}
CONF_PRESETS_WITH_AC = {
p: f"{p}_temp"
for p in (
PRESET_ECO,
PRESET_COMFORT,
PRESET_BOOST,
PRESET_ECO_AC,
PRESET_COMFORT_AC,
PRESET_BOOST_AC,
)
}
PRESET_AWAY_SUFFIX = "_away"
CONF_PRESETS_AWAY = {
p: f"{p}_temp"
for p in (
PRESET_ECO + PRESET_AWAY_SUFFIX,
PRESET_BOOST + PRESET_AWAY_SUFFIX,
PRESET_COMFORT + PRESET_AWAY_SUFFIX,
PRESET_BOOST + PRESET_AWAY_SUFFIX,
)
}
CONF_PRESETS_AWAY_WITH_AC = {
p: f"{p}_temp"
for p in (
PRESET_ECO + PRESET_AWAY_SUFFIX,
PRESET_COMFORT + PRESET_AWAY_SUFFIX,
PRESET_BOOST + PRESET_AWAY_SUFFIX,
PRESET_ECO_AC + PRESET_AWAY_SUFFIX,
PRESET_COMFORT_AC + PRESET_AWAY_SUFFIX,
PRESET_BOOST_AC + PRESET_AWAY_SUFFIX,
)
}
@@ -80,17 +152,25 @@ CONF_PRESETS_SELECTIONABLE = [PRESET_ECO, PRESET_COMFORT, PRESET_BOOST]
CONF_PRESETS_VALUES = list(CONF_PRESETS.values())
CONF_PRESETS_AWAY_VALUES = list(CONF_PRESETS_AWAY.values())
CONF_PRESETS_WITH_AC_VALUES = list(CONF_PRESETS_WITH_AC.values())
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_TEMP_SENSOR,
CONF_EXTERNAL_TEMP_SENSOR,
CONF_POWER_SENSOR,
CONF_MAX_POWER_SENSOR,
CONF_WINDOW_SENSOR,
CONF_WINDOW_DELAY,
CONF_WINDOW_AUTO_OPEN_THRESHOLD,
CONF_WINDOW_AUTO_CLOSE_THRESHOLD,
CONF_WINDOW_AUTO_MAX_DURATION,
CONF_MOTION_SENSOR,
CONF_MOTION_DELAY,
CONF_MOTION_PRESET,
@@ -111,30 +191,129 @@ ALL_CONF = (
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_AC_MODE,
CONF_VALVE,
CONF_VALVE_2,
CONF_VALVE_3,
CONF_VALVE_4,
CONF_AUTO_REGULATION_MODE,
CONF_AUTO_REGULATION_DTEMP,
CONF_AUTO_REGULATION_PERIOD_MIN,
CONF_INVERSE_SWITCH,
]
+ CONF_PRESETS_VALUES
+ CONF_PRESETS_AWAY_VALUES,
+ CONF_PRESETS_AWAY_VALUES
+ CONF_PRESETS_WITH_AC_VALUES
+ CONF_PRESETS_AWAY_WITH_AC_VALUES,
)
CONF_FUNCTIONS = [
PROPORTIONAL_FUNCTION_TPI,
]
CONF_THERMOSTAT_TYPES = [CONF_THERMOSTAT_SWITCH, CONF_THERMOSTAT_CLIMATE]
CONF_AUTO_REGULATION_MODES = [
CONF_AUTO_REGULATION_NONE,
CONF_AUTO_REGULATION_LIGHT,
CONF_AUTO_REGULATION_MEDIUM,
CONF_AUTO_REGULATION_STRONG,
CONF_AUTO_REGULATION_SLOW,
CONF_AUTO_REGULATION_EXPERT,
]
SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE
CONF_THERMOSTAT_TYPES = [
CONF_THERMOSTAT_SWITCH,
CONF_THERMOSTAT_CLIMATE,
CONF_THERMOSTAT_VALVE,
]
SUPPORT_FLAGS = ClimateEntityFeature.TARGET_TEMPERATURE
SERVICE_SET_PRESENCE = "set_presence"
SERVICE_SET_PRESET_TEMPERATURE = "set_preset_temperature"
SERVICE_SET_SECURITY = "set_security"
SERVICE_SET_WINDOW_BYPASS = "set_window_bypass"
SERVICE_SET_AUTO_REGULATION_MODE = "set_auto_regulation_mode"
DEFAULT_SECURITY_MIN_ON_PERCENT = 0.5
DEFAULT_SECURITY_DEFAULT_ON_PERCENT = 0.1
ATTR_TOTAL_ENERGY = "total_energy"
ATTR_MEAN_POWER_CYCLE = "mean_cycle_power"
# A special regulation parameter suggested by @Maia here: https://github.com/jmcollin78/versatile_thermostat/discussions/154
class RegulationParamSlow:
"""Light parameters for slow latency regulation"""
kp: float = 0.2 # 20% of the current internal regulation offset are caused by the current difference of target temperature and room temperature
ki: float = (
0.8 / 288.0
) # 80% of the current internal regulation offset are caused by the average offset of the past 24 hours
k_ext: float = (
1.0 / 25.0
) # this will add 1°C to the offset when it's 25°C colder outdoor than indoor
offset_max: float = 2.0 # limit to a final offset of -2°C to +2°C
stabilization_threshold: float = 0.0 # this needs to be disabled as otherwise the long term accumulated error will always be reset when the temp briefly crosses from/to below/above the target
accumulated_error_threshold: float = (
2.0 * 288
) # this allows up to 2°C long term offset in both directions
class RegulationParamLight:
"""Light parameters for regulation"""
kp: float = 0.2
ki: float = 0.05
k_ext: float = 0.05
offset_max: float = 1.5
stabilization_threshold: float = 0.1
accumulated_error_threshold: float = 10
class RegulationParamMedium:
"""Light parameters for regulation"""
kp: float = 0.3
ki: float = 0.05
k_ext: float = 0.1
offset_max: float = 2
stabilization_threshold: float = 0.1
accumulated_error_threshold: float = 20
class RegulationParamStrong:
"""Strong parameters for regulation
A set of parameters which doesn't take into account the external temp
and concentrate to internal temp error + accumulated error.
This should work for cold external conditions which else generates
high external_offset"""
kp: float = 0.4
ki: float = 0.08
k_ext: float = 0.0
offset_max: float = 5
stabilization_threshold: float = 0.1
accumulated_error_threshold: float = 50
# Not used now
class RegulationParamVeryStrong:
"""Strong parameters for regulation"""
kp: float = 0.6
ki: float = 0.1
k_ext: float = 0.2
offset_max: float = 4
stabilization_threshold: float = 0.1
accumulated_error_threshold: float = 30
class EventType(Enum):
"""The event type that can be sent"""
@@ -144,7 +323,25 @@ class EventType(Enum):
TEMPERATURE_EVENT: str = "versatile_thermostat_temperature_event"
HVAC_MODE_EVENT: str = "versatile_thermostat_hvac_mode_event"
PRESET_EVENT: str = "versatile_thermostat_preset_event"
WINDOW_AUTO_EVENT: str = "versatile_thermostat_window_auto_event"
class UnknownEntity(HomeAssistantError):
"""Error to indicate there is an unknown entity_id given."""
class WindowOpenDetectionMethod(HomeAssistantError):
"""Error to indicate there is an error in the window open detection method given."""
class overrides: # pylint: disable=invalid-name
"""An annotation to inform overrides"""
def __init__(self, func):
self.func = func
def __get__(self, instance, owner):
return self.func.__get__(instance, owner)
def __call__(self, *args, **kwargs):
raise RuntimeError(f"Method {self.func.__name__} should have been overridden")

View File

@@ -0,0 +1,92 @@
# pylint: disable=line-too-long
"""The Estimated Mobile Average calculation used for temperature slope
and maybe some others feature"""
import logging
import math
from datetime import datetime, tzinfo
_LOGGER = logging.getLogger(__name__)
MIN_TIME_DECAY_SEC = 0
# MAX_ALPHA:
# As for the EMA calculation of irregular time series, I've seen that it might be useful to
# have an upper limit for alpha in case the last measurement was too long ago.
# For example when using a half life of 10 minutes a measurement that is 60 minutes ago
# (if there's nothing inbetween) would contribute to the smoothed value with 1,5%,
# giving the current measurement 98,5% relevance. It could be wise to limit the alpha to e.g. 4x the half life (=0.9375).
class ExponentialMovingAverage:
"""A class that will do the Estimated Mobile Average calculation"""
def __init__(
self,
vterm_name: str,
halflife: float,
timezone: tzinfo,
precision: int = 3,
max_alpha: float = 0.5,
):
"""The halflife is the duration in secondes of a normal cycle"""
self._halflife: float = halflife
self._timezone = timezone
self._current_ema: float = None
self._last_timestamp: datetime = datetime.now(self._timezone)
self._name = vterm_name
self._precision = precision
self._max_alpha = max_alpha
def __str__(self) -> str:
return f"EMA-{self._name}"
def calculate_ema(self, measurement: float, timestamp: datetime) -> float | None:
"""Calculate the new EMA from a new measurement measured at timestamp
Return the EMA or None if all parameters are not initialized now
"""
if measurement is None or timestamp is None:
_LOGGER.warning(
"%s - Cannot calculate EMA: measurement and timestamp are mandatory. This message can be normal at startup but should not persist",
self,
)
return measurement
if self._current_ema is None:
_LOGGER.debug(
"%s - First init of the EMA",
self,
)
self._current_ema = measurement
self._last_timestamp = timestamp
return self._current_ema
time_decay = (timestamp - self._last_timestamp).total_seconds()
if time_decay < MIN_TIME_DECAY_SEC:
_LOGGER.debug(
"%s - time_decay %s is too small (< %s). Forget the measurement",
self,
time_decay,
MIN_TIME_DECAY_SEC,
)
return self._current_ema
alpha = 1 - math.exp(math.log(0.5) * time_decay / self._halflife)
# capping alpha to avoid gap if last measurement was long time ago
alpha = min(alpha, self._max_alpha)
new_ema = alpha * measurement + (1 - alpha) * self._current_ema
self._last_timestamp = timestamp
self._current_ema = new_ema
_LOGGER.debug(
"%s - timestamp=%s alpha=%.2f measurement=%.2f current_ema=%.2f new_ema=%.2f",
self,
timestamp,
alpha,
measurement,
self._current_ema,
new_ema,
)
return round(self._current_ema, self._precision)

View File

@@ -1,19 +1,19 @@
{
"version": "0.0.1",
"domain": "versatile_thermostat",
"name": "Versatile Thermostat",
"config_flow": true,
"documentation": "https://github.com/jmcollin78/versatile_thermostat",
"issue_tracker": "https://github.com/jmcollin78/versatile_thermostat/issues",
"requirements": [],
"ssdp": [],
"zeroconf": [],
"homekit": {},
"dependencies": [],
"codeowners": [
"@jmcollin78"
],
"quality_scale": "silver",
"config_flow": true,
"dependencies": [],
"documentation": "https://github.com/jmcollin78/versatile_thermostat",
"homekit": {},
"integration_type": "device",
"iot_class": "calculated",
"integration_type": "device"
}
"issue_tracker": "https://github.com/jmcollin78/versatile_thermostat/issues",
"quality_scale": "silver",
"requirements": [],
"ssdp": [],
"version": "4.2.0",
"zeroconf": []
}

View File

@@ -0,0 +1,148 @@
# pylint: disable=line-too-long
""" This file implements the Open Window by temperature algorithm
This algo works the following way:
- each time a new temperature is measured
- calculate the slope of the temperature curve. For this we calculate the slope(t) = 1/2 slope(t-1) + 1/2 * dTemp / dt
- if the slope is lower than a threshold the window opens alert is notified
- if the slope regain positive the end of the window open alert is notified
"""
import logging
from datetime import datetime
_LOGGER = logging.getLogger(__name__)
# To filter bad values
MIN_DELTA_T_SEC = 0 # two temp mesure should be > 0 sec
MAX_SLOPE_VALUE = (
120 # slope cannot be > 2°/min or < -2°/min -> else this is an aberrant point
)
MAX_DURATION_MIN = 30 # a fake data point is added in the cycle if last measurement was older than 30 min
MIN_NB_POINT = 4 # do not calculate slope until we have enough point
class WindowOpenDetectionAlgorithm:
"""The class that implements the algorithm listed above"""
_alert_threshold: float
_end_alert_threshold: float
_last_slope: float
_last_datetime: datetime
_last_temperature: float
_nb_point: int
def __init__(self, alert_threshold, end_alert_threshold) -> None:
"""Initalize a new algorithm with the both threshold"""
self._alert_threshold = alert_threshold
self._end_alert_threshold = end_alert_threshold
self._last_slope = None
self._last_datetime = None
self._nb_point = 0
def check_age_last_measurement(self, temperature, datetime_now) -> float:
""" " Check if last measurement is old and add
a fake measurement point if this is the case
"""
if self._last_datetime is None:
return self.add_temp_measurement(temperature, datetime_now)
delta_t_sec = float((datetime_now - self._last_datetime).total_seconds()) / 60.0
if delta_t_sec >= MAX_DURATION_MIN:
return self.add_temp_measurement(temperature, datetime_now, False)
else:
# do nothing
return self._last_slope
def add_temp_measurement(
self, temperature: float, datetime_measure: datetime, store_date: bool = True
) -> float:
"""Add a new temperature measurement
returns the last slope
"""
if self._last_datetime is None or self._last_temperature is None:
_LOGGER.debug("First initialisation")
self._last_datetime = datetime_measure
self._last_temperature = temperature
self._nb_point = self._nb_point + 1
return None
_LOGGER.debug(
"We are already initialized slope=%s last_temp=%0.2f",
self._last_slope,
self._last_temperature,
)
lspe = self._last_slope
delta_t_sec = float((datetime_measure - self._last_datetime).total_seconds())
delta_t = delta_t_sec / 60.0
if delta_t_sec <= MIN_DELTA_T_SEC:
_LOGGER.debug(
"Delta t is %d < %d which should be not possible. We don't consider this value",
delta_t_sec,
MIN_DELTA_T_SEC,
)
return lspe
delta_t_hour = delta_t / 60.0
delta_temp = float(temperature - self._last_temperature)
new_slope = delta_temp / delta_t_hour
if new_slope > MAX_SLOPE_VALUE or new_slope < -MAX_SLOPE_VALUE:
_LOGGER.debug(
"New_slope is abs(%.2f) > %.2f which should be not possible. We don't consider this value",
new_slope,
MAX_SLOPE_VALUE,
)
return lspe
if self._last_slope is None:
self._last_slope = round(new_slope, 2)
else:
self._last_slope = round((0.2 * self._last_slope) + (0.8 * new_slope), 2)
# if we are in cycle check and so adding a fake datapoint, we don't store the event datetime
# so that, when we will receive a real temperature point we will not calculate a wrong slope
if store_date:
self._last_datetime = datetime_measure
self._last_temperature = temperature
self._nb_point = self._nb_point + 1
_LOGGER.debug(
"delta_t=%.3f delta_temp=%.3f new_slope=%.3f last_slope=%s slope=%.3f nb_point=%s",
delta_t,
delta_temp,
new_slope,
lspe,
self._last_slope,
self._nb_point,
)
return self._last_slope
def is_window_open_detected(self) -> bool:
"""True if the last calculated slope is under (because negative value) the _alert_threshold"""
if self._alert_threshold is None:
return False
if self._nb_point < MIN_NB_POINT or self._last_slope is None:
return False
return self._last_slope < -self._alert_threshold
def is_window_close_detected(self) -> bool:
"""True if the last calculated slope is above (cause negative) the _end_alert_threshold"""
if self._end_alert_threshold is None:
return False
if self._nb_point < MIN_NB_POINT or self._last_slope is None:
return False
return self._last_slope >= self._end_alert_threshold
@property
def last_slope(self) -> float:
"""Return the last calculated slope"""
return self._last_slope

View File

@@ -0,0 +1,111 @@
# pylint: disable=line-too-long
""" The PI algorithm implementation """
import logging
_LOGGER = logging.getLogger(__name__)
class PITemperatureRegulator:
"""A class implementing a PI Algorithm
PI algorithms calculate a target temperature by adding an offset which is calculating as follow:
- offset = kp * error + ki * accumulated_error
To use it you must:
- instanciate the class and gives the algorithm parameters: kp, ki, offset_max, stabilization_threshold, accumulated_error_threshold
- call calculate_regulated_temperature with the internal and external temperature
- call set_target_temp when the target temperature change.
"""
def __init__(
self,
target_temp: float,
kp: float,
ki: float,
k_ext: float,
offset_max: float,
stabilization_threshold: float,
accumulated_error_threshold: float,
):
self.target_temp: float = target_temp
self.kp: float = kp # proportionnel gain
self.ki: float = ki # integral gain
self.k_ext: float = k_ext # exterior gain
self.offset_max: float = offset_max
self.stabilization_threshold: float = stabilization_threshold
self.accumulated_error: float = 0
self.accumulated_error_threshold: float = accumulated_error_threshold
def reset_accumulated_error(self):
"""Reset the accumulated error"""
self.accumulated_error = 0
def set_accumulated_error(self, accumulated_error):
"""Allow to persist and restore the accumulated_error"""
self.accumulated_error = accumulated_error
def set_target_temp(self, target_temp):
"""Set the new target_temp"""
self.target_temp = target_temp
# 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
def calculate_regulated_temperature(
self, internal_temp: float, external_temp: float
): # pylint: disable=unused-argument
"""Calculate a new target_temp given some temperature"""
if internal_temp is None:
_LOGGER.warning(
"Temporarily skipping the self-regulation algorithm while the configured sensor for room temperature is unavailable"
)
return self.target_temp
if external_temp is None:
_LOGGER.warning(
"Temporarily skipping the self-regulation algorithm while the configured sensor for outdoor temperature is unavailable"
)
return self.target_temp
# Calculate the error factor (P)
error = self.target_temp - internal_temp
# Calculate the sum of error (I)
self.accumulated_error += error
# Capping of the error
self.accumulated_error = min(
self.accumulated_error_threshold,
max(-self.accumulated_error_threshold, self.accumulated_error),
)
# Calculate the offset (proportionnel + intégral)
offset = self.kp * error + self.ki * self.accumulated_error
# Calculate the exterior offset
# 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)
# Capping of offset_ext
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(
"PITemperatureRegulator - Error: %.2f accumulated_error: %.2f offset: %.2f offset_ext: %.2f target_tem: %.1f regulatedTemp: %.1f",
error,
self.accumulated_error,
offset,
offset_ext,
self.target_temp,
result,
)
return result

View File

@@ -1,3 +1,4 @@
""" The TPI calculation module """
import logging
_LOGGER = logging.getLogger(__name__)
@@ -21,7 +22,7 @@ class PropAlgorithm:
tpi_coef_ext,
cycle_min: int,
minimal_activation_delay: int,
):
) -> 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",
@@ -44,19 +45,33 @@ class PropAlgorithm:
self._default_on_percent = 0
def calculate(
self, target_temp: float, current_temp: float, ext_current_temp: float
self,
target_temp: float,
current_temp: float,
ext_current_temp: float,
cooling=False,
):
"""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 will be disabled" # pylint: disable=line-too-long
"Proportional algorithm: calculation is not possible cause target_temp or current_temp is null. Heating/cooling will be disabled" # pylint: disable=line-too-long
)
self._calculated_on_percent = 0
else:
delta_temp = target_temp - current_temp
delta_ext_temp = (
target_temp - ext_current_temp if ext_current_temp is not None else 0
)
if cooling:
delta_temp = current_temp - target_temp
delta_ext_temp = (
ext_current_temp
if ext_current_temp is not None
else 0 - target_temp
)
else:
delta_temp = target_temp - current_temp
delta_ext_temp = (
target_temp - ext_current_temp
if ext_current_temp is not None
else 0
)
if self._function == PROPORTIONAL_FUNCTION_TPI:
self._calculated_on_percent = (
@@ -85,6 +100,12 @@ class PropAlgorithm:
def _calculate_internal(self):
"""Finish the calculation to get the on_percent in seconds"""
# calculated on_time duration in seconds
if self._calculated_on_percent > 1:
self._calculated_on_percent = 1
if self._calculated_on_percent < 0:
self._calculated_on_percent = 0
if self._security:
_LOGGER.debug(
"Security is On using the default_on_percent %f",
@@ -98,11 +119,6 @@ class PropAlgorithm:
)
self._on_percent = self._calculated_on_percent
# calculated on_time duration in seconds
if self._on_percent > 1:
self._on_percent = 1
if self._on_percent < 0:
self._on_percent = 0
self._on_time_sec = self._on_percent * self._cycle_min * 60
# Do not heat for less than xx sec

View File

@@ -0,0 +1,594 @@
# pylint: disable=unused-argument
""" Implements the VersatileThermostat sensors component """
import logging
import math
from homeassistant.core import HomeAssistant, callback, Event
from homeassistant.const import UnitOfTime, UnitOfPower, UnitOfEnergy, PERCENTAGE
from homeassistant.components.sensor import (
SensorEntity,
SensorDeviceClass,
SensorStateClass,
UnitOfTemperature,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .commons import VersatileThermostatBaseEntity
from .const import (
CONF_NAME,
CONF_DEVICE_POWER,
CONF_PROP_FUNCTION,
PROPORTIONAL_FUNCTION_TPI,
CONF_THERMOSTAT_SWITCH,
CONF_THERMOSTAT_VALVE,
CONF_THERMOSTAT_CLIMATE,
CONF_THERMOSTAT_TYPE,
)
THRESHOLD_WATT_KILO = 100
_LOGGER = logging.getLogger(__name__)
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the VersatileThermostat sensors 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)
entities = [
LastTemperatureSensor(hass, unique_id, name, entry.data),
LastExtTemperatureSensor(hass, unique_id, name, entry.data),
TemperatureSlopeSensor(hass, unique_id, name, entry.data),
EMATemperatureSensor(hass, unique_id, name, entry.data),
]
if entry.data.get(CONF_DEVICE_POWER):
entities.append(EnergySensor(hass, unique_id, name, entry.data))
if entry.data.get(CONF_THERMOSTAT_TYPE) in [
CONF_THERMOSTAT_SWITCH,
CONF_THERMOSTAT_VALVE,
]:
entities.append(MeanPowerSensor(hass, unique_id, name, entry.data))
if entry.data.get(CONF_PROP_FUNCTION) == PROPORTIONAL_FUNCTION_TPI:
entities.append(OnPercentSensor(hass, unique_id, name, entry.data))
entities.append(OnTimeSensor(hass, unique_id, name, entry.data))
entities.append(OffTimeSensor(hass, unique_id, name, entry.data))
if entry.data.get(CONF_THERMOSTAT_TYPE) == CONF_THERMOSTAT_VALVE:
entities.append(ValveOpenPercentSensor(hass, unique_id, name, entry.data))
if entry.data.get(CONF_THERMOSTAT_TYPE) == CONF_THERMOSTAT_CLIMATE:
entities.append(RegulatedTemperatureSensor(hass, unique_id, name, entry.data))
async_add_entities(entities, True)
class EnergySensor(VersatileThermostatBaseEntity, SensorEntity):
"""Representation of a Energy sensor which exposes the energy"""
def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None:
"""Initialize the energy sensor"""
super().__init__(hass, unique_id, entry_infos.get(CONF_NAME))
self._attr_name = "Energy"
self._attr_unique_id = f"{self._device_name}_energy"
@callback
async def async_my_climate_changed(self, event: Event = None):
"""Called when my climate have change"""
_LOGGER.debug("%s - climate state change", self._attr_unique_id)
if math.isnan(self.my_climate.total_energy) or math.isinf(
self.my_climate.total_energy
):
raise ValueError(f"Sensor has illegal state {self.my_climate.total_energy}")
old_state = self._attr_native_value
self._attr_native_value = round(
self.my_climate.total_energy, self.suggested_display_precision
)
if old_state != self._attr_native_value:
self.async_write_ha_state()
return
@property
def icon(self) -> str | None:
return "mdi:lightning-bolt"
@property
def device_class(self) -> SensorDeviceClass | None:
return SensorDeviceClass.ENERGY
@property
def state_class(self) -> SensorStateClass | None:
return SensorStateClass.TOTAL_INCREASING
@property
def native_unit_of_measurement(self) -> str | None:
if not self.my_climate:
return None
if self.my_climate.device_power > THRESHOLD_WATT_KILO:
return UnitOfEnergy.WATT_HOUR
else:
return UnitOfEnergy.KILO_WATT_HOUR
@property
def suggested_display_precision(self) -> int | None:
"""Return the suggested number of decimal digits for display."""
return 3
class MeanPowerSensor(VersatileThermostatBaseEntity, SensorEntity):
"""Representation of a power sensor which exposes the mean power in a cycle"""
def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None:
"""Initialize the energy sensor"""
super().__init__(hass, unique_id, entry_infos.get(CONF_NAME))
self._attr_name = "Mean power cycle"
self._attr_unique_id = f"{self._device_name}_mean_power_cycle"
@callback
async def async_my_climate_changed(self, event: Event = None):
"""Called when my climate have change"""
_LOGGER.debug("%s - climate state change", self._attr_unique_id)
if math.isnan(float(self.my_climate.mean_cycle_power)) or math.isinf(
self.my_climate.mean_cycle_power
):
raise ValueError(
f"Sensor has illegal state {self.my_climate.mean_cycle_power}"
)
old_state = self._attr_native_value
self._attr_native_value = round(
self.my_climate.mean_cycle_power, self.suggested_display_precision
)
if old_state != self._attr_native_value:
self.async_write_ha_state()
return
@property
def icon(self) -> str | None:
return "mdi:flash-outline"
@property
def device_class(self) -> SensorDeviceClass | None:
return SensorDeviceClass.POWER
@property
def state_class(self) -> SensorStateClass | None:
return SensorStateClass.MEASUREMENT
@property
def native_unit_of_measurement(self) -> str | None:
if not self.my_climate:
return None
if self.my_climate.device_power > THRESHOLD_WATT_KILO:
return UnitOfPower.WATT
else:
return UnitOfPower.KILO_WATT
@property
def suggested_display_precision(self) -> int | None:
"""Return the suggested number of decimal digits for display."""
return 3
class OnPercentSensor(VersatileThermostatBaseEntity, SensorEntity):
"""Representation of a on percent sensor which exposes the on_percent in a cycle"""
def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None:
"""Initialize the energy sensor"""
super().__init__(hass, unique_id, entry_infos.get(CONF_NAME))
self._attr_name = "Power percent"
self._attr_unique_id = f"{self._device_name}_power_percent"
@callback
async def async_my_climate_changed(self, event: Event = None):
"""Called when my climate have change"""
_LOGGER.debug("%s - climate state change", self._attr_unique_id)
on_percent = (
float(self.my_climate.proportional_algorithm.on_percent)
if self.my_climate and self.my_climate.proportional_algorithm
else None
)
if on_percent is None:
return
if math.isnan(on_percent) or math.isinf(on_percent):
raise ValueError(f"Sensor has illegal state {on_percent}")
old_state = self._attr_native_value
self._attr_native_value = round(
on_percent * 100.0, self.suggested_display_precision
)
if old_state != self._attr_native_value:
self.async_write_ha_state()
return
@property
def icon(self) -> str | None:
return "mdi:meter-electric-outline"
@property
def device_class(self) -> SensorDeviceClass | None:
return SensorDeviceClass.POWER_FACTOR
@property
def state_class(self) -> SensorStateClass | None:
return SensorStateClass.MEASUREMENT
@property
def native_unit_of_measurement(self) -> str | None:
return PERCENTAGE
@property
def suggested_display_precision(self) -> int | None:
"""Return the suggested number of decimal digits for display."""
return 1
class ValveOpenPercentSensor(VersatileThermostatBaseEntity, SensorEntity):
"""Representation of a on percent sensor which exposes the on_percent in a cycle"""
def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None:
"""Initialize the energy sensor"""
super().__init__(hass, unique_id, entry_infos.get(CONF_NAME))
self._attr_name = "Vave open percent"
self._attr_unique_id = f"{self._device_name}_valve_open_percent"
@callback
async def async_my_climate_changed(self, event: Event = None):
"""Called when my climate have change"""
_LOGGER.debug("%s - climate state change", self._attr_unique_id)
old_state = self._attr_native_value
self._attr_native_value = self.my_climate.valve_open_percent
if old_state != self._attr_native_value:
self.async_write_ha_state()
return
@property
def icon(self) -> str | None:
return "mdi:pipe-valve"
@property
def device_class(self) -> SensorDeviceClass | None:
return SensorDeviceClass.POWER_FACTOR
@property
def state_class(self) -> SensorStateClass | None:
return SensorStateClass.MEASUREMENT
@property
def native_unit_of_measurement(self) -> str | None:
return PERCENTAGE
@property
def suggested_display_precision(self) -> int | None:
"""Return the suggested number of decimal digits for display."""
return 0
class OnTimeSensor(VersatileThermostatBaseEntity, SensorEntity):
"""Representation of a on time sensor which exposes the on_time_sec in a cycle"""
def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None:
"""Initialize the energy sensor"""
super().__init__(hass, unique_id, entry_infos.get(CONF_NAME))
self._attr_name = "On time"
self._attr_unique_id = f"{self._device_name}_on_time"
@callback
async def async_my_climate_changed(self, event: Event = None):
"""Called when my climate have change"""
_LOGGER.debug("%s - climate state change", self._attr_unique_id)
on_time = (
float(self.my_climate.proportional_algorithm.on_time_sec)
if self.my_climate and self.my_climate.proportional_algorithm
else None
)
if on_time is None:
return
if math.isnan(on_time) or math.isinf(on_time):
raise ValueError(f"Sensor has illegal state {on_time}")
old_state = self._attr_native_value
self._attr_native_value = round(on_time)
if old_state != self._attr_native_value:
self.async_write_ha_state()
return
@property
def icon(self) -> str | None:
return "mdi:timer-play"
@property
def device_class(self) -> SensorDeviceClass | None:
return SensorDeviceClass.DURATION
@property
def state_class(self) -> SensorStateClass | None:
return SensorStateClass.MEASUREMENT
@property
def native_unit_of_measurement(self) -> str | None:
return UnitOfTime.SECONDS
class OffTimeSensor(VersatileThermostatBaseEntity, SensorEntity):
"""Representation of a on time sensor which exposes the off_time_sec in a cycle"""
def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None:
"""Initialize the energy sensor"""
super().__init__(hass, unique_id, entry_infos.get(CONF_NAME))
self._attr_name = "Off time"
self._attr_unique_id = f"{self._device_name}_off_time"
@callback
async def async_my_climate_changed(self, event: Event = None):
"""Called when my climate have change"""
_LOGGER.debug("%s - climate state change", self._attr_unique_id)
off_time = (
float(self.my_climate.proportional_algorithm.off_time_sec)
if self.my_climate and self.my_climate.proportional_algorithm
else None
)
if off_time is None:
return
if math.isnan(off_time) or math.isinf(off_time):
raise ValueError(f"Sensor has illegal state {off_time}")
old_state = self._attr_native_value
self._attr_native_value = round(off_time)
if old_state != self._attr_native_value:
self.async_write_ha_state()
return
@property
def icon(self) -> str | None:
return "mdi:timer-off-outline"
@property
def device_class(self) -> SensorDeviceClass | None:
return SensorDeviceClass.DURATION
@property
def state_class(self) -> SensorStateClass | None:
return SensorStateClass.MEASUREMENT
@property
def native_unit_of_measurement(self) -> str | None:
return UnitOfTime.SECONDS
class LastTemperatureSensor(VersatileThermostatBaseEntity, SensorEntity):
"""Representation of a last temperature datetime sensor"""
def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None:
"""Initialize the last temperature datetime sensor"""
super().__init__(hass, unique_id, entry_infos.get(CONF_NAME))
self._attr_name = "Last temperature date"
self._attr_unique_id = f"{self._device_name}_last_temp_datetime"
@callback
async def async_my_climate_changed(self, event: Event = None):
"""Called when my climate have change"""
_LOGGER.debug("%s - climate state change", self._attr_unique_id)
old_state = self._attr_native_value
self._attr_native_value = self.my_climate.last_temperature_mesure
if old_state != self._attr_native_value:
self.async_write_ha_state()
return
@property
def icon(self) -> str | None:
return "mdi:home-clock"
@property
def device_class(self) -> SensorDeviceClass | None:
return SensorDeviceClass.TIMESTAMP
class LastExtTemperatureSensor(VersatileThermostatBaseEntity, SensorEntity):
"""Representation of a last external temperature datetime sensor"""
def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None:
"""Initialize the last temperature datetime sensor"""
super().__init__(hass, unique_id, entry_infos.get(CONF_NAME))
self._attr_name = "Last external temperature date"
self._attr_unique_id = f"{self._device_name}_last_ext_temp_datetime"
@callback
async def async_my_climate_changed(self, event: Event = None):
"""Called when my climate have change"""
_LOGGER.debug("%s - climate state change", self._attr_unique_id)
old_state = self._attr_native_value
self._attr_native_value = self.my_climate.last_ext_temperature_mesure
if old_state != self._attr_native_value:
self.async_write_ha_state()
return
@property
def icon(self) -> str | None:
return "mdi:sun-clock"
@property
def device_class(self) -> SensorDeviceClass | None:
return SensorDeviceClass.TIMESTAMP
class TemperatureSlopeSensor(VersatileThermostatBaseEntity, SensorEntity):
"""Representation of a sensor which exposes the temperature slope curve"""
def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None:
"""Initialize the slope sensor"""
super().__init__(hass, unique_id, entry_infos.get(CONF_NAME))
self._attr_name = "Temperature slope"
self._attr_unique_id = f"{self._device_name}_temperature_slope"
@callback
async def async_my_climate_changed(self, event: Event = None):
"""Called when my climate have change"""
_LOGGER.debug("%s - climate state change", self._attr_unique_id)
last_slope = self.my_climate.last_temperature_slope
if last_slope is None:
return
if math.isnan(last_slope) or math.isinf(last_slope):
raise ValueError(f"Sensor has illegal state {last_slope}")
old_state = self._attr_native_value
self._attr_native_value = round(last_slope, self.suggested_display_precision)
if old_state != self._attr_native_value:
self.async_write_ha_state()
return
@property
def icon(self) -> str | None:
if self._attr_native_value is None or self._attr_native_value == 0:
return "mdi:thermometer"
elif self._attr_native_value > 0:
return "mdi:thermometer-chevron-up"
else:
return "mdi:thermometer-chevron-down"
@property
def state_class(self) -> SensorStateClass | None:
return SensorStateClass.MEASUREMENT
@property
def native_unit_of_measurement(self) -> str | None:
if not self.my_climate:
return None
return self.my_climate.temperature_unit + "/hour"
@property
def suggested_display_precision(self) -> int | None:
"""Return the suggested number of decimal digits for display."""
return 2
class RegulatedTemperatureSensor(VersatileThermostatBaseEntity, SensorEntity):
"""Representation of a Energy sensor which exposes the energy"""
def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None:
"""Initialize the regulated temperature sensor"""
super().__init__(hass, unique_id, entry_infos.get(CONF_NAME))
self._attr_name = "Regulated temperature"
self._attr_unique_id = f"{self._device_name}_regulated_temperature"
@callback
async def async_my_climate_changed(self, event: Event = None):
"""Called when my climate have change"""
_LOGGER.debug("%s - climate state change", self._attr_unique_id)
new_temp = self.my_climate.regulated_target_temp
if new_temp is None:
return
if math.isnan(new_temp) or math.isinf(new_temp):
raise ValueError(f"Sensor has illegal state {new_temp}")
old_state = self._attr_native_value
self._attr_native_value = round(new_temp, self.suggested_display_precision)
if old_state != self._attr_native_value:
self.async_write_ha_state()
return
@property
def icon(self) -> str | None:
return "mdi:thermometer-auto"
@property
def device_class(self) -> SensorDeviceClass | None:
return SensorDeviceClass.TEMPERATURE
@property
def state_class(self) -> SensorStateClass | None:
return SensorStateClass.MEASUREMENT
@property
def native_unit_of_measurement(self) -> str | None:
if not self.my_climate:
return UnitOfTemperature.CELSIUS
return self.my_climate.temperature_unit
@property
def suggested_display_precision(self) -> int | None:
"""Return the suggested number of decimal digits for display."""
return 1
class EMATemperatureSensor(VersatileThermostatBaseEntity, SensorEntity):
"""Representation of a Exponential Moving Average temp"""
def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None:
"""Initialize the regulated temperature sensor"""
super().__init__(hass, unique_id, entry_infos.get(CONF_NAME))
self._attr_name = "EMA temperature"
self._attr_unique_id = f"{self._device_name}_ema_temperature"
@callback
async def async_my_climate_changed(self, event: Event = None):
"""Called when my climate have change"""
_LOGGER.debug("%s - climate state change", self._attr_unique_id)
new_ema = self.my_climate.ema_temperature
if new_ema is None:
return
if math.isnan(new_ema) or math.isinf(new_ema):
raise ValueError(f"Sensor has illegal state {new_ema}")
old_state = self._attr_native_value
self._attr_native_value = new_ema
if old_state != self._attr_native_value:
self.async_write_ha_state()
return
@property
def icon(self) -> str | None:
return "mdi:thermometer-lines"
@property
def device_class(self) -> SensorDeviceClass | None:
return SensorDeviceClass.TEMPERATURE
@property
def state_class(self) -> SensorStateClass | None:
return SensorStateClass.MEASUREMENT
@property
def native_unit_of_measurement(self) -> str | None:
if not self.my_climate:
return UnitOfTemperature.CELSIUS
return self.my_climate.temperature_unit
@property
def suggested_display_precision(self) -> int | None:
"""Return the suggested number of decimal digits for display."""
return 2

View File

@@ -1,117 +1,163 @@
reload:
name: Reload
description: Reload all Versatile Thermostat entities.
set_presence:
name: Set presence
description: Force the presence mode in thermostat
target:
entity:
integration: versatile_thermostat
fields:
presence:
name: Presence
description: Presence setting
required: true
advanced: false
example: "on"
default: "on"
selector:
select:
options:
- "on"
- "off"
- "home"
- "not_home"
name: Set presence
description: Force the presence mode in thermostat
target:
entity:
integration: versatile_thermostat
fields:
presence:
name: Presence
description: Presence setting
required: true
advanced: false
example: "on"
default: "on"
selector:
select:
options:
- "on"
- "off"
- "home"
- "not_home"
set_preset_temperature:
name: Set temperature preset
description: Change the target temperature of a preset
target:
entity:
integration: versatile_thermostat
fields:
preset:
name: Preset
description: Preset name
required: true
advanced: false
example: "comfort"
selector:
select:
options:
- "eco"
- "comfort"
- "boost"
temperature:
name: Temperature when present
description: Target temperature for the preset when present
required: false
advanced: false
example: "19.5"
default: "17"
selector:
number:
min: 7
max: 35
step: 0.1
unit_of_measurement: °
mode: slider
temperature_away:
name: Temperature when not present
description: Target temperature for the preset when not present
required: false
advanced: false
example: "17"
default: "15"
selector:
number:
min: 7
max: 35
step: 0.1
unit_of_measurement: °
mode: slider
name: Set temperature preset
description: Change the target temperature of a preset
target:
entity:
integration: versatile_thermostat
fields:
preset:
name: Preset
description: Preset name
required: true
advanced: false
example: "comfort"
selector:
select:
options:
- "eco"
- "comfort"
- "boost"
- "eco_ac"
- "comfort_ac"
- "boost_ac"
temperature:
name: Temperature when present
description: Target temperature for the preset when present
required: false
advanced: false
example: "19.5"
default: "17"
selector:
number:
min: 7
max: 35
step: 0.1
unit_of_measurement: °
mode: slider
temperature_away:
name: Temperature when not present
description: Target temperature for the preset when not present
required: false
advanced: false
example: "17"
default: "15"
selector:
number:
min: 7
max: 35
step: 0.1
unit_of_measurement: °
mode: slider
set_security:
name: Set security
description: Change the security parameters
target:
entity:
integration: versatile_thermostat
fields:
delay_min:
name: Delay in minutes
description: Maximum allowed delay in minutes between two temperature mesures
required: false
advanced: false
example: "30"
selector:
number:
min: 0
max: 9999
unit_of_measurement: "min"
mode: box
min_on_percent:
name: Minimal on_percent
description: Minimal heating percent value for security preset activation
required: false
advanced: false
example: "0.5"
default: "0.5"
selector:
number:
min: 0
max: 1
step: 0.05
unit_of_measurement: "%"
mode: slider
default_on_percent:
name: on_percent used in security mode
description: The default heating percent value in security preset
required: false
advanced: false
example: "0.1"
default: "0.1"
selector:
number:
min: 0
max: 1
step: 0.05
unit_of_measurement: "%"
mode: slider
name: Set security
description: Change the security parameters
target:
entity:
integration: versatile_thermostat
fields:
delay_min:
name: Delay in minutes
description: Maximum allowed delay in minutes between two temperature mesures
required: false
advanced: false
example: "30"
selector:
number:
min: 0
max: 9999
unit_of_measurement: "min"
mode: box
min_on_percent:
name: Minimal on_percent
description: Minimal heating percent value for security preset activation
required: false
advanced: false
example: "0.5"
default: "0.5"
selector:
number:
min: 0
max: 1
step: 0.05
unit_of_measurement: "%"
mode: slider
default_on_percent:
name: on_percent used in security mode
description: The default heating percent value in security preset
required: false
advanced: false
example: "0.1"
default: "0.1"
selector:
number:
min: 0
max: 1
step: 0.05
unit_of_measurement: "%"
mode: slider
set_window_bypass:
name: Set Window ByPass
description: Bypass the window state to enable heating with window open.
target:
entity:
integration: versatile_thermostat
fields:
window_bypass:
name: Window ByPass
description: ByPass value
required: true
advanced: false
default: true
selector:
boolean:
set_auto_regulation_mode:
name: Set Auto Regulation mode
description: Change the mode of self-regulation (only for VTherm over climate)
target:
entity:
integration: versatile_thermostat
fields:
auto_regulation_mode:
name: Auto regulation mode
description: Possible values
required: true
advanced: false
default: true
selector:
select:
options:
- "None"
- "Light"
- "Medium"
- "Strong"
- "Slow"
- "Expert"

View File

@@ -14,6 +14,7 @@
"cycle_min": "Cycle duration (minutes)",
"temp_min": "Minimal temperature allowed",
"temp_max": "Maximal temperature allowed",
"device_power": "Device power",
"use_window_feature": "Use window detection",
"use_motion_feature": "Use motion detection",
"use_power_feature": "Use power management",
@@ -21,12 +22,47 @@
}
},
"type": {
"title": "Linked entity",
"description": "Linked entity attributes",
"title": "Linked entities",
"description": "Linked entities attributes",
"data": {
"heater_entity_id": "Heater entity id",
"heater_entity_id": "1rst heater switch",
"heater_entity2_id": "2nd heater switch",
"heater_entity3_id": "3rd heater switch",
"heater_entity4_id": "4th heater switch",
"proportional_function": "Algorithm",
"climate_entity_id": "1rst underlying climate",
"climate_entity2_id": "2nd underlying climate",
"climate_entity3_id": "3rd underlying climate",
"climate_entity4_id": "4th underlying climate",
"ac_mode": "AC mode",
"valve_entity_id": "1rst valve number",
"valve_entity2_id": "2nd valve number",
"valve_entity3_id": "3rd valve number",
"valve_entity4_id": "4th valve number",
"auto_regulation_mode": "Self-regulation",
"auto_regulation_dtemp": "Regulation threshold",
"auto_regulation_periode_min": "Regulation minimal period",
"inverse_switch_command": "Inverse switch command"
},
"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",
"proportional_function": "Algorithm to use (TPI is the only one for now)",
"climate_entity_id": "Underlying thermostat entity id"
"climate_entity_id": "Underlying climate entity id",
"climate_entity2_id": "2nd underlying climate entity id",
"climate_entity3_id": "3rd underlying climate entity id",
"climate_entity4_id": "4th underlying climate entity id",
"ac_mode": "Use the Air Conditioning (AC) mode",
"valve_entity_id": "1rst valve number entity id",
"valve_entity2_id": "2nd valve number entity id",
"valve_entity3_id": "3rd valve number entity id",
"valve_entity4_id": "4th valve number entity id",
"auto_regulation_mode": "Auto adjustment of the target temperature",
"auto_regulation_dtemp": "The threshold in ° under which the temperature change will not be send",
"auto_regulation_periode_min": "Duration in minutes between two regulation update",
"inverse_switch_command": "For switch with pilot wire and diode you may need to inverse the command"
}
},
"tpi": {
@@ -43,15 +79,28 @@
"data": {
"eco_temp": "Temperature in Eco preset",
"comfort_temp": "Temperature in Comfort preset",
"boost_temp": "Temperature in Boost preset"
"boost_temp": "Temperature in Boost preset",
"eco_ac_temp": "Temperature in Eco preset for AC mode",
"comfort_ac_temp": "Temperature in Comfort preset for AC mode",
"boost_ac_temp": "Temperature in Boost preset for AC mode"
}
},
"window": {
"title": "Window management",
"description": "Open window management.\nLeave corresponding entity_id empty if not used.",
"description": "Open window management.\nLeave corresponding entity_id empty if not used\nYou can also configure automatic window open detection based on temperature decrease",
"data": {
"window_sensor_entity_id": "Window sensor entity id",
"window_delay": "Window delay (seconds)"
"window_delay": "Window sensor delay (seconds)",
"window_auto_open_threshold": "Temperature decrease threshold for automatic window open detection (in °/hours)",
"window_auto_close_threshold": "Temperature increase threshold for end of automatic detection (in °/hours)",
"window_auto_max_duration": "Maximum duration of automatic window open detection (in min)"
},
"data_description": {
"window_sensor_entity_id": "Leave empty if no window sensor should be use",
"window_delay": "The delay in seconds before sensor detection is taken into account",
"window_auto_open_threshold": "Recommended value: between 3 and 10. Leave empty if automatic window open detection is not use",
"window_auto_close_threshold": "Recommended value: 0. Leave empty if automatic window open detection is not use",
"window_auto_max_duration": "Recommended value: 60 (one hour). Leave empty if automatic window open detection is not use"
}
},
"motion": {
@@ -59,7 +108,15 @@
"description": "Motion sensor management. Preset can switch automatically depending of a motion detection\nLeave corresponding entity_id empty if not used.\nmotion_preset and no_motion_preset should be set to the corresponding preset name",
"data": {
"motion_sensor_entity_id": "Motion sensor entity id",
"motion_delay": "Motion delay (seconds)",
"motion_delay": "Activation delay",
"motion_off_delay": "Deactivation delay",
"motion_preset": "Motion preset",
"no_motion_preset": "No motion preset"
},
"data_description": {
"motion_sensor_entity_id": "The entity id of the motion sensor",
"motion_delay": "Motion activation activation delay (seconds)",
"motion_off_delay": "Motion deactivation delay (seconds)",
"motion_preset": "Preset to use when motion is detected",
"no_motion_preset": "Preset to use when no motion is detected"
}
@@ -70,7 +127,6 @@
"data": {
"power_sensor_entity_id": "Power sensor entity id",
"max_power_sensor_entity_id": "Max power sensor entity id",
"device_power": "Device power (kW)",
"power_temp": "Temperature for Power shedding"
}
},
@@ -78,26 +134,36 @@
"title": "Presence management",
"description": "Presence management attributes.\nGives the a presence sensor of your home (true is someone is present).\nThen specify either the preset to use when presence sensor is false or the offset in temperature to apply.\nIf preset is given, the offset will not be used.\nLeave corresponding entity_id empty if not used.",
"data": {
"presence_sensor_entity_id": "Presence sensor entity id (true is present)",
"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"
"boost_away_temp": "Temperature in Boost preset when no presence",
"eco_ac_away_temp": "Temperature in Eco preset when no presence in AC mode",
"comfort_ac_away_temp": "Temperature in Comfort preset when no presence in AC mode",
"boost_ac_away_temp": "Temperature in Boost preset when no presence in AC mode"
}
},
"advanced": {
"title": "Advanced parameters",
"description": "Configuration of advanced parameters. Leave the default values if you don't know what you are doing.\nThis parameters can lead to a very bad temperature or power regulation.",
"data": {
"minimal_activation_delay": "Delay in secondes under which the equipment will not be activated",
"security_delay_min": "Maximum allowed delay in minutes between two temperature mesures. Above this delay, the thermostat will turn to a sceurity off state",
"security_min_on_percent": "Minimal heating percent value for security preset activation. Below this amount of on_percent the thermostat won't go into security preset",
"security_default_on_percent": "The default heating percent value in security preset. Set to 0 to switch off heater in security present"
"minimal_activation_delay": "Minimal activation delay",
"security_delay_min": "Security delay (in minutes)",
"security_min_on_percent": "Minimal power percent to enable security mode",
"security_default_on_percent": "Power percent to use in security mode"
},
"data_description": {
"minimal_activation_delay": "Delay in seconds under which the equipment will not be activated",
"security_delay_min": "Maximum allowed delay in minutes between two temperature mesures. Above this delay, the thermostat will turn to a security off state",
"security_min_on_percent": "Minimal heating percent value for security preset activation. Below this amount of power percent the thermostat won't go into security preset",
"security_default_on_percent": "The default heating power percent value in security preset. Set to 0 to switch off heater in security present"
}
}
},
"error": {
"unknown": "Unexpected error",
"unknown_entity": "Unknown entity id"
"unknown_entity": "Unknown entity id",
"window_open_detection_method": "Only one window open detection method should be used. Use sensor or automatic detection through temperature threshold but not both"
},
"abort": {
"already_configured": "Device is already configured"
@@ -117,6 +183,7 @@
"cycle_min": "Cycle duration (minutes)",
"temp_min": "Minimal temperature allowed",
"temp_max": "Maximal temperature allowed",
"device_power": "Device power (kW)",
"use_window_feature": "Use window detection",
"use_motion_feature": "Use motion detection",
"use_power_feature": "Use power management",
@@ -124,12 +191,47 @@
}
},
"type": {
"title": "Linked entity",
"description": "Linked entity attributes",
"title": "Linked entities",
"description": "Linked entities attributes",
"data": {
"heater_entity_id": "Heater entity id",
"heater_entity_id": "1rst heater switch",
"heater_entity2_id": "2nd heater switch",
"heater_entity3_id": "3rd heater switch",
"heater_entity4_id": "4th heater switch",
"proportional_function": "Algorithm",
"climate_entity_id": "1rst underlying climate",
"climate_entity2_id": "2nd underlying climate",
"climate_entity3_id": "3rd underlying climate",
"climate_entity4_id": "4th underlying climate",
"ac_mode": "AC mode",
"valve_entity_id": "1rst valve number",
"valve_entity2_id": "2nd valve number",
"valve_entity3_id": "3rd valve number",
"valve_entity4_id": "4th valve number",
"auto_regulation_mode": "Self-regulation",
"auto_regulation_dtemp": "Regulation threshold",
"auto_regulation_periode_min": "Regulation minimal period",
"inverse_switch_command": "Inverse switch command"
},
"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",
"proportional_function": "Algorithm to use (TPI is the only one for now)",
"climate_entity_id": "Underlying thermostat entity id"
"climate_entity_id": "Underlying climate entity id",
"climate_entity2_id": "2nd underlying climate entity id",
"climate_entity3_id": "3rd underlying climate entity id",
"climate_entity4_id": "4th underlying climate entity id",
"ac_mode": "Use the Air Conditioning (AC) mode",
"valve_entity_id": "1rst valve number entity id",
"valve_entity2_id": "2nd valve number entity id",
"valve_entity3_id": "3rd valve number entity id",
"valve_entity4_id": "4th valve number entity id",
"auto_regulation_mode": "Auto adjustment of the target temperature",
"auto_regulation_dtemp": "The threshold in ° under which the temperature change will not be send",
"auto_regulation_periode_min": "Duration in minutes between two regulation update",
"inverse_switch_command": "For switch with pilot wire and diode you may need to inverse the command"
}
},
"tpi": {
@@ -146,15 +248,28 @@
"data": {
"eco_temp": "Temperature in Eco preset",
"comfort_temp": "Temperature in Comfort preset",
"boost_temp": "Temperature in Boost preset"
"boost_temp": "Temperature in Boost preset",
"eco_ac_temp": "Temperature in Eco preset for AC mode",
"comfort_ac_temp": "Temperature in Comfort preset for AC mode",
"boost_ac_temp": "Temperature in Boost preset for AC mode"
}
},
"window": {
"title": "Window management",
"description": "Open window management.\nLeave corresponding entity_id empty if not used.",
"description": "Open window management.\nLeave corresponding entity_id empty if not used\nYou can also configure automatic window open detection based on temperature decrease",
"data": {
"window_sensor_entity_id": "Window sensor entity id",
"window_delay": "Window delay (seconds)"
"window_delay": "Window sensor delay (seconds)",
"window_auto_open_threshold": "Temperature decrease threshold for automatic window open detection (in °/hours)",
"window_auto_close_threshold": "Temperature increase threshold for end of automatic detection (in °/hours)",
"window_auto_max_duration": "Maximum duration of automatic window open detection (in min)"
},
"data_description": {
"window_sensor_entity_id": "Leave empty if no window sensor should be use",
"window_delay": "The delay in seconds before sensor detection is taken into account",
"window_auto_open_threshold": "Recommended value: between 3 and 10. Leave empty if automatic window open detection is not use",
"window_auto_close_threshold": "Recommended value: 0. Leave empty if automatic window open detection is not use",
"window_auto_max_duration": "Recommended value: 60 (one hour). Leave empty if automatic window open detection is not use"
}
},
"motion": {
@@ -162,7 +277,15 @@
"description": "Motion sensor management. Preset can switch automatically depending of a motion detection\nLeave corresponding entity_id empty if not used.\nmotion_preset and no_motion_preset should be set to the corresponding preset name",
"data": {
"motion_sensor_entity_id": "Motion sensor entity id",
"motion_delay": "Motion delay (seconds)",
"motion_delay": "Activation delay",
"motion_off_delay": "Deactivation delay",
"motion_preset": "Motion preset",
"no_motion_preset": "No motion preset"
},
"data_description": {
"motion_sensor_entity_id": "The entity id of the motion sensor",
"motion_delay": "Motion activation activation delay (seconds)",
"motion_off_delay": "Motion deactivation delay (seconds)",
"motion_preset": "Preset to use when motion is detected",
"no_motion_preset": "Preset to use when no motion is detected"
}
@@ -173,7 +296,6 @@
"data": {
"power_sensor_entity_id": "Power sensor entity id",
"max_power_sensor_entity_id": "Max power sensor entity id",
"device_power": "Device power (kW)",
"power_temp": "Temperature for Power shedding"
}
},
@@ -184,23 +306,33 @@
"presence_sensor_entity_id": "Presence sensor entity id (true is present)",
"eco_away_temp": "Temperature in Eco preset when no presence",
"comfort_away_temp": "Temperature in Comfort preset when no presence",
"boost_away_temp": "Temperature in Boost preset when no presence"
"boost_away_temp": "Temperature in Boost preset when no presence",
"eco_ac_away_temp": "Temperature in Eco preset when no presence in AC mode",
"comfort_ac_away_temp": "Temperature in Comfort preset when no presence in AC mode",
"boost_ac_away_temp": "Temperature in Boost preset when no presence in AC mode"
}
},
"advanced": {
"title": "Advanced parameters",
"description": "Configuration of advanced parameters. Leave the default values if you don't know what you are doing.\nThis parameters can lead to a very bad temperature or power regulation.",
"data": {
"minimal_activation_delay": "Delay in secondes under which the equipment will not be activated",
"minimal_activation_delay": "Minimal activation delay",
"security_delay_min": "Security delay (in minutes)",
"security_min_on_percent": "Minimal power percent for security mode",
"security_default_on_percent": "Power percent to use in security mode"
},
"data_description": {
"minimal_activation_delay": "Delay in seconds under which the equipment will not be activated",
"security_delay_min": "Maximum allowed delay in minutes between two temperature mesures. Above this delay, the thermostat will turn to a security off state",
"security_min_on_percent": "Minimal heating percent value for security preset activation. Below this amount of on_percent the thermostat won't go into security preset",
"security_default_on_percent": "The default heating percent value in security preset. Set to 0 to switch off heater in security present"
"security_min_on_percent": "Minimal heating percent value for security preset activation. Below this amount of power percent the thermostat won't go into security preset",
"security_default_on_percent": "The default heating power percent value in security preset. Set to 0 to switch off heater in security present"
}
}
},
"error": {
"unknown": "Unexpected error",
"unknown_entity": "Unknown entity id"
"unknown_entity": "Unknown entity id",
"window_open_detection_method": "Only one window open detection method should be used. Use sensor or automatic detection through temperature threshold but not both"
},
"abort": {
"already_configured": "Device is already configured"
@@ -210,7 +342,18 @@
"thermostat_type": {
"options": {
"thermostat_over_switch": "Thermostat over a switch",
"thermostat_over_climate": "Thermostat over another thermostat"
"thermostat_over_climate": "Thermostat over a climate",
"thermostat_over_valve": "Thermostat over a valve"
}
},
"auto_regulation_mode": {
"options": {
"auto_regulation_slow": "Slow",
"auto_regulation_strong": "Strong",
"auto_regulation_medium": "Medium",
"auto_regulation_light": "Light",
"auto_regulation_expert": "Expert",
"auto_regulation_none": "No auto-regulation"
}
}
},
@@ -228,16 +371,5 @@
}
}
}
},
"state_attributes": {
"_": {
"preset_mode": {
"state": {
"power": "Shedding",
"security": "Security",
"none": "Manual"
}
}
}
}
}

View File

@@ -0,0 +1,797 @@
# pylint: disable=line-too-long
""" A climate over switch classe """
import logging
from datetime import timedelta, datetime
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.event import (
async_track_state_change_event,
async_track_time_interval,
)
from homeassistant.components.climate import HVACAction, HVACMode
from .commons import NowClass, round_to_nearest
from .base_thermostat import BaseThermostat
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,
RegulationParamSlow,
RegulationParamLight,
RegulationParamMedium,
RegulationParamStrong,
)
from .vtherm_api import VersatileThermostatAPI
from .underlyings import UnderlyingClimate
_LOGGER = logging.getLogger(__name__)
class ThermostatOverClimate(BaseThermostat):
"""Representation of a base class for a Versatile Thermostat over a climate"""
_auto_regulation_mode: str = None
_regulation_algo = None
_regulated_target_temp: float = None
_auto_regulation_dtemp: float = None
_auto_regulation_period_min: int = None
_last_regulation_change: datetime = None
_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",
}
)
)
)
def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None:
"""Initialize the thermostat over switch."""
# super.__init__ calls post_init at the end. So it must be called after regulation initialization
super().__init__(hass, unique_id, name, entry_infos)
self._regulated_target_temp = self.target_temperature
self._last_regulation_change = NowClass.get_now(hass)
@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"""
# if one not IDLE or OFF -> return it
# else if one IDLE -> IDLE
# else OFF
one_idle = False
for under in self._underlyings:
if (action := under.hvac_action) not in [
HVACAction.IDLE,
HVACAction.OFF,
]:
return action
if under.hvac_action == HVACAction.IDLE:
one_idle = True
if one_idle:
return HVACAction.IDLE
return HVACAction.OFF
@overrides
async def _async_internal_set_temperature(self, temperature):
"""Set the target temperature and the target temperature of underlying climate if any"""
await super()._async_internal_set_temperature(temperature)
self._regulation_algo.set_target_temp(self.target_temperature)
await self._send_regulated_temperature(force=True)
async def _send_regulated_temperature(self, force=False):
"""Sends the regulated temperature to all underlying"""
_LOGGER.info(
"%s - Calling ThermostatClimate._send_regulated_temperature force=%s",
self,
force,
)
now: datetime = NowClass.get_now(self._hass)
period = float((now - self._last_regulation_change).total_seconds()) / 60.0
if not force and period < self._auto_regulation_period_min:
_LOGGER.info(
"%s - period (%.1f) min is < %.0f min -> forget the regulation send",
self,
period,
self._auto_regulation_period_min,
)
return
if not self._regulated_target_temp:
self._regulated_target_temp = self.target_temperature
_LOGGER.info("%s - regulation calculation will be done", self)
self._last_regulation_change = now
new_regulated_temp = round_to_nearest(
self._regulation_algo.calculate_regulated_temperature(
self.current_temperature, self._cur_ext_temp
),
self._auto_regulation_dtemp,
)
dtemp = new_regulated_temp - self._regulated_target_temp
if not force and abs(dtemp) < self._auto_regulation_dtemp:
_LOGGER.info(
"%s - dtemp (%.1f) is < %.1f -> forget the regulation send",
self,
dtemp,
self._auto_regulation_dtemp,
)
return
self._regulated_target_temp = new_regulated_temp
_LOGGER.info(
"%s - Regulated temp have changed to %.1f. Resend it to underlyings",
self,
new_regulated_temp,
)
for under in self._underlyings:
await under.set_temperature(
self.regulated_target_temp, self._attr_max_temp, self._attr_min_temp
)
@overrides
def post_init(self, entry_infos):
"""Initialize the Thermostat"""
super().post_init(entry_infos)
for climate in [
CONF_CLIMATE,
CONF_CLIMATE_2,
CONF_CLIMATE_3,
CONF_CLIMATE_4,
]:
if entry_infos.get(climate):
self._underlyings.append(
UnderlyingClimate(
hass=self._hass,
thermostat=self,
climate_entity_id=entry_infos.get(climate),
)
)
self.choose_auto_regulation_mode(
entry_infos.get(CONF_AUTO_REGULATION_MODE)
if entry_infos.get(CONF_AUTO_REGULATION_MODE) is not None
else CONF_AUTO_REGULATION_NONE
)
self._auto_regulation_dtemp = (
entry_infos.get(CONF_AUTO_REGULATION_DTEMP)
if entry_infos.get(CONF_AUTO_REGULATION_DTEMP) is not None
else 0.5
)
self._auto_regulation_period_min = (
entry_infos.get(CONF_AUTO_REGULATION_PERIOD_MIN)
if entry_infos.get(CONF_AUTO_REGULATION_PERIOD_MIN) is not None
else 5
)
def choose_auto_regulation_mode(self, auto_regulation_mode):
"""Choose or change the regulation mode"""
self._auto_regulation_mode = auto_regulation_mode
if self._auto_regulation_mode == CONF_AUTO_REGULATION_LIGHT:
self._regulation_algo = PITemperatureRegulator(
self.target_temperature,
RegulationParamLight.kp,
RegulationParamLight.ki,
RegulationParamLight.k_ext,
RegulationParamLight.offset_max,
RegulationParamLight.stabilization_threshold,
RegulationParamLight.accumulated_error_threshold,
)
elif self._auto_regulation_mode == CONF_AUTO_REGULATION_MEDIUM:
self._regulation_algo = PITemperatureRegulator(
self.target_temperature,
RegulationParamMedium.kp,
RegulationParamMedium.ki,
RegulationParamMedium.k_ext,
RegulationParamMedium.offset_max,
RegulationParamMedium.stabilization_threshold,
RegulationParamMedium.accumulated_error_threshold,
)
elif self._auto_regulation_mode == CONF_AUTO_REGULATION_STRONG:
self._regulation_algo = PITemperatureRegulator(
self.target_temperature,
RegulationParamStrong.kp,
RegulationParamStrong.ki,
RegulationParamStrong.k_ext,
RegulationParamStrong.offset_max,
RegulationParamStrong.stabilization_threshold,
RegulationParamStrong.accumulated_error_threshold,
)
elif self._auto_regulation_mode == CONF_AUTO_REGULATION_SLOW:
self._regulation_algo = PITemperatureRegulator(
self.target_temperature,
RegulationParamSlow.kp,
RegulationParamSlow.ki,
RegulationParamSlow.k_ext,
RegulationParamSlow.offset_max,
RegulationParamSlow.stabilization_threshold,
RegulationParamSlow.accumulated_error_threshold,
)
elif self._auto_regulation_mode == CONF_AUTO_REGULATION_EXPERT:
api: VersatileThermostatAPI = VersatileThermostatAPI.get_vtherm_api(
self._hass
)
if api is not None:
if (expert_param := api.self_regulation_expert) is not None:
self._regulation_algo = PITemperatureRegulator(
self.target_temperature,
expert_param.get("kp"),
expert_param.get("ki"),
expert_param.get("k_ext"),
expert_param.get("offset_max"),
expert_param.get("stabilization_threshold"),
expert_param.get("accumulated_error_threshold"),
)
else:
_LOGGER.error(
"%s - Cannot initialize Expert self-regulation mode due to VTherm API doesn't exists. Please contact the publisher of the integration",
self,
)
else:
_LOGGER.error(
"%s - Cannot initialize Expert self-regulation mode cause the configuration in configuration.yaml have not been found. Please see readme documentation for %s",
self,
DOMAIN,
)
if not self._regulation_algo:
# A default empty algo (which does nothing)
self._regulation_algo = PITemperatureRegulator(
self.target_temperature, 0, 0, 0, 0, 0.1, 0
)
@overrides
async def async_added_to_hass(self):
"""Run when entity about to be added."""
_LOGGER.debug("Calling async_added_to_hass")
await super().async_added_to_hass()
# Add listener to all underlying entities
for climate in self._underlyings:
self.async_on_remove(
async_track_state_change_event(
self.hass, [climate.entity_id], self._async_climate_changed
)
)
# Start the control_heating
# starts a cycle
self.async_on_remove(
async_track_time_interval(
self.hass,
self.async_control_heating,
interval=timedelta(minutes=self._cycle_min),
)
)
@overrides
def restore_specific_previous_state(self, old_state):
"""Restore my specific attributes from previous state"""
old_error = old_state.attributes.get("regulation_accumulated_error")
if old_error:
self._regulation_algo.set_accumulated_error(old_error)
_LOGGER.debug(
"%s - Old regulation accumulated_error have been restored to %f",
self,
old_error,
)
@overrides
def update_custom_attributes(self):
"""Custom attributes"""
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
)
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.async_write_ha_state()
_LOGGER.debug(
"%s - Calling update_custom_attributes: %s",
self,
self._attr_extra_state_attributes,
)
@overrides
def recalculate(self):
"""A utility function to force the calculation of a the algo and
update the custom attributes and write the state
"""
_LOGGER.debug("%s - recalculate all", self)
self.update_custom_attributes()
self.async_write_ha_state()
@overrides
async def restore_hvac_mode(self, need_control_heating=False):
"""Restore a previous hvac_mod"""
old_hvac_mode = self.hvac_mode
await super().restore_hvac_mode(need_control_heating=need_control_heating)
# Issue 133 - force the temperature in over_climate mode if unerlying are turned on
if old_hvac_mode == HVACMode.OFF and self.hvac_mode != HVACMode.OFF:
_LOGGER.info(
"%s - Force resent target temp cause we turn on some over climate"
)
await self._async_internal_set_temperature(self._target_temp)
@overrides
def incremente_energy(self):
"""increment the energy counter if device is active"""
if self.hvac_mode == HVACMode.OFF:
return
added_energy = 0
if self.is_over_climate and self._underlying_climate_delta_t is not None:
added_energy = self._device_power * self._underlying_climate_delta_t
self._total_energy += added_energy
_LOGGER.debug(
"%s - added energy is %.3f . Total energy is now: %.3f",
self,
added_energy,
self._total_energy,
)
@callback
async def _async_climate_changed(self, event):
"""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
less than 10 sec after the last command. What we want here is to take the values
from underlyings ONLY if someone have change directly on the underlying and not
as a return of the command. The only thing we take all the time is the HVACAction
which is important for feedaback and which cannot generates loops.
"""
async def end_climate_changed(changes):
"""To end the event management"""
if changes:
self.async_write_ha_state()
self.update_custom_attributes()
await self.async_control_heating()
new_state = event.data.get("new_state")
_LOGGER.debug("%s - _async_climate_changed new_state is %s", self, new_state)
if not new_state:
return
changes = False
new_hvac_mode = new_state.state
old_state = event.data.get("old_state")
old_hvac_action = (
old_state.attributes.get("hvac_action")
if old_state and old_state.attributes
else None
)
new_hvac_action = (
new_state.attributes.get("hvac_action")
if new_state and new_state.attributes
else None
)
old_state_date_changed = (
old_state.last_changed if old_state and old_state.last_changed else None
)
old_state_date_updated = (
old_state.last_updated if old_state and old_state.last_updated else None
)
new_state_date_changed = (
new_state.last_changed if new_state and new_state.last_changed else None
)
new_state_date_updated = (
new_state.last_updated if new_state and new_state.last_updated else None
)
# 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
_LOGGER.info(
"%s - Underlying climate changed. Event.new_hvac_mode is %s, current_hvac_mode=%s, new_hvac_action=%s, old_hvac_action=%s",
self,
new_hvac_mode,
self._hvac_mode,
new_hvac_action,
old_hvac_action,
)
_LOGGER.debug(
"%s - last_change_time=%s old_state_date_changed=%s old_state_date_updated=%s new_state_date_changed=%s new_state_date_updated=%s",
self,
self._last_change_time,
old_state_date_changed,
old_state_date_updated,
new_state_date_changed,
new_state_date_updated,
)
# 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)
)
_LOGGER.info(
"%s - underlying just switch ON. Set power and energy start date %s",
self,
self._underlying_climate_start_hvac_action_date.isoformat(),
)
changes = True
if old_hvac_action in HVAC_ACTION_ON and new_hvac_action not in HVAC_ACTION_ON:
stop_power_date = self.get_last_updated_date_or_now(new_state)
if self._underlying_climate_start_hvac_action_date:
delta = (
stop_power_date - self._underlying_climate_start_hvac_action_date
)
self._underlying_climate_delta_t = delta.total_seconds() / 3600.0
# increment energy at the end of the cycle
self.incremente_energy()
self._underlying_climate_start_hvac_action_date = None
_LOGGER.info(
"%s - underlying just switch OFF at %s. delta_h=%.3f h",
self,
stop_power_date.isoformat(),
self._underlying_climate_delta_t,
)
changes = True
# Issue #120 - Some TRV are chaning target temperature a very long time (6 sec) after the change.
# In that case a loop is possible if a user change multiple times during this 6 sec.
if new_state_date_updated and self._last_change_time:
delta = (new_state_date_updated - self._last_change_time).total_seconds()
if delta < 10:
_LOGGER.info(
"%s - underlying event is received less than 10 sec after command. Forget it to avoid loop",
self,
)
await end_climate_changed(changes)
return
if (
new_hvac_mode
in [
HVACMode.OFF,
HVACMode.HEAT,
HVACMode.COOL,
HVACMode.HEAT_COOL,
HVACMode.DRY,
HVACMode.AUTO,
HVACMode.FAN_ONLY,
None,
]
and self._hvac_mode != new_hvac_mode
):
changes = True
self._hvac_mode = new_hvac_mode
# Update all underlyings state
if self.is_over_climate:
for under in self._underlyings:
await under.set_hvac_mode(new_hvac_mode)
if not changes:
# try to manage new target temperature set if state
_LOGGER.debug(
"Do temperature check. temperature is %s, new_state.attributes is %s",
self.target_temperature,
new_state.attributes,
)
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
):
_LOGGER.info(
"%s - Target temp in underlying have change to %s",
self,
new_target_temp,
)
await self.async_set_temperature(temperature=new_target_temp)
changes = True
await end_climate_changed(changes)
@overrides
async def async_control_heating(self, force=False, _=None):
"""The main function used to run the calculation at each cycle"""
ret = await super().async_control_heating(force, _)
await self._send_regulated_temperature()
return ret
@property
def auto_regulation_mode(self):
"""Get the regulation mode"""
return self._auto_regulation_mode
@property
def regulated_target_temp(self):
"""Get the regulated target temperature"""
return self._regulated_target_temp
@property
def is_regulated(self):
"""Check if the ThermostatOverClimate is regulated"""
return self.auto_regulation_mode != CONF_AUTO_REGULATION_NONE
@property
def hvac_modes(self):
"""List of available operation modes."""
if self.underlying_entity(0):
return self.underlying_entity(0).hvac_modes
else:
return super.hvac_modes
@property
def mean_cycle_power(self) -> float | None:
"""Returns the mean power consumption during the cycle"""
return None
@property
def fan_mode(self) -> str | None:
"""Return the fan setting.
Requires ClimateEntityFeature.FAN_MODE.
"""
if self.underlying_entity(0):
return self.underlying_entity(0).fan_mode
return None
@property
def fan_modes(self) -> list[str] | None:
"""Return the list of available fan modes.
Requires ClimateEntityFeature.FAN_MODE.
"""
if self.underlying_entity(0):
return self.underlying_entity(0).fan_modes
return []
@property
def swing_mode(self) -> str | None:
"""Return the swing setting.
Requires ClimateEntityFeature.SWING_MODE.
"""
if self.underlying_entity(0):
return self.underlying_entity(0).swing_mode
return None
@property
def swing_modes(self) -> list[str] | None:
"""Return the list of available swing modes.
Requires ClimateEntityFeature.SWING_MODE.
"""
if self.underlying_entity(0):
return self.underlying_entity(0).swing_modes
return None
@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
@property
def supported_features(self):
"""Return the list of supported features."""
if self.underlying_entity(0):
return self.underlying_entity(0).supported_features | self._support_flags
return self._support_flags
@property
def target_temperature_step(self) -> float | None:
"""Return the supported step of target temperature."""
if self.underlying_entity(0):
return self.underlying_entity(0).target_temperature_step
return None
@property
def target_temperature_high(self) -> float | None:
"""Return the highbound target temperature we try to reach.
Requires ClimateEntityFeature.TARGET_TEMPERATURE_RANGE.
"""
if self.underlying_entity(0):
return self.underlying_entity(0).target_temperature_high
return None
@property
def target_temperature_low(self) -> float | None:
"""Return the lowbound target temperature we try to reach.
Requires ClimateEntityFeature.TARGET_TEMPERATURE_RANGE.
"""
if self.underlying_entity(0):
return self.underlying_entity(0).target_temperature_low
return None
@property
def is_aux_heat(self) -> bool | None:
"""Return true if aux heater.
Requires ClimateEntityFeature.AUX_HEAT.
"""
if self.underlying_entity(0):
return self.underlying_entity(0).is_aux_heat
return None
@overrides
def turn_aux_heat_on(self) -> None:
"""Turn auxiliary heater on."""
if self.underlying_entity(0):
return self.underlying_entity(0).turn_aux_heat_on()
raise NotImplementedError()
@overrides
async def async_turn_aux_heat_on(self) -> None:
"""Turn auxiliary heater on."""
for under in self._underlyings:
await under.async_turn_aux_heat_on()
@overrides
def turn_aux_heat_off(self) -> None:
"""Turn auxiliary heater off."""
for under in self._underlyings:
return under.turn_aux_heat_off()
@overrides
async def async_turn_aux_heat_off(self) -> None:
"""Turn auxiliary heater off."""
for under in self._underlyings:
await under.async_turn_aux_heat_off()
@overrides
async def async_set_fan_mode(self, fan_mode):
"""Set new target fan mode."""
_LOGGER.info("%s - Set fan mode: %s", self, fan_mode)
if fan_mode is None:
return
for under in self._underlyings:
await under.set_fan_mode(fan_mode)
self._fan_mode = fan_mode
self.async_write_ha_state()
@overrides
async def async_set_humidity(self, humidity: int):
"""Set new target humidity."""
_LOGGER.info("%s - Set fan mode: %s", self, humidity)
if humidity is None:
return
for under in self._underlyings:
await under.set_humidity(humidity)
self._humidity = humidity
self.async_write_ha_state()
@overrides
async def async_set_swing_mode(self, swing_mode):
"""Set new target swing operation."""
_LOGGER.info("%s - Set fan mode: %s", self, swing_mode)
if swing_mode is None:
return
for under in self._underlyings:
await under.set_swing_mode(swing_mode)
self._swing_mode = swing_mode
self.async_write_ha_state()
async def service_set_auto_regulation_mode(self, auto_regulation_mode):
"""Called by a service call:
service: versatile_thermostat.set_auto_regulation_mode
data:
auto_regulation_mode: [None | Light | Medium | Strong]
target:
entity_id: climate.thermostat_1
"""
_LOGGER.info(
"%s - Calling service_set_auto_regulation_mode, auto_regulation_mode: %s",
self,
auto_regulation_mode,
)
if auto_regulation_mode == "None":
self.choose_auto_regulation_mode(CONF_AUTO_REGULATION_NONE)
elif auto_regulation_mode == "Light":
self.choose_auto_regulation_mode(CONF_AUTO_REGULATION_LIGHT)
elif auto_regulation_mode == "Medium":
self.choose_auto_regulation_mode(CONF_AUTO_REGULATION_MEDIUM)
elif auto_regulation_mode == "Strong":
self.choose_auto_regulation_mode(CONF_AUTO_REGULATION_STRONG)
elif auto_regulation_mode == "Slow":
self.choose_auto_regulation_mode(CONF_AUTO_REGULATION_SLOW)
elif auto_regulation_mode == "Expert":
self.choose_auto_regulation_mode(CONF_AUTO_REGULATION_EXPERT)
await self._send_regulated_temperature()
self.update_custom_attributes()

View File

@@ -0,0 +1,212 @@
# pylint: disable=line-too-long
""" A climate over switch classe """
import logging
from homeassistant.core import callback
from homeassistant.helpers.event import async_track_state_change_event
from homeassistant.components.climate import HVACMode
from .const import (
CONF_HEATER,
CONF_HEATER_2,
CONF_HEATER_3,
CONF_HEATER_4,
CONF_INVERSE_SWITCH,
overrides,
)
from .base_thermostat import BaseThermostat
from .underlyings import UnderlyingSwitch
from .prop_algorithm import PropAlgorithm
_LOGGER = logging.getLogger(__name__)
class ThermostatOverSwitch(BaseThermostat):
"""Representation of a base class for a Versatile Thermostat over a switch."""
_entity_component_unrecorded_attributes = (
BaseThermostat._entity_component_unrecorded_attributes.union(
frozenset(
{
"is_over_switch",
"is_inversed",
"underlying_switch_0",
"underlying_switch_1",
"underlying_switch_2",
"underlying_switch_3",
"on_time_sec",
"off_time_sec",
"cycle_min",
"function",
"tpi_coef_int",
"tpi_coef_ext",
"power_percent",
}
)
)
)
# useless for now
# def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None:
# """Initialize the thermostat over switch."""
# super().__init__(hass, unique_id, name, entry_infos)
_is_inversed: bool = None
@property
def is_over_switch(self) -> bool:
"""True if the Thermostat is over_switch"""
return True
@property
def is_inversed(self) -> bool:
"""True if the switch is inversed (for pilot wire and diode)"""
return self._is_inversed is True
@property
def power_percent(self) -> float | None:
"""Get the current on_percent value"""
if self._prop_algorithm:
return round(self._prop_algorithm.on_percent * 100, 0)
else:
return None
@overrides
def post_init(self, entry_infos):
"""Initialize the Thermostat"""
super().post_init(entry_infos)
self._prop_algorithm = PropAlgorithm(
self._proportional_function,
self._tpi_coef_int,
self._tpi_coef_ext,
self._cycle_min,
self._minimal_activation_delay,
)
lst_switches = [entry_infos.get(CONF_HEATER)]
if entry_infos.get(CONF_HEATER_2):
lst_switches.append(entry_infos.get(CONF_HEATER_2))
if entry_infos.get(CONF_HEATER_3):
lst_switches.append(entry_infos.get(CONF_HEATER_3))
if entry_infos.get(CONF_HEATER_4):
lst_switches.append(entry_infos.get(CONF_HEATER_4))
delta_cycle = self._cycle_min * 60 / len(lst_switches)
for idx, switch in enumerate(lst_switches):
self._underlyings.append(
UnderlyingSwitch(
hass=self._hass,
thermostat=self,
switch_entity_id=switch,
initial_delay_sec=idx * delta_cycle,
)
)
self._is_inversed = entry_infos.get(CONF_INVERSE_SWITCH) is True
self._should_relaunch_control_heating = False
@overrides
async def async_added_to_hass(self):
"""Run when entity about to be added."""
_LOGGER.debug("Calling async_added_to_hass")
await super().async_added_to_hass()
# Add listener to all underlying entities
for switch in self._underlyings:
self.async_on_remove(
async_track_state_change_event(
self.hass, [switch.entity_id], self._async_switch_changed
)
)
self.hass.create_task(self.async_control_heating())
@overrides
def update_custom_attributes(self):
"""Custom attributes"""
super().update_custom_attributes()
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[
"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.async_write_ha_state()
_LOGGER.debug(
"%s - Calling update_custom_attributes: %s",
self,
self._attr_extra_state_attributes,
)
@overrides
def recalculate(self):
"""A utility function to force the calculation of a the algo and
update the custom attributes and write the state
"""
_LOGGER.debug("%s - recalculate all", self)
self._prop_algorithm.calculate(
self._target_temp,
self._cur_temp,
self._cur_ext_temp,
self._hvac_mode == HVACMode.COOL,
)
self.update_custom_attributes()
self.async_write_ha_state()
@overrides
def incremente_energy(self):
"""increment the energy counter if device is active"""
if self.hvac_mode == HVACMode.OFF:
return
added_energy = 0
if not self.is_over_climate and self.mean_cycle_power is not None:
added_energy = self.mean_cycle_power * float(self._cycle_min) / 60.0
self._total_energy += added_energy
_LOGGER.debug(
"%s - added energy is %.3f . Total energy is now: %.3f",
self,
added_energy,
self._total_energy,
)
@callback
def _async_switch_changed(self, event):
"""Handle heater switch state changes."""
new_state = event.data.get("new_state")
old_state = event.data.get("old_state")
if new_state is None:
return
if old_state is None:
self.hass.create_task(self._check_initial_state())
self.async_write_ha_state()
self.update_custom_attributes()

View File

@@ -0,0 +1,188 @@
# pylint: disable=line-too-long
""" A climate over switch classe """
import logging
from datetime import timedelta
from homeassistant.helpers.event import async_track_state_change_event, async_track_time_interval
from homeassistant.core import callback
from homeassistant.components.climate import HVACMode
from .base_thermostat import BaseThermostat
from .prop_algorithm import PropAlgorithm
from .const import CONF_VALVE, CONF_VALVE_2, CONF_VALVE_3, CONF_VALVE_4, overrides
from .underlyings import UnderlyingValve
_LOGGER = logging.getLogger(__name__)
class ThermostatOverValve(BaseThermostat):
"""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"
}))
# Useless for now
# def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None:
# """Initialize the thermostat over switch."""
# super().__init__(hass, unique_id, name, entry_infos)
@property
def is_over_valve(self) -> bool:
""" True if the Thermostat is over_valve"""
return True
@property
def valve_open_percent(self) -> int:
""" Gives the percentage of valve needed"""
if self._hvac_mode == HVACMode.OFF:
return 0
else:
return round(max(0, min(self.proportional_algorithm.on_percent, 1)) * 100)
@overrides
def post_init(self, entry_infos):
""" Initialize the Thermostat"""
super().post_init(entry_infos)
self._prop_algorithm = PropAlgorithm(
self._proportional_function,
self._tpi_coef_int,
self._tpi_coef_ext,
self._cycle_min,
self._minimal_activation_delay,
)
lst_valves = [entry_infos.get(CONF_VALVE)]
if entry_infos.get(CONF_VALVE_2):
lst_valves.append(entry_infos.get(CONF_VALVE_2))
if entry_infos.get(CONF_VALVE_3):
lst_valves.append(entry_infos.get(CONF_VALVE_3))
if entry_infos.get(CONF_VALVE_4):
lst_valves.append(entry_infos.get(CONF_VALVE_4))
for _, valve in enumerate(lst_valves):
self._underlyings.append(
UnderlyingValve(
hass=self._hass,
thermostat=self,
valve_entity_id=valve
)
)
self._should_relaunch_control_heating = False
@overrides
async def async_added_to_hass(self):
"""Run when entity about to be added."""
_LOGGER.debug("Calling async_added_to_hass")
await super().async_added_to_hass()
# Add listener to all underlying entities
for valve in self._underlyings:
self.async_on_remove(
async_track_state_change_event(
self.hass, [valve.entity_id], self._async_valve_changed
)
)
# Start the control_heating
# starts a cycle
self.async_on_remove(
async_track_time_interval(
self.hass,
self.async_control_heating,
interval=timedelta(minutes=self._cycle_min),
)
)
@callback
async def _async_valve_changed(self, event):
"""Handle unerdlying valve state changes.
This method just log the change. It changes nothing to avoid loops.
"""
new_state = event.data.get("new_state")
_LOGGER.debug("%s - _async_valve_changed new_state is %s", self, new_state.state)
@overrides
def update_custom_attributes(self):
""" Custom attributes """
super().update_custom_attributes()
self._attr_extra_state_attributes["valve_open_percent"] = self.valve_open_percent
self._attr_extra_state_attributes["is_over_valve"] = self.is_over_valve
self._attr_extra_state_attributes["underlying_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[
"on_percent"
] = self._prop_algorithm.on_percent
self._attr_extra_state_attributes[
"on_time_sec"
] = self._prop_algorithm.on_time_sec
self._attr_extra_state_attributes[
"off_time_sec"
] = self._prop_algorithm.off_time_sec
self._attr_extra_state_attributes["cycle_min"] = self._cycle_min
self._attr_extra_state_attributes["function"] = self._proportional_function
self._attr_extra_state_attributes["tpi_coef_int"] = self._tpi_coef_int
self._attr_extra_state_attributes["tpi_coef_ext"] = self._tpi_coef_ext
self.async_write_ha_state()
_LOGGER.debug(
"%s - Calling update_custom_attributes: %s",
self,
self._attr_extra_state_attributes,
)
@overrides
def recalculate(self):
"""A utility function to force the calculation of a the algo and
update the custom attributes and write the state
"""
_LOGGER.debug("%s - recalculate all", self)
self._prop_algorithm.calculate(
self._target_temp,
self._cur_temp,
self._cur_ext_temp,
self._hvac_mode == HVACMode.COOL,
)
for under in self._underlyings:
under.set_valve_open_percent(
self._prop_algorithm.on_percent
)
self.update_custom_attributes()
self.async_write_ha_state()
@overrides
def incremente_energy(self):
"""increment the energy counter if device is active"""
if self.hvac_mode == HVACMode.OFF:
return
added_energy = 0
if not self.is_over_climate and self.mean_cycle_power is not None:
added_energy = self.mean_cycle_power * float(self._cycle_min) / 60.0
self._total_energy += added_energy
_LOGGER.debug(
"%s - added energy is %.3f . Total energy is now: %.3f",
self,
added_energy,
self._total_energy,
)

View File

@@ -0,0 +1,375 @@
{
"title": "Διαμόρφωση Ευέλικτου Θερμοστάτη",
"config": {
"flow_title": "Διαμόρφωση Ευέλικτου Θερμοστάτη",
"step": {
"user": {
"title": "Προσθήκη νέου Ευέλικτου Θερμοστάτη",
"description": "Κύρια υποχρεωτικά χαρακτηριστικά",
"data": {
"name": "Όνομα",
"thermostat_type": "Τύπος Θερμοστάτη",
"temperature_sensor_entity_id": "Ταυτότητα οντότητας αισθητήρα θερμοκρασίας",
"external_temperature_sensor_entity_id": "Ταυτότητα οντότητας εξωτερικού αισθητήρα θερμοκρασίας",
"cycle_min": "Διάρκεια κύκλου (λεπτά)",
"temp_min": "Ελάχιστη επιτρεπτή θερμοκρασία",
"temp_max": "Μέγιστη επιτρεπτή θερμοκρασία",
"device_power": "Ισχύς συσκευής",
"use_window_feature": "Χρήση ανίχνευσης παραθύρου",
"use_motion_feature": "Χρήση ανίχνευσης κίνησης",
"use_power_feature": "Χρήση διαχείρισης ισχύος",
"use_presence_feature": "Χρήση ανίχνευσης παρουσίας"
}
},
"type": {
"title": "Συνδεδεμένες οντότητες",
"description": "Χαρακτηριστικά συνδεδεμένων οντοτήτων",
"data": {
"heater_entity_id": "1ος διακόπτης θερμαντήρα",
"heater_entity2_id": "2ος διακόπτης θερμαντήρα",
"heater_entity3_id": "3ος διακόπτης θερμαντήρα",
"heater_entity4_id": "4ος διακόπτης θερμαντήρα",
"proportional_function": "Αλγόριθμος",
"climate_entity_id": "1η υποκείμενη κλιματική οντότητα",
"climate_entity2_id": "2η υποκείμενη κλιματική οντότητα",
"climate_entity3_id": "3η υποκείμενη κλιματική οντότητα",
"climate_entity4_id": "4η υποκείμενη κλιματική οντότητα",
"ac_mode": "Λειτουργία AC",
"valve_entity_id": "1ος αριθμός βαλβίδας",
"valve_entity2_id": "2ος αριθμός βαλβίδας",
"valve_entity3_id": "3ος αριθμός βαλβίδας",
"valve_entity4_id": "4ος αριθμός βαλβίδας",
"auto_regulation_mode": "Αυτόματη ρύθμιση",
"auto_regulation_dtemp": "Όριο ρύθμισης",
"auto_regulation_periode_min": "Ελάχιστη περίοδος ρύθμισης",
"inverse_switch_command": "Αντίστροφη εντολή διακόπτη"
},
"data_description": {
"heater_entity_id": "Υποχρεωτική ταυτότητα οντότητας θερμαντήρα",
"heater_entity2_id": "Προαιρετική 2η ταυτότητα οντότητας θερμαντήρα. Αφήστε κενό αν δεν χρησιμοποιείται",
"heater_entity3_id": "Προαιρετική 3η ταυτότητα οντότητας θερμαντήρα. Αφήστε κενό αν δεν χρησιμοποιείται",
"heater_entity4_id": "Προαιρετική 4η ταυτότητα οντότητας θερμαντήρα. Αφήστε κενό αν δεν χρησιμοποιείται",
"proportional_function": "Αλγόριθμος προς χρήση (TPI είναι ο μόνος για τώρα)",
"climate_entity_id": "Ταυτότητα υποκείμενης κλιματικής οντότητας",
"climate_entity2_id": "2η ταυτότητα υποκείμενης κλιματικής οντότητας",
"climate_entity3_id": "3η ταυτότητα υποκείμενης κλιματικής οντότητας",
"climate_entity4_id": "4η ταυτότητα υποκείμενης κλιματικής οντότητας",
"ac_mode": "Χρήση της λειτουργίας Κλιματισμού (AC)",
"valve_entity_id": "1η ταυτότητα αριθμού βαλβίδας",
"valve_entity2_id": "2η ταυτότητα αριθμού βαλβίδας",
"valve_entity3_id": "3η ταυτότητα αριθμού βαλβίδας",
"valve_entity4_id": "4η ταυτότητα αριθμού βαλβίδας",
"auto_regulation_mode": "Αυτόματη προσαρμογή της στοχευμένης θερμοκρασίας",
"auto_regulation_dtemp": "Το όριο σε ° κάτω από το οποίο η αλλαγή θερμοκρασίας δεν θα αποστέλλεται",
"auto_regulation_periode_min": "Διάρκεια σε λεπτά μεταξύ δύο ενημερώσεων ρύθμισης",
"inverse_switch_command": "Για διακόπτη με πιλοτικό καλώδιο και δίοδο μπορεί να χρειαστεί να αντιστρέψετε την εντολή"
}
},
"tpi": {
"title": "TPI",
"description": "Χαρακτηριστικά Χρονικά Αναλογικού Ολοκληρωτικού (TPI)",
"data": {
"tpi_coef_int": "Συντελεστής για χρήση στη διαφορά εσωτερικής θερμοκρασίας",
"tpi_coef_ext": "Συντελεστής για χρήση στη διαφορά εξωτερικής θερμοκρασίας"
}
},
"presets": {
"title": "Προκαθορισμένα",
"description": "Για κάθε προκαθορισμένο, δώστε την επιθυμητή θερμοκρασία (0 για να αγνοηθεί το προκαθορισμένο)",
"data": {
"eco_temp": "Θερμοκρασία στο προκαθορισμένο Eco",
"comfort_temp": "Θερμοκρασία στο προκαθορισμένο Comfort",
"boost_temp": "Θερμοκρασία στο προκαθορισμένο Boost",
"eco_ac_temp": "Θερμοκρασία στο προκαθορισμένο Eco για λειτουργία AC",
"comfort_ac_temp": "Θερμοκρασία στο προκαθορισμένο Comfort για λειτουργία AC",
"boost_ac_temp": "Θερμοκρασία στο προκαθορισμένο Boost για λειτουργία AC"
}
},
"window": {
"title": "Διαχείριση Παραθύρων",
"description": "Ανοίξτε τη διαχείριση παραθύρων.\nΑφήστε το αντίστοιχο entity_id κενό αν δεν χρησιμοποιείται\nΜπορείτε επίσης να ρυθμίσετε αυτόματη ανίχνευση ανοίγματος παραθύρου με βάση τη μείωση της θερμοκρασίας",
"data": {
"window_sensor_entity_id": "Ταυτότητα οντότητας αισθητήρα παραθύρου",
"window_delay": "Καθυστέρηση αισθητήρα παραθύρου (δευτερόλεπτα)",
"window_auto_open_threshold": "Κατώφλι μείωσης θερμοκρασίας για αυτόματη ανίχνευση ανοίγματος παραθύρου (σε °/λεπτό)",
"window_auto_close_threshold": "Κατώφλι αύξησης θερμοκρασίας για τέλος αυτόματης ανίχνευσης (σε °/λεπτό)",
"window_auto_max_duration": "Μέγιστη διάρκεια αυτόματης ανίχνευσης ανοίγματος παραθύρου (σε λεπτά)"
},
"data_description": {
"window_sensor_entity_id": "Αφήστε κενό αν δεν πρέπει να χρησιμοποιηθεί αισθητήρας παραθύρου",
"window_delay": "Η καθυστέρηση σε δευτερόλεπτα πριν ληφθεί υπόψη η ανίχνευση του αισθητήρα",
"window_auto_open_threshold": "Συνιστώμενη τιμή: μεταξύ 0.05 και 0.1. Αφήστε κενό αν δεν χρησιμοποιείται αυτόματη ανίχνευση ανοίγματος παραθύρου",
"window_auto_close_threshold": "Συνιστώμενη τιμή: 0. Αφήστε κενό αν δεν χρησιμοποιείται αυτόματη ανίχνευση ανοίγματος παραθύρου",
"window_auto_max_duration": "Συνιστώμενη τιμή: 60 (μία ώρα). Αφήστε κενό αν δεν χρησιμοποιείται αυτόματη ανίχνευση ανοίγματος παραθύρου"
}
},
"motion": {
"title": "Διαχείριση Κίνησης",
"description": "Διαχείριση αισθητήρα κίνησης. Το προκαθορισμένο μπορεί να αλλάζει αυτόματα ανάλογα με ανίχνευση κίνησης\nΑφήστε το αντίστοιχο entity_id κενό αν δεν χρησιμοποιείται.\nΟι επιλογές motion_preset και no_motion_preset πρέπει να οριστούν στο αντίστοιχο όνομα προκαθορισμένου",
"data": {
"motion_sensor_entity_id": "Ταυτότητα οντότητας αισθητήρα κίνησης",
"motion_delay": "Καθυστέρηση ενεργοποίησης",
"motion_off_delay": "Καθυστέρηση απενεργοποίησης",
"motion_preset": "Προκαθορισμένο κίνησης",
"no_motion_preset": "Προκαθορισμένο χωρίς κίνηση"
},
"data_description": {
"motion_sensor_entity_id": "Η ταυτότητα οντότητας του αισθητήρα κίνησης",
"motion_delay": "Καθυστέρηση ενεργοποίησης κίνησης (δευτερόλεπτα)",
"motion_off_delay": "Καθυστέρηση απενεργοποίησης κίνησης (δευτερόλεπτα)",
"motion_preset": "Το προκαθορισμένο που θα χρησιμοποιηθεί όταν ανιχνευθεί κίνηση",
"no_motion_preset": "Το προκαθορισμένο που θα χρησιμοποιηθεί όταν δεν ανιχνευθεί κίνηση"
}
},
"power": {
"title": "Διαχείριση Ενέργειας",
"description": "Χαρακτηριστικά διαχείρισης ενέργειας.\nΔίνει τον αισθητήρα ενέργειας και τον μέγιστο αισθητήρα ενέργειας του σπιτιού σας.\nΣτη συνέχεια καθορίστε την κατανάλωση ενέργειας του θερμαντήρα όταν είναι ενεργοποιημένος.\nΌλοι οι αισθητήρες και η ισχύς της συσκευής πρέπει να έχουν την ίδια μονάδα (kW ή W).\nΑφήστε το αντίστοιχο entity_id κενό αν δεν χρησιμοποιείται.",
"data": {
"power_sensor_entity_id": "Ταυτότητα οντότητας αισθητήρα ενέργειας",
"max_power_sensor_entity_id": "Ταυτότητα οντότητας αισθητήρα μέγιστης ενέργειας",
"power_temp": "Θερμοκρασία για Αποβολή Ενέργειας"
}
},
"presence": {
"title": "Διαχείριση Παρουσίας",
"description": "Χαρακτηριστικά διαχείρισης παρουσίας.\nΔίνει έναν αισθητήρα παρουσίας του σπιτιού σας (αληθές αν κάποιος είναι παρών).\nΣτη συνέχεια καθορίστε είτε το προκαθορισμένο που θα χρησιμοποιηθεί όταν ο αισθητήρας παρουσίας είναι ψευδής ή την απόκλιση στη θερμοκρασία που θα εφαρμοστεί.\nΑν δοθεί προκαθορισμένο, η απόκλιση δεν θα χρησιμοποιηθεί.\nΑφήστε το αντίστοιχο entity_id κενό αν δεν χρησιμοποιείται.",
"data": {
"presence_sensor_entity_id": "Ταυτότητα οντότητας αισθητήρα παρουσίας",
"eco_away_temp": "Θερμοκρασία στο προκαθορισμένο Eco όταν δεν υπάρχει παρουσία",
"comfort_away_temp": "Θερμοκρασία στο προκαθορισμένο Comfort όταν δεν υπάρχει παρουσία",
"boost_away_temp": "Θερμοκρασία στο προκαθορισμένο Boost όταν δεν υπάρχει παρουσία",
"eco_ac_away_temp": "Θερμοκρασία στο προκαθορισμένο Eco όταν δεν υπάρχει παρουσία σε λειτουργία AC",
"comfort_ac_away_temp": "Θερμοκρασία στο προκαθορισμένο Comfort όταν δεν υπάρχει παρουσία σε λειτουργία AC",
"boost_ac_away_temp": "Θερμοκρασία στο προκαθορισμένο Boost όταν δεν υπάρχει παρουσία σε λειτουργία AC"
}
},
"advanced": {
"title": "Προχωρημένες Παράμετροι",
"description": "Διαμόρφωση των προχωρημένων παραμέτρων. Αφήστε τις προεπιλεγμένες τιμές αν δεν γνωρίζετε τι κάνετε.\nΑυτές οι παράμετροι μπορούν να οδηγήσουν σε πολύ κακή ρύθμιση θερμοκρασίας ή ενέργειας.",
"data": {
"minimal_activation_delay": "Ελάχιστη καθυστέρηση ενεργοποίησης",
"security_delay_min": "Καθυστέρηση ασφαλείας (σε λεπτά)",
"security_min_on_percent": "Ελάχιστο ποσοστό ισχύος για ενεργοποίηση λειτουργίας ασφαλείας",
"security_default_on_percent": "Ποσοστό ισχύος για χρήση σε λειτουργία ασφαλείας"
},
"data_description": {
"minimal_activation_delay": "Καθυστέρηση σε δευτερόλεπτα κάτω από την οποία η συσκευή δεν θα ενεργοποιηθεί",
"security_delay_min": "Μέγιστη επιτρεπτή καθυστέρηση σε λεπτά μεταξύ δύο μετρήσεων θερμοκρασίας. Πέρα από αυτή την καθυστέρηση, ο θερμοστάτης θα μεταβεί σε κατάσταση ασφαλείας",
"security_min_on_percent": "Ελάχιστη τιμή ποσοστού θέρμανσης για την ενεργοποίηση του προεπιλεγμένου ασφάλειας. Κάτω από αυτό το ποσοστό ισχύος το θερμοστάτη δεν θα πάει στο προεπιλεγμένο ασφάλειας.",
"security_default_on_percent": "Η προεπιλεγμένη τιμή ποσοστού ισχύος θέρμανσης στο προεπιλεγμένο ασφάλειας. Ορίστε σε 0 για να απενεργοποιήσετε τη θερμάστρα στο παρόν ασφάλειας."
}
}
},
"error": {
"unknown": "Απρόσμενο σφάλμα",
"unknown_entity": "Άγνωστο αναγνωριστικό οντότητας",
"window_open_detection_method": "Πρέπει να χρησιμοποιείται μόνο μία μέθοδος ανίχνευσης ανοιχτού παραθύρου. Χρησιμοποιήστε αισθητήρα ή αυτόματη ανίχνευση μέσω του κατωφλίου θερμοκρασίας, αλλά όχι και τα δύο"
},
"abort": {
"already_configured": "Η συσκευή έχει ήδη ρυθμιστεί"
}
},
"options": {
"flow_title": "Διαμόρφωση Ευέλικτου Θερμοστάτη",
"step": {
"user": {
"title": "Προσθήκη νέου Ευέλικτου Θερμοστάτη",
"description": "Κύρια υποχρεωτικά χαρακτηριστικά",
"data": {
"name": "Όνομα",
"thermostat_type": "Τύπος θερμοστάτη",
"temperature_sensor_entity_id": "Ταυτότητα οντότητας αισθητήρα θερμοκρασίας",
"external_temperature_sensor_entity_id": "Ταυτότητα οντότητας εξωτερικού αισθητήρα θερμοκρασίας",
"cycle_min": "Διάρκεια κύκλου (λεπτά)",
"temp_min": "Ελάχιστη επιτρεπόμενη θερμοκρασία",
"temp_max": "Μέγιστη επιτρεπόμενη θερμοκρασία",
"device_power": "Ισχύς συσκευής (kW)",
"use_window_feature": "Χρήση ανίχνευσης παραθύρου",
"use_motion_feature": "Χρήση ανίχνευσης κίνησης",
"use_power_feature": "Χρήση διαχείρισης ενέργειας",
"use_presence_feature": "Χρήση ανίχνευσης παρουσίας"
}
},
"type": {
"title": "Συνδεδεμένες οντότητες",
"description": "Χαρακτηριστικά συνδεδεμένων οντοτήτων",
"data": {
"heater_entity_id": "1ος διακόπτης θερμαντήρα",
"heater_entity2_id": "2ος διακόπτης θερμαντήρα",
"heater_entity3_id": "3ος διακόπτης θερμαντήρα",
"heater_entity4_id": "4ος διακόπτης θερμαντήρα",
"proportional_function": "Αλγόριθμος",
"climate_entity_id": "1η υποκείμενη κλιματική οντότητα",
"climate_entity2_id": "2η υποκείμενη κλιματική οντότητα",
"climate_entity3_id": "3η υποκείμενη κλιματική οντότητα",
"climate_entity4_id": "4η υποκείμενη κλιματική οντότητα",
"ac_mode": "Λειτουργία AC",
"valve_entity_id": "1ος αριθμός βαλβίδας",
"valve_entity2_id": "2ος αριθμός βαλβίδας",
"valve_entity3_id": "3ος αριθμός βαλβίδας",
"valve_entity4_id": "4ος αριθμός βαλβίδας",
"auto_regulation_mode": "Αυτορύθμιση",
"auto_regulation_dtemp": "Όριο ρύθμισης",
"auto_regulation_periode_min": "Ελάχιστη περίοδος ρύθμισης",
"inverse_switch_command": "Αντίστροφη εντολή διακόπτη"
},
"data_description": {
"heater_entity_id": "Υποχρεωτική ταυτότητα οντότητας θερμαντήρα",
"heater_entity2_id": "Προαιρετική ταυτότητα οντότητας 2ου θερμαντήρα. Αφήστε το κενό αν δεν χρησιμοποιείται",
"heater_entity3_id": "Προαιρετική ταυτότητα οντότητας 3ου θερμαντήρα. Αφήστε το κενό αν δεν χρησιμοποιείται",
"heater_entity4_id": "Προαιρετική ταυτότητα οντότητας 4ου θερμαντήρα. Αφήστε το κενό αν δεν χρησιμοποιείται",
"proportional_function": "Αλγόριθμος που θα χρησιμοποιηθεί (TPI είναι ο μόνος για τώρα)",
"climate_entity_id": "Ταυτότητα οντότητας υποκείμενου κλίματος",
"climate_entity2_id": "Ταυτότητα οντότητας 2ου υποκείμενου κλίματος",
"climate_entity3_id": "Ταυτότητα οντότητας 3ου υποκείμενου κλίματος",
"climate_entity4_id": "Ταυτότητα οντότητας 4ου υποκείμενου κλίματος",
"ac_mode": "Χρήση της λειτουργίας Κλιματισμού (AC)",
"valve_entity_id": "Ταυτότητα οντότητας 1ης βαλβίδας",
"valve_entity2_id": "Ταυτότητα οντότητας 2ης βαλβίδας",
"valve_entity3_id": "Ταυτότητα οντότητας 3ης βαλβίδας",
"valve_entity4_id": "Ταυτότητα οντότητας 4ης βαλβίδας",
"auto_regulation_mode": "Αυτόματη ρύθμιση της στοχευόμενης θερμοκρασίας",
"auto_regulation_dtemp": "Το κατώφλι σε °C κάτω από το οποίο η αλλαγή της θερμοκρασίας δεν θα αποστέλλεται",
"auto_regulation_periode_min": "Διάρκεια σε λεπτά μεταξύ δύο ενημερώσεων ρύθμισης",
"inverse_switch_command": "Για διακόπτες με πιλοτικό καλώδιο και δίοδο μπορεί να χρειαστεί να αντιστραφεί η εντολή"
}
},
"tpi": {
"title": "TPI",
"description": "Χαρακτηριστικά Χρονικού Αναλογικού Ολοκληρωτικού (TPI)",
"data": {
"tpi_coef_int": "Συντελεστής που θα χρησιμοποιηθεί για την εσωτερική διαφορά θερμοκρασίας",
"tpi_coef_ext": "Συντελεστής που θα χρησιμοποιηθεί για την εξωτερική διαφορά θερμοκρασίας"
}
},
"presets": {
"title": "Προεπιλογές",
"description": "Για κάθε προεπιλογή, δώστε τη στοχευόμενη θερμοκρασία (0 για να αγνοηθεί η προεπιλογή)",
"data": {
"eco_temp": "Θερμοκρασία στην οικονομική προεπιλογή",
"comfort_temp": "Θερμοκρασία στην άνετη προεπιλογή",
"boost_temp": "Θερμοκρασία στην ενισχυμένη προεπιλογή",
"eco_ac_temp": "Θερμοκρασία στην οικονομική προεπιλογή για τη λειτουργία AC",
"comfort_ac_temp": "Θερμοκρασία στην άνετη προεπιλογή για τη λειτουργία AC",
"boost_ac_temp": "Θερμοκρασία στην ενισχυμένη προεπιλογή για τη λειτουργία AC"
}
},
"window": {
"title": "Διαχείριση παραθύρου",
"description": "Διαχείριση ανοιχτού παραθύρου.\nΑφήστε την αντίστοιχη ταυτότητα οντότητας κενή αν δεν χρησιμοποιείται\nΜπορείτε επίσης να διαμορφώσετε την αυτόματη ανίχνευση ανοίγματος παραθύρου βάσει της μείωσης της θερμοκρασίας",
"data": {
"window_sensor_entity_id": "Ταυτότητα οντότητας αισθητήρα παραθύρου",
"window_delay": "Καθυστέρηση αισθητήρα παραθύρου (δευτερόλεπτα)",
"window_auto_open_threshold": "Όριο μείωσης θερμοκρασίας για αυτόματη ανίχνευση ανοίγματος παραθύρου (σε °/λεπτό)",
"window_auto_close_threshold": "Όριο αύξησης θερμοκρασίας για τέλος αυτόματης ανίχνευσης (σε °/λεπτό)",
"window_auto_max_duration": "Μέγιστη διάρκεια αυτόματης ανίχνευσης ανοίγματος παραθύρου (σε λεπτά)"
},
"data_description": {
"window_sensor_entity_id": "Αφήστε κενό αν δεν πρέπει να χρησιμοποιηθεί αισθητήρας παραθύρου",
"window_delay": "Η καθυστέρηση σε δευτερόλεπτα πριν ληφθεί υπόψη η ανίχνευση αισθητήρα",
"window_auto_open_threshold": "Συνιστώμενη τιμή: μεταξύ 0.05 και 0.1. Αφήστε κενό αν δεν χρησιμοποιείται αυτόματη ανίχνευση ανοίγματος παραθύρου",
"window_auto_close_threshold": "Συνιστώμενη τιμή: 0. Αφήστε κενό αν δεν χρησιμοποιείται αυτόματη ανίχνευση ανοίγματος παραθύρου",
"window_auto_max_duration": "Συνιστώμενη τιμή: 60 (μία ώρα). Αφήστε κενό αν δεν χρησιμοποιείται αυτόματη ανίχνευση ανοίγματος παραθύρου"
}
},
"motion": {
"title": "Διαχείριση κίνησης",
"description": "Διαχείριση αισθητήρα κίνησης. Ο προεπιλεγμένος τρόπος μπορεί να αλλάξει αυτόματα ανάλογα με την ανίχνευση κίνησης\nΑφήστε το αντίστοιχο entity_id κενό αν δεν χρησιμοποιείται.\nΤα motion_preset και no_motion_preset πρέπει να οριστούν στο αντίστοιχο όνομα προεπιλογής",
"data": {
"motion_sensor_entity_id": "Ανιχνευτής κίνησης entity id",
"motion_delay": "Καθυστέρηση ενεργοποίησης",
"motion_off_delay": "Καθυστέρηση απενεργοποίησης",
"motion_preset": "Προεπιλογή κίνησης",
"no_motion_preset": "Προεπιλογή χωρίς κίνηση"
},
"data_description": {
"motion_sensor_entity_id": "Το entity id του ανιχνευτή κίνησης",
"motion_delay": "Καθυστέρηση ενεργοποίησης κίνησης (δευτερόλεπτα)",
"motion_off_delay": "Καθυστέρηση απενεργοποίησης κίνησης (δευτερόλεπτα)",
"motion_preset": "Η προεπιλογή που χρησιμοποιείται όταν ανιχνεύεται κίνηση",
"no_motion_preset": "Η προεπιλογή που χρησιμοποιείται όταν δεν ανιχνεύεται κίνηση"
}
},
"power": {
"title": "Διαχείριση Ενέργειας",
"description": "Χαρακτηριστικά διαχείρισης ενέργειας.\nΠαρέχει τον αισθητήρα ισχύος και τον μέγιστο αισθητήρα ισχύος του σπιτιού σας.\nΣτη συνέχεια καθορίστε την κατανάλωση ενέργειας του θερμαντήρα όταν είναι ενεργοποιημένος.\nΌλοι οι αισθητήρες και η ισχύς της συσκευής πρέπει να έχουν την ίδια μονάδα (kW ή W).\nΑφήστε το αντίστοιχο entity_id κενό εάν δεν χρησιμοποιείται.",
"data": {
"power_sensor_entity_id": "Ταυτότητα οντότητας αισθητήρα ισχύος",
"max_power_sensor_entity_id": "Ταυτότητα οντότητας αισθητήρα μέγιστης ισχύος",
"power_temp": "Θερμοκρασία για Μείωση Ισχύος"
}
},
"presence": {
"title": "Διαχείριση Παρουσίας",
"description": "Χαρακτηριστικά διαχείρισης παρουσίας.\nΠαρέχει έναν αισθητήρα παρουσίας του σπιτιού σας (αληθές εάν κάποιος είναι παρών).\nΣτη συνέχεια καθορίστε είτε το προεπιλεγμένο πρόγραμμα που θα χρησιμοποιηθεί όταν ο αισθητήρας παρουσίας είναι ψευδής είτε την θερμοκρασιακή διαφορά που θα εφαρμοστεί.\nΕάν δίνεται προεπιλογή, η διαφορά δεν θα χρησιμοποιηθεί.\nΑφήστε το αντίστοιχο entity_id κενό εάν δεν χρησιμοποιείται.",
"data": {
"presence_sensor_entity_id": "Ταυτότητα οντότητας αισθητήρα παρουσίας (αληθές είναι παρών)",
"eco_away_temp": "Θερμοκρασία στο πρόγραμμα Eco όταν δεν υπάρχει παρουσία",
"comfort_away_temp": "Θερμοκρασία στο πρόγραμμα Comfort όταν δεν υπάρχει παρουσία",
"boost_away_temp": "Θερμοκρασία στο πρόγραμμα Boost όταν δεν υπάρχει παρουσία",
"eco_ac_away_temp": "Θερμοκρασία στο πρόγραμμα Eco όταν δεν υπάρχει παρουσία σε λειτουργία AC",
"comfort_ac_away_temp": "Θερμοκρασία στο πρόγραμμα Comfort όταν δεν υπάρχει παρουσία σε λειτουργία AC",
"boost_ac_away_temp": "Θερμοκρασία στο πρόγραμμα Boost όταν δεν υπάρχει παρουσία σε λειτουργία AC"
}
},
"advanced": {
"title": "Προηγμένες Παράμετροι",
"description": "Διαμόρφωση των προηγμένων παραμέτρων. Αφήστε τις προεπιλεγμένες τιμές εάν δεν γνωρίζετε τι κάνετε.\nΑυτές οι παράμετροι μπορούν να οδηγήσουν σε πολύ κακή ρύθμιση θερμοκρασίας ή ενέργειας.",
"data": {
"minimal_activation_delay": "Ελάχιστη καθυστέρηση ενεργοποίησης",
"security_delay_min": "Καθυστέρηση ασφαλείας (σε λεπτά)",
"security_min_on_percent": "Ελάχιστο ποσοστό ισχύος για τη λειτουργία ασφαλείας",
"security_default_on_percent": "Ποσοστό ισχύος που θα χρησιμοποιηθεί στη λειτουργία ασφαλείας"
},
"data_description": {
"minimal_activation_delay": "Καθυστέρηση σε δευτερόλεπτα κάτω από την οποία ο εξοπλισμός δεν θα ενεργοποιηθεί",
"security_delay_min": "Μέγιστη επιτρεπόμενη καθυστέρηση σε λεπτά μεταξύ δύο μετρήσεων θερμοκρασίας. Πάνω από αυτή την καθυστέρηση, ο θερμοστάτης θα μεταβεί σε κατάσταση ασφαλείας",
"security_min_on_percent": "Ελάχιστη τιμή ποσοστού θέρμανσης για ενεργοποίηση του προεπιλεγμένου ασφαλείας. Κάτω από αυτό το ποσοστό ισχύος, ο θερμοστάτης δεν θα μεταβεί στο προεπιλεγμένο ασφαλείας",
"security_default_on_percent": "Η προεπιλεγμένη τιμή ποσοστού ισχύος θέρμανσης στο προεπιλεγμένο ασφαλείας. Ορίστε σε 0 για να απενεργοποιήσετε τη θερμάστρα στο παρόν ασφαλείας"
}
}
},
"error": {
"unknown": "Απροσδόκητο λάθος",
"unknown_entity": "Άγνωστο αναγνωριστικό οντότητας",
"window_open_detection_method": "Πρέπει να χρησιμοποιηθεί μόνο μία μέθοδος ανίχνευσης ανοιχτού παραθύρου. Χρησιμοποιήστε αισθητήρα ή αυτόματη ανίχνευση μέσω κατωφλίου θερμοκρασίας αλλά όχι και τα δύο"
},
"abort": {
"already_configured": "Η συσκευή έχει ήδη ρυθμιστεί"
}
},
"selector": {
"thermostat_type": {
"options": {
"thermostat_over_switch": "Θερμοστάτης πάνω σε διακόπτη",
"thermostat_over_climate": "Θερμοστάτης πάνω σε κλίμα",
"thermostat_over_valve": "Θερμοστάτης πάνω σε βαλβίδα"
}
},
"auto_regulation_mode": {
"options": {
"auto_regulation_slow": "Αργή",
"auto_regulation_strong": "Δυνατή",
"auto_regulation_medium": "Μέτρια",
"auto_regulation_light": "Ελαφριά",
"auto_regulation_expert": "Εμπειρογνώμων",
"auto_regulation_none": "Χωρίς αυτόματη ρύθμιση"
}
}
},
"entity": {
"climate": {
"versatile_thermostat": {
"state_attributes": {
"preset_mode": {
"state": {
"power": "Μείωση",
"security": "Ασφάλεια",
"none": "Χειροκίνητο"
}
}
}
}
}
}
}

View File

@@ -14,6 +14,7 @@
"cycle_min": "Cycle duration (minutes)",
"temp_min": "Minimal temperature allowed",
"temp_max": "Maximal temperature allowed",
"device_power": "Device power",
"use_window_feature": "Use window detection",
"use_motion_feature": "Use motion detection",
"use_power_feature": "Use power management",
@@ -21,12 +22,47 @@
}
},
"type": {
"title": "Linked entity",
"description": "Linked entity attributes",
"title": "Linked entities",
"description": "Linked entities attributes",
"data": {
"heater_entity_id": "Heater entity id",
"heater_entity_id": "1st heater switch",
"heater_entity2_id": "2nd heater switch",
"heater_entity3_id": "3rd heater switch",
"heater_entity4_id": "4th heater switch",
"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",
"auto_regulation_mode": "Self-regulation",
"auto_regulation_dtemp": "Regulation threshold",
"auto_regulation_periode_min": "Regulation minimal period",
"inverse_switch_command": "Inverse switch command"
},
"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",
"proportional_function": "Algorithm to use (TPI is the only one for now)",
"climate_entity_id": "Underlying thermostat entity id"
"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",
"auto_regulation_mode": "Auto adjustment of the target temperature",
"auto_regulation_dtemp": "The threshold in ° under which the temperature change will not be sent",
"auto_regulation_periode_min": "Duration in minutes between two regulation update",
"inverse_switch_command": "For switch with pilot wire and diode you may need to invert the command"
}
},
"tpi": {
@@ -39,27 +75,48 @@
},
"presets": {
"title": "Presets",
"description": "For each presets, give the target temperature (0 to ignore preset)",
"description": "For each preset set the target temperature (0 to ignore preset)",
"data": {
"eco_temp": "Temperature in Eco preset",
"comfort_temp": "Temperature in Comfort preset",
"boost_temp": "Temperature in Boost preset"
"boost_temp": "Temperature in Boost preset",
"eco_ac_temp": "Temperature in Eco preset for AC mode",
"comfort_ac_temp": "Temperature in Comfort preset for AC mode",
"boost_ac_temp": "Temperature in Boost preset for AC mode"
}
},
"window": {
"title": "Window management",
"description": "Open window management.\nLeave corresponding entity_id empty if not used.",
"description": "Open window management.\nLeave corresponding entity_id empty if not used\nYou can also configure automatic window open detection based on temperature decrease",
"data": {
"window_sensor_entity_id": "Window sensor entity id",
"window_delay": "Window delay (seconds)"
"window_delay": "Window sensor delay (seconds)",
"window_auto_open_threshold": "Temperature decrease threshold for automatic window open detection (in °/hours)",
"window_auto_close_threshold": "Temperature increase threshold for end of automatic detection (in °/hours)",
"window_auto_max_duration": "Maximum duration of automatic window open detection (in min)"
},
"data_description": {
"window_sensor_entity_id": "Leave empty if no window sensor should be used",
"window_delay": "The delay in seconds before sensor detection is taken into account",
"window_auto_open_threshold": "Recommended value: between 3 and 10. Leave empty if automatic window open detection is not used",
"window_auto_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"
}
},
"motion": {
"title": "Motion management",
"description": "Motion sensor management. Preset can switch automatically depending of a motion detection\nLeave corresponding entity_id empty if not used.\nmotion_preset and no_motion_preset should be set to the corresponding preset name",
"description": "Motion sensor management. Preset can switch automatically depending on motion detection\nLeave corresponding entity_id empty if not used.\nmotion_preset and no_motion_preset should be set to the corresponding preset name",
"data": {
"motion_sensor_entity_id": "Motion sensor entity id",
"motion_delay": "Motion delay (seconds)",
"motion_delay": "Activation delay",
"motion_off_delay": "Deactivation delay",
"motion_preset": "Motion preset",
"no_motion_preset": "No motion preset"
},
"data_description": {
"motion_sensor_entity_id": "The entity id of the motion sensor",
"motion_delay": "Motion activation delay (seconds)",
"motion_off_delay": "Motion deactivation delay (seconds)",
"motion_preset": "Preset to use when motion is detected",
"no_motion_preset": "Preset to use when no motion is detected"
}
@@ -70,7 +127,6 @@
"data": {
"power_sensor_entity_id": "Power sensor entity id",
"max_power_sensor_entity_id": "Max power sensor entity id",
"device_power": "Device power (kW)",
"power_temp": "Temperature for Power shedding"
}
},
@@ -78,26 +134,36 @@
"title": "Presence management",
"description": "Presence management attributes.\nGives the a presence sensor of your home (true is someone is present).\nThen specify either the preset to use when presence sensor is false or the offset in temperature to apply.\nIf preset is given, the offset will not be used.\nLeave corresponding entity_id empty if not used.",
"data": {
"presence_sensor_entity_id": "Presence sensor entity id (true is present)",
"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"
"boost_away_temp": "Temperature in Boost preset when no presence",
"eco_ac_away_temp": "Temperature in Eco preset when no presence in AC mode",
"comfort_ac_away_temp": "Temperature in Comfort preset when no presence in AC mode",
"boost_ac_away_temp": "Temperature in Boost preset when no presence in AC mode"
}
},
"advanced": {
"title": "Advanced parameters",
"description": "Configuration of advanced parameters. Leave the default values if you don't know what you are doing.\nThis parameters can lead to a very bad temperature or power regulation.",
"description": "Configuration of advanced parameters. Leave the default values if you don't know what you are doing.\nThese parameters can lead to very poor temperature control or bad power regulation.",
"data": {
"minimal_activation_delay": "Delay in secondes under which the equipment will not be activated",
"security_delay_min": "Maximum allowed delay in minutes between two temperature mesures. Above this delay, the thermostat will turn to a sceurity off state",
"security_min_on_percent": "Minimal heating percent value for security preset activation. Below this amount of on_percent the thermostat won't go into security preset",
"security_default_on_percent": "The default heating percent value in security preset. Set to 0 to switch off heater in security present"
"minimal_activation_delay": "Minimal activation delay",
"security_delay_min": "Security delay (in minutes)",
"security_min_on_percent": "Minimal power percent to enable security mode",
"security_default_on_percent": "Power percent to use in security mode"
},
"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 security off state",
"security_min_on_percent": "Minimal heating percent value for security preset activation. Below this amount of power percent the thermostat won't go into security preset",
"security_default_on_percent": "The default heating power percent value in security preset. Set to 0 to switch off heater in security preset"
}
}
},
"error": {
"unknown": "Unexpected error",
"unknown_entity": "Unknown entity id"
"unknown_entity": "Unknown entity id",
"window_open_detection_method": "Only one window open detection method should be used. Use either window sensor or automatic detection through temperature threshold but not both"
},
"abort": {
"already_configured": "Device is already configured"
@@ -117,6 +183,7 @@
"cycle_min": "Cycle duration (minutes)",
"temp_min": "Minimal temperature allowed",
"temp_max": "Maximal temperature allowed",
"device_power": "Device power (kW)",
"use_window_feature": "Use window detection",
"use_motion_feature": "Use motion detection",
"use_power_feature": "Use power management",
@@ -124,12 +191,47 @@
}
},
"type": {
"title": "Linked entity",
"description": "Linked entity attributes",
"title": "Linked entities",
"description": "Linked entities attributes",
"data": {
"heater_entity_id": "Heater entity id",
"heater_entity_id": "1st heater switch",
"heater_entity2_id": "2nd heater switch",
"heater_entity3_id": "3rd heater switch",
"heater_entity4_id": "4th heater switch",
"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",
"auto_regulation_mode": "Self-regulation",
"auto_regulation_dtemp": "Regulation threshold",
"auto_regulation_periode_min": "Regulation minimal period",
"inverse_switch_command": "Inverse switch command"
},
"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",
"proportional_function": "Algorithm to use (TPI is the only one for now)",
"climate_entity_id": "Underlying thermostat entity id"
"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",
"auto_regulation_mode": "Auto adjustment of the target temperature",
"auto_regulation_dtemp": "The threshold in ° under which the temperature change will not be sent",
"auto_regulation_periode_min": "Duration in minutes between two regulation update",
"inverse_switch_command": "For switch with pilot wire and diode you may need to invert the command"
}
},
"tpi": {
@@ -142,19 +244,32 @@
},
"presets": {
"title": "Presets",
"description": "For each presets, give the target temperature (0 to ignore preset)",
"description": "For each preset set the target temperature (0 to ignore preset)",
"data": {
"eco_temp": "Temperature in Eco preset",
"comfort_temp": "Temperature in Comfort preset",
"boost_temp": "Temperature in Boost preset"
"boost_temp": "Temperature in Boost preset",
"eco_ac_temp": "Temperature in Eco preset for AC mode",
"comfort_ac_temp": "Temperature in Comfort preset for AC mode",
"boost_ac_temp": "Temperature in Boost preset for AC mode"
}
},
"window": {
"title": "Window management",
"description": "Open window management.\nLeave corresponding entity_id empty if not used.",
"description": "Open window management.\nLeave corresponding entity_id empty if not used\nYou can also configure automatic window open detection based on temperature decrease",
"data": {
"window_sensor_entity_id": "Window sensor entity id",
"window_delay": "Window delay (seconds)"
"window_delay": "Window sensor delay (seconds)",
"window_auto_open_threshold": "Temperature decrease threshold for automatic window open detection (in °/hours)",
"window_auto_close_threshold": "Temperature increase threshold for end of automatic detection (in °/hours)",
"window_auto_max_duration": "Maximum duration of automatic window open detection (in min)"
},
"data_description": {
"window_sensor_entity_id": "Leave empty if no window sensor should be used",
"window_delay": "The delay in seconds before sensor detection is taken into account",
"window_auto_open_threshold": "Recommended value: between 3 and 10. Leave empty if automatic window open detection is not used",
"window_auto_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"
}
},
"motion": {
@@ -162,7 +277,15 @@
"description": "Motion sensor management. Preset can switch automatically depending of a motion detection\nLeave corresponding entity_id empty if not used.\nmotion_preset and no_motion_preset should be set to the corresponding preset name",
"data": {
"motion_sensor_entity_id": "Motion sensor entity id",
"motion_delay": "Motion delay (seconds)",
"motion_delay": "Activation delay",
"motion_off_delay": "Deactivation delay",
"motion_preset": "Motion preset",
"no_motion_preset": "No motion preset"
},
"data_description": {
"motion_sensor_entity_id": "The entity id of the motion sensor",
"motion_delay": "Motion activation delay (seconds)",
"motion_off_delay": "Motion deactivation delay (seconds)",
"motion_preset": "Preset to use when motion is detected",
"no_motion_preset": "Preset to use when no motion is detected"
}
@@ -173,7 +296,6 @@
"data": {
"power_sensor_entity_id": "Power sensor entity id",
"max_power_sensor_entity_id": "Max power sensor entity id",
"device_power": "Device power (kW)",
"power_temp": "Temperature for Power shedding"
}
},
@@ -181,26 +303,36 @@
"title": "Presence management",
"description": "Presence management attributes.\nGives the a presence sensor of your home (true is someone is present).\nThen specify either the preset to use when presence sensor is false or the offset in temperature to apply.\nIf preset is given, the offset will not be used.\nLeave corresponding entity_id empty if not used.",
"data": {
"presence_sensor_entity_id": "Presence sensor entity id (true is present)",
"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"
"boost_away_temp": "Temperature in Boost preset when no presence",
"eco_ac_away_temp": "Temperature in Eco preset when no presence in AC mode",
"comfort_ac_away_temp": "Temperature in Comfort preset when no presence in AC mode",
"boost_ac_away_temp": "Temperature in Boost preset when no presence in AC mode"
}
},
"advanced": {
"title": "Advanced parameters",
"description": "Configuration of advanced parameters. Leave the default values if you don't know what you are doing.\nThis parameters can lead to a very bad temperature or power regulation.",
"description": "Configuration of advanced parameters. Leave the default values if you don't know what you are doing.\nThese parameters can lead to very poor temperature control or bad power regulation.",
"data": {
"minimal_activation_delay": "Delay in secondes under which the equipment will not be activated",
"security_delay_min": "Maximum allowed delay in minutes between two temperature mesures. Above this delay, the thermostat will turn to a sceurity off state",
"security_min_on_percent": "Minimal heating percent value for security preset activation. Below this amount of on_percent the thermostat won't go into security preset",
"security_default_on_percent": "The default heating percent value in security preset. Set to 0 to switch off heater in security present"
"minimal_activation_delay": "Minimal activation delay",
"security_delay_min": "Security delay (in minutes)",
"security_min_on_percent": "Minimal power percent to enable security mode",
"security_default_on_percent": "Power percent to use in security mode"
},
"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 security off state",
"security_min_on_percent": "Minimal heating percent value for security preset activation. Below this amount of power percent the thermostat won't go into security preset",
"security_default_on_percent": "The default heating power percent value in security preset. Set to 0 to switch off heater in security preset"
}
}
},
"error": {
"unknown": "Unexpected error",
"unknown_entity": "Unknown entity id"
"unknown_entity": "Unknown entity id",
"window_open_detection_method": "Only one window open detection method should be used. Use either window sensor or automatic detection through temperature threshold but not both"
},
"abort": {
"already_configured": "Device is already configured"
@@ -210,7 +342,18 @@
"thermostat_type": {
"options": {
"thermostat_over_switch": "Thermostat over a switch",
"thermostat_over_climate": "Thermostat over another thermostat"
"thermostat_over_climate": "Thermostat over a climate",
"thermostat_over_valve": "Thermostat over a valve"
}
},
"auto_regulation_mode": {
"options": {
"auto_regulation_slow": "Slow",
"auto_regulation_strong": "Strong",
"auto_regulation_medium": "Medium",
"auto_regulation_light": "Light",
"auto_regulation_expert": "Expert",
"auto_regulation_none": "No auto-regulation"
}
}
},
@@ -228,16 +371,5 @@
}
}
}
},
"state_attributes": {
"_": {
"preset_mode": {
"state": {
"power": "Shedding",
"security": "Security",
"none": "Manual"
}
}
}
}
}

View File

@@ -8,11 +8,13 @@
"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": "Temperature exterieure sensor entity id",
"external_temperature_sensor_entity_id": "Température exterieure sensor entity id",
"cycle_min": "Durée du cycle (minutes)",
"temp_min": "Température minimale permise",
"temp_max": "Température maximale permise",
"device_power": "Puissance de l'équipement",
"use_window_feature": "Avec détection des ouvertures",
"use_motion_feature": "Avec détection de mouvement",
"use_power_feature": "Avec gestion de la puissance",
@@ -20,12 +22,47 @@
}
},
"type": {
"title": "Entité liée",
"description": "Attributs de l'entité liée",
"title": "Entité(s) liée(s)",
"description": "Attributs de(s) l'entité(s) liée(s)",
"data": {
"heater_entity_id": "Radiateur entity id",
"heater_entity_id": "1er radiateur",
"heater_entity2_id": "2ème radiateur",
"heater_entity3_id": "3ème radiateur",
"heater_entity4_id": "4ème radiateur",
"proportional_function": "Algorithme",
"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",
"auto_regulation_mode": "Auto-régulation",
"auto_regulation_dtemp": "Seuil de régulation",
"auto_regulation_periode_min": "Période minimale de régulation",
"inverse_switch_command": "Inverser la commande"
},
"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",
"proportional_function": "Algorithme à utiliser (Seul TPI est disponible pour l'instant)",
"climate_entity_id": "Thermostat sous-jacent entity id"
"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 température cible",
"auto_regulation_dtemp": "Le seuil en ° au-dessous duquel la régulation ne sera pas envoyée",
"auto_regulation_periode_min": "La durée en minutes entre deux mise à jour faites par la régulation",
"inverse_switch_command": "Inverse la commande du switch pour une installation avec fil pilote et diode"
}
},
"tpi": {
@@ -42,23 +79,44 @@
"data": {
"eco_temp": "Température en preset Eco",
"comfort_temp": "Température en preset Comfort",
"boost_temp": "Température en preset Boost"
"boost_temp": "Température en preset Boost",
"eco_ac_temp": "Température en preset Eco en mode AC",
"comfort_ac_temp": "Température en preset Comfort en mode AC",
"boost_ac_temp": "Température en preset Boost en mode AC"
}
},
"window": {
"title": "Gestion d'une ouverture",
"description": "Coupe le radiateur si l'ouverture est ouverte.\nLaissez l'entity id vide si non utilisé.",
"data": {
"window_sensor_entity_id": "Ouverture sensor entity id",
"window_delay": "Délai avant extinction (seconds)"
"window_sensor_entity_id": "Détecteur d'ouverture (entity id)",
"window_delay": "Délai avant extinction (secondes)",
"window_auto_open_threshold": "Seuil haut de chute de température pour la détection automatique (en °/heure)",
"window_auto_close_threshold": "Seuil bas de chute de température pour la fin de détection automatique (en °/heure)",
"window_auto_max_duration": "Durée maximum d'une extinction automatique (en min)"
},
"data_description": {
"window_sensor_entity_id": "Laissez vide si vous n'avez de détecteur",
"window_delay": "Le délai (en secondes) avant que le changement du détecteur soit pris en compte",
"window_auto_open_threshold": "Valeur recommandée: entre 3 et 10. Laissez vide si vous n'utilisez pas la détection automatique",
"window_auto_close_threshold": "Valeur recommandée: 0. Laissez vide si vous n'utilisez pas la détection automatique",
"window_auto_max_duration": "Valeur recommandée: 60 (1 heure). Laissez vide si vous n'utilisez pas la détection automatique"
}
},
"motion": {
"title": "Gestion de la détection de mouvement",
"description": "Le preset s'ajuste automatiquement si un mouvement est détecté\nLaissez l'entity id vide si non utilisé.\n'Preset mouvement' et 'Preset sans mouvement' doivent être choisis avec les preset à utiliser.",
"data": {
"motion_sensor_entity_id": "Détecteur de mouvement",
"motion_delay": "Délai d'activation",
"motion_off_delay": "Délai de désactivation",
"motion_preset": "Preset si mouvement",
"no_motion_preset": "Preset si pas de mouvement"
},
"data_description": {
"motion_sensor_entity_id": "Détecteur de mouvement entity id",
"motion_delay": "Délai avant changement (seconds)",
"motion_delay": "Délai avant activation lorsqu'un mouvement est détecté (secondss)",
"motion_off_delai": "Délai avant désactivation lorsqu'aucun mouvement n'est détecté (secondes)",
"motion_preset": "Preset à utiliser si mouvement détecté",
"no_motion_preset": "Preset à utiliser si pas de mouvement détecté"
}
@@ -69,7 +127,6 @@
"data": {
"power_sensor_entity_id": "Capteur de puissance totale (entity id)",
"max_power_sensor_entity_id": "Capteur de puissance Max (entity id)",
"device_power": "Puissance de l'équipement",
"power_temp": "Température si délestaqe"
}
},
@@ -80,15 +137,24 @@
"presence_sensor_entity_id": "Capteur de présence entity id (true si quelqu'un est présent)",
"eco_away_temp": "Température en preset Eco en cas d'absence",
"comfort_away_temp": "Température en preset Comfort en cas d'absence",
"boost_away_temp": "Température en preset Boost en cas d'absence"
"boost_away_temp": "Température en preset Boost en cas d'absence",
"eco_ac_away_temp": "Température en preset Eco en cas d'absence en mode AC",
"comfort_ac_away_temp": "Température en preset Comfort en cas d'absence en mode AC",
"boost_ac_away_temp": "Température en preset Boost en cas d'absence en mode AC"
}
},
"advanced": {
"title": "Parameters avancés",
"description": "Configuration des paramètres avancés. Laissez les valeurs par défaut si vous ne savez pas ce que vous faites.\nCes paramètres peuvent induire des mauvais comportements du thermostat.",
"data": {
"minimal_activation_delay": "Délai en secondes en-dessous duquel l'équipement ne sera pas activé",
"security_delay_min": "Délai maximal autorisé en minutes entre 2 mesures de températures. Au-dessus de ce délai, le thermostat se mettra en position éteinte de sécurité",
"minimal_activation_delay": "Délai minimal d'activation",
"security_delay_min": "Délai maximal entre 2 mesures de températures",
"security_min_on_percent": "Pourcentage minimal de puissance",
"security_default_on_percent": "Pourcentage de puissance a utiliser en mode securité"
},
"data_description": {
"minimal_activation_delay": "Délai en seondes en-dessous duquel l'équipement ne sera pas activé",
"security_delay_min": "Délai maximal autorisé en minutes entre 2 mesures de températures. Au-dessus de ce délai, le thermostat se mettra en position de sécurité",
"security_min_on_percent": "Seuil minimal de pourcentage de chauffage en-dessous duquel le préréglage sécurité ne sera jamais activé",
"security_default_on_percent": "Valeur par défaut pour le pourcentage de chauffage en mode sécurité. Mettre 0 pour éteindre le radiateur en mode sécurité"
}
@@ -96,7 +162,8 @@
},
"error": {
"unknown": "Erreur inattendue",
"unknown_entity": "entity id inconnu"
"unknown_entity": "entity id inconnu",
"window_open_detection_method": "Une seule méthode de détection des ouvertures ouvertes doit être utilisée. Utilisez le détecteur d'ouverture ou les seuils de température mais pas les deux."
},
"abort": {
"already_configured": "Le device est déjà configuré"
@@ -117,6 +184,7 @@
"cycle_min": "Durée du cycle (minutes)",
"temp_min": "Température minimale permise",
"temp_max": "Température maximale permise",
"device_power": "Puissance de l'équipement",
"use_window_feature": "Avec détection des ouvertures",
"use_motion_feature": "Avec détection de mouvement",
"use_power_feature": "Avec gestion de la puissance",
@@ -124,12 +192,47 @@
}
},
"type": {
"title": "Entité liée",
"description": "Attributs de l'entité liée",
"title": "Entité(s) liée(s)",
"description": "Attributs de(s) l'entité(s) liée(s)",
"data": {
"heater_entity_id": "Radiateur entity id",
"heater_entity_id": "1er radiateur",
"heater_entity2_id": "2ème radiateur",
"heater_entity3_id": "3ème radiateur",
"heater_entity4_id": "4ème radiateur",
"proportional_function": "Algorithme",
"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",
"auto_regulation_dtemp": "Seuil de régulation",
"auto_regulation_periode_min": "Période minimale de régulation",
"inverse_switch_command": "Inverser la commande"
},
"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",
"proportional_function": "Algorithme à utiliser (Seul TPI est disponible pour l'instant)",
"climate_entity_id": "Thermostat sous-jacent entity id"
"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",
"auto_regulation_dtemp": "Le seuil en ° au-dessous duquel la régulation ne sera pas envoyée",
"auto_regulation_periode_min": "La durée en minutes entre deux mise à jour faites par la régulation",
"inverse_switch_command": "Inverse la commande du switch pour une installation avec fil pilote et diode"
}
},
"tpi": {
@@ -146,23 +249,44 @@
"data": {
"eco_temp": "Température en preset Eco",
"comfort_temp": "Température en preset Comfort",
"boost_temp": "Température en preset Boost"
"boost_temp": "Température en preset Boost",
"eco_ac_temp": "Température en preset Eco en mode AC",
"comfort_ac_temp": "Température en preset Comfort en mode AC",
"boost_ac_temp": "Température en preset Boost en mode AC"
}
},
"window": {
"title": "Gestion d'une ouverture",
"description": "Coupe le radiateur si l'ouverture est ouverte.\nLaissez l'entity id vide si non utilisé.",
"data": {
"window_sensor_entity_id": "Ouverture sensor entity id",
"window_delay": "Délai avant extinction (seconds)"
"window_sensor_entity_id": "Détecteur d'ouverture (entity id)",
"window_delay": "Délai avant extinction (secondes)",
"window_auto_open_threshold": "Seuil haut de chute de température pour la détection automatique (en °/heure)",
"window_auto_close_threshold": "Seuil bas de chute de température pour la fin de détection automatique (en °/heure)",
"window_auto_max_duration": "Durée maximum d'une extinction automatique (en min)"
},
"data_description": {
"window_sensor_entity_id": "Laissez vide si vous n'avez de détecteur",
"window_delay": "Le délai (en secondes) avant que le changement du détecteur soit pris en compte",
"window_auto_open_threshold": "Valeur recommandée: entre 3 et 10. Laissez vide si vous n'utilisez pas la détection automatique",
"window_auto_close_threshold": "Valeur recommandée: 0. Laissez vide si vous n'utilisez pas la détection automatique",
"window_auto_max_duration": "Valeur recommandée: 60 (1 heure). Laissez vide si vous n'utilisez pas la détection automatique"
}
},
"motion": {
"title": "Gestion de la détection de mouvement",
"description": "Le preset s'ajuste automatiquement si un mouvement est détecté\nLaissez l'entity id vide si non utilisé.\n'Preset mouvement' et 'Preset sans mouvement' doivent être choisis avec les preset à utiliser.",
"data": {
"motion_sensor_entity_id": "Détecteur de mouvement",
"motion_delay": "Délai d'activation",
"motion_off_delay": "Délai de désactivation",
"motion_preset": "Preset si mouvement",
"no_motion_preset": "Preset si pas de mouvement"
},
"data_description": {
"motion_sensor_entity_id": "Détecteur de mouvement entity id",
"motion_delay": "Délai avant changement (seconds)",
"motion_delay": "Délai avant activation lorsqu'un mouvement est détecté (secondss)",
"motion_off_delai": "Délai avant désactivation lorsqu'aucun mouvement n'est détecté (secondes)",
"motion_preset": "Preset à utiliser si mouvement détecté",
"no_motion_preset": "Preset à utiliser si pas de mouvement détecté"
}
@@ -173,7 +297,6 @@
"data": {
"power_sensor_entity_id": "Capteur de puissance totale (entity id)",
"max_power_sensor_entity_id": "Capteur de puissance Max (entity id)",
"device_power": "Puissance de l'équipement",
"power_temp": "Température si délestaqe"
}
},
@@ -184,15 +307,24 @@
"presence_sensor_entity_id": "Capteur de présence entity id (true si quelqu'un est présent)",
"eco_away_temp": "Température en preset Eco en cas d'absence",
"comfort_away_temp": "Température en preset Comfort en cas d'absence",
"boost_away_temp": "Température en preset Boost en cas d'absence"
"boost_away_temp": "Température en preset Boost en cas d'absence",
"eco_ac_away_temp": "Température en preset Eco en cas d'absence en mode AC",
"comfort_ac_away_temp": "Température en preset Comfort en cas d'absence en mode AC",
"boost_ac_away_temp": "Température en preset Boost en cas d'absence en mode AC"
}
},
"advanced": {
"title": "Parameters avancés",
"description": "Configuration des paramètres avancés. Laissez les valeurs par défaut si vous ne savez pas ce que vous faites.\nCes paramètres peuvent induire des mauvais comportements du thermostat.",
"data": {
"minimal_activation_delay": "Délai minimal d'activation",
"security_delay_min": "Délai maximal entre 2 mesures de températures",
"security_min_on_percent": "Pourcentage minimal de puissance",
"security_default_on_percent": "Pourcentage de puissance a utiliser en mode securité"
},
"data_description": {
"minimal_activation_delay": "Délai en seondes en-dessous duquel l'équipement ne sera pas activé",
"security_delay_min": "Délai maximal autorisé en minutes entre 2 mesures de températures. Au-dessus de ce délai, le thermostat se mettra en position éteinte de sécurité",
"security_delay_min": "Délai maximal autorisé en minutes entre 2 mesures de températures. Au-dessus de ce délai, le thermostat se mettra en position de sécurité",
"security_min_on_percent": "Seuil minimal de pourcentage de chauffage en-dessous duquel le préréglage sécurité ne sera jamais activé",
"security_default_on_percent": "Valeur par défaut pour le pourcentage de chauffage en mode sécurité. Mettre 0 pour éteindre le radiateur en mode sécurité"
}
@@ -200,7 +332,8 @@
},
"error": {
"unknown": "Erreur inattendue",
"unknown_entity": "entity id inconnu"
"unknown_entity": "entity id inconnu",
"window_open_detection_method": "Une seule méthode de détection des ouvertures ouvertes doit être utilisée. Utilisez le détecteur d'ouverture ou les seuils de température mais pas les deux."
},
"abort": {
"already_configured": "Le device est déjà configuré"
@@ -210,7 +343,18 @@
"thermostat_type": {
"options": {
"thermostat_over_switch": "Thermostat sur un switch",
"thermostat_over_climate": "Thermostat sur un autre thermostat"
"thermostat_over_climate": "Thermostat sur un autre thermostat",
"thermostat_over_valve": "Thermostat sur une valve"
}
},
"auto_regulation_mode": {
"options": {
"auto_regulation_slow": "Lente",
"auto_regulation_strong": "Forte",
"auto_regulation_medium": "Moyenne",
"auto_regulation_light": "Légère",
"auto_regulation_expert": "Expert",
"auto_regulation_none": "Aucune"
}
}
},
@@ -228,16 +372,5 @@
}
}
}
},
"state_attributes": {
"_": {
"preset_mode": {
"state": {
"power": "Délestage",
"security": "Sécurité",
"none": "Manuel"
}
}
}
}
}

View File

@@ -0,0 +1,353 @@
{
"title": "Configurazione Versatile Thermostat",
"config": {
"flow_title": "Configurazione Versatile Thermostat",
"step": {
"user": {
"title": "Aggiungi un nuovo Versatile Thermostat",
"description": "Principali parametri obbligatori",
"data": {
"name": "Nome",
"thermostat_type": "Tipologia di termostato",
"temperature_sensor_entity_id": "Entity id sensore temperatura",
"external_temperature_sensor_entity_id": "Entity id sensore temperatura esterna",
"cycle_min": "Durata del ciclo (minuti)",
"temp_min": "Temperatura minima ammessa",
"temp_max": "Temperatura massima ammessa",
"device_power": "Potenza dispositivo (kW)",
"use_window_feature": "Usa il rilevamento della finestra",
"use_motion_feature": "Usa il rilevamento del movimento",
"use_power_feature": "Usa la gestione della potenza",
"use_presence_feature": "Usa il rilevamento della presenza"
}
},
"type": {
"title": "Entità collegate",
"description": "Parametri entità collegate",
"data": {
"heater_entity_id": "Primo riscaldatore",
"heater_entity2_id": "Secondo riscaldatore",
"heater_entity3_id": "Terzo riscaldatore",
"heater_entity4_id": "Quarto riscaldatore",
"proportional_function": "Algoritmo",
"climate_entity_id": "Primo termostato",
"climate_entity2_id": "Secondo termostato",
"climate_entity3_id": "Terzo termostato",
"climate_entity4_id": "Quarto termostato",
"ac_mode": "AC mode ?",
"valve_entity_id": "Prima valvola",
"valve_entity2_id": "Seconda valvolao",
"valve_entity3_id": "Terza valvola",
"valve_entity4_id": "Quarta valvola",
"auto_regulation_mode": "Autoregolamentazione",
"inverse_switch_command": "Comando inverso"
},
"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",
"proportional_function": "Algoritmo da utilizzare (il TPI per adesso è l'unico)",
"climate_entity_id": "Entity id del primo termostato",
"climate_entity2_id": "Entity id del secondo termostato",
"climate_entity3_id": "Entity id del terzo termostato",
"climate_entity4_id": "Entity id del quarto termostato",
"ac_mode": "Utilizzare la modalità AC (Air Conditioned) ?",
"valve_entity_id": "Entity id della prima valvola",
"valve_entity2_id": "Entity id della seconda valvola",
"valve_entity3_id": "Entity id della terza valvola",
"valve_entity4_id": "Entity id della quarta valvola",
"auto_regulation_mode": "Regolazione automatica della temperatura target",
"inverse_switch_command": "Inverte il controllo dell'interruttore per un'installazione con filo pilota e diodo"
}
},
"tpi": {
"title": "TPI",
"description": "Parametri del Time Proportional Integral",
"data": {
"tpi_coef_int": "Coefficiente per il delta della temperatura interna",
"tpi_coef_ext": "Coefficiente per il delta della temperatura esterna"
}
},
"presets": {
"title": "Presets",
"description": "Per ogni preset, impostare la temperatura desiderata (0 per ignorare il preset)",
"data": {
"eco_temp": "Temperatura nel preset Eco",
"comfort_temp": "Temperatura nel preset Comfort",
"boost_temp": "Temperatura nel preset Boost",
"eco_ac_temp": "Temperatura nel preset Eco (AC mode)",
"comfort_ac_temp": "Temperatura nel preset Comfort (AC mode)",
"boost_ac_temp": "Temperatura nel preset Boost (AC mode)"
}
},
"window": {
"title": "Gestione della finestra",
"description": "Gestione della finestra aperta.\nLasciare vuoto l'entity_id corrispondente se non utilizzato\nÈ inoltre possibile configurare il rilevamento automatico della finestra aperta in base alla diminuzione della temperatura",
"data": {
"window_sensor_entity_id": "Entity id sensore finestra",
"window_delay": "Ritardo sensore finestra (secondi)",
"window_auto_open_threshold": "Soglia di diminuzione della temperatura per il rilevamento automatico della finestra aperta (in °/ora)",
"window_auto_close_threshold": "Soglia di aumento della temperatura per la fine del rilevamento automatico (in °/ora)",
"window_auto_max_duration": "Durata massima del rilevamento automatico della finestra aperta (in min)"
},
"data_description": {
"window_sensor_entity_id": "Lasciare vuoto se non deve essere utilizzato alcun sensore finestra",
"window_delay": "Ritardo in secondi prima che il rilevamento del sensore sia preso in considerazione",
"window_auto_open_threshold": "Valore consigliato: tra 3 e 10. Lasciare vuoto se il rilevamento automatico della finestra aperta non è utilizzato",
"window_auto_close_threshold": "Valore consigliato: 0. Lasciare vuoto se il rilevamento automatico della finestra aperta non è utilizzato",
"window_auto_max_duration": "Valore consigliato: 60 (un'ora). Lasciare vuoto se il rilevamento automatico della finestra aperta non è utilizzato"
}
},
"motion": {
"title": "Gestione movimento",
"description": "Gestione sensore movimento. Il preset può cambiare automaticamente a seconda di un rilevamento di movimento\nLasciare vuoto l'entity_id corrispondente se non utilizzato.\nmotion_preset e no_motion_preset devono essere impostati con il nome del preset corrispondente",
"data": {
"motion_sensor_entity_id": "Entity id sensore di movimento",
"motion_delay": "Ritardo in secondi prima che il rilevamento del sensore sia preso in considerazione",
"motion_off_delay": "Ritardo in secondi di disattivazione prima che del sensore sia preso in considerazione",
"motion_preset": "Preset da utilizzare quando viene rilevato il movimento",
"no_motion_preset": "Preset da utilizzare quando non viene rilevato il movimento"
}
},
"power": {
"title": "Gestione dell'energia",
"description": "Parametri di gestione dell'energia.\nInserire la potenza massima disponibile e l'entity_id del sensore che la misura.\nQuindi inserire il consumo del riscaldatore quando è in funzione.\nTutti i parametri devono essere nella stessa unità di misura (kW o W).\nLasciare vuoto l'entity_id corrispondente se non utilizzato.",
"data": {
"power_sensor_entity_id": "Entity id sensore potenza",
"max_power_sensor_entity_id": "Entity id sensore di massima potenza",
"power_temp": "Temperatura in caso di distribuzione del carico"
}
},
"presence": {
"title": "Gestione della presenza",
"description": "Parametri di gestione della presenza.\nInserire un sensore di presenza (true se è presente qualcuno).\nQuindi specificare il preset o la riduzione di temperatura da utilizzare quando il sensore di presenza è in false.\nSe è impostato il preset, la riduzione non sarà utilizzata.\nLasciare vuoto l'entity_id corrispondente se non utilizzato.",
"data": {
"presence_sensor_entity_id": "Entity id sensore presenza (true se è presente qualcuno)",
"eco_away_temp": "Temperatura al preset Eco in caso d'assenza",
"comfort_away_temp": "Temperatura al preset Comfort in caso d'assenza",
"boost_away_temp": "Temperatura al preset Boost in caso d'assenza",
"eco_ac_away_temp": "Temperatura al preset Eco in caso d'assenza (AC mode)",
"comfort_ac_away_temp": "Temperatura al preset Comfort in caso d'assenza (AC mode)",
"boost_ac_away_temp": "Temperatura al preset Boost in caso d'assenza (AC mode)"
}
},
"advanced": {
"title": "Parametri avanzati",
"description": "Configurazione avanzata dei parametri. Lasciare i valori predefiniti se non conoscete cosa state modificando.\nQuesti parametri possono determinare una pessima gestione della temperatura e della potenza.",
"data": {
"minimal_activation_delay": "Ritardo minimo di accensione",
"security_delay_min": "Ritardo di sicurezza (in minuti)",
"security_min_on_percent": "Percentuale minima di potenza per la modalità di sicurezza",
"security_default_on_percent": "Percentuale di potenza per la modalità di sicurezza"
},
"data_description": {
"minimal_activation_delay": "Ritardo in secondi al di sotto del quale l'apparecchiatura non verrà attivata",
"security_delay_min": "Ritardo massimo consentito in minuti tra due misure di temperatura. Al di sopra di questo ritardo, il termostato passerà allo stato di sicurezza",
"security_min_on_percent": "Soglia percentuale minima di riscaldamento al di sotto della quale il preset di sicurezza non verrà mai attivato",
"security_default_on_percent": "Valore percentuale predefinito della potenza di riscaldamento nella modalità di sicurezza. Impostare a 0 per spegnere il riscaldatore nella modalità di sicurezza"
}
}
},
"error": {
"unknown": "Errore inatteso",
"unknown_entity": "Entity id sconosciuta",
"window_open_detection_method": "Può essere utilizzato un solo metodo di rilevamento finestra aperta. Utilizzare il sensore od il rilevamento automatico ma non entrambi"
},
"abort": {
"already_configured": "Il dispositivo è già configurato"
}
},
"options": {
"flow_title": "Configurazione di Versatile Thermostat",
"step": {
"user": {
"title": "Aggiungi un nuovo Versatile Thermostat",
"description": "Principali attributi obbligatori",
"data": {
"name": "Nome",
"thermostat_type": "Tipologia termostato",
"temperature_sensor_entity_id": "Entity id sensore di temperatura",
"external_temperature_sensor_entity_id": "Entity id sensore temperatura esterna",
"cycle_min": "Durata del ciclo (minuti)",
"temp_min": "Temperatura minima consentita",
"temp_max": "Temperatura massima consentita",
"device_power": "Potenza dispositivo (kW)",
"use_window_feature": "Usa il rilevamento della finestra",
"use_motion_feature": "Usa il rilevamento del movimento",
"use_power_feature": "Usa la gestione della potenza",
"use_presence_feature": "Usa il rilevamento della presenza"
}
},
"type": {
"title": "Entità collegate",
"description": "Parametri entità collegate",
"data": {
"heater_entity_id": "Primo riscaldatore",
"heater_entity2_id": "Secondo riscaldatore",
"heater_entity3_id": "Terzo riscaldatore",
"heater_entity4_id": "Quarto riscaldatore",
"proportional_function": "Algoritmo",
"climate_entity_id": "Primo termostato",
"climate_entity2_id": "Secondo termostato",
"climate_entity3_id": "Terzo termostato",
"climate_entity4_id": "Quarto termostato",
"ac_mode": "AC mode ?",
"valve_entity_id": "Prima valvola",
"valve_entity2_id": "Seconda valvola",
"valve_entity3_id": "Terza valvola",
"valve_entity4_id": "Quarta valvola",
"auto_regulation_mode": "Autoregolamentazione",
"inverse_switch_command": "Comando inverso"
},
"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",
"proportional_function": "Algoritmo da utilizzare (il TPI per adesso è l'unico)",
"climate_entity_id": "Entity id del primo termostato",
"climate_entity2_id": "Entity id del secondo termostato",
"climate_entity3_id": "Entity id del terzo termostato",
"climate_entity4_id": "Entity id del quarto termostato",
"ac_mode": "Utilizzare la modalità AC (Air Conditioned) ?",
"valve_entity_id": "Entity id della prima valvola",
"valve_entity2_id": "Entity id della seconda valvola",
"valve_entity3_id": "Entity id della terza valvola",
"valve_entity4_id": "Entity id della quarta valvola",
"auto_regulation_mode": "Autoregolamentazione",
"inverse_switch_command": "Inverte il controllo dell'interruttore per un'installazione con filo pilota e diodo"
}
},
"tpi": {
"title": "TPI",
"description": "Parametri del Time Proportional Integral",
"data": {
"tpi_coef_int": "Coefficiente per il delta della temperatura interna",
"tpi_coef_ext": "Coefficiente per il delta della temperatura esterna"
}
},
"presets": {
"title": "Presets",
"description": "Per ogni preset, impostare la temperatura desiderata (0 per ignorare il preset)",
"data": {
"eco_temp": "Temperatura nel preset Eco",
"comfort_temp": "Temperatura nel preset Comfort",
"boost_temp": "Temperatura nel preset Boost",
"eco_ac_temp": "Temperatura nel preset Eco (AC mode)",
"comfort_ac_temp": "Temperatura nel preset Comfort (AC mode)",
"boost_ac_temp": "Temperatura nel preset Boost (AC mode)"
}
},
"window": {
"title": "Gestione della finestra",
"description": "Gestione della finestra aperta.\nLasciare vuoto l'entity_id corrispondente se non utilizzato\nÈ inoltre possibile configurare il rilevamento automatico della finestra aperta in base alla diminuzione della temperatura",
"data": {
"window_sensor_entity_id": "Entity id sensore finestra",
"window_delay": "Ritardo sensore finestra (secondi)",
"window_auto_open_threshold": "Soglia di diminuzione della temperatura per il rilevamento automatico della finestra aperta (in °/ora)",
"window_auto_close_threshold": "Soglia di aumento della temperatura per la fine del rilevamento automatico (in °/ora)",
"window_auto_max_duration": "Durata massima del rilevamento automatico della finestra aperta (in min)"
},
"data_description": {
"window_sensor_entity_id": "Lasciare vuoto se non deve essere utilizzato alcun sensore finestra",
"window_delay": "Ritardo in secondi prima che il rilevamento del sensore sia preso in considerazione",
"window_auto_open_threshold": "Valore consigliato: tra 3 e 10. Lasciare vuoto se il rilevamento automatico della finestra aperta non è utilizzato",
"window_auto_close_threshold": "Valore consigliato: 0. Lasciare vuoto se il rilevamento automatico della finestra aperta non è utilizzato",
"window_auto_max_duration": "Valore consigliato: 60 (un'ora). Lasciare vuoto se il rilevamento automatico della finestra aperta non è utilizzato"
}
},
"motion": {
"title": "Gestione movimento",
"description": "Gestione sensore movimento. Il preset può cambiare automaticamente a seconda di un rilevamento di movimento\nLasciare vuoto l'entity_id corrispondente se non utilizzato.\nmotion_preset e no_motion_preset devono essere impostati con il nome del preset corrispondente",
"data": {
"motion_sensor_entity_id": "Entity id sensore di movimento",
"motion_delay": "Ritardo in secondi prima che il rilevamento del sensore sia preso in considerazione",
"motion_off_delay": "Ritardo in secondi di disattivazione prima che del sensore sia preso in considerazione",
"motion_preset": "Preset da utilizzare quando viene rilevato il movimento",
"no_motion_preset": "Preset da utilizzare quando non viene rilevato il movimento"
}
},
"power": {
"title": "Gestione dell'energia",
"description": "Parametri di gestione dell'energia.\nInserire la potenza massima disponibile e l'entity_id del sensore che la misura.\nQuindi inserire il consumo del riscaldatore quando è in funzione.\nTutti i parametri devono essere nella stessa unità di misura (kW o W).\nLasciare vuoto l'entity_id corrispondente se non utilizzato.",
"data": {
"power_sensor_entity_id": "Entity id sensore potenza",
"max_power_sensor_entity_id": "Entity id sensore di massima potenza",
"power_temp": "Temperatura in caso di distribuzione del carico"
}
},
"presence": {
"title": "Gestione della presenza",
"description": "Parametri di gestione della presenza.\nInserire un sensore di presenza (true se è presente qualcuno).\nQuindi specificare il preset o la riduzione di temperatura da utilizzare quando il sensore di presenza è in false.\nSe è impostato il preset, la riduzione non sarà utilizzata.\nLasciare vuoto l'entity_id corrispondente se non utilizzato.",
"data": {
"presence_sensor_entity_id": "Entity id sensore presenza (true se è presente qualcuno)",
"eco_away_temp": "Temperatura al preset Eco in caso d'assenza",
"comfort_away_temp": "Temperatura al preset Comfort in caso d'assenza",
"boost_away_temp": "Temperatura al preset Boost in caso d'assenza",
"eco_ac_away_temp": "Temperatura al preset Eco in caso d'assenza (AC mode)",
"comfort_ac_away_temp": "Temperatura al preset Comfort in caso d'assenza (AC mode)",
"boost_ac_away_temp": "Temperatura al preset Boost in caso d'assenza (AC mode)"
}
},
"advanced": {
"title": "Parametri avanzati",
"description": "Configurazione avanzata dei parametri. Lasciare i valori predefiniti se non conoscete cosa state modificando.\nQuesti parametri possono determinare una pessima gestione della temperatura e della potenza.",
"data": {
"minimal_activation_delay": "Ritardo minimo di accensione",
"security_delay_min": "Ritardo di sicurezza (in minuti)",
"security_min_on_percent": "Percentuale minima di potenza per la modalità di sicurezza",
"security_default_on_percent": "Percentuale di potenza per la modalità di sicurezza"
},
"data_description": {
"minimal_activation_delay": "Ritardo in secondi al di sotto del quale l'apparecchiatura non verrà attivata",
"security_delay_min": "Ritardo massimo consentito in minuti tra due misure di temperatura. Al di sopra di questo ritardo, il termostato passerà allo stato di sicurezza",
"security_min_on_percent": "Soglia percentuale minima di riscaldamento al di sotto della quale il preset di sicurezza non verrà mai attivato",
"security_default_on_percent": "Valore percentuale predefinito della potenza di riscaldamento nella modalità di sicurezza. Impostare a 0 per spegnere il riscaldatore nella modalità di sicurezza"
}
}
},
"error": {
"unknown": "Errore inatteso",
"unknown_entity": "Entity id sconosciuta",
"window_open_detection_method": "Può essere utilizzato un solo metodo di rilevamento finestra aperta. Utilizzare il sensore od il rilevamento automatico ma non entrambi"
},
"abort": {
"already_configured": "Il dispositivo è già configurato"
}
},
"selector": {
"thermostat_type": {
"options": {
"thermostat_over_switch": "Termostato su un interruttore",
"thermostat_over_climate": "Termostato su un climatizzatore",
"thermostat_over_valve": "Termostato su una valvola"
}
},
"auto_regulation_mode": {
"options": {
"auto_regulation_slow": "Lento",
"auto_regulation_strong": "Forte",
"auto_regulation_medium": "Media",
"auto_regulation_light": "Leggera",
"auto_regulation_expert": "Esperto",
"auto_regulation_none": "Nessuna autoregolamentazione"
}
}
},
"entity": {
"climate": {
"versatile_thermostat": {
"state_attributes": {
"preset_mode": {
"state": {
"power": "Ripartizione",
"security": "Sicurezza",
"none": "Manuale"
}
}
}
}
}
}
}

View File

@@ -0,0 +1,375 @@
{
"title": "Všestranná konfigurácia termostatu",
"config": {
"flow_title": "Všestranná konfigurácia termostatu",
"step": {
"user": {
"title": "Pridajte nový všestranný termostat",
"description": "Hlavné povinné atribúty",
"data": {
"name": "Názov",
"thermostat_type": "Termostat typ",
"temperature_sensor_entity_id": "ID entity snímača teploty",
"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",
"device_power": "Napájanie zariadenia",
"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"
}
},
"type": {
"title": "Prepojené entity",
"description": "Atribúty prepojených entít",
"data": {
"heater_entity_id": "1. spínač ohrievača",
"heater_entity2_id": "2. spínač ohrievača",
"heater_entity3_id": "3. spínač ohrievača",
"heater_entity4_id": "4. spínač ohrievača",
"proportional_function": "Algoritmus",
"climate_entity_id": "1. základná klíma",
"climate_entity2_id": "2. základná klíma",
"climate_entity3_id": "3. základná klíma",
"climate_entity4_id": "4. základná klíma",
"ac_mode": "AC režim",
"valve_entity_id": "1. ventil číslo",
"valve_entity2_id": "2. ventil číslo",
"valve_entity3_id": "3. ventil číslo",
"valve_entity4_id": "4. ventil číslo",
"auto_regulation_mode": "Self-regulation",
"auto_regulation_dtemp": "Regulation threshold",
"auto_regulation_periode_min": "Regulation minimal period",
"inverse_switch_command": "Inverse switch command"
},
"data_description": {
"heater_entity_id": "ID entity povinného ohrievača",
"heater_entity2_id": "Voliteľné ID entity 2. ohrievača. Ak sa nepoužíva, nechajte prázdne",
"heater_entity3_id": "Voliteľné ID entity 3. ohrievača. Ak sa nepoužíva, nechajte prázdne",
"heater_entity4_id": "Voliteľné ID entity 4. ohrievača. Ak sa nepoužíva, nechajte prázdne",
"proportional_function": "Algoritmus, ktorý sa má použiť (TPI je zatiaľ jediný)",
"climate_entity_id": "ID základnej klimatickej entity",
"climate_entity2_id": "2. základné identifikačné číslo klimatickej entity",
"climate_entity3_id": "3. základné identifikačné číslo klimatickej entity",
"climate_entity4_id": "4. základné identifikačné číslo klimatickej entity",
"ac_mode": "Použite režim klimatizácie (AC)",
"valve_entity_id": "1. ventil číslo entity id",
"valve_entity2_id": "2. ventil číslo entity id",
"valve_entity3_id": "3. ventil číslo entity id",
"valve_entity4_id": "4. ventil číslo entity id",
"auto_regulation_mode": "Auto adjustment of the target temperature",
"auto_regulation_dtemp": "The threshold in ° under which the temperature change will not be send",
"auto_regulation_periode_min": "Duration in minutes between two regulation update",
"inverse_switch_command": "For switch with pilot wire and diode you may need to inverse the command"
}
},
"tpi": {
"title": "TPI",
"description": "Časovo proporcionálne integrálne atribúty",
"data": {
"tpi_coef_int": "Koeficient na použitie pre vnútornú teplotnú deltu",
"tpi_coef_ext": "Koeficient na použitie pre deltu vonkajšej teploty"
}
},
"presets": {
"title": "Predvoľby",
"description": "Pre každú predvoľbu zadajte cieľovú teplotu (0, ak chcete predvoľbu ignorovať)",
"data": {
"eco_temp": "Teplota v predvoľbe Eco",
"comfort_temp": "Prednastavená teplota v komfortnom režime",
"boost_temp": "Teplota v prednastavení Boost",
"eco_ac_temp": "Teplota v režime Eco prednastavená pre režim AC",
"comfort_ac_temp": "Teplota v režime Comfort je prednastavená pre režim AC",
"boost_ac_temp": "Prednastavená teplota v režime Boost pre režim AC"
}
},
"window": {
"title": "Správa okien",
"description": "Otvoriť správu okien.\nAk sa príslušné entity_id nepoužíva, ponechajte prázdne\nMôžete tiež nakonfigurovať automatickú detekciu otvoreného okna na základe poklesu teploty",
"data": {
"window_sensor_entity_id": "ID entity snímača okna",
"window_delay": "Oneskorenie snímača okna (sekundy)",
"window_auto_open_threshold": "Prah poklesu teploty pre automatickú detekciu otvoreného okna (v °/hodina)",
"window_auto_close_threshold": "Prahová hodnota zvýšenia teploty pre koniec automatickej detekcie (v °/hodina)",
"window_auto_max_duration": "Maximálne trvanie automatickej detekcie otvoreného okna (v min)"
},
"data_description": {
"window_sensor_entity_id": "Nechajte prázdne, ak nemáte použiť žiadny okenný senzor",
"window_delay": "Zohľadňuje sa oneskorenie v sekundách pred detekciou snímača",
"window_auto_open_threshold": "Odporúčaná hodnota: medzi 3 a 10. Ak sa nepoužíva automatická detekcia otvoreného okna, nechajte prázdne",
"window_auto_close_threshold": "Odporúčaná hodnota: 0. Ak sa nepoužíva automatická detekcia otvoreného okna, nechajte prázdne",
"window_auto_max_duration": "Odporúčaná hodnota: 60 (jedna hodina). Ak sa nepoužíva automatická detekcia otvoreného okna, nechajte prázdne"
}
},
"motion": {
"title": "Riadenie pohybu",
"description": "Správa snímača pohybu. Predvoľba sa môže automaticky prepínať v závislosti od detekcie pohybu\nAk sa nepoužíva, ponechajte zodpovedajúce entity_id prázdne.\nmotion_preset a no_motion_preset by mali byť nastavené na zodpovedajúci názov predvoľby",
"data": {
"motion_sensor_entity_id": "ID entity snímača pohybu",
"motion_delay": "Oneskorenie aktivácie",
"motion_off_delay": "Oneskorenie deaktivácie",
"motion_preset": "Prednastavený pohyb",
"no_motion_preset": "Žiadna predvoľba pohybu"
},
"data_description": {
"motion_sensor_entity_id": "ID entity snímača pohybu",
"motion_delay": "Oneskorenie aktivácie pohybu (sekundy)",
"motion_off_delay": "Oneskorenie deaktivácie pohybu (sekundy)",
"motion_preset": "Prednastavené na použitie pri detekcii pohybu",
"no_motion_preset": "Prednastavené na použitie, keď nie je detekovaný žiadny pohyb"
}
},
"power": {
"title": "Správa napájania",
"description": "Atribúty správy napájania.\nPoskytuje senzor výkonu a maximálneho výkonu vášho domova.\nPotom zadajte spotrebu energie ohrievača, keď je zapnutý.\nVšetky senzory a výkon zariadenia by mali mať rovnakú jednotku (kW alebo W).\nPonechajte zodpovedajúce entity_id prázdne ak sa nepoužíva.",
"data": {
"power_sensor_entity_id": "ID entity snímača výkonu",
"max_power_sensor_entity_id": "ID entity snímača maximálneho výkonu",
"power_temp": "Teplota pre zníženie výkonu"
}
},
"presence": {
"title": "Riadenie prítomnosti",
"description": "Atribúty správy prítomnosti.\nPoskytuje senzor prítomnosti vášho domova (pravda, ak je niekto prítomný).\nPotom zadajte buď predvoľbu, ktorá sa má použiť, keď je senzor prítomnosti nepravdivý, alebo posun teploty, ktorý sa má použiť.\nAk je zadaná predvoľba, posun sa nepoužije.\nAk sa nepoužije, ponechajte zodpovedajúce entity_id prázdne.",
"data": {
"presence_sensor_entity_id": "ID entity senzora prítomnosti",
"eco_away_temp": "Teplota v prednastavenej Eco, keď nie je žiadna prítomnosť",
"comfort_away_temp": "Teplota v režime Comfort je prednastavená, keď nie je prítomný",
"boost_away_temp": "Prednastavená teplota v režime Boost, keď nie je prítomný",
"eco_ac_away_temp": "Teplota v prednastavenej Eco, keď nie je prítomná v režime AC",
"comfort_ac_away_temp": "Teplota v režime Comfort je prednastavená, keď nie je prítomný v režime AC",
"boost_ac_away_temp": "Teplota v prednastavenom Boost, keď nie je prítomný v režime AC"
}
},
"advanced": {
"title": "Pokročilé parametre",
"description": "Konfigurácia pokročilých parametrov. Ak neviete, čo robíte, ponechajte predvolené hodnoty.\nTento parameter môže viesť k veľmi zlej regulácii teploty alebo výkonu.",
"data": {
"minimal_activation_delay": "Minimálne oneskorenie aktivácie",
"security_delay_min": "Bezpečnostné oneskorenie (v minútach)",
"security_min_on_percent": "Minimálne percento výkonu na aktiváciu bezpečnostného režimu",
"security_default_on_percent": "Percento výkonu na použitie v bezpečnostnom režime"
},
"data_description": {
"minimal_activation_delay": "Oneskorenie v sekundách, pri ktorom sa zariadenie neaktivuje",
"security_delay_min": "Maximálne povolené oneskorenie v minútach medzi dvoma meraniami teploty. Po uplynutí tohto oneskorenia sa termostat prepne do bezpečnostného vypnutého stavu",
"security_min_on_percent": "Minimálna percentuálna hodnota ohrevu pre aktiváciu prednastavenej bezpečnosti. Pod týmto percentom výkonu termostat neprejde do prednastavenia zabezpečenia",
"security_default_on_percent": "Predvolená percentuálna hodnota vykurovacieho výkonu v bezpečnostnej predvoľbe. Nastavte na 0, ak chcete vypnúť ohrievač v zabezpečenom stave"
}
}
},
"error": {
"unknown": "Neočakávaná chyba",
"unknown_entity": "Neznáme ID entity",
"window_open_detection_method": "Mala by sa použiť iba jedna metóda detekcie otvoreného okna. Použite senzor alebo automatickú detekciu cez teplotný prah, ale nie oboje"
},
"abort": {
"already_configured": "Zariadenie je už nakonfigurované"
}
},
"options": {
"flow_title": "Všestranná konfigurácia termostatu",
"step": {
"user": {
"title": "Pridajte nový všestranný termostat",
"description": "Hlavné povinné atribúty",
"data": {
"name": "Názov",
"thermostat_type": "Termostat typ",
"temperature_sensor_entity_id": "ID entity snímača teploty",
"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",
"device_power": "Výkon zariadenia (kW)",
"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"
}
},
"type": {
"title": "Prepojené entity",
"description": "Atribúty prepojených entít",
"data": {
"heater_entity_id": "Spínač ohrievača",
"heater_entity2_id": "2. spínač ohrievača",
"heater_entity3_id": "3. spínač ohrievača",
"heater_entity4_id": "4. spínač ohrievača",
"proportional_function": "Algoritmus",
"climate_entity_id": "Základná klíma",
"climate_entity2_id": "2. základná klíma",
"climate_entity3_id": "3. základná klíma",
"climate_entity4_id": "4. základná klíma",
"ac_mode": "AC režim",
"valve_entity_id": "1. ventil číslo",
"valve_entity2_id": "2. ventil číslo",
"valve_entity3_id": "3. ventil číslo",
"valve_entity4_id": "4. ventil číslo",
"auto_regulation_mode": "Self-regulation",
"auto_regulation_dtemp": "Regulation threshold",
"auto_regulation_periode_min": "Regulation minimal period",
"inverse_switch_command": "Inverse switch command"
},
"data_description": {
"heater_entity_id": "ID entity povinného ohrievača",
"heater_entity2_id": "Voliteľné ID entity 2. ohrievača. Ak sa nepoužíva, nechajte prázdne",
"heater_entity3_id": "Voliteľné ID entity 3. ohrievača. Ak sa nepoužíva, nechajte prázdne",
"heater_entity4_id": "Voliteľné ID entity 4. ohrievača. Ak sa nepoužíva, nechajte prázdne",
"proportional_function": "Algoritmus, ktorý sa má použiť (TPI je zatiaľ jediný)",
"climate_entity_id": "ID základnej klimatickej entity",
"climate_entity2_id": "2. základný identifikátor klimatickej entity",
"climate_entity3_id": "3. základný identifikátor klimatickej entity",
"climate_entity4_id": "4. základný identifikátor klimatickej entity",
"ac_mode": "Použite režim klimatizácie (AC)",
"valve_entity_id": "1. ventil číslo entity id",
"valve_entity2_id": "2. ventil číslo entity id",
"valve_entity3_id": "3. ventil číslo entity id",
"valve_entity4_id": "4. ventil číslo entity id",
"auto_regulation_mode": "Auto adjustment of the target temperature",
"auto_regulation_dtemp": "The threshold in ° under which the temperature change will not be send",
"auto_regulation_periode_min": "Duration in minutes between two regulation update",
"inverse_switch_command": "For switch with pilot wire and diode you may need to inverse the command"
}
},
"tpi": {
"title": "TPI",
"description": "Časovo proporcionálne integrálne atribúty",
"data": {
"tpi_coef_int": "Koeficient na použitie pre vnútornú teplotnú deltu",
"tpi_coef_ext": "Koeficient na použitie pre vonkajšiu teplotnú deltu"
}
},
"presets": {
"title": "Predvoľby",
"description": "Pre každú predvoľbu zadajte cieľovú teplotu (0, ak chcete predvoľbu ignorovať)",
"data": {
"eco_temp": "Teplota v predvoľbe Eco",
"comfort_temp": "Prednastavená teplota v komfortnom režime",
"boost_temp": "Teplota v prednastavení Boost",
"eco_ac_temp": "Teplota v režime Eco prednastavená pre režim AC",
"comfort_ac_temp": "Teplota v režime Comfort je prednastavená pre režim AC",
"boost_ac_temp": "Prednastavená teplota v režime Boost pre režim AC"
}
},
"window": {
"title": "Správa okien",
"description": "Otvoriť správu okien.\nAk sa príslušné entity_id nepoužíva, ponechajte prázdne\nMôžete tiež nakonfigurovať automatickú detekciu otvoreného okna na základe poklesu teploty",
"data": {
"window_sensor_entity_id": "ID entity snímača okna",
"window_delay": "Oneskorenie snímača okna (sekundy)",
"window_auto_open_threshold": "Prah poklesu teploty pre automatickú detekciu otvoreného okna (v °/hodina)",
"window_auto_close_threshold": "Prahová hodnota zvýšenia teploty pre koniec automatickej detekcie (v °/hodina)",
"window_auto_max_duration": "Maximálne trvanie automatickej detekcie otvoreného okna (v min)"
},
"data_description": {
"window_sensor_entity_id": "Nechajte prázdne, ak nemáte použiť žiadny okenný senzor",
"window_delay": "Zohľadňuje sa oneskorenie v sekundách pred detekciou snímača",
"window_auto_open_threshold": "Odporúčaná hodnota: medzi 3 a 10. Ak sa nepoužíva automatická detekcia otvoreného okna, nechajte prázdne",
"window_auto_close_threshold": "Odporúčaná hodnota: 0. Ak sa nepoužíva automatická detekcia otvoreného okna, nechajte prázdne",
"window_auto_max_duration": "Odporúčaná hodnota: 60 (jedna hodina). Ak sa nepoužíva automatická detekcia otvoreného okna, nechajte prázdne"
}
},
"motion": {
"title": "Riadenie pohybu",
"description": "Správa snímača pohybu. Predvoľba sa môže automaticky prepínať v závislosti od detekcie pohybu\nAk sa nepoužíva, ponechajte zodpovedajúce entity_id prázdne.\nmotion_preset a no_motion_preset by mali byť nastavené na zodpovedajúci názov predvoľby",
"data": {
"motion_sensor_entity_id": "ID entity snímača pohybu",
"motion_delay": "Oneskorenie aktivácie",
"motion_off_delay": "Oneskorenie deaktivácie",
"motion_preset": "Prednastavený pohyb",
"no_motion_preset": "Žiadna predvoľba pohybu"
},
"data_description": {
"motion_sensor_entity_id": "ID entity snímača pohybu",
"motion_delay": "Oneskorenie aktivácie pohybu (sekundy)",
"motion_off_delay": "Oneskorenie deaktivácie pohybu (sekundy)",
"motion_preset": "Prednastavené na použitie pri detekcii pohybu",
"no_motion_preset": "Prednastavené na použitie, keď nie je detekovaný žiadny pohyb"
}
},
"power": {
"title": "Správa napájania",
"description": "Atribúty správy napájania.\nPoskytuje senzor výkonu a maximálneho výkonu vášho domova.\nPotom zadajte spotrebu energie ohrievača, keď je zapnutý.\nVšetky senzory a výkon zariadenia by mali mať rovnakú jednotku (kW alebo W).\nPonechajte zodpovedajúce entity_id prázdne ak sa nepoužíva.",
"data": {
"power_sensor_entity_id": "ID entity snímača výkonu",
"max_power_sensor_entity_id": "ID entity snímača maximálneho výkonu",
"power_temp": "Teplota pre zníženie výkonu"
}
},
"presence": {
"title": "Riadenie prítomnosti",
"description": "Atribúty správy prítomnosti.\nPoskytuje senzor prítomnosti vášho domova (pravda, ak je niekto prítomný).\nPotom zadajte buď predvoľbu, ktorá sa má použiť, keď je senzor prítomnosti nepravdivý, alebo posun teploty, ktorý sa má použiť.\nAk je zadaná predvoľba, posun sa nepoužije.\nAk sa nepoužije, ponechajte zodpovedajúce entity_id prázdne.",
"data": {
"presence_sensor_entity_id": "ID entity senzora prítomnosti (pravda je prítomná)",
"eco_away_temp": "Teplota v prednastavenej Eco, keď nie je žiadna prítomnosť",
"comfort_away_temp": "Teplota v režime Comfort je prednastavená, keď nie je prítomný",
"boost_away_temp": "Prednastavená teplota v režime Boost, keď nie je prítomný",
"eco_ac_away_temp": "Teplota v prednastavenej Eco, keď nie je prítomná v režime AC",
"comfort_ac_away_temp": "Teplota v režime Comfort je prednastavená, keď nie je prítomný v režime AC",
"boost_ac_away_temp": "Teplota v prednastavenom Boost, keď nie je prítomný v režime AC"
}
},
"advanced": {
"title": "Pokročilé parametre",
"description": "Konfigurácia pokročilých parametrov. Ak neviete, čo robíte, ponechajte predvolené hodnoty.\nTento parameter môže viesť k veľmi zlej regulácii teploty alebo výkonu.",
"data": {
"minimal_activation_delay": "Minimálne oneskorenie aktivácie",
"security_delay_min": "Bezpečnostné oneskorenie (v minútach)",
"security_min_on_percent": "Minimálne percento výkonu pre bezpečnostný režim",
"security_default_on_percent": "Percento výkonu na použitie v bezpečnostnom režime"
},
"data_description": {
"minimal_activation_delay": "Oneskorenie v sekundách, pri ktorom sa zariadenie neaktivuje",
"security_delay_min": "Maximálne povolené oneskorenie v minútach medzi dvoma meraniami teploty. Po uplynutí tohto oneskorenia sa termostat prepne do bezpečnostného vypnutého stavu",
"security_min_on_percent": "Minimálna percentuálna hodnota ohrevu pre aktiváciu prednastavenej bezpečnosti. Pod týmto percentom výkonu termostat neprejde do prednastavenia zabezpečenia",
"security_default_on_percent": "Predvolená percentuálna hodnota vykurovacieho výkonu v bezpečnostnej predvoľbe. Nastavte na 0, ak chcete vypnúť ohrievač v zabezpečenom stave"
}
}
},
"error": {
"unknown": "Neočakávaná chyba",
"unknown_entity": "Neznáme ID entity",
"window_open_detection_method": "Mala by sa použiť iba jedna metóda detekcie otvoreného okna. Použite senzor alebo automatickú detekciu cez teplotný prah, ale nie oboje"
},
"abort": {
"already_configured": "Zariadenie je už nakonfigurované"
}
},
"selector": {
"thermostat_type": {
"options": {
"thermostat_over_switch": "Termostat nad spínačom",
"thermostat_over_climate": "Termostat nad iným termostatom",
"thermostat_over_valve": "Thermostat over a valve"
}
},
"auto_regulation_mode": {
"options": {
"auto_regulation_slow": "Slow",
"auto_regulation_strong": "Strong",
"auto_regulation_medium": "Medium",
"auto_regulation_light": "Light",
"auto_regulation_expert": "Expert",
"auto_regulation_none": "No auto-regulation"
}
}
},
"entity": {
"climate": {
"versatile_thermostat": {
"state_attributes": {
"preset_mode": {
"state": {
"power": "Vyradenie",
"security": "Zabezpečenie",
"none": "Manuálne"
}
}
}
}
}
}
}

View File

@@ -0,0 +1,769 @@
# pylint: disable=unused-argument, line-too-long
""" 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.exceptions import ServiceNotFound
from homeassistant.core import HomeAssistant, CALLBACK_TYPE
from homeassistant.components.climate import (
ClimateEntity,
ClimateEntityFeature,
DOMAIN as CLIMATE_DOMAIN,
HVACMode,
HVACAction,
SERVICE_SET_HVAC_MODE,
SERVICE_SET_FAN_MODE,
SERVICE_SET_HUMIDITY,
SERVICE_SET_SWING_MODE,
SERVICE_TURN_OFF,
SERVICE_TURN_ON,
SERVICE_SET_TEMPERATURE,
)
from homeassistant.components.number import SERVICE_SET_VALUE
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.event import async_call_later
from .const import UnknownEntity, overrides
_LOGGER = logging.getLogger(__name__)
# remove this
# _LOGGER.setLevel(logging.DEBUG)
class UnderlyingEntityType(StrEnum):
"""All underlying device type"""
# A switch
SWITCH = "switch"
# a climate
CLIMATE = "climate"
# a valve
VALVE = "valve"
class UnderlyingEntity:
"""Represent a underlying device which could be a switch or a climate"""
_hass: HomeAssistant
# Cannot import VersatileThermostat due to circular reference
_thermostat: Any
_entity_id: str
_type: UnderlyingEntityType
def __init__(
self,
hass: HomeAssistant,
thermostat: Any,
entity_type: UnderlyingEntityType,
entity_id: str,
) -> None:
"""Initialize the underlying entity"""
self._hass = hass
self._thermostat = thermostat
self._type = entity_type
self._entity_id = entity_id
def __str__(self):
return str(self._thermostat) + "-" + self._entity_id
@property
def entity_id(self):
"""The entiy id represented by this class"""
return self._entity_id
@property
def entity_type(self) -> UnderlyingEntityType:
"""The entity type represented by this class"""
return self._type
@property
def is_initialized(self) -> bool:
"""True if the underlying is initialized"""
return True
def startup(self):
"""Startup the Entity"""
return
async def set_hvac_mode(self, hvac_mode: HVACMode):
"""Set the HVACmode"""
return
@property
def is_device_active(self) -> bool | None:
"""If the toggleable device is currently active."""
return None
async def set_temperature(self, temperature, max_temp, min_temp):
"""Set the target temperature"""
return
# This should be the correct way to handle turn_off and turn_on but this breaks the unit test
# will an not understandable error: TypeError: object MagicMock can't be used in 'await' expression
async def turn_off(self):
""" Turn off the underlying equipement.
Need to be overriden"""
return NotImplementedError
async def turn_on(self):
""" Turn off the underlying equipement.
Need to be overriden"""
return NotImplementedError
@property
def is_inversed(self):
""" Tells if the switch command should be inversed"""
return False
def remove_entity(self):
"""Remove the underlying entity"""
return
async def check_initial_state(self, hvac_mode: HVACMode):
"""Prevent the underlying to be on but thermostat is off"""
if hvac_mode == HVACMode.OFF and self.is_device_active:
_LOGGER.warning(
"%s - The hvac mode is OFF, but the underlying device is ON. Turning off device %s",
self,
self._entity_id,
)
await self.set_hvac_mode(hvac_mode)
elif hvac_mode != HVACMode.OFF and not self.is_device_active:
_LOGGER.warning(
"%s - The hvac mode is ON, but the underlying device is not ON. Turning on device %s",
self,
self._entity_id,
)
await self.set_hvac_mode(hvac_mode)
# override to be able to mock the call
def call_later(
self, hass: HomeAssistant, delay_sec: int, called_method
) -> CALLBACK_TYPE:
"""Call the method after a delay"""
return async_call_later(hass, delay_sec, called_method)
async def start_cycle(
self,
hvac_mode: HVACMode,
on_time_sec: int,
off_time_sec: int,
on_percent: int,
force=False,
):
"""Starting cycle for switch"""
def _cancel_cycle(self):
""" Stops an eventual cycle """
class UnderlyingSwitch(UnderlyingEntity):
"""Represent a underlying switch"""
_initialDelaySec: int
_on_time_sec: int
_off_time_sec: int
_hvac_mode: HVACMode
def __init__(
self,
hass: HomeAssistant,
thermostat: Any,
switch_entity_id: str,
initial_delay_sec: int,
) -> None:
"""Initialize the underlying switch"""
super().__init__(
hass=hass,
thermostat=thermostat,
entity_type=UnderlyingEntityType.SWITCH,
entity_id=switch_entity_id,
)
self._initial_delay_sec = initial_delay_sec
self._async_cancel_cycle = None
self._should_relaunch_control_heating = False
self._on_time_sec = 0
self._off_time_sec = 0
self._hvac_mode = None
@property
def initial_delay_sec(self):
"""The initial delay for this class"""
return self._initial_delay_sec
@overrides
@property
def is_inversed(self):
""" Tells if the switch command should be inversed"""
return self._thermostat.is_inversed
# @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"""
if hvac_mode == HVACMode.OFF:
if self.is_device_active:
await self.turn_off()
self._cancel_cycle()
if self._hvac_mode != hvac_mode:
self._hvac_mode = hvac_mode
return True
else:
return False
@property
def is_device_active(self):
"""If the toggleable device is currently active."""
real_state = self._hass.states.is_state(self._entity_id, STATE_ON)
return (self.is_inversed and not real_state) or (not self.is_inversed and real_state)
# @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."""
_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,
)
except ServiceNotFound as err:
_LOGGER.error(err)
async def turn_on(self):
"""Turn heater toggleable device on."""
_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,
)
except ServiceNotFound as err:
_LOGGER.error(err)
@overrides
async def start_cycle(
self,
hvac_mode: HVACMode,
on_time_sec: int,
off_time_sec: int,
on_percent: int,
force=False,
):
"""Starting cycle for switch"""
_LOGGER.debug(
"%s - Starting new cycle hvac_mode=%s on_time_sec=%d off_time_sec=%d force=%s",
self,
hvac_mode,
on_time_sec,
off_time_sec,
force,
)
self._on_time_sec = on_time_sec
self._off_time_sec = off_time_sec
self._hvac_mode = hvac_mode
# Cancel eventual previous cycle if any
if self._async_cancel_cycle is not None:
if force:
_LOGGER.debug("%s - we force a new cycle", self)
self._cancel_cycle()
else:
_LOGGER.debug(
"%s - A previous cycle is alredy running and no force -> waits for its end",
self,
)
# self._should_relaunch_control_heating = True
_LOGGER.debug("%s - End of cycle (2)", self)
return
# If we should heat, starts the cycle with delay
if self._hvac_mode in [HVACMode.HEAT, HVACMode.COOL] and on_time_sec > 0:
# Starts the cycle after the initial delay
self._async_cancel_cycle = self.call_later(
self._hass, self._initial_delay_sec, self._turn_on_later
)
_LOGGER.debug("%s - _async_cancel_cycle=%s", self, self._async_cancel_cycle)
# if we not heat but device is active
elif self.is_device_active:
_LOGGER.info(
"%s - stop heating (2) for %d min %d sec",
self,
off_time_sec // 60,
off_time_sec % 60,
)
await self.turn_off()
else:
_LOGGER.debug("%s - nothing to do", self)
@overrides
def _cancel_cycle(self):
"""Cancel the cycle"""
if self._async_cancel_cycle:
self._async_cancel_cycle()
self._async_cancel_cycle = None
_LOGGER.debug("%s - Stopping cycle during calculation", self)
async def _turn_on_later(self, _):
"""Turn the heater on after a delay"""
_LOGGER.debug(
"%s - calling turn_on_later hvac_mode=%s, should_relaunch_later=%s off_time_sec=%d",
self,
self._hvac_mode,
self._should_relaunch_control_heating,
self._on_time_sec,
)
self._cancel_cycle()
if self._hvac_mode == HVACMode.OFF:
_LOGGER.debug("%s - End of cycle (HVAC_MODE_OFF - 2)", self)
if self.is_device_active:
await self.turn_off()
return
if await self._thermostat.check_overpowering():
_LOGGER.debug("%s - End of cycle (3)", self)
return
# Security mode could have change the on_time percent
await self._thermostat.check_security()
time = self._on_time_sec
action_label = "start"
if time > 0:
_LOGGER.info(
"%s - %s heating for %d min %d sec",
self,
action_label,
time // 60,
time % 60,
)
await self.turn_on()
else:
_LOGGER.debug("%s - No action on heater cause duration is 0", self)
self._async_cancel_cycle = self.call_later(
self._hass,
time,
self._turn_off_later,
)
async def _turn_off_later(self, _):
"""Turn the heater off and call the next cycle after the delay"""
_LOGGER.debug(
"%s - calling turn_off_later hvac_mode=%s, should_relaunch_later=%s off_time_sec=%d",
self,
self._hvac_mode,
self._should_relaunch_control_heating,
self._off_time_sec,
)
self._cancel_cycle()
if self._hvac_mode == HVACMode.OFF:
_LOGGER.debug("%s - End of cycle (HVAC_MODE_OFF - 2)", self)
if self.is_device_active:
await self.turn_off()
return
action_label = "stop"
time = self._off_time_sec
if time > 0:
_LOGGER.info(
"%s - %s heating for %d min %d sec",
self,
action_label,
time // 60,
time % 60,
)
await self.turn_off()
else:
_LOGGER.debug("%s - No action on heater cause duration is 0", self)
self._async_cancel_cycle = self.call_later(
self._hass,
time,
self._turn_on_later,
)
# increment energy at the end of the cycle
self._thermostat.incremente_energy()
@overrides
def remove_entity(self):
"""Remove the entity after stopping its cycle"""
self._cancel_cycle()
class UnderlyingClimate(UnderlyingEntity):
"""Represent a underlying climate"""
_underlying_climate: ClimateEntity
def __init__(
self,
hass: HomeAssistant,
thermostat: Any,
climate_entity_id: str,
) -> None:
"""Initialize the underlying climate"""
super().__init__(
hass=hass,
thermostat=thermostat,
entity_type=UnderlyingEntityType.CLIMATE,
entity_id=climate_entity_id,
)
self._underlying_climate = None
def find_underlying_climate(self) -> ClimateEntity:
"""Find the underlying climate entity"""
component: EntityComponent[ClimateEntity] = self._hass.data[CLIMATE_DOMAIN]
for entity in component.entities:
if self.entity_id == entity.entity_id:
return entity
return None
def startup(self):
"""Startup the Entity"""
# Get the underlying climate
self._underlying_climate = self.find_underlying_climate()
if self._underlying_climate:
_LOGGER.info(
"%s - The underlying climate entity: %s have been succesfully found",
self,
self._underlying_climate,
)
else:
_LOGGER.error(
"%s - Cannot find the underlying climate entity: %s. Thermostat will not be operational",
self,
self.entity_id,
)
# #56 keep the over_climate and try periodically to find the underlying climate
# self._is_over_climate = False
raise UnknownEntity(f"Underlying entity {self.entity_id} not found")
return
@property
def is_initialized(self) -> bool:
"""True if the underlying climate was found"""
return self._underlying_climate is not None
async def set_hvac_mode(self, hvac_mode: HVACMode) -> bool:
"""Set the HVACmode of the underlying climate. Returns true if something have change"""
if not self.is_initialized:
return False
data = {ATTR_ENTITY_ID: self._entity_id, "hvac_mode": hvac_mode}
await self._hass.services.async_call(
CLIMATE_DOMAIN,
SERVICE_SET_HVAC_MODE,
data,
)
return True
@property
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,
]
else:
return None
async def set_fan_mode(self, fan_mode):
"""Set new target fan mode."""
if not self.is_initialized:
return
data = {
ATTR_ENTITY_ID: self._entity_id,
"fan_mode": fan_mode,
}
await self._hass.services.async_call(
CLIMATE_DOMAIN,
SERVICE_SET_FAN_MODE,
data,
)
async def set_humidity(self, humidity: int):
"""Set new target humidity."""
_LOGGER.info("%s - Set fan mode: %s", self, humidity)
if not self.is_initialized:
return
data = {
ATTR_ENTITY_ID: self._entity_id,
"humidity": humidity,
}
await self._hass.services.async_call(
CLIMATE_DOMAIN,
SERVICE_SET_HUMIDITY,
data,
)
async def set_swing_mode(self, swing_mode):
"""Set new target swing operation."""
_LOGGER.info("%s - Set fan mode: %s", self, swing_mode)
if not self.is_initialized:
return
data = {
ATTR_ENTITY_ID: self._entity_id,
"swing_mode": swing_mode,
}
await self._hass.services.async_call(
CLIMATE_DOMAIN,
SERVICE_SET_SWING_MODE,
data,
)
async def set_temperature(self, temperature, max_temp, min_temp):
"""Set the target temperature"""
if not self.is_initialized:
return
data = {
ATTR_ENTITY_ID: self._entity_id,
"temperature": temperature,
"target_temp_high": max_temp,
"target_temp_low": min_temp,
}
await self._hass.services.async_call(
CLIMATE_DOMAIN,
SERVICE_SET_TEMPERATURE,
data,
)
@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
@property
def hvac_mode(self) -> HVACMode | None:
"""Get the hvac mode of the underlying"""
if not self.is_initialized:
return None
return self._underlying_climate.hvac_mode
@property
def fan_mode(self) -> str | None:
"""Get the fan_mode of the underlying"""
if not self.is_initialized:
return None
return self._underlying_climate.fan_mode
@property
def swing_mode(self) -> str | None:
"""Get the swing_mode of the underlying"""
if not self.is_initialized:
return None
return self._underlying_climate.swing_mode
@property
def supported_features(self) -> ClimateEntityFeature:
"""Get the supported features of the climate"""
if not self.is_initialized:
return ClimateEntityFeature.TARGET_TEMPERATURE
return self._underlying_climate.supported_features
@property
def hvac_modes(self) -> list[HVACMode]:
"""Get the hvac_modes"""
if not self.is_initialized:
return []
return self._underlying_climate.hvac_modes
@property
def fan_modes(self) -> list[str]:
"""Get the fan_modes"""
if not self.is_initialized:
return []
return self._underlying_climate.fan_modes
@property
def swing_modes(self) -> list[str]:
"""Get the swing_modes"""
if not self.is_initialized:
return []
return self._underlying_climate.swing_modes
@property
def temperature_unit(self) -> str:
"""Get the temperature_unit"""
if not self.is_initialized:
return UnitOfTemperature.CELSIUS
return self._underlying_climate.temperature_unit
@property
def target_temperature_step(self) -> float:
"""Get the target_temperature_step"""
if not self.is_initialized:
return 1
return self._underlying_climate.target_temperature_step
@property
def target_temperature_high(self) -> float:
"""Get the target_temperature_high"""
if not self.is_initialized:
return 30
return self._underlying_climate.target_temperature_high
@property
def target_temperature_low(self) -> float:
"""Get the target_temperature_low"""
if not self.is_initialized:
return 15
return self._underlying_climate.target_temperature_low
@property
def is_aux_heat(self) -> bool:
"""Get the is_aux_heat"""
if not self.is_initialized:
return False
return self._underlying_climate.is_aux_heat
def turn_aux_heat_on(self) -> None:
"""Turn auxiliary heater on."""
if not self.is_initialized:
return None
return self._underlying_climate.turn_aux_heat_on()
def turn_aux_heat_off(self) -> None:
"""Turn auxiliary heater on."""
if not self.is_initialized:
return None
return self._underlying_climate.turn_aux_heat_off()
class UnderlyingValve(UnderlyingEntity):
"""Represent a underlying switch"""
_hvac_mode: HVACMode
# This is the percentage of opening int integer (from 0 to 100)
_percent_open: int
def __init__(
self,
hass: HomeAssistant,
thermostat: Any,
valve_entity_id: str
) -> None:
"""Initialize the underlying switch"""
super().__init__(
hass=hass,
thermostat=thermostat,
entity_type=UnderlyingEntityType.VALVE,
entity_id=valve_entity_id,
)
self._async_cancel_cycle = None
self._should_relaunch_control_heating = False
self._hvac_mode = None
self._percent_open = self._thermostat.valve_open_percent
async def send_percent_open(self):
""" Send the percent open to the underlying valve """
# This may fails if called after shutdown
try:
data = { ATTR_ENTITY_ID: self._entity_id, "value": self._percent_open }
domain = self._entity_id.split('.')[0]
await self._hass.services.async_call(
domain,
SERVICE_SET_VALUE,
data,
)
except ServiceNotFound as err:
_LOGGER.error(err)
async def turn_off(self):
"""Turn heater toggleable device off."""
_LOGGER.debug("%s - Stopping underlying valve entity %s", self, self._entity_id)
self._percent_open = 0
if self.is_device_active:
await self.send_percent_open()
async def turn_on(self):
"""Nothing to do for Valve because it cannot be turned off"""
async def set_hvac_mode(self, hvac_mode: HVACMode) -> bool:
"""Set the HVACmode. Returns true if something have change"""
if hvac_mode == HVACMode.OFF:
await self.turn_off()
if self._hvac_mode != hvac_mode:
self._hvac_mode = hvac_mode
return True
else:
return False
@property
def is_device_active(self):
"""If the toggleable device is currently active."""
try:
return self._percent_open > 0
# To test if real device is open but this is causing some side effect
# because the activation can be deferred -
# or float(self._hass.states.get(self._entity_id).state) > 0
except Exception: # pylint: disable=broad-exception-caught
return False
@overrides
async def start_cycle(
self,
hvac_mode: HVACMode,
_1,
_2,
_3,
force=False,
):
"""We use this function to change the on_percent"""
if force:
await self.send_percent_open()
def set_valve_open_percent(self, percent):
""" Update the valve open percent """
caped_val = self._thermostat.valve_open_percent
if self._percent_open == caped_val:
# No changes
return
self._percent_open = caped_val
# Send the new command to valve via a service call
_LOGGER.info("%s - Setting valve ouverture percent to %s", self, self._percent_open)
# Send the change to the valve, in background
self._hass.create_task(self.send_percent_open())
def remove_entity(self):
"""Remove the entity after stopping its cycle"""
self._cancel_cycle()

View File

@@ -0,0 +1,77 @@
""" The API of Versatile Thermostat"""
import logging
from homeassistant.core import HomeAssistant
from homeassistant.config_entries import ConfigEntry
from .const import DOMAIN, CONF_AUTO_REGULATION_EXPERT, CONF_SHORT_EMA_PARAMS
VTHERM_API_NAME = "vtherm_api"
_LOGGER = logging.getLogger(__name__)
class VersatileThermostatAPI(dict):
"""The VersatileThermostatAPI"""
_hass: HomeAssistant
# _entries: Dict(str, ConfigEntry)
@classmethod
def get_vtherm_api(cls, hass: HomeAssistant):
"""Get the eventual VTherm API class instance"""
ret = hass.data.get(DOMAIN).get(VTHERM_API_NAME)
if ret is None:
ret = VersatileThermostatAPI(hass)
hass.data[DOMAIN][VTHERM_API_NAME] = ret
return ret
def __init__(self, hass: HomeAssistant) -> None:
_LOGGER.debug("building a VersatileThermostatAPI")
super().__init__()
self._hass = hass
self._expert_params = None
self._short_ema_params = None
def add_entry(self, entry: ConfigEntry):
"""Add a new entry"""
_LOGGER.debug("Add the entry %s", entry.entry_id)
# self._entries[entry.entry_id] = entry
# Add the entry in hass.data
self._hass.data[DOMAIN][entry.entry_id] = entry
def remove_entry(self, entry: ConfigEntry):
"""Remove an entry"""
_LOGGER.debug("Remove the entry %s", entry.entry_id)
# self._entries.pop(entry.entry_id)
self._hass.data[DOMAIN].pop(entry.entry_id)
# If not more entries are preset, remove the API
if len(self) == 0:
_LOGGER.debug("No more entries-> Remove the API from DOMAIN")
self._hass.data.pop(DOMAIN)
def set_global_config(self, config):
"""Read the global configuration from configuration.yaml file"""
_LOGGER.info("Read global config from configuration.yaml")
self._expert_params = config.get(CONF_AUTO_REGULATION_EXPERT)
if self._expert_params:
_LOGGER.debug("We have found expert params %s", self._expert_params)
self._short_ema_params = config.get(CONF_SHORT_EMA_PARAMS)
if self._short_ema_params:
_LOGGER.debug("We have found short ema params %s", self._short_ema_params)
@property
def self_regulation_expert(self):
"""Get the self regulation params"""
return self._expert_params
@property
def short_ema_params(self):
"""Get the self regulation params"""
return self._short_ema_params
@property
def hass(self):
"""Get the HomeAssistant object"""
return self._hass

View File

@@ -3,5 +3,5 @@
"content_in_root": false,
"render_readme": true,
"hide_default_branch": false,
"homeassistant": "2022.2.0"
"homeassistant": "2023.11.2"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.2 KiB

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

BIN
images/new-icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 152 KiB

2
requirements_dev.txt Normal file
View File

@@ -0,0 +1,2 @@
homeassistant==2023.11.2
ffmpeg

6
requirements_test.txt Normal file
View File

@@ -0,0 +1,6 @@
# Warning: For automatic run of test in Gitlab CI, we must not include other things that pytest-homeassistant-custom-component
-r requirements_dev.txt
aiodiscover
ulid_transform
pytest-asyncio
pytest-homeassistant-custom-component

29
scripts/starts_ha.sh Executable file
View File

@@ -0,0 +1,29 @@
#!/bin/bash
set -e
set -x
cd "$(dirname "$0")/.."
pwd
# Create config dir if not present
if [[ ! -d "${PWD}/config" ]]; then
mkdir -p "${PWD}/config"
# Add defaults configuration
hass --config "${PWD}/config" --script ensure_config
fi
# Overwrite configuration.yaml if provided
if [ -f ${PWD}/.devcontainer/configuration.yaml ]; then
rm -f ${PWD}/config/configuration.yaml
ln -s ${PWD}/.devcontainer/configuration.yaml ${PWD}/config/configuration.yaml
fi
# Set the path to custom_components
## This let's us have the structure we want <root>/custom_components/integration_blueprint
## while at the same time have Home Assistant configuration inside <root>/config
## without resulting to symlinks.
export PYTHONPATH="${PYTHONPATH}:${PWD}/custom_components"
# Start Home Assistant
hass --config "${PWD}/config" --debug

3
setup.cfg Normal file
View File

@@ -0,0 +1,3 @@
[tool:pytest]
testpaths = tests
asyncio_mode = auto

1
tests/__init__.py Normal file
View File

@@ -0,0 +1 @@
""" To make this repo a module """

575
tests/commons.py Normal file
View File

@@ -0,0 +1,575 @@
# pylint: disable=wildcard-import, unused-wildcard-import, protected-access, unused-argument, line-too-long
""" Some common resources """
import asyncio
import logging
from unittest.mock import patch, MagicMock
import pytest # pylint: disable=unused-import
from homeassistant.core import HomeAssistant, Event, EVENT_STATE_CHANGED, State
from homeassistant.const import UnitOfTemperature, STATE_ON, STATE_OFF, ATTR_TEMPERATURE
from homeassistant.config_entries import ConfigEntryState
from homeassistant.helpers.entity import Entity
from homeassistant.components.climate import (
ClimateEntity,
DOMAIN as CLIMATE_DOMAIN,
HVACMode,
HVACAction,
ClimateEntityFeature,
)
from pytest_homeassistant_custom_component.common import MockConfigEntry
from custom_components.versatile_thermostat.base_thermostat import BaseThermostat
from custom_components.versatile_thermostat.const import * # pylint: disable=wildcard-import, unused-wildcard-import
from custom_components.versatile_thermostat.underlyings import * # pylint: disable=wildcard-import, unused-wildcard-import
from custom_components.versatile_thermostat.commons import get_tz, NowClass # pylint: disable=unused-import
from .const import ( # pylint: disable=unused-import
MOCK_TH_OVER_SWITCH_USER_CONFIG,
MOCK_TH_OVER_4SWITCH_USER_CONFIG,
MOCK_TH_OVER_CLIMATE_USER_CONFIG,
MOCK_TH_OVER_SWITCH_TYPE_CONFIG,
MOCK_TH_OVER_SWITCH_AC_TYPE_CONFIG,
MOCK_TH_OVER_4SWITCH_TYPE_CONFIG,
MOCK_TH_OVER_CLIMATE_TYPE_CONFIG,
MOCK_TH_OVER_CLIMATE_TYPE_AC_CONFIG,
MOCK_TH_OVER_CLIMATE_TYPE_NOT_REGULATED_CONFIG,
MOCK_TH_OVER_SWITCH_TPI_CONFIG,
MOCK_PRESETS_CONFIG,
MOCK_PRESETS_AC_CONFIG,
MOCK_WINDOW_CONFIG,
MOCK_MOTION_CONFIG,
MOCK_POWER_CONFIG,
MOCK_PRESENCE_CONFIG,
MOCK_PRESENCE_AC_CONFIG,
MOCK_ADVANCED_CONFIG,
# MOCK_DEFAULT_FEATURE_CONFIG,
PRESET_BOOST,
PRESET_COMFORT,
PRESET_NONE,
PRESET_ECO,
PRESET_ACTIVITY,
)
FULL_SWITCH_CONFIG = (
MOCK_TH_OVER_SWITCH_USER_CONFIG
| MOCK_TH_OVER_SWITCH_TYPE_CONFIG
| MOCK_TH_OVER_SWITCH_TPI_CONFIG
| MOCK_PRESETS_CONFIG
| MOCK_WINDOW_CONFIG
| MOCK_MOTION_CONFIG
| MOCK_POWER_CONFIG
| MOCK_PRESENCE_CONFIG
| MOCK_ADVANCED_CONFIG
)
FULL_SWITCH_AC_CONFIG = (
MOCK_TH_OVER_SWITCH_USER_CONFIG
| MOCK_TH_OVER_SWITCH_AC_TYPE_CONFIG
| MOCK_TH_OVER_SWITCH_TPI_CONFIG
| MOCK_PRESETS_AC_CONFIG
| MOCK_WINDOW_CONFIG
| MOCK_MOTION_CONFIG
| MOCK_POWER_CONFIG
| MOCK_PRESENCE_AC_CONFIG
| MOCK_ADVANCED_CONFIG
)
PARTIAL_CLIMATE_CONFIG = (
MOCK_TH_OVER_CLIMATE_USER_CONFIG
| MOCK_TH_OVER_CLIMATE_TYPE_CONFIG
| MOCK_PRESETS_CONFIG
| MOCK_ADVANCED_CONFIG
)
PARTIAL_CLIMATE_NOT_REGULATED_CONFIG = (
MOCK_TH_OVER_CLIMATE_USER_CONFIG
| MOCK_TH_OVER_CLIMATE_TYPE_NOT_REGULATED_CONFIG
| MOCK_PRESETS_CONFIG
| MOCK_ADVANCED_CONFIG
)
PARTIAL_CLIMATE_AC_CONFIG = (
MOCK_TH_OVER_CLIMATE_USER_CONFIG
| MOCK_TH_OVER_CLIMATE_TYPE_AC_CONFIG
| MOCK_PRESETS_CONFIG
| MOCK_ADVANCED_CONFIG
)
FULL_4SWITCH_CONFIG = (
MOCK_TH_OVER_4SWITCH_USER_CONFIG
| MOCK_TH_OVER_4SWITCH_TYPE_CONFIG
| MOCK_TH_OVER_SWITCH_TPI_CONFIG
| MOCK_PRESETS_CONFIG
| MOCK_WINDOW_CONFIG
| MOCK_MOTION_CONFIG
| MOCK_POWER_CONFIG
| MOCK_PRESENCE_CONFIG
| MOCK_ADVANCED_CONFIG
)
_LOGGER = logging.getLogger(__name__)
class MockClimate(ClimateEntity):
"""A Mock Climate class used for Underlying climate mode"""
def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos, hvac_mode:HVACMode = HVACMode.OFF, hvac_action:HVACAction = HVACAction.OFF) -> None: # pylint: disable=unused-argument
"""Initialize the thermostat."""
super().__init__()
self.hass = hass
self.platform = 'climate'
self.entity_id= self.platform+'.'+unique_id
self._attr_extra_state_attributes = {}
self._unique_id = unique_id
self._name = name
self._attr_hvac_action = HVACAction.OFF if hvac_mode == HVACMode.OFF else HVACAction.HEATING
self._attr_hvac_mode = hvac_mode
self._attr_hvac_modes = [HVACMode.OFF, HVACMode.COOL, HVACMode.HEAT]
self._attr_temperature_unit = UnitOfTemperature.CELSIUS
self._attr_target_temperature = 20
self._attr_current_temperature = 15
self._attr_hvac_action = hvac_action
def set_temperature(self, **kwargs):
""" Set the target temperature"""
temperature = kwargs.get(ATTR_TEMPERATURE)
self._attr_target_temperature = temperature
async def async_set_hvac_mode(self, hvac_mode):
""" The hvac mode"""
self._attr_hvac_mode = hvac_mode
@property
def hvac_action(self):
""" The hvac action of the mock climate"""
return self._attr_hvac_action
def set_hvac_action(self, hvac_action: HVACAction):
""" Set the HVACaction """
self._attr_hvac_action = hvac_action
class MockUnavailableClimate(ClimateEntity):
"""A Mock Climate class used for Underlying climate mode"""
def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None: # pylint: disable=unused-argument
"""Initialize the thermostat."""
super().__init__()
self._hass = hass
self._attr_extra_state_attributes = {}
self._unique_id = unique_id
self._name = name
self._attr_hvac_action = None
self._attr_hvac_mode = None
self._attr_hvac_modes = [HVACMode.OFF, HVACMode.COOL, HVACMode.HEAT]
self._attr_temperature_unit = UnitOfTemperature.CELSIUS
class MagicMockClimate(MagicMock):
"""A Magic Mock class for a underlying climate entity"""
@property
def temperature_unit(self): # pylint: disable=missing-function-docstring
return UnitOfTemperature.CELSIUS
@property
def hvac_mode(self): # pylint: disable=missing-function-docstring
return HVACMode.HEAT
@property
def hvac_action(self): # pylint: disable=missing-function-docstring
return HVACAction.IDLE
@property
def target_temperature(self): # pylint: disable=missing-function-docstring
return 15
@property
def current_temperature(self): # pylint: disable=missing-function-docstring
return 14
@property
def target_temperature_step( # pylint: disable=missing-function-docstring
self,
) -> float | None:
return 0.5
@property
def target_temperature_high( # pylint: disable=missing-function-docstring
self,
) -> float | None:
return 35
@property
def target_temperature_low( # pylint: disable=missing-function-docstring
self,
) -> float | None:
return 7
@property
def hvac_modes( # pylint: disable=missing-function-docstring
self,
) -> list[str] | None:
return [HVACMode.HEAT, HVACMode.OFF, HVACMode.COOL]
@property
def fan_modes( # pylint: disable=missing-function-docstring
self,
) -> list[str] | None:
return None
@property
def swing_modes( # pylint: disable=missing-function-docstring
self,
) -> list[str] | None:
return None
@property
def fan_mode(self) -> str | None: # pylint: disable=missing-function-docstring
return None
@property
def swing_mode(self) -> str | None: # pylint: disable=missing-function-docstring
return None
@property
def supported_features(self): # pylint: disable=missing-function-docstring
return ClimateEntityFeature.TARGET_TEMPERATURE
async def create_thermostat(
hass: HomeAssistant, entry: MockConfigEntry, entity_id: str
) -> 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
# def find_my_entity(entity_id) -> ClimateEntity:
# """Find my new entity"""
# component: EntityComponent[ClimateEntity] = hass.data[CLIMATE_DOMAIN]
# for entity in component.entities:
# if entity.entity_id == entity_id:
# return entity
return search_entity(hass, entity_id, CLIMATE_DOMAIN)
def search_entity(hass: HomeAssistant, entity_id, domain) -> Entity:
"""Search and return the entity in the domain"""
component = hass.data[domain]
for entity in component.entities:
if entity.entity_id == entity_id:
return entity
return None
async def send_temperature_change_event(
entity: BaseThermostat, new_temp, date, sleep=True
):
"""Sending a new temperature event simulating a change on temperature sensor"""
_LOGGER.info(
"------- Testu: sending send_temperature_change_event, new_temp=%.2f date=%s on %s",
new_temp,
date,
entity,
)
temp_event = Event(
EVENT_STATE_CHANGED,
{
"new_state": State(
entity_id=entity.entity_id,
state=new_temp,
last_changed=date,
last_updated=date,
)
},
)
await entity._async_temperature_changed(temp_event)
if sleep:
await asyncio.sleep(0.1)
async def send_ext_temperature_change_event(
entity: BaseThermostat, new_temp, date, sleep=True
):
"""Sending a new external temperature event simulating a change on temperature sensor"""
_LOGGER.info(
"------- Testu: sending send_temperature_change_event, new_temp=%.2f date=%s on %s",
new_temp,
date,
entity,
)
temp_event = Event(
EVENT_STATE_CHANGED,
{
"new_state": State(
entity_id=entity.entity_id,
state=new_temp,
last_changed=date,
last_updated=date,
)
},
)
await entity._async_ext_temperature_changed(temp_event)
if sleep:
await asyncio.sleep(0.1)
async def send_power_change_event(
entity: BaseThermostat, new_power, date, sleep=True
):
"""Sending a new power event simulating a change on power sensor"""
_LOGGER.info(
"------- Testu: sending send_temperature_change_event, new_power=%.2f date=%s on %s",
new_power,
date,
entity,
)
power_event = Event(
EVENT_STATE_CHANGED,
{
"new_state": State(
entity_id=entity.entity_id,
state=new_power,
last_changed=date,
last_updated=date,
)
},
)
await entity._async_power_changed(power_event)
if sleep:
await asyncio.sleep(0.1)
async def send_max_power_change_event(
entity: BaseThermostat, new_power_max, date, sleep=True
):
"""Sending a new power max event simulating a change on power max sensor"""
_LOGGER.info(
"------- Testu: sending send_temperature_change_event, new_power_max=%.2f date=%s on %s",
new_power_max,
date,
entity,
)
power_event = Event(
EVENT_STATE_CHANGED,
{
"new_state": State(
entity_id=entity.entity_id,
state=new_power_max,
last_changed=date,
last_updated=date,
)
},
)
await entity._async_max_power_changed(power_event)
if sleep:
await asyncio.sleep(0.1)
async def send_window_change_event(
entity: BaseThermostat, new_state: bool, old_state: bool, date, sleep=True
):
"""Sending a new window event simulating a change on the window state"""
_LOGGER.info(
"------- Testu: sending send_temperature_change_event, new_state=%s old_state=%s date=%s on %s",
new_state,
old_state,
date,
entity,
)
window_event = Event(
EVENT_STATE_CHANGED,
{
"new_state": State(
entity_id=entity.entity_id,
state=STATE_ON if new_state else STATE_OFF,
last_changed=date,
last_updated=date,
),
"old_state": State(
entity_id=entity.entity_id,
state=STATE_ON if old_state else STATE_OFF,
last_changed=date,
last_updated=date,
),
},
)
ret = await entity._async_windows_changed(window_event)
if sleep:
await asyncio.sleep(0.1)
return ret
async def send_motion_change_event(
entity: BaseThermostat, new_state: bool, old_state: bool, date, sleep=True
):
"""Sending a new motion event simulating a change on the window state"""
_LOGGER.info(
"------- Testu: sending send_temperature_change_event, new_state=%s old_state=%s date=%s on %s",
new_state,
old_state,
date,
entity,
)
motion_event = Event(
EVENT_STATE_CHANGED,
{
"new_state": State(
entity_id=entity.entity_id,
state=STATE_ON if new_state else STATE_OFF,
last_changed=date,
last_updated=date,
),
"old_state": State(
entity_id=entity.entity_id,
state=STATE_ON if old_state else STATE_OFF,
last_changed=date,
last_updated=date,
),
},
)
ret = await entity._async_motion_changed(motion_event)
if sleep:
await asyncio.sleep(0.1)
return ret
async def send_presence_change_event(
entity: BaseThermostat, new_state: bool, old_state: bool, date, sleep=True
):
"""Sending a new presence event simulating a change on the window state"""
_LOGGER.info(
"------- Testu: sending send_temperature_change_event, new_state=%s old_state=%s date=%s on %s",
new_state,
old_state,
date,
entity,
)
presence_event = Event(
EVENT_STATE_CHANGED,
{
"new_state": State(
entity_id=entity.entity_id,
state=STATE_ON if new_state else STATE_OFF,
last_changed=date,
last_updated=date,
),
"old_state": State(
entity_id=entity.entity_id,
state=STATE_ON if old_state else STATE_OFF,
last_changed=date,
last_updated=date,
),
},
)
ret = await entity._async_presence_changed(presence_event)
if sleep:
await asyncio.sleep(0.1)
return ret
async def send_climate_change_event(
entity: BaseThermostat,
new_hvac_mode: HVACMode,
old_hvac_mode: HVACMode,
new_hvac_action: HVACAction,
old_hvac_action: HVACAction,
date,
sleep=True,
):
"""Sending a new climate event simulating a change on the underlying climate state"""
_LOGGER.info(
"------- Testu: sending send_temperature_change_event, new_hvac_mode=%s old_hvac_mode=%s new_hvac_action=%s old_hvac_action=%s date=%s on %s",
new_hvac_mode,
old_hvac_mode,
new_hvac_action,
old_hvac_action,
date,
entity,
)
climate_event = Event(
EVENT_STATE_CHANGED,
{
"new_state": State(
entity_id=entity.entity_id,
state=new_hvac_mode,
attributes={"hvac_action": new_hvac_action},
last_changed=date,
last_updated=date,
),
"old_state": State(
entity_id=entity.entity_id,
state=old_hvac_mode,
attributes={"hvac_action": old_hvac_action},
last_changed=date,
last_updated=date,
),
},
)
ret = await entity._async_climate_changed(climate_event)
if sleep:
await asyncio.sleep(0.1)
return ret
async def send_climate_change_event_with_temperature(
entity: BaseThermostat,
new_hvac_mode: HVACMode,
old_hvac_mode: HVACMode,
new_hvac_action: HVACAction,
old_hvac_action: HVACAction,
date,
temperature,
sleep=True,
):
"""Sending a new climate event simulating a change on the underlying climate state"""
_LOGGER.info(
"------- Testu: sending send_temperature_change_event, new_hvac_mode=%s old_hvac_mode=%s new_hvac_action=%s old_hvac_action=%s date=%s temperature=%s on %s",
new_hvac_mode,
old_hvac_mode,
new_hvac_action,
old_hvac_action,
date,
temperature,
entity,
)
climate_event = Event(
EVENT_STATE_CHANGED,
{
"new_state": State(
entity_id=entity.entity_id,
state=new_hvac_mode,
attributes={"hvac_action": new_hvac_action, "temperature": temperature},
last_changed=date,
last_updated=date,
),
"old_state": State(
entity_id=entity.entity_id,
state=old_hvac_mode,
attributes={"hvac_action": old_hvac_action},
last_changed=date,
last_updated=date,
),
},
)
ret = await entity._async_climate_changed(climate_event)
if sleep:
await asyncio.sleep(0.1)
return ret
def cancel_switchs_cycles(entity: BaseThermostat):
"""This method will cancel all running cycle on all underlying switch entity"""
if entity.is_over_climate:
return
for under in entity._underlyings:
under._cancel_cycle()

110
tests/conftest.py Normal file
View File

@@ -0,0 +1,110 @@
"""Global fixtures for integration_blueprint integration."""
# pylint: disable=line-too-long
# Fixtures allow you to replace functions with a Mock object. You can perform
# many options via the Mock to reflect a particular behavior from the original
# function that you want to see without going through the function's actual logic.
# Fixtures can either be passed into tests as parameters, or if autouse=True, they
# will automatically be used across all tests.
#
# Fixtures that are defined in conftest.py are available across all tests. You can also
# define fixtures within a particular test file to scope them locally.
#
# pytest_homeassistant_custom_component provides some fixtures that are provided by
# Home Assistant core. You can find those fixture definitions here:
# https://github.com/MatthewFlamm/pytest-homeassistant-custom-component/blob/master/pytest_homeassistant_custom_component/common.py
#
# See here for more info: https://docs.pytest.org/en/latest/fixture.html (note that
# pytest includes fixtures OOB which you can use as defined on this page)
from unittest.mock import patch
import pytest
from homeassistant.core import StateMachine
from custom_components.versatile_thermostat.config_flow import (
VersatileThermostatBaseConfigFlow,
)
from custom_components.versatile_thermostat.base_thermostat import BaseThermostat
pytest_plugins = "pytest_homeassistant_custom_component" # pylint: disable=invalid-name
# This fixture enables loading custom integrations in all tests.
# Remove to enable selective use of this fixture
@pytest.fixture(autouse=True)
def auto_enable_custom_integrations(enable_custom_integrations): # pylint: disable=unused-argument
"""Enable all integration in tests"""
yield
# This fixture is used to prevent HomeAssistant from attempting to create and dismiss persistent
# notifications. These calls would fail without this fixture since the persistent_notification
# integration is never loaded during a test.
@pytest.fixture(name="skip_notifications", autouse=True)
def skip_notifications_fixture():
"""Skip notification calls."""
with patch("homeassistant.components.persistent_notification.async_create"), patch(
"homeassistant.components.persistent_notification.async_dismiss"
):
yield
@pytest.fixture(name="skip_turn_on_off_heater")
def skip_turn_on_off_heater():
"""Skip turning on and off the heater"""
with patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingEntity.turn_on"
), patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingEntity.turn_off"
):
yield
# This fixture is used to bypass the validate_input function in config_flow
# NOT USED Now (keeped for memory)
@pytest.fixture(name="skip_validate_input")
def skip_validate_input_fixture():
"""Skip the validate_input in config flow"""
with patch.object(VersatileThermostatBaseConfigFlow, "validate_input"):
yield
@pytest.fixture(name="skip_hass_states_get")
def skip_hass_states_get_fixture():
"""Skip the get state in HomeAssistant"""
with patch.object(StateMachine, "get"):
yield
@pytest.fixture(name="skip_control_heating")
def skip_control_heating_fixture():
"""Skip the control_heating of VersatileThermostat"""
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.async_control_heating"
):
yield
@pytest.fixture(name="skip_find_underlying_climate")
def skip_find_underlying_climate_fixture():
"""Skip the find_underlying_climate of VersatileThermostat"""
with patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate"
):
yield
@pytest.fixture(name="skip_hass_states_is_state")
def skip_hass_states_is_state_fixture():
"""Skip the is_state in HomeAssistant"""
with patch.object(StateMachine, "is_state", return_value=False):
yield
@pytest.fixture(name="skip_send_event")
def skip_send_event_fixture():
"""Skip the send_event in BaseThermostat"""
with patch.object(BaseThermostat, "send_event"):
yield

222
tests/const.py Normal file
View File

@@ -0,0 +1,222 @@
""" The commons const for all tests """
from homeassistant.components.climate.const import ( # pylint: disable=unused-import
PRESET_BOOST,
PRESET_COMFORT,
PRESET_ECO,
PRESET_NONE,
PRESET_ACTIVITY,
)
from custom_components.versatile_thermostat.const import (
CONF_NAME,
CONF_HEATER,
CONF_HEATER_2,
CONF_HEATER_3,
CONF_HEATER_4,
CONF_THERMOSTAT_CLIMATE,
CONF_THERMOSTAT_SWITCH,
CONF_THERMOSTAT_TYPE,
CONF_AC_MODE,
CONF_TEMP_SENSOR,
CONF_EXTERNAL_TEMP_SENSOR,
CONF_CYCLE_MIN,
CONF_TEMP_MAX,
CONF_TEMP_MIN,
CONF_PROP_FUNCTION,
PROPORTIONAL_FUNCTION_TPI,
CONF_TPI_COEF_INT,
CONF_TPI_COEF_EXT,
CONF_MINIMAL_ACTIVATION_DELAY,
CONF_SECURITY_DELAY_MIN,
CONF_SECURITY_MIN_ON_PERCENT,
CONF_SECURITY_DEFAULT_ON_PERCENT,
CONF_USE_WINDOW_FEATURE,
CONF_USE_MOTION_FEATURE,
CONF_USE_POWER_FEATURE,
CONF_USE_PRESENCE_FEATURE,
CONF_WINDOW_SENSOR,
CONF_WINDOW_DELAY,
CONF_WINDOW_AUTO_OPEN_THRESHOLD,
CONF_WINDOW_AUTO_CLOSE_THRESHOLD,
CONF_WINDOW_AUTO_MAX_DURATION,
CONF_MOTION_SENSOR,
CONF_MOTION_DELAY,
CONF_MOTION_OFF_DELAY,
CONF_MOTION_PRESET,
CONF_NO_MOTION_PRESET,
CONF_POWER_SENSOR,
CONF_MAX_POWER_SENSOR,
CONF_DEVICE_POWER,
CONF_PRESET_POWER,
CONF_PRESENCE_SENSOR,
PRESET_AWAY_SUFFIX,
CONF_CLIMATE,
CONF_AUTO_REGULATION_MODE,
CONF_AUTO_REGULATION_STRONG,
CONF_AUTO_REGULATION_NONE,
CONF_AUTO_REGULATION_DTEMP,
CONF_AUTO_REGULATION_PERIOD_MIN,
CONF_INVERSE_SWITCH
)
MOCK_TH_OVER_SWITCH_USER_CONFIG = {
CONF_NAME: "TheOverSwitchMockName",
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_SWITCH,
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
CONF_CYCLE_MIN: 5,
CONF_TEMP_MIN: 15,
CONF_TEMP_MAX: 30,
CONF_DEVICE_POWER: 1,
CONF_USE_WINDOW_FEATURE: True,
CONF_USE_MOTION_FEATURE: True,
CONF_USE_POWER_FEATURE: True,
CONF_USE_PRESENCE_FEATURE: True,
}
MOCK_TH_OVER_4SWITCH_USER_CONFIG = {
CONF_NAME: "TheOver4SwitchMockName",
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_SWITCH,
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
CONF_CYCLE_MIN: 8,
CONF_TEMP_MIN: 15,
CONF_TEMP_MAX: 30,
CONF_DEVICE_POWER: 1,
CONF_USE_WINDOW_FEATURE: True,
CONF_USE_MOTION_FEATURE: True,
CONF_USE_POWER_FEATURE: True,
CONF_USE_PRESENCE_FEATURE: True,
}
MOCK_TH_OVER_CLIMATE_USER_CONFIG = {
CONF_NAME: "TheOverClimateMockName",
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_CLIMATE,
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
CONF_CYCLE_MIN: 5,
CONF_TEMP_MIN: 15,
CONF_TEMP_MAX: 30,
CONF_DEVICE_POWER: 1
# Keep default values which are False
}
MOCK_TH_OVER_SWITCH_TYPE_CONFIG = {
CONF_HEATER: "switch.mock_switch",
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
CONF_AC_MODE: False,
CONF_INVERSE_SWITCH: False
}
MOCK_TH_OVER_SWITCH_AC_TYPE_CONFIG = {
CONF_HEATER: "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_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
CONF_AC_MODE: False,
CONF_INVERSE_SWITCH: False
}
MOCK_TH_OVER_SWITCH_TPI_CONFIG = {
CONF_TPI_COEF_INT: 0.3,
CONF_TPI_COEF_EXT: 0.1,
}
MOCK_TH_OVER_CLIMATE_TYPE_CONFIG = {
CONF_CLIMATE: "climate.mock_climate",
CONF_AC_MODE: False,
CONF_AUTO_REGULATION_MODE: CONF_AUTO_REGULATION_STRONG,
CONF_AUTO_REGULATION_DTEMP: 0.5,
CONF_AUTO_REGULATION_PERIOD_MIN: 2
}
MOCK_TH_OVER_CLIMATE_TYPE_NOT_REGULATED_CONFIG = {
CONF_CLIMATE: "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_AC_MODE: True,
CONF_AUTO_REGULATION_MODE: CONF_AUTO_REGULATION_STRONG,
CONF_AUTO_REGULATION_DTEMP: 0.5,
CONF_AUTO_REGULATION_PERIOD_MIN: 1
}
MOCK_PRESETS_CONFIG = {
PRESET_ECO + "_temp": 16,
PRESET_COMFORT + "_temp": 17,
PRESET_BOOST + "_temp": 18,
}
MOCK_PRESETS_AC_CONFIG = {
PRESET_ECO + "_temp": 17,
PRESET_COMFORT + "_temp": 19,
PRESET_BOOST + "_temp": 20,
PRESET_ECO + "_ac_temp": 25,
PRESET_COMFORT + "_ac_temp": 23,
PRESET_BOOST + "_ac_temp": 21,
}
MOCK_WINDOW_CONFIG = {
CONF_WINDOW_SENSOR: "binary_sensor.window_sensor",
CONF_WINDOW_DELAY: 10,
}
MOCK_WINDOW_AUTO_CONFIG = {
CONF_WINDOW_AUTO_OPEN_THRESHOLD: 1.0,
CONF_WINDOW_AUTO_CLOSE_THRESHOLD: 0.0,
CONF_WINDOW_AUTO_MAX_DURATION: 5.0,
}
MOCK_MOTION_CONFIG = {
CONF_MOTION_SENSOR: "input_boolean.motion_sensor",
CONF_MOTION_DELAY: 10,
CONF_MOTION_OFF_DELAY: 30,
CONF_MOTION_PRESET: PRESET_COMFORT,
CONF_NO_MOTION_PRESET: PRESET_ECO,
}
MOCK_POWER_CONFIG = {
CONF_POWER_SENSOR: "sensor.power_sensor",
CONF_MAX_POWER_SENSOR: "sensor.power_max_sensor",
CONF_PRESET_POWER: 10,
}
MOCK_PRESENCE_CONFIG = {
CONF_PRESENCE_SENSOR: "person.presence_sensor",
PRESET_ECO + PRESET_AWAY_SUFFIX + "_temp": 16,
PRESET_COMFORT + PRESET_AWAY_SUFFIX + "_temp": 17,
PRESET_BOOST + PRESET_AWAY_SUFFIX + "_temp": 18,
}
MOCK_PRESENCE_AC_CONFIG = {
CONF_PRESENCE_SENSOR: "person.presence_sensor",
PRESET_ECO + PRESET_AWAY_SUFFIX + "_temp": 16,
PRESET_COMFORT + PRESET_AWAY_SUFFIX + "_temp": 17,
PRESET_BOOST + PRESET_AWAY_SUFFIX + "_temp": 18,
PRESET_ECO + "_ac" + PRESET_AWAY_SUFFIX + "_temp": 27,
PRESET_COMFORT + "_ac" + PRESET_AWAY_SUFFIX + "_temp": 26,
PRESET_BOOST + "_ac" + PRESET_AWAY_SUFFIX + "_temp": 25,
}
MOCK_ADVANCED_CONFIG = {
CONF_MINIMAL_ACTIVATION_DELAY: 10,
CONF_SECURITY_DELAY_MIN: 5,
CONF_SECURITY_MIN_ON_PERCENT: 0.4,
CONF_SECURITY_DEFAULT_ON_PERCENT: 0.3,
}
MOCK_DEFAULT_FEATURE_CONFIG = {
CONF_USE_WINDOW_FEATURE: False,
CONF_USE_MOTION_FEATURE: False,
CONF_USE_POWER_FEATURE: False,
CONF_USE_PRESENCE_FEATURE: False,
}

View File

@@ -0,0 +1,374 @@
# 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 datetime import datetime, timedelta
from homeassistant.core import HomeAssistant
from homeassistant.components.climate import HVACAction, HVACMode
from homeassistant.config_entries import ConfigEntryState
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.components.climate import ClimateEntity, DOMAIN as CLIMATE_DOMAIN
from pytest_homeassistant_custom_component.common import MockConfigEntry
# from custom_components.versatile_thermostat.base_thermostat import BaseThermostat
from custom_components.versatile_thermostat.thermostat_climate import (
ThermostatOverClimate,
)
from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_over_climate_regulation(
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,
)
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=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,
):
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")
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.hvac_action is HVACAction.OFF
assert entity.hvac_mode is HVACMode.OFF
assert entity.target_temperature == entity.min_temp
assert entity.preset_modes == [
PRESET_NONE,
PRESET_ECO,
PRESET_COMFORT,
PRESET_BOOST,
]
assert entity.preset_mode is PRESET_NONE
# 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
assert entity.regulated_target_temp == entity.min_temp
await send_temperature_change_event(entity, 15, event_timestamp)
await send_ext_temperature_change_event(entity, 10, event_timestamp)
# set manual target temp (at now - 7) -> the regulation should occurs
event_timestamp = now - timedelta(minutes=7)
with patch(
"custom_components.versatile_thermostat.commons.NowClass.get_now",
return_value=event_timestamp,
):
await entity.async_set_temperature(temperature=18)
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 greater
assert entity.regulated_target_temp > entity.target_temperature
# In medium we could go up to +3 degre
# normally the calcul gives 18 + 2.2 but we round the result to the nearest 0.5 which is 2.0
assert entity.regulated_target_temp == 18 + 1.5
assert entity.hvac_action == HVACAction.HEATING
# change temperature so that the regulated temperature should slow down
event_timestamp = now - timedelta(minutes=5)
with patch(
"custom_components.versatile_thermostat.commons.NowClass.get_now",
return_value=event_timestamp,
):
await send_temperature_change_event(entity, 23, event_timestamp)
await send_ext_temperature_change_event(entity, 19, event_timestamp)
# the regulated temperature should be under
assert entity.regulated_target_temp < entity.target_temperature
assert (
entity.regulated_target_temp == 18 - 2
) # normally 0.6 but round_to_nearest gives 0.5
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_over_climate_regulation_ac_mode(
hass: HomeAssistant, skip_hass_states_is_state, skip_send_event
):
"""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_AC_CONFIG,
)
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=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,
):
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")
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.hvac_action is HVACAction.OFF
assert entity.hvac_mode is HVACMode.OFF
assert entity.target_temperature == entity.max_temp
assert entity.preset_modes == [
PRESET_NONE,
PRESET_ECO,
PRESET_COMFORT,
PRESET_BOOST,
]
assert entity.preset_mode is PRESET_NONE
# 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, 30, event_timestamp)
await send_ext_temperature_change_event(entity, 35, event_timestamp)
# set manual target temp
event_timestamp = now - timedelta(minutes=7)
with patch(
"custom_components.versatile_thermostat.commons.NowClass.get_now",
return_value=event_timestamp,
):
await entity.async_set_temperature(temperature=25)
fake_underlying_climate.set_hvac_action(
HVACAction.COOLING
) # simulate under heating
assert entity.hvac_action == HVACAction.COOLING
assert entity.preset_mode == PRESET_NONE # Manual mode
# the regulated temperature should be lower
assert entity.regulated_target_temp < entity.target_temperature
assert (
entity.regulated_target_temp == 25 - 2.5
) # In medium we could go up to -3 degre
assert entity.hvac_action == HVACAction.COOLING
# change temperature so that the regulated temperature should slow down
event_timestamp = now - timedelta(minutes=5)
with patch(
"custom_components.versatile_thermostat.commons.NowClass.get_now",
return_value=event_timestamp,
):
await send_temperature_change_event(entity, 26, event_timestamp)
await send_ext_temperature_change_event(entity, 35, event_timestamp)
# the regulated temperature should be under
assert entity.regulated_target_temp < entity.target_temperature
assert (
entity.regulated_target_temp == 25 - 1
) # +2.3 without round_to_nearest
# change temperature so that the regulated temperature should slow down
event_timestamp = now - timedelta(minutes=3)
with patch(
"custom_components.versatile_thermostat.commons.NowClass.get_now",
return_value=event_timestamp,
):
await send_temperature_change_event(entity, 18, event_timestamp)
await send_ext_temperature_change_event(entity, 25, event_timestamp)
# the regulated temperature should be greater
assert entity.regulated_target_temp > entity.target_temperature
assert (
entity.regulated_target_temp == 25 + 3
) # +0.4 without round_to_nearest
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_over_climate_regulation_limitations(
hass: HomeAssistant, skip_hass_states_is_state, skip_send_event
):
"""Test the limitations of the regulation of an over climate thermostat:
1. test the period_min parameter: do not send regulation event too frequently
2. test the dtemp parameter: do not send regulation event if offset temp is lower than dtemp
"""
entry = MockConfigEntry(
domain=DOMAIN,
title="TheOverClimateMockName",
unique_id="uniqueId",
# This is include a medium regulation, dtemp=0.5, period_min=2
data=PARTIAL_CLIMATE_CONFIG,
)
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 at t-20
# 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,
):
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")
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
# it is cold today
await send_temperature_change_event(entity, 15, event_timestamp)
await send_ext_temperature_change_event(entity, 10, event_timestamp)
# set manual target temp (at now - 19) -> the regulation should be ignored because too early
event_timestamp = now - timedelta(minutes=19)
with patch(
"custom_components.versatile_thermostat.commons.NowClass.get_now",
return_value=event_timestamp,
):
await entity.async_set_temperature(temperature=18)
fake_underlying_climate.set_hvac_action(
HVACAction.HEATING
) # simulate under heating
assert entity.hvac_action == HVACAction.HEATING
# the regulated temperature will change because when we set temp manually it is forced
assert entity.regulated_target_temp == 19.5
# set manual target temp (at now - 18) -> the regulation should be taken into account
event_timestamp = now - timedelta(minutes=18)
with patch(
"custom_components.versatile_thermostat.commons.NowClass.get_now",
return_value=event_timestamp,
):
await entity.async_set_temperature(temperature=17)
assert entity.regulated_target_temp > entity.target_temperature
assert (
entity.regulated_target_temp == 18 + 0
) # In strong we could go up to +3 degre. 0.7 without round_to_nearest
old_regulated_temp = entity.regulated_target_temp
# change temperature so that dtemp < 0.5 and time is > period_min (+ 3min)
event_timestamp = now - timedelta(minutes=15)
with patch(
"custom_components.versatile_thermostat.commons.NowClass.get_now",
return_value=event_timestamp,
):
await send_temperature_change_event(entity, 16, event_timestamp)
await send_ext_temperature_change_event(entity, 10, event_timestamp)
# the regulated temperature should be under
assert entity.regulated_target_temp <= old_regulated_temp
# change temperature so that dtemp > 0.5 and time is > period_min (+ 3min)
event_timestamp = now - timedelta(minutes=12)
with patch(
"custom_components.versatile_thermostat.commons.NowClass.get_now",
return_value=event_timestamp,
):
await send_temperature_change_event(entity, 15, event_timestamp)
await send_ext_temperature_change_event(entity, 12, event_timestamp)
# the regulated should have been done
assert entity.regulated_target_temp != old_regulated_temp
assert entity.regulated_target_temp >= entity.target_temperature
assert (
entity.regulated_target_temp == 17 + 1.5
) # 0.7 without round_to_nearest

View File

@@ -0,0 +1,517 @@
# pylint: disable=wildcard-import, unused-wildcard-import, unused-argument, line-too-long
""" Test the normal start of a Thermostat """
from unittest.mock import patch
from datetime import timedelta, datetime
from homeassistant.core import HomeAssistant
from homeassistant.components.climate import HVACMode
from homeassistant.components.binary_sensor import BinarySensorDeviceClass
from pytest_homeassistant_custom_component.common import MockConfigEntry
from custom_components.versatile_thermostat.base_thermostat import BaseThermostat
from custom_components.versatile_thermostat.binary_sensor import (
SecurityBinarySensor,
OverpoweringBinarySensor,
WindowBinarySensor,
MotionBinarySensor,
PresenceBinarySensor,
)
from .commons import *
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_security_binary_sensors(
hass: HomeAssistant,
skip_hass_states_is_state,
skip_turn_on_off_heater,
skip_send_event,
): # pylint: disable=unused-argument
"""Test the security binary sensors in thermostat type"""
entry = MockConfigEntry(
domain=DOMAIN,
title="TheOverSwitchMockName",
unique_id="uniqueId",
data={
CONF_NAME: "TheOverSwitchMockName",
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_SWITCH,
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
CONF_CYCLE_MIN: 5,
CONF_TEMP_MIN: 15,
CONF_TEMP_MAX: 30,
"eco_temp": 17,
"comfort_temp": 18,
"boost_temp": 19,
CONF_USE_WINDOW_FEATURE: False,
CONF_USE_MOTION_FEATURE: False,
CONF_USE_POWER_FEATURE: False,
CONF_USE_PRESENCE_FEATURE: False,
CONF_HEATER: "switch.mock_switch",
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
CONF_TPI_COEF_INT: 0.3,
CONF_TPI_COEF_EXT: 0.01,
CONF_MINIMAL_ACTIVATION_DELAY: 30,
CONF_SECURITY_DELAY_MIN: 5,
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
},
)
entity: BaseThermostat = await create_thermostat (
hass, entry, "climate.theoverswitchmockname"
)
assert entity
security_binary_sensor: SecurityBinarySensor = search_entity(
hass, "binary_sensor.theoverswitchmockname_security_state", "binary_sensor"
)
assert security_binary_sensor
now: datetime = datetime.now(tz=get_tz(hass))
# Security should be disabled
await entity.async_set_preset_mode(PRESET_COMFORT)
await entity.async_set_hvac_mode(HVACMode.HEAT)
assert security_binary_sensor.state == STATE_OFF
assert security_binary_sensor.device_class == BinarySensorDeviceClass.SAFETY
# Set temperature in the past
event_timestamp = now - timedelta(minutes=6)
# set temperature to 15 so that on_percent will be > security_min_on_percent (0.2)
await send_temperature_change_event(entity, 15, event_timestamp)
assert entity.security_state is True
# Simulate the event reception
await security_binary_sensor.async_my_climate_changed()
assert security_binary_sensor.state == STATE_ON
# set temperature now
await send_temperature_change_event(entity, 15, now)
assert entity.security_state is False
# Simulate the event reception
await security_binary_sensor.async_my_climate_changed()
assert security_binary_sensor.state == STATE_OFF
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_overpowering_binary_sensors(
hass: HomeAssistant,
skip_hass_states_is_state,
skip_turn_on_off_heater,
skip_send_event,
):
"""Test the overpowering binary sensors in thermostat type"""
entry = MockConfigEntry(
domain=DOMAIN,
title="TheOverSwitchMockName",
unique_id="uniqueId",
data={
CONF_NAME: "TheOverSwitchMockName",
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_SWITCH,
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
CONF_CYCLE_MIN: 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: BaseThermostat = await create_thermostat(
hass, entry, "climate.theoverswitchmockname"
)
assert entity
overpowering_binary_sensor: OverpoweringBinarySensor = search_entity(
hass, "binary_sensor.theoverswitchmockname_overpowering_state", "binary_sensor"
)
assert overpowering_binary_sensor
now: datetime = datetime.now(tz=get_tz(hass))
# Overpowering should be not set because poer have not been received
await entity.async_set_preset_mode(PRESET_COMFORT)
await entity.async_set_hvac_mode(HVACMode.HEAT)
await send_temperature_change_event(entity, 15, now)
assert await entity.check_overpowering() is False
assert entity.overpowering_state is None
await overpowering_binary_sensor.async_my_climate_changed()
assert overpowering_binary_sensor.state is STATE_OFF
assert overpowering_binary_sensor.device_class == BinarySensorDeviceClass.POWER
await send_power_change_event(entity, 100, now)
await send_max_power_change_event(entity, 150, now)
assert await entity.check_overpowering() is True
assert entity.overpowering_state is True
# Simulate the event reception
await overpowering_binary_sensor.async_my_climate_changed()
assert overpowering_binary_sensor.state == STATE_ON
# set max power to a low value
await send_max_power_change_event(entity, 201, now)
assert await entity.check_overpowering() is False
assert entity.overpowering_state is False
# Simulate the event reception
await overpowering_binary_sensor.async_my_climate_changed()
assert overpowering_binary_sensor.state == STATE_OFF
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_window_binary_sensors(
hass: HomeAssistant,
skip_hass_states_is_state,
skip_turn_on_off_heater,
skip_send_event,
):
"""Test the window binary sensors in thermostat type"""
entry = MockConfigEntry(
domain=DOMAIN,
title="TheOverSwitchMockName",
unique_id="uniqueId",
data={
CONF_NAME: "TheOverSwitchMockName",
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_SWITCH,
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
CONF_CYCLE_MIN: 5,
CONF_TEMP_MIN: 15,
CONF_TEMP_MAX: 30,
"eco_temp": 17,
"comfort_temp": 18,
"boost_temp": 19,
CONF_USE_WINDOW_FEATURE: True,
CONF_USE_MOTION_FEATURE: False,
CONF_USE_POWER_FEATURE: False,
CONF_USE_PRESENCE_FEATURE: False,
CONF_HEATER: "switch.mock_switch",
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
CONF_TPI_COEF_INT: 0.3,
CONF_TPI_COEF_EXT: 0.01,
CONF_MINIMAL_ACTIVATION_DELAY: 30,
CONF_SECURITY_DELAY_MIN: 5,
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
CONF_WINDOW_SENSOR: "binary_sensor.mock_window_sensor",
CONF_WINDOW_DELAY: 0, # important to not been obliged to wait
},
)
entity: BaseThermostat = await create_thermostat(
hass, entry, "climate.theoverswitchmockname"
)
assert entity
window_binary_sensor: WindowBinarySensor = search_entity(
hass, "binary_sensor.theoverswitchmockname_window_state", "binary_sensor"
)
assert window_binary_sensor
now: datetime = datetime.now(tz=get_tz(hass))
# Overpowering should be not set because poer have not been received
await entity.async_set_preset_mode(PRESET_COMFORT)
await entity.async_set_hvac_mode(HVACMode.HEAT)
await send_temperature_change_event(entity, 15, now)
assert entity.window_state is STATE_OFF
await window_binary_sensor.async_my_climate_changed()
assert window_binary_sensor.state is STATE_OFF
assert window_binary_sensor.device_class == BinarySensorDeviceClass.WINDOW
# Open the window
with patch("homeassistant.helpers.condition.state", return_value=True):
try_window_condition = await send_window_change_event(entity, True, False, now)
# simulate the call to try_window_condition
await try_window_condition(None)
assert entity.window_state is STATE_ON
# Simulate the event reception
await window_binary_sensor.async_my_climate_changed()
assert window_binary_sensor.state == STATE_ON
# close the window
with patch("homeassistant.helpers.condition.state", return_value=True):
try_window_condition = await send_window_change_event(entity, False, True, now)
# simulate the call to try_window_condition
await try_window_condition(None)
assert entity.window_state is STATE_OFF
# Simulate the event reception
await window_binary_sensor.async_my_climate_changed()
assert window_binary_sensor.state == STATE_OFF
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_motion_binary_sensors(
hass: HomeAssistant,
skip_hass_states_is_state,
skip_turn_on_off_heater,
skip_send_event,
):
"""Test the motion binary sensors in thermostat type"""
entry = MockConfigEntry(
domain=DOMAIN,
title="TheOverSwitchMockName",
unique_id="uniqueId",
data={
CONF_NAME: "TheOverSwitchMockName",
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_SWITCH,
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
CONF_CYCLE_MIN: 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: 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: 0, # important to not been obliged to wait
},
)
entity: BaseThermostat = await create_thermostat(
hass, entry, "climate.theoverswitchmockname"
)
assert entity
motion_binary_sensor: MotionBinarySensor = search_entity(
hass, "binary_sensor.theoverswitchmockname_motion_state", "binary_sensor"
)
assert motion_binary_sensor
now: datetime = datetime.now(tz=get_tz(hass))
# Overpowering should be not set because poer have not been received
await entity.async_set_preset_mode(PRESET_COMFORT)
await entity.async_set_hvac_mode(HVACMode.HEAT)
await send_temperature_change_event(entity, 15, now)
assert entity.motion_state is None
await motion_binary_sensor.async_my_climate_changed()
assert motion_binary_sensor.state is STATE_OFF
assert motion_binary_sensor.device_class == BinarySensorDeviceClass.MOTION
# Detect motion
with patch("homeassistant.helpers.condition.state", return_value=True):
try_motion_condition = await send_motion_change_event(entity, True, False, now)
# simulate the call to try_window_condition
await try_motion_condition(None)
assert entity.motion_state is STATE_ON
# Simulate the event reception
await motion_binary_sensor.async_my_climate_changed()
assert motion_binary_sensor.state == STATE_ON
# Undetect motion
with patch("homeassistant.helpers.condition.state", return_value=True):
try_motion_condition = await send_motion_change_event(entity, False, True, now)
# simulate the call to try_motion_condition
await try_motion_condition(None)
assert entity.motion_state is STATE_OFF
# Simulate the event reception
await motion_binary_sensor.async_my_climate_changed()
assert motion_binary_sensor.state == STATE_OFF
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_presence_binary_sensors(
hass: HomeAssistant,
skip_hass_states_is_state,
skip_turn_on_off_heater,
skip_send_event,
):
"""Test the presence binary sensors in thermostat type"""
entry = MockConfigEntry(
domain=DOMAIN,
title="TheOverSwitchMockName",
unique_id="uniqueId",
data={
CONF_NAME: "TheOverSwitchMockName",
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_SWITCH,
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
CONF_CYCLE_MIN: 5,
CONF_TEMP_MIN: 15,
CONF_TEMP_MAX: 30,
"eco_temp": 17,
"comfort_temp": 18,
"boost_temp": 19,
"eco_away_temp": 12,
"comfort_away_temp": 13,
"boost_away_temp": 14,
CONF_USE_WINDOW_FEATURE: False,
CONF_USE_MOTION_FEATURE: False,
CONF_USE_POWER_FEATURE: False,
CONF_USE_PRESENCE_FEATURE: True,
CONF_HEATER: "switch.mock_switch",
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
CONF_TPI_COEF_INT: 0.3,
CONF_TPI_COEF_EXT: 0.01,
CONF_MINIMAL_ACTIVATION_DELAY: 30,
CONF_SECURITY_DELAY_MIN: 5,
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
CONF_PRESENCE_SENSOR: "binary_sensor.mock_presence_sensor",
},
)
entity: BaseThermostat = await create_thermostat(
hass, entry, "climate.theoverswitchmockname"
)
assert entity
presence_binary_sensor: PresenceBinarySensor = search_entity(
hass, "binary_sensor.theoverswitchmockname_presence_state", "binary_sensor"
)
assert presence_binary_sensor
now: datetime = datetime.now(tz=get_tz(hass))
# Overpowering should be not set because poer have not been received
await entity.async_set_preset_mode(PRESET_COMFORT)
await entity.async_set_hvac_mode(HVACMode.HEAT)
await send_temperature_change_event(entity, 15, now)
assert entity.presence_state is None
await presence_binary_sensor.async_my_climate_changed()
assert presence_binary_sensor.state is STATE_OFF
assert presence_binary_sensor.device_class == BinarySensorDeviceClass.PRESENCE
# Detect motion
await send_presence_change_event(entity, True, False, now)
assert entity.presence_state is STATE_ON
# Simulate the event reception
await presence_binary_sensor.async_my_climate_changed()
assert presence_binary_sensor.state == STATE_ON
# Undetect motion
await send_presence_change_event(entity, False, True, now)
assert entity.presence_state is STATE_OFF
# Simulate the event reception
await presence_binary_sensor.async_my_climate_changed()
assert presence_binary_sensor.state == STATE_OFF
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_binary_sensors_over_climate_minimal(
hass: HomeAssistant,
skip_hass_states_is_state,
skip_turn_on_off_heater,
skip_send_event,
):
"""Test the binary sensors with thermostat over climate type"""
the_mock_underlying = MagicMockClimate()
with patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
return_value=the_mock_underlying,
):
entry = MockConfigEntry(
domain=DOMAIN,
title="TheOverClimateMockName",
unique_id="uniqueId",
data={
CONF_NAME: "TheOverClimateMockName",
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_CLIMATE,
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
CONF_CYCLE_MIN: 5,
CONF_TEMP_MIN: 15,
CONF_TEMP_MAX: 30,
"eco_temp": 17,
"comfort_temp": 18,
"boost_temp": 19,
CONF_USE_WINDOW_FEATURE: False,
CONF_USE_MOTION_FEATURE: False,
CONF_USE_POWER_FEATURE: 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
assert entity.is_over_climate
security_binary_sensor: SecurityBinarySensor = search_entity(
hass, "binary_sensor.theoverclimatemockname_security_state", "binary_sensor"
)
assert security_binary_sensor is not None
overpowering_binary_sensor: OverpoweringBinarySensor = search_entity(
hass, "binary_sensor.theoverclimatemockname_overpowering_state", "binary_sensor"
)
assert overpowering_binary_sensor is None
window_binary_sensor: WindowBinarySensor = search_entity(
hass, "binary_sensor.theoverclimatemockname_window_state", "binary_sensor"
)
assert window_binary_sensor is None
motion_binary_sensor: MotionBinarySensor = search_entity(
hass, "binary_sensor.theoverclimatemockname_motion_state", "binary_sensor"
)
assert motion_binary_sensor is None
presence_binary_sensor: PresenceBinarySensor = search_entity(
hass, "binary_sensor.theoverclimatemockname_presence_state", "binary_sensor"
)
assert presence_binary_sensor is None

545
tests/test_bugs.py Normal file
View File

@@ -0,0 +1,545 @@
# pylint: disable=wildcard-import, unused-wildcard-import, protected-access, unused-argument, line-too-long
""" Test the Window management """
from unittest.mock import patch, call
from datetime import datetime, timedelta
import logging
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(
hass: HomeAssistant,
skip_hass_states_is_state,
skip_turn_on_off_heater,
skip_send_event,
):
"""Test that it should be possible to set the security_default_on_percent to 0"""
entry = MockConfigEntry(
domain=DOMAIN,
title="TheOverSwitchMockName",
unique_id="uniqueId",
data={
CONF_NAME: "TheOverSwitchMockName",
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_SWITCH,
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
CONF_CYCLE_MIN: 5,
CONF_TEMP_MIN: 15,
CONF_TEMP_MAX: 30,
"eco_temp": 17,
"comfort_temp": 18,
"boost_temp": 19,
CONF_USE_WINDOW_FEATURE: False,
CONF_USE_MOTION_FEATURE: False,
CONF_USE_POWER_FEATURE: False,
CONF_USE_PRESENCE_FEATURE: False,
CONF_HEATER: "switch.mock_switch",
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
CONF_TPI_COEF_INT: 0.3,
CONF_TPI_COEF_EXT: 0.01,
CONF_MINIMAL_ACTIVATION_DELAY: 30,
CONF_SECURITY_DELAY_MIN: 5,
CONF_SECURITY_MIN_ON_PERCENT: 0.0, # !! here
CONF_SECURITY_DEFAULT_ON_PERCENT: 0.0, # !! here
CONF_DEVICE_POWER: 200,
},
)
entity: BaseThermostat = await create_thermostat(
hass, entry, "climate.theoverswitchmockname"
)
assert entity
assert entity._security_min_on_percent == 0
assert entity._security_default_on_percent == 0
# Waiting for answer in https://github.com/jmcollin78/versatile_thermostat/issues/64
# Repro case not evident
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_bug_64(
hass: HomeAssistant,
skip_hass_states_is_state,
skip_turn_on_off_heater,
skip_send_event,
):
"""Test that it should be possible to set the security_default_on_percent to 0"""
entry = MockConfigEntry(
domain=DOMAIN,
title="TheOverSwitchMockName",
unique_id="uniqueId",
data={
CONF_NAME: "TheOverSwitchMockName",
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_SWITCH,
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
CONF_CYCLE_MIN: 5,
CONF_TEMP_MIN: 15,
CONF_TEMP_MAX: 30,
"eco_temp": 17,
"comfort_temp": 18,
"boost_temp": 19,
CONF_USE_WINDOW_FEATURE: False,
CONF_USE_MOTION_FEATURE: False,
CONF_USE_POWER_FEATURE: False,
CONF_USE_PRESENCE_FEATURE: False,
CONF_HEATER: "switch.mock_switch",
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
CONF_TPI_COEF_INT: 0.3,
CONF_TPI_COEF_EXT: 0.01,
CONF_MINIMAL_ACTIVATION_DELAY: 30,
CONF_SECURITY_DELAY_MIN: 5,
CONF_SECURITY_MIN_ON_PERCENT: 0.5,
CONF_SECURITY_DEFAULT_ON_PERCENT: 0.1, # !! here
CONF_DEVICE_POWER: 200,
},
)
entity: BaseThermostat = await create_thermostat(
hass, entry, "climate.theoverswitchmockname"
)
assert entity
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_bug_66(
hass: HomeAssistant,
skip_hass_states_is_state,
skip_turn_on_off_heater,
skip_send_event,
):
"""Test that it should be possible to open/close the window rapidly without side effect"""
tz = get_tz(hass) # pylint: disable=invalid-name
now: datetime = datetime.now(tz=tz)
entry = MockConfigEntry(
domain=DOMAIN,
title="TheOverSwitchMockName",
unique_id="uniqueId",
data={
CONF_NAME: "TheOverSwitchMockName",
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_SWITCH,
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
CONF_CYCLE_MIN: 5,
CONF_TEMP_MIN: 15,
CONF_TEMP_MAX: 30,
"eco_temp": 17,
"comfort_temp": 18,
"boost_temp": 19,
CONF_USE_WINDOW_FEATURE: True,
CONF_USE_MOTION_FEATURE: False,
CONF_USE_POWER_FEATURE: False,
CONF_USE_PRESENCE_FEATURE: False,
CONF_HEATER: "switch.mock_switch",
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
CONF_TPI_COEF_INT: 0.3,
CONF_TPI_COEF_EXT: 0.01,
CONF_MINIMAL_ACTIVATION_DELAY: 30,
CONF_SECURITY_DELAY_MIN: 5,
CONF_SECURITY_MIN_ON_PERCENT: 0.5,
CONF_SECURITY_DEFAULT_ON_PERCENT: 0.1, # !! here
CONF_DEVICE_POWER: 200,
CONF_WINDOW_SENSOR: "binary_sensor.mock_window_sensor",
CONF_WINDOW_DELAY: 0, # important to not been obliged to wait
},
)
entity: 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 security mode"""
tz = get_tz(hass) # pylint: disable=invalid-name
now: datetime = datetime.now(tz=tz)
entry = MockConfigEntry(
domain=DOMAIN,
title="TheOverClimateMockName",
unique_id="uniqueId",
data=PARTIAL_CLIMATE_CONFIG, # 5 minutes security delay
)
fake_underlying_climate = MockUnavailableClimate(hass, "mockUniqueId", "MockClimateName", {})
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
return_value=fake_underlying_climate,
) as mock_find_climate:
entry.add_to_hass(hass)
await hass.config_entries.async_setup(entry.entry_id)
assert entry.state is ConfigEntryState.LOADED
def find_my_entity(entity_id) -> ClimateEntity:
"""Find my new entity"""
component: EntityComponent[ClimateEntity] = hass.data[CLIMATE_DOMAIN]
for entity in component.entities:
if entity.entity_id == entity_id:
return entity
entity = find_my_entity("climate.theoverclimatemockname")
assert entity
assert entity.name == "TheOverClimateMockName"
assert entity.is_over_climate is True
# assert entity.hvac_action is HVACAction.OFF
assert entity.hvac_mode is HVACMode.OFF
# assert entity.hvac_mode is None
assert entity.target_temperature == entity.min_temp
assert entity.preset_modes == [
PRESET_NONE,
PRESET_ECO,
PRESET_COMFORT,
PRESET_BOOST,
]
assert entity.preset_mode is PRESET_NONE
assert entity._security_state is False
# should have been called with EventType.PRESET_EVENT and EventType.HVAC_MODE_EVENT
assert mock_send_event.call_count == 2
mock_send_event.assert_has_calls(
[
call.send_event(EventType.PRESET_EVENT, {"preset": PRESET_NONE}),
call.send_event(
EventType.HVAC_MODE_EVENT,
{"hvac_mode": HVACMode.OFF},
),
]
)
assert mock_find_climate.call_count == 1
assert mock_find_climate.mock_calls[0] == call()
mock_find_climate.assert_has_calls([call.find_underlying_entity()])
# Force security mode
assert entity._last_ext_temperature_mesure is not None
assert entity._last_temperature_mesure is not None
assert (entity._last_temperature_mesure.astimezone(tz) - now).total_seconds() < 1
assert (
entity._last_ext_temperature_mesure.astimezone(tz) - now
).total_seconds() < 1
# Tries to turns on the Thermostat
await entity.async_set_hvac_mode(HVACMode.HEAT)
assert entity.hvac_mode == HVACMode.HEAT
# 2. activate security feature when date is expired
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
):
event_timestamp = now - timedelta(minutes=6)
# set temperature to 15 so that on_percent will be > security_min_on_percent (0.2)
await send_temperature_change_event(entity, 15, event_timestamp)
# Should stay False
assert entity.security_state is False
assert entity.preset_mode == 'none'
assert entity._saved_preset_mode == 'none'
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_bug_101(
hass: HomeAssistant,
skip_hass_states_is_state,
skip_turn_on_off_heater,
skip_send_event,
):
"""Test that when a underlying climate target temp is changed, the VTherm change its own temperature target and switch to manual"""
tz = get_tz(hass) # pylint: disable=invalid-name
now: datetime = datetime.now(tz=tz)
entry = MockConfigEntry(
domain=DOMAIN,
title="TheOverClimateMockName",
unique_id="uniqueId",
data=PARTIAL_CLIMATE_NOT_REGULATED_CONFIG, # 5 minutes security delay
)
# Underlying is in HEAT mode but should be shutdown at startup
fake_underlying_climate = MockClimate(hass, "mockUniqueId", "MockClimateName", {}, HVACMode.HEAT, HVACAction.HEATING)
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
return_value=fake_underlying_climate,
) as mock_find_climate, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.set_hvac_mode"
) as mock_underlying_set_hvac_mode:
entry.add_to_hass(hass)
await hass.config_entries.async_setup(entry.entry_id)
assert entry.state is ConfigEntryState.LOADED
def find_my_entity(entity_id) -> ClimateEntity:
"""Find my new entity"""
component: EntityComponent[ClimateEntity] = hass.data[CLIMATE_DOMAIN]
for entity in component.entities:
if entity.entity_id == entity_id:
return entity
entity = find_my_entity("climate.theoverclimatemockname")
assert entity
assert entity.name == "TheOverClimateMockName"
assert entity.is_over_climate is True
assert entity.hvac_mode is HVACMode.OFF
# because in MockClimate HVACAction is HEATING if hvac_mode is not set
assert entity.hvac_action is HVACAction.HEATING
# Underlying should have been shutdown
assert mock_underlying_set_hvac_mode.call_count == 1
mock_underlying_set_hvac_mode.assert_has_calls(
[
call.set_hvac_mode(HVACMode.OFF),
]
)
assert entity.target_temperature == entity.min_temp
assert entity.preset_mode is PRESET_NONE
# should have been called with EventType.PRESET_EVENT and EventType.HVAC_MODE_EVENT
assert mock_send_event.call_count == 2
mock_send_event.assert_has_calls(
[
call.send_event(EventType.PRESET_EVENT, {"preset": PRESET_NONE}),
call.send_event(
EventType.HVAC_MODE_EVENT,
{"hvac_mode": HVACMode.OFF},
),
]
)
assert mock_find_climate.call_count == 1
assert mock_find_climate.mock_calls[0] == call()
mock_find_climate.assert_has_calls([call.find_underlying_entity()])
# Force preset mode
await entity.async_set_hvac_mode(HVACMode.HEAT)
assert entity.hvac_mode == HVACMode.HEAT
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

450
tests/test_config_flow.py Normal file
View File

@@ -0,0 +1,450 @@
# pylint: disable=unused-argument, line-too-long
""" Test the Versatile Thermostat config flow """
from homeassistant import data_entry_flow
from homeassistant.core import HomeAssistant
from homeassistant.config_entries import SOURCE_USER, ConfigEntry
from custom_components.versatile_thermostat.const import DOMAIN
from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import
from .const import * # pylint: disable=wildcard-import, unused-wildcard-import
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_show_form(hass: HomeAssistant) -> None:
"""Test that the form is served with no input"""
# Init the API
# hass.data["custom_components"] = None
# loader.async_get_custom_components(hass)
# BaseThermostatAPI(hass)
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == SOURCE_USER
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
# Disable this test which don't work anymore (kill the pytest !)
@pytest.mark.skip
async def test_user_config_flow_over_switch(
hass: HomeAssistant, skip_hass_states_get
): # pylint: disable=unused-argument
"""Test the config flow with all thermostat_over_switch features"""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == SOURCE_USER
result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input=MOCK_TH_OVER_SWITCH_USER_CONFIG
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "type"
assert result["errors"] == {}
result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input=MOCK_TH_OVER_SWITCH_TYPE_CONFIG
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "tpi"
assert result["errors"] == {}
result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input=MOCK_TH_OVER_SWITCH_TPI_CONFIG
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "presets"
assert result["errors"] == {}
result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input=MOCK_PRESETS_CONFIG
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "window"
assert result["errors"] == {}
result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input=MOCK_WINDOW_CONFIG
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "motion"
assert result["errors"] == {}
result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input=MOCK_MOTION_CONFIG
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "power"
assert result["errors"] == {}
result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input=MOCK_POWER_CONFIG
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "presence"
assert result["errors"] == {}
result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input=MOCK_PRESENCE_CONFIG
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "advanced"
assert result["errors"] == {}
result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input=MOCK_ADVANCED_CONFIG
)
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
assert (
result["data"]
== MOCK_TH_OVER_SWITCH_USER_CONFIG
| MOCK_TH_OVER_SWITCH_TYPE_CONFIG
| MOCK_TH_OVER_SWITCH_TPI_CONFIG
| MOCK_PRESETS_CONFIG
| MOCK_WINDOW_CONFIG
| MOCK_MOTION_CONFIG
| MOCK_POWER_CONFIG
| MOCK_PRESENCE_CONFIG
| MOCK_ADVANCED_CONFIG
)
assert result["result"]
assert result["result"].domain == DOMAIN
assert result["result"].version == 1
assert result["result"].title == "TheOverSwitchMockName"
assert isinstance(result["result"], ConfigEntry)
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_user_config_flow_over_climate(
hass: HomeAssistant, skip_hass_states_get
): # pylint: disable=unused-argument
"""Test the config flow with all thermostat_over_climate features and no additional features"""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == SOURCE_USER
result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input=MOCK_TH_OVER_CLIMATE_USER_CONFIG
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "type"
assert result["errors"] == {}
result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input=MOCK_TH_OVER_CLIMATE_TYPE_CONFIG
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "presets"
assert result["errors"] == {}
result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input=MOCK_PRESETS_CONFIG
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "advanced"
assert result["errors"] == {}
result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input=MOCK_ADVANCED_CONFIG
)
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
assert (
result["data"]
== MOCK_TH_OVER_CLIMATE_USER_CONFIG
| MOCK_TH_OVER_CLIMATE_TYPE_CONFIG
| MOCK_PRESETS_CONFIG
| MOCK_ADVANCED_CONFIG
| MOCK_DEFAULT_FEATURE_CONFIG
)
assert result["result"]
assert result["result"].domain == DOMAIN
assert result["result"].version == 1
assert result["result"].title == "TheOverClimateMockName"
assert isinstance(result["result"], ConfigEntry)
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_user_config_flow_window_auto_ok(
hass: HomeAssistant,
skip_hass_states_get,
skip_control_heating, # pylint: disable=unused-argument
):
"""Test the config flow with only window auto feature"""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == SOURCE_USER
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
user_input={
CONF_NAME: "TheOverSwitchMockName",
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_SWITCH,
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
CONF_CYCLE_MIN: 5,
CONF_TEMP_MIN: 15,
CONF_TEMP_MAX: 30,
CONF_DEVICE_POWER: 1,
CONF_USE_WINDOW_FEATURE: True,
CONF_USE_MOTION_FEATURE: False,
CONF_USE_POWER_FEATURE: False,
CONF_USE_PRESENCE_FEATURE: False,
},
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "type"
assert result["errors"] == {}
result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input=MOCK_TH_OVER_SWITCH_TYPE_CONFIG
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "tpi"
assert result["errors"] == {}
result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input=MOCK_TH_OVER_SWITCH_TPI_CONFIG
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "presets"
assert result["errors"] == {}
result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input=MOCK_PRESETS_CONFIG
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "window"
assert result["errors"] == {}
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
user_input=MOCK_WINDOW_AUTO_CONFIG,
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "advanced"
assert result["errors"] == {}
result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input=MOCK_ADVANCED_CONFIG
)
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
assert (
result["data"]
== MOCK_TH_OVER_SWITCH_USER_CONFIG
| {
CONF_USE_MOTION_FEATURE: False,
CONF_USE_POWER_FEATURE: False,
CONF_USE_PRESENCE_FEATURE: False,
CONF_WINDOW_DELAY: 30, # the default value is added
CONF_USE_POWER_FEATURE: False,
CONF_USE_PRESENCE_FEATURE: False,
}
| MOCK_TH_OVER_SWITCH_TYPE_CONFIG
| MOCK_TH_OVER_SWITCH_TPI_CONFIG
| MOCK_PRESETS_CONFIG
| MOCK_WINDOW_AUTO_CONFIG
| MOCK_ADVANCED_CONFIG
)
assert result["result"]
assert result["result"].domain == DOMAIN
assert result["result"].version == 1
assert result["result"].title == "TheOverSwitchMockName"
assert isinstance(result["result"], ConfigEntry)
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_user_config_flow_window_auto_ko(
hass: HomeAssistant, skip_hass_states_get # pylint: disable=unused-argument
):
"""Test the config flow with window auto and window features -> not allowed"""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == SOURCE_USER
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
user_input={
CONF_NAME: "TheOverSwitchMockName",
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_SWITCH,
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
CONF_CYCLE_MIN: 5,
CONF_TEMP_MIN: 15,
CONF_TEMP_MAX: 30,
CONF_DEVICE_POWER: 1,
CONF_USE_WINDOW_FEATURE: True,
CONF_USE_MOTION_FEATURE: False,
CONF_USE_POWER_FEATURE: False,
CONF_USE_PRESENCE_FEATURE: False,
},
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "type"
assert result["errors"] == {}
result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input=MOCK_TH_OVER_SWITCH_TYPE_CONFIG
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "tpi"
assert result["errors"] == {}
result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input=MOCK_TH_OVER_SWITCH_TPI_CONFIG
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "presets"
assert result["errors"] == {}
result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input=MOCK_PRESETS_CONFIG
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "window"
assert result["errors"] == {}
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
user_input=MOCK_WINDOW_AUTO_CONFIG | MOCK_WINDOW_CONFIG,
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
# We should stay on window with an error
assert result["step_id"] == "window"
assert result["errors"] == {
"window_sensor_entity_id": "window_open_detection_method"
}
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_user_config_flow_over_4_switches(
hass: HomeAssistant,
skip_hass_states_get,
skip_control_heating, # pylint: disable=unused-argument
):
"""Test the config flow with 4 switchs thermostat_over_switch features"""
SOURCE_CONFIG = { # pylint: disable=wildcard-import, invalid-name
CONF_NAME: "TheOver4SwitchMockName",
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_SWITCH,
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
CONF_CYCLE_MIN: 5,
CONF_TEMP_MIN: 15,
CONF_TEMP_MAX: 30,
CONF_DEVICE_POWER: 1,
CONF_USE_WINDOW_FEATURE: False,
CONF_USE_MOTION_FEATURE: False,
CONF_USE_POWER_FEATURE: False,
CONF_USE_PRESENCE_FEATURE: False,
}
TYPE_CONFIG = { # pylint: disable=wildcard-import, invalid-name
CONF_HEATER: "switch.mock_switch1",
CONF_HEATER_2: "switch.mock_switch2",
CONF_HEATER_3: "switch.mock_switch3",
CONF_HEATER_4: "switch.mock_switch4",
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
CONF_AC_MODE: False,
}
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == SOURCE_USER
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
user_input=SOURCE_CONFIG,
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "type"
assert result["errors"] == {}
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
user_input=TYPE_CONFIG,
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "tpi"
assert result["errors"] == {}
result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input=MOCK_TH_OVER_SWITCH_TPI_CONFIG
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "presets"
assert result["errors"] == {}
result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input=MOCK_PRESETS_CONFIG
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "advanced"
assert result["errors"] == {}
result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input=MOCK_ADVANCED_CONFIG
)
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
assert result[
"data"
] == SOURCE_CONFIG | TYPE_CONFIG | MOCK_TH_OVER_SWITCH_TPI_CONFIG | MOCK_PRESETS_CONFIG | MOCK_ADVANCED_CONFIG | {
CONF_INVERSE_SWITCH: False
}
assert result["result"]
assert result["result"].domain == DOMAIN
assert result["result"].version == 1
assert result["result"].title == "TheOver4SwitchMockName"
assert isinstance(result["result"], ConfigEntry)

54
tests/test_ema.py Normal file
View File

@@ -0,0 +1,54 @@
# pylint: disable=line-too-long
""" Tests de EMA calculation"""
from datetime import datetime, timedelta
from homeassistant.core import HomeAssistant
from custom_components.versatile_thermostat.ema import ExponentialMovingAverage
from .commons import get_tz
def test_ema_basics(hass: HomeAssistant):
"""Test the EMA calculation with basic features"""
tz = get_tz(hass) # pylint: disable=invalid-name
now: datetime = datetime.now(tz=tz)
the_ema = ExponentialMovingAverage(
"test",
# 5 minutes
300,
# Needed for time calculation
get_tz(hass),
1,
)
assert the_ema
current_timestamp = now
# First initialization
assert the_ema.calculate_ema(20, current_timestamp) == 20
current_timestamp = current_timestamp + timedelta(minutes=1)
# One minute later, same temperature. EMA temperature should not have change
assert the_ema.calculate_ema(20, current_timestamp) == 20
# Too short measurement should be ignored
assert the_ema.calculate_ema(2000, current_timestamp) == 20
current_timestamp = current_timestamp + timedelta(seconds=4)
assert the_ema.calculate_ema(20, current_timestamp) == 20
# a new normal measurement 5 minutes later
current_timestamp = current_timestamp + timedelta(minutes=5)
ema = the_ema.calculate_ema(25, current_timestamp)
assert ema > 20
assert ema == 22.5
# a big change in a short time does have a limited effect
current_timestamp = current_timestamp + timedelta(seconds=5)
ema = the_ema.calculate_ema(30, current_timestamp)
assert ema > 22.5
assert ema < 23
assert ema == 22.6

View File

@@ -0,0 +1,124 @@
# pylint: disable=unused-argument, line-too-long, protected-access
""" Test the Window management """
import asyncio
import logging
from unittest.mock import patch, call
from datetime import datetime, timedelta
from custom_components.versatile_thermostat.thermostat_switch import ThermostatOverSwitch
from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import
logging.getLogger().setLevel(logging.DEBUG)
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_inverted_switch(hass: HomeAssistant, skip_hass_states_is_state):
"""Test the Window auto management"""
entry = MockConfigEntry(
domain=DOMAIN,
title="TheOverSwitchMockName",
unique_id="uniqueId",
data={
CONF_NAME: "TheOverSwitchMockName",
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_SWITCH,
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
CONF_CYCLE_MIN: 5,
CONF_TEMP_MIN: 15,
CONF_TEMP_MAX: 30,
"eco_temp": 17,
"comfort_temp": 18,
"boost_temp": 21,
CONF_USE_WINDOW_FEATURE: False,
CONF_USE_MOTION_FEATURE: False,
CONF_USE_POWER_FEATURE: False,
CONF_USE_PRESENCE_FEATURE: False,
CONF_HEATER: "switch.mock_switch",
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
CONF_TPI_COEF_INT: 0.3,
CONF_TPI_COEF_EXT: 0.01,
CONF_MINIMAL_ACTIVATION_DELAY: 30,
CONF_SECURITY_DELAY_MIN: 5,
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
CONF_WINDOW_AUTO_OPEN_THRESHOLD: 0.1,
CONF_WINDOW_AUTO_CLOSE_THRESHOLD: 0.1,
CONF_WINDOW_AUTO_MAX_DURATION: 0, # Should be 0 for test
CONF_INVERSE_SWITCH: True
},
)
with patch(
"homeassistant.core.ServiceRegistry.async_call"
) as mock_service_call, patch(
"homeassistant.core.StateMachine.is_state", return_value=True # switch is On
):
entity: ThermostatOverSwitch = await create_thermostat(
hass, entry, "climate.theoverswitchmockname"
)
assert entity
assert entity.is_inversed
tz = get_tz(hass) # pylint: disable=invalid-name
now = datetime.now(tz)
tpi_algo = entity._prop_algorithm
assert tpi_algo
await entity.async_set_hvac_mode(HVACMode.HEAT)
await entity.async_set_preset_mode(PRESET_BOOST)
assert entity.hvac_mode is HVACMode.HEAT
assert entity.preset_mode is PRESET_BOOST
assert entity.target_temperature == 21
assert entity.is_device_active is False
assert mock_service_call.call_count == 0
# 1. Make the temperature down to activate the switch
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
), patch(
"homeassistant.core.ServiceRegistry.async_call"
) as mock_service_call, patch(
"homeassistant.core.StateMachine.is_state", return_value=True # switch is Off
):
event_timestamp = now - timedelta(minutes=4)
await send_temperature_change_event(entity, 19, event_timestamp)
# The heater turns on
assert entity.hvac_mode is HVACMode.HEAT
# not updated cause mocked assert entity.is_device_active is True
assert mock_service_call.call_count == 1
mock_service_call.assert_has_calls([
call.async_call('switch', SERVICE_TURN_OFF, {'entity_id': 'switch.mock_switch'}),
])
# 2. Make the temperature up to deactivate the switch
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
), patch(
"homeassistant.core.ServiceRegistry.async_call"
) as mock_service_call, patch(
"homeassistant.core.StateMachine.is_state", return_value=False # switch is On -> it should turned off
):
event_timestamp = now - timedelta(minutes=3)
await send_temperature_change_event(entity, 25, event_timestamp)
# The heater turns on
assert entity.hvac_mode is HVACMode.HEAT
# not updated cause mocked assert entity.is_device_active is False
# there is no change because the cycle is currenlty running.
# we should simulate the end of the cycle to see oif underlying switch turns on
await entity._underlyings[0].turn_off()
assert mock_service_call.call_count == 1
mock_service_call.assert_has_calls([
call.async_call('switch', SERVICE_TURN_ON, {'entity_id': 'switch.mock_switch'}),
])
# Clean the entity
entity.remove_thermostat()

601
tests/test_movement.py Normal file
View File

@@ -0,0 +1,601 @@
# pylint: disable=wildcard-import, unused-wildcard-import, protected-access, unused-argument, line-too-long, unused-variable
""" Test the Window management """
from datetime import datetime, timedelta
import logging
from unittest.mock import patch
from custom_components.versatile_thermostat.base_thermostat import BaseThermostat
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_movement_management_time_not_enough(
hass: HomeAssistant, skip_hass_states_is_state
):
"""Test the Presence management when time is not enough"""
entry = MockConfigEntry(
domain=DOMAIN,
title="TheOverSwitchMockName",
unique_id="uniqueId",
data={
CONF_NAME: "TheOverSwitchMockName",
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_SWITCH,
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
CONF_CYCLE_MIN: 5,
CONF_TEMP_MIN: 15,
CONF_TEMP_MAX: 30,
"eco_temp": 17,
"comfort_temp": 18,
"boost_temp": 19,
"eco_away_temp": 17,
"comfort_away_temp": 18,
"boost_away_temp": 19,
CONF_USE_WINDOW_FEATURE: False,
CONF_USE_MOTION_FEATURE: True,
CONF_USE_POWER_FEATURE: False,
CONF_USE_PRESENCE_FEATURE: True,
CONF_HEATER: "switch.mock_switch",
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
CONF_TPI_COEF_INT: 0.3,
CONF_TPI_COEF_EXT: 0.01,
CONF_MINIMAL_ACTIVATION_DELAY: 30,
CONF_SECURITY_DELAY_MIN: 10,
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
CONF_MOTION_SENSOR: "binary_sensor.mock_motion_sensor",
CONF_MOTION_DELAY: 10, # important to not been obliged to wait
CONF_MOTION_OFF_DELAY: 30,
CONF_MOTION_PRESET: "boost",
CONF_NO_MOTION_PRESET: "comfort",
CONF_PRESENCE_SENSOR: "binary_sensor.mock_presence_sensor",
},
)
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)
# start heating, in boost mode, when someone is present. We block the control_heating to avoid running a cycle
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.async_control_heating"
):
await entity.async_set_hvac_mode(HVACMode.HEAT)
await entity.async_set_preset_mode(PRESET_ACTIVITY)
assert entity.hvac_mode is HVACMode.HEAT
assert entity.preset_mode is PRESET_ACTIVITY
# because no motion is detected yet
assert entity.target_temperature == 18
assert entity.motion_state is None
assert entity.presence_state is None
event_timestamp = now - timedelta(minutes=5)
await send_temperature_change_event(entity, 18, event_timestamp)
await send_ext_temperature_change_event(entity, 10, event_timestamp)
await send_presence_change_event(entity, True, False, event_timestamp)
assert entity.presence_state is "on"
# starts detecting motion with time not enough
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
return_value=False,
), patch(
"homeassistant.helpers.condition.state", return_value=False
) as mock_condition:
event_timestamp = now - timedelta(minutes=4)
try_condition = await send_motion_change_event(entity, True, False, event_timestamp)
# Will return False -> we will stay on movement False
await try_condition(None)
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
# state is not changed if time is not enough
assert entity.motion_state is None
assert entity.presence_state is "on"
assert mock_send_event.call_count == 0
# Change is not confirmed
assert mock_heater_on.call_count == 0
assert mock_heater_off.call_count == 0
assert mock_send_event.call_count == 0
# starts detecting motion with time enough 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(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
return_value=False,
), patch(
"homeassistant.helpers.condition.state", return_value=True
) as mock_condition:
event_timestamp = now - timedelta(minutes=3)
try_condition = await send_motion_change_event(entity, True, False, event_timestamp)
# Will return True -> we will switch to movement On
await try_condition(None)
assert entity.hvac_mode is HVACMode.HEAT
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"
# stop detecting motion with off delay too low
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
return_value=True,
) as mock_device_active, patch(
"homeassistant.helpers.condition.state", return_value=False
) as mock_condition:
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
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 mock_send_event.call_count == 0
# The heater must heat now
assert mock_heater_on.call_count == 1
assert mock_heater_off.call_count == 0
assert mock_send_event.call_count == 0
# stop detecting motion with off delay enough long
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
return_value=True,
) as mock_device_active, patch(
"homeassistant.helpers.condition.state", return_value=True
) as mock_condition:
event_timestamp = now - timedelta(minutes=1)
try_condition = await send_motion_change_event(entity, False, True, event_timestamp)
# Will return True -> we will switch to movement Off
await try_condition(None)
assert entity.hvac_mode is HVACMode.HEAT
assert entity.preset_mode is PRESET_ACTIVITY
# because no motion is detected yet
assert entity.target_temperature == 18
assert entity.motion_state is "off"
assert entity.presence_state is "on"
assert mock_send_event.call_count == 0
# The heater must stop heating now
assert mock_heater_on.call_count == 0
assert mock_heater_off.call_count == 1
assert mock_send_event.call_count == 0
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_movement_management_time_enough_and_presence(
hass: HomeAssistant, skip_hass_states_is_state
):
"""Test the Presence management when time is not enough"""
entry = MockConfigEntry(
domain=DOMAIN,
title="TheOverSwitchMockName",
unique_id="uniqueId",
data={
CONF_NAME: "TheOverSwitchMockName",
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_SWITCH,
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
CONF_CYCLE_MIN: 5,
CONF_TEMP_MIN: 15,
CONF_TEMP_MAX: 30,
"eco_temp": 17,
"comfort_temp": 18,
"boost_temp": 19,
"eco_away_temp": 17,
"comfort_away_temp": 18,
"boost_away_temp": 19,
CONF_USE_WINDOW_FEATURE: False,
CONF_USE_MOTION_FEATURE: True,
CONF_USE_POWER_FEATURE: False,
CONF_USE_PRESENCE_FEATURE: True,
CONF_HEATER: "switch.mock_switch",
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
CONF_TPI_COEF_INT: 0.3,
CONF_TPI_COEF_EXT: 0.01,
CONF_MINIMAL_ACTIVATION_DELAY: 30,
CONF_SECURITY_DELAY_MIN: 5,
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
CONF_MOTION_SENSOR: "binary_sensor.mock_motion_sensor",
CONF_MOTION_DELAY: 0, # important to not been obliged to wait
CONF_MOTION_PRESET: "boost",
CONF_NO_MOTION_PRESET: "comfort",
CONF_PRESENCE_SENSOR: "binary_sensor.mock_presence_sensor",
},
)
entity: BaseThermostat = await create_thermostat(
hass, entry, "climate.theoverswitchmockname"
)
assert entity
tz = get_tz(hass) # pylint: disable=invalid-name
now: datetime = datetime.now(tz=tz)
# start heating, in boost mode. We block the control_heating to avoid running a cycle
with patch(
"custom_components.versatile_thermostat.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
assert entity.presence_state is None
event_timestamp = now - timedelta(minutes=4)
await send_temperature_change_event(entity, 18, event_timestamp)
await send_ext_temperature_change_event(entity, 10, event_timestamp)
await send_presence_change_event(entity, True, False, event_timestamp)
assert entity.presence_state is "on"
# starts detecting motion
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
return_value=False,
), patch(
"homeassistant.helpers.condition.state", return_value=True
):
event_timestamp = now - timedelta(minutes=3)
await send_motion_change_event(entity, True, False, event_timestamp)
assert entity.hvac_mode is HVACMode.HEAT
assert entity.preset_mode is PRESET_ACTIVITY
# because motion is detected yet -> switch to Boost mode
assert entity.target_temperature == 19
assert entity.motion_state is "on"
assert entity.presence_state is "on"
assert mock_send_event.call_count == 0
# Change is confirmed. Heater should be started
assert mock_heater_on.call_count == 1
assert mock_heater_off.call_count == 0
assert mock_send_event.call_count == 0
# stop detecting motion with confirmation of stop
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
return_value=True,
), patch(
"homeassistant.helpers.condition.state", return_value=True
):
event_timestamp = now - timedelta(minutes=2)
await send_motion_change_event(entity, False, True, event_timestamp)
assert entity.hvac_mode is HVACMode.HEAT
assert entity.preset_mode is PRESET_ACTIVITY
# because no motion is detected yet
assert entity.target_temperature == 18
assert entity.motion_state is "off"
assert entity.presence_state is "on"
assert mock_send_event.call_count == 0
assert mock_heater_on.call_count == 0
# Because heating is no more necessary
assert mock_heater_off.call_count == 1
assert mock_send_event.call_count == 0
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_movement_management_time_enoughand_not_presence(
hass: HomeAssistant, skip_hass_states_is_state
):
"""Test the Presence management when time is not enough"""
entry = MockConfigEntry(
domain=DOMAIN,
title="TheOverSwitchMockName",
unique_id="uniqueId",
data={
CONF_NAME: "TheOverSwitchMockName",
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_SWITCH,
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
CONF_CYCLE_MIN: 5,
CONF_TEMP_MIN: 15,
CONF_TEMP_MAX: 30,
"eco_temp": 17,
"comfort_temp": 18,
"boost_temp": 19,
"eco_away_temp": 17.1,
"comfort_away_temp": 18.1,
"boost_away_temp": 19.1,
CONF_USE_WINDOW_FEATURE: False,
CONF_USE_MOTION_FEATURE: True,
CONF_USE_POWER_FEATURE: False,
CONF_USE_PRESENCE_FEATURE: True,
CONF_HEATER: "switch.mock_switch",
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
CONF_TPI_COEF_INT: 0.3,
CONF_TPI_COEF_EXT: 0.01,
CONF_MINIMAL_ACTIVATION_DELAY: 30,
CONF_SECURITY_DELAY_MIN: 5,
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
CONF_MOTION_SENSOR: "binary_sensor.mock_motion_sensor",
CONF_MOTION_DELAY: 0, # important to not been obliged to wait
CONF_MOTION_PRESET: "boost",
CONF_NO_MOTION_PRESET: "comfort",
CONF_PRESENCE_SENSOR: "binary_sensor.mock_presence_sensor",
},
)
entity: BaseThermostat = await create_thermostat(
hass, entry, "climate.theoverswitchmockname"
)
assert entity
tz = get_tz(hass) # pylint: disable=invalid-name
now: datetime = datetime.now(tz=tz)
# start heating, in boost mode. We block the control_heating to avoid running a cycle
with patch(
"custom_components.versatile_thermostat.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 and presence is unknown
assert entity.target_temperature == 18
assert entity.motion_state is None
assert entity.presence_state is None
event_timestamp = now - timedelta(minutes=4)
await send_temperature_change_event(entity, 18, event_timestamp)
await send_ext_temperature_change_event(entity, 10, event_timestamp)
await send_presence_change_event(entity, False, True, event_timestamp)
assert entity.presence_state is "off"
# starts detecting motion
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
return_value=False,
), patch(
"homeassistant.helpers.condition.state", return_value=True
):
event_timestamp = now - timedelta(minutes=3)
await send_motion_change_event(entity, True, False, event_timestamp)
assert entity.hvac_mode is HVACMode.HEAT
assert entity.preset_mode is PRESET_ACTIVITY
# because motion is detected yet -> switch to Boost away mode
assert entity.target_temperature == 19.1
assert entity.motion_state is "on"
assert entity.presence_state is "off"
assert mock_send_event.call_count == 0
# Change is confirmed. Heater should be started
assert mock_heater_on.call_count == 1
assert mock_heater_off.call_count == 0
assert mock_send_event.call_count == 0
# stop detecting motion with confirmation of stop
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
return_value=True,
), patch(
"homeassistant.helpers.condition.state", return_value=True
):
event_timestamp = now - timedelta(minutes=2)
await send_motion_change_event(entity, False, True, event_timestamp)
assert entity.hvac_mode is HVACMode.HEAT
assert entity.preset_mode is PRESET_ACTIVITY
# because no motion is detected yet
assert entity.target_temperature == 18.1
assert entity.motion_state is "off"
assert entity.presence_state is "off"
assert mock_send_event.call_count == 0
# 18.1 starts heating with a low on_percent
assert mock_heater_on.call_count == 1
assert entity.proportional_algorithm.on_percent == 0.11
assert mock_heater_off.call_count == 0
assert mock_send_event.call_count == 0
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
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"""
entry = MockConfigEntry(
domain=DOMAIN,
title="TheOverSwitchMockName",
unique_id="uniqueId",
data={
CONF_NAME: "TheOverSwitchMockName",
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_SWITCH,
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
CONF_CYCLE_MIN: 5,
CONF_TEMP_MIN: 15,
CONF_TEMP_MAX: 30,
"eco_temp": 17,
"comfort_temp": 18,
"boost_temp": 19,
"eco_away_temp": 17,
"comfort_away_temp": 18,
"boost_away_temp": 19,
CONF_USE_WINDOW_FEATURE: False,
CONF_USE_MOTION_FEATURE: True,
CONF_USE_POWER_FEATURE: False,
CONF_USE_PRESENCE_FEATURE: True,
CONF_HEATER: "switch.mock_switch",
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
CONF_TPI_COEF_INT: 0.3,
CONF_TPI_COEF_EXT: 0.01,
CONF_MINIMAL_ACTIVATION_DELAY: 30,
CONF_SECURITY_DELAY_MIN: 5,
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
CONF_MOTION_SENSOR: "binary_sensor.mock_motion_sensor",
CONF_MOTION_DELAY: 10,
CONF_MOTION_OFF_DELAY: 30,
CONF_MOTION_PRESET: "boost",
CONF_NO_MOTION_PRESET: "comfort",
CONF_PRESENCE_SENSOR: "binary_sensor.mock_presence_sensor",
},
)
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)
# 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
assert entity.presence_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)
await send_presence_change_event(entity, False, True, event_timestamp)
assert entity.presence_state is "off"
# starts detecting motion
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
return_value=True,
), 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)
assert try_condition1 is not None
assert entity.hvac_mode is HVACMode.HEAT
assert entity.preset_mode is PRESET_ACTIVITY
# because motion is detected yet -> switch to Boost mode
assert entity.target_temperature == 18
assert entity.motion_state is None
assert entity.presence_state is "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
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"
# 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)
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"
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

View File

@@ -0,0 +1,737 @@
# pylint: disable=wildcard-import, unused-wildcard-import, protected-access, unused-argument, line-too-long, unused-variable
""" Test the Multiple switch management """
import asyncio
from unittest.mock import patch, call, ANY
from datetime import datetime, timedelta
import logging
from custom_components.versatile_thermostat.base_thermostat import BaseThermostat
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_one_switch_cycle(
hass: HomeAssistant,
skip_hass_states_is_state,
skip_send_event,
): # pylint: disable=unused-argument
"""Test that when multiple switch are configured the activation is distributed"""
tz = get_tz(hass) # pylint: disable=invalid-name
now: datetime = datetime.now(tz=tz)
entry = MockConfigEntry(
domain=DOMAIN,
title="TheOver4SwitchMockName",
unique_id="uniqueId",
data={
CONF_NAME: "TheOver4SwitchMockName",
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_SWITCH,
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
CONF_CYCLE_MIN: 8,
CONF_TEMP_MIN: 15,
CONF_TEMP_MAX: 30,
"eco_temp": 17,
"comfort_temp": 18,
"boost_temp": 19,
CONF_USE_WINDOW_FEATURE: False,
CONF_USE_MOTION_FEATURE: False,
CONF_USE_POWER_FEATURE: False,
CONF_USE_PRESENCE_FEATURE: False,
CONF_HEATER: "switch.mock_switch1",
CONF_MINIMAL_ACTIVATION_DELAY: 30,
CONF_SECURITY_DELAY_MIN: 5,
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
CONF_TPI_COEF_INT: 0.3,
CONF_TPI_COEF_EXT: 0.01,
},
)
entity: BaseThermostat = await create_thermostat(
hass, entry, "climate.theover4switchmockname"
)
assert entity
assert entity.is_over_climate is False
# start heating, in boost mode. We block the control_heating to avoid running a cycle
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.async_control_heating"
):
await entity.async_set_hvac_mode(HVACMode.HEAT)
await entity.async_set_preset_mode(PRESET_BOOST)
assert entity.hvac_mode is HVACMode.HEAT
assert entity.preset_mode is PRESET_BOOST
assert entity.target_temperature == 19
assert entity.window_state is STATE_OFF
event_timestamp = now - timedelta(minutes=4)
await send_temperature_change_event(entity, 15, event_timestamp)
# Checks that all heaters are off
with patch(
"homeassistant.core.StateMachine.is_state", return_value=False
) as mock_is_state:
assert entity.is_device_active is False # pylint: disable=protected-access
# Should be call for the Switch
assert mock_is_state.call_count == 1
# Set temperature to a low level
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
return_value=False,
) as mock_device_active, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.call_later",
return_value=None,
) as mock_call_later:
await send_ext_temperature_change_event(entity, 5, event_timestamp)
# No special event
assert mock_send_event.call_count == 0
assert mock_heater_off.call_count == 0
# The first heater should be on but because call_later is mocked heater_on is not called
# assert mock_heater_on.call_count == 1
assert mock_heater_on.call_count == 0
# There is no check if active
assert mock_device_active.call_count == 0
# 4 calls dispatched along the cycle
assert mock_call_later.call_count == 1
mock_call_later.assert_has_calls(
[
call.call_later(hass, 0.0, ANY),
]
)
# Set a temperature at middle level
event_timestamp = now - timedelta(minutes=4)
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
return_value=False,
) as mock_device_active:
await send_temperature_change_event(entity, 18, event_timestamp)
# No special event
assert mock_send_event.call_count == 0
assert mock_heater_off.call_count == 0
# The first heater should be turned on but is already on but because above we mock
# call_later the heater is not on. But this time it will be really on
assert mock_heater_on.call_count == 1
# Set another temperature at middle level
event_timestamp = now - timedelta(minutes=3)
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
return_value=True,
) as mock_device_active:
await send_temperature_change_event(entity, 18.1, event_timestamp)
# No special event
assert mock_send_event.call_count == 0
assert mock_heater_off.call_count == 0
# The heater is already on cycle. So we wait that the cycle ends and no heater action
# is done
assert mock_heater_on.call_count == 0
# assert entity.underlying_entity(0)._should_relaunch_control_heating is True
# Simulate the relaunch
await entity.underlying_entity(
0
)._turn_on_later( # pylint: disable=protected-access
None
)
# wait restart
await asyncio.sleep(0.1)
assert mock_heater_on.call_count == 1
# normal ? assert entity.underlying_entity(0)._should_relaunch_control_heating is False
# Simulate the end of heater on cycle
event_timestamp = now - timedelta(minutes=3)
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
return_value=True,
) as mock_device_active:
await entity.underlying_entity(
0
)._turn_off_later( # pylint: disable=protected-access
None
)
# No special event
assert mock_send_event.call_count == 0
assert mock_heater_on.call_count == 0
# The heater should be turned off this time
assert mock_heater_off.call_count == 1
# assert entity.underlying_entity(0)._should_relaunch_control_heating is False
# Simulate the start of heater on cycle
event_timestamp = now - timedelta(minutes=3)
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
return_value=True,
) as mock_device_active:
await entity.underlying_entity(
0
)._turn_on_later( # pylint: disable=protected-access
None
)
# No special event
assert mock_send_event.call_count == 0
assert mock_heater_on.call_count == 1
# The heater should be turned off this time
assert mock_heater_off.call_count == 0
# assert entity.underlying_entity(0)._should_relaunch_control_heating is False
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_multiple_switchs(
hass: HomeAssistant,
skip_hass_states_is_state,
skip_send_event,
): # pylint: disable=unused-argument
"""Test that when multiple switch are configured the activation is distributed"""
tz = get_tz(hass) # pylint: disable=invalid-name
now: datetime = datetime.now(tz=tz)
entry = MockConfigEntry(
domain=DOMAIN,
title="TheOver4SwitchMockName",
unique_id="uniqueId",
data={
CONF_NAME: "TheOver4SwitchMockName",
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_SWITCH,
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
CONF_CYCLE_MIN: 8,
CONF_TEMP_MIN: 15,
CONF_TEMP_MAX: 30,
"eco_temp": 17,
"comfort_temp": 18,
"boost_temp": 19,
CONF_USE_WINDOW_FEATURE: False,
CONF_USE_MOTION_FEATURE: False,
CONF_USE_POWER_FEATURE: False,
CONF_USE_PRESENCE_FEATURE: False,
CONF_HEATER: "switch.mock_switch1",
CONF_HEATER_2: "switch.mock_switch2",
CONF_HEATER_3: "switch.mock_switch3",
CONF_HEATER_4: "switch.mock_switch4",
CONF_MINIMAL_ACTIVATION_DELAY: 30,
CONF_SECURITY_DELAY_MIN: 5,
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
CONF_TPI_COEF_INT: 0.3,
CONF_TPI_COEF_EXT: 0.01,
},
)
entity: BaseThermostat = await create_thermostat(
hass, entry, "climate.theover4switchmockname"
)
assert entity
assert entity.is_over_climate is False
assert entity.nb_underlying_entities == 4
# start heating, in boost mode. We block the control_heating to avoid running a cycle
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.async_control_heating"
), patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.set_hvac_mode"
) as mock_underlying_set_hvac_mode:
await entity.async_set_hvac_mode(HVACMode.HEAT)
await entity.async_set_preset_mode(PRESET_BOOST)
assert entity.hvac_mode is HVACMode.HEAT
assert entity.preset_mode is PRESET_BOOST
assert entity.target_temperature == 19
assert entity.window_state is STATE_OFF
event_timestamp = now - timedelta(minutes=4)
await send_temperature_change_event(entity, 15, event_timestamp)
# Checks that all climates are off
assert entity.is_device_active is False # pylint: disable=protected-access
# Should be call for all Switch
assert mock_underlying_set_hvac_mode.call_count == 4
mock_underlying_set_hvac_mode.assert_has_calls(
[
call.set_hvac_mode(HVACMode.HEAT),
]
)
# Set temperature to a low level
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
return_value=False,
) as mock_device_active, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.call_later",
return_value=None,
) as mock_call_later:
await send_ext_temperature_change_event(entity, 5, event_timestamp)
# No special event
assert mock_send_event.call_count == 0
assert mock_heater_off.call_count == 0
# The first heater should be on but because call_later is mocked heater_on is not called
# assert mock_heater_on.call_count == 1
assert mock_heater_on.call_count == 0
# There is no check if active
assert mock_device_active.call_count == 0
# 4 calls dispatched along the cycle
assert mock_call_later.call_count == 4
mock_call_later.assert_has_calls(
[
call.call_later(hass, 0.0, ANY),
call.call_later(hass, 120.0, ANY),
call.call_later(hass, 240.0, ANY),
call.call_later(hass, 360.0, ANY),
]
)
# Set a temperature at middle level
event_timestamp = now - timedelta(minutes=4)
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
return_value=False,
) as mock_device_active:
await send_temperature_change_event(entity, 18, event_timestamp)
# No special event
assert mock_send_event.call_count == 0
assert mock_heater_off.call_count == 0
# The first heater should be turned on but is already on but because call_later
# is mocked, it is only turned on here
assert mock_heater_on.call_count == 1
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_multiple_climates(
hass: HomeAssistant,
skip_hass_states_is_state,
skip_send_event,
): # pylint: disable=unused-argument
"""Test that when multiple climates are configured the activation and deactivation
is propagated to all climates"""
tz = get_tz(hass) # pylint: disable=invalid-name
now: datetime = datetime.now(tz=tz)
entry = MockConfigEntry(
domain=DOMAIN,
title="TheOver4ClimateMockName",
unique_id="uniqueId",
data={
CONF_NAME: "TheOver4ClimateMockName",
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_CLIMATE,
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
CONF_CYCLE_MIN: 8,
CONF_TEMP_MIN: 15,
CONF_TEMP_MAX: 30,
"eco_temp": 17,
"comfort_temp": 18,
"boost_temp": 19,
CONF_USE_WINDOW_FEATURE: False,
CONF_USE_MOTION_FEATURE: False,
CONF_USE_POWER_FEATURE: False,
CONF_USE_PRESENCE_FEATURE: False,
CONF_CLIMATE: "switch.mock_climate1",
CONF_CLIMATE_2: "switch.mock_climate2",
CONF_CLIMATE_3: "switch.mock_climate3",
CONF_CLIMATE_4: "switch.mock_climate4",
CONF_MINIMAL_ACTIVATION_DELAY: 30,
CONF_SECURITY_DELAY_MIN: 5,
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
},
)
entity: BaseThermostat = await create_thermostat(
hass, entry, "climate.theover4climatemockname"
)
assert entity
assert entity.is_over_climate is True
assert entity.nb_underlying_entities == 4
# start heating, in boost mode. We block the control_heating to avoid running a cycle
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.async_control_heating"
), patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.set_hvac_mode"
) as mock_underlying_set_hvac_mode:
await entity.async_set_hvac_mode(HVACMode.HEAT)
await entity.async_set_preset_mode(PRESET_BOOST)
assert entity.hvac_mode is HVACMode.HEAT
assert entity.preset_mode is PRESET_BOOST
assert entity.target_temperature == 19
assert entity.window_state is STATE_OFF
event_timestamp = now - timedelta(minutes=4)
await send_temperature_change_event(entity, 15, event_timestamp)
# Should be call for all Switch
assert mock_underlying_set_hvac_mode.call_count == 4
mock_underlying_set_hvac_mode.assert_has_calls(
[
call.set_hvac_mode(HVACMode.HEAT),
]
)
assert entity.is_device_active is False # pylint: disable=protected-access
# Stop heating, in boost mode. We block the control_heating to avoid running a cycle
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.async_control_heating"
), patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.set_hvac_mode"
) as mock_underlying_set_hvac_mode:
await entity.async_set_hvac_mode(HVACMode.OFF)
assert entity.hvac_mode is HVACMode.OFF
assert entity.preset_mode is PRESET_BOOST
assert entity.target_temperature == 19
assert entity.window_state is STATE_OFF
event_timestamp = now - timedelta(minutes=4)
await send_temperature_change_event(entity, 15, event_timestamp)
# Should be call for all Switch
assert mock_underlying_set_hvac_mode.call_count == 4
mock_underlying_set_hvac_mode.assert_has_calls(
[
call.set_hvac_mode(HVACMode.OFF),
]
)
assert entity.is_device_active is False # pylint: disable=protected-access
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_multiple_climates_underlying_changes(
hass: HomeAssistant,
skip_hass_states_is_state,
skip_send_event,
): # pylint: disable=unused-argument
"""Test that when multiple switch are configured the activation of one underlying
climate activate the others"""
tz = get_tz(hass) # pylint: disable=invalid-name
now: datetime = datetime.now(tz=tz)
entry = MockConfigEntry(
domain=DOMAIN,
title="TheOver4ClimateMockName",
unique_id="uniqueId",
data={
CONF_NAME: "TheOver4ClimateMockName",
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_CLIMATE,
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
CONF_CYCLE_MIN: 8,
CONF_TEMP_MIN: 15,
CONF_TEMP_MAX: 30,
"eco_temp": 17,
"comfort_temp": 18,
"boost_temp": 19,
CONF_USE_WINDOW_FEATURE: False,
CONF_USE_MOTION_FEATURE: False,
CONF_USE_POWER_FEATURE: False,
CONF_USE_PRESENCE_FEATURE: False,
CONF_CLIMATE: "switch.mock_climate1",
CONF_CLIMATE_2: "switch.mock_climate2",
CONF_CLIMATE_3: "switch.mock_climate3",
CONF_CLIMATE_4: "switch.mock_climate4",
CONF_MINIMAL_ACTIVATION_DELAY: 30,
CONF_SECURITY_DELAY_MIN: 5,
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
},
)
entity: BaseThermostat = await create_thermostat(
hass, entry, "climate.theover4climatemockname"
)
assert entity
assert entity.is_over_climate is True
assert entity.nb_underlying_entities == 4
# start heating, in boost mode. We block the control_heating to avoid running a cycle
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.async_control_heating"
), patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.set_hvac_mode"
) as mock_underlying_set_hvac_mode:
await entity.async_set_hvac_mode(HVACMode.HEAT)
await entity.async_set_preset_mode(PRESET_BOOST)
assert entity.hvac_mode is HVACMode.HEAT
assert entity.preset_mode is PRESET_BOOST
assert entity.target_temperature == 19
assert entity.window_state is STATE_OFF
event_timestamp = now - timedelta(minutes=4)
await send_temperature_change_event(entity, 15, event_timestamp)
# Should be call for all Switch
assert mock_underlying_set_hvac_mode.call_count == 4
mock_underlying_set_hvac_mode.assert_has_calls(
[
call.set_hvac_mode(HVACMode.HEAT),
]
)
assert entity.is_device_active is False # pylint: disable=protected-access
# Stop heating on one underlying climate
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.async_control_heating"
), patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.set_hvac_mode"
) as mock_underlying_set_hvac_mode:
# Wait 11 sec so that the event will not be discarded
event_timestamp = now + timedelta(seconds=11)
await send_climate_change_event(
entity,
HVACMode.OFF,
HVACMode.HEAT,
HVACAction.OFF,
HVACAction.HEATING,
event_timestamp,
)
# Should be call for all Switch
assert mock_underlying_set_hvac_mode.call_count == 4
mock_underlying_set_hvac_mode.assert_has_calls(
[
call.set_hvac_mode(HVACMode.OFF),
]
)
assert entity.hvac_mode == HVACMode.OFF
assert entity.is_device_active is False # pylint: disable=protected-access
# Start heating on one underlying climate
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.async_control_heating"
), patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.set_hvac_mode"
) as mock_underlying_set_hvac_mode, patch(
# notice that there is no need of return_value=HVACAction.IDLE because this is not
# a function but a property
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.hvac_action",
HVACAction.IDLE,
):
# Wait 11 sec so that the event will not be discarded
event_timestamp = now + timedelta(seconds=11)
await send_climate_change_event(
entity,
HVACMode.HEAT,
HVACMode.OFF,
HVACAction.IDLE,
HVACAction.OFF,
event_timestamp,
)
# Should be call for all Switch
assert mock_underlying_set_hvac_mode.call_count == 4
mock_underlying_set_hvac_mode.assert_has_calls(
[
call.set_hvac_mode(HVACMode.HEAT),
]
)
assert entity.hvac_mode == HVACMode.HEAT
assert entity.hvac_action == HVACAction.IDLE
assert entity.is_device_active is False # pylint: disable=protected-access
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_multiple_switch_power_management(
hass: HomeAssistant, skip_hass_states_is_state
):
"""Test the Power management"""
entry = MockConfigEntry(
domain=DOMAIN,
title="TheOverSwitchMockName",
unique_id="uniqueId",
data={
CONF_NAME: "TheOver4SwitchMockName",
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_SWITCH,
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
CONF_CYCLE_MIN: 8,
CONF_TEMP_MIN: 15,
CONF_TEMP_MAX: 30,
"eco_temp": 17,
"comfort_temp": 18,
"boost_temp": 19,
CONF_USE_WINDOW_FEATURE: False,
CONF_USE_MOTION_FEATURE: False,
CONF_USE_POWER_FEATURE: True,
CONF_USE_PRESENCE_FEATURE: False,
CONF_HEATER: "switch.mock_switch1",
CONF_HEATER_2: "switch.mock_switch2",
CONF_HEATER_3: "switch.mock_switch3",
CONF_HEATER_4: "switch.mock_switch4",
CONF_MINIMAL_ACTIVATION_DELAY: 30,
CONF_SECURITY_DELAY_MIN: 5,
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
CONF_TPI_COEF_INT: 0.3,
CONF_TPI_COEF_EXT: 0.01,
CONF_POWER_SENSOR: "sensor.mock_power_sensor",
CONF_MAX_POWER_SENSOR: "sensor.mock_power_max_sensor",
CONF_DEVICE_POWER: 100,
CONF_PRESET_POWER: 12,
},
)
entity: BaseThermostat = await create_thermostat(
hass, entry, "climate.theover4switchmockname"
)
assert entity
assert entity.is_over_climate is False
assert entity.nb_underlying_entities == 4
tpi_algo = entity._prop_algorithm
assert tpi_algo
await entity.async_set_hvac_mode(HVACMode.HEAT)
await entity.async_set_preset_mode(PRESET_BOOST)
assert entity.hvac_mode is HVACMode.HEAT
assert entity.preset_mode is PRESET_BOOST
assert entity.overpowering_state is None
assert entity.target_temperature == 19
# 1. Send power mesurement
await send_power_change_event(entity, 50, datetime.now())
# Send power max mesurement
await send_max_power_change_event(entity, 300, datetime.now())
assert await entity.check_overpowering() is False
# All configuration is complete and power is < power_max
assert entity.preset_mode is PRESET_BOOST
assert entity.overpowering_state is False
# 2. Send power max mesurement too low and HVACMode is on
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off:
# 100 of the device / 4 -> 25, current power 50 so max is 75
await send_max_power_change_event(entity, 74, datetime.now())
assert await entity.check_overpowering() is True
# All configuration is complete and power is > power_max we switch to POWER preset
assert entity.preset_mode is PRESET_POWER
assert entity.overpowering_state is True
assert entity.target_temperature == 12
assert mock_send_event.call_count == 2
mock_send_event.assert_has_calls(
[
call.send_event(EventType.PRESET_EVENT, {"preset": PRESET_POWER}),
call.send_event(
EventType.POWER_EVENT,
{
"type": "start",
"current_power": 50,
"device_power": 100,
"current_power_max": 74,
"current_power_consumption": 25.0,
},
),
],
any_order=True,
)
assert mock_heater_on.call_count == 0
assert mock_heater_off.call_count == 4 # The fourth are shutdown
# 3. change PRESET
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event:
await entity.async_set_preset_mode(PRESET_ECO)
assert entity.preset_mode is PRESET_ECO
# No change
assert entity.overpowering_state is True
# 4. Send hugh power max mesurement to release overpowering
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off:
# 100 of the device / 4 -> 25, current power 50 so max is 75. With 150 no overheating
await send_max_power_change_event(entity, 150, datetime.now())
assert await entity.check_overpowering() is False
# All configuration is complete and power is > power_max we switch to POWER preset
assert entity.preset_mode is PRESET_ECO
assert entity.overpowering_state is False
assert entity.target_temperature == 17
assert (
mock_heater_on.call_count == 0
) # The fourth are not restarted because temperature is enought
assert mock_heater_off.call_count == 0

View File

@@ -0,0 +1,240 @@
# pylint: disable=unused-argument, line-too-long
""" Test the OpenWindow algorithm """
from datetime import datetime, timedelta
from custom_components.versatile_thermostat.open_window_algorithm import (
WindowOpenDetectionAlgorithm,
)
from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import
async def test_open_window_algo(
hass: HomeAssistant,
skip_hass_states_is_state,
):
"""Tests the Algo"""
the_algo = WindowOpenDetectionAlgorithm(60.0, 0.0)
assert the_algo.last_slope is None
tz = get_tz(hass) # pylint: disable=invalid-name
now = datetime.now(tz)
event_timestamp = now - timedelta(minutes=10)
last_slope = the_algo.add_temp_measurement(
temperature=10, datetime_measure=event_timestamp
)
# We need at least 4 measurement
assert last_slope is None
assert the_algo.last_slope is None
assert the_algo.is_window_close_detected() is False
assert the_algo.is_window_open_detected() is False
event_timestamp = now - timedelta(minutes=9)
last_slope = the_algo.add_temp_measurement(
temperature=10, datetime_measure=event_timestamp
)
event_timestamp = now - timedelta(minutes=8)
last_slope = the_algo.add_temp_measurement(
temperature=10, datetime_measure=event_timestamp
)
event_timestamp = now - timedelta(minutes=7)
last_slope = the_algo.add_temp_measurement(
temperature=10, datetime_measure=event_timestamp
)
# No slope because same temperature
assert last_slope == 0
assert the_algo.last_slope == 0
assert the_algo.is_window_close_detected() is True
assert the_algo.is_window_open_detected() is False
event_timestamp = now - timedelta(minutes=6)
last_slope = the_algo.add_temp_measurement(
temperature=9, datetime_measure=event_timestamp
)
# A slope is calculated
assert last_slope == -48.0
assert the_algo.last_slope == -48.0
assert the_algo.is_window_close_detected() is False
assert the_algo.is_window_open_detected() is False
# A new temperature with 2 degre less in one minute (value will be rejected)
event_timestamp = now - timedelta(minutes=5)
last_slope = the_algo.add_temp_measurement(
temperature=7, datetime_measure=event_timestamp
)
# A slope is calculated
assert last_slope == (-48.0 * 0.2 - 120.0 * 0.8)
assert the_algo.last_slope == -105.6
assert the_algo.is_window_close_detected() is False
assert the_algo.is_window_open_detected() is True
# A new temperature with 1 degre less
event_timestamp = now - timedelta(minutes=4)
last_slope = the_algo.add_temp_measurement(
temperature=6, datetime_measure=event_timestamp
)
# A slope is calculated
assert last_slope == -105.6 * 0.2 - 60.0 * 0.8
assert the_algo.last_slope == -69.12
assert the_algo.is_window_close_detected() is False
assert the_algo.is_window_open_detected() is True
# A new temperature with 0 degre less
event_timestamp = now - timedelta(minutes=3)
last_slope = the_algo.add_temp_measurement(
temperature=6, datetime_measure=event_timestamp
)
# A slope is calculated
assert last_slope == round(-69.12 * 0.2 - 0.0 * 0.8, 2)
assert the_algo.last_slope == -13.82
assert the_algo.is_window_close_detected() is False
assert the_algo.is_window_open_detected() is False
# A new temperature with 1 degre more
event_timestamp = now - timedelta(minutes=2)
last_slope = the_algo.add_temp_measurement(
temperature=7, datetime_measure=event_timestamp
)
# A slope is calculated
assert last_slope == round(-13.82 * 0.2 + 60.0 * 0.8, 2)
assert the_algo.last_slope == 45.24
assert the_algo.is_window_close_detected() is True
assert the_algo.is_window_open_detected() is False
async def test_open_window_algo_wrong(
hass: HomeAssistant,
skip_hass_states_is_state,
):
"""Tests the Algo with wrong date"""
the_algo = WindowOpenDetectionAlgorithm(60.0, 0.0)
assert the_algo.last_slope is None
tz = get_tz(hass) # pylint: disable=invalid-name
now = datetime.now(tz)
event_timestamp = now - timedelta(minutes=5)
last_slope = the_algo.add_temp_measurement(
temperature=10, datetime_measure=event_timestamp
)
# We need at least 2 measurement
assert last_slope is None
assert the_algo.last_slope is None
assert the_algo.is_window_close_detected() is False
assert the_algo.is_window_open_detected() is False
# The next datetime_measurement cannot be in the past
event_timestamp = now - timedelta(minutes=6)
last_slope = the_algo.add_temp_measurement(
temperature=18, datetime_measure=event_timestamp
)
# No slope because same temperature
assert last_slope is None
assert the_algo.last_slope is None
assert the_algo.is_window_close_detected() is False
assert the_algo.is_window_open_detected() is False
async def test_open_window_algo_fake_point(
hass: HomeAssistant,
skip_hass_states_is_state,
):
"""Tests the Algo with adding fake point"""
the_algo = WindowOpenDetectionAlgorithm(3.0, 0.1)
assert the_algo.last_slope is None
tz = get_tz(hass) # pylint: disable=invalid-name
now = datetime.now(tz)
event_timestamp = now
last_slope = the_algo.check_age_last_measurement(
temperature=10, datetime_now=event_timestamp
)
# We need at least 4 measurement
assert last_slope is None
assert the_algo.last_slope is None
assert the_algo.is_window_close_detected() is False
assert the_algo.is_window_open_detected() is False
event_timestamp = now + timedelta(minutes=1)
last_slope = the_algo.add_temp_measurement(
temperature=10, datetime_measure=event_timestamp
)
event_timestamp = now + timedelta(minutes=2)
last_slope = the_algo.add_temp_measurement(
temperature=10, datetime_measure=event_timestamp
)
event_timestamp = now + timedelta(minutes=3)
last_slope = the_algo.add_temp_measurement(
temperature=10, datetime_measure=event_timestamp
)
# No slope because same temperature
assert last_slope == 0
assert the_algo.last_slope == 0
assert the_algo.is_window_close_detected() is False
assert the_algo.is_window_open_detected() is False
event_timestamp = now + timedelta(minutes=4)
last_slope = the_algo.add_temp_measurement(
temperature=9, datetime_measure=event_timestamp
)
# A slope is calculated
assert last_slope == -48.0
assert the_algo.last_slope == -48.0
assert the_algo.is_window_close_detected() is False
assert the_algo.is_window_open_detected() is True # One degre in one minute
# 1 Add a fake point one minute later
event_timestamp = now + timedelta(minutes=5)
last_slope = the_algo.check_age_last_measurement(
temperature=8, datetime_now=event_timestamp
)
# The slope not have change (fake point is ignored)
assert last_slope == -48.0
assert the_algo.last_slope == -48.0
assert the_algo.is_window_close_detected() is False
assert the_algo.is_window_open_detected() is True # One degre in one minute
# 2 Add a fake point 31 minute later -> +2 degres in 32 minutes
event_timestamp = event_timestamp + timedelta(minutes=31)
last_slope = the_algo.check_age_last_measurement(
temperature=10, datetime_now=event_timestamp
)
# The slope should have change (fake point is added)
assert last_slope == -8.1
assert the_algo.last_slope == -8.1
assert the_algo.is_window_close_detected() is False
assert the_algo.is_window_open_detected() is True
# 3 Add a 2nd fake point 30 minute later -> +3 degres in 30 minutes
event_timestamp = event_timestamp + timedelta(minutes=31)
last_slope = the_algo.check_age_last_measurement(
temperature=13, datetime_now=event_timestamp
)
# The slope should have change (fake point is added)
assert last_slope == 0.67
assert the_algo.last_slope == 0.67
assert the_algo.is_window_close_detected() is True
assert the_algo.is_window_open_detected() is False

209
tests/test_pi.py Normal file
View File

@@ -0,0 +1,209 @@
# pylint: disable=line-too-long
""" Tests de PI algorithm used for auto-regulation """
from custom_components.versatile_thermostat.pi_algorithm import PITemperatureRegulator
def test_pi_algorithm_basics():
"""Test the PI algorithm"""
the_algo = PITemperatureRegulator(
target_temp=20,
kp=0.2,
ki=0.05,
k_ext=0.1,
offset_max=2,
stabilization_threshold=0.1,
accumulated_error_threshold=20,
)
assert the_algo
assert the_algo.calculate_regulated_temperature(20, 20) == 20
assert the_algo.calculate_regulated_temperature(20, 10) == 21
# to reset the accumulated erro
the_algo.set_target_temp(20)
the_algo.reset_accumulated_error()
# Test the accumulator threshold effect and offset_max
assert the_algo.calculate_regulated_temperature(10, 10) == 22 # +2
assert the_algo.calculate_regulated_temperature(10, 10) == 22
assert the_algo.calculate_regulated_temperature(10, 10) == 22
# Will keep infinitly 22.0
# to reset the accumulated error
the_algo.reset_accumulated_error()
assert the_algo.calculate_regulated_temperature(18, 10) == 21.3 # +1.5
assert the_algo.calculate_regulated_temperature(18.1, 10) == 21.4 # +1.6
assert the_algo.calculate_regulated_temperature(18.3, 10) == 21.4 # +1.6
assert the_algo.calculate_regulated_temperature(18.5, 10) == 21.5 # +1.7
assert the_algo.calculate_regulated_temperature(18.7, 10) == 21.6 # +1.7
assert the_algo.calculate_regulated_temperature(19, 10) == 21.6 # +1.7
assert the_algo.calculate_regulated_temperature(20, 10) == 21.5 # +1.5
assert the_algo.calculate_regulated_temperature(21, 10) == 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
# 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 # =
def test_pi_algorithm_light():
"""Test the PI algorithm"""
the_algo = PITemperatureRegulator(
target_temp=20,
kp=0.2,
ki=0.05,
k_ext=0.1,
offset_max=2,
stabilization_threshold=0.1,
accumulated_error_threshold=20,
)
assert the_algo
# to reset the accumulated erro
the_algo.set_target_temp(20)
assert the_algo.calculate_regulated_temperature(18, 10) == 21.3 # +1.5
assert the_algo.calculate_regulated_temperature(18.1, 10) == 21.4 # +1.6
assert the_algo.calculate_regulated_temperature(18.3, 10) == 21.4 # +1.6
assert the_algo.calculate_regulated_temperature(18.5, 10) == 21.5 # +1.7
assert the_algo.calculate_regulated_temperature(18.7, 10) == 21.6 # +1.7
assert the_algo.calculate_regulated_temperature(19, 10) == 21.6 # +1.7
assert the_algo.calculate_regulated_temperature(20, 10) == 21.5 # +1.5
assert the_algo.calculate_regulated_temperature(21, 10) == 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
# 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 # =
def test_pi_algorithm_medium():
"""Test the PI algorithm"""
the_algo = PITemperatureRegulator(
target_temp=20,
kp=0.5,
ki=0.1,
k_ext=0.1,
offset_max=3,
stabilization_threshold=0.1,
accumulated_error_threshold=30,
)
assert the_algo
# to reset the accumulated erro
the_algo.set_target_temp(20)
assert the_algo.calculate_regulated_temperature(18, 10) == 22.0
assert the_algo.calculate_regulated_temperature(18.1, 10) == 22.1
assert the_algo.calculate_regulated_temperature(18.3, 10) == 22.2
assert the_algo.calculate_regulated_temperature(18.5, 10) == 22.3
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
# 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
# to reset the accumulated erro
the_algo.set_target_temp(20)
the_algo.reset_accumulated_error()
# Test the error acculation effect
assert the_algo.calculate_regulated_temperature(19, 5) == 22.0
assert the_algo.calculate_regulated_temperature(19, 5) == 22.1
assert the_algo.calculate_regulated_temperature(19, 5) == 22.2
assert the_algo.calculate_regulated_temperature(19, 5) == 22.3
assert the_algo.calculate_regulated_temperature(19, 5) == 22.4
assert the_algo.calculate_regulated_temperature(19, 5) == 22.5
assert the_algo.calculate_regulated_temperature(19, 5) == 22.6
assert the_algo.calculate_regulated_temperature(19, 5) == 22.7
assert the_algo.calculate_regulated_temperature(19, 5) == 22.8
assert the_algo.calculate_regulated_temperature(19, 5) == 22.9
assert the_algo.calculate_regulated_temperature(19, 5) == 23
assert the_algo.calculate_regulated_temperature(19, 5) == 23
assert the_algo.calculate_regulated_temperature(19, 5) == 23
def test_pi_algorithm_strong():
"""Test the PI algorithm"""
the_algo = PITemperatureRegulator(
target_temp=20,
kp=0.6,
ki=0.2,
k_ext=0.2,
offset_max=4,
stabilization_threshold=0.1,
accumulated_error_threshold=40,
)
assert the_algo
# to reset the accumulated erro
the_algo.set_target_temp(20)
assert the_algo.calculate_regulated_temperature(18, 10) == 23.2
assert the_algo.calculate_regulated_temperature(18.1, 10) == 23.5
assert the_algo.calculate_regulated_temperature(18.3, 10) == 23.8
assert the_algo.calculate_regulated_temperature(18.5, 10) == 24
assert the_algo.calculate_regulated_temperature(18.7, 10) == 24
assert the_algo.calculate_regulated_temperature(19, 10) == 24
assert the_algo.calculate_regulated_temperature(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
# 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, -6) == 24
assert the_algo.calculate_regulated_temperature(20, -8) == 24
# to reset the accumulated erro
the_algo.set_target_temp(20)
the_algo.reset_accumulated_error()
# Test the error acculation effect
assert the_algo.calculate_regulated_temperature(19, 10) == 22.6
assert the_algo.calculate_regulated_temperature(19, 10) == 22.8
assert the_algo.calculate_regulated_temperature(19, 10) == 23.0
assert the_algo.calculate_regulated_temperature(19, 10) == 23.2
assert the_algo.calculate_regulated_temperature(19, 10) == 23.4
assert the_algo.calculate_regulated_temperature(19, 10) == 23.6
assert the_algo.calculate_regulated_temperature(19, 10) == 23.8
assert the_algo.calculate_regulated_temperature(19, 10) == 24.0
assert the_algo.calculate_regulated_temperature(19, 10) == 24.0
assert the_algo.calculate_regulated_temperature(19, 10) == 24.0
assert the_algo.calculate_regulated_temperature(19, 10) == 24.0

460
tests/test_power.py Normal file
View File

@@ -0,0 +1,460 @@
# pylint: disable=protected-access, unused-argument, line-too-long
""" Test the Power management """
from unittest.mock import patch, call
from datetime import datetime, timedelta
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_power_management_hvac_off(
hass: HomeAssistant, skip_hass_states_is_state
):
"""Test the Power management"""
entry = MockConfigEntry(
domain=DOMAIN,
title="TheOverSwitchMockName",
unique_id="uniqueId",
data={
CONF_NAME: "TheOverSwitchMockName",
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_SWITCH,
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
CONF_CYCLE_MIN: 5,
CONF_TEMP_MIN: 15,
CONF_TEMP_MAX: 30,
"eco_temp": 17,
"comfort_temp": 18,
"boost_temp": 19,
CONF_USE_WINDOW_FEATURE: False,
CONF_USE_MOTION_FEATURE: False,
CONF_USE_POWER_FEATURE: True,
CONF_USE_PRESENCE_FEATURE: False,
CONF_HEATER: "switch.mock_switch",
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
CONF_TPI_COEF_INT: 0.3,
CONF_TPI_COEF_EXT: 0.01,
CONF_MINIMAL_ACTIVATION_DELAY: 30,
CONF_SECURITY_DELAY_MIN: 5,
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
CONF_POWER_SENSOR: "sensor.mock_power_sensor",
CONF_MAX_POWER_SENSOR: "sensor.mock_power_max_sensor",
CONF_DEVICE_POWER: 100,
CONF_PRESET_POWER: 12,
},
)
entity: ThermostatOverSwitch = await create_thermostat(
hass, entry, "climate.theoverswitchmockname"
)
assert entity
tpi_algo = entity._prop_algorithm
assert tpi_algo
await entity.async_set_preset_mode(PRESET_BOOST)
assert entity.preset_mode is PRESET_BOOST
assert entity.target_temperature == 19
assert entity.overpowering_state is None
assert entity.hvac_mode == HVACMode.OFF
# Send power mesurement
await send_power_change_event(entity, 50, datetime.now())
assert await entity.check_overpowering() is False
# All configuration is not complete
assert entity.preset_mode is PRESET_BOOST
assert entity.overpowering_state is None
# Send power max mesurement
await send_max_power_change_event(entity, 300, datetime.now())
assert await entity.check_overpowering() is False
# All configuration is complete and power is < power_max
assert entity.preset_mode is PRESET_BOOST
assert entity.overpowering_state is False
# Send power max mesurement too low but HVACMode is off
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off:
await send_max_power_change_event(entity, 149, datetime.now())
assert await entity.check_overpowering() is True
# All configuration is complete and power is > power_max but we stay in Boost cause thermostat if Off
assert entity.preset_mode is PRESET_BOOST
assert entity.overpowering_state is True
assert mock_send_event.call_count == 0
assert mock_heater_on.call_count == 0
assert mock_heater_off.call_count == 0
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_power_management_hvac_on(hass: HomeAssistant, skip_hass_states_is_state):
"""Test the Power management"""
entry = MockConfigEntry(
domain=DOMAIN,
title="TheOverSwitchMockName",
unique_id="uniqueId",
data={
CONF_NAME: "TheOverSwitchMockName",
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_SWITCH,
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
CONF_CYCLE_MIN: 5,
CONF_TEMP_MIN: 15,
CONF_TEMP_MAX: 30,
"eco_temp": 17,
"comfort_temp": 18,
"boost_temp": 19,
CONF_USE_WINDOW_FEATURE: False,
CONF_USE_MOTION_FEATURE: False,
CONF_USE_POWER_FEATURE: True,
CONF_USE_PRESENCE_FEATURE: False,
CONF_HEATER: "switch.mock_switch",
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
CONF_TPI_COEF_INT: 0.3,
CONF_TPI_COEF_EXT: 0.01,
CONF_MINIMAL_ACTIVATION_DELAY: 30,
CONF_SECURITY_DELAY_MIN: 5,
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
CONF_POWER_SENSOR: "sensor.mock_power_sensor",
CONF_MAX_POWER_SENSOR: "sensor.mock_power_max_sensor",
CONF_DEVICE_POWER: 100,
CONF_PRESET_POWER: 12,
},
)
entity: ThermostatOverSwitch = await create_thermostat(
hass, entry, "climate.theoverswitchmockname"
)
assert entity
tpi_algo = entity._prop_algorithm
assert tpi_algo
await entity.async_set_hvac_mode(HVACMode.HEAT)
await entity.async_set_preset_mode(PRESET_BOOST)
assert entity.hvac_mode is HVACMode.HEAT
assert entity.preset_mode is PRESET_BOOST
assert entity.overpowering_state is None
assert entity.target_temperature == 19
# Send power mesurement
await send_power_change_event(entity, 50, datetime.now())
# Send power max mesurement
await send_max_power_change_event(entity, 300, datetime.now())
assert await entity.check_overpowering() is False
# All configuration is complete and power is < power_max
assert entity.preset_mode is PRESET_BOOST
assert entity.overpowering_state is False
# Send power max mesurement too low and HVACMode is on
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off:
await send_max_power_change_event(entity, 149, datetime.now())
assert await entity.check_overpowering() is True
# All configuration is complete and power is > power_max we switch to POWER preset
assert entity.preset_mode is PRESET_POWER
assert entity.overpowering_state is True
assert entity.target_temperature == 12
assert mock_send_event.call_count == 2
mock_send_event.assert_has_calls(
[
call.send_event(EventType.PRESET_EVENT, {"preset": PRESET_POWER}),
call.send_event(
EventType.POWER_EVENT,
{
"type": "start",
"current_power": 50,
"device_power": 100,
"current_power_max": 149,
"current_power_consumption": 100.0,
},
),
],
any_order=True,
)
assert mock_heater_on.call_count == 0
assert mock_heater_off.call_count == 1
# Send power mesurement low to unseet power preset
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off:
await send_power_change_event(entity, 48, datetime.now())
assert await entity.check_overpowering() is False
# All configuration is complete and power is < power_max, we restore previous preset
assert entity.preset_mode is PRESET_BOOST
assert entity.overpowering_state is False
assert entity.target_temperature == 19
assert mock_send_event.call_count == 2
mock_send_event.assert_has_calls(
[
call.send_event(EventType.PRESET_EVENT, {"preset": PRESET_BOOST}),
call.send_event(
EventType.POWER_EVENT,
{
"type": "end",
"current_power": 48,
"device_power": 100,
"current_power_max": 149,
},
),
],
any_order=True,
)
# No current temperature is set so the heater wont be turned on
assert mock_heater_on.call_count == 0
assert mock_heater_off.call_count == 0
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_power_management_energy_over_switch(
hass: HomeAssistant, skip_hass_states_is_state
):
"""Test the Power management energy mesurement"""
entry = MockConfigEntry(
domain=DOMAIN,
title="TheOverSwitchMockName",
unique_id="uniqueId",
data={
CONF_NAME: "TheOverSwitchMockName",
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_SWITCH,
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
CONF_CYCLE_MIN: 5,
CONF_TEMP_MIN: 15,
CONF_TEMP_MAX: 30,
"eco_temp": 17,
"comfort_temp": 18,
"boost_temp": 19,
CONF_USE_WINDOW_FEATURE: False,
CONF_USE_MOTION_FEATURE: False,
CONF_USE_POWER_FEATURE: True,
CONF_USE_PRESENCE_FEATURE: False,
CONF_HEATER: "switch.mock_switch",
CONF_HEATER_2: "switch.mock_switch2",
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
CONF_TPI_COEF_INT: 0.3,
CONF_TPI_COEF_EXT: 0.01,
CONF_MINIMAL_ACTIVATION_DELAY: 30,
CONF_SECURITY_DELAY_MIN: 5,
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
CONF_POWER_SENSOR: "sensor.mock_power_sensor",
CONF_MAX_POWER_SENSOR: "sensor.mock_power_max_sensor",
CONF_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
assert entity.total_energy == 0
assert entity.nb_underlying_entities == 2
# set temperature to 15 so that on_percent will be set
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off:
await entity.async_set_hvac_mode(HVACMode.HEAT)
await entity.async_set_preset_mode(PRESET_BOOST)
await send_temperature_change_event(entity, 15, datetime.now())
assert entity.hvac_mode is HVACMode.HEAT
assert entity.preset_mode is PRESET_BOOST
assert entity.target_temperature == 19
assert entity.current_temperature == 15
assert tpi_algo.on_percent == 1
assert entity.device_power == 100.0
assert mock_send_event.call_count == 2
assert mock_heater_on.call_count == 1
assert mock_heater_off.call_count == 0
entity.incremente_energy()
assert entity.total_energy == round(100 * 5 / 60.0, 2)
entity.incremente_energy()
assert entity.total_energy == round(2 * 100 * 5 / 60.0, 2)
# change temperature to a higher value
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off:
await send_temperature_change_event(entity, 18, datetime.now())
assert tpi_algo.on_percent == 0.3
assert entity.mean_cycle_power == 30.0
assert mock_send_event.call_count == 0
assert mock_heater_on.call_count == 0
assert mock_heater_off.call_count == 0
entity.incremente_energy()
assert round(entity.total_energy, 2) == round((2.0 + 0.3) * 100 * 5 / 60.0, 2)
entity.incremente_energy()
assert round(entity.total_energy, 2) == round((2.0 + 0.6) * 100 * 5 / 60.0, 2)
# change temperature to a much higher value so that heater will be shut down
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off:
await send_temperature_change_event(entity, 20, datetime.now())
assert tpi_algo.on_percent == 0.0
assert entity.mean_cycle_power == 0.0
assert mock_send_event.call_count == 0
assert mock_heater_on.call_count == 0
assert mock_heater_off.call_count == 0
entity.incremente_energy()
# No change on energy
assert round(entity.total_energy, 2) == round((2.0 + 0.6) * 100 * 5 / 60.0, 2)
# Still no change
entity.incremente_energy()
assert round(entity.total_energy, 2) == round((2.0 + 0.6) * 100 * 5 / 60.0, 2)
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_power_management_energy_over_climate(
hass: HomeAssistant, skip_hass_states_is_state
):
"""Test the Power management for a over_climate thermostat"""
the_mock_underlying = MagicMockClimate()
with patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
return_value=the_mock_underlying,
):
entry = MockConfigEntry(
domain=DOMAIN,
title="TheOverClimateMockName",
unique_id="uniqueId",
data={
CONF_NAME: "TheOverClimateMockName",
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_CLIMATE,
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
CONF_CYCLE_MIN: 5,
CONF_TEMP_MIN: 15,
CONF_TEMP_MAX: 30,
"eco_temp": 17,
"comfort_temp": 18,
"boost_temp": 19,
CONF_USE_WINDOW_FEATURE: False,
CONF_USE_MOTION_FEATURE: False,
CONF_USE_POWER_FEATURE: True,
CONF_USE_PRESENCE_FEATURE: False,
CONF_CLIMATE: "climate.mock_climate",
CONF_MINIMAL_ACTIVATION_DELAY: 30,
CONF_SECURITY_DELAY_MIN: 5,
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
CONF_POWER_SENSOR: "sensor.mock_power_sensor",
CONF_MAX_POWER_SENSOR: "sensor.mock_power_max_sensor",
CONF_DEVICE_POWER: 100,
CONF_PRESET_POWER: 12,
},
)
entity: ThermostatOverSwitch = await create_thermostat(
hass, entry, "climate.theoverclimatemockname"
)
assert entity
assert entity.is_over_climate
now = datetime.now(tz=get_tz(hass))
await send_temperature_change_event(entity, 15, now)
await entity.async_set_hvac_mode(HVACMode.HEAT)
await entity.async_set_preset_mode(PRESET_BOOST)
assert entity.hvac_mode is HVACMode.HEAT
assert entity.hvac_action is HVACAction.IDLE
assert entity.preset_mode is PRESET_BOOST
assert entity.target_temperature == 19
assert entity.current_temperature == 15
# Not initialised yet
assert entity.mean_cycle_power is None
assert entity._underlying_climate_start_hvac_action_date is None
# Send a climate_change event with HVACAction=HEATING
event_timestamp = now - timedelta(minutes=3)
await send_climate_change_event(
entity,
new_hvac_mode=HVACMode.HEAT,
old_hvac_mode=HVACMode.HEAT,
new_hvac_action=HVACAction.HEATING,
old_hvac_action=HVACAction.OFF,
date=event_timestamp,
)
# We have the start event and not the end event
assert (entity._underlying_climate_start_hvac_action_date - now).total_seconds() < 1
entity.incremente_energy()
assert entity.total_energy == 0
# Send a climate_change event with HVACAction=IDLE (end of heating)
await send_climate_change_event(
entity,
new_hvac_mode=HVACMode.HEAT,
old_hvac_mode=HVACMode.HEAT,
new_hvac_action=HVACAction.IDLE,
old_hvac_action=HVACAction.HEATING,
date=now,
)
# We have the end event -> we should have some power and on_percent
assert entity._underlying_climate_start_hvac_action_date is None
# 3 minutes at 100 W
assert entity.total_energy == 100 * 3.0 / 60
# Test the re-increment
entity.incremente_energy()
assert entity.total_energy == 2 * 100 * 3.0 / 60

508
tests/test_security.py Normal file
View File

@@ -0,0 +1,508 @@
# 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_climate import (
ThermostatOverClimate,
)
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_security_feature(hass: HomeAssistant, skip_hass_states_is_state):
"""Test the security feature and https://github.com/jmcollin78/versatile_thermostat/issues/49:
1. creates a thermostat and check that security is off
2. activate security feature when date is expired
3. change the preset to boost
4. check that security is still on
5. resolve the date issue
6. check that security is off and preset is changed to boost
"""
tz = get_tz(hass) # pylint: disable=invalid-name
entry = MockConfigEntry(
domain=DOMAIN,
title="TheOverSwitchMockName",
unique_id="uniqueId",
data={
"name": "TheOverSwitchMockName",
"thermostat_type": "thermostat_over_switch",
"temperature_sensor_entity_id": "sensor.mock_temp_sensor",
"external_temperature_sensor_entity_id": "sensor.mock_ext_temp_sensor",
"cycle_min": 5,
"temp_min": 15,
"temp_max": 30,
"eco_temp": 17,
"comfort_temp": 18,
"boost_temp": 19,
"use_window_feature": False,
"use_motion_feature": False,
"use_power_feature": False,
"use_presence_feature": False,
"heater_entity_id": "switch.mock_switch",
"proportional_function": "tpi",
"tpi_coef_int": 0.3,
"tpi_coef_ext": 0.01,
"minimal_activation_delay": 30,
"security_delay_min": 5, # 5 minutes
"security_min_on_percent": 0.2,
"security_default_on_percent": 0.1,
},
)
# 1. creates a thermostat and check that security is off
now: datetime = datetime.now(tz=tz)
entity: 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.preset_modes == [
PRESET_NONE,
PRESET_ECO,
PRESET_COMFORT,
PRESET_BOOST,
]
assert entity._last_ext_temperature_mesure is not None
assert entity._last_temperature_mesure is not None
assert (entity._last_temperature_mesure.astimezone(tz) - now).total_seconds() < 1
assert (
entity._last_ext_temperature_mesure.astimezone(tz) - now
).total_seconds() < 1
# set a preset
assert entity.preset_mode is PRESET_NONE
await entity.async_set_preset_mode(PRESET_COMFORT)
assert entity.preset_mode is PRESET_COMFORT
# Turn On the thermostat
assert entity.hvac_mode == HVACMode.OFF
await entity.async_set_hvac_mode(HVACMode.HEAT)
assert entity.hvac_mode == HVACMode.HEAT
# 2. activate security feature when date is expired
with patch(
"custom_components.versatile_thermostat.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 entity._saved_preset_mode == PRESET_COMFORT
assert entity._prop_algorithm.on_percent == 0.1
assert entity._prop_algorithm.calculated_on_percent == 0.9
assert mock_send_event.call_count == 3
mock_send_event.assert_has_calls(
[
call.send_event(EventType.PRESET_EVENT, {"preset": PRESET_SECURITY}),
call.send_event(
EventType.TEMPERATURE_EVENT,
{
"last_temperature_mesure": event_timestamp.isoformat(),
"last_ext_temperature_mesure": entity._last_ext_temperature_mesure.isoformat(),
"current_temp": 15,
"current_ext_temp": None,
"target_temp": 18,
},
),
call.send_event(
EventType.SECURITY_EVENT,
{
"type": "start",
"last_temperature_mesure": event_timestamp.isoformat(),
"last_ext_temperature_mesure": entity._last_ext_temperature_mesure.isoformat(),
"current_temp": 15,
"current_ext_temp": None,
"target_temp": 18,
},
),
],
any_order=True,
)
assert mock_heater_on.call_count == 1
# 3. Change the preset to Boost (we should stay in SECURITY)
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on:
await entity.async_set_preset_mode(PRESET_BOOST)
# 4. check that security is still on
assert entity._security_state is True
assert entity._prop_algorithm.on_percent == 0.1
assert entity._prop_algorithm.calculated_on_percent == 0.9
assert entity._saved_preset_mode == PRESET_BOOST
assert entity.preset_mode is PRESET_SECURITY
# 5. resolve the datetime issue
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on:
event_timestamp = datetime.now()
# set temperature to 15 so that on_percent will be > security_min_on_percent (0.2)
await send_temperature_change_event(entity, 15.2, event_timestamp)
assert entity._security_state is False
assert entity.preset_mode == PRESET_BOOST
assert entity._saved_preset_mode == PRESET_BOOST
assert entity._prop_algorithm.on_percent == 1.0
assert entity._prop_algorithm.calculated_on_percent == 1.0
assert mock_send_event.call_count == 2
mock_send_event.assert_has_calls(
[
call.send_event(EventType.PRESET_EVENT, {"preset": PRESET_BOOST}),
call.send_event(
EventType.SECURITY_EVENT,
{
"type": "end",
"last_temperature_mesure": event_timestamp.astimezone(
tz
).isoformat(),
"last_ext_temperature_mesure": entity._last_ext_temperature_mesure.astimezone(
tz
).isoformat(),
"current_temp": 15.2,
"current_ext_temp": None,
"target_temp": 19,
},
),
],
any_order=True,
)
# Heater is now on
assert mock_heater_on.call_count == 1
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_security_feature_back_on_percent(
hass: HomeAssistant, skip_hass_states_is_state
):
"""Test the security feature and https://github.com/jmcollin78/versatile_thermostat/issues/49:
1. creates a thermostat and check that security is off, preset Boost
2. change temperature so that on_percent is high
3. send next timestamp date so that security is on WITH A Eco preset that makes a on_percent low
4. this shoud resolve the date issue
4. check that security is off and preset is Boost
"""
tz = get_tz(hass) # pylint: disable=invalid-name
entry = MockConfigEntry(
domain=DOMAIN,
title="TheOverSwitchMockName",
unique_id="uniqueId",
data={
"name": "TheOverSwitchMockName",
"thermostat_type": "thermostat_over_switch",
"temperature_sensor_entity_id": "sensor.mock_temp_sensor",
"external_temperature_sensor_entity_id": "sensor.mock_ext_temp_sensor",
"cycle_min": 5,
"temp_min": 15,
"temp_max": 30,
"eco_temp": 17,
"comfort_temp": 18,
"boost_temp": 19,
"use_window_feature": False,
"use_motion_feature": False,
"use_power_feature": False,
"use_presence_feature": False,
"heater_entity_id": "switch.mock_switch",
"proportional_function": "tpi",
"tpi_coef_int": 0.3,
"tpi_coef_ext": 0.01,
"minimal_activation_delay": 30,
"security_delay_min": 5, # 5 minutes
"security_min_on_percent": 0.2,
"security_default_on_percent": 0.1,
},
)
# 1. creates a thermostat and check that security is off
now: datetime = datetime.now(tz=tz)
entity: ThermostatOverSwitch = await create_thermostat(
hass, entry, "climate.theoverswitchmockname"
)
assert entity
assert entity._security_state is False
assert entity.preset_mode is not PRESET_SECURITY
assert entity._last_ext_temperature_mesure is not None
assert entity._last_temperature_mesure is not None
assert (entity._last_temperature_mesure.astimezone(tz) - now).total_seconds() < 1
assert (
entity._last_ext_temperature_mesure.astimezone(tz) - now
).total_seconds() < 1
# set a preset
assert entity.preset_mode is PRESET_NONE
await entity.async_set_preset_mode(PRESET_BOOST)
assert entity.preset_mode is PRESET_BOOST
# Turn On the thermostat
assert entity.hvac_mode == HVACMode.OFF
await entity.async_set_hvac_mode(HVACMode.HEAT)
assert entity.hvac_mode == HVACMode.HEAT
# 2. activate on_percent
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on:
event_timestamp = now + timedelta(minutes=1)
entity._set_now(event_timestamp) # pylint: disable=protected-access
# set temperature to 17 so that on_percent will be > security_min_on_percent (0.2)
await send_temperature_change_event(entity, 17, event_timestamp)
assert entity._prop_algorithm.calculated_on_percent == 0.6
assert entity.preset_mode == PRESET_BOOST
assert entity.security_state is False
assert mock_send_event.call_count == 0
# 3. Set security mode with a preset change
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on:
# 6 min between two mesure
event_timestamp = event_timestamp + timedelta(minutes=6)
entity._set_now(event_timestamp) # pylint: disable=protected-access
await send_temperature_change_event(entity, 17, event_timestamp)
assert entity._prop_algorithm.calculated_on_percent == 0.6
assert entity.security_state is True
assert entity.preset_mode == PRESET_SECURITY
assert entity._saved_preset_mode == PRESET_BOOST
assert mock_send_event.call_count == 3
mock_send_event.assert_has_calls(
[
call.send_event(EventType.PRESET_EVENT, {"preset": PRESET_SECURITY}),
call.send_event(
EventType.TEMPERATURE_EVENT,
{
"last_temperature_mesure": event_timestamp.isoformat(),
"last_ext_temperature_mesure": entity._last_ext_temperature_mesure.isoformat(),
"current_temp": 17,
"current_ext_temp": None,
"target_temp": 19,
},
),
call.send_event(
EventType.SECURITY_EVENT,
{
"type": "start",
"last_temperature_mesure": event_timestamp.isoformat(),
"last_ext_temperature_mesure": entity._last_ext_temperature_mesure.isoformat(),
"current_temp": 17,
"current_ext_temp": None,
"target_temp": 19,
},
),
],
any_order=True,
)
# heating have been started on the previous call
assert mock_heater_on.call_count == 0
# 4. change preset so that on_percent will be low
event_timestamp = event_timestamp + timedelta(minutes=1)
entity._set_now(event_timestamp) # pylint: disable=protected-access
await entity.async_set_preset_mode(PRESET_ECO)
assert entity.security_state is True
assert entity.preset_mode == PRESET_SECURITY
# 5. resolve the datetime issue
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on:
# +2 min between two mesure
event_timestamp = event_timestamp + timedelta(minutes=2)
entity._set_now(event_timestamp) # pylint: disable=protected-access
# set temperature to 18.9 so that on_percent will be > security_min_on_percent (0.2)
await send_temperature_change_event(entity, 18.92, event_timestamp)
assert entity._security_state is False
assert entity.preset_mode == PRESET_ECO
assert entity._saved_preset_mode == PRESET_ECO
assert entity._prop_algorithm.on_percent == 0.0
assert entity._prop_algorithm.calculated_on_percent == 0.0
assert mock_send_event.call_count == 2
mock_send_event.assert_has_calls(
[
call.send_event(EventType.PRESET_EVENT, {"preset": PRESET_ECO}),
call.send_event(
EventType.SECURITY_EVENT,
{
"type": "end",
"last_temperature_mesure": event_timestamp.astimezone(
tz
).isoformat(),
"last_ext_temperature_mesure": entity._last_ext_temperature_mesure.astimezone(
tz
).isoformat(),
"current_temp": 18.92,
"current_ext_temp": None,
"target_temp": 17,
},
),
],
any_order=True,
)
# Heater is stays off
assert mock_heater_on.call_count == 0
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_security_over_climate(
hass: HomeAssistant,
skip_hass_states_is_state,
skip_turn_on_off_heater,
skip_send_event,
):
"""Test that when a underlying climate is not available the VTherm doesn't go into security mode"""
tz = get_tz(hass) # pylint: disable=invalid-name
now: datetime = datetime.now(tz=tz)
entry = MockConfigEntry(
domain=DOMAIN,
title="TheOverClimateMockName",
unique_id="uniqueId",
data=PARTIAL_CLIMATE_CONFIG, # 5 minutes security delay
)
fake_underlying_climate = MockClimate(
hass, "mockUniqueId", "MockClimateName", {}, HVACMode.HEAT, 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:
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")
assert entity
assert entity.name == "TheOverClimateMockName"
assert entity.is_over_climate is True
# Because the underlying is HEATING. In real life the underlying will be shut-off
assert entity.hvac_action is HVACAction.HEATING
assert entity.hvac_mode is HVACMode.OFF
assert entity.target_temperature == entity.min_temp
assert entity.preset_modes == [
PRESET_NONE,
PRESET_ECO,
PRESET_COMFORT,
PRESET_BOOST,
]
assert entity.preset_mode is PRESET_NONE
assert entity._security_state is False
# should have been called with EventType.PRESET_EVENT and EventType.HVAC_MODE_EVENT
assert mock_send_event.call_count == 2
mock_send_event.assert_has_calls(
[
# At startup
call.send_event(EventType.PRESET_EVENT, {"preset": PRESET_NONE}),
call.send_event(
EventType.HVAC_MODE_EVENT,
{"hvac_mode": HVACMode.OFF},
),
]
)
assert mock_find_climate.call_count == 1
assert mock_find_climate.mock_calls[0] == call()
mock_find_climate.assert_has_calls([call.find_underlying_entity()])
# Force security mode
assert entity._last_ext_temperature_mesure is not None
assert entity._last_temperature_mesure is not None
assert (
entity._last_temperature_mesure.astimezone(tz) - now
).total_seconds() < 1
assert (
entity._last_ext_temperature_mesure.astimezone(tz) - now
).total_seconds() < 1
# Tries to turns on the Thermostat
await entity.async_set_hvac_mode(HVACMode.HEAT)
assert entity.hvac_mode == HVACMode.HEAT
# One call more
assert mock_send_event.call_count == 3
mock_send_event.assert_has_calls(
[
call.send_event(
EventType.HVAC_MODE_EVENT,
{"hvac_mode": HVACMode.HEAT},
),
]
)
# 2. activate security feature when date is expired
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
):
event_timestamp = now - timedelta(minutes=6)
await send_temperature_change_event(entity, 15, event_timestamp)
# Should stay False because a climate is never in security mode
assert entity.security_state is False
assert entity.preset_mode == "none"
assert entity._saved_preset_mode == "none"

385
tests/test_sensors.py Normal file
View File

@@ -0,0 +1,385 @@
# pylint: disable=wildcard-import, unused-wildcard-import, protected-access, unused-argument, line-too-long
""" Test the normal start of a Thermostat """
from datetime import timedelta, datetime
from homeassistant.core import HomeAssistant
from homeassistant.components.climate import HVACMode
from homeassistant.components.sensor import (
SensorDeviceClass,
SensorStateClass,
)
from homeassistant.const import UnitOfTime, UnitOfPower, UnitOfEnergy, PERCENTAGE
from pytest_homeassistant_custom_component.common import MockConfigEntry
from custom_components.versatile_thermostat.base_thermostat import BaseThermostat
from custom_components.versatile_thermostat.sensor import (
EnergySensor,
MeanPowerSensor,
OnPercentSensor,
OnTimeSensor,
OffTimeSensor,
LastTemperatureSensor,
LastExtTemperatureSensor,
)
from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_sensors_over_switch(
hass: HomeAssistant,
skip_hass_states_is_state,
skip_turn_on_off_heater,
skip_send_event,
):
"""Test the sensors with a thermostat avec switch type"""
entry = MockConfigEntry(
domain=DOMAIN,
title="TheOverSwitchMockName",
unique_id="uniqueId",
data={
CONF_NAME: "TheOverSwitchMockName",
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_SWITCH,
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
CONF_CYCLE_MIN: 5,
CONF_TEMP_MIN: 15,
CONF_TEMP_MAX: 30,
"eco_temp": 17,
"comfort_temp": 18,
"boost_temp": 19,
CONF_USE_WINDOW_FEATURE: False,
CONF_USE_MOTION_FEATURE: False,
CONF_USE_POWER_FEATURE: False,
CONF_USE_PRESENCE_FEATURE: False,
CONF_HEATER: "switch.mock_switch",
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
CONF_TPI_COEF_INT: 0.3,
CONF_TPI_COEF_EXT: 0.01,
CONF_MINIMAL_ACTIVATION_DELAY: 30,
CONF_SECURITY_DELAY_MIN: 5,
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
CONF_DEVICE_POWER: 200,
},
)
entity: BaseThermostat = await create_thermostat(
hass, entry, "climate.theoverswitchmockname"
)
assert entity
energy_sensor: EnergySensor = search_entity(
hass, "sensor.theoverswitchmockname_energy", "sensor"
)
assert energy_sensor
mean_power_sensor: MeanPowerSensor = search_entity(
hass, "sensor.theoverswitchmockname_mean_power_cycle", "sensor"
)
assert mean_power_sensor
on_percent_sensor: OnPercentSensor = search_entity(
hass, "sensor.theoverswitchmockname_power_percent", "sensor"
)
assert on_percent_sensor
on_time_sensor: OnTimeSensor = search_entity(
hass, "sensor.theoverswitchmockname_on_time", "sensor"
)
assert on_time_sensor
off_time_sensor: OffTimeSensor = search_entity(
hass, "sensor.theoverswitchmockname_off_time", "sensor"
)
assert off_time_sensor
last_temperature_sensor: LastTemperatureSensor = search_entity(
hass, "sensor.theoverswitchmockname_last_temperature_date", "sensor"
)
assert last_temperature_sensor
last_ext_temperature_sensor: LastExtTemperatureSensor = search_entity(
hass, "sensor.theoverswitchmockname_last_external_temperature_date", "sensor"
)
assert last_ext_temperature_sensor
# Simulate the event reception
await energy_sensor.async_my_climate_changed()
assert energy_sensor.state == 0.0
await mean_power_sensor.async_my_climate_changed()
assert mean_power_sensor.state == 0.0
await on_percent_sensor.async_my_climate_changed()
assert on_percent_sensor.state == 0.0
await on_time_sensor.async_my_climate_changed()
assert on_time_sensor.state == 0.0
await off_time_sensor.async_my_climate_changed()
assert off_time_sensor.state == 300.0
await last_temperature_sensor.async_my_climate_changed()
assert last_temperature_sensor.state is not None
await last_ext_temperature_sensor.async_my_climate_changed()
assert last_ext_temperature_sensor.state is not None
last_temp_date = last_temperature_sensor.state
tz = get_tz(hass) # pylint: disable=invalid-name
now: datetime = datetime.now(tz=tz)
event_timestamp = now - timedelta(minutes=1)
# Start the heater to get some values
await entity.async_set_preset_mode(PRESET_COMFORT)
await entity.async_set_hvac_mode(HVACMode.HEAT)
await send_temperature_change_event(entity, 15, event_timestamp)
await send_ext_temperature_change_event(entity, 5, event_timestamp)
entity.incremente_energy()
await energy_sensor.async_my_climate_changed()
assert energy_sensor.state == round(16.667, 2)
assert energy_sensor.device_class == SensorDeviceClass.ENERGY
assert energy_sensor.state_class == SensorStateClass.TOTAL_INCREASING
# because device_power is 200
assert energy_sensor.unit_of_measurement == UnitOfEnergy.WATT_HOUR
await mean_power_sensor.async_my_climate_changed()
assert mean_power_sensor.state == 200.0
assert mean_power_sensor.device_class == SensorDeviceClass.POWER
assert mean_power_sensor.state_class == SensorStateClass.MEASUREMENT
# because device_power is 200
assert mean_power_sensor.unit_of_measurement == UnitOfPower.WATT
await on_percent_sensor.async_my_climate_changed()
assert on_percent_sensor.state == 100.0
assert on_percent_sensor.unit_of_measurement == PERCENTAGE
await on_time_sensor.async_my_climate_changed()
assert on_time_sensor.state == 300.0
assert on_time_sensor.device_class == SensorDeviceClass.DURATION
assert on_time_sensor.state_class == SensorStateClass.MEASUREMENT
assert on_time_sensor.unit_of_measurement == UnitOfTime.SECONDS
await off_time_sensor.async_my_climate_changed()
assert off_time_sensor.state == 0.0
assert off_time_sensor.device_class == SensorDeviceClass.DURATION
assert off_time_sensor.state_class == SensorStateClass.MEASUREMENT
assert off_time_sensor.unit_of_measurement == UnitOfTime.SECONDS
await last_temperature_sensor.async_my_climate_changed()
assert (
last_temperature_sensor.state is not None
and last_temperature_sensor.state != last_temp_date
)
assert last_temperature_sensor.device_class == SensorDeviceClass.TIMESTAMP
await last_ext_temperature_sensor.async_my_climate_changed()
assert (
last_ext_temperature_sensor.state is not None
and last_ext_temperature_sensor.state != last_temp_date
)
assert last_ext_temperature_sensor.device_class == SensorDeviceClass.TIMESTAMP
cancel_switchs_cycles(entity)
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_sensors_over_climate(
hass: HomeAssistant,
skip_hass_states_is_state,
skip_turn_on_off_heater,
skip_send_event,
):
"""Test the sensors with thermostat over climate type"""
the_mock_underlying = MagicMockClimate()
with patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
return_value=the_mock_underlying,
):
entry = MockConfigEntry(
domain=DOMAIN,
title="TheOverClimateMockName",
unique_id="uniqueId",
data={
CONF_NAME: "TheOverClimateMockName",
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_CLIMATE,
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
CONF_CYCLE_MIN: 5,
CONF_TEMP_MIN: 15,
CONF_TEMP_MAX: 30,
"eco_temp": 17,
"comfort_temp": 18,
"boost_temp": 19,
CONF_USE_WINDOW_FEATURE: False,
CONF_USE_MOTION_FEATURE: False,
CONF_USE_POWER_FEATURE: True,
CONF_USE_PRESENCE_FEATURE: False,
CONF_CLIMATE: "climate.mock_climate",
CONF_MINIMAL_ACTIVATION_DELAY: 30,
CONF_SECURITY_DELAY_MIN: 5,
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
CONF_POWER_SENSOR: "sensor.mock_power_sensor",
CONF_MAX_POWER_SENSOR: "sensor.mock_power_max_sensor",
CONF_DEVICE_POWER: 1.5,
CONF_PRESET_POWER: 12,
},
)
entity: BaseThermostat = await create_thermostat(
hass, entry, "climate.theoverclimatemockname"
)
assert entity
assert entity.is_over_climate
energy_sensor: EnergySensor = search_entity(
hass, "sensor.theoverclimatemockname_energy", "sensor"
)
assert energy_sensor
last_temperature_sensor: LastTemperatureSensor = search_entity(
hass, "sensor.theoverclimatemockname_last_temperature_date", "sensor"
)
assert last_temperature_sensor
last_ext_temperature_sensor: LastExtTemperatureSensor = search_entity(
hass, "sensor.theoverclimatemockname_last_external_temperature_date", "sensor"
)
assert last_ext_temperature_sensor
# Simulate the event reception
await energy_sensor.async_my_climate_changed()
assert energy_sensor.state == 0.0
await last_temperature_sensor.async_my_climate_changed()
assert last_temperature_sensor.state is not None
await last_ext_temperature_sensor.async_my_climate_changed()
assert last_ext_temperature_sensor.state is not None
last_temp_date = last_temperature_sensor.state
tz = get_tz(hass) # pylint: disable=invalid-name
now: datetime = datetime.now(tz=tz)
event_timestamp = now - timedelta(minutes=1)
# Start the heater to get some values
await entity.async_set_preset_mode(PRESET_COMFORT)
await entity.async_set_hvac_mode(HVACMode.HEAT)
await send_temperature_change_event(entity, 15, event_timestamp)
await send_ext_temperature_change_event(entity, 5, event_timestamp)
# to add energy we must have HVACAction underlying climate event
# Send a climate_change event with HVACAction=HEATING
event_timestamp = now - timedelta(minutes=60)
await send_climate_change_event(
entity,
new_hvac_mode=HVACMode.HEAT,
old_hvac_mode=HVACMode.HEAT,
new_hvac_action=HVACAction.HEATING,
old_hvac_action=HVACAction.OFF,
date=event_timestamp,
)
# Send a climate_change event with HVACAction=IDLE (end of heating)
await send_climate_change_event(
entity,
new_hvac_mode=HVACMode.HEAT,
old_hvac_mode=HVACMode.HEAT,
new_hvac_action=HVACAction.IDLE,
old_hvac_action=HVACAction.HEATING,
date=now,
)
# 60 minutes heating with 1.5 kW heating -> 1.5 kWh
await energy_sensor.async_my_climate_changed()
assert energy_sensor.state == 1.5
assert energy_sensor.device_class == SensorDeviceClass.ENERGY
assert energy_sensor.state_class == SensorStateClass.TOTAL_INCREASING
# because device_power is 1.5 kW
assert energy_sensor.unit_of_measurement == UnitOfEnergy.KILO_WATT_HOUR
entity.incremente_energy()
await energy_sensor.async_my_climate_changed()
assert energy_sensor.state == 3.0
await last_temperature_sensor.async_my_climate_changed()
assert (
last_temperature_sensor.state is not None
and last_temperature_sensor.state != last_temp_date
)
assert last_temperature_sensor.device_class == SensorDeviceClass.TIMESTAMP
await last_ext_temperature_sensor.async_my_climate_changed()
assert (
last_ext_temperature_sensor.state is not None
and last_ext_temperature_sensor.state != last_temp_date
)
assert last_ext_temperature_sensor.device_class == SensorDeviceClass.TIMESTAMP
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_sensors_over_climate_minimal(
hass: HomeAssistant,
skip_hass_states_is_state,
skip_turn_on_off_heater,
skip_send_event,
):
"""Test the sensors with thermostat over climate type"""
the_mock_underlying = MagicMockClimate()
with patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
return_value=the_mock_underlying,
):
entry = MockConfigEntry(
domain=DOMAIN,
title="TheOverClimateMockName",
unique_id="uniqueId",
data={
CONF_NAME: "TheOverClimateMockName",
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_CLIMATE,
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
CONF_CYCLE_MIN: 5,
CONF_TEMP_MIN: 15,
CONF_TEMP_MAX: 30,
"eco_temp": 17,
"comfort_temp": 18,
"boost_temp": 19,
CONF_USE_WINDOW_FEATURE: False,
CONF_USE_MOTION_FEATURE: False,
CONF_USE_POWER_FEATURE: 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
assert entity.is_over_climate
energy_sensor: EnergySensor = search_entity(
hass, "sensor.theoverclimatemockname_energy", "sensor"
)
assert energy_sensor is None
last_temperature_sensor: LastTemperatureSensor = search_entity(
hass, "sensor.theoverclimatemockname_last_temperature_date", "sensor"
)
assert last_temperature_sensor
last_ext_temperature_sensor: LastExtTemperatureSensor = search_entity(
hass, "sensor.theoverclimatemockname_last_external_temperature_date", "sensor"
)
assert last_ext_temperature_sensor

226
tests/test_start.py Normal file
View File

@@ -0,0 +1,226 @@
# pylint: disable=wildcard-import, unused-wildcard-import, protected-access, unused-argument, line-too-long
""" Test the normal start of a Thermostat """
from unittest.mock import patch, call
from homeassistant.core import HomeAssistant
from homeassistant.components.climate import HVACAction, HVACMode
from homeassistant.config_entries import ConfigEntryState
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.components.climate import ClimateEntity, DOMAIN as CLIMATE_DOMAIN
from pytest_homeassistant_custom_component.common import MockConfigEntry
from custom_components.versatile_thermostat.base_thermostat import BaseThermostat
from custom_components.versatile_thermostat.thermostat_climate import ThermostatOverClimate
from custom_components.versatile_thermostat.thermostat_switch import ThermostatOverSwitch
from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_over_switch_full_start(hass: HomeAssistant, skip_hass_states_is_state):
"""Test the normal full start of a thermostat in thermostat_over_switch type"""
entry = MockConfigEntry(
domain=DOMAIN,
title="TheOverSwitchMockName",
unique_id="uniqueId",
data=FULL_SWITCH_CONFIG,
)
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")
assert entity
assert isinstance(entity, ThermostatOverSwitch)
assert entity.name == "TheOverSwitchMockName"
assert entity.is_over_climate is False
assert entity.hvac_action is HVACAction.OFF
assert entity.hvac_mode is HVACMode.OFF
assert entity.target_temperature == entity.min_temp
assert entity.preset_modes == [
PRESET_NONE,
PRESET_ECO,
PRESET_COMFORT,
PRESET_BOOST,
PRESET_ACTIVITY,
]
assert entity.preset_mode is PRESET_NONE
assert entity._security_state is False
assert entity._window_state is None
assert entity._motion_state is None
assert entity._presence_state is None
assert entity._prop_algorithm is not 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},
),
]
)
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_over_climate_full_start(hass: HomeAssistant, skip_hass_states_is_state):
"""Test the normal full start of a thermostat in thermostat_over_climate type"""
entry = MockConfigEntry(
domain=DOMAIN,
title="TheOverClimateMockName",
unique_id="uniqueId",
data=PARTIAL_CLIMATE_CONFIG,
)
fake_underlying_climate = MockClimate(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 isinstance(entity, ThermostatOverClimate)
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.target_temperature == entity.min_temp
assert entity.preset_modes == [
PRESET_NONE,
PRESET_ECO,
PRESET_COMFORT,
PRESET_BOOST,
]
assert entity.preset_mode is PRESET_NONE
assert entity._security_state is False
assert entity._window_state is None
assert entity._motion_state is None
assert entity._presence_state is None
# should have been called with EventType.PRESET_EVENT and EventType.HVAC_MODE_EVENT
assert mock_send_event.call_count == 2
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()])
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_over_4switch_full_start(hass: HomeAssistant, skip_hass_states_is_state):
"""Test the normal full start of a thermostat in thermostat_over_switch with 4 switches type"""
entry = MockConfigEntry(
domain=DOMAIN,
title="TheOver4SwitchMockName",
unique_id="uniqueId",
data=FULL_4SWITCH_CONFIG,
)
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")
assert entity
assert entity.name == "TheOver4SwitchMockName"
assert entity.is_over_climate is False
assert entity.hvac_action is HVACAction.OFF
assert entity.hvac_mode is HVACMode.OFF
assert entity.target_temperature == entity.min_temp
assert entity.preset_modes == [
PRESET_NONE,
PRESET_ECO,
PRESET_COMFORT,
PRESET_BOOST,
PRESET_ACTIVITY,
]
assert entity.preset_mode is PRESET_NONE
assert entity._security_state is False
assert entity._window_state is None
assert entity._motion_state is None
assert entity._presence_state is None
assert entity._prop_algorithm is not None
assert entity.nb_underlying_entities == 4
# Checks that we have the 4 UnderlyingEntity correctly configured
for idx in range(4):
under = entity.underlying_entity(idx)
assert under is not None
assert isinstance(under, UnderlyingSwitch)
assert under.entity_id == "switch.mock_4switch" + str(idx)
assert under.initial_delay_sec == 8 * 60 / 4 * idx
# 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},
),
]
)

159
tests/test_switch_ac.py Normal file
View File

@@ -0,0 +1,159 @@
""" Test the normal start of a Switch AC Thermostat """
from unittest.mock import patch, call
from datetime import datetime, timedelta
from homeassistant.core import HomeAssistant
from homeassistant.components.climate import HVACAction, HVACMode
from homeassistant.config_entries import ConfigEntryState
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.components.climate import ClimateEntity, DOMAIN as CLIMATE_DOMAIN
from pytest_homeassistant_custom_component.common import MockConfigEntry
from custom_components.versatile_thermostat.base_thermostat import BaseThermostat
from custom_components.versatile_thermostat.thermostat_switch import ThermostatOverSwitch
from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_over_switch_ac_full_start(hass: HomeAssistant, skip_hass_states_is_state): # pylint: disable=unused-argument
"""Test the normal full start of a thermostat in thermostat_over_switch type"""
entry = MockConfigEntry(
domain=DOMAIN,
title="TheOverSwitchACMockName",
unique_id="uniqueId",
data=FULL_SWITCH_AC_CONFIG,
)
tz = get_tz(hass) # pylint: disable=invalid-name
now: datetime = datetime.now(tz=tz)
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) 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: BaseThermostat = find_my_entity("climate.theoverswitchmockname")
assert entity
assert isinstance(entity, ThermostatOverSwitch)
assert entity.name == "TheOverSwitchMockName"
assert entity.is_over_climate is False # pylint: disable=protected-access
assert entity.ac_mode is True
assert entity.hvac_action is HVACAction.OFF
assert entity.hvac_mode is HVACMode.OFF
assert entity.hvac_modes == [HVACMode.HEAT, HVACMode.COOL, HVACMode.OFF]
assert entity.target_temperature == entity.max_temp
assert entity.preset_modes == [
PRESET_NONE,
PRESET_ECO,
PRESET_COMFORT,
PRESET_BOOST,
PRESET_ACTIVITY,
]
assert entity.preset_mode is PRESET_NONE
assert entity._security_state is False # pylint: disable=protected-access
assert entity._window_state is None # pylint: disable=protected-access
assert entity._motion_state is None # pylint: disable=protected-access
assert entity._presence_state is None # pylint: disable=protected-access
assert entity._prop_algorithm is not None # pylint: disable=protected-access
# 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},
),
]
)
# Select a hvacmode, presence and preset
await entity.async_set_hvac_mode(HVACMode.COOL)
assert entity.hvac_mode is HVACMode.COOL
event_timestamp = now - timedelta(minutes=4)
await send_presence_change_event(entity, True, False, event_timestamp)
assert entity._presence_state == STATE_ON # pylint: disable=protected-access
await entity.async_set_hvac_mode(HVACMode.COOL)
assert entity.hvac_mode is HVACMode.COOL
await entity.async_set_preset_mode(PRESET_COMFORT)
assert entity.preset_mode is PRESET_COMFORT
assert entity.target_temperature == 23
# switch to Eco
await entity.async_set_preset_mode(PRESET_ECO)
assert entity.preset_mode is PRESET_ECO
assert entity.target_temperature == 25
# Unset the presence
event_timestamp = now - timedelta(minutes=3)
await send_presence_change_event(entity, False, True, event_timestamp)
assert entity._presence_state == STATE_OFF # pylint: disable=protected-access
assert entity.target_temperature == 27 # eco_ac_away
# Open a window
with patch(
"homeassistant.helpers.condition.state", return_value=True
):
event_timestamp = now - timedelta(minutes=2)
try_condition = await send_window_change_event(entity, True, False, event_timestamp)
# Confirme the window event
await try_condition(None)
assert entity.hvac_mode is HVACMode.OFF
assert entity.hvac_action is HVACAction.OFF
assert entity.target_temperature == 16 # eco_ac_away
# Close a window
with patch(
"homeassistant.helpers.condition.state", return_value=True
):
event_timestamp = now - timedelta(minutes=2)
try_condition = await send_window_change_event(entity, False, True, event_timestamp)
# Confirme the window event
await try_condition(None)
assert entity.hvac_mode is HVACMode.COOL
assert (entity.hvac_action is HVACAction.OFF or entity.hvac_action is HVACAction.IDLE)
assert entity.target_temperature == 27 # eco_ac_away
await entity.async_set_hvac_mode(HVACMode.HEAT)
assert entity.hvac_mode is HVACMode.HEAT
# switch to comfort
await entity.async_set_preset_mode(PRESET_COMFORT)
assert entity.preset_mode is PRESET_COMFORT
assert entity.target_temperature == 17
# switch to Eco
await entity.async_set_preset_mode(PRESET_ECO)
assert entity.preset_mode is PRESET_ECO
assert entity.target_temperature == 16
# switch to boost
await entity.async_set_preset_mode(PRESET_BOOST)
assert entity.preset_mode is PRESET_BOOST
assert entity.target_temperature == 18

104
tests/test_tpi.py Normal file
View File

@@ -0,0 +1,104 @@
""" Test the TPI algorithm """
from custom_components.versatile_thermostat.base_thermostat import BaseThermostat
from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_tpi_calculation(
hass: HomeAssistant, skip_hass_states_is_state: None
): # pylint: disable=unused-argument
"""Test the TPI calculation"""
entry = MockConfigEntry(
domain=DOMAIN,
title="TheOverSwitchMockName",
unique_id="uniqueId",
data={
CONF_NAME: "TheOverSwitchMockName",
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_SWITCH,
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
CONF_CYCLE_MIN: 5,
CONF_TEMP_MIN: 15,
CONF_TEMP_MAX: 30,
CONF_USE_WINDOW_FEATURE: False,
CONF_USE_MOTION_FEATURE: False,
CONF_USE_POWER_FEATURE: False,
CONF_USE_PRESENCE_FEATURE: False,
CONF_HEATER: "switch.mock_switch",
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
CONF_TPI_COEF_INT: 0.3,
CONF_TPI_COEF_EXT: 0.01,
CONF_MINIMAL_ACTIVATION_DELAY: 30,
CONF_SECURITY_DELAY_MIN: 5,
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
# CONF_DEVICE_POWER: 100,
},
)
entity: BaseThermostat = await create_thermostat(
hass, entry, "climate.theoverswitchmockname"
)
assert entity
tpi_algo = entity._prop_algorithm # pylint: disable=protected-access
assert tpi_algo
tpi_algo.calculate(15, 10, 7)
assert tpi_algo.on_percent == 1
assert tpi_algo.calculated_on_percent == 1
assert tpi_algo.on_time_sec == 300
assert tpi_algo.off_time_sec == 0
assert entity.mean_cycle_power is None # no device power configured
tpi_algo.calculate(15, 14, 5, False)
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)
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)
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)
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)
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)
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.set_security(0.09)
tpi_algo.calculate(25, 30, 35, True)
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

259
tests/test_valve.py Normal file
View File

@@ -0,0 +1,259 @@
# pylint: disable=line-too-long
""" Test the normal start of a Switch AC Thermostat """
from unittest.mock import patch, call
from datetime import datetime, timedelta
from homeassistant.core import HomeAssistant
from homeassistant.components.climate import HVACAction, HVACMode
from homeassistant.config_entries import ConfigEntryState
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.components.climate import ClimateEntity, DOMAIN as CLIMATE_DOMAIN
from pytest_homeassistant_custom_component.common import MockConfigEntry
from custom_components.versatile_thermostat.thermostat_valve import ThermostatOverValve
from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_over_valve_full_start(hass: HomeAssistant, skip_hass_states_is_state): # pylint: disable=unused-argument
"""Test the normal full start of a thermostat in thermostat_over_switch type"""
entry = MockConfigEntry(
domain=DOMAIN,
title="TheOverValveMockName",
unique_id="uniqueId",
data={
CONF_NAME: "TheOverValveMockName",
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_VALVE,
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
CONF_VALVE: "number.mock_valve",
CONF_CYCLE_MIN: 5,
CONF_TEMP_MIN: 15,
CONF_TEMP_MAX: 30,
PRESET_ECO + "_temp": 17,
PRESET_COMFORT + "_temp": 19,
PRESET_BOOST + "_temp": 21,
CONF_USE_WINDOW_FEATURE: True,
CONF_USE_MOTION_FEATURE: True,
CONF_USE_POWER_FEATURE: True,
CONF_USE_PRESENCE_FEATURE: True,
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
CONF_TPI_COEF_INT: 0.3,
CONF_TPI_COEF_EXT: 0.01,
CONF_MOTION_SENSOR: "input_boolean.motion_sensor",
CONF_WINDOW_SENSOR: "binary_sensor.window_sensor",
CONF_WINDOW_DELAY: 10,
CONF_MOTION_DELAY: 10,
CONF_MOTION_OFF_DELAY: 30,
CONF_MOTION_PRESET: PRESET_COMFORT,
CONF_NO_MOTION_PRESET: PRESET_ECO,
CONF_POWER_SENSOR: "sensor.power_sensor",
CONF_MAX_POWER_SENSOR: "sensor.power_max_sensor",
CONF_PRESENCE_SENSOR: "person.presence_sensor",
PRESET_ECO + PRESET_AWAY_SUFFIX + "_temp": 17.1,
PRESET_COMFORT + PRESET_AWAY_SUFFIX + "_temp": 17.2,
PRESET_BOOST + PRESET_AWAY_SUFFIX + "_temp": 17.3,
CONF_PRESET_POWER: 10,
CONF_MINIMAL_ACTIVATION_DELAY: 30,
CONF_SECURITY_DELAY_MIN: 5,
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
CONF_DEVICE_POWER: 100,
CONF_AC_MODE: False
},
)
tz = get_tz(hass) # pylint: disable=invalid-name
now: datetime = datetime.now(tz=tz)
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) 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")
assert entity
assert isinstance(entity, ThermostatOverValve)
assert entity.name == "TheOverValveMockName"
assert entity.is_over_climate is False
assert entity.is_over_switch is False
assert entity.is_over_valve is True
assert entity.ac_mode is False
assert entity.hvac_mode is HVACMode.OFF
assert entity.hvac_action is HVACAction.OFF
assert entity.hvac_modes == [HVACMode.HEAT, HVACMode.OFF]
assert entity.target_temperature == entity.min_temp
assert entity.preset_modes == [
PRESET_NONE,
PRESET_ECO,
PRESET_COMFORT,
PRESET_BOOST,
PRESET_ACTIVITY,
]
assert entity.preset_mode is PRESET_NONE
assert entity._security_state is False # pylint: disable=protected-access
assert entity._window_state is None # pylint: disable=protected-access
assert entity._motion_state is None # pylint: disable=protected-access
assert entity._presence_state is None # pylint: disable=protected-access
assert entity._prop_algorithm is not None # pylint: disable=protected-access
# 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},
),
]
)
# 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:
# Select a hvacmode, presence and preset
await entity.async_set_hvac_mode(HVACMode.HEAT)
#
assert entity.hvac_mode is HVACMode.HEAT
# No heating now
assert entity.valve_open_percent == 0
assert entity.hvac_action == HVACAction.IDLE
assert mock_send_event.call_count == 1
mock_send_event.assert_has_calls(
[
call.send_event(
EventType.HVAC_MODE_EVENT,
{"hvac_mode": HVACMode.HEAT},
),
]
)
# set manual target temp
await entity.async_set_temperature(temperature=18)
assert entity.preset_mode == PRESET_NONE # Manual mode
assert entity.target_temperature == 18
# Nothing have changed cause we don't have room and external temperature
assert mock_send_event.call_count == 1
# Set temperature and external temperature
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event, patch(
"homeassistant.core.ServiceRegistry.async_call"
) as mock_service_call, patch(
"homeassistant.core.StateMachine.get", return_value=State(entity_id="number.mock_valve", state="90")
):
# Change temperature
event_timestamp = now - timedelta(minutes=10)
await send_temperature_change_event(entity, 15, datetime.now())
assert entity.valve_open_percent == 90
await send_ext_temperature_change_event(entity, 10, datetime.now())
# Should heating strongly now
assert entity.valve_open_percent == 98
assert entity.is_device_active is True
assert entity.hvac_action == HVACAction.HEATING
assert mock_service_call.call_count == 2
mock_service_call.assert_has_calls([
call.async_call('number', 'set_value', {'entity_id': 'number.mock_valve', 'value': 90}),
call.async_call('number', 'set_value', {'entity_id': 'number.mock_valve', 'value': 98})
])
assert mock_send_event.call_count == 0
# Change to preset Comfort
await entity.async_set_preset_mode(preset_mode=PRESET_COMFORT)
assert entity.preset_mode == PRESET_COMFORT
assert entity.target_temperature == 17.2
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)
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
assert entity.target_temperature == 19
assert entity.valve_open_percent == 100 # Full heating
assert entity.is_device_active is True
assert entity.hvac_action == HVACAction.HEATING
# Change internal temperature
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event, patch(
"homeassistant.core.ServiceRegistry.async_call"
) as mock_service_call, patch(
"homeassistant.core.StateMachine.get", return_value=0
):
event_timestamp = now - timedelta(minutes=3)
await send_temperature_change_event(entity, 20, datetime.now())
assert entity.valve_open_percent == 0
assert entity.is_device_active is False
assert entity.hvac_action == HVACAction.IDLE
await send_temperature_change_event(entity, 17, datetime.now())
# switch to Eco
await entity.async_set_preset_mode(PRESET_ECO)
assert entity.preset_mode is PRESET_ECO
assert entity.target_temperature == 17
assert entity.valve_open_percent == 7
# Unset the presence
event_timestamp = now - timedelta(minutes=2)
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
assert entity.target_temperature == 17.1 # eco_away
assert entity.is_device_active is True
assert entity.hvac_action == HVACAction.HEATING
# Open a window
with patch(
"homeassistant.helpers.condition.state", return_value=True
):
event_timestamp = now - timedelta(minutes=1)
try_condition = await send_window_change_event(entity, True, False, event_timestamp)
# Confirme the window event
await try_condition(None)
assert entity.hvac_mode is HVACMode.OFF
assert entity.hvac_action is HVACAction.OFF
assert entity.target_temperature == 17.1 # eco
assert entity.valve_open_percent == 0
# Close a window
with patch(
"homeassistant.helpers.condition.state", return_value=True
):
event_timestamp = now - timedelta(minutes=0)
try_condition = await send_window_change_event(entity, False, True, event_timestamp)
# Confirme the window event
await try_condition(None)
assert entity.hvac_mode is HVACMode.HEAT
assert (entity.hvac_action is HVACAction.OFF or entity.hvac_action is HVACAction.IDLE)
assert entity.target_temperature == 17.1 # eco
assert entity.valve_open_percent == 10

1083
tests/test_window.py Normal file

File diff suppressed because it is too large Load Diff