Button fix

This commit is contained in:
2026-01-17 08:15:32 +00:00
parent 7aa14284dd
commit 3bf01c91f1

View File

@@ -93,8 +93,7 @@ class BCSCore:
async def async_initialize(self) -> None:
"""Async initialization that avoids blocking file IO."""
self.version = await self._read_manifest_version_async()
# Keep installed state in sync with /config/custom_components.
await self.reconcile_installed()
await self._refresh_installed_cache()
# After a successful HA restart, restart-required is no longer relevant.
self._clear_restart_required_issue()
@@ -156,8 +155,6 @@ 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()
@@ -517,76 +514,94 @@ 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:
"""Refresh installed cache and reconcile with filesystem.
If a user manually deletes a domain folder under /config/custom_components,
we automatically remove the installed flag from our storage so the Store UI
does not show stale "installed" state.
"""
try:
items = await self.storage.list_installed_repos()
cache: dict[str, Any] = {}
# Determine which installed repos still exist on disk.
cc_root = Path(self.hass.config.path("custom_components"))
to_remove: list[str] = []
for it in items:
domains = [str(d) for d in (it.domains or []) if str(d).strip()]
# A repo is considered "present" if at least one of its domains
# exists and contains a manifest.json.
present = False
for d in domains:
p = cc_root / d
if p.is_dir() and (p / "manifest.json").exists():
present = True
break
if not present:
to_remove.append(it.repo_id)
continue
cache[it.repo_id] = {
"installed": True,
"domains": it.domains,
"domains": domains,
"installed_version": it.installed_version,
"installed_manifest_version": it.installed_manifest_version,
"ref": it.ref,
"installed_at": it.installed_at,
}
# Remove stale installed entries from storage.
for rid in to_remove:
try:
await self.storage.remove_installed_repo(rid)
_LOGGER.info("BCS reconcile: removed stale installed repo_id=%s", rid)
except Exception:
_LOGGER.debug("BCS reconcile: failed removing stale repo_id=%s", rid, exc_info=True)
self._installed_cache = cache
except Exception:
self._installed_cache = {}
async def uninstall_repo(self, repo_id: str) -> dict[str, Any]:
"""Uninstall a repository by deleting its installed domains and clearing storage."""
async with self._install_lock:
inst = await self.storage.get_installed_repo(repo_id)
if not inst:
# Already uninstalled.
await self._refresh_installed_cache()
self.signal_updated()
return {"ok": True, "repo_id": repo_id, "removed": [], "restart_required": False}
cc_root = Path(self.hass.config.path("custom_components"))
removed: list[str] = []
def _remove_dir(path: Path) -> None:
if path.exists() and path.is_dir():
shutil.rmtree(path, ignore_errors=True)
for domain in inst.domains:
d = str(domain).strip()
if not d:
continue
target = cc_root / d
await self.hass.async_add_executor_job(_remove_dir, target)
removed.append(d)
await self.storage.remove_installed_repo(repo_id)
await self._refresh_installed_cache()
# Show restart required in Settings.
if removed:
self._mark_restart_required()
_LOGGER.info("BCS uninstall complete: repo_id=%s removed_domains=%s", repo_id, removed)
self.signal_updated()
return {"ok": True, "repo_id": repo_id, "removed": removed, "restart_required": bool(removed)}
async def install_repo(self, repo_id: str) -> dict[str, Any]:
repo = self.get_repo(repo_id)
if not repo:
@@ -662,48 +677,5 @@ 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)