Aller au contenu principal

Synchronisation temps réel et bus d'événements

Le pilotage domotique de leukOS utilise une architecture hybride : REST admet la commande, puis WebSocket diffuse son cycle de vie. Le daemon C++ reste l'autorité pour l'ordonnancement, l'exécution par le MCU et la persistance SQLite. QML, Vue.js et Android consomment le même contrat d'événement et appliquent le même comportement visuel.

Cette séparation évite de maintenir une requête HTTP ouverte pendant le traitement du matériel et permet à plusieurs interfaces de recevoir simultanément le même état.

Architecture​

Le hub Go ne transforme pas les événements : il expose un proxy WebSocket pour les clients web. Les clients locaux QML et Android peuvent se connecter directement au daemon.

Cycle d'une commande​

Une commande est retirée de la file après un succès ou un échec définitif. Les erreurs intermédiaires restent associées au même event_id.

Contrat d'événement​

Toutes les interfaces reçoivent le même objet JSON :

{
"event_type": "command_status",
"event_id": "1792093845123-42",
"domain": "module",
"target": "m_001122aabbcc",
"action": "on",
"status": "retrying",
"attempt": 2,
"max_attempts": 4,
"timestamp_ms": 1792093845123,
"error": "relay command rejected"
}
ChampDescription
event_typeType stable, actuellement command_status.
event_idIdentifiant unique conservé pendant toute la commande.
domainFamille de cible : module, relay ou valve.
targetIdentifiant stable permettant au composant UI de reconnaître l'événement.
actionAction demandée : par exemple on, off, open ou close.
statusÉtat courant du traitement.
attemptNuméro de la tentative courante ; 0 pendant pending.
max_attemptsTentative initiale plus le nombre de retries autorisés.
timestamp_msHorodatage Unix en millisecondes.
errorMessage optionnel présent lors de retrying ou failed.

États​

ÉtatSignificationComportement de l'interface
pendingLa commande est acceptée et placée dans la file.Loader et bouton bloqué.
processingLe dispatcher appelle le matériel.Loader conservé.
retryingUne tentative a échoué, une autre est planifiée.Loader et progression conservés.
succeededLe matériel a accepté et appliqué la commande.Fin du pending et état actualisé.
failedToutes les tentatives ont échoué.Bouton réactivé et erreur dans le label d'état.

REST et WebSocket​

Les commandes utilisent toujours les routes REST existantes. La réponse HTTP signifie uniquement que la commande a été admise dans le dispatcher :

{
"ok": true,
"event_id": "1792093845123-42",
"status": "pending"
}

Elle ne prouve pas que le MCU a exécuté l'action. Seuls les événements WebSocket succeeded ou failed sont définitifs.

EndpointRôle
GET /eventsUpgrade WebSocket direct sur le daemon C++.
GET /api/domotics/eventsProxy WebSocket du hub Go pour l'IHM Vue.js.
GET /command-event/{event_id}Relecture durable de l'état courant depuis SQLite.
GET /healthSanté de SQLite, du dispatcher et taille de la file.
GET /domotics/settingsLecture des paramètres du dispatcher.
POST /domotics/settingsApplication et persistance des paramètres.

Gestion du bouton et du timeout​

Lors d'un appui ON/OFF :

  1. le composant se désactive immédiatement pour empêcher les rafales ;
  2. un délai de 10 secondes démarre ;
  3. le composant attend un événement pending, processing ou retrying qui prouve que la commande est prise en charge ;
  4. après cet accusé, le composant affiche un loader et reste désactivé jusqu'à l'état final ;
  5. sans accusé après 10 secondes, le bouton est réactivé et le label habituellement utilisé pour « Allumé / Éteint » affiche l'erreur ;
  6. en cas de failed, le même label affiche le message transmis par le dispatcher, sans masquer ni supprimer la carte.

Pour éviter un faux timeout lors d'une reconnexion WebSocket, la réponse REST associe la commande à son event_id. Si aucun événement temps réel n'est reçu après une seconde, le client relit GET /command-event/{event_id}. Cette reprise consulte le même état persisté par le dispatcher et continue chaque seconde jusqu'à l'accusé ou au délai final de 10 secondes. Un contrôle n'accepte par ailleurs aucun nouvel appui tant que le handshake du flux WebSocket n'est pas terminé.

Le délai de 10 secondes concerne uniquement l'accusé de mise en attente. Une commande déjà reconnue peut rester en traitement pendant les retries sans être libérée par ce timer.

Retry et configuration​

Les paramètres sont accessibles dans Configuration → Domotique :

ParamètreValeur par défautSignification
retry_count3Nombre de nouvelles tentatives après la tentative initiale.
retry_interval_s5Délai entre deux tentatives, en secondes.

Avec la configuration par défaut, une commande peut donc être exécutée au maximum quatre fois. Les valeurs sont appliquées au dispatcher actif et persistées dans SQLite.

Exemple de configuration :

{
"domotics": {
"retry_count": 3,
"retry_interval_s": 5,
"pending_timeout_s": 10
}
}

pending_timeout_s formalise le contrat commun ; les interfaces utilisent actuellement la valeur imposée de 10 secondes.

Persistance SQLite​

Le dispatcher écrit chaque transition avant de la publier sur le bus :

  • command_events conserve le dernier état connu de chaque event_id ;
  • events conserve le journal chronologique des transitions ;
  • domotics_settings conserve les paramètres du dispatcher.

Un succès et un échec définitif sont ainsi consultables après redémarrage. Si SQLite ne répond plus, le dispatcher devient non sain et /health retourne un échec.

Bus côté interfaces​

Qt/QML​

NodeClient maintient la connexion WebSocket, décode les frames et émet un signal commandEvent. La page de la pièce utilise Connections pour router l'événement vers la carte dont le target correspond.

Vue.js​

Le client Web expose un bus publish/subscribe. La connexion au proxy du hub publie chaque événement reçu ; EntityCard s'abonne lors de son montage et se désabonne lors de sa destruction.

Android​

DomoticsEventBus maintient la socket et un ensemble de listeners. Le repository mappe le target backend vers l'identifiant de la vue, tandis que RoomScreen affiche le pending, le loader ou l'erreur.

Dans les trois cas, le composant ne dépend pas d'un callback spécifique à sa requête : il réagit à tout événement portant sa cible. Cela garantit la cohérence lorsqu'une commande provient d'une autre interface.

Watchdog et reprise​

Le watchdog applicatif interroge /health toutes les cinq secondes et contrôle aussi la présence du daemon. Après trois échecs consécutifs, il redémarre S90leukos-node. Un verrou empêche le lancement simultané de plusieurs watchdogs.

Le daemon ne nourrit le watchdog matériel que si SQLite et le dispatcher sont sains. Le mécanisme couvre donc notamment :

  • un daemon arrêté ou bloqué ;
  • une base SQLite inaccessible ;
  • un dispatcher qui ne parvient plus à journaliser les transitions.

Le watchdog ne rejoue pas arbitrairement une commande matérielle après redémarrage : les événements persistés servent d'audit, ce qui évite une double activation dangereuse.

Vérifications d'exploitation​

La santé courante peut être contrôlée avec :

curl -fsS http://127.0.0.1:8080/health

Réponse saine :

{
"ok": true,
"sqlite": true,
"dispatcher": true,
"queue_size": 0
}

Le scénario de validation du retry doit faire apparaître la séquence suivante dans SQLite et sur WebSocket :

pending
processing (tentative 1)
retrying
processing (tentative 2)
retrying
processing (tentative 3)
retrying
processing (tentative 4)
failed ou succeeded

Fichiers de référence​

  • nodes/agent/include/event_dispatcher.hpp
  • nodes/agent/src/event_dispatcher.cpp
  • nodes/agent/src/local_api.cpp
  • nodes/agent/src/local_db.cpp
  • nodes/agent/packaging/bin/leukos-node-watchdog
  • nodes/ui/src/node/node_client.cpp
  • nodes/ui/qml/pages/domotics/RoomDevicesPage.qml
  • domotic/hub/internal/api/ws.go
  • domotic/web/src/api.js
  • mobile/android/app/src/main/java/com/leukos/domotics/network/DomoticsEventBus.java

Pour aller plus loin​