Files
loogle-scripts/services/telegram-bot/open_meteo_precip.py
T
2026-08-21 11:57:29 +02:00

268 lines
8.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Precipitazione unificata Open-Meteo per Casa (San Marino).
Reference 072 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")
WMO_PRECIP_CODES = frozenset(
list(range(51, 68)) + list(range(71, 78)) + list(range(80, 83)) + [85, 86, 95, 96, 99]
)
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_component_from_hourly(hourly: Dict, key: str) -> Dict[str, float]:
"""Somma un campo orario (rain, showers, snowfall) per data locale YYYY-MM-DD."""
times = hourly.get("time") or []
arr = hourly.get(key) or []
out: Dict[str, float] = defaultdict(float)
for i, t in enumerate(times):
if not t or i >= len(arr) or arr[i] is None:
continue
try:
out[str(t)[:10]] += float(arr[i])
except (TypeError, ValueError):
continue
return dict(out)
def apply_hourly_daily_precip(daily: Dict, hourly: Dict) -> Dict:
"""Allinea i totali daily alla somma delle ore (stessa metrica della tabella oraria)."""
daily = dict(daily)
times = daily.get("time") or []
mapping = {
"precipitation_sum": daily_precip_from_hourly(hourly),
"rain_sum": daily_component_from_hourly(hourly, "rain"),
"showers_sum": daily_component_from_hourly(hourly, "showers"),
"snowfall_sum": daily_component_from_hourly(hourly, "snowfall"),
}
for key, totals in mapping.items():
arr = list(daily.get(key) or [])
while len(arr) < len(times):
arr.append(None)
for i, t in enumerate(times):
d = str(t)[:10]
if d in totals:
arr[i] = round(totals[d], 2)
daily[key] = arr
return daily
def hourly_table_should_show(hours_from_start: int, precip_mm: float, weathercode: int = 0) -> bool:
"""
Tabella 48h: prime 24 ore tutte; dalle 25 alle 48 ogni 2 ore,
ma un'ora con precipitazione (mm o codice WMO) non viene mai omessa.
"""
if hours_from_start < 24:
return True
if hours_from_start % 2 == 0:
return True
if float(precip_mm or 0) >= 0.1:
return True
try:
code = int(weathercode or 0)
except (TypeError, ValueError):
code = 0
return code in WMO_PRECIP_CODES
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 _time_key(t) -> str:
if not t:
return ""
return str(t).strip()[:16]
def _index_by_time(section: Dict) -> Dict[str, int]:
out: Dict[str, int] = {}
for i, t in enumerate(section.get("time") or []):
k = _time_key(t)
if k:
out[k] = i
return out
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