mirror of
https://github.com/home-assistant/core.git
synced 2025-04-23 16:57:53 +00:00
Create auxHeatOnly switch in Ecobee integration (#116323)
Co-authored-by: Franck Nijhof <frenck@frenck.nl>
This commit is contained in:
parent
856aa38539
commit
ed0e0eee71
@ -36,10 +36,17 @@ from homeassistant.helpers import entity_platform
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
|
||||
from homeassistant.util.unit_conversion import TemperatureConverter
|
||||
|
||||
from . import EcobeeData
|
||||
from .const import _LOGGER, DOMAIN, ECOBEE_MODEL_TO_NAME, MANUFACTURER
|
||||
from .const import (
|
||||
_LOGGER,
|
||||
DOMAIN,
|
||||
ECOBEE_AUX_HEAT_ONLY,
|
||||
ECOBEE_MODEL_TO_NAME,
|
||||
MANUFACTURER,
|
||||
)
|
||||
from .util import ecobee_date, ecobee_time, is_indefinite_hold
|
||||
|
||||
ATTR_COOL_TEMP = "cool_temp"
|
||||
@ -69,9 +76,6 @@ 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(
|
||||
[
|
||||
@ -79,9 +83,13 @@ ECOBEE_HVAC_TO_HASS = collections.OrderedDict(
|
||||
("cool", HVACMode.COOL),
|
||||
("auto", HVACMode.HEAT_COOL),
|
||||
("off", HVACMode.OFF),
|
||||
("auxHeatOnly", HVACMode.HEAT),
|
||||
(ECOBEE_AUX_HEAT_ONLY, HVACMode.HEAT),
|
||||
]
|
||||
)
|
||||
# Reverse key/value pair, drop auxHeatOnly as it doesn't map to specific HASS mode
|
||||
HASS_TO_ECOBEE_HVAC = {
|
||||
v: k for k, v in ECOBEE_HVAC_TO_HASS.items() if k != ECOBEE_AUX_HEAT_ONLY
|
||||
}
|
||||
|
||||
ECOBEE_HVAC_ACTION_TO_HASS = {
|
||||
# Map to None if we do not know how to represent.
|
||||
@ -570,17 +578,39 @@ class Thermostat(ClimateEntity):
|
||||
"""Return true if aux heater."""
|
||||
return self.settings["hvacMode"] == ECOBEE_AUX_HEAT_ONLY
|
||||
|
||||
def turn_aux_heat_on(self) -> None:
|
||||
async def async_turn_aux_heat_on(self) -> None:
|
||||
"""Turn auxiliary heater on."""
|
||||
async_create_issue(
|
||||
self.hass,
|
||||
DOMAIN,
|
||||
"migrate_aux_heat",
|
||||
breaks_in_ha_version="2024.10.0",
|
||||
is_fixable=True,
|
||||
is_persistent=True,
|
||||
translation_key="migrate_aux_heat",
|
||||
severity=IssueSeverity.WARNING,
|
||||
)
|
||||
_LOGGER.debug("Setting HVAC mode to auxHeatOnly to turn on aux heat")
|
||||
self._last_hvac_mode_before_aux_heat = self.hvac_mode
|
||||
self.data.ecobee.set_hvac_mode(self.thermostat_index, ECOBEE_AUX_HEAT_ONLY)
|
||||
await self.hass.async_add_executor_job(
|
||||
self.data.ecobee.set_hvac_mode, self.thermostat_index, ECOBEE_AUX_HEAT_ONLY
|
||||
)
|
||||
self.update_without_throttle = True
|
||||
|
||||
def turn_aux_heat_off(self) -> None:
|
||||
async def async_turn_aux_heat_off(self) -> None:
|
||||
"""Turn auxiliary heater off."""
|
||||
async_create_issue(
|
||||
self.hass,
|
||||
DOMAIN,
|
||||
"migrate_aux_heat",
|
||||
breaks_in_ha_version="2024.10.0",
|
||||
is_fixable=True,
|
||||
is_persistent=True,
|
||||
translation_key="migrate_aux_heat",
|
||||
severity=IssueSeverity.WARNING,
|
||||
)
|
||||
_LOGGER.debug("Setting HVAC mode to last mode to disable aux heat")
|
||||
self.set_hvac_mode(self._last_hvac_mode_before_aux_heat)
|
||||
await self.async_set_hvac_mode(self._last_hvac_mode_before_aux_heat)
|
||||
self.update_without_throttle = True
|
||||
|
||||
def set_preset_mode(self, preset_mode: str) -> None:
|
||||
@ -740,9 +770,7 @@ class Thermostat(ClimateEntity):
|
||||
|
||||
def set_hvac_mode(self, hvac_mode: HVACMode) -> None:
|
||||
"""Set HVAC mode (auto, auxHeatOnly, cool, heat, off)."""
|
||||
ecobee_value = next(
|
||||
(k for k, v in ECOBEE_HVAC_TO_HASS.items() if v == hvac_mode), None
|
||||
)
|
||||
ecobee_value = HASS_TO_ECOBEE_HVAC.get(hvac_mode)
|
||||
if ecobee_value is None:
|
||||
_LOGGER.error("Invalid mode for set_hvac_mode: %s", hvac_mode)
|
||||
return
|
||||
|
@ -55,6 +55,8 @@ PLATFORMS = [
|
||||
|
||||
MANUFACTURER = "ecobee"
|
||||
|
||||
ECOBEE_AUX_HEAT_ONLY = "auxHeatOnly"
|
||||
|
||||
# Translates ecobee API weatherSymbol to Home Assistant usable names
|
||||
# https://www.ecobee.com/home/developer/api/documentation/v1/objects/WeatherForecast.shtml
|
||||
ECOBEE_WEATHER_SYMBOL_TO_HASS = {
|
||||
|
@ -38,6 +38,11 @@
|
||||
"ventilator_min_type_away": {
|
||||
"name": "Ventilator min time away"
|
||||
}
|
||||
},
|
||||
"switch": {
|
||||
"aux_heat_only": {
|
||||
"name": "Aux heat only"
|
||||
}
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
@ -163,5 +168,18 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"issues": {
|
||||
"migrate_aux_heat": {
|
||||
"title": "Migration of Ecobee set_aux_heat service",
|
||||
"fix_flow": {
|
||||
"step": {
|
||||
"confirm": {
|
||||
"description": "The Ecobee `set_aux_heat` service has been migrated. A new `aux_heat_only` switch entity is available for each thermostat that supports a Heat Pump.\n\nUpdate any automations to use the new `aux_heat_only` switch entity. When this is done, fix this issue and restart Home Assistant.",
|
||||
"title": "Disable legacy Ecobee set_aux_heat service"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ from datetime import tzinfo
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.components.climate import HVACMode
|
||||
from homeassistant.components.switch import SwitchEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
@ -13,7 +14,8 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from . import EcobeeData
|
||||
from .const import DOMAIN
|
||||
from .climate import HASS_TO_ECOBEE_HVAC
|
||||
from .const import DOMAIN, ECOBEE_AUX_HEAT_ONLY
|
||||
from .entity import EcobeeBaseEntity
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@ -43,6 +45,12 @@ async def async_setup_entry(
|
||||
update_before_add=True,
|
||||
)
|
||||
|
||||
async_add_entities(
|
||||
EcobeeSwitchAuxHeatOnly(data, index)
|
||||
for index, thermostat in enumerate(data.ecobee.thermostats)
|
||||
if thermostat["settings"]["hasHeatPump"]
|
||||
)
|
||||
|
||||
|
||||
class EcobeeVentilator20MinSwitch(EcobeeBaseEntity, SwitchEntity):
|
||||
"""A Switch class, representing 20 min timer for an ecobee thermostat with ventilator attached."""
|
||||
@ -93,3 +101,39 @@ class EcobeeVentilator20MinSwitch(EcobeeBaseEntity, SwitchEntity):
|
||||
self.data.ecobee.set_ventilator_timer, self.thermostat_index, False
|
||||
)
|
||||
self.update_without_throttle = True
|
||||
|
||||
|
||||
class EcobeeSwitchAuxHeatOnly(EcobeeBaseEntity, SwitchEntity):
|
||||
"""Representation of a aux_heat_only ecobee switch."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
_attr_translation_key = "aux_heat_only"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
data: EcobeeData,
|
||||
thermostat_index: int,
|
||||
) -> None:
|
||||
"""Initialize ecobee ventilator platform."""
|
||||
super().__init__(data, thermostat_index)
|
||||
self._attr_unique_id = f"{self.base_unique_id}_aux_heat_only"
|
||||
|
||||
self._last_hvac_mode_before_aux_heat = HASS_TO_ECOBEE_HVAC.get(
|
||||
HVACMode.HEAT_COOL
|
||||
)
|
||||
|
||||
def turn_on(self, **kwargs: Any) -> None:
|
||||
"""Set the hvacMode to auxHeatOnly."""
|
||||
self._last_hvac_mode_before_aux_heat = self.thermostat["settings"]["hvacMode"]
|
||||
self.data.ecobee.set_hvac_mode(self.thermostat_index, ECOBEE_AUX_HEAT_ONLY)
|
||||
|
||||
def turn_off(self, **kwargs: Any) -> None:
|
||||
"""Set the hvacMode back to the prior setting."""
|
||||
self.data.ecobee.set_hvac_mode(
|
||||
self.thermostat_index, self._last_hvac_mode_before_aux_heat
|
||||
)
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
"""Return true if auxHeatOnly mode is active."""
|
||||
return self.thermostat["settings"]["hvacMode"] == ECOBEE_AUX_HEAT_ONLY
|
||||
|
@ -11,8 +11,14 @@
|
||||
},
|
||||
"program": {
|
||||
"climates": [
|
||||
{ "name": "Climate1", "climateRef": "c1" },
|
||||
{ "name": "Climate2", "climateRef": "c2" }
|
||||
{
|
||||
"name": "Climate1",
|
||||
"climateRef": "c1"
|
||||
},
|
||||
{
|
||||
"name": "Climate2",
|
||||
"climateRef": "c2"
|
||||
}
|
||||
],
|
||||
"currentClimateRef": "c1"
|
||||
},
|
||||
@ -39,6 +45,7 @@
|
||||
"isVentilatorTimerOn": false,
|
||||
"hasHumidifier": true,
|
||||
"humidifierMode": "manual",
|
||||
"hasHeatPump": false,
|
||||
"humidity": "30"
|
||||
},
|
||||
"equipmentStatus": "fan",
|
||||
@ -82,8 +89,14 @@
|
||||
"modelNumber": "athenaSmart",
|
||||
"program": {
|
||||
"climates": [
|
||||
{ "name": "Climate1", "climateRef": "c1" },
|
||||
{ "name": "Climate2", "climateRef": "c2" }
|
||||
{
|
||||
"name": "Climate1",
|
||||
"climateRef": "c1"
|
||||
},
|
||||
{
|
||||
"name": "Climate2",
|
||||
"climateRef": "c2"
|
||||
}
|
||||
],
|
||||
"currentClimateRef": "c1"
|
||||
},
|
||||
@ -109,6 +122,7 @@
|
||||
"isVentilatorTimerOn": false,
|
||||
"hasHumidifier": true,
|
||||
"humidifierMode": "manual",
|
||||
"hasHeatPump": true,
|
||||
"humidity": "30"
|
||||
},
|
||||
"equipmentStatus": "fan",
|
||||
@ -184,6 +198,7 @@
|
||||
"isVentilatorTimerOn": false,
|
||||
"hasHumidifier": true,
|
||||
"humidifierMode": "manual",
|
||||
"hasHeatPump": false,
|
||||
"humidity": "30"
|
||||
},
|
||||
"equipmentStatus": "fan",
|
||||
|
@ -3,6 +3,11 @@
|
||||
from http import HTTPStatus
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from homeassistant.components.climate import (
|
||||
ATTR_AUX_HEAT,
|
||||
DOMAIN as CLIMATE_DOMAIN,
|
||||
SERVICE_SET_AUX_HEAT,
|
||||
)
|
||||
from homeassistant.components.ecobee import DOMAIN
|
||||
from homeassistant.components.notify import DOMAIN as NOTIFY_DOMAIN
|
||||
from homeassistant.components.repairs.issue_handler import (
|
||||
@ -12,6 +17,7 @@ from homeassistant.components.repairs.websocket_api import (
|
||||
RepairsFlowIndexView,
|
||||
RepairsFlowResourceView,
|
||||
)
|
||||
from homeassistant.const import ATTR_ENTITY_ID
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import issue_registry as ir
|
||||
|
||||
@ -22,7 +28,7 @@ from tests.typing import ClientSessionGenerator
|
||||
THERMOSTAT_ID = 0
|
||||
|
||||
|
||||
async def test_ecobee_repair_flow(
|
||||
async def test_ecobee_notify_repair_flow(
|
||||
hass: HomeAssistant,
|
||||
mock_ecobee: MagicMock,
|
||||
hass_client: ClientSessionGenerator,
|
||||
@ -77,3 +83,32 @@ async def test_ecobee_repair_flow(
|
||||
issue_id=f"migrate_notify_{DOMAIN}_{DOMAIN}",
|
||||
)
|
||||
assert len(issue_registry.issues) == 0
|
||||
|
||||
|
||||
async def test_ecobee_aux_heat_repair_flow(
|
||||
hass: HomeAssistant,
|
||||
mock_ecobee: MagicMock,
|
||||
hass_client: ClientSessionGenerator,
|
||||
issue_registry: ir.IssueRegistry,
|
||||
) -> None:
|
||||
"""Test the ecobee aux_heat service repair flow is triggered."""
|
||||
await setup_platform(hass, CLIMATE_DOMAIN)
|
||||
await async_process_repairs_platforms(hass)
|
||||
|
||||
ENTITY_ID = "climate.ecobee2"
|
||||
|
||||
# Simulate legacy service being used
|
||||
assert hass.services.has_service(CLIMATE_DOMAIN, SERVICE_SET_AUX_HEAT)
|
||||
await hass.services.async_call(
|
||||
CLIMATE_DOMAIN,
|
||||
SERVICE_SET_AUX_HEAT,
|
||||
{ATTR_ENTITY_ID: ENTITY_ID, ATTR_AUX_HEAT: True},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
# Assert the issue is present
|
||||
assert issue_registry.async_get_issue(
|
||||
domain="ecobee",
|
||||
issue_id="migrate_aux_heat",
|
||||
)
|
||||
assert len(issue_registry.issues) == 1
|
||||
|
@ -112,3 +112,34 @@ async def test_turn_off_20min_ventilator(hass: HomeAssistant) -> None:
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
mock_set_20min_ventilator.assert_called_once_with(THERMOSTAT_ID, False)
|
||||
|
||||
|
||||
DEVICE_ID = "switch.ecobee2_aux_heat_only"
|
||||
|
||||
|
||||
async def test_aux_heat_only_turn_on(hass: HomeAssistant) -> None:
|
||||
"""Test the switch can be turned on."""
|
||||
with patch("pyecobee.Ecobee.set_hvac_mode") as mock_turn_on:
|
||||
await setup_platform(hass, DOMAIN)
|
||||
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_TURN_ON,
|
||||
{ATTR_ENTITY_ID: DEVICE_ID},
|
||||
blocking=True,
|
||||
)
|
||||
mock_turn_on.assert_called_once_with(1, "auxHeatOnly")
|
||||
|
||||
|
||||
async def test_aux_heat_only_turn_off(hass: HomeAssistant) -> None:
|
||||
"""Test the switch can be turned off."""
|
||||
with patch("pyecobee.Ecobee.set_hvac_mode") as mock_turn_off:
|
||||
await setup_platform(hass, DOMAIN)
|
||||
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_TURN_OFF,
|
||||
{ATTR_ENTITY_ID: DEVICE_ID},
|
||||
blocking=True,
|
||||
)
|
||||
mock_turn_off.assert_called_once_with(1, "auto")
|
||||
|
Loading…
x
Reference in New Issue
Block a user