¿Cuántas veces descartaste el aviso de "lavado terminado" y la ropa se quedó húmeda en la máquina hasta la noche? ¿O te preguntaste si ya te tomaste la pastilla del día? El problema no es que falte el aviso, es que descartarlo cuesta lo mismo que cumplirlo. Un botón de "listo" en la notificación se aprieta desde el sillón.

La solución de este tutorial es simple: un sticker NFC pegado en la máquina o en el pastillero. Home Assistant te recuerda la tarea cada 30 minutos y solo la da por cumplida cuando escaneas el sticker con el celular, y para escanearlo tienes que estar frente a la máquina. Vas a aprender a escribir los stickers (con la app o con un lector PN532 en una Raspberry Pi), a armar el recordatorio con un interruptor auxiliar y una automatización, y a saber hasta dónde llega el sistema.

La idea en tres piezas

  1. Un interruptor auxiliar (input_boolean) recuerda si la tarea está pendiente. Lo enciende el aviso de fin de ciclo de la lavadora o, en el caso de la pastilla, la hora.
  2. Una automatización escucha tres disparadores: el interruptor que se enciende, un reloj cada 30 minutos y el escaneo del sticker. Con los dos primeros manda el recordatorio si la tarea sigue pendiente; con el escaneo apaga el interruptor y confirma.
  3. El sticker NFC solo guarda una dirección: https://www.home-assistant.io/tag/<identificador>. El celular reconoce que esa dirección es de la app de Home Assistant, y la app le avisa a tu servidor qué sticker se escaneó.

Como el estado vive en el interruptor y no en la notificación, puedes descartar los recordatorios sin miedo: a la media hora llega otro. Y el interruptor conserva su estado aunque reinicies Home Assistant.

Qué necesitas

  • Stickers NFC NTAG213 (redondos de 25 mm son lo más común). Tienen que ser NTAG21x: los MIFARE Classic de 1K no los lee un iPhone y no sirven para esto.
  • Un celular con la app de Home Assistant: iPhone XS/XR o más nuevo, o un Android con NFC. Cada persona que vaya a escanear necesita la app en su teléfono.
  • Para el recordatorio de la lavadora: un enchufe inteligente con medición de consumo (el original usa un Shelly) y una automatización que detecte el fin de ciclo por la potencia.
  • Opcional: una Raspberry Pi con un lector PN532, si quieres escribir muchos stickers seguidos, verificarlos o bloquearlos contra reescritura.

No pegues el sticker directo sobre metal: la plancha de acero inoxidable de una lavavajillas absorbe el campo y el celular no lo lee. Para eso existen stickers "on metal", con una capa de ferrita atrás. Sobre plástico, madera o vidrio funcionan los normales.

Escribir los stickers

Con la app de Home Assistant (lo más fácil)

En la app, entra a Ajustes → Conectividad → Tags y toca Agregar tag. Ponle un nombre (por ejemplo, "Lavadora") y, en la opción de identificador personalizado, escribe uno legible como lavadora. Si lo dejas vacío, la app inventa uno al azar. Toca Crear y escribir y acerca la parte de arriba del celular al sticker hasta que confirme.

Diálogo de la app de Home Assistant para crear un tag nuevo, con el campo de nombre, la opción de identificador personalizado y los botones Crear y Crear y escribir (interfaz en alemán)

Usa identificadores en minúsculas, sin tildes, eñes ni espacios. Es lo que después va en tag_id dentro de la automatización.

Con un PN532 en la Raspberry Pi

El lector vale la pena si vas a preparar varios stickers, si quieres leerlos de vuelta para verificar o si los quieres bloquear. El PN532 se conecta a la Raspberry Pi por I²C (SDA, SCL, 3,3 V y GND, con los interruptores del módulo en modo I²C) y usa la librería adafruit_pn532 dentro de un entorno virtual en ~/nfc-env.

Sticker NFC apoyado plano sobre el módulo PN532 rojo, conectado con cables a la Raspberry Pi

Crea el archivo:

Bash
nano nfc_ha_tag.py

y pega este script. Construye el mensaje NDEF a mano, lo escribe página por página (cada página del NTAG213 son 4 bytes, y la memoria del usuario parte en la página 4), lo vuelve a leer para compararlo y, solo si se lo pides, lo bloquea:

Python
import signal
import sys

import board
import busio
from adafruit_pn532.i2c import PN532_I2C

# usage: python nfc_ha_tag.py <tag-id> [--lock]
args = [a for a in sys.argv[1:] if a != "--lock"]
LOCK = "--lock" in sys.argv[1:]
# Home Assistant tag URL; the part after /tag/ becomes the tag ID in Home Assistant
TAG_ID = args[0] if args else "spuelmaschine"
URL = f"www.home-assistant.io/tag/{TAG_ID}"

running = True


def stop(signum, frame):
    # finish the current command first, so the PN532 is not left mid-command
    global running
    running = False


signal.signal(signal.SIGINT, stop)


def ndef_url_message(url):
    # NDEF URI record, prefix 0x04 = "https://"
    payload = bytes([0x04]) + url.encode()
    record = bytes([0xD1, 0x01, len(payload), 0x55]) + payload
    # TLV: 0x03 = NDEF message, then length, record, 0xFE = terminator
    data = bytes([0x03, len(record)]) + record + bytes([0xFE])
    # pad to full 4-byte pages
    return data + bytes((-len(data)) % 4)


def write_page(page, block):
    # a weak field can drop single writes; select the sticker again and retry
    for _ in range(3):
        if pn532.ntag2xx_write_block(page, block):
            return True
        pn532.read_passive_target(timeout=0.5)
    return False


def lock_ntag213():
    # NTAG213 datasheet 8.5.2-8.5.4: all lock bits are OR'ed in and can never be cleared
    # dynamic lock bytes (page 0x28): pages 16-39 read-only, RFUI bits stay 0
    if not write_page(0x28, bytes([0xFF, 0x0F, 0x3F, 0x00])):
        return False
    # capability container byte 3 = 0x0F: NFC Forum "read-only" flag for phones
    if not write_page(3, bytes([0x00, 0x00, 0x00, 0x0F])):
        return False
    # static lock bytes (page 2, bytes 2-3): pages 3-15 read-only; bytes 0-1 are ignored
    return write_page(2, bytes([0x00, 0x00, 0xFF, 0xFF]))


def lock_verified():
    # the PN532 reports some refused writes as OK, so read the lock bits back
    static = pn532.ntag2xx_read_block(2)
    cc = pn532.ntag2xx_read_block(3)
    dynamic = pn532.ntag2xx_read_block(0x28)
    if None in (static, cc, dynamic):
        return False
    return static[2:4] == b"\xff\xff" and cc[3] == 0x0F and dynamic[0:3] == b"\xff\x0f\x3f"


if LOCK:
    print("--lock makes the sticker read-only FOREVER. It can never be rewritten.")
    if input("Type LOCK to continue: ").strip() != "LOCK":
        sys.exit("Not locking. Nothing was written.")

i2c = busio.I2C(board.SCL, board.SDA)
pn532 = PN532_I2C(i2c, debug=False)
pn532.SAM_configuration()

data = ndef_url_message(URL)
print(f"Writing https://{URL} ({len(data)} bytes, {len(data) // 4} pages)")
print("Hold the sticker on the reader ... (stop with Ctrl+C)")

uid = None
while running and uid is None:
    uid = pn532.read_passive_target(timeout=0.5)
if uid is None:
    sys.exit("Stopped.")
print(f"Sticker found, UID {uid.hex(':')}")

# page 3 = capability container; E1 = NDEF formatted, 0x12 = 144 bytes (NTAG213)
cc = pn532.ntag2xx_read_block(3)
print(f"Capability container: {cc.hex(' ')}")
if cc[0] != 0xE1:
    sys.exit("Not NDEF formatted - is this an NTAG sticker?")
if cc[3] != 0x00:
    sys.exit("Sticker is read-only (locked) and cannot be rewritten.")
if len(data) > cc[2] * 8:
    sys.exit("URL too long for this sticker.")
if LOCK and cc[2] != 0x12:
    sys.exit("--lock only supports NTAG213 stickers. Nothing was written.")


# user memory starts at page 4
for i in range(0, len(data), 4):
    if not write_page(4 + i // 4, data[i:i + 4]):
        sys.exit(f"Writing page {4 + i // 4} failed - keep the sticker still and retry.")

check = b"".join(pn532.ntag2xx_read_block(4 + i // 4) for i in range(0, len(data), 4))
if check != data:
    sys.exit("Read-back differs - write again.")
print(f"Done. Verified: https://{URL}")

if LOCK:
    if lock_ntag213() and lock_verified():
        print(f"Locked. Capability container now: {pn532.ntag2xx_read_block(3).hex(' ')}")
    else:
        sys.exit("Locking failed or incomplete - lock bits could not be verified.")

Ejecútalo con el identificador que quieras:

Bash
source ~/nfc-env/bin/activate
python nfc_ha_tag.py spuelmaschine

Con un sticker nuevo, la salida se ve así:

Text
Writing https://www.home-assistant.io/tag/spuelmaschine (48 bytes, 12 pages)
Hold the sticker on the reader ... (stop with Ctrl+C)
Sticker found, UID 53:ac:45:ac:43:00:01
Capability container: e1 10 12 00
Done. Verified: https://www.home-assistant.io/tag/spuelmaschine

Cómo leer esa salida. El e1 al inicio del contenedor de capacidades significa que el sticker está formateado para NDEF, y el 12 (hexadecimal) indica 144 bytes de memoria, la del NTAG213. La dirección ocupa 48, así que sobra espacio. El script se protege de tres errores: un sticker que no es NTAG, uno bloqueado y una dirección que no cabe.

Dos cuidados con el PN532. Deja el sticker plano y centrado sobre la antena: torcido o con distancia, el chip rechaza páginas sueltas (el script reintenta cada una hasta tres veces). Y una vez que aparece "Sticker found", no cortes el script con Ctrl+C: un PN532 interrumpido a mitad de un comando puede dejar trabado el bus I²C hasta que lo desconectes.

Bloquear un sticker (es para siempre)

Si el sticker va en un lugar donde cualquiera llega, como la puerta de la casa o la bodega del patio, alguien podría reescribirlo con una app NFC cualquiera. El script lo bloquea con --lock y pide que escribas LOCK para confirmar:

Bash
python nfc_ha_tag.py tablette --lock

Escribe los bits de bloqueo que define la hoja de datos del NTAG213 y cambia el último byte del contenedor de capacidades a 0f, la marca que le dice a los celulares que es de solo lectura. No hay vuelta atrás: esos bits solo se pueden encender, nunca apagar. Para stickers dentro de la casa no hace falta.

El primer escaneo

El iPhone lee NFC en segundo plano, pero solo con la pantalla encendida. Acerca la parte de arriba del teléfono al sticker y aparece una notificación de Home Assistant; tócala para que se abra la app.

Notificación del iPhone tras escanear el sticker, con la opción de abrir en Home Assistant (interfaz en alemán)

La primera vez, la app pregunta si quieres permitir ese tag y muestra su identificador. Elige Permitir siempre y no vuelve a preguntar.

Diálogo de la app de Home Assistant que pide permitir el tag spuelmaschine, con las opciones de permitir una vez o siempre (interfaz en alemán)

En Home Assistant, el sticker aparece en Ajustes → Tags con la hora del último escaneo, y además se crea una entidad (tag.lavadora, en nuestro ejemplo) cuyo estado es ese momento. Si un escaneo no llega, escanea de nuevo: las automatizaciones de abajo están hechas para que un escaneo doble no rompa nada.

Lista de tags en Home Assistant con dos stickers y la columna de último escaneo (interfaz en alemán)

En Android, según la documentación de la app, el escaneo se dispara directo, sin tocar la notificación. En iPhone, evitar ese toque exige una automatización de Atajos con disparador NFC en cada teléfono, y las automatizaciones tendrían que usar otro disparador; no compensa.

Receta 1: sacar la ropa de la lavadora (o vaciar la lavavajillas)

Esta receta se apoya en una automatización que ya detecta el fin de ciclo midiendo el consumo de la máquina. El original trabaja con la lavavajillas; para la lavadora es igual, cambiando nombres e identificadores. Los fragmentos de código de abajo vienen tal cual del original, con los nombres en alemán (spulmaschine es lavavajillas, ausraumen es vaciar): cámbialos por los tuyos de forma consistente.

Paso 1. El interruptor auxiliar. En Ajustes → Dispositivos y servicios → Ayudantes → Crear ayudante, crea un interruptor (input boolean) llamado, por ejemplo, "Lavadora por vaciar". Home Assistant le pone como identificador input_boolean.lavadora_por_vaciar; si usas tildes o eñes en el nombre, las quita.

Paso 2. Encenderlo al terminar el ciclo. En tu automatización de fin de ciclo, agrega al final de la secuencia una acción que encienda ese interruptor. En YAML son estas líneas:

YAML
          - action: input_boolean.turn_on
            target:
              entity_id: input_boolean.spulmaschine_ausraumen

Paso 3. Un panel con lo pendiente. Crea un dashboard nuevo, por ejemplo con la dirección dashboard-erinnerungen (o dashboard-recordatorios, si cambias también la url de la automatización), y agrega una tarjeta en el editor de código con estas dos baldosas. La roja aparece solo mientras la tarea está pendiente; la otra, cuando ya se cumplió, y las dos muestran desde cuándo:

YAML
type: vertical-stack
cards:
  - type: tile
    entity: input_boolean.spulmaschine_ausraumen
    name: Spülmaschine ausräumen – offen
    icon: mdi:dishwasher-alert
    color: red
    state_content: last_changed
    tap_action:
      action: none
    visibility:
      - condition: state
        entity: input_boolean.spulmaschine_ausraumen
        state: "on"
  - type: tile
    entity: input_boolean.spulmaschine_ausraumen
    name: Spülmaschine ausgeräumt
    icon: mdi:dishwasher
    state_content: last_changed
    tap_action:
      action: none
    visibility:
      - condition: state
        entity: input_boolean.spulmaschine_ausraumen
        state: "off"

Panel de recordatorios antes y después del escaneo: a la izquierda la baldosa roja de pendiente, a la derecha la de tarea cumplida (interfaz en alemán)

Paso 4. La automatización del recordatorio. Crea una automatización nueva, pasa a Editar en YAML y pega el código. Después cambia notify.mobile_app_euer_iphone (aparece cuatro veces) por el servicio de notificación de tu teléfono, que encuentras en Ajustes → Dispositivos y servicios → Mobile App, y pon en tag_id el identificador de tu sticker, no su nombre visible. Los textos de las notificaciones están en alemán: tradúcelos a tu gusto.

YAML
alias: Spülmaschine ausräumen – Erinnerung
description: Erinnert alle 30 Minuten, bis der NFC-Sticker an der Spülmaschine gescannt wird.
mode: queued
triggers:
  - trigger: state
    entity_id: input_boolean.spulmaschine_ausraumen
    to: "on"
    id: erinnern
  - trigger: time_pattern
    minutes: "/30"
    id: erinnern
  - trigger: tag
    tag_id: spuelmaschine
    id: gescannt
actions:
  - choose:
      - alias: Erinnern, solange nicht ausgeräumt
        conditions:
          - condition: trigger
            id: erinnern
          - condition: state
            entity_id: input_boolean.spulmaschine_ausraumen
            state: "on"
          - condition: time
            after: "07:00:00"
            before: "22:00:00"
        sequence:
          - action: notify.mobile_app_euer_iphone
            data:
              title: Spülmaschine ausräumen
              message: Die Spülmaschine ist fertig. Scannt den Sticker, sobald sie leer ist.
              data:
                tag: spuelmaschine
                url: /dashboard-erinnerungen/0
      - alias: Sticker gescannt
        conditions:
          - condition: trigger
            id: gescannt
        sequence:
          - if:
              - condition: state
                entity_id: input_boolean.spulmaschine_ausraumen
                state: "on"
            then:
              - action: input_boolean.turn_off
                target:
                  entity_id: input_boolean.spulmaschine_ausraumen
              - action: notify.mobile_app_euer_iphone
                data:
                  message: clear_notification
                  data:
                    tag: spuelmaschine
              - action: notify.mobile_app_euer_iphone
                data:
                  title: ✓ Erledigt
                  message: Spülmaschine als ausgeräumt markiert.
            else:
              - action: notify.mobile_app_euer_iphone
                data:
                  title: Nichts zu tun
                  message: Die Spülmaschine war schon als ausgeräumt markiert.

Editor de automatizaciones de Home Assistant con los tres disparadores: ayudante encendido, cada 30 minutos y tag escaneado (interfaz en alemán)

Lo que hace cada parte:

  • Los disparadores "interruptor encendido" y "cada 30 minutos" comparten el id erinnern. Así el primer recordatorio llega apenas termina el ciclo y los siguientes, cada media hora.
  • La condición de horario (7:00 a 22:00) respeta la noche. Si la máquina termina a las 23:00, el siguiente aviso llega a las 7:00.
  • El tag: dentro de los datos de la notificación no tiene nada que ver con el sticker: le pone nombre a la notificación, así cada recordatorio nuevo reemplaza al anterior en vez de apilarse. clear_notification borra el último al escanear.
  • url abre el panel del paso 3 al tocar el recordatorio; /0 es la primera vista del dashboard.
  • mode: queued pone en cola un escaneo que llega justo mientras la automatización manda un recordatorio, en vez de perderlo.

Para probar sin esperar un ciclo completo, enciende a mano el interruptor auxiliar: dentro del horario, el recordatorio llega al tiro, y al escanear recibes la confirmación.

Recordatorio en el iPhone que pide escanear el sticker cuando la máquina esté vacía (texto en alemán)

Confirmación en el iPhone de que la tarea quedó marcada como cumplida (texto en alemán)

Receta 2: ¿me tomé la pastilla?

Acá no hay máquina que avise: la tarea se abre sola a las 3:00 de la madrugada y los recordatorios empiezan a las 8:00. La gracia está en un segundo ayudante, un input_datetime que guarda la hora de la toma. Si escaneas el sticker por segunda vez, Home Assistant no cambia nada y te responde cuándo fue la última toma, que es justo la pregunta que querías contestar.

Crea dos ayudantes: un interruptor (en el original, input_boolean.tablette_nehmen) y una fecha y hora (input_datetime.tablette_zuletzt_genommen). La automatización se pega igual que en la receta 1, cambiando los mismos datos:

YAML
alias: Tablette genommen – Erinnerung
description: Ab 3 Uhr offen, erinnert ab 8 Uhr alle 30 Minuten bis zum Scan. Ein zweiter Scan zeigt, wann die Tablette genommen wurde.
mode: queued
triggers:
  - trigger: time
    at: "03:00:00"
    id: start
  - trigger: state
    entity_id: input_boolean.tablette_nehmen
    to: "on"
    id: erinnern
  - trigger: time_pattern
    minutes: "/30"
    id: erinnern
  - trigger: tag
    tag_id: tablette
    id: gescannt
actions:
  - choose:
      - alias: Neuer Tag
        conditions:
          - condition: trigger
            id: start
        sequence:
          - action: input_boolean.turn_on
            target:
              entity_id: input_boolean.tablette_nehmen
      - alias: Erinnern, solange nicht genommen
        conditions:
          - condition: trigger
            id: erinnern
          - condition: state
            entity_id: input_boolean.tablette_nehmen
            state: "on"
          - condition: time
            after: "08:00:00"
            before: "22:00:00"
        sequence:
          - action: notify.mobile_app_euer_iphone
            data:
              title: Tablette nehmen
              message: Die Tablette für heute ist noch offen. Scannt den Sticker an der Box, wenn ihr sie genommen habt.
              data:
                tag: tablette
                url: /dashboard-erinnerungen/0
      - alias: Sticker gescannt
        conditions:
          - condition: trigger
            id: gescannt
        sequence:
          - if:
              - condition: state
                entity_id: input_boolean.tablette_nehmen
                state: "on"
            then:
              - action: input_boolean.turn_off
                target:
                  entity_id: input_boolean.tablette_nehmen
              - action: input_datetime.set_datetime
                target:
                  entity_id: input_datetime.tablette_zuletzt_genommen
                data:
                  datetime: "{{ now().strftime('%Y-%m-%d %H:%M:%S') }}"
              - action: notify.mobile_app_euer_iphone
                data:
                  message: clear_notification
                  data:
                    tag: tablette
              - action: notify.mobile_app_euer_iphone
                data:
                  title: ✓ Erledigt
                  message: "Tablette um {{ now().strftime('%H:%M') }} Uhr als genommen markiert."
            else:
              - action: notify.mobile_app_euer_iphone
                data:
                  title: Schon erledigt
                  message: "Die Tablette wurde bereits am {{ as_timestamp(states('input_datetime.tablette_zuletzt_genommen')) | timestamp_custom('%d.%m. um %H:%M') }} Uhr genommen."

Si no tomas la pastilla antes de las 22:00, el interruptor queda encendido y los recordatorios vuelven a las 8:00 del día siguiente. Después de crear la automatización, enciende el interruptor a mano una vez; si no, el primer día queda cerrado hasta las 3:00 y tu primer escaneo respondería que ya estaba hecho. Y una aclaración necesaria: esto ayuda a acordarse, pero no reemplaza las indicaciones de tu médico ni un pastillero con alarma si la toma es crítica.

Límites que conviene conocer

  • Un escaneo vale para toda la casa. El primero que escanea cumple la tarea por todos. Para avisar a más de un celular, duplica cada acción notify con el servicio del otro teléfono.
  • Solo cuentan los teléfonos con la app conectada. Un celular sin la app de Home Assistant no dispara nada, y cada teléfono tiene que permitir cada sticker una vez.
  • Sin celular, no hay escaneo. Los niños o los adultos mayores sin teléfono quedan fuera, salvo que instales un lector fijo (ver la primera variante).

Variantes y mejoras

  • Una estación de registro sin celular. Monta un ESP32 con un lector RC522 o PN532 junto a la puerta o al pastillero y prográmalo con ESPHome: sus componentes rc522 y pn532 tienen el disparador on_tag, que puede llamar a homeassistant.tag_scanned con el identificador de la tarjeta. Para Home Assistant es un escaneo más, así que las automatizaciones de arriba funcionan sin tocarlas, y basta acercar una tarjeta o un llavero.
  • Confirmación en el mismo lugar. Suma un buzzer o un LED a esa estación para que confirme el registro ahí mismo, sin mirar el teléfono.
  • Más tareas con el mismo patrón. Cambiar el filtro del purificador, regar las plantas, sacar la basura el día que pasa el camión: cualquier tarea que se hace en un lugar fijo encaja con interruptor, reloj y sticker.

Personalización para Chile

En Chile consigues lo necesario para escribir los stickers en MechatronicStore:

  • Módulo NFC RFID PN532 (SKU GQ2-8): el lector y escritor que usa el script de Python por I²C.
  • Raspberry Pi 4 Model B de 8GB RAM (SKU M-204): corre el script de escritura (y, si quieres, el propio Home Assistant).
  • Cables Hembra Hembra 40 piezas 20cm (SKU C-413): para conectar los pines del PN532 al conector de la Raspberry Pi.

El original usa una Raspberry Pi 5; la 4 funciona igual con el mismo script. Los stickers deben ser NTAG213 (o NTAG215): las etiquetas MIFARE Classic de 1K no sirven para este proyecto, porque el iPhone no las lee. Para la variante sin celular, el Módulo Lector RFID NFC RC522 (SKU X8-8) y un ESP32 del catálogo son el punto de partida.

Recursos

Versión chilena con componentes en stock local en MechatronicStore.