From 504ce8e93aa90b3491f2292cb8a3210631aefc39 Mon Sep 17 00:00:00 2001 From: PeteRager <76050312+PeteRager@users.noreply.github.com> Date: Wed, 28 Sep 2022 22:23:11 -0400 Subject: [PATCH] Set nest entities as unavailable on lost connection (#78773) * NEST - Issues with lost internet connectivity #70479 Update Climate and Sensor entities to be unavailable when the device connectivity trait indicates the device is offline. The prior behavior, the last known values would be displayed indefinitely if the device lost internet connectivity. This was creating the illusion that the device was still connected. With this change, the Home Assistant entities will become unavailable when the device loses connectivity. * Update formatting * Add doc strings, fix indentation * Fix doc strings * Update test_climate_sdm.py * Update test_climate_sdm.py * Update test_sensor_sdm.py * Update test_sensor_sdm.py * more formatting fixes * Place availability logic in mixin 1. Consolidate repeated code into mixin and apply mixin to Climate and Sensor entities 2. Return true instead of super.available() 3. No unit test changes required to maintain code coverage * Define self._device is mixin to make linter happier * Remove logger used for debugging * restore whitespace * Fix test due to underlying merge change * Update availability_mixin.py * Move availability logic into device_info * Update sensor_sdm.py --- homeassistant/components/nest/climate_sdm.py | 5 ++ homeassistant/components/nest/const.py | 2 + homeassistant/components/nest/device_info.py | 13 +++- homeassistant/components/nest/sensor_sdm.py | 5 ++ tests/components/nest/test_climate_sdm.py | 66 +++++++++++++++++++- tests/components/nest/test_sensor_sdm.py | 53 ++++++++++++++++ 6 files changed, 141 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/nest/climate_sdm.py b/homeassistant/components/nest/climate_sdm.py index e40db60d5ed..3113cb2dd40 100644 --- a/homeassistant/components/nest/climate_sdm.py +++ b/homeassistant/components/nest/climate_sdm.py @@ -117,6 +117,11 @@ class ThermostatEntity(ClimateEntity): """Return device specific attributes.""" return self._device_info.device_info + @property + def available(self) -> bool: + """Return device availability.""" + return self._device_info.available + async def async_added_to_hass(self) -> None: """Run when entity is added to register update signal handler.""" self._attr_supported_features = self._get_supported_features() diff --git a/homeassistant/components/nest/const.py b/homeassistant/components/nest/const.py index 64c27c1643b..853e778977d 100644 --- a/homeassistant/components/nest/const.py +++ b/homeassistant/components/nest/const.py @@ -14,6 +14,8 @@ CONF_SUBSCRIBER_ID = "subscriber_id" CONF_SUBSCRIBER_ID_IMPORTED = "subscriber_id_imported" CONF_CLOUD_PROJECT_ID = "cloud_project_id" +CONNECTIVITY_TRAIT_OFFLINE = "OFFLINE" + SIGNAL_NEST_UPDATE = "nest_update" # For the Google Nest Device Access API diff --git a/homeassistant/components/nest/device_info.py b/homeassistant/components/nest/device_info.py index 2d2b01d3849..e269b76fcc4 100644 --- a/homeassistant/components/nest/device_info.py +++ b/homeassistant/components/nest/device_info.py @@ -5,13 +5,13 @@ from __future__ import annotations from collections.abc import Mapping from google_nest_sdm.device import Device -from google_nest_sdm.device_traits import InfoTrait +from google_nest_sdm.device_traits import ConnectivityTrait, InfoTrait from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import device_registry as dr from homeassistant.helpers.entity import DeviceInfo -from .const import DATA_DEVICE_MANAGER, DOMAIN +from .const import CONNECTIVITY_TRAIT_OFFLINE, DATA_DEVICE_MANAGER, DOMAIN DEVICE_TYPE_MAP: dict[str, str] = { "sdm.devices.types.CAMERA": "Camera", @@ -30,6 +30,15 @@ class NestDeviceInfo: """Initialize the DeviceInfo.""" self._device = device + @property + def available(self) -> bool: + """Return device availability.""" + if ConnectivityTrait.NAME in self._device.traits: + trait: ConnectivityTrait = self._device.traits[ConnectivityTrait.NAME] + if trait.status == CONNECTIVITY_TRAIT_OFFLINE: + return False + return True + @property def device_info(self) -> DeviceInfo: """Return device specific attributes.""" diff --git a/homeassistant/components/nest/sensor_sdm.py b/homeassistant/components/nest/sensor_sdm.py index 11edc9f3506..b36e9103196 100644 --- a/homeassistant/components/nest/sensor_sdm.py +++ b/homeassistant/components/nest/sensor_sdm.py @@ -62,6 +62,11 @@ class SensorBase(SensorEntity): self._attr_unique_id = f"{device.name}-{self.device_class}" self._attr_device_info = self._device_info.device_info + @property + def available(self) -> bool: + """Return the device availability.""" + return self._device_info.available + async def async_added_to_hass(self) -> None: """Run when entity is added to register update signal handler.""" self.async_on_remove( diff --git a/tests/components/nest/test_climate_sdm.py b/tests/components/nest/test_climate_sdm.py index 440855f6ab7..4ac58171fcd 100644 --- a/tests/components/nest/test_climate_sdm.py +++ b/tests/components/nest/test_climate_sdm.py @@ -34,7 +34,11 @@ from homeassistant.components.climate import ( HVACAction, HVACMode, ) -from homeassistant.const import ATTR_SUPPORTED_FEATURES, ATTR_TEMPERATURE +from homeassistant.const import ( + ATTR_SUPPORTED_FEATURES, + ATTR_TEMPERATURE, + STATE_UNAVAILABLE, +) from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError @@ -1442,3 +1446,63 @@ async def test_thermostat_hvac_mode_failure( with pytest.raises(HomeAssistantError): await common.async_set_preset_mode(hass, PRESET_ECO) await hass.async_block_till_done() + + +async def test_thermostat_available( + hass: HomeAssistant, setup_platform: PlatformSetup, create_device: CreateDevice +): + """Test a thermostat that is available.""" + create_device.create( + { + "sdm.devices.traits.ThermostatHvac": { + "status": "COOLING", + }, + "sdm.devices.traits.ThermostatMode": { + "availableModes": ["HEAT", "COOL", "HEATCOOL", "OFF"], + "mode": "COOL", + }, + "sdm.devices.traits.Temperature": { + "ambientTemperatureCelsius": 29.9, + }, + "sdm.devices.traits.ThermostatTemperatureSetpoint": { + "coolCelsius": 28.0, + }, + "sdm.devices.traits.Connectivity": {"status": "ONLINE"}, + }, + ) + await setup_platform() + + assert len(hass.states.async_all()) == 1 + thermostat = hass.states.get("climate.my_thermostat") + assert thermostat is not None + assert thermostat.state == HVACMode.COOL + + +async def test_thermostat_unavailable( + hass: HomeAssistant, setup_platform: PlatformSetup, create_device: CreateDevice +): + """Test a thermostat that is unavailable.""" + create_device.create( + { + "sdm.devices.traits.ThermostatHvac": { + "status": "COOLING", + }, + "sdm.devices.traits.ThermostatMode": { + "availableModes": ["HEAT", "COOL", "HEATCOOL", "OFF"], + "mode": "COOL", + }, + "sdm.devices.traits.Temperature": { + "ambientTemperatureCelsius": 29.9, + }, + "sdm.devices.traits.ThermostatTemperatureSetpoint": { + "coolCelsius": 28.0, + }, + "sdm.devices.traits.Connectivity": {"status": "OFFLINE"}, + }, + ) + await setup_platform() + + assert len(hass.states.async_all()) == 1 + thermostat = hass.states.get("climate.my_thermostat") + assert thermostat is not None + assert thermostat.state == STATE_UNAVAILABLE diff --git a/tests/components/nest/test_sensor_sdm.py b/tests/components/nest/test_sensor_sdm.py index d1a89317959..c3698cf4123 100644 --- a/tests/components/nest/test_sensor_sdm.py +++ b/tests/components/nest/test_sensor_sdm.py @@ -20,6 +20,7 @@ from homeassistant.const import ( ATTR_FRIENDLY_NAME, ATTR_UNIT_OF_MEASUREMENT, PERCENTAGE, + STATE_UNAVAILABLE, TEMP_CELSIUS, ) from homeassistant.core import HomeAssistant @@ -90,6 +91,58 @@ async def test_thermostat_device( assert device.identifiers == {("nest", DEVICE_ID)} +async def test_thermostat_device_available( + hass: HomeAssistant, create_device: CreateDevice, setup_platform: PlatformSetup +): + """Test a thermostat with temperature and humidity sensors that is Online.""" + create_device.create( + { + "sdm.devices.traits.Temperature": { + "ambientTemperatureCelsius": 25.1, + }, + "sdm.devices.traits.Humidity": { + "ambientHumidityPercent": 35.0, + }, + "sdm.devices.traits.Connectivity": {"status": "ONLINE"}, + } + ) + await setup_platform() + + temperature = hass.states.get("sensor.my_sensor_temperature") + assert temperature is not None + assert temperature.state == "25.1" + + humidity = hass.states.get("sensor.my_sensor_humidity") + assert humidity is not None + assert humidity.state == "35" + + +async def test_thermostat_device_unavailable( + hass: HomeAssistant, create_device: CreateDevice, setup_platform: PlatformSetup +): + """Test a thermostat with temperature and humidity sensors that is Offline.""" + create_device.create( + { + "sdm.devices.traits.Temperature": { + "ambientTemperatureCelsius": 25.1, + }, + "sdm.devices.traits.Humidity": { + "ambientHumidityPercent": 35.0, + }, + "sdm.devices.traits.Connectivity": {"status": "OFFLINE"}, + } + ) + await setup_platform() + + temperature = hass.states.get("sensor.my_sensor_temperature") + assert temperature is not None + assert temperature.state == STATE_UNAVAILABLE + + humidity = hass.states.get("sensor.my_sensor_humidity") + assert humidity is not None + assert humidity.state == STATE_UNAVAILABLE + + async def test_no_devices(hass: HomeAssistant, setup_platform: PlatformSetup): """Test no devices returned by the api.""" await setup_platform()