diff --git a/services/telegram-bot/bot.py b/services/telegram-bot/bot.py
index 7d0ef18..a2a0acd 100644
--- a/services/telegram-bot/bot.py
+++ b/services/telegram-bot/bot.py
@@ -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()
diff --git a/services/telegram-bot/civil_protection.py b/services/telegram-bot/civil_protection.py
index 187e5dc..272866d 100644
--- a/services/telegram-bot/civil_protection.py
+++ b/services/telegram-bot/civil_protection.py
@@ -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
)
+ 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()
diff --git a/services/telegram-bot/freeze_alert.py b/services/telegram-bot/freeze_alert.py
index cabd820..1ee32c6 100644
--- a/services/telegram-bot/freeze_alert.py
+++ b/services/telegram-bot/freeze_alert.py
@@ -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')}")
-
- msg_parts = [
- "❄️ ALLERTA GELO\n",
- f"📍 {html.escape(LOCATION_NAME)}\n\n",
- f"Minima prevista (entro {HOURS_AHEAD}h): {min_temp_val:.1f}°C\n",
- f"📅 Quando: {html.escape(fmt_dt(min_temp_time))}",
- ]
- if period_details:
- msg_parts.append("\n🕒 Fasce orarie: " + ", ".join(period_details))
- msg_parts.append("\n\nProteggere piante e tubature esterne.")
-
- msg = "".join(msg_parts)
+ # 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 = [
+ "❄️ ALLERTA GELO\n",
+ f"📍 {html.escape(LOCATION_NAME)}\n\n",
+ f"Minima prevista (entro {HOURS_AHEAD}h): {min_temp_val:.1f}°C\n",
+ f"📅 Quando: {html.escape(fmt_dt(min_temp_time))}",
+ ]
+ if period_details:
+ msg_parts.append("\n🕒 Fasce orarie: " + ", ".join(period_details))
+ msg_parts.append("\n\nProteggere piante e tubature esterne.")
+
+ 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,
diff --git a/services/telegram-bot/log_monitor.py b/services/telegram-bot/log_monitor.py
new file mode 100644
index 0000000..74cf910
--- /dev/null
+++ b/services/telegram-bot/log_monitor.py
@@ -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()
diff --git a/services/telegram-bot/meteo.py b/services/telegram-bot/meteo.py
index 97b4daf..36e7e1e 100644
--- a/services/telegram-bot/meteo.py
+++ b/services/telegram-bot/meteo.py
@@ -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()
diff --git a/services/telegram-bot/nowcast_120m_alert.py b/services/telegram-bot/nowcast_120m_alert.py
index 29a00f4..be31a09 100644
--- a/services/telegram-bot/nowcast_120m_alert.py
+++ b/services/telegram-bot/nowcast_120m_alert.py
@@ -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("ℹ️ Nessuna allerta confermata entro %s minuti." % 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)
diff --git a/services/telegram-bot/smart_irrigation_advisor.py b/services/telegram-bot/smart_irrigation_advisor.py
index e2d43d5..b88ef11 100755
--- a/services/telegram-bot/smart_irrigation_advisor.py
+++ b/services/telegram-bot/smart_irrigation_advisor.py
@@ -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()
diff --git a/services/telegram-bot/student_alert.py b/services/telegram-bot/student_alert.py
index d73792d..693a96e 100644
--- a/services/telegram-bot/student_alert.py
+++ b/services/telegram-bot/student_alert.py
@@ -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)