Compare commits

...

12 Commits
3.2.1 ... 3.2.6

Author SHA1 Message Date
Jean-Marc Collin
61eae8c066 FIX #89 _is_aux_heat error 2023-06-25 12:27:00 +02:00
Jean-Marc Collin
e16daa3d53 Add hacs.yaml github workflow 2023-05-14 11:11:55 +02:00
Jean-Marc Collin
90a6c926e3 Resolve actions warning 2023-05-14 11:02:29 +02:00
Jean-Marc Collin
64ce3aa0ad Issue #81 - recursive loop when security should be set 2023-04-28 12:12:38 +02:00
Jean-Marc Collin
3f498ffbd3 Add BuyMeCoffee contributors (fr) 2023-04-26 08:23:03 +02:00
Jean-Marc Collin
3236be6c3b Update README.md
Add BuyMeCoffee contributors
2023-04-26 08:21:54 +02:00
Jean-Marc Collin
be86fd3ac0 Merge branch 'main' of github.com:jmcollin78/versatile_thermostat into main 2023-04-22 11:06:12 +02:00
Jean-Marc Collin
e35ba57bd7 Fix compilation warnings 2023-04-22 11:05:38 +02:00
Bergoglio
72d7803ffa Add files via upload (#79)
Thanks @Bergoglio !
2023-04-22 10:29:48 +02:00
Jean-Marc Collin
4dd7c62a42 Migration to 2023.4 and fix lingering tasks and timers tests errors 2023-04-22 09:24:57 +02:00
Jean-Marc Collin
429ff47269 Typo in README 2023-04-14 08:34:43 +02:00
Jean-Marc Collin
a17423d470 FIX #73, #72. thanks to Salabur 2023-04-14 08:24:01 +02:00
24 changed files with 497 additions and 37 deletions

View File

@@ -10,7 +10,7 @@ jobs:
runs-on: "ubuntu-latest"
name: Validate
steps:
- uses: "actions/checkout@v2"
- uses: "actions/checkout@v3.5.2"
- name: HACS validation
uses: "hacs/action@main"

17
.github/workflows/hacs.yml vendored Normal file
View File

@@ -0,0 +1,17 @@
name: HACS Action
on:
push:
pull_request:
schedule:
- cron: "0 0 * * *"
jobs:
hacs:
name: HACS Action
runs-on: "ubuntu-latest"
steps:
- name: HACS Action
uses: "hacs/action@main"
with:
category: "integration"

View File

@@ -8,7 +8,7 @@ jobs:
runs-on: "ubuntu-latest"
name: Validate
steps:
- uses: "actions/checkout@v2"
- uses: "actions/checkout@v3.5.2"
- name: HACS validation
uses: "hacs/action@main"
@@ -23,8 +23,8 @@ jobs:
runs-on: "ubuntu-latest"
name: Check style formatting
steps:
- uses: "actions/checkout@v2"
- uses: "actions/setup-python@v1"
- uses: "actions/checkout@v3.5.2"
- uses: "actions/setup-python@v4.6.0"
with:
python-version: "3.x"
- run: python3 -m pip install black
@@ -35,9 +35,9 @@ jobs:
name: Run tests
steps:
- name: Check out code from GitHub
uses: "actions/checkout@v2"
uses: "actions/checkout@v3.5.2"
- name: Setup Python
uses: "actions/setup-python@v1"
uses: "actions/setup-python@v4.6.0"
with:
python-version: "3.8"
- name: Install requirements

View File

@@ -11,7 +11,7 @@ jobs:
runs-on: "ubuntu-latest"
name: Validate
steps:
- uses: "actions/checkout@v2"
- uses: "actions/checkout@v3.5.2"
- name: HACS validation
uses: "hacs/action@main"
@@ -26,8 +26,8 @@ jobs:
runs-on: "ubuntu-latest"
name: Check style formatting
steps:
- uses: "actions/checkout@v2"
- uses: "actions/setup-python@v1"
- uses: "actions/checkout@v3.5.2"
- uses: "actions/setup-python@v4.6.0"
with:
python-version: "3.x"
- run: python3 -m pip install black

View File

@@ -6,8 +6,9 @@
![Tip](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/icon.png?raw=true)
> ![Tip](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/tips.png?raw=true?raw=true) Cette intégration de thermostat vise à simplifier considérablement vos automatisations autour de la gestion du chauffage. Parce que tous les événements autour du chauffage classiques sont gérés nativement par le thermostat (personne à la maison ?, activité détectée dans une pièce ?, fenêtre ouverte ?, délestage de courant ?), vous n'avez pas à vous encombrer de scripts et d'automatismes compliqués pour gérer vos climats. ;-).
> ![Tip](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/tips.png?raw=true) Cette intégration de thermostat vise à simplifier considérablement vos automatisations autour de la gestion du chauffage. Parce que tous les événements autour du chauffage classiques sont gérés nativement par le thermostat (personne à la maison ?, activité détectée dans une pièce ?, fenêtre ouverte ?, délestage de courant ?), vous n'avez pas à vous encombrer de scripts et d'automatismes compliqués pour gérer vos climats. ;-).
- [Merci pour la bière buymecoffee: https://www.buymeacoffee.com/jmcollin78](#merci-pour-la-bière-buymecoffee-httpswwwbuymeacoffeecomjmcollin78)
- [Quand l'utiliser et ne pas l'utiliser](#quand-lutiliser-et-ne-pas-lutiliser)
- [Pourquoi une nouvelle implémentation du thermostat ?](#pourquoi-une-nouvelle-implémentation-du-thermostat-)
- [Comment installer cet incroyable Thermostat Versatile ?](#comment-installer-cet-incroyable-thermostat-versatile-)
@@ -60,6 +61,10 @@ Ce composant personnalisé pour Home Assistant est une mise à niveau et est une
> * **release 2.2** : ajout de fonction de sécurité permettant de ne pas laisser éternellement en chauffe un radiateur en cas de panne du thermomètre
> * **release majeure 2.0** : ajout du thermostat "over climate" permettant de transformer n'importe quel thermostat en Versatile Thermostat et lui ajouter toutes les fonctions de ce dernier.
# Merci pour la bière [buymecoffee](https://www.buymeacoffee.com/jmcollin78)
Un grand merci à @salabur, @pvince83 and @bergoglio pour les bières. Ca fait très plaisir.
# Quand l'utiliser et ne pas l'utiliser
Ce thermostat peut piloter 2 types d'équipement:
1. un radiateur qui ne fonctionne qu'en mode marche/arrêt (nommé ```thermostat_over_switch```). La configuration minimale nécessaire pour utiliser ce type thermostat est :

View File

@@ -6,8 +6,9 @@
![Tip](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/icon.png?raw=true)
> ![Tip](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/tips.png?raw=true?raw=true) This thermostat integration aims to drastically simplify your automations around climate management. Because all classical events in climate are natively handled by the thermostat (nobody at home ?, activity detected in a room ?, window open ?, power shedding ?), you don't have to build over complicated scripts and automations to manage your climates ;-).
> ![Tip](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/tips.png?raw=true) This thermostat integration aims to drastically simplify your automations around climate management. Because all classical events in climate are natively handled by the thermostat (nobody at home ?, activity detected in a room ?, window open ?, power shedding ?), you don't have to build over complicated scripts and automations to manage your climates ;-).
- [Thanks for the beer buymecoffee](#thanks-for-the-beer-buymecoffee)
- [When to use / not use](#when-to-use--not-use)
- [Why another thermostat implementation ?](#why-another-thermostat-implementation-)
- [How to install this incredible Versatile Thermostat ?](#how-to-install-this-incredible-versatile-thermostat-)
@@ -59,6 +60,10 @@ This custom component for Home Assistant is an upgrade and is a complete rewrite
> * **release 2.2**: addition of a safety function allowing a radiator not to be left heating forever in the event of a thermometer failure
> * **major release 2.0**: addition of the "over climate" thermostat allowing you to transform any thermostat into a Versatile Thermostat and add all the functions of the latter.
# Thanks for the beer [buymecoffee](https://www.buymeacoffee.com/jmcollin78)
Many thanks to @salabur, @pvince83 and @bergoglio for the beers. It's very pleasing.
# When to use / not use
This thermostat can control 2 types of equipment:
1. a heater that only works in on/off mode (named ```thermostat_over_switch```). The minimum configuration required to use this type of thermostat is:

View File

@@ -27,6 +27,7 @@ fi
if [ "$command" == "hassfest" ]; then
echo "Running container start"
python3 -m script.hassfest
# python -m script.hassfest --requirements --action validate --integration-path config/custom_components/versatile_thermostat/
fi
if [ "$command" == "restart" ]; then

View File

@@ -601,7 +601,7 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
)
)
self.async_on_remove(self.async_remove_thermostat)
self.async_on_remove(self.remove_thermostat)
try:
await self.async_startup()
@@ -609,11 +609,11 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
# Ingore this error which is possible if underlying climate is not found temporary
pass
def async_remove_thermostat(self):
async def remove_thermostat(self):
"""Called when the thermostat will be removed"""
_LOGGER.info("%s - Removing thermostat", self)
for under in self._underlyings:
under.remove_entity()
await under.remove_entity()
async def async_startup(self):
"""Triggered on startup, used to get old state and set internal states accordingly"""
@@ -1191,7 +1191,7 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
raise NotImplementedError()
async def async_set_hvac_mode(self, hvac_mode):
async def async_set_hvac_mode(self, hvac_mode, need_control_heating=True):
"""Set new target hvac mode."""
_LOGGER.info("%s - Set hvac mode: %s", self, hvac_mode)
@@ -1201,13 +1201,13 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
self._hvac_mode = hvac_mode
# Delegate to all underlying
need_control_heating = False
sub_need_control_heating = False
for under in self._underlyings:
need_control_heating = (
sub_need_control_heating = (
await under.set_hvac_mode(hvac_mode) or need_control_heating
)
if need_control_heating:
if need_control_heating and sub_need_control_heating:
await self._async_control_heating(force=True)
# Ensure we update the current operation after changing the mode
@@ -1452,7 +1452,7 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
self,
self._saved_hvac_mode,
)
await self.restore_hvac_mode()
await self.restore_hvac_mode(True)
elif self._window_state == STATE_ON:
_LOGGER.info(
"%s - Window is open. Set hvac_mode to '%s'", self, HVACMode.OFF
@@ -1521,7 +1521,9 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
)
# 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))
await self._async_internal_set_temperature(
self.find_preset_temp(new_preset)
)
self.recalculate()
await self._async_control_heating(force=True)
@@ -1576,7 +1578,7 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
)
_LOGGER.info(
"%s - Underlying climate changed. Event.new_state is %s, hvac_mode=%s, hvac_action=%s, old_hvac_action=%s",
"%s - Underlying climate changed. Event.new_state is %s, current_hvac_mode=%s, new_hvac_action=%s, old_hvac_action=%s",
self,
new_state,
self._hvac_mode,
@@ -1855,7 +1857,7 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
)
# Set attributes
self._window_auto_state = False
await self.restore_hvac_mode()
await self.restore_hvac_mode(True)
if self._window_call_cancel:
self._window_call_cancel()
@@ -1951,9 +1953,9 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
self._hvac_mode,
)
async def restore_hvac_mode(self):
async def restore_hvac_mode(self, need_control_heating=False):
"""Restore a previous hvac_mod"""
await self.async_set_hvac_mode(self._saved_hvac_mode)
await self.async_set_hvac_mode(self._saved_hvac_mode, need_control_heating)
_LOGGER.debug(
"%s - Restored hvac_mode - saved_hvac_mode is %s, hvac_mode is %s",
self,
@@ -2023,7 +2025,7 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
self._saved_preset_mode,
)
if self._is_over_climate:
await self.restore_hvac_mode()
await self.restore_hvac_mode(False)
await self.restore_preset_mode()
self.send_event(
EventType.POWER_EVENT,
@@ -2048,7 +2050,10 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
now - self._last_ext_temperature_mesure.replace(tzinfo=self._current_tz)
).total_seconds() / 60.0
mode_cond = self._is_over_climate or self._hvac_mode != HVACMode.OFF
# TODO before change:
# mode_cond = self._is_over_climate or self._hvac_mode != HVACMode.OFF
# fixed into this. Why if _is_over_climate we could into security even if HVACMode is OFF ?
mode_cond = self._hvac_mode != HVACMode.OFF
temp_cond: bool = (
delta_temp > self._security_delay_min
@@ -2125,7 +2130,7 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
await self._async_set_preset_mode_internal(PRESET_SECURITY)
# Turn off the underlying climate or heater if security default on_percent is 0
if self._is_over_climate or self._security_default_on_percent <= 0.0:
await self.async_set_hvac_mode(HVACMode.OFF)
await self.async_set_hvac_mode(HVACMode.OFF, False)
if self._prop_algorithm:
self._prop_algorithm.set_security(self._security_default_on_percent)
@@ -2159,7 +2164,7 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
self._security_state = ret
# Restore hvac_mode if previously saved
if self._is_over_climate or self._security_default_on_percent <= 0.0:
await self.restore_hvac_mode()
await self.restore_hvac_mode(False)
await self.restore_preset_mode()
if self._prop_algorithm:
self._prop_algorithm.unset_security()
@@ -2292,6 +2297,8 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
self.get_preset_away_name(PRESET_COMFORT)
),
"power_temp": self._power_temp,
"target_temp": self.target_temperature,
"current_temp": self._cur_temp,
"ext_current_temperature": self._cur_ext_temp,
"current_power": self._current_power,
"current_power_max": self._current_power_max,

View File

@@ -54,7 +54,7 @@ class WindowOpenDetectionAlgorithm:
delta_t_sec = float((datetime_measure - self._last_datetime).total_seconds())
delta_t = delta_t_sec / 60.0
if delta_t_sec <= MIN_DELTA_T_SEC:
_LOGGER.warning(
_LOGGER.debug(
"Delta t is %d < %d which should be not possible. We don't consider this value",
delta_t_sec,
MIN_DELTA_T_SEC,
@@ -64,7 +64,7 @@ class WindowOpenDetectionAlgorithm:
delta_temp = float(temperature - self._last_temperature)
new_slope = delta_temp / delta_t
if new_slope > MAX_SLOPE_VALUE or new_slope < -MAX_SLOPE_VALUE:
_LOGGER.warning(
_LOGGER.debug(
"New_slope is abs(%.2f) > %.2f which should be not possible. We don't consider this value",
new_slope,
MAX_SLOPE_VALUE,

View File

@@ -1 +1,2 @@
homeassistant
homeassistant
ffmpeg

View File

@@ -2,6 +2,7 @@
import asyncio
import logging
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

View File

@@ -9,6 +9,7 @@ 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 (
SecurityBinarySensor,
@@ -21,6 +22,8 @@ from ..binary_sensor import (
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_security_binary_sensors(
hass: HomeAssistant,
skip_hass_states_is_state,
@@ -96,6 +99,8 @@ async def test_security_binary_sensors(
assert security_binary_sensor.state == STATE_OFF
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_overpowering_binary_sensors(
hass: HomeAssistant,
skip_hass_states_is_state,
@@ -178,6 +183,8 @@ async def test_overpowering_binary_sensors(
assert overpowering_binary_sensor.state == STATE_OFF
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_window_binary_sensors(
hass: HomeAssistant,
skip_hass_states_is_state,
@@ -264,6 +271,8 @@ async def test_window_binary_sensors(
assert window_binary_sensor.state == STATE_OFF
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_motion_binary_sensors(
hass: HomeAssistant,
skip_hass_states_is_state,
@@ -350,6 +359,8 @@ async def test_motion_binary_sensors(
assert motion_binary_sensor.state == STATE_OFF
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_presence_binary_sensors(
hass: HomeAssistant,
skip_hass_states_is_state,
@@ -432,6 +443,8 @@ async def test_presence_binary_sensors(
assert presence_binary_sensor.state == STATE_OFF
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_binary_sensors_over_climate_minimal(
hass: HomeAssistant,
skip_hass_states_is_state,

View File

@@ -8,6 +8,8 @@ import logging
logging.getLogger().setLevel(logging.DEBUG)
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_bug_56(
hass: HomeAssistant,
skip_hass_states_is_state,
@@ -85,6 +87,8 @@ async def test_bug_56(
entity.update_custom_attributes()
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_bug_63(
hass: HomeAssistant,
skip_hass_states_is_state,
@@ -135,6 +139,8 @@ async def test_bug_63(
# Waiting for answer in https://github.com/jmcollin78/versatile_thermostat/issues/64
# Repro case not evident
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_bug_64(
hass: HomeAssistant,
skip_hass_states_is_state,
@@ -180,6 +186,8 @@ async def test_bug_64(
assert entity
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_bug_66(
hass: HomeAssistant,
skip_hass_states_is_state,

View File

@@ -6,9 +6,12 @@ from homeassistant.config_entries import SOURCE_USER, ConfigEntry
from custom_components.versatile_thermostat.const import DOMAIN
from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import
from .const 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_show_form(hass: HomeAssistant) -> None:
"""Test that the form is served with no input"""
# Init the API
@@ -24,6 +27,8 @@ async def test_show_form(hass: HomeAssistant) -> None:
assert result["step_id"] == SOURCE_USER
@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):
"""Test the config flow with all thermostat_over_switch features"""
result = await hass.config_entries.flow.async_init(
@@ -121,6 +126,8 @@ async def test_user_config_flow_over_switch(hass: HomeAssistant, skip_hass_state
assert isinstance(result["result"], ConfigEntry)
@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):
"""Test the config flow with all thermostat_over_climate features and no additional features"""
result = await hass.config_entries.flow.async_init(
@@ -206,6 +213,8 @@ async def test_user_config_flow_over_climate(hass: HomeAssistant, skip_hass_stat
assert isinstance(result["result"], ConfigEntry)
@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
):
@@ -301,6 +310,8 @@ async def test_user_config_flow_window_auto_ok(
assert isinstance(result["result"], ConfigEntry)
@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
):
@@ -371,6 +382,8 @@ 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
):

View File

@@ -9,6 +9,8 @@ import logging
logging.getLogger().setLevel(logging.DEBUG)
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_movement_management_time_not_enough(
hass: HomeAssistant, skip_hass_states_is_state
):
@@ -140,6 +142,8 @@ async def test_movement_management_time_not_enough(
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_time_enough_and_presence(
hass: HomeAssistant, skip_hass_states_is_state
):
@@ -270,6 +274,8 @@ async def test_movement_management_time_enough_and_presence(
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_time_enoughand_not_presence(
hass: HomeAssistant, skip_hass_states_is_state
):

View File

@@ -9,6 +9,8 @@ import logging
logging.getLogger().setLevel(logging.DEBUG)
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_one_switch_cycle(
hass: HomeAssistant,
skip_hass_states_is_state,
@@ -206,6 +208,8 @@ async def test_one_switch_cycle(
# assert entity.underlying_entity(0)._should_relaunch_control_heating is False
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_multiple_switchs(
hass: HomeAssistant,
skip_hass_states_is_state,

View File

@@ -10,6 +10,8 @@ import logging
logging.getLogger().setLevel(logging.DEBUG)
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_power_management_hvac_off(
hass: HomeAssistant, skip_hass_states_is_state
):
@@ -96,6 +98,8 @@ async def test_power_management_hvac_off(
assert mock_heater_off.call_count == 0
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_power_management_hvac_on(hass: HomeAssistant, skip_hass_states_is_state):
"""Test the Power management"""
@@ -226,6 +230,8 @@ async def test_power_management_hvac_on(hass: HomeAssistant, skip_hass_states_is
assert mock_heater_off.call_count == 0
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_power_management_energy_over_switch(
hass: HomeAssistant, skip_hass_states_is_state
):
@@ -350,6 +356,8 @@ async def test_power_management_energy_over_switch(
assert round(entity.total_energy, 2) == round((2.0 + 0.6) * 100 * 5 / 60.0, 2)
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_power_management_energy_over_climate(
hass: HomeAssistant, skip_hass_states_is_state
):

View File

@@ -9,6 +9,8 @@ import logging
logging.getLogger().setLevel(logging.DEBUG)
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_security_feature(hass: HomeAssistant, skip_hass_states_is_state):
"""Test the security feature and https://github.com/jmcollin78/versatile_thermostat/issues/49:
1. creates a thermostat and check that security is off

View File

@@ -26,6 +26,8 @@ from ..sensor import (
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_sensors_over_switch(
hass: HomeAssistant,
skip_hass_states_is_state,
@@ -182,6 +184,8 @@ async def test_sensors_over_switch(
cancel_switchs_cycles(entity)
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_sensors_over_climate(
hass: HomeAssistant,
skip_hass_states_is_state,
@@ -316,6 +320,8 @@ async def test_sensors_over_climate(
assert last_ext_temperature_sensor.device_class == SensorDeviceClass.TIMESTAMP
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_sensors_over_climate_minimal(
hass: HomeAssistant,
skip_hass_states_is_state,

View File

@@ -15,6 +15,8 @@ from ..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_full_start(hass: HomeAssistant, skip_hass_states_is_state):
"""Test the normal full start of a thermostat in thermostat_over_switch type"""
@@ -76,6 +78,8 @@ async def test_over_switch_full_start(hass: HomeAssistant, skip_hass_states_is_s
)
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_over_climate_full_start(hass: HomeAssistant, skip_hass_states_is_state):
"""Test the normal full start of a thermostat in thermostat_over_climate type"""
@@ -143,6 +147,8 @@ async def test_over_climate_full_start(hass: HomeAssistant, skip_hass_states_is_
mock_find_climate.assert_has_calls([call.find_underlying_entity()])
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_over_4switch_full_start(hass: HomeAssistant, skip_hass_states_is_state):
"""Test the normal full start of a thermostat in thermostat_over_switch with 4 switches type"""

View File

@@ -3,6 +3,8 @@
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_tpi_calculation(hass: HomeAssistant, skip_hass_states_is_state):
"""Test the TPI calculation"""

View File

@@ -9,6 +9,8 @@ import logging
logging.getLogger().setLevel(logging.DEBUG)
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_window_management_time_not_enough(
hass: HomeAssistant, skip_hass_states_is_state
):
@@ -92,7 +94,11 @@ async def test_window_management_time_not_enough(
await try_window_condition(None)
assert entity.window_state == STATE_OFF
await entity.remove_thermostat()
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_window_management_time_enough(
hass: HomeAssistant, skip_hass_states_is_state
):
@@ -227,7 +233,12 @@ async def test_window_management_time_enough(
)
assert entity.preset_mode is PRESET_BOOST
# Clean the entity
await entity.remove_thermostat()
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_window_auto_fast(hass: HomeAssistant, skip_hass_states_is_state):
"""Test the Power management"""
@@ -406,7 +417,12 @@ async def test_window_auto_fast(hass: HomeAssistant, skip_hass_states_is_state):
assert entity.window_auto_state == STATE_OFF
assert entity.hvac_mode is HVACMode.HEAT
# Clean the entity
await entity.remove_thermostat()
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_window_auto_auto_stop(hass: HomeAssistant, skip_hass_states_is_state):
"""Test the Power management"""
@@ -544,7 +560,12 @@ async def test_window_auto_auto_stop(hass: HomeAssistant, skip_hass_states_is_st
assert entity.hvac_mode is HVACMode.HEAT
assert entity.preset_mode is PRESET_BOOST
# Clean the entity
await entity.remove_thermostat()
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_window_auto_no_on_percent(
hass: HomeAssistant, skip_hass_states_is_state
):
@@ -647,3 +668,6 @@ async def test_window_auto_no_on_percent(
assert entity._window_auto_algo.is_window_close_detected() is False
assert entity.window_auto_state == STATE_OFF
assert entity.hvac_mode is HVACMode.HEAT
# Clean the entity
await entity.remove_thermostat()

View File

@@ -0,0 +1,288 @@
{
"title": "Configurazione Versatile Thermostat",
"config": {
"flow_title": "Configurazione Versatile Thermostat",
"step": {
"user": {
"title": "Aggiungi un nuovo Versatile Thermostat",
"description": "Principali parametri obbligatori",
"data": {
"name": "Nome",
"thermostat_type": "Tipologia di termostato",
"temperature_sensor_entity_id": "Entity id sensore temperatura",
"external_temperature_sensor_entity_id": "Entity id sensore temperatura esterna",
"cycle_min": "Durata del ciclo (minuti)",
"temp_min": "Temperatura minima ammessa",
"temp_max": "Temperatura massima ammessa",
"device_power": "Potenza dispositivo (kW)",
"use_window_feature": "Usa il rilevamento della finestra",
"use_motion_feature": "Usa il rilevamento del movimento",
"use_power_feature": "Usa la gestione della potenza",
"use_presence_feature": "Usa il rilevamento della presenza"
}
},
"type": {
"title": "Entità collegate",
"description": "Parametri entità collegate",
"data": {
"heater_entity_id": "Primo riscaldatore",
"heater_entity2_id": "Secondo riscaldatore",
"heater_entity3_id": "Terzo riscaldatore",
"heater_entity4_id": "Quarto riscaldatore",
"proportional_function": "Algoritmo",
"climate_entity_id": "Termostato sottostante"
},
"data_description": {
"heater_entity_id": "Entity id obbligatoria del primo riscaldatore",
"heater_entity2_id": "Entity id del secondo riscaldatore facoltativo. Lasciare vuoto se non utilizzato",
"heater_entity3_id": "Entity id del terzo riscaldatore facoltativo. Lasciare vuoto se non utilizzato",
"heater_entity4_id": "Entity id del quarto riscaldatore facoltativo. Lasciare vuoto se non utilizzato",
"proportional_function": "Algoritmo da utilizzare (il TPI per adesso è l'unico)",
"climate_entity_id": "Entity id del termostato sottostante"
}
},
"tpi": {
"title": "TPI",
"description": "Parametri del Time Proportional Integral",
"data": {
"tpi_coef_int": "Coefficiente per il delta della temperatura interna",
"tpi_coef_ext": "Coefficiente per il delta della temperatura esterna"
}
},
"presets": {
"title": "Presets",
"description": "Per ogni preset, impostare la temperatura desiderata (0 per ignorare il preset)",
"data": {
"eco_temp": "Temperatura nel preset Eco",
"comfort_temp": "Temperatura nel preset Comfort",
"boost_temp": "Temperatura nel preset Boost"
}
},
"window": {
"title": "Gestione della finestra",
"description": "Gestione della finestra aperta.\nLasciare vuoto l'entity_id corrispondente se non utilizzato\nÈ inoltre possibile configurare il rilevamento automatico della finestra aperta in base alla diminuzione della temperatura",
"data": {
"window_sensor_entity_id": "Entity id sensore finestra",
"window_delay": "Ritardo sensore finestra (secondi)",
"window_auto_open_threshold": "Soglia di diminuzione della temperatura per il rilevamento automatico della finestra aperta (in °/min)",
"window_auto_close_threshold": "Soglia di aumento della temperatura per la fine del rilevamento automatico (in °/min)",
"window_auto_max_duration": "Durata massima del rilevamento automatico della finestra aperta (in min)"
},
"data_description": {
"window_sensor_entity_id": "Lasciare vuoto se non deve essere utilizzato alcun sensore finestra",
"window_delay": "Ritardo in secondi prima che il rilevamento del sensore sia preso in considerazione",
"window_auto_open_threshold": "Valore consigliato: tra 0.05 e 0.1. Lasciare vuoto se il rilevamento automatico della finestra aperta non è utilizzato",
"window_auto_close_threshold": "Valore consigliato: 0. Lasciare vuoto se il rilevamento automatico della finestra aperta non è utilizzato",
"window_auto_max_duration": "Valore consigliato: 60 (un'ora). Lasciare vuoto se il rilevamento automatico della finestra aperta non è utilizzato"
}
},
"motion": {
"title": "Gestione movimento",
"description": "Gestione sensore movimento. Il preset può cambiare automaticamente a seconda di un rilevamento di movimento\nLasciare vuoto l'entity_id corrispondente se non utilizzato.\nmotion_preset e no_motion_preset devono essere impostati con il nome del preset corrispondente",
"data": {
"motion_sensor_entity_id": "Entity id sensore di movimento",
"motion_delay": "Ritardo in secondi prima che il rilevamento del sensore sia preso in considerazione",
"motion_preset": "Preset da utilizzare quando viene rilevato il movimento",
"no_motion_preset": "Preset da utilizzare quando non viene rilevato il movimento"
}
},
"power": {
"title": "Gestione dell'energia",
"description": "Parametri di gestione dell'energia.\nInserire la potenza massima disponibile e l'entity_id del sensore che la misura.\nQuindi inserire il consumo del riscaldatore quando è in funzione.\nTutti i parametri devono essere nella stessa unità di misura (kW o W).\nLasciare vuoto l'entity_id corrispondente se non utilizzato.",
"data": {
"power_sensor_entity_id": "Entity id sensore potenza",
"max_power_sensor_entity_id": "Entity id sensore di massima potenza",
"power_temp": "Temperatura in caso di distribuzione del carico"
}
},
"presence": {
"title": "Gestione della presenza",
"description": "Parametri di gestione della presenza.\nInserire un sensore di presenza (true se è presente qualcuno).\nQuindi specificare il preset o la riduzione di temperatura da utilizzare quando il sensore di presenza è in false.\nSe è impostato il preset, la riduzione non sarà utilizzata.\nLasciare vuoto l'entity_id corrispondente se non utilizzato.",
"data": {
"presence_sensor_entity_id": "Entity id sensore presenza (true se è presente qualcuno)",
"eco_away_temp": "Temperatura al preset Eco in caso d'assenza",
"comfort_away_temp": "Temperatura al preset Comfort in caso d'assenza",
"boost_away_temp": "Temperatura al preset Boost in caso d'assenza"
}
},
"advanced": {
"title": "Parametri avanzati",
"description": "Configurazione avanzata dei parametri. Lasciare i valori predefiniti se non conoscete cosa state modificando.\nQuesti parametri possono determinare una pessima gestione della temperatura e della potenza.",
"data": {
"minimal_activation_delay": "Ritardo minimo di accensione",
"security_delay_min": "Ritardo di sicurezza (in minuti)",
"security_min_on_percent": "Percentuale minima di potenza per la modalità di sicurezza",
"security_default_on_percent": "Percentuale di potenza per la modalità di sicurezza"
},
"data_description": {
"minimal_activation_delay": "Ritardo in secondi al di sotto del quale l'apparecchiatura non verrà attivata",
"security_delay_min": "Ritardo massimo consentito in minuti tra due misure di temperatura. Al di sopra di questo ritardo, il termostato passerà allo stato di sicurezza",
"security_min_on_percent": "Soglia percentuale minima di riscaldamento al di sotto della quale il preset di sicurezza non verrà mai attivato",
"security_default_on_percent": "Valore percentuale predefinito della potenza di riscaldamento nella modalità di sicurezza. Impostare a 0 per spegnere il riscaldatore nella modalità di sicurezza"
}
}
},
"error": {
"unknown": "Errore inatteso",
"unknown_entity": "Entity id sconosciuta",
"window_open_detection_method": "Può essere utilizzato un solo metodo di rilevamento finestra aperta. Utilizzare il sensore od il rilevamento automatico ma non entrambi"
},
"abort": {
"already_configured": "Il dispositivo è già configurato"
}
},
"options": {
"flow_title": "Configurazione di Versatile Thermostat",
"step": {
"user": {
"title": "Aggiungi un nuovo Versatile Thermostat",
"description": "Principali attributi obbligatori",
"data": {
"name": "Nome",
"thermostat_type": "Tipologia termostato",
"temperature_sensor_entity_id": "Entity id sensore di temperatura",
"external_temperature_sensor_entity_id": "Entity id sensore temperatura esterna",
"cycle_min": "Durata del ciclo (minuti)",
"temp_min": "Temperatura minima consentita",
"temp_max": "Temperatura massima consentita",
"device_power": "Potenza dispositivo (kW)",
"use_window_feature": "Usa il rilevamento della finestra",
"use_motion_feature": "Usa il rilevamento del movimento",
"use_power_feature": "Usa la gestione della potenza",
"use_presence_feature": "Usa il rilevamento della presenza"
}
},
"type": {
"title": "Entità collegate",
"description": "Attributi delle entità collegate",
"data": {
"heater_entity_id": "Interruttore riscaldatore",
"heater_entity2_id": "Secondo interruttore riscaldatore",
"heater_entity3_id": "Terzo interruttore riscaldatore",
"heater_entity4_id": "Quarto interruttore riscaldatore",
"proportional_function": "Algoritmo",
"climate_entity_id": "Termostato sottostante"
},
"data_description": {
"heater_entity_id": "Entity id obbligatoria del primo riscaldatore",
"heater_entity2_id": "Entity id del secondo riscaldatore facoltativo. Lasciare vuoto se non utilizzato",
"heater_entity3_id": "Entity id del terzo riscaldatore facoltativo. Lasciare vuoto se non utilizzato",
"heater_entity4_id": "Entity id del quarto riscaldatore facoltativo. Lasciare vuoto se non utilizzato",
"proportional_function": "Algoritmo da utilizzare (il TPI per adesso è l'unico)",
"climate_entity_id": "Entity id del termostato sottostante"
}
},
"tpi": {
"title": "TPI",
"description": "Parametri del Time Proportional Integral",
"data": {
"tpi_coef_int": "Coefficiente per il delta della temperatura interna",
"tpi_coef_ext": "Coefficiente per il delta della temperatura esterna"
}
},
"presets": {
"title": "Presets",
"description": "Per ogni preset, impostare la temperatura desiderata (0 per ignorare il preset)",
"data": {
"eco_temp": "Temperatura nel preset Eco",
"comfort_temp": "Temperatura nel preset Comfort",
"boost_temp": "Temperatura nel preset Boost"
}
},
"window": {
"title": "Gestione della finestra",
"description": "Gestione della finestra aperta.\nLasciare vuoto l'entity_id corrispondente se non utilizzato\nÈ inoltre possibile configurare il rilevamento automatico della finestra aperta in base alla diminuzione della temperatura",
"data": {
"window_sensor_entity_id": "Entity id sensore finestra",
"window_delay": "Ritardo sensore finestra (secondi)",
"window_auto_open_threshold": "Soglia di diminuzione della temperatura per il rilevamento automatico della finestra aperta (in °/min)",
"window_auto_close_threshold": "Soglia di aumento della temperatura per la fine del rilevamento automatico (in °/min)",
"window_auto_max_duration": "Durata massima del rilevamento automatico della finestra aperta (in min)"
},
"data_description": {
"window_sensor_entity_id": "Lasciare vuoto se non deve essere utilizzato alcun sensore finestra",
"window_delay": "Ritardo in secondi prima che il rilevamento del sensore sia preso in considerazione",
"window_auto_open_threshold": "Valore consigliato: tra 0.05 e 0.1. Lasciare vuoto se il rilevamento automatico della finestra aperta non è utilizzato",
"window_auto_close_threshold": "Valore consigliato: 0. Lasciare vuoto se il rilevamento automatico della finestra aperta non è utilizzato",
"window_auto_max_duration": "Valore consigliato: 60 (un'ora). Lasciare vuoto se il rilevamento automatico della finestra aperta non è utilizzato"
}
},
"motion": {
"title": "Gestione movimento",
"description": "Gestione sensore movimento. Il preset può cambiare automaticamente a seconda di un rilevamento di movimento\nLasciare vuoto l'entity_id corrispondente se non utilizzato.\nmotion_preset e no_motion_preset devono essere impostati con il nome del preset corrispondente",
"data": {
"motion_sensor_entity_id": "Entity id sensore di movimento",
"motion_delay": "Ritardo in secondi prima che il rilevamento del sensore sia preso in considerazione",
"motion_preset": "Preset da utilizzare quando viene rilevato il movimento",
"no_motion_preset": "Preset da utilizzare quando non viene rilevato il movimento"
}
},
"power": {
"title": "Gestione dell'energia",
"description": "Parametri di gestione dell'energia.\nInserire la potenza massima disponibile e l'entity_id del sensore che la misura.\nQuindi inserire il consumo del riscaldatore quando è in funzione.\nTutti i parametri devono essere nella stessa unità di misura (kW o W).\nLasciare vuoto l'entity_id corrispondente se non utilizzato.",
"data": {
"power_sensor_entity_id": "Entity id sensore potenza",
"max_power_sensor_entity_id": "Entity id sensore di massima potenza",
"power_temp": "Temperatura in caso di distribuzione del carico"
}
},
"presence": {
"title": "Gestione della presenza",
"description": "Parametri di gestione della presenza.\nInserire un sensore di presenza (true se è presente qualcuno).\nQuindi specificare il preset o la riduzione di temperatura da utilizzare quando il sensore di presenza è in false.\nSe è impostato il preset, la riduzione non sarà utilizzata.\nLasciare vuoto l'entity_id corrispondente se non utilizzato.",
"data": {
"presence_sensor_entity_id": "Entity id sensore presenza (true se è presente qualcuno)",
"eco_away_temp": "Temperatura al preset Eco in caso d'assenza",
"comfort_away_temp": "Temperatura al preset Comfort in caso d'assenza",
"boost_away_temp": "Temperatura al preset Boost in caso d'assenza"
}
},
"advanced": {
"title": "Parametri avanzati",
"description": "Configurazione avanzata dei parametri. Lasciare i valori predefiniti se non conoscete cosa state modificando.\nQuesti parametri possono determinare una pessima gestione della temperatura e della potenza.",
"data": {
"minimal_activation_delay": "Ritardo minimo di accensione",
"security_delay_min": "Ritardo di sicurezza (in minuti)",
"security_min_on_percent": "Percentuale minima di potenza per la modalità di sicurezza",
"security_default_on_percent": "Percentuale di potenza per la modalità di sicurezza"
},
"data_description": {
"minimal_activation_delay": "Ritardo in secondi al di sotto del quale l'apparecchiatura non verrà attivata",
"security_delay_min": "Ritardo massimo consentito in minuti tra due misure di temperatura. Al di sopra di questo ritardo, il termostato passerà allo stato di sicurezza",
"security_min_on_percent": "Soglia percentuale minima di riscaldamento al di sotto della quale il preset di sicurezza non verrà mai attivato",
"security_default_on_percent": "Valore percentuale predefinito della potenza di riscaldamento nella modalità di sicurezza. Impostare a 0 per spegnere il riscaldatore nella modalità di sicurezza"
}
}
},
"error": {
"unknown": "Errore inatteso",
"unknown_entity": "Entity id sconosciuta",
"window_open_detection_method": "Può essere utilizzato un solo metodo di rilevamento finestra aperta. Utilizzare il sensore od il rilevamento automatico ma non entrambi"
},
"abort": {
"already_configured": "Il dispositivo è già configurato"
}
},
"selector": {
"thermostat_type": {
"options": {
"thermostat_over_switch": "Termostato su un interruttore",
"thermostat_over_climate": "Termostato sopra un altro termostato"
}
}
},
"entity": {
"climate": {
"versatile_thermostat": {
"state_attributes": {
"preset_mode": {
"state": {
"power": "Ripartizione",
"security": "Sicurezza",
"none": "Manuale"
}
}
}
}
}
}
}

View File

@@ -167,6 +167,7 @@ class UnderlyingSwitch(UnderlyingEntity):
self._should_relaunch_control_heating = False
self._on_time_sec = 0
self._off_time_sec = 0
self._hvac_mode = None
@property
def initial_delay_sec(self):
@@ -174,11 +175,18 @@ class UnderlyingSwitch(UnderlyingEntity):
return self._initial_delay_sec
async def set_hvac_mode(self, hvac_mode: HVACMode) -> bool:
"""Set the HVACmode. Returns true if we need to redo a control_heating"""
"""Set the HVACmode. Returns true if something have change"""
if hvac_mode == HVACMode.OFF:
if self.is_device_active:
await self.turn_off()
return True
await self._cancel_cycle()
if self._hvac_mode != hvac_mode:
self._hvac_mode = hvac_mode
return True
else:
return False
@property
def is_device_active(self):
@@ -420,10 +428,10 @@ class UnderlyingClimate(UnderlyingEntity):
"""True if the underlying climate was found"""
return self._underlying_climate is not None
async def set_hvac_mode(self, hvac_mode: HVACMode):
"""Set the HVACmode of the underlying climate"""
async def set_hvac_mode(self, hvac_mode: HVACMode) -> bool:
"""Set the HVACmode of the underlying climate. Returns true if something have change"""
if not self.is_initialized:
return
return False
data = {ATTR_ENTITY_ID: self._entity_id, "hvac_mode": hvac_mode}
await self._hass.services.async_call(
@@ -432,6 +440,8 @@ class UnderlyingClimate(UnderlyingEntity):
data,
)
return True
@property
def is_device_active(self):
"""If the toggleable device is currently active."""
@@ -571,8 +581,41 @@ class UnderlyingClimate(UnderlyingEntity):
return self._underlying_climate.temperature_unit
@property
def target_temperature_step(self) -> str:
def target_temperature_step(self) -> float:
"""Get the target_temperature_step"""
if not self.is_initialized:
return 1
return self._underlying_climate.target_temperature_step
@property
def target_temperature_high(self) -> float:
"""Get the target_temperature_high"""
if not self.is_initialized:
return 30
return self._underlying_climate.target_temperature_high
@property
def target_temperature_low(self) -> float:
"""Get the target_temperature_low"""
if not self.is_initialized:
return 15
return self._underlying_climate.target_temperature_low
@property
def is_aux_heat(self) -> bool:
"""Get the is_aux_heat"""
if not self.is_initialized:
return False
return self._underlying_climate.is_aux_heat
def turn_aux_heat_on(self) -> None:
"""Turn auxiliary heater on."""
if not self.is_initialized:
return None
return self._underlying_climate.turn_aux_heat_on()
def turn_aux_heat_off(self) -> None:
"""Turn auxiliary heater on."""
if not self.is_initialized:
return None
return self._underlying_climate.turn_aux_heat_off()