diff --git a/custom_components/bahmcloud_store/__init__.py b/custom_components/bahmcloud_store/__init__.py index efbff00..1ca7c76 100644 --- a/custom_components/bahmcloud_store/__init__.py +++ b/custom_components/bahmcloud_store/__init__.py @@ -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) \ No newline at end of file + return True \ No newline at end of file