diff --git a/src/analysis/city_query_service.py b/src/analysis/city_query_service.py index 751ac789..1e74df69 100644 --- a/src/analysis/city_query_service.py +++ b/src/analysis/city_query_service.py @@ -103,7 +103,8 @@ def _derive_mgm_daily_highs_from_hourly( if not isinstance(hourly, list) or not hourly: return {} - daily_highs: Dict[str, float] = {} + samples: List[Tuple[str, float]] = [] + parsed_datetimes: List[datetime] = [] local_tz = timezone(timedelta(seconds=int(fallback_utc_offset))) for row in hourly: if not isinstance(row, dict): @@ -119,6 +120,9 @@ def _derive_mgm_daily_highs_from_hourly( dt = datetime.fromisoformat(raw_time.replace("Z", "+00:00")) if dt.tzinfo is not None: dt = dt.astimezone(local_tz) + else: + dt = dt.replace(tzinfo=local_tz) + parsed_datetimes.append(dt) date_key = dt.strftime("%Y-%m-%d") except Exception: if len(raw_time) >= 10 and raw_time[4] == "-" and raw_time[7] == "-": @@ -129,6 +133,24 @@ def _derive_mgm_daily_highs_from_hourly( if not date_key: continue + samples.append((date_key, temp)) + + if not samples: + return {} + + # Guardrail: do not derive "daily highs" from short intraday snippets. + if parsed_datetimes: + parsed_datetimes.sort() + horizon_hours = ( + parsed_datetimes[-1] - parsed_datetimes[0] + ).total_seconds() / 3600.0 + if horizon_hours < 30: + return {} + elif len(samples) < 24: + return {} + + daily_highs: Dict[str, float] = {} + for date_key, temp in samples: prev = daily_highs.get(date_key) daily_highs[date_key] = temp if prev is None else max(prev, temp) @@ -163,7 +185,7 @@ def _append_future_forecast_lines( if mgm_value is not None: mgm_display = f"{float(mgm_value):.1f}" future_forecasts.append( - f"{d[5:]}: {t}{temp_symbol} | πŸ‡ΊπŸ‡Έ MGM: {mgm_display}{temp_symbol}" + f"{d[5:]}: {t}{temp_symbol} | MGM: {mgm_display}{temp_symbol}" ) else: future_forecasts.append(f"{d[5:]}: {t}{temp_symbol}") diff --git a/src/data_collection/weather_sources.py b/src/data_collection/weather_sources.py index 179a291d..0ffd71db 100644 --- a/src/data_collection/weather_sources.py +++ b/src/data_collection/weather_sources.py @@ -768,22 +768,48 @@ class WeatherDataCollector: # 5. Fallback for daily_forecasts from hourly data if not results.get("daily_forecasts") and results.get("hourly"): - from collections import defaultdict - daily_max = defaultdict(list) - for h in results["hourly"]: - t = h.get("time", "") - temp = h.get("temp") - if t and temp is not None: - # Extract date from ISO timestamp like "2026-03-05T12:00:00.000Z" - date_str = t[:10] - daily_max[date_str].append(temp) - if daily_max: - results["daily_forecasts"] = {} - for d, temps in sorted(daily_max.items()): - results["daily_forecasts"][d] = max(temps) + # Guardrail: avoid treating short intraday snippets as full-day highs. + hourly_rows = results.get("hourly") or [] + parsed_times = [] + for h in hourly_rows: + t = str(h.get("time") or "") + if "T" not in t: + continue + try: + parsed_times.append(datetime.fromisoformat(t.replace("Z", "+00:00"))) + except Exception: + continue + + horizon_hours = 0.0 + if len(parsed_times) >= 2: + parsed_times.sort() + horizon_hours = ( + parsed_times[-1] - parsed_times[0] + ).total_seconds() / 3600.0 + + if len(hourly_rows) >= 24 or horizon_hours >= 30: + from collections import defaultdict + + daily_max = defaultdict(list) + for h in hourly_rows: + t = h.get("time", "") + temp = h.get("temp") + if t and temp is not None: + # Extract date from ISO timestamp like "2026-03-05T12:00:00.000Z" + date_str = t[:10] + daily_max[date_str].append(temp) + if daily_max: + results["daily_forecasts"] = {} + for d, temps in sorted(daily_max.items()): + results["daily_forecasts"][d] = max(temps) + logger.info( + f"πŸ“‹ MGM daily_forecasts (from hourly fallback): " + f"{dict(results['daily_forecasts'])}" + ) + else: logger.info( - f"πŸ“‹ MGM daily_forecasts (from hourly fallback): " - f"{dict(results['daily_forecasts'])}" + "πŸ“‹ Skip MGM daily_forecasts hourly fallback: " + f"hourly points={len(hourly_rows)}, horizon={horizon_hours:.1f}h" ) return results if "current" in results else None