diff --git a/README.md b/README.md index 89170dc..908d68e 100644 --- a/README.md +++ b/README.md @@ -251,9 +251,19 @@ Currrnetly supported presets are: To set presets you need to add entries for them in the configuration file like this: +You have 4 options here: + +1. Set the `temperature` for heat, cool or fan-only mode +2. Set the `target_temp_low` and `target_temp_high` for heat_cool mode. If `temperature` is not set but `target_temp_low` and `target_temp_high` are set, the `temperature` will be picked based on hvac mode. For heat mode it will be `target_temp_low` and for cool, fan_only mode it will be `target_temp_high` +3. Set the `humidity` for dry mode +4. Set all above + +### Presets Configuration + ```yaml preset_name: temperature: 13 + humidity: 50 # <-- only if dry mode configured target_temp_low: 12 target_temp_high: 14 ``` diff --git a/config/configuration.yaml b/config/configuration.yaml index c4a43b2..bb14458 100644 --- a/config/configuration.yaml +++ b/config/configuration.yaml @@ -373,6 +373,14 @@ climate: target_temp: 20 cold_tolerance: 0.3 hot_tolerance: 0.3 + away: + target_temp_high: 30 + target_temp_low: 23 + humidity: 55 + sleep: + target_temp_high: 26 + target_temp_low: 18 + humidity: 60 # - platform: dual_smart_thermostat # name: AUX Heat Room diff --git a/custom_components/dual_smart_thermostat/climate.py b/custom_components/dual_smart_thermostat/climate.py index a6ed10f..6693a28 100644 --- a/custom_components/dual_smart_thermostat/climate.py +++ b/custom_components/dual_smart_thermostat/climate.py @@ -264,27 +264,6 @@ async def async_setup_platform( sensor_humidity_entity_id = config.get(CONF_HUMIDITY_SENSOR) sensor_stale_duration: timedelta | None = config.get(CONF_STALE_DURATION) keep_alive = config.get(CONF_KEEP_ALIVE) - presets_dict = { - key: config[value] for key, value in CONF_PRESETS.items() if value in config - } - _LOGGER.debug("Presets dict: %s", presets_dict) - presets = { - key: values[ATTR_TEMPERATURE] - for key, values in presets_dict.items() - if ATTR_TEMPERATURE in values - } - _LOGGER.debug("Presets: %s", presets) - - # Try to load presets in old format and use if new format not available in config - old_presets = {k: config[v] for k, v in CONF_PRESETS_OLD.items() if v in config} - if old_presets: - _LOGGER.warning( - "Found deprecated presets settings in configuration. " - "Please remove and replace with new presets settings format. " - "Read documentation in integration repository for more details" - ) - if not presets_dict: - presets = old_presets precision = config.get(CONF_PRECISION) unit = hass.config.units.temperature_unit @@ -295,7 +274,6 @@ async def async_setup_platform( environment_manager = EnvironmentManager( hass, config, - presets, ) feature_manager = FeatureManager(hass, config, environment_manager) @@ -779,7 +757,6 @@ def _set_support_flags(self) -> None: self.presets.presets, self.presets.presets_range, self.presets.preset_mode, - self.hvac_device.hvac_modes, self._hvac_mode, ) self._attr_supported_features = self.features.supported_features @@ -1145,7 +1122,7 @@ def _is_device_active(self) -> bool: return self.hvac_device.is_active async def async_set_preset_mode(self, preset_mode: str) -> None: - self.presets.set_preset_mode(preset_mode) + self.presets.set_preset_mode(preset_mode, self.hvac_device.hvac_mode) self._attr_preset_mode = self.presets.preset_mode await self._async_control_climate(force=True) diff --git a/custom_components/dual_smart_thermostat/managers/environment_manager.py b/custom_components/dual_smart_thermostat/managers/environment_manager.py index 495b04b..cdc0411 100644 --- a/custom_components/dual_smart_thermostat/managers/environment_manager.py +++ b/custom_components/dual_smart_thermostat/managers/environment_manager.py @@ -9,7 +9,7 @@ DEFAULT_MAX_TEMP, DEFAULT_MIN_TEMP, ) -from homeassistant.components.climate.const import HVACMode +from homeassistant.components.climate.const import PRESET_NONE, HVACMode from homeassistant.const import ( ATTR_TEMPERATURE, PRECISION_WHOLE, @@ -69,12 +69,7 @@ def __init__(self, temperature: float, temp_high: float, temp_low: float) -> Non class EnvironmentManager(StateManager): """Class to manage the temperatures of the thermostat.""" - def __init__( - self, - hass: HomeAssistant, - config: ConfigType, - presets: dict[str, Any], - ): + def __init__(self, hass: HomeAssistant, config: ConfigType): self.hass = hass self._sensor_floor = config.get(CONF_FLOOR_SENSOR) self._sensor = config.get(CONF_SENSOR) @@ -102,7 +97,7 @@ def __init__( self._hot_tolerance = config.get(CONF_HOT_TOLERANCE) self._fan_hot_tolerance = config.get(CONF_FAN_HOT_TOLERANCE) - self._saved_target_temp = self.target_temp or next(iter(presets.values()), None) + self._saved_target_temp = self.target_temp or None self._saved_target_temp_low = None self._saved_target_temp_high = None self._temp_precision = config.get(CONF_PRECISION) @@ -581,6 +576,27 @@ def _set_default_temps_range_mode(self) -> None: else: self._target_temp_high += PRECISION_WHOLE + def set_temepratures_from_hvac_mode_and_presets( + self, hvac_mode: HVACMode, preset_mode: str, presets_range: dict[str, Any] + ) -> None: + if preset_mode is None or preset_mode is PRESET_NONE: + return + + _LOGGER.debug( + "Setting temperatures from hvac mode and presets: %s, %s, %s", + hvac_mode, + preset_mode, + presets_range, + ) + + if hvac_mode == HVACMode.HEAT and presets_range[preset_mode][0] is not None: + self._target_temp = presets_range[preset_mode][0] + elif ( + hvac_mode in [HVACMode.COOL, HVACMode.FAN_ONLY] + and presets_range[preset_mode][0] is not None + ): + self._target_temp = presets_range[preset_mode][1] + def apply_old_state(self, old_state: State) -> None: _LOGGER.debug("Applying old state: %s", old_state) if old_state is None: diff --git a/custom_components/dual_smart_thermostat/managers/feature_manager.py b/custom_components/dual_smart_thermostat/managers/feature_manager.py index 5e193d1..3899059 100644 --- a/custom_components/dual_smart_thermostat/managers/feature_manager.py +++ b/custom_components/dual_smart_thermostat/managers/feature_manager.py @@ -174,7 +174,6 @@ def set_support_flags( presets: dict[str, Any], presets_range, preset_mode: str, - hvac_modes: list[HVACMode], current_hvac_mode: HVACMode = None, ) -> None: """Set the correct support flags based on configuration.""" @@ -185,14 +184,19 @@ def set_support_flags( HVACMode.FAN_ONLY, HVACMode.HEAT, ): - if self.is_range_mode and preset_mode != PRESET_NONE: - self.environment.set_temperature_range_from_saved() self._supported_features = ( self._default_support_flags | ClimateEntityFeature.TARGET_TEMPERATURE ) if len(presets): - _LOGGER.debug("Setting support flags to %s", self._supported_features) + _LOGGER.debug( + "Setting support target mode flags to %s", self._supported_features + ) self._supported_features |= ClimateEntityFeature.PRESET_MODE + + self.environment.set_temepratures_from_hvac_mode_and_presets( + current_hvac_mode, preset_mode, presets_range + ) + elif current_hvac_mode == HVACMode.DRY: self._supported_features = ( self._default_support_flags | ClimateEntityFeature.TARGET_HUMIDITY @@ -213,12 +217,13 @@ def set_support_flags( "Setting support flags presets in range mode to %s", self._supported_features, ) - self.environment.set_default_target_temps( - self.is_target_mode, self.is_range_mode, current_hvac_mode - ) - if self.is_configured_for_dryer_mode: + if preset_mode == PRESET_NONE: + self.environment.set_default_target_temps( + self.is_target_mode, self.is_range_mode, current_hvac_mode + ) + if self.is_configured_for_dryer_mode: self._supported_features |= ClimateEntityFeature.TARGET_HUMIDITY def apply_old_state(self, old_state: State, hvac_mode, presets_range) -> None: diff --git a/custom_components/dual_smart_thermostat/managers/preset_manager.py b/custom_components/dual_smart_thermostat/managers/preset_manager.py index 8476df3..93b1111 100644 --- a/custom_components/dual_smart_thermostat/managers/preset_manager.py +++ b/custom_components/dual_smart_thermostat/managers/preset_manager.py @@ -5,6 +5,7 @@ ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, PRESET_NONE, + HVACMode, ) from homeassistant.components.humidifier import ATTR_HUMIDITY from homeassistant.const import ATTR_TEMPERATURE @@ -93,6 +94,11 @@ def __init__( set(self._preset_range_modes) - set(self._preset_modes) ) + # sets the target environment to the preset mode + self._environment.saved_target_temp = self._environment.target_temp or next( + iter(presets.values()), None + ) + @property def presets(self): return self._presets @@ -113,7 +119,7 @@ def preset_mode(self): def has_presets(self): return len(self.presets) > 0 - def set_preset_mode(self, preset_mode: str) -> None: + def set_preset_mode(self, preset_mode: str, hvac_mode: HVACMode) -> None: """Set new preset mode.""" _LOGGER.debug("Setting preset mode: %s", preset_mode) if preset_mode not in (self.preset_modes or []): @@ -127,7 +133,7 @@ def set_preset_mode(self, preset_mode: str) -> None: if preset_mode == PRESET_NONE: self._set_presets_when_no_preset_mode() else: - self._set_presets_when_have_preset_mode(preset_mode) + self._set_presets_when_have_preset_mode(preset_mode, hvac_mode) def _set_presets_when_no_preset_mode(self): """Sets target environment when preset is none.""" @@ -150,7 +156,7 @@ def _set_presets_when_no_preset_mode(self): if self._environment.saved_target_humidity: self._environment.target_humidity = self._environment.saved_target_humidity - def _set_presets_when_have_preset_mode(self, preset_mode: str): + def _set_presets_when_have_preset_mode(self, preset_mode: str, hvac_mode: HVACMode): """Sets target temperatures when have preset is not none.""" _LOGGER.debug("Setting presets when have preset mode") if self._features.is_range_mode: @@ -166,7 +172,16 @@ def _set_presets_when_have_preset_mode(self, preset_mode: str): else: if self._preset_mode == PRESET_NONE: self._environment.saved_target_temp = self._environment.target_temp - self._environment.target_temp = self._presets[preset_mode][ATTR_TEMPERATURE] + # handles when temperature is set in preset + if self._presets[preset_mode].get(ATTR_TEMPERATURE) is not None: + self._environment.target_temp = self._presets[preset_mode][ + ATTR_TEMPERATURE + ] + # handles when temperature is not set in preset but temp range is set + else: + self._environment.set_temepratures_from_hvac_mode_and_presets( + hvac_mode, preset_mode, self._presets_range + ) if self._features.is_configured_for_dryer_mode: if self._preset_mode == PRESET_NONE: diff --git a/tests/__init__.py b/tests/__init__.py index 0e44eed..5e5f814 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -832,6 +832,58 @@ async def setup_comp_heat_cool_presets(hass: HomeAssistant) -> None: await hass.async_block_till_done() +@pytest.fixture +async def setup_comp_heat_cool_presets_range_only(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, + "target_sensor": common.ENT_SENSOR, + "initial_hvac_mode": HVACMode.HEAT_COOL, + PRESET_AWAY: { + "target_temp_low": 16, + "target_temp_high": 30, + }, + PRESET_COMFORT: { + "target_temp_low": 20, + "target_temp_high": 27, + }, + PRESET_ECO: { + "target_temp_low": 18, + "target_temp_high": 29, + }, + PRESET_HOME: { + "target_temp_low": 19, + "target_temp_high": 23, + }, + PRESET_SLEEP: { + "target_temp_low": 17, + "target_temp_high": 24, + }, + PRESET_ACTIVITY: { + "target_temp_low": 21, + "target_temp_high": 28, + }, + "anti_freeze": { + "target_temp_low": 5, + "target_temp_high": 32, + }, + } + }, + ) + await hass.async_block_till_done() + + @pytest.fixture async def setup_comp_heat_cool_safety_delay(hass: HomeAssistant) -> None: """Initialize components.""" diff --git a/tests/test_dual_mode.py b/tests/test_dual_mode.py index 3e02416..ccec4e8 100644 --- a/tests/test_dual_mode.py +++ b/tests/test_dual_mode.py @@ -68,6 +68,7 @@ setup_comp_heat_cool_fan_config_2, setup_comp_heat_cool_fan_presets, setup_comp_heat_cool_presets, + setup_comp_heat_cool_presets_range_only, setup_comp_heat_cool_safety_delay, setup_floor_sensor, setup_humidity_sensor, @@ -1123,6 +1124,102 @@ async def test_heat_cool_set_preset_mode_set_temp_keeps_preset_mode( assert state.attributes.get("target_temp_high") == 22 +@pytest.mark.parametrize( + ("preset", "hvac_mode", "temp"), + [ + (PRESET_AWAY, HVACMode.HEAT, 16), + (PRESET_AWAY, HVACMode.COOL, 30), + (PRESET_COMFORT, HVACMode.HEAT, 20), + (PRESET_COMFORT, HVACMode.COOL, 27), + (PRESET_ECO, HVACMode.HEAT, 18), + (PRESET_ECO, HVACMode.COOL, 29), + (PRESET_HOME, HVACMode.HEAT, 19), + (PRESET_HOME, HVACMode.COOL, 23), + (PRESET_SLEEP, HVACMode.HEAT, 17), + (PRESET_SLEEP, HVACMode.COOL, 24), + (PRESET_ACTIVITY, HVACMode.HEAT, 21), + (PRESET_ACTIVITY, HVACMode.COOL, 28), + (PRESET_ANTI_FREEZE, HVACMode.HEAT, 5), + (PRESET_ANTI_FREEZE, HVACMode.COOL, 32), + ], +) +async def test_heat_cool_set_preset_mode_in_non_range_mode( + hass: HomeAssistant, + setup_comp_heat_cool_presets_range_only, # noqa: F811 + preset, + hvac_mode, + temp, +) -> None: + """Test the setting range preset mode while in target hvac mode""" + + await common.async_set_hvac_mode(hass, hvac_mode) + await hass.async_block_till_done() + + await common.async_set_preset_mode(hass, preset) + await hass.async_block_till_done() + + state = hass.states.get(common.ENTITY) + assert state.state == hvac_mode + assert state.attributes.get("preset_mode") == preset + assert state.attributes.get("temperature") == temp + + +@pytest.mark.parametrize( + ("preset", "temp_low", "temp_high"), + [ + # (PRESET_NONE, 18, 22), + (PRESET_AWAY, 16, 30), + (PRESET_COMFORT, 20, 27), + (PRESET_ECO, 18, 29), + (PRESET_HOME, 19, 23), + (PRESET_SLEEP, 17, 24), + (PRESET_ACTIVITY, 21, 28), + (PRESET_ANTI_FREEZE, 5, 32), + ], +) +async def test_heat_cool_set_preset_mode_auto_targe_temps_if_range_only_presets( + hass: HomeAssistant, + setup_comp_heat_cool_presets_range_only, # noqa: F811 + preset, + temp_low, + temp_high, +) -> None: + """Test the setting preset mode across hvac_modes using range-only preset values. + + Verify preset target temperatures are pcked up while switching hvac_modes. + """ + # starts in heat/cool mode + await common.async_set_hvac_mode(hass, HVACMode.HEAT_COOL) + await common.async_set_preset_mode(hass, preset) + await hass.async_block_till_done() + + state = hass.states.get(common.ENTITY) + assert state.attributes.get("target_temp_low") == temp_low + assert state.attributes.get("target_temp_high") == temp_high + + # verify heat mode picks the low target for target temp + await common.async_set_hvac_mode(hass, HVACMode.HEAT) + await hass.async_block_till_done() + + state = hass.states.get(common.ENTITY) + assert state.attributes.get("temperature") == temp_low + + # verify cool mode picks the high target for target temp + await common.async_set_hvac_mode(hass, HVACMode.COOL) + await hass.async_block_till_done() + + state = hass.states.get(common.ENTITY) + assert state.attributes.get("temperature") == temp_high + + # verify switcing back to heat/cool targets correct temps + await common.async_set_hvac_mode(hass, HVACMode.HEAT_COOL) + await hass.async_block_till_done() + + state = hass.states.get(common.ENTITY) + assert state.attributes.get("target_temp_low") == temp_low + assert state.attributes.get("target_temp_high") == temp_high + + @pytest.mark.parametrize( ("preset", "temp_low", "temp_high"), [ @@ -1179,7 +1276,7 @@ async def test_heat_cool_fan_set_preset_mode_set_temp_keeps_preset_mode( @pytest.mark.parametrize( ("preset", "temp_low", "temp_high"), [ - (PRESET_NONE, 18, 22), + # (PRESET_NONE, 18, 22), (PRESET_AWAY, 16, 30), (PRESET_COMFORT, 20, 27), (PRESET_ECO, 18, 29), @@ -1200,6 +1297,7 @@ async def test_heat_cool_fan_set_preset_mode_change_hvac_mode( Verify preset mode preserved while temperature updated. """ + await common.async_set_temperature(hass, 18, common.ENTITY, 22, 18) await common.async_set_preset_mode(hass, preset) state = hass.states.get(common.ENTITY) @@ -1211,7 +1309,7 @@ async def test_heat_cool_fan_set_preset_mode_change_hvac_mode( state = hass.states.get(common.ENTITY) assert state.attributes.get("preset_mode") == preset - assert state.attributes.get("temperature") == 18 + assert state.attributes.get("temperature") == temp_low assert state.attributes.get("target_temp_low") is None assert state.attributes.get("target_temp_high") is None @@ -1220,7 +1318,7 @@ async def test_heat_cool_fan_set_preset_mode_change_hvac_mode( state = hass.states.get(common.ENTITY) assert state.attributes.get("preset_mode") == preset - assert state.attributes.get("temperature") == 22 + assert state.attributes.get("temperature") == temp_high assert state.attributes.get("target_temp_low") is None assert state.attributes.get("target_temp_high") is None @@ -1229,7 +1327,7 @@ async def test_heat_cool_fan_set_preset_mode_change_hvac_mode( state = hass.states.get(common.ENTITY) assert state.attributes.get("preset_mode") == preset - assert state.attributes.get("temperature") == 22 + assert state.attributes.get("temperature") == temp_high assert state.attributes.get("target_temp_low") is None assert state.attributes.get("target_temp_high") is None