Connecter une chatiere Sure Petcare a Gladys via MQTT

Le contexte

J’ai migre toute ma domotique de Home Assistant vers Gladys. Un de mes sacrifices : la chatiere connectee Sure Petcare (Cat Flap Connect) qui n’a pas d’integration native dans Gladys.

Sur Home Assistant, l’integration Sure Petcare remontait la position du chat (dedans/dehors), la batterie de la chatiere, l’etat du verrou, etc. En passant sur Gladys, j’ai perdu tout ca.

La solution ? Le meme principe que pour mes bandeaux LED Magic Home : un bridge MQTT custom qui fait le lien entre l’API cloud Sure Petcare et Gladys.

Le bridge a ete developpe avec Claude Code (l’agent CLI d’Anthropic). Je ne m’en cache pas, ca fait partie de ma stack maintenant et ca m’a fait gagner un temps considerable sur la partie recherche API + code.

Update
Repos Github dispo :

L’architecture

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

Un seul container Python (~50 Mo RAM) qui :

  1. Poll l’API Sure Petcare toutes les 60 secondes via la librairie surepy (la meme que Home Assistant utilisait)

  2. Publie sur MQTT l’etat vers Gladys : position du chat, batterie de la chatiere

  3. Log intelligent : n’affiche que les changements d’etat (pas de spam dans les logs)

Ce qu’il faut

  • Gladys avec l’integration MQTT configuree et connectee a un broker Mosquitto

  • Un compte Sure Petcare (celui de l’app smartphone)

  • Une chatiere Cat Flap Connect ou Pet Door Connect avec son Hub connecte au WiFi

  • Docker sur votre serveur

Etape 1 : Preparer les fichiers

Creez un dossier pour le bridge, par exemple /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 et SUREPETCARE_PASSWORD requis')

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):
    """Nom -> 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):
    """Publie une valeur sur le topic 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 connecte ({MQTT_HOST}:{MQTT_PORT})')
    mqtt.on_disconnect = lambda *_: log.warning('MQTT deconnecte, reconnexion auto...')
    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 connexion impossible: {e}')

    mqtt.loop_start()

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

    log.info('Sure Petcare Bridge v1.0 demarre')
    log.info(f'Compte: {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'Decouvert: {len(pets)} animal(aux), {len(flaps)} chatiere(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'  Chatiere: {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 = 'dedans' if at_home else 'dehors'
                        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}: batterie={battery}% verrou={lock_label}')

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

if __name__ == '__main__':
    main()

Etape 2 : Builder et lancer

# Build l'image
docker build -t sure-petcare-bridge /chemin/vers/sure-petcare-bridge/

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

Securite : ne mettez jamais vos credentials en dur dans le code. Toujours en variables d’environnement.

Verifier les logs

docker logs sure-petcare-bridge

Vous devriez voir :

12:15:21 [sure-bridge] MQTT connecte (localhost:1883)
12:15:21 [sure-bridge] Sure Petcare Bridge v1.0 demarre
12:15:21 [sure-bridge] Compte: votre@email.com | poll: 60s
12:15:23 [sure-bridge] Decouvert: 1 animal(aux), 1 chatiere(s), 1 hub(s)
12:15:23 [sure-bridge]   Animal: Arwen (id=12345)
12:15:23 [sure-bridge]   Chatiere: CatDoor (id=67890)
12:15:23 [sure-bridge] [PET] Arwen: dedans
12:15:23 [sure-bridge] [FLAP] CatDoor: batterie=51% verrou=Unlocked

Le warning 404 sur /api/report/household/ est normal — c’est un endpoint optionnel de l’API Sure Petcare que surepy tente d’appeler. Ca n’affecte rien.

Etape 3 : Creer les devices MQTT dans Gladys

Device 1 — Votre chat

Allez dans Integrations → MQTT → Appareils → Nouveau +

Champ Valeur
Nom Arwen (ou le nom de votre chat)
ID externe mqtt:maison:pet-arwen
Piece (la ou votre chat traine le plus)

Feature a ajouter :

Champ Valeur
Nom Presence
ID externe mqtt:maison:pet-arwen:presence
Categorie Capteur de presence
Type Binaire
Est-ce un capteur ? Oui
Min 0
Max 1

Convention de nommage : l’ID externe du device et de la feature doivent correspondre exactement a ce que le bridge publie. Le format est mqtt:maison:pet-{nom-en-minuscules}. Si votre chat s’appelle « Moustache », ca donnera mqtt:maison:pet-moustache.

Device 2 — La chatiere

Champ Valeur
Nom Chatiere
ID externe mqtt:maison:chatiere-catdoor
Piece (la ou se trouve la chatiere)

Feature a ajouter :

Champ Valeur
Nom Batterie
ID externe mqtt:maison:chatiere-catdoor:battery
Categorie Batterie
Type Entier
Est-ce un capteur ? Oui
Min 0
Max 100
Unite %

Astuce : regardez les logs du bridge au premier demarrage pour trouver les bons slugs. Le bridge vous affiche le nom de chaque animal et chatiere detectes.

Etape 4 : Verifier dans le dashboard

Ajoutez les widgets sur votre dashboard Gladys :

  • Un widget avec la feature « Presence » du chat — affichera 1 (dedans) ou 0 (dehors)

  • Un widget « Jauge » pour la batterie de la chatiere

Le bridge poll toutes les 60 secondes. Quand votre chat passe la chatiere, la valeur change au prochain cycle de poll.

Comment ca marche

Le flux de donnees

  1. Votre chat passe la chatiere

  2. La chatiere detecte la puce (ou le tag RFID) et envoie l’info au Hub Sure Petcare

  3. Le Hub transmet au cloud Sure Petcare

  4. Le bridge poll l’API cloud via surepy (la meme librairie que Home Assistant utilisait)

  5. Le bridge publie le nouvel etat sur MQTT : gladys/master/device/mqtt:maison:pet-arwen/feature/mqtt:maison:pet-arwen:presence/state0 (dehors)

  6. Gladys recoit le message et met a jour le dashboard

Pourquoi du cloud polling ?

Sure Petcare n’expose pas d’API locale. Tout passe par leur cloud. C’est la meme limitation qu’on avait sur Home Assistant. Si les serveurs Sure Petcare tombent, plus de remontee d’info (mais l’app smartphone sera aussi HS).

Le polling a 60 secondes est un bon compromis : assez rapide pour savoir ou est le chat, pas assez agressif pour se faire rate-limit par l’API.

Le cache de changement

Le bridge ne spamme pas les logs. Il garde en memoire le dernier etat et ne log que quand quelque chose change. Les messages MQTT sont toujours publies (pour que Gladys ait toujours la derniere valeur), mais les logs restent propres.

Appareils compatibles

Le bridge detecte automatiquement tous les appareils de votre compte Sure Petcare :

  • Cat Flap Connect (chatiere a puce)

  • Pet Door Connect (porte pour animaux)

  • Hub (passerelle WiFi)

  • Feeder Connect / Felaqua (gamelle/fontaine — pas encore expose dans le bridge mais facile a ajouter)

Si vous avez plusieurs chats, le bridge creera un topic MQTT par animal. Il suffit de creer un device Gladys par chat.

Pourquoi MQTT et pas Matter / Matterbridge ?

On a aussi un plugin Matterbridge custom pour nos bandeaux LED Magic Home, donc la question s’est posee. On a etudie la piste serieusement avant de partir sur MQTT. Voila pourquoi on n’a pas retenu Matter pour la chatiere :

Sur les 3 infos qu’on voulait remonter, voici ce que Matter supporte cote Gladys aujourd’hui :

Besoin Cluster Matter Supporte par Gladys ?
Chat dedans/dehors BooleanState (ContactSensor) Oui — lu comme un binaire
Batterie PowerSource Non — pas encore mappe dans Gladys
Dernier passage aucun equivalent Matter Non applicable

En gros, 1 feature sur 3 passait par Matter. La batterie aurait necessite que Gladys implemente le cluster PowerSource (c’est standard, ca viendra probablement), et le « dernier passage » n’a tout simplement pas d’equivalent dans le protocole Matter.

Du coup, faire un hybride Matterbridge + MQTT pour compenser les manques, c’etait du bricolage pour pas grand-chose. Le bridge MQTT pur couvre 100% du besoin des le premier jour, sans attendre que des features soient ajoutees cote Gladys ou cote Matter.

Et soyons honnetes : pour du cloud polling a 60 secondes sur une chatiere, Matter n’apporte aucun avantage. Matter est fait pour du controle local temps reel — la c’est juste de la remontee de capteurs depuis un cloud tiers.

Limitations

  • Cloud only : pas de controle local, dependance aux serveurs Sure Petcare

  • Latence : entre 0 et 60 secondes selon le moment du poll (configurable via POLL_INTERVAL)

  • Lecture seule : le bridge ne gere pas (encore) le verrouillage/deverrouillage de la chatiere depuis Gladys. C’est techniquement possible avec surepy, si ca interesse du monde je peux ajouter la feature

  • Pas de dernier passage : Gladys n’a pas de feature « timestamp » native. Par contre, la feature presence dans Gladys enregistre quand la valeur a change — c’est de fait votre « dernier passage »

Stack technique

  • Python 3.12 (Docker slim)

  • surepy v0.9.0 — librairie Python pour l’API Sure Petcare (la meme que Home Assistant)

  • paho-mqtt v2.x — client MQTT

  • ~50 Mo RAM en fonctionnement


Teste avec succes sur un NAS Synology DS1520+ avec Gladys v4, Mosquitto, et une Cat Flap Connect + Hub Sure Petcare.

Si vous avez des questions ou des ameliorations a suggerer, n’hesitez pas !

1 « J'aime »

2 « J'aime »

Au taquet @David-Digitis !
Bravo pour ce tuto aux petits oignons :ok_hand:
Bon, je n’en aurai pas besoin car pas de chat ni chatière :rofl:

Petite question : comme tu as fait le tuto de tes LEDs en node.js, pourquoi ne pas avoir continuer et être passé sur python ?
Je pose la question car Gladys est en js et si un jour il y a une intégration, ce serait d’autant plus simple d’utiliser tel quel (ou presque) que de devoir convertir.

Et je me posais la question sur un repo github où tu mettrais directement le code et une image docker (et utiliser ghcr.io en créant un package) ?

J’ai un syno et j’avais remarqué que tu en avais aussi un dès que tu as mis /volume1/docker/....
Pour les non initiés, ce n’est pas forcément le plus simple de créer les fichiers avec le code (pyhton, js, etc.) sur une plateforme type syno ou autre.Par contre lancer juste une commande docker ça me parait plus simple.
Tu penses que ça pourrait être bien pour tes dev ?

Hello :wink:

En vrai, quand tu es bien organisé / formé avec les IA argentiques, tu peux faire beaucoup de choses très bien et très vite. Je n’ai pas beaucoup de mérite, hormis avoir les idées d’architecture.

Pour le langage utilisé, c’est tout simplement parce que nous utilisons une librairie existante. La même utilisée par HA.

Pendant que je t’écris cette réponse Claude est en train de faire un repo sur mon GitHub :laughing:

1 « J'aime »