From c15800fb105bdf84b4b0eb9b3a865ef9c1f7de72 Mon Sep 17 00:00:00 2001 From: minguezsanzjuanjose Date: Sun, 2 Aug 2026 14:45:54 +0200 Subject: [PATCH] 02-ago: [proyecto] vigilancia - comando tv_mensaje.py (mensaje parametrizado) + API remota tv_api.py (puerto 8600, via Tailscale) + fix espera FINISHED + AGENTS.md comando tv y Regla 23 --- AGENTS.md | 21 ++++++++++++++++++ vigilancia/.env.example | 3 +++ vigilancia/tv_api.py | 46 ++++++++++++++++++++++++++++++++++++++++ vigilancia/tv_mensaje.py | 21 ++++++++++++++++++ vigilancia/voz.py | 13 +++++------- 5 files changed, 96 insertions(+), 8 deletions(-) create mode 100644 vigilancia/tv_api.py create mode 100644 vigilancia/tv_mensaje.py diff --git a/AGENTS.md b/AGENTS.md index 97d7314..f476804 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -90,6 +90,7 @@ Start-Process powershell -WindowStyle Hidden -ArgumentList '-Command', 'python C | `ejecuta alarma Twilio` | Modo interactivo: pedir credenciales (o usar .env), 2 telefonos, estrategia (secuencial/paralelo/solo 1/solo 2), ejecutar llamada Twilio con AMD y reportar resultado. Ejecutar con: `python alarma_twilio/cli.py` en cualquier equipo con el repo clonado. | | `!alarma +numero` desde Telegram | Enviar `!alarma +34657866417` al bot @? (token en .env) desde el iPad/móvil → el VPS lo escucha vía Bot API y ejecuta la alarma. También funciona desde Saved Messages via watcher local. | | `sync-vaults` | Sincronizar vaults Obsidian: Remotely Save sync → copiar archivos nuevos/actualizados del portátil a sobremesa → forzar OneDrive sync. Ver [[07-Conocimiento/protocolo-sync-vaults-obsidian.md]]. | +| `tv "mensaje"` | Enviar un mensaje urgente a la tele Google (enciende → 15s → sirena emergencia → mensaje → apagado ADB): `python vigilancia/tv_mensaje.py "mensaje"`. Desde fuera de la WiFi: `curl http://:8600/tv/mensaje?texto=...` | --- @@ -443,4 +444,24 @@ Ejemplo: `22-jul: [conocimiento] Plan diario Jefe Desarrollo + protocolo guardad --- +## Regla 23: Mensajes urgentes a la tele (AUTOMATICO) + +El sistema `vigilancia/` permite enviar mensajes de voz a la tele Google de casa. Al usar el comando `tv`, el asistente DEBE: + +1. Ejecutar `python vigilancia/tv_mensaje.py ""` (en el PC que esté en la LAN de casa) +2. La secuencia es: **encender la tele en silencio → esperar GOOGLE_TV_ESPERA_SG (15s) → sirena de emergencia → mensaje → apagar por ADB** +3. Regla 5: antes de enviar, pedir la clave de doble factor (es un mensaje a la casa) + +**Mensajes urgentes registrados por el usuario:** +- Intimidación por límite: *"Por favor, os avisamos de que no podéis cruzar este límite. Tenéis que ir a vuestro cuarto."* (`VOZ_MENSAJE_ALERTA`) + +**Desde fuera de la WiFi** (con Tailscale en el dispositivo): el PC de casa corre `vigilancia/tv_api.py` (puerto 8600), y se llama: +``` +curl "http://:8600/tv/mensaje?texto=" +``` + +**Configuración relevante** (`.env` de vigilancia): `GOOGLE_TV_VOLUMEN` (30), `GOOGLE_TV_ESPERA_SG` (15), `GOOGLE_TV_PITIDO_SG` (6), `GOOGLE_TV_APAGAR_VIA_ADB` (1), `ADB_TV_IP` (192.168.50.16:5555), `VOZ_EDGE`/`VOZ_PITCH`/`VOZ_RATE` (voz grave). + +--- + *Creado: 26 Junio 2026 — Actualizado: 30 Jul 2026 (Regla 22: Categorización automática + comando superguardado)* diff --git a/vigilancia/.env.example b/vigilancia/.env.example index 99e2cf2..de88ea4 100644 --- a/vigilancia/.env.example +++ b/vigilancia/.env.example @@ -57,6 +57,9 @@ VOZ_RATE=-10% GOOGLE_TV_PUERTO=8500 ADB_TV_IP=192.168.50.16:5555 GOOGLE_TV_APAGAR_VIA_ADB=1 + +# API remota para enviar mensajes a la tele (tv_api.py). Acceso desde fuera via Tailscale. +TV_API_PUERTO=8600 VOZ_MENSAJE_ALERTA=Por favor, os avisamos de que no podéis cruzar este límite. Tenéis que ir a vuestro cuarto. VOZ_MENSAJE_CONOCIDO={nombres}, ¿qué estás haciendo? No entres ahí. diff --git a/vigilancia/tv_api.py b/vigilancia/tv_api.py new file mode 100644 index 0000000..ffe74ad --- /dev/null +++ b/vigilancia/tv_api.py @@ -0,0 +1,46 @@ +import json +import os +import threading +from http.server import BaseHTTPRequestHandler, HTTPServer +from urllib.parse import parse_qs, urlparse + +sys_dir = os.path.dirname(os.path.abspath(__file__)) +import sys +sys.path.insert(0, sys_dir) + +import config +from voz import anuncio_en_teles + + +class Handler(BaseHTTPRequestHandler): + def do_GET(self): + q = parse_qs(urlparse(self.path).query) + texto = (q.get("texto") or q.get("msg") or [""])[0] + if not texto: + self._resp(400, {"error": "falta parametro texto"}) + return + threading.Thread(target=anuncio_en_teles, args=(texto,), daemon=True).start() + print(f"[TV-API] Mensaje lanzado: {texto[:80]}") + self._resp(200, {"ok": True, "mensaje": texto}) + + def _resp(self, code, obj): + body = json.dumps(obj).encode("utf-8") + self.send_response(code) + self.send_header("Content-Type", "application/json; charset=utf-8") + self.send_header("Content-Length", str(len(body))) + self.end_headers() + self.wfile.write(body) + + def log_message(self, *a): + pass + + +def main(): + puerto = int(os.getenv("TV_API_PUERTO", "8600")) + print(f"TV API escuchando en :{puerto}") + print(f"Uso: http://:{puerto}/tv/mensaje?texto=Mensaje") + HTTPServer(("0.0.0.0", puerto), Handler).serve_forever() + + +if __name__ == "__main__": + main() diff --git a/vigilancia/tv_mensaje.py b/vigilancia/tv_mensaje.py new file mode 100644 index 0000000..4867bae --- /dev/null +++ b/vigilancia/tv_mensaje.py @@ -0,0 +1,21 @@ +import os +import sys + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + +import config +from voz import anuncio_en_teles + + +def main(): + texto = " ".join(sys.argv[1:]) + if not texto: + print("Uso: python tv_mensaje.py \"mensaje a la tele\"") + print("Ejemplo: python tv_mensaje.py \"A cenar, la comida está lista\"") + return + print(f"Enviando a la tele: {texto}") + anuncio_en_teles(texto) + + +if __name__ == "__main__": + main() diff --git a/vigilancia/voz.py b/vigilancia/voz.py index 8ffcdd4..150caa5 100644 --- a/vigilancia/voz.py +++ b/vigilancia/voz.py @@ -109,8 +109,8 @@ def _esperar_reproduccion(cast, timeout=15): return False -def _esperar_fin(cast, timeout=90): - """Espera a que termine (IDLE) despues de haber empezado a reproducir.""" +def _esperar_fin(cast, timeout=120): + """Espera a que el audio termine de verdad (IDLE + idle_reason FINISHED).""" mc = cast.media_controller t0 = time.time() visto_playing = False @@ -119,15 +119,12 @@ def _esperar_fin(cast, timeout=90): st = mc.status if st.player_state == "PLAYING": visto_playing = True - if st.player_state == "IDLE": - if st.idle_reason == "FINISHED": - return True - if visto_playing: - return True + if st.player_state == "IDLE" and st.idle_reason == "FINISHED": + return True except Exception: pass time.sleep(1) - return False + return visto_playing def _generar_pitido():