diff --git a/homeassistant/components/binary_sensor/zigbee.py b/homeassistant/components/binary_sensor/zigbee.py index 2eb508304d4..935d4b4bb3f 100644 --- a/homeassistant/components/binary_sensor/zigbee.py +++ b/homeassistant/components/binary_sensor/zigbee.py @@ -24,7 +24,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ def setup_platform(hass, config, add_devices, discovery_info=None): """Setup the ZigBee binary sensor platform.""" - add_devices([ZigBeeBinarySensor(hass, ZigBeeDigitalInConfig(config))]) + add_devices( + [ZigBeeBinarySensor(hass, ZigBeeDigitalInConfig(config))], True) class ZigBeeBinarySensor(ZigBeeDigitalIn, BinarySensorDevice): diff --git a/homeassistant/components/discovery.py b/homeassistant/components/discovery.py index b8999ee2c43..028a6df01fb 100644 --- a/homeassistant/components/discovery.py +++ b/homeassistant/components/discovery.py @@ -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 loaded before the EVENT_PLATFORM_DISCOVERED is fired. """ +import asyncio +from datetime import timedelta import logging -import threading import voluptuous as vol -import homeassistant.helpers.config_validation as cv 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.1'] DOMAIN = 'discovery' -SCAN_INTERVAL = 300 # seconds +SCAN_INTERVAL = timedelta(seconds=300) SERVICE_NETGEAR = 'netgear_router' SERVICE_WEMO = 'belkin_wemo' SERVICE_HASS_IOS_APP = 'hass_ios' @@ -48,18 +51,20 @@ SERVICE_HANDLERS = { CONF_IGNORE = 'ignore' CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ + vol.Required(DOMAIN): vol.Schema({ vol.Optional(CONF_IGNORE, default=[]): vol.All(cv.ensure_list, [vol.In(SERVICE_HANDLERS)]) }), }, extra=vol.ALLOW_EXTRA) -def setup(hass, config): +@asyncio.coroutine +def async_setup(hass, config): """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 logging.getLogger('zeroconf').setLevel(logging.CRITICAL) @@ -67,37 +72,56 @@ def setup(hass, config): # Platforms ignore by config ignored_platforms = config[DOMAIN][CONF_IGNORE] - lock = threading.Lock() - - def new_service_listener(service, info): + @asyncio.coroutine + def new_service_found(service, info): """Called when a new service is found.""" if service in ignored_platforms: logger.info("Ignoring service: %s %s", service, info) 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. - if not comp_plat: - return + # We do not know how to handle this service. + if not comp_plat: + return - component, platform = comp_plat + component, platform = comp_plat - if platform is None: - discover(hass, service, info, component, config) - else: - load_platform(hass, component, platform, info, config) + if platform is None: + yield from async_discover(hass, service, info, component, config) + else: + yield from async_load_platform( + hass, component, platform, info, config) - # pylint: disable=unused-argument - def start_discovery(event): - """Start discovering.""" - netdisco = DiscoveryService(SCAN_INTERVAL) - netdisco.add_listener(new_service_listener) - netdisco.start() + @asyncio.coroutine + def scan_devices(_): + """Scan for devices.""" + results = yield from hass.loop.run_in_executor( + None, _discover, netdisco) - 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 + + +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 diff --git a/homeassistant/components/hdmi_cec.py b/homeassistant/components/hdmi_cec.py index b7d6f04c440..7b966e25022 100644 --- a/homeassistant/components/hdmi_cec.py +++ b/homeassistant/components/hdmi_cec.py @@ -13,7 +13,7 @@ from functools import reduce import voluptuous as vol 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.switch import DOMAIN as SWITCH from homeassistant.config import load_yaml_config_file diff --git a/homeassistant/components/sensor/zigbee.py b/homeassistant/components/sensor/zigbee.py index 42ae64a2b1f..f3e8d5480a8 100644 --- a/homeassistant/components/sensor/zigbee.py +++ b/homeassistant/components/sensor/zigbee.py @@ -44,7 +44,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): _LOGGER.exception("Unknown ZigBee sensor type: %s", typ) return - add_devices([sensor_class(hass, config_class(config))]) + add_devices([sensor_class(hass, config_class(config))], True) class ZigBeeTemperatureSensor(Entity): @@ -54,8 +54,6 @@ class ZigBeeTemperatureSensor(Entity): """Initialize the sensor.""" self._config = config self._temp = None - # Get initial state - self.schedule_update_ha_state(True) @property def name(self): diff --git a/homeassistant/components/zigbee.py b/homeassistant/components/zigbee.py index 66ef19a5b99..817e7e432db 100644 --- a/homeassistant/components/zigbee.py +++ b/homeassistant/components/zigbee.py @@ -4,10 +4,9 @@ Support for ZigBee devices. For more details about this component, please refer to the documentation at https://home-assistant.io/components/zigbee/ """ +import asyncio import logging -import pickle from binascii import hexlify, unhexlify -from base64 import b64encode, b64decode import voluptuous as vol @@ -15,6 +14,8 @@ from homeassistant.const import ( EVENT_HOMEASSISTANT_STOP, CONF_DEVICE, CONF_NAME, CONF_PIN) from homeassistant.helpers.entity import Entity from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, dispatcher_send) REQUIREMENTS = ['xbee-helper==0.0.7'] @@ -22,7 +23,7 @@ _LOGGER = logging.getLogger(__name__) DOMAIN = 'zigbee' -EVENT_ZIGBEE_FRAME_RECEIVED = 'zigbee_frame_received' +SIGNAL_ZIGBEE_FRAME_RECEIVED = 'zigbee_frame_received' CONF_ADDRESS = 'address' CONF_BAUD = 'baud' @@ -102,9 +103,7 @@ def setup(hass, config): Pickles the frame, then encodes it into base64 since it contains non JSON serializable binary. """ - hass.bus.fire( - EVENT_ZIGBEE_FRAME_RECEIVED, - {ATTR_FRAME: b64encode(pickle.dumps(frame)).decode("ascii")}) + dispatcher_send(hass, SIGNAL_ZIGBEE_FRAME_RECEIVED, frame) DEVICE.add_frame_rx_handler(_frame_received) @@ -125,16 +124,6 @@ def frame_is_relevant(entity, frame): return True -def subscribe(hass, callback): - """Subscribe to incoming ZigBee frames.""" - def zigbee_frame_subscriber(event): - """Decode and unpickle the frame from the event bus, and call back.""" - frame = pickle.loads(b64decode(event.data[ATTR_FRAME])) - callback(frame) - - hass.bus.listen(EVENT_ZIGBEE_FRAME_RECEIVED, zigbee_frame_subscriber) - - class ZigBeeConfig(object): """Handle the fetching of configuration from the config file.""" @@ -288,6 +277,9 @@ class ZigBeeDigitalIn(Entity): self._config = config self._state = False + @asyncio.coroutine + def async_added_to_hass(self): + """Register callbacks.""" def handle_frame(frame): """Handle an incoming frame. @@ -302,12 +294,10 @@ class ZigBeeDigitalIn(Entity): # Doesn't contain information about our pin return self._state = self._config.state2bool[sample[pin_name]] - self.update_ha_state() + self.schedule_update_ha_state() - subscribe(hass, handle_frame) - - # Get initial state - self.schedule_update_ha_state(True) + async_dispatcher_connect( + self.hass, SIGNAL_ZIGBEE_FRAME_RECEIVED, handle_frame) @property def name(self): @@ -373,7 +363,7 @@ class ZigBeeDigitalOut(ZigBeeDigitalIn): return self._state = state if not self.should_poll: - self.update_ha_state() + self.schedule_update_ha_state() def turn_on(self, **kwargs): """Set the digital output to its 'on' state.""" @@ -410,6 +400,9 @@ class ZigBeeAnalogIn(Entity): self._config = config self._value = None + @asyncio.coroutine + def async_added_to_hass(self): + """Register callbacks.""" def handle_frame(frame): """Handle an incoming frame. @@ -428,12 +421,10 @@ class ZigBeeAnalogIn(Entity): ADC_PERCENTAGE, self._config.max_voltage ) - self.update_ha_state() + self.schedule_update_ha_state() - subscribe(hass, handle_frame) - - # Get initial state - hass.add_job(self.async_update_ha_state, True) + async_dispatcher_connect( + self.hass, SIGNAL_ZIGBEE_FRAME_RECEIVED, handle_frame) @property def name(self): diff --git a/homeassistant/const.py b/homeassistant/const.py index 3c93defc630..f52c524f314 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 39 -PATCH_VERSION = '1' +PATCH_VERSION = '2' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 4, 2) diff --git a/requirements_all.txt b/requirements_all.txt index 3931a14c5fb..641920c6767 100755 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -350,7 +350,7 @@ mutagen==1.36.2 myusps==1.0.3 # homeassistant.components.discovery -netdisco==0.8.3 +netdisco==0.9.1 # homeassistant.components.sensor.neurio_energy neurio==0.3.1 diff --git a/tests/components/test_discovery.py b/tests/components/test_discovery.py index f4bf307df6f..bc2be3ed463 100644 --- a/tests/components/test_discovery.py +++ b/tests/components/test_discovery.py @@ -1,14 +1,13 @@ """The tests for the discovery component.""" -import unittest +import asyncio -from unittest import mock 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.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 # what is already available. @@ -34,87 +33,96 @@ IGNORE_CONFIG = { } -@patch('netdisco.service.DiscoveryService') -@patch('homeassistant.components.discovery.load_platform') -@patch('homeassistant.components.discovery.discover') -class DiscoveryTest(unittest.TestCase): - """Test the discovery component.""" +@asyncio.coroutine +def test_unknown_service(hass): + """Test that unknown service is ignored.""" + result = yield from async_setup_component(hass, 'discovery', { + 'discovery': {}, + }) + assert result - def setUp(self): - """Setup things to be run when tests are started.""" - self.hass = get_test_home_assistant() - self.netdisco = mock.Mock() + def discover(netdisco): + """Fake discovery.""" + return [('this_service_will_never_be_supported', {'info': 'some'})] - def tearDown(self): - """Stop everything that was started.""" - self.hass.stop() + with patch.object(discovery, '_discover', discover), \ + 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() - def setup_discovery_component(self, discovery_service, config): - """Setup the discovery component with mocked netdisco.""" - discovery_service.return_value = self.netdisco + assert not mock_discover.called + assert not mock_platform.called - setup_component(self.hass, discovery.DOMAIN, config) - self.hass.bus.fire(EVENT_HOMEASSISTANT_START) - self.hass.block_till_done() +@asyncio.coroutine +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): - """Simulate that netdisco discovered a new service.""" - self.assertTrue(self.netdisco.add_listener.called) + def discover(netdisco): + """Fake discovery.""" + return [(SERVICE, SERVICE_INFO)] - # Extract a refernce to the service listener - args, _ = self.netdisco.add_listener.call_args - listener = args[0] + with patch.object(discovery, '_discover', discover), \ + 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() - # Call the listener (just like netdisco does) - listener(name, SERVICE_INFO) + assert not mock_discover.called + 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( - self, discover, load_platform, discovery_service): - """Test that unknown service is ignored.""" - self.setup_discovery_component(discovery_service, BASE_CONFIG) - self.discover_service(discovery_service, UNKNOWN_SERVICE) +@asyncio.coroutine +def test_load_component(hass): + """Test load a component.""" + result = yield from async_setup_component(hass, 'discovery', BASE_CONFIG) + assert result - self.assertFalse(load_platform.called) - self.assertFalse(discover.called) + def discover(netdisco): + """Fake discovery.""" + return [(SERVICE_NO_PLATFORM, SERVICE_INFO)] - def test_load_platform( - self, discover, load_platform, discovery_service): - """Test load a supported platform.""" - self.setup_discovery_component(discovery_service, BASE_CONFIG) - self.discover_service(discovery_service, SERVICE) + with patch.object(discovery, '_discover', discover), \ + 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() - load_platform.assert_called_with(self.hass, - SERVICE_COMPONENT, - SERVICE, - SERVICE_INFO, - BASE_CONFIG) + assert mock_discover.called + assert not mock_platform.called + mock_discover.assert_called_with( + hass, SERVICE_NO_PLATFORM, SERVICE_INFO, + 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, - SERVICE_NO_PLATFORM, - SERVICE_INFO, - SERVICE_NO_PLATFORM_COMPONENT, - BASE_CONFIG) +@asyncio.coroutine +def test_ignore_service(hass): + """Test ignore service.""" + result = yield from async_setup_component(hass, 'discovery', IGNORE_CONFIG) + assert result - def test_ignore_platforms( - self, discover, load_platform, discovery_service): - """Test that ignored platforms are not setup.""" - self.setup_discovery_component(discovery_service, IGNORE_CONFIG) + def discover(netdisco): + """Fake discovery.""" + return [(SERVICE_NO_PLATFORM, SERVICE_INFO)] - self.discover_service(discovery_service, SERVICE_NO_PLATFORM) - self.assertFalse(discover.called) + with patch.object(discovery, '_discover', discover), \ + 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) - self.assertTrue(load_platform.called) + assert not mock_discover.called + assert not mock_platform.called