mirror of
https://github.com/home-assistant/core.git
synced 2025-07-29 18:28:14 +02:00
Add ecobee set_sensors_used_in_climate service (#102871)
* Add set_active_sensors Service * Remove version bump from service addition commit * Reviewer suggested changes * Changed naming to be more clear of functionality * Adjusted additional naming to follow new convention * Updated to pass failing CI tests * Fix typo * Fix to pass CI * Changed argument from climate_name to preset_mode and changed service error * Made loop more clear and changed raised error to log msg * Fix typo Co-authored-by: Erik Montnemery <erik@montnemery.com> * Removed code that was accidentally added back in and fixed mypy errors * Add icon for service * Added sensors as attributes and updated tests * Revert changes made in #126587 * Added tests for remote_sensors and set_sensors_used_in_climate * Changed back to load multiplatforms (#126587) * Check for empty sensor list and negative tests for errors raised * Added tests and fixed errors * Add hass to class init to allow for device_registry lookup at startup and check for name changed by user * Added tests to test the new functions * Simplified code and fixed testing error for simplification * Added freeze in test * Fixed device filtering * Simplified code section * Maintains the ability to call `set_sensors_used_in_climate` function even is the user changes the device name from the ecobee app or thermostat without needing to reload home assistant. * Update tests with new functionality. Changed thermostat identifier to a string, since that is what is provided via the ecobee api * Changed function parameter * Search for specific ecobee identifier * Moved errors to strings.json * Added test for sensor not on thermostat * Improved tests and updated device check * Added attributes to _unrecoreded_attributes * Changed name to be more clear * Improve error message and add test for added property * Renamed variables for clarity * Added device_id to available_sensors to make it easier on user to find it --------- Co-authored-by: Robert Resch <robert@resch.dev> Co-authored-by: Erik Montnemery <erik@montnemery.com>
This commit is contained in:
@ -32,7 +32,8 @@ from homeassistant.const import (
|
||||
UnitOfTemperature,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, ServiceCall
|
||||
from homeassistant.helpers import entity_platform
|
||||
from homeassistant.exceptions import ServiceValidationError
|
||||
from homeassistant.helpers import device_registry as dr, entity_platform
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
@ -41,6 +42,8 @@ from homeassistant.util.unit_conversion import TemperatureConverter
|
||||
from . import EcobeeData
|
||||
from .const import (
|
||||
_LOGGER,
|
||||
ATTR_ACTIVE_SENSORS,
|
||||
ATTR_AVAILABLE_SENSORS,
|
||||
DOMAIN,
|
||||
ECOBEE_AUX_HEAT_ONLY,
|
||||
ECOBEE_MODEL_TO_NAME,
|
||||
@ -62,6 +65,8 @@ ATTR_DST_ENABLED = "dst_enabled"
|
||||
ATTR_MIC_ENABLED = "mic_enabled"
|
||||
ATTR_AUTO_AWAY = "auto_away"
|
||||
ATTR_FOLLOW_ME = "follow_me"
|
||||
ATTR_SENSOR_LIST = "device_ids"
|
||||
ATTR_PRESET_MODE = "preset_mode"
|
||||
|
||||
DEFAULT_RESUME_ALL = False
|
||||
PRESET_AWAY_INDEFINITELY = "away_indefinitely"
|
||||
@ -129,6 +134,7 @@ SERVICE_SET_FAN_MIN_ON_TIME = "set_fan_min_on_time"
|
||||
SERVICE_SET_DST_MODE = "set_dst_mode"
|
||||
SERVICE_SET_MIC_MODE = "set_mic_mode"
|
||||
SERVICE_SET_OCCUPANCY_MODES = "set_occupancy_modes"
|
||||
SERVICE_SET_SENSORS_USED_IN_CLIMATE = "set_sensors_used_in_climate"
|
||||
|
||||
DTGROUP_START_INCLUSIVE_MSG = (
|
||||
f"{ATTR_START_DATE} and {ATTR_START_TIME} must be specified together"
|
||||
@ -217,7 +223,7 @@ async def async_setup_entry(
|
||||
thermostat["name"],
|
||||
thermostat["modelNumber"],
|
||||
)
|
||||
entities.append(Thermostat(data, index, thermostat))
|
||||
entities.append(Thermostat(data, index, thermostat, hass))
|
||||
|
||||
async_add_entities(entities, True)
|
||||
|
||||
@ -327,6 +333,15 @@ async def async_setup_entry(
|
||||
"set_occupancy_modes",
|
||||
)
|
||||
|
||||
platform.async_register_entity_service(
|
||||
SERVICE_SET_SENSORS_USED_IN_CLIMATE,
|
||||
{
|
||||
vol.Optional(ATTR_PRESET_MODE): cv.string,
|
||||
vol.Required(ATTR_SENSOR_LIST): cv.ensure_list,
|
||||
},
|
||||
"set_sensors_used_in_climate",
|
||||
)
|
||||
|
||||
|
||||
class Thermostat(ClimateEntity):
|
||||
"""A thermostat class for Ecobee."""
|
||||
@ -342,7 +357,11 @@ class Thermostat(ClimateEntity):
|
||||
_attr_translation_key = "ecobee"
|
||||
|
||||
def __init__(
|
||||
self, data: EcobeeData, thermostat_index: int, thermostat: dict
|
||||
self,
|
||||
data: EcobeeData,
|
||||
thermostat_index: int,
|
||||
thermostat: dict,
|
||||
hass: HomeAssistant,
|
||||
) -> None:
|
||||
"""Initialize the thermostat."""
|
||||
self.data = data
|
||||
@ -352,6 +371,7 @@ class Thermostat(ClimateEntity):
|
||||
self.vacation = None
|
||||
self._last_active_hvac_mode = HVACMode.HEAT_COOL
|
||||
self._last_hvac_mode_before_aux_heat = HVACMode.HEAT_COOL
|
||||
self._hass = hass
|
||||
|
||||
self._attr_hvac_modes = []
|
||||
if self.settings["heatStages"] or self.settings["hasHeatPump"]:
|
||||
@ -361,7 +381,11 @@ class Thermostat(ClimateEntity):
|
||||
if len(self._attr_hvac_modes) == 2:
|
||||
self._attr_hvac_modes.insert(0, HVACMode.HEAT_COOL)
|
||||
self._attr_hvac_modes.append(HVACMode.OFF)
|
||||
|
||||
self._sensors = self.remote_sensors
|
||||
self._preset_modes = {
|
||||
comfort["climateRef"]: comfort["name"]
|
||||
for comfort in self.thermostat["program"]["climates"]
|
||||
}
|
||||
self.update_without_throttle = False
|
||||
|
||||
async def async_update(self) -> None:
|
||||
@ -552,6 +576,8 @@ class Thermostat(ClimateEntity):
|
||||
|
||||
return HVACAction.IDLE
|
||||
|
||||
_unrecorded_attributes = frozenset({ATTR_AVAILABLE_SENSORS, ATTR_ACTIVE_SENSORS})
|
||||
|
||||
@property
|
||||
def extra_state_attributes(self) -> dict[str, Any] | None:
|
||||
"""Return device specific state attributes."""
|
||||
@ -563,8 +589,62 @@ class Thermostat(ClimateEntity):
|
||||
),
|
||||
"equipment_running": status,
|
||||
"fan_min_on_time": self.settings["fanMinOnTime"],
|
||||
ATTR_AVAILABLE_SENSORS: self.remote_sensor_devices,
|
||||
ATTR_ACTIVE_SENSORS: self.active_sensor_devices_in_preset_mode,
|
||||
}
|
||||
|
||||
@property
|
||||
def remote_sensors(self) -> list:
|
||||
"""Return the remote sensor names of the thermostat."""
|
||||
sensors_info = self.thermostat.get("remoteSensors", [])
|
||||
return [sensor["name"] for sensor in sensors_info if sensor.get("name")]
|
||||
|
||||
@property
|
||||
def remote_sensor_devices(self) -> list:
|
||||
"""Return the remote sensor device name_by_user or name for the thermostat."""
|
||||
return sorted(
|
||||
[
|
||||
f'{item["name_by_user"]} ({item["id"]})'
|
||||
for item in self.remote_sensor_ids_names
|
||||
]
|
||||
)
|
||||
|
||||
@property
|
||||
def remote_sensor_ids_names(self) -> list:
|
||||
"""Return the remote sensor device id and name_by_user for the thermostat."""
|
||||
sensors_info = self.thermostat.get("remoteSensors", [])
|
||||
device_registry = dr.async_get(self._hass)
|
||||
|
||||
return [
|
||||
{
|
||||
"id": device.id,
|
||||
"name_by_user": device.name_by_user
|
||||
if device.name_by_user
|
||||
else device.name,
|
||||
}
|
||||
for device in device_registry.devices.values()
|
||||
for sensor_info in sensors_info
|
||||
if device.name == sensor_info["name"]
|
||||
]
|
||||
|
||||
@property
|
||||
def active_sensors_in_preset_mode(self) -> list:
|
||||
"""Return the currently active/participating sensors."""
|
||||
# https://support.ecobee.com/s/articles/SmartSensors-Sensor-Participation
|
||||
# During a manual hold, the ecobee will follow the Sensor Participation
|
||||
# rules for the Home Comfort Settings
|
||||
mode = self._preset_modes.get(self.preset_mode, "Home")
|
||||
return self._sensors_in_preset_mode(mode)
|
||||
|
||||
@property
|
||||
def active_sensor_devices_in_preset_mode(self) -> list:
|
||||
"""Return the currently active/participating sensor devices."""
|
||||
# https://support.ecobee.com/s/articles/SmartSensors-Sensor-Participation
|
||||
# During a manual hold, the ecobee will follow the Sensor Participation
|
||||
# rules for the Home Comfort Settings
|
||||
mode = self._preset_modes.get(self.preset_mode, "Home")
|
||||
return self._sensor_devices_in_preset_mode(mode)
|
||||
|
||||
def set_preset_mode(self, preset_mode: str) -> None:
|
||||
"""Activate a preset."""
|
||||
preset_mode = HASS_TO_ECOBEE_PRESET.get(preset_mode, preset_mode)
|
||||
@ -741,6 +821,115 @@ class Thermostat(ClimateEntity):
|
||||
)
|
||||
self.update_without_throttle = True
|
||||
|
||||
def set_sensors_used_in_climate(
|
||||
self, device_ids: list[str], preset_mode: str | None = None
|
||||
) -> None:
|
||||
"""Set the sensors used on a climate for a thermostat."""
|
||||
if preset_mode is None:
|
||||
preset_mode = self.preset_mode
|
||||
|
||||
# Check if climate is an available preset option.
|
||||
elif preset_mode not in self._preset_modes.values():
|
||||
if self.preset_modes:
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="invalid_preset",
|
||||
translation_placeholders={
|
||||
"options": ", ".join(self._preset_modes.values())
|
||||
},
|
||||
)
|
||||
|
||||
# Get device name from device id.
|
||||
device_registry = dr.async_get(self.hass)
|
||||
sensor_names: list[str] = []
|
||||
sensor_ids: list[str] = []
|
||||
for device_id in device_ids:
|
||||
device = device_registry.async_get(device_id)
|
||||
if device and device.name:
|
||||
r_sensors = self.thermostat.get("remoteSensors", [])
|
||||
ecobee_identifier = next(
|
||||
(
|
||||
identifier
|
||||
for identifier in device.identifiers
|
||||
if identifier[0] == "ecobee"
|
||||
),
|
||||
None,
|
||||
)
|
||||
if ecobee_identifier:
|
||||
code = ecobee_identifier[1]
|
||||
for r_sensor in r_sensors:
|
||||
if ( # occurs if remote sensor
|
||||
len(code) == 4 and r_sensor.get("code") == code
|
||||
) or ( # occurs if thermostat
|
||||
len(code) != 4 and r_sensor.get("type") == "thermostat"
|
||||
):
|
||||
sensor_ids.append(r_sensor.get("id")) # noqa: PERF401
|
||||
sensor_names.append(device.name)
|
||||
|
||||
# Ensure sensors provided are available for thermostat or not empty.
|
||||
if not set(sensor_names).issubset(set(self._sensors)) or not sensor_names:
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="invalid_sensor",
|
||||
translation_placeholders={
|
||||
"options": ", ".join(
|
||||
[
|
||||
f'{item["name_by_user"]} ({item["id"]})'
|
||||
for item in self.remote_sensor_ids_names
|
||||
]
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
# Check that an id was found for each sensor
|
||||
if len(device_ids) != len(sensor_ids):
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN, translation_key="sensor_lookup_failed"
|
||||
)
|
||||
|
||||
# Check if sensors are currently used on the climate for the thermostat.
|
||||
current_sensors_in_climate = self._sensors_in_preset_mode(preset_mode)
|
||||
if set(sensor_names) == set(current_sensors_in_climate):
|
||||
_LOGGER.debug(
|
||||
"This action would not be an update, current sensors on climate (%s) are: %s",
|
||||
preset_mode,
|
||||
", ".join(current_sensors_in_climate),
|
||||
)
|
||||
return
|
||||
|
||||
_LOGGER.debug(
|
||||
"Setting sensors %s to be used on thermostat %s for program %s",
|
||||
sensor_names,
|
||||
self.device_info.get("name"),
|
||||
preset_mode,
|
||||
)
|
||||
self.data.ecobee.update_climate_sensors(
|
||||
self.thermostat_index, preset_mode, sensor_ids=sensor_ids
|
||||
)
|
||||
self.update_without_throttle = True
|
||||
|
||||
def _sensors_in_preset_mode(self, preset_mode: str | None) -> list[str]:
|
||||
"""Return current sensors used in climate."""
|
||||
climates = self.thermostat["program"]["climates"]
|
||||
for climate in climates:
|
||||
if climate.get("name") == preset_mode:
|
||||
return [sensor["name"] for sensor in climate["sensors"]]
|
||||
|
||||
return []
|
||||
|
||||
def _sensor_devices_in_preset_mode(self, preset_mode: str | None) -> list[str]:
|
||||
"""Return current sensor device name_by_user or name used in climate."""
|
||||
device_registry = dr.async_get(self._hass)
|
||||
sensor_names = self._sensors_in_preset_mode(preset_mode)
|
||||
return sorted(
|
||||
[
|
||||
device.name_by_user if device.name_by_user else device.name
|
||||
for device in device_registry.devices.values()
|
||||
for sensor_name in sensor_names
|
||||
if device.name == sensor_name
|
||||
]
|
||||
)
|
||||
|
||||
def hold_preference(self):
|
||||
"""Return user preference setting for hold time."""
|
||||
# Values returned from thermostat are:
|
||||
|
@ -23,6 +23,8 @@ DOMAIN = "ecobee"
|
||||
DATA_ECOBEE_CONFIG = "ecobee_config"
|
||||
DATA_HASS_CONFIG = "ecobee_hass_config"
|
||||
ATTR_CONFIG_ENTRY_ID = "entry_id"
|
||||
ATTR_AVAILABLE_SENSORS = "available_sensors"
|
||||
ATTR_ACTIVE_SENSORS = "active_sensors"
|
||||
|
||||
CONF_REFRESH_TOKEN = "refresh_token"
|
||||
|
||||
|
@ -20,6 +20,9 @@
|
||||
},
|
||||
"set_occupancy_modes": {
|
||||
"service": "mdi:eye-settings"
|
||||
},
|
||||
"set_sensors_used_in_climate": {
|
||||
"service": "mdi:home-thermometer"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -134,3 +134,23 @@ set_occupancy_modes:
|
||||
follow_me:
|
||||
selector:
|
||||
boolean:
|
||||
|
||||
set_sensors_used_in_climate:
|
||||
target:
|
||||
entity:
|
||||
integration: ecobee
|
||||
domain: climate
|
||||
fields:
|
||||
preset_mode:
|
||||
example: "Home"
|
||||
selector:
|
||||
text:
|
||||
device_ids:
|
||||
required: true
|
||||
selector:
|
||||
device:
|
||||
multiple: true
|
||||
integration: ecobee
|
||||
entity:
|
||||
- domain: climate
|
||||
- domain: sensor
|
||||
|
@ -167,6 +167,35 @@
|
||||
"description": "Enable Follow Me mode."
|
||||
}
|
||||
}
|
||||
},
|
||||
"set_sensors_used_in_climate": {
|
||||
"name": "Set Sensors Used in Climate",
|
||||
"description": "Sets the participating sensors for a climate.",
|
||||
"fields": {
|
||||
"entity_id": {
|
||||
"name": "Entity",
|
||||
"description": "Ecobee thermostat on which to set active sensors."
|
||||
},
|
||||
"preset_mode": {
|
||||
"name": "Climate Name",
|
||||
"description": "Name of the climate program to set the sensors active on.\nDefaults to currently active program."
|
||||
},
|
||||
"device_ids": {
|
||||
"name": "Sensors",
|
||||
"description": "Sensors to set as participating sensors."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
"invalid_preset": {
|
||||
"message": "Invalid climate name, available options are: {options}"
|
||||
},
|
||||
"invalid_sensor": {
|
||||
"message": "Invalid sensor for thermostat, available options are: {options}"
|
||||
},
|
||||
"sensor_lookup_failed": {
|
||||
"message": "There was an error getting the sensor ids from sensor names. Try reloading the ecobee integration."
|
||||
}
|
||||
},
|
||||
"issues": {
|
||||
|
@ -11,7 +11,7 @@ from tests.common import MockConfigEntry
|
||||
|
||||
async def setup_platform(
|
||||
hass: HomeAssistant,
|
||||
platform: str,
|
||||
platforms: str | list[str],
|
||||
) -> MockConfigEntry:
|
||||
"""Set up the ecobee platform."""
|
||||
mock_entry = MockConfigEntry(
|
||||
@ -24,7 +24,9 @@ async def setup_platform(
|
||||
)
|
||||
mock_entry.add_to_hass(hass)
|
||||
|
||||
with patch("homeassistant.components.ecobee.PLATFORMS", [platform]):
|
||||
platforms = [platforms] if isinstance(platforms, str) else platforms
|
||||
|
||||
with patch("homeassistant.components.ecobee.PLATFORMS", platforms):
|
||||
await hass.config_entries.async_setup(mock_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
return mock_entry
|
||||
|
@ -1,7 +1,7 @@
|
||||
{
|
||||
"thermostatList": [
|
||||
{
|
||||
"identifier": 8675309,
|
||||
"identifier": "8675309",
|
||||
"name": "ecobee",
|
||||
"modelNumber": "athenaSmart",
|
||||
"utcTime": "2022-01-01 10:00:00",
|
||||
@ -11,13 +11,32 @@
|
||||
},
|
||||
"program": {
|
||||
"climates": [
|
||||
{
|
||||
"name": "Home",
|
||||
"climateRef": "home",
|
||||
"sensors": [
|
||||
{
|
||||
"name": "ecobee"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Climate1",
|
||||
"climateRef": "c1"
|
||||
"climateRef": "c1",
|
||||
"sensors": [
|
||||
{
|
||||
"name": "ecobee"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Climate2",
|
||||
"climateRef": "c2"
|
||||
"climateRef": "c2",
|
||||
"sensors": [
|
||||
{
|
||||
"name": "ecobee"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"currentClimateRef": "c1"
|
||||
@ -62,6 +81,24 @@
|
||||
}
|
||||
],
|
||||
"remoteSensors": [
|
||||
{
|
||||
"id": "ei:0",
|
||||
"name": "ecobee",
|
||||
"type": "thermostat",
|
||||
"inUse": true,
|
||||
"capability": [
|
||||
{
|
||||
"id": "1",
|
||||
"type": "temperature",
|
||||
"value": "782"
|
||||
},
|
||||
{
|
||||
"id": "2",
|
||||
"type": "humidity",
|
||||
"value": "54"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "rs:100",
|
||||
"name": "Remote Sensor 1",
|
||||
@ -157,6 +194,25 @@
|
||||
"value": "false"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "rs:101",
|
||||
"name": "Remote Sensor 2",
|
||||
"type": "ecobee3_remote_sensor",
|
||||
"code": "VTRK",
|
||||
"inUse": false,
|
||||
"capability": [
|
||||
{
|
||||
"id": "1",
|
||||
"type": "temperature",
|
||||
"value": "782"
|
||||
},
|
||||
{
|
||||
"id": "2",
|
||||
"type": "occupancy",
|
||||
"value": "false"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -3,16 +3,27 @@
|
||||
from http import HTTPStatus
|
||||
from unittest import mock
|
||||
|
||||
from freezegun.api import FrozenDateTimeFactory
|
||||
import pytest
|
||||
|
||||
from homeassistant import const
|
||||
from homeassistant.components.climate import ClimateEntityFeature
|
||||
from homeassistant.components.ecobee.climate import PRESET_AWAY_INDEFINITELY, Thermostat
|
||||
from homeassistant.const import ATTR_SUPPORTED_FEATURES, STATE_OFF
|
||||
from homeassistant.components.ecobee.climate import (
|
||||
ATTR_PRESET_MODE,
|
||||
ATTR_SENSOR_LIST,
|
||||
PRESET_AWAY_INDEFINITELY,
|
||||
Thermostat,
|
||||
)
|
||||
from homeassistant.components.ecobee.const import DOMAIN
|
||||
from homeassistant.const import ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, STATE_OFF
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ServiceValidationError
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
|
||||
from .common import setup_platform
|
||||
|
||||
from tests.common import MockConfigEntry, async_fire_time_changed
|
||||
|
||||
ENTITY_ID = "climate.ecobee"
|
||||
|
||||
|
||||
@ -25,9 +36,18 @@ def ecobee_fixture():
|
||||
"identifier": "abc",
|
||||
"program": {
|
||||
"climates": [
|
||||
{"name": "Climate1", "climateRef": "c1"},
|
||||
{"name": "Climate2", "climateRef": "c2"},
|
||||
{"name": "Away", "climateRef": "away"},
|
||||
{
|
||||
"name": "Climate1",
|
||||
"climateRef": "c1",
|
||||
"sensors": [{"name": "Ecobee"}],
|
||||
},
|
||||
{
|
||||
"name": "Climate2",
|
||||
"climateRef": "c2",
|
||||
"sensors": [{"name": "Ecobee"}],
|
||||
},
|
||||
{"name": "Away", "climateRef": "away", "sensors": [{"name": "Ecobee"}]},
|
||||
{"name": "Home", "climateRef": "home", "sensors": [{"name": "Ecobee"}]},
|
||||
],
|
||||
"currentClimateRef": "c1",
|
||||
},
|
||||
@ -60,8 +80,19 @@ def ecobee_fixture():
|
||||
"endTime": "10:00:00",
|
||||
}
|
||||
],
|
||||
"remoteSensors": [
|
||||
{
|
||||
"id": "ei:0",
|
||||
"name": "Ecobee",
|
||||
},
|
||||
{
|
||||
"id": "rs2:100",
|
||||
"name": "Remote Sensor 1",
|
||||
},
|
||||
],
|
||||
}
|
||||
mock_ecobee = mock.Mock()
|
||||
mock_ecobee.get = mock.Mock(side_effect=vals.get)
|
||||
mock_ecobee.__getitem__ = mock.Mock(side_effect=vals.__getitem__)
|
||||
mock_ecobee.__setitem__ = mock.Mock(side_effect=vals.__setitem__)
|
||||
return mock_ecobee
|
||||
@ -76,10 +107,10 @@ def data_fixture(ecobee_fixture):
|
||||
|
||||
|
||||
@pytest.fixture(name="thermostat")
|
||||
def thermostat_fixture(data):
|
||||
def thermostat_fixture(data, hass: HomeAssistant):
|
||||
"""Set up ecobee thermostat object."""
|
||||
thermostat = data.ecobee.get_thermostat(1)
|
||||
return Thermostat(data, 1, thermostat)
|
||||
return Thermostat(data, 1, thermostat, hass)
|
||||
|
||||
|
||||
async def test_name(thermostat) -> None:
|
||||
@ -186,6 +217,8 @@ async def test_extra_state_attributes(ecobee_fixture, thermostat) -> None:
|
||||
"climate_mode": "Climate1",
|
||||
"fan_min_on_time": 10,
|
||||
"equipment_running": "heatPump2",
|
||||
"available_sensors": [],
|
||||
"active_sensors": [],
|
||||
}
|
||||
|
||||
ecobee_fixture["equipmentStatus"] = "auxHeat2"
|
||||
@ -194,6 +227,8 @@ async def test_extra_state_attributes(ecobee_fixture, thermostat) -> None:
|
||||
"climate_mode": "Climate1",
|
||||
"fan_min_on_time": 10,
|
||||
"equipment_running": "auxHeat2",
|
||||
"available_sensors": [],
|
||||
"active_sensors": [],
|
||||
}
|
||||
|
||||
ecobee_fixture["equipmentStatus"] = "compCool1"
|
||||
@ -202,6 +237,8 @@ async def test_extra_state_attributes(ecobee_fixture, thermostat) -> None:
|
||||
"climate_mode": "Climate1",
|
||||
"fan_min_on_time": 10,
|
||||
"equipment_running": "compCool1",
|
||||
"available_sensors": [],
|
||||
"active_sensors": [],
|
||||
}
|
||||
ecobee_fixture["equipmentStatus"] = ""
|
||||
assert thermostat.extra_state_attributes == {
|
||||
@ -209,6 +246,8 @@ async def test_extra_state_attributes(ecobee_fixture, thermostat) -> None:
|
||||
"climate_mode": "Climate1",
|
||||
"fan_min_on_time": 10,
|
||||
"equipment_running": "",
|
||||
"available_sensors": [],
|
||||
"active_sensors": [],
|
||||
}
|
||||
|
||||
ecobee_fixture["equipmentStatus"] = "Unknown"
|
||||
@ -217,6 +256,8 @@ async def test_extra_state_attributes(ecobee_fixture, thermostat) -> None:
|
||||
"climate_mode": "Climate1",
|
||||
"fan_min_on_time": 10,
|
||||
"equipment_running": "Unknown",
|
||||
"available_sensors": [],
|
||||
"active_sensors": [],
|
||||
}
|
||||
|
||||
ecobee_fixture["program"]["currentClimateRef"] = "c2"
|
||||
@ -225,6 +266,8 @@ async def test_extra_state_attributes(ecobee_fixture, thermostat) -> None:
|
||||
"climate_mode": "Climate2",
|
||||
"fan_min_on_time": 10,
|
||||
"equipment_running": "Unknown",
|
||||
"available_sensors": [],
|
||||
"active_sensors": [],
|
||||
}
|
||||
|
||||
|
||||
@ -375,3 +418,203 @@ async def test_set_preset_mode(ecobee_fixture, thermostat, data) -> None:
|
||||
data.ecobee.set_climate_hold.assert_has_calls(
|
||||
[mock.call(1, "away", "indefinite", thermostat.hold_hours())]
|
||||
)
|
||||
|
||||
|
||||
async def test_remote_sensors(hass: HomeAssistant) -> None:
|
||||
"""Test remote sensors."""
|
||||
await setup_platform(hass, [const.Platform.CLIMATE, const.Platform.SENSOR])
|
||||
platform = hass.data[const.Platform.CLIMATE].entities
|
||||
for entity in platform:
|
||||
if entity.entity_id == "climate.ecobee":
|
||||
thermostat = entity
|
||||
break
|
||||
|
||||
assert thermostat is not None
|
||||
remote_sensors = thermostat.remote_sensors
|
||||
|
||||
assert sorted(remote_sensors) == sorted(["ecobee", "Remote Sensor 1"])
|
||||
|
||||
|
||||
async def test_remote_sensor_devices(
|
||||
hass: HomeAssistant, freezer: FrozenDateTimeFactory
|
||||
) -> None:
|
||||
"""Test remote sensor devices."""
|
||||
await setup_platform(hass, [const.Platform.CLIMATE, const.Platform.SENSOR])
|
||||
freezer.tick(100)
|
||||
async_fire_time_changed(hass)
|
||||
state = hass.states.get(ENTITY_ID)
|
||||
device_registry = dr.async_get(hass)
|
||||
for device in device_registry.devices.values():
|
||||
if device.name == "Remote Sensor 1":
|
||||
remote_sensor_1_id = device.id
|
||||
if device.name == "ecobee":
|
||||
ecobee_id = device.id
|
||||
assert sorted(state.attributes.get("available_sensors")) == sorted(
|
||||
[f"Remote Sensor 1 ({remote_sensor_1_id})", f"ecobee ({ecobee_id})"]
|
||||
)
|
||||
|
||||
|
||||
async def test_active_sensors_in_preset_mode(hass: HomeAssistant) -> None:
|
||||
"""Test active sensors in preset mode property."""
|
||||
await setup_platform(hass, [const.Platform.CLIMATE, const.Platform.SENSOR])
|
||||
platform = hass.data[const.Platform.CLIMATE].entities
|
||||
for entity in platform:
|
||||
if entity.entity_id == "climate.ecobee":
|
||||
thermostat = entity
|
||||
break
|
||||
|
||||
assert thermostat is not None
|
||||
remote_sensors = thermostat.active_sensors_in_preset_mode
|
||||
|
||||
assert sorted(remote_sensors) == sorted(["ecobee"])
|
||||
|
||||
|
||||
async def test_active_sensor_devices_in_preset_mode(hass: HomeAssistant) -> None:
|
||||
"""Test active sensor devices in preset mode."""
|
||||
await setup_platform(hass, [const.Platform.CLIMATE, const.Platform.SENSOR])
|
||||
state = hass.states.get(ENTITY_ID)
|
||||
|
||||
assert state.attributes.get("active_sensors") == ["ecobee"]
|
||||
|
||||
|
||||
async def test_remote_sensor_ids_names(hass: HomeAssistant) -> None:
|
||||
"""Test getting ids and names_by_user for thermostat."""
|
||||
await setup_platform(hass, [const.Platform.CLIMATE, const.Platform.SENSOR])
|
||||
platform = hass.data[const.Platform.CLIMATE].entities
|
||||
for entity in platform:
|
||||
if entity.entity_id == "climate.ecobee":
|
||||
thermostat = entity
|
||||
break
|
||||
|
||||
assert thermostat is not None
|
||||
|
||||
remote_sensor_ids_names = thermostat.remote_sensor_ids_names
|
||||
for id_name in remote_sensor_ids_names:
|
||||
assert id_name.get("id") is not None
|
||||
|
||||
name_by_user_list = [item["name_by_user"] for item in remote_sensor_ids_names]
|
||||
assert sorted(name_by_user_list) == sorted(["Remote Sensor 1", "ecobee"])
|
||||
|
||||
|
||||
async def test_set_sensors_used_in_climate(hass: HomeAssistant) -> None:
|
||||
"""Test set sensors used in climate."""
|
||||
# Get device_id of remote sensor from the device registry.
|
||||
await setup_platform(hass, [const.Platform.CLIMATE, const.Platform.SENSOR])
|
||||
device_registry = dr.async_get(hass)
|
||||
for device in device_registry.devices.values():
|
||||
if device.name == "Remote Sensor 1":
|
||||
remote_sensor_1_id = device.id
|
||||
if device.name == "ecobee":
|
||||
ecobee_id = device.id
|
||||
if device.name == "Remote Sensor 2":
|
||||
remote_sensor_2_id = device.id
|
||||
|
||||
entry = MockConfigEntry(domain="test")
|
||||
entry.add_to_hass(hass)
|
||||
device_from_other_integration = device_registry.async_get_or_create(
|
||||
config_entry_id=entry.entry_id, identifiers={("test", "unique")}
|
||||
)
|
||||
|
||||
# Test that the function call works in its entirety.
|
||||
with mock.patch("pyecobee.Ecobee.update_climate_sensors") as mock_sensors:
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
"set_sensors_used_in_climate",
|
||||
{
|
||||
ATTR_ENTITY_ID: ENTITY_ID,
|
||||
ATTR_PRESET_MODE: "Climate1",
|
||||
ATTR_SENSOR_LIST: [remote_sensor_1_id],
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
mock_sensors.assert_called_once_with(0, "Climate1", sensor_ids=["rs:100"])
|
||||
|
||||
# Update sensors without preset mode.
|
||||
with mock.patch("pyecobee.Ecobee.update_climate_sensors") as mock_sensors:
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
"set_sensors_used_in_climate",
|
||||
{
|
||||
ATTR_ENTITY_ID: ENTITY_ID,
|
||||
ATTR_SENSOR_LIST: [remote_sensor_1_id],
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
# `temp` is the preset running because of a hold.
|
||||
mock_sensors.assert_called_once_with(0, "temp", sensor_ids=["rs:100"])
|
||||
|
||||
# Check that sensors are not updated when the sent sensors are the currently set sensors.
|
||||
with mock.patch("pyecobee.Ecobee.update_climate_sensors") as mock_sensors:
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
"set_sensors_used_in_climate",
|
||||
{
|
||||
ATTR_ENTITY_ID: ENTITY_ID,
|
||||
ATTR_PRESET_MODE: "Climate1",
|
||||
ATTR_SENSOR_LIST: [ecobee_id],
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
mock_sensors.assert_not_called()
|
||||
|
||||
# Error raised because invalid climate name.
|
||||
with pytest.raises(ServiceValidationError) as execinfo:
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
"set_sensors_used_in_climate",
|
||||
{
|
||||
ATTR_ENTITY_ID: ENTITY_ID,
|
||||
ATTR_PRESET_MODE: "InvalidClimate",
|
||||
ATTR_SENSOR_LIST: [remote_sensor_1_id],
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
assert execinfo.value.translation_domain == "ecobee"
|
||||
assert execinfo.value.translation_key == "invalid_preset"
|
||||
|
||||
## Error raised because invalid sensor.
|
||||
with pytest.raises(ServiceValidationError) as execinfo:
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
"set_sensors_used_in_climate",
|
||||
{
|
||||
ATTR_ENTITY_ID: ENTITY_ID,
|
||||
ATTR_PRESET_MODE: "Climate1",
|
||||
ATTR_SENSOR_LIST: ["abcd"],
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
assert execinfo.value.translation_domain == "ecobee"
|
||||
assert execinfo.value.translation_key == "invalid_sensor"
|
||||
|
||||
## Error raised because sensor not available on device.
|
||||
with pytest.raises(ServiceValidationError):
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
"set_sensors_used_in_climate",
|
||||
{
|
||||
ATTR_ENTITY_ID: ENTITY_ID,
|
||||
ATTR_PRESET_MODE: "Climate1",
|
||||
ATTR_SENSOR_LIST: [remote_sensor_2_id],
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
with pytest.raises(ServiceValidationError) as execinfo:
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
"set_sensors_used_in_climate",
|
||||
{
|
||||
ATTR_ENTITY_ID: ENTITY_ID,
|
||||
ATTR_PRESET_MODE: "Climate1",
|
||||
ATTR_SENSOR_LIST: [
|
||||
remote_sensor_1_id,
|
||||
device_from_other_integration.id,
|
||||
],
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
assert execinfo.value.translation_domain == "ecobee"
|
||||
assert execinfo.value.translation_key == "sensor_lookup_failed"
|
||||
|
Reference in New Issue
Block a user