pyscript-Backend zur echten HA-Integration umgebaut (HACS-fähig)
Das Backend liegt jetzt als custom_components/audi_dashboard/ vor - eine normale Home-Assistant-Integration mit Config-Flow, einer sensor-Plattform und 18 Diensten. Damit ist die App über HACS installierbar; bis das Repo auf GitHub gespiegelt ist (HACS spricht ausschließlich mit GitHub), installiert homeassistant/installationspaket/install.ps1 denselben Ordner ohne HACS. Fünf Installationsschritte entfallen ersatzlos: der pyscript:-Block, der panel_custom:-Block, das Kopieren der Oberfläche nach www/, das langlebige Zugriffstoken (der Verlauf wird direkt über die recorder-API gelesen) und "pip install pypdf" (steht in manifest.json). Das Fahrzeugprofil legt die Integration beim ersten Start aus ihrer Vorlage an. Drei alte Schwächen sind dabei mit erledigt: - Die Nutzlast landet nicht mehr in der Recorder-Datenbank (_unrecorded_attributes - das kann nur eine echte Entität). - Eine laufende Fahrt überlebt einen Neustart (Store statt Arbeitsspeicher); fiel sie während eines Ausfalls ins Ende, schließt nach_neustart_fortsetzen() sie beim letzten aufgezeichneten Zeitpunkt. - Sensor-Zuordnungen wirken sofort - die Zustandsbeobachter werden neu gebunden, der Neustart-Hinweis und der Neustart-Dienst sind weg. Namensvertrag geändert, beide Oberflächen mitgezogen: pyscript.audi_dashboard_x -> sensor.audi_dashboard_x, pyscript.audi_dashboard_y -> audi_dashboard.y. Eine Companion-App vom alten Stand findet nach dem Umstieg nichts mehr und muss neu gebaut werden; das Panel liegt in der Integration und kann nicht driften. Der selbstgebaute Updater entfällt - HACS ist die Update-Mechanik, die Home Assistant kennt. Die Versionierung schrumpft auf eine Quelle: manifest.json. Geprüft am laufenden Testcontainer (Container byteweise identisch mit dem Repo): alle 18 Dienste, Panel, Config-Entry neu laden, Historienimport, echter Shell-Beleg in-process, Neuinstallation im Wegwerf-Container blank mit automatisch nachinstalliertem pypdf. Companion-App: tsc sauber, 112/112 Tests, beide Rauchtests gegen das laufende Backend grün. Belegparser 8/8. Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
@@ -0,0 +1,136 @@
|
||||
"""Audi Dashboard - Fahrtenbuch, Tankstatistik und Fahrzeugzustand als eigene
|
||||
Home-Assistant-Integration.
|
||||
|
||||
Diese Datei ist der Lebenszyklus: einrichten, entladen, neu laden. Die Fach-
|
||||
logik liegt in den Modulen daneben, der laufende Zustand im Koordinator.
|
||||
|
||||
WAS DIE INTEGRATION MITBRINGT, WAS FRÜHER VON HAND EINGERICHTET WERDEN MUSSTE
|
||||
-----------------------------------------------------------------------------
|
||||
Die pyscript-Fassung brauchte fünf getrennte Handgriffe in der Installation:
|
||||
einen `pyscript:`-Block mit allow_all_imports und hass_is_global, einen
|
||||
`panel_custom:`-Block, das Kopieren der Oberflächen-Dateien nach /config/www/,
|
||||
ein langlebiges Zugriffstoken in einer Textdatei und ein `pip install pypdf`
|
||||
im Container. Jeder davon konnte vergessen werden, und mehrere scheiterten
|
||||
still.
|
||||
|
||||
Alle fünf sind hier weg:
|
||||
- Das Panel meldet die Integration selbst an (kein YAML, kein Neustart).
|
||||
- Die Oberflächen-Dateien liegen in frontend/ und werden von hier unter
|
||||
einem eigenen URL-Pfad ausgeliefert (kein Kopieren nach www/).
|
||||
- Den Verlauf liest die Integration direkt über die recorder-API (kein
|
||||
Token, siehe verlauf.py).
|
||||
- pypdf steht über manifest.json bereit (kein pip-Aufruf, siehe belege.py).
|
||||
- Eingerichtet wird über die Oberfläche (config_flow), nicht über YAML.
|
||||
|
||||
Was NICHT hierher gehört und deshalb weiterhin von Hand in die
|
||||
configuration.yaml kommt: der `recorder:`-Block mit der verlängerten
|
||||
Aufbewahrung (recorder_snippet.yaml). Der betrifft die ganze Instanz und ihre
|
||||
Datenbankgröße - das ist eine Entscheidung des Betreibers, keine, die eine
|
||||
einzelne Integration für ihn treffen darf.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import os
|
||||
|
||||
from homeassistant.components.http import StaticPathConfig
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.components import frontend, panel_custom
|
||||
from homeassistant.loader import async_get_integration
|
||||
|
||||
from . import dienste
|
||||
from .const import (
|
||||
DOMAIN,
|
||||
PANEL_ICON,
|
||||
PANEL_KOMPONENTE,
|
||||
PANEL_TITEL,
|
||||
PANEL_URL,
|
||||
STATIK_ORDNER,
|
||||
STATIK_URL,
|
||||
)
|
||||
from .koordinator import Koordinator
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
PLATTFORMEN = [Platform.SENSOR]
|
||||
|
||||
type AudiConfigEntry = ConfigEntry[Koordinator]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: AudiConfigEntry) -> bool:
|
||||
integration = await async_get_integration(hass, DOMAIN)
|
||||
version = str(integration.version)
|
||||
|
||||
koordinator = Koordinator(hass, entry, version)
|
||||
entry.runtime_data = koordinator
|
||||
|
||||
await _statik_anmelden(hass)
|
||||
await koordinator.starten()
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATTFORMEN)
|
||||
await _panel_anmelden(hass, version)
|
||||
dienste.registrieren(hass, koordinator)
|
||||
|
||||
# Ändert sich die Einrichtung, wird die Integration neu geladen statt
|
||||
# halb angepasst - das ist die einzige Variante, bei der nichts von der
|
||||
# alten Einrichtung übrig bleiben kann.
|
||||
entry.async_on_unload(entry.add_update_listener(_neu_laden))
|
||||
|
||||
_LOGGER.info("Audi Dashboard %s eingerichtet", version)
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: AudiConfigEntry) -> bool:
|
||||
geladen = await hass.config_entries.async_unload_platforms(entry, PLATTFORMEN)
|
||||
if not geladen:
|
||||
return False
|
||||
dienste.abmelden(hass)
|
||||
frontend.async_remove_panel(hass, PANEL_URL)
|
||||
await entry.runtime_data.beenden()
|
||||
return True
|
||||
|
||||
|
||||
async def _neu_laden(hass: HomeAssistant, entry: AudiConfigEntry) -> None:
|
||||
await hass.config_entries.async_reload(entry.entry_id)
|
||||
|
||||
|
||||
async def _statik_anmelden(hass: HomeAssistant) -> None:
|
||||
"""Liefert frontend/ unter STATIK_URL aus.
|
||||
|
||||
Nur einmal je Home-Assistant-Lauf: aiohttp-Routen lassen sich nicht wieder
|
||||
entfernen, ein zweiter Versuch nach einem Neu-Laden der Integration würde
|
||||
scheitern. Das ist unkritisch - der Pfad zeigt auf einen Ordner, dessen
|
||||
Inhalt sich beim Neu-Laden nicht ändert.
|
||||
|
||||
cache_headers=False ist Absicht: die Dateien werden von HACS ersetzt, und
|
||||
ein Browser, der eine alte Fassung 31 Tage lang aus dem Cache bedient
|
||||
(die HA-Vorgabe für statische Pfade), wäre genau die stille Veraltung, die
|
||||
das Versions-Verfahren verhindern soll."""
|
||||
if hass.data.get(DOMAIN, {}).get("statik"):
|
||||
return
|
||||
ordner = os.path.join(os.path.dirname(__file__), STATIK_ORDNER)
|
||||
await hass.http.async_register_static_paths(
|
||||
[StaticPathConfig(STATIK_URL, ordner, False)]
|
||||
)
|
||||
hass.data.setdefault(DOMAIN, {})["statik"] = True
|
||||
|
||||
|
||||
async def _panel_anmelden(hass: HomeAssistant, version: str) -> None:
|
||||
"""Meldet den Sidebar-Eintrag an.
|
||||
|
||||
Die Version hängt als Query-Parameter an der Modul-URL: sie ändert sich
|
||||
genau dann, wenn eine neue Fassung installiert wurde, und ist damit der
|
||||
passende Cache-Brecher für den Lade-Stub."""
|
||||
await panel_custom.async_register_panel(
|
||||
hass,
|
||||
frontend_url_path=PANEL_URL,
|
||||
webcomponent_name=PANEL_KOMPONENTE,
|
||||
sidebar_title=PANEL_TITEL,
|
||||
sidebar_icon=PANEL_ICON,
|
||||
module_url=f"{STATIK_URL}/audi-dashboard-panel.js?v={version}",
|
||||
embed_iframe=False,
|
||||
require_admin=False,
|
||||
config={"version": version, "statik": STATIK_URL},
|
||||
)
|
||||
Reference in New Issue
Block a user