From d897e662d322e22ba2f06c9fb63112dbb7f749f7 Mon Sep 17 00:00:00 2001 From: juanjo Date: Sat, 5 Sep 2026 16:47:41 +0200 Subject: [PATCH] 05-sep: [tapo] controlar termo desde Telegram --- docs/raspberry_pi.md | 20 +++++ .../bitacora-dashboard-telegram-2026-09-05.md | 7 ++ vigilancia/.env.example | 5 ++ vigilancia/pi_bot.py | 18 ++++- vigilancia/tapo_control.py | 76 +++++++++++++++++++ 5 files changed, 125 insertions(+), 1 deletion(-) create mode 100644 vigilancia/tapo_control.py diff --git a/docs/raspberry_pi.md b/docs/raspberry_pi.md index f62a1ee..665aec0 100644 --- a/docs/raspberry_pi.md +++ b/docs/raspberry_pi.md @@ -93,3 +93,23 @@ Estas mejoras quedan apuntadas para la Raspberry actual, sin implementarlas ahor - Despliegue y actualizacion remota del codigo. - Capturas de camaras y control de dispositivos conectados. - Comando `/flota` preparado para cuando existan varias Raspberry. + +## Primer dispositivo Tapo + +El dispositivo `termo` es un Tapo Smart Wi-Fi Plug P100. El bot dispone de estos comandos directos: + +```text +/termo on +/termo off +/termo estado +``` + +La Raspberry necesita estas variables en `/home/pi/minipc/vigilancia/.env`, introducidas localmente y sin publicarlas: + +```text +TAPO_USERNAME=cuenta-de-tapo +TAPO_PASSWORD=contrasena-de-tapo +TAPO_TERMO_IP=192.168.50.6 +``` + +El control usa `python-kasa` directamente desde la Raspberry y no depende de Alexa. diff --git a/documentacion/bitacora-dashboard-telegram-2026-09-05.md b/documentacion/bitacora-dashboard-telegram-2026-09-05.md index 2487a38..4ecaf05 100644 --- a/documentacion/bitacora-dashboard-telegram-2026-09-05.md +++ b/documentacion/bitacora-dashboard-telegram-2026-09-05.md @@ -33,3 +33,10 @@ - **Resultado:** OK - **Decisiones:** Ethernet queda como ruta preferida, Wi-Fi como respaldo; se crea `raspberry_info.sh` como comando unico de diagnostico sin secretos. - **Gotchas/Lecciones:** El servicio correcto es `autossh-tunel.service`; la Pi tiene Raspbian 13 y dos IPs activas (`192.168.50.187` y `192.168.50.251`). + +### [16:20] Inicio de control directo Tapo +- **Que:** Preparar el primer dispositivo Telegram -> Raspberry -> Tapo sin Alexa. +- **Como:** Se instalo `python-kasa`, se descubrio el enchufe `termo` por MAC y se encontro en `192.168.50.6`. +- **Resultado:** Implementacion desplegada; pendiente de credenciales Tapo locales. +- **Decisiones:** Comandos permitidos: `/termo on`, `/termo off` y `/termo estado`. +- **Gotchas/Lecciones:** La cuenta Tapo y su contrasena no se guardan en el repositorio; deben introducirse directamente en el `.env` de la Pi. diff --git a/vigilancia/.env.example b/vigilancia/.env.example index 9095fa0..7643793 100644 --- a/vigilancia/.env.example +++ b/vigilancia/.env.example @@ -8,6 +8,11 @@ CAM_2_RTSP=rtsp://usuario:password@192.168.1.51:554/stream2 TELEGRAM_TOKEN=123456789:AAExampleTokenAqui TELEGRAM_CHAT_ID=987654321 +# Tapo directo desde la Raspberry (no compartir la contrasena) +TAPO_USERNAME= +TAPO_PASSWORD= +TAPO_TERMO_IP= + # Franja horaria activa (formato 24h) HORA_INICIO=08 HORA_FIN=17 diff --git a/vigilancia/pi_bot.py b/vigilancia/pi_bot.py index c4b998d..37bf824 100644 --- a/vigilancia/pi_bot.py +++ b/vigilancia/pi_bot.py @@ -369,6 +369,20 @@ def procesar(texto, chat_id): except Exception as e: log(f"descargar error: {e}") enviar(f"❌ Error en Download Station: {e}", chat_id) + elif comando == "termo": + accion = argumento.lower().strip() + acciones = {"on": "on", "encender": "on", "off": "off", + "apagar": "off", "estado": "status", "status": "status"} + if accion not in acciones: + enviar("Uso: /termo on, /termo off o /termo estado", chat_id) + else: + try: + from tapo_control import controlar_termo + resultado = controlar_termo(acciones[accion]) + enviar(f"🔌 {resultado}", chat_id) + except Exception as e: + log(f"termo error: {e}") + enviar(f"❌ Error controlando el termo: {e}", chat_id) elif t in ("pelis", "/pelis"): procesar_pelis(chat_id) elif t in ("series", "/series"): @@ -413,7 +427,9 @@ def procesar(texto, chat_id): "8. descargas — panel con progreso y botones\n" " ✏️ Ej: /descargas o /descargar\n" "9. descargar <enlace> — añade a Download Station\n" - " ✏️ Ej: /descargar magnet:?xt=urn:btih:...\n\n" + " ✏️ Ej: /descargar magnet:?xt=urn:btih:...\n" + "10. termo on|off|estado — controla el enchufe Tapo\n" + " ✏️ Ej: /termo estado\n\n" "━━━ 📺 TV ━━━\n" "10. tv <mensaje> — mensaje rápido a la tele\n" " ✏️ Ej: /tv la cena está lista\n" diff --git a/vigilancia/tapo_control.py b/vigilancia/tapo_control.py new file mode 100644 index 0000000..53b7d18 --- /dev/null +++ b/vigilancia/tapo_control.py @@ -0,0 +1,76 @@ +"""Control directo de dispositivos Tapo desde la Raspberry Pi.""" + +import asyncio +import os + +from kasa import Discover + + +def _load_env(): + values = {} + path = os.path.join(os.path.dirname(os.path.abspath(__file__)), ".env") + try: + with open(path) as stream: + for line in stream: + line = line.strip() + if line and not line.startswith("#") and "=" in line: + key, value = line.split("=", 1) + values[key.strip()] = value.strip() + except OSError: + pass + return values + + +FILE_ENV = _load_env() + + +def _env(name): + return os.getenv(name, FILE_ENV.get(name, "")).strip() + + +async def _device(): + host = _env("TAPO_TERMO_IP") + username = _env("TAPO_USERNAME") + password = _env("TAPO_PASSWORD") + if not host or not username or not password: + raise RuntimeError( + "Falta configurar TAPO_TERMO_IP, TAPO_USERNAME y TAPO_PASSWORD en .env" + ) + return await Discover.discover_single( + host, username=username, password=password, timeout=8 + ) + + +async def _run(action): + device = await _device() + await device.update() + if action == "on": + await device.turn_on() + elif action == "off": + await device.turn_off() + elif action != "status": + raise ValueError("Accion no soportada") + await device.update() + state = "encendido" if device.is_on else "apagado" + return f"termo: {state}" + + +def controlar_termo(action): + """Ejecuta una accion Tapo y devuelve un mensaje seguro para Telegram.""" + try: + return asyncio.run(_run(action)) + except Exception as exc: + message = str(exc) + if "TAPO_" in message: + return f"Error de configuracion Tapo: {message}" + return f"No se pudo controlar el termo: {message[:180]}" + + +async def _discover(): + devices = await Discover.discover(timeout=5) + for device in devices.values(): + print(f"{type(device).__name__} host={device.host!r} mac={device.mac}") + + +if __name__ == "__main__": + asyncio.run(_discover())