HVAC YZENTIS - Distech ECB-203 - Passerelle BACnet MS/TP vers MQTT sur ESP32-S3

Domotisation HVAC YZENTIS - Distech ECB-203

Passerelle BACnet MS/TP vers MQTT sur ESP32-S3

Salut la communauté !

Après avoir épluché de long en large le Forum HACF et GitHub à la recherche d’une solution pour piloter mon système de chauffage YZENTIS depuis Home Assistant, équipé d'un vieil automate Distech ECB-203, sans succès, je me suis lancé dans le développement d'une passerelle BACnet MS/TP vers MQTT à base d'ESP32-S3.

Je vous partage, ci dessous le concept, les différentes phases du projets et le résultat.

Le concept

B2M (BACnet 2 MQTT) , ou comment faire parler un protocole industriel avec la domotique.


*Concept modélisé avec Gemini Banana Pro

Le Matériel : Exit le bricolage

Pour obtenir une solution fiable, pas question de fils volants (style câbles DuPont) qui captent tous les parasites des relais 230V à proximité !
J'ai investi dans le module Waveshare ESP32-S3-RS485-CAN monté sur rail DIN.


Ce module est une petite merveille : il offre une isolation galvanique active indispensable pour assurer la stabilité des signaux sur le bus RS485 très sensible aux parasites.


L'intégration finale dans le tableau électrique : la passerelle Waveshare ESP32-S3 (LEDs allumées) est solidement clipsée à côté de l'imposant automate Distech Controls ECB-203.

La "Chronique d'un acharnement"

Pour dompter ce protocole industriel, le développement a suivi trois phases majeures caractérisées par pas mal de sueur, des maths, beaucoup de C++ et quelques nuits blanches.

Phase 1 : L'exploration et le décodage du protocole

Via ESPHome, j'ai d'abord développé un petit "custom component" pour que l'ESP32 agisse comme un simple "passe-plat" (pont TCP/RS485 half-duplex) vers mon PC. En combinant un port COM virtuel sur Windows, Yabe (Yet Another BACnet Explorer) et Wireshark, j'ai enfin pu écouter et analyser les "conversations intimes" de mon automate.


Analyse d'une trame sous Wireshark : capture d'un échange d'écriture/lecture de propriété BACnet (Complex-ACK sur l'objet analog-input 5001). C'est ici qu'on comprend l'importance cruciale de la structure des paquets NPDU/APDU.

Github ProxyRS485SerialTCP : https://github.com/zirco999-star/proxyserial

Phase 2 L'impasse Python :

Fort de mes captures, j'ai tenté d'écrire une application Python (inspirer de bacnet-pypes) pour HA afin d'émuler un serveur BACnet MS/TP en exploitant ce pont TCP/RS485.

Verdict : Échec critique. La norme ASHRAE 135 régit le bus MS/TP par une ronde de jetons (Token Passing) ultra-rapide. Un nœud n'a le droit de parler que lorsqu'il a le jeton, et le temps de réponse maximum d'un équipement (image) ne doit pas dépasser 40 bit times (environ 1,04 ms à 38 400 bps).

Impossible de garantir un tel niveau de précision temporelle et de réactivité dans un environnement système non temps réel comme Python sur HAOS.

Le jeton était perdu, l'automate Distech s'impatientait et reconstruisait la ronde en excluant totalement ma passerelle !

Phase 3 : Le C++ autonome et la guerre des microsecondes**

Pour s'en sortir, il fallait du lourd, du bas niveau, du temps réel. L'ESP32-S3 du waveshare (Dual-Core à 240 MHz avec 8 Mo de PSRAM) c'etait la plateforme idéale.

C'est là que l'acharnement a commencé !
3000 lignes de code, des centaines de tests, des nuits blanches...

  1. La FSM Matérielle et la division du travail :
    J'ai réécrit une Machine à États Finis (FSM) stricte à 9 états gérant les timings RS485 à la microseconde près (via esp_timer_get_time()). Le traitement est réparti sur les deux cœurs de l'ESP32-S3 :

    • Core 1 (Temps Réel) : Dédié uniquement à la tâche critique mstp_rx_task (Priorité maximale 20) pour la lecture UART en temps réel, et bacnet_task (Priorité 15) pour orchestrer la ronde des jetons.
    • Core 0 (Réseau & Web) : Gère le serveur Web, l'interface utilisateur (UI) et les publications MQTT.
  2. La chasse à la fragmentation mémoire (Zero-String) :
    Avec 98 objets BACnet identifiés sur mon Distech, l'utilisation classique de la classe String d'Arduino provoquait des crashs aléatoires au bout de quelques heures (la fameuse erreur LoadProhibited 0x30). J'ai banni les String au profit de structures statiques de taille fixe (char[]) allouées une fois pour toutes au démarrage.

  3. Le casse-tête du NVS et de la PSRAM :
    Pour éviter de relancer une découverte exhaustive des 98 objets à chaque démarrage (ce qui prend 2 à 3 minutes à 38400 bauds), j'ai écrit un moteur de persistance sur la mémoire Flash (NVS). Pour contourner la limite stricte de 1984 octets imposée par le NVS d'Espressif, j'ai implémenté une pagination par blocs de 20 objets et des namespaces dynamiques (st_[DEVICE_ID]). Pour éviter d'asphyxier la RAM interne de l'ESP32 lors de requêtes JSON lourdes, les allocations du parseur JSON (ArduinoJson7) ont été migrées vers la PSRAM externe de 8 Mo via un allocateur personnalisé.

Une intégration totale : L'écosystème B2M

Aujourd'hui, la passerelle dispose d'une suite logicielle complète et autonome, sans aucune dépendance extérieure.

1. L'Interface Web d'administration embarquée

Pas besoin de flasher du code pour ajuster les paramètres. La passerelle embarque son propre serveur web responsive.


Le Dashboard Web en temps réel : il affiche la santé du système, l'uptime, l'état de la mémoire Heap, et surtout les statistiques de la ronde du jeton (Tokens vus / RX / TX) ainsi qu'une barre de progression de découverte.

L'onglet BACnet : il liste tous les objets découverts sur l'automate (Analog Inputs, Binary Values, Multi-State Values). On peut y renommer les variables à chaud (ex: "Temp_bureau") et choisir d'activer ou non le polling de chaque point individuellement.

Les paramètres système : des sections d'accordéons épurées et ergonomiques pour configurer le réseau (IP statique ou DHCP), le broker MQTT, les timings fins de la machine à états BACnet et la sécurité.

2. L'intégration magique dans Home Assistant via MQTT

La passerelle implémente le protocole de découverte automatique de Home Assistant (MQTT Auto-Discovery) qui est très bien documenté et facile d'utilisation.

  • Découverte automatique instantanée : Dès qu'un objet BACnet est activé dans l'interface WEB de la passerelle,

il apparaît automatiquement comme entité propre dans Home Assistant.

  • Suivi de l'ESP32-S3 : La passerelle crée un appareil de diagnostic dédié MQTT (BACnet2MQTT Gateway) pour surveiller l'ESP32 (Uptime, Température de puce, Signal WiFi RSSI, etat de token passing, etc...).

  • Le casse-tête des Multi-States (MSV) résolu : Sur l'automate, les modes (Confort, Éco, Hors-Gel) sont représentés par des index numériques (1, 2, 3). Pour éviter de surcharger l'ESP32, la passerelle interroge une fois le tableau de texte de l'automate, puis injecte dynamiquement des templates 'Jinja2' dans la configuration MQTT.

Home Assistant s'occupe alors de la traduction graphique !

  • Uptime et Uptime Réseau : La passerelle utilise un Gatekeeper MQTT asynchrone doté d'un pacing matériel de 5 ms pour injecter les données sur le broker MQTT pour ne pas saturer la pile TCP/IP (AsyncTCP) de l'ESP32.


détail des objets sélectionnés pour la diffusion MQTT.

Dans les appareils de Home Assistant, la passerelle "BACnet2MQTT Gateway" apparaît proprement aux côtés de mes modules eBUS (qui gèrent la production de la chaudière Saunier-Duval).


Les métriques de diagnostic remontées en MQTT : température du chip ESP32, Uptime, statut du bus MS/TP et force du signal WiFi. De quoi assurer une maintenance préventive aux petits oignons.

Dashboard Home Assistant en action. Les températures ambiantes lues sur l'automate Distech sont historisées sans aucune latence, et le mode CVC est pilotable d'un simple clic.

L'interface HA permet d'envoyer directement des consignes et d'ajuster les offsets de température de l'automate (qui réagit instantanément grâce au mode d'écriture BACnet WriteProperty optimisé).

Appel à la communauté : Je cherche des testeurs !

Le projet est désormais dans sa version stable v6.8, validée et éprouvée (mais seulement par moi) après de nombreuses corrections, tests et optimisations de code. Le système tourne sans fuite mémoire depuis quelques jours sur mon installation sans soucis et zero CRC errors sur la liaison RS485 :winking_face_with_tongue:
Toutefois, la norme BACnet est vaste et chaque fabricant d'automates (Honeywell, Siemens, Johnson Controls, Distech...) a ses propres petites habitudes (ou défaut) d'implémentation.

Je cherche des personnes équipées d'un automate BACnet MS/TP (RS485) qui seraient prêtes à :

  1. Acquérir un module Waveshare ESP32-S3-RS485-CAN (compter environ 30€ sur Amazon/Aliexpress).
  2. Flasher mon firmware (je fournis le binaire ou le code, l'installation se fait en quelques clics via USB).
  3. Connecter les bornes A+ et B- sur leur bus pour tester la découverte automatique des objets et la stabilité de la ronde chez eux.

Si vous avez une installation de chauffage, de ventilation, ou une GTB qui dort chez vous et que vous rêvez de l'intégrer localement et proprement dans Home Assistant sans passer par un Cloud propriétaire, manifestez vous en commentaire ! Je serais ravi de vous partager mon travail et de vous aider à le mettre en place.

À bientôt dans les commentaires !

[ajout] : Je viens de publier dans GitHub :
Github Bacnet_2_mqtt : https://github.com/zirco999-star/bacnet_2_mqtt

[Update] Session "Reverse" & Hack pour l'été (low cost)

Salut à tous !

Petit teaser sur la prochaine feature de ma passerelle ESP32-S3 pour affronter les fortes chaleurs.

Je me suis fait une grosse session reverse-engineering (Hex Editor sur la config de l'automate + YABE), et j'ai trouvé la faille pour forcer le brassage de l'air frais de mon split du salon vers les chambres via l'UTA !

La découverte de la logique Distech

Impossible de forcer les volets ou le ventilateur directement : le programme interne du Distech écrase tout toutes les 100 ms.

Apres plusieurs essais :face_with_monocle:…

Le seul bypass (que j’ai trouvé) que sa logique ne peut pas écraser :

  1. Passer la sonde de température de la chambre (AI) en Out_Of_Service = True.
  2. Lui injecter une fausse température bien en dessous de la consigne (par exemple 12°C).
  3. Réaction immédiate : L'automate détecte un fort besoin thermique local, ouvre le volet de la chambre en grand et lance la ventilation après ~50 secondes (le temps de course de sécurité des moteurs de volets).
  4. Comme ma chaudière Saunier Duval est domotisée sous HA via eBus, je la verrouille en mode "Off/Eau chaude seule", comme ça même quand la vanne BO de l’eau s’ouvre, la chaudière ne déclenche pas :blush:.

Résultat : l'UTA brasse l’air au max en ouvrant les clapets des chambres, clapet salon fermé sans jamais lancer la chauffe !

Bon maintenant faut mettre en pratique et automatiser tout ça…

Prochaine étape : Du code et des tests !

Maintenant que la logique est validée à la main dans YABE, il n'y a plus qu'à :

  • Coder l'endpoint /api/hackAI dans le firmware C++ de l'ESP32.
  • Gérer un flag en PSRAM pour bloquer l'envoi de la fausse température sur MQTT (pour ne pas polluer l'historique de HA).
  • Tester et mesurer les températures réelles cet été pour quantifier l'efficacité de ce transfert de fraîcheur.

question : si il y a des professionnels parmi nous :
est ce qu’il y a des risques à faire ça ??

La suite au prochain épisode dès que l'API « hackAI » tourne sur la passerelle ! :call_me_hand:

Eorme boulot ! J'ai commandé le module Waveshare :wink:

Perso, j'ai 2 ECB-307 (modèle > 2016).

hello,
normalement c’est ok avec les séries 300, le protocole est bacnet mstp :blush:

Pour la gestion de plusieurs devices, normalement c’est prévu mais je ne peux pas tester (j’ai qu’un seul device)

Ps : j’ai encore progressé sur la compréhension du fonctionnement…
j’ai également ajouté des routes API pour faire des actions type writeproperty et Readproperty directement !

j’essaie de push la dernière « stable » demain