what_to_wear/tests/test_options_flow.py
Nora ff35d62679 feat(3.1): Options-Flow (Schwellen, Offset, Umschaltzeit, Entität)
Story 3.1 (TDD, phcc, Suite gruen 135/135):
- config_flow.py: WhatToWearOptionsFlow (Property-Muster), _build_options
  (Validierung+Normalisierung, Bandgrenzen strikt monoton, HH:MM:SS kanonisch,
  LLM-Block bewahrt), atomares async_update_entry(data+options)+abort -> ein Reload.
- async_test_entity als Modulfunktion mit switchover-Param (Zieldatum korrekt).
- coordinator/__init__: data-Snapshot -> Reload auch bei Wetter-Entitaet-Wechsel.
- translations options/abort/error; async_get_options_flow.
- luna-pro-Story-Review: 7/7 Findings uebernommen.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 15:04:14 +00:00

116 lines
4.8 KiB
Python

"""Story 3.1 — options flow (thresholds, offset, switchover, weather entity)."""
from __future__ import annotations
from datetime import timedelta
from homeassistant.components.weather import WeatherEntityFeature
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 custom_components.what_to_wear.const import CONF_WEATHER_ENTITY, DOMAIN
ENTITY = "weather.home"
ENTITY2 = "weather.station"
def _set_weather(hass, entity_id=ENTITY) -> None:
hass.states.async_set(
entity_id, "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) -> MockConfigEntry:
_set_weather(hass)
_register_forecast(hass)
entry = MockConfigEntry(domain=DOMAIN, data={CONF_WEATHER_ENTITY: ENTITY}, options={})
entry.add_to_hass(hass)
assert await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
return entry
def _full_input(**over):
base = {
CONF_WEATHER_ENTITY: ENTITY,
"cold_sensitivity_offset": 0,
"switchover_time": "10:00:00",
"warmth_band_0": 0.0, "warmth_band_1": 8.0, "warmth_band_2": 15.0, "warmth_band_3": 22.0,
"heat_threshold": 28.0, "rain_prob_should": 40, "rain_prob_must": 70,
"rain_amount_should": 1.0, "rain_amount_must": 5.0, "gust_should": 40, "wind_proxy_should": 30,
"uv_should": 6,
}
base.update(over)
return base
async def test_options_roundtrip(hass: HomeAssistant) -> None:
entry = await _setup(hass)
result = await hass.config_entries.options.async_init(entry.entry_id)
assert result["type"] == FlowResultType.FORM
result2 = await hass.config_entries.options.async_configure(
result["flow_id"], _full_input(cold_sensitivity_offset=2, switchover_time="09:30:00")
)
assert result2["type"] == FlowResultType.ABORT
await hass.async_block_till_done()
assert entry.options["cold_sensitivity_offset"] == 2
assert entry.options["switchover_time"] == "09:30:00"
assert entry.options["warmth_band_limits"] == [0.0, 8.0, 15.0, 22.0]
async def test_options_band_monotone_validation(hass: HomeAssistant) -> None:
entry = await _setup(hass)
result = await hass.config_entries.options.async_init(entry.entry_id)
result2 = await hass.config_entries.options.async_configure(
result["flow_id"], _full_input(warmth_band_1=20.0) # 0,20,15,22 -> not monotone
)
assert result2["type"] == FlowResultType.FORM
assert result2["errors"] == {"base": "band_not_monotone"}
async def test_options_change_weather_entity(hass: HomeAssistant) -> None:
entry = await _setup(hass)
_set_weather(hass, ENTITY2) # a second valid weather entity
result = await hass.config_entries.options.async_init(entry.entry_id)
result2 = await hass.config_entries.options.async_configure(
result["flow_id"], _full_input(**{CONF_WEATHER_ENTITY: ENTITY2})
)
assert result2["type"] == FlowResultType.ABORT
await hass.async_block_till_done()
assert entry.data[CONF_WEATHER_ENTITY] == ENTITY2 # written to entry.data
async def test_options_change_to_bad_entity_errors(hass: HomeAssistant) -> None:
entry = await _setup(hass)
result = await hass.config_entries.options.async_init(entry.entry_id)
result2 = await hass.config_entries.options.async_configure(
result["flow_id"], _full_input(**{CONF_WEATHER_ENTITY: "weather.ghost"})
)
assert result2["type"] == FlowResultType.FORM
assert result2["errors"] == {"base": "no_weather_entity"}
async def test_options_wired_into_rules(hass: HomeAssistant) -> None:
# A stricter rain threshold flows through to the coordinator after reload.
entry = await _setup(hass)
result = await hass.config_entries.options.async_init(entry.entry_id)
await hass.config_entries.options.async_configure(
result["flow_id"], _full_input(rain_prob_should=95)
)
await hass.async_block_till_done()
coordinator = hass.data[DOMAIN][entry.entry_id]
assert coordinator._options()["rain_prob_should"] == 95