Selbst-Update-Sicherung, Service-Prognose ohne Servicebuch und Sicherheit neu gestaltet

- aktualisierung.py: Backup-Ordner verliert sein manifest.json beim Sichern,
  sonst laedt HA ihn nach einem Neustart als zweite audi_dashboard-Integration
  (Ursache des raetselhaften "gleicher Pfad"-Umbenennungs-Fehlers).
- service.ts/audi-dashboard-app.js: Uebersicht faellt jetzt wie Service schon
  auf die Fahrzeugmeldung zurueck, wenn kein Servicebucheintrag existiert;
  eigenes (kuerzeres) Oelwechsel-Intervall wird jetzt auch ohne Servicebuch-
  eintrag naeherungsweise aus der Herstellermeldung hochgerechnet, statt
  weiterhin unveraendert die Herstellervorgabe zu zeigen.
- Sicherheit neu gestaltet: vier Sammelzeilen (Fahrzeug verriegelt / Tueren
  und Klappen geschlossen / Fenster und Dach geschlossen / Kein Licht) statt
  einer flachen Liste, mit neuen Tuerschloss-, Dach- und Standlicht-Sensor-
  rollen, Kreis-Haekchen-Symbolen und einer Detailseite je Tuer/Motorhaube/
  Kofferraum in beiden Frontends.

Details, Verifikation und Sensor-Rollen in AGENTS.md (Abschnitte J/M/N).

Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
2026-08-24 15:29:37 +02:00
co-authored by Claude Sonnet 5
parent 146f809e9b
commit 5a048488ea
23 changed files with 866 additions and 97 deletions
+175
View File
@@ -2309,6 +2309,38 @@ error, not as a permanent reminder cluttering the tile before the first check
idle branch; the same information already surfaces via `integration_update.fehler` when it's
actually true. companion-app never had this line in the first place, so no parity fix needed there.
**A real bug found the same day, from a genuinely confusing error report.** After a successful
install-then-restart cycle, a later "Update installieren" attempt failed with:
`Konnte den bestehenden Ordner nicht sichern: [Errno 2] No such file or directory:
'/config/custom_components/audi_dashboard_backup' -> '/config/custom_components/audi_dashboard_backup'`
— note both sides of the `os.rename` are the *same path*, which shouldn't be possible given
`INTEGRATIONSORDNER`/`_BACKUP_ORDNER` are built from clearly different string literals. Root cause,
found via `docker logs`: the log line's logger name was `custom_components.audi_dashboard_backup.dienste`,
not `custom_components.audi_dashboard.dienste`. Home Assistant scans **every** folder directly under
`custom_components/` for a `manifest.json` and loads whatever `domain` it declares — by folder
content, not folder name. The `audi_dashboard_backup` folder left behind by a successful swap is a
byte-for-byte copy of the integration, manifest included, so across a restart HA loaded it as a
*second* `audi_dashboard` integration. Python then imported its `dienste.py` as its own module
(`custom_components.audi_dashboard_backup.dienste`), whose `__file__`-derived `INTEGRATIONSORDNER`
*is* the backup folder — making `_BACKUP_ORDNER` collapse onto the same path for that rogue copy.
This duplicate-domain state also explained two other symptoms reported the same session: the panel
intermittently doing nothing visible on "Auf Update prüfen"/"Update installieren" (whichever
integration's service registration won the race), and the frontend getting stuck on the bootstrap
"Lädt …" screen after a restart (confirmed live: with the stray `audi_dashboard_backup` folder still
present, `audi_dashboard` appeared **twice** in the loader's warnings and the panel never finished
loading; deleting the stray folder and restarting fixed both immediately).
**Fix**, in `entpacken_pruefen_tauschen()`: right after the backup rename succeeds, its
`manifest.json` is itself renamed to `manifest.json.bak` — structurally invalid as an integration, so
HA's scanner skips it. On the rollback path (second rename fails), the `.bak` is restored to
`manifest.json` *before* the backup folder is renamed back into place, so a restored live folder is
never left without a valid manifest. Covered by two new unit tests (manifest absent from the backup
folder after a successful swap; manifest correctly restored after a forced rollback via a mocked
`os.rename`) — `tests/aktualisierung/test_aktualisierung.py` now has 8 tests, all passing. Verified
live: deployed the fix, deleted the stray backup folder from the test container, restarted — single
`audi_dashboard` loader warning, clean `eingerichtet` line, frontend loads normally. Manifest bumped
to `2026.8.24.7` (see section M below, same version bump covers both fixes).
### K) vCard import for the workshop contact (built 2026-08-24)
Service → Autohaus (gear icon) now has a "vCard importieren" button next to the existing form
@@ -2404,6 +2436,149 @@ the pre-restart file swap). companion-app: `tsc --noEmit` clean, full suite stil
bumped to `2026.8.24.6` and `npm run ota` rerun per the standing rule in `VERSIONIERUNG.md` — both
frontends changed (new spinner/restart-button behavior is real, visible UI, not internal-only).
### M) Übersicht/Service mismatch on a blank install: two real bugs (2026-08-24, same day)
The owner tested on a **blank installation** (no Servicebuch entries yet) and found two bugs:
1. **Service showed the vehicle's own km/date; Übersicht showed something else (or nothing).** Root
cause was worse on the panel than in companion-app: `termine()` (panel) built its candidate list
purely from `letzterEintrag()` (a real Servicebuch entry) — with none, `km`/`datum`/`ziel` all
stayed `null`, so `vHome()` (Übersicht) rendered a broken/empty service block, while `vService()`
already had its own separate `meldungsPrognose()` fallback and displayed correctly. companion-app's
`naechsterService()` did already call `meldungsPrognose()` as a fallback, but paired the *raw*
signed `fahrzeug.oelwechselFaelligKm` with a *computed* projected date, which could read
inconsistently with `Service.tsx`'s own primary line. Fixed by adding the same meldung-based
fallback to `termine()` (new block right after `oelTermin`/`inspTermin` are built: `if
(termin.basis) return;` then reads `oelMeldungsPrognose()`/`meldungsPrognose()`), and by having
`meldungsPrognose()` always return `restKm` (`Math.abs(faelligKm)`) so both codebases use one
consistent value instead of recomputing it differently at each call site. Verified live: emptied
the test container's `service.buch` array (backed up first, restored after), confirmed Service and
Übersicht showed byte-identical numbers (8.687 km / 28.03.2027) where Übersicht previously would
have shown nothing usable at all.
2. **The Ölwechsel forecast ignored the owner's own (shorter) interval from "Einrichten" until a
Servicebuch entry existed** — it silently kept showing the manufacturer's own countdown instead,
with no restKm shown either. Root cause: `meldungsPrognose()`/`fahrzeugMeldungRoh()` only ever
relay the vehicle sensor's own manufacturer-interval countdown; nothing about them reads
`oel.modus`/`oel.km`/`oel.monate`. Without a Servicebuch entry there's no known "last real oil
change" to anchor a custom-interval projection from — the owner chose (asked directly, see
conversation) to estimate one anyway: back-calculate an assumed last-service point as
`(manufacturer's own due point) (manufacturer's own interval length)`, then project the owner's
shorter interval from that same assumed point. An approximation, not exact — it can never be as
good as a real Servicebuch entry — but it's the only way to show a number that respects the
owner's chosen interval before the first entry exists, instead of silently ignoring the choice.
Implemented once in `service.ts`
(`meldungsPrognoseEigenesIntervall()` + `oelMeldungsPrognose()`, the latter dispatching to the
existing `meldungsPrognose()` unchanged when `modus === "hersteller"`) and mirrored 1:1 in the
panel (same two functions, same math, `Date`-based month arithmetic instead of `service.ts`'s
ISO-string `monatePlus()` since the panel's own `monatePlus()` only accepts `TT.MM.JJJJ` strings
and the vehicle meldung's timestamp is ISO). Both `Service.tsx`/`vService()`'s footnote for this
fallback case now show `restKm` alongside the date when the owner's interval is shorter than the
manufacturer's, exactly like the existing Servicebuch-based prognose already does. Inspektion is
unaffected — it has no owner-selectable interval, always the manufacturer's own.
Only Ölwechsel has a selectable interval, so this only applies there. If `oel.herstellerKm`/
`herstellerMonate` are themselves unknown, both functions degrade to the plain (unchanged)
`meldungsPrognose()` rather than guessing further.
Verified: companion-app — 4 new tests for `meldungsPrognoseEigenesIntervall()`/`oelMeldungsPrognose()`
(restKm/date math, the "already overdue by the owner's interval" clamp-to-zero case, no-fahrten
fallback, graceful degradation without hersteller values), full suite green at 142/142, `tsc --noEmit`
clean. Panel: `node --check` clean; live-verified in the test container using the exact same
blank-install simulation as bug 1 above — with `service.buch` emptied and the container's real
`oel` settings (`modus: "eigen"`, `10.000 km`/`12 Monate`, manufacturer `30.000 km`/`24 Monate`), the
footnote showed **8.700 km, voraussichtlich am 23.07.2027** — the derived number from
`28.700 30.000 + 10.000`, using the vehicle's real raw meldung — closely matching (not identical to,
as expected for an approximation) the real Servicebuch-based value of 8.687 km / 28.03.2027 seen with
the Servicebuch restored. Restored the container's real profile afterward; no live data was left
altered.
Manifest bumped to `2026.8.24.7` (covers this section and section J's manifest-neutering fix above —
one version bump, both landed the same day); superseded by `2026.8.24.8` in section N below the same
day. `npm run ota` rerun once, covering both this section's and section N's frontend changes together
(see section N).
### N) Sicherheit neu gestaltet: verriegelt / Türen & Klappen / Fenster & Dach / Licht (2026-08-24, same day)
The owner asked for a rework of the "Sicherheit" screen (`vSicherheit()`/`Fahrzeugstatus.tsx`), which
until now showed one flat list of every door/window/heckklappe/haube check with no grouping and no
lock, roof, or light status at all. Requested: four named collector rows — **Fahrzeug verriegelt**
(new lock sensors, state "abgeschlossen"), **Türen und Klappen geschlossen** (with an arrow to a
detail view of every door, Motorhaube, and Kofferraum individually), **Fenster und Dach geschlossen**
(Dach = Schiebedach), **Licht ausgeschaltet** (Standlicht, showing the text **"Kein Licht"** for the
good state, not a generic "in Ordnung"). The owner also supplied a concrete mockup: green
checkmark-circles instead of the old plain-color dot, only the second row carrying a chevron.
Went through plan mode given the cross-codebase, backend+frontend scope. Before planning, checked the
real entities available in the test container's `core.entity_registry` (`cupra_eu_data_act` platform)
to ground the sensor-role design in what actually exists rather than guessing: per-door
`binary_sensor.*_door_lock` (`device_class: lock`), `..._tailgate_lock`, `..._hood_lock`, `..._sunroof`
(`device_class: window`), `..._parking_lights` (`device_class: light`). HA's own convention for all of
these is `off` = the good state (locked/closed/no light) — the exact same `state == "off"` → `ok=True`
check this integration already uses for door/window sensors. No new check pattern needed, only new
sensor roles.
**Backend:**
- `einstellungen.py` — five new `Sensorzuordnung` fields: `TUERSCHLOSS_SENSOREN` (4 positions, mirrors
`TUER_SENSOREN`), `HECKKLAPPE_SCHLOSS_SENSOR`, `HAUBE_SCHLOSS_SENSOR` (paired with the existing
open-sensors), `DACH_SENSOR`, `LICHT_SENSOR`. Matching `FELDER` catalog entries under the existing
`"sicherheit"` group — the Setup-Menü is entirely `FELDER`-driven (confirmed: its group headers come
straight from this list), so the new roles appeared there with zero extra frontend work.
- `veroeffentlichung.py`'s `_sicherheitscheck()` — extended with the five new checks, each entry now
additionally tagged `"gruppe"`: `"verriegelt"` (all 6 locks), `"tueren_klappen"` (4 doors + Motorhaube
+ the renamed "Kofferraum" — was "Heckklappe"; the underlying `HECKKLAPPE_SENSOR` field itself is
unchanged, so an existing assignment isn't silently broken by the relabel), `"fenster_dach"` (4
windows + Dach), `"licht"` (Standlicht alone). `gesichert` is untouched — `all()` over the full flat
list is mathematically identical to "AND of all four group results," so no logic change, just more
entries in the same list.
- **Consequence flagged to the owner**: because every entry in the flat list (including brand-new,
unassigned ones) counts toward `gesichert`, any existing installation shows "Zustand nicht
vollständig bekannt" for the overall status until the three new roles are assigned in Setup — same
"unknown beats assumed-good" principle this project has followed throughout, but worth knowing before
it looks like a regression.
**Frontend (both codebases):** a small grouping helper (`sicherheitsGruppe()` in the panel,
`gruppenErgebnis()` + filter in companion-app) turns the flat list into a group's aggregate `ok`
(`null` the moment one entry is unknown, or if the group has zero entries at all — never `true` via an
empty-array vacuous truth). Both `vSicherheit()`/`Fahrzeugstatus.tsx` rewritten to the four-row layout;
each row's own label text changes with its state (e.g. "Kein Licht" / "Licht eingeschaltet" / "Licht
unbekannt") rather than pairing a fixed label with a separate status word — matches the mockup, which
shows no secondary value text at all. New checkmark-circle indicator (`statusKreis()` in the panel,
`StatusKreis` component in companion-app's `bausteine.tsx`) — green check / red cross / amber
question-mark SVG in a filled circle, replacing the old plain dot for this screen only; the rest of the
app keeps `.dot`/`.dm-punkt` unchanged. New route `"tuerenklappen"` in both routing tables
(`ZURUECK`/`navigation.ts`), reachable only from the "Türen und Klappen geschlossen" row; its detail
page is the old flat-list style filtered to just that group (`vTuerenKlappen()`, new
`screens/TuerenKlappen.tsx`).
**Verified:**
- Backend: `python3 -m py_compile` on both changed modules, clean restart in the test container with a
single `audi_dashboard` loader warning (no duplicate-domain regression from section J/M's fix).
- companion-app: `tsc --noEmit` clean, full suite green at 145/145 (added: grouping/unknown-propagation
test, chevron-navigates-only-this-row test, detail-page-shows-only-its-group test; updated the one
pre-existing test and the `beispieldaten.ts`/`profilAdapter.test.ts` fixtures for the now-required
`gruppe` field on `SicherheitsPunkt`).
- Panel: `node --check` clean.
- Live end-to-end in the browser against the test container: before assigning the new sensor roles, all
three new rows correctly showed "unbekannt" (amber) and the overall status read "Zustand nicht
vollständig bekannt" — confirming the flagged consequence above is real and correctly wired, not
theoretical. Opened Setup, scrolled to "Sicherheit": all five new fields were **already
auto-suggested** to the exact real entities found earlier (the existing stichworte/device_class
matching worked without any manual searching) — saved (one pre-existing, unrelated "doppelt
zugeordnet" warning about `Front left door`/`Front left window`, present before this change, dismissed
via the existing "Trotzdem speichern" override). After saving: "Fahrzeug nicht verriegelt" showed red
(the demo lock entities report unlocked — correct, not a bug), "Türen und Klappen geschlossen" and
"Fenster und Dach geschlossen" showed green, **"Kein Licht" showed green** — the exact wording asked
for. Clicked the chevron on row 2: navigated to "Türen und Klappen" showing all four doors, Kofferraum,
and Motorhaube individually, all green "zu" — confirmed the detail page and the "Kofferraum" relabel
both work.
Manifest bumped to `2026.8.24.8`. `npm run ota` rerun (`scripts/ota-paket.ps1 -Bauen`): picked up
`2026.8.24.8` automatically from the manifest, built clean, `bundle.zip` (233.244 Bytes) + sha256 +
`bundle.json` written — covers both this section's and section M's frontend changes, since neither had
triggered a bundle rebuild yet. `Installieren.cmd` picks the bundle up automatically; no separate step
needed for a self-update-based deployment.
---
## Working conventions (observed — keep them)