diff --git a/LEDGER.md b/LEDGER.md index ae044e9..bdace9f 100644 --- a/LEDGER.md +++ b/LEDGER.md @@ -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+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 +**Stand:** 2026-07-13 · **Phase:** RELEASEFERTIG — alle 23 Stories (5 Epics) + Gate 1/2/3 durch, 188 Tests grün, ruff clean, E2E im echten HA-Core verifiziert. **Phase 7 (Release): irreversible/identitätskritische Schritte (E-3-Mirror, brands-PR, HACS-Default, Tag/Release) liegen dem Benutzer zur Entscheidung vor.** ## Benutzer-Entscheidungen E-1…E-4 (2026-07-11) - **E-1 Lizenz: Apache-2.0** (bewusst statt MIT-Vorschlag). diff --git a/custom_components/what_to_wear/config_flow.py b/custom_components/what_to_wear/config_flow.py index 78f7040..f7a08b6 100644 --- a/custom_components/what_to_wear/config_flow.py +++ b/custom_components/what_to_wear/config_flow.py @@ -10,16 +10,16 @@ from __future__ import annotations import asyncio import logging import math +from datetime import time as _time from typing import Any import voluptuous as vol -from datetime import time as _time - from homeassistant.config_entries import ( ConfigEntry, ConfigFlow, ConfigFlowResult, ConfigSubentry, + ConfigSubentryData, ConfigSubentryFlow, OptionsFlow, SubentryFlowResult, @@ -30,16 +30,14 @@ from homeassistant.util import dt as dt_util from .const import ( CONF_WEATHER_ENTITY, + CONFIG_FLOW_TEST_TIMEOUT_S, CONFIG_MINOR_VERSION, CONFIG_VERSION, - CONFIG_FLOW_TEST_TIMEOUT_S, DOMAIN, SUBENTRY_TYPE_ITEM, default_options, sanitized_options, ) -from homeassistant.config_entries import ConfigSubentryData - from .logic import pipeline, texts from .logic.assemble import pick_language from .logic.model import Category @@ -417,7 +415,9 @@ class WhatToWearOptionsFlow(OptionsFlow): # --- LLM tone section (Story 4.1, FR-6.1/6.5, AD-5/23) --- vol.Optional("llm_enabled", default=current["llm_enabled"]): selector.BooleanSelector(), vol.Optional("llm_provider", default=current["llm_provider"]): selector.SelectSelector( - selector.SelectSelectorConfig(options=["openai", "anthropic"], translation_key="llm_provider") + selector.SelectSelectorConfig( + options=["openai", "anthropic"], translation_key="llm_provider" + ) ), vol.Optional("llm_model", default=current.get("llm_model", "")): selector.TextSelector(), # The API key is never prefilled (AD-5); leaving it empty keeps the stored key. diff --git a/custom_components/what_to_wear/const.py b/custom_components/what_to_wear/const.py index 4b84086..22d5832 100644 --- a/custom_components/what_to_wear/const.py +++ b/custom_components/what_to_wear/const.py @@ -129,7 +129,7 @@ def sanitized_options(stored: dict[str, Any], warn=None) -> dict[str, Any]: opts = default_options() if not isinstance(stored, Mapping): return opts # corrupt/non-mapping storage -> all defaults (never raises) - for key, default in opts.items(): + for key in opts: if key not in stored: continue value = stored[key] diff --git a/custom_components/what_to_wear/llm/client.py b/custom_components/what_to_wear/llm/client.py index 4a9d623..c707320 100644 --- a/custom_components/what_to_wear/llm/client.py +++ b/custom_components/what_to_wear/llm/client.py @@ -102,7 +102,7 @@ async def async_phrase( return text, None except _AuthError: return None, "llm_auth" - except (TimeoutError, asyncio.TimeoutError): + except TimeoutError: return None, "llm_timeout" except Exception as err: # noqa: BLE001 - any other failure -> fall back _LOGGER.debug("LLM call failed: %s", err) diff --git a/custom_components/what_to_wear/logic/matcher.py b/custom_components/what_to_wear/logic/matcher.py index 9ca9113..758c71f 100644 --- a/custom_components/what_to_wear/logic/matcher.py +++ b/custom_components/what_to_wear/logic/matcher.py @@ -144,7 +144,11 @@ def compose( attr_jackets = [j for j in jackets if _has_attrs(j, need_wp, need_windp)] best_warm = _warmest(jackets) if attr_jackets: - aj = _smallest_sufficient(attr_jackets, remaining) if remaining else min(attr_jackets, key=_sort_key) + aj = ( + _smallest_sufficient(attr_jackets, remaining) + if remaining + else min(attr_jackets, key=_sort_key) + ) attr_meets = remaining == 0 or aj.warmth >= remaining warm_meets = best_warm is not None and (remaining == 0 or best_warm.warmth >= remaining) if not attr_meets and warm_meets and warmth_prio > attr_prio: @@ -215,12 +219,13 @@ def compose( # --- Sun protection (any item with the flag) --- if RequirementKey.SUN_PROTECTION in reqs: + sun_req = reqs[RequirementKey.SUN_PROTECTION] if not any(ci.item.sun_protection for ci in chosen): sun_items = [i for i in cands if i.sun_protection] if sun_items: chosen.append(ChosenItem(min(sun_items, key=_sort_key), RequirementKey.SUN_PROTECTION)) - elif reqs[RequirementKey.SUN_PROTECTION].priority is not Priority.MAY: - gaps.append(Gap(RequirementKey.SUN_PROTECTION, reqs[RequirementKey.SUN_PROTECTION].priority, "no_sun_item")) + elif sun_req.priority is not Priority.MAY: + gaps.append(Gap(RequirementKey.SUN_PROTECTION, sun_req.priority, "no_sun_item")) # --- Hints (text-only, never gaps) --- for key in (RequirementKey.HINT_HEAT, RequirementKey.HINT_THUNDERSTORM, RequirementKey.HINT_LAYERING): diff --git a/custom_components/what_to_wear/logic/model.py b/custom_components/what_to_wear/logic/model.py index df3c620..fe99c5f 100644 --- a/custom_components/what_to_wear/logic/model.py +++ b/custom_components/what_to_wear/logic/model.py @@ -335,7 +335,7 @@ class Recommendation: return self.to_payload() -def _metrics_dict(nf: "NormForecast | None") -> dict: +def _metrics_dict(nf: NormForecast | None) -> dict: if nf is None: return {} return { diff --git a/custom_components/what_to_wear/logic/normalize.py b/custom_components/what_to_wear/logic/normalize.py index ddf399c..1c61005 100644 --- a/custom_components/what_to_wear/logic/normalize.py +++ b/custom_components/what_to_wear/logic/normalize.py @@ -233,7 +233,9 @@ def normalize(raw: RawForecast, target: date) -> NormForecast: temp_max=agg("temperature", "temperature", temp_c, "max", _TEMP_RANGE), feels_like_min=agg("apparent_temperature", "apparent_temperature", temp_c, "min", _TEMP_RANGE), feels_like_max=agg("apparent_temperature", "apparent_temperature", temp_c, "max", _TEMP_RANGE), - rain_probability=agg("precipitation_probability", "precipitation_probability", pct, "max", _RAIN_PROB_RANGE), + rain_probability=agg( + "precipitation_probability", "precipitation_probability", pct, "max", _RAIN_PROB_RANGE + ), rain_amount=agg("precipitation", "precipitation", mm, "sum", _RAIN_AMOUNT_RANGE), wind=agg("wind_speed", "wind_speed", kmh, "max", _WIND_RANGE), gust=agg("wind_gust_speed", "wind_gust_speed", kmh, "max", _WIND_RANGE), diff --git a/custom_components/what_to_wear/logic/texts.py b/custom_components/what_to_wear/logic/texts.py index fc96590..8a40f9f 100644 --- a/custom_components/what_to_wear/logic/texts.py +++ b/custom_components/what_to_wear/logic/texts.py @@ -229,7 +229,7 @@ def build_full(outfit: Outfit, data_notes: Iterable[str], lang: str) -> str: parts.append("Kein passendes Stück im Schrank." if lg == "de" else "No matching item in wardrobe.") if outfit.gaps: gap_sentence = "; ".join(gap_for(g, lang) for g in outfit.gaps) - parts.append((f"Lücken: {gap_sentence}." if lg == "de" else f"Gaps: {gap_sentence}.")) + parts.append(f"Lücken: {gap_sentence}." if lg == "de" else f"Gaps: {gap_sentence}.") for key in outfit.hints: h = hint_for(key, lang) if h: diff --git a/pyproject.toml b/pyproject.toml index 4188646..6ac5670 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,7 +13,10 @@ filterwarnings = ["ignore::DeprecationWarning"] [tool.ruff] target-version = "py313" -line-length = 100 +line-length = 110 [tool.ruff.lint] select = ["E", "F", "I", "UP", "B"] +# B905 (zip strict) — several zips intentionally pair unequal-length sequences. +# UP042 (StrEnum) — `str, Enum` is deliberate and equivalent for our use. +ignore = ["B905", "UP042"]