Integración personalizada, en desarrollo, para leer saldos y movimientos bancarios (vía Enable Banking) desde Home Assistant, con una base de datos local propia para auditar y categorizar los movimientos.
Versión actual: 0.4.1 · Licencia: MIT. Registro de cambios al final del documento.
Estado del proyecto: lo mantengo en mi tiempo libre para uso propio. Reviso issues y pull requests cuando puedo, pero sin tiempos de respuesta garantizados. Es software que lee tus movimientos bancarios: revisa el código antes de usarlo, igual que harías con cualquier integración de este tipo.
- Clave privada de tu aplicación de Enable Banking: cópiala a
/config/enablebanking/private.keyen el servidor de Home Assistant. Nunca en/config/www(esa carpeta es pública en/local/); el asistente de configuración rechaza esa ruta. - Componente:
- Vía HACS (recomendado): HACS → menú (⋮, arriba a la derecha) →
Repositorios personalizados → añade
https://github.com/lumeou/enablebankingcomo categoría Integración → busca "Enable Banking" en HACS → Descargar. - Manual: copia la carpeta
custom_components/enablebankingde este repositorio a/config/custom_components/.
- Vía HACS (recomendado): HACS → menú (⋮, arriba a la derecha) →
Repositorios personalizados → añade
- URL de redirección en el panel de Enable Banking: debe ser exactamente
https://<tu-dominio-local>/enablebanking/callback. HTTPS es obligatorio en aplicaciones de producción (el sandbox admite HTTP plano). Home Assistant necesita entonces tener HTTPS activado (certificado autofirmado vale, con resolución local por mDNS/.localo tu propio dominio). - Reinicia Home Assistant.
- Ajustes → Dispositivos y servicios → Añadir integración → Enable Banking. Se abre el login del banco; al terminar, la integración continúa sola (no hay que copiar y pegar ninguna URL).
- Puedes repetir el proceso para añadir varios bancos, cada uno como una integración independiente, con su propia aplicación de Enable Banking.
Banco → [motor de sincronización] → base de datos local (SQLite) → sensores · servicios · exportación · categorías
Solo el motor de sincronización habla con el banco. Sensores y servicios leen
siempre de una copia local en /config/enablebanking/<id_de_la_integración>.db
(un archivo por banco conectado). Esto existe porque los bancos limitan cuántas
veces al día se les puede consultar sin que el usuario esté delante.
- Primera descarga: al vincular la cuenta, pide el histórico más largo posible
(
strategy=longest), aprovechando la ventana en que el banco te considera "presente". - Sincronizaciones siguientes: incrementales, con 10 días de solapamiento sobre el último movimiento guardado, para capturar apuntes tardíos o corregidos.
- Calendario: cada 6 horas por defecto (configurable, mínimo 6h), con un tope de 4 sincronizaciones reales al día — el límite habitual que aplican los bancos a las consultas automáticas. El calendario y el tope se cuentan por separado para cada banco conectado.
- Si el banco devuelve "demasiadas peticiones" (429): se pausa varias horas sola y se sigue sirviendo lo último guardado.
- Si caduca la autorización (401): los sensores no desaparecen — siguen mostrando los últimos datos guardados — y Home Assistant pide reautenticar desde Ajustes → Dispositivos y servicios.
- Deduplicación: usa
entry_referenceoreference_numbercuando el banco los da y son únicos; si no, una huella de contenido (fecha efectiva, importe, contraparte y concepto normalizados — nunca el saldo posterior, que puede variar entre consultas del mismo movimiento). Si el identificador fuerte llega más tarde para un movimiento ya guardado por huella, se reconoce y se actualiza la misma fila en vez de duplicarla (detalle en el registro de cambios, v0.4.1). Los movimientos pendientes (PDNG) se sustituyen en cada consulta, nunca se acumulan. - Al eliminar una integración: se cierra el consentimiento en el banco y se borra su base de datos local. Para recuperar el histórico, vuelve a añadir la integración y vincula la cuenta de nuevo (consume una autorización, pero no cuenta para el límite diario de consultas).
No hace falta ningún sensor adicional de Home Assistant para tablas, tartas o
tendencias: los servicios ya devuelven justo lo necesario, y una plantilla
disparada por evento (template: con trigger:) guarda ese resultado en un
sensor propio que Lovelace puede leer. El patrón, aportado y verificado en la
práctica: llamar al servicio dentro de la plantilla con response_variable, y
usarlo como atributo de un sensor cuyo estado es solo la marca de tiempo de la
última actualización.
Esto no se puede crear desde la interfaz — los sensores de plantilla con
trigger: solo existen en YAML a día de hoy; hay una petición abierta en la
comunidad de Home Assistant pidiendo justo esa capacidad para el editor
visual, sin resolver todavía. Para no mezclarlo con el resto de tu
configuration.yaml, va como paquete: un archivo propio,
packages/enablebanking.yaml, que puedes borrar entero el día que quieras
quitarlo, sin dejar nada suelto. Si nunca has usado paquetes en tu instancia:
- Copia
packages/enablebanking.yaml(incluido junto a este README) a/config/packages/enablebanking.yaml. - En tu
configuration.yaml, asegúrate de tener esto (si ya existe una clavehomeassistant:, añade solo la líneapackages: ...dentro):homeassistant: packages: !include_dir_named packages
- Reinicia Home Assistant.
El archivo ya trae los seis sensores completos (los cinco periodos que pediste más la tendencia mensual). Aquí va su contenido, por si quieres entender o ajustar algo antes de copiarlo:
template:
- trigger:
- platform: time_pattern
minutes: "/15"
- platform: homeassistant
event: start
- platform: event
event_type: enablebanking_sync_finished
variables:
primer_dia_mes_actual: >-
{{ now().replace(day=1).strftime('%Y-%m-%d') }}
primer_dia_mes_anterior: >-
{{ (now().replace(day=1) - timedelta(days=1)).replace(day=1).strftime('%Y-%m-%d') }}
ultimo_dia_mes_anterior: >-
{{ (now().replace(day=1) - timedelta(days=1)).strftime('%Y-%m-%d') }}
action:
- service: enablebanking.get_summary
data:
group_by: category
date_from: "{{ primer_dia_mes_actual }}"
response_variable: mes_actual
- service: enablebanking.get_summary
data:
group_by: category
date_from: "{{ primer_dia_mes_anterior }}"
date_to: "{{ ultimo_dia_mes_anterior }}"
response_variable: mes_anterior
- service: enablebanking.get_summary
data:
group_by: category
date_from: "{{ now().strftime('%Y-01-01') }}"
response_variable: anio_actual
- service: enablebanking.get_summary
data:
group_by: category
date_from: "{{ (now().year - 1) }}-01-01"
date_to: "{{ (now().year - 1) }}-12-31"
response_variable: anio_anterior
- service: enablebanking.get_summary
data:
group_by: category
response_variable: historico
- service: enablebanking.get_summary
data:
group_by: month
date_from: "{{ now().strftime('%Y-01-01') }}"
response_variable: tendencia_mensual
sensor:
- name: "Enable Banking - Mes actual"
unique_id: enablebanking_resumen_mes_actual
state: "{{ now().isoformat() }}"
attributes:
income: "{{ mes_actual.income }}"
expense: "{{ mes_actual.expense }}"
net: "{{ mes_actual.net }}"
transaction_count: "{{ mes_actual.transaction_count }}"
groups: "{{ mes_actual.groups | to_json }}"
- name: "Enable Banking - Mes anterior"
unique_id: enablebanking_resumen_mes_anterior
state: "{{ now().isoformat() }}"
attributes:
income: "{{ mes_anterior.income }}"
expense: "{{ mes_anterior.expense }}"
net: "{{ mes_anterior.net }}"
transaction_count: "{{ mes_anterior.transaction_count }}"
groups: "{{ mes_anterior.groups | to_json }}"
- name: "Enable Banking - Año actual"
unique_id: enablebanking_resumen_anio_actual
state: "{{ now().isoformat() }}"
attributes:
income: "{{ anio_actual.income }}"
expense: "{{ anio_actual.expense }}"
net: "{{ anio_actual.net }}"
transaction_count: "{{ anio_actual.transaction_count }}"
groups: "{{ anio_actual.groups | to_json }}"
- name: "Enable Banking - Año anterior"
unique_id: enablebanking_resumen_anio_anterior
state: "{{ now().isoformat() }}"
attributes:
income: "{{ anio_anterior.income }}"
expense: "{{ anio_anterior.expense }}"
net: "{{ anio_anterior.net }}"
transaction_count: "{{ anio_anterior.transaction_count }}"
groups: "{{ anio_anterior.groups | to_json }}"
- name: "Enable Banking - Histórico"
unique_id: enablebanking_resumen_historico
state: "{{ now().isoformat() }}"
attributes:
income: "{{ historico.income }}"
expense: "{{ historico.expense }}"
net: "{{ historico.net }}"
transaction_count: "{{ historico.transaction_count }}"
groups: "{{ historico.groups | to_json }}"
- name: "Enable Banking - Tendencia mensual (año actual)"
unique_id: enablebanking_tendencia_mensual
state: "{{ now().isoformat() }}"
attributes:
groups: "{{ tendencia_mensual.groups | to_json }}"Para limitarte a un solo banco en cualquiera de estas llamadas, añade
account_iban: "TU_IBAN" en el data: del servicio correspondiente.
Desde enablebanking.get_summary, el diccionario groups que devuelve sale ya
ordenado de forma útil: cronológicamente cuando agrupas por month o
year (de más antiguo a más reciente), y de mayor a menor gasto cuando
agrupas por category, account o counterparty. Esto importa porque las
llamadas se hacen por separado a cada banco conectado y se combinan después:
sin este orden, un gráfico podría salir con los meses o las categorías
mezclados según qué banco respondiera primero.
Con plotly-graph-card
(HACS), que sabe generar tantas porciones como haga falta a partir de un
atributo, sin declarar cada categoría a mano:
type: custom:plotly-graph
title: Gastos por categoría - mes actual
entities:
- entity: sensor.enable_banking_mes_actual
type: pie
labels: |
$ex Object.entries(hass.states["sensor.enable_banking_mes_actual"]
?.attributes?.groups || {})
.filter(([, v]) => v.expense < 0)
.map(([k]) => k)
values: |
$ex Object.entries(hass.states["sensor.enable_banking_mes_actual"]
?.attributes?.groups || {})
.filter(([, v]) => v.expense < 0)
.map(([, v]) => Math.abs(v.expense))
textinfo: label+percent
textposition: inside
hovertemplate: |
<b>%{label}</b><br>
%{value:,.2f} €<br>
%{percent}<extra></extra>
sort: true
hours_to_show: 1
refresh_interval: auto
layout:
height: 450
margin:
l: 10
r: 10
t: 20
b: 20
legend:
orientation: h
"y": -10Mismo mecanismo, cambiando pie por bar y leyendo el sensor de tendencia
mensual en vez del de un solo periodo:
type: custom:plotly-graph
title: Neto por mes (año actual)
entities:
- entity: sensor.enable_banking_tendencia_mensual_ano_actual
type: bar
x: >
$ex
Object.keys(hass.states["sensor.enable_banking_tendencia_mensual_ano_actual"]
?.attributes?.groups || {})
"y": >
$ex
Object.values(hass.states["sensor.enable_banking_tendencia_mensual_ano_actual"]
?.attributes?.groups || {}).map(v => v.net)
hours_to_show: 8760
refresh_interval: autoPara comparar años en vez de meses, duplica el bloque de get_summary en la
plantilla con group_by: year y sin date_from (para tener todo el
histórico repartido por año) y apunta el gráfico de barras a ese sensor.
Con flex-table-card
(HACS), o simplemente una tarjeta Markdown:
type: markdown
title: Gastos del mes actual por categoría
content: >
| Categoría | Gasto |
|:---|--:|
{% for cat, datos in state_attr('sensor.enable_banking_mes_actual',
'groups').items() -%}
| {{ cat }} | {{ '%.2f'|format(datos.expense) }} € |
{% endfor %}| Sensor | Qué muestra |
|---|---|
| Saldo | El saldo más significativo disponible (prioriza saldo disponible sobre contable), con todos los saldos como atributo. |
| Último movimiento | Importe con signo, contraparte, concepto, categoría y el movimiento completo como atributo. |
| Número de movimientos | Diagnóstico, desactivado por defecto. Total guardado y pendientes. |
/config/enablebanking/categories.yamlse crea automáticamente con reglas de ejemplo (Alimentación, Restauración, Transporte, Vivienda, Suministros, Ocio, Salud, Nómina/Ingresos). Es un único archivo compartido por todos los bancos conectados, porque las reglas no dependen del banco. Edítalo y ejecutaenablebanking.recategorizepara aplicarlo.- Las categorías puestas a mano con
enablebanking.set_categorynunca se sobrescriben por las reglas, aunque vuelvas a ejecutarrecategorize.
Todos aparecen en Herramientas para desarrolladores → Acciones, con nombre y
descripción de cada campo en español o inglés según el idioma de tu Home
Assistant. Los que consultan movimientos (get_transactions, get_summary,
export) combinan automáticamente todos los bancos conectados, salvo que
indiques account_iban para limitarte a uno.
| Servicio | Qué hace |
|---|---|
enablebanking.sync_now (force) |
Fuerza una consulta a todos los bancos, saltándose el calendario si force: true (pero no un bloqueo activo por límite del banco). |
enablebanking.get_transactions |
Movimientos filtrados por cuenta, fechas, ingreso/gasto, importe, texto, categoría, con paginación (limit/offset) y orden. |
enablebanking.get_summary |
Totales de ingresos, gastos y neto, agrupados por categoría, mes, año, cuenta o contraparte. El orden de groups es cronológico para month/year y de mayor a menor gasto para el resto. |
enablebanking.export |
Escribe los movimientos filtrados en /config/enablebanking/exports/, en CSV o JSON. |
enablebanking.set_category |
Asigna a mano la categoría de un movimiento (por su id, el que devuelve get_transactions). Si el mismo id numérico existe en dos bancos distintos, hay que indicar account_iban para desambiguar. |
enablebanking.recategorize |
Aplica categories.yaml a los movimientos guardados, sin tocar los categorizados a mano. |
enablebanking.list_uncategorized |
Contrapartes más frecuentes sin categoría, para ayudarte a ampliar categories.yaml. |
| Evento | Cuándo | Datos |
|---|---|---|
enablebanking_new_transactions |
Al terminar una sincronización que trajo movimientos nuevos. | entry_id, bank, new_transactions, accounts |
enablebanking_sync_finished |
Al terminar cualquier sincronización (haya traído novedades o no). | entry_id, bank, status, reason |
Ambos identifican de qué banco vienen (entry_id/bank), imprescindible si tienes
más de una integración conectada.
Horas entre consultas automáticas (6–48h, por defecto 6). El tope diario de 4 consultas reales no es configurable desde la interfaz.
Por diseño, algunos ajustes no se exponen como opción de usuario y solo se
cambian editando custom_components/enablebanking/const.py: tope de consultas
diarias, duración de los bloqueos tras un 429, días de solapamiento en la
sincronización incremental, si se borra la base de datos al eliminar la
integración (por defecto sí), y si se usan las cabeceras del navegador como
señal de "usuario presente" en la primera descarga.
- No hay panel visual para navegar y categorizar movimientos; de momento la vía
es
get_transactions/get_summarydesde Herramientas para desarrolladores, o Excel conexport. Planeado para una versión futura. - Deliberadamente no hay sensores nativos de Home Assistant para totales por periodo o desgloses por categoría: se evaluaron (sensores en Python, uno por banco y por periodo) y se descartaron a favor del patrón de plantilla + servicio descrito en "Explotar los datos en Lovelace", que es más flexible (cualquier rango de fechas, no solo unos pocos fijos) y no obliga a mantener decenas de entidades.
- El código de error exacto que cada banco usa para "sesión caducada" no está
confirmado más que para el formato general de Enable Banking
(
{"error": "..."}); un 401 sin código reconocible se trata, por prudencia, como caducidad.
v0.4.1
- Corregido: movimientos duplicados en la base de datos local. La causa era
que la deduplicación usaba como identificador principal
entry_reference(o campos similares) tal cual los reporta el banco; algunos bancos (confirmado con un caso real) lo devuelven anullen la sincronización en la que el movimiento aparece por primera vez y lo rellenan días después para ese mismo movimiento. Al cambiar denulla un valor, el esquema de deduplicación "saltaba" a una clave distinta y el movimiento se insertaba por segunda vez en lugar de actualizarse.- Nueva huella de contenido (
fingerprint) para reconocer que dos filas son el mismo movimiento real, calculada sobre fecha efectiva, importe normalizado ("6","6.00"y6producen la misma huella), moneda, signo (cargo/abono), contraparte, IBAN de la contraparte y concepto (remittance) normalizados (espacios colapsados, sin distinguir mayúsculas/minúsculas). Deliberadamente no incluyebalance_after(puede faltar o variar entre consultas) ni las tres fechas en bruto (booking_date/value_date/transaction_date), sustituidas por la fecha efectiva ya resuelta. - Mecanismo de "promoción": si una fila ya guardada con clave de huella
(
fp:...) coincide en huella con un movimiento que llega después conentry_reference(oreference_number) ya informado, se reutiliza la clave de esa fila existente en lugar de crear una nueva; elUPSERTactualiza esa misma fila y rellena el identificador que llegó tarde, sin duplicar nada ni tocar la categoría que ya tuviera. - Migración de esquema (v1 → v2): añade la columna
fingerprinty calcula su valor para todos los movimientos ya almacenados a partir de sus columnas existentes, de forma automática al actualizar la integración; no requiere ninguna acción manual ni pierde histórico. - Añadido, por el mismo motivo,
reference_numbercomo segundo identificador fuerte (igual queentry_reference) para bancos que usen ese campo en su lugar. - Cubierto con pruebas automatizadas que reproducen exactamente el caso
reportado (mismo movimiento,
entry_referencenulo y luego informado en dos sincronizaciones sucesivas) y casos de no regresión (movimientos distintos el mismo día, movimientos idénticos genuinos el mismo día, bases de datos creadas antes de esta versión).
- Nueva huella de contenido (
v0.4.0
- Corregido: los movimientos nuevos no se categorizaban solos al descargarse.
categories.yamlsolo se aplicaba cuando se llamaba a mano al servicioenablebanking.recategorize; todo lo que entraba por sincronización automática se quedaba sin categoría hasta ese momento. Ahora cada movimiento se categoriza por reglas en el mismo instante en que se guarda por primera vez (incluida la descarga inicial completa al vincular una cuenta), sin tocar para nada los movimientos que ya tuvieran categoría puesta a mano o por una sincronización anterior.recategorizesigue existiendo, para volver a aplicar las reglas tras editarcategories.yamlo para el histórico que ya tenías de antes de esta versión.
v0.3.3
- El IBAN de cada cuenta ahora se ve de un vistazo: aparece como "Modelo" (la línea bajo el nombre del dispositivo, tanto en la lista de dispositivos de la integración como al abrir la ficha del dispositivo) y también en el campo "Número de serie" de esa ficha, con espacios cada 4 caracteres para que se lea como lo imprime el propio banco.
v0.3.2
- Corregido: la plantilla del paquete llamaba a
.strftime()sobre variables que ya habían pasado porvariables:, y Home Assistant las convierte a texto en ese paso — provocabaUndefinedError: 'str object' has no attribute 'strftime'. El formateo se hace ahora dentro de la propia variable, de una sola vez. - Corregido: los ejemplos de Lovelace de la sección "Explotar los datos en
Lovelace" apuntaban a
sensor.enablebanking_resumen_mes_actualysensor.enablebanking_tendencia_mensual, que no son losentity_idreales (Home Assistant los genera a partir dename:, no deunique_id). Los ejemplos habrían mostrado tarjetas en blanco. Corregidos asensor.enable_banking_mes_actualysensor.enable_banking_tendencia_mensual_ano_actual. - Ampliadas las reglas de categorización por defecto (
categories.py): de 8 a 13 categorías, con más palabras clave por categoría, y cuatro categorías nuevas (Compras, Hogar, Educación, Seguros, Finanzas).
v0.3.1
- El patrón de la sección "Explotar los datos en Lovelace" pasa de pegarse a
mano en
configuration.yamla un paquete propio (packages/enablebanking.yaml, incluido en el ZIP), autocontenido y fácil de quitar. Sin cambios en el componente Python.
v0.3.0
- Añadido
group_by: yearaenablebanking.get_summary, para comparativas año a año sin tener que sumar doce llamadas agrupadas por mes. - Corregido: el diccionario
groupsque devuelveget_summarysalía en un orden arbitrario cuando combinaba varios bancos (dependía de cuál respondiera antes). Ahora sale cronológico paramonth/yeary de mayor a menor gasto para el resto — importante para que un gráfico de barras o de tarta no salga desordenado. - Evaluados y descartados los sensores nativos de periodo (mes/año actual y
anterior, histórico) en favor del patrón de plantilla disparada por evento +
get_summary, documentado a fondo en la nueva sección "Explotar los datos en Lovelace", con ejemplos de tarta y barras conplotly-graph-cardy de tabla.
v0.2.2
- Corregido: los servicios de auditoría (
get_transactions,get_summary,export,set_category,recategorize,list_uncategorized) solo miraban la base de datos de la primera integración cargada; con más de un banco conectado, devolvían resultados vacíos o del banco equivocado. Ahora combinan todos los bancos y admitenaccount_ibanpara limitarse a uno. - Corregido:
enablebanking.sync_nowpodía perder el resultado de un banco si dos integraciones tenían el mismo nombre (mismo banco, dos aplicaciones). Ahora se indexa porentry_id. - Corregido: los eventos
enablebanking_new_transactionsyenablebanking_sync_finishedno indicaban de qué banco venían. - Corregido/completado: traducciones de todos los servicios y de los mensajes de
error de validación (antes solo estaba traducido
get_transactions, con un campo obsoleto). Añadidostrings.jsoncomo referencia.
v0.2.0 – v0.2.1
- Reescritura a arquitectura local-first: base de datos SQLite propia, motor de sincronización independiente con calendario y tope diario de consultas, gestión de errores 401/429, deduplicación de movimientos, categorización con reglas YAML y corrección manual, servicios de auditoría y exportación.
- URL de redirección con HTTPS para producción (documentado el porqué).
v0.1.0
- Primera versión: config flow con autenticación en el navegador (sin copiar y
pegar URLs), sensores de Saldo y Último movimiento por cuenta, servicio básico
get_transactionssin base de datos local (leía directo del banco en cada sincronización), reautenticación automática al caducar el consentimiento.