From 312646f5101e959d585cae5262767054a16cedb7 Mon Sep 17 00:00:00 2001 From: minguezsanzjuanjose Date: Sun, 2 Aug 2026 23:26:26 +0200 Subject: [PATCH] superguardado: fixes pi_bot (offset persistente, unit systemd) + voz.py (desconectar chromecast) + lecciones 27-31 --- docs/lecciones_aprendidas_vigilancia.md | 15 +++++++++++++++ vigilancia/pi_bot.py | 22 ++++++++++++++++++++-- vigilancia/voz.py | 12 ++++++++++++ 3 files changed, 47 insertions(+), 2 deletions(-) diff --git a/docs/lecciones_aprendidas_vigilancia.md b/docs/lecciones_aprendidas_vigilancia.md index ba135d4..56f3c16 100644 --- a/docs/lecciones_aprendidas_vigilancia.md +++ b/docs/lecciones_aprendidas_vigilancia.md @@ -71,3 +71,18 @@ 25. **Bot de Telegram en la Pi**: Responde a `estado`, `temperatura`, `tv`, `alarma`, `youtube`, `panorama`. Usa `requests` para getUpdates (long polling) y `sendMessage`/`sendPhoto` para responder. 26. **Alerta de temperatura**: Monitor en background (cada 5 min) que envía alerta si supera el umbral (80°C default). + +### Bugfixes (02 agosto tarde) + +27. **El bot corría una copia vieja (unit systemd)**: `pi-bot.service` apuntaba a `/home/pi/minipc/pi_bot.py` (raíz, sin `limpiar`), mientras el código nuevo estaba en `/home/pi/minipc/vigilancia/pi_bot.py`. El comando `limpiar` no funcionaba porque el bot que corría NO lo tenía. Al desplegar archivos nuevos hay que **revisar el unit de systemd** (`systemctl cat pi-bot` / `tv-api`). Fix: actualizar `WorkingDirectory` y `ExecStart` a `vigilancia/` + `daemon-reload` + restart. + +28. **Telegram reprocesa en ráfaga al perder el offset**: El bot guardaba `offset` solo en memoria. Al reiniciar (offset=None) `getUpdates` devuelve **todos los updates pendientes no confirmados** y los re-ejecuta de golpe (disparó `panorama`, `tv`, `youtube`, `limpieza` repetidos). Fix: **persistir el offset** en un archivo (`bot.offset`) con `cargar_offset()`/`guardar_offset()`, guardando tras cada update. Al desplegar, capturar el último update con `getUpdates?offset=-1` para no reprocesar. + +29. **pychromecast en proceso long-running deja zeroconf roto**: El comando `tv` generaba el audio (`anuncio.mp3`) pero la TV no reproducía: `AssertionError: Zeroconf instance loop must be running, was it already stopped?` y `Failed to connect to service ... retrying`. Causa: cada `get_chromecasts()` deja al objeto `Chromecast` con threads de reconexión que usan un zeroconf ya parado tras `stop_discovery()`. Funciona en proceso limpio pero falla en tv-api long-running. Fix: **desconectar cada cast** (`cast.disconnect()`) antes de `stop_discovery()` en los 3 puntos (`anuncio_en_teles`, `youtube_en_teles`, `anuncio_simple`). + +### Acceso a la Pi + +30. **SSH por password con paramiko**: Desde el portátil la llave no está configurada para `pi`, pero `user pi / password raspberry` funciona via paramiko a `192.168.50.187` (y `.251`). El sudo necesita `echo 'raspberry' | sudo -S `. Para escribir archivos con `sudo tee` sin romper el quoting, usar **base64**: `echo | base64 -d > archivo`. + +31. **stdout de tv-api en systemd está buffered**: Los `print()` de tv_api no salen en `journalctl` hasta flush. Para ver el resultado del casting, usar un proceso limpio o logear a archivo. + diff --git a/vigilancia/pi_bot.py b/vigilancia/pi_bot.py index da427f0..36fb041 100644 --- a/vigilancia/pi_bot.py +++ b/vigilancia/pi_bot.py @@ -9,6 +9,7 @@ import requests BASE = os.path.dirname(os.path.abspath(__file__)) LOG = os.path.join(BASE, "bot.log") +OFFSET_FILE = os.path.join(BASE, "bot.offset") TV_API = "http://127.0.0.1:8600/tv/mensaje" @@ -174,13 +175,29 @@ def monitor_temperatura(): f"(límite {ALERTA_TEMP}°C)\n{msg}", CHAT_ID) +def cargar_offset(): + try: + with open(OFFSET_FILE) as f: + return int(f.read().strip()) + except Exception: + return None + + +def guardar_offset(offset): + try: + with open(OFFSET_FILE, "w") as f: + f.write(str(offset)) + except Exception: + pass + + def main(): if not TOKEN: print("Falta TELEGRAM_TOKEN en .env") return threading.Thread(target=monitor_temperatura, daemon=True).start() - offset = None - log("Bot iniciado") + offset = cargar_offset() + log("Bot iniciado" + (f" (offset {offset})" if offset else "")) print("Bot iniciado. Esperando mensajes...") while True: try: @@ -193,6 +210,7 @@ def main(): ).json() for upd in r.get("result", []): offset = upd["update_id"] + 1 + guardar_offset(offset) log(f"UPDATE {upd.get('update_id')}") msg = upd.get("message") or upd.get("channel_post") if not msg: diff --git a/vigilancia/voz.py b/vigilancia/voz.py index 5f77ab6..87dfb61 100644 --- a/vigilancia/voz.py +++ b/vigilancia/voz.py @@ -231,6 +231,15 @@ def _url_silencio(): return f"http://{_ip_lan()}:{config.GOOGLE_TV_PUERTO}/{os.path.basename(ruta)}" +def _desconectar(chromecasts): + for cc in chromecasts: + try: + if cc.is_active and cc.is_connected: + cc.disconnect() + except Exception: + pass + + def _reproducir_y_esperar(c, url, content_type, titulo, desc): c.media_controller.play_media(url, content_type, title=titulo) print(f"[Voz] {desc}") @@ -307,6 +316,7 @@ def anuncio_en_teles(texto): except Exception as e: print(f"[Voz] Error en {c.cast_info.friendly_name}: {e}") finally: + _desconectar(chromecasts) pychromecast.discovery.stop_discovery(browser) except Exception as e: print(f"[Voz] Error de casting: {e}") @@ -358,6 +368,7 @@ def youtube_en_teles(url): except Exception as e: print(f"[Voz] Error en {c.cast_info.friendly_name}: {e}") finally: + _desconectar(chromecasts) pychromecast.discovery.stop_discovery(browser) except Exception as e: print(f"[Voz] Error YouTube: {e}") @@ -414,6 +425,7 @@ def anuncio_simple(texto): except Exception as e: print(f"[Voz] Error en {c.cast_info.friendly_name}: {e}") finally: + _desconectar(chromecasts) pychromecast.discovery.stop_discovery(browser) except Exception as e: print(f"[Voz] Error de casting: {e}")