Skip to content

Commit

Permalink
Add Ecobee humidifier (#45003)
Browse files Browse the repository at this point in the history
  • Loading branch information
treylok committed Apr 12, 2021
1 parent de4b1ee commit 7256e33
Show file tree
Hide file tree
Showing 8 changed files with 349 additions and 1 deletion.
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"
}

0 comments on commit 7256e33

Please sign in to comment.