feat(2.5): Blueprints Push & TTS — Epic 2 komplett
Story 2.5 (deklaratives Artefakt, Struktur-Validierung, Suite gruen 130/130): - blueprints/automation/what_to_wear/notify_push.yaml: device(mobile_app)+time+ boolean, Bedingung changed/gaps_count/alert, notify.mobile_app_ via slugify. - announce_tts.yaml: tts+media_player+trigger-Selector, tts.speak. - Beide: what_to_wear.recommend via response_variable, rec.llm_text || full_text, min_version 2025.3.0, gehaertete Templates (.get()+Defaults), continue_on_error. - luna-pro-Story-Review: 5/6 Findings uebernommen, 1 verworfen (flat-response-Evidenz). Epic 2 (15-Minuten-Wow) komplett: Kleiderschrank->Karte->Service->Push/TTS. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
dc941a127f
commit
8effa251cf
5 changed files with 186 additions and 5 deletions
|
|
@ -5,7 +5,7 @@
|
|||
> Dieses Ledger überlebt Kontext-Kompaktierung: erledigte Stories, offene Punkte, Gate-Ergebnisse
|
||||
> (inkl. je Kritiker-Aufruf geschickter Dateien + Modell), Finding-Urteile.
|
||||
|
||||
**Stand:** 2026-07-13 · **Phase:** 5 LAUFEND — Story-Loop; **EPIC 1 KOMPLETT** (Stories 1.1–1.9, 108 Tests grün, committet): installierbar, `sensor.what_to_wear` zeigt echte Empfehlung. Umgebung `.venv` Py3.13 + HA 2025.3.4. · Nächster Epic: 2 (15-Minuten-Wow: Kleiderschrank→Karte→Push), Story 2.1
|
||||
**Stand:** 2026-07-13 · **Phase:** 5 LAUFEND — Story-Loop; **EPIC 1+2 KOMPLETT** (Stories 1.1–2.5, 130 Tests grün): 15-Minuten-Wow-Pfad steht (Kleiderschrank→Karte→Service→Push/TTS). Umgebung `.venv` Py3.13 + HA 2025.3.4. · Nächster Epic: 3 (Anpassung & Robustheit), Story 3.1
|
||||
|
||||
## Benutzer-Entscheidungen E-1…E-4 (2026-07-11)
|
||||
- **E-1 Lizenz: Apache-2.0** (bewusst statt MIT-Vorschlag).
|
||||
|
|
@ -298,3 +298,10 @@ Pipes-and-Filters-Kern (`logic/`, hass-frei) in Ports-and-Adapters-Schale.
|
|||
last_success_utc stempeln; F4 eigene Registrierung per Flag), 2 verworfen mit Evidenz (F2 frühe
|
||||
Returns unerreichbar da Service nur mit Coordinator registriert + data nie None; F3 async_refresh
|
||||
propagiert keine Exceptions, to_event/response projizieren jeden Status).
|
||||
- **2.5 Blueprints Push & TTS** ✅ — 130 Tests grün. **EPIC 2 KOMPLETT.**
|
||||
`blueprints/automation/what_to_wear/{notify_push,announce_tts}.yaml` + `tests/test_blueprints.py`
|
||||
(deklaratives Artefakt: Struktur-/YAML-Validierung statt rotem Test). min_version 2025.3.0,
|
||||
device/time/boolean bzw. tts/media_player/trigger-Selektoren, what_to_wear.recommend via
|
||||
response_variable, changed/gaps_count/alert-Bedingung (FR-7.4/AD-11/20). **luna-pro-Review:**
|
||||
6 Findings, 5 übernommen (F1–F4 Template-Härtung mit .get()+Defaults; F5 continue_on_error am
|
||||
notify), 1 verworfen mit Evidenz (F6 to_response ist flach, kein Nesting).
|
||||
|
|
|
|||
42
blueprints/automation/what_to_wear/announce_tts.yaml
Normal file
42
blueprints/automation/what_to_wear/announce_tts.yaml
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
blueprint:
|
||||
name: What to Wear — Announce (TTS)
|
||||
description: >
|
||||
Announces the clothing recommendation on a media player via text-to-speech
|
||||
when your chosen trigger fires. It calls what_to_wear.recommend for a fresh
|
||||
result and speaks the LLM text when available, otherwise the rule text.
|
||||
domain: automation
|
||||
min_version: "2025.3.0"
|
||||
source_url: https://github.com/kenearos/what_to_wear/blob/main/blueprints/automation/what_to_wear/announce_tts.yaml
|
||||
input:
|
||||
tts_entity:
|
||||
name: Text-to-speech engine
|
||||
description: The tts.* entity used to synthesize speech.
|
||||
selector:
|
||||
entity:
|
||||
domain: tts
|
||||
media_player:
|
||||
name: Media player
|
||||
description: Where to announce the recommendation.
|
||||
selector:
|
||||
entity:
|
||||
domain: media_player
|
||||
announce_trigger:
|
||||
name: Trigger
|
||||
description: When to announce (e.g. a time, a button press, a person arriving).
|
||||
selector:
|
||||
trigger: {}
|
||||
|
||||
mode: single
|
||||
|
||||
trigger: !input announce_trigger
|
||||
|
||||
action:
|
||||
- service: what_to_wear.recommend
|
||||
response_variable: rec
|
||||
- service: tts.speak
|
||||
continue_on_error: true
|
||||
target:
|
||||
entity_id: !input tts_entity
|
||||
data:
|
||||
media_player_entity_id: !input media_player
|
||||
message: "{{ rec.get('llm_text') or rec.get('full_text') or 'What to Wear' }}"
|
||||
55
blueprints/automation/what_to_wear/notify_push.yaml
Normal file
55
blueprints/automation/what_to_wear/notify_push.yaml
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
blueprint:
|
||||
name: What to Wear — Push notification
|
||||
description: >
|
||||
Sends tomorrow's clothing recommendation to a mobile-app device at a chosen
|
||||
time. It calls what_to_wear.recommend for a fresh result and can optionally
|
||||
only notify when the recommendation changed or when there is a gap or alert.
|
||||
domain: automation
|
||||
min_version: "2025.3.0"
|
||||
source_url: https://github.com/kenearos/what_to_wear/blob/main/blueprints/automation/what_to_wear/notify_push.yaml
|
||||
input:
|
||||
notify_device:
|
||||
name: Mobile device
|
||||
description: The mobile-app device that should receive the notification.
|
||||
selector:
|
||||
device:
|
||||
integration: mobile_app
|
||||
push_time:
|
||||
name: Time
|
||||
description: When to send the notification.
|
||||
default: "20:00:00"
|
||||
selector:
|
||||
time: {}
|
||||
only_if_relevant:
|
||||
name: Only send when relevant
|
||||
description: >
|
||||
Only notify if the recommendation changed since the last delivery, or if
|
||||
there is a gap or an alert (a must-requirement beyond the base outfit).
|
||||
default: false
|
||||
selector:
|
||||
boolean: {}
|
||||
|
||||
mode: single
|
||||
|
||||
variables:
|
||||
notify_device: !input notify_device
|
||||
only_relevant: !input only_if_relevant
|
||||
|
||||
trigger:
|
||||
- platform: time
|
||||
at: !input push_time
|
||||
|
||||
action:
|
||||
- service: what_to_wear.recommend
|
||||
response_variable: rec
|
||||
- condition: template
|
||||
value_template: >
|
||||
{{ (not only_relevant)
|
||||
or (rec.get('changed') | default(false))
|
||||
or (rec.get('gaps_count') | default(0) | int(0)) > 0
|
||||
or (rec.get('alert') | default(false)) }}
|
||||
- service: "notify.mobile_app_{{ device_attr(notify_device, 'name') | slugify }}"
|
||||
continue_on_error: true
|
||||
data:
|
||||
title: What to Wear
|
||||
message: "{{ rec.get('llm_text') or rec.get('full_text') or 'What to Wear' }}"
|
||||
|
|
@ -21,17 +21,16 @@ async def _unload_wtw_entries(request):
|
|||
"""Unload any What to Wear entries after each hass test.
|
||||
|
||||
This cancels the coordinator's periodic refresh timer so it never lingers
|
||||
into a following test under randomized ordering.
|
||||
into a following test under randomized ordering. Only runs for tests that
|
||||
actually use the ``hass`` fixture; guarded so pure-logic tests are untouched.
|
||||
"""
|
||||
yield
|
||||
if "hass" not in request.fixturenames:
|
||||
return
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
|
||||
from custom_components.what_to_wear.const import DOMAIN
|
||||
|
||||
hass = request.getfixturevalue("hass")
|
||||
for entry in list(hass.config_entries.async_entries(DOMAIN)):
|
||||
for entry in list(hass.config_entries.async_entries("what_to_wear")):
|
||||
if entry.state is ConfigEntryState.LOADED:
|
||||
await hass.config_entries.async_unload(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
|
|
|||
78
tests/test_blueprints.py
Normal file
78
tests/test_blueprints.py
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
"""Story 2.5 — blueprint validity (declarative artifact, verifiable criterion).
|
||||
|
||||
Blueprints are not runtime-tested with a red test (DEV-METHOD: a declarative
|
||||
artifact gets a pre-noted, verifiable acceptance criterion instead). Here we
|
||||
assert they parse and carry the required structure, min_version, selectors, and
|
||||
the response_variable-based call into what_to_wear.recommend.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import pathlib
|
||||
|
||||
import yaml
|
||||
|
||||
BP_DIR = pathlib.Path(__file__).parent.parent / "blueprints" / "automation" / "what_to_wear"
|
||||
|
||||
|
||||
class _BlueprintLoader(yaml.SafeLoader):
|
||||
pass
|
||||
|
||||
|
||||
def _passthrough(loader, node):
|
||||
if isinstance(node, yaml.ScalarNode):
|
||||
return {"__tag__": node.tag, "value": loader.construct_scalar(node)}
|
||||
if isinstance(node, yaml.SequenceNode):
|
||||
return loader.construct_sequence(node)
|
||||
return loader.construct_mapping(node)
|
||||
|
||||
|
||||
# Handle HA's custom !input tag so PyYAML can parse the blueprints.
|
||||
_BlueprintLoader.add_constructor("!input", _passthrough)
|
||||
|
||||
|
||||
def _load(name: str) -> dict:
|
||||
with open(BP_DIR / name, encoding="utf-8") as fh:
|
||||
return yaml.load(fh, Loader=_BlueprintLoader)
|
||||
|
||||
|
||||
def _dump(obj) -> str:
|
||||
return yaml.dump(obj, default_flow_style=False)
|
||||
|
||||
|
||||
def test_push_blueprint_structure() -> None:
|
||||
bp = _load("notify_push.yaml")
|
||||
meta = bp["blueprint"]
|
||||
assert meta["domain"] == "automation"
|
||||
assert meta["min_version"] == "2025.3.0"
|
||||
inputs = meta["input"]
|
||||
assert inputs["notify_device"]["selector"]["device"]["integration"] == "mobile_app"
|
||||
assert "time" in inputs["push_time"]["selector"]
|
||||
assert "boolean" in inputs["only_if_relevant"]["selector"]
|
||||
# calls the service with a response_variable and uses the response, not the event
|
||||
text = _dump(bp)
|
||||
assert "what_to_wear.recommend" in text
|
||||
assert "response_variable" in text
|
||||
assert "llm_text" in text and "full_text" in text
|
||||
# the "only when relevant" logic uses changed/gaps_count/alert
|
||||
assert "changed" in text and "gaps_count" in text and "alert" in text
|
||||
|
||||
|
||||
def test_tts_blueprint_structure() -> None:
|
||||
bp = _load("announce_tts.yaml")
|
||||
meta = bp["blueprint"]
|
||||
assert meta["domain"] == "automation"
|
||||
assert meta["min_version"] == "2025.3.0"
|
||||
inputs = meta["input"]
|
||||
assert inputs["media_player"]["selector"]["entity"]["domain"] == "media_player"
|
||||
assert "trigger" in inputs["announce_trigger"]["selector"]
|
||||
text = _dump(bp)
|
||||
assert "what_to_wear.recommend" in text
|
||||
assert "response_variable" in text
|
||||
assert "tts.speak" in text
|
||||
assert "llm_text" in text and "full_text" in text
|
||||
|
||||
|
||||
def test_both_have_source_url_for_import() -> None:
|
||||
for name in ("notify_push.yaml", "announce_tts.yaml"):
|
||||
bp = _load(name)
|
||||
assert bp["blueprint"]["source_url"].startswith("https://")
|
||||
Loading…
Add table
Add a link
Reference in a new issue