This commit is contained in:
2026-01-16 19:09:47 +00:00
parent 01576153d8
commit 5796012189

View File

@@ -1,75 +1,166 @@
from __future__ import annotations from __future__ import annotations
import logging import logging
from datetime import timedelta from dataclasses import dataclass
from typing import Any
from homeassistant.core import HomeAssistant from homeassistant.components.update import UpdateEntity, UpdateEntityFeature
from homeassistant.components.panel_custom import async_register_panel from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.entity import EntityCategory
from .core import BCSCore, BCSConfig, BCSError from .core import DOMAIN, SIGNAL_UPDATED, BCSCore
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
DOMAIN = "bahmcloud_store"
DEFAULT_STORE_URL = "https://git.bahmcloud.de/bahmcloud/ha_store/raw/branch/main/store.yaml" def _pretty_repo_name(core: BCSCore, repo_id: str) -> str:
CONF_STORE_URL = "store_url" """Return a human-friendly name for a repo update entity."""
try:
repo = core.get_repo(repo_id)
if repo and getattr(repo, "name", None):
name = str(repo.name).strip()
if name:
return name
except Exception:
pass
# Fallbacks
if repo_id.startswith("index:"):
return f"BCS Index {repo_id.split(':', 1)[1]}"
if repo_id.startswith("custom:"):
return f"BCS Custom {repo_id.split(':', 1)[1]}"
return f"BCS {repo_id}"
async def async_setup(hass: HomeAssistant, config: dict) -> bool: @dataclass(frozen=True)
cfg = config.get(DOMAIN, {}) or {} class _RepoKey:
store_url = cfg.get(CONF_STORE_URL, DEFAULT_STORE_URL) repo_id: str
core = BCSCore(hass, BCSConfig(store_url=store_url))
hass.data[DOMAIN] = core
await core.async_initialize() class BCSRepoUpdateEntity(UpdateEntity):
"""Update entity representing a BCS-managed repository."""
from .views import ( _attr_entity_category = EntityCategory.DIAGNOSTIC
StaticAssetsView, _attr_supported_features = UpdateEntityFeature.INSTALL
BCSApiView,
BCSReadmeView, def __init__(self, core: BCSCore, repo_id: str) -> None:
BCSCustomRepoView, self._core = core
BCSInstallView, self._repo_id = repo_id
BCSUpdateView, self._in_progress = False
BCSRestartView,
# Stable unique id (do NOT change)
self._attr_unique_id = f"{DOMAIN}:{repo_id}"
# Human-friendly name in UI
pretty = _pretty_repo_name(core, repo_id)
self._attr_name = pretty
# Title shown in the entity dialog
self._attr_title = pretty
@property
def available(self) -> bool:
repo = self._core.get_repo(self._repo_id)
installed = self._core.get_installed(self._repo_id)
return repo is not None and installed is not None
@property
def in_progress(self) -> bool | None:
return self._in_progress
@property
def installed_version(self) -> str | None:
installed = self._core.get_installed(self._repo_id) or {}
v = installed.get("installed_version") or installed.get("ref")
return str(v) if v else None
@property
def latest_version(self) -> str | None:
repo = self._core.get_repo(self._repo_id)
if not repo:
return None
v = getattr(repo, "latest_version", None)
return str(v) if v else None
@property
def update_available(self) -> bool:
latest = self.latest_version
installed = self.installed_version
if not latest or not installed:
return False
return latest != installed
def version_is_newer(self, latest_version: str, installed_version: str) -> bool:
return latest_version != installed_version
@property
def release_url(self) -> str | None:
repo = self._core.get_repo(self._repo_id)
return getattr(repo, "url", None) if repo else None
async def async_install(self, version: str | None, backup: bool, **kwargs: Any) -> None:
if version is not None:
_LOGGER.debug(
"BCS update entity requested specific version=%s (ignored)", version
) )
hass.http.register_view(StaticAssetsView()) self._in_progress = True
hass.http.register_view(BCSApiView(core)) self.async_write_ha_state()
hass.http.register_view(BCSReadmeView(core))
hass.http.register_view(BCSCustomRepoView(core))
hass.http.register_view(BCSInstallView(core))
hass.http.register_view(BCSUpdateView(core))
hass.http.register_view(BCSRestartView(core))
await async_register_panel(
hass,
frontend_url_path="bahmcloud-store",
webcomponent_name="bahmcloud-store-panel",
# IMPORTANT: bump v to avoid caching old JS
module_url="/api/bahmcloud_store_static/panel.js?v=101",
sidebar_title="Bahmcloud Store",
sidebar_icon="mdi:store",
require_admin=True,
config={},
)
try: try:
await core.full_refresh(source="startup") await self._core.update_repo(self._repo_id)
except BCSError as e: finally:
_LOGGER.error("Initial refresh failed: %s", e) self._in_progress = False
self.async_write_ha_state()
async def periodic(_now) -> None:
try:
await core.full_refresh(source="timer")
except BCSError as e:
_LOGGER.warning("Periodic refresh failed: %s", e)
except Exception as e: # pylint: disable=broad-exception-caught
_LOGGER.exception("Unexpected error during periodic refresh: %s", e)
interval_seconds = int(getattr(core, "refresh_seconds", 300) or 300) @callback
async_track_time_interval(hass, periodic, timedelta(seconds=interval_seconds)) def _sync_entities(
core: BCSCore,
existing: dict[str, BCSRepoUpdateEntity],
async_add_entities: AddEntitiesCallback,
) -> None:
"""Ensure there is one update entity per installed repo."""
installed_map = getattr(core, "_installed_cache", {}) or {}
new_entities: list[BCSRepoUpdateEntity] = []
return True for repo_id, data in installed_map.items():
if not isinstance(data, dict):
continue
if repo_id in existing:
continue
ent = BCSRepoUpdateEntity(core, repo_id)
existing[repo_id] = ent
new_entities.append(ent)
if new_entities:
async_add_entities(new_entities)
for ent in existing.values():
ent.async_write_ha_state()
async def async_setup_platform(
hass: HomeAssistant,
config,
async_add_entities: AddEntitiesCallback,
discovery_info=None,
):
"""Set up BCS update entities."""
core: BCSCore | None = hass.data.get(DOMAIN)
if not core:
_LOGGER.debug("BCS core not available, skipping update platform setup")
return
entities: dict[str, BCSRepoUpdateEntity] = {}
_sync_entities(core, entities, async_add_entities)
@callback
def _handle_update() -> None:
_sync_entities(core, entities, async_add_entities)
async_dispatcher_connect(hass, SIGNAL_UPDATED, _handle_update)