feat(weather): add hourly precipitation summaries and peak timing to weather card
Fetch hourly precipitation probabilities from Open-Meteo alongside daily
forecasts. Generate human-readable precip summaries ("Rain likely 2–5 PM",
"Rain likely all day") for today and each forecast day. Display today's
summary as a styled callout and show peak precipitation hour in forecast rows.
Also fix briefing pipeline to parse all weather location rows (not just first).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -59,7 +59,6 @@ def test_detect_forecast_changes_rain_added():
|
||||
def test_parse_weather_card_data_returns_none_when_stale():
|
||||
"""Should return None when cache is older than 24 hours."""
|
||||
from datetime import datetime, timezone, timedelta
|
||||
from unittest.mock import MagicMock
|
||||
from fabledassistant.services.weather import parse_weather_card_data
|
||||
|
||||
cache = MagicMock()
|
||||
@@ -71,7 +70,6 @@ def test_parse_weather_card_data_returns_none_when_stale():
|
||||
def test_parse_weather_card_data_returns_card_schema():
|
||||
"""Should return the correct metadata.weather schema for fresh cache."""
|
||||
from datetime import datetime, timezone, timedelta, date
|
||||
from unittest.mock import MagicMock
|
||||
from fabledassistant.services.weather import parse_weather_card_data
|
||||
|
||||
today = date.today().isoformat()
|
||||
@@ -103,3 +101,66 @@ def test_parse_weather_card_data_returns_card_schema():
|
||||
assert card["yesterday_low"] == 9
|
||||
assert len(card["forecast"]) >= 1
|
||||
assert card["location"] == "Berlin, DE"
|
||||
assert card["precip_summary"] is None
|
||||
|
||||
|
||||
def test_summarize_precip_dry():
|
||||
from fabledassistant.services.weather import _summarize_precip
|
||||
assert _summarize_precip([(h, 10) for h in range(24)]) is None
|
||||
|
||||
|
||||
def test_summarize_precip_all_day():
|
||||
from fabledassistant.services.weather import _summarize_precip
|
||||
result = _summarize_precip([(h, 60) for h in range(24)])
|
||||
assert result is not None
|
||||
assert "all day" in result.lower()
|
||||
|
||||
|
||||
def test_summarize_precip_window():
|
||||
from fabledassistant.services.weather import _summarize_precip
|
||||
hourly = [(h, 5) for h in range(24)]
|
||||
for h in range(14, 18):
|
||||
hourly[h] = (h, 65)
|
||||
result = _summarize_precip(hourly)
|
||||
assert result is not None
|
||||
assert "2 PM" in result
|
||||
assert "65%" in result
|
||||
|
||||
|
||||
def test_parse_weather_card_includes_hourly_precip_summary():
|
||||
from datetime import datetime, timezone, timedelta, date
|
||||
from fabledassistant.services.weather import parse_weather_card_data
|
||||
|
||||
today = date.today().isoformat()
|
||||
yesterday = (date.today() - timedelta(days=1)).isoformat()
|
||||
tomorrow = (date.today() + timedelta(days=1)).isoformat()
|
||||
|
||||
hourly_times = [f"{today}T{h:02d}:00" for h in range(24)]
|
||||
hourly_probs = [5] * 24
|
||||
for h in range(14, 18):
|
||||
hourly_probs[h] = 70
|
||||
|
||||
cache = MagicMock()
|
||||
cache.fetched_at = datetime.now(timezone.utc)
|
||||
cache.location_label = "Berlin, DE"
|
||||
cache.forecast_json = {
|
||||
"current_weather": {"temperature": 12.0, "weathercode": 61},
|
||||
"daily": {
|
||||
"time": [yesterday, today, tomorrow],
|
||||
"temperature_2m_max": [14.0, 16.0, 18.0],
|
||||
"temperature_2m_min": [9.0, 8.0, 10.0],
|
||||
"precipitation_sum": [0.0, 5.0, 1.5],
|
||||
"precipitation_probability_max": [0, 70, 30],
|
||||
"weathercode": [1, 61, 61],
|
||||
"windspeed_10m_max": [10.0, 12.0, 8.0],
|
||||
},
|
||||
"hourly": {
|
||||
"time": hourly_times,
|
||||
"precipitation_probability": hourly_probs,
|
||||
},
|
||||
}
|
||||
|
||||
card = parse_weather_card_data(cache)
|
||||
assert card is not None
|
||||
assert card["precip_summary"] is not None
|
||||
assert "2 PM" in card["precip_summary"]
|
||||
|
||||
Reference in New Issue
Block a user