Button 1
This commit is contained in:
@@ -93,7 +93,8 @@ 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()
|
||||||
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.
|
# After a successful HA restart, restart-required is no longer relevant.
|
||||||
self._clear_restart_required_issue()
|
self._clear_restart_required_issue()
|
||||||
@@ -155,6 +156,8 @@ 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()
|
||||||
|
|
||||||
@@ -514,6 +517,59 @@ 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:
|
||||||
try:
|
try:
|
||||||
items = await self.storage.list_installed_repos()
|
items = await self.storage.list_installed_repos()
|
||||||
@@ -606,5 +662,48 @@ 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