Conectar una gatera Sure Petcare a Gladys vía MQTT

El contexto

He migrado toda mi domótica de Home Assistant a Gladys. Uno de mis sacrificios: la gatera conectada Sure Petcare (Cat Flap Connect) que no tiene integración nativa en Gladys.

En Home Assistant, la integración Sure Petcare reportaba la posición del gato (dentro/fuera), la batería de la gatera, el estado del cerrojo, etc. Al pasar a Gladys, lo perdí todo.

La solución: el mismo principio que para mis tiras LED Magic Home: un puente MQTT personalizado que hace de enlace entre la API en la nube de Sure Petcare y Gladys.

El puente se desarrolló con Claude Code (el agente CLI de Anthropic). No me lo oculto, forma parte de mi pila ahora y me ahorró un tiempo considerable en la parte de investigación de la API + código.

Actualización
Repositorio de GitHub disponible:

La arquitectura

Sure Petcare Cloud <--HTTPS--> sure-petcare-bridge (Python) <--MQTT--> Mosquitto <--> Gladys

Un solo contenedor Python (~50 Mo RAM) que:

  1. Consulta la API de Sure Petcare cada 60 segundos mediante la librería surepy (la misma que usaba Home Assistant)

  2. Publica en MQTT el estado hacia Gladys: posición del gato, batería de la gatera

  3. Registro inteligente: solo muestra los cambios de estado (sin spam en los registros)

Lo que necesitas

  • Gladys con la integración MQTT configurada y conectada a un broker Mosquitto

  • Una cuenta Sure Petcare (la misma de la aplicación móvil)

  • Una gatera Cat Flap Connect o Pet Door Connect con su Hub conectado al WiFi

  • Docker en tu servidor

Paso 1: Preparar los archivos

Crea una carpeta para el puente, por ejemplo /volume1/docker/sure-petcare-bridge/.

requirements.txt:

surepy>=0.9.0
paho-mqtt>=2.0

Dockerfile:

FROM python:3.12-slim

WORKDIR /app

RUN apt-get update && \
    apt-get install -y --no-install-recommends gcc libc6-dev && \
    rm -rf /var/lib/apt/lists/*

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

RUN apt-get purge -y gcc libc6-dev && apt-get autoremove -y

COPY bridge.py .

CMD ["python", "-u", "bridge.py"]

bridge.py:

#!/usr/bin/env python3
"""
Sure Petcare MQTT Bridge for Gladys Assistant v1.0

Consulta la API en la nube de Sure Petcare mediante surepy y publica el estado de la mascota/dispositivo
en Gladys mediante temas MQTT.

Variables de entorno:
  SUREPETCARE_EMAIL       (requerido)
  SUREPETCARE_PASSWORD    (requerido)
  MQTT_HOST               (por defecto: localhost)
  MQTT_PORT               (por defecto: 1883)
  POLL_INTERVAL           (por defecto: 60, segundos)
  LOG_LEVEL               (por defecto: INFO)
"""

import os
import sys
import signal
import asyncio
import logging

import paho.mqtt.client as paho
from surepy import Surepy
from surepy.enums import EntityType

# --- Configuración ---

EMAIL = os.environ.get('SUREPETCARE_EMAIL', '')
PASSWORD = os.environ.get('SUREPETCARE_PASSWORD', '')
MQTT_HOST = os.environ.get('MQTT_HOST', 'localhost')
MQTT_PORT = int(os.environ.get('MQTT_PORT', '1883'))
POLL_INTERVAL = int(os.environ.get('POLL_INTERVAL', '60'))
LOG_LEVEL = os.environ.get('LOG_LEVEL', 'INFO')

if not EMAIL or not PASSWORD:
    sys.exit('[FATAL] SUREPETCARE_EMAIL y SUREPETCARE_PASSWORD requeridos')

logging.basicConfig(
    level=getattr(logging, LOG_LEVEL.upper(), logging.INFO),
    format='%(asctime)s [%(name)s] %(message)s',
    datefmt='%H:%M:%S',
)
log = logging.getLogger('sure-bridge')


def slugify(name):
    """Nombre -> slug compatible external_id Gladys."""
    s = name.lower().replace(' ', '-').replace("'", '').replace('"', '')
    for old, new in {'e': 'e', 'e': 'e', 'e': 'e', 'a': 'a', 'u': 'u', 'c': 'c'}.items():
        s = s.replace(old, new)
    return s


def publish(client, device_ext, feature_suffix, value):
    """Publica un valor en el tema MQTT Gladys."""
    feat_ext = f'{device_ext}:{feature_suffix}'
    topic = f'gladys/master/device/{device_ext}/feature/{feat_ext}/state'
    client.publish(topic, str(value))


_prev_state = {}

def state_changed(key, **kwargs):
    prev = _prev_state.get(key)
    if prev != kwargs:
        _prev_state[key] = kwargs
        return True
    return False


async def run():
    # MQTT
    try:
        mqtt = paho.Client(paho.CallbackAPIVersion.VERSION2, client_id='sure-petcare-bridge')
    except (AttributeError, TypeError):
        mqtt = paho.Client(client_id='sure-petcare-bridge')

    mqtt.on_connect = lambda *_: log.info(f'MQTT conectado ({MQTT_HOST}:{MQTT_PORT})')
    mqtt.on_disconnect = lambda *_: log.warning('MQTT desconectado, reconexión automática...')
    mqtt.reconnect_delay_set(min_delay=1, max_delay=30)

    try:
        mqtt.connect(MQTT_HOST, MQTT_PORT)
    except Exception as e:
        sys.exit(f'[FATAL] Conexión MQTT imposible: {e}')

    mqtt.loop_start()

    # Sure Petcare
    surepy = Surepy(email=EMAIL, password=PASSWORD)

    log.info('Sure Petcare Bridge v1.0 iniciado')
    log.info(f'Cuenta: {EMAIL} | consulta: {POLL_INTERVAL}s')

    first_run = True

    try:
        while True:
            try:
                entities = await surepy.get_entities()

                if first_run:
                    pets = [e for e in entities.values() if e.type == EntityType.PET]
                    flaps = [e for e in entities.values()
                             if e.type in (EntityType.CAT_FLAP, EntityType.PET_FLAP)]
                    hubs = [e for e in entities.values() if e.type == EntityType.HUB]
                    log.info(f'Descubierto: {len(pets)} animal(es), {len(flaps)} gatera(s), {len(hubs)} hub(s)')
                    for p in pets:
                        log.info(f'  Animal: {p.name} (id={p.id})')
                    for f in flaps:
                        log.info(f'  Gatera: {f.name} (id={f.id})')
                    first_run = False

                # Mascotas
                for entity in entities.values():
                    if entity.type != EntityType.PET:
                        continue
                    slug = slugify(entity.name)
                    ext_id = f'mqtt:maison:pet-{slug}'
                    at_home = 1 if entity.at_home else 0
                    publish(mqtt, ext_id, 'presence', at_home)

                    if state_changed(f'pet-{entity.id}', at_home=at_home):
                        loc = 'dentro' if at_home else 'fuera'
                        log.info(f'[PET] {entity.name}: {loc}')

                # Gateras
                for entity in entities.values():
                    if entity.type not in (EntityType.CAT_FLAP, EntityType.PET_FLAP):
                        continue
                    slug = slugify(entity.name)
                    ext_id = f'mqtt:maison:chatiere-{slug}'

                    battery = getattr(entity, 'battery_level', None)
                    if battery is not None:
                        publish(mqtt, ext_id, 'battery', battery)

                    lock_state = getattr(entity, 'state', None)
                    lock_label = str(lock_state) if lock_state is not None else '?'

                    if state_changed(f'flap-{entity.id}', battery=battery, lock=lock_label):
                        log.info(f'[FLAP] {entity.name}: batería={battery}% cerrojo={lock_label}')

            except Exception as e:
                log.error(f'Error de consulta: {e}')

            await asyncio.sleep(POLL_INTERVAL)

    finally:
        mqtt.loop_stop()
        mqtt.disconnect()


def main():
    signal.signal(signal.SIGTERM, lambda *_: sys.exit(0))
    try:
        asyncio.run(run())
    except (KeyboardInterrupt, SystemExit):
        log.info('Detención.')

if __name__ == '__main__':
    main()

Paso 2: Construir y ejecutar

# Construir la imagen
docker build -t sure-petcare-bridge /ruta/vers/sure-petcare-bridge/

# Ejecutar el contenedor
docker run -d \
  --name sure-petcare-bridge \
  --network host \
  --restart unless-stopped \
  -e SUREPETCARE_EMAIL=tu@email.com \
  -e SUREPETCARE_PASSWORD=tu_contraseña \
  -e MQTT_HOST=localhost \
  -e MQTT_PORT=1883 \
  -e POLL_INTERVAL=60 \
  sure-petcare-bridge:latest

```> **Seguridad**: nunca coloque sus credenciales en el código. Siempre en variables de entorno.

### **Verificar los registros**

docker logs sure-petcare-bridge


Debería ver:

12:15:21 [sure-bridge] MQTT conectado (localhost:1883)
12:15:21 [sure-bridge] Sure Petcare Bridge v1.0 iniciado
12:15:21 [sure-bridge] Cuenta: su@email.com | poll: 60s
12:15:23 [sure-bridge] Descubierto: 1 animal(es), 1 gatera(s), 1 hub(s)
12:15:23 [sure-bridge] Animal: Arwen (id=12345)
12:15:23 [sure-bridge] Gatera: CatDoor (id=67890)
12:15:23 [sure-bridge] [PET] Arwen: dentro
12:15:23 [sure-bridge] [FLAP] CatDoor: batería=51% bloqueo=Desbloqueado


> La advertencia `404` en `/api/report/household/` es normal — es un endpoint opcional de la API Sure Petcare que surepy intenta llamar. No afecta nada.

## **Paso 3: Crear los dispositivos MQTT en Gladys**

### **Dispositivo 1 — Su gato**

Vaya a **Integrations → MQTT → Dispositivos → Nuevo +**

| Campo | Valor |
|:---|:---|
| Nombre | `Arwen` (o el nombre de su gato) |
| ID externo | `mqtt:casa:pet-arwen` |
| Habitación | (donde su gato pasa más tiempo) |

**Característica a agregar:**

| Campo | Valor |
|:---|:---|
| Nombre | `Presencia` |
| ID externo | `mqtt:casa:pet-arwen:presence` |
| Categoría | Sensor de presencia |
| Tipo | Binario |
| ¿Es un sensor? | **Sí** |
| Min | 0 |
| Max | 1 |

> **Convención de nombres**: el ID externo del dispositivo y la característica deben coincidir exactamente con lo que el bridge publica. El formato es `mqtt:casa:pet-{nombre-en-minúsculas}`. Si su gato se llama « Moustache », será `mqtt:casa:pet-moustache`.

### **Dispositivo 2 — La gatera**

| Campo | Valor |
|:---|:---|
| Nombre | `Gatera` |
| ID externo | `mqtt:casa:gatera-catdoor` |
| Habitación | (donde se encuentra la gatera) |

**Característica a agregar:**

| Campo | Valor |
|:---|:---|
| Nombre | `Batería` |
| ID externo | `mqtt:casa:gatera-catdoor:battery` |
| Categoría | Batería |
| Tipo | Entero |
| ¿Es un sensor? | **Sí** |
| Min | 0 |
| Max | 100 |
| Unidad | % |

> **Consejo**: consulte los registros del bridge en el primer arranque para encontrar los slugs correctos. El bridge le muestra el nombre de cada animal y gatera detectados.

## **Paso 4: Verificar en el panel de control**

Agregue los widgets en su panel de control Gladys:

* Un widget con la característica «Presencia» del gato — mostrará 1 (dentro) o 0 (fuera)

* Un widget «Medidor» para la batería de la gatera

El bridge realiza un sondeo cada 60 segundos. Cuando su gato pasa por la gatera, el valor cambia en el próximo ciclo de sondeo.

## **¿Cómo funciona?**

### **El flujo de datos**

1. Su gato pasa por la gatera

2. La gatera detecta el chip (o la etiqueta RFID) y envía la información al Hub Sure Petcare

3. El Hub transmite al cloud Sure Petcare

4. El bridge sondea la API cloud a través de **surepy** (la misma biblioteca que usaba Home Assistant)

5. El bridge publica el nuevo estado en MQTT: `gladys/master/device/mqtt:casa:pet-arwen/feature/mqtt:casa:pet-arwen:presence/state` → `0` (fuera)

6. Gladys recibe el mensaje y actualiza el panel de control

### **¿Por qué el sondeo en la nube?**

Sure Petcare no expone una API local. Todo pasa por su nube. Es la misma limitación que teníamos en Home Assistant. Si los servidores Sure Petcare caen, no habrá información de retorno (pero la aplicación móvil también estará fuera de servicio).

El sondeo cada 60 segundos es un buen compromiso: lo suficientemente rápido para saber dónde está el gato, no lo suficientemente agresivo como para ser limitado por la API.

### **La caché de cambios**

El bridge no satura los registros. Mantiene en memoria el último estado y solo registra cuando algo cambia. Los mensajes MQTT siempre se publican (para que Gladys tenga siempre el último valor), pero los registros permanecen limpios.

## **Dispositivos compatibles**

El bridge detecta automáticamente todos los dispositivos de su cuenta Sure Petcare:

* **Cat Flap Connect** (gatera con chip)

* **Pet Door Connect** (puerta para animales)

* **Hub** (puente WiFi)

* **Feeder Connect** / **Felaqua** (comederos/bebederos — aún no expuestos en el bridge, pero fáciles de agregar)

Si tiene varios gatos, el bridge creará un tema MQTT por animal. Solo necesita crear un dispositivo Gladys por gato.

## **¿Por qué MQTT y no Matter / Matterbridge?**

También tenemos un plugin Matterbridge personalizado para nuestras tiras LED Magic Home, por lo que la pregunta surgió. Lo estudiamos seriamente antes de optar por MQTT. Aquí está por qué no elegimos Matter para la gatera:

De las 3 informaciones que queríamos subir, aquí está lo que Matter soporta en Gladys hoy:

| Necesidad | Cluster Matter | ¿Soportado por Gladys? |
|:---|:---|:---|
| Gato dentro/fuera | `BooleanState` (ContactSensor) | Sí — leído como binario |
| Batería | `PowerSource` | **No** — aún no mapeado en Gladys |
| Último paso | ningún equivalente Matter | No aplicable |

En resumen, **1 característica de 3** pasaba por Matter. La batería habría requerido que Gladys implementara el cluster `PowerSource` (es estándar, probablemente llegará), y el «último paso» simplemente no tiene equivalente en el protocolo Matter.

Por lo tanto, hacer un híbrido Matterbridge + MQTT para compensar las carencias, era un bricolaje por poco. El bridge MQTT puro cubre el 100% de las necesidades desde el primer día, sin esperar a que las características se agreguen en Gladys o en Matter.

Y seamos honestos: para un sondeo en la nube cada 60 segundos en una gatera, Matter no aporta ninguna ventaja. Matter está hecho para el control local en tiempo real — aquí es solo la subida de sensores desde una nube de terceros.

## **Limitaciones**

* **Solo nube**: sin control local, dependencia de los servidores Sure Petcare

* **Latencia**: entre 0 y 60 segundos según el momento del sondeo (configurable mediante `POLL_INTERVAL`)

* **Solo lectura**: el bridge no gestiona (todavía) el bloqueo/desbloqueo de la gatera desde Gladys. Es técnicamente posible con surepy, si interesa a alguien, puedo agregar la característica

* **Sin último paso**: Gladys no tiene una característica «timestamp» nativa. Sin embargo, la característica de presencia en Gladys registra cuándo el valor cambió — es de hecho su «último paso»

## **Pila técnica**

* **Python 3.12** (Docker slim)

* **surepy** v0.9.0 — biblioteca Python para la API Sure Petcare (la misma que Home Assistant)

* **paho-mqtt** v2.x — cliente MQTT

* **~50 Mo RAM** en funcionamiento

---

Probado con éxito en un NAS Synology DS1520+ con Gladys v4, Mosquitto, y una Cat Flap Connect + Hub Sure Petcare.

Si tiene preguntas o mejoras que sugerir, no dude en hacerlo.

¡A tope @David-Digitis!
¡Bravo por este tutorial de lujo :ok_hand:
Bueno, no lo necesitaré porque no tengo gato ni gatera :rofl:

Pregunta pequeña: como hiciste el tutorial de tus LEDs en node.js, ¿por qué no seguiste y pasaste a python?
Hago la pregunta porque Gladys está en js y si un día hay una integración, sería aún más fácil usar tal cual (o casi) que tener que convertir.

Y me preguntaba sobre un repositorio de github donde pondrías directamente el código y una imagen docker (y usar ghcr.io creando un package) ?

Tengo un syno y noté que tú también tenías uno tan pronto como pusiste /volume1/docker/....
Para los no iniciados, no siempre es lo más fácil crear los archivos con el código (python, js, etc.) en una plataforma tipo syno u otra. Por otro lado, lanzar solo un comando docker me parece más simple.
¿Crees que podría ser bueno para tus desarrollos?

Hola :wink:

En realidad, cuando estás bien organizado / formado con las IA analógicas, puedes hacer muchas cosas muy bien y muy rápido. No tengo mucho mérito, excepto tener las ideas de arquitectura.

En cuanto al lenguaje utilizado, es simplemente porque utilizamos una librería existente. La misma que utiliza HA.

Mientras te escribo esta respuesta, Claude está creando un repositorio en mi GitHub :laughing: