Compare commits
58 Commits
3.2.0.beta
...
3.6.0.beta
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8b88ed5c9c | ||
|
|
c0d422a916 | ||
|
|
954fc6271c | ||
|
|
6dc078871d | ||
|
|
cd0ab3c88d | ||
|
|
eb54f2826f | ||
|
|
043fd5f7aa | ||
|
|
7e4e407732 | ||
|
|
81900ceeea | ||
|
|
66297c6044 | ||
|
|
f384225b0f | ||
|
|
e6ecd100f6 | ||
|
|
7ac49d7864 | ||
|
|
40da04838d | ||
|
|
fcdd93b4ae | ||
|
|
56fdbf4fba | ||
|
|
ef994e300b | ||
|
|
72c4105bbd | ||
|
|
79eb4a0a0d | ||
|
|
b032198c66 | ||
|
|
487c118b44 | ||
|
|
e29ff0568b | ||
|
|
814e4d3b83 | ||
|
|
abb6531f49 | ||
|
|
f970c18eaf | ||
|
|
af51ef62e0 | ||
|
|
b38fbd9d78 | ||
|
|
6e8e72e343 | ||
|
|
2bebe3e210 | ||
|
|
aa3b87762d | ||
|
|
f4cabbf2c0 | ||
|
|
24b59e545b | ||
|
|
5997a26c73 | ||
|
|
fe4b9ced81 | ||
|
|
c4fc976007 | ||
|
|
31d862acab | ||
|
|
9709a9eed0 | ||
|
|
61eae8c066 | ||
|
|
e16daa3d53 | ||
|
|
90a6c926e3 | ||
|
|
64ce3aa0ad | ||
|
|
3f498ffbd3 | ||
|
|
3236be6c3b | ||
|
|
be86fd3ac0 | ||
|
|
e35ba57bd7 | ||
|
|
72d7803ffa | ||
|
|
4dd7c62a42 | ||
|
|
429ff47269 | ||
|
|
a17423d470 | ||
|
|
81a467b8c3 | ||
|
|
4e3ee0703b | ||
|
|
539ec4a6bd | ||
|
|
9b085f1264 | ||
|
|
637367bd65 | ||
|
|
bcc0a32b6a | ||
|
|
80fa977c15 | ||
|
|
67d20dd083 | ||
|
|
e2e8499bdb |
4
.bashrc
4
.bashrc
@@ -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
|
||||
|
||||
@@ -1,182 +1,215 @@
|
||||
default_config:
|
||||
|
||||
logger:
|
||||
default: info
|
||||
logs:
|
||||
custom_components.versatile_thermostat: debug
|
||||
default: info
|
||||
logs:
|
||||
custom_components.versatile_thermostat: debug
|
||||
custom_components.versatile_thermostat.underlyings: debug
|
||||
custom_components.versatile_thermostat.climate: debug
|
||||
|
||||
# If you need to debug uncommment the line below (doc: https://www.home-assistant.io/integrations/debugpy/)
|
||||
debugpy:
|
||||
start: true
|
||||
wait: false
|
||||
port: 5678
|
||||
start: true
|
||||
wait: false
|
||||
port: 5678
|
||||
|
||||
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
|
||||
|
||||
input_boolean:
|
||||
# input_boolean to simulate the windows entity. Only for development environment.
|
||||
fake_window_sensor1:
|
||||
name: Window 1
|
||||
icon: mdi:window-closed-variant
|
||||
# input_boolean to simulate the heater entity switch. Only for development environment.
|
||||
fake_heater_switch1:
|
||||
name: Heater 1 (Linear)
|
||||
icon: mdi:radiator
|
||||
fake_heater_switch2:
|
||||
name: Heater (TPI with presence preset)
|
||||
icon: mdi:radiator
|
||||
fake_heater_switch3:
|
||||
name: Heater (TPI with offset)
|
||||
icon: mdi:radiator
|
||||
# input_boolean to simulate the motion sensor entity. Only for development environment.
|
||||
fake_motion_sensor1:
|
||||
name: Motion Sensor 1
|
||||
icon: mdi:run
|
||||
# input_boolean to simulate the presence sensor entity. Only for development environment.
|
||||
fake_presence_sensor1:
|
||||
name: Presence Sensor 1
|
||||
icon: mdi:home
|
||||
# input_boolean to simulate the windows entity. Only for development environment.
|
||||
fake_window_sensor1:
|
||||
name: Window 1
|
||||
icon: mdi:window-closed-variant
|
||||
# input_boolean to simulate the heater entity switch. Only for development environment.
|
||||
fake_heater_switch3:
|
||||
name: Heater 3
|
||||
icon: mdi:radiator
|
||||
fake_heater_switch2:
|
||||
name: Heater 2
|
||||
icon: mdi:radiator
|
||||
fake_heater_switch1:
|
||||
name: Heater 1
|
||||
icon: mdi:radiator
|
||||
fake_heater_ac1:
|
||||
name: Air contionner 1
|
||||
icon: mdi:air-conditioner
|
||||
fake_heater_4switch1:
|
||||
name: Heater (multiswitch1)
|
||||
icon: mdi:radiator
|
||||
fake_heater_4switch2:
|
||||
name: Heater (multiswitch2)
|
||||
icon: mdi:radiator
|
||||
fake_heater_4switch3:
|
||||
name: Heater (multiswitch3)
|
||||
icon: mdi:radiator
|
||||
fake_heater_4switch4:
|
||||
name: Heater (multiswitch4)
|
||||
icon: mdi:radiator
|
||||
fake_heater_4climate1:
|
||||
name: Heater (multiclimate1)
|
||||
icon: mdi:radiator
|
||||
fake_heater_4climate2:
|
||||
name: Heater (multiclimate2)
|
||||
icon: mdi:radiator
|
||||
fake_heater_4climate3:
|
||||
name: Heater (multiclimate3)
|
||||
icon: mdi:radiator
|
||||
fake_heater_4climate4:
|
||||
name: Heater (multiclimate4)
|
||||
icon: mdi:radiator
|
||||
# input_boolean to simulate the motion sensor entity. Only for development environment.
|
||||
fake_motion_sensor1:
|
||||
name: Motion Sensor 1
|
||||
icon: mdi:run
|
||||
# input_boolean to simulate the presence sensor entity. Only for development environment.
|
||||
fake_presence_sensor1:
|
||||
name: Presence Sensor 1
|
||||
icon: mdi:home
|
||||
|
||||
climate:
|
||||
- platform: generic_thermostat
|
||||
name: Underlying thermostat1
|
||||
heater: input_boolean.fake_heater_switch3
|
||||
target_sensor: input_number.fake_temperature_sensor1
|
||||
- platform: generic_thermostat
|
||||
name: Underlying thermostat2
|
||||
heater: input_boolean.fake_heater_switch3
|
||||
target_sensor: input_number.fake_temperature_sensor1
|
||||
- platform: generic_thermostat
|
||||
name: Underlying thermostat3
|
||||
heater: input_boolean.fake_heater_switch3
|
||||
target_sensor: input_number.fake_temperature_sensor1
|
||||
- platform: generic_thermostat
|
||||
name: Underlying thermostat4
|
||||
heater: input_boolean.fake_heater_switch3
|
||||
target_sensor: input_number.fake_temperature_sensor1
|
||||
- platform: generic_thermostat
|
||||
name: Underlying thermostat5
|
||||
heater: input_boolean.fake_heater_switch3
|
||||
target_sensor: input_number.fake_temperature_sensor1
|
||||
- platform: generic_thermostat
|
||||
name: Underlying thermostat6
|
||||
heater: input_boolean.fake_heater_switch3
|
||||
target_sensor: input_number.fake_temperature_sensor1
|
||||
- platform: generic_thermostat
|
||||
name: Underlying thermostat7
|
||||
heater: input_boolean.fake_heater_switch3
|
||||
target_sensor: input_number.fake_temperature_sensor1
|
||||
- platform: generic_thermostat
|
||||
name: Underlying thermostat8
|
||||
heater: input_boolean.fake_heater_switch3
|
||||
target_sensor: input_number.fake_temperature_sensor1
|
||||
- platform: generic_thermostat
|
||||
name: Underlying thermostat9
|
||||
heater: input_boolean.fake_heater_switch3
|
||||
target_sensor: input_number.fake_temperature_sensor1
|
||||
- platform: generic_thermostat
|
||||
name: Underlying thermostat1
|
||||
heater: input_boolean.fake_heater_switch3
|
||||
target_sensor: input_number.fake_temperature_sensor1
|
||||
- platform: generic_thermostat
|
||||
name: Underlying thermostat2
|
||||
heater: input_boolean.fake_heater_switch3
|
||||
target_sensor: input_number.fake_temperature_sensor1
|
||||
- platform: generic_thermostat
|
||||
name: Underlying thermostat3
|
||||
heater: input_boolean.fake_heater_switch3
|
||||
target_sensor: input_number.fake_temperature_sensor1
|
||||
- platform: generic_thermostat
|
||||
name: Underlying thermostat4
|
||||
heater: input_boolean.fake_heater_switch3
|
||||
target_sensor: input_number.fake_temperature_sensor1
|
||||
- platform: generic_thermostat
|
||||
name: Underlying thermostat 4-1
|
||||
heater: input_boolean.fake_heater_4climate1
|
||||
target_sensor: input_number.fake_temperature_sensor1
|
||||
ac_mode: false
|
||||
- platform: generic_thermostat
|
||||
name: Underlying thermostat 4-2
|
||||
heater: input_boolean.fake_heater_4climate2
|
||||
target_sensor: input_number.fake_temperature_sensor1
|
||||
ac_mode: false
|
||||
- platform: generic_thermostat
|
||||
name: Underlying thermostat 4-3
|
||||
heater: input_boolean.fake_heater_4climate3
|
||||
target_sensor: input_number.fake_temperature_sensor1
|
||||
ac_mode: false
|
||||
- platform: generic_thermostat
|
||||
name: Underlying thermostat 4-4
|
||||
heater: input_boolean.fake_heater_4climate4
|
||||
target_sensor: input_number.fake_temperature_sensor1
|
||||
ac_mode: false
|
||||
- platform: generic_thermostat
|
||||
name: Underlying thermostat9
|
||||
heater: input_boolean.fake_heater_switch3
|
||||
target_sensor: input_number.fake_temperature_sensor1
|
||||
|
||||
recorder:
|
||||
include:
|
||||
domains:
|
||||
- input_boolean
|
||||
- input_number
|
||||
- switch
|
||||
- climate
|
||||
- sensor
|
||||
include:
|
||||
domains:
|
||||
- input_boolean
|
||||
- input_number
|
||||
- switch
|
||||
- climate
|
||||
- sensor
|
||||
|
||||
template:
|
||||
- binary_sensor:
|
||||
- name: maison_occupee
|
||||
unique_id: maison_occupee
|
||||
state: "{{is_state('person.jmc', 'home') }}"
|
||||
device_class: occupancy
|
||||
- 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"
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
2
.github/workflows/cron.yaml
vendored
2
.github/workflows/cron.yaml
vendored
@@ -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
17
.github/workflows/hacs.yml
vendored
Normal 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"
|
||||
16
.github/workflows/pull.yml
vendored
16
.github/workflows/pull.yml
vendored
@@ -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 \
|
||||
|
||||
6
.github/workflows/push.yml
vendored
6
.github/workflows/push.yml
vendored
@@ -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
6
.gitignore
vendored
@@ -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
35
.vscode/launch.json
vendored
@@ -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
16
.vscode/settings.json
vendored
@@ -1,12 +1,20 @@
|
||||
{
|
||||
"[python]": {
|
||||
"editor.defaultFormatter": "ms-python.black-formatter"
|
||||
},
|
||||
"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
4
.vscode/tasks.json
vendored
@@ -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
61
CONTRIBUTING-fr.md
Normal 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
61
CONTRIBUTING.md
Normal 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.
|
||||
114
README-fr.md
114
README-fr.md
@@ -6,8 +6,9 @@
|
||||
|
||||

|
||||
|
||||
>  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. ;-).
|
||||
>  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](#merci-pour-la-bière-buymecoffee)
|
||||
- [Quand l'utiliser et ne pas l'utiliser](#quand-lutiliser-et-ne-pas-lutiliser)
|
||||
- [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-)
|
||||
@@ -15,7 +16,7 @@
|
||||
- [Installation manuelle](#installation-manuelle)
|
||||
- [Configuration](#configuration)
|
||||
- [Choix des attributs de base](#choix-des-attributs-de-base)
|
||||
- [Sélectionnez l'entité pilotée](#sélectionnez-lentité-pilotée)
|
||||
- [Sélectionnez des entités pilotées](#sélectionnez-des-entités-pilotées)
|
||||
- [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)
|
||||
@@ -25,6 +26,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)
|
||||
@@ -53,12 +55,21 @@ Ce composant personnalisé pour Home Assistant est une mise à niveau et est une
|
||||
|
||||
|
||||
>  _*Nouveautés*_
|
||||
> * **Release 3.6**: Ajout du paramètre `motion_off_delay` pour la gestion de l'activité.
|
||||
> * **Release 3.5**: Plusieurs thermostats sont possibles en "thermostat over climate" mode [#113](https://github.com/jmcollin78/versatile_thermostat/issues/113)
|
||||
> * **Release 3.4**: bug fix et exposition des preset temperatures pour le mode AC [#103](https://github.com/jmcollin78/versatile_thermostat/issues/103)
|
||||
> * **Release 3.3**: ajout du mode Air Conditionné (AC). Cette fonction vous permet d'utiliser le mode AC de votre thermostat sous-jacent. Pour l'utiliser, vous devez cocher l'option "Uitliser le mode AC" et définir les valeurs de température pour les presets et pour les presets en cas d'absence
|
||||
> * **Release 3.2** : ajout de la possibilité de commander plusieurs switch à partir du même thermostat. Dans ce mode, les switchs sont déclenchés avec un délai pour minimiser la puissance nécessaire à un instant (on minimise les périodes de recouvrement). Voir [Configuration](#sélectionnez-des-entités-pilotées)
|
||||
> * **Release 3.1** : ajout d'une détection de fenêtres/portes ouvertes par chute de température. Cette nouvelle fonction permet de stopper automatiquement un radiateur lorsque la température chute brutalement. Voir [Le mode auto](#le-mode-auto)
|
||||
> * **Release majeure 3.0** : ajout d'un équipement thermostat et de capteurs (binaires et non binaires) associés. Beaucoup plus proche de la philosphie Home Assistant, vous avez maintenant un accès direct à l'énergie consommée par le radiateur piloté par le thermostat et à plein d'autres capteurs qui seront utiles dans vos automatisations et dashboard.
|
||||
> * **release 2.3** : ajout de la mesure de puissance et d'énergie du radiateur piloté par le thermostat.
|
||||
> * **release 2.2** : ajout de fonction de sécurité permettant de ne pas laisser éternellement en chauffe un radiateur en cas de panne du thermomètre
|
||||
> * **release majeure 2.0** : ajout du thermostat "over climate" permettant de transformer n'importe quel thermostat en Versatile Thermostat et lui ajouter toutes les fonctions de ce dernier.
|
||||
|
||||
# Merci pour la bière [buymecoffee](https://www.buymeacoffee.com/jmcollin78)
|
||||
Un grand merci à @salabur, @pvince83, @bergoglio, @EPicLURcher, @Kriss1670, @maia pour les bières. Ca fait très plaisir.
|
||||
|
||||
|
||||
# Quand l'utiliser et ne pas l'utiliser
|
||||
Ce thermostat peut piloter 2 types d'équipement:
|
||||
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 :
|
||||
@@ -66,9 +77,7 @@ Ce thermostat peut piloter 2 types d'équipement:
|
||||
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)
|
||||
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)
|
||||
a. un équipement - comme une climatisation une valve thermostatique - qui est pilotée par sa propre entity de type ```climate```,
|
||||
|
||||
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.
|
||||
|
||||
@@ -139,12 +148,16 @@ Donnez les principaux attributs obligatoires :
|
||||
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
|
||||
|
||||
## Sélectionnez l'entité pilotée
|
||||
En fonction de votre choix sur le type de thermostat, vous devrez choisir une entité de type switch ou une entité de type climate. Seules les entités compatibles sont présentées.
|
||||
## Sélectionnez des entités pilotées
|
||||
En fonction de votre choix sur le type de thermostat, vous devrez choisir une ou plusieurs entités de type switch ou une entité de type climate. Seules les entités compatibles sont présentées.
|
||||
|
||||
Pour un thermostat de type ```thermostat_over_switch```:
|
||||

|
||||
L'algorithme à utiliser est aujourd'hui limité à TPI est disponible. Voir [algorithme](#algorithme)
|
||||
L'algorithme à utiliser est aujourd'hui limité à TPI est disponible. Voir [algorithme](#algorithme).
|
||||
Si plusieurs entités de type sont configurées, la thermostat décale les activations afin de minimiser le nombre de switch actif à un instant t. Ca permet une meilleure répartition de la puissance puisque chaque radiateur va s'allumer à son tour.
|
||||
Exemple de déclenchement synchronisé :
|
||||

|
||||
|
||||
|
||||
Pour un thermostat de type ```thermostat_over_climate```:
|
||||

|
||||
@@ -230,17 +243,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é ».
|
||||
|
||||
>  _*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
|
||||
@@ -302,6 +318,65 @@ Voir [exemple de réglages](#examples-tuning) pour avoir des exemples de réglag
|
||||
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.
|
||||
|
||||
## Synthèse des paramètres
|
||||
|
||||
| Paramètre | Libellé | "over switch" | "over climate" |
|
||||
| ----------| --------| --- | ---|
|
||||
| ``name`` | Nom | X | X |
|
||||
| ``thermostat_type`` | Type de thermostat | X | X |
|
||||
| ``temperature_sensor_entity_id`` | Temperature sensor entity id | X | - |
|
||||
| ``external_temperature_sensor_entity_id`` | Température exterieure sensor entity id | X | - |
|
||||
| ``cycle_min`` | Durée du cycle (minutes) | X | X |
|
||||
| ``temp_min`` | Température minimale permise | X | X |
|
||||
| ``temp_max`` | Température maximale permise | X | X |
|
||||
| ``device_power`` | Puissance de l'équipement | X | X |
|
||||
| ``use_window_feature`` | Avec détection des ouvertures | X | X |
|
||||
| ``use_motion_feature`` | Avec détection de mouvement | X | X |
|
||||
| ``use_power_feature`` | Avec gestion de la puissance | X | X |
|
||||
| ``use_presence_feature`` | Avec détection de présence | 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 |
|
||||
| ``ac_mode`` | utilisation de l'air conditionné (AC) ? | - | X |
|
||||
| ``tpi_coef_int`` | Coefficient à utiliser pour le delta de température interne | X | - |
|
||||
| ``tpi_coef_ext`` | Coefficient à utiliser pour le delta de température externe | X | - |
|
||||
| ``eco_temp`` | Température en preset Eco | X | X |
|
||||
| ``comfort_temp`` | Température en preset Confort | X | X |
|
||||
| ``boost_temp`` | Température en preset Boost | X | X |
|
||||
| ``eco_ac_temp`` | Température en preset Eco en mode AC | X | X |
|
||||
| ``comfort_ac_temp`` | Température en preset Confort en mode AC | X | X |
|
||||
| ``boost_ac_temp`` | Température en preset Boost en mode AC | X | X |
|
||||
| ``window_sensor_entity_id`` | Détecteur d'ouverture (entity id) | X | X |
|
||||
| ``window_delay`` | Délai avant extinction (secondes) | X | X |
|
||||
| ``window_auto_open_threshold`` | Seuil haut de chute de température pour la détection automatique (en °/min) | X | X |
|
||||
| ``window_auto_close_threshold`` | Seuil bas de chute de température pour la fin de détection automatique (en °/min) | X | X |
|
||||
| ``window_auto_max_duration`` | Durée maximum d'une extinction automatique (en min) | X | X |
|
||||
| ``motion_sensor_entity_id`` | Détecteur de mouvement entity id | X | X |
|
||||
| ``motion_delay`` | Délai avant prise en compte du mouvement (seconds) | X | X |
|
||||
| ``motion_off_delay`` | Délai avant prise en compte de la fin de mouvement (seconds) | X | X |
|
||||
| ``motion_preset`` | Preset à utiliser si mouvement détecté | X | X |
|
||||
| ``no_motion_preset`` | Preset à utiliser si pas de mouvement détecté | X | X |
|
||||
| ``power_sensor_entity_id`` | Capteur de puissance totale (entity id) | X | X |
|
||||
| ``max_power_sensor_entity_id`` | Capteur de puissance Max (entity id) | X | X |
|
||||
| ``power_temp`` | Température si délestaqe | X | X |
|
||||
| ``presence_sensor_entity_id`` | Capteur de présence entity id (true si quelqu'un est présent) | X | X |
|
||||
| ``eco_away_temp`` | Température en preset Eco en cas d'absence | X | X |
|
||||
| ``comfort_away_temp`` | Température en preset Comfort en cas d'absence | X | X |
|
||||
| ``boost_away_temp`` | Température en preset Boost en cas d'absence | X | X |
|
||||
| ``eco_ac_away_temp`` | Température en preset Eco en cas d'absence en mode AC | X | X |
|
||||
| ``comfort_ac_away_temp`` | Température en preset Comfort en cas d'absence en mode AC | X | X |
|
||||
| ``boost_ac_away_temp`` | Température en preset Boost en cas d'absence en mode AC | X | X |
|
||||
| ``minimal_activation_delay`` | Délai minimal d'activation | X | - |
|
||||
| ``security_delay_min`` | Délai maximal entre 2 mesures de températures | X | - |
|
||||
| ``security_min_on_percent`` | Pourcentage minimal de puissance pour passer en mode sécurité | X | - |
|
||||
| ``security_default_on_percent`` | Pourcentage de puissance a utiliser en mode securité | X | - |
|
||||
|
||||
# Exemples de réglage
|
||||
|
||||
## Chauffage électrique
|
||||
@@ -438,6 +513,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
|
||||
```
|
||||
|
||||
>  _*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.
|
||||
|
||||
|
||||
117
README.md
117
README.md
@@ -6,8 +6,9 @@
|
||||
|
||||

|
||||
|
||||
>  This thermostat integration aims to drastically simplify your automations around climate management. Because all classical events in climate are natively handled by the thermostat (nobody at home ?, activity detected in a room ?, window open ?, power shedding ?), you don't have to build over complicated scripts and automations to manage your climates ;-).
|
||||
>  This thermostat integration aims to drastically simplify your automations around climate management. Because all classical events in climate are natively handled by the thermostat (nobody at home ?, activity detected in a room ?, window open ?, power shedding ?), you don't have to build over complicated scripts and automations to manage your climates ;-).
|
||||
|
||||
- [Thanks for the beer buymecoffee](#thanks-for-the-beer-buymecoffee)
|
||||
- [When to use / not use](#when-to-use--not-use)
|
||||
- [Why another thermostat implementation ?](#why-another-thermostat-implementation-)
|
||||
- [How to install this incredible Versatile Thermostat ?](#how-to-install-this-incredible-versatile-thermostat-)
|
||||
@@ -25,6 +26,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)
|
||||
@@ -52,24 +54,31 @@
|
||||
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.
|
||||
|
||||
> _*News*_
|
||||
> * **Release 3.6**: Add a `motion_off_delay` parameter for activity management,
|
||||
> * **Release 3.5**: Multiple thermostats when using "thermostat over another thermostat" mode [#113](https://github.com/jmcollin78/versatile_thermostat/issues/113)
|
||||
> * **Release 3.4**: bug fixes and expose preset temperatures for AC mode [#103](https://github.com/jmcollin78/versatile_thermostat/issues/103)
|
||||
> * **Release 3.3**: add the Air Conditionned mode (AC). This feature allow to use the eventual AC mode of your underlying climate entity. You have to check the "Use AC mode" checkbox in configuration and give preset temperature value for AC mode and AC mode when absent if absence is configured
|
||||
> * **Release 3.2**: add the ability to control multiple switches from the same thermostat. In this mode, the switches are triggered with a delay to minimize the power required at one time (we minimize the recovery periods). See [Configuration](#select-the-driven-entity)
|
||||
> * **Release 3.1**: added detection of open windows/doors by temperature drop. This new function makes it possible to automatically stop a radiator when the temperature drops suddenly. See [Auto mode](#auto-mode)
|
||||
> * **Major release 3.0**: addition of thermostat equipment and associated sensors (binary and non-binary). Much closer to the Home Assistant philosophy, you now have direct access to the energy consumed by the radiator controlled by the thermostat and many other sensors that will be useful in your automations and dashboard.
|
||||
> * **release 2.3**: addition of the power and energy measurement of the radiator controlled by the thermostat.
|
||||
> * **release 2.2**: addition of a safety function allowing a radiator not to be left heating forever in the event of a thermometer failure
|
||||
> * **major release 2.0**: addition of the "over climate" thermostat allowing you to transform any thermostat into a Versatile Thermostat and add all the functions of the latter.
|
||||
|
||||
# Thanks for the beer [buymecoffee](https://www.buymeacoffee.com/jmcollin78)
|
||||
Many thanks to @@salabur, @pvince83, @bergoglio, @EPicLURcher, @Kriss1670, @maia for the beers. It's very pleasing.
|
||||
|
||||
|
||||
# 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)
|
||||
1. a heater that only works in on/off mode (named ```thermostat_over_switch```). Versatile Thermostat will regulate the length of a heating cycle and the pauses in-between by controlling a binary on/off switch. This mode is e.g. suitable for an electrical radiator controlled by a switch. The minimum configuration required to use this type of thermostat is:
|
||||
- an equipment such as a radiator (a ```switch``` or equivalent),
|
||||
- a temperature probe for the room (or an input_number),
|
||||
- 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```). Versatile Thermostat will regulate the target temperature of a climate entity. Common examples for this mode are the control of thermostatic radiator valves (TRV), air-conditions (AC), floor heating systems and pellet heating. For this type of thermostat, the minimum configuration requires:
|
||||
- an equipment such as air conditioning or thermostatic valve (TRV) which is controlled by its own ```climate``` type entity,
|
||||
|
||||
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.
|
||||
|
||||
# Why another thermostat implementation ?
|
||||
|
||||
@@ -141,6 +150,9 @@ Depending on your choice on the type of thermostat, you will have to choose a sw
|
||||
For a ```thermostat_over_switch``` thermostat:
|
||||

|
||||
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.
|
||||
Example of synchronized triggering:
|
||||

|
||||
|
||||
For a ```thermostat_over_climate``` thermostat:
|
||||

|
||||
@@ -208,6 +220,7 @@ And that's all ! your thermostat will turn off when the windows are open and tur
|
||||
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:
|
||||

|
||||
@@ -216,15 +229,17 @@ 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.
|
||||
|
||||
>  _*Notes*_
|
||||
@@ -287,6 +302,65 @@ See [example tuning](#examples-tuning) for common tuning examples
|
||||
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.
|
||||
|
||||
## Parameters synthesis
|
||||
|
||||
| Paramètre | Libellé | "over switch" | "over climate" |
|
||||
| ----------| --------| --- | --- |
|
||||
| ``name`` | Name | X | X |
|
||||
| ``thermostat_type`` | Thermostat type | X | X |
|
||||
| ``temperature_sensor_entity_id`` | Temperature sensor entity id | X | - |
|
||||
| ``external_temperature_sensor_entity_id`` | External temperature sensor entity id | X | - |
|
||||
| ``cycle_min`` | Cycle duration (minutes) | X | X |
|
||||
| ``temp_min`` | Minimal temperature allowed | X | X |
|
||||
| ``temp_max`` | Maximal temperature allowed | X | X |
|
||||
| ``device_power`` | Device power | X | X |
|
||||
| ``use_window_feature`` | Use window detection | X | X |
|
||||
| ``use_motion_feature`` | Use motion detection | X | X |
|
||||
| ``use_power_feature`` | Use power management | X | X |
|
||||
| ``use_presence_feature`` | Use presence detection | 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 | - |
|
||||
| ``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 |
|
||||
| ``ac_mode`` | Use the Air Conditioning (AC) mode | - | X |
|
||||
| ``tpi_coef_int`` | Coefficient to use for internal temperature delta | X | - |
|
||||
| ``tpi_coef_ext`` | Coefficient to use for external temperature delta | X | - |
|
||||
| ``eco_temp`` | Temperature in Eco preset | X | X |
|
||||
| ``comfort_temp`` | Temperature in Comfort preset | X | X |
|
||||
| ``boost_temp`` | Temperature in Boost preset | X | X |
|
||||
| ``eco_ac_temp`` | Temperature in Eco preset for AC mode | X | X |
|
||||
| ``comfort_ac_temp`` | Temperature in Comfort preset for AC mode | X | X |
|
||||
| ``boost_ac_temp`` | Temperature in Boost preset for AC mode | X | X |
|
||||
| ``window_sensor_entity_id`` | Window sensor entity id | X | X |
|
||||
| ``window_delay`` | Window sensor delay (seconds) | X | X |
|
||||
| ``window_auto_open_threshold`` | Temperature decrease threshold for automatic window open detection (in °/min) | X | X |
|
||||
| ``window_auto_close_threshold`` | Temperature increase threshold for end of automatic detection (in °/min) | X | X |
|
||||
| ``window_auto_max_duration`` | Maximum duration of automatic window open detection (in min) | X | X |
|
||||
| ``motion_sensor_entity_id`` | Motion sensor entity id | X | X |
|
||||
| ``motion_delay`` | Delay before considering the motion (seconds) | X | X |
|
||||
| ``motion_off_delay`` | Delay before considering the end of motion (seconds) | X | X |
|
||||
| ``motion_preset`` | Preset to use when motion is detected | X | X |
|
||||
| ``no_motion_preset`` | Preset to use when no motion is detected | X | X |
|
||||
| ``power_sensor_entity_id`` | Power sensor entity id | X | X |
|
||||
| ``max_power_sensor_entity_id`` | Max power sensor entity id | X | X |
|
||||
| ``power_temp`` | Temperature for Power shedding | X | X |
|
||||
| ``presence_sensor_entity_id`` | Presence sensor entity id | X | X |
|
||||
| ``eco_away_temp`` | Temperature in Eco preset when no presence | X | X |
|
||||
| ``comfort_away_temp`` | Temperature in Comfort preset when no presence | X | X |
|
||||
| ``boost_away_temp`` | Temperature in Boost preset when no presence | X | X |
|
||||
| ``eco_ac_away_temp`` | Temperature in Eco preset when no presence in AC mode | X | X |
|
||||
| ``comfort_ac_away_temp`` | Temperature in Comfort preset when no presence in AC mode | X | X |
|
||||
| ``boost_ac_away_temp`` | Temperature in Boost preset when no presence in AC mode | X | X |
|
||||
| ``minimal_activation_delay`` | Minimal activation delay | X | - |
|
||||
| ``security_delay_min`` | Security delay (in minutes) | X | X |
|
||||
| ``security_min_on_percent`` | Minimal power percent to enable security mode | X | X |
|
||||
| ``security_default_on_percent`` | Power percent to use in security mode | X | X |
|
||||
|
||||
# Examples tuning
|
||||
|
||||
## Electrical heater
|
||||
@@ -423,6 +497,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
|
||||
```
|
||||
|
||||
>  _*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.
|
||||
|
||||
|
||||
67
container
67
container
@@ -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
|
||||
|
||||
@@ -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,
|
||||
@@ -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
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -4,7 +4,8 @@ from datetime import timedelta
|
||||
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 .climate import VersatileThermostat
|
||||
@@ -19,7 +20,7 @@ class VersatileThermostatBaseEntity(Entity):
|
||||
_my_climate: VersatileThermostat
|
||||
hass: HomeAssistant
|
||||
_config_id: str
|
||||
_devince_name: str
|
||||
_device_name: str
|
||||
|
||||
def __init__(self, hass: HomeAssistant, config_id, device_name) -> None:
|
||||
"""The CTOR"""
|
||||
|
||||
@@ -41,6 +41,9 @@ from .const import (
|
||||
DOMAIN,
|
||||
CONF_NAME,
|
||||
CONF_HEATER,
|
||||
CONF_HEATER_2,
|
||||
CONF_HEATER_3,
|
||||
CONF_HEATER_4,
|
||||
CONF_TEMP_SENSOR,
|
||||
CONF_EXTERNAL_TEMP_SENSOR,
|
||||
CONF_POWER_SENSOR,
|
||||
@@ -52,13 +55,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,
|
||||
@@ -76,10 +82,14 @@ 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,
|
||||
UnknownEntity,
|
||||
WindowOpenDetectionMethod,
|
||||
@@ -129,35 +139,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."""
|
||||
|
||||
@@ -224,6 +205,21 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
|
||||
domain=[SWITCH_DOMAIN, INPUT_BOOLEAN_DOMAIN]
|
||||
),
|
||||
),
|
||||
vol.Optional(CONF_HEATER_2): selector.EntitySelector(
|
||||
selector.EntitySelectorConfig(
|
||||
domain=[SWITCH_DOMAIN, INPUT_BOOLEAN_DOMAIN]
|
||||
),
|
||||
),
|
||||
vol.Optional(CONF_HEATER_3): selector.EntitySelector(
|
||||
selector.EntitySelectorConfig(
|
||||
domain=[SWITCH_DOMAIN, INPUT_BOOLEAN_DOMAIN]
|
||||
),
|
||||
),
|
||||
vol.Optional(CONF_HEATER_4): selector.EntitySelector(
|
||||
selector.EntitySelectorConfig(
|
||||
domain=[SWITCH_DOMAIN, INPUT_BOOLEAN_DOMAIN]
|
||||
),
|
||||
),
|
||||
vol.Required(
|
||||
CONF_PROP_FUNCTION, default=PROPORTIONAL_FUNCTION_TPI
|
||||
): vol.In(
|
||||
@@ -231,6 +227,7 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
|
||||
PROPORTIONAL_FUNCTION_TPI,
|
||||
]
|
||||
),
|
||||
vol.Optional(CONF_AC_MODE, default=False): cv.boolean,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -239,6 +236,16 @@ 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,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -256,6 +263,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(
|
||||
@@ -278,6 +294,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
|
||||
),
|
||||
@@ -322,6 +339,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(
|
||||
@@ -471,9 +509,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) == 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"""
|
||||
@@ -524,9 +565,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) == 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,
|
||||
)
|
||||
@@ -658,9 +704,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) == 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"""
|
||||
@@ -718,9 +767,14 @@ class VersatileThermostatOptionsFlowHandler(
|
||||
"Into OptionsFlowHandler.async_step_presence user_input=%s", user_input
|
||||
)
|
||||
|
||||
if self._infos.get(CONF_AC_MODE) == 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,
|
||||
)
|
||||
|
||||
@@ -11,6 +11,11 @@ from homeassistant.components.climate import (
|
||||
ClimateEntityFeature,
|
||||
)
|
||||
|
||||
PRESET_AC_SUFFIX = "_ac"
|
||||
PRESET_ECO_AC = PRESET_ECO + PRESET_AC_SUFFIX
|
||||
PRESET_COMFORT_AC = PRESET_COMFORT + PRESET_AC_SUFFIX
|
||||
PRESET_BOOST_AC = PRESET_BOOST + PRESET_AC_SUFFIX
|
||||
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
|
||||
from .prop_algorithm import (
|
||||
@@ -30,6 +35,9 @@ DOMAIN = "versatile_thermostat"
|
||||
PLATFORMS: list[Platform] = [Platform.CLIMATE, Platform.BINARY_SENSOR, Platform.SENSOR]
|
||||
|
||||
CONF_HEATER = "heater_entity_id"
|
||||
CONF_HEATER_2 = "heater_entity2_id"
|
||||
CONF_HEATER_3 = "heater_entity3_id"
|
||||
CONF_HEATER_4 = "heater_entity4_id"
|
||||
CONF_TEMP_SENSOR = "temperature_sensor_entity_id"
|
||||
CONF_EXTERNAL_TEMP_SENSOR = "external_temperature_sensor_entity_id"
|
||||
CONF_POWER_SENSOR = "power_sensor_entity_id"
|
||||
@@ -41,6 +49,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"
|
||||
@@ -57,10 +66,14 @@ CONF_THERMOSTAT_TYPE = "thermostat_type"
|
||||
CONF_THERMOSTAT_SWITCH = "thermostat_over_switch"
|
||||
CONF_THERMOSTAT_CLIMATE = "thermostat_over_climate"
|
||||
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"
|
||||
@@ -74,14 +87,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,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -89,11 +127,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,
|
||||
@@ -123,13 +166,19 @@ 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_PRESETS_VALUES
|
||||
+ CONF_PRESETS_AWAY_VALUES,
|
||||
+ CONF_PRESETS_AWAY_VALUES
|
||||
+ CONF_PRESETS_WITH_AC_VALUES
|
||||
+ CONF_PRESETS_AWAY_WITH_AC_VALUES,
|
||||
)
|
||||
|
||||
CONF_FUNCTIONS = [
|
||||
|
||||
@@ -14,6 +14,6 @@
|
||||
"quality_scale": "silver",
|
||||
"requirements": [],
|
||||
"ssdp": [],
|
||||
"version": "3.0.0",
|
||||
"version": "3.5.3",
|
||||
"zeroconf": []
|
||||
}
|
||||
@@ -54,7 +54,7 @@ class WindowOpenDetectionAlgorithm:
|
||||
delta_t_sec = float((datetime_measure - self._last_datetime).total_seconds())
|
||||
delta_t = delta_t_sec / 60.0
|
||||
if delta_t_sec <= MIN_DELTA_T_SEC:
|
||||
_LOGGER.warning(
|
||||
_LOGGER.debug(
|
||||
"Delta t is %d < %d which should be not possible. We don't consider this value",
|
||||
delta_t_sec,
|
||||
MIN_DELTA_T_SEC,
|
||||
@@ -64,7 +64,7 @@ class WindowOpenDetectionAlgorithm:
|
||||
delta_temp = float(temperature - self._last_temperature)
|
||||
new_slope = delta_temp / delta_t
|
||||
if new_slope > MAX_SLOPE_VALUE or new_slope < -MAX_SLOPE_VALUE:
|
||||
_LOGGER.warning(
|
||||
_LOGGER.debug(
|
||||
"New_slope is abs(%.2f) > %.2f which should be not possible. We don't consider this value",
|
||||
new_slope,
|
||||
MAX_SLOPE_VALUE,
|
||||
|
||||
@@ -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 = (
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
homeassistant
|
||||
@@ -1,4 +0,0 @@
|
||||
# -r requirements_dev.txt
|
||||
# aiodiscover
|
||||
ulid_transform
|
||||
pytest-homeassistant-custom-component
|
||||
@@ -1,120 +1,124 @@
|
||||
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
|
||||
|
||||
@@ -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",
|
||||
@@ -22,12 +22,31 @@
|
||||
}
|
||||
},
|
||||
"type": {
|
||||
"title": "Linked entity",
|
||||
"description": "Linked entity attributes",
|
||||
"title": "Linked entities",
|
||||
"description": "Linked entities attributes",
|
||||
"data": {
|
||||
"heater_entity_id": "Heater entity id",
|
||||
"heater_entity_id": "1rst heater switch",
|
||||
"heater_entity2_id": "2nd heater switch",
|
||||
"heater_entity3_id": "3rd heater switch",
|
||||
"heater_entity4_id": "4th heater switch",
|
||||
"proportional_function": "Algorithm",
|
||||
"climate_entity_id": "1rst underlying climate",
|
||||
"climate_entity2_id": "2nd underlying climate",
|
||||
"climate_entity3_id": "3rd underlying climate",
|
||||
"climate_entity4_id": "4th underlying climate",
|
||||
"ac_mode": "AC mode"
|
||||
},
|
||||
"data_description": {
|
||||
"heater_entity_id": "Mandatory heater entity id",
|
||||
"heater_entity2_id": "Optional 2nd Heater entity id. Leave empty if not used",
|
||||
"heater_entity3_id": "Optional 3rd Heater entity id. Leave empty if not used",
|
||||
"heater_entity4_id": "Optional 4th Heater entity id. Leave empty if not used",
|
||||
"proportional_function": "Algorithm to use (TPI is the only one for now)",
|
||||
"climate_entity_id": "Underlying thermostat entity id"
|
||||
"climate_entity_id": "Underlying climate entity id",
|
||||
"climate_entity2_id": "2nd underlying climate entity id",
|
||||
"climate_entity3_id": "3rd underlying climate entity id",
|
||||
"climate_entity4_id": "4th underlying climate entity id",
|
||||
"ac_mode": "Use the Air Conditioning (AC) mode"
|
||||
}
|
||||
},
|
||||
"tpi": {
|
||||
@@ -44,7 +63,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": {
|
||||
@@ -70,7 +92,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"
|
||||
}
|
||||
@@ -88,10 +118,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": {
|
||||
@@ -100,7 +133,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": {
|
||||
@@ -142,12 +175,31 @@
|
||||
}
|
||||
},
|
||||
"type": {
|
||||
"title": "Linked entity",
|
||||
"description": "Linked entity attributes",
|
||||
"title": "Linked entities",
|
||||
"description": "Linked entities attributes",
|
||||
"data": {
|
||||
"heater_entity_id": "Heater entity id",
|
||||
"heater_entity_id": "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_entity2_id": "2nd underlying climate",
|
||||
"climate_entity3_id": "3rd underlying climate",
|
||||
"climate_entity4_id": "4th underlying climate",
|
||||
"ac_mode": "AC mode"
|
||||
},
|
||||
"data_description": {
|
||||
"heater_entity_id": "Mandatory heater entity id",
|
||||
"heater_entity2_id": "Optional 2nd Heater entity id. Leave empty if not used",
|
||||
"heater_entity3_id": "Optional 3rd Heater entity id. Leave empty if not used",
|
||||
"heater_entity4_id": "Optional 4th Heater entity id. Leave empty if not used",
|
||||
"proportional_function": "Algorithm to use (TPI is the only one for now)",
|
||||
"climate_entity_id": "Underlying thermostat entity id"
|
||||
"climate_entity_id": "Underlying climate entity id",
|
||||
"climate_entity2_id": "2nd underlying climate entity id",
|
||||
"climate_entity3_id": "3rd underlying climate entity id",
|
||||
"climate_entity4_id": "4th underlying climate entity id",
|
||||
"ac_mode": "Use the Air Conditioning (AC) mode"
|
||||
}
|
||||
},
|
||||
"tpi": {
|
||||
@@ -164,7 +216,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": {
|
||||
@@ -190,7 +245,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"
|
||||
}
|
||||
@@ -211,7 +274,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": {
|
||||
@@ -262,16 +328,5 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"state_attributes": {
|
||||
"_": {
|
||||
"preset_mode": {
|
||||
"state": {
|
||||
"power": "Shedding",
|
||||
"security": "Security",
|
||||
"none": "Manual"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
@@ -22,12 +22,31 @@
|
||||
}
|
||||
},
|
||||
"type": {
|
||||
"title": "Linked entity",
|
||||
"description": "Linked entity attributes",
|
||||
"title": "Linked entities",
|
||||
"description": "Linked entities attributes",
|
||||
"data": {
|
||||
"heater_entity_id": "Heater entity id",
|
||||
"heater_entity_id": "1rst heater switch",
|
||||
"heater_entity2_id": "2nd heater switch",
|
||||
"heater_entity3_id": "3rd heater switch",
|
||||
"heater_entity4_id": "4th heater switch",
|
||||
"proportional_function": "Algorithm",
|
||||
"climate_entity_id": "1rst underlying climate",
|
||||
"climate_entity2_id": "2nd underlying climate",
|
||||
"climate_entity3_id": "3rd underlying climate",
|
||||
"climate_entity4_id": "4th underlying climate",
|
||||
"ac_mode": "AC mode"
|
||||
},
|
||||
"data_description": {
|
||||
"heater_entity_id": "Mandatory heater entity id",
|
||||
"heater_entity2_id": "Optional 2nd Heater entity id. Leave empty if not used",
|
||||
"heater_entity3_id": "Optional 3rd Heater entity id. Leave empty if not used",
|
||||
"heater_entity4_id": "Optional 4th Heater entity id. Leave empty if not used",
|
||||
"proportional_function": "Algorithm to use (TPI is the only one for now)",
|
||||
"climate_entity_id": "Underlying thermostat entity id"
|
||||
"climate_entity_id": "Underlying climate entity id",
|
||||
"climate_entity2_id": "2nd underlying climate entity id",
|
||||
"climate_entity3_id": "3rd underlying climate entity id",
|
||||
"climate_entity4_id": "4th underlying climate entity id",
|
||||
"ac_mode": "Use the Air Conditioning (AC) mode"
|
||||
}
|
||||
},
|
||||
"tpi": {
|
||||
@@ -44,7 +63,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": {
|
||||
@@ -70,7 +92,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"
|
||||
}
|
||||
@@ -88,10 +118,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": {
|
||||
@@ -100,7 +133,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": {
|
||||
@@ -142,12 +175,31 @@
|
||||
}
|
||||
},
|
||||
"type": {
|
||||
"title": "Linked entity",
|
||||
"description": "Linked entity attributes",
|
||||
"title": "Linked entities",
|
||||
"description": "Linked entities attributes",
|
||||
"data": {
|
||||
"heater_entity_id": "Heater entity id",
|
||||
"heater_entity_id": "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_entity2_id": "2nd underlying climate",
|
||||
"climate_entity3_id": "3rd underlying climate",
|
||||
"climate_entity4_id": "4th underlying climate",
|
||||
"ac_mode": "AC mode"
|
||||
},
|
||||
"data_description": {
|
||||
"heater_entity_id": "Mandatory heater entity id",
|
||||
"heater_entity2_id": "Optional 2nd Heater entity id. Leave empty if not used",
|
||||
"heater_entity3_id": "Optional 3rd Heater entity id. Leave empty if not used",
|
||||
"heater_entity4_id": "Optional 4th Heater entity id. Leave empty if not used",
|
||||
"proportional_function": "Algorithm to use (TPI is the only one for now)",
|
||||
"climate_entity_id": "Underlying thermostat entity id"
|
||||
"climate_entity_id": "Underlying climate entity id",
|
||||
"climate_entity2_id": "2nd underlying climate entity id",
|
||||
"climate_entity3_id": "3rd underlying climate entity id",
|
||||
"climate_entity4_id": "4th underlying climate entity id",
|
||||
"ac_mode": "Use the Air Conditioning (AC) mode"
|
||||
}
|
||||
},
|
||||
"tpi": {
|
||||
@@ -164,7 +216,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": {
|
||||
@@ -190,7 +245,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"
|
||||
}
|
||||
@@ -211,7 +274,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": {
|
||||
@@ -262,16 +328,5 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"state_attributes": {
|
||||
"_": {
|
||||
"preset_mode": {
|
||||
"state": {
|
||||
"power": "Shedding",
|
||||
"security": "Security",
|
||||
"none": "Manual"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
@@ -21,12 +22,31 @@
|
||||
}
|
||||
},
|
||||
"type": {
|
||||
"title": "Entité liée",
|
||||
"description": "Attributs de l'entité liée",
|
||||
"title": "Entité(s) liée(s)",
|
||||
"description": "Attributs de(s) l'entité(s) liée(s)",
|
||||
"data": {
|
||||
"heater_entity_id": "Radiateur entity id",
|
||||
"heater_entity_id": "1er radiateur",
|
||||
"heater_entity2_id": "2ème radiateur",
|
||||
"heater_entity3_id": "3ème radiateur",
|
||||
"heater_entity4_id": "4ème radiateur",
|
||||
"proportional_function": "Algorithme",
|
||||
"climate_entity_id": "Thermostat sous-jacent",
|
||||
"climate_entity2_id": "2ème thermostat sous-jacent",
|
||||
"climate_entity3_id": "3ème thermostat sous-jacent",
|
||||
"climate_entity4_id": "4ème thermostat sous-jacent",
|
||||
"ac_mode": "AC mode ?"
|
||||
},
|
||||
"data_description": {
|
||||
"heater_entity_id": "Entity id du 1er radiateur obligatoire",
|
||||
"heater_entity2_id": "Optionnel entity id du 2ème radiateur",
|
||||
"heater_entity3_id": "Optionnel entity id du 3ème radiateur",
|
||||
"heater_entity4_id": "Optionnel entity id du 4ème radiateur",
|
||||
"proportional_function": "Algorithme à utiliser (Seul TPI est disponible pour l'instant)",
|
||||
"climate_entity_id": "Thermostat sous-jacent entity id"
|
||||
"climate_entity_id": "Entity id du thermostat sous-jacent",
|
||||
"climate_entity2_id": "Entity id du 2ème thermostat sous-jacent",
|
||||
"climate_entity3_id": "Entity id du 3ème thermostat sous-jacent",
|
||||
"climate_entity4_id": "Entity id du 4ème thermostat sous-jacent",
|
||||
"ac_mode": "Utilisation du mode Air Conditionné (AC)"
|
||||
}
|
||||
},
|
||||
"tpi": {
|
||||
@@ -43,7 +63,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": {
|
||||
@@ -52,7 +75,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)"
|
||||
},
|
||||
@@ -68,8 +91,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é"
|
||||
}
|
||||
@@ -90,7 +121,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": {
|
||||
@@ -142,12 +176,31 @@
|
||||
}
|
||||
},
|
||||
"type": {
|
||||
"title": "Entité liée",
|
||||
"description": "Attributs de l'entité liée",
|
||||
"title": "Entité(s) liée(s)",
|
||||
"description": "Attributs de(s) l'entité(s) liée(s)",
|
||||
"data": {
|
||||
"heater_entity_id": "Radiateur entity id",
|
||||
"heater_entity_id": "1er radiateur",
|
||||
"heater_entity2_id": "2ème radiateur",
|
||||
"heater_entity3_id": "3ème radiateur",
|
||||
"heater_entity4_id": "4ème radiateur",
|
||||
"proportional_function": "Algorithme",
|
||||
"climate_entity_id": "Thermostat sous-jacent",
|
||||
"climate_entity2_id": "2ème thermostat sous-jacent",
|
||||
"climate_entity3_id": "3ème thermostat sous-jacent",
|
||||
"climate_entity4_id": "4ème thermostat sous-jacent",
|
||||
"ac_mode": "AC mode ?"
|
||||
},
|
||||
"data_description": {
|
||||
"heater_entity_id": "Entity id du 1er radiateur obligatoire",
|
||||
"heater_entity2_id": "Optionnel entity id du 2ème radiateur",
|
||||
"heater_entity3_id": "Optionnel entity id du 3ème radiateur",
|
||||
"heater_entity4_id": "Optionnel entity id du 4ème radiateur",
|
||||
"proportional_function": "Algorithme à utiliser (Seul TPI est disponible pour l'instant)",
|
||||
"climate_entity_id": "Thermostat sous-jacent entity id"
|
||||
"climate_entity_id": "Entity id du thermostat sous-jacent",
|
||||
"climate_entity2_id": "Entity id du 2ème thermostat sous-jacent",
|
||||
"climate_entity3_id": "Entity id du 3ème thermostat sous-jacent",
|
||||
"climate_entity4_id": "Entity id du 4ème thermostat sous-jacent",
|
||||
"ac_mode": "Utilisation du mode Air Conditionné (AC)"
|
||||
}
|
||||
},
|
||||
"tpi": {
|
||||
@@ -164,7 +217,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": {
|
||||
@@ -189,8 +245,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é"
|
||||
}
|
||||
@@ -211,7 +275,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": {
|
||||
@@ -262,16 +329,5 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"state_attributes": {
|
||||
"_": {
|
||||
"preset_mode": {
|
||||
"state": {
|
||||
"power": "Délestage",
|
||||
"security": "Sécurité",
|
||||
"none": "Manuel"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
318
custom_components/versatile_thermostat/translations/it.json
Normal file
318
custom_components/versatile_thermostat/translations/it.json
Normal file
@@ -0,0 +1,318 @@
|
||||
{
|
||||
"title": "Configurazione Versatile Thermostat",
|
||||
"config": {
|
||||
"flow_title": "Configurazione Versatile Thermostat",
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "Aggiungi un nuovo Versatile Thermostat",
|
||||
"description": "Principali parametri obbligatori",
|
||||
"data": {
|
||||
"name": "Nome",
|
||||
"thermostat_type": "Tipologia di termostato",
|
||||
"temperature_sensor_entity_id": "Entity id sensore temperatura",
|
||||
"external_temperature_sensor_entity_id": "Entity id sensore temperatura esterna",
|
||||
"cycle_min": "Durata del ciclo (minuti)",
|
||||
"temp_min": "Temperatura minima ammessa",
|
||||
"temp_max": "Temperatura massima ammessa",
|
||||
"device_power": "Potenza dispositivo (kW)",
|
||||
"use_window_feature": "Usa il rilevamento della finestra",
|
||||
"use_motion_feature": "Usa il rilevamento del movimento",
|
||||
"use_power_feature": "Usa la gestione della potenza",
|
||||
"use_presence_feature": "Usa il rilevamento della presenza"
|
||||
}
|
||||
},
|
||||
"type": {
|
||||
"title": "Entità collegate",
|
||||
"description": "Parametri entità collegate",
|
||||
"data": {
|
||||
"heater_entity_id": "Primo riscaldatore",
|
||||
"heater_entity2_id": "Secondo riscaldatore",
|
||||
"heater_entity3_id": "Terzo riscaldatore",
|
||||
"heater_entity4_id": "Quarto riscaldatore",
|
||||
"proportional_function": "Algoritmo",
|
||||
"climate_entity_id": "Termostato sottostante",
|
||||
"climate_entity2_id": "Secundo termostato sottostante",
|
||||
"climate_entity3_id": "Terzo termostato sottostante",
|
||||
"climate_entity4_id": "Quarto termostato sottostante",
|
||||
"ac_mode": "AC mode ?"
|
||||
},
|
||||
"data_description": {
|
||||
"heater_entity_id": "Entity id obbligatoria del primo riscaldatore",
|
||||
"heater_entity2_id": "Entity id del secondo riscaldatore facoltativo. Lasciare vuoto se non utilizzato",
|
||||
"heater_entity3_id": "Entity id del terzo riscaldatore facoltativo. Lasciare vuoto se non utilizzato",
|
||||
"heater_entity4_id": "Entity id del quarto riscaldatore facoltativo. Lasciare vuoto se non utilizzato",
|
||||
"proportional_function": "Algoritmo da utilizzare (il TPI per adesso è l'unico)",
|
||||
"climate_entity_id": "Entity id del termostato sottostante",
|
||||
"climate_entity2_id": "Entity id del secundo termostato sottostante",
|
||||
"climate_entity3_id": "Entity id del terzo termostato sottostante",
|
||||
"climate_entity4_id": "Entity id del quarto termostato sottostante",
|
||||
"ac_mode": "Utilizzare la modalità AC (Air Conditioned) ?"
|
||||
}
|
||||
},
|
||||
"tpi": {
|
||||
"title": "TPI",
|
||||
"description": "Parametri del Time Proportional Integral",
|
||||
"data": {
|
||||
"tpi_coef_int": "Coefficiente per il delta della temperatura interna",
|
||||
"tpi_coef_ext": "Coefficiente per il delta della temperatura esterna"
|
||||
}
|
||||
},
|
||||
"presets": {
|
||||
"title": "Presets",
|
||||
"description": "Per ogni preset, impostare la temperatura desiderata (0 per ignorare il preset)",
|
||||
"data": {
|
||||
"eco_temp": "Temperatura nel preset Eco",
|
||||
"comfort_temp": "Temperatura nel preset Comfort",
|
||||
"boost_temp": "Temperatura nel preset Boost",
|
||||
"eco_ac_temp": "Temperatura nel preset Eco (AC mode)",
|
||||
"comfort_ac_temp": "Temperatura nel preset Comfort (AC mode)",
|
||||
"boost_ac_temp": "Temperatura nel preset Boost (AC mode)"
|
||||
}
|
||||
},
|
||||
"window": {
|
||||
"title": "Gestione della finestra",
|
||||
"description": "Gestione della finestra aperta.\nLasciare vuoto l'entity_id corrispondente se non utilizzato\nÈ inoltre possibile configurare il rilevamento automatico della finestra aperta in base alla diminuzione della temperatura",
|
||||
"data": {
|
||||
"window_sensor_entity_id": "Entity id sensore finestra",
|
||||
"window_delay": "Ritardo sensore finestra (secondi)",
|
||||
"window_auto_open_threshold": "Soglia di diminuzione della temperatura per il rilevamento automatico della finestra aperta (in °/min)",
|
||||
"window_auto_close_threshold": "Soglia di aumento della temperatura per la fine del rilevamento automatico (in °/min)",
|
||||
"window_auto_max_duration": "Durata massima del rilevamento automatico della finestra aperta (in min)"
|
||||
},
|
||||
"data_description": {
|
||||
"window_sensor_entity_id": "Lasciare vuoto se non deve essere utilizzato alcun sensore finestra",
|
||||
"window_delay": "Ritardo in secondi prima che il rilevamento del sensore sia preso in considerazione",
|
||||
"window_auto_open_threshold": "Valore consigliato: tra 0.05 e 0.1. Lasciare vuoto se il rilevamento automatico della finestra aperta non è utilizzato",
|
||||
"window_auto_close_threshold": "Valore consigliato: 0. Lasciare vuoto se il rilevamento automatico della finestra aperta non è utilizzato",
|
||||
"window_auto_max_duration": "Valore consigliato: 60 (un'ora). Lasciare vuoto se il rilevamento automatico della finestra aperta non è utilizzato"
|
||||
}
|
||||
},
|
||||
"motion": {
|
||||
"title": "Gestione movimento",
|
||||
"description": "Gestione sensore movimento. Il preset può cambiare automaticamente a seconda di un rilevamento di movimento\nLasciare vuoto l'entity_id corrispondente se non utilizzato.\nmotion_preset e no_motion_preset devono essere impostati con il nome del preset corrispondente",
|
||||
"data": {
|
||||
"motion_sensor_entity_id": "Entity id sensore di movimento",
|
||||
"motion_delay": "Ritardo in secondi prima che il rilevamento del sensore sia preso in considerazione",
|
||||
"motion_off_delay": "Ritardo in secondi di disattivazione prima che del sensore sia preso in considerazione",
|
||||
"motion_preset": "Preset da utilizzare quando viene rilevato il movimento",
|
||||
"no_motion_preset": "Preset da utilizzare quando non viene rilevato il movimento"
|
||||
}
|
||||
},
|
||||
"power": {
|
||||
"title": "Gestione dell'energia",
|
||||
"description": "Parametri di gestione dell'energia.\nInserire la potenza massima disponibile e l'entity_id del sensore che la misura.\nQuindi inserire il consumo del riscaldatore quando è in funzione.\nTutti i parametri devono essere nella stessa unità di misura (kW o W).\nLasciare vuoto l'entity_id corrispondente se non utilizzato.",
|
||||
"data": {
|
||||
"power_sensor_entity_id": "Entity id sensore potenza",
|
||||
"max_power_sensor_entity_id": "Entity id sensore di massima potenza",
|
||||
"power_temp": "Temperatura in caso di distribuzione del carico"
|
||||
}
|
||||
},
|
||||
"presence": {
|
||||
"title": "Gestione della presenza",
|
||||
"description": "Parametri di gestione della presenza.\nInserire un sensore di presenza (true se è presente qualcuno).\nQuindi specificare il preset o la riduzione di temperatura da utilizzare quando il sensore di presenza è in false.\nSe è impostato il preset, la riduzione non sarà utilizzata.\nLasciare vuoto l'entity_id corrispondente se non utilizzato.",
|
||||
"data": {
|
||||
"presence_sensor_entity_id": "Entity id sensore presenza (true se è presente qualcuno)",
|
||||
"eco_away_temp": "Temperatura al preset Eco in caso d'assenza",
|
||||
"comfort_away_temp": "Temperatura al preset Comfort in caso d'assenza",
|
||||
"boost_away_temp": "Temperatura al preset Boost in caso d'assenza",
|
||||
"eco_ac_away_temp": "Temperatura al preset Eco in caso d'assenza (AC mode)",
|
||||
"comfort_ac_away_temp": "Temperatura al preset Comfort in caso d'assenza (AC mode)",
|
||||
"boost_ac_away_temp": "Temperatura al preset Boost in caso d'assenza (AC mode)"
|
||||
}
|
||||
},
|
||||
"advanced": {
|
||||
"title": "Parametri avanzati",
|
||||
"description": "Configurazione avanzata dei parametri. Lasciare i valori predefiniti se non conoscete cosa state modificando.\nQuesti parametri possono determinare una pessima gestione della temperatura e della potenza.",
|
||||
"data": {
|
||||
"minimal_activation_delay": "Ritardo minimo di accensione",
|
||||
"security_delay_min": "Ritardo di sicurezza (in minuti)",
|
||||
"security_min_on_percent": "Percentuale minima di potenza per la modalità di sicurezza",
|
||||
"security_default_on_percent": "Percentuale di potenza per la modalità di sicurezza"
|
||||
},
|
||||
"data_description": {
|
||||
"minimal_activation_delay": "Ritardo in secondi al di sotto del quale l'apparecchiatura non verrà attivata",
|
||||
"security_delay_min": "Ritardo massimo consentito in minuti tra due misure di temperatura. Al di sopra di questo ritardo, il termostato passerà allo stato di sicurezza",
|
||||
"security_min_on_percent": "Soglia percentuale minima di riscaldamento al di sotto della quale il preset di sicurezza non verrà mai attivato",
|
||||
"security_default_on_percent": "Valore percentuale predefinito della potenza di riscaldamento nella modalità di sicurezza. Impostare a 0 per spegnere il riscaldatore nella modalità di sicurezza"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"unknown": "Errore inatteso",
|
||||
"unknown_entity": "Entity id sconosciuta",
|
||||
"window_open_detection_method": "Può essere utilizzato un solo metodo di rilevamento finestra aperta. Utilizzare il sensore od il rilevamento automatico ma non entrambi"
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "Il dispositivo è già configurato"
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"flow_title": "Configurazione di Versatile Thermostat",
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "Aggiungi un nuovo Versatile Thermostat",
|
||||
"description": "Principali attributi obbligatori",
|
||||
"data": {
|
||||
"name": "Nome",
|
||||
"thermostat_type": "Tipologia termostato",
|
||||
"temperature_sensor_entity_id": "Entity id sensore di temperatura",
|
||||
"external_temperature_sensor_entity_id": "Entity id sensore temperatura esterna",
|
||||
"cycle_min": "Durata del ciclo (minuti)",
|
||||
"temp_min": "Temperatura minima consentita",
|
||||
"temp_max": "Temperatura massima consentita",
|
||||
"device_power": "Potenza dispositivo (kW)",
|
||||
"use_window_feature": "Usa il rilevamento della finestra",
|
||||
"use_motion_feature": "Usa il rilevamento del movimento",
|
||||
"use_power_feature": "Usa la gestione della potenza",
|
||||
"use_presence_feature": "Usa il rilevamento della presenza"
|
||||
}
|
||||
},
|
||||
"type": {
|
||||
"title": "Entità collegate",
|
||||
"description": "Attributi delle 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",
|
||||
"proportional_function": "Algoritmo",
|
||||
"climate_entity_id": "Termostato sottostante",
|
||||
"climate_entity2_id": "Secundo termostato sottostante",
|
||||
"climate_entity3_id": "Terzo termostato sottostante",
|
||||
"climate_entity4_id": "Quarto termostato sottostante",
|
||||
"ac_mode": "AC mode ?"
|
||||
},
|
||||
"data_description": {
|
||||
"heater_entity_id": "Entity id obbligatoria del primo riscaldatore",
|
||||
"heater_entity2_id": "Entity id del secondo riscaldatore facoltativo. Lasciare vuoto se non utilizzato",
|
||||
"heater_entity3_id": "Entity id del terzo riscaldatore facoltativo. Lasciare vuoto se non utilizzato",
|
||||
"heater_entity4_id": "Entity id del quarto riscaldatore facoltativo. Lasciare vuoto se non utilizzato",
|
||||
"proportional_function": "Algoritmo da utilizzare (il TPI per adesso è l'unico)",
|
||||
"climate_entity_id": "Entity id del 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) ?"
|
||||
}
|
||||
},
|
||||
"tpi": {
|
||||
"title": "TPI",
|
||||
"description": "Parametri del Time Proportional Integral",
|
||||
"data": {
|
||||
"tpi_coef_int": "Coefficiente per il delta della temperatura interna",
|
||||
"tpi_coef_ext": "Coefficiente per il delta della temperatura esterna"
|
||||
}
|
||||
},
|
||||
"presets": {
|
||||
"title": "Presets",
|
||||
"description": "Per ogni preset, impostare la temperatura desiderata (0 per ignorare il preset)",
|
||||
"data": {
|
||||
"eco_temp": "Temperatura nel preset Eco",
|
||||
"comfort_temp": "Temperatura nel preset Comfort",
|
||||
"boost_temp": "Temperatura nel preset Boost",
|
||||
"eco_ac_temp": "Temperatura nel preset Eco (AC mode)",
|
||||
"comfort_ac_temp": "Temperatura nel preset Comfort (AC mode)",
|
||||
"boost_ac_temp": "Temperatura nel preset Boost (AC mode)"
|
||||
}
|
||||
},
|
||||
"window": {
|
||||
"title": "Gestione della finestra",
|
||||
"description": "Gestione della finestra aperta.\nLasciare vuoto l'entity_id corrispondente se non utilizzato\nÈ inoltre possibile configurare il rilevamento automatico della finestra aperta in base alla diminuzione della temperatura",
|
||||
"data": {
|
||||
"window_sensor_entity_id": "Entity id sensore finestra",
|
||||
"window_delay": "Ritardo sensore finestra (secondi)",
|
||||
"window_auto_open_threshold": "Soglia di diminuzione della temperatura per il rilevamento automatico della finestra aperta (in °/min)",
|
||||
"window_auto_close_threshold": "Soglia di aumento della temperatura per la fine del rilevamento automatico (in °/min)",
|
||||
"window_auto_max_duration": "Durata massima del rilevamento automatico della finestra aperta (in min)"
|
||||
},
|
||||
"data_description": {
|
||||
"window_sensor_entity_id": "Lasciare vuoto se non deve essere utilizzato alcun sensore finestra",
|
||||
"window_delay": "Ritardo in secondi prima che il rilevamento del sensore sia preso in considerazione",
|
||||
"window_auto_open_threshold": "Valore consigliato: tra 0.05 e 0.1. Lasciare vuoto se il rilevamento automatico della finestra aperta non è utilizzato",
|
||||
"window_auto_close_threshold": "Valore consigliato: 0. Lasciare vuoto se il rilevamento automatico della finestra aperta non è utilizzato",
|
||||
"window_auto_max_duration": "Valore consigliato: 60 (un'ora). Lasciare vuoto se il rilevamento automatico della finestra aperta non è utilizzato"
|
||||
}
|
||||
},
|
||||
"motion": {
|
||||
"title": "Gestione movimento",
|
||||
"description": "Gestione sensore movimento. Il preset può cambiare automaticamente a seconda di un rilevamento di movimento\nLasciare vuoto l'entity_id corrispondente se non utilizzato.\nmotion_preset e no_motion_preset devono essere impostati con il nome del preset corrispondente",
|
||||
"data": {
|
||||
"motion_sensor_entity_id": "Entity id sensore di movimento",
|
||||
"motion_delay": "Ritardo in secondi prima che il rilevamento del sensore sia preso in considerazione",
|
||||
"motion_off_delay": "Ritardo in secondi di disattivazione prima che del sensore sia preso in considerazione",
|
||||
"motion_preset": "Preset da utilizzare quando viene rilevato il movimento",
|
||||
"no_motion_preset": "Preset da utilizzare quando non viene rilevato il movimento"
|
||||
}
|
||||
},
|
||||
"power": {
|
||||
"title": "Gestione dell'energia",
|
||||
"description": "Parametri di gestione dell'energia.\nInserire la potenza massima disponibile e l'entity_id del sensore che la misura.\nQuindi inserire il consumo del riscaldatore quando è in funzione.\nTutti i parametri devono essere nella stessa unità di misura (kW o W).\nLasciare vuoto l'entity_id corrispondente se non utilizzato.",
|
||||
"data": {
|
||||
"power_sensor_entity_id": "Entity id sensore potenza",
|
||||
"max_power_sensor_entity_id": "Entity id sensore di massima potenza",
|
||||
"power_temp": "Temperatura in caso di distribuzione del carico"
|
||||
}
|
||||
},
|
||||
"presence": {
|
||||
"title": "Gestione della presenza",
|
||||
"description": "Parametri di gestione della presenza.\nInserire un sensore di presenza (true se è presente qualcuno).\nQuindi specificare il preset o la riduzione di temperatura da utilizzare quando il sensore di presenza è in false.\nSe è impostato il preset, la riduzione non sarà utilizzata.\nLasciare vuoto l'entity_id corrispondente se non utilizzato.",
|
||||
"data": {
|
||||
"presence_sensor_entity_id": "Entity id sensore presenza (true se è presente qualcuno)",
|
||||
"eco_away_temp": "Temperatura al preset Eco in caso d'assenza",
|
||||
"comfort_away_temp": "Temperatura al preset Comfort in caso d'assenza",
|
||||
"boost_away_temp": "Temperatura al preset Boost in caso d'assenza",
|
||||
"eco_ac_away_temp": "Temperatura al preset Eco in caso d'assenza (AC mode)",
|
||||
"comfort_ac_away_temp": "Temperatura al preset Comfort in caso d'assenza (AC mode)",
|
||||
"boost_ac_away_temp": "Temperatura al preset Boost in caso d'assenza (AC mode)"
|
||||
}
|
||||
},
|
||||
"advanced": {
|
||||
"title": "Parametri avanzati",
|
||||
"description": "Configurazione avanzata dei parametri. Lasciare i valori predefiniti se non conoscete cosa state modificando.\nQuesti parametri possono determinare una pessima gestione della temperatura e della potenza.",
|
||||
"data": {
|
||||
"minimal_activation_delay": "Ritardo minimo di accensione",
|
||||
"security_delay_min": "Ritardo di sicurezza (in minuti)",
|
||||
"security_min_on_percent": "Percentuale minima di potenza per la modalità di sicurezza",
|
||||
"security_default_on_percent": "Percentuale di potenza per la modalità di sicurezza"
|
||||
},
|
||||
"data_description": {
|
||||
"minimal_activation_delay": "Ritardo in secondi al di sotto del quale l'apparecchiatura non verrà attivata",
|
||||
"security_delay_min": "Ritardo massimo consentito in minuti tra due misure di temperatura. Al di sopra di questo ritardo, il termostato passerà allo stato di sicurezza",
|
||||
"security_min_on_percent": "Soglia percentuale minima di riscaldamento al di sotto della quale il preset di sicurezza non verrà mai attivato",
|
||||
"security_default_on_percent": "Valore percentuale predefinito della potenza di riscaldamento nella modalità di sicurezza. Impostare a 0 per spegnere il riscaldatore nella modalità di sicurezza"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"unknown": "Errore inatteso",
|
||||
"unknown_entity": "Entity id sconosciuta",
|
||||
"window_open_detection_method": "Può essere utilizzato un solo metodo di rilevamento finestra aperta. Utilizzare il sensore od il rilevamento automatico ma non entrambi"
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "Il dispositivo è già configurato"
|
||||
}
|
||||
},
|
||||
"selector": {
|
||||
"thermostat_type": {
|
||||
"options": {
|
||||
"thermostat_over_switch": "Termostato su un interruttore",
|
||||
"thermostat_over_climate": "Termostato sopra un altro termostato"
|
||||
}
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"climate": {
|
||||
"versatile_thermostat": {
|
||||
"state_attributes": {
|
||||
"preset_mode": {
|
||||
"state": {
|
||||
"power": "Ripartizione",
|
||||
"security": "Sicurezza",
|
||||
"none": "Manuale"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
628
custom_components/versatile_thermostat/underlyings.py
Normal file
628
custom_components/versatile_thermostat/underlyings.py
Normal file
@@ -0,0 +1,628 @@
|
||||
""" Underlying entities classes """
|
||||
import logging
|
||||
from typing import Any
|
||||
from enum import StrEnum
|
||||
|
||||
from homeassistant.const import ATTR_ENTITY_ID, STATE_ON, UnitOfTemperature
|
||||
|
||||
from homeassistant.exceptions import ServiceNotFound
|
||||
|
||||
from homeassistant.core import HomeAssistant, DOMAIN as HA_DOMAIN, CALLBACK_TYPE
|
||||
from homeassistant.components.climate import (
|
||||
ClimateEntity,
|
||||
ClimateEntityFeature,
|
||||
DOMAIN as CLIMATE_DOMAIN,
|
||||
HVACMode,
|
||||
HVACAction,
|
||||
SERVICE_SET_HVAC_MODE,
|
||||
SERVICE_SET_FAN_MODE,
|
||||
SERVICE_SET_HUMIDITY,
|
||||
SERVICE_SET_SWING_MODE,
|
||||
SERVICE_TURN_OFF,
|
||||
SERVICE_TURN_ON,
|
||||
SERVICE_SET_TEMPERATURE,
|
||||
)
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
from homeassistant.helpers.event import async_call_later
|
||||
|
||||
from .const import UnknownEntity
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
# remove this
|
||||
# _LOGGER.setLevel(logging.DEBUG)
|
||||
|
||||
|
||||
class UnderlyingEntityType(StrEnum):
|
||||
"""All underlying device type"""
|
||||
|
||||
# A switch
|
||||
SWITCH = "switch"
|
||||
|
||||
# a climate
|
||||
CLIMATE = "climate"
|
||||
|
||||
|
||||
class UnderlyingEntity:
|
||||
"""Represent a underlying device which could be a switch or a climate"""
|
||||
|
||||
_hass: HomeAssistant
|
||||
# Cannot import VersatileThermostat due to circular reference
|
||||
_thermostat: Any
|
||||
_entity_id: str
|
||||
_type: UnderlyingEntityType
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
thermostat: Any,
|
||||
entity_type: UnderlyingEntityType,
|
||||
entity_id: str,
|
||||
) -> None:
|
||||
"""Initialize the underlying entity"""
|
||||
self._hass = hass
|
||||
self._thermostat = thermostat
|
||||
self._type = entity_type
|
||||
self._entity_id = entity_id
|
||||
|
||||
def __str__(self):
|
||||
return str(self._thermostat) + "-" + self._entity_id
|
||||
|
||||
@property
|
||||
def entity_id(self):
|
||||
"""The entiy id represented by this class"""
|
||||
return self._entity_id
|
||||
|
||||
@property
|
||||
def entity_type(self) -> UnderlyingEntityType:
|
||||
"""The entity type represented by this class"""
|
||||
return self._type
|
||||
|
||||
@property
|
||||
def is_initialized(self) -> bool:
|
||||
"""True if the underlying is initialized"""
|
||||
return True
|
||||
|
||||
def startup(self):
|
||||
"""Startup the Entity"""
|
||||
return
|
||||
|
||||
async def set_hvac_mode(self, hvac_mode: HVACMode):
|
||||
"""Set the HVACmode"""
|
||||
return
|
||||
|
||||
@property
|
||||
def is_device_active(self) -> bool | None:
|
||||
"""If the toggleable device is currently active."""
|
||||
return None
|
||||
|
||||
async def 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
|
||||
|
||||
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 self.is_device_active:
|
||||
_LOGGER.warning(
|
||||
"%s - The hvac mode is ON, but the underlying device is not ON. Turning on device %s",
|
||||
self,
|
||||
self._entity_id,
|
||||
)
|
||||
await self.set_hvac_mode(hvac_mode)
|
||||
|
||||
# override to be able to mock the call
|
||||
def call_later(
|
||||
self, hass: HomeAssistant, delay_sec: int, called_method
|
||||
) -> CALLBACK_TYPE:
|
||||
"""Call the method after a delay"""
|
||||
return async_call_later(hass, delay_sec, called_method)
|
||||
|
||||
|
||||
class UnderlyingSwitch(UnderlyingEntity):
|
||||
"""Represent a underlying switch"""
|
||||
|
||||
_initialDelaySec: int
|
||||
_on_time_sec: int
|
||||
_off_time_sec: int
|
||||
_hvac_mode: HVACMode
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
thermostat: Any,
|
||||
switch_entity_id: str,
|
||||
initial_delay_sec: int,
|
||||
) -> None:
|
||||
"""Initialize the underlying switch"""
|
||||
|
||||
super().__init__(
|
||||
hass=hass,
|
||||
thermostat=thermostat,
|
||||
entity_type=UnderlyingEntityType.SWITCH,
|
||||
entity_id=switch_entity_id,
|
||||
)
|
||||
self._initial_delay_sec = initial_delay_sec
|
||||
self._async_cancel_cycle = None
|
||||
self._should_relaunch_control_heating = False
|
||||
self._on_time_sec = 0
|
||||
self._off_time_sec = 0
|
||||
self._hvac_mode = None
|
||||
|
||||
@property
|
||||
def initial_delay_sec(self):
|
||||
"""The initial delay for this class"""
|
||||
return self._initial_delay_sec
|
||||
|
||||
async def set_hvac_mode(self, hvac_mode: HVACMode) -> bool:
|
||||
"""Set the HVACmode. Returns true if something have change"""
|
||||
|
||||
if hvac_mode == HVACMode.OFF:
|
||||
if self.is_device_active:
|
||||
await self.turn_off()
|
||||
self._cancel_cycle()
|
||||
|
||||
if self._hvac_mode != hvac_mode:
|
||||
self._hvac_mode = hvac_mode
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
@property
|
||||
def is_device_active(self):
|
||||
"""If the toggleable device is currently active."""
|
||||
return self._hass.states.is_state(self._entity_id, STATE_ON)
|
||||
|
||||
async def start_cycle(
|
||||
self,
|
||||
hvac_mode: HVACMode,
|
||||
on_time_sec: int,
|
||||
off_time_sec: int,
|
||||
force=False,
|
||||
):
|
||||
"""Starting cycle for switch"""
|
||||
_LOGGER.debug(
|
||||
"%s - Starting new cycle hvac_mode=%s on_time_sec=%d off_time_sec=%d force=%s",
|
||||
self,
|
||||
hvac_mode,
|
||||
on_time_sec,
|
||||
off_time_sec,
|
||||
force,
|
||||
)
|
||||
|
||||
self._on_time_sec = on_time_sec
|
||||
self._off_time_sec = off_time_sec
|
||||
self._hvac_mode = hvac_mode
|
||||
|
||||
# Cancel eventual previous cycle if any
|
||||
if self._async_cancel_cycle is not None:
|
||||
if force:
|
||||
_LOGGER.debug("%s - we force a new cycle", self)
|
||||
self._cancel_cycle()
|
||||
else:
|
||||
_LOGGER.debug(
|
||||
"%s - A previous cycle is alredy running and no force -> waits for its end",
|
||||
self,
|
||||
)
|
||||
# self._should_relaunch_control_heating = True
|
||||
_LOGGER.debug("%s - End of cycle (2)", self)
|
||||
return
|
||||
|
||||
# If we should heat, starts the cycle with delay
|
||||
if self._hvac_mode in [HVACMode.HEAT, HVACMode.COOL] and on_time_sec > 0:
|
||||
# Starts the cycle after the initial delay
|
||||
self._async_cancel_cycle = self.call_later(
|
||||
self._hass, self._initial_delay_sec, self._turn_on_later
|
||||
)
|
||||
_LOGGER.debug("%s - _async_cancel_cycle=%s", self, self._async_cancel_cycle)
|
||||
|
||||
# if we not heat but device is active
|
||||
elif self.is_device_active:
|
||||
_LOGGER.info(
|
||||
"%s - stop heating (2) for %d min %d sec",
|
||||
self,
|
||||
off_time_sec // 60,
|
||||
off_time_sec % 60,
|
||||
)
|
||||
await self.turn_off()
|
||||
else:
|
||||
_LOGGER.debug("%s - nothing to do", self)
|
||||
|
||||
def _cancel_cycle(self):
|
||||
"""Cancel the cycle"""
|
||||
if self._async_cancel_cycle:
|
||||
self._async_cancel_cycle()
|
||||
self._async_cancel_cycle = None
|
||||
_LOGGER.debug("%s - Stopping cycle during calculation", self)
|
||||
|
||||
async def _turn_on_later(self, _):
|
||||
"""Turn the heater on after a delay"""
|
||||
_LOGGER.debug(
|
||||
"%s - calling turn_on_later hvac_mode=%s, should_relaunch_later=%s off_time_sec=%d",
|
||||
self,
|
||||
self._hvac_mode,
|
||||
self._should_relaunch_control_heating,
|
||||
self._on_time_sec,
|
||||
)
|
||||
|
||||
self._cancel_cycle()
|
||||
|
||||
if self._hvac_mode == HVACMode.OFF:
|
||||
_LOGGER.debug("%s - End of cycle (HVAC_MODE_OFF - 2)", self)
|
||||
if self.is_device_active:
|
||||
await self.turn_off()
|
||||
return
|
||||
|
||||
if await self._thermostat.check_overpowering():
|
||||
_LOGGER.debug("%s - End of cycle (3)", self)
|
||||
return
|
||||
# Security mode could have change the on_time percent
|
||||
await self._thermostat.check_security()
|
||||
time = self._on_time_sec
|
||||
|
||||
action_label = "start"
|
||||
# if 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(
|
||||
"%s - %s heating for %d min %d sec",
|
||||
self,
|
||||
action_label,
|
||||
time // 60,
|
||||
time % 60,
|
||||
)
|
||||
await self.turn_on()
|
||||
else:
|
||||
_LOGGER.debug("%s - No action on heater cause duration is 0", self)
|
||||
self._async_cancel_cycle = self.call_later(
|
||||
self._hass,
|
||||
time,
|
||||
self._turn_off_later,
|
||||
)
|
||||
|
||||
async def _turn_off_later(self, _):
|
||||
"""Turn the heater off and call the next cycle after the delay"""
|
||||
_LOGGER.debug(
|
||||
"%s - calling turn_off_later hvac_mode=%s, should_relaunch_later=%s off_time_sec=%d",
|
||||
self,
|
||||
self._hvac_mode,
|
||||
self._should_relaunch_control_heating,
|
||||
self._off_time_sec,
|
||||
)
|
||||
self._cancel_cycle()
|
||||
|
||||
if self._hvac_mode == HVACMode.OFF:
|
||||
_LOGGER.debug("%s - End of cycle (HVAC_MODE_OFF - 2)", self)
|
||||
if self.is_device_active:
|
||||
await self.turn_off()
|
||||
return
|
||||
|
||||
action_label = "stop"
|
||||
# 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:
|
||||
_LOGGER.info(
|
||||
"%s - %s heating for %d min %d sec",
|
||||
self,
|
||||
action_label,
|
||||
time // 60,
|
||||
time % 60,
|
||||
)
|
||||
await self.turn_off()
|
||||
else:
|
||||
_LOGGER.debug("%s - No action on heater cause duration is 0", self)
|
||||
self._async_cancel_cycle = self.call_later(
|
||||
self._hass,
|
||||
time,
|
||||
self._turn_on_later,
|
||||
)
|
||||
|
||||
# increment energy at the end of the cycle
|
||||
self._thermostat.incremente_energy()
|
||||
|
||||
def remove_entity(self):
|
||||
"""Remove the entity after stopping its cycle"""
|
||||
self._cancel_cycle()
|
||||
|
||||
|
||||
class UnderlyingClimate(UnderlyingEntity):
|
||||
"""Represent a underlying climate"""
|
||||
|
||||
_underlying_climate: ClimateEntity
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
thermostat: Any,
|
||||
climate_entity_id: str,
|
||||
) -> None:
|
||||
"""Initialize the underlying climate"""
|
||||
|
||||
super().__init__(
|
||||
hass=hass,
|
||||
thermostat=thermostat,
|
||||
entity_type=UnderlyingEntityType.CLIMATE,
|
||||
entity_id=climate_entity_id,
|
||||
)
|
||||
self._underlying_climate = None
|
||||
|
||||
def find_underlying_climate(self) -> ClimateEntity:
|
||||
"""Find the underlying climate entity"""
|
||||
component: EntityComponent[ClimateEntity] = self._hass.data[CLIMATE_DOMAIN]
|
||||
for entity in component.entities:
|
||||
if self.entity_id == entity.entity_id:
|
||||
return entity
|
||||
return None
|
||||
|
||||
def startup(self):
|
||||
"""Startup the Entity"""
|
||||
# Get the underlying climate
|
||||
self._underlying_climate = self.find_underlying_climate()
|
||||
if self._underlying_climate:
|
||||
_LOGGER.info(
|
||||
"%s - The underlying climate entity: %s have been succesfully found",
|
||||
self,
|
||||
self._underlying_climate,
|
||||
)
|
||||
else:
|
||||
_LOGGER.error(
|
||||
"%s - Cannot find the underlying climate entity: %s. Thermostat will not be operational",
|
||||
self,
|
||||
self.entity_id,
|
||||
)
|
||||
# #56 keep the over_climate and try periodically to find the underlying climate
|
||||
# self._is_over_climate = False
|
||||
raise UnknownEntity(f"Underlying entity {self.entity_id} not found")
|
||||
return
|
||||
|
||||
@property
|
||||
def is_initialized(self) -> bool:
|
||||
"""True if the underlying climate was found"""
|
||||
return self._underlying_climate is not None
|
||||
|
||||
async def set_hvac_mode(self, hvac_mode: HVACMode) -> bool:
|
||||
"""Set the HVACmode of the underlying climate. Returns true if something have change"""
|
||||
if not self.is_initialized:
|
||||
return False
|
||||
|
||||
data = {ATTR_ENTITY_ID: self._entity_id, "hvac_mode": hvac_mode}
|
||||
await self._hass.services.async_call(
|
||||
CLIMATE_DOMAIN,
|
||||
SERVICE_SET_HVAC_MODE,
|
||||
data,
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
@property
|
||||
def is_device_active(self):
|
||||
"""If the toggleable device is currently active."""
|
||||
if self.is_initialized:
|
||||
return self._underlying_climate.hvac_mode != HVACMode.OFF and self._underlying_climate.hvac_action not in [
|
||||
HVACAction.IDLE,
|
||||
HVACAction.OFF,
|
||||
]
|
||||
else:
|
||||
return None
|
||||
|
||||
async def set_fan_mode(self, fan_mode):
|
||||
"""Set new target fan mode."""
|
||||
if not self.is_initialized:
|
||||
return
|
||||
data = {
|
||||
ATTR_ENTITY_ID: self._entity_id,
|
||||
"fan_mode": fan_mode,
|
||||
}
|
||||
|
||||
await self._hass.services.async_call(
|
||||
CLIMATE_DOMAIN,
|
||||
SERVICE_SET_FAN_MODE,
|
||||
data,
|
||||
)
|
||||
|
||||
async def set_humidity(self, humidity: int):
|
||||
"""Set new target humidity."""
|
||||
_LOGGER.info("%s - Set fan mode: %s", self, humidity)
|
||||
if not self.is_initialized:
|
||||
return
|
||||
data = {
|
||||
ATTR_ENTITY_ID: self._entity_id,
|
||||
"humidity": humidity,
|
||||
}
|
||||
|
||||
await self._hass.services.async_call(
|
||||
CLIMATE_DOMAIN,
|
||||
SERVICE_SET_HUMIDITY,
|
||||
data,
|
||||
)
|
||||
|
||||
async def set_swing_mode(self, swing_mode):
|
||||
"""Set new target swing operation."""
|
||||
_LOGGER.info("%s - Set fan mode: %s", self, swing_mode)
|
||||
if not self.is_initialized:
|
||||
return
|
||||
data = {
|
||||
ATTR_ENTITY_ID: self._entity_id,
|
||||
"swing_mode": swing_mode,
|
||||
}
|
||||
|
||||
await self._hass.services.async_call(
|
||||
CLIMATE_DOMAIN,
|
||||
SERVICE_SET_SWING_MODE,
|
||||
data,
|
||||
)
|
||||
|
||||
async def set_temperature(self, temperature, max_temp, min_temp):
|
||||
"""Set the target temperature"""
|
||||
if not self.is_initialized:
|
||||
return
|
||||
data = {
|
||||
ATTR_ENTITY_ID: self._entity_id,
|
||||
"temperature": temperature,
|
||||
"target_temp_high": max_temp,
|
||||
"target_temp_low": min_temp,
|
||||
}
|
||||
|
||||
await self._hass.services.async_call(
|
||||
CLIMATE_DOMAIN,
|
||||
SERVICE_SET_TEMPERATURE,
|
||||
data,
|
||||
)
|
||||
|
||||
@property
|
||||
def hvac_action(self) -> HVACAction | None:
|
||||
"""Get the hvac action of the underlying"""
|
||||
if not self.is_initialized:
|
||||
return None
|
||||
return self._underlying_climate.hvac_action
|
||||
|
||||
@property
|
||||
def hvac_mode(self) -> HVACMode | None:
|
||||
"""Get the hvac mode of the underlying"""
|
||||
if not self.is_initialized:
|
||||
return None
|
||||
return self._underlying_climate.hvac_mode
|
||||
|
||||
@property
|
||||
def fan_mode(self) -> str | None:
|
||||
"""Get the fan_mode of the underlying"""
|
||||
if not self.is_initialized:
|
||||
return None
|
||||
return self._underlying_climate.fan_mode
|
||||
|
||||
@property
|
||||
def swing_mode(self) -> str | None:
|
||||
"""Get the swing_mode of the underlying"""
|
||||
if not self.is_initialized:
|
||||
return None
|
||||
return self._underlying_climate.swing_mode
|
||||
|
||||
@property
|
||||
def supported_features(self) -> ClimateEntityFeature:
|
||||
"""Get the supported features of the climate"""
|
||||
if not self.is_initialized:
|
||||
return ClimateEntityFeature.TARGET_TEMPERATURE
|
||||
return self._underlying_climate.supported_features
|
||||
|
||||
@property
|
||||
def hvac_modes(self) -> list[HVACMode]:
|
||||
"""Get the hvac_modes"""
|
||||
if not self.is_initialized:
|
||||
return []
|
||||
return self._underlying_climate.hvac_modes
|
||||
|
||||
@property
|
||||
def fan_modes(self) -> list[str]:
|
||||
"""Get the fan_modes"""
|
||||
if not self.is_initialized:
|
||||
return []
|
||||
return self._underlying_climate.fan_modes
|
||||
|
||||
@property
|
||||
def swing_modes(self) -> list[str]:
|
||||
"""Get the swing_modes"""
|
||||
if not self.is_initialized:
|
||||
return []
|
||||
return self._underlying_climate.swing_modes
|
||||
|
||||
@property
|
||||
def temperature_unit(self) -> str:
|
||||
"""Get the temperature_unit"""
|
||||
if not self.is_initialized:
|
||||
return UnitOfTemperature.CELSIUS
|
||||
return self._underlying_climate.temperature_unit
|
||||
|
||||
@property
|
||||
def target_temperature_step(self) -> float:
|
||||
"""Get the target_temperature_step"""
|
||||
if not self.is_initialized:
|
||||
return 1
|
||||
return self._underlying_climate.target_temperature_step
|
||||
|
||||
@property
|
||||
def target_temperature_high(self) -> float:
|
||||
"""Get the target_temperature_high"""
|
||||
if not self.is_initialized:
|
||||
return 30
|
||||
return self._underlying_climate.target_temperature_high
|
||||
|
||||
@property
|
||||
def target_temperature_low(self) -> float:
|
||||
"""Get the target_temperature_low"""
|
||||
if not self.is_initialized:
|
||||
return 15
|
||||
return self._underlying_climate.target_temperature_low
|
||||
|
||||
@property
|
||||
def is_aux_heat(self) -> bool:
|
||||
"""Get the is_aux_heat"""
|
||||
if not self.is_initialized:
|
||||
return False
|
||||
return self._underlying_climate.is_aux_heat
|
||||
|
||||
def turn_aux_heat_on(self) -> None:
|
||||
"""Turn auxiliary heater on."""
|
||||
if not self.is_initialized:
|
||||
return None
|
||||
return self._underlying_climate.turn_aux_heat_on()
|
||||
|
||||
def turn_aux_heat_off(self) -> None:
|
||||
"""Turn auxiliary heater on."""
|
||||
if not self.is_initialized:
|
||||
return None
|
||||
return self._underlying_climate.turn_aux_heat_off()
|
||||
@@ -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: 13 KiB After Width: | Height: | Size: 37 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 46 KiB |
BIN
images/multi-switch-activation.png
Normal file
BIN
images/multi-switch-activation.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 22 KiB |
2
requirements_dev.txt
Normal file
2
requirements_dev.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
homeassistant==2023.10.3
|
||||
ffmpeg
|
||||
6
requirements_test.txt
Normal file
6
requirements_test.txt
Normal 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
29
scripts/starts_ha.sh
Executable 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
3
setup.cfg
Normal file
@@ -0,0 +1,3 @@
|
||||
[tool:pytest]
|
||||
testpaths = tests
|
||||
asyncio_mode = auto
|
||||
@@ -1,8 +1,11 @@
|
||||
""" Some common resources """
|
||||
import asyncio
|
||||
import logging
|
||||
from unittest.mock import patch, MagicMock
|
||||
import pytest # pylint: disable=unused-import
|
||||
|
||||
from homeassistant.core import HomeAssistant, Event, EVENT_STATE_CHANGED, State
|
||||
from homeassistant.const import UnitOfTemperature, STATE_ON, STATE_OFF
|
||||
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
|
||||
@@ -17,20 +20,26 @@ 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 custom_components.versatile_thermostat.climate import VersatileThermostat
|
||||
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 .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_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,
|
||||
@@ -52,6 +61,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
|
||||
@@ -59,11 +81,57 @@ PARTIAL_CLIMATE_CONFIG = (
|
||||
| MOCK_ADVANCED_CONFIG
|
||||
)
|
||||
|
||||
FULL_4SWITCH_CONFIG = (
|
||||
MOCK_TH_OVER_4SWITCH_USER_CONFIG
|
||||
| MOCK_TH_OVER_4SWITCH_TYPE_CONFIG
|
||||
| MOCK_TH_OVER_SWITCH_TPI_CONFIG
|
||||
| MOCK_PRESETS_CONFIG
|
||||
| MOCK_WINDOW_CONFIG
|
||||
| MOCK_MOTION_CONFIG
|
||||
| MOCK_POWER_CONFIG
|
||||
| MOCK_PRESENCE_CONFIG
|
||||
| MOCK_ADVANCED_CONFIG
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class MockClimate(ClimateEntity):
|
||||
"""A Mock Climate class used for Underlying climate mode"""
|
||||
|
||||
def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None:
|
||||
def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos, hvac_mode:HVACMode = HVACMode.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
|
||||
|
||||
def set_temperature(self, **kwargs):
|
||||
""" Set the target temperature"""
|
||||
temperature = kwargs.get(ATTR_TEMPERATURE)
|
||||
self._attr_target_temperature = temperature
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def async_set_hvac_mode(self, hvac_mode):
|
||||
""" The hvac mode"""
|
||||
self._attr_hvac_mode = hvac_mode
|
||||
self.async_write_ha_state()
|
||||
|
||||
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__()
|
||||
@@ -72,12 +140,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"""
|
||||
|
||||
@@ -180,8 +247,16 @@ def search_entity(hass: HomeAssistant, entity_id, domain) -> Entity:
|
||||
return None
|
||||
|
||||
|
||||
async def send_temperature_change_event(entity: VersatileThermostat, new_temp, date):
|
||||
async def send_temperature_change_event(
|
||||
entity: VersatileThermostat, new_temp, date, sleep=True
|
||||
):
|
||||
"""Sending a new temperature event simulating a change on temperature sensor"""
|
||||
_LOGGER.info(
|
||||
"------- Testu: sending send_temperature_change_event, new_temp=%.2f date=%s on %s",
|
||||
new_temp,
|
||||
date,
|
||||
entity,
|
||||
)
|
||||
temp_event = Event(
|
||||
EVENT_STATE_CHANGED,
|
||||
{
|
||||
@@ -193,13 +268,21 @@ async def send_temperature_change_event(entity: VersatileThermostat, new_temp, d
|
||||
)
|
||||
},
|
||||
)
|
||||
return await entity._async_temperature_changed(temp_event)
|
||||
await entity._async_temperature_changed(temp_event)
|
||||
if sleep:
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
|
||||
async def send_ext_temperature_change_event(
|
||||
entity: VersatileThermostat, new_temp, date
|
||||
entity: VersatileThermostat, new_temp, date, sleep=True
|
||||
):
|
||||
"""Sending a new external temperature event simulating a change on temperature sensor"""
|
||||
_LOGGER.info(
|
||||
"------- Testu: sending send_temperature_change_event, new_temp=%.2f date=%s on %s",
|
||||
new_temp,
|
||||
date,
|
||||
entity,
|
||||
)
|
||||
temp_event = Event(
|
||||
EVENT_STATE_CHANGED,
|
||||
{
|
||||
@@ -211,11 +294,21 @@ async def send_ext_temperature_change_event(
|
||||
)
|
||||
},
|
||||
)
|
||||
return await entity._async_ext_temperature_changed(temp_event)
|
||||
await entity._async_ext_temperature_changed(temp_event)
|
||||
if sleep:
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
|
||||
async def send_power_change_event(entity: VersatileThermostat, new_power, date):
|
||||
async def send_power_change_event(
|
||||
entity: VersatileThermostat, new_power, date, sleep=True
|
||||
):
|
||||
"""Sending a new power event simulating a change on power sensor"""
|
||||
_LOGGER.info(
|
||||
"------- Testu: sending send_temperature_change_event, new_power=%.2f date=%s on %s",
|
||||
new_power,
|
||||
date,
|
||||
entity,
|
||||
)
|
||||
power_event = Event(
|
||||
EVENT_STATE_CHANGED,
|
||||
{
|
||||
@@ -227,11 +320,21 @@ async def send_power_change_event(entity: VersatileThermostat, new_power, date):
|
||||
)
|
||||
},
|
||||
)
|
||||
return await entity._async_power_changed(power_event)
|
||||
await entity._async_power_changed(power_event)
|
||||
if sleep:
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
|
||||
async def send_max_power_change_event(entity: VersatileThermostat, new_power_max, date):
|
||||
async def send_max_power_change_event(
|
||||
entity: VersatileThermostat, new_power_max, date, sleep=True
|
||||
):
|
||||
"""Sending a new power max event simulating a change on power max sensor"""
|
||||
_LOGGER.info(
|
||||
"------- Testu: sending send_temperature_change_event, new_power_max=%.2f date=%s on %s",
|
||||
new_power_max,
|
||||
date,
|
||||
entity,
|
||||
)
|
||||
power_event = Event(
|
||||
EVENT_STATE_CHANGED,
|
||||
{
|
||||
@@ -243,13 +346,22 @@ async def send_max_power_change_event(entity: VersatileThermostat, new_power_max
|
||||
)
|
||||
},
|
||||
)
|
||||
return await entity._async_max_power_changed(power_event)
|
||||
await entity._async_max_power_changed(power_event)
|
||||
if sleep:
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
|
||||
async def send_window_change_event(
|
||||
entity: VersatileThermostat, new_state: bool, old_state: bool, date
|
||||
entity: VersatileThermostat, new_state: bool, old_state: bool, date, sleep=True
|
||||
):
|
||||
"""Sending a new window event simulating a change on the window state"""
|
||||
_LOGGER.info(
|
||||
"------- Testu: sending send_temperature_change_event, new_state=%s old_state=%s date=%s on %s",
|
||||
new_state,
|
||||
old_state,
|
||||
date,
|
||||
entity,
|
||||
)
|
||||
window_event = Event(
|
||||
EVENT_STATE_CHANGED,
|
||||
{
|
||||
@@ -268,13 +380,22 @@ async def send_window_change_event(
|
||||
},
|
||||
)
|
||||
ret = await entity._async_windows_changed(window_event)
|
||||
if sleep:
|
||||
await asyncio.sleep(0.1)
|
||||
return ret
|
||||
|
||||
|
||||
async def send_motion_change_event(
|
||||
entity: VersatileThermostat, new_state: bool, old_state: bool, date
|
||||
entity: VersatileThermostat, new_state: bool, old_state: bool, date, sleep=True
|
||||
):
|
||||
"""Sending a new motion event simulating a change on the window state"""
|
||||
_LOGGER.info(
|
||||
"------- Testu: sending send_temperature_change_event, new_state=%s old_state=%s date=%s on %s",
|
||||
new_state,
|
||||
old_state,
|
||||
date,
|
||||
entity,
|
||||
)
|
||||
motion_event = Event(
|
||||
EVENT_STATE_CHANGED,
|
||||
{
|
||||
@@ -293,13 +414,22 @@ async def send_motion_change_event(
|
||||
},
|
||||
)
|
||||
ret = await entity._async_motion_changed(motion_event)
|
||||
if sleep:
|
||||
await asyncio.sleep(0.1)
|
||||
return ret
|
||||
|
||||
|
||||
async def send_presence_change_event(
|
||||
entity: VersatileThermostat, new_state: bool, old_state: bool, date
|
||||
entity: VersatileThermostat, new_state: bool, old_state: bool, date, sleep=True
|
||||
):
|
||||
"""Sending a new presence event simulating a change on the window state"""
|
||||
_LOGGER.info(
|
||||
"------- Testu: sending send_temperature_change_event, new_state=%s old_state=%s date=%s on %s",
|
||||
new_state,
|
||||
old_state,
|
||||
date,
|
||||
entity,
|
||||
)
|
||||
presence_event = Event(
|
||||
EVENT_STATE_CHANGED,
|
||||
{
|
||||
@@ -318,6 +448,8 @@ async def send_presence_change_event(
|
||||
},
|
||||
)
|
||||
ret = await entity._async_presence_changed(presence_event)
|
||||
if sleep:
|
||||
await asyncio.sleep(0.1)
|
||||
return ret
|
||||
|
||||
|
||||
@@ -334,8 +466,18 @@ async def send_climate_change_event(
|
||||
new_hvac_action: HVACAction,
|
||||
old_hvac_action: HVACAction,
|
||||
date,
|
||||
sleep=True,
|
||||
):
|
||||
"""Sending a new climate event simulating a change on the underlying climate state"""
|
||||
_LOGGER.info(
|
||||
"------- Testu: sending send_temperature_change_event, new_hvac_mode=%s old_hvac_mode=%s new_hvac_action=%s old_hvac_action=%s date=%s on %s",
|
||||
new_hvac_mode,
|
||||
old_hvac_mode,
|
||||
new_hvac_action,
|
||||
old_hvac_action,
|
||||
date,
|
||||
entity,
|
||||
)
|
||||
climate_event = Event(
|
||||
EVENT_STATE_CHANGED,
|
||||
{
|
||||
@@ -356,4 +498,59 @@ async def send_climate_change_event(
|
||||
},
|
||||
)
|
||||
ret = await entity._async_climate_changed(climate_event)
|
||||
if sleep:
|
||||
await asyncio.sleep(0.1)
|
||||
return ret
|
||||
|
||||
async def send_climate_change_event_with_temperature(
|
||||
entity: VersatileThermostat,
|
||||
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):
|
||||
"""This method will cancel all running cycle on all underlying switch entity"""
|
||||
if entity._is_over_climate:
|
||||
return
|
||||
for under in entity._underlyings:
|
||||
under._cancel_cycle()
|
||||
@@ -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.
|
||||
@@ -34,7 +36,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
|
||||
|
||||
@@ -55,9 +57,9 @@ def skip_notifications_fixture():
|
||||
def skip_turn_on_off_heater():
|
||||
"""Skip turning on and off the heater"""
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.climate.VersatileThermostat._async_heater_turn_on"
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingEntity.turn_on"
|
||||
), patch(
|
||||
"custom_components.versatile_thermostat.climate.VersatileThermostat._async_underlying_entity_turn_off"
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingEntity.turn_off"
|
||||
):
|
||||
yield
|
||||
|
||||
@@ -87,6 +89,15 @@ def skip_control_heating_fixture():
|
||||
yield
|
||||
|
||||
|
||||
@pytest.fixture(name="skip_find_underlying_climate")
|
||||
def skip_find_underlying_climate_fixture():
|
||||
"""Skip the find_underlying_climate of VersatileThermostat"""
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate"
|
||||
):
|
||||
yield
|
||||
|
||||
|
||||
@pytest.fixture(name="skip_hass_states_is_state")
|
||||
def skip_hass_states_is_state_fixture():
|
||||
"""Skip the is_state in HomeAssistant"""
|
||||
@@ -9,9 +9,13 @@ from homeassistant.components.climate.const import ( # pylint: disable=unused-i
|
||||
from custom_components.versatile_thermostat.const import (
|
||||
CONF_NAME,
|
||||
CONF_HEATER,
|
||||
CONF_HEATER_2,
|
||||
CONF_HEATER_3,
|
||||
CONF_HEATER_4,
|
||||
CONF_THERMOSTAT_CLIMATE,
|
||||
CONF_THERMOSTAT_SWITCH,
|
||||
CONF_THERMOSTAT_TYPE,
|
||||
CONF_AC_MODE,
|
||||
CONF_TEMP_SENSOR,
|
||||
CONF_EXTERNAL_TEMP_SENSOR,
|
||||
CONF_CYCLE_MIN,
|
||||
@@ -36,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,
|
||||
@@ -46,7 +51,6 @@ from custom_components.versatile_thermostat.const import (
|
||||
PRESET_AWAY_SUFFIX,
|
||||
CONF_CLIMATE,
|
||||
)
|
||||
|
||||
MOCK_TH_OVER_SWITCH_USER_CONFIG = {
|
||||
CONF_NAME: "TheOverSwitchMockName",
|
||||
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_SWITCH,
|
||||
@@ -62,6 +66,21 @@ MOCK_TH_OVER_SWITCH_USER_CONFIG = {
|
||||
CONF_USE_PRESENCE_FEATURE: True,
|
||||
}
|
||||
|
||||
MOCK_TH_OVER_4SWITCH_USER_CONFIG = {
|
||||
CONF_NAME: "TheOver4SwitchMockName",
|
||||
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_SWITCH,
|
||||
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
|
||||
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
|
||||
CONF_CYCLE_MIN: 8,
|
||||
CONF_TEMP_MIN: 15,
|
||||
CONF_TEMP_MAX: 30,
|
||||
CONF_DEVICE_POWER: 1,
|
||||
CONF_USE_WINDOW_FEATURE: True,
|
||||
CONF_USE_MOTION_FEATURE: True,
|
||||
CONF_USE_POWER_FEATURE: True,
|
||||
CONF_USE_PRESENCE_FEATURE: True,
|
||||
}
|
||||
|
||||
MOCK_TH_OVER_CLIMATE_USER_CONFIG = {
|
||||
CONF_NAME: "TheOverClimateMockName",
|
||||
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_CLIMATE,
|
||||
@@ -77,6 +96,22 @@ MOCK_TH_OVER_CLIMATE_USER_CONFIG = {
|
||||
MOCK_TH_OVER_SWITCH_TYPE_CONFIG = {
|
||||
CONF_HEATER: "switch.mock_switch",
|
||||
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
|
||||
CONF_AC_MODE: False,
|
||||
}
|
||||
|
||||
MOCK_TH_OVER_SWITCH_AC_TYPE_CONFIG = {
|
||||
CONF_HEATER: "switch.mock_air_conditioner",
|
||||
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
|
||||
CONF_AC_MODE: True,
|
||||
}
|
||||
|
||||
MOCK_TH_OVER_4SWITCH_TYPE_CONFIG = {
|
||||
CONF_HEATER: "switch.mock_4switch0",
|
||||
CONF_HEATER_2: "switch.mock_4switch1",
|
||||
CONF_HEATER_3: "switch.mock_4switch2",
|
||||
CONF_HEATER_4: "switch.mock_4switch3",
|
||||
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
|
||||
CONF_AC_MODE: False,
|
||||
}
|
||||
|
||||
MOCK_TH_OVER_SWITCH_TPI_CONFIG = {
|
||||
@@ -86,6 +121,7 @@ MOCK_TH_OVER_SWITCH_TPI_CONFIG = {
|
||||
|
||||
MOCK_TH_OVER_CLIMATE_TYPE_CONFIG = {
|
||||
CONF_CLIMATE: "climate.mock_climate",
|
||||
CONF_AC_MODE: False,
|
||||
}
|
||||
|
||||
MOCK_PRESETS_CONFIG = {
|
||||
@@ -94,6 +130,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,
|
||||
@@ -108,6 +153,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,
|
||||
}
|
||||
@@ -125,6 +171,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,
|
||||
@@ -9,8 +9,8 @@ from homeassistant.components.binary_sensor import BinarySensorDeviceClass
|
||||
|
||||
from pytest_homeassistant_custom_component.common import MockConfigEntry
|
||||
|
||||
from ..climate import VersatileThermostat
|
||||
from ..binary_sensor import (
|
||||
from custom_components.versatile_thermostat.climate import VersatileThermostat
|
||||
from custom_components.versatile_thermostat.binary_sensor import (
|
||||
SecurityBinarySensor,
|
||||
OverpoweringBinarySensor,
|
||||
WindowBinarySensor,
|
||||
@@ -21,12 +21,14 @@ from ..binary_sensor import (
|
||||
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_security_binary_sensors(
|
||||
hass: HomeAssistant,
|
||||
skip_hass_states_is_state,
|
||||
skip_turn_on_off_heater,
|
||||
skip_send_event,
|
||||
):
|
||||
): # pylint: disable=unused-argument
|
||||
"""Test the security binary sensors in thermostat type"""
|
||||
|
||||
entry = MockConfigEntry(
|
||||
@@ -96,6 +98,8 @@ async def test_security_binary_sensors(
|
||||
assert security_binary_sensor.state == STATE_OFF
|
||||
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_overpowering_binary_sensors(
|
||||
hass: HomeAssistant,
|
||||
skip_hass_states_is_state,
|
||||
@@ -178,6 +182,8 @@ async def test_overpowering_binary_sensors(
|
||||
assert overpowering_binary_sensor.state == STATE_OFF
|
||||
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_window_binary_sensors(
|
||||
hass: HomeAssistant,
|
||||
skip_hass_states_is_state,
|
||||
@@ -264,6 +270,8 @@ async def test_window_binary_sensors(
|
||||
assert window_binary_sensor.state == STATE_OFF
|
||||
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_motion_binary_sensors(
|
||||
hass: HomeAssistant,
|
||||
skip_hass_states_is_state,
|
||||
@@ -350,6 +358,8 @@ async def test_motion_binary_sensors(
|
||||
assert motion_binary_sensor.state == STATE_OFF
|
||||
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_presence_binary_sensors(
|
||||
hass: HomeAssistant,
|
||||
skip_hass_states_is_state,
|
||||
@@ -432,6 +442,8 @@ async def test_presence_binary_sensors(
|
||||
assert presence_binary_sensor.state == STATE_OFF
|
||||
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_binary_sensors_over_climate_minimal(
|
||||
hass: HomeAssistant,
|
||||
skip_hass_states_is_state,
|
||||
@@ -442,7 +454,7 @@ async def test_binary_sensors_over_climate_minimal(
|
||||
|
||||
the_mock_underlying = MagicMockClimate()
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.climate.VersatileThermostat.find_underlying_climate",
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
|
||||
return_value=the_mock_underlying,
|
||||
):
|
||||
entry = MockConfigEntry(
|
||||
@@ -1,5 +1,5 @@
|
||||
""" Test the Window management """
|
||||
from unittest.mock import patch
|
||||
from unittest.mock import patch, call
|
||||
from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
@@ -8,6 +8,8 @@ import logging
|
||||
logging.getLogger().setLevel(logging.DEBUG)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_bug_56(
|
||||
hass: HomeAssistant,
|
||||
skip_hass_states_is_state,
|
||||
@@ -18,7 +20,7 @@ async def test_bug_56(
|
||||
|
||||
the_mock_underlying = MagicMockClimate()
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.climate.VersatileThermostat.find_underlying_climate",
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
|
||||
return_value=None, # dont find the underlying climate
|
||||
):
|
||||
entry = MockConfigEntry(
|
||||
@@ -53,24 +55,22 @@ async def test_bug_56(
|
||||
assert entity
|
||||
# cause the underlying climate was not found
|
||||
assert entity.is_over_climate is True
|
||||
assert entity._underlying_climate is None
|
||||
assert entity.underlying_entity(0)._underlying_climate is None
|
||||
|
||||
# Should not failed
|
||||
entity.update_custom_attributes()
|
||||
|
||||
# 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
|
||||
|
||||
# This time the underlying will be found
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.climate.VersatileThermostat.find_underlying_climate",
|
||||
"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
|
||||
@@ -85,6 +85,8 @@ async def test_bug_56(
|
||||
entity.update_custom_attributes()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_bug_63(
|
||||
hass: HomeAssistant,
|
||||
skip_hass_states_is_state,
|
||||
@@ -135,6 +137,8 @@ async def test_bug_63(
|
||||
|
||||
# Waiting for answer in https://github.com/jmcollin78/versatile_thermostat/issues/64
|
||||
# Repro case not evident
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_bug_64(
|
||||
hass: HomeAssistant,
|
||||
skip_hass_states_is_state,
|
||||
@@ -180,6 +184,8 @@ async def test_bug_64(
|
||||
assert entity
|
||||
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_bug_66(
|
||||
hass: HomeAssistant,
|
||||
skip_hass_states_is_state,
|
||||
@@ -241,24 +247,26 @@ async def test_bug_66(
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
|
||||
) as mock_send_event, patch(
|
||||
"custom_components.versatile_thermostat.climate.VersatileThermostat._async_heater_turn_on"
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
|
||||
) as mock_heater_on, patch(
|
||||
"custom_components.versatile_thermostat.climate.VersatileThermostat._async_underlying_entity_turn_off"
|
||||
"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.climate.VersatileThermostat._is_device_active",
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
|
||||
return_value=True,
|
||||
):
|
||||
await send_temperature_change_event(entity, 15, now)
|
||||
try_window_condition = await send_window_change_event(entity, True, False, now)
|
||||
try_window_condition = await send_window_change_event(
|
||||
entity, True, False, now, False
|
||||
)
|
||||
|
||||
# simulate the call to try_window_condition
|
||||
await try_window_condition(None)
|
||||
|
||||
assert mock_send_event.call_count == 1
|
||||
assert mock_heater_on.call_count == 1
|
||||
assert mock_heater_off.call_count == 2
|
||||
assert mock_heater_off.call_count >= 1
|
||||
assert mock_condition.call_count == 1
|
||||
|
||||
assert entity.window_state == STATE_ON
|
||||
@@ -267,13 +275,13 @@ async def test_bug_66(
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
|
||||
) as mock_send_event, patch(
|
||||
"custom_components.versatile_thermostat.climate.VersatileThermostat._async_heater_turn_on"
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
|
||||
) as mock_heater_on, patch(
|
||||
"custom_components.versatile_thermostat.climate.VersatileThermostat._async_underlying_entity_turn_off"
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
|
||||
) as mock_heater_off, patch(
|
||||
"homeassistant.helpers.condition.state", return_value=False
|
||||
) as mock_condition, patch(
|
||||
"custom_components.versatile_thermostat.climate.VersatileThermostat._is_device_active",
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
|
||||
return_value=False,
|
||||
):
|
||||
event_timestamp = now + timedelta(minutes=1)
|
||||
@@ -290,13 +298,13 @@ async def test_bug_66(
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
|
||||
) as mock_send_event, patch(
|
||||
"custom_components.versatile_thermostat.climate.VersatileThermostat._async_heater_turn_on"
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
|
||||
) as mock_heater_on, patch(
|
||||
"custom_components.versatile_thermostat.climate.VersatileThermostat._async_underlying_entity_turn_off"
|
||||
"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.climate.VersatileThermostat._is_device_active",
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
|
||||
return_value=False,
|
||||
):
|
||||
try_window_condition = await send_window_change_event(
|
||||
@@ -313,13 +321,13 @@ async def test_bug_66(
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
|
||||
) as mock_send_event, patch(
|
||||
"custom_components.versatile_thermostat.climate.VersatileThermostat._async_heater_turn_on"
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
|
||||
) as mock_heater_on, patch(
|
||||
"custom_components.versatile_thermostat.climate.VersatileThermostat._async_underlying_entity_turn_off"
|
||||
"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.climate.VersatileThermostat._is_device_active",
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
|
||||
return_value=False,
|
||||
):
|
||||
event_timestamp = now + timedelta(minutes=2)
|
||||
@@ -333,3 +341,205 @@ 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.climate.VersatileThermostat.send_event"
|
||||
) as mock_send_event, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
|
||||
return_value=fake_underlying_climate,
|
||||
) as mock_find_climate:
|
||||
entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(entry.entry_id)
|
||||
assert entry.state is ConfigEntryState.LOADED
|
||||
|
||||
def find_my_entity(entity_id) -> ClimateEntity:
|
||||
"""Find my new entity"""
|
||||
component: EntityComponent[ClimateEntity] = hass.data[CLIMATE_DOMAIN]
|
||||
for entity in component.entities:
|
||||
if entity.entity_id == entity_id:
|
||||
return entity
|
||||
|
||||
entity = find_my_entity("climate.theoverclimatemockname")
|
||||
|
||||
assert entity
|
||||
|
||||
assert entity.name == "TheOverClimateMockName"
|
||||
assert entity._is_over_climate is True
|
||||
# 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.climate.VersatileThermostat.send_event"
|
||||
) as mock_send_event, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
|
||||
) as mock_heater_on:
|
||||
event_timestamp = now - timedelta(minutes=6)
|
||||
|
||||
# set temperature to 15 so that on_percent will be > security_min_on_percent (0.2)
|
||||
await send_temperature_change_event(entity, 15, event_timestamp)
|
||||
# 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_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)
|
||||
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
|
||||
) as mock_send_event, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
|
||||
return_value=fake_underlying_climate,
|
||||
) as mock_find_climate, 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 the underlying is heating. In real life the underlying should be shut-off
|
||||
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)
|
||||
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
|
||||
|
||||
|
||||
|
||||
@@ -6,9 +6,12 @@ from homeassistant.config_entries import SOURCE_USER, ConfigEntry
|
||||
|
||||
from custom_components.versatile_thermostat.const import DOMAIN
|
||||
|
||||
from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import
|
||||
from .const import * # pylint: disable=wildcard-import, unused-wildcard-import
|
||||
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_show_form(hass: HomeAssistant) -> None:
|
||||
"""Test that the form is served with no input"""
|
||||
# Init the API
|
||||
@@ -24,7 +27,9 @@ async def test_show_form(hass: HomeAssistant) -> None:
|
||||
assert result["step_id"] == SOURCE_USER
|
||||
|
||||
|
||||
async def test_user_config_flow_over_switch(hass: HomeAssistant, skip_hass_states_get):
|
||||
@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): # 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}
|
||||
@@ -121,7 +126,9 @@ async def test_user_config_flow_over_switch(hass: HomeAssistant, skip_hass_state
|
||||
assert isinstance(result["result"], ConfigEntry)
|
||||
|
||||
|
||||
async def test_user_config_flow_over_climate(hass: HomeAssistant, skip_hass_states_get):
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_user_config_flow_over_climate(hass: HomeAssistant, skip_hass_states_get): # pylint: disable=unused-argument
|
||||
"""Test the config flow with all thermostat_over_climate features and no additional features"""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
@@ -150,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"] == {}
|
||||
@@ -206,8 +181,10 @@ async def test_user_config_flow_over_climate(hass: HomeAssistant, skip_hass_stat
|
||||
assert isinstance(result["result"], ConfigEntry)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_user_config_flow_window_auto_ok(
|
||||
hass: HomeAssistant, skip_hass_states_get, skip_control_heating
|
||||
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(
|
||||
@@ -301,8 +278,10 @@ async def test_user_config_flow_window_auto_ok(
|
||||
assert isinstance(result["result"], ConfigEntry)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_user_config_flow_window_auto_ko(
|
||||
hass: HomeAssistant, skip_hass_states_get
|
||||
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(
|
||||
@@ -369,3 +348,95 @@ async def test_user_config_flow_window_auto_ko(
|
||||
assert result["errors"] == {
|
||||
"window_sensor_entity_id": "window_open_detection_method"
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_user_config_flow_over_4_switches(
|
||||
hass: HomeAssistant, skip_hass_states_get, skip_control_heating # pylint: disable=unused-argument
|
||||
):
|
||||
"""Test the config flow with 4 switchs thermostat_over_switch features"""
|
||||
|
||||
SOURCE_CONFIG = { # pylint: disable=wildcard-import, invalid-name
|
||||
CONF_NAME: "TheOver4SwitchMockName",
|
||||
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_SWITCH,
|
||||
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
|
||||
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
|
||||
CONF_CYCLE_MIN: 5,
|
||||
CONF_TEMP_MIN: 15,
|
||||
CONF_TEMP_MAX: 30,
|
||||
CONF_DEVICE_POWER: 1,
|
||||
CONF_USE_WINDOW_FEATURE: False,
|
||||
CONF_USE_MOTION_FEATURE: False,
|
||||
CONF_USE_POWER_FEATURE: False,
|
||||
CONF_USE_PRESENCE_FEATURE: False,
|
||||
}
|
||||
|
||||
TYPE_CONFIG = { # pylint: disable=wildcard-import, invalid-name
|
||||
CONF_HEATER: "switch.mock_switch1",
|
||||
CONF_HEATER_2: "switch.mock_switch2",
|
||||
CONF_HEATER_3: "switch.mock_switch3",
|
||||
CONF_HEATER_4: "switch.mock_switch4",
|
||||
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
|
||||
CONF_AC_MODE: False,
|
||||
}
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["step_id"] == SOURCE_USER
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input=SOURCE_CONFIG,
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["step_id"] == "type"
|
||||
assert result["errors"] == {}
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input=TYPE_CONFIG,
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["step_id"] == "tpi"
|
||||
assert result["errors"] == {}
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input=MOCK_TH_OVER_SWITCH_TPI_CONFIG
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["step_id"] == "presets"
|
||||
assert result["errors"] == {}
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input=MOCK_PRESETS_CONFIG
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["step_id"] == "advanced"
|
||||
assert result["errors"] == {}
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input=MOCK_ADVANCED_CONFIG
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
|
||||
assert (
|
||||
result["data"]
|
||||
== SOURCE_CONFIG
|
||||
| TYPE_CONFIG
|
||||
| MOCK_TH_OVER_SWITCH_TPI_CONFIG
|
||||
| MOCK_PRESETS_CONFIG
|
||||
| MOCK_ADVANCED_CONFIG
|
||||
)
|
||||
assert result["result"]
|
||||
assert result["result"].domain == DOMAIN
|
||||
assert result["result"].version == 1
|
||||
assert result["result"].title == "TheOver4SwitchMockName"
|
||||
assert isinstance(result["result"], ConfigEntry)
|
||||
600
tests/test_movement.py
Normal file
600
tests/test_movement.py
Normal file
@@ -0,0 +1,600 @@
|
||||
""" Test the Window management """
|
||||
import asyncio
|
||||
from datetime import datetime, timedelta
|
||||
import logging
|
||||
from unittest.mock import patch, call, PropertyMock
|
||||
from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import
|
||||
|
||||
|
||||
logging.getLogger().setLevel(logging.DEBUG)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_movement_management_time_not_enough(
|
||||
hass: HomeAssistant, skip_hass_states_is_state
|
||||
):
|
||||
"""Test the Presence management when time is not enough"""
|
||||
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
title="TheOverSwitchMockName",
|
||||
unique_id="uniqueId",
|
||||
data={
|
||||
CONF_NAME: "TheOverSwitchMockName",
|
||||
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_SWITCH,
|
||||
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
|
||||
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
|
||||
CONF_CYCLE_MIN: 5,
|
||||
CONF_TEMP_MIN: 15,
|
||||
CONF_TEMP_MAX: 30,
|
||||
"eco_temp": 17,
|
||||
"comfort_temp": 18,
|
||||
"boost_temp": 19,
|
||||
"eco_away_temp": 17,
|
||||
"comfort_away_temp": 18,
|
||||
"boost_away_temp": 19,
|
||||
CONF_USE_WINDOW_FEATURE: False,
|
||||
CONF_USE_MOTION_FEATURE: True,
|
||||
CONF_USE_POWER_FEATURE: False,
|
||||
CONF_USE_PRESENCE_FEATURE: True,
|
||||
CONF_HEATER: "switch.mock_switch",
|
||||
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
|
||||
CONF_TPI_COEF_INT: 0.3,
|
||||
CONF_TPI_COEF_EXT: 0.01,
|
||||
CONF_MINIMAL_ACTIVATION_DELAY: 30,
|
||||
CONF_SECURITY_DELAY_MIN: 10,
|
||||
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
|
||||
CONF_MOTION_SENSOR: "binary_sensor.mock_motion_sensor",
|
||||
CONF_MOTION_DELAY: 10, # important to not been obliged to wait
|
||||
CONF_MOTION_OFF_DELAY: 30,
|
||||
CONF_MOTION_PRESET: "boost",
|
||||
CONF_NO_MOTION_PRESET: "comfort",
|
||||
CONF_PRESENCE_SENSOR: "binary_sensor.mock_presence_sensor",
|
||||
},
|
||||
)
|
||||
|
||||
entity: VersatileThermostat = await create_thermostat(
|
||||
hass, entry, "climate.theoverswitchmockname"
|
||||
)
|
||||
assert entity
|
||||
|
||||
tz = get_tz(hass) # pylint: disable=invalid-name
|
||||
now: datetime = datetime.now(tz=tz)
|
||||
|
||||
# start heating, in boost mode, when someone is present. We block the control_heating to avoid running a cycle
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.climate.VersatileThermostat._async_control_heating"
|
||||
):
|
||||
await entity.async_set_hvac_mode(HVACMode.HEAT)
|
||||
await entity.async_set_preset_mode(PRESET_ACTIVITY)
|
||||
|
||||
assert entity.hvac_mode is HVACMode.HEAT
|
||||
assert entity.preset_mode is PRESET_ACTIVITY
|
||||
# because no motion is detected yet
|
||||
assert entity.target_temperature == 18
|
||||
assert entity.motion_state is None
|
||||
assert entity.presence_state is None
|
||||
|
||||
event_timestamp = now - timedelta(minutes=5)
|
||||
await send_temperature_change_event(entity, 18, event_timestamp)
|
||||
await send_ext_temperature_change_event(entity, 10, event_timestamp)
|
||||
|
||||
await send_presence_change_event(entity, True, False, event_timestamp)
|
||||
assert entity.presence_state is "on"
|
||||
|
||||
# starts detecting motion with time not enough
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
|
||||
) as mock_send_event, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
|
||||
) as mock_heater_on, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
|
||||
) as mock_heater_off, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
|
||||
return_value=False,
|
||||
), patch(
|
||||
"homeassistant.helpers.condition.state", return_value=False
|
||||
) as mock_condition:
|
||||
event_timestamp = now - timedelta(minutes=4)
|
||||
try_condition = await send_motion_change_event(entity, True, False, event_timestamp)
|
||||
|
||||
# Will return False -> we will stay on movement False
|
||||
await try_condition(None)
|
||||
|
||||
assert entity.hvac_mode is HVACMode.HEAT
|
||||
assert entity.preset_mode is PRESET_ACTIVITY
|
||||
# because no motion is detected yet
|
||||
assert entity.target_temperature == 18
|
||||
# state is not changed if time is not enough
|
||||
assert entity.motion_state is None
|
||||
assert entity.presence_state is "on"
|
||||
|
||||
assert mock_send_event.call_count == 0
|
||||
# Change is not confirmed
|
||||
assert mock_heater_on.call_count == 0
|
||||
assert mock_heater_off.call_count == 0
|
||||
assert mock_send_event.call_count == 0
|
||||
|
||||
# starts detecting motion with time enough this time
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
|
||||
) as mock_send_event, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
|
||||
) as mock_heater_on, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
|
||||
) as mock_heater_off, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
|
||||
return_value=False,
|
||||
), 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.climate.VersatileThermostat.send_event"
|
||||
) as mock_send_event, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
|
||||
) as mock_heater_on, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
|
||||
) as mock_heater_off, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
|
||||
return_value=True,
|
||||
) 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.climate.VersatileThermostat.send_event"
|
||||
) as mock_send_event, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
|
||||
) as mock_heater_on, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
|
||||
) as mock_heater_off, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
|
||||
return_value=True,
|
||||
) as mock_device_active, patch(
|
||||
"homeassistant.helpers.condition.state", return_value=True
|
||||
) as mock_condition:
|
||||
event_timestamp = now - timedelta(minutes=1)
|
||||
try_condition = await send_motion_change_event(entity, False, True, event_timestamp)
|
||||
|
||||
# Will return True -> we will switch to movement Off
|
||||
await try_condition(None)
|
||||
|
||||
assert entity.hvac_mode is HVACMode.HEAT
|
||||
assert entity.preset_mode is PRESET_ACTIVITY
|
||||
# because no motion is detected yet
|
||||
assert entity.target_temperature == 18
|
||||
assert entity.motion_state is "off"
|
||||
assert entity.presence_state is "on"
|
||||
|
||||
assert mock_send_event.call_count == 0
|
||||
# The heater must stop heating now
|
||||
assert mock_heater_on.call_count == 0
|
||||
assert mock_heater_off.call_count == 1
|
||||
assert mock_send_event.call_count == 0
|
||||
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_movement_management_time_enough_and_presence(
|
||||
hass: HomeAssistant, skip_hass_states_is_state
|
||||
):
|
||||
"""Test the Presence management when time is not enough"""
|
||||
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
title="TheOverSwitchMockName",
|
||||
unique_id="uniqueId",
|
||||
data={
|
||||
CONF_NAME: "TheOverSwitchMockName",
|
||||
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_SWITCH,
|
||||
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
|
||||
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
|
||||
CONF_CYCLE_MIN: 5,
|
||||
CONF_TEMP_MIN: 15,
|
||||
CONF_TEMP_MAX: 30,
|
||||
"eco_temp": 17,
|
||||
"comfort_temp": 18,
|
||||
"boost_temp": 19,
|
||||
"eco_away_temp": 17,
|
||||
"comfort_away_temp": 18,
|
||||
"boost_away_temp": 19,
|
||||
CONF_USE_WINDOW_FEATURE: False,
|
||||
CONF_USE_MOTION_FEATURE: True,
|
||||
CONF_USE_POWER_FEATURE: False,
|
||||
CONF_USE_PRESENCE_FEATURE: True,
|
||||
CONF_HEATER: "switch.mock_switch",
|
||||
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
|
||||
CONF_TPI_COEF_INT: 0.3,
|
||||
CONF_TPI_COEF_EXT: 0.01,
|
||||
CONF_MINIMAL_ACTIVATION_DELAY: 30,
|
||||
CONF_SECURITY_DELAY_MIN: 5,
|
||||
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
|
||||
CONF_MOTION_SENSOR: "binary_sensor.mock_motion_sensor",
|
||||
CONF_MOTION_DELAY: 0, # important to not been obliged to wait
|
||||
CONF_MOTION_PRESET: "boost",
|
||||
CONF_NO_MOTION_PRESET: "comfort",
|
||||
CONF_PRESENCE_SENSOR: "binary_sensor.mock_presence_sensor",
|
||||
},
|
||||
)
|
||||
|
||||
entity: VersatileThermostat = 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.climate.VersatileThermostat._async_control_heating"
|
||||
):
|
||||
await entity.async_set_hvac_mode(HVACMode.HEAT)
|
||||
await entity.async_set_preset_mode(PRESET_ACTIVITY)
|
||||
|
||||
assert entity.hvac_mode is HVACMode.HEAT
|
||||
assert entity.preset_mode is PRESET_ACTIVITY
|
||||
# because no motion is detected yet
|
||||
assert entity.target_temperature == 18
|
||||
assert entity.motion_state is None
|
||||
assert entity.presence_state is None
|
||||
|
||||
event_timestamp = now - timedelta(minutes=4)
|
||||
await send_temperature_change_event(entity, 18, event_timestamp)
|
||||
await send_ext_temperature_change_event(entity, 10, event_timestamp)
|
||||
|
||||
await send_presence_change_event(entity, True, False, event_timestamp)
|
||||
assert entity.presence_state is "on"
|
||||
|
||||
# starts detecting motion
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
|
||||
) as mock_send_event, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
|
||||
) as mock_heater_on, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
|
||||
) as mock_heater_off, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
|
||||
return_value=False,
|
||||
), patch(
|
||||
"homeassistant.helpers.condition.state", return_value=True
|
||||
):
|
||||
event_timestamp = now - timedelta(minutes=3)
|
||||
await send_motion_change_event(entity, True, False, event_timestamp)
|
||||
|
||||
assert entity.hvac_mode is HVACMode.HEAT
|
||||
assert entity.preset_mode is PRESET_ACTIVITY
|
||||
# because motion is detected yet -> switch to Boost mode
|
||||
assert entity.target_temperature == 19
|
||||
assert entity.motion_state is "on"
|
||||
assert entity.presence_state is "on"
|
||||
|
||||
assert mock_send_event.call_count == 0
|
||||
# Change is confirmed. Heater should be started
|
||||
assert mock_heater_on.call_count == 1
|
||||
assert mock_heater_off.call_count == 0
|
||||
assert mock_send_event.call_count == 0
|
||||
|
||||
# stop detecting motion with confirmation of stop
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
|
||||
) as mock_send_event, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
|
||||
) as mock_heater_on, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
|
||||
) as mock_heater_off, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
|
||||
return_value=True,
|
||||
), patch(
|
||||
"homeassistant.helpers.condition.state", return_value=True
|
||||
):
|
||||
event_timestamp = now - timedelta(minutes=2)
|
||||
await send_motion_change_event(entity, False, True, event_timestamp)
|
||||
|
||||
assert entity.hvac_mode is HVACMode.HEAT
|
||||
assert entity.preset_mode is PRESET_ACTIVITY
|
||||
# because no motion is detected yet
|
||||
assert entity.target_temperature == 18
|
||||
assert entity.motion_state is "off"
|
||||
assert entity.presence_state is "on"
|
||||
|
||||
assert mock_send_event.call_count == 0
|
||||
assert mock_heater_on.call_count == 0
|
||||
# Because heating is no more necessary
|
||||
assert mock_heater_off.call_count == 1
|
||||
assert mock_send_event.call_count == 0
|
||||
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_movement_management_time_enoughand_not_presence(
|
||||
hass: HomeAssistant, skip_hass_states_is_state
|
||||
):
|
||||
"""Test the Presence management when time is not enough"""
|
||||
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
title="TheOverSwitchMockName",
|
||||
unique_id="uniqueId",
|
||||
data={
|
||||
CONF_NAME: "TheOverSwitchMockName",
|
||||
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_SWITCH,
|
||||
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
|
||||
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
|
||||
CONF_CYCLE_MIN: 5,
|
||||
CONF_TEMP_MIN: 15,
|
||||
CONF_TEMP_MAX: 30,
|
||||
"eco_temp": 17,
|
||||
"comfort_temp": 18,
|
||||
"boost_temp": 19,
|
||||
"eco_away_temp": 17.1,
|
||||
"comfort_away_temp": 18.1,
|
||||
"boost_away_temp": 19.1,
|
||||
CONF_USE_WINDOW_FEATURE: False,
|
||||
CONF_USE_MOTION_FEATURE: True,
|
||||
CONF_USE_POWER_FEATURE: False,
|
||||
CONF_USE_PRESENCE_FEATURE: True,
|
||||
CONF_HEATER: "switch.mock_switch",
|
||||
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
|
||||
CONF_TPI_COEF_INT: 0.3,
|
||||
CONF_TPI_COEF_EXT: 0.01,
|
||||
CONF_MINIMAL_ACTIVATION_DELAY: 30,
|
||||
CONF_SECURITY_DELAY_MIN: 5,
|
||||
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
|
||||
CONF_MOTION_SENSOR: "binary_sensor.mock_motion_sensor",
|
||||
CONF_MOTION_DELAY: 0, # important to not been obliged to wait
|
||||
CONF_MOTION_PRESET: "boost",
|
||||
CONF_NO_MOTION_PRESET: "comfort",
|
||||
CONF_PRESENCE_SENSOR: "binary_sensor.mock_presence_sensor",
|
||||
},
|
||||
)
|
||||
|
||||
entity: VersatileThermostat = 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.climate.VersatileThermostat._async_control_heating"
|
||||
):
|
||||
await entity.async_set_hvac_mode(HVACMode.HEAT)
|
||||
await entity.async_set_preset_mode(PRESET_ACTIVITY)
|
||||
|
||||
assert entity.hvac_mode is HVACMode.HEAT
|
||||
assert entity.preset_mode is PRESET_ACTIVITY
|
||||
# because no motion is detected yet and presence is unknown
|
||||
assert entity.target_temperature == 18
|
||||
assert entity.motion_state is None
|
||||
assert entity.presence_state is None
|
||||
|
||||
event_timestamp = now - timedelta(minutes=4)
|
||||
await send_temperature_change_event(entity, 18, event_timestamp)
|
||||
await send_ext_temperature_change_event(entity, 10, event_timestamp)
|
||||
|
||||
await send_presence_change_event(entity, False, True, event_timestamp)
|
||||
assert entity.presence_state is "off"
|
||||
|
||||
# starts detecting motion
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
|
||||
) as mock_send_event, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
|
||||
) as mock_heater_on, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
|
||||
) as mock_heater_off, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
|
||||
return_value=False,
|
||||
), patch(
|
||||
"homeassistant.helpers.condition.state", return_value=True
|
||||
):
|
||||
event_timestamp = now - timedelta(minutes=3)
|
||||
await send_motion_change_event(entity, True, False, event_timestamp)
|
||||
|
||||
assert entity.hvac_mode is HVACMode.HEAT
|
||||
assert entity.preset_mode is PRESET_ACTIVITY
|
||||
# because motion is detected yet -> switch to Boost away mode
|
||||
assert entity.target_temperature == 19.1
|
||||
assert entity.motion_state is "on"
|
||||
assert entity.presence_state is "off"
|
||||
|
||||
assert mock_send_event.call_count == 0
|
||||
# Change is confirmed. Heater should be started
|
||||
assert mock_heater_on.call_count == 1
|
||||
assert mock_heater_off.call_count == 0
|
||||
assert mock_send_event.call_count == 0
|
||||
|
||||
# stop detecting motion with confirmation of stop
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
|
||||
) as mock_send_event, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
|
||||
) as mock_heater_on, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
|
||||
) as mock_heater_off, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
|
||||
return_value=True,
|
||||
), patch(
|
||||
"homeassistant.helpers.condition.state", return_value=True
|
||||
):
|
||||
event_timestamp = now - timedelta(minutes=2)
|
||||
await send_motion_change_event(entity, False, True, event_timestamp)
|
||||
|
||||
assert entity.hvac_mode is HVACMode.HEAT
|
||||
assert entity.preset_mode is PRESET_ACTIVITY
|
||||
# because no motion is detected yet
|
||||
assert entity.target_temperature == 18.1
|
||||
assert entity.motion_state is "off"
|
||||
assert entity.presence_state is "off"
|
||||
|
||||
assert mock_send_event.call_count == 0
|
||||
# 18.1 starts heating with a low on_percent
|
||||
assert mock_heater_on.call_count == 1
|
||||
assert entity.proportional_algorithm.on_percent == 0.11
|
||||
assert mock_heater_off.call_count == 0
|
||||
assert mock_send_event.call_count == 0
|
||||
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_movement_management_with_stop_during_condition(
|
||||
hass: HomeAssistant, skip_hass_states_is_state
|
||||
):
|
||||
"""Test the Presence management when the movement sensor switch to off and then to on during the test condition"""
|
||||
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
title="TheOverSwitchMockName",
|
||||
unique_id="uniqueId",
|
||||
data={
|
||||
CONF_NAME: "TheOverSwitchMockName",
|
||||
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_SWITCH,
|
||||
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
|
||||
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
|
||||
CONF_CYCLE_MIN: 5,
|
||||
CONF_TEMP_MIN: 15,
|
||||
CONF_TEMP_MAX: 30,
|
||||
"eco_temp": 17,
|
||||
"comfort_temp": 18,
|
||||
"boost_temp": 19,
|
||||
"eco_away_temp": 17,
|
||||
"comfort_away_temp": 18,
|
||||
"boost_away_temp": 19,
|
||||
CONF_USE_WINDOW_FEATURE: False,
|
||||
CONF_USE_MOTION_FEATURE: True,
|
||||
CONF_USE_POWER_FEATURE: False,
|
||||
CONF_USE_PRESENCE_FEATURE: True,
|
||||
CONF_HEATER: "switch.mock_switch",
|
||||
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
|
||||
CONF_TPI_COEF_INT: 0.3,
|
||||
CONF_TPI_COEF_EXT: 0.01,
|
||||
CONF_MINIMAL_ACTIVATION_DELAY: 30,
|
||||
CONF_SECURITY_DELAY_MIN: 5,
|
||||
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
|
||||
CONF_MOTION_SENSOR: "binary_sensor.mock_motion_sensor",
|
||||
CONF_MOTION_DELAY: 10,
|
||||
CONF_MOTION_OFF_DELAY: 30,
|
||||
CONF_MOTION_PRESET: "boost",
|
||||
CONF_NO_MOTION_PRESET: "comfort",
|
||||
CONF_PRESENCE_SENSOR: "binary_sensor.mock_presence_sensor",
|
||||
},
|
||||
)
|
||||
|
||||
entity: VersatileThermostat = 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.climate.VersatileThermostat._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.climate.VersatileThermostat.send_event"
|
||||
) as mock_send_event, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
|
||||
) as mock_heater_on, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
|
||||
) as mock_heater_off, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
|
||||
return_value=True,
|
||||
), 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
|
||||
|
||||
590
tests/test_multiple_switch.py
Normal file
590
tests/test_multiple_switch.py
Normal file
@@ -0,0 +1,590 @@
|
||||
""" Test the Multiple switch management """
|
||||
import asyncio
|
||||
from unittest.mock import patch, call, ANY
|
||||
from datetime import datetime, timedelta
|
||||
import logging
|
||||
|
||||
from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import
|
||||
|
||||
logging.getLogger().setLevel(logging.DEBUG)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_one_switch_cycle(
|
||||
hass: HomeAssistant,
|
||||
skip_hass_states_is_state,
|
||||
skip_send_event,
|
||||
): # pylint: disable=unused-argument
|
||||
"""Test that when multiple switch are configured the activation is distributed"""
|
||||
|
||||
tz = get_tz(hass) # pylint: disable=invalid-name
|
||||
now: datetime = datetime.now(tz=tz)
|
||||
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
title="TheOver4SwitchMockName",
|
||||
unique_id="uniqueId",
|
||||
data={
|
||||
CONF_NAME: "TheOver4SwitchMockName",
|
||||
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_SWITCH,
|
||||
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
|
||||
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
|
||||
CONF_CYCLE_MIN: 8,
|
||||
CONF_TEMP_MIN: 15,
|
||||
CONF_TEMP_MAX: 30,
|
||||
"eco_temp": 17,
|
||||
"comfort_temp": 18,
|
||||
"boost_temp": 19,
|
||||
CONF_USE_WINDOW_FEATURE: False,
|
||||
CONF_USE_MOTION_FEATURE: False,
|
||||
CONF_USE_POWER_FEATURE: False,
|
||||
CONF_USE_PRESENCE_FEATURE: False,
|
||||
CONF_HEATER: "switch.mock_switch1",
|
||||
CONF_MINIMAL_ACTIVATION_DELAY: 30,
|
||||
CONF_SECURITY_DELAY_MIN: 5,
|
||||
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
|
||||
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
|
||||
CONF_TPI_COEF_INT: 0.3,
|
||||
CONF_TPI_COEF_EXT: 0.01,
|
||||
},
|
||||
)
|
||||
|
||||
entity: VersatileThermostat = await create_thermostat(
|
||||
hass, entry, "climate.theover4switchmockname"
|
||||
)
|
||||
assert entity
|
||||
assert entity.is_over_climate is False
|
||||
|
||||
# start heating, in boost mode. We block the control_heating to avoid running a cycle
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.climate.VersatileThermostat._async_control_heating"
|
||||
):
|
||||
await entity.async_set_hvac_mode(HVACMode.HEAT)
|
||||
await entity.async_set_preset_mode(PRESET_BOOST)
|
||||
|
||||
assert entity.hvac_mode is HVACMode.HEAT
|
||||
assert entity.preset_mode is PRESET_BOOST
|
||||
assert entity.target_temperature == 19
|
||||
assert entity.window_state is None
|
||||
|
||||
event_timestamp = now - timedelta(minutes=4)
|
||||
await send_temperature_change_event(entity, 15, event_timestamp)
|
||||
|
||||
# Checks that all heaters are off
|
||||
with patch(
|
||||
"homeassistant.core.StateMachine.is_state", return_value=False
|
||||
) as mock_is_state:
|
||||
assert entity._is_device_active is False # pylint: disable=protected-access
|
||||
|
||||
# Should be call for the Switch
|
||||
assert mock_is_state.call_count == 1
|
||||
|
||||
# Set temperature to a low level
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
|
||||
) as mock_send_event, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
|
||||
) as mock_heater_on, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
|
||||
) as mock_heater_off, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
|
||||
return_value=False,
|
||||
) as mock_device_active, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.call_later",
|
||||
return_value=None,
|
||||
) as mock_call_later:
|
||||
await send_ext_temperature_change_event(entity, 5, event_timestamp)
|
||||
|
||||
# No special event
|
||||
assert mock_send_event.call_count == 0
|
||||
assert mock_heater_off.call_count == 0
|
||||
|
||||
# The first heater should be on but because call_later is mocked heater_on is not called
|
||||
# assert mock_heater_on.call_count == 1
|
||||
assert mock_heater_on.call_count == 0
|
||||
# There is no check if active
|
||||
assert mock_device_active.call_count == 0
|
||||
|
||||
# 4 calls dispatched along the cycle
|
||||
assert mock_call_later.call_count == 1
|
||||
mock_call_later.assert_has_calls(
|
||||
[
|
||||
call.call_later(hass, 0.0, ANY),
|
||||
]
|
||||
)
|
||||
|
||||
# Set a temperature at middle level
|
||||
event_timestamp = now - timedelta(minutes=4)
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
|
||||
) as mock_send_event, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
|
||||
) as mock_heater_on, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
|
||||
) as mock_heater_off, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
|
||||
return_value=False,
|
||||
) as mock_device_active:
|
||||
await send_temperature_change_event(entity, 18, event_timestamp)
|
||||
|
||||
# No special event
|
||||
assert mock_send_event.call_count == 0
|
||||
assert mock_heater_off.call_count == 0
|
||||
|
||||
# The first heater should be turned on but is already on but because above we mock
|
||||
# call_later the heater is not on. But this time it will be really on
|
||||
assert mock_heater_on.call_count == 1
|
||||
|
||||
# Set another temperature at middle level
|
||||
event_timestamp = now - timedelta(minutes=3)
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
|
||||
) as mock_send_event, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
|
||||
) as mock_heater_on, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
|
||||
) as mock_heater_off, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
|
||||
return_value=True,
|
||||
) as mock_device_active:
|
||||
await send_temperature_change_event(entity, 18.1, event_timestamp)
|
||||
|
||||
# No special event
|
||||
assert mock_send_event.call_count == 0
|
||||
assert mock_heater_off.call_count == 0
|
||||
|
||||
# The heater is already on cycle. So we wait that the cycle ends and no heater action
|
||||
# is done
|
||||
assert mock_heater_on.call_count == 0
|
||||
# assert entity.underlying_entity(0)._should_relaunch_control_heating is True
|
||||
|
||||
# Simulate the relaunch
|
||||
await entity.underlying_entity(0)._turn_on_later( # pylint: disable=protected-access
|
||||
None
|
||||
)
|
||||
# wait restart
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
assert mock_heater_on.call_count == 1
|
||||
# normal ? assert entity.underlying_entity(0)._should_relaunch_control_heating is False
|
||||
|
||||
# Simulate the end of heater on cycle
|
||||
event_timestamp = now - timedelta(minutes=3)
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
|
||||
) as mock_send_event, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
|
||||
) as mock_heater_on, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
|
||||
) as mock_heater_off, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
|
||||
return_value=True,
|
||||
) as mock_device_active:
|
||||
await entity.underlying_entity(0)._turn_off_later( # pylint: disable=protected-access
|
||||
None
|
||||
)
|
||||
|
||||
# No special event
|
||||
assert mock_send_event.call_count == 0
|
||||
assert mock_heater_on.call_count == 0
|
||||
# The heater should be turned off this time
|
||||
assert mock_heater_off.call_count == 1
|
||||
# assert entity.underlying_entity(0)._should_relaunch_control_heating is False
|
||||
|
||||
# Simulate the start of heater on cycle
|
||||
event_timestamp = now - timedelta(minutes=3)
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
|
||||
) as mock_send_event, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
|
||||
) as mock_heater_on, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
|
||||
) as mock_heater_off, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
|
||||
return_value=True,
|
||||
) as mock_device_active:
|
||||
await entity.underlying_entity(0)._turn_on_later( # pylint: disable=protected-access
|
||||
None
|
||||
)
|
||||
|
||||
# No special event
|
||||
assert mock_send_event.call_count == 0
|
||||
assert mock_heater_on.call_count == 1
|
||||
# The heater should be turned off this time
|
||||
assert mock_heater_off.call_count == 0
|
||||
# assert entity.underlying_entity(0)._should_relaunch_control_heating is False
|
||||
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_multiple_switchs(
|
||||
hass: HomeAssistant,
|
||||
skip_hass_states_is_state,
|
||||
skip_send_event,
|
||||
): # pylint: disable=unused-argument
|
||||
"""Test that when multiple switch are configured the activation is distributed"""
|
||||
|
||||
tz = get_tz(hass) # pylint: disable=invalid-name
|
||||
now: datetime = datetime.now(tz=tz)
|
||||
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
title="TheOver4SwitchMockName",
|
||||
unique_id="uniqueId",
|
||||
data={
|
||||
CONF_NAME: "TheOver4SwitchMockName",
|
||||
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_SWITCH,
|
||||
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
|
||||
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
|
||||
CONF_CYCLE_MIN: 8,
|
||||
CONF_TEMP_MIN: 15,
|
||||
CONF_TEMP_MAX: 30,
|
||||
"eco_temp": 17,
|
||||
"comfort_temp": 18,
|
||||
"boost_temp": 19,
|
||||
CONF_USE_WINDOW_FEATURE: False,
|
||||
CONF_USE_MOTION_FEATURE: False,
|
||||
CONF_USE_POWER_FEATURE: False,
|
||||
CONF_USE_PRESENCE_FEATURE: False,
|
||||
CONF_HEATER: "switch.mock_switch1",
|
||||
CONF_HEATER_2: "switch.mock_switch2",
|
||||
CONF_HEATER_3: "switch.mock_switch3",
|
||||
CONF_HEATER_4: "switch.mock_switch4",
|
||||
CONF_MINIMAL_ACTIVATION_DELAY: 30,
|
||||
CONF_SECURITY_DELAY_MIN: 5,
|
||||
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
|
||||
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
|
||||
CONF_TPI_COEF_INT: 0.3,
|
||||
CONF_TPI_COEF_EXT: 0.01,
|
||||
},
|
||||
)
|
||||
|
||||
entity: VersatileThermostat = 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"
|
||||
), patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.set_hvac_mode"
|
||||
) as mock_underlying_set_hvac_mode:
|
||||
await entity.async_set_hvac_mode(HVACMode.HEAT)
|
||||
await entity.async_set_preset_mode(PRESET_BOOST)
|
||||
|
||||
assert entity.hvac_mode is HVACMode.HEAT
|
||||
assert entity.preset_mode is PRESET_BOOST
|
||||
assert entity.target_temperature == 19
|
||||
assert entity.window_state is None
|
||||
|
||||
event_timestamp = now - timedelta(minutes=4)
|
||||
await send_temperature_change_event(entity, 15, event_timestamp)
|
||||
|
||||
# Checks that all climates are off
|
||||
assert entity._is_device_active is False # pylint: disable=protected-access
|
||||
|
||||
# Should be call for all Switch
|
||||
assert mock_underlying_set_hvac_mode.call_count == 4
|
||||
mock_underlying_set_hvac_mode.assert_has_calls(
|
||||
[
|
||||
call.set_hvac_mode(HVACMode.HEAT),
|
||||
]
|
||||
)
|
||||
|
||||
# Set temperature to a low level
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
|
||||
) as mock_send_event, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
|
||||
) as mock_heater_on, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
|
||||
) as mock_heater_off, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
|
||||
return_value=False,
|
||||
) as mock_device_active, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.call_later",
|
||||
return_value=None,
|
||||
) as mock_call_later:
|
||||
await send_ext_temperature_change_event(entity, 5, event_timestamp)
|
||||
|
||||
# No special event
|
||||
assert mock_send_event.call_count == 0
|
||||
assert mock_heater_off.call_count == 0
|
||||
|
||||
# The first heater should be on but because call_later is mocked heater_on is not called
|
||||
# assert mock_heater_on.call_count == 1
|
||||
assert mock_heater_on.call_count == 0
|
||||
# There is no check if active
|
||||
assert mock_device_active.call_count == 0
|
||||
|
||||
# 4 calls dispatched along the cycle
|
||||
assert mock_call_later.call_count == 4
|
||||
mock_call_later.assert_has_calls(
|
||||
[
|
||||
call.call_later(hass, 0.0, ANY),
|
||||
call.call_later(hass, 120.0, ANY),
|
||||
call.call_later(hass, 240.0, ANY),
|
||||
call.call_later(hass, 360.0, ANY),
|
||||
]
|
||||
)
|
||||
|
||||
# Set a temperature at middle level
|
||||
event_timestamp = now - timedelta(minutes=4)
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
|
||||
) as mock_send_event, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
|
||||
) as mock_heater_on, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
|
||||
) as mock_heater_off, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
|
||||
return_value=False,
|
||||
) as mock_device_active:
|
||||
await send_temperature_change_event(entity, 18, event_timestamp)
|
||||
|
||||
# No special event
|
||||
assert mock_send_event.call_count == 0
|
||||
assert mock_heater_off.call_count == 0
|
||||
|
||||
# The first heater should be turned on but is already on but because call_later
|
||||
# is mocked, it is only turned on here
|
||||
assert mock_heater_on.call_count == 1
|
||||
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_multiple_climates(
|
||||
hass: HomeAssistant,
|
||||
skip_hass_states_is_state,
|
||||
skip_send_event,
|
||||
): # pylint: disable=unused-argument
|
||||
"""Test that when multiple climates are configured the activation and deactivation
|
||||
is propagated to all climates"""
|
||||
|
||||
tz = get_tz(hass) # pylint: disable=invalid-name
|
||||
now: datetime = datetime.now(tz=tz)
|
||||
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
title="TheOver4ClimateMockName",
|
||||
unique_id="uniqueId",
|
||||
data={
|
||||
CONF_NAME: "TheOver4ClimateMockName",
|
||||
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_CLIMATE,
|
||||
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
|
||||
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
|
||||
CONF_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: VersatileThermostat = 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.climate.VersatileThermostat._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.climate.VersatileThermostat._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: VersatileThermostat = 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.climate.VersatileThermostat._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.climate.VersatileThermostat._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.climate.VersatileThermostat._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
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import
|
||||
from ..open_window_algorithm import WindowOpenDetectionAlgorithm
|
||||
from custom_components.versatile_thermostat.open_window_algorithm import WindowOpenDetectionAlgorithm
|
||||
|
||||
|
||||
async def test_open_window_algo(
|
||||
@@ -10,6 +10,8 @@ import logging
|
||||
logging.getLogger().setLevel(logging.DEBUG)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_power_management_hvac_off(
|
||||
hass: HomeAssistant, skip_hass_states_is_state
|
||||
):
|
||||
@@ -81,9 +83,9 @@ async def test_power_management_hvac_off(
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
|
||||
) as mock_send_event, patch(
|
||||
"custom_components.versatile_thermostat.climate.VersatileThermostat._async_heater_turn_on"
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
|
||||
) as mock_heater_on, patch(
|
||||
"custom_components.versatile_thermostat.climate.VersatileThermostat._async_underlying_entity_turn_off"
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
|
||||
) as mock_heater_off:
|
||||
await send_max_power_change_event(entity, 149, datetime.now())
|
||||
assert await entity.check_overpowering() is True
|
||||
@@ -96,6 +98,8 @@ async def test_power_management_hvac_off(
|
||||
assert mock_heater_off.call_count == 0
|
||||
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_power_management_hvac_on(hass: HomeAssistant, skip_hass_states_is_state):
|
||||
"""Test the Power management"""
|
||||
|
||||
@@ -160,9 +164,9 @@ async def test_power_management_hvac_on(hass: HomeAssistant, skip_hass_states_is
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
|
||||
) as mock_send_event, patch(
|
||||
"custom_components.versatile_thermostat.climate.VersatileThermostat._async_heater_turn_on"
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
|
||||
) as mock_heater_on, patch(
|
||||
"custom_components.versatile_thermostat.climate.VersatileThermostat._async_underlying_entity_turn_off"
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
|
||||
) as mock_heater_off:
|
||||
await send_max_power_change_event(entity, 149, datetime.now())
|
||||
assert await entity.check_overpowering() is True
|
||||
@@ -194,9 +198,9 @@ async def test_power_management_hvac_on(hass: HomeAssistant, skip_hass_states_is
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
|
||||
) as mock_send_event, patch(
|
||||
"custom_components.versatile_thermostat.climate.VersatileThermostat._async_heater_turn_on"
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
|
||||
) as mock_heater_on, patch(
|
||||
"custom_components.versatile_thermostat.climate.VersatileThermostat._async_underlying_entity_turn_off"
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
|
||||
) as mock_heater_off:
|
||||
await send_power_change_event(entity, 48, datetime.now())
|
||||
assert await entity.check_overpowering() is False
|
||||
@@ -226,6 +230,8 @@ async def test_power_management_hvac_on(hass: HomeAssistant, skip_hass_states_is
|
||||
assert mock_heater_off.call_count == 0
|
||||
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_power_management_energy_over_switch(
|
||||
hass: HomeAssistant, skip_hass_states_is_state
|
||||
):
|
||||
@@ -278,13 +284,13 @@ async def test_power_management_energy_over_switch(
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
|
||||
) as mock_send_event, patch(
|
||||
"custom_components.versatile_thermostat.climate.VersatileThermostat._async_heater_turn_on"
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
|
||||
) as mock_heater_on, patch(
|
||||
"custom_components.versatile_thermostat.climate.VersatileThermostat._async_underlying_entity_turn_off"
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
|
||||
) as mock_heater_off:
|
||||
await send_temperature_change_event(entity, 15, datetime.now())
|
||||
await entity.async_set_hvac_mode(HVACMode.HEAT)
|
||||
await entity.async_set_preset_mode(PRESET_BOOST)
|
||||
await send_temperature_change_event(entity, 15, datetime.now())
|
||||
|
||||
assert entity.hvac_mode is HVACMode.HEAT
|
||||
assert entity.preset_mode is PRESET_BOOST
|
||||
@@ -307,9 +313,9 @@ async def test_power_management_energy_over_switch(
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
|
||||
) as mock_send_event, patch(
|
||||
"custom_components.versatile_thermostat.climate.VersatileThermostat._async_heater_turn_on"
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
|
||||
) as mock_heater_on, patch(
|
||||
"custom_components.versatile_thermostat.climate.VersatileThermostat._async_underlying_entity_turn_off"
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
|
||||
) as mock_heater_off:
|
||||
await send_temperature_change_event(entity, 18, datetime.now())
|
||||
assert tpi_algo.on_percent == 0.3
|
||||
@@ -329,9 +335,9 @@ async def test_power_management_energy_over_switch(
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
|
||||
) as mock_send_event, patch(
|
||||
"custom_components.versatile_thermostat.climate.VersatileThermostat._async_heater_turn_on"
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
|
||||
) as mock_heater_on, patch(
|
||||
"custom_components.versatile_thermostat.climate.VersatileThermostat._async_underlying_entity_turn_off"
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
|
||||
) as mock_heater_off:
|
||||
await send_temperature_change_event(entity, 20, datetime.now())
|
||||
assert tpi_algo.on_percent == 0.0
|
||||
@@ -350,6 +356,8 @@ async def test_power_management_energy_over_switch(
|
||||
assert round(entity.total_energy, 2) == round((2.0 + 0.6) * 100 * 5 / 60.0, 2)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_power_management_energy_over_climate(
|
||||
hass: HomeAssistant, skip_hass_states_is_state
|
||||
):
|
||||
@@ -357,7 +365,7 @@ async def test_power_management_energy_over_climate(
|
||||
|
||||
the_mock_underlying = MagicMockClimate()
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.climate.VersatileThermostat.find_underlying_climate",
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
|
||||
return_value=the_mock_underlying,
|
||||
):
|
||||
entry = MockConfigEntry(
|
||||
@@ -9,6 +9,8 @@ import logging
|
||||
logging.getLogger().setLevel(logging.DEBUG)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_security_feature(hass: HomeAssistant, skip_hass_states_is_state):
|
||||
"""Test the security feature and https://github.com/jmcollin78/versatile_thermostat/issues/49:
|
||||
1. creates a thermostat and check that security is off
|
||||
@@ -87,7 +89,7 @@ async def test_security_feature(hass: HomeAssistant, skip_hass_states_is_state):
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
|
||||
) as mock_send_event, patch(
|
||||
"custom_components.versatile_thermostat.climate.VersatileThermostat._async_heater_turn_on"
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
|
||||
) as mock_heater_on:
|
||||
event_timestamp = now - timedelta(minutes=6)
|
||||
|
||||
@@ -134,7 +136,7 @@ async def test_security_feature(hass: HomeAssistant, skip_hass_states_is_state):
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
|
||||
) as mock_send_event, patch(
|
||||
"custom_components.versatile_thermostat.climate.VersatileThermostat._async_heater_turn_on"
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
|
||||
) as mock_heater_on:
|
||||
await entity.async_set_preset_mode(PRESET_BOOST)
|
||||
|
||||
@@ -149,7 +151,7 @@ async def test_security_feature(hass: HomeAssistant, skip_hass_states_is_state):
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
|
||||
) as mock_send_event, patch(
|
||||
"custom_components.versatile_thermostat.climate.VersatileThermostat._async_heater_turn_on"
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
|
||||
) as mock_heater_on:
|
||||
event_timestamp = datetime.now()
|
||||
|
||||
@@ -185,4 +187,119 @@ async def test_security_feature(hass: HomeAssistant, skip_hass_states_is_state):
|
||||
any_order=True,
|
||||
)
|
||||
|
||||
assert mock_heater_on.call_count == 0
|
||||
# Heater is now on
|
||||
assert mock_heater_on.call_count == 1
|
||||
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_security_over_climate(
|
||||
hass: HomeAssistant,
|
||||
skip_hass_states_is_state,
|
||||
skip_turn_on_off_heater,
|
||||
skip_send_event,
|
||||
):
|
||||
"""Test that when a underlying climate is not available the VTherm doesn't go into security mode"""
|
||||
|
||||
tz = get_tz(hass) # pylint: disable=invalid-name
|
||||
now: datetime = datetime.now(tz=tz)
|
||||
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
title="TheOverClimateMockName",
|
||||
unique_id="uniqueId",
|
||||
data=PARTIAL_CLIMATE_CONFIG, # 5 minutes security delay
|
||||
)
|
||||
|
||||
fake_underlying_climate = MockClimate(hass, "mockUniqueId", "MockClimateName", {}, HVACMode.HEAT)
|
||||
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
|
||||
) as mock_send_event, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
|
||||
return_value=fake_underlying_climate,
|
||||
) as mock_find_climate:
|
||||
entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(entry.entry_id)
|
||||
assert entry.state is ConfigEntryState.LOADED
|
||||
|
||||
def find_my_entity(entity_id) -> ClimateEntity:
|
||||
"""Find my new entity"""
|
||||
component: EntityComponent[ClimateEntity] = hass.data[CLIMATE_DOMAIN]
|
||||
for entity in component.entities:
|
||||
if entity.entity_id == entity_id:
|
||||
return entity
|
||||
|
||||
entity = find_my_entity("climate.theoverclimatemockname")
|
||||
|
||||
assert entity
|
||||
|
||||
assert entity.name == "TheOverClimateMockName"
|
||||
assert entity._is_over_climate is True
|
||||
|
||||
# Because the underlying is HEATING. In real life the underlying will be shut-off
|
||||
assert entity.hvac_action is HVACAction.HEATING
|
||||
assert entity.hvac_mode is HVACMode.OFF
|
||||
assert entity.target_temperature == entity.min_temp
|
||||
assert entity.preset_modes == [
|
||||
PRESET_NONE,
|
||||
PRESET_ECO,
|
||||
PRESET_COMFORT,
|
||||
PRESET_BOOST,
|
||||
]
|
||||
assert entity.preset_mode is PRESET_NONE
|
||||
assert entity._security_state is False
|
||||
|
||||
# should have been called with EventType.PRESET_EVENT and EventType.HVAC_MODE_EVENT
|
||||
assert mock_send_event.call_count == 2
|
||||
mock_send_event.assert_has_calls(
|
||||
[
|
||||
# At startup
|
||||
call.send_event(EventType.PRESET_EVENT, {"preset": PRESET_NONE}),
|
||||
call.send_event(
|
||||
EventType.HVAC_MODE_EVENT,
|
||||
{"hvac_mode": HVACMode.OFF},
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
assert mock_find_climate.call_count == 1
|
||||
assert mock_find_climate.mock_calls[0] == call()
|
||||
mock_find_climate.assert_has_calls([call.find_underlying_entity()])
|
||||
|
||||
# Force security mode
|
||||
assert entity._last_ext_temperature_mesure is not None
|
||||
assert entity._last_temperature_mesure is not None
|
||||
assert (entity._last_temperature_mesure.astimezone(tz) - now).total_seconds() < 1
|
||||
assert (
|
||||
entity._last_ext_temperature_mesure.astimezone(tz) - now
|
||||
).total_seconds() < 1
|
||||
|
||||
# Tries to turns on the Thermostat
|
||||
await entity.async_set_hvac_mode(HVACMode.HEAT)
|
||||
assert entity.hvac_mode == HVACMode.HEAT
|
||||
|
||||
# One call more
|
||||
assert mock_send_event.call_count == 3
|
||||
mock_send_event.assert_has_calls(
|
||||
[
|
||||
call.send_event(
|
||||
EventType.HVAC_MODE_EVENT,
|
||||
{"hvac_mode": HVACMode.HEAT},
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
# 2. activate security feature when date is expired
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
|
||||
) as mock_send_event, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
|
||||
) as mock_heater_on:
|
||||
event_timestamp = now - timedelta(minutes=6)
|
||||
|
||||
await send_temperature_change_event(entity, 15, event_timestamp)
|
||||
# Should stay False because a climate is never in security mode
|
||||
assert entity.security_state is False
|
||||
assert entity.preset_mode == 'none'
|
||||
assert entity._saved_preset_mode == 'none'
|
||||
@@ -12,8 +12,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.climate import VersatileThermostat
|
||||
from custom_components.versatile_thermostat.sensor import (
|
||||
EnergySensor,
|
||||
MeanPowerSensor,
|
||||
OnPercentSensor,
|
||||
@@ -26,6 +26,8 @@ from ..sensor import (
|
||||
from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import
|
||||
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_sensors_over_switch(
|
||||
hass: HomeAssistant,
|
||||
skip_hass_states_is_state,
|
||||
@@ -179,7 +181,11 @@ async def test_sensors_over_switch(
|
||||
)
|
||||
assert last_ext_temperature_sensor.device_class == SensorDeviceClass.TIMESTAMP
|
||||
|
||||
cancel_switchs_cycles(entity)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_sensors_over_climate(
|
||||
hass: HomeAssistant,
|
||||
skip_hass_states_is_state,
|
||||
@@ -190,7 +196,7 @@ async def test_sensors_over_climate(
|
||||
|
||||
the_mock_underlying = MagicMockClimate()
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.climate.VersatileThermostat.find_underlying_climate",
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
|
||||
return_value=the_mock_underlying,
|
||||
):
|
||||
entry = MockConfigEntry(
|
||||
@@ -314,6 +320,8 @@ async def test_sensors_over_climate(
|
||||
assert last_ext_temperature_sensor.device_class == SensorDeviceClass.TIMESTAMP
|
||||
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_sensors_over_climate_minimal(
|
||||
hass: HomeAssistant,
|
||||
skip_hass_states_is_state,
|
||||
@@ -324,7 +332,7 @@ async def test_sensors_over_climate_minimal(
|
||||
|
||||
the_mock_underlying = MagicMockClimate()
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.climate.VersatileThermostat.find_underlying_climate",
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
|
||||
return_value=the_mock_underlying,
|
||||
):
|
||||
entry = MockConfigEntry(
|
||||
@@ -10,11 +10,13 @@ from homeassistant.components.climate import ClimateEntity, DOMAIN as CLIMATE_DO
|
||||
|
||||
from pytest_homeassistant_custom_component.common import MockConfigEntry
|
||||
|
||||
from ..climate import VersatileThermostat
|
||||
from custom_components.versatile_thermostat.climate import VersatileThermostat
|
||||
|
||||
from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import
|
||||
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_over_switch_full_start(hass: HomeAssistant, skip_hass_states_is_state):
|
||||
"""Test the normal full start of a thermostat in thermostat_over_switch type"""
|
||||
|
||||
@@ -76,6 +78,8 @@ async def test_over_switch_full_start(hass: HomeAssistant, skip_hass_states_is_s
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_over_climate_full_start(hass: HomeAssistant, skip_hass_states_is_state):
|
||||
"""Test the normal full start of a thermostat in thermostat_over_climate type"""
|
||||
|
||||
@@ -91,7 +95,7 @@ async def test_over_climate_full_start(hass: HomeAssistant, skip_hass_states_is_
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
|
||||
) as mock_send_event, patch(
|
||||
"custom_components.versatile_thermostat.climate.VersatileThermostat.find_underlying_climate",
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
|
||||
return_value=fake_underlying_climate,
|
||||
) as mock_find_climate:
|
||||
entry.add_to_hass(hass)
|
||||
@@ -139,7 +143,78 @@ async def test_over_climate_full_start(hass: HomeAssistant, skip_hass_states_is_
|
||||
)
|
||||
|
||||
assert mock_find_climate.call_count == 1
|
||||
assert mock_find_climate.mock_calls[0] == call("climate.mock_climate")
|
||||
mock_find_climate.assert_has_calls(
|
||||
[call.find_underlying_entity("climate.mock_climate")]
|
||||
assert mock_find_climate.mock_calls[0] == call()
|
||||
mock_find_climate.assert_has_calls([call.find_underlying_entity()])
|
||||
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_over_4switch_full_start(hass: HomeAssistant, skip_hass_states_is_state):
|
||||
"""Test the normal full start of a thermostat in thermostat_over_switch with 4 switches type"""
|
||||
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
title="TheOver4SwitchMockName",
|
||||
unique_id="uniqueId",
|
||||
data=FULL_4SWITCH_CONFIG,
|
||||
)
|
||||
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
|
||||
) as mock_send_event:
|
||||
entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(entry.entry_id)
|
||||
assert entry.state is ConfigEntryState.LOADED
|
||||
|
||||
def find_my_entity(entity_id) -> ClimateEntity:
|
||||
"""Find my new entity"""
|
||||
component: EntityComponent[ClimateEntity] = hass.data[CLIMATE_DOMAIN]
|
||||
for entity in component.entities:
|
||||
if entity.entity_id == entity_id:
|
||||
return entity
|
||||
|
||||
entity: VersatileThermostat = find_my_entity("climate.theover4switchmockname")
|
||||
|
||||
assert entity
|
||||
|
||||
assert entity.name == "TheOver4SwitchMockName"
|
||||
assert entity._is_over_climate is False
|
||||
assert entity.hvac_action is HVACAction.OFF
|
||||
assert entity.hvac_mode is HVACMode.OFF
|
||||
assert entity.target_temperature == entity.min_temp
|
||||
assert entity.preset_modes == [
|
||||
PRESET_NONE,
|
||||
PRESET_ECO,
|
||||
PRESET_COMFORT,
|
||||
PRESET_BOOST,
|
||||
PRESET_ACTIVITY,
|
||||
]
|
||||
assert entity.preset_mode is PRESET_NONE
|
||||
assert entity._security_state is False
|
||||
assert entity._window_state is None
|
||||
assert entity._motion_state is None
|
||||
assert entity._presence_state is None
|
||||
assert entity._prop_algorithm is not None
|
||||
|
||||
assert entity.nb_underlying_entities == 4
|
||||
|
||||
# Checks that we have the 4 UnderlyingEntity correctly configured
|
||||
for idx in range(4):
|
||||
under = entity.underlying_entity(idx)
|
||||
assert under is not None
|
||||
assert isinstance(under, UnderlyingSwitch)
|
||||
assert under.entity_id == "switch.mock_4switch" + str(idx)
|
||||
assert under.initial_delay_sec == 8 * 60 / 4 * idx
|
||||
|
||||
# should have been called with EventType.PRESET_EVENT and EventType.HVAC_MODE_EVENT
|
||||
assert mock_send_event.call_count == 2
|
||||
|
||||
mock_send_event.assert_has_calls(
|
||||
[
|
||||
call.send_event(EventType.PRESET_EVENT, {"preset": PRESET_NONE}),
|
||||
call.send_event(
|
||||
EventType.HVAC_MODE_EVENT,
|
||||
{"hvac_mode": HVACMode.OFF},
|
||||
),
|
||||
]
|
||||
)
|
||||
140
tests/test_switch_ac.py
Normal file
140
tests/test_switch_ac.py
Normal 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.climate import VersatileThermostat
|
||||
|
||||
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.climate.VersatileThermostat.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: VersatileThermostat = find_my_entity("climate.theoverswitchmockname")
|
||||
|
||||
assert entity
|
||||
|
||||
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
|
||||
|
||||
|
||||
@@ -3,7 +3,11 @@
|
||||
from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import
|
||||
|
||||
|
||||
async def test_tpi_calculation(hass: HomeAssistant, skip_hass_states_is_state):
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_tpi_calculation(
|
||||
hass: HomeAssistant, skip_hass_states_is_state: None
|
||||
): # pylint: disable=unused-argument
|
||||
"""Test the TPI calculation"""
|
||||
|
||||
entry = MockConfigEntry(
|
||||
@@ -38,7 +42,7 @@ async def test_tpi_calculation(hass: HomeAssistant, skip_hass_states_is_state):
|
||||
)
|
||||
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)
|
||||
@@ -48,36 +52,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
|
||||
@@ -9,6 +9,8 @@ import logging
|
||||
logging.getLogger().setLevel(logging.DEBUG)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_window_management_time_not_enough(
|
||||
hass: HomeAssistant, skip_hass_states_is_state
|
||||
):
|
||||
@@ -66,18 +68,16 @@ async def test_window_management_time_not_enough(
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
|
||||
) as mock_send_event, patch(
|
||||
"custom_components.versatile_thermostat.climate.VersatileThermostat._async_heater_turn_on"
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
|
||||
) as mock_heater_on, patch(
|
||||
"custom_components.versatile_thermostat.climate.VersatileThermostat._async_underlying_entity_turn_off"
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
|
||||
) as mock_heater_off, patch(
|
||||
"homeassistant.helpers.condition.state", return_value=False
|
||||
) as mock_condition:
|
||||
await send_temperature_change_event(entity, 15, datetime.now())
|
||||
try_window_condition = await send_window_change_event(
|
||||
entity, True, False, datetime.now()
|
||||
)
|
||||
# simulate the call to try_window_condition
|
||||
await try_window_condition(None)
|
||||
await send_window_change_event(entity, True, False, datetime.now())
|
||||
# simulate the call to try_window_condition. No need due to 0 WINDOW_DELAY and sleep after event is sent
|
||||
# await try_window_condition(None)
|
||||
|
||||
assert mock_send_event.call_count == 0
|
||||
assert mock_heater_on.call_count == 1
|
||||
@@ -94,7 +94,11 @@ async def test_window_management_time_not_enough(
|
||||
await try_window_condition(None)
|
||||
assert entity.window_state == STATE_OFF
|
||||
|
||||
entity.remove_thermostat()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_window_management_time_enough(
|
||||
hass: HomeAssistant, skip_hass_states_is_state
|
||||
):
|
||||
@@ -148,49 +152,79 @@ async def test_window_management_time_enough(
|
||||
|
||||
assert entity.window_state is None
|
||||
|
||||
# Open the window, but condition of time is not satisfied and check the thermostat don't turns off
|
||||
# change temperature to force turning on the heater
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
|
||||
) as mock_send_event, patch(
|
||||
"custom_components.versatile_thermostat.climate.VersatileThermostat._async_heater_turn_on"
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
|
||||
) as mock_heater_on, patch(
|
||||
"custom_components.versatile_thermostat.climate.VersatileThermostat._async_underlying_entity_turn_off"
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
|
||||
) as mock_heater_off, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
|
||||
return_value=False,
|
||||
):
|
||||
await send_temperature_change_event(entity, 15, datetime.now())
|
||||
|
||||
# Heater shoud turn-on
|
||||
assert mock_heater_on.call_count >= 1
|
||||
assert mock_heater_off.call_count == 0
|
||||
assert mock_send_event.call_count == 0
|
||||
|
||||
# Open the window, condition of time is satisfied, check the thermostat and heater turns off
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
|
||||
) as mock_send_event, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
|
||||
) as mock_heater_on, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
|
||||
) as mock_heater_off, patch(
|
||||
"homeassistant.helpers.condition.state", return_value=True
|
||||
) as mock_condition, patch(
|
||||
"custom_components.versatile_thermostat.climate.VersatileThermostat._is_device_active",
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
|
||||
return_value=True,
|
||||
):
|
||||
await send_temperature_change_event(entity, 15, datetime.now())
|
||||
try_window_condition = await send_window_change_event(
|
||||
entity, True, False, datetime.now()
|
||||
)
|
||||
# simulate the call to try_window_condition
|
||||
await try_window_condition(None)
|
||||
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})]
|
||||
)
|
||||
assert mock_heater_on.call_count == 1
|
||||
# One call in turn_oiff and one call in the control_heating
|
||||
|
||||
# Heater should not be on
|
||||
assert mock_heater_on.call_count == 0
|
||||
# One call in set_hvac_mode turn_off and one call in the control_heating for security
|
||||
assert mock_heater_off.call_count == 2
|
||||
assert mock_condition.call_count == 1
|
||||
|
||||
assert entity.hvac_mode is HVACMode.OFF
|
||||
assert entity.window_state == STATE_ON
|
||||
|
||||
# Close the window
|
||||
try_window_condition = await send_window_change_event(
|
||||
entity, False, True, datetime.now()
|
||||
# Close the window
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
|
||||
) as mock_send_event, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
|
||||
) as mock_heater_on, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
|
||||
) as mock_heater_off, patch(
|
||||
"homeassistant.helpers.condition.state", return_value=True
|
||||
) as mock_condition, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
|
||||
return_value=False,
|
||||
):
|
||||
try_function = await send_window_change_event(
|
||||
entity, False, True, datetime.now(), sleep=False
|
||||
)
|
||||
# simulate the call to try_window_condition
|
||||
await try_window_condition(None)
|
||||
|
||||
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 == 2
|
||||
assert mock_send_event.call_count == 2
|
||||
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.OFF}),
|
||||
call.send_event(
|
||||
EventType.HVAC_MODE_EVENT, {"hvac_mode": HVACMode.HEAT}
|
||||
),
|
||||
@@ -199,7 +233,12 @@ async def test_window_management_time_enough(
|
||||
)
|
||||
assert entity.preset_mode is PRESET_BOOST
|
||||
|
||||
# Clean the entity
|
||||
entity.remove_thermostat()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_window_auto_fast(hass: HomeAssistant, skip_hass_states_is_state):
|
||||
"""Test the Power management"""
|
||||
|
||||
@@ -259,11 +298,11 @@ async def test_window_auto_fast(hass: HomeAssistant, skip_hass_states_is_state):
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
|
||||
) as mock_send_event, patch(
|
||||
"custom_components.versatile_thermostat.climate.VersatileThermostat._async_heater_turn_on"
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
|
||||
) as mock_heater_on, patch(
|
||||
"custom_components.versatile_thermostat.climate.VersatileThermostat._async_underlying_entity_turn_off"
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
|
||||
) as mock_heater_off, patch(
|
||||
"custom_components.versatile_thermostat.climate.VersatileThermostat._is_device_active",
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
|
||||
return_value=True,
|
||||
):
|
||||
event_timestamp = now - timedelta(minutes=4)
|
||||
@@ -281,11 +320,11 @@ async def test_window_auto_fast(hass: HomeAssistant, skip_hass_states_is_state):
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
|
||||
) as mock_send_event, patch(
|
||||
"custom_components.versatile_thermostat.climate.VersatileThermostat._async_heater_turn_on"
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
|
||||
) as mock_heater_on, patch(
|
||||
"custom_components.versatile_thermostat.climate.VersatileThermostat._async_underlying_entity_turn_off"
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
|
||||
) as mock_heater_off, patch(
|
||||
"custom_components.versatile_thermostat.climate.VersatileThermostat._is_device_active",
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
|
||||
return_value=True,
|
||||
):
|
||||
event_timestamp = now - timedelta(minutes=3)
|
||||
@@ -316,11 +355,11 @@ async def test_window_auto_fast(hass: HomeAssistant, skip_hass_states_is_state):
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
|
||||
) as mock_send_event, patch(
|
||||
"custom_components.versatile_thermostat.climate.VersatileThermostat._async_heater_turn_on"
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
|
||||
) as mock_heater_on, patch(
|
||||
"custom_components.versatile_thermostat.climate.VersatileThermostat._async_underlying_entity_turn_off"
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
|
||||
) as mock_heater_off, patch(
|
||||
"custom_components.versatile_thermostat.climate.VersatileThermostat._is_device_active",
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
|
||||
new_callable=PropertyMock,
|
||||
return_value=False,
|
||||
):
|
||||
@@ -341,11 +380,11 @@ async def test_window_auto_fast(hass: HomeAssistant, skip_hass_states_is_state):
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
|
||||
) as mock_send_event, patch(
|
||||
"custom_components.versatile_thermostat.climate.VersatileThermostat._async_heater_turn_on"
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
|
||||
) as mock_heater_on, patch(
|
||||
"custom_components.versatile_thermostat.climate.VersatileThermostat._async_underlying_entity_turn_off"
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
|
||||
) as mock_heater_off, patch(
|
||||
"custom_components.versatile_thermostat.climate.VersatileThermostat._is_device_active",
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
|
||||
new_callable=PropertyMock,
|
||||
return_value=False,
|
||||
):
|
||||
@@ -378,7 +417,12 @@ async def test_window_auto_fast(hass: HomeAssistant, skip_hass_states_is_state):
|
||||
assert entity.window_auto_state == STATE_OFF
|
||||
assert entity.hvac_mode is HVACMode.HEAT
|
||||
|
||||
# Clean the entity
|
||||
entity.remove_thermostat()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_window_auto_auto_stop(hass: HomeAssistant, skip_hass_states_is_state):
|
||||
"""Test the Power management"""
|
||||
|
||||
@@ -438,11 +482,11 @@ async def test_window_auto_auto_stop(hass: HomeAssistant, skip_hass_states_is_st
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
|
||||
) as mock_send_event, patch(
|
||||
"custom_components.versatile_thermostat.climate.VersatileThermostat._async_heater_turn_on"
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
|
||||
) as mock_heater_on, patch(
|
||||
"custom_components.versatile_thermostat.climate.VersatileThermostat._async_underlying_entity_turn_off"
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
|
||||
) as mock_heater_off, patch(
|
||||
"custom_components.versatile_thermostat.climate.VersatileThermostat._is_device_active",
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
|
||||
return_value=True,
|
||||
):
|
||||
event_timestamp = now - timedelta(minutes=4)
|
||||
@@ -459,18 +503,32 @@ async def test_window_auto_auto_stop(hass: HomeAssistant, skip_hass_states_is_st
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
|
||||
) as mock_send_event, patch(
|
||||
"custom_components.versatile_thermostat.climate.VersatileThermostat._async_heater_turn_on"
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
|
||||
) as mock_heater_on, patch(
|
||||
"custom_components.versatile_thermostat.climate.VersatileThermostat._async_underlying_entity_turn_off"
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
|
||||
) as mock_heater_off, patch(
|
||||
"custom_components.versatile_thermostat.climate.VersatileThermostat._is_device_active",
|
||||
"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)
|
||||
await send_temperature_change_event(entity, 18, event_timestamp, sleep=False)
|
||||
|
||||
# The heater turns on
|
||||
assert mock_send_event.call_count == 2
|
||||
# The heater turns off
|
||||
mock_send_event.assert_has_calls(
|
||||
[
|
||||
call.send_event(EventType.HVAC_MODE_EVENT, {"hvac_mode": HVACMode.OFF}),
|
||||
call.send_event(
|
||||
EventType.WINDOW_AUTO_EVENT,
|
||||
{
|
||||
"type": "start",
|
||||
"cause": "slope alert",
|
||||
"curve_slope": -1.0,
|
||||
},
|
||||
),
|
||||
],
|
||||
any_order=True,
|
||||
)
|
||||
assert mock_heater_on.call_count == 0
|
||||
assert mock_heater_off.call_count >= 1
|
||||
assert entity.last_temperature_slope == -1
|
||||
@@ -483,11 +541,11 @@ async def test_window_auto_auto_stop(hass: HomeAssistant, skip_hass_states_is_st
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
|
||||
) as mock_send_event, patch(
|
||||
"custom_components.versatile_thermostat.climate.VersatileThermostat._async_heater_turn_on"
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
|
||||
) as mock_heater_on, patch(
|
||||
"custom_components.versatile_thermostat.climate.VersatileThermostat._async_underlying_entity_turn_off"
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
|
||||
) as mock_heater_off, patch(
|
||||
"custom_components.versatile_thermostat.climate.VersatileThermostat._is_device_active",
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
|
||||
return_value=False,
|
||||
):
|
||||
await asyncio.sleep(0.3)
|
||||
@@ -502,7 +560,12 @@ async def test_window_auto_auto_stop(hass: HomeAssistant, skip_hass_states_is_st
|
||||
assert entity.hvac_mode is HVACMode.HEAT
|
||||
assert entity.preset_mode is PRESET_BOOST
|
||||
|
||||
# Clean the entity
|
||||
entity.remove_thermostat()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_window_auto_no_on_percent(
|
||||
hass: HomeAssistant, skip_hass_states_is_state
|
||||
):
|
||||
@@ -564,11 +627,11 @@ async def test_window_auto_no_on_percent(
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
|
||||
) as mock_send_event, patch(
|
||||
"custom_components.versatile_thermostat.climate.VersatileThermostat._async_heater_turn_on"
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
|
||||
) as mock_heater_on, patch(
|
||||
"custom_components.versatile_thermostat.climate.VersatileThermostat._async_underlying_entity_turn_off"
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
|
||||
) as mock_heater_off, patch(
|
||||
"custom_components.versatile_thermostat.climate.VersatileThermostat._is_device_active",
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
|
||||
return_value=True,
|
||||
):
|
||||
event_timestamp = now - timedelta(minutes=4)
|
||||
@@ -586,11 +649,11 @@ async def test_window_auto_no_on_percent(
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
|
||||
) as mock_send_event, patch(
|
||||
"custom_components.versatile_thermostat.climate.VersatileThermostat._async_heater_turn_on"
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
|
||||
) as mock_heater_on, patch(
|
||||
"custom_components.versatile_thermostat.climate.VersatileThermostat._async_underlying_entity_turn_off"
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
|
||||
) as mock_heater_off, patch(
|
||||
"custom_components.versatile_thermostat.climate.VersatileThermostat._is_device_active",
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
|
||||
return_value=True,
|
||||
):
|
||||
event_timestamp = now - timedelta(minutes=3)
|
||||
@@ -605,3 +668,6 @@ async def test_window_auto_no_on_percent(
|
||||
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()
|
||||
Reference in New Issue
Block a user