Story 2.1 (TDD, phcc, Suite gruen 114/114):
- config_flow.py: async_get_supported_subentry_types + ItemSubentryFlow (user +
reconfigure mit Kompat-Accessor 2025.3/2025.4), Item-Schema (AD-22), Validierung
(name 1-60, temp_range), Werte-Erhalt bei Fehler.
- coordinator/__init__: Options-vs-Subentry-Unterscheidung via Snapshot (Options->reload,
Subentry->refresh, AD-7).
- translations/{en,de}.json: config_subentries + selector-Optionen (AD-13).
- luna-pro-Story-Review: 3/4 Findings uebernommen, 1 verworfen (Evidenz im Ledger).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
139 lines
6.1 KiB
Python
139 lines
6.1 KiB
Python
"""Story 2.1 — wardrobe item subentry flow (add/edit) and recompute."""
|
|
from __future__ import annotations
|
|
|
|
from datetime import timedelta
|
|
|
|
from homeassistant.components.weather import WeatherEntityFeature
|
|
from homeassistant.config_entries import ConfigSubentry, ConfigSubentryData
|
|
from homeassistant.core import HomeAssistant, ServiceResponse, SupportsResponse
|
|
from homeassistant.data_entry_flow import FlowResultType
|
|
from homeassistant.util import dt as dt_util
|
|
from pytest_homeassistant_custom_component.common import MockConfigEntry
|
|
from types import MappingProxyType
|
|
|
|
from custom_components.what_to_wear.config_flow import (
|
|
ItemSubentryFlow,
|
|
_clean_item,
|
|
_validate_item,
|
|
)
|
|
from custom_components.what_to_wear.const import CONF_WEATHER_ENTITY, DOMAIN, SENSOR_ENTITY_ID
|
|
from custom_components.what_to_wear.coordinator import build_items
|
|
|
|
ENTITY = "weather.home"
|
|
|
|
|
|
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 _setup(hass, subentries=()):
|
|
_set_weather(hass)
|
|
_register_forecast(hass)
|
|
entry = MockConfigEntry(domain=DOMAIN, data={CONF_WEATHER_ENTITY: ENTITY}, options={},
|
|
subentries_data=list(subentries))
|
|
entry.add_to_hass(hass)
|
|
assert await hass.config_entries.async_setup(entry.entry_id)
|
|
await hass.async_block_till_done()
|
|
return entry
|
|
|
|
|
|
def test_validate_and_clean() -> None:
|
|
assert _validate_item({"name": "", "category": "top", "warmth": 2}) == "invalid_name"
|
|
assert _validate_item({"name": "X" * 61, "category": "top", "warmth": 2}) == "invalid_name"
|
|
assert _validate_item({"name": "A", "category": "top", "warmth": 2,
|
|
"temp_min": 20, "temp_max": 10}) == "temp_range"
|
|
assert _validate_item({"name": "Jeans", "category": "bottom", "warmth": 3}) is None
|
|
cleaned = _clean_item({"name": " Jeans ", "category": "bottom", "warmth": "3",
|
|
"waterproof": True})
|
|
assert cleaned == {"name": "Jeans", "category": "bottom", "warmth": 3, "waterproof": True,
|
|
"windproof": False, "sun_protection": False, "formality": "casual",
|
|
"active": True}
|
|
|
|
|
|
async def test_add_item_via_subentry_flow(hass: HomeAssistant) -> None:
|
|
entry = await _setup(hass)
|
|
result = await hass.config_entries.subentries.async_init(
|
|
(entry.entry_id, "item"), context={"source": "user"}
|
|
)
|
|
assert result["type"] == FlowResultType.FORM
|
|
result2 = await hass.config_entries.subentries.async_configure(
|
|
result["flow_id"],
|
|
{"name": "Jeans", "category": "bottom", "warmth": 3},
|
|
)
|
|
assert result2["type"] == FlowResultType.CREATE_ENTRY
|
|
await hass.async_block_till_done()
|
|
# the item is now stored as a subentry and flows into the recommendation
|
|
items = build_items(entry)
|
|
assert any(i.name == "Jeans" for i in items)
|
|
|
|
|
|
async def test_add_item_invalid_name_shows_error(hass: HomeAssistant) -> None:
|
|
entry = await _setup(hass)
|
|
result = await hass.config_entries.subentries.async_init(
|
|
(entry.entry_id, "item"), context={"source": "user"}
|
|
)
|
|
result2 = await hass.config_entries.subentries.async_configure(
|
|
result["flow_id"], {"name": "", "category": "top", "warmth": 2}
|
|
)
|
|
assert result2["type"] == FlowResultType.FORM
|
|
assert result2["errors"] == {"base": "invalid_name"}
|
|
|
|
|
|
async def test_supported_subentry_types(hass: HomeAssistant) -> None:
|
|
from custom_components.what_to_wear.config_flow import WhatToWearConfigFlow
|
|
|
|
entry = MockConfigEntry(domain=DOMAIN, data={CONF_WEATHER_ENTITY: ENTITY})
|
|
types = WhatToWearConfigFlow.async_get_supported_subentry_types(entry)
|
|
assert types == {"item": ItemSubentryFlow}
|
|
|
|
|
|
async def test_subentry_change_triggers_recompute(hass: HomeAssistant) -> None:
|
|
entry = await _setup(hass)
|
|
for name, cat in [("Shirt", "top"), ("Jeans", "bottom"), ("Boots", "shoes")]:
|
|
sub = ConfigSubentry(
|
|
data=MappingProxyType({"name": name, "category": cat, "warmth": 3}),
|
|
subentry_type="item", title=name, unique_id=None,
|
|
)
|
|
assert hass.config_entries.async_add_subentry(entry, sub)
|
|
# A subentry change routes to a refresh (not a reload); force the pending
|
|
# recompute deterministically and verify the items flow into the sensor.
|
|
coordinator = hass.data[DOMAIN][entry.entry_id]
|
|
await coordinator.async_refresh()
|
|
await hass.async_block_till_done()
|
|
state = hass.states.get(SENSOR_ENTITY_ID)
|
|
names = {it["name"] for it in state.attributes["items"]}
|
|
assert {"Shirt", "Jeans", "Boots"} <= names
|
|
|
|
|
|
async def test_single_subentry_add_auto_refreshes(hass: HomeAssistant) -> None:
|
|
# A single item add fires the update listener -> subentry route -> refresh.
|
|
entry = await _setup(hass, [
|
|
ConfigSubentryData(data={"name": "Jeans", "category": "bottom", "warmth": 3},
|
|
subentry_type="item", title="Jeans", unique_id=None),
|
|
ConfigSubentryData(data={"name": "Boots", "category": "shoes", "warmth": 3},
|
|
subentry_type="item", title="Boots", unique_id=None),
|
|
])
|
|
sub = ConfigSubentry(
|
|
data=MappingProxyType({"name": "Shirt", "category": "top", "warmth": 3}),
|
|
subentry_type="item", title="Shirt", unique_id=None,
|
|
)
|
|
assert hass.config_entries.async_add_subentry(entry, sub)
|
|
await hass.async_block_till_done()
|
|
state = hass.states.get(SENSOR_ENTITY_ID)
|
|
names = {it["name"] for it in state.attributes["items"]}
|
|
assert "Shirt" in names # the newly added item was picked up automatically
|