Button fix
This commit is contained in:
@@ -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)
|
||||
Reference in New Issue
Block a user