mirror of
https://github.com/home-assistant/core.git
synced 2025-07-23 13:17:32 +00:00
Merge pull request #862 from balloob/alexa-service
Add calling service functionality to Alexa
This commit is contained in:
commit
058f6f9303
@ -11,6 +11,7 @@ import logging
|
|||||||
|
|
||||||
from homeassistant.const import HTTP_OK, HTTP_UNPROCESSABLE_ENTITY
|
from homeassistant.const import HTTP_OK, HTTP_UNPROCESSABLE_ENTITY
|
||||||
from homeassistant.util import template
|
from homeassistant.util import template
|
||||||
|
from homeassistant.helpers.service import call_from_config
|
||||||
|
|
||||||
DOMAIN = 'alexa'
|
DOMAIN = 'alexa'
|
||||||
DEPENDENCIES = ['http']
|
DEPENDENCIES = ['http']
|
||||||
@ -23,6 +24,7 @@ API_ENDPOINT = '/api/alexa'
|
|||||||
CONF_INTENTS = 'intents'
|
CONF_INTENTS = 'intents'
|
||||||
CONF_CARD = 'card'
|
CONF_CARD = 'card'
|
||||||
CONF_SPEECH = 'speech'
|
CONF_SPEECH = 'speech'
|
||||||
|
CONF_ACTION = 'action'
|
||||||
|
|
||||||
|
|
||||||
def setup(hass, config):
|
def setup(hass, config):
|
||||||
@ -80,6 +82,7 @@ def _handle_alexa(handler, path_match, data):
|
|||||||
|
|
||||||
speech = config.get(CONF_SPEECH)
|
speech = config.get(CONF_SPEECH)
|
||||||
card = config.get(CONF_CARD)
|
card = config.get(CONF_CARD)
|
||||||
|
action = config.get(CONF_ACTION)
|
||||||
|
|
||||||
# pylint: disable=unsubscriptable-object
|
# pylint: disable=unsubscriptable-object
|
||||||
if speech is not None:
|
if speech is not None:
|
||||||
@ -89,6 +92,9 @@ def _handle_alexa(handler, path_match, data):
|
|||||||
response.add_card(CardType[card['type']], card['title'],
|
response.add_card(CardType[card['type']], card['title'],
|
||||||
card['content'])
|
card['content'])
|
||||||
|
|
||||||
|
if action is not None:
|
||||||
|
call_from_config(handler.server.hass, action, True)
|
||||||
|
|
||||||
handler.write_json(response.as_dict())
|
handler.write_json(response.as_dict())
|
||||||
|
|
||||||
|
|
||||||
|
@ -9,9 +9,9 @@ https://home-assistant.io/components/automation/
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
from homeassistant.bootstrap import prepare_setup_platform
|
from homeassistant.bootstrap import prepare_setup_platform
|
||||||
from homeassistant.util import split_entity_id
|
from homeassistant.const import CONF_PLATFORM
|
||||||
from homeassistant.const import ATTR_ENTITY_ID, CONF_PLATFORM
|
|
||||||
from homeassistant.components import logbook
|
from homeassistant.components import logbook
|
||||||
|
from homeassistant.helpers.service import call_from_config
|
||||||
|
|
||||||
DOMAIN = 'automation'
|
DOMAIN = 'automation'
|
||||||
|
|
||||||
@ -19,8 +19,6 @@ DEPENDENCIES = ['group']
|
|||||||
|
|
||||||
CONF_ALIAS = 'alias'
|
CONF_ALIAS = 'alias'
|
||||||
CONF_SERVICE = 'service'
|
CONF_SERVICE = 'service'
|
||||||
CONF_SERVICE_ENTITY_ID = 'entity_id'
|
|
||||||
CONF_SERVICE_DATA = 'data'
|
|
||||||
|
|
||||||
CONF_CONDITION = 'condition'
|
CONF_CONDITION = 'condition'
|
||||||
CONF_ACTION = 'action'
|
CONF_ACTION = 'action'
|
||||||
@ -96,22 +94,7 @@ def _get_action(hass, config, name):
|
|||||||
_LOGGER.info('Executing %s', name)
|
_LOGGER.info('Executing %s', name)
|
||||||
logbook.log_entry(hass, name, 'has been triggered', DOMAIN)
|
logbook.log_entry(hass, name, 'has been triggered', DOMAIN)
|
||||||
|
|
||||||
domain, service = split_entity_id(config[CONF_SERVICE])
|
call_from_config(hass, config)
|
||||||
service_data = config.get(CONF_SERVICE_DATA, {})
|
|
||||||
|
|
||||||
if not isinstance(service_data, dict):
|
|
||||||
_LOGGER.error("%s should be a dictionary", CONF_SERVICE_DATA)
|
|
||||||
service_data = {}
|
|
||||||
|
|
||||||
if CONF_SERVICE_ENTITY_ID in config:
|
|
||||||
try:
|
|
||||||
service_data[ATTR_ENTITY_ID] = \
|
|
||||||
config[CONF_SERVICE_ENTITY_ID].split(",")
|
|
||||||
except AttributeError:
|
|
||||||
service_data[ATTR_ENTITY_ID] = \
|
|
||||||
config[CONF_SERVICE_ENTITY_ID]
|
|
||||||
|
|
||||||
hass.services.call(domain, service, service_data)
|
|
||||||
|
|
||||||
return action
|
return action
|
||||||
|
|
||||||
|
43
homeassistant/helpers/service.py
Normal file
43
homeassistant/helpers/service.py
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
"""Service calling related helpers."""
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from homeassistant.util import split_entity_id
|
||||||
|
from homeassistant.const import ATTR_ENTITY_ID
|
||||||
|
|
||||||
|
CONF_SERVICE = 'service'
|
||||||
|
CONF_SERVICE_ENTITY_ID = 'entity_id'
|
||||||
|
CONF_SERVICE_DATA = 'data'
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def call_from_config(hass, config, blocking=False):
|
||||||
|
"""Call a service based on a config hash."""
|
||||||
|
if not isinstance(config, dict) or CONF_SERVICE not in config:
|
||||||
|
_LOGGER.error('Missing key %s: %s', CONF_SERVICE, config)
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
domain, service = split_entity_id(config[CONF_SERVICE])
|
||||||
|
except ValueError:
|
||||||
|
_LOGGER.error('Invalid service specified: %s', config[CONF_SERVICE])
|
||||||
|
return
|
||||||
|
|
||||||
|
service_data = config.get(CONF_SERVICE_DATA)
|
||||||
|
|
||||||
|
if service_data is None:
|
||||||
|
service_data = {}
|
||||||
|
elif isinstance(service_data, dict):
|
||||||
|
service_data = dict(service_data)
|
||||||
|
else:
|
||||||
|
_LOGGER.error("%s should be a dictionary", CONF_SERVICE_DATA)
|
||||||
|
service_data = {}
|
||||||
|
|
||||||
|
entity_id = config.get(CONF_SERVICE_ENTITY_ID)
|
||||||
|
if isinstance(entity_id, str):
|
||||||
|
service_data[ATTR_ENTITY_ID] = [ent.strip() for ent in
|
||||||
|
entity_id.split(",")]
|
||||||
|
elif entity_id is not None:
|
||||||
|
service_data[ATTR_ENTITY_ID] = entity_id
|
||||||
|
|
||||||
|
hass.services.call(domain, service, service_data, blocking)
|
@ -27,12 +27,13 @@ API_URL = "http://127.0.0.1:{}{}".format(SERVER_PORT, alexa.API_ENDPOINT)
|
|||||||
HA_HEADERS = {const.HTTP_HEADER_HA_AUTH: API_PASSWORD}
|
HA_HEADERS = {const.HTTP_HEADER_HA_AUTH: API_PASSWORD}
|
||||||
|
|
||||||
hass = None
|
hass = None
|
||||||
|
calls = []
|
||||||
|
|
||||||
|
|
||||||
@patch('homeassistant.components.http.util.get_local_ip',
|
@patch('homeassistant.components.http.util.get_local_ip',
|
||||||
return_value='127.0.0.1')
|
return_value='127.0.0.1')
|
||||||
def setUpModule(mock_get_local_ip): # pylint: disable=invalid-name
|
def setUpModule(mock_get_local_ip): # pylint: disable=invalid-name
|
||||||
""" Initalizes a Home Assistant server. """
|
"""Initalize a Home Assistant server for testing this module."""
|
||||||
global hass
|
global hass
|
||||||
|
|
||||||
hass = ha.HomeAssistant()
|
hass = ha.HomeAssistant()
|
||||||
@ -42,6 +43,8 @@ def setUpModule(mock_get_local_ip): # pylint: disable=invalid-name
|
|||||||
{http.DOMAIN: {http.CONF_API_PASSWORD: API_PASSWORD,
|
{http.DOMAIN: {http.CONF_API_PASSWORD: API_PASSWORD,
|
||||||
http.CONF_SERVER_PORT: SERVER_PORT}})
|
http.CONF_SERVER_PORT: SERVER_PORT}})
|
||||||
|
|
||||||
|
hass.services.register('test', 'alexa', lambda call: calls.append(call))
|
||||||
|
|
||||||
bootstrap.setup_component(hass, alexa.DOMAIN, {
|
bootstrap.setup_component(hass, alexa.DOMAIN, {
|
||||||
'alexa': {
|
'alexa': {
|
||||||
'intents': {
|
'intents': {
|
||||||
@ -61,7 +64,20 @@ def setUpModule(mock_get_local_ip): # pylint: disable=invalid-name
|
|||||||
'GetZodiacHoroscopeIntent': {
|
'GetZodiacHoroscopeIntent': {
|
||||||
'speech': {
|
'speech': {
|
||||||
'type': 'plaintext',
|
'type': 'plaintext',
|
||||||
'text': 'You told us your sign is {{ ZodiacSign }}.'
|
'text': 'You told us your sign is {{ ZodiacSign }}.',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'CallServiceIntent': {
|
||||||
|
'speech': {
|
||||||
|
'type': 'plaintext',
|
||||||
|
'text': 'Service called',
|
||||||
|
},
|
||||||
|
'action': {
|
||||||
|
'service': 'test.alexa',
|
||||||
|
'data': {
|
||||||
|
'hello': 1
|
||||||
|
},
|
||||||
|
'entity_id': 'switch.test',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -231,6 +247,39 @@ class TestAlexa(unittest.TestCase):
|
|||||||
text = req.json().get('response', {}).get('outputSpeech', {}).get('text')
|
text = req.json().get('response', {}).get('outputSpeech', {}).get('text')
|
||||||
self.assertEqual('You are both home, you silly', text)
|
self.assertEqual('You are both home, you silly', text)
|
||||||
|
|
||||||
|
def test_intent_request_calling_service(self):
|
||||||
|
data = {
|
||||||
|
'version': '1.0',
|
||||||
|
'session': {
|
||||||
|
'new': False,
|
||||||
|
'sessionId': 'amzn1.echo-api.session.0000000-0000-0000-0000-00000000000',
|
||||||
|
'application': {
|
||||||
|
'applicationId': 'amzn1.echo-sdk-ams.app.000000-d0ed-0000-ad00-000000d00ebe'
|
||||||
|
},
|
||||||
|
'attributes': {},
|
||||||
|
'user': {
|
||||||
|
'userId': 'amzn1.account.AM3B00000000000000000000000'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'request': {
|
||||||
|
'type': 'IntentRequest',
|
||||||
|
'requestId': ' amzn1.echo-api.request.0000000-0000-0000-0000-00000000000',
|
||||||
|
'timestamp': '2015-05-13T12:34:56Z',
|
||||||
|
'intent': {
|
||||||
|
'name': 'CallServiceIntent',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
call_count = len(calls)
|
||||||
|
req = _req(data)
|
||||||
|
self.assertEqual(200, req.status_code)
|
||||||
|
self.assertEqual(call_count + 1, len(calls))
|
||||||
|
call = calls[-1]
|
||||||
|
self.assertEqual('test', call.domain)
|
||||||
|
self.assertEqual('alexa', call.service)
|
||||||
|
self.assertEqual(['switch.test'], call.data.get('entity_id'))
|
||||||
|
self.assertEqual(1, call.data.get('hello'))
|
||||||
|
|
||||||
def test_session_ended_request(self):
|
def test_session_ended_request(self):
|
||||||
data = {
|
data = {
|
||||||
'version': '1.0',
|
'version': '1.0',
|
||||||
|
68
tests/helpers/test_service.py
Normal file
68
tests/helpers/test_service.py
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
"""
|
||||||
|
tests.helpers.test_service
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
Test service helpers.
|
||||||
|
"""
|
||||||
|
import unittest
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
from homeassistant.const import SERVICE_TURN_ON
|
||||||
|
from homeassistant.helpers import service
|
||||||
|
|
||||||
|
from tests.common import get_test_home_assistant, mock_service
|
||||||
|
|
||||||
|
|
||||||
|
class TestServiceHelpers(unittest.TestCase):
|
||||||
|
"""
|
||||||
|
Tests the Home Assistant service helpers.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def setUp(self): # pylint: disable=invalid-name
|
||||||
|
""" things to be run when tests are started. """
|
||||||
|
self.hass = get_test_home_assistant()
|
||||||
|
self.calls = mock_service(self.hass, 'test_domain', 'test_service')
|
||||||
|
|
||||||
|
def tearDown(self): # pylint: disable=invalid-name
|
||||||
|
""" Stop down stuff we started. """
|
||||||
|
self.hass.stop()
|
||||||
|
|
||||||
|
def test_split_entity_string(self):
|
||||||
|
service.call_from_config(self.hass, {
|
||||||
|
'service': 'test_domain.test_service',
|
||||||
|
'entity_id': 'hello.world, sensor.beer'
|
||||||
|
})
|
||||||
|
self.hass.pool.block_till_done()
|
||||||
|
self.assertEqual(['hello.world', 'sensor.beer'],
|
||||||
|
self.calls[-1].data.get('entity_id'))
|
||||||
|
|
||||||
|
def test_not_mutate_input(self):
|
||||||
|
orig = {
|
||||||
|
'service': 'test_domain.test_service',
|
||||||
|
'entity_id': 'hello.world, sensor.beer',
|
||||||
|
'data': {
|
||||||
|
'hello': 1,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
service.call_from_config(self.hass, orig)
|
||||||
|
self.hass.pool.block_till_done()
|
||||||
|
self.assertEqual({
|
||||||
|
'service': 'test_domain.test_service',
|
||||||
|
'entity_id': 'hello.world, sensor.beer',
|
||||||
|
'data': {
|
||||||
|
'hello': 1,
|
||||||
|
},
|
||||||
|
}, orig)
|
||||||
|
|
||||||
|
@patch('homeassistant.helpers.service._LOGGER.error')
|
||||||
|
def test_fail_silently_if_no_service(self, mock_log):
|
||||||
|
service.call_from_config(self.hass, None)
|
||||||
|
self.assertEqual(1, mock_log.call_count)
|
||||||
|
|
||||||
|
service.call_from_config(self.hass, {})
|
||||||
|
self.assertEqual(2, mock_log.call_count)
|
||||||
|
|
||||||
|
service.call_from_config(self.hass, {
|
||||||
|
'service': 'invalid'
|
||||||
|
})
|
||||||
|
self.assertEqual(3, mock_log.call_count)
|
Loading…
x
Reference in New Issue
Block a user