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"
|
||||
alias ll='ls -l'
|
||||
export HA='/home/vscode/core'
|
||||
cd $HA
|
||||
source venv/bin/activate
|
||||
# source venv/bin/activate
|
||||
|
||||
@@ -60,6 +60,9 @@ input_boolean:
|
||||
fake_heater_switch1:
|
||||
name: Heater 1
|
||||
icon: mdi:radiator
|
||||
fake_heater_ac1:
|
||||
name: Air contionner 1
|
||||
icon: mdi:air-conditioner
|
||||
fake_heater_4switch1:
|
||||
name: Heater (multiswitch1)
|
||||
icon: mdi:radiator
|
||||
@@ -114,22 +117,22 @@ climate:
|
||||
name: Underlying thermostat 4-1
|
||||
heater: input_boolean.fake_heater_4climate1
|
||||
target_sensor: input_number.fake_temperature_sensor1
|
||||
ac_mode: true
|
||||
ac_mode: false
|
||||
- platform: generic_thermostat
|
||||
name: Underlying thermostat 4-2
|
||||
heater: input_boolean.fake_heater_4climate2
|
||||
target_sensor: input_number.fake_temperature_sensor1
|
||||
ac_mode: true
|
||||
ac_mode: false
|
||||
- platform: generic_thermostat
|
||||
name: Underlying thermostat 4-3
|
||||
heater: input_boolean.fake_heater_4climate3
|
||||
target_sensor: input_number.fake_temperature_sensor1
|
||||
ac_mode: true
|
||||
ac_mode: false
|
||||
- platform: generic_thermostat
|
||||
name: Underlying thermostat 4-4
|
||||
heater: input_boolean.fake_heater_4climate4
|
||||
target_sensor: input_number.fake_temperature_sensor1
|
||||
ac_mode: true
|
||||
ac_mode: false
|
||||
- platform: generic_thermostat
|
||||
name: Underlying thermostat9
|
||||
heater: input_boolean.fake_heater_switch3
|
||||
|
||||
@@ -1,43 +1,54 @@
|
||||
// See https://aka.ms/vscode-remote/devcontainer.json for format details.
|
||||
// "image": "ghcr.io/ludeeus/devcontainer/integration:latest",
|
||||
{
|
||||
"image": "mcr.microsoft.com/vscode/devcontainers/python:0-3.10",
|
||||
"image": "mcr.microsoft.com/devcontainers/python:1-3.11-bullseye",
|
||||
"name": "Versatile Thermostat integration",
|
||||
"context": "..",
|
||||
"appPort": [
|
||||
"9123:8123"
|
||||
"8123:8123"
|
||||
],
|
||||
// "postCreateCommand": "container install",
|
||||
"postCreateCommand": "./container install",
|
||||
"extensions": [
|
||||
"ms-python.python",
|
||||
"github.vscode-pull-request-github",
|
||||
"ryanluker.vscode-coverage-gutters",
|
||||
"ms-python.vscode-pylance"
|
||||
],
|
||||
"postCreateCommand": "./container dev-setup",
|
||||
|
||||
"mounts": [
|
||||
"source=${localWorkspaceFolder}/.devcontainer/configuration.yaml,target=/home/vscode/core/config/configuration.yaml,type=bind,consistency=cached",
|
||||
"source=${localWorkspaceFolder}/custom_components,target=/home/vscode/core/config/custom_components,type=bind,consistency=cached"
|
||||
"source=/Users/jmcollin/.ssh,target=/home/vscode/.ssh,type=bind,consistency=cached"
|
||||
],
|
||||
"settings": {
|
||||
"files.eol": "\n",
|
||||
"editor.tabSize": 4,
|
||||
"terminal.integrated.profiles.linux": {
|
||||
"Bash Profile": {
|
||||
"path": "bash",
|
||||
"args": []
|
||||
|
||||
"customizations": {
|
||||
"vscode": {
|
||||
"extensions": [
|
||||
"ms-python.python",
|
||||
"github.vscode-pull-request-github",
|
||||
"ryanluker.vscode-coverage-gutters",
|
||||
"ms-python.vscode-pylance"
|
||||
],
|
||||
// "mounts": [
|
||||
// "source=${localWorkspaceFolder}/.devcontainer/configuration.yaml,target=/home/vscode/core/config/configuration.yaml,type=bind,consistency=cached",
|
||||
// "source=${localWorkspaceFolder}/custom_components,target=/home/vscode/core/config/custom_components,type=bind,consistency=cached"
|
||||
// ],
|
||||
"settings": {
|
||||
"files.eol": "\n",
|
||||
"editor.tabSize": 4,
|
||||
"terminal.integrated.profiles.linux": {
|
||||
"bash": {
|
||||
"path": "bash",
|
||||
"args": []
|
||||
}
|
||||
},
|
||||
"terminal.integrated.defaultProfile.linux": "bash",
|
||||
// "terminal.integrated.shell.linux": "/bin/bash",
|
||||
"python.pythonPath": "/usr/bin/python3",
|
||||
"python.analysis.autoSearchPaths": true,
|
||||
"python.linting.pylintEnabled": true,
|
||||
"python.linting.enabled": true,
|
||||
"python.formatting.provider": "black",
|
||||
"python.formatting.blackPath": "/usr/local/py-utils/bin/black",
|
||||
"editor.formatOnPaste": false,
|
||||
"editor.formatOnSave": true,
|
||||
"editor.formatOnType": true,
|
||||
"files.trimTrailingWhitespace": true,
|
||||
"python.experiments.optOutFrom": ["pythonTestAdapter"],
|
||||
"python.analysis.logLevel": "Trace"
|
||||
}
|
||||
},
|
||||
"terminal.integrated.defaultProfile.linux": "Bash Profile",
|
||||
// "terminal.integrated.shell.linux": "/bin/bash",
|
||||
"python.pythonPath": "/usr/bin/python3",
|
||||
"python.analysis.autoSearchPaths": true,
|
||||
"python.linting.pylintEnabled": true,
|
||||
"python.linting.enabled": true,
|
||||
"python.formatting.provider": "black",
|
||||
"editor.formatOnPaste": false,
|
||||
"editor.formatOnSave": true,
|
||||
"editor.formatOnType": true,
|
||||
"files.trimTrailingWhitespace": true
|
||||
}
|
||||
}
|
||||
}
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -105,4 +105,6 @@ dist
|
||||
|
||||
# init file required for unittest
|
||||
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",
|
||||
"configurations": [
|
||||
{
|
||||
// Example of attaching to local debug server
|
||||
"name": "Python: Attach Local",
|
||||
"name": "Home Assistant (debug)",
|
||||
"type": "python",
|
||||
"request": "attach",
|
||||
"port": 5678,
|
||||
"host": "localhost",
|
||||
"request": "launch",
|
||||
"module": "homeassistant",
|
||||
"justMyCode": false,
|
||||
"pathMappings": [
|
||||
// {
|
||||
// "localRoot": "${workspaceFolder}",
|
||||
// "remoteRoot": "."
|
||||
//},
|
||||
{
|
||||
"localRoot": "${workspaceFolder}/../core",
|
||||
"remoteRoot": "/home/vscode/core"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
// Example of attaching to my production server
|
||||
"name": "Python: Attach Remote",
|
||||
"type": "python",
|
||||
"request": "attach",
|
||||
"port": 5678,
|
||||
"host": "homeassistant.local",
|
||||
"pathMappings": [
|
||||
{
|
||||
"localRoot": "${workspaceFolder}",
|
||||
"remoteRoot": "/usr/src/homeassistant"
|
||||
}
|
||||
"args": [
|
||||
"--debug",
|
||||
"-c",
|
||||
"config"
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
16
.vscode/settings.json
vendored
16
.vscode/settings.json
vendored
@@ -1,12 +1,20 @@
|
||||
{
|
||||
"[python]": {
|
||||
"editor.defaultFormatter": "ms-python.black-formatter"
|
||||
},
|
||||
"python.linting.pylintEnabled": true,
|
||||
"python.linting.enabled": true,
|
||||
"python.pythonPath": "/usr/local/bin/python",
|
||||
"files.associations": {
|
||||
"*.yaml": "home-assistant"
|
||||
},
|
||||
"python.testing.pytestArgs": [
|
||||
"tests"
|
||||
],
|
||||
"python.testing.unittestEnabled": false,
|
||||
"python.testing.pytestEnabled": true,
|
||||
"python.analysis.extraPaths": [
|
||||
"/home/vscode/core",
|
||||
"/workspaces/versatile_thermostat"
|
||||
]
|
||||
// "/home/vscode/core",
|
||||
"/workspaces/versatile_thermostat/custom_components/versatile_thermostat"
|
||||
],
|
||||
"python.formatting.provider": "none"
|
||||
}
|
||||
4
.vscode/tasks.json
vendored
4
.vscode/tasks.json
vendored
@@ -2,13 +2,13 @@
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"label": "Run Home Assistant on port 9123",
|
||||
"label": "Run Home Assistant on port 8123",
|
||||
"type": "shell",
|
||||
"command": "./container start",
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "Restart Home Assistant on port 9123",
|
||||
"label": "Restart Home Assistant on port 8123",
|
||||
"type": "shell",
|
||||
"command": "./container restart",
|
||||
"problemMatcher": []
|
||||
|
||||
61
CONTRIBUTING-fr.md
Normal file
61
CONTRIBUTING-fr.md
Normal file
@@ -0,0 +1,61 @@
|
||||
# Consignes de contribution
|
||||
|
||||
Contribuer à ce projet doit être aussi simple et transparent que possible, que ce soit :
|
||||
|
||||
- Signaler un bug
|
||||
- Discuter de l'état actuel du code
|
||||
- Soumettre un correctif
|
||||
- Proposer de nouvelles fonctionnalités
|
||||
|
||||
## Github est utilisé pour tout
|
||||
|
||||
Github est utilisé pour héberger du code, pour suivre les problèmes et les demandes de fonctionnalités, ainsi que pour accepter les demandes d'extraction.
|
||||
|
||||
Les demandes d'extraction sont le meilleur moyen de proposer des modifications à la base de code.
|
||||
|
||||
1. Fourchez le dépôt et créez votre branche à partir de `master`.
|
||||
2. Si vous avez modifié quelque chose, mettez à jour la documentation.
|
||||
3. Assurez-vous que votre code peluche (en utilisant du noir).
|
||||
4. Testez votre contribution.
|
||||
5. Émettez cette pull request !
|
||||
|
||||
## Toutes les contributions que vous ferez seront sous la licence logicielle MIT
|
||||
|
||||
En bref, lorsque vous soumettez des modifications de code, vos soumissions sont considérées comme étant sous la même [licence MIT](http://choosealicense.com/licenses/mit/) qui couvre le projet. N'hésitez pas à contacter les mainteneurs si cela vous préoccupe.
|
||||
|
||||
## Signaler les bogues en utilisant les [issues] de Github (../../issues)
|
||||
|
||||
Les problèmes GitHub sont utilisés pour suivre les bogues publics.
|
||||
Signalez un bogue en [ouvrant un nouveau problème](../../issues/new/choose) ; C'est si facile!
|
||||
|
||||
## Rédiger des rapports de bogue avec des détails, un arrière-plan et un exemple de code
|
||||
|
||||
Les **rapports de bogues géniaux** ont tendance à avoir :
|
||||
|
||||
- Un résumé rapide et/ou un historique
|
||||
- Étapes à reproduire
|
||||
- Être spécifique!
|
||||
- Donnez un exemple de code si vous le pouvez.
|
||||
- Ce à quoi vous vous attendiez arriverait
|
||||
- Que se passe-t-il réellement
|
||||
- Notes (y compris éventuellement pourquoi vous pensez que cela pourrait se produire, ou des choses que vous avez essayées qui n'ont pas fonctionné)
|
||||
|
||||
Les gens *adorent* les rapports de bogues approfondis. Je ne plaisante même pas.
|
||||
|
||||
## Utilisez un style de codage cohérent
|
||||
|
||||
Utilisez [black](https://github.com/ambv/black) pour vous assurer que le code suit le style.
|
||||
|
||||
## Testez votre modification de code
|
||||
|
||||
Ce composant personnalisé est basé sur les meilleures pratiques décrites ici [modèle d'intégration_blueprint](https://github.com/custom-components/integration_blueprint).
|
||||
|
||||
Il est livré avec un environnement de développement dans un conteneur, facile à lancer
|
||||
si vous utilisez Visual Studio Code. Avec ce conteneur, vous aurez un stand alone
|
||||
Instance de Home Assistant en cours d'exécution et déjà configurée avec le inclus
|
||||
[`.devcontainer/configuration.yaml`](./.devcontainer/configuration.yaml)
|
||||
déposer.
|
||||
|
||||
## Licence
|
||||
|
||||
En contribuant, vous acceptez que vos contributions soient autorisées sous sa licence MIT.
|
||||
61
CONTRIBUTING.md
Normal file
61
CONTRIBUTING.md
Normal file
@@ -0,0 +1,61 @@
|
||||
# Contribution guidelines
|
||||
|
||||
Contributing to this project should be as easy and transparent as possible, whether it's:
|
||||
|
||||
- Reporting a bug
|
||||
- Discussing the current state of the code
|
||||
- Submitting a fix
|
||||
- Proposing new features
|
||||
|
||||
## Github is used for everything
|
||||
|
||||
Github is used to host code, to track issues and feature requests, as well as accept pull requests.
|
||||
|
||||
Pull requests are the best way to propose changes to the codebase.
|
||||
|
||||
1. Fork the repo and create your branch from `master`.
|
||||
2. If you've changed something, update the documentation.
|
||||
3. Make sure your code lints (using black).
|
||||
4. Test you contribution.
|
||||
5. Issue that pull request!
|
||||
|
||||
## Any contributions you make will be under the MIT Software License
|
||||
|
||||
In short, when you submit code changes, your submissions are understood to be under the same [MIT License](http://choosealicense.com/licenses/mit/) that covers the project. Feel free to contact the maintainers if that's a concern.
|
||||
|
||||
## Report bugs using Github's [issues](../../issues)
|
||||
|
||||
GitHub issues are used to track public bugs.
|
||||
Report a bug by [opening a new issue](../../issues/new/choose); it's that easy!
|
||||
|
||||
## Write bug reports with detail, background, and sample code
|
||||
|
||||
**Great Bug Reports** tend to have:
|
||||
|
||||
- A quick summary and/or background
|
||||
- Steps to reproduce
|
||||
- Be specific!
|
||||
- Give sample code if you can.
|
||||
- What you expected would happen
|
||||
- What actually happens
|
||||
- Notes (possibly including why you think this might be happening, or stuff you tried that didn't work)
|
||||
|
||||
People *love* thorough bug reports. I'm not even kidding.
|
||||
|
||||
## Use a Consistent Coding Style
|
||||
|
||||
Use [black](https://github.com/ambv/black) to make sure the code follows the style.
|
||||
|
||||
## Test your code modification
|
||||
|
||||
This custom component is based on best practices described here [integration_blueprint template](https://github.com/custom-components/integration_blueprint).
|
||||
|
||||
It comes with development environment in a container, easy to launch
|
||||
if you use Visual Studio Code. With this container you will have a stand alone
|
||||
Home Assistant instance running and already configured with the included
|
||||
[`.devcontainer/configuration.yaml`](./.devcontainer/configuration.yaml)
|
||||
file.
|
||||
|
||||
## License
|
||||
|
||||
By contributing, you agree that your contributions will be licensed under its MIT License.
|
||||
18
container
18
container
@@ -4,15 +4,12 @@
|
||||
|
||||
. .bashrc
|
||||
|
||||
cd $HA
|
||||
|
||||
function get_dev() {
|
||||
cd /workspaces/versatile_thermostat/custom_components/versatile_thermostat/
|
||||
pip install pytest
|
||||
pip install -r requirements_dev.txt
|
||||
pip install -r requirements_test.txt
|
||||
sudo chown -R vscode: /home/vscode/core
|
||||
cd -
|
||||
if [ -d /home/vscode/core ]; then
|
||||
sudo chown -R vscode: /home/vscode/core
|
||||
fi
|
||||
}
|
||||
|
||||
echo "arguments are: "$1
|
||||
@@ -20,8 +17,7 @@ echo "arguments are: "$1
|
||||
case $1 in
|
||||
start)
|
||||
echo "Running container start"
|
||||
cd $HA
|
||||
hass -c ./config --debug
|
||||
./scripts/starts_ha.sh
|
||||
;;
|
||||
dev-setup)
|
||||
get_dev
|
||||
@@ -43,8 +39,8 @@ case $1 in
|
||||
restart)
|
||||
echo "Killing existing container"
|
||||
pkill hass
|
||||
echo "Killing existing container"
|
||||
cd $HA
|
||||
hass -c ./config
|
||||
echo "Restarting existing container"
|
||||
pwd
|
||||
./scripts/starts_ha.sh
|
||||
;;
|
||||
esac
|
||||
|
||||
@@ -54,7 +54,9 @@ async def async_setup_entry(
|
||||
class SecurityBinarySensor(VersatileThermostatBaseEntity, BinarySensorEntity):
|
||||
"""Representation of a BinarySensor which exposes the security state"""
|
||||
|
||||
def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None:
|
||||
def __init__(
|
||||
self, hass: HomeAssistant, unique_id, name, entry_infos
|
||||
) -> None: # pylint: disable=unused-argument
|
||||
"""Initialize the SecurityState Binary sensor"""
|
||||
super().__init__(hass, unique_id, entry_infos.get(CONF_NAME))
|
||||
self._attr_name = "Security state"
|
||||
@@ -87,7 +89,9 @@ class SecurityBinarySensor(VersatileThermostatBaseEntity, BinarySensorEntity):
|
||||
class OverpoweringBinarySensor(VersatileThermostatBaseEntity, BinarySensorEntity):
|
||||
"""Representation of a BinarySensor which exposes the overpowering state"""
|
||||
|
||||
def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None:
|
||||
def __init__(
|
||||
self, hass: HomeAssistant, unique_id, name, entry_infos
|
||||
) -> None: # pylint: disable=unused-argument
|
||||
"""Initialize the OverpoweringState Binary sensor"""
|
||||
super().__init__(hass, unique_id, entry_infos.get(CONF_NAME))
|
||||
self._attr_name = "Overpowering state"
|
||||
@@ -120,7 +124,9 @@ class OverpoweringBinarySensor(VersatileThermostatBaseEntity, BinarySensorEntity
|
||||
class WindowBinarySensor(VersatileThermostatBaseEntity, BinarySensorEntity):
|
||||
"""Representation of a BinarySensor which exposes the window state"""
|
||||
|
||||
def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None:
|
||||
def __init__(
|
||||
self, hass: HomeAssistant, unique_id, name, entry_infos
|
||||
) -> None: # pylint: disable=unused-argument
|
||||
"""Initialize the WindowState Binary sensor"""
|
||||
super().__init__(hass, unique_id, entry_infos.get(CONF_NAME))
|
||||
self._attr_name = "Window state"
|
||||
@@ -134,7 +140,10 @@ class WindowBinarySensor(VersatileThermostatBaseEntity, BinarySensorEntity):
|
||||
|
||||
old_state = self._attr_is_on
|
||||
# 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.my_climate.window_state == STATE_ON
|
||||
or self.my_climate.window_auto_state == STATE_ON
|
||||
@@ -161,7 +170,9 @@ class WindowBinarySensor(VersatileThermostatBaseEntity, BinarySensorEntity):
|
||||
class MotionBinarySensor(VersatileThermostatBaseEntity, BinarySensorEntity):
|
||||
"""Representation of a BinarySensor which exposes the motion state"""
|
||||
|
||||
def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None:
|
||||
def __init__(
|
||||
self, hass: HomeAssistant, unique_id, name, entry_infos
|
||||
) -> None: # pylint: disable=unused-argument
|
||||
"""Initialize the MotionState Binary sensor"""
|
||||
super().__init__(hass, unique_id, entry_infos.get(CONF_NAME))
|
||||
self._attr_name = "Motion state"
|
||||
@@ -195,7 +206,9 @@ class MotionBinarySensor(VersatileThermostatBaseEntity, BinarySensorEntity):
|
||||
class PresenceBinarySensor(VersatileThermostatBaseEntity, BinarySensorEntity):
|
||||
"""Representation of a BinarySensor which exposes the presence state"""
|
||||
|
||||
def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None:
|
||||
def __init__(
|
||||
self, hass: HomeAssistant, unique_id, name, entry_infos
|
||||
) -> None: # pylint: disable=unused-argument
|
||||
"""Initialize the PresenceState Binary sensor"""
|
||||
super().__init__(hass, unique_id, entry_infos.get(CONF_NAME))
|
||||
self._attr_name = "Presence state"
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
# pylint: disable=line-too-long
|
||||
# pylint: disable=too-many-lines
|
||||
# pylint: disable=invalid-name
|
||||
""" Implements the VersatileThermostat climate component """
|
||||
import math
|
||||
import logging
|
||||
@@ -425,7 +428,7 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
||||
self._presence_on = self._presence_sensor_entity_id is not None
|
||||
|
||||
if self._ac_mode:
|
||||
self._hvac_list = [HVACMode.HEAT, HVACMode.COOL, HVACMode.OFF]
|
||||
self._hvac_list = [HVACMode.COOL, HVACMode.OFF]
|
||||
else:
|
||||
self._hvac_list = [HVACMode.HEAT, HVACMode.OFF]
|
||||
|
||||
@@ -652,7 +655,11 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
||||
|
||||
# Initialize all UnderlyingEntities
|
||||
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)
|
||||
if temperature_state and temperature_state.state not in (
|
||||
@@ -912,6 +919,11 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
||||
|
||||
return self._hvac_list
|
||||
|
||||
@property
|
||||
def ac_mode(self) -> bool:
|
||||
""" Get the ac_mode of the Themostat"""
|
||||
return self._ac_mode
|
||||
|
||||
@property
|
||||
def fan_mode(self) -> str | None:
|
||||
"""Return the fan setting.
|
||||
@@ -1312,7 +1324,9 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
||||
self.recalculate()
|
||||
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"""
|
||||
self._last_change_time = datetime.now(tz=self._current_tz)
|
||||
_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:
|
||||
return self._power_temp
|
||||
else:
|
||||
# Select _ac presets if in COOL Mode
|
||||
if self._ac_mode and self._hvac_mode == HVACMode.COOL:
|
||||
# Select _ac presets if in COOL Mode (or over_switch with _ac_mode)
|
||||
if self._ac_mode and (self._hvac_mode == HVACMode.COOL or not self._is_over_climate):
|
||||
preset_mode = preset_mode + PRESET_AC_SUFFIX
|
||||
|
||||
if self._presence_on is False or self._presence_state in [
|
||||
@@ -1546,7 +1560,11 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
||||
# Check delay condition
|
||||
async def try_motion_condition(_):
|
||||
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(
|
||||
self.hass,
|
||||
self._motion_sensor_entity_id,
|
||||
@@ -1583,13 +1601,17 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
||||
await self._async_control_heating(force=True)
|
||||
self._motion_call_cancel = None
|
||||
|
||||
im_on = (self._motion_state == STATE_ON)
|
||||
delay_running = (self._motion_call_cancel is not None)
|
||||
event_on = (new_state.state == STATE_ON)
|
||||
im_on = self._motion_state == STATE_ON
|
||||
delay_running = self._motion_call_cancel is not None
|
||||
event_on = new_state.state == STATE_ON
|
||||
|
||||
def arm():
|
||||
""" Arm the timer"""
|
||||
delay = self._motion_delay_sec if new_state.state == STATE_ON else self._motion_off_delay_sec
|
||||
"""Arm the timer"""
|
||||
delay = (
|
||||
self._motion_delay_sec
|
||||
if new_state.state == STATE_ON
|
||||
else self._motion_off_delay_sec
|
||||
)
|
||||
self._motion_call_cancel = async_call_later(
|
||||
self.hass, timedelta(seconds=delay), try_motion_condition
|
||||
)
|
||||
@@ -1602,7 +1624,10 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
||||
# if I'm off
|
||||
if not im_on:
|
||||
if event_on and not delay_running:
|
||||
_LOGGER.debug("%s - Arm delay cause i'm off and event is on and no delay is running", self)
|
||||
_LOGGER.debug(
|
||||
"%s - Arm delay cause i'm off and event is on and no delay is running",
|
||||
self,
|
||||
)
|
||||
arm()
|
||||
return try_motion_condition
|
||||
# Ignore the event
|
||||
@@ -1614,7 +1639,10 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
||||
arm()
|
||||
return try_motion_condition
|
||||
if event_on and delay_running:
|
||||
_LOGGER.debug("%s - Desarm off delay cause i'm on and event is on and a delay is running", self)
|
||||
_LOGGER.debug(
|
||||
"%s - Desarm off delay cause i'm on and event is on and a delay is running",
|
||||
self,
|
||||
)
|
||||
desarm()
|
||||
return None
|
||||
# Ignore the event
|
||||
@@ -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 114 - Remove this because hvac_mode is now managed by local _hvac_mode and use idle action as is
|
||||
# if self._hvac_mode == HVACMode.OFF and new_hvac_action == HVACAction.IDLE:
|
||||
# _LOGGER.debug(
|
||||
# "The underlying switch to idle instead of OFF. We will consider it as OFF"
|
||||
# )
|
||||
# _LOGGER.debug("The underlying switch to idle instead of OFF. We will consider it as OFF")
|
||||
# new_hvac_mode = HVACMode.OFF
|
||||
|
||||
_LOGGER.info(
|
||||
@@ -1710,17 +1736,15 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
||||
old_hvac_action,
|
||||
)
|
||||
|
||||
if new_hvac_mode in [
|
||||
HVACMode.OFF,
|
||||
HVACMode.HEAT,
|
||||
HVACMode.COOL,
|
||||
HVACMode.HEAT_COOL,
|
||||
HVACMode.DRY,
|
||||
HVACMode.AUTO,
|
||||
HVACMode.FAN_ONLY,
|
||||
None,
|
||||
]:
|
||||
self._hvac_mode = new_hvac_mode
|
||||
_LOGGER.debug(
|
||||
"%s - last_change_time=%s old_state_date_changed=%s old_state_date_updated=%s new_state_date_changed=%s new_state_date_updated=%s",
|
||||
self,
|
||||
self._last_change_time,
|
||||
old_state_date_changed,
|
||||
old_state_date_updated,
|
||||
new_state_date_changed,
|
||||
new_state_date_updated,
|
||||
)
|
||||
|
||||
# Interpretation of hvac action
|
||||
HVAC_ACTION_ON = [ # pylint: disable=invalid-name
|
||||
@@ -1955,25 +1979,25 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
||||
if self._attr_preset_mode not in [PRESET_BOOST, PRESET_COMFORT, PRESET_ECO]:
|
||||
return
|
||||
|
||||
# Change temperature with preset named _way
|
||||
new_temp = None
|
||||
if new_state == STATE_ON or new_state == STATE_HOME:
|
||||
new_temp = self._presets[self._attr_preset_mode]
|
||||
_LOGGER.info(
|
||||
"%s - Someone is back home. Restoring temperature to %.2f",
|
||||
self,
|
||||
new_temp,
|
||||
)
|
||||
else:
|
||||
new_temp = self._presets_away[
|
||||
self.get_preset_away_name(self._attr_preset_mode)
|
||||
]
|
||||
_LOGGER.info(
|
||||
"%s - No one is at home. Apply temperature %.2f",
|
||||
self,
|
||||
new_temp,
|
||||
)
|
||||
|
||||
# Change temperature with preset named _away
|
||||
# new_temp = None
|
||||
#if new_state == STATE_ON or new_state == STATE_HOME:
|
||||
# new_temp = self._presets[self._attr_preset_mode]
|
||||
# _LOGGER.info(
|
||||
# "%s - Someone is back home. Restoring temperature to %.2f",
|
||||
# self,
|
||||
# new_temp,
|
||||
# )
|
||||
#else:
|
||||
# new_temp = self._presets_away[
|
||||
# self.get_preset_away_name(self._attr_preset_mode)
|
||||
# ]
|
||||
# _LOGGER.info(
|
||||
# "%s - No one is at home. Apply temperature %.2f",
|
||||
# self,
|
||||
# new_temp,
|
||||
# )
|
||||
new_temp = self.find_preset_temp(self.preset_mode)
|
||||
if new_temp is not None:
|
||||
_LOGGER.debug(
|
||||
"%s - presence change in temperature mode new_temp will be: %.2f",
|
||||
@@ -2484,6 +2508,7 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
||||
"target_temp": self.target_temperature,
|
||||
"current_temp": self._cur_temp,
|
||||
"ext_current_temperature": self._cur_ext_temp,
|
||||
"ac_mode": self._ac_mode,
|
||||
"current_power": self._current_power,
|
||||
"current_power_max": self._current_power_max,
|
||||
"saved_preset_mode": self._saved_preset_mode,
|
||||
|
||||
@@ -245,6 +245,7 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
|
||||
vol.Optional(CONF_CLIMATE_4): selector.EntitySelector(
|
||||
selector.EntitySelectorConfig(domain=CLIMATE_DOMAIN),
|
||||
),
|
||||
vol.Optional(CONF_AC_MODE, default=False): cv.boolean,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
""" Underlying entities classes """
|
||||
import logging
|
||||
from typing import Any
|
||||
from enum import StrEnum
|
||||
|
||||
from homeassistant.const import ATTR_ENTITY_ID, STATE_ON, UnitOfTemperature
|
||||
|
||||
from homeassistant.exceptions import ServiceNotFound
|
||||
|
||||
from enum import StrEnum
|
||||
from homeassistant.core import HomeAssistant, DOMAIN as HA_DOMAIN, CALLBACK_TYPE
|
||||
from homeassistant.components.climate import (
|
||||
ClimateEntity,
|
||||
|
||||
@@ -2,4 +2,5 @@
|
||||
-r requirements_dev.txt
|
||||
aiodiscover
|
||||
ulid_transform
|
||||
pytest-asyncio
|
||||
pytest-homeassistant-custom-component
|
||||
29
scripts/starts_ha.sh
Executable file
29
scripts/starts_ha.sh
Executable file
@@ -0,0 +1,29 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
set -x
|
||||
|
||||
cd "$(dirname "$0")/.."
|
||||
pwd
|
||||
|
||||
# Create config dir if not present
|
||||
if [[ ! -d "${PWD}/config" ]]; then
|
||||
mkdir -p "${PWD}/config"
|
||||
# Add defaults configuration
|
||||
hass --config "${PWD}/config" --script ensure_config
|
||||
fi
|
||||
|
||||
# Overwrite configuration.yaml if provided
|
||||
if [ -f ${PWD}/.devcontainer/configuration.yaml ]; then
|
||||
rm -f ${PWD}/config/configuration.yaml
|
||||
ln -s ${PWD}/.devcontainer/configuration.yaml ${PWD}/config/configuration.yaml
|
||||
fi
|
||||
|
||||
# Set the path to custom_components
|
||||
## This let's us have the structure we want <root>/custom_components/integration_blueprint
|
||||
## while at the same time have Home Assistant configuration inside <root>/config
|
||||
## without resulting to symlinks.
|
||||
export PYTHONPATH="${PYTHONPATH}:${PWD}/custom_components"
|
||||
|
||||
# Start Home Assistant
|
||||
hass --config "${PWD}/config" --debug
|
||||
3
setup.cfg
Normal file
3
setup.cfg
Normal file
@@ -0,0 +1,3 @@
|
||||
[tool:pytest]
|
||||
testpaths = tests
|
||||
asyncio_mode = auto
|
||||
@@ -5,7 +5,7 @@ from unittest.mock import patch, MagicMock
|
||||
import pytest # pylint: disable=unused-import
|
||||
|
||||
from homeassistant.core import HomeAssistant, Event, EVENT_STATE_CHANGED, State
|
||||
from homeassistant.const import UnitOfTemperature, STATE_ON, STATE_OFF
|
||||
from homeassistant.const import UnitOfTemperature, STATE_ON, STATE_OFF, ATTR_TEMPERATURE
|
||||
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.util import dt as dt_util
|
||||
@@ -20,23 +20,26 @@ from homeassistant.components.climate import (
|
||||
|
||||
from pytest_homeassistant_custom_component.common import MockConfigEntry
|
||||
|
||||
from ..climate import VersatileThermostat
|
||||
from ..const import * # pylint: disable=wildcard-import, unused-wildcard-import
|
||||
from ..underlyings import * # pylint: disable=wildcard-import, unused-wildcard-import
|
||||
from custom_components.versatile_thermostat.climate import VersatileThermostat
|
||||
from custom_components.versatile_thermostat.const import * # pylint: disable=wildcard-import, unused-wildcard-import
|
||||
from custom_components.versatile_thermostat.underlyings import * # pylint: disable=wildcard-import, unused-wildcard-import
|
||||
|
||||
from .const import ( # pylint: disable=unused-import
|
||||
MOCK_TH_OVER_SWITCH_USER_CONFIG,
|
||||
MOCK_TH_OVER_4SWITCH_USER_CONFIG,
|
||||
MOCK_TH_OVER_CLIMATE_USER_CONFIG,
|
||||
MOCK_TH_OVER_SWITCH_TYPE_CONFIG,
|
||||
MOCK_TH_OVER_SWITCH_AC_TYPE_CONFIG,
|
||||
MOCK_TH_OVER_4SWITCH_TYPE_CONFIG,
|
||||
MOCK_TH_OVER_CLIMATE_TYPE_CONFIG,
|
||||
MOCK_TH_OVER_SWITCH_TPI_CONFIG,
|
||||
MOCK_PRESETS_CONFIG,
|
||||
MOCK_PRESETS_AC_CONFIG,
|
||||
MOCK_WINDOW_CONFIG,
|
||||
MOCK_MOTION_CONFIG,
|
||||
MOCK_POWER_CONFIG,
|
||||
MOCK_PRESENCE_CONFIG,
|
||||
MOCK_PRESENCE_AC_CONFIG,
|
||||
MOCK_ADVANCED_CONFIG,
|
||||
# MOCK_DEFAULT_FEATURE_CONFIG,
|
||||
PRESET_BOOST,
|
||||
@@ -58,6 +61,19 @@ FULL_SWITCH_CONFIG = (
|
||||
| MOCK_ADVANCED_CONFIG
|
||||
)
|
||||
|
||||
FULL_SWITCH_AC_CONFIG = (
|
||||
MOCK_TH_OVER_SWITCH_USER_CONFIG
|
||||
| MOCK_TH_OVER_SWITCH_AC_TYPE_CONFIG
|
||||
| MOCK_TH_OVER_SWITCH_TPI_CONFIG
|
||||
| MOCK_PRESETS_AC_CONFIG
|
||||
| MOCK_WINDOW_CONFIG
|
||||
| MOCK_MOTION_CONFIG
|
||||
| MOCK_POWER_CONFIG
|
||||
| MOCK_PRESENCE_AC_CONFIG
|
||||
| MOCK_ADVANCED_CONFIG
|
||||
)
|
||||
|
||||
|
||||
PARTIAL_CLIMATE_CONFIG = (
|
||||
MOCK_TH_OVER_CLIMATE_USER_CONFIG
|
||||
| MOCK_TH_OVER_CLIMATE_TYPE_CONFIG
|
||||
@@ -83,7 +99,7 @@ _LOGGER = logging.getLogger(__name__)
|
||||
class MockClimate(ClimateEntity):
|
||||
"""A Mock Climate class used for Underlying climate mode"""
|
||||
|
||||
def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos, 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."""
|
||||
|
||||
super().__init__()
|
||||
@@ -101,12 +117,13 @@ class MockClimate(ClimateEntity):
|
||||
self._attr_target_temperature = 20
|
||||
self._attr_current_temperature = 15
|
||||
|
||||
def set_temperature(self, temperature):
|
||||
def set_temperature(self, **kwargs):
|
||||
""" Set the target temperature"""
|
||||
temperature = kwargs.get(ATTR_TEMPERATURE)
|
||||
self._attr_target_temperature = temperature
|
||||
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"""
|
||||
self._attr_hvac_mode = hvac_mode
|
||||
self.async_write_ha_state()
|
||||
@@ -114,7 +131,7 @@ class MockClimate(ClimateEntity):
|
||||
class MockUnavailableClimate(ClimateEntity):
|
||||
"""A Mock Climate class used for Underlying climate mode"""
|
||||
|
||||
def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None:
|
||||
def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None: # pylint: disable=unused-argument
|
||||
"""Initialize the thermostat."""
|
||||
|
||||
super().__init__()
|
||||
@@ -1,4 +1,6 @@
|
||||
"""Global fixtures for integration_blueprint integration."""
|
||||
# pylint: disable=line-too-long
|
||||
|
||||
# Fixtures allow you to replace functions with a Mock object. You can perform
|
||||
# many options via the Mock to reflect a particular behavior from the original
|
||||
# function that you want to see without going through the function's actual logic.
|
||||
@@ -34,7 +36,7 @@ pytest_plugins = "pytest_homeassistant_custom_component" # pylint: disable=inva
|
||||
# This fixture enables loading custom integrations in all tests.
|
||||
# Remove to enable selective use of this fixture
|
||||
@pytest.fixture(autouse=True)
|
||||
def auto_enable_custom_integrations(enable_custom_integrations):
|
||||
def auto_enable_custom_integrations(enable_custom_integrations): # pylint: disable=unused-argument
|
||||
"""Enable all integration in tests"""
|
||||
yield
|
||||
|
||||
@@ -51,7 +51,6 @@ from custom_components.versatile_thermostat.const import (
|
||||
PRESET_AWAY_SUFFIX,
|
||||
CONF_CLIMATE,
|
||||
)
|
||||
|
||||
MOCK_TH_OVER_SWITCH_USER_CONFIG = {
|
||||
CONF_NAME: "TheOverSwitchMockName",
|
||||
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_SWITCH,
|
||||
@@ -100,6 +99,12 @@ MOCK_TH_OVER_SWITCH_TYPE_CONFIG = {
|
||||
CONF_AC_MODE: False,
|
||||
}
|
||||
|
||||
MOCK_TH_OVER_SWITCH_AC_TYPE_CONFIG = {
|
||||
CONF_HEATER: "switch.mock_air_conditioner",
|
||||
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
|
||||
CONF_AC_MODE: True,
|
||||
}
|
||||
|
||||
MOCK_TH_OVER_4SWITCH_TYPE_CONFIG = {
|
||||
CONF_HEATER: "switch.mock_4switch0",
|
||||
CONF_HEATER_2: "switch.mock_4switch1",
|
||||
@@ -116,6 +121,7 @@ MOCK_TH_OVER_SWITCH_TPI_CONFIG = {
|
||||
|
||||
MOCK_TH_OVER_CLIMATE_TYPE_CONFIG = {
|
||||
CONF_CLIMATE: "climate.mock_climate",
|
||||
CONF_AC_MODE: False,
|
||||
}
|
||||
|
||||
MOCK_PRESETS_CONFIG = {
|
||||
@@ -124,6 +130,15 @@ MOCK_PRESETS_CONFIG = {
|
||||
PRESET_BOOST + "_temp": 18,
|
||||
}
|
||||
|
||||
MOCK_PRESETS_AC_CONFIG = {
|
||||
PRESET_ECO + "_temp": 17,
|
||||
PRESET_COMFORT + "_temp": 19,
|
||||
PRESET_BOOST + "_temp": 20,
|
||||
PRESET_ECO + "_ac_temp": 25,
|
||||
PRESET_COMFORT + "_ac_temp": 23,
|
||||
PRESET_BOOST + "_ac_temp": 21,
|
||||
}
|
||||
|
||||
MOCK_WINDOW_CONFIG = {
|
||||
CONF_WINDOW_SENSOR: "binary_sensor.window_sensor",
|
||||
CONF_WINDOW_DELAY: 10,
|
||||
@@ -156,6 +171,16 @@ MOCK_PRESENCE_CONFIG = {
|
||||
PRESET_BOOST + PRESET_AWAY_SUFFIX + "_temp": 18,
|
||||
}
|
||||
|
||||
MOCK_PRESENCE_AC_CONFIG = {
|
||||
CONF_PRESENCE_SENSOR: "person.presence_sensor",
|
||||
PRESET_ECO + PRESET_AWAY_SUFFIX + "_temp": 16,
|
||||
PRESET_COMFORT + PRESET_AWAY_SUFFIX + "_temp": 17,
|
||||
PRESET_BOOST + PRESET_AWAY_SUFFIX + "_temp": 18,
|
||||
PRESET_ECO + "_ac" + PRESET_AWAY_SUFFIX + "_temp": 27,
|
||||
PRESET_COMFORT + "_ac" + PRESET_AWAY_SUFFIX + "_temp": 26,
|
||||
PRESET_BOOST + "_ac" + PRESET_AWAY_SUFFIX + "_temp": 25,
|
||||
}
|
||||
|
||||
MOCK_ADVANCED_CONFIG = {
|
||||
CONF_MINIMAL_ACTIVATION_DELAY: 10,
|
||||
CONF_SECURITY_DELAY_MIN: 5,
|
||||
@@ -9,9 +9,8 @@ from homeassistant.components.binary_sensor import BinarySensorDeviceClass
|
||||
|
||||
from pytest_homeassistant_custom_component.common import MockConfigEntry
|
||||
|
||||
from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import
|
||||
from ..climate import VersatileThermostat
|
||||
from ..binary_sensor import (
|
||||
from custom_components.versatile_thermostat.climate import VersatileThermostat
|
||||
from custom_components.versatile_thermostat.binary_sensor import (
|
||||
SecurityBinarySensor,
|
||||
OverpoweringBinarySensor,
|
||||
WindowBinarySensor,
|
||||
@@ -29,7 +28,7 @@ async def test_security_binary_sensors(
|
||||
skip_hass_states_is_state,
|
||||
skip_turn_on_off_heater,
|
||||
skip_send_event,
|
||||
):
|
||||
): # pylint: disable=unused-argument
|
||||
"""Test the security binary sensors in thermostat type"""
|
||||
|
||||
entry = MockConfigEntry(
|
||||
@@ -29,7 +29,7 @@ async def test_show_form(hass: HomeAssistant) -> None:
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_user_config_flow_over_switch(hass: HomeAssistant, skip_hass_states_get):
|
||||
async def test_user_config_flow_over_switch(hass: HomeAssistant, skip_hass_states_get): # pylint: disable=unused-argument
|
||||
"""Test the config flow with all thermostat_over_switch features"""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
@@ -128,7 +128,7 @@ async def test_user_config_flow_over_switch(hass: HomeAssistant, skip_hass_state
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_user_config_flow_over_climate(hass: HomeAssistant, skip_hass_states_get):
|
||||
async def test_user_config_flow_over_climate(hass: HomeAssistant, skip_hass_states_get): # pylint: disable=unused-argument
|
||||
"""Test the config flow with all thermostat_over_climate features and no additional features"""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
@@ -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_timers", [True])
|
||||
async def test_user_config_flow_window_auto_ok(
|
||||
hass: HomeAssistant, skip_hass_states_get, skip_control_heating
|
||||
hass: HomeAssistant, skip_hass_states_get, skip_control_heating # pylint: disable=unused-argument
|
||||
):
|
||||
"""Test the config flow with only window auto feature"""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
@@ -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_timers", [True])
|
||||
async def test_user_config_flow_window_auto_ko(
|
||||
hass: HomeAssistant, skip_hass_states_get
|
||||
hass: HomeAssistant, skip_hass_states_get # pylint: disable=unused-argument
|
||||
):
|
||||
"""Test the config flow with window auto and window features -> not allowed"""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
@@ -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_timers", [True])
|
||||
async def test_user_config_flow_over_4_switches(
|
||||
hass: HomeAssistant, skip_hass_states_get, skip_control_heating
|
||||
hass: HomeAssistant, skip_hass_states_get, skip_control_heating # pylint: disable=unused-argument
|
||||
):
|
||||
"""Test the config flow with 4 switchs thermostat_over_switch features"""
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
""" Test the Window management """
|
||||
import asyncio
|
||||
from datetime import datetime, timedelta
|
||||
import logging
|
||||
from unittest.mock import patch, call, PropertyMock
|
||||
from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
import logging
|
||||
|
||||
logging.getLogger().setLevel(logging.DEBUG)
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
""" Test the Multiple switch management """
|
||||
import asyncio
|
||||
from unittest.mock import patch, call, ANY
|
||||
from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
import logging
|
||||
|
||||
from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import
|
||||
|
||||
logging.getLogger().setLevel(logging.DEBUG)
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ async def test_one_switch_cycle(
|
||||
hass: HomeAssistant,
|
||||
skip_hass_states_is_state,
|
||||
skip_send_event,
|
||||
):
|
||||
): # pylint: disable=unused-argument
|
||||
"""Test that when multiple switch are configured the activation is distributed"""
|
||||
|
||||
tz = get_tz(hass) # pylint: disable=invalid-name
|
||||
@@ -75,7 +75,7 @@ async def test_one_switch_cycle(
|
||||
with patch(
|
||||
"homeassistant.core.StateMachine.is_state", return_value=False
|
||||
) as mock_is_state:
|
||||
assert entity._is_device_active is False
|
||||
assert entity._is_device_active is False # pylint: disable=protected-access
|
||||
|
||||
# Should be call for the Switch
|
||||
assert mock_is_state.call_count == 1
|
||||
@@ -132,7 +132,8 @@ async def test_one_switch_cycle(
|
||||
assert mock_send_event.call_count == 0
|
||||
assert mock_heater_off.call_count == 0
|
||||
|
||||
# The first heater should be turned on but is already on but because above we mock call_later the heater is not on. But this time it will be really on
|
||||
# The first heater should be turned on but is already on but because above we mock
|
||||
# call_later the heater is not on. But this time it will be really on
|
||||
assert mock_heater_on.call_count == 1
|
||||
|
||||
# Set another temperature at middle level
|
||||
@@ -153,12 +154,15 @@ async def test_one_switch_cycle(
|
||||
assert mock_send_event.call_count == 0
|
||||
assert mock_heater_off.call_count == 0
|
||||
|
||||
# The heater is already on cycle. So we wait that the cycle ends and no heater action is done
|
||||
# The heater is already on cycle. So we wait that the cycle ends and no heater action
|
||||
# is done
|
||||
assert mock_heater_on.call_count == 0
|
||||
# assert entity.underlying_entity(0)._should_relaunch_control_heating is True
|
||||
|
||||
# Simulate the relaunch
|
||||
await entity.underlying_entity(0)._turn_on_later(None)
|
||||
await entity.underlying_entity(0)._turn_on_later( # pylint: disable=protected-access
|
||||
None
|
||||
)
|
||||
# wait restart
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
@@ -177,7 +181,9 @@ async def test_one_switch_cycle(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
|
||||
return_value=True,
|
||||
) as mock_device_active:
|
||||
await entity.underlying_entity(0)._turn_off_later(None)
|
||||
await entity.underlying_entity(0)._turn_off_later( # pylint: disable=protected-access
|
||||
None
|
||||
)
|
||||
|
||||
# No special event
|
||||
assert mock_send_event.call_count == 0
|
||||
@@ -198,7 +204,9 @@ async def test_one_switch_cycle(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
|
||||
return_value=True,
|
||||
) as mock_device_active:
|
||||
await entity.underlying_entity(0)._turn_on_later(None)
|
||||
await entity.underlying_entity(0)._turn_on_later( # pylint: disable=protected-access
|
||||
None
|
||||
)
|
||||
|
||||
# No special event
|
||||
assert mock_send_event.call_count == 0
|
||||
@@ -214,7 +222,7 @@ async def test_multiple_switchs(
|
||||
hass: HomeAssistant,
|
||||
skip_hass_states_is_state,
|
||||
skip_send_event,
|
||||
):
|
||||
): # pylint: disable=unused-argument
|
||||
"""Test that when multiple switch are configured the activation is distributed"""
|
||||
|
||||
tz = get_tz(hass) # pylint: disable=invalid-name
|
||||
@@ -277,7 +285,7 @@ async def test_multiple_switchs(
|
||||
await send_temperature_change_event(entity, 15, event_timestamp)
|
||||
|
||||
# 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
|
||||
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_heater_off.call_count == 0
|
||||
|
||||
# The first heater should be turned on but is already on but because call_later is mocked, it is only turned on here
|
||||
# The first heater should be turned on but is already on but because call_later
|
||||
# is mocked, it is only turned on here
|
||||
assert mock_heater_on.call_count == 1
|
||||
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_multiple_climates(
|
||||
hass: HomeAssistant,
|
||||
skip_hass_states_is_state,
|
||||
skip_send_event,
|
||||
):
|
||||
"""Test that when multiple climates are configured the activation and deactivation is propagated to all climates"""
|
||||
): # pylint: disable=unused-argument
|
||||
"""Test that when multiple climates are configured the activation and deactivation
|
||||
is propagated to all climates"""
|
||||
|
||||
tz = get_tz(hass) # pylint: disable=invalid-name
|
||||
now: datetime = datetime.now(tz=tz)
|
||||
@@ -416,7 +427,7 @@ async def test_multiple_climates(
|
||||
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
|
||||
with patch(
|
||||
@@ -441,7 +452,8 @@ async def test_multiple_climates(
|
||||
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_timers", [True])
|
||||
@@ -449,8 +461,9 @@ async def test_multiple_climates_underlying_changes(
|
||||
hass: HomeAssistant,
|
||||
skip_hass_states_is_state,
|
||||
skip_send_event,
|
||||
):
|
||||
"""Test that when multiple switch are configured the activation of one underlying climate activate the others"""
|
||||
): # pylint: disable=unused-argument
|
||||
"""Test that when multiple switch are configured the activation of one underlying
|
||||
climate activate the others"""
|
||||
|
||||
tz = get_tz(hass) # pylint: disable=invalid-name
|
||||
now: datetime = datetime.now(tz=tz)
|
||||
@@ -514,7 +527,7 @@ async def test_multiple_climates_underlying_changes(
|
||||
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
|
||||
with patch(
|
||||
@@ -524,7 +537,14 @@ async def test_multiple_climates_underlying_changes(
|
||||
) as mock_underlying_set_hvac_mode:
|
||||
# Wait 11 sec so that the event will not be discarded
|
||||
event_timestamp = now + timedelta(seconds=11)
|
||||
await send_climate_change_event(entity, HVACMode.OFF, HVACMode.HEAT, HVACAction.OFF, HVACAction.HEATING, event_timestamp)
|
||||
await send_climate_change_event(
|
||||
entity,
|
||||
HVACMode.OFF,
|
||||
HVACMode.HEAT,
|
||||
HVACAction.OFF,
|
||||
HVACAction.HEATING,
|
||||
event_timestamp,
|
||||
)
|
||||
|
||||
# Should be call for all Switch
|
||||
assert mock_underlying_set_hvac_mode.call_count == 4
|
||||
@@ -534,7 +554,7 @@ async def test_multiple_climates_underlying_changes(
|
||||
]
|
||||
)
|
||||
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
|
||||
with patch(
|
||||
@@ -542,12 +562,21 @@ async def test_multiple_climates_underlying_changes(
|
||||
), patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.set_hvac_mode"
|
||||
) as mock_underlying_set_hvac_mode, patch(
|
||||
# notice that there is no need of return_value=HVACAction.IDLE because this is not a function but a property
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.hvac_action", HVACAction.IDLE
|
||||
) as mock_underlying_get_hvac_action:
|
||||
# notice that there is no need of return_value=HVACAction.IDLE because this is not
|
||||
# a function but a property
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.hvac_action",
|
||||
HVACAction.IDLE,
|
||||
):
|
||||
# Wait 11 sec so that the event will not be discarded
|
||||
event_timestamp = now + timedelta(seconds=11)
|
||||
await send_climate_change_event(entity, HVACMode.HEAT, HVACMode.OFF, HVACAction.IDLE, HVACAction.OFF, event_timestamp)
|
||||
await send_climate_change_event(
|
||||
entity,
|
||||
HVACMode.HEAT,
|
||||
HVACMode.OFF,
|
||||
HVACAction.IDLE,
|
||||
HVACAction.OFF,
|
||||
event_timestamp,
|
||||
)
|
||||
|
||||
# Should be call for all Switch
|
||||
assert mock_underlying_set_hvac_mode.call_count == 4
|
||||
@@ -558,5 +587,4 @@ async def test_multiple_climates_underlying_changes(
|
||||
)
|
||||
assert entity.hvac_mode == HVACMode.HEAT
|
||||
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 .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(
|
||||
@@ -12,8 +12,8 @@ from homeassistant.const import UnitOfTime, UnitOfPower, UnitOfEnergy, PERCENTAG
|
||||
|
||||
from pytest_homeassistant_custom_component.common import MockConfigEntry
|
||||
|
||||
from ..climate import VersatileThermostat
|
||||
from ..sensor import (
|
||||
from custom_components.versatile_thermostat.climate import VersatileThermostat
|
||||
from custom_components.versatile_thermostat.sensor import (
|
||||
EnergySensor,
|
||||
MeanPowerSensor,
|
||||
OnPercentSensor,
|
||||
@@ -10,7 +10,7 @@ from homeassistant.components.climate import ClimateEntity, DOMAIN as CLIMATE_DO
|
||||
|
||||
from pytest_homeassistant_custom_component.common import MockConfigEntry
|
||||
|
||||
from ..climate import VersatileThermostat
|
||||
from custom_components.versatile_thermostat.climate import VersatileThermostat
|
||||
|
||||
from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import
|
||||
|
||||
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_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"""
|
||||
|
||||
entry = MockConfigEntry(
|
||||
@@ -40,7 +42,7 @@ async def test_tpi_calculation(hass: HomeAssistant, skip_hass_states_is_state: N
|
||||
)
|
||||
assert entity
|
||||
|
||||
tpi_algo = entity._prop_algorithm
|
||||
tpi_algo = entity._prop_algorithm # pylint: disable=protected-access
|
||||
assert tpi_algo
|
||||
|
||||
tpi_algo.calculate(15, 10, 7)
|
||||
Reference in New Issue
Block a user