custom_components/bahmcloud_store/core.py aktualisiert
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import hashlib
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import time
|
import time
|
||||||
@@ -14,8 +15,6 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
|||||||
from homeassistant.util import yaml as ha_yaml
|
from homeassistant.util import yaml as ha_yaml
|
||||||
|
|
||||||
from .storage import BCSStorage, CustomRepo
|
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 .providers import fetch_repo_info, detect_provider, RepoInfo, fetch_readme_markdown
|
||||||
from .metadata import fetch_repo_metadata, RepoMetadata
|
from .metadata import fetch_repo_metadata, RepoMetadata
|
||||||
|
|
||||||
@@ -67,16 +66,30 @@ class BCSCore:
|
|||||||
self.repos: dict[str, RepoItem] = {}
|
self.repos: dict[str, RepoItem] = {}
|
||||||
self._listeners: list[callable] = []
|
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:
|
# Diagnostics (helps verify refresh behavior)
|
||||||
try:
|
self.last_index_url: str | None = None
|
||||||
manifest_path = Path(__file__).resolve().parent / "manifest.json"
|
self.last_index_bytes: int | None = None
|
||||||
data = json.loads(manifest_path.read_text(encoding="utf-8"))
|
self.last_index_hash: str | None = None
|
||||||
v = data.get("version")
|
self.last_index_loaded_at: float | None = None
|
||||||
return str(v) if v else "unknown"
|
|
||||||
except Exception:
|
async def async_initialize(self) -> None:
|
||||||
return "unknown"
|
"""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:
|
def add_listener(self, cb) -> None:
|
||||||
self._listeners.append(cb)
|
self._listeners.append(cb)
|
||||||
@@ -89,21 +102,11 @@ class BCSCore:
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
async def full_refresh(self, source: str = "manual") -> None:
|
async def full_refresh(self, source: str = "manual") -> None:
|
||||||
"""Run a full store refresh and notify listeners.
|
"""Single refresh entry-point used by both timer and manual button."""
|
||||||
|
|
||||||
This is the single entry-point used by both the periodic timer and
|
|
||||||
the manual refresh button.
|
|
||||||
"""
|
|
||||||
_LOGGER.info("BCS full refresh triggered (source=%s)", source)
|
_LOGGER.info("BCS full refresh triggered (source=%s)", source)
|
||||||
await self.refresh()
|
await self.refresh()
|
||||||
self.signal_updated()
|
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:
|
def get_repo(self, repo_id: str) -> RepoItem | None:
|
||||||
return self.repos.get(repo_id)
|
return self.repos.get(repo_id)
|
||||||
|
|
||||||
@@ -132,6 +135,13 @@ class BCSCore:
|
|||||||
await self._enrich_and_resolve(merged)
|
await self._enrich_and_resolve(merged)
|
||||||
self.repos = 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:
|
async def _enrich_and_resolve(self, merged: dict[str, RepoItem]) -> None:
|
||||||
sem = asyncio.Semaphore(6)
|
sem = asyncio.Semaphore(6)
|
||||||
|
|
||||||
@@ -191,7 +201,7 @@ class BCSCore:
|
|||||||
"Expires": "0",
|
"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:
|
if resp.status != 200:
|
||||||
raise BCSError(f"store_url returned {resp.status}")
|
raise BCSError(f"store_url returned {resp.status}")
|
||||||
return await resp.text()
|
return await resp.text()
|
||||||
@@ -206,15 +216,32 @@ class BCSCore:
|
|||||||
try:
|
try:
|
||||||
raw = await self._fetch_store_text(url)
|
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():
|
if "<html" in raw.lower() or "<!doctype html" in raw.lower():
|
||||||
fallback = self._add_cache_buster(self._gitea_src_to_raw(store_url))
|
fallback = self._add_cache_buster(self._gitea_src_to_raw(store_url))
|
||||||
if fallback != 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)
|
raw = await self._fetch_store_text(fallback)
|
||||||
|
url = fallback
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise BCSError(f"Failed fetching store index: {e}") from 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:
|
try:
|
||||||
data = ha_yaml.parse_yaml(raw)
|
data = ha_yaml.parse_yaml(raw)
|
||||||
if not isinstance(data, dict):
|
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
|
return items, refresh_seconds
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise BCSError(f"Invalid store.yaml: {e}") from e
|
raise BCSError(f"Invalid store.yaml: {e}") from e
|
||||||
@@ -253,14 +281,12 @@ class BCSCore:
|
|||||||
raise BCSError("Missing url")
|
raise BCSError("Missing url")
|
||||||
|
|
||||||
c = await self.storage.add_custom_repo(url, name)
|
c = await self.storage.add_custom_repo(url, name)
|
||||||
await self.refresh()
|
await self.full_refresh(source="custom_repo_add")
|
||||||
self.signal_updated()
|
|
||||||
return c
|
return c
|
||||||
|
|
||||||
async def remove_custom_repo(self, repo_id: str) -> None:
|
async def remove_custom_repo(self, repo_id: str) -> None:
|
||||||
await self.storage.remove_custom_repo(repo_id)
|
await self.storage.remove_custom_repo(repo_id)
|
||||||
await self.refresh()
|
await self.full_refresh(source="custom_repo_remove")
|
||||||
self.signal_updated()
|
|
||||||
|
|
||||||
async def list_custom_repos(self) -> list[CustomRepo]:
|
async def list_custom_repos(self) -> list[CustomRepo]:
|
||||||
return await self.storage.list_custom_repos()
|
return await self.storage.list_custom_repos()
|
||||||
|
|||||||
Reference in New Issue
Block a user