From fd814e7bdf7d766cc065a8b063f530e0c2453c62 Mon Sep 17 00:00:00 2001 From: Daniel <49846893+danielbrunt57@users.noreply.github.com> Date: Thu, 18 Jul 2024 10:57:52 -0700 Subject: [PATCH 1/2] refactor: remove defunct service: clear_history (#2324) --- custom_components/alexa_media/services.py | 38 --------- custom_components/alexa_media/strings.json | 14 ---- .../alexa_media/translations/en.json | 82 +++++++++---------- setup.cfg | 4 +- 4 files changed, 40 insertions(+), 98 deletions(-) diff --git a/custom_components/alexa_media/services.py b/custom_components/alexa_media/services.py index 3f85e723..e2684665 100644 --- a/custom_components/alexa_media/services.py +++ b/custom_components/alexa_media/services.py @@ -84,44 +84,6 @@ async def unregister(self): ) self.hass.services.async_remove(DOMAIN, SERVICE_FORCE_LOGOUT) - @_catch_login_errors - async def clear_history(self, call): - """Handle clear history service request. - - Arguments - call.ATTR_EMAIL {List[str: None]} -- Case-sensitive Alexa emails. - Default is all known emails. - call.ATTR_NUM_ENTRIES {int: 50} -- Number of entries to delete. - - Returns - bool -- True if deletion successful - - """ - _LOGGER.debug("call %s", call) - 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 - ) - success = False - for email, account_dict in self.hass.data[DATA_ALEXAMEDIA]["accounts"].items(): - if requested_emails and email not in requested_emails: - continue - login_obj = account_dict["login_obj"] - try: - await AlexaAPI.clear_history(login_obj, items) - except AlexapyLoginError: - report_relogin_required(self.hass, login_obj, email) - success = True - except AlexapyConnectionError: - _LOGGER.error( - "Unable to connect to Alexa for %s;" - " check your network connection and try again", - hide_email(email), - ) - return success - @_catch_login_errors async def force_logout(self, call) -> bool: """Handle force logout service request. diff --git a/custom_components/alexa_media/strings.json b/custom_components/alexa_media/strings.json index 8eeae9af..065b7b3b 100644 --- a/custom_components/alexa_media/strings.json +++ b/custom_components/alexa_media/strings.json @@ -67,20 +67,6 @@ } }, "services": { - "clear_history": { - "name": "Clear Amazon Voice History", - "description": "Clear last entries from Alexa Voice history for each Alexa account.", - "fields": { - "email": { - "name": "Email address", - "description": "Accounts to clear. Empty will clear all." - }, - "entries": { - "name": "Number of Entries", - "description": "Number of entries to clear from 1 to 50. If empty, clear 50." - } - } - }, "force_logout": { "name": "Force Logout", "description": "Force account to logout. Used mainly for debugging.", diff --git a/custom_components/alexa_media/translations/en.json b/custom_components/alexa_media/translations/en.json index 963267f4..065b7b3b 100644 --- a/custom_components/alexa_media/translations/en.json +++ b/custom_components/alexa_media/translations/en.json @@ -6,20 +6,38 @@ "reauth_successful": "Alexa Media Player successfully reauthenticated. Please ignore the \"Aborted\" message from HA." }, "error": { - "2fa_key_invalid": "Invalid Built-In 2FA key", "connection_error": "Error connecting; check network and retry", "identifier_exists": "Email for Alexa URL already registered", "invalid_credentials": "Invalid credentials", "invalid_url": "URL is invalid: {message}", - "unable_to_connect_hass_url": "Unable to connect to Home Assistant url. Please check the External Url under Configuration -> General", + "2fa_key_invalid": "Invalid Built-In 2FA key", + "unable_to_connect_hass_url": "Unable to connect to Home Assistant URL. Please check the Internal URL under Configuration -> General", "unknown_error": "Unknown error: {message}" }, "step": { + "user": { + "data": { + "url": "Amazon region domain (e.g., amazon.co.uk)", + "email": "Email Address", + "password": "Password", + "securitycode": "[%key_id:55616596%]", + "otp_secret": "Built-in 2FA App Key - This is 52 characters, not six!", + "hass_url": "Url to access Home Assistant", + "include_devices": "Included device (comma separated)", + "exclude_devices": "Excluded device (comma separated)", + "scan_interval": "Seconds between scans", + "queue_delay": "Seconds to wait to queue commands together", + "extended_entity_discovery": "Include devices connected via Echo", + "debug": "Advanced debugging" + }, + "description": "Required *", + "title": "Alexa Media Player - Configuration" + }, "proxy_warning": { "data": { "proxy_warning": "Ignore and Continue - I understand that no support for login issues are provided for bypassing this warning." }, - "description": "The HA server cannot connect to the URL provided: {hass_url}.\n> {error}\n\nTo fix this, please confirm your **HA server** can reach {hass_url}. This field is from the External Url under Configuration -> General but you can try your internal url.\n\nIf you are **certain** your client can reach this url, you can bypass this warning.", + "description": "The HA server cannot connect to the URL provided: {hass_url}.\n> {error}\n\nTo fix this, please confirm your **HA server** can reach {hass_url}. This field is from the External URL under Configuration -> General but you can try your internal URL.\n\nIf you are **certain** your client can reach this URL, you can bypass this warning.", "title": "Alexa Media Player - Unable to Connect to HA URL" }, "totp_register": { @@ -28,22 +46,6 @@ }, "description": "**{email} - alexa.{url}** \nHave you successfully confirmed an OTP from the Built-in 2FA App Key with Amazon? \n >OTP Code {message}", "title": "Alexa Media Player - OTP Confirmation" - }, - "user": { - "data": { - "debug": "Advanced debugging", - "email": "Email Address", - "exclude_devices": "Excluded device (comma separated)", - "hass_url": "Url to access Home Assistant", - "include_devices": "Included device (comma separated)", - "otp_secret": "Built-in 2FA App Key (automatically generate 2FA Codes). This is not six digits long.", - "password": "Password", - "scan_interval": "Seconds between scans", - "securitycode": "[%key_id:55616596%]", - "url": "Amazon region domain (e.g., amazon.co.uk)" - }, - "description": "Please confirm the information below. For legacy configuration, disable `Use Login Proxy method` option.", - "title": "Alexa Media Player - Configuration" } } }, @@ -51,47 +53,39 @@ "step": { "init": { "data": { + "hass_url": "Public URL to access Home Assistant (including trailing '/')", + "include_devices": "Included device (comma separated)", + "exclude_devices": "Excluded device (comma separated)", + "scan_interval": "Seconds between scans", + "queue_delay": "Seconds to wait to queue commands together", "extended_entity_discovery": "Include devices connected via Echo", - "public_url": "Public URL to access Home Assistant (including trailing '/')", - "queue_delay": "Seconds to wait to queue commands together" - } + "debug": "Advanced debugging" + }, + "description": "Required *", + "title": "Alexa Media Player - Reconfiguration" } } }, "services": { - "clear_history": { - "description": "Clear last entries from Alexa Voice history for each Alexa account.", - "fields": { - "email": { - "description": "Accounts to clear. Empty will clear all.", - "name": "Email address" - }, - "entries": { - "description": "Number of entries to clear from 1 to 50. If empty, clear 50.", - "name": "Number of Entries" - } - }, - "name": "Clear Amazon Voice History" - }, "force_logout": { + "name": "Force Logout", "description": "Force account to logout. Used mainly for debugging.", "fields": { "email": { - "description": "Accounts to clear. Empty will clear all.", - "name": "Email address" + "name": "Email address", + "description": "Accounts to clear. Empty will clear all." } - }, - "name": "Force Logout" + } }, "update_last_called": { + "name": "Update Last Called Sensor", "description": "Forces update of last_called echo device for each Alexa account.", "fields": { "email": { - "description": "List of Alexa accounts to update. If empty, will update all known accounts.", - "name": "Email address" + "name": "Email address", + "description": "List of Alexa accounts to update. If empty, will update all known accounts." } - }, - "name": "Update Last Called Sensor" + } } } } diff --git a/setup.cfg b/setup.cfg index 0aa61ce6..a57196b7 100644 --- a/setup.cfg +++ b/setup.cfg @@ -47,8 +47,8 @@ line_length=88 indent = " " # will group `import x` and `from x import` of the same module. force_sort_within_sections = true -sections = FUTURE,STDLIB,THIRDPARTY,FIRSTPARTY,LOCALFOLDER -default_section = THIRDPARTY +sections = FUTURE,STDLIB,THIRD-PARTY,FIRSTPARTY,LOCALFOLDER +default_section = THIRD-PARTY known_first_party = alexa_media,tests forced_separate = tests combine_as_imports = true From ecc617a90fe04fa407b39ac2d120b48cafd74b92 Mon Sep 17 00:00:00 2001 From: Daniel <49846893+danielbrunt57@users.noreply.github.com> Date: Sat, 20 Jul 2024 00:48:03 -0700 Subject: [PATCH 2/2] fix: use dict.get for OptionsFlow handler closes #2323 --- custom_components/alexa_media/__init__.py | 17 ++- custom_components/alexa_media/config_flow.py | 109 ++++++++++++------- custom_components/alexa_media/const.py | 2 +- custom_components/alexa_media/services.py | 38 +++++++ custom_components/alexa_media/strings.json | 14 +++ 5 files changed, 137 insertions(+), 43 deletions(-) diff --git a/custom_components/alexa_media/__init__.py b/custom_components/alexa_media/__init__.py index 394c156c..f6889f78 100644 --- a/custom_components/alexa_media/__init__.py +++ b/custom_components/alexa_media/__init__.py @@ -90,7 +90,6 @@ 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] ), @@ -98,6 +97,9 @@ cv.ensure_list, [cv.string] ), vol.Optional(CONF_SCAN_INTERVAL, default=SCAN_INTERVAL): cv.time_period, + vol.Optional(CONF_QUEUE_DELAY, default=DEFAULT_QUEUE_DELAY): cv.positive_float, + vol.Optional(CONF_EXTENDED_ENTITY_DISCOVERY, default=False): cv.boolean, + vol.Optional(CONF_DEBUG, default=False): cv.boolean, } ) @@ -146,18 +148,22 @@ async def async_setup(hass, config, discovery_info=None): CONF_EMAIL: account[CONF_EMAIL], 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_QUEUE_DELAY: account[CONF_QUEUE_DELAY], CONF_OAUTH: account.get( CONF_OAUTH, entry.data.get(CONF_OAUTH, {}) ), CONF_OTPSECRET: account.get( CONF_OTPSECRET, entry.data.get(CONF_OTPSECRET, "") ), + CONF_EXTENDED_ENTITY_DISCOVERY: account[ + CONF_EXTENDED_ENTITY_DISCOVERY + ], + CONF_DEBUG: account[CONF_DEBUG], }, ) entry_found = True @@ -172,12 +178,16 @@ async def async_setup(hass, config, discovery_info=None): CONF_EMAIL: account[CONF_EMAIL], 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_QUEUE_DELAY: account[CONF_QUEUE_DELAY], CONF_OAUTH: account.get(CONF_OAUTH, {}), CONF_OTPSECRET: account.get(CONF_OTPSECRET, ""), + CONF_EXTENDED_ENTITY_DISCOVERY: account[ + CONF_EXTENDED_ENTITY_DISCOVERY + ], + CONF_DEBUG: account[CONF_DEBUG], }, ) ) @@ -302,6 +312,7 @@ async def login_success(event=None) -> None: CONF_EXTENDED_ENTITY_DISCOVERY: config_entry.options.get( CONF_EXTENDED_ENTITY_DISCOVERY, DEFAULT_EXTENDED_ENTITY_DISCOVERY ), + CONF_DEBUG: config_entry.options.get(CONF_DEBUG, False), }, DATA_LISTENER: [config_entry.add_update_listener(update_listener)], }, diff --git a/custom_components/alexa_media/config_flow.py b/custom_components/alexa_media/config_flow.py index 666f3814..241a5797 100644 --- a/custom_components/alexa_media/config_flow.py +++ b/custom_components/alexa_media/config_flow.py @@ -30,7 +30,7 @@ from homeassistant.components.http.view import HomeAssistantView from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, CONF_SCAN_INTERVAL, CONF_URL from homeassistant.core import callback -from homeassistant.data_entry_flow import UnknownFlow +from homeassistant.data_entry_flow import FlowResult, UnknownFlow from homeassistant.exceptions import Unauthorized from homeassistant.helpers import config_validation as cv from homeassistant.helpers.network import NoURLAvailableError, get_url @@ -61,7 +61,7 @@ DEFAULT_EXTENDED_ENTITY_DISCOVERY, DEFAULT_PUBLIC_URL, DEFAULT_QUEUE_DELAY, - DEFAULT_SCAN_DELAY, + DEFAULT_SCAN_INTERVAL, DOMAIN, ISSUE_URL, STARTUP, @@ -70,6 +70,8 @@ _LOGGER = logging.getLogger(__name__) +CONFIG_VERSION = 1 + @callback def configured_instances(hass): @@ -87,7 +89,8 @@ def in_progess_instances(hass): class AlexaMediaFlowHandler(config_entries.ConfigFlow): """Handle a Alexa Media config flow.""" - VERSION = 1 + VERSION = CONFIG_VERSION + CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL proxy: AlexaProxy = None proxy_view: "AlexaMediaAuthorizationProxyView" = None @@ -196,7 +199,9 @@ async def async_step_user(self, user_input=None): ( vol.Optional( CONF_SCAN_INTERVAL, - default=self.config.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_DELAY), + default=self.config.get( + CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL + ), ), int, ), @@ -828,10 +833,65 @@ class OptionsFlowHandler(config_entries.OptionsFlow): def __init__(self, config_entry: config_entries.ConfigEntry) -> None: """Initialize options flow.""" + self.config = OrderedDict() self.config_entry = config_entry - async def async_step_init(self, user_input=None): - """Handle options flow.""" + async def async_step_init( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Manage the options""" + + self.options_schema = OrderedDict( + [ + ( + vol.Optional( + CONF_INCLUDE_DEVICES, + default=self.config.get(CONF_INCLUDE_DEVICES, ""), + ), + str, + ), + ( + vol.Optional( + CONF_EXCLUDE_DEVICES, + default=self.config.get(CONF_EXCLUDE_DEVICES, ""), + ), + str, + ), + ( + vol.Optional( + CONF_SCAN_INTERVAL, + default=self.config.get( + CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL + ), + ), + int, + ), + ( + vol.Optional( + CONF_QUEUE_DELAY, + default=self.config.get(CONF_QUEUE_DELAY, DEFAULT_QUEUE_DELAY), + ), + float, + ), + ( + vol.Optional( + CONF_EXTENDED_ENTITY_DISCOVERY, + default=self.config.get( + CONF_EXTENDED_ENTITY_DISCOVERY, + DEFAULT_EXTENDED_ENTITY_DISCOVERY, + ), + ), + bool, + ), + ( + vol.Optional( + CONF_DEBUG, default=self.config.get(CONF_DEBUG, DEFAULT_DEBUG) + ), + bool, + ), + ] + ) + if user_input is not None: """Preserve these parameters""" if CONF_URL in self.config_entry.data: @@ -846,7 +906,8 @@ async def async_step_init(self, user_input=None): ] if CONF_OTPSECRET in self.config_entry.data: user_input[CONF_OTPSECRET] = self.config_entry.data[CONF_OTPSECRET] - + if CONF_OAUTH in self.config_entry.data: + user_input[CONF_OAUTH] = self.config_entry.data[CONF_OAUTH] self.hass.config_entries.async_update_entry( self.config_entry, data=user_input, options=self.config_entry.options ) @@ -854,38 +915,8 @@ async def async_step_init(self, user_input=None): return self.async_show_form( step_id="init", - data_schema=vol.Schema( - { - # vol.Optional( - # CONF_HASS_URL, - # default=self.config_entry.data[CONF_HASS_URL], - # ): cv.string, - vol.Optional( - CONF_INCLUDE_DEVICES, - default=self.config_entry.data[CONF_INCLUDE_DEVICES], - ): cv.string, - vol.Optional( - CONF_EXCLUDE_DEVICES, - default=self.config_entry.data[CONF_EXCLUDE_DEVICES], - ): cv.string, - vol.Optional( - CONF_SCAN_INTERVAL, - default=self.config_entry.data[CONF_SCAN_INTERVAL], - ): vol.All(vol.Coerce(int), vol.Range(min=0, max=1800)), - vol.Optional( - CONF_QUEUE_DELAY, - default=self.config_entry.data[CONF_QUEUE_DELAY], - ): vol.All(vol.Coerce(float), vol.Range(min=0.5, max=5.0)), - vol.Optional( - CONF_EXTENDED_ENTITY_DISCOVERY, - default=self.config_entry.data[CONF_EXTENDED_ENTITY_DISCOVERY], - ): cv.boolean, - vol.Optional( - CONF_DEBUG, - default=self.config_entry.data[CONF_DEBUG], - ): cv.boolean, - } - ), + data_schema=vol.Schema(self.options_schema), + description_placeholders={"message": ""}, ) diff --git a/custom_components/alexa_media/const.py b/custom_components/alexa_media/const.py index 128f7733..243e0100 100644 --- a/custom_components/alexa_media/const.py +++ b/custom_components/alexa_media/const.py @@ -59,7 +59,7 @@ EXCEPTION_TEMPLATE = "An exception of type {0} occurred. Arguments:\n{1!r}" -DEFAULT_SCAN_DELAY = 60 +DEFAULT_SCAN_INTERVAL = 60 DEFAULT_QUEUE_DELAY = 1.5 DEFAULT_PUBLIC_URL = "" DEFAULT_EXTENDED_ENTITY_DISCOVERY = False diff --git a/custom_components/alexa_media/services.py b/custom_components/alexa_media/services.py index e2684665..3f85e723 100644 --- a/custom_components/alexa_media/services.py +++ b/custom_components/alexa_media/services.py @@ -84,6 +84,44 @@ async def unregister(self): ) self.hass.services.async_remove(DOMAIN, SERVICE_FORCE_LOGOUT) + @_catch_login_errors + async def clear_history(self, call): + """Handle clear history service request. + + Arguments + call.ATTR_EMAIL {List[str: None]} -- Case-sensitive Alexa emails. + Default is all known emails. + call.ATTR_NUM_ENTRIES {int: 50} -- Number of entries to delete. + + Returns + bool -- True if deletion successful + + """ + _LOGGER.debug("call %s", call) + 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 + ) + success = False + for email, account_dict in self.hass.data[DATA_ALEXAMEDIA]["accounts"].items(): + if requested_emails and email not in requested_emails: + continue + login_obj = account_dict["login_obj"] + try: + await AlexaAPI.clear_history(login_obj, items) + except AlexapyLoginError: + report_relogin_required(self.hass, login_obj, email) + success = True + except AlexapyConnectionError: + _LOGGER.error( + "Unable to connect to Alexa for %s;" + " check your network connection and try again", + hide_email(email), + ) + return success + @_catch_login_errors async def force_logout(self, call) -> bool: """Handle force logout service request. diff --git a/custom_components/alexa_media/strings.json b/custom_components/alexa_media/strings.json index 065b7b3b..8eeae9af 100644 --- a/custom_components/alexa_media/strings.json +++ b/custom_components/alexa_media/strings.json @@ -67,6 +67,20 @@ } }, "services": { + "clear_history": { + "name": "Clear Amazon Voice History", + "description": "Clear last entries from Alexa Voice history for each Alexa account.", + "fields": { + "email": { + "name": "Email address", + "description": "Accounts to clear. Empty will clear all." + }, + "entries": { + "name": "Number of Entries", + "description": "Number of entries to clear from 1 to 50. If empty, clear 50." + } + } + }, "force_logout": { "name": "Force Logout", "description": "Force account to logout. Used mainly for debugging.",