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

Cloud: allow managing Alexa entities via UI #24522

Merged
merged 3 commits into from
Jun 13, 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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 28 additions & 8 deletions homeassistant/components/alexa/config.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,33 @@
"""Config helpers for Alexa."""


class Config:
class AbstractConfig:
"""Hold the configuration for Alexa."""

def __init__(self, endpoint, async_get_access_token, should_expose,
entity_config=None):
"""Initialize the configuration."""
self.endpoint = endpoint
self.async_get_access_token = async_get_access_token
self.should_expose = should_expose
self.entity_config = entity_config or {}
@property
def supports_auth(self):
"""Return if config supports auth."""
return False

@property
def endpoint(self):
"""Endpoint for report state."""
return None

@property
def entity_config(self):
"""Return entity config."""
return {}

def should_expose(self, entity_id):
"""If an entity should be exposed."""
# pylint: disable=no-self-use
return False

async def async_get_access_token(self):
"""Get an access token."""
raise NotImplementedError

async def async_accept_grant(self, code):
"""Accept a grant."""
raise NotImplementedError
2 changes: 0 additions & 2 deletions homeassistant/components/alexa/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,6 @@
CONF_DESCRIPTION = 'description'
CONF_DISPLAY_CATEGORIES = 'display_categories'

AUTH_KEY = "alexa.smart_home.auth"

API_TEMP_UNITS = {
TEMP_FAHRENHEIT: 'FAHRENHEIT',
TEMP_CELSIUS: 'CELSIUS',
Expand Down
5 changes: 2 additions & 3 deletions homeassistant/components/alexa/handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@
from homeassistant.util.temperature import convert as convert_temperature

from .const import (
AUTH_KEY,
API_TEMP_UNITS,
API_THERMOSTAT_MODES,
Cause,
Expand Down Expand Up @@ -86,8 +85,8 @@ async def async_api_accept_grant(hass, config, directive, context):
auth_code = directive.payload['grant']['code']
_LOGGER.debug("AcceptGrant code: %s", auth_code)

if AUTH_KEY in hass.data:
await hass.data[AUTH_KEY].async_do_auth(auth_code)
if config.supports_auth:
await config.async_accept_grant(auth_code)
await async_enable_proactive_mode(hass, config)

return directive.response(
Expand Down
61 changes: 44 additions & 17 deletions homeassistant/components/alexa/smart_home_http.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,8 @@
from homeassistant.components.http.view import HomeAssistantView

from .auth import Auth
from .config import Config
from .config import AbstractConfig
from .const import (
AUTH_KEY,
CONF_CLIENT_ID,
CONF_CLIENT_SECRET,
CONF_ENDPOINT,
Expand All @@ -21,6 +20,47 @@
SMART_HOME_HTTP_ENDPOINT = '/api/alexa/smart_home'


class AlexaConfig(AbstractConfig):
"""Alexa config."""

def __init__(self, hass, config):
"""Initialize Alexa config."""
self._config = config

if config.get(CONF_CLIENT_ID) and config.get(CONF_CLIENT_SECRET):
self._auth = Auth(hass, config[CONF_CLIENT_ID],
config[CONF_CLIENT_SECRET])
else:
self._auth = None

@property
def supports_auth(self):
"""Return if config supports auth."""
return self._auth is not None

@property
def endpoint(self):
"""Endpoint for report state."""
return self._config.get(CONF_ENDPOINT)

@property
def entity_config(self):
"""Return entity config."""
return self._config.get(CONF_ENTITY_CONFIG, {})

def should_expose(self, entity_id):
"""If an entity should be exposed."""
return self._config[CONF_FILTER](entity_id)

async def async_get_access_token(self):
"""Get an access token."""
return await self._auth.async_get_access_token()

async def async_accept_grant(self, code):
"""Accept a grant."""
return await self._auth.async_do_auth(code)


async def async_setup(hass, config):
"""Activate Smart Home functionality of Alexa component.

Expand All @@ -30,23 +70,10 @@ async def async_setup(hass, config):
Even if that's disabled, the functionality in this module may still be used
by the cloud component which will call async_handle_message directly.
"""
if config.get(CONF_CLIENT_ID) and config.get(CONF_CLIENT_SECRET):
hass.data[AUTH_KEY] = Auth(hass, config[CONF_CLIENT_ID],
config[CONF_CLIENT_SECRET])

async_get_access_token = \
hass.data[AUTH_KEY].async_get_access_token if AUTH_KEY in hass.data \
else None

smart_home_config = Config(
endpoint=config.get(CONF_ENDPOINT),
async_get_access_token=async_get_access_token,
should_expose=config[CONF_FILTER],
entity_config=config.get(CONF_ENTITY_CONFIG),
)
smart_home_config = AlexaConfig(hass, config)
hass.http.register_view(SmartHomeView(smart_home_config))

if AUTH_KEY in hass.data:
if smart_home_config.supports_auth:
await async_enable_proactive_mode(hass, smart_home_config)


Expand Down
1 change: 0 additions & 1 deletion homeassistant/components/cloud/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,6 @@
DOMAIN: vol.Schema({
vol.Optional(CONF_MODE, default=DEFAULT_MODE):
vol.In([MODE_DEV, MODE_PROD]),
# Change to optional when we include real servers
vol.Optional(CONF_COGNITO_CLIENT_ID): str,
vol.Optional(CONF_USER_POOL_ID): str,
vol.Optional(CONF_REGION): str,
Expand Down
61 changes: 36 additions & 25 deletions homeassistant/components/cloud/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,38 @@
from .prefs import CloudPreferences


class AlexaConfig(alexa_config.AbstractConfig):
"""Alexa Configuration."""

def __init__(self, config, prefs):
"""Initialize the Alexa config."""
self._config = config
self._prefs = prefs

@property
def endpoint(self):
"""Endpoint for report state."""
return None

@property
def entity_config(self):
"""Return entity config."""
return self._config.get(CONF_ENTITY_CONFIG, {})

def should_expose(self, entity_id):
"""If an entity should be exposed."""
if entity_id in CLOUD_NEVER_EXPOSED_ENTITIES:
return False

if not self._config[CONF_FILTER].empty_filter:
return self._config[CONF_FILTER](entity_id)

entity_configs = self._prefs.alexa_entity_configs
entity_config = entity_configs.get(entity_id, {})
return entity_config.get(
PREF_SHOULD_EXPOSE, DEFAULT_SHOULD_EXPOSE)


class CloudClient(Interface):
"""Interface class for Home Assistant Cloud."""

Expand All @@ -36,10 +68,10 @@ def __init__(self, hass: HomeAssistantType, prefs: CloudPreferences,
self._hass = hass
self._prefs = prefs
self._websession = websession
self._alexa_user_config = alexa_cfg
self._google_user_config = google_config
self.google_user_config = google_config
self.alexa_user_config = alexa_cfg

self._alexa_config = None
self.alexa_config = AlexaConfig(alexa_cfg, prefs)
self._google_config = None

@property
Expand Down Expand Up @@ -77,26 +109,11 @@ def remote_autostart(self) -> bool:
"""Return true if we want start a remote connection."""
return self._prefs.remote_enabled

@property
def alexa_config(self) -> alexa_config.Config:
"""Return Alexa config."""
if not self._alexa_config:
alexa_conf = self._alexa_user_config

self._alexa_config = alexa_config.Config(
endpoint=None,
async_get_access_token=None,
should_expose=alexa_conf[CONF_FILTER],
entity_config=alexa_conf.get(CONF_ENTITY_CONFIG),
)

return self._alexa_config

@property
def google_config(self) -> ga_h.Config:
"""Return Google config."""
if not self._google_config:
google_conf = self._google_user_config
google_conf = self.google_user_config

def should_expose(entity):
"""If an entity should be exposed."""
Expand Down Expand Up @@ -134,14 +151,8 @@ def should_2fa(entity):

return self._google_config

@property
def google_user_config(self) -> Dict[str, Any]:
"""Return google action user config."""
return self._google_user_config

async def cleanups(self) -> None:
"""Cleanup some stuff after logout."""
self._alexa_config = None
self._google_config = None

@callback
Expand Down
1 change: 1 addition & 0 deletions homeassistant/components/cloud/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
PREF_CLOUDHOOKS = 'cloudhooks'
PREF_CLOUD_USER = 'cloud_user'
PREF_GOOGLE_ENTITY_CONFIGS = 'google_entity_configs'
PREF_ALEXA_ENTITY_CONFIGS = 'alexa_entity_configs'
PREF_OVERRIDE_NAME = 'override_name'
PREF_DISABLE_2FA = 'disable_2fa'
PREF_ALIASES = 'aliases'
Expand Down
54 changes: 53 additions & 1 deletion homeassistant/components/cloud/http_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,9 @@ async def async_setup(hass):
hass.components.websocket_api.async_register_command(
google_assistant_update)

hass.components.websocket_api.async_register_command(alexa_list)
hass.components.websocket_api.async_register_command(alexa_update)

hass.http.register_view(GoogleActionsSyncView)
hass.http.register_view(CloudLoginView)
hass.http.register_view(CloudLogoutView)
Expand Down Expand Up @@ -420,7 +423,7 @@ def _account_data(cloud):
'cloud': cloud.iot.state,
'prefs': client.prefs.as_dict(),
'google_entities': client.google_user_config['filter'].config,
'alexa_entities': client.alexa_config.should_expose.config,
'alexa_entities': client.alexa_user_config['filter'].config,
'alexa_domains': list(alexa_entities.ENTITY_ADAPTERS),
'remote_domain': remote.instance_domain,
'remote_connected': remote.is_connected,
Expand Down Expand Up @@ -508,3 +511,52 @@ async def google_assistant_update(hass, connection, msg):
connection.send_result(
msg['id'],
cloud.client.prefs.google_entity_configs.get(msg['entity_id']))


@websocket_api.require_admin
@_require_cloud_login
@websocket_api.async_response
@_ws_handle_cloud_errors
@websocket_api.websocket_command({
'type': 'cloud/alexa/entities'
})
async def alexa_list(hass, connection, msg):
"""List all alexa entities."""
cloud = hass.data[DOMAIN]
entities = alexa_entities.async_get_entities(
hass, cloud.client.alexa_config
)

result = []

for entity in entities:
result.append({
'entity_id': entity.entity_id,
'display_categories': entity.default_display_categories(),
'interfaces': [ifc.name() for ifc in entity.interfaces()],
})

connection.send_result(msg['id'], result)


@websocket_api.require_admin
@_require_cloud_login
@websocket_api.async_response
@_ws_handle_cloud_errors
@websocket_api.websocket_command({
'type': 'cloud/alexa/entities/update',
'entity_id': str,
vol.Optional('should_expose'): bool,
})
async def alexa_update(hass, connection, msg):
"""Update alexa entity config."""
cloud = hass.data[DOMAIN]
changes = dict(msg)
changes.pop('type')
changes.pop('id')

await cloud.client.prefs.async_update_alexa_entity_config(**changes)

connection.send_result(
msg['id'],
cloud.client.prefs.alexa_entity_configs.get(msg['entity_id']))
Loading