Allow teaching logbook about events (#32444)

* Allow teaching logbook about events

* Use async_add_executor_job

* Fix tests
This commit is contained in:
Paulus Schoutsen 2020-03-05 11:55:50 -08:00 committed by GitHub
parent 7c51318861
commit 8aea538662
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 175 additions and 105 deletions

View File

@ -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)

View File

@ -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"

View File

@ -4,5 +4,6 @@
"documentation": "https://www.home-assistant.io/integrations/alexa",
"requirements": [],
"dependencies": ["http"],
"after_dependencies": ["logbook"],
"codeowners": ["@home-assistant/cloud", "@ochlocracy"]
}

View File

@ -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.

View File

@ -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"]
}

View File

@ -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

View File

@ -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()

View File

@ -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"

View File

@ -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": {},
},
)
)

View File

@ -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"