epics: Epic-/Story-Zerlegung final (5 Epics, 23 Stories) — Party-Mode + Readiness

Phase 4 der AI-Dev-Method abgeschlossen:
- 5 Epics, 23 Stories, alle mit Given/When/Then + FR-/AD-Referenzen.
- Pflicht-Party-Mode (4 Linsen Architekt/Dev/PM/UX): Draft reorganisiert —
  '15-Minuten-Wow'-Pfad (S-2) landet Ende Epic 2; grosse Epic-1-Stories gesplittet.
- Readiness-Check (adversarial): READY-MIT-AUFLAGEN, alle 34 FR / 10 NFR / 24 AD
  abgedeckt; 3 Auflagen eingearbeitet (Plattform-Naht, NFR-1-Assert, Reason-Keys).
- Test-Umgebung provisioniert (.venv Py3.13 + phcc 0.13.225 = HA 2025.3.4).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Nora 2026-07-13 13:12:33 +00:00
parent 03dd8b573c
commit 2686f253d6
3 changed files with 749 additions and 1 deletions

5
.gitignore vendored
View file

@ -27,3 +27,8 @@ secrets.yaml
*.swp
.idea/
.vscode/
.venv/
__pycache__/
*.pyc
.pytest_cache/
.ruff_cache/

View file

@ -5,7 +5,7 @@
> Dieses Ledger überlebt Kontext-Kompaktierung: erledigte Stories, offene Punkte, Gate-Ergebnisse
> (inkl. je Kritiker-Aufruf geschickter Dateien + Modell), Finding-Urteile.
**Stand:** 2026-07-11 · **Phase:** 3 ABGESCHLOSSEN — Architektur-Spine **final** (Gate 2 bestanden) · Nächster Schritt: Phase 4 Epics & Stories
**Stand:** 2026-07-13 · **Phase:** 4 LAUFEND — Epics & Stories (5 Epics, 23 Stories) nach Party-Mode reorganisiert; Readiness-Check läuft · Nächster Schritt: Phase 5 Sprint-Planung + Story-Loop
## Benutzer-Entscheidungen E-1…E-4 (2026-07-11)
- **E-1 Lizenz: Apache-2.0** (bewusst statt MIT-Vorschlag).
@ -127,6 +127,31 @@ der Wetterprognose, optional sprachlich schön formuliert, ausgespielt über Das
9. Naive datetimes ✔ übernommen (verwerfen statt Zeitzone raten, AD-24)
- **Status: Gate 2 BESTANDEN** (nach Einarbeitung; Spine `status: final`).
### Phase 4 — Epics & Stories + Party-Mode (2026-07-13)
- **Artefakt:** `_bmad-output/planning-artifacts/epics.md` — 5 Epics, 23 Stories, alle mit
Given/When/Then + FR-/AD-Referenzen. Reihenfolge vorwärts-abhängigkeitsfrei; »15-Min-Wow«
(S-2/JTBD-3) landet Ende Epic 2.
- **Party-Mode (Pflicht, 4 Linsen parallel: Architekt/Dev/PM/UX):** Draft (5 Epics/19 Stories) →
reorganisiert (5 Epics/23 Stories). Alle Findings selbst geprüft, sämtlich berechtigt:
- **PM [KRITISCH]:** Golden-Path über 3 Epics verstreut → Epic 2 = »15-Minuten-Wow«
(Kleiderschrank+Karte+Service+Push zusammengezogen).
- **Architekt [HOCH]:** Story 1.6/1.7 zu groß → gesplittet (1.6 ha_entity+Provider-Port /
1.7 Coordinator / 1.8 Config-Flow+Lebenszyklus / 1.9 Sensor+Stale); `changed`/Store-Owner →
Coordinator (1.7); AD-23-Options-Schema einmalig in 1.1/const.py; CI-grün von Mirror entkoppelt.
- **Dev [HOCH]:** RawForecast/NormForecast ins Modell (1.1); WeatherProvider-Protocol-AC (1.6);
`__init__`-Unload-Lebenszyklus (1.8); NFR-7-Timing-Assert (1.7).
- **UX [KRITISCH]:** `stale`-Feld nutzersichtbar (Payload 1.5 + Sensor-Owner 1.9 + Karten-Render 2.3);
Erklärbarkeit im Volltext (1.5); Doku-Links in Config-Flow-Fehlern (1.8); Ton-Indikator auf Karte (2.3).
- LLM-Phraser (rein) von LLM-Client (Adapter) getrennt (4.2/4.3).
- **Readiness-Check (adversarial):** **READY-MIT-AUFLAGEN** → alle 34 FRs / 10 NFRs / 24 ADs in
≥ 1 Story-AC; alle 10 Party-Mode-Fixes bestätigt. 3 Auflagen eingearbeitet: (1) 1.8/1.9-
Plattform-Naht (1.8 testet mit `PLATFORMS=[]`, 1.9 schaltet Forwarding scharf → keine
Forward-Dependency); (2) NFR-1 expliziter Netz-Abstinenz-Assert im 5.3-E2E; (3) 1.4-Matcher
emittiert sprachneutrale Grund-Keys, 1.5 lokalisiert (AD-13). **epics.md `status: final`.**
- **Test-Umgebung provisioniert:** `.venv` (Python 3.13.11) + `pytest-homeassistant-custom-component
==0.13.225` (HA **2025.3.4** = Min-Ziel) + pytest 8.3.4. aiohttp==3.11.13 ist yanked → expliziter
Pin nötig (für CI-Requirements merken). Bereit für den Story-Loop.
### Dokumentierte PRD-/Addendum-Präzisierungen (Architekturphase, FR-4.1-konform)
1. FR-7.1 »LLM-Text in State/Karte« → State trägt max 255 Zeichen ⇒ `llm_text` nur in
Event + Service-Response; Karte v1 = Kurztext + Strukturdaten (AD-20).

View file

@ -0,0 +1,718 @@
---
stepsCompleted: [step-01-validate-prerequisites, step-02-design-epics, step-03-create-stories, party-mode-4-lenses, step-04-final-validation]
inputDocuments:
- _bmad-output/planning-artifacts/prd.md
- _bmad-output/planning-artifacts/prd-addendum.md
- _bmad-output/planning-artifacts/architecture/architecture-what-to-wear-2026-07-11/ARCHITECTURE-SPINE.md
status: final
---
# What to Wear (WTW) — Epic Breakdown
## Overview
Dieses Dokument zerlegt PRD (Rev. 3, final) und Architektur-Spine (final, 24 ADs) in
implementierbare Epics und Stories. **Ordnungsprinzip:** Nutzerwert je Epic; innerhalb eines Epics
strikt sequenzielle, einzeln abschließbare Stories **ohne Vorwärts-Abhängigkeit**, jede mit
grüner Suite abschließbar (DEV-METHOD Story-Loop). Die Architektur (Pipes-and-Filters-Kern
`logic/` hass-frei, Ports-and-Adapters-Schale) erlaubt es, den deterministischen Kern rein und
testbar Story für Story aufzubauen, bevor die HA-Adapter ihn sichtbar machen. Struktur nach
Party-Mode-Runde (4 Linsen: Architekt/Dev/PM/UX) überarbeitet — der »15-Minuten-Wow«-Pfad
(S-2/JTBD-3) landet vollständig am Ende von Epic 2.
**Konventionen für alle UI-Stories:** Jede Story, die einen Flow/Entity/Service einführt,
liefert ihre en/de-Übersetzungsschlüssel **inkrementell** mit (kein `strings.json`, AD-13);
Story 5.1 validiert nur Vollständigkeit + hassfest. Das normative Options-/Data-Schema (AD-23)
inkl. Defaults und `TO_REDACT` entsteht **einmalig** in Story 1.1 (`const.py`); spätere Stories
verdrahten nur UI dagegen, bauen es nie neu.
## Requirements Inventory
### Functional Requirements
- **FR-1.1** HACS-Custom-Integration installierbar (Domain `what_to_wear`); Distribution braucht öffentliches GitHub-Repo (E-3).
- **FR-1.2** Config-Flow: `weather.*`-Entität per Selector + sofortiger Test-Abruf; 4 unterscheidbare, übersetzte Fehlerfälle (keine Entität / keine Prognose / reicht nicht bis Zieldatum / temporär).
- **FR-1.3** Config-Flow: Kälteempfinden-Offset (2…+2, Default 0, Richtung im Label); Sprachbindung zweigeteilt (dynamisch je Berechnung / registriert beim Setup); Entity-ID sprachneutral fix.
- **FR-1.4** Options-Flow: Schwellwerte + Umschaltzeitpunkt (jede Schwelle einzeln, validiert, monotone Bandgrenzen), LLM-Ton (Key+Provider, maskiert), Wetter-Entität wechseln (mit Test-Abruf).
- **FR-1.5** Config-Flow-Option »Beispiel-Kleiderschrank anlegen« (12 Stücke aus Addendum §8, sofort editier-/löschbar).
- **FR-1.6** Genau ein Config-Entry (`single_config_entry`); Mehrfach-Instanzen = v1.1.
- **FR-2.1** Stücke als Sub-Entries anlegen/bearbeiten/löschen (native HA-UI). 3 Pflichtfelder (Name, Kategorie, Wärmegrad 15) + optionale Felder (wasserdicht, winddicht, Sonnenschutz, Formalität, Temperaturbereich, aktiv).
- **FR-2.2** Stücke mit `aktiv = nein` werden nie empfohlen.
- **FR-2.3** Effektiv leerer Schrank verhindert die Empfehlung nicht (nennt Requirements als Kategorien + Ursache).
- **FR-2.4** Doku warnt: Entfernen der Integration löscht das Inventar; HA-Backups sichern es mit.
- **FR-3.1** Tägliche und (falls verfügbar) stündliche Prognosen der Entität über HA-Forecast-Mechanismus; Herkunft/Vorrang/Mindestabdeckung nach Addendum §5.
- **FR-3.2** Zieldatum: vor Umschaltzeitpunkt heute, danach morgen (lokales Kalenderdatum, DST-fest); Zieldatum + Erstellzeitpunkt als Attribute.
- **FR-3.3** Fehlende Prognosefelder als »fehlend« (nie 0); außerhalb Plausibilität = fehlend.
- **FR-3.4** Keine Prognose fürs Zieldatum → erklärender Fehlzustand (kein Absturz, keine stumme Null).
- **FR-4.1** Deterministische Requirements aus der Normprognose inkl. Basis-Outfit; Regeltabelle (Addendum §3) verbindlicher Default; Schwellwerte konfigurierbar.
- **FR-4.2** Offset verschiebt ausschließlich die °C-Schwellen um +2 °C je Stufe (Richtung: +1 = früher wärmer); Regen/Wind/UV unberührt.
- **FR-4.3** Temperatur-Regeln auf gefühlte Temperatur im Morgenfenster; Degradations-Tabelle (Addendum §3a) verbindlich; Datenlage-Hinweis nur bei betroffenem muss/soll.
- **FR-4.4** Tagesmaximum ≥ 2 Wärmebänder über Morgenfenster → Tagesgang-Hinweis.
- **FR-5.1** Matcher baut vollständiges Outfit (Basis + Schichten bis Wärmebedarf + Zusatzstücke); Außenschicht-Attribute auf äußerster Schicht; Regeln nach Addendum §4; deterministisch stabil.
- **FR-5.2** Unerfüllbare muss/soll-Requirements → benannte Lücken; Mapping Requirement→Kategorie (Addendum §4a).
- **FR-5.3** Jedes empfohlene Stück trägt eine menschenlesbare Begründung.
- **FR-6.1** LLM-Ton default aus; aktivierbar nur per eigenem Key (OpenAI/Anthropic).
- **FR-6.2** LLM erhält nur die strukturierte Empfehlung + Zielsprache; nie Fotos/Koordinaten/Entity-IDs; nur umformulieren (Prompt-Härtung + Ausgabe-Validierung).
- **FR-6.3** LLM-Fehler/Timeout/kein Key → deterministischer Regeltext; Empfehlung fällt nie aus.
- **FR-6.4** Key nie in Logs/Attributen/Events/Diagnostics; im HA-Config-Storage; Doku transparent (at-rest/Backups).
- **FR-6.5** Beim Aktivieren zeigt der Options-Flow die übertragenen Daten (Drittland möglich) + Provider-Bedingungen; README »Datenschutz« de+en.
- **FR-6.6** Stücknamen als Untrusted Data; LLM-Ausgabe gegen harte Kriterien validiert (Fließtext, ≤700, jeder Stückname, jede Lücke); Verstoß → Fallback; Injection-Testkatalog.
- **FR-7.1** Sensor `sensor.what_to_wear` (State ≤255; Attribute: Stücke/Requirements/Lücken/Kennwerte/Quelle/Abrufzeit/Zieldatum/Erstellzeit/Datenlage); Größengrenzen 16 KB / 30 Stücke; LLM-Text nie in Attributen.
- **FR-7.2** Service `what_to_wear.recommend` erzwingt Neuberechnung, liefert Service-Response + feuert Event; Blueprints nutzen die Response.
- **FR-7.3** Read-only Lovelace-Karte im Integrationsordner, Storage-Mode automatisch registriert, YAML-Mode dokumentiert; zeigt Empfehlung/Zieldatum/Stücke/Lücken.
- **FR-7.4** Zwei Blueprints (Push: notify-Ziel + Uhrzeit + »nur bei Änderung/Lücke/Warnlage«; TTS: media_player + Auslöser); beide rufen `recommend` vorher auf.
- **FR-7.5** Aktualisierung stündlich + bei Neustart + exakt am Umschaltzeitpunkt; letzte Berechnung > 6 h → als veraltet gekennzeichnet (Sensor **und Karte**).
- **FR-8.1** UI + Empfehlungstexte in de + en; Auswahl folgt HA-Sprache.
### NonFunctional Requirements
- **NFR-1** Keine externen Netzaufrufe außer optional aktiviertem LLM; keine Telemetrie.
- **NFR-2** Alle IO-Pfade async; Event-Loop nie blockiert.
- **NFR-3** Unit-Tests (Normalisierung/Regeln/Matcher mit Fixtures) + Integrationstests (phcc) + Injection-Katalog + E2E vor Release.
- **NFR-4** Min-HA ≥ 2025.3; gegen reale APIs verifiziert; in CI gegen die Minimalversion getestet.
- **NFR-5** HACS/Brands-Konformität (Repo, hacs.json, manifest, brands-PR, hassfest + HACS-Action).
- **NFR-6** API-Keys nur im HA-Config-Storage; nie in Logs/State/Attributen/Events/Diagnostics.
- **NFR-7** Empfehlung ohne LLM < 5 s; mit LLM < 30 s, sonst Fallback.
- **NFR-8** Jede Empfehlung aus den Sensor-Attributen vollständig nachvollziehbar.
- **NFR-9** LICENSE (Apache-2.0) + NOTICE; Dependencies kompatibel; gebündelte JS behalten Lizenzhinweise.
- **NFR-10** Config-/Sub-Entry-Schema ab v1 versioniert; Migrationspfad ohne Datenverlust.
### Additional Requirements
Aus dem Architektur-Spine (kein Starter-Template; greenfield; Pipes-and-Filters-Kern in
Ports-and-Adapters-Schale). Verbindliche Umsetzungs-Constraints (AD = Architektur-Entscheidung):
- **AD-1/AD-24** `logic/*` importiert kein `homeassistant.*` (auch kein `dt_util`); Zeit-Parsing im Adapter (`weather/ha_entity.py`), Kern nutzt `zoneinfo`. Durchsetzung: Import-Scan-Test.
- **AD-2/AD-7** Fehlermodell `status ∈ {ok, fehler_prognose}` mit exaktem Prädikat; Coordinator ist einziger Mutator (asyncio.Lock); Setup wartet nicht aufs Wetter (Sensor immer da, nie `data is None`); Stale-Besitz beim Sensor.
- **AD-3** Zieldatum halboffen (morgen ⇔ Uhrzeit ≥ Umschaltzeit); `async_track_time_change`; DST-/TZ-Listener.
- **AD-4** SI-Normmodell; Einheiten aus Quell-Entität lesen; Plausibilität nach Konvertierung; `NormField(value|None, source, note)`.
- **AD-6** Keine SDK-Dependencies (`requirements: []`); LLM raw-HTTP mit URL-Konstanten, `allow_redirects=False`, Timeout 18 s, Body-Cap 64 KB, 1 Versuch.
- **AD-8** `entry.data={weather_entity_id}`, `entry.options`=Schema AD-23, Stücke=Subentries; `VERSION=1`; Robustheits-Contract beim Lesen.
- **AD-9** ItemSubentryFlow mit `async_step_user` + `async_step_reconfigure` (Kompat-Accessor 2025.3/2025.4); Beispiel-Set nur im initialen user-Step.
- **AD-10** 2025.3-Kompat-Leitplanken (kein OptionsFlowWithReload/sync register_static_path/dict-lovelace-Zugriff); CI-Matrix min+latest.
- **AD-11** Service `recommend` OPTIONAL-Response, garantiert frisch (Coordinator-Lock + `await async_refresh`), Event immer.
- **AD-12** Karten-Registrierung (StaticPathConfig + Lovelace-Ressource idempotent, YAML-Mode-Erkennung, Deregistrierung); Karte XSS-sicher (`textContent`), Platzhalter bei fehlender Entität.
- **AD-13** Kein `strings.json`; `translations/{en,de}.json` (en vollständig, hassfest-valid); dynamische Texte in `logic/texts.py`; Karte rendert nur gelieferte Strings.
- **AD-14/AD-19/AD-20** Frozen `Recommendation`; eine `to_payload()` mit englischer Key-Liste + 4 Projektionen (state/attributes/event/response); Größen-Enforcement + Shedding-Kaskade nur in Projektionen; Textmodell (short/full/llm).
- **AD-15** Stabile Entity-ID (`unique_id` + `self.entity_id` vor `async_add_entities`); manifest-Pflichtfelder.
- **AD-16** LLM-Phraser: JSON-escaped Untrusted-Block, Validierung (a)(e); `tone`+`data_notes`-Sichtbarkeit; dokumentiertes Rest-Risiko.
- **AD-17** Forecast-Konsum (`get_forecasts` blocking+return_response), Feature-Check, Fehler-Taxonomie, Morgenfenster-Prädikat, hourly-only-Ableitung, Condition-Mapping.
- **AD-18** Test-Architektur: met.no-/°F-/DST-/Grenzwert-/Injection-Fixtures; Start-ohne-Entität-Test; E2E-Release-Gate.
- **AD-21** Requirement-Registry (Enum, einzige Key-Quelle); rules liefert kanonisch gemergte Map (max-Priorität, warmth-Merge).
- **AD-22** Item-Schema-Registry (englische Keys/Kategorie-Enum; temp_range-Semantik; Überlappung inklusiv; Bandzuordnung ungerundet).
- **AD-23** Normatives Options-Schema (flach, `const.py`); Offset genau einmal in `rules.py`; `TO_REDACT`.
- **Distribution:** GitHub-Repo als HACS-Vertriebskanal (E-3-Mirror); brands-PR; CI-Sicherheit (`permissions: contents: read`, Actions möglichst SHA-gepinnt).
### UX Design Requirements
Keine separate UX-Design-Spezifikation (bmad-ux) vorhanden. Die UI ist bewusst minimal und
vollständig aus PRD/Architektur ableitbar: native HA-Config-/Options-/Subentry-Flows (keine
Custom-UI), eine read-only Lovelace-Karte, zwei Blueprints. Karten-Visuals sind bewusst
Story-Ebene (Deferred im Spine); der Daten-/Anzeigepfad ist über AD-12/13/19/20 fixiert.
### FR Coverage Map
- FR-1.1 → Story 1.1 (Skeleton/Manifest) + Story 5.2 (HACS-Konformität)
- FR-1.2 → Story 1.8 (Config-Flow Wetter-Entität + Test-Abruf)
- FR-1.3 → Story 1.8 (Offset im Config-Flow) + Story 5.1 (Sprachbindung/Übersetzungen)
- FR-1.4 → Story 3.1 (Options-Flow) + Story 4.1 (LLM-Teil)
- FR-1.5 → Story 2.2 (Beispiel-Kleiderschrank)
- FR-1.6 → Story 1.1 (single_config_entry im Manifest) + Story 1.8 (Flow)
- FR-2.1, FR-2.2 → Story 2.1 (Subentry-CRUD, aktiv-Filter im Matcher 1.4)
- FR-2.3 → Story 1.4 (leerer Schrank im Matcher) + Story 1.9 (Sensor-Anzeige)
- FR-2.4 → Story 3.2 (Verhalten) + Story 5.3 (README-Löschwarnung)
- FR-3.1, FR-3.3 → Story 1.2 (Normalizer)
- FR-3.2 → Story 1.7 (Zieldatum/Coordinator)
- FR-3.4 → Story 1.7 (Fehlzustand Coordinator) + Story 1.9 (Sensor-Anzeige)
- FR-4.14.4 → Story 1.3 (Regel-Engine); Konfigurierbarkeit: Schema 1.1 + Wiring 3.1
- FR-5.15.3 → Story 1.4 (Matcher) + Story 1.5 (Gründe im Text)
- FR-6.16.6 → Epic 4 (LLM-Ton)
- FR-7.1 → Story 1.5 (Payload) + Story 1.9 (Sensor)
- FR-7.2 → Story 2.4 (Service/Event)
- FR-7.3 → Story 2.3 (Karte)
- FR-7.4 → Story 2.5 (Blueprints)
- FR-7.5 → Story 1.7 (Recompute-Trigger) + Story 1.9 (Sensor-Stale) + Story 2.3 (Karten-Stale)
- FR-8.1 → Story 5.1 (i18n)
- NFR-1/2 → Story 1.1 (`requirements: []`/Guard) + Story 5.3 (C-2-Assert), durchgängig
- NFR-3 → Story 1.1 (Test-Fundament) + je Story + Story 5.3 (E2E)
- NFR-4 → Story 5.2 (CI-Matrix min+latest)
- NFR-5/9 → Story 5.2 (HACS/Brands/Lizenz)
- NFR-6 → Story 4.4 (Diagnostics) + Story 4.1 (Key-Schutz)
- NFR-7 → Story 1.7 (Timing-Assert ohne LLM) + Story 4.3 (LLM-Budget) + Story 5.3 (E2E)
- NFR-8 → Story 1.5 (Payload/Shedding schützt Keys)
- NFR-10 → Story 3.2 (Migration/Versionierung)
## Epic List
### Epic 1: Deterministische Empfehlung sichtbar (Wetter → Outfit → Sensor)
Nach diesem Epic installiert ein Nutzer WTW, wählt seine Wetter-Entität und sieht auf
`sensor.what_to_wear` eine echte, erklärbare Outfit-Empfehlung fürs Zieldatum — inklusive
korrektem »leerer Schrank«-Verhalten, wenn noch keine Stücke gepflegt sind. Enthält den gesamten
reinen Kern (`logic/`), Wetter-Adapter, Coordinator, Config-Flow und Sensor.
**FRs:** FR-1.1, FR-1.2, FR-1.3 (Offset), FR-1.6, FR-2.3, FR-3.13.4, FR-4.14.4, FR-5.15.3, FR-7.1, FR-7.5.
### Epic 2: Der 15-Minuten-Wow (Kleiderschrank → Karte → Push)
Der vollständige »Wow«-Pfad (S-2/JTBD-3): der Nutzer verwaltet echte Stücke, legt beim Setup ein
Beispiel-Set an, sieht das Outfit auf einer Dashboard-Karte und bekommt es aufs Handy. Enthält
Subentry-CRUD, Beispiel-Set, Lovelace-Karte, Service+Event, Push- und TTS-Blueprints.
**FRs:** FR-1.5, FR-2.1, FR-2.2, FR-7.2, FR-7.3, FR-7.4.
### Epic 3: Anpassung & Robustheit
Volle Konfigurierbarkeit über den Options-Flow (Schwellen, Umschaltzeit, Wetter-Entität-Wechsel)
und ein verlustfreier Migrationspfad. Baut den Options-Flow (Scaffold), den spätere Stories
erweitern.
**FRs:** FR-1.4 (Schwellen/Umschaltzeit/Entität), FR-2.4 (Datenverlust-Verhalten), NFR-10.
### Epic 4: Optionaler LLM-Ton (Bring-your-own-Key)
Wer will, aktiviert mit eigenem Key einen sprachlich schöneren Ton — opt-in, transparent, sicher
(Prompt-Härtung, Ausgabe-Validierung, Key-Schutz, Diagnostics-Redaction), mit garantiertem
Regeltext-Fallback.
**FRs:** FR-6.16.6, NFR-6.
### Epic 5: Internationalisierung & Release-Reife
Vollständige Zweisprachigkeit, CI-Matrix (min+latest HA), HACS-/Brands-/Lizenz-Konformität, README
mit Pflichtabschnitten und E2E-Verifikation — HACS-ready.
**FRs:** FR-1.3 (Sprachbindung), FR-2.4 (README), FR-8.1, NFR-3, NFR-4, NFR-5, NFR-9.
---
## Epic 1: Deterministische Empfehlung sichtbar (Wetter → Outfit → Sensor)
Ziel: ein installierbarer, testbarer vertikaler Durchstich — vom reinen Rechenkern bis zum
sichtbaren Sensor. Stories 1.11.5 bauen den hass-freien Kern (rein, mit Fixtures testbar),
1.61.9 machen ihn in HA sichtbar. Keine Story hängt von einer späteren ab.
### Story 1.1: Projekt-Skeleton, Datenmodell, Options-Schema & Schichtungs-Guard
As a Entwickler,
I want ein installierbares Integrations-Gerüst mit den eingefrorenen Kern-Datenmodellen, dem vollständigen Options-Schema und einem Import-Schichtungs-Test,
So that alle folgenden Stories auf einem stabilen, konventionskonformen und drift-freien Datenvertrag aufsetzen.
**Acceptance Criteria:**
**Given** ein leeres `custom_components/what_to_wear/`
**When** das Skeleton angelegt ist
**Then** existieren `manifest.json` (domain `what_to_wear`, `version`, `single_config_entry: true`, `config_flow: true`, `iot_class: calculated`, `integration_type: service`, `requirements: []`, `codeowners`, `documentation`, `issue_tracker`, `dependencies: [http, frontend, lovelace]`), `hacs.json` (`name`, `homeassistant: "2025.3.0"`), `const.py`, `logic/model.py` und `logic/__init__.py`
**Given** `logic/model.py`
**When** es geladen wird
**Then** definiert es die frozen Dataclasses `NormField`, `RawForecast`, `NormForecast`, `Requirement`, `Item`, `Outfit`, `Recommendation` und die Enums `RequirementKey` (AD-21: `base_top, base_bottom, base_shoes, warmth, waterproof_outer, windproof_outer, sturdy_shoes, hat, gloves, scarf, sun_protection, hint_heat, hint_thunderstorm, hint_layering`), `Category` (AD-22: `top, sweater, jacket, bottom, shoes, head, hands, neck, accessory`), `Priority` (`must, should, may`)
**And** `Recommendation` enumeriert vollständig die Felder für `to_payload()` (AD-19): `status, target_date, target_label, created_at, forecast_fetched_at, language, tone, short_text, full_text, llm_text, items, items_truncated, requirements, gaps, metrics, data_notes, source, signature, changed, stale, alert, gaps_count` — wobei `changed`/`stale` reine Platzhalter (Default) sind, die HA-seitige Owner (Coordinator/Sensor) füllen; `signature` wird im Kern berechnet
**Given** das normative Options-/Data-Schema (AD-23)
**When** `const.py` geladen wird
**Then** enthält es einmalig das vollständige, flache, englische Schema mit Typ/Bereich/Default (`warmth_band_limits, heat_threshold, rain_prob_should/_must, rain_amount_should/_must, gust_should, wind_proxy_should, uv_should, cold_sensitivity_offset, switchover_time, llm_enabled, llm_provider, llm_api_key, llm_model`), die Konstante `TO_REDACT = {llm_api_key, weather_entity_id}`, die Timeout-Konstanten und die LLM-URL-Konstanten — spätere Stories verdrahten nur dagegen
**Given** die Konvention AD-1 (Kern importiert kein `homeassistant.*`)
**When** die Test-Suite läuft
**Then** scannt ein Pflicht-Test alle Dateien unter `logic/` und schlägt fehl, sobald ein `homeassistant`- oder `dt_util`-Import auftaucht; `Recommendation`-Mutation wirft `FrozenInstanceError` (AD-14); `pytest` läuft grün
### Story 1.2: Forecast-Normalizer (SI-Normmodell mit Missingness)
As a Nutzer,
I want dass beliebige Wetterprognosen verlässlich in ein einheitliches SI-Modell fürs Zieldatum überführt werden,
So that die Regeln unabhängig von Einheiten und Datenlücken korrekt rechnen.
**Acceptance Criteria:**
**Given** ein `RawForecast` mit TZ-awaren Zeitstempeln, Einheiten-Angaben und dem IANA-TZ-Namen (Adapter-Vertrag AD-24)
**When** `normalize.py` ihn verarbeitet
**Then** liefert es eine `NormForecast` mit den §5-Feldern, je als `NormField(value|None, source, note)`; Herkunft `daily`/`hourly`/`derived`; Einheiten explizit nach SI konvertiert (°F→°C, mph/m/s→km/h, inch→mm); fehlt die Einheiten-Angabe zu einem Wert, gilt das Feld als fehlend (nie geraten, AD-4)
**Given** ein Wert außerhalb der Plausibilitätsgrenzen (§5) **nach** SI-Konvertierung
**When** normalisiert wird
**Then** gilt das Feld als fehlend + Vermerk (fehlend ≠ 0, FR-3.3)
**Given** hourly-Einträge im 3-h-Raster (met.no-Realität)
**When** das Morgenfenster [06:00, 09:00) über die Kern-Funktion `morning_window_coverage()` ausgewertet wird
**Then** gilt es als abgedeckt, sobald ≥ 1 Eintrag im Fenster liegt; Fensterwert = Minimum der Fenster-Einträge (AD-17); fehlt daily ganz, werden Tagesaggregate deterministisch aus den hourly-Einträgen abgeleitet (min/max/Summe/Maximum, `source=derived`)
**Given** Fixtures für met.no-Profil (kein `apparent_temperature`), °F/mph-Einheiten, DST-Tag (23/25 h), Teilabdeckung
**When** die Unit-Tests laufen
**Then** sind alle grün und decken Einheiten, Missingness, Plausibilität, Morgenfenster und DST ab (AD-18)
### Story 1.3: Regel-Engine (Requirements aus der Normprognose)
As a Nutzer,
I want dass aus dem Wetter deterministisch nachvollziehbare Anforderungen abgeleitet werden,
So that jede Empfehlung erklärbar auf konkreten Wetterbedingungen beruht.
**Acceptance Criteria:**
**Given** eine `NormForecast` und die Schwellwerte aus dem `const.py`-Schema (Defaults; per Argument übergeben)
**When** `rules.py` läuft
**Then** liest es die Schwellen ausschließlich aus dem übergebenen Schema-Objekt (keine Hardcodes außer den `const.py`-Defaults) — so wirkt der spätere Options-Flow (Story 3.1) ohne Änderung an `rules.py` (FR-4.1) — und erzeugt immer das Basis-Outfit (`base_top/bottom/shoes`, must) plus die §3-Requirements als **kanonisch gemergte Map** `key → (priority, params)`: gleicher Key ⇒ `max(priority)`, `warmth` ⇒ ein Eintrag mit `N = max(band_N, 4 bei Schnee)`, `hint_*` dedupliziert (AD-21)
**Given** ein Kälteempfinden-Offset ≠ 0
**When** die Bänder bewertet werden
**Then** verschiebt `rules.py` alle °C-Schwellen (inkl. `heat_threshold`) **genau einmal** um `2 · offset` (+1 ⇒ +2 °C, früher wärmer); Regen/Wind/UV unberührt (AD-23, FR-4.2)
**Given** fehlende Felder
**When** die Regeln greifen
**Then** folgt das Verhalten exakt der Degradations-Tabelle §3a; ein Datenlage-Hinweis entsteht nur bei betroffenem muss/soll-Requirement (FR-4.3); Tagesmaximum ≥ 2 Bänder über Morgenfenster → `hint_layering` (FR-4.4)
**Given** Grenzwert-Fixtures (halboffene Bänder auf ungerundeten SI-Werten: 7.9999 °C; Offset +1 verschiebt 7.5-°C-Morgen von Band 4 nach Band 3; Frost+Schnee+Starkregen)
**When** die Unit-Tests laufen
**Then** sind sie grün und die Merge-Semantik liefert genau einen `waterproof_outer (must)` und einen `warmth:5`
### Story 1.4: Matcher (vollständiges Outfit + Lücken)
As a Nutzer,
I want dass aus meinen Stücken ein vollständiges, wetterpassendes Outfit zusammengestellt und fehlende Stücke benannt werden,
So that ich sehe, was ich rauslegen soll — und was mir fehlt.
**Acceptance Criteria:**
**Given** eine gemergte Requirement-Map und eine Liste aktiver `Item`s
**When** `matcher.py` läuft
**Then** baut es ein vollständiges Outfit nach §4 (Basis + Schichten Oberteil→Pullover→Jacke bis Schichtsumme ≥ N, Hosen-/Schuh-Mindestwärme, Außenschicht-Attribute auf äußerster Schicht) und liefert je Stück eine **sprachneutrale Begründung als Grund-Key** (erfülltes Requirement, `RequirementKey`), den Story 1.5 nach de/en lokalisiert (AD-13; kein deutscher Klartext im Matcher) (FR-5.1, FR-5.3)
**Given** ein muss/soll-Requirement ohne passenden Kandidaten (Mapping §4a)
**When** komponiert wird
**Then** entsteht eine benannte Lücke; bei nicht erfüllbarem Wärmebedarf wählt der Matcher die wärmste mögliche Kombination und benennt die Lücke; bei Konflikt Außenschicht vs. Wärmebedarf gewinnt die höhere Priorität (FR-5.2, §4.6)
**Given** ein Stück mit Temperaturbereich
**When** gefiltert wird
**Then** ist der Kandidat zulässig, wenn sein Bereich mit `[morgen_wert, tagesmax]` beidseitig **inklusiv** überlappt; fehlt eine Grenze → Punkt-/Halbintervall; fehlen beide Temperaturwerte → Filter inaktiv (AD-22); `aktiv = nein` wird nie gewählt (FR-2.2)
**Given** ein effektiv leerer Schrank (leer / alle inaktiv / alle ausgefiltert)
**When** komponiert wird
**Then** entsteht keine Ausnahme; das Ergebnis nennt die Requirements als Kategorien + Ursache (FR-2.3)
**Given** das normative Beispiel-Set (§8) + Frost/Regen-Fixture (1 °C, 70 % Regen)
**When** der Abnahmetest läuft
**Then** ergibt sich Thermoshirt + Wollpullover + Regenjacke (außen, wasserdicht) + Jeans + Stiefel + Mütze + Schal; einzige Lücke: Handschuhe (§8)
### Story 1.5: Regeltext, Texte (de/en) & Payload-Projektionen
As a Nutzer,
I want eine kurze, verständliche, erklärende Empfehlung in meiner Sprache und vollständig nachvollziehbare Ausgabedaten,
So that ich die Empfehlung auf einen Blick verstehe und Automationen zuverlässig darauf aufbauen können.
**Acceptance Criteria:**
**Given** eine fertige `Recommendation`
**When** `texts.py` den Text baut
**Then** entsteht ein deterministischer Kurztext (≤ 255 Zeichen) und ein Volltext, je in de/en gemäß Sprache (Fallback en; Matching über primäres Subtag, AD-13); **der Volltext webt die Stück-Gründe ein (FR-5.3) und nennt den Datenlage-Hinweis, wenn ein muss/soll-Requirement betroffen ist (FR-4.3)** — nicht nur als Attribut
**Given** die `Recommendation`
**When** die Projektionen erzeugt werden
**Then** liefert `logic/model.py` **eine** `to_payload()` (englische Key-Liste aus 1.1) und die vier Projektionen `to_state()` (≤255), `to_attributes()` (≤15 KB, Stückliste ≤30 + Restzähler, Shedding-Kaskade, enthält `stale`, **kein** `llm_text`), `to_event()` (≤32 KB, Assert), `to_response()` (vollständig)
**Given** übergroße Eingaben
**When** `to_attributes()` projiziert
**Then** greift die deterministische Shedding-Kaskade (erst Stückliste, dann Kurzgründe, dann Requirement-Freitexte); Status/Zieldatum/Keys/Lücken weichen nie (AD-14, NFR-8)
**Given** zwei Empfehlungen mit gleichem vs. verändertem Outfit
**When** die `signature` (rein, im Kern) berechnet wird
**Then** ist sie stabil bei gleichem Inhalt und ändert sich bei verändertem Outfit/Lücken/alert; `alert` ⇔ mind. ein muss-Requirement jenseits `base_*`; `changed`/`stale` bleiben im Kern auf Default (Owner: Coordinator/Sensor) (AD-19)
### Story 1.6: Wetter-Adapter (`WeatherProvider`-Port + HA-Entität)
As a Nutzer,
I want dass die Integration die Prognose meiner Wetter-Entität robust und einheitensicher abruft,
So that die Empfehlung auf verlässlichen Wetterdaten beruht.
**Acceptance Criteria:**
**Given** `weather/provider.py`
**When** es geladen wird
**Then** definiert es das Protocol `WeatherProvider` mit `async get_raw_forecast() -> RawForecast`; der spätere Coordinator typisiert nur gegen dieses Protocol (INV-6)
**Given** eine gewählte `weather.*`-Entität
**When** `weather/ha_entity.py` (implementiert `WeatherProvider`) die Prognose abruft
**Then** nutzt es `hass.services.async_call("weather","get_forecasts", …, blocking=True, return_response=True)` nach `get_supported_features`-Check (daily und/oder hourly; nur-twice_daily = ungeeignet), parst alle Zeitstempel mit `dt_util.parse_datetime` (verwirft naive/unparsebare → Feld fehlend), liest die Einheiten-Attribute, mappt Conditions ({rainy,pouring,lightning-rainy,hail}→regen; {snowy,snowy-rainy}→schnee; {lightning,lightning-rainy}→gewitter) und liefert einen TZ-awaren `RawForecast` (AD-17/AD-24)
**Given** ein phcc-Integrationstest mit einer Mock-Weather-Entität
**When** der Adapter läuft
**Then** ist die Suite grün (inkl. Entität ohne passenden Forecast-Typ, `unavailable`, Response-Key fehlt), und `normalize.py` (1.2) verarbeitet den gelieferten `RawForecast` zu einer plausiblen `NormForecast`
### Story 1.7: Coordinator (einziger Mutator, Trigger, Delta, Timing)
As a Nutzer,
I want dass die Integration die Empfehlung aktuell hält und nie stumm ausfällt,
So that die Empfehlung ohne mein Zutun stimmt.
**Acceptance Criteria:**
**Given** der `DataUpdateCoordinator` (erzeugt mit `config_entry=entry`)
**When** er aktualisiert
**Then** ist er der einzige, der die `Recommendation` berechnet (asyncio.Lock um `_async_update_data`), mit Zieldatum-Logik (morgen ⇔ Uhrzeit ≥ Umschaltzeit, lokal/DST-fest via `async_track_time_change`), `asyncio.timeout(10)` um den Forecast-Abruf, und den Triggern stündlich / Neustart / Umschaltzeitpunkt / Quell-Entität-verfügbar (AD-3/AD-7)
**Given** ein Forecast-Fehler
**When** `_async_update_data` läuft
**Then** liefert es ohne Vorgänger-Daten `Recommendation(status=fehler_prognose)` (nie `data is None`); mit Vorgänger-Daten → `raise UpdateFailed` (letzte Empfehlung bleibt) (AD-2/AD-7, FR-3.4)
**Given** die zuletzt ausgelieferte Signatur (persistiert via `helpers.storage.Store`, pro Entry)
**When** eine neue Empfehlung entsteht
**Then** setzt der Coordinator `changed` = (signature ≠ persistierte Vor-Signatur) und pflegt die Zeitmarke `last_success_utc` für die Stale-Berechnung (AD-19/AD-2)
**Given** ein phcc-Test mit gemocktem Provider
**When** der Coordinator ohne LLM rechnet
**Then** ist die Suite grün (inkl. Start-mit-fehlender-Entität → `status=fehler_prognose`, nie None) **und** eine Berechnung dauert gemessen < 5 s (NFR-7-Timing-Assert)
### Story 1.8: Config-Flow (Wetter-Entität + Test-Abruf) & Entry-Lebenszyklus
As a Nutzer,
I want WTW über die HA-UI einzurichten,
So that die Einrichtung geführt und fehlerrobust ist.
**Acceptance Criteria:**
**Given** ein frischer Config-Flow
**When** ich eine `weather.*`-Entität per Selector wähle
**Then** verifiziert der Flow sie sofort per Test-Abruf (nutzt `get_supported_features`/`morning_window_coverage` aus 1.6/1.2) und unterscheidet vier übersetzte Fehlerfälle: keine `weather.*`-Entität / keine Prognose / Prognose reicht nicht bis Zieldatum / temporär (`unavailable`/`unknown`/Timeout 10 s); **jede Meldung nennt Ursache und einen Handlungshinweis/Doku-Link** (FR-1.2/J1/C-1)
**Given** der Config-Flow
**When** ich ihn durchlaufe
**Then** kann ich den Kälteempfinden-Offset (2…+2, Default 0, Richtung im Label »Ich friere leicht ↔ Mir ist schnell warm«) setzen; genau ein Config-Entry (`single_config_entry`); der Flow liefert seine en/de-Übersetzungsschlüssel inkrementell mit (FR-1.3/FR-1.6/AD-13)
**Given** ein angelegter Entry
**When** `async_setup_entry` / `async_unload_entry` / `async_remove_entry` laufen
**Then** richtet Setup den Coordinator + die Listener ein und cancelt beim Unload **alle** Listener (Zeit-, Entity-, Options-Listener) leak-frei via `entry.async_on_unload`; ein phcc-Test weist Setup→Unload→Setup ohne Doppel-Registrierung nach — mit `PLATFORMS=[]` (Coordinator/Listener-Lebenszyklus, **ohne** Sensor-Plattform-Forwarding; das schaltet Story 1.9 scharf, keine Vorwärts-Abhängigkeit) (AD-7/AD-10)
**Given** phcc-Integrationstests
**When** sie laufen
**Then** decken sie alle vier Fehlerfälle und den Setup/Unload-Zyklus grün ab (NFR-3)
### Story 1.9: Sensor mit stabiler ID & Stale-Kennzeichnung
As a Nutzer,
I want sofort eine sichtbare Empfehlung auf einem stabil benannten Sensor,
So that sich die Einrichtung sofort sichtbar lohnt.
**Acceptance Criteria:**
**Given** ein Entry mit laufendem Coordinator
**When** die Sensor-Plattform lädt
**Then** existiert `sensor.what_to_wear` (stabile ID: `_attr_unique_id` + `self.entity_id` vor `async_add_entities`, `has_entity_name` + `translation_key`) mit State = `to_state()` und Attributen = `to_attributes()`; `__init__` forwardet ab dieser Story `Platform.SENSOR` (aktiviert die in 1.8 vorbereitete Setup/Unload-Mechanik) (FR-7.1/AD-15)
**Given** die letzte erfolgreiche Berechnung ist > 6 h her (`last_success_utc`)
**When** der Sensor benachrichtigt wird
**Then** berechnet **der Sensor** (Stale-Besitz, AD-2) `stale = true`, schreibt es in die Attribute und plant einen Einmal-Timer auf `last_success + 6 h` für den exakten Kipp-Zeitpunkt (FR-7.5)
**Given** ein Entry ohne gepflegte Stücke
**When** der Sensor rechnet
**Then** zeigt er die »leerer Schrank«-Empfehlung (Kategorien + Ursache), nie einen Absturz (FR-2.3); phcc-Test deckt Attribute + Stale + leerer-Schrank grün ab
---
## Epic 2: Der 15-Minuten-Wow (Kleiderschrank → Karte → Push)
Ziel: der vollständige S-2-Pfad. Baut auf Epic 1 (Matcher/Sensor/Coordinator/Payload). Nach 2.5
kann ein neuer Nutzer in < 15 min installieren, Beispiel-Set anlegen, das Outfit auf der Karte
sehen und den Push aufs Handy einrichten. Jede Story ist eigenständig testbar.
### Story 2.1: Kleidungsstücke anlegen, bearbeiten, löschen (Sub-Entries)
As a Nutzer,
I want meine Kleidungsstücke direkt in der HA-UI zu pflegen,
So that die Empfehlung meinen echten Schrank nutzt.
**Acceptance Criteria:**
**Given** ein bestehender Config-Entry
**When** ich ein Stück anlege
**Then** führt `ItemSubentryFlow.async_step_user` durch die 3 Pflichtfelder (Name 160 Zeichen; Kategorie aus dem `Category`-Enum via Selector; Wärmegrad 15) und die optionalen Felder (wasserdicht, winddicht, Sonnenschutz, Formalität leger/business, Temperaturbereich min ≤ max, aktiv) mit Validierung (FR-2.1/AD-22); Daten im englischen Schema (AD-22); Stück-ID = `subentry_id`
**Given** ein vorhandenes Stück
**When** ich es bearbeite
**Then** öffnet `async_step_reconfigure` (inkl. editierbarem Titel) den Flow mit aktuellen Werten; Zugriff über den Kompat-Accessor (2025.3 `_get_reconfigure_entry` / 2025.4+ `_get_entry`) (AD-9)
**Given** ein Stück angelegt/geändert/gelöscht
**When** der Flow abschließt
**Then** feuert die Änderung den Update-Listener bzw. der Flow stößt selbst `async_request_refresh` an; die nächste Empfehlung nutzt den neuen Stand (AD-7, J4); der Flow liefert seine en/de-Keys mit
**Given** phcc-Integrationstests
**When** sie laufen
**Then** decken sie Anlegen, Reconfigure und Löschen grün ab, inkl. Recompute nach Änderung (J4: neues Stück in < 1 min nächste Empfehlung nutzt es)
### Story 2.2: Beispiel-Kleiderschrank beim Setup
As a neuer Nutzer,
I want beim Einrichten optional ein Beispiel-Set anlegen zu lassen,
So that ich ohne eigene Pflege sofort eine vollständige Empfehlung sehe.
**Acceptance Criteria:**
**Given** der initiale Config-Flow (nur dort, nie im Reconfigure/Options)
**When** ich »Beispiel-Kleiderschrank anlegen« wähle
**Then** hängt der Flow die 12 Stücke aus §8 additiv als `subentries=`-Parameter an die bestehende `async_create_entry` (Story 1.8) an; alle sind sofort editier-/löschbar (FR-1.5/AD-9)
**Given** die Namen der Beispiel-Stücke
**When** sie angelegt werden
**Then** stammen sie aus `logic/texts.py` in der Setup-Sprache (de/en), nicht aus `translations/` (AD-13); Kategorien = `Category`-Enum-Werte
**Given** das angelegte Beispiel-Set + Frost/Regen-Fixture
**When** der Abnahmetest läuft
**Then** ergibt sich ein vollständiges Outfit mit einziger Lücke »Handschuhe« (FR-1.5, §8); der Reconfigure-Pfad bietet die Option **nicht** erneut an (keine Duplikate)
### Story 2.3: Read-only Lovelace-Karte mit Auto-Registrierung
As a Nutzer,
I want eine Dashboard-Karte, die meine Empfehlung dauerhaft und ehrlich zeigt,
So that ich abends und morgens auf einen Blick sehe, was rauszulegen ist — und ob die Daten frisch sind.
**Acceptance Criteria:**
**Given** die Integration lädt (Storage-Mode)
**When** das Setup läuft
**Then** registriert `frontend.py` die statische Datei via `async_register_static_paths([StaticPathConfig("/what_to_wear/what-to-wear-card.js", …)])` und legt/aktualisiert die Lovelace-Ressource idempotent (URL-Prefix-Scan, `async_update_item` auf `?v=<version>`, sonst `async_create_item`) — nur über Attribut-Zugriff `hass.data["lovelace"].resources`; YAML-Mode: nicht schreiben, README-Schritt; `async_remove_entry` deregistriert (AD-12/FR-7.3)
**Given** die Karte rendert Empfehlungsdaten
**When** Stücknamen/Texte/Gründe/Lücken angezeigt werden
**Then** nutzt sie ausschließlich `textContent`/DOM-APIs (nie `innerHTML` mit Daten — XSS-sicher), zeigt Empfehlung/Zieldatum/Stücke/Lücken und rendert nur gelieferte Strings (AD-12/AD-13)
**Given** die Attribute tragen `stale = true` bzw. `tone = rules` mit `data_notes`-Grund
**When** die Karte rendert
**Then** zeigt sie einen sichtbaren »veraltet«-Hinweis (FR-7.5) und einen dezenten Ton-Indikator (LLM aktiv/Regeltext), damit ein dauerhafter LLM-Fallback am Dashboard sichtbar ist; bei fehlender/`unavailable` Entität einen Platzhalter (nie Exception)
**Given** phcc-Test der Registrierung + Review/Lint der JS-Datei
**When** sie laufen
**Then** ist die Registrierung idempotent grün und die JS-Datei enthält nachweislich keinen `innerHTML`-Datenpfad (verankertes Kriterium für das deklarative JS-Artefakt)
### Story 2.4: Service `recommend` mit garantiert frischer Response + Event
As a Automatisierer,
I want einen Service, der eine frische Empfehlung erzwingt und zurückgibt,
So that meine Automationen immer den aktuellen Stand ausspielen.
**Acceptance Criteria:**
**Given** der registrierte Service `what_to_wear.recommend` (`SupportsResponse.OPTIONAL`)
**When** er aufgerufen wird
**Then** ruft der Handler `await coordinator.async_refresh()` (ungedrosselt; Coordinator serialisiert über sein Lock), liest **danach** `coordinator.data` und liefert `to_response()` nur bei `call.return_response`; er feuert **immer** das Event `what_to_wear_recommendation` mit `to_event()` (AD-11/AD-19)
**Given** zwei nahezu gleichzeitige Aufrufe plus ein parallel laufender Zeit-Trigger
**When** sie eintreffen
**Then** stammt jede Response aus einer nach Call-Eingang gestarteten/abgeschlossenen Berechnung (Frische-Garantie; phcc-Test mit Provider-Zähler inkl. parallelem Trigger)
**Given** `services.yaml`
**When** es geladen wird
**Then** definiert es nur Struktur/Selectors; Name/Beschreibung kommen aus `translations` (`services.recommend.*`, inkrementell mitgeliefert)
### Story 2.5: Blueprints Push & TTS
As a Nutzer,
I want fertige Automationsvorlagen für Handy-Push und Sprachansage,
So that ich ohne YAML-Bastelei benachrichtigt werde.
**Acceptance Criteria:**
**Given** die zwei Blueprints (`blueprints/automation/what_to_wear/notify_push.yaml`, `announce_tts.yaml`)
**When** ich sie über den README-Import-Link (`my.home-assistant.io/redirect/blueprint_import/?blueprint_url=…`) importiere
**Then** haben beide `min_version: 2025.3.0`; Push nutzt Device-Selector (Filter `mobile_app`) + `time`-Selector; TTS `media_player`-entity-Selector + Auslöser (FR-7.4)
**Given** eine Blueprint-Automation
**When** sie auslöst
**Then** ruft sie zuerst `what_to_wear.recommend` mit `response_variable` auf und verwendet ausschließlich dessen Rückgabe (`response.llm_text | default(response.full_text)`), nie den Event (AD-11/AD-20)
**Given** die Push-Option »nur senden, wenn geändert oder Lücke/Warnlage«
**When** sie aktiv ist
**Then** entscheidet die Automation anhand der Response-Felder `changed`, `gaps_count > 0` bzw. `alert` (AD-19) — deckt J2 (Push mit Lücke »Handschuhe«) und J3 (nur re-notify bei Änderung) ab
**Given** ein YAML-Lint/Schema-Check der Blueprints
**When** er läuft
**Then** sind beide gültig (`blueprint.domain: automation`, `input:` mit Selectors) — verankertes, nachprüfbares Akzeptanzkriterium (deklaratives Artefakt)
---
## Epic 3: Anpassung & Robustheit
Ziel: Konfigurierbarkeit + verlustfreie Migration. 3.1 erzeugt den Options-Flow (Scaffold), den
Epic 4 (LLM) additiv erweitert. Baut auf Epic 1 (const.py-Schema, rules liest Schwellen, Test-Abruf).
### Story 3.1: Options-Flow (Schwellen, Umschaltzeit, Wetter-Entität)
As a Nutzer,
I want Schwellwerte, Umschaltzeit und Wetter-Entität nachträglich anzupassen,
So that ich WTW an mein Klima und meinen Tagesrhythmus anpassen kann.
**Acceptance Criteria:**
**Given** der Options-Flow (neue `OptionsFlow`-Klasse, Property-Muster ohne `__init__`, AD-10)
**When** ich ihn öffne
**Then** kann ich jede Schwelle einzeln setzen (Wertebereich + Validierung; Bandgrenzen als strikt monotone Liste), den Umschaltzeitpunkt (TimeSelector `HH:MM:SS`), den Offset und den Wetter-Entität-Wechsel (mit Test-Abruf wie FR-1.2, Reuse aus 1.6/1.8) (FR-1.4)
**Given** das Options-Schema
**When** Werte gespeichert werden
**Then** liegen sie flach/englisch nach `const.py`-Schema (AD-23); der Handler schreibt die gewechselte Wetter-Entität nach `entry.data` (AD-8); die Umschaltzeit wird an genau einer Stelle mit `time.fromisoformat` geparst
**Given** eine geänderte Option
**When** der Options-Flow abschließt
**Then** triggert der Update-Listener `async_reload` (Zeit-Listener neu, leak-frei via 1.8-Lebenszyklus); Optionen wirken sofort auf `rules.py` (das die Schwellen bereits aus dem Schema liest, 1.3) (AD-7/AD-10)
**Given** phcc-Tests
**When** sie laufen
**Then** decken sie Options-Roundtrip, monotone Validierung, `HH:MM:SS`-Parsing und Entität-Wechsel grün ab
### Story 3.2: Schema-Versionierung & Migration
As a Nutzer,
I want dass Updates meine gepflegten Stücke und Einstellungen nie verlieren,
So that ich WTW bedenkenlos aktualisieren kann.
**Acceptance Criteria:**
**Given** ein Config-Entry mit `VERSION=1, MINOR_VERSION=1`
**When** die Integration lädt
**Then** existiert `async_migrate_entry`; ein v1→v1-Lauf ist ein No-op ohne Datenverlust (NFR-10/AD-8)
**Given** fehlende, unbekannte oder typfalsche Options-/Subentry-Keys
**When** gelesen wird
**Then** greift der Robustheits-Contract: fehlend ⇒ Default aus `const.py`, unbekannt ⇒ bleibt erhalten, typfalsch ⇒ Default + Warn-Log — nie Setup-Abbruch (AD-8)
**Given** phcc-Tests
**When** sie laufen
**Then** decken sie den No-op-Migrationslauf und den Robustheits-Contract grün ab (FR-2.4-Verhalten »Löschen entfernt Subentries« ist Core-Verhalten; die README-Warnung liefert Story 5.3)
---
## Epic 4: Optionaler LLM-Ton (Bring-your-own-Key)
Ziel: schönerer Ton per opt-in, sicher, mit garantiertem Fallback. Erweitert den Options-Flow aus
3.1 additiv. Sicherheitskritisch → stärkstes Modell bei der Umsetzung.
### Story 4.1: LLM-Optionen & Datenschutz-Disclosure
As a Nutzer,
I want den LLM-Ton mit meinem eigenen Key transparent zu aktivieren,
So that ich die Kontrolle über Kosten und übertragene Daten behalte.
**Acceptance Criteria:**
**Given** der bestehende Options-Flow (3.1)
**When** ich den LLM-Ton aktiviere
**Then** ergänzt eine additive Options-Sektion Provider (openai/anthropic), Key (Passwort-Selector, maskiert) und optional Modell; Default aus (FR-6.1/AD-23)
**Given** der Aktivierungs-Step
**When** er angezeigt wird
**Then** zeigt er vor dem Speichern die Datenübertragungs-Disclosure (Stücknamen, Wetterkennwerte, Zielsprache; Drittland möglich; Link auf Provider-Bedingungen) als Step-Description (FR-6.5)
**Given** ein bereits gespeicherter Key
**When** ich den Options-Flow erneut öffne und nur anderes ändere
**Then** wird der Key nie als Default/suggested_value zurückgespielt; leeres Feld ⇒ Key bleibt; Entfernen nur über »LLM deaktivieren« (AD-5); phcc-Test deckt Aktivieren, Disclosure, »leer lassen« grün ab
### Story 4.2: LLM-Phraser & Injection-Katalog (reiner Kern)
As a Nutzer,
I want einen sprachlich schöneren, aber inhaltlich unveränderten und injektionssicheren Empfehlungstext,
So that die Ansage angenehm klingt, ohne Fakten, Auswahl oder Sicherheit zu gefährden.
**Acceptance Criteria:**
**Given** eine strukturierte Empfehlung
**When** `logic/phraser.py` den Prompt baut
**Then** übergibt es Stücknamen JSON-escaped in einem JSON-Block (Untrusted Data) + Ignorier-Instruktion; nie Fotos/Koordinaten/Entity-IDs (FR-6.2/AD-16)
**Given** eine LLM-Antwort (String, aus dem Adapter übergeben)
**When** `phraser.py` sie validiert
**Then** prüft es (a) reiner Fließtext ohne `<`/`[`/```` ``` ````/`http(s)://`, (b) ≤ 700 Zeichen, (c) jeder empfohlene Stückname wörtlich enthalten, (d) jede Lücke genannt, (e) nach Trim ≥ 20 Zeichen; jede Verletzung ⇒ `None` (→ Adapter/Coordinator nutzt Regeltext) (FR-6.6)
**Given** der Injection-Testkatalog (bösartige Stücknamen: Markup, Links, Prompt-Befehle, 700-Zeichen-Namen)
**When** die Unit-Tests laufen (rein, ohne HA)
**Then** greift in jedem Fall die Ablehnung und kein Markup/Link passiert die Validierung (FR-6.6/NFR-3)
### Story 4.3: LLM-Client (Adapter) & Ton-Fallback
As a Nutzer,
I want dass der LLM-Aufruf sicher und mit hartem Budget erfolgt und bei jedem Fehler nahtlos auf den Regeltext zurückfällt,
So that die Empfehlung nie ausfällt oder Geld/Zeit verschwendet.
**Acceptance Criteria:**
**Given** `llm/client.py`
**When** es den Provider aufruft
**Then** nutzt es `async_get_clientsession(hass)`, URL-Konstanten aus `const.py` (OpenAI `/v1/chat/completions` Bearer; Anthropic `/v1/messages` mit `x-api-key` + `anthropic-version: 2023-06-01` + `max_tokens`), `allow_redirects=False`, Timeout 18 s, Body-Cap 64 KB, genau ein Versuch; jeder Nicht-2xx/Timeout ⇒ `None` (AD-6)
**Given** die Coordinator-Verdrahtung
**When** LLM aktiv ist und `phraser.validate` einen Text akzeptiert
**Then** wird er als `llm_text`/`tone=llm` geführt; bei aus/Fehler/Timeout/ungültig/Validierungsbruch → deterministischer Regeltext, `tone=rules` + kategorialer Grund (`llm_auth|llm_timeout|llm_invalid`) in `data_notes` (FR-6.3/AD-16); NFR-7 (< 30 s) durch das 18-s-Budget gedeckt
**Given** phcc-Tests mit gemockten Provider-Antworten (2xx gültig, 2xx ungültig, 401, Timeout, Übergroß)
**When** sie laufen
**Then** ist die Suite grün und in jedem Negativfall erscheint der Regeltext mit korrektem `tone`/Grund
### Story 4.4: Diagnostics-Redaction (Whitelist)
As a Nutzer,
I want dass Diagnose-Downloads keine Geheimnisse oder Standortdaten enthalten,
So that ich sie gefahrlos an ein GitHub-Issue anhängen kann.
**Acceptance Criteria:**
**Given** `diagnostics.py`
**When** ein Diagnose-Download erzeugt wird
**Then** ist er eine Whitelist (Options mit `TO_REDACT = {llm_api_key, weather_entity_id}` redigiert, Norm-Kennwerte, Status, Zieldatum, `data_notes`, Stück-**Anzahl** je Kategorie — nie Namen); kein Feld trägt die Wetter-Entity-ID (auch nicht `source`) (AD-5/FR-6.4/NFR-6)
**Given** ein voll befüllter Entry (Key + Entität + Stücke)
**When** der Substring-Test über den Diagnostics-JSON läuft
**Then** taucht weder der Key-Wert noch die Entity-ID im JSON auf
---
## Epic 5: Internationalisierung & Release-Reife
Ziel: Zweisprachigkeit-Vollständigkeit, CI, HACS-/Brands-/Lizenz-Konformität, README, E2E —
HACS-ready. Baut auf allen vorigen Epics (die en/de-Keys wurden inkrementell mitgeliefert; hier
Vollständigkeits-/Konformitäts-Validierung).
### Story 5.1: Vollständige Internationalisierung (de/en)
As a Nutzer,
I want die gesamte UI und alle Empfehlungstexte in meiner Sprache,
So that WTW sich in meiner Sprache natürlich anfühlt.
**Acceptance Criteria:**
**Given** die inkrementell gelieferten `translations/en.json` und `de.json`
**When** die Konsolidierung läuft
**Then** sind alle registrierten Texte vollständig (config/options/`config_subentries.item.initiate_flow.{user,reconfigure}`/entity/services/exceptions/selector); en ist vollständig; kein `strings.json` (AD-13)
**Given** hassfest
**When** die CI läuft
**Then** validiert es die Übersetzungen ohne Fehler (keine eigenen Top-Level-Keys) (AD-13/NFR-5)
**Given** die dynamischen Empfehlungstexte
**When** eine Berechnung läuft
**Then** folgen sie `hass.config.language` je Berechnung (Fallback en); registrierte Texte nutzen die HA-Translations; die Entity-ID bleibt sprachneutral fix (FR-1.3/FR-8.1)
### Story 5.2: CI-Pipeline, HACS-/Brands-/Lizenz-Konformität
As a Maintainer,
I want eine grüne CI und HACS-konforme Metadaten,
So that WTW verlässlich installierbar und HACS-ready ist.
**Acceptance Criteria:**
**Given** `.github/workflows/ci.yaml`
**When** die CI läuft
**Then** enthält sie eine Matrix (Job »min«: Python 3.13 + phcc 0.13.225 = HA 2025.3.4; Job »latest«: Python 3.14 + aktuelles phcc), führt pytest, hassfest (`home-assistant/actions/hassfest`) und die HACS-Action (`category: integration`) aus; `permissions: contents: read` als Default; ein Ressourcen-Registrierungs-Test läuft im latest-Job (AD-10/AD-18/NFR-4). **Das Grün-Kriterium bezieht sich auf den CI-Runner selbst und ist unabhängig vom GitHub-Mirror-Setup (E-3)**
**Given** die Metadaten
**When** sie geprüft werden
**Then** sind `hacs.json` (`name`, `homeassistant: "2025.3.0"`), `manifest.json` (alle Pflichtfelder) vollständig; `LICENSE` (Apache-2.0) und `NOTICE` liegen im Repo; keine gebündelten Fremdlizenzen (Karte ohne Libs) (NFR-5/NFR-9)
**Given** der brands-Eintrag
**When** er vorbereitet wird
**Then** ist ein PR an `home-assistant/brands` (`custom_integrations/what_to_wear/icon.png` 256×256 + `icon@2x.png` 512×512) vorbereitet; zusätzlich `custom_components/what_to_wear/brand/` mitgeliefert (wirkt ab 2026.3) (NFR-5) — die Einreichung selbst ist ein Benutzer-Schritt (Story 5.3)
### Story 5.3: README, E2E-Verifikation & Release-Freigabe
As a Nutzer,
I want eine klare Doku und den Nachweis, dass WTW real und schnell funktioniert,
So that ich WTW vertrauensvoll installiere und einrichte.
**Acceptance Criteria:**
**Given** `README.md`
**When** es erstellt ist
**Then** enthält es die Pflichtabschnitte: Datenschutz de+en (FR-6.5) inkl. at-rest/Backup-Hinweis (FR-6.4) und LLM-Rest-Risiko (AD-16), Inventar-Löschwarnung (FR-2.4), YAML-Mode-Schritt (AD-12), Blueprint-Import-Badges (FR-7.4), Setup-Doku-Links + Custom-Repository-Installationsweg
**Given** eine Wegwerf-HA-Docker-Instanz
**When** die E2E-Verifikation läuft
**Then** wird der Kern-Flow real und **getaktet** durchgespielt: Installation → Wetter-Entität → Beispiel-Set → Karte sichtbar → Blueprint-Push real; **die erste Empfehlung entsteht in < 15 min (S-2, Protokoll unter `docs/beta/`)**; ein phcc-Assert weist nach, dass WTW beim Setup **keine Blocking-/Startzeit-Warnung** erzeugt (C-2/NFR-2) und im Default-Setup (LLM aus) **keine ausgehende Netzverbindung** öffnet (NFR-1, z. B. via gepatchter Session/`aioclient_mock` ohne erwartete Calls); Negativfälle (keine Wetter-Entität, Entität ohne Forecast, falscher LLM-Key)
**Given** `CHANGELOG.md`, Version `v1.0.0` (== `manifest.json`) und der GitHub-Mirror/Release
**When** das Release vorbereitet wird
**Then** werden die irreversiblen/identitätskritischen Schritte dem Benutzer vorgelegt (GitHub-Mirror-Einrichtung E-3 mit Leitplanken, brands-PR-Einreichung, HACS-Default-Antrag, Tag/Release) und **nicht autonom ausgeführt**; »CI grün auf dem Mirror« ist das finale, messbare Release-Gate nach der Mirror-Freigabe (S-3)