Skip to content

Commit

Permalink
Add ecobee ventilator 20 min timer (#115969)
Browse files Browse the repository at this point in the history
* add 20 min timer Ecobee

* modify local value with estimated time

* add ecobee test switch

* removed manual setting of data

* Add no throttle updates

* add more test cases

* move timezone calculation in update function

* update attribute based on feedback

* use timezone for time comparaison

* add location data to tests

* remove is_on function

* update python-ecobee-api lib

* remove uncessary checks

---------

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
  • Loading branch information
marcolivierarsenault and MartinHjelmare authored May 3, 2024
1 parent a3791fd commit 0e23d04
Show file tree
Hide file tree
Showing 8 changed files with 220 additions and 4 deletions.
1 change: 1 addition & 0 deletions homeassistant/components/ecobee/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
Platform.NOTIFY,
Platform.NUMBER,
Platform.SENSOR,
Platform.SWITCH,
Platform.WEATHER,
]

Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/ecobee/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -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."
Expand Down
90 changes: 90 additions & 0 deletions homeassistant/components/ecobee/switch.py
Original file line number Diff line number Diff line change
@@ -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
2 changes: 1 addition & 1 deletion requirements_all.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion requirements_test_all.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 5 additions & 1 deletion tests/components/ecobee/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"},
Expand Down Expand Up @@ -92,7 +95,8 @@
"humidifierMode": "manual",
"humidity": "30",
"hasHeatPump": True,
"ventilatorType": "none",
"ventilatorType": "hrv",
"ventilatorOffDateTime": "2022-01-01 6:00:00",
},
"equipmentStatus": "fan",
"events": [
Expand Down
6 changes: 6 additions & 0 deletions tests/components/ecobee/fixtures/ecobee-data.json
Original file line number Diff line number Diff line change
Expand Up @@ -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" },
Expand All @@ -30,6 +35,7 @@
"ventilatorType": "hrv",
"ventilatorMinOnTimeHome": 20,
"ventilatorMinOnTimeAway": 10,
"ventilatorOffDateTime": "2022-01-01 6:00:00",
"isVentilatorTimerOn": false,
"hasHumidifier": true,
"humidifierMode": "manual",
Expand Down
115 changes: 115 additions & 0 deletions tests/components/ecobee/test_switch.py
Original file line number Diff line number Diff line change
@@ -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)

0 comments on commit 0e23d04

Please sign in to comment.