Merge pull request #6339 from home-assistant/release-0-39-2

0.39.2
This commit is contained in:
Paulus Schoutsen 2017-03-01 12:43:00 -08:00 committed by GitHub
commit a9db6b16eb
8 changed files with 156 additions and 134 deletions

View File

@ -24,7 +24,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
def setup_platform(hass, config, add_devices, discovery_info=None): def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the ZigBee binary sensor platform.""" """Setup the ZigBee binary sensor platform."""
add_devices([ZigBeeBinarySensor(hass, ZigBeeDigitalInConfig(config))]) add_devices(
[ZigBeeBinarySensor(hass, ZigBeeDigitalInConfig(config))], True)
class ZigBeeBinarySensor(ZigBeeDigitalIn, BinarySensorDevice): class ZigBeeBinarySensor(ZigBeeDigitalIn, BinarySensorDevice):

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.1']
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

@ -44,7 +44,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
_LOGGER.exception("Unknown ZigBee sensor type: %s", typ) _LOGGER.exception("Unknown ZigBee sensor type: %s", typ)
return return
add_devices([sensor_class(hass, config_class(config))]) add_devices([sensor_class(hass, config_class(config))], True)
class ZigBeeTemperatureSensor(Entity): class ZigBeeTemperatureSensor(Entity):
@ -54,8 +54,6 @@ class ZigBeeTemperatureSensor(Entity):
"""Initialize the sensor.""" """Initialize the sensor."""
self._config = config self._config = config
self._temp = None self._temp = None
# Get initial state
self.schedule_update_ha_state(True)
@property @property
def name(self): def name(self):

View File

@ -4,10 +4,9 @@ Support for ZigBee devices.
For more details about this component, please refer to the documentation at For more details about this component, please refer to the documentation at
https://home-assistant.io/components/zigbee/ https://home-assistant.io/components/zigbee/
""" """
import asyncio
import logging import logging
import pickle
from binascii import hexlify, unhexlify from binascii import hexlify, unhexlify
from base64 import b64encode, b64decode
import voluptuous as vol import voluptuous as vol
@ -15,6 +14,8 @@ from homeassistant.const import (
EVENT_HOMEASSISTANT_STOP, CONF_DEVICE, CONF_NAME, CONF_PIN) EVENT_HOMEASSISTANT_STOP, CONF_DEVICE, CONF_NAME, CONF_PIN)
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from homeassistant.helpers import config_validation as cv from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.dispatcher import (
async_dispatcher_connect, dispatcher_send)
REQUIREMENTS = ['xbee-helper==0.0.7'] REQUIREMENTS = ['xbee-helper==0.0.7']
@ -22,7 +23,7 @@ _LOGGER = logging.getLogger(__name__)
DOMAIN = 'zigbee' DOMAIN = 'zigbee'
EVENT_ZIGBEE_FRAME_RECEIVED = 'zigbee_frame_received' SIGNAL_ZIGBEE_FRAME_RECEIVED = 'zigbee_frame_received'
CONF_ADDRESS = 'address' CONF_ADDRESS = 'address'
CONF_BAUD = 'baud' CONF_BAUD = 'baud'
@ -102,9 +103,7 @@ def setup(hass, config):
Pickles the frame, then encodes it into base64 since it contains Pickles the frame, then encodes it into base64 since it contains
non JSON serializable binary. non JSON serializable binary.
""" """
hass.bus.fire( dispatcher_send(hass, SIGNAL_ZIGBEE_FRAME_RECEIVED, frame)
EVENT_ZIGBEE_FRAME_RECEIVED,
{ATTR_FRAME: b64encode(pickle.dumps(frame)).decode("ascii")})
DEVICE.add_frame_rx_handler(_frame_received) DEVICE.add_frame_rx_handler(_frame_received)
@ -125,16 +124,6 @@ def frame_is_relevant(entity, frame):
return True 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): class ZigBeeConfig(object):
"""Handle the fetching of configuration from the config file.""" """Handle the fetching of configuration from the config file."""
@ -288,6 +277,9 @@ class ZigBeeDigitalIn(Entity):
self._config = config self._config = config
self._state = False self._state = False
@asyncio.coroutine
def async_added_to_hass(self):
"""Register callbacks."""
def handle_frame(frame): def handle_frame(frame):
"""Handle an incoming frame. """Handle an incoming frame.
@ -302,12 +294,10 @@ class ZigBeeDigitalIn(Entity):
# Doesn't contain information about our pin # Doesn't contain information about our pin
return return
self._state = self._config.state2bool[sample[pin_name]] self._state = self._config.state2bool[sample[pin_name]]
self.update_ha_state() self.schedule_update_ha_state()
subscribe(hass, handle_frame) async_dispatcher_connect(
self.hass, SIGNAL_ZIGBEE_FRAME_RECEIVED, handle_frame)
# Get initial state
self.schedule_update_ha_state(True)
@property @property
def name(self): def name(self):
@ -373,7 +363,7 @@ class ZigBeeDigitalOut(ZigBeeDigitalIn):
return return
self._state = state self._state = state
if not self.should_poll: if not self.should_poll:
self.update_ha_state() self.schedule_update_ha_state()
def turn_on(self, **kwargs): def turn_on(self, **kwargs):
"""Set the digital output to its 'on' state.""" """Set the digital output to its 'on' state."""
@ -410,6 +400,9 @@ class ZigBeeAnalogIn(Entity):
self._config = config self._config = config
self._value = None self._value = None
@asyncio.coroutine
def async_added_to_hass(self):
"""Register callbacks."""
def handle_frame(frame): def handle_frame(frame):
"""Handle an incoming frame. """Handle an incoming frame.
@ -428,12 +421,10 @@ class ZigBeeAnalogIn(Entity):
ADC_PERCENTAGE, ADC_PERCENTAGE,
self._config.max_voltage self._config.max_voltage
) )
self.update_ha_state() self.schedule_update_ha_state()
subscribe(hass, handle_frame) async_dispatcher_connect(
self.hass, SIGNAL_ZIGBEE_FRAME_RECEIVED, handle_frame)
# Get initial state
hass.add_job(self.async_update_ha_state, True)
@property @property
def name(self): def name(self):

View File

@ -2,7 +2,7 @@
"""Constants used by Home Assistant components.""" """Constants used by Home Assistant components."""
MAJOR_VERSION = 0 MAJOR_VERSION = 0
MINOR_VERSION = 39 MINOR_VERSION = 39
PATCH_VERSION = '1' PATCH_VERSION = '2'
__short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION)
__version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION)
REQUIRED_PYTHON_VER = (3, 4, 2) REQUIRED_PYTHON_VER = (3, 4, 2)

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.1
# 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