mirror of
https://github.com/home-assistant/core.git
synced 2026-04-06 23:47:33 +00:00
Add support for migrated Hue bridge (#151411)
Co-authored-by: Joostlek <joostlek@outlook.com>
This commit is contained in:
committed by
Franck Nijhof
parent
cdf7d8df16
commit
7fc8da6769
@@ -9,7 +9,9 @@ from typing import Any
|
||||
import aiohttp
|
||||
from aiohue import LinkButtonNotPressed, create_app_key
|
||||
from aiohue.discovery import DiscoveredHueBridge, discover_bridge, discover_nupnp
|
||||
from aiohue.errors import AiohueException
|
||||
from aiohue.util import normalize_bridge_id
|
||||
from aiohue.v2 import HueBridgeV2
|
||||
import slugify as unicode_slug
|
||||
import voluptuous as vol
|
||||
|
||||
@@ -40,6 +42,9 @@ HUE_MANUFACTURERURL = ("http://www.philips.com", "http://www.philips-hue.com")
|
||||
HUE_IGNORED_BRIDGE_NAMES = ["Home Assistant Bridge", "Espalexa"]
|
||||
HUE_MANUAL_BRIDGE_ID = "manual"
|
||||
|
||||
BSB002_MODEL_ID = "BSB002"
|
||||
BSB003_MODEL_ID = "BSB003"
|
||||
|
||||
|
||||
class HueFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a Hue config flow."""
|
||||
@@ -74,7 +79,14 @@ class HueFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
"""Return a DiscoveredHueBridge object."""
|
||||
try:
|
||||
bridge = await discover_bridge(
|
||||
host, websession=aiohttp_client.async_get_clientsession(self.hass)
|
||||
host,
|
||||
websession=aiohttp_client.async_get_clientsession(
|
||||
# NOTE: we disable SSL verification for now due to the fact that the (BSB003)
|
||||
# Hue bridge uses a certificate from a on-bridge root authority.
|
||||
# We need to specifically handle this case in a follow-up update.
|
||||
self.hass,
|
||||
verify_ssl=False,
|
||||
),
|
||||
)
|
||||
except aiohttp.ClientError as err:
|
||||
LOGGER.warning(
|
||||
@@ -110,7 +122,9 @@ class HueFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
try:
|
||||
async with asyncio.timeout(5):
|
||||
bridges = await discover_nupnp(
|
||||
websession=aiohttp_client.async_get_clientsession(self.hass)
|
||||
websession=aiohttp_client.async_get_clientsession(
|
||||
self.hass, verify_ssl=False
|
||||
)
|
||||
)
|
||||
except TimeoutError:
|
||||
bridges = []
|
||||
@@ -178,7 +192,9 @@ class HueFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
app_key = await create_app_key(
|
||||
bridge.host,
|
||||
f"home-assistant#{device_name}",
|
||||
websession=aiohttp_client.async_get_clientsession(self.hass),
|
||||
websession=aiohttp_client.async_get_clientsession(
|
||||
self.hass, verify_ssl=False
|
||||
),
|
||||
)
|
||||
except LinkButtonNotPressed:
|
||||
errors["base"] = "register_failed"
|
||||
@@ -228,7 +244,6 @@ class HueFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
self._abort_if_unique_id_configured(
|
||||
updates={CONF_HOST: discovery_info.host}, reload_on_update=True
|
||||
)
|
||||
|
||||
# we need to query the other capabilities too
|
||||
bridge = await self._get_bridge(
|
||||
discovery_info.host, discovery_info.properties["bridgeid"]
|
||||
@@ -236,6 +251,14 @@ class HueFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
if bridge is None:
|
||||
return self.async_abort(reason="cannot_connect")
|
||||
self.bridge = bridge
|
||||
if (
|
||||
bridge.supports_v2
|
||||
and discovery_info.properties.get("modelid") == BSB003_MODEL_ID
|
||||
):
|
||||
# try to handle migration of BSB002 --> BSB003
|
||||
if await self._check_migrated_bridge(bridge):
|
||||
return self.async_abort(reason="migrated_bridge")
|
||||
|
||||
return await self.async_step_link()
|
||||
|
||||
async def async_step_homekit(
|
||||
@@ -272,6 +295,55 @@ class HueFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
self.bridge = bridge
|
||||
return await self.async_step_link()
|
||||
|
||||
async def _check_migrated_bridge(self, bridge: DiscoveredHueBridge) -> bool:
|
||||
"""Check if the discovered bridge is a migrated bridge."""
|
||||
# Try to handle migration of BSB002 --> BSB003.
|
||||
# Once we detect a BSB003 bridge on the network which has not yet been
|
||||
# configured in HA (otherwise we would have had a unique id match),
|
||||
# we check if we have any existing (BSB002) entries and if we can connect to the
|
||||
# new bridge with our previously stored api key.
|
||||
# If that succeeds, we migrate the entry to the new bridge.
|
||||
for conf_entry in self.hass.config_entries.async_entries(
|
||||
DOMAIN, include_ignore=False, include_disabled=False
|
||||
):
|
||||
if conf_entry.data[CONF_API_VERSION] != 2:
|
||||
continue
|
||||
if conf_entry.data[CONF_HOST] == bridge.host:
|
||||
continue
|
||||
# found an existing (BSB002) bridge entry,
|
||||
# check if we can connect to the new BSB003 bridge using the old credentials
|
||||
api = HueBridgeV2(bridge.host, conf_entry.data[CONF_API_KEY])
|
||||
try:
|
||||
await api.fetch_full_state()
|
||||
except (AiohueException, aiohttp.ClientError):
|
||||
continue
|
||||
old_bridge_id = conf_entry.unique_id
|
||||
assert old_bridge_id is not None
|
||||
# found a matching entry, migrate it
|
||||
self.hass.config_entries.async_update_entry(
|
||||
conf_entry,
|
||||
data={
|
||||
**conf_entry.data,
|
||||
CONF_HOST: bridge.host,
|
||||
},
|
||||
unique_id=bridge.id,
|
||||
)
|
||||
# also update the bridge device
|
||||
dev_reg = dr.async_get(self.hass)
|
||||
if bridge_device := dev_reg.async_get_device(
|
||||
identifiers={(DOMAIN, old_bridge_id)}
|
||||
):
|
||||
dev_reg.async_update_device(
|
||||
bridge_device.id,
|
||||
# overwrite identifiers with new bridge id
|
||||
new_identifiers={(DOMAIN, bridge.id)},
|
||||
# overwrite mac addresses with empty set to drop the old (incorrect) addresses
|
||||
# this will be auto corrected once the integration is loaded
|
||||
new_connections=set(),
|
||||
)
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
class HueV1OptionsFlowHandler(OptionsFlow):
|
||||
"""Handle Hue options for V1 implementation."""
|
||||
|
||||
@@ -4,7 +4,7 @@ from ipaddress import ip_address
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
from aiohue.discovery import URL_NUPNP
|
||||
from aiohue.errors import LinkButtonNotPressed
|
||||
from aiohue.errors import AiohueException, LinkButtonNotPressed
|
||||
import pytest
|
||||
import voluptuous as vol
|
||||
|
||||
@@ -732,3 +732,216 @@ async def test_bridge_connection_failed(
|
||||
)
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "cannot_connect"
|
||||
|
||||
|
||||
async def test_bsb003_bridge_discovery(
|
||||
hass: HomeAssistant,
|
||||
aioclient_mock: AiohttpClientMocker,
|
||||
device_registry: dr.DeviceRegistry,
|
||||
) -> None:
|
||||
"""Test a bridge being discovered."""
|
||||
entry = MockConfigEntry(
|
||||
domain=const.DOMAIN,
|
||||
data={"host": "192.168.1.217", "api_version": 2, "api_key": "abc"},
|
||||
unique_id="bsb002_00000",
|
||||
)
|
||||
entry.add_to_hass(hass)
|
||||
device = device_registry.async_get_or_create(
|
||||
config_entry_id=entry.entry_id,
|
||||
identifiers={(const.DOMAIN, "bsb002_00000")},
|
||||
connections={(dr.CONNECTION_NETWORK_MAC, "AA:BB:CC:DD:EE:FF")},
|
||||
)
|
||||
create_mock_api_discovery(
|
||||
aioclient_mock,
|
||||
[("192.168.1.217", "bsb002_00000"), ("192.168.1.218", "bsb003_00000")],
|
||||
)
|
||||
disc_bridge = get_discovered_bridge(
|
||||
bridge_id="bsb003_00000", host="192.168.1.218", supports_v2=True
|
||||
)
|
||||
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.hue.config_flow.discover_bridge",
|
||||
return_value=disc_bridge,
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.hue.config_flow.HueBridgeV2",
|
||||
autospec=True,
|
||||
) as mock_bridge,
|
||||
):
|
||||
mock_bridge.return_value.fetch_full_state.return_value = {}
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
const.DOMAIN,
|
||||
context={"source": config_entries.SOURCE_ZEROCONF},
|
||||
data=ZeroconfServiceInfo(
|
||||
ip_address=ip_address("192.168.1.218"),
|
||||
ip_addresses=[ip_address("192.168.1.218")],
|
||||
port=443,
|
||||
hostname="Philips-hue.local",
|
||||
type="_hue._tcp.local.",
|
||||
name="Philips Hue - ABCABC._hue._tcp.local.",
|
||||
properties={
|
||||
"bridgeid": "bsb003_00000",
|
||||
"modelid": "BSB003",
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "migrated_bridge"
|
||||
|
||||
migrated_device = device_registry.async_get(device.id)
|
||||
|
||||
assert migrated_device is not None
|
||||
assert len(migrated_device.identifiers) == 1
|
||||
assert list(migrated_device.identifiers)[0] == (const.DOMAIN, "bsb003_00000")
|
||||
# The tests don't add new connection, but that will happen
|
||||
# outside of the config flow
|
||||
assert len(migrated_device.connections) == 0
|
||||
assert entry.data["host"] == "192.168.1.218"
|
||||
|
||||
|
||||
async def test_bsb003_bridge_discovery_old_version(
|
||||
hass: HomeAssistant,
|
||||
aioclient_mock: AiohttpClientMocker,
|
||||
device_registry: dr.DeviceRegistry,
|
||||
) -> None:
|
||||
"""Test a bridge being discovered."""
|
||||
entry = MockConfigEntry(
|
||||
domain=const.DOMAIN,
|
||||
data={"host": "192.168.1.217", "api_version": 1, "api_key": "abc"},
|
||||
unique_id="bsb002_00000",
|
||||
)
|
||||
entry.add_to_hass(hass)
|
||||
disc_bridge = get_discovered_bridge(
|
||||
bridge_id="bsb003_00000", host="192.168.1.218", supports_v2=True
|
||||
)
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.hue.config_flow.discover_bridge",
|
||||
return_value=disc_bridge,
|
||||
):
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
const.DOMAIN,
|
||||
context={"source": config_entries.SOURCE_ZEROCONF},
|
||||
data=ZeroconfServiceInfo(
|
||||
ip_address=ip_address("192.168.1.218"),
|
||||
ip_addresses=[ip_address("192.168.1.218")],
|
||||
port=443,
|
||||
hostname="Philips-hue.local",
|
||||
type="_hue._tcp.local.",
|
||||
name="Philips Hue - ABCABC._hue._tcp.local.",
|
||||
properties={
|
||||
"bridgeid": "bsb003_00000",
|
||||
"modelid": "BSB003",
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "link"
|
||||
|
||||
|
||||
async def test_bsb003_bridge_discovery_same_host(
|
||||
hass: HomeAssistant,
|
||||
aioclient_mock: AiohttpClientMocker,
|
||||
device_registry: dr.DeviceRegistry,
|
||||
) -> None:
|
||||
"""Test a bridge being discovered."""
|
||||
entry = MockConfigEntry(
|
||||
domain=const.DOMAIN,
|
||||
data={"host": "192.168.1.217", "api_version": 2, "api_key": "abc"},
|
||||
unique_id="bsb002_00000",
|
||||
)
|
||||
entry.add_to_hass(hass)
|
||||
create_mock_api_discovery(
|
||||
aioclient_mock,
|
||||
[("192.168.1.217", "bsb003_00000")],
|
||||
)
|
||||
disc_bridge = get_discovered_bridge(
|
||||
bridge_id="bsb003_00000", host="192.168.1.217", supports_v2=True
|
||||
)
|
||||
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.hue.config_flow.discover_bridge",
|
||||
return_value=disc_bridge,
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.hue.config_flow.HueBridgeV2",
|
||||
autospec=True,
|
||||
),
|
||||
):
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
const.DOMAIN,
|
||||
context={"source": config_entries.SOURCE_ZEROCONF},
|
||||
data=ZeroconfServiceInfo(
|
||||
ip_address=ip_address("192.168.1.217"),
|
||||
ip_addresses=[ip_address("192.168.1.217")],
|
||||
port=443,
|
||||
hostname="Philips-hue.local",
|
||||
type="_hue._tcp.local.",
|
||||
name="Philips Hue - ABCABC._hue._tcp.local.",
|
||||
properties={
|
||||
"bridgeid": "bsb003_00000",
|
||||
"modelid": "BSB003",
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "link"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("exception", [AiohueException, ClientError])
|
||||
async def test_bsb003_bridge_discovery_cannot_connect(
|
||||
hass: HomeAssistant,
|
||||
aioclient_mock: AiohttpClientMocker,
|
||||
device_registry: dr.DeviceRegistry,
|
||||
exception: Exception,
|
||||
) -> None:
|
||||
"""Test a bridge being discovered."""
|
||||
entry = MockConfigEntry(
|
||||
domain=const.DOMAIN,
|
||||
data={"host": "192.168.1.217", "api_version": 2, "api_key": "abc"},
|
||||
unique_id="bsb002_00000",
|
||||
)
|
||||
entry.add_to_hass(hass)
|
||||
create_mock_api_discovery(
|
||||
aioclient_mock,
|
||||
[("192.168.1.217", "bsb003_00000")],
|
||||
)
|
||||
disc_bridge = get_discovered_bridge(
|
||||
bridge_id="bsb003_00000", host="192.168.1.217", supports_v2=True
|
||||
)
|
||||
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.hue.config_flow.discover_bridge",
|
||||
return_value=disc_bridge,
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.hue.config_flow.HueBridgeV2",
|
||||
autospec=True,
|
||||
) as mock_bridge,
|
||||
):
|
||||
mock_bridge.return_value.fetch_full_state.side_effect = exception
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
const.DOMAIN,
|
||||
context={"source": config_entries.SOURCE_ZEROCONF},
|
||||
data=ZeroconfServiceInfo(
|
||||
ip_address=ip_address("192.168.1.217"),
|
||||
ip_addresses=[ip_address("192.168.1.217")],
|
||||
port=443,
|
||||
hostname="Philips-hue.local",
|
||||
type="_hue._tcp.local.",
|
||||
name="Philips Hue - ABCABC._hue._tcp.local.",
|
||||
properties={
|
||||
"bridgeid": "bsb003_00000",
|
||||
"modelid": "BSB003",
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "link"
|
||||
|
||||
Reference in New Issue
Block a user