Interfacer une chaudière opentherm avec OTGW et nodemcu

Bonjour,

Une petite contribution aux personnes qui ont une chaudière compatible opentherm :
J’ai interfacé (pour mon fils, qui a une De Dietrich Naneo) cette chaudière à jeedom grace à la gateway opentherm de nodoshop : https://www.nodo-shop.nl/en/opentherm-gateway/188-opentherm-gateway.html et d’un nodemcu ‹ monté › dessus pour pouvoir y accéder à distance, par wifi.
Cout total : une cinquantaine d’euros, si on inclu le chargeur (24V) pour l’alimentation de l’ensemble.

A noter que cette gateway est entièrement compatible avec l’Opentherm Gateway (OTGW) proposée à http://otgw.tclcode.com/
On trouve sur ce site toute la doc relative à son fonctionnement.

Dans le cas d’utilisation d’un nodemcu, nodo-shop préconise d’installer EspEasy pour mettre à disposition les informations séries de l’OTGW en telnet, via wifi.
J’avais essayé ; ca marche, mais c’est un peu dommage de n’utiliser le nodemcu que pour cet effet ; et pas particulièrement facile à interfacer avec un logiciel domotique.

J’ai développé un peu de code permettant de donner un peu plus d’intelligence au nodemcu ; en particulier :
. plusieurs clients telnet simultanés en liaison avec l’interface série de OTGW
. un accès telnet (ESP commands) dédié à l’administration et au suivi du fonctionnement du nodemcu
. un accès telnet dédié au debugging à distance
. un accès http pour permettre d’interfacer une application domotique (jeedom, par exemple) avec OTGW de manière simple
. une mise à jour OTA du firmware du nodemcu

En particulier, le nodemcu permet d’accèder facilement, en http, à différentes informations de la chaudière.

J’ai également développé un petit script php, à mettre coté jeedom, pour remonter ces informations dans un virtuel.

C’est du brut de décoffrage, c’est à adapter aux besoins spécifiques.
Il faut quelques compétences ‹ arduino › et php pour mettre en oeuvre.

Je n’ai pas les compétences, ni le temps, pour faire quelque chose de générique qui serait utilisable par un utilisateur non averti.

Si ca peut servir à quelques uns …

Au fait : c’est dispo à https://github.com/vmath54/OTGW_nodemcu

3 « J'aime »

@vmath54
Bonjour
De ta connaissance de cet équipement, si on branche un thermostat opentherm « du commerce pas diy » est ce qu’on peut lire ces données (consigne, température…).

Il existe d’autres cartes électroniques compatible opentherm

https://diyless.com/product/esp8266-opentherm-gateway

Et

Je ne sais pas s’il y a des différences mais l’avantage c’est l’alimentation en 5volts.

Merci

@limad44
Désolé de répondre tardivement ; je n’avais pas vu ton post.

Il y a aussi celui-ci : https://www.tindie.com/products/jiripraus/opentherm-gateway-arduino-shield/
J’en ai un exemplaire chez moi, neuf, jamais testé. Comme les deux liens que tu proposes, ca nécessite de se palucher du code assez ardu, et ca risque de ne pas pouvoir alimenter électriquement le thermostat (voir explication plus loin).

Ce qu’il faut savoir avec opentherm, c’est que, même si on veut se limiter à sniffer les messages, il faut se mettre en coupure ; donc à minima, lire les trames de chaque coté, et les ré-émettre de l’autre coté. Avec des contraintes de temps entre l’émission d’une trame d’un coté et la réponse de l’autre.
C’est lié au protocole ; je ne sais plus bien expliquer pourquoi la coupure est nécessaire, car un peu vieux. Mais tout cela complique drolement le fait de mettre en place un tel sniffer, sans perturber le fonctionnement de la chaudière.

Voir les spécifs : https://www.domoticaforum.eu/uploaded/Ard%20M/Opentherm%20Protocol%20v2-2.pdf

Les signaux échangés sont à une tension entre 15 et 18V. A voir comment font les modules alimentés en 5V.

Dans le cas de mon fils, le thermostat est celui-ci : https://www.dedietrich-thermique.fr/content/download/7301/47884/version/2/file/NOT-123100-001-AD.pdf
il peut fonctionner avec ou sans piles. Si sans piles (c’est le cas chez lui), la puissance d’alimentation du thermostat est fournie normalement par la chaudière ; si on met un boitier en coupure, il faut que celui-ci soit capable de le faire…

Bref, la gateway que j’ai acheté chez nodoshop, qui vaut 30€, sait faire tout ça ; elle est entièrement compatible avec ceci : http://otgw.tclcode.com/
Sans soft par dessus, si on l’intercale dans la liaison opentherm (donc en coupure entre le thermostat et la chausière), cette liaison reste entièrement opérationnelle, ce qui est déja pas mal.

Cette gateway communique en liaison série ; pour de la domotique, ce n’est pas pratique.
D’ou l’ajout d’un nodemcu (3€), qui se branche facilement dessus, alimenté par la gateway, et qui permet de communiquer en wifi. Cerise sur le gateau, c’est programmable facilement, ce qui permet une intégration aisée ; c’est l’objet de mon développement.
Voir https://www.nodo-shop.nl/nl/index.php?controller=attachment&id_attachment=35

Mon code se limite à récupérer le ‹ summary › de la gateway ; donc une photo à un instant T des derniers paramètres importants qui ont circulés.
Et les rendre accessibles facilement.
Il ne décode pas les trames opentherm.

On pourrait faire beaucoup plus et du spécifique ; c’est plus couteux en développement. Il faudrait décoder toutes les trames opentherm qui circulent, voire gérer des commandes spécifiques. La gateway le permet, bon courage.

Tout ça tourne chez mon fils depuis plus d’un an, sans problème. Le cable opentherm (2 fils) a été coupé, et on a mis 2 connecteurs compatibles avec la gateway de chaque coté.
Par précaution, on a fait un petit bricolo qui permettrait de court-circuiter rapidement la gateway en cas de dysfonctionnement de celle-ci ; jamais eu besoin.

Dans ce code, les paramètres accessibles depuis le ‹ summary › de la gateway sont listés :

Donc, oui, consigne (‹ controlSetPoint ›), température (‹ roomTemperature ›), et bien d’autres paramètres : température chaudière, température extérieure (si sonde), fonctionnement ou non du bruleur, …

1 « J'aime »

Bonjour
merci pour ta réponse.
j’ai ôté pour l’équipement de diyless qui est malheureusement arrivé non-soudé et j’ai du payer 14€ de frais de douane :rage:
il va falloir que je me penche sur tous ça… car il faut aussi injecter un Sketch dans wemos D1.

1 « J'aime »

Bonjour,
histoire de partager les informations que j’ai pu remonter de ma chaudière (De Dietrich Twineo EGC25 avec thermostat AD304) :
J’utilise aussi la passerelle de nodo shop mais en USB (j’ai bien la carte fille Ethernet mais la non-sécurisation du telnet me pose des soucis de configuration réseau pour le moment).
Je suis parti de l’approche de @klona sur l’ancien forum voir ici pour me faire un script adapté à mes besoins.
C’est un scénario lancé toutes les minutes qui lance les différentes actions, il est certainement possible de faire ça plus élégamment mais cela fonctionne sans souci avec ma propre « loi d’eau » depuis 3 hivers.
Le script dans le plugin script : (attention, l’écriture de logs est probablement à désactiver si vous êtes sur un système utilisant une carte SD, et la réponse de la commande « PS=1 » semble avoir changé avec le dernier firmware OTGW)

#!/usr/bin/env python
import time
import serial
import sys
import requests
import os.path
import logging
from logging.handlers import RotatingFileHandler

logfile ="/var/www/html/plugins/script/data/OTGW.log"

MaxLogFileSize = 5*1024*1024 # taille maxi de log en octet x*1024*1024 ==> x Mo max 
ecriture_log = logging.getLogger()
ecriture_log.setLevel(logging.CRITICAL)
formatter = logging.Formatter('%(asctime)s :: %(levelname)s :: %(message)s')
file_handler = RotatingFileHandler(logfile, 'a', MaxLogFileSize, 1)
file_handler.setLevel(logging.DEBUG)
file_handler.setFormatter(formatter)
ecriture_log.addHandler(file_handler)


#Constantes : 
# URL de Jeedom
URL_JEEDOM = "http://localhost/core/api/jeeApi.php"
# cle API Jeedom 
API_KEY = "CLEAPIXXXXXXXXXXXXXXX"
# port serie
PORT_SERIE = '/dev/ttyUSB2'
# identifiants objets Jeedom
# ID commande keepalive passerelle (bypass de la passerelle si off, ZMNHND1 en auto-off pilote un relais 4 inverseurs)
RELAIS_OTGW_ON = 6524
# ID des informations Jeedom
CHAUFFAGE_CENTRAL_AUTORISE = 6768
EAU_CHAUDE_SANITAIRE_AUTORISEE = 6769
REFROIDISSEMENT_AUTORISE = 6770
CORRECTION_TEMPERATURE_EXTERIEURE_ACTIVE = 6771
CHAUFFAGE_CENTRAL_2_AUTORISE = 6772
INDICATEUR_ANOMALIE_CHAUDIERE = 6773
ETAT_CHAUFFAGE_CENTRAL = 6774
ETAT_EAU_CHAUDE_SANITAIRE = 6775
ETAT_FLAMME = 6776
ETAT_REFROIDISSEMENT = 6777
ETAT_CHAUFFAGE_CENTRAL_2 = 6778
DIAGNOSTIC_CHAUDIERE = 6779
DATA_CONTROL_SETPOINT = 6780
SUPPORT_TRANSFERT_CONSIGNE_ECS = 6781
TRANSFERT_CONSIGNE_ECS_MAXI_SUPPORTEE = 6782
SUPPORT_ECRITURE_CONSIGNE_ECS = 6783
SUPPORT_ECRITURE_CONSIGNE_CHAUFFAGE_CENTRAL = 6784
MODULATION_RELATIVE_MAXI = 6785
CAPACITE_MAXI_CHAUDIERE = 6786
MODULATION_RELATIVE_MINI = 6787
CONSIGNE_THERMOSTAT_AMBIANCE = 6788
MODULATION_RELATIVE = 6789
PRESSION_EAU_CHAUFFAGE_CENTRAL = 6790
TEMPERATURE_THERMOSTAT_AMBIANCE = 6791
TEMPERATURE_CHAUFFAGE_CENTRAL_ALLER = 6792
TEMPERATURE_EAU_CHAUDE_SANITAIRE = 6793
TEMPERATURE_EXTERIEURE = 6794
TEMPERATURE_CHAUFFAGE_CENTRAL_RETOUR = 6795
CONSIGNE_EAU_CHAUDE_SANITAIRE_MAXI = 6796
CONSIGNE_EAU_CHAUDE_SANITAIRE_MINI = 6797
CONSIGNE_TEMPERATURE_CHAUFFAGE_CENTRAL_MAXI = 6798
CONSIGNE_TEMPERATURE_CHAUFFAGE_CENTRAL_MINI = 6799
CONSIGNE_ECS = 6800
CONSIGNE_CHAUFFAGE_CENTRAL_MAXI = 6801
TOTAL_DEPARTS_BRULEUR = 6802
DEPARTS_POMPE_CHAUFFAGE_CENTRAL = 6803
DEPARTS_POMPE_ECS = 6804
DEPARTS_BRULEUR_ECS = 6805
TOTAL_HEURES_BRULEUR = 6806
HEURES_POMPE_CHAUFFAGE = 6807
HEURES_POMPE_ECS = 6808
HEURES_BRULEUR_ECS = 6809


# MAJ des informations dans Jeedom
def Ecriture_Jeedom(identifiant_objet,valeur):
    if identifiant_objet == RELAIS_OTGW_ON:
        data = {'apikey':API_KEY, 
        'type':'cmd',
        'id':identifiant_objet}  
    else:  
        data = {'plugin':'virtual', 
        'apikey':API_KEY, 
        'type':'virtual',
        'id':identifiant_objet, 
        'value':str(valeur)} 
    response = requests.post(URL_JEEDOM, params=data)
# ordre OTGW simple
def Action_OTGW(operation):
    return Operation_OTGW(operation,'')
# ordre OTGW avec valeur  
def Operation_OTGW(operation, valeur):
    ecriture_log.debug ("Operation_OTGW operation : "+ operation)
    ecriture_log.debug ("valeur : "+valeur)
    if operation =='etatcomplet' or operation =='etat':
        commande="PS=1"
    elif operation =='autorisation_ecs':
        if valeur != '':
          commande="HW="+str(valeur)
        else:
          commande="HW=1"
    elif operation =='interdiction_ecs':
        commande="HW=0"
    elif operation =='thermostat_ecs':
        commande="HW=A"
    elif operation =='thermostat_modulation':
        commande="MM=-"
    elif operation =='thermostat_consigne_radiateurs':
        commande="CS=0"
    elif operation =='chauffage_actif':
        commande="CH=1"
    elif operation =='chauffage_inactif':
        commande="CH=0"
    elif operation =='consigne_radiateurs':
        commande="CS="+valeur
    elif operation =='modulation_maxi':
        commande="MM="+valeur
    elif operation =='consigne_ambiance':
        commande="TC="+valeur
    elif operation =='transmission_horaire_jour':
        commande="SC="+valeur    
    elif operation =='reset_consigne_ambiance':
        commande="TC=0"    
    elif operation =='type_thermostat':
        commande="FT=D"
# si besoin        
    elif operation =='transmission_temp_retour':
        commande="AA=28"
# emulation de retour
# affichage programme A sur thermostat De Dietrich AD304      
    elif operation =='programme_a':
        commande="SR=99:1"
# affichage confort sur le thermostat      
    elif operation =='confort':
        commande="SR=99:2"
# affichage eco sur le thermostat      
    elif operation =='eco':
        commande="SR=99:4"
# affichage hors-gel sur thermostat      
    elif operation =='hors_gel':
        commande="SR=99:5"
# reset emulation mode thermostat De Dietrich AD304     
    elif operation =='reset_mode':
        commande="CR=99"    
    else:
        commande=operation.upper()
    ecriture_log.debug ("commande : "+commande)    
    return Ecriture_Port_Serie(commande)    
    
def Ecriture_Port_Serie(commande):
    try:
        serialport = serial.Serial(
         port=PORT_SERIE,
         baudrate = 9600,
         parity=serial.PARITY_NONE,
         stopbits=serial.STOPBITS_ONE,
         bytesize=serial.EIGHTBITS,
         timeout=1
        )
        serialport.close()
        serialport.open()
        serialport.write(commande + chr(13))   # envoi commande
        ACK = serialport.readline()
        retour = serialport.readline()
        serialport.close()
    except serial.SerialException:
        ACK=''
        retour=''
        ecriture_log.warning("Ecriture_Port_Serie Anomalie port serie")
    
    ACK = ACK.replace("\r\n","").replace(" ","")
    retour = retour.replace("\r\n","").replace(" ","")
    ecriture_log.debug("Ecriture_Port_Serie commande : "+commande)
    ecriture_log.debug("ACK : "+ACK)
    ecriture_log.debug("Retour : "+retour)    
    valeurRetour = ACK.split(':')[-1].split('=')[-1]
    ecriture_log.debug("valeurRetour : "+valeurRetour)
    if valeurRetour=='NG' or valeurRetour=='SE' or valeurRetour=='BV' or valeurRetour=='OR' or valeurRetour=='NS' or valeurRetour=='NF' or valeurRetour=='OE':
        ecriture_log.warning("Anomalie : "+ACK)
        ecriture_log.warning("Retour : "+retour)
    if len(retour)<1:
        retour = valeurRetour
    ecriture_log.debug("Retour fonction : "+retour)
    return retour 
      
    
      
      

#params
if len(sys.argv)>1:
    Operation = sys.argv[1].lower()
else:
    Operation = 'etatcomplet'
if len(sys.argv)>2:
    Valeur = sys.argv[2].lower()
else:
    Valeur = ''
ecriture_log.debug('OTGW.py Operation : ' + Operation)
ecriture_log.debug('Valeur : ' + Valeur)
    
if  Operation =='etatcomplet' or Operation =='etat':
    retour=Action_OTGW(Operation)
    if len(retour)>1:
        #decomposition
        fields = retour.split(',')
        device_status = fields[0].split('/')
        master_status = device_status[0]
        if len(device_status)>1:
            slave_status = device_status[1]
        if len(fields)>14:  
            remote_params = fields[2].split('/')
            capmodlimits = fields[4].split('/')
            dhw_setp_bounds = fields[13].split('/')
            ch_setp_bounds = fields[14].split('/')
        if (len(fields)>24 and len(master_status)>7 and len(slave_status)>7):
#MAJ partielle des valeurs de Jeedom (perfs)          
            if  Operation =='etatcomplet' or Operation =='etat':
                Ecriture_Jeedom(CHAUFFAGE_CENTRAL_AUTORISE,(int(master_status[7])))
                Ecriture_Jeedom(EAU_CHAUDE_SANITAIRE_AUTORISEE,(int(master_status[6])))
                Ecriture_Jeedom(ETAT_CHAUFFAGE_CENTRAL,(int(slave_status[6])))
                Ecriture_Jeedom(ETAT_EAU_CHAUDE_SANITAIRE,(int(slave_status[5])))
                Ecriture_Jeedom(ETAT_FLAMME,(int(slave_status[4])))
                Ecriture_Jeedom(PRESSION_EAU_CHAUFFAGE_CENTRAL,(float(fields[7])))
                Ecriture_Jeedom(MODULATION_RELATIVE,(float(fields[6])))
                Ecriture_Jeedom(TEMPERATURE_CHAUFFAGE_CENTRAL_ALLER,(float(fields[9])))
                Ecriture_Jeedom(TEMPERATURE_CHAUFFAGE_CENTRAL_RETOUR,(float(fields[12])))
                Ecriture_Jeedom(CONSIGNE_THERMOSTAT_AMBIANCE,(float(fields[5])))
                Ecriture_Jeedom(TEMPERATURE_THERMOSTAT_AMBIANCE,(float(fields[8])))
                Ecriture_Jeedom(TEMPERATURE_EAU_CHAUDE_SANITAIRE,(float(fields[10])))
#MAJ des valeurs de Jeedom (infos variant plus lentement)                
            if  Operation =='etatcomplet':
                Ecriture_Jeedom(REFROIDISSEMENT_AUTORISE,(int(master_status[5])))
                Ecriture_Jeedom(CORRECTION_TEMPERATURE_EXTERIEURE_ACTIVE,(int(master_status[4])))
                Ecriture_Jeedom(CHAUFFAGE_CENTRAL_2_AUTORISE,(int(master_status[3])))
                Ecriture_Jeedom(INDICATEUR_ANOMALIE_CHAUDIERE,(int(slave_status[7])))
                Ecriture_Jeedom(ETAT_REFROIDISSEMENT,(int(slave_status[3])))
                Ecriture_Jeedom(ETAT_CHAUFFAGE_CENTRAL_2,(int(slave_status[2])))
                Ecriture_Jeedom(DIAGNOSTIC_CHAUDIERE,(int(slave_status[1])))
                Ecriture_Jeedom(DATA_CONTROL_SETPOINT,(float(fields[1])))
                Ecriture_Jeedom(SUPPORT_TRANSFERT_CONSIGNE_ECS,(int(remote_params[0][7])))
                Ecriture_Jeedom(TRANSFERT_CONSIGNE_ECS_MAXI_SUPPORTEE,(int(remote_params[0][6])))
                Ecriture_Jeedom(SUPPORT_ECRITURE_CONSIGNE_ECS,(int(remote_params[1][7])))
                Ecriture_Jeedom(SUPPORT_ECRITURE_CONSIGNE_CHAUFFAGE_CENTRAL,(int(remote_params[1][6])))
                Ecriture_Jeedom(MODULATION_RELATIVE_MAXI,(float(fields[3])))
                Ecriture_Jeedom(CAPACITE_MAXI_CHAUDIERE,(int(capmodlimits[0])))
                Ecriture_Jeedom(MODULATION_RELATIVE_MINI,(int(capmodlimits[1])))
                Ecriture_Jeedom(TEMPERATURE_EXTERIEURE,(float(fields[11])))
                Ecriture_Jeedom(CONSIGNE_EAU_CHAUDE_SANITAIRE_MAXI,(int(dhw_setp_bounds[0])))
                Ecriture_Jeedom(CONSIGNE_EAU_CHAUDE_SANITAIRE_MINI,(int(dhw_setp_bounds[1])))
                Ecriture_Jeedom(CONSIGNE_TEMPERATURE_CHAUFFAGE_CENTRAL_MAXI,(int(ch_setp_bounds[0])))
                Ecriture_Jeedom(CONSIGNE_TEMPERATURE_CHAUFFAGE_CENTRAL_MINI,(int(ch_setp_bounds[1])))
                Ecriture_Jeedom(CONSIGNE_ECS,(float(fields[15])))
                Ecriture_Jeedom(CONSIGNE_CHAUFFAGE_CENTRAL_MAXI,(float(fields[16])))
                Ecriture_Jeedom(TOTAL_DEPARTS_BRULEUR,(int(fields[17])))
                Ecriture_Jeedom(DEPARTS_POMPE_CHAUFFAGE_CENTRAL,(int(fields[18])))
                Ecriture_Jeedom(DEPARTS_POMPE_ECS,(int(fields[19])))
                Ecriture_Jeedom(DEPARTS_BRULEUR_ECS,(int(fields[20])))
                Ecriture_Jeedom(TOTAL_HEURES_BRULEUR,(int(fields[21])))
                Ecriture_Jeedom(HEURES_POMPE_CHAUFFAGE,(int(fields[22])))
                Ecriture_Jeedom(HEURES_POMPE_ECS,(int(fields[23])))
                Ecriture_Jeedom(HEURES_BRULEUR_ECS,(int(fields[24])))
#maintien de la passerelle dans le circuit (si script KO --> bypass)                
elif Operation =='keepalive':
    Ecriture_Jeedom(RELAIS_OTGW_ON,1)
else:
    Operation_OTGW(Operation, Valeur)

Et les commandes script utilisées qui sont donc du type :
/var/www/html/plugins/script/data/OTGW.py etatcomplet
/var/www/html/plugins/script/data/OTGW.py consigne_radiateurs #[Confort][Chaudière][Consigne radiateurs calculée]#
etc.

1 « J'aime »