custom_components/bahmcloud_store/core.py aktualisiert

This commit is contained in:
2026-01-15 17:17:48 +00:00
parent a9a681d801
commit 32946c1a98

View File

@@ -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 "<html" in raw.lower() or "<!doctype html" in raw.lower():
fallback = self._add_cache_buster(self._gitea_src_to_raw(store_url))
if fallback != url:
_LOGGER.debug("BCS store index looked like HTML, retrying raw URL")
_LOGGER.warning("BCS store index looked like HTML, retrying raw URL")
raw = await self._fetch_store_text(fallback)
url = fallback
except Exception as e:
raise BCSError(f"Failed fetching store index: {e}") from e
# Diagnostics
b = raw.encode("utf-8", errors="replace")
h = hashlib.sha256(b).hexdigest()[:12]
self.last_index_url = url
self.last_index_bytes = len(b)
self.last_index_hash = h
self.last_index_loaded_at = time.time()
_LOGGER.info(
"BCS index loaded: url=%s bytes=%s sha=%s",
self.last_index_url,
self.last_index_bytes,
self.last_index_hash,
)
try:
data = ha_yaml.parse_yaml(raw)
if not isinstance(data, dict):
@@ -243,6 +270,7 @@ class BCSCore:
)
)
_LOGGER.info("BCS index parsed: repos=%s refresh_seconds=%s", len(items), refresh_seconds)
return items, refresh_seconds
except Exception as e:
raise BCSError(f"Invalid store.yaml: {e}") from e
@@ -253,14 +281,12 @@ class BCSCore:
raise BCSError("Missing url")
c = await self.storage.add_custom_repo(url, name)
await self.refresh()
self.signal_updated()
await self.full_refresh(source="custom_repo_add")
return c
async def remove_custom_repo(self, repo_id: str) -> 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()