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/`,
|
||||
`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
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",
|
||||
"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
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