Discovery fix (#6321)

* Fix incorrect import

* Create own discovery service

* Fix tests

* Fix hdmi_cec bad import
pull/6339/head
Paulus Schoutsen 2017-03-01 07:38:49 -08:00
parent 52b1e13aca
commit 6736534a52
4 changed files with 134 additions and 102 deletions

View File

@ -6,20 +6,23 @@ Will emit EVENT_PLATFORM_DISCOVERED whenever a new service has been discovered.
Knows which components handle certain types, will make sure they are Knows which components handle certain types, will make sure they are
loaded before the EVENT_PLATFORM_DISCOVERED is fired. loaded before the EVENT_PLATFORM_DISCOVERED is fired.
""" """
import asyncio
from datetime import timedelta
import logging import logging
import threading
import voluptuous as vol import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.const import EVENT_HOMEASSISTANT_START from homeassistant.const import EVENT_HOMEASSISTANT_START
from homeassistant.helpers.discovery import load_platform, discover import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.event import async_track_point_in_utc_time
from homeassistant.helpers.discovery import async_load_platform, async_discover
import homeassistant.util.dt as dt_util
REQUIREMENTS = ['netdisco==0.8.3'] REQUIREMENTS = ['netdisco==0.9.0']
DOMAIN = 'discovery' DOMAIN = 'discovery'
SCAN_INTERVAL = 300 # seconds SCAN_INTERVAL = timedelta(seconds=300)
SERVICE_NETGEAR = 'netgear_router' SERVICE_NETGEAR = 'netgear_router'
SERVICE_WEMO = 'belkin_wemo' SERVICE_WEMO = 'belkin_wemo'
SERVICE_HASS_IOS_APP = 'hass_ios' SERVICE_HASS_IOS_APP = 'hass_ios'
@ -48,18 +51,20 @@ SERVICE_HANDLERS = {
CONF_IGNORE = 'ignore' CONF_IGNORE = 'ignore'
CONFIG_SCHEMA = vol.Schema({ CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({ vol.Required(DOMAIN): vol.Schema({
vol.Optional(CONF_IGNORE, default=[]): vol.Optional(CONF_IGNORE, default=[]):
vol.All(cv.ensure_list, [vol.In(SERVICE_HANDLERS)]) vol.All(cv.ensure_list, [vol.In(SERVICE_HANDLERS)])
}), }),
}, extra=vol.ALLOW_EXTRA) }, extra=vol.ALLOW_EXTRA)
def setup(hass, config): @asyncio.coroutine
def async_setup(hass, config):
"""Start a discovery service.""" """Start a discovery service."""
logger = logging.getLogger(__name__) from netdisco.discovery import NetworkDiscovery
from netdisco.service import DiscoveryService logger = logging.getLogger(__name__)
netdisco = NetworkDiscovery()
# Disable zeroconf logging, it spams # Disable zeroconf logging, it spams
logging.getLogger('zeroconf').setLevel(logging.CRITICAL) logging.getLogger('zeroconf').setLevel(logging.CRITICAL)
@ -67,37 +72,56 @@ def setup(hass, config):
# Platforms ignore by config # Platforms ignore by config
ignored_platforms = config[DOMAIN][CONF_IGNORE] ignored_platforms = config[DOMAIN][CONF_IGNORE]
lock = threading.Lock() @asyncio.coroutine
def new_service_found(service, info):
def new_service_listener(service, info):
"""Called when a new service is found.""" """Called when a new service is found."""
if service in ignored_platforms: if service in ignored_platforms:
logger.info("Ignoring service: %s %s", service, info) logger.info("Ignoring service: %s %s", service, info)
return return
with lock: logger.info("Found new service: %s %s", service, info)
logger.info("Found new service: %s %s", service, info)
comp_plat = SERVICE_HANDLERS.get(service) comp_plat = SERVICE_HANDLERS.get(service)
# We do not know how to handle this service. # We do not know how to handle this service.
if not comp_plat: if not comp_plat:
return return
component, platform = comp_plat component, platform = comp_plat
if platform is None: if platform is None:
discover(hass, service, info, component, config) yield from async_discover(hass, service, info, component, config)
else: else:
load_platform(hass, component, platform, info, config) yield from async_load_platform(
hass, component, platform, info, config)
# pylint: disable=unused-argument @asyncio.coroutine
def start_discovery(event): def scan_devices(_):
"""Start discovering.""" """Scan for devices."""
netdisco = DiscoveryService(SCAN_INTERVAL) results = yield from hass.loop.run_in_executor(
netdisco.add_listener(new_service_listener) None, _discover, netdisco)
netdisco.start()
hass.bus.listen_once(EVENT_HOMEASSISTANT_START, start_discovery) for result in results:
hass.async_add_job(new_service_found(*result))
async_track_point_in_utc_time(hass, scan_devices,
dt_util.utcnow() + SCAN_INTERVAL)
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, scan_devices)
return True return True
def _discover(netdisco):
"""Discover devices."""
results = []
try:
netdisco.scan()
for disc in netdisco.discover():
for service in netdisco.get_info(disc):
results.append((disc, service))
finally:
netdisco.stop()
return results

View File

@ -13,7 +13,7 @@ from functools import reduce
import voluptuous as vol import voluptuous as vol
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.components import discovery from homeassistant.helpers import discovery
from homeassistant.components.media_player import DOMAIN as MEDIA_PLAYER from homeassistant.components.media_player import DOMAIN as MEDIA_PLAYER
from homeassistant.components.switch import DOMAIN as SWITCH from homeassistant.components.switch import DOMAIN as SWITCH
from homeassistant.config import load_yaml_config_file from homeassistant.config import load_yaml_config_file

View File

@ -350,7 +350,7 @@ mutagen==1.36.2
myusps==1.0.3 myusps==1.0.3
# homeassistant.components.discovery # homeassistant.components.discovery
netdisco==0.8.3 netdisco==0.9.0
# homeassistant.components.sensor.neurio_energy # homeassistant.components.sensor.neurio_energy
neurio==0.3.1 neurio==0.3.1

View File

@ -1,14 +1,13 @@
"""The tests for the discovery component.""" """The tests for the discovery component."""
import unittest import asyncio
from unittest import mock
from unittest.mock import patch from unittest.mock import patch
from homeassistant.bootstrap import setup_component from homeassistant.bootstrap import async_setup_component
from homeassistant.components import discovery from homeassistant.components import discovery
from homeassistant.const import EVENT_HOMEASSISTANT_START from homeassistant.const import EVENT_HOMEASSISTANT_START
from tests.common import get_test_home_assistant from tests.common import mock_coro
# One might consider to "mock" services, but it's easy enough to just use # One might consider to "mock" services, but it's easy enough to just use
# what is already available. # what is already available.
@ -34,87 +33,96 @@ IGNORE_CONFIG = {
} }
@patch('netdisco.service.DiscoveryService') @asyncio.coroutine
@patch('homeassistant.components.discovery.load_platform') def test_unknown_service(hass):
@patch('homeassistant.components.discovery.discover') """Test that unknown service is ignored."""
class DiscoveryTest(unittest.TestCase): result = yield from async_setup_component(hass, 'discovery', {
"""Test the discovery component.""" 'discovery': {},
})
assert result
def setUp(self): def discover(netdisco):
"""Setup things to be run when tests are started.""" """Fake discovery."""
self.hass = get_test_home_assistant() return [('this_service_will_never_be_supported', {'info': 'some'})]
self.netdisco = mock.Mock()
def tearDown(self): with patch.object(discovery, '_discover', discover), \
"""Stop everything that was started.""" patch('homeassistant.components.discovery.async_discover',
self.hass.stop() return_value=mock_coro()) as mock_discover, \
patch('homeassistant.components.discovery.async_load_platform',
return_value=mock_coro()) as mock_platform:
hass.bus.async_fire(EVENT_HOMEASSISTANT_START)
yield from hass.async_block_till_done()
def setup_discovery_component(self, discovery_service, config): assert not mock_discover.called
"""Setup the discovery component with mocked netdisco.""" assert not mock_platform.called
discovery_service.return_value = self.netdisco
setup_component(self.hass, discovery.DOMAIN, config)
self.hass.bus.fire(EVENT_HOMEASSISTANT_START) @asyncio.coroutine
self.hass.block_till_done() def test_load_platform(hass):
"""Test load a platform."""
result = yield from async_setup_component(hass, 'discovery', BASE_CONFIG)
assert result
def discover_service(self, discovery_service, name): def discover(netdisco):
"""Simulate that netdisco discovered a new service.""" """Fake discovery."""
self.assertTrue(self.netdisco.add_listener.called) return [(SERVICE, SERVICE_INFO)]
# Extract a refernce to the service listener with patch.object(discovery, '_discover', discover), \
args, _ = self.netdisco.add_listener.call_args patch('homeassistant.components.discovery.async_discover',
listener = args[0] return_value=mock_coro()) as mock_discover, \
patch('homeassistant.components.discovery.async_load_platform',
return_value=mock_coro()) as mock_platform:
hass.bus.async_fire(EVENT_HOMEASSISTANT_START)
yield from hass.async_block_till_done()
# Call the listener (just like netdisco does) assert not mock_discover.called
listener(name, SERVICE_INFO) assert mock_platform.called
mock_platform.assert_called_with(
hass, SERVICE_COMPONENT, SERVICE, SERVICE_INFO, BASE_CONFIG)
def test_netdisco_is_started(
self, discover, load_platform, discovery_service):
"""Test that netdisco is started."""
self.setup_discovery_component(discovery_service, BASE_CONFIG)
self.assertTrue(self.netdisco.start.called)
def test_unknown_service( @asyncio.coroutine
self, discover, load_platform, discovery_service): def test_load_component(hass):
"""Test that unknown service is ignored.""" """Test load a component."""
self.setup_discovery_component(discovery_service, BASE_CONFIG) result = yield from async_setup_component(hass, 'discovery', BASE_CONFIG)
self.discover_service(discovery_service, UNKNOWN_SERVICE) assert result
self.assertFalse(load_platform.called) def discover(netdisco):
self.assertFalse(discover.called) """Fake discovery."""
return [(SERVICE_NO_PLATFORM, SERVICE_INFO)]
def test_load_platform( with patch.object(discovery, '_discover', discover), \
self, discover, load_platform, discovery_service): patch('homeassistant.components.discovery.async_discover',
"""Test load a supported platform.""" return_value=mock_coro()) as mock_discover, \
self.setup_discovery_component(discovery_service, BASE_CONFIG) patch('homeassistant.components.discovery.async_load_platform',
self.discover_service(discovery_service, SERVICE) return_value=mock_coro()) as mock_platform:
hass.bus.async_fire(EVENT_HOMEASSISTANT_START)
yield from hass.async_block_till_done()
load_platform.assert_called_with(self.hass, assert mock_discover.called
SERVICE_COMPONENT, assert not mock_platform.called
SERVICE, mock_discover.assert_called_with(
SERVICE_INFO, hass, SERVICE_NO_PLATFORM, SERVICE_INFO,
BASE_CONFIG) SERVICE_NO_PLATFORM_COMPONENT, BASE_CONFIG)
def test_discover_platform(
self, discover, load_platform, discovery_service):
"""Test discover a supported platform."""
self.setup_discovery_component(discovery_service, BASE_CONFIG)
self.discover_service(discovery_service, SERVICE_NO_PLATFORM)
discover.assert_called_with(self.hass, @asyncio.coroutine
SERVICE_NO_PLATFORM, def test_ignore_service(hass):
SERVICE_INFO, """Test ignore service."""
SERVICE_NO_PLATFORM_COMPONENT, result = yield from async_setup_component(hass, 'discovery', IGNORE_CONFIG)
BASE_CONFIG) assert result
def test_ignore_platforms( def discover(netdisco):
self, discover, load_platform, discovery_service): """Fake discovery."""
"""Test that ignored platforms are not setup.""" return [(SERVICE_NO_PLATFORM, SERVICE_INFO)]
self.setup_discovery_component(discovery_service, IGNORE_CONFIG)
self.discover_service(discovery_service, SERVICE_NO_PLATFORM) with patch.object(discovery, '_discover', discover), \
self.assertFalse(discover.called) patch('homeassistant.components.discovery.async_discover',
return_value=mock_coro()) as mock_discover, \
patch('homeassistant.components.discovery.async_load_platform',
return_value=mock_coro()) as mock_platform:
hass.bus.async_fire(EVENT_HOMEASSISTANT_START)
yield from hass.async_block_till_done()
self.discover_service(discovery_service, SERVICE) assert not mock_discover.called
self.assertTrue(load_platform.called) assert not mock_platform.called