Les champs suivants demandent de saisir l’adresse IP locale du module USR-W610 ainsi que le port saisie dans la configuration (dans mon cas 26). Pour ce qui est de l’Unit ID, il doit être positionné à 10 car c’est l’adresse de la chaudière maitre. Enfin, le Polling peut-être fixé comme on le souhaite, ici à 5 secondes et sans garder la connexion active.
Mais ça ne marche pas ton truc !
Si vous commencez à saisir des commandes pour espérer obtenir des réponses, vous verrez que cela n’est pas stable : parfois la chaudière répond, parfois pas et généralement le démon tombe… Mais pourquoi !?
Comme expliqué plus haut, cela est lié au mode bi-maitre utilisé par De Dietrich qui fait que la chaudière envoi des trames puis se met en sommeil environ toutes les 5 secondes. Là on pourrait se dire qu’il suffirait de fixer le polling à 5 secondes comme nous l’avons fait et de lancer l’échange au bon moment… Oui mais compliqué à caler avec un risque important de voir le démon tomber au bout d’un moment car nous ne nous sommes pas positionnés parfaitement.
Aussi, il a fallut regarder le code du plugin et voir comment se sortir de cette situation.
Modification du code du plugin pour s’en sortir
Lors que l’on regard avec un outil pour envoyer et recevoir des trames, on constate que lorsque nous envoyons un demande pour lire un registre, la chaudière :
- soit répond correctement,
- soit retourne n’importe quoi.
Lorsque n’importe quoi est retourné, on s’aperçoit que la trame ne ressemble à rien ce qui génère une erreur… ce qui est intéressant…
Pour aller voir ce que fait le plugin, il faut aller dans l’Editeur de fichiers de Jeedom (Réglages > Système) puis aller dans le répertoire plugins > mymodbus > ressources. Là plusieurs fichiers sont présents dont :
- celui qui traite de la lecture « mymodbus_demond.py »
- celui qui traite de l’écriture « mymodbus_write.py »
Pour l’instant, intéressons-nous à la lecture et ouvrons le premier fichier. On remarque tout d’abord que le code est fait par @Bebel27 que je profite pour remercier au passage pour son excellent plugin. Ensuite, nous avons différents imports de librairies et autres vérifications de versions de python…
NOTE
Je ne vais pas décrire la totalité du code du plugin mais tenter d’expliquer un peu les principaux endroits qui méritent notre attention.
Nous avons également la liste des arguments envoyés au plugin lorsque nous émettons une trame dont le protocole utilisé, l’adresse de l’hôte, le port, etc. Nous avons également le type de fonction utilisée pour la lecture dont « Holding register (hrs) » qui nous intéresse (c’est Type Entrée/Sortie qu’il faudra utiliser pour échanger avec la chaudière).
Plus bas, nous voyons l’endroit où le protocol « tcpip » est vérifié puis nous rentrons dans une boucle qui vérifie la fonction utilisée. Nous descendons plus bas jusqu’à voir :
if (args.hrs) != None :
Cette ligne vérifie que l’argument relatif à la fonction est bien hrs comme « Holding register » que nous utilisons.
Là nous avons plusieurs boucles qui permettent de parcourir les différents registres que nous demandons. En effet, le plugin envoi plusieurs demandes en une fois ce qui entraine la construction d’un tableau avec tous nos registres.
List_hrs = (args.hrs).split(',')
Aussi, si nous envoyons en arguments « hrs » les valeurs de registres « 14,15,456 » par exemple, le code reprend ces éléments pour les répartir dans un tableau appelé « List_hrs » construit en utilisant la virgule comme indice de séparation.
La boucle for ensuite permet de parcourir le tableau « List_hrs » en mettant dans la variable table chaque valeur du tableau à chaque tour de boucle.
for table in List_hrs:
Plusieurs « if » s’enchainent ensuite mais nous remarquons plus particulièrement les endroits où la trame est envoyée puis réceptionnée.
rr = client.read_holding_registers(int(hreg_first),i,unit=args.unid)
assert(not rr.isError()) # test that we are not an error
subprocess.Popen(['/usr/bin/php',mymodbus,'add='+args.host,'unit='+str(args.unid),'eqid='+str(args.eqid),'type=holding_registers','sortie=1','inputs='+str(int(hreg_first)),'values='+str(rr.registers)])
Par contre on voir que si une erreur est constatée dans la trame, alors le code s’arrête grâce à la fonction de contrôle d’erreur assert().
ATTENTION
C’est là que nous avons un problème car la chaudière nous retourne n’importe quoi après une demande faite au mauvais moment. Du coup, le code s’arrête et le démon se met en défaut.
Pour contourner ce problème, il faudrait chercher à identifier les zones de silences pour pouvoir communiquer. Ce serait la manière la plus propre… mais je n’ai pas trouvé comment le faire n’ayant pas des compétences fortes en développement.
Aussi, je suis plutôt parti du principe de vérifier si nous avions une erreur et de relancer la requête tant que la trame reçue est incorrecte… Cela multiplie les requêtes mais ça fonctionne et ne met plus le démon en défaut.
Pour faire cela, il suffit de remplacer le code précédent par celui-ci :
while True:
time.sleep(1)
rr = client.read_holding_registers(int(hreg_first),i,unit=args.unid)
if (not rr.isError()): # test that we are not an error
break
#rr = client.read_holding_registers(int(hreg_first),i,unit=args.unid)
#assert(not rr.isError()) # test that we are not an error
subprocess.Popen(['/usr/bin/php',mymodbus,'add='+args.host,'unit='+str(args.unid),'eqid='+str(args.eqid),'type=holding_registers','sortie=1','inputs='+str(int(hreg_first)),'values='+str(rr.registers)])
Je rentre donc dans une boucle qui répète toute les secondes la requête et vérifie s’il y a une erreur. Si oui alors on boucle, sinon on sort de la boucle et on traite la réponse. Le code précédent a été mis en commentaire afin de ne pas le perdre.
Il faut donc remplacer cela à tous les endroits du code où se trouve ces requêtes dans la partie « hrs ». Pour faciliter les choses, le code complet est ci-dessous.
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Code : Demond Mymobus
date: 17/02/2021
Auteur: @Bebel27
Version: b2.0
"""
import sys
import time
import argparse
import os
import subprocess
from threading import Thread, Lock
# Conversion
from pymodbus.constants import Endian
from pymodbus.payload import BinaryPayloadDecoder
from pymodbus.payload import BinaryPayloadBuilder
#Compatibility
from pymodbus.compat import IS_PYTHON3, PYTHON_VERSION
if IS_PYTHON3 and PYTHON_VERSION >= (3, 4):
print("Version de python ok")
else:
sys.stderr("merci d'installer Python 3 ou de relancer les dépendances Mymodbus")
sys.exit(1)
mymodbus = os.path.abspath(os.path.join(os.path.dirname(__file__), '../core/php/mymodbus.inc.php'))
parser = argparse.ArgumentParser(description='Mymodbus values.')
#-----------Générale---------------------------------------------------------------------
parser.add_argument("--verbosity", help="mode debug")
parser.add_argument("--protocol", type=str ,help="Choix protocole Modbus" ,required=True)
parser.add_argument("--host", type=str ,help="Choix de l'adresse host")
parser.add_argument("--port", type=str ,help="Choix du port", required=True)
parser.add_argument("--polling", type=int ,help="polling en s", required=True)
parser.add_argument("--unid", type=int ,help="choix Unit Id", required=True)
parser.add_argument("--keepopen", type=int ,help="Garde la connexion ouverte")
parser.add_argument("--eqid", type=int ,help="Numero equipement Jeedom", required=True)
#------------RTU-----------------------------------------------------------
parser.add_argument("--baudrate", type=int ,help="vitesse de com en bauds")
parser.add_argument("--stopbits", type=int ,help="bit de stop 1 ou 2")
parser.add_argument("--parity", type=int ,help="parity oui ou non ")
parser.add_argument("--bytesize", type=int ,help="Taile du mot 7 ou 8 ")
#-----------Fonctions---------------------------------------------
parser.add_argument("--coils", type=str ,help="Type Coils")
parser.add_argument("--dis", type=str, help="discrete imput")
parser.add_argument("--hrs", type=str ,help="Holding register")
parser.add_argument("--irs", type=str ,help="imput register")
#------------------------------------------------------------------
# Options demandées
#---------------------
parser.add_argument("--virg", type=str ,help="Holding à virgules")
parser.add_argument("--swapi32", type=str ,help="inverse 32bit")
parser.add_argument("--sign", type=str ,help="valeurs signées")
args = parser.parse_args()
#if args.verbosity:
# print("verbosity turned on")
# mymodbus polling thread
def polling_thread():
if args.protocol == 'rtu':
from pymodbus.client.sync import ModbusSerialClient as ModbusClient
client = ModbusClient(method='rtu', port=args.port, timeout=10,stopbits = 1, bytesize = 8, parity = 'N', baudrate= args.baudrate)
if args.protocol == 'tcpip':
from pymodbus.client.sync import ModbusTcpClient as ModbusClient
client = ModbusClient(host=args.host, port=args.port, timeout=10)
if args.protocol == 'rtuovertcp':
from pymodbus.client.sync import ModbusTcpClient as ModbusClient
from pymodbus.transaction import ModbusRtuFramer as ModbusFramer
client = ModbusClient(host=args.host, port=args.port, framer=ModbusFramer)
while True:
client.connect()
#lecture Discrete_inputs (2)
if (args.dis) != None :
List_dis = (args.dis).split(',')
di_start=List_dis[0]
i=1
for di in List_dis:
if int(di) == int(di_start):
di_previous=di_start
if int(di) == int(List_dis[-1]):
rr = client.read_discrete_inputs(int(di_start),i,unit=args.unid)
subprocess.Popen(['/usr/bin/php',mymodbus,'add='+args.host,'unit='+str(args.unid),'eqid='+str(args.eqid),'type=discrete_inputs','sortie=1','inputs='+str(int(di_start)),'values='+str(rr.bits[:i])])
print(int(di_start))
elif int(di) == int(di_previous) + 1 :
di_previous=int(di)
i += 1
if int(di) == int(List_dis[-1]):
rr = client.read_discrete_inputs(int(di_start),i,unit=args.unid)
subprocess.Popen(['/usr/bin/php',mymodbus,'add='+args.host,'unit='+str(args.unid),'eqid='+str(args.eqid),'type=discrete_inputs','sortie=2','inputs='+str(list(range(int(di_start),int(di_start)+i))),'values='+str(rr.bits[:i])])
else :
if int(di) != int(di_previous):
rr = client.read_discrete_inputs(int(di_start),i,unit=args.unid)
subprocess.Popen(['/usr/bin/php',mymodbus,'add='+args.host,'unit='+str(args.unid),'eqid='+str(args.eqid),'type=discrete_inputs','sortie=3','inputs='+str(list(range(int(di_start),int(di_start)+i))),'values='+str(rr.bits[:i])])
di_start=int(di)
di_previous=int(di)
i=1
if int(di) == int(List_dis[-1]):
rr = client.read_discrete_inputs(int(di_start),i,unit=args.unid)
subprocess.Popen(['/usr/bin/php',mymodbus,'add='+args.host,'unit='+str(args.unid),'eqid='+str(args.eqid),'type=discrete_inputs','sortie=4','inputs='+str(list(range(int(di_start),int(di_start)+i))),'values='+str(rr.bits[:i])])
#lecture holding register (3)
if (args.hrs) != None :
List_hrs = (args.hrs).split(',')
hreg_first=List_hrs[0]
i=1
for table in List_hrs:
if int(table) == int(hreg_first):
hr_previous=hreg_first
if int(table) == int(List_hrs[-1]):
while True:
time.sleep(1)
rr = client.read_holding_registers(int(hreg_first),i,unit=args.unid)
if (not rr.isError()): # test that we are not an error
break
#rr = client.read_holding_registers(int(hreg_first),i,unit=args.unid)
#assert(not rr.isError()) # test that we are not an error
subprocess.Popen(['/usr/bin/php',mymodbus,'add='+args.host,'unit='+str(args.unid),'eqid='+str(args.eqid),'type=holding_registers','sortie=1','inputs='+str(int(hreg_first)),'values='+str(rr.registers)])
elif int(table) == int(hr_previous)+1:
hr_previous=int(table)
i += 1
if int(table) == int(List_hrs[-1]):
while True:
time.sleep(1)
rr = client.read_holding_registers(int(hreg_first),i,unit=args.unid)
if (not rr.isError()): # test that we are not an error
break
#rr = client.read_holding_registers(int(hreg_first),i,unit=args.unid)
#assert(not rr.isError()) # test that we are not an error
subprocess.Popen(['/usr/bin/php',mymodbus,'add='+args.host,'unit='+str(args.unid),'eqid='+str(args.eqid),'type=holding_registers','sortie=2','inputs='+str(list(range(int(hreg_first),int(hreg_first)+i))),'values='+str(rr.registers)])
else :
if int(table) != int(hr_previous):
while True:
time.sleep(1)
rr = client.read_holding_registers(int(hreg_first),i,unit=args.unid)
if (not rr.isError()): # test that we are not an error
break
subprocess.Popen(['/usr/bin/php',mymodbus,'add='+args.host,'unit='+str(args.unid),'eqid='+str(args.eqid),'type=holding_registers','sortie=3','inputs='+str(list(range(int(hreg_first),int(hreg_first)+i))),'values='+str(rr.registers)])
hreg_first=int(table)
hr_previous=int(table)
time.sleep(0.1) #pause pour la pac
i=1
if int(table) == int(List_hrs[-1]):
while True:
time.sleep(1)
rr = client.read_holding_registers(int(hreg_first),i,unit=args.unid)
if (not rr.isError()): # test that we are not an error
break
#rr = client.read_holding_registers(int(hreg_first),i,unit=args.unid)
#assert(not rr.isError()) # test that we are not an error
subprocess.Popen(['/usr/bin/php',mymodbus,'add='+args.host,'unit='+str(args.unid),'eqid='+str(args.eqid),'type=holding_registers','sortie=4','inputs='+str(list(range(int(hreg_first),int(hreg_first)+i))),'values='+str(rr.registers)])
#lecture coils (1)
if (args.coils) != None :
List_coils = (args.coils).split(',')
coil_start=List_coils[0]
i=1
for coil in List_coils:
if int(coil) == int(coil_start):
coil_previous=coil_start
if int(coil) == int(List_coils[-1]):
rr = client.read_coils(int(coil_start),i,unit=args.unid)
subprocess.Popen(['/usr/bin/php',mymodbus,'add='+args.host,'unit='+str(args.unid),'eqid='+str(args.eqid),'type=coils','sortie=1','inputs='+str(int(coil_start)),'values='+str(rr.bits[:i])])
elif int(coil) == int(coil_previous) + 1 :
coil_previous=int(coil)
i += 1
if int(coil) == int(List_coils[-1]):
rr = client.read_coils(int(coil_start),i,unit=args.unid)
subprocess.Popen(['/usr/bin/php',mymodbus,'add='+args.host,'unit='+str(args.unid),'eqid='+str(args.eqid),'type=coils','sortie=2','inputs='+str(list(range(int(coil_start),int(coil_start)+i))),'values='+str(rr.bits[:i])])
else :
if int(coil) != int(coil_previous):
rr = client.read_coils(int(coil_start),i,unit=args.unid)
subprocess.Popen(['/usr/bin/php',mymodbus,'add='+args.host,'unit='+str(args.unid),'eqid='+str(args.eqid),'type=coils','sortie=3','inputs='+str(list(range(int(coil_start),int(coil_start)+i))),'values='+str(rr.bits[:i])])
coil_start=int(coil)
coil_previous=int(coil)
i=1
if int(coil) == int(List_coils[-1]):
rr = client.read_coils(int(coil_start),i,unit=args.unid)
subprocess.Popen(['/usr/bin/php',mymodbus,'add='+args.host,'unit='+str(args.unid),'eqid='+str(args.eqid),'type=coils','sortie=4','inputs='+str(list(range(int(coil_start),int(coil_start)+i))),'values='+str(rr.bits[:i])])
#lecture input registers
if (args.irs) != None :
List_irs = (args.irs).split(',')
ir_start=List_irs[0]
i=1
for ir in List_irs:
if int(ir) == int(ir_start):
ir_previous=ir_start
if int(ir) == int(List_irs[-1]):
rr = client.read_input_registers(int(ir_start),i,unit=args.unid)
#assert(not rr.isError()
subprocess.Popen(['/usr/bin/php',mymodbus,'add='+args.host,'unit='+str(args.unid),'eqid='+str(args.eqid),'type=input_registers','sortie=1','inputs='+str(int(ir_start)),'values='+str(rr.registers)])
elif int(ir) == int(ir_previous) + 1 :
ir_previous=int(ir)
i += 1
if int(ir) == int(List_irs[-1]):
rr = client.read_input_registers(int(ir_start),i,unit=args.unid)
subprocess.Popen(['/usr/bin/php',mymodbus,'add='+args.host,'unit='+str(args.unid),'eqid='+str(args.eqid),'type=input_registers','sortie=2','inputs='+str(list(range(int(ir_start),int(ir_start)+i))),'values='+str(rr.registers)])
else :
if int(ir) != int(ir_previous):
rr = client.read_input_registers(int(ir_start),i,unit=args.unid)
subprocess.Popen(['/usr/bin/php',mymodbus,'add='+args.host,'unit='+str(args.unid),'eqid='+str(args.eqid),'type=input_registers','sortie=3','inputs='+str(list(range(int(ir_start),int(ir_start)+i))),'values='+str(rr.registers)])
ir_start=int(ir)
ir_previous=int(ir)
i=1
if int(ir) == int(List_irs[-1]):
rr = client.read_input_registers(int(ir_start),i,unit=args.unid)
subprocess.Popen(['/usr/bin/php',mymodbus,'add='+args.host,'unit='+str(args.unid),'eqid='+str(args.eqid),'type=input_registers','sortie=4','inputs='+str(list(range(int(ir_start),int(ir_start)+i))),'values='+str(rr.registers)])
#lecture des valeurs signées
if (args.sign) != None :
List_sign = (args.sign).split(',')
i = 1
int_first=List_sign[0]
for sign_16 in List_sign:
rr = client.read_holding_registers(int(sign_16),i,unit=args.unid)
#assert(not rr.isError())
decoder = BinaryPayloadDecoder.fromRegisters(rr.registers,byteorder=Endian.Big,wordorder=Endian.Little)
#print (int (decoder.decode_16bit_int()))
subprocess.Popen(['/usr/bin/php',mymodbus,'add='+args.host,'unit='+str(args.unid),'eqid='+str(args.eqid),'type=sign','sortie=1','inputs='+str(int(sign_16)),'values='+str(int(decoder.decode_16bit_int()))])
#lecture des valeurs à virgules
if (args.virg) != None :
List_virg = (args.virg).split(',')
i= 2
virg_first=List_virg[0]
for virg_reg in List_virg:
rr = client.read_holding_registers(int(virg_reg),i,unit=args.unid)
decoder = BinaryPayloadDecoder.fromRegisters(rr.registers,byteorder=Endian.Big,wordorder=Endian.Little)
subprocess.Popen(['/usr/bin/php',mymodbus,'add='+args.host,'unit='+str(args.unid),'eqid='+str(args.eqid),'type=virg','sortie=1','inputs='+str(int(virg_reg)),'values='+str(float(round(decoder.decode_32bit_float(),2)))])
#lecture des imputregisters swapées
if (args.swapi32) != None :
List_swapi32 = (args.swapi32).split(',')
i= 2
swapi32_first=List_swapi32[0]
for swapi32_reg in List_swapi32:
rr = client.read_input_registers(int(swapi32_reg),i,unit=args.unid)
decoder = BinaryPayloadDecoder.fromRegisters(rr.registers,byteorder=Endian.Big,wordorder=Endian.Big)
subprocess.Popen(['/usr/bin/php',mymodbus,'add='+args.host,'unit='+str(args.unid),'eqid='+str(args.eqid),'type=swapi32','sortie=1','inputs='+str(int(swapi32_reg)),'values='+str(float(round(decoder.decode_32bit_float(),2)))])
# ----------------------------------------------------------------------- #
# close the client
# ----------------------------------------------------------------------- #
if args.keepopen == 0 :
client.close()
time.sleep(args.polling)
# start polling thread
t = Thread(target=polling_thread)
# set demond
t.daemon = True
t.start()
if __name__ == '__main__':
while True:
if t.is_alive():
pass
#print("Thread_Ok")
else:
#print("thread_Ko")
raise ParameterException('Thread en défaut')
time.sleep(1)
Il suffit d’en faire un fichier mymodbus_demond-.py et de remplacer celui du plugin. Le mieux est de sauvegarder quelque part le précédent en cas de souci pour y revenir facilement.
ATTENTION
Si vous modifiez vous même le fichier, prenez garde à l’indentation car le langage Python y est très sensible. Il est souvent préférable d’utiliser les 4 espaces et non une tabulation.
Pour la partie écriture, c’est exactement le même principe. Le code complet est ci-dessous :
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Code : Write Mymobus
date: 27/02/2021
Auteur: @Bebel27
Version: b2.0
"""
from pymodbus.compat import IS_PYTHON3, PYTHON_VERSION
if IS_PYTHON3 and PYTHON_VERSION >= (3, 4):
print("Version de python ok")
else:
sys.stderr("merci d'installer Python 3 ou de relancer les dépendances Mymodbus")
sys.exit(1)
import time
import sys
import argparse
import os
import subprocess
parser = argparse.ArgumentParser(description='Mymodbus Write')
#-----------Générale---------------------------------------------------------------------
parser.add_argument("--verbosity", help="mode debug")
parser.add_argument("--protocol", type=str ,help="Choix protocole Modbus" ,required=True)
parser.add_argument("--host", type=str ,help="Choix de l'adresse host")
parser.add_argument("--port", type=str ,help="Choix du port", required=True)
parser.add_argument("--unid", type=int ,help="choix Unit Id", required=True)
parser.add_argument("--eqid", type=int ,help="Numero equipement Jeedom")
#------------RTU-----------------------------------------------------------
parser.add_argument("--baudrate", type=int ,help="vitesse de com en bauds")
parser.add_argument("--stopbits", type=int ,help="bit de stop 1 ou 2")
parser.add_argument("--parity", type=int ,help="parity oui ou non ")
parser.add_argument("--bytesize", type=int ,help="Taile du mot 7 ou 8 ")
#-----------Fonctions---------------------------------------------
parser.add_argument("--wsc", type=int ,help="Write single Coil")
parser.add_argument("--whr", type=int, help="Write holding register")
parser.add_argument("--wmhr", type=int ,help="Write multiple holdings registers")
parser.add_argument("--value", type=int ,help="value")
#------------------------------------------------------------------
# Options demandées
#---------------------
#parser.add_argument("--virg", type=str ,help="Holding à virgules")
#parser.add_argument("--swapi32", type=str ,help="inverse 32bit")
#parser.add_argument("--sign", type=str ,help="valeurs signées")
args = parser.parse_args()
#if args.verbosity:
# print("verbosity turned on")
if args.protocol == 'rtu':
from pymodbus.client.sync import ModbusSerialClient as ModbusClient
#client = ModbusClient(method='rtu', port=args.port, timeout=1,baudrate=38400)
client = ModbusClient(method='rtu', port=args.port, timeout=1,stopbits = 1, bytesize = 8, parity = 'N', baudrate= args.baudrate)
if args.protocol == 'tcpip':
from pymodbus.client.sync import ModbusTcpClient as ModbusClient
client = ModbusClient(host=args.host, port=args.port,retries=3, retry_on_empty=True)
if args.protocol == 'rtuovertcp':
from pymodbus.client.sync import ModbusTcpClient as ModbusClient
from pymodbus.transaction import ModbusRtuFramer as ModbusFramer
client = ModbusClient(host=args.host, port=args.port, framer=ModbusFramer)
#if not client.connect():
# print("unable to connect to "+host+":"+str(port))
client.connect()
if (args.wsc) != None :
if (args.value) == 1:
val = True
if (args.value) == 0 :
val = False
rq = client.write_coil(args.wsc, val, unit=args.unid)
assert(not rq.isError()) # test that we are not an error
if (args.whr) != None :
while True:
time.sleep(1)
rq = client.write_register(args.whr, args.value, unit=args.unid)
if (not rq.isError()): # test that we are not an error
break
#rq = client.write_register(args.whr, args.value, unit=args.unid)
#assert(not rq.isError()) # test that we are not an error
if (args.wmhr) != None :
while True:
time.sleep(1)
rq = client.write_registers(args.wmhr, args.value, unit=args.unid)
if (not rq.isError()): # test that we are not an error
break
#rq = client.write_registers(args.wmhr, args.value, unit=args.unid)
client.close()