async def ws_close_handler(): """Handle websocket close. This should attempt to reconnect up to 5 times """ from asyncio import sleep import time email: Text = login_obj.email errors: int = (hass.data[DATA_ALEXAMEDIA]["accounts"][email]["websocketerror"]) delay: int = 5 * 2 ** errors last_attempt = hass.data[DATA_ALEXAMEDIA]["accounts"][email][ "websocket_lastattempt" ] now = time.time() if (now - last_attempt) < delay: return while errors < 5 and not ( hass.data[DATA_ALEXAMEDIA]["accounts"][email]["websocket"] ): _LOGGER.debug( "%s: Websocket closed; reconnect #%i in %is", hide_email(email), errors, delay, ) hass.data[DATA_ALEXAMEDIA]["accounts"][email][ "websocket_lastattempt" ] = time.time() hass.data[DATA_ALEXAMEDIA]["accounts"][email][ "websocket" ] = await ws_connect() errors = hass.data[DATA_ALEXAMEDIA]["accounts"][email]["websocketerror"] = ( hass.data[DATA_ALEXAMEDIA]["accounts"][email]["websocketerror"] + 1 ) delay = 5 * 2 ** errors await sleep(delay) errors = hass.data[DATA_ALEXAMEDIA]["accounts"][email]["websocketerror"] _LOGGER.debug( "%s: Websocket closed; retries exceeded; polling", hide_email(email) ) hass.data[DATA_ALEXAMEDIA]["accounts"][email]["websocket"] = None await update_devices( # pylint: disable=unexpected-keyword-arg login_obj, no_throttle=True )
async def _catch_login_errors(func, instance, args, kwargs) -> Any: """Detect AlexapyLoginError and attempt relogin.""" result = None if instance is None and args: instance = args[0] if hasattr(instance, "check_login_changes"): # _LOGGER.debug( # "%s checking for login changes", instance, # ) instance.check_login_changes() try: result = await func(*args, **kwargs) except AlexapyLoginCloseRequested: _LOGGER.debug( "%s.%s: Ignoring attempt to access Alexa after HA shutdown", func.__module__[func.__module__.find(".") + 1:], func.__name__, ) return None except AlexapyLoginError as ex: login = None email = None all_args = list(args) + list(kwargs.values()) # _LOGGER.debug("Func %s instance %s %s %s", func, instance, args, kwargs) if instance: if hasattr(instance, "_login"): login = instance._login hass = instance.hass else: for arg in all_args: _LOGGER.debug("Checking %s", arg) if isinstance(arg, AlexaLogin): login = arg break if hasattr(arg, "_login"): login = instance._login hass = instance.hass break if login: email = login.email _LOGGER.debug( "%s.%s: detected bad login for %s: %s", func.__module__[func.__module__.find(".") + 1:], func.__name__, hide_email(email), EXCEPTION_TEMPLATE.format(type(ex).__name__, ex.args), ) try: hass except NameError: hass = None report_relogin_required(hass, login, email) return None return result
async def async_step_check_proxy(self, user_input=None): """Check status of proxy for login.""" _LOGGER.debug( "Checking proxy response for %s - %s", hide_email(self.login.email), self.login.url, ) self.proxy_view.reset() return self.async_external_step_done(next_step_id="finish_proxy")
async def ws_open_handler(): """Handle websocket open.""" import time email: Text = login_obj.email _LOGGER.debug("%s: Websocket succesfully connected", hide_email(email)) (hass.data[DATA_ALEXAMEDIA]['accounts'][email]['websocketerror'] ) = 0 # set errors to 0 (hass.data[DATA_ALEXAMEDIA]['accounts'][email]['websocket_lastattempt'] ) = time.time()
async def ws_open_handler(): """Handle websocket open.""" email: Text = login_obj.email _LOGGER.debug("%s: Websocket succesfully connected", hide_email(email)) hass.data[DATA_ALEXAMEDIA]["accounts"][email][ "websocketerror"] = 0 # set errors to 0 hass.data[DATA_ALEXAMEDIA]["accounts"][email][ "websocket_lastattempt"] = time.time()
async def ws_connect() -> WebsocketEchoClient: """Open WebSocket connection. This will only attempt one login before failing. """ websocket: Optional[WebsocketEchoClient] = None try: websocket = WebsocketEchoClient(login_obj, ws_handler, ws_open_handler, ws_close_handler, ws_error_handler) _LOGGER.debug("%s: Websocket created: %s", hide_email(email), websocket) await websocket.async_run() except BaseException as exception_: _LOGGER.debug("%s: Websocket creation failed: %s", hide_email(email), exception_) return return websocket
def __init__(self, device, login) -> None: # pylint: disable=unexpected-keyword-arg """Initialize the Alexa device.""" # Class info self._login = login self.alexa_api = AlexaAPI(device, login) self.email = login.email self.account = hide_email(login.email)
async def ws_close_handler(): """Handle websocket close. This should attempt to reconnect up to 5 times """ from asyncio import sleep import time email: Text = login_obj.email errors: int = ( hass.data[DATA_ALEXAMEDIA]["accounts"][email]["websocketerror"]) delay: int = 5 * 2**errors last_attempt = hass.data[DATA_ALEXAMEDIA]["accounts"][email][ "websocket_lastattempt"] now = time.time() if (now - last_attempt) < delay: return while errors < 5 and not ( hass.data[DATA_ALEXAMEDIA]["accounts"][email]["websocket"]): _LOGGER.debug( "%s: Websocket closed; reconnect #%i in %is", hide_email(email), errors, delay, ) hass.data[DATA_ALEXAMEDIA]["accounts"][email][ "websocket_lastattempt"] = time.time() hass.data[DATA_ALEXAMEDIA]["accounts"][email][ "websocket"] = await ws_connect() errors = hass.data[DATA_ALEXAMEDIA]["accounts"][email][ "websocketerror"] = (hass.data[DATA_ALEXAMEDIA]["accounts"] [email]["websocketerror"] + 1) delay = 5 * 2**errors await sleep(delay) errors = hass.data[DATA_ALEXAMEDIA]["accounts"][email][ "websocketerror"] _LOGGER.debug("%s: Websocket closed; retries exceeded; polling", hide_email(email)) hass.data[DATA_ALEXAMEDIA]["accounts"][email]["websocket"] = None coordinator = hass.data[DATA_ALEXAMEDIA]["accounts"][email].get( "coordinator") if coordinator: coordinator.update_interval = scan_interval await coordinator.async_request_refresh()
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)) 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 await notify_async_unload_entry(hass, entry) await close_connections(hass, email) 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 = [] if hass.data[DATA_ALEXAMEDIA].get("config_flows"): for key, flow in hass.data[DATA_ALEXAMEDIA]["config_flows"].items(): if key.startswith(email) and flow: _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( f"alexa_media_{slugify(email)}{slugify((entry.data['url'])[7:])}") 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
async def close_connections(hass, email: Text) -> None: """Clear open aiohttp connections for email.""" if (email not in hass.data[DATA_ALEXAMEDIA]["accounts"] or "login_obj" not in hass.data[DATA_ALEXAMEDIA]["accounts"][email]): return account_dict = hass.data[DATA_ALEXAMEDIA]["accounts"][email] login_obj = account_dict["login_obj"] await login_obj.close() _LOGGER.debug("%s: Connection closed: %s", hide_email(email), login_obj.session.closed)
async def login_success(event=None) -> None: """Relogin to Alexa.""" for email, _ in hass.data[DATA_ALEXAMEDIA]["accounts"].items(): if hide_email(email) == event.data.get("email"): _LOGGER.debug("Received Login success: %s", event) email = account.get(CONF_EMAIL) login_obj: AlexaLogin = hass.data[DATA_ALEXAMEDIA]["accounts"][ email].get("login_obj") await setup_alexa(hass, config_entry, login_obj) break
async def async_step_check_proxy(self, user_input=None): """Check status of proxy for login.""" _LOGGER.debug( "Checking proxy response for %s - %s", hide_email(self.login.email), self.login.url, ) if self.proxy: await self.proxy.stop_proxy() self._cancel_proxy_listener() return self.async_external_step_done(next_step_id="finish_proxy")
async def close_connections(hass, email: Text) -> None: """Clear open aiohttp connections for email.""" if (email not in hass.data[DATA_ALEXAMEDIA]['accounts'] or 'login_obj' not in hass.data[DATA_ALEXAMEDIA]['accounts'][email]): return account_dict = hass.data[DATA_ALEXAMEDIA]['accounts'][email] login_obj = account_dict['login_obj'] await login_obj.close() _LOGGER.debug("%s: Connection closed: %s", hide_email(email), login_obj._session.closed) await clear_configurator(hass, email)
def report_relogin_required(hass, login, email) -> bool: """Send message for relogin required.""" if hass and login and email: if login.status: _LOGGER.debug( "Reporting need to relogin to %s with %s stats: %s", login.url, hide_email(email), login.stats, ) hass.bus.async_fire( "alexa_media_relogin_required", event_data={ "email": hide_email(email), "url": login.url, "stats": login.stats, }, ) return True return False
async def async_step_check_proxy(self, user_input=None): """Check status of proxy for login.""" _LOGGER.debug( "Checking proxy response for %s - %s", hide_email(self.login.email), self.login.url, ) if self.proxy: await self.proxy.stop_proxy() if await self.login.test_loggedin(): await self.login.finalize_login() return self.async_external_step_done(next_step_id="finish_proxy") return self.async_abort(reason=self.login.status.get("login_failed"))
async def update_dnd_state(login_obj) -> None: """Update the dnd state on ws dnd combo event.""" dnd = await AlexaAPI.get_dnd_state(login_obj) if "doNotDisturbDeviceStatusList" in dnd: async_dispatcher_send( hass, f"{DOMAIN}_{hide_email(email)}"[0:32], {"dnd_update": dnd["doNotDisturbDeviceStatusList"]}, ) return _LOGGER.debug("%s: get_dnd_state failed: dnd:%s", hide_email(email), dnd) return
async def ws_error_handler(message): """Handle websocket error. This currently logs the error. In the future, this should invalidate the websocket and determine if a reconnect should be done. By specification, websockets will issue a close after every error. """ email = login_obj.email errors = hass.data[DATA_ALEXAMEDIA]["accounts"][email]["websocketerror"] _LOGGER.debug( "%s: Received websocket error #%i %s: type %s", hide_email(email), errors, message, type(message), ) hass.data[DATA_ALEXAMEDIA]["accounts"][email]["websocket"] = None if message == "<class 'aiohttp.streams.EofStream'>": hass.data[DATA_ALEXAMEDIA]["accounts"][email]["websocketerror"] = 5 _LOGGER.debug("%s: Immediate abort on EoFstream", hide_email(email)) return hass.data[DATA_ALEXAMEDIA]["accounts"][email]["websocketerror"] = errors + 1
async def ws_error_handler(message): """Handle websocket error. This currently logs the error. In the future, this should invalidate the websocket and determine if a reconnect should be done. By specification, websockets will issue a close after every error. """ email = login_obj.email errors = hass.data[DATA_ALEXAMEDIA]["accounts"][email]["websocketerror"] _LOGGER.debug( "%s: Received websocket error #%i %s", hide_email(email), errors, message ) (hass.data[DATA_ALEXAMEDIA]["accounts"][email]["websocket"]) = None (hass.data[DATA_ALEXAMEDIA]["accounts"][email]["websocketerror"]) = errors + 1
async def ws_close_handler(): """Handle websocket close. This should attempt to reconnect up to 5 times """ from asyncio import sleep email: Text = login_obj.email errors: int = ( hass.data[DATA_ALEXAMEDIA]['accounts'][email]['websocketerror']) delay: int = 5 * 2**errors if (errors < 5): _LOGGER.debug("%s: Websocket closed; reconnect #%i in %is", hide_email(email), errors, delay) await sleep(delay) if (not (hass.data[DATA_ALEXAMEDIA]['accounts'][email]['websocket'] )): (hass.data[DATA_ALEXAMEDIA]['accounts'][email]['websocket'] ) = await ws_connect() else: _LOGGER.debug("%s: Websocket closed; retries exceeded; polling", hide_email(email)) (hass.data[DATA_ALEXAMEDIA]['accounts'][email]['websocket']) = None await update_devices(login_obj, no_throttle=True)
async def update_last_called(login_obj, last_called=None): """Update the last called device for the login_obj. This will store the last_called in hass.data and also fire an event to notify listeners. """ from alexapy import AlexaAPI if not last_called: last_called = await AlexaAPI.get_last_device_serial(login_obj) _LOGGER.debug("%s: Updated last_called: %s", hide_email(email), hide_serial(last_called)) stored_data = hass.data[DATA_ALEXAMEDIA]['accounts'][email] if (('last_called' in stored_data and last_called != stored_data['last_called']) or ('last_called' not in stored_data and last_called is not None)): _LOGGER.debug( "%s: last_called changed: %s to %s", hide_email(email), hide_serial(stored_data['last_called'] if 'last_called' in stored_data else None), hide_serial(last_called)) hass.bus.async_fire(f'{DOMAIN}_{hide_email(email)}'[0:32], {'last_called_change': last_called}) (hass.data[DATA_ALEXAMEDIA]['accounts'][email]['last_called'] ) = last_called
async def update_last_called(login_obj, last_called=None, force=False): """Update the last called device for the login_obj. This will store the last_called in hass.data and also fire an event to notify listeners. """ if not last_called or not (last_called and last_called.get("summary")): try: last_called = await AlexaAPI.get_last_device_serial(login_obj) except TypeError: _LOGGER.debug( "%s: Error updating last_called: %s", hide_email(email), hide_serial(last_called), ) return _LOGGER.debug("%s: Updated last_called: %s", hide_email(email), hide_serial(last_called)) stored_data = hass.data[DATA_ALEXAMEDIA]["accounts"][email] if (force or "last_called" in stored_data and last_called != stored_data["last_called"]) or ( "last_called" not in stored_data and last_called is not None): _LOGGER.debug( "%s: last_called changed: %s to %s", hide_email(email), hide_serial(stored_data["last_called"] if "last_called" in stored_data else None), hide_serial(last_called), ) async_dispatcher_send( hass, f"{DOMAIN}_{hide_email(email)}"[0:32], {"last_called_change": last_called}, ) hass.data[DATA_ALEXAMEDIA]["accounts"][email][ "last_called"] = last_called
async def relogin(event=None) -> None: """Relogin to Alexa.""" if hide_email(email) == event.data.get("email"): _LOGGER.debug("%s: Received relogin request: %s", hide_email(email), event) login_obj: AlexaLogin = hass.data[DATA_ALEXAMEDIA]["accounts"][ email].get("login_obj") if login_obj is None: login_obj = AlexaLogin( url=url, email=email, password=password, outputpath=hass.config.path, debug=account.get(CONF_DEBUG), otp_secret=account.get(CONF_OTPSECRET, ""), oauth=account.get(CONF_OAUTH, {}), uuid=await hass.helpers.instance_id.async_get(), ) hass.data[DATA_ALEXAMEDIA]["accounts"][email][ "login_obj"] = login_obj await login_obj.reset() # await login_obj.login() if await test_login_status(hass, config_entry, login_obj): await setup_alexa(hass, config_entry, login_obj)
async def async_unload_entry(hass, entry) -> bool: """Unload a config entry.""" 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 from .notify import async_unload_entry await async_unload_entry(hass, entry) email = entry.data['email'] await close_connections(hass, email) await clear_configurator(hass, email) hass.data[DATA_ALEXAMEDIA]['accounts'].pop(email) _LOGGER.debug("Unloaded entry for %s", hide_email(email)) return True
async def async_step_reauth(self, user_input=None): """Handle reauth processing for the config flow.""" self.config = user_input self.login = self.hass.data[DATA_ALEXAMEDIA]["accounts"][ user_input[CONF_EMAIL]].get("login_obj") try: _LOGGER.debug( "Attempting relogin for %s to %s", hide_email(self.config[CONF_EMAIL]), self.config[CONF_URL], ) await self.login.login(data=self.config) return await self._test_login() except AlexapyConnectionError: return self.async_show_form(step_id="reauth", errors={"base": "connection_error"})
async def wrapper(*args, **kwargs) -> Any: instance = args[0] result = None if hasattr(instance, "check_login_changes"): instance.check_login_changes() try: result = await func(*args, **kwargs) except AlexapyLoginCloseRequested: _LOGGER.debug( "%s.%s: Ignoring attempt to access Alexa after HA shutdown", func.__module__[func.__module__.find(".") + 1:], func.__name__, ) return None except AlexapyLoginError as ex: _LOGGER.debug( "%s.%s: detected bad login: %s", func.__module__[func.__module__.find(".") + 1:], func.__name__, EXCEPTION_TEMPLATE.format(type(ex).__name__, ex.args), ) instance = args[0] if hasattr(instance, "_login"): login = instance._login email = login.email hass = instance.hass if instance.hass else None if hass and ("configurator" not in (hass.data[DATA_ALEXAMEDIA]["accounts"][email]) or not (hass.data[DATA_ALEXAMEDIA]["accounts"] [email]["configurator"])): config_entry = hass.data[DATA_ALEXAMEDIA]["accounts"][ email]["config_entry"] setup_alexa = hass.data[DATA_ALEXAMEDIA]["accounts"][ email]["setup_alexa"] test_login_status = hass.data[DATA_ALEXAMEDIA]["accounts"][ email]["test_login_status"] _LOGGER.debug( "%s: Alexa API disconnected; attempting to relogin", hide_email(email), ) if login.status: await login.reset() await login.login() await test_login_status(hass, config_entry, login, setup_alexa) return None return result
async def async_step_start_proxy(self, user_input=None): """Start proxy for login.""" _LOGGER.debug( "Starting proxy for %s - %s", hide_email(self.login.email), self.login.url, ) await self.proxy.start_proxy() self._cancel_proxy_listener = async_call_later( self.hass, 600, lambda _: self._cancel_proxy(), ) self.hass.http.register_view(AlexaMediaAuthorizationCallbackView) callback_url = ( f"{self.config['hass_url']}{AUTH_CALLBACK_PATH}?flow_id={self.flow_id}" ) proxy_url = f"{self.proxy.access_url()}?config_flow_id={self.flow_id}&callback_url={callback_url}" if self.login.lastreq: proxy_url = f"{self.proxy.access_url()}/resume?config_flow_id={self.flow_id}&callback_url={callback_url}" return self.async_external_step(step_id="check_proxy", url=proxy_url)
def __init__(self, login, media_players=None) -> None: # pylint: disable=unexpected-keyword-arg """Initialize the Alexa device.""" super().__init__(None, login) _LOGGER.debug("%s: Initiating alarm control panel", hide_email(login.email)) # AlexaAPI requires a AlexaClient object, need to clean this up self._available = None self._assumed_state = None # Guard info self._appliance_id = None self._guard_entity_id = None self._friendly_name = "Alexa Guard" self._state = None self._should_poll = False self._attrs: Dict[Text, Text] = {} self._media_players = {} or media_players
def check_login_changes(self): """Update Login object if it has changed.""" # _LOGGER.debug("Checking if Login object has changed") try: login = self.hass.data[DATA_ALEXAMEDIA]["accounts"][self.email]["login_obj"] except (AttributeError, KeyError): return # _LOGGER.debug("Login object %s closed status: %s", login, login.session.closed) # _LOGGER.debug( # "Alexaapi %s closed status: %s", # self.alexa_api, # self.alexa_api._session.closed, # ) if self.alexa_api.update_login(login): _LOGGER.debug("Login object has changed; updating") self._login = login self.email = login.email self.account = hide_email(login.email)
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 ( "configurator" not in (hass.data[DATA_ALEXAMEDIA]["accounts"][email]) or not ( hass.data[DATA_ALEXAMEDIA]["accounts"][email]["configurator"] ) ): config_entry = hass.data[DATA_ALEXAMEDIA]["accounts"][email][ "config_entry" ] setup_alexa = hass.data[DATA_ALEXAMEDIA]["accounts"][email][ "setup_alexa" ] test_login_status = hass.data[DATA_ALEXAMEDIA]["accounts"][email][ "test_login_status" ] _LOGGER.debug( "%s: Alexa API disconnected; attempting to relogin", hide_email(email), ) if login.status and not await test_login_status( hass, config_entry, login, setup_alexa ): login.status = {} await login.login() return None return result
async def relogin(event=None) -> None: """Relogin to Alexa.""" for email, _ in hass.data[DATA_ALEXAMEDIA]["accounts"].items(): if hide_email(email) == event.data.get("email"): _LOGGER.debug("Received relogin request: %s", event) email = account.get(CONF_EMAIL) login_obj = hass.data[DATA_ALEXAMEDIA]["accounts"][email].get( "login_obj") if login_obj is None: login_obj = AlexaLogin(url, email, password, hass.config.path, account.get(CONF_DEBUG)) hass.data[DATA_ALEXAMEDIA]["accounts"][email][ "login_obj"] = login_obj await login_obj.reset() # await login_obj.login() if await test_login_status(hass, config_entry, login_obj): await setup_alexa(hass, config_entry, login_obj) break