diff --git a/homeassistant/components/nest/__init__.py b/homeassistant/components/nest/__init__.py index ae32527bdc4..16ff05f9091 100644 --- a/homeassistant/components/nest/__init__.py +++ b/homeassistant/components/nest/__init__.py @@ -30,7 +30,11 @@ from homeassistant.helpers import ( config_entry_oauth2_flow, config_validation as cv, ) -from homeassistant.helpers.dispatcher import async_dispatcher_connect, dispatcher_send +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, + async_dispatcher_send, + dispatcher_send, +) from homeassistant.helpers.entity import Entity from . import api, config_flow, local_auth @@ -176,7 +180,7 @@ class SignalUpdateCallback(EventCallback): # This event triggered an update to a device that changed some # properties which the DeviceManager should already have received. # Send a signal to refresh state of all listening devices. - dispatcher_send(self._hass, SIGNAL_NEST_UPDATE) + async_dispatcher_send(self._hass, SIGNAL_NEST_UPDATE) async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): @@ -203,7 +207,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): auth, config[CONF_PROJECT_ID], config[CONF_SUBSCRIBER_ID] ) subscriber.set_update_callback(SignalUpdateCallback(hass)) - hass.loop.create_task(subscriber.start_async()) + asyncio.create_task(subscriber.start_async()) hass.data[DOMAIN][entry.entry_id] = subscriber for component in PLATFORMS: diff --git a/homeassistant/components/nest/config_flow.py b/homeassistant/components/nest/config_flow.py index aa4b7af7e12..e792b496da5 100644 --- a/homeassistant/components/nest/config_flow.py +++ b/homeassistant/components/nest/config_flow.py @@ -61,6 +61,10 @@ class CodeInvalid(NestAuthError): """Raised when invalid authorization code.""" +class UnexpectedStateError(HomeAssistantError): + """Raised when the config flow is invoked in a 'should not happen' case.""" + + @config_entries.HANDLERS.register(DOMAIN) class NestFlowHandler( config_entry_oauth2_flow.AbstractOAuth2FlowHandler, domain=DOMAIN @@ -111,7 +115,7 @@ class NestFlowHandler( async def async_step_init(self, user_input=None): """Handle a flow start.""" if self.is_sdm_api(): - return None + raise UnexpectedStateError("Step only supported for legacy API") flows = self.hass.data.get(DATA_FLOW_IMPL, {}) @@ -142,7 +146,7 @@ class NestFlowHandler( deliver the authentication code. """ if self.is_sdm_api(): - return None + raise UnexpectedStateError("Step only supported for legacy API") flow = self.hass.data[DATA_FLOW_IMPL][self.flow_impl] @@ -185,7 +189,7 @@ class NestFlowHandler( async def async_step_import(self, info): """Import existing auth from Nest.""" if self.is_sdm_api(): - return None + raise UnexpectedStateError("Step only supported for legacy API") if self.hass.config_entries.async_entries(DOMAIN): return self.async_abort(reason="single_instance_allowed") diff --git a/homeassistant/components/nest/sensor.py b/homeassistant/components/nest/sensor.py index b3df3dd393e..794e8210f82 100644 --- a/homeassistant/components/nest/sensor.py +++ b/homeassistant/components/nest/sensor.py @@ -13,5 +13,5 @@ async def async_setup_entry( ) -> None: """Set up the sensors.""" if DATA_SDM not in entry.data: - return await async_setup_legacy_entry(hass, entry, async_add_entities) - return await async_setup_sdm_entry(hass, entry, async_add_entities) + await async_setup_legacy_entry(hass, entry, async_add_entities) + await async_setup_sdm_entry(hass, entry, async_add_entities) diff --git a/homeassistant/components/nest/sensor_sdm.py b/homeassistant/components/nest/sensor_sdm.py index 8c567c9b36e..6c4e37e57b3 100644 --- a/homeassistant/components/nest/sensor_sdm.py +++ b/homeassistant/components/nest/sensor_sdm.py @@ -18,6 +18,13 @@ from homeassistant.helpers.typing import HomeAssistantType from .const import DOMAIN, SIGNAL_NEST_UPDATE +DEVICE_TYPE_MAP = { + "sdm.devices.types.CAMERA": "Camera", + "sdm.devices.types.DISPLAY": "Display", + "sdm.devices.types.DOORBELL": "Doorbell", + "sdm.devices.types.THERMOSTAT": "Thermostat", +} + async def async_setup_sdm_entry( hass: HomeAssistantType, entry: ConfigEntry, async_add_entities @@ -46,7 +53,7 @@ class SensorBase(Entity): self._device = device @property - def should_pool(self) -> bool: + def should_poll(self) -> bool: """Disable polling since entities have state pushed via pubsub.""" return False @@ -89,28 +96,19 @@ class SensorBase(Entity): # The API intentionally returns minimal information about specific # devices, instead relying on traits, but we can infer a generic model # name based on the type - if self._device.type == "sdm.devices.types.CAMERA": - return "Camera" - if self._device.type == "sdm.devices.types.DISPLAY": - return "Display" - if self._device.type == "sdm.devices.types.DOORBELL": - return "Doorbell" - if self._device.type == "sdm.devices.types.THERMOSTAT": - return "Thermostat" - return None + return DEVICE_TYPE_MAP.get(self._device.type) async def async_added_to_hass(self): """Run when entity is added to register update signal handler.""" - - async def async_update_state(): - """Update sensor state.""" - await self.async_update_ha_state(True) - # Event messages trigger the SIGNAL_NEST_UPDATE, which is intercepted # here to re-fresh the signals from _device. Unregister this callback # when the entity is removed. self.async_on_remove( - async_dispatcher_connect(self.hass, SIGNAL_NEST_UPDATE, async_update_state) + async_dispatcher_connect( + self.hass, + SIGNAL_NEST_UPDATE, + self.async_write_ha_state, + ) ) diff --git a/tests/components/nest/sensor_sdm_test.py b/tests/components/nest/sensor_sdm_test.py index fbd13701260..ab2cae3d9f3 100644 --- a/tests/components/nest/sensor_sdm_test.py +++ b/tests/components/nest/sensor_sdm_test.py @@ -41,6 +41,8 @@ CONFIG_ENTRY_DATA = { }, } +THERMOSTAT_TYPE = "sdm.devices.types.THERMOSTAT" + class FakeDeviceManager(DeviceManager): """Fake DeviceManager that can supply a list of devices and structures.""" @@ -104,7 +106,7 @@ async def setup_sensor(hass, devices={}, structures={}): "homeassistant.components.nest.GoogleNestSubscriber", return_value=subscriber ): assert await async_setup_component(hass, DOMAIN, CONFIG) - await hass.async_block_till_done() + await hass.async_block_till_done() return subscriber @@ -114,7 +116,7 @@ async def test_thermostat_device(hass): "some-device-id": Device.MakeDevice( { "name": "some-device-id", - "type": "sdm.devices.types.Thermostat", + "type": THERMOSTAT_TYPE, "traits": { "sdm.devices.traits.Info": { "customName": "My Sensor", @@ -140,6 +142,18 @@ async def test_thermostat_device(hass): assert humidity is not None assert humidity.state == "35.0" + registry = await hass.helpers.entity_registry.async_get_registry() + entry = registry.async_get("sensor.my_sensor_temperature") + assert entry.unique_id == "some-device-id-temperature" + assert entry.original_name == "My Sensor Temperature" + assert entry.domain == "sensor" + + device_registry = await hass.helpers.device_registry.async_get_registry() + device = device_registry.async_get(entry.device_id) + assert device.name == "My Sensor" + assert device.model == "Thermostat" + assert device.identifiers == {("nest", "some-device-id")} + async def test_no_devices(hass): """Test no devices returned by the api.""" @@ -158,7 +172,7 @@ async def test_device_no_sensor_traits(hass): "some-device-id": Device.MakeDevice( { "name": "some-device-id", - "type": "sdm.devices.types.Thermostat", + "type": THERMOSTAT_TYPE, "traits": {}, }, auth=None, @@ -179,7 +193,7 @@ async def test_device_name_from_structure(hass): "some-device-id": Device.MakeDevice( { "name": "some-device-id", - "type": "sdm.devices.types.Thermostat", + "type": THERMOSTAT_TYPE, "traits": { "sdm.devices.traits.Temperature": { "ambientTemperatureCelsius": 25.2, @@ -205,7 +219,7 @@ async def test_event_updates_sensor(hass): "some-device-id": Device.MakeDevice( { "name": "some-device-id", - "type": "sdm.devices.types.Thermostat", + "type": THERMOSTAT_TYPE, "traits": { "sdm.devices.traits.Info": { "customName": "My Sensor", @@ -246,3 +260,41 @@ async def test_event_updates_sensor(hass): temperature = hass.states.get("sensor.my_sensor_temperature") assert temperature is not None assert temperature.state == "26.2" + + +async def test_device_with_unknown_type(hass): + """Test a device without a custom name, inferring name from structure.""" + devices = { + "some-device-id": Device.MakeDevice( + { + "name": "some-device-id", + "type": "some-unknown-type", + "traits": { + "sdm.devices.traits.Info": { + "customName": "My Sensor", + }, + "sdm.devices.traits.Temperature": { + "ambientTemperatureCelsius": 25.1, + }, + }, + }, + auth=None, + ) + } + await setup_sensor(hass, devices) + + temperature = hass.states.get("sensor.my_sensor_temperature") + assert temperature is not None + assert temperature.state == "25.1" + + registry = await hass.helpers.entity_registry.async_get_registry() + entry = registry.async_get("sensor.my_sensor_temperature") + assert entry.unique_id == "some-device-id-temperature" + assert entry.original_name == "My Sensor Temperature" + assert entry.domain == "sensor" + + device_registry = await hass.helpers.device_registry.async_get_registry() + device = device_registry.async_get(entry.device_id) + assert device.name == "My Sensor" + assert device.model is None + assert device.identifiers == {("nest", "some-device-id")}