Compare commits

..

1 Commits

Author SHA1 Message Date
Jean-Marc Collin
75744675a7 Issue #128 - add motion_off_delay 2023-10-20 23:40:57 +02:00
33 changed files with 251 additions and 723 deletions

View File

@@ -1,4 +1,6 @@
echo "Sourcing .bashrc" echo "Sourcing .bashrc"
alias ll='ls -l' alias ll='ls -l'
# source venv/bin/activate export HA='/home/vscode/core'
cd $HA
source venv/bin/activate

View File

@@ -1,54 +1,44 @@
// 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/devcontainers/python:1-3.11-bullseye", "image": "mcr.microsoft.com/vscode/devcontainers/python:0-3.10",
"name": "Versatile Thermostat integration", "name": "Versatile Thermostat integration",
"context": "..",
"appPort": [ "appPort": [
"9123:8123" "9123:8123"
], ],
// "postCreateCommand": "container install", // "postCreateCommand": "container install",
"postCreateCommand": "./container dev-setup", "postCreateCommand": "./container install",
"extensions": [
"mounts": [ "ms-python.python",
"source=/Users/jmcollin/.ssh,target=/home/vscode/.ssh,type=bind,consistency=cached" "github.vscode-pull-request-github",
"ryanluker.vscode-coverage-gutters",
"ms-python.vscode-pylance"
], ],
"mounts": [
"customizations": { "source=/Users/jmcollin/SugarSync/Projets/home-assistant/core,target=/home/vscode/core,type=bind,consistency=cached",
"vscode": { "source=${localWorkspaceFolder}/.devcontainer/configuration.yaml,target=/home/vscode/core/config/configuration.yaml,type=bind,consistency=cached",
"extensions": [ "source=${localWorkspaceFolder}/custom_components,target=/home/vscode/core/config/custom_components,type=bind,consistency=cached"
"ms-python.python", ],
"github.vscode-pull-request-github", "settings": {
"ryanluker.vscode-coverage-gutters", "files.eol": "\n",
"ms-python.vscode-pylance" "editor.tabSize": 4,
], "terminal.integrated.profiles.linux": {
// "mounts": [ "Bash Profile": {
// "source=${localWorkspaceFolder}/.devcontainer/configuration.yaml,target=/home/vscode/core/config/configuration.yaml,type=bind,consistency=cached", "path": "bash",
// "source=${localWorkspaceFolder}/custom_components,target=/home/vscode/core/config/custom_components,type=bind,consistency=cached" "args": []
// ],
"settings": {
"files.eol": "\n",
"editor.tabSize": 4,
"terminal.integrated.profiles.linux": {
"bash": {
"path": "bash",
"args": []
}
},
"terminal.integrated.defaultProfile.linux": "bash",
// "terminal.integrated.shell.linux": "/bin/bash",
"python.pythonPath": "/usr/bin/python3",
"python.analysis.autoSearchPaths": true,
"python.linting.pylintEnabled": true,
"python.linting.enabled": true,
"python.formatting.provider": "black",
"python.formatting.blackPath": "/usr/local/py-utils/bin/black",
"editor.formatOnPaste": false,
"editor.formatOnSave": true,
"editor.formatOnType": true,
"files.trimTrailingWhitespace": true,
"python.experiments.optOutFrom": ["pythonTestAdapter"],
"python.analysis.logLevel": "Trace"
} }
} },
"terminal.integrated.defaultProfile.linux": "Bash Profile",
// "terminal.integrated.shell.linux": "/bin/bash",
"python.pythonPath": "/usr/bin/python3",
"python.analysis.autoSearchPaths": true,
"python.linting.pylintEnabled": true,
"python.linting.enabled": true,
"python.formatting.provider": "black",
"editor.formatOnPaste": false,
"editor.formatOnSave": true,
"editor.formatOnType": true,
"files.trimTrailingWhitespace": true
} }
} }

6
.gitignore vendored
View File

@@ -103,8 +103,4 @@ dist
# TernJS port file # TernJS port file
.tern-port .tern-port
# init file required for unittest __pycache__
custom_components/__init__.py
__pycache__
config/**

35
.vscode/launch.json vendored
View File

@@ -3,15 +3,36 @@
"version": "0.2.0", "version": "0.2.0",
"configurations": [ "configurations": [
{ {
"name": "Home Assistant (debug)", // Example of attaching to local debug server
"name": "Python: Attach Local",
"type": "python", "type": "python",
"request": "launch", "request": "attach",
"module": "homeassistant", "port": 5678,
"host": "localhost",
"justMyCode": false, "justMyCode": false,
"args": [ "pathMappings": [
"--debug", // {
"-c", // "localRoot": "${workspaceFolder}",
"config" // "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"
}
] ]
} }
] ]

16
.vscode/settings.json vendored
View File

@@ -1,20 +1,12 @@
{ {
"[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/custom_components/versatile_thermostat" "/workspaces/versatile_thermostat"
], ]
"python.formatting.provider": "none"
} }

View File

@@ -1,61 +0,0 @@
# 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.

View File

@@ -1,61 +0,0 @@
# 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.

View File

@@ -4,43 +4,35 @@
. .bashrc . .bashrc
function get_dev() { cd $HA
pip install -r requirements_dev.txt
pip install -r requirements_test.txt
if [ -d /home/vscode/core ]; then
sudo chown -R vscode: /home/vscode/core
fi
}
echo "arguments are: "$1 echo "arguments are: "$*
# Post installation of container
command=$1
if [ "$command" == "install" ]; then
echo "Running container post installation"
script/setup
fi
case $1 in if [ "$command" == "start" ]; then
start) echo "Running container start"
echo "Running container start" hass -c ./config --debug
./scripts/starts_ha.sh fi
;;
dev-setup) if [ "$command" == "translations" ]; then
get_dev echo "Running container start"
;; python3 -m script.translations develop
install) fi
echo "Running container post installation"
script/setup if [ "$command" == "hassfest" ]; then
;; echo "Running container start"
translations) python3 -m script.hassfest
echo "Running container start" # python -m script.hassfest --requirements --action validate --integration-path config/custom_components/versatile_thermostat/
cd $HA fi
python3 -m script.translations develop
;; if [ "$command" == "restart" ]; then
hassfest) echo "Killing existing container"
echo "Running container start" pkill hass
python3 -m script.hassfest echo "Killing existing container"
# python -m script.hassfest --requirements --action validate --integration-path config/custom_components/versatile_thermostat/ hass -c ./config
;; fi
restart)
echo "Killing existing container"
pkill hass
echo "Restarting existing container"
pwd
./scripts/starts_ha.sh
;;
esac

View File

@@ -54,9 +54,7 @@ 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__( def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None:
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"
@@ -89,9 +87,7 @@ 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__( def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None:
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"
@@ -124,9 +120,7 @@ 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__( def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None:
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"
@@ -140,10 +134,7 @@ 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 [ if self.my_climate.window_state in [STATE_ON, STATE_OFF] or self.my_climate.window_auto_state in [STATE_ON, STATE_OFF]:
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
@@ -170,9 +161,7 @@ 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__( def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None:
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"
@@ -206,9 +195,7 @@ 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__( def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None:
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"

View File

@@ -1,6 +1,3 @@
# 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
@@ -304,7 +301,7 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
entry_infos, entry_infos,
) )
self._ac_mode = entry_infos.get(CONF_AC_MODE) is True self._ac_mode = entry_infos.get(CONF_AC_MODE) == True
# convert entry_infos into usable attributes # convert entry_infos into usable attributes
presets = {} presets = {}
items = CONF_PRESETS_WITH_AC.items() if self._ac_mode else CONF_PRESETS.items() items = CONF_PRESETS_WITH_AC.items() if self._ac_mode else CONF_PRESETS.items()
@@ -342,12 +339,7 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
self._thermostat_type = entry_infos.get(CONF_THERMOSTAT_TYPE) self._thermostat_type = entry_infos.get(CONF_THERMOSTAT_TYPE)
if self._thermostat_type == CONF_THERMOSTAT_CLIMATE: if self._thermostat_type == CONF_THERMOSTAT_CLIMATE:
self._is_over_climate = True self._is_over_climate = True
for climate in [ for climate in [CONF_CLIMATE, CONF_CLIMATE_2, CONF_CLIMATE_3, CONF_CLIMATE_4]:
CONF_CLIMATE,
CONF_CLIMATE_2,
CONF_CLIMATE_3,
CONF_CLIMATE_4,
]:
if entry_infos.get(climate): if entry_infos.get(climate):
self._underlyings.append( self._underlyings.append(
UnderlyingClimate( UnderlyingClimate(
@@ -655,11 +647,7 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
# Initialize all UnderlyingEntities # Initialize all UnderlyingEntities
for under in self._underlyings: for under in self._underlyings:
try: under.startup()
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 (
@@ -781,10 +769,7 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
self.async_write_ha_state() self.async_write_ha_state()
if self._prop_algorithm: if self._prop_algorithm:
self._prop_algorithm.calculate( self._prop_algorithm.calculate(
self._target_temp, self._target_temp, self._cur_temp, self._cur_ext_temp
self._cur_temp,
self._cur_ext_temp,
self._hvac_mode == HVACMode.COOL,
) )
self.hass.create_task(self._check_switch_initial_state()) self.hass.create_task(self._check_switch_initial_state())
@@ -977,8 +962,8 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
# Issue #114 - returns my current hvac_mode and not the underlying hvac_mode which could be different # Issue #114 - returns my current hvac_mode and not the underlying hvac_mode which could be different
# delta will be managed by climate_state_change event. # delta will be managed by climate_state_change event.
# if self._is_over_climate: # if self._is_over_climate:
# if one not OFF -> return it # if one not OFF -> return it
# else OFF # else OFF
# for under in self._underlyings: # for under in self._underlyings:
# if (mode := under.hvac_mode) not in [HVACMode.OFF] # if (mode := under.hvac_mode) not in [HVACMode.OFF]
# return mode # return mode
@@ -998,10 +983,7 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
# else OFF # else OFF
one_idle = False one_idle = False
for under in self._underlyings: for under in self._underlyings:
if (action := under.hvac_action) not in [ if (action := under.hvac_action) not in [HVACAction.IDLE, HVACAction.OFF]:
HVACAction.IDLE,
HVACAction.OFF,
]:
return action return action
if under.hvac_action == HVACAction.IDLE: if under.hvac_action == HVACAction.IDLE:
one_idle = True one_idle = True
@@ -1013,8 +995,6 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
return HVACAction.OFF return HVACAction.OFF
if not self._is_device_active: if not self._is_device_active:
return HVACAction.IDLE return HVACAction.IDLE
if self._hvac_mode == HVACMode.COOL:
return HVACAction.COOLING
return HVACAction.HEATING return HVACAction.HEATING
@property @property
@@ -1086,7 +1066,7 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
@property @property
def mean_cycle_power(self) -> float | None: def mean_cycle_power(self) -> float | None:
"""Returns the mean power consumption during the cycle""" """Returns tne mean power consumption during the cycle"""
if not self._device_power or self._is_over_climate: if not self._device_power or self._is_over_climate:
return None return None
@@ -1319,9 +1299,7 @@ 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( def reset_last_change_time(self, old_preset_mode=None):
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)
@@ -1555,11 +1533,6 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
# Check delay condition # Check delay condition
async def try_motion_condition(_): async def try_motion_condition(_):
try: try:
delay = (
self._motion_delay_sec
if new_state.state == STATE_ON
else self._motion_off_delay_sec
)
long_enough = condition.state( long_enough = condition.state(
self.hass, self.hass,
self._motion_sensor_entity_id, self._motion_sensor_entity_id,
@@ -1573,83 +1546,48 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
_LOGGER.debug( _LOGGER.debug(
"Motion delay condition is not satisfied. Ignore motion event" "Motion delay condition is not satisfied. Ignore motion event"
) )
else: return
_LOGGER.debug("%s - Motion delay condition is satisfied", self)
self._motion_state = new_state.state
if self._attr_preset_mode == PRESET_ACTIVITY:
new_preset = (
self._motion_preset
if self._motion_state == STATE_ON
else self._no_motion_preset
)
_LOGGER.info(
"%s - Motion condition have changes. New preset temp will be %s",
self,
new_preset,
)
# We do not change the preset which is kept to ACTIVITY but only the target_temperature
# We take the presence into account
await self._async_internal_set_temperature(
self.find_preset_temp(new_preset)
)
self.recalculate()
await self._async_control_heating(force=True)
self._motion_call_cancel = None
im_on = self._motion_state == STATE_ON _LOGGER.debug("%s - Motion delay condition is satisfied", self)
delay_running = self._motion_call_cancel is not None self._motion_state = new_state.state
event_on = new_state.state == STATE_ON if self._attr_preset_mode == PRESET_ACTIVITY:
new_preset = (
self._motion_preset
if self._motion_state == STATE_ON
else self._no_motion_preset
)
_LOGGER.info(
"%s - Motion condition have changes. New preset temp will be %s",
self,
new_preset,
)
# We do not change the preset which is kept to ACTIVITY but only the target_temperature
# We take the presence into account
await self._async_internal_set_temperature(
self.find_preset_temp(new_preset)
)
self.recalculate()
await self._async_control_heating(force=True)
def arm(): if self._motion_call_cancel:
"""Arm the timer"""
delay = (
self._motion_delay_sec
if new_state.state == STATE_ON
else self._motion_off_delay_sec
)
self._motion_call_cancel = async_call_later(
self.hass, timedelta(seconds=delay), try_motion_condition
)
def desarm():
# restart the timer
self._motion_call_cancel() self._motion_call_cancel()
self._motion_call_cancel = None self._motion_call_cancel = None
# if I'm off # Delay
if not im_on: delay = self._motion_delay_sec if new_state.state == STATE_ON else self._motion_off_delay_sec
if event_on and not delay_running: self._motion_call_cancel = async_call_later(
_LOGGER.debug( self.hass, timedelta(seconds=delay), try_motion_condition
"%s - Arm delay cause i'm off and event is on and no delay is running", )
self,
) # For testing purpose we need to access the inner function
arm() return try_motion_condition
return try_motion_condition
# Ignore the event
_LOGGER.debug("%s - Event ignored cause i'm already off", self)
return None
else: # I'm On
if not event_on and not delay_running:
_LOGGER.info("%s - Arm delay cause i'm on and event is off", self)
arm()
return try_motion_condition
if event_on and delay_running:
_LOGGER.debug(
"%s - Desarm off delay cause i'm on and event is on and a delay is running",
self,
)
desarm()
return None
# Ignore the event
_LOGGER.debug("%s - Event ignored cause i'm already on", self)
return None
@callback @callback
async def _check_switch_initial_state(self): async def _check_switch_initial_state(self):
"""Prevent the device from keep running if HVAC_MODE_OFF.""" """Prevent the device from keep running if HVAC_MODE_OFF."""
_LOGGER.debug("%s - Calling _check_switch_initial_state", self) _LOGGER.debug("%s - Calling _check_switch_initial_state", self)
# We need to do the same check for over_climate underlyings # We need to do the same check for over_climate underlyings
# if self.is_over_climate: #if self.is_over_climate:
# return # return
for under in self._underlyings: for under in self._underlyings:
await under.check_initial_state(self._hvac_mode) await under.check_initial_state(self._hvac_mode)
@@ -1668,16 +1606,16 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
@callback @callback
async def _async_climate_changed(self, event): async def _async_climate_changed(self, event):
"""Handle unerdlying climate state changes. """Handle unerdlying climate state changes.
This method takes the underlying values and update the VTherm with them. This method takes the underlying values and update the VTherm with them.
To avoid loops (issues #121 #101 #95 #99), we discard the event if it is received To avoid loops (issues #121 #101 #95 #99), we discard the event if it is received
less than 10 sec after the last command. What we want here is to take the values less than 10 sec after the last command. What we want here is to take the values
from underlyings ONLY if someone have change directly on the underlying and not from underlyings ONLY if someone have change directly on the underlying and not
as a return of the command. The only thing we take all the time is the HVACAction as a return of the command. The only thing we take all the time is the HVACAction
which is important for feedaback and which cannot generates loops. which is important for feedaback and which cannot generates loops.
""" """
async def end_climate_changed(changes): async def end_climate_changed(changes):
"""To end the event management""" """ To end the event management"""
if changes: if changes:
self.async_write_ha_state() self.async_write_ha_state()
self.update_custom_attributes() self.update_custom_attributes()
@@ -1703,22 +1641,14 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
else None else None
) )
old_state_date_changed = ( old_state_date_changed = old_state.last_changed if old_state and old_state.last_changed else None
old_state.last_changed if old_state and old_state.last_changed else None old_state_date_updated = old_state.last_updated if old_state and old_state.last_updated else None
) new_state_date_changed = new_state.last_changed if new_state and new_state.last_changed else None
old_state_date_updated = ( new_state_date_updated = new_state.last_updated if new_state and new_state.last_updated else None
old_state.last_updated if old_state and old_state.last_updated else None
)
new_state_date_changed = (
new_state.last_changed if new_state and new_state.last_changed else None
)
new_state_date_updated = (
new_state.last_updated if new_state and new_state.last_updated else None
)
# Issue 99 - some AC turn hvac_mode=cool and hvac_action=idle when sending a HVACMode_OFF command # Issue 99 - some AC turn hvac_mode=cool and hvac_action=idle when sending a HVACMode_OFF command
# Issue 114 - Remove this because hvac_mode is now managed by local _hvac_mode and use idle action as is # Issue 114 - Remove this because hvac_mode is now managed by local _hvac_mode and use idle action as is
# if self._hvac_mode == HVACMode.OFF and new_hvac_action == HVACAction.IDLE: #if self._hvac_mode == HVACMode.OFF and new_hvac_action == HVACAction.IDLE:
# _LOGGER.debug("The underlying switch to idle instead of OFF. We will consider it as OFF") # _LOGGER.debug("The underlying switch to idle instead of OFF. We will consider it as OFF")
# new_hvac_mode = HVACMode.OFF # new_hvac_mode = HVACMode.OFF
@@ -1731,15 +1661,7 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
old_hvac_action, old_hvac_action,
) )
_LOGGER.debug( _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)
"%s - last_change_time=%s old_state_date_changed=%s old_state_date_updated=%s new_state_date_changed=%s new_state_date_updated=%s",
self,
self._last_change_time,
old_state_date_changed,
old_state_date_updated,
new_state_date_changed,
new_state_date_updated,
)
# Interpretation of hvac action # Interpretation of hvac action
HVAC_ACTION_ON = [ # pylint: disable=invalid-name HVAC_ACTION_ON = [ # pylint: disable=invalid-name
@@ -1785,27 +1707,21 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
if new_state_date_updated and self._last_change_time: if new_state_date_updated and self._last_change_time:
delta = (new_state_date_updated - self._last_change_time).total_seconds() delta = (new_state_date_updated - self._last_change_time).total_seconds()
if delta < 10: if delta < 10:
_LOGGER.info( _LOGGER.info("%s - underlying event is received less than 10 sec after command. Forget it to avoid loop", self
"%s - underlying event is received less than 10 sec after command. Forget it to avoid loop",
self,
) )
await end_climate_changed(changes) await end_climate_changed(changes)
return return
if ( if new_hvac_mode in [
new_hvac_mode HVACMode.OFF,
in [ HVACMode.HEAT,
HVACMode.OFF, HVACMode.COOL,
HVACMode.HEAT, HVACMode.HEAT_COOL,
HVACMode.COOL, HVACMode.DRY,
HVACMode.HEAT_COOL, HVACMode.AUTO,
HVACMode.DRY, HVACMode.FAN_ONLY,
HVACMode.AUTO, None
HVACMode.FAN_ONLY, ] and self._hvac_mode != new_hvac_mode:
None,
]
and self._hvac_mode != new_hvac_mode
):
changes = True changes = True
self._hvac_mode = new_hvac_mode self._hvac_mode = new_hvac_mode
# Update all underlyings state # Update all underlyings state
@@ -1815,27 +1731,15 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
if not changes: if not changes:
# try to manage new target temperature set if state # try to manage new target temperature set if state
_LOGGER.debug( _LOGGER.debug("Do temperature check. temperature is %s, new_state.attributes is %s", self.target_temperature, new_state.attributes)
"Do temperature check. temperature is %s, new_state.attributes is %s", if self._is_over_climate and new_state.attributes and (new_target_temp := new_state.attributes.get("temperature")) and new_target_temp != self.target_temperature:
self.target_temperature, _LOGGER.info("%s - Target temp in underlying have change to %s", self, new_target_temp)
new_state.attributes, await self.async_set_temperature(temperature = new_target_temp)
)
if (
self._is_over_climate
and new_state.attributes
and (new_target_temp := new_state.attributes.get("temperature"))
and new_target_temp != self.target_temperature
):
_LOGGER.info(
"%s - Target temp in underlying have change to %s",
self,
new_target_temp,
)
await self.async_set_temperature(temperature=new_target_temp)
changes = True changes = True
await end_climate_changed(changes) await end_climate_changed(changes)
@callback @callback
async def _async_update_temp(self, state: State): async def _async_update_temp(self, state: State):
"""Update thermostat with latest state from sensor.""" """Update thermostat with latest state from sensor."""
@@ -2278,19 +2182,13 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
) )
# Issue 99 - a climate is regulated by the device itself and not by VTherm. So a VTherm should never be in security ! # Issue 99 - a climate is regulated by the device itself and not by VTherm. So a VTherm should never be in security !
shouldClimateBeInSecurity = False # temp_cond and climate_cond shouldClimateBeInSecurity = False # temp_cond and climate_cond
shouldSwitchBeInSecurity = temp_cond and switch_cond shouldSwitchBeInSecurity = temp_cond and switch_cond
shouldBeInSecurity = shouldClimateBeInSecurity or shouldSwitchBeInSecurity shouldBeInSecurity = shouldClimateBeInSecurity or shouldSwitchBeInSecurity
shouldStartSecurity = ( shouldStartSecurity = mode_cond and not self._security_state and shouldBeInSecurity
mode_cond and not self._security_state and shouldBeInSecurity
)
# attr_preset_mode is not necessary normaly. It is just here to be sure # attr_preset_mode is not necessary normaly. It is just here to be sure
shouldStopSecurity = ( shouldStopSecurity = self._security_state and not shouldBeInSecurity and self._attr_preset_mode == PRESET_SECURITY
self._security_state
and not shouldBeInSecurity
and self._attr_preset_mode == PRESET_SECURITY
)
# Logging and event # Logging and event
if shouldStartSecurity: if shouldStartSecurity:
@@ -2452,10 +2350,7 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
_LOGGER.debug("%s - recalculate all", self) _LOGGER.debug("%s - recalculate all", self)
if not self._is_over_climate: if not self._is_over_climate:
self._prop_algorithm.calculate( self._prop_algorithm.calculate(
self._target_temp, self._target_temp, self._cur_temp, self._cur_ext_temp
self._cur_temp,
self._cur_ext_temp,
self._hvac_mode == HVACMode.COOL,
) )
self.update_custom_attributes() self.update_custom_attributes()
self.async_write_ha_state() self.async_write_ha_state()
@@ -2542,18 +2437,18 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
"max_power_sensor_entity_id": self._max_power_sensor_entity_id, "max_power_sensor_entity_id": self._max_power_sensor_entity_id,
} }
if self._is_over_climate: if self._is_over_climate:
self._attr_extra_state_attributes[ self._attr_extra_state_attributes["underlying_climate_0"] = self._underlyings[
"underlying_climate_0" 0
] = self._underlyings[0].entity_id ].entity_id
self._attr_extra_state_attributes["underlying_climate_1"] = ( self._attr_extra_state_attributes["underlying_climate_1"] = self._underlyings[
self._underlyings[1].entity_id if len(self._underlyings) > 1 else None 1
) ].entity_id if len(self._underlyings) > 1 else None
self._attr_extra_state_attributes["underlying_climate_2"] = ( self._attr_extra_state_attributes["underlying_climate_2"] = self._underlyings[
self._underlyings[2].entity_id if len(self._underlyings) > 2 else None 2
) ].entity_id if len(self._underlyings) > 2 else None
self._attr_extra_state_attributes["underlying_climate_3"] = ( self._attr_extra_state_attributes["underlying_climate_3"] = self._underlyings[
self._underlyings[3].entity_id if len(self._underlyings) > 3 else None 3
) ].entity_id if len(self._underlyings) > 3 else None
self._attr_extra_state_attributes[ self._attr_extra_state_attributes[
"start_hvac_action_date" "start_hvac_action_date"
@@ -2645,9 +2540,7 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
# If the changed preset is active, change the current temperature # If the changed preset is active, change the current temperature
# Issue #119 - reload new preset temperature also in ac mode # Issue #119 - reload new preset temperature also in ac mode
if preset.startswith(self._attr_preset_mode): if preset.startswith(self._attr_preset_mode):
await self._async_set_preset_mode_internal( await self._async_set_preset_mode_internal(preset.rstrip(PRESET_AC_SUFFIX), force=True)
preset.rstrip(PRESET_AC_SUFFIX), force=True
)
await self._async_control_heating(force=True) await self._async_control_heating(force=True)
async def service_set_security(self, delay_min, min_on_percent, default_on_percent): async def service_set_security(self, delay_min, min_on_percent, default_on_percent):

View File

@@ -227,7 +227,6 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
PROPORTIONAL_FUNCTION_TPI, PROPORTIONAL_FUNCTION_TPI,
] ]
), ),
vol.Optional(CONF_AC_MODE, default=False): cv.boolean,
} }
) )
@@ -245,6 +244,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,
} }
) )

View File

@@ -45,33 +45,19 @@ class PropAlgorithm:
self._default_on_percent = 0 self._default_on_percent = 0
def calculate( def calculate(
self, self, target_temp: float, current_temp: float, ext_current_temp: float
target_temp: float,
current_temp: float,
ext_current_temp: float,
cooling=False,
): ):
"""Do the calculation of the duration""" """Do the calculation of the duration"""
if target_temp is None or current_temp is None: if target_temp is None or current_temp is None:
_LOGGER.warning( _LOGGER.warning(
"Proportional algorithm: calculation is not possible cause target_temp or current_temp is null. Heating/cooling will be disabled" # pylint: disable=line-too-long "Proportional algorithm: calculation is not possible cause target_temp or current_temp is null. Heating will be disabled" # pylint: disable=line-too-long
) )
self._calculated_on_percent = 0 self._calculated_on_percent = 0
else: else:
if cooling: delta_temp = target_temp - current_temp
delta_temp = current_temp - target_temp delta_ext_temp = (
delta_ext_temp = ( target_temp - ext_current_temp if ext_current_temp is not None else 0
ext_current_temp )
if ext_current_temp is not None
else 0 - target_temp
)
else:
delta_temp = target_temp - current_temp
delta_ext_temp = (
target_temp - ext_current_temp
if ext_current_temp is not None
else 0
)
if self._function == PROPORTIONAL_FUNCTION_TPI: if self._function == PROPORTIONAL_FUNCTION_TPI:
self._calculated_on_percent = ( self._calculated_on_percent = (

View File

@@ -2,5 +2,4 @@
-r requirements_dev.txt -r requirements_dev.txt
aiodiscover aiodiscover
ulid_transform ulid_transform
pytest-asyncio
pytest-homeassistant-custom-component pytest-homeassistant-custom-component

View File

@@ -20,9 +20,9 @@ from homeassistant.components.climate import (
from pytest_homeassistant_custom_component.common import MockConfigEntry from pytest_homeassistant_custom_component.common import MockConfigEntry
from custom_components.versatile_thermostat.climate import VersatileThermostat from ..climate import VersatileThermostat
from custom_components.versatile_thermostat.const import * # pylint: disable=wildcard-import, unused-wildcard-import from ..const import * # pylint: disable=wildcard-import, unused-wildcard-import
from custom_components.versatile_thermostat.underlyings import * # pylint: disable=wildcard-import, unused-wildcard-import from ..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,

View File

@@ -1,6 +1,4 @@
"""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.
@@ -36,7 +34,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): # pylint: disable=unused-argument def auto_enable_custom_integrations(enable_custom_integrations):
"""Enable all integration in tests""" """Enable all integration in tests"""
yield yield

View File

@@ -97,7 +97,6 @@ MOCK_TH_OVER_CLIMATE_USER_CONFIG = {
MOCK_TH_OVER_SWITCH_TYPE_CONFIG = { MOCK_TH_OVER_SWITCH_TYPE_CONFIG = {
CONF_HEATER: "switch.mock_switch", CONF_HEATER: "switch.mock_switch",
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI, CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
CONF_AC_MODE: False,
} }
MOCK_TH_OVER_4SWITCH_TYPE_CONFIG = { MOCK_TH_OVER_4SWITCH_TYPE_CONFIG = {
@@ -106,7 +105,6 @@ MOCK_TH_OVER_4SWITCH_TYPE_CONFIG = {
CONF_HEATER_3: "switch.mock_4switch2", CONF_HEATER_3: "switch.mock_4switch2",
CONF_HEATER_4: "switch.mock_4switch3", CONF_HEATER_4: "switch.mock_4switch3",
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI, CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
CONF_AC_MODE: False,
} }
MOCK_TH_OVER_SWITCH_TPI_CONFIG = { MOCK_TH_OVER_SWITCH_TPI_CONFIG = {
@@ -116,6 +114,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 = {

View File

@@ -9,8 +9,9 @@ from homeassistant.components.binary_sensor import BinarySensorDeviceClass
from pytest_homeassistant_custom_component.common import MockConfigEntry 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
from custom_components.versatile_thermostat.binary_sensor import ( from ..climate import VersatileThermostat
from ..binary_sensor import (
SecurityBinarySensor, SecurityBinarySensor,
OverpoweringBinarySensor, OverpoweringBinarySensor,
WindowBinarySensor, WindowBinarySensor,
@@ -28,7 +29,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(

View File

@@ -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): # pylint: disable=unused-argument async def test_user_config_flow_over_switch(hass: HomeAssistant, skip_hass_states_get):
"""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): # pylint: disable=unused-argument async def test_user_config_flow_over_climate(hass: HomeAssistant, skip_hass_states_get):
"""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 # pylint: disable=unused-argument hass: HomeAssistant, skip_hass_states_get, skip_control_heating
): ):
"""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 # pylint: disable=unused-argument hass: HomeAssistant, skip_hass_states_get
): ):
"""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 # pylint: disable=unused-argument hass: HomeAssistant, skip_hass_states_get, skip_control_heating
): ):
"""Test the config flow with 4 switchs thermostat_over_switch features""" """Test the config flow with 4 switchs thermostat_over_switch features"""
@@ -378,7 +378,6 @@ async def test_user_config_flow_over_4_switches(
CONF_HEATER_3: "switch.mock_switch3", CONF_HEATER_3: "switch.mock_switch3",
CONF_HEATER_4: "switch.mock_switch4", CONF_HEATER_4: "switch.mock_switch4",
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI, CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
CONF_AC_MODE: False,
} }
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(

View File

@@ -470,131 +470,3 @@ async def test_movement_management_time_enoughand_not_presence(
assert entity.proportional_algorithm.on_percent == 0.11 assert entity.proportional_algorithm.on_percent == 0.11
assert mock_heater_off.call_count == 0 assert mock_heater_off.call_count == 0
assert mock_send_event.call_count == 0 assert mock_send_event.call_count == 0
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_movement_management_with_stop_during_condition(
hass: HomeAssistant, skip_hass_states_is_state
):
"""Test the Presence management when the movement sensor switch to off and then to on during the test condition"""
entry = MockConfigEntry(
domain=DOMAIN,
title="TheOverSwitchMockName",
unique_id="uniqueId",
data={
CONF_NAME: "TheOverSwitchMockName",
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_SWITCH,
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
CONF_CYCLE_MIN: 5,
CONF_TEMP_MIN: 15,
CONF_TEMP_MAX: 30,
"eco_temp": 17,
"comfort_temp": 18,
"boost_temp": 19,
"eco_away_temp": 17,
"comfort_away_temp": 18,
"boost_away_temp": 19,
CONF_USE_WINDOW_FEATURE: False,
CONF_USE_MOTION_FEATURE: True,
CONF_USE_POWER_FEATURE: False,
CONF_USE_PRESENCE_FEATURE: True,
CONF_HEATER: "switch.mock_switch",
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
CONF_TPI_COEF_INT: 0.3,
CONF_TPI_COEF_EXT: 0.01,
CONF_MINIMAL_ACTIVATION_DELAY: 30,
CONF_SECURITY_DELAY_MIN: 5,
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
CONF_MOTION_SENSOR: "binary_sensor.mock_motion_sensor",
CONF_MOTION_DELAY: 10,
CONF_MOTION_OFF_DELAY: 30,
CONF_MOTION_PRESET: "boost",
CONF_NO_MOTION_PRESET: "comfort",
CONF_PRESENCE_SENSOR: "binary_sensor.mock_presence_sensor",
},
)
entity: VersatileThermostat = await create_thermostat(
hass, entry, "climate.theoverswitchmockname"
)
assert entity
tz = get_tz(hass) # pylint: disable=invalid-name
now: datetime = datetime.now(tz=tz)
# start heating, in boost mode. We block the control_heating to avoid running a cycle
with patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat._async_control_heating"
):
await entity.async_set_hvac_mode(HVACMode.HEAT)
await entity.async_set_preset_mode(PRESET_ACTIVITY)
assert entity.hvac_mode is HVACMode.HEAT
assert entity.preset_mode is PRESET_ACTIVITY
# because no motion is detected yet
assert entity.target_temperature == 18
assert entity.motion_state is None
assert entity.presence_state is None
event_timestamp = now - timedelta(minutes=6)
await send_temperature_change_event(entity, 18, event_timestamp)
await send_ext_temperature_change_event(entity, 10, event_timestamp)
await send_presence_change_event(entity, False, True, event_timestamp)
assert entity.presence_state is "off"
# starts detecting motion
with patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
) as mock_send_event, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
return_value=True,
), patch("homeassistant.helpers.condition.state", return_value=True): # Not needed for this test
event_timestamp = now - timedelta(minutes=5)
try_condition1 = await send_motion_change_event(entity, True, False, event_timestamp)
assert try_condition1 is not None
assert entity.hvac_mode is HVACMode.HEAT
assert entity.preset_mode is PRESET_ACTIVITY
# because motion is detected yet -> switch to Boost mode
assert entity.target_temperature == 18
assert entity.motion_state is None
assert entity.presence_state is "off"
# Send a stop detection
event_timestamp = now - timedelta(minutes=4)
try_condition = await send_motion_change_event(entity, False, True, event_timestamp)
assert try_condition is None # The timer should not have been stopped
assert entity.hvac_mode is HVACMode.HEAT
assert entity.preset_mode is PRESET_ACTIVITY
assert entity.target_temperature == 18
assert entity.motion_state is None
assert entity.presence_state is "off"
# Resend a start detection
event_timestamp = now - timedelta(minutes=3)
try_condition = await send_motion_change_event(entity, True, False, event_timestamp)
assert try_condition is None # The timer should not have been restarted (we keep the first one)
assert entity.hvac_mode is HVACMode.HEAT
assert entity.preset_mode is PRESET_ACTIVITY
# still no motion detected
assert entity.target_temperature == 18
assert entity.motion_state is None
assert entity.presence_state is "off"
await try_condition1(None)
# We should have switch this time
assert entity.target_temperature == 19 # Boost
assert entity.motion_state is "on" # switch to movement on
assert entity.presence_state is "off" # Non change

View File

@@ -1,10 +1,10 @@
""" 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 datetime import datetime, timedelta
import logging
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)
@@ -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 # pylint: disable=protected-access assert entity._is_device_active is False
# 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,8 +132,7 @@ 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 # 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
# 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
@@ -154,15 +153,12 @@ 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 # The heater is already on cycle. So we wait that the cycle ends and no heater action is done
# 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( # pylint: disable=protected-access await entity.underlying_entity(0)._turn_on_later(None)
None
)
# wait restart # wait restart
await asyncio.sleep(0.1) await asyncio.sleep(0.1)
@@ -181,9 +177,7 @@ 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( # pylint: disable=protected-access await entity.underlying_entity(0)._turn_off_later(None)
None
)
# No special event # No special event
assert mock_send_event.call_count == 0 assert mock_send_event.call_count == 0
@@ -204,9 +198,7 @@ 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( # pylint: disable=protected-access await entity.underlying_entity(0)._turn_on_later(None)
None
)
# No special event # No special event
assert mock_send_event.call_count == 0 assert mock_send_event.call_count == 0
@@ -222,7 +214,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
@@ -285,7 +277,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 # pylint: disable=protected-access assert entity._is_device_active is False
# 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
@@ -350,20 +342,17 @@ 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 # The first heater should be turned on but is already on but because call_later is mocked, it is only turned on here
# 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 """Test that when multiple climates are configured the activation and deactivation is propagated to all climates"""
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)
@@ -427,7 +416,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 # pylint: disable=protected-access assert entity._is_device_active is False
# 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(
@@ -452,8 +441,7 @@ async def test_multiple_climates(
call.set_hvac_mode(HVACMode.OFF), call.set_hvac_mode(HVACMode.OFF),
] ]
) )
assert entity._is_device_active is False # pylint: disable=protected-access assert entity._is_device_active is False
@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])
@@ -461,9 +449,8 @@ 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 """Test that when multiple switch are configured the activation of one underlying climate activate the others"""
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)
@@ -527,7 +514,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 # pylint: disable=protected-access assert entity._is_device_active is False
# Stop heating on one underlying climate # Stop heating on one underlying climate
with patch( with patch(
@@ -537,14 +524,7 @@ 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( await send_climate_change_event(entity, HVACMode.OFF, HVACMode.HEAT, HVACAction.OFF, HVACAction.HEATING, event_timestamp)
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
@@ -554,7 +534,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 # pylint: disable=protected-access assert entity._is_device_active is False
# Start heating on one underlying climate # Start heating on one underlying climate
with patch( with patch(
@@ -562,21 +542,12 @@ 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 # notice that there is no need of return_value=HVACAction.IDLE because this is not a function but a property
# a function but a property "custom_components.versatile_thermostat.underlyings.UnderlyingClimate.hvac_action", HVACAction.IDLE
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.hvac_action", ) as mock_underlying_get_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( await send_climate_change_event(entity, HVACMode.HEAT, HVACMode.OFF, HVACAction.IDLE, HVACAction.OFF, event_timestamp)
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
@@ -587,4 +558,5 @@ 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 # pylint: disable=protected-access assert entity._is_device_active is False

View File

@@ -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 custom_components.versatile_thermostat.open_window_algorithm import WindowOpenDetectionAlgorithm from ..open_window_algorithm import WindowOpenDetectionAlgorithm
async def test_open_window_algo( async def test_open_window_algo(

View File

@@ -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 custom_components.versatile_thermostat.climate import VersatileThermostat from ..climate import VersatileThermostat
from custom_components.versatile_thermostat.sensor import ( from ..sensor import (
EnergySensor, EnergySensor,
MeanPowerSensor, MeanPowerSensor,
OnPercentSensor, OnPercentSensor,

View File

@@ -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 custom_components.versatile_thermostat.climate import VersatileThermostat from ..climate import VersatileThermostat
from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import

View File

@@ -5,9 +5,7 @@ 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( async def test_tpi_calculation(hass: HomeAssistant, skip_hass_states_is_state):
hass: HomeAssistant, skip_hass_states_is_state: None
): # pylint: disable=unused-argument
"""Test the TPI calculation""" """Test the TPI calculation"""
entry = MockConfigEntry( entry = MockConfigEntry(
@@ -42,7 +40,7 @@ async def test_tpi_calculation(
) )
assert entity assert entity
tpi_algo = entity._prop_algorithm # pylint: disable=protected-access tpi_algo = entity._prop_algorithm
assert tpi_algo assert tpi_algo
tpi_algo.calculate(15, 10, 7) tpi_algo.calculate(15, 10, 7)
@@ -52,52 +50,36 @@ async def test_tpi_calculation(
assert tpi_algo.off_time_sec == 0 assert tpi_algo.off_time_sec == 0
assert entity.mean_cycle_power is None # no device power configured assert entity.mean_cycle_power is None # no device power configured
tpi_algo.calculate(15, 14, 5, False) tpi_algo.calculate(15, 14, 5)
assert tpi_algo.on_percent == 0.4 assert tpi_algo.on_percent == 0.4
assert tpi_algo.calculated_on_percent == 0.4 assert tpi_algo.calculated_on_percent == 0.4
assert tpi_algo.on_time_sec == 120 assert tpi_algo.on_time_sec == 120
assert tpi_algo.off_time_sec == 180 assert tpi_algo.off_time_sec == 180
tpi_algo.set_security(0.1) tpi_algo.set_security(0.1)
tpi_algo.calculate(15, 14, 5, False) tpi_algo.calculate(15, 14, 5)
assert tpi_algo.on_percent == 0.1 assert tpi_algo.on_percent == 0.1
assert tpi_algo.calculated_on_percent == 0.4 assert tpi_algo.calculated_on_percent == 0.4
assert tpi_algo.on_time_sec == 30 # >= minimal_activation_delay (=30) assert tpi_algo.on_time_sec == 30 # >= minimal_activation_delay (=30)
assert tpi_algo.off_time_sec == 270 assert tpi_algo.off_time_sec == 270
tpi_algo.unset_security() tpi_algo.unset_security()
tpi_algo.calculate(15, 14, 5, False) tpi_algo.calculate(15, 14, 5)
assert tpi_algo.on_percent == 0.4 assert tpi_algo.on_percent == 0.4
assert tpi_algo.calculated_on_percent == 0.4 assert tpi_algo.calculated_on_percent == 0.4
assert tpi_algo.on_time_sec == 120 assert tpi_algo.on_time_sec == 120
assert tpi_algo.off_time_sec == 180 assert tpi_algo.off_time_sec == 180
# Test minimal activation delay # Test minimal activation delay
tpi_algo.calculate(15, 14.7, 15, False) tpi_algo.calculate(15, 14.7, 15)
assert tpi_algo.on_percent == 0.09 assert tpi_algo.on_percent == 0.09
assert tpi_algo.calculated_on_percent == 0.09 assert tpi_algo.calculated_on_percent == 0.09
assert tpi_algo.on_time_sec == 0 assert tpi_algo.on_time_sec == 0
assert tpi_algo.off_time_sec == 300 assert tpi_algo.off_time_sec == 300
tpi_algo.set_security(0.09) tpi_algo.set_security(0.09)
tpi_algo.calculate(15, 14.7, 15, False) tpi_algo.calculate(15, 14.7, 15)
assert tpi_algo.on_percent == 0.09 assert tpi_algo.on_percent == 0.09
assert tpi_algo.calculated_on_percent == 0.09 assert tpi_algo.calculated_on_percent == 0.09
assert tpi_algo.on_time_sec == 0 assert tpi_algo.on_time_sec == 0
assert tpi_algo.off_time_sec == 300 assert tpi_algo.off_time_sec == 300
tpi_algo.unset_security()
tpi_algo.calculate(25, 30, 35, True)
assert tpi_algo.on_percent == 1
assert tpi_algo.calculated_on_percent == 1
assert tpi_algo.on_time_sec == 300
assert tpi_algo.off_time_sec == 0
assert entity.mean_cycle_power is None # no device power configured
tpi_algo.set_security(0.09)
tpi_algo.calculate(25, 30, 35, True)
assert tpi_algo.on_percent == 0.09
assert tpi_algo.calculated_on_percent == 1
assert tpi_algo.on_time_sec == 0
assert tpi_algo.off_time_sec == 300
assert entity.mean_cycle_power is None # no device power configured

View File

@@ -246,7 +246,7 @@ class UnderlyingSwitch(UnderlyingEntity):
return return
# If we should heat, starts the cycle with delay # If we should heat, starts the cycle with delay
if self._hvac_mode in [HVACMode.HEAT, HVACMode.COOL] and on_time_sec > 0: if self._hvac_mode == HVACMode.HEAT and on_time_sec > 0:
# Starts the cycle after the initial delay # Starts the cycle after the initial delay
self._async_cancel_cycle = self.call_later( self._async_cancel_cycle = self.call_later(
self._hass, self._initial_delay_sec, self._turn_on_later self._hass, self._initial_delay_sec, self._turn_on_later

View File

@@ -1,28 +0,0 @@
#!/bin/bash
set -e
set -x
cd "$(dirname "$0")/.."
pwd
# Create config dir if not present
if [[ ! -d "${PWD}/config" ]]; then
mkdir -p "${PWD}/config"
# Add defaults configuration
hass --config "${PWD}/config" --script ensure_config
# Overwrite configuration.yaml if provided
if [ -f ${PWD}/.devcontainer/configuration.yaml ]; then
rm -f ${PWD}/config/configuration.yaml
ln -s ${PWD}/.devcontainer/configuration.yaml ${PWD}/config/configuration.yaml
fi
fi
# Set the path to custom_components
## This let's us have the structure we want <root>/custom_components/integration_blueprint
## while at the same time have Home Assistant configuration inside <root>/config
## without resulting to symlinks.
export PYTHONPATH="${PYTHONPATH}:${PWD}/custom_components"
# Start Home Assistant
hass --config "${PWD}/config" --debug

View File

@@ -1,3 +0,0 @@
[tool:pytest]
testpaths = tests
asyncio_mode = auto