Contrôler la litière connectée Whisker Litter-Robot depuis votre système domotique via une API Docker

Contrôler la litière connectée Whisker Litter-Robot depuis votre système domotique via une API Docker

Vous avez une litière connectée Whisker Litter-Robot et vous rêvez de l’intégrer à votre système domotique ? Voici comment créer une API légère et dockerisée pour la piloter depuis Jeedom ou Node-RED.
Dans ce tutoriel, nous allons créer une API REST en Python, la conteneuriser avec Docker, puis l'intégrer dans Jeedom pour un contrôle complet de votre litière automatique.


Prérequis

  • Docker et Docker Compose installés
  • Un compte Whisker (application Litter-Robot)
  • Jeedom avec le plugin Script
  • Quelques connaissances de base en ligne de commande
  • Et bien entendu une litière Litter-Robot en version connecté (cet article à était écrit en utilisant une litter robot 3 connect, la version 4 et le feeder robot sont pris en charge également)

Litter-Robot 3 Connect

Acheter sur Amazon

Architecture de la solution

Notre solution se compose de trois éléments principaux :

  1. Une API Python utilisant Flask et la bibliothèque pylitterbot qui se connecte au cloud whisker.
  2. Un conteneur Docker pour isoler et déployer facilement l'API.
  3. L'intégration dans le plugin Script de Jeedom pour contrôler le robot depuis votre interface domotique.
💡
Le code fourni dans cet article a été généré avec l'aide de l'Intelligence Artificielle, il a été testé et est fonctionnel chez moi, mais n'est pas forcément optimisé.

Étape 1 : Création du conteneur Docker et du script Python

Créer un dossier pour le projet Docker à l'emplacement de votre choix :

mkdir litter-robot-api
cd litter-robot-api

Créez un fichier Dockerfile avec la commande nano Dockerfile et collez y le contenu suivant :

FROM python:3.11-slim

# Répertoire de travail
WORKDIR /app

# Installation des dépendances système minimales
RUN apt-get update && apt-get install -y --no-install-recommends \
    curl tzdata \
    && rm -rf /var/lib/apt/lists/*

# Copier et installer les dépendances Python
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# Copier le code source
COPY litterbot_api.py .

# Port exposé
EXPOSE 5001

# Point d'entrée
CMD ["python", "litterbot_api.py"]

Créez le fichier pour définir les routes Flask avec la commande nano requirements.txt et collez le contenu suivant :

flask
pylitterbot

Créez le script python avec la commande nano litterbot_api.py avec le contenu suivant :

from flask import Flask, jsonify
import os, asyncio, ast
from pylitterbot import Account

app = Flask(__name__)

# ========================
# UTILITAIRES
# ========================
async def get_robot():
    """Connecte le compte et retourne (account, robot)."""
    account = Account()
    await account.connect(
        username=os.environ["WHISKER_USERNAME"],
        password=os.environ["WHISKER_PASSWORD"],
        load_robots=True
    )
    if not account.robots:
        await account.disconnect()
        raise ValueError("Aucun robot trouvé pour ce compte")
    robot = account.robots[0]
    return account, robot


def parse_robot_data(robot):
    """Retourne robot._data sous forme de dict exploitable."""
    raw_data = getattr(robot, "_data", {})
    if isinstance(raw_data, str):  # parfois c’est une string
        try:
            raw_data = ast.literal_eval(raw_data)
        except Exception:
            raw_data = {"error": "impossible de parser _data", "raw": raw_data}
    return raw_data


def human_readable_status(robot):
    """Transforme les infos brutes en statut lisible."""
    data = parse_robot_data(robot)

    # ========================
    # Table complète unitStatus → traduction FR
    # ========================
    status_map = {
        "RDY": "Au repos",
        "CCC": "Cycle terminé",
        "CCP": "Nettoyage en cours",
        "CST": "Cycle démarré",
        "CSD": "Vidage de la litière",
        "CSC": "Remplissage / repositionnement",
        "CSP": "Cycle en pause",
        "BR": "Erreur / capot retiré",
        "DFS": "Tiroir plein (capteur activé)",
        "DF1": "Tiroir presque plein (alerte 1)",
        "DF2": "Tiroir plein (alerte 2)",
        "PPR": "Sécurité activée (pinch detect)",
        "OFF": "Éteint",
        "OFFL": "Hors ligne",
        "EC": "Cycle de vidage manuel",
        "CCCW": "Cycle terminé avec avertissement",
        "WFI": "En attente d’action utilisateur",
        "WFS": "Prêt mais cycle pas démarré",
        "UNK": "Statut inconnu",
    }

    raw_status = data.get("unitStatus", "UNK")
    status = status_map.get(raw_status, f"Statut inconnu ({raw_status})")

    drawer_status = "Tiroir plein" if data.get("isDFITriggered") == "1" else "Tiroir OK"
    online_status = "En ligne" if not data.get("didNotifyOffline", False) else "Hors ligne"

    return {
        "statut": status,
        "statut_brut": raw_status,
        "tiroir": drawer_status,
        "en_ligne": online_status,
        "cycle_count": data.get("cycleCount"),
        "cycles_after_drawer_full": data.get("cyclesAfterDrawerFull"),
        "last_seen": data.get("lastSeen"),
    }


# ========================
# ROUTES
# ========================
@app.route("/status", methods=["GET"])
def status_route():
    async def status():
        account, robot = await get_robot()
        try:
            await robot.refresh()
            insight = await robot.get_insight()
            human_status = human_readable_status(robot)
            return {
                "ok": True,
                "robot_name": getattr(robot, "name", "unknown"),
                "statut_humain": human_status,
                "insight": insight,
            }
        finally:
            await account.disconnect()
    return jsonify(asyncio.run(status()))


@app.route("/raw", methods=["GET"])
def raw_route():
    async def raw():
        account, robot = await get_robot()
        try:
            await robot.refresh()
            raw_dict = {k: str(v) for k, v in robot.__dict__.items()}
            return {
                "ok": True,
                "robot_name": getattr(robot, "name", "unknown"),
                "raw": raw_dict,
            }
        finally:
            await account.disconnect()
    return jsonify(asyncio.run(raw()))


@app.route("/data", methods=["GET"])
def data_route():
    async def data_view():
        account, robot = await get_robot()
        try:
            await robot.refresh()
            parsed_data = parse_robot_data(robot)
            return {
                "ok": True,
                "robot_name": getattr(robot, "name", "unknown"),
                "data": parsed_data,
            }
        finally:
            await account.disconnect()
    return jsonify(asyncio.run(data_view()))


# ========================
# ROUTES ACTIONS SIMPLES
# ========================
async def perform_action(action_func):
    account, robot = await get_robot()
    try:
        await action_func(robot)
        return {"ok": True}
    finally:
        await account.disconnect()


@app.route("/clean", methods=["POST"])
def clean_route():
    return jsonify(asyncio.run(perform_action(lambda r: r.start_cleaning())))


@app.route("/pause", methods=["POST"])
def pause_route():
    return jsonify(asyncio.run(perform_action(lambda r: r.pause_cleaning())))


@app.route("/resume", methods=["POST"])
def resume_route():
    return jsonify(asyncio.run(perform_action(lambda r: r.resume_cleaning())))


@app.route("/reset_drawer", methods=["POST"])
def reset_drawer_route():
    return jsonify(asyncio.run(perform_action(lambda r: r.reset_drawer())))


@app.route("/power_off", methods=["POST"])
def power_off_route():
    return jsonify(asyncio.run(perform_action(lambda r: r.turn_off())))


@app.route("/power_on", methods=["POST"])
def power_on_route():
    return jsonify(asyncio.run(perform_action(lambda r: r.turn_on())))


# ========================
# MAIN
# ========================
if __name__ == "__main__":
    app.run(host="0.0.0.0", port=5001)

Étape 2 : Configuration Docker Compose

Créez le fichier compose.yaml avec la commande nano compose.yaml et collez le contenu suivant :

version: "3.9"

services:
  litterbot-api:
    build: .
    container_name: litterbot-api
    restart: unless-stopped
    environment:
      - WHISKER_USERNAME=votre_email_whisker
      - WHISKER_PASSWORD=votre_mot_de_passe
    ports:
      - "5001:5001"
Remplacez votre_email_whisker et votre_mot_de_passe par vos véritables identifiants Whisker.
Si besoin, vous pouvez adapter le port exposé.

Lancez le conteneur avec la commande :

docker-compose up -d

Connectez vous au conteneur pour tester l'API, testez le statut et lancez un nettoyage :

docker exec -it litterbot-api bash
curl http://localhost:5001/status
curl -X POST http://localhost:5001/clean

Une fois les tests locaux réussis, testez depuis votre navigateur :

http://IP_de_votre_serveur:5001/status

Étape 3 : Intégration dans Jeedom

Assurez vous que le plugin Script est installé et activé dans Jeedom.

Créez un nouvel équipement dans le plugin Script, et configurez les commandes pour récupérer les informations :

La première commande permettra de récupérer le statut de la litière et doit être définie de cette manière :

  • Type : Info
  • Sous-type : Chaîne
  • Requête : http://IP_DOCKER:5001/status
  • Parser JSON pour extraire les valeurs souhaitées

Créez ensuite des commandes de type Action et sous-type Défaut pour chaque fonction :

  • Nettoyer : POST http://IP_DOCKER:5001/clean
  • Pause : POST http://IP_DOCKER:5001/pause
  • Reprendre : POST http://IP_DOCKER:5001/resume
  • Reset tiroir : POST http://IP_DOCKER:5001/reset_drawer
  • Éteindre : POST http://IP_DOCKER:5001/power_off
  • Allumer : POST http://IP_DOCKER:5001/power_on

Compatibilité avec d'autres systèmes

Cette API REST est bien entendu compatible avec tout système capable d'exécuter des scripts ou des appels HTTP, comme Node-RED, Home Assistant, Domoticz, ...


Utilisation avancée

Vous pouvez désormais créer des scénarios d’automatisation pour gérer plus facilement la litière. Ces scénarios vous permettront de réduire le temps consacré à cette corvée tout en améliorant le suivi de l’appareil.

La signification des statuts bruts est disponible dans le fichier litterbot_api.py, qui sont plus simple à utiliser dans vos scénarios.

Voici quelques idées d’utilisation :

🕒 Nettoyage automatique à heure fixe

Objectif : déclencher un cycle de nettoyage tous les jours à 22h, après que le chat a fini sa journée.

  • Déclencheur : planifié à 22h.
  • Action : appel de l’API /clean.
  • Option : envoyer une notification si le robot est déjà en cours de nettoyage.

📦 Alerte tiroir plein

Objectif : être prévenu dès que le tiroir est plein pour le vider rapidement.

  • Déclencheur : API status_brut == "DF1".
  • Action : envoi d’une notification Jeedom (SMS, push, email).
  • Option : afficher un message sur ton dashboard ou allumer une LED via un module domotique.

💤 Mode nuit silencieux

Objectif : éviter que le robot ne nettoie pendant la nuit pour ne pas réveiller les humains (ou effrayer le chat).

  • Déclencheur : à 23h et 7h.
  • Action : appel de l’API /pause à 23h, puis /resume à 7h.
  • Alternative : couper l’alimentation avec une prise connectée.

🧹 Nettoyage après détection de passage

Objectif : lancer un nettoyage automatique après que le chat est passé.

  • Déclencheur : capteur de mouvement devant la Litter-Robot.
  • Condition : attendre 5 minutes pour laisser le chat tranquille.
  • Action : appel de l’API /clean.

🧼 Réinitialisation automatique du tiroir

Objectif : remettre à zéro le compteur du tiroir après chaque vidage manuel.

  • Déclencheur : bouton physique ou scénario manuel dans Jeedom.
  • Action : appel de l’API /reset_drawer.

🔌 Extinction automatique en cas d’absence

Objectif : couper le robot quand personne n’est à la maison.

  • Déclencheur : mode "absent" dans Jeedom.
  • Action : appel de l’API /power_off.
  • Option : rallumer automatiquement à ton retour avec /power_on.

Conclusion

Cette solution vous offre un contrôle complet de votre Litter Robot depuis Jeedom ou tout autre système domotique. L'architecture modulaire permet facilement d'ajouter de nouvelles fonctionnalités ou d'adapter l'API à vos besoins spécifiques.

Le conteneur Docker garantit une installation rapide et portable, tandis que l'API REST assure une compatibilité maximale avec les systèmes existants.

N'hésitez pas à personnaliser ce code selon vos besoins et à partager vos améliorations ou venir chercher de l'aide avec la communauté sur le groupe Telegram ou bien en commentaire !