199 lines
6.3 KiB
Python
199 lines
6.3 KiB
Python
#!/usr/bin/env python3
|
||
# -*- coding: utf-8 -*-
|
||
"""
|
||
Precipitazione unificata Open-Meteo per Casa (San Marino).
|
||
|
||
Reference 0–72 h: ICON Italia (ARPAE 2i). Totale orario = max(precipitation, rain + showers).
|
||
|
||
Copia autosufficiente per il container loogle-bot (/app); la sorgente condivisa
|
||
resta in docker/shared/open_meteo/open_meteo_precip.py.
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
from collections import defaultdict
|
||
from typing import Dict, List, Optional
|
||
|
||
try:
|
||
from open_meteo_client import open_meteo_get
|
||
except ImportError:
|
||
import requests
|
||
|
||
def open_meteo_get(url, params=None, headers=None, timeout=(5, 25), retries=3, backoff=0.8):
|
||
return requests.get(url, params=params, headers=headers or {}, timeout=timeout)
|
||
|
||
OPEN_METEO_URL = "https://api.open-meteo.com/v1/forecast"
|
||
DEFAULT_HEADERS = {"User-Agent": "open-meteo-precip/1.0"}
|
||
|
||
CASA_LAT = 43.9356
|
||
CASA_LON = 12.4296
|
||
CASA_TZ = "Europe/Rome"
|
||
ICON_ITALIA_MODEL = "italia_meteo_arpae_icon_2i"
|
||
|
||
ICON_HOURLY_VARS = (
|
||
"precipitation,rain,showers,snowfall,weathercode,"
|
||
"temperature_2m,windspeed_10m,winddirection_10m"
|
||
)
|
||
ICON_DAILY_VARS = (
|
||
"precipitation_sum,rain_sum,showers_sum,snowfall_sum,weathercode,"
|
||
"temperature_2m_min,temperature_2m_max"
|
||
)
|
||
|
||
PRECIP_HOURLY_KEYS = ("precipitation", "rain", "showers", "snowfall")
|
||
PRECIP_DAILY_KEYS = ("precipitation_sum", "rain_sum", "showers_sum", "snowfall_sum", "precipitation_hours")
|
||
|
||
|
||
def is_casa(lat: float, lon: float, tol: float = 0.01) -> bool:
|
||
return abs(lat - CASA_LAT) < tol and abs(lon - CASA_LON) < tol
|
||
|
||
|
||
def hourly_precip_mm(
|
||
precipitation: Optional[float] = None,
|
||
rain: Optional[float] = None,
|
||
showers: Optional[float] = None,
|
||
) -> float:
|
||
p = float(precipitation or 0)
|
||
r = float(rain or 0)
|
||
s = float(showers or 0)
|
||
return max(p, r + s)
|
||
|
||
|
||
def hourly_precip_at_index(hourly: Dict, index: int) -> float:
|
||
def g(key: str) -> Optional[float]:
|
||
arr = hourly.get(key) or []
|
||
if index >= len(arr):
|
||
return None
|
||
v = arr[index]
|
||
return float(v) if v is not None else None
|
||
|
||
return hourly_precip_mm(g("precipitation"), g("rain"), g("showers"))
|
||
|
||
|
||
def hourly_precip_series(hourly: Dict) -> List[float]:
|
||
times = hourly.get("time") or []
|
||
return [hourly_precip_at_index(hourly, i) for i in range(len(times))]
|
||
|
||
|
||
def daily_precip_from_hourly(hourly: Dict) -> Dict[str, float]:
|
||
times = hourly.get("time") or []
|
||
out: Dict[str, float] = defaultdict(float)
|
||
for i, t in enumerate(times):
|
||
if not t:
|
||
continue
|
||
out[str(t)[:10]] += hourly_precip_at_index(hourly, i)
|
||
return dict(out)
|
||
|
||
|
||
def daily_precip_sum(daily: Dict, date_str: str) -> Optional[float]:
|
||
times = daily.get("time") or []
|
||
arr = daily.get("precipitation_sum") or []
|
||
for i, t in enumerate(times):
|
||
if str(t)[:10] == date_str[:10] and i < len(arr) and arr[i] is not None:
|
||
return float(arr[i])
|
||
return None
|
||
|
||
|
||
def fetch_icon_italia(
|
||
lat: float,
|
||
lon: float,
|
||
tz: str = CASA_TZ,
|
||
forecast_days: int = 10,
|
||
past_days: int = 0,
|
||
headers: Optional[Dict[str, str]] = None,
|
||
) -> Optional[Dict]:
|
||
params = {
|
||
"latitude": lat,
|
||
"longitude": lon,
|
||
"timezone": tz,
|
||
"forecast_days": forecast_days,
|
||
"models": ICON_ITALIA_MODEL,
|
||
"hourly": ICON_HOURLY_VARS,
|
||
"daily": ICON_DAILY_VARS,
|
||
}
|
||
if past_days:
|
||
params["past_days"] = past_days
|
||
try:
|
||
resp = open_meteo_get(
|
||
OPEN_METEO_URL,
|
||
params=params,
|
||
headers=headers or DEFAULT_HEADERS,
|
||
)
|
||
if resp.status_code == 200:
|
||
return resp.json()
|
||
except Exception:
|
||
pass
|
||
return None
|
||
|
||
|
||
def _index_by_time(section: Dict) -> Dict[str, int]:
|
||
return {str(t): i for i, t in enumerate(section.get("time") or [])}
|
||
|
||
|
||
def overlay_icon_precip_on_hourly(target: Dict, icon_hourly: Dict) -> Dict:
|
||
if not target or not icon_hourly:
|
||
return target
|
||
tgt_idx = _index_by_time(target)
|
||
icon_idx = _index_by_time(icon_hourly)
|
||
out = dict(target)
|
||
for key in PRECIP_HOURLY_KEYS:
|
||
arr = list(out.get(key) or [None] * len(out.get("time") or []))
|
||
while len(arr) < len(out.get("time") or []):
|
||
arr.append(None)
|
||
icon_arr = icon_hourly.get(key) or []
|
||
for t, i in tgt_idx.items():
|
||
j = icon_idx.get(t)
|
||
if j is not None and j < len(icon_arr):
|
||
arr[i] = icon_arr[j]
|
||
out[key] = arr
|
||
times = out.get("time") or []
|
||
out["precipitation"] = [hourly_precip_at_index(out, i) for i in range(len(times))]
|
||
return out
|
||
|
||
|
||
def overlay_icon_precip_on_daily(target: Dict, icon_daily: Dict) -> Dict:
|
||
if not target or not icon_daily:
|
||
return target
|
||
tgt_idx = {str(t)[:10]: i for i, t in enumerate(target.get("time") or [])}
|
||
icon_idx = {str(t)[:10]: i for i, t in enumerate(icon_daily.get("time") or [])}
|
||
out = dict(target)
|
||
for key in PRECIP_DAILY_KEYS:
|
||
arr = list(out.get(key) or [None] * len(out.get("time") or []))
|
||
while len(arr) < len(out.get("time") or []):
|
||
arr.append(None)
|
||
icon_arr = icon_daily.get(key) or []
|
||
for d, i in tgt_idx.items():
|
||
j = icon_idx.get(d)
|
||
if j is not None and j < len(icon_arr):
|
||
arr[i] = icon_arr[j]
|
||
out[key] = arr
|
||
times = out.get("time") or []
|
||
psum = list(out.get("precipitation_sum") or [None] * len(times))
|
||
while len(psum) < len(times):
|
||
psum.append(None)
|
||
for d, j in icon_idx.items():
|
||
i = tgt_idx.get(d)
|
||
if i is None:
|
||
continue
|
||
arr = icon_daily.get("precipitation_sum") or []
|
||
if j < len(arr) and arr[j] is not None:
|
||
psum[i] = float(arr[j])
|
||
out["precipitation_sum"] = psum
|
||
return out
|
||
|
||
|
||
def icon_precip_daily_totals(icon_data: Optional[Dict]) -> Dict[str, float]:
|
||
if not icon_data:
|
||
return {}
|
||
hourly = icon_data.get("hourly") or {}
|
||
if hourly.get("time"):
|
||
totals = daily_precip_from_hourly(hourly)
|
||
if totals:
|
||
return totals
|
||
daily = icon_data.get("daily") or {}
|
||
out: Dict[str, float] = {}
|
||
for i, t in enumerate(daily.get("time") or []):
|
||
arr = daily.get("precipitation_sum") or []
|
||
if i < len(arr) and arr[i] is not None:
|
||
out[str(t)[:10]] = float(arr[i])
|
||
return out
|