Compare commits
5 Commits
3.6.0.beta
...
3.6.0.beta
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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
|
|
||||||
|
|||||||
@@ -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"
|
"9123: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/custom_components/versatile_thermostat"
|
||||||
]
|
],
|
||||||
|
"python.formatting.provider": "none"
|
||||||
}
|
}
|
||||||
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.
|
||||||
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
|
||||||
@@ -301,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()
|
||||||
@@ -339,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(
|
||||||
@@ -647,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 (
|
||||||
@@ -769,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())
|
||||||
@@ -962,8 +977,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
|
||||||
@@ -983,7 +998,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
|
||||||
@@ -995,6 +1013,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
|
||||||
@@ -1066,7 +1086,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
|
||||||
|
|
||||||
@@ -1299,7 +1319,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)
|
||||||
@@ -1533,7 +1555,11 @@ 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
|
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,
|
||||||
@@ -1570,13 +1596,17 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
|||||||
await self._async_control_heating(force=True)
|
await self._async_control_heating(force=True)
|
||||||
self._motion_call_cancel = None
|
self._motion_call_cancel = None
|
||||||
|
|
||||||
im_on = (self._motion_state == STATE_ON)
|
im_on = self._motion_state == STATE_ON
|
||||||
delay_running = (self._motion_call_cancel is not None)
|
delay_running = self._motion_call_cancel is not None
|
||||||
event_on = (new_state.state == STATE_ON)
|
event_on = new_state.state == STATE_ON
|
||||||
|
|
||||||
def arm():
|
def arm():
|
||||||
""" Arm the timer"""
|
"""Arm the timer"""
|
||||||
delay = self._motion_delay_sec if new_state.state == STATE_ON else self._motion_off_delay_sec
|
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._motion_call_cancel = async_call_later(
|
||||||
self.hass, timedelta(seconds=delay), try_motion_condition
|
self.hass, timedelta(seconds=delay), try_motion_condition
|
||||||
)
|
)
|
||||||
@@ -1589,7 +1619,10 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
|||||||
# if I'm off
|
# if I'm off
|
||||||
if not im_on:
|
if not im_on:
|
||||||
if event_on and not delay_running:
|
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)
|
_LOGGER.debug(
|
||||||
|
"%s - Arm delay cause i'm off and event is on and no delay is running",
|
||||||
|
self,
|
||||||
|
)
|
||||||
arm()
|
arm()
|
||||||
return try_motion_condition
|
return try_motion_condition
|
||||||
# Ignore the event
|
# Ignore the event
|
||||||
@@ -1601,7 +1634,10 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
|||||||
arm()
|
arm()
|
||||||
return try_motion_condition
|
return try_motion_condition
|
||||||
if event_on and delay_running:
|
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)
|
_LOGGER.debug(
|
||||||
|
"%s - Desarm off delay cause i'm on and event is on and a delay is running",
|
||||||
|
self,
|
||||||
|
)
|
||||||
desarm()
|
desarm()
|
||||||
return None
|
return None
|
||||||
# Ignore the event
|
# Ignore the event
|
||||||
@@ -1613,7 +1649,7 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
|||||||
"""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)
|
||||||
@@ -1632,16 +1668,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()
|
||||||
@@ -1667,14 +1703,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
|
||||||
|
|
||||||
@@ -1687,7 +1731,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
|
||||||
@@ -1733,21 +1785,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
|
||||||
@@ -1757,15 +1815,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."""
|
||||||
@@ -2208,13 +2278,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:
|
||||||
@@ -2376,7 +2452,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()
|
||||||
@@ -2463,18 +2542,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"
|
||||||
@@ -2566,7 +2645,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):
|
||||||
|
|||||||
@@ -227,6 +227,7 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
|
|||||||
PROPORTIONAL_FUNCTION_TPI,
|
PROPORTIONAL_FUNCTION_TPI,
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
|
vol.Optional(CONF_AC_MODE, default=False): cv.boolean,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -244,7 +245,6 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
|
|||||||
vol.Optional(CONF_CLIMATE_4): selector.EntitySelector(
|
vol.Optional(CONF_CLIMATE_4): selector.EntitySelector(
|
||||||
selector.EntitySelectorConfig(domain=CLIMATE_DOMAIN),
|
selector.EntitySelectorConfig(domain=CLIMATE_DOMAIN),
|
||||||
),
|
),
|
||||||
vol.Optional(CONF_AC_MODE, default=False): cv.boolean,
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -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 = (
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
28
scripts/starts_ha.sh
Executable file
28
scripts/starts_ha.sh
Executable file
@@ -0,0 +1,28 @@
|
|||||||
|
#!/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
|
||||||
|
# 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
|
||||||
|
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
|
||||||
@@ -20,9 +20,9 @@ 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,
|
||||||
@@ -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
|
||||||
|
|
||||||
@@ -97,6 +97,7 @@ 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_4SWITCH_TYPE_CONFIG = {
|
MOCK_TH_OVER_4SWITCH_TYPE_CONFIG = {
|
||||||
@@ -105,6 +106,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 = {
|
||||||
@@ -114,7 +116,6 @@ MOCK_TH_OVER_SWITCH_TPI_CONFIG = {
|
|||||||
|
|
||||||
MOCK_TH_OVER_CLIMATE_TYPE_CONFIG = {
|
MOCK_TH_OVER_CLIMATE_TYPE_CONFIG = {
|
||||||
CONF_CLIMATE: "climate.mock_climate",
|
CONF_CLIMATE: "climate.mock_climate",
|
||||||
CONF_AC_MODE: False,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
MOCK_PRESETS_CONFIG = {
|
MOCK_PRESETS_CONFIG = {
|
||||||
@@ -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}
|
||||||
@@ -184,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(
|
||||||
@@ -281,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(
|
||||||
@@ -353,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"""
|
||||||
|
|
||||||
@@ -378,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,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
|
||||||
|
|
||||||
@@ -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