Hue: Create new config flow when auth is lost (#28204)

* Hue: Create new config flow when auth is lost

* Fix tests

* Fix tests

* Comments

* Lint
This commit is contained in:
Bram Kragten 2019-10-28 16:45:08 +01:00 committed by Paulus Schoutsen
parent 30f4ee121a
commit 549e8cf2c5
7 changed files with 112 additions and 19 deletions

View file

@ -5,12 +5,12 @@ import aiohue
import async_timeout
import voluptuous as vol
from homeassistant import config_entries
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import aiohttp_client, config_validation as cv
from .const import DOMAIN, LOGGER
from .errors import AuthenticationRequired, CannotConnect
from .helpers import create_config_flow
SERVICE_HUE_SCENE = "hue_activate_scene"
ATTR_GROUP_NAME = "group_name"
@ -30,6 +30,7 @@ class HueBridge:
self.allow_unreachable = allow_unreachable
self.allow_groups = allow_groups
self.available = True
self.authorized = False
self.api = None
@property
@ -49,13 +50,7 @@ class HueBridge:
# We are going to fail the config entry setup and initiate a new
# linking procedure. When linking succeeds, it will remove the
# old config entry.
hass.async_create_task(
hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_IMPORT},
data={"host": host},
)
)
create_config_flow(hass, host)
return False
except CannotConnect:
@ -82,6 +77,7 @@ class HueBridge:
DOMAIN, SERVICE_HUE_SCENE, self.hue_activate_scene, schema=SCENE_SCHEMA
)
self.authorized = True
return True
async def async_reset(self):
@ -155,6 +151,17 @@ class HueBridge:
await group.set_action(scene=scene.id)
async def handle_unauthorized_error(self):
"""Create a new config flow when the authorization is no longer valid."""
if not self.authorized:
# we already created a new config flow, no need to do it again
return
LOGGER.error(
"Unable to authorize to bridge %s, setup the linking again.", self.host
)
self.authorized = False
create_config_flow(self.hass, self.host)
async def get_bridge(hass, host, username=None):
"""Create a bridge object and verify authentication."""

View file

@ -1,6 +1,7 @@
"""Helper functions for Philips Hue."""
from homeassistant.helpers.device_registry import async_get_registry as get_dev_reg
from homeassistant.helpers.entity_registry import async_get_registry as get_ent_reg
from homeassistant import config_entries
from .const import DOMAIN
@ -31,3 +32,14 @@ async def remove_devices(hass, config_entry, api_ids, current):
for item_id in removed_items:
del current[item_id]
def create_config_flow(hass, host):
"""Start a config flow."""
hass.async_create_task(
hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_IMPORT},
data={"host": host},
)
)

View file

@ -189,6 +189,9 @@ async def async_update_items(
progress_waiting,
):
"""Update either groups or lights from the bridge."""
if not bridge.authorized:
return
if is_group:
api_type = "group"
api = bridge.api.groups
@ -200,6 +203,9 @@ async def async_update_items(
start = monotonic()
with async_timeout.timeout(4):
await api.update()
except aiohue.Unauthorized:
await bridge.handle_unauthorized_error()
return
except (asyncio.TimeoutError, aiohue.AiohueException) as err:
_LOGGER.debug("Failed to fetch %s: %s", api_type, err)
@ -337,10 +343,14 @@ class HueLight(Light):
@property
def available(self):
"""Return if light is available."""
return self.bridge.available and (
self.is_group
or self.bridge.allow_unreachable
or self.light.state["reachable"]
return (
self.bridge.available
and self.bridge.authorized
and (
self.is_group
or self.bridge.allow_unreachable
or self.light.state["reachable"]
)
)
@property

View file

@ -4,7 +4,7 @@ from datetime import timedelta
import logging
from time import monotonic
from aiohue import AiohueException
from aiohue import AiohueException, Unauthorized
from aiohue.sensors import TYPE_ZLL_PRESENCE
import async_timeout
@ -80,6 +80,11 @@ class SensorManager:
async def async_update_bridge(now):
"""Will update sensors from the bridge."""
# don't update when we are not authorized
if not self.bridge.authorized:
return
await self.async_update_items()
async_track_point_in_utc_time(
@ -96,6 +101,9 @@ class SensorManager:
start = monotonic()
with async_timeout.timeout(4):
await api.update()
except Unauthorized:
await self.bridge.handle_unauthorized_error()
return
except (asyncio.TimeoutError, AiohueException) as err:
_LOGGER.debug("Failed to fetch sensor: %s", err)
@ -220,8 +228,10 @@ class GenericHueSensor:
@property
def available(self):
"""Return if sensor is available."""
return self.bridge.available and (
self.bridge.allow_unreachable or self.sensor.config["reachable"]
return (
self.bridge.available
and self.bridge.authorized
and (self.bridge.allow_unreachable or self.sensor.config["reachable"])
)
@property

View file

@ -91,3 +91,25 @@ async def test_reset_unloads_entry_if_setup():
assert len(hass.config_entries.async_forward_entry_unload.mock_calls) == 3
assert len(hass.services.async_remove.mock_calls) == 1
async def test_handle_unauthorized():
"""Test handling an unauthorized error on update."""
hass = Mock()
entry = Mock()
entry.data = {"host": "1.2.3.4", "username": "mock-username"}
hue_bridge = bridge.HueBridge(hass, entry, False, False)
with patch.object(bridge, "get_bridge", return_value=mock_coro(Mock())):
assert await hue_bridge.async_setup() is True
assert hue_bridge.authorized is True
await hue_bridge.handle_unauthorized_error()
assert hue_bridge.authorized is False
assert len(hass.async_create_task.mock_calls) == 4
assert len(hass.config_entries.flow.async_init.mock_calls) == 1
assert hass.config_entries.flow.async_init.mock_calls[0][2]["data"] == {
"host": "1.2.3.4"
}

View file

@ -180,6 +180,7 @@ def mock_bridge(hass):
"""Mock a Hue bridge."""
bridge = Mock(
available=True,
authorized=True,
allow_unreachable=False,
allow_groups=False,
api=Mock(),
@ -598,13 +599,13 @@ async def test_update_timeout(hass, mock_bridge):
async def test_update_unauthorized(hass, mock_bridge):
"""Test bridge marked as not available if unauthorized during update."""
"""Test bridge marked as not authorized if unauthorized during update."""
mock_bridge.api.lights.update = Mock(side_effect=aiohue.Unauthorized)
mock_bridge.api.groups.update = Mock(side_effect=aiohue.Unauthorized)
await setup_bridge(hass, mock_bridge)
assert len(mock_bridge.mock_requests) == 0
assert len(hass.states.async_all()) == 0
assert mock_bridge.available is False
assert len(mock_bridge.handle_unauthorized_error.mock_calls) == 1
async def test_light_turn_on_service(hass, mock_bridge):

View file

@ -256,6 +256,7 @@ def create_mock_bridge():
"""Create a mock Hue bridge."""
bridge = Mock(
available=True,
authorized=True,
allow_unreachable=False,
allow_groups=False,
api=Mock(),
@ -425,6 +426,36 @@ async def test_new_sensor_discovered(hass, mock_bridge):
assert temperature.state == "17.75"
async def test_sensor_removed(hass, mock_bridge):
"""Test if 2nd update has removed sensor."""
mock_bridge.mock_sensor_responses.append(SENSOR_RESPONSE)
await setup_bridge(hass, mock_bridge)
assert len(mock_bridge.mock_requests) == 1
assert len(hass.states.async_all()) == 6
mock_bridge.mock_sensor_responses.clear()
keys = ("1", "2", "3")
mock_bridge.mock_sensor_responses.append({k: SENSOR_RESPONSE[k] for k in keys})
# Force updates to run again
sm_key = hue_sensor_base.SENSOR_MANAGER_FORMAT.format("mock-host")
sm = hass.data[hue.DOMAIN][sm_key]
await sm.async_update_items()
# To flush out the service call to update the group
await hass.async_block_till_done()
assert len(mock_bridge.mock_requests) == 2
assert len(hass.states.async_all()) == 3
sensor = hass.states.get("binary_sensor.living_room_sensor_motion")
assert sensor is not None
removed_sensor = hass.states.get("binary_sensor.kitchen_sensor_motion")
assert removed_sensor is None
async def test_update_timeout(hass, mock_bridge):
"""Test bridge marked as not available if timeout error during update."""
mock_bridge.api.sensors.update = Mock(side_effect=asyncio.TimeoutError)
@ -435,9 +466,9 @@ async def test_update_timeout(hass, mock_bridge):
async def test_update_unauthorized(hass, mock_bridge):
"""Test bridge marked as not available if unauthorized during update."""
"""Test bridge marked as not authorized if unauthorized during update."""
mock_bridge.api.sensors.update = Mock(side_effect=aiohue.Unauthorized)
await setup_bridge(hass, mock_bridge)
assert len(mock_bridge.mock_requests) == 0
assert len(hass.states.async_all()) == 0
assert mock_bridge.available is False
assert len(mock_bridge.handle_unauthorized_error.mock_calls) == 1