Compare commits

..

10 Commits

Author SHA1 Message Date
Jean-Marc Collin
8961a80086 UPdate README with release 5.0 2023-12-17 10:54:07 +00:00
Jean-Marc Collin
2f6c46ef11 Update strings.json and replace security by safety in README. 2023-12-17 10:03:57 +00:00
Jean-Marc Collin
af817e74b7 Add reload VTherms when central configuration is changed 2023-12-17 09:41:22 +00:00
Jean-Marc Collin
46307286b1 With fixture for init_vtherm_api and init_central_config 2023-12-17 08:57:08 +00:00
Jean-Marc Collin
c8398a48fe All testu ok 2023-12-17 08:16:37 +00:00
Jean-Marc Collin
5063ba3802 With central configuration testu ok 2023-12-17 06:57:06 +00:00
Jean-Marc Collin
fb76a84bde Init data in base_thermostat ok 2023-12-16 15:24:11 +00:00
Jean-Marc Collin
03f6045d34 Ignore central confic in instanciate entities 2023-12-15 22:58:36 +00:00
Jean-Marc Collin
aeb4b2fbbe Test manual of confif_flow ok 2023-12-15 22:37:40 +00:00
Jean-Marc Collin
47f5aa9595 Add central config into ConfigFlow 2023-12-15 18:37:25 +00:00
20 changed files with 130 additions and 447 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: 10 min: 0
max: 90 max: 100
icon: mdi:pipe-valve icon: mdi:pipe-valve
unit_of_measurement: percentage unit_of_measurement: percentage

View File

@@ -30,8 +30,13 @@
"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,8 +14,7 @@
"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 @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 # 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, @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 # When to use / not use
This thermostat can control 3 types of equipment: This thermostat can control 3 types of equipment:

View File

@@ -86,10 +86,7 @@ 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()
if vtherm_api is not None: self._central_config = vtherm_api.find_central_configuration()
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)
@@ -122,7 +119,6 @@ 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,
): ):
@@ -171,7 +167,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_AUTO_OPEN_THRESHOLD) raise WindowOpenDetectionMethod(CONF_WINDOW_SENSOR)
# 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:
@@ -364,7 +360,7 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
schema = schema_ac_or_not schema = schema_ac_or_not
elif user_input and user_input.get(CONF_USE_PRESETS_CENTRAL_CONFIG) is False: elif user_input and user_input.get(CONF_USE_PRESETS_CENTRAL_CONFIG) is False:
next_step = self.async_step_spec_presets 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) return await self.generic_step("presets", schema, user_input, next_step)
@@ -408,11 +404,7 @@ 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":
# If we have a window sensor don't display the auto window parameters schema = STEP_CENTRAL_WINDOW_DATA_SCHEMA
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
@@ -427,8 +419,6 @@ 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,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 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,7 +413,6 @@
"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

@@ -136,11 +136,6 @@ class ThermostatOverClimate(BaseThermostat):
async def _send_regulated_temperature(self, force=False): async def _send_regulated_temperature(self, force=False):
"""Sends the regulated temperature to all underlying""" """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( _LOGGER.info(
"%s - Calling ThermostatClimate._send_regulated_temperature force=%s", "%s - Calling ThermostatClimate._send_regulated_temperature force=%s",
self, self,
@@ -239,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, config_entry): def post_init(self, entry_infos):
"""Initialize the Thermostat""" """Initialize the Thermostat"""
super().post_init(config_entry) super().post_init(entry_infos)
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 config_entry.get(climate): if entry_infos.get(climate):
self._underlyings.append( self._underlyings.append(
UnderlyingClimate( UnderlyingClimate(
hass=self._hass, hass=self._hass,
thermostat=self, thermostat=self,
climate_entity_id=config_entry.get(climate), climate_entity_id=entry_infos.get(climate),
) )
) )
self.choose_auto_regulation_mode( self.choose_auto_regulation_mode(
config_entry.get(CONF_AUTO_REGULATION_MODE) entry_infos.get(CONF_AUTO_REGULATION_MODE)
if config_entry.get(CONF_AUTO_REGULATION_MODE) is not None if entry_infos.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 = (
config_entry.get(CONF_AUTO_REGULATION_DTEMP) entry_infos.get(CONF_AUTO_REGULATION_DTEMP)
if config_entry.get(CONF_AUTO_REGULATION_DTEMP) is not None if entry_infos.get(CONF_AUTO_REGULATION_DTEMP) is not None
else 0.5 else 0.5
) )
self._auto_regulation_period_min = ( self._auto_regulation_period_min = (
config_entry.get(CONF_AUTO_REGULATION_PERIOD_MIN) entry_infos.get(CONF_AUTO_REGULATION_PERIOD_MIN)
if config_entry.get(CONF_AUTO_REGULATION_PERIOD_MIN) is not None if entry_infos.get(CONF_AUTO_REGULATION_PERIOD_MIN) is not None
else 5 else 5
) )
self._auto_fan_mode = ( self._auto_fan_mode = (
config_entry.get(CONF_AUTO_FAN_MODE) entry_infos.get(CONF_AUTO_FAN_MODE)
if config_entry.get(CONF_AUTO_FAN_MODE) is not None if entry_infos.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, config_entry) -> None: # def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None:
# """Initialize the thermostat over switch.""" # """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 _is_inversed: bool = None
@property @property
@@ -72,10 +72,10 @@ class ThermostatOverSwitch(BaseThermostat):
return None return None
@overrides @overrides
def post_init(self, config_entry): def post_init(self, entry_infos):
"""Initialize the Thermostat""" """Initialize the Thermostat"""
super().post_init(config_entry) super().post_init(entry_infos)
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 = [config_entry.get(CONF_HEATER)] lst_switches = [entry_infos.get(CONF_HEATER)]
if config_entry.get(CONF_HEATER_2): if entry_infos.get(CONF_HEATER_2):
lst_switches.append(config_entry.get(CONF_HEATER_2)) lst_switches.append(entry_infos.get(CONF_HEATER_2))
if config_entry.get(CONF_HEATER_3): if entry_infos.get(CONF_HEATER_3):
lst_switches.append(config_entry.get(CONF_HEATER_3)) lst_switches.append(entry_infos.get(CONF_HEATER_3))
if config_entry.get(CONF_HEATER_4): if entry_infos.get(CONF_HEATER_4):
lst_switches.append(config_entry.get(CONF_HEATER_4)) lst_switches.append(entry_infos.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 = 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 self._should_relaunch_control_heating = False
@overrides @overrides

View File

@@ -3,10 +3,7 @@
import logging import logging
from datetime import timedelta from datetime import timedelta
from homeassistant.helpers.event import ( from homeassistant.helpers.event import async_track_state_change_event, async_track_time_interval
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
@@ -19,53 +16,39 @@ 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 = ( _entity_component_unrecorded_attributes = BaseThermostat._entity_component_unrecorded_attributes.union(frozenset(
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",
"is_over_valve", "cycle_min", "function", "tpi_coef_int", "tpi_coef_ext"
"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, config_entry) -> None: # def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None:
# """Initialize the thermostat over switch.""" # """Initialize the thermostat over switch."""
# super().__init__(hass, unique_id, name, config_entry) # super().__init__(hass, unique_id, name, entry_infos)
@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, config_entry): def post_init(self, entry_infos):
"""Initialize the Thermostat""" """ Initialize the Thermostat"""
super().post_init(config_entry) super().post_init(entry_infos)
self._prop_algorithm = PropAlgorithm( self._prop_algorithm = PropAlgorithm(
self._proportional_function, self._proportional_function,
self._tpi_coef_int, self._tpi_coef_int,
@@ -74,17 +57,21 @@ class ThermostatOverValve(BaseThermostat):
self._minimal_activation_delay, self._minimal_activation_delay,
) )
lst_valves = [config_entry.get(CONF_VALVE)] lst_valves = [entry_infos.get(CONF_VALVE)]
if config_entry.get(CONF_VALVE_2): if entry_infos.get(CONF_VALVE_2):
lst_valves.append(config_entry.get(CONF_VALVE_2)) lst_valves.append(entry_infos.get(CONF_VALVE_2))
if config_entry.get(CONF_VALVE_3): if entry_infos.get(CONF_VALVE_3):
lst_valves.append(config_entry.get(CONF_VALVE_3)) lst_valves.append(entry_infos.get(CONF_VALVE_3))
if config_entry.get(CONF_VALVE_4): if entry_infos.get(CONF_VALVE_4):
lst_valves.append(config_entry.get(CONF_VALVE_4)) lst_valves.append(entry_infos.get(CONF_VALVE_4))
for _, valve in enumerate(lst_valves): for _, valve in enumerate(lst_valves):
self._underlyings.append( 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 self._should_relaunch_control_heating = False
@@ -102,7 +89,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
@@ -120,34 +107,29 @@ 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( _LOGGER.debug("%s - _async_valve_changed new_state is %s", self, new_state.state)
"%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[ self._attr_extra_state_attributes["valve_open_percent"] = self.valve_open_percent
"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._underlyings[ self._attr_extra_state_attributes["underlying_valve_0"] = (
0 self._underlyings[0].entity_id)
].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
@@ -180,7 +162,9 @@ 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()

View File

@@ -413,7 +413,6 @@
"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,7 +6,6 @@ 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
@@ -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 # 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):
@@ -165,11 +164,7 @@ 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):
@@ -210,7 +205,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
@@ -232,16 +227,14 @@ 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 ( return (self.is_inversed and not real_state) or (not self.is_inversed and real_state)
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}
@@ -257,7 +250,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(
@@ -268,6 +261,7 @@ 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,
@@ -496,14 +490,10 @@ 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 ( return self._underlying_climate.hvac_mode != HVACMode.OFF and self._underlying_climate.hvac_action not in [
self._underlying_climate.hvac_mode != HVACMode.OFF HVACAction.IDLE,
and self._underlying_climate.hvac_action HVACAction.OFF,
not in [ ]
HVACAction.IDLE,
HVACAction.OFF,
]
)
else: else:
return None return None
@@ -560,7 +550,7 @@ class UnderlyingClimate(UnderlyingEntity):
return return
data = { data = {
ATTR_ENTITY_ID: self._entity_id, ATTR_ENTITY_ID: self._entity_id,
"temperature": self.cap_sent_value(temperature), "temperature": temperature,
"target_temp_high": max_temp, "target_temp_high": max_temp,
"target_temp_low": min_temp, "target_temp_low": min_temp,
} }
@@ -674,40 +664,6 @@ 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"""
@@ -716,7 +672,10 @@ class UnderlyingValve(UnderlyingEntity):
_percent_open: int _percent_open: int
def __init__( def __init__(
self, hass: HomeAssistant, thermostat: Any, valve_entity_id: str self,
hass: HomeAssistant,
thermostat: Any,
valve_entity_id: str
) -> None: ) -> None:
"""Initialize the underlying switch""" """Initialize the underlying switch"""
@@ -730,14 +689,13 @@ 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,
@@ -776,7 +734,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
@@ -792,40 +750,9 @@ class UnderlyingValve(UnderlyingEntity):
if force: if force:
await self.send_percent_open() await self.send_percent_open()
@overrides def set_valve_open_percent(self, percent):
def cap_sent_value(self, value) -> float: """ Update the valve open percent """
"""Try to adapt the open_percent value to the min / max found caped_val = self._thermostat.valve_open_percent
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
@@ -833,9 +760,7 @@ 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( _LOGGER.info("%s - Setting valve ouverture percent to %s", self, self._percent_open)
"%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,27 +19,26 @@ _LOGGER = logging.getLogger(__name__)
class VersatileThermostatAPI(dict): class VersatileThermostatAPI(dict):
"""The VersatileThermostatAPI""" """The VersatileThermostatAPI"""
_hass: HomeAssistant = None _hass: HomeAssistant
# _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 or """Get the eventual VTherm API class instance"""
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:
VersatileThermostatAPI._hass.data.setdefault(DOMAIN, {}) 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()
VersatileThermostatAPI._hass.data[DOMAIN][VTHERM_API_NAME] = ret hass.data[DOMAIN][VTHERM_API_NAME] = ret
return ret return ret
def __init__(self) -> None: def __init__(self) -> None:
@@ -47,6 +46,7 @@ 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
): ):
central_config = config_entry self._central_config = config_entry
return central_config return self._central_config
return None return None
def add_entry(self, entry: ConfigEntry): def add_entry(self, entry: ConfigEntry):

View File

@@ -336,14 +336,6 @@ 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,11 +139,6 @@ 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,10 +6,6 @@ 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)
@@ -572,141 +568,3 @@ 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
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,
},
),
]
)

View File

@@ -31,8 +31,6 @@ 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(
@@ -97,8 +95,6 @@ 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
): ):
@@ -173,8 +169,6 @@ 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
): ):
@@ -287,8 +281,6 @@ 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
): ):
@@ -396,8 +388,6 @@ 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,17 +472,15 @@ 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_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 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"] == "advanced" assert result["step_id"] == "window"
@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, State from homeassistant.core import HomeAssistant
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,60 +214,20 @@ 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=expected_state "homeassistant.core.StateMachine.get", return_value=0
): ):
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 True # Should be 0 but in fact 10 is send assert entity.is_device_active is False
assert ( assert entity.hvac_action == HVACAction.IDLE
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