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:
parent
30f4ee121a
commit
549e8cf2c5
7 changed files with 112 additions and 19 deletions
|
@ -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."""
|
||||
|
|
|
@ -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},
|
||||
)
|
||||
)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Add table
Reference in a new issue