Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Move intent registration to own integration #29280

Merged
merged 2 commits into from Dec 1, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
12 changes: 9 additions & 3 deletions homeassistant/components/conversation/default_agent.py
Expand Up @@ -3,9 +3,12 @@
import re
from typing import Optional

from homeassistant import core
from homeassistant.components.cover import INTENT_CLOSE_COVER, INTENT_OPEN_COVER
from homeassistant.components.shopping_list import INTENT_ADD_ITEM, INTENT_LAST_ITEMS
from homeassistant import core, setup
from homeassistant.components.cover.intent import INTENT_CLOSE_COVER, INTENT_OPEN_COVER
from homeassistant.components.shopping_list.intent import (
INTENT_ADD_ITEM,
INTENT_LAST_ITEMS,
)
from homeassistant.const import EVENT_COMPONENT_LOADED
from homeassistant.core import callback
from homeassistant.helpers import intent
Expand Down Expand Up @@ -58,6 +61,9 @@ def __init__(self, hass: core.HomeAssistant):

async def async_initialize(self, config):
"""Initialize the default agent."""
if "intent" not in self.hass.config.components:
await setup.async_setup_component(self.hass, "intent", {})

config = config.get(DOMAIN, {})
intents = self.hass.data.setdefault(DOMAIN, {})

Expand Down
14 changes: 0 additions & 14 deletions homeassistant/components/cover/__init__.py
Expand Up @@ -15,7 +15,6 @@
)
from homeassistant.helpers.config_validation import ENTITY_SERVICE_SCHEMA
from homeassistant.components import group
from homeassistant.helpers import intent
from homeassistant.const import (
SERVICE_OPEN_COVER,
SERVICE_CLOSE_COVER,
Expand Down Expand Up @@ -83,8 +82,6 @@
ATTR_POSITION = "position"
ATTR_TILT_POSITION = "tilt_position"

INTENT_OPEN_COVER = "HassOpenCover"
INTENT_CLOSE_COVER = "HassCloseCover"

COVER_SET_COVER_POSITION_SCHEMA = ENTITY_SERVICE_SCHEMA.extend(
{vol.Required(ATTR_POSITION): vol.All(vol.Coerce(int), vol.Range(min=0, max=100))}
Expand Down Expand Up @@ -158,17 +155,6 @@ async def async_setup(hass, config):
SERVICE_TOGGLE_COVER_TILT, ENTITY_SERVICE_SCHEMA, "async_toggle_tilt"
)

hass.helpers.intent.async_register(
intent.ServiceIntentHandler(
INTENT_OPEN_COVER, DOMAIN, SERVICE_OPEN_COVER, "Opened {}"
)
)
hass.helpers.intent.async_register(
intent.ServiceIntentHandler(
INTENT_CLOSE_COVER, DOMAIN, SERVICE_CLOSE_COVER, "Closed {}"
)
)

return True


Expand Down
22 changes: 22 additions & 0 deletions homeassistant/components/cover/intent.py
@@ -0,0 +1,22 @@
"""Intents for the cover integration."""
from homeassistant.core import HomeAssistant
from homeassistant.helpers import intent

from . import DOMAIN, SERVICE_OPEN_COVER, SERVICE_CLOSE_COVER

INTENT_OPEN_COVER = "HassOpenCover"
INTENT_CLOSE_COVER = "HassCloseCover"


async def async_setup_intents(hass: HomeAssistant) -> None:
"""Set up the cover intents."""
hass.helpers.intent.async_register(
intent.ServiceIntentHandler(
INTENT_OPEN_COVER, DOMAIN, SERVICE_OPEN_COVER, "Opened {}"
)
)
hass.helpers.intent.async_register(
intent.ServiceIntentHandler(
INTENT_CLOSE_COVER, DOMAIN, SERVICE_CLOSE_COVER, "Closed {}"
)
)
33 changes: 33 additions & 0 deletions homeassistant/components/intent/__init__.py
@@ -1,22 +1,55 @@
"""The Intent integration."""
import asyncio
import logging

import voluptuous as vol

from homeassistant.core import HomeAssistant
from homeassistant.const import EVENT_COMPONENT_LOADED
from homeassistant.setup import ATTR_COMPONENT
from homeassistant.components import http
from homeassistant.components.http.data_validator import RequestDataValidator
from homeassistant.helpers import config_validation as cv, intent
from homeassistant.loader import async_get_integration, IntegrationNotFound

from .const import DOMAIN

CONFIG_SCHEMA = vol.Schema({DOMAIN: vol.Schema({})}, extra=vol.ALLOW_EXTRA)
_LOGGER = logging.getLogger(__name__)


async def async_setup(hass: HomeAssistant, config: dict):
"""Set up the Intent component."""
hass.http.register_view(IntentHandleView())

tasks = [_async_process_intent(hass, comp) for comp in hass.config.components]

async def async_component_loaded(event):
"""Handle a new component loaded."""
await _async_process_intent(hass, event.data[ATTR_COMPONENT])

hass.bus.async_listen(EVENT_COMPONENT_LOADED, async_component_loaded)

if tasks:
await asyncio.gather(*tasks)

return True


async def _async_process_intent(hass: HomeAssistant, component_name: str):
"""Process the intents of a component."""
try:
integration = await async_get_integration(hass, component_name)
platform = integration.get_platform(DOMAIN)
except (IntegrationNotFound, ImportError):
return

try:
await platform.async_setup_intents(hass)
except Exception: # pylint: disable=broad-except
_LOGGER.exception("Error setting up intents for %s", component_name)


class IntentHandleView(http.HomeAssistantView):
"""View to handle intents from JSON."""

Expand Down
64 changes: 0 additions & 64 deletions homeassistant/components/light/__init__.py
Expand Up @@ -26,7 +26,6 @@
)
from homeassistant.helpers.entity import ToggleEntity
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers import intent
from homeassistant.loader import bind_hass
import homeassistant.util.color as color_util

Expand Down Expand Up @@ -141,8 +140,6 @@
vol.ExactSequence((str, cv.small_float, cv.small_float, cv.byte))
)

INTENT_SET = "HassLightSet"

_LOGGER = logging.getLogger(__name__)


Expand Down Expand Up @@ -196,65 +193,6 @@ def preprocess_turn_off(params):
return (False, None) # Light should be turned on


class SetIntentHandler(intent.IntentHandler):
"""Handle set color intents."""

intent_type = INTENT_SET
slot_schema = {
vol.Required("name"): cv.string,
vol.Optional("color"): color_util.color_name_to_rgb,
vol.Optional("brightness"): vol.All(vol.Coerce(int), vol.Range(0, 100)),
}

async def async_handle(self, intent_obj):
"""Handle the hass intent."""
hass = intent_obj.hass
slots = self.async_validate_slots(intent_obj.slots)
state = hass.helpers.intent.async_match_state(
slots["name"]["value"],
[state for state in hass.states.async_all() if state.domain == DOMAIN],
)

service_data = {ATTR_ENTITY_ID: state.entity_id}
speech_parts = []

if "color" in slots:
intent.async_test_feature(state, SUPPORT_COLOR, "changing colors")
service_data[ATTR_RGB_COLOR] = slots["color"]["value"]
# Use original passed in value of the color because we don't have
# human readable names for that internally.
speech_parts.append(
"the color {}".format(intent_obj.slots["color"]["value"])
)

if "brightness" in slots:
intent.async_test_feature(state, SUPPORT_BRIGHTNESS, "changing brightness")
service_data[ATTR_BRIGHTNESS_PCT] = slots["brightness"]["value"]
speech_parts.append("{}% brightness".format(slots["brightness"]["value"]))

await hass.services.async_call(
DOMAIN, SERVICE_TURN_ON, service_data, context=intent_obj.context
)

response = intent_obj.create_response()

if not speech_parts: # No attributes changed
speech = f"Turned on {state.name}"
else:
parts = [f"Changed {state.name} to"]
for index, part in enumerate(speech_parts):
if index == 0:
parts.append(f" {part}")
elif index != len(speech_parts) - 1:
parts.append(f", {part}")
else:
parts.append(f" and {part}")
speech = "".join(parts)

response.async_set_speech(speech)
return response


async def async_setup(hass, config):
"""Expose light control via state machine and services."""
component = hass.data[DOMAIN] = EntityComponent(
Expand Down Expand Up @@ -341,8 +279,6 @@ async def async_handle_light_on_service(service):
SERVICE_TOGGLE, LIGHT_TOGGLE_SCHEMA, "async_toggle"
)

hass.helpers.intent.async_register(SetIntentHandler())

return True


Expand Down
84 changes: 84 additions & 0 deletions homeassistant/components/light/intent.py
@@ -0,0 +1,84 @@
"""Intents for the light integration."""
import voluptuous as vol

from homeassistant.core import HomeAssistant
from homeassistant.helpers import intent
import homeassistant.util.color as color_util
import homeassistant.helpers.config_validation as cv

from . import (
ATTR_ENTITY_ID,
SUPPORT_COLOR,
ATTR_RGB_COLOR,
ATTR_BRIGHTNESS_PCT,
SUPPORT_BRIGHTNESS,
DOMAIN,
SERVICE_TURN_ON,
)


INTENT_SET = "HassLightSet"


async def async_setup_intents(hass: HomeAssistant) -> None:
"""Set up the light intents."""
hass.helpers.intent.async_register(SetIntentHandler())


class SetIntentHandler(intent.IntentHandler):
"""Handle set color intents."""

intent_type = INTENT_SET
slot_schema = {
vol.Required("name"): cv.string,
vol.Optional("color"): color_util.color_name_to_rgb,
vol.Optional("brightness"): vol.All(vol.Coerce(int), vol.Range(0, 100)),
}

async def async_handle(self, intent_obj: intent.Intent) -> intent.IntentResponse:
"""Handle the hass intent."""
hass = intent_obj.hass
slots = self.async_validate_slots(intent_obj.slots)
state = hass.helpers.intent.async_match_state(
slots["name"]["value"],
[state for state in hass.states.async_all() if state.domain == DOMAIN],
)

service_data = {ATTR_ENTITY_ID: state.entity_id}
speech_parts = []

if "color" in slots:
intent.async_test_feature(state, SUPPORT_COLOR, "changing colors")
service_data[ATTR_RGB_COLOR] = slots["color"]["value"]
# Use original passed in value of the color because we don't have
# human readable names for that internally.
speech_parts.append(
"the color {}".format(intent_obj.slots["color"]["value"])
)

if "brightness" in slots:
intent.async_test_feature(state, SUPPORT_BRIGHTNESS, "changing brightness")
service_data[ATTR_BRIGHTNESS_PCT] = slots["brightness"]["value"]
speech_parts.append("{}% brightness".format(slots["brightness"]["value"]))

await hass.services.async_call(
DOMAIN, SERVICE_TURN_ON, service_data, context=intent_obj.context
)

response = intent_obj.create_response()

if not speech_parts: # No attributes changed
speech = f"Turned on {state.name}"
else:
parts = [f"Changed {state.name} to"]
for index, part in enumerate(speech_parts):
if index == 0:
parts.append(f" {part}")
elif index != len(speech_parts) - 1:
parts.append(f", {part}")
else:
parts.append(f" and {part}")
speech = "".join(parts)

response.async_set_speech(speech)
return response
49 changes: 0 additions & 49 deletions homeassistant/components/shopping_list/__init__.py
Expand Up @@ -9,7 +9,6 @@
from homeassistant.core import callback
from homeassistant.components import http
from homeassistant.components.http.data_validator import RequestDataValidator
from homeassistant.helpers import intent
import homeassistant.helpers.config_validation as cv
from homeassistant.util.json import load_json, save_json
from homeassistant.components import websocket_api
Expand All @@ -20,8 +19,6 @@
_LOGGER = logging.getLogger(__name__)
CONFIG_SCHEMA = vol.Schema({DOMAIN: {}}, extra=vol.ALLOW_EXTRA)
EVENT = "shopping_list_updated"
INTENT_ADD_ITEM = "HassShoppingListAddItem"
INTENT_LAST_ITEMS = "HassShoppingListLastItems"
ITEM_UPDATE_SCHEMA = vol.Schema({"complete": bool, ATTR_NAME: str})
PERSISTENCE = ".shopping_list.json"

Expand Down Expand Up @@ -86,9 +83,6 @@ def complete_item_service(call):
data = hass.data[DOMAIN] = ShoppingData(hass)
yield from data.async_load()

intent.async_register(hass, AddItemIntent())
intent.async_register(hass, ListTopItemsIntent())

hass.services.async_register(
DOMAIN, SERVICE_ADD_ITEM, add_item_service, schema=SERVICE_ITEM_SCHEMA
)
Expand Down Expand Up @@ -175,49 +169,6 @@ def save(self):
save_json(self.hass.config.path(PERSISTENCE), self.items)


class AddItemIntent(intent.IntentHandler):
"""Handle AddItem intents."""

intent_type = INTENT_ADD_ITEM
slot_schema = {"item": cv.string}

@asyncio.coroutine
def async_handle(self, intent_obj):
"""Handle the intent."""
slots = self.async_validate_slots(intent_obj.slots)
item = slots["item"]["value"]
intent_obj.hass.data[DOMAIN].async_add(item)

response = intent_obj.create_response()
response.async_set_speech(f"I've added {item} to your shopping list")
intent_obj.hass.bus.async_fire(EVENT)
return response


class ListTopItemsIntent(intent.IntentHandler):
"""Handle AddItem intents."""

intent_type = INTENT_LAST_ITEMS
slot_schema = {"item": cv.string}

@asyncio.coroutine
def async_handle(self, intent_obj):
"""Handle the intent."""
items = intent_obj.hass.data[DOMAIN].items[-5:]
response = intent_obj.create_response()

if not items:
response.async_set_speech("There are no items on your shopping list")
else:
response.async_set_speech(
"These are the top {} items on your shopping list: {}".format(
min(len(items), 5),
", ".join(itm["name"] for itm in reversed(items)),
)
)
return response


class ShoppingListView(http.HomeAssistantView):
"""View to retrieve shopping list content."""

Expand Down