what_to_wear/tests/test_weather_adapter.py
Nora 5f5cdac259 feat(1.6): Wetter-Adapter — WeatherProvider-Port + HA-Entitaet
Story 1.6 (TDD, phcc, Suite gruen 79/79):
- weather/provider.py: runtime-checkable Protocol WeatherProvider (INV-6).
- weather/ha_entity.py: get_forecasts (blocking+return_response), Feature-Check
  (nur twice_daily->ForecastUnsupported), dt_util-Parsing/Verwerfen naiver Zeit-
  stempel, Einheiten aus State-Attributen (AD-17/24); defensive Struktur-Guards.
- tests/conftest.py: phcc enable_custom_integrations.
- luna-pro-Story-Review: 4/4 Findings uebernommen.

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

124 lines
4.8 KiB
Python

"""Story 1.6 — weather adapter (WeatherProvider port + HA entity impl)."""
from __future__ import annotations
from datetime import datetime, timezone
import pytest
from homeassistant.components.weather import WeatherEntityFeature
from homeassistant.core import HomeAssistant, ServiceResponse, SupportsResponse
from homeassistant.exceptions import HomeAssistantError
from custom_components.what_to_wear.logic import normalize
from custom_components.what_to_wear.logic.model import RawForecast
from custom_components.what_to_wear.weather import provider as provider_mod
from custom_components.what_to_wear.weather.errors import ForecastUnsupported
from custom_components.what_to_wear.weather.ha_entity import HAEntityProvider
ENTITY = "weather.home"
def _set_weather(hass: HomeAssistant, features: int, attrs: dict | None = None) -> None:
base = {
"temperature_unit": "°C",
"wind_speed_unit": "km/h",
"precipitation_unit": "mm",
"supported_features": features,
}
if attrs:
base.update(attrs)
hass.states.async_set(ENTITY, "cloudy", base)
def _register_forecast(hass: HomeAssistant, by_type: dict | None = None, raises=False, response=None):
"""Register a mock weather.get_forecasts service."""
async def handler(call) -> ServiceResponse:
if raises:
raise HomeAssistantError("entity unavailable")
if response is not None:
return response
ftype = call.data.get("type")
entries = (by_type or {}).get(ftype, [])
return {call.data["entity_id"]: {"forecast": entries}}
hass.services.async_register(
"weather", "get_forecasts", handler, supports_response=SupportsResponse.ONLY
)
@pytest.mark.asyncio
async def test_provider_protocol_shape() -> None:
# The port is a runtime-checkable Protocol with the async method.
assert hasattr(provider_mod, "WeatherProvider")
assert hasattr(HAEntityProvider, "get_raw_forecast")
@pytest.mark.asyncio
async def test_daily_and_hourly_fetch_builds_rawforecast(hass: HomeAssistant) -> None:
_set_weather(hass, WeatherEntityFeature.FORECAST_DAILY | WeatherEntityFeature.FORECAST_HOURLY)
daily = [{"datetime": "2026-07-14T12:00:00+00:00", "temperature": 20.0, "condition": "rainy"}]
hourly = [{"datetime": "2026-07-14T05:00:00+00:00", "temperature": 11.0}]
_register_forecast(hass, by_type={"daily": daily, "hourly": hourly})
prov = HAEntityProvider(hass, ENTITY)
raw = await prov.get_raw_forecast()
assert isinstance(raw, RawForecast)
assert raw.units["temperature"] == "°C"
assert len(raw.daily) == 1 and len(raw.hourly) == 1
# datetimes are parsed to tz-aware datetime objects (AD-24).
assert isinstance(raw.daily[0]["datetime"], datetime)
assert raw.daily[0]["datetime"].tzinfo is not None
# and the RawForecast feeds normalize without error.
nf = normalize.normalize(raw, datetime(2026, 7, 14, tzinfo=timezone.utc).date())
assert nf.temp_max.value == 20.0
@pytest.mark.asyncio
async def test_twice_daily_only_is_unsupported(hass: HomeAssistant) -> None:
_set_weather(hass, WeatherEntityFeature.FORECAST_TWICE_DAILY)
prov = HAEntityProvider(hass, ENTITY)
with pytest.raises(ForecastUnsupported):
await prov.get_raw_forecast()
@pytest.mark.asyncio
async def test_missing_entity_is_unsupported(hass: HomeAssistant) -> None:
prov = HAEntityProvider(hass, "weather.does_not_exist")
with pytest.raises(ForecastUnsupported):
await prov.get_raw_forecast()
@pytest.mark.asyncio
async def test_service_error_propagates(hass: HomeAssistant) -> None:
_set_weather(hass, WeatherEntityFeature.FORECAST_DAILY)
_register_forecast(hass, raises=True)
prov = HAEntityProvider(hass, ENTITY)
with pytest.raises(HomeAssistantError):
await prov.get_raw_forecast()
@pytest.mark.asyncio
async def test_unparsable_datetime_entries_dropped(hass: HomeAssistant) -> None:
_set_weather(hass, WeatherEntityFeature.FORECAST_DAILY)
daily = [
{"datetime": "2026-07-14T12:00:00+00:00", "temperature": 20.0},
{"datetime": "not-a-date", "temperature": 99.0},
{"temperature": 88.0}, # no datetime at all
]
_register_forecast(hass, by_type={"daily": daily})
prov = HAEntityProvider(hass, ENTITY)
raw = await prov.get_raw_forecast()
assert len(raw.daily) == 1 # only the parseable entry survives
assert raw.daily[0]["temperature"] == 20.0
@pytest.mark.asyncio
async def test_missing_response_key_is_unsupported(hass: HomeAssistant) -> None:
# unavailable entity -> service returns without our key.
_set_weather(hass, WeatherEntityFeature.FORECAST_DAILY)
_register_forecast(hass, response={}) # key missing
prov = HAEntityProvider(hass, ENTITY)
raw = await prov.get_raw_forecast()
assert raw.daily == () # gracefully empty, normalize will yield fehler upstream