mirror of
https://github.com/home-assistant/core.git
synced 2025-04-23 16:57:53 +00:00
Allow teaching logbook about events (#32444)
* Allow teaching logbook about events * Use async_add_executor_job * Fix tests
This commit is contained in:
parent
7c51318861
commit
8aea538662
@ -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)
|
||||
|
@ -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"
|
||||
|
@ -4,5 +4,6 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/alexa",
|
||||
"requirements": [],
|
||||
"dependencies": ["http"],
|
||||
"after_dependencies": ["logbook"],
|
||||
"codeowners": ["@home-assistant/cloud", "@ochlocracy"]
|
||||
}
|
||||
|
@ -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.
|
||||
|
@ -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"]
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
|
63
tests/components/alexa/test_init.py
Normal file
63
tests/components/alexa/test_init.py
Normal 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"
|
@ -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": {},
|
||||
},
|
||||
)
|
||||
)
|
||||
|
@ -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"
|
||||
|
Loading…
x
Reference in New Issue
Block a user