Skip to content

Commit

Permalink
Clean up Google Config (home-assistant#24663)
Browse files Browse the repository at this point in the history
* Clean up Google Config

* Lint

* pylint

* pylint2
  • Loading branch information
balloob authored and pvizeli committed Jun 21, 2019
1 parent d468d0f commit 78b7ed0
Show file tree
Hide file tree
Showing 12 changed files with 460 additions and 387 deletions.
244 changes: 244 additions & 0 deletions homeassistant/components/cloud/alexa_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,244 @@
"""Alexa configuration for Home Assistant Cloud."""
import asyncio
from datetime import timedelta
import logging

import aiohttp
import async_timeout
from hass_nabucasa import cloud_api

from homeassistant.const import CLOUD_NEVER_EXPOSED_ENTITIES
from homeassistant.helpers import entity_registry
from homeassistant.helpers.event import async_call_later
from homeassistant.util.dt import utcnow
from homeassistant.components.alexa import (
config as alexa_config,
errors as alexa_errors,
entities as alexa_entities,
state_report as alexa_state_report,
)


from .const import (
CONF_ENTITY_CONFIG, CONF_FILTER, PREF_SHOULD_EXPOSE, DEFAULT_SHOULD_EXPOSE,
RequireRelink
)

_LOGGER = logging.getLogger(__name__)

# Time to wait when entity preferences have changed before syncing it to
# the cloud.
SYNC_DELAY = 1


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

def __init__(self, hass, config, prefs, cloud):
"""Initialize the Alexa config."""
super().__init__(hass)
self._config = config
self._prefs = prefs
self._cloud = cloud
self._token = None
self._token_valid = None
self._cur_entity_prefs = prefs.alexa_entity_configs
self._alexa_sync_unsub = None
self._endpoint = None

prefs.async_listen_updates(self._async_prefs_updated)
hass.bus.async_listen(
entity_registry.EVENT_ENTITY_REGISTRY_UPDATED,
self._handle_entity_registry_updated
)

@property
def enabled(self):
"""Return if Alexa is enabled."""
return self._prefs.alexa_enabled

@property
def supports_auth(self):
"""Return if config supports auth."""
return True

@property
def should_report_state(self):
"""Return if states should be proactively reported."""
return self._prefs.alexa_report_state

@property
def endpoint(self):
"""Endpoint for report state."""
if self._endpoint is None:
raise ValueError("No endpoint available. Fetch access token first")

return self._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."""
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)

async def async_get_access_token(self):
"""Get an access token."""
if self._token_valid is not None and self._token_valid < utcnow():
return self._token

resp = await cloud_api.async_alexa_access_token(self._cloud)
body = await resp.json()

if resp.status == 400:
if body['reason'] in ('RefreshTokenNotFound', 'UnknownRegion'):
raise RequireRelink

raise alexa_errors.NoTokenAvailable

self._token = body['access_token']
self._endpoint = body['event_endpoint']
self._token_valid = utcnow() + timedelta(seconds=body['expires_in'])
return self._token

async def _async_prefs_updated(self, prefs):
"""Handle updated preferences."""
if self.should_report_state != self.is_reporting_states:
if self.should_report_state:
await self.async_enable_proactive_mode()
else:
await self.async_disable_proactive_mode()

# If entity prefs are the same or we have filter in config.yaml,
# don't sync.
if (self._cur_entity_prefs is prefs.alexa_entity_configs or
not self._config[CONF_FILTER].empty_filter):
return

if self._alexa_sync_unsub:
self._alexa_sync_unsub()

self._alexa_sync_unsub = async_call_later(
self.hass, SYNC_DELAY, self._sync_prefs)

async def _sync_prefs(self, _now):
"""Sync the updated preferences to Alexa."""
self._alexa_sync_unsub = None
old_prefs = self._cur_entity_prefs
new_prefs = self._prefs.alexa_entity_configs

seen = set()
to_update = []
to_remove = []

for entity_id, info in old_prefs.items():
seen.add(entity_id)
old_expose = info.get(PREF_SHOULD_EXPOSE)

if entity_id in new_prefs:
new_expose = new_prefs[entity_id].get(PREF_SHOULD_EXPOSE)
else:
new_expose = None

if old_expose == new_expose:
continue

if new_expose:
to_update.append(entity_id)
else:
to_remove.append(entity_id)

# Now all the ones that are in new prefs but never were in old prefs
for entity_id, info in new_prefs.items():
if entity_id in seen:
continue

new_expose = info.get(PREF_SHOULD_EXPOSE)

if new_expose is None:
continue

# Only test if we should expose. It can never be a remove action,
# as it didn't exist in old prefs object.
if new_expose:
to_update.append(entity_id)

# We only set the prefs when update is successful, that way we will
# retry when next change comes in.
if await self._sync_helper(to_update, to_remove):
self._cur_entity_prefs = new_prefs

async def async_sync_entities(self):
"""Sync all entities to Alexa."""
to_update = []
to_remove = []

for entity in alexa_entities.async_get_entities(self.hass, self):
if self.should_expose(entity.entity_id):
to_update.append(entity.entity_id)
else:
to_remove.append(entity.entity_id)

return await self._sync_helper(to_update, to_remove)

async def _sync_helper(self, to_update, to_remove) -> bool:
"""Sync entities to Alexa.
Return boolean if it was successful.
"""
if not to_update and not to_remove:
return True

tasks = []

if to_update:
tasks.append(alexa_state_report.async_send_add_or_update_message(
self.hass, self, to_update
))

if to_remove:
tasks.append(alexa_state_report.async_send_delete_message(
self.hass, self, to_remove
))

try:
with async_timeout.timeout(10):
await asyncio.wait(tasks, return_when=asyncio.ALL_COMPLETED)

return True

except asyncio.TimeoutError:
_LOGGER.warning("Timeout trying to sync entitites to Alexa")
return False

except aiohttp.ClientError as err:
_LOGGER.warning("Error trying to sync entities to Alexa: %s", err)
return False

async def _handle_entity_registry_updated(self, event):
"""Handle when entity registry updated."""
if not self.enabled or not self._cloud.is_logged_in:
return

action = event.data['action']
entity_id = event.data['entity_id']
to_update = []
to_remove = []

if action == 'create' and self.should_expose(entity_id):
to_update.append(entity_id)
elif action == 'remove' and self.should_expose(entity_id):
to_remove.append(entity_id)

await self._sync_helper(to_update, to_remove)
Loading

0 comments on commit 78b7ed0

Please sign in to comment.