Add ecobee ventilator (#83645)
Co-authored-by: G Johansson <goran.johansson@shiftit.se> Co-authored-by: J. Nick Koston <nick@koston.org>
This commit is contained in:
parent
4aa61b0d64
commit
fe9f6823c3
8 changed files with 299 additions and 4 deletions
|
@ -288,8 +288,8 @@ build.json @home-assistant/supervisor
|
||||||
/tests/components/eafm/ @Jc2k
|
/tests/components/eafm/ @Jc2k
|
||||||
/homeassistant/components/easyenergy/ @klaasnicolaas
|
/homeassistant/components/easyenergy/ @klaasnicolaas
|
||||||
/tests/components/easyenergy/ @klaasnicolaas
|
/tests/components/easyenergy/ @klaasnicolaas
|
||||||
/homeassistant/components/ecobee/ @marthoc
|
/homeassistant/components/ecobee/ @marthoc @marcolivierarsenault
|
||||||
/tests/components/ecobee/ @marthoc
|
/tests/components/ecobee/ @marthoc @marcolivierarsenault
|
||||||
/homeassistant/components/econet/ @vangorra @w1ll1am23
|
/homeassistant/components/econet/ @vangorra @w1ll1am23
|
||||||
/tests/components/econet/ @vangorra @w1ll1am23
|
/tests/components/econet/ @vangorra @w1ll1am23
|
||||||
/homeassistant/components/ecovacs/ @OverloadUT @mib1185
|
/homeassistant/components/ecovacs/ @OverloadUT @mib1185
|
||||||
|
|
|
@ -91,7 +91,9 @@ class EcobeeData:
|
||||||
Also handle refreshing tokens and updating config entry with refreshed tokens.
|
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."""
|
"""Initialize the Ecobee data object."""
|
||||||
self._hass = hass
|
self._hass = hass
|
||||||
self._entry = entry
|
self._entry = entry
|
||||||
|
|
|
@ -45,6 +45,7 @@ PLATFORMS = [
|
||||||
Platform.BINARY_SENSOR,
|
Platform.BINARY_SENSOR,
|
||||||
Platform.CLIMATE,
|
Platform.CLIMATE,
|
||||||
Platform.HUMIDIFIER,
|
Platform.HUMIDIFIER,
|
||||||
|
Platform.NUMBER,
|
||||||
Platform.SENSOR,
|
Platform.SENSOR,
|
||||||
Platform.WEATHER,
|
Platform.WEATHER,
|
||||||
]
|
]
|
||||||
|
|
39
homeassistant/components/ecobee/entity.py
Normal file
39
homeassistant/components/ecobee/entity.py
Normal file
|
@ -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"]
|
|
@ -4,7 +4,7 @@
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"documentation": "https://www.home-assistant.io/integrations/ecobee",
|
"documentation": "https://www.home-assistant.io/integrations/ecobee",
|
||||||
"requirements": ["python-ecobee-api==0.2.14"],
|
"requirements": ["python-ecobee-api==0.2.14"],
|
||||||
"codeowners": ["@marthoc"],
|
"codeowners": ["@marthoc", "@marcolivierarsenault"],
|
||||||
"homekit": {
|
"homekit": {
|
||||||
"models": ["EB-*", "ecobee*"]
|
"models": ["EB-*", "ecobee*"]
|
||||||
},
|
},
|
||||||
|
|
107
homeassistant/components/ecobee/number.py
Normal file
107
homeassistant/components/ecobee/number.py
Normal file
|
@ -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))
|
|
@ -27,6 +27,78 @@
|
||||||
"fanMinOnTime": 10,
|
"fanMinOnTime": 10,
|
||||||
"heatCoolMinDelta": 50,
|
"heatCoolMinDelta": 50,
|
||||||
"holdAction": "nextTransition",
|
"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,
|
"hasHumidifier": true,
|
||||||
"humidifierMode": "manual",
|
"humidifierMode": "manual",
|
||||||
"humidity": "30"
|
"humidity": "30"
|
||||||
|
|
74
tests/components/ecobee/test_number.py
Normal file
74
tests/components/ecobee/test_number.py
Normal file
|
@ -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)
|
Loading…
Add table
Add a link
Reference in a new issue