8 files
This commit is contained in:
@@ -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 = []
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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 = []
|
||||
|
||||
Reference in New Issue
Block a user