Skip to content

Commit

Permalink
feat: add force_logout service
Browse files Browse the repository at this point in the history
This is intended for debugging use but can be used to simulate a
Amazon disconnect. This will delete any .pickle files as a side effect.
  • Loading branch information
alandtse committed Sep 6, 2020
1 parent 6d8a2a5 commit fe057fb
Show file tree
Hide file tree
Showing 4 changed files with 234 additions and 102 deletions.
137 changes: 38 additions & 99 deletions custom_components/alexa_media/__init__.py
Expand Up @@ -11,7 +11,6 @@
from datetime import datetime, timedelta
from json import JSONDecodeError
import logging
from sys import call_tracing
import time
from typing import Optional, Text

Expand All @@ -23,33 +22,27 @@
__version__ as alexapy_version,
hide_email,
hide_serial,
obfuscate,
)
from alexapy.errors import AlexapyConnectionError
import async_timeout
from homeassistant import util
from homeassistant.config_entries import SOURCE_IMPORT
from homeassistant.const import (
CONF_EMAIL,
CONF_NAME,
CONF_PASSWORD,
CONF_SCAN_INTERVAL,
CONF_URL,
EVENT_HOMEASSISTANT_STOP,
)
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import config_validation as cv, entity
from homeassistant.helpers.discovery import async_load_platform
from homeassistant.data_entry_flow import UnknownFlow
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.helpers.event import async_call_later
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from homeassistant.util import dt
import voluptuous as vol

from .config_flow import configured_instances
from .config_flow import in_progess_instances
from .const import (
ALEXA_COMPONENTS,
ATTR_EMAIL,
ATTR_NUM_ENTRIES,
CONF_ACCOUNTS,
CONF_DEBUG,
CONF_EXCLUDE_DEVICES,
Expand All @@ -60,15 +53,12 @@
DEFAULT_QUEUE_DELAY,
DOMAIN,
ISSUE_URL,
MIN_TIME_BETWEEN_FORCED_SCANS,
MIN_TIME_BETWEEN_SCANS,
SCAN_INTERVAL,
SERVICE_CLEAR_HISTORY,
SERVICE_UPDATE_LAST_CALLED,
STARTUP,
)
from .helpers import _catch_login_errors, _existing_serials, report_relogin_required
from .helpers import _existing_serials
from .notify import async_unload_entry as notify_async_unload_entry
from .services import AlexaMediaServices

_LOGGER = logging.getLogger(__name__)

Expand Down Expand Up @@ -102,19 +92,6 @@
extra=vol.ALLOW_EXTRA,
)

CLEAR_HISTORY_SCHEMA = vol.Schema(
{
vol.Optional(ATTR_EMAIL, default=[]): vol.All(cv.ensure_list, [cv.string]),
vol.Optional(ATTR_NUM_ENTRIES, default=50): vol.All(
int, vol.Range(min=1, max=50)
),
}
)

LAST_CALL_UPDATE_SCHEMA = vol.Schema(
{vol.Optional(ATTR_EMAIL, default=[]): vol.All(cv.ensure_list, [cv.string])}
)


async def async_setup(hass, config, discovery_info=None):
# pylint: disable=unused-argument
Expand Down Expand Up @@ -568,65 +545,6 @@ async def update_bluetooth_state(login_obj, device_serial):
)
return None

@_catch_login_errors
async def clear_history(call):
"""Handle clear history service request.
Arguments
call.ATTR_EMAIL {List[str: None]} -- Case-sensitive Alexa emails.
Default is all known emails.
call.ATTR_NUM_ENTRIES {int: 50} -- Number of entries to delete.
Returns
bool -- True if deletion successful
"""
requested_emails = call.data.get(ATTR_EMAIL)
items: int = int(call.data.get(ATTR_NUM_ENTRIES))

_LOGGER.debug(
"Service clear_history called for: %i items for %s", items, requested_emails
)
for email, account_dict in hass.data[DATA_ALEXAMEDIA]["accounts"].items():
if requested_emails and email not in requested_emails:
continue
login_obj = account_dict["login_obj"]
try:
await AlexaAPI.clear_history(login_obj, items)
except AlexapyLoginError:
report_relogin_required(hass, login_obj, email)
except AlexapyConnectionError:
_LOGGER.error(
"Unable to connect to Alexa for %s;"
" check your network connection and try again",
hide_email(email),
)

async def last_call_handler(call):
"""Handle last call service request.
Args:
call.ATTR_EMAIL: List of case-sensitive Alexa email addresses. If None
all accounts are updated.
"""
requested_emails = call.data.get(ATTR_EMAIL)
_LOGGER.debug("Service update_last_called for: %s", requested_emails)
for email, account_dict in hass.data[DATA_ALEXAMEDIA]["accounts"].items():
if requested_emails and email not in requested_emails:
continue
login_obj = account_dict["login_obj"]
try:
await update_last_called(login_obj)
except AlexapyLoginError:
report_relogin_required(hass, login_obj, email)
except AlexapyConnectionError:
_LOGGER.error(
"Unable to connect to Alexa for %s;"
" check your network connection and try again",
hide_email(email),
)

async def ws_connect() -> WebsocketEchoClient:
"""Open WebSocket connection.
Expand Down Expand Up @@ -969,25 +887,17 @@ async def ws_error_handler(message):
await coordinator.async_refresh()

coordinator.async_add_listener(lambda: None)

hass.services.async_register(
DOMAIN,
SERVICE_UPDATE_LAST_CALLED,
last_call_handler,
schema=LAST_CALL_UPDATE_SCHEMA,
)
hass.services.async_register(
DOMAIN, SERVICE_CLEAR_HISTORY, clear_history, schema=CLEAR_HISTORY_SCHEMA
hass.data[DATA_ALEXAMEDIA]["services"] = alexa_services = AlexaMediaServices(
hass, functions={"update_last_called": update_last_called}
)
await alexa_services.register()
return True


async def async_unload_entry(hass, entry) -> bool:
"""Unload a config entry."""
email = entry.data["email"]
_LOGGER.debug("Attempting to unload entry for %s", hide_email(email))
hass.services.async_remove(DOMAIN, SERVICE_UPDATE_LAST_CALLED)
hass.services.async_remove(DOMAIN, SERVICE_CLEAR_HISTORY)
for component in ALEXA_COMPONENTS:
await hass.config_entries.async_forward_entry_unload(entry, component)
# notify has to be handled manually as the forward does not work yet
Expand All @@ -996,6 +906,35 @@ async def async_unload_entry(hass, entry) -> bool:
for listener in hass.data[DATA_ALEXAMEDIA]["accounts"][email][DATA_LISTENER]:
listener()
hass.data[DATA_ALEXAMEDIA]["accounts"].pop(email)
# Clean up config flows in progress
flows_to_remove = []
for key, flow in hass.data[DATA_ALEXAMEDIA]["config_flows"].items():
if key.startswith(email):
_LOGGER.debug("Aborting flow %s %s", key, flow)
flows_to_remove.append(key)
try:
hass.config_entries.flow.async_abort(flow.get("flow_id"))
except UnknownFlow:
pass
for flow in flows_to_remove:
hass.data[DATA_ALEXAMEDIA]["config_flows"].pop(flow)
# Clean up hass.data
if not hass.data[DATA_ALEXAMEDIA]["accounts"]:
_LOGGER.debug("Removing accounts data and services")
hass.data[DATA_ALEXAMEDIA].pop("accounts")
alexa_services = hass.data[DATA_ALEXAMEDIA].get("services")
if alexa_services:
await alexa_services.unregister()
hass.data[DATA_ALEXAMEDIA].pop("services")
if not hass.data[DATA_ALEXAMEDIA]["config_flows"]:
_LOGGER.debug("Removing config_flows data")
hass.components.persistent_notification.async_dismiss(
"alexa_media_player_relogin_required"
)
hass.data[DATA_ALEXAMEDIA].pop("config_flows")
if not hass.data[DATA_ALEXAMEDIA]:
_LOGGER.debug("Removing alexa_media data structure")
hass.data.pop(DATA_ALEXAMEDIA)
_LOGGER.debug("Unloaded entry for %s", hide_email(email))
return True

Expand Down
1 change: 1 addition & 0 deletions custom_components/alexa_media/const.py
Expand Up @@ -39,6 +39,7 @@
DEFAULT_QUEUE_DELAY = 1.5
SERVICE_CLEAR_HISTORY = "clear_history"
SERVICE_UPDATE_LAST_CALLED = "update_last_called"
SERVICE_FORCE_LOGOUT = "force_logout"

RECURRING_PATTERN = {
None: "Never Repeat",
Expand Down

0 comments on commit fe057fb

Please sign in to comment.