From 3bf01c91f1f7abcc9c8a8fe92df67edcd9844857 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Bachmann?= Date: Sat, 17 Jan 2026 08:15:32 +0000 Subject: [PATCH] Button fix --- custom_components/bahmcloud_store/core.py | 174 +++++++++------------- 1 file changed, 73 insertions(+), 101 deletions(-) diff --git a/custom_components/bahmcloud_store/core.py b/custom_components/bahmcloud_store/core.py index 055e42d..ec6a8d4 100644 --- a/custom_components/bahmcloud_store/core.py +++ b/custom_components/bahmcloud_store/core.py @@ -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) \ No newline at end of file