From 9e86f82a1b42c4ff82f45f19ce357b2e8b3dee01 Mon Sep 17 00:00:00 2001 From: alexsydell <3269831+alexsydell@users.noreply.github.com> Date: Fri, 22 Mar 2024 15:45:54 -0600 Subject: [PATCH] Add ecobee indefinite away preset, remove unusable/broken presets (#108636) * Add ecobee indefinite away preset, remove unusable/broken presets * Revert cleanup of presets which no longer work --- homeassistant/components/ecobee/climate.py | 16 +++++-- homeassistant/components/ecobee/strings.json | 11 +++++ homeassistant/components/ecobee/util.py | 12 +++++- tests/components/ecobee/__init__.py | 12 ++++-- .../ecobee/fixtures/ecobee-data.json | 12 ++++-- tests/components/ecobee/test_climate.py | 42 +++++++++++++++++-- 6 files changed, 88 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/ecobee/climate.py b/homeassistant/components/ecobee/climate.py index cfb73153f45..3f871dd20dd 100644 --- a/homeassistant/components/ecobee/climate.py +++ b/homeassistant/components/ecobee/climate.py @@ -12,7 +12,6 @@ from homeassistant.components.climate import ( ATTR_TARGET_TEMP_LOW, FAN_AUTO, FAN_ON, - PRESET_AWAY, PRESET_NONE, ClimateEntity, ClimateEntityFeature, @@ -38,7 +37,7 @@ from homeassistant.util.unit_conversion import TemperatureConverter from . import EcobeeData from .const import _LOGGER, DOMAIN, ECOBEE_MODEL_TO_NAME, MANUFACTURER -from .util import ecobee_date, ecobee_time +from .util import ecobee_date, ecobee_time, is_indefinite_hold ATTR_COOL_TEMP = "cool_temp" ATTR_END_DATE = "end_date" @@ -56,6 +55,7 @@ ATTR_AUTO_AWAY = "auto_away" ATTR_FOLLOW_ME = "follow_me" DEFAULT_RESUME_ALL = False +PRESET_AWAY_INDEFINITELY = "away_indefinitely" PRESET_TEMPERATURE = "temp" PRESET_VACATION = "vacation" PRESET_HOLD_NEXT_TRANSITION = "next_transition" @@ -325,6 +325,7 @@ class Thermostat(ClimateEntity): _attr_name = None _attr_has_entity_name = True _enable_turn_on_off_backwards_compatibility = False + _attr_translation_key = "ecobee" def __init__( self, data: EcobeeData, thermostat_index: int, thermostat: dict @@ -481,6 +482,11 @@ class Thermostat(ClimateEntity): continue if event["type"] == "hold": + if event["holdClimateRef"] == "away" and is_indefinite_hold( + event["startDate"], event["endDate"] + ): + return PRESET_AWAY_INDEFINITELY + if event["holdClimateRef"] in self._preset_modes: return self._preset_modes[event["holdClimateRef"]] @@ -577,7 +583,7 @@ class Thermostat(ClimateEntity): if self.preset_mode == PRESET_VACATION: self.data.ecobee.delete_vacation(self.thermostat_index, self.vacation) - if preset_mode == PRESET_AWAY: + if preset_mode == PRESET_AWAY_INDEFINITELY: self.data.ecobee.set_climate_hold( self.thermostat_index, "away", "indefinite", self.hold_hours() ) @@ -625,7 +631,9 @@ class Thermostat(ClimateEntity): @property def preset_modes(self): """Return available preset modes.""" - return list(self._preset_modes.values()) + # Return presets provided by the ecobee API, and an indefinite away + # preset which we handle separately in set_preset_mode(). + return [*self._preset_modes.values(), PRESET_AWAY_INDEFINITELY] def set_auto_temp_hold(self, heat_temp, cool_temp): """Set temperature hold in auto mode.""" diff --git a/homeassistant/components/ecobee/strings.json b/homeassistant/components/ecobee/strings.json index 484d5bf1e1e..b1d1df65417 100644 --- a/homeassistant/components/ecobee/strings.json +++ b/homeassistant/components/ecobee/strings.json @@ -20,6 +20,17 @@ } }, "entity": { + "climate": { + "ecobee": { + "state_attributes": { + "preset_mode": { + "state": { + "away_indefinitely": "Away Indefinitely" + } + } + } + } + }, "number": { "ventilator_min_type_home": { "name": "Ventilator min time home" diff --git a/homeassistant/components/ecobee/util.py b/homeassistant/components/ecobee/util.py index 1eba02f9795..e2e607c84d0 100644 --- a/homeassistant/components/ecobee/util.py +++ b/homeassistant/components/ecobee/util.py @@ -1,6 +1,6 @@ """Validation utility functions for ecobee services.""" -from datetime import datetime +from datetime import date, datetime, timedelta import voluptuous as vol @@ -23,3 +23,13 @@ def ecobee_time(time_string): "Time does not match ecobee 24-hour time format HH:MM:SS" ) from err return time_string + + +def is_indefinite_hold(start_date_string: str, end_date_string: str) -> bool: + """Determine if the given start and end dates from the ecobee API represent an indefinite hold. + + This is not documented in the API, so a rough heuristic is used where a hold over 1 year is considered indefinite. + """ + return date.fromisoformat(end_date_string) - date.fromisoformat( + start_date_string + ) > timedelta(days=365) diff --git a/tests/components/ecobee/__init__.py b/tests/components/ecobee/__init__.py index 3dba80090d4..52c6fcc6a4e 100644 --- a/tests/components/ecobee/__init__.py +++ b/tests/components/ecobee/__init__.py @@ -39,8 +39,10 @@ GENERIC_THERMOSTAT_INFO = { "running": True, "type": "hold", "holdClimateRef": "away", - "endDate": "2022-01-01 10:00:00", - "startDate": "2022-02-02 11:00:00", + "startDate": "2022-02-02", + "startTime": "11:00:00", + "endDate": "2022-01-01", + "endTime": "10:00:00", } ], "remoteSensors": [ @@ -99,8 +101,10 @@ GENERIC_THERMOSTAT_INFO_WITH_HEATPUMP = { "running": True, "type": "hold", "holdClimateRef": "away", - "endDate": "2022-01-01 10:00:00", - "startDate": "2022-02-02 11:00:00", + "startDate": "2022-02-02", + "startTime": "11:00:00", + "endDate": "2022-01-01", + "endTime": "10:00:00", } ], "remoteSensors": [ diff --git a/tests/components/ecobee/fixtures/ecobee-data.json b/tests/components/ecobee/fixtures/ecobee-data.json index 9fe19af35c6..d9406c20c3b 100644 --- a/tests/components/ecobee/fixtures/ecobee-data.json +++ b/tests/components/ecobee/fixtures/ecobee-data.json @@ -42,8 +42,10 @@ "running": true, "type": "hold", "holdClimateRef": "away", - "endDate": "2022-01-01 10:00:00", - "startDate": "2022-02-02 11:00:00" + "startDate": "2022-02-02", + "startTime": "11:00:00", + "endDate": "2022-01-01", + "endTime": "10:00:00" } ], "remoteSensors": [ @@ -110,8 +112,10 @@ "running": true, "type": "hold", "holdClimateRef": "away", - "endDate": "2022-01-01 10:00:00", - "startDate": "2022-02-02 11:00:00" + "startDate": "2022-02-02", + "startTime": "11:00:00", + "endDate": "2022-01-01", + "endTime": "10:00:00" } ], "remoteSensors": [ diff --git a/tests/components/ecobee/test_climate.py b/tests/components/ecobee/test_climate.py index 7b9a31739dc..0ec4f9cee68 100644 --- a/tests/components/ecobee/test_climate.py +++ b/tests/components/ecobee/test_climate.py @@ -9,7 +9,11 @@ import pytest from homeassistant.components import climate from homeassistant.components.climate import ClimateEntityFeature -from homeassistant.components.ecobee.climate import ECOBEE_AUX_HEAT_ONLY, Thermostat +from homeassistant.components.ecobee.climate import ( + ECOBEE_AUX_HEAT_ONLY, + PRESET_AWAY_INDEFINITELY, + Thermostat, +) import homeassistant.const as const from homeassistant.const import ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, STATE_OFF from homeassistant.core import HomeAssistant @@ -31,6 +35,7 @@ def ecobee_fixture(): "climates": [ {"name": "Climate1", "climateRef": "c1"}, {"name": "Climate2", "climateRef": "c2"}, + {"name": "Away", "climateRef": "away"}, ], "currentClimateRef": "c1", }, @@ -56,9 +61,11 @@ def ecobee_fixture(): "name": "Event1", "running": True, "type": "hold", - "holdClimateRef": "away", - "endDate": "2017-01-01 10:00:00", - "startDate": "2017-02-02 11:00:00", + "holdClimateRef": "c1", + "startDate": "2017-02-02", + "startTime": "11:00:00", + "endDate": "2017-01-01", + "endTime": "10:00:00", } ], } @@ -428,3 +435,30 @@ async def test_turn_aux_heat_off(hass: HomeAssistant, mock_ecobee: MagicMock) -> ) assert mock_ecobee.set_hvac_mode.call_count == 1 assert mock_ecobee.set_hvac_mode.call_args == mock.call(0, "auto") + + +async def test_preset_indefinite_away(ecobee_fixture, thermostat) -> None: + """Test indefinite away showing correctly, and not as temporary away.""" + ecobee_fixture["program"]["currentClimateRef"] = "away" + ecobee_fixture["events"][0]["holdClimateRef"] = "away" + assert thermostat.preset_mode == "Away" + + ecobee_fixture["events"][0]["endDate"] = "2999-01-01" + assert thermostat.preset_mode == PRESET_AWAY_INDEFINITELY + + +async def test_set_preset_mode(ecobee_fixture, thermostat, data) -> None: + """Test set preset mode.""" + # Set a preset provided by ecobee. + data.reset_mock() + thermostat.set_preset_mode("Climate2") + data.ecobee.set_climate_hold.assert_has_calls( + [mock.call(1, "c2", thermostat.hold_preference(), thermostat.hold_hours())] + ) + + # Set the indefinite away preset provided by this integration. + data.reset_mock() + thermostat.set_preset_mode(PRESET_AWAY_INDEFINITELY) + data.ecobee.set_climate_hold.assert_has_calls( + [mock.call(1, "away", "indefinite", thermostat.hold_hours())] + )