Compare commits

...

59 Commits
3.2.3 ... 3.8.0

Author SHA1 Message Date
Jean-Marc Collin
ca56b58587 Documentation for wire pilot over_switch 2023-11-01 12:32:26 +01:00
Jean-Marc Collin
2ebeac30e6 Feature 124 add inversion pilot wire (#149)
* Add inverse switch command for ThermostatOverSwitch

---------

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

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

* Update sk.json

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

---------

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

* FIX merge from #108 have lost some changes

* FIX entity_not found error

* Change port

* Add tests for over switch AC mode

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

* Build release 3.6.0

---------

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

* Fix movement detection

---------

Co-authored-by: Jean-Marc Collin <jean-marc.collin-extern@renault.com>
2023-10-21 08:28:19 +02:00
Jean-Marc Collin
7e4e407732 Issue #125 - remove thermometers for over climate mode. 2023-10-18 11:34:19 +02:00
Jean-Marc Collin
81900ceeea Issue #120 - presence sensor not updated 2023-10-15 19:04:46 +02:00
Jean-Marc Collin
66297c6044 FIX non catched error (see issue #120) 2023-10-15 18:39:13 +02:00
Jean-Marc Collin
f384225b0f Maj documentation + issue #115 2023-10-15 18:16:24 +02:00
Jean-Marc Collin
e6ecd100f6 Issue #119 - Set preset target temperature not updating in ac mode 2023-10-15 12:22:15 +02:00
Jean-Marc Collin
7ac49d7864 undo remove other packages. Test don't run into gitlab-ci environment 2023-10-15 10:43:59 +02:00
Jean-Marc Collin
40da04838d Bug #121 loop in over climate
* Issue #121 - loop when underlying is slow
* Issue #121 - try fix
* Release 3.5.3
* Fix tests step
2023-10-15 10:29:54 +02:00
Jean-Marc Collin
fcdd93b4ae Issue # 114 - add unit test for multi-climate VTherm 2023-10-08 13:08:34 +02:00
Jean-Marc Collin
56fdbf4fba FIX Issue #114 - Thermostat not reporting on / off properly (90 sec delay) 2023-10-08 11:05:04 +02:00
Jean-Marc Collin
ef994e300b Documentation 2023-10-08 01:29:25 +02:00
Jean-Marc Collin
72c4105bbd Issue #113 - Add multi thermostat over climate 2023-10-08 01:23:25 +02:00
Jean-Marc Collin
79eb4a0a0d Enhance documentation. 2023-10-07 18:44:39 +02:00
Jean-Marc Collin
b032198c66 Issue #103 - expose preset temp pour AC mode 2023-10-07 18:16:20 +02:00
Jean-Marc Collin
487c118b44 Issue #101 - Use target temperature of underlying if changed 2023-10-07 17:54:50 +02:00
Jean-Marc Collin
e29ff0568b Issue #82 - unavailable climate goes into security mode 2023-10-07 10:49:40 +02:00
Jean-Marc Collin
814e4d3b83 Last HA versions 2023-10-03 08:09:12 +02:00
felix schwenzel
abb6531f49 Update climate.py (#112)
without these parenthesis the `action` and therefore returned `hvac_action` seems to be `True` instead of i.e. `heating`
(did not see that when using an underlying homekit devices climate device, but when the underlying was a generic thermostat)
2023-10-03 08:06:52 +02:00
Jean-Marc Collin
f970c18eaf Issue #100: compatibility with HA 2023.9.0 2023-09-08 08:48:09 +02:00
Jean-Marc Collin
af51ef62e0 Issue #99 : over climate VTherm a regulated by the device itself and should not goes into security 2023-08-30 09:06:26 +02:00
Jean-Marc Collin
b38fbd9d78 Issue #99 - security mode toggling 100 times within 2 minutes 2023-08-27 18:06:43 +02:00
Jean-Marc Collin
6e8e72e343 FIX Service name Github error 2023-08-16 22:30:46 +02:00
Jean-Marc Collin
2bebe3e210 Issue #95 - the integration would switch ac on and off rapidly and lock up home assistant if outside temp is NaN 2023-08-05 20:02:54 +02:00
Jean-Marc Collin
aa3b87762d FIX AC climate stops even if already stopped 2023-07-30 21:25:20 +02:00
Jean-Marc Collin
f4cabbf2c0 FIX unit tests 2023-07-30 18:09:42 +02:00
Jean-Marc Collin
24b59e545b FIX cancel_timer await 2023-07-29 11:22:50 +02:00
Jean-Marc Collin
5997a26c73 FIX #90 WarCOzes remark 2023-07-23 09:08:56 +02:00
Jean-Marc Collin
fe4b9ced81 Documentation 2023-07-22 17:23:42 +02:00
Jean-Marc Collin
c4fc976007 Terminate Add AC mode and fix remove_thermostat sync 2023-07-22 17:05:54 +02:00
Jean-Marc Collin
31d862acab With config_flow ok 2023-07-22 12:21:54 +02:00
Jean-Marc Collin
9709a9eed0 Add AC mode configFlow 2023-07-22 10:47:10 +02:00
Jean-Marc Collin
61eae8c066 FIX #89 _is_aux_heat error 2023-06-25 12:27:00 +02:00
Jean-Marc Collin
e16daa3d53 Add hacs.yaml github workflow 2023-05-14 11:11:55 +02:00
Jean-Marc Collin
90a6c926e3 Resolve actions warning 2023-05-14 11:02:29 +02:00
Jean-Marc Collin
64ce3aa0ad Issue #81 - recursive loop when security should be set 2023-04-28 12:12:38 +02:00
Jean-Marc Collin
3f498ffbd3 Add BuyMeCoffee contributors (fr) 2023-04-26 08:23:03 +02:00
Jean-Marc Collin
3236be6c3b Update README.md
Add BuyMeCoffee contributors
2023-04-26 08:21:54 +02:00
Jean-Marc Collin
be86fd3ac0 Merge branch 'main' of github.com:jmcollin78/versatile_thermostat into main 2023-04-22 11:06:12 +02:00
Jean-Marc Collin
e35ba57bd7 Fix compilation warnings 2023-04-22 11:05:38 +02:00
70 changed files with 8695 additions and 3377 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,196 +1,222 @@
default_config:
logger:
default: info
logs:
custom_components.versatile_thermostat: info
custom_components.versatile_thermostat.underlyings: debug
custom_components.versatile_thermostat.climate: debug
default: info
logs:
custom_components.versatile_thermostat: debug
custom_components.versatile_thermostat.underlyings: debug
custom_components.versatile_thermostat.climate: debug
# If you need to debug uncommment the line below (doc: https://www.home-assistant.io/integrations/debugpy/)
debugpy:
start: true
wait: false
port: 5678
start: true
wait: false
port: 5678
input_number:
fake_temperature_sensor1:
name: Temperature
min: 0
max: 35
step: .1
icon: mdi:thermometer
unit_of_measurement: °C
mode: box
fake_external_temperature_sensor1:
name: Ext Temperature
min: -10
max: 35
step: .1
icon: mdi:home-thermometer
unit_of_measurement: °C
mode: box
fake_current_power:
name: Current power
min: 0
max: 1000
step: 10
icon: mdi:flash
unit_of_measurement: kW
fake_current_power_max:
name: Current power max threshold
min: 0
max: 1000
step: 10
icon: mdi:flash
unit_of_measurement: kW
fake_temperature_sensor1:
name: Temperature
min: 0
max: 35
step: .1
icon: mdi:thermometer
unit_of_measurement: °C
mode: box
fake_external_temperature_sensor1:
name: Ext Temperature
min: -10
max: 35
step: .1
icon: mdi:home-thermometer
unit_of_measurement: °C
mode: box
fake_current_power:
name: Current power
min: 0
max: 1000
step: 10
icon: mdi:flash
unit_of_measurement: kW
fake_current_power_max:
name: Current power max threshold
min: 0
max: 1000
step: 10
icon: mdi:flash
unit_of_measurement: kW
fake_valve1:
name: The valve 1
min: 0
max: 100
icon: mdi:pipe-valve
unit_of_measurement: percentage
input_boolean:
# input_boolean to simulate the windows entity. Only for development environment.
fake_window_sensor1:
name: Window 1
icon: mdi:window-closed-variant
# input_boolean to simulate the heater entity switch. Only for development environment.
fake_heater_switch3:
name: Heater 3
icon: mdi:radiator
fake_heater_switch2:
name: Heater 2
icon: mdi:radiator
fake_heater_switch1:
name: Heater 1
icon: mdi:radiator
fake_heater_4switch1:
name: Heater (multiswitch1)
icon: mdi:radiator
fake_heater_4switch2:
name: Heater (multiswitch2)
icon: mdi:radiator
fake_heater_4switch3:
name: Heater (multiswitch3)
icon: mdi:radiator
fake_heater_4switch4:
name: Heater (multiswitch4)
icon: mdi:radiator
# input_boolean to simulate the motion sensor entity. Only for development environment.
fake_motion_sensor1:
name: Motion Sensor 1
icon: mdi:run
# input_boolean to simulate the presence sensor entity. Only for development environment.
fake_presence_sensor1:
name: Presence Sensor 1
icon: mdi:home
# input_boolean to simulate the windows entity. Only for development environment.
fake_window_sensor1:
name: Window 1
icon: mdi:window-closed-variant
# input_boolean to simulate the heater entity switch. Only for development environment.
fake_heater_switch3:
name: Heater 3
icon: mdi:radiator
fake_heater_switch2:
name: Heater 2
icon: mdi:radiator
fake_heater_switch1:
name: Heater 1
icon: mdi:radiator
fake_heater_ac1:
name: Air contionner 1
icon: mdi:air-conditioner
fake_heater_4switch1:
name: Heater (multiswitch1)
icon: mdi:radiator
fake_heater_4switch2:
name: Heater (multiswitch2)
icon: mdi:radiator
fake_heater_4switch3:
name: Heater (multiswitch3)
icon: mdi:radiator
fake_heater_4switch4:
name: Heater (multiswitch4)
icon: mdi:radiator
fake_heater_4climate1:
name: Heater (multiclimate1)
icon: mdi:radiator
fake_heater_4climate2:
name: Heater (multiclimate2)
icon: mdi:radiator
fake_heater_4climate3:
name: Heater (multiclimate3)
icon: mdi:radiator
fake_heater_4climate4:
name: Heater (multiclimate4)
icon: mdi:radiator
# input_boolean to simulate the motion sensor entity. Only for development environment.
fake_motion_sensor1:
name: Motion Sensor 1
icon: mdi:run
# input_boolean to simulate the presence sensor entity. Only for development environment.
fake_presence_sensor1:
name: Presence Sensor 1
icon: mdi:home
climate:
- platform: generic_thermostat
name: Underlying thermostat1
heater: input_boolean.fake_heater_switch3
target_sensor: input_number.fake_temperature_sensor1
- platform: generic_thermostat
name: Underlying thermostat2
heater: input_boolean.fake_heater_switch3
target_sensor: input_number.fake_temperature_sensor1
- platform: generic_thermostat
name: Underlying thermostat3
heater: input_boolean.fake_heater_switch3
target_sensor: input_number.fake_temperature_sensor1
- platform: generic_thermostat
name: Underlying thermostat4
heater: input_boolean.fake_heater_switch3
target_sensor: input_number.fake_temperature_sensor1
- platform: generic_thermostat
name: Underlying thermostat5
heater: input_boolean.fake_heater_switch3
target_sensor: input_number.fake_temperature_sensor1
- platform: generic_thermostat
name: Underlying thermostat6
heater: input_boolean.fake_heater_switch3
target_sensor: input_number.fake_temperature_sensor1
- platform: generic_thermostat
name: Underlying thermostat7
heater: input_boolean.fake_heater_switch3
target_sensor: input_number.fake_temperature_sensor1
- platform: generic_thermostat
name: Underlying thermostat8
heater: input_boolean.fake_heater_switch3
target_sensor: input_number.fake_temperature_sensor1
- platform: generic_thermostat
name: Underlying thermostat9
heater: input_boolean.fake_heater_switch3
target_sensor: input_number.fake_temperature_sensor1
- platform: generic_thermostat
name: Underlying thermostat1
heater: input_boolean.fake_heater_switch3
target_sensor: input_number.fake_temperature_sensor1
- platform: generic_thermostat
name: Underlying thermostat2
heater: input_boolean.fake_heater_switch3
target_sensor: input_number.fake_temperature_sensor1
- platform: generic_thermostat
name: Underlying thermostat3
heater: input_boolean.fake_heater_switch3
target_sensor: input_number.fake_temperature_sensor1
- platform: generic_thermostat
name: Underlying thermostat4
heater: input_boolean.fake_heater_switch3
target_sensor: input_number.fake_temperature_sensor1
- platform: generic_thermostat
name: Underlying thermostat 4-1
heater: input_boolean.fake_heater_4climate1
target_sensor: input_number.fake_temperature_sensor1
ac_mode: false
- platform: generic_thermostat
name: Underlying thermostat 4-2
heater: input_boolean.fake_heater_4climate2
target_sensor: input_number.fake_temperature_sensor1
ac_mode: false
- platform: generic_thermostat
name: Underlying thermostat 4-3
heater: input_boolean.fake_heater_4climate3
target_sensor: input_number.fake_temperature_sensor1
ac_mode: false
- platform: generic_thermostat
name: Underlying thermostat 4-4
heater: input_boolean.fake_heater_4climate4
target_sensor: input_number.fake_temperature_sensor1
ac_mode: false
- platform: generic_thermostat
name: Underlying thermostat9
heater: input_boolean.fake_heater_switch3
target_sensor: input_number.fake_temperature_sensor1
recorder:
include:
domains:
- input_boolean
- input_number
- switch
- climate
- sensor
include:
domains:
- input_boolean
- input_number
- switch
- climate
- sensor
template:
- binary_sensor:
- name: maison_occupee
unique_id: maison_occupee
state: "{{is_state('person.jmc', 'home') }}"
device_class: occupancy
- sensor:
- name: "Total énergie switch1"
unique_id: total_energie_switch1
unit_of_measurement: "kWh"
device_class: energy
state_class: total_increasing
state: >
{% set energy = state_attr('climate.thermostat_switch_1', 'total_energy') %}
{% if energy == 'unavailable' or energy is none%}unavailable{% else %}
{{ ((energy | float) / 1.0) | round(2, default=0) }}
{% endif %}
- name: "Total énergie climate 2"
unique_id: total_energie_climate2
unit_of_measurement: "kWh"
device_class: energy
state_class: total_increasing
state: >
{% set energy = state_attr('climate.thermostat_climate_2', 'total_energy') %}
{% if energy == 'unavailable' or energy is none%}unavailable{% else %}
{{ ((energy | float) / 1.0) | round(2, default=0) }}
{% endif %}
- name: "Total énergie chambre"
unique_id: total_energie_chambre
unit_of_measurement: "kWh"
device_class: energy
state_class: total_increasing
state: >
{% set energy = state_attr('climate.thermostat_chambre', 'total_energy') %}
{% if energy == 'unavailable' or energy is none%}unavailable{% else %}
{{ ((energy | float) / 1.0) | round(2, default=0) }}
{% endif %}
- binary_sensor:
- name: maison_occupee
unique_id: maison_occupee
state: "{{is_state('person.jmc', 'home') }}"
device_class: occupancy
- sensor:
- name: "Total énergie switch1"
unique_id: total_energie_switch1
unit_of_measurement: "kWh"
device_class: energy
state_class: total_increasing
state: >
{% set energy = state_attr('climate.thermostat_switch_1', 'total_energy') | float(default=-1) %}
{% if energy < 0 %}{{none}}{% else %}
{{ energy | round(2, default=0) }}
{% endif %}
- name: "Total énergie climate 2"
unique_id: total_energie_climate2
unit_of_measurement: "kWh"
device_class: energy
state_class: total_increasing
state: >
{% set energy = state_attr('climate.thermostat_climate_2', 'total_energy') | float(default=-1) %}
{% if energy < 0 %}{{none}}{% else %}
{{ energy | round(2, default=0) }}
{% endif %}
- name: "Total énergie chambre"
unique_id: total_energie_chambre
unit_of_measurement: "kWh"
device_class: energy
state_class: total_increasing
state: >
{% set energy = state_attr('climate.thermostat_chambre', 'total_energy') | float(default=-1) %}
{% if energy < 0 %}{{none}}{% else %}
{{ energy | round(2, default=0) }}
{% endif %}
switch:
- platform: template
switches:
pilote_sdb_rdc:
friendly_name: "Pilote chauffage SDB RDC"
value_template: "{{ is_state_attr('switch_seche_serviettes_sdb_rdc', 'sensor_state', 'on') }}"
turn_on:
service: select.select_option
data:
option: comfort
target:
entity_id: select.seche_serviettes_sdb_rdc_cable_outlet_mode
- platform: template
switches:
pilote_sdb_rdc:
friendly_name: "Pilote chauffage SDB RDC"
value_template: "{{ is_state_attr('switch_seche_serviettes_sdb_rdc', 'sensor_state', 'on') }}"
turn_on:
service: select.select_option
data:
option: comfort
target:
entity_id: select.seche_serviettes_sdb_rdc_cable_outlet_mode
turn_off:
service: select.select_option
data:
option: comfort-2
target:
entity_id: select.seche_serviettes_sdb_rdc_cable_outlet_mode
turn_off:
service: select.select_option
data:
option: comfort-2
target:
entity_id: select.seche_serviettes_sdb_rdc_cable_outlet_mode
frontend:
themes:
versatile_thermostat_theme:
state-binary_sensor-safety-on-color: "#FF0B0B"
state-binary_sensor-power-on-color: "#FF0B0B"
state-binary_sensor-window-on-color: "rgb(156, 39, 176)"
state-binary_sensor-motion-on-color: "rgb(156, 39, 176)"
state-binary_sensor-presence-on-color: "lightgreen"
themes:
versatile_thermostat_theme:
state-binary_sensor-safety-on-color: "#FF0B0B"
state-binary_sensor-power-on-color: "#FF0B0B"
state-binary_sensor-window-on-color: "rgb(156, 39, 176)"
state-binary_sensor-motion-on-color: "rgb(156, 39, 176)"
state-binary_sensor-presence-on-color: "lightgreen"
state-binary_sensor-running-on-color: "orange"

View File

@@ -1,44 +1,54 @@
// 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"
],
"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.vscode-pylance"
],
// "mounts": [
// "source=${localWorkspaceFolder}/.devcontainer/configuration.yaml,target=/home/vscode/core/config/configuration.yaml,type=bind,consistency=cached",
// "source=${localWorkspaceFolder}/custom_components,target=/home/vscode/core/config/custom_components,type=bind,consistency=cached"
// ],
"settings": {
"files.eol": "\n",
"editor.tabSize": 4,
"terminal.integrated.profiles.linux": {
"bash": {
"path": "bash",
"args": []
}
},
"terminal.integrated.defaultProfile.linux": "bash",
// "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",
"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

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

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

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

View File

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

View File

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

6
.gitignore vendored
View File

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

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

16
.vscode/settings.json vendored
View File

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

4
.vscode/tasks.json vendored
View File

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

61
CONTRIBUTING-fr.md Normal file
View File

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

61
CONTRIBUTING.md Normal file
View File

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

View File

@@ -8,8 +8,9 @@
> ![Tip](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/tips.png?raw=true) Cette intégration de thermostat vise à simplifier considérablement vos automatisations autour de la gestion du chauffage. Parce que tous les événements autour du chauffage classiques sont gérés nativement par le thermostat (personne à la maison ?, activité détectée dans une pièce ?, fenêtre ouverte ?, délestage de courant ?), vous n'avez pas à vous encombrer de scripts et d'automatismes compliqués pour gérer vos climats. ;-).
- [Merci pour la bière buymecoffee: https://www.buymeacoffee.com/jmcollin78](#merci-pour-la-bière-buymecoffee-httpswwwbuymeacoffeecomjmcollin78)
- [Merci pour la bière buymecoffee](#merci-pour-la-bière-buymecoffee)
- [Quand l'utiliser et ne pas l'utiliser](#quand-lutiliser-et-ne-pas-lutiliser)
- [Incompatibilités](#incompatibilités)
- [Pourquoi une nouvelle implémentation du thermostat ?](#pourquoi-une-nouvelle-implémentation-du-thermostat-)
- [Comment installer cet incroyable Thermostat Versatile ?](#comment-installer-cet-incroyable-thermostat-versatile-)
- [HACS installation (recommendé)](#hacs-installation-recommendé)
@@ -17,6 +18,10 @@
- [Configuration](#configuration)
- [Choix des attributs de base](#choix-des-attributs-de-base)
- [Sélectionnez des entités pilotées](#sélectionnez-des-entités-pilotées)
- [Pour un thermostat de type ```thermostat_over_switch```](#pour-un-thermostat-de-type-thermostat_over_switch)
- [Pour un thermostat de type ```thermostat_over_climate```:](#pour-un-thermostat-de-type-thermostat_over_climate)
- [L'auto-régulation](#lauto-régulation)
- [Pour un thermostat de type ```thermostat_over_valve```:](#pour-un-thermostat-de-type-thermostat_over_valve)
- [Configurez les coefficients de l'algorithme TPI](#configurez-les-coefficients-de-lalgorithme-tpi)
- [Configurer la température préréglée](#configurer-la-température-préréglée)
- [Configurer les portes/fenêtres en allumant/éteignant les thermostats](#configurer-les-portesfenêtres-en-allumantéteignant-les-thermostats)
@@ -26,6 +31,7 @@
- [Configurer la gestion de la puissance](#configurer-la-gestion-de-la-puissance)
- [Configurer la présence ou l'occupation](#configurer-la-présence-ou-loccupation)
- [Configuration avancée](#configuration-avancée)
- [Synthèse des paramètres](#synthèse-des-paramètres)
- [Exemples de réglage](#exemples-de-réglage)
- [Chauffage électrique](#chauffage-électrique)
- [Chauffage central (chauffage gaz ou fuel)](#chauffage-central-chauffage-gaz-ou-fuel)
@@ -39,6 +45,7 @@
- [Forcer la présence/occupation](#forcer-la-présenceoccupation)
- [Modifier la température des préréglages](#modifier-la-température-des-préréglages)
- [Modifier les paramètres de sécurité](#modifier-les-paramètres-de-sécurité)
- [ByPass Window Check](#bypass-window-check)
- [Notifications](#notifications)
- [Attributs personnalisés](#attributs-personnalisés)
- [Quelques résultats](#quelques-résultats)
@@ -54,31 +61,45 @@ Ce composant personnalisé pour Home Assistant est une mise à niveau et est une
> ![Nouveau](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/new-icon.png?raw=true) _*Nouveautés*_
> * **Release 3.8**: Ajout d'une **fonction d'auto-régulation** pour les thermostats `over climate` dont la régulation est faite par le climate sous-jacent. Cf. [L'auto-régulation](#lauto-régulation) et [#129](https://github.com/jmcollin78/versatile_thermostat/issues/129). Ajout de la **possibilité d'inverser la commande** pour un thermostat `over switch` pour adresser les installations avec fil pilote et diode [#124](https://github.com/jmcollin78/versatile_thermostat/issues/124).
> * **Release 3.7**: Ajout du type de **Versatile Thermostat `over valve`** pour piloter une vanne TRV directement ou tout autre équipement type gradateur pour le chauffage. La régulation se fait alors directement en agissant sur le pourcentage d'ouverture de l'entité sous-jacente : 0 la vanne est coupée, 100 : la vanne est ouverte à fond. Cf. [#131](https://github.com/jmcollin78/versatile_thermostat/issues/131). Ajout d'une fonction permettant le bypass de la détection d'ouverture [#138](https://github.com/jmcollin78/versatile_thermostat/issues/138). Ajout de la langue Slovaque
> * **Release 3.6**: Ajout du paramètre `motion_off_delay` pour améliorer la gestion de des mouvements [#116](https://github.com/jmcollin78/versatile_thermostat/issues/116), [#128](https://github.com/jmcollin78/versatile_thermostat/issues/128). Ajout du mode AC (air conditionné) pour un VTherm over switch. Préparation du projet Github pour faciliter les contributions [#127](https://github.com/jmcollin78/versatile_thermostat/issues/127)
> * **Release 3.5**: Plusieurs thermostats sont possibles en "thermostat over climate" mode [#113](https://github.com/jmcollin78/versatile_thermostat/issues/113)
<details>
<summary>Autres versions</summary>
> * **Release 3.4**: bug fix et exposition des preset temperatures pour le mode AC [#103](https://github.com/jmcollin78/versatile_thermostat/issues/103)
> * **Release 3.3**: ajout du mode Air Conditionné (AC). Cette fonction vous permet d'utiliser le mode AC de votre thermostat sous-jacent. Pour l'utiliser, vous devez cocher l'option "Uitliser le mode AC" et définir les valeurs de température pour les presets et pour les presets en cas d'absence
> * **Release 3.2** : ajout de la possibilité de commander plusieurs switch à partir du même thermostat. Dans ce mode, les switchs sont déclenchés avec un délai pour minimiser la puissance nécessaire à un instant (on minimise les périodes de recouvrement). Voir [Configuration](#sélectionnez-des-entités-pilotées)
> * **Release 3.1** : ajout d'une détection de fenêtres/portes ouvertes par chute de température. Cette nouvelle fonction permet de stopper automatiquement un radiateur lorsque la température chute brutalement. Voir [Le mode auto](#le-mode-auto)
> * **Release majeure 3.0** : ajout d'un équipement thermostat et de capteurs (binaires et non binaires) associés. Beaucoup plus proche de la philosphie Home Assistant, vous avez maintenant un accès direct à l'énergie consommée par le radiateur piloté par le thermostat et à plein d'autres capteurs qui seront utiles dans vos automatisations et dashboard.
> * **release 2.3** : ajout de la mesure de puissance et d'énergie du radiateur piloté par le thermostat.
> * **release 2.2** : ajout de fonction de sécurité permettant de ne pas laisser éternellement en chauffe un radiateur en cas de panne du thermomètre
> * **release majeure 2.0** : ajout du thermostat "over climate" permettant de transformer n'importe quel thermostat en Versatile Thermostat et lui ajouter toutes les fonctions de ce dernier.
</details>
# Merci pour la bière [buymecoffee](https://www.buymeacoffee.com/jmcollin78)
Un grand merci à @salabur pour la bière. Ca fait très plaisir.
Un grand merci à @salabur, @pvince83, @bergoglio, @EPicLURcher, @ecolorado66, @Kriss1670, @maia, @f.maymil, @moutte69, @Jerome pour les bières. Ca fait très plaisir et ça m'encourage à continuer !
# Quand l'utiliser et ne pas l'utiliser
Ce thermostat peut piloter 2 types d'équipement:
Ce thermostat peut piloter 3 types d'équipements :
1. un radiateur qui ne fonctionne qu'en mode marche/arrêt (nommé ```thermostat_over_switch```). La configuration minimale nécessaire pour utiliser ce type thermostat est :
a. un équipement comme un radiateur (un ```switch``` ou équivalent),
b. une sonde de température pour la pièce (ou un input_number),
c. un capteur de température externe (pensez à l'intégration météo si vous n'en avez pas)
1. un équipement comme un radiateur (un ```switch``` ou équivalent),
2. une sonde de température pour la pièce (ou un input_number),
3. un capteur de température externe (pensez à l'intégration météo si vous n'en avez pas)
2. un autre thermostat qui a ses propres modes de fonctionnement (nommé ```thermostat_over_climate```). Pour ce type de thermostat la configuration minimale nécessite :
a. un équipement comme une climatisation qui est pilotée par sa propre entity de type ```climate```,
b. une sonde de température pour la pièce (ou un input_number),
c. un capteur de température externe (pensez à l'intégration météo si vous n'en avez pas)
1. un équipement - comme une climatisation, une valve thermostatique - qui est pilotée par sa propre entity de type ```climate```,
3. un équipement qui peut prendre une valeur de 0 à 100% (nommée ```thermostat_over_valve```). A 0 le chauffage est coupé, 100% il est ouvert à fond. Ce type permet de piloter une valve thermostatique (cf. valve Shelly) qui expose une entité de type `number.` permetttant de piloter directement l'ouverture de la vanne. Versatile Thermostat régule la température de la pièce en jouant sur le pourcentage d'ouverture, à l'aide des capteurs de température intérieur et extérieur en utilisant l'algorithme TPI décrit ci-dessous.
Le type ```thermostat_over_climate``` permet d'ajouter à votre équipement existant toutes les fonctionnalités fournies par VersatileThermostat. L'entité climate VersatileThermostat pilotera votre entité climate, en la coupant si les fenêtres sont ouvertes, la passant en mode Eco si personne n'est présent, etc. Cf. [ici](#pourquoi-une-nouvelle-implémentation-du-thermostat). Pour ce type de thermostat, les cycles éventuels de chauffe sont pilotés par l'entité climate sous-jacente et pas par le Versatile Thermostat lui-même.
Le type `over_climate` vous permet d'ajouter à votre équipement existant toutes les fonctionnalités apportées par VersatileThermostat. L'entité climate VersatileThermostat contrôlera votre entité climate sous-jacente, l'éteindra si les fenêtres sont ouvertes, la fera passer en mode Eco si personne n'est présent, etc. Voir [ici] (#pourquoi-un-nouveau-thermostat-implémentation). Pour ce type de thermostat, tous les cycles de chauffage sont contrôlés par l'entité climate sous-jacente et non par le thermostat polyvalent lui-même. Une fonction facultative d'auto-régulation permet au Versatile Thermostat d'ajuster la température donnée en consigne au sous-jacent afin d'atteindre la consigne.
Parce que cette intégration vise à commander le radiateur en tenant compte du préréglage configuré (preset) et de la température ambiante, ces informations sont obligatoires.
Les installations avec fil pilote et diode d'activation bénéficie d'une option qui permet d'inverser la commande on/off du radiateur sous-jacent. Pour cela, utilisez le type `over switch` et cochez l'option d'inversion de la commande.
## Incompatibilités
Certains thermostat de type TRV sont réputés incompatibles avec le Versatile Thermostat. C'est le cas des vannes suivantes :
1. les vannes POPP de Danfoss avec retour de température. Il est impossible d'éteindre cette vanne et elle s'auto-régule d'elle-même causant des conflits avec le VTherm,
2. les vannes thermstatiques "Homematic radio". Elles ont un cycle de service incompatible avec une commande par le Versatile Thermostat
# Pourquoi une nouvelle implémentation du thermostat ?
@@ -99,10 +120,10 @@ Ce composant nommé __Versatile thermostat__ gère les cas d'utilisation suivant
## HACS installation (recommendé)
1. Installez [HACS](https://hacs.xyz/). De cette façon, vous obtenez automatiquement les mises à jour.
2. Ajoutez ce repository Github en tant que repository personnalisé dans les paramètres HACS.
2. L'intégration Versatile Thermostat est maintenant proposée directement depuis l'interface HACF (onglet intégrations),
3. recherchez et installez "Versatile Thermostat" dans HACS et cliquez sur "installer".
4. Redémarrez Home Assistant.
5. Ensuite, vous pouvez ajouter une intégration de Versatile Thermostat dans la page d'intégration. Vous ajoutez autant de thermostats dont vous avez besoin (généralement un par radiateur qui doit être géré ou par pompe dans le cas d'un chauffage centralisé)
5. Ensuite, vous pouvez ajouter une intégration de Versatile Thermostat dans la page Paramètres / Intégrations. Vous ajoutez autant de thermostats dont vous avez besoin (généralement un par radiateur ou par groupe de radiateurs qui doivent être gérés ou par pompe dans le cas d'un chauffage centralisé)
## Installation manuelle
@@ -133,35 +154,82 @@ Suivez ensuite les étapes de configuration comme suit :
Donnez les principaux attributs obligatoires :
1. un nom (sera le nom de l'intégration et aussi le nom de l'entité climate)
2. le type de thermostat ```thermostat_over_switch``` pour piloter un radiateur commandé par un switch ou ```thermostat_over_climate``` pour piloter un autre thermostat. Cf. [ci-dessus](#pourquoi-une-nouvelle-implémentation-du-thermostat)
2. le type de thermostat ```thermostat_over_switch``` pour piloter un radiateur commandé par un switch ou ```thermostat_over_climate``` pour piloter un autre thermostat, ou ```thermostat_over_valve``` Cf. [ci-dessus](#pourquoi-une-nouvelle-implémentation-du-thermostat)
4. un identifiant d'entité de capteur de température qui donne la température de la pièce dans laquelle le radiateur est installé,
5. une entité capteur de température donnant la température extérieure. Si vous n'avez pas de capteur externe, vous pouvez utiliser l'intégration météo locale
6. une durée de cycle en minutes. A chaque cycle, le radiateur s'allumera puis s'éteindra pendant une durée calculée afin d'atteindre la température ciblée (voir [preset](#configure-the-preset-temperature) ci-dessous),
6. une durée de cycle en minutes. A chaque cycle, le radiateur s'allumera puis s'éteindra pendant une durée calculée afin d'atteindre la température ciblée (voir [preset](#configure-the-preset-temperature) ci-dessous). En mode ```over_climate```, le cycle ne sert qu'à faire des controles de base mais ne régule pas directement la température. C'est le ```climate``` sous-jacent qui le fait,
7. les températures minimales et maximales du thermostat,
8. une puissance de l'équipement ce qui va activer les capteurs de puissance et énergie consommée par l'appareil,
9. la liste des fonctionnalités qui seront utilisées pour ce thermostat. En fonction de vos choix, les écrans de configuration suivants s'afficheront ou pas.
> ![Astuce](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/tips.png?raw=true) _*Notes*_
1. avec le type ```thermostat_over_swutch```, les calculs sont effectués à chaque cycle. Donc en cas de changement de conditions, il faudra attendre le prochain cycle pour voir un changement. Pour cette raison, le cycle ne doit pas être trop long. **5 min est une bonne valeur**,
2. si le cycle est trop court, le radiateur ne pourra jamais atteindre la température cible en effet pour le radiateur à accumulation et il sera sollicité inutilement
> 1. avec les types ```over_switch``` et ```over_valve```, les calculs sont effectués à chaque cycle. Donc en cas de changement de conditions, il faudra attendre le prochain cycle pour voir un changement. Pour cette raison, le cycle ne doit pas être trop long. **5 min est une bonne valeur**,
> 2. si le cycle est trop court, le radiateur ne pourra jamais atteindre la température cible. Pour le radiateur à accumulation par exemple il sera sollicité inutilement.
## Sélectionnez des entités pilotées
En fonction de votre choix sur le type de thermostat, vous devrez choisir une ou plusieurs entités de type switch ou une entité de type climate. Seules les entités compatibles sont présentées.
En fonction de votre choix sur le type de thermostat, vous devrez choisir une ou plusieurs entités de type `switch`, `climate` ou `number`. Seules les entités compatibles avec le type sont présentées.
Pour un thermostat de type ```thermostat_over_switch```:
> ![Astuce](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/tips.png?raw=true) _*Comment choisir le type*_
> Le choix du type est important. Même si il toujours possible de le modifier ensuite via l'IHM de configuration, il est préférable de se poser les quelques questions suivantes :
> 1. **quel type d'équipement je vais piloter ?** Dans l'ordre voici ce qu'il faut faire :
> 1. si vous avez une vanne thermostatique (TRV) commandable dans Home Assistant via une entité de type ```number``` (par exemple une _Shelly TRV_), choisissez le type `over_valve`. C'est le type le plus direct et qui assure la meilleure régulation,
> 2. si vous avez un radiateur électrique (avec ou sans fil pilote) et qu'une entité de type ```switch``` permet de l'allumer ou de l'éteindre, alors le type ```over_switch``` est préférable. La régulation sera faite par le Versatile Thermostat en fonction de la température mesuré par votre thermomètre, à l'endroit ou vous l'avez placé,
> 3. dans tous les autres cas, utilisez le mode ```over_climate```. Vous gardez votre entité ```climate``` d'origine et le Versatile Thermostat "ne fait que" piloter le on/off et la température cible de votre thermostat d'origine. La régulation est faite par votre thermostat d'origine dans ce cas. Ce mode est particulièrement adapté aux climatisations réversible tout-en-un dont l'exposition dans Home Assistant se limite à une entité de type ```climate```
> 2. **quelle type de régulation je veux ?** Si l'équipement piloté possède son propre mécanisme de régulation (clim, certaine vanne TRV) et que cette régulation fonctionne bien, optez pour un ```over_climate```
### Pour un thermostat de type ```thermostat_over_switch```
![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/config-linked-entity.png?raw=true)
L'algorithme à utiliser est aujourd'hui limité à TPI est disponible. Voir [algorithme](#algorithme).
Si plusieurs entités de type sont configurées, la thermostat décale les activations afin de minimiser le nombre de switch actif à un instant t. Ca permet une meilleure répartition de la puissance puisque chaque radiateur va s'allumer à son tour.
Exemple de déclenchement synchronisé :
![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/multi-switch-activation.png?raw=true)
Il est possible de choisir un thermostat over switch qui commande une climatisation en cochant la case "AC Mode". Dans ce cas, seul le mode refroidissement sera visible.
Pour un thermostat de type ```thermostat_over_climate```:
Si votre équipement est commandé par un fil pilote avec un diode, vous aurez certainement besoin de cocher la case "Inverser la case". Elle permet de mettre le switch à On lorsqu'on doit étiendre l'équipement et à Off lorsqu'on doit l'allumer.
### Pour un thermostat de type ```thermostat_over_climate```:
![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/config-linked-entity2.png?raw=true)
Il est possible de choisir un thermostat over climate qui commande une climatisation réversible en cochant la case "AC Mode". Dans ce cas, selon l'équipement commandé vous aurez accès au chauffage et/ou au réfroidissement.
#### L'auto-régulation
Depuis la release 3.8, vous avez la possibilité d'activer la fonction d'auto-régulation. Cette fonction autorise VersatileThermostat à adapter la consigne de température donnée au climate sous-jacent afin que la température de la pièce atteigne réellement la consigne.
Pour faire ça, le VersatileThermostat calcule un décalage basé sur les informations suivantes :
1. la différence actuelle entre la température réelle et la température de consigne,
2. l'accumulation des différences passées,
3. la différence entre la température extérieure et la consigne
Ces trois informations sont combinées pour calculer le décalage qui sera ajouté à la consigne courante et envoyé au climate sous-jacent.
La fonction d'auto-régulation se paramètre avec :
1. une dégré de régulation :
1. Légère - pour des faibles besoin en auto-régulation. Dans ce mode, le décalage maximal sera de 1,5°,
2. Medium - pour une auto-régulation moyenne. Un décalage maximal de 2° est possible dans ce mode,
3. Forte - pour un fort besoin d'auto-régulation. Le décalage maximal est de 3° dans ce mode et l'auto-régulation réagira fortement aux changements de température.
2. Un seuil d'auto-régulation : valeur en dessous de laquelle une nouvelle régulation ne sera pas appliquée. Imaginons qu'à un instant t, le décalage soit de 2°. Si au prochain calcul, le décalage est de 2.4°, il sera pas appliqué. Il ne sera appliqué que la différence entre 2 décalages sera au moins égal à ce seuil,
3. Période minimal entre 2 auto-régulation : ce nombre, exprimé en minute, indique la durée entre 2 changements de régulation.
Ces trois paramètres permettent de moduler la régulation et éviter de multiplier les envois de régulation. Certains équipements comme les TRV, les chaudières n'aiment pas qu'on change la consigne de température trop souvent.
> ![Astuce](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/tips.png?raw=true) _*Conseil de mise en place*_
> 1. Ne démarrez pas tout de suite l'auto-régulation. Regardez comment se passe la régulation naturelle de votre équipement. Si vous constatez que la température de consigne n'est pas atteinte ou qu'elle met trop de temps à être atteinte, démarrez la régulation,
> 2. D'abord commencez par une légère auto-régulation et gardez les deux paramètres avec leur valeurs par défaut. Attendez quelques jours et vérifiez si la situation s'est améliorée,
> 3. Si ce n'est pas suffisant, passez en auto-régulation Medium, attendez une stabilisation,
> 4. Si ce n'est toujours pas suffisant, passez en auto-régulation Forte.
L'auto-régulation consiste à forcer l'équipement a aller plus loin en lui forçant sa température de consigne régulièrement. Sa consommation peut donc être augmentée, ainsi que son usure.
### Pour un thermostat de type ```thermostat_over_valve```:
![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/config-linked-entity3.png?raw=true)
Vous pouvez choisir jusqu'à entité du domaine ```number``` ou ```ìnput_number``` qui vont commander les vannes.
L'algorithme à utiliser est aujourd'hui limité à TPI est disponible. Voir [algorithme](#algorithme).
Il est possible de choisir un thermostat over valve qui commande une climatisation en cochant la case "AC Mode". Dans ce cas, seul le mode refroidissement sera visible.
## Configurez les coefficients de l'algorithme TPI
Si vous avez choisi un thermostat de type ```thermostat_over_switch``` vous arriverez sur cette page :
Si vous avez choisi un thermostat de type ```over_switch``` ou ```over_valve``` vous arriverez sur cette page :
![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/config-tpi.png?raw=true)
@@ -182,14 +250,16 @@ Le mode préréglé (preset) vous permet de préconfigurer la température cibl
- **Confort** : l'appareil est en mode confort
- **Boost** : l'appareil tourne toutes les vannes à fond
Si le mode AC est utilisé, vous pourrez aussi configurer les températures lorsque l'équipement en mode climatisation.
**Aucun** est toujours ajouté dans la liste des modes, car c'est un moyen de ne pas utiliser les preset mais une **température manuelle** à la place.
> ![Astuce](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/tips.png?raw=true) _*Notes*_
1. En modifiant manuellement la température cible, réglez le préréglage sur Aucun (pas de préréglage). De cette façon, vous pouvez toujours définir une température cible même si aucun préréglage n'est disponible.
2. Le préréglage standard ``Away`` est un préréglage caché qui n'est pas directement sélectionnable. Versatile Thermostat utilise la gestion de présence ou la gestion de mouvement pour régler automatiquement et dynamiquement la température cible en fonction d'une présence dans le logement ou d'une activité dans la pièce. Voir [gestion de la présence](#configure-the-presence-management).
3. Si vous utilisez la gestion du délestage, vous verrez un préréglage caché nommé ``power``. Le préréglage de l'élément chauffant est réglé sur « puissance » lorsque des conditions de surpuissance sont rencontrées et que le délestage est actif pour cet élément chauffant. Voir [gestion de l'alimentation](#configure-the-power-management).
4. si vous utilisez la configuration avancée, vous verrez le préréglage défini sur ``sécurité`` si la température n'a pas pu être récupérée après un certain délai
5. Si vous ne souhaitez pas utiliser le préréglage, indiquez 0 comme température. Le préréglage sera alors ignoré et ne s'affichera pas dans le composant front
> 1. En modifiant manuellement la température cible, réglez le préréglage sur Aucun (pas de préréglage). De cette façon, vous pouvez toujours définir une température cible même si aucun préréglage n'est disponible.
> 2. Le préréglage standard ``Away`` est un préréglage caché qui n'est pas directement sélectionnable. Versatile Thermostat utilise la gestion de présence ou la gestion de mouvement pour régler automatiquement et dynamiquement la température cible en fonction d'une présence dans le logement ou d'une activité dans la pièce. Voir [gestion de la présence](#configure-the-presence-management).
> 3. Si vous utilisez la gestion du délestage, vous verrez un préréglage caché nommé ``power``. Le préréglage de l'élément chauffant est réglé sur « puissance » lorsque des conditions de surpuissance sont rencontrées et que le délestage est actif pour cet élément chauffant. Voir [gestion de l'alimentation](#configure-the-power-management).
> 4. si vous utilisez la configuration avancée, vous verrez le préréglage défini sur ``sécurité`` si la température n'a pas pu être récupérée après un certain délai
> 5. Si vous ne souhaitez pas utiliser le préréglage, indiquez 0 comme température. Le préréglage sera alors ignoré et ne s'affichera pas dans le composant front
## Configurer les portes/fenêtres en allumant/éteignant les thermostats
Vous devez avoir choisi la fonctionnalité ```Avec détection des ouvertures``` dans la première page pour arriver sur cette page.
@@ -227,10 +297,10 @@ Pour bien régler il est conseillé d'affocher sur un même graphique historique
Et c'est tout ! votre thermostat s'éteindra lorsque les fenêtres seront ouvertes et se rallumera lorsqu'il sera fermé.
> ![Astuce](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/tips.png?raw=true) _*Notes*_
1. Si vous souhaitez utiliser **plusieurs capteurs de porte/fenêtre** pour automatiser votre thermostat, créez simplement un groupe avec le comportement habituel (https://www.home-assistant.io/integrations/binary_sensor.group/)
2. Si vous n'avez pas de capteur de fenêtre/porte dans votre chambre, laissez simplement l'identifiant de l'entité du capteur vide,
3. **Un seul mode est permis**. On ne peut pas configurer un thermostat avec un capteur et une détection automatique. Les 2 modes risquant de se contredire, il n'est pas possible d'avoir les 2 modes en même temps,
4. Il est déconseillé d'utiliser le mode automatique pour un équipement soumis à des variations de température fréquentes et normales (couloirs, zones ouvertes, ...)
> 1. Si vous souhaitez utiliser **plusieurs capteurs de porte/fenêtre** pour automatiser votre thermostat, créez simplement un groupe avec le comportement habituel (https://www.home-assistant.io/integrations/binary_sensor.group/)
> 2. Si vous n'avez pas de capteur de fenêtre/porte dans votre chambre, laissez simplement l'identifiant de l'entité du capteur vide,
> 3. **Un seul mode est permis**. On ne peut pas configurer un thermostat avec un capteur et une détection automatique. Les 2 modes risquant de se contredire, il n'est pas possible d'avoir les 2 modes en même temps,
> 4. Il est déconseillé d'utiliser le mode automatique pour un équipement soumis à des variations de température fréquentes et normales (couloirs, zones ouvertes, ...)
## Configurer le mode d'activité ou la détection de mouvement
Si vous avez choisi la fonctionnalité ```Avec détection de mouvement```, cliquez sur 'Valider' sur la page précédente et vous y arriverez :
@@ -240,17 +310,20 @@ Si vous avez choisi la fonctionnalité ```Avec détection de mouvement```, cliqu
Nous allons maintenant voir comment configurer le nouveau mode Activité.
Ce dont nous avons besoin:
- un **capteur de mouvement**. ID d'entité d'un capteur de mouvement. Les états du capteur de mouvement doivent être « on » (mouvement détecté) ou « off » (aucun mouvement détecté)
- une durée de **délai de mouvement** (en secondes) définissant combien de temps nous attendons la confirmation du mouvement avant de considérer le mouvement
- une durée de **délai de mouvement** (en secondes) définissant combien de temps nous attendons la confirmation du mouvement avant de considérer le mouvement. Ce paramètre peut être supérieur à la temporision de votre détecteur de mouvement, sinon la détection se fera à chaque mouvement signalé par le détecteur,
- une durée de fin **délai de mouvement** (en secondes) définissant combien de temps nous attendons la confirmation d'une fin de mouvement avant de ne plus considérer le mouvement.
- un **préréglage de "mouvement" **. Nous utiliserons la température de ce préréglage lorsqu'une activité sera détectée.
- un **préréglage "pas de mouvement"**. Nous utiliserons la température de ce deuxième préréglage lorsqu'aucune activité n'est détectée.
Alors imaginons que nous voulions avoir le comportement suivant :
- nous avons une pièce avec un thermostat réglé en mode activité, le mode "mouvement" choisi est confort (21.5C), le mode "pas de mouvement" choisi est Eco (18.5C) et la temporisation du mouvement est de 5 min.
- la pièce est vide depuis un moment (aucune activité détectée), la température de cette pièce est de 18,5 C
- quelqu'un entre dans la pièce, une activité est détectée la température est fixée à 21,5 C
- la personne quitte la chambre, au bout de 5 min la température est ramenée à 18,5 C
- nous avons une pièce avec un thermostat réglé en mode activité, le mode "mouvement" choisi est confort (21,5°C), le mode "pas de mouvement" choisi est Eco (18.5°C) et la temporisation du mouvement est de 30 sec lors de la détection et de 5 minutes sur fin de détection.
- la pièce est vide depuis un moment (aucune activité détectée), la température de cette pièce est de 18,5°
- quelqu'un entre dans la pièce, une activité est détectée si le mouvement est présent pendant au moins 30 sec. La température passe alors à 21,5°
- si le mouvement est présent pendant moins de 30 sec (passage rapide), la température reste sur 18,5°,
- imaginons que la température soit passée sur 21,5°, lorsque la personne quitte la pièce, au bout de 5 min la température est ramenée à 18,5°.
- si la personne revient avant les 5 minutes, la température reste sur 21,5°
Pour que cela fonctionne, le thermostat climatique doit être en mode préréglé « Activité ».
Pour que cela fonctionne, le thermostat doit être en mode préréglé « Activité ».
> ![Astuce](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/tips.png?raw=true) _*Notes*_
1. Sachez que comme pour les autres modes prédéfinis, ``Activity`` ne sera proposé que s'il est correctement configuré. En d'autres termes, les 4 clés de configuration doivent être définies si vous souhaitez voir l'activité dans l'interface de l'assistant domestique
@@ -267,10 +340,10 @@ Notez que toutes les valeurs de puissance doivent avoir les mêmes unités (kW o
Cela vous permet de modifier la puissance maximale au fil du temps à l'aide d'un planificateur ou de ce que vous voulez.
> ![Astuce](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/tips.png?raw=true) _*Notes*_
1. En cas de délestage, le radiateur est réglé sur le préréglage nommé ```power```. Il s'agit d'un préréglage caché, vous ne pouvez pas le sélectionner manuellement.
2. Je l'utilise pour éviter de dépasser la limite de mon contrat d'électricité lorsqu'un véhicule électrique est en charge. Cela crée une sorte d'autorégulation.
3. Gardez toujours une marge, car la puissance max peut être brièvement dépassée en attendant le calcul du prochain cycle typiquement ou par des équipements non régulés.
4. Si vous ne souhaitez pas utiliser cette fonctionnalité, laissez simplement l'identifiant des entités vide
> 1. En cas de délestage, le radiateur est réglé sur le préréglage nommé ```power```. Il s'agit d'un préréglage caché, vous ne pouvez pas le sélectionner manuellement.
> 2. Je l'utilise pour éviter de dépasser la limite de mon contrat d'électricité lorsqu'un véhicule électrique est en charge. Cela crée une sorte d'autorégulation.
> 3. Gardez toujours une marge, car la puissance max peut être brièvement dépassée en attendant le calcul du prochain cycle typiquement ou par des équipements non régulés.
> 4. Si vous ne souhaitez pas utiliser cette fonctionnalité, laissez simplement l'identifiant des entités vide
## Configurer la présence ou l'occupation
Si sélectionnée en première page, cette fonction vous permet de modifier dynamiquement la température de tous les préréglages du thermostat configurés lorsque personne n'est à la maison ou lorsque quelqu'un rentre à la maison. Pour cela, vous devez configurer la température qui sera utilisée pour chaque préréglage lorsque la présence est désactivée. Lorsque le capteur de présence s'éteint, ces températures seront utilisées. Lorsqu'il se rallume, la température "normale" configurée pour le préréglage est utilisée. Voir [gestion des préréglages](#configure-the-preset-temperature).
@@ -284,9 +357,11 @@ Pour cela, vous devez configurer :
3. La **température utilisée en Confort** préréglée en cas d'absence,
4. La **température utilisée en Boost** préréglée en cas d'absence
Si le mode AC est utilisé, vous pourrez aussi configurer les températures lorsque l'équipement en mode climatisation.
> ![Astuce](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/tips.png?raw=true) _*Notes*_
1. le changement de température est immédiat et se répercute sur le volet avant. Le calcul prendra en compte la nouvelle température cible au prochain calcul du cycle,
2. vous pouvez utiliser le capteur direct person.xxxx ou un groupe de capteurs de Home Assistant. Le capteur de présence gère les états ``on`` ou ``home`` comme présents et les états ``off`` ou ``not_home`` comme absents.
> 1. le changement de température est immédiat et se répercute sur le volet avant. Le calcul prendra en compte la nouvelle température cible au prochain calcul du cycle,
> 2. vous pouvez utiliser le capteur direct person.xxxx ou un groupe de capteurs de Home Assistant. Le capteur de présence gère les états ``on`` ou ``home`` comme présents et les états ``off`` ou ``not_home`` comme absents.
## Configuration avancée
Ces paramètres permettent d'affiner le réglage du thermostat.
@@ -306,11 +381,77 @@ Le quatrième param§tre (``security_default_on_percent``) est la valeur de ``on
Voir [exemple de réglages](#examples-tuning) pour avoir des exemples de réglage communs
> ![Astuce](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/tips.png?raw=true) _*Notes*_
1. Lorsque le capteur de température viendra à la vie et renverra les températures, le préréglage sera restauré à sa valeur précédente,
3. Attention, deux températures sont nécessaires : la température interne et la température externe et chacune doit donner la température, sinon le thermostat sera en préréglage "security",
4. Un service est disponible qui permet de régler les 3 paramètres de sécurité. Ca peut servir à adapter la fonction de sécurité à votre usage,
5. Pour un usage naturel, le ``security_default_on_percent`` doit être inférieur à ``security_min_on_percent``,
6. Lorsqu'un thermostat de type ``thermostat_over_climate`` passe en mode ``security`` il est éteint. Les paramètres ``security_min_on_percent`` et ``security_default_on_percent`` ne sont alors pas utilisés.
> 1. Lorsque le capteur de température viendra à la vie et renverra les températures, le préréglage sera restauré à sa valeur précédente,
> 2. Attention, deux températures sont nécessaires : la température interne et la température externe et chacune doit donner la température, sinon le thermostat sera en préréglage "security",
> 3. Un service est disponible qui permet de régler les 3 paramètres de sécurité. Ca peut servir à adapter la fonction de sécurité à votre usage,
> 4. Pour un usage naturel, le ``security_default_on_percent`` doit être inférieur à ``security_min_on_percent``,
> 5. Lorsqu'un thermostat de type ``thermostat_over_climate`` passe en mode ``security`` il est éteint. Les paramètres ``security_min_on_percent`` et ``security_default_on_percent`` ne sont alors pas utilisés.
## Synthèse des paramètres
| Paramètre | Libellé | "over switch" | "over climate" | over valve |
| - | - | - | - | - |
| ``name`` | Nom | X | X | X |
| ``thermostat_type`` | Type de thermostat | X | X | X |
| ``temperature_sensor_entity_id`` | Temperature sensor entity id | X | - | X |
| ``external_temperature_sensor_entity_id`` | Température exterieure sensor entity id | X | - | X |
| ``cycle_min`` | Durée du cycle (minutes) | X | X | X |
| ``temp_min`` | Température minimale permise | X | X | X |
| ``temp_max`` | Température maximale permise | X | X | X |
| ``device_power`` | Puissance de l'équipement | X | X | X |
| ``use_window_feature`` | Avec détection des ouvertures | X | X | X |
| ``use_motion_feature`` | Avec détection de mouvement | X | X | X |
| ``use_power_feature`` | Avec gestion de la puissance | X | X | X |
| ``use_presence_feature`` | Avec détection de présence | X | X | X |
| ``heater_entity1_id`` | 1er radiateur | X | - | - |
| ``heater_entity2_id`` | 2ème radiateur | X | - | - |
| ``heater_entity3_id`` | 3ème radiateur | X | - | - |
| ``heater_entity4_id`` | 4ème radiateur | X | - | - |
| ``proportional_function`` | Algorithme | X | - | - |
| ``climate_entity1_id`` | Thermostat sous-jacent | - | X | - |
| ``climate_entity2_id`` | 2ème thermostat sous-jacent | - | X | - |
| ``climate_entity3_id`` | 3ème thermostat sous-jacent | - | X | - |
| ``climate_entity4_id`` | 4ème thermostat sous-jacent | - | X | - |
| ``valve_entity1_id`` | Vanne sous-jacente | - | - | X |
| ``valve_entity2_id`` | 2ème vanne sous-jacente | - | - | X |
| ``valve_entity3_id`` | 3ème vanne sous-jacente | - | - | X |
| ``valve_entity4_id`` | 4ème vanne sous-jacente | - | - | X |
| ``ac_mode`` | utilisation de l'air conditionné (AC) ? | X | X | X |
| ``tpi_coef_int`` | Coefficient à utiliser pour le delta de température interne | X | - | X |
| ``tpi_coef_ext`` | Coefficient à utiliser pour le delta de température externe | X | - | X |
| ``eco_temp`` | Température en preset Eco | X | X | X |
| ``comfort_temp`` | Température en preset Confort | X | X | X |
| ``boost_temp`` | Température en preset Boost | X | X | X |
| ``eco_ac_temp`` | Température en preset Eco en mode AC | X | X | X |
| ``comfort_ac_temp`` | Température en preset Confort en mode AC | X | X | X |
| ``boost_ac_temp`` | Température en preset Boost en mode AC | X | X | X |
| ``window_sensor_entity_id`` | Détecteur d'ouverture (entity id) | X | X | X |
| ``window_delay`` | Délai avant extinction (secondes) | X | X | X |
| ``window_auto_open_threshold`` | Seuil haut de chute de température pour la détection automatique (en °/min) | X | X | X |
| ``window_auto_close_threshold`` | Seuil bas de chute de température pour la fin de détection automatique (en °/min) | X | X | X |
| ``window_auto_max_duration`` | Durée maximum d'une extinction automatique (en min) | X | X | X |
| ``motion_sensor_entity_id`` | Détecteur de mouvement entity id | X | X | X |
| ``motion_delay`` | Délai avant prise en compte du mouvement (seconds) | X | X | X |
| ``motion_off_delay`` | Délai avant prise en compte de la fin de mouvement (seconds) | X | X | X |
| ``motion_preset`` | Preset à utiliser si mouvement détecté | X | X | X |
| ``no_motion_preset`` | Preset à utiliser si pas de mouvement détecté | X | X | X |
| ``power_sensor_entity_id`` | Capteur de puissance totale (entity id) | X | X | X |
| ``max_power_sensor_entity_id`` | Capteur de puissance Max (entity id) | X | X | X |
| ``power_temp`` | Température si délestaqe | X | X | X |
| ``presence_sensor_entity_id`` | Capteur de présence entity id (true si quelqu'un est présent) | X | X | X |
| ``eco_away_temp`` | Température en preset Eco en cas d'absence | X | X | X |
| ``comfort_away_temp`` | Température en preset Comfort en cas d'absence | X | X | X |
| ``boost_away_temp`` | Température en preset Boost en cas d'absence | X | X | X |
| ``eco_ac_away_temp`` | Température en preset Eco en cas d'absence en mode AC | X | X | X |
| ``comfort_ac_away_temp`` | Température en preset Comfort en cas d'absence en mode AC | X | X | X |
| ``boost_ac_away_temp`` | Température en preset Boost en cas d'absence en mode AC | X | X | X |
| ``minimal_activation_delay`` | Délai minimal d'activation | X | - | - |
| ``security_delay_min`` | Délai maximal entre 2 mesures de températures | X | - | X |
| ``security_min_on_percent`` | Pourcentage minimal de puissance pour passer en mode sécurité | X | - | X |
| ``auto_regulation_mode`` | Le mode d'auto-régulation | - | X | - |
| ``auto_regulation_dtemp`` | La seuil d'auto-régulation | - | X | - |
| ``auto_regulation_period_min`` | La période minimale d'auto-régulation | - | X | - |
| ``inverse_switch_command`` | Inverse la commande du switch (pour switch avec fil pilote) | X | - | - |
# Exemples de réglage
@@ -361,12 +502,12 @@ Cette intégration utilise un algorithme proportionnel. Un algorithme proportion
Cet algorithme fait converger la température et arrête d'osciller.
## Algorithme TPI
L'algorithme TPI consiste à calculer à chaque cycle un pourcentage d'état On vs Off pour le radiateur en utilisant la température cible, la température actuelle dans la pièce et la température extérieure actuelle.
L'algorithme TPI consiste à calculer à chaque cycle un pourcentage d'état On vs Off pour le radiateur en utilisant la température cible, la température actuelle dans la pièce et la température extérieure actuelle. Cet algorithme n'est donc valable que pour les Versatile Thermostat qui régulent : `over_switch` et `over_valve`.
Le pourcentage est calculé avec cette formule :
on_percent = coef_int * (température cible - température actuelle) + coef_ext * (température cible - température extérieure)
Ensuite, faites 0 <= on_percent <= 1
Ensuite, l'algo fait en sorte que 0 <= on_percent <= 1
Les valeurs par défaut pour coef_int et coef_ext sont respectivement : ``0.6`` et ``0.01``. Ces valeurs par défaut conviennent à une pièce standard bien isolée.
@@ -374,7 +515,9 @@ Pour régler ces coefficients, gardez à l'esprit que :
1. **si la température cible n'est pas atteinte** après une situation stable, vous devez augmenter le ``coef_ext`` (le ``on_percent`` est trop bas),
2. **si la température cible est dépassée** après une situation stable, vous devez diminuer le ``coef_ext`` (le ``on_percent`` est trop haut),
3. **si l'atteinte de la température cible est trop lente**, vous pouvez augmenter le ``coef_int`` pour donner plus de puissance au réchauffeur,
4. **si l'atteinte de la température cible est trop rapide et que des oscillations apparaissent** autour de la cible, vous pouvez diminuer le ``coef_int`` pour donner moins de puissance au radiateur
4. **si l'atteinte de la température cible est trop rapide et que des oscillations apparaissent** autour de la cible, vous pouvez diminuer le ``coef_int`` pour donner moins de puissance au radiateur.
En type `over_valve` le `on_percent` est ramené à une valeur entre 0 et 100% et sert directement à commander l'ouverture de la vanne.
Voir quelques situations à [examples](#some-results).
@@ -398,6 +541,7 @@ Dans l'ordre, il y a :
11. l'état de sécurité,
12. l'état de l'ouverture (si la gestion des ouvertures est configurée),
13. l'état du mouvement (si la gestion du mouvements est configurée)
14. le pourcentage d'ouverture de la vanne (pour le type `over_valve`)
Pour colorer les capteurs, ajouter ces lignes et personnalisez les au besoin, dans votre configuration.yaml :
@@ -410,6 +554,7 @@ 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"
```
et choisissez le thème ```versatile_thermostat_theme``` dans la configuration du panel. Vous obtiendrez quelque-chose qui va ressembler à ça :
@@ -424,7 +569,7 @@ Ce service permet de forcer l'état de présence indépendamment du capteur de p
Le code pour appeler ce service est le suivant :
```
service : thermostat_polyvalent.set_presence
service : versatile_thermostat.set_presence
Les données:
présence : "off"
cible:
@@ -439,7 +584,7 @@ Vous pouvez modifier l'une ou les deux températures (lorsqu'elles sont présent
Utilisez le code suivant pour régler la température du préréglage :
```
service : thermostat_polyvalent.set_preset_temperature
service : versatile_thermostat.set_preset_temperature
date:
preset : boost
temperature : 17,8
@@ -448,6 +593,17 @@ target:
entity_id : climate.my_thermostat
```
Ou pour changer le pré-réglage du mode Air Conditionné (AC) ajoutez un préfixe `_ac`` au nom du preset comme ceci :
```
service: versatile_thermostat.set_preset_temperature
data:
preset: boost_ac
temperature: 25
temperature_away: 30
target:
entity_id: climate.my_thermostat
```
> ![Astuce](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/tips.png?raw=true) _*Notes*_
- après un redémarrage, les préréglages sont réinitialisés à la température configurée. Si vous souhaitez que votre changement soit permanent, vous devez modifier le préréglage de la température dans la configuration de l'intégration.
@@ -457,8 +613,8 @@ Si le thermostat est en mode ``security`` les nouveaux paramètres sont appliqu
Pour changer les paramètres de sécurité utilisez le code suivant :
```
service : thermostat_polyvalent.set_security
date:
service : versatile_thermostat.set_security
data:
min_on_percent: "0.5"
default_on_percent: "0.1"
delay_min: 60
@@ -466,9 +622,23 @@ target:
entity_id : climate.my_thermostat
```
## ByPass Window Check
Ce service permet d'activer ou non un bypass de la vérification des fenetres.
Il permet de continuer à chauffer même si la fenetre est detectée ouverte.
Mis à ``true`` les modifications de status de la fenetre n'auront plus d'effet sur le thermostat, remis à ``false`` cela s'assurera de désactiver le thermostat si la fenetre est toujours ouverte.
Pour changer le paramètre de bypass utilisez le code suivant :
```
service : versatile_thermostat.set_window_bypass
data:
window_bypass: true
target:
entity_id : climate.my_thermostat
```
# Notifications
Les évènements marquant du thermostat sont notifiés par l'intermédiaire du bus de message.
Les évènements notifiés sont les suivants:
Les évènements notifiés sont les suivants:
- ``versatile_thermostat_security_event`` : un thermostat entre ou sort du preset ``security``
- ``versatile_thermostat_power_event`` : un thermostat entre ou sort du preset ``power``
@@ -505,29 +675,33 @@ Les attributs personnalisés sont les suivants :
| ``[eco/confort/boost]_temp`` | La température configurée pour le préréglage xxx |
| ``[eco/confort/boost]_away_temp`` | La température configurée pour le préréglage xxx lorsque la présence est désactivée ou not_home |
| ``temp_power`` | La température utilisée lors de la détection de la perte |
| ``on_percent`` | (déprécié) Le pourcentage sur calculé par l'algorithme TPI |
| ``on_time_sec`` | (déprécié) La période On en sec. Doit être ```on_percent * cycle_min``` |
| ``off_time_sec`` | (déprécié) La période d'arrêt en sec. Doit être ```(1 - on_percent) * cycle_min``` |
| ``on_percent`` | Le pourcentage sur calculé par l'algorithme TPI |
| ``on_time_sec`` | La période On en sec. Doit être ```on_percent * cycle_min``` |
| ``off_time_sec`` | La période d'arrêt en sec. Doit être ```(1 - on_percent) * cycle_min``` |
| ``cycle_min`` | Le cycle de calcul en minutes |
| ``function`` | L'algorithme utilisé pour le calcul du cycle |
| ``tpi_coef_int`` | Le ``coef_int`` de l'algorithme TPI |
| ``tpi_coef_ext`` | Le ``coef_ext`` de l'algorithme TPI |
| ``saved_preset_mode`` | Le dernier preset utilisé avant le basculement automatique du preset |
| ``saved_target_temp`` | La dernière température utilisée avant la commutation automatique |
| ``window_state`` | (déprécié) Le dernier état connu du capteur de fenêtre. Aucun si la fenêtre n'est pas configurée |
| ``motion_state`` | (déprécié) Le dernier état connu du capteur de mouvement. Aucun si le mouvement n'est pas configuré |
| ``overpowering_state`` | (déprécié) Le dernier état connu du capteur surpuissant. Aucun si la gestion de l'alimentation n'est pas configurée |
| ``presence_state`` | (déprécié) Le dernier état connu du capteur de présence. Aucun si la gestion de présence n'est pas configurée |
| ``window_state`` | Le dernier état connu du capteur de fenêtre. Aucun si la fenêtre n'est pas configurée |
| ``window_bypass_state`` | True si le bypass de la détection d'ouverture et activé |
| ``motion_state`` | Le dernier état connu du capteur de mouvement. Aucun si le mouvement n'est pas configuré |
| ``overpowering_state`` | Le dernier état connu du capteur surpuissant. Aucun si la gestion de l'alimentation n'est pas configurée |
| ``presence_state`` | Le dernier état connu du capteur de présence. Aucun si la gestion de présence n'est pas configurée |
| ``security_delay_min`` | Le délai avant de régler le mode de sécurité lorsque le capteur de température est éteint |
| ``security_min_on_percent`` | Pourcentage de chauffe en dessous duquel le thermostat ne passera pas en sécurité |
| ``security_default_on_percent`` | Pourcentage de chauffe utilisé lorsque le thermostat est en sécurité |
| ``last_temperature_datetime`` | (déprécié) La date et l'heure au format ISO8866 de la dernière réception de température interne |
| ``last_ext_temperature_datetime`` | (déprécié) La date et l'heure au format ISO8866 de la dernière réception de température extérieure |
| ``security_state`` | (déprécié) L'état de sécurité. vrai ou faux |
| ``last_temperature_datetime`` | La date et l'heure au format ISO8866 de la dernière réception de température interne |
| ``last_ext_temperature_datetime`` | La date et l'heure au format ISO8866 de la dernière réception de température extérieure |
| ``security_state`` | L'état de sécurité. vrai ou faux |
| ``minimal_activation_delay_sec`` | Le délai d'activation minimal en secondes |
| ``last_update_datetime`` | La date et l'heure au format ISO8866 de cet état |
| ``friendly_name`` | Le nom du thermostat |
| ``supported_features`` | Une combinaison de toutes les fonctionnalités prises en charge par ce thermostat. Voir la documentation officielle sur l'intégration climatique pour plus d'informations |
| ``valve_open_percent`` | Le pourcentage d'ouverture de la vanne |
| ``regulated_target_temperature`` | La température de consigne calculée par l'auto-régulation |
| ``is_inversed`` | True si la commande est inversée (fil pilote avec diode) |
# Quelques résultats
@@ -659,11 +833,21 @@ series:
name: Current temp
curve: smooth
yaxis_id: left
- entity: climate.thermostat_mythermostat
- entity: climate.thermostat_mythermostat <--- for over_switch
attribute: on_percent
name: Power percent
curve: stepline
yaxis_id: right
- entity: climate.thermostat_mythermostat <--- for over_thermostast
attribute: regulated_target_temperature
name: Regulated temperature
curve: stepline
yaxis_id: left
- entity: climate.thermostat_mythermostat <--- for over_valve
attribute: valve_open_percent
name: Valve open percent
curve: stepline
yaxis_id: right
```
## Et toujours de mieux en mieux avec l'AappDaemon NOTIFIER pour notifier les évènements

315
README.md
View File

@@ -10,6 +10,7 @@
- [Thanks for the beer buymecoffee](#thanks-for-the-beer-buymecoffee)
- [When to use / not use](#when-to-use--not-use)
- [Incompatibilities](#incompatibilities)
- [Why another thermostat implementation ?](#why-another-thermostat-implementation-)
- [How to install this incredible Versatile Thermostat ?](#how-to-install-this-incredible-versatile-thermostat-)
- [HACS installation (recommended)](#hacs-installation-recommended)
@@ -17,6 +18,10 @@
- [Configuration](#configuration)
- [Minimal configuration update](#minimal-configuration-update)
- [Select the driven entity](#select-the-driven-entity)
- [For a ```thermostat_over_switch``` type thermostat](#for-a-thermostat_over_switch-type-thermostat)
- [For a thermostat of type ```thermostat_over_climate```:](#for-a-thermostat-of-type-thermostat_over_climate)
- [Self-regulation](#self-regulation)
- [For a thermostat of type ```thermostat_over_valve```:](#for-a-thermostat-of-type-thermostat_over_valve)
- [Configure the TPI algorithm coefficients](#configure-the-tpi-algorithm-coefficients)
- [Configure the preset temperature](#configure-the-preset-temperature)
- [Configure the doors/windows turning on/off the thermostats](#configure-the-doorswindows-turning-onoff-the-thermostats)
@@ -26,6 +31,7 @@
- [Configure the power management](#configure-the-power-management)
- [Configure the presence or occupancy](#configure-the-presence-or-occupancy)
- [Advanced configuration](#advanced-configuration)
- [Parameters synthesis](#parameters-synthesis)
- [Examples tuning](#examples-tuning)
- [Electrical heater](#electrical-heater)
- [Central heating (gaz or fuel heating system)](#central-heating-gaz-or-fuel-heating-system)
@@ -39,6 +45,7 @@
- [Force the presence / occupancy](#force-the-presence--occupancy)
- [Change the temperature of presets](#change-the-temperature-of-presets)
- [Change security settings](#change-security-settings)
- [ByPass Window Check](#bypass-window-check)
- [Notifications](#notifications)
- [Custom attributes](#custom-attributes)
- [Some results](#some-results)
@@ -53,32 +60,48 @@
This custom component for Home Assistant is an upgrade and is a complete rewrite of the component "Awesome thermostat" (see [Github](https://github.com/dadge/awesome_thermostat)) with addition of features.
>![New](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/new-icon.png?raw=true) _*News*_
> * **Release 3.2**: added the ability to control multiple switches from the same thermostat. In this mode, the switches are triggered with a delay to minimize the power required at one time (we minimize the recovery periods). See [Configuration](#select-the-driven-entity)
> * **Release 3.8**: Added a **self-regulation function** for `over climate` thermostats whose regulation is done by the underlying climate. See [Self-regulation](#self-regulation) and [#129](https://github.com/jmcollin78/versatile_thermostat/issues/129). Added the possibility of **inverting the command** for an `over switch` thermostat to address installations with pilot wire and diode [#124](https://github.com/jmcollin78/versatile_thermostat/issues/124).
> * **Release 3.7**: Addition of the **Versatile Thermostat type `over valve`** to control a TRV valve directly or any other dimmer type equipment for heating. Regulation is then done directly by acting on the opening percentage of the underlying entity: 0 the valve is cut off, 100: the valve is fully opened. See [#131](https://github.com/jmcollin78/versatile_thermostat/issues/131). Added a function allowing the bypass of opening detection [#138](https://github.com/jmcollin78/versatile_thermostat/issues/138). Added Slovak language
> * **Release 3.6**: Added the `motion_off_delay` parameter to improve motion management [#116](https://github.com/jmcollin78/versatile_thermostat/issues/116), [#128](https://github.com/jmcollin78/versatile_thermostat/issues/128). Added AC (air conditioning) mode for a VTherm over switch. Preparing the Github project to facilitate contributions [#127](https://github.com/jmcollin78/versatile_thermostat/issues/127)
> * **Release 3.5**: Multiple thermostats when using "thermostat over another thermostat" mode [#113](https://github.com/jmcollin78/versatile_thermostat/issues/113)
<details>
<summary>Others releases</summary>
> * **Release 3.4**: bug fixes and expose preset temperatures for AC mode [#103](https://github.com/jmcollin78/versatile_thermostat/issues/103)
> * **Release 3.3**: add the Air Conditionned mode (AC). This feature allow to use the eventual AC mode of your underlying climate entity. You have to check the "Use AC mode" checkbox in configuration and give preset temperature value for AC mode and AC mode when absent if absence is configured
> * **Release 3.2**: add the ability to control multiple switches from the same thermostat. In this mode, the switches are triggered with a delay to minimize the power required at one time (we minimize the recovery periods). See [Configuration](#select-the-driven-entity)
> * **Release 3.1**: added detection of open windows/doors by temperature drop. This new function makes it possible to automatically stop a radiator when the temperature drops suddenly. See [Auto mode](#auto-mode)
> * **Major release 3.0**: addition of thermostat equipment and associated sensors (binary and non-binary). Much closer to the Home Assistant philosophy, you now have direct access to the energy consumed by the radiator controlled by the thermostat and many other sensors that will be useful in your automations and dashboard.
> * **release 2.3**: addition of the power and energy measurement of the radiator controlled by the thermostat.
> * **release 2.2**: addition of a safety function allowing a radiator not to be left heating forever in the event of a thermometer failure
> * **major release 2.0**: addition of the "over climate" thermostat allowing you to transform any thermostat into a Versatile Thermostat and add all the functions of the latter.
</details>
# Thanks for the beer [buymecoffee](https://www.buymeacoffee.com/jmcollin78)
Many thanks to @salabur for the beer. It's very pleasing. (Vielen Dank an @salabur für das Bier. Es ist sehr erfreulich.)
Many thanks to @salabur, @pvince83, @bergoglio, @EPicLURcher, @ecolorado66, @Kriss1670, @maia, @f.maymil, @moutte69, @Jerome for the beers. It's very nice and encourages me to continue!
# When to use / not use
This thermostat can control 2 types of equipment:
1. a heater that only works in on/off mode (named ```thermostat_over_switch```). The minimum configuration required to use this type of thermostat is:
has. equipment such as a radiator (a ```switch``` or equivalent),
b. a temperature probe for the room (or an input_number),
vs. an external temperature sensor (think about weather integration if you don't have one)
2. another thermostat that has its own operating modes (named ```thermostat_over_climate```). For this type of thermostat, the minimum configuration requires:
has. equipment such as air conditioning which is controlled by its own ```climate``` type entity,
b. a temperature probe for the room (or an input_number),
vs. an external temperature sensor (think about weather integration if you don't have one)
This thermostat can control 3 types of equipment:
1. a radiator that only operates in on/off mode (called ``thermostat_over_switch```). The minimum configuration necessary to use this type thermostat is:
1. equipment such as a radiator (a ``switch``` or equivalent),
2. a temperature probe for the room (or an input_number),
3. an external temperature sensor (consider weather integration if you don't have one)
2. another thermostat which has its own operating modes (named ``thermostat_over_climate```). For this type of thermostat the minimum configuration requires:
1. equipment - such as air conditioning, a thermostatic valve - which is controlled by its own ``climate'' type entity,
3. equipment which can take a value from 0 to 100% (called `thermostat_over_valve`). At 0 the heating is cut off, 100% it is fully opened. This type allows you to control a thermostatic valve (see Shelly valve) which exposes an entity of type `number.` allowing you to directly control the opening of the valve. Versatile Thermostat regulates the room temperature by adjusting the opening percentage, using the interior and exterior temperature sensors using the TPI algorithm described below.
The ```thermostat_over_climate``` type allows you to add all the functionality provided by VersatileThermostat to your existing equipment. The climate VersatileThermostat entity will control your climate entity, turning it off if the windows are open, switching it to Eco mode if no one is present, etc. See [here](#why-a-new-implementation-of-the-thermostat). For this type of thermostat, any heating cycles are controlled by the underlying climate entity and not by the Versatile Thermostat itself.
The ```thermostat_over_climate``` type allows you to add all the functionality provided by VersatileThermostat to your existing equipment. The climate VersatileThermostat entity will control your existing climate entity, turning it off if the windows are open, switching it to Eco mode if no one is present, etc. See [here](#why-a-new-implementation-of-the-thermostat). For this type of thermostat, any heating cycles are controlled by the underlying climate entity and not by the Versatile Thermostat itself. An optional self-regulation function allows the Versatile Thermostat to adjust the temperature given as a setpoint to the underlying in order to reach the setpoint.
Installations with pilot wire and activation diode benefit from an option which allows the on/off control of the underlying radiator to be reversed. To do this, use the `over switch` type and check the command inversion option.
## Incompatibilities
Some TRV type thermostats are known to be incompatible with the Versatile Thermostat. This is the case for the following valves:
1. Danfoss POPP valves with temperature feedback. It is impossible to turn off this valve and it self-regulates, causing conflicts with the VTherm,
2. “Homematic radio” thermostatic valves. They have a duty cycle incompatible with control by the Versatile Thermostat
# Why another thermostat implementation ?
This component named __Versatile thermostat__ manage the following use cases :
- Configuration through standard integration GUI (using Config Entry flow),
- Full uses of **presets mode**,
@@ -96,7 +119,7 @@ This component named __Versatile thermostat__ manage the following use cases :
## HACS installation (recommended)
1. Install [HACS](https://hacs.xyz/). That way you get updates automatically.
2. Add this Github repository as custom repository in HACS settings.
2. The Versatile Thermostat integration is now offered directly from the HACF interface (integration tab).
3. search and install "Versatile Thermostat" in HACS and click `install`.
4. Restart Home Assistant,
5. Then you can add an Versatile Thermostat integration in the integration page. You add as many Versatile Thermostat that you need (typically one per heater that should be managed)
@@ -128,33 +151,82 @@ Then follow the configurations steps as follow:
Give the main mandatory attributes:
1. a name (will be the name of the integration and also the name of the climate entity)
2. the type of thermostat ```thermostat_over_switch``` to control a radiator controlled by a switch or ```thermostat_over_climate``` to control another thermostat. Cf. [above](#why-a-new-thermostat-implementation)
2. the thermostat type ```thermostat_over_switch``` to control a radiator controlled by a switch or ```thermostat_over_climate``` to control another thermostat, or ```thermostat_over_valve``` Cf. [above](# why-a-new-thermostat-implementation)
4. a temperature sensor entity identifier which gives the temperature of the room in which the radiator is installed,
5. a temperature sensor entity giving the outside temperature. If you don't have an external sensor, you can use local weather integration
6. a cycle duration in minutes. On each cycle, the heater will cycle on and then off for a calculated time to reach the target temperature (see [preset](#configure-the-preset-temperature) below),
6. a cycle duration in minutes. On each cycle, the heater will cycle on and then off for a calculated time to reach the target temperature (see [preset](#configure-the-preset-temperature) below). In ```over_climate``` mode, the cycle is only used to carry out basic controls but does not directly regulate the temperature. It's the underlying climate that does it,
7. minimum and maximum thermostat temperatures,
8. the power of the l'équipement which will activate the power and energy sensors of the device,
9. the list of features that will be used for this thermostat. Depending on your choices, the following configuration screens will appear or not.
> ![Tip](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/tips.png?raw=true) _*Notes*_
1. With the ```thermostat_over_switch``` type, calculation are done at each cycle. So in case of conditions change, you will have to wait for the next cycle to see a change. For this reason, the cycle should not be too long. **5 min is a good value**,
2. if the cycle is too short, the heater could never reach the target temperature indeed for heater with accumulation features and it will be unnecessary solicited
> 1. With the ```thermostat_over_switch``` type, calculation are done at each cycle. So in case of conditions change, you will have to wait for the next cycle to see a change. For this reason, the cycle should not be too long. **5 min is a good value**,
> 2. if the cycle is too short, the heater could never reach the target temperature. For the storage radiator for example it will be used unnecessarily.
## Select the driven entity
Depending on your choice on the type of thermostat, you will have to choose a switch type entity or a climate type entity. Only compatible entities are shown.
Depending on your choice of thermostat type, you will need to choose one or more `switch`, `climate` or `number` type entities. Only entities compatible with the type are presented.
For a ```thermostat_over_switch``` thermostat:
> ![Tip](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/tips.png?raw=true) _*How to choose the type*_
> The choice of type is important. Even if it is always possible to modify it afterwards via the configuration HMI, it is preferable to ask yourself the following few questions:
> 1. **what type of equipment am I going to pilot?** In order, here is what to do:
> 1. if you have a thermostatic valve (TRV) that can be controlled in Home Assistant via a ```number``` type entity (for example a _Shelly TRV_), choose the `over_valve` type. It is the most direct type and which ensures the best regulation,
> 2. if you have an electric radiator (with or without pilot wire) and a ```switch``` type entity allows you to turn it on or off, then the ```over_switch``` type is preferable. Regulation will be done by the Versatile Thermostat according to the temperature measured by your thermometer, where you have placed it,
> 3. in all other cases, use the ```over_climate``` mode. You keep your original `climate` entity and the Versatile Thermostat "only" controls the on/off and the target temperature of your original thermostat. Regulation is done by your original thermostat in this case. This mode is particularly suitable for all-in-one reversible air conditioning systems whose exposure in Home Assistant is limited to a `climate` type entity.
> 2. **what type of regulation do I want?** If the controlled equipment has its own regulation mechanism (air conditioning, certain TRV valve) and this regulation works well, opt for an ``over_climate```
It is possible to choose an over switch thermostat which controls air conditioning by checking the "AC Mode" box. In this case, only the cooling mode will be visible.
### For a ```thermostat_over_switch``` type thermostat
![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/config-linked-entity.png?raw=true)
The algorithm to be used today is limited to TPI is available. See [algorithm](#algorithm)
If several type entities are configured, the thermostat staggers the activations in order to minimize the number of active switches at a time t. This allows a better distribution of power since each radiator will turn on in turn.
The algorithm to use is currently limited to TPI is available. See [algorithm](#algorithm).
If several type entities are configured, the thermostat shifts the activations in order to minimize the number of switches active at a time t. This allows for better power distribution since each radiator will turn on in turn.
Example of synchronized triggering:
![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/multi-switch-activation.png?raw=true)
For a ```thermostat_over_climate``` thermostat:
It is possible to choose an over switch thermostat which controls air conditioning by checking the "AC Mode" box. In this case, only the cooling mode will be visible.
If your equipment is controlled by a pilot wire with a diode, you will certainly need to check the "Invert Check" box. It allows you to set the switch to On when you need to turn the equipment off and to Off when you need to turn it on.
### For a thermostat of type ```thermostat_over_climate```:
![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/config-linked-entity2.png?raw=true)
It is possible to choose an over climate thermostat which controls reversible air conditioning by checking the “AC Mode” box. In this case, depending on the equipment ordered, you will have access to heating and/or cooling.
#### Self-regulation
Since release 3.8, you have the possibility to activate the self-regulation function. This function allows VersatileThermostat to adapt the temperature setpoint given to the underlying climate so that the room temperature actually reaches the setpoint.
To do this, the VersatileThermostat calculates an offset based on the following information:
1. the current difference between the actual temperature and the set temperature,
2. the accumulation of past differences,
3. the difference between the outside temperature and the setpoint
These three pieces of information are combined to calculate the offset which will be added to the current setpoint and sent to the underlying climate.
The self-regulation function is configured with:
1. a degree of regulation:
1. Light - for low self-regulation needs. In this mode, the maximum offset will be 1.5°,
2. Medium - for average self-regulation. A maximum offset of 2° is possible in this mode,
3. Strong - for a strong need for self-regulation. The maximum offset is 3° in this mode and the auto-regulation will react strongly to temperature changes.
2. A self-regulation threshold: value below which new regulation will not be applied. Let us imagine that at a time t, the offset is 2°. If in the next calculation, the offset is 2.4°, it will not be applied. It will only be applied that the difference between 2 offsets will be at least equal to this threshold,
3. Minimum period between 2 self-regulation changes: this number, expressed in minutes, indicates the duration between 2 regulation changes.
These three parameters make it possible to modulate the regulation and avoid multiplying the regulation sendings. Some equipment such as TRVs and boilers do not like the temperature setpoint to be changed too often.
> ![Tip](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/tips.png?raw=true) _*Implementation tip*_
> 1. Do not start self-regulation straight away. Watch how the natural regulation of your equipment works. If you notice that the set temperature is not reached or that it is taking too long to be reached, start the regulation,
> 2. First start with a slight self-regulation and keep both parameters at their default values. Wait a few days and check if the situation has improved,
> 3. If this is not sufficient, switch to Medium self-regulation, wait for stabilization,
> 4. If this is still not sufficient, switch to Strong self-regulation.
Self-regulation consists of forcing the equipment to go further by forcing its set temperature regularly. Its consumption can therefore be increased, as well as its wear.
### For a thermostat of type ```thermostat_over_valve```:
![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/config-linked-entity3.png?raw=true)
You can choose up to domain entity ```number``` or ```ìnput_number``` which will control the valves.
The algorithm to use is currently limited to TPI is available. See [algorithm](#algorithm).
It is possible to choose an over valve thermostat which controls air conditioning by checking the "AC Mode" box. In this case, only the cooling mode will be visible.
## Configure the TPI algorithm coefficients
Click on 'Validate' on the previous page and you will get there:
click on 'Validate' on the previous page, and if you choose a ```over_switch``` or ```over_valve``` thermostat and you will get there:
![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/config-tpi.png?raw=true)
For more informations on the TPI algorithm and tuned please refer to [algorithm](#algorithm).
@@ -168,14 +240,16 @@ The preset mode allows you to pre-configurate targeted temperature. Used in conj
- **Comfort** : device is in comfort mode
- **Boost** : device turn all valve full up
If AC mode is used, you will also be able to configure temperatures when the equipment is in cooling mode.
**None** is always added in the list of modes, as it is a way to not use the presets modes but a **manual temperature** instead.
> ![Tip](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/tips.png?raw=true) _*Notes*_
1. Changing manually the target temperature, set the preset to None (no preset). This way you can always set a target temperature even if no preset are available.
2. standard ``Away`` preset is a hidden preset which is not directly selectable. Versatile Thermostat uses the presence management or movement management to set automatically and dynamically the target temperature depending on a presence in the home or an activity in the room. See [presence management](#configure-the-presence-management).
3. if you uses the power shedding management, you will see a hidden preset named ``power``. The heater preset is set to ``power`` when overpowering conditions are encountered and shedding is active for this heater. See [power management](#configure-the-power-management).
4. if you uses the advanced configuration you will see the preset set to ``security`` if the temperature could not be retrieved after a certain delay
5. ff you don't want to use the preseet, give 0 as temperature. The preset will then been ignored and will not displayed in the front component
> 1. Changing manually the target temperature, set the preset to None (no preset). This way you can always set a target temperature even if no preset are available.
> 2. standard ``Away`` preset is a hidden preset which is not directly selectable. Versatile Thermostat uses the presence management or movement management to set automatically and dynamically the target temperature depending on a presence in the home or an activity in the room. See [presence management](#configure-the-presence-management).
> 3. if you uses the power shedding management, you will see a hidden preset named ``power``. The heater preset is set to ``power`` when overpowering conditions are encountered and shedding is active for this heater. See [power management](#configure-the-power-management).
> 4. if you uses the advanced configuration you will see the preset set to ``security`` if the temperature could not be retrieved after a certain delay
> 5. ff you don't want to use the preseet, give 0 as temperature. The preset will then been ignored and will not displayed in the front component
## Configure the doors/windows turning on/off the thermostats
You must have chosen the ```With opening detection``` feature on the first page to arrive on this page.
@@ -212,10 +286,11 @@ To properly adjust it is advisable to display on the same historical graph the t
And that's all ! your thermostat will turn off when the windows are open and turn back on when closed.
> ![Tip](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/tips.png?raw=true) _*Notes*_
1. If you want to use **multiple door/window sensors** to automate your thermostat, just create a group with the usual behavior (https://www.home-assistant.io/integrations/binary_sensor.group/)
2. If you don't have a window/door sensor in your room, just leave the sensor entity id blank,
3. **Only one mode is allowed**. You cannot configure a thermostat with a sensor and automatic detection. The 2 modes may contradict each other, it is not possible to have the 2 modes at the same time,
4. It is not recommended to use the automatic mode for equipment subject to frequent and normal temperature variations (corridors, open areas, ...)
> 1. If you want to use **multiple door/window sensors** to automate your thermostat, just create a group with the usual behavior (https://www.home-assistant.io/integrations/binary_sensor.group/)
> 2. If you don't have a window/door sensor in your room, just leave the sensor entity id blank,
> 3. **Only one mode is allowed**. You cannot configure a thermostat with a sensor and automatic detection. The 2 modes may contradict each other, it is not possible to have the 2 modes at the same time,
> 4. It is not recommended to use the automatic mode for equipment subject to frequent and normal temperature variations (corridors, open areas, ...)
## Configure the activity mode or motion detection
If you choose the ```Motion management``` feature, lick on 'Validate' on the previous page and you will get there:
![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/config-motion.png?raw=true)
@@ -224,19 +299,21 @@ We will now see how to configure the new Activity mode.
What we need:
- a **motion sensor**. The entity id of a motion sensor. Motion sensor states should be 'on' (motion detected) or 'off' (no motion detected)
- a **motion delay** (in seconds) duration defining how long we wait for motion confirmation before considering the motion
- a **end of motion delay** (in seconds) duration defining how long we wait for end of motion confirmation before considering the end of motion
- a **target "motion" preset**. We will used the temperature of this preset when an activity is detected.
- a **target "no motion" preset**. We will used the temperature of this second preset when no activity is detected.
So imagine we want to have the following behavior :
- we have room with a thermostat set in activity mode, the "motion" mode chosen is comfort (21.5C), the "no motion" mode chosen is Eco (18.5 C) and the motion delay is 5 min.
- the room is empty for a while (no activity detected), the temperature of this room is 18.5 C
- somebody enters into the room, an activity is detected the temperature is set to 21.5 C
- the person leaves the room, after 5 min the temperature is set back to 18.5 C
- we have a room with a thermostat set to activity mode, the "movement" mode chosen is comfort (21.5°C), the "no movement" mode chosen is Eco (18.5°C) and the movement delay is 30 sec during detection and 5 minutes at the end of detection.
- the room has been empty for a while (no activity detected), the temperature of this room is 18.5°
- someone enters the room, activity is detected if movement is present for at least 30 seconds. The temperature then rises to 21.5°
- if the movement is present for less than 30 seconds (rapid passage), the temperature remains at 18.5°,
- imagine that the temperature has risen to 21.5°, when the person leaves the room, after 5 minutes the temperature is reduced to 18.5°.
- if the person returns before 5 minutes, the temperature remains at 21.5°
For this to work, the climate thermostat should be in ``Activity`` preset mode.
> ![Tip](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/tips.png?raw=true) _*Notes*_
1. Be aware that as for the others preset modes, ``Activity`` will only be proposed if it's correctly configure. In other words, the 4 configuration keys have to be set if you want to see Activity in home assistant Interface
> 1. Be aware that as for the others preset modes, ``Activity`` will only be proposed if it's correctly configure. In other words, the 4 configuration keys have to be set if you want to see Activity in home assistant Interface
## Configure the power management
@@ -250,10 +327,10 @@ Note that all power values should have the same units (kW or W for example).
This allows you to change the max power along time using a Scheduler or whatever you like.
> ![Tip](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/tips.png?raw=true) _*Notes*_
1. When shedding is encountered, the heater is set to the preset named ``power``. This is a hidden preset, you cannot select it manually.
2. I use this to avoid exceeded the limit of my electrical power contract when an electrical vehicle is charging. This makes a kind of auto-regulation.
3. Always keep a margin, because max power can be briefly exceeded while waiting for the next cycle calculation typically or by not regulated equipement.
4. If you don't want to use this feature, just leave the entities id empty
> 1. When shedding is encountered, the heater is set to the preset named ``power``. This is a hidden preset, you cannot select it manually.
> 2. I use this to avoid exceeded the limit of my electrical power contract when an electrical vehicle is charging. This makes a kind of auto-regulation.
> 3. Always keep a margin, because max power can be briefly exceeded while waiting for the next cycle calculation typically or by not regulated equipement.
> 4. If you don't want to use this feature, just leave the entities id empty
## Configure the presence or occupancy
If you choose the ```Presence management``` feature, this feature allows you to dynamically changes the temperature of all configured Versatile thermostat's presets when nobody is at home or when someone comes back home. For this, you have to configure the temperature that will be used for each preset when presence is off. When the occupancy sensor turns to off, those tempoeratures will be used. When it turns on again the "normal" temperature configured for the preset is used. See [preset management](#configure-the-preset-temperature).
@@ -267,9 +344,11 @@ For this you need to configure:
3. The **temperature used in Comfort** preset when absent,
4. The **temperature used in Boost** preset when absent
Si le mode AC est utilisé, vous pourrez aussi configurer les températures lorsque l'équipement en mode climatisation.
> ![Tip](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/tips.png?raw=true) _*Notes*_
1. the switch of temperature is immediate and is reflected on the front component. The calculation will take the new target temperature into account at the next cycle calculation,
2. you can use direct person.xxxx sensor or group of sensors of Home Assistant. The presence sensor handles ``on`` or ``home`` states as present and ``off`` or ``not_home`` state as absent.
> 1. the switch of temperature is immediate and is reflected on the front component. The calculation will take the new target temperature into account at the next cycle calculation,
> 2. you can use direct person.xxxx sensor or group of sensors of Home Assistant. The presence sensor handles ``on`` or ``home`` states as present and ``off`` or ``not_home`` state as absent.
## Advanced configuration
Those parameters allows to fine tune the thermostat.
@@ -289,11 +368,78 @@ The fourth parameter (``security_default_on_percent``) is the ``on_percent`` val
See [example tuning](#examples-tuning) for common tuning examples
>![Tip](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/tips.png?raw=true) _*Notes*_
1. When the temperature sensor comes to life and returns the temperatures, the preset will be restored to its previous value,
3. Attention, two temperatures are needed: internal temperature and external temperature and each must give the temperature, otherwise the thermostat will be in "security" preset,
4. A service is available that allows you to set the 3 security parameters. This can be used to adapt the security function to your use.
5. For natural usage, the ``security_default_on_percent`` should be less than ``security_min_on_percent``,
6. When a ``thermostat_over_climate`` type thermostat goes into ``security`` mode it is turned off. The ``security_min_on_percent`` and ``security_default_on_percent`` parameters are then not used.
> 1. When the temperature sensor comes to life and returns the temperatures, the preset will be restored to its previous value,
> 2. Attention, two temperatures are needed: internal temperature and external temperature and each must give the temperature, otherwise the thermostat will be in "security" preset,
> 3. A service is available that allows you to set the 3 security parameters. This can be used to adapt the security function to your use.
> 4. For natural usage, the ``security_default_on_percent`` should be less than ``security_min_on_percent``,
> 5. When a ``thermostat_over_climate`` type thermostat goes into ``security`` mode it is turned off. The ``security_min_on_percent`` and ``security_default_on_percent`` parameters are then not used.
## Parameters synthesis
| Paramètre | Libellé | "over switch" | "over climate" | "over valve" |
| ----------| --------| --- | --- | -- |
| ``name`` | Name | X | X | X |
| ``thermostat_type`` | Thermostat type | X | X | X |
| ``temperature_sensor_entity_id`` | Temperature sensor entity id | X | - | X |
| ``external_temperature_sensor_entity_id`` | External temperature sensor entity id | X | - | X |
| ``cycle_min`` | Cycle duration (minutes) | X | X | X |
| ``temp_min`` | Minimal temperature allowed | X | X | X |
| ``temp_max`` | Maximal temperature allowed | X | X | X |
| ``device_power`` | Device power | X | X | X |
| ``use_window_feature`` | Use window detection | X | X | X |
| ``use_motion_feature`` | Use motion detection | X | X | X |
| ``use_power_feature`` | Use power management | X | X | X |
| ``use_presence_feature`` | Use presence detection | X | X | X |
| ``heater_entity1_id`` | 1rst heater switch | X | - | - |
| ``heater_entity2_id`` | 2nd heater switch | X | - | - |
| ``heater_entity3_id`` | 3rd heater switch | X | - | - |
| ``heater_entity4_id`` | 4th heater switch | X | - | - |
| ``proportional_function`` | Algorithm | X | - | X |
| ``climate_entity1_id`` | 1rst underlying climate | - | X | - |
| ``climate_entity2_id`` | 2nd underlying climate | - | X | - |
| ``climate_entity3_id`` | 3rd underlying climate | - | X | - |
| ``climate_entity4_id`` | 4th underlying climate | - | X | - |
| ``valve_entity1_id`` | 1rst underlying valve | - | - | X |
| ``valve_entity2_id`` | 2nd underlying valve | - | - | X |
| ``valve_entity3_id`` | 3rd underlying valve | - | - | X |
| ``valve_entity4_id`` | 4th underlying valve | - | - | X |
| ``ac_mode`` | Use the Air Conditioning (AC) mode | X | X | X |
| ``tpi_coef_int`` | Coefficient to use for internal temperature delta | X | - | X |
| ``tpi_coef_ext`` | Coefficient to use for external temperature delta | X | - | X |
| ``eco_temp`` | Temperature in Eco preset | X | X | X |
| ``comfort_temp`` | Temperature in Comfort preset | X | X | X |
| ``boost_temp`` | Temperature in Boost preset | X | X | X |
| ``eco_ac_temp`` | Temperature in Eco preset for AC mode | X | X | X |
| ``comfort_ac_temp`` | Temperature in Comfort preset for AC mode | X | X | X |
| ``boost_ac_temp`` | Temperature in Boost preset for AC mode | X | X | X |
| ``window_sensor_entity_id`` | Window sensor entity id | X | X | X |
| ``window_delay`` | Window sensor delay (seconds) | X | X | X |
| ``window_auto_open_threshold`` | Temperature decrease threshold for automatic window open detection (in °/min) | X | X | X |
| ``window_auto_close_threshold`` | Temperature increase threshold for end of automatic detection (in °/min) | X | X | X |
| ``window_auto_max_duration`` | Maximum duration of automatic window open detection (in min) | X | X | X |
| ``motion_sensor_entity_id`` | Motion sensor entity id | X | X | X |
| ``motion_delay`` | Delay before considering the motion (seconds) | X | X | X |
| ``motion_off_delay`` | Delay before considering the end of motion (seconds) | X | X | X |
| ``motion_preset`` | Preset to use when motion is detected | X | X | X |
| ``no_motion_preset`` | Preset to use when no motion is detected | X | X | X |
| ``power_sensor_entity_id`` | Power sensor entity id | X | X | X |
| ``max_power_sensor_entity_id`` | Max power sensor entity id | X | X | X |
| ``power_temp`` | Temperature for Power shedding | X | X | X |
| ``presence_sensor_entity_id`` | Presence sensor entity id | X | X | X |
| ``eco_away_temp`` | Temperature in Eco preset when no presence | X | X | X |
| ``comfort_away_temp`` | Temperature in Comfort preset when no presence | X | X | X |
| ``boost_away_temp`` | Temperature in Boost preset when no presence | X | X | X |
| ``eco_ac_away_temp`` | Temperature in Eco preset when no presence in AC mode | X | X | X |
| ``comfort_ac_away_temp`` | Temperature in Comfort preset when no presence in AC mode | X | X | X |
| ``boost_ac_away_temp`` | Temperature in Boost preset when no presence in AC mode | X | X | X |
| ``minimal_activation_delay`` | Minimal activation delay | X | - | X |
| ``security_delay_min`` | Security delay (in minutes) | X | X | X |
| ``security_min_on_percent`` | Minimal power percent to enable security mode | X | X | X |
| ``security_default_on_percent`` | Power percent to use in security mode | X | X | X |
| ``auto_regulation_mode`` | Le mode d'auto-régulation | - | X | - |
| ``auto_regulation_dtemp`` | La seuil d'auto-régulation | - | X | - |
| ``auto_regulation_period_min`` | La période minimale d'auto-régulation | - | X | - |
| ``inverse_switch_command`` | Inverse the switch command (for pilot wire switch) | X | - | - |
# Examples tuning
@@ -344,7 +490,7 @@ This integration uses a proportional algorithm. A Proportional algorithm is usef
This algorithm make the temperature converge and stop oscillating.
## TPI algorithm
The TPI algorithm consist in the calculation at each cycle of a percentage of On state vs Off state for the heater using the target temperature, the current temperature in the room and the current external temperature.
The TPI algorithm consist in the calculation at each cycle of a percentage of On state vs Off state for the heater using the target temperature, the current temperature in the room and the current external temperature. This algorithm is therefore only valid for Versatile Thermostats which regulate: `over_switch` and `over_valve`.
The percentage is calculated with this formula:
@@ -359,6 +505,8 @@ To tune those coefficients keep in mind that:
3. **if reaching the target temperature is too slow**, you can increase the ``coef_int`` to give more power to the heater,
4. **if reaching the target temperature is too fast and some oscillations appears** around the target, you can decrease the ``coef_int`` to give less power to the heater
In type `over_valve` the `on_percent` is reduced to a value between 0 and 100% and is used directly to control the opening of the valve.
See some situations at [examples](#some-results).
# Sensors
@@ -380,7 +528,8 @@ In order, there are:
10. presence status (if presence management is configured),
11. security status,
12. opening status (if opening management is configured),
13. motion status (if motion management is configured)
13. motion status (if motion management is configured),
14. the valve opening percentage (for the `over_valve` type)
To color the sensors, add these lines and customize them as needed, in your configuration.yaml:
@@ -393,6 +542,7 @@ 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"
```
and choose the ```versatile_thermostat_theme``` theme in the panel configuration. You will get something that will look like this:
@@ -431,6 +581,17 @@ target:
entity_id: climate.my_thermostat
```
Or to change the preset of the AC mode, add _ac to the preset name like this:
```
service: versatile_thermostat.set_preset_temperature
data:
preset: boost_ac
temperature: 25
temperature_away: 30
target:
entity_id: climate.my_thermostat
```
> ![Tip](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/tips.png?raw=true) _*Notes*_
- after a restart the preset are resetted to the configured temperature. If you want your change to be permanent you should modify the temperature preset into the confguration of the integration.
@@ -441,13 +602,25 @@ If the thermostat is in ``security`` mode the new settings are applied immediate
To change the security settings use the following code:
```
service : thermostat_polyvalent.set_security
date:
data:
min_on_percent: "0.5"
default_on_percent: "0.1"
delay_min: 60
target:
entity_id : climate.my_thermostat
```
## ByPass Window Check
This service is used to bypass the window check implemented to stop thermostat when an open window is detected.
When set to ``true`` window event won't have any effect on the thermostat, when set back to ``false`` it will make sure to disable the thermostat if window is still open.
To change the bypass setting use the following code:
```
service : thermostat_polyvalent.set_window_bypass
data:
window_bypass: true
target:
entity_id : climate.my_thermostat
```
# Notifications
Significant thermostat events are notified via the message bus.
@@ -488,29 +661,33 @@ Custom attributes are the following:
| ``[eco/comfort/boost]_temp`` | The temperature configured for the preset xxx |
| ``[eco/comfort/boost]_away_temp`` | The temperature configured for the preset xxx when presence is off or not_home |
| ``power_temp`` | The temperature used when shedding is detected |
| ``on_percent`` | (deprecated) The percentage on calculated by the TPI algorithm |
| ``on_time_sec`` | (deprecated) The On period in sec. Should be ```on_percent * cycle_min``` |
| ``off_time_sec`` | (deprecated) The Off period in sec. Should be ```(1 - on_percent) * cycle_min``` |
| ``on_percent`` | The percentage on calculated by the TPI algorithm |
| ``on_time_sec`` | The On period in sec. Should be ```on_percent * cycle_min``` |
| ``off_time_sec`` | The Off period in sec. Should be ```(1 - on_percent) * cycle_min``` |
| ``cycle_min`` | The calculation cycle in minutes |
| ``function`` | The algorithm used for cycle calculation |
| ``tpi_coef_int`` | The ``coef_int`` of the TPI algorithm |
| ``tpi_coef_ext`` | The ``coef_ext`` of the TPI algorithm |
| ``saved_preset_mode`` | The last preset used before automatic switch of the preset |
| ``saved_target_temp`` | The last temperature used before automatic switching |
| ``window_state`` | (deprecated) The last known state of the window sensor. None if window is not configured |
| ``motion_state`` | (deprecated) The last known state of the motion sensor. None if motion is not configured |
| ``overpowering_state`` | (deprecated) The last known state of the overpowering sensor. None if power management is not configured |
| ``presence_state`` | (deprecated) The last known state of the presence sensor. None if presence management is not configured |
| ``window_state`` | The last known state of the window sensor. None if window is not configured |
| ``window_bypass_state`` | True if the bypass of the window detection is activated |
| ``motion_state`` | The last known state of the motion sensor. None if motion is not configured |
| ``overpowering_state`` | The last known state of the overpowering sensor. None if power management is not configured |
| ``presence_state`` | The last known state of the presence sensor. None if presence management is not configured |
| ``security_delay_min`` | The delay before setting the security mode when temperature sensor are off |
| ``security_min_on_percent`` | The minimal on_percent below which security preset won't be trigger |
| ``security_default_on_percent`` | The on_percent used when thermostat is in ``security`` |
| ``last_temperature_datetime`` | (deprecated) The date and time in ISO8866 format of the last internal temperature reception |
| ``last_ext_temperature_datetime`` | (deprecated) The date and time in ISO8866 format of the last external temperature reception |
| ``security_state`` | (deprecated) The security state. true or false |
| ``last_temperature_datetime`` | The date and time in ISO8866 format of the last internal temperature reception |
| ``last_ext_temperature_datetime`` | The date and time in ISO8866 format of the last external temperature reception |
| ``security_state`` | The security state. true or false |
| ``minimal_activation_delay_sec`` | The minimal activation delay in seconds |
| ``last_update_datetime`` | The date and time in ISO8866 format of this state |
| ``friendly_name`` | The name of the thermostat |
| ``supported_features`` | A combination of all features supported by this thermostat. See official climate integration documentation for more informations |
| ``valve_open_percent`` | The opening percentage of the valve |
| ``regulated_target_temperature`` | The self-regulated target temperature calculated |
| ``is_inversed`` | True if the command is inversed (pilot wire with diode) |
# Some results
@@ -641,11 +818,21 @@ series:
name: Current temp
curve: smooth
yaxis_id: left
- entity: climate.thermostat_mythermostat
- entity: climate.thermostat_mythermostat <--- for over_switch
attribute: on_percent
name: Power percent
curve: stepline
yaxis_id: right
- entity: climate.thermostat_mythermostat <--- for over_thermostast
attribute: regulated_target_temperature
name: Regulated temperature
curve: stepline
yaxis_id: left
- entity: climate.thermostat_mythermostat <--- for over_valve
attribute: valve_open_percent
name: Valve open percent
curve: stepline
yaxis_id: right
```
## And always better and better with the NOTIFIER daemon app to notify events

View File

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

View File

@@ -8,7 +8,7 @@ import logging
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from .climate import VersatileThermostat
from .base_thermostat import BaseThermostat
from .const import DOMAIN, PLATFORMS

File diff suppressed because it is too large Load Diff

View File

@@ -3,7 +3,7 @@ import logging
from homeassistant.core import HomeAssistant, callback, Event
from homeassistant.const import STATE_ON
from homeassistant.const import STATE_ON, STATE_OFF
from homeassistant.components.binary_sensor import (
BinarySensorEntity,
@@ -38,7 +38,7 @@ async def async_setup_entry(
unique_id = entry.entry_id
name = entry.data.get(CONF_NAME)
entities = [SecurityBinarySensor(hass, unique_id, name, entry.data)]
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):
@@ -54,7 +54,9 @@ 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, entry_infos
) -> None: # pylint: disable=unused-argument
"""Initialize the SecurityState Binary sensor"""
super().__init__(hass, unique_id, entry_infos.get(CONF_NAME))
self._attr_name = "Security state"
@@ -87,7 +89,9 @@ 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, entry_infos
) -> None: # pylint: disable=unused-argument
"""Initialize the OverpoweringState Binary sensor"""
super().__init__(hass, unique_id, entry_infos.get(CONF_NAME))
self._attr_name = "Overpowering state"
@@ -120,7 +124,9 @@ 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, entry_infos
) -> None: # pylint: disable=unused-argument
"""Initialize the WindowState Binary sensor"""
super().__init__(hass, unique_id, entry_infos.get(CONF_NAME))
self._attr_name = "Window state"
@@ -133,12 +139,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 +170,9 @@ 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, entry_infos
) -> None: # pylint: disable=unused-argument
"""Initialize the MotionState Binary sensor"""
super().__init__(hass, unique_id, entry_infos.get(CONF_NAME))
self._attr_name = "Motion state"
@@ -171,9 +184,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 +206,9 @@ 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, entry_infos
) -> None: # pylint: disable=unused-argument
"""Initialize the PresenceState Binary sensor"""
super().__init__(hass, unique_id, entry_infos.get(CONF_NAME))
self._attr_name = "Presence state"
@@ -204,9 +221,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 +238,38 @@ class PresenceBinarySensor(VersatileThermostatBaseEntity, BinarySensorEntity):
return "mdi:home-account"
else:
return "mdi:nature-people"
#PR - Adding Window ByPass
class WindowByPassBinarySensor(VersatileThermostatBaseEntity, BinarySensorEntity):
"""Representation of a BinarySensor which exposes the Window ByPass state"""
def __init__(
self, hass: HomeAssistant, unique_id, name, entry_infos
) -> None: # pylint: disable=unused-argument
"""Initialize the WindowByPass Binary sensor"""
super().__init__(hass, unique_id, entry_infos.get(CONF_NAME))
self._attr_name = "Window bypass"
self._attr_unique_id = f"{self._device_name}_window_bypass_state"
self._attr_is_on = False
@callback
async def async_my_climate_changed(self, event: Event = None):
"""Called when my climate have change"""
_LOGGER.debug("%s - climate state change", self._attr_unique_id)
old_state = self._attr_is_on
if self.my_climate.window_bypass_state in [True, False]:
self._attr_is_on = self.my_climate.window_bypass_state
if old_state != self._attr_is_on:
self.async_write_ha_state()
return
@property
def device_class(self) -> BinarySensorDeviceClass | None:
return BinarySensorDeviceClass.RUNNING
@property
def icon(self) -> str | None:
if self._attr_is_on:
return "mdi:window-shutter-cog"
else:
return "mdi:window-shutter-auto"

File diff suppressed because it is too large Load Diff

View File

@@ -1,22 +1,55 @@
""" Some usefull commons class """
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, DeviceInfo, DeviceEntryType
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 .base_thermostat import BaseThermostat
from .const import DOMAIN, DEVICE_MANUFACTURER
_LOGGER = logging.getLogger(__name__)
def get_tz(hass: HomeAssistant):
"""Get the current timezone"""
return dt_util.get_time_zone(hass.config.time_zone)
class NowClass:
""" For testing purpose only"""
@staticmethod
def get_now(hass: HomeAssistant) -> datetime:
""" A test function to get the now.
For testing purpose this method can be overriden to get a specific
timestamp.
"""
return datetime.now( get_tz(hass))
def round_to_nearest(n:float, x: float)->float:
""" Round a number to the nearest x (which should be decimal but not null)
Example:
nombre1 = 3.2
nombre2 = 4.7
x = 0.3
nombre_arrondi1 = round_to_nearest(nombre1, x)
nombre_arrondi2 = round_to_nearest(nombre2, x)
print(nombre_arrondi1) # Output: 3.3
print(nombre_arrondi2) # Output: 4.6
"""
assert x > 0
return round(n * (1/x)) / (1/x)
class VersatileThermostatBaseEntity(Entity):
"""A base class for all entities"""
_my_climate: VersatileThermostat
_my_climate: BaseThermostat
hass: HomeAssistant
_config_id: str
_device_name: str
@@ -36,7 +69,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()
@@ -53,7 +86,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]
@@ -97,7 +130,7 @@ 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
"""

View File

@@ -1,3 +1,5 @@
# pylint: disable=line-too-long, too-many-lines, invalid-name
"""Config flow for Versatile Thermostat integration."""
from __future__ import annotations
@@ -24,6 +26,7 @@ from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers import selector
from homeassistant.components.climate import ClimateEntity, DOMAIN as CLIMATE_DOMAIN
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
from homeassistant.components.number import DOMAIN as NUMBER_DOMAIN
from homeassistant.components.input_boolean import (
DOMAIN as INPUT_BOOLEAN_DOMAIN,
)
@@ -55,13 +58,16 @@ from .const import (
CONF_WINDOW_AUTO_OPEN_THRESHOLD,
CONF_MOTION_SENSOR,
CONF_MOTION_DELAY,
CONF_MOTION_OFF_DELAY,
CONF_MOTION_PRESET,
CONF_NO_MOTION_PRESET,
CONF_DEVICE_POWER,
CONF_CYCLE_MIN,
CONF_PRESET_POWER,
CONF_PRESETS,
CONF_PRESETS_WITH_AC,
CONF_PRESETS_AWAY,
CONF_PRESETS_AWAY_WITH_AC,
CONF_PRESETS_SELECTIONABLE,
CONF_PROP_FUNCTION,
CONF_TPI_COEF_EXT,
@@ -79,11 +85,26 @@ from .const import (
CONF_THERMOSTAT_TYPE,
CONF_THERMOSTAT_SWITCH,
CONF_CLIMATE,
CONF_CLIMATE_2,
CONF_CLIMATE_3,
CONF_CLIMATE_4,
CONF_USE_WINDOW_FEATURE,
CONF_USE_MOTION_FEATURE,
CONF_USE_PRESENCE_FEATURE,
CONF_USE_POWER_FEATURE,
CONF_AC_MODE,
CONF_THERMOSTAT_TYPES,
CONF_THERMOSTAT_VALVE,
CONF_VALVE,
CONF_VALVE_2,
CONF_VALVE_3,
CONF_VALVE_4,
CONF_AUTO_REGULATION_MODES,
CONF_AUTO_REGULATION_MODE,
CONF_AUTO_REGULATION_NONE,
CONF_AUTO_REGULATION_DTEMP,
CONF_AUTO_REGULATION_PERIOD_MIN,
CONF_INVERSE_SWITCH,
UnknownEntity,
WindowOpenDetectionMethod,
)
@@ -132,35 +153,6 @@ def add_suggested_values_to_schema(
return vol.Schema(schema)
# def is_temperature_sensor(sensor: RegistryEntry):
# """Check if a registryEntry is a temperature sensor or assimilable to a temperature sensor"""
# if not sensor.entity_id.startswith(
# INPUT_NUMBER_DOMAIN
# ) and not sensor.entity_id.startswith(SENSOR_DOMAIN):
# return False
# return (
# sensor.device_class == TEMPERATURE
# or sensor.original_device_class == TEMPERATURE
# or sensor.unit_of_measurement in TEMPERATURE_UNITS
# )
#
#
# def is_power_sensor(sensor: RegistryEntry):
# """Check if a registryEntry is a power sensor or assimilable to a temperature sensor"""
# if not sensor.entity_id.startswith(
# INPUT_NUMBER_DOMAIN
# ) and not sensor.entity_id.startswith(SENSOR_DOMAIN):
# return False
# return (
# sensor.unit_of_measurement
# in [
# UnitOfPower.KILO_WATT,
# UnitOfPower.WATT,
# UnitOfPower.BTU_PER_HOUR,
# ]
# )
class VersatileThermostatBaseConfigFlow(FlowHandler):
"""The base Config flow class. Used to put some code in commons."""
@@ -249,6 +241,8 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
PROPORTIONAL_FUNCTION_TPI,
]
),
vol.Optional(CONF_AC_MODE, default=False): cv.boolean,
vol.Optional(CONF_INVERSE_SWITCH, default=False): cv.boolean,
}
)
@@ -257,6 +251,59 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
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
}
)
self.STEP_THERMOSTAT_VALVE = vol.Schema( # pylint: disable=invalid-name
{
vol.Required(CONF_VALVE): selector.EntitySelector(
selector.EntitySelectorConfig(
domain=[NUMBER_DOMAIN, INPUT_NUMBER_DOMAIN]
),
),
vol.Optional(CONF_VALVE_2): selector.EntitySelector(
selector.EntitySelectorConfig(
domain=[NUMBER_DOMAIN, INPUT_NUMBER_DOMAIN]
),
),
vol.Optional(CONF_VALVE_3): selector.EntitySelector(
selector.EntitySelectorConfig(
domain=[NUMBER_DOMAIN, INPUT_NUMBER_DOMAIN]
),
),
vol.Optional(CONF_VALVE_4): selector.EntitySelector(
selector.EntitySelectorConfig(
domain=[NUMBER_DOMAIN, INPUT_NUMBER_DOMAIN]
),
),
vol.Required(
CONF_PROP_FUNCTION, default=PROPORTIONAL_FUNCTION_TPI
): vol.In(
[
PROPORTIONAL_FUNCTION_TPI,
]
),
vol.Optional(CONF_AC_MODE, default=False): cv.boolean,
}
)
@@ -274,6 +321,15 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
}
)
self.STEP_PRESETS_WITH_AC_DATA_SCHEMA = ( # pylint: disable=invalid-name
vol.Schema( # pylint: disable=invalid-name
{
vol.Optional(v, default=0.0): vol.Coerce(float)
for (k, v) in CONF_PRESETS_WITH_AC.items()
}
)
)
self.STEP_WINDOW_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name
{
vol.Optional(CONF_WINDOW_SENSOR): selector.EntitySelector(
@@ -296,6 +352,7 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
),
),
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
),
@@ -340,6 +397,27 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
}
)
self.STEP_PRESENCE_WITH_AC_DATA_SCHEMA = ( # pylint: disable=invalid-name
vol.Schema(
{
vol.Optional(CONF_PRESENCE_SENSOR): selector.EntitySelector(
selector.EntitySelectorConfig(
domain=[
PERSON_DOMAIN,
BINARY_SENSOR_DOMAIN,
INPUT_BOOLEAN_DOMAIN,
]
),
),
}
).extend(
{
vol.Optional(v, default=17): vol.Coerce(float)
for (k, v) in CONF_PRESETS_AWAY_WITH_AC.items()
}
)
)
self.STEP_ADVANCED_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name
{
vol.Required(
@@ -459,6 +537,10 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
return await self.generic_step(
"type", self.STEP_THERMOSTAT_SWITCH, user_input, self.async_step_tpi
)
elif self._infos[CONF_THERMOSTAT_TYPE] == CONF_THERMOSTAT_VALVE:
return await self.generic_step(
"type", self.STEP_THERMOSTAT_VALVE, user_input, self.async_step_tpi
)
else:
return await self.generic_step(
"type",
@@ -489,9 +571,12 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
elif self._infos[CONF_USE_PRESENCE_FEATURE]:
next_step = self.async_step_presence
return await self.generic_step(
"presets", self.STEP_PRESETS_DATA_SCHEMA, user_input, next_step
)
if self._infos.get(CONF_AC_MODE) is True:
schema = self.STEP_PRESETS_WITH_AC_DATA_SCHEMA
else:
schema = self.STEP_PRESETS_DATA_SCHEMA
return await self.generic_step("presets", schema, user_input, next_step)
async def async_step_window(self, user_input: dict | None = None) -> FlowResult:
"""Handle the window sensor flow steps"""
@@ -542,9 +627,14 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
"""Handle the presence management flow steps"""
_LOGGER.debug("Into ConfigFlow.async_step_presence user_input=%s", user_input)
if self._infos.get(CONF_AC_MODE) is True:
schema = self.STEP_PRESENCE_WITH_AC_DATA_SCHEMA
else:
schema = self.STEP_PRESENCE_DATA_SCHEMA
return await self.generic_step(
"presence",
self.STEP_PRESENCE_DATA_SCHEMA,
schema,
user_input,
self.async_step_advanced,
)
@@ -642,6 +732,10 @@ class VersatileThermostatOptionsFlowHandler(
return await self.generic_step(
"type", self.STEP_THERMOSTAT_SWITCH, user_input, self.async_step_tpi
)
elif self._infos[CONF_THERMOSTAT_TYPE] == CONF_THERMOSTAT_VALVE:
return await self.generic_step(
"type", self.STEP_THERMOSTAT_VALVE, user_input, self.async_step_tpi
)
else:
return await self.generic_step(
"type",
@@ -676,9 +770,12 @@ class VersatileThermostatOptionsFlowHandler(
elif self._infos[CONF_USE_PRESENCE_FEATURE]:
next_step = self.async_step_presence
return await self.generic_step(
"presets", self.STEP_PRESETS_DATA_SCHEMA, user_input, next_step
)
if self._infos.get(CONF_AC_MODE) is True:
schema = self.STEP_PRESETS_WITH_AC_DATA_SCHEMA
else:
schema = self.STEP_PRESETS_DATA_SCHEMA
return await self.generic_step("presets", schema, user_input, next_step)
async def async_step_window(self, user_input: dict | None = None) -> FlowResult:
"""Handle the window sensor flow steps"""
@@ -736,9 +833,14 @@ class VersatileThermostatOptionsFlowHandler(
"Into OptionsFlowHandler.async_step_presence user_input=%s", user_input
)
if self._infos.get(CONF_AC_MODE) is True:
schema = self.STEP_PRESENCE_WITH_AC_DATA_SCHEMA
else:
schema = self.STEP_PRESENCE_DATA_SCHEMA
return await self.generic_step(
"presence",
self.STEP_PRESENCE_DATA_SCHEMA,
schema,
user_input,
self.async_step_advanced,
)

View File

@@ -1,3 +1,4 @@
# pylint: disable=line-too-long
"""Constants for the Versatile Thermostat integration."""
from enum import Enum
@@ -16,6 +17,11 @@ from homeassistant.exceptions import HomeAssistantError
from .prop_algorithm import (
PROPORTIONAL_FUNCTION_TPI,
)
PRESET_AC_SUFFIX = "_ac"
PRESET_ECO_AC = PRESET_ECO + PRESET_AC_SUFFIX
PRESET_COMFORT_AC = PRESET_COMFORT + PRESET_AC_SUFFIX
PRESET_BOOST_AC = PRESET_BOOST + PRESET_AC_SUFFIX
DEVICE_MANUFACTURER = "JMCOLLIN"
DEVICE_MODEL = "Versatile Thermostat"
@@ -44,6 +50,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"
@@ -59,14 +66,31 @@ CONF_SECURITY_DEFAULT_ON_PERCENT = "security_default_on_percent"
CONF_THERMOSTAT_TYPE = "thermostat_type"
CONF_THERMOSTAT_SWITCH = "thermostat_over_switch"
CONF_THERMOSTAT_CLIMATE = "thermostat_over_climate"
CONF_THERMOSTAT_VALVE = "thermostat_over_valve"
CONF_CLIMATE = "climate_entity_id"
CONF_CLIMATE_2 = "climate_entity2_id"
CONF_CLIMATE_3 = "climate_entity3_id"
CONF_CLIMATE_4 = "climate_entity4_id"
CONF_USE_WINDOW_FEATURE = "use_window_feature"
CONF_USE_MOTION_FEATURE = "use_motion_feature"
CONF_USE_PRESENCE_FEATURE = "use_presence_feature"
CONF_USE_POWER_FEATURE = "use_power_feature"
CONF_AC_MODE = "ac_mode"
CONF_WINDOW_AUTO_OPEN_THRESHOLD = "window_auto_open_threshold"
CONF_WINDOW_AUTO_CLOSE_THRESHOLD = "window_auto_close_threshold"
CONF_WINDOW_AUTO_MAX_DURATION = "window_auto_max_duration"
CONF_VALVE = "valve_entity_id"
CONF_VALVE_2 = "valve_entity2_id"
CONF_VALVE_3 = "valve_entity3_id"
CONF_VALVE_4 = "valve_entity4_id"
CONF_AUTO_REGULATION_MODE= "auto_regulation_mode"
CONF_AUTO_REGULATION_NONE= "auto_regulation_none"
CONF_AUTO_REGULATION_LIGHT= "auto_regulation_light"
CONF_AUTO_REGULATION_MEDIUM= "auto_regulation_medium"
CONF_AUTO_REGULATION_STRONG= "auto_regulation_strong"
CONF_AUTO_REGULATION_DTEMP="auto_regulation_dtemp"
CONF_AUTO_REGULATION_PERIOD_MIN="auto_regulation_periode_min"
CONF_INVERSE_SWITCH="inverse_switch_command"
CONF_PRESETS = {
p: f"{p}_temp"
@@ -77,14 +101,39 @@ CONF_PRESETS = {
)
}
CONF_PRESETS_WITH_AC = {
p: f"{p}_temp"
for p in (
PRESET_ECO,
PRESET_COMFORT,
PRESET_BOOST,
PRESET_ECO_AC,
PRESET_COMFORT_AC,
PRESET_BOOST_AC,
)
}
PRESET_AWAY_SUFFIX = "_away"
CONF_PRESETS_AWAY = {
p: f"{p}_temp"
for p in (
PRESET_ECO + PRESET_AWAY_SUFFIX,
PRESET_BOOST + PRESET_AWAY_SUFFIX,
PRESET_COMFORT + PRESET_AWAY_SUFFIX,
PRESET_BOOST + PRESET_AWAY_SUFFIX,
)
}
CONF_PRESETS_AWAY_WITH_AC = {
p: f"{p}_temp"
for p in (
PRESET_ECO + PRESET_AWAY_SUFFIX,
PRESET_COMFORT + PRESET_AWAY_SUFFIX,
PRESET_BOOST + PRESET_AWAY_SUFFIX,
PRESET_ECO_AC + PRESET_AWAY_SUFFIX,
PRESET_COMFORT_AC + PRESET_AWAY_SUFFIX,
PRESET_BOOST_AC + PRESET_AWAY_SUFFIX,
)
}
@@ -92,11 +141,16 @@ CONF_PRESETS_SELECTIONABLE = [PRESET_ECO, PRESET_COMFORT, PRESET_BOOST]
CONF_PRESETS_VALUES = list(CONF_PRESETS.values())
CONF_PRESETS_AWAY_VALUES = list(CONF_PRESETS_AWAY.values())
CONF_PRESETS_WITH_AC_VALUES = list(CONF_PRESETS_WITH_AC.values())
CONF_PRESETS_AWAY_WITH_AC_VALUES = list(CONF_PRESETS_AWAY_WITH_AC.values())
ALL_CONF = (
[
CONF_NAME,
CONF_HEATER,
CONF_HEATER_2,
CONF_HEATER_3,
CONF_HEATER_4,
CONF_TEMP_SENSOR,
CONF_EXTERNAL_TEMP_SENSOR,
CONF_POWER_SENSOR,
@@ -126,26 +180,44 @@ ALL_CONF = (
CONF_THERMOSTAT_SWITCH,
CONF_THERMOSTAT_CLIMATE,
CONF_CLIMATE,
CONF_CLIMATE_2,
CONF_CLIMATE_3,
CONF_CLIMATE_4,
CONF_USE_WINDOW_FEATURE,
CONF_USE_MOTION_FEATURE,
CONF_USE_PRESENCE_FEATURE,
CONF_USE_POWER_FEATURE,
CONF_AC_MODE,
CONF_VALVE,
CONF_VALVE_2,
CONF_VALVE_3,
CONF_VALVE_4,
CONF_AUTO_REGULATION_MODE,
CONF_AUTO_REGULATION_DTEMP,
CONF_AUTO_REGULATION_PERIOD_MIN,
CONF_INVERSE_SWITCH
]
+ CONF_PRESETS_VALUES
+ CONF_PRESETS_AWAY_VALUES,
+ CONF_PRESETS_AWAY_VALUES
+ CONF_PRESETS_WITH_AC_VALUES
+ CONF_PRESETS_AWAY_WITH_AC_VALUES,
)
CONF_FUNCTIONS = [
PROPORTIONAL_FUNCTION_TPI,
]
CONF_THERMOSTAT_TYPES = [CONF_THERMOSTAT_SWITCH, CONF_THERMOSTAT_CLIMATE]
CONF_AUTO_REGULATION_MODES = [CONF_AUTO_REGULATION_NONE, CONF_AUTO_REGULATION_LIGHT, CONF_AUTO_REGULATION_MEDIUM, CONF_AUTO_REGULATION_STRONG]
CONF_THERMOSTAT_TYPES = [CONF_THERMOSTAT_SWITCH, CONF_THERMOSTAT_CLIMATE, CONF_THERMOSTAT_VALVE]
SUPPORT_FLAGS = ClimateEntityFeature.TARGET_TEMPERATURE
SERVICE_SET_PRESENCE = "set_presence"
SERVICE_SET_PRESET_TEMPERATURE = "set_preset_temperature"
SERVICE_SET_SECURITY = "set_security"
SERVICE_SET_WINDOW_BYPASS = "set_window_bypass"
SERVICE_SET_AUTO_REGULATION_MODE = "set_auto_regulation_mode"
DEFAULT_SECURITY_MIN_ON_PERCENT = 0.5
DEFAULT_SECURITY_DEFAULT_ON_PERCENT = 0.1
@@ -153,6 +225,44 @@ DEFAULT_SECURITY_DEFAULT_ON_PERCENT = 0.1
ATTR_TOTAL_ENERGY = "total_energy"
ATTR_MEAN_POWER_CYCLE = "mean_cycle_power"
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:
""" Medium parameters for regulation"""
kp:float = 0.4
ki:float = 0.08
k_ext:float = 0.1
offset_max:float = 3
stabilization_threshold:float = 0.1
accumulated_error_threshold:float = 25
# 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"""
@@ -171,3 +281,14 @@ class UnknownEntity(HomeAssistantError):
class WindowOpenDetectionMethod(HomeAssistantError):
"""Error to indicate there is an error in the window open detection method given."""
class overrides: # pylint: disable=invalid-name
""" An annotation to inform overrides """
def __init__(self, func):
self.func = func
def __get__(self, instance, owner):
return self.func.__get__(instance, owner)
def __call__(self, *args, **kwargs):
raise RuntimeError(f"Method {self.func.__name__} should have been overridden")

View File

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

View File

@@ -0,0 +1,75 @@
# 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_target_temp(self, target_temp):
""" Set the new target_temp"""
self.target_temp = target_temp
# Do not reset the accumulated error
# 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 or external_temp is None:
_LOGGER.warning("Internal_temp or external_temp are not set. Regulation will be suspended")
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
offset_ext = self.k_ext * (self.target_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
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

@@ -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 = (

View File

@@ -1 +0,0 @@
homeassistant

View File

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

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,6 +24,8 @@ from .const import (
CONF_PROP_FUNCTION,
PROPORTIONAL_FUNCTION_TPI,
CONF_THERMOSTAT_SWITCH,
CONF_THERMOSTAT_VALVE,
CONF_THERMOSTAT_CLIMATE,
CONF_THERMOSTAT_TYPE,
)
@@ -50,7 +54,7 @@ async def async_setup_entry(
]
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 +62,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)
@@ -224,6 +234,47 @@ class OnPercentSensor(VersatileThermostatBaseEntity, SensorEntity):
"""Return the suggested number of decimal digits for display."""
return 1
class ValveOpenPercentSensor(VersatileThermostatBaseEntity, SensorEntity):
"""Representation of a on percent sensor which exposes the on_percent in a cycle"""
def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None:
"""Initialize the energy sensor"""
super().__init__(hass, unique_id, entry_infos.get(CONF_NAME))
self._attr_name = "Vave open percent"
self._attr_unique_id = f"{self._device_name}_valve_open_percent"
@callback
async def async_my_climate_changed(self, event: Event = None):
"""Called when my climate have change"""
_LOGGER.debug("%s - climate state change", self._attr_unique_id)
old_state = self._attr_native_value
self._attr_native_value = self.my_climate.valve_open_percent
if old_state != self._attr_native_value:
self.async_write_ha_state()
return
@property
def icon(self) -> str | None:
return "mdi:pipe-valve"
@property
def device_class(self) -> SensorDeviceClass | None:
return SensorDeviceClass.POWER_FACTOR
@property
def state_class(self) -> SensorStateClass | None:
return SensorStateClass.MEASUREMENT
@property
def native_unit_of_measurement(self) -> str | None:
return PERCENTAGE
@property
def suggested_display_precision(self) -> int | None:
"""Return the suggested number of decimal digits for display."""
return 0
class OnTimeSensor(VersatileThermostatBaseEntity, SensorEntity):
"""Representation of a on time sensor which exposes the on_time_sec in a cycle"""
@@ -424,3 +475,53 @@ class TemperatureSlopeSensor(VersatileThermostatBaseEntity, SensorEntity):
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)
if math.isnan(self.my_climate.regulated_target_temp) or math.isinf(
self.my_climate.regulated_target_temp
):
raise ValueError(f"Sensor has illegal state {self.my_climate.regulated_target_temp}")
old_state = self._attr_native_value
self._attr_native_value = round(
self.my_climate.regulated_target_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

View File

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

View File

@@ -14,7 +14,7 @@
"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_window_feature": "Use window detection",
"use_motion_feature": "Use motion detection",
"use_power_feature": "Use power management",
@@ -25,12 +25,24 @@
"title": "Linked entities",
"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": "1rst heater switch",
"heater_entity2_id": "2nd heater switch",
"heater_entity3_id": "3rd heater switch",
"heater_entity4_id": "4th heater switch",
"proportional_function": "Algorithm",
"climate_entity_id": "Underlying thermostat"
"climate_entity_id": "1rst underlying climate",
"climate_entity2_id": "2nd underlying climate",
"climate_entity3_id": "3rd underlying climate",
"climate_entity4_id": "4th underlying climate",
"ac_mode": "AC mode",
"valve_entity_id": "1rst valve number",
"valve_entity2_id": "2nd valve number",
"valve_entity3_id": "3rd valve number",
"valve_entity4_id": "4th valve number",
"auto_regulation_mode": "Self-regulation",
"auto_regulation_dtemp": "Regulation threshold",
"auto_regulation_periode_min": "Regulation minimal period",
"inverse_switch_command": "Inverse switch command"
},
"data_description": {
"heater_entity_id": "Mandatory heater entity id",
@@ -38,7 +50,19 @@
"heater_entity3_id": "Optional 3rd Heater entity id. Leave empty if not used",
"heater_entity4_id": "Optional 4th Heater entity id. Leave empty if not used",
"proportional_function": "Algorithm to use (TPI is the only one for now)",
"climate_entity_id": "Underlying climate entity id"
"climate_entity_id": "Underlying climate entity id",
"climate_entity2_id": "2nd underlying climate entity id",
"climate_entity3_id": "3rd underlying climate entity id",
"climate_entity4_id": "4th underlying climate entity id",
"ac_mode": "Use the Air Conditioning (AC) mode",
"valve_entity_id": "1rst valve number entity id",
"valve_entity2_id": "2nd valve number entity id",
"valve_entity3_id": "3rd valve number entity id",
"valve_entity4_id": "4th valve number entity id",
"auto_regulation_mode": "Auto adjustment of the target temperature",
"auto_regulation_dtemp": "The threshold in ° under which the temperature change will not be send",
"auto_regulation_periode_min": "Duration in minutes between two regulation update",
"inverse_switch_command": "For switch with pilot wire and diode you may need to inverse the command"
}
},
"tpi": {
@@ -55,7 +79,10 @@
"data": {
"eco_temp": "Temperature in Eco preset",
"comfort_temp": "Temperature in Comfort preset",
"boost_temp": "Temperature in Boost preset"
"boost_temp": "Temperature in Boost preset",
"eco_ac_temp": "Temperature in Eco preset for AC mode",
"comfort_ac_temp": "Temperature in Comfort preset for AC mode",
"boost_ac_temp": "Temperature in Boost preset for AC mode"
}
},
"window": {
@@ -81,7 +108,15 @@
"description": "Motion sensor management. Preset can switch automatically depending of a motion detection\nLeave corresponding entity_id empty if not used.\nmotion_preset and no_motion_preset should be set to the corresponding preset name",
"data": {
"motion_sensor_entity_id": "Motion sensor entity id",
"motion_delay": "Motion delay (seconds)",
"motion_delay": "Activation delay",
"motion_off_delay": "Deactivation delay",
"motion_preset": "Motion preset",
"no_motion_preset": "No motion preset"
},
"data_description": {
"motion_sensor_entity_id": "The entity id of the motion sensor",
"motion_delay": "Motion activation activation delay (seconds)",
"motion_off_delay": "Motion deactivation delay (seconds)",
"motion_preset": "Preset to use when motion is detected",
"no_motion_preset": "Preset to use when no motion is detected"
}
@@ -99,10 +134,13 @@
"title": "Presence management",
"description": "Presence management attributes.\nGives the a presence sensor of your home (true is someone is present).\nThen specify either the preset to use when presence sensor is false or the offset in temperature to apply.\nIf preset is given, the offset will not be used.\nLeave corresponding entity_id empty if not used.",
"data": {
"presence_sensor_entity_id": "Presence sensor entity id (true is present)",
"presence_sensor_entity_id": "Presence sensor entity id",
"eco_away_temp": "Temperature in Eco preset when no presence",
"comfort_away_temp": "Temperature in Comfort preset when no presence",
"boost_away_temp": "Temperature in Boost preset when no presence"
"boost_away_temp": "Temperature in Boost preset when no presence",
"eco_ac_away_temp": "Temperature in Eco preset when no presence in AC mode",
"comfort_ac_away_temp": "Temperature in Comfort preset when no presence in AC mode",
"boost_ac_away_temp": "Temperature in Boost preset when no presence in AC mode"
}
},
"advanced": {
@@ -111,7 +149,7 @@
"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_min_on_percent": "Minimal power percent to enable security mode",
"security_default_on_percent": "Power percent to use in security mode"
},
"data_description": {
@@ -156,12 +194,24 @@
"title": "Linked entities",
"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": "1rst heater switch",
"heater_entity2_id": "2nd heater switch",
"heater_entity3_id": "3rd heater switch",
"heater_entity4_id": "4th heater switch",
"proportional_function": "Algorithm",
"climate_entity_id": "Underlying thermostat"
"climate_entity_id": "1rst underlying climate",
"climate_entity2_id": "2nd underlying climate",
"climate_entity3_id": "3rd underlying climate",
"climate_entity4_id": "4th underlying climate",
"ac_mode": "AC mode",
"valve_entity_id": "1rst valve number",
"valve_entity2_id": "2nd valve number",
"valve_entity3_id": "3rd valve number",
"valve_entity4_id": "4th valve number",
"auto_regulation_mode": "Self-regulation",
"auto_regulation_dtemp": "Regulation threshold",
"auto_regulation_periode_min": "Regulation minimal period",
"inverse_switch_command": "Inverse switch command"
},
"data_description": {
"heater_entity_id": "Mandatory heater entity id",
@@ -169,7 +219,19 @@
"heater_entity3_id": "Optional 3rd Heater entity id. Leave empty if not used",
"heater_entity4_id": "Optional 4th Heater entity id. Leave empty if not used",
"proportional_function": "Algorithm to use (TPI is the only one for now)",
"climate_entity_id": "Underlying climate entity id"
"climate_entity_id": "Underlying climate entity id",
"climate_entity2_id": "2nd underlying climate entity id",
"climate_entity3_id": "3rd underlying climate entity id",
"climate_entity4_id": "4th underlying climate entity id",
"ac_mode": "Use the Air Conditioning (AC) mode",
"valve_entity_id": "1rst valve number entity id",
"valve_entity2_id": "2nd valve number entity id",
"valve_entity3_id": "3rd valve number entity id",
"valve_entity4_id": "4th valve number entity id",
"auto_regulation_mode": "Auto adjustment of the target temperature",
"auto_regulation_dtemp": "The threshold in ° under which the temperature change will not be send",
"auto_regulation_periode_min": "Duration in minutes between two regulation update",
"inverse_switch_command": "For switch with pilot wire and diode you may need to inverse the command"
}
},
"tpi": {
@@ -186,7 +248,10 @@
"data": {
"eco_temp": "Temperature in Eco preset",
"comfort_temp": "Temperature in Comfort preset",
"boost_temp": "Temperature in Boost preset"
"boost_temp": "Temperature in Boost preset",
"eco_ac_temp": "Temperature in Eco preset for AC mode",
"comfort_ac_temp": "Temperature in Comfort preset for AC mode",
"boost_ac_temp": "Temperature in Boost preset for AC mode"
}
},
"window": {
@@ -212,7 +277,15 @@
"description": "Motion sensor management. Preset can switch automatically depending of a motion detection\nLeave corresponding entity_id empty if not used.\nmotion_preset and no_motion_preset should be set to the corresponding preset name",
"data": {
"motion_sensor_entity_id": "Motion sensor entity id",
"motion_delay": "Motion delay (seconds)",
"motion_delay": "Activation delay",
"motion_off_delay": "Deactivation delay",
"motion_preset": "Motion preset",
"no_motion_preset": "No motion preset"
},
"data_description": {
"motion_sensor_entity_id": "The entity id of the motion sensor",
"motion_delay": "Motion activation activation delay (seconds)",
"motion_off_delay": "Motion deactivation delay (seconds)",
"motion_preset": "Preset to use when motion is detected",
"no_motion_preset": "Preset to use when no motion is detected"
}
@@ -233,7 +306,10 @@
"presence_sensor_entity_id": "Presence sensor entity id (true is present)",
"eco_away_temp": "Temperature in Eco preset when no presence",
"comfort_away_temp": "Temperature in Comfort preset when no presence",
"boost_away_temp": "Temperature in Boost preset when no presence"
"boost_away_temp": "Temperature in Boost preset when no presence",
"eco_ac_away_temp": "Temperature in Eco preset when no presence in AC mode",
"comfort_ac_away_temp": "Temperature in Comfort preset when no presence in AC mode",
"boost_ac_away_temp": "Temperature in Boost preset when no presence in AC mode"
}
},
"advanced": {
@@ -266,7 +342,16 @@
"thermostat_type": {
"options": {
"thermostat_over_switch": "Thermostat over a switch",
"thermostat_over_climate": "Thermostat over another thermostat"
"thermostat_over_climate": "Thermostat over a climate",
"thermostat_over_valve": "Thermostat over a valve"
}
},
"auto_regulation_mode": {
"options": {
"auto_regulation_strong": "Strong",
"auto_regulation_medium": "Medium",
"auto_regulation_light": "Light",
"auto_regulation_none": "No auto-regulation"
}
}
},

View File

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

View File

@@ -0,0 +1,184 @@
# 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", "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"
}))
# useless for now
# def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None:
# """Initialize the thermostat over switch."""
# super().__init__(hass, unique_id, name, entry_infos)
_is_inversed: bool = None
@property
def is_over_switch(self) -> bool:
""" True if the Thermostat is over_switch"""
return True
@property
def is_inversed(self) -> bool:
""" True if the switch is inversed (for pilot wire and diode)"""
return self._is_inversed is True
@overrides
def post_init(self, entry_infos):
""" Initialize the Thermostat"""
super().post_init(entry_infos)
self._prop_algorithm = PropAlgorithm(
self._proportional_function,
self._tpi_coef_int,
self._tpi_coef_ext,
self._cycle_min,
self._minimal_activation_delay,
)
lst_switches = [entry_infos.get(CONF_HEATER)]
if entry_infos.get(CONF_HEATER_2):
lst_switches.append(entry_infos.get(CONF_HEATER_2))
if entry_infos.get(CONF_HEATER_3):
lst_switches.append(entry_infos.get(CONF_HEATER_3))
if entry_infos.get(CONF_HEATER_4):
lst_switches.append(entry_infos.get(CONF_HEATER_4))
delta_cycle = self._cycle_min * 60 / len(lst_switches)
for idx, switch in enumerate(lst_switches):
self._underlyings.append(
UnderlyingSwitch(
hass=self._hass,
thermostat=self,
switch_entity_id=switch,
initial_delay_sec=idx * delta_cycle,
)
)
self._is_inversed = entry_infos.get(CONF_INVERSE_SWITCH) is True
self._should_relaunch_control_heating = False
@overrides
async def async_added_to_hass(self):
"""Run when entity about to be added."""
_LOGGER.debug("Calling async_added_to_hass")
await super().async_added_to_hass()
# Add listener to all underlying entities
for switch in self._underlyings:
self.async_on_remove(
async_track_state_change_event(
self.hass, [switch.entity_id], self._async_switch_changed
)
)
self.hass.create_task(self.async_control_heating())
@overrides
def update_custom_attributes(self):
""" Custom attributes """
super().update_custom_attributes()
self._attr_extra_state_attributes["is_over_switch"] = self.is_over_switch
self._attr_extra_state_attributes["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[
"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()

View File

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

View File

@@ -14,7 +14,7 @@
"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_window_feature": "Use window detection",
"use_motion_feature": "Use motion detection",
"use_power_feature": "Use power management",
@@ -25,12 +25,24 @@
"title": "Linked entities",
"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": "1rst heater switch",
"heater_entity2_id": "2nd heater switch",
"heater_entity3_id": "3rd heater switch",
"heater_entity4_id": "4th heater switch",
"proportional_function": "Algorithm",
"climate_entity_id": "Underlying thermostat"
"climate_entity_id": "1rst underlying climate",
"climate_entity2_id": "2nd underlying climate",
"climate_entity3_id": "3rd underlying climate",
"climate_entity4_id": "4th underlying climate",
"ac_mode": "AC mode",
"valve_entity_id": "1rst valve number",
"valve_entity2_id": "2nd valve number",
"valve_entity3_id": "3rd valve number",
"valve_entity4_id": "4th valve number",
"auto_regulation_mode": "Self-regulation",
"auto_regulation_dtemp": "Regulation threshold",
"auto_regulation_periode_min": "Regulation minimal period",
"inverse_switch_command": "Inverse switch command"
},
"data_description": {
"heater_entity_id": "Mandatory heater entity id",
@@ -38,7 +50,19 @@
"heater_entity3_id": "Optional 3rd Heater entity id. Leave empty if not used",
"heater_entity4_id": "Optional 4th Heater entity id. Leave empty if not used",
"proportional_function": "Algorithm to use (TPI is the only one for now)",
"climate_entity_id": "Underlying climate entity id"
"climate_entity_id": "Underlying climate entity id",
"climate_entity2_id": "2nd underlying climate entity id",
"climate_entity3_id": "3rd underlying climate entity id",
"climate_entity4_id": "4th underlying climate entity id",
"ac_mode": "Use the Air Conditioning (AC) mode",
"valve_entity_id": "1rst valve number entity id",
"valve_entity2_id": "2nd valve number entity id",
"valve_entity3_id": "3rd valve number entity id",
"valve_entity4_id": "4th valve number entity id",
"auto_regulation_mode": "Auto adjustment of the target temperature",
"auto_regulation_dtemp": "The threshold in ° under which the temperature change will not be send",
"auto_regulation_periode_min": "Duration in minutes between two regulation update",
"inverse_switch_command": "For switch with pilot wire and diode you may need to inverse the command"
}
},
"tpi": {
@@ -55,7 +79,10 @@
"data": {
"eco_temp": "Temperature in Eco preset",
"comfort_temp": "Temperature in Comfort preset",
"boost_temp": "Temperature in Boost preset"
"boost_temp": "Temperature in Boost preset",
"eco_ac_temp": "Temperature in Eco preset for AC mode",
"comfort_ac_temp": "Temperature in Comfort preset for AC mode",
"boost_ac_temp": "Temperature in Boost preset for AC mode"
}
},
"window": {
@@ -81,7 +108,15 @@
"description": "Motion sensor management. Preset can switch automatically depending of a motion detection\nLeave corresponding entity_id empty if not used.\nmotion_preset and no_motion_preset should be set to the corresponding preset name",
"data": {
"motion_sensor_entity_id": "Motion sensor entity id",
"motion_delay": "Motion delay (seconds)",
"motion_delay": "Activation delay",
"motion_off_delay": "Deactivation delay",
"motion_preset": "Motion preset",
"no_motion_preset": "No motion preset"
},
"data_description": {
"motion_sensor_entity_id": "The entity id of the motion sensor",
"motion_delay": "Motion activation activation delay (seconds)",
"motion_off_delay": "Motion deactivation delay (seconds)",
"motion_preset": "Preset to use when motion is detected",
"no_motion_preset": "Preset to use when no motion is detected"
}
@@ -99,10 +134,13 @@
"title": "Presence management",
"description": "Presence management attributes.\nGives the a presence sensor of your home (true is someone is present).\nThen specify either the preset to use when presence sensor is false or the offset in temperature to apply.\nIf preset is given, the offset will not be used.\nLeave corresponding entity_id empty if not used.",
"data": {
"presence_sensor_entity_id": "Presence sensor entity id (true is present)",
"presence_sensor_entity_id": "Presence sensor entity id",
"eco_away_temp": "Temperature in Eco preset when no presence",
"comfort_away_temp": "Temperature in Comfort preset when no presence",
"boost_away_temp": "Temperature in Boost preset when no presence"
"boost_away_temp": "Temperature in Boost preset when no presence",
"eco_ac_away_temp": "Temperature in Eco preset when no presence in AC mode",
"comfort_ac_away_temp": "Temperature in Comfort preset when no presence in AC mode",
"boost_ac_away_temp": "Temperature in Boost preset when no presence in AC mode"
}
},
"advanced": {
@@ -111,7 +149,7 @@
"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_min_on_percent": "Minimal power percent to enable security mode",
"security_default_on_percent": "Power percent to use in security mode"
},
"data_description": {
@@ -156,12 +194,24 @@
"title": "Linked entities",
"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": "1rst heater switch",
"heater_entity2_id": "2nd heater switch",
"heater_entity3_id": "3rd heater switch",
"heater_entity4_id": "4th heater switch",
"proportional_function": "Algorithm",
"climate_entity_id": "Underlying thermostat"
"climate_entity_id": "1rst underlying climate",
"climate_entity2_id": "2nd underlying climate",
"climate_entity3_id": "3rd underlying climate",
"climate_entity4_id": "4th underlying climate",
"ac_mode": "AC mode",
"valve_entity_id": "1rst valve number",
"valve_entity2_id": "2nd valve number",
"valve_entity3_id": "3rd valve number",
"valve_entity4_id": "4th valve number",
"auto_regulation_mode": "Self-regulation",
"auto_regulation_dtemp": "Regulation threshold",
"auto_regulation_periode_min": "Regulation minimal period",
"inverse_switch_command": "Inverse switch command"
},
"data_description": {
"heater_entity_id": "Mandatory heater entity id",
@@ -169,7 +219,19 @@
"heater_entity3_id": "Optional 3rd Heater entity id. Leave empty if not used",
"heater_entity4_id": "Optional 4th Heater entity id. Leave empty if not used",
"proportional_function": "Algorithm to use (TPI is the only one for now)",
"climate_entity_id": "Underlying climate entity id"
"climate_entity_id": "Underlying climate entity id",
"climate_entity2_id": "2nd underlying climate entity id",
"climate_entity3_id": "3rd underlying climate entity id",
"climate_entity4_id": "4th underlying climate entity id",
"ac_mode": "Use the Air Conditioning (AC) mode",
"valve_entity_id": "1rst valve number entity id",
"valve_entity2_id": "2nd valve number entity id",
"valve_entity3_id": "3rd valve number entity id",
"valve_entity4_id": "4th valve number entity id",
"auto_regulation_mode": "Auto adjustment of the target temperature",
"auto_regulation_dtemp": "The threshold in ° under which the temperature change will not be send",
"auto_regulation_periode_min": "Duration in minutes between two regulation update",
"inverse_switch_command": "For switch with pilot wire and diode you may need to inverse the command"
}
},
"tpi": {
@@ -186,7 +248,10 @@
"data": {
"eco_temp": "Temperature in Eco preset",
"comfort_temp": "Temperature in Comfort preset",
"boost_temp": "Temperature in Boost preset"
"boost_temp": "Temperature in Boost preset",
"eco_ac_temp": "Temperature in Eco preset for AC mode",
"comfort_ac_temp": "Temperature in Comfort preset for AC mode",
"boost_ac_temp": "Temperature in Boost preset for AC mode"
}
},
"window": {
@@ -212,7 +277,15 @@
"description": "Motion sensor management. Preset can switch automatically depending of a motion detection\nLeave corresponding entity_id empty if not used.\nmotion_preset and no_motion_preset should be set to the corresponding preset name",
"data": {
"motion_sensor_entity_id": "Motion sensor entity id",
"motion_delay": "Motion delay (seconds)",
"motion_delay": "Activation delay",
"motion_off_delay": "Deactivation delay",
"motion_preset": "Motion preset",
"no_motion_preset": "No motion preset"
},
"data_description": {
"motion_sensor_entity_id": "The entity id of the motion sensor",
"motion_delay": "Motion activation activation delay (seconds)",
"motion_off_delay": "Motion deactivation delay (seconds)",
"motion_preset": "Preset to use when motion is detected",
"no_motion_preset": "Preset to use when no motion is detected"
}
@@ -233,7 +306,10 @@
"presence_sensor_entity_id": "Presence sensor entity id (true is present)",
"eco_away_temp": "Temperature in Eco preset when no presence",
"comfort_away_temp": "Temperature in Comfort preset when no presence",
"boost_away_temp": "Temperature in Boost preset when no presence"
"boost_away_temp": "Temperature in Boost preset when no presence",
"eco_ac_away_temp": "Temperature in Eco preset when no presence in AC mode",
"comfort_ac_away_temp": "Temperature in Comfort preset when no presence in AC mode",
"boost_ac_away_temp": "Temperature in Boost preset when no presence in AC mode"
}
},
"advanced": {
@@ -266,7 +342,16 @@
"thermostat_type": {
"options": {
"thermostat_over_switch": "Thermostat over a switch",
"thermostat_over_climate": "Thermostat over another thermostat"
"thermostat_over_climate": "Thermostat over a climate",
"thermostat_over_valve": "Thermostat over a valve"
}
},
"auto_regulation_mode": {
"options": {
"auto_regulation_strong": "Strong",
"auto_regulation_medium": "Medium",
"auto_regulation_light": "Light",
"auto_regulation_none": "No auto-regulation"
}
}
},

View File

@@ -8,8 +8,9 @@
"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",
@@ -29,7 +30,19 @@
"heater_entity3_id": "3ème radiateur",
"heater_entity4_id": "4ème radiateur",
"proportional_function": "Algorithme",
"climate_entity_id": "Thermostat sous-jacent"
"climate_entity_id": "Thermostat sous-jacent",
"climate_entity2_id": "2ème thermostat sous-jacent",
"climate_entity3_id": "3ème thermostat sous-jacent",
"climate_entity4_id": "4ème thermostat sous-jacent",
"ac_mode": "AC mode ?",
"valve_entity_id": "1ère valve number",
"valve_entity2_id": "2ème valve number",
"valve_entity3_id": "3ème valve number",
"valve_entity4_id": "4ème valve number",
"auto_regulation_mode": "Auto-régulation",
"auto_regulation_dtemp": "Seuil de régulation",
"auto_regulation_periode_min": "Période minimale de régulation",
"inverse_switch_command": "Inverser la commande"
},
"data_description": {
"heater_entity_id": "Entity id du 1er radiateur obligatoire",
@@ -37,7 +50,19 @@
"heater_entity3_id": "Optionnel entity id du 3ème radiateur",
"heater_entity4_id": "Optionnel entity id du 4ème radiateur",
"proportional_function": "Algorithme à utiliser (Seul TPI est disponible pour l'instant)",
"climate_entity_id": "Entity id du thermostat sous-jacent"
"climate_entity_id": "Entity id du thermostat sous-jacent",
"climate_entity2_id": "Entity id du 2ème thermostat sous-jacent",
"climate_entity3_id": "Entity id du 3ème thermostat sous-jacent",
"climate_entity4_id": "Entity id du 4ème thermostat sous-jacent",
"ac_mode": "Utilisation du mode Air Conditionné (AC)",
"valve_entity_id": "Entity id de la 1ère valve",
"valve_entity2_id": "Entity id de la 2ème valve",
"valve_entity3_id": "Entity id de la 3ème valve",
"valve_entity4_id": "Entity id de la 4ème valve",
"auto_regulation_mode": "Ajustement automatique de la température cible",
"auto_regulation_dtemp": "Le seuil en ° au-dessous duquel la régulation ne sera pas envoyée",
"auto_regulation_periode_min": "La durée en minutes entre deux mise à jour faites par la régulation",
"inverse_switch_command": "Inverse la commande du switch pour une installation avec fil pilote et diode"
}
},
"tpi": {
@@ -54,7 +79,10 @@
"data": {
"eco_temp": "Température en preset Eco",
"comfort_temp": "Température en preset Comfort",
"boost_temp": "Température en preset Boost"
"boost_temp": "Température en preset Boost",
"eco_ac_temp": "Température en preset Eco en mode AC",
"comfort_ac_temp": "Température en preset Comfort en mode AC",
"boost_ac_temp": "Température en preset Boost en mode AC"
}
},
"window": {
@@ -63,7 +91,7 @@
"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_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)"
},
@@ -79,8 +107,16 @@
"title": "Gestion de la détection de mouvement",
"description": "Le preset s'ajuste automatiquement si un mouvement est détecté\nLaissez l'entity id vide si non utilisé.\n'Preset mouvement' et 'Preset sans mouvement' doivent être choisis avec les preset à utiliser.",
"data": {
"motion_sensor_entity_id": "Détecteur de mouvement",
"motion_delay": "Délai d'activation",
"motion_off_delay": "Délai de désactivation",
"motion_preset": "Preset si mouvement",
"no_motion_preset": "Preset si pas de mouvement"
},
"data_description": {
"motion_sensor_entity_id": "Détecteur de mouvement entity id",
"motion_delay": "Délai avant changement (seconds)",
"motion_delay": "Délai avant activation lorsqu'un mouvement est détecté (secondss)",
"motion_off_delai": "Délai avant désactivation lorsqu'aucun mouvement n'est détecté (secondes)",
"motion_preset": "Preset à utiliser si mouvement détecté",
"no_motion_preset": "Preset à utiliser si pas de mouvement détecté"
}
@@ -101,7 +137,10 @@
"presence_sensor_entity_id": "Capteur de présence entity id (true si quelqu'un est présent)",
"eco_away_temp": "Température en preset Eco en cas d'absence",
"comfort_away_temp": "Température en preset Comfort en cas d'absence",
"boost_away_temp": "Température en preset Boost en cas d'absence"
"boost_away_temp": "Température en preset Boost en cas d'absence",
"eco_ac_away_temp": "Température en preset Eco en cas d'absence en mode AC",
"comfort_ac_away_temp": "Température en preset Comfort en cas d'absence en mode AC",
"boost_ac_away_temp": "Température en preset Boost en cas d'absence en mode AC"
}
},
"advanced": {
@@ -161,7 +200,19 @@
"heater_entity3_id": "3ème radiateur",
"heater_entity4_id": "4ème radiateur",
"proportional_function": "Algorithme",
"climate_entity_id": "Thermostat sous-jacent"
"climate_entity_id": "Thermostat sous-jacent",
"climate_entity2_id": "2ème thermostat sous-jacent",
"climate_entity3_id": "3ème thermostat sous-jacent",
"climate_entity4_id": "4ème thermostat sous-jacent",
"ac_mode": "AC mode ?",
"valve_entity_id": "1ère valve",
"valve_entity2_id": "2ème valve",
"valve_entity3_id": "3ème valve",
"valve_entity4_id": "4ème valve",
"auto_regulation_mode": "Auto-regulation",
"auto_regulation_dtemp": "Seuil de régulation",
"auto_regulation_periode_min": "Période minimale de régulation",
"inverse_switch_command": "Inverser la commande"
},
"data_description": {
"heater_entity_id": "Entity id du 1er radiateur obligatoire",
@@ -169,7 +220,19 @@
"heater_entity3_id": "Optionnel entity id du 3ème radiateur",
"heater_entity4_id": "Optionnel entity id du 4ème radiateur",
"proportional_function": "Algorithme à utiliser (Seul TPI est disponible pour l'instant)",
"climate_entity_id": "Entity id du thermostat sous-jacent"
"climate_entity_id": "Entity id du thermostat sous-jacent",
"climate_entity2_id": "Entity id du 2ème thermostat sous-jacent",
"climate_entity3_id": "Entity id du 3ème thermostat sous-jacent",
"climate_entity4_id": "Entity id du 4ème thermostat sous-jacent",
"ac_mode": "Utilisation du mode Air Conditionné (AC)",
"valve_entity_id": "Entity id de la 1ère valve",
"valve_entity2_id": "Entity id de la 2ème valve",
"valve_entity3_id": "Entity id de la 3ème valve",
"valve_entity4_id": "Entity id de la 4ème valve",
"auto_regulation_mode": "Ajustement automatique de la consigne",
"auto_regulation_dtemp": "Le seuil en ° au-dessous duquel la régulation ne sera pas envoyée",
"auto_regulation_periode_min": "La durée en minutes entre deux mise à jour faites par la régulation",
"inverse_switch_command": "Inverse la commande du switch pour une installation avec fil pilote et diode"
}
},
"tpi": {
@@ -186,7 +249,10 @@
"data": {
"eco_temp": "Température en preset Eco",
"comfort_temp": "Température en preset Comfort",
"boost_temp": "Température en preset Boost"
"boost_temp": "Température en preset Boost",
"eco_ac_temp": "Température en preset Eco en mode AC",
"comfort_ac_temp": "Température en preset Comfort en mode AC",
"boost_ac_temp": "Température en preset Boost en mode AC"
}
},
"window": {
@@ -211,8 +277,16 @@
"title": "Gestion de la détection de mouvement",
"description": "Le preset s'ajuste automatiquement si un mouvement est détecté\nLaissez l'entity id vide si non utilisé.\n'Preset mouvement' et 'Preset sans mouvement' doivent être choisis avec les preset à utiliser.",
"data": {
"motion_sensor_entity_id": "Détecteur de mouvement",
"motion_delay": "Délai d'activation",
"motion_off_delay": "Délai de désactivation",
"motion_preset": "Preset si mouvement",
"no_motion_preset": "Preset si pas de mouvement"
},
"data_description": {
"motion_sensor_entity_id": "Détecteur de mouvement entity id",
"motion_delay": "Délai avant changement (seconds)",
"motion_delay": "Délai avant activation lorsqu'un mouvement est détecté (secondss)",
"motion_off_delai": "Délai avant désactivation lorsqu'aucun mouvement n'est détecté (secondes)",
"motion_preset": "Preset à utiliser si mouvement détecté",
"no_motion_preset": "Preset à utiliser si pas de mouvement détecté"
}
@@ -233,7 +307,10 @@
"presence_sensor_entity_id": "Capteur de présence entity id (true si quelqu'un est présent)",
"eco_away_temp": "Température en preset Eco en cas d'absence",
"comfort_away_temp": "Température en preset Comfort en cas d'absence",
"boost_away_temp": "Température en preset Boost en cas d'absence"
"boost_away_temp": "Température en preset Boost en cas d'absence",
"eco_ac_away_temp": "Température en preset Eco en cas d'absence en mode AC",
"comfort_ac_away_temp": "Température en preset Comfort en cas d'absence en mode AC",
"boost_ac_away_temp": "Température en preset Boost en cas d'absence en mode AC"
}
},
"advanced": {
@@ -266,7 +343,16 @@
"thermostat_type": {
"options": {
"thermostat_over_switch": "Thermostat sur un switch",
"thermostat_over_climate": "Thermostat sur un autre thermostat"
"thermostat_over_climate": "Thermostat sur un autre thermostat",
"thermostat_over_valve": "Thermostat sur une valve"
}
},
"auto_regulation_mode": {
"options": {
"auto_regulation_strong": "Forte",
"auto_regulation_medium": "Moyenne",
"auto_regulation_light": "Légère",
"auto_regulation_none": "Aucune"
}
}
},

View File

@@ -30,7 +30,17 @@
"heater_entity3_id": "Terzo riscaldatore",
"heater_entity4_id": "Quarto riscaldatore",
"proportional_function": "Algoritmo",
"climate_entity_id": "Termostato sottostante"
"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 ?",
"valve_entity_id": "Primo valvola numero",
"valve_entity2_id": "Secondo valvola numero",
"valve_entity3_id": "Terzo valvola numero",
"valve_entity4_id": "Quarto valvola numero",
"auto_regulation_mode": "Autoregolamentazione",
"inverse_switch_command": "Comando inverso"
},
"data_description": {
"heater_entity_id": "Entity id obbligatoria del primo riscaldatore",
@@ -38,7 +48,17 @@
"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_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) ?",
"valve_entity_id": "Entity id del primo valvola numero",
"valve_entity2_id": "Entity id del secondo valvola numero",
"valve_entity3_id": "Entity id del terzo valvola numero",
"valve_entity4_id": "Entity id del quarto valvola numero",
"auto_regulation_mode": "Regolazione automatica della temperatura target",
"inverse_switch_command": "Inverte il controllo dell'interruttore per un'installazione con filo pilota e diodo"
}
},
"tpi": {
@@ -55,7 +75,10 @@
"data": {
"eco_temp": "Temperatura nel preset Eco",
"comfort_temp": "Temperatura nel preset Comfort",
"boost_temp": "Temperatura nel preset Boost"
"boost_temp": "Temperatura nel preset Boost",
"eco_ac_temp": "Temperatura nel preset Eco (AC mode)",
"comfort_ac_temp": "Temperatura nel preset Comfort (AC mode)",
"boost_ac_temp": "Temperatura nel preset Boost (AC mode)"
}
},
"window": {
@@ -82,6 +105,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"
}
@@ -102,7 +126,10 @@
"presence_sensor_entity_id": "Entity id sensore presenza (true se è presente qualcuno)",
"eco_away_temp": "Temperatura al preset Eco in caso d'assenza",
"comfort_away_temp": "Temperatura al preset Comfort in caso d'assenza",
"boost_away_temp": "Temperatura al preset Boost in caso d'assenza"
"boost_away_temp": "Temperatura al preset Boost in caso d'assenza",
"eco_ac_away_temp": "Temperatura al preset Eco in caso d'assenza (AC mode)",
"comfort_ac_away_temp": "Temperatura al preset Comfort in caso d'assenza (AC mode)",
"boost_ac_away_temp": "Temperatura al preset Boost in caso d'assenza (AC mode)"
}
},
"advanced": {
@@ -154,14 +181,24 @@
},
"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_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 ?",
"valve_entity_id": "Primo valvola numero",
"valve_entity2_id": "Secondo valvola numero",
"valve_entity3_id": "Terzo valvola numero",
"valve_entity4_id": "Quarto valvola numero",
"auto_regulation_mode": "Autoregolamentazione",
"inverse_switch_command": "Comando inverso"
},
"data_description": {
"heater_entity_id": "Entity id obbligatoria del primo riscaldatore",
@@ -169,7 +206,17 @@
"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_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) ?",
"valve_entity_id": "Entity id del primo valvola numero",
"valve_entity2_id": "Entity id del secondo valvola numero",
"valve_entity3_id": "Entity id del terzo valvola numero",
"valve_entity4_id": "Entity id del quarto valvola numero",
"auto_regulation_mode": "Autoregolamentazione",
"inverse_switch_command": "Inverte il controllo dell'interruttore per un'installazione con filo pilota e diodo"
}
},
"tpi": {
@@ -186,7 +233,10 @@
"data": {
"eco_temp": "Temperatura nel preset Eco",
"comfort_temp": "Temperatura nel preset Comfort",
"boost_temp": "Temperatura nel preset Boost"
"boost_temp": "Temperatura nel preset Boost",
"eco_ac_temp": "Temperatura nel preset Eco (AC mode)",
"comfort_ac_temp": "Temperatura nel preset Comfort (AC mode)",
"boost_ac_temp": "Temperatura nel preset Boost (AC mode)"
}
},
"window": {
@@ -213,6 +263,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"
}
@@ -233,7 +284,10 @@
"presence_sensor_entity_id": "Entity id sensore presenza (true se è presente qualcuno)",
"eco_away_temp": "Temperatura al preset Eco in caso d'assenza",
"comfort_away_temp": "Temperatura al preset Comfort in caso d'assenza",
"boost_away_temp": "Temperatura al preset Boost in caso d'assenza"
"boost_away_temp": "Temperatura al preset Boost in caso d'assenza",
"eco_ac_away_temp": "Temperatura al preset Eco in caso d'assenza (AC mode)",
"comfort_ac_away_temp": "Temperatura al preset Comfort in caso d'assenza (AC mode)",
"boost_ac_away_temp": "Temperatura al preset Boost in caso d'assenza (AC mode)"
}
},
"advanced": {
@@ -266,7 +320,16 @@
"thermostat_type": {
"options": {
"thermostat_over_switch": "Termostato su un interruttore",
"thermostat_over_climate": "Termostato sopra un altro termostato"
"thermostat_over_climate": "Termostato sopra un altro termostato",
"thermostat_over_valve": "Thermostato su una valvola"
}
},
"auto_regulation_mode": {
"options": {
"auto_regulation_strong": "Forte",
"auto_regulation_medium": "Media",
"auto_regulation_light": "Leggera",
"auto_regulation_none": "Nessuna autoregolamentazione"
}
}
},
@@ -285,4 +348,4 @@
}
}
}
}
}

View File

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

View File

@@ -1,13 +1,15 @@
# pylint: disable=unused-argument, line-too-long
""" Underlying entities classes """
import logging
from typing import Any
from enum import StrEnum
from homeassistant.const import ATTR_ENTITY_ID, STATE_ON, UnitOfTemperature
from homeassistant.exceptions import ServiceNotFound
from homeassistant.backports.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 +24,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 +47,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,41 +104,48 @@ 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
async def remove_entity(self):
# This should be the correct way to handle turn_off and turn_on but this breaks the unit test
# will an not understandable error: TypeError: object MagicMock can't be used in 'await' expression
async def turn_off(self):
""" Turn off the underlying equipement.
Need to be overriden"""
return NotImplementedError
async def turn_on(self):
""" Turn off the underlying equipement.
Need to be overriden"""
return NotImplementedError
@property
def is_inversed(self):
""" Tells if the switch command should be inversed"""
return False
def remove_entity(self):
"""Remove the underlying entity"""
return
async def check_initial_state(self, hvac_mode: HVACMode):
"""Prevent the underlying to be on but thermostat is off"""
if hvac_mode == HVACMode.OFF and self.is_device_active:
_LOGGER.warning(
"%s - The hvac mode is OFF, but the underlying device is ON. Turning off device %s",
self,
self._entity_id,
)
await self.set_hvac_mode(hvac_mode)
elif hvac_mode != HVACMode.OFF and not self.is_device_active:
_LOGGER.warning(
"%s - The hvac mode is ON, but the underlying device is not ON. Turning on device %s",
self,
self._entity_id,
)
await self.set_hvac_mode(hvac_mode)
# override to be able to mock the call
def call_later(
self, hass: HomeAssistant, delay_sec: int, called_method
@@ -138,6 +153,19 @@ 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 """
class UnderlyingSwitch(UnderlyingEntity):
"""Represent a underlying switch"""
@@ -167,40 +195,80 @@ class UnderlyingSwitch(UnderlyingEntity):
self._should_relaunch_control_heating = False
self._on_time_sec = 0
self._off_time_sec = 0
self._hvac_mode = None
@property
def initial_delay_sec(self):
"""The initial delay for this class"""
return self._initial_delay_sec
@overrides
@property
def is_inversed(self):
""" Tells if the switch command should be inversed"""
return self._thermostat.is_inversed
# @overrides this breaks some unit tests TypeError: object MagicMock can't be used in 'await' expression
async def set_hvac_mode(self, hvac_mode: HVACMode) -> bool:
"""Set the HVACmode. Returns true if we need to redo a control_heating"""
"""Set the HVACmode. Returns true if something have change"""
if hvac_mode == HVACMode.OFF:
if self.is_device_active:
await self.turn_off()
await self._cancel_cycle()
return True
self._cancel_cycle()
if self._hvac_mode != hvac_mode:
self._hvac_mode = hvac_mode
return True
else:
return False
@property
def is_device_active(self):
"""If the toggleable device is currently active."""
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
async def check_initial_state(self, hvac_mode: HVACMode):
"""Prevent the heater to be on but thermostat is off"""
if hvac_mode == HVACMode.OFF and self.is_device_active:
_LOGGER.warning(
"%s - The hvac mode is OFF, but the switch device is ON. Turning off device %s",
self,
self._entity_id,
# @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,
)
await self.turn_off()
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"""
@@ -221,7 +289,7 @@ class UnderlyingSwitch(UnderlyingEntity):
if self._async_cancel_cycle is not None:
if force:
_LOGGER.debug("%s - we force a new cycle", self)
await self._cancel_cycle()
self._cancel_cycle()
else:
_LOGGER.debug(
"%s - A previous cycle is alredy running and no force -> waits for its end",
@@ -232,7 +300,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
@@ -251,7 +319,8 @@ class UnderlyingSwitch(UnderlyingEntity):
else:
_LOGGER.debug("%s - nothing to do", self)
async def _cancel_cycle(self):
@overrides
def _cancel_cycle(self):
"""Cancel the cycle"""
if self._async_cancel_cycle:
self._async_cancel_cycle()
@@ -268,7 +337,7 @@ class UnderlyingSwitch(UnderlyingEntity):
self._on_time_sec,
)
await self._cancel_cycle()
self._cancel_cycle()
if self._hvac_mode == HVACMode.OFF:
_LOGGER.debug("%s - End of cycle (HVAC_MODE_OFF - 2)", self)
@@ -284,15 +353,6 @@ class UnderlyingSwitch(UnderlyingEntity):
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(
@@ -320,7 +380,7 @@ class UnderlyingSwitch(UnderlyingEntity):
self._should_relaunch_control_heating,
self._off_time_sec,
)
await self._cancel_cycle()
self._cancel_cycle()
if self._hvac_mode == HVACMode.OFF:
_LOGGER.debug("%s - End of cycle (HVAC_MODE_OFF - 2)", self)
@@ -329,16 +389,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:
@@ -361,9 +411,10 @@ class UnderlyingSwitch(UnderlyingEntity):
# increment energy at the end of the cycle
self._thermostat.incremente_energy()
async def remove_entity(self):
"""Remove the entity"""
await self._cancel_cycle()
@overrides
def remove_entity(self):
"""Remove the entity after stopping its cycle"""
self._cancel_cycle()
class UnderlyingClimate(UnderlyingEntity):
@@ -421,10 +472,10 @@ class UnderlyingClimate(UnderlyingEntity):
"""True if the underlying climate was found"""
return self._underlying_climate is not None
async def set_hvac_mode(self, hvac_mode: HVACMode):
"""Set the HVACmode of the underlying climate"""
async def set_hvac_mode(self, hvac_mode: HVACMode) -> bool:
"""Set the HVACmode of the underlying climate. Returns true if something have change"""
if not self.is_initialized:
return
return False
data = {ATTR_ENTITY_ID: self._entity_id, "hvac_mode": hvac_mode}
await self._hass.services.async_call(
@@ -433,11 +484,13 @@ class UnderlyingClimate(UnderlyingEntity):
data,
)
return True
@property
def is_device_active(self):
"""If the toggleable device is currently active."""
if self.is_initialized:
return self._underlying_climate.hvac_action not in [
return self._underlying_climate.hvac_mode != HVACMode.OFF and self._underlying_climate.hvac_action not in [
HVACAction.IDLE,
HVACAction.OFF,
]
@@ -572,8 +625,145 @@ class UnderlyingClimate(UnderlyingEntity):
return self._underlying_climate.temperature_unit
@property
def target_temperature_step(self) -> str:
def target_temperature_step(self) -> float:
"""Get the target_temperature_step"""
if not self.is_initialized:
return 1
return self._underlying_climate.target_temperature_step
@property
def target_temperature_high(self) -> float:
"""Get the target_temperature_high"""
if not self.is_initialized:
return 30
return self._underlying_climate.target_temperature_high
@property
def target_temperature_low(self) -> float:
"""Get the target_temperature_low"""
if not self.is_initialized:
return 15
return self._underlying_climate.target_temperature_low
@property
def is_aux_heat(self) -> bool:
"""Get the is_aux_heat"""
if not self.is_initialized:
return False
return self._underlying_climate.is_aux_heat
def turn_aux_heat_on(self) -> None:
"""Turn auxiliary heater on."""
if not self.is_initialized:
return None
return self._underlying_climate.turn_aux_heat_on()
def turn_aux_heat_off(self) -> None:
"""Turn auxiliary heater on."""
if not self.is_initialized:
return None
return self._underlying_climate.turn_aux_heat_off()
class UnderlyingValve(UnderlyingEntity):
"""Represent a underlying switch"""
_hvac_mode: HVACMode
# This is the percentage of opening int integer (from 0 to 100)
_percent_open: int
def __init__(
self,
hass: HomeAssistant,
thermostat: Any,
valve_entity_id: str
) -> None:
"""Initialize the underlying switch"""
super().__init__(
hass=hass,
thermostat=thermostat,
entity_type=UnderlyingEntityType.VALVE,
entity_id=valve_entity_id,
)
self._async_cancel_cycle = None
self._should_relaunch_control_heating = False
self._hvac_mode = None
self._percent_open = self._thermostat.valve_open_percent
async def send_percent_open(self):
""" Send the percent open to the underlying valve """
# This may fails if called after shutdown
try:
data = { ATTR_ENTITY_ID: self._entity_id, "value": self._percent_open }
domain = self._entity_id.split('.')[0]
await self._hass.services.async_call(
domain,
SERVICE_SET_VALUE,
data,
)
except ServiceNotFound as err:
_LOGGER.error(err)
async def turn_off(self):
"""Turn heater toggleable device off."""
_LOGGER.debug("%s - Stopping underlying valve entity %s", self, self._entity_id)
self._percent_open = 0
if self.is_device_active:
await self.send_percent_open()
async def turn_on(self):
"""Nothing to do for Valve because it cannot be turned off"""
async def set_hvac_mode(self, hvac_mode: HVACMode) -> bool:
"""Set the HVACmode. Returns true if something have change"""
if hvac_mode == HVACMode.OFF:
await self.turn_off()
if self._hvac_mode != hvac_mode:
self._hvac_mode = hvac_mode
return True
else:
return False
@property
def is_device_active(self):
"""If the toggleable device is currently active."""
try:
return self._percent_open > 0
# To test if real device is open but this is causing some side effect
# because the activation can be deferred -
# or float(self._hass.states.get(self._entity_id).state) > 0
except Exception: # pylint: disable=broad-exception-caught
return False
@overrides
async def start_cycle(
self,
hvac_mode: HVACMode,
_1,
_2,
_3,
force=False,
):
"""We use this function to change the on_percent"""
if force:
await self.send_percent_open()
def set_valve_open_percent(self, percent):
""" Update the valve open percent """
caped_val = self._thermostat.valve_open_percent
if self._percent_open == caped_val:
# No changes
return
self._percent_open = caped_val
# Send the new command to valve via a service call
_LOGGER.info("%s - Setting valve ouverture percent to %s", self, self._percent_open)
# Send the change to the valve, in background
self._hass.create_task(self.send_percent_open())
def remove_entity(self):
"""Remove the entity after stopping its cycle"""
self._cancel_cycle()

View File

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

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: 46 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: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 46 KiB

2
requirements_dev.txt Normal file
View File

@@ -0,0 +1,2 @@
homeassistant==2023.10.3
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,14 +1,15 @@
# pylint: disable=wildcard-import, unused-wildcard-import, protected-access, unused-argument, line-too-long
""" Some common resources """
import asyncio
import logging
from unittest.mock import patch, MagicMock
import pytest
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,29 @@ 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 get_tz, NowClass # pylint: disable=unused-import
from .const import ( # pylint: disable=unused-import
MOCK_TH_OVER_SWITCH_USER_CONFIG,
MOCK_TH_OVER_4SWITCH_USER_CONFIG,
MOCK_TH_OVER_CLIMATE_USER_CONFIG,
MOCK_TH_OVER_SWITCH_TYPE_CONFIG,
MOCK_TH_OVER_SWITCH_AC_TYPE_CONFIG,
MOCK_TH_OVER_4SWITCH_TYPE_CONFIG,
MOCK_TH_OVER_CLIMATE_TYPE_CONFIG,
MOCK_TH_OVER_CLIMATE_TYPE_AC_CONFIG,
MOCK_TH_OVER_CLIMATE_TYPE_NOT_REGULATED_CONFIG,
MOCK_TH_OVER_SWITCH_TPI_CONFIG,
MOCK_PRESETS_CONFIG,
MOCK_PRESETS_AC_CONFIG,
MOCK_WINDOW_CONFIG,
MOCK_MOTION_CONFIG,
MOCK_POWER_CONFIG,
MOCK_PRESENCE_CONFIG,
MOCK_PRESENCE_AC_CONFIG,
MOCK_ADVANCED_CONFIG,
# MOCK_DEFAULT_FEATURE_CONFIG,
PRESET_BOOST,
@@ -58,6 +65,19 @@ FULL_SWITCH_CONFIG = (
| MOCK_ADVANCED_CONFIG
)
FULL_SWITCH_AC_CONFIG = (
MOCK_TH_OVER_SWITCH_USER_CONFIG
| MOCK_TH_OVER_SWITCH_AC_TYPE_CONFIG
| MOCK_TH_OVER_SWITCH_TPI_CONFIG
| MOCK_PRESETS_AC_CONFIG
| MOCK_WINDOW_CONFIG
| MOCK_MOTION_CONFIG
| MOCK_POWER_CONFIG
| MOCK_PRESENCE_AC_CONFIG
| MOCK_ADVANCED_CONFIG
)
PARTIAL_CLIMATE_CONFIG = (
MOCK_TH_OVER_CLIMATE_USER_CONFIG
| MOCK_TH_OVER_CLIMATE_TYPE_CONFIG
@@ -65,6 +85,20 @@ PARTIAL_CLIMATE_CONFIG = (
| MOCK_ADVANCED_CONFIG
)
PARTIAL_CLIMATE_NOT_REGULATED_CONFIG = (
MOCK_TH_OVER_CLIMATE_USER_CONFIG
| MOCK_TH_OVER_CLIMATE_TYPE_NOT_REGULATED_CONFIG
| MOCK_PRESETS_CONFIG
| MOCK_ADVANCED_CONFIG
)
PARTIAL_CLIMATE_AC_CONFIG = (
MOCK_TH_OVER_CLIMATE_USER_CONFIG
| MOCK_TH_OVER_CLIMATE_TYPE_AC_CONFIG
| MOCK_PRESETS_CONFIG
| MOCK_ADVANCED_CONFIG
)
FULL_4SWITCH_CONFIG = (
MOCK_TH_OVER_4SWITCH_USER_CONFIG
| MOCK_TH_OVER_4SWITCH_TYPE_CONFIG
@@ -83,7 +117,47 @@ _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) -> None:
def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos, hvac_mode:HVACMode = HVACMode.OFF, hvac_action:HVACAction = HVACAction.OFF) -> None: # pylint: disable=unused-argument
"""Initialize the thermostat."""
super().__init__()
self.hass = hass
self.platform = 'climate'
self.entity_id= self.platform+'.'+unique_id
self._attr_extra_state_attributes = {}
self._unique_id = unique_id
self._name = name
self._attr_hvac_action = HVACAction.OFF if hvac_mode == HVACMode.OFF else HVACAction.HEATING
self._attr_hvac_mode = hvac_mode
self._attr_hvac_modes = [HVACMode.OFF, HVACMode.COOL, HVACMode.HEAT]
self._attr_temperature_unit = UnitOfTemperature.CELSIUS
self._attr_target_temperature = 20
self._attr_current_temperature = 15
self._attr_hvac_action = hvac_action
def set_temperature(self, **kwargs):
""" Set the target temperature"""
temperature = kwargs.get(ATTR_TEMPERATURE)
self._attr_target_temperature = temperature
async def async_set_hvac_mode(self, hvac_mode):
""" The hvac mode"""
self._attr_hvac_mode = hvac_mode
@property
def hvac_action(self):
""" The hvac action of the mock climate"""
return self._attr_hvac_action
def set_hvac_action(self, hvac_action: HVACAction):
""" Set the HVACaction """
self._attr_hvac_action = hvac_action
class MockUnavailableClimate(ClimateEntity):
"""A Mock Climate class used for Underlying climate mode"""
def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None: # pylint: disable=unused-argument
"""Initialize the thermostat."""
super().__init__()
@@ -92,12 +166,11 @@ class MockClimate(ClimateEntity):
self._attr_extra_state_attributes = {}
self._unique_id = unique_id
self._name = name
self._attr_hvac_action = HVACAction.OFF
self._attr_hvac_mode = HVACMode.OFF
self._attr_hvac_action = None
self._attr_hvac_mode = None
self._attr_hvac_modes = [HVACMode.OFF, HVACMode.COOL, HVACMode.HEAT]
self._attr_temperature_unit = UnitOfTemperature.CELSIUS
class MagicMockClimate(MagicMock):
"""A Magic Mock class for a underlying climate entity"""
@@ -172,10 +245,10 @@ class MagicMockClimate(MagicMock):
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)
@@ -201,7 +274,7 @@ def search_entity(hass: HomeAssistant, entity_id, domain) -> Entity:
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(
@@ -227,7 +300,7 @@ async def send_temperature_change_event(
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(
@@ -253,7 +326,7 @@ async def send_ext_temperature_change_event(
async def send_power_change_event(
entity: VersatileThermostat, new_power, date, sleep=True
entity: BaseThermostat, new_power, date, sleep=True
):
"""Sending a new power event simulating a change on power sensor"""
_LOGGER.info(
@@ -279,7 +352,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(
@@ -305,7 +378,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(
@@ -339,7 +412,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(
@@ -373,7 +446,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(
@@ -405,15 +478,8 @@ async def send_presence_change_event(
await asyncio.sleep(0.1)
return ret
def get_tz(hass: HomeAssistant):
"""Get the current timezone"""
return dt_util.get_time_zone(hass.config.time_zone)
async def send_climate_change_event(
entity: VersatileThermostat,
entity: BaseThermostat,
new_hvac_mode: HVACMode,
old_hvac_mode: HVACMode,
new_hvac_action: HVACAction,
@@ -455,10 +521,55 @@ async def send_climate_change_event(
await asyncio.sleep(0.1)
return ret
async def send_climate_change_event_with_temperature(
entity: BaseThermostat,
new_hvac_mode: HVACMode,
old_hvac_mode: HVACMode,
new_hvac_action: HVACAction,
old_hvac_action: HVACAction,
date,
temperature,
sleep=True,
):
"""Sending a new climate event simulating a change on the underlying climate state"""
_LOGGER.info(
"------- Testu: sending send_temperature_change_event, new_hvac_mode=%s old_hvac_mode=%s new_hvac_action=%s old_hvac_action=%s date=%s temperature=%s on %s",
new_hvac_mode,
old_hvac_mode,
new_hvac_action,
old_hvac_action,
date,
temperature,
entity,
)
climate_event = Event(
EVENT_STATE_CHANGED,
{
"new_state": State(
entity_id=entity.entity_id,
state=new_hvac_mode,
attributes={"hvac_action": new_hvac_action, "temperature": temperature},
last_changed=date,
last_updated=date,
),
"old_state": State(
entity_id=entity.entity_id,
state=old_hvac_mode,
attributes={"hvac_action": old_hvac_action},
last_changed=date,
last_updated=date,
),
},
)
ret = await entity._async_climate_changed(climate_event)
if sleep:
await asyncio.sleep(0.1)
return ret
def cancel_switchs_cycles(entity: 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,7 @@ from custom_components.versatile_thermostat.config_flow import (
VersatileThermostatBaseConfigFlow,
)
from custom_components.versatile_thermostat.climate import (
VersatileThermostat,
)
from custom_components.versatile_thermostat.base_thermostat import BaseThermostat
pytest_plugins = "pytest_homeassistant_custom_component" # pylint: disable=invalid-name
@@ -34,7 +34,7 @@ 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 +82,7 @@ def skip_hass_states_get_fixture():
def skip_control_heating_fixture():
"""Skip the control_heating of VersatileThermostat"""
with patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat._async_control_heating"
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.async_control_heating"
):
yield
@@ -105,6 +105,6 @@ def skip_hass_states_is_state_fixture():
@pytest.fixture(name="skip_send_event")
def skip_send_event_fixture():
"""Skip the send_event in VersatileThermostat"""
with patch.object(VersatileThermostat, "send_event"):
"""Skip the send_event in BaseThermostat"""
with patch.object(BaseThermostat, "send_event"):
yield

View File

@@ -15,6 +15,7 @@ from custom_components.versatile_thermostat.const import (
CONF_THERMOSTAT_CLIMATE,
CONF_THERMOSTAT_SWITCH,
CONF_THERMOSTAT_TYPE,
CONF_AC_MODE,
CONF_TEMP_SENSOR,
CONF_EXTERNAL_TEMP_SENSOR,
CONF_CYCLE_MIN,
@@ -39,6 +40,7 @@ from custom_components.versatile_thermostat.const import (
CONF_WINDOW_AUTO_MAX_DURATION,
CONF_MOTION_SENSOR,
CONF_MOTION_DELAY,
CONF_MOTION_OFF_DELAY,
CONF_MOTION_PRESET,
CONF_NO_MOTION_PRESET,
CONF_POWER_SENSOR,
@@ -48,8 +50,13 @@ from custom_components.versatile_thermostat.const import (
CONF_PRESENCE_SENSOR,
PRESET_AWAY_SUFFIX,
CONF_CLIMATE,
CONF_AUTO_REGULATION_MODE,
CONF_AUTO_REGULATION_STRONG,
CONF_AUTO_REGULATION_NONE,
CONF_AUTO_REGULATION_DTEMP,
CONF_AUTO_REGULATION_PERIOD_MIN,
CONF_INVERSE_SWITCH
)
MOCK_TH_OVER_SWITCH_USER_CONFIG = {
CONF_NAME: "TheOverSwitchMockName",
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_SWITCH,
@@ -88,13 +95,22 @@ MOCK_TH_OVER_CLIMATE_USER_CONFIG = {
CONF_CYCLE_MIN: 5,
CONF_TEMP_MIN: 15,
CONF_TEMP_MAX: 30,
CONF_DEVICE_POWER: 1,
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 = {
@@ -103,6 +119,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 = {
@@ -112,6 +130,24 @@ 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
}
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 = {
@@ -120,6 +156,15 @@ MOCK_PRESETS_CONFIG = {
PRESET_BOOST + "_temp": 18,
}
MOCK_PRESETS_AC_CONFIG = {
PRESET_ECO + "_temp": 17,
PRESET_COMFORT + "_temp": 19,
PRESET_BOOST + "_temp": 20,
PRESET_ECO + "_ac_temp": 25,
PRESET_COMFORT + "_ac_temp": 23,
PRESET_BOOST + "_ac_temp": 21,
}
MOCK_WINDOW_CONFIG = {
CONF_WINDOW_SENSOR: "binary_sensor.window_sensor",
CONF_WINDOW_DELAY: 10,
@@ -134,6 +179,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,
}
@@ -151,6 +197,16 @@ MOCK_PRESENCE_CONFIG = {
PRESET_BOOST + PRESET_AWAY_SUFFIX + "_temp": 18,
}
MOCK_PRESENCE_AC_CONFIG = {
CONF_PRESENCE_SENSOR: "person.presence_sensor",
PRESET_ECO + PRESET_AWAY_SUFFIX + "_temp": 16,
PRESET_COMFORT + PRESET_AWAY_SUFFIX + "_temp": 17,
PRESET_BOOST + PRESET_AWAY_SUFFIX + "_temp": 18,
PRESET_ECO + "_ac" + PRESET_AWAY_SUFFIX + "_temp": 27,
PRESET_COMFORT + "_ac" + PRESET_AWAY_SUFFIX + "_temp": 26,
PRESET_BOOST + "_ac" + PRESET_AWAY_SUFFIX + "_temp": 25,
}
MOCK_ADVANCED_CONFIG = {
CONF_MINIMAL_ACTIVATION_DELAY: 10,
CONF_SECURITY_DELAY_MIN: 5,

View File

@@ -0,0 +1,334 @@
# pylint: disable=wildcard-import, unused-wildcard-import, protected-access, unused-argument, line-too-long
""" Test the normal start of a Thermostat """
from unittest.mock import patch #, call
from datetime import datetime, timedelta
from homeassistant.core import HomeAssistant
from homeassistant.components.climate import HVACAction, HVACMode
from homeassistant.config_entries import ConfigEntryState
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.components.climate import ClimateEntity, DOMAIN as CLIMATE_DOMAIN
from pytest_homeassistant_custom_component.common import MockConfigEntry
# from custom_components.versatile_thermostat.base_thermostat import BaseThermostat
from custom_components.versatile_thermostat.thermostat_climate import ThermostatOverClimate
from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_over_climate_regulation(hass: HomeAssistant, skip_hass_states_is_state, skip_send_event):
"""Test the regulation of an over climate thermostat"""
entry = MockConfigEntry(
domain=DOMAIN,
title="TheOverClimateMockName",
unique_id="uniqueId",
# This is include a medium regulation
data=PARTIAL_CLIMATE_CONFIG,
)
tz = get_tz(hass) # pylint: disable=invalid-name
now: datetime = datetime.now(tz=tz)
fake_underlying_climate = MockClimate(hass, "mockUniqueId", "MockClimateName", {})
# Creates the regulated VTherm over climate
# change temperature so that the heating will start
event_timestamp = now - timedelta(minutes=10)
with patch(
"custom_components.versatile_thermostat.commons.NowClass.get_now", return_value=event_timestamp
), patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
return_value=fake_underlying_climate,
):
entry.add_to_hass(hass)
await hass.config_entries.async_setup(entry.entry_id)
assert entry.state is ConfigEntryState.LOADED
def find_my_entity(entity_id) -> ClimateEntity:
"""Find my new entity"""
component: EntityComponent[ClimateEntity] = hass.data[CLIMATE_DOMAIN]
for entity in component.entities:
if entity.entity_id == entity_id:
return entity
entity:ThermostatOverClimate = find_my_entity("climate.theoverclimatemockname")
assert entity
assert isinstance(entity, ThermostatOverClimate)
assert entity.name == "TheOverClimateMockName"
assert entity.is_over_climate is True
assert entity.is_regulated is True
assert entity.hvac_action is HVACAction.OFF
assert entity.hvac_mode is HVACMode.OFF
assert entity.target_temperature == entity.min_temp
assert entity.preset_modes == [
PRESET_NONE,
PRESET_ECO,
PRESET_COMFORT,
PRESET_BOOST,
]
assert entity.preset_mode is PRESET_NONE
# Activate the heating by changing HVACMode and temperature
# Select a hvacmode, presence and preset
await entity.async_set_hvac_mode(HVACMode.HEAT)
assert entity.hvac_mode is HVACMode.HEAT
assert entity.hvac_action == HVACAction.OFF
assert entity.regulated_target_temp == entity.min_temp
await send_temperature_change_event(entity, 15, event_timestamp)
await send_ext_temperature_change_event(entity, 10, event_timestamp)
# set manual target temp (at now - 7) -> the regulation should occurs
event_timestamp = now - timedelta(minutes=7)
with patch(
"custom_components.versatile_thermostat.commons.NowClass.get_now", return_value=event_timestamp
):
await entity.async_set_temperature(temperature=18)
fake_underlying_climate.set_hvac_action(HVACAction.HEATING) # simulate under heating
assert entity.hvac_action == HVACAction.HEATING
assert entity.preset_mode == PRESET_NONE # Manual mode
# the regulated temperature should be greater
assert entity.regulated_target_temp > entity.target_temperature
# In medium we could go up to +3 degre
# normally the calcul gives 18 + 2.2 but we round the result to the nearest 0.5 which is 2.0
assert entity.regulated_target_temp == 18+2.0
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, 22, 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-0.5 # normally 0.6 but round_to_nearest gives 0.5
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_over_climate_regulation_ac_mode(hass: HomeAssistant, skip_hass_states_is_state, skip_send_event):
"""Test the regulation of an over climate thermostat"""
entry = MockConfigEntry(
domain=DOMAIN,
title="TheOverClimateMockName",
unique_id="uniqueId",
# This is include a medium regulation
data=PARTIAL_CLIMATE_AC_CONFIG,
)
tz = get_tz(hass) # pylint: disable=invalid-name
now: datetime = datetime.now(tz=tz)
fake_underlying_climate = MockClimate(hass, "mockUniqueId", "MockClimateName", {})
# Creates the regulated VTherm over climate
# change temperature so that the heating will start
event_timestamp = now - timedelta(minutes=10)
with patch(
"custom_components.versatile_thermostat.commons.NowClass.get_now", return_value=event_timestamp
), patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
return_value=fake_underlying_climate,
):
entry.add_to_hass(hass)
await hass.config_entries.async_setup(entry.entry_id)
assert entry.state is ConfigEntryState.LOADED
def find_my_entity(entity_id) -> ClimateEntity:
"""Find my new entity"""
component: EntityComponent[ClimateEntity] = hass.data[CLIMATE_DOMAIN]
for entity in component.entities:
if entity.entity_id == entity_id:
return entity
entity:ThermostatOverClimate = find_my_entity("climate.theoverclimatemockname")
assert entity
assert isinstance(entity, ThermostatOverClimate)
assert entity.name == "TheOverClimateMockName"
assert entity.is_over_climate is True
assert entity.is_regulated is True
assert entity.hvac_action is HVACAction.OFF
assert entity.hvac_mode is HVACMode.OFF
assert entity.target_temperature == entity.max_temp
assert entity.preset_modes == [
PRESET_NONE,
PRESET_ECO,
PRESET_COMFORT,
PRESET_BOOST,
]
assert entity.preset_mode is PRESET_NONE
# Activate the heating by changing HVACMode and temperature
# Select a hvacmode, presence and preset
await entity.async_set_hvac_mode(HVACMode.HEAT)
assert entity.hvac_mode is HVACMode.HEAT
assert entity.hvac_action == HVACAction.OFF
# change temperature so that the heating will start
await send_temperature_change_event(entity, 30, event_timestamp)
await send_ext_temperature_change_event(entity, 35, event_timestamp)
# set manual target temp
event_timestamp = now - timedelta(minutes=7)
with patch(
"custom_components.versatile_thermostat.commons.NowClass.get_now", return_value=event_timestamp
):
await entity.async_set_temperature(temperature=25)
fake_underlying_climate.set_hvac_action(HVACAction.COOLING) # simulate under heating
assert entity.hvac_action == HVACAction.COOLING
assert entity.preset_mode == PRESET_NONE # Manual mode
# the regulated temperature should be lower
assert entity.regulated_target_temp < entity.target_temperature
assert entity.regulated_target_temp == 25-3 # 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-2.5 # +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, 20, 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+0.5 # +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 == 20.
# 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+1 # 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, 18, 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 # 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 *
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
@@ -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,12 @@
# pylint: disable=wildcard-import, unused-wildcard-import, protected-access, unused-argument, line-too-long
""" Test the Window management """
from unittest.mock import patch
from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import
from unittest.mock import patch, call
from datetime import datetime, timedelta
import logging
from .commons import *
logging.getLogger().setLevel(logging.DEBUG)
@@ -49,7 +51,7 @@ async def test_bug_56(
},
)
entity: VersatileThermostat = await create_thermostat(
entity: BaseThermostat = await create_thermostat(
hass, entry, "climate.theoverclimatemockname"
)
assert entity
@@ -60,13 +62,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 +75,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 +128,7 @@ async def test_bug_63(
},
)
entity: VersatileThermostat = await create_thermostat(
entity: BaseThermostat = await create_thermostat(
hass, entry, "climate.theoverswitchmockname"
)
assert entity
@@ -180,7 +180,7 @@ async def test_bug_64(
},
)
entity: VersatileThermostat = await create_thermostat(
entity: BaseThermostat = await create_thermostat(
hass, entry, "climate.theoverswitchmockname"
)
assert entity
@@ -232,7 +232,7 @@ async def test_bug_66(
},
)
entity: VersatileThermostat = await create_thermostat(
entity: BaseThermostat = await create_thermostat(
hass, entry, "climate.theoverswitchmockname"
)
assert entity
@@ -247,7 +247,7 @@ async def test_bug_66(
# 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 +275,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 +298,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 +321,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(
@@ -343,3 +343,203 @@ async def test_bug_66(
assert entity.window_state == STATE_OFF
assert entity.hvac_mode is HVACMode.HEAT
assert entity.preset_mode is PRESET_BOOST
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_bug_82(
hass: HomeAssistant,
skip_hass_states_is_state,
skip_turn_on_off_heater,
skip_send_event,
):
"""Test that when a underlying climate is not available the VTherm doesn't go into security mode"""
tz = get_tz(hass) # pylint: disable=invalid-name
now: datetime = datetime.now(tz=tz)
entry = MockConfigEntry(
domain=DOMAIN,
title="TheOverClimateMockName",
unique_id="uniqueId",
data=PARTIAL_CLIMATE_CONFIG, # 5 minutes security delay
)
fake_underlying_climate = MockUnavailableClimate(hass, "mockUniqueId", "MockClimateName", {})
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
return_value=fake_underlying_climate,
) as mock_find_climate:
entry.add_to_hass(hass)
await hass.config_entries.async_setup(entry.entry_id)
assert entry.state is ConfigEntryState.LOADED
def find_my_entity(entity_id) -> ClimateEntity:
"""Find my new entity"""
component: EntityComponent[ClimateEntity] = hass.data[CLIMATE_DOMAIN]
for entity in component.entities:
if entity.entity_id == entity_id:
return entity
entity = find_my_entity("climate.theoverclimatemockname")
assert entity
assert entity.name == "TheOverClimateMockName"
assert entity.is_over_climate is True
# assert entity.hvac_action is HVACAction.OFF
assert entity.hvac_mode is HVACMode.OFF
# assert entity.hvac_mode is None
assert entity.target_temperature == entity.min_temp
assert entity.preset_modes == [
PRESET_NONE,
PRESET_ECO,
PRESET_COMFORT,
PRESET_BOOST,
]
assert entity.preset_mode is PRESET_NONE
assert entity._security_state is False
# should have been called with EventType.PRESET_EVENT and EventType.HVAC_MODE_EVENT
assert mock_send_event.call_count == 2
mock_send_event.assert_has_calls(
[
call.send_event(EventType.PRESET_EVENT, {"preset": PRESET_NONE}),
call.send_event(
EventType.HVAC_MODE_EVENT,
{"hvac_mode": HVACMode.OFF},
),
]
)
assert mock_find_climate.call_count == 1
assert mock_find_climate.mock_calls[0] == call()
mock_find_climate.assert_has_calls([call.find_underlying_entity()])
# Force security mode
assert entity._last_ext_temperature_mesure is not None
assert entity._last_temperature_mesure is not None
assert (entity._last_temperature_mesure.astimezone(tz) - now).total_seconds() < 1
assert (
entity._last_ext_temperature_mesure.astimezone(tz) - now
).total_seconds() < 1
# Tries to turns on the Thermostat
await entity.async_set_hvac_mode(HVACMode.HEAT)
assert entity.hvac_mode == HVACMode.HEAT
# 2. activate security feature when date is expired
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
):
event_timestamp = now - timedelta(minutes=6)
# set temperature to 15 so that on_percent will be > security_min_on_percent (0.2)
await send_temperature_change_event(entity, 15, event_timestamp)
# Should stay False
assert entity.security_state is False
assert entity.preset_mode == 'none'
assert entity._saved_preset_mode == 'none'
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_bug_101(
hass: HomeAssistant,
skip_hass_states_is_state,
skip_turn_on_off_heater,
skip_send_event,
):
"""Test that when a underlying climate target temp is changed, the VTherm change its own temperature target and switch to manual"""
tz = get_tz(hass) # pylint: disable=invalid-name
now: datetime = datetime.now(tz=tz)
entry = MockConfigEntry(
domain=DOMAIN,
title="TheOverClimateMockName",
unique_id="uniqueId",
data=PARTIAL_CLIMATE_NOT_REGULATED_CONFIG, # 5 minutes security delay
)
# Underlying is in HEAT mode but should be shutdown at startup
fake_underlying_climate = MockClimate(hass, "mockUniqueId", "MockClimateName", {}, HVACMode.HEAT, HVACAction.HEATING)
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
return_value=fake_underlying_climate,
) as mock_find_climate, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.set_hvac_mode"
) as mock_underlying_set_hvac_mode:
entry.add_to_hass(hass)
await hass.config_entries.async_setup(entry.entry_id)
assert entry.state is ConfigEntryState.LOADED
def find_my_entity(entity_id) -> ClimateEntity:
"""Find my new entity"""
component: EntityComponent[ClimateEntity] = hass.data[CLIMATE_DOMAIN]
for entity in component.entities:
if entity.entity_id == entity_id:
return entity
entity = find_my_entity("climate.theoverclimatemockname")
assert entity
assert entity.name == "TheOverClimateMockName"
assert entity.is_over_climate is True
assert entity.hvac_mode is HVACMode.OFF
# because in MockClimate HVACAction is HEATING if hvac_mode is not set
assert entity.hvac_action is HVACAction.HEATING
# Underlying should have been shutdown
assert mock_underlying_set_hvac_mode.call_count == 1
mock_underlying_set_hvac_mode.assert_has_calls(
[
call.set_hvac_mode(HVACMode.OFF),
]
)
assert entity.target_temperature == entity.min_temp
assert entity.preset_mode is PRESET_NONE
# should have been called with EventType.PRESET_EVENT and EventType.HVAC_MODE_EVENT
assert mock_send_event.call_count == 2
mock_send_event.assert_has_calls(
[
call.send_event(EventType.PRESET_EVENT, {"preset": PRESET_NONE}),
call.send_event(
EventType.HVAC_MODE_EVENT,
{"hvac_mode": HVACMode.OFF},
),
]
)
assert mock_find_climate.call_count == 1
assert mock_find_climate.mock_calls[0] == call()
mock_find_climate.assert_has_calls([call.find_underlying_entity()])
# Force preset mode
await entity.async_set_hvac_mode(HVACMode.HEAT)
assert entity.hvac_mode == HVACMode.HEAT
await entity.async_set_preset_mode(PRESET_COMFORT)
assert entity.preset_mode == PRESET_COMFORT
# 2. Change the target temp of underlying thermostat at now -> the event will be disgarded because to fast (to avoid loop cf issue 121)
await send_climate_change_event_with_temperature(entity, HVACMode.HEAT, HVACMode.HEAT, HVACAction.OFF, HVACAction.OFF, now, 12.75)
# Should NOT have been switched to Manual preset
assert entity.target_temperature == 17
assert entity.preset_mode is PRESET_COMFORT
# 2. Change the target temp of underlying thermostat at 11 sec later -> the event will be taken
# Wait 11 sec
event_timestamp = now + timedelta(seconds=11)
assert entity.is_regulated is False
await send_climate_change_event_with_temperature(entity, HVACMode.HEAT, HVACMode.HEAT, HVACAction.OFF, HVACAction.OFF, event_timestamp, 12.75)
assert entity.target_temperature == 12.75
assert entity.preset_mode is PRESET_NONE

View File

@@ -17,7 +17,7 @@ async def test_show_form(hass: HomeAssistant) -> None:
# Init the API
# hass.data["custom_components"] = None
# loader.async_get_custom_components(hass)
# VersatileThermostatAPI(hass)
# BaseThermostatAPI(hass)
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}
@@ -29,7 +29,7 @@ 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):
async def test_user_config_flow_over_switch(hass: HomeAssistant, skip_hass_states_get): # pylint: disable=unused-argument
"""Test the config flow with all thermostat_over_switch features"""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}
@@ -128,7 +128,7 @@ 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"""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}
@@ -157,38 +157,6 @@ async def test_user_config_flow_over_climate(hass: HomeAssistant, skip_hass_stat
result["flow_id"], user_input=MOCK_PRESETS_CONFIG
)
# assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
# assert result["step_id"] == "window"
# assert result["errors"] == {}
# result = await hass.config_entries.flow.async_configure(
# result["flow_id"], user_input=MOCK_WINDOW_CONFIG
# )
# assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
# assert result["step_id"] == "motion"
# assert result["errors"] == {}
# result = await hass.config_entries.flow.async_configure(
# result["flow_id"], user_input=MOCK_MOTION_CONFIG
# )
# assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
# assert result["step_id"] == "power"
# assert result["errors"] == {}
# result = await hass.config_entries.flow.async_configure(
# result["flow_id"], user_input=MOCK_POWER_CONFIG
# )
# assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
# assert result["step_id"] == "presence"
# assert result["errors"] == {}
# result = await hass.config_entries.flow.async_configure(
# result["flow_id"], user_input=MOCK_PRESENCE_CONFIG
# )
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "advanced"
assert result["errors"] == {}
@@ -216,7 +184,7 @@ 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"""
result = await hass.config_entries.flow.async_init(
@@ -313,7 +281,7 @@ 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"""
result = await hass.config_entries.flow.async_init(
@@ -385,7 +353,7 @@ async def test_user_config_flow_window_auto_ko(
@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"""
@@ -401,7 +369,7 @@ async def test_user_config_flow_over_4_switches(
CONF_USE_WINDOW_FEATURE: False,
CONF_USE_MOTION_FEATURE: False,
CONF_USE_POWER_FEATURE: False,
CONF_USE_PRESENCE_FEATURE: False,
CONF_USE_PRESENCE_FEATURE: False
}
TYPE_CONFIG = { # pylint: disable=wildcard-import, invalid-name
@@ -410,6 +378,7 @@ 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,
}
result = await hass.config_entries.flow.async_init(
@@ -465,6 +434,7 @@ async def test_user_config_flow_over_4_switches(
| MOCK_TH_OVER_SWITCH_TPI_CONFIG
| MOCK_PRESETS_CONFIG
| MOCK_ADVANCED_CONFIG
| { CONF_INVERSE_SWITCH: False }
)
assert result["result"]
assert result["result"].domain == DOMAIN

View File

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

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)
@@ -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,25 @@ 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 +184,9 @@ 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 +198,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 +207,9 @@ 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 +225,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,16 +263,19 @@ async def test_multiple_switchs(
},
)
entity: VersatileThermostat = await create_thermostat(
entity: BaseThermostat = await create_thermostat(
hass, entry, "climate.theover4switchmockname"
)
assert entity
assert entity.is_over_climate is False
assert entity.nb_underlying_entities == 4
# start heating, in boost mode. We block the control_heating to avoid running a cycle
with patch(
"custom_components.versatile_thermostat.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:
await entity.async_set_hvac_mode(HVACMode.HEAT)
await entity.async_set_preset_mode(PRESET_BOOST)
@@ -273,18 +287,20 @@ async def test_multiple_switchs(
event_timestamp = now - timedelta(minutes=4)
await send_temperature_change_event(entity, 15, event_timestamp)
# Checks that all heaters are off
with patch(
"homeassistant.core.StateMachine.is_state", return_value=False
) as mock_is_state:
assert entity._is_device_active is False
# Checks that all climates are off
assert entity.is_device_active is False # pylint: disable=protected-access
# Should be call for all Switch
assert mock_is_state.call_count == 4
assert mock_underlying_set_hvac_mode.call_count == 4
mock_underlying_set_hvac_mode.assert_has_calls(
[
call.set_hvac_mode(HVACMode.HEAT),
]
)
# Set temperature to a low level
with patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
"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(
@@ -322,7 +338,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(
@@ -337,5 +353,241 @@ 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,
): # pylint: disable=unused-argument
"""Test that when multiple climates are configured the activation and deactivation
is propagated to all climates"""
tz = get_tz(hass) # pylint: disable=invalid-name
now: datetime = datetime.now(tz=tz)
entry = MockConfigEntry(
domain=DOMAIN,
title="TheOver4ClimateMockName",
unique_id="uniqueId",
data={
CONF_NAME: "TheOver4ClimateMockName",
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_CLIMATE,
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
CONF_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 None
event_timestamp = now - timedelta(minutes=4)
await send_temperature_change_event(entity, 15, event_timestamp)
# Should be call for all Switch
assert mock_underlying_set_hvac_mode.call_count == 4
mock_underlying_set_hvac_mode.assert_has_calls(
[
call.set_hvac_mode(HVACMode.HEAT),
]
)
assert entity.is_device_active is False # pylint: disable=protected-access
# Stop heating, in boost mode. We block the control_heating to avoid running a cycle
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.async_control_heating"
), patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.set_hvac_mode"
) as mock_underlying_set_hvac_mode:
await entity.async_set_hvac_mode(HVACMode.OFF)
assert entity.hvac_mode is HVACMode.OFF
assert entity.preset_mode is PRESET_BOOST
assert entity.target_temperature == 19
assert entity.window_state is None
event_timestamp = now - timedelta(minutes=4)
await send_temperature_change_event(entity, 15, event_timestamp)
# Should be call for all Switch
assert mock_underlying_set_hvac_mode.call_count == 4
mock_underlying_set_hvac_mode.assert_has_calls(
[
call.set_hvac_mode(HVACMode.OFF),
]
)
assert entity.is_device_active is False # pylint: disable=protected-access
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_multiple_climates_underlying_changes(
hass: HomeAssistant,
skip_hass_states_is_state,
skip_send_event,
): # pylint: disable=unused-argument
"""Test that when multiple switch are configured the activation of one underlying
climate activate the others"""
tz = get_tz(hass) # pylint: disable=invalid-name
now: datetime = datetime.now(tz=tz)
entry = MockConfigEntry(
domain=DOMAIN,
title="TheOver4ClimateMockName",
unique_id="uniqueId",
data={
CONF_NAME: "TheOver4ClimateMockName",
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_CLIMATE,
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
CONF_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 None
event_timestamp = now - timedelta(minutes=4)
await send_temperature_change_event(entity, 15, event_timestamp)
# Should be call for all Switch
assert mock_underlying_set_hvac_mode.call_count == 4
mock_underlying_set_hvac_mode.assert_has_calls(
[
call.set_hvac_mode(HVACMode.HEAT),
]
)
assert entity.is_device_active is False # pylint: disable=protected-access
# Stop heating on one underlying climate
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.async_control_heating"
), patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.set_hvac_mode"
) as mock_underlying_set_hvac_mode:
# Wait 11 sec so that the event will not be discarded
event_timestamp = now + timedelta(seconds=11)
await send_climate_change_event(
entity,
HVACMode.OFF,
HVACMode.HEAT,
HVACAction.OFF,
HVACAction.HEATING,
event_timestamp,
)
# Should be call for all Switch
assert mock_underlying_set_hvac_mode.call_count == 4
mock_underlying_set_hvac_mode.assert_has_calls(
[
call.set_hvac_mode(HVACMode.OFF),
]
)
assert entity.hvac_mode == HVACMode.OFF
assert entity.is_device_active is False # pylint: disable=protected-access
# Start heating on one underlying climate
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.async_control_heating"
), patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.set_hvac_mode"
) as mock_underlying_set_hvac_mode, patch(
# notice that there is no need of return_value=HVACAction.IDLE because this is not
# a function but a property
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.hvac_action",
HVACAction.IDLE,
):
# Wait 11 sec so that the event will not be discarded
event_timestamp = now + timedelta(seconds=11)
await send_climate_change_event(
entity,
HVACMode.HEAT,
HVACMode.OFF,
HVACAction.IDLE,
HVACAction.OFF,
event_timestamp,
)
# Should be call for all Switch
assert mock_underlying_set_hvac_mode.call_count == 4
mock_underlying_set_hvac_mode.assert_has_calls(
[
call.set_hvac_mode(HVACMode.HEAT),
]
)
assert entity.hvac_mode == HVACMode.HEAT
assert entity.hvac_action == HVACAction.IDLE
assert entity.is_device_active is False # pylint: disable=protected-access

View File

@@ -1,8 +1,10 @@
# 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
from ..open_window_algorithm import WindowOpenDetectionAlgorithm
async def test_open_window_algo(

174
tests/test_pi.py Normal file
View File

@@ -0,0 +1,174 @@
# 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.5 # +1.5
assert the_algo.calculate_regulated_temperature(18.1, 10) == 21.6 # +1.6
assert the_algo.calculate_regulated_temperature(18.3, 10) == 21.6 # +1.6
assert the_algo.calculate_regulated_temperature(18.5, 10) == 21.7 # +1.7
assert the_algo.calculate_regulated_temperature(18.7, 10) == 21.7 # +1.7
assert the_algo.calculate_regulated_temperature(19, 10) == 21.7 # +1.7
assert the_algo.calculate_regulated_temperature(20, 10) == 21.5 # +1.5
assert the_algo.calculate_regulated_temperature(21, 10) == 20.8 # +0.8
assert the_algo.calculate_regulated_temperature(21, 10) == 20.7 # +0.7
assert the_algo.calculate_regulated_temperature(20, 10) == 20.9 # +0.7
# Test temperature external
assert the_algo.calculate_regulated_temperature(20, 12) == 20.8 # +0.8
assert the_algo.calculate_regulated_temperature(20, 15) == 20.5 # +0.5
assert the_algo.calculate_regulated_temperature(20, 18) == 20.2 # +0.2
assert the_algo.calculate_regulated_temperature(20, 20) == 20.0 # =
def test_pi_algorithm_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.5 # +1.5
assert the_algo.calculate_regulated_temperature(18.1, 10) == 21.6 # +1.6
assert the_algo.calculate_regulated_temperature(18.3, 10) == 21.6 # +1.6
assert the_algo.calculate_regulated_temperature(18.5, 10) == 21.7 # +1.7
assert the_algo.calculate_regulated_temperature(18.7, 10) == 21.7 # +1.7
assert the_algo.calculate_regulated_temperature(19, 10) == 21.7 # +1.7
assert the_algo.calculate_regulated_temperature(20, 10) == 21.5 # +1.5
assert the_algo.calculate_regulated_temperature(21, 10) == 20.8 # +0.8
assert the_algo.calculate_regulated_temperature(21, 10) == 20.7 # +0.7
assert the_algo.calculate_regulated_temperature(20, 10) == 20.9 # +0.7
# Test temperature external
assert the_algo.calculate_regulated_temperature(20, 12) == 20.8 # +0.8
assert the_algo.calculate_regulated_temperature(20, 15) == 20.5 # +0.5
assert the_algo.calculate_regulated_temperature(20, 18) == 20.2 # +0.2
assert the_algo.calculate_regulated_temperature(20, 20) == 20.0 # =
def test_pi_algorithm_medium():
""" 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.2
assert the_algo.calculate_regulated_temperature(18.1, 10) == 22.3
assert the_algo.calculate_regulated_temperature(18.3, 10) == 22.4
assert the_algo.calculate_regulated_temperature(18.5, 10) == 22.5
assert the_algo.calculate_regulated_temperature(18.7, 10) == 22.5
assert the_algo.calculate_regulated_temperature(19, 10) == 22.4
assert the_algo.calculate_regulated_temperature(20, 10) == 21.9
assert the_algo.calculate_regulated_temperature(21, 10) == 20.4
assert the_algo.calculate_regulated_temperature(21, 10) == 20.3
assert the_algo.calculate_regulated_temperature(20, 10) == 20.8
# Test temperature external
assert the_algo.calculate_regulated_temperature(20, 8) == 21.2
assert the_algo.calculate_regulated_temperature(20, 6) == 21.4
assert the_algo.calculate_regulated_temperature(20, 4) == 21.6
assert the_algo.calculate_regulated_temperature(20, 2) == 21.8
assert the_algo.calculate_regulated_temperature(20, 0) == 22.0
assert the_algo.calculate_regulated_temperature(20, -2) == 22.2
assert the_algo.calculate_regulated_temperature(20, -4) == 22.4
assert the_algo.calculate_regulated_temperature(20, -6) == 22.6
assert the_algo.calculate_regulated_temperature(20, -8) == 22.8
# 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.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
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.6
assert the_algo.calculate_regulated_temperature(18.1, 10) == 23.9
assert the_algo.calculate_regulated_temperature(18.3, 10) == 24.0
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) == 21.2
assert the_algo.calculate_regulated_temperature(21, 10) == 21
assert the_algo.calculate_regulated_temperature(21, 10) == 20.8
assert the_algo.calculate_regulated_temperature(21, 10) == 20.6
assert the_algo.calculate_regulated_temperature(21, 10) == 20.4
assert the_algo.calculate_regulated_temperature(21, 10) == 20.2
assert the_algo.calculate_regulated_temperature(21, 10) == 20
# Test temperature external
assert the_algo.calculate_regulated_temperature(20, 8) == 21.0
assert the_algo.calculate_regulated_temperature(20, 6) == 22.8
assert the_algo.calculate_regulated_temperature(20, 4) == 23.2
assert the_algo.calculate_regulated_temperature(20, 2) == 23.6
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.8
assert the_algo.calculate_regulated_temperature(19, 10) == 23
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
assert the_algo.calculate_regulated_temperature(19, 10) == 24
assert the_algo.calculate_regulated_temperature(19, 10) == 24
assert the_algo.calculate_regulated_temperature(19, 10) == 24
assert the_algo.calculate_regulated_temperature(19, 10) == 24

View File

@@ -1,12 +1,11 @@
# 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 +49,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 +80,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 +135,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 +161,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(
@@ -196,7 +195,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(
@@ -270,7 +269,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
@@ -282,7 +281,7 @@ async def test_power_management_energy_over_switch(
# 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(
@@ -305,13 +304,13 @@ async def test_power_management_energy_over_switch(
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 +332,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 +397,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

View File

@@ -1,11 +1,15 @@
# 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 .commons import * # pylint: disable=wildcard-import, unused-wildcard-import
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)
@@ -55,7 +59,7 @@ async def test_security_feature(hass: HomeAssistant, skip_hass_states_is_state):
# 1. creates a thermostat and check that security is off
now: datetime = datetime.now(tz=tz)
entity: VersatileThermostat = await create_thermostat(
entity: ThermostatOverSwitch = await create_thermostat(
hass, entry, "climate.theoverswitchmockname"
)
assert entity
@@ -87,7 +91,7 @@ async def test_security_feature(hass: HomeAssistant, skip_hass_states_is_state):
# 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:
@@ -134,7 +138,7 @@ async def test_security_feature(hass: HomeAssistant, skip_hass_states_is_state):
# 3. Change the preset to Boost (we should stay in SECURITY)
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:
@@ -149,7 +153,7 @@ async def test_security_feature(hass: HomeAssistant, skip_hass_states_is_state):
# 5. resolve the datetime issue
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:
@@ -189,3 +193,117 @@ async def test_security_feature(hass: HomeAssistant, skip_hass_states_is_state):
# 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, HVACAction.HEATING)
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
return_value=fake_underlying_climate,
) as mock_find_climate:
entry.add_to_hass(hass)
await hass.config_entries.async_setup(entry.entry_id)
assert entry.state is ConfigEntryState.LOADED
def find_my_entity(entity_id) -> ClimateEntity:
"""Find my new entity"""
component: EntityComponent[ClimateEntity] = hass.data[CLIMATE_DOMAIN]
for entity in component.entities:
if entity.entity_id == entity_id:
return entity
entity: ThermostatOverClimate = find_my_entity("climate.theoverclimatemockname")
assert entity
assert entity.name == "TheOverClimateMockName"
assert entity.is_over_climate is True
# Because the underlying is HEATING. In real life the underlying will be shut-off
assert entity.hvac_action is HVACAction.HEATING
assert entity.hvac_mode is HVACMode.OFF
assert entity.target_temperature == entity.min_temp
assert entity.preset_modes == [
PRESET_NONE,
PRESET_ECO,
PRESET_COMFORT,
PRESET_BOOST,
]
assert entity.preset_mode is PRESET_NONE
assert entity._security_state is False
# should have been called with EventType.PRESET_EVENT and EventType.HVAC_MODE_EVENT
assert mock_send_event.call_count == 2
mock_send_event.assert_has_calls(
[
# At startup
call.send_event(EventType.PRESET_EVENT, {"preset": PRESET_NONE}),
call.send_event(
EventType.HVAC_MODE_EVENT,
{"hvac_mode": HVACMode.OFF},
),
]
)
assert mock_find_climate.call_count == 1
assert mock_find_climate.mock_calls[0] == call()
mock_find_climate.assert_has_calls([call.find_underlying_entity()])
# Force security mode
assert entity._last_ext_temperature_mesure is not None
assert entity._last_temperature_mesure is not None
assert (entity._last_temperature_mesure.astimezone(tz) - now).total_seconds() < 1
assert (
entity._last_ext_temperature_mesure.astimezone(tz) - now
).total_seconds() < 1
# Tries to turns on the Thermostat
await entity.async_set_hvac_mode(HVACMode.HEAT)
assert entity.hvac_mode == HVACMode.HEAT
# One call more
assert mock_send_event.call_count == 3
mock_send_event.assert_has_calls(
[
call.send_event(
EventType.HVAC_MODE_EVENT,
{"hvac_mode": HVACMode.HEAT},
),
]
)
# 2. activate security feature when date is expired
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
):
event_timestamp = now - timedelta(minutes=6)
await send_temperature_change_event(entity, 15, event_timestamp)
# Should stay False because a climate is never in security mode
assert entity.security_state is False
assert entity.preset_mode == 'none'
assert entity._saved_preset_mode == 'none'

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,9 @@ 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 +32,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,12 +45,13 @@ 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
@@ -93,7 +98,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,9 +117,10 @@ 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
@@ -160,7 +166,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,12 +179,12 @@ 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

140
tests/test_switch_ac.py Normal file
View File

@@ -0,0 +1,140 @@
""" 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.COOL, HVACMode.OFF]
assert entity.target_temperature == entity.max_temp
assert entity.preset_modes == [
PRESET_NONE,
PRESET_ECO,
PRESET_COMFORT,
PRESET_BOOST,
PRESET_ACTIVITY,
]
assert entity.preset_mode is PRESET_NONE
assert entity._security_state is False # pylint: disable=protected-access
assert entity._window_state is None # pylint: disable=protected-access
assert entity._motion_state is None # pylint: disable=protected-access
assert entity._presence_state is None # pylint: disable=protected-access
assert entity._prop_algorithm is not None # pylint: disable=protected-access
# should have been called with EventType.PRESET_EVENT and EventType.HVAC_MODE_EVENT
assert mock_send_event.call_count == 2
mock_send_event.assert_has_calls(
[
call.send_event(EventType.PRESET_EVENT, {"preset": PRESET_NONE}),
call.send_event(
EventType.HVAC_MODE_EVENT,
{"hvac_mode": HVACMode.OFF},
),
]
)
# Select a hvacmode, presence and preset
await entity.async_set_hvac_mode(HVACMode.COOL)
assert entity.hvac_mode is HVACMode.COOL
event_timestamp = now - timedelta(minutes=4)
await send_presence_change_event(entity, True, False, event_timestamp)
assert entity._presence_state == STATE_ON # pylint: disable=protected-access
await entity.async_set_hvac_mode(HVACMode.COOL)
assert entity.hvac_mode is HVACMode.COOL
await entity.async_set_preset_mode(PRESET_COMFORT)
assert entity.preset_mode is PRESET_COMFORT
assert entity.target_temperature == 23
# switch to Eco
await entity.async_set_preset_mode(PRESET_ECO)
assert entity.preset_mode is PRESET_ECO
assert entity.target_temperature == 25
# Unset the presence
event_timestamp = now - timedelta(minutes=3)
await send_presence_change_event(entity, False, True, event_timestamp)
assert entity._presence_state == STATE_OFF # pylint: disable=protected-access
assert entity.target_temperature == 27 # eco_ac_away
# Open a window
with patch(
"homeassistant.helpers.condition.state", return_value=True
):
event_timestamp = now - timedelta(minutes=2)
try_condition = await send_window_change_event(entity, True, False, event_timestamp)
# Confirme the window event
await try_condition(None)
assert entity.hvac_mode is HVACMode.OFF
assert entity.hvac_action is HVACAction.OFF
assert entity.target_temperature == 27 # eco_ac_away
# Close a window
with patch(
"homeassistant.helpers.condition.state", return_value=True
):
event_timestamp = now - timedelta(minutes=2)
try_condition = await send_window_change_event(entity, False, True, event_timestamp)
# Confirme the window event
await try_condition(None)
assert entity.hvac_mode is HVACMode.COOL
assert (entity.hvac_action is HVACAction.OFF or entity.hvac_action is HVACAction.IDLE)
assert entity.target_temperature == 27 # eco_ac_away

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

259
tests/test_valve.py Normal file
View File

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

View File

@@ -1,10 +1,12 @@
# pylint: disable=unused-argument, line-too-long, protected-access
""" Test the Window management """
import asyncio
import logging
from unittest.mock import patch, call, PropertyMock
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)
@@ -47,7 +49,7 @@ async def test_window_management_time_not_enough(
},
)
entity: VersatileThermostat = await create_thermostat(
entity: BaseThermostat = await create_thermostat(
hass, entry, "climate.theoverswitchmockname"
)
assert entity
@@ -66,7 +68,7 @@ async def test_window_management_time_not_enough(
# 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"
"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(
@@ -94,7 +96,7 @@ async def test_window_management_time_not_enough(
await try_window_condition(None)
assert entity.window_state == STATE_OFF
await entity.remove_thermostat()
entity.remove_thermostat()
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@@ -135,7 +137,7 @@ async def test_window_management_time_enough(
},
)
entity: VersatileThermostat = await create_thermostat(
entity: BaseThermostat = await create_thermostat(
hass, entry, "climate.theoverswitchmockname"
)
assert entity
@@ -154,7 +156,7 @@ async def test_window_management_time_enough(
# change temperature to force turning on the heater
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(
@@ -172,7 +174,7 @@ async def test_window_management_time_enough(
# 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"
"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(
@@ -200,7 +202,7 @@ async def test_window_management_time_enough(
# Close the window
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(
@@ -234,7 +236,7 @@ async def test_window_management_time_enough(
assert entity.preset_mode is PRESET_BOOST
# Clean the entity
await entity.remove_thermostat()
entity.remove_thermostat()
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@@ -274,7 +276,7 @@ async def test_window_auto_fast(hass: HomeAssistant, skip_hass_states_is_state):
},
)
entity: VersatileThermostat = await create_thermostat(
entity: BaseThermostat = await create_thermostat(
hass, entry, "climate.theoverswitchmockname"
)
assert entity
@@ -296,7 +298,7 @@ async def test_window_auto_fast(hass: HomeAssistant, skip_hass_states_is_state):
# Make the temperature 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(
@@ -318,7 +320,7 @@ async def test_window_auto_fast(hass: HomeAssistant, skip_hass_states_is_state):
# send one degre down in one minute
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 +355,7 @@ async def test_window_auto_fast(hass: HomeAssistant, skip_hass_states_is_state):
# send another 0.1 degre in one minute -> no change
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 +380,7 @@ async def test_window_auto_fast(hass: HomeAssistant, skip_hass_states_is_state):
# send another plus 1.1 degre in one minute -> restore state
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(
@@ -418,13 +420,13 @@ async def test_window_auto_fast(hass: HomeAssistant, skip_hass_states_is_state):
assert entity.hvac_mode is HVACMode.HEAT
# Clean the entity
await entity.remove_thermostat()
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"""
"""Test the Window auto management"""
entry = MockConfigEntry(
domain=DOMAIN,
@@ -458,7 +460,7 @@ async def test_window_auto_auto_stop(hass: HomeAssistant, skip_hass_states_is_st
},
)
entity: VersatileThermostat = await create_thermostat(
entity: BaseThermostat = await create_thermostat(
hass, entry, "climate.theoverswitchmockname"
)
assert entity
@@ -480,7 +482,7 @@ async def test_window_auto_auto_stop(hass: HomeAssistant, skip_hass_states_is_st
# Make the temperature 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(
@@ -501,7 +503,7 @@ async def test_window_auto_auto_stop(hass: HomeAssistant, skip_hass_states_is_st
# send one degre down in one minute
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(
@@ -539,7 +541,7 @@ async def test_window_auto_auto_stop(hass: HomeAssistant, skip_hass_states_is_st
# Waits for automatic disable
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(
@@ -561,7 +563,7 @@ async def test_window_auto_auto_stop(hass: HomeAssistant, skip_hass_states_is_st
assert entity.preset_mode is PRESET_BOOST
# Clean the entity
await entity.remove_thermostat()
entity.remove_thermostat()
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@@ -603,7 +605,7 @@ async def test_window_auto_no_on_percent(
},
)
entity: VersatileThermostat = await create_thermostat(
entity: BaseThermostat = await create_thermostat(
hass, entry, "climate.theoverswitchmockname"
)
assert entity
@@ -625,7 +627,7 @@ async def test_window_auto_no_on_percent(
# Make the temperature 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(
@@ -647,7 +649,7 @@ async def test_window_auto_no_on_percent(
# send one degre down in one minute
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(
@@ -670,4 +672,381 @@ async def test_window_auto_no_on_percent(
assert entity.hvac_mode is HVACMode.HEAT
# Clean the entity
await entity.remove_thermostat()
entity.remove_thermostat()
#PR - Adding Window Bypass
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_window_bypass(
hass: HomeAssistant, skip_hass_states_is_state
):
"""Test the Window management when bypass enabled"""
entry = MockConfigEntry(
domain=DOMAIN,
title="TheOverSwitchMockName",
unique_id="uniqueId",
data={
CONF_NAME: "TheOverSwitchMockName",
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_SWITCH,
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
CONF_CYCLE_MIN: 5,
CONF_TEMP_MIN: 15,
CONF_TEMP_MAX: 30,
"eco_temp": 17,
"comfort_temp": 18,
"boost_temp": 19,
CONF_USE_WINDOW_FEATURE: True,
CONF_USE_MOTION_FEATURE: False,
CONF_USE_POWER_FEATURE: False,
CONF_USE_PRESENCE_FEATURE: False,
CONF_HEATER: "switch.mock_switch",
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
CONF_TPI_COEF_INT: 0.3,
CONF_TPI_COEF_EXT: 0.01,
CONF_MINIMAL_ACTIVATION_DELAY: 30,
CONF_SECURITY_DELAY_MIN: 5,
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
CONF_WINDOW_SENSOR: "binary_sensor.mock_window_sensor",
CONF_WINDOW_DELAY: 0, # important to not been obliged to wait
},
)
entity: BaseThermostat = await create_thermostat(
hass, entry, "climate.theoverswitchmockname"
)
assert entity
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.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,
):
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
# Set Window ByPass to true
await entity.service_set_window_bypass_state(True)
assert entity.window_bypass_state is True
# entity._window_bypass_state = True
# Open the window, condition of time is satisfied, check the thermostat and heater turns off
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off, 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 == 0
# 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 == 0
assert mock_condition.call_count == 1
assert entity.hvac_mode is HVACMode.HEAT
assert entity.window_state == STATE_ON
# Close the window
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off, patch(
"homeassistant.helpers.condition.state", return_value=True
) as mock_condition, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
return_value=False,
):
try_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 == 0
assert mock_send_event.call_count == 0
assert entity.hvac_mode is HVACMode.HEAT
assert entity.preset_mode is PRESET_BOOST
# Clean the entity
entity.remove_thermostat()
#PR - Adding Window bypass for window auto algorithm
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_window_auto_bypass(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: 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: BaseThermostat = 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.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,
):
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 window bypass on
await entity.service_set_window_bypass_state(True)
assert entity.window_bypass_state is True
# entity._window_bypass_state = True
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,
):
event_timestamp = now - timedelta(minutes=3)
await send_temperature_change_event(entity, 18, event_timestamp, sleep=False)
# No change should have been done
assert mock_send_event.call_count == 0
assert mock_heater_on.call_count == 0
assert mock_heater_off.call_count == 0
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_OFF
assert entity.hvac_mode is HVACMode.HEAT
# Clean the entity
entity.remove_thermostat()
#PR - Adding Window bypass AFTER detection have been done should reactivate the heater
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_window_bypass_reactivate(hass: HomeAssistant, skip_hass_states_is_state):
"""Test the Window management when window is open and then bypass is set to on"""
entry = MockConfigEntry(
domain=DOMAIN,
title="TheOverSwitchMockName",
unique_id="uniqueId",
data={
CONF_NAME: "TheOverSwitchMockName",
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_SWITCH,
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
CONF_CYCLE_MIN: 5,
CONF_TEMP_MIN: 15,
CONF_TEMP_MAX: 30,
"eco_temp": 17,
"comfort_temp": 18,
"boost_temp": 19,
CONF_USE_WINDOW_FEATURE: True,
CONF_USE_MOTION_FEATURE: False,
CONF_USE_POWER_FEATURE: False,
CONF_USE_PRESENCE_FEATURE: False,
CONF_HEATER: "switch.mock_switch",
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
CONF_TPI_COEF_INT: 0.3,
CONF_TPI_COEF_EXT: 0.01,
CONF_MINIMAL_ACTIVATION_DELAY: 30,
CONF_SECURITY_DELAY_MIN: 5,
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
CONF_WINDOW_SENSOR: "binary_sensor.mock_window_sensor",
CONF_WINDOW_DELAY: 0, # important to not been obliged to wait
},
)
entity: BaseThermostat = await create_thermostat(
hass, entry, "climate.theoverswitchmockname"
)
assert entity
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.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,
):
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.base_thermostat.BaseThermostat.send_event"
) as mock_send_event, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off, patch(
"homeassistant.helpers.condition.state", return_value=True
) as mock_condition, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
return_value=True,
):
await send_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
# Call the set bypass service to set bypass 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, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
return_value=False,
):
await entity.service_set_window_bypass_state(True)
assert entity.window_state == STATE_ON
assert entity.preset_mode is PRESET_BOOST
assert entity.hvac_mode is HVACMode.HEAT
# 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,
)
# Clean the entity
entity.remove_thermostat()