chore: ruff-clean (line-length 110, B905/UP042 ignoriert) + Release-Stand
- Alle Integrationsdateien ruff-clean (E/F/I/UP/B); B907/UP042 als opinioniert ignoriert, restliche E501/B007 behoben. Suite gruen 188/188. - Ledger: Stand RELEASEFERTIG — 23 Stories + Gate 1/2/3 durch, E2E verifiziert. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
aae8c38a20
commit
3e0674fc5d
9 changed files with 26 additions and 16 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+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).
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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"]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue