05-sep: [infra] configurar Raspberry Pi y documentar flota
This commit is contained in:
@@ -83,6 +83,7 @@ Start-Process powershell -WindowStyle Hidden -ArgumentList '-Command', 'python C
|
||||
| `consumo` | Abrir https://opencode.ai/auth, esperar captura del panel Go y analizarla con image-vision |
|
||||
| `memoria` | Protocolo completo: git pull, actualizar ficheros de memoria (MEMORIA_ABSOLUTA, ESTADO_DIARIO, AGENTS, DOCUMENTACION, README), Engram local + cloud, Obsidian sync, **trello-sync**, git push |
|
||||
| `recordatorios` | Consultar API recordatorios en VPS: GET /todo (calendarios + recordatorios webhook) |
|
||||
| `raspberry` | Diagnosticar y mantener las Raspberry Pi del laboratorio |
|
||||
| `idd-procesar` o `procesar IDD` | Fetch manual de mensajes pendientes del canal IDD Telegram via Bot API, guardarlos en Google Drive y clasificarlos localmente |
|
||||
| `trello-sync` | Sincronizar pendientes con Trello: local→Trello (crea/actualiza cards) + Trello→local (elimina tareas archivadas en Trello de la base de conocimiento) |
|
||||
| `pelicula <nombre>` | Buscar pelicula en el videoclub y descargar a NAS |
|
||||
|
||||
80
docs/raspberry_pi.md
Normal file
80
docs/raspberry_pi.md
Normal file
@@ -0,0 +1,80 @@
|
||||
# Raspberry Pi del laboratorio
|
||||
|
||||
## Objetivo
|
||||
|
||||
Centralizar la configuracion, el acceso y el mantenimiento de una flota de Raspberry Pi. La primera unidad es `minipc`; el mismo procedimiento debe servir para las cinco unidades previstas.
|
||||
|
||||
## Inventario actual
|
||||
|
||||
| Campo | Valor |
|
||||
|---|---|
|
||||
| Hostname | `minipc` |
|
||||
| Sistema | Raspbian GNU/Linux 13 (trixie) |
|
||||
| Ethernet | `192.168.50.187` |
|
||||
| Wi-Fi | `192.168.50.251` |
|
||||
| Usuario operativo | `pi` |
|
||||
| Codigo | `/home/pi/minipc/` |
|
||||
| Servicio bot | `pi-bot.service` |
|
||||
| Servicio tunel | `autossh-tunel.service` |
|
||||
| Zona horaria | `Europe/Madrid` |
|
||||
| Teclado | Espanol (`es`) |
|
||||
|
||||
No se documentan contrasenas, tokens ni claves privadas en este fichero.
|
||||
|
||||
## Comando unico de diagnostico
|
||||
|
||||
En la Raspberry:
|
||||
|
||||
```bash
|
||||
bash /home/pi/minipc/vigilancia/raspberry_info.sh
|
||||
```
|
||||
|
||||
El comando muestra identidad, red, teclado, locale, hora, servicios y recursos sin revelar secretos.
|
||||
|
||||
Desde el PC, pasando por el VPS:
|
||||
|
||||
```powershell
|
||||
ssh -i C:\Users\juanm\Documents\GitHub\contabo root@185.187.169.109 "ssh -i /root/.ssh/pi_deploy -p 2222 pi@127.0.0.1 'bash /home/pi/minipc/vigilancia/raspberry_info.sh'"
|
||||
```
|
||||
|
||||
## Configuracion aplicada en minipc
|
||||
|
||||
```bash
|
||||
sudo raspi-config nonint do_configure_keyboard es
|
||||
sudo localectl set-x11-keymap es pc105
|
||||
sudo locale-gen es_ES.UTF-8
|
||||
sudo localectl set-locale LANG=es_ES.UTF-8
|
||||
sudo timedatectl set-timezone Europe/Madrid
|
||||
sudo timedatectl set-ntp true
|
||||
```
|
||||
|
||||
El cambio de idioma puede requerir cerrar la sesion y volver a entrar. El teclado de consola queda definido en `/etc/default/keyboard` con `XKBLAYOUT="es"`.
|
||||
|
||||
## Conectividad
|
||||
|
||||
- Ethernet es la ruta preferida y usa la IP `192.168.50.187`.
|
||||
- Wi-Fi queda disponible como respaldo en `192.168.50.251`.
|
||||
- El tunel inverso publica SSH de la Pi en el VPS como `127.0.0.1:2222`.
|
||||
- El acceso remoto usa la clave privada del VPS configurada para el tunel; nunca se copia ni documenta aqui.
|
||||
- Si la IP cambia, consultar `hostname -I` en la Pi y actualizar el inventario.
|
||||
|
||||
## Despliegue
|
||||
|
||||
La Pi no tiene Git operativo para este flujo. El despliegue se hace por el VPS y el tunel inverso:
|
||||
|
||||
1. Copiar el fichero al VPS.
|
||||
2. Copiarlo por SSH a `pi@127.0.0.1:2222`.
|
||||
3. Validar con `python3 -m py_compile` si es Python.
|
||||
4. Reiniciar el servicio correspondiente.
|
||||
5. Verificar `systemctl is-active` y los logs.
|
||||
|
||||
## Escalado a cinco Raspberry
|
||||
|
||||
Cada unidad debe tener un registro con hostname, MAC, IP Ethernet, IP Wi-Fi, servicios, funcion y ubicacion. Se recomienda:
|
||||
|
||||
- Reservas DHCP por MAC para evitar cambios de IP.
|
||||
- Ethernet como red principal.
|
||||
- Un hostname unico por unidad: `minipc-01`, `minipc-02`, etc.
|
||||
- Un servicio `autossh-tunel` por unidad con un puerto de tunel distinto.
|
||||
- Una clave SSH de despliegue independiente por unidad.
|
||||
- Un script de diagnostico identico en todas las unidades.
|
||||
35
documentacion/bitacora-dashboard-telegram-2026-09-05.md
Normal file
35
documentacion/bitacora-dashboard-telegram-2026-09-05.md
Normal file
@@ -0,0 +1,35 @@
|
||||
# Bitacora: Dashboard de descargas en Telegram
|
||||
|
||||
**Fecha:** 2026-09-05 · **Equipo:** sobremesa · **Agentes:** OpenCode
|
||||
|
||||
---
|
||||
|
||||
## Registro en tiempo real
|
||||
|
||||
### [14:35] Diseño inicial
|
||||
- **Que:** Crear una vista de descargas dentro de Telegram.
|
||||
- **Como:** Reutilizar la consulta existente a Download Station y editar un unico mensaje con botones inline.
|
||||
- **Resultado:** OK
|
||||
- **Decisiones:** No se crea una web externa ni una Mini App en esta primera version.
|
||||
- **Gotchas/Lecciones:** El bot usa directamente la Bot API mediante `requests` y actualmente solo procesa mensajes normales.
|
||||
|
||||
### [14:42] Implementacion
|
||||
- **Que:** Añadir panel editable, botones inline, callbacks y refresco automatico.
|
||||
- **Como:** `pi_bot.py` mantiene un mensaje por chat y lo actualiza cada 20 segundos; `nas_dl.py` genera la barra Unicode usando el porcentaje de piezas descargadas.
|
||||
- **Resultado:** OK
|
||||
- **Decisiones:** `descargas` y `descargar` sin argumento abren el panel; `descargar <enlace>` mantiene la funcion de añadir tareas.
|
||||
- **Gotchas/Lecciones:** Telegram necesita recibir y responder `callback_query` para que los botones funcionen correctamente.
|
||||
|
||||
### [14:47] Verificacion local
|
||||
- **Que:** Comprobar sintaxis, barra de progreso y callbacks sin llamar a Telegram.
|
||||
- **Como:** `py_compile`, prueba de extremos de `progress_bar` y callbacks simulados.
|
||||
- **Resultado:** OK
|
||||
- **Decisiones:** No se enviaron mensajes reales ni se modificaron tareas del NAS durante la prueba.
|
||||
- **Gotchas/Lecciones:** La Pi no acepta conexiones SSH desde este equipo en `192.168.50.187:22`; el despliegue queda pendiente.
|
||||
|
||||
### [15:43] Configuracion Raspberry Pi y flota
|
||||
- **Que:** Recuperar el acceso por clave, configurar teclado/locale espanoles, zona horaria y NTP, y centralizar el mantenimiento de Raspberry Pi.
|
||||
- **Como:** Se uso el salto PC -> VPS -> tunel inverso -> Pi; se aplico `raspi-config` para teclado, `localectl` para locale/X11 y `timedatectl` para hora.
|
||||
- **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`).
|
||||
@@ -149,6 +149,13 @@ def human_size(n):
|
||||
return f"{n:.1f}PB"
|
||||
|
||||
|
||||
def progress_bar(percent, width=12):
|
||||
"""Genera una barra de progreso compacta para mensajes de Telegram."""
|
||||
percent = max(0.0, min(100.0, float(percent or 0)))
|
||||
filled = int(round(percent * width / 100))
|
||||
return "█" * filled + "░" * (width - filled)
|
||||
|
||||
|
||||
def listar_descargas(hablar_solo_activas=True, session=None):
|
||||
"""Devuelve (lineas_texto, activas). Lista las tareas de Download Station
|
||||
con estado, porcentaje de progreso, tamaño descargado y velocidad.
|
||||
@@ -181,7 +188,7 @@ def listar_descargas(hablar_solo_activas=True, session=None):
|
||||
size = transfer.get("size_downloaded") or 0
|
||||
speed = transfer.get("speed_download") or 0
|
||||
if estado == "downloading":
|
||||
activas.append(f"▶️ <b>{pct:.1f}%</b> {human_size(size)} ⬇️{speed // 1024} KB/s\n"
|
||||
activas.append(f"▶️ <b>{progress_bar(pct)} {pct:.1f}%</b> {human_size(size)} ⬇️{speed // 1024} KB/s\n"
|
||||
f" {titulo}")
|
||||
else:
|
||||
resto.append((estado, pct, size, titulo))
|
||||
|
||||
@@ -5,6 +5,7 @@ import sys
|
||||
import threading
|
||||
import time
|
||||
import urllib.parse
|
||||
import json
|
||||
|
||||
import requests
|
||||
|
||||
@@ -13,6 +14,7 @@ ultima_titulos = {} # chat_id -> {'query':..., 'results':[...]}
|
||||
ultimos_torrents = {} # chat_id -> {'titulo':..., 'torrents':[...]}
|
||||
flujo_genero = {} # chat_id -> 'esperando_tipo' | 'esperando_genero' | None
|
||||
flujo_genero_tipo = {} # chat_id -> 'movie' | 'series' (tipo ya elegido)
|
||||
panel_descargas = {} # chat_id -> estado del panel editable
|
||||
|
||||
BASE = os.path.dirname(os.path.abspath(__file__))
|
||||
LOG = os.path.join(BASE, "bot.log")
|
||||
@@ -100,6 +102,148 @@ def enviar(texto, chat_id):
|
||||
return ultimo_id
|
||||
|
||||
|
||||
def teclado_descargas():
|
||||
return {
|
||||
"inline_keyboard": [
|
||||
[
|
||||
{"text": "🔄 Actualizar", "callback_data": "dl:refresh"},
|
||||
{"text": "▶️ Activas", "callback_data": "dl:active"},
|
||||
],
|
||||
[
|
||||
{"text": "📁 Todas", "callback_data": "dl:all"},
|
||||
{"text": "✖️ Cerrar", "callback_data": "dl:close"},
|
||||
],
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
def enviar_panel_descargas(texto, chat_id):
|
||||
try:
|
||||
r = requests.post(
|
||||
f"https://api.telegram.org/bot{TOKEN}/sendMessage",
|
||||
data={
|
||||
"chat_id": chat_id,
|
||||
"text": texto,
|
||||
"parse_mode": "HTML",
|
||||
"reply_markup": json.dumps(teclado_descargas()),
|
||||
},
|
||||
timeout=10,
|
||||
)
|
||||
d = r.json()
|
||||
if d.get("ok"):
|
||||
return d["result"].get("message_id")
|
||||
log(f"Telegram panel send error: {d.get('description', d)}")
|
||||
except Exception as e:
|
||||
log(f"Telegram panel send exception: {e}")
|
||||
return None
|
||||
|
||||
|
||||
def editar_panel_descargas(chat_id, message_id, texto):
|
||||
try:
|
||||
r = requests.post(
|
||||
f"https://api.telegram.org/bot{TOKEN}/editMessageText",
|
||||
data={
|
||||
"chat_id": chat_id,
|
||||
"message_id": message_id,
|
||||
"text": texto,
|
||||
"parse_mode": "HTML",
|
||||
"reply_markup": json.dumps(teclado_descargas()),
|
||||
},
|
||||
timeout=10,
|
||||
)
|
||||
d = r.json()
|
||||
if not d.get("ok") and "not modified" not in d.get("description", "").lower():
|
||||
log(f"Telegram panel edit error: {d.get('description', d)}")
|
||||
return d.get("ok", False)
|
||||
except Exception as e:
|
||||
log(f"Telegram panel edit exception: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def responder_callback(callback_id):
|
||||
try:
|
||||
requests.post(
|
||||
f"https://api.telegram.org/bot{TOKEN}/answerCallbackQuery",
|
||||
data={"callback_query_id": callback_id},
|
||||
timeout=10,
|
||||
)
|
||||
except Exception as e:
|
||||
log(f"Telegram callback error: {e}")
|
||||
|
||||
|
||||
def texto_panel_descargas(modo="active"):
|
||||
import nas_dl
|
||||
lineas, activas = nas_dl.listar_descargas(
|
||||
hablar_solo_activas=(modo == "active")
|
||||
)
|
||||
encabezado = "📥 <b>DESCARGAS DEL NAS</b>"
|
||||
if modo == "active":
|
||||
encabezado += " · ACTIVAS"
|
||||
texto = encabezado + "\n\n" + "\n".join(lineas)
|
||||
texto += f"\n\n🕒 Actualizado: <code>{time.strftime('%H:%M:%S')}</code>"
|
||||
return texto, activas
|
||||
|
||||
|
||||
def abrir_panel_descargas(chat_id):
|
||||
anterior = panel_descargas.pop(chat_id, None)
|
||||
if anterior:
|
||||
anterior["stop"].set()
|
||||
try:
|
||||
texto, _activas = texto_panel_descargas()
|
||||
message_id = enviar_panel_descargas(texto, chat_id)
|
||||
if not message_id:
|
||||
enviar("❌ No se pudo abrir el panel de descargas.", chat_id)
|
||||
return
|
||||
estado_panel = {
|
||||
"message_id": message_id,
|
||||
"modo": "active",
|
||||
"stop": threading.Event(),
|
||||
}
|
||||
panel_descargas[chat_id] = estado_panel
|
||||
threading.Thread(
|
||||
target=monitor_panel_descargas,
|
||||
args=(chat_id, estado_panel),
|
||||
daemon=True,
|
||||
).start()
|
||||
except Exception as e:
|
||||
log(f"panel descargas error: {e}")
|
||||
enviar(f"❌ Error en el panel de descargas: {e}", chat_id)
|
||||
|
||||
|
||||
def monitor_panel_descargas(chat_id, estado_panel):
|
||||
while not estado_panel["stop"].wait(20):
|
||||
try:
|
||||
texto, activas = texto_panel_descargas(estado_panel["modo"])
|
||||
editar_panel_descargas(chat_id, estado_panel["message_id"], texto)
|
||||
if not activas:
|
||||
break
|
||||
except Exception as e:
|
||||
log(f"monitor panel descargas error: {e}")
|
||||
|
||||
|
||||
def procesar_callback(callback):
|
||||
callback_id = callback.get("id")
|
||||
mensaje = callback.get("message") or {}
|
||||
chat_id = (mensaje.get("chat") or {}).get("id")
|
||||
accion = (callback.get("data") or "").split(":", 1)[-1]
|
||||
responder_callback(callback_id)
|
||||
estado_panel = panel_descargas.get(chat_id)
|
||||
if not estado_panel:
|
||||
return
|
||||
if accion == "close":
|
||||
estado_panel["stop"].set()
|
||||
panel_descargas.pop(chat_id, None)
|
||||
borrar(chat_id, estado_panel["message_id"])
|
||||
return
|
||||
if accion in ("active", "all"):
|
||||
estado_panel["modo"] = "active" if accion == "active" else "all"
|
||||
try:
|
||||
texto, _activas = texto_panel_descargas(estado_panel["modo"])
|
||||
editar_panel_descargas(chat_id, estado_panel["message_id"], texto)
|
||||
except Exception as e:
|
||||
log(f"callback panel descargas error: {e}")
|
||||
|
||||
|
||||
def borrar(chat_id, message_id):
|
||||
try:
|
||||
requests.post(
|
||||
@@ -213,6 +357,8 @@ def procesar(texto, chat_id):
|
||||
else:
|
||||
ok = mandar_youtube(url)
|
||||
enviar("🎬 YouTube lanzado en la TV ✅" if ok else "❌ No se pudo lanzar YouTube", chat_id)
|
||||
elif t in ("descargas", "/descargas") or (comando == "descargar" and not argumento):
|
||||
abrir_panel_descargas(chat_id)
|
||||
elif comando == "descargar":
|
||||
try:
|
||||
import nas_dl
|
||||
@@ -220,11 +366,6 @@ def procesar(texto, chat_id):
|
||||
ok = nas_dl.add_torrent(argumento)
|
||||
enviar("📥 Añadido a Download Station del NAS ✅" if ok
|
||||
else "❌ No se pudo añadir al NAS. Revisa <code>bot.log</code>.", chat_id)
|
||||
else:
|
||||
# Incluye también pendientes, pausadas y terminadas para que
|
||||
# el usuario pueda ver el estado real de cada torrent.
|
||||
lineas, _act = nas_dl.listar_descargas(hablar_solo_activas=False)
|
||||
enviar("\n".join(lineas), chat_id)
|
||||
except Exception as e:
|
||||
log(f"descargar error: {e}")
|
||||
enviar(f"❌ Error en Download Station: {e}", chat_id)
|
||||
@@ -269,8 +410,8 @@ def procesar(texto, chat_id):
|
||||
"<b>7.</b> <code>novedades</code> — últimas novedades del catálogo\n"
|
||||
" ✏️ Ej: <code>/novedades</code>\n\n"
|
||||
"━━━ 🖥️ NAS / DESCARGAS ━━━\n"
|
||||
"<b>8.</b> <code>descargar</code> — torrents descargándose y su progreso\n"
|
||||
" ✏️ Ej: <code>/descargar</code>\n"
|
||||
"<b>8.</b> <code>descargas</code> — panel con progreso y botones\n"
|
||||
" ✏️ Ej: <code>/descargas</code> o <code>/descargar</code>\n"
|
||||
"<b>9.</b> <code>descargar <enlace></code> — añade a Download Station\n"
|
||||
" ✏️ Ej: <code>/descargar magnet:?xt=urn:btih:...</code>\n\n"
|
||||
"━━━ 📺 TV ━━━\n"
|
||||
@@ -646,6 +787,9 @@ def main():
|
||||
offset = upd["update_id"] + 1
|
||||
guardar_offset(offset)
|
||||
log(f"UPDATE {upd.get('update_id')}")
|
||||
if upd.get("callback_query"):
|
||||
procesar_callback(upd["callback_query"])
|
||||
continue
|
||||
msg = upd.get("message") or upd.get("channel_post")
|
||||
if not msg:
|
||||
continue
|
||||
|
||||
36
vigilancia/raspberry_info.sh
Normal file
36
vigilancia/raspberry_info.sh
Normal file
@@ -0,0 +1,36 @@
|
||||
#!/bin/bash
|
||||
# Diagnostico seguro de una Raspberry Pi del laboratorio.
|
||||
# No muestra credenciales, tokens ni contenido de ficheros .env.
|
||||
set -u
|
||||
|
||||
echo "== IDENTIDAD =="
|
||||
hostname
|
||||
cat /etc/os-release | sed -n 's/^\(PRETTY_NAME\)=/\1=/p'
|
||||
|
||||
echo "== RED =="
|
||||
hostname -I
|
||||
ip -br addr
|
||||
ip route
|
||||
|
||||
echo "== TECLADO Y LOCALE =="
|
||||
localectl status 2>&1
|
||||
if [ -f /etc/default/keyboard ]; then
|
||||
sed -n '/^XKBMODEL=/p;/^XKBLAYOUT=/p;/^XKBVARIANT=/p;/^XKBOPTIONS=/p' /etc/default/keyboard
|
||||
fi
|
||||
|
||||
echo "== HORA =="
|
||||
timedatectl status 2>&1
|
||||
|
||||
echo "== SERVICIOS =="
|
||||
for service in ssh pi-bot autossh-tunel tv-api; do
|
||||
printf '%-16s' "$service"
|
||||
systemctl is-active "$service" 2>/dev/null || true
|
||||
done
|
||||
|
||||
echo "== RECURSOS =="
|
||||
uptime
|
||||
free -h
|
||||
df -h / | tail -n 1
|
||||
if command -v vcgencmd >/dev/null 2>&1; then
|
||||
vcgencmd measure_temp
|
||||
fi
|
||||
Reference in New Issue
Block a user