Ölwechsel/Inspektion-Anzeige repariert, Selbst-Update der Integration gebaut
Zwei getrennte Themen in einem Commit, beide in derselben Sitzung entstanden:
1. Ölwechsel/Inspektion wurden komplett ausgeblendet ("kein Eintrag im
Servicebuch"), sobald kein Servicebucheintrag vorlag - selbst wenn der
zugeordnete Sensor eine gültige Fälligkeit meldete. In beiden Frontends
prüfte die Anzeige nur den Servicebuch-Zweig, bevor sie die
Fahrzeugmeldung überhaupt las. Jetzt steht die Fahrzeugmeldung für sich;
fehlt zusätzlich ein Servicebucheintrag, übernimmt eine neue,
fahrtenlog-basierte Prognose (kmProTagAusFahrten()/meldungsPrognose())
die Hochrechnung statt der Servicebuch-Rate - deckelt auf die vom
Fahrzeug selbst gemeldete Zeitgrenze, falls zu wenig gefahren wird.
2. install.ps1 als Update-Weg wird von Windows Smart App Control blockiert,
ohne Umgehungsmöglichkeit. Die Integration lädt sich jetzt auf
Tastendruck selbst von Gitea (aktualisierung.py), verifiziert das
Manifest vor jedem Tausch und tauscht per os.rename mit automatischem
Rollback bei Fehlern - install.ps1 bleibt nur noch für die
Erstinstallation nötig. Zugangstoken über einen neuen OptionsFlow in
entry.options, nie in configuration.yaml.
Nebenbei: mehrere seit der HACS-Ausschluss-Entscheidung liegen gebliebene
falsche HACS-Referenzen in Code-Kommentaren und einem UI-Text korrigiert.
Details, Sicherheitsbegründung und Verifikationsstand in AGENTS.md,
Abschnitte I und J.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1926,6 +1926,30 @@ Gitea-vs-mirrored-GitHub changes it: HACS refuses private repos regardless of wh
|
||||
mirrored, and the private/public constraint (licensed Audi assets in the repo) isn't going away.
|
||||
`install.ps1` is the permanent, only install path, not a stand-in.
|
||||
|
||||
**Stale HACS claims found and fixed in code comments/UI text (2026-08-24).** The docs-level cleanup
|
||||
above happened the same day as this section was written, but a full-repo grep afterward (prompted by
|
||||
the owner spotting live UI text still claiming "Updates laufen über HACS") turned up leftovers the
|
||||
first pass missed, because it focused on the standalone docs (`README.md`, `INSTALL.md`, etc.) and
|
||||
not source comments or UI strings. Fixed: the panel's Einstellungen → Version tile literally told the
|
||||
user HACS would notice and install updates itself — replaced with the actual mechanism
|
||||
(`install.ps1`, panel ships inside the integration and can't go stale on its own). Also fixed,
|
||||
all now pointing at `install.ps1`/"jeder Auslieferung" instead of "HACS"/"HACS-Update": comments in
|
||||
`audi-dashboard-app.js` (the version-tracking block and the retired-updater history note),
|
||||
`companion-app/vite.config.ts` (manifest-version header comment), `const.py` (`STATIK_URL`,
|
||||
`BUENDEL_ORDNER`, `BILDER_ORDNER` comments), `bilder.py` (module docstring), `__init__.py`
|
||||
(`cache_headers=False` rationale), and a diagram branch in `VERSIONIERUNG.md` that stated "HACS
|
||||
vergleicht sie gegen das Repository und meldet Updates" as flat fact, contradicting the boxed note
|
||||
one screen above it. Left alone: `install.ps1`'s own header (already correctly explains the
|
||||
exclusion), `ota-paket.ps1`'s comment (already correct), and one debugging note in
|
||||
`audi-dashboard-app.js` (~line 4720) that recalls what was actually running in a specific test
|
||||
container during a past incident — a historical fact about that container, not a claim about how
|
||||
this project works, so not in scope for this cleanup. Also out of scope, a different topic entirely:
|
||||
mentions of the *retired vehicle-data-source* HACS integration `TommiG1/HA_VAG-EU-Data-Act` in older
|
||||
planning docs (`SPECIFICATION.md`, `COMPANION_APP_ARCHITECTURE.md`, `bauauftrag.md/html`,
|
||||
`testumgebung/`) — that HACS integration is unrelated to whether HACS can install *this* app; it was
|
||||
FMM003's predecessor as a data source, already documented as retired elsewhere in those same files.
|
||||
Manifest bumped to `2026.8.24.3` and `npm run ota` rerun (the panel text change is user-visible).
|
||||
|
||||
### The deployed-parity problem, and what was built for it (2026-08-23)
|
||||
|
||||
The parity rule (binding, above) guarantees *source* parity: both codebases change in the same
|
||||
@@ -2131,6 +2155,147 @@ was 1.00:1, white on white. Both pills now follow the app's own button language
|
||||
as a button fill. No companion-app port: the location sheet's share/route pills do not exist there,
|
||||
which the parity rule exempts as panel-only.
|
||||
|
||||
### I) Ölwechsel/Inspektion could vanish entirely with a mapped sensor and no Servicebuch (found and fixed 2026-08-24)
|
||||
|
||||
Owner reported: "no inspektion and ölwechsel shown even if there is the right sensor and
|
||||
herstellervorgabe." Both were true — sensor mapped correctly, "Herstellervorgabe" selected — and the
|
||||
row still just said "kein Eintrag im Servicebuch" (no Servicebuch entry).
|
||||
|
||||
**Root cause, identical in both codebases.** The Ölwechsel/Inspektion display combines two
|
||||
independent sources on purpose (see the header comment in `companion-app/src/screens/Service.tsx`):
|
||||
the vehicle's own reported due date/km (`fahrzeugMeldungRoh()` / `fahrzeug.oelwechselFaelligTs` etc.,
|
||||
sourced from the mapped HA sensor) is meant to stand *alongside* the app's own Servicebuch-based
|
||||
forecast, not depend on it. But the actual gating checked only the Servicebuch side:
|
||||
- Panel (`frontend/audi-dashboard-app.js`, `vService()`): `const iv = intervalle(t); if (!iv) return
|
||||
...„kein Eintrag im Servicebuch"...` — and `intervalle()` returns `null` whenever there's no
|
||||
Servicebuch basis, full stop. The vehicle-reported `fm`/`hatMeldung` was computed *after* this early
|
||||
return, so a mapped sensor with valid data never even got read for a fahrzeug with an empty
|
||||
Servicebuch — exactly the state of a freshly set-up car.
|
||||
- companion-app (`src/screens/Service.tsx`): same shape, `{oelHatEintrag ? ... : "kein Eintrag im
|
||||
Servicebuch"}` — gated purely on a Servicebuch entry existing, ignoring `oelHatMeldung`/
|
||||
`inspHatMeldung` computed one line above it. `src/daten/service.ts`'s `naechsterService()` (feeds
|
||||
the Home-screen "Nächster Service" tile) had the same gap one level up: it only ever built
|
||||
candidates from `oelwechselPrognose()`/`inspektionPrognose()` (both Servicebuch-only), never
|
||||
falling back to `fahrzeug.oelwechselFaelligTs`/`inspektionFaelligTs`.
|
||||
|
||||
**Fixed by reordering, not by adding a new data path** — the sensor data was already being read
|
||||
correctly by the backend and delivered to both frontends; only the display-gating ignored it.
|
||||
Panel: `fm`/`hatMeldung` now computed *before* the early-return check, which now reads `if (!iv &&
|
||||
!hatMeldung)`; `anzeige`/`herstellervorgabe` guarded for `iv` possibly being `null` (both remain safe
|
||||
because the code paths that dereference `iv` only run when `hatMeldung` is false, and the new guard
|
||||
guarantees `iv` is non-null whenever that's the case). companion-app: `oelHatEintrag ||
|
||||
oelHatMeldung` / `inspHatEintrag || inspHatMeldung` as the render condition; `naechsterService()`
|
||||
gained an `else if (fahrzeug.oelwechselFaelligTs)` / `...inspektionFaelligTs` fallback branch per
|
||||
service type, added only when the Servicebuch-based prognose is `null` — a real Servicebuch entry
|
||||
still wins when both exist (own forecast is finer-grained, factoring actual driven km/day). The
|
||||
Hauptuntersuchung path is unaffected either way: `fahrzeugMeldungRoh("Hauptuntersuchung")` always
|
||||
returns `{ts: null, km: null}`, so `hatMeldung` is always `false` for it — same behavior as before.
|
||||
|
||||
Verified: `tsc --noEmit` clean, all 120 companion-app tests pass (including 3 new regression tests in
|
||||
`service.test.ts` covering the meldung-only fallback for both service types and confirming a real
|
||||
Servicebuch prognose still takes priority when both are present), panel JS re-checked with `node
|
||||
--check`. `manifest.json` bumped to `2026.8.24.2` and `npm run ota` rerun (own rule from section H,
|
||||
enforced by `install.ps1` since the same day) since this touched both the panel and the companion-app.
|
||||
|
||||
### J) Self-update: the integration pulls its own updates from Gitea (built 2026-08-24)
|
||||
|
||||
**Why.** `install.ps1` (section H) stopped being a reliable update path: Windows Smart App Control
|
||||
blocks its execution, and unlike SmartScreen there is no "run anyway" override — once fully
|
||||
enabled, Smart App Control can only be turned off by reinstalling Windows. The owner's actual daily
|
||||
pain wasn't the block itself but the whole cycle it forced: open Gitea, download a zip, unpack it,
|
||||
run the (now-blocked) installer, re-enter the Samba password every time (see section H's Samba-auth
|
||||
fix earlier the same day). Owner chose, from three options laid out (free workarounds first / code-sign
|
||||
the installer / have Home Assistant pull its own updates), the third: **the integration now updates
|
||||
itself.** `install.ps1` remains necessary for the *first* install only — the integration has to
|
||||
already be running before it can update itself — which is the rare case; the daily-update tedium is
|
||||
what this closes.
|
||||
|
||||
**Safety design — stricter than install.ps1, because it runs inside what it replaces.**
|
||||
`install.ps1` is an external process against a foreign HA instance; this feature's code lives
|
||||
*inside* `custom_components/audi_dashboard/` and replaces that very folder while its own service
|
||||
call is still executing. `aktualisierung.py`'s module docstring carries the full reasoning; the
|
||||
shape:
|
||||
1. Download and verification happen entirely in a sibling staging folder
|
||||
(`custom_components/audi_dashboard_update_staging/`) — the live folder is never touched until the
|
||||
download is confirmed complete and plausible.
|
||||
2. The swap is `os.rename`, not delete-then-copy: the live folder is renamed to
|
||||
`audi_dashboard_backup` (kept, not deleted — reversible) before the staging folder is renamed into
|
||||
its place. If the second rename fails, the first is undone automatically rather than leaving a
|
||||
half-replaced folder.
|
||||
3. The downloaded `manifest.json` is checked for `domain == "audi_dashboard"` and a `version` field
|
||||
before either rename runs — matches install.ps1's own "won't touch a folder that isn't
|
||||
provably ours" rule (section G/H).
|
||||
4. **No automatic reload or restart.** A reload triggered from inside the same service call that's
|
||||
still running would risk being interrupted by its own reload. The UI shows a "restart Home
|
||||
Assistant" message instead — the same pattern install.ps1's Restliste already uses.
|
||||
5. Nothing outside `custom_components/` is touched — same invariant as install.ps1.
|
||||
6. The check step (a Gitea network call) runs only on explicit button press, never on the
|
||||
coordinator's normal publish tick — same "no unsolicited background action" stance as the OTA
|
||||
`autoUpdate: false` decision.
|
||||
|
||||
Renaming the very folder the running code was imported from is safe on Linux (this project runs on
|
||||
Home Assistant OS / Docker): Python holds no open file handles on `.py` sources after import
|
||||
completes, and `os.rename` only operates on path strings, not live handles.
|
||||
|
||||
**Token storage.** No prior mechanism existed for secrets in this integration — `config_flow.py`'s
|
||||
`entry.data` was always `{}`, no `OptionsFlow` existed. Added `AudiDashboardOptionsFlow`
|
||||
(Settings → Devices & Services → Audi Dashboard → Configure), one masked field
|
||||
(`CONF_GITEA_TOKEN` in `const.py`), stored in `entry.options` — HA's own encrypted config-entry
|
||||
store, never `configuration.yaml`. A read-only-scoped Gitea token is enough; the form text says so.
|
||||
|
||||
**What was built:**
|
||||
- `custom_components/audi_dashboard/aktualisierung.py` — `version_pruefen()` (one lightweight Gitea
|
||||
Contents-API call for just `manifest.json`, no repo download) and `update_installieren()`
|
||||
(downloads the full repo archive via Gitea's archive endpoint — Gitea has no single-folder
|
||||
download — delegates to the blocking `entpacken_pruefen_tauschen()` via
|
||||
`hass.async_add_executor_job`). The blocking function takes all paths as parameters rather than
|
||||
reading module constants directly, specifically so it's testable against a temp directory without
|
||||
touching the real integration folder.
|
||||
- Two new services (`update_pruefen`, `update_installieren`), registered in `dienste.py` following
|
||||
the existing `SCHEMATA`/`behandler`/`_als_dienst()` pattern; both zero-field, both catch
|
||||
`aktualisierung.AktualisierungsFehler` and turn it into a user-readable message instead of an
|
||||
unhandled exception.
|
||||
- `koordinator.py`: `alles_veroeffentlichen()`'s `E_APP_VERSION` publish was factored out into its
|
||||
own `app_version_veroeffentlichen()` method and extended with `integration_update`. This holds the
|
||||
*last* check/install result (`self.letzte_update_pruefung`) — deliberately **not** recomputed on
|
||||
every publish tick, only ever set by the two new service handlers, so normal operation triggers zero
|
||||
Gitea calls.
|
||||
- Both frontends got a new "Integration-Update" tile (companion-app: `Einstellungen.tsx` +
|
||||
`IntegrationUpdateAngabe` type in `api/types.ts` + `integrationUpdate` in `DatenKontext.tsx`; panel:
|
||||
new tile next to the existing Version tile in `audi-dashboard-app.js`, backed by a new
|
||||
`INTEGRATION_UPDATE` global read from `daten.integration_update`, wired to the existing
|
||||
`data-*`/`serviceRufen()`/`ereignisseVerdrahten()` click-dispatch pattern) — not an extension of the
|
||||
existing OTA "App-Update" tile, which is a different concept (the Capacitor iOS shell going stale)
|
||||
that doesn't apply to a browser-rendered panel.
|
||||
|
||||
**Verified, honestly scoped by what this session's sandbox could actually run:**
|
||||
- The safety-critical pure logic (`entpacken_pruefen_tauschen()`) has 7 unit tests
|
||||
(`tests/aktualisierung/test_aktualisierung.py`, `unittest`, same style as
|
||||
`tests/belegparser/test_shell_beleg_parser.py`) covering: successful swap with the old version
|
||||
preserved in the backup folder, `__pycache__` cleanup, wrong domain rejected, missing `version`
|
||||
field rejected, missing `manifest.json` rejected, corrupt zip rejected, archive missing the
|
||||
expected path rejected — and every rejection case asserts the live folder is byte-for-byte
|
||||
untouched. Run inside the project's existing throwaway `ghcr.io/home-assistant/home-assistant:stable`
|
||||
test container (`audi_ha_test`) via `docker exec`, since no Python interpreter exists in the
|
||||
session sandbox itself.
|
||||
- The complete integration (config flow, both services, koordinator changes, both frontends) was
|
||||
deployed into that same container and restarted from a clean state, twice, ending on
|
||||
`Audi Dashboard 2026.8.24.4 eingerichtet` with zero errors or tracebacks — confirms the
|
||||
`OptionsFlow`/service registration/coordinator wiring is structurally correct against a real,
|
||||
current (2026.8.0) Home Assistant.
|
||||
- companion-app: `tsc --noEmit` clean, all 127 tests pass.
|
||||
- **Not verified**: an actual successful download+swap against the real private Gitea repo, since
|
||||
that needs the owner's own token — entering it is the owner's action via the HA UI, not something
|
||||
to hand to an assistant in chat. The failure paths (missing token, bad token, unreachable Gitea) are
|
||||
covered by the unit tests and code review, not by a live call against Gitea. First real end-to-end
|
||||
use is the owner's to do once a token is configured — worth a first careful try on the test
|
||||
container before the real instance, same caution as every other install.ps1-era change in this
|
||||
project.
|
||||
|
||||
**Docs updated to stop calling install.ps1 "the only way":** `README.md`, `VERSIONIERUNG.md` — both
|
||||
now say install.ps1 is the only way *to install*; the self-update is now the preferred way *to
|
||||
update* an already-running instance.
|
||||
|
||||
---
|
||||
|
||||
## Working conventions (observed — keep them)
|
||||
|
||||
Reference in New Issue
Block a user