Button fix
This commit is contained in:
@@ -93,8 +93,7 @@ class BCSCore:
|
|||||||
async def async_initialize(self) -> None:
|
async def async_initialize(self) -> None:
|
||||||
"""Async initialization that avoids blocking file IO."""
|
"""Async initialization that avoids blocking file IO."""
|
||||||
self.version = await self._read_manifest_version_async()
|
self.version = await self._read_manifest_version_async()
|
||||||
# Keep installed state in sync with /config/custom_components.
|
await self._refresh_installed_cache()
|
||||||
await self.reconcile_installed()
|
|
||||||
|
|
||||||
# After a successful HA restart, restart-required is no longer relevant.
|
# After a successful HA restart, restart-required is no longer relevant.
|
||||||
self._clear_restart_required_issue()
|
self._clear_restart_required_issue()
|
||||||
@@ -156,8 +155,6 @@ class BCSCore:
|
|||||||
async def full_refresh(self, source: str = "manual") -> None:
|
async def full_refresh(self, source: str = "manual") -> None:
|
||||||
"""Single refresh entry-point used by both timer and manual button."""
|
"""Single refresh entry-point used by both timer and manual button."""
|
||||||
_LOGGER.info("BCS full refresh triggered (source=%s)", source)
|
_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()
|
await self.refresh()
|
||||||
self.signal_updated()
|
self.signal_updated()
|
||||||
|
|
||||||
@@ -517,76 +514,94 @@ class BCSCore:
|
|||||||
|
|
||||||
return await self.hass.async_add_executor_job(_read)
|
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:
|
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:
|
try:
|
||||||
items = await self.storage.list_installed_repos()
|
items = await self.storage.list_installed_repos()
|
||||||
cache: dict[str, Any] = {}
|
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:
|
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] = {
|
cache[it.repo_id] = {
|
||||||
"installed": True,
|
"installed": True,
|
||||||
"domains": it.domains,
|
"domains": domains,
|
||||||
"installed_version": it.installed_version,
|
"installed_version": it.installed_version,
|
||||||
"installed_manifest_version": it.installed_manifest_version,
|
"installed_manifest_version": it.installed_manifest_version,
|
||||||
"ref": it.ref,
|
"ref": it.ref,
|
||||||
"installed_at": it.installed_at,
|
"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
|
self._installed_cache = cache
|
||||||
except Exception:
|
except Exception:
|
||||||
self._installed_cache = {}
|
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]:
|
async def install_repo(self, repo_id: str) -> dict[str, Any]:
|
||||||
repo = self.get_repo(repo_id)
|
repo = self.get_repo(repo_id)
|
||||||
if not repo:
|
if not repo:
|
||||||
@@ -662,48 +677,5 @@ class BCSCore:
|
|||||||
_LOGGER.info("BCS update started: repo_id=%s", repo_id)
|
_LOGGER.info("BCS update started: repo_id=%s", repo_id)
|
||||||
return await self.install_repo(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:
|
async def request_restart(self) -> None:
|
||||||
await self.hass.services.async_call("homeassistant", "restart", {}, blocking=False)
|
await self.hass.services.async_call("homeassistant", "restart", {}, blocking=False)
|
||||||
Reference in New Issue
Block a user