This commit is contained in:
Home Assistant Version Control
2026-10-12 22:32:16 +00:00
parent b12fb1db7c
commit 957b247d4a
8 changed files with 140 additions and 67 deletions
+42 -24
View File
@@ -46,7 +46,7 @@ from homeassistant.const import (
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.data_entry_flow import UnknownFlow
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers import config_validation as cv, device_registry as dr
from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
@@ -70,7 +70,6 @@ from .const import (
CONF_QUEUE_DELAY,
CONF_SCAN_INTERVAL,
DATA_ALEXAMEDIA,
DATA_LISTENER,
DEFAULT_EXTENDED_ENTITY_DISCOVERY,
DEFAULT_PUBLIC_URL,
DEFAULT_QUEUE_DELAY,
@@ -744,26 +743,23 @@ async def async_setup_entry(hass, config_entry):
"notifications_pending": set(), # doppler serials that need a refresh
"notifications_refresh_task": None, # running task or None
"notifications_retry_count": 0, # simple backoff counter
"options": {
CONF_INCLUDE_DEVICES: config_entry.data.get(CONF_INCLUDE_DEVICES, ""),
CONF_EXCLUDE_DEVICES: config_entry.data.get(CONF_EXCLUDE_DEVICES, ""),
CONF_QUEUE_DELAY: config_entry.data.get(
CONF_QUEUE_DELAY, DEFAULT_QUEUE_DELAY
),
CONF_SCAN_INTERVAL: config_entry.data.get(
CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL
),
CONF_PUBLIC_URL: config_entry.data.get(
CONF_PUBLIC_URL, DEFAULT_PUBLIC_URL
),
CONF_EXTENDED_ENTITY_DISCOVERY: config_entry.data.get(
CONF_EXTENDED_ENTITY_DISCOVERY, DEFAULT_EXTENDED_ENTITY_DISCOVERY
),
CONF_DEBUG: config_entry.data.get(CONF_DEBUG, False),
},
DATA_LISTENER: [config_entry.add_update_listener(update_listener)],
},
)
# Refreshed on every attempt: the account dict survives setup retries and
# a failed setup, so options saved in the meantime would otherwise be lost.
hass.data[DATA_ALEXAMEDIA]["accounts"][email]["options"] = {
CONF_INCLUDE_DEVICES: config_entry.data.get(CONF_INCLUDE_DEVICES, ""),
CONF_EXCLUDE_DEVICES: config_entry.data.get(CONF_EXCLUDE_DEVICES, ""),
CONF_QUEUE_DELAY: config_entry.data.get(CONF_QUEUE_DELAY, DEFAULT_QUEUE_DELAY),
CONF_SCAN_INTERVAL: config_entry.data.get(
CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL
),
CONF_PUBLIC_URL: config_entry.data.get(CONF_PUBLIC_URL, DEFAULT_PUBLIC_URL),
CONF_EXTENDED_ENTITY_DISCOVERY: config_entry.data.get(
CONF_EXTENDED_ENTITY_DISCOVERY, DEFAULT_EXTENDED_ENTITY_DISCOVERY
),
CONF_DEBUG: config_entry.data.get(CONF_DEBUG, False),
}
uuid_dict = await calculate_uuid(hass, email, url)
uuid = uuid_dict["uuid"]
hass.data[DATA_ALEXAMEDIA]["accounts"][email]["second_account_index"] = uuid_dict[
@@ -797,8 +793,6 @@ async def async_setup_entry(hass, config_entry):
if not hass.data[DATA_ALEXAMEDIA]["accounts"][email]["second_account_index"]:
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, close_alexa_media)
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STARTED, complete_startup)
hass.bus.async_listen("alexa_media_relogin_required", relogin)
hass.bus.async_listen("alexa_media_relogin_success", login_success)
try:
_t = time.monotonic()
cookies = await login.load_cookie()
@@ -837,6 +831,16 @@ async def async_setup_entry(hass, config_entry):
try:
async with async_timeout.timeout(LOGIN_MAX_WAIT_S):
await login.login(cookies=cookies)
except (RuntimeError, ValueError) as err:
if "Session is closed" in str(err) or "unrecoverable" in str(err):
_LOGGER.warning(
"Alexa Media Player session is closed. Triggering Re-authentication flow."
)
# This tells Home Assistant to show a persistent notification requiring reauth
raise ConfigEntryAuthFailed(
"Amazon session closed. Please reconfigure your credentials."
) from err
raise
except asyncio.TimeoutError as err:
# An interrupted login can leave partial request state on the
# client, which alexapy resumes from on the next attempt. Both
@@ -853,9 +857,25 @@ async def async_setup_entry(hass, config_entry):
_LOGGER.debug("[BOOT] login completed in %.2fs", time.monotonic() - _t)
_t = time.monotonic()
if await test_login_status(hass, config_entry, login):
# Registered only on success: HA processes async_on_unload
# callbacks on unload and on the ConfigEntryNotReady retry
# path, but not for a plain `False` return, so registering
# any earlier would leak a pair on every failed login check.
config_entry.async_on_unload(
hass.bus.async_listen("alexa_media_relogin_required", relogin)
)
config_entry.async_on_unload(
hass.bus.async_listen("alexa_media_relogin_success", login_success)
)
_LOGGER.debug("[BOOT] test_login_status in %.2fs", time.monotonic() - _t)
_t = time.monotonic()
await setup_alexa(hass, config_entry, login)
# Registered once the entry has loaded, so that Home Assistant
# removes it on unload. Any earlier and each setup retry, or failed
# login check, would leave another copy on the entry.
config_entry.async_on_unload(
config_entry.add_update_listener(update_listener)
)
_LOGGER.debug(
"[BOOT] setup_entry total: %.2fs", time.monotonic() - _boot_start
)
@@ -3128,8 +3148,6 @@ async def async_unload_entry(hass, entry) -> bool:
except Exception:
_LOGGER.error("Error unloading: %s", component)
await close_connections(hass, email)
for listener in hass.data[DATA_ALEXAMEDIA]["accounts"][email][DATA_LISTENER]:
listener()
hass.data[DATA_ALEXAMEDIA]["accounts"].pop(email)
# Clean up config flows in progress
flows_to_remove = []
+67 -11
View File
@@ -513,6 +513,8 @@ class AlexaMediaFlowHandler(config_entries.ConfigFlow):
)
else:
_LOGGER.debug("Using existing login")
if self.config.get("reauth") and self.config.get(CONF_EMAIL):
self.login.email = self.config[CONF_EMAIL]
if (
not self.config.get("reauth")
and user_input
@@ -623,6 +625,9 @@ class AlexaMediaFlowHandler(config_entries.ConfigFlow):
"""Handle reauth processing for the config flow."""
self._save_user_input_to_config(user_input)
self.config["reauth"] = True
self.reauth_entry = self.hass.config_entries.async_get_entry(
self.context["entry_id"]
)
reauth_schema = self._update_schema_defaults()
_LOGGER.debug(
"Creating reauth form with %s",
@@ -660,7 +665,16 @@ class AlexaMediaFlowHandler(config_entries.ConfigFlow):
email = login.email
_LOGGER.debug("Testing login status: %s", login.status)
if login.status and login.status.get("login_successful"):
existing_entry = await self.async_set_unique_id(f"{email} - {login.url}")
existing_entry = getattr(self, "reauth_entry", None)
_LOGGER.debug(
"Reauth existing entry: %s",
existing_entry.entry_id if existing_entry else None,
)
if existing_entry is None:
existing_entry = await self.async_set_unique_id(
f"{email} - {login.url}"
)
if self.config.get("reauth"):
self.config.pop("reauth")
if self.config.get(CONF_SECURITYCODE):
@@ -681,10 +695,61 @@ class AlexaMediaFlowHandler(config_entries.ConfigFlow):
)
self.hass.data[DATA_ALEXAMEDIA].setdefault("accounts", {})
self.hass.data[DATA_ALEXAMEDIA].setdefault("config_flows", {})
if existing_entry:
old_email = existing_entry.data.get(CONF_EMAIL)
accounts = self.hass.data[DATA_ALEXAMEDIA]["accounts"]
target_account = accounts.get(email)
if target_account is not None:
target_entry = target_account.get("config_entry")
if (
target_entry is None
or target_entry.entry_id != existing_entry.entry_id
):
_LOGGER.error(
"Cannot reauth %s: runtime account belongs to "
"another config entry",
hide_email(email),
)
return self.async_abort(reason="already_configured")
# Runtime accounts only exist for loaded entries, so also check
# every config entry (disabled or not yet loaded) for ownership.
new_unique_id = f"{login.email} - {login.url}"
for other_entry in self.hass.config_entries.async_entries(DOMAIN):
if other_entry.entry_id == existing_entry.entry_id:
continue
if other_entry.unique_id == new_unique_id:
_LOGGER.error(
"Cannot reauth %s: another config entry already "
"owns this account",
hide_email(email),
)
return self.async_abort(reason="already_configured")
if old_email and old_email != email and old_email in accounts:
_LOGGER.debug(
"Moving Alexa runtime account from %s to %s",
hide_email(old_email),
hide_email(email),
)
account_data = accounts.pop(old_email)
account_data["config_entry"] = existing_entry
account_data["login_obj"] = self.login
accounts[email] = account_data
else:
if email in accounts:
accounts[email]["login_obj"] = self.login
self.hass.config_entries.async_update_entry(
existing_entry, data=self.config
existing_entry,
data=self.config,
title=f"{login.email} - {login.url}",
unique_id=f"{login.email} - {login.url}",
)
_LOGGER.debug("Reauth successful for %s", hide_email(email))
self.hass.bus.async_fire(
"alexa_media_relogin_success",
@@ -696,15 +761,6 @@ class AlexaMediaFlowHandler(config_entries.ConfigFlow):
self.hass,
notification_id,
)
if not self.hass.data[DATA_ALEXAMEDIA]["accounts"].get(
self.config[CONF_EMAIL]
):
self.hass.data[DATA_ALEXAMEDIA]["accounts"][
self.config[CONF_EMAIL]
] = {}
self.hass.data[DATA_ALEXAMEDIA]["accounts"][self.config[CONF_EMAIL]][
"login_obj"
] = self.login
self.hass.data[DATA_ALEXAMEDIA]["config_flows"][
f"{email} - {login.url}"
] = None
+2 -7
View File
@@ -8,11 +8,6 @@
"iot_class": "cloud_polling",
"issue_tracker": "https://github.com/alandtse/alexa_media_player/issues",
"loggers": ["alexapy", "authcaptureproxy"],
"requirements": [
"alexapy==1.30.1",
"packaging>=20.3",
"wrapt>=1.14.0",
"dictor>=0.1.12,<0.2"
],
"version": "5.16.1"
"requirements": ["alexapy==1.31.0", "wrapt>=1.14.0", "dictor>=0.1.12,<0.2"],
"version": "5.16.2"
}
+3 -2
View File
@@ -211,8 +211,9 @@ class AlexaNotificationService(BaseNotificationService):
try:
targets = json.loads(targets)
except json.JSONDecodeError:
_LOGGER.error("Target must be a valid json")
return
targets = [targets]
if targets is None:
targets = []
processed_targets = []
for target in targets:
_LOGGER.debug("Processing: %s", target)
+1
View File
@@ -215,6 +215,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
hass, entry, implementation
),
log_traces=current_options.get(CONF_LOG_TRACES, DEFAULT_LOG_TRACES),
state_yaml=hass.config.path("hilo_state.yaml"),
)
except (TimeoutError, client_exceptions.ClientConnectorError):
+2 -2
View File
@@ -11,6 +11,6 @@
"documentation": "https://github.com/dvd-dev/hilo",
"iot_class": "cloud_push",
"issue_tracker": "https://github.com/dvd-dev/hilo/issues",
"requirements": ["python-hilo==2026.9.1"],
"version": "2026.10.1"
"requirements": ["python-hilo==2026.10.1"],
"version": "2026.10.2"
}
+3 -1
View File
@@ -739,7 +739,9 @@ class HiloRewardSensor(HiloEntity, RestoreEntity, SensorEntity):
Platform.SENSOR,
)
LOG.debug("Setting up RewardSensor entity: %s", self._attr_name)
self._history_state_yaml: str = "hilo_eventhistory_state.yaml"
self._history_state_yaml: str = hilo._hass.config.path(
"hilo_eventhistory_state.yaml"
)
self.scan_interval = timedelta(seconds=REWARD_SCAN_INTERVAL)
self._state = 0
self._history = []