Compare commits
5 Commits
main
...
3.6.0.beta
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8b88ed5c9c | ||
|
|
c0d422a916 | ||
|
|
954fc6271c | ||
|
|
6dc078871d | ||
|
|
cd0ab3c88d |
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,43 +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=${localWorkspaceFolder}/.devcontainer/configuration.yaml,target=/home/vscode/core/config/configuration.yaml,type=bind,consistency=cached",
|
"source=/Users/jmcollin/.ssh,target=/home/vscode/.ssh,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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -105,4 +105,6 @@ dist
|
|||||||
|
|
||||||
# init file required for unittest
|
# init file required for unittest
|
||||||
custom_components/__init__.py
|
custom_components/__init__.py
|
||||||
__pycache__
|
__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.
|
||||||
18
container
18
container
@@ -4,15 +4,12 @@
|
|||||||
|
|
||||||
. .bashrc
|
. .bashrc
|
||||||
|
|
||||||
cd $HA
|
|
||||||
|
|
||||||
function get_dev() {
|
function get_dev() {
|
||||||
cd /workspaces/versatile_thermostat/custom_components/versatile_thermostat/
|
|
||||||
pip install pytest
|
|
||||||
pip install -r requirements_dev.txt
|
pip install -r requirements_dev.txt
|
||||||
pip install -r requirements_test.txt
|
pip install -r requirements_test.txt
|
||||||
sudo chown -R vscode: /home/vscode/core
|
if [ -d /home/vscode/core ]; then
|
||||||
cd -
|
sudo chown -R vscode: /home/vscode/core
|
||||||
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
echo "arguments are: "$1
|
echo "arguments are: "$1
|
||||||
@@ -20,8 +17,7 @@ echo "arguments are: "$1
|
|||||||
case $1 in
|
case $1 in
|
||||||
start)
|
start)
|
||||||
echo "Running container start"
|
echo "Running container start"
|
||||||
cd $HA
|
./scripts/starts_ha.sh
|
||||||
hass -c ./config --debug
|
|
||||||
;;
|
;;
|
||||||
dev-setup)
|
dev-setup)
|
||||||
get_dev
|
get_dev
|
||||||
@@ -43,8 +39,8 @@ case $1 in
|
|||||||
restart)
|
restart)
|
||||||
echo "Killing existing container"
|
echo "Killing existing container"
|
||||||
pkill hass
|
pkill hass
|
||||||
echo "Killing existing container"
|
echo "Restarting existing container"
|
||||||
cd $HA
|
pwd
|
||||||
hass -c ./config
|
./scripts/starts_ha.sh
|
||||||
;;
|
;;
|
||||||
esac
|
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
|
||||||
@@ -425,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]
|
||||||
|
|
||||||
@@ -652,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 (
|
||||||
@@ -912,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.
|
||||||
@@ -1312,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)
|
||||||
@@ -1336,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 [
|
||||||
@@ -1546,7 +1560,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,
|
||||||
@@ -1583,13 +1601,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
|
||||||
)
|
)
|
||||||
@@ -1602,7 +1624,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
|
||||||
@@ -1614,7 +1639,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
|
||||||
@@ -1696,9 +1724,7 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
|||||||
# 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(
|
# _LOGGER.debug("The underlying switch to idle instead of OFF. We will consider it as OFF")
|
||||||
# "The underlying switch to idle instead of OFF. We will consider it as OFF"
|
|
||||||
# )
|
|
||||||
# new_hvac_mode = HVACMode.OFF
|
# new_hvac_mode = HVACMode.OFF
|
||||||
|
|
||||||
_LOGGER.info(
|
_LOGGER.info(
|
||||||
@@ -1710,17 +1736,15 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
|||||||
old_hvac_action,
|
old_hvac_action,
|
||||||
)
|
)
|
||||||
|
|
||||||
if new_hvac_mode in [
|
_LOGGER.debug(
|
||||||
HVACMode.OFF,
|
"%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",
|
||||||
HVACMode.HEAT,
|
self,
|
||||||
HVACMode.COOL,
|
self._last_change_time,
|
||||||
HVACMode.HEAT_COOL,
|
old_state_date_changed,
|
||||||
HVACMode.DRY,
|
old_state_date_updated,
|
||||||
HVACMode.AUTO,
|
new_state_date_changed,
|
||||||
HVACMode.FAN_ONLY,
|
new_state_date_updated,
|
||||||
None,
|
)
|
||||||
]:
|
|
||||||
self._hvac_mode = new_hvac_mode
|
|
||||||
|
|
||||||
# Interpretation of hvac action
|
# Interpretation of hvac action
|
||||||
HVAC_ACTION_ON = [ # pylint: disable=invalid-name
|
HVAC_ACTION_ON = [ # pylint: disable=invalid-name
|
||||||
@@ -1955,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",
|
||||||
@@ -2484,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,
|
||||||
|
|||||||
@@ -245,6 +245,7 @@ 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,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
@@ -51,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,
|
||||||
@@ -100,6 +99,12 @@ MOCK_TH_OVER_SWITCH_TYPE_CONFIG = {
|
|||||||
CONF_AC_MODE: False,
|
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 = {
|
||||||
CONF_HEATER: "switch.mock_4switch0",
|
CONF_HEATER: "switch.mock_4switch0",
|
||||||
CONF_HEATER_2: "switch.mock_4switch1",
|
CONF_HEATER_2: "switch.mock_4switch1",
|
||||||
@@ -116,6 +121,7 @@ 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 = {
|
||||||
@@ -124,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,
|
||||||
@@ -156,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}
|
||||||
@@ -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"""
|
||||||
|
|
||||||
@@ -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)
|
||||||
|
|
||||||
@@ -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: None):
|
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: N
|
|||||||
)
|
)
|
||||||
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)
|
||||||
Reference in New Issue
Block a user