ADR-0006: Casa que aprende — modelos ML pequeños sobre HA recorder¶
| Campo | Valor |
|---|---|
| Status | Proposed |
| Date | 2026-05-17 |
| Decision by | ramon |
| Reviewers | claude (Opus 4.7 1M) |
| Supersedes | — |
| Builds on | ADR-0001 (routing), ADR-0003 (saneamiento) |
Contexto¶
Home Assistant (VM 171, HAOS, 6 GB RAM) acumula ~6 meses de historial de
estados, presencias, sensores Shelly, webostv, ESP32, device_trackers iOS y
calefacción. El recorder ya usa MariaDB (addon core-mariadb, db_url en
secrets.yaml) en lugar del SQLite por defecto — esto cambia la conversación:
hay base sólida sobre la que hacer queries no triviales sin matar al recorder
"vivo".
Se propone construir un sistema "Casa que aprende": modelos ML clásico (NO LLM, NO deep learning) que aprendan patrones recurrentes del hogar y expongan probabilidades a HA para automatizaciones más finas que un horario fijo. El usuario rechaza explícitamente LLMs por coste, latencia, no-determinismo y privacidad.
Casos de uso candidatos¶
- Predictor de llegada (ETA Ramon): weekday + hora + última posición → ventana ETA. Trigger calefacción T-30min.
- Predictor Jellyfin/TV: probabilidad de sesión esta noche → pre-cache top-N episodios en SSD VM 208.
- Anomaly detector de estados: luz a las 04:00 sin motion previo, alarma con nadie esperado → notif.
- Predictor de "modo trabajo": weekday/hora/media activity/presencia → silenciar notifs.
- Predictor de consumo eléctrico mensual (requiere Shelly EM, aún no instalado).
Restricciones duras¶
- Privacidad total: nada sale de la LAN.
- Inferencia <100ms para que HA lo use síncrono en automation.
- Deterministico-ish: dos llamadas con mismo input → mismo output (modelos congelados entre retrains).
- Coste = 0€/mes.
- Operable por una persona sin equipo de MLOps.
Decisiones¶
D1. Deploy: LXC nuevo dedicado en pmx-50 (Opción B)¶
Decisión: crear LXC ha-ml en pmx-50 (2 vCPU, 2 GB RAM, 8 GB disk,
Debian 12, IP fija 192.168.0.172) con Python 3.12 venv + FastAPI + cron.
Alternativas evaluadas:
| Opción | Pros | Contras | Veredicto |
|---|---|---|---|
| A. HA Add-on Python custom | Inside HA, fácil acceso supervisor | HAOS sandbox limita paquetes nativos (scikit-learn compila), update HA puede romper, debug pésimo | ❌ |
| B. LXC dedicado pmx-50 | Libertad total, aislamiento, snapshots PBS, propia memoria sin afectar HA | +1 LXC al inventario | ✅ |
| C. Container Docker en VM 208 | Reaprovecha infra | VM 208 ya carga 54 containers + Loki + Prom; añadir batch ML semanal compite por CPU; mezcla concerns "media server" + "ML personal" | ❌ |
| D. AppDaemon | Nativo HA, fácil bindings | Pensado para automation logic, no para entrenar/servir modelos con dependencias pesadas; ejecutar sklearn en thread del mismo proceso de automations = mala idea | ❌ |
LXC sigue la estética del homelab (tv-gw, hermesbot, rag, etc. todos LXCs en
pmx-50). Snapshots PBS gratis. Si el experimento fracasa, pct destroy y
fuera.
D2. Stack ML: scikit-learn + LightGBM + statsmodels¶
Decisión: empezar con scikit-learn (RandomForest, LogisticRegression, IsolationForest) y subir a LightGBM solo cuando un caso lo justifique. Time-series con statsmodels (SARIMAX) o naïve baselines antes de Prophet.
Justificación: - scikit-learn: maduro, footprint ~80MB, inferencia ms, todo CPU. - LightGBM: gradient boosting top-tier con datasets pequeños (10³-10⁴ muestras), entrena en segundos en CPU, footprint ~5MB por modelo. - statsmodels SARIMAX/Holt-Winters: suficiente para series de presencia y energía con estacionalidad weekday/hora. - Prophet descartado de inicio: dependencias pesadas (cmdstan), overkill para el tamaño de los datos. Si SARIMAX no llega, reconsiderar. - Redes neuronales descartadas: el dataset es pequeño y sparse; un RF con feature engineering decente bate a un MLP con menos esfuerzo y mucho menos overhead operacional.
D3. Pipeline: batch training, REST serving, Prometheus drift¶
HA recorder (MariaDB)──┐
▼
┌────────────────────────┐
│ ha-ml LXC (.172) │
│ │
│ cron weekly: │
│ extract.py ─► train.py ─► models/*.pkl
│ │
│ FastAPI :8000 │
│ /predict/arrival │
│ /predict/jellyfin │
│ /anomaly/state │
│ /metrics (prom) │
└────────────────────────┘
▲
│ rest sensor / command_line
│
Home Assistant (.171)
Extracción: queries SQL directas a MariaDB en read-only user (NUEVO
usuario ha_ml_ro con SELECT solo sobre tablas states, events,
statistics). NO usar la API REST de HA para batch (paginated y lenta).
Feature engineering (módulo features.py):
- hour_of_day (cyclical sin/cos)
- day_of_week (one-hot)
- is_holiday (integración HA workday ya disponible)
- weather_* (HA tiene OpenWeatherMap; cachear features en parquet)
- last_state_change_age por entidad relevante
Training: cron 0 4 * * 0 (domingos 04:00, fuera de horas de uso).
Pickle modelos a /var/lib/ha-ml/models/{arrival,jellyfin,...}.pkl.
Serving: FastAPI con lifespan que carga pickles a memoria. Endpoint
devuelve {prediction, confidence, model_version, trained_at}.
Integración HA:
sensor:
- platform: rest
name: ml_arrival_ramon
resource: http://192.168.0.172:8000/predict/arrival?person=ramon
value_template: "{{ value_json.eta_minutes }}"
json_attributes: [confidence, model_version, trained_at]
scan_interval: 600
D4. Retención recorder: subir a 90 días¶
Decisión: añadir a recorder: del HA:
Justificación: default 10 días mata cualquier intento de aprender
patrones semanales/mensuales. 90 días en MariaDB con los exclude_globs
actuales (RSSI, packets, presence-lite targets) debe pesar <3GB. Verificar
con SELECT table_schema, ROUND(SUM(data_length+index_length)/1024/1024,1)
FROM information_schema.tables WHERE table_schema='homeassistant'.
Para series de energía mensual: statistics table (long-term) ya retiene
indefinido por diseño, suficiente.
D5. Métricas y observabilidad: Prometheus + Grafana folder ml¶
Decisión: FastAPI expone /metrics con prometheus_client:
- ha_ml_predictions_total{model, outcome}
- ha_ml_prediction_confidence{model} (histogram)
- ha_ml_prediction_error{model} (gauge, MAE rolling 7d)
- ha_ml_model_age_days{model}
- ha_ml_training_duration_seconds{model}
Prometheus scrape añadido a /home/monxas/appdata/prometheus/prometheus.yml
job ha-ml. Grafana folder ml con 1 dashboard inicial.
Drift alert (Grafana, severity warning):
- ha_ml_prediction_error{model="arrival"} > umbral_por_modelo for 24h →
webhook n8n con [ml-drift] tag.
- ha_ml_model_age_days > 14 → retrain no se ha ejecutado.
D6. Cold-start strategy: heurística primero, ML después¶
Decisión: ningún modelo entra a producción hasta tener mínimo 4 semanas de datos relevantes Y un baseline heurístico contra el que comparar.
Para cada caso de uso:
1. Implementar heurística trivial (ej. "Ramon llega entre 18:30-19:30
lunes-jueves").
2. Loguear predicted_heuristic vs actual durante 4-8 semanas.
3. Entrenar modelo, comparar MAE/F1 vs heurística.
4. Solo desplegar el modelo si bate la heurística por margen claro
(ej. MAE 20% menor, F1 +0.1). Si no, mantener heurística.
Esto evita el clásico anti-pattern "tengo ML, luego mola" cuando un if
weekday < 5: return 18:45 ya iguala al modelo.
D7. Honesto: qué predictores SÍ y NO funcionarán¶
Análisis a priori, a revisar tras datos reales.
| Caso | Veredicto inicial | Razón |
|---|---|---|
| Llegada Ramon | ✅ probable que funcione | Patrón semanal fuerte, location del iPhone es señal directa, ventana de ±20min es aceptable. Es el caso más "data-rich" y universal. |
| Anomaly state | ✅ funciona con IsolationForest | No predice, detecta. F1 bajo es tolerable porque las acciones (notif) son baratas. Cuidar falsos positivos calibrando contamination. |
| Modo trabajo/ocio | 🟡 marginal | Pocas señales en la casa para distinguir "trabajo concentrado" del "ocio sentado en la misma silla". Necesita inputs explícitos (calendario, status manual) o sensores extra. |
| Jellyfin pre-cache | 🟡 marginal | Frecuencia baja (~3-5 sesiones/semana), gran varianza ("hoy peli con pareja" vs "noche serie solo"). Pre-cachear top-5 es barato así que tolerar baja precisión, pero no esperar 80% accuracy. |
| Consumo eléctrico mensual | ❌ aún no | Requiere Shelly EM instalado. Sin ese sensor es charlatanería. Aplazar hasta hardware. |
| "Predecir mood" explotando solo señales pasivas | ❌ charlatanería | Sin biometría/calendario, no hay señal. Descartado. |
Política: si tras 8 semanas de feedback un modelo no bate la heurística del 50%, se retira del LXC. No mantener zombies "porque mola".
Implementación (plan MVP, 3 semanas)¶
Semana 1: infra¶
- Crear LXC 172
ha-mlen pmx-50 (Debian 12, 2c/2GB/8GB) - Pubkey monxas + promtail + node_exporter (alineado con el resto)
- Crear user MariaDB
ha_ml_rocon SELECT only - Subir
purge_keep_days: 90en HA, restart, verificar tamaño DB - Repo
ha-ml/en/home/monxas/stacks/services/ha-ml/(Python project, pyproject.toml,extract.py,features.py,train.py,serve.py) - FastAPI stub con
/healthy/metrics - Systemd unit
ha-ml.service+ha-ml-train.timer(weekly) - Caddy NO necesario (servicio LAN-only). Prom scrape directo a .172:8000
Semana 2: primer modelo (llegada Ramon)¶
- Query histórico
device_tracker.iphone_ramon,person.ramon,zone.hometransitions - Feature pipeline + dataset parquet en
/var/lib/ha-ml/data/ - Heurística baseline + logging vs actual
- Modelo RandomForest regressor → ETA en minutos
- Endpoint
/predict/arrival?person=ramon - Sensor REST en HA, dashboard Lovelace con
ml_arrival_ramon - Sin acción automática todavía: solo medir 2 semanas
Semana 3: segundo modelo + observabilidad¶
- IsolationForest sobre transiciones de estado nocturnas (00:00-06:00)
- Dashboard Grafana
ml/casa-que-aprendecon: predicciones vs reales, drift, confidence histogram, training duration - Drift alerts en folder
ml - Documentar runbook en
~/stacks/docs/runbooks/ha-ml.md
Pendientes condicionados¶
- Calefacción automática T-30min: solo cuando MAE arrival < 15min sostenido durante 4 semanas y confidence > 0.8.
- Jellyfin pre-cache: solo si tras 6 semanas de log el top-5 cubre >60% de las sesiones reales.
- Consumo eléctrico: bloqueado hasta Shelly EM.
Consecuencias¶
- +1 LXC en pmx-50. RAM real esperada <500MB en idle, <1.5GB durante retrain semanal (5 min). Sin impacto en HA.
- +1 usuario MariaDB read-only. Riesgo: query pesada del extractor
puede degradar recorder. Mitigar con
STRAIGHT_JOINcontrolado, batch off-hours (04:00 domingo). - +90 días retención recorder. DB pasa de ~100MB a estimado 1-3GB. Backup VM 171 sigue siendo manejable (<10GB total).
- Compromiso de retirada: modelos que no batan heurísticas durante 8 semanas se desinstalan. No acumular zombies ML.
- Privacidad intacta: todo el pipeline es LAN. Modelos no se publican, no requieren CF Access.
Riesgos y mitigaciones¶
| Riesgo | Mitigación |
|---|---|
| Predictor de llegada falla y la calefacción se enciende inútilmente → coste energético | Fase "shadow mode" 4 semanas antes de tocar calefacción. Confidence threshold alto. Fallback heurístico. |
| Recorder DB se infla con 90d y degrada HA | Verificar tamaño a 30d/60d/90d. Si pasa de 5GB, refinar excludes. |
| Drift silencioso (modelo entrenado con patrones COVID o vacaciones) | Drift alerts + retrain semanal automático + métrica model_age_days. |
| Falsos positivos del anomaly detector saturan Telegram | Calibrar contamination, cooldown 1h por entity, severity info no critical. |
| LXC ha-ml muere → HA llamadas REST timeout | scan_interval: 600 + sensor con availability template, automations con if state != unavailable. |
| Ramon pierde interés en mantener esto en 2 meses | Aceptado: pct destroy 172, revert recorder, cero deuda residual. |
Decisión final¶
Stack recomendado:
- Deploy: LXC nuevo ha-ml (.172) en pmx-50, Debian 12, Python venv,
FastAPI + systemd + cron weekly.
- ML: scikit-learn baseline, LightGBM cuando justifique, statsmodels
para series. NO Prophet, NO redes neuronales, NO LLM.
- Data: MariaDB recorder con retention 90d, read-only user dedicado.
- Observabilidad: Prometheus scrape + Grafana folder ml + drift alerts
al n8n forwarder existente.
- Política: heurística primero, ML solo si bate heurística por margen
claro, retirada activa de zombies.
Primer caso de uso a producción: predictor de llegada de Ramon (datos ricos, patrón fuerte, acción asociada barata de validar en shadow mode).
Pendientes inmediatos¶
- Aprobar este ADR (cambiar Status a Accepted)
- Crear LXC 172, snapshot post-bootstrap en PBS
- Subir
purge_keep_days: 90, monitorear DB size 4 semanas - Repo
ha-ml/con stub FastAPI + heurística baseline - Empezar a loguear
actual_arrivalvsheuristic_arrivalya, antes siquiera de tener modelo
Referencias¶
homeassistant_access.md— VM 171, REST/WS API, MariaDB addonobservability_stack.md— Prometheus/Grafana/n8n forwarder- ADR-0001 (routing) — patrón LXC + Caddy (aquí Caddy no aplica, LAN-only)
- ADR-0003 (saneamiento) — patrón "monitoring preventivo desde día 1"