diff --git a/homeassistant/components/ecobee/const.py b/homeassistant/components/ecobee/const.py index 0eed0ab67f9c9..8adc7f9638b6f 100644 --- a/homeassistant/components/ecobee/const.py +++ b/homeassistant/components/ecobee/const.py @@ -49,6 +49,7 @@ Platform.NOTIFY, Platform.NUMBER, Platform.SENSOR, + Platform.SWITCH, Platform.WEATHER, ] diff --git a/homeassistant/components/ecobee/manifest.json b/homeassistant/components/ecobee/manifest.json index 7e46123060046..b11bdf8afb082 100644 --- a/homeassistant/components/ecobee/manifest.json +++ b/homeassistant/components/ecobee/manifest.json @@ -10,7 +10,7 @@ }, "iot_class": "cloud_polling", "loggers": ["pyecobee"], - "requirements": ["python-ecobee-api==0.2.17"], + "requirements": ["python-ecobee-api==0.2.18"], "zeroconf": [ { "type": "_ecobee._tcp.local." diff --git a/homeassistant/components/ecobee/switch.py b/homeassistant/components/ecobee/switch.py new file mode 100644 index 0000000000000..44528a5f4219d --- /dev/null +++ b/homeassistant/components/ecobee/switch.py @@ -0,0 +1,90 @@ +"""Support for using switch with ecobee thermostats.""" + +from __future__ import annotations + +import logging +from typing import Any + +from homeassistant.components.switch import SwitchEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.util import dt as dt_util + +from . import EcobeeData +from .const import DOMAIN +from .entity import EcobeeBaseEntity + +_LOGGER = logging.getLogger(__name__) + +DATE_FORMAT = "%Y-%m-%d %H:%M:%S" + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the ecobee thermostat switch entity.""" + data: EcobeeData = hass.data[DOMAIN] + + async_add_entities( + ( + EcobeeVentilator20MinSwitch(data, index) + for index, thermostat in enumerate(data.ecobee.thermostats) + if thermostat["settings"]["ventilatorType"] != "none" + ), + True, + ) + + +class EcobeeVentilator20MinSwitch(EcobeeBaseEntity, SwitchEntity): + """A Switch class, representing 20 min timer for an ecobee thermostat with ventilator attached.""" + + _attr_has_entity_name = True + _attr_name = "Ventilator 20m Timer" + + def __init__( + self, + data: EcobeeData, + thermostat_index: int, + ) -> None: + """Initialize ecobee ventilator platform.""" + super().__init__(data, thermostat_index) + self._attr_unique_id = f"{self.base_unique_id}_ventilator_20m_timer" + self._attr_is_on = False + self.update_without_throttle = False + self._operating_timezone = dt_util.get_time_zone( + self.thermostat["location"]["timeZone"] + ) + + async def async_update(self) -> None: + """Get the latest state from the thermostat.""" + + if self.update_without_throttle: + await self.data.update(no_throttle=True) + self.update_without_throttle = False + else: + await self.data.update() + + ventilator_off_date_time = self.thermostat["settings"]["ventilatorOffDateTime"] + + self._attr_is_on = ventilator_off_date_time and dt_util.parse_datetime( + ventilator_off_date_time, raise_on_error=True + ).replace(tzinfo=self._operating_timezone) >= dt_util.now( + self._operating_timezone + ) + + async def async_turn_on(self, **kwargs: Any) -> None: + """Set ventilator 20 min timer on.""" + await self.hass.async_add_executor_job( + self.data.ecobee.set_ventilator_timer, self.thermostat_index, True + ) + self.update_without_throttle = True + + async def async_turn_off(self, **kwargs: Any) -> None: + """Set ventilator 20 min timer off.""" + await self.hass.async_add_executor_job( + self.data.ecobee.set_ventilator_timer, self.thermostat_index, False + ) + self.update_without_throttle = True diff --git a/requirements_all.txt b/requirements_all.txt index a9b31fa2f03d9..84e14c33e12ae 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2215,7 +2215,7 @@ python-clementine-remote==1.0.1 python-digitalocean==1.13.2 # homeassistant.components.ecobee -python-ecobee-api==0.2.17 +python-ecobee-api==0.2.18 # homeassistant.components.etherscan python-etherscan-api==0.0.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 938c0705bf90f..0352fb0dac175 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1730,7 +1730,7 @@ python-awair==0.2.4 python-bsblan==0.5.18 # homeassistant.components.ecobee -python-ecobee-api==0.2.17 +python-ecobee-api==0.2.18 # homeassistant.components.fully_kiosk python-fullykiosk==0.0.12 diff --git a/tests/components/ecobee/__init__.py b/tests/components/ecobee/__init__.py index 52c6fcc6a4eb9..f89729df9bb83 100644 --- a/tests/components/ecobee/__init__.py +++ b/tests/components/ecobee/__init__.py @@ -65,6 +65,9 @@ "identifier": 8675309, "name": "ecobee", "modelNumber": "athenaSmart", + "utcTime": "2022-01-01 10:00:00", + "thermostatTime": "2022-01-01 6:00:00", + "location": {"timeZone": "America/Toronto"}, "program": { "climates": [ {"name": "Climate1", "climateRef": "c1"}, @@ -92,7 +95,8 @@ "humidifierMode": "manual", "humidity": "30", "hasHeatPump": True, - "ventilatorType": "none", + "ventilatorType": "hrv", + "ventilatorOffDateTime": "2022-01-01 6:00:00", }, "equipmentStatus": "fan", "events": [ diff --git a/tests/components/ecobee/fixtures/ecobee-data.json b/tests/components/ecobee/fixtures/ecobee-data.json index d8621bd8c4bcd..c86782d9c0bd2 100644 --- a/tests/components/ecobee/fixtures/ecobee-data.json +++ b/tests/components/ecobee/fixtures/ecobee-data.json @@ -4,6 +4,11 @@ "identifier": 8675309, "name": "ecobee", "modelNumber": "athenaSmart", + "utcTime": "2022-01-01 10:00:00", + "thermostatTime": "2022-01-01 6:00:00", + "location": { + "timeZone": "America/Toronto" + }, "program": { "climates": [ { "name": "Climate1", "climateRef": "c1" }, @@ -30,6 +35,7 @@ "ventilatorType": "hrv", "ventilatorMinOnTimeHome": 20, "ventilatorMinOnTimeAway": 10, + "ventilatorOffDateTime": "2022-01-01 6:00:00", "isVentilatorTimerOn": false, "hasHumidifier": true, "humidifierMode": "manual", diff --git a/tests/components/ecobee/test_switch.py b/tests/components/ecobee/test_switch.py new file mode 100644 index 0000000000000..383abf9644cc3 --- /dev/null +++ b/tests/components/ecobee/test_switch.py @@ -0,0 +1,115 @@ +"""The test for the ecobee thermostat switch module.""" + +import copy +from datetime import datetime, timedelta +from unittest import mock +from unittest.mock import patch + +import pytest + +from homeassistant.components.ecobee.switch import DATE_FORMAT +from homeassistant.components.switch import DOMAIN, SERVICE_TURN_OFF, SERVICE_TURN_ON +from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.core import HomeAssistant + +from .common import setup_platform + +from tests.components.ecobee import GENERIC_THERMOSTAT_INFO_WITH_HEATPUMP + +VENTILATOR_20MIN_ID = "switch.ecobee_ventilator_20m_timer" +THERMOSTAT_ID = 0 + + +@pytest.fixture(name="data") +def data_fixture(): + """Set up data mock.""" + data = mock.Mock() + data.return_value = copy.deepcopy(GENERIC_THERMOSTAT_INFO_WITH_HEATPUMP) + return data + + +async def test_ventilator_20min_attributes(hass: HomeAssistant) -> None: + """Test the ventilator switch on home attributes are correct.""" + await setup_platform(hass, DOMAIN) + + state = hass.states.get(VENTILATOR_20MIN_ID) + assert state.state == "off" + + +async def test_ventilator_20min_when_on(hass: HomeAssistant, data) -> None: + """Test the ventilator switch goes on.""" + + data.return_value["settings"]["ventilatorOffDateTime"] = ( + datetime.now() + timedelta(days=1) + ).strftime(DATE_FORMAT) + with mock.patch("pyecobee.Ecobee.get_thermostat", data): + await setup_platform(hass, DOMAIN) + + state = hass.states.get(VENTILATOR_20MIN_ID) + assert state.state == "on" + + data.reset_mock() + + +async def test_ventilator_20min_when_off(hass: HomeAssistant, data) -> None: + """Test the ventilator switch goes on.""" + + data.return_value["settings"]["ventilatorOffDateTime"] = ( + datetime.now() - timedelta(days=1) + ).strftime(DATE_FORMAT) + with mock.patch("pyecobee.Ecobee.get_thermostat", data): + await setup_platform(hass, DOMAIN) + + state = hass.states.get(VENTILATOR_20MIN_ID) + assert state.state == "off" + + data.reset_mock() + + +async def test_ventilator_20min_when_empty(hass: HomeAssistant, data) -> None: + """Test the ventilator switch goes on.""" + + data.return_value["settings"]["ventilatorOffDateTime"] = "" + with mock.patch("pyecobee.Ecobee.get_thermostat", data): + await setup_platform(hass, DOMAIN) + + state = hass.states.get(VENTILATOR_20MIN_ID) + assert state.state == "off" + + data.reset_mock() + + +async def test_turn_on_20min_ventilator(hass: HomeAssistant) -> None: + """Test the switch 20 min timer (On).""" + + with patch( + "homeassistant.components.ecobee.Ecobee.set_ventilator_timer" + ) as mock_set_20min_ventilator: + await setup_platform(hass, DOMAIN) + + await hass.services.async_call( + DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: VENTILATOR_20MIN_ID}, + blocking=True, + ) + await hass.async_block_till_done() + mock_set_20min_ventilator.assert_called_once_with(THERMOSTAT_ID, True) + + +async def test_turn_off_20min_ventilator(hass: HomeAssistant) -> None: + """Test the switch 20 min timer (off).""" + + with patch( + "homeassistant.components.ecobee.Ecobee.set_ventilator_timer" + ) as mock_set_20min_ventilator: + await setup_platform(hass, DOMAIN) + + await hass.services.async_call( + DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: VENTILATOR_20MIN_ID}, + blocking=True, + ) + await hass.async_block_till_done() + mock_set_20min_ventilator.assert_called_once_with(THERMOSTAT_ID, False)