This commit is contained in:
2026-01-17 08:04:02 +00:00
parent 1a1ebd3821
commit e10624df6b

View File

@@ -93,7 +93,8 @@ class BCSCore:
async def async_initialize(self) -> None:
"""Async initialization that avoids blocking file IO."""
self.version = await self._read_manifest_version_async()
await self._refresh_installed_cache()
# Keep installed state in sync with /config/custom_components.
await self.reconcile_installed()
# After a successful HA restart, restart-required is no longer relevant.
self._clear_restart_required_issue()
@@ -155,6 +156,8 @@ class BCSCore:
async def full_refresh(self, source: str = "manual") -> None:
"""Single refresh entry-point used by both timer and manual button."""
_LOGGER.info("BCS full refresh triggered (source=%s)", source)
# Keep installed state consistent even if users deleted folders manually.
await self.reconcile_installed()
await self.refresh()
self.signal_updated()
@@ -514,6 +517,59 @@ class BCSCore:
return await self.hass.async_add_executor_job(_read)
async def reconcile_installed(self) -> dict[str, int]:
"""Reconcile installed repos with /config/custom_components.
Users may delete integrations manually. In that case, our persistent
installed registry would be stale and the UI would incorrectly show
them as installed. We fix this by checking for each stored domain
directory whether it still exists and contains a manifest.json.
Returns diagnostics: {"removed": X, "updated": Y}.
"""
removed = 0
updated = 0
try:
items = await self.storage.list_installed_repos()
except Exception:
items = []
for it in items:
keep_domains: list[str] = []
for domain in (it.domains or []):
d = str(domain).strip()
if not d:
continue
p = Path(self.hass.config.path("custom_components", d))
if p.is_dir() and (p / "manifest.json").exists():
keep_domains.append(d)
if not keep_domains:
try:
await self.storage.remove_installed_repo(it.repo_id)
removed += 1
except Exception:
pass
continue
if keep_domains != (it.domains or []):
try:
await self.storage.set_installed_repo(
repo_id=it.repo_id,
url=it.url,
domains=keep_domains,
installed_version=it.installed_version,
installed_manifest_version=it.installed_manifest_version,
ref=it.ref or it.installed_version,
)
updated += 1
except Exception:
pass
await self._refresh_installed_cache()
return {"removed": removed, "updated": updated}
async def _refresh_installed_cache(self) -> None:
try:
items = await self.storage.list_installed_repos()
@@ -606,5 +662,48 @@ class BCSCore:
_LOGGER.info("BCS update started: repo_id=%s", repo_id)
return await self.install_repo(repo_id)
async def uninstall_repo(self, repo_id: str) -> dict[str, Any]:
"""Uninstall a previously installed repository.
This removes all integration folders (domains) that were installed
from the repo under /config/custom_components and clears the installed
marker in storage.
"""
async with self._install_lock:
installed = await self.storage.get_installed_repo(repo_id)
if not installed:
# Reconcile first (maybe it was removed manually).
await self.reconcile_installed()
installed = await self.storage.get_installed_repo(repo_id)
if not installed:
raise BCSInstallError(f"repo_id is not installed: {repo_id}")
domains = [str(d) for d in (installed.domains or []) if str(d).strip()]
removed_domains: list[str] = []
def _remove_domain_dir(domain: str) -> None:
p = Path(self.hass.config.path("custom_components", domain))
if p.exists() and p.is_dir():
shutil.rmtree(p, ignore_errors=True)
for domain in domains:
await self.hass.async_add_executor_job(_remove_domain_dir, domain)
removed_domains.append(domain)
await self.storage.remove_installed_repo(repo_id)
await self._refresh_installed_cache()
# Removing integrations also requires a restart to unload code cleanly.
self._mark_restart_required()
_LOGGER.info("BCS uninstall complete: repo_id=%s domains=%s", repo_id, removed_domains)
self.signal_updated()
return {
"ok": True,
"repo_id": repo_id,
"domains": removed_domains,
"restart_required": True,
}
async def request_restart(self) -> None:
await self.hass.services.async_call("homeassistant", "restart", {}, blocking=False)