Skip to content

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

  1. Predictor de llegada (ETA Ramon): weekday + hora + última posición → ventana ETA. Trigger calefacción T-30min.
  2. Predictor Jellyfin/TV: probabilidad de sesión esta noche → pre-cache top-N episodios en SSD VM 208.
  3. Anomaly detector de estados: luz a las 04:00 sin motion previo, alarma con nadie esperado → notif.
  4. Predictor de "modo trabajo": weekday/hora/media activity/presencia → silenciar notifs.
  5. 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:

recorder:
  purge_keep_days: 90
  db_url: !secret recorder_db_url

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-ml en pmx-50 (Debian 12, 2c/2GB/8GB)
  • Pubkey monxas + promtail + node_exporter (alineado con el resto)
  • Crear user MariaDB ha_ml_ro con SELECT only
  • Subir purge_keep_days: 90 en 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 /health y /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.home transitions
  • 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-aprende con: 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_JOIN controlado, 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_arrival vs heuristic_arrival ya, antes siquiera de tener modelo

Referencias

  • homeassistant_access.md — VM 171, REST/WS API, MariaDB addon
  • observability_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"