Compare commits

...

104 Commits

Author SHA1 Message Date
Jean-Marc Collin
513e65f79c Add events in README 2024-01-14 16:50:59 +00:00
Jean-Marc Collin
31f782ffa3 Documentation and release. 2024-01-14 16:44:38 +00:00
Jean-Marc Collin
2831257732 Full featured but without testu 2024-01-14 12:37:09 +00:00
Jean-Marc Collin
0343d0f0e8 Fonctional before testu. Miss the service call 2024-01-14 00:33:52 +00:00
Jean-Marc Collin
208c80752c Creation of the central boiler config + binary_sensor entity 2024-01-13 17:46:41 +00:00
Jean-Marc Collin
e8bb465b43 Try to fix issue #334 - loop when underlying is late to update 2024-01-13 11:30:11 +00:00
Jean-Marc Collin
d7ec6770c4 Update version manifest.json 2024-01-12 12:20:35 +01:00
Jean-Marc Collin
51428aa875 Issue #324 - (re) 2024-01-09 20:25:10 +00:00
Jean-Marc Collin
6ea6fe8542 Issue #324 - don't use window auto detection is sensor is given 2024-01-09 20:24:45 +00:00
misa1515
a18d10fa3f Update sk.json (#322)
* Update sk.json

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

* Fix interaction with window

* FIX complex scenario

* pylint warning

* Release

* Issue #306

* Issue #306

---------

Co-authored-by: Jean-Marc Collin <jean-marc.collin-extern@renault.com>
2024-01-03 17:52:34 +01:00
Jean-Marc Collin
c222feda1a Issue #295 - No floating point value for target temp 2024-01-01 16:55:41 +00:00
Jean-Marc Collin
d05df021ab Beer from Lajull 2023-12-24 09:07:59 +00:00
Jean-Marc Collin
27a267139f FIX #159 - Doesn't send target temp if VTherm is off 2023-12-20 19:06:34 +00:00
Jean-Marc Collin
707f40d406 FIX issue #284 - preset not saved 2023-12-20 18:54:35 +00:00
Jean-Marc Collin
a01f5770d9 FIX issue #272 and #24ç - min and max values depending of the underlying 2023-12-19 19:39:33 +00:00
Jean-Marc Collin
04d0b28f1d Issue #280 - enable to use central config for window configuration 2023-12-18 21:39:58 +00:00
Jean-Marc Collin
30c3418f1b Issue #281 - cannot use central config at first integration installation 2023-12-18 20:54:39 +00:00
Jean-Marc Collin
efb8ce257d Beers from @Mexx62, @Someone 2023-12-18 20:16:37 +00:00
Jean-Marc Collin
8f934a3298 Feature 239 creates central config panel (#276)
* Add central config into ConfigFlow

* Test manual of confif_flow ok

* Ignore central confic in instanciate entities

* Init data in base_thermostat ok

* With central configuration testu ok

* All testu ok

* With fixture for init_vtherm_api and init_central_config

* Add reload VTherms when central configuration is changed

* Update strings.json and replace security by safety in README.

* UPdate README with release 5.0

* FIX missing Presets central configuration initialisation

* FIX frost_away_temp translation missing

---------

Co-authored-by: Jean-Marc Collin <jean-marc.collin-extern@renault.com>
2023-12-17 16:16:44 +01:00
Jean-Marc Collin
5a468fe2b9 Add beers and presence sensor for multiple persons 2023-12-13 11:22:26 +00:00
Jean-Marc Collin
fa248a3cfd Feature 218 add hors gel (#263)
* Implementation ok

* Add attributes for UI Card actions

---------

Co-authored-by: Jean-Marc Collin <jean-marc.collin-extern@renault.com>
2023-12-10 16:56:01 +01:00
Jean-Marc Collin
a7480e15c4 FIX anto_fan rule for Cooling mode. 2023-12-10 09:04:09 +00:00
Jean-Marc Collin
76416d28d2 Add instructions into ISSUE_TEMPLATE 2023-12-10 08:34:47 +00:00
Jean-Marc Collin
2bbd7ed8d6 Troubleshoot security mode 2023-12-10 00:10:34 +00:00
Jean-Marc Collin
7851df84ec Feature 223 use fan control in over climate (#260)
* Issue #223 - add auto_fan_mode

* Update README

---------

Co-authored-by: Jean-Marc Collin <jean-marc.collin-extern@renault.com>
2023-12-09 18:39:54 +01:00
Jean-Marc Collin
f7c4e20de3 Issue #257 - Migrate to HA 2023.12 2023-12-09 07:14:47 +00:00
Jean-Marc Collin
4d2888b220 Abyssal beers ! 2023-12-08 17:21:15 +00:00
Jean-Marc Collin
d2829bb951 Issue #248 - Ignore preset which are 0 2023-12-06 19:56:00 +00:00
Jean-Marc Collin
cd50c9b6e8 Add extensions into devcontainer 2023-12-05 06:25:20 +00:00
Jean-Marc Collin
b6f52bcc1b Issue #21à - rename mesure with measure 2023-12-03 18:05:22 +00:00
Jean-Marc Collin
5df77a1f74 Issue #189 - cannot remove window auto detection 2023-12-03 17:59:48 +00:00
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
92 changed files with 17175 additions and 5251 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,5 +1,8 @@
default_config:
# ffmeg
ffmpeg:
logger:
default: info
logs:
@@ -13,6 +16,19 @@ debugpy:
wait: false
port: 5678
versatile_thermostat:
auto_regulation_expert:
kp: 0.4
ki: 0.08
k_ext: 0.0
offset_max: 5
stabilization_threshold: 0.1
accumulated_error_threshold: 50
short_ema_params:
max_alpha: 0.6
halflife_sec: 301
precision: 3
input_number:
fake_temperature_sensor1:
name: Temperature
@@ -44,6 +60,19 @@ input_number:
step: 10
icon: mdi:flash
unit_of_measurement: kW
fake_valve1:
name: The valve 1
min: 10
max: 90
icon: mdi:pipe-valve
unit_of_measurement: percentage
fake_boiler_temperature:
name: Boiler temperature
min: 0
max: 30
icon: mdi:water-boiler
unit_of_measurement: °C
mode: box
input_boolean:
# input_boolean to simulate the windows entity. Only for development environment.
@@ -60,6 +89,9 @@ input_boolean:
fake_heater_switch1:
name: Heater 1
icon: mdi:radiator
fake_heater_ac1:
name: Air contionner 1
icon: mdi:air-conditioner
fake_heater_4switch1:
name: Heater (multiswitch1)
icon: mdi:radiator
@@ -114,18 +146,22 @@ climate:
name: Underlying thermostat 4-1
heater: input_boolean.fake_heater_4climate1
target_sensor: input_number.fake_temperature_sensor1
ac_mode: false
- platform: generic_thermostat
name: Underlying thermostat 4-2
heater: input_boolean.fake_heater_4climate2
target_sensor: input_number.fake_temperature_sensor1
ac_mode: false
- platform: generic_thermostat
name: Underlying thermostat 4-3
heater: input_boolean.fake_heater_4climate3
target_sensor: input_number.fake_temperature_sensor1
ac_mode: false
- platform: generic_thermostat
name: Underlying thermostat 4-4
heater: input_boolean.fake_heater_4climate4
target_sensor: input_number.fake_temperature_sensor1
ac_mode: false
- platform: generic_thermostat
name: Underlying thermostat9
heater: input_boolean.fake_heater_switch3
@@ -139,6 +175,7 @@ recorder:
- switch
- climate
- sensor
- binary_sensor
template:
- binary_sensor:
@@ -199,6 +236,8 @@ switch:
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"
@@ -206,3 +245,4 @@ frontend:
state-binary_sensor-window-on-color: "rgb(156, 39, 176)"
state-binary_sensor-motion-on-color: "rgb(156, 39, 176)"
state-binary_sensor-presence-on-color: "lightgreen"
state-binary_sensor-running-on-color: "orange"

View File

@@ -1,44 +1,60 @@
// 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",
"donjayamanne.githistory",
"waderyan.gitblame",
"keesschollaart.vscode-home-assistant",
"vscode.markdown-math",
"yzhang.markdown-all-in-one"
],
"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

@@ -4,6 +4,12 @@ about: Suggest an idea for this project
---
**Consider the alternative to create a free discusssion before making a feature request**
Discussions forum is [here](https://github.com/jmcollin78/versatile_thermostat/discussions).
You should check that a discussion relative to the same issue have not been already answered in the forum.
Please also check in the [closed issues](https://github.com/jmcollin78/versatile_thermostat/issues) for a similar case.
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]

View File

@@ -4,36 +4,141 @@ about: Create a report to help us improve
---
<!-- This template will allow the maintainer to be efficient and post the more accurante response as possible. There is many types / modes / configuration possible, so the analysis can be very tricky. If don't follow this template, your issue could be rejected without any message. Please help me to help you. -->
<!-- Before you open a new issue, search through the existing issues to see if others have had the same problem.
If you have a simple question or you are not sure this is an issue, don't open an issue but open a new discussion [here](https://github.com/jmcollin78/versatile_thermostat/discussions).
Check also in the [Troubleshooting](#troubleshooting) paragrah of the README if the aswer is not already given.
Issues not containing the minimum requirements will be closed:
- Issues without a description (using the header is not good enough) will be closed.
- Issues without debug logging will be closed.
- Issues without configuration will be closed
- Issues that don't follow this template could be closed
-->
## Version of the custom_component
<!-- If you are not using the newest version, download and try that before opening an issue
If you are unsure about the version check the const.py file.
If you are unsure about the version check the manifest.json file.
-->
## Configuration
<!-- Copy / paste the attributes of the VTherm here. You can go to Development Tool / States, find and select your VTherm and the copy/paste the attributes. Surround these attributes by a yaml formatting ```yaml <put the attributes> .... ```
Without these attribute support is impossible due to the number of configuration attributes the VTherm have (more than 60). -->
My VTherm attributes are the following:
```yaml
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. -->
## If it is releveant to regulation performance or optimisation some curves are needed
To have a great curves demonstrating what you think is a problem, please install and configure what is described here: [Even better with Plotly to tune your Thermostat](#even-better-with-plotly-to-tune-your-thermostat)
## Describe the bug
A clear and concise description of what the bug is.
I'm trying to:
<!-- compleete the description -->
And I expect:
<!-- complete the expectations -->
But I observe this ....
<!-- complete what you observe and why you think it is erroneous. -->
I read the documentation on the README.md file and I don't find any relevant information about this issue.
## Debug log
<!-- To enable debug logs check this https://www.home-assistant.io/components/logger/ -->
<!-- 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

@@ -31,6 +31,8 @@ jobs:
- run: black .
tests:
# Tests don't run in Gitlab ci environment
if: 0
runs-on: "ubuntu-latest"
name: Run tests
steps:
@@ -41,10 +43,10 @@ jobs:
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 \

8
.gitignore vendored
View File

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

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"
]
}
]

21
.vscode/settings.json vendored
View File

@@ -1,12 +1,21 @@
{
"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",
"/home/vscode/.local/lib/python3.11/site-packages/homeassistant"
],
"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.

File diff suppressed because it is too large Load Diff

965
README.md

File diff suppressed because it is too large Load Diff

View File

@@ -4,35 +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
# python -m script.hassfest --requirements --action validate --integration-path config/custom_components/versatile_thermostat/
fi
if [ "$command" == "restart" ]; then
echo "Killing existing container"
pkill hass
echo "Killing existing container"
hass -c ./config
fi
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

@@ -3,17 +3,112 @@ from __future__ import annotations
from typing import Dict
import asyncio
import logging
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import SERVICE_RELOAD
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, PLATFORMS
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,
CONF_THERMOSTAT_CENTRAL_CONFIG,
CONF_THERMOSTAT_TYPE,
)
from .vtherm_api import VersatileThermostatAPI
_LOGGER = logging.getLogger(__name__)
SELF_REGULATION_PARAM_SCHEMA = {
vol.Required("kp"): vol.Coerce(float),
vol.Required("ki"): vol.Coerce(float),
vol.Required("k_ext"): vol.Coerce(float),
vol.Required("offset_max"): vol.Coerce(float),
vol.Required("stabilization_threshold"): vol.Coerce(float),
vol.Required("accumulated_error_threshold"): vol.Coerce(float),
}
EMA_PARAM_SCHEMA = {
vol.Required("max_alpha"): vol.Coerce(float),
vol.Required("halflife_sec"): vol.Coerce(float),
vol.Required("precision"): cv.positive_int,
}
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),
)
async def _handle_reload(_):
"""The reload callback"""
await reload_all_vtherm(hass)
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")
hass.helpers.service.async_register_admin_service(
DOMAIN,
SERVICE_RELOAD,
_handle_reload,
)
return True
async def reload_all_vtherm(hass):
"""Handle reload service call."""
_LOGGER.info("Service %s.reload called: reloading integration", DOMAIN)
current_entries = hass.config_entries.async_entries(DOMAIN)
reload_tasks = [
hass.config_entries.async_reload(entry.entry_id) for entry in current_entries
]
await asyncio.gather(*reload_tasks)
api: VersatileThermostatAPI = VersatileThermostatAPI.get_vtherm_api(hass)
if api:
await api.reload_central_boiler_entities_list()
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Versatile Thermostat from a config entry."""
@@ -24,11 +119,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)
@@ -41,57 +132,28 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:
"""Update listener."""
await hass.config_entries.async_reload(entry.entry_id)
if entry.data.get(CONF_THERMOSTAT_TYPE) == CONF_THERMOSTAT_CENTRAL_CONFIG:
await reload_all_vtherm(hass)
else:
await hass.config_entries.async_reload(entry.entry_id)
# Reload the central boiler list of entities
api: VersatileThermostatAPI = VersatileThermostatAPI.get_vtherm_api(hass)
if api is not None:
await api.reload_central_boiler_entities_list()
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
api: VersatileThermostatAPI = hass.data.get(DOMAIN)
api: VersatileThermostatAPI = VersatileThermostatAPI.get_vtherm_api(hass)
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
if api:
api.remove_entry(entry)
await api.reload_central_boiler_entities_list()
return unload_ok
class VersatileThermostatAPI(dict):
"""The VersatileThermostatAPI"""
_hass: HomeAssistant
# _entries: Dict(str, ConfigEntry)
def __init__(self, hass: HomeAssistant) -> None:
_LOGGER.debug("building a VersatileThermostatAPI")
super().__init__()
self._hass = hass
# self._entries = dict()
# Add the API in hass.data
self._hass.data[DOMAIN] = self
def add_entry(self, entry: ConfigEntry):
"""Add a new entry"""
_LOGGER.debug("Add the entry %s", entry.entry_id)
# self._entries[entry.entry_id] = entry
# Add the entry in hass.data
self._hass.data[DOMAIN][entry.entry_id] = entry
def remove_entry(self, entry: ConfigEntry):
"""Remove an entry"""
_LOGGER.debug("Remove the entry %s", entry.entry_id)
# self._entries.pop(entry.entry_id)
self._hass.data[DOMAIN].pop(entry.entry_id)
# If not more entries are preset, remove the API
if len(self) == 0:
_LOGGER.debug("No more entries-> Remove the API from DOMAIN")
self._hass.data.pop(DOMAIN)
@property
def hass(self):
"""Get the HomeAssistant object"""
return self._hass
# Example migration function
async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry):
"""Migrate old entry."""

File diff suppressed because it is too large Load Diff

View File

@@ -1,9 +1,21 @@
""" Implements the VersatileThermostat binary sensors component """
# pylint: disable=unused-argument
import logging
from homeassistant.core import HomeAssistant, callback, Event
from homeassistant.core import (
HomeAssistant,
callback,
Event,
CoreState,
HomeAssistantError,
)
from homeassistant.const import STATE_ON
from homeassistant.const import STATE_ON, STATE_OFF, EVENT_HOMEASSISTANT_START
from homeassistant.helpers.device_registry import DeviceInfo, DeviceEntryType
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.event import async_track_state_change_event
from homeassistant.components.binary_sensor import (
BinarySensorEntity,
@@ -13,13 +25,34 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .commons import VersatileThermostatBaseEntity
from homeassistant.components.climate import (
ClimateEntity,
HVACMode,
HVACAction,
DOMAIN as CLIMATE_DOMAIN,
)
from custom_components.versatile_thermostat.base_thermostat import BaseThermostat
from .vtherm_api import VersatileThermostatAPI
from .commons import (
VersatileThermostatBaseEntity,
check_and_extract_service_configuration,
)
from .const import (
DOMAIN,
DEVICE_MANUFACTURER,
CONF_NAME,
CONF_USE_POWER_FEATURE,
CONF_USE_PRESENCE_FEATURE,
CONF_USE_MOTION_FEATURE,
CONF_USE_WINDOW_FEATURE,
CONF_THERMOSTAT_TYPE,
CONF_THERMOSTAT_CENTRAL_CONFIG,
CONF_CENTRAL_BOILER_ACTIVATION_SRV,
CONF_CENTRAL_BOILER_DEACTIVATION_SRV,
overrides,
EventType,
send_vtherm_event,
)
_LOGGER = logging.getLogger(__name__)
@@ -37,16 +70,25 @@ async def async_setup_entry(
unique_id = entry.entry_id
name = entry.data.get(CONF_NAME)
vt_type = entry.data.get(CONF_THERMOSTAT_TYPE)
entities = [SecurityBinarySensor(hass, unique_id, name, entry.data)]
if entry.data.get(CONF_USE_MOTION_FEATURE):
entities.append(MotionBinarySensor(hass, unique_id, name, entry.data))
if entry.data.get(CONF_USE_WINDOW_FEATURE):
entities.append(WindowBinarySensor(hass, unique_id, name, entry.data))
if entry.data.get(CONF_USE_PRESENCE_FEATURE):
entities.append(PresenceBinarySensor(hass, unique_id, name, entry.data))
if entry.data.get(CONF_USE_POWER_FEATURE):
entities.append(OverpoweringBinarySensor(hass, unique_id, name, entry.data))
if vt_type == CONF_THERMOSTAT_CENTRAL_CONFIG:
entities = [
CentralBoilerBinarySensor(hass, unique_id, name, entry.data),
]
else:
entities = [
SecurityBinarySensor(hass, unique_id, name, entry.data),
WindowByPassBinarySensor(hass, unique_id, name, entry.data),
]
if entry.data.get(CONF_USE_MOTION_FEATURE):
entities.append(MotionBinarySensor(hass, unique_id, name, entry.data))
if entry.data.get(CONF_USE_WINDOW_FEATURE):
entities.append(WindowBinarySensor(hass, unique_id, name, entry.data))
if entry.data.get(CONF_USE_PRESENCE_FEATURE):
entities.append(PresenceBinarySensor(hass, unique_id, name, entry.data))
if entry.data.get(CONF_USE_POWER_FEATURE):
entities.append(OverpoweringBinarySensor(hass, unique_id, name, entry.data))
async_add_entities(entities, True)
@@ -54,7 +96,13 @@ async def async_setup_entry(
class SecurityBinarySensor(VersatileThermostatBaseEntity, BinarySensorEntity):
"""Representation of a BinarySensor which exposes the security state"""
def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None:
def __init__(
self,
hass: HomeAssistant,
unique_id,
name, # pylint: disable=unused-argument
entry_infos,
) -> None:
"""Initialize the SecurityState Binary sensor"""
super().__init__(hass, unique_id, entry_infos.get(CONF_NAME))
self._attr_name = "Security state"
@@ -87,7 +135,13 @@ class SecurityBinarySensor(VersatileThermostatBaseEntity, BinarySensorEntity):
class OverpoweringBinarySensor(VersatileThermostatBaseEntity, BinarySensorEntity):
"""Representation of a BinarySensor which exposes the overpowering state"""
def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None:
def __init__(
self,
hass: HomeAssistant,
unique_id,
name, # pylint: disable=unused-argument
entry_infos,
) -> None:
"""Initialize the OverpoweringState Binary sensor"""
super().__init__(hass, unique_id, entry_infos.get(CONF_NAME))
self._attr_name = "Overpowering state"
@@ -120,7 +174,13 @@ class OverpoweringBinarySensor(VersatileThermostatBaseEntity, BinarySensorEntity
class WindowBinarySensor(VersatileThermostatBaseEntity, BinarySensorEntity):
"""Representation of a BinarySensor which exposes the window state"""
def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None:
def __init__(
self,
hass: HomeAssistant,
unique_id,
name, # pylint: disable=unused-argument
entry_infos,
) -> None:
"""Initialize the WindowState Binary sensor"""
super().__init__(hass, unique_id, entry_infos.get(CONF_NAME))
self._attr_name = "Window state"
@@ -133,12 +193,17 @@ class WindowBinarySensor(VersatileThermostatBaseEntity, BinarySensorEntity):
_LOGGER.debug("%s - climate state change", self._attr_unique_id)
old_state = self._attr_is_on
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()
# 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
@@ -159,7 +224,13 @@ class WindowBinarySensor(VersatileThermostatBaseEntity, BinarySensorEntity):
class MotionBinarySensor(VersatileThermostatBaseEntity, BinarySensorEntity):
"""Representation of a BinarySensor which exposes the motion state"""
def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None:
def __init__(
self,
hass: HomeAssistant,
unique_id,
name, # pylint: disable=unused-argument
entry_infos,
) -> None:
"""Initialize the MotionState Binary sensor"""
super().__init__(hass, unique_id, entry_infos.get(CONF_NAME))
self._attr_name = "Motion state"
@@ -171,9 +242,11 @@ class MotionBinarySensor(VersatileThermostatBaseEntity, BinarySensorEntity):
"""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.motion_state == STATE_ON
if old_state != self._attr_is_on:
self.async_write_ha_state()
# 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
@@ -191,7 +264,13 @@ class MotionBinarySensor(VersatileThermostatBaseEntity, BinarySensorEntity):
class PresenceBinarySensor(VersatileThermostatBaseEntity, BinarySensorEntity):
"""Representation of a BinarySensor which exposes the presence state"""
def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None:
def __init__(
self,
hass: HomeAssistant,
unique_id,
name, # pylint: disable=unused-argument
entry_infos,
) -> None:
"""Initialize the PresenceState Binary sensor"""
super().__init__(hass, unique_id, entry_infos.get(CONF_NAME))
self._attr_name = "Presence state"
@@ -204,9 +283,11 @@ class PresenceBinarySensor(VersatileThermostatBaseEntity, BinarySensorEntity):
_LOGGER.debug("%s - climate state change", self._attr_unique_id)
old_state = self._attr_is_on
self._attr_is_on = self.my_climate.presence_state == STATE_ON
if old_state != self._attr_is_on:
self.async_write_ha_state()
# 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
@@ -219,3 +300,200 @@ class PresenceBinarySensor(VersatileThermostatBaseEntity, BinarySensorEntity):
return "mdi:home-account"
else:
return "mdi:nature-people"
class WindowByPassBinarySensor(VersatileThermostatBaseEntity, BinarySensorEntity):
"""Representation of a BinarySensor which exposes the Window ByPass state"""
def __init__(
self,
hass: HomeAssistant,
unique_id,
name, # pylint: disable=unused-argument
entry_infos,
) -> None:
"""Initialize the WindowByPass Binary sensor"""
super().__init__(hass, unique_id, entry_infos.get(CONF_NAME))
self._attr_name = "Window bypass"
self._attr_unique_id = f"{self._device_name}_window_bypass_state"
self._attr_is_on = False
@callback
async def async_my_climate_changed(self, event: Event = None):
"""Called when my climate have change"""
_LOGGER.debug("%s - climate state change", self._attr_unique_id)
old_state = self._attr_is_on
if self.my_climate.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"
class CentralBoilerBinarySensor(BinarySensorEntity):
"""Representation of a BinarySensor which exposes the Central Boiler state"""
def __init__(
self,
hass: HomeAssistant,
unique_id,
name, # pylint: disable=unused-argument
entry_infos,
) -> None:
"""Initialize the CentralBoiler Binary sensor"""
self._config_id = unique_id
self._attr_name = "Central boiler"
self._attr_unique_id = "central_boiler_state"
self._attr_is_on = False
self._device_name = entry_infos.get(CONF_NAME)
self._entities = []
self._hass = hass
self._service_activate = check_and_extract_service_configuration(
entry_infos.get(CONF_CENTRAL_BOILER_ACTIVATION_SRV)
)
self._service_deactivate = check_and_extract_service_configuration(
entry_infos.get(CONF_CENTRAL_BOILER_DEACTIVATION_SRV)
)
@property
def device_info(self) -> DeviceInfo:
"""Return the device info."""
return DeviceInfo(
entry_type=DeviceEntryType.SERVICE,
identifiers={(DOMAIN, self._config_id)},
name=self._device_name,
manufacturer=DEVICE_MANUFACTURER,
model=DOMAIN,
)
@property
def device_class(self) -> BinarySensorDeviceClass | None:
return BinarySensorDeviceClass.RUNNING
@property
def icon(self) -> str | None:
if self._attr_is_on:
return "mdi:water-boiler"
else:
return "mdi:water-boiler-off"
@overrides
async def async_added_to_hass(self) -> None:
await super().async_added_to_hass()
@callback
async def _async_startup_internal(*_):
_LOGGER.debug("%s - Calling async_startup_internal", self)
api: VersatileThermostatAPI = VersatileThermostatAPI.get_vtherm_api(
self._hass
)
api.register_central_boiler(self)
await self.listen_vtherms_entities()
if self.hass.state == CoreState.running:
await _async_startup_internal()
else:
self.hass.bus.async_listen_once(
EVENT_HOMEASSISTANT_START, _async_startup_internal
)
async def listen_vtherms_entities(self):
"""Initialize the listening of state change of VTherms"""
# Listen to all VTherm state change
self._entities = []
entities_id = []
component: EntityComponent[ClimateEntity] = self.hass.data[CLIMATE_DOMAIN]
for entity in component.entities:
if isinstance(entity, BaseThermostat) and entity.is_used_by_central_boiler:
self._entities.append(entity)
entities_id.append(entity.entity_id)
if len(self._entities) > 0:
# Arme l'écoute de la première entité
listener_cancel = async_track_state_change_event(
self._hass,
entities_id,
self.calculate_central_boiler_state,
)
_LOGGER.info(
"%s - VTherm that could controls the central boiler are %s",
self,
entities_id,
)
self.async_on_remove(listener_cancel)
else:
_LOGGER.debug("%s - no VTherm could controls the central boiler", self)
await self.calculate_central_boiler_state(None)
async def calculate_central_boiler_state(self, _):
"""Calculate the central boiler state depending on all VTherm that
controls this central boiler"""
_LOGGER.debug("%s - calculating the new central boiler state", self)
active = False
for entity in self._entities:
_LOGGER.debug(
"Examining the hvac_action of %s",
entity.name,
)
if (
entity.hvac_mode == HVACMode.HEAT
and entity.hvac_action == HVACAction.HEATING
):
active = True
break
if self._attr_is_on != active:
try:
if active:
await self.call_service(self._service_activate)
_LOGGER.info("%s - central boiler have been turned on", self)
else:
await self.call_service(self._service_deactivate)
_LOGGER.info("%s - central boiler have been turned off", self)
self._attr_is_on = active
send_vtherm_event(
hass=self._hass,
event_type=EventType.CENTRAL_BOILER_EVENT,
entity=self,
data={"central_boiler": active},
)
self.async_write_ha_state()
except HomeAssistantError as err:
_LOGGER.error(
"%s - Impossible to activate/deactivat boiler due to error %s."
"Central boiler will not being controled by VTherm."
"Please check your service configuration. Cf. README.",
self,
err,
)
async def call_service(self, service_config: dict):
"""Make a call to a service if correctly configured"""
if not service_config:
return
await self._hass.services.async_call(
service_config["service_domain"],
service_config["service_name"],
service_data=service_config["data"],
target={
"entity_id": service_config["entity_id"],
},
)
def __str__(self):
return f"VersatileThermostat-{self.name}"

File diff suppressed because it is too large Load Diff

View File

@@ -1,23 +1,164 @@
""" Some usefull commons class """
# pylint: disable=line-too-long
import logging
from datetime import timedelta
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 .climate import VersatileThermostat
from .const import DOMAIN, DEVICE_MANUFACTURER
from .base_thermostat import BaseThermostat
from .const import DOMAIN, DEVICE_MANUFACTURER, ServiceConfigurationError
_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)
def check_and_extract_service_configuration(service_config) -> dict:
"""Raise a ServiceConfigurationError. In return you have a dict formatted like follows.
Example if you call with 'climate.central_boiler/climate.set_temperature/temperature:10':
{
"service_domain": "climate",
"service_name": "set_temperature",
"entity_id": "climate.central_boiler",
"entity_domain": "climate",
"entity_name": "central_boiler",
"data": {
"temperature": "10"
},
"attribute_name": "temperature",
"attribute_value: "10"
}
For this example 'switch.central_boiler/switch.turn_off' you will have this:
{
"service_domain": "switch",
"service_name": "turn_off",
"entity_id": "switch.central_boiler",
"entity_domain": "switch",
"entity_name": "central_boiler",
"data": { },
}
All values are striped (white space are removed) and are string
"""
ret = {}
if service_config is None:
return ret
parties = service_config.split("/")
if len(parties) < 2:
raise ServiceConfigurationError(
f"Incorrect service configuration. Service {service_config} should be formatted with: 'entity_name/service_name[/data]'. See README for more information."
)
entity_id = parties[0]
service_name = parties[1]
service_infos = service_name.split(".")
if len(service_infos) != 2:
raise ServiceConfigurationError(
f"Incorrect service configuration. The service {service_config} should be formatted like: 'domain.service_name' (ex: 'switch.turn_on'). See README for more information."
)
ret.update(
{
"service_domain": service_infos[0].strip(),
"service_name": service_infos[1].strip(),
}
)
entity_infos = entity_id.split(".")
if len(entity_infos) != 2:
raise ServiceConfigurationError(
f"Incorrect service configuration. The entity_id {entity_id} should be formatted like: 'domain.entity_name' (ex: 'switch.central_boiler_switch'). See README for more information."
)
ret.update(
{
"entity_domain": entity_infos[0].strip(),
"entity_name": entity_infos[1].strip(),
"entity_id": entity_id.strip(),
}
)
if len(parties) == 3:
data = parties[2]
if len(data) > 0:
data_infos = None
data_infos = data.split(":")
if (
len(data_infos) != 2
or len(data_infos[0]) <= 0
or len(data_infos[1]) <= 0
):
raise ServiceConfigurationError(
f"Incorrect service configuration. The data {data} should be formatted like: 'attribute:value' (ex: 'value:25'). See README for more information."
)
ret.update(
{
"attribute_name": data_infos[0].strip(),
"attribute_value": data_infos[1].strip(),
"data": {data_infos[0].strip(): data_infos[1].strip()},
}
)
else:
raise ServiceConfigurationError(
f"Incorrect service configuration. The data {data} should be formatted like: 'attribute:value' (ex: 'value:25'). See README for more information."
)
else:
ret.update({"data": {}})
_LOGGER.debug(
"check_and_extract_service_configuration(%s) gives '%s'", service_config, ret
)
return ret
class VersatileThermostatBaseEntity(Entity):
"""A base class for all entities"""
_my_climate: VersatileThermostat
_my_climate: BaseThermostat
hass: HomeAssistant
_config_id: str
_device_name: str
@@ -37,7 +178,7 @@ class VersatileThermostatBaseEntity(Entity):
return False
@property
def my_climate(self) -> VersatileThermostat | None:
def my_climate(self) -> BaseThermostat | None:
"""Returns my climate if found"""
if not self._my_climate:
self._my_climate = self.find_my_versatile_thermostat()
@@ -54,7 +195,7 @@ class VersatileThermostatBaseEntity(Entity):
model=DOMAIN,
)
def find_my_versatile_thermostat(self) -> VersatileThermostat:
def find_my_versatile_thermostat(self) -> BaseThermostat:
"""Find the underlying climate entity"""
try:
component: EntityComponent[ClimateEntity] = self.hass.data[CLIMATE_DOMAIN]
@@ -98,7 +239,9 @@ class VersatileThermostatBaseEntity(Entity):
await try_find_climate(None)
@callback
async def async_my_climate_changed(self, event: Event):
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
"""

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,312 @@
""" All the schemas for ConfigFlow validation"""
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers import selector
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
from homeassistant.components.number import DOMAIN as NUMBER_DOMAIN
from homeassistant.components.climate import DOMAIN as CLIMATE_DOMAIN
from homeassistant.components.input_boolean import (
DOMAIN as INPUT_BOOLEAN_DOMAIN,
)
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
from homeassistant.components.input_number import (
DOMAIN as INPUT_NUMBER_DOMAIN,
)
from homeassistant.components.person import DOMAIN as PERSON_DOMAIN
from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN
from .const import * # pylint: disable=wildcard-import, unused-wildcard-import
STEP_USER_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name
{
vol.Required(
CONF_THERMOSTAT_TYPE, default=CONF_THERMOSTAT_SWITCH
): selector.SelectSelector(
selector.SelectSelectorConfig(
options=CONF_THERMOSTAT_TYPES, translation_key="thermostat_type"
)
)
}
)
STEP_MAIN_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name
{
vol.Required(CONF_NAME): cv.string,
vol.Required(CONF_TEMP_SENSOR): selector.EntitySelector(
selector.EntitySelectorConfig(domain=[SENSOR_DOMAIN, INPUT_NUMBER_DOMAIN]),
),
vol.Required(CONF_CYCLE_MIN, default=5): cv.positive_int,
vol.Optional(CONF_DEVICE_POWER, default="1"): vol.Coerce(float),
vol.Optional(CONF_USE_CENTRAL_MODE, default=True): cv.boolean,
vol.Required(CONF_USE_MAIN_CENTRAL_CONFIG, default=True): cv.boolean,
vol.Optional(CONF_USE_WINDOW_FEATURE, default=False): cv.boolean,
vol.Optional(CONF_USE_MOTION_FEATURE, default=False): cv.boolean,
vol.Optional(CONF_USE_POWER_FEATURE, default=False): cv.boolean,
vol.Optional(CONF_USE_PRESENCE_FEATURE, default=False): cv.boolean,
vol.Required(CONF_USED_BY_CENTRAL_BOILER, default=False): cv.boolean,
}
)
STEP_CENTRAL_MAIN_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name
{
vol.Required(CONF_EXTERNAL_TEMP_SENSOR): selector.EntitySelector(
selector.EntitySelectorConfig(domain=[SENSOR_DOMAIN, INPUT_NUMBER_DOMAIN]),
),
vol.Required(CONF_TEMP_MIN, default=7): vol.Coerce(float),
vol.Required(CONF_TEMP_MAX, default=35): vol.Coerce(float),
vol.Required(CONF_ADD_CENTRAL_BOILER_CONTROL, default=False): cv.boolean,
}
)
STEP_CENTRAL_SPEC_MAIN_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name
{
vol.Required(CONF_EXTERNAL_TEMP_SENSOR): selector.EntitySelector(
selector.EntitySelectorConfig(domain=[SENSOR_DOMAIN, INPUT_NUMBER_DOMAIN]),
),
vol.Required(CONF_TEMP_MIN, default=7): vol.Coerce(float),
vol.Required(CONF_TEMP_MAX, default=35): vol.Coerce(float),
}
)
STEP_CENTRAL_BOILER_SCHEMA = vol.Schema(
{
vol.Optional(CONF_CENTRAL_BOILER_ACTIVATION_SRV, default=""): str,
vol.Optional(CONF_CENTRAL_BOILER_DEACTIVATION_SRV, default=""): str,
}
)
STEP_THERMOSTAT_SWITCH = vol.Schema( # pylint: disable=invalid-name
{
vol.Required(CONF_HEATER): selector.EntitySelector(
selector.EntitySelectorConfig(domain=[SWITCH_DOMAIN, INPUT_BOOLEAN_DOMAIN]),
),
vol.Optional(CONF_HEATER_2): selector.EntitySelector(
selector.EntitySelectorConfig(domain=[SWITCH_DOMAIN, INPUT_BOOLEAN_DOMAIN]),
),
vol.Optional(CONF_HEATER_3): selector.EntitySelector(
selector.EntitySelectorConfig(domain=[SWITCH_DOMAIN, INPUT_BOOLEAN_DOMAIN]),
),
vol.Optional(CONF_HEATER_4): selector.EntitySelector(
selector.EntitySelectorConfig(domain=[SWITCH_DOMAIN, INPUT_BOOLEAN_DOMAIN]),
),
vol.Required(CONF_PROP_FUNCTION, default=PROPORTIONAL_FUNCTION_TPI): vol.In(
[
PROPORTIONAL_FUNCTION_TPI,
]
),
vol.Optional(CONF_AC_MODE, default=False): cv.boolean,
vol.Optional(CONF_INVERSE_SWITCH, default=False): cv.boolean,
}
)
STEP_THERMOSTAT_CLIMATE = vol.Schema( # pylint: disable=invalid-name
{
vol.Required(CONF_CLIMATE): selector.EntitySelector(
selector.EntitySelectorConfig(domain=CLIMATE_DOMAIN),
),
vol.Optional(CONF_CLIMATE_2): selector.EntitySelector(
selector.EntitySelectorConfig(domain=CLIMATE_DOMAIN),
),
vol.Optional(CONF_CLIMATE_3): selector.EntitySelector(
selector.EntitySelectorConfig(domain=CLIMATE_DOMAIN),
),
vol.Optional(CONF_CLIMATE_4): selector.EntitySelector(
selector.EntitySelectorConfig(domain=CLIMATE_DOMAIN),
),
vol.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,
vol.Optional(
CONF_AUTO_FAN_MODE, default=CONF_AUTO_FAN_HIGH
): selector.SelectSelector(
selector.SelectSelectorConfig(
options=CONF_AUTO_FAN_MODES,
translation_key="auto_fan_mode",
)
),
}
)
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,
}
)
STEP_TPI_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name
{
vol.Required(CONF_USE_TPI_CENTRAL_CONFIG, default=True): cv.boolean,
}
)
STEP_CENTRAL_TPI_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name
{
vol.Required(CONF_TPI_COEF_INT, default=0.6): vol.Coerce(float),
vol.Required(CONF_TPI_COEF_EXT, default=0.01): vol.Coerce(float),
}
)
STEP_PRESETS_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name
{
vol.Required(CONF_USE_PRESETS_CENTRAL_CONFIG, default=True): cv.boolean,
}
)
STEP_CENTRAL_PRESETS_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name
{vol.Optional(v, default=0): vol.Coerce(float) for (k, v) in CONF_PRESETS.items()}
)
STEP_CENTRAL_PRESETS_WITH_AC_DATA_SCHEMA = (
vol.Schema( # pylint: disable=invalid-name # pylint: disable=invalid-name
{
vol.Optional(v, default=0): vol.Coerce(float)
for (k, v) in CONF_PRESETS_WITH_AC.items()
}
)
)
STEP_WINDOW_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name
{
vol.Optional(CONF_WINDOW_SENSOR): selector.EntitySelector(
selector.EntitySelectorConfig(
domain=[BINARY_SENSOR_DOMAIN, INPUT_BOOLEAN_DOMAIN]
),
),
vol.Required(CONF_USE_WINDOW_CENTRAL_CONFIG, default=True): cv.boolean,
}
)
STEP_CENTRAL_WINDOW_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name
{
vol.Optional(CONF_WINDOW_DELAY, default=30): cv.positive_int,
vol.Optional(CONF_WINDOW_AUTO_OPEN_THRESHOLD, default=3): vol.Coerce(float),
vol.Optional(CONF_WINDOW_AUTO_CLOSE_THRESHOLD, default=0): vol.Coerce(float),
vol.Optional(CONF_WINDOW_AUTO_MAX_DURATION, default=30): cv.positive_int,
}
)
STEP_CENTRAL_WINDOW_WO_AUTO_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name
{
vol.Optional(CONF_WINDOW_DELAY, default=30): cv.positive_int,
}
)
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.Required(CONF_USE_MOTION_CENTRAL_CONFIG, default=True): cv.boolean,
}
)
STEP_CENTRAL_MOTION_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name
{
vol.Optional(CONF_MOTION_DELAY, default=30): cv.positive_int,
vol.Optional(CONF_MOTION_OFF_DELAY, default=300): cv.positive_int,
vol.Optional(CONF_MOTION_PRESET, default="comfort"): vol.In(
CONF_PRESETS_SELECTIONABLE
),
vol.Optional(CONF_NO_MOTION_PRESET, default="eco"): vol.In(
CONF_PRESETS_SELECTIONABLE
),
}
)
STEP_CENTRAL_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.Optional(CONF_MAX_POWER_SENSOR): selector.EntitySelector(
selector.EntitySelectorConfig(domain=[SENSOR_DOMAIN, INPUT_NUMBER_DOMAIN]),
),
vol.Optional(CONF_PRESET_POWER, default="13"): vol.Coerce(float),
}
)
STEP_POWER_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name
{
vol.Required(CONF_USE_POWER_CENTRAL_CONFIG, default=True): cv.boolean,
}
)
STEP_CENTRAL_PRESENCE_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name
{
vol.Optional(v, default=17): vol.Coerce(float)
for (k, v) in CONF_PRESETS_AWAY.items()
}
)
STEP_CENTRAL_PRESENCE_WITH_AC_DATA_SCHEMA = { # pylint: disable=invalid-name
vol.Optional(v, default=17): vol.Coerce(float)
for (k, v) in CONF_PRESETS_AWAY_WITH_AC.items()
}
STEP_PRESENCE_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name
{
vol.Optional(CONF_PRESENCE_SENSOR): selector.EntitySelector(
selector.EntitySelectorConfig(
domain=[
PERSON_DOMAIN,
BINARY_SENSOR_DOMAIN,
INPUT_BOOLEAN_DOMAIN,
]
),
),
vol.Required(CONF_USE_PRESENCE_CENTRAL_CONFIG, default=True): cv.boolean,
}
)
STEP_CENTRAL_ADVANCED_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name
{
vol.Required(CONF_MINIMAL_ACTIVATION_DELAY, default=10): cv.positive_int,
vol.Required(CONF_SECURITY_DELAY_MIN, default=60): cv.positive_int,
vol.Required(
CONF_SECURITY_MIN_ON_PERCENT,
default=DEFAULT_SECURITY_MIN_ON_PERCENT,
): vol.Coerce(float),
vol.Required(
CONF_SECURITY_DEFAULT_ON_PERCENT,
default=DEFAULT_SECURITY_DEFAULT_ON_PERCENT,
): vol.Coerce(float),
}
)
STEP_ADVANCED_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name
{
vol.Required(CONF_USE_ADVANCED_CENTRAL_CONFIG, default=True): cv.boolean,
}
)

View File

@@ -1,5 +1,8 @@
# pylint: disable=line-too-long
"""Constants for the Versatile Thermostat integration."""
import logging
from enum import Enum
from homeassistant.const import CONF_NAME, Platform
@@ -11,28 +14,37 @@ from homeassistant.components.climate import (
ClimateEntityFeature,
)
PRESET_AC_SUFFIX = "_ac"
PRESET_ECO_AC = PRESET_ECO + PRESET_AC_SUFFIX
PRESET_COMFORT_AC = PRESET_COMFORT + PRESET_AC_SUFFIX
PRESET_BOOST_AC = PRESET_BOOST + PRESET_AC_SUFFIX
from homeassistant.exceptions import HomeAssistantError
from .prop_algorithm import (
PROPORTIONAL_FUNCTION_TPI,
)
_LOGGER = logging.getLogger(__name__)
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"
PRESET_FROST_PROTECTION = "frost"
HIDDEN_PRESETS = [PRESET_POWER, PRESET_SECURITY]
DOMAIN = "versatile_thermostat"
PLATFORMS: list[Platform] = [Platform.CLIMATE, Platform.BINARY_SENSOR, Platform.SENSOR]
PLATFORMS: list[Platform] = [
Platform.CLIMATE,
Platform.BINARY_SENSOR,
Platform.SENSOR,
Platform.SELECT,
]
CONF_HEATER = "heater_entity_id"
CONF_HEATER_2 = "heater_entity2_id"
@@ -49,6 +61,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"
@@ -62,8 +75,10 @@ CONF_SECURITY_DELAY_MIN = "security_delay_min"
CONF_SECURITY_MIN_ON_PERCENT = "security_min_on_percent"
CONF_SECURITY_DEFAULT_ON_PERCENT = "security_default_on_percent"
CONF_THERMOSTAT_TYPE = "thermostat_type"
CONF_THERMOSTAT_CENTRAL_CONFIG = "thermostat_central_config"
CONF_THERMOSTAT_SWITCH = "thermostat_over_switch"
CONF_THERMOSTAT_CLIMATE = "thermostat_over_climate"
CONF_THERMOSTAT_VALVE = "thermostat_over_valve"
CONF_CLIMATE = "climate_entity_id"
CONF_CLIMATE_2 = "climate_entity2_id"
CONF_CLIMATE_3 = "climate_entity3_id"
@@ -76,10 +91,56 @@ 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"
CONF_AUTO_FAN_MODE = "auto_fan_mode"
CONF_AUTO_FAN_NONE = "auto_fan_none"
CONF_AUTO_FAN_LOW = "auto_fan_low"
CONF_AUTO_FAN_MEDIUM = "auto_fan_medium"
CONF_AUTO_FAN_HIGH = "auto_fan_high"
CONF_AUTO_FAN_TURBO = "auto_fan_turbo"
CONF_USE_MAIN_CENTRAL_CONFIG = "use_main_central_config"
CONF_USE_TPI_CENTRAL_CONFIG = "use_tpi_central_config"
CONF_USE_WINDOW_CENTRAL_CONFIG = "use_window_central_config"
CONF_USE_MOTION_CENTRAL_CONFIG = "use_motion_central_config"
CONF_USE_POWER_CENTRAL_CONFIG = "use_power_central_config"
CONF_USE_PRESENCE_CENTRAL_CONFIG = "use_presence_central_config"
CONF_USE_PRESETS_CENTRAL_CONFIG = "use_presets_central_config"
CONF_USE_ADVANCED_CENTRAL_CONFIG = "use_advanced_central_config"
CONF_USE_CENTRAL_MODE = "use_central_mode"
CONF_ADD_CENTRAL_BOILER_CONTROL = "add_central_boiler_control"
CONF_CENTRAL_BOILER_ACTIVATION_SRV = "central_boiler_activation_service"
CONF_CENTRAL_BOILER_DEACTIVATION_SRV = "central_boiler_deactivation_service"
CONF_USED_BY_CENTRAL_BOILER = "used_by_controls_central_boiler"
DEFAULT_SHORT_EMA_PARAMS = {
"max_alpha": 0.5,
# In sec
"halflife_sec": 300,
"precision": 2,
}
CONF_PRESETS = {
p: f"{p}_temp"
for p in (
PRESET_FROST_PROTECTION,
PRESET_ECO,
PRESET_COMFORT,
PRESET_BOOST,
@@ -89,6 +150,7 @@ CONF_PRESETS = {
CONF_PRESETS_WITH_AC = {
p: f"{p}_temp"
for p in (
PRESET_FROST_PROTECTION,
PRESET_ECO,
PRESET_COMFORT,
PRESET_BOOST,
@@ -104,6 +166,7 @@ PRESET_AWAY_SUFFIX = "_away"
CONF_PRESETS_AWAY = {
p: f"{p}_temp"
for p in (
PRESET_FROST_PROTECTION + PRESET_AWAY_SUFFIX,
PRESET_ECO + PRESET_AWAY_SUFFIX,
PRESET_COMFORT + PRESET_AWAY_SUFFIX,
PRESET_BOOST + PRESET_AWAY_SUFFIX,
@@ -113,6 +176,7 @@ CONF_PRESETS_AWAY = {
CONF_PRESETS_AWAY_WITH_AC = {
p: f"{p}_temp"
for p in (
PRESET_FROST_PROTECTION + PRESET_AWAY_SUFFIX,
PRESET_ECO + PRESET_AWAY_SUFFIX,
PRESET_COMFORT + PRESET_AWAY_SUFFIX,
PRESET_BOOST + PRESET_AWAY_SUFFIX,
@@ -122,7 +186,12 @@ CONF_PRESETS_AWAY_WITH_AC = {
)
}
CONF_PRESETS_SELECTIONABLE = [PRESET_ECO, PRESET_COMFORT, PRESET_BOOST]
CONF_PRESETS_SELECTIONABLE = [
PRESET_FROST_PROTECTION,
PRESET_ECO,
PRESET_COMFORT,
PRESET_BOOST,
]
CONF_PRESETS_VALUES = list(CONF_PRESETS.values())
CONF_PRESETS_AWAY_VALUES = list(CONF_PRESETS_AWAY.values())
@@ -173,6 +242,28 @@ ALL_CONF = (
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_AUTO_FAN_MODE,
CONF_USE_MAIN_CENTRAL_CONFIG,
CONF_USE_TPI_CENTRAL_CONFIG,
CONF_USE_PRESETS_CENTRAL_CONFIG,
CONF_USE_WINDOW_CENTRAL_CONFIG,
CONF_USE_MOTION_CENTRAL_CONFIG,
CONF_USE_POWER_CENTRAL_CONFIG,
CONF_USE_PRESENCE_CENTRAL_CONFIG,
CONF_USE_ADVANCED_CENTRAL_CONFIG,
CONF_USE_CENTRAL_MODE,
CONF_ADD_CENTRAL_BOILER_CONTROL,
CONF_USED_BY_CENTRAL_BOILER,
CONF_CENTRAL_BOILER_ACTIVATION_SRV,
CONF_CENTRAL_BOILER_DEACTIVATION_SRV,
]
+ CONF_PRESETS_VALUES
+ CONF_PRESETS_AWAY_VALUES
@@ -184,13 +275,38 @@ 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,
]
CONF_THERMOSTAT_TYPES = [
CONF_THERMOSTAT_CENTRAL_CONFIG,
CONF_THERMOSTAT_SWITCH,
CONF_THERMOSTAT_CLIMATE,
CONF_THERMOSTAT_VALVE,
]
CONF_AUTO_FAN_MODES = [
CONF_AUTO_FAN_NONE,
CONF_AUTO_FAN_LOW,
CONF_AUTO_FAN_MEDIUM,
CONF_AUTO_FAN_HIGH,
CONF_AUTO_FAN_TURBO,
]
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"
SERVICE_SET_AUTO_FAN_MODE = "set_auto_fan_mode"
DEFAULT_SECURITY_MIN_ON_PERCENT = 0.5
DEFAULT_SECURITY_DEFAULT_ON_PERCENT = 0.1
@@ -198,6 +314,91 @@ DEFAULT_SECURITY_DEFAULT_ON_PERCENT = 0.1
ATTR_TOTAL_ENERGY = "total_energy"
ATTR_MEAN_POWER_CYCLE = "mean_cycle_power"
AUTO_FAN_DTEMP_THRESHOLD = 2
AUTO_FAN_DEACTIVATED_MODES = ["mute", "auto", "low"]
CENTRAL_CONFIG_NAME = "Central configuration"
CENTRAL_MODE_AUTO = "Auto"
CENTRAL_MODE_STOPPED = "Stopped"
CENTRAL_MODE_HEAT_ONLY = "Heat only"
CENTRAL_MODE_COOL_ONLY = "Cool only"
CENTRAL_MODE_FROST_PROTECTION = "Frost protection"
CENTRAL_MODES = [
CENTRAL_MODE_AUTO,
CENTRAL_MODE_STOPPED,
CENTRAL_MODE_HEAT_ONLY,
CENTRAL_MODE_COOL_ONLY,
CENTRAL_MODE_FROST_PROTECTION,
]
# A special regulation parameter suggested by @Maia here: https://github.com/jmcollin78/versatile_thermostat/discussions/154
class RegulationParamSlow:
"""Light parameters for slow latency regulation"""
kp: float = 0.2 # 20% of the current internal regulation offset are caused by the current difference of target temperature and room temperature
ki: float = (
0.8 / 288.0
) # 80% of the current internal regulation offset are caused by the average offset of the past 24 hours
k_ext: float = (
1.0 / 25.0
) # this will add 1°C to the offset when it's 25°C colder outdoor than indoor
offset_max: float = 2.0 # limit to a final offset of -2°C to +2°C
stabilization_threshold: float = 0.0 # this needs to be disabled as otherwise the long term accumulated error will always be reset when the temp briefly crosses from/to below/above the target
accumulated_error_threshold: float = (
2.0 * 288
) # this allows up to 2°C long term offset in both directions
class RegulationParamLight:
"""Light parameters for regulation"""
kp: float = 0.2
ki: float = 0.05
k_ext: float = 0.05
offset_max: float = 1.5
stabilization_threshold: float = 0.1
accumulated_error_threshold: float = 10
class RegulationParamMedium:
"""Light parameters for regulation"""
kp: float = 0.3
ki: float = 0.05
k_ext: float = 0.1
offset_max: float = 2
stabilization_threshold: float = 0.1
accumulated_error_threshold: float = 20
class RegulationParamStrong:
"""Strong parameters for regulation
A set of parameters which doesn't take into account the external temp
and concentrate to internal temp error + accumulated error.
This should work for cold external conditions which else generates
high external_offset"""
kp: float = 0.4
ki: float = 0.08
k_ext: float = 0.0
offset_max: float = 5
stabilization_threshold: float = 0.1
accumulated_error_threshold: float = 50
# Not used now
class RegulationParamVeryStrong:
"""Strong parameters for regulation"""
kp: float = 0.6
ki: float = 0.1
k_ext: float = 0.2
offset_max: float = 4
stabilization_threshold: float = 0.1
accumulated_error_threshold: float = 30
class EventType(Enum):
"""The event type that can be sent"""
@@ -206,13 +407,44 @@ class EventType(Enum):
POWER_EVENT: str = "versatile_thermostat_power_event"
TEMPERATURE_EVENT: str = "versatile_thermostat_temperature_event"
HVAC_MODE_EVENT: str = "versatile_thermostat_hvac_mode_event"
CENTRAL_BOILER_EVENT: str = "versatile_thermostat_central_boiler_event"
PRESET_EVENT: str = "versatile_thermostat_preset_event"
WINDOW_AUTO_EVENT: str = "versatile_thermostat_window_auto_event"
def send_vtherm_event(hass, event_type: EventType, entity, data: dict):
"""Send an event"""
_LOGGER.info("%s - Sending event %s with data: %s", entity, event_type, data)
data["entity_id"] = entity.entity_id
data["name"] = entity.name
data["state_attributes"] = entity.state_attributes
hass.bus.fire(event_type.value, data)
class UnknownEntity(HomeAssistantError):
"""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 NoCentralConfig(HomeAssistantError):
"""Error to indicate that we try to use a central configuration but no VTherm of type CENTRAL CONFIGURATION has been found"""
class ServiceConfigurationError(HomeAssistantError):
"""Error in the service configuration to control the central boiler"""
class 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

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

View File

@@ -1,3 +1,4 @@
# pylint: disable=line-too-long
""" This file implements the Open Window by temperature algorithm
This algo works the following way:
- each time a new temperature is measured
@@ -12,8 +13,14 @@ from datetime import datetime
_LOGGER = logging.getLogger(__name__)
# To filter bad values
MIN_DELTA_T_SEC = 10 # two temp mesure should be > 10 sec
MAX_SLOPE_VALUE = 2 # slope cannot be > 2 or < -2 -> else this is an aberrant point
MIN_DELTA_T_SEC = 0 # two temp mesure should be > 0 sec
MAX_SLOPE_VALUE = (
120 # slope cannot be > 2°/min or < -2°/min -> else this is an aberrant point
)
MAX_DURATION_MIN = 30 # a fake data point is added in the cycle if last measurement was older than 30 min
MIN_NB_POINT = 4 # do not calculate slope until we have enough point
class WindowOpenDetectionAlgorithm:
@@ -24,6 +31,7 @@ class WindowOpenDetectionAlgorithm:
_last_slope: float
_last_datetime: datetime
_last_temperature: float
_nb_point: int
def __init__(self, alert_threshold, end_alert_threshold) -> None:
"""Initalize a new algorithm with the both threshold"""
@@ -31,9 +39,24 @@ class WindowOpenDetectionAlgorithm:
self._end_alert_threshold = end_alert_threshold
self._last_slope = None
self._last_datetime = None
self._nb_point = 0
def check_age_last_measurement(self, temperature, datetime_now) -> float:
""" " Check if last measurement is old and add
a fake measurement point if this is the case
"""
if self._last_datetime is None:
return self.add_temp_measurement(temperature, datetime_now)
delta_t_sec = float((datetime_now - self._last_datetime).total_seconds()) / 60.0
if delta_t_sec >= MAX_DURATION_MIN:
return self.add_temp_measurement(temperature, datetime_now, False)
else:
# do nothing
return self._last_slope
def add_temp_measurement(
self, temperature: float, datetime_measure: datetime
self, temperature: float, datetime_measure: datetime, store_date: bool = True
) -> float:
"""Add a new temperature measurement
returns the last slope
@@ -42,6 +65,7 @@ class WindowOpenDetectionAlgorithm:
_LOGGER.debug("First initialisation")
self._last_datetime = datetime_measure
self._last_temperature = temperature
self._nb_point = self._nb_point + 1
return None
_LOGGER.debug(
@@ -61,8 +85,10 @@ class WindowOpenDetectionAlgorithm:
)
return lspe
delta_t_hour = delta_t / 60.0
delta_temp = float(temperature - self._last_temperature)
new_slope = delta_temp / delta_t
new_slope = delta_temp / delta_t_hour
if new_slope > MAX_SLOPE_VALUE or new_slope < -MAX_SLOPE_VALUE:
_LOGGER.debug(
"New_slope is abs(%.2f) > %.2f which should be not possible. We don't consider this value",
@@ -72,21 +98,28 @@ class WindowOpenDetectionAlgorithm:
return lspe
if self._last_slope is None:
self._last_slope = new_slope
self._last_slope = round(new_slope, 2)
else:
self._last_slope = (0.5 * self._last_slope) + (0.5 * new_slope)
self._last_slope = round((0.2 * self._last_slope) + (0.8 * new_slope), 2)
# if we are in cycle check and so adding a fake datapoint, we don't store the event datetime
# so that, when we will receive a real temperature point we will not calculate a wrong slope
if store_date:
self._last_datetime = datetime_measure
self._last_datetime = datetime_measure
self._last_temperature = temperature
self._nb_point = self._nb_point + 1
_LOGGER.debug(
"delta_t=%.3f delta_temp=%.3f new_slope=%.3f last_slope=%s slope=%.3f",
"delta_t=%.3f delta_temp=%.3f new_slope=%.3f last_slope=%s slope=%.3f nb_point=%s",
delta_t,
delta_temp,
new_slope,
lspe,
self._last_slope,
self._nb_point,
)
return self._last_slope
def is_window_open_detected(self) -> bool:
@@ -94,22 +127,20 @@ class WindowOpenDetectionAlgorithm:
if self._alert_threshold is None:
return False
return (
self._last_slope < -self._alert_threshold
if self._last_slope is not None
else False
)
if self._nb_point < MIN_NB_POINT or self._last_slope is None:
return False
return self._last_slope < -self._alert_threshold
def is_window_close_detected(self) -> bool:
"""True if the last calculated slope is above (cause negative) the _end_alert_threshold"""
if self._end_alert_threshold is None:
return False
return (
self._last_slope >= self._end_alert_threshold
if self._last_slope is not None
else False
)
if self._nb_point < MIN_NB_POINT or self._last_slope is None:
return False
return self._last_slope >= self._end_alert_threshold
@property
def last_slope(self) -> float:

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

@@ -25,7 +25,7 @@ class PropAlgorithm:
) -> 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",
"Creation new PropAlgorithm function_type: %s, tpi_coef_int: %s, tpi_coef_ext: %s, cycle_min:%d, minimal_activation_delay:%d", # pylint: disable=line-too-long
function_type,
tpi_coef_int,
tpi_coef_ext,
@@ -45,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 = (
@@ -126,27 +140,27 @@ class PropAlgorithm:
self._off_time_sec = self._cycle_min * 60 - self._on_time_sec
def set_security(self, default_on_percent: float):
"""Set a default value for on_percent (used for security mode)"""
"""Set a default value for on_percent (used for safety mode)"""
self._security = True
self._default_on_percent = default_on_percent
self._calculate_internal()
def unset_security(self):
"""Unset the security mode"""
"""Unset the safety mode"""
self._security = False
self._calculate_internal()
@property
def on_percent(self) -> float:
"""Returns the percentage the heater must be ON
In security mode this value is overriden with the _default_on_percent
In safety mode this value is overriden with the _default_on_percent
(1 means the heater will be always on, 0 never on)""" # pylint: disable=line-too-long
return round(self._on_percent, 2)
@property
def calculated_on_percent(self) -> float:
"""Returns the calculated percentage the heater must be ON
Calculated means NOT overriden even in security mode
Calculated means NOT overriden even in safety mode
(1 means the heater will be always on, 0 never on)""" # pylint: disable=line-too-long
return round(self._calculated_on_percent, 2)

View File

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

View File

@@ -1,4 +0,0 @@
-r requirements_dev.txt
# aiodiscover
ulid_transform
pytest-homeassistant-custom-component

View File

@@ -0,0 +1,134 @@
# pylint: disable=unused-argument
""" Implements the VersatileThermostat select component """
import logging
from homeassistant.const import EVENT_HOMEASSISTANT_START
from homeassistant.core import HomeAssistant, CoreState, callback
from homeassistant.components.climate import ClimateEntity, DOMAIN as CLIMATE_DOMAIN
from homeassistant.components.select import SelectEntity
from homeassistant.helpers.device_registry import DeviceInfo, DeviceEntryType
from homeassistant.config_entries import ConfigEntry
from homeassistant.helpers.restore_state import RestoreEntity
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.entity_component import EntityComponent
from custom_components.versatile_thermostat.base_thermostat import BaseThermostat
from .const import (
DOMAIN,
DEVICE_MANUFACTURER,
CONF_NAME,
CONF_THERMOSTAT_TYPE,
CONF_THERMOSTAT_CENTRAL_CONFIG,
CENTRAL_MODE_AUTO,
CENTRAL_MODES,
overrides,
)
_LOGGER = logging.getLogger(__name__)
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the VersatileThermostat selects with config flow."""
_LOGGER.debug(
"Calling async_setup_entry entry=%s, data=%s", entry.entry_id, entry.data
)
unique_id = entry.entry_id
name = entry.data.get(CONF_NAME)
vt_type = entry.data.get(CONF_THERMOSTAT_TYPE)
if vt_type != CONF_THERMOSTAT_CENTRAL_CONFIG:
return
entities = [
CentralModeSelect(hass, unique_id, name, entry.data),
]
async_add_entities(entities, True)
class CentralModeSelect(SelectEntity, RestoreEntity):
"""Representation of a Energy sensor which exposes the energy"""
def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None:
"""Initialize the energy sensor"""
self._config_id = unique_id
self._device_name = entry_infos.get(CONF_NAME)
self._attr_name = "Central Mode"
self._attr_unique_id = "central_mode"
self._attr_options = CENTRAL_MODES
self._attr_current_option = CENTRAL_MODE_AUTO
@property
def icon(self) -> str | None:
return "mdi:form-select"
@property
def device_info(self) -> DeviceInfo:
"""Return the device info."""
return DeviceInfo(
entry_type=DeviceEntryType.SERVICE,
identifiers={(DOMAIN, self._config_id)},
name=self._device_name,
manufacturer=DEVICE_MANUFACTURER,
model=DOMAIN,
)
@overrides
async def async_added_to_hass(self) -> None:
await super().async_added_to_hass()
old_state = await self.async_get_last_state()
_LOGGER.debug(
"%s - Calling async_added_to_hass old_state is %s", self, old_state
)
if old_state is not None:
self._attr_current_option = old_state.state
@callback
async def _async_startup_internal(*_):
_LOGGER.debug("%s - Calling async_startup_internal", self)
await self.notify_central_mode_change()
if self.hass.state == CoreState.running:
await _async_startup_internal()
else:
self.hass.bus.async_listen_once(
EVENT_HOMEASSISTANT_START, _async_startup_internal
)
@overrides
async def async_select_option(self, option: str) -> None:
"""Change the selected option."""
old_option = self._attr_current_option
if option == old_option:
return
if option in CENTRAL_MODES:
self._attr_current_option = option
await self.notify_central_mode_change(old_central_mode=old_option)
async def notify_central_mode_change(self, old_central_mode=None):
"""Notify all VTherm that the central_mode have change"""
# Update all VTherm states
component: EntityComponent[ClimateEntity] = self.hass.data[CLIMATE_DOMAIN]
for entity in component.entities:
if isinstance(entity, BaseThermostat):
_LOGGER.debug(
"Changing the central_mode. We have find %s to update",
entity.name,
)
await entity.check_central_mode(
self._attr_current_option, old_central_mode
)
def __str__(self):
return f"VersatileThermostat-{self.name}"

View File

@@ -1,3 +1,4 @@
# pylint: disable=unused-argument
""" Implements the VersatileThermostat sensors component """
import logging
import math
@@ -10,6 +11,7 @@ from homeassistant.components.sensor import (
SensorEntity,
SensorDeviceClass,
SensorStateClass,
UnitOfTemperature,
)
from homeassistant.config_entries import ConfigEntry
@@ -22,7 +24,10 @@ from .const import (
CONF_PROP_FUNCTION,
PROPORTIONAL_FUNCTION_TPI,
CONF_THERMOSTAT_SWITCH,
CONF_THERMOSTAT_VALVE,
CONF_THERMOSTAT_CLIMATE,
CONF_THERMOSTAT_TYPE,
CONF_THERMOSTAT_CENTRAL_CONFIG,
)
THRESHOLD_WATT_KILO = 100
@@ -42,15 +47,23 @@ async def async_setup_entry(
unique_id = entry.entry_id
name = entry.data.get(CONF_NAME)
vt_type = entry.data.get(CONF_THERMOSTAT_TYPE)
if vt_type == CONF_THERMOSTAT_CENTRAL_CONFIG:
return
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) == CONF_THERMOSTAT_SWITCH:
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:
@@ -58,6 +71,12 @@ async def async_setup_entry(
entities.append(OnTimeSensor(hass, unique_id, name, entry.data))
entities.append(OffTimeSensor(hass, unique_id, name, entry.data))
if entry.data.get(CONF_THERMOSTAT_TYPE) == CONF_THERMOSTAT_VALVE:
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)
@@ -192,6 +211,9 @@ class OnPercentSensor(VersatileThermostatBaseEntity, SensorEntity):
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}")
@@ -225,6 +247,48 @@ class OnPercentSensor(VersatileThermostatBaseEntity, SensorEntity):
return 1
class ValveOpenPercentSensor(VersatileThermostatBaseEntity, SensorEntity):
"""Representation of a on percent sensor which exposes the on_percent in a cycle"""
def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None:
"""Initialize the energy sensor"""
super().__init__(hass, unique_id, entry_infos.get(CONF_NAME))
self._attr_name = "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"""
@@ -244,6 +308,10 @@ class OnTimeSensor(VersatileThermostatBaseEntity, SensorEntity):
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}")
@@ -289,6 +357,9 @@ class OffTimeSensor(VersatileThermostatBaseEntity, SensorEntity):
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}")
@@ -330,7 +401,7 @@ class LastTemperatureSensor(VersatileThermostatBaseEntity, SensorEntity):
_LOGGER.debug("%s - climate state change", self._attr_unique_id)
old_state = self._attr_native_value
self._attr_native_value = self.my_climate.last_temperature_mesure
self._attr_native_value = self.my_climate.last_temperature_measure
if old_state != self._attr_native_value:
self.async_write_ha_state()
return
@@ -359,7 +430,7 @@ class LastExtTemperatureSensor(VersatileThermostatBaseEntity, SensorEntity):
_LOGGER.debug("%s - climate state change", self._attr_unique_id)
old_state = self._attr_native_value
self._attr_native_value = self.my_climate.last_ext_temperature_mesure
self._attr_native_value = self.my_climate.last_ext_temperature_measure
if old_state != self._attr_native_value:
self.async_write_ha_state()
return
@@ -418,7 +489,109 @@ class TemperatureSlopeSensor(VersatileThermostatBaseEntity, SensorEntity):
if not self.my_climate:
return None
return self.my_climate.temperature_unit + "/min"
return self.my_climate.temperature_unit + "/hour"
@property
def suggested_display_precision(self) -> int | None:
"""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:

View File

@@ -43,6 +43,7 @@ set_preset_temperature:
- "eco"
- "comfort"
- "boost"
- "frost"
- "eco_ac"
- "comfort_ac"
- "boost_ac"
@@ -76,8 +77,8 @@ set_preset_temperature:
mode: slider
set_security:
name: Set security
description: Change the security parameters
name: Set safety
description: Change the safety parameters
target:
entity:
integration: versatile_thermostat
@@ -96,7 +97,7 @@ set_security:
mode: box
min_on_percent:
name: Minimal on_percent
description: Minimal heating percent value for security preset activation
description: Minimal heating percent value for safety preset activation
required: false
advanced: false
example: "0.5"
@@ -109,8 +110,8 @@ set_security:
unit_of_measurement: "%"
mode: slider
default_on_percent:
name: on_percent used in security mode
description: The default heating percent value in security preset
name: on_percent used in safety mode
description: The default heating percent value in safety preset
required: false
advanced: false
example: "0.1"
@@ -122,3 +123,64 @@ set_security:
step: 0.05
unit_of_measurement: "%"
mode: slider
set_window_bypass:
name: Set Window ByPass
description: Bypass the window state to enable heating with window open.
target:
entity:
integration: versatile_thermostat
fields:
window_bypass:
name: Window ByPass
description: ByPass value
required: true
advanced: false
default: true
selector:
boolean:
set_auto_regulation_mode:
name: Set Auto Regulation mode
description: Change the mode of self-regulation (only for VTherm over climate)
target:
entity:
integration: versatile_thermostat
fields:
auto_regulation_mode:
name: Auto regulation mode
description: Possible values
required: true
advanced: false
default: true
selector:
select:
options:
- "None"
- "Light"
- "Medium"
- "Strong"
- "Slow"
- "Expert"
set_auto_fan_mode:
name: Set Auto Fan mode
description: Change the mode of auto-fan (only for VTherm over climate)
target:
entity:
integration: versatile_thermostat
fields:
auto_fan_mode:
name: Auto fan mode
description: Possible values
required: true
advanced: false
default: true
selector:
select:
options:
- "None"
- "Low"
- "Medium"
- "High"
- "Turbo"

View File

@@ -4,37 +4,62 @@
"flow_title": "Versatile Thermostat configuration",
"step": {
"user": {
"title": "Type of Versatile Thermostat",
"data": {
"thermostat_type": "Thermostat type"
},
"data_description": {
"thermostat_type": "Only one central configuration type is possible"
}
},
"main": {
"title": "Add new Versatile Thermostat",
"description": "Main mandatory attributes",
"data": {
"name": "Name",
"thermostat_type": "Thermostat type",
"temperature_sensor_entity_id": "Temperature sensor entity id",
"external_temperature_sensor_entity_id": "External temperature sensor entity id",
"external_temperature_sensor_entity_id": "Outdoor temperature sensor entity id",
"cycle_min": "Cycle duration (minutes)",
"temp_min": "Minimal temperature allowed",
"temp_max": "Maximal temperature allowed",
"device_power": "Device power (kW)",
"device_power": "Device power",
"use_central_mode": "Enable the control by central entity (need central config). Check to enable the control of the VTherm with the select central_mode entities.",
"use_window_feature": "Use window detection",
"use_motion_feature": "Use motion detection",
"use_power_feature": "Use power management",
"use_presence_feature": "Use presence detection"
"use_presence_feature": "Use presence detection",
"use_main_central_config": "Use central main configuration. Check to use the central main configuration. Uncheck to use a specific main configuration for this VTherm",
"add_central_boiler_control": "Add a central boiler. Check to add a control to your central boiler. You will have to configure the VTherm which will have a control of the central boiler after seecting this checkbox to take effect. If one VTherm need heating, the boiler will be turned on. If no VTherm needs heating, the boiler will be turned off. Commands for turning on/off the central boiler are given in the next configuration page",
"used_by_controls_central_boiler": "Used by central boiler. Check if this VTherm should have control on the central boiler"
},
"data_description": {
"external_temperature_sensor_entity_id": "Outdoor temperature sensor entity id. Not used if central configuration is selected"
}
},
"type": {
"title": "Linked entities",
"description": "Linked entities attributes",
"data": {
"heater_entity_id": "Heater switch",
"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": "Underlying climate",
"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"
"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",
"auto_fan_mode": " Auto fan mode"
},
"data_description": {
"heater_entity_id": "Mandatory heater entity id",
@@ -46,100 +71,162 @@
"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"
"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 inverse the command",
"auto_fan_mode": " Automatically activate fan when huge heating/cooling is necessary"
}
},
"tpi": {
"title": "TPI",
"description": "Time Proportional Integral attributes",
"data": {
"tpi_coef_int": "coef_int",
"tpi_coef_ext": "coef_ext",
"use_tpi_central_config": "Use central TPI configuration"
},
"data_description": {
"tpi_coef_int": "Coefficient to use for internal temperature delta",
"tpi_coef_ext": "Coefficient to use for external temperature delta"
"tpi_coef_ext": "Coefficient to use for external temperature delta",
"use_tpi_central_config": "Check to use the central TPI configuration. Uncheck to use a specific TPI configuration for this VTherm"
}
},
"presets": {
"title": "Presets",
"description": "For each presets, give the target temperature (0 to ignore preset)",
"description": "For each preset set the target temperature (0 to ignore preset)",
"data": {
"eco_temp": "Eco preset",
"comfort_temp": "Comfort preset",
"boost_temp": "Boost preset",
"frost_temp": "Frost protection preset",
"eco_ac_temp": "Eco preset for AC mode",
"comfort_ac_temp": "Comfort preset for AC mode",
"boost_ac_temp": "Boost preset for AC mode",
"use_presets_central_config": "Use central presets configuration"
},
"data_description": {
"eco_temp": "Temperature in Eco preset",
"comfort_temp": "Temperature in Comfort preset",
"boost_temp": "Temperature in Boost preset",
"frost_temp": "Temperature in Frost protection preset",
"eco_ac_temp": "Temperature in Eco preset for AC mode",
"comfort_ac_temp": "Temperature in Comfort preset for AC mode",
"boost_ac_temp": "Temperature in Boost preset for AC mode"
"boost_ac_temp": "Temperature in Boost preset for AC mode",
"use_presets_central_config": "Check to use the central presets configuration. Uncheck to use a specific presets configuration for this VTherm"
}
},
"window": {
"title": "Window management",
"description": "Open window management.\nLeave corresponding entity_id empty if not used\nYou can also configure automatic window open detection based on temperature decrease",
"description": "Open window management.\nYou can also configure automatic window open detection based on temperature decrease",
"data": {
"window_sensor_entity_id": "Window sensor entity id",
"window_delay": "Window sensor delay (seconds)",
"window_auto_open_threshold": "Temperature decrease threshold for automatic window open detection (in °/min)",
"window_auto_close_threshold": "Temperature increase threshold for end of automatic detection (in °/min)",
"window_auto_max_duration": "Maximum duration of automatic window open detection (in min)"
"window_auto_open_threshold": "Temperature decrease threshold for automatic window open detection (in °/hours)",
"window_auto_close_threshold": "Temperature increase threshold for end of automatic detection (in °/hours)",
"window_auto_max_duration": "Maximum duration of automatic window open detection (in min)",
"use_window_central_config": "Use central window configuration"
},
"data_description": {
"window_sensor_entity_id": "Leave empty if no window sensor should be use",
"window_sensor_entity_id": "Leave empty if no window sensor should be used and to use the automatic detection",
"window_delay": "The delay in seconds before sensor detection is taken into account",
"window_auto_open_threshold": "Recommended value: between 0.05 and 0.1. Leave empty if automatic window open detection is not use",
"window_auto_close_threshold": "Recommended value: 0. Leave empty if automatic window open detection is not use",
"window_auto_max_duration": "Recommended value: 60 (one hour). Leave empty if automatic window open detection is not use"
"window_auto_open_threshold": "Recommended value: between 3 and 10. Leave empty if automatic window open detection is not used",
"window_auto_close_threshold": "Recommended value: 0. Leave empty if automatic window open detection is not used",
"window_auto_max_duration": "Recommended value: 60 (one hour). Leave empty if automatic window open detection is not used",
"use_window_central_config": "Check to use the central window configuration. Uncheck to use a specific window configuration for this VTherm"
}
},
"motion": {
"title": "Motion management",
"description": "Motion sensor management. Preset can switch automatically depending of a motion detection\nLeave corresponding entity_id empty if not used.\nmotion_preset and no_motion_preset should be set to the corresponding preset name",
"description": "Motion sensor management. Preset can switch automatically depending on motion detection\nmotion_preset and no_motion_preset should be set to the corresponding preset name",
"data": {
"motion_sensor_entity_id": "Motion sensor entity id",
"motion_delay": "Motion delay (seconds)",
"motion_delay": "Activation delay",
"motion_off_delay": "Deactivation delay",
"motion_preset": "Motion preset",
"no_motion_preset": "No motion preset",
"use_motion_central_config": "Use central motion configuration"
},
"data_description": {
"motion_sensor_entity_id": "The entity id of the motion sensor",
"motion_delay": "Motion activation delay (seconds)",
"motion_off_delay": "Motion deactivation delay (seconds)",
"motion_preset": "Preset to use when motion is detected",
"no_motion_preset": "Preset to use when no motion is detected"
"no_motion_preset": "Preset to use when no motion is detected",
"use_motion_central_config": "Check to use the central motion configuration. Uncheck to use a specific motion configuration for this VTherm"
}
},
"power": {
"title": "Power management",
"description": "Power management attributes.\nGives the power and max power sensor of your home.\nThen specify the power consumption of the heater when on.\nAll sensors and device power should have the same unit (kW or W).\nLeave corresponding entity_id empty if not used.",
"description": "Power management attributes.\nGives the power and max power sensor of your home.\nThen specify the power consumption of the heater when on.\nAll sensors and device power should have the same unit (kW or W).",
"data": {
"power_sensor_entity_id": "Power",
"max_power_sensor_entity_id": "Max power",
"power_temp": "Shedding temperature",
"use_power_central_config": "Use central power configuration"
},
"data_description": {
"power_sensor_entity_id": "Power sensor entity id",
"max_power_sensor_entity_id": "Max power sensor entity id",
"power_temp": "Temperature for Power shedding"
"power_temp": "Temperature for Power shedding",
"use_power_central_config": "Check to use the central power configuration. Uncheck to use a specific power configuration for this VTherm"
}
},
"presence": {
"title": "Presence management",
"description": "Presence management attributes.\nGives the a presence sensor of your home (true is someone is present).\nThen specify either the preset to use when presence sensor is false or the offset in temperature to apply.\nIf preset is given, the offset will not be used.\nLeave corresponding entity_id empty if not used.",
"description": "Presence management attributes.\nGives the a presence sensor of your home (true is someone is present) and give the corresponding temperature preset setting.",
"data": {
"presence_sensor_entity_id": "Presence sensor entity id (true is present)",
"presence_sensor_entity_id": "Presence sensor",
"eco_away_temp": "Eco preset",
"comfort_away_temp": "Comfort preset",
"boost_away_temp": "Boost preset",
"frost_away_temp": "Frost protection preset",
"eco_ac_away_temp": "Eco preset in AC mode",
"comfort_ac_away_temp": "Comfort preset in AC mode",
"boost_ac_away_temp": "Boost pres et in AC mode",
"use_presence_central_config": "Use central presence configuration"
},
"data_description": {
"presence_sensor_entity_id": "Presence sensor entity id",
"eco_away_temp": "Temperature in Eco preset when no presence",
"comfort_away_temp": "Temperature in Comfort preset when no presence",
"boost_away_temp": "Temperature in Boost preset when no presence",
"frost_away_temp": "Temperature in Frost protection preset when no presence",
"eco_ac_away_temp": "Temperature in Eco preset when no presence in AC mode",
"comfort_ac_away_temp": "Temperature in Comfort preset when no presence in AC mode",
"boost_ac_away_temp": "Temperature in Boost preset when no presence in AC mode"
"boost_ac_away_temp": "Temperature in Boost preset when no presence in AC mode",
"use_presence_central_config": "Check to use the central presence configuration. Uncheck to use a specific presence configuration for this VTherm"
}
},
"advanced": {
"title": "Advanced parameters",
"description": "Configuration of advanced parameters. Leave the default values if you don't know what you are doing.\nThis parameters can lead to a very bad temperature or power regulation.",
"description": "Configuration of advanced parameters. Leave the default values if you don't know what you are doing.\nThese parameters can lead to very poor temperature control or bad power regulation.",
"data": {
"minimal_activation_delay": "Minimal activation delay",
"security_delay_min": "Security delay (in minutes)",
"security_min_on_percent": "Minimal power percent for security mode",
"security_default_on_percent": "Power percent to use in security mode"
"security_delay_min": "Safety delay (in minutes)",
"security_min_on_percent": "Minimal power percent to enable safety mode",
"security_default_on_percent": "Power percent to use in safety mode",
"use_advanced_central_config": "Use central advanced configuration"
},
"data_description": {
"minimal_activation_delay": "Delay in seconds under which the equipment will not be activated",
"security_delay_min": "Maximum allowed delay in minutes between two temperature mesures. Above this delay, the thermostat will turn to a security off state",
"security_min_on_percent": "Minimal heating percent value for security preset activation. Below this amount of power percent the thermostat won't go into security preset",
"security_default_on_percent": "The default heating power percent value in security preset. Set to 0 to switch off heater in security present"
"security_delay_min": "Maximum allowed delay in minutes between two temperature measurements. Above this delay the thermostat will turn to a safety off state",
"security_min_on_percent": "Minimal heating percent value for safety preset activation. Below this amount of power percent the thermostat won't go into safety preset",
"security_default_on_percent": "The default heating power percent value in safety preset. Set to 0 to switch off heater in safety preset",
"use_advanced_central_config": "Check to use the central advanced configuration. Uncheck to use a specific advanced configuration for this VTherm"
}
}
},
"error": {
"unknown": "Unexpected error",
"unknown_entity": "Unknown entity id",
"window_open_detection_method": "Only one window open detection method should be used. Use sensor or automatic detection through temperature threshold but not both"
"window_open_detection_method": "Only one window open detection method should be used. Use either window sensor or automatic detection through temperature threshold but not both",
"no_central_config": "You cannot check 'use central configuration' because no central configuration was found. You need to create a Versatile Thermostat of type 'Central Configuration' to use it."
},
"abort": {
"already_configured": "Device is already configured"
@@ -149,37 +236,62 @@
"flow_title": "Versatile Thermostat configuration",
"step": {
"user": {
"title": "Add new Versatile Thermostat",
"title": "Type - {name}",
"data": {
"thermostat_type": "Thermostat type"
},
"data_description": {
"thermostat_type": "Only one central configuration type is possible"
}
},
"main": {
"title": "Main - {name}",
"description": "Main mandatory attributes",
"data": {
"name": "Name",
"thermostat_type": "Thermostat type",
"temperature_sensor_entity_id": "Temperature sensor entity id",
"external_temperature_sensor_entity_id": "External temperature sensor entity id",
"temperature_sensor_entity_id": "Room temperature sensor entity id",
"external_temperature_sensor_entity_id": "Outdoor temperature sensor entity id",
"cycle_min": "Cycle duration (minutes)",
"temp_min": "Minimal temperature allowed",
"temp_max": "Maximal temperature allowed",
"device_power": "Device power (kW)",
"device_power": "Device power",
"use_central_mode": "Enable the control by central entity (need central config). Check to enable the control of the VTherm with the select central_mode entities.",
"use_window_feature": "Use window detection",
"use_motion_feature": "Use motion detection",
"use_power_feature": "Use power management",
"use_presence_feature": "Use presence detection"
"use_presence_feature": "Use presence detection",
"use_main_central_config": "Use central main configuration. Check to use the central main configuration. Uncheck to use a specific main configuration for this VTherm",
"add_central_boiler_control": "Add a central boiler. Check to add a control to your central boiler. You will have to configure the VTherm which will have a control of the central boiler after seecting this checkbox to take effect. If one VTherm need heating, the boiler will be turned on. If no VTherm needs heating, the boiler will be turned off. Commands for turning on/off the central boiler are given in the next configuration page",
"used_by_controls_central_boiler": "Used by central boiler. Check if this VTherm should have control on the central boiler"
},
"data_description": {
"external_temperature_sensor_entity_id": "Outdoor temperature sensor entity id. Not used if central configuration is selected"
}
},
"type": {
"title": "Linked entities",
"title": "Entities - {name}",
"description": "Linked entities attributes",
"data": {
"heater_entity_id": "Heater switch",
"heater_entity2_id": "2nd Heater switch",
"heater_entity3_id": "3rd Heater switch",
"heater_entity4_id": "4th Heater switch",
"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": "Underlying thermostat",
"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"
"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",
"auto_fan_mode": " Auto fan mode"
},
"data_description": {
"heater_entity_id": "Mandatory heater entity id",
@@ -191,100 +303,163 @@
"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"
"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",
"auto_fan_mode": " Automatically activate fan when huge heating/cooling is necessary"
}
},
"tpi": {
"title": "TPI",
"title": "TPI - {name}",
"description": "Time Proportional Integral attributes",
"data": {
"tpi_coef_int": "coef_int",
"tpi_coef_ext": "coef_ext",
"use_tpi_central_config": "Use central TPI configuration"
},
"data_description": {
"tpi_coef_int": "Coefficient to use for internal temperature delta",
"tpi_coef_ext": "Coefficient to use for external temperature delta"
"tpi_coef_ext": "Coefficient to use for external temperature delta",
"use_tpi_central_config": "Check to use the central TPI configuration. Uncheck to use a specific TPI configuration for this VTherm"
}
},
"presets": {
"title": "Presets",
"description": "For each presets, give the target temperature (0 to ignore preset)",
"title": "Presets - {name}",
"description": "For each preset set the target temperature (0 to ignore preset)",
"data": {
"eco_temp": "Eco preset",
"comfort_temp": "Comfort preset",
"boost_temp": "Boost preset",
"frost_temp": "Frost protection preset",
"eco_ac_temp": "Eco preset for AC mode",
"comfort_ac_temp": "Comfort preset for AC mode",
"boost_ac_temp": "Boost preset for AC mode",
"use_presets_central_config": "Use central presets configuration"
},
"data_description": {
"eco_temp": "Temperature in Eco preset",
"comfort_temp": "Temperature in Comfort preset",
"boost_temp": "Temperature in Boost preset",
"frost_temp": "Temperature in Frost protection preset",
"eco_ac_temp": "Temperature in Eco preset for AC mode",
"comfort_ac_temp": "Temperature in Comfort preset for AC mode",
"boost_ac_temp": "Temperature in Boost preset for AC mode"
"boost_ac_temp": "Temperature in Boost preset for AC mode",
"use_presets_central_config": "Check to use the central presets configuration. Uncheck to use a specific presets configuration for this VTherm"
}
},
"window": {
"title": "Window management",
"description": "Open window management.\nLeave corresponding entity_id empty if not used\nYou can also configure automatic window open detection based on temperature decrease",
"title": "Window - {name}",
"description": "Open window management.\nYou can also configure automatic window open detection based on temperature decrease",
"data": {
"window_sensor_entity_id": "Window sensor entity id",
"window_delay": "Window sensor delay (seconds)",
"window_auto_open_threshold": "Temperature decrease threshold for automatic window open detection (in °/min)",
"window_auto_close_threshold": "Temperature increase threshold for end of automatic detection (in °/min)",
"window_auto_max_duration": "Maximum duration of automatic window open detection (in min)"
"window_auto_open_threshold": "Temperature decrease threshold for automatic window open detection (in °/hours)",
"window_auto_close_threshold": "Temperature increase threshold for end of automatic detection (in °/hours)",
"window_auto_max_duration": "Maximum duration of automatic window open detection (in min)",
"use_window_central_config": "Use central window configuration"
},
"data_description": {
"window_sensor_entity_id": "Leave empty if no window sensor should be use",
"window_sensor_entity_id": "Leave empty if no window sensor should be used and to use the automatic detection",
"window_delay": "The delay in seconds before sensor detection is taken into account",
"window_auto_open_threshold": "Recommended value: between 0.05 and 0.1. Leave empty if automatic window open detection is not use",
"window_auto_close_threshold": "Recommended value: 0. Leave empty if automatic window open detection is not use",
"window_auto_max_duration": "Recommended value: 60 (one hour). Leave empty if automatic window open detection is not use"
"window_auto_open_threshold": "Recommended value: between 3 and 10. Leave empty if automatic window open detection is not used",
"window_auto_close_threshold": "Recommended value: 0. Leave empty if automatic window open detection is not used",
"window_auto_max_duration": "Recommended value: 60 (one hour). Leave empty if automatic window open detection is not used",
"use_window_central_config": "Check to use the central window configuration. Uncheck to use a specific window configuration for this VTherm"
}
},
"motion": {
"title": "Motion management",
"description": "Motion sensor management. Preset can switch automatically depending of a motion detection\nLeave corresponding entity_id empty if not used.\nmotion_preset and no_motion_preset should be set to the corresponding preset name",
"title": "Motion - {name}",
"description": "Motion management. Preset can switch automatically depending of a motion detection\nmotion_preset and no_motion_preset should be set to the corresponding preset name",
"data": {
"motion_sensor_entity_id": "Motion sensor entity id",
"motion_delay": "Motion delay (seconds)",
"motion_delay": "Activation delay",
"motion_off_delay": "Deactivation delay",
"motion_preset": "Motion preset",
"no_motion_preset": "No motion preset",
"use_motion_central_config": "Use central motion configuration"
},
"data_description": {
"motion_sensor_entity_id": "The entity id of the motion sensor",
"motion_delay": "Motion activation delay (seconds)",
"motion_off_delay": "Motion deactivation delay (seconds)",
"motion_preset": "Preset to use when motion is detected",
"no_motion_preset": "Preset to use when no motion is detected"
"no_motion_preset": "Preset to use when no motion is detected",
"use_motion_central_config": "Check to use the central motion configuration. Uncheck to use a specific motion configuration for this VTherm"
}
},
"power": {
"title": "Power management",
"description": "Power management attributes.\nGives the power and max power sensor of your home.\nThen specify the power consumption of the heater when on.\nAll sensors and device power should have the same unit (kW or W).\nLeave corresponding entity_id empty if not used.",
"title": "Power - {name}",
"description": "Power management attributes.\nGives the power and max power sensor of your home.\nThen specify the power consumption of the heater when on.\nAll sensors and device power should have the same unit (kW or W).",
"data": {
"power_sensor_entity_id": "Power",
"max_power_sensor_entity_id": "Max power",
"power_temp": "Shedding temperature",
"use_power_central_config": "Use central power configuration"
},
"data_description": {
"power_sensor_entity_id": "Power sensor entity id",
"max_power_sensor_entity_id": "Max power sensor entity id",
"power_temp": "Temperature for Power shedding"
"power_temp": "Temperature for Power shedding",
"use_power_central_config": "Check to use the central power configuration. Uncheck to use a specific power configuration for this VTherm"
}
},
"presence": {
"title": "Presence management",
"description": "Presence management attributes.\nGives the a presence sensor of your home (true is someone is present).\nThen specify either the preset to use when presence sensor is false or the offset in temperature to apply.\nIf preset is given, the offset will not be used.\nLeave corresponding entity_id empty if not used.",
"title": "Presence - {name}",
"description": "Presence management attributes.\nGives the a presence sensor of your home (true is someone is present) and give the corresponding temperature preset setting.",
"data": {
"presence_sensor_entity_id": "Presence sensor entity id (true is present)",
"presence_sensor_entity_id": "Presence sensor",
"eco_away_temp": "Eco away preset",
"comfort_away_temp": "Comfort away preset",
"boost_away_temp": "Boost away preset",
"frost_away_temp": "Frost protection preset",
"eco_ac_away_temp": "Eco away preset in AC mode",
"comfort_ac_away_temp": "Comfort away preset in AC mode",
"boost_ac_away_temp": "Boost away preset in AC mode",
"use_presence_central_config": "Use central presence configuration"
},
"data_description": {
"presence_sensor_entity_id": "Presence sensor entity id",
"eco_away_temp": "Temperature in Eco preset when no presence",
"comfort_away_temp": "Temperature in Comfort preset when no presence",
"boost_away_temp": "Temperature in Boost preset when no presence",
"frost_away_temp": "Temperature in Frost protection preset when no presence",
"eco_ac_away_temp": "Temperature in Eco preset when no presence in AC mode",
"comfort_ac_away_temp": "Temperature in Comfort preset when no presence in AC mode",
"boost_ac_away_temp": "Temperature in Boost preset when no presence in AC mode"
"boost_ac_away_temp": "Temperature in Boost preset when no presence in AC mode",
"use_presence_central_config": "Check to use the central presence configuration. Uncheck to use a specific presence configuration for this VTherm"
}
},
"advanced": {
"title": "Advanced parameters",
"description": "Configuration of advanced parameters. Leave the default values if you don't know what you are doing.\nThis parameters can lead to a very bad temperature or power regulation.",
"title": "Advanced - {name}",
"description": "Advanced parameters. Leave the default values if you don't know what you are doing.\nThese parameters can lead to very poor temperature control or bad power regulation.",
"data": {
"minimal_activation_delay": "Minimal activation delay",
"security_delay_min": "Security delay (in minutes)",
"security_min_on_percent": "Minimal power percent for security mode",
"security_default_on_percent": "Power percent to use in security mode"
"security_delay_min": "Safety delay (in minutes)",
"security_min_on_percent": "Minimal power percent to enable safety mode",
"security_default_on_percent": "Power percent to use in safety mode",
"use_advanced_central_config": "Use central advanced configuration"
},
"data_description": {
"minimal_activation_delay": "Delay in seconds under which the equipment will not be activated",
"security_delay_min": "Maximum allowed delay in minutes between two temperature mesures. Above this delay, the thermostat will turn to a security off state",
"security_min_on_percent": "Minimal heating percent value for security preset activation. Below this amount of power percent the thermostat won't go into security preset",
"security_default_on_percent": "The default heating power percent value in security preset. Set to 0 to switch off heater in security present"
"security_delay_min": "Maximum allowed delay in minutes between two temperature measurements. Above this delay the thermostat will turn to a safety off state",
"security_min_on_percent": "Minimal heating percent value for safety preset activation. Below this amount of power percent the thermostat won't go into safety preset",
"security_default_on_percent": "The default heating power percent value in safety preset. Set to 0 to switch off heater in safety preset",
"use_advanced_central_config": "Check to use the central advanced configuration. Uncheck to use a specific advanced configuration for this VTherm"
}
}
},
"error": {
"unknown": "Unexpected error",
"unknown_entity": "Unknown entity id",
"window_open_detection_method": "Only one window open detection method should be used. Use sensor or automatic detection through temperature threshold but not both"
"window_open_detection_method": "Only one window open detection method should be used. Use either window sensor or automatic detection through temperature threshold but not both",
"no_central_config": "You cannot check 'use central configuration' because no central configuration was found. You need to create a Versatile Thermostat of type 'Central Configuration' to use it.",
"service_configuration_format": "The format of the service configuration is wrong"
},
"abort": {
"already_configured": "Device is already configured"
@@ -293,8 +468,29 @@
"selector": {
"thermostat_type": {
"options": {
"thermostat_central_config": "Central configuration",
"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"
}
},
"auto_fan_mode": {
"options": {
"auto_fan_none": "No auto fan",
"auto_fan_low": "Low",
"auto_fan_medium": "Medium",
"auto_fan_high": "High",
"auto_fan_turbo": "Turbo"
}
}
},
@@ -305,7 +501,7 @@
"preset_mode": {
"state": {
"power": "Shedding",
"security": "Security",
"security": "Safety",
"none": "Manual"
}
}

View File

@@ -1,134 +0,0 @@
""" Test the OpenWindow algorithm """
from datetime import datetime, timedelta
from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import
from ..open_window_algorithm import WindowOpenDetectionAlgorithm
async def test_open_window_algo(
hass: HomeAssistant,
skip_hass_states_is_state,
):
"""Tests the Algo"""
the_algo = WindowOpenDetectionAlgorithm(1.0, 0.0)
assert the_algo.last_slope is None
tz = get_tz(hass) # pylint: disable=invalid-name
now = datetime.now(tz)
event_timestamp = now - timedelta(minutes=5)
last_slope = the_algo.add_temp_measurement(
temperature=10, datetime_measure=event_timestamp
)
# We need at least 2 measurement
assert last_slope is None
assert the_algo.last_slope is None
assert the_algo.is_window_close_detected() is False
assert the_algo.is_window_open_detected() is False
event_timestamp = now - timedelta(minutes=4)
last_slope = the_algo.add_temp_measurement(
temperature=10, datetime_measure=event_timestamp
)
# No slope because same temperature
assert last_slope == 0
assert the_algo.last_slope == 0
assert the_algo.is_window_close_detected() is True
assert the_algo.is_window_open_detected() is False
event_timestamp = now - timedelta(minutes=3)
last_slope = the_algo.add_temp_measurement(
temperature=9, datetime_measure=event_timestamp
)
# A slope is calculated
assert last_slope == -0.5
assert the_algo.last_slope == -0.5
assert the_algo.is_window_close_detected() is False
assert the_algo.is_window_open_detected() is False
# A new temperature with 2 degre less in one minute (value will be rejected)
event_timestamp = now - timedelta(minutes=2)
last_slope = the_algo.add_temp_measurement(
temperature=7, datetime_measure=event_timestamp
)
# A slope is calculated
assert last_slope == -0.5 / 2.0 - 2.0 / 2.0
assert the_algo.last_slope == -1.25
assert the_algo.is_window_close_detected() is False
assert the_algo.is_window_open_detected() is True
# A new temperature with 1 degre less
event_timestamp = now - timedelta(minutes=1)
last_slope = the_algo.add_temp_measurement(
temperature=6, datetime_measure=event_timestamp
)
# A slope is calculated
assert last_slope == -1.25 / 2 - 1.0 / 2.0
assert the_algo.last_slope == -1.125
assert the_algo.is_window_close_detected() is False
assert the_algo.is_window_open_detected() is True
# A new temperature with 0 degre less
event_timestamp = now - timedelta(minutes=0)
last_slope = the_algo.add_temp_measurement(
temperature=6, datetime_measure=event_timestamp
)
# A slope is calculated
assert last_slope == -1.125 / 2
assert the_algo.last_slope == -1.125 / 2
assert the_algo.is_window_close_detected() is False
assert the_algo.is_window_open_detected() is False
# A new temperature with 1 degre more
event_timestamp = now + timedelta(minutes=1)
last_slope = the_algo.add_temp_measurement(
temperature=7, datetime_measure=event_timestamp
)
# A slope is calculated
assert last_slope == -1.125 / 4 + 0.5
assert the_algo.last_slope == 0.21875
assert the_algo.is_window_close_detected() is True
assert the_algo.is_window_open_detected() is False
async def test_open_window_algo_wrong(
hass: HomeAssistant,
skip_hass_states_is_state,
):
"""Tests the Algo with wrong date"""
the_algo = WindowOpenDetectionAlgorithm(1.0, 0.0)
assert the_algo.last_slope is None
tz = get_tz(hass) # pylint: disable=invalid-name
now = datetime.now(tz)
event_timestamp = now - timedelta(minutes=5)
last_slope = the_algo.add_temp_measurement(
temperature=10, datetime_measure=event_timestamp
)
# We need at least 2 measurement
assert last_slope is None
assert the_algo.last_slope is None
assert the_algo.is_window_close_detected() is False
assert the_algo.is_window_open_detected() is False
# The next datetime_measurement cannot be in the past
event_timestamp = now - timedelta(minutes=6)
last_slope = the_algo.add_temp_measurement(
temperature=18, datetime_measure=event_timestamp
)
# No slope because same temperature
assert last_slope is None
assert the_algo.last_slope is None
assert the_algo.is_window_close_detected() is False
assert the_algo.is_window_open_detected() is False

View File

@@ -1,305 +0,0 @@
""" Test the Security featrure """
from unittest.mock import patch, call
from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import
from datetime import timedelta, datetime
import logging
logging.getLogger().setLevel(logging.DEBUG)
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_security_feature(hass: HomeAssistant, skip_hass_states_is_state):
"""Test the security feature and https://github.com/jmcollin78/versatile_thermostat/issues/49:
1. creates a thermostat and check that security is off
2. activate security feature when date is expired
3. change the preset to boost
4. check that security is still on
5. resolve the date issue
6. check that security is off and preset is changed to boost
"""
tz = get_tz(hass) # pylint: disable=invalid-name
entry = MockConfigEntry(
domain=DOMAIN,
title="TheOverSwitchMockName",
unique_id="uniqueId",
data={
"name": "TheOverSwitchMockName",
"thermostat_type": "thermostat_over_switch",
"temperature_sensor_entity_id": "sensor.mock_temp_sensor",
"external_temperature_sensor_entity_id": "sensor.mock_ext_temp_sensor",
"cycle_min": 5,
"temp_min": 15,
"temp_max": 30,
"eco_temp": 17,
"comfort_temp": 18,
"boost_temp": 19,
"use_window_feature": False,
"use_motion_feature": False,
"use_power_feature": False,
"use_presence_feature": False,
"heater_entity_id": "switch.mock_switch",
"proportional_function": "tpi",
"tpi_coef_int": 0.3,
"tpi_coef_ext": 0.01,
"minimal_activation_delay": 30,
"security_delay_min": 5, # 5 minutes
"security_min_on_percent": 0.2,
"security_default_on_percent": 0.1,
},
)
# 1. creates a thermostat and check that security is off
now: datetime = datetime.now(tz=tz)
entity: VersatileThermostat = await create_thermostat(
hass, entry, "climate.theoverswitchmockname"
)
assert entity
assert entity._security_state is False
assert entity.preset_mode is not PRESET_SECURITY
assert entity.preset_modes == [
PRESET_NONE,
PRESET_ECO,
PRESET_COMFORT,
PRESET_BOOST,
]
assert entity._last_ext_temperature_mesure is not None
assert entity._last_temperature_mesure is not None
assert (entity._last_temperature_mesure.astimezone(tz) - now).total_seconds() < 1
assert (
entity._last_ext_temperature_mesure.astimezone(tz) - now
).total_seconds() < 1
# set a preset
assert entity.preset_mode is PRESET_NONE
await entity.async_set_preset_mode(PRESET_COMFORT)
assert entity.preset_mode is PRESET_COMFORT
# Turn On the thermostat
assert entity.hvac_mode == HVACMode.OFF
await entity.async_set_hvac_mode(HVACMode.HEAT)
assert entity.hvac_mode == HVACMode.HEAT
# 2. activate security feature when date is expired
with patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
) as mock_send_event, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on:
event_timestamp = now - timedelta(minutes=6)
# set temperature to 15 so that on_percent will be > security_min_on_percent (0.2)
await send_temperature_change_event(entity, 15, event_timestamp)
assert entity.security_state is True
assert entity.preset_mode == PRESET_SECURITY
assert entity._saved_preset_mode == PRESET_COMFORT
assert entity._prop_algorithm.on_percent == 0.1
assert entity._prop_algorithm.calculated_on_percent == 0.9
assert mock_send_event.call_count == 3
mock_send_event.assert_has_calls(
[
call.send_event(EventType.PRESET_EVENT, {"preset": PRESET_SECURITY}),
call.send_event(
EventType.TEMPERATURE_EVENT,
{
"last_temperature_mesure": event_timestamp.isoformat(),
"last_ext_temperature_mesure": entity._last_ext_temperature_mesure.isoformat(),
"current_temp": 15,
"current_ext_temp": None,
"target_temp": 18,
},
),
call.send_event(
EventType.SECURITY_EVENT,
{
"type": "start",
"last_temperature_mesure": event_timestamp.isoformat(),
"last_ext_temperature_mesure": entity._last_ext_temperature_mesure.isoformat(),
"current_temp": 15,
"current_ext_temp": None,
"target_temp": 18,
},
),
],
any_order=True,
)
assert mock_heater_on.call_count == 1
# 3. Change the preset to Boost (we should stay in SECURITY)
with patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
) as mock_send_event, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on:
await entity.async_set_preset_mode(PRESET_BOOST)
# 4. check that security is still on
assert entity._security_state is True
assert entity._prop_algorithm.on_percent == 0.1
assert entity._prop_algorithm.calculated_on_percent == 0.9
assert entity._saved_preset_mode == PRESET_BOOST
assert entity.preset_mode is PRESET_SECURITY
# 5. resolve the datetime issue
with patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
) as mock_send_event, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on:
event_timestamp = datetime.now()
# set temperature to 15 so that on_percent will be > security_min_on_percent (0.2)
await send_temperature_change_event(entity, 15.2, event_timestamp)
assert entity._security_state is False
assert entity.preset_mode == PRESET_BOOST
assert entity._saved_preset_mode == PRESET_BOOST
assert entity._prop_algorithm.on_percent == 1.0
assert entity._prop_algorithm.calculated_on_percent == 1.0
assert mock_send_event.call_count == 2
mock_send_event.assert_has_calls(
[
call.send_event(EventType.PRESET_EVENT, {"preset": PRESET_BOOST}),
call.send_event(
EventType.SECURITY_EVENT,
{
"type": "end",
"last_temperature_mesure": event_timestamp.astimezone(
tz
).isoformat(),
"last_ext_temperature_mesure": entity._last_ext_temperature_mesure.astimezone(
tz
).isoformat(),
"current_temp": 15.2,
"current_ext_temp": None,
"target_temp": 19,
},
),
],
any_order=True,
)
# Heater is now on
assert mock_heater_on.call_count == 1
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_security_over_climate(
hass: HomeAssistant,
skip_hass_states_is_state,
skip_turn_on_off_heater,
skip_send_event,
):
"""Test that when a underlying climate is not available the VTherm doesn't go into security mode"""
tz = get_tz(hass) # pylint: disable=invalid-name
now: datetime = datetime.now(tz=tz)
entry = MockConfigEntry(
domain=DOMAIN,
title="TheOverClimateMockName",
unique_id="uniqueId",
data=PARTIAL_CLIMATE_CONFIG, # 5 minutes security delay
)
fake_underlying_climate = MockClimate(hass, "mockUniqueId", "MockClimateName", {}, HVACMode.HEAT)
with patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
) as mock_send_event, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
return_value=fake_underlying_climate,
) as mock_find_climate:
entry.add_to_hass(hass)
await hass.config_entries.async_setup(entry.entry_id)
assert entry.state is ConfigEntryState.LOADED
def find_my_entity(entity_id) -> ClimateEntity:
"""Find my new entity"""
component: EntityComponent[ClimateEntity] = hass.data[CLIMATE_DOMAIN]
for entity in component.entities:
if entity.entity_id == entity_id:
return entity
entity = find_my_entity("climate.theoverclimatemockname")
assert entity
assert entity.name == "TheOverClimateMockName"
assert entity._is_over_climate is True
# Because the underlying is HEATING. In real life the underlying will be shut-off
assert entity.hvac_action is HVACAction.HEATING
assert entity.hvac_mode is HVACMode.OFF
assert entity.target_temperature == entity.min_temp
assert entity.preset_modes == [
PRESET_NONE,
PRESET_ECO,
PRESET_COMFORT,
PRESET_BOOST,
]
assert entity.preset_mode is PRESET_NONE
assert entity._security_state is False
# should have been called with EventType.PRESET_EVENT and EventType.HVAC_MODE_EVENT
assert mock_send_event.call_count == 2
mock_send_event.assert_has_calls(
[
# At startup
call.send_event(EventType.PRESET_EVENT, {"preset": PRESET_NONE}),
call.send_event(
EventType.HVAC_MODE_EVENT,
{"hvac_mode": HVACMode.OFF},
),
]
)
assert mock_find_climate.call_count == 1
assert mock_find_climate.mock_calls[0] == call()
mock_find_climate.assert_has_calls([call.find_underlying_entity()])
# Force security mode
assert entity._last_ext_temperature_mesure is not None
assert entity._last_temperature_mesure is not None
assert (entity._last_temperature_mesure.astimezone(tz) - now).total_seconds() < 1
assert (
entity._last_ext_temperature_mesure.astimezone(tz) - now
).total_seconds() < 1
# Tries to turns on the Thermostat
await entity.async_set_hvac_mode(HVACMode.HEAT)
assert entity.hvac_mode == HVACMode.HEAT
# One call more
assert mock_send_event.call_count == 3
mock_send_event.assert_has_calls(
[
call.send_event(
EventType.HVAC_MODE_EVENT,
{"hvac_mode": HVACMode.HEAT},
),
]
)
# 2. activate security feature when date is expired
with patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
) as mock_send_event, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on:
event_timestamp = now - timedelta(minutes=6)
await send_temperature_change_event(entity, 15, event_timestamp)
# Should stay False because a climate is never in security mode
assert entity.security_state is False
assert entity.preset_mode == 'none'
assert entity._saved_preset_mode == 'none'

View File

@@ -1,673 +0,0 @@
""" Test the Window management """
import asyncio
from unittest.mock import patch, call, PropertyMock
from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import
from datetime import datetime, timedelta
import logging
logging.getLogger().setLevel(logging.DEBUG)
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_window_management_time_not_enough(
hass: HomeAssistant, skip_hass_states_is_state
):
"""Test the Window management when time is not enough"""
entry = MockConfigEntry(
domain=DOMAIN,
title="TheOverSwitchMockName",
unique_id="uniqueId",
data={
CONF_NAME: "TheOverSwitchMockName",
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_SWITCH,
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
CONF_CYCLE_MIN: 5,
CONF_TEMP_MIN: 15,
CONF_TEMP_MAX: 30,
"eco_temp": 17,
"comfort_temp": 18,
"boost_temp": 19,
CONF_USE_WINDOW_FEATURE: True,
CONF_USE_MOTION_FEATURE: False,
CONF_USE_POWER_FEATURE: False,
CONF_USE_PRESENCE_FEATURE: False,
CONF_HEATER: "switch.mock_switch",
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
CONF_TPI_COEF_INT: 0.3,
CONF_TPI_COEF_EXT: 0.01,
CONF_MINIMAL_ACTIVATION_DELAY: 30,
CONF_SECURITY_DELAY_MIN: 5,
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
CONF_WINDOW_SENSOR: "binary_sensor.mock_window_sensor",
CONF_WINDOW_DELAY: 0, # important to not been obliged to wait
},
)
entity: VersatileThermostat = await create_thermostat(
hass, entry, "climate.theoverswitchmockname"
)
assert entity
tpi_algo = entity._prop_algorithm
assert tpi_algo
await entity.async_set_hvac_mode(HVACMode.HEAT)
await entity.async_set_preset_mode(PRESET_BOOST)
assert entity.hvac_mode is HVACMode.HEAT
assert entity.preset_mode is PRESET_BOOST
assert entity.overpowering_state is None
assert entity.target_temperature == 19
assert entity.window_state is None
# Open the window, but condition of time is not satisfied and check the thermostat don't turns off
with patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
) as mock_send_event, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off, patch(
"homeassistant.helpers.condition.state", return_value=False
) as mock_condition:
await send_temperature_change_event(entity, 15, datetime.now())
await send_window_change_event(entity, True, False, datetime.now())
# simulate the call to try_window_condition. No need due to 0 WINDOW_DELAY and sleep after event is sent
# await try_window_condition(None)
assert mock_send_event.call_count == 0
assert mock_heater_on.call_count == 1
assert mock_heater_off.call_count == 0
assert mock_condition.call_count == 1
assert entity.window_state == STATE_OFF
# Close the window
try_window_condition = await send_window_change_event(
entity, False, False, datetime.now()
)
# simulate the call to try_window_condition
await try_window_condition(None)
assert entity.window_state == STATE_OFF
entity.remove_thermostat()
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_window_management_time_enough(
hass: HomeAssistant, skip_hass_states_is_state
):
"""Test the Window management when time is enough"""
entry = MockConfigEntry(
domain=DOMAIN,
title="TheOverSwitchMockName",
unique_id="uniqueId",
data={
CONF_NAME: "TheOverSwitchMockName",
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_SWITCH,
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
CONF_CYCLE_MIN: 5,
CONF_TEMP_MIN: 15,
CONF_TEMP_MAX: 30,
"eco_temp": 17,
"comfort_temp": 18,
"boost_temp": 19,
CONF_USE_WINDOW_FEATURE: True,
CONF_USE_MOTION_FEATURE: False,
CONF_USE_POWER_FEATURE: False,
CONF_USE_PRESENCE_FEATURE: False,
CONF_HEATER: "switch.mock_switch",
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
CONF_TPI_COEF_INT: 0.3,
CONF_TPI_COEF_EXT: 0.01,
CONF_MINIMAL_ACTIVATION_DELAY: 30,
CONF_SECURITY_DELAY_MIN: 5,
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
CONF_WINDOW_SENSOR: "binary_sensor.mock_window_sensor",
CONF_WINDOW_DELAY: 0, # important to not been obliged to wait
},
)
entity: VersatileThermostat = await create_thermostat(
hass, entry, "climate.theoverswitchmockname"
)
assert entity
tpi_algo = entity._prop_algorithm
assert tpi_algo
await entity.async_set_hvac_mode(HVACMode.HEAT)
await entity.async_set_preset_mode(PRESET_BOOST)
assert entity.hvac_mode is HVACMode.HEAT
assert entity.preset_mode is PRESET_BOOST
assert entity.overpowering_state is None
assert entity.target_temperature == 19
assert entity.window_state is None
# change temperature to force turning on the heater
with patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
) as mock_send_event, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
return_value=False,
):
await send_temperature_change_event(entity, 15, datetime.now())
# Heater shoud turn-on
assert mock_heater_on.call_count >= 1
assert mock_heater_off.call_count == 0
assert mock_send_event.call_count == 0
# Open the window, condition of time is satisfied, check the thermostat and heater turns off
with patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
) as mock_send_event, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off, patch(
"homeassistant.helpers.condition.state", return_value=True
) as mock_condition, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
return_value=True,
):
await send_window_change_event(entity, True, False, datetime.now())
assert mock_send_event.call_count == 1
mock_send_event.assert_has_calls(
[call.send_event(EventType.HVAC_MODE_EVENT, {"hvac_mode": HVACMode.OFF})]
)
# Heater should not be on
assert mock_heater_on.call_count == 0
# One call in set_hvac_mode turn_off and one call in the control_heating for security
assert mock_heater_off.call_count == 2
assert mock_condition.call_count == 1
assert entity.hvac_mode is HVACMode.OFF
assert entity.window_state == STATE_ON
# Close the window
with patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
) as mock_send_event, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off, patch(
"homeassistant.helpers.condition.state", return_value=True
) as mock_condition, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
return_value=False,
):
try_function = await send_window_change_event(
entity, False, True, datetime.now(), sleep=False
)
await try_function(None)
# Wait for initial delay of heater
await asyncio.sleep(0.3)
assert entity.window_state == STATE_OFF
assert mock_heater_on.call_count == 1
assert mock_send_event.call_count == 1
mock_send_event.assert_has_calls(
[
call.send_event(
EventType.HVAC_MODE_EVENT, {"hvac_mode": HVACMode.HEAT}
),
],
any_order=False,
)
assert entity.preset_mode is PRESET_BOOST
# Clean the entity
entity.remove_thermostat()
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_window_auto_fast(hass: HomeAssistant, skip_hass_states_is_state):
"""Test the Power management"""
entry = MockConfigEntry(
domain=DOMAIN,
title="TheOverSwitchMockName",
unique_id="uniqueId",
data={
CONF_NAME: "TheOverSwitchMockName",
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_SWITCH,
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
CONF_CYCLE_MIN: 5,
CONF_TEMP_MIN: 15,
CONF_TEMP_MAX: 30,
"eco_temp": 17,
"comfort_temp": 18,
"boost_temp": 21,
CONF_USE_WINDOW_FEATURE: True,
CONF_USE_MOTION_FEATURE: False,
CONF_USE_POWER_FEATURE: False,
CONF_USE_PRESENCE_FEATURE: False,
CONF_HEATER: "switch.mock_switch",
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
CONF_TPI_COEF_INT: 0.3,
CONF_TPI_COEF_EXT: 0.01,
CONF_MINIMAL_ACTIVATION_DELAY: 30,
CONF_SECURITY_DELAY_MIN: 5,
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
CONF_WINDOW_AUTO_OPEN_THRESHOLD: 0.1,
CONF_WINDOW_AUTO_CLOSE_THRESHOLD: 0.1,
CONF_WINDOW_AUTO_MAX_DURATION: 10, # Should be 0 for test
},
)
entity: VersatileThermostat = await create_thermostat(
hass, entry, "climate.theoverswitchmockname"
)
assert entity
tz = get_tz(hass) # pylint: disable=invalid-name
now = datetime.now(tz)
tpi_algo = entity._prop_algorithm
assert tpi_algo
await entity.async_set_hvac_mode(HVACMode.HEAT)
await entity.async_set_preset_mode(PRESET_BOOST)
assert entity.hvac_mode is HVACMode.HEAT
assert entity.preset_mode is PRESET_BOOST
assert entity.overpowering_state is None
assert entity.target_temperature == 21
assert entity.window_state is None
# Make the temperature down
with patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
) as mock_send_event, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
return_value=True,
):
event_timestamp = now - timedelta(minutes=4)
await send_temperature_change_event(entity, 19, event_timestamp)
# The heater turns on
assert mock_send_event.call_count == 0
assert mock_heater_on.call_count == 1
assert entity.last_temperature_slope is None
assert entity._window_auto_algo.is_window_open_detected() is False
assert entity._window_auto_algo.is_window_close_detected() is False
assert entity.hvac_mode is HVACMode.HEAT
# send one degre down in one minute
with patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
) as mock_send_event, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
return_value=True,
):
event_timestamp = now - timedelta(minutes=3)
await send_temperature_change_event(entity, 18, event_timestamp)
# The heater turns on
assert mock_send_event.call_count == 2
assert mock_heater_on.call_count == 0
assert mock_heater_off.call_count >= 1
assert entity.last_temperature_slope == -1
assert entity._window_auto_algo.is_window_open_detected() is True
assert entity._window_auto_algo.is_window_close_detected() is False
assert entity.window_auto_state == STATE_ON
assert entity.hvac_mode is HVACMode.OFF
mock_send_event.assert_has_calls(
[
call.send_event(EventType.HVAC_MODE_EVENT, {"hvac_mode": HVACMode.OFF}),
call.send_event(
EventType.WINDOW_AUTO_EVENT,
{"type": "start", "cause": "slope alert", "curve_slope": -1.0},
),
],
any_order=True,
)
# send another 0.1 degre in one minute -> no change
with patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
) as mock_send_event, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
new_callable=PropertyMock,
return_value=False,
):
event_timestamp = now - timedelta(minutes=2)
await send_temperature_change_event(entity, 17.9, event_timestamp)
# The heater turns on
assert mock_send_event.call_count == 0
assert mock_heater_on.call_count == 0
assert mock_heater_off.call_count == 0
assert round(entity.last_temperature_slope, 3) == -0.1 * 0.5 - 1 * 0.5
assert entity._window_auto_algo.is_window_open_detected() is True
assert entity._window_auto_algo.is_window_close_detected() is False
assert entity.window_auto_state == STATE_ON
assert entity.hvac_mode is HVACMode.OFF
# send another plus 1.1 degre in one minute -> restore state
with patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
) as mock_send_event, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
new_callable=PropertyMock,
return_value=False,
):
event_timestamp = now - timedelta(minutes=1)
await send_temperature_change_event(entity, 19, event_timestamp)
# The heater turns on
assert mock_send_event.call_count == 2
mock_send_event.assert_has_calls(
[
call.send_event(
EventType.HVAC_MODE_EVENT, {"hvac_mode": HVACMode.HEAT}
),
call.send_event(
EventType.WINDOW_AUTO_EVENT,
{
"type": "end",
"cause": "end of slope alert",
"curve_slope": 0.27500000000000036,
},
),
],
any_order=True,
)
assert mock_heater_on.call_count == 1
assert mock_heater_off.call_count == 0
assert round(entity.last_temperature_slope, 3) == 0.275
assert entity._window_auto_algo.is_window_open_detected() is False
assert entity._window_auto_algo.is_window_close_detected() is True
assert entity.window_auto_state == STATE_OFF
assert entity.hvac_mode is HVACMode.HEAT
# Clean the entity
entity.remove_thermostat()
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_window_auto_auto_stop(hass: HomeAssistant, skip_hass_states_is_state):
"""Test the Power management"""
entry = MockConfigEntry(
domain=DOMAIN,
title="TheOverSwitchMockName",
unique_id="uniqueId",
data={
CONF_NAME: "TheOverSwitchMockName",
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_SWITCH,
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
CONF_CYCLE_MIN: 5,
CONF_TEMP_MIN: 15,
CONF_TEMP_MAX: 30,
"eco_temp": 17,
"comfort_temp": 18,
"boost_temp": 21,
CONF_USE_WINDOW_FEATURE: True,
CONF_USE_MOTION_FEATURE: False,
CONF_USE_POWER_FEATURE: False,
CONF_USE_PRESENCE_FEATURE: False,
CONF_HEATER: "switch.mock_switch",
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
CONF_TPI_COEF_INT: 0.3,
CONF_TPI_COEF_EXT: 0.01,
CONF_MINIMAL_ACTIVATION_DELAY: 30,
CONF_SECURITY_DELAY_MIN: 5,
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
CONF_WINDOW_AUTO_OPEN_THRESHOLD: 0.1,
CONF_WINDOW_AUTO_CLOSE_THRESHOLD: 0.1,
CONF_WINDOW_AUTO_MAX_DURATION: 0, # Should be 0 for test
},
)
entity: VersatileThermostat = await create_thermostat(
hass, entry, "climate.theoverswitchmockname"
)
assert entity
tz = get_tz(hass) # pylint: disable=invalid-name
now = datetime.now(tz)
tpi_algo = entity._prop_algorithm
assert tpi_algo
await entity.async_set_hvac_mode(HVACMode.HEAT)
await entity.async_set_preset_mode(PRESET_BOOST)
assert entity.hvac_mode is HVACMode.HEAT
assert entity.preset_mode is PRESET_BOOST
assert entity.overpowering_state is None
assert entity.target_temperature == 21
assert entity.window_state is None
# Make the temperature down
with patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
) as mock_send_event, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
return_value=True,
):
event_timestamp = now - timedelta(minutes=4)
await send_temperature_change_event(entity, 19, event_timestamp)
# The heater turns on
assert mock_heater_on.call_count == 1
assert entity.last_temperature_slope is None
assert entity._window_auto_algo.is_window_open_detected() is False
assert entity._window_auto_algo.is_window_close_detected() is False
assert entity.hvac_mode is HVACMode.HEAT
# send one degre down in one minute
with patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
) as mock_send_event, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
return_value=True,
):
event_timestamp = now - timedelta(minutes=3)
await send_temperature_change_event(entity, 18, event_timestamp, sleep=False)
assert mock_send_event.call_count == 2
# The heater turns off
mock_send_event.assert_has_calls(
[
call.send_event(EventType.HVAC_MODE_EVENT, {"hvac_mode": HVACMode.OFF}),
call.send_event(
EventType.WINDOW_AUTO_EVENT,
{
"type": "start",
"cause": "slope alert",
"curve_slope": -1.0,
},
),
],
any_order=True,
)
assert mock_heater_on.call_count == 0
assert mock_heater_off.call_count >= 1
assert entity.last_temperature_slope == -1
assert entity._window_auto_algo.is_window_open_detected() is True
assert entity._window_auto_algo.is_window_close_detected() is False
assert entity.window_auto_state == STATE_ON
assert entity.hvac_mode is HVACMode.OFF
# Waits for automatic disable
with patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
) as mock_send_event, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
return_value=False,
):
await asyncio.sleep(0.3)
assert mock_heater_on.call_count == 1
assert mock_heater_off.call_count == 0
assert round(entity.last_temperature_slope, 3) == -1
# Because the algorithm is not aware of the expiration, for the algo we are still in alert
assert entity._window_auto_algo.is_window_open_detected() is True
assert entity._window_auto_algo.is_window_close_detected() is False
assert entity.window_auto_state == STATE_OFF
assert entity.hvac_mode is HVACMode.HEAT
assert entity.preset_mode is PRESET_BOOST
# Clean the entity
entity.remove_thermostat()
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_window_auto_no_on_percent(
hass: HomeAssistant, skip_hass_states_is_state
):
"""Test the Power management"""
entry = MockConfigEntry(
domain=DOMAIN,
title="TheOverSwitchMockName",
unique_id="uniqueId",
data={
CONF_NAME: "TheOverSwitchMockName",
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_SWITCH,
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
CONF_CYCLE_MIN: 5,
CONF_TEMP_MIN: 15,
CONF_TEMP_MAX: 30,
"eco_temp": 17,
"comfort_temp": 18,
"boost_temp": 21,
CONF_USE_WINDOW_FEATURE: True,
CONF_USE_MOTION_FEATURE: False,
CONF_USE_POWER_FEATURE: False,
CONF_USE_PRESENCE_FEATURE: False,
CONF_HEATER: "switch.mock_switch",
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
CONF_TPI_COEF_INT: 0.3,
CONF_TPI_COEF_EXT: 0.01,
CONF_MINIMAL_ACTIVATION_DELAY: 30,
CONF_SECURITY_DELAY_MIN: 5,
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
CONF_WINDOW_AUTO_OPEN_THRESHOLD: 0.1,
CONF_WINDOW_AUTO_CLOSE_THRESHOLD: 0.1,
CONF_WINDOW_AUTO_MAX_DURATION: 0, # Should be 0 for test
},
)
entity: VersatileThermostat = await create_thermostat(
hass, entry, "climate.theoverswitchmockname"
)
assert entity
tz = get_tz(hass) # pylint: disable=invalid-name
now = datetime.now(tz)
tpi_algo = entity._prop_algorithm
assert tpi_algo
await entity.async_set_hvac_mode(HVACMode.HEAT)
await entity.async_set_preset_mode(PRESET_BOOST)
assert entity.hvac_mode is HVACMode.HEAT
assert entity.preset_mode is PRESET_BOOST
assert entity.overpowering_state is None
assert entity.target_temperature == 21
assert entity.window_state is None
# Make the temperature down
with patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
) as mock_send_event, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
return_value=True,
):
event_timestamp = now - timedelta(minutes=4)
await send_temperature_change_event(entity, 21.5, event_timestamp)
# The heater turns on
assert mock_heater_on.call_count == 0
assert entity.last_temperature_slope is None
assert entity._window_auto_algo.is_window_open_detected() is False
assert entity._window_auto_algo.is_window_close_detected() is False
assert entity.hvac_mode is HVACMode.HEAT
assert entity.proportional_algorithm.on_percent == 0.0
# send one degre down in one minute
with patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
) as mock_send_event, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
return_value=True,
):
event_timestamp = now - timedelta(minutes=3)
await send_temperature_change_event(entity, 20, event_timestamp)
# The heater turns on but no alert because the heater was not heating
assert mock_send_event.call_count == 0
assert mock_heater_on.call_count == 1
assert mock_heater_off.call_count == 0
assert entity.last_temperature_slope == -1.5
assert entity._window_auto_algo.is_window_open_detected() is True
assert entity._window_auto_algo.is_window_close_detected() is False
assert entity.window_auto_state == STATE_OFF
assert entity.hvac_mode is HVACMode.HEAT
# Clean the entity
entity.remove_thermostat()

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,213 @@
# 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, config_entry) -> None:
# """Initialize the thermostat over switch."""
# super().__init__(hass, unique_id, name, config_entry)
_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, config_entry):
"""Initialize the Thermostat"""
super().post_init(config_entry)
self._prop_algorithm = PropAlgorithm(
self._proportional_function,
self._tpi_coef_int,
self._tpi_coef_ext,
self._cycle_min,
self._minimal_activation_delay,
)
lst_switches = [config_entry.get(CONF_HEATER)]
if config_entry.get(CONF_HEATER_2):
lst_switches.append(config_entry.get(CONF_HEATER_2))
if config_entry.get(CONF_HEATER_3):
lst_switches.append(config_entry.get(CONF_HEATER_3))
if config_entry.get(CONF_HEATER_4):
lst_switches.append(config_entry.get(CONF_HEATER_4))
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 = config_entry.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,210 @@
# 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, config_entry) -> None:
# """Initialize the thermostat over switch."""
# super().__init__(hass, unique_id, name, config_entry)
@property
def is_over_valve(self) -> bool:
"""True if the Thermostat is over_valve"""
return True
@property
def valve_open_percent(self) -> int:
"""Gives the percentage of valve needed"""
if self._hvac_mode == HVACMode.OFF:
return 0
else:
return round(max(0, min(self.proportional_algorithm.on_percent, 1)) * 100)
@overrides
def post_init(self, config_entry):
"""Initialize the Thermostat"""
super().post_init(config_entry)
self._prop_algorithm = PropAlgorithm(
self._proportional_function,
self._tpi_coef_int,
self._tpi_coef_ext,
self._cycle_min,
self._minimal_activation_delay,
)
lst_valves = [config_entry.get(CONF_VALVE)]
if config_entry.get(CONF_VALVE_2):
lst_valves.append(config_entry.get(CONF_VALVE_2))
if config_entry.get(CONF_VALVE_3):
lst_valves.append(config_entry.get(CONF_VALVE_3))
if config_entry.get(CONF_VALVE_4):
lst_valves.append(config_entry.get(CONF_VALVE_4))
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.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,392 @@
{
"title": "Διαμόρφωση Ευέλικτου Θερμοστάτη",
"config": {
"flow_title": "Διαμόρφωση Ευέλικτου Θερμοστάτη",
"step": {
"user": {
"title": "Προσθήκη νέου Ευέλικτου Θερμοστάτη",
"description": "Κύρια υποχρεωτικά χαρακτηριστικά",
"data": {
"name": "Όνομα",
"thermostat_type": "Τύπος Θερμοστάτη",
"temperature_sensor_entity_id": "Ταυτότητα οντότητας αισθητήρα θερμοκρασίας",
"external_temperature_sensor_entity_id": "Ταυτότητα οντότητας εξωτερικού αισθητήρα θερμοκρασίας",
"cycle_min": "Διάρκεια κύκλου (λεπτά)",
"temp_min": "Ελάχιστη επιτρεπτή θερμοκρασία",
"temp_max": "Μέγιστη επιτρεπτή θερμοκρασία",
"device_power": "Ισχύς συσκευής",
"use_window_feature": "Χρήση ανίχνευσης παραθύρου",
"use_motion_feature": "Χρήση ανίχνευσης κίνησης",
"use_power_feature": "Χρήση διαχείρισης ισχύος",
"use_presence_feature": "Χρήση ανίχνευσης παρουσίας"
}
},
"type": {
"title": "Συνδεδεμένες οντότητες",
"description": "Χαρακτηριστικά συνδεδεμένων οντοτήτων",
"data": {
"heater_entity_id": "1ος διακόπτης θερμαντήρα",
"heater_entity2_id": "2ος διακόπτης θερμαντήρα",
"heater_entity3_id": "3ος διακόπτης θερμαντήρα",
"heater_entity4_id": "4ος διακόπτης θερμαντήρα",
"proportional_function": "Αλγόριθμος",
"climate_entity_id": "1η υποκείμενη κλιματική οντότητα",
"climate_entity2_id": "2η υποκείμενη κλιματική οντότητα",
"climate_entity3_id": "3η υποκείμενη κλιματική οντότητα",
"climate_entity4_id": "4η υποκείμενη κλιματική οντότητα",
"ac_mode": "Λειτουργία AC",
"valve_entity_id": "1ος αριθμός βαλβίδας",
"valve_entity2_id": "2ος αριθμός βαλβίδας",
"valve_entity3_id": "3ος αριθμός βαλβίδας",
"valve_entity4_id": "4ος αριθμός βαλβίδας",
"auto_regulation_mode": "Αυτόματη ρύθμιση",
"auto_regulation_dtemp": "Όριο ρύθμισης",
"auto_regulation_periode_min": "Ελάχιστη περίοδος ρύθμισης",
"inverse_switch_command": "Αντίστροφη εντολή διακόπτη",
"auto_fan_mode": " Auto fan mode"
},
"data_description": {
"heater_entity_id": "Υποχρεωτική ταυτότητα οντότητας θερμαντήρα",
"heater_entity2_id": "Προαιρετική 2η ταυτότητα οντότητας θερμαντήρα. Αφήστε κενό αν δεν χρησιμοποιείται",
"heater_entity3_id": "Προαιρετική 3η ταυτότητα οντότητας θερμαντήρα. Αφήστε κενό αν δεν χρησιμοποιείται",
"heater_entity4_id": "Προαιρετική 4η ταυτότητα οντότητας θερμαντήρα. Αφήστε κενό αν δεν χρησιμοποιείται",
"proportional_function": "Αλγόριθμος προς χρήση (TPI είναι ο μόνος για τώρα)",
"climate_entity_id": "Ταυτότητα υποκείμενης κλιματικής οντότητας",
"climate_entity2_id": "2η ταυτότητα υποκείμενης κλιματικής οντότητας",
"climate_entity3_id": "3η ταυτότητα υποκείμενης κλιματικής οντότητας",
"climate_entity4_id": "4η ταυτότητα υποκείμενης κλιματικής οντότητας",
"ac_mode": "Χρήση της λειτουργίας Κλιματισμού (AC)",
"valve_entity_id": "1η ταυτότητα αριθμού βαλβίδας",
"valve_entity2_id": "2η ταυτότητα αριθμού βαλβίδας",
"valve_entity3_id": "3η ταυτότητα αριθμού βαλβίδας",
"valve_entity4_id": "4η ταυτότητα αριθμού βαλβίδας",
"auto_regulation_mode": "Αυτόματη προσαρμογή της στοχευμένης θερμοκρασίας",
"auto_regulation_dtemp": "Το όριο σε ° κάτω από το οποίο η αλλαγή θερμοκρασίας δεν θα αποστέλλεται",
"auto_regulation_periode_min": "Διάρκεια σε λεπτά μεταξύ δύο ενημερώσεων ρύθμισης",
"inverse_switch_command": "Για διακόπτη με πιλοτικό καλώδιο και δίοδο μπορεί να χρειαστεί να αντιστρέψετε την εντολή",
"auto_fan_mode": " Automatically activate fan when huge heating/cooling is necessary"
}
},
"tpi": {
"title": "TPI",
"description": "Χαρακτηριστικά Χρονικά Αναλογικού Ολοκληρωτικού (TPI)",
"data": {
"tpi_coef_int": "Συντελεστής για χρήση στη διαφορά εσωτερικής θερμοκρασίας",
"tpi_coef_ext": "Συντελεστής για χρήση στη διαφορά εξωτερικής θερμοκρασίας"
}
},
"presets": {
"title": "Προκαθορισμένα",
"description": "Για κάθε προκαθορισμένο, δώστε την επιθυμητή θερμοκρασία (0 για να αγνοηθεί το προκαθορισμένο)",
"data": {
"eco_temp": "Θερμοκρασία στο προκαθορισμένο Eco",
"comfort_temp": "Θερμοκρασία στο προκαθορισμένο Comfort",
"boost_temp": "Θερμοκρασία στο προκαθορισμένο Boost",
"frost_temp": "Θερμοκρασία στο προκαθορισμένο Frost protection",
"eco_ac_temp": "Θερμοκρασία στο προκαθορισμένο Eco για λειτουργία AC",
"comfort_ac_temp": "Θερμοκρασία στο προκαθορισμένο Comfort για λειτουργία AC",
"boost_ac_temp": "Θερμοκρασία στο προκαθορισμένο Boost για λειτουργία AC"
}
},
"window": {
"title": "Διαχείριση Παραθύρων",
"description": "Ανοίξτε τη διαχείριση παραθύρων.\nΑφήστε το αντίστοιχο entity_id κενό αν δεν χρησιμοποιείται\nΜπορείτε επίσης να ρυθμίσετε αυτόματη ανίχνευση ανοίγματος παραθύρου με βάση τη μείωση της θερμοκρασίας",
"data": {
"window_sensor_entity_id": "Ταυτότητα οντότητας αισθητήρα παραθύρου",
"window_delay": "Καθυστέρηση αισθητήρα παραθύρου (δευτερόλεπτα)",
"window_auto_open_threshold": "Κατώφλι μείωσης θερμοκρασίας για αυτόματη ανίχνευση ανοίγματος παραθύρου (σε °/λεπτό)",
"window_auto_close_threshold": "Κατώφλι αύξησης θερμοκρασίας για τέλος αυτόματης ανίχνευσης (σε °/λεπτό)",
"window_auto_max_duration": "Μέγιστη διάρκεια αυτόματης ανίχνευσης ανοίγματος παραθύρου (σε λεπτά)"
},
"data_description": {
"window_sensor_entity_id": "Αφήστε κενό αν δεν πρέπει να χρησιμοποιηθεί αισθητήρας παραθύρου",
"window_delay": "Η καθυστέρηση σε δευτερόλεπτα πριν ληφθεί υπόψη η ανίχνευση του αισθητήρα",
"window_auto_open_threshold": "Συνιστώμενη τιμή: μεταξύ 0.05 και 0.1. Αφήστε κενό αν δεν χρησιμοποιείται αυτόματη ανίχνευση ανοίγματος παραθύρου",
"window_auto_close_threshold": "Συνιστώμενη τιμή: 0. Αφήστε κενό αν δεν χρησιμοποιείται αυτόματη ανίχνευση ανοίγματος παραθύρου",
"window_auto_max_duration": "Συνιστώμενη τιμή: 60 (μία ώρα). Αφήστε κενό αν δεν χρησιμοποιείται αυτόματη ανίχνευση ανοίγματος παραθύρου"
}
},
"motion": {
"title": "Διαχείριση Κίνησης",
"description": "Διαχείριση αισθητήρα κίνησης. Το προκαθορισμένο μπορεί να αλλάζει αυτόματα ανάλογα με ανίχνευση κίνησης\nΑφήστε το αντίστοιχο entity_id κενό αν δεν χρησιμοποιείται.\nΟι επιλογές motion_preset και no_motion_preset πρέπει να οριστούν στο αντίστοιχο όνομα προκαθορισμένου",
"data": {
"motion_sensor_entity_id": "Ταυτότητα οντότητας αισθητήρα κίνησης",
"motion_delay": "Καθυστέρηση ενεργοποίησης",
"motion_off_delay": "Καθυστέρηση απενεργοποίησης",
"motion_preset": "Προκαθορισμένο κίνησης",
"no_motion_preset": "Προκαθορισμένο χωρίς κίνηση"
},
"data_description": {
"motion_sensor_entity_id": "Η ταυτότητα οντότητας του αισθητήρα κίνησης",
"motion_delay": "Καθυστέρηση ενεργοποίησης κίνησης (δευτερόλεπτα)",
"motion_off_delay": "Καθυστέρηση απενεργοποίησης κίνησης (δευτερόλεπτα)",
"motion_preset": "Το προκαθορισμένο που θα χρησιμοποιηθεί όταν ανιχνευθεί κίνηση",
"no_motion_preset": "Το προκαθορισμένο που θα χρησιμοποιηθεί όταν δεν ανιχνευθεί κίνηση"
}
},
"power": {
"title": "Διαχείριση Ενέργειας",
"description": "Χαρακτηριστικά διαχείρισης ενέργειας.\nΔίνει τον αισθητήρα ενέργειας και τον μέγιστο αισθητήρα ενέργειας του σπιτιού σας.\nΣτη συνέχεια καθορίστε την κατανάλωση ενέργειας του θερμαντήρα όταν είναι ενεργοποιημένος.\nΌλοι οι αισθητήρες και η ισχύς της συσκευής πρέπει να έχουν την ίδια μονάδα (kW ή W).\nΑφήστε το αντίστοιχο entity_id κενό αν δεν χρησιμοποιείται.",
"data": {
"power_sensor_entity_id": "Ταυτότητα οντότητας αισθητήρα ενέργειας",
"max_power_sensor_entity_id": "Ταυτότητα οντότητας αισθητήρα μέγιστης ενέργειας",
"power_temp": "Θερμοκρασία για Αποβολή Ενέργειας"
}
},
"presence": {
"title": "Διαχείριση Παρουσίας",
"description": "Χαρακτηριστικά διαχείρισης παρουσίας.\nΔίνει έναν αισθητήρα παρουσίας του σπιτιού σας (αληθές αν κάποιος είναι παρών).\nΣτη συνέχεια καθορίστε είτε το προκαθορισμένο που θα χρησιμοποιηθεί όταν ο αισθητήρας παρουσίας είναι ψευδής ή την απόκλιση στη θερμοκρασία που θα εφαρμοστεί.\nΑν δοθεί προκαθορισμένο, η απόκλιση δεν θα χρησιμοποιηθεί.\nΑφήστε το αντίστοιχο entity_id κενό αν δεν χρησιμοποιείται.",
"data": {
"presence_sensor_entity_id": "Ταυτότητα οντότητας αισθητήρα παρουσίας",
"eco_away_temp": "Θερμοκρασία στο προκαθορισμένο Eco όταν δεν υπάρχει παρουσία",
"comfort_away_temp": "Θερμοκρασία στο προκαθορισμένο Comfort όταν δεν υπάρχει παρουσία",
"boost_away_temp": "Θερμοκρασία στο προκαθορισμένο Boost όταν δεν υπάρχει παρουσία",
"frost_away_temp": "Θερμοκρασία στο προκαθορισμένο Frost protection όταν δεν υπάρχει παρουσία",
"eco_ac_away_temp": "Θερμοκρασία στο προκαθορισμένο Eco όταν δεν υπάρχει παρουσία σε λειτουργία AC",
"comfort_ac_away_temp": "Θερμοκρασία στο προκαθορισμένο Comfort όταν δεν υπάρχει παρουσία σε λειτουργία AC",
"boost_ac_away_temp": "Θερμοκρασία στο προκαθορισμένο Boost όταν δεν υπάρχει παρουσία σε λειτουργία AC"
}
},
"advanced": {
"title": "Προχωρημένες Παράμετροι",
"description": "Διαμόρφωση των προχωρημένων παραμέτρων. Αφήστε τις προεπιλεγμένες τιμές αν δεν γνωρίζετε τι κάνετε.\nΑυτές οι παράμετροι μπορούν να οδηγήσουν σε πολύ κακή ρύθμιση θερμοκρασίας ή ενέργειας.",
"data": {
"minimal_activation_delay": "Ελάχιστη καθυστέρηση ενεργοποίησης",
"security_delay_min": "Καθυστέρηση ασφαλείας (σε λεπτά)",
"security_min_on_percent": "Ελάχιστο ποσοστό ισχύος για ενεργοποίηση λειτουργίας ασφαλείας",
"security_default_on_percent": "Ποσοστό ισχύος για χρήση σε λειτουργία ασφαλείας"
},
"data_description": {
"minimal_activation_delay": "Καθυστέρηση σε δευτερόλεπτα κάτω από την οποία η συσκευή δεν θα ενεργοποιηθεί",
"security_delay_min": "Μέγιστη επιτρεπτή καθυστέρηση σε λεπτά μεταξύ δύο μετρήσεων θερμοκρασίας. Πέρα από αυτή την καθυστέρηση, ο θερμοστάτης θα μεταβεί σε κατάσταση ασφαλείας",
"security_min_on_percent": "Ελάχιστη τιμή ποσοστού θέρμανσης για την ενεργοποίηση του προεπιλεγμένου ασφάλειας. Κάτω από αυτό το ποσοστό ισχύος το θερμοστάτη δεν θα πάει στο προεπιλεγμένο ασφάλειας.",
"security_default_on_percent": "Η προεπιλεγμένη τιμή ποσοστού ισχύος θέρμανσης στο προεπιλεγμένο ασφάλειας. Ορίστε σε 0 για να απενεργοποιήσετε τη θερμάστρα στο παρόν ασφάλειας."
}
}
},
"error": {
"unknown": "Απρόσμενο σφάλμα",
"unknown_entity": "Άγνωστο αναγνωριστικό οντότητας",
"window_open_detection_method": "Πρέπει να χρησιμοποιείται μόνο μία μέθοδος ανίχνευσης ανοιχτού παραθύρου. Χρησιμοποιήστε αισθητήρα ή αυτόματη ανίχνευση μέσω του κατωφλίου θερμοκρασίας, αλλά όχι και τα δύο"
},
"abort": {
"already_configured": "Η συσκευή έχει ήδη ρυθμιστεί"
}
},
"options": {
"flow_title": "Διαμόρφωση Ευέλικτου Θερμοστάτη",
"step": {
"user": {
"title": "Προσθήκη νέου Ευέλικτου Θερμοστάτη",
"description": "Κύρια υποχρεωτικά χαρακτηριστικά",
"data": {
"name": "Όνομα",
"thermostat_type": "Τύπος θερμοστάτη",
"temperature_sensor_entity_id": "Ταυτότητα οντότητας αισθητήρα θερμοκρασίας",
"external_temperature_sensor_entity_id": "Ταυτότητα οντότητας εξωτερικού αισθητήρα θερμοκρασίας",
"cycle_min": "Διάρκεια κύκλου (λεπτά)",
"temp_min": "Ελάχιστη επιτρεπόμενη θερμοκρασία",
"temp_max": "Μέγιστη επιτρεπόμενη θερμοκρασία",
"device_power": "Ισχύς συσκευής (kW)",
"use_window_feature": "Χρήση ανίχνευσης παραθύρου",
"use_motion_feature": "Χρήση ανίχνευσης κίνησης",
"use_power_feature": "Χρήση διαχείρισης ενέργειας",
"use_presence_feature": "Χρήση ανίχνευσης παρουσίας"
}
},
"type": {
"title": "Συνδεδεμένες οντότητες",
"description": "Χαρακτηριστικά συνδεδεμένων οντοτήτων",
"data": {
"heater_entity_id": "1ος διακόπτης θερμαντήρα",
"heater_entity2_id": "2ος διακόπτης θερμαντήρα",
"heater_entity3_id": "3ος διακόπτης θερμαντήρα",
"heater_entity4_id": "4ος διακόπτης θερμαντήρα",
"proportional_function": "Αλγόριθμος",
"climate_entity_id": "1η υποκείμενη κλιματική οντότητα",
"climate_entity2_id": "2η υποκείμενη κλιματική οντότητα",
"climate_entity3_id": "3η υποκείμενη κλιματική οντότητα",
"climate_entity4_id": "4η υποκείμενη κλιματική οντότητα",
"ac_mode": "Λειτουργία AC",
"valve_entity_id": "1ος αριθμός βαλβίδας",
"valve_entity2_id": "2ος αριθμός βαλβίδας",
"valve_entity3_id": "3ος αριθμός βαλβίδας",
"valve_entity4_id": "4ος αριθμός βαλβίδας",
"auto_regulation_mode": "Αυτορύθμιση",
"auto_regulation_dtemp": "Όριο ρύθμισης",
"auto_regulation_periode_min": "Ελάχιστη περίοδος ρύθμισης",
"inverse_switch_command": "Αντίστροφη εντολή διακόπτη",
"auto_fan_mode": " Auto fan mode"
},
"data_description": {
"heater_entity_id": "Υποχρεωτική ταυτότητα οντότητας θερμαντήρα",
"heater_entity2_id": "Προαιρετική ταυτότητα οντότητας 2ου θερμαντήρα. Αφήστε το κενό αν δεν χρησιμοποιείται",
"heater_entity3_id": "Προαιρετική ταυτότητα οντότητας 3ου θερμαντήρα. Αφήστε το κενό αν δεν χρησιμοποιείται",
"heater_entity4_id": "Προαιρετική ταυτότητα οντότητας 4ου θερμαντήρα. Αφήστε το κενό αν δεν χρησιμοποιείται",
"proportional_function": "Αλγόριθμος που θα χρησιμοποιηθεί (TPI είναι ο μόνος για τώρα)",
"climate_entity_id": "Ταυτότητα οντότητας υποκείμενου κλίματος",
"climate_entity2_id": "Ταυτότητα οντότητας 2ου υποκείμενου κλίματος",
"climate_entity3_id": "Ταυτότητα οντότητας 3ου υποκείμενου κλίματος",
"climate_entity4_id": "Ταυτότητα οντότητας 4ου υποκείμενου κλίματος",
"ac_mode": "Χρήση της λειτουργίας Κλιματισμού (AC)",
"valve_entity_id": "Ταυτότητα οντότητας 1ης βαλβίδας",
"valve_entity2_id": "Ταυτότητα οντότητας 2ης βαλβίδας",
"valve_entity3_id": "Ταυτότητα οντότητας 3ης βαλβίδας",
"valve_entity4_id": "Ταυτότητα οντότητας 4ης βαλβίδας",
"auto_regulation_mode": "Αυτόματη ρύθμιση της στοχευόμενης θερμοκρασίας",
"auto_regulation_dtemp": "Το κατώφλι σε °C κάτω από το οποίο η αλλαγή της θερμοκρασίας δεν θα αποστέλλεται",
"auto_regulation_periode_min": "Διάρκεια σε λεπτά μεταξύ δύο ενημερώσεων ρύθμισης",
"inverse_switch_command": "Για διακόπτες με πιλοτικό καλώδιο και δίοδο μπορεί να χρειαστεί να αντιστραφεί η εντολή",
"auto_fan_mode": " Automatically activate fan when huge heating/cooling is necessary"
}
},
"tpi": {
"title": "TPI",
"description": "Χαρακτηριστικά Χρονικού Αναλογικού Ολοκληρωτικού (TPI)",
"data": {
"tpi_coef_int": "Συντελεστής που θα χρησιμοποιηθεί για την εσωτερική διαφορά θερμοκρασίας",
"tpi_coef_ext": "Συντελεστής που θα χρησιμοποιηθεί για την εξωτερική διαφορά θερμοκρασίας"
}
},
"presets": {
"title": "Προεπιλογές",
"description": "Για κάθε προεπιλογή, δώστε τη στοχευόμενη θερμοκρασία (0 για να αγνοηθεί η προεπιλογή)",
"data": {
"eco_temp": "Θερμοκρασία στην οικονομική προεπιλογή",
"comfort_temp": "Θερμοκρασία στην άνετη προεπιλογή",
"boost_temp": "Θερμοκρασία στην ενισχυμένη προεπιλογή",
"frost_temp": "Θερμοκρασία στο προκαθορισμένο Frost protection",
"eco_ac_temp": "Θερμοκρασία στην οικονομική προεπιλογή για τη λειτουργία AC",
"comfort_ac_temp": "Θερμοκρασία στην άνετη προεπιλογή για τη λειτουργία AC",
"boost_ac_temp": "Θερμοκρασία στην ενισχυμένη προεπιλογή για τη λειτουργία AC"
}
},
"window": {
"title": "Διαχείριση παραθύρου",
"description": "Διαχείριση ανοιχτού παραθύρου.\nΑφήστε την αντίστοιχη ταυτότητα οντότητας κενή αν δεν χρησιμοποιείται\nΜπορείτε επίσης να διαμορφώσετε την αυτόματη ανίχνευση ανοίγματος παραθύρου βάσει της μείωσης της θερμοκρασίας",
"data": {
"window_sensor_entity_id": "Ταυτότητα οντότητας αισθητήρα παραθύρου",
"window_delay": "Καθυστέρηση αισθητήρα παραθύρου (δευτερόλεπτα)",
"window_auto_open_threshold": "Όριο μείωσης θερμοκρασίας για αυτόματη ανίχνευση ανοίγματος παραθύρου (σε °/λεπτό)",
"window_auto_close_threshold": "Όριο αύξησης θερμοκρασίας για τέλος αυτόματης ανίχνευσης (σε °/λεπτό)",
"window_auto_max_duration": "Μέγιστη διάρκεια αυτόματης ανίχνευσης ανοίγματος παραθύρου (σε λεπτά)"
},
"data_description": {
"window_sensor_entity_id": "Αφήστε κενό αν δεν πρέπει να χρησιμοποιηθεί αισθητήρας παραθύρου",
"window_delay": "Η καθυστέρηση σε δευτερόλεπτα πριν ληφθεί υπόψη η ανίχνευση αισθητήρα",
"window_auto_open_threshold": "Συνιστώμενη τιμή: μεταξύ 0.05 και 0.1. Αφήστε κενό αν δεν χρησιμοποιείται αυτόματη ανίχνευση ανοίγματος παραθύρου",
"window_auto_close_threshold": "Συνιστώμενη τιμή: 0. Αφήστε κενό αν δεν χρησιμοποιείται αυτόματη ανίχνευση ανοίγματος παραθύρου",
"window_auto_max_duration": "Συνιστώμενη τιμή: 60 (μία ώρα). Αφήστε κενό αν δεν χρησιμοποιείται αυτόματη ανίχνευση ανοίγματος παραθύρου"
}
},
"motion": {
"title": "Διαχείριση κίνησης",
"description": "Διαχείριση αισθητήρα κίνησης. Ο προεπιλεγμένος τρόπος μπορεί να αλλάξει αυτόματα ανάλογα με την ανίχνευση κίνησης\nΑφήστε το αντίστοιχο entity_id κενό αν δεν χρησιμοποιείται.\nΤα motion_preset και no_motion_preset πρέπει να οριστούν στο αντίστοιχο όνομα προεπιλογής",
"data": {
"motion_sensor_entity_id": "Ανιχνευτής κίνησης entity id",
"motion_delay": "Καθυστέρηση ενεργοποίησης",
"motion_off_delay": "Καθυστέρηση απενεργοποίησης",
"motion_preset": "Προεπιλογή κίνησης",
"no_motion_preset": "Προεπιλογή χωρίς κίνηση"
},
"data_description": {
"motion_sensor_entity_id": "Το entity id του ανιχνευτή κίνησης",
"motion_delay": "Καθυστέρηση ενεργοποίησης κίνησης (δευτερόλεπτα)",
"motion_off_delay": "Καθυστέρηση απενεργοποίησης κίνησης (δευτερόλεπτα)",
"motion_preset": "Η προεπιλογή που χρησιμοποιείται όταν ανιχνεύεται κίνηση",
"no_motion_preset": "Η προεπιλογή που χρησιμοποιείται όταν δεν ανιχνεύεται κίνηση"
}
},
"power": {
"title": "Διαχείριση Ενέργειας",
"description": "Χαρακτηριστικά διαχείρισης ενέργειας.\nΠαρέχει τον αισθητήρα ισχύος και τον μέγιστο αισθητήρα ισχύος του σπιτιού σας.\nΣτη συνέχεια καθορίστε την κατανάλωση ενέργειας του θερμαντήρα όταν είναι ενεργοποιημένος.\nΌλοι οι αισθητήρες και η ισχύς της συσκευής πρέπει να έχουν την ίδια μονάδα (kW ή W).\nΑφήστε το αντίστοιχο entity_id κενό εάν δεν χρησιμοποιείται.",
"data": {
"power_sensor_entity_id": "Ταυτότητα οντότητας αισθητήρα ισχύος",
"max_power_sensor_entity_id": "Ταυτότητα οντότητας αισθητήρα μέγιστης ισχύος",
"power_temp": "Θερμοκρασία για Μείωση Ισχύος"
}
},
"presence": {
"title": "Διαχείριση Παρουσίας",
"description": "Χαρακτηριστικά διαχείρισης παρουσίας.\nΠαρέχει έναν αισθητήρα παρουσίας του σπιτιού σας (αληθές εάν κάποιος είναι παρών).\nΣτη συνέχεια καθορίστε είτε το προεπιλεγμένο πρόγραμμα που θα χρησιμοποιηθεί όταν ο αισθητήρας παρουσίας είναι ψευδής είτε την θερμοκρασιακή διαφορά που θα εφαρμοστεί.\nΕάν δίνεται προεπιλογή, η διαφορά δεν θα χρησιμοποιηθεί.\nΑφήστε το αντίστοιχο entity_id κενό εάν δεν χρησιμοποιείται.",
"data": {
"presence_sensor_entity_id": "Ταυτότητα οντότητας αισθητήρα παρουσίας (αληθές είναι παρών)",
"eco_away_temp": "Θερμοκρασία στο πρόγραμμα Eco όταν δεν υπάρχει παρουσία",
"comfort_away_temp": "Θερμοκρασία στο πρόγραμμα Comfort όταν δεν υπάρχει παρουσία",
"boost_away_temp": "Θερμοκρασία στο πρόγραμμα Boost όταν δεν υπάρχει παρουσία",
"frost_away_temp": "Θερμοκρασία στο προκαθορισμένο Frost protection όταν δεν υπάρχει παρουσία",
"eco_ac_away_temp": "Θερμοκρασία στο πρόγραμμα Eco όταν δεν υπάρχει παρουσία σε λειτουργία AC",
"comfort_ac_away_temp": "Θερμοκρασία στο πρόγραμμα Comfort όταν δεν υπάρχει παρουσία σε λειτουργία AC",
"boost_ac_away_temp": "Θερμοκρασία στο πρόγραμμα Boost όταν δεν υπάρχει παρουσία σε λειτουργία AC"
}
},
"advanced": {
"title": "Προηγμένες Παράμετροι",
"description": "Διαμόρφωση των προηγμένων παραμέτρων. Αφήστε τις προεπιλεγμένες τιμές εάν δεν γνωρίζετε τι κάνετε.\nΑυτές οι παράμετροι μπορούν να οδηγήσουν σε πολύ κακή ρύθμιση θερμοκρασίας ή ενέργειας.",
"data": {
"minimal_activation_delay": "Ελάχιστη καθυστέρηση ενεργοποίησης",
"security_delay_min": "Καθυστέρηση ασφαλείας (σε λεπτά)",
"security_min_on_percent": "Ελάχιστο ποσοστό ισχύος για τη λειτουργία ασφαλείας",
"security_default_on_percent": "Ποσοστό ισχύος που θα χρησιμοποιηθεί στη λειτουργία ασφαλείας"
},
"data_description": {
"minimal_activation_delay": "Καθυστέρηση σε δευτερόλεπτα κάτω από την οποία ο εξοπλισμός δεν θα ενεργοποιηθεί",
"security_delay_min": "Μέγιστη επιτρεπόμενη καθυστέρηση σε λεπτά μεταξύ δύο μετρήσεων θερμοκρασίας. Πάνω από αυτή την καθυστέρηση, ο θερμοστάτης θα μεταβεί σε κατάσταση ασφαλείας",
"security_min_on_percent": "Ελάχιστη τιμή ποσοστού θέρμανσης για ενεργοποίηση του προεπιλεγμένου ασφαλείας. Κάτω από αυτό το ποσοστό ισχύος, ο θερμοστάτης δεν θα μεταβεί στο προεπιλεγμένο ασφαλείας",
"security_default_on_percent": "Η προεπιλεγμένη τιμή ποσοστού ισχύος θέρμανσης στο προεπιλεγμένο ασφαλείας. Ορίστε σε 0 για να απενεργοποιήσετε τη θερμάστρα στο παρόν ασφαλείας"
}
}
},
"error": {
"unknown": "Απροσδόκητο λάθος",
"unknown_entity": "Άγνωστο αναγνωριστικό οντότητας",
"window_open_detection_method": "Πρέπει να χρησιμοποιηθεί μόνο μία μέθοδος ανίχνευσης ανοιχτού παραθύρου. Χρησιμοποιήστε αισθητήρα ή αυτόματη ανίχνευση μέσω κατωφλίου θερμοκρασίας αλλά όχι και τα δύο"
},
"abort": {
"already_configured": "Η συσκευή έχει ήδη ρυθμιστεί"
}
},
"selector": {
"thermostat_type": {
"options": {
"thermostat_over_switch": "Θερμοστάτης πάνω σε διακόπτη",
"thermostat_over_climate": "Θερμοστάτης πάνω σε κλίμα",
"thermostat_over_valve": "Θερμοστάτης πάνω σε βαλβίδα"
}
},
"auto_regulation_mode": {
"options": {
"auto_regulation_slow": "Αργή",
"auto_regulation_strong": "Δυνατή",
"auto_regulation_medium": "Μέτρια",
"auto_regulation_light": "Ελαφριά",
"auto_regulation_expert": "Εμπειρογνώμων",
"auto_regulation_none": "Χωρίς αυτόματη ρύθμιση"
}
},
"auto_fan_mode": {
"options": {
"auto_fan_none": "No auto fan",
"auto_fan_low": "Low",
"auto_fan_medium": "Medium",
"auto_fan_high": "High",
"auto_fan_turbo": "Turbo"
}
}
},
"entity": {
"climate": {
"versatile_thermostat": {
"state_attributes": {
"preset_mode": {
"state": {
"power": "Μείωση",
"security": "Ασφάλεια",
"none": "Χειροκίνητο"
}
}
}
}
}
}
}

View File

@@ -4,37 +4,62 @@
"flow_title": "Versatile Thermostat configuration",
"step": {
"user": {
"title": "Type of Versatile Thermostat",
"data": {
"thermostat_type": "Thermostat type"
},
"data_description": {
"thermostat_type": "Only one central configuration type is possible"
}
},
"main": {
"title": "Add new Versatile Thermostat",
"description": "Main mandatory attributes",
"data": {
"name": "Name",
"thermostat_type": "Thermostat type",
"temperature_sensor_entity_id": "Temperature sensor entity id",
"external_temperature_sensor_entity_id": "External temperature sensor entity id",
"external_temperature_sensor_entity_id": "Outdoor temperature sensor entity id",
"cycle_min": "Cycle duration (minutes)",
"temp_min": "Minimal temperature allowed",
"temp_max": "Maximal temperature allowed",
"device_power": "Device power (kW)",
"device_power": "Device power",
"use_central_mode": "Enable the control by central entity (need central config). Check to enable the control of the VTherm with the select central_mode entities.",
"use_window_feature": "Use window detection",
"use_motion_feature": "Use motion detection",
"use_power_feature": "Use power management",
"use_presence_feature": "Use presence detection"
"use_presence_feature": "Use presence detection",
"use_main_central_config": "Use central main configuration. Check to use the central main configuration. Uncheck to use a specific main configuration for this VTherm",
"add_central_boiler_control": "Add a central boiler. Check to add a control to your central boiler. You will have to configure the VTherm which will have a control of the central boiler after seecting this checkbox to take effect. If one VTherm need heating, the boiler will be turned on. If no VTherm needs heating, the boiler will be turned off. Commands for turning on/off the central boiler are given in the next configuration page",
"used_by_controls_central_boiler": "Used by central boiler. Check if this VTherm should have control on the central boiler"
},
"data_description": {
"external_temperature_sensor_entity_id": "Outdoor temperature sensor entity id. Not used if central configuration is selected"
}
},
"type": {
"title": "Linked entities",
"description": "Linked entities attributes",
"data": {
"heater_entity_id": "Heater switch",
"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": "Underlying climate",
"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"
"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",
"auto_fan_mode": " Auto fan mode"
},
"data_description": {
"heater_entity_id": "Mandatory heater entity id",
@@ -46,100 +71,162 @@
"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"
"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 inverse the command",
"auto_fan_mode": " Automatically activate fan when huge heating/cooling is necessary"
}
},
"tpi": {
"title": "TPI",
"description": "Time Proportional Integral attributes",
"data": {
"tpi_coef_int": "coef_int",
"tpi_coef_ext": "coef_ext",
"use_tpi_central_config": "Use central TPI configuration"
},
"data_description": {
"tpi_coef_int": "Coefficient to use for internal temperature delta",
"tpi_coef_ext": "Coefficient to use for external temperature delta"
"tpi_coef_ext": "Coefficient to use for external temperature delta",
"use_tpi_central_config": "Check to use the central TPI configuration. Uncheck to use a specific TPI configuration for this VTherm"
}
},
"presets": {
"title": "Presets",
"description": "For each presets, give the target temperature (0 to ignore preset)",
"description": "For each preset set the target temperature (0 to ignore preset)",
"data": {
"eco_temp": "Eco preset",
"comfort_temp": "Comfort preset",
"boost_temp": "Boost preset",
"frost_temp": "Frost protection preset",
"eco_ac_temp": "Eco preset for AC mode",
"comfort_ac_temp": "Comfort preset for AC mode",
"boost_ac_temp": "Boost preset for AC mode",
"use_presets_central_config": "Use central presets configuration"
},
"data_description": {
"eco_temp": "Temperature in Eco preset",
"comfort_temp": "Temperature in Comfort preset",
"boost_temp": "Temperature in Boost preset",
"frost_temp": "Temperature in Frost protection preset",
"eco_ac_temp": "Temperature in Eco preset for AC mode",
"comfort_ac_temp": "Temperature in Comfort preset for AC mode",
"boost_ac_temp": "Temperature in Boost preset for AC mode"
"boost_ac_temp": "Temperature in Boost preset for AC mode",
"use_presets_central_config": "Check to use the central presets configuration. Uncheck to use a specific presets configuration for this VTherm"
}
},
"window": {
"title": "Window management",
"description": "Open window management.\nLeave corresponding entity_id empty if not used\nYou can also configure automatic window open detection based on temperature decrease",
"description": "Open window management.\nYou can also configure automatic window open detection based on temperature decrease",
"data": {
"window_sensor_entity_id": "Window sensor entity id",
"window_delay": "Window sensor delay (seconds)",
"window_auto_open_threshold": "Temperature decrease threshold for automatic window open detection (in °/min)",
"window_auto_close_threshold": "Temperature increase threshold for end of automatic detection (in °/min)",
"window_auto_max_duration": "Maximum duration of automatic window open detection (in min)"
"window_auto_open_threshold": "Temperature decrease threshold for automatic window open detection (in °/hours)",
"window_auto_close_threshold": "Temperature increase threshold for end of automatic detection (in °/hours)",
"window_auto_max_duration": "Maximum duration of automatic window open detection (in min)",
"use_window_central_config": "Use central window configuration"
},
"data_description": {
"window_sensor_entity_id": "Leave empty if no window sensor should be use",
"window_sensor_entity_id": "Leave empty if no window sensor should be used and to use the automatic detection",
"window_delay": "The delay in seconds before sensor detection is taken into account",
"window_auto_open_threshold": "Recommended value: between 0.05 and 0.1. Leave empty if automatic window open detection is not use",
"window_auto_close_threshold": "Recommended value: 0. Leave empty if automatic window open detection is not use",
"window_auto_max_duration": "Recommended value: 60 (one hour). Leave empty if automatic window open detection is not use"
"window_auto_open_threshold": "Recommended value: between 3 and 10. Leave empty if automatic window open detection is not used",
"window_auto_close_threshold": "Recommended value: 0. Leave empty if automatic window open detection is not used",
"window_auto_max_duration": "Recommended value: 60 (one hour). Leave empty if automatic window open detection is not used",
"use_window_central_config": "Check to use the central window configuration. Uncheck to use a specific window configuration for this VTherm"
}
},
"motion": {
"title": "Motion management",
"description": "Motion sensor management. Preset can switch automatically depending of a motion detection\nLeave corresponding entity_id empty if not used.\nmotion_preset and no_motion_preset should be set to the corresponding preset name",
"description": "Motion sensor management. Preset can switch automatically depending on motion detection\nmotion_preset and no_motion_preset should be set to the corresponding preset name",
"data": {
"motion_sensor_entity_id": "Motion sensor entity id",
"motion_delay": "Motion delay (seconds)",
"motion_delay": "Activation delay",
"motion_off_delay": "Deactivation delay",
"motion_preset": "Motion preset",
"no_motion_preset": "No motion preset",
"use_motion_central_config": "Use central motion configuration"
},
"data_description": {
"motion_sensor_entity_id": "The entity id of the motion sensor",
"motion_delay": "Motion activation delay (seconds)",
"motion_off_delay": "Motion deactivation delay (seconds)",
"motion_preset": "Preset to use when motion is detected",
"no_motion_preset": "Preset to use when no motion is detected"
"no_motion_preset": "Preset to use when no motion is detected",
"use_motion_central_config": "Check to use the central motion configuration. Uncheck to use a specific motion configuration for this VTherm"
}
},
"power": {
"title": "Power management",
"description": "Power management attributes.\nGives the power and max power sensor of your home.\nThen specify the power consumption of the heater when on.\nAll sensors and device power should have the same unit (kW or W).\nLeave corresponding entity_id empty if not used.",
"description": "Power management attributes.\nGives the power and max power sensor of your home.\nThen specify the power consumption of the heater when on.\nAll sensors and device power should have the same unit (kW or W).",
"data": {
"power_sensor_entity_id": "Power",
"max_power_sensor_entity_id": "Max power",
"power_temp": "Shedding temperature",
"use_power_central_config": "Use central power configuration"
},
"data_description": {
"power_sensor_entity_id": "Power sensor entity id",
"max_power_sensor_entity_id": "Max power sensor entity id",
"power_temp": "Temperature for Power shedding"
"power_temp": "Temperature for Power shedding",
"use_power_central_config": "Check to use the central power configuration. Uncheck to use a specific power configuration for this VTherm"
}
},
"presence": {
"title": "Presence management",
"description": "Presence management attributes.\nGives the a presence sensor of your home (true is someone is present).\nThen specify either the preset to use when presence sensor is false or the offset in temperature to apply.\nIf preset is given, the offset will not be used.\nLeave corresponding entity_id empty if not used.",
"description": "Presence management attributes.\nGives the a presence sensor of your home (true is someone is present) and give the corresponding temperature preset setting.",
"data": {
"presence_sensor_entity_id": "Presence sensor entity id (true is present)",
"presence_sensor_entity_id": "Presence sensor",
"eco_away_temp": "Eco preset",
"comfort_away_temp": "Comfort preset",
"boost_away_temp": "Boost preset",
"frost_away_temp": "Frost protection preset",
"eco_ac_away_temp": "Eco preset in AC mode",
"comfort_ac_away_temp": "Comfort preset in AC mode",
"boost_ac_away_temp": "Boost pres et in AC mode",
"use_presence_central_config": "Use central presence configuration"
},
"data_description": {
"presence_sensor_entity_id": "Presence sensor entity id",
"eco_away_temp": "Temperature in Eco preset when no presence",
"comfort_away_temp": "Temperature in Comfort preset when no presence",
"boost_away_temp": "Temperature in Boost preset when no presence",
"frost_away_temp": "Temperature in Frost protection preset when no presence",
"eco_ac_away_temp": "Temperature in Eco preset when no presence in AC mode",
"comfort_ac_away_temp": "Temperature in Comfort preset when no presence in AC mode",
"boost_ac_away_temp": "Temperature in Boost preset when no presence in AC mode"
"boost_ac_away_temp": "Temperature in Boost preset when no presence in AC mode",
"use_presence_central_config": "Check to use the central presence configuration. Uncheck to use a specific presence configuration for this VTherm"
}
},
"advanced": {
"title": "Advanced parameters",
"description": "Configuration of advanced parameters. Leave the default values if you don't know what you are doing.\nThis parameters can lead to a very bad temperature or power regulation.",
"description": "Configuration of advanced parameters. Leave the default values if you don't know what you are doing.\nThese parameters can lead to very poor temperature control or bad power regulation.",
"data": {
"minimal_activation_delay": "Minimal activation delay",
"security_delay_min": "Security delay (in minutes)",
"security_min_on_percent": "Minimal power percent for security mode",
"security_default_on_percent": "Power percent to use in security mode"
"security_delay_min": "Safety delay (in minutes)",
"security_min_on_percent": "Minimal power percent to enable safety mode",
"security_default_on_percent": "Power percent to use in safety mode",
"use_advanced_central_config": "Use central advanced configuration"
},
"data_description": {
"minimal_activation_delay": "Delay in seconds under which the equipment will not be activated",
"security_delay_min": "Maximum allowed delay in minutes between two temperature mesures. Above this delay, the thermostat will turn to a security off state",
"security_min_on_percent": "Minimal heating percent value for security preset activation. Below this amount of power percent the thermostat won't go into security preset",
"security_default_on_percent": "The default heating power percent value in security preset. Set to 0 to switch off heater in security present"
"security_delay_min": "Maximum allowed delay in minutes between two temperature measurements. Above this delay the thermostat will turn to a safety off state",
"security_min_on_percent": "Minimal heating percent value for safety preset activation. Below this amount of power percent the thermostat won't go into safety preset",
"security_default_on_percent": "The default heating power percent value in safety preset. Set to 0 to switch off heater in safety preset",
"use_advanced_central_config": "Check to use the central advanced configuration. Uncheck to use a specific advanced configuration for this VTherm"
}
}
},
"error": {
"unknown": "Unexpected error",
"unknown_entity": "Unknown entity id",
"window_open_detection_method": "Only one window open detection method should be used. Use sensor or automatic detection through temperature threshold but not both"
"window_open_detection_method": "Only one window open detection method should be used. Use either window sensor or automatic detection through temperature threshold but not both",
"no_central_config": "You cannot check 'use central configuration' because no central configuration was found. You need to create a Versatile Thermostat of type 'Central Configuration' to use it."
},
"abort": {
"already_configured": "Device is already configured"
@@ -149,37 +236,62 @@
"flow_title": "Versatile Thermostat configuration",
"step": {
"user": {
"title": "Add new Versatile Thermostat",
"title": "Type - {name}",
"data": {
"thermostat_type": "Thermostat type"
},
"data_description": {
"thermostat_type": "Only one central configuration type is possible"
}
},
"main": {
"title": "Main - {name}",
"description": "Main mandatory attributes",
"data": {
"name": "Name",
"thermostat_type": "Thermostat type",
"temperature_sensor_entity_id": "Temperature sensor entity id",
"external_temperature_sensor_entity_id": "External temperature sensor entity id",
"temperature_sensor_entity_id": "Room temperature sensor entity id",
"external_temperature_sensor_entity_id": "Outdoor temperature sensor entity id",
"cycle_min": "Cycle duration (minutes)",
"temp_min": "Minimal temperature allowed",
"temp_max": "Maximal temperature allowed",
"device_power": "Device power (kW)",
"device_power": "Device power",
"use_central_mode": "Enable the control by central entity (need central config). Check to enable the control of the VTherm with the select central_mode entities.",
"use_window_feature": "Use window detection",
"use_motion_feature": "Use motion detection",
"use_power_feature": "Use power management",
"use_presence_feature": "Use presence detection"
"use_presence_feature": "Use presence detection",
"use_main_central_config": "Use central main configuration. Check to use the central main configuration. Uncheck to use a specific main configuration for this VTherm",
"add_central_boiler_control": "Add a central boiler. Check to add a control to your central boiler. You will have to configure the VTherm which will have a control of the central boiler after seecting this checkbox to take effect. If one VTherm need heating, the boiler will be turned on. If no VTherm needs heating, the boiler will be turned off. Commands for turning on/off the central boiler are given in the next configuration page",
"used_by_controls_central_boiler": "Used by central boiler. Check if this VTherm should have control on the central boiler"
},
"data_description": {
"external_temperature_sensor_entity_id": "Outdoor temperature sensor entity id. Not used if central configuration is selected"
}
},
"type": {
"title": "Linked entities",
"title": "Entities - {name}",
"description": "Linked entities attributes",
"data": {
"heater_entity_id": "Heater switch",
"heater_entity2_id": "2nd Heater switch",
"heater_entity3_id": "3rd Heater switch",
"heater_entity4_id": "4th Heater switch",
"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": "Underlying thermostat",
"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"
"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",
"auto_fan_mode": " Auto fan mode"
},
"data_description": {
"heater_entity_id": "Mandatory heater entity id",
@@ -191,100 +303,163 @@
"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"
"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",
"auto_fan_mode": " Automatically activate fan when huge heating/cooling is necessary"
}
},
"tpi": {
"title": "TPI",
"title": "TPI - {name}",
"description": "Time Proportional Integral attributes",
"data": {
"tpi_coef_int": "coef_int",
"tpi_coef_ext": "coef_ext",
"use_tpi_central_config": "Use central TPI configuration"
},
"data_description": {
"tpi_coef_int": "Coefficient to use for internal temperature delta",
"tpi_coef_ext": "Coefficient to use for external temperature delta"
"tpi_coef_ext": "Coefficient to use for external temperature delta",
"use_tpi_central_config": "Check to use the central TPI configuration. Uncheck to use a specific TPI configuration for this VTherm"
}
},
"presets": {
"title": "Presets",
"description": "For each presets, give the target temperature (0 to ignore preset)",
"title": "Presets - {name}",
"description": "For each preset set the target temperature (0 to ignore preset)",
"data": {
"eco_temp": "Eco preset",
"comfort_temp": "Comfort preset",
"boost_temp": "Boost preset",
"frost_temp": "Frost protection preset",
"eco_ac_temp": "Eco preset for AC mode",
"comfort_ac_temp": "Comfort preset for AC mode",
"boost_ac_temp": "Boost preset for AC mode",
"use_presets_central_config": "Use central presets configuration"
},
"data_description": {
"eco_temp": "Temperature in Eco preset",
"comfort_temp": "Temperature in Comfort preset",
"boost_temp": "Temperature in Boost preset",
"frost_temp": "Temperature in Frost protection preset",
"eco_ac_temp": "Temperature in Eco preset for AC mode",
"comfort_ac_temp": "Temperature in Comfort preset for AC mode",
"boost_ac_temp": "Temperature in Boost preset for AC mode"
"boost_ac_temp": "Temperature in Boost preset for AC mode",
"use_presets_central_config": "Check to use the central presets configuration. Uncheck to use a specific presets configuration for this VTherm"
}
},
"window": {
"title": "Window management",
"description": "Open window management.\nLeave corresponding entity_id empty if not used\nYou can also configure automatic window open detection based on temperature decrease",
"title": "Window - {name}",
"description": "Open window management.\nYou can also configure automatic window open detection based on temperature decrease",
"data": {
"window_sensor_entity_id": "Window sensor entity id",
"window_delay": "Window sensor delay (seconds)",
"window_auto_open_threshold": "Temperature decrease threshold for automatic window open detection (in °/min)",
"window_auto_close_threshold": "Temperature increase threshold for end of automatic detection (in °/min)",
"window_auto_max_duration": "Maximum duration of automatic window open detection (in min)"
"window_auto_open_threshold": "Temperature decrease threshold for automatic window open detection (in °/hours)",
"window_auto_close_threshold": "Temperature increase threshold for end of automatic detection (in °/hours)",
"window_auto_max_duration": "Maximum duration of automatic window open detection (in min)",
"use_window_central_config": "Use central window configuration"
},
"data_description": {
"window_sensor_entity_id": "Leave empty if no window sensor should be use",
"window_sensor_entity_id": "Leave empty if no window sensor should be used and to use the automatic detection",
"window_delay": "The delay in seconds before sensor detection is taken into account",
"window_auto_open_threshold": "Recommended value: between 0.05 and 0.1. Leave empty if automatic window open detection is not use",
"window_auto_close_threshold": "Recommended value: 0. Leave empty if automatic window open detection is not use",
"window_auto_max_duration": "Recommended value: 60 (one hour). Leave empty if automatic window open detection is not use"
"window_auto_open_threshold": "Recommended value: between 3 and 10. Leave empty if automatic window open detection is not used",
"window_auto_close_threshold": "Recommended value: 0. Leave empty if automatic window open detection is not used",
"window_auto_max_duration": "Recommended value: 60 (one hour). Leave empty if automatic window open detection is not used",
"use_window_central_config": "Check to use the central window configuration. Uncheck to use a specific window configuration for this VTherm"
}
},
"motion": {
"title": "Motion management",
"description": "Motion sensor management. Preset can switch automatically depending of a motion detection\nLeave corresponding entity_id empty if not used.\nmotion_preset and no_motion_preset should be set to the corresponding preset name",
"title": "Motion - {name}",
"description": "Motion management. Preset can switch automatically depending of a motion detection\nmotion_preset and no_motion_preset should be set to the corresponding preset name",
"data": {
"motion_sensor_entity_id": "Motion sensor entity id",
"motion_delay": "Motion delay (seconds)",
"motion_delay": "Activation delay",
"motion_off_delay": "Deactivation delay",
"motion_preset": "Motion preset",
"no_motion_preset": "No motion preset",
"use_motion_central_config": "Use central motion configuration"
},
"data_description": {
"motion_sensor_entity_id": "The entity id of the motion sensor",
"motion_delay": "Motion activation delay (seconds)",
"motion_off_delay": "Motion deactivation delay (seconds)",
"motion_preset": "Preset to use when motion is detected",
"no_motion_preset": "Preset to use when no motion is detected"
"no_motion_preset": "Preset to use when no motion is detected",
"use_motion_central_config": "Check to use the central motion configuration. Uncheck to use a specific motion configuration for this VTherm"
}
},
"power": {
"title": "Power management",
"description": "Power management attributes.\nGives the power and max power sensor of your home.\nThen specify the power consumption of the heater when on.\nAll sensors and device power should have the same unit (kW or W).\nLeave corresponding entity_id empty if not used.",
"title": "Power - {name}",
"description": "Power management attributes.\nGives the power and max power sensor of your home.\nThen specify the power consumption of the heater when on.\nAll sensors and device power should have the same unit (kW or W).",
"data": {
"power_sensor_entity_id": "Power",
"max_power_sensor_entity_id": "Max power",
"power_temp": "Shedding temperature",
"use_power_central_config": "Use central power configuration"
},
"data_description": {
"power_sensor_entity_id": "Power sensor entity id",
"max_power_sensor_entity_id": "Max power sensor entity id",
"power_temp": "Temperature for Power shedding"
"power_temp": "Temperature for Power shedding",
"use_power_central_config": "Check to use the central power configuration. Uncheck to use a specific power configuration for this VTherm"
}
},
"presence": {
"title": "Presence management",
"description": "Presence management attributes.\nGives the a presence sensor of your home (true is someone is present).\nThen specify either the preset to use when presence sensor is false or the offset in temperature to apply.\nIf preset is given, the offset will not be used.\nLeave corresponding entity_id empty if not used.",
"title": "Presence - {name}",
"description": "Presence management attributes.\nGives the a presence sensor of your home (true is someone is present) and give the corresponding temperature preset setting.",
"data": {
"presence_sensor_entity_id": "Presence sensor entity id (true is present)",
"presence_sensor_entity_id": "Presence sensor",
"eco_away_temp": "Eco away preset",
"comfort_away_temp": "Comfort away preset",
"boost_away_temp": "Boost away preset",
"frost_away_temp": "Frost protection preset",
"eco_ac_away_temp": "Eco away preset in AC mode",
"comfort_ac_away_temp": "Comfort away preset in AC mode",
"boost_ac_away_temp": "Boost away preset in AC mode",
"use_presence_central_config": "Use central presence configuration"
},
"data_description": {
"presence_sensor_entity_id": "Presence sensor entity id",
"eco_away_temp": "Temperature in Eco preset when no presence",
"comfort_away_temp": "Temperature in Comfort preset when no presence",
"boost_away_temp": "Temperature in Boost preset when no presence",
"frost_away_temp": "Temperature in Frost protection preset when no presence",
"eco_ac_away_temp": "Temperature in Eco preset when no presence in AC mode",
"comfort_ac_away_temp": "Temperature in Comfort preset when no presence in AC mode",
"boost_ac_away_temp": "Temperature in Boost preset when no presence in AC mode"
"boost_ac_away_temp": "Temperature in Boost preset when no presence in AC mode",
"use_presence_central_config": "Check to use the central presence configuration. Uncheck to use a specific presence configuration for this VTherm"
}
},
"advanced": {
"title": "Advanced parameters",
"description": "Configuration of advanced parameters. Leave the default values if you don't know what you are doing.\nThis parameters can lead to a very bad temperature or power regulation.",
"title": "Advanced - {name}",
"description": "Advanced parameters. Leave the default values if you don't know what you are doing.\nThese parameters can lead to very poor temperature control or bad power regulation.",
"data": {
"minimal_activation_delay": "Minimal activation delay",
"security_delay_min": "Security delay (in minutes)",
"security_min_on_percent": "Minimal power percent for security mode",
"security_default_on_percent": "Power percent to use in security mode"
"security_delay_min": "Safety delay (in minutes)",
"security_min_on_percent": "Minimal power percent to enable safety mode",
"security_default_on_percent": "Power percent to use in safety mode",
"use_advanced_central_config": "Use central advanced configuration"
},
"data_description": {
"minimal_activation_delay": "Delay in seconds under which the equipment will not be activated",
"security_delay_min": "Maximum allowed delay in minutes between two temperature mesures. Above this delay, the thermostat will turn to a security off state",
"security_min_on_percent": "Minimal heating percent value for security preset activation. Below this amount of power percent the thermostat won't go into security preset",
"security_default_on_percent": "The default heating power percent value in security preset. Set to 0 to switch off heater in security present"
"security_delay_min": "Maximum allowed delay in minutes between two temperature measurements. Above this delay the thermostat will turn to a safety off state",
"security_min_on_percent": "Minimal heating percent value for safety preset activation. Below this amount of power percent the thermostat won't go into safety preset",
"security_default_on_percent": "The default heating power percent value in safety preset. Set to 0 to switch off heater in safety preset",
"use_advanced_central_config": "Check to use the central advanced configuration. Uncheck to use a specific advanced configuration for this VTherm"
}
}
},
"error": {
"unknown": "Unexpected error",
"unknown_entity": "Unknown entity id",
"window_open_detection_method": "Only one window open detection method should be used. Use sensor or automatic detection through temperature threshold but not both"
"window_open_detection_method": "Only one window open detection method should be used. Use either window sensor or automatic detection through temperature threshold but not both",
"no_central_config": "You cannot check 'use central configuration' because no central configuration was found. You need to create a Versatile Thermostat of type 'Central Configuration' to use it.",
"service_configuration_format": "The format of the service configuration is wrong"
},
"abort": {
"already_configured": "Device is already configured"
@@ -293,8 +468,29 @@
"selector": {
"thermostat_type": {
"options": {
"thermostat_central_config": "Central configuration",
"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"
}
},
"auto_fan_mode": {
"options": {
"auto_fan_none": "No auto fan",
"auto_fan_low": "Low",
"auto_fan_medium": "Medium",
"auto_fan_high": "High",
"auto_fan_turbo": "Turbo"
}
}
},
@@ -305,7 +501,7 @@
"preset_mode": {
"state": {
"power": "Shedding",
"security": "Security",
"security": "Safety",
"none": "Manual"
}
}

View File

@@ -4,20 +4,37 @@
"flow_title": "Versatile Thermostat configuration",
"step": {
"user": {
"title": "Type du nouveau Versatile Thermostat",
"data": {
"thermostat_type": "Type de thermostat"
},
"data_description": {
"thermostat_type": "Un seul thermostat de type Configuration centrale est possible."
}
},
"main": {
"title": "Ajout d'un nouveau thermostat",
"description": "Principaux attributs obligatoires",
"data": {
"name": "Nom",
"thermostat_type": "Type de thermostat",
"temperature_sensor_entity_id": "Température sensor entity id",
"external_temperature_sensor_entity_id": "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_central_mode": "Autoriser le controle par une entity centrale ('nécessite une config. centrale`). Cochez pour autoriser le contrôle du VTherm par la liste déroulante 'central_mode' de l'entité configuration centrale.",
"use_window_feature": "Avec détection des ouvertures",
"use_motion_feature": "Avec détection de mouvement",
"use_power_feature": "Avec gestion de la puissance",
"use_presence_feature": "Avec détection de présence"
"use_presence_feature": "Avec détection de présence",
"use_main_central_config": "Utiliser la configuration centrale. Cochez pour utiliser la configuration centrale. Décochez et saisissez les attributs pour utiliser une configuration spécifique.",
"add_central_boiler_control": "Ajouter une chaudière centrale. Cochez pour ajouter un controle sur une chaudière centrale. Vous devrez ensuite configurer les VTherms qui commande la chaudière centrale pour que cette option prenne effet. Si au moins un des VTherm a besoin de chauffer, la chaudière centrale sera activée. Si aucun VTherm n'a besoin de chauffer, elle sera éteinte. Les commandes pour allumer/éteindre la chaudière centrale sont données dans la page de configuration suivante.",
"used_by_controls_central_boiler": "Utilisé par la chaudière centrale. Cochez si ce VTherm doit contrôler la chaudière centrale."
},
"data_description": {
"external_temperature_sensor_entity_id": "Entity id du capteur de température extérieure."
}
},
"type": {
@@ -33,7 +50,16 @@
"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 ?"
"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",
"auto_fan_mode": " Auto ventilation mode"
},
"data_description": {
"heater_entity_id": "Entity id du 1er radiateur obligatoire",
@@ -45,77 +71,136 @@
"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)"
"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",
"auto_fan_mode": "Active la ventilation automatiquement en cas d'écart important"
}
},
"tpi": {
"title": "TPI",
"description": "Attributs de l'algo Time Proportional Integral",
"data": {
"tpi_coef_int": "coeff_int : Coefficient à utiliser pour le delta de température interne",
"tpi_coef_ext": "coeff_ext : Coefficient à utiliser pour le delta de température externe"
"tpi_coef_int": "coeff_int",
"tpi_coef_ext": "coeff_ext",
"use_tpi_central_config": "Utiliser la configuration TPI centrale"
},
"data_description": {
"tpi_coef_int": "Coefficient à utiliser pour le delta de température interne",
"tpi_coef_ext": "Coefficient à utiliser pour le delta de température externe",
"use_tpi_central_config": "Cochez pour utiliser la configuration TPI centrale. Décochez et saisissez les attributs pour utiliser une configuration TPI spécifique"
}
},
"presets": {
"title": "Presets",
"description": "Pour chaque preset, donnez la température cible (0 pour ignorer le preset)",
"data": {
"eco_temp": "Preset Eco",
"comfort_temp": "Preset Comfort",
"boost_temp": "Preset Boost",
"frost_temp": "Preset Hors-gel",
"eco_ac_temp": "Preset Eco en mode AC",
"comfort_ac_temp": "Preset Comfort en mode AC",
"boost_ac_temp": "Preset Boost en mode AC",
"use_presets_central_config": "Utiliser la configuration des presets centrale"
},
"data_description": {
"eco_temp": "Température en preset Eco",
"comfort_temp": "Température en preset Comfort",
"boost_temp": "Température en preset Boost",
"frost_temp": "Température en preset Hors-gel",
"eco_ac_temp": "Température en preset Eco en mode AC",
"comfort_ac_temp": "Température en preset Comfort en mode AC",
"boost_ac_temp": "Température en preset Boost en mode AC"
"boost_ac_temp": "Température en preset Boost en mode AC",
"use_presets_central_config": "Cochez pour utiliser la configuration des presets centrale. Décochez et saisissez les attributs pour utiliser une configuration des presets spécifique"
}
},
"window": {
"title": "Gestion d'une ouverture",
"description": "Coupe le radiateur si l'ouverture est ouverte.\nLaissez l'entity id vide si non utilisé.",
"description": "Coupe le radiateur si l'ouverture est ouverte.\nLaissez l'id d'entité vide pour utiliser la détection automatique.",
"data": {
"window_sensor_entity_id": "Détecteur d'ouverture (entity id)",
"window_delay": "Délai avant extinction (secondes)",
"window_auto_open_threshold": "seuil haut de chute de température pour la détection automatique (en °/min)",
"window_auto_close_threshold": "Seuil bas de chute de température pour la fin de détection automatique (en °/min)",
"window_auto_max_duration": "Durée maximum d'une extinction automatique (en min)"
"window_auto_open_threshold": "Seuil haut de chute de température pour la détection automatique (en °/heure)",
"window_auto_close_threshold": "Seuil bas de chute de température pour la fin de détection automatique (en °/heure)",
"window_auto_max_duration": "Durée maximum d'une extinction automatique (en min)",
"use_window_central_config": "Utiliser la configuration centrale des ouvertures"
},
"data_description": {
"window_sensor_entity_id": "Laissez vide si vous n'avez de détecteur",
"window_sensor_entity_id": "Laissez vide si vous n'avez de détecteur et pour utiliser la détection automatique",
"window_delay": "Le délai (en secondes) avant que le changement du détecteur soit pris en compte",
"window_auto_open_threshold": "Valeur recommandée: entre 0.05 et 0.1. Laissez vide si vous n'utilisez pas la détection automatique",
"window_auto_open_threshold": "Valeur recommandée: entre 3 et 10. Laissez vide si vous n'utilisez pas la détection automatique",
"window_auto_close_threshold": "Valeur recommandée: 0. Laissez vide si vous n'utilisez pas la détection automatique",
"window_auto_max_duration": "Valeur recommandée: 60 (1 heure). Laissez vide si vous n'utilisez pas la détection automatique"
"window_auto_max_duration": "Valeur recommandée: 60 (1 heure). Laissez vide si vous n'utilisez pas la détection automatique",
"use_window_central_config": "Cochez pour utiliser la configuration centrale des ouvertures. Décochez et saisissez les attributs pour utiliser une configuration spécifique des ouvertures"
}
},
"motion": {
"title": "Gestion de la détection de mouvement",
"description": "Le preset s'ajuste automatiquement si un mouvement est détecté\nLaissez l'entity id vide si non utilisé.\n'Preset mouvement' et 'Preset sans mouvement' doivent être choisis avec les preset à utiliser.",
"description": "Le preset s'ajuste automatiquement si un mouvement est détecté\n'Preset mouvement' et 'Preset sans mouvement' doivent être choisis avec les preset à utiliser.",
"data": {
"motion_sensor_entity_id": "Détecteur de mouvement entity id",
"motion_delay": "Délai avant changement (seconds)",
"motion_sensor_entity_id": "Détecteur de mouvement",
"motion_delay": "Délai d'activation",
"motion_off_delay": "Délai de désactivation",
"motion_preset": "Preset si mouvement",
"no_motion_preset": "Preset sans mouvement",
"use_motion_central_config": "Utiliser la condfiguration centrale du mouvement"
},
"data_description": {
"motion_sensor_entity_id": "Id d'entité du détecteur de mouvement",
"motion_delay": "Délai avant activation lorsqu'un mouvement est détecté (secondss)",
"motion_off_delai": "Délai avant désactivation lorsqu'aucun mouvement n'est détecté (secondes)",
"motion_preset": "Preset à utiliser si mouvement détecté",
"no_motion_preset": "Preset à utiliser si pas de mouvement détecté"
"no_motion_preset": "Preset à utiliser si pas de mouvement détecté",
"use_motion_central_config": "Cochez pour utiliser la configuration centrale du mouvement. Décochez et saisissez les attributs pour utiliser une configuration spécifique du mouvement"
}
},
"power": {
"title": "Gestion de l'énergie",
"title": "Gestion de la puissance",
"description": "Sélectionne automatiquement le preset 'power' si la puissance consommée est supérieure à un maximum.\nDonnez les entity id des capteurs qui mesurent la puissance totale et la puissance max autorisée.\nEnsuite donnez la puissance de l'équipement.\nTous les capteurs et la puissance consommée par l'équipement doivent avoir la même unité de mesure (kW ou W).",
"data": {
"power_sensor_entity_id": "Capteur de puissance totale (entity id)",
"max_power_sensor_entity_id": "Capteur de puissance Max (entity id)",
"power_temp": "Température si délestaqe"
"power_temp": "Température si délestaqe",
"use_power_central_config": "Utiliser la configuration centrale de la puissance"
},
"data_description": {
"power_sensor_entity_id": "Entity id du capteur de puissance totale du logement",
"max_power_sensor_entity_id": "Entity id du capteur de puissance Max autorisée avant délestage",
"power_temp": "Température cible si délestaqe",
"use_power_central_config": "Cochez pour utiliser la configuration centrale de la puissance. Décochez et saisissez les attributs pour utiliser une configuration spécifique de la puissance"
}
},
"presence": {
"title": "Gestion de la présence",
"description": "Donnez un capteur de présence (true si quelqu'un est présent).\nEnsuite spécifiez soit un preset à utiliser, soit un offset de température à appliquer lorsque personne n'est présent.\nSi le préset est utilisé, l'offset ne sera pas pris en compte.\nLaissez l'entity id vide si la gestion de la présence est non utilisée.",
"description": "Donnez un capteur de présence (true si quelqu'un est présent) et les températures cibles à utiliser en cas d'absence.",
"data": {
"presence_sensor_entity_id": "Capteur de présence entity id (true si quelqu'un est présent)",
"presence_sensor_entity_id": "Capteur de présence",
"eco_away_temp": "preset Eco",
"comfort_away_temp": "preset Comfort",
"boost_away_temp": "preset Boost",
"frost_away_temp": "preset Hors-gel",
"eco_ac_away_temp": "preset Eco en mode AC",
"comfort_ac_away_temp": "preset Comfort en mode AC",
"boost_ac_away_temp": "preset Boost en mode AC",
"use_presence_central_config": "Utiliser la configuration centrale de la présence"
},
"data_description": {
"presence_sensor_entity_id": "Id d'entité du capteur de présence",
"eco_away_temp": "Température en preset Eco en cas d'absence",
"comfort_away_temp": "Température en preset Comfort en cas d'absence",
"boost_away_temp": "Température en preset Boost en cas d'absence",
"frost_away_temp": "Température en preset Hors-gel en cas d'absence",
"eco_ac_away_temp": "Température en preset Eco en cas d'absence en mode AC",
"comfort_ac_away_temp": "Température en preset Comfort en cas d'absence en mode AC",
"boost_ac_away_temp": "Température en preset Boost en cas d'absence en mode AC"
"boost_ac_away_temp": "Température en preset Boost en cas d'absence en mode AC",
"use_presence_central_config": "Cochez pour utiliser la configuration centrale de la présence. Décochez et saisissez les attributs pour utiliser une configuration spécifique de la présence"
}
},
"advanced": {
@@ -125,20 +210,35 @@
"minimal_activation_delay": "Délai minimal d'activation",
"security_delay_min": "Délai maximal entre 2 mesures de températures",
"security_min_on_percent": "Pourcentage minimal de puissance",
"security_default_on_percent": "Pourcentage de puissance a utiliser en mode securité"
"security_default_on_percent": "Pourcentage de puissance a utiliser en mode securité",
"use_advanced_central_config": "Utiliser la configuration centrale avancée"
},
"data_description": {
"minimal_activation_delay": "Délai en seondes en-dessous duquel l'équipement ne sera pas activé",
"security_delay_min": "Délai maximal autorisé en minutes entre 2 mesures de températures. Au-dessus de ce délai, le thermostat se mettra en position de sécurité",
"security_min_on_percent": "Seuil minimal de pourcentage de chauffage en-dessous duquel le préréglage sécurité ne sera jamais activé",
"security_default_on_percent": "Valeur par défaut pour le pourcentage de chauffage en mode sécurité. Mettre 0 pour éteindre le radiateur en mode sécurité"
"security_default_on_percent": "Valeur par défaut pour le pourcentage de chauffage en mode sécurité. Mettre 0 pour éteindre le radiateur en mode sécurité",
"use_advanced_central_config": "Cochez pour utiliser la configuration centrale avancée. Décochez et saisissez les attributs pour utiliser une configuration spécifique avancée"
}
},
"central_boiler": {
"title": "Contrôle de la chaudière centrale",
"description": "Donnez les services à appeler pour allumer/éteindre la chaudière centrale. Laissez vide, si aucun appel de service ne doit être effectué (dans ce cas, vous devrez gérer vous même l'allumage/extinction de votre chaudière centrale). Le service a appelé doit être formatté comme suit: `entity_id/service_name[/attribut:valeur]` (/attribut:valeur est facultatif)\nPar exemple:\n- pour allumer un switch: `switch.controle_chaudiere/switch.turn_on`\n- pour éteindre un switch: `switch.controle_chaudiere/switch.turn_off`\n- pour programmer la chaudière sur 25° et ainsi forcer son allumage: `climate.thermostat_chaudiere/climate.set_temperature/temperature:25`\n- pour envoyer 10° à la chaudière et ainsi forcer son extinction: `climate.thermostat_chaudiere/climate.set_temperature/temperature:10`",
"data": {
"central_boiler_activation_service": "Commande pour allumer",
"central_boiler_deactivation_service": "Commande pour éteindre"
},
"data_description": {
"central_boiler_activation_service": "Commande à éxecuter pour allumer la chaudière centrale au format entity_id/service_name[/attribut:valeur]",
"central_boiler_deactivation_service": "Commande à éxecuter pour étiendre la chaudière centrale au format entity_id/service_name[/attribut:valeur]"
}
}
},
"error": {
"unknown": "Erreur inattendue",
"unknown_entity": "entity id inconnu",
"window_open_detection_method": "Une seule méthode de détection des ouvertures ouvertes doit être utilisée. Utilisez le détecteur d'ouverture ou les seuils de température mais pas les deux."
"window_open_detection_method": "Une seule méthode de détection des ouvertures ouvertes doit être utilisée. Utilisez le détecteur d'ouverture ou les seuils de température mais pas les deux.",
"no_central_config": "Vous ne pouvez pas cocher 'Utiliser la configuration centrale' car aucune configuration centrale n'a été trouvée. Vous devez créer un Versatile Thermostat de type 'Central Configuration' pour pouvoir l'utiliser."
},
"abort": {
"already_configured": "Le device est déjà configuré"
@@ -148,26 +248,41 @@
"flow_title": "Versatile Thermostat configuration",
"step": {
"user": {
"title": "Ajout d'un nouveau thermostat",
"title": "Type - {name}",
"data": {
"thermostat_type": "Type de thermostat"
},
"data_description": {
"thermostat_type": "Un seul thermostat de type Configuration centrale est possible."
}
},
"main": {
"title": "Attributs - {name}",
"description": "Principaux attributs obligatoires",
"data": {
"name": "Nom",
"thermostat_over_switch": "Thermostat sur un switch",
"thermostat_over_climate": "Thermostat sur un autre thermostat",
"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_central_mode": "Autoriser le controle par une entity centrale ('nécessite une config. centrale`). Cochez pour autoriser le contrôle du VTherm par la liste déroulante 'central_mode' de l'entité configuration centrale.",
"use_window_feature": "Avec détection des ouvertures",
"use_motion_feature": "Avec détection de mouvement",
"use_power_feature": "Avec gestion de la puissance",
"use_presence_feature": "Avec détection de présence"
"use_presence_feature": "Avec détection de présence",
"use_main_central_config": "Utiliser la configuration centrale. Cochez pour utiliser la configuration centrale. Décochez et saisissez les attributs pour utiliser une configuration spécifique.",
"add_central_boiler_control": "Ajouter une chaudière centrale. Cochez pour ajouter un controle sur une chaudière centrale. Vous devrez ensuite configurer les VTherms qui commande la chaudière centrale pour que cette option prenne effet. Si au moins un des VTherm a besoin de chauffer, la chaudière centrale sera activée. Si aucun VTherm n'a besoin de chauffer, elle sera éteinte. Les commandes pour allumer/éteindre la chaudière centrale sont données dans la page de configuration suivante.",
"used_by_controls_central_boiler": "Utilisé par la chaudière centrale. Cochez si ce VTherm doit contrôler la chaudière centrale."
},
"data_description": {
"external_temperature_sensor_entity_id": "Entity id du capteur de température extérieure. N'est pas utilisé si la configuration centrale est utilisée."
}
},
"type": {
"title": "Entité(s) liée(s)",
"title": "Entités - {name}",
"description": "Attributs de(s) l'entité(s) liée(s)",
"data": {
"heater_entity_id": "1er radiateur",
@@ -179,7 +294,16 @@
"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 ?"
"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",
"auto_fan_mode": " Auto fan mode"
},
"data_description": {
"heater_entity_id": "Entity id du 1er radiateur obligatoire",
@@ -191,11 +315,20 @@
"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)"
"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",
"auto_fan_mode": "Active la ventilation automatiquement en cas d'écart important"
}
},
"tpi": {
"title": "TPI",
"title": "TPI - {name}",
"description": "Attributs de l'algo Time Proportional Integral",
"data": {
"tpi_coef_int": "coeff_int : Coefficient à utiliser pour le delta de température interne",
@@ -203,88 +336,148 @@
}
},
"presets": {
"title": "Presets",
"description": "Pour chaque preset, donnez la température cible (0 pour ignorer le preset)",
"title": "Pre-réglages - {name}",
"description": "Réglage des presets. Donnez la température cible (0 pour ignorer le preset)",
"data": {
"eco_temp": "Preset Eco",
"comfort_temp": "Preset Comfort",
"boost_temp": "Preset Boost",
"frost_temp": "Preset Hors-gel",
"eco_ac_temp": "Preset Eco en mode AC",
"comfort_ac_temp": "Preset Comfort en mode AC",
"boost_ac_temp": "Preset Boost en mode AC",
"use_presets_central_config": "Utiliser la configuration centrale des presets"
},
"data_description": {
"eco_temp": "Température en preset Eco",
"comfort_temp": "Température en preset Comfort",
"boost_temp": "Température en preset Boost",
"frost_temp": "Température en preset Hors-gel",
"eco_ac_temp": "Température en preset Eco en mode AC",
"comfort_ac_temp": "Température en preset Comfort en mode AC",
"boost_ac_temp": "Température en preset Boost en mode AC"
"boost_ac_temp": "Température en preset Boost en mode AC",
"use_presets_central_config": "Cochez pour utiliser la configuration centrale des presets. Décochez et saisissez les attributs pour utiliser une configuration des presets spécifique"
}
},
"window": {
"title": "Gestion d'une ouverture",
"description": "Coupe le radiateur si l'ouverture est ouverte.\nLaissez l'entity id vide si non utilisé.",
"title": "Ouverture - {name}",
"description": "Coupe le radiateur si l'ouverture est ouverte.\nLaissez l'id d'entité vide pour utiliser la détection automatique.",
"data": {
"window_sensor_entity_id": "Détecteur d'ouverture (entity id)",
"window_delay": "Délai avant extinction (secondes)",
"window_auto_open_threshold": "seuil haut de chute de température pour la détection automatique (en °/min)",
"window_auto_close_threshold": "Seuil bas de chute de température pour la fin de détection automatique (en °/min)",
"window_auto_max_duration": "Durée maximum d'une extinction automatique (en min)"
"window_auto_open_threshold": "Seuil haut de chute de température pour la détection automatique (en °/heure)",
"window_auto_close_threshold": "Seuil bas de chute de température pour la fin de détection automatique (en °/heure)",
"window_auto_max_duration": "Durée maximum d'une extinction automatique (en min)",
"use_window_central_config": "Utiliser la configuration centrale des ouvertures"
},
"data_description": {
"window_sensor_entity_id": "Laissez vide si vous n'avez de détecteur",
"window_sensor_entity_id": "Laissez vide si vous n'avez de détecteur et pour utiliser la détection automatique",
"window_delay": "Le délai (en secondes) avant que le changement du détecteur soit pris en compte",
"window_auto_open_threshold": "Valeur recommandée: entre 0.05 et 0.1. Laissez vide si vous n'utilisez pas la détection automatique",
"window_auto_open_threshold": "Valeur recommandée: entre 3 et 10. Laissez vide si vous n'utilisez pas la détection automatique",
"window_auto_close_threshold": "Valeur recommandée: 0. Laissez vide si vous n'utilisez pas la détection automatique",
"window_auto_max_duration": "Valeur recommandée: 60 (1 heure). Laissez vide si vous n'utilisez pas la détection automatique"
"window_auto_max_duration": "Valeur recommandée: 60 (1 heure). Laissez vide si vous n'utilisez pas la détection automatique",
"use_window_central_config": "Cochez pour utiliser la configuration centrale des ouvertures. Décochez et saisissez les attributs pour utiliser une configuration spécifique des ouvertures"
}
},
"motion": {
"title": "Gestion de la détection de mouvement",
"description": "Le preset s'ajuste automatiquement si un mouvement est détecté\nLaissez l'entity id vide si non utilisé.\n'Preset mouvement' et 'Preset sans mouvement' doivent être choisis avec les preset à utiliser.",
"title": "Mouvement - {name}",
"description": "Gestion du mouvement. Le preset s'ajuste automatiquement si un mouvement est détecté\n'Preset mouvement' et 'Preset sans mouvement' doivent être choisis avec les preset à utiliser.",
"data": {
"motion_sensor_entity_id": "Détecteur de mouvement entity id",
"motion_delay": "Délai avant changement (seconds)",
"motion_sensor_entity_id": "Détecteur de mouvement",
"motion_delay": "Délai d'activation",
"motion_off_delay": "Délai de désactivation",
"motion_preset": "Preset si mouvement",
"no_motion_preset": "Preset sans mouvement",
"use_motion_central_config": "Utiliser la condfiguration centrale du mouvement"
},
"data_description": {
"motion_sensor_entity_id": "Id d'entité du détecteur de mouvement",
"motion_delay": "Délai avant activation lorsqu'un mouvement est détecté (secondss)",
"motion_off_delai": "Délai avant désactivation lorsqu'aucun mouvement n'est détecté (secondes)",
"motion_preset": "Preset à utiliser si mouvement détecté",
"no_motion_preset": "Preset à utiliser si pas de mouvement détecté"
"no_motion_preset": "Preset à utiliser si pas de mouvement détecté",
"use_motion_central_config": "Cochez pour utiliser la configuration centrale du mouvement. Décochez et saisissez les attributs pour utiliser une configuration spécifique du mouvement"
}
},
"power": {
"title": "Gestion de l'énergie",
"description": "Sélectionne automatiquement le preset 'power' si la puissance consommée est supérieure à un maximum.\nDonnez les entity id des capteurs qui mesurent la puissance totale et la puissance max autorisée.\nEnsuite donnez la puissance de l'équipement.\nTous les capteurs et la puissance consommée par l'équipement doivent avoir la même unité de mesure (kW ou W).",
"title": "Puissance - {name}",
"description": "Gestion de la puissance. Sélectionne automatiquement le preset 'power' si la puissance consommée est supérieure à un maximum. Tous les capteurs et la puissance consommée par l'équipement doivent avoir la même unité de mesure (kW ou W).",
"data": {
"power_sensor_entity_id": "Capteur de puissance totale (entity id)",
"power_sensor_entity_id": "Puissance totale",
"max_power_sensor_entity_id": "Capteur de puissance Max (entity id)",
"power_temp": "Température si délestaqe"
"power_temp": "Température si délestaqe",
"use_power_central_config": "Utiliser la configuration centrale de la puissance"
},
"data_description": {
"power_sensor_entity_id": "Entity id du capteur de puissance totale du logement",
"max_power_sensor_entity_id": "Entity id du capteur de puissance Max autorisée avant délestage",
"power_temp": "Température cible si délestaqe",
"use_power_central_config": "Cochez pour utiliser la configuration centrale de la puissance. Décochez et saisissez les attributs pour utiliser une configuration spécifique de la puissance"
}
},
"presence": {
"title": "Gestion de la présence",
"description": "Donnez un capteur de présence (true si quelqu'un est présent).\nEnsuite spécifiez soit un preset à utiliser, soit un offset de température à appliquer lorsque personne n'est présent.\nSi le préset est utilisé, l'offset ne sera pas pris en compte.\nLaissez l'entity id vide si la gestion de la présence est non utilisée.",
"title": "Présence - {name}",
"description": "Donnez un capteur de présence (true si quelqu'un est présent) et les températures cibles à utiliser en cas d'absence.",
"data": {
"presence_sensor_entity_id": "Capteur de présence entity id (true si quelqu'un est présent)",
"presence_sensor_entity_id": "Capteur de présence",
"eco_away_temp": "preset Eco",
"comfort_away_temp": "preset Comfort",
"boost_away_temp": "preset Boost",
"frost_away_temp": "preset Hors-gel",
"eco_ac_away_temp": "preset Eco en mode AC",
"comfort_ac_away_temp": "preset Comfort en mode AC",
"boost_ac_away_temp": "preset Boost en mode AC",
"use_presence_central_config": "Utiliser la configuration centrale de la présence"
},
"data_description": {
"presence_sensor_entity_id": "Id d'entité du capteur de présence",
"eco_away_temp": "Température en preset Eco en cas d'absence",
"comfort_away_temp": "Température en preset Comfort en cas d'absence",
"boost_away_temp": "Température en preset Boost en cas d'absence",
"frost_away_temp": "Température en preset Hors-gel en cas d'absence",
"eco_ac_away_temp": "Température en preset Eco en cas d'absence en mode AC",
"comfort_ac_away_temp": "Température en preset Comfort en cas d'absence en mode AC",
"boost_ac_away_temp": "Température en preset Boost en cas d'absence en mode AC"
"boost_ac_away_temp": "Température en preset Boost en cas d'absence en mode AC",
"use_presence_central_config": "Cochez pour utiliser la configuration centrale de la présence. Décochez et saisissez les attributs pour utiliser une configuration spécifique de la présence"
}
},
"advanced": {
"title": "Parameters avancés",
"description": "Configuration des paramètres avancés. Laissez les valeurs par défaut si vous ne savez pas ce que vous faites.\nCes paramètres peuvent induire des mauvais comportements du thermostat.",
"title": "Avancés - {name}",
"description": "Paramètres avancés. Laissez les valeurs par défaut si vous ne savez pas ce que vous faites.\nCes paramètres peuvent induire des mauvais comportements du thermostat.",
"data": {
"minimal_activation_delay": "Délai minimal d'activation",
"security_delay_min": "Délai maximal entre 2 mesures de températures",
"security_min_on_percent": "Pourcentage minimal de puissance",
"security_default_on_percent": "Pourcentage de puissance a utiliser en mode securité"
"security_default_on_percent": "Pourcentage de puissance a utiliser en mode securité",
"use_advanced_central_config": "Utiliser la configuration centrale avancée"
},
"data_description": {
"minimal_activation_delay": "Délai en seondes en-dessous duquel l'équipement ne sera pas activé",
"security_delay_min": "Délai maximal autorisé en minutes entre 2 mesures de températures. Au-dessus de ce délai, le thermostat se mettra en position de sécurité",
"security_min_on_percent": "Seuil minimal de pourcentage de chauffage en-dessous duquel le préréglage sécurité ne sera jamais activé",
"security_default_on_percent": "Valeur par défaut pour le pourcentage de chauffage en mode sécurité. Mettre 0 pour éteindre le radiateur en mode sécurité"
"security_default_on_percent": "Valeur par défaut pour le pourcentage de chauffage en mode sécurité. Mettre 0 pour éteindre le radiateur en mode sécurité",
"use_advanced_central_config": "Cochez pour utiliser la configuration centrale avancée. Décochez et saisissez les attributs pour utiliser une configuration spécifique avancée"
}
},
"central_boiler": {
"title": "Contrôle de la chaudière centrale - {name}",
"description": "Donnez les services à appeler pour allumer/éteindre la chaudière centrale. Laissez vide, si aucun appel de service ne doit être effectué (dans ce cas, vous devrez gérer vous même l'allumage/extinction de votre chaudière centrale). Le service a appelé doit être formatté comme suit: `entity_id/service_name[/attribut:valeur]` (/attribut:valeur est facultatif)\nPar exemple:\n- pour allumer un switch: `switch.controle_chaudiere/switch.turn_on`\n- pour éteindre un switch: `switch.controle_chaudiere/switch.turn_off`\n- pour programmer la chaudière sur 25° et ainsi forcer son allumage: `climate.thermostat_chaudiere/climate.set_temperature/temperature:25`\n- pour envoyer 10° à la chaudière et ainsi forcer son extinction: `climate.thermostat_chaudiere/climate.set_temperature/temperature:10`",
"data": {
"central_boiler_activation_service": "Commande pour allumer",
"central_boiler_deactivation_service": "Commande pour éteindre"
},
"data_description": {
"central_boiler_activation_service": "Commande à éxecuter pour allumer la chaudière centrale au format entity_id/service_name[/attribut:valeur]",
"central_boiler_deactivation_service": "Commande à éxecuter pour étiendre la chaudière centrale au format entity_id/service_name[/attribut:valeur]"
}
}
},
"error": {
"unknown": "Erreur inattendue",
"unknown_entity": "entity id inconnu",
"window_open_detection_method": "Une seule méthode de détection des ouvertures ouvertes doit être utilisée. Utilisez le détecteur d'ouverture ou les seuils de température mais pas les deux."
"window_open_detection_method": "Une seule méthode de détection des ouvertures ouvertes doit être utilisée. Utilisez le détecteur d'ouverture ou les seuils de température mais pas les deux.",
"no_central_config": "Vous ne pouvez pas cocher 'Utiliser la configuration centrale' car aucune configuration centrale n'a été trouvée. Vous devez créer un Versatile Thermostat de type 'Central Configuration' pour pouvoir l'utiliser.",
"service_configuration_format": "Mauvais format de la configuration du service"
},
"abort": {
"already_configured": "Le device est déjà configuré"
@@ -293,8 +486,29 @@
"selector": {
"thermostat_type": {
"options": {
"thermostat_central_config": "Configuration centrale",
"thermostat_over_switch": "Thermostat sur un switch",
"thermostat_over_climate": "Thermostat sur un autre thermostat"
"thermostat_over_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"
}
},
"auto_fan_mode": {
"options": {
"auto_fan_none": "Pas d'auto fan",
"auto_fan_low": "Faible",
"auto_fan_medium": "Moyenne",
"auto_fan_high": "Forte",
"auto_fan_turbo": "Turbo"
}
}
},

View File

@@ -30,11 +30,18 @@
"heater_entity3_id": "Terzo riscaldatore",
"heater_entity4_id": "Quarto riscaldatore",
"proportional_function": "Algoritmo",
"climate_entity_id": "Termostato sottostante",
"climate_entity2_id": "Secundo termostato sottostante",
"climate_entity3_id": "Terzo termostato sottostante",
"climate_entity4_id": "Quarto termostato sottostante",
"ac_mode": "AC mode ?"
"climate_entity_id": "Primo termostato",
"climate_entity2_id": "Secondo termostato",
"climate_entity3_id": "Terzo termostato",
"climate_entity4_id": "Quarto termostato",
"ac_mode": "AC mode ?",
"valve_entity_id": "Prima valvola",
"valve_entity2_id": "Seconda valvolao",
"valve_entity3_id": "Terza valvola",
"valve_entity4_id": "Quarta valvola",
"auto_regulation_mode": "Autoregolamentazione",
"inverse_switch_command": "Comando inverso",
"auto_fan_mode": " Auto fan mode"
},
"data_description": {
"heater_entity_id": "Entity id obbligatoria del primo riscaldatore",
@@ -42,11 +49,18 @@
"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 termostato sottostante",
"climate_entity2_id": "Entity id del secundo termostato sottostante",
"climate_entity3_id": "Entity id del terzo termostato sottostante",
"climate_entity4_id": "Entity id del quarto termostato sottostante",
"ac_mode": "Utilizzare la modalità AC (Air Conditioned) ?"
"climate_entity_id": "Entity id del primo termostato",
"climate_entity2_id": "Entity id del secondo termostato",
"climate_entity3_id": "Entity id del terzo termostato",
"climate_entity4_id": "Entity id del quarto termostato",
"ac_mode": "Utilizzare la modalità AC (Air Conditioned) ?",
"valve_entity_id": "Entity id della prima valvola",
"valve_entity2_id": "Entity id della seconda valvola",
"valve_entity3_id": "Entity id della terza valvola",
"valve_entity4_id": "Entity id della quarta valvola",
"auto_regulation_mode": "Regolazione automatica della temperatura target",
"inverse_switch_command": "Inverte il controllo dell'interruttore per un'installazione con filo pilota e diodo",
"auto_fan_mode": " Automatically activate fan when huge heating/cooling is necessary"
}
},
"tpi": {
@@ -64,6 +78,7 @@
"eco_temp": "Temperatura nel preset Eco",
"comfort_temp": "Temperatura nel preset Comfort",
"boost_temp": "Temperatura nel preset Boost",
"frost_temp": "Temperatura nel preset Frost protection",
"eco_ac_temp": "Temperatura nel preset Eco (AC mode)",
"comfort_ac_temp": "Temperatura nel preset Comfort (AC mode)",
"boost_ac_temp": "Temperatura nel preset Boost (AC mode)"
@@ -75,14 +90,14 @@
"data": {
"window_sensor_entity_id": "Entity id sensore finestra",
"window_delay": "Ritardo sensore finestra (secondi)",
"window_auto_open_threshold": "Soglia di diminuzione della temperatura per il rilevamento automatico della finestra aperta (in °/min)",
"window_auto_close_threshold": "Soglia di aumento della temperatura per la fine del rilevamento automatico (in °/min)",
"window_auto_open_threshold": "Soglia di diminuzione della temperatura per il rilevamento automatico della finestra aperta (in °/ora)",
"window_auto_close_threshold": "Soglia di aumento della temperatura per la fine del rilevamento automatico (in °/ora)",
"window_auto_max_duration": "Durata massima del rilevamento automatico della finestra aperta (in min)"
},
"data_description": {
"window_sensor_entity_id": "Lasciare vuoto se non deve essere utilizzato alcun sensore finestra",
"window_delay": "Ritardo in secondi prima che il rilevamento del sensore sia preso in considerazione",
"window_auto_open_threshold": "Valore consigliato: tra 0.05 e 0.1. Lasciare vuoto se il rilevamento automatico della finestra aperta non è utilizzato",
"window_auto_open_threshold": "Valore consigliato: tra 3 e 10. Lasciare vuoto se il rilevamento automatico della finestra aperta non è utilizzato",
"window_auto_close_threshold": "Valore consigliato: 0. Lasciare vuoto se il rilevamento automatico della finestra aperta non è utilizzato",
"window_auto_max_duration": "Valore consigliato: 60 (un'ora). Lasciare vuoto se il rilevamento automatico della finestra aperta non è utilizzato"
}
@@ -93,6 +108,7 @@
"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"
}
@@ -114,6 +130,7 @@
"eco_away_temp": "Temperatura al preset Eco in caso d'assenza",
"comfort_away_temp": "Temperatura al preset Comfort in caso d'assenza",
"boost_away_temp": "Temperatura al preset Boost in caso d'assenza",
"frost_away_temp": "Temperatura al preset Frost protection in caso d'assenza",
"eco_ac_away_temp": "Temperatura al preset Eco in caso d'assenza (AC mode)",
"comfort_ac_away_temp": "Temperatura al preset Comfort in caso d'assenza (AC mode)",
"boost_ac_away_temp": "Temperatura al preset Boost in caso d'assenza (AC mode)"
@@ -168,18 +185,25 @@
},
"type": {
"title": "Entità collegate",
"description": "Attributi delle entità collegate",
"description": "Parametri entità collegate",
"data": {
"heater_entity_id": "Interruttore riscaldatore",
"heater_entity2_id": "Secondo interruttore riscaldatore",
"heater_entity3_id": "Terzo interruttore riscaldatore",
"heater_entity4_id": "Quarto interruttore riscaldatore",
"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": "Termostato sottostante",
"climate_entity2_id": "Secundo termostato sottostante",
"climate_entity3_id": "Terzo termostato sottostante",
"climate_entity4_id": "Quarto termostato sottostante",
"ac_mode": "AC mode ?"
"climate_entity_id": "Primo termostato",
"climate_entity2_id": "Secondo termostato",
"climate_entity3_id": "Terzo termostato",
"climate_entity4_id": "Quarto termostato",
"ac_mode": "AC mode ?",
"valve_entity_id": "Prima valvola",
"valve_entity2_id": "Seconda valvola",
"valve_entity3_id": "Terza valvola",
"valve_entity4_id": "Quarta valvola",
"auto_regulation_mode": "Autoregolamentazione",
"inverse_switch_command": "Comando inverso",
"auto_fan_mode": " Auto fan mode"
},
"data_description": {
"heater_entity_id": "Entity id obbligatoria del primo riscaldatore",
@@ -187,11 +211,18 @@
"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 termostato sottostante",
"climate_entity2_id": "Entity id del secundo termostato sottostante",
"climate_entity3_id": "Entity id del terzo termostato sottostante",
"climate_entity4_id": "Entity id del quarto termostato sottostante",
"ac_mode": "Utilizzare la modalità AC (Air Conditioned) ?"
"climate_entity_id": "Entity id del primo termostato",
"climate_entity2_id": "Entity id del secondo termostato",
"climate_entity3_id": "Entity id del terzo termostato",
"climate_entity4_id": "Entity id del quarto termostato",
"ac_mode": "Utilizzare la modalità AC (Air Conditioned) ?",
"valve_entity_id": "Entity id della prima valvola",
"valve_entity2_id": "Entity id della seconda valvola",
"valve_entity3_id": "Entity id della terza valvola",
"valve_entity4_id": "Entity id della quarta valvola",
"auto_regulation_mode": "Autoregolamentazione",
"inverse_switch_command": "Inverte il controllo dell'interruttore per un'installazione con filo pilota e diodo",
"auto_fan_mode": " Automatically activate fan when huge heating/cooling is necessary"
}
},
"tpi": {
@@ -209,6 +240,7 @@
"eco_temp": "Temperatura nel preset Eco",
"comfort_temp": "Temperatura nel preset Comfort",
"boost_temp": "Temperatura nel preset Boost",
"frost_temp": "Temperatura nel preset Frost protection",
"eco_ac_temp": "Temperatura nel preset Eco (AC mode)",
"comfort_ac_temp": "Temperatura nel preset Comfort (AC mode)",
"boost_ac_temp": "Temperatura nel preset Boost (AC mode)"
@@ -220,14 +252,14 @@
"data": {
"window_sensor_entity_id": "Entity id sensore finestra",
"window_delay": "Ritardo sensore finestra (secondi)",
"window_auto_open_threshold": "Soglia di diminuzione della temperatura per il rilevamento automatico della finestra aperta (in °/min)",
"window_auto_close_threshold": "Soglia di aumento della temperatura per la fine del rilevamento automatico (in °/min)",
"window_auto_open_threshold": "Soglia di diminuzione della temperatura per il rilevamento automatico della finestra aperta (in °/ora)",
"window_auto_close_threshold": "Soglia di aumento della temperatura per la fine del rilevamento automatico (in °/ora)",
"window_auto_max_duration": "Durata massima del rilevamento automatico della finestra aperta (in min)"
},
"data_description": {
"window_sensor_entity_id": "Lasciare vuoto se non deve essere utilizzato alcun sensore finestra",
"window_delay": "Ritardo in secondi prima che il rilevamento del sensore sia preso in considerazione",
"window_auto_open_threshold": "Valore consigliato: tra 0.05 e 0.1. Lasciare vuoto se il rilevamento automatico della finestra aperta non è utilizzato",
"window_auto_open_threshold": "Valore consigliato: tra 3 e 10. Lasciare vuoto se il rilevamento automatico della finestra aperta non è utilizzato",
"window_auto_close_threshold": "Valore consigliato: 0. Lasciare vuoto se il rilevamento automatico della finestra aperta non è utilizzato",
"window_auto_max_duration": "Valore consigliato: 60 (un'ora). Lasciare vuoto se il rilevamento automatico della finestra aperta non è utilizzato"
}
@@ -238,6 +270,7 @@
"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"
}
@@ -259,6 +292,7 @@
"eco_away_temp": "Temperatura al preset Eco in caso d'assenza",
"comfort_away_temp": "Temperatura al preset Comfort in caso d'assenza",
"boost_away_temp": "Temperatura al preset Boost in caso d'assenza",
"frost_away_temp": "Temperatura al preset Frost protection in caso d'assenza",
"eco_ac_away_temp": "Temperatura al preset Eco in caso d'assenza (AC mode)",
"comfort_ac_away_temp": "Temperatura al preset Comfort in caso d'assenza (AC mode)",
"boost_ac_away_temp": "Temperatura al preset Boost in caso d'assenza (AC mode)"
@@ -294,7 +328,27 @@
"thermostat_type": {
"options": {
"thermostat_over_switch": "Termostato su un interruttore",
"thermostat_over_climate": "Termostato sopra un altro termostato"
"thermostat_over_climate": "Termostato su un climatizzatore",
"thermostat_over_valve": "Termostato su una valvola"
}
},
"auto_regulation_mode": {
"options": {
"auto_regulation_slow": "Lento",
"auto_regulation_strong": "Forte",
"auto_regulation_medium": "Media",
"auto_regulation_light": "Leggera",
"auto_regulation_expert": "Esperto",
"auto_regulation_none": "Nessuna autoregolamentazione"
}
},
"auto_fan_mode": {
"options": {
"auto_fan_none": "Nessune autofan",
"auto_fan_low": "Leggera",
"auto_fan_medium": "Media",
"auto_fan_high": "Forte",
"auto_fan_turbo": "Turbo"
}
}
},

View File

@@ -0,0 +1,511 @@
{
"title": "Všestranná konfigurácia termostatu",
"config": {
"flow_title": "Všestranná konfigurácia termostatu",
"step": {
"user": {
"title": "Typ všestranného termostatu",
"data": {
"thermostat_type": "Typ termostatu"
},
"data_description": {
"thermostat_type": "Len jeden centrálny typ konfigurácie je možný"
}
},
"main": {
"title": "Pridajte nový všestranný termostat",
"description": "Hlavné povinné atribúty",
"data": {
"name": "Názov",
"thermostat_type": "Termostat typ",
"temperature_sensor_entity_id": "ID entity snímača teploty",
"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_central_mode": "Povoliť ovládanie centrálnou entitou (potrebná centrálna konfigurácia)",
"use_window_feature": "Použite detekciu okien",
"use_motion_feature": "Použite detekciu pohybu",
"use_power_feature": "Použite správu napájania",
"use_presence_feature": "Použite detekciu prítomnosti",
"use_main_central_config": "Použite centrálnu hlavnú konfiguráciu"
},
"data_description": {
"use_central_mode": "Zaškrtnutím povolíte ovládanie VTherm pomocou vybraných entít central_mode",
"use_main_central_config": "Začiarknite, ak chcete použiť centrálnu hlavnú konfiguráciu. Zrušte začiarknutie, ak chcete použiť špecifickú hlavnú konfiguráciu pre tento VTherm",
"external_temperature_sensor_entity_id": "ID entity snímača vonkajšej teploty. Nepoužíva sa, ak je zvolená centrálna konfigurácia"
}
},
"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": "Samoregulácia",
"auto_regulation_dtemp": "Regulačný prah",
"auto_regulation_periode_min": "Regulačné minimálne obdobie",
"inverse_switch_command": "Inverzný prepínací príkaz",
"auto_fan_mode": "Režim automatického ventilátora"
},
"data_description": {
"heater_entity_id": "ID entity povinného ohrievača",
"heater_entity2_id": "Voliteľné ID entity 2. ohrievača. Ak sa nepoužíva, nechajte prázdne",
"heater_entity3_id": "Voliteľné ID entity 3. ohrievača. Ak sa nepoužíva, nechajte prázdne",
"heater_entity4_id": "Voliteľné ID entity 4. ohrievača. Ak sa nepoužíva, nechajte prázdne",
"proportional_function": "Algoritmus, ktorý sa má použiť (TPI je zatiaľ jediný)",
"climate_entity_id": "ID základnej klimatickej entity",
"climate_entity2_id": "2. základné identifikačné číslo klimatickej entity",
"climate_entity3_id": "3. základné identifikačné číslo klimatickej entity",
"climate_entity4_id": "4. základné identifikačné číslo klimatickej entity",
"ac_mode": "Použite režim klimatizácie (AC)",
"valve_entity_id": "1. ventil číslo entity id",
"valve_entity2_id": "2. ventil číslo entity id",
"valve_entity3_id": "3. ventil číslo entity id",
"valve_entity4_id": "4. ventil číslo entity id",
"auto_regulation_mode": "Automatické nastavenie cieľovej teploty",
"auto_regulation_dtemp": "Hranica v °, pod ktorou sa zmena teploty neodošle",
"auto_regulation_periode_min": "Trvanie v minútach medzi dvoma aktualizáciami predpisov",
"inverse_switch_command": "V prípade spínača s pilotným vodičom a diódou možno budete musieť príkaz invertovať",
"auto_fan_mode": "Automaticky aktivujte ventilátor, keď je potrebné veľké vykurovanie/chladenie"
}
},
"tpi": {
"title": "TPI",
"description": "Časovo proporcionálne integrálne atribúty",
"data": {
"tpi_coef_int": "Koeficient na použitie pre vnútornú teplotnú deltu",
"tpi_coef_ext": "Koeficient na použitie pre deltu vonkajšej teploty",
"use_tpi_central_config": "Použite centrálnu konfiguráciu TPI"
},
"data_description": {
"tpi_coef_int": "Koeficient na použitie pre vnútornú teplotnú deltu",
"tpi_coef_ext": "Koeficient na použitie pre deltu vonkajšej teploty",
"use_tpi_central_config": "Začiarknite, ak chcete použiť centrálnu konfiguráciu TPI. Zrušte začiarknutie, ak chcete použiť špecifickú konfiguráciu TPI pre tento VTherm"
}
},
"presets": {
"title": "Predvoľby",
"description": "Pre každú predvoľbu zadajte cieľovú teplotu (0, ak chcete predvoľbu ignorovať)",
"data": {
"eco_temp": "Teplota v predvoľbe Eco",
"comfort_temp": "Prednastavená teplota v komfortnom režime",
"boost_temp": "Teplota v prednastavení Boost",
"frost_temp": "Teplota v prednastavení Frost protection",
"eco_ac_temp": "Teplota v režime Eco prednastavená pre režim AC",
"comfort_ac_temp": "Teplota v režime Comfort je prednastavená pre režim AC",
"boost_ac_temp": "Prednastavená teplota v režime Boost pre režim AC",
"use_presets_central_config": "Použite konfiguráciu centrálnych predvolieb"
},
"data_description": {
"eco_temp": "Teplota v predvoľbe Eco",
"comfort_temp": "Prednastavená teplota v komfortnom režime",
"boost_temp": "Teplota v prednastavení Boost",
"frost_temp": "Teplota v prednastavenej ochrane proti mrazu",
"eco_ac_temp": "Teplota v režime Eco prednastavená pre režim AC",
"comfort_ac_temp": "Teplota v režime Comfort je prednastavená pre režim AC",
"boost_ac_temp": "Prednastavená teplota v režime Boost pre režim AC",
"use_presets_central_config": "Začiarknite, ak chcete použiť konfiguráciu centrálnych predvolieb. Zrušte začiarknutie, ak chcete použiť špecifickú konfiguráciu predvolieb pre tento VTherm"
}
},
"window": {
"title": "Správa okien",
"description": "Otvoriť správu okien.\nAk sa príslušné entity_id nepoužíva, ponechajte prázdne\nMôžete tiež nakonfigurovať automatickú detekciu otvoreného okna na základe poklesu teploty",
"data": {
"window_sensor_entity_id": "ID entity snímača okna",
"window_delay": "Oneskorenie snímača okna (sekundy)",
"window_auto_open_threshold": "Prah poklesu teploty pre automatickú detekciu otvoreného okna (v °/hodina)",
"window_auto_close_threshold": "Prahová hodnota zvýšenia teploty pre koniec automatickej detekcie (v °/hodina)",
"window_auto_max_duration": "Maximálne trvanie automatickej detekcie otvoreného okna (v min)",
"use_window_central_config": "Použite centrálnu konfiguráciu okna"
},
"data_description": {
"window_sensor_entity_id": "Nechajte prázdne, ak nemáte použiť žiadny okenný senzor",
"window_delay": "Zohľadňuje sa oneskorenie v sekundách pred detekciou snímača",
"window_auto_open_threshold": "Odporúčaná hodnota: medzi 3 a 10. Ak sa nepoužíva automatická detekcia otvoreného okna, nechajte prázdne",
"window_auto_close_threshold": "Odporúčaná hodnota: 0. Ak sa nepoužíva automatická detekcia otvoreného okna, nechajte prázdne",
"window_auto_max_duration": "Odporúčaná hodnota: 60 (jedna hodina). Ak sa nepoužíva automatická detekcia otvoreného okna, nechajte prázdne",
"use_window_central_config": "Začiarknite, ak chcete použiť konfiguráciu centrálneho okna. Zrušte začiarknutie, ak chcete použiť špecifickú konfiguráciu okna pre tento VTherm"
}
},
"motion": {
"title": "Riadenie pohybu",
"description": "Správa snímača pohybu. Predvoľba sa môže automaticky prepínať v závislosti od detekcie pohybu\nAk sa nepoužíva, ponechajte zodpovedajúce entity_id prázdne.\nmotion_preset a no_motion_preset by mali byť nastavené na zodpovedajúci názov predvoľby",
"data": {
"motion_sensor_entity_id": "ID entity snímača pohybu",
"motion_delay": "Oneskorenie aktivácie",
"motion_off_delay": "Oneskorenie deaktivácie",
"motion_preset": "Prednastavený pohyb",
"no_motion_preset": "Žiadna predvoľba pohybu",
"use_motion_central_config": "Použite centrálnu konfiguráciu pohybu"
},
"data_description": {
"motion_sensor_entity_id": "ID entity snímača pohybu",
"motion_delay": "Oneskorenie aktivácie pohybu (sekundy)",
"motion_off_delay": "Oneskorenie deaktivácie pohybu (sekundy)",
"motion_preset": "Prednastavené na použitie pri detekcii pohybu",
"no_motion_preset": "Prednastavené na použitie, keď nie je detekovaný žiadny pohyb",
"use_motion_central_config": "Začiarknite, ak chcete použiť konfiguráciu centrálneho pohybu. Zrušte začiarknutie, ak chcete použiť špecifickú konfiguráciu pohybu pre tento VTherm"
}
},
"power": {
"title": "Správa napájania",
"description": "Atribúty správy napájania.\nPoskytuje senzor výkonu a maximálneho výkonu vášho domova.\nPotom zadajte spotrebu energie ohrievača, keď je zapnutý.\nVšetky senzory a výkon zariadenia by mali mať rovnakú jednotku (kW alebo W).\nPonechajte zodpovedajúce entity_id prázdne ak sa nepoužíva.",
"data": {
"power_sensor_entity_id": "ID entity snímača výkonu",
"max_power_sensor_entity_id": "ID entity snímača maximálneho výkonu",
"power_temp": "Teplota pre zníženie výkonu",
"use_power_central_config": "Použite centrálnu konfiguráciu napájania"
},
"data_description": {
"power_sensor_entity_id": "ID entity snímača výkonu",
"max_power_sensor_entity_id": "ID entity snímača maximálneho výkonu",
"power_temp": "Teplota pre zníženie výkonu",
"use_power_central_config": "Začiarknite, ak chcete použiť konfiguráciu centrálneho napájania. Zrušte začiarknutie, ak chcete použiť špecifickú konfiguráciu napájania pre tento VTherm"
}
},
"presence": {
"title": "Riadenie prítomnosti",
"description": "Atribúty správy prítomnosti.\nPoskytuje senzor prítomnosti vášho domova (pravda, ak je niekto prítomný).\nPotom zadajte buď predvoľbu, ktorá sa má použiť, keď je senzor prítomnosti nepravdivý, alebo posun teploty, ktorý sa má použiť.\nAk je zadaná predvoľba, posun sa nepoužije.\nAk sa nepoužije, ponechajte zodpovedajúce entity_id prázdne.",
"data": {
"presence_sensor_entity_id": "ID entity senzora prítomnosti",
"eco_away_temp": "Teplota v prednastavenej Eco, keď nie je žiadna prítomnosť",
"comfort_away_temp": "Teplota v režime Comfort je prednastavená, keď nie je prítomný",
"boost_away_temp": "Prednastavená teplota v režime Boost, keď nie je prítomný",
"frost_away_temp": "Prednastavená teplota v režime Frost protection, keď nie je prítomný",
"eco_ac_away_temp": "Teplota v prednastavenej Eco, keď nie je prítomná v režime AC",
"comfort_ac_away_temp": "Teplota v režime Comfort je prednastavená, keď nie je prítomný v režime AC",
"boost_ac_away_temp": "Teplota v prednastavenom Boost, keď nie je prítomný v režime AC",
"use_presence_central_config": "Použite centrálnu konfiguráciu prítomnosti"
},
"data_description": {
"presence_sensor_entity_id": "ID entity senzora prítomnosti",
"eco_away_temp": "Teplota v prednastavenej Eco, keď nie je žiadna prítomnosť",
"comfort_away_temp": "Teplota v režime Comfort je prednastavená, keď nie je prítomný",
"boost_away_temp": "Prednastavená teplota v režime Boost, keď nie je prítomný",
"frost_away_temp": "Teplota v Prednastavená ochrana pred mrazom, keď nie je prítomný",
"eco_ac_away_temp": "Teplota v prednastavenej Eco, keď nie je prítomná v režime AC",
"comfort_ac_away_temp": "Teplota v režime Comfort je prednastavená, keď nie je prítomný v režime AC",
"boost_ac_away_temp": "Teplota v prednastavenom Boost, keď nie je prítomný v režime AC",
"use_presence_central_config": "Začiarknite, ak chcete použiť konfiguráciu centrálnej prítomnosti. Zrušte začiarknutie, ak chcete použiť špecifickú konfiguráciu prítomnosti pre tento VTherm"
}
},
"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",
"use_advanced_central_config": "Použite centrálnu rozšírenú konfiguráciu"
},
"data_description": {
"minimal_activation_delay": "Oneskorenie v sekundách, pri ktorom sa zariadenie neaktivuje",
"security_delay_min": "Maximálne povolené oneskorenie v minútach medzi dvoma meraniami teploty. Po uplynutí tohto oneskorenia sa termostat prepne do bezpečnostného vypnutého stavu",
"security_min_on_percent": "Minimálna percentuálna hodnota ohrevu pre aktiváciu prednastavenej bezpečnosti. Pod týmto percentom výkonu termostat neprejde do prednastavenia zabezpečenia",
"security_default_on_percent": "Predvolená percentuálna hodnota vykurovacieho výkonu v bezpečnostnej predvoľbe. Nastavte na 0, ak chcete vypnúť ohrievač v zabezpečenom stave",
"use_advanced_central_config": "Začiarknite, ak chcete použiť centrálnu rozšírenú konfiguráciu. Zrušte začiarknutie, ak chcete použiť špecifickú rozšírenú konfiguráciu pre tento VTherm"
}
}
},
"error": {
"unknown": "Neočakávaná chyba",
"unknown_entity": "Neznáme ID entity",
"window_open_detection_method": "Mala by sa použiť iba jedna metóda detekcie otvoreného okna. Použite senzor alebo automatickú detekciu cez teplotný prah, ale nie oboje",
"no_central_config": "Nemôžete zaškrtnúť „použiť centrálnu konfiguráciu“, pretože sa nenašla žiadna centrálna konfigurácia. Aby ste ho mohli používať, musíte si vytvoriť všestranný termostat typu „Central Configuration“."
},
"abort": {
"already_configured": "Zariadenie je už nakonfigurované"
}
},
"options": {
"flow_title": "Všestranná konfigurácia termostatu",
"step": {
"user": {
"title": "Typ - {name}",
"data": {
"thermostat_type": "Typ termostatu"
},
"data_description": {
"thermostat_type": "Je možný len jeden centrálny typ konfigurácie"
}
},
"main": {
"title": "Hlavný - {name}",
"description": "Hlavné povinné atribúty",
"data": {
"name": "Názov",
"thermostat_type": "Termostat typ",
"temperature_sensor_entity_id": "ID entity snímača teploty",
"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_central_mode": "Povoliť ovládanie centrálnou entitou (potrebná centrálna konfigurácia)",
"use_window_feature": "Použite detekciu okien",
"use_motion_feature": "Použite detekciu pohybu",
"use_power_feature": "Použite správu napájania",
"use_presence_feature": "Použite detekciu prítomnosti",
"use_main_central_config": "Použite centrálnu hlavnú konfiguráciu"
},
"data_description": {
"use_central_mode": "Zaškrtnutím povolíte ovládanie VTherm pomocou vybraných entít central_mode",
"use_main_central_config": "Začiarknite, ak chcete použiť centrálnu hlavnú konfiguráciu. Ak chcete použiť špecifickú konfiguráciu pre tento VTherm, zrušte začiarknutie",
"external_temperature_sensor_entity_id": "ID entity snímača vonkajšej teploty. Nepoužíva sa, ak je zvolená centrálna konfigurácia"
}
},
"type": {
"title": "Prepojené entity - {name}",
"description": "Atribúty prepojených entít",
"data": {
"heater_entity_id": "Spínač ohrievača",
"heater_entity2_id": "2. spínač ohrievača",
"heater_entity3_id": "3. spínač ohrievača",
"heater_entity4_id": "4. spínač ohrievača",
"proportional_function": "Algoritmus",
"climate_entity_id": "Základná klíma",
"climate_entity2_id": "2. základná klíma",
"climate_entity3_id": "3. základná klíma",
"climate_entity4_id": "4. základná klíma",
"ac_mode": "AC režim",
"valve_entity_id": "1. ventil číslo",
"valve_entity2_id": "2. ventil číslo",
"valve_entity3_id": "3. ventil číslo",
"valve_entity4_id": "4. ventil číslo",
"auto_regulation_mode": "Samoregulácia",
"auto_regulation_dtemp": "Regulačný prah",
"auto_regulation_periode_min": "Regulačné minimálne obdobie",
"inverse_switch_command": "Inverzný prepínací príkaz",
"auto_fan_mode": "Režim automatického ventilátora"
},
"data_description": {
"heater_entity_id": "ID entity povinného ohrievača",
"heater_entity2_id": "Voliteľné ID entity 2. ohrievača. Ak sa nepoužíva, nechajte prázdne",
"heater_entity3_id": "Voliteľné ID entity 3. ohrievača. Ak sa nepoužíva, nechajte prázdne",
"heater_entity4_id": "Voliteľné ID entity 4. ohrievača. Ak sa nepoužíva, nechajte prázdne",
"proportional_function": "Algoritmus, ktorý sa má použiť (TPI je zatiaľ jediný)",
"climate_entity_id": "ID základnej klimatickej entity",
"climate_entity2_id": "2. základný identifikátor klimatickej entity",
"climate_entity3_id": "3. základný identifikátor klimatickej entity",
"climate_entity4_id": "4. základný identifikátor klimatickej entity",
"ac_mode": "Použite režim klimatizácie (AC)",
"valve_entity_id": "1. ventil číslo entity id",
"valve_entity2_id": "2. ventil číslo entity id",
"valve_entity3_id": "3. ventil číslo entity id",
"valve_entity4_id": "4. ventil číslo entity id",
"auto_regulation_mode": "Automatické nastavenie cieľovej teploty",
"auto_regulation_dtemp": "Hranica v °, pod ktorou sa zmena teploty neodošle",
"auto_regulation_periode_min": "Trvanie v minútach medzi dvoma aktualizáciami predpisov",
"inverse_switch_command": "V prípade spínača s pilotným vodičom a diódou možno budete musieť príkaz invertovať",
"auto_fan_mode": "Automaticky aktivujte ventilátor, keď je potrebné veľké vykurovanie/chladenie"
}
},
"tpi": {
"title": "TPI - {name}",
"description": "Časovo proporcionálne integrálne atribúty",
"data": {
"tpi_coef_int": "Koeficient na použitie pre vnútornú teplotnú deltu",
"tpi_coef_ext": "Koeficient na použitie pre vonkajšiu teplotnú deltu",
"use_tpi_central_config": "Použite centrálnu konfiguráciu TPI"
},
"data_description": {
"tpi_coef_int": "Koeficient na použitie pre vnútornú teplotnú deltu",
"tpi_coef_ext": "Koeficient na použitie pre deltu vonkajšej teploty",
"use_tpi_central_config": "Začiarknite, ak chcete použiť centrálnu konfiguráciu TPI. Zrušte začiarknutie, ak chcete použiť špecifickú konfiguráciu TPI pre tento VTherm"
}
},
"presets": {
"title": "Predvoľby - {name}",
"description": "Pre každú predvoľbu zadajte cieľovú teplotu (0, ak chcete predvoľbu ignorovať)",
"data": {
"eco_temp": "Teplota v predvoľbe Eco",
"comfort_temp": "Prednastavená teplota v komfortnom režime",
"boost_temp": "Teplota v prednastavení Boost",
"frost_temp": "Teplota v prednastavení Frost protection",
"eco_ac_temp": "Teplota v režime Eco prednastavená pre režim AC",
"comfort_ac_temp": "Teplota v režime Comfort je prednastavená pre režim AC",
"boost_ac_temp": "Prednastavená teplota v režime Boost pre režim AC",
"use_presets_central_config": "Použite konfiguráciu centrálnych predvolieb"
},
"data_description": {
"eco_temp": "Teplota v predvoľbe Eco",
"comfort_temp": "Prednastavená teplota v komfortnom režime",
"boost_temp": "Teplota v prednastavení Boost",
"frost_temp": "Teplota v prednastavenej ochrane proti mrazu",
"eco_ac_temp": "Teplota v režime Eco prednastavená pre režim AC",
"comfort_ac_temp": "Teplota v režime Comfort je prednastavená pre režim AC",
"boost_ac_temp": "Prednastavená teplota v režime Boost pre režim AC",
"use_presets_central_config": "Začiarknite, ak chcete použiť konfiguráciu centrálnych predvolieb. Zrušte začiarknutie, ak chcete použiť špecifickú konfiguráciu predvolieb pre tento VTherm"
}
},
"window": {
"title": "Správa okien - {name}",
"description": "Otvoriť správu okien.\nAk sa príslušné entity_id nepoužíva, ponechajte prázdne\nMôžete tiež nakonfigurovať automatickú detekciu otvoreného okna na základe poklesu teploty",
"data": {
"window_sensor_entity_id": "ID entity snímača okna",
"window_delay": "Oneskorenie snímača okna (sekundy)",
"window_auto_open_threshold": "Prah poklesu teploty pre automatickú detekciu otvoreného okna (v °/hodina)",
"window_auto_close_threshold": "Prahová hodnota zvýšenia teploty pre koniec automatickej detekcie (v °/hodina)",
"window_auto_max_duration": "Maximálne trvanie automatickej detekcie otvoreného okna (v min)",
"use_window_central_config": "Použite centrálnu konfiguráciu okna"
},
"data_description": {
"window_sensor_entity_id": "Nechajte prázdne, ak nemáte použiť žiadny okenný senzor",
"window_delay": "Zohľadňuje sa oneskorenie v sekundách pred detekciou snímača",
"window_auto_open_threshold": "Odporúčaná hodnota: medzi 3 a 10. Ak sa nepoužíva automatická detekcia otvoreného okna, nechajte prázdne",
"window_auto_close_threshold": "Odporúčaná hodnota: 0. Ak sa nepoužíva automatická detekcia otvoreného okna, nechajte prázdne",
"window_auto_max_duration": "Odporúčaná hodnota: 60 (jedna hodina). Ak sa nepoužíva automatická detekcia otvoreného okna, nechajte prázdne",
"use_window_central_config": "Začiarknite, ak chcete použiť konfiguráciu centrálneho okna. Zrušte začiarknutie, ak chcete použiť špecifickú konfiguráciu okna pre tento VTherm"
}
},
"motion": {
"title": "Riadenie pohybu - {name}",
"description": "Správa snímača pohybu. Predvoľba sa môže automaticky prepínať v závislosti od detekcie pohybu\nAk sa nepoužíva, ponechajte zodpovedajúce entity_id prázdne.\nmotion_preset a no_motion_preset by mali byť nastavené na zodpovedajúci názov predvoľby",
"data": {
"motion_sensor_entity_id": "ID entity snímača pohybu",
"motion_delay": "Oneskorenie aktivácie",
"motion_off_delay": "Oneskorenie deaktivácie",
"motion_preset": "Prednastavený pohyb",
"no_motion_preset": "Žiadna predvoľba pohybu",
"use_motion_central_config": "Použite centrálnu konfiguráciu pohybu"
},
"data_description": {
"motion_sensor_entity_id": "ID entity snímača pohybu",
"motion_delay": "Oneskorenie aktivácie pohybu (sekundy)",
"motion_off_delay": "Oneskorenie deaktivácie pohybu (sekundy)",
"motion_preset": "Prednastavené na použitie pri detekcii pohybu",
"no_motion_preset": "Prednastavené na použitie, keď nie je detekovaný žiadny pohyb",
"use_motion_central_config": "Začiarknite, ak chcete použiť konfiguráciu centrálneho pohybu. Zrušte začiarknutie, ak chcete použiť špecifickú konfiguráciu pohybu pre tento VTherm"
}
},
"power": {
"title": "Správa napájania - {name}",
"description": "Atribúty správy napájania.\nPoskytuje senzor výkonu a maximálneho výkonu vášho domova.\nPotom zadajte spotrebu energie ohrievača, keď je zapnutý.\nVšetky senzory a výkon zariadenia by mali mať rovnakú jednotku (kW alebo W).\nPonechajte zodpovedajúce entity_id prázdne ak sa nepoužíva.",
"data": {
"power_sensor_entity_id": "ID entity snímača výkonu",
"max_power_sensor_entity_id": "ID entity snímača maximálneho výkonu",
"power_temp": "Teplota pre zníženie výkonu",
"use_power_central_config": "Použite centrálnu konfiguráciu napájania"
},
"data_description": {
"power_sensor_entity_id": "ID entity snímača výkonu",
"max_power_sensor_entity_id": "ID entity snímača maximálneho výkonu",
"power_temp": "Teplota pre zníženie výkonu",
"use_power_central_config": "Začiarknite, ak chcete použiť konfiguráciu centrálneho napájania. Zrušte začiarknutie, ak chcete použiť špecifickú konfiguráciu napájania pre tento VTherm"
}
},
"presence": {
"title": "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ý",
"frost_away_temp": "Prednastavená teplota v režime Frost protection, keď nie je prítomný",
"eco_ac_away_temp": "Teplota v prednastavenej Eco, keď nie je prítomná v režime AC",
"comfort_ac_away_temp": "Teplota v režime Comfort je prednastavená, keď nie je prítomný v režime AC",
"boost_ac_away_temp": "Teplota v prednastavenom Boost, keď nie je prítomný v režime AC",
"use_presence_central_config": "Použite centrálnu konfiguráciu prítomnosti"
},
"data_description": {
"presence_sensor_entity_id": "ID entity senzora prítomnosti",
"eco_away_temp": "Teplota v prednastavenej Eco, keď nie je žiadna prítomnosť",
"comfort_away_temp": "Teplota v režime Comfort je prednastavená, keď nie je prítomný",
"boost_away_temp": "Prednastavená teplota v režime Boost, keď nie je prítomný",
"frost_away_temp": "Teplota v Prednastavená ochrana pred mrazom, keď nie je prítomný",
"eco_ac_away_temp": "Teplota v prednastavenej Eco, keď nie je prítomná v režime AC",
"comfort_ac_away_temp": "Teplota v režime Comfort je prednastavená, keď nie je prítomný v režime AC",
"boost_ac_away_temp": "Teplota v prednastavenom Boost, keď nie je prítomný v režime AC",
"use_presence_central_config": "Začiarknite, ak chcete použiť konfiguráciu centrálnej prítomnosti. Zrušte začiarknutie, ak chcete použiť špecifickú konfiguráciu prítomnosti pre tento VTherm"
}
},
"advanced": {
"title": "Pokročilé parametre - {name}",
"description": "Konfigurácia pokročilých parametrov. Ak neviete, čo robíte, ponechajte predvolené hodnoty.\nTento parameter môže viesť k veľmi zlej regulácii teploty alebo výkonu.",
"data": {
"minimal_activation_delay": "Minimálne oneskorenie aktivácie",
"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",
"use_advanced_central_config": "Použite centrálnu rozšírenú konfiguráciu"
},
"data_description": {
"minimal_activation_delay": "Oneskorenie v sekundách, pri ktorom sa zariadenie neaktivuje",
"security_delay_min": "Maximálne povolené oneskorenie v minútach medzi dvoma meraniami teploty. Po uplynutí tohto oneskorenia sa termostat prepne do bezpečnostného vypnutého stavu",
"security_min_on_percent": "Minimálna percentuálna hodnota ohrevu pre aktiváciu prednastavenej bezpečnosti. Pod týmto percentom výkonu termostat neprejde do prednastavenia zabezpečenia",
"security_default_on_percent": "Predvolená percentuálna hodnota vykurovacieho výkonu v bezpečnostnej predvoľbe. Nastavte na 0, ak chcete vypnúť ohrievač v zabezpečenom stave",
"use_advanced_central_config": "Začiarknite, ak chcete použiť centrálnu rozšírenú konfiguráciu. Zrušte začiarknutie, ak chcete použiť špecifickú rozšírenú konfiguráciu pre tento VTherm"
}
}
},
"error": {
"unknown": "Neočakávaná chyba",
"unknown_entity": "Neznáme ID entity",
"window_open_detection_method": "Mala by sa použiť iba jedna metóda detekcie otvoreného okna. Použite senzor alebo automatickú detekciu cez teplotný prah, ale nie oboje",
"no_central_config": "Nemôžete zaškrtnúť „použiť centrálnu konfiguráciu“, pretože sa nenašla žiadna centrálna konfigurácia. Aby ste ho mohli používať, musíte si vytvoriť všestranný termostat typu „Central Configuration“."
},
"abort": {
"already_configured": "Zariadenie je už nakonfigurované"
}
},
"selector": {
"thermostat_type": {
"options": {
"thermostat_central_config": "Centrálna konfigurácia",
"thermostat_over_switch": "Termostat nad spínačom",
"thermostat_over_climate": "Termostat nad iným termostatom",
"thermostat_over_valve": "Termostat nad ventilom"
}
},
"auto_regulation_mode": {
"options": {
"auto_regulation_slow": "Pomalé",
"auto_regulation_strong": "Silné",
"auto_regulation_medium": "Stredné",
"auto_regulation_light": "Jemné",
"auto_regulation_expert": "Expertné",
"auto_regulation_none": "Nie auto-regulácia"
}
},
"auto_fan_mode": {
"options": {
"auto_fan_none": "Žiadny automatický ventilátor",
"auto_fan_low": "Nízky",
"auto_fan_medium": "Stredný",
"auto_fan_high": "Vysoký",
"auto_fan_turbo": "Turbo"
}
}
},
"entity": {
"climate": {
"versatile_thermostat": {
"state_attributes": {
"preset_mode": {
"state": {
"power": "Vyradenie",
"security": "Zabezpečenie",
"none": "Manuálne"
}
}
}
}
}
}
}

View File

@@ -1,13 +1,16 @@
# 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.core import State
from homeassistant.exceptions import ServiceNotFound
from enum import StrEnum
from homeassistant.core import HomeAssistant, DOMAIN as HA_DOMAIN, CALLBACK_TYPE
from homeassistant.core import HomeAssistant, CALLBACK_TYPE
from homeassistant.components.climate import (
ClimateEntity,
ClimateEntityFeature,
@@ -22,10 +25,13 @@ from homeassistant.components.climate import (
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
from .const import UnknownEntity, overrides
_LOGGER = logging.getLogger(__name__)
@@ -42,6 +48,9 @@ class UnderlyingEntityType(StrEnum):
# a climate
CLIMATE = "climate"
# a valve
VALVE = "valve"
class UnderlyingEntity:
"""Represent a underlying device which could be a switch or a climate"""
@@ -96,37 +105,27 @@ class UnderlyingEntity:
"""If the toggleable device is currently active."""
return None
async def turn_off(self):
"""Turn heater toggleable device off."""
_LOGGER.debug("%s - Stopping underlying entity %s", self, self._entity_id)
# This may fails if called after shutdown
try:
data = {ATTR_ENTITY_ID: self._entity_id}
await self._hass.services.async_call(
HA_DOMAIN,
SERVICE_TURN_OFF,
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)
try:
data = {ATTR_ENTITY_ID: self._entity_id}
await self._hass.services.async_call(
HA_DOMAIN,
SERVICE_TURN_ON,
data,
)
except ServiceNotFound as err:
_LOGGER.error(err)
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
@@ -140,10 +139,11 @@ class UnderlyingEntity:
self._entity_id,
)
await self.set_hvac_mode(hvac_mode)
elif hvac_mode != HVACMode.OFF and self.is_device_active:
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",
"%s - The hvac mode is %s, but the underlying device is not ON. Turning on device %s if needed",
self,
hvac_mode,
self._entity_id,
)
await self.set_hvac_mode(hvac_mode)
@@ -155,6 +155,23 @@ class UnderlyingEntity:
"""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"""
def cap_sent_value(self, value) -> float:
"""capping of the value send to the underlying eqt"""
return value
class UnderlyingSwitch(UnderlyingEntity):
"""Represent a underlying switch"""
@@ -191,6 +208,13 @@ class UnderlyingSwitch(UnderlyingEntity):
"""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"""
@@ -208,13 +232,50 @@ class UnderlyingSwitch(UnderlyingEntity):
@property
def is_device_active(self):
"""If the toggleable device is currently active."""
return self._hass.states.is_state(self._entity_id, STATE_ON)
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"""
@@ -246,7 +307,7 @@ class UnderlyingSwitch(UnderlyingEntity):
return
# If we should heat, starts the cycle with delay
if self._hvac_mode == HVACMode.HEAT and on_time_sec > 0:
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
@@ -265,6 +326,7 @@ class UnderlyingSwitch(UnderlyingEntity):
else:
_LOGGER.debug("%s - nothing to do", self)
@overrides
def _cancel_cycle(self):
"""Cancel the cycle"""
if self._async_cancel_cycle:
@@ -293,20 +355,11 @@ class UnderlyingSwitch(UnderlyingEntity):
if await self._thermostat.check_overpowering():
_LOGGER.debug("%s - End of cycle (3)", self)
return
# Security mode could have change the on_time percent
# safety mode could have change the on_time percent
await self._thermostat.check_security()
time = self._on_time_sec
action_label = "start"
# if self._should_relaunch_control_heating:
# _LOGGER.debug("Don't %s cause a cycle have to be relaunch", action_label)
# self._should_relaunch_control_heating = False
# # self.hass.create_task(self._async_control_heating())
# await self.start_cycle(
# self._hvac_mode, self._on_time_sec, self._off_time_sec
# )
# _LOGGER.debug("%s - End of cycle (3)", self)
# return
if time > 0:
_LOGGER.info(
@@ -343,16 +396,6 @@ class UnderlyingSwitch(UnderlyingEntity):
return
action_label = "stop"
# if self._should_relaunch_control_heating:
# _LOGGER.debug("Don't %s cause a cycle have to be relaunch", action_label)
# self._should_relaunch_control_heating = False
# # self.hass.create_task(self._async_control_heating())
# await self.start_cycle(
# self._hvac_mode, self._on_time_sec, self._off_time_sec
# )
# _LOGGER.debug("%s - End of cycle (3)", self)
# return
time = self._off_time_sec
if time > 0:
@@ -375,6 +418,7 @@ class UnderlyingSwitch(UnderlyingEntity):
# 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()
@@ -440,6 +484,14 @@ class UnderlyingClimate(UnderlyingEntity):
if not self.is_initialized:
return False
if self._underlying_climate.hvac_mode == hvac_mode:
_LOGGER.debug(
"%s - hvac_mode is already is requested state %s. Do not send any command",
self,
self._underlying_climate.hvac_mode,
)
return False
data = {ATTR_ENTITY_ID: self._entity_id, "hvac_mode": hvac_mode}
await self._hass.services.async_call(
CLIMATE_DOMAIN,
@@ -453,10 +505,14 @@ class UnderlyingClimate(UnderlyingEntity):
def is_device_active(self):
"""If the toggleable device is currently active."""
if self.is_initialized:
return self._underlying_climate.hvac_mode != HVACMode.OFF and self._underlying_climate.hvac_action not in [
HVACAction.IDLE,
HVACAction.OFF,
]
return (
self._underlying_climate.hvac_mode != HVACMode.OFF
and self._underlying_climate.hvac_action
not in [
HVACAction.IDLE,
HVACAction.OFF,
]
)
else:
return None
@@ -513,7 +569,7 @@ class UnderlyingClimate(UnderlyingEntity):
return
data = {
ATTR_ENTITY_ID: self._entity_id,
"temperature": temperature,
"temperature": self.cap_sent_value(temperature),
"target_temp_high": max_temp,
"target_temp_low": min_temp,
}
@@ -626,3 +682,173 @@ class UnderlyingClimate(UnderlyingEntity):
if not self.is_initialized:
return None
return self._underlying_climate.turn_aux_heat_off()
@overrides
def cap_sent_value(self, value) -> float:
"""Try to adapt the target temp value to the min_temp / max_temp found
in the underlying entity (if any)"""
if not self.is_initialized:
return value
# Gets the min_temp and max_temp
if (
self._underlying_climate.min_temp is not None
and self._underlying_climate is not None
):
min_val = self._underlying_climate.min_temp
max_val = self._underlying_climate.max_temp
new_value = max(min_val, min(value, max_val))
else:
_LOGGER.debug("%s - no min and max attributes on underlying", self)
new_value = value
if new_value != value:
_LOGGER.info(
"%s - Target temp have been updated due min, max of the underlying entity. new_value=%.0f value=%.0f min=%.0f max=%.0f",
self,
new_value,
value,
min_val,
max_val,
)
return new_value
class UnderlyingValve(UnderlyingEntity):
"""Represent a underlying switch"""
_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
self._valve_entity_id = valve_entity_id
async def send_percent_open(self):
"""Send the percent open to the underlying valve"""
# This may fails if called after shutdown
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:
self._percent_open = self.cap_sent_value(self._percent_open)
await self.send_percent_open()
@overrides
def cap_sent_value(self, value) -> float:
"""Try to adapt the open_percent value to the min / max found
in the underlying entity (if any)"""
# Gets the last number state
valve_state: State = self._hass.states.get(self._valve_entity_id)
if valve_state is None:
return value
if "min" in valve_state.attributes and "max" in valve_state.attributes:
min_val = valve_state.attributes["min"]
max_val = valve_state.attributes["max"]
new_value = round(max(min_val, min(value, max_val)))
else:
_LOGGER.debug("%s - no min and max attributes on underlying", self)
new_value = value
if new_value != value:
_LOGGER.info(
"%s - Valve open percent have been updated due min, max of the underlying entity. new_value=%.0f value=%.0f min=%.0f max=%.0f",
self,
new_value,
value,
min_val,
max_val,
)
return new_value
def set_valve_open_percent(self):
"""Update the valve open percent"""
caped_val = self.cap_sent_value(self._thermostat.valve_open_percent)
if self._percent_open == caped_val:
# No changes
return
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,114 @@
""" 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,
CONF_THERMOSTAT_TYPE,
CONF_THERMOSTAT_CENTRAL_CONFIG,
)
VTHERM_API_NAME = "vtherm_api"
_LOGGER = logging.getLogger(__name__)
class VersatileThermostatAPI(dict):
"""The VersatileThermostatAPI"""
_hass: HomeAssistant = None
@classmethod
def get_vtherm_api(cls, hass=None):
"""Get the eventual VTherm API class instance or
instantiate it if it doesn't exists"""
if hass is not None:
VersatileThermostatAPI._hass = hass
if VersatileThermostatAPI._hass is None:
return None
domain = VersatileThermostatAPI._hass.data.get(DOMAIN)
if not domain:
VersatileThermostatAPI._hass.data.setdefault(DOMAIN, {})
ret = VersatileThermostatAPI._hass.data.get(DOMAIN).get(VTHERM_API_NAME)
if ret is None:
ret = VersatileThermostatAPI()
VersatileThermostatAPI._hass.data[DOMAIN][VTHERM_API_NAME] = ret
return ret
def __init__(self) -> None:
_LOGGER.debug("building a VersatileThermostatAPI")
super().__init__()
self._expert_params = None
self._short_ema_params = None
self._central_boiler_entity = None
def find_central_configuration(self):
"""Search for a central configuration"""
for config_entry in VersatileThermostatAPI._hass.config_entries.async_entries(
DOMAIN
):
if (
config_entry.data.get(CONF_THERMOSTAT_TYPE)
== CONF_THERMOSTAT_CENTRAL_CONFIG
):
central_config = config_entry
return central_config
return None
def add_entry(self, entry: ConfigEntry):
"""Add a new entry"""
_LOGGER.debug("Add the entry %s", entry.entry_id)
# Add the entry in hass.data
VersatileThermostatAPI._hass.data[DOMAIN][entry.entry_id] = entry
def remove_entry(self, entry: ConfigEntry):
"""Remove an entry"""
_LOGGER.debug("Remove the entry %s", entry.entry_id)
VersatileThermostatAPI._hass.data[DOMAIN].pop(entry.entry_id)
# If not more entries are preset, remove the API
if len(self) == 0:
_LOGGER.debug("No more entries-> Remove the API from DOMAIN")
VersatileThermostatAPI._hass.data.pop(DOMAIN)
def set_global_config(self, config):
"""Read the global configuration from configuration.yaml file"""
_LOGGER.info("Read global config from configuration.yaml")
self._expert_params = config.get(CONF_AUTO_REGULATION_EXPERT)
if self._expert_params:
_LOGGER.debug("We have found expert params %s", self._expert_params)
self._short_ema_params = config.get(CONF_SHORT_EMA_PARAMS)
if self._short_ema_params:
_LOGGER.debug("We have found short ema params %s", self._short_ema_params)
def register_central_boiler(self, central_boiler_entity):
"""Register the central boiler entity. This is used by the CentralBoilerBinarySensor
class to register itself at creation"""
self._central_boiler_entity = central_boiler_entity
async def reload_central_boiler_entities_list(self):
"""Reload the central boiler list of entities if a central boiler is used"""
if self._central_boiler_entity is not None:
await self._central_boiler_entity.listen_vtherms_entities()
@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 VersatileThermostatAPI._hass

View File

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

BIN
images/central_mode.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.2 KiB

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 45 KiB

BIN
images/config-main0.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

BIN
images/plotly-curves.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

7
pyrightconfig.json Normal file
View File

@@ -0,0 +1,7 @@
{
"include": [
"custom_components/versatile_thermostat/**",
"homeassistant/**"
],
"reportShadowedImports": false
}

2
requirements_dev.txt Normal file
View File

@@ -0,0 +1,2 @@
homeassistant==2023.12.1
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

View File

@@ -1,3 +1,5 @@
# pylint: disable=wildcard-import, unused-wildcard-import, protected-access, unused-argument, line-too-long
""" Some common resources """
import asyncio
import logging
@@ -5,10 +7,9 @@ from unittest.mock import patch, MagicMock
import pytest # pylint: disable=unused-import
from homeassistant.core import HomeAssistant, Event, EVENT_STATE_CHANGED, State
from homeassistant.const import UnitOfTemperature, STATE_ON, STATE_OFF
from homeassistant.const import UnitOfTemperature, STATE_ON, STATE_OFF, ATTR_TEMPERATURE
from homeassistant.config_entries import ConfigEntryState
from homeassistant.util import dt as dt_util
from homeassistant.helpers.entity import Entity
from homeassistant.components.climate import (
ClimateEntity,
@@ -20,23 +21,38 @@ from homeassistant.components.climate import (
from pytest_homeassistant_custom_component.common import MockConfigEntry
from ..climate import VersatileThermostat
from ..const import * # pylint: disable=wildcard-import, unused-wildcard-import
from ..underlyings import * # pylint: disable=wildcard-import, unused-wildcard-import
from 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 ( # pylint: disable=unused-import
get_tz,
NowClass,
)
from custom_components.versatile_thermostat.vtherm_api import VersatileThermostatAPI
from .const import ( # pylint: disable=unused-import
MOCK_TH_OVER_SWITCH_USER_CONFIG,
MOCK_TH_OVER_4SWITCH_USER_CONFIG,
MOCK_TH_OVER_CLIMATE_USER_CONFIG,
MOCK_TH_OVER_SWITCH_MAIN_CONFIG,
MOCK_TH_OVER_SWITCH_CENTRAL_MAIN_CONFIG,
MOCK_TH_OVER_SWITCH_TYPE_CONFIG,
MOCK_TH_OVER_SWITCH_AC_TYPE_CONFIG,
MOCK_TH_OVER_4SWITCH_TYPE_CONFIG,
MOCK_TH_OVER_CLIMATE_MAIN_CONFIG,
MOCK_TH_OVER_CLIMATE_CENTRAL_MAIN_CONFIG,
MOCK_TH_OVER_CLIMATE_TYPE_CONFIG,
MOCK_TH_OVER_CLIMATE_TYPE_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,
@@ -46,8 +62,11 @@ from .const import ( # pylint: disable=unused-import
PRESET_ACTIVITY,
)
FULL_SWITCH_CONFIG = (
MOCK_TH_OVER_SWITCH_USER_CONFIG
| MOCK_TH_OVER_SWITCH_MAIN_CONFIG
| MOCK_TH_OVER_SWITCH_CENTRAL_MAIN_CONFIG
| MOCK_TH_OVER_SWITCH_TYPE_CONFIG
| MOCK_TH_OVER_SWITCH_TPI_CONFIG
| MOCK_PRESETS_CONFIG
@@ -58,13 +77,48 @@ FULL_SWITCH_CONFIG = (
| MOCK_ADVANCED_CONFIG
)
FULL_SWITCH_AC_CONFIG = (
MOCK_TH_OVER_SWITCH_USER_CONFIG
| MOCK_TH_OVER_SWITCH_MAIN_CONFIG
| MOCK_TH_OVER_SWITCH_CENTRAL_MAIN_CONFIG
| MOCK_TH_OVER_SWITCH_AC_TYPE_CONFIG
| MOCK_TH_OVER_SWITCH_TPI_CONFIG
| MOCK_PRESETS_AC_CONFIG
| MOCK_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_MAIN_CONFIG
| MOCK_TH_OVER_CLIMATE_CENTRAL_MAIN_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_MAIN_CONFIG
| MOCK_TH_OVER_CLIMATE_CENTRAL_MAIN_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_TH_OVER_CLIMATE_MAIN_CONFIG
| MOCK_TH_OVER_CLIMATE_CENTRAL_MAIN_CONFIG
| MOCK_PRESETS_CONFIG
| MOCK_ADVANCED_CONFIG
)
FULL_4SWITCH_CONFIG = (
MOCK_TH_OVER_4SWITCH_USER_CONFIG
| MOCK_TH_OVER_4SWITCH_TYPE_CONFIG
@@ -77,44 +131,130 @@ FULL_4SWITCH_CONFIG = (
| MOCK_ADVANCED_CONFIG
)
FULL_CENTRAL_CONFIG = {
CONF_NAME: CENTRAL_CONFIG_NAME,
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_CENTRAL_CONFIG,
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
CONF_TEMP_MIN: 15,
CONF_TEMP_MAX: 30,
CONF_TPI_COEF_INT: 0.5,
CONF_TPI_COEF_EXT: 0.02,
"frost_temp": 10,
"eco_temp": 17.1,
"comfort_temp": 0,
"boost_temp": 19.1,
"eco_ac_temp": 25.1,
"comfort_ac_temp": 23.1,
"boost_ac_temp": 21.1,
"frost_away_temp": 15.1,
"eco_away_temp": 15.2,
"comfort_away_temp": 0,
"boost_away_temp": 15.4,
"eco_ac_away_temp": 30.5,
"comfort_ac_away_temp": 0,
"boost_ac_away_temp": 30.7,
CONF_WINDOW_DELAY: 15,
CONF_WINDOW_AUTO_OPEN_THRESHOLD: 4,
CONF_WINDOW_AUTO_CLOSE_THRESHOLD: 1,
CONF_WINDOW_AUTO_MAX_DURATION: 31,
CONF_MOTION_DELAY: 31,
CONF_MOTION_OFF_DELAY: 301,
CONF_MOTION_PRESET: "boost",
CONF_NO_MOTION_PRESET: "frost",
CONF_POWER_SENSOR: "sensor.mock_power_sensor",
CONF_MAX_POWER_SENSOR: "sensor.mock_max_power_sensor",
CONF_PRESET_POWER: 14,
CONF_MINIMAL_ACTIVATION_DELAY: 11,
CONF_SECURITY_DELAY_MIN: 61,
CONF_SECURITY_MIN_ON_PERCENT: 0.5,
CONF_SECURITY_DEFAULT_ON_PERCENT: 0.2,
}
_LOGGER = logging.getLogger(__name__)
class MockClimate(ClimateEntity):
"""A Mock Climate class used for Underlying climate mode"""
def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos, hvac_mode:HVACMode = HVACMode.OFF) -> None:
def __init__( # pylint: disable=unused-argument, dangerous-default-value
self,
hass: HomeAssistant,
unique_id,
name,
entry_infos={},
hvac_mode: HVACMode = HVACMode.OFF,
hvac_action: HVACAction = HVACAction.OFF,
fan_modes: list[str] = None,
hvac_modes: list[str] = None,
) -> None:
"""Initialize the thermostat."""
super().__init__()
self.hass = hass
self.platform = 'climate'
self.entity_id= self.platform+'.'+unique_id
self.platform = "climate"
self.entity_id = self.platform + "." + unique_id
self._attr_extra_state_attributes = {}
self._unique_id = unique_id
self._name = name
self._attr_hvac_action = HVACAction.OFF if hvac_mode == HVACMode.OFF else HVACAction.HEATING
self._attr_hvac_action = (
HVACAction.OFF if hvac_mode == HVACMode.OFF else HVACAction.HEATING
)
self._attr_hvac_mode = hvac_mode
self._attr_hvac_modes = [HVACMode.OFF, HVACMode.COOL, HVACMode.HEAT]
self._attr_hvac_modes = (
hvac_modes
if hvac_modes is not None
else [HVACMode.OFF, HVACMode.COOL, HVACMode.HEAT]
)
self._attr_temperature_unit = UnitOfTemperature.CELSIUS
self._attr_target_temperature = 20
self._attr_current_temperature = 15
self._attr_hvac_action = hvac_action
self._fan_modes = fan_modes if fan_modes else None
self._attr_fan_mode = None
def set_temperature(self, temperature):
""" Set the target temperature"""
@property
def hvac_action(self):
"""The hvac action of the mock climate"""
return self._attr_hvac_action
@property
def fan_modes(self) -> list[str] | None:
"""The list of fan_modes"""
return self._fan_modes
def set_fan_mode(self, fan_mode):
"""Set the fan mode"""
self._attr_fan_mode = fan_mode
@property
def supported_features(self) -> int:
"""The supported feature of this climate entity"""
ret = ClimateEntityFeature.TARGET_TEMPERATURE
if self._fan_modes:
ret = ret | ClimateEntityFeature.FAN_MODE
return ret
def set_temperature(self, **kwargs):
"""Set the target temperature"""
temperature = kwargs.get(ATTR_TEMPERATURE)
self._attr_target_temperature = temperature
self.async_write_ha_state()
def async_set_hvac_mode(self, hvac_mode):
""" The hvac mode"""
async def async_set_hvac_mode(self, hvac_mode):
"""The hvac mode"""
self._attr_hvac_mode = hvac_mode
self.async_write_ha_state()
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:
def __init__(
self, hass: HomeAssistant, unique_id, name, entry_infos
) -> None: # pylint: disable=unused-argument
"""Initialize the thermostat."""
super().__init__()
@@ -127,6 +267,8 @@ class MockUnavailableClimate(ClimateEntity):
self._attr_hvac_mode = None
self._attr_hvac_modes = [HVACMode.OFF, HVACMode.COOL, HVACMode.HEAT]
self._attr_temperature_unit = UnitOfTemperature.CELSIUS
self._attr_fan_mode = None
class MagicMockClimate(MagicMock):
"""A Magic Mock class for a underlying climate entity"""
@@ -199,13 +341,21 @@ class MagicMockClimate(MagicMock):
def supported_features(self): # pylint: disable=missing-function-docstring
return ClimateEntityFeature.TARGET_TEMPERATURE
@property
def min_temp(self): # pylint: disable=missing-function-docstring
return 15
@property
def max_temp(self): # pylint: disable=missing-function-docstring
return 19
async def create_thermostat(
hass: HomeAssistant, entry: MockConfigEntry, entity_id: str
) -> VersatileThermostat:
) -> BaseThermostat:
"""Creates and return a TPI Thermostat"""
with patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
):
entry.add_to_hass(hass)
await hass.config_entries.async_setup(entry.entry_id)
@@ -221,6 +371,27 @@ async def create_thermostat(
return search_entity(hass, entity_id, CLIMATE_DOMAIN)
async def create_central_config( # pylint: disable=dangerous-default-value
hass: HomeAssistant, entry: MockConfigEntry = FULL_CENTRAL_CONFIG
):
"""Creates a Central Configuration from entry given in argument"""
central_config_entry = MockConfigEntry(
domain=DOMAIN,
title="TheCentralConfigMockName",
unique_id="centralConfigUniqueId",
data=entry,
)
central_config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(central_config_entry.entry_id)
assert central_config_entry.state is ConfigEntryState.LOADED
# Test that VTherm API find the CentralConfig
api = VersatileThermostatAPI.get_vtherm_api(hass)
central_configuration = api.find_central_configuration()
assert central_configuration is not None
def search_entity(hass: HomeAssistant, entity_id, domain) -> Entity:
"""Search and return the entity in the domain"""
component = hass.data[domain]
@@ -230,8 +401,14 @@ def search_entity(hass: HomeAssistant, entity_id, domain) -> Entity:
return None
def count_entities(hass: HomeAssistant, entity_id, domain) -> Entity:
"""Search and return the entity in the domain"""
component = hass.data[domain]
return len(list(component.entities)) if component.entities else 0
async def send_temperature_change_event(
entity: VersatileThermostat, new_temp, date, sleep=True
entity: BaseThermostat, new_temp, date, sleep=True
):
"""Sending a new temperature event simulating a change on temperature sensor"""
_LOGGER.info(
@@ -251,13 +428,15 @@ async def send_temperature_change_event(
)
},
)
await entity._async_temperature_changed(temp_event)
dearm_window_auto = await entity._async_temperature_changed(temp_event)
if sleep:
await asyncio.sleep(0.1)
return dearm_window_auto
async def send_ext_temperature_change_event(
entity: VersatileThermostat, new_temp, date, sleep=True
entity: BaseThermostat, new_temp, date, sleep=True
):
"""Sending a new external temperature event simulating a change on temperature sensor"""
_LOGGER.info(
@@ -282,9 +461,7 @@ async def send_ext_temperature_change_event(
await asyncio.sleep(0.1)
async def send_power_change_event(
entity: VersatileThermostat, new_power, date, sleep=True
):
async def send_power_change_event(entity: BaseThermostat, new_power, date, sleep=True):
"""Sending a new power event simulating a change on power sensor"""
_LOGGER.info(
"------- Testu: sending send_temperature_change_event, new_power=%.2f date=%s on %s",
@@ -309,7 +486,7 @@ async def send_power_change_event(
async def send_max_power_change_event(
entity: VersatileThermostat, new_power_max, date, sleep=True
entity: BaseThermostat, new_power_max, date, sleep=True
):
"""Sending a new power max event simulating a change on power max sensor"""
_LOGGER.info(
@@ -335,7 +512,7 @@ async def send_max_power_change_event(
async def send_window_change_event(
entity: VersatileThermostat, new_state: bool, old_state: bool, date, sleep=True
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(
@@ -369,7 +546,7 @@ async def send_window_change_event(
async def send_motion_change_event(
entity: VersatileThermostat, new_state: bool, old_state: bool, date, sleep=True
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(
@@ -403,7 +580,7 @@ async def send_motion_change_event(
async def send_presence_change_event(
entity: VersatileThermostat, new_state: bool, old_state: bool, date, sleep=True
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(
@@ -436,20 +613,15 @@ async def send_presence_change_event(
return ret
def get_tz(hass: HomeAssistant):
"""Get the current timezone"""
return dt_util.get_time_zone(hass.config.time_zone)
async def send_climate_change_event(
entity: VersatileThermostat,
entity: BaseThermostat,
new_hvac_mode: HVACMode,
old_hvac_mode: HVACMode,
new_hvac_action: HVACAction,
old_hvac_action: HVACAction,
date,
sleep=True,
underlying_entity_id: str = None,
):
"""Sending a new climate event simulating a change on the underlying climate state"""
_LOGGER.info(
@@ -461,18 +633,23 @@ async def send_climate_change_event(
date,
entity,
)
send_from_entity_id = (
underlying_entity_id if underlying_entity_id is not None else entity.entity_id
)
climate_event = Event(
EVENT_STATE_CHANGED,
{
"new_state": State(
entity_id=entity.entity_id,
entity_id=send_from_entity_id,
state=new_hvac_mode,
attributes={"hvac_action": new_hvac_action},
last_changed=date,
last_updated=date,
),
"old_state": State(
entity_id=entity.entity_id,
entity_id=send_from_entity_id,
state=old_hvac_mode,
attributes={"hvac_action": old_hvac_action},
last_changed=date,
@@ -485,8 +662,9 @@ async def send_climate_change_event(
await asyncio.sleep(0.1)
return ret
async def send_climate_change_event_with_temperature(
entity: VersatileThermostat,
entity: BaseThermostat,
new_hvac_mode: HVACMode,
old_hvac_mode: HVACMode,
new_hvac_action: HVACAction,
@@ -531,9 +709,9 @@ async def send_climate_change_event_with_temperature(
return ret
def cancel_switchs_cycles(entity: VersatileThermostat):
def cancel_switchs_cycles(entity: BaseThermostat):
"""This method will cancel all running cycle on all underlying switch entity"""
if entity._is_over_climate:
if entity.is_over_climate:
return
for under in entity._underlyings:
under._cancel_cycle()

View File

@@ -1,4 +1,6 @@
"""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.
@@ -24,9 +26,10 @@ from custom_components.versatile_thermostat.config_flow import (
VersatileThermostatBaseConfigFlow,
)
from custom_components.versatile_thermostat.climate import (
VersatileThermostat,
)
from custom_components.versatile_thermostat.vtherm_api import VersatileThermostatAPI
from custom_components.versatile_thermostat.base_thermostat import BaseThermostat
from .commons import create_central_config
pytest_plugins = "pytest_homeassistant_custom_component" # pylint: disable=invalid-name
@@ -34,7 +37,9 @@ pytest_plugins = "pytest_homeassistant_custom_component" # pylint: disable=inva
# 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):
def auto_enable_custom_integrations(
enable_custom_integrations,
): # pylint: disable=unused-argument
"""Enable all integration in tests"""
yield
@@ -82,7 +87,7 @@ def skip_hass_states_get_fixture():
def skip_control_heating_fixture():
"""Skip the control_heating of VersatileThermostat"""
with patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat._async_control_heating"
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.async_control_heating"
):
yield
@@ -105,6 +110,23 @@ def skip_hass_states_is_state_fixture():
@pytest.fixture(name="skip_send_event")
def skip_send_event_fixture():
"""Skip the send_event in VersatileThermostat"""
with patch.object(VersatileThermostat, "send_event"):
"""Skip the send_event in BaseThermostat"""
with patch.object(BaseThermostat, "send_event"):
yield
@pytest.fixture(name="init_vtherm_api")
def init_vtherm_api_fixture(hass):
"""Initialize the VTherm API"""
VersatileThermostatAPI.get_vtherm_api(hass)
yield
@pytest.fixture(name="init_central_config")
async def init_central_config_fixture(
hass, init_vtherm_api
): # pylint: disable=unused-argument
"""Initialize the VTherm API"""
await create_central_config(hass)
yield

View File

@@ -6,64 +6,23 @@ from homeassistant.components.climate.const import ( # pylint: disable=unused-i
PRESET_NONE,
PRESET_ACTIVITY,
)
from custom_components.versatile_thermostat.const import (
CONF_NAME,
CONF_HEATER,
CONF_HEATER_2,
CONF_HEATER_3,
CONF_HEATER_4,
CONF_THERMOSTAT_CLIMATE,
CONF_THERMOSTAT_SWITCH,
CONF_THERMOSTAT_TYPE,
CONF_AC_MODE,
CONF_TEMP_SENSOR,
CONF_EXTERNAL_TEMP_SENSOR,
CONF_CYCLE_MIN,
CONF_TEMP_MAX,
CONF_TEMP_MIN,
CONF_PROP_FUNCTION,
PROPORTIONAL_FUNCTION_TPI,
CONF_TPI_COEF_INT,
CONF_TPI_COEF_EXT,
CONF_MINIMAL_ACTIVATION_DELAY,
CONF_SECURITY_DELAY_MIN,
CONF_SECURITY_MIN_ON_PERCENT,
CONF_SECURITY_DEFAULT_ON_PERCENT,
CONF_USE_WINDOW_FEATURE,
CONF_USE_MOTION_FEATURE,
CONF_USE_POWER_FEATURE,
CONF_USE_PRESENCE_FEATURE,
CONF_WINDOW_SENSOR,
CONF_WINDOW_DELAY,
CONF_WINDOW_AUTO_OPEN_THRESHOLD,
CONF_WINDOW_AUTO_CLOSE_THRESHOLD,
CONF_WINDOW_AUTO_MAX_DURATION,
CONF_MOTION_SENSOR,
CONF_MOTION_DELAY,
CONF_MOTION_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,
)
from custom_components.versatile_thermostat.const import * # pylint: disable=wildcard-import, unused-wildcard-import
MOCK_TH_OVER_SWITCH_USER_CONFIG = {
CONF_NAME: "TheOverSwitchMockName",
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_SWITCH,
}
MOCK_TH_OVER_SWITCH_MAIN_CONFIG = {
CONF_NAME: "TheOverSwitchMockName",
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
CONF_CYCLE_MIN: 5,
CONF_TEMP_MIN: 15,
CONF_TEMP_MAX: 30,
CONF_DEVICE_POWER: 1,
CONF_USE_WINDOW_FEATURE: True,
CONF_USE_MOTION_FEATURE: True,
CONF_USE_POWER_FEATURE: True,
CONF_USE_PRESENCE_FEATURE: True,
CONF_USE_MAIN_CENTRAL_CONFIG: True,
}
MOCK_TH_OVER_4SWITCH_USER_CONFIG = {
@@ -82,20 +41,46 @@ MOCK_TH_OVER_4SWITCH_USER_CONFIG = {
}
MOCK_TH_OVER_CLIMATE_USER_CONFIG = {
CONF_NAME: "TheOverClimateMockName",
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_CLIMATE,
}
MOCK_TH_OVER_CLIMATE_MAIN_CONFIG = {
CONF_NAME: "TheOverClimateMockName",
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
CONF_CYCLE_MIN: 5,
CONF_DEVICE_POWER: 1,
CONF_USE_MAIN_CENTRAL_CONFIG: False,
CONF_USE_CENTRAL_MODE: True
# Keep default values which are False
}
MOCK_TH_OVER_CLIMATE_CENTRAL_MAIN_CONFIG = {
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
CONF_TEMP_MIN: 15,
CONF_TEMP_MAX: 30,
# Keep default values which are False
}
MOCK_TH_OVER_SWITCH_CENTRAL_MAIN_CONFIG = {
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
CONF_TEMP_MIN: 15,
CONF_TEMP_MAX: 30,
CONF_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 = {
@@ -104,6 +89,8 @@ MOCK_TH_OVER_4SWITCH_TYPE_CONFIG = {
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 = {
@@ -114,16 +101,50 @@ MOCK_TH_OVER_SWITCH_TPI_CONFIG = {
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,
CONF_AUTO_FAN_MODE: CONF_AUTO_FAN_HIGH,
}
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_FROST_PROTECTION + "_temp": 7,
PRESET_ECO + "_temp": 16,
PRESET_COMFORT + "_temp": 17,
PRESET_BOOST + "_temp": 18,
}
MOCK_PRESETS_AC_CONFIG = {
PRESET_FROST_PROTECTION + "_temp": 7,
PRESET_ECO + "_temp": 17,
PRESET_COMFORT + "_temp": 19,
PRESET_BOOST + "_temp": 20,
PRESET_ECO + "_ac_temp": 25,
PRESET_COMFORT + "_ac_temp": 23,
PRESET_BOOST + "_ac_temp": 21,
}
MOCK_WINDOW_CONFIG = {
CONF_WINDOW_SENSOR: "binary_sensor.window_sensor",
# Not used normally only for tests to avoid rewrite all tests
CONF_WINDOW_DELAY: 10,
}
MOCK_WINDOW_DELAY_CONFIG = {
CONF_WINDOW_DELAY: 10,
}
@@ -136,6 +157,7 @@ MOCK_WINDOW_AUTO_CONFIG = {
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,
}
@@ -153,6 +175,17 @@ MOCK_PRESENCE_CONFIG = {
PRESET_BOOST + PRESET_AWAY_SUFFIX + "_temp": 18,
}
MOCK_PRESENCE_AC_CONFIG = {
CONF_PRESENCE_SENSOR: "person.presence_sensor",
PRESET_FROST_PROTECTION + PRESET_AWAY_SUFFIX + "_temp": 7,
PRESET_ECO + PRESET_AWAY_SUFFIX + "_temp": 16,
PRESET_COMFORT + PRESET_AWAY_SUFFIX + "_temp": 17,
PRESET_BOOST + PRESET_AWAY_SUFFIX + "_temp": 18,
PRESET_ECO + "_ac" + PRESET_AWAY_SUFFIX + "_temp": 27,
PRESET_COMFORT + "_ac" + PRESET_AWAY_SUFFIX + "_temp": 26,
PRESET_BOOST + "_ac" + PRESET_AWAY_SUFFIX + "_temp": 25,
}
MOCK_ADVANCED_CONFIG = {
CONF_MINIMAL_ACTIVATION_DELAY: 10,
CONF_SECURITY_DELAY_MIN: 5,
@@ -166,3 +199,14 @@ MOCK_DEFAULT_FEATURE_CONFIG = {
CONF_USE_POWER_FEATURE: False,
CONF_USE_PRESENCE_FEATURE: False,
}
MOCK_DEFAULT_CENTRAL_CONFIG = {
CONF_USE_MAIN_CENTRAL_CONFIG: False,
CONF_USE_TPI_CENTRAL_CONFIG: False,
CONF_USE_PRESETS_CENTRAL_CONFIG: False,
CONF_USE_WINDOW_CENTRAL_CONFIG: False,
CONF_USE_MOTION_CENTRAL_CONFIG: False,
CONF_USE_POWER_CENTRAL_CONFIG: False,
CONF_USE_PRESENCE_CENTRAL_CONFIG: False,
CONF_USE_ADVANCED_CENTRAL_CONFIG: False,
}

346
tests/test_auto_fan_mode.py Normal file
View File

@@ -0,0 +1,346 @@
# pylint: disable=wildcard-import, unused-wildcard-import, protected-access, unused-argument, line-too-long
""" Test the auto fan mode of a over_climate thermostat """
from unittest.mock import patch, call
from datetime import datetime # , timedelta
from homeassistant.core import HomeAssistant
# from homeassistant.components.climate import HVACAction, HVACMode
from homeassistant.config_entries import ConfigEntryState
# from homeassistant.helpers.entity_component import EntityComponent
# from homeassistant.components.climate import ClimateEntity, DOMAIN as CLIMATE_DOMAIN
from pytest_homeassistant_custom_component.common import MockConfigEntry
# from custom_components.versatile_thermostat.base_thermostat import BaseThermostat
from custom_components.versatile_thermostat.thermostat_climate import (
ThermostatOverClimate,
)
from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_over_climate_auto_fan_mode_turbo(
hass: HomeAssistant, skip_hass_states_is_state, skip_send_event
):
"""Test the init of an over climate thermostat with auto_fan_mode = Turbo which exists"""
fan_modes = ["low", "medium", "high", "boost", "mute", "auto", "turbo"]
entry = MockConfigEntry(
domain=DOMAIN,
title="TheOverClimateMockName",
unique_id="uniqueId",
data={
CONF_NAME: "TheOverClimateMockName",
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_CLIMATE,
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
CONF_CYCLE_MIN: 5,
CONF_TEMP_MIN: 15,
CONF_TEMP_MAX: 30,
"eco_temp": 17,
"comfort_temp": 18,
"boost_temp": 19,
CONF_USE_WINDOW_FEATURE: False,
CONF_USE_MOTION_FEATURE: False,
CONF_USE_POWER_FEATURE: False,
CONF_USE_PRESENCE_FEATURE: False,
CONF_CLIMATE: "climate.mock_climate",
CONF_MINIMAL_ACTIVATION_DELAY: 30,
CONF_SECURITY_DELAY_MIN: 5,
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
CONF_AUTO_FAN_MODE: CONF_AUTO_FAN_TURBO,
},
)
fake_underlying_climate = MockClimate(
hass=hass,
unique_id="mockUniqueId",
name="MockClimateName",
fan_modes=fan_modes,
)
with patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
return_value=fake_underlying_climate,
):
entry.add_to_hass(hass)
await hass.config_entries.async_setup(entry.entry_id)
assert entry.state is ConfigEntryState.LOADED
entity: ThermostatOverClimate = search_entity(
hass, "climate.theoverclimatemockname", "climate"
)
assert entity
assert isinstance(entity, ThermostatOverClimate)
assert entity.name == "TheOverClimateMockName"
assert entity.is_over_climate is True
assert entity.fan_modes == fan_modes
assert entity._auto_fan_mode == "auto_fan_turbo"
assert entity._auto_activated_fan_mode == "turbo"
assert entity._auto_deactivated_fan_mode == "mute"
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_over_climate_auto_fan_mode_not_turbo(
hass: HomeAssistant, skip_hass_states_is_state, skip_send_event
):
"""Test the init of an over climate thermostat with auto_fan_mode = Turbo which doesn't exists"""
fan_modes = ["low", "medium", "high", "boost", "auto"]
entry = MockConfigEntry(
domain=DOMAIN,
title="TheOverClimateMockName",
unique_id="uniqueId",
data={
CONF_NAME: "TheOverClimateMockName",
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_CLIMATE,
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
CONF_CYCLE_MIN: 5,
CONF_TEMP_MIN: 15,
CONF_TEMP_MAX: 30,
"eco_temp": 17,
"comfort_temp": 18,
"boost_temp": 19,
CONF_USE_WINDOW_FEATURE: False,
CONF_USE_MOTION_FEATURE: False,
CONF_USE_POWER_FEATURE: False,
CONF_USE_PRESENCE_FEATURE: False,
CONF_CLIMATE: "climate.mock_climate",
CONF_MINIMAL_ACTIVATION_DELAY: 30,
CONF_SECURITY_DELAY_MIN: 5,
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
CONF_AUTO_FAN_MODE: CONF_AUTO_FAN_TURBO,
},
)
fake_underlying_climate = MockClimate(
hass=hass,
unique_id="mockUniqueId",
name="MockClimateName",
fan_modes=fan_modes,
)
with patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
return_value=fake_underlying_climate,
):
entry.add_to_hass(hass)
await hass.config_entries.async_setup(entry.entry_id)
assert entry.state is ConfigEntryState.LOADED
entity: ThermostatOverClimate = search_entity(
hass, "climate.theoverclimatemockname", "climate"
)
assert entity
assert isinstance(entity, ThermostatOverClimate)
assert entity.name == "TheOverClimateMockName"
assert entity.is_over_climate is True
assert entity.fan_modes == fan_modes
assert entity._auto_fan_mode == "auto_fan_turbo"
# Turbo doesn't exists -> fallback to high
assert entity._auto_activated_fan_mode == "high"
# Mute doesn't exists -> fallback to auto
assert entity._auto_deactivated_fan_mode == "auto"
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_over_climate_auto_fan_mode_turbo_activation(
hass: HomeAssistant, skip_hass_states_is_state, skip_send_event
):
"""Test the init of an over climate thermostat with auto_fan_mode = Turbo which exists"""
fan_modes = ["low", "medium", "high", "boost", "mute", "auto", "turbo"]
entry = MockConfigEntry(
domain=DOMAIN,
title="TheOverClimateMockName",
unique_id="uniqueId",
data={
CONF_NAME: "TheOverClimateMockName",
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_CLIMATE,
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
CONF_CYCLE_MIN: 5,
CONF_TEMP_MIN: 15,
CONF_TEMP_MAX: 30,
"eco_temp": 17,
"comfort_temp": 18,
"boost_temp": 19,
"eco_ac_temp": 25,
"comfort_ac_temp": 23,
"boost_ac_temp": 21,
CONF_USE_WINDOW_FEATURE: False,
CONF_USE_MOTION_FEATURE: False,
CONF_USE_POWER_FEATURE: False,
CONF_USE_PRESENCE_FEATURE: False,
CONF_CLIMATE: "climate.mock_climate",
CONF_MINIMAL_ACTIVATION_DELAY: 30,
CONF_SECURITY_DELAY_MIN: 5,
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
CONF_AUTO_FAN_MODE: CONF_AUTO_FAN_TURBO,
CONF_AC_MODE: True,
},
)
tz = get_tz(hass) # pylint: disable=invalid-name
now: datetime = datetime.now(tz=tz)
fake_underlying_climate = MockClimate(
hass=hass,
unique_id="mockUniqueId",
name="MockClimateName",
fan_modes=fan_modes,
)
# 1. Init fan mode
with patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
return_value=fake_underlying_climate,
):
entry.add_to_hass(hass)
await hass.config_entries.async_setup(entry.entry_id)
assert entry.state is ConfigEntryState.LOADED
entity: ThermostatOverClimate = search_entity(
hass, "climate.theoverclimatemockname", "climate"
)
assert entity
assert isinstance(entity, ThermostatOverClimate)
assert entity.name == "TheOverClimateMockName"
assert entity.is_over_climate is True
assert entity.fan_modes == fan_modes
assert entity.fan_mode is None
assert entity._auto_fan_mode == "auto_fan_turbo"
assert entity._auto_activated_fan_mode == "turbo"
assert entity._auto_deactivated_fan_mode == "mute"
# 2. Turn on and set temperature cold
with patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.set_fan_mode"
) as mock_send_fan_mode:
# Force preset mode
await entity.async_set_hvac_mode(HVACMode.HEAT)
assert entity.hvac_mode == HVACMode.HEAT
await entity.async_set_preset_mode(PRESET_COMFORT)
assert entity.preset_mode == PRESET_COMFORT
assert entity.target_temperature == 18
# Change the current temperature to 16 which is 2° under
await send_temperature_change_event(entity, 16, now, True)
fake_underlying_climate.set_fan_mode("turbo")
assert mock_send_fan_mode.call_count == 1
mock_send_fan_mode.assert_has_calls([call.set_fan_mode("turbo")])
assert entity.fan_mode == "turbo"
# 3. Set another low temperature
with patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.set_fan_mode"
) as mock_send_fan_mode:
fake_underlying_climate.set_fan_mode("turbo")
# Change the current temperature to 17 which is 1° under
await send_temperature_change_event(entity, 15, now, True)
# Nothing is send cause we are already in turbo fan mode
assert mock_send_fan_mode.call_count == 0
assert entity.fan_mode == "turbo"
# 4. Set temperature not so cold
with patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.set_fan_mode"
) as mock_send_fan_mode:
# Change the current temperature to 17 which is 1° under
await send_temperature_change_event(entity, 17, now, True)
fake_underlying_climate.set_fan_mode("mute")
assert mock_send_fan_mode.call_count == 1
mock_send_fan_mode.assert_has_calls([call.set_fan_mode("mute")])
assert entity.fan_mode == "mute"
# 5. Set temperature not so cold another time
with patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.set_fan_mode"
) as mock_send_fan_mode:
fake_underlying_climate.set_fan_mode("mute")
# Change the current temperature to 17 which is 1° under
await send_temperature_change_event(entity, 17.1, now, True)
assert mock_send_fan_mode.call_count == 0
assert entity.fan_mode == "mute"
# 6. Set temperature very high above the target
with patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.set_fan_mode"
) as mock_send_fan_mode:
fake_underlying_climate.set_fan_mode("mute")
# Change the current temperature to 17 which is 1° under
await send_temperature_change_event(entity, 21, now, True)
assert mock_send_fan_mode.call_count == 0
assert entity.fan_mode == "mute"
# 7. In AC mode, set temperature very high under the target
with patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.set_fan_mode"
) as mock_send_fan_mode:
await entity.async_set_hvac_mode(HVACMode.COOL)
assert entity.hvac_mode == HVACMode.COOL
assert entity.preset_mode == PRESET_COMFORT
assert entity.target_temperature == 23
assert entity.current_temperature == 21
fake_underlying_climate.set_fan_mode("mute")
# Change the current temperature to 17 which is 1° under
await send_temperature_change_event(entity, 20, now, True)
assert mock_send_fan_mode.call_count == 0
assert entity.fan_mode == "mute"
# 8. In AC mode, set temperature not so high above the target
with patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.set_fan_mode"
) as mock_send_fan_mode:
assert entity.target_temperature == 23
await send_temperature_change_event(entity, 24, now, True)
assert entity.current_temperature == 24
fake_underlying_climate.set_fan_mode("mute")
assert mock_send_fan_mode.call_count == 0
assert entity.fan_mode == "mute"
# 8. In AC mode, set temperature high above the target
with patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.set_fan_mode"
) as mock_send_fan_mode:
assert entity.target_temperature == 23
await send_temperature_change_event(entity, 25.1, now, True)
assert entity.current_temperature == 25.1
fake_underlying_climate.set_fan_mode("turbo")
assert mock_send_fan_mode.call_count == 1
mock_send_fan_mode.assert_has_calls([call.set_fan_mode("turbo")])
assert entity.fan_mode == "turbo"

View File

@@ -0,0 +1,376 @@
# 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_mode is HVACMode.OFF
assert entity.hvac_action is HVACAction.OFF
assert entity.target_temperature == entity.min_temp
assert entity.preset_modes == [
PRESET_NONE,
PRESET_FROST_PROTECTION,
PRESET_ECO,
PRESET_COMFORT,
PRESET_BOOST,
]
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_FROST_PROTECTION,
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

@@ -1,3 +1,5 @@
# 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
@@ -9,9 +11,8 @@ from homeassistant.components.binary_sensor import BinarySensorDeviceClass
from pytest_homeassistant_custom_component.common import MockConfigEntry
from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import
from ..climate import VersatileThermostat
from ..binary_sensor import (
from custom_components.versatile_thermostat.base_thermostat import BaseThermostat
from custom_components.versatile_thermostat.binary_sensor import (
SecurityBinarySensor,
OverpoweringBinarySensor,
WindowBinarySensor,
@@ -19,7 +20,7 @@ from ..binary_sensor import (
PresenceBinarySensor,
)
from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import
from .commons import *
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@@ -29,7 +30,7 @@ async def test_security_binary_sensors(
skip_hass_states_is_state,
skip_turn_on_off_heater,
skip_send_event,
):
): # pylint: disable=unused-argument
"""Test the security binary sensors in thermostat type"""
entry = MockConfigEntry(
@@ -61,7 +62,7 @@ async def test_security_binary_sensors(
},
)
entity: VersatileThermostat = await create_thermostat(
entity: BaseThermostat = await create_thermostat (
hass, entry, "climate.theoverswitchmockname"
)
assert entity
@@ -142,7 +143,7 @@ async def test_overpowering_binary_sensors(
},
)
entity: VersatileThermostat = await create_thermostat(
entity: BaseThermostat = await create_thermostat(
hass, entry, "climate.theoverswitchmockname"
)
assert entity
@@ -224,7 +225,7 @@ async def test_window_binary_sensors(
},
)
entity: VersatileThermostat = await create_thermostat(
entity: BaseThermostat = await create_thermostat(
hass, entry, "climate.theoverswitchmockname"
)
assert entity
@@ -240,7 +241,7 @@ async def test_window_binary_sensors(
await entity.async_set_preset_mode(PRESET_COMFORT)
await entity.async_set_hvac_mode(HVACMode.HEAT)
await send_temperature_change_event(entity, 15, now)
assert entity.window_state is None
assert entity.window_state is STATE_OFF
await window_binary_sensor.async_my_climate_changed()
assert window_binary_sensor.state is STATE_OFF
@@ -312,7 +313,7 @@ async def test_motion_binary_sensors(
},
)
entity: VersatileThermostat = await create_thermostat(
entity: BaseThermostat = await create_thermostat(
hass, entry, "climate.theoverswitchmockname"
)
assert entity
@@ -402,7 +403,7 @@ async def test_presence_binary_sensors(
},
)
entity: VersatileThermostat = await create_thermostat(
entity: BaseThermostat = await create_thermostat(
hass, entry, "climate.theoverswitchmockname"
)
assert entity
@@ -484,7 +485,7 @@ async def test_binary_sensors_over_climate_minimal(
},
)
entity: VersatileThermostat = await create_thermostat(
entity: BaseThermostat = await create_thermostat(
hass, entry, "climate.theoverclimatemockname"
)
assert entity

View File

@@ -1,10 +1,17 @@
# 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 .commons import * # pylint: disable=wildcard-import, unused-wildcard-import
from datetime import datetime, timedelta
import logging
from homeassistant.components.climate import (
SERVICE_SET_TEMPERATURE,
)
from .commons import *
logging.getLogger().setLevel(logging.DEBUG)
@@ -49,7 +56,7 @@ async def test_bug_56(
},
)
entity: VersatileThermostat = await create_thermostat(
entity: BaseThermostat = await create_thermostat(
hass, entry, "climate.theoverclimatemockname"
)
assert entity
@@ -60,13 +67,11 @@ async def test_bug_56(
# Should not failed
entity.update_custom_attributes()
# try to call _async_control_heating
# try to call async_control_heating
try:
await entity._async_control_heating()
ret = await entity.async_control_heating()
# an exception should be send
assert False
except UnknownEntity:
pass
assert ret is False
except Exception: # pylint: disable=broad-exception-caught
assert False
@@ -75,9 +80,9 @@ async def test_bug_56(
"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 to call async_control_heating
try:
await entity._async_control_heating()
await entity.async_control_heating()
except UnknownEntity:
assert False
except Exception: # pylint: disable=broad-exception-caught
@@ -128,7 +133,7 @@ async def test_bug_63(
},
)
entity: VersatileThermostat = await create_thermostat(
entity: BaseThermostat = await create_thermostat(
hass, entry, "climate.theoverswitchmockname"
)
assert entity
@@ -180,7 +185,7 @@ async def test_bug_64(
},
)
entity: VersatileThermostat = await create_thermostat(
entity: BaseThermostat = await create_thermostat(
hass, entry, "climate.theoverswitchmockname"
)
assert entity
@@ -232,7 +237,7 @@ async def test_bug_66(
},
)
entity: VersatileThermostat = await create_thermostat(
entity: BaseThermostat = await create_thermostat(
hass, entry, "climate.theoverswitchmockname"
)
assert entity
@@ -243,11 +248,11 @@ async def test_bug_66(
assert entity.hvac_mode is HVACMode.HEAT
assert entity.preset_mode is PRESET_BOOST
assert entity.target_temperature == 19
assert entity.window_state is None
assert entity.window_state is STATE_OFF
# Open the window and let the thermostat shut down
with patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
"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(
@@ -275,7 +280,7 @@ async def test_bug_66(
# Close the window but too shortly
with patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
"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(
@@ -298,7 +303,7 @@ async def test_bug_66(
# Reopen immediatly with sufficient time
with patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
"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(
@@ -321,7 +326,7 @@ async def test_bug_66(
# Close the window but with sufficient time this time
with patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
"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(
@@ -353,7 +358,7 @@ async def test_bug_82(
skip_turn_on_off_heater,
skip_send_event,
):
"""Test that when a underlying climate is not available the VTherm doesn't go into security mode"""
"""Test that when a underlying climate is not available the VTherm doesn't go into safety mode"""
tz = get_tz(hass) # pylint: disable=invalid-name
now: datetime = datetime.now(tz=tz)
@@ -362,13 +367,15 @@ async def test_bug_82(
domain=DOMAIN,
title="TheOverClimateMockName",
unique_id="uniqueId",
data=PARTIAL_CLIMATE_CONFIG, # 5 minutes security delay
data=PARTIAL_CLIMATE_CONFIG, # 5 minutes security delay
)
fake_underlying_climate = MockUnavailableClimate(hass, "mockUniqueId", "MockClimateName", {})
fake_underlying_climate = MockUnavailableClimate(
hass, "mockUniqueId", "MockClimateName", {}
)
with patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
"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,
@@ -389,13 +396,14 @@ async def test_bug_82(
assert entity
assert entity.name == "TheOverClimateMockName"
assert entity._is_over_climate is True
assert entity.is_over_climate is True
# assert entity.hvac_action is HVACAction.OFF
assert entity.hvac_mode is HVACMode.OFF
# assert entity.hvac_mode is None
assert entity.target_temperature == entity.min_temp
assert entity.preset_modes == [
PRESET_NONE,
PRESET_FROST_PROTECTION,
PRESET_ECO,
PRESET_COMFORT,
PRESET_BOOST,
@@ -419,12 +427,14 @@ async def test_bug_82(
assert mock_find_climate.mock_calls[0] == call()
mock_find_climate.assert_has_calls([call.find_underlying_entity()])
# Force security mode
assert entity._last_ext_temperature_mesure is not None
assert entity._last_temperature_mesure is not None
assert (entity._last_temperature_mesure.astimezone(tz) - now).total_seconds() < 1
# Force safety mode
assert entity._last_ext_temperature_measure is not None
assert entity._last_temperature_measure is not None
assert (
entity._last_ext_temperature_mesure.astimezone(tz) - now
entity._last_temperature_measure.astimezone(tz) - now
).total_seconds() < 1
assert (
entity._last_ext_temperature_measure.astimezone(tz) - now
).total_seconds() < 1
# Tries to turns on the Thermostat
@@ -433,18 +443,19 @@ async def test_bug_82(
# 2. activate security feature when date is expired
with patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
"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)
# Should stay False
assert entity.security_state is False
assert entity.preset_mode == 'none'
assert entity._saved_preset_mode == 'none'
assert entity.preset_mode == "none"
assert entity._saved_preset_mode == "none"
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
@@ -463,14 +474,16 @@ async def test_bug_101(
domain=DOMAIN,
title="TheOverClimateMockName",
unique_id="uniqueId",
data=PARTIAL_CLIMATE_CONFIG, # 5 minutes security delay
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)
fake_underlying_climate = MockClimate(
hass, "mockUniqueId", "MockClimateName", {}, HVACMode.HEAT, HVACAction.HEATING
)
with patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
"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,
@@ -493,9 +506,9 @@ async def test_bug_101(
assert entity
assert entity.name == "TheOverClimateMockName"
assert entity._is_over_climate is True
assert entity.is_over_climate is True
assert entity.hvac_mode is HVACMode.OFF
# because the underlying is heating. In real life the underlying should be shut-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
@@ -530,11 +543,172 @@ async def test_bug_101(
await entity.async_set_preset_mode(PRESET_COMFORT)
assert entity.preset_mode == PRESET_COMFORT
# 2. Change the target temp of underlying thermostat
await send_climate_change_event_with_temperature(entity, HVACMode.HEAT, HVACMode.HEAT, HVACAction.OFF, HVACAction.OFF, now, 12.75)
# Should have been switched to Manual preset
# 2. Change the target temp of underlying thermostat at now -> the event will be disgarded because to fast (to avoid loop cf issue 121)
await send_climate_change_event_with_temperature(
entity,
HVACMode.HEAT,
HVACMode.HEAT,
HVACAction.OFF,
HVACAction.OFF,
now,
12.75,
)
# Should NOT have been switched to Manual preset
assert entity.target_temperature == 17
assert entity.preset_mode is PRESET_COMFORT
# 2. Change the target temp of underlying thermostat at 11 sec later -> the event will be taken
# Wait 11 sec
event_timestamp = now + timedelta(seconds=11)
assert entity.is_regulated is False
await send_climate_change_event_with_temperature(
entity,
HVACMode.HEAT,
HVACMode.HEAT,
HVACAction.OFF,
HVACAction.OFF,
event_timestamp,
12.75,
)
assert entity.target_temperature == 12.75
assert entity.preset_mode is PRESET_NONE
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_bug_272(
hass: HomeAssistant,
skip_hass_states_is_state,
skip_turn_on_off_heater,
skip_send_event,
):
"""Test that it not possible to set the target temperature under the min_temp setting"""
tz = get_tz(hass) # pylint: disable=invalid-name
now: datetime = datetime.now(tz=tz)
entry = MockConfigEntry(
domain=DOMAIN,
title="TheOverClimateMockName",
unique_id="uniqueId",
data=PARTIAL_CLIMATE_CONFIG, # 5 minutes security delay
)
# Min_temp is 15 and max_temp is 19
fake_underlying_climate = MagicMockClimate()
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
), patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
return_value=fake_underlying_climate,
), patch(
"homeassistant.core.ServiceRegistry.async_call"
) as mock_service_call:
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
assert entity.target_temperature == entity.min_temp
assert entity.is_regulated is True
assert mock_service_call.call_count == 0
# Set the hvac_mode to HEAT
await entity.async_set_hvac_mode(HVACMode.HEAT)
# In the accepted interval
await entity.async_set_temperature(temperature=17.5)
# MagicMock climate is already HEAT by default. So there is no SET_HAVC_MODE call
assert mock_service_call.call_count == 1
mock_service_call.assert_has_calls(
[
# call.async_call(
# "climate",
# SERVICE_SET_HVAC_MODE,
# {"entity_id": "climate.mock_climate", "hvac_mode": HVACMode.HEAT},
# ),
call.async_call(
"climate",
SERVICE_SET_TEMPERATURE,
{
"entity_id": "climate.mock_climate",
"temperature": 17.5,
"target_temp_high": 30,
"target_temp_low": 15,
},
),
]
)
tz = get_tz(hass) # pylint: disable=invalid-name
now: datetime = datetime.now(tz=tz)
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
), patch("homeassistant.core.ServiceRegistry.async_call") as mock_service_call:
# Set room temperature to something very cold
event_timestamp = now + timedelta(minutes=1)
await send_temperature_change_event(entity, 13, event_timestamp)
await send_ext_temperature_change_event(entity, 9, event_timestamp)
# Not in the accepted interval (15-19)
await entity.async_set_temperature(temperature=10)
assert mock_service_call.call_count == 1
mock_service_call.assert_has_calls(
[
call.async_call(
"climate",
SERVICE_SET_TEMPERATURE,
{
"entity_id": "climate.mock_climate",
"temperature": 15, # the minimum acceptable
"target_temp_high": 30,
"target_temp_low": 15,
},
),
]
)
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
), patch("homeassistant.core.ServiceRegistry.async_call") as mock_service_call:
# Set room temperature to something very cold
event_timestamp = now + timedelta(minutes=1)
await send_temperature_change_event(entity, 13, event_timestamp)
await send_ext_temperature_change_event(entity, 9, event_timestamp)
# In the accepted interval
await entity.async_set_temperature(temperature=20.8)
assert mock_service_call.call_count == 1
mock_service_call.assert_has_calls(
[
call.async_call(
"climate",
SERVICE_SET_TEMPERATURE,
{
"entity_id": "climate.mock_climate",
"temperature": 19, # the maximum acceptable
"target_temp_high": 30,
"target_temp_low": 15,
},
),
]
)

View File

@@ -0,0 +1,442 @@
# pylint: disable=wildcard-import, unused-wildcard-import, protected-access, unused-argument, line-too-long
""" Test the central_configuration """
from unittest.mock import patch # , call
# from datetime import datetime # , timedelta
from homeassistant import data_entry_flow
from homeassistant.core import HomeAssistant
# from homeassistant.components.climate import HVACAction, HVACMode
from homeassistant.config_entries import ConfigEntryState, SOURCE_USER
# from homeassistant.helpers.entity_component import EntityComponent
# from homeassistant.components.climate import ClimateEntity, DOMAIN as CLIMATE_DOMAIN
from pytest_homeassistant_custom_component.common import MockConfigEntry
# from custom_components.versatile_thermostat.base_thermostat import BaseThermostat
from custom_components.versatile_thermostat.thermostat_climate import (
ThermostatOverClimate,
)
from custom_components.versatile_thermostat.thermostat_switch import (
ThermostatOverSwitch,
)
from custom_components.versatile_thermostat.vtherm_api import VersatileThermostatAPI
from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import
from .const import * # pylint: disable=wildcard-import, unused-wildcard-import
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_add_a_central_config(hass: HomeAssistant, skip_hass_states_is_state):
"""Tests the clean_central_config_doubon of base_thermostat"""
central_config_entry = MockConfigEntry(
domain=DOMAIN,
title="TheCentralConfigMockName",
unique_id="centralConfigUniqueId",
data={
CONF_NAME: CENTRAL_CONFIG_NAME,
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_CENTRAL_CONFIG,
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_central_ext_temp_sensor",
CONF_TEMP_MIN: 15,
CONF_TEMP_MAX: 30,
CONF_TPI_COEF_INT: 0.5,
CONF_TPI_COEF_EXT: 0.02,
"frost_temp": 10,
"eco_temp": 17.1,
"comfort_temp": 18.1,
"boost_temp": 19.1,
"eco_ac_temp": 25.1,
"comfort_ac_temp": 23.1,
"boost_ac_temp": 21.1,
"frost_away_temp": 15.1,
"eco_away_temp": 15.2,
"comfort_away_temp": 15.3,
"boost_away_temp": 15.4,
"eco_ac_away_temp": 30.5,
"comfort_ac_away_temp": 30.6,
"boost_ac_away_temp": 30.7,
CONF_WINDOW_DELAY: 15,
CONF_WINDOW_AUTO_OPEN_THRESHOLD: 4,
CONF_WINDOW_AUTO_CLOSE_THRESHOLD: 1,
CONF_WINDOW_AUTO_MAX_DURATION: 31,
CONF_MOTION_DELAY: 31,
CONF_MOTION_OFF_DELAY: 301,
CONF_MOTION_PRESET: "boost",
CONF_NO_MOTION_PRESET: "frost",
CONF_POWER_SENSOR: "sensor.mock_central_power_sensor",
CONF_MAX_POWER_SENSOR: "sensor.mock_central_max_power_sensor",
CONF_PRESET_POWER: 14,
CONF_MINIMAL_ACTIVATION_DELAY: 11,
CONF_SECURITY_DELAY_MIN: 61,
CONF_SECURITY_MIN_ON_PERCENT: 0.5,
CONF_SECURITY_DEFAULT_ON_PERCENT: 0.2,
},
)
central_config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(central_config_entry.entry_id)
assert central_config_entry.state is ConfigEntryState.LOADED
entity: ThermostatOverClimate = search_entity(
hass, "climate.thecentralconfigmockname", "climate"
)
assert entity is None
assert count_entities(hass, "climate.thecentralconfigmockname", "climate") == 0
# Test that VTherm API find the CentralConfig
api = VersatileThermostatAPI.get_vtherm_api(hass)
central_configuration = api.find_central_configuration()
assert central_configuration is not None
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_minimal_over_switch_wo_central_config(
hass: HomeAssistant, skip_hass_states_is_state, init_vtherm_api
):
"""Tests that a VTherm without any central_configuration is working with its own attributes"""
# Add a Switch VTherm
entry = MockConfigEntry(
domain=DOMAIN,
title="TheOverSwitchMockName",
unique_id="uniqueId",
data={
CONF_NAME: "TheOverSwitchMockName",
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_SWITCH,
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
CONF_CYCLE_MIN: 5,
CONF_TEMP_MIN: 8,
CONF_TEMP_MAX: 18,
"frost_temp": 10,
"eco_temp": 17,
"comfort_temp": 18,
"boost_temp": 21,
CONF_USE_WINDOW_FEATURE: False,
CONF_USE_MOTION_FEATURE: False,
CONF_USE_POWER_FEATURE: False,
CONF_USE_PRESENCE_FEATURE: False,
CONF_HEATER: "switch.mock_switch",
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
CONF_TPI_COEF_INT: 0.3,
CONF_TPI_COEF_EXT: 0.01,
CONF_MINIMAL_ACTIVATION_DELAY: 30,
CONF_SECURITY_DELAY_MIN: 5,
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
CONF_SECURITY_DEFAULT_ON_PERCENT: 0.1,
# CONF_WINDOW_AUTO_OPEN_THRESHOLD: 0.1,
# CONF_WINDOW_AUTO_CLOSE_THRESHOLD: 0.1,
# CONF_WINDOW_AUTO_MAX_DURATION: 0, # Should be 0 for test
CONF_INVERSE_SWITCH: True,
# CONF_USE_MAIN_CENTRAL_CONFIG: False,
# CONF_USE_TPI_CENTRAL_CONFIG: False,
# CONF_USE_WINDOW_CENTRAL_CONFIG: False,
# CONF_USE_MOTION_CENTRAL_CONFIG: False,
# CONF_USE_POWER_CENTRAL_CONFIG: False,
# CONF_USE_PRESENCE_CENTRAL_CONFIG: False,
# CONF_USE_PRESETS_CENTRAL_CONFIG: False,
# CONF_USE_ADVANCED_CENTRAL_CONFIG: False,
},
)
with patch("homeassistant.core.ServiceRegistry.async_call"):
entity: ThermostatOverSwitch = await create_thermostat(
hass, entry, "climate.theoverswitchmockname"
)
assert entity
assert entity.name == "TheOverSwitchMockName"
assert entity.is_over_switch
assert entity._temp_sensor_entity_id == "sensor.mock_temp_sensor"
assert entity._ext_temp_sensor_entity_id == "sensor.mock_ext_temp_sensor"
assert entity._cycle_min == 5
assert entity._attr_min_temp == 8
assert entity._attr_max_temp == 18
assert entity.preset_modes == ["none", "frost", "eco", "comfort", "boost"]
assert entity.is_window_auto_enabled is False
assert entity.nb_underlying_entities == 1
assert entity.underlying_entity_id(0) == "switch.mock_switch"
assert entity.proportional_algorithm is not None
assert entity.proportional_algorithm._tpi_coef_int == 0.3
assert entity.proportional_algorithm._tpi_coef_ext == 0.01
assert entity.proportional_algorithm._minimal_activation_delay == 30
assert entity._security_delay_min == 5
assert entity._security_min_on_percent == 0.3
assert entity._security_default_on_percent == 0.1
assert entity.is_inversed
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_full_over_switch_wo_central_config(
hass: HomeAssistant, skip_hass_states_is_state, init_vtherm_api
):
"""Tests that a VTherm without any central_configuration is working with its own attributes"""
# Add a Switch VTherm
entry = MockConfigEntry(
domain=DOMAIN,
title="TheOverSwitchMockName",
unique_id="uniqueId",
data={
CONF_NAME: "TheOverSwitchMockName",
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_SWITCH,
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
CONF_CYCLE_MIN: 5,
CONF_TEMP_MIN: 8,
CONF_TEMP_MAX: 18,
"frost_temp": 10,
"eco_temp": 17,
"comfort_temp": 18,
"boost_temp": 21,
"frost_away_temp": 13,
"eco_away_temp": 13,
"comfort_away_temp": 13,
"boost_away_temp": 13,
CONF_USE_WINDOW_FEATURE: True,
CONF_USE_MOTION_FEATURE: True,
CONF_USE_POWER_FEATURE: True,
CONF_USE_PRESENCE_FEATURE: True,
CONF_HEATER: "switch.mock_switch",
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
CONF_INVERSE_SWITCH: False,
CONF_TPI_COEF_INT: 0.3,
CONF_TPI_COEF_EXT: 0.01,
CONF_MINIMAL_ACTIVATION_DELAY: 30,
CONF_SECURITY_DELAY_MIN: 5,
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
CONF_SECURITY_DEFAULT_ON_PERCENT: 0.1,
CONF_WINDOW_SENSOR: "binary_sensor.mock_window_sensor",
CONF_WINDOW_DELAY: 30,
CONF_WINDOW_AUTO_OPEN_THRESHOLD: 3,
CONF_WINDOW_AUTO_CLOSE_THRESHOLD: 0.1,
CONF_WINDOW_AUTO_MAX_DURATION: 5,
CONF_MOTION_DELAY: 10,
CONF_MOTION_OFF_DELAY: 29,
CONF_MOTION_PRESET: "comfort",
CONF_NO_MOTION_PRESET: "eco",
CONF_MOTION_SENSOR: "binary_sensor.mock_motion_sensor",
CONF_POWER_SENSOR: "sensor.mock_power_sensor",
CONF_MAX_POWER_SENSOR: "sensor.mock_max_power_sensor",
CONF_PRESENCE_SENSOR: "binary_sensor.mock_presence_sensor",
CONF_USE_MAIN_CENTRAL_CONFIG: False,
CONF_USE_TPI_CENTRAL_CONFIG: False,
CONF_USE_WINDOW_CENTRAL_CONFIG: False,
CONF_USE_MOTION_CENTRAL_CONFIG: False,
CONF_USE_POWER_CENTRAL_CONFIG: False,
CONF_USE_PRESENCE_CENTRAL_CONFIG: False,
CONF_USE_PRESETS_CENTRAL_CONFIG: False,
CONF_USE_ADVANCED_CENTRAL_CONFIG: False,
},
)
with patch("homeassistant.core.ServiceRegistry.async_call"):
entity: ThermostatOverSwitch = await create_thermostat(
hass, entry, "climate.theoverswitchmockname"
)
assert entity
assert entity.name == "TheOverSwitchMockName"
assert entity.is_over_switch
assert entity._temp_sensor_entity_id == "sensor.mock_temp_sensor"
assert entity._ext_temp_sensor_entity_id == "sensor.mock_ext_temp_sensor"
assert entity._cycle_min == 5
assert entity._attr_min_temp == 8
assert entity._attr_max_temp == 18
assert entity.preset_modes == [
"none",
"frost",
"eco",
"comfort",
"boost",
"activity",
]
assert entity.nb_underlying_entities == 1
assert entity.underlying_entity_id(0) == "switch.mock_switch"
assert entity.proportional_algorithm is not None
assert entity.proportional_algorithm._tpi_coef_int == 0.3
assert entity.proportional_algorithm._tpi_coef_ext == 0.01
assert entity.proportional_algorithm._minimal_activation_delay == 30
assert entity._security_delay_min == 5
assert entity._security_min_on_percent == 0.3
assert entity._security_default_on_percent == 0.1
assert entity.is_inversed is False
assert entity.is_window_auto_enabled is False # we have an entity_id
assert entity._window_sensor_entity_id == "binary_sensor.mock_window_sensor"
assert entity._window_delay_sec == 30
assert entity._window_auto_close_threshold == 0.1
assert entity._window_auto_open_threshold == 3
assert entity._window_auto_max_duration == 5
assert entity._motion_sensor_entity_id == "binary_sensor.mock_motion_sensor"
assert entity._motion_delay_sec == 10
assert entity._motion_off_delay_sec == 29
assert entity._motion_preset == "comfort"
assert entity._no_motion_preset == "eco"
assert entity._power_sensor_entity_id == "sensor.mock_power_sensor"
assert entity._max_power_sensor_entity_id == "sensor.mock_max_power_sensor"
assert entity._presence_sensor_entity_id == "binary_sensor.mock_presence_sensor"
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_full_over_switch_with_central_config(
hass: HomeAssistant, skip_hass_states_is_state, init_central_config
):
"""Tests that a VTherm with central_configuration is working with the central_config attributes"""
# Add a Switch VTherm
entry = MockConfigEntry(
domain=DOMAIN,
title="TheOverSwitchMockName",
unique_id="uniqueId",
data={
CONF_NAME: "TheOverSwitchMockName",
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_SWITCH,
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
CONF_CYCLE_MIN: 5,
CONF_TEMP_MIN: 8,
CONF_TEMP_MAX: 18,
"frost_temp": 10,
"eco_temp": 17,
"comfort_temp": 18,
"boost_temp": 21,
CONF_USE_WINDOW_FEATURE: True,
CONF_USE_MOTION_FEATURE: True,
CONF_USE_POWER_FEATURE: True,
CONF_USE_PRESENCE_FEATURE: True,
CONF_HEATER: "switch.mock_switch",
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
CONF_INVERSE_SWITCH: False,
CONF_TPI_COEF_INT: 0.3,
CONF_TPI_COEF_EXT: 0.01,
CONF_MINIMAL_ACTIVATION_DELAY: 30,
CONF_SECURITY_DELAY_MIN: 5,
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
CONF_SECURITY_DEFAULT_ON_PERCENT: 0.1,
CONF_WINDOW_SENSOR: "binary_sensor.mock_window_sensor",
CONF_WINDOW_DELAY: 30,
CONF_WINDOW_AUTO_OPEN_THRESHOLD: 3,
CONF_WINDOW_AUTO_CLOSE_THRESHOLD: 0.1,
CONF_WINDOW_AUTO_MAX_DURATION: 5,
CONF_MOTION_DELAY: 10,
CONF_MOTION_OFF_DELAY: 29,
CONF_MOTION_PRESET: "comfort",
CONF_NO_MOTION_PRESET: "eco",
CONF_MOTION_SENSOR: "binary_sensor.mock_motion_sensor",
CONF_POWER_SENSOR: "sensor.mock_power_sensor",
CONF_MAX_POWER_SENSOR: "sensor.mock_max_power_sensor",
CONF_PRESENCE_SENSOR: "binary_sensor.mock_presence_sensor",
CONF_USE_MAIN_CENTRAL_CONFIG: True,
CONF_USE_TPI_CENTRAL_CONFIG: True,
CONF_USE_WINDOW_CENTRAL_CONFIG: True,
CONF_USE_MOTION_CENTRAL_CONFIG: True,
CONF_USE_POWER_CENTRAL_CONFIG: True,
CONF_USE_PRESENCE_CENTRAL_CONFIG: True,
CONF_USE_PRESETS_CENTRAL_CONFIG: True,
CONF_USE_ADVANCED_CENTRAL_CONFIG: True,
},
)
with patch("homeassistant.core.ServiceRegistry.async_call"):
entity: ThermostatOverSwitch = await create_thermostat(
hass, entry, "climate.theoverswitchmockname"
)
assert entity
assert entity.name == "TheOverSwitchMockName"
assert entity.is_over_switch
assert entity._temp_sensor_entity_id == "sensor.mock_temp_sensor"
assert entity._ext_temp_sensor_entity_id == "sensor.mock_ext_temp_sensor"
assert entity._cycle_min == 5
assert entity._attr_min_temp == 15
assert entity._attr_max_temp == 30
assert entity.preset_modes == [
"none",
"frost",
"eco",
"boost",
"activity",
]
assert entity.nb_underlying_entities == 1
assert entity.underlying_entity_id(0) == "switch.mock_switch"
assert entity.proportional_algorithm is not None
assert entity.proportional_algorithm._tpi_coef_int == 0.5
assert entity.proportional_algorithm._tpi_coef_ext == 0.02
assert entity.proportional_algorithm._minimal_activation_delay == 11
assert entity._security_delay_min == 61
assert entity._security_min_on_percent == 0.5
assert entity._security_default_on_percent == 0.2
assert entity.is_inversed is False
# We have an entity so window auto is not enabled
assert entity.is_window_auto_enabled is False
assert entity._window_sensor_entity_id == "binary_sensor.mock_window_sensor"
assert entity._window_delay_sec == 15
assert entity._window_auto_close_threshold == 1
assert entity._window_auto_open_threshold == 4
assert entity._window_auto_max_duration == 31
assert entity._motion_sensor_entity_id == "binary_sensor.mock_motion_sensor"
assert entity._motion_delay_sec == 31
assert entity._motion_off_delay_sec == 301
assert entity._motion_preset == "boost"
assert entity._no_motion_preset == "frost"
assert entity._power_sensor_entity_id == "sensor.mock_power_sensor"
assert entity._max_power_sensor_entity_id == "sensor.mock_max_power_sensor"
assert entity._presence_sensor_entity_id == "binary_sensor.mock_presence_sensor"
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_over_switch_with_central_config_but_no_central_config(
hass: HomeAssistant, skip_hass_states_get, init_vtherm_api
):
"""Tests that a VTherm with a central_configuration flag but no central config. Should lead to an error"""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == SOURCE_USER
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
user_input={
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_SWITCH,
},
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "main"
assert result["errors"] == {}
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
user_input={
CONF_NAME: "TheOverSwitchMockName",
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
CONF_CYCLE_MIN: 5,
CONF_DEVICE_POWER: 1,
CONF_USE_WINDOW_FEATURE: True,
CONF_USE_MOTION_FEATURE: False,
CONF_USE_POWER_FEATURE: False,
CONF_USE_PRESENCE_FEATURE: False,
CONF_USE_MAIN_CENTRAL_CONFIG: True,
},
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
# in case of error we stays in main
assert result["step_id"] == "main"
assert result["errors"] == {"use_main_central_config": "no_central_config"}

1040
tests/test_central_mode.py Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,3 +1,4 @@
# pylint: disable=unused-argument, line-too-long
""" Test the Versatile Thermostat config flow """
from homeassistant import data_entry_flow
@@ -12,13 +13,8 @@ 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:
async def test_show_form(hass: HomeAssistant, init_vtherm_api) -> None:
"""Test that the form is served with no input"""
# Init the API
# hass.data["custom_components"] = None
# loader.async_get_custom_components(hass)
# VersatileThermostatAPI(hass)
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}
)
@@ -29,8 +25,12 @@ async def test_show_form(hass: HomeAssistant) -> None:
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_user_config_flow_over_switch(hass: HomeAssistant, skip_hass_states_get):
"""Test the config flow with all thermostat_over_switch features"""
# 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, init_central_config
): # pylint: disable=unused-argument
"""Test the config flow with all thermostat_over_switch features and central config on"""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}
)
@@ -42,6 +42,14 @@ async def test_user_config_flow_over_switch(hass: HomeAssistant, skip_hass_state
result["flow_id"], user_input=MOCK_TH_OVER_SWITCH_USER_CONFIG
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "main"
assert result["errors"] == {}
result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input=MOCK_TH_OVER_SWITCH_MAIN_CONFIG
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "type"
assert result["errors"] == {}
@@ -55,7 +63,7 @@ async def test_user_config_flow_over_switch(hass: HomeAssistant, skip_hass_state
assert result["errors"] == {}
result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input=MOCK_TH_OVER_SWITCH_TPI_CONFIG
result["flow_id"], user_input={CONF_USE_TPI_CENTRAL_CONFIG: True}
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
@@ -63,7 +71,7 @@ async def test_user_config_flow_over_switch(hass: HomeAssistant, skip_hass_state
assert result["errors"] == {}
result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input=MOCK_PRESETS_CONFIG
result["flow_id"], user_input={CONF_USE_PRESETS_CENTRAL_CONFIG: True}
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
@@ -71,7 +79,11 @@ async def test_user_config_flow_over_switch(hass: HomeAssistant, skip_hass_state
assert result["errors"] == {}
result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input=MOCK_WINDOW_CONFIG
result["flow_id"],
user_input={
CONF_WINDOW_SENSOR: "binary_sensor.window_sensor",
CONF_USE_WINDOW_CENTRAL_CONFIG: True,
},
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
@@ -79,7 +91,11 @@ async def test_user_config_flow_over_switch(hass: HomeAssistant, skip_hass_state
assert result["errors"] == {}
result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input=MOCK_MOTION_CONFIG
result["flow_id"],
user_input={
CONF_MOTION_SENSOR: "input_boolean.motion_sensor",
CONF_USE_MOTION_CENTRAL_CONFIG: True,
},
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
@@ -87,7 +103,7 @@ async def test_user_config_flow_over_switch(hass: HomeAssistant, skip_hass_state
assert result["errors"] == {}
result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input=MOCK_POWER_CONFIG
result["flow_id"], user_input={CONF_USE_POWER_CENTRAL_CONFIG: True}
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
@@ -95,7 +111,11 @@ async def test_user_config_flow_over_switch(hass: HomeAssistant, skip_hass_state
assert result["errors"] == {}
result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input=MOCK_PRESENCE_CONFIG
result["flow_id"],
user_input={
CONF_PRESENCE_SENSOR: "person.presence_sensor",
CONF_USE_PRESENCE_CENTRAL_CONFIG: True,
},
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
@@ -103,21 +123,29 @@ async def test_user_config_flow_over_switch(hass: HomeAssistant, skip_hass_state
assert result["errors"] == {}
result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input=MOCK_ADVANCED_CONFIG
result["flow_id"], user_input={CONF_USE_ADVANCED_CENTRAL_CONFIG: True}
)
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
assert (
result["data"]
== MOCK_TH_OVER_SWITCH_USER_CONFIG
assert result["data"] == (
MOCK_TH_OVER_SWITCH_USER_CONFIG
| MOCK_TH_OVER_SWITCH_MAIN_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
| {CONF_WINDOW_SENSOR: "binary_sensor.window_sensor"}
| {CONF_MOTION_SENSOR: "input_boolean.motion_sensor"}
| {CONF_PRESENCE_SENSOR: "person.presence_sensor"}
| {
CONF_USE_MAIN_CENTRAL_CONFIG: True,
CONF_USE_TPI_CENTRAL_CONFIG: True,
CONF_USE_PRESETS_CENTRAL_CONFIG: True,
CONF_USE_WINDOW_CENTRAL_CONFIG: True,
CONF_USE_MOTION_CENTRAL_CONFIG: True,
CONF_USE_POWER_CENTRAL_CONFIG: True,
CONF_USE_PRESENCE_CENTRAL_CONFIG: True,
CONF_USE_ADVANCED_CENTRAL_CONFIG: True,
CONF_USE_CENTRAL_MODE: True,
CONF_USED_BY_CENTRAL_BOILER: False,
}
)
assert result["result"]
assert result["result"].domain == DOMAIN
@@ -128,8 +156,12 @@ async def test_user_config_flow_over_switch(hass: HomeAssistant, skip_hass_state
@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):
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"""
await create_central_config(hass)
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}
)
@@ -141,6 +173,22 @@ async def test_user_config_flow_over_climate(hass: HomeAssistant, skip_hass_stat
result["flow_id"], user_input=MOCK_TH_OVER_CLIMATE_USER_CONFIG
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "main"
assert result["errors"] == {}
result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input=MOCK_TH_OVER_CLIMATE_MAIN_CONFIG
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "main"
assert result["errors"] == {}
result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input=MOCK_TH_OVER_CLIMATE_CENTRAL_MAIN_CONFIG
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "type"
assert result["errors"] == {}
@@ -153,41 +201,25 @@ async def test_user_config_flow_over_climate(hass: HomeAssistant, skip_hass_stat
assert result["step_id"] == "presets"
assert result["errors"] == {}
result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input={CONF_USE_PRESETS_CENTRAL_CONFIG: False}
)
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"] == {}
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_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
# )
result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input={CONF_USE_ADVANCED_CENTRAL_CONFIG: False}
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "advanced"
@@ -198,14 +230,19 @@ async def test_user_config_flow_over_climate(hass: HomeAssistant, skip_hass_stat
)
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[
"data"
] == MOCK_TH_OVER_CLIMATE_USER_CONFIG | MOCK_TH_OVER_CLIMATE_MAIN_CONFIG | MOCK_TH_OVER_CLIMATE_CENTRAL_MAIN_CONFIG | MOCK_TH_OVER_CLIMATE_TYPE_CONFIG | MOCK_PRESETS_CONFIG | MOCK_ADVANCED_CONFIG | MOCK_DEFAULT_FEATURE_CONFIG | {
CONF_USE_MAIN_CENTRAL_CONFIG: False,
CONF_USE_TPI_CENTRAL_CONFIG: False,
CONF_USE_PRESETS_CENTRAL_CONFIG: False,
CONF_USE_WINDOW_CENTRAL_CONFIG: False,
CONF_USE_MOTION_CENTRAL_CONFIG: False,
CONF_USE_POWER_CENTRAL_CONFIG: False,
CONF_USE_PRESENCE_CENTRAL_CONFIG: False,
CONF_USE_ADVANCED_CENTRAL_CONFIG: False,
CONF_USED_BY_CENTRAL_BOILER: False,
}
assert result["result"]
assert result["result"].domain == DOMAIN
assert result["result"].version == 1
@@ -216,9 +253,13 @@ async def test_user_config_flow_over_climate(hass: HomeAssistant, skip_hass_stat
@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
hass: HomeAssistant,
skip_hass_states_get,
skip_control_heating, # pylint: disable=unused-argument
):
"""Test the config flow with only window auto feature"""
await create_central_config(hass)
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}
)
@@ -229,18 +270,27 @@ async def test_user_config_flow_window_auto_ok(
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
user_input={
CONF_NAME: "TheOverSwitchMockName",
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_SWITCH,
},
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "main"
assert result["errors"] == {}
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
user_input={
CONF_NAME: "TheOverSwitchMockName",
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
CONF_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,
CONF_USE_MAIN_CENTRAL_CONFIG: True,
CONF_USED_BY_CENTRAL_BOILER: True,
},
)
@@ -256,6 +306,14 @@ async def test_user_config_flow_window_auto_ok(
assert result["step_id"] == "tpi"
assert result["errors"] == {}
result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input={CONF_USE_TPI_CENTRAL_CONFIG: False}
)
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
)
@@ -265,7 +323,16 @@ async def test_user_config_flow_window_auto_ok(
assert result["errors"] == {}
result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input=MOCK_PRESETS_CONFIG
result["flow_id"], user_input={CONF_USE_PRESETS_CENTRAL_CONFIG: True}
)
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={CONF_USE_WINDOW_CENTRAL_CONFIG: False},
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
@@ -282,27 +349,36 @@ async def test_user_config_flow_window_auto_ok(
assert result["errors"] == {}
result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input=MOCK_ADVANCED_CONFIG
result["flow_id"], user_input={CONF_USE_ADVANCED_CENTRAL_CONFIG: True}
)
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["data"] == {
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_SWITCH,
CONF_NAME: "TheOverSwitchMockName",
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
CONF_CYCLE_MIN: 5,
CONF_DEVICE_POWER: 1,
CONF_USE_WINDOW_FEATURE: True,
CONF_USE_MOTION_FEATURE: False,
CONF_USE_POWER_FEATURE: False,
CONF_USE_PRESENCE_FEATURE: False,
CONF_USE_MOTION_FEATURE: False,
CONF_USE_POWER_FEATURE: False,
CONF_USE_PRESENCE_FEATURE: False,
CONF_WINDOW_DELAY: 30, # the default value is added
CONF_USE_CENTRAL_MODE: True, # True is the defaulf value
} | MOCK_TH_OVER_SWITCH_TYPE_CONFIG | MOCK_TH_OVER_SWITCH_TPI_CONFIG | MOCK_WINDOW_AUTO_CONFIG | {
CONF_USE_MAIN_CENTRAL_CONFIG: True,
CONF_USE_TPI_CENTRAL_CONFIG: False,
CONF_USE_PRESETS_CENTRAL_CONFIG: True,
CONF_USE_WINDOW_CENTRAL_CONFIG: False,
CONF_USE_MOTION_CENTRAL_CONFIG: False,
CONF_USE_POWER_CENTRAL_CONFIG: False,
CONF_USE_PRESENCE_CENTRAL_CONFIG: False,
CONF_USE_ADVANCED_CENTRAL_CONFIG: True,
CONF_USED_BY_CENTRAL_BOILER: True,
}
assert result["result"]
assert result["result"].domain == DOMAIN
assert result["result"].version == 1
@@ -313,9 +389,12 @@ async def test_user_config_flow_window_auto_ok(
@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
hass: HomeAssistant, skip_hass_states_get # pylint: disable=unused-argument
):
"""Test the config flow with window auto and window features -> not allowed"""
await create_central_config(hass)
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}
)
@@ -326,18 +405,26 @@ async def test_user_config_flow_window_auto_ko(
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
user_input={
CONF_NAME: "TheOverSwitchMockName",
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_SWITCH,
},
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "main"
assert result["errors"] == {}
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
user_input={
CONF_NAME: "TheOverSwitchMockName",
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
CONF_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,
CONF_USE_MAIN_CENTRAL_CONFIG: True,
},
)
@@ -353,6 +440,14 @@ async def test_user_config_flow_window_auto_ko(
assert result["step_id"] == "tpi"
assert result["errors"] == {}
result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input={CONF_USE_TPI_CENTRAL_CONFIG: False}
)
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
)
@@ -362,7 +457,7 @@ async def test_user_config_flow_window_auto_ko(
assert result["errors"] == {}
result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input=MOCK_PRESETS_CONFIG
result["flow_id"], user_input={CONF_USE_PRESETS_CENTRAL_CONFIG: True}
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
@@ -371,37 +466,58 @@ async def test_user_config_flow_window_auto_ko(
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
user_input=MOCK_WINDOW_AUTO_CONFIG | MOCK_WINDOW_CONFIG,
user_input={
CONF_WINDOW_SENSOR: "binary_sensor.window_sensor",
CONF_USE_WINDOW_CENTRAL_CONFIG: False,
},
)
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"
}
assert result["errors"] == {}
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
user_input=MOCK_WINDOW_DELAY_CONFIG,
)
# Since issue #280 we cannot have the error because we only display the
# MOCK_WINDOW_DELAY_CONFIG form if we have a sensor configured
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
# We should stay on window with an error
assert result["errors"] == {}
# "window_sensor_entity_id": "window_open_detection_method"
# }
assert result["step_id"] == "advanced"
@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
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",
await create_central_config(hass)
SOURCE_CONFIG = {
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_SWITCH,
}
MAIN_CONFIG = { # pylint: disable=wildcard-import, invalid-name
CONF_NAME: "TheOver4SwitchMockName",
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,
CONF_USE_MAIN_CENTRAL_CONFIG: True,
CONF_USE_CENTRAL_MODE: False,
CONF_USED_BY_CENTRAL_BOILER: False,
}
TYPE_CONFIG = { # pylint: disable=wildcard-import, invalid-name
@@ -410,6 +526,8 @@ async def test_user_config_flow_over_4_switches(
CONF_HEATER_3: "switch.mock_switch3",
CONF_HEATER_4: "switch.mock_switch4",
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
CONF_AC_MODE: False,
CONF_INVERSE_SWITCH: False,
}
result = await hass.config_entries.flow.async_init(
@@ -424,6 +542,15 @@ async def test_user_config_flow_over_4_switches(
user_input=SOURCE_CONFIG,
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "main"
assert result["errors"] == {}
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
user_input=MAIN_CONFIG,
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "type"
assert result["errors"] == {}
@@ -438,7 +565,7 @@ async def test_user_config_flow_over_4_switches(
assert result["errors"] == {}
result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input=MOCK_TH_OVER_SWITCH_TPI_CONFIG
result["flow_id"], user_input={CONF_USE_TPI_CENTRAL_CONFIG: True}
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
@@ -446,7 +573,7 @@ async def test_user_config_flow_over_4_switches(
assert result["errors"] == {}
result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input=MOCK_PRESETS_CONFIG
result["flow_id"], user_input={CONF_USE_PRESETS_CENTRAL_CONFIG: True}
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
@@ -454,17 +581,24 @@ async def test_user_config_flow_over_4_switches(
assert result["errors"] == {}
result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input=MOCK_ADVANCED_CONFIG
result["flow_id"], user_input={CONF_USE_ADVANCED_CENTRAL_CONFIG: True}
)
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
assert (
result["data"]
== SOURCE_CONFIG
assert result["data"] == (
SOURCE_CONFIG
| MAIN_CONFIG
| TYPE_CONFIG
| MOCK_TH_OVER_SWITCH_TPI_CONFIG
| MOCK_PRESETS_CONFIG
| MOCK_ADVANCED_CONFIG
| {
CONF_USE_MAIN_CENTRAL_CONFIG: True,
CONF_USE_TPI_CENTRAL_CONFIG: True,
CONF_USE_PRESETS_CENTRAL_CONFIG: True,
CONF_USE_WINDOW_CENTRAL_CONFIG: False,
CONF_USE_MOTION_CENTRAL_CONFIG: False,
CONF_USE_POWER_CENTRAL_CONFIG: False,
CONF_USE_PRESENCE_CENTRAL_CONFIG: False,
CONF_USE_ADVANCED_CENTRAL_CONFIG: True,
}
)
assert result["result"]
assert result["result"].domain == DOMAIN

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,134 @@
# 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()

View File

@@ -1,10 +1,12 @@
""" Test the Window management """
import asyncio
from unittest.mock import patch, call, PropertyMock
from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import
from datetime import datetime, timedelta
# 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)
@@ -43,17 +45,18 @@ async def test_movement_management_time_not_enough(
CONF_TPI_COEF_INT: 0.3,
CONF_TPI_COEF_EXT: 0.01,
CONF_MINIMAL_ACTIVATION_DELAY: 30,
CONF_SECURITY_DELAY_MIN: 5,
CONF_SECURITY_DELAY_MIN: 10,
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_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: VersatileThermostat = await create_thermostat(
entity: BaseThermostat = await create_thermostat(
hass, entry, "climate.theoverswitchmockname"
)
assert entity
@@ -61,9 +64,9 @@ async def test_movement_management_time_not_enough(
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
# 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.climate.VersatileThermostat._async_control_heating"
"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)
@@ -75,33 +78,37 @@ async def test_movement_management_time_not_enough(
assert entity.motion_state is None
assert entity.presence_state is None
event_timestamp = now - timedelta(minutes=4)
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
# starts detecting motion with time not enough
with patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
"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,
return_value=False,
), patch(
"homeassistant.helpers.condition.state", return_value=False
):
event_timestamp = now - timedelta(minutes=3)
await send_motion_change_event(entity, True, False, event_timestamp)
) 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"
@@ -111,9 +118,67 @@ async def test_movement_management_time_not_enough(
assert mock_heater_off.call_count == 0
assert mock_send_event.call_count == 0
# stop detecting motion with confirmation of stop
# starts detecting motion with time enough this time
with patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
"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(
@@ -124,8 +189,11 @@ async def test_movement_management_time_not_enough(
) as mock_device_active, patch(
"homeassistant.helpers.condition.state", return_value=True
) as mock_condition:
event_timestamp = now - timedelta(minutes=2)
await send_motion_change_event(entity, False, True, event_timestamp)
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
@@ -135,9 +203,8 @@ async def test_movement_management_time_not_enough(
assert entity.presence_state is "on"
assert mock_send_event.call_count == 0
# Change is not confirmed
# The heater must stop heating now
assert mock_heater_on.call_count == 0
# Because device is active
assert mock_heater_off.call_count == 1
assert mock_send_event.call_count == 0
@@ -186,7 +253,7 @@ async def test_movement_management_time_enough_and_presence(
},
)
entity: VersatileThermostat = await create_thermostat(
entity: BaseThermostat = await create_thermostat(
hass, entry, "climate.theoverswitchmockname"
)
assert entity
@@ -196,7 +263,7 @@ async def test_movement_management_time_enough_and_presence(
# start heating, in boost mode. We block the control_heating to avoid running a cycle
with patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat._async_control_heating"
"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)
@@ -217,7 +284,7 @@ async def test_movement_management_time_enough_and_presence(
# starts detecting motion
with patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
"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(
@@ -246,7 +313,7 @@ async def test_movement_management_time_enough_and_presence(
# stop detecting motion with confirmation of stop
with patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
"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(
@@ -318,7 +385,7 @@ async def test_movement_management_time_enoughand_not_presence(
},
)
entity: VersatileThermostat = await create_thermostat(
entity: BaseThermostat = await create_thermostat(
hass, entry, "climate.theoverswitchmockname"
)
assert entity
@@ -328,7 +395,7 @@ async def test_movement_management_time_enoughand_not_presence(
# start heating, in boost mode. We block the control_heating to avoid running a cycle
with patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat._async_control_heating"
"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)
@@ -349,7 +416,7 @@ async def test_movement_management_time_enoughand_not_presence(
# starts detecting motion
with patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
"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(
@@ -378,7 +445,7 @@ async def test_movement_management_time_enoughand_not_presence(
# stop detecting motion with confirmation of stop
with patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
"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(
@@ -405,3 +472,130 @@ async def test_movement_management_time_enoughand_not_presence(
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

@@ -1,11 +1,14 @@
# 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 .commons import * # pylint: disable=wildcard-import, unused-wildcard-import
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)
@@ -15,7 +18,7 @@ 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
@@ -50,7 +53,7 @@ async def test_one_switch_cycle(
},
)
entity: VersatileThermostat = await create_thermostat(
entity: BaseThermostat = await create_thermostat(
hass, entry, "climate.theover4switchmockname"
)
assert entity
@@ -58,7 +61,7 @@ async def test_one_switch_cycle(
# start heating, in boost mode. We block the control_heating to avoid running a cycle
with patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat._async_control_heating"
"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)
@@ -66,7 +69,7 @@ async def test_one_switch_cycle(
assert entity.hvac_mode is HVACMode.HEAT
assert entity.preset_mode is PRESET_BOOST
assert entity.target_temperature == 19
assert entity.window_state is None
assert entity.window_state is STATE_OFF
event_timestamp = now - timedelta(minutes=4)
await send_temperature_change_event(entity, 15, event_timestamp)
@@ -75,14 +78,14 @@ async def test_one_switch_cycle(
with patch(
"homeassistant.core.StateMachine.is_state", return_value=False
) as mock_is_state:
assert entity._is_device_active is False
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.climate.VersatileThermostat.send_event"
"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(
@@ -117,7 +120,7 @@ async def test_one_switch_cycle(
# Set a temperature at middle level
event_timestamp = now - timedelta(minutes=4)
with patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
"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(
@@ -132,13 +135,14 @@ async def test_one_switch_cycle(
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
# The first heater should be turned on but is already on but because above we mock
# call_later the heater is not on. But this time it will be really on
assert mock_heater_on.call_count == 1
# Set another temperature at middle level
event_timestamp = now - timedelta(minutes=3)
with patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
"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(
@@ -153,22 +157,27 @@ async def test_one_switch_cycle(
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
# The heater is already on cycle. So we wait that the cycle ends and no heater action
# is done
assert mock_heater_on.call_count == 0
# assert entity.underlying_entity(0)._should_relaunch_control_heating is True
# Simulate the relaunch
await entity.underlying_entity(0)._turn_on_later(None)
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
# TODO normal ? assert entity.underlying_entity(0)._should_relaunch_control_heating is False
# normal ? assert entity.underlying_entity(0)._should_relaunch_control_heating is False
# Simulate the end of heater on cycle
event_timestamp = now - timedelta(minutes=3)
with patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
"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(
@@ -177,7 +186,11 @@ async def test_one_switch_cycle(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
return_value=True,
) as mock_device_active:
await entity.underlying_entity(0)._turn_off_later(None)
await entity.underlying_entity(
0
)._turn_off_later( # pylint: disable=protected-access
None
)
# No special event
assert mock_send_event.call_count == 0
@@ -189,7 +202,7 @@ async def test_one_switch_cycle(
# Simulate the start of heater on cycle
event_timestamp = now - timedelta(minutes=3)
with patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
"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(
@@ -198,7 +211,11 @@ async def test_one_switch_cycle(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
return_value=True,
) as mock_device_active:
await entity.underlying_entity(0)._turn_on_later(None)
await entity.underlying_entity(
0
)._turn_on_later( # pylint: disable=protected-access
None
)
# No special event
assert mock_send_event.call_count == 0
@@ -214,7 +231,7 @@ 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
@@ -252,7 +269,7 @@ async def test_multiple_switchs(
},
)
entity: VersatileThermostat = await create_thermostat(
entity: BaseThermostat = await create_thermostat(
hass, entry, "climate.theover4switchmockname"
)
assert entity
@@ -261,7 +278,7 @@ async def test_multiple_switchs(
# start heating, in boost mode. We block the control_heating to avoid running a cycle
with patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat._async_control_heating"
"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:
@@ -271,13 +288,13 @@ async def test_multiple_switchs(
assert entity.hvac_mode is HVACMode.HEAT
assert entity.preset_mode is PRESET_BOOST
assert entity.target_temperature == 19
assert entity.window_state is None
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
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
@@ -289,7 +306,7 @@ async def test_multiple_switchs(
# Set temperature to a low level
with patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
"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(
@@ -327,7 +344,7 @@ async def test_multiple_switchs(
# Set a temperature at middle level
event_timestamp = now - timedelta(minutes=4)
with patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
"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(
@@ -342,17 +359,20 @@ async def test_multiple_switchs(
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
# The first heater should be turned on but is already on but because call_later
# is mocked, it is only turned on here
assert mock_heater_on.call_count == 1
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_multiple_climates(
hass: HomeAssistant,
skip_hass_states_is_state,
skip_send_event,
):
"""Test that when multiple climates are configured the activation and deactivation is propagated to all climates"""
): # 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)
@@ -366,6 +386,7 @@ async def test_multiple_climates(
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_CLIMATE,
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
CONF_CYCLE_MIN: 8,
CONF_TEMP_MIN: 15,
CONF_TEMP_MAX: 30,
"eco_temp": 17,
@@ -385,7 +406,7 @@ async def test_multiple_climates(
},
)
entity: VersatileThermostat = await create_thermostat(
entity: BaseThermostat = await create_thermostat(
hass, entry, "climate.theover4climatemockname"
)
assert entity
@@ -394,7 +415,7 @@ async def test_multiple_climates(
# start heating, in boost mode. We block the control_heating to avoid running a cycle
with patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat._async_control_heating"
"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:
@@ -404,7 +425,7 @@ async def test_multiple_climates(
assert entity.hvac_mode is HVACMode.HEAT
assert entity.preset_mode is PRESET_BOOST
assert entity.target_temperature == 19
assert entity.window_state is None
assert entity.window_state is STATE_OFF
event_timestamp = now - timedelta(minutes=4)
await send_temperature_change_event(entity, 15, event_timestamp)
@@ -416,11 +437,11 @@ async def test_multiple_climates(
call.set_hvac_mode(HVACMode.HEAT),
]
)
assert entity._is_device_active is False
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.climate.VersatileThermostat._async_control_heating"
"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:
@@ -429,7 +450,7 @@ async def test_multiple_climates(
assert entity.hvac_mode is HVACMode.OFF
assert entity.preset_mode is PRESET_BOOST
assert entity.target_temperature == 19
assert entity.window_state is None
assert entity.window_state is STATE_OFF
event_timestamp = now - timedelta(minutes=4)
await send_temperature_change_event(entity, 15, event_timestamp)
@@ -441,7 +462,8 @@ async def test_multiple_climates(
call.set_hvac_mode(HVACMode.OFF),
]
)
assert entity._is_device_active is False
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])
@@ -449,8 +471,9 @@ async def test_multiple_climates_underlying_changes(
hass: HomeAssistant,
skip_hass_states_is_state,
skip_send_event,
):
"""Test that when multiple switch are configured the activation of one underlying climate activate the others"""
): # pylint: disable=unused-argument
"""Test that when multiple climate 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)
@@ -464,6 +487,7 @@ async def test_multiple_climates_underlying_changes(
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_CLIMATE,
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
CONF_CYCLE_MIN: 8,
CONF_TEMP_MIN: 15,
CONF_TEMP_MAX: 30,
"eco_temp": 17,
@@ -483,7 +507,7 @@ async def test_multiple_climates_underlying_changes(
},
)
entity: VersatileThermostat = await create_thermostat(
entity: BaseThermostat = await create_thermostat(
hass, entry, "climate.theover4climatemockname"
)
assert entity
@@ -492,7 +516,7 @@ async def test_multiple_climates_underlying_changes(
# start heating, in boost mode. We block the control_heating to avoid running a cycle
with patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat._async_control_heating"
"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:
@@ -502,7 +526,7 @@ async def test_multiple_climates_underlying_changes(
assert entity.hvac_mode is HVACMode.HEAT
assert entity.preset_mode is PRESET_BOOST
assert entity.target_temperature == 19
assert entity.window_state is None
assert entity.window_state is STATE_OFF
event_timestamp = now - timedelta(minutes=4)
await send_temperature_change_event(entity, 15, event_timestamp)
@@ -514,15 +538,29 @@ async def test_multiple_climates_underlying_changes(
call.set_hvac_mode(HVACMode.HEAT),
]
)
assert entity._is_device_active is False
assert entity.is_device_active is False # pylint: disable=protected-access
# Stop heating on one underlying climate
# All underlying supposed to be aligned with the hvac_mode now
with patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat._async_control_heating"
"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 send_climate_change_event(entity, HVACMode.OFF, HVACMode.HEAT, HVACAction.OFF, HVACAction.HEATING, now)
) as mock_underlying_set_hvac_mode, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.hvac_mode",
HVACMode.HEAT,
):
# Wait 11 sec so that the event will not be discarded
event_timestamp = now + timedelta(seconds=11)
await send_climate_change_event(
entity,
HVACMode.OFF,
HVACMode.HEAT,
HVACAction.OFF,
HVACAction.HEATING,
event_timestamp,
underlying_entity_id="switch.mock_climate3",
)
# Should be call for all Switch
assert mock_underlying_set_hvac_mode.call_count == 4
@@ -532,18 +570,32 @@ async def test_multiple_climates_underlying_changes(
]
)
assert entity.hvac_mode == HVACMode.OFF
assert entity._is_device_active is False
assert entity.is_device_active is False # pylint: disable=protected-access
# Start heating on one underlying climate
with patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat._async_control_heating"
"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
) as mock_underlying_get_hvac_action:
await send_climate_change_event(entity, HVACMode.HEAT, HVACMode.OFF, HVACAction.IDLE, HVACAction.OFF, now)
# 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,
), patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.hvac_mode",
HVACMode.OFF,
):
# Wait 11 sec so that the event will not be discarded
event_timestamp = now + timedelta(seconds=11)
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
@@ -554,5 +606,247 @@ async def test_multiple_climates_underlying_changes(
)
assert entity.hvac_mode == HVACMode.HEAT
assert entity.hvac_action == HVACAction.IDLE
assert entity._is_device_active is False
assert entity.is_device_active is False # pylint: disable=protected-access
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_multiple_climates_underlying_changes_not_aligned(
hass: HomeAssistant,
skip_hass_states_is_state,
skip_send_event,
): # pylint: disable=unused-argument
"""Test that when multiple climate are configured the activation of one underlying
climate don't activate the others if their havc_mode are not aligned"""
tz = get_tz(hass) # pylint: disable=invalid-name
now: datetime = datetime.now(tz=tz)
entry = MockConfigEntry(
domain=DOMAIN,
title="TheOver4ClimateMockName",
unique_id="uniqueId",
data={
CONF_NAME: "TheOver4ClimateMockName",
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_CLIMATE,
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
CONF_CYCLE_MIN: 8,
CONF_TEMP_MIN: 15,
CONF_TEMP_MAX: 30,
"eco_temp": 17,
"comfort_temp": 18,
"boost_temp": 19,
CONF_USE_WINDOW_FEATURE: False,
CONF_USE_MOTION_FEATURE: False,
CONF_USE_POWER_FEATURE: False,
CONF_USE_PRESENCE_FEATURE: False,
CONF_CLIMATE: "switch.mock_climate1",
CONF_CLIMATE_2: "switch.mock_climate2",
CONF_CLIMATE_3: "switch.mock_climate3",
CONF_CLIMATE_4: "switch.mock_climate4",
CONF_MINIMAL_ACTIVATION_DELAY: 30,
CONF_SECURITY_DELAY_MIN: 5,
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
},
)
entity: BaseThermostat = await create_thermostat(
hass, entry, "climate.theover4climatemockname"
)
assert entity
assert entity.is_over_climate is True
assert entity.nb_underlying_entities == 4
# start heating, in boost mode. We block the control_heating to avoid running a cycle
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.async_control_heating"
), patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.set_hvac_mode"
) as mock_underlying_set_hvac_mode:
await entity.async_set_hvac_mode(HVACMode.HEAT)
await entity.async_set_preset_mode(PRESET_BOOST)
assert entity.hvac_mode is HVACMode.HEAT
assert entity.preset_mode is PRESET_BOOST
assert entity.target_temperature == 19
assert entity.window_state is STATE_OFF
event_timestamp = now - timedelta(minutes=4)
await send_temperature_change_event(entity, 15, event_timestamp)
# Should be call for all Switch
assert mock_underlying_set_hvac_mode.call_count == 4
mock_underlying_set_hvac_mode.assert_has_calls(
[
call.set_hvac_mode(HVACMode.HEAT),
]
)
# Stop heating on one underlying climate
# All underlying supposed to be aligned with the hvac_mode now
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.async_control_heating"
), patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.set_hvac_mode"
) as mock_underlying_set_hvac_mode, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.hvac_mode",
HVACMode.COOL,
):
# Wait 11 sec so that the event will not be discarded
event_timestamp = now + timedelta(seconds=11)
await send_climate_change_event(
entity,
HVACMode.OFF,
HVACMode.HEAT,
HVACAction.OFF,
HVACAction.HEATING,
event_timestamp,
underlying_entity_id="switch.mock_climate3",
)
# Should be call for all Switch
assert mock_underlying_set_hvac_mode.call_count == 0
# mock_underlying_set_hvac_mode.assert_has_calls(
# [
# call.set_hvac_mode(HVACMode.OFF),
# ]
# )
# No change
assert entity.hvac_mode == HVACMode.HEAT
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_multiple_switch_power_management(
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

View File

@@ -1,12 +1,14 @@
# pylint: disable=protected-access, unused-argument, line-too-long
""" Test the Power management """
from unittest.mock import patch, call
from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import
from datetime import datetime, timedelta
from homeassistant.const import UnitOfTemperature
import logging
from custom_components.versatile_thermostat.thermostat_switch import (
ThermostatOverSwitch,
)
from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import
logging.getLogger().setLevel(logging.DEBUG)
@@ -50,7 +52,7 @@ async def test_power_management_hvac_off(
},
)
entity: VersatileThermostat = await create_thermostat(
entity: ThermostatOverSwitch = await create_thermostat(
hass, entry, "climate.theoverswitchmockname"
)
assert entity
@@ -81,7 +83,7 @@ async def test_power_management_hvac_off(
# Send power max mesurement too low but HVACMode is off
with patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
"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(
@@ -136,7 +138,7 @@ async def test_power_management_hvac_on(hass: HomeAssistant, skip_hass_states_is
},
)
entity: VersatileThermostat = await create_thermostat(
entity: ThermostatOverSwitch = await create_thermostat(
hass, entry, "climate.theoverswitchmockname"
)
assert entity
@@ -162,7 +164,7 @@ async def test_power_management_hvac_on(hass: HomeAssistant, skip_hass_states_is
# Send power max mesurement too low and HVACMode is on
with patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
"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(
@@ -186,6 +188,7 @@ async def test_power_management_hvac_on(hass: HomeAssistant, skip_hass_states_is
"current_power": 50,
"device_power": 100,
"current_power_max": 149,
"current_power_consumption": 100.0,
},
),
],
@@ -196,7 +199,7 @@ async def test_power_management_hvac_on(hass: HomeAssistant, skip_hass_states_is
# Send power mesurement low to unseet power preset
with patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
"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(
@@ -257,6 +260,7 @@ async def test_power_management_energy_over_switch(
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,
@@ -270,7 +274,7 @@ async def test_power_management_energy_over_switch(
},
)
entity: VersatileThermostat = await create_thermostat(
entity: ThermostatOverSwitch = await create_thermostat(
hass, entry, "climate.theoverswitchmockname"
)
assert entity
@@ -279,10 +283,11 @@ async def test_power_management_energy_over_switch(
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.climate.VersatileThermostat.send_event"
"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(
@@ -298,20 +303,20 @@ async def test_power_management_energy_over_switch(
assert entity.current_temperature == 15
assert tpi_algo.on_percent == 1
assert entity.mean_cycle_power == 100.0
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 == 100 * 5 / 60.0
assert entity.total_energy == round(100 * 5 / 60.0, 2)
entity.incremente_energy()
assert entity.total_energy == 2 * 100 * 5 / 60.0
assert entity.total_energy == round(2 * 100 * 5 / 60.0, 2)
# change temperature to a higher value
with patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
"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(
@@ -333,7 +338,7 @@ async def test_power_management_energy_over_switch(
# change temperature to a much higher value so that heater will be shut down
with patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
"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(
@@ -398,7 +403,7 @@ async def test_power_management_energy_over_climate(
},
)
entity: VersatileThermostat = await create_thermostat(
entity: ThermostatOverSwitch = await create_thermostat(
hass, entry, "climate.theoverclimatemockname"
)
assert entity

511
tests/test_security.py Normal file
View File

@@ -0,0 +1,511 @@
# 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,
"frost_temp": 7,
"eco_temp": 17,
"comfort_temp": 18,
"boost_temp": 19,
"use_window_feature": False,
"use_motion_feature": False,
"use_power_feature": False,
"use_presence_feature": False,
"heater_entity_id": "switch.mock_switch",
"proportional_function": "tpi",
"tpi_coef_int": 0.3,
"tpi_coef_ext": 0.01,
"minimal_activation_delay": 30,
"security_delay_min": 5, # 5 minutes
"security_min_on_percent": 0.2,
"security_default_on_percent": 0.1,
},
)
# 1. creates a thermostat and check that security is off
now: datetime = datetime.now(tz=tz)
entity: ThermostatOverSwitch = await create_thermostat(
hass, entry, "climate.theoverswitchmockname"
)
assert entity
assert entity._security_state is False
assert entity.preset_mode is not PRESET_SECURITY
assert entity.preset_modes == [
PRESET_NONE,
PRESET_FROST_PROTECTION,
PRESET_ECO,
PRESET_COMFORT,
PRESET_BOOST,
]
assert entity._last_ext_temperature_measure is not None
assert entity._last_temperature_measure is not None
assert (entity._last_temperature_measure.astimezone(tz) - now).total_seconds() < 1
assert (
entity._last_ext_temperature_measure.astimezone(tz) - now
).total_seconds() < 1
# set a preset
assert entity.preset_mode is PRESET_NONE
await entity.async_set_preset_mode(PRESET_COMFORT)
assert entity.preset_mode is PRESET_COMFORT
# Turn On the thermostat
assert entity.hvac_mode == HVACMode.OFF
await entity.async_set_hvac_mode(HVACMode.HEAT)
assert entity.hvac_mode == HVACMode.HEAT
# 2. activate security feature when date is expired
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on:
event_timestamp = now - timedelta(minutes=6)
# set temperature to 15 so that on_percent will be > security_min_on_percent (0.2)
await send_temperature_change_event(entity, 15, event_timestamp)
assert entity.security_state is True
assert entity.preset_mode == PRESET_SECURITY
assert 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_measure": event_timestamp.isoformat(),
"last_ext_temperature_measure": entity._last_ext_temperature_measure.isoformat(),
"current_temp": 15,
"current_ext_temp": None,
"target_temp": 18,
},
),
call.send_event(
EventType.SECURITY_EVENT,
{
"type": "start",
"last_temperature_measure": event_timestamp.isoformat(),
"last_ext_temperature_measure": entity._last_ext_temperature_measure.isoformat(),
"current_temp": 15,
"current_ext_temp": None,
"target_temp": 18,
},
),
],
any_order=True,
)
assert mock_heater_on.call_count == 1
# 3. Change the 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_measure": event_timestamp.astimezone(
tz
).isoformat(),
"last_ext_temperature_measure": entity._last_ext_temperature_measure.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_measure is not None
assert entity._last_temperature_measure is not None
assert (entity._last_temperature_measure.astimezone(tz) - now).total_seconds() < 1
assert (
entity._last_ext_temperature_measure.astimezone(tz) - now
).total_seconds() < 1
# set a preset
assert entity.preset_mode is PRESET_NONE
await entity.async_set_preset_mode(PRESET_BOOST)
assert entity.preset_mode is PRESET_BOOST
# Turn On the thermostat
assert entity.hvac_mode == HVACMode.OFF
await entity.async_set_hvac_mode(HVACMode.HEAT)
assert entity.hvac_mode == HVACMode.HEAT
# 2. activate on_percent
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on:
event_timestamp = now + timedelta(minutes=1)
entity._set_now(event_timestamp) # pylint: disable=protected-access
# set temperature to 17 so that on_percent will be > security_min_on_percent (0.2)
await send_temperature_change_event(entity, 17, event_timestamp)
assert entity._prop_algorithm.calculated_on_percent == 0.6
assert entity.preset_mode == PRESET_BOOST
assert entity.security_state is False
assert mock_send_event.call_count == 0
# 3. Set safety mode with a preset change
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on:
# 6 min between two mesure
event_timestamp = event_timestamp + timedelta(minutes=6)
entity._set_now(event_timestamp) # pylint: disable=protected-access
await send_temperature_change_event(entity, 17, event_timestamp)
assert entity._prop_algorithm.calculated_on_percent == 0.6
assert entity.security_state is True
assert entity.preset_mode == PRESET_SECURITY
assert entity._saved_preset_mode == PRESET_BOOST
assert mock_send_event.call_count == 3
mock_send_event.assert_has_calls(
[
call.send_event(EventType.PRESET_EVENT, {"preset": PRESET_SECURITY}),
call.send_event(
EventType.TEMPERATURE_EVENT,
{
"last_temperature_measure": event_timestamp.isoformat(),
"last_ext_temperature_measure": entity._last_ext_temperature_measure.isoformat(),
"current_temp": 17,
"current_ext_temp": None,
"target_temp": 19,
},
),
call.send_event(
EventType.SECURITY_EVENT,
{
"type": "start",
"last_temperature_measure": event_timestamp.isoformat(),
"last_ext_temperature_measure": entity._last_ext_temperature_measure.isoformat(),
"current_temp": 17,
"current_ext_temp": None,
"target_temp": 19,
},
),
],
any_order=True,
)
# heating have been started on the previous call
assert mock_heater_on.call_count == 0
# 4. change preset so that on_percent will be low
event_timestamp = event_timestamp + timedelta(minutes=1)
entity._set_now(event_timestamp) # pylint: disable=protected-access
await entity.async_set_preset_mode(PRESET_ECO)
assert entity.security_state is True
assert entity.preset_mode == PRESET_SECURITY
# 5. resolve the datetime issue
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on:
# +2 min between two mesure
event_timestamp = event_timestamp + timedelta(minutes=2)
entity._set_now(event_timestamp) # pylint: disable=protected-access
# set temperature to 18.9 so that on_percent will be > security_min_on_percent (0.2)
await send_temperature_change_event(entity, 18.92, event_timestamp)
assert entity._security_state is False
assert entity.preset_mode == PRESET_ECO
assert entity._saved_preset_mode == PRESET_ECO
assert entity._prop_algorithm.on_percent == 0.0
assert entity._prop_algorithm.calculated_on_percent == 0.0
assert mock_send_event.call_count == 2
mock_send_event.assert_has_calls(
[
call.send_event(EventType.PRESET_EVENT, {"preset": PRESET_ECO}),
call.send_event(
EventType.SECURITY_EVENT,
{
"type": "end",
"last_temperature_measure": event_timestamp.astimezone(
tz
).isoformat(),
"last_ext_temperature_measure": entity._last_ext_temperature_measure.astimezone(
tz
).isoformat(),
"current_temp": 18.92,
"current_ext_temp": None,
"target_temp": 17,
},
),
],
any_order=True,
)
# Heater is stays off
assert mock_heater_on.call_count == 0
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_security_over_climate(
hass: HomeAssistant,
skip_hass_states_is_state,
skip_turn_on_off_heater,
skip_send_event,
):
"""Test that when a underlying climate is not available the VTherm doesn't go into safety mode"""
tz = get_tz(hass) # pylint: disable=invalid-name
now: datetime = datetime.now(tz=tz)
entry = MockConfigEntry(
domain=DOMAIN,
title="TheOverClimateMockName",
unique_id="uniqueId",
data=PARTIAL_CLIMATE_CONFIG, # 5 minutes security delay
)
fake_underlying_climate = 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_FROST_PROTECTION,
PRESET_ECO,
PRESET_COMFORT,
PRESET_BOOST,
]
assert entity.preset_mode is PRESET_NONE
assert entity._security_state is False
# should have been called with EventType.PRESET_EVENT and EventType.HVAC_MODE_EVENT
assert mock_send_event.call_count == 2
mock_send_event.assert_has_calls(
[
# 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 safety mode
assert entity._last_ext_temperature_measure is not None
assert entity._last_temperature_measure is not None
assert (
entity._last_temperature_measure.astimezone(tz) - now
).total_seconds() < 1
assert (
entity._last_ext_temperature_measure.astimezone(tz) - now
).total_seconds() < 1
# Tries to turns on the Thermostat
await entity.async_set_hvac_mode(HVACMode.HEAT)
assert entity.hvac_mode == HVACMode.HEAT
# 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 safety mode
assert entity.security_state is False
assert entity.preset_mode == "none"
assert entity._saved_preset_mode == "none"

View File

@@ -1,3 +1,5 @@
# 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
@@ -12,8 +14,8 @@ from homeassistant.const import UnitOfTime, UnitOfPower, UnitOfEnergy, PERCENTAG
from pytest_homeassistant_custom_component.common import MockConfigEntry
from ..climate import VersatileThermostat
from ..sensor import (
from custom_components.versatile_thermostat.base_thermostat import BaseThermostat
from custom_components.versatile_thermostat.sensor import (
EnergySensor,
MeanPowerSensor,
OnPercentSensor,
@@ -66,7 +68,7 @@ async def test_sensors_over_switch(
},
)
entity: VersatileThermostat = await create_thermostat(
entity: BaseThermostat = await create_thermostat(
hass, entry, "climate.theoverswitchmockname"
)
assert entity
@@ -138,7 +140,7 @@ async def test_sensors_over_switch(
entity.incremente_energy()
await energy_sensor.async_my_climate_changed()
assert energy_sensor.state == 16.667
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
@@ -229,7 +231,7 @@ async def test_sensors_over_climate(
},
)
entity: VersatileThermostat = await create_thermostat(
entity: BaseThermostat = await create_thermostat(
hass, entry, "climate.theoverclimatemockname"
)
assert entity
@@ -361,7 +363,7 @@ async def test_sensors_over_climate_minimal(
},
)
entity: VersatileThermostat = await create_thermostat(
entity: BaseThermostat = await create_thermostat(
hass, entry, "climate.theoverclimatemockname"
)
assert entity

View File

@@ -1,3 +1,5 @@
# 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
@@ -10,7 +12,13 @@ from homeassistant.components.climate import ClimateEntity, DOMAIN as CLIMATE_DO
from pytest_homeassistant_custom_component.common import MockConfigEntry
from ..climate import VersatileThermostat
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
@@ -28,7 +36,7 @@ async def test_over_switch_full_start(hass: HomeAssistant, skip_hass_states_is_s
)
with patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
"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)
@@ -41,17 +49,19 @@ async def test_over_switch_full_start(hass: HomeAssistant, skip_hass_states_is_s
if entity.entity_id == entity_id:
return entity
entity: VersatileThermostat = find_my_entity("climate.theoverswitchmockname")
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.is_over_climate is False
assert entity.hvac_action is HVACAction.OFF
assert entity.hvac_mode is HVACMode.OFF
assert entity.target_temperature == entity.min_temp
assert entity.preset_modes == [
PRESET_NONE,
PRESET_FROST_PROTECTION,
PRESET_ECO,
PRESET_COMFORT,
PRESET_BOOST,
@@ -93,7 +103,7 @@ async def test_over_climate_full_start(hass: HomeAssistant, skip_hass_states_is_
fake_underlying_climate = MockClimate(hass, "mockUniqueId", "MockClimateName", {})
with patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
"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,
@@ -112,14 +122,16 @@ async def test_over_climate_full_start(hass: HomeAssistant, skip_hass_states_is_
entity = find_my_entity("climate.theoverclimatemockname")
assert entity
assert isinstance(entity, ThermostatOverClimate)
assert entity.name == "TheOverClimateMockName"
assert entity._is_over_climate is True
assert entity.is_over_climate is True
assert entity.hvac_action is HVACAction.OFF
assert entity.hvac_mode is HVACMode.OFF
assert entity.target_temperature == entity.min_temp
assert entity.preset_modes == [
PRESET_NONE,
PRESET_FROST_PROTECTION,
PRESET_ECO,
PRESET_COMFORT,
PRESET_BOOST,
@@ -160,7 +172,7 @@ async def test_over_4switch_full_start(hass: HomeAssistant, skip_hass_states_is_
)
with patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
"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)
@@ -173,17 +185,18 @@ async def test_over_4switch_full_start(hass: HomeAssistant, skip_hass_states_is_
if entity.entity_id == entity_id:
return entity
entity: VersatileThermostat = find_my_entity("climate.theover4switchmockname")
entity: BaseThermostat = find_my_entity("climate.theover4switchmockname")
assert entity
assert entity.name == "TheOver4SwitchMockName"
assert entity._is_over_climate is False
assert entity.is_over_climate is False
assert entity.hvac_action is HVACAction.OFF
assert entity.hvac_mode is HVACMode.OFF
assert entity.target_temperature == entity.min_temp
assert entity.preset_modes == [
PRESET_NONE,
PRESET_FROST_PROTECTION,
PRESET_ECO,
PRESET_COMFORT,
PRESET_BOOST,
@@ -218,3 +231,65 @@ async def test_over_4switch_full_start(hass: HomeAssistant, skip_hass_states_is_
),
]
)
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_over_switch_deactivate_preset(
hass: HomeAssistant, skip_hass_states_is_state
):
"""Test the normal full start of a thermostat in thermostat_over_switch type"""
entry = MockConfigEntry(
domain=DOMAIN,
title="TheOverSwitchMockName",
unique_id="uniqueId",
data={
CONF_NAME: "TheOverSwitchMockName",
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_SWITCH,
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
CONF_CYCLE_MIN: 8,
CONF_TEMP_MIN: 15,
CONF_TEMP_MAX: 30,
"frost_temp": 0,
"eco_temp": 17,
"comfort_temp": 0,
"boost_temp": 19,
CONF_USE_WINDOW_FEATURE: False,
CONF_USE_MOTION_FEATURE: False,
CONF_USE_POWER_FEATURE: False,
CONF_USE_PRESENCE_FEATURE: False,
CONF_HEATER: "switch.mock_switch1",
CONF_HEATER_2: None,
CONF_HEATER_3: None,
CONF_HEATER_4: None,
CONF_SECURITY_DELAY_MIN: 10,
CONF_MINIMAL_ACTIVATION_DELAY: 10,
},
)
entity: BaseThermostat = await create_thermostat(
hass, entry, "climate.theoverswitchmockname"
)
assert entity
assert isinstance(entity, ThermostatOverSwitch)
assert entity.preset_modes == [
PRESET_NONE,
# PRESET_FROST_PROTECTION,
PRESET_ECO,
# PRESET_COMFORT,
PRESET_BOOST,
]
assert entity.preset_mode is PRESET_NONE
# try to set the COMFORT Preset which is absent
try:
await entity.async_set_preset_mode(PRESET_COMFORT)
except ValueError as err:
print(err)
else:
assert False
finally:
assert entity.preset_mode is PRESET_NONE

167
tests/test_switch_ac.py Normal file
View File

@@ -0,0 +1,167 @@
""" 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_FROST_PROTECTION,
PRESET_ECO,
PRESET_COMFORT,
PRESET_BOOST,
PRESET_ACTIVITY,
]
assert entity.preset_mode is PRESET_NONE
assert entity._security_state is False # pylint: disable=protected-access
assert entity._window_state is None # pylint: disable=protected-access
assert entity._motion_state is None # pylint: disable=protected-access
assert entity._presence_state is None # pylint: disable=protected-access
assert entity._prop_algorithm is not None # pylint: disable=protected-access
# 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 == 27 # eco_ac_away (no change)
# 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

View File

@@ -1,11 +1,14 @@
""" 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):
async def test_tpi_calculation(
hass: HomeAssistant, skip_hass_states_is_state: None
): # pylint: disable=unused-argument
"""Test the TPI calculation"""
entry = MockConfigEntry(
@@ -35,12 +38,12 @@ async def test_tpi_calculation(hass: HomeAssistant, skip_hass_states_is_state):
},
)
entity: VersatileThermostat = await create_thermostat(
entity: BaseThermostat = await create_thermostat(
hass, entry, "climate.theoverswitchmockname"
)
assert entity
tpi_algo = entity._prop_algorithm
tpi_algo = entity._prop_algorithm # pylint: disable=protected-access
assert tpi_algo
tpi_algo.calculate(15, 10, 7)
@@ -50,36 +53,52 @@ async def test_tpi_calculation(hass: HomeAssistant, skip_hass_states_is_state):
assert tpi_algo.off_time_sec == 0
assert entity.mean_cycle_power is None # no device power configured
tpi_algo.calculate(15, 14, 5)
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)
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)
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)
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)
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

329
tests/test_valve.py Normal file
View File

@@ -0,0 +1,329 @@
# 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, State
from homeassistant.components.climate import HVACAction, HVACMode
from homeassistant.config_entries import ConfigEntryState
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.components.climate import ClimateEntity, DOMAIN as CLIMATE_DOMAIN
from pytest_homeassistant_custom_component.common import MockConfigEntry
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_FROST_PROTECTION + "_temp": 7,
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_FROST_PROTECTION + PRESET_AWAY_SUFFIX + "_temp": 7,
PRESET_ECO + PRESET_AWAY_SUFFIX + "_temp": 17.1,
PRESET_COMFORT + PRESET_AWAY_SUFFIX + "_temp": 17.2,
PRESET_BOOST + PRESET_AWAY_SUFFIX + "_temp": 17.3,
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_FROST_PROTECTION,
PRESET_ECO,
PRESET_COMFORT,
PRESET_BOOST,
PRESET_ACTIVITY,
]
assert entity.preset_mode is PRESET_NONE
assert entity._security_state is False # pylint: disable=protected-access
assert entity._window_state is None # pylint: disable=protected-access
assert entity._motion_state is None # pylint: disable=protected-access
assert entity._presence_state is None # pylint: disable=protected-access
assert entity._prop_algorithm is not None # pylint: disable=protected-access
# 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
expected_state = State(
entity_id="number.mock_valve", state="0", attributes={"min": 10, "max": 50}
)
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=expected_state
):
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 True # Should be 0 but in fact 10 is send
assert (
entity.hvac_action == HVACAction.HEATING
) # Should be IDLE but heating due to 10
assert mock_service_call.call_count == 1
# The VTherm valve is 0, but the underlying have received 10 which is the min
mock_service_call.assert_has_calls(
[
call.async_call(
"number",
"set_value",
{"entity_id": "number.mock_valve", "value": 10},
)
]
)
await send_temperature_change_event(entity, 17, datetime.now())
assert mock_service_call.call_count == 2
# The VTherm valve is 0, but the underlying have received 10 which is the min
mock_service_call.assert_has_calls(
[
call.async_call(
"number",
"set_value",
{
"entity_id": "number.mock_valve",
"value": 10,
}, # the min allowed value
),
call.async_call(
"number",
"set_value",
{
"entity_id": "number.mock_valve",
"value": 50,
}, # the max allowed value
),
]
)
# 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
# Test window open/close (with a normal min/max so that is_device_active is False when open_percent is 0)
expected_state = State(
entity_id="number.mock_valve", state="0", attributes={"min": 0, "max": 99}
)
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=expected_state
):
# 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

1209
tests/test_window.py Normal file

File diff suppressed because it is too large Load Diff