Skip to content

using asyncio in modbus_tcp#1019

Open
CaeruleusAqua wants to merge 4 commits into
smarthomeNG:developfrom
CaeruleusAqua:modbus_tcp_async
Open

using asyncio in modbus_tcp#1019
CaeruleusAqua wants to merge 4 commits into
smarthomeNG:developfrom
CaeruleusAqua:modbus_tcp_async

Conversation

@CaeruleusAqua

Copy link
Copy Markdown
Contributor

PR Summary

Refactor the modbus_tcp SmartHomeNG plugin to use the pymodbus Async API (AsyncModbusTcpClient) with a plugin-owned asyncio event loop (separate thread). All Modbus operations (connect/read/write) are now non-blocking.

What changed

  • Replaced synchronous ModbusTcpClient with AsyncModbusTcpClient
  • Added persistent connection handling with automatic reconnect/backoff
  • Removed classic scheduler polling for reads; reads run continuously async
  • Reinterpreted cycle:
    • None / 0: no reads, no item updates
    • > 0: buffer latest values, flush to items every cycle seconds (scheduler used only for flushing)
    • < 0: write to items immediately (no buffering)

Why this is useful

  • Enables more accurate timestamps: values are acquired continuously and timestamped at read time, rather than being tied to scheduler execution jitter.
  • Prevents data loss with short cycle times: acquisition runs independently of the flush interval, and the latest value is always available even if scheduler callbacks are delayed.

@CaeruleusAqua

Copy link
Copy Markdown
Contributor Author

As discussed, we will stick with pymodbus 3.10. However, if you prefer, I can also update all plugins to 4.x. I have some time over the holidays.

@Morg42

Morg42 commented Jan 4, 2026

Copy link
Copy Markdown
Member

Sadly, I can't test this, but it sounds promising!

@onkelandy

Copy link
Copy Markdown
Member

Da gäbs jetzt seit 1.11 neue Funktionen für asyncio: https://smarthomeng.github.io/dev_doc/referenz/plugins/asyncio_support.html
Das hue3 Plugin hat das umgesetzt. Ich hab versucht, das hier auch umzusetzen: https://github.com/onkelandy/huawei_sun2000
Habe keine Ahnung, ob ich das richtig gemacht habe, vielleicht kannst du mal drüber schauen? Scheinst dich ja mit dem Kram auszukennen @CaeruleusAqua

Und evtl. bietet es sich an, das modbus Plugin noch etwas zu vereinfachen mit den integrierten shng Methoden?

@CaeruleusAqua

Copy link
Copy Markdown
Contributor Author

Hhmm das mit dem async io hab ich garnicht gesehen...

Eigentlich gehört es hier nicht her.

Aber ich sehe in deinem Code ne Menge probleme, fehlende returns fehlende awaits...

Z.b. awaits self.inverter_write(register

Warum ?
len(ex) == 101 and ex[86:-1] == 'IllegalAddress

Bin gerade nur am Handy daher ist viel schreiben gerade schwer.

Schön, dass es für den inverter ne python lib gibt. Ich würde aber sagen es ist einfacher die Modbusregister selber aus dem Datenblatt zu holen und das modbus plugin zu nehmen, statt nen eigenes Plugin dafür zu schreiben ..

@bmxp

bmxp commented Jan 24, 2026

Copy link
Copy Markdown
Member

Ich denke auch das wir von dem ganzen Modbus Wirrwar eher wegkommen sollten als noch was neues dazuzudichten.

@Morg42

Morg42 commented Jan 25, 2026

Copy link
Copy Markdown
Member

@CaeruleusAqua was meinst du mit fehlenden returns?

@CaeruleusAqua

CaeruleusAqua commented Jan 25, 2026

Copy link
Copy Markdown
Contributor Author

async def check_forstop(self):

return True oder None.... (weil return für fehlt)

Geht sicher auch so, schön ists aber nicht...

@Morg42

Morg42 commented Jan 25, 2026

Copy link
Copy Markdown
Member

Am Ende von jedem def:-Block wird implizit return None ausgeführt. Alle Methode mit return oder return None abzuschließen, ist nur notwendig, wenn man etwas anderes als None zurückgeben möchte.

@CaeruleusAqua

Copy link
Copy Markdown
Contributor Author

Schon klar, die Funktion sollte aber False zurückgeben und nicht None. Funktioniert trotzdem aber schön ist es nicht. Aber vielleicht bin auch nicht Pythonic genug ;-)

@Morg42

Morg42 commented Jan 25, 2026

Copy link
Copy Markdown
Member

Wenn der falsche Typ zurückgegeben wird, hast du völlig Recht. So im Detail habe ich nicht in den Code geschaut...

@onkelandy

Copy link
Copy Markdown
Member

Ich denke auch das wir von dem ganzen Modbus Wirrwar eher wegkommen sollten als noch was neues dazuzudichten.

Worauf beziehst du dich?

@Morg42

Morg42 commented Jan 26, 2026

Copy link
Copy Markdown
Member

Wahrscheinlich darauf, dass ein generelles ModBus-Plugin wartbarer und über die Breite stabiler gehalten werden kann, als gesonderte Plugins für jedes ModBus-angebundene Gerät zu erstellen...

@Tom-Bom-badil

Copy link
Copy Markdown
Contributor

Ich denke auch das wir von dem ganzen Modbus Wirrwar eher wegkommen sollten als noch was neues dazuzudichten.

Offtopic, aber passend zum Thema 'Wirrwarr' : Hendrik (@henfri) hat zu pymodbus kurz vor Weihnachten einen recht interessanten Post geschrieben.

Nachdem Jan Iversen seinen entsprechenden PR gemerged hat (siehe Github-Link im Post), ist es jetzt möglich, für jedes Plugin eine eigene pymodbus-Instanz zu verwenden. Somit kann es eine generelle pymodbus-shNG-Version geben, aber Plugins können individuell von dieser abweichen.

Das sollte zumindest helfen, den gleichzeitigen Updatezwang aus der Vergangenheit (alle Plugins müssen gleichzeitig hochgezogen werden) zu entschärfen. Instanz der jeweils funktionierenden Version im Plugin 'festtackern' sollte reichen (?ggf alternatives pymodbus als Unterverzeichnis im Plugin mitliefern?).

Natürlich hat das noch diverse andere Implikationen (Code-Aktualität, veraltete Bibliotheken, potentielle Inkompatibilitäten zwischen verschiedenen pymodbus- und Python-Versionen); aber zumindest ist es ein denkbarer Ausweg aus dem 'Chaos' und dem Updatezwang - also eine Tür, die vorher noch nicht offen war.

/tom

@onkelandy

Copy link
Copy Markdown
Member

Wahrscheinlich darauf, dass ein generelles ModBus-Plugin wartbarer und über die Breite stabiler gehalten werden kann, als gesonderte Plugins für jedes ModBus-angebundene Gerät zu erstellen...

Ja, das würde ich eh gleich sehen.. ich hätte ja mal visioniert, Modbus ins SDP-Konstrukt zu bringen, aber ich denke, das ist mit asyncio und Co etwas zu kompliziert und wenig zielführend.

Frage ist für mich nur.. das Plugin hier ist ja wirklich nur TCP. Was ist mit Modbus_RTU? Ließe sich das integrieren oder ist das eine ganz andere Sache? Das ist nämlich durch das huawei-solar Modul auch abgedeckt. Inwieweit hier noch andere Vorteile existieren, kann ich schwer abschätzen. Wenn "nichts", dann würde ich auch eher sagen, ein Zusammenführen mit Modbus-Plugin(s)? wäre ein guter Ansatz.

@onkelandy

Copy link
Copy Markdown
Member

Hhmm das mit dem async io hab ich garnicht gesehen...
Warum ? len(ex) == 101 and ex[86:-1] == 'IllegalAddress
Schön, dass es für den inverter ne python lib gibt. Ich würde aber sagen es ist einfacher die Modbusregister selber aus dem Datenblatt zu holen und das modbus plugin zu nehmen, statt nen eigenes Plugin dafür zu schreiben ..

Ich muss dazu sagen, dass das Plugin eigentlich von @CannonRS kommt. Würde aber auch vorschlagen, Infos zum Code am besten in meinem Repo weiterzuführen und uns hier lediglich darüber zu unterhalten, ob eigene Plugins für versch. Modbus-Geräte überhaupt Sinn machen und wenn ja wann.

Die eine Zeile mit der IllegalAddress macht schon Sinn. Das Plugin liefert verschiedene Structs, aber je nach eingesetztem Gerät, sind trotzdem nicht alle Register vorhanden. Da kommt dann "Illegal" retour. Und damit das nicht jedes Mal abgefragt wird, kommt das in eine Skip Liste.

@bmxp

bmxp commented Feb 26, 2026

Copy link
Copy Markdown
Member

Ich denke wir sollten das das auf Async umgeschriebene Plugin als modbus_tcp2 mergen.

@onkelandy

Copy link
Copy Markdown
Member

Meinst du, eine neue Bezeichnung ist nötig? Sind ja irgendwie primär "under the hood" Änderungen oder..? Man kann ja die alte Version mit rein nehmen, dann kann auch jeder die noch nutzen, indem die Versionsnummer angegeben wird.
Würde übrigens schon noch vorschlagen, dass gleich die neuen Methoden, die SHNG ja bereitstellt mitgenutzt werden. Wenn schon neu, dann "richtig".

@msinn

msinn commented Feb 26, 2026

Copy link
Copy Markdown
Member

Aufgrund der grundlegenden Änderungen bei der Einführung von asyncio habe ich auch das hue Plugin als extra version gemerged. Statt asyncio in die bestehende hue2 Plugin Version zu integrieren, habe ich ein hue3 Plugin erzeugt. Damit kann jeder einfach entscheiden, ob die asyncio Version für ihn passt.

@bmxp

bmxp commented Feb 27, 2026

Copy link
Copy Markdown
Member

Genau das ist die Intention. Wer Async gerne haben möchte kann aktiv auf das neue Plugin wechseln.

@CaeruleusAqua

Copy link
Copy Markdown
Contributor Author

Eigentlich möchte ich mich da nicht einmischen, aber ich würde sagen, dass es sinnvoller ist, kein weiteres Plugin dafür zu erstellen. Die müssen letztlich ja auch gewartet werden.

@onkelandy

Copy link
Copy Markdown
Member

Denke auch, dass es für den User nicht verständlich ist, warum Version 1 oder 2 genutzt werden sollte... müssten wir intern vielleicht nochmals diskutieren.
Auch wie wir mit solchen Plugins wie eben dem huawei, helios, kostal, ksem, pluggit, sma, systemair, trovis umgehen wollen. Die bauen alle auf modbus auf. Ist natürlich ein Riesenfass - aber längerfristig würde ich hier Potenzial für einen Umbau a la "SmartDevicePlugin" sehen, oder nicht..?

@CaeruleusAqua möchtest du noch kleine Adaptionen machen, damit die internen neuen Methoden genutzt werden?

@CaeruleusAqua

Copy link
Copy Markdown
Contributor Author

Denke auch, dass es für den User nicht verständlich ist, warum Version 1 oder 2 genutzt werden sollte... müssten wir intern vielleicht nochmals diskutieren. Auch wie wir mit solchen Plugins wie eben dem huawei, helios, kostal, ksem, pluggit, sma, systemair, trovis umgehen wollen. Die bauen alle auf modbus auf. Ist natürlich ein Riesenfass - aber längerfristig würde ich hier Potenzial für einen Umbau a la "SmartDevicePlugin" sehen, oder nicht..?

@CaeruleusAqua möchtest du noch kleine Adaptionen machen, damit die internen neuen Methoden genutzt werden?

Done!

@CaeruleusAqua

CaeruleusAqua commented Mar 1, 2026

Copy link
Copy Markdown
Contributor Author

Nur mal so eine Idee: Wäre es nicht sinnvoll, das Registermapping der anderen Plug-ins einfach als struct in das Modbus-Plug-in zu integrieren? Die machen doch zu 90 % eh nur das Mapping.

@Morg42

Morg42 commented Mar 2, 2026

Copy link
Copy Markdown
Member

Das war schon eine Weile mal als Idee im Raum. Solange es keine (oder vernachlässigbar wenige) Spezialfälle gibt, wo das Mapping für gewisse Funktionen nicht reicht, wäre das tatsächlich die sinnvollste Lösung.

@onkelandy

Copy link
Copy Markdown
Member

Hab's mal getestet - Verbindung scheint zu klappen. Aber zumindest wenn ich 0 Items habe, die gelesen werden, wird offenbar so oft wie möglich gepollt. Also das Logfile stieg innerhalb von Stunden auf GB an. Ob es mit registrierten Items anders läuft, hab ich noch nicht probieren können.

Die struct-Sache zum Ersetzen versch. Plugins werde ich dann mal angehen, zuerst für Huawei und dann nach und nach die anderen.

@bmxp

bmxp commented Jun 4, 2026

Copy link
Copy Markdown
Member

Ich habe bei der ganzen Sache noch einen Featurewunsch: Mein Solar Edge Wechselrichter hat die Eigenart das zwei verschiedene Modbus Register einen Wert bilden: Im einen Register a steht z.B. ein Integer 23518 und im Register b steht dann ebenfalls ein Integer oder Byte. b bildet dann einen Exponenten zur Basis 10 als Multiplikator, also z.B. -2
Ergibt dann Wert = a * 10 ^b Also im Beispiel 235,18
Es müssen also erst alle Register gelesen werden und erst wenn beide Registerwerte gelesen wurden und auch in ihren Wertebereich passen darf der Wert eines Items gesetzt werden.
Die Krücke über eval führt bei WLAN Verbindungen manchmal zu den abstrusesten Modbus Fehlern

@onkelandy

Copy link
Copy Markdown
Member

Hab noch das Problem gefunden, dass die ganze Decoder-Geschichte wohl deprecated ist. So nach und nach komme ich zum richtigen Ergebnis glaub ich. Ich denke, ich teste dann zuerst mal die bisherige asyncio-Version, merge und mach dann einen weiteren PR für den Fix.

Kann jemand das Logging/Polling-Problem von oben bestätigen?

@CaeruleusAqua

Copy link
Copy Markdown
Contributor Author

Das polling problem kann ich bestätigen, ich Versuche das heute Abend zu fixen...

@CaeruleusAqua

Copy link
Copy Markdown
Contributor Author

gister einen Wert bilden: Im einen Register a steht z.B. ein Integer 23518 und im Register b steht dann ebenfalls ein Integer oder Byte. b bildet dann einen Exponenten zur Basis 10 als Multiplikator, also z.B. -2
Ergibt dann Wert = a * 10 ^b Also im Beispiel 235,18
Es müssen also erst alle Register gelesen werden und erst wenn beide Registe

Das bekommen wir hin, aber nicht in diesem PR ;-)

@CaeruleusAqua

Copy link
Copy Markdown
Contributor Author

Ich habe den Fix jetzt in den Branch gemerged.

  • das Polling/Logging-Problem gefixt:

    • wenn keine Read-Items vorhanden sind, läuft die Async-Loop nicht mehr ohne Pause durch
    • dadurch sollte das Logfile nicht mehr innerhalb kurzer Zeit explodieren
  • die deprecated BinaryPayloadDecoder / BinaryPayloadBuilder Geschichte rausgenommen:

    • Lesen/Schreiben läuft jetzt über die convert_from_registers / convert_to_registers Methoden von pymodbus
    • String-Verhalten wurde dabei gegen das alte Verhalten geprüft, damit hier nichts durch Byte-/WordOrder kaputtgeht
  • cycle / crontab Verhalten korrigiert:

    • crontab-only Setups sollten jetzt wieder funktionieren
    • cycle < 0 bleibt weiterhin der direkte/continuous Modus
  • Fehlerlogging beim Lesen gedrosselt:

    • Modbus-Read-Errors gehen jetzt über die vorhandene log_error() Logik
  • Requirements angepasst:

    • pymodbus>=3.8,<3.10, weil die neue Convert-API gebraucht wird

@onkelandy

Copy link
Copy Markdown
Member

Schöne Sache! Was hältst du davon, wenn wir auch gleich schauen, dass das neueste pymodbus 3.13 funktioniert?
bzw. sonst noch aufräumen.
Die parse_item Sache ist ja auch nicht ganz ideal.
Frage wäre, ob es die byteorder überhaupt braucht - hab überall gelesen, dass es bei Modbus TCP eh immer Endian.BIG ist.
Außerdem ist der Code offenbar sehr alt.. denn anstatt byteOrderStr zu setzen und zu checken, ob es die entsprechende item.conf gibt, sollte imho nur byteOrderStr = self.get_iattr_value(item.conf, AttrByteOrder) genutzt werden, weil es ja im plugin.yaml bereits ein default=Endian.BIG gibt, oder..?

byteOrderStr = 'Endian.BIG'
if self.has_iattr(item.conf, AttrByteOrder):
                byteOrderStr = self.get_iattr_value(item.conf, AttrByteOrder)
 try:
                byteOrder = Endian[(str(byteOrderStr).split('.')[-1]).upper()]
            except Exception as e:
                self.logger.warning(f"Invalid byteOrder -> default(Endian.BIG) is used. Error:{e}")
                byteOrder = Endian.BIG

@onkelandy

onkelandy commented Jun 15, 2026

Copy link
Copy Markdown
Member

Also der Fix für 0 Items scheint zu klappen!
Allerdings werden nun die Items kontinuierlich gepollt, also 20-30 Logeinträge pro Sekunde und kein Ende in Sicht. cycle wäre auf 60 gestellt.

Komischerweise habe ich auch das hier im Log:
Jun 15 22:38:32 CEST DEBUG plugins.modbus_tcp init plugins.modbus_tcp.asyncio read HoldingRegister.30348.0 (address.slaveUnit) regCount:4 result:ReadHoldingRegistersResponse(dev_id=0, transaction_id=2389, address=0, count=0, bits=[], registers=[0, 0, 35, 47930], status=1) -- (init.py:__read_Registers_async:810)
Aber das entsprechende Item bleibt auf 0.

@onkelandy

Copy link
Copy Markdown
Member

@CaeruleusAqua kannst du das och anschauen wegen des pollings und dem Problem, dass Items nicht aktualisiert werden?

Ich habe das normale Modbus plugin seit einiger Zeit erfolgreich mit Huawei Solar-Struct im Einsatz. Cool wäre, wenn wir nach und nach Structs für die anderen Modbus-Plugins integrieren könnten.

@CaeruleusAqua

Copy link
Copy Markdown
Contributor Author

Bin dran muss noch testen... versuche diesmal alle pfade durchzutesten...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants