Backup automatico script del 2026-01-18 07:00
This commit is contained in:
@@ -199,8 +199,12 @@ def call_meteo_script(args_list):
|
||||
try:
|
||||
# Esegui: python3 meteo.py --arg1 val1 ...
|
||||
cmd = ["python3", METEO_SCRIPT] + args_list
|
||||
result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
|
||||
# Timeout aumentato a 90s per gestire retry e chiamate API multiple
|
||||
# (get_forecast può fare retry + fallback, get_visibility_forecast può fare 2 chiamate)
|
||||
result = subprocess.run(cmd, capture_output=True, text=True, timeout=90)
|
||||
return result.stdout if result.returncode == 0 else f"Errore Script: {result.stderr}"
|
||||
except subprocess.TimeoutExpired:
|
||||
return f"Errore esecuzione script: Timeout dopo 90 secondi (script troppo lento)"
|
||||
except Exception as e:
|
||||
return f"Errore esecuzione script: {e}"
|
||||
|
||||
@@ -270,7 +274,7 @@ async def meteo_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> N
|
||||
f"✈️ **Report Meteo - {viaggio_attivo['name']}**\n\n{report_viaggio}",
|
||||
parse_mode="Markdown"
|
||||
)
|
||||
else:
|
||||
else:
|
||||
# Nessun viaggio attivo: invia report per Casa
|
||||
await update.message.reply_text("🔄 Generazione report meteo per Casa...", parse_mode="Markdown")
|
||||
report_casa = call_meteo_script(["--home"])
|
||||
@@ -661,7 +665,7 @@ async def meteo_viaggio_command(update: Update, context: ContextTypes.DEFAULT_TY
|
||||
f"📊 **Report Meteo - {name}**\n\n{report_meteo}",
|
||||
parse_mode="Markdown"
|
||||
)
|
||||
else:
|
||||
else:
|
||||
await update.message.reply_text(
|
||||
f"⚠️ Errore nella generazione del report meteo:\n{report_meteo}",
|
||||
parse_mode="Markdown"
|
||||
@@ -729,11 +733,13 @@ async def meteo_viaggio_command(update: Update, context: ContextTypes.DEFAULT_TY
|
||||
await update.message.reply_text(f"❌ Errore durante l'elaborazione: {str(e)}", parse_mode="Markdown")
|
||||
|
||||
async def scheduled_morning_report(context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||
# LANCIAMO LO SCRIPT ESTERNO PER CASA
|
||||
# Esegui una sola chiamata e invia il report a tutti i chat_id
|
||||
report = call_meteo_script(["--home"])
|
||||
for uid in ALLOWED_IDS:
|
||||
try: await context.bot.send_message(chat_id=uid, text=report, parse_mode="Markdown")
|
||||
except: pass
|
||||
try:
|
||||
await context.bot.send_message(chat_id=uid, text=report, parse_mode="Markdown")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
@restricted
|
||||
async def button_handler(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||
@@ -838,7 +844,7 @@ def main():
|
||||
application.add_handler(CallbackQueryHandler(button_handler))
|
||||
|
||||
job_queue = application.job_queue
|
||||
job_queue.run_daily(scheduled_morning_report, time=datetime.time(hour=8, minute=0, tzinfo=TZINFO), days=(0, 1, 2, 3, 4, 5, 6))
|
||||
job_queue.run_daily(scheduled_morning_report, time=datetime.time(hour=7, minute=15, tzinfo=TZINFO), days=(0, 1, 2, 3, 4, 5, 6))
|
||||
|
||||
application.run_polling()
|
||||
|
||||
|
||||
@@ -138,6 +138,10 @@ def telegram_send_html(message_html: str, chat_ids: Optional[List[str]] = None)
|
||||
message_html: Messaggio HTML da inviare
|
||||
chat_ids: Lista di chat IDs (default: TELEGRAM_CHAT_IDS)
|
||||
"""
|
||||
# Sanitize unsupported HTML tags (Telegram non supporta <br>)
|
||||
if message_html:
|
||||
message_html = re.sub(r"<\s*br\s*/?\s*>", "\n", message_html, flags=re.IGNORECASE)
|
||||
|
||||
token = load_bot_token()
|
||||
if not token:
|
||||
LOGGER.warning("Token Telegram assente. Nessun invio effettuato.")
|
||||
@@ -363,8 +367,8 @@ def main(chat_ids: Optional[List[str]] = None, debug_mode: bool = False):
|
||||
else:
|
||||
LOGGER.warning("Invio debug non riuscito (token mancante o errore Telegram).")
|
||||
else:
|
||||
LOGGER.info("Nessuna allerta nelle zone monitorate. Nessuna notifica inviata.")
|
||||
return
|
||||
LOGGER.info("Nessuna allerta nelle zone monitorate. Nessuna notifica inviata.")
|
||||
return
|
||||
|
||||
sig = build_signature(parsed)
|
||||
state = load_state()
|
||||
|
||||
@@ -476,41 +476,41 @@ def analyze_freeze(chat_ids: Optional[List[str]] = None, debug_mode: bool = Fals
|
||||
LOGGER.info(" Nuova fascia %d: %s - %s", i+1, start.strftime("%d/%m %H:%M"), end.strftime("%d/%m %H:%M"))
|
||||
|
||||
if should_notify:
|
||||
# Costruisci messaggio con dettagli sulle nuove fasce orarie
|
||||
period_details = []
|
||||
if has_new_periods:
|
||||
for start, end in new_periods[:3]: # Max 3 fasce nel messaggio
|
||||
if start.date() == end.date():
|
||||
period_details.append(f"{start.strftime('%d/%m %H:%M')}-{end.strftime('%H:%M')}")
|
||||
else:
|
||||
period_details.append(f"{start.strftime('%d/%m %H:%M')}-{end.strftime('%d/%m %H:%M')}")
|
||||
# Costruisci messaggio con dettagli sulle nuove fasce orarie
|
||||
period_details = []
|
||||
if has_new_periods:
|
||||
for start, end in new_periods[:3]: # Max 3 fasce nel messaggio
|
||||
if start.date() == end.date():
|
||||
period_details.append(f"{start.strftime('%d/%m %H:%M')}-{end.strftime('%H:%M')}")
|
||||
else:
|
||||
period_details.append(f"{start.strftime('%d/%m %H:%M')}-{end.strftime('%d/%m %H:%M')}")
|
||||
|
||||
msg_parts = [
|
||||
"❄️ <b>ALLERTA GELO</b>\n",
|
||||
f"📍 {html.escape(LOCATION_NAME)}\n\n",
|
||||
f"Minima prevista (entro {HOURS_AHEAD}h): <b>{min_temp_val:.1f}°C</b>\n",
|
||||
f"📅 Quando: <b>{html.escape(fmt_dt(min_temp_time))}</b>",
|
||||
]
|
||||
if period_details:
|
||||
msg_parts.append("\n🕒 Fasce orarie: " + ", ".join(period_details))
|
||||
msg_parts.append("\n\n<i>Proteggere piante e tubature esterne.</i>")
|
||||
msg_parts = [
|
||||
"❄️ <b>ALLERTA GELO</b>\n",
|
||||
f"📍 {html.escape(LOCATION_NAME)}\n\n",
|
||||
f"Minima prevista (entro {HOURS_AHEAD}h): <b>{min_temp_val:.1f}°C</b>\n",
|
||||
f"📅 Quando: <b>{html.escape(fmt_dt(min_temp_time))}</b>",
|
||||
]
|
||||
if period_details:
|
||||
msg_parts.append("\n🕒 Fasce orarie: " + ", ".join(period_details))
|
||||
msg_parts.append("\n\n<i>Proteggere piante e tubature esterne.</i>")
|
||||
|
||||
msg = "".join(msg_parts)
|
||||
msg = "".join(msg_parts)
|
||||
ok = telegram_send_html(msg, chat_ids=chat_ids)
|
||||
if ok:
|
||||
LOGGER.info("Allerta gelo inviata. Tmin=%.1f°C at %s, nuove fasce: %d",
|
||||
min_temp_val, min_temp_time.isoformat(), len(new_periods))
|
||||
# Aggiorna le fasce notificate
|
||||
for start, end in new_periods:
|
||||
notified_periods.append({
|
||||
"start": start.isoformat(),
|
||||
"end": end.isoformat(),
|
||||
})
|
||||
LOGGER.info("Allerta gelo inviata. Tmin=%.1f°C at %s, nuove fasce: %d",
|
||||
min_temp_val, min_temp_time.isoformat(), len(new_periods))
|
||||
# Aggiorna le fasce notificate
|
||||
for start, end in new_periods:
|
||||
notified_periods.append({
|
||||
"start": start.isoformat(),
|
||||
"end": end.isoformat(),
|
||||
})
|
||||
else:
|
||||
LOGGER.warning("Allerta gelo NON inviata (token mancante o errore Telegram).")
|
||||
|
||||
else:
|
||||
LOGGER.info("Gelo già notificato (nessuna nuova fascia oraria, peggioramento < 2°C). Tmin=%.1f°C", min_temp_val)
|
||||
LOGGER.info("Gelo già notificato (nessuna nuova fascia oraria, peggioramento < 2°C). Tmin=%.1f°C", min_temp_val)
|
||||
|
||||
state.update({
|
||||
"alert_active": True,
|
||||
|
||||
231
services/telegram-bot/log_monitor.py
Normal file
231
services/telegram-bot/log_monitor.py
Normal file
@@ -0,0 +1,231 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import argparse
|
||||
import datetime
|
||||
import os
|
||||
import re
|
||||
from pathlib import Path
|
||||
from collections import defaultdict, deque
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
|
||||
import requests
|
||||
|
||||
|
||||
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
DEFAULT_PATTERNS = ["*.log", "*_log.txt"]
|
||||
TOKEN_FILE_HOME = os.path.expanduser("~/.telegram_dpc_bot_token")
|
||||
TOKEN_FILE_ETC = "/etc/telegram_dpc_bot_token"
|
||||
|
||||
TS_RE = re.compile(r"(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})")
|
||||
|
||||
CATEGORIES = {
|
||||
"open_meteo_timeout": re.compile(
|
||||
r"timeout|timed out|Read timed out|Gateway Time-out|504", re.IGNORECASE
|
||||
),
|
||||
"ssl_handshake": re.compile(r"handshake", re.IGNORECASE),
|
||||
"permission_error": re.compile(r"PermissionError|permesso negato|Errno 13", re.IGNORECASE),
|
||||
"telegram_error": re.compile(r"Telegram error|Bad Request|chat not found|can't parse entities", re.IGNORECASE),
|
||||
"traceback": re.compile(r"Traceback", re.IGNORECASE),
|
||||
"exception": re.compile(r"\bERROR\b|Exception", re.IGNORECASE),
|
||||
"token_missing": re.compile(r"token missing|Token Telegram assente", re.IGNORECASE),
|
||||
}
|
||||
|
||||
|
||||
def load_text_file(path: str) -> str:
|
||||
try:
|
||||
with open(path, "r", encoding="utf-8") as f:
|
||||
return f.read().strip()
|
||||
except Exception:
|
||||
return ""
|
||||
|
||||
|
||||
def load_bot_token() -> str:
|
||||
tok = os.environ.get("TELEGRAM_BOT_TOKEN", "").strip()
|
||||
if tok:
|
||||
return tok
|
||||
tok = load_text_file(TOKEN_FILE_HOME)
|
||||
if tok:
|
||||
return tok
|
||||
tok = load_text_file(TOKEN_FILE_ETC)
|
||||
return tok.strip() if tok else ""
|
||||
|
||||
|
||||
def send_telegram(text: str, chat_ids: Optional[List[str]]) -> bool:
|
||||
token = load_bot_token()
|
||||
if not token or not chat_ids:
|
||||
return False
|
||||
url = f"https://api.telegram.org/bot{token}/sendMessage"
|
||||
base_payload = {
|
||||
"text": text,
|
||||
"disable_web_page_preview": True,
|
||||
}
|
||||
ok = False
|
||||
with requests.Session() as s:
|
||||
for chat_id in chat_ids:
|
||||
payload = dict(base_payload)
|
||||
payload["chat_id"] = chat_id
|
||||
try:
|
||||
resp = s.post(url, json=payload, timeout=15)
|
||||
if resp.status_code == 200:
|
||||
ok = True
|
||||
except Exception:
|
||||
continue
|
||||
return ok
|
||||
|
||||
|
||||
def tail_lines(path: str, max_lines: int) -> List[str]:
|
||||
dq: deque[str] = deque(maxlen=max_lines)
|
||||
with open(path, "r", encoding="utf-8", errors="ignore") as f:
|
||||
for line in f:
|
||||
dq.append(line.rstrip("\n"))
|
||||
return list(dq)
|
||||
|
||||
|
||||
def parse_ts(line: str) -> Optional[datetime.datetime]:
|
||||
m = TS_RE.search(line)
|
||||
if not m:
|
||||
return None
|
||||
try:
|
||||
return datetime.datetime.strptime(m.group(1), "%Y-%m-%d %H:%M:%S")
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def analyze_logs(files: List[str], since: datetime.datetime, max_lines: int) -> Tuple[Dict, Dict, Dict, Dict]:
|
||||
category_hits = defaultdict(list)
|
||||
per_file_counts = defaultdict(lambda: defaultdict(int))
|
||||
timeout_minutes = defaultdict(int)
|
||||
stale_logs = {} # path -> (last_ts, hours_since_update)
|
||||
|
||||
now = datetime.datetime.now()
|
||||
|
||||
for path in files:
|
||||
if not os.path.isfile(path):
|
||||
continue
|
||||
last_ts = None
|
||||
for line in tail_lines(path, max_lines):
|
||||
ts = parse_ts(line)
|
||||
if ts:
|
||||
last_ts = ts
|
||||
if not last_ts or last_ts < since:
|
||||
continue
|
||||
for cat, regex in CATEGORIES.items():
|
||||
if regex.search(line):
|
||||
category_hits[cat].append((last_ts, path, line))
|
||||
per_file_counts[path][cat] += 1
|
||||
if cat == "open_meteo_timeout":
|
||||
timeout_minutes[last_ts.strftime("%H:%M")] += 1
|
||||
break
|
||||
|
||||
# Verifica se il log è "stale" (non aggiornato da più di 24 ore)
|
||||
if last_ts:
|
||||
hours_since = (now - last_ts).total_seconds() / 3600.0
|
||||
if hours_since > 24:
|
||||
stale_logs[path] = (last_ts, hours_since)
|
||||
else:
|
||||
# Se non ha timestamp, verifica data di modifica del file
|
||||
try:
|
||||
mtime = os.path.getmtime(path)
|
||||
file_mtime = datetime.datetime.fromtimestamp(mtime)
|
||||
hours_since = (now - file_mtime).total_seconds() / 3600.0
|
||||
if hours_since > 24:
|
||||
stale_logs[path] = (file_mtime, hours_since)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return category_hits, per_file_counts, timeout_minutes, stale_logs
|
||||
|
||||
|
||||
def format_report(
|
||||
days: int,
|
||||
files: List[str],
|
||||
category_hits: Dict,
|
||||
per_file_counts: Dict,
|
||||
timeout_minutes: Dict,
|
||||
stale_logs: Dict,
|
||||
) -> str:
|
||||
now = datetime.datetime.now()
|
||||
since = now - datetime.timedelta(days=days)
|
||||
lines = []
|
||||
lines.append(f"🧾 Log Monitor - ultimi {days} giorni")
|
||||
lines.append(f"Intervallo: {since.strftime('%Y-%m-%d %H:%M')} → {now.strftime('%Y-%m-%d %H:%M')}")
|
||||
lines.append(f"File analizzati: {len(files)}")
|
||||
lines.append("")
|
||||
|
||||
# Sezione log non aggiornati
|
||||
if stale_logs:
|
||||
lines.append("⚠️ Log non aggiornati (>24h):")
|
||||
for path, (last_ts, hours_since) in sorted(stale_logs.items(), key=lambda x: x[1][1], reverse=True):
|
||||
short_path = os.path.basename(path)
|
||||
days_ago = hours_since / 24.0
|
||||
if days_ago >= 1:
|
||||
lines.append(f" • {short_path}: {days_ago:.1f} giorni fa ({last_ts.strftime('%Y-%m-%d %H:%M')})")
|
||||
else:
|
||||
lines.append(f" • {short_path}: {hours_since:.1f} ore fa ({last_ts.strftime('%Y-%m-%d %H:%M')})")
|
||||
lines.append("")
|
||||
|
||||
total_issues = sum(len(v) for v in category_hits.values())
|
||||
if total_issues == 0 and not stale_logs:
|
||||
lines.append("✅ Nessun problema rilevato nelle ultime 72 ore.")
|
||||
return "\n".join(lines)
|
||||
|
||||
lines.append(f"Problemi rilevati: {total_issues}")
|
||||
lines.append("")
|
||||
|
||||
for cat, items in sorted(category_hits.items(), key=lambda x: len(x[1]), reverse=True):
|
||||
lines.append(f"- {cat}: {len(items)}")
|
||||
# show up to 3 latest samples
|
||||
for ts, path, msg in sorted(items, key=lambda x: x[0], reverse=True)[:3]:
|
||||
short_path = os.path.basename(path)
|
||||
lines.append(f" • {ts.strftime('%Y-%m-%d %H:%M:%S')} | {short_path} | {msg[:180]}")
|
||||
lines.append("")
|
||||
|
||||
if timeout_minutes:
|
||||
lines.append("Timeout: minuti più frequenti")
|
||||
for minute, count in sorted(timeout_minutes.items(), key=lambda x: x[1], reverse=True)[:6]:
|
||||
lines.append(f" • {minute} -> {count}")
|
||||
lines.append("")
|
||||
|
||||
# Per-file summary (top 6 files)
|
||||
lines.append("File più problematici")
|
||||
file_totals = []
|
||||
for path, cats in per_file_counts.items():
|
||||
file_totals.append((sum(cats.values()), path))
|
||||
for total, path in sorted(file_totals, reverse=True)[:6]:
|
||||
short_path = os.path.basename(path)
|
||||
lines.append(f" • {short_path}: {total}")
|
||||
|
||||
return "\\n".join(lines)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--days", type=int, default=3, help="Numero di giorni da analizzare")
|
||||
parser.add_argument("--max-lines", type=int, default=5000, help="Limite righe per file")
|
||||
parser.add_argument("--chat_id", help="Chat ID Telegram (separati da virgola)")
|
||||
parser.add_argument("--log", action="append", help="Aggiungi un file log specifico")
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.log:
|
||||
files = [p for p in args.log if os.path.exists(p)]
|
||||
else:
|
||||
from pathlib import Path
|
||||
files = []
|
||||
for pat in DEFAULT_PATTERNS:
|
||||
files.extend(sorted([str(p) for p in Path(BASE_DIR).glob(pat)]))
|
||||
files = sorted(set(files))
|
||||
|
||||
since = datetime.datetime.now() - datetime.timedelta(days=args.days)
|
||||
category_hits, per_file_counts, timeout_minutes, stale_logs = analyze_logs(files, since, args.max_lines)
|
||||
report = format_report(args.days, files, category_hits, per_file_counts, timeout_minutes, stale_logs)
|
||||
|
||||
if args.chat_id:
|
||||
chat_ids = [c.strip() for c in args.chat_id.split(",") if c.strip()]
|
||||
send_telegram(report, chat_ids)
|
||||
else:
|
||||
print(report)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -221,7 +221,9 @@ def get_forecast(lat, lon, model=None, is_home=False, timezone=None, retry_after
|
||||
|
||||
# Nota: minutely_15 non è usato in meteo.py (solo per script di allerta)
|
||||
try:
|
||||
r = requests.get(OPEN_METEO_URL, params=params, headers=HTTP_HEADERS, timeout=25)
|
||||
t0 = time.time()
|
||||
# Timeout ridotto a 20s per fallire più velocemente in caso di problemi
|
||||
r = requests.get(OPEN_METEO_URL, params=params, headers=HTTP_HEADERS, timeout=20)
|
||||
if r.status_code != 200:
|
||||
# Dettagli errore più specifici
|
||||
error_details = f"Status {r.status_code}"
|
||||
@@ -238,22 +240,23 @@ def get_forecast(lat, lon, model=None, is_home=False, timezone=None, retry_after
|
||||
logger.error(f"API Error {error_details}")
|
||||
return None, error_details # Restituisce anche i dettagli dell'errore
|
||||
response_data = r.json()
|
||||
logger.info("get_forecast ok model=%s points=5 elapsed=%.2fs", model or "best_match", time.time() - t0)
|
||||
# Open-Meteo per multiple locations (lat/lon separati da virgola) restituisce
|
||||
# direttamente un dict con "hourly", "daily", etc. che contiene liste di valori
|
||||
# per ogni location. Per semplicità, restituiamo il dict così com'è
|
||||
# e lo gestiamo nel codice chiamante
|
||||
return response_data, None
|
||||
except requests.exceptions.Timeout as e:
|
||||
error_details = f"Timeout dopo 25s: {str(e)}"
|
||||
logger.error(f"Request timeout: {error_details}")
|
||||
error_details = f"Timeout dopo 20s: {str(e)}"
|
||||
logger.error("Request timeout: %s elapsed=%.2fs", error_details, time.time() - t0)
|
||||
return None, error_details
|
||||
except requests.exceptions.ConnectionError as e:
|
||||
error_details = f"Errore connessione: {str(e)}"
|
||||
logger.error(f"Connection error: {error_details}")
|
||||
logger.error("Connection error: %s elapsed=%.2fs", error_details, time.time() - t0)
|
||||
return None, error_details
|
||||
except Exception as e:
|
||||
error_details = f"Errore richiesta: {type(e).__name__}: {str(e)}"
|
||||
logger.error(f"Request error: {error_details}")
|
||||
logger.error("Request error: %s elapsed=%.2fs", error_details, time.time() - t0)
|
||||
return None, error_details
|
||||
|
||||
def get_visibility_forecast(lat, lon):
|
||||
@@ -271,16 +274,19 @@ def get_visibility_forecast(lat, lon):
|
||||
"hourly": "visibility"
|
||||
}
|
||||
try:
|
||||
r = requests.get(OPEN_METEO_URL, params=params_ecmwf, headers=HTTP_HEADERS, timeout=15)
|
||||
t0 = time.time()
|
||||
# Timeout ridotto a 12s per fallire più velocemente
|
||||
r = requests.get(OPEN_METEO_URL, params=params_ecmwf, headers=HTTP_HEADERS, timeout=12)
|
||||
if r.status_code == 200:
|
||||
data = r.json()
|
||||
hourly = data.get("hourly", {})
|
||||
vis = hourly.get("visibility", [])
|
||||
# Verifica se ci sono valori validi (non tutti None)
|
||||
if vis and any(v is not None for v in vis):
|
||||
logger.info("get_visibility_forecast ok model=ecmwf_ifs04 elapsed=%.2fs", time.time() - t0)
|
||||
return vis
|
||||
except Exception as e:
|
||||
logger.debug(f"ECMWF IFS visibility request error: {e}")
|
||||
logger.debug("ECMWF IFS visibility request error: %s elapsed=%.2fs", e, time.time() - t0)
|
||||
|
||||
# Fallback: usa best match (senza models) che seleziona automaticamente GFS o ICON-D2
|
||||
params_best = {
|
||||
@@ -291,16 +297,20 @@ def get_visibility_forecast(lat, lon):
|
||||
"hourly": "visibility"
|
||||
}
|
||||
try:
|
||||
r = requests.get(OPEN_METEO_URL, params=params_best, headers=HTTP_HEADERS, timeout=15)
|
||||
t0 = time.time()
|
||||
# Timeout ridotto a 12s per fallire più velocemente
|
||||
r = requests.get(OPEN_METEO_URL, params=params_best, headers=HTTP_HEADERS, timeout=12)
|
||||
if r.status_code == 200:
|
||||
data = r.json()
|
||||
hourly = data.get("hourly", {})
|
||||
logger.info("get_visibility_forecast ok model=best_match elapsed=%.2fs", time.time() - t0)
|
||||
return hourly.get("visibility", [])
|
||||
except Exception as e:
|
||||
logger.error(f"Visibility request error: {e}")
|
||||
logger.error("Visibility request error: %s elapsed=%.2fs", e, time.time() - t0)
|
||||
return None
|
||||
|
||||
def generate_weather_report(lat, lon, location_name, debug_mode=False, cc="IT", timezone=None) -> str:
|
||||
t_total = time.time()
|
||||
# Determina se è Casa
|
||||
is_home = (abs(lat - HOME_LAT) < 0.01 and abs(lon - HOME_LON) < 0.01)
|
||||
|
||||
@@ -596,7 +606,9 @@ def generate_weather_report(lat, lon, location_name, debug_mode=False, cc="IT",
|
||||
if not blocks:
|
||||
return f"❌ Nessun dato da mostrare nelle prossime 48 ore (da {current_hour.strftime('%H:%M')})."
|
||||
|
||||
return f"🌤️ *METEO REPORT*\n📍 {location_name}\n🧠 Fonte: {model_name}\n\n" + "\n\n".join(blocks)
|
||||
report = f"🌤️ *METEO REPORT*\n📍 {location_name}\n🧠 Fonte: {model_name}\n\n" + "\n\n".join(blocks)
|
||||
logger.info("generate_weather_report ok elapsed=%.2fs", time.time() - t_total)
|
||||
return report
|
||||
|
||||
if __name__ == "__main__":
|
||||
args_parser = argparse.ArgumentParser()
|
||||
|
||||
@@ -698,6 +698,10 @@ def find_confirmed_start(
|
||||
def main(chat_ids: Optional[List[str]] = None, debug_mode: bool = False) -> None:
|
||||
LOGGER.info("--- Nowcast 120m alert ---")
|
||||
|
||||
# Carica state e inizializza active_events
|
||||
state = load_state()
|
||||
active_events = state.get("active_events", {})
|
||||
|
||||
# Estendi forecast a 3 giorni per avere 48h di analisi neve completa
|
||||
data_arome = get_forecast(MODEL_AROME, forecast_days=3)
|
||||
if not data_arome:
|
||||
@@ -825,10 +829,10 @@ def main(chat_ids: Optional[List[str]] = None, debug_mode: bool = False) -> None
|
||||
# Calcola picco ICON se disponibile
|
||||
max_g_icon = 0.0
|
||||
if len(gust_icon) >= n:
|
||||
for i in range(n):
|
||||
dt = parse_time_local(times[i])
|
||||
for i in range(n):
|
||||
dt = parse_time_local(times[i])
|
||||
if dt < window_start or dt > window_end:
|
||||
continue
|
||||
continue
|
||||
max_g_icon = max(max_g_icon, val(gust_icon, i))
|
||||
|
||||
# Comparazione
|
||||
@@ -922,7 +926,7 @@ def main(chat_ids: Optional[List[str]] = None, debug_mode: bool = False) -> None
|
||||
alerts.append("ℹ️ <i>Nessuna allerta confermata entro %s minuti.</i>" % WINDOW_MINUTES)
|
||||
sig_parts.append("NO_ALERT")
|
||||
else:
|
||||
LOGGER.info("Nessuna allerta confermata entro %s minuti.", WINDOW_MINUTES)
|
||||
LOGGER.info("Nessuna allerta confermata entro %s minuti.", WINDOW_MINUTES)
|
||||
# Salva state aggiornato (con eventi puliti) anche se non inviamo notifiche
|
||||
state["active_events"] = active_events
|
||||
save_state(state)
|
||||
|
||||
@@ -114,11 +114,36 @@ def setup_logger() -> logging.Logger:
|
||||
logger.setLevel(logging.DEBUG if DEBUG else logging.INFO)
|
||||
logger.handlers.clear()
|
||||
|
||||
fh = RotatingFileHandler(LOG_FILE, maxBytes=500_000, backupCount=3, encoding="utf-8")
|
||||
fh.setLevel(logging.DEBUG)
|
||||
fmt = logging.Formatter("%(asctime)s %(levelname)s %(message)s")
|
||||
fh.setFormatter(fmt)
|
||||
logger.addHandler(fh)
|
||||
parent_dir = os.path.dirname(LOG_FILE)
|
||||
if parent_dir and not os.path.exists(parent_dir):
|
||||
os.makedirs(parent_dir, exist_ok=True)
|
||||
|
||||
try:
|
||||
fh = RotatingFileHandler(LOG_FILE, maxBytes=500_000, backupCount=3, encoding="utf-8")
|
||||
fh.setLevel(logging.DEBUG)
|
||||
fh.setFormatter(fmt)
|
||||
logger.addHandler(fh)
|
||||
except PermissionError:
|
||||
fallback_log = "/tmp/irrigation_advisor.log"
|
||||
try:
|
||||
fh = RotatingFileHandler(fallback_log, maxBytes=500_000, backupCount=3, encoding="utf-8")
|
||||
fh.setLevel(logging.DEBUG)
|
||||
fh.setFormatter(fmt)
|
||||
logger.addHandler(fh)
|
||||
logger.warning("Permesso negato su %s, uso fallback %s", LOG_FILE, fallback_log)
|
||||
except Exception:
|
||||
sh = logging.StreamHandler()
|
||||
sh.setLevel(logging.DEBUG)
|
||||
sh.setFormatter(fmt)
|
||||
logger.addHandler(sh)
|
||||
logger.warning("Permesso negato su %s, fallback su stderr", LOG_FILE)
|
||||
except Exception:
|
||||
sh = logging.StreamHandler()
|
||||
sh.setLevel(logging.DEBUG)
|
||||
sh.setFormatter(fmt)
|
||||
logger.addHandler(sh)
|
||||
logger.warning("Errore logger file %s, fallback su stderr", LOG_FILE)
|
||||
|
||||
if DEBUG:
|
||||
sh = logging.StreamHandler()
|
||||
|
||||
@@ -691,7 +691,7 @@ def main(chat_ids: Optional[List[str]] = None, debug_mode: bool = False) -> None
|
||||
if rain_dur > 0:
|
||||
parts.append(f"🌧️ pioggia forte da ~{html.escape(x['rain_persist_time'] or '—')} (durata ~{rain_dur:.0f}h, totale ~{rain_tot:.1f}mm)")
|
||||
else:
|
||||
parts.append(f"🌧️ pioggia forte da ~{html.escape(x['rain_persist_time'] or '—')}")
|
||||
parts.append(f"🌧️ pioggia forte da ~{html.escape(x['rain_persist_time'] or '—')}")
|
||||
line += " | ".join(parts)
|
||||
msg.append(line)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user