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())