Asyncio notify component migration (#5377)

* Async migrate notify/platform

* convert group to async

* fix unittest
This commit is contained in:
Pascal Vizeli 2017-01-18 07:08:03 +01:00 committed by Paulus Schoutsen
parent f7ac644c11
commit 2a362fd1ff
6 changed files with 136 additions and 88 deletions

View File

@ -4,13 +4,15 @@ Provides functionality to notify people.
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/notify/ https://home-assistant.io/components/notify/
""" """
import asyncio
import logging import logging
import os import os
from functools import partial from functools import partial
import voluptuous as vol import voluptuous as vol
import homeassistant.bootstrap as bootstrap from homeassistant.bootstrap import async_prepare_setup_platform
from homeassistant.exceptions import HomeAssistantError
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.config import load_yaml_config_file from homeassistant.config import load_yaml_config_file
from homeassistant.const import CONF_NAME, CONF_PLATFORM from homeassistant.const import CONF_NAME, CONF_PLATFORM
@ -64,67 +66,84 @@ def send_message(hass, message, title=None, data=None):
hass.services.call(DOMAIN, SERVICE_NOTIFY, info) hass.services.call(DOMAIN, SERVICE_NOTIFY, info)
def setup(hass, config): @asyncio.coroutine
def async_setup(hass, config):
"""Setup the notify services.""" """Setup the notify services."""
descriptions = load_yaml_config_file( descriptions = yield from hass.loop.run_in_executor(
None, load_yaml_config_file,
os.path.join(os.path.dirname(__file__), 'services.yaml')) os.path.join(os.path.dirname(__file__), 'services.yaml'))
targets = {} targets = {}
def setup_notify_platform(platform, p_config=None, discovery_info=None): @asyncio.coroutine
def async_setup_platform(p_type, p_config=None, discovery_info=None):
"""Set up a notify platform.""" """Set up a notify platform."""
if p_config is None: if p_config is None:
p_config = {} p_config = {}
if discovery_info is None: if discovery_info is None:
discovery_info = {} discovery_info = {}
notify_implementation = bootstrap.prepare_setup_platform( platform = yield from async_prepare_setup_platform(
hass, config, DOMAIN, platform) hass, config, DOMAIN, p_type)
if notify_implementation is None: if platform is None:
_LOGGER.error("Unknown notification service specified") _LOGGER.error("Unknown notification service specified")
return False return
notify_service = notify_implementation.get_service( _LOGGER.info("Setting up %s.%s", DOMAIN, p_type)
hass, p_config, discovery_info) notify_service = None
try:
if hasattr(platform, 'async_get_service'):
notify_service = yield from \
platform.async_get_service(hass, p_config, discovery_info)
elif hasattr(platform, 'get_service'):
notify_service = yield from hass.loop.run_in_executor(
None, platform.get_service, hass, p_config, discovery_info)
else:
raise HomeAssistantError("Invalid notify platform.")
if notify_service is None: if notify_service is None:
_LOGGER.error("Failed to initialize notification service %s", _LOGGER.error(
platform) "Failed to initialize notification service %s", p_type)
return False return
def notify_message(notify_service, call): except Exception: # pylint: disable=broad-except
_LOGGER.exception('Error setting up platform %s', p_type)
return
notify_service.hass = hass
@asyncio.coroutine
def async_notify_message(service):
"""Handle sending notification message service calls.""" """Handle sending notification message service calls."""
kwargs = {} kwargs = {}
message = call.data[ATTR_MESSAGE] message = service.data[ATTR_MESSAGE]
title = call.data.get(ATTR_TITLE) title = service.data.get(ATTR_TITLE)
if title: if title:
title.hass = hass title.hass = hass
kwargs[ATTR_TITLE] = title.render() kwargs[ATTR_TITLE] = title.async_render()
if targets.get(call.service) is not None: if targets.get(service.service) is not None:
kwargs[ATTR_TARGET] = [targets[call.service]] kwargs[ATTR_TARGET] = [targets[service.service]]
elif call.data.get(ATTR_TARGET) is not None: elif service.data.get(ATTR_TARGET) is not None:
kwargs[ATTR_TARGET] = call.data.get(ATTR_TARGET) kwargs[ATTR_TARGET] = service.data.get(ATTR_TARGET)
message.hass = hass message.hass = hass
kwargs[ATTR_MESSAGE] = message.render() kwargs[ATTR_MESSAGE] = message.async_render()
kwargs[ATTR_DATA] = call.data.get(ATTR_DATA) kwargs[ATTR_DATA] = service.data.get(ATTR_DATA)
notify_service.send_message(**kwargs) yield from notify_service.async_send_message(**kwargs)
service_call_handler = partial(notify_message, notify_service)
if hasattr(notify_service, 'targets'): if hasattr(notify_service, 'targets'):
platform_name = ( platform_name = (
p_config.get(CONF_NAME) or discovery_info.get(CONF_NAME) or p_config.get(CONF_NAME) or discovery_info.get(CONF_NAME) or
platform) p_type)
for name, target in notify_service.targets.items(): for name, target in notify_service.targets.items():
target_name = slugify('{}_{}'.format(platform_name, name)) target_name = slugify('{}_{}'.format(platform_name, name))
targets[target_name] = target targets[target_name] = target
hass.services.register(DOMAIN, target_name, hass.services.async_register(
service_call_handler, DOMAIN, target_name, async_notify_message,
descriptions.get(SERVICE_NOTIFY), descriptions.get(SERVICE_NOTIFY),
schema=NOTIFY_SERVICE_SCHEMA) schema=NOTIFY_SERVICE_SCHEMA)
@ -133,22 +152,24 @@ def setup(hass, config):
SERVICE_NOTIFY) SERVICE_NOTIFY)
platform_name_slug = slugify(platform_name) platform_name_slug = slugify(platform_name)
hass.services.register( hass.services.async_register(
DOMAIN, platform_name_slug, service_call_handler, DOMAIN, platform_name_slug, async_notify_message,
descriptions.get(SERVICE_NOTIFY), schema=NOTIFY_SERVICE_SCHEMA) descriptions.get(SERVICE_NOTIFY), schema=NOTIFY_SERVICE_SCHEMA)
return True return True
for platform, p_config in config_per_platform(config, DOMAIN): setup_tasks = [async_setup_platform(p_type, p_config) for p_type, p_config
if not setup_notify_platform(platform, p_config): in config_per_platform(config, DOMAIN)]
_LOGGER.error("Failed to set up platform %s", platform)
continue
def platform_discovered(platform, info): if setup_tasks:
yield from asyncio.wait(setup_tasks, loop=hass.loop)
@asyncio.coroutine
def async_platform_discovered(platform, info):
"""Callback to load a platform.""" """Callback to load a platform."""
setup_notify_platform(platform, discovery_info=info) yield from async_setup_platform(platform, discovery_info=info)
discovery.listen_platform(hass, DOMAIN, platform_discovered) discovery.async_listen_platform(hass, DOMAIN, async_platform_discovered)
return True return True
@ -156,9 +177,20 @@ def setup(hass, config):
class BaseNotificationService(object): class BaseNotificationService(object):
"""An abstract class for notification services.""" """An abstract class for notification services."""
hass = None
def send_message(self, message, **kwargs): def send_message(self, message, **kwargs):
"""Send a message. """Send a message.
kwargs can contain ATTR_TITLE to specify a title. kwargs can contain ATTR_TITLE to specify a title.
""" """
raise NotImplementedError raise NotImplementedError()
def async_send_message(self, message, **kwargs):
"""Send a message.
kwargs can contain ATTR_TITLE to specify a title.
This method must be run in the event loop and returns a coroutine.
"""
return self.hass.loop.run_in_executor(
None, partial(self.send_message, message, **kwargs))

View File

@ -4,7 +4,7 @@ import asyncio
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.notify import ( from homeassistant.components.notify import (
PLATFORM_SCHEMA, BaseNotificationService) PLATFORM_SCHEMA, BaseNotificationService, ATTR_TARGET)
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -32,20 +32,16 @@ class DiscordNotificationService(BaseNotificationService):
self.hass = hass self.hass = hass
@asyncio.coroutine @asyncio.coroutine
def async_send_message(self, message, target): def async_send_message(self, message, **kwargs):
"""Login to Discord, send message to channel(s) and log out.""" """Login to Discord, send message to channel(s) and log out."""
import discord import discord
discord_bot = discord.Client(loop=self.hass.loop) discord_bot = discord.Client(loop=self.hass.loop)
yield from discord_bot.login(self.token) yield from discord_bot.login(self.token)
for channelid in target: for channelid in kwargs[ATTR_TARGET]:
channel = discord.Object(id=channelid) channel = discord.Object(id=channelid)
yield from discord_bot.send_message(channel, message) yield from discord_bot.send_message(channel, message)
yield from discord_bot.logout() yield from discord_bot.logout()
yield from discord_bot.close() yield from discord_bot.close()
def send_message(self, message=None, target=None, **kwargs):
"""Send a message using Discord."""
self.hass.async_add_job(self.async_send_message(message, target))

View File

@ -4,15 +4,15 @@ Group platform for notify component.
For more details about this platform, please refer to the documentation at For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/notify.group/ https://home-assistant.io/components/notify.group/
""" """
import asyncio
import collections import collections
from copy import deepcopy from copy import deepcopy
import logging import logging
import voluptuous as vol import voluptuous as vol
from homeassistant.const import ATTR_SERVICE from homeassistant.const import ATTR_SERVICE
from homeassistant.components.notify import (DOMAIN, ATTR_MESSAGE, ATTR_DATA, from homeassistant.components.notify import (
PLATFORM_SCHEMA, DOMAIN, ATTR_MESSAGE, ATTR_DATA, PLATFORM_SCHEMA, BaseNotificationService)
BaseNotificationService)
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -28,7 +28,10 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
def update(input_dict, update_source): def update(input_dict, update_source):
"""Deep update a dictionary.""" """Deep update a dictionary.
Async friendly.
"""
for key, val in update_source.items(): for key, val in update_source.items():
if isinstance(val, collections.Mapping): if isinstance(val, collections.Mapping):
recurse = update(input_dict.get(key, {}), val) recurse = update(input_dict.get(key, {}), val)
@ -38,7 +41,8 @@ def update(input_dict, update_source):
return input_dict return input_dict
def get_service(hass, config, discovery_info=None): @asyncio.coroutine
def async_get_service(hass, config, discovery_info=None):
"""Get the Group notification service.""" """Get the Group notification service."""
return GroupNotifyPlatform(hass, config.get(CONF_SERVICES)) return GroupNotifyPlatform(hass, config.get(CONF_SERVICES))
@ -51,14 +55,19 @@ class GroupNotifyPlatform(BaseNotificationService):
self.hass = hass self.hass = hass
self.entities = entities self.entities = entities
def send_message(self, message="", **kwargs): @asyncio.coroutine
def async_send_message(self, message="", **kwargs):
"""Send message to all entities in the group.""" """Send message to all entities in the group."""
payload = {ATTR_MESSAGE: message} payload = {ATTR_MESSAGE: message}
payload.update({key: val for key, val in kwargs.items() if val}) payload.update({key: val for key, val in kwargs.items() if val})
tasks = []
for entity in self.entities: for entity in self.entities:
sending_payload = deepcopy(payload.copy()) sending_payload = deepcopy(payload.copy())
if entity.get(ATTR_DATA) is not None: if entity.get(ATTR_DATA) is not None:
update(sending_payload, entity.get(ATTR_DATA)) update(sending_payload, entity.get(ATTR_DATA))
self.hass.services.call(DOMAIN, entity.get(ATTR_SERVICE), tasks.append(self.hass.services.async_call(
sending_payload) DOMAIN, entity.get(ATTR_SERVICE), sending_payload))
if tasks:
yield from asyncio.wait(tasks, loop=self.hass.loop)

View File

@ -41,7 +41,9 @@ class TestApns(unittest.TestCase):
assert setup_component(self.hass, notify.DOMAIN, CONFIG) assert setup_component(self.hass, notify.DOMAIN, CONFIG)
assert handle_config[notify.DOMAIN] assert handle_config[notify.DOMAIN]
def test_apns_setup_full(self): @patch('os.path.isfile', return_value=True)
@patch('os.access', return_value=True)
def test_apns_setup_full(self, mock_access, mock_isfile):
"""Test setup with all data.""" """Test setup with all data."""
config = { config = {
'notify': { 'notify': {
@ -53,7 +55,9 @@ class TestApns(unittest.TestCase):
} }
} }
self.assertTrue(notify.setup(self.hass, config)) with assert_setup_component(1) as handle_config:
assert setup_component(self.hass, notify.DOMAIN, config)
assert handle_config[notify.DOMAIN]
def test_apns_setup_missing_name(self): def test_apns_setup_missing_name(self):
"""Test setup with missing name.""" """Test setup with missing name."""

View File

@ -1,4 +1,5 @@
"""The tests for the notify demo platform.""" """The tests for the notify demo platform."""
import asyncio
import unittest import unittest
from unittest.mock import patch from unittest.mock import patch
@ -16,6 +17,12 @@ CONFIG = {
} }
@asyncio.coroutine
def mock_setup_platform():
"""Mock prepare_setup_platform."""
return None
class TestNotifyDemo(unittest.TestCase): class TestNotifyDemo(unittest.TestCase):
"""Test the demo notify.""" """Test the demo notify."""
@ -45,23 +52,16 @@ class TestNotifyDemo(unittest.TestCase):
"""Test setup.""" """Test setup."""
self._setup_notify() self._setup_notify()
@patch('homeassistant.bootstrap.prepare_setup_platform') @patch('homeassistant.bootstrap.async_prepare_setup_platform',
return_value=mock_setup_platform())
def test_no_prepare_setup_platform(self, mock_prep_setup_platform): def test_no_prepare_setup_platform(self, mock_prep_setup_platform):
"""Test missing notify platform.""" """Test missing notify platform."""
mock_prep_setup_platform.return_value = None with assert_setup_component(0):
with self.assertLogs('homeassistant.components.notify', setup_component(self.hass, notify.DOMAIN, CONFIG)
level='ERROR') as log_handle:
self._setup_notify()
self.hass.block_till_done()
assert mock_prep_setup_platform.called
self.assertEqual(
log_handle.output,
['ERROR:homeassistant.components.notify:'
'Unknown notification service specified',
'ERROR:homeassistant.components.notify:'
'Failed to set up platform demo'])
@patch('homeassistant.components.notify.demo.get_service') assert mock_prep_setup_platform.called
@patch('homeassistant.components.notify.demo.get_service', autospec=True)
def test_no_notify_service(self, mock_demo_get_service): def test_no_notify_service(self, mock_demo_get_service):
"""Test missing platform notify service instance.""" """Test missing platform notify service instance."""
mock_demo_get_service.return_value = None mock_demo_get_service.return_value = None
@ -73,11 +73,9 @@ class TestNotifyDemo(unittest.TestCase):
self.assertEqual( self.assertEqual(
log_handle.output, log_handle.output,
['ERROR:homeassistant.components.notify:' ['ERROR:homeassistant.components.notify:'
'Failed to initialize notification service demo', 'Failed to initialize notification service demo'])
'ERROR:homeassistant.components.notify:'
'Failed to set up platform demo'])
@patch('homeassistant.components.notify.demo.get_service') @patch('homeassistant.components.notify.demo.get_service', autospec=True)
def test_discover_notify(self, mock_demo_get_service): def test_discover_notify(self, mock_demo_get_service):
"""Test discovery of notify demo platform.""" """Test discovery of notify demo platform."""
assert notify.DOMAIN not in self.hass.config.components assert notify.DOMAIN not in self.hass.config.components

View File

@ -5,6 +5,7 @@ from unittest.mock import MagicMock, patch
from homeassistant.bootstrap import setup_component from homeassistant.bootstrap import setup_component
import homeassistant.components.notify as notify import homeassistant.components.notify as notify
from homeassistant.components.notify import group, demo from homeassistant.components.notify import group, demo
from homeassistant.util.async import run_coroutine_threadsafe
from tests.common import assert_setup_component, get_test_home_assistant from tests.common import assert_setup_component, get_test_home_assistant
@ -16,8 +17,11 @@ class TestNotifyGroup(unittest.TestCase):
"""Setup things to be run when tests are started.""" """Setup things to be run when tests are started."""
self.hass = get_test_home_assistant() self.hass = get_test_home_assistant()
self.events = [] self.events = []
self.service1 = MagicMock() self.service1 = demo.DemoNotificationService(self.hass)
self.service2 = MagicMock() self.service2 = demo.DemoNotificationService(self.hass)
self.service1.send_message = MagicMock(autospec=True)
self.service2.send_message = MagicMock(autospec=True)
def mock_get_service(hass, config, discovery_info=None): def mock_get_service(hass, config, discovery_info=None):
if config['name'] == 'demo1': if config['name'] == 'demo1':
@ -37,11 +41,14 @@ class TestNotifyGroup(unittest.TestCase):
}] }]
}) })
self.service = group.get_service(self.hass, {'services': [ self.service = run_coroutine_threadsafe(
group.async_get_service(self.hass, {'services': [
{'service': 'demo1'}, {'service': 'demo1'},
{'service': 'demo2', {'service': 'demo2',
'data': {'target': 'unnamed device', 'data': {'target': 'unnamed device',
'data': {'test': 'message'}}}]}) 'data': {'test': 'message'}}}]}),
self.hass.loop
).result()
assert self.service is not None assert self.service is not None
@ -51,17 +58,19 @@ class TestNotifyGroup(unittest.TestCase):
def test_send_message_with_data(self): def test_send_message_with_data(self):
"""Test sending a message with to a notify group.""" """Test sending a message with to a notify group."""
self.service.send_message('Hello', title='Test notification', run_coroutine_threadsafe(
data={'hello': 'world'}) self.service.async_send_message(
'Hello', title='Test notification', data={'hello': 'world'}),
self.hass.loop).result()
self.hass.block_till_done() self.hass.block_till_done()
assert self.service1.send_message.mock_calls[0][1][0] == 'Hello'
assert self.service1.send_message.mock_calls[0][2] == { assert self.service1.send_message.mock_calls[0][2] == {
'message': 'Hello',
'title': 'Test notification', 'title': 'Test notification',
'data': {'hello': 'world'} 'data': {'hello': 'world'}
} }
assert self.service2.send_message.mock_calls[0][1][0] == 'Hello'
assert self.service2.send_message.mock_calls[0][2] == { assert self.service2.send_message.mock_calls[0][2] == {
'message': 'Hello',
'target': ['unnamed device'], 'target': ['unnamed device'],
'title': 'Test notification', 'title': 'Test notification',
'data': {'hello': 'world', 'test': 'message'} 'data': {'hello': 'world', 'test': 'message'}