From 8aea5386626f8b41b83458e36e5fa36068e8674f Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 5 Mar 2020 11:55:50 -0800 Subject: [PATCH] Allow teaching logbook about events (#32444) * Allow teaching logbook about events * Use async_add_executor_job * Fix tests --- homeassistant/components/alexa/__init__.py | 34 +++++- homeassistant/components/alexa/const.py | 1 + homeassistant/components/alexa/manifest.json | 1 + homeassistant/components/alexa/smart_home.py | 4 +- homeassistant/components/cloud/manifest.json | 4 +- homeassistant/components/logbook/__init__.py | 51 ++++---- homeassistant/scripts/benchmark/__init__.py | 2 +- tests/components/alexa/test_init.py | 63 ++++++++++ tests/components/alexa/test_intent.py | 3 +- tests/components/logbook/test_init.py | 117 ++++++++----------- 10 files changed, 175 insertions(+), 105 deletions(-) create mode 100644 tests/components/alexa/test_init.py diff --git a/homeassistant/components/alexa/__init__.py b/homeassistant/components/alexa/__init__.py index 5861c4cc985..1355b0123b8 100644 --- a/homeassistant/components/alexa/__init__.py +++ b/homeassistant/components/alexa/__init__.py @@ -4,6 +4,7 @@ import logging import voluptuous as vol from homeassistant.const import CONF_NAME +from homeassistant.core import callback from homeassistant.helpers import config_validation as cv, entityfilter from . import flash_briefings, intent, smart_home_http @@ -23,6 +24,7 @@ from .const import ( CONF_TITLE, CONF_UID, DOMAIN, + EVENT_ALEXA_SMART_HOME, ) _LOGGER = logging.getLogger(__name__) @@ -80,7 +82,37 @@ CONFIG_SCHEMA = vol.Schema( async def async_setup(hass, config): """Activate the Alexa component.""" - config = config.get(DOMAIN, {}) + + @callback + def async_describe_logbook_event(event): + """Describe a logbook event.""" + data = event.data + entity_id = data["request"].get("entity_id") + + if entity_id: + state = hass.states.get(entity_id) + name = state.name if state else entity_id + message = f"send command {data['request']['namespace']}/{data['request']['name']} for {name}" + else: + message = ( + f"send command {data['request']['namespace']}/{data['request']['name']}" + ) + + return { + "name": "Amazon Alexa", + "message": message, + "entity_id": entity_id, + } + + hass.components.logbook.async_describe_event( + DOMAIN, EVENT_ALEXA_SMART_HOME, async_describe_logbook_event + ) + + if DOMAIN not in config: + return True + + config = config[DOMAIN] + flash_briefings_config = config.get(CONF_FLASH_BRIEFINGS) intent.async_setup(hass) diff --git a/homeassistant/components/alexa/const.py b/homeassistant/components/alexa/const.py index e45bcf824bc..ca1c6236fe6 100644 --- a/homeassistant/components/alexa/const.py +++ b/homeassistant/components/alexa/const.py @@ -6,6 +6,7 @@ from homeassistant.components.climate import const as climate from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT DOMAIN = "alexa" +EVENT_ALEXA_SMART_HOME = "alexa_smart_home" # Flash briefing constants CONF_UID = "uid" diff --git a/homeassistant/components/alexa/manifest.json b/homeassistant/components/alexa/manifest.json index 5334cf765b8..bf8d4b08ba4 100644 --- a/homeassistant/components/alexa/manifest.json +++ b/homeassistant/components/alexa/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/alexa", "requirements": [], "dependencies": ["http"], + "after_dependencies": ["logbook"], "codeowners": ["@home-assistant/cloud", "@ochlocracy"] } diff --git a/homeassistant/components/alexa/smart_home.py b/homeassistant/components/alexa/smart_home.py index 9b0955f8fca..0f166ab3a27 100644 --- a/homeassistant/components/alexa/smart_home.py +++ b/homeassistant/components/alexa/smart_home.py @@ -3,15 +3,13 @@ import logging import homeassistant.core as ha -from .const import API_DIRECTIVE, API_HEADER +from .const import API_DIRECTIVE, API_HEADER, EVENT_ALEXA_SMART_HOME from .errors import AlexaBridgeUnreachableError, AlexaError from .handlers import HANDLERS from .messages import AlexaDirective _LOGGER = logging.getLogger(__name__) -EVENT_ALEXA_SMART_HOME = "alexa_smart_home" - async def async_handle_message(hass, config, request, context=None, enabled=True): """Handle incoming API messages. diff --git a/homeassistant/components/cloud/manifest.json b/homeassistant/components/cloud/manifest.json index 34ef7a6dfa5..f3ea66971ac 100644 --- a/homeassistant/components/cloud/manifest.json +++ b/homeassistant/components/cloud/manifest.json @@ -3,7 +3,7 @@ "name": "Home Assistant Cloud", "documentation": "https://www.home-assistant.io/integrations/cloud", "requirements": ["hass-nabucasa==0.31"], - "dependencies": ["http", "webhook"], - "after_dependencies": ["alexa", "google_assistant"], + "dependencies": ["http", "webhook", "alexa"], + "after_dependencies": ["google_assistant"], "codeowners": ["@home-assistant/cloud"] } diff --git a/homeassistant/components/logbook/__init__.py b/homeassistant/components/logbook/__init__.py index 9921abdb59d..959b2be68d9 100644 --- a/homeassistant/components/logbook/__init__.py +++ b/homeassistant/components/logbook/__init__.py @@ -8,7 +8,6 @@ from sqlalchemy.exc import SQLAlchemyError import voluptuous as vol from homeassistant.components import sun -from homeassistant.components.alexa.smart_home import EVENT_ALEXA_SMART_HOME from homeassistant.components.homekit.const import ( ATTR_DISPLAY_NAME, ATTR_VALUE, @@ -90,7 +89,6 @@ ALL_EVENT_TYPES = [ EVENT_LOGBOOK_ENTRY, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, - EVENT_ALEXA_SMART_HOME, EVENT_HOMEKIT_CHANGED, EVENT_AUTOMATION_TRIGGERED, EVENT_SCRIPT_STARTED, @@ -124,6 +122,12 @@ def async_log_entry(hass, name, message, domain=None, entity_id=None): hass.bus.async_fire(EVENT_LOGBOOK_ENTRY, data) +@bind_hass +def async_describe_event(hass, domain, event_name, describe_callback): + """Teach logbook how to describe a new event.""" + hass.data.setdefault(DOMAIN, {})[event_name] = (domain, describe_callback) + + async def async_setup(hass, config): """Listen for download events to download files.""" @@ -237,7 +241,17 @@ def humanify(hass, events): start_stop_events[event.time_fired.minute] = 2 # Yield entries + external_events = hass.data.get(DOMAIN, {}) for event in events_batch: + if event.event_type in external_events: + domain, describe_event = external_events[event.event_type] + data = describe_event(event) + data["when"] = event.time_fired + data["domain"] = domain + data["context_id"] = event.context.id + data["context_user_id"] = event.context.user_id + yield data + if event.event_type == EVENT_STATE_CHANGED: to_state = State.from_dict(event.data.get("new_state")) @@ -320,27 +334,6 @@ def humanify(hass, events): "context_user_id": event.context.user_id, } - elif event.event_type == EVENT_ALEXA_SMART_HOME: - data = event.data - entity_id = data["request"].get("entity_id") - - if entity_id: - state = hass.states.get(entity_id) - name = state.name if state else entity_id - message = f"send command {data['request']['namespace']}/{data['request']['name']} for {name}" - else: - message = f"send command {data['request']['namespace']}/{data['request']['name']}" - - yield { - "when": event.time_fired, - "name": "Amazon Alexa", - "message": message, - "domain": "alexa", - "entity_id": entity_id, - "context_id": event.context.id, - "context_user_id": event.context.user_id, - } - elif event.event_type == EVENT_HOMEKIT_CHANGED: data = event.data entity_id = data.get(ATTR_ENTITY_ID) @@ -436,7 +429,7 @@ def _get_events(hass, config, start_day, end_day, entity_id=None): """Yield Events that are not filtered away.""" for row in query.yield_per(500): event = row.to_native() - if _keep_event(event, entities_filter): + if _keep_event(hass, event, entities_filter): yield event with session_scope(hass=hass) as session: @@ -449,7 +442,9 @@ def _get_events(hass, config, start_day, end_day, entity_id=None): session.query(Events) .order_by(Events.time_fired) .outerjoin(States, (Events.event_id == States.event_id)) - .filter(Events.event_type.in_(ALL_EVENT_TYPES)) + .filter( + Events.event_type.in_(ALL_EVENT_TYPES + list(hass.data.get(DOMAIN, {}))) + ) .filter((Events.time_fired > start_day) & (Events.time_fired < end_day)) .filter( ( @@ -463,7 +458,7 @@ def _get_events(hass, config, start_day, end_day, entity_id=None): return list(humanify(hass, yield_events(query))) -def _keep_event(event, entities_filter): +def _keep_event(hass, event, entities_filter): domain, entity_id = None, None if event.event_type == EVENT_STATE_CHANGED: @@ -514,8 +509,8 @@ def _keep_event(event, entities_filter): domain = "script" entity_id = event.data.get(ATTR_ENTITY_ID) - elif event.event_type == EVENT_ALEXA_SMART_HOME: - domain = "alexa" + elif event.event_type in hass.data.get(DOMAIN, {}): + domain = hass.data[DOMAIN][event.event_type][0] elif event.event_type == EVENT_HOMEKIT_CHANGED: domain = DOMAIN_HOMEKIT diff --git a/homeassistant/scripts/benchmark/__init__.py b/homeassistant/scripts/benchmark/__init__.py index 4d7df6d7248..d28b8ab08f7 100644 --- a/homeassistant/scripts/benchmark/__init__.py +++ b/homeassistant/scripts/benchmark/__init__.py @@ -177,7 +177,7 @@ def _logbook_filtering(hass, last_changed, last_updated): # pylint: disable=protected-access entities_filter = logbook._generate_filter_from_config({}) for _ in range(10 ** 5): - if logbook._keep_event(event, entities_filter): + if logbook._keep_event(hass, event, entities_filter): yield event start = timer() diff --git a/tests/components/alexa/test_init.py b/tests/components/alexa/test_init.py new file mode 100644 index 00000000000..212b48cb436 --- /dev/null +++ b/tests/components/alexa/test_init.py @@ -0,0 +1,63 @@ +"""Tests for alexa.""" +from homeassistant.components import logbook +from homeassistant.components.alexa.const import EVENT_ALEXA_SMART_HOME +import homeassistant.core as ha +from homeassistant.setup import async_setup_component + + +async def test_humanify_alexa_event(hass): + """Test humanifying Alexa event.""" + await async_setup_component(hass, "alexa", {}) + hass.states.async_set("light.kitchen", "on", {"friendly_name": "Kitchen Light"}) + + results = list( + logbook.humanify( + hass, + [ + ha.Event( + EVENT_ALEXA_SMART_HOME, + {"request": {"namespace": "Alexa.Discovery", "name": "Discover"}}, + ), + ha.Event( + EVENT_ALEXA_SMART_HOME, + { + "request": { + "namespace": "Alexa.PowerController", + "name": "TurnOn", + "entity_id": "light.kitchen", + } + }, + ), + ha.Event( + EVENT_ALEXA_SMART_HOME, + { + "request": { + "namespace": "Alexa.PowerController", + "name": "TurnOn", + "entity_id": "light.non_existing", + } + }, + ), + ], + ) + ) + + event1, event2, event3 = results + + assert event1["name"] == "Amazon Alexa" + assert event1["message"] == "send command Alexa.Discovery/Discover" + assert event1["entity_id"] is None + + assert event2["name"] == "Amazon Alexa" + assert ( + event2["message"] + == "send command Alexa.PowerController/TurnOn for Kitchen Light" + ) + assert event2["entity_id"] == "light.kitchen" + + assert event3["name"] == "Amazon Alexa" + assert ( + event3["message"] + == "send command Alexa.PowerController/TurnOn for light.non_existing" + ) + assert event3["entity_id"] == "light.non_existing" diff --git a/tests/components/alexa/test_intent.py b/tests/components/alexa/test_intent.py index 962ba677403..8937a7938ac 100644 --- a/tests/components/alexa/test_intent.py +++ b/tests/components/alexa/test_intent.py @@ -37,7 +37,8 @@ def alexa_client(loop, hass, hass_client): alexa.DOMAIN, { # Key is here to verify we allow other keys in config too - "homeassistant": {} + "homeassistant": {}, + "alexa": {}, }, ) ) diff --git a/tests/components/logbook/test_init.py b/tests/components/logbook/test_init.py index 750ad17b523..e64abe8dbb7 100644 --- a/tests/components/logbook/test_init.py +++ b/tests/components/logbook/test_init.py @@ -4,6 +4,7 @@ from datetime import datetime, timedelta import logging import unittest +from asynctest import patch import pytest import voluptuous as vol @@ -169,7 +170,7 @@ class TestComponentLogbook(unittest.TestCase): events = [ e for e in (ha.Event(EVENT_HOMEASSISTANT_STOP), eventA, eventB) - if logbook._keep_event(e, entities_filter) + if logbook._keep_event(self.hass, e, entities_filter) ] entries = list(logbook.humanify(self.hass, events)) @@ -196,7 +197,7 @@ class TestComponentLogbook(unittest.TestCase): events = [ e for e in (ha.Event(EVENT_HOMEASSISTANT_STOP), eventA, eventB) - if logbook._keep_event(e, entities_filter) + if logbook._keep_event(self.hass, e, entities_filter) ] entries = list(logbook.humanify(self.hass, events)) @@ -224,7 +225,7 @@ class TestComponentLogbook(unittest.TestCase): events = [ e for e in (ha.Event(EVENT_HOMEASSISTANT_STOP), eventA, eventB) - if logbook._keep_event(e, entities_filter) + if logbook._keep_event(self.hass, e, entities_filter) ] entries = list(logbook.humanify(self.hass, events)) @@ -258,7 +259,7 @@ class TestComponentLogbook(unittest.TestCase): events = [ e for e in (ha.Event(EVENT_HOMEASSISTANT_STOP), eventA, eventB) - if logbook._keep_event(e, entities_filter) + if logbook._keep_event(self.hass, e, entities_filter) ] entries = list(logbook.humanify(self.hass, events)) @@ -300,7 +301,7 @@ class TestComponentLogbook(unittest.TestCase): eventA, eventB, ) - if logbook._keep_event(e, entities_filter) + if logbook._keep_event(self.hass, e, entities_filter) ] entries = list(logbook.humanify(self.hass, events)) @@ -341,7 +342,7 @@ class TestComponentLogbook(unittest.TestCase): events = [ e for e in (ha.Event(EVENT_HOMEASSISTANT_STOP), eventA, eventB) - if logbook._keep_event(e, entities_filter) + if logbook._keep_event(self.hass, e, entities_filter) ] entries = list(logbook.humanify(self.hass, events)) @@ -380,7 +381,7 @@ class TestComponentLogbook(unittest.TestCase): events = [ e for e in (ha.Event(EVENT_HOMEASSISTANT_STOP), eventA, eventB) - if logbook._keep_event(e, entities_filter) + if logbook._keep_event(self.hass, e, entities_filter) ] entries = list(logbook.humanify(self.hass, events)) @@ -412,7 +413,7 @@ class TestComponentLogbook(unittest.TestCase): events = [ e for e in (ha.Event(EVENT_HOMEASSISTANT_STOP), eventA, eventB) - if logbook._keep_event(e, entities_filter) + if logbook._keep_event(self.hass, e, entities_filter) ] entries = list(logbook.humanify(self.hass, events)) @@ -426,6 +427,7 @@ class TestComponentLogbook(unittest.TestCase): def test_include_events_domain(self): """Test if events are filtered if domain is included in config.""" + assert setup_component(self.hass, "alexa", {}) entity_id = "switch.bla" entity_id2 = "sensor.blu" pointA = dt_util.utcnow() @@ -467,7 +469,7 @@ class TestComponentLogbook(unittest.TestCase): eventA, eventB, ) - if logbook._keep_event(e, entities_filter) + if logbook._keep_event(self.hass, e, entities_filter) ] entries = list(logbook.humanify(self.hass, events)) @@ -521,7 +523,7 @@ class TestComponentLogbook(unittest.TestCase): eventB1, eventB2, ) - if logbook._keep_event(e, entities_filter) + if logbook._keep_event(self.hass, e, entities_filter) ] entries = list(logbook.humanify(self.hass, events)) @@ -553,7 +555,9 @@ class TestComponentLogbook(unittest.TestCase): entities_filter = logbook._generate_filter_from_config({}) events = [ - e for e in (eventA, eventB) if logbook._keep_event(e, entities_filter) + e + for e in (eventA, eventB) + if logbook._keep_event(self.hass, e, entities_filter) ] entries = list(logbook.humanify(self.hass, events)) @@ -576,7 +580,9 @@ class TestComponentLogbook(unittest.TestCase): entities_filter = logbook._generate_filter_from_config({}) events = [ - e for e in (eventA, eventB) if logbook._keep_event(e, entities_filter) + e + for e in (eventA, eventB) + if logbook._keep_event(self.hass, e, entities_filter) ] entries = list(logbook.humanify(self.hass, events)) @@ -1333,63 +1339,6 @@ async def test_logbook_view_period_entity(hass, hass_client): assert json[0]["entity_id"] == entity_id_test -async def test_humanify_alexa_event(hass): - """Test humanifying Alexa event.""" - hass.states.async_set("light.kitchen", "on", {"friendly_name": "Kitchen Light"}) - - results = list( - logbook.humanify( - hass, - [ - ha.Event( - EVENT_ALEXA_SMART_HOME, - {"request": {"namespace": "Alexa.Discovery", "name": "Discover"}}, - ), - ha.Event( - EVENT_ALEXA_SMART_HOME, - { - "request": { - "namespace": "Alexa.PowerController", - "name": "TurnOn", - "entity_id": "light.kitchen", - } - }, - ), - ha.Event( - EVENT_ALEXA_SMART_HOME, - { - "request": { - "namespace": "Alexa.PowerController", - "name": "TurnOn", - "entity_id": "light.non_existing", - } - }, - ), - ], - ) - ) - - event1, event2, event3 = results - - assert event1["name"] == "Amazon Alexa" - assert event1["message"] == "send command Alexa.Discovery/Discover" - assert event1["entity_id"] is None - - assert event2["name"] == "Amazon Alexa" - assert ( - event2["message"] - == "send command Alexa.PowerController/TurnOn for Kitchen Light" - ) - assert event2["entity_id"] == "light.kitchen" - - assert event3["name"] == "Amazon Alexa" - assert ( - event3["message"] - == "send command Alexa.PowerController/TurnOn for light.non_existing" - ) - assert event3["entity_id"] == "light.non_existing" - - async def test_humanify_homekit_changed_event(hass): """Test humanifying HomeKit changed event.""" event1, event2 = list( @@ -1517,3 +1466,33 @@ async def test_humanify_same_state(hass): ) assert len(events) == 1 + + +async def test_logbook_describe_event(hass, hass_client): + """Test teaching logbook about a new event.""" + await hass.async_add_executor_job(init_recorder_component, hass) + assert await async_setup_component(hass, "logbook", {}) + with patch( + "homeassistant.util.dt.utcnow", + return_value=dt_util.utcnow() - timedelta(seconds=5), + ): + hass.bus.async_fire("some_event") + await hass.async_block_till_done() + await hass.async_add_executor_job( + hass.data[recorder.DATA_INSTANCE].block_till_done + ) + + def _describe(event): + """Describe an event.""" + return {"name": "Test Name", "message": "tested a message"} + + hass.components.logbook.async_describe_event("test_domain", "some_event", _describe) + + client = await hass_client() + response = await client.get("/api/logbook") + results = await response.json() + assert len(results) == 1 + event = results[0] + assert event["name"] == "Test Name" + assert event["message"] == "tested a message" + assert event["domain"] == "test_domain"