Compare commits
10 Commits
5.1.0
...
5.0.0-alph
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8961a80086 | ||
|
|
2f6c46ef11 | ||
|
|
af817e74b7 | ||
|
|
46307286b1 | ||
|
|
c8398a48fe | ||
|
|
5063ba3802 | ||
|
|
fb76a84bde | ||
|
|
03f6045d34 | ||
|
|
aeb4b2fbbe | ||
|
|
47f5aa9595 |
@@ -59,8 +59,8 @@ input_number:
|
||||
unit_of_measurement: kW
|
||||
fake_valve1:
|
||||
name: The valve 1
|
||||
min: 10
|
||||
max: 90
|
||||
min: 0
|
||||
max: 100
|
||||
icon: mdi:pipe-valve
|
||||
unit_of_measurement: percentage
|
||||
|
||||
|
||||
@@ -30,8 +30,13 @@
|
||||
"waderyan.gitblame",
|
||||
"keesschollaart.vscode-home-assistant",
|
||||
"vscode.markdown-math",
|
||||
"yzhang.markdown-all-in-one"
|
||||
"yzhang.markdown-all-in-one",
|
||||
"ms-python.vscode-pylance"
|
||||
],
|
||||
// "mounts": [
|
||||
// "source=${localWorkspaceFolder}/.devcontainer/configuration.yaml,target=${localWorkspaceFolder}/config/www/community/,type=bind,consistency=cached",
|
||||
// "source=${localWorkspaceFolder}/custom_components,target=/home/vscode/core/config/custom_components,type=bind,consistency=cached"
|
||||
// ],
|
||||
"settings": {
|
||||
"files.eol": "\n",
|
||||
"editor.tabSize": 4,
|
||||
|
||||
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
@@ -14,8 +14,7 @@
|
||||
"python.testing.pytestEnabled": true,
|
||||
"python.analysis.extraPaths": [
|
||||
// "/home/vscode/core",
|
||||
"/workspaces/versatile_thermostat/custom_components/versatile_thermostat",
|
||||
"/home/vscode/.local/lib/python3.11/site-packages/homeassistant"
|
||||
"/workspaces/versatile_thermostat/custom_components/versatile_thermostat"
|
||||
],
|
||||
"python.formatting.provider": "none"
|
||||
}
|
||||
@@ -114,7 +114,7 @@ En conséquence toute la phase de paramètrage d'un VTherm a été profondemment
|
||||
**Note :** les copies d'écran de la configuration d'un VTherm n'ont pas été mises à jour.
|
||||
|
||||
# Merci pour la bière [buymecoffee](https://www.buymeacoffee.com/jmcollin78)
|
||||
Un grand merci à @salabur, @pvince83, @bergoglio, @EPicLURcher, @ecolorado66, @Kriss1670, @maia, @f.maymil, @moutte69, @Jerome, @Gunnar M, @Greg.o, @John Burgess, @abyssmal, @capinfo26, @Helge, @MattG @Mexx62, @Someone pour les bières. Ca fait très plaisir et ça m'encourage à continuer !
|
||||
Un grand merci à @salabur, @pvince83, @bergoglio, @EPicLURcher, @ecolorado66, @Kriss1670, @maia, @f.maymil, @moutte69, @Jerome, @Gunnar M, @Greg.o, @John Burgess, @abyssmal, @capinfo26, @Helge, @MattG pour les bières. Ca fait très plaisir et ça m'encourage à continuer !
|
||||
|
||||
|
||||
# Quand l'utiliser et ne pas l'utiliser
|
||||
|
||||
@@ -114,7 +114,7 @@ Consequently, the entire configuration phase of a VTherm has been profoundly mod
|
||||
**Note:** the VTherm configuration screenshots have not been updated.
|
||||
|
||||
# Thanks for the beer [buymecoffee](https://www.buymeacoffee.com/jmcollin78)
|
||||
Many thanks to @salabur, @pvince83, @bergoglio, @EPicLURcher, @ecolorado66, @Kriss1670, @maia, @f.maymil, @moutte69, @Jerome, @Gunnar M, @Greg.o, @John Burgess, @abyssmal, @capinfo26, @Helge, @MattG, @MattG, @Mexx62, @Someone for the beers. It's very nice and encourages me to continue!
|
||||
Many thanks to @salabur, @pvince83, @bergoglio, @EPicLURcher, @ecolorado66, @Kriss1670, @maia, @f.maymil, @moutte69, @Jerome, @Gunnar M, @Greg.o, @John Burgess, @abyssmal, @capinfo26, @Helge, @MattG for the beers. It's very nice and encourages me to continue!
|
||||
|
||||
# When to use / not use
|
||||
This thermostat can control 3 types of equipment:
|
||||
|
||||
@@ -86,10 +86,7 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
|
||||
|
||||
# VTherm API should have been initialized before arriving here
|
||||
vtherm_api = VersatileThermostatAPI.get_vtherm_api()
|
||||
if vtherm_api is not None:
|
||||
self._central_config = vtherm_api.find_central_configuration()
|
||||
else:
|
||||
self._central_config = None
|
||||
self._central_config = vtherm_api.find_central_configuration()
|
||||
|
||||
self._init_feature_flags(infos)
|
||||
self._init_central_config_flags(infos)
|
||||
@@ -122,7 +119,6 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
|
||||
CONF_USE_WINDOW_CENTRAL_CONFIG,
|
||||
CONF_USE_MOTION_CENTRAL_CONFIG,
|
||||
CONF_USE_POWER_CENTRAL_CONFIG,
|
||||
CONF_USE_PRESETS_CENTRAL_CONFIG,
|
||||
CONF_USE_PRESENCE_CENTRAL_CONFIG,
|
||||
CONF_USE_ADVANCED_CENTRAL_CONFIG,
|
||||
):
|
||||
@@ -171,7 +167,7 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
|
||||
_LOGGER.error(
|
||||
"Only one window detection method should be used. Use window_sensor or auto window open detection but not both"
|
||||
)
|
||||
raise WindowOpenDetectionMethod(CONF_WINDOW_AUTO_OPEN_THRESHOLD)
|
||||
raise WindowOpenDetectionMethod(CONF_WINDOW_SENSOR)
|
||||
|
||||
# Check that is USE_CENTRAL config is used, that a central config exists
|
||||
if self._central_config is None:
|
||||
@@ -364,7 +360,7 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
|
||||
schema = schema_ac_or_not
|
||||
elif user_input and user_input.get(CONF_USE_PRESETS_CENTRAL_CONFIG) is False:
|
||||
next_step = self.async_step_spec_presets
|
||||
schema = STEP_PRESETS_DATA_SCHEMA
|
||||
schema = schema_ac_or_not
|
||||
|
||||
return await self.generic_step("presets", schema, user_input, next_step)
|
||||
|
||||
@@ -408,11 +404,7 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
|
||||
next_step = self.async_step_motion
|
||||
# If comes from async_step_spec_window
|
||||
elif self._infos.get(COMES_FROM) == "async_step_spec_window":
|
||||
# If we have a window sensor don't display the auto window parameters
|
||||
if self._infos.get(CONF_WINDOW_SENSOR) is not None:
|
||||
schema = STEP_CENTRAL_WINDOW_WO_AUTO_DATA_SCHEMA
|
||||
else:
|
||||
schema = STEP_CENTRAL_WINDOW_DATA_SCHEMA
|
||||
schema = STEP_CENTRAL_WINDOW_DATA_SCHEMA
|
||||
elif user_input and user_input.get(CONF_USE_WINDOW_CENTRAL_CONFIG) is False:
|
||||
next_step = self.async_step_spec_window
|
||||
|
||||
@@ -427,8 +419,6 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
|
||||
)
|
||||
|
||||
schema = STEP_CENTRAL_WINDOW_DATA_SCHEMA
|
||||
if self._infos.get(CONF_WINDOW_SENSOR) is not None:
|
||||
schema = STEP_CENTRAL_WINDOW_WO_AUTO_DATA_SCHEMA
|
||||
|
||||
self._infos[COMES_FROM] = "async_step_spec_window"
|
||||
|
||||
|
||||
@@ -195,12 +195,6 @@ STEP_CENTRAL_WINDOW_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name
|
||||
}
|
||||
)
|
||||
|
||||
STEP_CENTRAL_WINDOW_WO_AUTO_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name
|
||||
{
|
||||
vol.Optional(CONF_WINDOW_DELAY, default=30): cv.positive_int,
|
||||
}
|
||||
)
|
||||
|
||||
STEP_MOTION_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name
|
||||
{
|
||||
vol.Optional(CONF_MOTION_SENSOR): selector.EntitySelector(
|
||||
|
||||
@@ -413,7 +413,6 @@
|
||||
"eco_away_temp": "Eco away preset",
|
||||
"comfort_away_temp": "Comfort away preset",
|
||||
"boost_away_temp": "Boost away preset",
|
||||
"frost_away_temp": "Frost protection preset",
|
||||
"eco_ac_away_temp": "Eco away preset in AC mode",
|
||||
"comfort_ac_away_temp": "Comfort away preset in AC mode",
|
||||
"boost_ac_away_temp": "Boost away preset in AC mode",
|
||||
|
||||
@@ -136,11 +136,6 @@ class ThermostatOverClimate(BaseThermostat):
|
||||
|
||||
async def _send_regulated_temperature(self, force=False):
|
||||
"""Sends the regulated temperature to all underlying"""
|
||||
|
||||
if self.hvac_mode == HVACMode.OFF:
|
||||
_LOGGER.debug("%s - don't send regulated temperature cause VTherm is off ")
|
||||
return
|
||||
|
||||
_LOGGER.info(
|
||||
"%s - Calling ThermostatClimate._send_regulated_temperature force=%s",
|
||||
self,
|
||||
@@ -239,45 +234,45 @@ class ThermostatOverClimate(BaseThermostat):
|
||||
await self.async_set_fan_mode(self._auto_deactivated_fan_mode)
|
||||
|
||||
@overrides
|
||||
def post_init(self, config_entry):
|
||||
def post_init(self, entry_infos):
|
||||
"""Initialize the Thermostat"""
|
||||
|
||||
super().post_init(config_entry)
|
||||
super().post_init(entry_infos)
|
||||
for climate in [
|
||||
CONF_CLIMATE,
|
||||
CONF_CLIMATE_2,
|
||||
CONF_CLIMATE_3,
|
||||
CONF_CLIMATE_4,
|
||||
]:
|
||||
if config_entry.get(climate):
|
||||
if entry_infos.get(climate):
|
||||
self._underlyings.append(
|
||||
UnderlyingClimate(
|
||||
hass=self._hass,
|
||||
thermostat=self,
|
||||
climate_entity_id=config_entry.get(climate),
|
||||
climate_entity_id=entry_infos.get(climate),
|
||||
)
|
||||
)
|
||||
|
||||
self.choose_auto_regulation_mode(
|
||||
config_entry.get(CONF_AUTO_REGULATION_MODE)
|
||||
if config_entry.get(CONF_AUTO_REGULATION_MODE) is not None
|
||||
entry_infos.get(CONF_AUTO_REGULATION_MODE)
|
||||
if entry_infos.get(CONF_AUTO_REGULATION_MODE) is not None
|
||||
else CONF_AUTO_REGULATION_NONE
|
||||
)
|
||||
|
||||
self._auto_regulation_dtemp = (
|
||||
config_entry.get(CONF_AUTO_REGULATION_DTEMP)
|
||||
if config_entry.get(CONF_AUTO_REGULATION_DTEMP) is not None
|
||||
entry_infos.get(CONF_AUTO_REGULATION_DTEMP)
|
||||
if entry_infos.get(CONF_AUTO_REGULATION_DTEMP) is not None
|
||||
else 0.5
|
||||
)
|
||||
self._auto_regulation_period_min = (
|
||||
config_entry.get(CONF_AUTO_REGULATION_PERIOD_MIN)
|
||||
if config_entry.get(CONF_AUTO_REGULATION_PERIOD_MIN) is not None
|
||||
entry_infos.get(CONF_AUTO_REGULATION_PERIOD_MIN)
|
||||
if entry_infos.get(CONF_AUTO_REGULATION_PERIOD_MIN) is not None
|
||||
else 5
|
||||
)
|
||||
|
||||
self._auto_fan_mode = (
|
||||
config_entry.get(CONF_AUTO_FAN_MODE)
|
||||
if config_entry.get(CONF_AUTO_FAN_MODE) is not None
|
||||
entry_infos.get(CONF_AUTO_FAN_MODE)
|
||||
if entry_infos.get(CONF_AUTO_FAN_MODE) is not None
|
||||
else CONF_AUTO_FAN_NONE
|
||||
)
|
||||
|
||||
|
||||
@@ -48,9 +48,9 @@ class ThermostatOverSwitch(BaseThermostat):
|
||||
)
|
||||
|
||||
# useless for now
|
||||
# def __init__(self, hass: HomeAssistant, unique_id, name, config_entry) -> None:
|
||||
# def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None:
|
||||
# """Initialize the thermostat over switch."""
|
||||
# super().__init__(hass, unique_id, name, config_entry)
|
||||
# super().__init__(hass, unique_id, name, entry_infos)
|
||||
_is_inversed: bool = None
|
||||
|
||||
@property
|
||||
@@ -72,10 +72,10 @@ class ThermostatOverSwitch(BaseThermostat):
|
||||
return None
|
||||
|
||||
@overrides
|
||||
def post_init(self, config_entry):
|
||||
def post_init(self, entry_infos):
|
||||
"""Initialize the Thermostat"""
|
||||
|
||||
super().post_init(config_entry)
|
||||
super().post_init(entry_infos)
|
||||
|
||||
self._prop_algorithm = PropAlgorithm(
|
||||
self._proportional_function,
|
||||
@@ -85,13 +85,13 @@ class ThermostatOverSwitch(BaseThermostat):
|
||||
self._minimal_activation_delay,
|
||||
)
|
||||
|
||||
lst_switches = [config_entry.get(CONF_HEATER)]
|
||||
if config_entry.get(CONF_HEATER_2):
|
||||
lst_switches.append(config_entry.get(CONF_HEATER_2))
|
||||
if config_entry.get(CONF_HEATER_3):
|
||||
lst_switches.append(config_entry.get(CONF_HEATER_3))
|
||||
if config_entry.get(CONF_HEATER_4):
|
||||
lst_switches.append(config_entry.get(CONF_HEATER_4))
|
||||
lst_switches = [entry_infos.get(CONF_HEATER)]
|
||||
if entry_infos.get(CONF_HEATER_2):
|
||||
lst_switches.append(entry_infos.get(CONF_HEATER_2))
|
||||
if entry_infos.get(CONF_HEATER_3):
|
||||
lst_switches.append(entry_infos.get(CONF_HEATER_3))
|
||||
if entry_infos.get(CONF_HEATER_4):
|
||||
lst_switches.append(entry_infos.get(CONF_HEATER_4))
|
||||
|
||||
delta_cycle = self._cycle_min * 60 / len(lst_switches)
|
||||
for idx, switch in enumerate(lst_switches):
|
||||
@@ -104,7 +104,7 @@ class ThermostatOverSwitch(BaseThermostat):
|
||||
)
|
||||
)
|
||||
|
||||
self._is_inversed = config_entry.get(CONF_INVERSE_SWITCH) is True
|
||||
self._is_inversed = entry_infos.get(CONF_INVERSE_SWITCH) is True
|
||||
self._should_relaunch_control_heating = False
|
||||
|
||||
@overrides
|
||||
|
||||
@@ -3,10 +3,7 @@
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
|
||||
from homeassistant.helpers.event import (
|
||||
async_track_state_change_event,
|
||||
async_track_time_interval,
|
||||
)
|
||||
from homeassistant.helpers.event import async_track_state_change_event, async_track_time_interval
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.components.climate import HVACMode
|
||||
|
||||
@@ -19,53 +16,39 @@ from .underlyings import UnderlyingValve
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ThermostatOverValve(BaseThermostat):
|
||||
"""Representation of a class for a Versatile Thermostat over a Valve"""
|
||||
|
||||
_entity_component_unrecorded_attributes = (
|
||||
BaseThermostat._entity_component_unrecorded_attributes.union(
|
||||
frozenset(
|
||||
{
|
||||
"is_over_valve",
|
||||
"underlying_valve_0",
|
||||
"underlying_valve_1",
|
||||
"underlying_valve_2",
|
||||
"underlying_valve_3",
|
||||
"on_time_sec",
|
||||
"off_time_sec",
|
||||
"cycle_min",
|
||||
"function",
|
||||
"tpi_coef_int",
|
||||
"tpi_coef_ext",
|
||||
}
|
||||
)
|
||||
)
|
||||
)
|
||||
_entity_component_unrecorded_attributes = BaseThermostat._entity_component_unrecorded_attributes.union(frozenset(
|
||||
{
|
||||
"is_over_valve", "underlying_valve_0", "underlying_valve_1",
|
||||
"underlying_valve_2", "underlying_valve_3", "on_time_sec", "off_time_sec",
|
||||
"cycle_min", "function", "tpi_coef_int", "tpi_coef_ext"
|
||||
}))
|
||||
|
||||
# Useless for now
|
||||
# def __init__(self, hass: HomeAssistant, unique_id, name, config_entry) -> None:
|
||||
# def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None:
|
||||
# """Initialize the thermostat over switch."""
|
||||
# super().__init__(hass, unique_id, name, config_entry)
|
||||
# super().__init__(hass, unique_id, name, entry_infos)
|
||||
|
||||
@property
|
||||
def is_over_valve(self) -> bool:
|
||||
"""True if the Thermostat is over_valve"""
|
||||
""" True if the Thermostat is over_valve"""
|
||||
return True
|
||||
|
||||
@property
|
||||
def valve_open_percent(self) -> int:
|
||||
"""Gives the percentage of valve needed"""
|
||||
""" Gives the percentage of valve needed"""
|
||||
if self._hvac_mode == HVACMode.OFF:
|
||||
return 0
|
||||
else:
|
||||
return round(max(0, min(self.proportional_algorithm.on_percent, 1)) * 100)
|
||||
|
||||
@overrides
|
||||
def post_init(self, config_entry):
|
||||
"""Initialize the Thermostat"""
|
||||
def post_init(self, entry_infos):
|
||||
""" Initialize the Thermostat"""
|
||||
|
||||
super().post_init(config_entry)
|
||||
super().post_init(entry_infos)
|
||||
self._prop_algorithm = PropAlgorithm(
|
||||
self._proportional_function,
|
||||
self._tpi_coef_int,
|
||||
@@ -74,17 +57,21 @@ class ThermostatOverValve(BaseThermostat):
|
||||
self._minimal_activation_delay,
|
||||
)
|
||||
|
||||
lst_valves = [config_entry.get(CONF_VALVE)]
|
||||
if config_entry.get(CONF_VALVE_2):
|
||||
lst_valves.append(config_entry.get(CONF_VALVE_2))
|
||||
if config_entry.get(CONF_VALVE_3):
|
||||
lst_valves.append(config_entry.get(CONF_VALVE_3))
|
||||
if config_entry.get(CONF_VALVE_4):
|
||||
lst_valves.append(config_entry.get(CONF_VALVE_4))
|
||||
lst_valves = [entry_infos.get(CONF_VALVE)]
|
||||
if entry_infos.get(CONF_VALVE_2):
|
||||
lst_valves.append(entry_infos.get(CONF_VALVE_2))
|
||||
if entry_infos.get(CONF_VALVE_3):
|
||||
lst_valves.append(entry_infos.get(CONF_VALVE_3))
|
||||
if entry_infos.get(CONF_VALVE_4):
|
||||
lst_valves.append(entry_infos.get(CONF_VALVE_4))
|
||||
|
||||
for _, valve in enumerate(lst_valves):
|
||||
self._underlyings.append(
|
||||
UnderlyingValve(hass=self._hass, thermostat=self, valve_entity_id=valve)
|
||||
UnderlyingValve(
|
||||
hass=self._hass,
|
||||
thermostat=self,
|
||||
valve_entity_id=valve
|
||||
)
|
||||
)
|
||||
|
||||
self._should_relaunch_control_heating = False
|
||||
@@ -102,7 +89,7 @@ class ThermostatOverValve(BaseThermostat):
|
||||
async_track_state_change_event(
|
||||
self.hass, [valve.entity_id], self._async_valve_changed
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
# Start the control_heating
|
||||
# starts a cycle
|
||||
@@ -120,34 +107,29 @@ class ThermostatOverValve(BaseThermostat):
|
||||
This method just log the change. It changes nothing to avoid loops.
|
||||
"""
|
||||
new_state = event.data.get("new_state")
|
||||
_LOGGER.debug(
|
||||
"%s - _async_valve_changed new_state is %s", self, new_state.state
|
||||
)
|
||||
_LOGGER.debug("%s - _async_valve_changed new_state is %s", self, new_state.state)
|
||||
|
||||
@overrides
|
||||
def update_custom_attributes(self):
|
||||
"""Custom attributes"""
|
||||
""" Custom attributes """
|
||||
super().update_custom_attributes()
|
||||
self._attr_extra_state_attributes[
|
||||
"valve_open_percent"
|
||||
] = self.valve_open_percent
|
||||
self._attr_extra_state_attributes["valve_open_percent"] = self.valve_open_percent
|
||||
self._attr_extra_state_attributes["is_over_valve"] = self.is_over_valve
|
||||
self._attr_extra_state_attributes["underlying_valve_0"] = self._underlyings[
|
||||
0
|
||||
].entity_id
|
||||
self._attr_extra_state_attributes["underlying_valve_0"] = (
|
||||
self._underlyings[0].entity_id)
|
||||
self._attr_extra_state_attributes["underlying_valve_1"] = (
|
||||
self._underlyings[1].entity_id if len(self._underlyings) > 1 else None
|
||||
)
|
||||
self._underlyings[1].entity_id if len(self._underlyings) > 1 else None
|
||||
)
|
||||
self._attr_extra_state_attributes["underlying_valve_2"] = (
|
||||
self._underlyings[2].entity_id if len(self._underlyings) > 2 else None
|
||||
)
|
||||
self._underlyings[2].entity_id if len(self._underlyings) > 2 else None
|
||||
)
|
||||
self._attr_extra_state_attributes["underlying_valve_3"] = (
|
||||
self._underlyings[3].entity_id if len(self._underlyings) > 3 else None
|
||||
)
|
||||
self._underlyings[3].entity_id if len(self._underlyings) > 3 else None
|
||||
)
|
||||
|
||||
self._attr_extra_state_attributes[
|
||||
"on_percent"
|
||||
] = self._prop_algorithm.on_percent
|
||||
"on_percent"
|
||||
] = self._prop_algorithm.on_percent
|
||||
self._attr_extra_state_attributes[
|
||||
"on_time_sec"
|
||||
] = self._prop_algorithm.on_time_sec
|
||||
@@ -180,7 +162,9 @@ class ThermostatOverValve(BaseThermostat):
|
||||
)
|
||||
|
||||
for under in self._underlyings:
|
||||
under.set_valve_open_percent()
|
||||
under.set_valve_open_percent(
|
||||
self._prop_algorithm.on_percent
|
||||
)
|
||||
|
||||
self.update_custom_attributes()
|
||||
self.async_write_ha_state()
|
||||
@@ -201,4 +185,4 @@ class ThermostatOverValve(BaseThermostat):
|
||||
self,
|
||||
added_energy,
|
||||
self._total_energy,
|
||||
)
|
||||
)
|
||||
@@ -413,7 +413,6 @@
|
||||
"eco_away_temp": "Eco away preset",
|
||||
"comfort_away_temp": "Comfort away preset",
|
||||
"boost_away_temp": "Boost away preset",
|
||||
"frost_away_temp": "Frost protection preset",
|
||||
"eco_ac_away_temp": "Eco away preset in AC mode",
|
||||
"comfort_ac_away_temp": "Comfort away preset in AC mode",
|
||||
"boost_ac_away_temp": "Boost away preset in AC mode",
|
||||
|
||||
@@ -6,7 +6,6 @@ from typing import Any
|
||||
from enum import StrEnum
|
||||
|
||||
from homeassistant.const import ATTR_ENTITY_ID, STATE_ON, UnitOfTemperature
|
||||
from homeassistant.core import State
|
||||
|
||||
from homeassistant.exceptions import ServiceNotFound
|
||||
|
||||
@@ -112,18 +111,18 @@ class UnderlyingEntity:
|
||||
# This should be the correct way to handle turn_off and turn_on but this breaks the unit test
|
||||
# will an not understandable error: TypeError: object MagicMock can't be used in 'await' expression
|
||||
async def turn_off(self):
|
||||
"""Turn off the underlying equipement.
|
||||
Need to be overriden"""
|
||||
""" Turn off the underlying equipement.
|
||||
Need to be overriden"""
|
||||
return NotImplementedError
|
||||
|
||||
async def turn_on(self):
|
||||
"""Turn off the underlying equipement.
|
||||
Need to be overriden"""
|
||||
""" Turn off the underlying equipement.
|
||||
Need to be overriden"""
|
||||
return NotImplementedError
|
||||
|
||||
@property
|
||||
def is_inversed(self):
|
||||
"""Tells if the switch command should be inversed"""
|
||||
""" Tells if the switch command should be inversed"""
|
||||
return False
|
||||
|
||||
def remove_entity(self):
|
||||
@@ -165,11 +164,7 @@ class UnderlyingEntity:
|
||||
"""Starting cycle for switch"""
|
||||
|
||||
def _cancel_cycle(self):
|
||||
"""Stops an eventual cycle"""
|
||||
|
||||
def cap_sent_value(self, value) -> float:
|
||||
"""capping of the value send to the underlying eqt"""
|
||||
return value
|
||||
""" Stops an eventual cycle """
|
||||
|
||||
|
||||
class UnderlyingSwitch(UnderlyingEntity):
|
||||
@@ -210,7 +205,7 @@ class UnderlyingSwitch(UnderlyingEntity):
|
||||
@overrides
|
||||
@property
|
||||
def is_inversed(self):
|
||||
"""Tells if the switch command should be inversed"""
|
||||
""" Tells if the switch command should be inversed"""
|
||||
return self._thermostat.is_inversed
|
||||
|
||||
# @overrides this breaks some unit tests TypeError: object MagicMock can't be used in 'await' expression
|
||||
@@ -232,16 +227,14 @@ class UnderlyingSwitch(UnderlyingEntity):
|
||||
def is_device_active(self):
|
||||
"""If the toggleable device is currently active."""
|
||||
real_state = self._hass.states.is_state(self._entity_id, STATE_ON)
|
||||
return (self.is_inversed and not real_state) or (
|
||||
not self.is_inversed and real_state
|
||||
)
|
||||
return (self.is_inversed and not real_state) or (not self.is_inversed and real_state)
|
||||
|
||||
# @overrides this breaks some unit tests TypeError: object MagicMock can't be used in 'await' expression
|
||||
async def turn_off(self):
|
||||
"""Turn heater toggleable device off."""
|
||||
_LOGGER.debug("%s - Stopping underlying entity %s", self, self._entity_id)
|
||||
command = SERVICE_TURN_OFF if not self.is_inversed else SERVICE_TURN_ON
|
||||
domain = self._entity_id.split(".")[0]
|
||||
domain = self._entity_id.split('.')[0]
|
||||
# This may fails if called after shutdown
|
||||
try:
|
||||
data = {ATTR_ENTITY_ID: self._entity_id}
|
||||
@@ -257,7 +250,7 @@ class UnderlyingSwitch(UnderlyingEntity):
|
||||
"""Turn heater toggleable device on."""
|
||||
_LOGGER.debug("%s - Starting underlying entity %s", self, self._entity_id)
|
||||
command = SERVICE_TURN_ON if not self.is_inversed else SERVICE_TURN_OFF
|
||||
domain = self._entity_id.split(".")[0]
|
||||
domain = self._entity_id.split('.')[0]
|
||||
try:
|
||||
data = {ATTR_ENTITY_ID: self._entity_id}
|
||||
await self._hass.services.async_call(
|
||||
@@ -268,6 +261,7 @@ class UnderlyingSwitch(UnderlyingEntity):
|
||||
except ServiceNotFound as err:
|
||||
_LOGGER.error(err)
|
||||
|
||||
|
||||
@overrides
|
||||
async def start_cycle(
|
||||
self,
|
||||
@@ -496,14 +490,10 @@ class UnderlyingClimate(UnderlyingEntity):
|
||||
def is_device_active(self):
|
||||
"""If the toggleable device is currently active."""
|
||||
if self.is_initialized:
|
||||
return (
|
||||
self._underlying_climate.hvac_mode != HVACMode.OFF
|
||||
and self._underlying_climate.hvac_action
|
||||
not in [
|
||||
HVACAction.IDLE,
|
||||
HVACAction.OFF,
|
||||
]
|
||||
)
|
||||
return self._underlying_climate.hvac_mode != HVACMode.OFF and self._underlying_climate.hvac_action not in [
|
||||
HVACAction.IDLE,
|
||||
HVACAction.OFF,
|
||||
]
|
||||
else:
|
||||
return None
|
||||
|
||||
@@ -560,7 +550,7 @@ class UnderlyingClimate(UnderlyingEntity):
|
||||
return
|
||||
data = {
|
||||
ATTR_ENTITY_ID: self._entity_id,
|
||||
"temperature": self.cap_sent_value(temperature),
|
||||
"temperature": temperature,
|
||||
"target_temp_high": max_temp,
|
||||
"target_temp_low": min_temp,
|
||||
}
|
||||
@@ -674,40 +664,6 @@ class UnderlyingClimate(UnderlyingEntity):
|
||||
return None
|
||||
return self._underlying_climate.turn_aux_heat_off()
|
||||
|
||||
@overrides
|
||||
def cap_sent_value(self, value) -> float:
|
||||
"""Try to adapt the target temp value to the min_temp / max_temp found
|
||||
in the underlying entity (if any)"""
|
||||
|
||||
if not self.is_initialized:
|
||||
return value
|
||||
|
||||
# Gets the min_temp and max_temp
|
||||
if (
|
||||
self._underlying_climate.min_temp is not None
|
||||
and self._underlying_climate is not None
|
||||
):
|
||||
min_val = self._underlying_climate.min_temp
|
||||
max_val = self._underlying_climate.max_temp
|
||||
|
||||
new_value = round(max(min_val, min(value, max_val)))
|
||||
else:
|
||||
_LOGGER.debug("%s - no min and max attributes on underlying", self)
|
||||
new_value = value
|
||||
|
||||
if new_value != value:
|
||||
_LOGGER.info(
|
||||
"%s - Target temp have been updated due min, max of the underlying entity. new_value=%.0f value=%.0f min=%.0f max=%.0f",
|
||||
self,
|
||||
new_value,
|
||||
value,
|
||||
min_val,
|
||||
max_val,
|
||||
)
|
||||
|
||||
return new_value
|
||||
|
||||
|
||||
class UnderlyingValve(UnderlyingEntity):
|
||||
"""Represent a underlying switch"""
|
||||
|
||||
@@ -716,7 +672,10 @@ class UnderlyingValve(UnderlyingEntity):
|
||||
_percent_open: int
|
||||
|
||||
def __init__(
|
||||
self, hass: HomeAssistant, thermostat: Any, valve_entity_id: str
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
thermostat: Any,
|
||||
valve_entity_id: str
|
||||
) -> None:
|
||||
"""Initialize the underlying switch"""
|
||||
|
||||
@@ -730,14 +689,13 @@ class UnderlyingValve(UnderlyingEntity):
|
||||
self._should_relaunch_control_heating = False
|
||||
self._hvac_mode = None
|
||||
self._percent_open = self._thermostat.valve_open_percent
|
||||
self._valve_entity_id = valve_entity_id
|
||||
|
||||
async def send_percent_open(self):
|
||||
"""Send the percent open to the underlying valve"""
|
||||
""" Send the percent open to the underlying valve """
|
||||
# This may fails if called after shutdown
|
||||
try:
|
||||
data = {ATTR_ENTITY_ID: self._entity_id, "value": self._percent_open}
|
||||
domain = self._entity_id.split(".")[0]
|
||||
data = { ATTR_ENTITY_ID: self._entity_id, "value": self._percent_open }
|
||||
domain = self._entity_id.split('.')[0]
|
||||
await self._hass.services.async_call(
|
||||
domain,
|
||||
SERVICE_SET_VALUE,
|
||||
@@ -776,7 +734,7 @@ class UnderlyingValve(UnderlyingEntity):
|
||||
# To test if real device is open but this is causing some side effect
|
||||
# because the activation can be deferred -
|
||||
# or float(self._hass.states.get(self._entity_id).state) > 0
|
||||
except Exception: # pylint: disable=broad-exception-caught
|
||||
except Exception: # pylint: disable=broad-exception-caught
|
||||
return False
|
||||
|
||||
@overrides
|
||||
@@ -792,40 +750,9 @@ class UnderlyingValve(UnderlyingEntity):
|
||||
if force:
|
||||
await self.send_percent_open()
|
||||
|
||||
@overrides
|
||||
def cap_sent_value(self, value) -> float:
|
||||
"""Try to adapt the open_percent value to the min / max found
|
||||
in the underlying entity (if any)"""
|
||||
|
||||
# Gets the last number state
|
||||
valve_state: State = self._hass.states.get(self._valve_entity_id)
|
||||
if valve_state is None:
|
||||
return value
|
||||
|
||||
if "min" in valve_state.attributes and "max" in valve_state.attributes:
|
||||
min_val = valve_state.attributes["min"]
|
||||
max_val = valve_state.attributes["max"]
|
||||
|
||||
new_value = round(max(min_val, min(value, max_val)))
|
||||
else:
|
||||
_LOGGER.debug("%s - no min and max attributes on underlying", self)
|
||||
new_value = value
|
||||
|
||||
if new_value != value:
|
||||
_LOGGER.info(
|
||||
"%s - Valve open percent have been updated due min, max of the underlying entity. new_value=%.0f value=%.0f min=%.0f max=%.0f",
|
||||
self,
|
||||
new_value,
|
||||
value,
|
||||
min_val,
|
||||
max_val,
|
||||
)
|
||||
|
||||
return new_value
|
||||
|
||||
def set_valve_open_percent(self):
|
||||
"""Update the valve open percent"""
|
||||
caped_val = self.cap_sent_value(self._thermostat.valve_open_percent)
|
||||
def set_valve_open_percent(self, percent):
|
||||
""" Update the valve open percent """
|
||||
caped_val = self._thermostat.valve_open_percent
|
||||
if self._percent_open == caped_val:
|
||||
# No changes
|
||||
return
|
||||
@@ -833,9 +760,7 @@ class UnderlyingValve(UnderlyingEntity):
|
||||
self._percent_open = caped_val
|
||||
# Send the new command to valve via a service call
|
||||
|
||||
_LOGGER.info(
|
||||
"%s - Setting valve ouverture percent to %s", self, self._percent_open
|
||||
)
|
||||
_LOGGER.info("%s - Setting valve ouverture percent to %s", self, self._percent_open)
|
||||
# Send the change to the valve, in background
|
||||
self._hass.create_task(self.send_percent_open())
|
||||
|
||||
|
||||
@@ -19,27 +19,26 @@ _LOGGER = logging.getLogger(__name__)
|
||||
class VersatileThermostatAPI(dict):
|
||||
"""The VersatileThermostatAPI"""
|
||||
|
||||
_hass: HomeAssistant = None
|
||||
_hass: HomeAssistant
|
||||
# _entries: Dict(str, ConfigEntry)
|
||||
|
||||
@classmethod
|
||||
def get_vtherm_api(cls, hass=None):
|
||||
"""Get the eventual VTherm API class instance or
|
||||
instantiate it if it doesn't exists"""
|
||||
"""Get the eventual VTherm API class instance"""
|
||||
if hass is not None:
|
||||
VersatileThermostatAPI._hass = hass
|
||||
|
||||
if VersatileThermostatAPI._hass is None:
|
||||
return None
|
||||
else:
|
||||
if VersatileThermostatAPI._hass is None:
|
||||
return None
|
||||
|
||||
domain = VersatileThermostatAPI._hass.data.get(DOMAIN)
|
||||
if not domain:
|
||||
VersatileThermostatAPI._hass.data.setdefault(DOMAIN, {})
|
||||
hass.data.setdefault(DOMAIN, {})
|
||||
|
||||
ret = VersatileThermostatAPI._hass.data.get(DOMAIN).get(VTHERM_API_NAME)
|
||||
if ret is None:
|
||||
ret = VersatileThermostatAPI()
|
||||
VersatileThermostatAPI._hass.data[DOMAIN][VTHERM_API_NAME] = ret
|
||||
hass.data[DOMAIN][VTHERM_API_NAME] = ret
|
||||
return ret
|
||||
|
||||
def __init__(self) -> None:
|
||||
@@ -47,6 +46,7 @@ class VersatileThermostatAPI(dict):
|
||||
super().__init__()
|
||||
self._expert_params = None
|
||||
self._short_ema_params = None
|
||||
self._central_config = None
|
||||
|
||||
def find_central_configuration(self):
|
||||
"""Search for a central configuration"""
|
||||
@@ -57,8 +57,8 @@ class VersatileThermostatAPI(dict):
|
||||
config_entry.data.get(CONF_THERMOSTAT_TYPE)
|
||||
== CONF_THERMOSTAT_CENTRAL_CONFIG
|
||||
):
|
||||
central_config = config_entry
|
||||
return central_config
|
||||
self._central_config = config_entry
|
||||
return self._central_config
|
||||
return None
|
||||
|
||||
def add_entry(self, entry: ConfigEntry):
|
||||
|
||||
@@ -336,14 +336,6 @@ class MagicMockClimate(MagicMock):
|
||||
def supported_features(self): # pylint: disable=missing-function-docstring
|
||||
return ClimateEntityFeature.TARGET_TEMPERATURE
|
||||
|
||||
@property
|
||||
def min_temp(self): # pylint: disable=missing-function-docstring
|
||||
return 15
|
||||
|
||||
@property
|
||||
def max_temp(self): # pylint: disable=missing-function-docstring
|
||||
return 19
|
||||
|
||||
|
||||
async def create_thermostat(
|
||||
hass: HomeAssistant, entry: MockConfigEntry, entity_id: str
|
||||
|
||||
@@ -139,11 +139,6 @@ MOCK_PRESETS_AC_CONFIG = {
|
||||
|
||||
MOCK_WINDOW_CONFIG = {
|
||||
CONF_WINDOW_SENSOR: "binary_sensor.window_sensor",
|
||||
# Not used normally only for tests to avoid rewrite all tests
|
||||
CONF_WINDOW_DELAY: 10,
|
||||
}
|
||||
|
||||
MOCK_WINDOW_DELAY_CONFIG = {
|
||||
CONF_WINDOW_DELAY: 10,
|
||||
}
|
||||
|
||||
|
||||
@@ -6,10 +6,6 @@ from datetime import datetime, timedelta
|
||||
|
||||
import logging
|
||||
|
||||
from homeassistant.components.climate import (
|
||||
SERVICE_SET_TEMPERATURE,
|
||||
)
|
||||
|
||||
from .commons import *
|
||||
|
||||
logging.getLogger().setLevel(logging.DEBUG)
|
||||
@@ -572,141 +568,3 @@ async def test_bug_101(
|
||||
)
|
||||
assert entity.target_temperature == 12.75
|
||||
assert entity.preset_mode is PRESET_NONE
|
||||
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_bug_272(
|
||||
hass: HomeAssistant,
|
||||
skip_hass_states_is_state,
|
||||
skip_turn_on_off_heater,
|
||||
skip_send_event,
|
||||
):
|
||||
"""Test that it not possible to set the target temperature under the min_temp setting"""
|
||||
|
||||
tz = get_tz(hass) # pylint: disable=invalid-name
|
||||
now: datetime = datetime.now(tz=tz)
|
||||
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
title="TheOverClimateMockName",
|
||||
unique_id="uniqueId",
|
||||
data=PARTIAL_CLIMATE_CONFIG, # 5 minutes security delay
|
||||
)
|
||||
|
||||
# Min_temp is 15 and max_temp is 19
|
||||
fake_underlying_climate = MagicMockClimate()
|
||||
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
|
||||
), patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
|
||||
return_value=fake_underlying_climate,
|
||||
), patch(
|
||||
"homeassistant.core.ServiceRegistry.async_call"
|
||||
) as mock_service_call:
|
||||
entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(entry.entry_id)
|
||||
assert entry.state is ConfigEntryState.LOADED
|
||||
|
||||
def find_my_entity(entity_id) -> ClimateEntity:
|
||||
"""Find my new entity"""
|
||||
component: EntityComponent[ClimateEntity] = hass.data[CLIMATE_DOMAIN]
|
||||
for entity in component.entities:
|
||||
if entity.entity_id == entity_id:
|
||||
return entity
|
||||
|
||||
entity = find_my_entity("climate.theoverclimatemockname")
|
||||
|
||||
assert entity
|
||||
|
||||
assert entity.name == "TheOverClimateMockName"
|
||||
assert entity.is_over_climate is True
|
||||
assert entity.hvac_mode is HVACMode.OFF
|
||||
assert entity.target_temperature == entity.min_temp
|
||||
assert entity.is_regulated is True
|
||||
|
||||
assert mock_service_call.call_count == 0
|
||||
|
||||
# Set the hvac_mode to HEAT
|
||||
await entity.async_set_hvac_mode(HVACMode.HEAT)
|
||||
|
||||
# In the accepted interval
|
||||
await entity.async_set_temperature(temperature=17)
|
||||
assert mock_service_call.call_count == 2
|
||||
mock_service_call.assert_has_calls(
|
||||
[
|
||||
call.async_call(
|
||||
"climate",
|
||||
SERVICE_SET_HVAC_MODE,
|
||||
{"entity_id": "climate.mock_climate", "hvac_mode": HVACMode.HEAT},
|
||||
),
|
||||
call.async_call(
|
||||
"climate",
|
||||
SERVICE_SET_TEMPERATURE,
|
||||
{
|
||||
"entity_id": "climate.mock_climate",
|
||||
"temperature": 17,
|
||||
"target_temp_high": 30,
|
||||
"target_temp_low": 15,
|
||||
},
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
tz = get_tz(hass) # pylint: disable=invalid-name
|
||||
now: datetime = datetime.now(tz=tz)
|
||||
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
|
||||
), patch("homeassistant.core.ServiceRegistry.async_call") as mock_service_call:
|
||||
# Set room temperature to something very cold
|
||||
event_timestamp = now + timedelta(minutes=1)
|
||||
|
||||
await send_temperature_change_event(entity, 13, event_timestamp)
|
||||
await send_ext_temperature_change_event(entity, 9, event_timestamp)
|
||||
|
||||
# In the accepted interval
|
||||
await entity.async_set_temperature(temperature=10)
|
||||
assert mock_service_call.call_count == 1
|
||||
mock_service_call.assert_has_calls(
|
||||
[
|
||||
call.async_call(
|
||||
"climate",
|
||||
SERVICE_SET_TEMPERATURE,
|
||||
{
|
||||
"entity_id": "climate.mock_climate",
|
||||
"temperature": 15, # the minimum acceptable
|
||||
"target_temp_high": 30,
|
||||
"target_temp_low": 15,
|
||||
},
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
|
||||
), patch("homeassistant.core.ServiceRegistry.async_call") as mock_service_call:
|
||||
# Set room temperature to something very cold
|
||||
event_timestamp = now + timedelta(minutes=1)
|
||||
|
||||
await send_temperature_change_event(entity, 13, event_timestamp)
|
||||
await send_ext_temperature_change_event(entity, 9, event_timestamp)
|
||||
|
||||
# In the accepted interval
|
||||
await entity.async_set_temperature(temperature=20)
|
||||
assert mock_service_call.call_count == 1
|
||||
mock_service_call.assert_has_calls(
|
||||
[
|
||||
call.async_call(
|
||||
"climate",
|
||||
SERVICE_SET_TEMPERATURE,
|
||||
{
|
||||
"entity_id": "climate.mock_climate",
|
||||
"temperature": 19, # the maximum acceptable
|
||||
"target_temp_high": 30,
|
||||
"target_temp_low": 15,
|
||||
},
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
@@ -31,8 +31,6 @@ from .commons import * # pylint: disable=wildcard-import, unused-wildcard-impor
|
||||
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_add_a_central_config(hass: HomeAssistant, skip_hass_states_is_state):
|
||||
"""Tests the clean_central_config_doubon of base_thermostat"""
|
||||
central_config_entry = MockConfigEntry(
|
||||
@@ -97,8 +95,6 @@ async def test_add_a_central_config(hass: HomeAssistant, skip_hass_states_is_sta
|
||||
assert central_configuration is not None
|
||||
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_minimal_over_switch_wo_central_config(
|
||||
hass: HomeAssistant, skip_hass_states_is_state, init_vtherm_api
|
||||
):
|
||||
@@ -173,8 +169,6 @@ async def test_minimal_over_switch_wo_central_config(
|
||||
assert entity.is_inversed
|
||||
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_full_over_switch_wo_central_config(
|
||||
hass: HomeAssistant, skip_hass_states_is_state, init_vtherm_api
|
||||
):
|
||||
@@ -287,8 +281,6 @@ async def test_full_over_switch_wo_central_config(
|
||||
assert entity._presence_sensor_entity_id == "binary_sensor.mock_presence_sensor"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_full_over_switch_with_central_config(
|
||||
hass: HomeAssistant, skip_hass_states_is_state, init_central_config
|
||||
):
|
||||
@@ -396,8 +388,6 @@ async def test_full_over_switch_with_central_config(
|
||||
assert entity._presence_sensor_entity_id == "binary_sensor.mock_presence_sensor"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_over_switch_with_central_config_but_no_central_config(
|
||||
hass: HomeAssistant, skip_hass_states_get, init_vtherm_api
|
||||
):
|
||||
|
||||
@@ -472,17 +472,15 @@ async def test_user_config_flow_window_auto_ko(
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input=MOCK_WINDOW_DELAY_CONFIG,
|
||||
user_input=MOCK_WINDOW_AUTO_CONFIG,
|
||||
)
|
||||
|
||||
# Since issue #280 we cannot have the error because we only display the
|
||||
# MOCK_WINDOW_DELAY_CONFIG form if we have a sensor configured
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
# We should stay on window with an error
|
||||
assert result["errors"] == {}
|
||||
# "window_sensor_entity_id": "window_open_detection_method"
|
||||
# }
|
||||
assert result["step_id"] == "advanced"
|
||||
assert result["errors"] == {
|
||||
"window_sensor_entity_id": "window_open_detection_method"
|
||||
}
|
||||
assert result["step_id"] == "window"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
from unittest.mock import patch, call
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from homeassistant.core import HomeAssistant, State
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.components.climate import HVACAction, HVACMode
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
|
||||
@@ -214,60 +214,20 @@ async def test_over_valve_full_start(
|
||||
assert entity.hvac_action == HVACAction.HEATING
|
||||
|
||||
# Change internal temperature
|
||||
expected_state = State(
|
||||
entity_id="number.mock_valve", state="0", attributes={"min": 10, "max": 50}
|
||||
)
|
||||
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
|
||||
) as mock_send_event, patch(
|
||||
"homeassistant.core.ServiceRegistry.async_call"
|
||||
) as mock_service_call, patch(
|
||||
"homeassistant.core.StateMachine.get", return_value=expected_state
|
||||
"homeassistant.core.StateMachine.get", return_value=0
|
||||
):
|
||||
event_timestamp = now - timedelta(minutes=3)
|
||||
await send_temperature_change_event(entity, 20, datetime.now())
|
||||
assert entity.valve_open_percent == 0
|
||||
assert entity.is_device_active is True # Should be 0 but in fact 10 is send
|
||||
assert (
|
||||
entity.hvac_action == HVACAction.HEATING
|
||||
) # Should be IDLE but heating due to 10
|
||||
|
||||
assert mock_service_call.call_count == 1
|
||||
# The VTherm valve is 0, but the underlying have received 10 which is the min
|
||||
mock_service_call.assert_has_calls(
|
||||
[
|
||||
call.async_call(
|
||||
"number",
|
||||
"set_value",
|
||||
{"entity_id": "number.mock_valve", "value": 10},
|
||||
)
|
||||
]
|
||||
)
|
||||
assert entity.is_device_active is False
|
||||
assert entity.hvac_action == HVACAction.IDLE
|
||||
|
||||
await send_temperature_change_event(entity, 17, datetime.now())
|
||||
assert mock_service_call.call_count == 2
|
||||
# The VTherm valve is 0, but the underlying have received 10 which is the min
|
||||
mock_service_call.assert_has_calls(
|
||||
[
|
||||
call.async_call(
|
||||
"number",
|
||||
"set_value",
|
||||
{
|
||||
"entity_id": "number.mock_valve",
|
||||
"value": 10,
|
||||
}, # the min allowed value
|
||||
),
|
||||
call.async_call(
|
||||
"number",
|
||||
"set_value",
|
||||
{
|
||||
"entity_id": "number.mock_valve",
|
||||
"value": 50,
|
||||
}, # the max allowed value
|
||||
),
|
||||
]
|
||||
)
|
||||
# switch to Eco
|
||||
await entity.async_set_preset_mode(PRESET_ECO)
|
||||
assert entity.preset_mode is PRESET_ECO
|
||||
|
||||
Reference in New Issue
Block a user