mirror of
https://github.com/home-assistant/core
synced 2024-10-05 15:17:19 +00:00
Support Ecobee climate Aux Heat on/off (#86100)
Co-authored-by: J. Nick Koston <nick@koston.org>
This commit is contained in:
parent
f0af0e2b42
commit
84763c793d
|
@ -35,6 +35,7 @@ from homeassistant.helpers.entity import DeviceInfo
|
|||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.util.unit_conversion import TemperatureConverter
|
||||
|
||||
from . import EcobeeData
|
||||
from .const import _LOGGER, DOMAIN, ECOBEE_MODEL_TO_NAME, MANUFACTURER
|
||||
from .util import ecobee_date, ecobee_time
|
||||
|
||||
|
@ -61,11 +62,14 @@ PRESET_HOLD_INDEFINITE = "indefinite"
|
|||
AWAY_MODE = "awayMode"
|
||||
PRESET_HOME = "home"
|
||||
PRESET_SLEEP = "sleep"
|
||||
HAS_HEAT_PUMP = "hasHeatPump"
|
||||
|
||||
DEFAULT_MIN_HUMIDITY = 15
|
||||
DEFAULT_MAX_HUMIDITY = 50
|
||||
HUMIDIFIER_MANUAL_MODE = "manual"
|
||||
|
||||
ECOBEE_AUX_HEAT_ONLY = "auxHeatOnly"
|
||||
|
||||
|
||||
# Order matters, because for reverse mapping we don't want to map HEAT to AUX
|
||||
ECOBEE_HVAC_TO_HASS = collections.OrderedDict(
|
||||
|
@ -161,7 +165,6 @@ SET_FAN_MIN_ON_TIME_SCHEMA = vol.Schema(
|
|||
SUPPORT_FLAGS = (
|
||||
ClimateEntityFeature.TARGET_TEMPERATURE
|
||||
| ClimateEntityFeature.PRESET_MODE
|
||||
| ClimateEntityFeature.AUX_HEAT
|
||||
| ClimateEntityFeature.TARGET_TEMPERATURE_RANGE
|
||||
| ClimateEntityFeature.FAN_MODE
|
||||
)
|
||||
|
@ -308,7 +311,9 @@ class Thermostat(ClimateEntity):
|
|||
_attr_precision = PRECISION_TENTHS
|
||||
_attr_temperature_unit = UnitOfTemperature.FAHRENHEIT
|
||||
|
||||
def __init__(self, data, thermostat_index, thermostat):
|
||||
def __init__(
|
||||
self, data: EcobeeData, thermostat_index: int, thermostat: dict
|
||||
) -> None:
|
||||
"""Initialize the thermostat."""
|
||||
self.data = data
|
||||
self.thermostat_index = thermostat_index
|
||||
|
@ -318,12 +323,9 @@ class Thermostat(ClimateEntity):
|
|||
self._last_active_hvac_mode = HVACMode.HEAT_COOL
|
||||
|
||||
self._operation_list = []
|
||||
if (
|
||||
self.thermostat["settings"]["heatStages"]
|
||||
or self.thermostat["settings"]["hasHeatPump"]
|
||||
):
|
||||
if self.settings["heatStages"] or self.settings["hasHeatPump"]:
|
||||
self._operation_list.append(HVACMode.HEAT)
|
||||
if self.thermostat["settings"]["coolStages"]:
|
||||
if self.settings["coolStages"]:
|
||||
self._operation_list.append(HVACMode.COOL)
|
||||
if len(self._operation_list) == 2:
|
||||
self._operation_list.insert(0, HVACMode.HEAT_COOL)
|
||||
|
@ -355,9 +357,12 @@ class Thermostat(ClimateEntity):
|
|||
@property
|
||||
def supported_features(self) -> ClimateEntityFeature:
|
||||
"""Return the list of supported features."""
|
||||
supported = SUPPORT_FLAGS
|
||||
if self.has_humidifier_control:
|
||||
return SUPPORT_FLAGS | ClimateEntityFeature.TARGET_HUMIDITY
|
||||
return SUPPORT_FLAGS
|
||||
supported = supported | ClimateEntityFeature.TARGET_HUMIDITY
|
||||
if self.has_aux_heat:
|
||||
supported = supported | ClimateEntityFeature.AUX_HEAT
|
||||
return supported
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
|
@ -411,13 +416,23 @@ class Thermostat(ClimateEntity):
|
|||
return PRECISION_HALVES
|
||||
|
||||
@property
|
||||
def has_humidifier_control(self):
|
||||
def settings(self) -> dict[str, Any]:
|
||||
"""Return the settings of the thermostat."""
|
||||
return self.thermostat["settings"]
|
||||
|
||||
@property
|
||||
def has_humidifier_control(self) -> bool:
|
||||
"""Return true if humidifier connected to thermostat and set to manual/on mode."""
|
||||
return (
|
||||
self.thermostat["settings"]["hasHumidifier"]
|
||||
and self.thermostat["settings"]["humidifierMode"] == HUMIDIFIER_MANUAL_MODE
|
||||
bool(self.settings.get("hasHumidifier"))
|
||||
and self.settings.get("humidifierMode") == HUMIDIFIER_MANUAL_MODE
|
||||
)
|
||||
|
||||
@property
|
||||
def has_aux_heat(self) -> bool:
|
||||
"""Return true if the ecobee has a heat pump."""
|
||||
return bool(self.settings.get(HAS_HEAT_PUMP))
|
||||
|
||||
@property
|
||||
def target_humidity(self) -> int | None:
|
||||
"""Return the desired humidity set point."""
|
||||
|
@ -489,7 +504,7 @@ class Thermostat(ClimateEntity):
|
|||
@property
|
||||
def hvac_mode(self):
|
||||
"""Return current operation."""
|
||||
return ECOBEE_HVAC_TO_HASS[self.thermostat["settings"]["hvacMode"]]
|
||||
return ECOBEE_HVAC_TO_HASS[self.settings["hvacMode"]]
|
||||
|
||||
@property
|
||||
def hvac_modes(self):
|
||||
|
@ -541,23 +556,25 @@ class Thermostat(ClimateEntity):
|
|||
self.thermostat["program"]["currentClimateRef"]
|
||||
],
|
||||
"equipment_running": status,
|
||||
"fan_min_on_time": self.thermostat["settings"]["fanMinOnTime"],
|
||||
"fan_min_on_time": self.settings["fanMinOnTime"],
|
||||
}
|
||||
|
||||
@property
|
||||
def is_aux_heat(self):
|
||||
def is_aux_heat(self) -> bool:
|
||||
"""Return true if aux heater."""
|
||||
return "auxHeat" in self.thermostat["equipmentStatus"]
|
||||
return self.settings["hvacMode"] == ECOBEE_AUX_HEAT_ONLY
|
||||
|
||||
async def async_turn_aux_heat_on(self) -> None:
|
||||
def turn_aux_heat_on(self) -> None:
|
||||
"""Turn auxiliary heater on."""
|
||||
if not self.is_aux_heat:
|
||||
_LOGGER.warning("# Changing aux heat is not supported")
|
||||
_LOGGER.debug("Setting HVAC mode to auxHeatOnly to turn on aux heat")
|
||||
self.data.ecobee.set_hvac_mode(self.thermostat_index, ECOBEE_AUX_HEAT_ONLY)
|
||||
self.update_without_throttle = True
|
||||
|
||||
async def async_turn_aux_heat_off(self) -> None:
|
||||
def turn_aux_heat_off(self) -> None:
|
||||
"""Turn auxiliary heater off."""
|
||||
if self.is_aux_heat:
|
||||
_LOGGER.warning("# Changing aux heat is not supported")
|
||||
_LOGGER.debug("Setting HVAC mode to last mode to disable aux heat")
|
||||
self.set_hvac_mode(self._last_active_hvac_mode)
|
||||
self.update_without_throttle = True
|
||||
|
||||
def set_preset_mode(self, preset_mode: str) -> None:
|
||||
"""Activate a preset."""
|
||||
|
@ -680,7 +697,7 @@ class Thermostat(ClimateEntity):
|
|||
heat_temp = temp
|
||||
cool_temp = temp
|
||||
else:
|
||||
delta = self.thermostat["settings"]["heatCoolMinDelta"] / 10.0
|
||||
delta = self.settings["heatCoolMinDelta"] / 10.0
|
||||
heat_temp = temp - delta
|
||||
cool_temp = temp + delta
|
||||
self.set_auto_temp_hold(heat_temp, cool_temp)
|
||||
|
@ -739,7 +756,7 @@ class Thermostat(ClimateEntity):
|
|||
# "useEndTime2hour", "useEndTime4hour"
|
||||
# "nextPeriod", "askMe"
|
||||
# "indefinite"
|
||||
device_preference = self.thermostat["settings"]["holdAction"]
|
||||
device_preference = self.settings["holdAction"]
|
||||
# Currently supported pyecobee holdTypes:
|
||||
# dateTime, nextTransition, indefinite, holdHours
|
||||
hold_pref_map = {
|
||||
|
@ -755,7 +772,7 @@ class Thermostat(ClimateEntity):
|
|||
# "useEndTime2hour", "useEndTime4hour"
|
||||
# "nextPeriod", "askMe"
|
||||
# "indefinite"
|
||||
device_preference = self.thermostat["settings"]["holdAction"]
|
||||
device_preference = self.settings["holdAction"]
|
||||
hold_hours_map = {
|
||||
"useEndTime2hour": 2,
|
||||
"useEndTime4hour": 4,
|
||||
|
|
|
@ -1 +1,119 @@
|
|||
"""Tests for Ecobee integration."""
|
||||
|
||||
GENERIC_THERMOSTAT_INFO = {
|
||||
"identifier": 8675309,
|
||||
"name": "ecobee",
|
||||
"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",
|
||||
"hasHumidifier": False,
|
||||
"humidifierMode": "manual",
|
||||
"humidity": "30",
|
||||
"ventilatorType": "none",
|
||||
},
|
||||
"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"},
|
||||
],
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
GENERIC_THERMOSTAT_INFO_WITH_HEATPUMP = {
|
||||
"identifier": 8675309,
|
||||
"name": "ecobee",
|
||||
"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",
|
||||
"hasHumidifier": False,
|
||||
"humidifierMode": "manual",
|
||||
"humidity": "30",
|
||||
"hasHeatPump": True,
|
||||
"ventilatorType": "none",
|
||||
},
|
||||
"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"},
|
||||
],
|
||||
}
|
||||
],
|
||||
}
|
||||
|
|
|
@ -8,7 +8,7 @@ from homeassistant.setup import async_setup_component
|
|||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
async def setup_platform(hass, platform):
|
||||
async def setup_platform(hass, platform) -> MockConfigEntry:
|
||||
"""Set up the ecobee platform."""
|
||||
mock_entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
"""Fixtures for tests."""
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.ecobee import ECOBEE_API_KEY, ECOBEE_REFRESH_TOKEN
|
||||
|
||||
from tests.common import load_fixture
|
||||
|
||||
|
||||
|
@ -15,3 +19,15 @@ def requests_mock_fixture(requests_mock):
|
|||
"https://api.ecobee.com/token",
|
||||
text=load_fixture("ecobee/ecobee-token.json"),
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_ecobee():
|
||||
"""Mock an Ecobee object."""
|
||||
ecobee = MagicMock()
|
||||
ecobee.request_pin.return_value = True
|
||||
ecobee.refresh_tokens.return_value = True
|
||||
|
||||
ecobee.config = {ECOBEE_API_KEY: "mocked_key", ECOBEE_REFRESH_TOKEN: "mocked_token"}
|
||||
with patch("homeassistant.components.ecobee.Ecobee", return_value=ecobee):
|
||||
yield ecobee
|
||||
|
|
|
@ -1,12 +1,22 @@
|
|||
"""The test for the Ecobee thermostat module."""
|
||||
import copy
|
||||
from http import HTTPStatus
|
||||
from unittest import mock
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.ecobee import climate as ecobee
|
||||
from homeassistant.components import climate
|
||||
from homeassistant.components.climate import ClimateEntityFeature
|
||||
from homeassistant.components.ecobee.climate import ECOBEE_AUX_HEAT_ONLY, Thermostat
|
||||
import homeassistant.const as const
|
||||
from homeassistant.const import STATE_OFF
|
||||
from homeassistant.const import ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, STATE_OFF
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from tests.components.ecobee import GENERIC_THERMOSTAT_INFO_WITH_HEATPUMP
|
||||
from tests.components.ecobee.common import setup_platform
|
||||
|
||||
ENTITY_ID = "climate.ecobee"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
|
@ -68,7 +78,7 @@ def data_fixture(ecobee_fixture):
|
|||
def thermostat_fixture(data):
|
||||
"""Set up ecobee thermostat object."""
|
||||
thermostat = data.ecobee.get_thermostat(1)
|
||||
return ecobee.Thermostat(data, 1, thermostat)
|
||||
return Thermostat(data, 1, thermostat)
|
||||
|
||||
|
||||
async def test_name(thermostat) -> None:
|
||||
|
@ -76,6 +86,37 @@ async def test_name(thermostat) -> None:
|
|||
assert thermostat.name == "Ecobee"
|
||||
|
||||
|
||||
async def test_aux_heat_not_supported_by_default(hass):
|
||||
"""Default setup should not support Aux heat."""
|
||||
await setup_platform(hass, const.Platform.CLIMATE)
|
||||
state = hass.states.get(ENTITY_ID)
|
||||
assert (
|
||||
state.attributes.get(ATTR_SUPPORTED_FEATURES)
|
||||
== ClimateEntityFeature.PRESET_MODE
|
||||
| ClimateEntityFeature.FAN_MODE
|
||||
| ClimateEntityFeature.TARGET_HUMIDITY
|
||||
| ClimateEntityFeature.TARGET_TEMPERATURE_RANGE
|
||||
| ClimateEntityFeature.TARGET_TEMPERATURE
|
||||
)
|
||||
|
||||
|
||||
async def test_aux_heat_supported_with_heat_pump(hass):
|
||||
"""Aux Heat should be supported if thermostat has heatpump."""
|
||||
mock_get_thermostat = mock.Mock()
|
||||
mock_get_thermostat.return_value = GENERIC_THERMOSTAT_INFO_WITH_HEATPUMP
|
||||
with mock.patch("pyecobee.Ecobee.get_thermostat", mock_get_thermostat):
|
||||
await setup_platform(hass, const.Platform.CLIMATE)
|
||||
state = hass.states.get(ENTITY_ID)
|
||||
assert (
|
||||
state.attributes.get(ATTR_SUPPORTED_FEATURES)
|
||||
== ClimateEntityFeature.PRESET_MODE
|
||||
| ClimateEntityFeature.FAN_MODE
|
||||
| ClimateEntityFeature.TARGET_TEMPERATURE_RANGE
|
||||
| ClimateEntityFeature.TARGET_TEMPERATURE
|
||||
| ClimateEntityFeature.AUX_HEAT
|
||||
)
|
||||
|
||||
|
||||
async def test_current_temperature(ecobee_fixture, thermostat) -> None:
|
||||
"""Test current temperature."""
|
||||
assert thermostat.current_temperature == 30
|
||||
|
@ -201,11 +242,27 @@ async def test_extra_state_attributes(ecobee_fixture, thermostat) -> None:
|
|||
} == thermostat.extra_state_attributes
|
||||
|
||||
|
||||
async def test_is_aux_heat_on(ecobee_fixture, thermostat) -> None:
|
||||
"""Test aux heat property."""
|
||||
assert not thermostat.is_aux_heat
|
||||
ecobee_fixture["equipmentStatus"] = "fan, auxHeat"
|
||||
assert thermostat.is_aux_heat
|
||||
async def test_is_aux_heat_on(hass):
|
||||
"""Test aux heat property is only enabled for auxHeatOnly."""
|
||||
mock_get_thermostat = mock.Mock()
|
||||
mock_get_thermostat.return_value = copy.deepcopy(
|
||||
GENERIC_THERMOSTAT_INFO_WITH_HEATPUMP
|
||||
)
|
||||
mock_get_thermostat.return_value["settings"]["hvacMode"] = "auxHeatOnly"
|
||||
with mock.patch("pyecobee.Ecobee.get_thermostat", mock_get_thermostat):
|
||||
await setup_platform(hass, const.Platform.CLIMATE)
|
||||
state = hass.states.get(ENTITY_ID)
|
||||
assert state.attributes[climate.ATTR_AUX_HEAT] == "on"
|
||||
|
||||
|
||||
async def test_is_aux_heat_off(hass):
|
||||
"""Test aux heat property is only enabled for auxHeatOnly."""
|
||||
mock_get_thermostat = mock.Mock()
|
||||
mock_get_thermostat.return_value = GENERIC_THERMOSTAT_INFO_WITH_HEATPUMP
|
||||
with mock.patch("pyecobee.Ecobee.get_thermostat", mock_get_thermostat):
|
||||
await setup_platform(hass, const.Platform.CLIMATE)
|
||||
state = hass.states.get(ENTITY_ID)
|
||||
assert state.attributes[climate.ATTR_AUX_HEAT] == "off"
|
||||
|
||||
|
||||
async def test_set_temperature(ecobee_fixture, thermostat, data) -> None:
|
||||
|
@ -335,3 +392,33 @@ async def test_set_fan_mode_auto(thermostat, data) -> None:
|
|||
data.ecobee.set_fan_mode.assert_has_calls(
|
||||
[mock.call(1, "auto", "nextTransition", holdHours=None)]
|
||||
)
|
||||
|
||||
|
||||
async def test_turn_aux_heat_on(hass: HomeAssistant, mock_ecobee: MagicMock) -> None:
|
||||
"""Test when aux heat is set on. This must change the HVAC mode."""
|
||||
mock_ecobee.get_thermostat.return_value = GENERIC_THERMOSTAT_INFO_WITH_HEATPUMP
|
||||
mock_ecobee.thermostats = [GENERIC_THERMOSTAT_INFO_WITH_HEATPUMP]
|
||||
await setup_platform(hass, const.Platform.CLIMATE)
|
||||
await hass.services.async_call(
|
||||
climate.DOMAIN,
|
||||
climate.SERVICE_SET_AUX_HEAT,
|
||||
{ATTR_ENTITY_ID: ENTITY_ID, climate.ATTR_AUX_HEAT: True},
|
||||
blocking=True,
|
||||
)
|
||||
assert mock_ecobee.set_hvac_mode.call_count == 1
|
||||
assert mock_ecobee.set_hvac_mode.call_args == mock.call(0, ECOBEE_AUX_HEAT_ONLY)
|
||||
|
||||
|
||||
async def test_turn_aux_heat_off(hass: HomeAssistant, mock_ecobee: MagicMock) -> None:
|
||||
"""Test when aux heat is tuned off. Must change HVAC mode back to last used."""
|
||||
mock_ecobee.get_thermostat.return_value = GENERIC_THERMOSTAT_INFO_WITH_HEATPUMP
|
||||
mock_ecobee.thermostats = [GENERIC_THERMOSTAT_INFO_WITH_HEATPUMP]
|
||||
await setup_platform(hass, const.Platform.CLIMATE)
|
||||
await hass.services.async_call(
|
||||
climate.DOMAIN,
|
||||
climate.SERVICE_SET_AUX_HEAT,
|
||||
{ATTR_ENTITY_ID: ENTITY_ID, climate.ATTR_AUX_HEAT: False},
|
||||
blocking=True,
|
||||
)
|
||||
assert mock_ecobee.set_hvac_mode.call_count == 1
|
||||
assert mock_ecobee.set_hvac_mode.call_args == mock.call(0, "auto")
|
||||
|
|
Loading…
Reference in a new issue