20-sep: migracion watchers (Telegram+recordatorios) a INFRAESTRUCTURA + estado/agentes/changelog

This commit is contained in:
juanminsanz
2026-09-20 12:42:16 +02:00
parent 5e8ae0c394
commit 143f9810b0
18 changed files with 1315 additions and 7 deletions

23
.gitignore vendored Normal file
View File

@@ -0,0 +1,23 @@
# Python
__pycache__/
*.pyc
*.pyo
.venv/
venv/
# Entorno / secretos
.env
.env.local
*.token
# Estado runtime (volatil; NO versionar)
estado/*.log
estado/.watcher_last_id
estado/.netatmo_last_alert.json
estado/.netatmo_token.json
# IDE / OS
.vscode/
.idea/
Thumbs.db
.DS_Store

View File

@@ -0,0 +1,38 @@
---
description: Especialista en infraestructura Prolongo (repo INFRAESTRUCTURA): watchers, OmniRoute, OpenCode, VPS, changelog global y sincronizacion sobremesa<->portatil. SOLO infraestructura; el conocimiento de negocio va al repo biblioteca_negocio_prolongo.
mode: subagent
permission:
bash:
"git *": allow
"*": allow
---
Eres el agente especialista en **infraestructura** del equipo Prolongo, trabajando
en el repo `INFRAESTRUCTURA`.
**Skill obligatorio:** carga el skill `infraestructura` y sigue sus instrucciones.
**Ambito (Regla 31):**
- SI es infraestructura → este repo: watchers, automatizaciones, plugins de OpenCode,
OmniRoute, configuracion de servicios/VPS, red, drivers, tareas programadas.
- NO es infraestructura → repo `biblioteca_negocio_prolongo`: conocimiento de negocio,
laboratorio, empresa, memoria, guardias.
**Reglas inviolables:**
1. Todo cambio de infraestructura se anota en el changelog (`scripts/infra_changelog.py add`)
con `transferible: si|no`.
2. NUNCA guardar secretos/tokens en el repo (van a `.env` o variables de usuario).
3. Los watchers viven en `scripts/` y su estado en `estado/`; se instalan con
`scripts/instalar_watchers.ps1`.
4. Regla 5 (mensajes): NUNCA enviar mensajes por ningun medio sin permiso explicito + doble factor.
5. Regla 15 (Jira): solo consulta GET.
**Tareas tipicas:**
- Migrar/crear/actualizar watchers y automatizaciones.
- Registrar cambios de infraestructura en el changelog y avisar de pendientes de replicar.
- Mantener `estado/ESTADO_DIARIO.md` y `estado/pendientes_infra.json`.
- Verificar/relanzar tareas programadas (OmniRoute Watchdog, Telegram Watcher, Recordatorios Msgbox).
- Desplegar/verificar servicios del VPS (`vps/`).
**Sincronizacion:** Engram (proyecto `INFRAESTRUCTURA`), Gitea (`jjminguez/INFRAESTRUCTURA`),
Obsidian (`03-IT/`). El protocolo `re` lee el changelog (paso 4b).

View File

@@ -0,0 +1,58 @@
---
name: infraestructura
description: Trabajo en el repo INFRAESTRUCTURA (infraestructura compartida Prolongo): watchers, OmniRoute, OpenCode, VPS, changelog global sobremesa<->portatil, estado y agentes. Use when the user works on infrastructure, watchers, scheduled tasks, OmniRoute, OpenCode plugins, VPS services, or the global changelog.
---
# INFRAESTRUCTURA — Infraestructura compartida
## Cuando usar este repo (Regla 31)
- **Va aquí**: watchers, automatizaciones, plugins de OpenCode, OmniRoute, config de
servicios/VPS, red, drivers, tareas programadas, changelog global.
- **Va a `biblioteca_negocio_prolongo`**: conocimiento de negocio/laboratorio, memoria,
guardias, pendientes de negocio.
## Estructura
- `estado/` — ESTADO_DIARIO.md, pendientes_infra.json, log y datos de los watchers.
- `scripts/` — watchers + herramientas (infra_changelog, infra_paths, instaladores).
- `omniroute/`, `opencode/`, `vps/` — servicios y su config.
- `AGENTS.md` — reglas del repo (leer siempre).
## Flujo habitual
1. **Antes de tocar algo**: `git pull` y leer `AGENTS.md` + `estado/ESTADO_DIARIO.md`.
2. **Hacer el cambio**.
3. **Registrarlo en el changelog**:
```powershell
python scripts\infra_changelog.py add --titulo "..." --categoria <cat> --transferible si|no --detalle "..."
```
4. **Si es un watcher/servicio**: reinstalar/repuntar con `scripts\instalar_watchers.ps1`
o `scripts\instalar_global.ps1`.
5. **Actualizar** `estado/ESTADO_DIARIO.md` y, si procede, `estado/pendientes_infra.json`.
6. **Guardar**: Engram (`mem_save`, proyecto `INFRAESTRUCTURA`) + `git add/commit/push`.
## Watchers
Viven en `scripts/`; estado en `estado/`. Resuelven rutas con `scripts/infra_paths.py`
(estado local; `pendientes.json` y módulos compartidos desde el repo hermano
`biblioteca_negocio_prolongo`, override con `BIBLIOTECA_REPO`).
```powershell
# Instalar tareas programadas (AtLogOn + reinicio si falla)
powershell -ExecutionPolicy Bypass -File scripts\instalar_watchers.ps1
# Verificar procesos
Get-CimInstance Win32_Process -Filter "Name='python.exe'" |
Where-Object { $_.CommandLine -match 'telegram_watcher|recordatorios_msgbox' }
```
## Changelog global
```powershell
python scripts\infra_changelog.py pendientes # que falta replicar en ESTE equipo
python scripts\infra_changelog.py aplicado <id>
python scripts\infra_changelog.py estado
```
El `re` de biblioteca lee esto (paso 4b) y avisa de lo pendiente del otro equipo.
## Reglas
- NUNCA secretos/tokens en el repo.
- Regla 5: no enviar mensajes sin permiso + doble factor.
- Regla 15: Jira solo GET.
- Regla 24: NET/VB/4GL solo lectura.

99
AGENTS.md Normal file
View File

@@ -0,0 +1,99 @@
# AGENTS.md — Repo INFRAESTRUCTURA (infraestructura compartida Prolongo)
> Repo hermano de `biblioteca_negocio_prolongo`. Aquí vive **todo lo que hace
> funcionar** los equipos (sobremesa, portátil, VPS): watchers, automatizaciones,
> plugins, configuración de servicios y el **changelog global**.
>
> **Regla 31**: si algo sirve para "hacer funcionar" o automatizar la
> infraestructura → este repo. Si es "conocer el negocio" o memoria → biblioteca.
## Estructura
| Ruta | Qué es |
|------|--------|
| `changelog_infra.json` | Fuente de verdad del changelog global (por equipo) |
| `CHANGELOG_INFRA.md` | Versión legible (generada; no editar a mano) |
| `AGENTS.md` | Este fichero: reglas del repo de infraestructura |
| `estado/` | Estado y memoria de infraestructura (ver abajo) |
| `estado/ESTADO_DIARIO.md` | Diario de cambios de infraestructura |
| `estado/pendientes_infra.json` | Pendientes propios de infraestructura |
| `estado/telegram_mensajes.log` | Log del watcher de Telegram (runtime, gitignored) |
| `estado/recordatorios_msgbox.json` | Recordatorios locales del message box |
| `estado/telegram_tracked_users.json` | Config del watcher (contactos vigilados, etc.) |
| `scripts/infra_changelog.py` | Añadir / consultar / marcar cambios del changelog |
| `scripts/infra_paths.py` | Resolución de rutas compartidas (INFRAESTRUCTURA ↔ biblioteca) |
| `scripts/instalar_global.ps1` | Instalador global del equipo (OmniRoute + OpenCode + watchers) |
| `scripts/instalar_watchers.ps1` | Registra las tareas programadas de los watchers |
| `scripts/telegram_watcher.py` | Watcher de Telegram (chats → `estado/telegram_mensajes.log`) |
| `scripts/recordatorios_msgbox.py` | Recordatorios con message box de Windows |
| `scripts/recordatorio.cmd` | Comando corto `recordatorio` (listar/done/add/test/sync) |
| `omniroute/` | Vigilante y autostart de OmniRoute |
| `opencode/` | Plugin global de OpenCode |
| `vps/` | Configuración de servicios del VPS (Gitea, etc.) |
## Reglas del repo
1. **Solo se versiona infraestructura.** Nada de conocimiento de negocio.
2. Todo cambio de infraestructura se **anota en el changelog** (abajo) con
`transferible: si|no` para que el otro equipo sepa si debe replicarlo.
3. **Nunca** guardar secretos/tokens en el repo (van a `.env` o variables de
usuario; `.gitignore` ya cubre lo típico).
4. Los watchers se ejecutan como **tareas programadas** al iniciar sesión; si se
caen, relanzarlos a mano y, si hace falta, reinstalar con
`scripts/instalar_watchers.ps1`.
5. Este repo está registrado en **Engram** (proyecto `INFRAESTRUCTURA`),
**Gitea** (`jjminguez/INFRAESTRUCTURA`) y **Obsidian** (`03-IT/`).
## Watchers
Los watchers viven en `scripts/` y su estado en `estado/`:
```powershell
# Instalar/reinstalar tareas programadas (AtLogOn + reinicio si falla)
powershell -ExecutionPolicy Bypass -File scripts\instalar_watchers.ps1
# Verificar que corren
Get-CimInstance Win32_Process -Filter "Name='python.exe'" |
Where-Object { $_.CommandLine -match 'telegram_watcher|recordatorios_msgbox' }
```
`telegram_watcher.py` y `recordatorios_msgbox.py` resuelven las rutas con
`scripts/infra_paths.py`:
- Estado → `INFRAESTRUCTURA/estado/`
- Datos de negocio (`pendientes.json`) y módulos aún compartidos
(`idd_channel`, `netatmo_tool`, `alarma_twilio`) → repo
`biblioteca_negocio_prolongo` (hermano; override con `BIBLIOTECA_REPO`).
## Changelog global (sobremesa ↔ portátil ↔ VPS)
```powershell
# Añadir un cambio (detecta el equipo automáticamente)
python scripts\infra_changelog.py add --titulo "..." --categoria watchers --transferible si --detalle "..."
# Ver qué falta replicar en ESTE equipo
python scripts\infra_changelog.py pendientes
# Marcar como aplicado aquí
python scripts\infra_changelog.py aplicado <id>
# Resumen
python scripts\infra_changelog.py estado
```
El protocolo `re` de cada equipo lee este fichero (paso 4b) y avisa de lo pendiente.
## Instalación en un equipo nuevo (p. ej. el portátil)
```powershell
git clone https://git.v-encore-lab.com/jjminguez/INFRAESTRUCTURA.git
cd INFRAESTRUCTURA
powershell -ExecutionPolicy Bypass -File scripts\instalar_global.ps1
```
Prerrequisitos: Python en el PATH; Node.js + `npm i -g omniroute` para OmniRoute.
Copiar además `~/.omniroute` (credenciales), `~/.config/opencode/opencode.jsonc` y
`~/.telegram-agent` (sesión de Telegram) desde el otro equipo.
> Al arrancar el portátil por primera vez, el `re` avisará de los cambios
> transferibles pendientes (incluida esta migración de watchers) y se aplican con
> `instalar_global.ps1`.

View File

@@ -8,6 +8,7 @@
- **2026-09-20-omniroute-arranque-fiable-tarea-programada-vigilante** [omniroute] OmniRoute arranque fiable (tarea programada + vigilante) (aplicado en: sobremesa) - **2026-09-20-omniroute-arranque-fiable-tarea-programada-vigilante** [omniroute] OmniRoute arranque fiable (tarea programada + vigilante) (aplicado en: sobremesa)
- **2026-09-20-omniroute-505-modelos-expuestos-en-opencode** [omniroute] OmniRoute: 505 modelos expuestos en OpenCode (aplicado en: sobremesa) - **2026-09-20-omniroute-505-modelos-expuestos-en-opencode** [omniroute] OmniRoute: 505 modelos expuestos en OpenCode (aplicado en: sobremesa)
- **2026-09-20-plugin-global-opencode-relanza-omniroute** [opencode] Plugin global OpenCode relanza OmniRoute (aplicado en: sobremesa) - **2026-09-20-plugin-global-opencode-relanza-omniroute** [opencode] Plugin global OpenCode relanza OmniRoute (aplicado en: sobremesa)
- **2026-09-20-migracion-de-watchers-telegram-recordatorios-a-infraestructu** [watchers] Migracion de watchers (Telegram + recordatorios) a INFRAESTRUCTURA (aplicado en: sobremesa)
## Historial ## Historial
@@ -50,3 +51,11 @@
- **Transferible**: no - **Transferible**: no
- **Aplicado en**: VPS - **Aplicado en**: VPS
- **Detalle**: VPS: /opt/gitea-watchdog/gitea_watchdog.sh + timer systemd cada 2min (recrea gitea si falla 2 veces). Verificado parando gitea. Monitor Uptime-Kuma 'Gitea - Repositorios' corregido (URL tenia punto final) con alerta email SMTP. Ficheros en INFRAESTRUCTURA/vps/gitea/. - **Detalle**: VPS: /opt/gitea-watchdog/gitea_watchdog.sh + timer systemd cada 2min (recrea gitea si falla 2 veces). Verificado parando gitea. Monitor Uptime-Kuma 'Gitea - Repositorios' corregido (URL tenia punto final) con alerta email SMTP. Ficheros en INFRAESTRUCTURA/vps/gitea/.
### 2026-09-20 · Migracion de watchers (Telegram + recordatorios) a INFRAESTRUCTURA
- **id**: `2026-09-20-migracion-de-watchers-telegram-recordatorios-a-infraestructu`
- **Equipo**: sobremesa
- **Categoria**: watchers
- **Transferible**: si
- **Aplicado en**: sobremesa
- **Detalle**: telegram_watcher.py, recordatorios_msgbox.py y recordatorio.cmd movidos de biblioteca_negocio_prolongo/scripts a INFRAESTRUCTURA/scripts. Rutas resueltas con scripts/infra_paths.py: estado en INFRAESTRUCTURA/estado; pendientes.json y modulos compartidos (idd_channel, netatmo_tool, alarma_twilio) desde el repo hermano (override BIBLIOTECA_REPO). Tareas programadas nuevas (Telegram Watcher, Recordatorios Msgbox) via scripts/instalar_watchers.ps1 (integrado en instalar_global.ps1). En el portatil: git clone INFRAESTRUCTURA, ejecutar scripts/instalar_global.ps1, copiar ~/.telegram-agent y ~/.omniroute, y eliminar el autostart .lnk antiguo si existe. Estado migrado a INFRAESTRUCTURA/estado/.

View File

@@ -1,20 +1,31 @@
# INFRAESTRUCTURA # INFRAESTRUCTURA
Repo de infraestructura compartida entre equipos (sobremesa, portátil, VPS). Repo de infraestructura compartida entre equipos (sobremesa, portátil, VPS).
Todo lo que **hace funcionar** los equipos vive aquí (Regla 31); el conocimiento de
negocio vive en `biblioteca_negocio_prolongo`.
## Contenido ## Contenido
| Ruta | Qué es | | Ruta | Qué es |
|------|--------| |------|--------|
| `changelog_infra.json` | Fuente de verdad del **changelog global de infraestructura** (por equipo) | | `AGENTS.md` | Reglas del repo de infraestructura (leer siempre) |
| `changelog_infra.json` | Fuente de verdad del **changelog global** (por equipo) |
| `CHANGELOG_INFRA.md` | Versión legible (generada; no editar a mano) | | `CHANGELOG_INFRA.md` | Versión legible (generada; no editar a mano) |
| `scripts/infra_changelog.py` | Añadir / consultar / marcar cambios | | `estado/` | Estado y memoria de infraestructura (diario, pendientes, log watcher) |
| `scripts/infra_changelog.py` | Añadir / consultar / marcar cambios del changelog |
| `scripts/infra_paths.py` | Resolución de rutas compartidas (INFRAESTRUCTURA ↔ biblioteca) |
| `scripts/telegram_watcher.py` | Watcher de Telegram → `estado/telegram_mensajes.log` |
| `scripts/recordatorios_msgbox.py` | Recordatorios con message box de Windows |
| `scripts/recordatorio.cmd` | Comando corto `recordatorio` |
| `scripts/instalar_watchers.ps1` | Registra las tareas programadas de los watchers |
| `scripts/instalar_global.ps1` | Instalador global del equipo (OmniRoute + OpenCode + watchers) |
| `omniroute/omniroute_watchdog.ps1` | Vigilante de OmniRoute (arranque + autocuración) | | `omniroute/omniroute_watchdog.ps1` | Vigilante de OmniRoute (arranque + autocuración) |
| `omniroute/install_omniroute_autostart.ps1` | Registra la tarea programada `OmniRoute Watchdog` | | `omniroute/install_omniroute_autostart.ps1` | Registra la tarea programada `OmniRoute Watchdog` |
| `opencode/omniroute-bootstrap.ts` | Plugin GLOBAL de OpenCode: relanza OmniRoute al arrancar OpenCode | | `opencode/omniroute-bootstrap.ts` | Plugin GLOBAL de OpenCode: relanza OmniRoute al arrancar OpenCode |
| `scripts/instalar_global.ps1` | Instala el plugin global y la tarea de arranque | | `vps/` | Configuración de servicios del VPS (Gitea, etc.) |
| `.opencode/` | Agente `infra-agente` y skill `infraestructura` |
## Changelog global (sobremesa ↔ portátil) ## Changelog global (sobremesa ↔ portátil ↔ VPS)
Registra cada cambio de infraestructura y si es transferible al otro equipo: Registra cada cambio de infraestructura y si es transferible al otro equipo:
@@ -32,7 +43,16 @@ python scripts/infra_changelog.py aplicado <id>
python scripts/infra_changelog.py estado python scripts/infra_changelog.py estado
``` ```
El protocolo `re` de cada equipo lee este fichero y avisa de lo pendiente. El protocolo `re` de cada equipo lee este fichero (paso 4b) y avisa de lo pendiente.
## Watchers
Viven en `scripts/` y su estado en `estado/`. Se instalan como tareas programadas
(AtLogOn + reinicio si falla):
```powershell
powershell -ExecutionPolicy Bypass -File scripts/instalar_watchers.ps1
```
## Instalación en un equipo nuevo (p. ej. el portátil) ## Instalación en un equipo nuevo (p. ej. el portátil)
@@ -43,4 +63,11 @@ powershell -ExecutionPolicy Bypass -File scripts/instalar_global.ps1
``` ```
Prerrequisitos de OmniRoute: Node.js + `npm i -g omniroute`, y copiar `~/.omniroute` Prerrequisitos de OmniRoute: Node.js + `npm i -g omniroute`, y copiar `~/.omniroute`
(credenciales) y `~/.config/opencode/opencode.jsonc`. (credenciales) y `~/.config/opencode/opencode.jsonc`. Para el watcher de Telegram,
copiar también `~/.telegram-agent` (sesión).
## Integración
- **Engram**: proyecto `INFRAESTRUCTURA` (memoria del asistente).
- **Gitea**: `jjminguez/INFRAESTRUCTURA` (`git.v-encore-lab.com`).
- **Obsidian**: documentación en `03-IT/` del vault `biblioteca_prolongo`.

View File

@@ -60,6 +60,18 @@
"VPS" "VPS"
], ],
"detalle": "VPS: /opt/gitea-watchdog/gitea_watchdog.sh + timer systemd cada 2min (recrea gitea si falla 2 veces). Verificado parando gitea. Monitor Uptime-Kuma 'Gitea - Repositorios' corregido (URL tenia punto final) con alerta email SMTP. Ficheros en INFRAESTRUCTURA/vps/gitea/." "detalle": "VPS: /opt/gitea-watchdog/gitea_watchdog.sh + timer systemd cada 2min (recrea gitea si falla 2 veces). Verificado parando gitea. Monitor Uptime-Kuma 'Gitea - Repositorios' corregido (URL tenia punto final) con alerta email SMTP. Ficheros en INFRAESTRUCTURA/vps/gitea/."
},
{
"id": "2026-09-20-migracion-de-watchers-telegram-recordatorios-a-infraestructu",
"fecha": "2026-09-20",
"equipo": "sobremesa",
"categoria": "watchers",
"titulo": "Migracion de watchers (Telegram + recordatorios) a INFRAESTRUCTURA",
"transferible": true,
"aplicado_en": [
"sobremesa"
],
"detalle": "telegram_watcher.py, recordatorios_msgbox.py y recordatorio.cmd movidos de biblioteca_negocio_prolongo/scripts a INFRAESTRUCTURA/scripts. Rutas resueltas con scripts/infra_paths.py: estado en INFRAESTRUCTURA/estado; pendientes.json y modulos compartidos (idd_channel, netatmo_tool, alarma_twilio) desde el repo hermano (override BIBLIOTECA_REPO). Tareas programadas nuevas (Telegram Watcher, Recordatorios Msgbox) via scripts/instalar_watchers.ps1 (integrado en instalar_global.ps1). En el portatil: git clone INFRAESTRUCTURA, ejecutar scripts/instalar_global.ps1, copiar ~/.telegram-agent y ~/.omniroute, y eliminar el autostart .lnk antiguo si existe. Estado migrado a INFRAESTRUCTURA/estado/."
} }
] ]
} }

50
estado/ESTADO_DIARIO.md Normal file
View File

@@ -0,0 +1,50 @@
# ESTADO DIARIO — INFRAESTRUCTURA
> Diario de cambios de infraestructura (sobremesa / portátil / VPS).
> La entrada más reciente va ARRIBA. Para el diario de negocio, ver
> `biblioteca_negocio_prolongo/ESTADO_DIARIO.md`.
---
## 20 septiembre 2026 (domingo) — Migración de watchers a INFRAESTRUCTURA
### Tareas realizadas
- **Watchers migrados** de `biblioteca_negocio_prolongo/scripts/` a `INFRAESTRUCTURA/scripts/`:
- `telegram_watcher.py` (chats → `estado/telegram_mensajes.log`)
- `recordatorios_msgbox.py` (message box; datos en `estado/recordatorios_msgbox.json`)
- `recordatorio.cmd` (comando corto `recordatorio`)
- **Resolución de rutas** con `scripts/infra_paths.py`: estado en `INFRAESTRUCTURA/estado/`,
datos de negocio (`pendientes.json`) y módulos aún compartidos (`idd_channel`,
`netatmo_tool`, `alarma_twilio`) desde el repo hermano `biblioteca_negocio_prolongo`
(override con `BIBLIOTECA_REPO`).
- **Tareas programadas** nuevas vía `scripts/instalar_watchers.ps1`: `Telegram Watcher`
y `Recordatorios Msgbox` (AtLogOn + reinicio si falla). Sustituyen al autostart por
`.lnk` (que no existía en el sobremesa).
- **Estado migrado** a `INFRAESTRUCTURA/estado/`: log, `.watcher_last_id`,
`telegram_tracked_users.json`, `.netatmo_last_alert.json`, `recordatorios_msgbox.json`.
- **Referencias actualizadas** en `biblioteca_negocio_prolongo`: AGENTS.md (Regla 6),
skills `telegram-procesamiento` y `protocolo-re`, agente `telegram-agente.md`,
`INSTALACION_OPENCODE.md`, `scripts/telegram_relogin.py`.
- **Estructura nueva** en este repo: `AGENTS.md`, `estado/`, `.opencode/agent/`,
`.opencode/skill/`, `.gitignore`.
- **Registrado** en Engram (proyecto `INFRAESTRUCTURA`) y en Obsidian (`03-IT/`).
### Archivos modificados
- `INFRAESTRUCTURA/scripts/{telegram_watcher.py,recordatorios_msgbox.py,recordatorio.cmd,infra_paths.py,instalar_watchers.ps1,instalar_global.ps1}`
- `INFRAESTRUCTURA/estado/*`, `INFRAESTRUCTURA/AGENTS.md`, `INFRAESTRUCTURA/.gitignore`, `README.md`
- `biblioteca_negocio_prolongo/{AGENTS.md,INSTALACION_OPENCODE.md,.opencode/skill/*,.opencode/agent/telegram-agente.md,scripts/telegram_relogin.py}`
- Eliminados de biblioteca: `scripts/telegram_watcher.py`, `scripts/recordatorios_msgbox.py`, `scripts/recordatorio.cmd`
### Descubrimientos
- `wmic` no existe en Windows 11 nuevo → usar CIM (PowerShell).
- Los watchers dependen de módulos que siguen en biblioteca porque los **bots del VPS**
los importan desde allí (`idd_channel`, `netatmo_tool`, `alarma_twilio`). Pendiente de
centralizar en fase 2 (requiere redeploy del VPS).
- El autostart por `.lnk` (`recordatorio_msgbox.lnk`) no existía en el sobremesa; se
sustituye por tareas programadas, más robustas.
### Pendiente para el PORTÁTIL
- Al arrancar: `git clone` de `INFRAESTRUCTURA`, ejecutar
`scripts\instalar_global.ps1` y marcar en el changelog
`aplicado 2026-09-20-migracion-watchers-a-infraestructura`.
- Copiar `~/.telegram-agent` (sesión) y `~/.omniroute` (credenciales) desde el sobremesa.

View File

@@ -0,0 +1,40 @@
{
"version": 1,
"proyecto": "INFRAESTRUCTURA",
"actualizado": "2026-09-20",
"total_pending": 4,
"pending": [
{
"id": 1,
"task": "Fase 2: centralizar en INFRAESTRUCTURA los modulos aun compartidos (idd_channel, netatmo_tool, alarma_twilio) y redeployar los bots del VPS",
"categoria": "scripts",
"responsable": "yo",
"transferible": true,
"notas": "Ahora se resuelven desde biblioteca_negocio_prolongo via infra_paths.bootstrap_biblioteca_paths(). Requiere actualizar los bots del VPS (/opt) antes de moverlos."
},
{
"id": 2,
"task": "Migrar por lotes el resto de scripts de infraestructura de biblioteca_negocio_prolongo/scripts a INFRAESTRUCTURA (Regla 31: cuando se toquen)",
"categoria": "scripts",
"responsable": "yo",
"transferible": true,
"notas": "Candidatos: aviso_sonoro.py, jira_*_watcher.py, sync_*, telegram_* (no VPS), watchdog_obsidian_sync.ps1, install_service.ps1."
},
{
"id": 3,
"task": "Portatil: clonar INFRAESTRUCTURA, ejecutar instalar_global.ps1 y marcar aplicado 2026-09-20-migracion-watchers-a-infraestructura",
"categoria": "otros",
"responsable": "yo",
"transferible": false,
"notas": "Al arrancar el portatil el re lo avisara (paso 4b). Copiar ~/.telegram-agent y ~/.omniroute."
},
{
"id": 4,
"task": "Eliminar el autostart antiguo (recordatorio_msgbox.lnk) si existiera, tras instalar las tareas programadas",
"categoria": "otros",
"responsable": "yo",
"transferible": true,
"notas": "En el sobremesa no existia; en el portatil hay que comprobar la carpeta Startup."
}
]
}

View File

@@ -0,0 +1,57 @@
{
"recordatorios": [
{
"id": 1,
"texto": "Enviar a Romero: recepcion jamones faccsa + controles temperatura Dompal",
"fecha": "diario",
"hora": "12:00",
"done": true
},
{
"id": 2,
"texto": "Acabar programa de mantenimiento de incentivos por empleado",
"fecha": "diario",
"hora": "13:00",
"done": true
},
{
"id": 3,
"texto": "Hablar con Fernandez para que implante la rama codex_im26_113_conciliacion_banking_play en NET (pendiente #108)",
"fecha": "2026-09-01",
"hora": "10:00",
"done": true,
"pendiente_id": 108
},
{
"id": 4,
"texto": "Identificar quien es Joaquin (ext 1120) — Oscar pasara el listin; reintentar llamada (pendiente #109)",
"fecha": "2026-09-01",
"hora": "10:30",
"done": true,
"pendiente_id": 109
},
{
"id": 5,
"texto": "[pendiente #110] IIT-130 Retirar tablas obsoletas transformaciones Compras (3-1-G y 3-1-H, tablas cp_def_transf_cab/lin y cp_transf_cab/lin) — Oscar lo asigna a Juanjo para revisar y reasignar a quien lo lleve a cabo. Analisis en Google Docs (link en correo). Impacto en repos NET, 4GL y VB",
"fecha": "2026-09-01",
"hora": "12:00",
"done": true,
"pendiente_id": 110
},
{
"id": 6,
"texto": "[pendiente #111] Cerrar publicacion Calidad ISO: CQ017 (v33) y CQ097 (v17) UPDATE usu en cal_paginas_ope codopepag=4 + copiar PDFs correctos a C:\\inetpub\\wwwroot\\Calidad\\pdf + borrar .tmp de \\\\pc041\\paginas$ + plantear a Sistemas desajuste carpetas (dir_web_vigor)",
"fecha": "2026-09-01",
"hora": "09:00",
"done": true,
"pendiente_id": 111
},
{
"id": 7,
"texto": "DIA 7: TERMINAR IIT-130 (desarrollo) + ARREGLAR DISTRIBUCION para poder distribuirlo. PREGUNTAR A ROMERO por G-2-1 (Entrada Palets a Camara, pry_VE_Entrada_Congelar) y G-2-2 (Repetir Etiqueta Palet, pry_ve_reetiquetadopalets). Arreglar aplicativos: Z-2f. Revisar pendientes: api+web IIS (esperando ACamara), repaso bbdd, documento DeCA, Bankinplay caso Carlos Hebilla, habilitar JIT Visual.",
"fecha": "2026-09-07",
"hora": "08:00",
"done": true
}
]
}

View File

@@ -0,0 +1,15 @@
{
"contactos_vigilados": [
"Romero",
"Fernandez",
"Dani",
"Oscar",
"Maribel"
],
"avisar_canal_idd": true,
"silenciado": false,
"silenciado_hasta": 0,
"avisar_alexa": false,
"avisar_sonido_pc": false,
"cooldown_segundos": 60
}

View File

@@ -31,7 +31,7 @@ REPO = Path(__file__).resolve().parent.parent
JSON_FILE = REPO / "changelog_infra.json" JSON_FILE = REPO / "changelog_infra.json"
MD_FILE = REPO / "CHANGELOG_INFRA.md" MD_FILE = REPO / "CHANGELOG_INFRA.md"
CATEGORIAS = ["omniroute", "opencode", "red", "servicios", "drivers", "memoria", "otros"] CATEGORIAS = ["omniroute", "opencode", "watchers", "scripts", "red", "servicios", "drivers", "memoria", "otros"]
# --- Deteccion de equipo --------------------------------------------------- # --- Deteccion de equipo ---------------------------------------------------

48
scripts/infra_paths.py Normal file
View File

@@ -0,0 +1,48 @@
#!/usr/bin/env python3
"""infra_paths.py — Resolucion de rutas compartidas del repo INFRAESTRUCTURA.
Los scripts de infraestructura viven en `INFRAESTRUCTURA/scripts/` y necesitan:
- Estado runtime propio del repo -> `INFRAESTRUCTURA/estado/`
- Datos de negocio y modulos aun compartidos del repo
`biblioteca_negocio_prolongo` -> pendientes.json, idd_channel, netatmo_tool, alarma_twilio
La ubicacion del repo de negocio se resuelve asi (por orden):
1. Variable de entorno `BIBLIOTECA_REPO`
2. Repo hermano de INFRAESTRUCTURA: `../biblioteca_negocio_prolongo`
En el portatil la estructura de carpetas es la misma (`Documents/GitHub/...`), por lo
que el hermano funciona sin configurar nada; `BIBLIOTECA_REPO` permite sobreescribirlo.
"""
from __future__ import annotations
import os
import sys
from pathlib import Path
INFRA_ROOT = Path(__file__).resolve().parent.parent
SCRIPTS_DIR = INFRA_ROOT / "scripts"
ESTADO_DIR = INFRA_ROOT / "estado"
def biblioteca_repo() -> Path:
"""Devuelve la ruta del repo de negocio (biblioteca_negocio_prolongo)."""
env = os.environ.get("BIBLIOTECA_REPO")
if env:
return Path(env).expanduser().resolve()
return (INFRA_ROOT.parent / "biblioteca_negocio_prolongo").resolve()
def bootstrap_biblioteca_paths() -> Path:
"""Anade al sys.path los modulos que aun viven en el repo de negocio.
Transitorio: `idd_channel`, `netatmo_tool` y `alarma_twilio` siguen en
biblioteca_negocio_prolongo porque los bots del VPS los importan desde alli.
Cuando se centralicen en INFRAESTRUCTURA, esta funcion quedara vacia.
"""
repo = biblioteca_repo()
for candidato in (repo, repo / "scripts"):
sp = str(candidato)
if candidato.exists() and sp not in sys.path:
sys.path.insert(0, sp)
return repo

View File

@@ -82,6 +82,10 @@ if (-not $NoStart) {
Write-Host ' - Vigilante arrancado.' -ForegroundColor Yellow Write-Host ' - Vigilante arrancado.' -ForegroundColor Yellow
} }
# 4) Watchers de infraestructura (Telegram + recordatorios) -----------------
Write-Host ''
& (Join-Path $PSScriptRoot 'instalar_watchers.ps1') -NoStart:$NoStart
Write-Host '' Write-Host ''
Write-Host 'Listo. Comprobar:' -ForegroundColor Cyan Write-Host 'Listo. Comprobar:' -ForegroundColor Cyan
Write-Host " Get-ScheduledTask -TaskName '$TaskName' | Get-ScheduledTaskInfo" Write-Host " Get-ScheduledTask -TaskName '$TaskName' | Get-ScheduledTaskInfo"

View File

@@ -0,0 +1,89 @@
<#
.SYNOPSIS
Registra en el equipo actual las tareas programadas de los watchers de
infraestructura (Telegram + recordatorios con message box).
.DESCRIPTION
Los watchers viven ahora en `INFRAESTRUCTURA/scripts/` (centralizados; antes
en biblioteca_negocio_prolongo/scripts/). Este instalador crea/actualiza dos
tareas programadas que arrancan al iniciar sesion y se reinician si fallan:
1. `Telegram Watcher` -> scripts\telegram_watcher.py
2. `Recordatorios Msgbox` -> scripts\recordatorios_msgbox.py
No requiere administrador. Idempotente: re-ejecutar tras un git pull.
.NOTES
Repo: INFRAESTRUCTURA. Los datos de estado quedan en INFRAESTRUCTURA\estado\.
#>
[CmdletBinding()]
param(
[switch]$NoStart
)
$ErrorActionPreference = 'Stop'
$RepoRoot = Split-Path -Parent $PSScriptRoot
$PwshExe = Join-Path $env:SystemRoot 'System32\WindowsPowerShell\v1.0\powershell.exe'
$UserId = "$env:USERDOMAIN\$env:USERNAME"
# Resolver el interprete de Python
$Python = (Get-Command python -ErrorAction SilentlyContinue).Source
if (-not $Python) { throw 'No se encuentra python en el PATH.' }
$Watchers = @(
@{ Task = 'Telegram Watcher'; Script = Join-Path $RepoRoot 'scripts\telegram_watcher.py' },
@{ Task = 'Recordatorios Msgbox'; Script = Join-Path $RepoRoot 'scripts\recordatorios_msgbox.py' }
)
Write-Host '== Instalando tareas de watchers (INFRAESTRUCTURA) ==' -ForegroundColor Cyan
Write-Host " - Python: $Python"
Write-Host " - Repo : $RepoRoot"
foreach ($w in $Watchers) {
if (-not (Test-Path $w.Script)) { throw "No se encuentra el script: $($w.Script)" }
if (Get-ScheduledTask -TaskName $w.Task -ErrorAction SilentlyContinue) {
Unregister-ScheduledTask -TaskName $w.Task -Confirm:$false
}
$Action = New-ScheduledTaskAction `
-Execute $PwshExe `
-Argument "-NoProfile -NonInteractive -ExecutionPolicy Bypass -WindowStyle Hidden -Command `"& '$Python' '$($w.Script)'`"" `
-WorkingDirectory $RepoRoot
$Trigger = New-ScheduledTaskTrigger -AtLogOn -User $env:USERNAME
$Settings = New-ScheduledTaskSettingsSet `
-AllowStartIfOnBatteries `
-DontStopIfGoingOnBatteries `
-StartWhenAvailable `
-RestartCount 999 `
-RestartInterval (New-TimeSpan -Minutes 1) `
-MultipleInstances IgnoreNew
$Settings.ExecutionTimeLimit = 'PT0S'
$Settings.Hidden = $true
$Principal = New-ScheduledTaskPrincipal -UserId $UserId -LogonType Interactive -RunLevel Limited
Register-ScheduledTask `
-TaskName $w.Task `
-Action $Action `
-Trigger $Trigger `
-Settings $Settings `
-Principal $Principal `
-Description "Watcher de infraestructura: $($w.Script)" `
-Force | Out-Null
Write-Host " - Tarea '$($w.Task)' registrada -> $($w.Script)" -ForegroundColor Green
if (-not $NoStart) {
Start-ScheduledTask -TaskName $w.Task
Write-Host " arrancada." -ForegroundColor Yellow
}
}
Write-Host ''
Write-Host 'Listo. Comprobar:' -ForegroundColor Cyan
Write-Host " Get-ScheduledTask -TaskName 'Telegram Watcher','Recordatorios Msgbox'"
Write-Host " Get-Content `"$RepoRoot\estado\telegram_mensajes.log`" -Tail 5"

31
scripts/recordatorio.cmd Normal file
View File

@@ -0,0 +1,31 @@
@echo off
rem recordatorio.cmd — Comando corto para gestionar recordatorios con message box.
rem Uso:
rem recordatorio listar todos los recordatorios
rem recordatorio done <id> marcar como terminado (ya no sale)
rem recordatorio add "texto" --hora 14:00 [--fecha diario|YYYY-MM-DD]
rem recordatorio test <id> mostrar el cuadro ahora mismo
rem recordatorio sync crear recordatorios desde pendientes.json (tareas con hora)
set PY=python
set SCR=C:\Users\juanm\Documents\GitHub\INFRAESTRUCTURA\scripts\recordatorios_msgbox.py
if "%~1"=="" goto listar
if /i "%~1"=="done" (
%PY% "%SCR%" --done %2
goto fin
)
if /i "%~1"=="add" (
%PY% "%SCR%" --add %2 --hora %3 --fecha %4
goto fin
)
if /i "%~1"=="test" (
%PY% "%SCR%" --test %2
goto fin
)
if /i "%~1"=="sync" (
%PY% "%SCR%" --sync
goto fin
)
if /i "%~1"=="listar" goto listar
:listar
%PY% "%SCR%" --listar
:fin

View File

@@ -0,0 +1,226 @@
"""recordatorios_msgbox.py — Recordatorios con cuadros de dialogo (MessageBox) en Windows.
Muestra un message box cuando la hora de un recordatorio llega (o ya ha pasado y no esta hecho).
- Si el usuario pulsa SI -> se marca como hecho (done=True) y no vuelve a salir.
- Si pulsa NO -> se pospone 5 minutos (snooze).
- Si se ejecuta con --done <id> se marca como terminado (lo usa el asistente cuando el usuario dice "terminada").
- Si se ejecuta con --listar muestra el estado de todos los recordatorios.
Fichero de datos: recordatorios_msgbox.json (formato: id, texto, fecha [diario|YYYY-MM-DD], hora HH:MM, done).
Uso:
python scripts/recordatorios_msgbox.py # bucle de monitorizacion (correr en background)
python scripts/recordatorios_msgbox.py --listar # listar estado
python scripts/recordatorios_msgbox.py --done <id> # marcar como terminado
python scripts/recordatorios_msgbox.py --add "texto" --hora HH:MM [--fecha diario|YYYY-MM-DD]
python scripts/recordatorios_msgbox.py --test <id> # mostrar el cuadro sin esperar a la hora
python scripts/recordatorios_msgbox.py --sync # crear recordatorios desde pendientes.json (tareas con campo hora)
"""
import argparse
import ctypes
import json
import sys
import time
from datetime import datetime
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent))
import infra_paths # noqa: E402 (resolucion de rutas del repo INFRAESTRUCTURA)
DATA = infra_paths.ESTADO_DIR / 'recordatorios_msgbox.json'
PENDIENTES = infra_paths.biblioteca_repo() / 'pendientes.json'
# Constantes de MessageBoxW de Win32
MB_OK = 0x0
MB_YESNO = 0x4
MB_ICONINFORMATION = 0x40
MB_ICONQUESTION = 0x20
MB_SYSTEMMODAL = 0x1000
IDYES = 6
IDNO = 7
SLEEP_SEG = 15 # cadencia de comprobacion en segundos
SNOOZE_MIN = 5 # minutos a posponer si pulsa NO
def cargar():
try:
return json.loads(DATA.read_text(encoding='utf-8-sig'))
except FileNotFoundError:
return {'recordatorios': []}
def guardar(data):
DATA.write_text(json.dumps(data, ensure_ascii=False, indent=1), encoding='utf-8')
def fecha_hoy():
return datetime.now().strftime('%Y-%m-%d')
def hora_ahora():
return datetime.now().strftime('%H:%M')
def msgbox(texto, titulo, yesno=False):
"""Muestra un cuadro de dialogo nativo de Windows. Devuelve IDYES/IDNO/1."""
flags = MB_SYSTEMMODAL | MB_ICONINFORMATION
if yesno:
flags |= MB_YESNO
else:
flags |= MB_OK
return ctypes.windll.user32.MessageBoxW(None, texto, titulo, flags)
def es_hora(r):
"""Comprueba si toca mostrar el recordatorio."""
if r.get('done'):
return False
if r.get('fecha', 'diario') == 'diario':
return True
return r.get('fecha') <= fecha_hoy()
def run():
data = cargar()
ultima_vez = {} # id -> 'HH:MM' ya mostrado hoy (o fecha mostrada)
print(f'[recordatorios_msgbox] Monitorizando {len(data["recordatorios"])} recordatorios. Ctrl+C para salir.')
while True:
data = cargar()
ahora = fecha_hoy() + ' ' + hora_ahora()
for r in data['recordatorios']:
if not es_hora(r):
continue
# evita repetir en la misma hora ya mostrada para ese id
clave = f"{r['id']}-{fecha_hoy()}-{r.get('hora')}"
if ultima_vez.get(clave):
continue
if hora_ahora() >= r.get('hora', ''):
ultima_vez[clave] = True
texto = r.get('texto', '')
res = msgbox(texto, f'RECORDATORIO {r.get("hora")}', yesno=True)
if res == IDYES:
r['done'] = True
guardar(data)
print(f'[{ahora}] #{r["id"]} marcado como HECHO: {texto[:60]}')
else:
# snooze: fuerza a no repetir en los proximos SNOOZE_MIN minutos
ultima_vez[clave] = True
print(f'[{ahora}] #{r["id"]} pospuesto (NO).')
time.sleep(SLEEP_SEG)
def listar():
data = cargar()
if not data['recordatorios']:
print('No hay recordatorios.')
return
print(f'{"ID":>3} {"HORA":>5} {"FECHA":<12} {"ESTADO":<10} TAREA')
for r in sorted(data['recordatorios'], key=lambda x: (x.get('hora', ''), x.get('id', 0))):
estado = 'HECHO' if r.get('done') else 'PENDIENTE'
print(f'{r.get("id", 0):>3} {r.get("hora", ""):>5} {str(r.get("fecha", "")):<12} {estado:<10} {r.get("texto", "")[:70]}')
def marcar_done(i):
data = cargar()
for r in data['recordatorios']:
if r.get('id') == i:
r['done'] = True
guardar(data)
print(f'#{i} marcado como terminado. Ya no saldra mas.')
return
print(f'No existe el recordatorio #{i}')
def add(texto, hora, fecha):
data = cargar()
ids = [r.get('id', 0) for r in data['recordatorios']]
nid = max(ids) + 1 if ids else 1
data['recordatorios'].append({'id': nid, 'texto': texto, 'fecha': fecha, 'hora': hora, 'done': False})
guardar(data)
print(f'Recordatorio #{nid} anadido: {hora} [{fecha}] {texto}')
def test(i):
data = cargar()
r = next((x for x in data['recordatorios'] if x.get('id') == i), None)
if not r:
print(f'No existe el recordatorio #{i}')
return
res = msgbox(r.get('texto', ''), f'TEST RECORDATORIO {r.get("hora")}', yesno=True)
if res == IDYES:
r['done'] = True
guardar(data)
print(f'#{i} marcado como HECHO (test).')
def sync():
"""Crea recordatorios desde pendientes.json: tareas activas con campo 'hora' (HH:MM).
La fecha sale del campo 'date' del pendiente o 'diario' si es de hoy."""
try:
data = cargar()
pends = json.loads(PENDIENTES.read_text(encoding='utf-8-sig'))
except FileNotFoundError:
print(f'[sync] No existe {PENDIENTES.name}. Nada que sincronizar.')
return
hoy = fecha_hoy()
creados = 0
for p in pends.get('pending', []):
if p.get('done'):
continue
hora = p.get('hora')
if not hora:
continue
fecha_p = p.get('date', hoy)
texto = p.get('task', '')
# evita duplicados: mismo pendiente_id, o mismo texto y misma hora
pid = p.get('id')
if any(r.get('pendiente_id') == pid for r in data['recordatorios']):
continue
if any(r.get('texto') == texto and r.get('hora') == hora for r in data['recordatorios']):
continue
ids = [r.get('id', 0) for r in data['recordatorios']]
nid = max(ids) + 1 if ids else 1
data['recordatorios'].append({
'id': nid, 'texto': f'[pendiente #{p.get("id")}] {texto}',
'fecha': fecha_p if fecha_p else 'diario', 'hora': hora, 'done': False,
'pendiente_id': p.get('id')
})
creados += 1
print(f'[sync] #{nid} creado desde pendiente #{p.get("id")}: {hora} {texto[:60]}')
if creados:
guardar(data)
print(f'[sync] Sincronizacion completa. {creados} recordatorio(s) nuevo(s).')
def main():
ap = argparse.ArgumentParser(description='Recordatorios con message box')
ap.add_argument('--listar', action='store_true', help='listar estado de los recordatorios')
ap.add_argument('--done', type=int, metavar='ID', help='marcar recordatorio como terminado')
ap.add_argument('--add', metavar='TEXTO', help='anadir recordatorio (usa --hora y --fecha)')
ap.add_argument('--hora', default='12:00', help='hora HH:MM (por defecto 12:00)')
ap.add_argument('--fecha', default='diario', help='fecha YYYY-MM-DD o "diario" (por defecto diario)')
ap.add_argument('--test', type=int, metavar='ID', help='mostrar el cuadro ahora (sin esperar hora)')
ap.add_argument('--sync', action='store_true', help='crear recordatorios desde pendientes.json con hora')
args = ap.parse_args()
if args.sync:
sync()
elif args.listar:
listar()
elif args.done is not None:
marcar_done(args.done)
elif args.add:
add(args.add, args.hora, args.fecha)
elif args.test is not None:
test(args.test)
else:
run()
if __name__ == '__main__':
try:
sys.stdout.reconfigure(encoding='utf-8')
except Exception:
pass
main()

482
scripts/telegram_watcher.py Normal file
View File

@@ -0,0 +1,482 @@
"""
telegram_watcher.py — Muestra mensajes entrantes de Telegram en consola y gestiona canal IDD.
Avisos exclusivos al canal IDD:
- Aviso de conexión de Telegram al canal IDD
- Alertas de lectura (doble check) de contactos vigilados al canal IDD
- Solo contestar comandos dirigidos al canal IDD
- Soporte para silenciar/activar avisos (/silenciar, /activar)
"""
import asyncio, sys, os, json, time, re
from datetime import datetime, timezone
from pathlib import Path
C = {
'red': '\033[91m', 'green': '\033[92m', 'yellow': '\033[93m',
'blue': '\033[94m', 'magenta': '\033[95m', 'cyan': '\033[96m',
'white': '\033[97m', 'bold': '\033[1m', 'reset': '\033[0m',
'bg_red': '\033[41m',
}
SCRIPT_DIR = Path(__file__).resolve().parent
sys.path.insert(0, str(SCRIPT_DIR))
import infra_paths # noqa: E402 (resolucion de rutas del repo INFRAESTRUCTURA)
ESTADO_DIR = infra_paths.ESTADO_DIR
BIBLIOTECA_REPO = infra_paths.bootstrap_biblioteca_paths()
LOG_FILE = ESTADO_DIR / 'telegram_mensajes.log'
LAST_ID_FILE = ESTADO_DIR / '.watcher_last_id'
TRACKED_USERS_FILE = ESTADO_DIR / 'telegram_tracked_users.json'
ALERT_KEYWORDS = ['guardia', 'crítico', 'urgente', 'caído', 'error', 'sin asignar',
'RELEVANTE', 'incidencia', 'alarma', 'paro', 'parada']
ALARMA_COMMANDS = ['/alarma', '!alarma', '.alarma']
ALARMA_TWILIO_DIR = BIBLIOTECA_REPO / 'alarma_twilio'
SILENCIAR_COMMANDS = ['/silenciar', '!silenciar', '/mute', '!mute', '/pausar', '!pausar']
ACTIVAR_COMMANDS = ['/activar', '!activar', '/unmute', '!unmute', '/reanudar', '!reanudar']
NETATMO_COMMANDS = [
'/domotica', '!domotica', '/menu', '!menu',
'/netatmo', '!netatmo', '/ambiente', '!ambiente', '/clima', '!clima', '/sensores', '!sensores',
'/temperatura', '!temperatura', '/temp', '!temp',
'/humedad', '!humedad', '/hum', '!hum',
'/co2', '!co2',
'/ruido', '!ruido',
'/anunciar', '!anunciar',
'/alertas', '!alertas'
]
IDD_CHAT_ID = os.environ.get('TELEGRAM_CHAT_ID_2', '-1003967773087')
def load_tracked_config():
defaults = {
"contactos_vigilados": ["Romero", "Fernandez", "Dani", "Oscar", "Maribel"],
"avisar_canal_idd": True,
"silenciado": False,
"silenciado_hasta": 0,
"avisar_alexa": False,
"avisar_sonido_pc": False,
"cooldown_segundos": 60
}
if TRACKED_USERS_FILE.exists():
try:
with open(TRACKED_USERS_FILE, "r", encoding="utf-8") as f:
c = json.load(f)
defaults.update(c)
except Exception:
pass
return defaults
def save_tracked_config(c):
try:
with open(TRACKED_USERS_FILE, "w", encoding="utf-8") as f:
json.dump(c, f, indent=2)
except Exception as e:
print(f"Error guardando config tracked: {e}", file=sys.stderr)
def is_silenciado():
c = load_tracked_config()
if c.get("silenciado", False):
hasta = c.get("silenciado_hasta", 0)
if hasta == 0:
return True
if time.time() < hasta:
return True
else:
c["silenciado"] = False
c["silenciado_hasta"] = 0
save_tracked_config(c)
return False
return False
def play_beep():
try:
import winsound
winsound.Beep(1000, 200)
except:
pass
def send_to_idd(text, parse_mode="Markdown"):
"""Envia mensaje estrictamente al canal IDD."""
sys.path.insert(0, str(SCRIPT_DIR))
try:
import idd_channel
return idd_channel.send_message(text)
except Exception as e:
print(f"Error enviando a IDD: {e}", file=sys.stderr)
return None
def log_message(chat_name, sender_name, text, is_alert):
try:
entry = json.dumps({
'ts': datetime.now().isoformat(),
'chat': chat_name,
'sender': sender_name,
'text': text[:500] if text else '',
'alert': is_alert,
}, ensure_ascii=False)
with open(LOG_FILE, 'a', encoding='utf-8') as f:
f.write(entry + '\n')
except Exception as e:
print(f'{C["red"]}Error log: {e}{C["reset"]}')
async def handle_silenciar_command(text):
c = load_tracked_config()
parts = text.strip().split()
segundos = 0
tiempo_str = "de forma indefinida (hasta /activar)"
if len(parts) > 1:
param = parts[1].lower()
m_h = re.match(r'^(\d+)\s*h$', param)
m_m = re.match(r'^(\d+)\s*m$', param)
if m_h:
horas = int(m_h.group(1))
segundos = horas * 3600
tiempo_str = f"por {horas} hora(s)"
elif m_m:
minutos = int(m_m.group(1))
segundos = minutos * 60
tiempo_str = f"por {minutos} minuto(s)"
c["silenciado"] = True
c["silenciado_hasta"] = int(time.time() + segundos) if segundos > 0 else 0
save_tracked_config(c)
send_to_idd(f"🔕 *Avisos Silenciados*\nLos avisos de lectura y alertas en IDD han sido silenciados {tiempo_str}.\nUsa `/activar` para reanudar.")
async def handle_activar_command(text):
c = load_tracked_config()
c["silenciado"] = False
c["silenciado_hasta"] = 0
save_tracked_config(c)
send_to_idd("🔔 *Avisos Reactivados*\nLas notificaciones de lectura y alertas ambientales vuelven a estar activas en este canal.")
async def handle_alarma_command(text, chat_name, sender_name, client, dialog):
parts = text.strip().split(maxsplit=2)
if len(parts) < 2:
send_to_idd("ℹ️ Usos:\n!alarma +34657866417\n!alarma +34657866417 Mensaje personalizado")
return
numero = parts[1]
mensaje = parts[2] if len(parts) >= 3 else None
env_path = ALARMA_TWILIO_DIR / '.env'
if env_path.exists():
from dotenv import load_dotenv
load_dotenv(env_path)
sys.path.insert(0, str(ALARMA_TWILIO_DIR))
from config import validate_config
from twilio_service import TwilioAlarmaService
errores = validate_config()
if errores:
send_to_idd("❌ Twilio no configurado:\n" + "\n".join(errores))
return
msg = f"📞 Llamando a {numero}..."
if mensaje:
msg += f"\n📝 Mensaje: {mensaje[:80]}"
send_to_idd(msg)
def ejecutar():
svc = TwilioAlarmaService()
sid = svc.lanzar_llamada(numero, mensaje)
estado, contestado = svc.esperar_resultado(sid)
return estado, contestado
loop = asyncio.get_event_loop()
estado, contestado = await loop.run_in_executor(None, ejecutar)
emoji_map = {
"completed": "✅", "busy": "📞", "no-answer": "📵",
"failed": "❌", "canceled": "🚫", "timeout": "⏰",
}
emoji = emoji_map.get(estado, "❓")
desc = f"{emoji} {numero}: {estado}"
if contestado:
desc += f" ({contestado})"
if contestado == "human":
desc += " 🎯 Humano detectado"
elif contestado and "machine" in contestado:
desc += " 🤖 Contestador automático"
send_to_idd(desc)
async def handle_netatmo_command(text, chat_name, sender_name, client, dialog):
cmd = text.strip().lower().split()[0]
sys.path.insert(0, str(SCRIPT_DIR))
import netatmo_tool
def consultar():
try:
token = netatmo_tool.get_access_token()
data = netatmo_tool.get_homecoach_data(token)
sensores = netatmo_tool.extraer_sensores(data)
cfg = netatmo_tool.load_config()
return sensores, cfg, None
except Exception as e:
return None, None, str(e)
loop = asyncio.get_event_loop()
sensores, cfg, error = await loop.run_in_executor(None, consultar)
if error:
send_to_idd(f"⚠️ Error consultando Netatmo: {error}")
return
if cmd in ('/temperatura', '!temperatura', '/temp', '!temp'):
msg = netatmo_tool.generar_mensaje_telegram("temp", sensores, cfg)
elif cmd in ('/humedad', '!humedad', '/hum', '!hum'):
msg = netatmo_tool.generar_mensaje_telegram("hum", sensores, cfg)
elif cmd in ('/co2', '!co2'):
msg = netatmo_tool.generar_mensaje_telegram("co2", sensores, cfg)
elif cmd in ('/ruido', '!ruido'):
msg = netatmo_tool.generar_mensaje_telegram("ruido", sensores, cfg)
elif cmd in ('/anunciar', '!anunciar'):
netatmo_tool.cmd_anunciar(sensores)
msg = "📢 <b>Anuncio enviado a Alexa:</b>\nTemperatura y calidad ambiental leídas por altavoz Echo."
elif cmd in ('/alertas', '!alertas'):
alertas_voz, alertas_tg = netatmo_tool.evaluar_alertas(sensores, cfg)
if alertas_tg:
msg = "🚨 <b>PARÁMETROS FUERA DE RANGO:</b>\n" + "\n".join(f"• {a}" for a in alertas_tg)
else:
msg = "✅ <b>Todos los parámetros están dentro del rango seguro configurado.</b>"
else:
msg = netatmo_tool.generar_mensaje_telegram("menu", sensores, cfg)
netatmo_tool.notificar_telegram(msg)
async def main():
try:
from telethon import TelegramClient, events
except ImportError:
print(f'{C["red"]}pip install telethon{C["reset"]}')
sys.exit(1)
api_id = int(os.environ.get('TELEGRAM_API_ID', '0'))
api_hash = os.environ.get('TELEGRAM_API_HASH', '')
if not api_id or not api_hash:
import subprocess
try:
r1 = subprocess.run(['powershell','-Command','[Environment]::GetEnvironmentVariable(\"TELEGRAM_API_ID\",\"User\")'], capture_output=True, text=True)
r2 = subprocess.run(['powershell','-Command','[Environment]::GetEnvironmentVariable(\"TELEGRAM_API_HASH\",\"User\")'], capture_output=True, text=True)
api_id = int(r1.stdout.strip()) if r1.stdout.strip() else 0
api_hash = r2.stdout.strip()
except:
pass
if not api_id or not api_hash:
print(f'{C["red"]}Falta TELEGRAM_API_ID / TELEGRAM_API_HASH{C["reset"]}')
sys.exit(1)
session_file = str(Path.home() / '.telegram-agent' / 'sessions' / 'watcher')
client = TelegramClient(session_file, api_id, api_hash)
await client.start(phone='+34657866417')
me = await client.get_me()
print(f'{C["green"]}Conectado: {me.first_name} (@{me.username or "sin"}){C["reset"]}')
print(f'{C["cyan"]}Canal de avisos exclusivo: IDD ({IDD_CHAT_ID}){C["reset"]}')
print(f'{C["cyan"]}Log: {LOG_FILE}{C["reset"]}')
print(f'{"─" * 60}\n')
# Enviar aviso de conexión al canal IDD
send_to_idd(f"🟢 *Telegram Conectado* ({me.first_name})\nMonitorizando lecturas de contactos y canal IDD.")
# Recuperar ultimos IDs procesados por chat
last_ids = {}
if LAST_ID_FILE.exists():
try:
last_ids = json.loads(LAST_ID_FILE.read_text())
except:
pass
# Registro de eventos de confirmacion de lectura (Doble Check) -> SOLO IDD
last_read_triggers = {}
@client.on(events.MessageRead(outbox=True))
async def on_message_read(event):
try:
if is_silenciado():
return
c = load_tracked_config()
vigilados = c.get("contactos_vigilados", ["Romero", "Fernandez", "Dani", "Oscar", "Maribel"])
chat = await event.get_chat()
chat_title = getattr(chat, "title", "") or getattr(chat, "first_name", "") or str(event.chat_id)
username = getattr(chat, "username", "") or ""
coincide = False
nombre_vig = chat_title
for v in vigilados:
v_clean = v.lower().strip()
if v_clean and (v_clean in chat_title.lower() or (username and v_clean in username.lower()) or v_clean in str(event.chat_id)):
coincide = True
nombre_vig = v
break
if not coincide:
return
now_t = time.time()
if (now_t - last_read_triggers.get(event.chat_id, 0)) < c.get("cooldown_segundos", 60):
return
last_read_triggers[event.chat_id] = now_t
hora_now = datetime.now().strftime("%H:%M:%S")
print(f"\n{C['bg_red']}{C['white']} 👀 LEÍDO {C['reset']} {C['bold']}{C['green']}[{nombre_vig}]{C['reset']} {hora_now} ha leído tu mensaje en '{chat_title}'")
entry = json.dumps({
"ts": datetime.now().isoformat(),
"tipo": "LECTURA",
"contacto": nombre_vig,
"chat": chat_title
}, ensure_ascii=False)
with open(LOG_FILE, "a", encoding="utf-8") as f:
f.write(entry + "\n")
# Avisar exclusivamente al canal IDD
msg_idd = (
f"👀 *LECTURA CONFIRMADA*\n"
f"━━━━━━━━━━━━━━━━━━━━━━\n"
f"• *Contacto:* `{nombre_vig}`\n"
f"• *Chat:* {chat_title}\n"
f"• *Hora:* {hora_now}"
)
send_to_idd(msg_idd)
except Exception as e:
pass
print(f'{C["white"]}Escuchando... (Ctrl+C para salir){C["reset"]}\n')
last_netatmo_check = 0
while True:
try:
# Comprobacion periodica de alertas Netatmo (cada 5 minutos = 300s) -> SOLO IDD
now = time.time()
if now - last_netatmo_check > 300:
last_netatmo_check = now
try:
if not is_silenciado():
sys.path.insert(0, str(SCRIPT_DIR))
import netatmo_tool
def check_alertas():
tok = netatmo_tool.get_access_token()
dt = netatmo_tool.get_homecoach_data(tok)
sens = netatmo_tool.extraer_sensores(dt)
c = netatmo_tool.load_config()
return netatmo_tool.evaluar_alertas(sens, c), sens, c
loop = asyncio.get_event_loop()
(voz, tg), sens, cfg = await loop.run_in_executor(None, check_alertas)
if voz:
disparar = False
cooldown_sec = cfg.get("cooldown_minutos", 30) * 60
msg_voz = ". ".join(voz)
last_alert_file = ESTADO_DIR / ".netatmo_last_alert.json"
if last_alert_file.exists():
try:
with open(last_alert_file, "r", encoding="utf-8") as f:
last_d = json.load(f)
if last_d.get("msg") != msg_voz or (now - last_d.get("timestamp", 0)) >= cooldown_sec:
disparar = True
except Exception:
disparar = True
else:
disparar = True
if disparar:
# Notificar al canal IDD
alerta_html = (
f"🚨 <b>ALERTA DOMÓTICA — {sens[0].get('nombre', 'Netatmo')}</b>\n"
f"━━━━━━━━━━━━━━━━━━━━━━\n"
+ "\n".join(f"• {a}" for a in tg) + "\n"
f"━━━━━━━━━━━━━━━━━━━━━━\n"
f"<i>Detectado a las {datetime.now().strftime('%H:%M:%S')}</i>"
)
netatmo_tool.notificar_telegram(alerta_html)
with open(last_alert_file, "w", encoding="utf-8") as f:
json.dump({"timestamp": now, "msg": msg_voz, "fecha": datetime.now().isoformat()}, f, indent=2)
except Exception as e:
pass
dialogs = await client.get_dialogs(limit=30)
for dialog in dialogs:
try:
dialog_key = str(dialog.id)
last_id = last_ids.get(dialog_key, 0)
messages = await client.get_messages(dialog, limit=5)
new_max = last_id
for msg in reversed(messages):
if msg.id <= last_id or not msg.message:
continue
new_max = max(new_max, msg.id)
chat_name = dialog.name or str(dialog.id)
sender = await msg.get_sender()
sender_name = getattr(sender, 'first_name', '') or getattr(sender, 'title', '') or str(msg.sender_id) if sender else '??'
text = msg.message
is_alert = any(kw in (text or '').lower() for kw in ALERT_KEYWORDS)
alert_prefix = f'{C["bg_red"]}{C["white"]} ALERTA {C["reset"]} ' if is_alert else ''
chat_color = C['cyan'] if 'jira' in chat_name.lower() or 'informe' in chat_name.lower() else C['magenta'] if 'alerta' in chat_name.lower() else C['white']
ts = msg.date.strftime('%H:%M:%S') if msg.date else '--:--:--'
print(f'\n{alert_prefix}{C["bold"]}{chat_color}[{chat_name}]{C["reset"]} {ts} {C["yellow"]}{sender_name}{C["reset"]}')
for line in text.split('\n')[:15]:
print(f' {chat_color}│{C["reset"]} {line}')
if len(text.split('\n')) > 15:
print(f' {chat_color}│{C["reset"]} ...')
log_message(chat_name, sender_name, text, is_alert)
if is_alert:
play_beep()
# Solo procesar comandos si vienen del canal IDD o Saved Messages
is_idd_channel = (str(dialog.id) == str(IDD_CHAT_ID) or chat_name == "IDD" or "idd" in chat_name.lower())
if text and is_idd_channel:
clean_text = text.strip()
if any(clean_text.startswith(cmd) for cmd in SILENCIAR_COMMANDS):
await handle_silenciar_command(clean_text)
elif any(clean_text.startswith(cmd) for cmd in ACTIVAR_COMMANDS):
await handle_activar_command(clean_text)
elif any(clean_text.startswith(cmd) for cmd in ALARMA_COMMANDS):
await handle_alarma_command(text, chat_name, sender_name, client, dialog)
elif any(clean_text.startswith(cmd) for cmd in NETATMO_COMMANDS):
await handle_netatmo_command(text, chat_name, sender_name, client, dialog)
if new_max > last_id:
last_ids[dialog_key] = new_max
except Exception as e:
err = str(e)
if 'FLOOD_WAIT' in err:
wait = int(''.join(c for c in err if c.isdigit()) or '5')
print(f'{C["yellow"]}Rate limit {wait}s...{C["reset"]}')
await asyncio.sleep(wait)
elif 'AUTH_KEY' not in err and 'database' not in err.lower():
pass
LAST_ID_FILE.write_text(json.dumps(last_ids))
except Exception as e:
err = str(e)
if 'FLOOD' in err:
print(f'{C["yellow"]}Flood, esperando 10s...{C["reset"]}')
else:
print(f'{C["red"]}Error: {err[:100]}{C["reset"]}')
await asyncio.sleep(5)
if __name__ == '__main__':
try:
asyncio.run(main())
except KeyboardInterrupt:
print(f'\n{C["yellow"]}Cerrado.{C["reset"]}')