Connect a Sure Petcare cat flap to Gladys via MQTT

The Context

I migrated all my home automation from Home Assistant to Gladys. One of my sacrifices: the Sure Petcare connected cat flap (Cat Flap Connect) which doesn’t have a native integration in Gladys.

On Home Assistant, the Sure Petcare integration reported the cat’s position (inside/outside), the cat flap’s battery, the lock status, etc. By switching to Gladys, I lost all of that.

The solution? The same principle as for my Magic Home LED strips: a custom MQTT bridge that links the Sure Petcare cloud API to Gladys.

The bridge was developed with Claude Code (Anthropic’s CLI agent). I’m not hiding it, it’s now part of my stack and it saved me a considerable amount of time on the API research + coding part.

Update
GitHub repo available:

The Architecture

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

A single Python container (~50 MB RAM) that:

  1. Polls the Sure Petcare API every 60 seconds via the surepy library (the same one Home Assistant used)

  2. Publishes to MQTT the state to Gladys: cat position, cat flap battery

  3. Smart logging: only displays state changes (no log spam)

What You Need

  • Gladys with the MQTT integration configured and connected to a Mosquitto broker

  • A Sure Petcare account (the one from the smartphone app)

  • A Cat Flap Connect or Pet Door Connect cat flap with its Hub connected to WiFi

  • Docker on your server

Step 1: Prepare the Files

Create a folder for the bridge, for example /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

Polls Sure Petcare cloud API via surepy and publishes pet/device state
to Gladys via MQTT topics.

Environment variables:
  SUREPETCARE_EMAIL       (required)
  SUREPETCARE_PASSWORD    (required)
  MQTT_HOST               (default: localhost)
  MQTT_PORT               (default: 1883)
  POLL_INTERVAL           (default: 60, seconds)
  LOG_LEVEL               (default: 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

# --- Configuration ---

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 and SUREPETCARE_PASSWORD required')

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):
    """Name -> 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):
    """Publishes a value to the Gladys MQTT topic."""
    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 connected ({MQTT_HOST}:{MQTT_PORT})')
    mqtt.on_disconnect = lambda *_: log.warning('MQTT disconnected, auto-reconnecting...')
    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] MQTT connection failed: {e}')

    mqtt.loop_start()

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

    log.info('Sure Petcare Bridge v1.0 started')
    log.info(f'Account: {EMAIL} | poll: {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'Discovered: {len(pets)} pet(s), {len(flaps)} cat flap(s), {len(hubs)} hub(s)')
                    for p in pets:
                        log.info(f'  Pet: {p.name} (id={p.id})')
                    for f in flaps:
                        log.info(f'  Cat flap: {f.name} (id={f.id})')
                    first_run = False

                # Pets
                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 = 'inside' if at_home else 'outside'
                        log.info(f'[PET] {entity.name}: {loc}')

                # Flaps
                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}: battery={battery}% lock={lock_label}')

            except Exception as e:
                log.error(f'Poll error: {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('Stopped.')

if __name__ == '__main__':
    main()

Step 2: Build and Run

# Build the image
docker build -t sure-petcare-bridge /path/to/sure-petcare-bridge/

# Run the container
docker run -d \
  --name sure-petcare-bridge \
  --network host \
  --restart unless-stopped \
  -e SUREPETCARE_EMAIL=your@email.com \
  -e SUREPETCARE_PASSWORD=your_password \
  -e MQTT_HOST=localhost \
  -e MQTT_PORT=1883 \
  -e POLL_INTERVAL=60 \
  sure-petcare-bridge:latest

```> **Security**: Never hardcode your credentials in the code. Always use environment variables.

### **Check the logs**

docker logs sure-petcare-bridge


You should see:

12:15:21 [sure-bridge] MQTT connected (localhost:1883)
12:15:21 [sure-bridge] Sure Petcare Bridge v1.0 started
12:15:21 [sure-bridge] Account: your@email.com | poll: 60s
12:15:23 [sure-bridge] Discovered: 1 animal(s), 1 cat flap(s), 1 hub(s)
12:15:23 [sure-bridge] Animal: Arwen (id=12345)
12:15:23 [sure-bridge] Cat flap: CatDoor (id=67890)
12:15:23 [sure-bridge] [PET] Arwen: inside
12:15:23 [sure-bridge] [FLAP] CatDoor: battery=51% lock=Unlocked


> The `404` warning on `/api/report/household/` is normal — this is an optional API endpoint from Sure Petcare that surepy tries to call. It doesn’t affect anything.

## **Step 3: Create the MQTT devices in Gladys**

### **Device 1 — Your cat**

Go to **Integrations → MQTT → Devices → New +**

| Field | Value |
|:---|:---|
| Name | `Arwen` (or your cat’s name) |
| External ID | `mqtt:house:pet-arwen` |
| Room | (where your cat hangs out the most) |

**Feature to add:**

| Field | Value |
|:---|:---|
| Name | `Presence` |
| External ID | `mqtt:house:pet-arwen:presence` |
| Category | Presence sensor |
| Type | Binary |
| Is this a sensor? | **Yes** |
| Min | 0 |
| Max | 1 |

> **Naming convention**: The external ID of the device and the feature must exactly match what the bridge publishes. The format is `mqtt:house:pet-{name-in-lowercase}`. If your cat is called “Moustache”, it will be `mqtt:house:pet-moustache`.

### **Device 2 — The cat flap**

| Field | Value |
|:---|:---|
| Name | `Cat flap` |
| External ID | `mqtt:house:catflap-catdoor` |
| Room | (where the cat flap is located) |

**Feature to add:**

| Field | Value |
|:---|:---|
| Name | `Battery` |
| External ID | `mqtt:house:catflap-catdoor:battery` |
| Category | Battery |
| Type | Integer |
| Is this a sensor? | **Yes** |
| Min | 0 |
| Max | 100 |
| Unit | % |

> **Tip**: Check the bridge logs on first startup to find the correct slugs. The bridge displays the name of each detected animal and cat flap.

## **Step 4: Check in the dashboard**

Add the widgets to your Gladys dashboard:

* A widget with the cat’s “Presence” feature — will display 1 (inside) or 0 (outside)

* A “Gauge” widget for the cat flap battery

The bridge polls every 60 seconds. When your cat passes through the cat flap, the value changes at the next poll cycle.

## **How it works**

### **The data flow**

1. Your cat passes through the cat flap

2. The cat flap detects the chip (or RFID tag) and sends the info to the Sure Petcare Hub

3. The Hub transmits to the Sure Petcare cloud

4. The bridge polls the cloud API via **surepy** (the same library that Home Assistant used)

5. The bridge publishes the new state on MQTT: `gladys/master/device/mqtt:house:pet-arwen/feature/mqtt:house:pet-arwen:presence/state` → `0` (outside)

6. Gladys receives the message and updates the dashboard

### **Why cloud polling?**

Sure Petcare does not expose a local API. Everything goes through their cloud. This is the same limitation we had on Home Assistant. If the Sure Petcare servers go down, no more info upload (but the smartphone app will also be down).

Polling every 60 seconds is a good compromise: fast enough to know where the cat is, not aggressive enough to be rate-limited by the API.

### **The change cache**

The bridge does not spam the logs. It keeps the last state in memory and only logs when something changes. MQTT messages are always published (so Gladys always has the latest value), but the logs remain clean.

## **Compatible devices**

The bridge automatically detects all devices in your Sure Petcare account:

* **Cat Flap Connect** (chip cat flap)

* **Pet Door Connect** (pet door)

* **Hub** (WiFi gateway)

* **Feeder Connect** / **Felaqua** (feeder/fountain — not yet exposed in the bridge but easy to add)

If you have multiple cats, the bridge will create an MQTT topic per animal. You just need to create one Gladys device per cat.

## **Why MQTT and not Matter / Matterbridge?**

We also have a custom Matterbridge plugin for our Magic Home LED strips, so the question came up. We seriously studied the option before going with MQTT. Here’s why we didn’t choose Matter for the cat flap:

Out of the 3 pieces of info we wanted to report, here’s what Matter supports on the Gladys side today:

| Need | Matter Cluster | Supported by Gladys? |
|:---|:---|:---|
| Cat inside/outside | `BooleanState` (ContactSensor) | Yes — read as a binary |
| Battery | `PowerSource` | **No** — not yet mapped in Gladys |
| Last passage | no Matter equivalent | Not applicable |

In short, **1 feature out of 3** would go through Matter. The battery would have required Gladys to implement the `PowerSource` cluster (it’s standard, it will probably come), and the “last passage” simply has no equivalent in the Matter protocol.

So, doing a hybrid Matterbridge + MQTT to compensate for the gaps was a lot of tinkering for not much. The pure MQTT bridge covers 100% of the need from day one, without waiting for features to be added on the Gladys or Matter side.

And let’s be honest: for cloud polling every 60 seconds on a cat flap, Matter brings no advantage. Matter is designed for real-time local control — this is just sensor data reporting from a third-party cloud.

## **Limitations**

* **Cloud only**: no local control, dependency on Sure Petcare servers

* **Latency**: between 0 and 60 seconds depending on the poll time (configurable via `POLL_INTERVAL`)

* **Read-only**: the bridge does not yet handle locking/unlocking the cat flap from Gladys. It is technically possible with surepy, if it interests people I can add the feature

* **No last passage**: Gladys does not have a native “timestamp” feature. However, the presence feature in Gladys records when the value changed — this is effectively your “last passage”

## **Technical stack**

* **Python 3.12** (Docker slim)

* **surepy** v0.9.0 — Python library for the Sure Petcare API (the same as Home Assistant)

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

* **~50 MB RAM** in operation

---

Successfully tested on a Synology DS1520+ NAS with Gladys v4, Mosquitto, and a Cat Flap Connect + Sure Petcare Hub.

If you have any questions or suggestions for improvement, feel free to ask!

You’re on fire @David-Digitis !
Great job on this top-notch tutorial :ok_hand:
Well, I won’t need it since I don’t have a cat or a cat flap :rofl:

Quick question: since you made your LEDs tutorial in node.js, why not continue and switch to Python?
I’m asking because Gladys is in

Hello :wink:

Honestly, when you’re well organized/trained with agentic AIs, you can do a lot of things very well and very quickly. I don’t deserve much credit, apart from having the architectural ideas.

As for the language used, it’s simply because we use an existing library. The same one used by HA.

While I’m writing this reply, Claude is creating a repo on my GitHub :laughing: