Skip to content

Commit

Permalink
Allow teaching logbook about events (home-assistant#32444)
Browse files Browse the repository at this point in the history
* Allow teaching logbook about events

* Use async_add_executor_job

* Fix tests
  • Loading branch information
balloob authored Mar 5, 2020
1 parent 7c51318 commit 8aea538
Show file tree
Hide file tree
Showing 10 changed files with 175 additions and 105 deletions.
34 changes: 33 additions & 1 deletion homeassistant/components/alexa/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
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
Expand All @@ -23,6 +24,7 @@
CONF_TITLE,
CONF_UID,
DOMAIN,
EVENT_ALEXA_SMART_HOME,
)

_LOGGER = logging.getLogger(__name__)
Expand Down Expand Up @@ -80,7 +82,37 @@

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)
Expand Down
1 change: 1 addition & 0 deletions homeassistant/components/alexa/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT

DOMAIN = "alexa"
EVENT_ALEXA_SMART_HOME = "alexa_smart_home"

# Flash briefing constants
CONF_UID = "uid"
Expand Down
1 change: 1 addition & 0 deletions homeassistant/components/alexa/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@
"documentation": "https://www.home-assistant.io/integrations/alexa",
"requirements": [],
"dependencies": ["http"],
"after_dependencies": ["logbook"],
"codeowners": ["@home-assistant/cloud", "@ochlocracy"]
}
4 changes: 1 addition & 3 deletions homeassistant/components/alexa/smart_home.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,13 @@

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.
Expand Down
4 changes: 2 additions & 2 deletions homeassistant/components/cloud/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
}
51 changes: 23 additions & 28 deletions homeassistant/components/logbook/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
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,
Expand Down Expand Up @@ -90,7 +89,6 @@
EVENT_LOGBOOK_ENTRY,
EVENT_HOMEASSISTANT_START,
EVENT_HOMEASSISTANT_STOP,
EVENT_ALEXA_SMART_HOME,
EVENT_HOMEKIT_CHANGED,
EVENT_AUTOMATION_TRIGGERED,
EVENT_SCRIPT_STARTED,
Expand Down Expand Up @@ -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."""

Expand Down Expand Up @@ -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"))

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -436,7 +429,7 @@ def yield_events(query):
"""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:
Expand All @@ -449,7 +442,9 @@ def yield_events(query):
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(
(
Expand All @@ -463,7 +458,7 @@ def yield_events(query):
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:
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/scripts/benchmark/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ def yield_events(event):
# 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()
Expand Down
63 changes: 63 additions & 0 deletions tests/components/alexa/test_init.py
Original file line number Diff line number Diff line change
@@ -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"
3 changes: 2 additions & 1 deletion tests/components/alexa/test_intent.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@ def mock_service(call):
alexa.DOMAIN,
{
# Key is here to verify we allow other keys in config too
"homeassistant": {}
"homeassistant": {},
"alexa": {},
},
)
)
Expand Down
Loading

0 comments on commit 8aea538

Please sign in to comment.