From 8698be8bd4b0163c1e9c3caaedfccaf972e0a8e3 Mon Sep 17 00:00:00 2001 From: = <=> Date: Mon, 8 Jul 2024 09:18:37 +0000 Subject: [PATCH] fix: ignores min_cycle_duration to avoid gaps in cooling when using fan_hot_tolerance Fixes #218 --- .../dual_smart_thermostat/climate.py | 2 +- .../hvac_device/cooler_fan_device.py | 13 ++- .../managers/environment_manager.py | 4 +- tests/__init__.py | 29 +++++++ tests/test_fan_mode.py | 87 ++++++++++++++++++- 5 files changed, 128 insertions(+), 7 deletions(-) diff --git a/custom_components/dual_smart_thermostat/climate.py b/custom_components/dual_smart_thermostat/climate.py index a32b2f8..2582c73 100644 --- a/custom_components/dual_smart_thermostat/climate.py +++ b/custom_components/dual_smart_thermostat/climate.py @@ -434,7 +434,7 @@ async def async_added_to_hass(self) -> None: ) ) - # registre device's on-remove + # register device's on-remove self.async_on_remove(self.hvac_device.call_on_remove_callbacks) if self.sensor_floor_entity_id is not None: diff --git a/custom_components/dual_smart_thermostat/hvac_device/cooler_fan_device.py b/custom_components/dual_smart_thermostat/hvac_device/cooler_fan_device.py index 5195bc1..4d5d980 100644 --- a/custom_components/dual_smart_thermostat/hvac_device/cooler_fan_device.py +++ b/custom_components/dual_smart_thermostat/hvac_device/cooler_fan_device.py @@ -82,19 +82,28 @@ async def async_control_hvac(self, time=None, force=False): is_warmer_outside = self.environment.is_warmer_outside is_fan_air_outside = self.fan_device.fan_air_surce_outside + # If the fan_hot_tolerance is set, enforce the action for the fan or cooler device + # to ignore cycles as we switch between the fan and cooler device + # and we want to avoid idle time gaps between the devices + forceOverride = ( + True + if self.environment.fan_hot_tolerance is not None + else force + ) + if is_within_fan_tolerance and not ( is_fan_air_outside and is_warmer_outside ): _LOGGER.debug("within fan tolerance") self.fan_device.hvac_mode = HVACMode.FAN_ONLY - await self.fan_device.async_control_hvac(time, force) + await self.fan_device.async_control_hvac(time, forceOverride) await self.cooler_device.async_turn_off() self.HVACActionReason = ( HVACActionReason.TARGET_TEMP_NOT_REACHED_WITH_FAN ) else: _LOGGER.debug("outside fan tolerance") - await self.cooler_device.async_control_hvac(time, force) + await self.cooler_device.async_control_hvac(time, forceOverride) await self.fan_device.async_turn_off() self.HVACActionReason = self.cooler_device.HVACActionReason diff --git a/custom_components/dual_smart_thermostat/managers/environment_manager.py b/custom_components/dual_smart_thermostat/managers/environment_manager.py index 25d1eb1..1cb15d7 100644 --- a/custom_components/dual_smart_thermostat/managers/environment_manager.py +++ b/custom_components/dual_smart_thermostat/managers/environment_manager.py @@ -215,7 +215,7 @@ def saved_target_humidity(self, humidity: float) -> None: self._saved_target_humidity = humidity @property - def fan_cold_tolerance(self) -> float: + def fan_hot_tolerance(self) -> float: return self._fan_hot_tolerance @property @@ -510,7 +510,7 @@ def _set_default_temps_range_mode(self) -> None: self._target_temp_low = self.min_temp self._target_temp_high = self.max_temp _LOGGER.warning( - "Undefined target temperature range, falled back to %s-%s-%s", + "Undefined target temperature range, fell back to %s-%s-%s", self._target_temp, self._target_temp_low, self._target_temp_high, diff --git a/tests/__init__.py b/tests/__init__.py index 5e5f814..d1c7163 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -359,6 +359,35 @@ async def setup_comp_heat_ac_cool_fan_config_tolerance(hass: HomeAssistant) -> N await hass.async_block_till_done() +@pytest.fixture +async def setup_comp_heat_ac_cool_fan_config_tolerance_min_cycle( + hass: HomeAssistant, +) -> None: + """Initialize components.""" + hass.config.units = METRIC_SYSTEM + assert await async_setup_component( + hass, + CLIMATE, + { + "climate": { + "platform": DOMAIN, + "name": "test", + "cold_tolerance": 2, + "hot_tolerance": 4, + "ac_mode": True, + "heater": common.ENT_SWITCH, + "target_sensor": common.ENT_SENSOR, + "fan": common.ENT_FAN, + "fan_hot_tolerance": 1, + "min_cycle_duration": datetime.timedelta(minutes=10), + "initial_hvac_mode": HVACMode.OFF, + PRESET_AWAY: {"temperature": 30}, + } + }, + ) + await hass.async_block_till_done() + + @pytest.fixture async def setup_comp_heat_ac_cool_fan_config_cycle(hass: HomeAssistant) -> None: """Initialize components.""" diff --git a/tests/test_fan_mode.py b/tests/test_fan_mode.py index eaedecf..dddbdd7 100644 --- a/tests/test_fan_mode.py +++ b/tests/test_fan_mode.py @@ -2353,6 +2353,89 @@ async def test_set_target_temp_ac_fan_on( assert call.data["entity_id"] == common.ENT_FAN +async def test_set_target_temp_ac_on_tolerance_and_cycle( + hass: HomeAssistant, setup_comp_1 # noqa: F811 +) -> None: + """Test if target temperature turn ac or fan on without cycle gap.""" + cooler_switch = "input_boolean.test" + fan_switch = "input_boolean.fan" + + assert await async_setup_component( + hass, + input_boolean.DOMAIN, + {"input_boolean": {"test": None, "fan": None}}, + ) + + assert await async_setup_component( + hass, + input_number.DOMAIN, + { + "input_number": { + "temp": {"name": "test", "initial": 10, "min": 0, "max": 40, "step": 1} + } + }, + ) + + assert await async_setup_component( + hass, + CLIMATE, + { + "climate": { + "platform": DOMAIN, + "name": "test", + "cold_tolerance": 0.2, + "hot_tolerance": 0.2, + "ac_mode": True, + "heater": cooler_switch, + "target_sensor": common.ENT_SENSOR, + "fan": fan_switch, + "fan_hot_tolerance": 0.5, + "min_cycle_duration": timedelta(minutes=10), + "initial_hvac_mode": HVACMode.OFF, + } + }, + ) + await hass.async_block_till_done() + + await common.async_set_hvac_mode(hass, HVACMode.COOL) + await common.async_set_temperature(hass, 20) + + # below hot_tolerance + setup_sensor(hass, 20) + await hass.async_block_till_done() + + assert hass.states.get(cooler_switch).state == STATE_OFF + assert hass.states.get(fan_switch).state == STATE_OFF + + # within hot_tolerance and fan_hot_tolerance + setup_sensor(hass, 20.2) + await hass.async_block_till_done() + + assert hass.states.get(cooler_switch).state == STATE_OFF + assert hass.states.get(fan_switch).state == STATE_ON + + # within hot_tolerance and fan_hot_tolerance + setup_sensor(hass, 20.5) + await hass.async_block_till_done() + + assert hass.states.get(cooler_switch).state == STATE_OFF + assert hass.states.get(fan_switch).state == STATE_ON + + # within hot_tolerance and fan_hot_tolerance + setup_sensor(hass, 20.7) + await hass.async_block_till_done() + + assert hass.states.get(cooler_switch).state == STATE_OFF + assert hass.states.get(fan_switch).state == STATE_ON + + # outside fan_hot_tolerance, within hot_tolerance + setup_sensor(hass, 20.8) + await hass.async_block_till_done() + + assert hass.states.get(cooler_switch).state == STATE_ON + assert hass.states.get(fan_switch).state == STATE_OFF + + async def test_set_target_temp_ac_on_after_fan_tolerance( hass: HomeAssistant, setup_comp_heat_ac_cool_fan_config_tolerance # noqa: F811 ) -> None: @@ -2558,8 +2641,8 @@ async def test_set_target_temp_ac_on_dont_ignore_fan_tolerance( hass: HomeAssistant, setup_comp_1 # noqa: F811 ) -> None: """Test if target temperature turn ac on. - ignoring fan tolerance if fan blows outside air - that is warmer than the inside air""" + not ignoring fan tolerance if outside temp + is colder than target temp""" cooler_switch = "input_boolean.test" fan_switch = "input_boolean.fan"