custom_components/bahmcloud_store/store.py aktualisiert

This commit is contained in:
2026-01-14 18:47:12 +00:00
parent 3875d29d16
commit 7b748a0f0b

View File

@@ -35,7 +35,7 @@ class StoreConfig:
class Package: class Package:
id: str id: str
name: str name: str
type: str type: str # "integration" | "store"
domain: str domain: str
repo: str repo: str
owner: str owner: str
@@ -76,12 +76,19 @@ class BahmcloudStore:
u = urlparse(repo_url.rstrip("/")) u = urlparse(repo_url.rstrip("/"))
return f"{u.scheme}://{u.netloc}" return f"{u.scheme}://{u.netloc}"
@staticmethod
def _raw_manifest_url(repo: str, branch: str, source_path: str) -> str:
# Example:
# https://git.bahmcloud.de/bahmcloud/easy_proxmox/raw/branch/main/custom_components/easy_proxmox/manifest.json
return f"{repo.rstrip('/')}/raw/branch/{branch}/{source_path.rstrip('/')}/manifest.json"
async def _fetch_latest_version(self, pkg: Package) -> tuple[str | None, str | None]: async def _fetch_latest_version(self, pkg: Package) -> tuple[str | None, str | None]:
""" """
Returns (latest_version, release_url) Returns (latest_version, release_url)
Strategy: Strategy:
1) releases/latest -> tag_name 1) releases/latest -> tag_name
2) tags?limit=1 -> first tag name 2) tags?limit=1 -> first tag name
3) fallback: read manifest.json from repo (version field)
""" """
session = async_get_clientsession(self.hass) session = async_get_clientsession(self.hass)
base = self._base_from_repo(pkg.repo) base = self._base_from_repo(pkg.repo)
@@ -94,7 +101,8 @@ class BahmcloudStore:
data = await resp.json() data = await resp.json()
tag = data.get("tag_name") tag = data.get("tag_name")
html_url = data.get("html_url") html_url = data.get("html_url")
return (str(tag) if tag else None, str(html_url) if html_url else None) if tag:
return (str(tag), str(html_url) if html_url else None)
except Exception: except Exception:
pass pass
@@ -106,7 +114,21 @@ class BahmcloudStore:
tags = await resp.json() tags = await resp.json()
if tags and isinstance(tags, list): if tags and isinstance(tags, list):
name = tags[0].get("name") name = tags[0].get("name")
return (str(name) if name else None, None) if name:
return (str(name), None)
except Exception:
pass
# 3) fallback: manifest.json version from repo
try:
manifest_url = self._raw_manifest_url(pkg.repo, pkg.branch, pkg.source_path)
async with session.get(manifest_url, timeout=20) as resp:
if resp.status == 200:
text = await resp.text()
data = json.loads(text)
ver = data.get("version")
if ver:
return (str(ver), None)
except Exception: except Exception:
pass pass
@@ -198,6 +220,9 @@ class BahmcloudStore:
shutil.rmtree(target) shutil.rmtree(target)
shutil.copytree(src, target) shutil.copytree(src, target)
# Nach Installation: Entities neu aufbauen (damit es als Update auftaucht)
self.signal_entities_updated()
persistent_notification.async_create( persistent_notification.async_create(
self.hass, self.hass,
( (
@@ -228,7 +253,7 @@ class BahmcloudStore:
async def register_http_views(self) -> None: async def register_http_views(self) -> None:
"""Register HTTP views for static panel assets and JSON API.""" """Register HTTP views for static panel assets and JSON API."""
self.hass.http.register_view(_StaticView()) self.hass.http.register_view(_StaticView())
self.hass.http.register_view(_APIListView(self)) self.hass.http.register_view(_APIView(self))
class _StaticView(HomeAssistantView): class _StaticView(HomeAssistantView):
@@ -236,12 +261,6 @@ class _StaticView(HomeAssistantView):
IMPORTANT: IMPORTANT:
Custom Panel JS modules are loaded WITHOUT Authorization headers. Custom Panel JS modules are loaded WITHOUT Authorization headers.
Therefore static panel assets must be publicly accessible (no auth). Therefore static panel assets must be publicly accessible (no auth).
Serves:
/api/bahmcloud_store_static/index.html
/api/bahmcloud_store_static/panel.js
/api/bahmcloud_store_static/app.js
/api/bahmcloud_store_static/styles.css
""" """
requires_auth = False requires_auth = False
name = "bahmcloud_store_static" name = "bahmcloud_store_static"
@@ -254,7 +273,6 @@ class _StaticView(HomeAssistantView):
f = (base / path).resolve() f = (base / path).resolve()
# Prevent path traversal
if not str(f).startswith(str(base)) or not f.exists() or not f.is_file(): if not str(f).startswith(str(base)) or not f.exists() or not f.is_file():
return web.Response(status=404, text="Not found") return web.Response(status=404, text="Not found")
@@ -269,10 +287,11 @@ class _StaticView(HomeAssistantView):
return web.Response(body=f.read_bytes(), content_type="application/octet-stream") return web.Response(body=f.read_bytes(), content_type="application/octet-stream")
class _APIListView(HomeAssistantView): class _APIView(HomeAssistantView):
""" """
Store API MUST stay protected. Auth-protected API:
UI loads data via fetch() with HA auth handled by frontend. GET /api/bahmcloud_store -> list packages
POST /api/bahmcloud_store {op:...} -> install/update a package
""" """
requires_auth = True requires_auth = True
name = "bahmcloud_store_api" name = "bahmcloud_store_api"
@@ -300,3 +319,20 @@ class _APIListView(HomeAssistantView):
} }
) )
return self.json({"packages": items, "store_url": self.store.config.store_url}) return self.json({"packages": items, "store_url": self.store.config.store_url})
async def post(self, request):
data = await request.json()
op = data.get("op")
package_id = data.get("package_id")
if op not in ("install", "update"):
return self.json({"error": "unknown op"}, status_code=400)
if not package_id:
return self.json({"error": "package_id missing"}, status_code=400)
pkg = self.store.packages.get(package_id)
if not pkg:
return self.json({"error": "unknown package_id"}, status_code=404)
await self.store.install_from_zip(pkg)
return self.json({"ok": True})