20-sep: [fix] Sistema completo apagado/encendido remoto - shutdown listener + HA actualizado
This commit is contained in:
171
docs/SISTEMA_APAGADO_ENCENDIDO_REMOTO.md
Normal file
171
docs/SISTEMA_APAGADO_ENCENDIDO_REMOTO.md
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
# Sistema de Apagado/Encendido Remoto del PC (20-sep-2026)
|
||||||
|
|
||||||
|
## Arquitectura del sistema
|
||||||
|
|
||||||
|
```
|
||||||
|
TELEGRAM (iPad/Móvil)
|
||||||
|
↓ /apagar o /encender
|
||||||
|
BOT TELEGRAM (PC local - telegram_bot_pc.py)
|
||||||
|
↓ POST a webhook
|
||||||
|
HOME ASSISTANT (VPS 185.187.169.109)
|
||||||
|
↓
|
||||||
|
├─ APAGAR: POST http://192.168.1.239:5555/shutdown → PC ejecuta shutdown
|
||||||
|
│ → Espera 30s
|
||||||
|
│ → Alexa apaga luces (barras + cuartillo)
|
||||||
|
│ → Alexa apaga regleta (corta corriente)
|
||||||
|
│
|
||||||
|
└─ ENCENDER: Alexa enciende regleta
|
||||||
|
→ Espera 60s (PC arranca físicamente)
|
||||||
|
→ Wake-on-LAN a 192.168.1.239
|
||||||
|
```
|
||||||
|
|
||||||
|
## Componentes
|
||||||
|
|
||||||
|
### 1. Bot Telegram (PC local)
|
||||||
|
**Archivo:** `INFRAESTRUCTURA/scripts/telegram_bot_pc.py`
|
||||||
|
**Función:** Interfaz de usuario desde Telegram
|
||||||
|
**Características:**
|
||||||
|
- Pide confirmación antes de apagar
|
||||||
|
- Watchdog automático tras apagar (30s + ping)
|
||||||
|
- Watchdog automático tras encender (90s + 3 reintentos ping)
|
||||||
|
- Filtro de autorización por USER_ID
|
||||||
|
|
||||||
|
**Arrancar:**
|
||||||
|
```powershell
|
||||||
|
cd C:\Users\juanm\Documents\Github\INFRAESTRUCTURA\scripts
|
||||||
|
python telegram_bot_pc.py
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Shutdown Listener (PC local)
|
||||||
|
**Archivo:** `INFRAESTRUCTURA/scripts/shutdown_listener.py`
|
||||||
|
**Puerto:** 5555
|
||||||
|
**Función:** Recibe comandos HTTP de Home Assistant para ejecutar shutdown local
|
||||||
|
**Seguridad:** Token de autenticación (SHUTDOWN_TOKEN en .env)
|
||||||
|
|
||||||
|
**Arrancar:**
|
||||||
|
```powershell
|
||||||
|
cd C:\Users\juanm\Documents\Github\INFRAESTRUCTURA\scripts
|
||||||
|
python shutdown_listener.py
|
||||||
|
```
|
||||||
|
|
||||||
|
**Health check:**
|
||||||
|
```powershell
|
||||||
|
Invoke-WebRequest -Uri "http://127.0.0.1:5555/health"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Ejecutar shutdown (requiere token):**
|
||||||
|
```powershell
|
||||||
|
Invoke-WebRequest -Uri "http://127.0.0.1:5555/shutdown" `
|
||||||
|
-Method POST `
|
||||||
|
-Headers @{ "Authorization" = "Bearer $env:SHUTDOWN_TOKEN" }
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Home Assistant (VPS)
|
||||||
|
**Ubicación:** `root@185.187.169.109:/opt/homeassistant`
|
||||||
|
**Función:** Orquestador central (recibe webhooks, ejecuta shutdown, controla luces/regleta vía Alexa)
|
||||||
|
|
||||||
|
**Archivos de configuración:**
|
||||||
|
- `/opt/homeassistant/config/automations.yaml` — Webhooks `/apagar-pc-juanjo` y `/encender-pc-juanjo`
|
||||||
|
- `/opt/homeassistant/config/scripts.yaml` — Scripts `apagar_pc_completo` y `encender_pc_completo`
|
||||||
|
- `/opt/homeassistant/config/configuration.yaml` — REST command `shutdown_pc_local`
|
||||||
|
|
||||||
|
**Reiniciar:**
|
||||||
|
```bash
|
||||||
|
cd /opt/homeassistant && docker compose restart homeassistant
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Dispositivos Alexa
|
||||||
|
- **Echo Dot de Nahid:** Ejecuta comandos de voz para luces y regleta
|
||||||
|
- **Luces:** barras, cuartillo
|
||||||
|
- **Regleta:** Enchufe inteligente que alimenta el PC
|
||||||
|
|
||||||
|
## Flujo de apagado completo
|
||||||
|
|
||||||
|
1. Usuario en Telegram: `/apagar` → Botón "🔴 Apagar PC"
|
||||||
|
2. Bot pide confirmación: "✅ SÍ, apagar" | "❌ Cancelar"
|
||||||
|
3. Si confirma → Bot envía POST a `http://185.187.169.109:8123/api/webhook/apagar-pc-juanjo`
|
||||||
|
4. Home Assistant ejecuta `script.apagar_pc_completo`:
|
||||||
|
- **4.1** POST a `http://192.168.1.239:5555/shutdown` (con token)
|
||||||
|
- **4.2** PC ejecuta `shutdown /s /t 10`
|
||||||
|
- **4.3** Espera 30s
|
||||||
|
- **4.4** Alexa apaga "barras"
|
||||||
|
- **4.5** Alexa apaga "cuartillo"
|
||||||
|
- **4.6** Alexa apaga "regleta" (corta corriente al PC)
|
||||||
|
5. Bot espera 30s y verifica con ping:
|
||||||
|
- Si responde → "⚠️ APAGADO FALLIDO"
|
||||||
|
- Si no responde → "✅ PC APAGADO CONFIRMADO"
|
||||||
|
|
||||||
|
## Flujo de encendido completo
|
||||||
|
|
||||||
|
1. Usuario en Telegram: `/encender` → Botón "🟢 Encender PC"
|
||||||
|
2. Bot envía POST a `http://185.187.169.109:8123/api/webhook/encender-pc-juanjo`
|
||||||
|
3. Home Assistant ejecuta `script.encender_pc_completo`:
|
||||||
|
- **3.1** Alexa enciende "regleta" (da corriente al PC)
|
||||||
|
- **3.2** Espera 60s (PC arranca físicamente)
|
||||||
|
- **3.3** Envía Wake-on-LAN a `192.168.1.239` (MAC: `70:85:c2:6b:3a:03`)
|
||||||
|
4. Bot espera 90s + hasta 3 reintentos (15s cada uno) verificando con ping:
|
||||||
|
- Si responde → "✅ PC ENCENDIDO CORRECTAMENTE"
|
||||||
|
- Si no responde → "❌ ENCENDIDO FALLIDO"
|
||||||
|
5. PC arranca → Windows inicia → **Autologon** → Escritorio listo → Watchers arrancan
|
||||||
|
|
||||||
|
## Variables de entorno necesarias (.env)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Bot Telegram
|
||||||
|
TELEGRAM_BOT_PC_TOKEN=<token del bot @juanjo_pc_control_bot>
|
||||||
|
TELEGRAM_USER_ID=325737724
|
||||||
|
|
||||||
|
# Shutdown Listener
|
||||||
|
SHUTDOWN_TOKEN=H7iLGJixPp3J3pthCmnR2Ibq0tvERD5THyr_foAeN_8
|
||||||
|
```
|
||||||
|
|
||||||
|
## Tareas programadas recomendadas
|
||||||
|
|
||||||
|
### Shutdown Listener (arrancar al iniciar Windows)
|
||||||
|
```xml
|
||||||
|
Nombre: Shutdown Listener
|
||||||
|
Descripción: Listener HTTP para apagado remoto del PC
|
||||||
|
Acción: python C:\Users\juanm\Documents\Github\INFRAESTRUCTURA\scripts\shutdown_listener.py
|
||||||
|
Desencadenador: Al iniciar sesión
|
||||||
|
Usuario: juanm
|
||||||
|
```
|
||||||
|
|
||||||
|
### Bot Telegram (arrancar al iniciar Windows)
|
||||||
|
```xml
|
||||||
|
Nombre: Bot Telegram Control PC
|
||||||
|
Descripción: Bot de Telegram para control remoto del PC
|
||||||
|
Acción: python C:\Users\juanm\Documents\Github\INFRAESTRUCTURA\scripts\telegram_bot_pc.py
|
||||||
|
Desencadenador: Al iniciar sesión
|
||||||
|
Usuario: juanm
|
||||||
|
```
|
||||||
|
|
||||||
|
Script de instalación: `INFRAESTRUCTURA/scripts/instalar_watchers.ps1` (añadir estas tareas)
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### El apagado no funciona
|
||||||
|
1. Verificar que el shutdown listener está corriendo: `http://127.0.0.1:5555/health`
|
||||||
|
2. Verificar token en `.env` (SHUTDOWN_TOKEN)
|
||||||
|
3. Revisar logs de Home Assistant: `ssh root@185.187.169.109 "docker logs homeassistant -f"`
|
||||||
|
4. Probar shutdown manual: `Invoke-WebRequest -Uri "http://127.0.0.1:5555/shutdown" -Method POST -Headers @{ "Authorization" = "Bearer TOKEN" }`
|
||||||
|
|
||||||
|
### El encendido no funciona
|
||||||
|
1. Verificar que la regleta enciende (luz física)
|
||||||
|
2. Verificar Wake-on-LAN habilitado en BIOS
|
||||||
|
3. Verificar MAC del PC correcta en HA: `70:85:c2:6b:3a:03`
|
||||||
|
4. Probar WoL manual: `wakeonlan 70:85:c2:6b:3a:03`
|
||||||
|
|
||||||
|
### El bot no responde
|
||||||
|
1. Verificar que está corriendo: `Get-CimInstance Win32_Process | Where-Object { $_.CommandLine -match "telegram_bot_pc" }`
|
||||||
|
2. Revisar logs: `C:\Users\juanm\Documents\Github\INFRAESTRUCTURA\scripts\telegram_bot_pc.log`
|
||||||
|
3. Verificar token: `TELEGRAM_BOT_PC_TOKEN` en `.env`
|
||||||
|
4. Verificar USER_ID autorizado: `TELEGRAM_USER_ID=325737724`
|
||||||
|
|
||||||
|
## Seguridad
|
||||||
|
|
||||||
|
- ✅ Bot solo responde a USER_ID autorizado (325737724)
|
||||||
|
- ✅ Shutdown listener requiere token de autenticación
|
||||||
|
- ✅ Listener solo acepta conexiones locales (bind a 0.0.0.0 pero firewall protege)
|
||||||
|
- ✅ Home Assistant en VPS con IP fija conocida
|
||||||
|
- ⚠️ Token en `.env` en texto plano (mitigar con permisos de archivo)
|
||||||
|
- ⚠️ Autologon guarda contraseña en registro (mitigar con BitLocker + bloqueo automático)
|
||||||
52
scripts/shutdown_listener.py
Normal file
52
scripts/shutdown_listener.py
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
"""
|
||||||
|
Listener HTTP local que ejecuta shutdown cuando recibe POST /shutdown
|
||||||
|
Home Assistant puede llamar a http://192.168.1.239:5555/shutdown para apagar el PC
|
||||||
|
|
||||||
|
Uso:
|
||||||
|
python shutdown_listener.py
|
||||||
|
|
||||||
|
Debe correr como servicio/tarea programada al iniciar Windows.
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
from flask import Flask, request, jsonify
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
|
# Cargar .env
|
||||||
|
dotenv_path = r"C:\Users\juanm\Documents\GitHub\biblioteca_negocio_prolongo\.env"
|
||||||
|
load_dotenv(dotenv_path)
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
|
||||||
|
# Token de seguridad para autorizar shutdowns
|
||||||
|
SHUTDOWN_TOKEN = os.getenv("SHUTDOWN_TOKEN", "cambiar-este-token")
|
||||||
|
|
||||||
|
@app.route('/shutdown', methods=['POST'])
|
||||||
|
def shutdown():
|
||||||
|
"""Ejecuta shutdown del PC si el token es correcto"""
|
||||||
|
# Verificar token
|
||||||
|
token = request.headers.get('Authorization', '').replace('Bearer ', '')
|
||||||
|
if token != SHUTDOWN_TOKEN:
|
||||||
|
print(f"[ACCESO DENEGADO] Token incorrecto: {token[:10]}...")
|
||||||
|
return jsonify({"error": "Unauthorized"}), 401
|
||||||
|
|
||||||
|
print("[SHUTDOWN] Apagando PC en 10 segundos...")
|
||||||
|
|
||||||
|
# Ejecutar shutdown
|
||||||
|
os.system("shutdown /s /t 10")
|
||||||
|
|
||||||
|
return jsonify({"status": "ok", "message": "Shutdown iniciado (10s)"}), 200
|
||||||
|
|
||||||
|
@app.route('/health', methods=['GET'])
|
||||||
|
def health():
|
||||||
|
"""Health check"""
|
||||||
|
return jsonify({"status": "ok", "service": "shutdown-listener"}), 200
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
print("=" * 60)
|
||||||
|
print("Shutdown Listener - Esperando comandos en puerto 5555")
|
||||||
|
print(f"Token configurado: {SHUTDOWN_TOKEN[:10]}..." if SHUTDOWN_TOKEN != "cambiar-este-token" else "Token: [NO CONFIGURADO]")
|
||||||
|
print("Endpoint: POST http://192.168.1.239:5555/shutdown")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
# Escuchar en todas las interfaces (0.0.0.0) para que Home Assistant pueda acceder
|
||||||
|
app.run(host='0.0.0.0', port=5555, debug=False)
|
||||||
0
scripts/telegram_bot_pc.log
Normal file
0
scripts/telegram_bot_pc.log
Normal file
@@ -30,12 +30,30 @@ HA_WEBHOOK_APAGAR = "http://185.187.169.109:8123/api/webhook/apagar-pc-juanjo"
|
|||||||
HA_WEBHOOK_ENCENDER = "http://185.187.169.109:8123/api/webhook/encender-pc-juanjo"
|
HA_WEBHOOK_ENCENDER = "http://185.187.169.109:8123/api/webhook/encender-pc-juanjo"
|
||||||
PC_IP = "192.168.1.239" # IP del PC sobremesa
|
PC_IP = "192.168.1.239" # IP del PC sobremesa
|
||||||
|
|
||||||
|
def user_autorizado(update: Update) -> bool:
|
||||||
|
"""Verifica si el usuario está autorizado"""
|
||||||
|
user_id = str(update.effective_user.id) if update.effective_user else None
|
||||||
|
autorizado = user_id == TELEGRAM_USER_ID
|
||||||
|
if not autorizado:
|
||||||
|
print(f"[ACCESO DENEGADO] Usuario no autorizado: {user_id} (esperado: {TELEGRAM_USER_ID})")
|
||||||
|
return autorizado
|
||||||
|
|
||||||
async def start(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
async def start(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||||
"""Comando /start"""
|
"""Comando /start"""
|
||||||
|
if not user_autorizado(update):
|
||||||
|
await update.message.reply_text("❌ Acceso denegado. Bot de uso personal.")
|
||||||
|
return
|
||||||
|
print(f"[START] Usuario {update.effective_user.id} autenticado correctamente")
|
||||||
await menu(update, context)
|
await menu(update, context)
|
||||||
|
|
||||||
async def menu(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
async def menu(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||||
"""Muestra el menú principal"""
|
"""Muestra el menú principal"""
|
||||||
|
if not user_autorizado(update):
|
||||||
|
if update.callback_query:
|
||||||
|
await update.callback_query.answer("❌ Acceso denegado")
|
||||||
|
return
|
||||||
|
|
||||||
|
print(f"[MENU] Mostrando menú a usuario {update.effective_user.id}")
|
||||||
keyboard = [
|
keyboard = [
|
||||||
[
|
[
|
||||||
InlineKeyboardButton("🔴 Apagar PC", callback_data='apagar'),
|
InlineKeyboardButton("🔴 Apagar PC", callback_data='apagar'),
|
||||||
@@ -67,6 +85,10 @@ async def menu(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|||||||
|
|
||||||
async def apagar_pc(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
async def apagar_pc(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||||
"""Apaga el PC con confirmación"""
|
"""Apaga el PC con confirmación"""
|
||||||
|
if not user_autorizado(update):
|
||||||
|
return
|
||||||
|
|
||||||
|
print(f"[APAGAR] Solicitando confirmación")
|
||||||
query = update.callback_query if update.callback_query else None
|
query = update.callback_query if update.callback_query else None
|
||||||
|
|
||||||
# CONFIRMAR antes de apagar
|
# CONFIRMAR antes de apagar
|
||||||
@@ -155,6 +177,10 @@ async def apagar_pc_confirmar(update: Update, context: ContextTypes.DEFAULT_TYPE
|
|||||||
|
|
||||||
async def encender_pc(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
async def encender_pc(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||||
"""Enciende el PC y verifica que arrancó"""
|
"""Enciende el PC y verifica que arrancó"""
|
||||||
|
if not user_autorizado(update):
|
||||||
|
return
|
||||||
|
|
||||||
|
print(f"[ENCENDER] Iniciando proceso de encendido")
|
||||||
query = update.callback_query if update.callback_query else None
|
query = update.callback_query if update.callback_query else None
|
||||||
|
|
||||||
if query:
|
if query:
|
||||||
@@ -228,6 +254,10 @@ async def encender_pc(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|||||||
|
|
||||||
async def estado_pc(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
async def estado_pc(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||||
"""Verifica si el PC está encendido"""
|
"""Verifica si el PC está encendido"""
|
||||||
|
if not user_autorizado(update):
|
||||||
|
return
|
||||||
|
|
||||||
|
print(f"[ESTADO] Verificando estado del PC")
|
||||||
query = update.callback_query if update.callback_query else None
|
query = update.callback_query if update.callback_query else None
|
||||||
|
|
||||||
if query:
|
if query:
|
||||||
@@ -300,7 +330,15 @@ def main():
|
|||||||
print("ERROR: Falta TELEGRAM_BOT_PC_TOKEN en .env")
|
print("ERROR: Falta TELEGRAM_BOT_PC_TOKEN en .env")
|
||||||
return
|
return
|
||||||
|
|
||||||
print("Iniciando bot de Telegram para control de PC...")
|
if not TELEGRAM_USER_ID:
|
||||||
|
print("ERROR: Falta TELEGRAM_USER_ID en .env")
|
||||||
|
return
|
||||||
|
|
||||||
|
print("=" * 50)
|
||||||
|
print("Iniciando bot de Telegram para control de PC")
|
||||||
|
print(f"Usuario autorizado: {TELEGRAM_USER_ID}")
|
||||||
|
print(f"IP del PC: {PC_IP}")
|
||||||
|
print("=" * 50)
|
||||||
|
|
||||||
app = Application.builder().token(TELEGRAM_BOT_TOKEN).build()
|
app = Application.builder().token(TELEGRAM_BOT_TOKEN).build()
|
||||||
|
|
||||||
|
|||||||
0
scripts/telegram_bot_pc_error.log
Normal file
0
scripts/telegram_bot_pc_error.log
Normal file
Reference in New Issue
Block a user