diff --git a/custom_components/alexa_media/__init__.py b/custom_components/alexa_media/__init__.py index bf48c7fe..8c309398 100644 --- a/custom_components/alexa_media/__init__.py +++ b/custom_components/alexa_media/__init__.py @@ -12,7 +12,8 @@ from typing import Optional, Text import voluptuous as vol -from alexapy import WebsocketEchoClient, hide_email, hide_serial +from alexapy import (AlexapyLoginError, WebsocketEchoClient, hide_email, + hide_serial) from homeassistant import util from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import (CONF_EMAIL, CONF_NAME, CONF_PASSWORD, @@ -134,6 +135,12 @@ async def close_alexa_media(event=None) -> None: login = AlexaLogin(url, email, password, hass.config.path, account.get(CONF_DEBUG)) (hass.data[DATA_ALEXAMEDIA]['accounts'][email]['login_obj']) = login + (hass.data[DATA_ALEXAMEDIA]['accounts'][email] + ['config_entry']) = config_entry + (hass.data[DATA_ALEXAMEDIA]['accounts'][email] + ['setup_platform_callback']) = setup_platform_callback + (hass.data[DATA_ALEXAMEDIA]['accounts'][email] + ['test_login_status']) = test_login_status await login.login_with_cookie() await test_login_status(hass, config_entry, login, setup_platform_callback) @@ -146,6 +153,7 @@ async def setup_platform_callback(hass, config_entry, login, callback_data): Args: callback_data (json): Returned data from configurator passed through request_configuration and configuration_callback + """ _LOGGER.debug(("Configurator closed for Status: %s\n" " got captcha: %s securitycode: %s" @@ -358,11 +366,11 @@ async def update_devices(login_obj): if ((devices is None or bluetooth is None) and not (hass.data[DATA_ALEXAMEDIA] ['accounts'][email]['configurator'])): - raise RuntimeError() - except RuntimeError: + raise AlexapyLoginError() + except (AlexapyLoginError, RuntimeError): _LOGGER.debug("%s: Alexa API disconnected; attempting to relogin", hide_email(email)) - await login_obj.login() + await login_obj.login_with_cookie() await test_login_status(hass, config_entry, login_obj, setup_platform_callback) diff --git a/custom_components/alexa_media/alarm_control_panel.py b/custom_components/alexa_media/alarm_control_panel.py index ab914025..c6b79d11 100644 --- a/custom_components/alexa_media/alarm_control_panel.py +++ b/custom_components/alexa_media/alarm_control_panel.py @@ -19,7 +19,7 @@ DATA_ALEXAMEDIA) from . import DOMAIN as ALEXA_DOMAIN from . import MIN_TIME_BETWEEN_FORCED_SCANS, MIN_TIME_BETWEEN_SCANS, hide_email -from .helpers import add_devices, retry_async +from .helpers import _catch_login_errors, add_devices, retry_async _LOGGER = logging.getLogger(__name__) @@ -165,6 +165,7 @@ def _handle_event(self, event): self.async_update(no_throttle=True))) @util.Throttle(MIN_TIME_BETWEEN_SCANS, MIN_TIME_BETWEEN_FORCED_SCANS) + @_catch_login_errors async def async_update(self): """Update Guard state.""" try: @@ -205,6 +206,7 @@ async def async_update(self): _LOGGER.debug("%s: Alarm State: %s", self.account, self.state) self.async_schedule_update_ha_state() + @_catch_login_errors async def async_alarm_disarm(self, code=None) -> None: # pylint: disable=unexpected-keyword-arg """Send disarm command. @@ -218,6 +220,7 @@ async def async_alarm_disarm(self, code=None) -> None: pass await self.async_alarm_arm_home() + @_catch_login_errors async def async_alarm_arm_home(self, code=None) -> None: """Send arm home command.""" try: @@ -231,6 +234,7 @@ async def async_alarm_arm_home(self, code=None) -> None: await self.async_update(no_throttle=True) self.async_schedule_update_ha_state() + @_catch_login_errors async def async_alarm_arm_away(self, code=None) -> None: """Send arm away command.""" # pylint: disable=unexpected-keyword-arg diff --git a/custom_components/alexa_media/helpers.py b/custom_components/alexa_media/helpers.py index 5a53e713..7d64475c 100644 --- a/custom_components/alexa_media/helpers.py +++ b/custom_components/alexa_media/helpers.py @@ -11,9 +11,12 @@ import logging from typing import Any, Callable, List, Text +from alexapy import AlexapyLoginError, hide_email from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity_component import EntityComponent +from . import DATA_ALEXAMEDIA + _LOGGER = logging.getLogger(__name__) @@ -114,18 +117,68 @@ async def wrapper(*args, **kwargs) -> Any: message = template.format(type(ex).__name__, ex.args) _LOGGER.debug( "%s.%s: failure caught due to exception: %s", - func.__module__[func.__module__.find('.')+1:], - func.__name__, - message) + func.__module__[func.__module__.find('.')+1:], + func.__name__, + message) _LOGGER.debug( "%s.%s: Try: %s/%s after waiting %s seconds result: %s", - func.__module__[func.__module__.find('.')+1:], - func.__name__, - retries, - limit, - next_try, - result - ) + func.__module__[func.__module__.find('.')+1:], + func.__name__, + retries, + limit, + next_try, + result + ) return result return wrapper return wrap + + +def _catch_login_errors(func) -> Callable: + """Detect AlexapyLoginError and attempt relogin.""" + import functools + @functools.wraps(func) + async def wrapper(*args, **kwargs) -> Any: + try: + result = await func(*args, **kwargs) + except AlexapyLoginError as ex: # pylint: disable=broad-except + template = ("An exception of type {0} occurred." + " Arguments:\n{1!r}") + message = template.format(type(ex).__name__, ex.args) + _LOGGER.debug("%s.%s: detected bad login: %s", + func.__module__[func.__module__.find('.')+1:], + func.__name__, + message) + instance = args[0] + if hasattr(instance, '_login'): + login = instance._login + email = login.email + hass = instance.hass if instance.hass else None + if (hass and not + (hass.data[DATA_ALEXAMEDIA]['accounts'][email] + ['configurator'])): + config_entry = ( + hass.data[DATA_ALEXAMEDIA] + ['accounts'] + [email] + ['config_entry']) + callback = ( + hass.data[DATA_ALEXAMEDIA] + ['accounts'] + [email] + ['setup_platform_callback']) + test_login_status = ( + hass.data[DATA_ALEXAMEDIA] + ['accounts'] + [email] + ['test_login_status']) + _LOGGER.debug( + "%s: Alexa API disconnected; attempting to relogin", + hide_email(email)) + await login.login_with_cookie() + await test_login_status(hass, + config_entry, login, + callback) + return None + return result + return wrapper diff --git a/custom_components/alexa_media/media_player.py b/custom_components/alexa_media/media_player.py index b4b75af2..5e5e8838 100644 --- a/custom_components/alexa_media/media_player.py +++ b/custom_components/alexa_media/media_player.py @@ -27,7 +27,7 @@ from . import (MIN_TIME_BETWEEN_FORCED_SCANS, MIN_TIME_BETWEEN_SCANS, hide_email, hide_serial) from .const import PLAY_SCAN_INTERVAL -from .helpers import add_devices, retry_async +from .helpers import _catch_login_errors, add_devices, retry_async SUPPORT_ALEXA = (SUPPORT_PAUSE | SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | SUPPORT_STOP | @@ -278,6 +278,7 @@ async def _set_authentication_details(self, auth): self._customer_name = auth['customerName'] @util.Throttle(MIN_TIME_BETWEEN_SCANS, MIN_TIME_BETWEEN_FORCED_SCANS) + @_catch_login_errors async def refresh(self, device=None): """Refresh device data. @@ -392,6 +393,7 @@ def source_list(self): """List of available input sources.""" return self._source_list + @_catch_login_errors async def async_select_source(self, source): """Select input source.""" if source == 'Local Speaker': @@ -606,6 +608,7 @@ def dnd_state(self, state): """Set the Do Not Disturb state.""" self._dnd = state + @_catch_login_errors async def async_set_shuffle(self, shuffle): """Enable/disable shuffle mode.""" await self.alexa_api.shuffle(shuffle) @@ -636,6 +639,7 @@ def supported_features(self): """Flag media player features that are supported.""" return SUPPORT_ALEXA + @_catch_login_errors async def async_set_volume_level(self, volume): """Set volume level, range 0..1.""" if not self.available: @@ -658,6 +662,7 @@ def is_volume_muted(self): return True return False + @_catch_login_errors async def async_mute_volume(self, mute): """Mute the volume. @@ -681,6 +686,7 @@ async def async_mute_volume(self, mute): ['accounts'][self._login.email]['websocket']): await self.async_update() + @_catch_login_errors async def async_media_play(self): """Send play command.""" if not (self.state in [STATE_PLAYING, STATE_PAUSED] @@ -691,6 +697,7 @@ async def async_media_play(self): ['accounts'][self._login.email]['websocket']): await self.async_update() + @_catch_login_errors async def async_media_pause(self): """Send pause command.""" if not (self.state in [STATE_PLAYING, STATE_PAUSED] @@ -701,6 +708,7 @@ async def async_media_pause(self): ['accounts'][self._login.email]['websocket']): await self.async_update() + @_catch_login_errors async def async_turn_off(self): """Turn the client off. @@ -711,6 +719,7 @@ async def async_turn_off(self): await self.async_media_pause() await self._clear_media_details() + @_catch_login_errors async def async_turn_on(self): """Turn the client on. @@ -720,6 +729,7 @@ async def async_turn_on(self): self._should_poll = True await self.async_media_pause() + @_catch_login_errors async def async_media_next_track(self): """Send next track command.""" if not (self.state in [STATE_PLAYING, STATE_PAUSED] @@ -730,6 +740,7 @@ async def async_media_next_track(self): ['accounts'][self._login.email]['websocket']): await self.async_update() + @_catch_login_errors async def async_media_previous_track(self): """Send previous track command.""" if not (self.state in [STATE_PLAYING, STATE_PAUSED] @@ -740,6 +751,7 @@ async def async_media_previous_track(self): ['accounts'][self._login.email]['websocket']): await self.async_update() + @_catch_login_errors async def async_send_tts(self, message): """Send TTS to Device. @@ -747,18 +759,21 @@ async def async_send_tts(self, message): """ await self.alexa_api.send_tts(message, customer_id=self._customer_id) + @_catch_login_errors async def async_send_announcement(self, message, **kwargs): """Send announcement to the media player.""" await self.alexa_api.send_announcement(message, customer_id=self._customer_id, **kwargs) + @_catch_login_errors async def async_send_mobilepush(self, message, **kwargs): """Send push to the media player's associated mobile devices.""" await self.alexa_api.send_mobilepush(message, customer_id=self._customer_id, **kwargs) + @_catch_login_errors async def async_play_media(self, media_type, media_id, enqueue=None, **kwargs): """Send the play_media command to the media player.""" diff --git a/custom_components/alexa_media/switch.py b/custom_components/alexa_media/switch.py index bfa85331..2141a158 100644 --- a/custom_components/alexa_media/switch.py +++ b/custom_components/alexa_media/switch.py @@ -17,7 +17,7 @@ DATA_ALEXAMEDIA) from . import DOMAIN as ALEXA_DOMAIN from . import (hide_email, hide_serial) -from .helpers import add_devices, retry_async +from .helpers import _catch_login_errors, add_devices, retry_async _LOGGER = logging.getLogger(__name__) @@ -124,6 +124,7 @@ def __init__(self, """Initialize the Alexa Switch device.""" # Class info self._client = client + self._login = client._login self._account = account self._name = name self._switch_property = switch_property @@ -165,6 +166,7 @@ def _handle_event(self, event): self._state = getattr(self._client, self._switch_property) self.async_schedule_update_ha_state() + @_catch_login_errors async def _set_switch(self, state, **kwargs): try: if not self.enabled: