Story 2.2 (TDD, phcc, Suite gruen 118/118, stabil): - logic/texts.py: example_items(lang) — 12 §8-Stücke, Namen de/en lokalisiert, englisches Schema, Handschuhe bewusst fehlend (J4-Luecke, FR-1.5). - config_flow.py: create_example-Checkbox -> async_create_entry(subentries=), nur im initialen user-Step (single_config_entry -> kein Duplikat). - tests: §8-Frost/Regen-Abnahmetest mit echtem Set; conftest entlaedt Entries (kein Coordinator-Timer-Flake unter Randomisierung). - luna-pro-Story-Review: 2/2 Findings uebernommen. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
98 lines
4.4 KiB
Python
98 lines
4.4 KiB
Python
"""Story 2.2 — example wardrobe (§8) at setup."""
|
|
from __future__ import annotations
|
|
|
|
from datetime import date, datetime, timedelta, timezone
|
|
|
|
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
|
|
from custom_components.what_to_wear.logic import matcher, pipeline, rules, texts
|
|
from custom_components.what_to_wear.logic.model import Category, Item, NormField, NormForecast, RequirementKey
|
|
from custom_components.what_to_wear import const
|
|
|
|
ENTITY = "weather.home"
|
|
UTC = timezone.utc
|
|
|
|
|
|
def test_example_items_count_and_gloves_absent() -> None:
|
|
de = texts.example_items("de")
|
|
en = texts.example_items("en")
|
|
assert len(de) == 12 and len(en) == 12
|
|
# no gloves (hands) — the deliberate J4 learning gap (§8)
|
|
assert all(i["category"] != "hands" for i in de)
|
|
assert {i["name"] for i in de} >= {"Thermoshirt", "Regenjacke", "Wasserdichte Stiefel"}
|
|
# localized names differ
|
|
assert de[0]["name"] != en[0]["name"]
|
|
|
|
|
|
def test_example_set_frost_rain_acceptance() -> None:
|
|
# §8 acceptance: example set + frost/rain -> full outfit, only gap gloves.
|
|
items = [
|
|
Item(id=str(i), name=d["name"], category=Category(d["category"]), warmth=d["warmth"],
|
|
waterproof=d.get("waterproof", False), windproof=d.get("windproof", False),
|
|
temp_min=d.get("temp_min"), temp_max=d.get("temp_max"))
|
|
for i, d in enumerate(texts.example_items("de"))
|
|
]
|
|
nf = NormForecast(
|
|
feels_like_morning=NormField(-1.0, "hourly", None),
|
|
temp_morning=NormField(-1.0, "hourly", None),
|
|
temp_min=NormField(-2.0, "daily", None), temp_max=NormField(1.0, "daily", None),
|
|
feels_like_min=NormField(None, None, None), feels_like_max=NormField(None, None, None),
|
|
rain_probability=NormField(70.0, "daily", None), rain_amount=NormField(None, None, None),
|
|
wind=NormField(10.0, "daily", None), gust=NormField(None, None, None),
|
|
uv_index=NormField(None, None, None), condition="regen",
|
|
)
|
|
result = rules.evaluate(nf, const.default_options())
|
|
outfit = matcher.compose(result.requirements, items, -1.0, 1.0)
|
|
names = {ci.item.name for ci in outfit.items}
|
|
assert names == {"Thermoshirt", "Wollpullover", "Regenjacke", "Jeans",
|
|
"Wasserdichte Stiefel", "Mütze", "Schal"}
|
|
assert {g.key for g in outfit.gaps} == {RequirementKey.GLOVES}
|
|
|
|
|
|
def _set_weather(hass) -> None:
|
|
hass.states.async_set(
|
|
ENTITY, "cloudy",
|
|
{"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()
|
|
entries = [{"datetime": (base + timedelta(days=o)).replace(hour=12), "temperature": 8.0,
|
|
"templow": 5.0} for o in range(-1, 4)]
|
|
return {call.data["entity_id"]: {"forecast": entries}}
|
|
|
|
hass.services.async_register("weather", "get_forecasts", handler,
|
|
supports_response=SupportsResponse.ONLY)
|
|
|
|
|
|
async def test_flow_creates_example_subentries(hass: HomeAssistant) -> None:
|
|
_set_weather(hass)
|
|
_register_forecast(hass)
|
|
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"]
|
|
items = [s for s in entry.subentries.values() if s.subentry_type == "item"]
|
|
assert len(items) == 12
|
|
|
|
|
|
async def test_flow_without_example_has_no_items(hass: HomeAssistant) -> None:
|
|
_set_weather(hass)
|
|
_register_forecast(hass)
|
|
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": False}
|
|
)
|
|
entry = result2["result"]
|
|
items = [s for s in entry.subentries.values() if s.subentry_type == "item"]
|
|
assert len(items) == 0
|