deCONZ migrate to SSDP discovery (#24252)

* Migrate deCONZ to use new SSDP discovery
Add new discovery info manufacturer URL to be able to separate Hue and deCONZ bridges

* Mark deCONZ as migrated in Discovery component

* Fix tests

* Fix Hue discovery ignore deCONZ bridge

* Less snake more badger

* Mushroom

* Fix indentation

* Config flow ignore manufacturer url that is not philips
This commit is contained in:
Robert Svensson 2019-06-03 18:26:01 +02:00 committed by Paulus Schoutsen
parent 704cdac874
commit 9ed5b70d01
10 changed files with 78 additions and 20 deletions

View File

@ -9,12 +9,14 @@ from pydeconz.utils import (
async_discovery, async_get_api_key, async_get_bridgeid) async_discovery, async_get_api_key, async_get_bridgeid)
from homeassistant import config_entries from homeassistant import config_entries
from homeassistant.components.ssdp import ATTR_MANUFACTURERURL, ATTR_SERIAL
from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PORT from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PORT
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.helpers import aiohttp_client from homeassistant.helpers import aiohttp_client
from .const import CONF_BRIDGEID, DEFAULT_PORT, DOMAIN from .const import CONF_BRIDGEID, DEFAULT_PORT, DOMAIN
DECONZ_MANUFACTURERURL = 'http://www.dresden-elektronik.de'
CONF_SERIAL = 'serial' CONF_SERIAL = 'serial'
@ -149,12 +151,12 @@ class DeconzFlowHandler(config_entries.ConfigFlow):
entry.data[CONF_HOST] = host entry.data[CONF_HOST] = host
self.hass.config_entries.async_update_entry(entry) self.hass.config_entries.async_update_entry(entry)
async def async_step_discovery(self, discovery_info): async def async_step_ssdp(self, discovery_info):
"""Prepare configuration for a discovered deCONZ bridge. """Handle a discovered deCONZ bridge."""
if discovery_info[ATTR_MANUFACTURERURL] != DECONZ_MANUFACTURERURL:
return self.async_abort(reason='not_deconz_bridge')
This flow is triggered by the discovery component. bridgeid = discovery_info[ATTR_SERIAL]
"""
bridgeid = discovery_info[CONF_SERIAL]
gateway_entries = configured_gateways(self.hass) gateway_entries = configured_gateways(self.hass)
if bridgeid in gateway_entries: if bridgeid in gateway_entries:
@ -162,10 +164,17 @@ class DeconzFlowHandler(config_entries.ConfigFlow):
await self._update_entry(entry, discovery_info[CONF_HOST]) await self._update_entry(entry, discovery_info[CONF_HOST])
return self.async_abort(reason='updated_instance') return self.async_abort(reason='updated_instance')
# pylint: disable=unsupported-assignment-operation
self.context[ATTR_SERIAL] = bridgeid
if any(bridgeid == flow['context'][ATTR_SERIAL]
for flow in self._async_in_progress()):
return self.async_abort(reason='already_in_progress')
deconz_config = { deconz_config = {
CONF_HOST: discovery_info[CONF_HOST], CONF_HOST: discovery_info[CONF_HOST],
CONF_PORT: discovery_info[CONF_PORT], CONF_PORT: discovery_info[CONF_PORT],
CONF_BRIDGEID: discovery_info[CONF_SERIAL] CONF_BRIDGEID: bridgeid
} }
return await self.async_step_import(deconz_config) return await self.async_step_import(deconz_config)

View File

@ -6,6 +6,11 @@
"requirements": [ "requirements": [
"pydeconz==59" "pydeconz==59"
], ],
"ssdp": {
"manufacturer": [
"Royal Philips Electronics"
]
},
"dependencies": [], "dependencies": [],
"codeowners": [ "codeowners": [
"@kane610" "@kane610"

View File

@ -34,9 +34,11 @@
}, },
"abort": { "abort": {
"already_configured": "Bridge is already configured", "already_configured": "Bridge is already configured",
"already_in_progress": "Config flow for bridge is already in progress.",
"no_bridges": "No deCONZ bridges discovered", "no_bridges": "No deCONZ bridges discovered",
"updated_instance": "Updated deCONZ instance with new host address", "not_deconz_bridge": "Not a deCONZ bridge",
"one_instance_only": "Component only supports one deCONZ instance" "one_instance_only": "Component only supports one deCONZ instance",
"updated_instance": "Updated deCONZ instance with new host address"
} }
} }
} }

View File

@ -25,7 +25,6 @@ DOMAIN = 'discovery'
SCAN_INTERVAL = timedelta(seconds=300) SCAN_INTERVAL = timedelta(seconds=300)
SERVICE_APPLE_TV = 'apple_tv' SERVICE_APPLE_TV = 'apple_tv'
SERVICE_DAIKIN = 'daikin' SERVICE_DAIKIN = 'daikin'
SERVICE_DECONZ = 'deconz'
SERVICE_DLNA_DMR = 'dlna_dmr' SERVICE_DLNA_DMR = 'dlna_dmr'
SERVICE_ENIGMA2 = 'enigma2' SERVICE_ENIGMA2 = 'enigma2'
SERVICE_FREEBOX = 'freebox' SERVICE_FREEBOX = 'freebox'
@ -48,7 +47,6 @@ SERVICE_XIAOMI_GW = 'xiaomi_gw'
CONFIG_ENTRY_HANDLERS = { CONFIG_ENTRY_HANDLERS = {
SERVICE_DAIKIN: 'daikin', SERVICE_DAIKIN: 'daikin',
SERVICE_DECONZ: 'deconz',
'google_cast': 'cast', 'google_cast': 'cast',
SERVICE_HEOS: 'heos', SERVICE_HEOS: 'heos',
SERVICE_TELLDUSLIVE: 'tellduslive', SERVICE_TELLDUSLIVE: 'tellduslive',
@ -98,6 +96,7 @@ OPTIONAL_SERVICE_HANDLERS = {
MIGRATED_SERVICE_HANDLERS = { MIGRATED_SERVICE_HANDLERS = {
'axis': None, 'axis': None,
'deconz': None,
'esphome': None, 'esphome': None,
'ikea_tradfri': None, 'ikea_tradfri': None,
'homekit': None, 'homekit': None,

View File

@ -8,6 +8,7 @@ import async_timeout
import voluptuous as vol import voluptuous as vol
from homeassistant import config_entries from homeassistant import config_entries
from homeassistant.components.ssdp import ATTR_MANUFACTURERURL
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.helpers import aiohttp_client from homeassistant.helpers import aiohttp_client
@ -15,6 +16,8 @@ from .bridge import get_bridge
from .const import DOMAIN, LOGGER from .const import DOMAIN, LOGGER
from .errors import AuthenticationRequired, CannotConnect from .errors import AuthenticationRequired, CannotConnect
HUE_MANUFACTURERURL = 'http://www.philips.com'
@callback @callback
def configured_hosts(hass): def configured_hosts(hass):
@ -143,6 +146,9 @@ class HueFlowHandler(config_entries.ConfigFlow):
This flow is triggered by the SSDP component. It will check if the This flow is triggered by the SSDP component. It will check if the
host is already configured and delegate to the import step if not. host is already configured and delegate to the import step if not.
""" """
if discovery_info[ATTR_MANUFACTURERURL] != HUE_MANUFACTURERURL:
return self.async_abort(reason='not_hue_bridge')
# Filter out emulated Hue # Filter out emulated Hue
if "HASS Bridge" in discovery_info.get('name', ''): if "HASS Bridge" in discovery_info.get('name', ''):
return self.async_abort(reason='already_configured') return self.async_abort(reason='already_configured')

View File

@ -24,7 +24,8 @@
"unknown": "Unknown error occurred", "unknown": "Unknown error occurred",
"cannot_connect": "Unable to connect to the bridge", "cannot_connect": "Unable to connect to the bridge",
"already_configured": "Bridge is already configured", "already_configured": "Bridge is already configured",
"already_in_progress": "Config flow for bridge is already in progress." "already_in_progress": "Config flow for bridge is already in progress.",
"not_hue_bridge": "Not a Hue bridge"
} }
} }
} }

View File

@ -23,6 +23,7 @@ ATTR_MODEL_NAME = 'model_name'
ATTR_MODEL_NUMBER = 'model_number' ATTR_MODEL_NUMBER = 'model_number'
ATTR_SERIAL = 'serial_number' ATTR_SERIAL = 'serial_number'
ATTR_MANUFACTURER = 'manufacturer' ATTR_MANUFACTURER = 'manufacturer'
ATTR_MANUFACTURERURL = 'manufacturerURL'
ATTR_UDN = 'udn' ATTR_UDN = 'udn'
ATTR_UPNP_DEVICE_TYPE = 'upnp_device_type' ATTR_UPNP_DEVICE_TYPE = 'upnp_device_type'
@ -164,6 +165,7 @@ def info_from_entry(entry, device_info):
info[ATTR_MODEL_NUMBER] = device_info.get('modelNumber') info[ATTR_MODEL_NUMBER] = device_info.get('modelNumber')
info[ATTR_SERIAL] = device_info.get('serialNumber') info[ATTR_SERIAL] = device_info.get('serialNumber')
info[ATTR_MANUFACTURER] = device_info.get('manufacturer') info[ATTR_MANUFACTURER] = device_info.get('manufacturer')
info[ATTR_MANUFACTURERURL] = device_info.get('manufacturerURL')
info[ATTR_UDN] = device_info.get('UDN') info[ATTR_UDN] = device_info.get('UDN')
info[ATTR_UPNP_DEVICE_TYPE] = device_info.get('deviceType') info[ATTR_UPNP_DEVICE_TYPE] = device_info.get('deviceType')

View File

@ -8,6 +8,7 @@ SSDP = {
"device_type": {}, "device_type": {},
"manufacturer": { "manufacturer": {
"Royal Philips Electronics": [ "Royal Philips Electronics": [
"deconz",
"hue" "hue"
] ]
}, },

View File

@ -168,22 +168,38 @@ async def test_link_no_api_key(hass):
assert result['errors'] == {'base': 'no_key'} assert result['errors'] == {'base': 'no_key'}
async def test_bridge_discovery(hass): async def test_bridge_ssdp_discovery(hass):
"""Test a bridge being discovered.""" """Test a bridge being discovered over ssdp."""
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
config_flow.DOMAIN, config_flow.DOMAIN,
data={ data={
config_flow.CONF_HOST: '1.2.3.4', config_flow.CONF_HOST: '1.2.3.4',
config_flow.CONF_PORT: 80, config_flow.CONF_PORT: 80,
config_flow.CONF_SERIAL: 'id', config_flow.ATTR_SERIAL: 'id',
config_flow.ATTR_MANUFACTURERURL:
config_flow.DECONZ_MANUFACTURERURL
}, },
context={'source': 'discovery'} context={'source': 'ssdp'}
) )
assert result['type'] == 'form' assert result['type'] == 'form'
assert result['step_id'] == 'link' assert result['step_id'] == 'link'
async def test_bridge_ssdp_discovery_not_deconz_bridge(hass):
"""Test a non deconz bridge being discovered over ssdp."""
result = await hass.config_entries.flow.async_init(
config_flow.DOMAIN,
data={
config_flow.ATTR_MANUFACTURERURL: 'not deconz bridge'
},
context={'source': 'ssdp'}
)
assert result['type'] == 'abort'
assert result['reason'] == 'not_deconz_bridge'
async def test_bridge_discovery_update_existing_entry(hass): async def test_bridge_discovery_update_existing_entry(hass):
"""Test if a discovered bridge has already been configured.""" """Test if a discovered bridge has already been configured."""
entry = MockConfigEntry(domain=config_flow.DOMAIN, data={ entry = MockConfigEntry(domain=config_flow.DOMAIN, data={
@ -195,9 +211,11 @@ async def test_bridge_discovery_update_existing_entry(hass):
config_flow.DOMAIN, config_flow.DOMAIN,
data={ data={
config_flow.CONF_HOST: 'mock-deconz', config_flow.CONF_HOST: 'mock-deconz',
config_flow.CONF_SERIAL: 'id', config_flow.ATTR_SERIAL: 'id',
config_flow.ATTR_MANUFACTURERURL:
config_flow.DECONZ_MANUFACTURERURL
}, },
context={'source': 'discovery'} context={'source': 'ssdp'}
) )
assert result['type'] == 'abort' assert result['type'] == 'abort'

View File

@ -195,13 +195,26 @@ async def test_bridge_ssdp(hass):
side_effect=errors.AuthenticationRequired): side_effect=errors.AuthenticationRequired):
result = await flow.async_step_ssdp({ result = await flow.async_step_ssdp({
'host': '0.0.0.0', 'host': '0.0.0.0',
'serial': '1234' 'serial': '1234',
'manufacturerURL': config_flow.HUE_MANUFACTURERURL
}) })
assert result['type'] == 'form' assert result['type'] == 'form'
assert result['step_id'] == 'link' assert result['step_id'] == 'link'
async def test_bridge_ssdp_discover_other_bridge(hass):
"""Test that discovery ignores other bridges."""
flow = config_flow.HueFlowHandler()
flow.hass = hass
result = await flow.async_step_ssdp({
'manufacturerURL': 'http://www.notphilips.com'
})
assert result['type'] == 'abort'
async def test_bridge_ssdp_emulated_hue(hass): async def test_bridge_ssdp_emulated_hue(hass):
"""Test if discovery info is from an emulated hue instance.""" """Test if discovery info is from an emulated hue instance."""
flow = config_flow.HueFlowHandler() flow = config_flow.HueFlowHandler()
@ -211,7 +224,8 @@ async def test_bridge_ssdp_emulated_hue(hass):
result = await flow.async_step_ssdp({ result = await flow.async_step_ssdp({
'name': 'HASS Bridge', 'name': 'HASS Bridge',
'host': '0.0.0.0', 'host': '0.0.0.0',
'serial': '1234' 'serial': '1234',
'manufacturerURL': config_flow.HUE_MANUFACTURERURL
}) })
assert result['type'] == 'abort' assert result['type'] == 'abort'
@ -229,7 +243,8 @@ async def test_bridge_ssdp_already_configured(hass):
result = await flow.async_step_ssdp({ result = await flow.async_step_ssdp({
'host': '0.0.0.0', 'host': '0.0.0.0',
'serial': '1234' 'serial': '1234',
'manufacturerURL': config_flow.HUE_MANUFACTURERURL
}) })
assert result['type'] == 'abort' assert result['type'] == 'abort'