This commit is contained in:
2026-01-16 19:55:20 +00:00
parent c18e93406a
commit 828d84caa3

View File

@@ -1,166 +1,80 @@
from __future__ import annotations
import logging
from dataclasses import dataclass
from typing import Any
from datetime import timedelta
from homeassistant.components.update import UpdateEntity, UpdateEntityFeature
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.entity import EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.components.panel_custom import async_register_panel
from homeassistant.helpers.event import async_track_time_interval
from homeassistant.helpers.discovery import async_load_platform
from .core import DOMAIN, SIGNAL_UPDATED, BCSCore
from .core import BCSCore, BCSConfig, BCSError
_LOGGER = logging.getLogger(__name__)
DOMAIN = "bahmcloud_store"
DEFAULT_STORE_URL = "https://git.bahmcloud.de/bahmcloud/ha_store/raw/branch/main/store.yaml"
CONF_STORE_URL = "store_url"
async def async_setup(hass: HomeAssistant, config: dict) -> bool:
cfg = config.get(DOMAIN, {}) or {}
store_url = cfg.get(CONF_STORE_URL, DEFAULT_STORE_URL)
core = BCSCore(hass, BCSConfig(store_url=store_url))
hass.data[DOMAIN] = core
await core.async_initialize()
# Provide native Update entities in Settings -> System -> Updates.
# This integration is YAML-based (async_setup), therefore we load the platform manually.
await async_load_platform(hass, "update", DOMAIN, {}, config)
from .views import (
StaticAssetsView,
BCSApiView,
BCSReadmeView,
BCSCustomRepoView,
BCSInstallView,
BCSUpdateView,
BCSRestartView,
)
hass.http.register_view(StaticAssetsView())
hass.http.register_view(BCSApiView(core))
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={},
)
def _pretty_repo_name(core: BCSCore, repo_id: str) -> str:
"""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}"
@dataclass(frozen=True)
class _RepoKey:
repo_id: str
class BCSRepoUpdateEntity(UpdateEntity):
"""Update entity representing a BCS-managed repository."""
_attr_entity_category = EntityCategory.DIAGNOSTIC
_attr_supported_features = UpdateEntityFeature.INSTALL
def __init__(self, core: BCSCore, repo_id: str) -> None:
self._core = core
self._repo_id = repo_id
self._in_progress = False
# 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
)
self._in_progress = True
self.async_write_ha_state()
await core.full_refresh(source="startup")
except BCSError as e:
_LOGGER.error("Initial refresh failed: %s", e)
async def periodic(_now) -> None:
try:
await self._core.update_repo(self._repo_id)
finally:
self._in_progress = False
self.async_write_ha_state()
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)
async_track_time_interval(hass, periodic, timedelta(seconds=interval_seconds))
@callback
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] = []
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)
return True