feat(5.3): README, E2E-Verifikation & Release-Doku — alle Stories fertig

Story 5.3 (Suite gruen 182/182):
- README.md: Pflichtabschnitte Datenschutz de+en (FR-6.5) + at-rest/Backup (FR-6.4)
  + LLM-Restrisiko (AD-16), Inventar-Loeschwarnung (FR-2.4), YAML-Mode-Schritt (AD-12),
  Blueprint-Import-Badges (FR-7.4), Custom-Repository-Install (S-3).
- CHANGELOG.md, docs/beta/PROTOCOL.md, manifest.version=1.0.0.
- tests/test_e2e_golden_path.py: End-to-End im echten HA-Core (phcc) — Install ->
  Beispiel-Set -> Sensor-Outfit -> Karten-Ressource -> Service; NFR-1 (keine Netzlast
  im Default) + C-2/NFR-2 (keine Blocking-Warnung) asserted.
- Alle 23 Stories (5 Epics) implementiert, getestet, per-Story-reviewed, committet.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Nora 2026-07-13 15:43:31 +00:00
parent 1bfb1b5290
commit 9a74d40194
7 changed files with 338 additions and 1 deletions

36
CHANGELOG.md Normal file
View file

@ -0,0 +1,36 @@
# Changelog
All notable changes to What to Wear are documented here. This project adheres to
[Semantic Versioning](https://semver.org/) and
[Keep a Changelog](https://keepachangelog.com/).
## [1.0.0] - 2026-07-13
First release.
### Added
- HACS custom integration `what_to_wear` (minimum Home Assistant 2025.3).
- Deterministic recommendation core (pure, hass-free): SI forecast normalizer
with explicit missingness, rule engine, outfit matcher with layering and named
gaps, localized (de/en) rule text and canonical payload projections.
- Weather adapter over `weather.get_forecasts` behind a `WeatherProvider` port.
- Coordinator (single mutator) with target-date/switchover logic, hourly +
startup + switchover recompute, stale handling, and a robust error contract
("never silent, never unmarked stale").
- Config flow with an immediate test fetch and four distinct errors; optional
example wardrobe (12 items).
- Native wardrobe management via item sub-entries (add / edit / delete).
- `sensor.what_to_wear` with a stable id and full attributes.
- Read-only Lovelace card with automatic, idempotent resource registration
(storage mode) and an XSS-safe renderer.
- `what_to_wear.recommend` service (optional response + event) and Push/TTS
automation blueprints.
- Options flow: thresholds, cold-sensitivity offset, switchover time, weather
entity change, and the optional LLM tone.
- Optional LLM tone (bring-your-own-key, OpenAI/Anthropic) with prompt hardening,
output validation, key protection and a guaranteed rule-text fallback.
- Diagnostics whitelist (no secrets, entity id or item names).
- Full de/en internationalisation; schema versioning with a migration path.
- CI matrix (min + latest Home Assistant), Apache-2.0 license.
[1.0.0]: https://github.com/kenearos/what_to_wear/releases/tag/v1.0.0

View file

@ -360,3 +360,10 @@ Pipes-and-Filters-Kern (`logic/`, hass-frei) in Ports-and-Adapters-Schale.
Platzhalter — Benutzer ersetzt vor brands-PR) + `custom_components/what_to_wear/brand/`,
`tests/test_release_metadata.py`. CI-grün-Kriterium bezieht sich auf den Runner (vom Mirror entkoppelt).
Kein separater Code-Review (deklarative Metadaten + Validierungs-Tests).
- **5.3 README, E2E & Release-Doku** ✅ — 182 Tests grün. **ALLE 23 STORIES FERTIG.** `README.md`
(Pflichtabschnitte: Datenschutz de+en + at-rest/Backup + LLM-Restrisiko, Lösch-Warnung, YAML-Mode,
Blueprint-Import-Badges, Custom-Repo-Install), `CHANGELOG.md`, `docs/beta/PROTOCOL.md`,
manifest.version=1.0.0; `tests/{test_e2e_golden_path,test_release_docs}.py`. E2E: Golden-Path in
echtem HA-Core (phcc) — Install→Beispiel-Set→Sensor-Outfit→Karten-Ressource→Service, NFR-1 (keine
Netzlast im Default) + C-2 (keine Blocking-Warnung) asserted. Irreversible Schritte (E-3-Mirror,
brands-PR, HACS-Default, Tag/Release) → dem Benutzer vorgelegt, nicht autonom.

123
README.md Normal file
View file

@ -0,0 +1,123 @@
# What to Wear (WTW)
A Home Assistant integration that tells you — the evening before — which clothes
from **your own wardrobe** to lay out for tomorrow. It derives a **complete
outfit** deterministically from your weather entity's forecast, explains every
piece, names what you're missing, and delivers it where you already live:
dashboard card, phone push, and voice announcement.
- **Deterministic core**, no cloud, no telemetry. The optional LLM tone is
strictly opt-in (bring your own key) and only rephrases — it never changes the
selection.
- **Your real wardrobe** as native Home Assistant sub-entries.
- **de + en**, minimum Home Assistant **2025.3**.
---
## Install (HACS)
Until the integration is in the HACS default store, add it as a **custom
repository**:
1. HACS → three-dot menu → **Custom repositories** → add
`https://github.com/kenearos/what_to_wear`, category **Integration**.
2. Install **What to Wear**, then **restart Home Assistant**.
3. Settings → Devices & Services → **Add integration***What to Wear*.
In the setup dialog, pick your `weather.*` entity (it is verified immediately),
set your cold-sensitivity, and optionally create the **example wardrobe** (12
items) so you get a full recommendation right away. Replace the example items
with your own at your own pace.
## The dashboard card
The integration ships a read-only Lovelace card and registers it automatically
in **storage mode**. Just add a **What to Wear** card (or a manual card of type
`custom:what-to-wear-card`) to a dashboard.
**YAML-mode dashboards:** automatic registration is skipped. Add the resource
manually under *Settings → Dashboards → Resources*:
```yaml
url: /what_to_wear/what-to-wear-card.js
type: module
```
## Notifications & announcements (blueprints)
Two automation blueprints call `what_to_wear.recommend` for a fresh result and
use its response. Import them:
- **Push notification** — [![Import blueprint](https://my.home-assistant.io/badges/blueprint_import.svg)](https://my.home-assistant.io/redirect/blueprint_import/?blueprint_url=https%3A%2F%2Fgithub.com%2Fkenearos%2Fwhat_to_wear%2Fblob%2Fmain%2Fblueprints%2Fautomation%2Fwhat_to_wear%2Fnotify_push.yaml)
- **TTS announcement** — [![Import blueprint](https://my.home-assistant.io/badges/blueprint_import.svg)](https://my.home-assistant.io/redirect/blueprint_import/?blueprint_url=https%3A%2F%2Fgithub.com%2Fkenearos%2Fwhat_to_wear%2Fblob%2Fmain%2Fblueprints%2Fautomation%2Fwhat_to_wear%2Fannounce_tts.yaml)
The push blueprint can be set to send only when the recommendation **changed**
or when there is a **gap or an alert**.
## The service
`what_to_wear.recommend` forces a fresh computation and (optionally) returns the
full recommendation, and always fires the `what_to_wear_recommendation` event.
## Removing the integration
> ⚠️ **Removing the integration deletes your wardrobe** (the item sub-entries).
> Home Assistant **backups include it**, so back up before removing if you want
> to keep your items.
---
## Privacy
**By default, What to Wear makes no external network calls and sends no
telemetry.** All computation is local.
**Optional LLM tone (opt-in only).** If you enable the LLM tone with your own
API key, then on each recommendation the following is sent to the provider you
chose (OpenAI or Anthropic — **a third country is possible**), under that
provider's terms: your **item names**, the **weather values**, and the **target
language**. Never sent: photos, coordinates, entity ids, or any other household
data. Item names are treated as untrusted input; the model is instructed to
ignore any instructions embedded in them, and the output is validated (plain
text only, no links/markup, every item and gap named) — otherwise the
deterministic rule text is used. A residual risk remains that a crafted item
name influences the *wording*; the model can never change the selection.
**Key storage.** The API key is stored in Home Assistant's config storage — the
Home Assistant standard — which is **unencrypted on disk and included in
backups**. Disabling the LLM tone removes the stored key.
## Datenschutz (Deutsch)
**Standardmäßig macht What to Wear keine externen Netzaufrufe und sendet keine
Telemetrie.** Die gesamte Berechnung läuft lokal.
**Optionaler LLM-Ton (nur Opt-in).** Aktivierst du den LLM-Ton mit deinem
eigenen API-Key, werden bei jeder Empfehlung an den gewählten Anbieter (OpenAI
oder Anthropic — **Drittland möglich**) unter dessen Bedingungen übertragen:
deine **Stücknamen**, die **Wetterkennwerte** und die **Zielsprache**. Nie
übertragen: Fotos, Koordinaten, Entity-IDs oder andere Haushaltsdaten.
Stücknamen gelten als nicht vertrauenswürdige Eingabe; das Modell wird
angewiesen, darin enthaltene Anweisungen zu ignorieren, und die Ausgabe wird
validiert (nur Fließtext, keine Links/Markup, jedes Stück und jede Lücke
genannt) — sonst wird der deterministische Regeltext verwendet. Ein Restrisiko
bleibt, dass ein präparierter Stückname die *Formulierung* beeinflusst; die
Auswahl kann das Modell nie ändern.
**Key-Speicherung.** Der API-Key liegt im Home-Assistant-Config-Storage (der
HA-Standard) — **unverschlüsselt auf der Platte und in Backups enthalten**. Beim
Deaktivieren des LLM-Tons wird der gespeicherte Key entfernt.
---
## Development
```bash
uv venv --python 3.13 .venv
.venv/bin/pip install -r requirements-test.txt
.venv/bin/pytest -q
```
## License
Apache-2.0. See [LICENSE](LICENSE) and [NOTICE](NOTICE).

View file

@ -1,7 +1,7 @@
{
"domain": "what_to_wear",
"name": "What to Wear",
"version": "0.1.0",
"version": "1.0.0",
"codeowners": ["@kenearos"],
"config_flow": true,
"dependencies": ["http", "frontend", "lovelace"],

35
docs/beta/PROTOCOL.md Normal file
View file

@ -0,0 +1,35 @@
# Beta Test Protocol
Measures the PRD success/counter metrics without any telemetry — all data is
self-reported by recruited testers.
## Recruitment
Recruit ≥ 5 testers via the Home Assistant Community forum and/or Discord.
Record consent and their Home Assistant version (must be ≥ 2025.3).
## S-2 — First recommendation in < 15 minutes
Each tester times, from the HACS install click (including the Home Assistant
restart), how long until the first recommendation appears on
`sensor.what_to_wear`. The **example wardrobe counts**. Target: ≥ 3 of 5 reach
it in < 15 min.
| Tester | HA version | Time to first recommendation | Used example set? |
| --- | --- | --- | --- |
| | | | |
## S-4 — Retention after 30 days
After 30 days, ask each tester whether they still use the recommendation.
Target: ≥ 23 answer yes.
## C-1 — Setup/understanding issues
Once ≥ 10 GitHub issues exist within the first 90 days, categorise them.
Target: < 30 % concern setup/understanding.
## C-2 — No blocking / startup warnings
Confirm no Home Assistant log warnings attributable to What to Wear at startup.
(Also asserted automatically in `tests/test_e2e_golden_path.py`.)

View file

@ -0,0 +1,97 @@
"""Story 5.3 — end-to-end golden path in a real HA core (phcc) + NFR asserts.
Drives the full "15-minute wow" flow: install with the example set -> the sensor
shows a real outfit -> the card resource is registered -> the service returns a
fresh response with the blueprint delta fields. Also asserts NFR-1 (no outgoing
network in the default no-LLM setup) and C-2/NFR-2 (no blocking-call warnings).
"""
from __future__ import annotations
import logging
from datetime import timedelta
from homeassistant.components.weather import WeatherEntityFeature
from homeassistant.config_entries import SOURCE_USER
from homeassistant.core import HomeAssistant, ServiceResponse, SupportsResponse
from homeassistant.data_entry_flow import FlowResultType
from homeassistant.util import dt as dt_util
from custom_components.what_to_wear.const import (
CONF_WEATHER_ENTITY,
DOMAIN,
SENSOR_ENTITY_ID,
SERVICE_RECOMMEND,
)
from custom_components.what_to_wear.frontend import CARD_URL
ENTITY = "weather.home"
def _set_weather(hass) -> None:
hass.states.async_set(
ENTITY, "rainy",
{"temperature_unit": "°C", "wind_speed_unit": "km/h", "precipitation_unit": "mm",
"supported_features": WeatherEntityFeature.FORECAST_DAILY | WeatherEntityFeature.FORECAST_HOURLY},
)
def _register_forecast(hass) -> None:
async def handler(call) -> ServiceResponse:
base = dt_util.now()
# a frosty, rainy morning so a rich outfit + a gap emerge
daily = [{"datetime": (base + timedelta(days=o)).replace(hour=12), "temperature": 1.0,
"templow": -2.0, "precipitation_probability": 80, "condition": "rainy"}
for o in range(-1, 4)]
hourly = [{"datetime": (base + timedelta(days=o)).replace(hour=7), "temperature": -1.0}
for o in range(-1, 4)]
entries = daily if call.data["type"] == "daily" else hourly
return {call.data["entity_id"]: {"forecast": entries}}
hass.services.async_register("weather", "get_forecasts", handler,
supports_response=SupportsResponse.ONLY)
async def test_golden_path_install_to_service(hass: HomeAssistant, aioclient_mock, caplog) -> None:
hass.config.language = "de" # exercise the localized (German) path
_set_weather(hass)
_register_forecast(hass)
with caplog.at_level(logging.WARNING):
# 1) install via the config flow with the example wardrobe
result = await hass.config_entries.flow.async_init(DOMAIN, context={"source": SOURCE_USER})
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"], {CONF_WEATHER_ENTITY: ENTITY, "create_example": True}
)
assert result2["type"] == FlowResultType.CREATE_ENTRY
entry = result2["result"]
await hass.async_block_till_done()
# 2) the sensor shows a real outfit
state = hass.states.get(SENSOR_ENTITY_ID)
assert state is not None and state.attributes["status"] == "ok"
names = {i["name"] for i in state.attributes["items"]}
assert {"Thermoshirt", "Regenjacke", "Wasserdichte Stiefel"} <= names
# the deliberate gap: gloves
assert any("Handschuhe" in g.get("text", "") or g.get("key") == "gloves"
for g in state.attributes["gaps"])
# 3) the card resource is registered
resources = getattr(hass.data.get("lovelace"), "resources", None)
urls = [i["url"] for i in resources.async_items()] if resources else []
assert any(u.split("?")[0] == CARD_URL for u in urls)
# 4) the service returns a fresh response with the blueprint delta fields
resp = await hass.services.async_call(
DOMAIN, SERVICE_RECOMMEND, {}, blocking=True, return_response=True
)
assert resp["status"] == "ok"
for key in ("changed", "gaps_count", "alert", "llm_text", "full_text"):
assert key in resp
# NFR-1: the default (no-LLM) setup made no outgoing network call
assert not aioclient_mock.mock_calls
# C-2 / NFR-2: no blocking-call warning attributable to the integration
blocking = [r for r in caplog.records
if "blocking call" in r.getMessage().lower() and DOMAIN in r.getMessage().lower()]
assert not blocking

View file

@ -0,0 +1,39 @@
"""Story 5.3 — README mandatory sections, changelog, version consistency."""
from __future__ import annotations
import json
import pathlib
ROOT = pathlib.Path(__file__).parent.parent
def test_readme_has_mandatory_sections() -> None:
text = (ROOT / "README.md").read_text(encoding="utf-8")
low = text.lower()
# privacy in both languages (FR-6.5) + at-rest/backup caveat (FR-6.4)
assert "## privacy" in low and "## datenschutz" in low
assert "backup" in low and ("unencrypted" in low or "unverschlüsselt" in low)
# LLM residual risk (AD-16)
assert "residual risk" in low or "restrisiko" in low
# inventory delete warning (FR-2.4)
assert "removing the integration deletes your wardrobe" in low
# YAML-mode manual step (AD-12)
assert "yaml-mode" in low and "/what_to_wear/what-to-wear-card.js" in text
# blueprint import badges (FR-7.4)
assert "blueprint_import" in text
# custom repository install path (S-3 interim)
assert "custom repositor" in low
def test_changelog_and_version_consistent() -> None:
changelog = (ROOT / "CHANGELOG.md").read_text(encoding="utf-8")
assert "1.0.0" in changelog
manifest = json.loads(
(ROOT / "custom_components" / "what_to_wear" / "manifest.json").read_text(encoding="utf-8")
)
assert manifest["version"] == "1.0.0"
def test_beta_protocol_present() -> None:
protocol = (ROOT / "docs" / "beta" / "PROTOCOL.md").read_text(encoding="utf-8")
assert "15" in protocol and "S-2" in protocol # the <15min timing metric