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 @@ class VersatileThermostatBaseConfigFlow(FlowHandler): 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 @@ class VersatileThermostatBaseConfigFlow(FlowHandler): 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 @@ STEP_VALVE_REGULATION = vol.Schema( # pylint: disable=invalid-name 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_STEP_TEMPERATURE = "step_temperature" 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/manifest.json b/custom_components/versatile_thermostat/manifest.json index 6fd859b..1fc9cca 100644 --- a/custom_components/versatile_thermostat/manifest.json +++ b/custom_components/versatile_thermostat/manifest.json @@ -14,6 +14,6 @@ "quality_scale": "silver", "requirements": [], "ssdp": [], - "version": "6.8.3", + "version": "6.8.4", "zeroconf": [] } \ No newline at end of file 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 @@ class ThermostatOverClimateValve(ThermostatOverClimate): 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 @@ class ThermostatOverClimateValve(ThermostatOverClimate): 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 @@ class ThermostatOverClimateValve(ThermostatOverClimate): 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 @@ class ThermostatOverClimateValve(ThermostatOverClimate): 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 @@ class UnderlyingValveRegulation(UnderlyingValve): 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 @@ class UnderlyingValveRegulation(UnderlyingValve): 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 @@ class UnderlyingValveRegulation(UnderlyingValve): 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 @@ class UnderlyingValveRegulation(UnderlyingValve): """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/documentation/en/images/1.png b/documentation/en/images/1.png new file mode 100644 index 0000000..f37ca28 Binary files /dev/null and b/documentation/en/images/1.png differ diff --git a/documentation/en/images/config-self-regulation-valve-2.png b/documentation/en/images/config-self-regulation-valve-2.png index f37ca28..1d4d2f2 100644 Binary files a/documentation/en/images/config-self-regulation-valve-2.png and b/documentation/en/images/config-self-regulation-valve-2.png differ diff --git a/documentation/en/self-regulation.md b/documentation/en/self-regulation.md index 188b238..c9966d4 100644 --- a/documentation/en/self-regulation.md +++ b/documentation/en/self-regulation.md @@ -32,6 +32,7 @@ You need to provide: 1. As many valve opening control entities as there are underlying devices, and in the same order. These parameters are mandatory. 2. As many temperature calibration entities as there are underlying devices, and in the same order. These parameters are optional; they must either all be provided or none. 3. As many valve closure control entities as there are underlying devices, and in the same order. These parameters are optional; they must either all be provided or none. +4. A list of minimum opening values ​​for the valve when it needs to be opened. This field is a list of integers. If the valve needs to be opened, it will be opened at a minimum of this opening value. This allows enough water to pass through when it needs to be opened. The opening rate calculation algorithm is based on the _TPI_ algorithm described [here](algorithms.md). This is the same algorithm used for _VTherms_ `over_switch` and `over_valve`. diff --git a/documentation/fr/images/config-self-regulation-valve-2.png b/documentation/fr/images/config-self-regulation-valve-2.png index 5a513cb..8da1377 100644 Binary files a/documentation/fr/images/config-self-regulation-valve-2.png and b/documentation/fr/images/config-self-regulation-valve-2.png differ diff --git a/documentation/fr/self-regulation.md b/documentation/fr/self-regulation.md index 1c2a0a8..049e916 100644 --- a/documentation/fr/self-regulation.md +++ b/documentation/fr/self-regulation.md @@ -32,7 +32,8 @@ Elle permet de configurer les entités de contrôle de la vanne : Vous devez donner : 1. autant d'entités de contrôle d'ouverture de la vanne qu'il y a de sous-jacents et dans le même odre. Ces paramètres sont obligatoires, 2. autant d'entités de calibrage du décalage de température qu'il y a de sous-jacents et dans le même ordre. Ces paramètres sont facultatifs ; ils doivent être tous founis ou aucun, -3. autant d'entités de de contrôile du taux de fermture qu'il y a de sous-jacents et dans le même ordre. Ces paramètres sont facultatifs ; ils doivent être tous founis ou aucun +3. autant d'entités de de contrôile du taux de fermture qu'il y a de sous-jacents et dans le même ordre. Ces paramètres sont facultatifs ; ils doivent être tous founis ou aucun, +4. une liste de valeurs minimales d'ouverture de la vanne lorsqu'elle doit être ouverte. Ce champ est une liste d'entier. Si la vanne doit être ouverte, elle le sera au minimum avec cette valeur d'ouverture. Cela permet de laisser passer suffisamment d'eau lorsqu'elle doit être ouverte. L'algorithme de calcul du taux d'ouverture est basé sur le _TPI_ qui est décrit [ici](algorithms.md). C'est le même algorithme qui est utilisé pour les _VTherm_ `over_switch` et `over_valve`. 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