Compare commits
7 Commits
3.6.0.beta
...
3.6.0.beta
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8b88ed5c9c | ||
|
|
c0d422a916 | ||
|
|
954fc6271c | ||
|
|
6dc078871d | ||
|
|
cd0ab3c88d | ||
|
|
eb54f2826f | ||
|
|
043fd5f7aa |
4
.bashrc
4
.bashrc
@@ -1,6 +1,4 @@
|
|||||||
|
|
||||||
echo "Sourcing .bashrc"
|
echo "Sourcing .bashrc"
|
||||||
alias ll='ls -l'
|
alias ll='ls -l'
|
||||||
export HA='/home/vscode/core'
|
# source venv/bin/activate
|
||||||
cd $HA
|
|
||||||
source venv/bin/activate
|
|
||||||
|
|||||||
@@ -60,6 +60,9 @@ input_boolean:
|
|||||||
fake_heater_switch1:
|
fake_heater_switch1:
|
||||||
name: Heater 1
|
name: Heater 1
|
||||||
icon: mdi:radiator
|
icon: mdi:radiator
|
||||||
|
fake_heater_ac1:
|
||||||
|
name: Air contionner 1
|
||||||
|
icon: mdi:air-conditioner
|
||||||
fake_heater_4switch1:
|
fake_heater_4switch1:
|
||||||
name: Heater (multiswitch1)
|
name: Heater (multiswitch1)
|
||||||
icon: mdi:radiator
|
icon: mdi:radiator
|
||||||
@@ -114,22 +117,22 @@ climate:
|
|||||||
name: Underlying thermostat 4-1
|
name: Underlying thermostat 4-1
|
||||||
heater: input_boolean.fake_heater_4climate1
|
heater: input_boolean.fake_heater_4climate1
|
||||||
target_sensor: input_number.fake_temperature_sensor1
|
target_sensor: input_number.fake_temperature_sensor1
|
||||||
ac_mode: true
|
ac_mode: false
|
||||||
- platform: generic_thermostat
|
- platform: generic_thermostat
|
||||||
name: Underlying thermostat 4-2
|
name: Underlying thermostat 4-2
|
||||||
heater: input_boolean.fake_heater_4climate2
|
heater: input_boolean.fake_heater_4climate2
|
||||||
target_sensor: input_number.fake_temperature_sensor1
|
target_sensor: input_number.fake_temperature_sensor1
|
||||||
ac_mode: true
|
ac_mode: false
|
||||||
- platform: generic_thermostat
|
- platform: generic_thermostat
|
||||||
name: Underlying thermostat 4-3
|
name: Underlying thermostat 4-3
|
||||||
heater: input_boolean.fake_heater_4climate3
|
heater: input_boolean.fake_heater_4climate3
|
||||||
target_sensor: input_number.fake_temperature_sensor1
|
target_sensor: input_number.fake_temperature_sensor1
|
||||||
ac_mode: true
|
ac_mode: false
|
||||||
- platform: generic_thermostat
|
- platform: generic_thermostat
|
||||||
name: Underlying thermostat 4-4
|
name: Underlying thermostat 4-4
|
||||||
heater: input_boolean.fake_heater_4climate4
|
heater: input_boolean.fake_heater_4climate4
|
||||||
target_sensor: input_number.fake_temperature_sensor1
|
target_sensor: input_number.fake_temperature_sensor1
|
||||||
ac_mode: true
|
ac_mode: false
|
||||||
- platform: generic_thermostat
|
- platform: generic_thermostat
|
||||||
name: Underlying thermostat9
|
name: Underlying thermostat9
|
||||||
heater: input_boolean.fake_heater_switch3
|
heater: input_boolean.fake_heater_switch3
|
||||||
|
|||||||
@@ -1,44 +1,54 @@
|
|||||||
// See https://aka.ms/vscode-remote/devcontainer.json for format details.
|
// See https://aka.ms/vscode-remote/devcontainer.json for format details.
|
||||||
// "image": "ghcr.io/ludeeus/devcontainer/integration:latest",
|
// "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",
|
"name": "Versatile Thermostat integration",
|
||||||
"context": "..",
|
|
||||||
"appPort": [
|
"appPort": [
|
||||||
"9123:8123"
|
"8123:8123"
|
||||||
],
|
],
|
||||||
// "postCreateCommand": "container install",
|
// "postCreateCommand": "container install",
|
||||||
"postCreateCommand": "./container install",
|
"postCreateCommand": "./container dev-setup",
|
||||||
"extensions": [
|
|
||||||
"ms-python.python",
|
|
||||||
"github.vscode-pull-request-github",
|
|
||||||
"ryanluker.vscode-coverage-gutters",
|
|
||||||
"ms-python.vscode-pylance"
|
|
||||||
],
|
|
||||||
"mounts": [
|
"mounts": [
|
||||||
"source=/Users/jmcollin/SugarSync/Projets/home-assistant/core,target=/home/vscode/core,type=bind,consistency=cached",
|
"source=/Users/jmcollin/.ssh,target=/home/vscode/.ssh,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"
|
|
||||||
],
|
],
|
||||||
"settings": {
|
|
||||||
"files.eol": "\n",
|
"customizations": {
|
||||||
"editor.tabSize": 4,
|
"vscode": {
|
||||||
"terminal.integrated.profiles.linux": {
|
"extensions": [
|
||||||
"Bash Profile": {
|
"ms-python.python",
|
||||||
"path": "bash",
|
"github.vscode-pull-request-github",
|
||||||
"args": []
|
"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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
6
.gitignore
vendored
6
.gitignore
vendored
@@ -103,4 +103,8 @@ dist
|
|||||||
# TernJS port file
|
# TernJS port file
|
||||||
.tern-port
|
.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",
|
"version": "0.2.0",
|
||||||
"configurations": [
|
"configurations": [
|
||||||
{
|
{
|
||||||
// Example of attaching to local debug server
|
"name": "Home Assistant (debug)",
|
||||||
"name": "Python: Attach Local",
|
|
||||||
"type": "python",
|
"type": "python",
|
||||||
"request": "attach",
|
"request": "launch",
|
||||||
"port": 5678,
|
"module": "homeassistant",
|
||||||
"host": "localhost",
|
|
||||||
"justMyCode": false,
|
"justMyCode": false,
|
||||||
"pathMappings": [
|
"args": [
|
||||||
// {
|
"--debug",
|
||||||
// "localRoot": "${workspaceFolder}",
|
"-c",
|
||||||
// "remoteRoot": "."
|
"config"
|
||||||
//},
|
|
||||||
{
|
|
||||||
"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"
|
|
||||||
}
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
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.pylintEnabled": true,
|
||||||
"python.linting.enabled": true,
|
"python.linting.enabled": true,
|
||||||
"python.pythonPath": "/usr/local/bin/python",
|
|
||||||
"files.associations": {
|
"files.associations": {
|
||||||
"*.yaml": "home-assistant"
|
"*.yaml": "home-assistant"
|
||||||
},
|
},
|
||||||
|
"python.testing.pytestArgs": [
|
||||||
|
"tests"
|
||||||
|
],
|
||||||
|
"python.testing.unittestEnabled": false,
|
||||||
|
"python.testing.pytestEnabled": true,
|
||||||
"python.analysis.extraPaths": [
|
"python.analysis.extraPaths": [
|
||||||
"/home/vscode/core",
|
// "/home/vscode/core",
|
||||||
"/workspaces/versatile_thermostat"
|
"/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",
|
"version": "2.0.0",
|
||||||
"tasks": [
|
"tasks": [
|
||||||
{
|
{
|
||||||
"label": "Run Home Assistant on port 9123",
|
"label": "Run Home Assistant on port 8123",
|
||||||
"type": "shell",
|
"type": "shell",
|
||||||
"command": "./container start",
|
"command": "./container start",
|
||||||
"problemMatcher": []
|
"problemMatcher": []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "Restart Home Assistant on port 9123",
|
"label": "Restart Home Assistant on port 8123",
|
||||||
"type": "shell",
|
"type": "shell",
|
||||||
"command": "./container restart",
|
"command": "./container restart",
|
||||||
"problemMatcher": []
|
"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.
|
||||||
19
README-fr.md
19
README-fr.md
@@ -55,6 +55,7 @@ Ce composant personnalisé pour Home Assistant est une mise à niveau et est une
|
|||||||
|
|
||||||
|
|
||||||
>  _*Nouveautés*_
|
>  _*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.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.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.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
|
||||||
@@ -242,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é.
|
Nous allons maintenant voir comment configurer le nouveau mode Activité.
|
||||||
Ce dont nous avons besoin:
|
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é)
|
- 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 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.
|
- 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 :
|
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.
|
- 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 C
|
- 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 la température est fixée à 21,5 C
|
- 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°
|
||||||
- la personne quitte la chambre, au bout de 5 min la température est ramenée à 18,5 C
|
- 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*_
|
>  _*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
|
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
|
||||||
@@ -354,7 +358,8 @@ Voir [exemple de réglages](#examples-tuning) pour avoir des exemples de réglag
|
|||||||
| ``window_auto_close_threshold`` | Seuil bas de chute de température pour la fin de 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 |
|
| ``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_sensor_entity_id`` | Détecteur de mouvement entity id | X | X |
|
||||||
| ``motion_delay`` | Délai avant changement (seconds) | 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 |
|
| ``motion_preset`` | Preset à utiliser si mouvement détecté | X | X |
|
||||||
| ``no_motion_preset`` | Preset à utiliser si pas de 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 |
|
| ``power_sensor_entity_id`` | Capteur de puissance totale (entity id) | X | X |
|
||||||
|
|||||||
18
README.md
18
README.md
@@ -54,6 +54,7 @@
|
|||||||
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.
|
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*_
|
> _*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.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.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.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
|
||||||
@@ -228,15 +229,17 @@ We will now see how to configure the new Activity mode.
|
|||||||
What we need:
|
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 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 **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 "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.
|
- 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 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.
|
||||||
- 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 has been empty for a while (no activity detected), the temperature of this room is 18.5°
|
||||||
- the room is empty for a while (no activity detected), the temperature of this room is 18.5 C
|
- someone enters the room, activity is detected if movement is present for at least 30 seconds. The temperature then rises to 21.5°
|
||||||
- somebody enters into the room, an activity is detected the temperature is set to 21.5 C
|
- if the movement is present for less than 30 seconds (rapid passage), the temperature remains at 18.5°,
|
||||||
- the person leaves the room, after 5 min the temperature is set back to 18.5 C
|
- 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.
|
For this to work, the climate thermostat should be in ``Activity`` preset mode.
|
||||||
|
|
||||||
>  _*Notes*_
|
>  _*Notes*_
|
||||||
@@ -339,7 +342,8 @@ See [example tuning](#examples-tuning) for common tuning examples
|
|||||||
| ``window_auto_close_threshold`` | Temperature increase threshold for end of automatic 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 |
|
| ``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_sensor_entity_id`` | Motion sensor entity id | X | X |
|
||||||
| ``motion_delay`` | Motion delay (seconds) | 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 |
|
| ``motion_preset`` | Preset to use when motion is detected | X | X |
|
||||||
| ``no_motion_preset`` | Preset to use when no 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 |
|
| ``power_sensor_entity_id`` | Power sensor entity id | X | X |
|
||||||
|
|||||||
68
container
68
container
@@ -4,35 +4,43 @@
|
|||||||
|
|
||||||
. .bashrc
|
. .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: "$*
|
echo "arguments are: "$1
|
||||||
# Post installation of container
|
|
||||||
command=$1
|
|
||||||
if [ "$command" == "install" ]; then
|
|
||||||
echo "Running container post installation"
|
|
||||||
script/setup
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ "$command" == "start" ]; then
|
case $1 in
|
||||||
echo "Running container start"
|
start)
|
||||||
hass -c ./config --debug
|
echo "Running container start"
|
||||||
fi
|
./scripts/starts_ha.sh
|
||||||
|
;;
|
||||||
if [ "$command" == "translations" ]; then
|
dev-setup)
|
||||||
echo "Running container start"
|
get_dev
|
||||||
python3 -m script.translations develop
|
;;
|
||||||
fi
|
install)
|
||||||
|
echo "Running container post installation"
|
||||||
if [ "$command" == "hassfest" ]; then
|
script/setup
|
||||||
echo "Running container start"
|
;;
|
||||||
python3 -m script.hassfest
|
translations)
|
||||||
# python -m script.hassfest --requirements --action validate --integration-path config/custom_components/versatile_thermostat/
|
echo "Running container start"
|
||||||
fi
|
cd $HA
|
||||||
|
python3 -m script.translations develop
|
||||||
if [ "$command" == "restart" ]; then
|
;;
|
||||||
echo "Killing existing container"
|
hassfest)
|
||||||
pkill hass
|
echo "Running container start"
|
||||||
echo "Killing existing container"
|
python3 -m script.hassfest
|
||||||
hass -c ./config
|
# python -m script.hassfest --requirements --action validate --integration-path config/custom_components/versatile_thermostat/
|
||||||
fi
|
;;
|
||||||
|
restart)
|
||||||
|
echo "Killing existing container"
|
||||||
|
pkill hass
|
||||||
|
echo "Restarting existing container"
|
||||||
|
pwd
|
||||||
|
./scripts/starts_ha.sh
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|||||||
@@ -54,7 +54,9 @@ async def async_setup_entry(
|
|||||||
class SecurityBinarySensor(VersatileThermostatBaseEntity, BinarySensorEntity):
|
class SecurityBinarySensor(VersatileThermostatBaseEntity, BinarySensorEntity):
|
||||||
"""Representation of a BinarySensor which exposes the security state"""
|
"""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"""
|
"""Initialize the SecurityState Binary sensor"""
|
||||||
super().__init__(hass, unique_id, entry_infos.get(CONF_NAME))
|
super().__init__(hass, unique_id, entry_infos.get(CONF_NAME))
|
||||||
self._attr_name = "Security state"
|
self._attr_name = "Security state"
|
||||||
@@ -87,7 +89,9 @@ class SecurityBinarySensor(VersatileThermostatBaseEntity, BinarySensorEntity):
|
|||||||
class OverpoweringBinarySensor(VersatileThermostatBaseEntity, BinarySensorEntity):
|
class OverpoweringBinarySensor(VersatileThermostatBaseEntity, BinarySensorEntity):
|
||||||
"""Representation of a BinarySensor which exposes the overpowering state"""
|
"""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"""
|
"""Initialize the OverpoweringState Binary sensor"""
|
||||||
super().__init__(hass, unique_id, entry_infos.get(CONF_NAME))
|
super().__init__(hass, unique_id, entry_infos.get(CONF_NAME))
|
||||||
self._attr_name = "Overpowering state"
|
self._attr_name = "Overpowering state"
|
||||||
@@ -120,7 +124,9 @@ class OverpoweringBinarySensor(VersatileThermostatBaseEntity, BinarySensorEntity
|
|||||||
class WindowBinarySensor(VersatileThermostatBaseEntity, BinarySensorEntity):
|
class WindowBinarySensor(VersatileThermostatBaseEntity, BinarySensorEntity):
|
||||||
"""Representation of a BinarySensor which exposes the window state"""
|
"""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"""
|
"""Initialize the WindowState Binary sensor"""
|
||||||
super().__init__(hass, unique_id, entry_infos.get(CONF_NAME))
|
super().__init__(hass, unique_id, entry_infos.get(CONF_NAME))
|
||||||
self._attr_name = "Window state"
|
self._attr_name = "Window state"
|
||||||
@@ -134,7 +140,10 @@ class WindowBinarySensor(VersatileThermostatBaseEntity, BinarySensorEntity):
|
|||||||
|
|
||||||
old_state = self._attr_is_on
|
old_state = self._attr_is_on
|
||||||
# Issue 120 - only take defined presence value
|
# 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]:
|
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._attr_is_on = (
|
||||||
self.my_climate.window_state == STATE_ON
|
self.my_climate.window_state == STATE_ON
|
||||||
or self.my_climate.window_auto_state == STATE_ON
|
or self.my_climate.window_auto_state == STATE_ON
|
||||||
@@ -161,7 +170,9 @@ class WindowBinarySensor(VersatileThermostatBaseEntity, BinarySensorEntity):
|
|||||||
class MotionBinarySensor(VersatileThermostatBaseEntity, BinarySensorEntity):
|
class MotionBinarySensor(VersatileThermostatBaseEntity, BinarySensorEntity):
|
||||||
"""Representation of a BinarySensor which exposes the motion state"""
|
"""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"""
|
"""Initialize the MotionState Binary sensor"""
|
||||||
super().__init__(hass, unique_id, entry_infos.get(CONF_NAME))
|
super().__init__(hass, unique_id, entry_infos.get(CONF_NAME))
|
||||||
self._attr_name = "Motion state"
|
self._attr_name = "Motion state"
|
||||||
@@ -195,7 +206,9 @@ class MotionBinarySensor(VersatileThermostatBaseEntity, BinarySensorEntity):
|
|||||||
class PresenceBinarySensor(VersatileThermostatBaseEntity, BinarySensorEntity):
|
class PresenceBinarySensor(VersatileThermostatBaseEntity, BinarySensorEntity):
|
||||||
"""Representation of a BinarySensor which exposes the presence state"""
|
"""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"""
|
"""Initialize the PresenceState Binary sensor"""
|
||||||
super().__init__(hass, unique_id, entry_infos.get(CONF_NAME))
|
super().__init__(hass, unique_id, entry_infos.get(CONF_NAME))
|
||||||
self._attr_name = "Presence state"
|
self._attr_name = "Presence state"
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
# pylint: disable=line-too-long
|
||||||
|
# pylint: disable=too-many-lines
|
||||||
|
# pylint: disable=invalid-name
|
||||||
""" Implements the VersatileThermostat climate component """
|
""" Implements the VersatileThermostat climate component """
|
||||||
import math
|
import math
|
||||||
import logging
|
import logging
|
||||||
@@ -95,6 +98,7 @@ from .const import (
|
|||||||
CONF_WINDOW_AUTO_MAX_DURATION,
|
CONF_WINDOW_AUTO_MAX_DURATION,
|
||||||
CONF_MOTION_SENSOR,
|
CONF_MOTION_SENSOR,
|
||||||
CONF_MOTION_DELAY,
|
CONF_MOTION_DELAY,
|
||||||
|
CONF_MOTION_OFF_DELAY,
|
||||||
CONF_MOTION_PRESET,
|
CONF_MOTION_PRESET,
|
||||||
CONF_NO_MOTION_PRESET,
|
CONF_NO_MOTION_PRESET,
|
||||||
CONF_DEVICE_POWER,
|
CONF_DEVICE_POWER,
|
||||||
@@ -300,7 +304,7 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
|||||||
entry_infos,
|
entry_infos,
|
||||||
)
|
)
|
||||||
|
|
||||||
self._ac_mode = entry_infos.get(CONF_AC_MODE) == True
|
self._ac_mode = entry_infos.get(CONF_AC_MODE) is True
|
||||||
# convert entry_infos into usable attributes
|
# convert entry_infos into usable attributes
|
||||||
presets = {}
|
presets = {}
|
||||||
items = CONF_PRESETS_WITH_AC.items() if self._ac_mode else CONF_PRESETS.items()
|
items = CONF_PRESETS_WITH_AC.items() if self._ac_mode else CONF_PRESETS.items()
|
||||||
@@ -338,7 +342,12 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
|||||||
self._thermostat_type = entry_infos.get(CONF_THERMOSTAT_TYPE)
|
self._thermostat_type = entry_infos.get(CONF_THERMOSTAT_TYPE)
|
||||||
if self._thermostat_type == CONF_THERMOSTAT_CLIMATE:
|
if self._thermostat_type == CONF_THERMOSTAT_CLIMATE:
|
||||||
self._is_over_climate = True
|
self._is_over_climate = True
|
||||||
for climate in [CONF_CLIMATE, CONF_CLIMATE_2, CONF_CLIMATE_3, CONF_CLIMATE_4]:
|
for climate in [
|
||||||
|
CONF_CLIMATE,
|
||||||
|
CONF_CLIMATE_2,
|
||||||
|
CONF_CLIMATE_3,
|
||||||
|
CONF_CLIMATE_4,
|
||||||
|
]:
|
||||||
if entry_infos.get(climate):
|
if entry_infos.get(climate):
|
||||||
self._underlyings.append(
|
self._underlyings.append(
|
||||||
UnderlyingClimate(
|
UnderlyingClimate(
|
||||||
@@ -399,6 +408,10 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
|||||||
|
|
||||||
self._motion_sensor_entity_id = entry_infos.get(CONF_MOTION_SENSOR)
|
self._motion_sensor_entity_id = entry_infos.get(CONF_MOTION_SENSOR)
|
||||||
self._motion_delay_sec = entry_infos.get(CONF_MOTION_DELAY)
|
self._motion_delay_sec = entry_infos.get(CONF_MOTION_DELAY)
|
||||||
|
self._motion_off_delay_sec = entry_infos.get(CONF_MOTION_OFF_DELAY)
|
||||||
|
if not self._motion_off_delay_sec:
|
||||||
|
self._motion_off_delay_sec = self._motion_delay_sec
|
||||||
|
|
||||||
self._motion_preset = entry_infos.get(CONF_MOTION_PRESET)
|
self._motion_preset = entry_infos.get(CONF_MOTION_PRESET)
|
||||||
self._no_motion_preset = entry_infos.get(CONF_NO_MOTION_PRESET)
|
self._no_motion_preset = entry_infos.get(CONF_NO_MOTION_PRESET)
|
||||||
self._motion_on = (
|
self._motion_on = (
|
||||||
@@ -415,7 +428,7 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
|||||||
self._presence_on = self._presence_sensor_entity_id is not None
|
self._presence_on = self._presence_sensor_entity_id is not None
|
||||||
|
|
||||||
if self._ac_mode:
|
if self._ac_mode:
|
||||||
self._hvac_list = [HVACMode.HEAT, HVACMode.COOL, HVACMode.OFF]
|
self._hvac_list = [HVACMode.COOL, HVACMode.OFF]
|
||||||
else:
|
else:
|
||||||
self._hvac_list = [HVACMode.HEAT, HVACMode.OFF]
|
self._hvac_list = [HVACMode.HEAT, HVACMode.OFF]
|
||||||
|
|
||||||
@@ -642,7 +655,11 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
|||||||
|
|
||||||
# Initialize all UnderlyingEntities
|
# Initialize all UnderlyingEntities
|
||||||
for under in self._underlyings:
|
for under in self._underlyings:
|
||||||
under.startup()
|
try:
|
||||||
|
under.startup()
|
||||||
|
except UnknownEntity:
|
||||||
|
# Not found, we will try later
|
||||||
|
pass
|
||||||
|
|
||||||
temperature_state = self.hass.states.get(self._temp_sensor_entity_id)
|
temperature_state = self.hass.states.get(self._temp_sensor_entity_id)
|
||||||
if temperature_state and temperature_state.state not in (
|
if temperature_state and temperature_state.state not in (
|
||||||
@@ -764,7 +781,10 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
|||||||
self.async_write_ha_state()
|
self.async_write_ha_state()
|
||||||
if self._prop_algorithm:
|
if self._prop_algorithm:
|
||||||
self._prop_algorithm.calculate(
|
self._prop_algorithm.calculate(
|
||||||
self._target_temp, self._cur_temp, self._cur_ext_temp
|
self._target_temp,
|
||||||
|
self._cur_temp,
|
||||||
|
self._cur_ext_temp,
|
||||||
|
self._hvac_mode == HVACMode.COOL,
|
||||||
)
|
)
|
||||||
|
|
||||||
self.hass.create_task(self._check_switch_initial_state())
|
self.hass.create_task(self._check_switch_initial_state())
|
||||||
@@ -899,6 +919,11 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
|||||||
|
|
||||||
return self._hvac_list
|
return self._hvac_list
|
||||||
|
|
||||||
|
@property
|
||||||
|
def ac_mode(self) -> bool:
|
||||||
|
""" Get the ac_mode of the Themostat"""
|
||||||
|
return self._ac_mode
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def fan_mode(self) -> str | None:
|
def fan_mode(self) -> str | None:
|
||||||
"""Return the fan setting.
|
"""Return the fan setting.
|
||||||
@@ -957,8 +982,8 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
|||||||
# Issue #114 - returns my current hvac_mode and not the underlying hvac_mode which could be different
|
# Issue #114 - returns my current hvac_mode and not the underlying hvac_mode which could be different
|
||||||
# delta will be managed by climate_state_change event.
|
# delta will be managed by climate_state_change event.
|
||||||
# if self._is_over_climate:
|
# if self._is_over_climate:
|
||||||
# if one not OFF -> return it
|
# if one not OFF -> return it
|
||||||
# else OFF
|
# else OFF
|
||||||
# for under in self._underlyings:
|
# for under in self._underlyings:
|
||||||
# if (mode := under.hvac_mode) not in [HVACMode.OFF]
|
# if (mode := under.hvac_mode) not in [HVACMode.OFF]
|
||||||
# return mode
|
# return mode
|
||||||
@@ -978,7 +1003,10 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
|||||||
# else OFF
|
# else OFF
|
||||||
one_idle = False
|
one_idle = False
|
||||||
for under in self._underlyings:
|
for under in self._underlyings:
|
||||||
if (action := under.hvac_action) not in [HVACAction.IDLE, HVACAction.OFF]:
|
if (action := under.hvac_action) not in [
|
||||||
|
HVACAction.IDLE,
|
||||||
|
HVACAction.OFF,
|
||||||
|
]:
|
||||||
return action
|
return action
|
||||||
if under.hvac_action == HVACAction.IDLE:
|
if under.hvac_action == HVACAction.IDLE:
|
||||||
one_idle = True
|
one_idle = True
|
||||||
@@ -990,6 +1018,8 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
|||||||
return HVACAction.OFF
|
return HVACAction.OFF
|
||||||
if not self._is_device_active:
|
if not self._is_device_active:
|
||||||
return HVACAction.IDLE
|
return HVACAction.IDLE
|
||||||
|
if self._hvac_mode == HVACMode.COOL:
|
||||||
|
return HVACAction.COOLING
|
||||||
return HVACAction.HEATING
|
return HVACAction.HEATING
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -1061,7 +1091,7 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def mean_cycle_power(self) -> float | None:
|
def mean_cycle_power(self) -> float | None:
|
||||||
"""Returns tne mean power consumption during the cycle"""
|
"""Returns the mean power consumption during the cycle"""
|
||||||
if not self._device_power or self._is_over_climate:
|
if not self._device_power or self._is_over_climate:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -1294,7 +1324,9 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
|||||||
self.recalculate()
|
self.recalculate()
|
||||||
self.send_event(EventType.PRESET_EVENT, {"preset": self._attr_preset_mode})
|
self.send_event(EventType.PRESET_EVENT, {"preset": self._attr_preset_mode})
|
||||||
|
|
||||||
def reset_last_change_time(self, old_preset_mode=None):
|
def reset_last_change_time(
|
||||||
|
self, old_preset_mode=None
|
||||||
|
): # pylint: disable=unused-argument
|
||||||
"""Reset to now the last change time"""
|
"""Reset to now the last change time"""
|
||||||
self._last_change_time = datetime.now(tz=self._current_tz)
|
self._last_change_time = datetime.now(tz=self._current_tz)
|
||||||
_LOGGER.debug("%s - last_change_time is now %s", self, self._last_change_time)
|
_LOGGER.debug("%s - last_change_time is now %s", self, self._last_change_time)
|
||||||
@@ -1318,8 +1350,8 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
|||||||
if preset_mode == PRESET_POWER:
|
if preset_mode == PRESET_POWER:
|
||||||
return self._power_temp
|
return self._power_temp
|
||||||
else:
|
else:
|
||||||
# Select _ac presets if in COOL Mode
|
# Select _ac presets if in COOL Mode (or over_switch with _ac_mode)
|
||||||
if self._ac_mode and self._hvac_mode == HVACMode.COOL:
|
if self._ac_mode and (self._hvac_mode == HVACMode.COOL or not self._is_over_climate):
|
||||||
preset_mode = preset_mode + PRESET_AC_SUFFIX
|
preset_mode = preset_mode + PRESET_AC_SUFFIX
|
||||||
|
|
||||||
if self._presence_on is False or self._presence_state in [
|
if self._presence_on is False or self._presence_state in [
|
||||||
@@ -1528,11 +1560,16 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
|||||||
# Check delay condition
|
# Check delay condition
|
||||||
async def try_motion_condition(_):
|
async def try_motion_condition(_):
|
||||||
try:
|
try:
|
||||||
|
delay = (
|
||||||
|
self._motion_delay_sec
|
||||||
|
if new_state.state == STATE_ON
|
||||||
|
else self._motion_off_delay_sec
|
||||||
|
)
|
||||||
long_enough = condition.state(
|
long_enough = condition.state(
|
||||||
self.hass,
|
self.hass,
|
||||||
self._motion_sensor_entity_id,
|
self._motion_sensor_entity_id,
|
||||||
new_state.state,
|
new_state.state,
|
||||||
timedelta(seconds=self._motion_delay_sec),
|
timedelta(seconds=delay),
|
||||||
)
|
)
|
||||||
except ConditionError:
|
except ConditionError:
|
||||||
long_enough = False
|
long_enough = False
|
||||||
@@ -1541,45 +1578,83 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
|||||||
_LOGGER.debug(
|
_LOGGER.debug(
|
||||||
"Motion delay condition is not satisfied. Ignore motion event"
|
"Motion delay condition is not satisfied. Ignore motion event"
|
||||||
)
|
)
|
||||||
return
|
else:
|
||||||
|
_LOGGER.debug("%s - Motion delay condition is satisfied", self)
|
||||||
|
self._motion_state = new_state.state
|
||||||
|
if self._attr_preset_mode == PRESET_ACTIVITY:
|
||||||
|
new_preset = (
|
||||||
|
self._motion_preset
|
||||||
|
if self._motion_state == STATE_ON
|
||||||
|
else self._no_motion_preset
|
||||||
|
)
|
||||||
|
_LOGGER.info(
|
||||||
|
"%s - Motion condition have changes. New preset temp will be %s",
|
||||||
|
self,
|
||||||
|
new_preset,
|
||||||
|
)
|
||||||
|
# We do not change the preset which is kept to ACTIVITY but only the target_temperature
|
||||||
|
# We take the presence into account
|
||||||
|
await self._async_internal_set_temperature(
|
||||||
|
self.find_preset_temp(new_preset)
|
||||||
|
)
|
||||||
|
self.recalculate()
|
||||||
|
await self._async_control_heating(force=True)
|
||||||
|
self._motion_call_cancel = None
|
||||||
|
|
||||||
_LOGGER.debug("%s - Motion delay condition is satisfied", self)
|
im_on = self._motion_state == STATE_ON
|
||||||
self._motion_state = new_state.state
|
delay_running = self._motion_call_cancel is not None
|
||||||
if self._attr_preset_mode == PRESET_ACTIVITY:
|
event_on = new_state.state == STATE_ON
|
||||||
new_preset = (
|
|
||||||
self._motion_preset
|
|
||||||
if self._motion_state == STATE_ON
|
|
||||||
else self._no_motion_preset
|
|
||||||
)
|
|
||||||
_LOGGER.info(
|
|
||||||
"%s - Motion condition have changes. New preset temp will be %s",
|
|
||||||
self,
|
|
||||||
new_preset,
|
|
||||||
)
|
|
||||||
# We do not change the preset which is kept to ACTIVITY but only the target_temperature
|
|
||||||
# We take the presence into account
|
|
||||||
await self._async_internal_set_temperature(
|
|
||||||
self.find_preset_temp(new_preset)
|
|
||||||
)
|
|
||||||
self.recalculate()
|
|
||||||
await self._async_control_heating(force=True)
|
|
||||||
|
|
||||||
if self._motion_call_cancel:
|
def arm():
|
||||||
|
"""Arm the timer"""
|
||||||
|
delay = (
|
||||||
|
self._motion_delay_sec
|
||||||
|
if new_state.state == STATE_ON
|
||||||
|
else self._motion_off_delay_sec
|
||||||
|
)
|
||||||
|
self._motion_call_cancel = async_call_later(
|
||||||
|
self.hass, timedelta(seconds=delay), try_motion_condition
|
||||||
|
)
|
||||||
|
|
||||||
|
def desarm():
|
||||||
|
# restart the timer
|
||||||
self._motion_call_cancel()
|
self._motion_call_cancel()
|
||||||
self._motion_call_cancel = None
|
self._motion_call_cancel = None
|
||||||
self._motion_call_cancel = async_call_later(
|
|
||||||
self.hass, timedelta(seconds=self._motion_delay_sec), try_motion_condition
|
|
||||||
)
|
|
||||||
|
|
||||||
# For testing purpose we need to access the inner function
|
# if I'm off
|
||||||
return try_motion_condition
|
if not im_on:
|
||||||
|
if event_on and not delay_running:
|
||||||
|
_LOGGER.debug(
|
||||||
|
"%s - Arm delay cause i'm off and event is on and no delay is running",
|
||||||
|
self,
|
||||||
|
)
|
||||||
|
arm()
|
||||||
|
return try_motion_condition
|
||||||
|
# Ignore the event
|
||||||
|
_LOGGER.debug("%s - Event ignored cause i'm already off", self)
|
||||||
|
return None
|
||||||
|
else: # I'm On
|
||||||
|
if not event_on and not delay_running:
|
||||||
|
_LOGGER.info("%s - Arm delay cause i'm on and event is off", self)
|
||||||
|
arm()
|
||||||
|
return try_motion_condition
|
||||||
|
if event_on and delay_running:
|
||||||
|
_LOGGER.debug(
|
||||||
|
"%s - Desarm off delay cause i'm on and event is on and a delay is running",
|
||||||
|
self,
|
||||||
|
)
|
||||||
|
desarm()
|
||||||
|
return None
|
||||||
|
# Ignore the event
|
||||||
|
_LOGGER.debug("%s - Event ignored cause i'm already on", self)
|
||||||
|
return None
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
async def _check_switch_initial_state(self):
|
async def _check_switch_initial_state(self):
|
||||||
"""Prevent the device from keep running if HVAC_MODE_OFF."""
|
"""Prevent the device from keep running if HVAC_MODE_OFF."""
|
||||||
_LOGGER.debug("%s - Calling _check_switch_initial_state", self)
|
_LOGGER.debug("%s - Calling _check_switch_initial_state", self)
|
||||||
# We need to do the same check for over_climate underlyings
|
# We need to do the same check for over_climate underlyings
|
||||||
#if self.is_over_climate:
|
# if self.is_over_climate:
|
||||||
# return
|
# return
|
||||||
for under in self._underlyings:
|
for under in self._underlyings:
|
||||||
await under.check_initial_state(self._hvac_mode)
|
await under.check_initial_state(self._hvac_mode)
|
||||||
@@ -1598,16 +1673,16 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
|||||||
@callback
|
@callback
|
||||||
async def _async_climate_changed(self, event):
|
async def _async_climate_changed(self, event):
|
||||||
"""Handle unerdlying climate state changes.
|
"""Handle unerdlying climate state changes.
|
||||||
This method takes the underlying values and update the VTherm with them.
|
This method takes the underlying values and update the VTherm with them.
|
||||||
To avoid loops (issues #121 #101 #95 #99), we discard the event if it is received
|
To avoid loops (issues #121 #101 #95 #99), we discard the event if it is received
|
||||||
less than 10 sec after the last command. What we want here is to take the values
|
less than 10 sec after the last command. What we want here is to take the values
|
||||||
from underlyings ONLY if someone have change directly on the underlying and not
|
from underlyings ONLY if someone have change directly on the underlying and not
|
||||||
as a return of the command. The only thing we take all the time is the HVACAction
|
as a return of the command. The only thing we take all the time is the HVACAction
|
||||||
which is important for feedaback and which cannot generates loops.
|
which is important for feedaback and which cannot generates loops.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
async def end_climate_changed(changes):
|
async def end_climate_changed(changes):
|
||||||
""" To end the event management"""
|
"""To end the event management"""
|
||||||
if changes:
|
if changes:
|
||||||
self.async_write_ha_state()
|
self.async_write_ha_state()
|
||||||
self.update_custom_attributes()
|
self.update_custom_attributes()
|
||||||
@@ -1633,14 +1708,22 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
|||||||
else None
|
else None
|
||||||
)
|
)
|
||||||
|
|
||||||
old_state_date_changed = old_state.last_changed if old_state and old_state.last_changed else None
|
old_state_date_changed = (
|
||||||
old_state_date_updated = old_state.last_updated if old_state and old_state.last_updated else None
|
old_state.last_changed if old_state and old_state.last_changed else None
|
||||||
new_state_date_changed = new_state.last_changed if new_state and new_state.last_changed else None
|
)
|
||||||
new_state_date_updated = new_state.last_updated if new_state and new_state.last_updated else None
|
old_state_date_updated = (
|
||||||
|
old_state.last_updated if old_state and old_state.last_updated else None
|
||||||
|
)
|
||||||
|
new_state_date_changed = (
|
||||||
|
new_state.last_changed if new_state and new_state.last_changed else None
|
||||||
|
)
|
||||||
|
new_state_date_updated = (
|
||||||
|
new_state.last_updated if new_state and new_state.last_updated else None
|
||||||
|
)
|
||||||
|
|
||||||
# Issue 99 - some AC turn hvac_mode=cool and hvac_action=idle when sending a HVACMode_OFF command
|
# Issue 99 - some AC turn hvac_mode=cool and hvac_action=idle when sending a HVACMode_OFF command
|
||||||
# Issue 114 - Remove this because hvac_mode is now managed by local _hvac_mode and use idle action as is
|
# Issue 114 - Remove this because hvac_mode is now managed by local _hvac_mode and use idle action as is
|
||||||
#if self._hvac_mode == HVACMode.OFF and new_hvac_action == HVACAction.IDLE:
|
# if self._hvac_mode == HVACMode.OFF and new_hvac_action == HVACAction.IDLE:
|
||||||
# _LOGGER.debug("The underlying switch to idle instead of OFF. We will consider it as OFF")
|
# _LOGGER.debug("The underlying switch to idle instead of OFF. We will consider it as OFF")
|
||||||
# new_hvac_mode = HVACMode.OFF
|
# new_hvac_mode = HVACMode.OFF
|
||||||
|
|
||||||
@@ -1653,7 +1736,15 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
|||||||
old_hvac_action,
|
old_hvac_action,
|
||||||
)
|
)
|
||||||
|
|
||||||
_LOGGER.debug("%s - last_change_time=%s old_state_date_changed=%s old_state_date_updated=%s new_state_date_changed=%s new_state_date_updated=%s", self, self._last_change_time, old_state_date_changed, old_state_date_updated, new_state_date_changed, new_state_date_updated)
|
_LOGGER.debug(
|
||||||
|
"%s - last_change_time=%s old_state_date_changed=%s old_state_date_updated=%s new_state_date_changed=%s new_state_date_updated=%s",
|
||||||
|
self,
|
||||||
|
self._last_change_time,
|
||||||
|
old_state_date_changed,
|
||||||
|
old_state_date_updated,
|
||||||
|
new_state_date_changed,
|
||||||
|
new_state_date_updated,
|
||||||
|
)
|
||||||
|
|
||||||
# Interpretation of hvac action
|
# Interpretation of hvac action
|
||||||
HVAC_ACTION_ON = [ # pylint: disable=invalid-name
|
HVAC_ACTION_ON = [ # pylint: disable=invalid-name
|
||||||
@@ -1699,21 +1790,27 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
|||||||
if new_state_date_updated and self._last_change_time:
|
if new_state_date_updated and self._last_change_time:
|
||||||
delta = (new_state_date_updated - self._last_change_time).total_seconds()
|
delta = (new_state_date_updated - self._last_change_time).total_seconds()
|
||||||
if delta < 10:
|
if delta < 10:
|
||||||
_LOGGER.info("%s - underlying event is received less than 10 sec after command. Forget it to avoid loop", self
|
_LOGGER.info(
|
||||||
|
"%s - underlying event is received less than 10 sec after command. Forget it to avoid loop",
|
||||||
|
self,
|
||||||
)
|
)
|
||||||
await end_climate_changed(changes)
|
await end_climate_changed(changes)
|
||||||
return
|
return
|
||||||
|
|
||||||
if new_hvac_mode in [
|
if (
|
||||||
HVACMode.OFF,
|
new_hvac_mode
|
||||||
HVACMode.HEAT,
|
in [
|
||||||
HVACMode.COOL,
|
HVACMode.OFF,
|
||||||
HVACMode.HEAT_COOL,
|
HVACMode.HEAT,
|
||||||
HVACMode.DRY,
|
HVACMode.COOL,
|
||||||
HVACMode.AUTO,
|
HVACMode.HEAT_COOL,
|
||||||
HVACMode.FAN_ONLY,
|
HVACMode.DRY,
|
||||||
None
|
HVACMode.AUTO,
|
||||||
] and self._hvac_mode != new_hvac_mode:
|
HVACMode.FAN_ONLY,
|
||||||
|
None,
|
||||||
|
]
|
||||||
|
and self._hvac_mode != new_hvac_mode
|
||||||
|
):
|
||||||
changes = True
|
changes = True
|
||||||
self._hvac_mode = new_hvac_mode
|
self._hvac_mode = new_hvac_mode
|
||||||
# Update all underlyings state
|
# Update all underlyings state
|
||||||
@@ -1723,15 +1820,27 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
|||||||
|
|
||||||
if not changes:
|
if not changes:
|
||||||
# try to manage new target temperature set if state
|
# try to manage new target temperature set if state
|
||||||
_LOGGER.debug("Do temperature check. temperature is %s, new_state.attributes is %s", self.target_temperature, new_state.attributes)
|
_LOGGER.debug(
|
||||||
if self._is_over_climate and new_state.attributes and (new_target_temp := new_state.attributes.get("temperature")) and new_target_temp != self.target_temperature:
|
"Do temperature check. temperature is %s, new_state.attributes is %s",
|
||||||
_LOGGER.info("%s - Target temp in underlying have change to %s", self, new_target_temp)
|
self.target_temperature,
|
||||||
await self.async_set_temperature(temperature = new_target_temp)
|
new_state.attributes,
|
||||||
|
)
|
||||||
|
if (
|
||||||
|
self._is_over_climate
|
||||||
|
and new_state.attributes
|
||||||
|
and (new_target_temp := new_state.attributes.get("temperature"))
|
||||||
|
and new_target_temp != self.target_temperature
|
||||||
|
):
|
||||||
|
_LOGGER.info(
|
||||||
|
"%s - Target temp in underlying have change to %s",
|
||||||
|
self,
|
||||||
|
new_target_temp,
|
||||||
|
)
|
||||||
|
await self.async_set_temperature(temperature=new_target_temp)
|
||||||
changes = True
|
changes = True
|
||||||
|
|
||||||
await end_climate_changed(changes)
|
await end_climate_changed(changes)
|
||||||
|
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
async def _async_update_temp(self, state: State):
|
async def _async_update_temp(self, state: State):
|
||||||
"""Update thermostat with latest state from sensor."""
|
"""Update thermostat with latest state from sensor."""
|
||||||
@@ -1870,25 +1979,25 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
|||||||
if self._attr_preset_mode not in [PRESET_BOOST, PRESET_COMFORT, PRESET_ECO]:
|
if self._attr_preset_mode not in [PRESET_BOOST, PRESET_COMFORT, PRESET_ECO]:
|
||||||
return
|
return
|
||||||
|
|
||||||
# Change temperature with preset named _way
|
# Change temperature with preset named _away
|
||||||
new_temp = None
|
# new_temp = None
|
||||||
if new_state == STATE_ON or new_state == STATE_HOME:
|
#if new_state == STATE_ON or new_state == STATE_HOME:
|
||||||
new_temp = self._presets[self._attr_preset_mode]
|
# new_temp = self._presets[self._attr_preset_mode]
|
||||||
_LOGGER.info(
|
# _LOGGER.info(
|
||||||
"%s - Someone is back home. Restoring temperature to %.2f",
|
# "%s - Someone is back home. Restoring temperature to %.2f",
|
||||||
self,
|
# self,
|
||||||
new_temp,
|
# new_temp,
|
||||||
)
|
# )
|
||||||
else:
|
#else:
|
||||||
new_temp = self._presets_away[
|
# new_temp = self._presets_away[
|
||||||
self.get_preset_away_name(self._attr_preset_mode)
|
# self.get_preset_away_name(self._attr_preset_mode)
|
||||||
]
|
# ]
|
||||||
_LOGGER.info(
|
# _LOGGER.info(
|
||||||
"%s - No one is at home. Apply temperature %.2f",
|
# "%s - No one is at home. Apply temperature %.2f",
|
||||||
self,
|
# self,
|
||||||
new_temp,
|
# new_temp,
|
||||||
)
|
# )
|
||||||
|
new_temp = self.find_preset_temp(self.preset_mode)
|
||||||
if new_temp is not None:
|
if new_temp is not None:
|
||||||
_LOGGER.debug(
|
_LOGGER.debug(
|
||||||
"%s - presence change in temperature mode new_temp will be: %.2f",
|
"%s - presence change in temperature mode new_temp will be: %.2f",
|
||||||
@@ -2174,13 +2283,19 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Issue 99 - a climate is regulated by the device itself and not by VTherm. So a VTherm should never be in security !
|
# Issue 99 - a climate is regulated by the device itself and not by VTherm. So a VTherm should never be in security !
|
||||||
shouldClimateBeInSecurity = False # temp_cond and climate_cond
|
shouldClimateBeInSecurity = False # temp_cond and climate_cond
|
||||||
shouldSwitchBeInSecurity = temp_cond and switch_cond
|
shouldSwitchBeInSecurity = temp_cond and switch_cond
|
||||||
shouldBeInSecurity = shouldClimateBeInSecurity or shouldSwitchBeInSecurity
|
shouldBeInSecurity = shouldClimateBeInSecurity or shouldSwitchBeInSecurity
|
||||||
|
|
||||||
shouldStartSecurity = mode_cond and not self._security_state and shouldBeInSecurity
|
shouldStartSecurity = (
|
||||||
|
mode_cond and not self._security_state and shouldBeInSecurity
|
||||||
|
)
|
||||||
# attr_preset_mode is not necessary normaly. It is just here to be sure
|
# attr_preset_mode is not necessary normaly. It is just here to be sure
|
||||||
shouldStopSecurity = self._security_state and not shouldBeInSecurity and self._attr_preset_mode == PRESET_SECURITY
|
shouldStopSecurity = (
|
||||||
|
self._security_state
|
||||||
|
and not shouldBeInSecurity
|
||||||
|
and self._attr_preset_mode == PRESET_SECURITY
|
||||||
|
)
|
||||||
|
|
||||||
# Logging and event
|
# Logging and event
|
||||||
if shouldStartSecurity:
|
if shouldStartSecurity:
|
||||||
@@ -2342,7 +2457,10 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
|||||||
_LOGGER.debug("%s - recalculate all", self)
|
_LOGGER.debug("%s - recalculate all", self)
|
||||||
if not self._is_over_climate:
|
if not self._is_over_climate:
|
||||||
self._prop_algorithm.calculate(
|
self._prop_algorithm.calculate(
|
||||||
self._target_temp, self._cur_temp, self._cur_ext_temp
|
self._target_temp,
|
||||||
|
self._cur_temp,
|
||||||
|
self._cur_ext_temp,
|
||||||
|
self._hvac_mode == HVACMode.COOL,
|
||||||
)
|
)
|
||||||
self.update_custom_attributes()
|
self.update_custom_attributes()
|
||||||
self.async_write_ha_state()
|
self.async_write_ha_state()
|
||||||
@@ -2390,6 +2508,7 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
|||||||
"target_temp": self.target_temperature,
|
"target_temp": self.target_temperature,
|
||||||
"current_temp": self._cur_temp,
|
"current_temp": self._cur_temp,
|
||||||
"ext_current_temperature": self._cur_ext_temp,
|
"ext_current_temperature": self._cur_ext_temp,
|
||||||
|
"ac_mode": self._ac_mode,
|
||||||
"current_power": self._current_power,
|
"current_power": self._current_power,
|
||||||
"current_power_max": self._current_power_max,
|
"current_power_max": self._current_power_max,
|
||||||
"saved_preset_mode": self._saved_preset_mode,
|
"saved_preset_mode": self._saved_preset_mode,
|
||||||
@@ -2429,18 +2548,18 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
|||||||
"max_power_sensor_entity_id": self._max_power_sensor_entity_id,
|
"max_power_sensor_entity_id": self._max_power_sensor_entity_id,
|
||||||
}
|
}
|
||||||
if self._is_over_climate:
|
if self._is_over_climate:
|
||||||
self._attr_extra_state_attributes["underlying_climate_0"] = self._underlyings[
|
self._attr_extra_state_attributes[
|
||||||
0
|
"underlying_climate_0"
|
||||||
].entity_id
|
] = self._underlyings[0].entity_id
|
||||||
self._attr_extra_state_attributes["underlying_climate_1"] = self._underlyings[
|
self._attr_extra_state_attributes["underlying_climate_1"] = (
|
||||||
1
|
self._underlyings[1].entity_id if len(self._underlyings) > 1 else None
|
||||||
].entity_id if len(self._underlyings) > 1 else None
|
)
|
||||||
self._attr_extra_state_attributes["underlying_climate_2"] = self._underlyings[
|
self._attr_extra_state_attributes["underlying_climate_2"] = (
|
||||||
2
|
self._underlyings[2].entity_id if len(self._underlyings) > 2 else None
|
||||||
].entity_id if len(self._underlyings) > 2 else None
|
)
|
||||||
self._attr_extra_state_attributes["underlying_climate_3"] = self._underlyings[
|
self._attr_extra_state_attributes["underlying_climate_3"] = (
|
||||||
3
|
self._underlyings[3].entity_id if len(self._underlyings) > 3 else None
|
||||||
].entity_id if len(self._underlyings) > 3 else None
|
)
|
||||||
|
|
||||||
self._attr_extra_state_attributes[
|
self._attr_extra_state_attributes[
|
||||||
"start_hvac_action_date"
|
"start_hvac_action_date"
|
||||||
@@ -2532,7 +2651,9 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
|||||||
# If the changed preset is active, change the current temperature
|
# If the changed preset is active, change the current temperature
|
||||||
# Issue #119 - reload new preset temperature also in ac mode
|
# Issue #119 - reload new preset temperature also in ac mode
|
||||||
if preset.startswith(self._attr_preset_mode):
|
if preset.startswith(self._attr_preset_mode):
|
||||||
await self._async_set_preset_mode_internal(preset.rstrip(PRESET_AC_SUFFIX), force=True)
|
await self._async_set_preset_mode_internal(
|
||||||
|
preset.rstrip(PRESET_AC_SUFFIX), force=True
|
||||||
|
)
|
||||||
await self._async_control_heating(force=True)
|
await self._async_control_heating(force=True)
|
||||||
|
|
||||||
async def service_set_security(self, delay_min, min_on_percent, default_on_percent):
|
async def service_set_security(self, delay_min, min_on_percent, default_on_percent):
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ from .const import (
|
|||||||
CONF_WINDOW_AUTO_OPEN_THRESHOLD,
|
CONF_WINDOW_AUTO_OPEN_THRESHOLD,
|
||||||
CONF_MOTION_SENSOR,
|
CONF_MOTION_SENSOR,
|
||||||
CONF_MOTION_DELAY,
|
CONF_MOTION_DELAY,
|
||||||
|
CONF_MOTION_OFF_DELAY,
|
||||||
CONF_MOTION_PRESET,
|
CONF_MOTION_PRESET,
|
||||||
CONF_NO_MOTION_PRESET,
|
CONF_NO_MOTION_PRESET,
|
||||||
CONF_DEVICE_POWER,
|
CONF_DEVICE_POWER,
|
||||||
@@ -226,6 +227,7 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
|
|||||||
PROPORTIONAL_FUNCTION_TPI,
|
PROPORTIONAL_FUNCTION_TPI,
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
|
vol.Optional(CONF_AC_MODE, default=False): cv.boolean,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -292,6 +294,7 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
vol.Optional(CONF_MOTION_DELAY, default=30): cv.positive_int,
|
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(
|
vol.Optional(CONF_MOTION_PRESET, default="comfort"): vol.In(
|
||||||
CONF_PRESETS_SELECTIONABLE
|
CONF_PRESETS_SELECTIONABLE
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ CONF_CYCLE_MIN = "cycle_min"
|
|||||||
CONF_PROP_FUNCTION = "proportional_function"
|
CONF_PROP_FUNCTION = "proportional_function"
|
||||||
CONF_WINDOW_DELAY = "window_delay"
|
CONF_WINDOW_DELAY = "window_delay"
|
||||||
CONF_MOTION_DELAY = "motion_delay"
|
CONF_MOTION_DELAY = "motion_delay"
|
||||||
|
CONF_MOTION_OFF_DELAY = "motion_off_delay"
|
||||||
CONF_MOTION_PRESET = "motion_preset"
|
CONF_MOTION_PRESET = "motion_preset"
|
||||||
CONF_NO_MOTION_PRESET = "no_motion_preset"
|
CONF_NO_MOTION_PRESET = "no_motion_preset"
|
||||||
CONF_TPI_COEF_INT = "tpi_coef_int"
|
CONF_TPI_COEF_INT = "tpi_coef_int"
|
||||||
|
|||||||
@@ -45,19 +45,33 @@ class PropAlgorithm:
|
|||||||
self._default_on_percent = 0
|
self._default_on_percent = 0
|
||||||
|
|
||||||
def calculate(
|
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"""
|
"""Do the calculation of the duration"""
|
||||||
if target_temp is None or current_temp is None:
|
if target_temp is None or current_temp is None:
|
||||||
_LOGGER.warning(
|
_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
|
self._calculated_on_percent = 0
|
||||||
else:
|
else:
|
||||||
delta_temp = target_temp - current_temp
|
if cooling:
|
||||||
delta_ext_temp = (
|
delta_temp = current_temp - target_temp
|
||||||
target_temp - ext_current_temp if ext_current_temp is not None else 0
|
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:
|
if self._function == PROPORTIONAL_FUNCTION_TPI:
|
||||||
self._calculated_on_percent = (
|
self._calculated_on_percent = (
|
||||||
|
|||||||
@@ -92,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",
|
"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": {
|
"data": {
|
||||||
"motion_sensor_entity_id": "Motion sensor entity id",
|
"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",
|
"motion_preset": "Preset to use when motion is detected",
|
||||||
"no_motion_preset": "Preset to use when no motion is detected"
|
"no_motion_preset": "Preset to use when no motion is detected"
|
||||||
}
|
}
|
||||||
@@ -237,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",
|
"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": {
|
"data": {
|
||||||
"motion_sensor_entity_id": "Motion sensor entity id",
|
"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",
|
"motion_preset": "Preset to use when motion is detected",
|
||||||
"no_motion_preset": "Preset to use when no motion is detected"
|
"no_motion_preset": "Preset to use when no motion is detected"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -92,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",
|
"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": {
|
"data": {
|
||||||
"motion_sensor_entity_id": "Motion sensor entity id",
|
"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",
|
"motion_preset": "Preset to use when motion is detected",
|
||||||
"no_motion_preset": "Preset to use when no motion is detected"
|
"no_motion_preset": "Preset to use when no motion is detected"
|
||||||
}
|
}
|
||||||
@@ -237,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",
|
"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": {
|
"data": {
|
||||||
"motion_sensor_entity_id": "Motion sensor entity id",
|
"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",
|
"motion_preset": "Preset to use when motion is detected",
|
||||||
"no_motion_preset": "Preset to use when no motion is detected"
|
"no_motion_preset": "Preset to use when no motion is detected"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -91,8 +91,16 @@
|
|||||||
"title": "Gestion de la détection de mouvement",
|
"title": "Gestion de la détection de mouvement",
|
||||||
"description": "Le preset s'ajuste automatiquement si un mouvement est détecté\nLaissez l'entity id vide si non utilisé.\n'Preset mouvement' et 'Preset sans mouvement' doivent être choisis avec les preset à utiliser.",
|
"description": "Le preset s'ajuste automatiquement si un mouvement est détecté\nLaissez l'entity id vide si non utilisé.\n'Preset mouvement' et 'Preset sans mouvement' doivent être choisis avec les preset à utiliser.",
|
||||||
"data": {
|
"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_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é",
|
"motion_preset": "Preset à utiliser si mouvement détecté",
|
||||||
"no_motion_preset": "Preset à utiliser si pas de mouvement détecté"
|
"no_motion_preset": "Preset à utiliser si pas de mouvement détecté"
|
||||||
}
|
}
|
||||||
@@ -237,8 +245,16 @@
|
|||||||
"title": "Gestion de la détection de mouvement",
|
"title": "Gestion de la détection de mouvement",
|
||||||
"description": "Le preset s'ajuste automatiquement si un mouvement est détecté\nLaissez l'entity id vide si non utilisé.\n'Preset mouvement' et 'Preset sans mouvement' doivent être choisis avec les preset à utiliser.",
|
"description": "Le preset s'ajuste automatiquement si un mouvement est détecté\nLaissez l'entity id vide si non utilisé.\n'Preset mouvement' et 'Preset sans mouvement' doivent être choisis avec les preset à utiliser.",
|
||||||
"data": {
|
"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_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é",
|
"motion_preset": "Preset à utiliser si mouvement détecté",
|
||||||
"no_motion_preset": "Preset à utiliser si pas de mouvement détecté"
|
"no_motion_preset": "Preset à utiliser si pas de mouvement détecté"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -93,6 +93,7 @@
|
|||||||
"data": {
|
"data": {
|
||||||
"motion_sensor_entity_id": "Entity id sensore di movimento",
|
"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_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",
|
"motion_preset": "Preset da utilizzare quando viene rilevato il movimento",
|
||||||
"no_motion_preset": "Preset da utilizzare quando non viene rilevato il movimento"
|
"no_motion_preset": "Preset da utilizzare quando non viene rilevato il movimento"
|
||||||
}
|
}
|
||||||
@@ -238,6 +239,7 @@
|
|||||||
"data": {
|
"data": {
|
||||||
"motion_sensor_entity_id": "Entity id sensore di movimento",
|
"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_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",
|
"motion_preset": "Preset da utilizzare quando viene rilevato il movimento",
|
||||||
"no_motion_preset": "Preset da utilizzare quando non viene rilevato il movimento"
|
"no_motion_preset": "Preset da utilizzare quando non viene rilevato il movimento"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
""" Underlying entities classes """
|
""" Underlying entities classes """
|
||||||
import logging
|
import logging
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
from enum import StrEnum
|
||||||
|
|
||||||
from homeassistant.const import ATTR_ENTITY_ID, STATE_ON, UnitOfTemperature
|
from homeassistant.const import ATTR_ENTITY_ID, STATE_ON, UnitOfTemperature
|
||||||
|
|
||||||
from homeassistant.exceptions import ServiceNotFound
|
from homeassistant.exceptions import ServiceNotFound
|
||||||
|
|
||||||
from enum import StrEnum
|
|
||||||
from homeassistant.core import HomeAssistant, DOMAIN as HA_DOMAIN, CALLBACK_TYPE
|
from homeassistant.core import HomeAssistant, DOMAIN as HA_DOMAIN, CALLBACK_TYPE
|
||||||
from homeassistant.components.climate import (
|
from homeassistant.components.climate import (
|
||||||
ClimateEntity,
|
ClimateEntity,
|
||||||
@@ -246,7 +246,7 @@ class UnderlyingSwitch(UnderlyingEntity):
|
|||||||
return
|
return
|
||||||
|
|
||||||
# If we should heat, starts the cycle with delay
|
# If we should heat, starts the cycle with delay
|
||||||
if self._hvac_mode == HVACMode.HEAT and on_time_sec > 0:
|
if self._hvac_mode in [HVACMode.HEAT, HVACMode.COOL] and on_time_sec > 0:
|
||||||
# Starts the cycle after the initial delay
|
# Starts the cycle after the initial delay
|
||||||
self._async_cancel_cycle = self.call_later(
|
self._async_cancel_cycle = self.call_later(
|
||||||
self._hass, self._initial_delay_sec, self._turn_on_later
|
self._hass, self._initial_delay_sec, self._turn_on_later
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 46 KiB |
@@ -2,4 +2,5 @@
|
|||||||
-r requirements_dev.txt
|
-r requirements_dev.txt
|
||||||
aiodiscover
|
aiodiscover
|
||||||
ulid_transform
|
ulid_transform
|
||||||
|
pytest-asyncio
|
||||||
pytest-homeassistant-custom-component
|
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
|
||||||
@@ -5,7 +5,7 @@ from unittest.mock import patch, MagicMock
|
|||||||
import pytest # pylint: disable=unused-import
|
import pytest # pylint: disable=unused-import
|
||||||
|
|
||||||
from homeassistant.core import HomeAssistant, Event, EVENT_STATE_CHANGED, State
|
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.config_entries import ConfigEntryState
|
||||||
from homeassistant.util import dt as dt_util
|
from homeassistant.util import dt as dt_util
|
||||||
@@ -20,23 +20,26 @@ from homeassistant.components.climate import (
|
|||||||
|
|
||||||
from pytest_homeassistant_custom_component.common import MockConfigEntry
|
from pytest_homeassistant_custom_component.common import MockConfigEntry
|
||||||
|
|
||||||
from ..climate import VersatileThermostat
|
from custom_components.versatile_thermostat.climate import VersatileThermostat
|
||||||
from ..const import * # pylint: disable=wildcard-import, unused-wildcard-import
|
from custom_components.versatile_thermostat.const import * # pylint: disable=wildcard-import, unused-wildcard-import
|
||||||
from ..underlyings 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
|
from .const import ( # pylint: disable=unused-import
|
||||||
MOCK_TH_OVER_SWITCH_USER_CONFIG,
|
MOCK_TH_OVER_SWITCH_USER_CONFIG,
|
||||||
MOCK_TH_OVER_4SWITCH_USER_CONFIG,
|
MOCK_TH_OVER_4SWITCH_USER_CONFIG,
|
||||||
MOCK_TH_OVER_CLIMATE_USER_CONFIG,
|
MOCK_TH_OVER_CLIMATE_USER_CONFIG,
|
||||||
MOCK_TH_OVER_SWITCH_TYPE_CONFIG,
|
MOCK_TH_OVER_SWITCH_TYPE_CONFIG,
|
||||||
|
MOCK_TH_OVER_SWITCH_AC_TYPE_CONFIG,
|
||||||
MOCK_TH_OVER_4SWITCH_TYPE_CONFIG,
|
MOCK_TH_OVER_4SWITCH_TYPE_CONFIG,
|
||||||
MOCK_TH_OVER_CLIMATE_TYPE_CONFIG,
|
MOCK_TH_OVER_CLIMATE_TYPE_CONFIG,
|
||||||
MOCK_TH_OVER_SWITCH_TPI_CONFIG,
|
MOCK_TH_OVER_SWITCH_TPI_CONFIG,
|
||||||
MOCK_PRESETS_CONFIG,
|
MOCK_PRESETS_CONFIG,
|
||||||
|
MOCK_PRESETS_AC_CONFIG,
|
||||||
MOCK_WINDOW_CONFIG,
|
MOCK_WINDOW_CONFIG,
|
||||||
MOCK_MOTION_CONFIG,
|
MOCK_MOTION_CONFIG,
|
||||||
MOCK_POWER_CONFIG,
|
MOCK_POWER_CONFIG,
|
||||||
MOCK_PRESENCE_CONFIG,
|
MOCK_PRESENCE_CONFIG,
|
||||||
|
MOCK_PRESENCE_AC_CONFIG,
|
||||||
MOCK_ADVANCED_CONFIG,
|
MOCK_ADVANCED_CONFIG,
|
||||||
# MOCK_DEFAULT_FEATURE_CONFIG,
|
# MOCK_DEFAULT_FEATURE_CONFIG,
|
||||||
PRESET_BOOST,
|
PRESET_BOOST,
|
||||||
@@ -58,6 +61,19 @@ FULL_SWITCH_CONFIG = (
|
|||||||
| MOCK_ADVANCED_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 = (
|
PARTIAL_CLIMATE_CONFIG = (
|
||||||
MOCK_TH_OVER_CLIMATE_USER_CONFIG
|
MOCK_TH_OVER_CLIMATE_USER_CONFIG
|
||||||
| MOCK_TH_OVER_CLIMATE_TYPE_CONFIG
|
| MOCK_TH_OVER_CLIMATE_TYPE_CONFIG
|
||||||
@@ -83,7 +99,7 @@ _LOGGER = logging.getLogger(__name__)
|
|||||||
class MockClimate(ClimateEntity):
|
class MockClimate(ClimateEntity):
|
||||||
"""A Mock Climate class used for Underlying climate mode"""
|
"""A Mock Climate class used for Underlying climate mode"""
|
||||||
|
|
||||||
def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos, hvac_mode:HVACMode = HVACMode.OFF) -> None:
|
def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos, hvac_mode:HVACMode = HVACMode.OFF) -> None: # pylint: disable=unused-argument
|
||||||
"""Initialize the thermostat."""
|
"""Initialize the thermostat."""
|
||||||
|
|
||||||
super().__init__()
|
super().__init__()
|
||||||
@@ -101,12 +117,13 @@ class MockClimate(ClimateEntity):
|
|||||||
self._attr_target_temperature = 20
|
self._attr_target_temperature = 20
|
||||||
self._attr_current_temperature = 15
|
self._attr_current_temperature = 15
|
||||||
|
|
||||||
def set_temperature(self, temperature):
|
def set_temperature(self, **kwargs):
|
||||||
""" Set the target temperature"""
|
""" Set the target temperature"""
|
||||||
|
temperature = kwargs.get(ATTR_TEMPERATURE)
|
||||||
self._attr_target_temperature = temperature
|
self._attr_target_temperature = temperature
|
||||||
self.async_write_ha_state()
|
self.async_write_ha_state()
|
||||||
|
|
||||||
def async_set_hvac_mode(self, hvac_mode):
|
async def async_set_hvac_mode(self, hvac_mode):
|
||||||
""" The hvac mode"""
|
""" The hvac mode"""
|
||||||
self._attr_hvac_mode = hvac_mode
|
self._attr_hvac_mode = hvac_mode
|
||||||
self.async_write_ha_state()
|
self.async_write_ha_state()
|
||||||
@@ -114,7 +131,7 @@ class MockClimate(ClimateEntity):
|
|||||||
class MockUnavailableClimate(ClimateEntity):
|
class MockUnavailableClimate(ClimateEntity):
|
||||||
"""A Mock Climate class used for Underlying climate mode"""
|
"""A Mock Climate class used for Underlying climate mode"""
|
||||||
|
|
||||||
def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None:
|
def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None: # pylint: disable=unused-argument
|
||||||
"""Initialize the thermostat."""
|
"""Initialize the thermostat."""
|
||||||
|
|
||||||
super().__init__()
|
super().__init__()
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
"""Global fixtures for integration_blueprint integration."""
|
"""Global fixtures for integration_blueprint integration."""
|
||||||
|
# pylint: disable=line-too-long
|
||||||
|
|
||||||
# Fixtures allow you to replace functions with a Mock object. You can perform
|
# 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
|
# 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.
|
# 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.
|
# This fixture enables loading custom integrations in all tests.
|
||||||
# Remove to enable selective use of this fixture
|
# Remove to enable selective use of this fixture
|
||||||
@pytest.fixture(autouse=True)
|
@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"""
|
"""Enable all integration in tests"""
|
||||||
yield
|
yield
|
||||||
|
|
||||||
@@ -40,6 +40,7 @@ from custom_components.versatile_thermostat.const import (
|
|||||||
CONF_WINDOW_AUTO_MAX_DURATION,
|
CONF_WINDOW_AUTO_MAX_DURATION,
|
||||||
CONF_MOTION_SENSOR,
|
CONF_MOTION_SENSOR,
|
||||||
CONF_MOTION_DELAY,
|
CONF_MOTION_DELAY,
|
||||||
|
CONF_MOTION_OFF_DELAY,
|
||||||
CONF_MOTION_PRESET,
|
CONF_MOTION_PRESET,
|
||||||
CONF_NO_MOTION_PRESET,
|
CONF_NO_MOTION_PRESET,
|
||||||
CONF_POWER_SENSOR,
|
CONF_POWER_SENSOR,
|
||||||
@@ -50,7 +51,6 @@ from custom_components.versatile_thermostat.const import (
|
|||||||
PRESET_AWAY_SUFFIX,
|
PRESET_AWAY_SUFFIX,
|
||||||
CONF_CLIMATE,
|
CONF_CLIMATE,
|
||||||
)
|
)
|
||||||
|
|
||||||
MOCK_TH_OVER_SWITCH_USER_CONFIG = {
|
MOCK_TH_OVER_SWITCH_USER_CONFIG = {
|
||||||
CONF_NAME: "TheOverSwitchMockName",
|
CONF_NAME: "TheOverSwitchMockName",
|
||||||
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_SWITCH,
|
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_SWITCH,
|
||||||
@@ -96,6 +96,13 @@ MOCK_TH_OVER_CLIMATE_USER_CONFIG = {
|
|||||||
MOCK_TH_OVER_SWITCH_TYPE_CONFIG = {
|
MOCK_TH_OVER_SWITCH_TYPE_CONFIG = {
|
||||||
CONF_HEATER: "switch.mock_switch",
|
CONF_HEATER: "switch.mock_switch",
|
||||||
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
|
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 = {
|
MOCK_TH_OVER_4SWITCH_TYPE_CONFIG = {
|
||||||
@@ -104,6 +111,7 @@ MOCK_TH_OVER_4SWITCH_TYPE_CONFIG = {
|
|||||||
CONF_HEATER_3: "switch.mock_4switch2",
|
CONF_HEATER_3: "switch.mock_4switch2",
|
||||||
CONF_HEATER_4: "switch.mock_4switch3",
|
CONF_HEATER_4: "switch.mock_4switch3",
|
||||||
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
|
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
|
||||||
|
CONF_AC_MODE: False,
|
||||||
}
|
}
|
||||||
|
|
||||||
MOCK_TH_OVER_SWITCH_TPI_CONFIG = {
|
MOCK_TH_OVER_SWITCH_TPI_CONFIG = {
|
||||||
@@ -122,6 +130,15 @@ MOCK_PRESETS_CONFIG = {
|
|||||||
PRESET_BOOST + "_temp": 18,
|
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 = {
|
MOCK_WINDOW_CONFIG = {
|
||||||
CONF_WINDOW_SENSOR: "binary_sensor.window_sensor",
|
CONF_WINDOW_SENSOR: "binary_sensor.window_sensor",
|
||||||
CONF_WINDOW_DELAY: 10,
|
CONF_WINDOW_DELAY: 10,
|
||||||
@@ -136,6 +153,7 @@ MOCK_WINDOW_AUTO_CONFIG = {
|
|||||||
MOCK_MOTION_CONFIG = {
|
MOCK_MOTION_CONFIG = {
|
||||||
CONF_MOTION_SENSOR: "input_boolean.motion_sensor",
|
CONF_MOTION_SENSOR: "input_boolean.motion_sensor",
|
||||||
CONF_MOTION_DELAY: 10,
|
CONF_MOTION_DELAY: 10,
|
||||||
|
CONF_MOTION_OFF_DELAY: 30,
|
||||||
CONF_MOTION_PRESET: PRESET_COMFORT,
|
CONF_MOTION_PRESET: PRESET_COMFORT,
|
||||||
CONF_NO_MOTION_PRESET: PRESET_ECO,
|
CONF_NO_MOTION_PRESET: PRESET_ECO,
|
||||||
}
|
}
|
||||||
@@ -153,6 +171,16 @@ MOCK_PRESENCE_CONFIG = {
|
|||||||
PRESET_BOOST + PRESET_AWAY_SUFFIX + "_temp": 18,
|
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 = {
|
MOCK_ADVANCED_CONFIG = {
|
||||||
CONF_MINIMAL_ACTIVATION_DELAY: 10,
|
CONF_MINIMAL_ACTIVATION_DELAY: 10,
|
||||||
CONF_SECURITY_DELAY_MIN: 5,
|
CONF_SECURITY_DELAY_MIN: 5,
|
||||||
@@ -9,9 +9,8 @@ from homeassistant.components.binary_sensor import BinarySensorDeviceClass
|
|||||||
|
|
||||||
from pytest_homeassistant_custom_component.common import MockConfigEntry
|
from pytest_homeassistant_custom_component.common import MockConfigEntry
|
||||||
|
|
||||||
from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import
|
from custom_components.versatile_thermostat.climate import VersatileThermostat
|
||||||
from ..climate import VersatileThermostat
|
from custom_components.versatile_thermostat.binary_sensor import (
|
||||||
from ..binary_sensor import (
|
|
||||||
SecurityBinarySensor,
|
SecurityBinarySensor,
|
||||||
OverpoweringBinarySensor,
|
OverpoweringBinarySensor,
|
||||||
WindowBinarySensor,
|
WindowBinarySensor,
|
||||||
@@ -29,7 +28,7 @@ async def test_security_binary_sensors(
|
|||||||
skip_hass_states_is_state,
|
skip_hass_states_is_state,
|
||||||
skip_turn_on_off_heater,
|
skip_turn_on_off_heater,
|
||||||
skip_send_event,
|
skip_send_event,
|
||||||
):
|
): # pylint: disable=unused-argument
|
||||||
"""Test the security binary sensors in thermostat type"""
|
"""Test the security binary sensors in thermostat type"""
|
||||||
|
|
||||||
entry = MockConfigEntry(
|
entry = MockConfigEntry(
|
||||||
@@ -29,7 +29,7 @@ async def test_show_form(hass: HomeAssistant) -> None:
|
|||||||
|
|
||||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||||
async def test_user_config_flow_over_switch(hass: HomeAssistant, skip_hass_states_get):
|
async def test_user_config_flow_over_switch(hass: HomeAssistant, skip_hass_states_get): # pylint: disable=unused-argument
|
||||||
"""Test the config flow with all thermostat_over_switch features"""
|
"""Test the config flow with all thermostat_over_switch features"""
|
||||||
result = await hass.config_entries.flow.async_init(
|
result = await hass.config_entries.flow.async_init(
|
||||||
DOMAIN, context={"source": SOURCE_USER}
|
DOMAIN, context={"source": SOURCE_USER}
|
||||||
@@ -128,7 +128,7 @@ async def test_user_config_flow_over_switch(hass: HomeAssistant, skip_hass_state
|
|||||||
|
|
||||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||||
async def test_user_config_flow_over_climate(hass: HomeAssistant, skip_hass_states_get):
|
async def test_user_config_flow_over_climate(hass: HomeAssistant, skip_hass_states_get): # pylint: disable=unused-argument
|
||||||
"""Test the config flow with all thermostat_over_climate features and no additional features"""
|
"""Test the config flow with all thermostat_over_climate features and no additional features"""
|
||||||
result = await hass.config_entries.flow.async_init(
|
result = await hass.config_entries.flow.async_init(
|
||||||
DOMAIN, context={"source": SOURCE_USER}
|
DOMAIN, context={"source": SOURCE_USER}
|
||||||
@@ -157,38 +157,6 @@ async def test_user_config_flow_over_climate(hass: HomeAssistant, skip_hass_stat
|
|||||||
result["flow_id"], user_input=MOCK_PRESETS_CONFIG
|
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["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||||
assert result["step_id"] == "advanced"
|
assert result["step_id"] == "advanced"
|
||||||
assert result["errors"] == {}
|
assert result["errors"] == {}
|
||||||
@@ -216,7 +184,7 @@ async def test_user_config_flow_over_climate(hass: HomeAssistant, skip_hass_stat
|
|||||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||||
async def test_user_config_flow_window_auto_ok(
|
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"""
|
"""Test the config flow with only window auto feature"""
|
||||||
result = await hass.config_entries.flow.async_init(
|
result = await hass.config_entries.flow.async_init(
|
||||||
@@ -313,7 +281,7 @@ async def test_user_config_flow_window_auto_ok(
|
|||||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||||
async def test_user_config_flow_window_auto_ko(
|
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"""
|
"""Test the config flow with window auto and window features -> not allowed"""
|
||||||
result = await hass.config_entries.flow.async_init(
|
result = await hass.config_entries.flow.async_init(
|
||||||
@@ -385,7 +353,7 @@ async def test_user_config_flow_window_auto_ko(
|
|||||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||||
async def test_user_config_flow_over_4_switches(
|
async def test_user_config_flow_over_4_switches(
|
||||||
hass: HomeAssistant, skip_hass_states_get, skip_control_heating
|
hass: HomeAssistant, skip_hass_states_get, skip_control_heating # pylint: disable=unused-argument
|
||||||
):
|
):
|
||||||
"""Test the config flow with 4 switchs thermostat_over_switch features"""
|
"""Test the config flow with 4 switchs thermostat_over_switch features"""
|
||||||
|
|
||||||
@@ -410,6 +378,7 @@ async def test_user_config_flow_over_4_switches(
|
|||||||
CONF_HEATER_3: "switch.mock_switch3",
|
CONF_HEATER_3: "switch.mock_switch3",
|
||||||
CONF_HEATER_4: "switch.mock_switch4",
|
CONF_HEATER_4: "switch.mock_switch4",
|
||||||
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
|
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
|
||||||
|
CONF_AC_MODE: False,
|
||||||
}
|
}
|
||||||
|
|
||||||
result = await hass.config_entries.flow.async_init(
|
result = await hass.config_entries.flow.async_init(
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
""" Test the Window management """
|
""" Test the Window management """
|
||||||
import asyncio
|
import asyncio
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
import logging
|
||||||
from unittest.mock import patch, call, PropertyMock
|
from unittest.mock import patch, call, PropertyMock
|
||||||
from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import
|
from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import
|
||||||
from datetime import datetime, timedelta
|
|
||||||
|
|
||||||
import logging
|
|
||||||
|
|
||||||
logging.getLogger().setLevel(logging.DEBUG)
|
logging.getLogger().setLevel(logging.DEBUG)
|
||||||
|
|
||||||
@@ -43,10 +43,11 @@ async def test_movement_management_time_not_enough(
|
|||||||
CONF_TPI_COEF_INT: 0.3,
|
CONF_TPI_COEF_INT: 0.3,
|
||||||
CONF_TPI_COEF_EXT: 0.01,
|
CONF_TPI_COEF_EXT: 0.01,
|
||||||
CONF_MINIMAL_ACTIVATION_DELAY: 30,
|
CONF_MINIMAL_ACTIVATION_DELAY: 30,
|
||||||
CONF_SECURITY_DELAY_MIN: 5,
|
CONF_SECURITY_DELAY_MIN: 10,
|
||||||
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
|
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
|
||||||
CONF_MOTION_SENSOR: "binary_sensor.mock_motion_sensor",
|
CONF_MOTION_SENSOR: "binary_sensor.mock_motion_sensor",
|
||||||
CONF_MOTION_DELAY: 0, # important to not been obliged to wait
|
CONF_MOTION_DELAY: 10, # important to not been obliged to wait
|
||||||
|
CONF_MOTION_OFF_DELAY: 30,
|
||||||
CONF_MOTION_PRESET: "boost",
|
CONF_MOTION_PRESET: "boost",
|
||||||
CONF_NO_MOTION_PRESET: "comfort",
|
CONF_NO_MOTION_PRESET: "comfort",
|
||||||
CONF_PRESENCE_SENSOR: "binary_sensor.mock_presence_sensor",
|
CONF_PRESENCE_SENSOR: "binary_sensor.mock_presence_sensor",
|
||||||
@@ -61,7 +62,7 @@ async def test_movement_management_time_not_enough(
|
|||||||
tz = get_tz(hass) # pylint: disable=invalid-name
|
tz = get_tz(hass) # pylint: disable=invalid-name
|
||||||
now: datetime = datetime.now(tz=tz)
|
now: datetime = datetime.now(tz=tz)
|
||||||
|
|
||||||
# start heating, in boost mode. We block the control_heating to avoid running a cycle
|
# start heating, in boost mode, when someone is present. We block the control_heating to avoid running a cycle
|
||||||
with patch(
|
with patch(
|
||||||
"custom_components.versatile_thermostat.climate.VersatileThermostat._async_control_heating"
|
"custom_components.versatile_thermostat.climate.VersatileThermostat._async_control_heating"
|
||||||
):
|
):
|
||||||
@@ -75,14 +76,14 @@ async def test_movement_management_time_not_enough(
|
|||||||
assert entity.motion_state is None
|
assert entity.motion_state is None
|
||||||
assert entity.presence_state is None
|
assert entity.presence_state is None
|
||||||
|
|
||||||
event_timestamp = now - timedelta(minutes=4)
|
event_timestamp = now - timedelta(minutes=5)
|
||||||
await send_temperature_change_event(entity, 18, event_timestamp)
|
await send_temperature_change_event(entity, 18, event_timestamp)
|
||||||
await send_ext_temperature_change_event(entity, 10, event_timestamp)
|
await send_ext_temperature_change_event(entity, 10, event_timestamp)
|
||||||
|
|
||||||
await send_presence_change_event(entity, True, False, event_timestamp)
|
await send_presence_change_event(entity, True, False, event_timestamp)
|
||||||
assert entity.presence_state is "on"
|
assert entity.presence_state is "on"
|
||||||
|
|
||||||
# starts detecting motion
|
# starts detecting motion with time not enough
|
||||||
with patch(
|
with patch(
|
||||||
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
|
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
|
||||||
) as mock_send_event, patch(
|
) as mock_send_event, patch(
|
||||||
@@ -91,17 +92,21 @@ async def test_movement_management_time_not_enough(
|
|||||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
|
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
|
||||||
) as mock_heater_off, patch(
|
) as mock_heater_off, patch(
|
||||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
|
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
|
||||||
return_value=True,
|
return_value=False,
|
||||||
), patch(
|
), patch(
|
||||||
"homeassistant.helpers.condition.state", return_value=False
|
"homeassistant.helpers.condition.state", return_value=False
|
||||||
):
|
) as mock_condition:
|
||||||
event_timestamp = now - timedelta(minutes=3)
|
event_timestamp = now - timedelta(minutes=4)
|
||||||
await send_motion_change_event(entity, True, False, event_timestamp)
|
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.hvac_mode is HVACMode.HEAT
|
||||||
assert entity.preset_mode is PRESET_ACTIVITY
|
assert entity.preset_mode is PRESET_ACTIVITY
|
||||||
# because no motion is detected yet
|
# because no motion is detected yet
|
||||||
assert entity.target_temperature == 18
|
assert entity.target_temperature == 18
|
||||||
|
# state is not changed if time is not enough
|
||||||
assert entity.motion_state is None
|
assert entity.motion_state is None
|
||||||
assert entity.presence_state is "on"
|
assert entity.presence_state is "on"
|
||||||
|
|
||||||
@@ -111,7 +116,65 @@ async def test_movement_management_time_not_enough(
|
|||||||
assert mock_heater_off.call_count == 0
|
assert mock_heater_off.call_count == 0
|
||||||
assert mock_send_event.call_count == 0
|
assert mock_send_event.call_count == 0
|
||||||
|
|
||||||
# stop detecting motion with confirmation of stop
|
# starts detecting motion with time enough this time
|
||||||
|
with patch(
|
||||||
|
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
|
||||||
|
) 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(
|
with patch(
|
||||||
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
|
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
|
||||||
) as mock_send_event, patch(
|
) as mock_send_event, patch(
|
||||||
@@ -124,8 +187,11 @@ async def test_movement_management_time_not_enough(
|
|||||||
) as mock_device_active, patch(
|
) as mock_device_active, patch(
|
||||||
"homeassistant.helpers.condition.state", return_value=True
|
"homeassistant.helpers.condition.state", return_value=True
|
||||||
) as mock_condition:
|
) as mock_condition:
|
||||||
event_timestamp = now - timedelta(minutes=2)
|
event_timestamp = now - timedelta(minutes=1)
|
||||||
await send_motion_change_event(entity, False, True, event_timestamp)
|
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.hvac_mode is HVACMode.HEAT
|
||||||
assert entity.preset_mode is PRESET_ACTIVITY
|
assert entity.preset_mode is PRESET_ACTIVITY
|
||||||
@@ -135,9 +201,8 @@ async def test_movement_management_time_not_enough(
|
|||||||
assert entity.presence_state is "on"
|
assert entity.presence_state is "on"
|
||||||
|
|
||||||
assert mock_send_event.call_count == 0
|
assert mock_send_event.call_count == 0
|
||||||
# Change is not confirmed
|
# The heater must stop heating now
|
||||||
assert mock_heater_on.call_count == 0
|
assert mock_heater_on.call_count == 0
|
||||||
# Because device is active
|
|
||||||
assert mock_heater_off.call_count == 1
|
assert mock_heater_off.call_count == 1
|
||||||
assert mock_send_event.call_count == 0
|
assert mock_send_event.call_count == 0
|
||||||
|
|
||||||
@@ -405,3 +470,131 @@ async def test_movement_management_time_enoughand_not_presence(
|
|||||||
assert entity.proportional_algorithm.on_percent == 0.11
|
assert entity.proportional_algorithm.on_percent == 0.11
|
||||||
assert mock_heater_off.call_count == 0
|
assert mock_heater_off.call_count == 0
|
||||||
assert mock_send_event.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
|
||||||
|
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
""" Test the Multiple switch management """
|
""" Test the Multiple switch management """
|
||||||
import asyncio
|
import asyncio
|
||||||
from unittest.mock import patch, call, ANY
|
from unittest.mock import patch, call, ANY
|
||||||
from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import
|
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import
|
||||||
|
|
||||||
logging.getLogger().setLevel(logging.DEBUG)
|
logging.getLogger().setLevel(logging.DEBUG)
|
||||||
|
|
||||||
|
|
||||||
@@ -15,7 +15,7 @@ async def test_one_switch_cycle(
|
|||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
skip_hass_states_is_state,
|
skip_hass_states_is_state,
|
||||||
skip_send_event,
|
skip_send_event,
|
||||||
):
|
): # pylint: disable=unused-argument
|
||||||
"""Test that when multiple switch are configured the activation is distributed"""
|
"""Test that when multiple switch are configured the activation is distributed"""
|
||||||
|
|
||||||
tz = get_tz(hass) # pylint: disable=invalid-name
|
tz = get_tz(hass) # pylint: disable=invalid-name
|
||||||
@@ -75,7 +75,7 @@ async def test_one_switch_cycle(
|
|||||||
with patch(
|
with patch(
|
||||||
"homeassistant.core.StateMachine.is_state", return_value=False
|
"homeassistant.core.StateMachine.is_state", return_value=False
|
||||||
) as mock_is_state:
|
) as mock_is_state:
|
||||||
assert entity._is_device_active is False
|
assert entity._is_device_active is False # pylint: disable=protected-access
|
||||||
|
|
||||||
# Should be call for the Switch
|
# Should be call for the Switch
|
||||||
assert mock_is_state.call_count == 1
|
assert mock_is_state.call_count == 1
|
||||||
@@ -132,7 +132,8 @@ async def test_one_switch_cycle(
|
|||||||
assert mock_send_event.call_count == 0
|
assert mock_send_event.call_count == 0
|
||||||
assert mock_heater_off.call_count == 0
|
assert mock_heater_off.call_count == 0
|
||||||
|
|
||||||
# The first heater should be turned on but is already on but because above we mock call_later the heater is not on. But this time it will be really on
|
# The first heater should be turned on but is already on but because above we mock
|
||||||
|
# call_later the heater is not on. But this time it will be really on
|
||||||
assert mock_heater_on.call_count == 1
|
assert mock_heater_on.call_count == 1
|
||||||
|
|
||||||
# Set another temperature at middle level
|
# Set another temperature at middle level
|
||||||
@@ -153,12 +154,15 @@ async def test_one_switch_cycle(
|
|||||||
assert mock_send_event.call_count == 0
|
assert mock_send_event.call_count == 0
|
||||||
assert mock_heater_off.call_count == 0
|
assert mock_heater_off.call_count == 0
|
||||||
|
|
||||||
# The heater is already on cycle. So we wait that the cycle ends and no heater action is done
|
# The heater is already on cycle. So we wait that the cycle ends and no heater action
|
||||||
|
# is done
|
||||||
assert mock_heater_on.call_count == 0
|
assert mock_heater_on.call_count == 0
|
||||||
# assert entity.underlying_entity(0)._should_relaunch_control_heating is True
|
# assert entity.underlying_entity(0)._should_relaunch_control_heating is True
|
||||||
|
|
||||||
# Simulate the relaunch
|
# Simulate the relaunch
|
||||||
await entity.underlying_entity(0)._turn_on_later(None)
|
await entity.underlying_entity(0)._turn_on_later( # pylint: disable=protected-access
|
||||||
|
None
|
||||||
|
)
|
||||||
# wait restart
|
# wait restart
|
||||||
await asyncio.sleep(0.1)
|
await asyncio.sleep(0.1)
|
||||||
|
|
||||||
@@ -177,7 +181,9 @@ async def test_one_switch_cycle(
|
|||||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
|
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
|
||||||
return_value=True,
|
return_value=True,
|
||||||
) as mock_device_active:
|
) as mock_device_active:
|
||||||
await entity.underlying_entity(0)._turn_off_later(None)
|
await entity.underlying_entity(0)._turn_off_later( # pylint: disable=protected-access
|
||||||
|
None
|
||||||
|
)
|
||||||
|
|
||||||
# No special event
|
# No special event
|
||||||
assert mock_send_event.call_count == 0
|
assert mock_send_event.call_count == 0
|
||||||
@@ -198,7 +204,9 @@ async def test_one_switch_cycle(
|
|||||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
|
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
|
||||||
return_value=True,
|
return_value=True,
|
||||||
) as mock_device_active:
|
) as mock_device_active:
|
||||||
await entity.underlying_entity(0)._turn_on_later(None)
|
await entity.underlying_entity(0)._turn_on_later( # pylint: disable=protected-access
|
||||||
|
None
|
||||||
|
)
|
||||||
|
|
||||||
# No special event
|
# No special event
|
||||||
assert mock_send_event.call_count == 0
|
assert mock_send_event.call_count == 0
|
||||||
@@ -214,7 +222,7 @@ async def test_multiple_switchs(
|
|||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
skip_hass_states_is_state,
|
skip_hass_states_is_state,
|
||||||
skip_send_event,
|
skip_send_event,
|
||||||
):
|
): # pylint: disable=unused-argument
|
||||||
"""Test that when multiple switch are configured the activation is distributed"""
|
"""Test that when multiple switch are configured the activation is distributed"""
|
||||||
|
|
||||||
tz = get_tz(hass) # pylint: disable=invalid-name
|
tz = get_tz(hass) # pylint: disable=invalid-name
|
||||||
@@ -277,7 +285,7 @@ async def test_multiple_switchs(
|
|||||||
await send_temperature_change_event(entity, 15, event_timestamp)
|
await send_temperature_change_event(entity, 15, event_timestamp)
|
||||||
|
|
||||||
# Checks that all climates are off
|
# Checks that all climates are off
|
||||||
assert entity._is_device_active is False
|
assert entity._is_device_active is False # pylint: disable=protected-access
|
||||||
|
|
||||||
# Should be call for all Switch
|
# Should be call for all Switch
|
||||||
assert mock_underlying_set_hvac_mode.call_count == 4
|
assert mock_underlying_set_hvac_mode.call_count == 4
|
||||||
@@ -342,17 +350,20 @@ async def test_multiple_switchs(
|
|||||||
assert mock_send_event.call_count == 0
|
assert mock_send_event.call_count == 0
|
||||||
assert mock_heater_off.call_count == 0
|
assert mock_heater_off.call_count == 0
|
||||||
|
|
||||||
# The first heater should be turned on but is already on but because call_later is mocked, it is only turned on here
|
# The first heater should be turned on but is already on but because call_later
|
||||||
|
# is mocked, it is only turned on here
|
||||||
assert mock_heater_on.call_count == 1
|
assert mock_heater_on.call_count == 1
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||||
async def test_multiple_climates(
|
async def test_multiple_climates(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
skip_hass_states_is_state,
|
skip_hass_states_is_state,
|
||||||
skip_send_event,
|
skip_send_event,
|
||||||
):
|
): # pylint: disable=unused-argument
|
||||||
"""Test that when multiple climates are configured the activation and deactivation is propagated to all climates"""
|
"""Test that when multiple climates are configured the activation and deactivation
|
||||||
|
is propagated to all climates"""
|
||||||
|
|
||||||
tz = get_tz(hass) # pylint: disable=invalid-name
|
tz = get_tz(hass) # pylint: disable=invalid-name
|
||||||
now: datetime = datetime.now(tz=tz)
|
now: datetime = datetime.now(tz=tz)
|
||||||
@@ -416,7 +427,7 @@ async def test_multiple_climates(
|
|||||||
call.set_hvac_mode(HVACMode.HEAT),
|
call.set_hvac_mode(HVACMode.HEAT),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
assert entity._is_device_active is False
|
assert entity._is_device_active is False # pylint: disable=protected-access
|
||||||
|
|
||||||
# Stop heating, in boost mode. We block the control_heating to avoid running a cycle
|
# Stop heating, in boost mode. We block the control_heating to avoid running a cycle
|
||||||
with patch(
|
with patch(
|
||||||
@@ -441,7 +452,8 @@ async def test_multiple_climates(
|
|||||||
call.set_hvac_mode(HVACMode.OFF),
|
call.set_hvac_mode(HVACMode.OFF),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
assert entity._is_device_active is False
|
assert entity._is_device_active is False # pylint: disable=protected-access
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||||
@@ -449,8 +461,9 @@ async def test_multiple_climates_underlying_changes(
|
|||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
skip_hass_states_is_state,
|
skip_hass_states_is_state,
|
||||||
skip_send_event,
|
skip_send_event,
|
||||||
):
|
): # pylint: disable=unused-argument
|
||||||
"""Test that when multiple switch are configured the activation of one underlying climate activate the others"""
|
"""Test that when multiple switch are configured the activation of one underlying
|
||||||
|
climate activate the others"""
|
||||||
|
|
||||||
tz = get_tz(hass) # pylint: disable=invalid-name
|
tz = get_tz(hass) # pylint: disable=invalid-name
|
||||||
now: datetime = datetime.now(tz=tz)
|
now: datetime = datetime.now(tz=tz)
|
||||||
@@ -514,7 +527,7 @@ async def test_multiple_climates_underlying_changes(
|
|||||||
call.set_hvac_mode(HVACMode.HEAT),
|
call.set_hvac_mode(HVACMode.HEAT),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
assert entity._is_device_active is False
|
assert entity._is_device_active is False # pylint: disable=protected-access
|
||||||
|
|
||||||
# Stop heating on one underlying climate
|
# Stop heating on one underlying climate
|
||||||
with patch(
|
with patch(
|
||||||
@@ -524,7 +537,14 @@ async def test_multiple_climates_underlying_changes(
|
|||||||
) as mock_underlying_set_hvac_mode:
|
) as mock_underlying_set_hvac_mode:
|
||||||
# Wait 11 sec so that the event will not be discarded
|
# Wait 11 sec so that the event will not be discarded
|
||||||
event_timestamp = now + timedelta(seconds=11)
|
event_timestamp = now + timedelta(seconds=11)
|
||||||
await send_climate_change_event(entity, HVACMode.OFF, HVACMode.HEAT, HVACAction.OFF, HVACAction.HEATING, event_timestamp)
|
await send_climate_change_event(
|
||||||
|
entity,
|
||||||
|
HVACMode.OFF,
|
||||||
|
HVACMode.HEAT,
|
||||||
|
HVACAction.OFF,
|
||||||
|
HVACAction.HEATING,
|
||||||
|
event_timestamp,
|
||||||
|
)
|
||||||
|
|
||||||
# Should be call for all Switch
|
# Should be call for all Switch
|
||||||
assert mock_underlying_set_hvac_mode.call_count == 4
|
assert mock_underlying_set_hvac_mode.call_count == 4
|
||||||
@@ -534,7 +554,7 @@ async def test_multiple_climates_underlying_changes(
|
|||||||
]
|
]
|
||||||
)
|
)
|
||||||
assert entity.hvac_mode == HVACMode.OFF
|
assert entity.hvac_mode == HVACMode.OFF
|
||||||
assert entity._is_device_active is False
|
assert entity._is_device_active is False # pylint: disable=protected-access
|
||||||
|
|
||||||
# Start heating on one underlying climate
|
# Start heating on one underlying climate
|
||||||
with patch(
|
with patch(
|
||||||
@@ -542,12 +562,21 @@ async def test_multiple_climates_underlying_changes(
|
|||||||
), patch(
|
), patch(
|
||||||
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.set_hvac_mode"
|
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.set_hvac_mode"
|
||||||
) as mock_underlying_set_hvac_mode, patch(
|
) 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
|
# notice that there is no need of return_value=HVACAction.IDLE because this is not
|
||||||
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.hvac_action", HVACAction.IDLE
|
# a function but a property
|
||||||
) as mock_underlying_get_hvac_action:
|
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.hvac_action",
|
||||||
|
HVACAction.IDLE,
|
||||||
|
):
|
||||||
# Wait 11 sec so that the event will not be discarded
|
# Wait 11 sec so that the event will not be discarded
|
||||||
event_timestamp = now + timedelta(seconds=11)
|
event_timestamp = now + timedelta(seconds=11)
|
||||||
await send_climate_change_event(entity, HVACMode.HEAT, HVACMode.OFF, HVACAction.IDLE, HVACAction.OFF, event_timestamp)
|
await send_climate_change_event(
|
||||||
|
entity,
|
||||||
|
HVACMode.HEAT,
|
||||||
|
HVACMode.OFF,
|
||||||
|
HVACAction.IDLE,
|
||||||
|
HVACAction.OFF,
|
||||||
|
event_timestamp,
|
||||||
|
)
|
||||||
|
|
||||||
# Should be call for all Switch
|
# Should be call for all Switch
|
||||||
assert mock_underlying_set_hvac_mode.call_count == 4
|
assert mock_underlying_set_hvac_mode.call_count == 4
|
||||||
@@ -558,5 +587,4 @@ async def test_multiple_climates_underlying_changes(
|
|||||||
)
|
)
|
||||||
assert entity.hvac_mode == HVACMode.HEAT
|
assert entity.hvac_mode == HVACMode.HEAT
|
||||||
assert entity.hvac_action == HVACAction.IDLE
|
assert entity.hvac_action == HVACAction.IDLE
|
||||||
assert entity._is_device_active is False
|
assert entity._is_device_active is False # pylint: disable=protected-access
|
||||||
|
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import
|
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(
|
async def test_open_window_algo(
|
||||||
@@ -12,8 +12,8 @@ from homeassistant.const import UnitOfTime, UnitOfPower, UnitOfEnergy, PERCENTAG
|
|||||||
|
|
||||||
from pytest_homeassistant_custom_component.common import MockConfigEntry
|
from pytest_homeassistant_custom_component.common import MockConfigEntry
|
||||||
|
|
||||||
from ..climate import VersatileThermostat
|
from custom_components.versatile_thermostat.climate import VersatileThermostat
|
||||||
from ..sensor import (
|
from custom_components.versatile_thermostat.sensor import (
|
||||||
EnergySensor,
|
EnergySensor,
|
||||||
MeanPowerSensor,
|
MeanPowerSensor,
|
||||||
OnPercentSensor,
|
OnPercentSensor,
|
||||||
@@ -10,7 +10,7 @@ from homeassistant.components.climate import ClimateEntity, DOMAIN as CLIMATE_DO
|
|||||||
|
|
||||||
from pytest_homeassistant_custom_component.common import MockConfigEntry
|
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
|
from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
|
|
||||||
@@ -5,7 +5,9 @@ from .commons import * # pylint: disable=wildcard-import, unused-wildcard-impor
|
|||||||
|
|
||||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||||
async def test_tpi_calculation(hass: HomeAssistant, skip_hass_states_is_state):
|
async def test_tpi_calculation(
|
||||||
|
hass: HomeAssistant, skip_hass_states_is_state: None
|
||||||
|
): # pylint: disable=unused-argument
|
||||||
"""Test the TPI calculation"""
|
"""Test the TPI calculation"""
|
||||||
|
|
||||||
entry = MockConfigEntry(
|
entry = MockConfigEntry(
|
||||||
@@ -40,7 +42,7 @@ async def test_tpi_calculation(hass: HomeAssistant, skip_hass_states_is_state):
|
|||||||
)
|
)
|
||||||
assert entity
|
assert entity
|
||||||
|
|
||||||
tpi_algo = entity._prop_algorithm
|
tpi_algo = entity._prop_algorithm # pylint: disable=protected-access
|
||||||
assert tpi_algo
|
assert tpi_algo
|
||||||
|
|
||||||
tpi_algo.calculate(15, 10, 7)
|
tpi_algo.calculate(15, 10, 7)
|
||||||
@@ -50,36 +52,52 @@ async def test_tpi_calculation(hass: HomeAssistant, skip_hass_states_is_state):
|
|||||||
assert tpi_algo.off_time_sec == 0
|
assert tpi_algo.off_time_sec == 0
|
||||||
assert entity.mean_cycle_power is None # no device power configured
|
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.on_percent == 0.4
|
||||||
assert tpi_algo.calculated_on_percent == 0.4
|
assert tpi_algo.calculated_on_percent == 0.4
|
||||||
assert tpi_algo.on_time_sec == 120
|
assert tpi_algo.on_time_sec == 120
|
||||||
assert tpi_algo.off_time_sec == 180
|
assert tpi_algo.off_time_sec == 180
|
||||||
|
|
||||||
tpi_algo.set_security(0.1)
|
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.on_percent == 0.1
|
||||||
assert tpi_algo.calculated_on_percent == 0.4
|
assert tpi_algo.calculated_on_percent == 0.4
|
||||||
assert tpi_algo.on_time_sec == 30 # >= minimal_activation_delay (=30)
|
assert tpi_algo.on_time_sec == 30 # >= minimal_activation_delay (=30)
|
||||||
assert tpi_algo.off_time_sec == 270
|
assert tpi_algo.off_time_sec == 270
|
||||||
|
|
||||||
tpi_algo.unset_security()
|
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.on_percent == 0.4
|
||||||
assert tpi_algo.calculated_on_percent == 0.4
|
assert tpi_algo.calculated_on_percent == 0.4
|
||||||
assert tpi_algo.on_time_sec == 120
|
assert tpi_algo.on_time_sec == 120
|
||||||
assert tpi_algo.off_time_sec == 180
|
assert tpi_algo.off_time_sec == 180
|
||||||
|
|
||||||
# Test minimal activation delay
|
# 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.on_percent == 0.09
|
||||||
assert tpi_algo.calculated_on_percent == 0.09
|
assert tpi_algo.calculated_on_percent == 0.09
|
||||||
assert tpi_algo.on_time_sec == 0
|
assert tpi_algo.on_time_sec == 0
|
||||||
assert tpi_algo.off_time_sec == 300
|
assert tpi_algo.off_time_sec == 300
|
||||||
|
|
||||||
tpi_algo.set_security(0.09)
|
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.on_percent == 0.09
|
||||||
assert tpi_algo.calculated_on_percent == 0.09
|
assert tpi_algo.calculated_on_percent == 0.09
|
||||||
assert tpi_algo.on_time_sec == 0
|
assert tpi_algo.on_time_sec == 0
|
||||||
assert tpi_algo.off_time_sec == 300
|
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
|
||||||
Reference in New Issue
Block a user