diff --git a/custom_components/bahmcloud_store/core.py b/custom_components/bahmcloud_store/core.py index 2a9a491..82258db 100644 --- a/custom_components/bahmcloud_store/core.py +++ b/custom_components/bahmcloud_store/core.py @@ -1,6 +1,7 @@ from __future__ import annotations import asyncio +import hashlib import json import logging import time @@ -14,8 +15,6 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.util import yaml as ha_yaml from .storage import BCSStorage, CustomRepo -from .views import StaticAssetsView, BCSApiView, BCSReadmeView -from .custom_repo_view import BCSCustomRepoView from .providers import fetch_repo_info, detect_provider, RepoInfo, fetch_readme_markdown from .metadata import fetch_repo_metadata, RepoMetadata @@ -67,16 +66,30 @@ class BCSCore: self.repos: dict[str, RepoItem] = {} self._listeners: list[callable] = [] - self.version: str = self._read_manifest_version() + # Will be loaded asynchronously (no blocking IO in event loop) + self.version: str = "unknown" - def _read_manifest_version(self) -> str: - try: - manifest_path = Path(__file__).resolve().parent / "manifest.json" - data = json.loads(manifest_path.read_text(encoding="utf-8")) - v = data.get("version") - return str(v) if v else "unknown" - except Exception: - return "unknown" + # Diagnostics (helps verify refresh behavior) + self.last_index_url: str | None = None + self.last_index_bytes: int | None = None + self.last_index_hash: str | None = None + self.last_index_loaded_at: float | None = None + + async def async_initialize(self) -> None: + """Async initialization that avoids blocking file IO.""" + self.version = await self._read_manifest_version_async() + + async def _read_manifest_version_async(self) -> str: + def _read() -> str: + try: + manifest_path = Path(__file__).resolve().parent / "manifest.json" + data = json.loads(manifest_path.read_text(encoding="utf-8")) + v = data.get("version") + return str(v) if v else "unknown" + except Exception: + return "unknown" + + return await self.hass.async_add_executor_job(_read) def add_listener(self, cb) -> None: self._listeners.append(cb) @@ -89,21 +102,11 @@ class BCSCore: pass async def full_refresh(self, source: str = "manual") -> None: - """Run a full store refresh and notify listeners. - - This is the single entry-point used by both the periodic timer and - the manual refresh button. - """ + """Single refresh entry-point used by both timer and manual button.""" _LOGGER.info("BCS full refresh triggered (source=%s)", source) await self.refresh() self.signal_updated() - async def register_http_views(self) -> None: - self.hass.http.register_view(StaticAssetsView()) - self.hass.http.register_view(BCSApiView(self)) - self.hass.http.register_view(BCSReadmeView(self)) - self.hass.http.register_view(BCSCustomRepoView(self)) - def get_repo(self, repo_id: str) -> RepoItem | None: return self.repos.get(repo_id) @@ -132,6 +135,13 @@ class BCSCore: await self._enrich_and_resolve(merged) self.repos = merged + _LOGGER.info( + "BCS refresh complete: repos=%s (index=%s, custom=%s)", + len(self.repos), + len([r for r in self.repos.values() if r.source == "index"]), + len([r for r in self.repos.values() if r.source == "custom"]), + ) + async def _enrich_and_resolve(self, merged: dict[str, RepoItem]) -> None: sem = asyncio.Semaphore(6) @@ -191,7 +201,7 @@ class BCSCore: "Expires": "0", } - async with session.get(url, timeout=20, headers=headers) as resp: + async with session.get(url, timeout=30, headers=headers) as resp: if resp.status != 200: raise BCSError(f"store_url returned {resp.status}") return await resp.text() @@ -206,15 +216,32 @@ class BCSCore: try: raw = await self._fetch_store_text(url) + # If we fetched a HTML page (wrong endpoint), attempt raw conversion. if " None: await self.storage.remove_custom_repo(repo_id) - await self.refresh() - self.signal_updated() + await self.full_refresh(source="custom_repo_remove") async def list_custom_repos(self) -> list[CustomRepo]: return await self.storage.list_custom_repos()