Compare commits

..

5 Commits

Author SHA1 Message Date
Jean-Marc Collin
a01f5770d9 FIX issue #272 and #24ç - min and max values depending of the underlying 2023-12-19 19:39:33 +00:00
Jean-Marc Collin
04d0b28f1d Issue #280 - enable to use central config for window configuration 2023-12-18 21:39:58 +00:00
Jean-Marc Collin
30c3418f1b Issue #281 - cannot use central config at first integration installation 2023-12-18 20:54:39 +00:00
Jean-Marc Collin
efb8ce257d Beers from @Mexx62, @Someone 2023-12-18 20:16:37 +00:00
Jean-Marc Collin
8f934a3298 Feature 239 creates central config panel (#276)
* Add central config into ConfigFlow

* Test manual of confif_flow ok

* Ignore central confic in instanciate entities

* Init data in base_thermostat ok

* With central configuration testu ok

* All testu ok

* With fixture for init_vtherm_api and init_central_config

* Add reload VTherms when central configuration is changed

* Update strings.json and replace security by safety in README.

* UPdate README with release 5.0

* FIX missing Presets central configuration initialisation

* FIX frost_away_temp translation missing

---------

Co-authored-by: Jean-Marc Collin <jean-marc.collin-extern@renault.com>
2023-12-17 16:16:44 +01:00
20 changed files with 436 additions and 129 deletions

View File

@@ -59,8 +59,8 @@ input_number:
unit_of_measurement: kW unit_of_measurement: kW
fake_valve1: fake_valve1:
name: The valve 1 name: The valve 1
min: 0 min: 10
max: 100 max: 90
icon: mdi:pipe-valve icon: mdi:pipe-valve
unit_of_measurement: percentage unit_of_measurement: percentage

View File

@@ -30,13 +30,8 @@
"waderyan.gitblame", "waderyan.gitblame",
"keesschollaart.vscode-home-assistant", "keesschollaart.vscode-home-assistant",
"vscode.markdown-math", "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": { "settings": {
"files.eol": "\n", "files.eol": "\n",
"editor.tabSize": 4, "editor.tabSize": 4,

View File

@@ -14,7 +14,8 @@
"python.testing.pytestEnabled": true, "python.testing.pytestEnabled": true,
"python.analysis.extraPaths": [ "python.analysis.extraPaths": [
// "/home/vscode/core", // "/home/vscode/core",
"/workspaces/versatile_thermostat/custom_components/versatile_thermostat" "/workspaces/versatile_thermostat/custom_components/versatile_thermostat",
"/home/vscode/.local/lib/python3.11/site-packages/homeassistant"
], ],
"python.formatting.provider": "none" "python.formatting.provider": "none"
} }

View File

@@ -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. **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) # 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 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 @Mexx62, @Someone pour les bières. Ca fait très plaisir et ça m'encourage à continuer !
# Quand l'utiliser et ne pas l'utiliser # Quand l'utiliser et ne pas l'utiliser

View File

@@ -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. **Note:** the VTherm configuration screenshots have not been updated.
# Thanks for the beer [buymecoffee](https://www.buymeacoffee.com/jmcollin78) # 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 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, @MattG, @Mexx62, @Someone for the beers. It's very nice and encourages me to continue!
# When to use / not use # When to use / not use
This thermostat can control 3 types of equipment: This thermostat can control 3 types of equipment:

View File

@@ -86,7 +86,10 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
# VTherm API should have been initialized before arriving here # VTherm API should have been initialized before arriving here
vtherm_api = VersatileThermostatAPI.get_vtherm_api() vtherm_api = VersatileThermostatAPI.get_vtherm_api()
self._central_config = vtherm_api.find_central_configuration() if vtherm_api is not None:
self._central_config = vtherm_api.find_central_configuration()
else:
self._central_config = None
self._init_feature_flags(infos) self._init_feature_flags(infos)
self._init_central_config_flags(infos) self._init_central_config_flags(infos)
@@ -119,6 +122,7 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
CONF_USE_WINDOW_CENTRAL_CONFIG, CONF_USE_WINDOW_CENTRAL_CONFIG,
CONF_USE_MOTION_CENTRAL_CONFIG, CONF_USE_MOTION_CENTRAL_CONFIG,
CONF_USE_POWER_CENTRAL_CONFIG, CONF_USE_POWER_CENTRAL_CONFIG,
CONF_USE_PRESETS_CENTRAL_CONFIG,
CONF_USE_PRESENCE_CENTRAL_CONFIG, CONF_USE_PRESENCE_CENTRAL_CONFIG,
CONF_USE_ADVANCED_CENTRAL_CONFIG, CONF_USE_ADVANCED_CENTRAL_CONFIG,
): ):
@@ -167,7 +171,7 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
_LOGGER.error( _LOGGER.error(
"Only one window detection method should be used. Use window_sensor or auto window open detection but not both" "Only one window detection method should be used. Use window_sensor or auto window open detection but not both"
) )
raise WindowOpenDetectionMethod(CONF_WINDOW_SENSOR) raise WindowOpenDetectionMethod(CONF_WINDOW_AUTO_OPEN_THRESHOLD)
# Check that is USE_CENTRAL config is used, that a central config exists # Check that is USE_CENTRAL config is used, that a central config exists
if self._central_config is None: if self._central_config is None:
@@ -404,7 +408,11 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
next_step = self.async_step_motion next_step = self.async_step_motion
# If comes from async_step_spec_window # If comes from async_step_spec_window
elif self._infos.get(COMES_FROM) == "async_step_spec_window": elif self._infos.get(COMES_FROM) == "async_step_spec_window":
schema = STEP_CENTRAL_WINDOW_DATA_SCHEMA # 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
elif user_input and user_input.get(CONF_USE_WINDOW_CENTRAL_CONFIG) is False: elif user_input and user_input.get(CONF_USE_WINDOW_CENTRAL_CONFIG) is False:
next_step = self.async_step_spec_window next_step = self.async_step_spec_window
@@ -419,6 +427,8 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
) )
schema = STEP_CENTRAL_WINDOW_DATA_SCHEMA 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" self._infos[COMES_FROM] = "async_step_spec_window"

View File

@@ -195,6 +195,12 @@ 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 STEP_MOTION_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name
{ {
vol.Optional(CONF_MOTION_SENSOR): selector.EntitySelector( vol.Optional(CONF_MOTION_SENSOR): selector.EntitySelector(

View File

@@ -413,6 +413,7 @@
"eco_away_temp": "Eco away preset", "eco_away_temp": "Eco away preset",
"comfort_away_temp": "Comfort away preset", "comfort_away_temp": "Comfort away preset",
"boost_away_temp": "Boost away preset", "boost_away_temp": "Boost away preset",
"frost_away_temp": "Frost protection preset",
"eco_ac_away_temp": "Eco away preset in AC mode", "eco_ac_away_temp": "Eco away preset in AC mode",
"comfort_ac_away_temp": "Comfort 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", "boost_ac_away_temp": "Boost away preset in AC mode",

View File

@@ -234,45 +234,45 @@ class ThermostatOverClimate(BaseThermostat):
await self.async_set_fan_mode(self._auto_deactivated_fan_mode) await self.async_set_fan_mode(self._auto_deactivated_fan_mode)
@overrides @overrides
def post_init(self, entry_infos): def post_init(self, config_entry):
"""Initialize the Thermostat""" """Initialize the Thermostat"""
super().post_init(entry_infos) super().post_init(config_entry)
for climate in [ for climate in [
CONF_CLIMATE, CONF_CLIMATE,
CONF_CLIMATE_2, CONF_CLIMATE_2,
CONF_CLIMATE_3, CONF_CLIMATE_3,
CONF_CLIMATE_4, CONF_CLIMATE_4,
]: ]:
if entry_infos.get(climate): if config_entry.get(climate):
self._underlyings.append( self._underlyings.append(
UnderlyingClimate( UnderlyingClimate(
hass=self._hass, hass=self._hass,
thermostat=self, thermostat=self,
climate_entity_id=entry_infos.get(climate), climate_entity_id=config_entry.get(climate),
) )
) )
self.choose_auto_regulation_mode( self.choose_auto_regulation_mode(
entry_infos.get(CONF_AUTO_REGULATION_MODE) config_entry.get(CONF_AUTO_REGULATION_MODE)
if entry_infos.get(CONF_AUTO_REGULATION_MODE) is not None if config_entry.get(CONF_AUTO_REGULATION_MODE) is not None
else CONF_AUTO_REGULATION_NONE else CONF_AUTO_REGULATION_NONE
) )
self._auto_regulation_dtemp = ( self._auto_regulation_dtemp = (
entry_infos.get(CONF_AUTO_REGULATION_DTEMP) config_entry.get(CONF_AUTO_REGULATION_DTEMP)
if entry_infos.get(CONF_AUTO_REGULATION_DTEMP) is not None if config_entry.get(CONF_AUTO_REGULATION_DTEMP) is not None
else 0.5 else 0.5
) )
self._auto_regulation_period_min = ( self._auto_regulation_period_min = (
entry_infos.get(CONF_AUTO_REGULATION_PERIOD_MIN) config_entry.get(CONF_AUTO_REGULATION_PERIOD_MIN)
if entry_infos.get(CONF_AUTO_REGULATION_PERIOD_MIN) is not None if config_entry.get(CONF_AUTO_REGULATION_PERIOD_MIN) is not None
else 5 else 5
) )
self._auto_fan_mode = ( self._auto_fan_mode = (
entry_infos.get(CONF_AUTO_FAN_MODE) config_entry.get(CONF_AUTO_FAN_MODE)
if entry_infos.get(CONF_AUTO_FAN_MODE) is not None if config_entry.get(CONF_AUTO_FAN_MODE) is not None
else CONF_AUTO_FAN_NONE else CONF_AUTO_FAN_NONE
) )

View File

@@ -48,9 +48,9 @@ class ThermostatOverSwitch(BaseThermostat):
) )
# useless for now # useless for now
# def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None: # def __init__(self, hass: HomeAssistant, unique_id, name, config_entry) -> None:
# """Initialize the thermostat over switch.""" # """Initialize the thermostat over switch."""
# super().__init__(hass, unique_id, name, entry_infos) # super().__init__(hass, unique_id, name, config_entry)
_is_inversed: bool = None _is_inversed: bool = None
@property @property
@@ -72,10 +72,10 @@ class ThermostatOverSwitch(BaseThermostat):
return None return None
@overrides @overrides
def post_init(self, entry_infos): def post_init(self, config_entry):
"""Initialize the Thermostat""" """Initialize the Thermostat"""
super().post_init(entry_infos) super().post_init(config_entry)
self._prop_algorithm = PropAlgorithm( self._prop_algorithm = PropAlgorithm(
self._proportional_function, self._proportional_function,
@@ -85,13 +85,13 @@ class ThermostatOverSwitch(BaseThermostat):
self._minimal_activation_delay, self._minimal_activation_delay,
) )
lst_switches = [entry_infos.get(CONF_HEATER)] lst_switches = [config_entry.get(CONF_HEATER)]
if entry_infos.get(CONF_HEATER_2): if config_entry.get(CONF_HEATER_2):
lst_switches.append(entry_infos.get(CONF_HEATER_2)) lst_switches.append(config_entry.get(CONF_HEATER_2))
if entry_infos.get(CONF_HEATER_3): if config_entry.get(CONF_HEATER_3):
lst_switches.append(entry_infos.get(CONF_HEATER_3)) lst_switches.append(config_entry.get(CONF_HEATER_3))
if entry_infos.get(CONF_HEATER_4): if config_entry.get(CONF_HEATER_4):
lst_switches.append(entry_infos.get(CONF_HEATER_4)) lst_switches.append(config_entry.get(CONF_HEATER_4))
delta_cycle = self._cycle_min * 60 / len(lst_switches) delta_cycle = self._cycle_min * 60 / len(lst_switches)
for idx, switch in enumerate(lst_switches): for idx, switch in enumerate(lst_switches):
@@ -104,7 +104,7 @@ class ThermostatOverSwitch(BaseThermostat):
) )
) )
self._is_inversed = entry_infos.get(CONF_INVERSE_SWITCH) is True self._is_inversed = config_entry.get(CONF_INVERSE_SWITCH) is True
self._should_relaunch_control_heating = False self._should_relaunch_control_heating = False
@overrides @overrides

View File

@@ -3,7 +3,10 @@
import logging import logging
from datetime import timedelta 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.core import callback
from homeassistant.components.climate import HVACMode from homeassistant.components.climate import HVACMode
@@ -16,39 +19,53 @@ from .underlyings import UnderlyingValve
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
class ThermostatOverValve(BaseThermostat): class ThermostatOverValve(BaseThermostat):
"""Representation of a class for a Versatile Thermostat over a Valve""" """Representation of a class for a Versatile Thermostat over a Valve"""
_entity_component_unrecorded_attributes = BaseThermostat._entity_component_unrecorded_attributes.union(frozenset( _entity_component_unrecorded_attributes = (
{ BaseThermostat._entity_component_unrecorded_attributes.union(
"is_over_valve", "underlying_valve_0", "underlying_valve_1", frozenset(
"underlying_valve_2", "underlying_valve_3", "on_time_sec", "off_time_sec", {
"cycle_min", "function", "tpi_coef_int", "tpi_coef_ext" "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 # Useless for now
# def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None: # def __init__(self, hass: HomeAssistant, unique_id, name, config_entry) -> None:
# """Initialize the thermostat over switch.""" # """Initialize the thermostat over switch."""
# super().__init__(hass, unique_id, name, entry_infos) # super().__init__(hass, unique_id, name, config_entry)
@property @property
def is_over_valve(self) -> bool: def is_over_valve(self) -> bool:
""" True if the Thermostat is over_valve""" """True if the Thermostat is over_valve"""
return True return True
@property @property
def valve_open_percent(self) -> int: def valve_open_percent(self) -> int:
""" Gives the percentage of valve needed""" """Gives the percentage of valve needed"""
if self._hvac_mode == HVACMode.OFF: if self._hvac_mode == HVACMode.OFF:
return 0 return 0
else: else:
return round(max(0, min(self.proportional_algorithm.on_percent, 1)) * 100) return round(max(0, min(self.proportional_algorithm.on_percent, 1)) * 100)
@overrides @overrides
def post_init(self, entry_infos): def post_init(self, config_entry):
""" Initialize the Thermostat""" """Initialize the Thermostat"""
super().post_init(entry_infos) super().post_init(config_entry)
self._prop_algorithm = PropAlgorithm( self._prop_algorithm = PropAlgorithm(
self._proportional_function, self._proportional_function,
self._tpi_coef_int, self._tpi_coef_int,
@@ -57,21 +74,17 @@ class ThermostatOverValve(BaseThermostat):
self._minimal_activation_delay, self._minimal_activation_delay,
) )
lst_valves = [entry_infos.get(CONF_VALVE)] lst_valves = [config_entry.get(CONF_VALVE)]
if entry_infos.get(CONF_VALVE_2): if config_entry.get(CONF_VALVE_2):
lst_valves.append(entry_infos.get(CONF_VALVE_2)) lst_valves.append(config_entry.get(CONF_VALVE_2))
if entry_infos.get(CONF_VALVE_3): if config_entry.get(CONF_VALVE_3):
lst_valves.append(entry_infos.get(CONF_VALVE_3)) lst_valves.append(config_entry.get(CONF_VALVE_3))
if entry_infos.get(CONF_VALVE_4): if config_entry.get(CONF_VALVE_4):
lst_valves.append(entry_infos.get(CONF_VALVE_4)) lst_valves.append(config_entry.get(CONF_VALVE_4))
for _, valve in enumerate(lst_valves): for _, valve in enumerate(lst_valves):
self._underlyings.append( self._underlyings.append(
UnderlyingValve( UnderlyingValve(hass=self._hass, thermostat=self, valve_entity_id=valve)
hass=self._hass,
thermostat=self,
valve_entity_id=valve
)
) )
self._should_relaunch_control_heating = False self._should_relaunch_control_heating = False
@@ -89,7 +102,7 @@ class ThermostatOverValve(BaseThermostat):
async_track_state_change_event( async_track_state_change_event(
self.hass, [valve.entity_id], self._async_valve_changed self.hass, [valve.entity_id], self._async_valve_changed
) )
) )
# Start the control_heating # Start the control_heating
# starts a cycle # starts a cycle
@@ -107,29 +120,34 @@ class ThermostatOverValve(BaseThermostat):
This method just log the change. It changes nothing to avoid loops. This method just log the change. It changes nothing to avoid loops.
""" """
new_state = event.data.get("new_state") 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 @overrides
def update_custom_attributes(self): def update_custom_attributes(self):
""" Custom attributes """ """Custom attributes"""
super().update_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["is_over_valve"] = self.is_over_valve
self._attr_extra_state_attributes["underlying_valve_0"] = ( self._attr_extra_state_attributes["underlying_valve_0"] = self._underlyings[
self._underlyings[0].entity_id) 0
].entity_id
self._attr_extra_state_attributes["underlying_valve_1"] = ( 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._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._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[ self._attr_extra_state_attributes[
"on_percent" "on_percent"
] = self._prop_algorithm.on_percent ] = self._prop_algorithm.on_percent
self._attr_extra_state_attributes[ self._attr_extra_state_attributes[
"on_time_sec" "on_time_sec"
] = self._prop_algorithm.on_time_sec ] = self._prop_algorithm.on_time_sec
@@ -162,9 +180,7 @@ class ThermostatOverValve(BaseThermostat):
) )
for under in self._underlyings: 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.update_custom_attributes()
self.async_write_ha_state() self.async_write_ha_state()
@@ -185,4 +201,4 @@ class ThermostatOverValve(BaseThermostat):
self, self,
added_energy, added_energy,
self._total_energy, self._total_energy,
) )

View File

@@ -413,6 +413,7 @@
"eco_away_temp": "Eco away preset", "eco_away_temp": "Eco away preset",
"comfort_away_temp": "Comfort away preset", "comfort_away_temp": "Comfort away preset",
"boost_away_temp": "Boost away preset", "boost_away_temp": "Boost away preset",
"frost_away_temp": "Frost protection preset",
"eco_ac_away_temp": "Eco away preset in AC mode", "eco_ac_away_temp": "Eco away preset in AC mode",
"comfort_ac_away_temp": "Comfort 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", "boost_ac_away_temp": "Boost away preset in AC mode",

View File

@@ -6,6 +6,7 @@ from typing import Any
from enum import StrEnum from enum import StrEnum
from homeassistant.const import ATTR_ENTITY_ID, STATE_ON, UnitOfTemperature from homeassistant.const import ATTR_ENTITY_ID, STATE_ON, UnitOfTemperature
from homeassistant.core import State
from homeassistant.exceptions import ServiceNotFound from homeassistant.exceptions import ServiceNotFound
@@ -111,18 +112,18 @@ class UnderlyingEntity:
# This should be the correct way to handle turn_off and turn_on but this breaks the unit test # 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 # will an not understandable error: TypeError: object MagicMock can't be used in 'await' expression
async def turn_off(self): async def turn_off(self):
""" Turn off the underlying equipement. """Turn off the underlying equipement.
Need to be overriden""" Need to be overriden"""
return NotImplementedError return NotImplementedError
async def turn_on(self): async def turn_on(self):
""" Turn off the underlying equipement. """Turn off the underlying equipement.
Need to be overriden""" Need to be overriden"""
return NotImplementedError return NotImplementedError
@property @property
def is_inversed(self): def is_inversed(self):
""" Tells if the switch command should be inversed""" """Tells if the switch command should be inversed"""
return False return False
def remove_entity(self): def remove_entity(self):
@@ -164,7 +165,11 @@ class UnderlyingEntity:
"""Starting cycle for switch""" """Starting cycle for switch"""
def _cancel_cycle(self): def _cancel_cycle(self):
""" Stops an eventual cycle """ """Stops an eventual cycle"""
def cap_sent_value(self, value) -> float:
"""capping of the value send to the underlying eqt"""
return value
class UnderlyingSwitch(UnderlyingEntity): class UnderlyingSwitch(UnderlyingEntity):
@@ -205,7 +210,7 @@ class UnderlyingSwitch(UnderlyingEntity):
@overrides @overrides
@property @property
def is_inversed(self): 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 return self._thermostat.is_inversed
# @overrides this breaks some unit tests TypeError: object MagicMock can't be used in 'await' expression # @overrides this breaks some unit tests TypeError: object MagicMock can't be used in 'await' expression
@@ -227,14 +232,16 @@ class UnderlyingSwitch(UnderlyingEntity):
def is_device_active(self): def is_device_active(self):
"""If the toggleable device is currently active.""" """If the toggleable device is currently active."""
real_state = self._hass.states.is_state(self._entity_id, STATE_ON) 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 # @overrides this breaks some unit tests TypeError: object MagicMock can't be used in 'await' expression
async def turn_off(self): async def turn_off(self):
"""Turn heater toggleable device off.""" """Turn heater toggleable device off."""
_LOGGER.debug("%s - Stopping underlying entity %s", self, self._entity_id) _LOGGER.debug("%s - Stopping underlying entity %s", self, self._entity_id)
command = SERVICE_TURN_OFF if not self.is_inversed else SERVICE_TURN_ON 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 # This may fails if called after shutdown
try: try:
data = {ATTR_ENTITY_ID: self._entity_id} data = {ATTR_ENTITY_ID: self._entity_id}
@@ -250,7 +257,7 @@ class UnderlyingSwitch(UnderlyingEntity):
"""Turn heater toggleable device on.""" """Turn heater toggleable device on."""
_LOGGER.debug("%s - Starting underlying entity %s", self, self._entity_id) _LOGGER.debug("%s - Starting underlying entity %s", self, self._entity_id)
command = SERVICE_TURN_ON if not self.is_inversed else SERVICE_TURN_OFF 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: try:
data = {ATTR_ENTITY_ID: self._entity_id} data = {ATTR_ENTITY_ID: self._entity_id}
await self._hass.services.async_call( await self._hass.services.async_call(
@@ -261,7 +268,6 @@ class UnderlyingSwitch(UnderlyingEntity):
except ServiceNotFound as err: except ServiceNotFound as err:
_LOGGER.error(err) _LOGGER.error(err)
@overrides @overrides
async def start_cycle( async def start_cycle(
self, self,
@@ -490,10 +496,14 @@ class UnderlyingClimate(UnderlyingEntity):
def is_device_active(self): def is_device_active(self):
"""If the toggleable device is currently active.""" """If the toggleable device is currently active."""
if self.is_initialized: if self.is_initialized:
return self._underlying_climate.hvac_mode != HVACMode.OFF and self._underlying_climate.hvac_action not in [ return (
HVACAction.IDLE, self._underlying_climate.hvac_mode != HVACMode.OFF
HVACAction.OFF, and self._underlying_climate.hvac_action
] not in [
HVACAction.IDLE,
HVACAction.OFF,
]
)
else: else:
return None return None
@@ -550,7 +560,7 @@ class UnderlyingClimate(UnderlyingEntity):
return return
data = { data = {
ATTR_ENTITY_ID: self._entity_id, ATTR_ENTITY_ID: self._entity_id,
"temperature": temperature, "temperature": self.cap_sent_value(temperature),
"target_temp_high": max_temp, "target_temp_high": max_temp,
"target_temp_low": min_temp, "target_temp_low": min_temp,
} }
@@ -664,6 +674,40 @@ class UnderlyingClimate(UnderlyingEntity):
return None return None
return self._underlying_climate.turn_aux_heat_off() 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): class UnderlyingValve(UnderlyingEntity):
"""Represent a underlying switch""" """Represent a underlying switch"""
@@ -672,10 +716,7 @@ class UnderlyingValve(UnderlyingEntity):
_percent_open: int _percent_open: int
def __init__( def __init__(
self, self, hass: HomeAssistant, thermostat: Any, valve_entity_id: str
hass: HomeAssistant,
thermostat: Any,
valve_entity_id: str
) -> None: ) -> None:
"""Initialize the underlying switch""" """Initialize the underlying switch"""
@@ -689,13 +730,14 @@ class UnderlyingValve(UnderlyingEntity):
self._should_relaunch_control_heating = False self._should_relaunch_control_heating = False
self._hvac_mode = None self._hvac_mode = None
self._percent_open = self._thermostat.valve_open_percent self._percent_open = self._thermostat.valve_open_percent
self._valve_entity_id = valve_entity_id
async def send_percent_open(self): 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 # This may fails if called after shutdown
try: try:
data = { ATTR_ENTITY_ID: self._entity_id, "value": self._percent_open } data = {ATTR_ENTITY_ID: self._entity_id, "value": self._percent_open}
domain = self._entity_id.split('.')[0] domain = self._entity_id.split(".")[0]
await self._hass.services.async_call( await self._hass.services.async_call(
domain, domain,
SERVICE_SET_VALUE, SERVICE_SET_VALUE,
@@ -734,7 +776,7 @@ class UnderlyingValve(UnderlyingEntity):
# To test if real device is open but this is causing some side effect # To test if real device is open but this is causing some side effect
# because the activation can be deferred - # because the activation can be deferred -
# or float(self._hass.states.get(self._entity_id).state) > 0 # 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 return False
@overrides @overrides
@@ -750,9 +792,40 @@ class UnderlyingValve(UnderlyingEntity):
if force: if force:
await self.send_percent_open() await self.send_percent_open()
def set_valve_open_percent(self, percent): @overrides
""" Update the valve open percent """ def cap_sent_value(self, value) -> float:
caped_val = self._thermostat.valve_open_percent """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)
if self._percent_open == caped_val: if self._percent_open == caped_val:
# No changes # No changes
return return
@@ -760,7 +833,9 @@ class UnderlyingValve(UnderlyingEntity):
self._percent_open = caped_val self._percent_open = caped_val
# Send the new command to valve via a service call # 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 # Send the change to the valve, in background
self._hass.create_task(self.send_percent_open()) self._hass.create_task(self.send_percent_open())

View File

@@ -19,26 +19,27 @@ _LOGGER = logging.getLogger(__name__)
class VersatileThermostatAPI(dict): class VersatileThermostatAPI(dict):
"""The VersatileThermostatAPI""" """The VersatileThermostatAPI"""
_hass: HomeAssistant _hass: HomeAssistant = None
# _entries: Dict(str, ConfigEntry) # _entries: Dict(str, ConfigEntry)
@classmethod @classmethod
def get_vtherm_api(cls, hass=None): def get_vtherm_api(cls, hass=None):
"""Get the eventual VTherm API class instance""" """Get the eventual VTherm API class instance or
instantiate it if it doesn't exists"""
if hass is not None: if hass is not None:
VersatileThermostatAPI._hass = hass VersatileThermostatAPI._hass = hass
else:
if VersatileThermostatAPI._hass is None: if VersatileThermostatAPI._hass is None:
return None return None
domain = VersatileThermostatAPI._hass.data.get(DOMAIN) domain = VersatileThermostatAPI._hass.data.get(DOMAIN)
if not domain: if not domain:
hass.data.setdefault(DOMAIN, {}) VersatileThermostatAPI._hass.data.setdefault(DOMAIN, {})
ret = VersatileThermostatAPI._hass.data.get(DOMAIN).get(VTHERM_API_NAME) ret = VersatileThermostatAPI._hass.data.get(DOMAIN).get(VTHERM_API_NAME)
if ret is None: if ret is None:
ret = VersatileThermostatAPI() ret = VersatileThermostatAPI()
hass.data[DOMAIN][VTHERM_API_NAME] = ret VersatileThermostatAPI._hass.data[DOMAIN][VTHERM_API_NAME] = ret
return ret return ret
def __init__(self) -> None: def __init__(self) -> None:
@@ -46,7 +47,6 @@ class VersatileThermostatAPI(dict):
super().__init__() super().__init__()
self._expert_params = None self._expert_params = None
self._short_ema_params = None self._short_ema_params = None
self._central_config = None
def find_central_configuration(self): def find_central_configuration(self):
"""Search for a central configuration""" """Search for a central configuration"""
@@ -57,8 +57,8 @@ class VersatileThermostatAPI(dict):
config_entry.data.get(CONF_THERMOSTAT_TYPE) config_entry.data.get(CONF_THERMOSTAT_TYPE)
== CONF_THERMOSTAT_CENTRAL_CONFIG == CONF_THERMOSTAT_CENTRAL_CONFIG
): ):
self._central_config = config_entry central_config = config_entry
return self._central_config return central_config
return None return None
def add_entry(self, entry: ConfigEntry): def add_entry(self, entry: ConfigEntry):

View File

@@ -336,6 +336,14 @@ class MagicMockClimate(MagicMock):
def supported_features(self): # pylint: disable=missing-function-docstring def supported_features(self): # pylint: disable=missing-function-docstring
return ClimateEntityFeature.TARGET_TEMPERATURE 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( async def create_thermostat(
hass: HomeAssistant, entry: MockConfigEntry, entity_id: str hass: HomeAssistant, entry: MockConfigEntry, entity_id: str

View File

@@ -139,6 +139,11 @@ MOCK_PRESETS_AC_CONFIG = {
MOCK_WINDOW_CONFIG = { MOCK_WINDOW_CONFIG = {
CONF_WINDOW_SENSOR: "binary_sensor.window_sensor", 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, CONF_WINDOW_DELAY: 10,
} }

View File

@@ -6,6 +6,10 @@ from datetime import datetime, timedelta
import logging import logging
from homeassistant.components.climate import (
SERVICE_SET_TEMPERATURE,
)
from .commons import * from .commons import *
logging.getLogger().setLevel(logging.DEBUG) logging.getLogger().setLevel(logging.DEBUG)
@@ -568,3 +572,136 @@ async def test_bug_101(
) )
assert entity.target_temperature == 12.75 assert entity.target_temperature == 12.75
assert entity.preset_mode is PRESET_NONE 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
entity.async_set_hvac_mode(HVACMode.HEAT)
# In the accepted interval
await entity.async_set_temperature(temperature=17)
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": 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,
},
),
]
)

View File

@@ -31,6 +31,8 @@ from .commons import * # pylint: disable=wildcard-import, unused-wildcard-impor
from .const 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_add_a_central_config(hass: HomeAssistant, skip_hass_states_is_state): async def test_add_a_central_config(hass: HomeAssistant, skip_hass_states_is_state):
"""Tests the clean_central_config_doubon of base_thermostat""" """Tests the clean_central_config_doubon of base_thermostat"""
central_config_entry = MockConfigEntry( central_config_entry = MockConfigEntry(
@@ -95,6 +97,8 @@ async def test_add_a_central_config(hass: HomeAssistant, skip_hass_states_is_sta
assert central_configuration is not None 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( async def test_minimal_over_switch_wo_central_config(
hass: HomeAssistant, skip_hass_states_is_state, init_vtherm_api hass: HomeAssistant, skip_hass_states_is_state, init_vtherm_api
): ):
@@ -169,6 +173,8 @@ async def test_minimal_over_switch_wo_central_config(
assert entity.is_inversed 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( async def test_full_over_switch_wo_central_config(
hass: HomeAssistant, skip_hass_states_is_state, init_vtherm_api hass: HomeAssistant, skip_hass_states_is_state, init_vtherm_api
): ):
@@ -281,6 +287,8 @@ async def test_full_over_switch_wo_central_config(
assert entity._presence_sensor_entity_id == "binary_sensor.mock_presence_sensor" 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( async def test_full_over_switch_with_central_config(
hass: HomeAssistant, skip_hass_states_is_state, init_central_config hass: HomeAssistant, skip_hass_states_is_state, init_central_config
): ):
@@ -388,6 +396,8 @@ async def test_full_over_switch_with_central_config(
assert entity._presence_sensor_entity_id == "binary_sensor.mock_presence_sensor" 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( async def test_over_switch_with_central_config_but_no_central_config(
hass: HomeAssistant, skip_hass_states_get, init_vtherm_api hass: HomeAssistant, skip_hass_states_get, init_vtherm_api
): ):

View File

@@ -472,15 +472,17 @@ async def test_user_config_flow_window_auto_ko(
result = await hass.config_entries.flow.async_configure( result = await hass.config_entries.flow.async_configure(
result["flow_id"], result["flow_id"],
user_input=MOCK_WINDOW_AUTO_CONFIG, user_input=MOCK_WINDOW_DELAY_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 assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
# We should stay on window with an error # We should stay on window with an error
assert result["errors"] == { assert result["errors"] == {}
"window_sensor_entity_id": "window_open_detection_method" # "window_sensor_entity_id": "window_open_detection_method"
} # }
assert result["step_id"] == "window" assert result["step_id"] == "advanced"
@pytest.mark.parametrize("expected_lingering_tasks", [True]) @pytest.mark.parametrize("expected_lingering_tasks", [True])

View File

@@ -4,7 +4,7 @@
from unittest.mock import patch, call from unittest.mock import patch, call
from datetime import datetime, timedelta from datetime import datetime, timedelta
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant, State
from homeassistant.components.climate import HVACAction, HVACMode from homeassistant.components.climate import HVACAction, HVACMode
from homeassistant.config_entries import ConfigEntryState from homeassistant.config_entries import ConfigEntryState
@@ -214,20 +214,60 @@ async def test_over_valve_full_start(
assert entity.hvac_action == HVACAction.HEATING assert entity.hvac_action == HVACAction.HEATING
# Change internal temperature # Change internal temperature
expected_state = State(
entity_id="number.mock_valve", state="0", attributes={"min": 10, "max": 50}
)
with patch( with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event" "custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event, patch( ) as mock_send_event, patch(
"homeassistant.core.ServiceRegistry.async_call" "homeassistant.core.ServiceRegistry.async_call"
) as mock_service_call, patch( ) as mock_service_call, patch(
"homeassistant.core.StateMachine.get", return_value=0 "homeassistant.core.StateMachine.get", return_value=expected_state
): ):
event_timestamp = now - timedelta(minutes=3) event_timestamp = now - timedelta(minutes=3)
await send_temperature_change_event(entity, 20, datetime.now()) await send_temperature_change_event(entity, 20, datetime.now())
assert entity.valve_open_percent == 0 assert entity.valve_open_percent == 0
assert entity.is_device_active is False assert entity.is_device_active is True # Should be 0 but in fact 10 is send
assert entity.hvac_action == HVACAction.IDLE 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},
)
]
)
await send_temperature_change_event(entity, 17, datetime.now()) 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 # switch to Eco
await entity.async_set_preset_mode(PRESET_ECO) await entity.async_set_preset_mode(PRESET_ECO)
assert entity.preset_mode is PRESET_ECO assert entity.preset_mode is PRESET_ECO