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鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Ecobee humidifier #45003

Merged
merged 13 commits into from
Apr 12, 2021
2 changes: 1 addition & 1 deletion homeassistant/components/ecobee/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
"vulcanSmart": "ecobee4 Smart",
}

PLATFORMS = ["binary_sensor", "climate", "sensor", "weather"]
PLATFORMS = ["binary_sensor", "climate", "humidifier", "sensor", "weather"]

MANUFACTURER = "ecobee"

Expand Down
123 changes: 123 additions & 0 deletions homeassistant/components/ecobee/humidifier.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
"""Support for using humidifier with ecobee thermostats."""
from datetime import timedelta

from homeassistant.components.humidifier import HumidifierEntity
from homeassistant.components.humidifier.const import (
DEFAULT_MAX_HUMIDITY,
DEFAULT_MIN_HUMIDITY,
DEVICE_CLASS_HUMIDIFIER,
MODE_AUTO,
SUPPORT_MODES,
)

from .const import DOMAIN

SCAN_INTERVAL = timedelta(minutes=3)

MODE_MANUAL = "manual"
MODE_OFF = "off"


async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up the ecobee thermostat humidifier entity."""
data = hass.data[DOMAIN]
entities = []
for index in range(len(data.ecobee.thermostats)):
thermostat = data.ecobee.get_thermostat(index)
if thermostat["settings"]["hasHumidifier"]:
entities.append(EcobeeHumidifier(data, index))

async_add_entities(entities, True)


class EcobeeHumidifier(HumidifierEntity):
"""A humidifier class for an ecobee thermostat with humidifer attached."""

def __init__(self, data, thermostat_index):
"""Initialize ecobee humidifier platform."""
self.data = data
self.thermostat_index = thermostat_index
self.thermostat = self.data.ecobee.get_thermostat(self.thermostat_index)
self._name = self.thermostat["name"]
self._last_humidifier_on_mode = MODE_MANUAL

self.update_without_throttle = False

async def async_update(self):
"""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()
self.thermostat = self.data.ecobee.get_thermostat(self.thermostat_index)
if self.mode != MODE_OFF:
self._last_humidifier_on_mode = self.mode

@property
def available_modes(self):
"""Return the list of available modes."""
return [MODE_OFF, MODE_AUTO, MODE_MANUAL]

@property
def device_class(self):
"""Return the device class type."""
return DEVICE_CLASS_HUMIDIFIER

@property
def is_on(self):
"""Return True if the humidifier is on."""
return self.mode != MODE_OFF

@property
def max_humidity(self):
"""Return the maximum humidity."""
return DEFAULT_MAX_HUMIDITY

@property
def min_humidity(self):
"""Return the minimum humidity."""
return DEFAULT_MIN_HUMIDITY

@property
def mode(self):
"""Return the current mode, e.g., off, auto, manual."""
return self.thermostat["settings"]["humidifierMode"]

@property
def name(self):
"""Return the name of the ecobee thermostat."""
return self._name

@property
def supported_features(self):
"""Return the list of supported features."""
return SUPPORT_MODES

@property
def target_humidity(self) -> int:
"""Return the desired humidity set point."""
return int(self.thermostat["runtime"]["desiredHumidity"])

def set_mode(self, mode):
"""Set humidifier mode (auto, off, manual)."""
if mode.lower() not in (self.available_modes):
raise ValueError(
f"Invalid mode value: {mode} Valid values are {', '.join(self.available_modes)}."
)

self.data.ecobee.set_humidifier_mode(self.thermostat_index, mode)
self.update_without_throttle = True

def set_humidity(self, humidity):
"""Set the humidity level."""
self.data.ecobee.set_humidity(self.thermostat_index, humidity)
self.update_without_throttle = True

def turn_off(self, **kwargs):
"""Set humidifier to off mode."""
self.set_mode(MODE_OFF)

def turn_on(self, **kwargs):
"""Set humidifier to on mode."""
self.set_mode(self._last_humidifier_on_mode)
27 changes: 27 additions & 0 deletions tests/components/ecobee/common.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
"""Common methods used across tests for Ecobee."""
from unittest.mock import patch

from homeassistant.components.ecobee.const import CONF_REFRESH_TOKEN, DOMAIN
from homeassistant.const import CONF_API_KEY
from homeassistant.setup import async_setup_component

from tests.common import MockConfigEntry


async def setup_platform(hass, platform):
"""Set up the ecobee platform."""
mock_entry = MockConfigEntry(
domain=DOMAIN,
data={
CONF_API_KEY: "ABC123",
CONF_REFRESH_TOKEN: "EFG456",
},
)
mock_entry.add_to_hass(hass)

with patch("homeassistant.components.ecobee.const.PLATFORMS", [platform]):
assert await async_setup_component(hass, DOMAIN, {})

await hass.async_block_till_done()

return mock_entry
17 changes: 17 additions & 0 deletions tests/components/ecobee/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
"""Fixtures for tests."""
import pytest

from tests.common import load_fixture


@pytest.fixture(autouse=True)
def requests_mock_fixture(requests_mock):
"""Fixture to provide a requests mocker."""
requests_mock.get(
"https://api.ecobee.com/1/thermostat",
text=load_fixture("ecobee/ecobee-data.json"),
)
requests_mock.post(
"https://api.ecobee.com/token",
text=load_fixture("ecobee/ecobee-token.json"),
)
1 change: 1 addition & 0 deletions tests/components/ecobee/test_climate.py
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,7 @@ async def test_extra_state_attributes(ecobee_fixture, thermostat):
"fan_min_on_time": 10,
"equipment_running": "auxHeat2",
} == thermostat.extra_state_attributes

ecobee_fixture["equipmentStatus"] = "compCool1"
assert {
"fan": "off",
Expand Down
130 changes: 130 additions & 0 deletions tests/components/ecobee/test_humidifier.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
"""The test for the ecobee thermostat humidifier module."""
from unittest.mock import patch

import pytest

from homeassistant.components.ecobee.humidifier import MODE_MANUAL, MODE_OFF
from homeassistant.components.humidifier import DOMAIN as HUMIDIFIER_DOMAIN
from homeassistant.components.humidifier.const import (
ATTR_AVAILABLE_MODES,
ATTR_HUMIDITY,
ATTR_MAX_HUMIDITY,
ATTR_MIN_HUMIDITY,
DEFAULT_MAX_HUMIDITY,
DEFAULT_MIN_HUMIDITY,
DEVICE_CLASS_HUMIDIFIER,
MODE_AUTO,
SERVICE_SET_HUMIDITY,
SERVICE_SET_MODE,
SUPPORT_MODES,
)
from homeassistant.const import (
ATTR_DEVICE_CLASS,
ATTR_ENTITY_ID,
ATTR_FRIENDLY_NAME,
ATTR_MODE,
ATTR_SUPPORTED_FEATURES,
SERVICE_TURN_OFF,
SERVICE_TURN_ON,
STATE_OFF,
)

from .common import setup_platform

DEVICE_ID = "humidifier.ecobee"


async def test_attributes(hass):
"""Test the humidifier attributes are correct."""
await setup_platform(hass, HUMIDIFIER_DOMAIN)

state = hass.states.get(DEVICE_ID)
assert state.state == STATE_OFF
assert state.attributes.get(ATTR_MIN_HUMIDITY) == DEFAULT_MIN_HUMIDITY
assert state.attributes.get(ATTR_MAX_HUMIDITY) == DEFAULT_MAX_HUMIDITY
assert state.attributes.get(ATTR_HUMIDITY) == 40
assert state.attributes.get(ATTR_AVAILABLE_MODES) == [
MODE_OFF,
MODE_AUTO,
MODE_MANUAL,
]
assert state.attributes.get(ATTR_FRIENDLY_NAME) == "ecobee"
assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_HUMIDIFIER
assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == SUPPORT_MODES


async def test_turn_on(hass):
"""Test the humidifer can be turned on."""
with patch("pyecobee.Ecobee.set_humidifier_mode") as mock_turn_on:
await setup_platform(hass, HUMIDIFIER_DOMAIN)

await hass.services.async_call(
HUMIDIFIER_DOMAIN,
SERVICE_TURN_ON,
{ATTR_ENTITY_ID: DEVICE_ID},
blocking=True,
)
await hass.async_block_till_done()
mock_turn_on.assert_called_once_with(0, "manual")


async def test_turn_off(hass):
"""Test the humidifer can be turned off."""
with patch("pyecobee.Ecobee.set_humidifier_mode") as mock_turn_off:
await setup_platform(hass, HUMIDIFIER_DOMAIN)

await hass.services.async_call(
HUMIDIFIER_DOMAIN,
SERVICE_TURN_OFF,
{ATTR_ENTITY_ID: DEVICE_ID},
blocking=True,
)
await hass.async_block_till_done()
mock_turn_off.assert_called_once_with(0, STATE_OFF)


async def test_set_mode(hass):
"""Test the humidifer can change modes."""
with patch("pyecobee.Ecobee.set_humidifier_mode") as mock_set_mode:
await setup_platform(hass, HUMIDIFIER_DOMAIN)

await hass.services.async_call(
HUMIDIFIER_DOMAIN,
SERVICE_SET_MODE,
{ATTR_ENTITY_ID: DEVICE_ID, ATTR_MODE: MODE_AUTO},
blocking=True,
)
await hass.async_block_till_done()
mock_set_mode.assert_called_once_with(0, MODE_AUTO)

await hass.services.async_call(
HUMIDIFIER_DOMAIN,
SERVICE_SET_MODE,
{ATTR_ENTITY_ID: DEVICE_ID, ATTR_MODE: MODE_MANUAL},
blocking=True,
)
await hass.async_block_till_done()
mock_set_mode.assert_called_with(0, MODE_MANUAL)

with pytest.raises(ValueError):
await hass.services.async_call(
HUMIDIFIER_DOMAIN,
SERVICE_SET_MODE,
{ATTR_ENTITY_ID: DEVICE_ID, ATTR_MODE: "ModeThatDoesntExist"},
blocking=True,
)


async def test_set_humidity(hass):
"""Test the humidifer can set humidity level."""
with patch("pyecobee.Ecobee.set_humidity") as mock_set_humidity:
await setup_platform(hass, HUMIDIFIER_DOMAIN)

await hass.services.async_call(
HUMIDIFIER_DOMAIN,
SERVICE_SET_HUMIDITY,
{ATTR_ENTITY_ID: DEVICE_ID, ATTR_HUMIDITY: 60},
blocking=True,
)
await hass.async_block_till_done()
mock_set_humidity.assert_called_once_with(0, 60)
43 changes: 43 additions & 0 deletions tests/fixtures/ecobee/ecobee-data.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
{
"thermostatList": [
{"name": "ecobee",
"program": {
"climates": [
{"name": "Climate1", "climateRef": "c1"},
{"name": "Climate2", "climateRef": "c2"}
],
"currentClimateRef": "c1"
},
"runtime": {
"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",
"hasHumidifier": true,
"humidifierMode": "off",
"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"
}
]}
]

}
7 changes: 7 additions & 0 deletions tests/fixtures/ecobee/ecobee-token.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"access_token": "Rc7JE8P7XUgSCPogLOx2VLMfITqQQrjg",
"token_type": "Bearer",
"expires_in": 3599,
"refresh_token": "og2Obost3ucRo1ofo0EDoslGltmFMe2g",
"scope": "smartWrite"
}