Aggiorna script backup e bot meteo Telegram.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
3a7b867e83
commit
beb790ed7e
6 files changed
+350
-102
No files matched your search
@@ -1,16 +1,50 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# raspiBackup post extension — notifica Loogle Casa (solo WebApp).
|
# raspiBackup post extension — notifica Loogle Casa (solo WebApp).
|
||||||
|
#
|
||||||
|
# RaspiBackup chiama il post hook PRIMA di STARTSERVICES: su Pi2 Casa è giù
|
||||||
|
# (before-stop ferma loogle-casa, STOPSERVICES ferma docker). Non notificare
|
||||||
|
# subito: systemd-run stacca dal cgroup e riprova finché l'API risponde.
|
||||||
|
#
|
||||||
# Sourced da raspiBackup: $1 = return code backup.
|
# Sourced da raspiBackup: $1 = return code backup.
|
||||||
|
# Eseguito da systemd-run: $1 = --wait-send
|
||||||
|
|
||||||
|
LOOGLE_NOTIFY="/home/daniely/docker/loogle-casa/scripts/loogle-notify.sh"
|
||||||
|
MAX_TRIES="${LOOGLE_BACKUP_NOTIFY_TRIES:-36}"
|
||||||
|
SLEEP_SECS="${LOOGLE_BACKUP_NOTIFY_SLEEP:-5}"
|
||||||
|
|
||||||
|
wait_send() {
|
||||||
|
local title="$1" body="$2" severity="$3" tag="$4"
|
||||||
|
local i
|
||||||
|
if [[ ! -x "$LOOGLE_NOTIFY" ]]; then
|
||||||
|
echo "loogle-notify.sh assente" >&2
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
for ((i = 1; i <= MAX_TRIES; i++)); do
|
||||||
|
if "$LOOGLE_NOTIFY" --title "$title" --body "$body" --category raspi_backup \
|
||||||
|
--severity "$severity" --tag "$tag"; then
|
||||||
|
echo "notify ok try=$i tag=$tag"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
echo "notify retry $i/$MAX_TRIES tag=$tag" >&2
|
||||||
|
sleep "$SLEEP_SECS"
|
||||||
|
done
|
||||||
|
echo "notify failed after ${MAX_TRIES} tries tag=$tag" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if [[ "${1:-}" == "--wait-send" ]]; then
|
||||||
|
wait_send "${2:-}" "${3:-}" "${4:-info}" "${5:-raspi_backup}"
|
||||||
|
exit $?
|
||||||
|
fi
|
||||||
|
|
||||||
RC="${1:-1}"
|
RC="${1:-1}"
|
||||||
LOOGLE_NOTIFY="/home/daniely/docker/loogle-casa/scripts/loogle-notify.sh"
|
HOST="${HOSTNAME:-$(hostname)}"
|
||||||
|
HOST="${HOST%%.*}"
|
||||||
|
|
||||||
if [[ ! -x "$LOOGLE_NOTIFY" ]]; then
|
if [[ ! -x "$LOOGLE_NOTIFY" ]]; then
|
||||||
return 0 2>/dev/null || exit 0
|
return 0 2>/dev/null || exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
HOST="${HOSTNAME:-$(hostname)}"
|
|
||||||
|
|
||||||
if (( RC == 0 )); then
|
if (( RC == 0 )); then
|
||||||
TITLE="Backup completato"
|
TITLE="Backup completato"
|
||||||
BODY="Backup di ${HOST} terminato con successo."
|
BODY="Backup di ${HOST} terminato con successo."
|
||||||
@@ -21,7 +55,19 @@ else
|
|||||||
SEVERITY="error"
|
SEVERITY="error"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
"$LOOGLE_NOTIFY" --title "$TITLE" --body "$BODY" --category raspi_backup \
|
TAG="raspi_backup-${HOST}"
|
||||||
--severity "$SEVERITY" &
|
SELF="$(readlink -f "${BASH_SOURCE[0]:-$0}" 2>/dev/null || echo /usr/local/bin/raspiBackup_loogle_post.sh)"
|
||||||
|
|
||||||
|
# Stacca dal servizio raspiBackup (KillMode=control-group) e aspetta Casa.
|
||||||
|
if command -v systemd-run >/dev/null 2>&1; then
|
||||||
|
systemd-run --quiet --collect \
|
||||||
|
--description="Loogle Casa raspiBackup notify ${HOST}" \
|
||||||
|
/bin/bash "$SELF" --wait-send "$TITLE" "$BODY" "$SEVERITY" "$TAG" \
|
||||||
|
|| true
|
||||||
|
else
|
||||||
|
nohup /bin/bash "$SELF" --wait-send "$TITLE" "$BODY" "$SEVERITY" "$TAG" \
|
||||||
|
>/dev/null 2>&1 &
|
||||||
|
disown 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
return 0 2>/dev/null || exit 0
|
return 0 2>/dev/null || exit 0
|
||||||
@@ -17,6 +17,7 @@ from open_meteo_precip import (
|
|||||||
CASA_TZ,
|
CASA_TZ,
|
||||||
daily_precip_from_hourly,
|
daily_precip_from_hourly,
|
||||||
hourly_precip_mm,
|
hourly_precip_mm,
|
||||||
|
hourly_table_should_show,
|
||||||
is_casa,
|
is_casa,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -499,16 +500,15 @@ def generate_weather_report(lat, lon, location_name, debug_mode=False, cc="IT",
|
|||||||
day_date = dt.date()
|
day_date = dt.date()
|
||||||
is_new_day = (current_day is not None and day_date != current_day)
|
is_new_day = (current_day is not None and day_date != current_day)
|
||||||
|
|
||||||
# Determina se mostrare questo timestamp in base alla posizione nelle 48h
|
# Prime 24h: ogni ora. Dalla 25a alla 48a: ogni 2 ore, ma mai nascondere
|
||||||
# Prime 24h: ogni ora (step=1)
|
# un'ora con precipitazione (altrimenti il picco cade sulle ore dispari
|
||||||
# Dalla 25a alla 48a: ogni 2 ore (step=2)
|
# e la tabella 48h mostra 5 mm mentre Meteo7 ha 22.4 mm sul giorno).
|
||||||
if hours_from_start < 24:
|
Pr_early = get_val(l_prec[idx], 0)
|
||||||
step = 1 # Prime 24h: dettaglio 1 ora
|
Rain_early = get_val(l_rain[idx], 0)
|
||||||
else:
|
Showers_early = get_val(l_showers[idx], 0) if idx < len(l_showers) else 0
|
||||||
step = 2 # Dalla 25a alla 48a: dettaglio 2 ore
|
Code_early = int(get_val(l_code[idx], 0))
|
||||||
|
Pr_display = hourly_precip_mm(Pr_early, Rain_early, Showers_early)
|
||||||
# Controlla se questo timestamp deve essere mostrato
|
should_show = hourly_table_should_show(hours_from_start, Pr_display, Code_early)
|
||||||
should_show = (hours_from_start % step == 0)
|
|
||||||
|
|
||||||
# Se è un nuovo giorno, chiudi il blocco precedente
|
# Se è un nuovo giorno, chiudi il blocco precedente
|
||||||
if is_new_day and current_block_lines:
|
if is_new_day and current_block_lines:
|
||||||
@@ -541,12 +541,11 @@ def generate_weather_report(lat, lon, location_name, debug_mode=False, cc="IT",
|
|||||||
elif diff >= 2.5: t_suffix = "H"
|
elif diff >= 2.5: t_suffix = "H"
|
||||||
t_s = f"{int(round(T))}{t_suffix}"
|
t_s = f"{int(round(T))}{t_suffix}"
|
||||||
|
|
||||||
Pr = get_val(l_prec[idx], 0)
|
Pr = Pr_early
|
||||||
Sn = get_val(l_snow[idx], 0)
|
Sn = get_val(l_snow[idx], 0)
|
||||||
Code = int(get_val(l_code[idx], 0))
|
Code = Code_early
|
||||||
Rain = get_val(l_rain[idx], 0)
|
Rain = Rain_early
|
||||||
Showers = get_val(l_showers[idx], 0) if idx < len(l_showers) else 0
|
Showers = Showers_early
|
||||||
Pr_display = hourly_precip_mm(Pr, Rain, Showers)
|
|
||||||
|
|
||||||
# Determina se è neve
|
# Determina se è neve
|
||||||
is_snowing = Sn > 0 or (Code in [71, 73, 75, 77, 85, 86])
|
is_snowing = Sn > 0 or (Code in [71, 73, 75, 77, 85, 86])
|
||||||
@@ -676,7 +675,7 @@ def generate_weather_report(lat, lon, location_name, debug_mode=False, cc="IT",
|
|||||||
|
|
||||||
legend = {
|
legend = {
|
||||||
"temp": "W=wind chill, H=heat index",
|
"temp": "W=wind chill, H=heat index",
|
||||||
"precip": "G=grandine, Z=ghiacciato, N=neve",
|
"precip": "G=grandine, Z=ghiacciato, N=neve. Dalla 25ª ora: secco ogni 2h, pioggia sempre visibile",
|
||||||
"cloud": "FOG=nebbia",
|
"cloud": "FOG=nebbia",
|
||||||
"sky": "Icona condizioni (☀️🌧️⛈️…)",
|
"sky": "Icona condizioni (☀️🌧️⛈️…)",
|
||||||
"sx": "☃️ neve · 🧊 ghiaccio · ⚡/🌪️ temporali · 🥵 caldo · ☔️ pioggia · 💨 vento forte",
|
"sx": "☃️ neve · 🧊 ghiaccio · ⚡/🌪️ temporali · 🥵 caldo · ☔️ pioggia · 💨 vento forte",
|
||||||
|
|||||||
@@ -41,6 +41,9 @@ ICON_DAILY_VARS = (
|
|||||||
|
|
||||||
PRECIP_HOURLY_KEYS = ("precipitation", "rain", "showers", "snowfall")
|
PRECIP_HOURLY_KEYS = ("precipitation", "rain", "showers", "snowfall")
|
||||||
PRECIP_DAILY_KEYS = ("precipitation_sum", "rain_sum", "showers_sum", "snowfall_sum", "precipitation_hours")
|
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:
|
def is_casa(lat: float, lon: float, tol: float = 0.01) -> bool:
|
||||||
@@ -84,6 +87,61 @@ def daily_precip_from_hourly(hourly: Dict) -> Dict[str, float]:
|
|||||||
return dict(out)
|
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]:
|
def daily_precip_sum(daily: Dict, date_str: str) -> Optional[float]:
|
||||||
times = daily.get("time") or []
|
times = daily.get("time") or []
|
||||||
arr = daily.get("precipitation_sum") or []
|
arr = daily.get("precipitation_sum") or []
|
||||||
@@ -125,8 +183,19 @@ def fetch_icon_italia(
|
|||||||
return None
|
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]:
|
def _index_by_time(section: Dict) -> Dict[str, int]:
|
||||||
return {str(t): i for i, t in enumerate(section.get("time") or [])}
|
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:
|
def overlay_icon_precip_on_hourly(target: Dict, icon_hourly: Dict) -> Dict:
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ from open_meteo_precip import (
|
|||||||
CASA_LAT,
|
CASA_LAT,
|
||||||
CASA_LON,
|
CASA_LON,
|
||||||
CASA_TZ,
|
CASA_TZ,
|
||||||
daily_precip_from_hourly,
|
apply_hourly_daily_precip,
|
||||||
fetch_icon_italia,
|
fetch_icon_italia,
|
||||||
hourly_precip_at_index,
|
hourly_precip_at_index,
|
||||||
hourly_precip_series,
|
hourly_precip_series,
|
||||||
@@ -560,6 +560,7 @@ def merge_multi_model_forecast(models_data, forecast_days=10):
|
|||||||
"snowfall": [],
|
"snowfall": [],
|
||||||
"snow_depth": [],
|
"snow_depth": [],
|
||||||
"rain": [],
|
"rain": [],
|
||||||
|
"showers": [],
|
||||||
"weathercode": [],
|
"weathercode": [],
|
||||||
"windspeed_10m": [],
|
"windspeed_10m": [],
|
||||||
"winddirection_10m": [],
|
"winddirection_10m": [],
|
||||||
@@ -1055,67 +1056,55 @@ def analyze_daily_events(times, codes, probs, precip, winds, temps, dewpoints, s
|
|||||||
start_ice = i
|
start_ice = i
|
||||||
ice_type = current_ice_condition
|
ice_type = current_ice_condition
|
||||||
|
|
||||||
# 2. PRECIPITAZIONI
|
# 2. PRECIPITAZIONI — tot_mm è sempre la somma del solo blocco orario (mai il totale giorno)
|
||||||
|
def _precip_type_at(idx):
|
||||||
|
code_val = codes[idx] if idx < len(codes) and codes[idx] is not None else 0
|
||||||
|
t_val = temps[idx] if idx < len(temps) and temps[idx] is not None else None
|
||||||
|
try:
|
||||||
|
code_val = int(code_val) if code_val is not None else 0
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
code_val = 0
|
||||||
|
return get_precip_type(code_val, temp=t_val)
|
||||||
|
|
||||||
|
def _emit_rain(start, end_inclusive, rain_type):
|
||||||
|
if end_inclusive < start:
|
||||||
|
return
|
||||||
|
block_precip = precip[start:end_inclusive + 1]
|
||||||
|
block_precip_clean = [p for p in block_precip if p is not None]
|
||||||
|
tot_mm = sum(float(p) for p in block_precip_clean)
|
||||||
|
if tot_mm <= 0:
|
||||||
|
return
|
||||||
|
hours_str = _format_event_hours(times, start, end_inclusive)
|
||||||
|
avg_intensity = tot_mm / len(block_precip) if block_precip else 0
|
||||||
|
events.append(
|
||||||
|
f"{rain_type} ({get_intensity_label(avg_intensity)}):\n"
|
||||||
|
f" 🕒 {hours_str} | 💧 {tot_mm:.1f}mm"
|
||||||
|
)
|
||||||
|
|
||||||
in_rain = False
|
in_rain = False
|
||||||
start_idx = 0
|
start_idx = 0
|
||||||
current_rain_type = ""
|
current_rain_type = ""
|
||||||
|
|
||||||
for i in range(len(times)):
|
for i in range(len(times)):
|
||||||
p_val = precip[i] if i < len(precip) and precip[i] is not None else 0
|
p_val = precip[i] if i < len(precip) and precip[i] is not None else 0
|
||||||
is_raining = p_val >= MIN_MM_PER_EVENTO
|
is_raining = p_val >= MIN_MM_PER_EVENTO
|
||||||
|
is_last = i == len(times) - 1
|
||||||
|
|
||||||
if is_raining and not in_rain:
|
if is_raining and not in_rain:
|
||||||
in_rain = True
|
in_rain = True
|
||||||
start_idx = i
|
start_idx = i
|
||||||
code_val = codes[i] if i < len(codes) and codes[i] is not None else 0
|
current_rain_type = _precip_type_at(i)
|
||||||
t_val = temps[i] if i < len(temps) and temps[i] is not None else None
|
elif in_rain and is_raining:
|
||||||
try:
|
new_type = _precip_type_at(i)
|
||||||
code_val = int(code_val) if code_val is not None else 0
|
|
||||||
except (ValueError, TypeError):
|
|
||||||
code_val = 0
|
|
||||||
current_rain_type = get_precip_type(code_val, temp=t_val)
|
|
||||||
elif in_rain and is_raining and i < len(codes):
|
|
||||||
code_val = codes[i] if codes[i] is not None else 0
|
|
||||||
t_val = temps[i] if i < len(temps) and temps[i] is not None else None
|
|
||||||
try:
|
|
||||||
code_val = int(code_val) if code_val is not None else 0
|
|
||||||
except (ValueError, TypeError):
|
|
||||||
code_val = 0
|
|
||||||
new_type = get_precip_type(code_val, temp=t_val)
|
|
||||||
if new_type != current_rain_type:
|
if new_type != current_rain_type:
|
||||||
end_inclusive = i - 1
|
_emit_rain(start_idx, i - 1, current_rain_type)
|
||||||
block_precip = precip[start_idx:i] if i <= len(precip) else precip[start_idx:]
|
|
||||||
block_precip_clean = [p for p in block_precip if p is not None]
|
|
||||||
tot_mm = sum(block_precip_clean)
|
|
||||||
hours_str = _format_event_hours(times, start_idx, end_inclusive)
|
|
||||||
avg_intensity = tot_mm / len(block_precip) if block_precip else 0
|
|
||||||
|
|
||||||
events.append(
|
|
||||||
f"{current_rain_type} ({get_intensity_label(avg_intensity)}):\n"
|
|
||||||
f" 🕒 {hours_str} | 💧 {tot_mm:.1f}mm"
|
|
||||||
)
|
|
||||||
start_idx = i
|
start_idx = i
|
||||||
current_rain_type = new_type
|
current_rain_type = new_type
|
||||||
elif (not is_raining and in_rain) or (in_rain and i == len(times)-1):
|
|
||||||
|
if in_rain and (not is_raining or is_last):
|
||||||
|
end_inclusive = i if is_raining else i - 1
|
||||||
|
_emit_rain(start_idx, end_inclusive, current_rain_type)
|
||||||
in_rain = False
|
in_rain = False
|
||||||
if not is_raining:
|
|
||||||
end_inclusive = i - 1
|
|
||||||
end_slice = i
|
|
||||||
else:
|
|
||||||
end_inclusive = i
|
|
||||||
end_slice = i + 1
|
|
||||||
block_precip = precip[start_idx:end_slice] if end_slice <= len(precip) else precip[start_idx:]
|
|
||||||
block_precip_clean = [p for p in block_precip if p is not None]
|
|
||||||
tot_mm = sum(block_precip_clean)
|
|
||||||
|
|
||||||
if tot_mm > 0 and end_inclusive >= start_idx:
|
|
||||||
hours_str = _format_event_hours(times, start_idx, end_inclusive)
|
|
||||||
avg_intensity = tot_mm / len(block_precip) if block_precip else 0
|
|
||||||
|
|
||||||
events.append(
|
|
||||||
f"{current_rain_type} ({get_intensity_label(avg_intensity)}):\n"
|
|
||||||
f" 🕒 {hours_str} | 💧 {tot_mm:.1f}mm"
|
|
||||||
)
|
|
||||||
|
|
||||||
# 3. VENTO
|
# 3. VENTO
|
||||||
if winds:
|
if winds:
|
||||||
@@ -1249,16 +1238,7 @@ def _apply_unified_precip(hourly: Dict, daily: Dict, casa: bool) -> Tuple[Dict,
|
|||||||
if icon_d.get("time"):
|
if icon_d.get("time"):
|
||||||
daily = overlay_icon_precip_on_daily(daily, icon_d)
|
daily = overlay_icon_precip_on_daily(daily, icon_d)
|
||||||
hourly["precipitation"] = hourly_precip_series(hourly)
|
hourly["precipitation"] = hourly_precip_series(hourly)
|
||||||
totals = daily_precip_from_hourly(hourly)
|
daily = apply_hourly_daily_precip(daily, hourly)
|
||||||
times = daily.get("time") or []
|
|
||||||
psum = list(daily.get("precipitation_sum") or [])
|
|
||||||
while len(psum) < len(times):
|
|
||||||
psum.append(None)
|
|
||||||
for i, t in enumerate(times):
|
|
||||||
d = str(t)[:10]
|
|
||||||
if d in totals:
|
|
||||||
psum[i] = round(totals[d], 2)
|
|
||||||
daily["precipitation_sum"] = psum
|
|
||||||
return hourly, daily
|
return hourly, daily
|
||||||
|
|
||||||
|
|
||||||
@@ -1806,27 +1786,36 @@ def format_weather_context_report(models_data, location_name, country_code, as_j
|
|||||||
if day_info['precip_sum'] > 0.1:
|
if day_info['precip_sum'] > 0.1:
|
||||||
# Caratterizza usando dati daily se disponibili
|
# Caratterizza usando dati daily se disponibili
|
||||||
precip_parts = []
|
precip_parts = []
|
||||||
|
snow_sum = day_info.get("snowfall_sum", 0) or 0
|
||||||
# Neve (solo con aria abbastanza fredda: evita cm spurii in estate)
|
rain_sum = day_info.get("rain_sum", 0) or 0
|
||||||
if day_info.get('snowfall_sum', 0) > 0.1 and day_info['t_min'] <= ICE_EVENT_MAX_AIR_TEMP:
|
showers_sum = day_info.get("showers_sum", 0) or 0
|
||||||
precip_parts.append(f"❄️ {day_info['snowfall_sum']:.1f}cm")
|
precip_sum = day_info.get("precip_sum", 0) or 0
|
||||||
|
|
||||||
# Pioggia
|
if snow_sum > 0.1 and day_info["t_min"] <= ICE_EVENT_MAX_AIR_TEMP:
|
||||||
if day_info.get('rain_sum', 0) > 0.1:
|
precip_parts.append(f"❄️ {snow_sum:.1f}cm")
|
||||||
precip_parts.append(f"🌧️ {day_info['rain_sum']:.1f}mm")
|
|
||||||
|
overlapping = (
|
||||||
# Temporali (showers)
|
rain_sum > 0.1 and showers_sum > 0.1 and (
|
||||||
if day_info.get('showers_sum', 0) > 0.1:
|
abs(rain_sum - precip_sum) < 0.25
|
||||||
precip_parts.append(f"⛈️ {day_info['showers_sum']:.1f}mm")
|
or abs(showers_sum - precip_sum) < 0.25
|
||||||
|
or (rain_sum + showers_sum) > precip_sum + 0.3
|
||||||
# Se non abbiamo dati daily dettagliati, usa il tipo generale
|
)
|
||||||
if not precip_parts:
|
)
|
||||||
precip_symbol = "❄️" if day_info['precip_type'] == "snow" else "⛈️" if day_info['precip_type'] in ("hail", "thunderstorms") else "🌨️" if day_info['precip_type'] == "mixed" else "🌧️"
|
if overlapping or (rain_sum <= 0.1 and showers_sum <= 0.1):
|
||||||
precip_parts.append(f"{precip_symbol} {day_info['precip_sum']:.1f}mm")
|
if precip_sum > 0.1:
|
||||||
elif day_info['precip_sum'] > 0.1 and day_info.get('snowfall_sum', 0) > 0.1 and day_info['t_min'] > ICE_EVENT_MAX_AIR_TEMP:
|
precip_symbol = (
|
||||||
# snowfall spurio a caldo: assicurati che l'accumulo pioggia totale sia visibile
|
"❄️" if day_info["precip_type"] == "snow"
|
||||||
if not any("🌧️" in p or "⛈️" in p for p in precip_parts):
|
else "⛈️" if day_info["precip_type"] in ("hail", "thunderstorms")
|
||||||
precip_parts.append(f"🌧️ {day_info['precip_sum']:.1f}mm")
|
else "🌨️" if day_info["precip_type"] == "mixed"
|
||||||
|
else "🌧️"
|
||||||
|
)
|
||||||
|
if not (day_info["precip_type"] == "snow" and snow_sum > 0.1 and day_info["t_min"] <= ICE_EVENT_MAX_AIR_TEMP):
|
||||||
|
precip_parts.append(f"{precip_symbol} {precip_sum:.1f}mm")
|
||||||
|
else:
|
||||||
|
if rain_sum > 0.1:
|
||||||
|
precip_parts.append(f"🌧️ {rain_sum:.1f}mm")
|
||||||
|
if showers_sum > 0.1:
|
||||||
|
precip_parts.append(f"⛈️ {showers_sum:.1f}mm")
|
||||||
|
|
||||||
line += f" | {' + '.join(precip_parts)}"
|
line += f" | {' + '.join(precip_parts)}"
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,86 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Regressione visibilità ore nella tabella meteo 48h."""
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||||
|
|
||||||
|
from open_meteo_precip import (
|
||||||
|
apply_hourly_daily_precip,
|
||||||
|
hourly_table_should_show,
|
||||||
|
overlay_icon_precip_on_hourly,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_first_24h_all_shown():
|
||||||
|
for h in range(24):
|
||||||
|
assert hourly_table_should_show(h, 0.0, 3) is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_second_day_even_hours_shown():
|
||||||
|
assert hourly_table_should_show(24, 0.0, 3) is True # lun 08
|
||||||
|
assert hourly_table_should_show(32, 5.0, 63) is True # lun 16
|
||||||
|
|
||||||
|
|
||||||
|
def test_second_day_odd_dry_hidden():
|
||||||
|
assert hourly_table_should_show(31, 0.0, 3) is False # lun 15 secco
|
||||||
|
|
||||||
|
|
||||||
|
def test_second_day_odd_wet_shown():
|
||||||
|
# Caso reale 17/08/2026: 15:00 = 17.4 mm (code 65) era nascosto → 48h mostrava solo 5 mm alle 16
|
||||||
|
assert hourly_table_should_show(31, 17.4, 65) is True
|
||||||
|
assert hourly_table_should_show(31, 0.0, 65) is True # codice precip anche con mm 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_apply_hourly_daily_precip_matches_hours():
|
||||||
|
hourly = {
|
||||||
|
"time": [f"2026-08-17T{h:02d}:00" for h in range(24)],
|
||||||
|
"precipitation": [0.0] * 24,
|
||||||
|
"rain": [0.0] * 24,
|
||||||
|
"showers": [0.0] * 24,
|
||||||
|
"snowfall": [0.0] * 24,
|
||||||
|
}
|
||||||
|
hourly["rain"][15] = 17.4
|
||||||
|
hourly["rain"][16] = 5.0
|
||||||
|
hourly["precipitation"][15] = 17.4
|
||||||
|
hourly["precipitation"][16] = 5.0
|
||||||
|
daily = {
|
||||||
|
"time": ["2026-08-17"],
|
||||||
|
"precipitation_sum": [18.0],
|
||||||
|
"rain_sum": [18.0],
|
||||||
|
"showers_sum": [20.0],
|
||||||
|
"snowfall_sum": [0.0],
|
||||||
|
}
|
||||||
|
out = apply_hourly_daily_precip(daily, hourly)
|
||||||
|
assert out["precipitation_sum"][0] == 22.4
|
||||||
|
assert out["rain_sum"][0] == 22.4
|
||||||
|
assert out["showers_sum"][0] == 0.0
|
||||||
|
|
||||||
|
|
||||||
|
def test_overlay_matches_normalized_timestamps():
|
||||||
|
target = {
|
||||||
|
"time": ["2026-08-17T15:00"],
|
||||||
|
"precipitation": [0.0],
|
||||||
|
"rain": [0.0],
|
||||||
|
"showers": [0.0],
|
||||||
|
"snowfall": [0.0],
|
||||||
|
}
|
||||||
|
icon = {
|
||||||
|
"time": ["2026-08-17T15:00:00"],
|
||||||
|
"precipitation": [17.4],
|
||||||
|
"rain": [17.4],
|
||||||
|
"showers": [0.0],
|
||||||
|
"snowfall": [0.0],
|
||||||
|
}
|
||||||
|
out = overlay_icon_precip_on_hourly(target, icon)
|
||||||
|
assert abs(out["precipitation"][0] - 17.4) < 0.01
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
test_first_24h_all_shown()
|
||||||
|
test_second_day_even_hours_shown()
|
||||||
|
test_second_day_odd_dry_hidden()
|
||||||
|
test_second_day_odd_wet_shown()
|
||||||
|
test_apply_hourly_daily_precip_matches_hours()
|
||||||
|
test_overlay_matches_normalized_timestamps()
|
||||||
|
print("OK hourly_table_should_show")
|
||||||
@@ -109,6 +109,62 @@ def test_real_gelicidio_cold():
|
|||||||
print("OK real gelicidio at subzero")
|
print("OK real gelicidio at subzero")
|
||||||
|
|
||||||
|
|
||||||
|
def test_monday_storm_event_mm_is_hourly_sum():
|
||||||
|
"""15:00=17.4 + 16:00=5.0 → un evento 22.4 mm, non 5 mm né 22.4 ripetuto."""
|
||||||
|
times = [f"2026-08-17T{h:02d}:00" for h in range(24)]
|
||||||
|
precip = [0.0] * 24
|
||||||
|
precip[15] = 17.4
|
||||||
|
precip[16] = 5.0
|
||||||
|
codes = [3] * 24
|
||||||
|
codes[15] = 65
|
||||||
|
codes[16] = 63
|
||||||
|
temps = [26.0] * 24
|
||||||
|
events = analyze_daily_events(
|
||||||
|
times, codes, None, precip, [18.0] * 24, temps, [16.0] * 24,
|
||||||
|
snowfalls=[0.0] * 24, rains=precip[:],
|
||||||
|
)
|
||||||
|
rain_ev = [e for e in events if "🕒" in e]
|
||||||
|
assert len(rain_ev) == 1, rain_ev
|
||||||
|
assert "15:00-17:00" in rain_ev[0]
|
||||||
|
assert "22.4mm" in rain_ev[0]
|
||||||
|
assert rain_ev[0].count("22.4mm") == 1
|
||||||
|
print("OK lun 17 15-17h = 22.4mm")
|
||||||
|
|
||||||
|
|
||||||
|
def test_three_pulses_keep_own_mm_not_daily_total():
|
||||||
|
times = [f"2026-08-16T{h:02d}:00" for h in range(24)]
|
||||||
|
precip = [0.0] * 24
|
||||||
|
precip[10] = 18.0
|
||||||
|
precip[14] = 19.0
|
||||||
|
precip[18] = 20.0
|
||||||
|
events = analyze_daily_events(
|
||||||
|
times, [63] * 24, None, precip, [10.0] * 24, [24.0] * 24, [14.0] * 24,
|
||||||
|
snowfalls=[0.0] * 24, rains=precip[:],
|
||||||
|
)
|
||||||
|
rain_ev = [e for e in events if "🕒" in e]
|
||||||
|
assert len(rain_ev) == 3, rain_ev
|
||||||
|
assert "18.0mm" in rain_ev[0]
|
||||||
|
assert "19.0mm" in rain_ev[1]
|
||||||
|
assert "20.0mm" in rain_ev[2]
|
||||||
|
assert all("57.0mm" not in e for e in rain_ev)
|
||||||
|
print("OK tre fasce con mm propri")
|
||||||
|
|
||||||
|
|
||||||
|
def test_last_hour_rain_is_emitted():
|
||||||
|
times = [f"2026-08-17T{h:02d}:00" for h in range(24)]
|
||||||
|
precip = [0.0] * 24
|
||||||
|
precip[23] = 4.2
|
||||||
|
events = analyze_daily_events(
|
||||||
|
times, [61] * 24, None, precip, [8.0] * 24, [20.0] * 24, [12.0] * 24,
|
||||||
|
snowfalls=[0.0] * 24, rains=precip[:],
|
||||||
|
)
|
||||||
|
rain_ev = [e for e in events if "🕒" in e]
|
||||||
|
assert rain_ev, events
|
||||||
|
assert "23:00-00:00" in rain_ev[0]
|
||||||
|
assert "4.2mm" in rain_ev[0]
|
||||||
|
print("OK pioggia ultima ora del giorno")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
test_weathercode_mode_not_median()
|
test_weathercode_mode_not_median()
|
||||||
test_get_precip_type_temp_gate()
|
test_get_precip_type_temp_gate()
|
||||||
@@ -116,4 +172,7 @@ if __name__ == "__main__":
|
|||||||
test_single_hour_event_range()
|
test_single_hour_event_range()
|
||||||
test_trend_max_min_not_media()
|
test_trend_max_min_not_media()
|
||||||
test_real_gelicidio_cold()
|
test_real_gelicidio_cold()
|
||||||
|
test_monday_storm_event_mm_is_hourly_sum()
|
||||||
|
test_three_pulses_keep_own_mm_not_daily_total()
|
||||||
|
test_last_hour_rain_is_emitted()
|
||||||
print("\nAll interpretation regression checks passed.")
|
print("\nAll interpretation regression checks passed.")
|
||||||
Reference in new issue
Block a user