what_to_wear/_bmad-output/planning-artifacts/epics.md
Nora 2686f253d6 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>
2026-07-13 13:12:33 +00:00

718 lines
48 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
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/skm/h, inchmm); 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 OberteilPulloverJacke 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 SetupUnloadSetup 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 v1v1-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)