From ce8b169e0bc6d8a040af3a744cb1435e2cb026c1 Mon Sep 17 00:00:00 2001 From: = <=> Date: Wed, 1 May 2024 13:46:34 +0000 Subject: [PATCH] fix: the heat/cool fuction is not working with the fan-mode Fixes #175 --- config/configuration.yaml | 16 ++++ .../dual_smart_thermostat/climate.py | 3 +- .../hvac_device/cooler_device.py | 15 +++- .../hvac_device/cooler_fan_device.py | 17 +++-- .../hvac_device/heater_aux_heater_device.py | 7 +- .../hvac_device/heater_device.py | 19 +++-- .../hvac_device/hvac_device.py | 10 +++ .../hvac_device/hvac_device_factory.py | 20 +++-- .../hvac_device/specific_hvac_device.py | 22 ++++-- .../managers/feature_manager.py | 5 ++ .../managers/temperature_manager.py | 8 +- tests/__init__.py | 75 +++++++++++++++++++ tests/test_dual_mode.py | 3 + 13 files changed, 189 insertions(+), 31 deletions(-) diff --git a/config/configuration.yaml b/config/configuration.yaml index a0a348a..6cf1dc6 100644 --- a/config/configuration.yaml +++ b/config/configuration.yaml @@ -226,6 +226,22 @@ climate: target_temp_step: 0.5 initial_hvac_mode: heat + - platform: dual_smart_thermostat + name: Edge Case 175 + unique_id: edge_case_175 + heater: switch.heater + cooler: switch.cooler + target_sensor: sensor.room_temp + heat_cool_mode: true + fan: switch.fan + fan_hot_tolerance: 1 + target_temp_step: 0.5 + min_temp: 9 + max_temp: 32 + target_temp: 19.5 + target_temp_high: 20.5 + target_temp_low: 19.5 + - platform: dual_smart_thermostat name: AUX Heat Room unique_id: aux_heat_room diff --git a/custom_components/dual_smart_thermostat/climate.py b/custom_components/dual_smart_thermostat/climate.py index d6cbbdd..6cf8703 100644 --- a/custom_components/dual_smart_thermostat/climate.py +++ b/custom_components/dual_smart_thermostat/climate.py @@ -655,10 +655,11 @@ async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: _LOGGER.debug("Unrecognized hvac mode: %s", hvac_mode) return - await self.hvac_device.async_set_hvac_mode(hvac_mode) self._hvac_mode = hvac_mode self._set_support_flags() + await self.hvac_device.async_set_hvac_mode(hvac_mode) + self._hvac_action_reason = self.hvac_device.HVACActionReason # Ensure we update the current operation after changing the mode diff --git a/custom_components/dual_smart_thermostat/hvac_device/cooler_device.py b/custom_components/dual_smart_thermostat/hvac_device/cooler_device.py index 4c1b751..36875c3 100644 --- a/custom_components/dual_smart_thermostat/hvac_device/cooler_device.py +++ b/custom_components/dual_smart_thermostat/hvac_device/cooler_device.py @@ -7,6 +7,9 @@ from custom_components.dual_smart_thermostat.hvac_device.specific_hvac_device import ( SpecificHVACDevice, ) +from custom_components.dual_smart_thermostat.managers.feature_manager import ( + FeatureManager, +) from custom_components.dual_smart_thermostat.managers.opening_manager import ( OpeningManager, ) @@ -29,7 +32,7 @@ def __init__( initial_hvac_mode: HVACMode, temperatures: TemperatureManager, openings: OpeningManager, - range_mode: bool = False, + features: FeatureManager, ) -> None: super().__init__( hass, @@ -38,10 +41,16 @@ def __init__( initial_hvac_mode, temperatures, openings, + features, ) - if range_mode: - self._target_temp_attr = "_target_temp_high" + @property + def target_temp_attr(self) -> str: + return ( + "_target_temp_high" + if self.features.is_range_mode + else self._target_temp_attr + ) @property def hvac_action(self) -> HVACAction: 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 fb9813b..6ae43a6 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 @@ -18,6 +18,9 @@ HVACDevice, merge_hvac_modes, ) +from custom_components.dual_smart_thermostat.managers.feature_manager import ( + FeatureManager, +) from custom_components.dual_smart_thermostat.managers.opening_manager import ( OpeningManager, ) @@ -38,19 +41,17 @@ def __init__( initial_hvac_mode: HVACMode, temperatures: TemperatureManager, openings: OpeningManager, - fan_on_with_cooler: bool = False, - range_mode: bool = False, + features: FeatureManager, ) -> None: super().__init__(hass, temperatures, openings) + self._features = features + self._device_type = self.__class__.__name__ - self._fan_on_with_cooler = fan_on_with_cooler + self._fan_on_with_cooler = features.is_configured_for_fan_on_with_cooler self.cooler_device = cooler_device self.fan_device = fan_device - if range_mode: - self._target_temp_attr = "_target_temp_high" - # _hvac_modes are the combined values of the cooler_device.hvac_modes and fan_device.hvac_modes without duplicates self.hvac_modes = merge_hvac_modes( cooler_device.hvac_modes, fan_device.hvac_modes @@ -135,7 +136,9 @@ async def async_control_hvac(self, time=None, force=False): self.HVACActionReason = self.cooler_device.HVACActionReason else: - if self.temperatures.is_within_fan_tolerance: + if self.temperatures.is_within_fan_tolerance( + self.fan_device.target_temp_attr + ): _LOGGER.info("within fan tolerance") await self.fan_device.async_control_hvac(time, force) await self.cooler_device.async_turn_off() diff --git a/custom_components/dual_smart_thermostat/hvac_device/heater_aux_heater_device.py b/custom_components/dual_smart_thermostat/hvac_device/heater_aux_heater_device.py index 2bbf965..0a33673 100644 --- a/custom_components/dual_smart_thermostat/hvac_device/heater_aux_heater_device.py +++ b/custom_components/dual_smart_thermostat/hvac_device/heater_aux_heater_device.py @@ -22,6 +22,9 @@ HVACDevice, merge_hvac_modes, ) +from custom_components.dual_smart_thermostat.managers.feature_manager import ( + FeatureManager, +) from custom_components.dual_smart_thermostat.managers.opening_manager import ( OpeningManager, ) @@ -46,7 +49,7 @@ def __init__( initial_hvac_mode: HVACMode, temperatures: TemperatureManager, openings: OpeningManager, - range_mode: bool = False, + features: FeatureManager, ) -> None: super().__init__(hass, temperatures, openings) @@ -56,7 +59,7 @@ def __init__( self._aux_heater_timeout = aux_heater_timeout self._aux_heater_dual_mode = aux_heater_dual_mode - if range_mode: + if features.is_range_mode: self._target_temp_attr = "_target_temp_low" self._aux_heater_last_run: datetime = None diff --git a/custom_components/dual_smart_thermostat/hvac_device/heater_device.py b/custom_components/dual_smart_thermostat/hvac_device/heater_device.py index 23041d7..c389539 100644 --- a/custom_components/dual_smart_thermostat/hvac_device/heater_device.py +++ b/custom_components/dual_smart_thermostat/hvac_device/heater_device.py @@ -10,6 +10,9 @@ from custom_components.dual_smart_thermostat.hvac_device.specific_hvac_device import ( SpecificHVACDevice, ) +from custom_components.dual_smart_thermostat.managers.feature_manager import ( + FeatureManager, +) from custom_components.dual_smart_thermostat.managers.opening_manager import ( OpeningManager, ) @@ -32,7 +35,7 @@ def __init__( initial_hvac_mode: HVACMode, temperatures: TemperatureManager, openings: OpeningManager, - range_mode: bool = False, + features: FeatureManager, ) -> None: super().__init__( hass, @@ -41,10 +44,16 @@ def __init__( initial_hvac_mode, temperatures, openings, + features, ) - if range_mode: - self._target_temp_attr = "_target_temp_low" + @property + def target_temp_attr(self) -> str: + return ( + "_target_temp_low" + if self.features.is_range_mode + else self._target_temp_attr + ) @property def hvac_action(self) -> HVACAction: @@ -72,7 +81,7 @@ async def async_control_hvac(self, time=None, force=False): async def _async_control_device_when_on(self, time=None) -> None: """Check if we need to turn heating on or off when theheater is on.""" - too_hot = self.temperatures.is_too_hot(self._target_temp_attr) + too_hot = self.temperatures.is_too_hot(self.target_temp_attr) is_floor_hot = self.temperatures.is_floor_hot is_floor_cold = self.temperatures.is_floor_cold any_opening_open = self.openings.any_opening_open(self.hvac_mode) @@ -104,7 +113,7 @@ async def _async_control_device_when_off(self, time=None) -> None: """Check if we need to turn heating on or off when the heater is off.""" _LOGGER.debug("%s _async_control_device_when_off", self.__class__.__name__) - too_cold = self.temperatures.is_too_cold(self._target_temp_attr) + too_cold = self.temperatures.is_too_cold(self.target_temp_attr) is_floor_hot = self.temperatures.is_floor_hot is_floor_cold = self.temperatures.is_floor_cold any_opening_open = self.openings.any_opening_open(self.hvac_mode) diff --git a/custom_components/dual_smart_thermostat/hvac_device/hvac_device.py b/custom_components/dual_smart_thermostat/hvac_device/hvac_device.py index 65b48a5..f4fd399 100644 --- a/custom_components/dual_smart_thermostat/hvac_device/hvac_device.py +++ b/custom_components/dual_smart_thermostat/hvac_device/hvac_device.py @@ -28,6 +28,16 @@ async def async_turn_off(self): pass +class CanTargetTemperature(ABC): + + _target_temp_attr: str = "_target_temp" + + @property + @abstractmethod + def target_temp_attr(self) -> str: + pass + + class HVACDevice: _active: bool diff --git a/custom_components/dual_smart_thermostat/hvac_device/hvac_device_factory.py b/custom_components/dual_smart_thermostat/hvac_device/hvac_device_factory.py index 738415c..ee40026 100644 --- a/custom_components/dual_smart_thermostat/hvac_device/hvac_device_factory.py +++ b/custom_components/dual_smart_thermostat/hvac_device/hvac_device_factory.py @@ -96,6 +96,7 @@ def create_device( self._initial_hvac_mode, temperatures, openings, + self._features, ) if ( @@ -114,6 +115,7 @@ def create_device( self._initial_hvac_mode, temperatures, openings, + self._features, ) elif ( @@ -127,6 +129,7 @@ def create_device( None, temperatures, openings, + self._features, ) fan_device = FanDevice( self.hass, @@ -135,6 +138,7 @@ def create_device( None, temperatures, openings, + self._features, ) _LOGGER.info( @@ -150,7 +154,7 @@ def create_device( self._initial_hvac_mode, temperatures, openings, - fan_on_with_cooler=self._fan_on_with_cooler, + self._features, ) elif self._features.is_configured_for_dual_mode: @@ -168,6 +172,7 @@ def _create_heater_device( self._initial_hvac_mode, temperatures, openings, + self._features, ) if self._features.is_configured_for_aux_heating_mode: aux_heater_device = HeaterDevice( @@ -177,6 +182,7 @@ def _create_heater_device( self._initial_hvac_mode, temperatures, openings, + self._features, ) return HeaterAUXHeaterDevice( self.hass, @@ -187,6 +193,7 @@ def _create_heater_device( self._initial_hvac_mode, temperatures, openings, + self._features, ) _LOGGER.info( "Creating HeaterDevice device, _is_configured_for_aux_heating_mode: %s", @@ -204,7 +211,7 @@ def _create_heat_cool_device( self._initial_hvac_mode, temperatures, openings, - range_mode=self._features.is_configured_for_heat_cool_mode, + self._features, ) if self._features.is_configured_for_aux_heating_mode: @@ -215,7 +222,7 @@ def _create_heat_cool_device( self._initial_hvac_mode, temperatures, openings, - range_mode=self._features.is_configured_for_heat_cool_mode, + self._features, ) return HeaterAUXHeaterDevice( self.hass, @@ -226,6 +233,7 @@ def _create_heat_cool_device( self._initial_hvac_mode, temperatures, openings, + self._features, ) cooler_device = CoolerDevice( @@ -235,7 +243,7 @@ def _create_heat_cool_device( self._initial_hvac_mode, temperatures, openings, - range_mode=self._features.is_configured_for_heat_cool_mode, + self._features, ) cooler_fan_device = None @@ -248,7 +256,7 @@ def _create_heat_cool_device( self._initial_hvac_mode, temperatures, openings, - range_mode=self._features.is_configured_for_heat_cool_mode, + self._features, ) cooler_fan_device = CoolerFanDevice( self.hass, @@ -257,7 +265,7 @@ def _create_heat_cool_device( self._initial_hvac_mode, temperatures, openings, - fan_on_with_cooler=self._fan_on_with_cooler, + self._features, ) _LOGGER.info( diff --git a/custom_components/dual_smart_thermostat/hvac_device/specific_hvac_device.py b/custom_components/dual_smart_thermostat/hvac_device/specific_hvac_device.py index 47000ef..ea61a12 100644 --- a/custom_components/dual_smart_thermostat/hvac_device/specific_hvac_device.py +++ b/custom_components/dual_smart_thermostat/hvac_device/specific_hvac_device.py @@ -22,9 +22,13 @@ ControlableHVACDevice, ) from custom_components.dual_smart_thermostat.hvac_device.hvac_device import ( + CanTargetTemperature, HVACDevice, Switchable, ) +from custom_components.dual_smart_thermostat.managers.feature_manager import ( + FeatureManager, +) from custom_components.dual_smart_thermostat.managers.opening_manager import ( OpeningManager, ) @@ -35,7 +39,9 @@ _LOGGER = logging.getLogger(__name__) -class SpecificHVACDevice(HVACDevice, ControlableHVACDevice, Switchable): +class SpecificHVACDevice( + HVACDevice, ControlableHVACDevice, Switchable, CanTargetTemperature +): _target_temp_attr: str = "_target_temp" @@ -47,9 +53,11 @@ def __init__( initial_hvac_mode: HVACMode, temperatures: TemperatureManager, openings: OpeningManager, + features: FeatureManager, ) -> None: super().__init__(hass, temperatures, openings) self._device_type = self.__class__.__name__ + self.features = features self.entity_id = entity_id self.min_cycle_duration = min_cycle_duration @@ -90,6 +98,10 @@ async def async_turn_off(self): HA_DOMAIN, SERVICE_TURN_OFF, data, context=self._context ) + @property + def target_temp_attr(self) -> str: + return self._target_temp_attr + @property def is_active(self) -> bool: """If the toggleable cooler device is currently active.""" @@ -124,8 +136,8 @@ def _set_self_active(self) -> None: """Checks if active state needs to be set true.""" _LOGGER.debug("_active: %s", self._active) _LOGGER.debug("cur_temp: %s", self.temperatures.cur_temp) - _LOGGER.debug("_target_temp_attr: %s", self._target_temp_attr) - target_temp = getattr(self.temperatures, self._target_temp_attr) + _LOGGER.debug("target_temp_attr: %s", self.target_temp_attr) + target_temp = getattr(self.temperatures, self.target_temp_attr) _LOGGER.debug("target_temp: %s", target_temp) if ( @@ -194,7 +206,7 @@ async def async_control_hvac(self, time=None, force=False): async def _async_control_when_active(self, time=None) -> None: _LOGGER.debug("%s _async_control_when_active", self.__class__.__name__) - too_cold = self.temperatures.is_too_cold(self._target_temp_attr) + too_cold = self.temperatures.is_too_cold(self.target_temp_attr) any_opening_open = self.openings.any_opening_open(self.hvac_mode) if too_cold or any_opening_open: @@ -215,7 +227,7 @@ async def _async_control_when_active(self, time=None) -> None: self._hvac_action_reason = HVACActionReason.TARGET_TEMP_NOT_REACHED async def _async_control_when_inactive(self, time=None) -> None: - too_hot = self.temperatures.is_too_hot(self._target_temp_attr) + too_hot = self.temperatures.is_too_hot(self.target_temp_attr) any_opening_open = self.openings.any_opening_open(self.hvac_mode) _LOGGER.debug("too_hot: %s", too_hot) diff --git a/custom_components/dual_smart_thermostat/managers/feature_manager.py b/custom_components/dual_smart_thermostat/managers/feature_manager.py index e6a3146..86361dd 100644 --- a/custom_components/dual_smart_thermostat/managers/feature_manager.py +++ b/custom_components/dual_smart_thermostat/managers/feature_manager.py @@ -133,6 +133,11 @@ def is_configured_for_fan_only_mode(self) -> bool: and self._fan_entity_id is None ) + @property + def is_configured_for_fan_on_with_cooler(self) -> bool: + """Determines if the fan mode with cooler is configured.""" + return self._fan_on_with_cooler + def set_support_flags( self, presets: dict[str, Any], diff --git a/custom_components/dual_smart_thermostat/managers/temperature_manager.py b/custom_components/dual_smart_thermostat/managers/temperature_manager.py index 6b9ccb9..855b5e8 100644 --- a/custom_components/dual_smart_thermostat/managers/temperature_manager.py +++ b/custom_components/dual_smart_thermostat/managers/temperature_manager.py @@ -227,7 +227,6 @@ def set_temperature_range( self._target_temp_low = temp_low self._target_temp_high = temp_high - @property def is_within_fan_tolerance(self, target_attr="_target_temp") -> bool: """Checks if the current temperature is below target.""" if self._cur_temp is None or self._fan_hot_tolerance is None: @@ -256,7 +255,12 @@ def is_too_cold(self, target_attr="_target_temp") -> bool: if self._cur_temp is None: return False target_temp = getattr(self, target_attr) - _LOGGER.debug("Target temp: %s, current temp: %s", target_temp, self._cur_temp) + _LOGGER.debug( + "Target temp atte: %s, Target temp: %s, current temp: %s", + target_attr, + target_temp, + self._cur_temp, + ) return target_temp >= self._cur_temp + self._cold_tolerance def is_too_hot(self, target_attr="_target_temp") -> bool: diff --git a/tests/__init__.py b/tests/__init__.py index 95cc679..7e2b884 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -626,6 +626,61 @@ async def setup_comp_heat_cool_fan_config(hass: HomeAssistant) -> None: await hass.async_block_till_done() +@pytest.fixture +async def setup_comp_heat_cool_fan_config_tolerance(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, + "heat_cool_mode": True, + "heater": common.ENT_HEATER, + "cooler": common.ENT_COOLER, + "fan": common.ENT_FAN, + "fan_hot_tolerance": 1, + "target_sensor": common.ENT_SENSOR, + "initial_hvac_mode": HVACMode.HEAT_COOL, + } + }, + ) + await hass.async_block_till_done() + + +@pytest.fixture +async def setup_comp_heat_cool_fan_config_2(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, + "heater": common.ENT_HEATER, + "cooler": common.ENT_COOLER, + "fan": common.ENT_FAN, + "target_sensor": common.ENT_SENSOR, + "initial_hvac_mode": HVACMode.HEAT_COOL, + "min_temp": 9, + "max_temp": 32, + "target_temp": 19.5, + "target_temp_high": 20.5, + "target_temp_low": 19.5, + } + }, + ) + await hass.async_block_till_done() + + @pytest.fixture async def setup_comp_dual_presets(hass: HomeAssistant) -> None: """Initialize components.""" @@ -790,6 +845,26 @@ def log_call(call) -> None: return calls +def setup_switch_heat_cool_fan( + hass: HomeAssistant, is_on: bool, is_cooler_on: bool, is_fan_on: bool +) -> None: + """Set up the test switch.""" + hass.states.async_set(common.ENT_SWITCH, STATE_ON if is_on else STATE_OFF) + hass.states.async_set(common.ENT_COOLER, STATE_ON if is_cooler_on else STATE_OFF) + hass.states.async_set(common.ENT_FAN, STATE_ON if is_fan_on else STATE_OFF) + calls = [] + + @callback + def log_call(call) -> None: + """Log service calls.""" + calls.append(call) + + hass.services.async_register(ha.DOMAIN, SERVICE_TURN_ON, log_call) + hass.services.async_register(ha.DOMAIN, SERVICE_TURN_OFF, log_call) + + return calls + + def setup_fan(hass: HomeAssistant, is_on: bool) -> None: """Set up the test switch.""" hass.states.async_set(common.ENT_FAN, STATE_ON if is_on else STATE_OFF) diff --git a/tests/test_dual_mode.py b/tests/test_dual_mode.py index 603ce6a..19fcfe3 100644 --- a/tests/test_dual_mode.py +++ b/tests/test_dual_mode.py @@ -52,9 +52,12 @@ setup_comp_heat_cool_1, setup_comp_heat_cool_2, setup_comp_heat_cool_fan_config, + setup_comp_heat_cool_fan_config_2, setup_comp_heat_cool_presets, setup_floor_sensor, setup_sensor, + setup_switch_dual, + setup_switch_heat_cool_fan, ) COLD_TOLERANCE = 0.3