Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ecobee ventilator 20 min timer #115969

Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
105 changes: 105 additions & 0 deletions homeassistant/components/ecobee/switch.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
"""Support for using switch with ecobee thermostats."""

from __future__ import annotations

from collections.abc import Awaitable, Callable
from dataclasses import dataclass
from datetime import datetime
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 . import EcobeeData
from .const import DOMAIN
from .entity import EcobeeBaseEntity

_LOGGER = logging.getLogger(__name__)

DATE_FORMAT = "%Y-%m-%d %H:%M:%S"


@dataclass(frozen=True, kw_only=True)
class EcobeeSwitchEntityDescription(SwitchEntity):
marcolivierarsenault marked this conversation as resolved.
Show resolved Hide resolved
"""Class describing Ecobee switch entities."""

ecobee_setting_key: str
set_fn: Callable[[EcobeeData, int, int], Awaitable]


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]
_LOGGER.debug("Adding ventilators 20 min switch if present")
marcolivierarsenault marked this conversation as resolved.
Show resolved Hide resolved

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."""

def __init__(
self,
data: EcobeeData,
thermostat_index: int,
) -> None:
"""Initialize ecobee ventilator platform."""
super().__init__(data, thermostat_index)
self._attr_unique_id = "ventilator_20m_timer"
MartinHjelmare marked this conversation as resolved.
Show resolved Hide resolved
self._attr_native_value = False
marcolivierarsenault marked this conversation as resolved.
Show resolved Hide resolved
self.update_without_throttle = False

@property
def is_on(self) -> bool:
marcolivierarsenault marked this conversation as resolved.
Show resolved Hide resolved
"""Get the latest state from the thermostat."""
return self._attr_native_value

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()

ventilatorOffDateTime = self.thermostat["settings"]["ventilatorOffDateTime"]
marcolivierarsenault marked this conversation as resolved.
Show resolved Hide resolved

time_zone_delay = datetime.strptime(
self.thermostat["utcTime"], DATE_FORMAT
) - datetime.strptime(self.thermostat["thermostatTime"], DATE_FORMAT)

self._attr_native_value = (
ventilatorOffDateTime is not None
and ventilatorOffDateTime != ""
and datetime.strptime(ventilatorOffDateTime, DATE_FORMAT) + time_zone_delay
>= datetime.now()
MartinHjelmare marked this conversation as resolved.
Show resolved Hide resolved
)

marcolivierarsenault marked this conversation as resolved.
Show resolved Hide resolved
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
5 changes: 4 additions & 1 deletion tests/components/ecobee/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@
"identifier": 8675309,
"name": "ecobee",
"modelNumber": "athenaSmart",
"utcTime": "2022-01-01 10:00:00",
"thermostatTime": "2022-01-01 6:00:00",
"program": {
"climates": [
{"name": "Climate1", "climateRef": "c1"},
Expand Down Expand Up @@ -92,7 +94,8 @@
"humidifierMode": "manual",
"humidity": "30",
"hasHeatPump": True,
"ventilatorType": "none",
"ventilatorType": "hrv",
"ventilatorOffDateTime": "2022-01-01 6:00:00",
},
"equipmentStatus": "fan",
"events": [
Expand Down
3 changes: 3 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,8 @@
"identifier": 8675309,
"name": "ecobee",
"modelNumber": "athenaSmart",
"utcTime": "2022-01-01 10:00:00",
"thermostatTime": "2022-01-01 6:00:00",
"program": {
"climates": [
{ "name": "Climate1", "climateRef": "c1" },
Expand All @@ -30,6 +32,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)