From 083a3a4c81b7de0d4e2839ee494a34342008e04f Mon Sep 17 00:00:00 2001 From: Jean-Marc Collin Date: Sat, 21 Dec 2024 19:29:35 +0100 Subject: [PATCH] ok + testu ok (#729) Co-authored-by: Jean-Marc Collin --- .../versatile_thermostat/config_flow.py | 17 ++ .../versatile_thermostat/config_schema.py | 1 + .../versatile_thermostat/const.py | 5 + .../versatile_thermostat/strings.json | 15 +- .../thermostat_climate_valve.py | 19 ++ .../versatile_thermostat/translations/en.json | 12 +- .../versatile_thermostat/translations/fr.json | 15 +- .../versatile_thermostat/underlyings.py | 10 + tests/test_config_flow.py | 3 + tests/test_overclimate_valve.py | 183 ++++++++++++++++++ 10 files changed, 266 insertions(+), 14 deletions(-) diff --git a/custom_components/versatile_thermostat/config_flow.py b/custom_components/versatile_thermostat/config_flow.py index 2f20a3b..d36af96 100644 --- a/custom_components/versatile_thermostat/config_flow.py +++ b/custom_components/versatile_thermostat/config_flow.py @@ -259,6 +259,21 @@ async def validate_input(self, data: dict, step_id) -> None: if not self.check_valve_regulation_nb_entities(data, step_id): raise ValveRegulationNbEntitiesIncorrect() + # Check that the min_opening_degrees is correctly set + raw_list = data.get(CONF_MIN_OPENING_DEGREES, None) + if raw_list: + try: + # Validation : Convertir la liste saisie + int_list = [int(x.strip()) for x in raw_list.split(",")] + + # Optionnel : Vérifiez des conditions supplémentaires sur la liste + if any(x < 0 for x in int_list): + raise ValueError + except ValueError as exc: + raise ValveRegulationMinOpeningDegreesIncorrect( + CONF_MIN_OPENING_DEGREES + ) from exc + def check_config_complete(self, infos) -> bool: """True if the config is now complete (ie all mandatory attributes are set)""" is_central_config = ( @@ -399,6 +414,8 @@ async def generic_step(self, step_id, data_schema, user_input, next_step_functio errors["base"] = "configuration_not_complete" except ValveRegulationNbEntitiesIncorrect as err: errors["base"] = "valve_regulation_nb_entities_incorrect" + except ValveRegulationMinOpeningDegreesIncorrect as err: + errors[str(err)] = "min_opening_degrees_format" except Exception: # pylint: disable=broad-except _LOGGER.exception("Unexpected exception") errors["base"] = "unknown" diff --git a/custom_components/versatile_thermostat/config_schema.py b/custom_components/versatile_thermostat/config_schema.py index 0e199d2..92fba1f 100644 --- a/custom_components/versatile_thermostat/config_schema.py +++ b/custom_components/versatile_thermostat/config_schema.py @@ -219,6 +219,7 @@ PROPORTIONAL_FUNCTION_TPI, ] ), + vol.Optional(CONF_MIN_OPENING_DEGREES, default=""): str, } ) diff --git a/custom_components/versatile_thermostat/const.py b/custom_components/versatile_thermostat/const.py index f66c530..52cfecf 100644 --- a/custom_components/versatile_thermostat/const.py +++ b/custom_components/versatile_thermostat/const.py @@ -123,6 +123,7 @@ CONF_OFFSET_CALIBRATION_LIST = "offset_calibration_entity_ids" CONF_OPENING_DEGREE_LIST = "opening_degree_entity_ids" CONF_CLOSING_DEGREE_LIST = "closing_degree_entity_ids" +CONF_MIN_OPENING_DEGREES = "min_opening_degrees" # Deprecated CONF_HEATER = "heater_entity_id" @@ -552,6 +553,10 @@ class ValveRegulationNbEntitiesIncorrect(HomeAssistantError): The number of specific entities is incorrect.""" +class ValveRegulationMinOpeningDegreesIncorrect(HomeAssistantError): + """Error to indicate that the minimal opening degrees is not a list of int separated by coma""" + + class overrides: # pylint: disable=invalid-name """An annotation to inform overrides""" diff --git a/custom_components/versatile_thermostat/strings.json b/custom_components/versatile_thermostat/strings.json index d989f3d..5a43d68 100644 --- a/custom_components/versatile_thermostat/strings.json +++ b/custom_components/versatile_thermostat/strings.json @@ -224,13 +224,15 @@ "offset_calibration_entity_ids": "Offset calibration entities", "opening_degree_entity_ids": "Opening degree entities", "closing_degree_entity_ids": "Closing degree entities", - "proportional_function": "Algorithm" + "proportional_function": "Algorithm", + "min_opening_degrees": "Min opening degrees" }, "data_description": { "offset_calibration_entity_ids": "The list of the 'offset calibration' entities. Set it if your TRV have the entity for better regulation. There should be one per underlying climate entities", "opening_degree_entity_ids": "The list of the 'opening degree' entities. There should be one per underlying climate entities", "closing_degree_entity_ids": "The list of the 'closing degree' entities. Set it if your TRV have the entity for better regulation. There should be one per underlying climate entities", - "proportional_function": "Algorithm to use (TPI is the only one for now)" + "proportional_function": "Algorithm to use (TPI is the only one for now)", + "min_opening_degrees": "A comma seperated list of minimal opening degrees. Default to 0. Example: 20, 25, 30" } } }, @@ -468,13 +470,15 @@ "offset_calibration_entity_ids": "Offset calibration entities", "opening_degree_entity_ids": "Opening degree entities", "closing_degree_entity_ids": "Closing degree entities", - "proportional_function": "Algorithm" + "proportional_function": "Algorithm", + "min_opening_degrees": "Min opening degrees" }, "data_description": { "offset_calibration_entity_ids": "The list of the 'offset calibration' entities. Set it if your TRV have the entity for better regulation. There should be one per underlying climate entities", "opening_degree_entity_ids": "The list of the 'opening degree' entities. There should be one per underlying climate entities", "closing_degree_entity_ids": "The list of the 'closing degree' entities. Set it if your TRV have the entity for better regulation. There should be one per underlying climate entities", - "proportional_function": "Algorithm to use (TPI is the only one for now)" + "proportional_function": "Algorithm to use (TPI is the only one for now)", + "min_opening_degrees": "A comma seperated list of minimal opening degrees. Default to 0. Example: 20, 25, 30" } } }, @@ -484,7 +488,8 @@ "window_open_detection_method": "Only one window open detection method should be used. Use either window sensor or automatic detection through temperature threshold but not both", "no_central_config": "You cannot check 'use central configuration' because no central configuration was found. You need to create a Versatile Thermostat of type 'Central Configuration' to use it.", "service_configuration_format": "The format of the service configuration is wrong", - "valve_regulation_nb_entities_incorrect": "The number of valve entities for valve regulation should be equal to the number of underlyings" + "valve_regulation_nb_entities_incorrect": "The number of valve entities for valve regulation should be equal to the number of underlyings", + "min_opening_degrees_format": "A comma separated list of positive integer is expected. Example: 20, 25, 30" }, "abort": { "already_configured": "Device is already configured" diff --git a/custom_components/versatile_thermostat/thermostat_climate_valve.py b/custom_components/versatile_thermostat/thermostat_climate_valve.py index ab285d7..064fc08 100644 --- a/custom_components/versatile_thermostat/thermostat_climate_valve.py +++ b/custom_components/versatile_thermostat/thermostat_climate_valve.py @@ -37,6 +37,7 @@ class ThermostatOverClimateValve(ThermostatOverClimate): "tpi_coef_int", "tpi_coef_ext", "power_percent", + "min_opening_degrees", } ) ) @@ -51,6 +52,7 @@ def __init__( self._last_calculation_timestamp: datetime | None = None self._auto_regulation_dpercent: float | None = None self._auto_regulation_period_min: int | None = None + self._min_opening_degress: list[int] = [] super().__init__(hass, unique_id, name, entry_infos) @@ -86,6 +88,14 @@ def post_init(self, config_entry: ConfigData): offset_list = config_entry.get(CONF_OFFSET_CALIBRATION_LIST, []) opening_list = config_entry.get(CONF_OPENING_DEGREE_LIST) closing_list = config_entry.get(CONF_CLOSING_DEGREE_LIST, []) + + self._min_opening_degrees = config_entry.get(CONF_MIN_OPENING_DEGREES, None) + min_opening_degrees_list = [] + if self._min_opening_degrees: + min_opening_degrees_list = [ + int(x.strip()) for x in self._min_opening_degrees.split(",") + ] + for idx, _ in enumerate(config_entry.get(CONF_UNDERLYING_LIST)): offset = offset_list[idx] if idx < len(offset_list) else None # number of opening should equal number of underlying @@ -98,6 +108,11 @@ def post_init(self, config_entry: ConfigData): opening_degree_entity_id=opening, closing_degree_entity_id=closing, climate_underlying=self._underlyings[idx], + min_opening_degree=( + min_opening_degrees_list[idx] + if idx < len(min_opening_degrees_list) + else 0 + ), ) self._underlyings_valve_regulation.append(under) @@ -130,6 +145,10 @@ def update_custom_attributes(self): self._attr_extra_state_attributes["tpi_coef_int"] = self._tpi_coef_int self._attr_extra_state_attributes["tpi_coef_ext"] = self._tpi_coef_ext + self._attr_extra_state_attributes["min_opening_degrees"] = ( + self._min_opening_degrees + ) + self._attr_extra_state_attributes["valve_open_percent"] = ( self.valve_open_percent ) diff --git a/custom_components/versatile_thermostat/translations/en.json b/custom_components/versatile_thermostat/translations/en.json index d989f3d..1a48eaf 100644 --- a/custom_components/versatile_thermostat/translations/en.json +++ b/custom_components/versatile_thermostat/translations/en.json @@ -224,13 +224,15 @@ "offset_calibration_entity_ids": "Offset calibration entities", "opening_degree_entity_ids": "Opening degree entities", "closing_degree_entity_ids": "Closing degree entities", - "proportional_function": "Algorithm" + "proportional_function": "Algorithm", + "min_opening_degrees": "Min opening degrees" }, "data_description": { "offset_calibration_entity_ids": "The list of the 'offset calibration' entities. Set it if your TRV have the entity for better regulation. There should be one per underlying climate entities", "opening_degree_entity_ids": "The list of the 'opening degree' entities. There should be one per underlying climate entities", "closing_degree_entity_ids": "The list of the 'closing degree' entities. Set it if your TRV have the entity for better regulation. There should be one per underlying climate entities", - "proportional_function": "Algorithm to use (TPI is the only one for now)" + "proportional_function": "Algorithm to use (TPI is the only one for now)", + "min_opening_degrees": "A comma seperated list of minimal opening degrees. Default to 0. Example: 20, 25, 30" } } }, @@ -468,13 +470,15 @@ "offset_calibration_entity_ids": "Offset calibration entities", "opening_degree_entity_ids": "Opening degree entities", "closing_degree_entity_ids": "Closing degree entities", - "proportional_function": "Algorithm" + "proportional_function": "Algorithm", + "min_opening_degrees": "Min opening degrees" }, "data_description": { "offset_calibration_entity_ids": "The list of the 'offset calibration' entities. Set it if your TRV have the entity for better regulation. There should be one per underlying climate entities", "opening_degree_entity_ids": "The list of the 'opening degree' entities. There should be one per underlying climate entities", "closing_degree_entity_ids": "The list of the 'closing degree' entities. Set it if your TRV have the entity for better regulation. There should be one per underlying climate entities", - "proportional_function": "Algorithm to use (TPI is the only one for now)" + "proportional_function": "Algorithm to use (TPI is the only one for now)", + "min_opening_degrees": "A comma seperated list of minimal opening degrees. Default to 0. Example: 20, 25, 30" } } }, diff --git a/custom_components/versatile_thermostat/translations/fr.json b/custom_components/versatile_thermostat/translations/fr.json index a73bf35..3b2fb04 100644 --- a/custom_components/versatile_thermostat/translations/fr.json +++ b/custom_components/versatile_thermostat/translations/fr.json @@ -224,13 +224,15 @@ "offset_calibration_entity_ids": "Entités de 'calibrage du décalage''", "opening_degree_entity_ids": "Entités 'ouverture de vanne'", "closing_degree_entity_ids": "Entités 'fermeture de la vanne'", - "proportional_function": "Algorithme" + "proportional_function": "Algorithme", + "min_opening_degrees": "Ouvertures minimales" }, "data_description": { "offset_calibration_entity_ids": "La liste des entités 'calibrage du décalage' (offset calibration). Configurez le si votre TRV possède cette fonction pour une meilleure régulation. Il doit y en avoir une par entité climate sous-jacente", "opening_degree_entity_ids": "La liste des entités 'ouverture de vanne'. Il doit y en avoir une par entité climate sous-jacente", "closing_degree_entity_ids": "La liste des entités 'fermeture de la vanne'. Configurez le si votre TRV possède cette fonction pour une meilleure régulation. Il doit y en avoir une par entité climate sous-jacente", - "proportional_function": "Algorithme à utiliser (seulement TPI est disponible)" + "proportional_function": "Algorithme à utiliser (seulement TPI est disponible)", + "min_opening_degrees": "Une liste séparée par des virgules de minimum d'ouverture. Valeur par défaut : 0. Exemple : 20, 25, 30" } } }, @@ -462,13 +464,15 @@ "offset_calibration_entity_ids": "Entités de 'calibrage du décalage''", "opening_degree_entity_ids": "Entités 'ouverture de vanne'", "closing_degree_entity_ids": "Entités 'fermeture de la vanne'", - "proportional_function": "Algorithme" + "proportional_function": "Algorithme", + "min_opening_degrees": "Ouvertures minimales" }, "data_description": { "offset_calibration_entity_ids": "La liste des entités 'calibrage du décalage' (offset calibration). Configurez le si votre TRV possède cette fonction pour une meilleure régulation. Il doit y en avoir une par entité climate sous-jacente", "opening_degree_entity_ids": "La liste des entités 'ouverture de vanne'. Il doit y en avoir une par entité climate sous-jacente", "closing_degree_entity_ids": "La liste des entités 'fermeture de la vanne'. Configurez le si votre TRV possède cette fonction pour une meilleure régulation. Il doit y en avoir une par entité climate sous-jacente", - "proportional_function": "Algorithme à utiliser (seulement TPI est disponible)" + "proportional_function": "Algorithme à utiliser (seulement TPI est disponible)", + "min_opening_degrees": "Une liste séparée par des virgules de minimum d'ouverture. Valeur par défaut : 0. Exemple : 20, 25, 30" } } }, @@ -478,7 +482,8 @@ "window_open_detection_method": "Une seule méthode de détection des ouvertures ouvertes doit être utilisée. Utilisez le détecteur d'ouverture ou les seuils de température mais pas les deux.", "no_central_config": "Vous ne pouvez pas cocher 'Utiliser la configuration centrale' car aucune configuration centrale n'a été trouvée. Vous devez créer un Versatile Thermostat de type 'Central Configuration' pour pouvoir l'utiliser.", "service_configuration_format": "Mauvais format de la configuration du service", - "valve_regulation_nb_entities_incorrect": "Le nombre d'entités pour la régulation par vanne doit être égal au nombre d'entité sous-jacentes" + "valve_regulation_nb_entities_incorrect": "Le nombre d'entités pour la régulation par vanne doit être égal au nombre d'entité sous-jacentes", + "min_opening_degrees_format": "Une liste d'entiers positifs séparés par des ',' est attendu. Exemple : 20, 25, 30" }, "abort": { "already_configured": "Le device est déjà configuré" diff --git a/custom_components/versatile_thermostat/underlyings.py b/custom_components/versatile_thermostat/underlyings.py index c4a894a..267b2b8 100644 --- a/custom_components/versatile_thermostat/underlyings.py +++ b/custom_components/versatile_thermostat/underlyings.py @@ -1029,6 +1029,7 @@ def __init__( opening_degree_entity_id: str, closing_degree_entity_id: str, climate_underlying: UnderlyingClimate, + min_opening_degree: int = 0, ) -> None: """Initialize the underlying TRV with valve regulation""" super().__init__( @@ -1045,6 +1046,7 @@ def __init__( self._max_opening_degree: float = None self._min_offset_calibration: float = None self._max_offset_calibration: float = None + self._min_opening_degree: int = min_opening_degree async def send_percent_open(self): """Send the percent open to the underlying valve""" @@ -1079,6 +1081,9 @@ async def send_percent_open(self): return # Send opening_degree + if 0 < self._percent_open < self._min_opening_degree: + self._percent_open = self._min_opening_degree + await super().send_percent_open() # Send closing_degree if set @@ -1138,6 +1143,11 @@ def closing_degree_entity_id(self) -> str: """The offset_calibration_entity_id""" return self._closing_degree_entity_id + @property + def min_opening_degree(self) -> int: + """The minimum opening degree""" + return self._min_opening_degree + @property def have_closing_degree_entity(self) -> bool: """Return True if the underlying have a closing_degree entity""" diff --git a/tests/test_config_flow.py b/tests/test_config_flow.py index d73b501..3b87b63 100644 --- a/tests/test_config_flow.py +++ b/tests/test_config_flow.py @@ -1581,6 +1581,7 @@ async def test_user_config_flow_over_climate_valve( CONF_OFFSET_CALIBRATION_LIST: ["number.offset_calibration1"], CONF_OPENING_DEGREE_LIST: ["number.opening_degree1"], CONF_CLOSING_DEGREE_LIST: ["number.closing_degree1"], + CONF_MIN_OPENING_DEGREES: "10, 20,0", }, ) assert result["type"] == FlowResultType.FORM @@ -1619,6 +1620,7 @@ async def test_user_config_flow_over_climate_valve( "number.opening_degree2", ], CONF_CLOSING_DEGREE_LIST: [], + CONF_MIN_OPENING_DEGREES: "10, 20,0", }, ) assert result["type"] == FlowResultType.MENU @@ -1715,6 +1717,7 @@ async def test_user_config_flow_over_climate_valve( CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI, CONF_TPI_COEF_INT: 0.3, CONF_TPI_COEF_EXT: 0.1, + CONF_MIN_OPENING_DEGREES: "10, 20,0", } assert result["result"] assert result["result"].domain == DOMAIN diff --git a/tests/test_overclimate_valve.py b/tests/test_overclimate_valve.py index 1040891..8dde208 100644 --- a/tests/test_overclimate_valve.py +++ b/tests/test_overclimate_valve.py @@ -483,3 +483,186 @@ async def test_over_climate_valve_multi_presence( ) assert vtherm.nb_device_actives == 0 + + +@pytest.mark.parametrize("expected_lingering_timers", [True]) +async def test_over_climate_valve_multi_min_opening_degrees( + hass: HomeAssistant, skip_hass_states_get +): + """Test the normal full start of a thermostat in thermostat_over_climate type + with valve_regulation and min_opening_degreess set""" + + entry = MockConfigEntry( + domain=DOMAIN, + title="TheOverClimateMockName", + unique_id="uniqueId", + data={ + CONF_NAME: "TheOverClimateMockName", + CONF_TEMP_SENSOR: "sensor.mock_temp_sensor", + CONF_CYCLE_MIN: 5, + CONF_DEVICE_POWER: 1, + CONF_USE_MAIN_CENTRAL_CONFIG: False, + CONF_USE_CENTRAL_MODE: False, + CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_CLIMATE, + CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor", + CONF_TEMP_MIN: 15, + CONF_TEMP_MAX: 30, + CONF_STEP_TEMPERATURE: 0.1, + CONF_UNDERLYING_LIST: ["climate.mock_climate1", "climate.mock_climate2"], + CONF_AC_MODE: False, + CONF_AUTO_REGULATION_MODE: CONF_AUTO_REGULATION_VALVE, + CONF_AUTO_REGULATION_DTEMP: 0.01, + CONF_AUTO_REGULATION_PERIOD_MIN: 0, + CONF_AUTO_FAN_MODE: CONF_AUTO_FAN_HIGH, + CONF_AUTO_REGULATION_USE_DEVICE_TEMP: False, + CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI, + CONF_TPI_COEF_INT: 0.3, + CONF_TPI_COEF_EXT: 0.1, + CONF_OPENING_DEGREE_LIST: [ + "number.mock_opening_degree1", + "number.mock_opening_degree2", + ], + CONF_CLOSING_DEGREE_LIST: [ + "number.mock_closing_degree1", + "number.mock_closing_degree2", + ], + CONF_OFFSET_CALIBRATION_LIST: [ + "number.mock_offset_calibration1", + "number.mock_offset_calibration2", + ], + CONF_USE_PRESENCE_FEATURE: False, + CONF_USE_WINDOW_FEATURE: False, + CONF_USE_MOTION_FEATURE: False, + CONF_USE_POWER_FEATURE: False, + CONF_MIN_OPENING_DEGREES: "60,70", + } + | MOCK_DEFAULT_CENTRAL_CONFIG + | MOCK_ADVANCED_CONFIG, + ) + + fake_underlying_climate1 = MockClimate( + hass, "mockUniqueId1", "MockClimateName1", {} + ) + fake_underlying_climate2 = MockClimate( + hass, "mockUniqueId2", "MockClimateName2", {} + ) + + # mock_get_state will be called for each OPENING/CLOSING/OFFSET_CALIBRATION list + mock_get_state_side_effect = SideEffects( + { + # Valve 1 is open + "number.mock_opening_degree1": State( + "number.mock_opening_degree1", "10", {"min": 0, "max": 100} + ), + "number.mock_closing_degree1": State( + "number.mock_closing_degree1", "90", {"min": 0, "max": 100} + ), + "number.mock_offset_calibration1": State( + "number.mock_offset_calibration1", "0", {"min": -12, "max": 12} + ), + # Valve 2 is closed + "number.mock_opening_degree2": State( + "number.mock_opening_degree2", "0", {"min": 0, "max": 100} + ), + "number.mock_closing_degree2": State( + "number.mock_closing_degree2", "100", {"min": 0, "max": 100} + ), + "number.mock_offset_calibration2": State( + "number.mock_offset_calibration2", "10", {"min": -12, "max": 12} + ), + }, + State("unknown.entity_id", "unknown"), + ) + + # 1. initialize the VTherm + tz = get_tz(hass) # pylint: disable=invalid-name + now: datetime = datetime.now(tz=tz) + + # fmt: off + with patch("custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event") as mock_send_event, \ + patch("custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate", side_effect=[fake_underlying_climate1, fake_underlying_climate2]) as mock_find_climate, \ + patch("homeassistant.core.ServiceRegistry.async_call") as mock_service_call,\ + patch("homeassistant.core.StateMachine.get", side_effect=mock_get_state_side_effect.get_side_effects()) as mock_get_state: + # fmt: on + + vtherm: ThermostatOverClimateValve = await create_thermostat(hass, entry, "climate.theoverclimatemockname") + assert vtherm + assert isinstance(vtherm, ThermostatOverClimateValve) + + assert vtherm.name == "TheOverClimateMockName" + assert vtherm.is_over_climate is True + assert vtherm.have_valve_regulation is True + + vtherm._set_now(now) + + # initialize the temps + await set_all_climate_preset_temp(hass, vtherm, default_temperatures, "theoverclimatemockname") + + await send_temperature_change_event(vtherm, 20, now, True) + await send_ext_temperature_change_event(vtherm, 20, now, True) + await send_presence_change_event(vtherm, False, True, now) + + await vtherm.async_set_preset_mode(PRESET_COMFORT) + await vtherm.async_set_hvac_mode(HVACMode.HEAT) + + assert vtherm.target_temperature == 19 + assert vtherm.nb_device_actives == 0 + + # 2: set temperature -> should activate the valve and change target + # fmt: off + with patch("custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event") as mock_send_event, \ + patch("homeassistant.core.ServiceRegistry.async_call") as mock_service_call,\ + patch("homeassistant.core.StateMachine.get", side_effect=mock_get_state_side_effect.get_side_effects()) as mock_get_state: + # fmt: on + now = now + timedelta(minutes=3) + vtherm._set_now(now) + + await send_temperature_change_event(vtherm, 18, now, True) + await hass.async_block_till_done() + + assert vtherm.is_device_active is True + assert vtherm.valve_open_percent == 20 + + # the underlying set temperature call and the call to the valve + assert mock_service_call.call_count == 6 + mock_service_call.assert_has_calls([ + # min is 60 + call(domain='number', service='set_value', service_data={'value': 60}, target={'entity_id': 'number.mock_opening_degree1'}), + call(domain='number', service='set_value', service_data={'value': 40}, target={'entity_id': 'number.mock_closing_degree1'}), + call(domain='number', service='set_value', service_data={'value': 3.0}, target={'entity_id': 'number.mock_offset_calibration1'}), + call(domain='number', service='set_value', service_data={'value': 70}, target={'entity_id': 'number.mock_opening_degree2'}), + call(domain='number', service='set_value', service_data={'value': 30}, target={'entity_id': 'number.mock_closing_degree2'}), + call(domain='number', service='set_value', service_data={'value': 12}, target={'entity_id': 'number.mock_offset_calibration2'}) + ] + ) + + assert vtherm.nb_device_actives >= 2 # should be 2 but when run in // with the first test it give 3 + + # 3: set high temperature -> should deactivate the valve and change target + # fmt: off + with patch("custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event") as mock_send_event, \ + patch("homeassistant.core.ServiceRegistry.async_call") as mock_service_call,\ + patch("homeassistant.core.StateMachine.get", side_effect=mock_get_state_side_effect.get_side_effects()) as mock_get_state: + # fmt: on + now = now + timedelta(minutes=3) + vtherm._set_now(now) + + await send_temperature_change_event(vtherm, 22, now, True) + await hass.async_block_till_done() + + assert vtherm.is_device_active is False + assert vtherm.valve_open_percent == 0 + + # the underlying set temperature call and the call to the valve + assert mock_service_call.call_count == 6 + mock_service_call.assert_has_calls([ + call(domain='number', service='set_value', service_data={'value': 0}, target={'entity_id': 'number.mock_opening_degree1'}), + call(domain='number', service='set_value', service_data={'value': 100}, target={'entity_id': 'number.mock_closing_degree1'}), + call(domain='number', service='set_value', service_data={'value': 7.0}, target={'entity_id': 'number.mock_offset_calibration1'}), + call(domain='number', service='set_value', service_data={'value': 0}, target={'entity_id': 'number.mock_opening_degree2'}), + call(domain='number', service='set_value', service_data={'value': 100}, target={'entity_id': 'number.mock_closing_degree2'}), + call(domain='number', service='set_value', service_data={'value': 12}, target={'entity_id': 'number.mock_offset_calibration2'}) + ] + ) + + assert vtherm.nb_device_actives == 0