Updated apps
This commit is contained in:
@@ -0,0 +1,174 @@
|
||||
"""Nudge HACS to refresh this component's repository info when a newer
|
||||
component is detected (#1783/#1785 follow-up).
|
||||
|
||||
The component notices a newer custom component quickly — it checks PyPI every
|
||||
6 hours (and on every reload/restart) and the auto-update gate reads the
|
||||
component version shipped at each server release's tag. HACS, by contrast,
|
||||
refreshes a *custom* repository's release data only about every 48 hours, so
|
||||
the component update the hold (or the component-outdated repair) is waiting on
|
||||
is usually not yet visible in HACS. This module runs the same force-refresh
|
||||
that HACS's own repository "Update information" menu action performs, so HACS's
|
||||
update entity flips promptly and Home Assistant advertises the component update
|
||||
natively instead of the user waiting out HACS's cache.
|
||||
|
||||
HACS registers no service and ``homeassistant.update_entity`` is a no-op on its
|
||||
entities, so there is no supported API for this: the refresh reaches directly
|
||||
into HACS internals (``hass.data["hacs"]``). Those internals can change under us
|
||||
at any HACS release, so EVERY access here is defensive and the whole interaction
|
||||
is advisory — any failure degrades to a debug log and never touches the caller's
|
||||
update-check path (which runs on bring-up, gating webhook registration, and on
|
||||
the version coordinator's listener). The same unsupported ``hass.data["hacs"]``
|
||||
reach as install_source_check, but deliberately logged at debug where that
|
||||
module warns: this path retries on every 6h check, so a persistent HACS shape
|
||||
change would otherwise warn forever about an advisory nicety.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from .const import (
|
||||
DOMAIN,
|
||||
HACS_LEGACY_REPO_FULL_NAME,
|
||||
HACS_MIRROR_REPO_FULL_NAME,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
# hass.data[DOMAIN] sub-key holding the SET of component versions HACS was
|
||||
# already asked to refresh for. The hold check runs every 6h; without this the
|
||||
# same pending version would force a fresh HACS network refresh on every pass.
|
||||
# A set, not a single string: the update hold nudges with the shipped version
|
||||
# while the component-outdated check nudges with the required version, and a
|
||||
# scalar marker would ping-pong between the two, re-refreshing every pass
|
||||
# (review finding). Distinct from the other DOMAIN sub-keys (const.py) so both
|
||||
# entry types can share hass.data[DOMAIN].
|
||||
_DATA_HACS_NUDGED_VERSIONS = "hacs_nudged_versions"
|
||||
|
||||
# The repository full_names (owner/repo) HACS may track this component under, in
|
||||
# lookup order: the dedicated mirror first (the current install path), the
|
||||
# legacy main-repo path second (pre-mirror installs — see install_source_check).
|
||||
_CANDIDATE_REPO_FULL_NAMES = (
|
||||
HACS_MIRROR_REPO_FULL_NAME,
|
||||
HACS_LEGACY_REPO_FULL_NAME,
|
||||
)
|
||||
|
||||
|
||||
def async_schedule_hacs_nudge(hass: HomeAssistant, target_version: str) -> None:
|
||||
"""Fire-and-forget the HACS refresh nudge for ``target_version``.
|
||||
|
||||
Scheduled as a background task rather than awaited: ``update_repository``
|
||||
makes a GitHub network call, and the callers must not block on it — the
|
||||
component-compat check is awaited inside the server bring-up *before*
|
||||
webhook registration (blocking it would delay the connect URLs for a
|
||||
display-only refresh), and the auto-update hold runs on the version
|
||||
coordinator's listener. ``async_create_task`` keeps a strong reference so
|
||||
the task is not garbage-collected mid-flight; every failure is contained
|
||||
inside :func:`async_nudge_hacs_refresh`.
|
||||
"""
|
||||
hass.async_create_task(
|
||||
async_nudge_hacs_refresh(hass, target_version),
|
||||
f"{DOMAIN}_hacs_nudge",
|
||||
)
|
||||
|
||||
|
||||
async def async_nudge_hacs_refresh(hass: HomeAssistant, target_version: str) -> None:
|
||||
"""Ask HACS to re-fetch this component's repository info for ``target_version``.
|
||||
|
||||
Throttled to at most one refresh per detected component version (the marker
|
||||
lives in ``hass.data[DOMAIN]``): the hold check repeats every 6h, and
|
||||
hammering HACS's GitHub fetch each pass for the same pending version would
|
||||
be pointless. Absent / broken HACS and a not-yet-registered repository stay
|
||||
unthrottled so a later pass (once HACS is ready) still gets its one refresh.
|
||||
"""
|
||||
domain_data = hass.data.setdefault(DOMAIN, {})
|
||||
nudged_versions: set[str] = domain_data.setdefault(
|
||||
_DATA_HACS_NUDGED_VERSIONS, set()
|
||||
)
|
||||
if target_version in nudged_versions:
|
||||
# Already refreshed HACS for this pending component version.
|
||||
return
|
||||
|
||||
try:
|
||||
refreshed = await _async_force_hacs_repo_refresh(hass)
|
||||
except Exception:
|
||||
# HACS internals are unsupported and may change shape (missing hacs,
|
||||
# renamed attributes, a network failure inside update_repository); any
|
||||
# of it must degrade to a debug log, never fault the caller.
|
||||
_LOGGER.debug(
|
||||
"HA-MCP: could not nudge HACS to refresh the component repository",
|
||||
exc_info=True,
|
||||
)
|
||||
return
|
||||
|
||||
if refreshed:
|
||||
# Throttle only on a completed refresh, so a transient miss (HACS not
|
||||
# set up yet, repo not registered this pass) is retried next check
|
||||
# rather than suppressed for this version forever.
|
||||
nudged_versions.add(target_version)
|
||||
|
||||
|
||||
async def _async_force_hacs_repo_refresh(hass: HomeAssistant) -> bool:
|
||||
"""Run HACS's "Update information" force-refresh for this component's repo.
|
||||
|
||||
Returns True when a tracked repository was found and its refresh completed,
|
||||
False when there is nothing to refresh (no HACS, or no INSTALLED repository
|
||||
under either candidate name). Reaches into HACS internals —
|
||||
the top-level lookups are ``getattr``-guarded so a wholly different HACS
|
||||
shape returns False cleanly; anything deeper that changes shape raises and is
|
||||
swallowed by :func:`async_nudge_hacs_refresh`.
|
||||
"""
|
||||
hacs = hass.data.get("hacs")
|
||||
if hacs is None:
|
||||
# No HACS (manual/copy install, or HACS set up later this run) — nothing
|
||||
# to refresh; the caller leaves the throttle unset so a later pass retries.
|
||||
return False
|
||||
|
||||
repositories = getattr(hacs, "repositories", None)
|
||||
get_by_full_name = getattr(repositories, "get_by_full_name", None)
|
||||
if get_by_full_name is None:
|
||||
return False
|
||||
|
||||
repository = None
|
||||
for full_name in _CANDIDATE_REPO_FULL_NAMES:
|
||||
candidate = get_by_full_name(full_name)
|
||||
if candidate is None:
|
||||
continue
|
||||
# HACS keeps a repository record for every ADDED repo, downloaded or
|
||||
# not, but only creates an update entity for DOWNLOADED ones — and a
|
||||
# legacy->mirror migration can leave the mirror added but not yet
|
||||
# (re)installed while the running component is still tracked under the
|
||||
# legacy record. Refreshing an uninstalled record lights up nothing, so
|
||||
# only an installed candidate counts (review finding).
|
||||
if not getattr(getattr(candidate, "data", None), "installed", False):
|
||||
continue
|
||||
repository = candidate
|
||||
break
|
||||
if repository is None:
|
||||
return False
|
||||
|
||||
# The repository's "Update information" menu action: re-fetch its release
|
||||
# data ignoring cached state, then push the fresh data to HACS's own update
|
||||
# entity so Home Assistant advertises the component update immediately.
|
||||
await repository.update_repository(ignore_issues=True, force=True)
|
||||
# The refresh is complete at this point; the listener push below only
|
||||
# re-publishes the fresh data to HACS's update entity sooner. Guarded
|
||||
# separately so a HACS shape change here cannot void the completed
|
||||
# refresh's throttle and re-run the network fetch every pass (review
|
||||
# finding).
|
||||
try:
|
||||
coordinators = getattr(hacs, "coordinators", None) or {}
|
||||
category = getattr(getattr(repository, "data", None), "category", None)
|
||||
coordinator = coordinators.get(category)
|
||||
if coordinator is not None:
|
||||
coordinator.async_update_listeners()
|
||||
except Exception:
|
||||
_LOGGER.debug(
|
||||
"HA-MCP: HACS listener push after the repository refresh failed",
|
||||
exc_info=True,
|
||||
)
|
||||
return True
|
||||
Reference in New Issue
Block a user