Skip to content

Commit

Permalink
Add ecobee ventilator (#83645)
Browse files Browse the repository at this point in the history
Co-authored-by: G Johansson <goran.johansson@shiftit.se>
Co-authored-by: J. Nick Koston <nick@koston.org>
  • Loading branch information
3 people authored Feb 7, 2023
1 parent 4aa61b0 commit fe9f682
Show file tree
Hide file tree
Showing 8 changed files with 299 additions and 4 deletions.
4 changes: 2 additions & 2 deletions CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
Expand Up @@ -288,8 +288,8 @@ build.json @home-assistant/supervisor
/tests/components/eafm/ @Jc2k
/homeassistant/components/easyenergy/ @klaasnicolaas
/tests/components/easyenergy/ @klaasnicolaas
/homeassistant/components/ecobee/ @marthoc
/tests/components/ecobee/ @marthoc
/homeassistant/components/ecobee/ @marthoc @marcolivierarsenault
/tests/components/ecobee/ @marthoc @marcolivierarsenault
/homeassistant/components/econet/ @vangorra @w1ll1am23
/tests/components/econet/ @vangorra @w1ll1am23
/homeassistant/components/ecovacs/ @OverloadUT @mib1185
Expand Down
4 changes: 3 additions & 1 deletion homeassistant/components/ecobee/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,9 @@ class EcobeeData:
Also handle refreshing tokens and updating config entry with refreshed tokens.
"""

def __init__(self, hass, entry, api_key, refresh_token):
def __init__(
self, hass: HomeAssistant, entry: ConfigEntry, api_key: str, refresh_token: str
) -> None:
"""Initialize the Ecobee data object."""
self._hass = hass
self._entry = entry
Expand Down
1 change: 1 addition & 0 deletions homeassistant/components/ecobee/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
Platform.BINARY_SENSOR,
Platform.CLIMATE,
Platform.HUMIDIFIER,
Platform.NUMBER,
Platform.SENSOR,
Platform.WEATHER,
]
Expand Down
39 changes: 39 additions & 0 deletions homeassistant/components/ecobee/entity.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
"""Base classes shared among Ecobee entities."""
from __future__ import annotations

import logging
from typing import Any

from homeassistant.helpers.entity import DeviceInfo, Entity

from . import EcobeeData
from .const import DOMAIN, ECOBEE_MODEL_TO_NAME, MANUFACTURER

_LOGGER = logging.getLogger(__name__)


class EcobeeBaseEntity(Entity):
"""Base methods for Ecobee entities."""

def __init__(self, data: EcobeeData, thermostat_index: int) -> None:
"""Initiate base methods for Ecobee entities."""
self.data = data
self.thermostat_index = thermostat_index
thermostat = self.thermostat
self.base_unique_id = thermostat["identifier"]
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, thermostat["identifier"])},
manufacturer=MANUFACTURER,
model=ECOBEE_MODEL_TO_NAME.get(thermostat["modelNumber"]),
name=thermostat["name"],
)

@property
def thermostat(self) -> dict[str, Any]:
"""Return the thermostat data for the entity."""
return self.data.ecobee.get_thermostat(self.thermostat_index)

@property
def available(self) -> bool:
"""Return if device is available."""
return self.thermostat["runtime"]["connected"]
2 changes: 1 addition & 1 deletion homeassistant/components/ecobee/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/ecobee",
"requirements": ["python-ecobee-api==0.2.14"],
"codeowners": ["@marthoc"],
"codeowners": ["@marthoc", "@marcolivierarsenault"],
"homekit": {
"models": ["EB-*", "ecobee*"]
},
Expand Down
107 changes: 107 additions & 0 deletions homeassistant/components/ecobee/number.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
"""Support for using number with ecobee thermostats."""
from __future__ import annotations

from collections.abc import Awaitable, Callable
from dataclasses import dataclass
import logging

from homeassistant.components.number import NumberEntity, NumberEntityDescription
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import UnitOfTime
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__)


@dataclass
class EcobeeNumberEntityDescriptionBase:
"""Required values when describing Ecobee number entities."""

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


@dataclass
class EcobeeNumberEntityDescription(
NumberEntityDescription, EcobeeNumberEntityDescriptionBase
):
"""Class describing Ecobee number entities."""


VENTILATOR_NUMBERS = (
EcobeeNumberEntityDescription(
key="home",
name="home",
ecobee_setting_key="ventilatorMinOnTimeHome",
set_fn=lambda data, id, min_time: data.ecobee.set_ventilator_min_on_time_home(
id, min_time
),
),
EcobeeNumberEntityDescription(
key="away",
name="away",
ecobee_setting_key="ventilatorMinOnTimeAway",
set_fn=lambda data, id, min_time: data.ecobee.set_ventilator_min_on_time_away(
id, min_time
),
),
)


async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the ecobee thermostat number entity."""
data: EcobeeData = hass.data[DOMAIN]
entities = []
_LOGGER.debug("Adding min time ventilators numbers (if present)")
for index, thermostat in enumerate(data.ecobee.thermostats):
if thermostat["settings"]["ventilatorType"] == "none":
continue
_LOGGER.debug("Adding %s's ventilator min times number", thermostat["name"])
for numbers in VENTILATOR_NUMBERS:
entities.append(EcobeeVentilatorMinTime(data, index, numbers))

async_add_entities(entities, True)


class EcobeeVentilatorMinTime(EcobeeBaseEntity, NumberEntity):
"""A number class, representing min time for an ecobee thermostat with ventilator attached."""

entity_description: EcobeeNumberEntityDescription

_attr_native_min_value = 0
_attr_native_max_value = 60
_attr_native_step = 5
_attr_native_unit_of_measurement = UnitOfTime.MINUTES
_attr_has_entity_name = True

def __init__(
self,
data: EcobeeData,
thermostat_index: int,
description: EcobeeNumberEntityDescription,
) -> None:
"""Initialize ecobee ventilator platform."""
super().__init__(data, thermostat_index)
self.entity_description = description
self._attr_name = f"Ventilator min time {description.name}"
self._attr_unique_id = f"{self.base_unique_id}_ventilator_{description.key}"

async def async_update(self) -> None:
"""Get the latest state from the thermostat."""
await self.data.update()
self._attr_native_value = self.thermostat["settings"][
self.entity_description.ecobee_setting_key
]

def set_native_value(self, value: float) -> None:
"""Set new ventilator Min On Time value."""
self.entity_description.set_fn(self.data, self.thermostat_index, int(value))
72 changes: 72 additions & 0 deletions tests/components/ecobee/fixtures/ecobee-data.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,78 @@
"fanMinOnTime": 10,
"heatCoolMinDelta": 50,
"holdAction": "nextTransition",
"ventilatorType": "hrv",
"ventilatorMinOnTimeHome": 20,
"ventilatorMinOnTimeAway": 10,
"isVentilatorTimerOn": false,
"hasHumidifier": true,
"humidifierMode": "manual",
"humidity": "30"
},
"equipmentStatus": "fan",
"events": [
{
"name": "Event1",
"running": true,
"type": "hold",
"holdClimateRef": "away",
"endDate": "2022-01-01 10:00:00",
"startDate": "2022-02-02 11:00:00"
}
],
"remoteSensors": [
{
"id": "rs:100",
"name": "Remote Sensor 1",
"type": "ecobee3_remote_sensor",
"code": "WKRP",
"inUse": false,
"capability": [
{
"id": "1",
"type": "temperature",
"value": "782"
},
{
"id": "2",
"type": "occupancy",
"value": "false"
}
]
}
]
},
{
"identifier": 8675308,
"name": "ecobee2",
"modelNumber": "athenaSmart",
"program": {
"climates": [
{ "name": "Climate1", "climateRef": "c1" },
{ "name": "Climate2", "climateRef": "c2" }
],
"currentClimateRef": "c1"
},
"runtime": {
"connected": true,
"actualTemperature": 300,
"actualHumidity": 15,
"desiredHeat": 400,
"desiredCool": 200,
"desiredFanMode": "on",
"desiredHumidity": 40
},
"settings": {
"hvacMode": "auto",
"heatStages": 1,
"coolStages": 1,
"fanMinOnTime": 10,
"heatCoolMinDelta": 50,
"holdAction": "nextTransition",
"ventilatorType": "none",
"ventilatorMinOnTimeHome": 20,
"ventilatorMinOnTimeAway": 10,
"isVentilatorTimerOn": false,
"hasHumidifier": true,
"humidifierMode": "manual",
"humidity": "30"
Expand Down
74 changes: 74 additions & 0 deletions tests/components/ecobee/test_number.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
"""The test for the ecobee thermostat number module."""
from unittest.mock import patch

from homeassistant.components.number import ATTR_VALUE, DOMAIN, SERVICE_SET_VALUE
from homeassistant.const import ATTR_ENTITY_ID, UnitOfTime
from homeassistant.core import HomeAssistant

from .common import setup_platform

VENTILATOR_MIN_HOME_ID = "number.ecobee_ventilator_min_time_home"
VENTILATOR_MIN_AWAY_ID = "number.ecobee_ventilator_min_time_away"
THERMOSTAT_ID = 0


async def test_ventilator_min_on_home_attributes(hass):
"""Test the ventilator number on home attributes are correct."""
await setup_platform(hass, DOMAIN)

state = hass.states.get(VENTILATOR_MIN_HOME_ID)
assert state.state == "20"
assert state.attributes.get("min") == 0
assert state.attributes.get("max") == 60
assert state.attributes.get("step") == 5
assert state.attributes.get("friendly_name") == "ecobee Ventilator min time home"
assert state.attributes.get("unit_of_measurement") == UnitOfTime.MINUTES


async def test_ventilator_min_on_away_attributes(hass):
"""Test the ventilator number on away attributes are correct."""
await setup_platform(hass, DOMAIN)

state = hass.states.get(VENTILATOR_MIN_AWAY_ID)
assert state.state == "10"
assert state.attributes.get("min") == 0
assert state.attributes.get("max") == 60
assert state.attributes.get("step") == 5
assert state.attributes.get("friendly_name") == "ecobee Ventilator min time away"
assert state.attributes.get("unit_of_measurement") == UnitOfTime.MINUTES


async def test_set_min_time_home(hass: HomeAssistant):
"""Test the number can set min time home."""
target_value = 40
with patch(
"homeassistant.components.ecobee.Ecobee.set_ventilator_min_on_time_home"
) as mock_set_min_home_time:
await setup_platform(hass, DOMAIN)

await hass.services.async_call(
DOMAIN,
SERVICE_SET_VALUE,
{ATTR_ENTITY_ID: VENTILATOR_MIN_HOME_ID, ATTR_VALUE: target_value},
blocking=True,
)
await hass.async_block_till_done()
mock_set_min_home_time.assert_called_once_with(THERMOSTAT_ID, target_value)


async def test_set_min_time_away(hass: HomeAssistant) -> None:
"""Test the number can set min time away."""
target_value = 0
with patch(
"homeassistant.components.ecobee.Ecobee.set_ventilator_min_on_time_away"
) as mock_set_min_away_time:
await setup_platform(hass, DOMAIN)

await hass.services.async_call(
DOMAIN,
SERVICE_SET_VALUE,
{ATTR_ENTITY_ID: VENTILATOR_MIN_AWAY_ID, ATTR_VALUE: target_value},
blocking=True,
)
await hass.async_block_till_done()
mock_set_min_away_time.assert_called_once_with(THERMOSTAT_ID, target_value)

0 comments on commit fe9f682

Please sign in to comment.