diff --git a/custom_components/alexa_media/__init__.py b/custom_components/alexa_media/__init__.py index 8eb8b5b2..7ef77d00 100644 --- a/custom_components/alexa_media/__init__.py +++ b/custom_components/alexa_media/__init__.py @@ -7,65 +7,90 @@ For more details about this platform, please refer to the documentation at https://community.home-assistant.io/t/echo-devices-alexa-as-media-player-testers-needed/58639 """ -import logging from datetime import timedelta +import logging from typing import List, Optional, Text -import voluptuous as vol -from alexapy import (AlexapyLoginError, 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, - CONF_SCAN_INTERVAL, CONF_URL, - EVENT_HOMEASSISTANT_STOP) +from homeassistant.const import ( + CONF_EMAIL, + CONF_NAME, + CONF_PASSWORD, + CONF_SCAN_INTERVAL, + CONF_URL, + EVENT_HOMEASSISTANT_STOP, +) from homeassistant.helpers import config_validation as cv from homeassistant.helpers.discovery import async_load_platform from homeassistant.helpers.event import async_call_later +import voluptuous as vol from .config_flow import configured_instances -from .const import (ALEXA_COMPONENTS, ATTR_EMAIL, ATTR_NUM_ENTRIES, - CONF_ACCOUNTS, CONF_DEBUG, CONF_EXCLUDE_DEVICES, - CONF_INCLUDE_DEVICES, DATA_ALEXAMEDIA, DOMAIN, - MIN_TIME_BETWEEN_FORCED_SCANS, MIN_TIME_BETWEEN_SCANS, - SCAN_INTERVAL, SERVICE_CLEAR_HISTORY, - SERVICE_UPDATE_LAST_CALLED, STARTUP) +from .const import ( + ALEXA_COMPONENTS, + ATTR_EMAIL, + ATTR_NUM_ENTRIES, + CONF_ACCOUNTS, + CONF_DEBUG, + CONF_EXCLUDE_DEVICES, + CONF_INCLUDE_DEVICES, + DATA_ALEXAMEDIA, + DOMAIN, + MIN_TIME_BETWEEN_FORCED_SCANS, + MIN_TIME_BETWEEN_SCANS, + SCAN_INTERVAL, + SERVICE_CLEAR_HISTORY, + SERVICE_UPDATE_LAST_CALLED, + STARTUP, +) from .helpers import retry_async _LOGGER = logging.getLogger(__name__) -ACCOUNT_CONFIG_SCHEMA = vol.Schema({ - vol.Required(CONF_EMAIL): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Required(CONF_URL): cv.string, - vol.Optional(CONF_DEBUG, default=False): cv.boolean, - vol.Optional(CONF_INCLUDE_DEVICES, default=[]): - vol.All(cv.ensure_list, [cv.string]), - vol.Optional(CONF_EXCLUDE_DEVICES, default=[]): - vol.All(cv.ensure_list, [cv.string]), - vol.Optional(CONF_SCAN_INTERVAL, default=SCAN_INTERVAL): - cv.time_period, -}) - -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - vol.Optional(CONF_ACCOUNTS): - vol.All(cv.ensure_list, [ACCOUNT_CONFIG_SCHEMA]), - }), -}, 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]), -}) +ACCOUNT_CONFIG_SCHEMA = vol.Schema( + { + vol.Required(CONF_EMAIL): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Required(CONF_URL): cv.string, + vol.Optional(CONF_DEBUG, default=False): cv.boolean, + vol.Optional(CONF_INCLUDE_DEVICES, default=[]): vol.All( + cv.ensure_list, [cv.string] + ), + vol.Optional(CONF_EXCLUDE_DEVICES, default=[]): vol.All( + cv.ensure_list, [cv.string] + ), + vol.Optional(CONF_SCAN_INTERVAL, default=SCAN_INTERVAL): cv.time_period, + } +) + +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.Schema( + { + vol.Optional(CONF_ACCOUNTS): vol.All( + cv.ensure_list, [ACCOUNT_CONFIG_SCHEMA] + ), + } + ), + }, + 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): @@ -86,13 +111,12 @@ async def async_setup(hass, config, discovery_info=None): CONF_PASSWORD: account[CONF_PASSWORD], CONF_URL: account[CONF_URL], CONF_DEBUG: account[CONF_DEBUG], - CONF_INCLUDE_DEVICES: - account[CONF_INCLUDE_DEVICES], - CONF_EXCLUDE_DEVICES: - account[CONF_EXCLUDE_DEVICES], - CONF_SCAN_INTERVAL: - account[CONF_SCAN_INTERVAL].total_seconds(), - } + CONF_INCLUDE_DEVICES: account[CONF_INCLUDE_DEVICES], + CONF_EXCLUDE_DEVICES: account[CONF_EXCLUDE_DEVICES], + CONF_SCAN_INTERVAL: account[ + CONF_SCAN_INTERVAL + ].total_seconds(), + }, ) break else: @@ -107,8 +131,7 @@ async def async_setup(hass, config, discovery_info=None): CONF_DEBUG: account[CONF_DEBUG], CONF_INCLUDE_DEVICES: account[CONF_INCLUDE_DEVICES], CONF_EXCLUDE_DEVICES: account[CONF_EXCLUDE_DEVICES], - CONF_SCAN_INTERVAL: - account[CONF_SCAN_INTERVAL].total_seconds(), + CONF_SCAN_INTERVAL: account[CONF_SCAN_INTERVAL].total_seconds(), }, ) ) @@ -118,16 +141,18 @@ async def async_setup(hass, config, discovery_info=None): @retry_async(limit=5, delay=5, catch_exceptions=True) async def async_setup_entry(hass, config_entry): """Set up Alexa Media Player as config entry.""" + async def close_alexa_media(event=None) -> None: """Clean up Alexa connections.""" _LOGGER.debug("Received shutdown request: %s", event) - for email, _ in (hass.data - [DATA_ALEXAMEDIA]['accounts'].items()): + for email, _ in hass.data[DATA_ALEXAMEDIA]["accounts"].items(): await close_connections(hass, email) + if DATA_ALEXAMEDIA not in hass.data: hass.data[DATA_ALEXAMEDIA] = {} - hass.data[DATA_ALEXAMEDIA]['accounts'] = {} + hass.data[DATA_ALEXAMEDIA]["accounts"] = {} from alexapy import AlexaLogin, __version__ as alexapy_version + _LOGGER.info(STARTUP) _LOGGER.info("Loaded alexapy==%s", alexapy_version) hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, close_alexa_media) @@ -135,23 +160,24 @@ async def close_alexa_media(event=None) -> None: email = account.get(CONF_EMAIL) password = account.get(CONF_PASSWORD) url = account.get(CONF_URL) - if email not in hass.data[DATA_ALEXAMEDIA]['accounts']: - hass.data[DATA_ALEXAMEDIA]['accounts'][email] = {} - if 'login_obj' in hass.data[DATA_ALEXAMEDIA]['accounts'][email]: - login = hass.data[DATA_ALEXAMEDIA]['accounts'][email]['login_obj'] + if email not in hass.data[DATA_ALEXAMEDIA]["accounts"]: + hass.data[DATA_ALEXAMEDIA]["accounts"][email] = {} + if "login_obj" in hass.data[DATA_ALEXAMEDIA]["accounts"][email]: + login = hass.data[DATA_ALEXAMEDIA]["accounts"][email]["login_obj"] else: - 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 + 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) + await test_login_status(hass, config_entry, login, setup_platform_callback) return True @@ -163,189 +189,210 @@ async def setup_platform_callback(hass, config_entry, login, callback_data): request_configuration and configuration_callback """ - _LOGGER.debug(("Configurator closed for Status: %s\n" - " got captcha: %s securitycode: %s" - " Claimsoption: %s AuthSelectOption: %s " - " VerificationCode: %s"), - login.status, - callback_data.get('captcha'), - callback_data.get('securitycode'), - callback_data.get('claimsoption'), - callback_data.get('authselectoption'), - callback_data.get('verificationcode')) + _LOGGER.debug( + ( + "Configurator closed for Status: %s\n" + " got captcha: %s securitycode: %s" + " Claimsoption: %s AuthSelectOption: %s " + " VerificationCode: %s" + ), + login.status, + callback_data.get("captcha"), + callback_data.get("securitycode"), + callback_data.get("claimsoption"), + callback_data.get("authselectoption"), + callback_data.get("verificationcode"), + ) await login.login(data=callback_data) - await test_login_status(hass, config_entry, login, - setup_platform_callback) + await test_login_status(hass, config_entry, login, setup_platform_callback) -async def request_configuration(hass, config_entry, login, - setup_platform_callback): +async def request_configuration(hass, config_entry, login, setup_platform_callback): """Request configuration steps from the user using the configurator.""" + async def configuration_callback(callback_data): """Handle the submitted configuration.""" - await hass.async_add_job(setup_platform_callback, hass, config_entry, - login, callback_data) + await hass.async_add_job( + setup_platform_callback, hass, config_entry, login, callback_data + ) configurator = hass.components.configurator status = login.status email = login.email # links = "" footer = "" - if 'error_message' in status and status['error_message']: - footer = ('\nNOTE: Actual Amazon error message in red below. ' - 'Remember password will be provided automatically' - ' and Amazon error message normally appears first!') + if "error_message" in status and status["error_message"]: + footer = ( + "\nNOTE: Actual Amazon error message in red below. " + "Remember password will be provided automatically" + " and Amazon error message normally appears first!" + ) # if login.links: # links = '\n\nGo to link with link# (e.g. link0)\n' + login.links # Get Captcha - if (status and 'captcha_image_url' in status and - status['captcha_image_url'] is not None): + if ( + status + and "captcha_image_url" in status + and status["captcha_image_url"] is not None + ): config_id = configurator.async_request_config( "Alexa Media Player - Captcha - {}".format(email), configuration_callback, - description=('Please enter the text for the captcha.' - ' Please hit confirm to reload image.' - # + links - + footer - ), - description_image=status['captcha_image_url'], + description=( + "Please enter the text for the captcha." + " Please hit confirm to reload image." + # + links + + footer + ), + description_image=status["captcha_image_url"], submit_caption="Confirm", - fields=[{'id': 'captcha', 'name': 'Captcha'}] + fields=[{"id": "captcha", "name": "Captcha"}], ) - elif (status and 'securitycode_required' in status and - status['securitycode_required']): # Get 2FA code + elif ( + status and "securitycode_required" in status and status["securitycode_required"] + ): # Get 2FA code config_id = configurator.async_request_config( "Alexa Media Player - 2FA - {}".format(email), configuration_callback, - description=('Please enter your Two-Factor Security code.' - # + links - + footer), + description=( + "Please enter your Two-Factor Security code." + # + links + + footer + ), submit_caption="Confirm", - fields=[{'id': 'securitycode', 'name': 'Security Code'}] + fields=[{"id": "securitycode", "name": "Security Code"}], ) - elif (status and 'claimspicker_required' in status and - status['claimspicker_required']): # Get picker method - options = status['claimspicker_message'] + elif ( + status and "claimspicker_required" in status and status["claimspicker_required"] + ): # Get picker method + options = status["claimspicker_message"] if options: config_id = configurator.async_request_config( "Alexa Media Player - Verification Method - {}".format(email), configuration_callback, - description=('Please select the verification method. ' - '(e.g., `sms` or `email`).\n{}'.format(options) - # + links - + footer), + description=( + "Please select the verification method. " + "(e.g., `sms` or `email`).\n{}".format(options) + # + links + + footer + ), submit_caption="Confirm", - fields=[{'id': 'claimsoption', 'name': 'Option'}] + fields=[{"id": "claimsoption", "name": "Option"}], ) else: await configuration_callback({}) - elif (status and 'authselect_required' in status and - status['authselect_required']): # Get picker method - options = status['authselect_message'] + elif ( + status and "authselect_required" in status and status["authselect_required"] + ): # Get picker method + options = status["authselect_message"] if options: config_id = configurator.async_request_config( "Alexa Media Player - OTP Method - {}".format(email), configuration_callback, - description=('Please select the OTP method. ' - '(e.g., `0`, `1`).
{}'.format(options) - # + links - + footer), + description=( + "Please select the OTP method. " + "(e.g., `0`, `1`).
{}".format(options) + # + links + + footer + ), submit_caption="Confirm", - fields=[{'id': 'authselectoption', 'name': 'Option'}] + fields=[{"id": "authselectoption", "name": "Option"}], ) else: await configuration_callback({}) - elif (status and 'verificationcode_required' in status and - status['verificationcode_required']): # Get picker method + elif ( + status + and "verificationcode_required" in status + and status["verificationcode_required"] + ): # Get picker method config_id = configurator.async_request_config( "Alexa Media Player - Verification Code - {}".format(email), configuration_callback, - description=('Please enter received verification code.' - # + links - + footer), + description=( + "Please enter received verification code." + # + links + + footer + ), submit_caption="Confirm", - fields=[{'id': 'verificationcode', 'name': 'Verification Code'}] + fields=[{"id": "verificationcode", "name": "Verification Code"}], ) else: # Check login config_id = configurator.async_request_config( "Alexa Media Player - Begin - {}".format(email), configuration_callback, - description=('Please hit confirm to begin login attempt.'), + description=("Please hit confirm to begin login attempt."), submit_caption="Confirm", - fields=[] + fields=[], ) - if 'configurator' not in hass.data[DATA_ALEXAMEDIA]['accounts'][email]: - hass.data[DATA_ALEXAMEDIA]['accounts'][email] = {"configurator": []} - hass.data[DATA_ALEXAMEDIA]['accounts'][email]['configurator'].append( - config_id) - if 'error_message' in status and status['error_message']: - configurator.async_notify_errors( - config_id, - status['error_message']) - if len(hass.data[DATA_ALEXAMEDIA]['accounts'][email]['configurator']) > 1: + if "configurator" not in hass.data[DATA_ALEXAMEDIA]["accounts"][email]: + hass.data[DATA_ALEXAMEDIA]["accounts"][email] = {"configurator": []} + hass.data[DATA_ALEXAMEDIA]["accounts"][email]["configurator"].append(config_id) + if "error_message" in status and status["error_message"]: + configurator.async_notify_errors(config_id, status["error_message"]) + if len(hass.data[DATA_ALEXAMEDIA]["accounts"][email]["configurator"]) > 1: configurator.async_request_done( - (hass.data[DATA_ALEXAMEDIA]['accounts'][email] - ['configurator']).pop(0)) + (hass.data[DATA_ALEXAMEDIA]["accounts"][email]["configurator"]).pop(0) + ) -async def test_login_status(hass, config_entry, login, - setup_platform_callback) -> None: +async def test_login_status(hass, config_entry, login, setup_platform_callback) -> None: """Test the login status and spawn requests for info.""" _LOGGER.debug("Testing login status: %s", login.status) - if 'login_successful' in login.status and login.status['login_successful']: - _LOGGER.debug("Setting up Alexa devices for %s", - hide_email(login.email)) - await hass.async_add_job(setup_alexa, hass, config_entry, - login) + if "login_successful" in login.status and login.status["login_successful"]: + _LOGGER.debug("Setting up Alexa devices for %s", hide_email(login.email)) + await hass.async_add_job(setup_alexa, hass, config_entry, login) return - if ('captcha_required' in login.status and - login.status['captcha_required']): + if "captcha_required" in login.status and login.status["captcha_required"]: _LOGGER.debug("Creating configurator to request captcha") - elif ('securitycode_required' in login.status and - login.status['securitycode_required']): + elif ( + "securitycode_required" in login.status + and login.status["securitycode_required"] + ): _LOGGER.debug("Creating configurator to request 2FA") - elif ('claimspicker_required' in login.status and - login.status['claimspicker_required']): + elif ( + "claimspicker_required" in login.status + and login.status["claimspicker_required"] + ): _LOGGER.debug("Creating configurator to select verification option") - elif ('authselect_required' in login.status and - login.status['authselect_required']): + elif "authselect_required" in login.status and login.status["authselect_required"]: _LOGGER.debug("Creating configurator to select OTA option") - elif ('verificationcode_required' in login.status and - login.status['verificationcode_required']): + elif ( + "verificationcode_required" in login.status + and login.status["verificationcode_required"] + ): _LOGGER.debug("Creating configurator to enter verification code") - elif ('login_failed' in login.status and - login.status['login_failed']): + elif "login_failed" in login.status and login.status["login_failed"]: _LOGGER.debug("Creating configurator to start new login attempt") - await hass.async_add_job(request_configuration, hass, config_entry, login, - setup_platform_callback) + await hass.async_add_job( + request_configuration, hass, config_entry, login, setup_platform_callback + ) async def setup_alexa(hass, config_entry, login_obj): """Set up a alexa api based on host parameter.""" + def _existing_serials() -> List: email: Text = login_obj.email - existing_serials = (list(hass.data[DATA_ALEXAMEDIA] - ['accounts'] - [email] - ['entities'] - ['media_player'].keys()) if 'entities' in ( - hass.data[DATA_ALEXAMEDIA] - ['accounts'] - [email]) - else []) + existing_serials = ( + list( + hass.data[DATA_ALEXAMEDIA]["accounts"][email]["entities"][ + "media_player" + ].keys() + ) + if "entities" in (hass.data[DATA_ALEXAMEDIA]["accounts"][email]) + else [] + ) for serial in existing_serials: - device = ( - hass.data[DATA_ALEXAMEDIA] - ['accounts'] - [email] - ['devices'] - ['media_player'] - [serial]) - if 'appDeviceList' in device and device['appDeviceList']: - apps = list(map( - lambda x: x['serialNumber'] if 'serialNumber' in x - else None, - device['appDeviceList'])) + device = hass.data[DATA_ALEXAMEDIA]["accounts"][email]["devices"][ + "media_player" + ][serial] + if "appDeviceList" in device and device["appDeviceList"]: + apps = list( + map( + lambda x: x["serialNumber"] if "serialNumber" in x else None, + device["appDeviceList"], + ) + ) # _LOGGER.debug("Combining %s with %s", # existing_serials, apps) existing_serials = existing_serials + apps @@ -366,21 +413,21 @@ async def update_devices(login_obj): Each AlexaAPI call generally results in two webpage requests. """ from alexapy import AlexaAPI + email: Text = login_obj.email - if email not in hass.data[DATA_ALEXAMEDIA]['accounts']: + if email not in hass.data[DATA_ALEXAMEDIA]["accounts"]: return existing_serials = _existing_serials() - existing_entities = (hass.data[DATA_ALEXAMEDIA] - ['accounts'] - [email] - ['entities'] - ['media_player'].values()) - if ('websocket' in hass.data[DATA_ALEXAMEDIA]['accounts'][email] - and hass.data[DATA_ALEXAMEDIA]['accounts'][email]['websocket'] - and not (hass.data[DATA_ALEXAMEDIA] - ['accounts'][email]['new_devices'])): + existing_entities = hass.data[DATA_ALEXAMEDIA]["accounts"][email]["entities"][ + "media_player" + ].values() + if ( + "websocket" in hass.data[DATA_ALEXAMEDIA]["accounts"][email] + and hass.data[DATA_ALEXAMEDIA]["accounts"][email]["websocket"] + and not (hass.data[DATA_ALEXAMEDIA]["accounts"][email]["new_devices"]) + ): return - hass.data[DATA_ALEXAMEDIA]['accounts'][email]['new_devices'] = False + hass.data[DATA_ALEXAMEDIA]["accounts"][email]["new_devices"] = False try: auth_info = await AlexaAPI.get_authentication(login_obj) devices = await AlexaAPI.get_devices(login_obj) @@ -388,21 +435,24 @@ async def update_devices(login_obj): preferences = await AlexaAPI.get_device_preferences(login_obj) dnd = await AlexaAPI.get_dnd_state(login_obj) raw_notifications = await AlexaAPI.get_notifications(login_obj) - _LOGGER.debug("%s: Found %s devices, %s bluetooth", - hide_email(email), - len(devices) if devices is not None else '', - len(bluetooth) if bluetooth is not None else '') - if ((devices is None or bluetooth is None) - and not (hass.data[DATA_ALEXAMEDIA] - ['accounts'][email]['configurator'])): + _LOGGER.debug( + "%s: Found %s devices, %s bluetooth", + hide_email(email), + len(devices) if devices is not None else "", + len(bluetooth) if bluetooth is not None else "", + ) + if (devices is None or bluetooth is None) and not ( + hass.data[DATA_ALEXAMEDIA]["accounts"][email]["configurator"] + ): raise AlexapyLoginError() except (AlexapyLoginError, RuntimeError): - _LOGGER.debug("%s: Alexa API disconnected; attempting to relogin", - hide_email(email)) + _LOGGER.debug( + "%s: Alexa API disconnected; attempting to relogin", hide_email(email) + ) await login_obj.login_with_cookie() - await test_login_status(hass, - config_entry, login_obj, - setup_platform_callback) + await test_login_status( + hass, config_entry, login_obj, setup_platform_callback + ) return new_alexa_clients = [] # list of newly discovered device names @@ -410,77 +460,82 @@ async def update_devices(login_obj): include_filter = [] for device in devices: - if include and device['accountName'] not in include: - include_filter.append(device['accountName']) - if 'appDeviceList' in device: - for app in device['appDeviceList']: - (hass.data[DATA_ALEXAMEDIA] - ['accounts'] - [email] - ['excluded'] - [app['serialNumber']]) = device - (hass.data[DATA_ALEXAMEDIA] - ['accounts'] - [email] - ['excluded'] - [device['serialNumber']]) = device + if include and device["accountName"] not in include: + include_filter.append(device["accountName"]) + if "appDeviceList" in device: + for app in device["appDeviceList"]: + ( + hass.data[DATA_ALEXAMEDIA]["accounts"][email]["excluded"][ + app["serialNumber"] + ] + ) = device + ( + hass.data[DATA_ALEXAMEDIA]["accounts"][email]["excluded"][ + device["serialNumber"] + ] + ) = device continue - elif exclude and device['accountName'] in exclude: - exclude_filter.append(device['accountName']) - if 'appDeviceList' in device: - for app in device['appDeviceList']: - (hass.data[DATA_ALEXAMEDIA] - ['accounts'] - [email] - ['excluded'] - [app['serialNumber']]) = device - (hass.data[DATA_ALEXAMEDIA] - ['accounts'] - [email] - ['excluded'] - [device['serialNumber']]) = device + elif exclude and device["accountName"] in exclude: + exclude_filter.append(device["accountName"]) + if "appDeviceList" in device: + for app in device["appDeviceList"]: + ( + hass.data[DATA_ALEXAMEDIA]["accounts"][email]["excluded"][ + app["serialNumber"] + ] + ) = device + ( + hass.data[DATA_ALEXAMEDIA]["accounts"][email]["excluded"][ + device["serialNumber"] + ] + ) = device continue - if 'bluetoothStates' in bluetooth: - for b_state in bluetooth['bluetoothStates']: - if device['serialNumber'] == b_state['deviceSerialNumber']: - device['bluetooth_state'] = b_state - - if 'devicePreferences' in preferences: - for dev in preferences['devicePreferences']: - if dev['deviceSerialNumber'] == device['serialNumber']: - device['locale'] = dev['locale'] - device['timeZoneId'] = dev['timeZoneId'] - _LOGGER.debug("Locale %s timezone %s found for %s", - device['locale'], - device['timeZoneId'], - hide_serial(device['serialNumber'])) - - if 'doNotDisturbDeviceStatusList' in dnd: - for dev in dnd['doNotDisturbDeviceStatusList']: - if dev['deviceSerialNumber'] == device['serialNumber']: - device['dnd'] = dev['enabled'] - _LOGGER.debug("DND %s found for %s", - device['dnd'], - hide_serial(device['serialNumber'])) - device['auth_info'] = auth_info - (hass.data[DATA_ALEXAMEDIA] - ['accounts'] - [email] - ['devices'] - ['media_player'] - [device['serialNumber']]) = device - - if device['serialNumber'] not in existing_serials: - new_alexa_clients.append(device['accountName']) - _LOGGER.debug("%s: Existing: %s New: %s;" - " Filtered out by not being in include: %s " - "or in exclude: %s", - hide_email(email), - list(existing_entities), - new_alexa_clients, - include_filter, - exclude_filter) + if "bluetoothStates" in bluetooth: + for b_state in bluetooth["bluetoothStates"]: + if device["serialNumber"] == b_state["deviceSerialNumber"]: + device["bluetooth_state"] = b_state + + if "devicePreferences" in preferences: + for dev in preferences["devicePreferences"]: + if dev["deviceSerialNumber"] == device["serialNumber"]: + device["locale"] = dev["locale"] + device["timeZoneId"] = dev["timeZoneId"] + _LOGGER.debug( + "Locale %s timezone %s found for %s", + device["locale"], + device["timeZoneId"], + hide_serial(device["serialNumber"]), + ) + + if "doNotDisturbDeviceStatusList" in dnd: + for dev in dnd["doNotDisturbDeviceStatusList"]: + if dev["deviceSerialNumber"] == device["serialNumber"]: + device["dnd"] = dev["enabled"] + _LOGGER.debug( + "DND %s found for %s", + device["dnd"], + hide_serial(device["serialNumber"]), + ) + device["auth_info"] = auth_info + ( + hass.data[DATA_ALEXAMEDIA]["accounts"][email]["devices"][ + "media_player" + ][device["serialNumber"]] + ) = device + + if device["serialNumber"] not in existing_serials: + new_alexa_clients.append(device["accountName"]) + _LOGGER.debug( + "%s: Existing: %s New: %s;" + " Filtered out by not being in include: %s " + "or in exclude: %s", + hide_email(email), + list(existing_entities), + new_alexa_clients, + include_filter, + exclude_filter, + ) if new_alexa_clients: cleaned_config = config.copy() @@ -489,56 +544,60 @@ async def update_devices(login_obj): for component in ALEXA_COMPONENTS: if component == "notify": hass.async_create_task( - async_load_platform(hass, - component, - DOMAIN, - {CONF_NAME: DOMAIN, - "config": cleaned_config}, - config)) + async_load_platform( + hass, + component, + DOMAIN, + {CONF_NAME: DOMAIN, "config": cleaned_config}, + config, + ) + ) else: hass.async_add_job( hass.config_entries.async_forward_entry_setup( - config_entry, - component)) + config_entry, component + ) + ) await process_notifications(login_obj, raw_notifications) # Process last_called data to fire events await update_last_called(login_obj) - async_call_later(hass, scan_interval, lambda _: - hass.async_create_task( - update_devices(login_obj, - no_throttle=True))) + async_call_later( + hass, + scan_interval, + lambda _: hass.async_create_task( + update_devices(login_obj, no_throttle=True) + ), + ) async def process_notifications(login_obj, raw_notifications=None): """Process raw notifications json.""" from alexapy import AlexaAPI + if not raw_notifications: raw_notifications = await AlexaAPI.get_notifications(login_obj) email: Text = login_obj.email notifications = {} for notification in raw_notifications: - n_dev_id = notification['deviceSerialNumber'] - n_type = notification['type'] + n_dev_id = notification["deviceSerialNumber"] + n_type = notification["type"] if n_type == "MusicAlarm": n_type = "Alarm" - n_id = notification['notificationIndex'] - n_date = notification['originalDate'] - n_time = notification['originalTime'] - notification['date_time'] = f"{n_date} {n_time}" + n_id = notification["notificationIndex"] + n_date = notification["originalDate"] + n_time = notification["originalTime"] + notification["date_time"] = f"{n_date} {n_time}" if n_dev_id not in notifications: notifications[n_dev_id] = {} if n_type not in notifications[n_dev_id]: notifications[n_dev_id][n_type] = {} notifications[n_dev_id][n_type][n_id] = notification - (hass.data[DATA_ALEXAMEDIA] - ['accounts'] - [email] - ['notifications']) = notifications + (hass.data[DATA_ALEXAMEDIA]["accounts"][email]["notifications"]) = notifications _LOGGER.debug( "%s: Updated %s notifications for %s devices", hide_email(email), len(raw_notifications), - len(notifications) + len(notifications), ) async def update_last_called(login_obj, last_called=None): @@ -548,53 +607,54 @@ async def update_last_called(login_obj, last_called=None): 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)) + _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 + 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_bluetooth_state(login_obj, device_serial): """Update the bluetooth state on ws bluetooth event.""" from alexapy import AlexaAPI + bluetooth = await AlexaAPI.get_bluetooth(login_obj) - device = (hass.data[DATA_ALEXAMEDIA] - ['accounts'] - [email] - ['devices'] - ['media_player'] - [device_serial]) - - if 'bluetoothStates' in bluetooth: - for b_state in bluetooth['bluetoothStates']: - if device_serial == b_state['deviceSerialNumber']: + device = hass.data[DATA_ALEXAMEDIA]["accounts"][email]["devices"][ + "media_player" + ][device_serial] + + if "bluetoothStates" in bluetooth: + for b_state in bluetooth["bluetoothStates"]: + if device_serial == b_state["deviceSerialNumber"]: # _LOGGER.debug("%s: setting value for: %s to %s", # hide_email(email), # hide_serial(device_serial), # hide_serial(b_state)) - device['bluetooth_state'] = b_state - return device['bluetooth_state'] - _LOGGER.debug("%s: get_bluetooth for: %s failed with %s", - hide_email(email), - hide_serial(device_serial), - hide_serial(bluetooth)) + device["bluetooth_state"] = b_state + return device["bluetooth_state"] + _LOGGER.debug( + "%s: get_bluetooth for: %s failed with %s", + hide_email(email), + hide_serial(device_serial), + hide_serial(bluetooth), + ) return None async def clear_history(call): @@ -610,17 +670,17 @@ async def clear_history(call): """ from alexapy import AlexaAPI + 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()): + _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'] + login_obj = account_dict["login_obj"] return await AlexaAPI.clear_history(login_obj, items) async def last_call_handler(call): @@ -633,11 +693,10 @@ async def last_call_handler(call): """ 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()): + 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'] + login_obj = account_dict["login_obj"] await update_last_called(login_obj) async def ws_connect() -> WebsocketEchoClient: @@ -647,18 +706,19 @@ async def ws_connect() -> WebsocketEchoClient: """ 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) + 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_) + _LOGGER.debug( + "%s: Websocket creation failed: %s", hide_email(email), exception_ + ) return return websocket @@ -668,134 +728,168 @@ async def ws_handler(message_obj): This allows push notifications from Alexa to update last_called and media state. """ - command = (message_obj.json_payload['command'] - if isinstance(message_obj.json_payload, dict) and - 'command' in message_obj.json_payload - else None) - json_payload = (message_obj.json_payload['payload'] - if isinstance(message_obj.json_payload, dict) and - 'payload' in message_obj.json_payload - else None) + command = ( + message_obj.json_payload["command"] + if isinstance(message_obj.json_payload, dict) + and "command" in message_obj.json_payload + else None + ) + json_payload = ( + message_obj.json_payload["payload"] + if isinstance(message_obj.json_payload, dict) + and "payload" in message_obj.json_payload + else None + ) existing_serials = _existing_serials() - if 'websocket_commands' not in (hass.data[DATA_ALEXAMEDIA]['accounts'] - [email]): - (hass.data[DATA_ALEXAMEDIA]['accounts'] - [email]['websocket_commands']) = {} - seen_commands = (hass.data[DATA_ALEXAMEDIA]['accounts'] - [email]['websocket_commands']) + if "websocket_commands" not in (hass.data[DATA_ALEXAMEDIA]["accounts"][email]): + (hass.data[DATA_ALEXAMEDIA]["accounts"][email]["websocket_commands"]) = {} + seen_commands = hass.data[DATA_ALEXAMEDIA]["accounts"][email][ + "websocket_commands" + ] if command and json_payload: import time - _LOGGER.debug("%s: Received websocket command: %s : %s", - hide_email(email), - command, hide_serial(json_payload)) + + _LOGGER.debug( + "%s: Received websocket command: %s : %s", + hide_email(email), + command, + hide_serial(json_payload), + ) serial = None if command not in seen_commands: seen_commands[command] = time.time() - _LOGGER.debug( - "Adding %s to seen_commands: %s", - command, - seen_commands) - if ('dopplerId' in json_payload and - 'deviceSerialNumber' in json_payload['dopplerId']): - serial = (json_payload['dopplerId']['deviceSerialNumber']) - elif ('key' in json_payload and 'entryId' in json_payload['key'] - and json_payload['key']['entryId'].find('#') != -1): - serial = (json_payload['key']['entryId']).split('#')[2] + _LOGGER.debug("Adding %s to seen_commands: %s", command, seen_commands) + if ( + "dopplerId" in json_payload + and "deviceSerialNumber" in json_payload["dopplerId"] + ): + serial = json_payload["dopplerId"]["deviceSerialNumber"] + elif ( + "key" in json_payload + and "entryId" in json_payload["key"] + and json_payload["key"]["entryId"].find("#") != -1 + ): + serial = (json_payload["key"]["entryId"]).split("#")[2] else: serial = None - if command == 'PUSH_ACTIVITY': + if command == "PUSH_ACTIVITY": # Last_Alexa Updated last_called = { - 'serialNumber': serial, - 'timestamp': json_payload['timestamp'] + "serialNumber": serial, + "timestamp": json_payload["timestamp"], } - if (serial and serial in existing_serials): - await update_last_called(login_obj, - last_called) + if serial and serial in existing_serials: + await update_last_called(login_obj, last_called) hass.bus.async_fire( - f'{DOMAIN}_{hide_email(email)}'[0:32], - {'push_activity': json_payload}) - elif command in ('PUSH_AUDIO_PLAYER_STATE', 'PUSH_MEDIA_CHANGE', - 'PUSH_MEDIA_PROGRESS_CHANGE'): + f"{DOMAIN}_{hide_email(email)}"[0:32], + {"push_activity": json_payload}, + ) + elif command in ( + "PUSH_AUDIO_PLAYER_STATE", + "PUSH_MEDIA_CHANGE", + "PUSH_MEDIA_PROGRESS_CHANGE", + ): # Player update/ Push_media from tune_in - if (serial and serial in existing_serials): - _LOGGER.debug("Updating media_player: %s", - hide_serial(json_payload)) + if serial and serial in existing_serials: + _LOGGER.debug( + "Updating media_player: %s", hide_serial(json_payload) + ) hass.bus.async_fire( - f'{DOMAIN}_{hide_email(email)}'[0:32], - {'player_state': json_payload}) - elif command == 'PUSH_VOLUME_CHANGE': + f"{DOMAIN}_{hide_email(email)}"[0:32], + {"player_state": json_payload}, + ) + elif command == "PUSH_VOLUME_CHANGE": # Player volume update - if (serial and serial in existing_serials): - _LOGGER.debug("Updating media_player volume: %s", - hide_serial(json_payload)) + if serial and serial in existing_serials: + _LOGGER.debug( + "Updating media_player volume: %s", hide_serial(json_payload) + ) hass.bus.async_fire( - f'{DOMAIN}_{hide_email(email)}'[0:32], - {'player_state': json_payload}) - elif command in ('PUSH_DOPPLER_CONNECTION_CHANGE', - 'PUSH_EQUALIZER_STATE_CHANGE'): + f"{DOMAIN}_{hide_email(email)}"[0:32], + {"player_state": json_payload}, + ) + elif command in ( + "PUSH_DOPPLER_CONNECTION_CHANGE", + "PUSH_EQUALIZER_STATE_CHANGE", + ): # Player availability update - if (serial and serial in existing_serials): - _LOGGER.debug("Updating media_player availability %s", - hide_serial(json_payload)) + if serial and serial in existing_serials: + _LOGGER.debug( + "Updating media_player availability %s", + hide_serial(json_payload), + ) hass.bus.async_fire( - f'{DOMAIN}_{hide_email(email)}'[0:32], - {'player_state': json_payload}) - elif command == 'PUSH_BLUETOOTH_STATE_CHANGE': + f"{DOMAIN}_{hide_email(email)}"[0:32], + {"player_state": json_payload}, + ) + elif command == "PUSH_BLUETOOTH_STATE_CHANGE": # Player bluetooth update - bt_event = json_payload['bluetoothEvent'] - bt_success = json_payload['bluetoothEventSuccess'] - if (serial and serial in existing_serials and - bt_success and bt_event and bt_event in [ - 'DEVICE_CONNECTED', - 'DEVICE_DISCONNECTED']): - _LOGGER.debug("Updating media_player bluetooth %s", - hide_serial(json_payload)) - bluetooth_state = await update_bluetooth_state(login_obj, - serial) + bt_event = json_payload["bluetoothEvent"] + bt_success = json_payload["bluetoothEventSuccess"] + if ( + serial + and serial in existing_serials + and bt_success + and bt_event + and bt_event in ["DEVICE_CONNECTED", "DEVICE_DISCONNECTED"] + ): + _LOGGER.debug( + "Updating media_player bluetooth %s", hide_serial(json_payload) + ) + bluetooth_state = await update_bluetooth_state(login_obj, serial) # _LOGGER.debug("bluetooth_state %s", # hide_serial(bluetooth_state)) if bluetooth_state: hass.bus.async_fire( - f'{DOMAIN}_{hide_email(email)}'[0:32], - {'bluetooth_change': bluetooth_state}) - elif command == 'PUSH_MEDIA_QUEUE_CHANGE': + f"{DOMAIN}_{hide_email(email)}"[0:32], + {"bluetooth_change": bluetooth_state}, + ) + elif command == "PUSH_MEDIA_QUEUE_CHANGE": # Player availability update - if (serial and serial in existing_serials): - _LOGGER.debug("Updating media_player queue %s", - hide_serial(json_payload)) + if serial and serial in existing_serials: + _LOGGER.debug( + "Updating media_player queue %s", hide_serial(json_payload) + ) hass.bus.async_fire( - f'{DOMAIN}_{hide_email(email)}'[0:32], - {'queue_state': json_payload}) - elif command == 'PUSH_NOTIFICATION_CHANGE': + f"{DOMAIN}_{hide_email(email)}"[0:32], + {"queue_state": json_payload}, + ) + elif command == "PUSH_NOTIFICATION_CHANGE": # Player update await process_notifications(login_obj) - if (serial and serial in existing_serials): - _LOGGER.debug("Updating mediaplayer notifications: %s", - hide_serial(json_payload)) + if serial and serial in existing_serials: + _LOGGER.debug( + "Updating mediaplayer notifications: %s", + hide_serial(json_payload), + ) hass.bus.async_fire( - f'{DOMAIN}_{hide_email(email)}'[0:32], - {'notification_update': json_payload}) - if (serial and serial not in existing_serials - and serial not in (hass.data[DATA_ALEXAMEDIA] - ['accounts'] - [email] - ['excluded'].keys())): + f"{DOMAIN}_{hide_email(email)}"[0:32], + {"notification_update": json_payload}, + ) + if ( + serial + and serial not in existing_serials + and serial + not in ( + hass.data[DATA_ALEXAMEDIA]["accounts"][email]["excluded"].keys() + ) + ): _LOGGER.debug("Discovered new media_player %s", serial) - (hass.data[DATA_ALEXAMEDIA] - ['accounts'][email]['new_devices']) = True + (hass.data[DATA_ALEXAMEDIA]["accounts"][email]["new_devices"]) = True await update_devices(login_obj, no_throttle=True) 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() + _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_close_handler(): """Handle websocket close. @@ -804,33 +898,39 @@ async def ws_close_handler(): """ from asyncio import sleep import time + email: Text = login_obj.email - errors: int = (hass.data - [DATA_ALEXAMEDIA]['accounts'][email]['websocketerror']) + errors: int = (hass.data[DATA_ALEXAMEDIA]["accounts"][email]["websocketerror"]) delay: int = 5 * 2 ** errors - last_attempt = (hass.data[DATA_ALEXAMEDIA]['accounts'] - [email]['websocket_lastattempt']) + last_attempt = hass.data[DATA_ALEXAMEDIA]["accounts"][email][ + "websocket_lastattempt" + ] now = time.time() - if ((now - last_attempt) < delay): + 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) + 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, + ) await sleep(delay) - (hass.data[DATA_ALEXAMEDIA]['accounts'] - [email]['websocket_lastattempt']) = time.time() - (hass.data[DATA_ALEXAMEDIA]['accounts'] - [email]['websocket']) = await ws_connect() + ( + hass.data[DATA_ALEXAMEDIA]["accounts"][email]["websocket_lastattempt"] + ) = time.time() + ( + hass.data[DATA_ALEXAMEDIA]["accounts"][email]["websocket"] + ) = await ws_connect() errors += 1 delay = 5 * 2 ** errors else: - _LOGGER.debug("%s: Websocket closed; retries exceeded; polling", - hide_email(email)) - (hass.data[DATA_ALEXAMEDIA]['accounts'] - [email]['websocket']) = None + _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 ws_error_handler(message): @@ -841,15 +941,13 @@ async def ws_error_handler(message): 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 + 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 + config = config_entry.data email = config.get(CONF_EMAIL) include = config.get(CONF_INCLUDE_DEVICES) @@ -859,32 +957,36 @@ async def ws_error_handler(message): if isinstance(config.get(CONF_SCAN_INTERVAL), timedelta) else config.get(CONF_SCAN_INTERVAL) ) - if 'login_obj' not in hass.data[DATA_ALEXAMEDIA]['accounts'][email]: - (hass.data[DATA_ALEXAMEDIA]['accounts'][email] - ['login_obj']) = login_obj - if 'devices' not in hass.data[DATA_ALEXAMEDIA]['accounts'][email]: - (hass.data[DATA_ALEXAMEDIA]['accounts'][email] - ['devices']) = {'media_player': {}} - if 'excluded' not in hass.data[DATA_ALEXAMEDIA]['accounts'][email]: - (hass.data[DATA_ALEXAMEDIA]['accounts'][email] - ['excluded']) = {} - if 'entities' not in hass.data[DATA_ALEXAMEDIA]['accounts'][email]: - (hass.data[DATA_ALEXAMEDIA]['accounts'][email] - ['entities']) = {'media_player': {}} - (hass.data[DATA_ALEXAMEDIA] - ['accounts'][email]['new_devices']) = True # force initial update - (hass.data[DATA_ALEXAMEDIA] - ['accounts'][email]['websocket_lastattempt']) = 0 - (hass.data[DATA_ALEXAMEDIA] - ['accounts'][email]['websocketerror']) = 0 # set errors to 0 - (hass.data[DATA_ALEXAMEDIA]['accounts'][email]['websocket']) = \ - await ws_connect() + if "login_obj" not in hass.data[DATA_ALEXAMEDIA]["accounts"][email]: + (hass.data[DATA_ALEXAMEDIA]["accounts"][email]["login_obj"]) = login_obj + if "devices" not in hass.data[DATA_ALEXAMEDIA]["accounts"][email]: + (hass.data[DATA_ALEXAMEDIA]["accounts"][email]["devices"]) = { + "media_player": {} + } + if "excluded" not in hass.data[DATA_ALEXAMEDIA]["accounts"][email]: + (hass.data[DATA_ALEXAMEDIA]["accounts"][email]["excluded"]) = {} + if "entities" not in hass.data[DATA_ALEXAMEDIA]["accounts"][email]: + (hass.data[DATA_ALEXAMEDIA]["accounts"][email]["entities"]) = { + "media_player": {} + } + ( + hass.data[DATA_ALEXAMEDIA]["accounts"][email]["new_devices"] + ) = True # force initial update + (hass.data[DATA_ALEXAMEDIA]["accounts"][email]["websocket_lastattempt"]) = 0 + ( + hass.data[DATA_ALEXAMEDIA]["accounts"][email]["websocketerror"] + ) = 0 # set errors to 0 + (hass.data[DATA_ALEXAMEDIA]["accounts"][email]["websocket"]) = await ws_connect() await update_devices(login_obj, no_throttle=True) - 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.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 + ) # Clear configurator. We delay till here to avoid leaving a modal orphan await clear_configurator(hass, email) return True @@ -898,36 +1000,38 @@ async def async_unload_entry(hass, entry) -> bool: 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'] + email = entry.data["email"] await close_connections(hass, email) await clear_configurator(hass, email) - hass.data[DATA_ALEXAMEDIA]['accounts'].pop(email) + hass.data[DATA_ALEXAMEDIA]["accounts"].pop(email) _LOGGER.debug("Unloaded entry for %s", hide_email(email)) return True async def clear_configurator(hass, email: Text) -> None: """Clear open configurators for email.""" - if email not in hass.data[DATA_ALEXAMEDIA]['accounts']: + if email not in hass.data[DATA_ALEXAMEDIA]["accounts"]: return - if 'configurator' in hass.data[DATA_ALEXAMEDIA]['accounts'][email]: - for config_id in (hass.data[DATA_ALEXAMEDIA]['accounts'][email] - ['configurator']): + if "configurator" in hass.data[DATA_ALEXAMEDIA]["accounts"][email]: + for config_id in hass.data[DATA_ALEXAMEDIA]["accounts"][email]["configurator"]: configurator = hass.components.configurator configurator.async_request_done(config_id) - hass.data[DATA_ALEXAMEDIA]['accounts'][email]['configurator'] = [] + hass.data[DATA_ALEXAMEDIA]["accounts"][email]["configurator"] = [] 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]): + 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'] + 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) + _LOGGER.debug( + "%s: Connection closed: %s", hide_email(email), login_obj._session.closed + ) await clear_configurator(hass, email) diff --git a/custom_components/alexa_media/alarm_control_panel.py b/custom_components/alexa_media/alarm_control_panel.py index 568d7cd2..76fefa20 100644 --- a/custom_components/alexa_media/alarm_control_panel.py +++ b/custom_components/alexa_media/alarm_control_panel.py @@ -15,9 +15,16 @@ from homeassistant.const import STATE_ALARM_ARMED_AWAY, STATE_ALARM_DISARMED from homeassistant.helpers.event import async_call_later -from . import CONF_EMAIL, CONF_EXCLUDE_DEVICES, CONF_INCLUDE_DEVICES, DATA_ALEXAMEDIA -from . import DOMAIN as ALEXA_DOMAIN -from . import MIN_TIME_BETWEEN_FORCED_SCANS, MIN_TIME_BETWEEN_SCANS, hide_email +from . import ( + CONF_EMAIL, + CONF_EXCLUDE_DEVICES, + CONF_INCLUDE_DEVICES, + DATA_ALEXAMEDIA, + DOMAIN as ALEXA_DOMAIN, + MIN_TIME_BETWEEN_FORCED_SCANS, + MIN_TIME_BETWEEN_SCANS, + hide_email, +) from .helpers import _catch_login_errors, add_devices, retry_async _LOGGER = logging.getLogger(__name__) diff --git a/custom_components/alexa_media/config_flow.py b/custom_components/alexa_media/config_flow.py index 9f7b4e78..ab85b8b1 100644 --- a/custom_components/alexa_media/config_flow.py +++ b/custom_components/alexa_media/config_flow.py @@ -7,21 +7,31 @@ For more details about this platform, please refer to the documentation at https://community.home-assistant.io/t/echo-devices-alexa-as-media-player-testers-needed/58639 """ -import logging from collections import OrderedDict +import logging from typing import Text -import voluptuous as vol from alexapy import AlexapyConnectionError from homeassistant import config_entries -from homeassistant.const import (CONF_EMAIL, CONF_NAME, CONF_PASSWORD, - CONF_SCAN_INTERVAL, CONF_URL, - EVENT_HOMEASSISTANT_STOP) +from homeassistant.const import ( + CONF_EMAIL, + CONF_NAME, + CONF_PASSWORD, + CONF_SCAN_INTERVAL, + CONF_URL, + EVENT_HOMEASSISTANT_STOP, +) from homeassistant.core import callback from homeassistant.helpers import config_validation as cv +import voluptuous as vol -from .const import (CONF_DEBUG, CONF_EXCLUDE_DEVICES, CONF_INCLUDE_DEVICES, - DATA_ALEXAMEDIA, DOMAIN) +from .const import ( + CONF_DEBUG, + CONF_EXCLUDE_DEVICES, + CONF_INCLUDE_DEVICES, + DATA_ALEXAMEDIA, + DOMAIN, +) _LOGGER = logging.getLogger(__name__) @@ -164,8 +174,8 @@ async def async_step_user(self, user_input=None): if isinstance(user_input[CONF_INCLUDE_DEVICES], str): self.config[CONF_INCLUDE_DEVICES] = ( user_input[CONF_INCLUDE_DEVICES].split(",") - if CONF_INCLUDE_DEVICES in user_input and - user_input[CONF_INCLUDE_DEVICES] != "" + if CONF_INCLUDE_DEVICES in user_input + and user_input[CONF_INCLUDE_DEVICES] != "" else [] ) else: @@ -173,8 +183,8 @@ async def async_step_user(self, user_input=None): if isinstance(user_input[CONF_EXCLUDE_DEVICES], str): self.config[CONF_EXCLUDE_DEVICES] = ( user_input[CONF_EXCLUDE_DEVICES].split(",") - if CONF_EXCLUDE_DEVICES in user_input and - user_input[CONF_EXCLUDE_DEVICES] != "" + if CONF_EXCLUDE_DEVICES in user_input + and user_input[CONF_EXCLUDE_DEVICES] != "" else [] ) else: @@ -230,8 +240,7 @@ async def async_step_process(self, user_input=None): try: await self.login.login(data=user_input) except AlexapyConnectionError: - return await self._show_form( - errors={"base": "connection_error"}) + return await self._show_form(errors={"base": "connection_error"}) except BaseException as ex: _LOGGER.warning("Unknown error: %s", ex) return await self._show_form(errors={"base": "unknown_error"}) @@ -276,8 +285,7 @@ async def _test_login(self): ): _LOGGER.debug("Creating config_flow to request 2FA") message = "> {0}".format( - login.status["error_message"] - if "error_message" in login.status else "" + login.status["error_message"] if "error_message" in login.status else "" ) return await self._show_form( "twofactor", @@ -294,8 +302,7 @@ async def _test_login(self): and login.status["claimspicker_required"] ): error_message = "> {0}".format( - login.status["error_message"] - if "error_message" in login.status else "" + login.status["error_message"] if "error_message" in login.status else "" ) _LOGGER.debug("Creating config_flow to select verification method") claimspicker_message = login.status["claimspicker_message"] @@ -306,8 +313,9 @@ async def _test_login(self): placeholders={ "email": login.email, "url": login.url, - "message": "> {0}\n> {1}".format(claimspicker_message, - error_message), + "message": "> {0}\n> {1}".format( + claimspicker_message, error_message + ), }, ) elif ( @@ -316,8 +324,7 @@ async def _test_login(self): ): _LOGGER.debug("Creating config_flow to select OTA method") error_message = ( - login.status["error_message"] - if "error_message" in login.status else "" + login.status["error_message"] if "error_message" in login.status else "" ) authselect_message = login.status["authselect_message"] return await self._show_form( @@ -326,8 +333,7 @@ async def _test_login(self): placeholders={ "email": login.email, "url": login.url, - "message": "> {0}\n> {1}".format(authselect_message, - error_message), + "message": "> {0}\n> {1}".format(authselect_message, error_message), }, ) elif ( diff --git a/custom_components/alexa_media/const.py b/custom_components/alexa_media/const.py index 5f363d28..8bb0b183 100644 --- a/custom_components/alexa_media/const.py +++ b/custom_components/alexa_media/const.py @@ -13,32 +13,26 @@ PROJECT_URL = "https://github.com/custom-components/alexa_media_player/" ISSUE_URL = "{}issues".format(PROJECT_URL) -DOMAIN = 'alexa_media' -DATA_ALEXAMEDIA = 'alexa_media' +DOMAIN = "alexa_media" +DATA_ALEXAMEDIA = "alexa_media" PLAY_SCAN_INTERVAL = 20 SCAN_INTERVAL = timedelta(seconds=60) MIN_TIME_BETWEEN_SCANS = SCAN_INTERVAL MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(seconds=1) -ALEXA_COMPONENTS = [ - 'media_player', - 'notify', - 'alarm_control_panel', - 'switch', - 'sensor' -] +ALEXA_COMPONENTS = ["media_player", "notify", "alarm_control_panel", "switch", "sensor"] -CONF_ACCOUNTS = 'accounts' -CONF_DEBUG = 'debug' -CONF_INCLUDE_DEVICES = 'include_devices' -CONF_EXCLUDE_DEVICES = 'exclude_devices' +CONF_ACCOUNTS = "accounts" +CONF_DEBUG = "debug" +CONF_INCLUDE_DEVICES = "include_devices" +CONF_EXCLUDE_DEVICES = "exclude_devices" -SERVICE_CLEAR_HISTORY = 'clear_history' -SERVICE_UPDATE_LAST_CALLED = 'update_last_called' -ATTR_MESSAGE = 'message' -ATTR_EMAIL = 'email' -ATTR_NUM_ENTRIES = 'entries' +SERVICE_CLEAR_HISTORY = "clear_history" +SERVICE_UPDATE_LAST_CALLED = "update_last_called" +ATTR_MESSAGE = "message" +ATTR_EMAIL = "email" +ATTR_NUM_ENTRIES = "entries" STARTUP = """ ------------------------------------------------------------------- {} @@ -47,4 +41,6 @@ If you have any issues with this you need to open an issue here: {} ------------------------------------------------------------------- -""".format(DOMAIN, __version__, ISSUE_URL) +""".format( + DOMAIN, __version__, ISSUE_URL +) diff --git a/custom_components/alexa_media/helpers.py b/custom_components/alexa_media/helpers.py index 5ca9466b..dd016131 100644 --- a/custom_components/alexa_media/helpers.py +++ b/custom_components/alexa_media/helpers.py @@ -20,19 +20,23 @@ _LOGGER = logging.getLogger(__name__) -async def add_devices(account: Text, - devices: List[EntityComponent], - add_devices_callback: Callable, - include_filter: List[Text] = [], - exclude_filter: List[Text] = []) -> bool: +async def add_devices( + account: Text, + devices: List[EntityComponent], + add_devices_callback: Callable, + include_filter: List[Text] = [], + exclude_filter: List[Text] = [], +) -> bool: """Add devices using add_devices_callback.""" new_devices = [] for device in devices: - if (include_filter and device.name not in include_filter - or exclude_filter and device.name in exclude_filter): - _LOGGER.debug("%s: Excluding device: %s", - account, - device) + if ( + include_filter + and device.name not in include_filter + or exclude_filter + and device.name in exclude_filter + ): + _LOGGER.debug("%s: Excluding device: %s", account, device) continue new_devices.append(device) devices = new_devices @@ -44,30 +48,23 @@ async def add_devices(account: Text, except HomeAssistantError as exception_: message = exception_.message # type: str if message.startswith("Entity id already exists"): - _LOGGER.debug("%s: Device already added: %s", - account, - message) + _LOGGER.debug("%s: Device already added: %s", account, message) else: - _LOGGER.debug("%s: Unable to add devices: %s : %s", - account, - devices, - message) + _LOGGER.debug( + "%s: Unable to add devices: %s : %s", account, devices, message + ) except BaseException as ex: - template = ("An exception of type {0} occurred." - " Arguments:\n{1!r}") + template = "An exception of type {0} occurred." " Arguments:\n{1!r}" message = template.format(type(ex).__name__, ex.args) - _LOGGER.debug("%s: Unable to add devices: %s", - account, - message) + _LOGGER.debug("%s: Unable to add devices: %s", account, message) else: return True return False -def retry_async(limit: int = 5, - delay: float = 1, - catch_exceptions: bool = True - ) -> Callable: +def retry_async( + limit: int = 5, delay: float = 1, catch_exceptions: bool = True +) -> Callable: """Wrap function with retry logic. The function will retry until true or the limit is reached. It will delay @@ -87,22 +84,25 @@ def retry_async(limit: int = 5, Wrapped function. """ + def wrap(func) -> Callable: import functools import asyncio + @functools.wraps(func) async def wrapper(*args, **kwargs) -> Any: _LOGGER.debug( "%s.%s: Trying with limit %s delay %s catch_exceptions %s", - func.__module__[func.__module__.find('.')+1:], + func.__module__[func.__module__.find(".") + 1 :], func.__name__, limit, delay, - catch_exceptions) + catch_exceptions, + ) retries: int = 0 result: bool = False next_try: int = 0 - while (not result and retries < limit): + while not result and retries < limit: if retries != 0: next_try = delay * 2 ** retries await asyncio.sleep(next_try) @@ -112,75 +112,75 @@ async def wrapper(*args, **kwargs) -> Any: except Exception as ex: # pylint: disable=broad-except if not catch_exceptions: raise - template = ("An exception of type {0} occurred." - " Arguments:\n{1!r}") + template = "An exception of type {0} occurred." " Arguments:\n{1!r}" 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.__module__[func.__module__.find(".") + 1 :], func.__name__, - message) + message, + ) _LOGGER.debug( "%s.%s: Try: %s/%s after waiting %s seconds result: %s", - func.__module__[func.__module__.find('.')+1:], + func.__module__[func.__module__.find(".") + 1 :], func.__name__, retries, limit, next_try, - result - ) + 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}") + 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) + _LOGGER.debug( + "%s.%s: detected bad login: %s", + func.__module__[func.__module__.find(".") + 1 :], + func.__name__, + message, + ) instance = args[0] - if hasattr(instance, '_login'): + 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']) - callback = ( - hass.data[DATA_ALEXAMEDIA] - ['accounts'] - [email] - ['setup_platform_callback']) - test_login_status = ( - hass.data[DATA_ALEXAMEDIA] - ['accounts'] - [email] - ['test_login_status']) + 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" + ] + 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)) + hide_email(email), + ) await login.login_with_cookie() - await test_login_status(hass, - config_entry, login, - callback) + 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 11a04ffd..e419b590 100644 --- a/custom_components/alexa_media/media_player.py +++ b/custom_components/alexa_media/media_player.py @@ -14,76 +14,93 @@ from homeassistant import util from homeassistant.components.media_player import MediaPlayerDevice from homeassistant.components.media_player.const import ( - MEDIA_TYPE_MUSIC, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY, - SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK, SUPPORT_SELECT_SOURCE, - SUPPORT_SHUFFLE_SET, SUPPORT_STOP, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, - SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET) -from homeassistant.const import (STATE_IDLE, STATE_PAUSED, STATE_PLAYING, - STATE_STANDBY) + MEDIA_TYPE_MUSIC, + SUPPORT_NEXT_TRACK, + SUPPORT_PAUSE, + SUPPORT_PLAY, + SUPPORT_PLAY_MEDIA, + SUPPORT_PREVIOUS_TRACK, + SUPPORT_SELECT_SOURCE, + SUPPORT_SHUFFLE_SET, + SUPPORT_STOP, + SUPPORT_TURN_OFF, + SUPPORT_TURN_ON, + SUPPORT_VOLUME_MUTE, + SUPPORT_VOLUME_SET, +) +from homeassistant.const import STATE_IDLE, STATE_PAUSED, STATE_PLAYING, STATE_STANDBY from homeassistant.helpers.event import async_call_later -from . import CONF_EMAIL, DATA_ALEXAMEDIA -from . import DOMAIN as ALEXA_DOMAIN -from . import (MIN_TIME_BETWEEN_FORCED_SCANS, MIN_TIME_BETWEEN_SCANS, - hide_email, hide_serial) +from . import ( + CONF_EMAIL, + DATA_ALEXAMEDIA, + DOMAIN as ALEXA_DOMAIN, + MIN_TIME_BETWEEN_FORCED_SCANS, + MIN_TIME_BETWEEN_SCANS, + hide_email, + hide_serial, +) from .const import PLAY_SCAN_INTERVAL from .helpers import _catch_login_errors, add_devices, retry_async -SUPPORT_ALEXA = (SUPPORT_PAUSE | SUPPORT_PREVIOUS_TRACK | - SUPPORT_NEXT_TRACK | SUPPORT_STOP | - SUPPORT_VOLUME_SET | SUPPORT_PLAY | - SUPPORT_PLAY_MEDIA | SUPPORT_TURN_OFF | SUPPORT_TURN_ON | - SUPPORT_VOLUME_MUTE | SUPPORT_PAUSE | - SUPPORT_SELECT_SOURCE | SUPPORT_SHUFFLE_SET) +SUPPORT_ALEXA = ( + SUPPORT_PAUSE + | SUPPORT_PREVIOUS_TRACK + | SUPPORT_NEXT_TRACK + | SUPPORT_STOP + | SUPPORT_VOLUME_SET + | SUPPORT_PLAY + | SUPPORT_PLAY_MEDIA + | SUPPORT_TURN_OFF + | SUPPORT_TURN_ON + | SUPPORT_VOLUME_MUTE + | SUPPORT_PAUSE + | SUPPORT_SELECT_SOURCE + | SUPPORT_SHUFFLE_SET +) _LOGGER = logging.getLogger(__name__) DEPENDENCIES = [ALEXA_DOMAIN] @retry_async(limit=5, delay=2, catch_exceptions=True) -async def async_setup_platform(hass, config, add_devices_callback, - discovery_info=None): +async def async_setup_platform(hass, config, add_devices_callback, discovery_info=None): """Set up the Alexa media player platform.""" devices = [] # type: List[AlexaClient] account = config[CONF_EMAIL] - account_dict = hass.data[DATA_ALEXAMEDIA]['accounts'][account] - for key, device in account_dict['devices']['media_player'].items(): - if key not in account_dict['entities']['media_player']: - alexa_client = AlexaClient(device, - account_dict['login_obj'] - ) + account_dict = hass.data[DATA_ALEXAMEDIA]["accounts"][account] + for key, device in account_dict["devices"]["media_player"].items(): + if key not in account_dict["entities"]["media_player"]: + alexa_client = AlexaClient(device, account_dict["login_obj"]) await alexa_client.init(device) devices.append(alexa_client) - (hass.data[DATA_ALEXAMEDIA] - ['accounts'] - [account] - ['entities'] - ['media_player'][key]) = alexa_client + ( + hass.data[DATA_ALEXAMEDIA]["accounts"][account]["entities"][ + "media_player" + ][key] + ) = alexa_client else: - _LOGGER.debug("%s: Skipping already added device: %s:%s", - hide_email(account), - hide_serial(key), - alexa_client - ) - return await add_devices(hide_email(account), - devices, - add_devices_callback) + _LOGGER.debug( + "%s: Skipping already added device: %s:%s", + hide_email(account), + hide_serial(key), + alexa_client, + ) + return await add_devices(hide_email(account), devices, add_devices_callback) async def async_setup_entry(hass, config_entry, async_add_devices): """Set up the Alexa media player platform by config_entry.""" return await async_setup_platform( - hass, - config_entry.data, - async_add_devices, - discovery_info=None) + hass, config_entry.data, async_add_devices, discovery_info=None + ) async def async_unload_entry(hass, entry) -> bool: """Unload a config entry.""" account = entry.data[CONF_EMAIL] - account_dict = hass.data[DATA_ALEXAMEDIA]['accounts'][account] - for device in account_dict['entities']['media_player'].values(): + account_dict = hass.data[DATA_ALEXAMEDIA]["accounts"][account] + for device in account_dict["entities"]["media_player"].values(): await device.async_remove() return True @@ -154,8 +171,8 @@ async def async_added_to_hass(self): """Perform tasks after loading.""" # Register event handler on bus self._listener = self.hass.bus.async_listen( - f'{ALEXA_DOMAIN}_{hide_email(self._login.email)}'[0:32], - self._handle_event) + f"{ALEXA_DOMAIN}_{hide_email(self._login.email)}"[0:32], self._handle_event + ) async def async_will_remove_from_hass(self): """Prepare to remove entity.""" @@ -178,23 +195,33 @@ async def _handle_event(self, event): is self.update will pull data from Amazon, while schedule_update assumes the MediaClient state is already updated. """ + async def _refresh_if_no_audiopush(already_refreshed=False): email = self._login.email - seen_commands = ((self.hass.data[DATA_ALEXAMEDIA]['accounts'] - [email]['websocket_commands'].keys() - if 'websocket_commands' in ( - self.hass.data[DATA_ALEXAMEDIA] - ['accounts'] - [email]) else None)) - if (not already_refreshed and seen_commands and - ('PUSH_AUDIO_PLAYER_STATE' not in seen_commands - and 'PUSH_MEDIA_CHANGE' not in seen_commands)): + seen_commands = ( + self.hass.data[DATA_ALEXAMEDIA]["accounts"][email][ + "websocket_commands" + ].keys() + if "websocket_commands" + in (self.hass.data[DATA_ALEXAMEDIA]["accounts"][email]) + else None + ) + if ( + not already_refreshed + and seen_commands + and ( + "PUSH_AUDIO_PLAYER_STATE" not in seen_commands + and "PUSH_MEDIA_CHANGE" not in seen_commands + ) + ): # force refresh if player_state update not found, see #397 _LOGGER.debug( "%s: No PUSH_AUDIO_PLAYER_STATE in %s; forcing refresh", hide_email(email), - seen_commands) + seen_commands, + ) await self.async_update() + try: if not self.enabled: return @@ -202,103 +229,126 @@ async def _refresh_if_no_audiopush(already_refreshed=False): pass already_refreshed = False event_serial = None - if 'last_called_change' in event.data: - event_serial = (event.data['last_called_change']['serialNumber'] - if event.data['last_called_change'] else None) - elif 'bluetooth_change' in event.data: + if "last_called_change" in event.data: + event_serial = ( + event.data["last_called_change"]["serialNumber"] + if event.data["last_called_change"] + else None + ) + elif "bluetooth_change" in event.data: event_serial = ( - event.data['bluetooth_change']['deviceSerialNumber'] - if event.data['bluetooth_change'] else None) - elif 'player_state' in event.data: - event_serial = (event.data['player_state']['dopplerId'] - ['deviceSerialNumber'] - if event.data['player_state'] else None) - elif 'queue_state' in event.data: - event_serial = (event.data['queue_state']['dopplerId'] - ['deviceSerialNumber'] - if event.data['queue_state'] else None) + event.data["bluetooth_change"]["deviceSerialNumber"] + if event.data["bluetooth_change"] + else None + ) + elif "player_state" in event.data: + event_serial = ( + event.data["player_state"]["dopplerId"]["deviceSerialNumber"] + if event.data["player_state"] + else None + ) + elif "queue_state" in event.data: + event_serial = ( + event.data["queue_state"]["dopplerId"]["deviceSerialNumber"] + if event.data["queue_state"] + else None + ) if not event_serial: return self._available = True self.async_schedule_update_ha_state() - if 'last_called_change' in event.data: - if (event_serial == self.device_serial_number or - any(item['serialNumber'] == - event_serial for item in self._app_device_list)): - _LOGGER.debug("%s is last_called: %s", self.name, - hide_serial(self.device_serial_number)) + if "last_called_change" in event.data: + if event_serial == self.device_serial_number or any( + item["serialNumber"] == event_serial for item in self._app_device_list + ): + _LOGGER.debug( + "%s is last_called: %s", + self.name, + hide_serial(self.device_serial_number), + ) self._last_called = True else: self._last_called = False - if (self.hass and self.async_schedule_update_ha_state): + if self.hass and self.async_schedule_update_ha_state: email = self._login.email - force_refresh = not (self.hass.data[DATA_ALEXAMEDIA] - ['accounts'][email]['websocket']) - self.async_schedule_update_ha_state( - force_refresh=force_refresh) - elif 'bluetooth_change' in event.data: + force_refresh = not ( + self.hass.data[DATA_ALEXAMEDIA]["accounts"][email]["websocket"] + ) + self.async_schedule_update_ha_state(force_refresh=force_refresh) + elif "bluetooth_change" in event.data: if event_serial == self.device_serial_number: - _LOGGER.debug("%s bluetooth_state update: %s", - self.name, - hide_serial(event.data['bluetooth_change'])) - self._bluetooth_state = event.data['bluetooth_change'] + _LOGGER.debug( + "%s bluetooth_state update: %s", + self.name, + hide_serial(event.data["bluetooth_change"]), + ) + self._bluetooth_state = event.data["bluetooth_change"] # the setting of bluetooth_state is not consistent as this # takes from the event instead of the hass storage. We're # setting the value twice. Architectually we should have a # single authorative source of truth. self._source = await self._get_source() self._source_list = await self._get_source_list() - if (self.hass and self.async_schedule_update_ha_state): + if self.hass and self.async_schedule_update_ha_state: self.async_schedule_update_ha_state() - elif 'player_state' in event.data: - player_state = event.data['player_state'] + elif "player_state" in event.data: + player_state = event.data["player_state"] if event_serial == self.device_serial_number: - if 'audioPlayerState' in player_state: - _LOGGER.debug("%s state update: %s", - self.name, - player_state['audioPlayerState']) + if "audioPlayerState" in player_state: + _LOGGER.debug( + "%s state update: %s", + self.name, + player_state["audioPlayerState"], + ) await self.async_update() already_refreshed = True # refresh is necessary to pull all data - elif 'mediaReferenceId' in player_state: - _LOGGER.debug("%s media update: %s", - self.name, - player_state['mediaReferenceId']) + elif "mediaReferenceId" in player_state: + _LOGGER.debug( + "%s media update: %s", + self.name, + player_state["mediaReferenceId"], + ) await self.async_update() already_refreshed = True # refresh is necessary to pull all data - elif 'volumeSetting' in player_state: - _LOGGER.debug("%s volume updated: %s", - self.name, - player_state['volumeSetting']) - self._media_vol_level = player_state['volumeSetting']/100 - if (self.hass and self.async_schedule_update_ha_state): + elif "volumeSetting" in player_state: + _LOGGER.debug( + "%s volume updated: %s", + self.name, + player_state["volumeSetting"], + ) + self._media_vol_level = player_state["volumeSetting"] / 100 + if self.hass and self.async_schedule_update_ha_state: self.async_schedule_update_ha_state() - elif 'dopplerConnectionState' in player_state: - self._available = (player_state['dopplerConnectionState'] - == "ONLINE") - if (self.hass and self.async_schedule_update_ha_state): + elif "dopplerConnectionState" in player_state: + self._available = player_state["dopplerConnectionState"] == "ONLINE" + if self.hass and self.async_schedule_update_ha_state: self.async_schedule_update_ha_state() await _refresh_if_no_audiopush(already_refreshed) - if 'queue_state' in event.data: - queue_state = event.data['queue_state'] + if "queue_state" in event.data: + queue_state = event.data["queue_state"] if event_serial == self.device_serial_number: - if ('trackOrderChanged' in queue_state and - not queue_state['trackOrderChanged'] and - 'loopMode' in queue_state): - self._repeat = (queue_state['loopMode'] - == 'LOOP_QUEUE') - _LOGGER.debug("%s repeat updated to: %s %s", - self.name, - self._repeat, - queue_state['loopMode']) - elif 'playBackOrder' in queue_state: - self._shuffle = (queue_state['playBackOrder'] - == 'SHUFFLE_ALL') - _LOGGER.debug("%s shuffle updated to: %s %s", - self.name, - self._shuffle, - queue_state['playBackOrder']) + if ( + "trackOrderChanged" in queue_state + and not queue_state["trackOrderChanged"] + and "loopMode" in queue_state + ): + self._repeat = queue_state["loopMode"] == "LOOP_QUEUE" + _LOGGER.debug( + "%s repeat updated to: %s %s", + self.name, + self._repeat, + queue_state["loopMode"], + ) + elif "playBackOrder" in queue_state: + self._shuffle = queue_state["playBackOrder"] == "SHUFFLE_ALL" + _LOGGER.debug( + "%s shuffle updated to: %s %s", + self.name, + self._shuffle, + queue_state["playBackOrder"], + ) await _refresh_if_no_audiopush(already_refreshed) async def _clear_media_details(self): @@ -317,11 +367,11 @@ async def _clear_media_details(self): async def _set_authentication_details(self, auth): """Set Authentication based off auth.""" - self._authenticated = auth['authenticated'] - self._can_access_prime_music = auth['canAccessPrimeMusicContent'] - self._customer_email = auth['customerEmail'] - self._customer_id = auth['customerId'] - self._customer_name = auth['customerName'] + self._authenticated = auth["authenticated"] + self._can_access_prime_music = auth["canAccessPrimeMusicContent"] + self._customer_email = auth["customerEmail"] + self._customer_id = auth["customerId"] + self._customer_name = auth["customerName"] @util.Throttle(MIN_TIME_BETWEEN_SCANS, MIN_TIME_BETWEEN_FORCED_SCANS) @_catch_login_errors @@ -340,35 +390,41 @@ async def refresh(self, device=None): """ if device is not None: self._device = device - self._device_name = device['accountName'] - self._device_family = device['deviceFamily'] - self._device_type = device['deviceType'] - self._device_serial_number = device['serialNumber'] - self._app_device_list = device['appDeviceList'] - self._device_owner_customer_id = device['deviceOwnerCustomerId'] - self._software_version = device['softwareVersion'] - self._available = device['online'] - self._capabilities = device['capabilities'] - self._cluster_members = device['clusterMembers'] - self._parent_clusters = device['parentClusters'] - self._bluetooth_state = device['bluetooth_state'] - self._locale = device['locale'] if 'locale' in device else 'en-US' - self._timezone = (device['timeZoneId'] - if 'timeZoneId' in device else 'UTC') - self._dnd = device['dnd'] if 'dnd' in device else None - await self._set_authentication_details(device['auth_info']) + self._device_name = device["accountName"] + self._device_family = device["deviceFamily"] + self._device_type = device["deviceType"] + self._device_serial_number = device["serialNumber"] + self._app_device_list = device["appDeviceList"] + self._device_owner_customer_id = device["deviceOwnerCustomerId"] + self._software_version = device["softwareVersion"] + self._available = device["online"] + self._capabilities = device["capabilities"] + self._cluster_members = device["clusterMembers"] + self._parent_clusters = device["parentClusters"] + self._bluetooth_state = device["bluetooth_state"] + self._locale = device["locale"] if "locale" in device else "en-US" + self._timezone = device["timeZoneId"] if "timeZoneId" in device else "UTC" + self._dnd = device["dnd"] if "dnd" in device else None + await self._set_authentication_details(device["auth_info"]) session = None if self._available: _LOGGER.debug("%s: Refreshing %s", self.account, self.name) if self._parent_clusters and self.hass: - playing_parents = list(filter( - lambda x: ( - self.hass.data[DATA_ALEXAMEDIA] - ['accounts'] - [self._login.email] - ['entities'] - ['media_player'][x].state == STATE_PLAYING), - self._parent_clusters)) + playing_parents = list( + filter( + lambda x: ( + self.hass.data[DATA_ALEXAMEDIA]["accounts"][ + self._login.email + ]["entities"]["media_player"].get(x) + and + self.hass.data[DATA_ALEXAMEDIA]["accounts"][ + self._login.email + ]["entities"]["media_player"][x].state + == STATE_PLAYING + ), + self._parent_clusters, + ) + ) else: playing_parents = [] if "PAIR_BT_SOURCE" in self._capabilities: @@ -380,15 +436,11 @@ async def refresh(self, device=None): if playing_parents: if len(playing_parents) > 1: _LOGGER.warning( - "Found multiple playing parents " - "please file an issue") - parent = ( - self.hass.data[DATA_ALEXAMEDIA] - ['accounts'] - [self._login.email] - ['entities'] - ['media_player'][playing_parents[0]] - ) + "Found multiple playing parents " "please file an issue" + ) + parent = self.hass.data[DATA_ALEXAMEDIA]["accounts"][ + self._login.email + ]["entities"]["media_player"][playing_parents[0]] self._playing_parent = parent parent_session = parent.session if parent_session: @@ -396,11 +448,13 @@ async def refresh(self, device=None): session["isPlayingInLemur"] = False session["lemurVolume"] = None session["volume"] = ( - parent_session["lemurVolume"]["memberVolume"] - [self.device_serial_number] if - parent_session["lemurVolume"] + parent_session["lemurVolume"]["memberVolume"][ + self.device_serial_number + ] + if parent_session["lemurVolume"] and "memberVolume" in parent_session["lemurVolume"] - else session["volume"]) + else session["volume"] + ) session = {"playerInfo": session} else: self._playing_parent = None @@ -409,122 +463,143 @@ async def refresh(self, device=None): # update the session if it exists; not doing relogin here if session: self._session = session - if self._session and 'playerInfo' in self._session: - self._session = self._session['playerInfo'] - if self._session['transport'] is not None: - self._shuffle = (self._session['transport'] - ['shuffle'] == "SELECTED" - if ('shuffle' in self._session['transport'] - and self._session['transport']['shuffle'] - != 'DISABLED') - else None) - self._repeat = (self._session['transport'] - ['repeat'] == "SELECTED" - if ('repeat' in self._session['transport'] - and self._session['transport']['repeat'] - != 'DISABLED') - else None) - if self._session['state'] is not None: - self._media_player_state = self._session['state'] - self._media_pos = (self._session['progress']['mediaProgress'] - if (self._session['progress'] is not None - and 'mediaProgress' in - self._session['progress']) - else None) - self._media_title = (self._session['infoText']['title'] - if (self._session['infoText'] is not None - and 'title' in - self._session['infoText']) - else None) - self._media_artist = (self._session['infoText']['subText1'] - if (self._session['infoText'] is not None - and 'subText1' in - self._session['infoText']) - else None) - self._media_album_name = (self._session['infoText']['subText2'] - if (self._session['infoText'] is not - None and 'subText2' in - self._session['infoText']) - else None) - self._media_image_url = (self._session['mainArt']['url'] - if (self._session['mainArt'] is not - None and 'url' in - self._session['mainArt']) - else None) - self._media_duration = (self._session['progress'] - ['mediaLength'] - if (self._session['progress'] is not - None and 'mediaLength' in - self._session['progress']) - else None) + if self._session and "playerInfo" in self._session: + self._session = self._session["playerInfo"] + if self._session["transport"] is not None: + self._shuffle = ( + self._session["transport"]["shuffle"] == "SELECTED" + if ( + "shuffle" in self._session["transport"] + and self._session["transport"]["shuffle"] != "DISABLED" + ) + else None + ) + self._repeat = ( + self._session["transport"]["repeat"] == "SELECTED" + if ( + "repeat" in self._session["transport"] + and self._session["transport"]["repeat"] != "DISABLED" + ) + else None + ) + if self._session["state"] is not None: + self._media_player_state = self._session["state"] + self._media_pos = ( + self._session["progress"]["mediaProgress"] + if ( + self._session["progress"] is not None + and "mediaProgress" in self._session["progress"] + ) + else None + ) + self._media_title = ( + self._session["infoText"]["title"] + if ( + self._session["infoText"] is not None + and "title" in self._session["infoText"] + ) + else None + ) + self._media_artist = ( + self._session["infoText"]["subText1"] + if ( + self._session["infoText"] is not None + and "subText1" in self._session["infoText"] + ) + else None + ) + self._media_album_name = ( + self._session["infoText"]["subText2"] + if ( + self._session["infoText"] is not None + and "subText2" in self._session["infoText"] + ) + else None + ) + self._media_image_url = ( + self._session["mainArt"]["url"] + if ( + self._session["mainArt"] is not None + and "url" in self._session["mainArt"] + ) + else None + ) + self._media_duration = ( + self._session["progress"]["mediaLength"] + if ( + self._session["progress"] is not None + and "mediaLength" in self._session["progress"] + ) + else None + ) if not self._session["lemurVolume"]: self._media_is_muted = ( - self._session['volume']['muted'] - if (self._session['volume'] is not None - and 'muted' in - self._session['volume']) - else None) + self._session["volume"]["muted"] + if ( + self._session["volume"] is not None + and "muted" in self._session["volume"] + ) + else None + ) self._media_vol_level = ( - self._session['volume'] - ['volume'] / 100 + self._session["volume"]["volume"] / 100 if ( - self._session['volume'] is not None - and 'volume' in - self._session['volume']) - else self._media_vol_level) + self._session["volume"] is not None + and "volume" in self._session["volume"] + ) + else self._media_vol_level + ) else: self._media_is_muted = ( - self._session['lemurVolume']['compositeVolume']['muted'] - if (self._session['lemurVolume'] and - 'compositeVolume' in self._session['lemurVolume'] - and - self._session['lemurVolume']['compositeVolume'] - and 'muted' in - self._session['lemurVolume']['compositeVolume']) - else None) + self._session["lemurVolume"]["compositeVolume"]["muted"] + if ( + self._session["lemurVolume"] + and "compositeVolume" in self._session["lemurVolume"] + and self._session["lemurVolume"]["compositeVolume"] + and "muted" + in self._session["lemurVolume"]["compositeVolume"] + ) + else None + ) self._media_vol_level = ( - self._session['lemurVolume']['compositeVolume'] - ['volume'] / 100 + self._session["lemurVolume"]["compositeVolume"]["volume"] / 100 if ( - self._session['lemurVolume'] and - 'compositeVolume' in self._session['lemurVolume'] - and - 'volume' in - self._session['lemurVolume']['compositeVolume'] - and - (self._session['lemurVolume']['compositeVolume'] - ['volume'] - ) + self._session["lemurVolume"] + and "compositeVolume" in self._session["lemurVolume"] + and "volume" + in self._session["lemurVolume"]["compositeVolume"] + and ( + self._session["lemurVolume"]["compositeVolume"][ + "volume" + ] + ) ) - else self._media_vol_level) + else self._media_vol_level + ) if not self.hass: return asyncio.gather( *map( lambda x: ( - self.hass.data[DATA_ALEXAMEDIA] - ['accounts'] - [self._login.email] - ['entities'] - ['media_player'][x].async_update()), + self.hass.data[DATA_ALEXAMEDIA]["accounts"][ + self._login.email + ]["entities"]["media_player"][x].async_update() + ), filter( lambda x: ( - x in ( - self.hass.data[DATA_ALEXAMEDIA] - ['accounts'] - [self._login.email] - ['entities'] - ['media_player']) - and - self.hass.data[DATA_ALEXAMEDIA] - ['accounts'] - [self._login.email] - ['entities'] - ['media_player'][x].available - ), - self._cluster_members - ) - ) + x + in ( + self.hass.data[DATA_ALEXAMEDIA]["accounts"][ + self._login.email + ]["entities"]["media_player"] + ) + and self.hass.data[DATA_ALEXAMEDIA]["accounts"][ + self._login.email + ]["entities"]["media_player"][x].available + ), + self._cluster_members, + ), + ) ) @property @@ -540,54 +615,64 @@ def source_list(self): @_catch_login_errors async def async_select_source(self, source): """Select input source.""" - if source == 'Local Speaker': + if source == "Local Speaker": await self.alexa_api.disconnect_bluetooth() - self._source = 'Local Speaker' - elif self._bluetooth_state['pairedDeviceList'] is not None: - for devices in self._bluetooth_state['pairedDeviceList']: - if devices['friendlyName'] == source: - await self.alexa_api.set_bluetooth(devices['address']) + self._source = "Local Speaker" + elif self._bluetooth_state["pairedDeviceList"] is not None: + for devices in self._bluetooth_state["pairedDeviceList"]: + if devices["friendlyName"] == source: + await self.alexa_api.set_bluetooth(devices["address"]) self._source = source - if not (self.hass.data[DATA_ALEXAMEDIA] - ['accounts'][self._login.email]['websocket']): + if not ( + self.hass.data[DATA_ALEXAMEDIA]["accounts"][self._login.email]["websocket"] + ): await self.async_update() async def _get_source(self): - source = 'Local Speaker' - if self._bluetooth_state['pairedDeviceList'] is not None: - for device in self._bluetooth_state['pairedDeviceList']: - if (device['connected'] is True and - device['friendlyName'] in self.source_list): - return device['friendlyName'] + source = "Local Speaker" + if self._bluetooth_state["pairedDeviceList"] is not None: + for device in self._bluetooth_state["pairedDeviceList"]: + if ( + device["connected"] is True + and device["friendlyName"] in self.source_list + ): + return device["friendlyName"] return source async def _get_source_list(self): sources = [] - if self._bluetooth_state['pairedDeviceList'] is not None: - for devices in self._bluetooth_state['pairedDeviceList']: - if (devices['profiles'] and - 'A2DP-SOURCE' in devices['profiles']): - sources.append(devices['friendlyName']) - return ['Local Speaker'] + sources + if self._bluetooth_state["pairedDeviceList"] is not None: + for devices in self._bluetooth_state["pairedDeviceList"]: + if devices["profiles"] and "A2DP-SOURCE" in devices["profiles"]: + sources.append(devices["friendlyName"]) + return ["Local Speaker"] + sources async def _get_last_called(self): try: - last_called_serial = (None if self.hass is None else - (self.hass.data[DATA_ALEXAMEDIA] - ['accounts'] - [self._login.email] - ['last_called'] - ['serialNumber'])) + last_called_serial = ( + None + if self.hass is None + else ( + self.hass.data[DATA_ALEXAMEDIA]["accounts"][self._login.email][ + "last_called" + ]["serialNumber"] + ) + ) except (TypeError, KeyError): last_called_serial = None - _LOGGER.debug("%s: Last_called check: self: %s reported: %s", - self._device_name, - hide_serial(self._device_serial_number), - hide_serial(last_called_serial)) - return (last_called_serial is not None and - (self._device_serial_number == last_called_serial or - any(item['serialNumber'] == - last_called_serial for item in self._app_device_list))) + _LOGGER.debug( + "%s: Last_called check: self: %s reported: %s", + self._device_name, + hide_serial(self._device_serial_number), + hide_serial(last_called_serial), + ) + return last_called_serial is not None and ( + self._device_serial_number == last_called_serial + or any( + item["serialNumber"] == last_called_serial + for item in self._app_device_list + ) + ) @property def available(self): @@ -622,11 +707,11 @@ def session(self): @property def state(self): """Return the state of the device.""" - if self._media_player_state == 'PLAYING': + if self._media_player_state == "PLAYING": return STATE_PLAYING - if self._media_player_state == 'PAUSED': + if self._media_player_state == "PAUSED": return STATE_PAUSED - if self._media_player_state == 'IDLE': + if self._media_player_state == "IDLE": return STATE_IDLE return STATE_STANDBY @@ -645,58 +730,70 @@ async def async_update(self): return except AttributeError: pass - if (self._device is None or self.entity_id is None): + if self._device is None or self.entity_id is None: # Device has not initialized yet return email = self._login.email - if email not in self.hass.data[DATA_ALEXAMEDIA]['accounts']: + if email not in self.hass.data[DATA_ALEXAMEDIA]["accounts"]: return - device = (self.hass.data[DATA_ALEXAMEDIA] - ['accounts'] - [email] - ['devices'] - ['media_player'] - [self.unique_id]) - seen_commands = ((self.hass.data[DATA_ALEXAMEDIA]['accounts'] - [email]['websocket_commands'].keys() - if 'websocket_commands' in ( - self.hass.data[DATA_ALEXAMEDIA] - ['accounts'] - [email]) else None)) - await self.refresh(device, # pylint: disable=unexpected-keyword-arg - no_throttle=True) - if (self.state in [STATE_PLAYING] and - # only enable polling if websocket not connected - (not self.hass.data[DATA_ALEXAMEDIA] - ['accounts'][email]['websocket'] or - # or if no PUSH_AUDIO_PLAYER_STATE - not seen_commands or - 'PUSH_AUDIO_PLAYER_STATE' not in seen_commands)): + device = self.hass.data[DATA_ALEXAMEDIA]["accounts"][email]["devices"][ + "media_player" + ][self.unique_id] + seen_commands = ( + self.hass.data[DATA_ALEXAMEDIA]["accounts"][email][ + "websocket_commands" + ].keys() + if "websocket_commands" + in (self.hass.data[DATA_ALEXAMEDIA]["accounts"][email]) + else None + ) + await self.refresh( + device, no_throttle=True # pylint: disable=unexpected-keyword-arg + ) + if ( + self.state in [STATE_PLAYING] + and + # only enable polling if websocket not connected + ( + not self.hass.data[DATA_ALEXAMEDIA]["accounts"][email]["websocket"] + or + # or if no PUSH_AUDIO_PLAYER_STATE + not seen_commands + or "PUSH_AUDIO_PLAYER_STATE" not in seen_commands + ) + ): self._should_poll = False # disable polling since manual update - if(self._last_update == 0 or util.dt.as_timestamp(util.utcnow()) - - util.dt.as_timestamp(self._last_update) - > PLAY_SCAN_INTERVAL): - _LOGGER.debug("%s playing; scheduling update in %s seconds", - self.name, PLAY_SCAN_INTERVAL) - async_call_later(self.hass, PLAY_SCAN_INTERVAL, lambda _: - self.async_schedule_update_ha_state( - force_refresh=True)) + if ( + self._last_update == 0 + or util.dt.as_timestamp(util.utcnow()) + - util.dt.as_timestamp(self._last_update) + > PLAY_SCAN_INTERVAL + ): + _LOGGER.debug( + "%s playing; scheduling update in %s seconds", + self.name, + PLAY_SCAN_INTERVAL, + ) + async_call_later( + self.hass, + PLAY_SCAN_INTERVAL, + lambda _: self.async_schedule_update_ha_state(force_refresh=True), + ) elif self._should_poll: # Not playing, one last poll self._should_poll = False - if not (self.hass.data[DATA_ALEXAMEDIA] - ['accounts'][email]['websocket']): - _LOGGER.debug("Disabling polling and scheduling last update in" - " 300 seconds for %s", - self.name) + if not (self.hass.data[DATA_ALEXAMEDIA]["accounts"][email]["websocket"]): + _LOGGER.debug( + "Disabling polling and scheduling last update in" + " 300 seconds for %s", + self.name, + ) async_call_later( self.hass, 300, - lambda _: - self.async_schedule_update_ha_state( - force_refresh=True)) + lambda _: self.async_schedule_update_ha_state(force_refresh=True), + ) else: - _LOGGER.debug("Disabling polling for %s", - self.name) + _LOGGER.debug("Disabling polling for %s", self.name) self._last_update = util.utcnow() self.async_schedule_update_ha_state() @@ -800,8 +897,9 @@ async def async_set_volume_level(self, volume): return await self.alexa_api.set_volume(volume) self._media_vol_level = volume - if not (self.hass.data[DATA_ALEXAMEDIA] - ['accounts'][self._login.email]['websocket']): + if not ( + self.hass.data[DATA_ALEXAMEDIA]["accounts"][self._login.email]["websocket"] + ): await self.async_update() @property @@ -836,36 +934,37 @@ async def async_mute_volume(self, mute): await self.alexa_api.set_volume(self._previous_volume) else: await self.alexa_api.set_volume(50) - if not (self.hass.data[DATA_ALEXAMEDIA] - ['accounts'][self._login.email]['websocket']): + if not ( + self.hass.data[DATA_ALEXAMEDIA]["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] - and self.available): + if not (self.state in [STATE_PLAYING, STATE_PAUSED] and self.available): return if self._playing_parent: await self._playing_parent.async_media_play() else: await self.alexa_api.play() - if not (self.hass.data[DATA_ALEXAMEDIA] - ['accounts'][self._login.email]['websocket']): + if not ( + self.hass.data[DATA_ALEXAMEDIA]["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] - and self.available): + if not (self.state in [STATE_PLAYING, STATE_PAUSED] and self.available): return if self._playing_parent: await self._playing_parent.async_media_pause() else: await self.alexa_api.pause() - if not (self.hass.data[DATA_ALEXAMEDIA] - ['accounts'][self._login.email]['websocket']): + if not ( + self.hass.data[DATA_ALEXAMEDIA]["accounts"][self._login.email]["websocket"] + ): await self.async_update() @_catch_login_errors @@ -892,29 +991,29 @@ async def async_turn_on(self): @_catch_login_errors async def async_media_next_track(self): """Send next track command.""" - if not (self.state in [STATE_PLAYING, STATE_PAUSED] - and self.available): + if not (self.state in [STATE_PLAYING, STATE_PAUSED] and self.available): return if self._playing_parent: await self._playing_parent.async_media_next_track() else: await self.alexa_api.next() - if not (self.hass.data[DATA_ALEXAMEDIA] - ['accounts'][self._login.email]['websocket']): + if not ( + self.hass.data[DATA_ALEXAMEDIA]["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] - and self.available): + if not (self.state in [STATE_PLAYING, STATE_PAUSED] and self.available): return if self._playing_parent: await self._playing_parent.async_media_previous_track() else: await self.alexa_api.previous() - if not (self.hass.data[DATA_ALEXAMEDIA] - ['accounts'][self._login.email]['websocket']): + if not ( + self.hass.data[DATA_ALEXAMEDIA]["accounts"][self._login.email]["websocket"] + ): await self.async_update() @_catch_login_errors @@ -928,51 +1027,49 @@ async def async_send_tts(self, message): @_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) + 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) + 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): + async def async_play_media(self, media_type, media_id, enqueue=None, **kwargs): """Send the play_media command to the media player.""" if media_type == "music": await self.async_send_tts( "Sorry, text to speech can only be called" " with the notify.alexa_media service." - " Please see the alexa_media wiki for details.") + " Please see the alexa_media wiki for details." + ) elif media_type == "sequence": - await self.alexa_api.send_sequence(media_id, - customer_id=self._customer_id, - **kwargs) + await self.alexa_api.send_sequence( + media_id, customer_id=self._customer_id, **kwargs + ) elif media_type == "routine": await self.alexa_api.run_routine(media_id) elif media_type == "sound": await self.alexa_api.play_sound( - media_id, - customer_id=self._customer_id, **kwargs) + media_id, customer_id=self._customer_id, **kwargs + ) else: await self.alexa_api.play_music( - media_type, media_id, - customer_id=self._customer_id, **kwargs) - if not (self.hass.data[DATA_ALEXAMEDIA] - ['accounts'][self._login.email]['websocket']): + media_type, media_id, customer_id=self._customer_id, **kwargs + ) + if not ( + self.hass.data[DATA_ALEXAMEDIA]["accounts"][self._login.email]["websocket"] + ): await self.async_update() @property def device_state_attributes(self): """Return the scene state attributes.""" - attr = { - 'available': self._available, - 'last_called': self._last_called - } + attr = {"available": self._available, "last_called": self._last_called} return attr @property @@ -984,11 +1081,9 @@ def should_poll(self): def device_info(self): """Return the device_info of the device.""" return { - 'identifiers': { - (ALEXA_DOMAIN, self.unique_id) - }, - 'name': self.name, - 'manufacturer': "Amazon", - 'model': f"{self._device_family} {self._device_type}", - 'sw_version': self._software_version, + "identifiers": {(ALEXA_DOMAIN, self.unique_id)}, + "name": self.name, + "manufacturer": "Amazon", + "model": f"{self._device_family} {self._device_type}", + "sw_version": self._software_version, } diff --git a/custom_components/alexa_media/notify.py b/custom_components/alexa_media/notify.py index f04e10e4..d802f9e6 100644 --- a/custom_components/alexa_media/notify.py +++ b/custom_components/alexa_media/notify.py @@ -9,10 +9,14 @@ """ import logging -from homeassistant.components.notify import (ATTR_DATA, ATTR_TARGET, - ATTR_TITLE, ATTR_TITLE_DEFAULT, - SERVICE_NOTIFY, - BaseNotificationService) +from homeassistant.components.notify import ( + ATTR_DATA, + ATTR_TARGET, + ATTR_TITLE, + ATTR_TITLE_DEFAULT, + SERVICE_NOTIFY, + BaseNotificationService, +) from . import CONF_EMAIL, DATA_ALEXAMEDIA, DOMAIN, hide_email, hide_serial from .helpers import retry_async @@ -24,14 +28,14 @@ async def async_get_service(hass, config, discovery_info=None): # pylint: disable=unused-argument """Get the demo notification service.""" - for account, account_dict in ( - hass.data[DATA_ALEXAMEDIA]['accounts'].items()): - for key, _ in account_dict['devices']['media_player'].items(): - if key not in account_dict['entities']['media_player']: + for account, account_dict in hass.data[DATA_ALEXAMEDIA]["accounts"].items(): + for key, _ in account_dict["devices"]["media_player"].items(): + if key not in account_dict["entities"]["media_player"]: _LOGGER.debug( "%s: Media player %s not loaded yet; delaying load", hide_email(account), - hide_serial(key)) + hide_serial(key), + ) return False return AlexaNotificationService(hass) @@ -40,17 +44,13 @@ async def async_unload_entry(hass, entry) -> bool: """Unload a config entry.""" target_account = entry.data[CONF_EMAIL] other_accounts = False - for account, account_dict in (hass.data[DATA_ALEXAMEDIA] - ['accounts'].items()): + for account, account_dict in hass.data[DATA_ALEXAMEDIA]["accounts"].items(): if account == target_account: - if 'entities' not in account_dict: + if "entities" not in account_dict: continue - for device in (account_dict['entities'] - ['media_player'].values()): - entity_id = device.entity_id.split('.') - hass.services.async_remove( - SERVICE_NOTIFY, - f"{DOMAIN}_{entity_id[1]}") + for device in account_dict["entities"]["media_player"].values(): + entity_id = device.entity_id.split(".") + hass.services.async_remove(SERVICE_NOTIFY, f"{DOMAIN}_{entity_id[1]}") else: other_accounts = True if not other_accounts: @@ -92,14 +92,15 @@ async def convert(self, names, type_="entities", filter_matches=False): for item in names: matched = False for alexa in self.devices: - _LOGGER.debug("Testing item: %s against (%s, %s, %s, %s)", - item, - alexa, - alexa.name, - hide_serial(alexa.unique_id), - alexa.entity_id) - if item in (alexa, alexa.name, alexa.unique_id, - alexa.entity_id): + _LOGGER.debug( + "Testing item: %s against (%s, %s, %s, %s)", + item, + alexa, + alexa.name, + hide_serial(alexa.unique_id), + alexa.entity_id, + ) + if item in (alexa, alexa.name, alexa.unique_id, alexa.entity_id): if type_ == "entities": converted = alexa elif type_ == "serialnumbers": @@ -110,10 +111,7 @@ async def convert(self, names, type_="entities", filter_matches=False): converted = alexa.entity_id devices.append(converted) matched = True - _LOGGER.debug("Converting: %s to (%s): %s", - item, - type_, - converted) + _LOGGER.debug("Converting: %s to (%s): %s", item, type_, converted) if not filter_matches and not matched: devices.append(item) return devices @@ -122,81 +120,80 @@ async def convert(self, names, type_="entities", filter_matches=False): def targets(self): """Return a dictionary of Alexa devices.""" devices = {} - for _, account_dict in (self.hass.data[DATA_ALEXAMEDIA] - ['accounts'].items()): - if ('devices' not in account_dict): + for _, account_dict in self.hass.data[DATA_ALEXAMEDIA]["accounts"].items(): + if "devices" not in account_dict: return devices - for serial, alexa in (account_dict - ['devices']['media_player'].items()): - devices[alexa['accountName']] = serial + for serial, alexa in account_dict["devices"]["media_player"].items(): + devices[alexa["accountName"]] = serial return devices @property def devices(self): """Return a list of Alexa devices.""" devices = [] - if ('accounts' not in self.hass.data[DATA_ALEXAMEDIA] and - not self.hass.data[DATA_ALEXAMEDIA]['accounts'].items()): + if ( + "accounts" not in self.hass.data[DATA_ALEXAMEDIA] + and not self.hass.data[DATA_ALEXAMEDIA]["accounts"].items() + ): return devices - for _, account_dict in (self.hass.data[DATA_ALEXAMEDIA] - ['accounts'].items()): - devices = devices + list(account_dict - ['entities']['media_player'].values()) + for _, account_dict in self.hass.data[DATA_ALEXAMEDIA]["accounts"].items(): + devices = devices + list(account_dict["entities"]["media_player"].values()) return devices async def async_send_message(self, message="", **kwargs): """Send a message to a Alexa device.""" - _LOGGER.debug("Message: %s, kwargs: %s", - message, - kwargs) - kwargs['message'] = message + _LOGGER.debug("Message: %s, kwargs: %s", message, kwargs) + kwargs["message"] = message targets = kwargs.get(ATTR_TARGET) - title = (kwargs.get(ATTR_TITLE) if ATTR_TITLE in kwargs - else ATTR_TITLE_DEFAULT) + title = kwargs.get(ATTR_TITLE) if ATTR_TITLE in kwargs else ATTR_TITLE_DEFAULT data = kwargs.get(ATTR_DATA) if isinstance(targets, str): targets = [targets] entities = await self.convert(targets, type_="entities") try: - entities.extend(self.hass.components.group.expand_entity_ids( - entities)) + entities.extend(self.hass.components.group.expand_entity_ids(entities)) except ValueError: _LOGGER.debug("Invalid Home Assistant entity in %s", entities) - if data['type'] == "tts": - targets = await self.convert(entities, type_="entities", - filter_matches=True) + if data["type"] == "tts": + targets = await self.convert( + entities, type_="entities", filter_matches=True + ) _LOGGER.debug("TTS entities: %s", targets) for alexa in targets: _LOGGER.debug("TTS by %s : %s", alexa, message) await alexa.async_send_tts(message) - elif data['type'] == "announce": - targets = await self.convert(entities, type_="serialnumbers", - filter_matches=True) - _LOGGER.debug("Announce targets: %s entities: %s", - list(map(hide_serial, targets)), - entities) - for account, account_dict in (self.hass.data[DATA_ALEXAMEDIA] - ['accounts'].items()): - for alexa in (account_dict['entities'] - ['media_player'].values()): + elif data["type"] == "announce": + targets = await self.convert( + entities, type_="serialnumbers", filter_matches=True + ) + _LOGGER.debug( + "Announce targets: %s entities: %s", + list(map(hide_serial, targets)), + entities, + ) + for account, account_dict in self.hass.data[DATA_ALEXAMEDIA][ + "accounts" + ].items(): + for alexa in account_dict["entities"]["media_player"].values(): if alexa.unique_id in targets and alexa.available: - _LOGGER.debug(("%s: Announce by %s to " - "targets: %s: %s"), - hide_email(account), - alexa, - list(map(hide_serial, targets)), - message) + _LOGGER.debug( + ("%s: Announce by %s to " "targets: %s: %s"), + hide_email(account), + alexa, + list(map(hide_serial, targets)), + message, + ) await alexa.async_send_announcement( message, targets=targets, title=title, - method=(data['method'] if - 'method' in data - else 'all')) + method=(data["method"] if "method" in data else "all"), + ) break - elif data['type'] == "push": - targets = await self.convert(entities, type_="entities", - filter_matches=True) + elif data["type"] == "push": + targets = await self.convert( + entities, type_="entities", filter_matches=True + ) for alexa in targets: _LOGGER.debug("Push by %s : %s %s", alexa, title, message) await alexa.async_send_mobilepush(message, title=title) diff --git a/custom_components/alexa_media/sensor.py b/custom_components/alexa_media/sensor.py index 88e8844d..d84dd56a 100644 --- a/custom_components/alexa_media/sensor.py +++ b/custom_components/alexa_media/sensor.py @@ -16,16 +16,20 @@ from homeassistant.helpers.entity import Entity from homeassistant.util import dt -from . import (CONF_EMAIL, CONF_EXCLUDE_DEVICES, CONF_INCLUDE_DEVICES, - DATA_ALEXAMEDIA) -from . import DOMAIN as ALEXA_DOMAIN -from . import (hide_email, hide_serial) +from . import ( + CONF_EMAIL, + CONF_EXCLUDE_DEVICES, + CONF_INCLUDE_DEVICES, + DATA_ALEXAMEDIA, + DOMAIN as ALEXA_DOMAIN, + hide_email, + hide_serial, +) from .helpers import add_devices, retry_async _LOGGER = logging.getLogger(__name__) -LOCAL_TIMEZONE = datetime.datetime.now( - datetime.timezone.utc).astimezone().tzinfo +LOCAL_TIMEZONE = datetime.datetime.now(datetime.timezone.utc).astimezone().tzinfo RECURRING_PATTERN = { None: "Never Repeat", @@ -38,101 +42,98 @@ "XXXX-WXX-4": "Every Thursday", "XXXX-WXX-5": "Every Friday", "XXXX-WXX-6": "Every Saturday", - "XXXX-WXX-7": "Every Sunday" + "XXXX-WXX-7": "Every Sunday", } @retry_async(limit=5, delay=5, catch_exceptions=False) -async def async_setup_platform(hass, config, add_devices_callback, - discovery_info=None): +async def async_setup_platform(hass, config, add_devices_callback, discovery_info=None): """Set up the Alexa sensor platform.""" devices: List[AlexaMediaSensor] = [] SENSOR_TYPES = { - 'Alarm': AlarmSensor, - 'Timer': TimerSensor, - 'Reminder': ReminderSensor + "Alarm": AlarmSensor, + "Timer": TimerSensor, + "Reminder": ReminderSensor, } account = config[CONF_EMAIL] include_filter = config.get(CONF_INCLUDE_DEVICES, []) exclude_filter = config.get(CONF_EXCLUDE_DEVICES, []) - account_dict = hass.data[DATA_ALEXAMEDIA]['accounts'][account] - _LOGGER.debug("%s: Loading sensors", - hide_email(account)) - if 'sensor' not in account_dict['entities']: - (hass.data[DATA_ALEXAMEDIA] - ['accounts'] - [account] - ['entities'] - ['sensor']) = {} - for key, device in account_dict['devices']['media_player'].items(): - if key not in account_dict['entities']['media_player']: - _LOGGER.debug("%s: Media player %s not loaded yet; delaying load", - hide_email(account), - hide_serial(key)) + account_dict = hass.data[DATA_ALEXAMEDIA]["accounts"][account] + _LOGGER.debug("%s: Loading sensors", hide_email(account)) + if "sensor" not in account_dict["entities"]: + (hass.data[DATA_ALEXAMEDIA]["accounts"][account]["entities"]["sensor"]) = {} + for key, device in account_dict["devices"]["media_player"].items(): + if key not in account_dict["entities"]["media_player"]: + _LOGGER.debug( + "%s: Media player %s not loaded yet; delaying load", + hide_email(account), + hide_serial(key), + ) return False - if key not in (account_dict - ['entities'] - ['sensor']): - (account_dict - ['entities'] - ['sensor'][key]) = {} + if key not in (account_dict["entities"]["sensor"]): + (account_dict["entities"]["sensor"][key]) = {} for (n_type, class_) in SENSOR_TYPES.items(): n_type_dict = ( - account_dict['notifications'][key][n_type] - if key in account_dict['notifications'] and - n_type in account_dict['notifications'][key] else {}) - if (n_type in ('Alarm, Timer') and - 'TIMERS_AND_ALARMS' in device['capabilities']): + account_dict["notifications"][key][n_type] + if key in account_dict["notifications"] + and n_type in account_dict["notifications"][key] + else {} + ) + if ( + n_type in ("Alarm, Timer") + and "TIMERS_AND_ALARMS" in device["capabilities"] + ): alexa_client = class_( - account_dict['entities']['media_player'][key], + account_dict["entities"]["media_player"][key], n_type_dict, - account) - elif (n_type in ('Reminder') and - 'REMINDERS' in device['capabilities']): + account, + ) + elif n_type in ("Reminder") and "REMINDERS" in device["capabilities"]: alexa_client = class_( - account_dict['entities']['media_player'][key], + account_dict["entities"]["media_player"][key], n_type_dict, - account) + account, + ) else: continue - _LOGGER.debug("%s: Found %s %s sensor (%s) with next: %s", - hide_email(account), - hide_serial(key), - n_type, - len(n_type_dict.keys()), - alexa_client.state) + _LOGGER.debug( + "%s: Found %s %s sensor (%s) with next: %s", + hide_email(account), + hide_serial(key), + n_type, + len(n_type_dict.keys()), + alexa_client.state, + ) devices.append(alexa_client) - (account_dict - ['entities'] - ['sensor'] - [key] - [n_type]) = alexa_client + (account_dict["entities"]["sensor"][key][n_type]) = alexa_client else: - for alexa_client in (account_dict['entities'] - ['sensor'] - [key].values()): - _LOGGER.debug("%s: Skipping already added device: %s", - hide_email(account), - alexa_client) - return await add_devices(hide_email(account), - devices, add_devices_callback, - include_filter, exclude_filter) + for alexa_client in account_dict["entities"]["sensor"][key].values(): + _LOGGER.debug( + "%s: Skipping already added device: %s", + hide_email(account), + alexa_client, + ) + return await add_devices( + hide_email(account), + devices, + add_devices_callback, + include_filter, + exclude_filter, + ) async def async_setup_entry(hass, config_entry, async_add_devices): """Set up the Alexa sensor platform by config_entry.""" return await async_setup_platform( - hass, - config_entry.data, - async_add_devices, - discovery_info=None) + hass, config_entry.data, async_add_devices, discovery_info=None + ) async def async_unload_entry(hass, entry) -> bool: """Unload a config entry.""" account = entry.data[CONF_EMAIL] - account_dict = hass.data[DATA_ALEXAMEDIA]['accounts'][account] - for key, sensors in (account_dict['entities']['sensor'].items()): + account_dict = hass.data[DATA_ALEXAMEDIA]["accounts"][account] + for key, sensors in account_dict["entities"]["sensor"].items(): for device in sensors[key].values(): await device.async_remove() return True @@ -141,13 +142,15 @@ async def async_unload_entry(hass, entry) -> bool: class AlexaMediaSensor(Entity): """Representation of Alexa Media sensors.""" - def __init__(self, - client, - n_dict, - sensor_property: Text, - account, - name="Next Notification", - icon=None): + def __init__( + self, + client, + n_dict, + sensor_property: Text, + account, + name="Next Notification", + icon=None, + ): """Initialize the Alexa sensor device.""" # Class info self._client = client @@ -159,16 +162,22 @@ def __init__(self, self._unit = None self._device_class = DEVICE_CLASS_TIMESTAMP self._icon = icon - self._all = (sorted(self._n_dict.items(), - key=lambda x: x[1][self._sensor_property]) - if self._n_dict else []) + self._all = ( + sorted(self._n_dict.items(), key=lambda x: x[1][self._sensor_property]) + if self._n_dict + else [] + ) self._all = list(map(self._fix_alarm_date_time, self._all)) - self._sorted = list(filter(lambda x: x[1]['status'] == 'ON', - self._all)) if self._all else [] + self._sorted = ( + list(filter(lambda x: x[1]["status"] == "ON", self._all)) + if self._all + else [] + ) self._next = self._sorted[0][1] if self._sorted else None def _fix_alarm_date_time(self, value): import pytz + if self._sensor_property != "date_time" or not value: return value naive_time = dt.parse_datetime(value[1][self._sensor_property]) @@ -180,7 +189,8 @@ def _fix_alarm_date_time(self, value): "%s does not have a timezone set. " "Returned times may be wrong. " "Please set the timezone in the Alexa app.", - self._client.name) + self._client.name, + ) return value async def async_added_to_hass(self): @@ -192,8 +202,8 @@ async def async_added_to_hass(self): pass # Register event handler on bus self._listener = self.hass.bus.async_listen( - f'{ALEXA_DOMAIN}_{hide_email(self._account)}'[0:32], - self._handle_event) + f"{ALEXA_DOMAIN}_{hide_email(self._account)}"[0:32], self._handle_event + ) async def async_will_remove_from_hass(self): """Prepare to remove entity.""" @@ -211,9 +221,11 @@ def _handle_event(self, event): return except AttributeError: pass - if 'notification_update' in event.data: - if (event.data['notification_update']['dopplerId'] - ['deviceSerialNumber'] == self._client.unique_id): + if "notification_update" in event.data: + if ( + event.data["notification_update"]["dopplerId"]["deviceSerialNumber"] + == self._client.unique_id + ): _LOGGER.debug("Updating sensor %s", self.name) self.async_schedule_update_ha_state(True) @@ -235,14 +247,18 @@ def name(self): @property def should_poll(self): """Return the polling state.""" - return not (self.hass.data[DATA_ALEXAMEDIA] - ['accounts'][self._account]['websocket']) + return not ( + self.hass.data[DATA_ALEXAMEDIA]["accounts"][self._account]["websocket"] + ) @property def state(self): """Return the state of the sensor.""" - return self._next[self._sensor_property].replace( - tzinfo=LOCAL_TIMEZONE).isoformat() if self._next else STATE_UNAVAILABLE + return ( + self._next[self._sensor_property].replace(tzinfo=LOCAL_TIMEZONE).isoformat() + if self._next + else STATE_UNAVAILABLE + ) @property def unit_of_measurement(self): @@ -261,19 +277,22 @@ async def async_update(self): return except AttributeError: pass - account_dict = (self.hass.data[DATA_ALEXAMEDIA]['accounts'] - [self._account]) + account_dict = self.hass.data[DATA_ALEXAMEDIA]["accounts"][self._account] try: - self._n_dict = (account_dict['notifications'][self._dev_id] - [self._type]) + self._n_dict = account_dict["notifications"][self._dev_id][self._type] except KeyError: self._n_dict = None - self._all = (sorted(self._n_dict.items(), - key=lambda x: x[1][self._sensor_property]) - if self._n_dict else []) + self._all = ( + sorted(self._n_dict.items(), key=lambda x: x[1][self._sensor_property]) + if self._n_dict + else [] + ) self._all = list(map(self._fix_alarm_date_time, self._all)) - self._sorted = list(filter(lambda x: x[1]['status'] == 'ON', - self._all)) if self._all else [] + self._sorted = ( + list(filter(lambda x: x[1]["status"] == "ON", self._all)) + if self._all + else [] + ) self._next = self._sorted[0][1] if self._sorted else None try: self.async_schedule_update_ha_state() @@ -284,10 +303,8 @@ async def async_update(self): def device_info(self): """Return the device_info of the device.""" return { - 'identifiers': { - (ALEXA_DOMAIN, self._dev_id) - }, - 'via_device': (ALEXA_DOMAIN, self._dev_id), + "identifiers": {(ALEXA_DOMAIN, self._dev_id)}, + "via_device": (ALEXA_DOMAIN, self._dev_id), } @property @@ -298,19 +315,19 @@ def icon(self): @property def recurrence(self): """Return the icon of the sensor.""" - return (RECURRING_PATTERN[self._next['recurringPattern']] - if self._next else None) + return RECURRING_PATTERN[self._next["recurringPattern"]] if self._next else None @property def device_state_attributes(self): """Return additional attributes.""" import json + attr = { - 'recurrence': self.recurrence, - 'total_active': len(self._sorted), - 'total_all': len(self._all), - 'sorted_active': json.dumps(self._sorted, default=str), - 'sorted_all': json.dumps(self._all, default=str), + "recurrence": self.recurrence, + "total_active": len(self._sorted), + "total_all": len(self._all), + "sorted_active": json.dumps(self._sorted, default=str), + "sorted_all": json.dumps(self._all, default=str), } return attr @@ -321,14 +338,10 @@ class AlarmSensor(AlexaMediaSensor): def __init__(self, client, n_json, account): """Initialize the Alexa sensor.""" # Class info - self._type = 'Alarm' + self._type = "Alarm" super().__init__( - client, - n_json, - 'date_time', - account, - f"next {self._type}", - 'mdi:alarm') + client, n_json, "date_time", account, f"next {self._type}", "mdi:alarm" + ) class TimerSensor(AlexaMediaSensor): @@ -337,28 +350,28 @@ class TimerSensor(AlexaMediaSensor): def __init__(self, client, n_json, account): """Initialize the Alexa sensor.""" # Class info - self._type = 'Timer' + self._type = "Timer" super().__init__( - client, - n_json, - 'remainingTime', - account, - f"next {self._type}", - "mdi:timer") + client, n_json, "remainingTime", account, f"next {self._type}", "mdi:timer" + ) @property def state(self) -> datetime.datetime: """Return the state of the sensor.""" return ( - dt.as_local(dt.utc_from_timestamp( - dt.utcnow().timestamp() + - self._next[self._sensor_property]/1000)).isoformat() - if self._next else STATE_UNAVAILABLE) + dt.as_local( + dt.utc_from_timestamp( + dt.utcnow().timestamp() + self._next[self._sensor_property] / 1000 + ) + ).isoformat() + if self._next + else STATE_UNAVAILABLE + ) @property def paused(self) -> bool: """Return the paused state of the sensor.""" - return self._next['status'] == 'PAUSED' if self._next else None + return self._next["status"] == "PAUSED" if self._next else None @property def icon(self): @@ -372,34 +385,32 @@ class ReminderSensor(AlexaMediaSensor): def __init__(self, client, n_json, account): """Initialize the Alexa sensor.""" # Class info - self._type = 'Reminder' + self._type = "Reminder" super().__init__( - client, - n_json, - 'alarmTime', - account, - f"next {self._type}", - 'mdi:reminder') + client, n_json, "alarmTime", account, f"next {self._type}", "mdi:reminder" + ) @property def state(self): """Return the state of the sensor.""" - return dt.as_local(datetime.datetime.fromtimestamp( - self._next[self._sensor_property]/1000, - tz=LOCAL_TIMEZONE)).isoformat() if self._next else STATE_UNAVAILABLE + return ( + dt.as_local( + datetime.datetime.fromtimestamp( + self._next[self._sensor_property] / 1000, tz=LOCAL_TIMEZONE + ) + ).isoformat() + if self._next + else STATE_UNAVAILABLE + ) @property def reminder(self): """Return the reminder of the sensor.""" - return self._next['reminderLabel'] if self._next else None + return self._next["reminderLabel"] if self._next else None @property def device_state_attributes(self): """Return the scene state attributes.""" attr = super().device_state_attributes - attr.update( - { - 'reminder': self.reminder - } - ) + attr.update({"reminder": self.reminder}) return attr diff --git a/custom_components/alexa_media/switch.py b/custom_components/alexa_media/switch.py index f0e75590..106b960a 100644 --- a/custom_components/alexa_media/switch.py +++ b/custom_components/alexa_media/switch.py @@ -13,100 +13,99 @@ from homeassistant.components.switch import SwitchDevice from homeassistant.exceptions import NoEntitySpecifiedError -from . import (CONF_EMAIL, CONF_EXCLUDE_DEVICES, CONF_INCLUDE_DEVICES, - DATA_ALEXAMEDIA) -from . import DOMAIN as ALEXA_DOMAIN -from . import (hide_email, hide_serial) +from . import ( + CONF_EMAIL, + CONF_EXCLUDE_DEVICES, + CONF_INCLUDE_DEVICES, + DATA_ALEXAMEDIA, + DOMAIN as ALEXA_DOMAIN, + hide_email, + hide_serial, +) from .helpers import _catch_login_errors, add_devices, retry_async _LOGGER = logging.getLogger(__name__) @retry_async(limit=5, delay=5, catch_exceptions=True) -async def async_setup_platform(hass, config, add_devices_callback, - discovery_info=None): +async def async_setup_platform(hass, config, add_devices_callback, discovery_info=None): """Set up the Alexa switch platform.""" devices = [] # type: List[DNDSwitch] SWITCH_TYPES = [ - ('dnd', DNDSwitch), - ('shuffle', ShuffleSwitch), - ('repeat', RepeatSwitch) + ("dnd", DNDSwitch), + ("shuffle", ShuffleSwitch), + ("repeat", RepeatSwitch), ] account = config[CONF_EMAIL] include_filter = config.get(CONF_INCLUDE_DEVICES, []) exclude_filter = config.get(CONF_EXCLUDE_DEVICES, []) - account_dict = hass.data[DATA_ALEXAMEDIA]['accounts'][account] - _LOGGER.debug("%s: Loading switches", - hide_email(account)) - if 'switch' not in account_dict['entities']: - (hass.data[DATA_ALEXAMEDIA] - ['accounts'] - [account] - ['entities'] - ['switch']) = {} - for key, _ in account_dict['devices']['media_player'].items(): - if key not in account_dict['entities']['media_player']: - _LOGGER.debug("%s: Media player %s not loaded yet; delaying load", - hide_email(account), - hide_serial(key)) + account_dict = hass.data[DATA_ALEXAMEDIA]["accounts"][account] + _LOGGER.debug("%s: Loading switches", hide_email(account)) + if "switch" not in account_dict["entities"]: + (hass.data[DATA_ALEXAMEDIA]["accounts"][account]["entities"]["switch"]) = {} + for key, _ in account_dict["devices"]["media_player"].items(): + if key not in account_dict["entities"]["media_player"]: + _LOGGER.debug( + "%s: Media player %s not loaded yet; delaying load", + hide_email(account), + hide_serial(key), + ) return False - if key not in (hass.data[DATA_ALEXAMEDIA] - ['accounts'] - [account] - ['entities'] - ['switch']): - (hass.data[DATA_ALEXAMEDIA] - ['accounts'] - [account] - ['entities'] - ['switch'][key]) = {} + if key not in ( + hass.data[DATA_ALEXAMEDIA]["accounts"][account]["entities"]["switch"] + ): + ( + hass.data[DATA_ALEXAMEDIA]["accounts"][account]["entities"]["switch"][ + key + ] + ) = {} for (switch_key, class_) in SWITCH_TYPES: - alexa_client = class_(account_dict['entities'] - ['media_player'] - [key], - account) # type: AlexaMediaSwitch - _LOGGER.debug("%s: Found %s %s switch with status: %s", - hide_email(account), - hide_serial(key), - switch_key, - alexa_client.is_on) + alexa_client = class_( + account_dict["entities"]["media_player"][key], account + ) # type: AlexaMediaSwitch + _LOGGER.debug( + "%s: Found %s %s switch with status: %s", + hide_email(account), + hide_serial(key), + switch_key, + alexa_client.is_on, + ) devices.append(alexa_client) - (hass.data[DATA_ALEXAMEDIA] - ['accounts'] - [account] - ['entities'] - ['switch'] - [key] - [switch_key]) = alexa_client + ( + hass.data[DATA_ALEXAMEDIA]["accounts"][account]["entities"][ + "switch" + ][key][switch_key] + ) = alexa_client else: - for alexa_client in (hass.data[DATA_ALEXAMEDIA] - ['accounts'] - [account] - ['entities'] - ['switch'] - [key].values()): - _LOGGER.debug("%s: Skipping already added device: %s", - hide_email(account), - alexa_client) - return await add_devices(hide_email(account), - devices, add_devices_callback, - include_filter, exclude_filter) + for alexa_client in hass.data[DATA_ALEXAMEDIA]["accounts"][account][ + "entities" + ]["switch"][key].values(): + _LOGGER.debug( + "%s: Skipping already added device: %s", + hide_email(account), + alexa_client, + ) + return await add_devices( + hide_email(account), + devices, + add_devices_callback, + include_filter, + exclude_filter, + ) async def async_setup_entry(hass, config_entry, async_add_devices): """Set up the Alexa switch platform by config_entry.""" return await async_setup_platform( - hass, - config_entry.data, - async_add_devices, - discovery_info=None) + hass, config_entry.data, async_add_devices, discovery_info=None + ) async def async_unload_entry(hass, entry) -> bool: """Unload a config entry.""" account = entry.data[CONF_EMAIL] - account_dict = hass.data[DATA_ALEXAMEDIA]['accounts'][account] - for key, switches in (account_dict['entities']['switch'].items()): + account_dict = hass.data[DATA_ALEXAMEDIA]["accounts"][account] + for key, switches in account_dict["entities"]["switch"].items(): for device in switches[key].values(): await device.async_remove() return True @@ -115,12 +114,7 @@ async def async_unload_entry(hass, entry) -> bool: class AlexaMediaSwitch(SwitchDevice): """Representation of a Alexa Media switch.""" - def __init__(self, - client, - switch_property, - switch_function, - account, - name="Alexa"): + def __init__(self, client, switch_property, switch_function, account, name="Alexa"): """Initialize the Alexa Switch device.""" # Class info self._client = client @@ -140,8 +134,8 @@ async def async_added_to_hass(self): pass # Register event handler on bus self._listener = self.hass.bus.async_listen( - f'{ALEXA_DOMAIN}_{hide_email(self._login.email)}'[0:32], - self._handle_event) + f"{ALEXA_DOMAIN}_{hide_email(self._login.email)}"[0:32], self._handle_event + ) async def async_will_remove_from_hass(self): """Prepare to remove entity.""" @@ -159,10 +153,9 @@ def _handle_event(self, event): return except AttributeError: pass - if 'queue_state' in event.data: - queue_state = event.data['queue_state'] - if (queue_state['dopplerId'] - ['deviceSerialNumber'] == self._client.unique_id): + if "queue_state" in event.data: + queue_state = event.data["queue_state"] + if queue_state["dopplerId"]["deviceSerialNumber"] == self._client.unique_id: self._state = getattr(self._client, self._switch_property) self.async_schedule_update_ha_state() @@ -177,17 +170,20 @@ async def _set_switch(self, state, **kwargs): # if function returns success, make immediate state change if success: setattr(self._client, self._switch_property, state) - _LOGGER.debug("Switch set to %s based on %s", - getattr(self._client, - self._switch_property), - state) + _LOGGER.debug( + "Switch set to %s based on %s", + getattr(self._client, self._switch_property), + state, + ) self.async_schedule_update_ha_state() elif self.should_poll: # if we need to poll, refresh media_client - _LOGGER.debug("Requesting update of %s due to %s switch to %s", - self._client, - self._name, - state) + _LOGGER.debug( + "Requesting update of %s due to %s switch to %s", + self._client, + self._name, + state, + ) await self._client.async_update() @property @@ -211,7 +207,7 @@ def available(self): @property def unique_id(self): """Return the unique ID.""" - return self._client.unique_id + '_' + self._name + return self._client.unique_id + "_" + self._name @property def name(self): @@ -221,8 +217,9 @@ def name(self): @property def should_poll(self): """Return the polling state.""" - return not (self.hass.data[DATA_ALEXAMEDIA] - ['accounts'][self._account]['websocket']) + return not ( + self.hass.data[DATA_ALEXAMEDIA]["accounts"][self._account]["websocket"] + ) @_catch_login_errors async def async_update(self): @@ -241,10 +238,8 @@ async def async_update(self): def device_info(self): """Return device_info for device registry.""" return { - 'identifiers': { - (ALEXA_DOMAIN, self._client.unique_id) - }, - 'via_device': (ALEXA_DOMAIN, self._client.unique_id), + "identifiers": {(ALEXA_DOMAIN, self._client.unique_id)}, + "via_device": (ALEXA_DOMAIN, self._client.unique_id), } @property @@ -264,10 +259,11 @@ def __init__(self, client, account): # Class info super().__init__( client, - 'dnd_state', + "dnd_state", client.alexa_api.set_dnd_state, account, - "do not disturb") + "do not disturb", + ) @property def icon(self): @@ -282,11 +278,8 @@ def __init__(self, client, account): """Initialize the Alexa Switch.""" # Class info super().__init__( - client, - 'shuffle', - client.alexa_api.shuffle, - account, - "shuffle") + client, "shuffle", client.alexa_api.shuffle, account, "shuffle" + ) @property def icon(self): @@ -301,11 +294,8 @@ def __init__(self, client, account): """Initialize the Alexa Switch.""" # Class info super().__init__( - client, - 'repeat_state', - client.alexa_api.repeat, - account, - "repeat") + client, "repeat_state", client.alexa_api.repeat, account, "repeat" + ) @property def icon(self): diff --git a/pylintrc b/pylintrc new file mode 100644 index 00000000..186d6654 --- /dev/null +++ b/pylintrc @@ -0,0 +1,17 @@ +[MASTER] + +[MESSAGES CONTROL] +# Reasons disabled: +# format - handled by black +# unnecessary-pass - This can hurt readability +# too-many-instance-attributes - This should be refactored later +# duplicate-code - This should be refactored later as architecture has redundant Home-assistant devices. +# too-many-arguments +disable= + format,unnecessary-pass,too-many-instance-attributes,duplicate-code,too-many-arguments,too-many-boolean-expressions + +[REPORTS] +reports=no + +[FORMAT] +expected-line-ending-format=LF diff --git a/setup.cfg b/setup.cfg index 94fdf0d8..5feccdc1 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,3 +1,55 @@ [semantic_release] version_variable=custom_components/alexa_media/const.py:__version__ upload_to_pypi=false + +[pydocstyle] +ignore = D202, D212, D416, D213, D203, D407 + +[flake8] +exclude = .venv,.git,.tox,docs,venv,bin,lib,deps,build +# To work with Black +max-line-length = 88 +# E501: line too long +# H301: on mport per line +# W503: Line break occurred before a binary operator +# E203: Whitespace before ':' +# D202 No blank lines allowed after function docstring +# W504 line break after binary operator +# H102 missing apache +ignore = + E501, + H301, + W503, + E203, + D202, + W504, + H102 + +[isort] +# https://github.com/timothycrosley/isort +# https://github.com/timothycrosley/isort/wiki/isort-Settings +# splits long import on multiple lines indented by 4 spaces +multi_line_output = 3 +include_trailing_comma=True +force_grid_wrap=0 +use_parentheses=True +line_length=88 +indent = " " +# by default isort don't check module indexes +not_skip = __init__.py +# will group `import x` and `from x import` of the same module. +force_sort_within_sections = true +sections = FUTURE,STDLIB,INBETWEENS,THIRDPARTY,FIRSTPARTY,LOCALFOLDER +default_section = THIRDPARTY +known_first_party = alexa_media,tests +forced_separate = tests +combine_as_imports = true + +[mypy] +python_version = 3.7 +ignore_errors = true +follow_imports = silent +ignore_missing_imports = true +warn_incomplete_stub = true +warn_redundant_casts = true +warn_unused_configs = true