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:
parent
1bfb1b5290
commit
9a74d40194
7 changed files with 338 additions and 1 deletions
36
CHANGELOG.md
Normal file
36
CHANGELOG.md
Normal 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
|
||||||
|
|
@ -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/`,
|
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).
|
`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).
|
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
123
README.md
Normal 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** — [](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** — [](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).
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"domain": "what_to_wear",
|
"domain": "what_to_wear",
|
||||||
"name": "What to Wear",
|
"name": "What to Wear",
|
||||||
"version": "0.1.0",
|
"version": "1.0.0",
|
||||||
"codeowners": ["@kenearos"],
|
"codeowners": ["@kenearos"],
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"dependencies": ["http", "frontend", "lovelace"],
|
"dependencies": ["http", "frontend", "lovelace"],
|
||||||
|
|
|
||||||
35
docs/beta/PROTOCOL.md
Normal file
35
docs/beta/PROTOCOL.md
Normal 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: ≥ 2⁄3 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`.)
|
||||||
97
tests/test_e2e_golden_path.py
Normal file
97
tests/test_e2e_golden_path.py
Normal 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
|
||||||
39
tests/test_release_docs.py
Normal file
39
tests/test_release_docs.py
Normal 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
|
||||||
Loading…
Add table
Add a link
Reference in a new issue