diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
index 16b515b6..4d7624d2 100755
--- a/.devcontainer/devcontainer.json
+++ b/.devcontainer/devcontainer.json
@@ -11,7 +11,6 @@
       "extensions": [
         "ms-python.python",
         "charliermarsh.ruff",
-        "ms-python.black-formatter",
         "ms-python.vscode-pylance",
         "github.vscode-pull-request-github",
         "ryanluker.vscode-coverage-gutters",
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 7da91946..70a5d49c 100755
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -6,14 +6,6 @@ repos:
       - id: check-yaml
       - id: end-of-file-fixer
       - id: trailing-whitespace
-  - repo: local
-    hooks:
-      - id: black
-        name: black
-        entry: black
-        language: system
-        types: [python]
-        require_serial: true
   - repo: https://github.com/pre-commit/mirrors-prettier
     rev: v2.2.1
     hooks:
diff --git a/.prettierignore b/.prettierignore
index bd3739de..c76fb396 100644
--- a/.prettierignore
+++ b/.prettierignore
@@ -1 +1,2 @@
 tests/__snapshots__
+custom_components/foxess_modbus/vendor
diff --git a/.vscode/settings.json b/.vscode/settings.json
index d9035590..a7538c3f 100755
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -27,5 +27,8 @@
       "source.fixAll": "explicit"
     },
     "editor.defaultFormatter": "ms-python.black-formatter"
-  }
+  },
+  "python.analysis.extraPaths": [
+    "./custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4"
+  ]
 }
diff --git a/custom_components/foxess_modbus/client/custom_modbus_tcp_client.py b/custom_components/foxess_modbus/client/custom_modbus_tcp_client.py
index 95f3be77..ef6bdf4a 100644
--- a/custom_components/foxess_modbus/client/custom_modbus_tcp_client.py
+++ b/custom_components/foxess_modbus/client/custom_modbus_tcp_client.py
@@ -5,8 +5,8 @@
 from typing import Any
 from typing import cast
 
-from pymodbus.client import ModbusTcpClient
-from pymodbus.exceptions import ConnectionException
+from ..vendor.pymodbus import ConnectionException
+from ..vendor.pymodbus import ModbusTcpClient
 
 _LOGGER = logging.getLogger(__name__)
 
diff --git a/custom_components/foxess_modbus/client/modbus_client.py b/custom_components/foxess_modbus/client/modbus_client.py
index 8b6f01ee..2752f69c 100644
--- a/custom_components/foxess_modbus/client/modbus_client.py
+++ b/custom_components/foxess_modbus/client/modbus_client.py
@@ -11,14 +11,6 @@
 
 import serial
 from homeassistant.core import HomeAssistant
-from pymodbus.client import ModbusSerialClient
-from pymodbus.client import ModbusUdpClient
-from pymodbus.framer import FramerType
-from pymodbus.pdu import ModbusPDU
-from pymodbus.pdu.register_read_message import ReadHoldingRegistersResponse
-from pymodbus.pdu.register_read_message import ReadInputRegistersResponse
-from pymodbus.pdu.register_write_message import WriteMultipleRegistersResponse
-from pymodbus.pdu.register_write_message import WriteSingleRegisterResponse
 
 from .. import client
 from ..common.types import ConnectionType
@@ -28,6 +20,14 @@
 from ..const import TCP
 from ..const import UDP
 from ..inverter_adapters import InverterAdapter
+from ..vendor.pymodbus import FramerType
+from ..vendor.pymodbus import ModbusPDU
+from ..vendor.pymodbus import ModbusSerialClient
+from ..vendor.pymodbus import ModbusUdpClient
+from ..vendor.pymodbus import ReadHoldingRegistersResponse
+from ..vendor.pymodbus import ReadInputRegistersResponse
+from ..vendor.pymodbus import WriteMultipleRegistersResponse
+from ..vendor.pymodbus import WriteSingleRegisterResponse
 from .custom_modbus_tcp_client import CustomModbusTcpClient
 
 _LOGGER = logging.getLogger(__name__)
diff --git a/custom_components/foxess_modbus/flow/adapter_flow_segment.py b/custom_components/foxess_modbus/flow/adapter_flow_segment.py
index f038b7a2..523793b1 100644
--- a/custom_components/foxess_modbus/flow/adapter_flow_segment.py
+++ b/custom_components/foxess_modbus/flow/adapter_flow_segment.py
@@ -7,8 +7,6 @@
 from homeassistant.data_entry_flow import FlowResult
 from homeassistant.helpers import config_validation as cv
 from homeassistant.helpers.selector import selector
-from pymodbus.exceptions import ConnectionException
-from pymodbus.exceptions import ModbusIOException
 
 from ..client.modbus_client import ModbusClient
 from ..client.modbus_client import ModbusClientFailedError
@@ -23,6 +21,8 @@
 from ..inverter_adapters import InverterAdapter
 from ..inverter_adapters import InverterAdapterType
 from ..modbus_controller import ModbusController
+from ..vendor.pymodbus import ConnectionException
+from ..vendor.pymodbus import ModbusIOException
 from .flow_handler_mixin import FlowHandlerMixin
 from .flow_handler_mixin import ValidationFailedError
 from .inverter_data import InverterData
diff --git a/custom_components/foxess_modbus/manifest.json b/custom_components/foxess_modbus/manifest.json
index 9d774a57..dcbc111e 100755
--- a/custom_components/foxess_modbus/manifest.json
+++ b/custom_components/foxess_modbus/manifest.json
@@ -8,6 +8,5 @@
   "integration_type": "service",
   "iot_class": "local_push",
   "issue_tracker": "https://github.com/nathanmarlor/foxess_modbus/issues",
-  "requirements": ["pymodbus>=3.7.4"],
   "version": "1.0.0"
 }
diff --git a/custom_components/foxess_modbus/modbus_controller.py b/custom_components/foxess_modbus/modbus_controller.py
index abceb8a2..5d29381e 100644
--- a/custom_components/foxess_modbus/modbus_controller.py
+++ b/custom_components/foxess_modbus/modbus_controller.py
@@ -18,7 +18,6 @@
 from homeassistant.helpers import issue_registry
 from homeassistant.helpers.event import async_track_time_interval
 from homeassistant.helpers.issue_registry import IssueSeverity
-from pymodbus.exceptions import ConnectionException
 
 from .client.modbus_client import ModbusClient
 from .client.modbus_client import ModbusClientFailedError
@@ -38,6 +37,7 @@
 from .inverter_profiles import INVERTER_PROFILES
 from .inverter_profiles import InverterModelConnectionTypeProfile
 from .remote_control_manager import RemoteControlManager
+from .vendor.pymodbus import ConnectionException
 
 _LOGGER = logging.getLogger(__name__)
 
diff --git a/custom_components/foxess_modbus/services/update_charge_period_service.py b/custom_components/foxess_modbus/services/update_charge_period_service.py
index 9f28d161..e3d967bb 100644
--- a/custom_components/foxess_modbus/services/update_charge_period_service.py
+++ b/custom_components/foxess_modbus/services/update_charge_period_service.py
@@ -10,13 +10,13 @@
 from homeassistant.core import ServiceCall
 from homeassistant.exceptions import HomeAssistantError
 from homeassistant.helpers import config_validation as cv
-from pymodbus.exceptions import ModbusIOException
 
 from ..const import DOMAIN
 from ..entities.modbus_charge_period_sensors import is_time_value_valid
 from ..entities.modbus_charge_period_sensors import parse_time_value
 from ..entities.modbus_charge_period_sensors import serialize_time_to_value
 from ..modbus_controller import ModbusController
+from ..vendor.pymodbus import ModbusIOException
 from .utils import get_controller_from_friendly_name_or_device_id
 
 _LOGGER: logging.Logger = logging.getLogger(__package__)
diff --git a/custom_components/foxess_modbus/services/write_registers_service.py b/custom_components/foxess_modbus/services/write_registers_service.py
index 3b3fa659..c42b821a 100644
--- a/custom_components/foxess_modbus/services/write_registers_service.py
+++ b/custom_components/foxess_modbus/services/write_registers_service.py
@@ -8,10 +8,10 @@
 from homeassistant.core import ServiceCall
 from homeassistant.exceptions import HomeAssistantError
 from homeassistant.helpers import config_validation as cv
-from pymodbus.exceptions import ModbusIOException
 
 from ..const import DOMAIN
 from ..modbus_controller import ModbusController
+from ..vendor.pymodbus import ModbusIOException
 from .utils import get_controller_from_friendly_name_or_device_id
 
 _LOGGER: logging.Logger = logging.getLogger(__package__)
diff --git a/custom_components/foxess_modbus/vendor/pymodbus/__init__.py b/custom_components/foxess_modbus/vendor/pymodbus/__init__.py
new file mode 100644
index 00000000..9107a25c
--- /dev/null
+++ b/custom_components/foxess_modbus/vendor/pymodbus/__init__.py
@@ -0,0 +1,34 @@
+import sys
+from pathlib import Path
+
+# Update python.analysis.extraPaths in .vscode/settings.json if you change this.
+# If changed, make sure subclasses in modbus_client are still valid!
+sys.path.insert(0, str((Path(__file__).parent / "pymodbus-3.7.4").absolute()))
+
+from pymodbus.client import ModbusSerialClient
+from pymodbus.client import ModbusTcpClient
+from pymodbus.client import ModbusUdpClient
+from pymodbus.exceptions import ConnectionException
+from pymodbus.exceptions import ModbusIOException
+from pymodbus.framer import FramerType
+from pymodbus.pdu import ModbusPDU
+from pymodbus.pdu.register_read_message import ReadHoldingRegistersResponse
+from pymodbus.pdu.register_read_message import ReadInputRegistersResponse
+from pymodbus.pdu.register_write_message import WriteMultipleRegistersResponse
+from pymodbus.pdu.register_write_message import WriteSingleRegisterResponse
+
+sys.path.pop(0)
+
+__all__ = [
+    "ModbusSerialClient",
+    "ModbusTcpClient",
+    "ModbusUdpClient",
+    "ConnectionException",
+    "ModbusIOException",
+    "FramerType",
+    "ModbusPDU",
+    "ReadHoldingRegistersResponse",
+    "ReadInputRegistersResponse",
+    "WriteMultipleRegistersResponse",
+    "WriteSingleRegisterResponse",
+]
diff --git a/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/bin/pymodbus.simulator.exe b/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/bin/pymodbus.simulator.exe
new file mode 100644
index 00000000..51aa2318
Binary files /dev/null and b/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/bin/pymodbus.simulator.exe differ
diff --git a/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus-3.7.4.dist-info/AUTHORS.rst b/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus-3.7.4.dist-info/AUTHORS.rst
new file mode 100644
index 00000000..9f1a8e91
--- /dev/null
+++ b/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus-3.7.4.dist-info/AUTHORS.rst
@@ -0,0 +1,185 @@
+Authors
+=======
+All these versions would not be possible without volunteers!
+
+This is a complete list for each major version.
+
+A big "thank you" to everybody who helped out.
+
+Pymodbus version 3 family
+-------------------------
+Thanks to
+
+- ahcm-dev
+- AKJ7
+- Alex
+- Alex Ruddick
+- Alexander Lanin
+- Alexandre CUER
+- Alois Hockenschlohe
+- Andy Walker
+- Arjan
+- André Srinivasan
+- andrew-harness
+- banana-sun
+- Blaise Thompson
+- CapraTheBest
+- cgernert
+- corollaries
+- Chandler Riehm
+- Chris Hung
+- Christian Krause
+- Daniel Rauber
+- dhoomakethu
+- doelki
+- DominicDataP
+- Dominique Martinet
+- Dries
+- duc996
+- efdx
+- Esco441-91
+- Farzad Panahi
+- Fredo70
+- Gao Fang
+- Ghostkeeper
+- Hangyu Fan
+- Hayden Roche
+- Iktek
+- Ilkka Ollakka
+- Jakob Ruhe
+- Jakob Schlyter
+- James Braza
+- James Cameron
+- James Hilliard
+- jan iversen
+- Jerome Velociter
+- Joe Burmeister
+- John Miko
+- Jonathan Reichelt Gjertsen
+- julian
+- Justin Standring
+- Kenny Johansson
+- Kürşat Aktaş
+- laund
+- Logan Gunthorpe
+- Marko Luther
+- Martyy
+- Máté Szabó
+- Matthias Straka
+- Matthias Urlichs
+- Michel F
+- Mickaël Schoentgen
+- Pavel Kostromitinov
+- peufeu2
+- Philip Couling
+- Qi Li
+- Sebastian Machuca
+- Sefa Keleş
+- Steffen Beyer
+- sumguytho
+- Thijs W
+- Totally a booplicate
+- WouterTuinstra
+- wriswith
+- Yash Jani
+- Yohrog
+- yyokusa
+
+
+Pymodbus version 2 family
+-------------------------
+Thanks to
+
+- alecjohanson
+- Alexey Andreyev
+- Andrea Canidio
+- Carlos Gomez
+- Cougar
+- Christian Sandberg
+- dhoomakethu
+- dices
+- Dmitri Zimine
+- Emil Vanherp
+- er888kh
+- Eric Duminil
+- Erlend Egeberg Aasland
+- hackerboygn
+- Jian-Hong Pan
+- Jose J Rodriguez
+- Justin Searle
+- Karl Palsson
+- Kim Hansen
+- Kristoffer Sjöberg
+- Kyle Altendorf
+- Lars Kruse
+- Malte Kliemann
+- Memet Bilgin
+- Michael Corcoran
+- Mike
+- sanjay
+- Sekenre
+- Siarhei Farbotka
+- Steffen Vogel
+- tcplomp
+- Thor Michael Støre
+- Tim Gates
+- Ville Skyttä
+- Wild Stray
+- Yegor Yefremov
+
+
+Pymodbus version 1 family
+-------------------------
+Thanks to
+
+- Antoine Pitrou
+- Bart de Waal
+- bashwork
+- bje-
+- Claudio Catterina
+- Chintalagiri Shashank
+- dhoomakethu
+- dragoshenron
+- Elvis Stansvik
+- Eren Inan Canpolat
+- Everley
+- Fabio Bonelli
+- fleimgruber
+- francozappa
+- Galen Collins
+- Gordon Broom
+- Hamilton Kibbe
+- Hynek Petrak
+- idahogray
+- Ingo van Lil
+- Jack
+- jbiswas
+- jon mills
+- Josh Kelley
+- Karl Palsson
+- Matheus Frata
+- Patrick Fuller
+- Perry Kundert
+- Philippe Gauthier
+- Rahul Raghunath
+- sanjay
+- schubduese42
+- semyont
+- Semyon Teplitsky
+- Stuart Longland
+- Yegor Yefremov
+
+
+Pymodbus version 0 family
+-------------------------
+Thanks to
+
+- Albert Brandl
+- Galen Collins
+
+Import to github was based on code from:
+
+- S.W.A.C. GmbH, Germany.
+- S.W.A.C. Bohemia s.r.o., Czech Republic.
+- Hynek Petrak
+- Galen Collins
diff --git a/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus-3.7.4.dist-info/INSTALLER b/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus-3.7.4.dist-info/INSTALLER
new file mode 100644
index 00000000..a1b589e3
--- /dev/null
+++ b/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus-3.7.4.dist-info/INSTALLER
@@ -0,0 +1 @@
+pip
diff --git a/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus-3.7.4.dist-info/LICENSE b/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus-3.7.4.dist-info/LICENSE
new file mode 100644
index 00000000..d3dda3d9
--- /dev/null
+++ b/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus-3.7.4.dist-info/LICENSE
@@ -0,0 +1,23 @@
+Copyright 2008-2023 Pymodbus
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions
+are met:
+1. Redistributions of source code must retain the above copyright
+   notice, this list of conditions and the following disclaimer.
+2. Redistributions in binary form must reproduce the above copyright
+   notice, this list of conditions and the following disclaimer in the
+   documentation and/or other materials provided with the distribution.
+3. The name of the author may not be used to endorse or promote products
+   derived from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
+IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
+INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus-3.7.4.dist-info/METADATA b/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus-3.7.4.dist-info/METADATA
new file mode 100644
index 00000000..5e895905
--- /dev/null
+++ b/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus-3.7.4.dist-info/METADATA
@@ -0,0 +1,423 @@
+Metadata-Version: 2.1
+Name: pymodbus
+Version: 3.7.4
+Summary: A fully featured modbus protocol stack in python
+Author: Galen Collins, Jan Iversen
+Maintainer: dhoomakethu, janiversen
+License: BSD-3-Clause
+Project-URL: Homepage, https://github.com/pymodbus-dev/pymodbus/
+Project-URL: Source Code, https://github.com/pymodbus-dev/pymodbus
+Project-URL: Bug Reports, https://github.com/pymodbus-dev/pymodbus/issues
+Project-URL: Docs: Dev, https://pymodbus.readthedocs.io/en/latest/?badge=latest
+Project-URL: Discord, https://discord.gg/vcP8qAz2
+Keywords: modbus,asyncio,scada,client,server,simulator
+Platform: 'Linux'
+Platform: 'Mac OS X'
+Platform: 'Win'
+Classifier: Development Status :: 5 - Production/Stable
+Classifier: Environment :: Console
+Classifier: Framework :: AsyncIO
+Classifier: Intended Audience :: Developers
+Classifier: License :: OSI Approved :: BSD License
+Classifier: Operating System :: POSIX :: Linux
+Classifier: Operating System :: Unix
+Classifier: Operating System :: MacOS :: MacOS X
+Classifier: Operating System :: OS Independent
+Classifier: Operating System :: Microsoft
+Classifier: Programming Language :: Python :: 3.9
+Classifier: Programming Language :: Python :: 3.10
+Classifier: Programming Language :: Python :: 3.11
+Classifier: Programming Language :: Python :: 3.12
+Classifier: Programming Language :: Python :: 3.13
+Classifier: Topic :: System :: Networking
+Classifier: Topic :: Utilities
+Requires-Python: >=3.9.0
+Description-Content-Type: text/x-rst
+License-File: LICENSE
+License-File: AUTHORS.rst
+Provides-Extra: all
+Requires-Dist: pymodbus[development,documentation,repl,serial,simulator] ; extra == 'all'
+Provides-Extra: development
+Requires-Dist: build >=1.2.2 ; extra == 'development'
+Requires-Dist: codespell >=2.3.0 ; extra == 'development'
+Requires-Dist: coverage >=7.6.1 ; extra == 'development'
+Requires-Dist: mypy >=1.11.2 ; extra == 'development'
+Requires-Dist: pylint >=3.3.0 ; extra == 'development'
+Requires-Dist: pytest >=8.3.3 ; extra == 'development'
+Requires-Dist: pytest-asyncio >=0.24.0 ; extra == 'development'
+Requires-Dist: pytest-cov >=5.0.0 ; extra == 'development'
+Requires-Dist: pytest-timeout >=2.3.1 ; extra == 'development'
+Requires-Dist: pytest-xdist >=3.6.1 ; extra == 'development'
+Requires-Dist: pytest-aiohttp >=1.0.5 ; extra == 'development'
+Requires-Dist: ruff >=0.5.3 ; extra == 'development'
+Requires-Dist: twine >=5.1.1 ; extra == 'development'
+Requires-Dist: types-Pygments ; extra == 'development'
+Requires-Dist: types-pyserial ; extra == 'development'
+Requires-Dist: pytest-profiling >=1.7.0 ; (python_version < "3.13") and extra == 'development'
+Provides-Extra: documentation
+Requires-Dist: recommonmark >=0.7.1 ; extra == 'documentation'
+Requires-Dist: Sphinx >=7.3.7 ; extra == 'documentation'
+Requires-Dist: sphinx-rtd-theme >=2.0.0 ; extra == 'documentation'
+Provides-Extra: repl
+Requires-Dist: pymodbus-repl >=2.0.4 ; extra == 'repl'
+Provides-Extra: serial
+Requires-Dist: pyserial >=3.5 ; extra == 'serial'
+Provides-Extra: simulator
+Requires-Dist: aiohttp >=3.8.6 ; (python_version < "3.12") and extra == 'simulator'
+Requires-Dist: aiohttp >=3.10.5 ; (python_version >= "3.12") and extra == 'simulator'
+
+PyModbus - A Python Modbus Stack
+================================
+.. image:: https://github.com/pymodbus-dev/pymodbus/actions/workflows/ci.yml/badge.svg?branch=dev
+   :target: https://github.com/pymodbus-dev/pymodbus/actions/workflows/ci.yml
+.. image:: https://readthedocs.org/projects/pymodbus/badge/?version=latest
+   :target: https://pymodbus.readthedocs.io/en/latest/?badge=latest
+   :alt: Documentation Status
+.. image:: https://pepy.tech/badge/pymodbus
+   :target: https://pepy.tech/project/pymodbus
+   :alt: Downloads
+.. image:: https://img.shields.io/badge/Gurubase-Ask%20PyModbus%20Guru-006BFF
+   :target: https://gurubase.io/g/pymodbus
+   :alt: PyModbus Guru
+
+Pymodbus is a full Modbus protocol implementation offering client/server with synchronous/asynchronous API and simulators.
+
+Our releases is defined as X.Y.Z, and we have strict rules what to release when:
+
+- **Z**, No API changes! bug fixes and smaller enhancements.
+- **Y**, API changes, bug fixes and bigger enhancements.
+- **X**, Major changes in API and/or method to use pymodbus
+
+Upgrade examples:
+
+- 3.6.1 -> 3.6.9: just plugin the new version, no changes needed.
+- 3.6.1 -> 3.7.0: Smaller changes to the pymodbus calls might be needed
+- 2.5.4 -> 3.0.0: Major changes in the application might be needed
+
+Current release is `3.7.4 <https://github.com/pymodbus-dev/pymodbus/releases/tag/v3.7.4>`_.
+
+Bleeding edge (not released) is `dev <https://github.com/pymodbus-dev/pymodbus/tree/dev>`_.
+
+Waiting for v3.8.0 (not released) is `wait_next_API <https://github.com/pymodbus-dev/pymodbus/tree/wait_next_API>`_. This contains
+dev + merged pull requests that have API changes, and thus have to wait.
+
+All changes are described in `release notes <https://pymodbus.readthedocs.io/en/latest/source/changelog.html>`_
+and all API changes are `documented <https://pymodbus.readthedocs.io/en/latest/source/api_changes.html>`_
+
+A big thanks to all the `volunteers <https://pymodbus.readthedocs.io/en/latest/source/authors.html>`_ that helps make pymodbus a great project.
+
+Source code on `github <https://github.com/pymodbus-dev/pymodbus>`_
+
+Pymodbus in a nutshell
+----------------------
+Pymodbus consist of 5 parts:
+
+- **client**, connect to your favorite device(s)
+- **server**, simulate your favorite device(s)
+- **repl**, a commandline text based client/server simulator
+- **simulator**, an html based server simulator
+- **examples**, showing both simple and advances usage
+
+Common features
+^^^^^^^^^^^^^^^
+* Full modbus standard protocol implementation
+* Support for custom function codes
+* support serial (rs-485), tcp, tls and udp communication
+* support all standard frames: socket, rtu, rtu-over-tcp, tcp and ascii
+* does not have third party dependencies, apart from pyserial (optional)
+* very lightweight project
+* requires Python >= 3.9
+* thorough test suite, that test all corners of the library
+* automatically tested on Windows, Linux and MacOS combined with python 3.9 - 3.13
+* strongly typed API (py.typed present)
+
+The modbus protocol specification: Modbus_Application_Protocol_V1_1b3.pdf can be found on
+`modbus org <https://modbus.org>`_
+
+
+Client Features
+^^^^^^^^^^^^^^^
+* asynchronous API and synchronous API for applications
+* very simple setup and call sequence (just 6 lines of code)
+* utilities to convert int/float to/from multiple registers
+* payload builder/decoder to help with complex data
+
+`Client documentation <https://pymodbus.readthedocs.io/en/latest/source/client.html>`_
+
+
+Server Features
+^^^^^^^^^^^^^^^
+* asynchronous implementation for high performance
+* synchronous API classes for convenience
+* simulate real life devices
+* full server control context (device information, counters, etc)
+* different backend datastores to manage register values
+* callback to intercept requests/responses
+* work on RS485 in parallel with other devices
+
+`Server documentation <https://pymodbus.readthedocs.io/en/latest/source/server.html>`_
+
+
+REPL Features
+^^^^^^^^^^^^^
+- server/client commandline emulator
+- easy test of real device (client)
+- easy test of client app (server)
+- simulation of broken requests/responses
+- simulation of error responses (hard to provoke in real devices)
+
+`REPL documentation <https://github.com/pymodbus-dev/repl>`_
+
+
+Simulator Features
+^^^^^^^^^^^^^^^^^^
+- server simulator with WEB interface
+- configure the structure of a real device
+- monitor traffic online
+- allow distributed team members to work on a virtual device using internet
+- simulation of broken requests/responses
+- simulation of error responses (hard to provoke in real devices)
+
+`Simulator documentation <https://pymodbus.readthedocs.io/en/dev/source/simulator.html>`_
+
+Use Cases
+---------
+The client is the most typically used. It is embedded into applications,
+where it abstract the modbus protocol from the application by providing an
+easy to use API. The client is integrated into some well known projects like
+`home-assistant <https://www.home-assistant.io>`_.
+
+Although most system administrators will find little need for a Modbus
+server, the server is handy to verify the functionality of an application.
+
+The simulator and/or server is often used to simulate real life devices testing
+applications. The server is excellent to perform high volume testing (e.g.
+houndreds of devices connected to the application). The advantage of the server is
+that it runs not only a "normal" computers but also on small ones like Raspberry PI.
+
+Since the library is written in python, it allows for easy scripting and/or integration into their existing
+solutions.
+
+For more information please browse the project documentation:
+
+https://readthedocs.org/docs/pymodbus/en/latest/index.html
+
+
+
+Install
+-------
+The library is available on pypi.org and github.com to install with
+
+- :code:`pip` for those who just want to use the library
+- :code:`git clone` for those who wants to help or just are curious
+
+Be aware that there are a number of project, who have forked pymodbus and
+
+- seems just to provide a version frozen in time
+- extended pymodbus with extra functionality
+
+The latter is not because we rejected the extra functionality (we welcome all changes),
+but because the codeowners made that decision.
+
+In both cases, please understand, we cannot offer support to users of these projects as we do not known
+what have been changed nor what status the forked code have.
+
+A growing number of Linux distributions include pymodbus in their standard installation.
+
+You need to have python3 installed, preferable 3.11.
+
+Install with pip
+^^^^^^^^^^^^^^^^
+You can install using pip by issuing the following
+commands in a terminal window::
+
+   pip install pymodbus
+
+If you want to use the serial interface::
+
+   pip install pymodbus[serial]
+
+This will install pymodbus with the pyserial dependency.
+
+Pymodbus offers a number of extra options:
+
+- **repl**, needed by pymodbus.repl
+- **serial**, needed for serial communication
+- **simulator**, needed by pymodbus.simulator
+- **documentation**, needed to generate documentation
+- **development**, needed for development
+- **all**, installs all of the above
+
+which can be installed as::
+
+   pip install pymodbus[<option>,...]
+
+It is possible to install old releases if needed::
+
+   pip install pymodbus==3.5.4
+
+
+Install with github
+^^^^^^^^^^^^^^^^^^^
+On github, fork https://github.com/pymodbus-dev/pymodbus.git
+
+Clone the source, and make a virtual environment::
+
+
+   git clone git://github.com/<your account>/pymodbus.git
+   cd pymodbus
+   python3 -m venv .venv
+
+Activate the virtual environment, this command needs repeated in every new terminal::
+
+   source .venv/bin/activate
+
+To get a specific release::
+
+   git checkout v3.5.2
+
+or the bleeding edge::
+
+   git checkout dev
+
+Some distributions have an old pip, which needs to be upgraded:
+
+   pip install --upgrade pip
+
+Install required development tools::
+
+   pip install ".[development]"
+
+Install all (allows creation of documentation etc):
+
+   pip install ".[all]"
+
+Install git hooks, that helps control the commit and avoid errors when submitting a Pull Request:
+
+  cp githooks/* .git/hooks
+
+This installs dependencies in your virtual environment
+with pointers directly to the pymodbus directory,
+so any change you make is immediately available as if installed.
+
+The repository contains a number of important branches and tags.
+  * **dev** is where all development happens, this branch is not always stable.
+  * **master** is where are releases are kept.
+  * **vX.Y.Z** (e.g. v2.5.3) is a specific release
+
+
+Example Code
+------------
+For those of you that just want to get started fast, here you go::
+
+    from pymodbus.client import ModbusTcpClient
+
+    client = ModbusTcpClient('MyDevice.lan')
+    client.connect()
+    client.write_coil(1, True)
+    result = client.read_coils(1,1)
+    print(result.bits[0])
+    client.close()
+
+We provide a couple of simple ready to go clients:
+
+- `async client <https://github.com/pymodbus-dev/pymodbus/blob/dev/examples/simple_async_client.py>`_
+- `sync client <https://github.com/pymodbus-dev/pymodbus/blob/dev/examples/simple_sync_client.py>`_
+
+For more advanced examples, check out `Examples <https://pymodbus.readthedocs.io/en/dev/source/examples.html>`_ included in the
+repository. If you have created any utilities that meet a specific
+need, feel free to submit them so others can benefit.
+
+Also, if you have a question, please `create a post in discussions q&a topic <https://github.com/pymodbus-dev/pymodbus/discussions/new?category=q-a>`_,
+so that others can benefit from the results.
+
+If you think, that something in the code is broken/not running well, please `open an issue <https://github.com/pymodbus-dev/pymodbus/issues/new>`_,
+read the Template-text first and then post your issue with your setup information.
+
+`Example documentation <https://pymodbus.readthedocs.io/en/dev/source/examples.html>`_
+
+
+Contributing
+------------
+Just fork the repo and raise your Pull Request against :code:`dev` branch.
+
+We always have more work than time, so feel free to open a discussion / issue on a theme you want to solve.
+
+If your company would like your device tested or have a cloud based device
+simulation, feel free to contact us.
+We are happy to help your company solve your modbus challenges.
+
+That said, the current work mainly involves polishing the library and
+solving issues:
+
+* Fixing bugs/feature requests
+* Architecture documentation
+* Functional testing against any reference we can find
+
+There are 2 bigger projects ongoing:
+
+   * rewriting the internal part of all clients (both sync and async)
+   * Add features to and simulator, and enhance the web design
+
+
+Development instructions
+------------------------
+The current code base is compatible with python >= 3.9.
+
+Here are some of the common commands to perform a range of activities::
+
+   source .venv/bin/activate   <-- Activate the virtual environment
+   ./check_ci.sh               <-- run the same checks as CI runs on a pull request.
+
+
+Make a pull request::
+
+   git checkout dev          <-- activate development branch
+   git pull                  <-- update branch with newest changes
+   git checkout -b feature   <-- make new branch for pull request
+   ... make source changes
+   git commit                <-- commit change to git
+   git push                  <-- push to your account on github
+
+   on github open a pull request, check that CI turns green and then wait for review comments.
+
+Test your changes::
+
+   cd test
+   pytest
+
+you can also do extended testing::
+
+   pytest --cov         <-- Coverage html report in build/html
+   pytest --profile     <-- Call profile report in prof
+
+Internals
+^^^^^^^^^
+
+There are no documentation of the architecture (help is welcome), but most classes and
+methods are documented:
+
+`Pymodbus internals <https://pymodbus.readthedocs.io/en/dev/source/internals.html>`_
+
+
+Generate documentation
+^^^^^^^^^^^^^^^^^^^^^^
+
+**Remark** Assumes that you have installed documentation tools:;
+
+   pip install ".[documentation]"
+
+to build do::
+
+   cd doc
+   ./build_html
+
+The documentation is available in <root>/build/html
+
+Remark: this generates a new zip/tgz file of examples which are uploaded.
+
+
+License Information
+-------------------
+
+Released under the `BSD License <https://github.com/pymodbus-dev/pymodbus/blob/dev/LICENSE>`_
diff --git a/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus-3.7.4.dist-info/RECORD b/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus-3.7.4.dist-info/RECORD
new file mode 100644
index 00000000..e8f356ac
--- /dev/null
+++ b/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus-3.7.4.dist-info/RECORD
@@ -0,0 +1,122 @@
+../../bin/pymodbus.simulator.exe,sha256=jBfNi1RbmT59KnnTPmE6BcrGdRCv_9RSwOkx2exzTh8,108388
+pymodbus-3.7.4.dist-info/AUTHORS.rst,sha256=-3j3nlLKABLEBIQ0PdEe8i8HEN0-SrHcgZIAcQSSdxk,2761
+pymodbus-3.7.4.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
+pymodbus-3.7.4.dist-info/LICENSE,sha256=R_kEJJFy55Wmsy3pH0cQcyl77__XThfK_CiNNIpVly4,1367
+pymodbus-3.7.4.dist-info/METADATA,sha256=v4vK2xwTwj7cPnclxhmsj7bu01k3-nPWIXFokCDNlLg,15450
+pymodbus-3.7.4.dist-info/RECORD,,
+pymodbus-3.7.4.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
+pymodbus-3.7.4.dist-info/WHEEL,sha256=OVMc5UfuAQiSplgO0_WdW7vXVGAt9Hdd6qtN4HotdyA,91
+pymodbus-3.7.4.dist-info/entry_points.txt,sha256=-ig4bOfoNwZ2C3GqUWs2DgArzF8YzgdlmcWOUa1EjTc,75
+pymodbus-3.7.4.dist-info/top_level.txt,sha256=HJWcaj1-eMuBze0N2TieOGTgQD6XNXgTaqG7Ik6oxw0,9
+pymodbus-3.7.4.dist-info/zip-safe,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
+pymodbus/__init__.py,sha256=zY0kf_meZKxvNPDt4C65XoLc-vl1xOSKqKzwrDVwSRY,512
+pymodbus/__pycache__/__init__.cpython-313.pyc,,
+pymodbus/__pycache__/constants.cpython-313.pyc,,
+pymodbus/__pycache__/device.cpython-313.pyc,,
+pymodbus/__pycache__/events.cpython-313.pyc,,
+pymodbus/__pycache__/exceptions.cpython-313.pyc,,
+pymodbus/__pycache__/logging.cpython-313.pyc,,
+pymodbus/__pycache__/payload.cpython-313.pyc,,
+pymodbus/__pycache__/transaction.cpython-313.pyc,,
+pymodbus/__pycache__/utilities.cpython-313.pyc,,
+pymodbus/client/__init__.py,sha256=fkvpNNXZfVpLtYbDHpf7cAIcHq-iNtrMLljySOD-Sds,603
+pymodbus/client/__pycache__/__init__.cpython-313.pyc,,
+pymodbus/client/__pycache__/base.cpython-313.pyc,,
+pymodbus/client/__pycache__/mixin.cpython-313.pyc,,
+pymodbus/client/__pycache__/modbusclientprotocol.cpython-313.pyc,,
+pymodbus/client/__pycache__/serial.cpython-313.pyc,,
+pymodbus/client/__pycache__/tcp.cpython-313.pyc,,
+pymodbus/client/__pycache__/tls.cpython-313.pyc,,
+pymodbus/client/__pycache__/udp.cpython-313.pyc,,
+pymodbus/client/base.py,sha256=N9-0yt9jTYQOa1R1OM1hZzs2fjEr8Lx9P0rwL55rYRk,10460
+pymodbus/client/mixin.py,sha256=_YOcVDw25fR9ho1q_GM9cKJL8G8ygZlsbJNlF0hrtKs,23947
+pymodbus/client/modbusclientprotocol.py,sha256=pdCGZivHwBcb1lEouM7eDTC3mr2cSefN30_PzXtKd5I,2712
+pymodbus/client/serial.py,sha256=E3e2N0xd5yoEzyF6emSviZTsGR9gv6cO3lN-CSIVar8,12351
+pymodbus/client/tcp.py,sha256=RsCJdPB1fkDAvVrR5Aa8UXnzH9pGoB6pXZxupkftoUI,10182
+pymodbus/client/tls.py,sha256=uLz6eosdVsIpDNtfFfVM1RkgXszrY5MbEjcIXLwFFZk,7788
+pymodbus/client/udp.py,sha256=Ci7-Oo5X2vnn4uxKVcpt1hQn5q2MX7PbDjJyi8YomVk,6817
+pymodbus/constants.py,sha256=SY8UeXMtCiNrzPidW8drNmv07PuNsConfrsI6CEcNs8,3499
+pymodbus/datastore/__init__.py,sha256=7DoMjoxfHQYq_7Sn9buUq4dOOHzNot113AmrwgEHliE,491
+pymodbus/datastore/__pycache__/__init__.cpython-313.pyc,,
+pymodbus/datastore/__pycache__/context.cpython-313.pyc,,
+pymodbus/datastore/__pycache__/remote.cpython-313.pyc,,
+pymodbus/datastore/__pycache__/simulator.cpython-313.pyc,,
+pymodbus/datastore/__pycache__/store.cpython-313.pyc,,
+pymodbus/datastore/context.py,sha256=eICS-XBGrumZw81rpd7_WuRXllzU6qJNXEZjOoWpc_U,9061
+pymodbus/datastore/remote.py,sha256=3NbJWbFc3NOgHuIV4jFtxY0tCKjUTuaZM9eXI3luz80,4418
+pymodbus/datastore/simulator.py,sha256=H-5Y_6UiYF3kpo0-IY3e-BxMaSSJVyvyabjcS16WDIs,30931
+pymodbus/datastore/store.py,sha256=-XRkFlbj6igeSU7WXf754fJJg53OwlSTvO2g9LgzHt0,12036
+pymodbus/device.py,sha256=5-Uja0DjcX99WIW6rUEX3PiROeOkRhdRAJOsIPRWtcY,22039
+pymodbus/events.py,sha256=EG5BJ3liCtCmqVXWPTL4AsvI8gyofMu93iJ04r8YLCM,6318
+pymodbus/exceptions.py,sha256=cxHWXIj9LWBa8Ckvvbki3J36jySB0fFJiyEd9h5bkv4,3189
+pymodbus/framer/__init__.py,sha256=j_lGV9ifsHUmiZeHf15smPenmsT0A2-c4Kwdgjpk43Q,534
+pymodbus/framer/__pycache__/__init__.cpython-313.pyc,,
+pymodbus/framer/__pycache__/ascii.cpython-313.pyc,,
+pymodbus/framer/__pycache__/base.cpython-313.pyc,,
+pymodbus/framer/__pycache__/rtu.cpython-313.pyc,,
+pymodbus/framer/__pycache__/socket.cpython-313.pyc,,
+pymodbus/framer/__pycache__/tls.cpython-313.pyc,,
+pymodbus/framer/ascii.py,sha256=Pm4Dxm_q8psdd8ZPPipip5x1VzA18gJe7ako7V3xUAw,3011
+pymodbus/framer/base.py,sha256=ksXcHqHPFbuAFZZUwiBNrr4nKd4wIXvrKJBe3J2ATBg,3068
+pymodbus/framer/rtu.py,sha256=0gs2k9CYz5ZB_78QiRpaCoOWwY_IJU-3Wptf5ZxJtfQ,5951
+pymodbus/framer/socket.py,sha256=bqXNfBRvKsvIXNePlePFJBfLQjcKqWIALaFpn9N_PRU,1444
+pymodbus/framer/tls.py,sha256=TAaz-T9HtcpNh9RzqXHhsNyk2r8E3pPx50ShmJOFtzo,509
+pymodbus/logging.py,sha256=5TbQaW_VZZyds1m4Dt8QFFNe-Y3BT5Jmt4OmW8VQlvI,3996
+pymodbus/payload.py,sha256=cEzL6jij8bT1IiQUVXUDXRNgoxjPFqo6CaacY_JJGBM,15402
+pymodbus/pdu/__init__.py,sha256=sNWx9obbcXAdtyybsJvndpzAkHx5y5kNEFD4C53tgEc,257
+pymodbus/pdu/__pycache__/__init__.cpython-313.pyc,,
+pymodbus/pdu/__pycache__/bit_read_message.cpython-313.pyc,,
+pymodbus/pdu/__pycache__/bit_write_message.cpython-313.pyc,,
+pymodbus/pdu/__pycache__/decoders.cpython-313.pyc,,
+pymodbus/pdu/__pycache__/diag_message.cpython-313.pyc,,
+pymodbus/pdu/__pycache__/file_message.cpython-313.pyc,,
+pymodbus/pdu/__pycache__/mei_message.cpython-313.pyc,,
+pymodbus/pdu/__pycache__/other_message.cpython-313.pyc,,
+pymodbus/pdu/__pycache__/pdu.cpython-313.pyc,,
+pymodbus/pdu/__pycache__/register_read_message.cpython-313.pyc,,
+pymodbus/pdu/__pycache__/register_write_message.cpython-313.pyc,,
+pymodbus/pdu/bit_read_message.py,sha256=6vi252pm59xB9jsuz4Ffv3sDLPJO9T7PTbER5snk_IQ,9340
+pymodbus/pdu/bit_write_message.py,sha256=wZHUL_NqsrKXu6Jm8qdzsazB22lzjJnpC5ZmU1-E24k,9237
+pymodbus/pdu/decoders.py,sha256=OsQeLqQ0BVUk1kRa0zrycxl_G2j1IghAq7mAxAWDpuo,6642
+pymodbus/pdu/diag_message.py,sha256=kbRw10nPXo_5hyVVemApBcgHLz_Ofn-cun3VwIfGZRQ,30729
+pymodbus/pdu/file_message.py,sha256=baO1o7WNftZ8agOZrKqgaOwyWiQZ0tcKXKqgJ_YzqGs,15316
+pymodbus/pdu/mei_message.py,sha256=zHJNc1UaZ2g17EdE6Adfrt6LJiyCASSN4rcxEGp-m2Q,7972
+pymodbus/pdu/other_message.py,sha256=9f_Lv3PcNtCbAKxty7RdDeU0oRtMVNs5-x1Dr5x616g,15541
+pymodbus/pdu/pdu.py,sha256=QjVeb7NTBGmA45UbuUOTrfPwcqPLcjru5OQd24_Iswk,3992
+pymodbus/pdu/register_read_message.py,sha256=PGwsJ43Ccr0OEsBuUNEpZo34DMpxo8MDSWwR0QcDPiQ,13564
+pymodbus/pdu/register_write_message.py,sha256=K1P2MHsifKa-pfFCLj9Z9M2TFbMwW4kC_6svm2ckOWc,12804
+pymodbus/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
+pymodbus/server/__init__.py,sha256=q6EiG-hXtjtj-OP5a39qmUPlFiF18HuAnk7J0j3JNJE,996
+pymodbus/server/__pycache__/__init__.cpython-313.pyc,,
+pymodbus/server/__pycache__/async_io.cpython-313.pyc,,
+pymodbus/server/async_io.py,sha256=1B1tlr8xzlqX_QeOR8ycdUKR6canDhBDl342hvbZJ6o,26703
+pymodbus/server/simulator/__init__.py,sha256=TkIbeFNGu-JC0VbN64G4ImZOZdWUTZyXY0x1IQY0OQk,18
+pymodbus/server/simulator/__pycache__/__init__.cpython-313.pyc,,
+pymodbus/server/simulator/__pycache__/custom_actions.cpython-313.pyc,,
+pymodbus/server/simulator/__pycache__/http_server.cpython-313.pyc,,
+pymodbus/server/simulator/__pycache__/main.cpython-313.pyc,,
+pymodbus/server/simulator/custom_actions.py,sha256=96R3tp7u5yGF5P4gNvIde9pY3M5KhlkEgGstHzg-Szk,187
+pymodbus/server/simulator/http_server.py,sha256=bdv2-1F11f_yQcgn875gRWdnvb133flvK8Ipfd7Wsqo,31476
+pymodbus/server/simulator/main.py,sha256=kKceLq968V7u2mnFMpsnn2O-aFeHde6evCEH00A98S4,4200
+pymodbus/server/simulator/setup.json,sha256=SG-qrG50Nh1zM_YHPKm0QIo2gh-zjKorGzsbPLsvODU,7643
+pymodbus/server/simulator/web/apple120.png,sha256=kymnXpkuOnlk13QIlBHZ30RXqYL0Mep6PQ7BMHhON14,11369
+pymodbus/server/simulator/web/apple152.png,sha256=56hV4HGeyfnBlbHgnMt_shWuVTJB7dKXx7EQpt9rjEc,15391
+pymodbus/server/simulator/web/apple60.png,sha256=2YegT0ZTkBCQ3v32S8dmUOqCLdXXg2UnFp4QKYMWGrQ,4817
+pymodbus/server/simulator/web/apple76.png,sha256=LDrwW4YW1ijbx1k2i384wka05U9NvBW5yvNfCTEXDe0,6344
+pymodbus/server/simulator/web/favicon.ico,sha256=-eUdKiH2TBpF3HHawVjLnIaP_Y1oqe2t_hS0dOCY2ZY,12014
+pymodbus/server/simulator/web/generator/calls,sha256=Lr7tTaTf8M7XT2vlrvROqLjyA07miMOExdsi7tmu60A,4314
+pymodbus/server/simulator/web/generator/log,sha256=D-lDQKT0SZoNzTGZL-B05RdjvPdRF_6WxB5mJrpnaGc,1132
+pymodbus/server/simulator/web/generator/pymodbus_icon_original.png,sha256=0VIAZ8wS-SP9qUfBVHaChzcNrAQzojyacFnizcieLIY,5850
+pymodbus/server/simulator/web/generator/registers,sha256=7qD_pRY8szWJp9miATGVdCrUSkV1m0U7ZcRGVPsuq2U,2710
+pymodbus/server/simulator/web/generator/server,sha256=lvkxOEA9HtP9cWhcabWK4veFIiNDpg4HunyvcedVul0,893
+pymodbus/server/simulator/web/index.html,sha256=TQO1qDFdCAvEF5t2yS2xJ-IUuvg1EcYAP_0qTxvex2c,1944
+pymodbus/server/simulator/web/pymodbus.css,sha256=0Nc2PYZ6bEgvOnvvSXICJvAhFJI-m7XOFDrCNIfcieA,919
+pymodbus/server/simulator/web/welcome.html,sha256=6VLVyWdXMIVawjE56IkXITlotkXG-kJhu5zjjzZOXuI,710
+pymodbus/transaction.py,sha256=_Ud1fvt_Lo16LtCp8qZ5gHM5KQXicoa_DrKI8lwMU-U,17512
+pymodbus/transport/__init__.py,sha256=H9lHMyh5as-OlNnfSUYbaa8pFjf1D7QZt0skOWX_C0A,225
+pymodbus/transport/__pycache__/__init__.cpython-313.pyc,,
+pymodbus/transport/__pycache__/serialtransport.cpython-313.pyc,,
+pymodbus/transport/__pycache__/transport.cpython-313.pyc,,
+pymodbus/transport/serialtransport.py,sha256=bWQ-HjFLrWHVmRZXiwOVmHR2IoAvj0eM9SVUTOmigAQ,6223
+pymodbus/transport/transport.py,sha256=gW58_4rd7DZreWQsX2zuhDmhWJ3ZoOM2yU3S_MxvnqU,23242
+pymodbus/utilities.py,sha256=mkmB_x7tKH9VaI4VoPVB9R2-SkHoJmoI-V1qigAzx8Q,4612
diff --git a/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus-3.7.4.dist-info/REQUESTED b/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus-3.7.4.dist-info/REQUESTED
new file mode 100644
index 00000000..e69de29b
diff --git a/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus-3.7.4.dist-info/WHEEL b/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus-3.7.4.dist-info/WHEEL
new file mode 100644
index 00000000..da25d7b4
--- /dev/null
+++ b/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus-3.7.4.dist-info/WHEEL
@@ -0,0 +1,5 @@
+Wheel-Version: 1.0
+Generator: setuptools (75.2.0)
+Root-Is-Purelib: true
+Tag: py3-none-any
+
diff --git a/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus-3.7.4.dist-info/entry_points.txt b/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus-3.7.4.dist-info/entry_points.txt
new file mode 100644
index 00000000..4a799e95
--- /dev/null
+++ b/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus-3.7.4.dist-info/entry_points.txt
@@ -0,0 +1,2 @@
+[console_scripts]
+pymodbus.simulator = pymodbus.server.simulator.main:main
diff --git a/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus-3.7.4.dist-info/top_level.txt b/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus-3.7.4.dist-info/top_level.txt
new file mode 100644
index 00000000..b5140c7e
--- /dev/null
+++ b/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus-3.7.4.dist-info/top_level.txt
@@ -0,0 +1 @@
+pymodbus
diff --git a/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus-3.7.4.dist-info/zip-safe b/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus-3.7.4.dist-info/zip-safe
new file mode 100644
index 00000000..8b137891
--- /dev/null
+++ b/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus-3.7.4.dist-info/zip-safe
@@ -0,0 +1 @@
+
diff --git a/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/__init__.py b/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/__init__.py
new file mode 100644
index 00000000..c2fed39e
--- /dev/null
+++ b/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/__init__.py
@@ -0,0 +1,22 @@
+"""Pymodbus: Modbus Protocol Implementation.
+
+Released under the BSD license
+"""
+
+__all__ = [
+    "ExceptionResponse",
+    "FramerType",
+    "ModbusException",
+    "pymodbus_apply_logging_config",
+    "__version__",
+    "__version_full__",
+]
+
+from pymodbus.exceptions import ModbusException
+from pymodbus.framer import FramerType
+from pymodbus.logging import pymodbus_apply_logging_config
+from pymodbus.pdu import ExceptionResponse
+
+
+__version__ = "3.7.4"
+__version_full__ = f"[pymodbus, version {__version__}]"
diff --git a/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/client/__init__.py b/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/client/__init__.py
new file mode 100644
index 00000000..245bcdf0
--- /dev/null
+++ b/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/client/__init__.py
@@ -0,0 +1,19 @@
+"""Client."""
+
+__all__ = [
+    "AsyncModbusSerialClient",
+    "AsyncModbusTcpClient",
+    "AsyncModbusTlsClient",
+    "AsyncModbusUdpClient",
+    "ModbusBaseClient",
+    "ModbusSerialClient",
+    "ModbusTcpClient",
+    "ModbusTlsClient",
+    "ModbusUdpClient",
+]
+
+from pymodbus.client.base import ModbusBaseClient
+from pymodbus.client.serial import AsyncModbusSerialClient, ModbusSerialClient
+from pymodbus.client.tcp import AsyncModbusTcpClient, ModbusTcpClient
+from pymodbus.client.tls import AsyncModbusTlsClient, ModbusTlsClient
+from pymodbus.client.udp import AsyncModbusUdpClient, ModbusUdpClient
diff --git a/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/client/base.py b/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/client/base.py
new file mode 100644
index 00000000..884d8f01
--- /dev/null
+++ b/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/client/base.py
@@ -0,0 +1,308 @@
+"""Base for all clients."""
+from __future__ import annotations
+
+import asyncio
+import socket
+from abc import abstractmethod
+from collections.abc import Awaitable, Callable
+
+from pymodbus.client.mixin import ModbusClientMixin
+from pymodbus.client.modbusclientprotocol import ModbusClientProtocol
+from pymodbus.exceptions import ConnectionException, ModbusIOException
+from pymodbus.framer import FRAMER_NAME_TO_CLASS, FramerBase, FramerType
+from pymodbus.logging import Log
+from pymodbus.pdu import DecodePDU, ExceptionResponse, ModbusPDU
+from pymodbus.transaction import SyncModbusTransactionManager
+from pymodbus.transport import CommParams
+from pymodbus.utilities import ModbusTransactionState
+
+
+class ModbusBaseClient(ModbusClientMixin[Awaitable[ModbusPDU]]):
+    """**ModbusBaseClient**.
+
+    :mod:`ModbusBaseClient` is normally not referenced outside :mod:`pymodbus`.
+    """
+
+    def __init__(
+        self,
+        framer: FramerType,
+        retries: int,
+        on_connect_callback: Callable[[bool], None] | None,
+        comm_params: CommParams | None = None,
+    ) -> None:
+        """Initialize a client instance.
+
+        :meta private:
+        """
+        ModbusClientMixin.__init__(self)  # type: ignore[arg-type]
+        if comm_params:
+            self.comm_params = comm_params
+        self.retries = retries
+        self.ctx = ModbusClientProtocol(
+            framer,
+            self.comm_params,
+            on_connect_callback,
+        )
+
+        # Common variables.
+        self.use_udp = False
+        self.state = ModbusTransactionState.IDLE
+        self.last_frame_end: float | None = 0
+        self.silent_interval: float = 0
+        self._lock = asyncio.Lock()
+        self.accept_no_response_limit = 3
+        self.count_no_responses = 0
+
+    @property
+    def connected(self) -> bool:
+        """Return state of connection."""
+        return self.ctx.is_active()
+
+    async def connect(self) -> bool:
+        """Call transport connect."""
+        self.ctx.reset_delay()
+        Log.debug(
+            "Connecting to {}:{}.",
+            self.ctx.comm_params.host,
+            self.ctx.comm_params.port,
+        )
+        return await self.ctx.connect()
+
+    def register(self, custom_response_class: type[ModbusPDU]) -> None:
+        """Register a custom response class with the decoder (call **sync**).
+
+        :param custom_response_class: (optional) Modbus response class.
+        :raises MessageRegisterException: Check exception text.
+
+        Use register() to add non-standard responses (like e.g. a login prompt) and
+        have them interpreted automatically.
+        """
+        self.ctx.framer.decoder.register(custom_response_class)
+
+    def close(self) -> None:
+        """Close connection."""
+        self.ctx.close()
+
+    def execute(self, no_response_expected: bool, request: ModbusPDU):
+        """Execute request and get response (call **sync/async**).
+
+        :meta private:
+        """
+        if not self.ctx.transport:
+            raise ConnectionException(f"Not connected[{self!s}]")
+        return self.async_execute(no_response_expected, request)
+
+    async def async_execute(self, no_response_expected: bool, request) -> ModbusPDU | None:
+        """Execute requests asynchronously.
+
+        :meta private:
+        """
+        request.transaction_id = self.ctx.transaction.getNextTID()
+        packet = self.ctx.framer.buildFrame(request)
+
+        count = 0
+        while count <= self.retries:
+            async with self._lock:
+                req = self.build_response(request)
+                self.ctx.send(packet)
+                if no_response_expected:
+                    resp = None
+                    break
+                try:
+                    resp = await asyncio.wait_for(
+                        req, timeout=self.ctx.comm_params.timeout_connect
+                    )
+                    break
+                except asyncio.exceptions.TimeoutError:
+                    count += 1
+        if count > self.retries:
+            if self.count_no_responses >= self.accept_no_response_limit:
+                self.ctx.connection_lost(asyncio.TimeoutError("Server not responding"))
+                raise ModbusIOException(
+                    f"ERROR: No response received of the last {self.accept_no_response_limit} request, CLOSING CONNECTION."
+                )
+            self.count_no_responses += 1
+            Log.error(f"No response received after {self.retries} retries, continue with next request")
+            return ExceptionResponse(request.function_code)
+
+        self.count_no_responses = 0
+        return resp
+
+    def build_response(self, request: ModbusPDU):
+        """Return a deferred response for the current request.
+
+        :meta private:
+        """
+        my_future: asyncio.Future = asyncio.Future()
+        request.fut = my_future
+        if not self.ctx.transport:
+            if not my_future.done():
+                my_future.set_exception(ConnectionException("Client is not connected"))
+        else:
+            self.ctx.transaction.addTransaction(request)
+        return my_future
+
+    async def __aenter__(self):
+        """Implement the client with enter block.
+
+        :returns: The current instance of the client
+        :raises ConnectionException:
+        """
+        await self.connect()
+        return self
+
+    async def __aexit__(self, klass, value, traceback):
+        """Implement the client with aexit block."""
+        self.close()
+
+    def __str__(self):
+        """Build a string representation of the connection.
+
+        :returns: The string representation
+        """
+        return (
+            f"{self.__class__.__name__} {self.ctx.comm_params.host}:{self.ctx.comm_params.port}"
+        )
+
+
+class ModbusBaseSyncClient(ModbusClientMixin[ModbusPDU]):
+    """**ModbusBaseClient**.
+
+    :mod:`ModbusBaseClient` is normally not referenced outside :mod:`pymodbus`.
+    """
+
+    def __init__(
+        self,
+        framer: FramerType,
+        retries: int,
+        comm_params: CommParams | None = None,
+    ) -> None:
+        """Initialize a client instance.
+
+        :meta private:
+        """
+        ModbusClientMixin.__init__(self)  # type: ignore[arg-type]
+        if comm_params:
+            self.comm_params = comm_params
+        self.retries = retries
+        self.slaves: list[int] = []
+
+        # Common variables.
+        self.framer: FramerBase = (FRAMER_NAME_TO_CLASS[framer])(DecodePDU(False))
+        self.transaction = SyncModbusTransactionManager(
+            self,
+            self.retries,
+        )
+        self.reconnect_delay_current = self.comm_params.reconnect_delay or 0
+        self.use_udp = False
+        self.state = ModbusTransactionState.IDLE
+        self.last_frame_end: float | None = 0
+        self.silent_interval: float = 0
+        self.transport = None
+
+    # ----------------------------------------------------------------------- #
+    # Client external interface
+    # ----------------------------------------------------------------------- #
+    def register(self, custom_response_class: type[ModbusPDU]) -> None:
+        """Register a custom response class with the decoder.
+
+        :param custom_response_class: (optional) Modbus response class.
+        :raises MessageRegisterException: Check exception text.
+
+        Use register() to add non-standard responses (like e.g. a login prompt) and
+        have them interpreted automatically.
+        """
+        self.framer.decoder.register(custom_response_class)
+
+    def idle_time(self) -> float:
+        """Time before initiating next transaction (call **sync**).
+
+        Applications can call message functions without checking idle_time(),
+        this is done automatically.
+        """
+        if self.last_frame_end is None or self.silent_interval is None:
+            return 0
+        return self.last_frame_end + self.silent_interval
+
+    def execute(self, no_response_expected: bool, request: ModbusPDU) -> ModbusPDU:
+        """Execute request and get response (call **sync/async**).
+
+        :param no_response_expected: The client will not expect a response to the request
+        :param request: The request to process
+        :returns: The result of the request execution
+        :raises ConnectionException: Check exception text.
+
+        :meta private:
+        """
+        if not self.connect():
+            raise ConnectionException(f"Failed to connect[{self!s}]")
+        return self.transaction.execute(no_response_expected, request)
+
+    # ----------------------------------------------------------------------- #
+    # Internal methods
+    # ----------------------------------------------------------------------- #
+    def _start_send(self):
+        """Send request.
+
+        :meta private:
+        """
+        if self.state != ModbusTransactionState.RETRYING:
+            Log.debug('New Transaction state "SENDING"')
+            self.state = ModbusTransactionState.SENDING
+
+    @abstractmethod
+    def send(self, request: bytes) -> int:
+        """Send request.
+
+        :meta private:
+        """
+
+    @abstractmethod
+    def recv(self, size: int | None) -> bytes:
+        """Receive data.
+
+        :meta private:
+        """
+
+    @classmethod
+    def get_address_family(cls, address):
+        """Get the correct address family.
+
+        :meta private:
+        """
+        try:
+            _ = socket.inet_pton(socket.AF_INET6, address)
+        except OSError:  # not a valid ipv6 address
+            return socket.AF_INET
+        return socket.AF_INET6
+
+    def connect(self) -> bool:  # type: ignore[empty-body]
+        """Connect to other end, overwritten."""
+
+    def close(self):
+        """Close connection, overwritten."""
+
+    # ----------------------------------------------------------------------- #
+    # The magic methods
+    # ----------------------------------------------------------------------- #
+    def __enter__(self):
+        """Implement the client with enter block.
+
+        :returns: The current instance of the client
+        :raises ConnectionException:
+        """
+        self.connect()
+        return self
+
+    def __exit__(self, klass, value, traceback):
+        """Implement the client with exit block."""
+        self.close()
+
+    def __str__(self):
+        """Build a string representation of the connection.
+
+        :returns: The string representation
+        """
+        return (
+            f"{self.__class__.__name__} {self.comm_params.host}:{self.comm_params.port}"
+        )
diff --git a/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/client/mixin.py b/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/client/mixin.py
new file mode 100644
index 00000000..b31e1ef8
--- /dev/null
+++ b/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/client/mixin.py
@@ -0,0 +1,525 @@
+"""Modbus Client Common."""
+from __future__ import annotations
+
+import struct
+from enum import Enum
+from typing import Generic, TypeVar
+
+import pymodbus.pdu.bit_read_message as pdu_bit_read
+import pymodbus.pdu.bit_write_message as pdu_bit_write
+import pymodbus.pdu.diag_message as pdu_diag
+import pymodbus.pdu.file_message as pdu_file_msg
+import pymodbus.pdu.mei_message as pdu_mei
+import pymodbus.pdu.other_message as pdu_other_msg
+import pymodbus.pdu.register_read_message as pdu_reg_read
+import pymodbus.pdu.register_write_message as pdu_req_write
+from pymodbus.exceptions import ModbusException
+from pymodbus.pdu import ModbusPDU
+
+
+T = TypeVar("T", covariant=False)
+
+
+class ModbusClientMixin(Generic[T]):  # pylint: disable=too-many-public-methods
+    """**ModbusClientMixin**.
+
+    This is an interface class to facilitate the sending requests/receiving responses like read_coils.
+    execute() allows to make a call with non-standard or user defined function codes (remember to add a PDU
+    in the transport class to interpret the request/response).
+
+    Simple modbus message call::
+
+        response = client.read_coils(1, 10)
+        # or
+        response = await client.read_coils(1, 10)
+
+    Advanced modbus message call::
+
+        request = ReadCoilsRequest(1,10)
+        response = client.execute(request)
+        # or
+        request = ReadCoilsRequest(1,10)
+        response = await client.execute(request)
+
+    .. tip::
+        All methods can be used directly (synchronous) or
+        with await <method> (asynchronous) depending on the client used.
+    """
+
+    def __init__(self):
+        """Initialize."""
+
+    def execute(self,  _no_response_expected: bool, _request: ModbusPDU,) -> T:
+        """Execute request (code ???).
+
+        :raises ModbusException:
+
+        Call with custom function codes.
+
+        .. tip::
+            Response is not interpreted.
+        """
+        raise NotImplementedError("execute of ModbusClientMixin needs to be overridden")
+
+    def read_coils(self, address: int, count: int = 1, slave: int = 1, no_response_expected: bool = False) -> T:
+        """Read coils (code 0x01).
+
+        :param address: Start address to read from
+        :param count: (optional) Number of coils to read
+        :param slave: (optional) Modbus slave ID
+        :param no_response_expected: (optional) The client will not expect a response to the request
+        :raises ModbusException:
+        """
+        return self.execute(no_response_expected, pdu_bit_read.ReadCoilsRequest(address=address, count=count, slave=slave))
+
+    def read_discrete_inputs(self,
+                             address: int,
+                             count: int = 1,
+                             slave: int = 1,
+                             no_response_expected: bool = False) -> T:
+        """Read discrete inputs (code 0x02).
+
+        :param address: Start address to read from
+        :param count: (optional) Number of coils to read
+        :param slave: (optional) Modbus slave ID
+        :param no_response_expected: (optional) The client will not expect a response to the request
+        :raises ModbusException:
+        """
+        return self.execute(no_response_expected, pdu_bit_read.ReadDiscreteInputsRequest(address=address, count=count, slave=slave, ))
+
+    def read_holding_registers(self,
+                               address: int,
+                               count: int = 1,
+                               slave: int = 1,
+                               no_response_expected: bool = False) -> T:
+        """Read holding registers (code 0x03).
+
+        :param address: Start address to read from
+        :param count: (optional) Number of coils to read
+        :param slave: (optional) Modbus slave ID
+        :param no_response_expected: (optional) The client will not expect a response to the request
+        :raises ModbusException:
+        """
+        return self.execute(no_response_expected, pdu_reg_read.ReadHoldingRegistersRequest(address=address, count=count, slave=slave))
+
+    def read_input_registers(self,
+                             address: int,
+                             count: int = 1,
+                             slave: int = 1,
+                             no_response_expected: bool = False) -> T:
+        """Read input registers (code 0x04).
+
+        :param address: Start address to read from
+        :param count: (optional) Number of coils to read
+        :param slave: (optional) Modbus slave ID
+        :param no_response_expected: (optional) The client will not expect a response to the request
+        :raises ModbusException:
+        """
+        return self.execute(no_response_expected, pdu_reg_read.ReadInputRegistersRequest(address, count, slave=slave))
+
+    def write_coil(self, address: int, value: bool, slave: int = 1, no_response_expected: bool = False) -> T:
+        """Write single coil (code 0x05).
+
+        :param address: Address to write to
+        :param value: Boolean to write
+        :param slave: (optional) Modbus slave ID
+        :param no_response_expected: (optional) The client will not expect a response to the request
+        :raises ModbusException:
+        """
+        return self.execute(no_response_expected, pdu_bit_write.WriteSingleCoilRequest(address, value, slave=slave))
+
+    def write_register(self, address: int, value: bytes | int, slave: int = 1, no_response_expected: bool = False) -> T:
+        """Write register (code 0x06).
+
+        :param address: Address to write to
+        :param value: Value to write
+        :param slave: (optional) Modbus slave ID
+        :param no_response_expected: (optional) The client will not expect a response to the request
+        :raises ModbusException:
+        """
+        return self.execute(no_response_expected, pdu_req_write.WriteSingleRegisterRequest(address, value, slave=slave))
+
+    def read_exception_status(self, slave: int = 1, no_response_expected: bool = False) -> T:
+        """Read Exception Status (code 0x07).
+
+        :param slave: (optional) Modbus slave ID
+        :param no_response_expected: (optional) The client will not expect a response to the request
+        :raises ModbusException:
+        """
+        return self.execute(no_response_expected, pdu_other_msg.ReadExceptionStatusRequest(slave=slave))
+
+    def diag_query_data(self, msg: bytes, slave: int = 1, no_response_expected: bool = False) -> T:
+        """Diagnose query data (code 0x08 sub 0x00).
+
+        :param msg: Message to be returned
+        :param slave: (optional) Modbus slave ID
+        :param no_response_expected: (optional) The client will not expect a response to the request
+        :raises ModbusException:
+        """
+        return self.execute(no_response_expected, pdu_diag.ReturnQueryDataRequest(msg, slave=slave))
+
+    def diag_restart_communication(self, toggle: bool, slave: int = 1, no_response_expected: bool = False) -> T:
+        """Diagnose restart communication (code 0x08 sub 0x01).
+
+        :param toggle: True if toggled.
+        :param slave: (optional) Modbus slave ID
+        :param no_response_expected: (optional) The client will not expect a response to the request
+        :raises ModbusException:
+        """
+        return self.execute(no_response_expected, pdu_diag.RestartCommunicationsOptionRequest(toggle, slave=slave))
+
+    def diag_read_diagnostic_register(self, slave: int = 1, no_response_expected: bool = False) -> T:
+        """Diagnose read diagnostic register (code 0x08 sub 0x02).
+
+        :param slave: (optional) Modbus slave ID
+        :param no_response_expected: (optional) The client will not expect a response to the request
+        :raises ModbusException:
+        """
+        return self.execute(no_response_expected, pdu_diag.ReturnDiagnosticRegisterRequest(slave=slave))
+
+    def diag_change_ascii_input_delimeter(self, slave: int = 1, no_response_expected: bool = False) -> T:
+        """Diagnose change ASCII input delimiter (code 0x08 sub 0x03).
+
+        :param slave: (optional) Modbus slave ID
+        :param no_response_expected: (optional) The client will not expect a response to the request
+        :raises ModbusException:
+        """
+        return self.execute(no_response_expected, pdu_diag.ChangeAsciiInputDelimiterRequest(slave=slave))
+
+    def diag_force_listen_only(self, slave: int = 1, no_response_expected: bool = False) -> T:
+        """Diagnose force listen only (code 0x08 sub 0x04).
+
+        :param slave: (optional) Modbus slave ID
+        :param no_response_expected: (optional) The client will not expect a response to the request
+        :raises ModbusException:
+        """
+        return self.execute(no_response_expected, pdu_diag.ForceListenOnlyModeRequest(slave=slave))
+
+    def diag_clear_counters(self, slave: int = 1, no_response_expected: bool = False) -> T:
+        """Diagnose clear counters (code 0x08 sub 0x0A).
+
+        :param slave: (optional) Modbus slave ID
+        :param no_response_expected: (optional) The client will not expect a response to the request
+        :raises ModbusException:
+        """
+        return self.execute(no_response_expected, pdu_diag.ClearCountersRequest(slave=slave))
+
+    def diag_read_bus_message_count(self, slave: int = 1, no_response_expected: bool = False) -> T:
+        """Diagnose read bus message count (code 0x08 sub 0x0B).
+
+        :param slave: (optional) Modbus slave ID
+        :param no_response_expected: (optional) The client will not expect a response to the request
+        :raises ModbusException:
+        """
+        return self.execute(no_response_expected, pdu_diag.ReturnBusMessageCountRequest(slave=slave))
+
+    def diag_read_bus_comm_error_count(self, slave: int = 1, no_response_expected: bool = False) -> T:
+        """Diagnose read Bus Communication Error Count (code 0x08 sub 0x0C).
+
+        :param slave: (optional) Modbus slave ID
+        :param no_response_expected: (optional) The client will not expect a response to the request
+        :raises ModbusException:
+        """
+        return self.execute(no_response_expected, pdu_diag.ReturnBusCommunicationErrorCountRequest(slave=slave))
+
+    def diag_read_bus_exception_error_count(self, slave: int = 1, no_response_expected: bool = False) -> T:
+        """Diagnose read Bus Exception Error Count (code 0x08 sub 0x0D).
+
+        :param slave: (optional) Modbus slave ID
+        :param no_response_expected: (optional) The client will not expect a response to the request
+        :raises ModbusException:
+        """
+        return self.execute(no_response_expected, pdu_diag.ReturnBusExceptionErrorCountRequest(slave=slave))
+
+    def diag_read_slave_message_count(self, slave: int = 1, no_response_expected: bool = False) -> T:
+        """Diagnose read Slave Message Count (code 0x08 sub 0x0E).
+
+        :param slave: (optional) Modbus slave ID
+        :param no_response_expected: (optional) The client will not expect a response to the request
+        :raises ModbusException:
+        """
+        return self.execute(no_response_expected, pdu_diag.ReturnSlaveMessageCountRequest(slave=slave))
+
+    def diag_read_slave_no_response_count(self, slave: int = 1, no_response_expected: bool = False) -> T:
+        """Diagnose read Slave No Response Count (code 0x08 sub 0x0F).
+
+        :param slave: (optional) Modbus slave ID
+        :param no_response_expected: (optional) The client will not expect a response to the request
+        :raises ModbusException:
+        """
+        return self.execute(no_response_expected, pdu_diag.ReturnSlaveNoResponseCountRequest(slave=slave))
+
+    def diag_read_slave_nak_count(self, slave: int = 1, no_response_expected: bool = False) -> T:
+        """Diagnose read Slave NAK Count (code 0x08 sub 0x10).
+
+        :param slave: (optional) Modbus slave ID
+        :param no_response_expected: (optional) The client will not expect a response to the request
+        :raises ModbusException:
+        """
+        return self.execute(no_response_expected, pdu_diag.ReturnSlaveNAKCountRequest(slave=slave))
+
+    def diag_read_slave_busy_count(self, slave: int = 1, no_response_expected: bool = False) -> T:
+        """Diagnose read Slave Busy Count (code 0x08 sub 0x11).
+
+        :param slave: (optional) Modbus slave ID
+        :param no_response_expected: (optional) The client will not expect a response to the request
+        :raises ModbusException:
+        """
+        return self.execute(no_response_expected, pdu_diag.ReturnSlaveBusyCountRequest(slave=slave))
+
+    def diag_read_bus_char_overrun_count(self, slave: int = 1, no_response_expected: bool = False) -> T:
+        """Diagnose read Bus Character Overrun Count (code 0x08 sub 0x12).
+
+        :param slave: (optional) Modbus slave ID
+        :param no_response_expected: (optional) The client will not expect a response to the request
+        :raises ModbusException:
+        """
+        return self.execute(no_response_expected, pdu_diag.ReturnSlaveBusCharacterOverrunCountRequest(slave=slave))
+
+    def diag_read_iop_overrun_count(self, slave: int = 1, no_response_expected: bool = False) -> T:
+        """Diagnose read Iop overrun count (code 0x08 sub 0x13).
+
+        :param slave: (optional) Modbus slave ID
+        :param no_response_expected: (optional) The client will not expect a response to the request
+        :raises ModbusException:
+        """
+        return self.execute(no_response_expected, pdu_diag.ReturnIopOverrunCountRequest(slave=slave))
+
+    def diag_clear_overrun_counter(self, slave: int = 1, no_response_expected: bool = False) -> T:
+        """Diagnose Clear Overrun Counter and Flag (code 0x08 sub 0x14).
+
+        :param slave: (optional) Modbus slave ID
+        :param no_response_expected: (optional) The client will not expect a response to the request
+        :raises ModbusException:
+        """
+        return self.execute(no_response_expected, pdu_diag.ClearOverrunCountRequest(slave=slave))
+
+    def diag_getclear_modbus_response(self, slave: int = 1, no_response_expected: bool = False) -> T:
+        """Diagnose Get/Clear modbus plus (code 0x08 sub 0x15).
+
+        :param slave: (optional) Modbus slave ID
+        :param no_response_expected: (optional) The client will not expect a response to the request
+        :raises ModbusException:
+        """
+        return self.execute(no_response_expected, pdu_diag.GetClearModbusPlusRequest(slave=slave))
+
+    def diag_get_comm_event_counter(self, slave: int = 1, no_response_expected: bool = False) -> T:
+        """Diagnose get event counter (code 0x0B).
+
+        :param slave: (optional) Modbus slave ID
+        :param no_response_expected: (optional) The client will not expect a response to the request
+        :raises ModbusException:
+        """
+        return self.execute(no_response_expected, pdu_other_msg.GetCommEventCounterRequest(slave=slave))
+
+    def diag_get_comm_event_log(self, slave: int = 1, no_response_expected: bool = False) -> T:
+        """Diagnose get event counter (code 0x0C).
+
+        :param slave: (optional) Modbus slave ID
+        :param no_response_expected: (optional) The client will not expect a response to the request
+        :raises ModbusException:
+        """
+        return self.execute(no_response_expected, pdu_other_msg.GetCommEventLogRequest(slave=slave))
+
+    def write_coils(
+        self,
+        address: int,
+        values: list[bool] | bool,
+        slave: int = 1,
+        no_response_expected: bool = False
+    ) -> T:
+        """Write coils (code 0x0F).
+
+        :param address: Start address to write to
+        :param values: List of booleans to write, or a single boolean to write
+        :param slave: (optional) Modbus slave ID
+        :param no_response_expected: (optional) The client will not expect a response to the request
+        :raises ModbusException:
+        """
+        return self.execute(no_response_expected, pdu_bit_write.WriteMultipleCoilsRequest(address, values=values, slave=slave))
+
+    def write_registers(
+        self,
+        address: int,
+        values: list[bytes | int],
+        slave: int = 1,
+        skip_encode: bool = False,
+        no_response_expected: bool = False
+    ) -> T:
+        """Write registers (code 0x10).
+
+        :param address: Start address to write to
+        :param values: List of values to write
+        :param slave: (optional) Modbus slave ID
+        :param skip_encode: (optional) do not encode values
+        :param no_response_expected: (optional) The client will not expect a response to the request
+        :raises ModbusException:
+        """
+        return self.execute(no_response_expected, pdu_req_write.WriteMultipleRegistersRequest(address, values,slave=slave,skip_encode=skip_encode))
+
+    def report_slave_id(self, slave: int = 1, no_response_expected: bool = False) -> T:
+        """Report slave ID (code 0x11).
+
+        :param slave: (optional) Modbus slave ID
+        :param no_response_expected: (optional) The client will not expect a response to the request
+        :raises ModbusException:
+        """
+        return self.execute(no_response_expected, pdu_other_msg.ReportSlaveIdRequest(slave=slave))
+
+    def read_file_record(self, records: list[tuple], slave: int = 1, no_response_expected: bool = False) -> T:
+        """Read file record (code 0x14).
+
+        :param records: List of (Reference type, File number, Record Number, Record Length)
+        :param slave: device id
+        :param no_response_expected: (optional) The client will not expect a response to the request
+        :raises ModbusException:
+        """
+        return self.execute(no_response_expected, pdu_file_msg.ReadFileRecordRequest(records, slave=slave))
+
+    def write_file_record(self, records: list[tuple], slave: int = 1, no_response_expected: bool = False) -> T:
+        """Write file record (code 0x15).
+
+        :param records: List of (Reference type, File number, Record Number, Record Length)
+        :param slave: (optional) Device id
+        :param no_response_expected: (optional) The client will not expect a response to the request
+        :param no_response_expected: (optional) The client will not expect a response to the request
+        :raises ModbusException:
+        """
+        return self.execute(no_response_expected, pdu_file_msg.WriteFileRecordRequest(records,slave=slave))
+
+    def mask_write_register(
+        self,
+        address: int = 0x0000,
+        and_mask: int = 0xFFFF,
+        or_mask: int = 0x0000,
+        slave: int = 1,
+        no_response_expected: bool = False
+    ) -> T:
+        """Mask write register (code 0x16).
+
+        :param address: The mask pointer address (0x0000 to 0xffff)
+        :param and_mask: The and bitmask to apply to the register address
+        :param or_mask: The or bitmask to apply to the register address
+        :param slave: (optional) device id
+        :param no_response_expected: (optional) The client will not expect a response to the request
+        :raises ModbusException:
+        """
+        return self.execute(no_response_expected, pdu_req_write.MaskWriteRegisterRequest(address, and_mask, or_mask, slave=slave))
+
+    def readwrite_registers(
+        self,
+        read_address: int = 0,
+        read_count: int = 0,
+        write_address: int = 0,
+        address: int | None = None,
+        values: list[int] | int = 0,
+        slave: int = 1,
+        no_response_expected: bool = False
+    ) -> T:
+        """Read/Write registers (code 0x17).
+
+        :param read_address: The address to start reading from
+        :param read_count: The number of registers to read from address
+        :param write_address: The address to start writing to
+        :param address: (optional) use as read/write address
+        :param values: List of values to write, or a single value to write
+        :param slave: (optional) Modbus slave ID
+        :param no_response_expected: (optional) The client will not expect a response to the request
+        :raises ModbusException:
+        """
+        if address:
+            read_address = address
+            write_address = address
+        return self.execute(no_response_expected, pdu_reg_read.ReadWriteMultipleRegistersRequest( read_address=read_address, read_count=read_count, write_address=write_address, write_registers=values,slave=slave))
+
+    def read_fifo_queue(self, address: int = 0x0000, slave: int = 1, no_response_expected: bool = False) -> T:
+        """Read FIFO queue (code 0x18).
+
+        :param address: The address to start reading from
+        :param slave: (optional) device id
+        :param no_response_expected: (optional) The client will not expect a response to the request
+        :raises ModbusException:
+        """
+        return self.execute(no_response_expected, pdu_file_msg.ReadFifoQueueRequest(address, slave=slave))
+
+    # code 0x2B sub 0x0D: CANopen General Reference Request and Response, NOT IMPLEMENTED
+
+    def read_device_information(self, read_code: int | None = None,
+                                object_id: int = 0x00,
+                                slave: int = 1,
+                                no_response_expected: bool = False) -> T:
+        """Read FIFO queue (code 0x2B sub 0x0E).
+
+        :param read_code: The device information read code
+        :param object_id: The object to read from
+        :param slave: (optional) Device id
+        :param no_response_expected: (optional) The client will not expect a response to the request
+        :raises ModbusException:
+        """
+        return self.execute(no_response_expected, pdu_mei.ReadDeviceInformationRequest(read_code, object_id, slave=slave))
+
+    # ------------------
+    # Converter methods
+    # ------------------
+
+    class DATATYPE(Enum):
+        """Datatype enum (name and number of bytes), used for convert_* calls."""
+
+        INT16 = ("h", 1)
+        UINT16 = ("H", 1)
+        INT32 = ("i", 2)
+        UINT32 = ("I", 2)
+        INT64 = ("q", 4)
+        UINT64 = ("Q", 4)
+        FLOAT32 = ("f", 2)
+        FLOAT64 = ("d", 4)
+        STRING = ("s", 0)
+
+    @classmethod
+    def convert_from_registers(
+        cls, registers: list[int], data_type: DATATYPE
+    ) -> int | float | str:
+        """Convert registers to int/float/str.
+
+        :param registers: list of registers received from e.g. read_holding_registers()
+        :param data_type: data type to convert to
+        :returns: int, float or str depending on "data_type"
+        :raises ModbusException: when size of registers is not 1, 2 or 4
+        """
+        byte_list = bytearray()
+        for x in registers:
+            byte_list.extend(int.to_bytes(x, 2, "big"))
+        if data_type == cls.DATATYPE.STRING:
+            if byte_list[-1:] == b"\00":
+                byte_list = byte_list[:-1]
+            return byte_list.decode("utf-8")
+        if len(registers) != data_type.value[1]:
+            raise ModbusException(
+                f"Illegal size ({len(registers)}) of register array, cannot convert!"
+            )
+        return struct.unpack(f">{data_type.value[0]}", byte_list)[0]
+
+    @classmethod
+    def convert_to_registers(
+        cls, value: int | float | str, data_type: DATATYPE
+    ) -> list[int]:
+        """Convert int/float/str to registers (16/32/64 bit).
+
+        :param value: value to be converted
+        :param data_type: data type to be encoded as registers
+        :returns: List of registers, can be used directly in e.g. write_registers()
+        :raises TypeError: when there is a mismatch between data_type and value
+        """
+        if data_type == cls.DATATYPE.STRING:
+            if not isinstance(value, str):
+                raise TypeError(f"Value should be string but is {type(value)}.")
+            byte_list = value.encode()
+            if len(byte_list) % 2:
+                byte_list += b"\x00"
+        else:
+            byte_list = struct.pack(f">{data_type.value[0]}", value)
+        regs = [
+            int.from_bytes(byte_list[x : x + 2], "big")
+            for x in range(0, len(byte_list), 2)
+        ]
+        return regs
diff --git a/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/client/modbusclientprotocol.py b/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/client/modbusclientprotocol.py
new file mode 100644
index 00000000..49c788b3
--- /dev/null
+++ b/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/client/modbusclientprotocol.py
@@ -0,0 +1,83 @@
+"""ModbusProtocol implementation for all clients."""
+from __future__ import annotations
+
+from collections.abc import Callable
+
+from pymodbus.framer import (
+    FRAMER_NAME_TO_CLASS,
+    FramerBase,
+    FramerType,
+)
+from pymodbus.logging import Log
+from pymodbus.pdu import DecodePDU
+from pymodbus.transaction import ModbusTransactionManager
+from pymodbus.transport import CommParams, ModbusProtocol
+
+
+class ModbusClientProtocol(ModbusProtocol):
+    """**ModbusClientProtocol**.
+
+    :mod:`ModbusClientProtocol` is normally not referenced outside :mod:`pymodbus`.
+    """
+
+    def __init__(
+        self,
+        framer: FramerType,
+        params: CommParams,
+        on_connect_callback: Callable[[bool], None] | None = None,
+    ) -> None:
+        """Initialize a client instance."""
+        ModbusProtocol.__init__(
+            self,
+            params,
+            False,
+        )
+        self.on_connect_callback = on_connect_callback
+
+        # Common variables.
+        self.framer: FramerBase = (FRAMER_NAME_TO_CLASS[framer])(DecodePDU(False))
+        self.transaction = ModbusTransactionManager()
+
+    def _handle_response(self, reply):
+        """Handle the processed response and link to correct deferred."""
+        if reply is not None:
+            tid = reply.transaction_id
+            if handler := self.transaction.getTransaction(tid):
+                reply.request = handler
+                if not handler.fut.done():
+                    handler.fut.set_result(reply)
+            else:
+                Log.debug("Unrequested message: {}", reply, ":str")
+
+    def callback_new_connection(self):
+        """Call when listener receive new connection request."""
+
+    def callback_connected(self) -> None:
+        """Call when connection is succcesfull."""
+        if self.on_connect_callback:
+            self.loop.call_soon(self.on_connect_callback, True)
+
+    def callback_disconnected(self, exc: Exception | None) -> None:
+        """Call when connection is lost."""
+        Log.debug("callback_disconnected called: {}", exc)
+        if self.on_connect_callback:
+            self.loop.call_soon(self.on_connect_callback, False)
+
+    def callback_data(self, data: bytes, addr: tuple | None = None) -> int:
+        """Handle received data.
+
+        returns number of bytes consumed
+        """
+        used_len, pdu = self.framer.processIncomingFrame(data)
+        if pdu:
+            self._handle_response(pdu)
+        return used_len
+
+    def __str__(self):
+        """Build a string representation of the connection.
+
+        :returns: The string representation
+        """
+        return (
+            f"{self.__class__.__name__} {self.comm_params.host}:{self.comm_params.port}"
+        )
diff --git a/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/client/serial.py b/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/client/serial.py
new file mode 100644
index 00000000..a9450262
--- /dev/null
+++ b/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/client/serial.py
@@ -0,0 +1,348 @@
+"""Modbus client async serial communication."""
+from __future__ import annotations
+
+import contextlib
+import sys
+import time
+from collections.abc import Callable
+from functools import partial
+
+from pymodbus.client.base import ModbusBaseClient, ModbusBaseSyncClient
+from pymodbus.exceptions import ConnectionException
+from pymodbus.framer import FramerType
+from pymodbus.logging import Log
+from pymodbus.transport import CommParams, CommType
+from pymodbus.utilities import ModbusTransactionState
+
+
+with contextlib.suppress(ImportError):
+    import serial
+
+
+class AsyncModbusSerialClient(ModbusBaseClient):
+    """**AsyncModbusSerialClient**.
+
+    Fixed parameters:
+
+    :param port: Serial port used for communication.
+
+    Optional parameters:
+
+    :param framer: Framer name, default FramerType.RTU
+    :param baudrate: Bits per second.
+    :param bytesize: Number of bits per byte 7-8.
+    :param parity: 'E'ven, 'O'dd or 'N'one
+    :param stopbits: Number of stop bits 1, 1.5, 2.
+    :param handle_local_echo: Discard local echo from dongle.
+    :param name: Set communication name, used in logging
+    :param reconnect_delay: Minimum delay in seconds.milliseconds before reconnecting.
+    :param reconnect_delay_max: Maximum delay in seconds.milliseconds before reconnecting.
+    :param timeout: Timeout for connecting and receiving data, in seconds.
+    :param retries: Max number of retries per request.
+    :param on_connect_callback: Function that will be called just before a connection attempt.
+
+    .. tip::
+        **reconnect_delay** doubles automatically with each unsuccessful connect, from
+        **reconnect_delay** to **reconnect_delay_max**.
+        Set `reconnect_delay=0` to avoid automatic reconnection.
+
+    Example::
+
+        from pymodbus.client import AsyncModbusSerialClient
+
+        async def run():
+            client = AsyncModbusSerialClient("dev/serial0")
+
+            await client.connect()
+            ...
+            client.close()
+
+    Please refer to :ref:`Pymodbus internals` for advanced usage.
+    """
+
+    def __init__(  # pylint: disable=too-many-arguments
+        self,
+        port: str,
+        framer: FramerType = FramerType.RTU,
+        baudrate: int = 19200,
+        bytesize: int = 8,
+        parity: str = "N",
+        stopbits: int = 1,
+        handle_local_echo: bool = False,
+        name: str = "comm",
+        reconnect_delay: float = 0.1,
+        reconnect_delay_max: float = 300,
+        timeout: float = 3,
+        retries: int = 3,
+        on_connect_callback: Callable[[bool], None] | None = None,
+    ) -> None:
+        """Initialize Asyncio Modbus Serial Client."""
+        if "serial" not in sys.modules:
+            raise RuntimeError(
+                "Serial client requires pyserial "
+                'Please install with "pip install pyserial" and try again.'
+            )
+        self.comm_params = CommParams(
+            comm_type=CommType.SERIAL,
+            host=port,
+            baudrate=baudrate,
+            bytesize=bytesize,
+            parity=parity,
+            stopbits=stopbits,
+            handle_local_echo=handle_local_echo,
+            comm_name=name,
+            reconnect_delay=reconnect_delay,
+            reconnect_delay_max=reconnect_delay_max,
+            timeout_connect=timeout,
+        )
+        ModbusBaseClient.__init__(
+            self,
+            framer,
+            retries,
+            on_connect_callback,
+        )
+
+
+class ModbusSerialClient(ModbusBaseSyncClient):
+    """**ModbusSerialClient**.
+
+    Fixed parameters:
+
+    :param port: Serial port used for communication.
+
+    Optional parameters:
+
+    :param framer: Framer name, default FramerType.RTU
+    :param baudrate: Bits per second.
+    :param bytesize: Number of bits per byte 7-8.
+    :param parity: 'E'ven, 'O'dd or 'N'one
+    :param stopbits: Number of stop bits 0-2.
+    :param handle_local_echo: Discard local echo from dongle.
+    :param name: Set communication name, used in logging
+    :param reconnect_delay: Not used in the sync client
+    :param reconnect_delay_max: Not used in the sync client
+    :param timeout: Timeout for connecting and receiving data, in seconds.
+    :param retries: Max number of retries per request.
+
+    Note that unlike the async client, the sync client does not perform
+    retries. If the connection has closed, the client will attempt to reconnect
+    once before executing each read/write request, and will raise a
+    ConnectionException if this fails.
+
+    Example::
+
+        from pymodbus.client import ModbusSerialClient
+
+        def run():
+            client = ModbusSerialClient("dev/serial0")
+
+            client.connect()
+            ...
+            client.close()
+
+    Please refer to :ref:`Pymodbus internals` for advanced usage.
+    """
+
+    state = ModbusTransactionState.IDLE
+    inter_byte_timeout: float = 0
+    silent_interval: float = 0
+
+    def __init__(  # pylint: disable=too-many-arguments
+        self,
+        port: str,
+        framer: FramerType = FramerType.RTU,
+        baudrate: int = 19200,
+        bytesize: int = 8,
+        parity: str = "N",
+        stopbits: int = 1,
+        handle_local_echo: bool = False,
+        name: str = "comm",
+        reconnect_delay: float = 0.1,
+        reconnect_delay_max: float = 300,
+        timeout: float = 3,
+        retries: int = 3,
+    ) -> None:
+        """Initialize Modbus Serial Client."""
+        self.comm_params = CommParams(
+            comm_type=CommType.SERIAL,
+            host=port,
+            baudrate=baudrate,
+            bytesize=bytesize,
+            parity=parity,
+            stopbits=stopbits,
+            handle_local_echo=handle_local_echo,
+            comm_name=name,
+            reconnect_delay=reconnect_delay,
+            reconnect_delay_max=reconnect_delay_max,
+            timeout_connect=timeout,
+        )
+        super().__init__(
+            framer,
+            retries,
+        )
+        if "serial" not in sys.modules:
+            raise RuntimeError(
+                "Serial client requires pyserial "
+                'Please install with "pip install pyserial" and try again.'
+            )
+        self.socket: serial.Serial | None = None
+        self.last_frame_end = None
+        self._t0 = float(1 + bytesize + stopbits) / baudrate
+
+        # Check every 4 bytes / 2 registers if the reading is ready
+        self._recv_interval = self._t0 * 4
+        # Set a minimum of 1ms for high baudrates
+        self._recv_interval = max(self._recv_interval, 0.001)
+
+        if baudrate > 19200:
+            self.silent_interval = 1.75 / 1000  # ms
+        else:
+            self.inter_byte_timeout = 1.5 * self._t0
+            self.silent_interval = 3.5 * self._t0
+        self.silent_interval = round(self.silent_interval, 6)
+
+    @property
+    def connected(self) -> bool:
+        """Check if socket exists."""
+        return self.socket is not None
+
+    def connect(self) -> bool:
+        """Connect to the modbus serial server."""
+        if self.socket:
+            return True
+        try:
+            self.socket = serial.serial_for_url(
+                self.comm_params.host,
+                timeout=self.comm_params.timeout_connect,
+                bytesize=self.comm_params.bytesize,
+                stopbits=self.comm_params.stopbits,
+                baudrate=self.comm_params.baudrate,
+                parity=self.comm_params.parity,
+                exclusive=True,
+            )
+            self.socket.inter_byte_timeout = self.inter_byte_timeout
+            self.last_frame_end = None
+        # except serial.SerialException as msg:
+        # pyserial raises undocumented exceptions like termios
+        except Exception as msg:  # pylint: disable=broad-exception-caught
+            Log.error("{}", msg)
+            self.close()
+        return self.socket is not None
+
+    def close(self):
+        """Close the underlying socket connection."""
+        if self.socket:
+            self.socket.close()
+        self.socket = None
+
+    def _in_waiting(self):
+        """Return waiting bytes."""
+        return getattr(self.socket, "in_waiting") if hasattr(self.socket, "in_waiting") else getattr(self.socket, "inWaiting")()
+
+    def _send(self, request: bytes) -> int:
+        """Send data on the underlying socket.
+
+        If receive buffer still holds some data then flush it.
+
+        Sleep if last send finished less than 3.5 character times ago.
+        """
+        super()._start_send()
+        if not self.socket:
+            raise ConnectionException(str(self))
+        if request:
+            if waitingbytes := self._in_waiting():
+                result = self.socket.read(waitingbytes)
+                Log.warning("Cleanup recv buffer before send: {}", result, ":hex")
+            if (size := self.socket.write(request)) is None:
+                size = 0
+            return size
+        return 0
+
+    def send(self, request: bytes) -> int:
+        """Send data on the underlying socket."""
+        start = time.time()
+        if hasattr(self,"ctx"):
+          timeout = start + self.ctx.comm_params.timeout_connect
+        else:
+            timeout = start + self.comm_params.timeout_connect
+        while self.state != ModbusTransactionState.IDLE:
+            if self.state == ModbusTransactionState.TRANSACTION_COMPLETE:
+                timestamp = round(time.time(), 6)
+                Log.debug(
+                    "Changing state to IDLE - Last Frame End - {} Current Time stamp - {}",
+                    self.last_frame_end,
+                    timestamp,
+                )
+                if self.last_frame_end:
+                    idle_time = self.idle_time()
+                    if round(timestamp - idle_time, 6) <= self.silent_interval:
+                        Log.debug(
+                            "Waiting for 3.5 char before next send - {} ms",
+                            self.silent_interval * 1000,
+                        )
+                        time.sleep(self.silent_interval)
+                else:
+                    # Recovering from last error ??
+                    time.sleep(self.silent_interval)
+                self.state = ModbusTransactionState.IDLE
+            elif self.state == ModbusTransactionState.RETRYING:
+                # Simple lets settle down!!!
+                # To check for higher baudrates
+                time.sleep(self.comm_params.timeout_connect)
+                break
+            elif time.time() > timeout:
+                Log.debug(
+                    "Spent more time than the read time out, "
+                    "resetting the transaction to IDLE"
+                )
+                self.state = ModbusTransactionState.IDLE
+            else:
+                Log.debug("Sleeping")
+                time.sleep(self.silent_interval)
+        size = self._send(request)
+        self.last_frame_end = round(time.time(), 6)
+        return size
+
+    def _wait_for_data(self) -> int:
+        """Wait for data."""
+        size = 0
+        more_data = False
+        condition = partial(
+            lambda start, timeout: (time.time() - start) <= timeout,
+            timeout=self.comm_params.timeout_connect,
+        )
+        start = time.time()
+        while condition(start):
+            available = self._in_waiting()
+            if (more_data and not available) or (more_data and available == size):
+                break
+            if available and available != size:
+                more_data = True
+                size = available
+            time.sleep(self._recv_interval)
+        return size
+
+    def recv(self, size: int | None) -> bytes:
+        """Read data from the underlying descriptor."""
+        if not self.socket:
+            raise ConnectionException(str(self))
+        if size is None:
+            size = self._wait_for_data()
+        if size > self._in_waiting():
+            self._wait_for_data()
+        result = self.socket.read(size)
+        self.last_frame_end = round(time.time(), 6)
+        return result
+
+    def is_socket_open(self) -> bool:
+        """Check if socket is open."""
+        if self.socket:
+            return self.socket.is_open
+        return False
+
+    def __repr__(self):
+        """Return string representation."""
+        return (
+            f"<{self.__class__.__name__} at {hex(id(self))} socket={self.socket}, "
+            f"framer={self.framer}, timeout={self.comm_params.timeout_connect}>"
+        )
diff --git a/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/client/tcp.py b/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/client/tcp.py
new file mode 100644
index 00000000..0983f2c1
--- /dev/null
+++ b/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/client/tcp.py
@@ -0,0 +1,290 @@
+"""Modbus client async TCP communication."""
+from __future__ import annotations
+
+import select
+import socket
+import time
+from collections.abc import Callable
+
+from pymodbus.client.base import ModbusBaseClient, ModbusBaseSyncClient
+from pymodbus.exceptions import ConnectionException
+from pymodbus.framer import FramerType
+from pymodbus.logging import Log
+from pymodbus.transport import CommParams, CommType
+
+
+class AsyncModbusTcpClient(ModbusBaseClient):
+    """**AsyncModbusTcpClient**.
+
+    Fixed parameters:
+
+    :param host: Host IP address or host name
+
+    Optional parameters:
+
+    :param framer: Framer name, default FramerType.SOCKET
+    :param port: Port used for communication
+    :param name: Set communication name, used in logging
+    :param source_address: source address of client
+    :param reconnect_delay: Minimum delay in seconds.milliseconds before reconnecting.
+    :param reconnect_delay_max: Maximum delay in seconds.milliseconds before reconnecting.
+    :param timeout: Timeout for connecting and receiving data, in seconds.
+    :param retries: Max number of retries per request.
+    :param on_connect_callback: Function that will be called just before a connection attempt.
+
+    .. tip::
+        **reconnect_delay** doubles automatically with each unsuccessful connect, from
+        **reconnect_delay** to **reconnect_delay_max**.
+        Set `reconnect_delay=0` to avoid automatic reconnection.
+
+    Example::
+
+        from pymodbus.client import AsyncModbusTcpClient
+
+        async def run():
+            client = AsyncModbusTcpClient("localhost")
+
+            await client.connect()
+            ...
+            client.close()
+
+    Please refer to :ref:`Pymodbus internals` for advanced usage.
+    """
+
+    def __init__(  # pylint: disable=too-many-arguments
+        self,
+        host: str,
+        framer: FramerType = FramerType.SOCKET,
+        port: int = 502,
+        name: str = "comm",
+        source_address: tuple[str, int] | None = None,
+        reconnect_delay: float = 0.1,
+        reconnect_delay_max: float = 300,
+        timeout: float = 3,
+        retries: int = 3,
+        on_connect_callback: Callable[[bool], None] | None = None,
+    ) -> None:
+        """Initialize Asyncio Modbus TCP Client."""
+        if not hasattr(self,"comm_params"):
+            self.comm_params = CommParams(
+                comm_type=CommType.TCP,
+                host=host,
+                port=port,
+                comm_name=name,
+                source_address=source_address,
+                reconnect_delay=reconnect_delay,
+                reconnect_delay_max=reconnect_delay_max,
+                timeout_connect=timeout,
+            )
+        ModbusBaseClient.__init__(
+            self,
+            framer,
+            retries,
+            on_connect_callback,
+        )
+
+
+class ModbusTcpClient(ModbusBaseSyncClient):
+    """**ModbusTcpClient**.
+
+    Fixed parameters:
+
+    :param host: Host IP address or host name
+
+    Optional parameters:
+
+    :param framer: Framer name, default FramerType.SOCKET
+    :param port: Port used for communication
+    :param name: Set communication name, used in logging
+    :param source_address: source address of client
+    :param reconnect_delay: Not used in the sync client
+    :param reconnect_delay_max: Not used in the sync client
+    :param timeout: Timeout for connecting and receiving data, in seconds.
+    :param retries: Max number of retries per request.
+
+    .. tip::
+        Unlike the async client, the sync client does not perform
+        retries. If the connection has closed, the client will attempt to reconnect
+        once before executing each read/write request, and will raise a
+        ConnectionException if this fails.
+
+    Example::
+
+        from pymodbus.client import ModbusTcpClient
+
+        async def run():
+            client = ModbusTcpClient("localhost")
+
+            client.connect()
+            ...
+            client.close()
+
+    Please refer to :ref:`Pymodbus internals` for advanced usage.
+    """
+
+    socket: socket.socket | None
+
+    def __init__(
+        self,
+        host: str,
+        framer: FramerType = FramerType.SOCKET,
+        port: int = 502,
+        name: str = "comm",
+        source_address: tuple[str, int] | None = None,
+        reconnect_delay: float = 0.1,
+        reconnect_delay_max: float = 300,
+        timeout: float = 3,
+        retries: int = 3,
+    ) -> None:
+        """Initialize Modbus TCP Client."""
+        if not hasattr(self,"comm_params"):
+            self.comm_params = CommParams(
+                comm_type=CommType.TCP,
+                host=host,
+                port=port,
+                comm_name=name,
+                source_address=source_address,
+                reconnect_delay=reconnect_delay,
+                reconnect_delay_max=reconnect_delay_max,
+                timeout_connect=timeout,
+            )
+        super().__init__(framer, retries)
+        self.socket = None
+
+    @property
+    def connected(self) -> bool:
+        """Check if socket exists."""
+        return self.socket is not None
+
+    def connect(self):
+        """Connect to the modbus tcp server."""
+        if self.socket:
+            return True
+        try:
+            self.socket = socket.create_connection(
+                (self.comm_params.host, self.comm_params.port),
+                timeout=self.comm_params.timeout_connect,
+                source_address=self.comm_params.source_address,
+            )
+            Log.debug(
+                "Connection to Modbus server established. Socket {}",
+                self.socket.getsockname(),
+            )
+        except OSError as msg:
+            Log.error(
+                "Connection to ({}, {}) failed: {}",
+                self.comm_params.host,
+                self.comm_params.port,
+                msg,
+            )
+            self.close()
+        return self.socket is not None
+
+    def close(self):
+        """Close the underlying socket connection."""
+        if self.socket:
+            self.socket.close()
+        self.socket = None
+
+    def send(self, request):
+        """Send data on the underlying socket."""
+        super()._start_send()
+        if not self.socket:
+            raise ConnectionException(str(self))
+        if request:
+            return self.socket.send(request)
+        return 0
+
+    def recv(self, size: int | None) -> bytes:
+        """Read data from the underlying descriptor."""
+        if not self.socket:
+            raise ConnectionException(str(self))
+
+        # socket.recv(size) waits until it gets some data from the host but
+        # not necessarily the entire response that can be fragmented in
+        # many packets.
+        # To avoid split responses to be recognized as invalid
+        # messages and to be discarded, loops socket.recv until full data
+        # is received or timeout is expired.
+        # If timeout expires returns the read data, also if its length is
+        # less than the expected size.
+        self.socket.setblocking(False)
+
+        timeout = self.comm_params.timeout_connect or 0
+
+        # If size isn't specified read up to 4096 bytes at a time.
+        if size is None:
+            recv_size = 4096
+        else:
+            recv_size = size
+
+        data: list[bytes] = []
+        data_length = 0
+        time_ = time.time()
+        end = time_ + timeout
+        while recv_size > 0:
+            try:
+                ready = select.select([self.socket], [], [], end - time_)
+            except ValueError:
+                return self._handle_abrupt_socket_close(size, data, time.time() - time_)
+            if ready[0]:
+                if (recv_data := self.socket.recv(recv_size)) == b"":
+                    return self._handle_abrupt_socket_close(
+                        size, data, time.time() - time_
+                    )
+                data.append(recv_data)
+                data_length += len(recv_data)
+            time_ = time.time()
+
+            # If size isn't specified continue to read until timeout expires.
+            if size:
+                recv_size = size - data_length
+
+            # Timeout is reduced also if some data has been received in order
+            # to avoid infinite loops when there isn't an expected response
+            # size and the slave sends noisy data continuously.
+            if time_ > end:
+                break
+        self.last_frame_end = round(time.time(), 6)
+        return b"".join(data)
+
+    def _handle_abrupt_socket_close(self, size: int | None, data: list[bytes], duration: float) -> bytes:
+        """Handle unexpected socket close by remote end.
+
+        Intended to be invoked after determining that the remote end
+        has unexpectedly closed the connection, to clean up and handle
+        the situation appropriately.
+
+        :param size: The number of bytes that was attempted to read
+        :param data: The actual data returned
+        :param duration: Duration from the read was first attempted
+               until it was determined that the remote closed the
+               socket
+        :return: The more than zero bytes read from the remote end
+        :raises ConnectionException: If the remote end didn't send any
+                 data at all before closing the connection.
+        """
+        self.close()
+        size_txt = size if size else "unbounded read"
+        readsize = f"read of {size_txt} bytes"
+        msg = (
+            f"{self}: Connection unexpectedly closed "
+            f"{duration:.3f} seconds into {readsize}"
+        )
+        if data:
+            result = b"".join(data)
+            Log.warning(" after returning {} bytes: {} ", len(result), result)
+            return result
+        msg += " without response from slave before it closed connection"
+        raise ConnectionException(msg)
+
+    def is_socket_open(self) -> bool:
+        """Check if socket is open."""
+        return self.socket is not None
+
+    def __repr__(self):
+        """Return string representation."""
+        return (
+            f"<{self.__class__.__name__} at {hex(id(self))} socket={self.socket}, "
+            f"ipaddr={self.comm_params.host}, port={self.comm_params.port}, timeout={self.comm_params.timeout_connect}>"
+        )
diff --git a/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/client/tls.py b/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/client/tls.py
new file mode 100644
index 00000000..8b368d47
--- /dev/null
+++ b/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/client/tls.py
@@ -0,0 +1,231 @@
+"""Modbus client async TLS communication."""
+from __future__ import annotations
+
+import socket
+import ssl
+from collections.abc import Callable
+
+from pymodbus.client.tcp import AsyncModbusTcpClient, ModbusTcpClient
+from pymodbus.framer import FramerType
+from pymodbus.logging import Log
+from pymodbus.transport import CommParams, CommType
+
+
+class AsyncModbusTlsClient(AsyncModbusTcpClient):
+    """**AsyncModbusTlsClient**.
+
+    Fixed parameters:
+
+    :param host: Host IP address or host name
+
+    Optional parameters:
+
+    :param sslctx: SSLContext to use for TLS
+    :param framer: Framer name, default FramerType.TLS
+    :param port: Port used for communication
+    :param name: Set communication name, used in logging
+    :param source_address: Source address of client
+    :param reconnect_delay: Minimum delay in seconds.milliseconds before reconnecting.
+    :param reconnect_delay_max: Maximum delay in seconds.milliseconds before reconnecting.
+    :param timeout: Timeout for connecting and receiving data, in seconds.
+    :param retries: Max number of retries per request.
+    :param on_connect_callback: Function that will be called just before a connection attempt.
+
+    .. tip::
+        **reconnect_delay** doubles automatically with each unsuccessful connect, from
+        **reconnect_delay** to **reconnect_delay_max**.
+        Set `reconnect_delay=0` to avoid automatic reconnection.
+
+    Example::
+
+        from pymodbus.client import AsyncModbusTlsClient
+
+        async def run():
+            client = AsyncModbusTlsClient("localhost")
+
+            await client.connect()
+            ...
+            client.close()
+
+    Please refer to :ref:`Pymodbus internals` for advanced usage.
+    """
+
+    def __init__(  # pylint: disable=too-many-arguments
+        self,
+        host: str,
+        sslctx: ssl.SSLContext = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT),
+        framer: FramerType = FramerType.TLS,
+        port: int = 802,
+        name: str = "comm",
+        source_address: tuple[str, int] | None = None,
+        reconnect_delay: float = 0.1,
+        reconnect_delay_max: float = 300,
+        timeout: float = 3,
+        retries: int = 3,
+        on_connect_callback: Callable[[bool], None] | None = None,
+    ):
+        """Initialize Asyncio Modbus TLS Client."""
+        self.comm_params = CommParams(
+            comm_type=CommType.TLS,
+            host=host,
+            sslctx=sslctx,
+            port=port,
+            comm_name=name,
+            source_address=source_address,
+            reconnect_delay=reconnect_delay,
+            reconnect_delay_max=reconnect_delay_max,
+            timeout_connect=timeout,
+        )
+        AsyncModbusTcpClient.__init__(
+            self,
+            "",
+            framer=framer,
+            retries=retries,
+            on_connect_callback=on_connect_callback,
+        )
+
+    @classmethod
+    def generate_ssl(
+        cls,
+        certfile: str | None = None,
+        keyfile: str | None = None,
+        password: str | None = None,
+    ) -> ssl.SSLContext:
+        """Generate sslctx from cert/key/password.
+
+        :param certfile: Cert file path for TLS server request
+        :param keyfile: Key file path for TLS server request
+        :param password: Password for for decrypting private key file
+
+        Remark:
+        - MODBUS/TCP Security Protocol Specification demands TLSv2 at least
+        - verify_mode is set to ssl.NONE
+        """
+        return CommParams.generate_ssl(
+            False, certfile=certfile, keyfile=keyfile, password=password
+        )
+
+class ModbusTlsClient(ModbusTcpClient):
+    """**ModbusTlsClient**.
+
+    Fixed parameters:
+
+    :param host: Host IP address or host name
+
+    Optional parameters:
+
+    :param sslctx: SSLContext to use for TLS
+    :param framer: Framer name, default FramerType.TLS
+    :param port: Port used for communication
+    :param name: Set communication name, used in logging
+    :param source_address: Source address of client
+    :param reconnect_delay: Not used in the sync client
+    :param reconnect_delay_max: Not used in the sync client
+    :param timeout: Timeout for connecting and receiving data, in seconds.
+    :param retries: Max number of retries per request.
+
+    .. tip::
+        Unlike the async client, the sync client does not perform
+        retries. If the connection has closed, the client will attempt to reconnect
+        once before executing each read/write request, and will raise a
+        ConnectionException if this fails.
+
+    Example::
+
+        from pymodbus.client import ModbusTlsClient
+
+        async def run():
+            client = ModbusTlsClient("localhost")
+
+            client.connect()
+            ...
+            client.close()
+
+    Please refer to :ref:`Pymodbus internals` for advanced usage.
+    """
+
+    def __init__(  # pylint: disable=too-many-arguments
+        self,
+        host: str,
+        sslctx: ssl.SSLContext = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT),
+        framer: FramerType = FramerType.TLS,
+        port: int = 802,
+        name: str = "comm",
+        source_address: tuple[str, int] | None = None,
+        reconnect_delay: float = 0.1,
+        reconnect_delay_max: float = 300,
+        timeout: float = 3,
+        retries: int = 3,
+    ):
+        """Initialize Modbus TLS Client."""
+        self.comm_params = CommParams(
+            comm_type=CommType.TLS,
+            host=host,
+            sslctx=sslctx,
+            port=port,
+            comm_name=name,
+            source_address=source_address,
+            reconnect_delay=reconnect_delay,
+            reconnect_delay_max=reconnect_delay_max,
+            timeout_connect=timeout,
+        )
+        super().__init__(
+            "",
+            framer=framer,
+            retries=retries,
+        )
+
+    @classmethod
+    def generate_ssl(
+        cls,
+        certfile: str | None = None,
+        keyfile: str | None = None,
+        password: str | None = None,
+    ) -> ssl.SSLContext:
+        """Generate sslctx from cert/key/password.
+
+        :param certfile: Cert file path for TLS server request
+        :param keyfile: Key file path for TLS server request
+        :param password: Password for for decrypting private key file
+
+        Remark:
+        - MODBUS/TCP Security Protocol Specification demands TLSv2 at least
+        - verify_mode is set to ssl.NONE
+        """
+        return CommParams.generate_ssl(
+            False, certfile=certfile, keyfile=keyfile, password=password,
+        )
+
+    @property
+    def connected(self) -> bool:
+        """Connect internal."""
+        return self.transport is not None
+
+    def connect(self):
+        """Connect to the modbus tls server."""
+        if self.socket:
+            return True
+        try:
+            sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+            if self.comm_params.source_address:
+                sock.bind(self.comm_params.source_address)
+            self.socket = self.comm_params.sslctx.wrap_socket(sock, server_side=False)  # type: ignore[union-attr]
+            self.socket.settimeout(self.comm_params.timeout_connect)
+            self.socket.connect((self.comm_params.host, self.comm_params.port))
+        except OSError as msg:
+            Log.error(
+                "Connection to ({}, {}) failed: {}",
+                self.comm_params.host,
+                self.comm_params.port,
+                msg,
+            )
+            self.close()
+        return self.socket is not None
+
+    def __repr__(self):
+        """Return string representation."""
+        return (
+            f"<{self.__class__.__name__} at {hex(id(self))} socket={self.socket}, "
+            f"ipaddr={self.comm_params.host}, port={self.comm_params.port}, sslctx={self.comm_params.sslctx}, "
+            f"timeout={self.comm_params.timeout_connect}>"
+        )
diff --git a/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/client/udp.py b/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/client/udp.py
new file mode 100644
index 00000000..86c05895
--- /dev/null
+++ b/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/client/udp.py
@@ -0,0 +1,222 @@
+"""Modbus client async UDP communication."""
+from __future__ import annotations
+
+import socket
+import time
+from collections.abc import Callable
+
+from pymodbus.client.base import ModbusBaseClient, ModbusBaseSyncClient
+from pymodbus.exceptions import ConnectionException
+from pymodbus.framer import FramerType
+from pymodbus.logging import Log
+from pymodbus.transport import CommParams, CommType
+
+
+DGRAM_TYPE = socket.SOCK_DGRAM
+
+
+class AsyncModbusUdpClient(ModbusBaseClient):
+    """**AsyncModbusUdpClient**.
+
+    Fixed parameters:
+
+    :param host: Host IP address or host name
+
+    Optional parameters:
+
+    :param framer: Framer name, default FramerType.SOCKET
+    :param port: Port used for communication.
+    :param name: Set communication name, used in logging
+    :param source_address: source address of client,
+    :param reconnect_delay: Minimum delay in seconds.milliseconds before reconnecting.
+    :param reconnect_delay_max: Maximum delay in seconds.milliseconds before reconnecting.
+    :param timeout: Timeout for connecting and receiving data, in seconds.
+    :param retries: Max number of retries per request.
+    :param on_connect_callback: Function that will be called just before a connection attempt.
+
+    .. tip::
+        **reconnect_delay** doubles automatically with each unsuccessful connect, from
+        **reconnect_delay** to **reconnect_delay_max**.
+        Set `reconnect_delay=0` to avoid automatic reconnection.
+
+    Example::
+
+        from pymodbus.client import AsyncModbusUdpClient
+
+        async def run():
+            client = AsyncModbusUdpClient("localhost")
+
+            await client.connect()
+            ...
+            client.close()
+
+    Please refer to :ref:`Pymodbus internals` for advanced usage.
+    """
+
+    def __init__(  # pylint: disable=too-many-arguments
+        self,
+        host: str,
+        framer: FramerType = FramerType.SOCKET,
+        port: int = 502,
+        name: str = "comm",
+        source_address: tuple[str, int] | None = None,
+        reconnect_delay: float = 0.1,
+        reconnect_delay_max: float = 300,
+        timeout: float = 3,
+        retries: int = 3,
+        on_connect_callback: Callable[[bool], None] | None = None,
+    ) -> None:
+        """Initialize Asyncio Modbus UDP Client."""
+        self.comm_params = CommParams(
+            comm_type=CommType.UDP,
+            host=host,
+            port=port,
+            comm_name=name,
+            source_address=source_address,
+            reconnect_delay=reconnect_delay,
+            reconnect_delay_max=reconnect_delay_max,
+            timeout_connect=timeout,
+        )
+        ModbusBaseClient.__init__(
+            self,
+            framer,
+            retries,
+            on_connect_callback,
+        )
+        self.source_address = source_address
+
+
+class ModbusUdpClient(ModbusBaseSyncClient):
+    """**ModbusUdpClient**.
+
+    Fixed parameters:
+
+    :param host: Host IP address or host name
+
+    Optional parameters:
+
+    :param framer: Framer name, default FramerType.SOCKET
+    :param port: Port used for communication.
+    :param name: Set communication name, used in logging
+    :param source_address: source address of client,
+    :param reconnect_delay: Not used in the sync client
+    :param reconnect_delay_max: Not used in the sync client
+    :param timeout: Timeout for connecting and receiving data, in seconds.
+    :param retries: Max number of retries per request.
+
+    .. tip::
+        Unlike the async client, the sync client does not perform
+        retries. If the connection has closed, the client will attempt to reconnect
+        once before executing each read/write request, and will raise a
+        ConnectionException if this fails.
+
+    Example::
+
+        from pymodbus.client import ModbusUdpClient
+
+        async def run():
+            client = ModbusUdpClient("localhost")
+
+            client.connect()
+            ...
+            client.close()
+
+    Please refer to :ref:`Pymodbus internals` for advanced usage.
+    """
+
+    socket: socket.socket | None
+
+    def __init__(
+        self,
+        host: str,
+        framer: FramerType = FramerType.SOCKET,
+        port: int = 502,
+        name: str = "comm",
+        source_address: tuple[str, int] | None = None,
+        reconnect_delay: float = 0.1,
+        reconnect_delay_max: float = 300,
+        timeout: float = 3,
+        retries: int = 3,
+    ) -> None:
+        """Initialize Modbus UDP Client."""
+        self.comm_params = CommParams(
+            comm_type=CommType.UDP,
+            host=host,
+            port=port,
+            comm_name=name,
+            source_address=source_address,
+            reconnect_delay=reconnect_delay,
+            reconnect_delay_max=reconnect_delay_max,
+            timeout_connect=timeout,
+        )
+        super().__init__(framer, retries)
+        self.socket = None
+
+    @property
+    def connected(self) -> bool:
+        """Connect internal."""
+        return self.socket is not None
+
+    def connect(self):
+        """Connect to the modbus tcp server.
+
+        :meta private:
+        """
+        if self.socket:
+            return True
+        try:
+            family = ModbusUdpClient.get_address_family(self.comm_params.host)
+            self.socket = socket.socket(family, socket.SOCK_DGRAM)
+            self.socket.settimeout(self.comm_params.timeout_connect)
+        except OSError as exc:
+            Log.error("Unable to create udp socket {}", exc)
+            self.close()
+        return self.socket is not None
+
+    def close(self):
+        """Close the underlying socket connection.
+
+        :meta private:
+        """
+        self.socket = None
+
+    def send(self, request: bytes) -> int:
+        """Send data on the underlying socket.
+
+        :meta private:
+        """
+        super()._start_send()
+        if not self.socket:
+            raise ConnectionException(str(self))
+        if request:
+            return self.socket.sendto(
+                request, (self.comm_params.host, self.comm_params.port)
+            )
+        return 0
+
+    def recv(self, size: int | None) -> bytes:
+        """Read data from the underlying descriptor.
+
+        :meta private:
+        """
+        if not self.socket:
+            raise ConnectionException(str(self))
+        if size is None:
+            size = 0
+        data = self.socket.recvfrom(size)[0]
+        self.last_frame_end = round(time.time(), 6)
+        return data
+
+    def is_socket_open(self):
+        """Check if socket is open.
+
+        :meta private:
+        """
+        return True
+
+    def __repr__(self):
+        """Return string representation."""
+        return (
+            f"<{self.__class__.__name__} at {hex(id(self))} socket={self.socket}, "
+            f"ipaddr={self.comm_params.host}, port={self.comm_params.port}, timeout={self.comm_params.timeout_connect}>"
+        )
diff --git a/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/constants.py b/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/constants.py
new file mode 100644
index 00000000..446defe9
--- /dev/null
+++ b/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/constants.py
@@ -0,0 +1,144 @@
+"""Constants For Modbus Server/Client.
+
+This is the single location for storing default
+values for the servers and clients.
+"""
+import enum
+
+
+INTERNAL_ERROR = "Pymodbus internal error"
+
+
+class ModbusStatus(int, enum.Enum):
+    """These represent various status codes in the modbus protocol.
+
+    .. attribute:: WAITING
+
+       This indicates that a modbus device is currently
+       waiting for a given request to finish some running task.
+
+    .. attribute:: READY
+
+       This indicates that a modbus device is currently
+       free to perform the next request task.
+
+    .. attribute:: ON
+
+       This indicates that the given modbus entity is on
+
+    .. attribute:: OFF
+
+       This indicates that the given modbus entity is off
+
+    .. attribute:: SLAVE_ON
+
+       This indicates that the given modbus slave is running
+
+    .. attribute:: SLAVE_OFF
+
+       This indicates that the given modbus slave is not running
+    """
+
+    WAITING = 0xFFFF
+    READY = 0x0000
+    ON = 0xFF00
+    OFF = 0x0000
+    SLAVE_ON = 0xFF
+    SLAVE_OFF = 0x00
+
+
+class Endian(str, enum.Enum):
+    """An enumeration representing the various byte endianness.
+
+    .. attribute:: AUTO
+
+       This indicates that the byte order is chosen by the
+       current native environment.
+
+    .. attribute:: BIG
+
+       This indicates that the bytes are in big endian format
+
+    .. attribute:: LITTLE
+
+       This indicates that the bytes are in little endian format
+
+    .. note:: I am simply borrowing the format strings from the
+       python struct module for my convenience.
+    """
+
+    AUTO = "@"
+    BIG = ">"
+    LITTLE = "<"
+
+
+class ModbusPlusOperation(int, enum.Enum):
+    """Represents the type of modbus plus request.
+
+    .. attribute:: GET_STATISTICS
+
+       Operation requesting that the current modbus plus statistics
+       be returned in the response.
+
+    .. attribute:: CLEAR_STATISTICS
+
+       Operation requesting that the current modbus plus statistics
+       be cleared and not returned in the response.
+    """
+
+    GET_STATISTICS = 0x0003
+    CLEAR_STATISTICS = 0x0004
+
+
+class DeviceInformation(int, enum.Enum):
+    """Represents what type of device information to read.
+
+    .. attribute:: BASIC
+
+       This is the basic (required) device information to be returned.
+       This includes VendorName, ProductCode, and MajorMinorRevision
+       code.
+
+    .. attribute:: REGULAR
+
+       In addition to basic data objects, the device provides additional
+       and optional identification and description data objects. All of
+       the objects of this category are defined in the standard but their
+       implementation is optional.
+
+    .. attribute:: EXTENDED
+
+       In addition to regular data objects, the device provides additional
+       and optional identification and description private data about the
+       physical device itself. All of these data are device dependent.
+
+    .. attribute:: SPECIFIC
+
+       Request to return a single data object.
+    """
+
+    BASIC = 0x01
+    REGULAR = 0x02
+    EXTENDED = 0x03
+    SPECIFIC = 0x04
+
+    def __str__(self):
+        """Override to force int representation for enum members."""
+        return str(int(self))
+
+
+class MoreData(int, enum.Enum):
+    """Represents the more follows condition.
+
+    .. attribute:: NOTHING
+
+       This indicates that no more objects are going to be returned.
+
+    .. attribute:: KEEP_READING
+
+       This indicates that there are more objects to be returned.
+    """
+
+    NOTHING = 0x00
+    KEEP_READING = 0xFF
+
diff --git a/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/datastore/__init__.py b/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/datastore/__init__.py
new file mode 100644
index 00000000..e66600d7
--- /dev/null
+++ b/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/datastore/__init__.py
@@ -0,0 +1,21 @@
+"""Datastore."""
+
+__all__ = [
+    "ModbusBaseSlaveContext",
+    "ModbusSequentialDataBlock",
+    "ModbusSparseDataBlock",
+    "ModbusSlaveContext",
+    "ModbusServerContext",
+    "ModbusSimulatorContext",
+]
+
+from pymodbus.datastore.context import (
+    ModbusBaseSlaveContext,
+    ModbusServerContext,
+    ModbusSlaveContext,
+)
+from pymodbus.datastore.simulator import ModbusSimulatorContext
+from pymodbus.datastore.store import (
+    ModbusSequentialDataBlock,
+    ModbusSparseDataBlock,
+)
diff --git a/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/datastore/context.py b/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/datastore/context.py
new file mode 100644
index 00000000..ea76b7ff
--- /dev/null
+++ b/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/datastore/context.py
@@ -0,0 +1,250 @@
+"""Context for datastore."""
+
+from __future__ import annotations
+
+# pylint: disable=missing-type-doc
+from pymodbus.datastore.store import ModbusSequentialDataBlock
+from pymodbus.exceptions import NoSuchSlaveException
+from pymodbus.logging import Log
+
+
+class ModbusBaseSlaveContext:
+    """Interface for a modbus slave data context.
+
+    Derived classes must implemented the following methods:
+            reset(self)
+            validate(self, fx, address, count=1)
+            getValues/async_getValues(self, fc_as_hex, address, count=1)
+            setValues/async_setValues(self, fc_as_hex, address, values)
+    """
+
+    _fx_mapper = {2: "d", 4: "i"}
+    _fx_mapper.update([(i, "h") for i in (3, 6, 16, 22, 23)])
+    _fx_mapper.update([(i, "c") for i in (1, 5, 15)])
+
+    def decode(self, fx):
+        """Convert the function code to the datastore to.
+
+        :param fx: The function we are working with
+        :returns: one of [d(iscretes),i(nputs),h(olding),c(oils)
+        """
+        return self._fx_mapper[fx]
+
+    async def async_getValues(self, fc_as_hex: int, address: int, count: int = 1) -> list[int | bool | None]:
+        """Get `count` values from datastore.
+
+        :param fc_as_hex: The function we are working with
+        :param address: The starting address
+        :param count: The number of values to retrieve
+        :returns: The requested values from a:a+c
+        """
+        return self.getValues(fc_as_hex, address, count)
+
+    async def async_setValues(self, fc_as_hex: int, address: int, values: list[int | bool]) -> None:
+        """Set the datastore with the supplied values.
+
+        :param fc_as_hex: The function we are working with
+        :param address: The starting address
+        :param values: The new values to be set
+        """
+        self.setValues(fc_as_hex, address, values)
+
+    def getValues(self, fc_as_hex: int, address: int, count: int = 1) -> list[int | bool | None]:
+        """Get `count` values from datastore.
+
+        :param fc_as_hex: The function we are working with
+        :param address: The starting address
+        :param count: The number of values to retrieve
+        :returns: The requested values from a:a+c
+        """
+        Log.error("getValues({},{},{}) not implemented!", fc_as_hex, address, count)
+        return []
+
+    def setValues(self, fc_as_hex: int, address: int, values: list[int | bool]) -> None:
+        """Set the datastore with the supplied values.
+
+        :param fc_as_hex: The function we are working with
+        :param address: The starting address
+        :param values: The new values to be set
+        """
+
+
+# ---------------------------------------------------------------------------#
+#  Slave Contexts
+# ---------------------------------------------------------------------------#
+class ModbusSlaveContext(ModbusBaseSlaveContext):
+    """Create a modbus data model with data stored in a block.
+
+    :param di: discrete inputs initializer ModbusDataBlock
+    :param co: coils initializer ModbusDataBlock
+    :param hr: holding register initializer ModbusDataBlock
+    :param ir: input registers initializer ModbusDataBlock
+    :param zero_mode: Not add one to address
+
+        When True, a request for address zero to n will map to
+        datastore address zero to n.
+
+        When False, a request for address zero to n will map to
+        datastore address one to n+1, based on section 4.4 of
+        specification.
+
+        Default is False.
+
+    """
+
+    def __init__(self, *_args,
+                    di=ModbusSequentialDataBlock.create(),
+                    co=ModbusSequentialDataBlock.create(),
+                    ir=ModbusSequentialDataBlock.create(),
+                    hr=ModbusSequentialDataBlock.create(),
+                    zero_mode=False):
+        """Initialize the datastores."""
+        self.store = {}
+        self.store["d"] = di
+        self.store["c"] = co
+        self.store["i"] = ir
+        self.store["h"] = hr
+        self.zero_mode = zero_mode
+
+    def __str__(self):
+        """Return a string representation of the context.
+
+        :returns: A string representation of the context
+        """
+        return "Modbus Slave Context"
+
+    def reset(self):
+        """Reset all the datastores to their default values."""
+        for datastore in iter(self.store.values()):
+            datastore.reset()
+
+    def validate(self, fc_as_hex, address, count=1):
+        """Validate the request to make sure it is in range.
+
+        :param fc_as_hex: The function we are working with
+        :param address: The starting address
+        :param count: The number of values to test
+        :returns: True if the request in within range, False otherwise
+        """
+        if not self.zero_mode:
+            address += 1
+        Log.debug("validate: fc-[{}] address-{}: count-{}", fc_as_hex, address, count)
+        return self.store[self.decode(fc_as_hex)].validate(address, count)
+
+    def getValues(self, fc_as_hex, address, count=1):
+        """Get `count` values from datastore.
+
+        :param fc_as_hex: The function we are working with
+        :param address: The starting address
+        :param count: The number of values to retrieve
+        :returns: The requested values from a:a+c
+        """
+        if not self.zero_mode:
+            address += 1
+        Log.debug("getValues: fc-[{}] address-{}: count-{}", fc_as_hex, address, count)
+        return self.store[self.decode(fc_as_hex)].getValues(address, count)
+
+    def setValues(self, fc_as_hex, address, values):
+        """Set the datastore with the supplied values.
+
+        :param fc_as_hex: The function we are working with
+        :param address: The starting address
+        :param values: The new values to be set
+        """
+        if not self.zero_mode:
+            address += 1
+        Log.debug("setValues[{}] address-{}: count-{}", fc_as_hex, address, len(values))
+        self.store[self.decode(fc_as_hex)].setValues(address, values)
+
+    def register(self, function_code, fc_as_hex, datablock=None):
+        """Register a datablock with the slave context.
+
+        :param function_code: function code (int)
+        :param fc_as_hex: string representation of function code (e.g "cf" )
+        :param datablock: datablock to associate with this function code
+        """
+        self.store[fc_as_hex] = datablock or ModbusSequentialDataBlock.create()
+        self._fx_mapper[function_code] = fc_as_hex
+
+
+class ModbusServerContext:
+    """This represents a master collection of slave contexts.
+
+    If single is set to true, it will be treated as a single
+    context so every slave_id returns the same context. If single
+    is set to false, it will be interpreted as a collection of
+    slave contexts.
+    """
+
+    def __init__(self, slaves=None, single=True):
+        """Initialize a new instance of a modbus server context.
+
+        :param slaves: A dictionary of client contexts
+        :param single: Set to true to treat this as a single context
+        """
+        self.single = single
+        self._slaves = slaves or {}
+        if self.single:
+            self._slaves = {0: self._slaves}
+
+    def __iter__(self):
+        """Iterate over the current collection of slave contexts.
+
+        :returns: An iterator over the slave contexts
+        """
+        return iter(self._slaves.items())
+
+    def __contains__(self, slave):
+        """Check if the given slave is in this list.
+
+        :param slave: slave The slave to check for existence
+        :returns: True if the slave exists, False otherwise
+        """
+        if self.single and self._slaves:
+            return True
+        return slave in self._slaves
+
+    def __setitem__(self, slave, context):
+        """Use to set a new slave context.
+
+        :param slave: The slave context to set
+        :param context: The new context to set for this slave
+        :raises NoSuchSlaveException:
+        """
+        if self.single:
+            slave = 0
+        if 0xF7 >= slave >= 0x00:
+            self._slaves[slave] = context
+        else:
+            raise NoSuchSlaveException(f"slave index :{slave} out of range")
+
+    def __delitem__(self, slave):
+        """Use to access the slave context.
+
+        :param slave: The slave context to remove
+        :raises NoSuchSlaveException:
+        """
+        if not self.single and (0xF7 >= slave >= 0x00):
+            del self._slaves[slave]
+        else:
+            raise NoSuchSlaveException(f"slave index: {slave} out of range")
+
+    def __getitem__(self, slave):
+        """Use to get access to a slave context.
+
+        :param slave: The slave context to get
+        :returns: The requested slave context
+        :raises NoSuchSlaveException:
+        """
+        if self.single:
+            slave = 0
+        if slave in self._slaves:
+            return self._slaves.get(slave)
+        raise NoSuchSlaveException(
+            f"slave - {slave} does not exist, or is out of range"
+        )
+
+    def slaves(self):
+        """Define slaves."""
+        # Python3 now returns keys() as iterable
+        return list(self._slaves.keys())
diff --git a/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/datastore/remote.py b/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/datastore/remote.py
new file mode 100644
index 00000000..85452be5
--- /dev/null
+++ b/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/datastore/remote.py
@@ -0,0 +1,129 @@
+"""Remote datastore."""
+from pymodbus.datastore import ModbusBaseSlaveContext
+from pymodbus.exceptions import NotImplementedException
+from pymodbus.logging import Log
+
+
+# ---------------------------------------------------------------------------#
+#  Context
+# ---------------------------------------------------------------------------#
+class RemoteSlaveContext(ModbusBaseSlaveContext):
+    """TODO.
+
+    This creates a modbus data model that connects to
+    a remote device (depending on the client used)
+    """
+
+    def __init__(self, client, slave=None):
+        """Initialize the datastores.
+
+        :param client: The client to retrieve values with
+        :param slave: Unit ID of the remote slave
+        """
+        self._client = client
+        self.slave = slave
+        self.result = None
+        self.__build_mapping()
+        if not self.__set_callbacks:
+            Log.error("Init went wrong.")
+
+    def reset(self):
+        """Reset all the datastores to their default values."""
+        raise NotImplementedException()
+
+    def validate(self, _fc_as_hex, _address, _count):
+        """Validate the request to make sure it is in range.
+
+        :returns: True
+        """
+        return True
+
+    def getValues(self, fc_as_hex, _address, _count=1):
+        """Get values from real call in validate."""
+        if fc_as_hex in self._write_fc:
+            return [0]
+        group_fx = self.decode(fc_as_hex)
+        func_fc = self.__get_callbacks[group_fx]
+        self.result = func_fc(_address, _count)
+        return self.__extract_result(self.decode(fc_as_hex), self.result)
+
+    def setValues(self, fc_as_hex, address, values):
+        """Set the datastore with the supplied values."""
+        group_fx = self.decode(fc_as_hex)
+        if fc_as_hex not in self._write_fc:
+            raise ValueError(f"setValues() called with an non-write function code {fc_as_hex}")
+        func_fc = self.__set_callbacks[f"{group_fx}{fc_as_hex}"]
+        if fc_as_hex in {0x0F, 0x10}:  # Write Multiple Coils, Write Multiple Registers
+            self.result = func_fc(address, values)
+        else:
+            self.result = func_fc(address, values[0])
+        # if self.result.isError():
+        #    return self.result
+
+    def __str__(self):
+        """Return a string representation of the context.
+
+        :returns: A string representation of the context
+        """
+        return f"Remote Slave Context({self._client})"
+
+    def __build_mapping(self):
+        """Build the function code mapper."""
+        params = {}
+        if self.slave:
+            params["slave"] = self.slave
+        self.__get_callbacks = {
+            "d": lambda a, c: self._client.read_discrete_inputs(
+                a, c, **params
+            ),
+            "c": lambda a, c: self._client.read_coils(
+                a, c, **params
+            ),
+            "h": lambda a, c: self._client.read_holding_registers(
+                a, c, **params
+            ),
+            "i": lambda a, c: self._client.read_input_registers(
+                a, c, **params
+            ),
+        }
+        self.__set_callbacks = {
+            "d5": lambda a, v: self._client.write_coil(
+                a, v, **params
+            ),
+            "d15": lambda a, v: self._client.write_coils(
+                a, v, **params
+            ),
+            "c5": lambda a, v: self._client.write_coil(
+                a, v, **params
+            ),
+            "c15": lambda a, v: self._client.write_coils(
+                a, v, **params
+            ),
+            "h6": lambda a, v: self._client.write_register(
+                a, v, **params
+            ),
+            "h16": lambda a, v: self._client.write_registers(
+                a, v, **params
+            ),
+            "i6": lambda a, v: self._client.write_register(
+                a, v, **params
+            ),
+            "i16": lambda a, v: self._client.write_registers(
+                a, v, **params
+            ),
+        }
+        self._write_fc = (0x05, 0x06, 0x0F, 0x10)
+
+    def __extract_result(self, fc_as_hex, result):
+        """Extract the values out of a response.
+
+        TODO make this consistent (values?)
+        """
+        if not result.isError():
+            if fc_as_hex in {"d", "c"}:
+                return result.bits
+            if fc_as_hex in {"h", "i"}:
+                return result.registers
+        else:
+            return result
+        return None
diff --git a/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/datastore/simulator.py b/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/datastore/simulator.py
new file mode 100644
index 00000000..69ecb2a2
--- /dev/null
+++ b/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/datastore/simulator.py
@@ -0,0 +1,803 @@
+"""Pymodbus ModbusSimulatorContext."""
+from __future__ import annotations
+
+import dataclasses
+import random
+import struct
+from collections.abc import Callable
+from datetime import datetime
+from typing import Any
+
+from pymodbus.datastore.context import ModbusBaseSlaveContext
+
+
+WORD_SIZE = 16
+
+
+@dataclasses.dataclass(frozen=True)
+class CellType:
+    """Define single cell types."""
+
+    INVALID: int = 0
+    BITS: int = 1
+    UINT16: int = 2
+    UINT32: int = 3
+    FLOAT32: int = 4
+    STRING: int = 5
+    NEXT: int = 6
+
+
+@dataclasses.dataclass(repr=False, eq=False)
+class Cell:
+    """Handle a single cell."""
+
+    type: int = CellType.INVALID
+    access: bool = False
+    value: int = 0
+    action: int = 0
+    action_parameters: dict[str, Any] | None = None
+    count_read: int = 0
+    count_write: int = 0
+
+
+class TextCell:  # pylint: disable=too-few-public-methods
+    """A textual representation of a single cell."""
+
+    type: str
+    access: str
+    value: str
+    action: str
+    action_parameters: str
+    count_read: str
+    count_write: str
+
+
+@dataclasses.dataclass
+class Label:  # pylint: disable=too-many-instance-attributes
+    """Defines all dict values.
+
+    :meta private:
+    """
+
+    action: str = "action"
+    addr: str = "addr"
+    any: str = "any"
+    co_size: str = "co size"
+    defaults: str = "defaults"
+    di_size: str = "di size"
+    hr_size: str = "hr size"
+    increment: str = "increment"
+    invalid: str = "invalid"
+    ir_size: str = "ir size"
+    parameters: str = "parameters"
+    method: str = "method"
+    next: str = "next"
+    none: str = "none"
+    random: str = "random"
+    repeat: str = "repeat"
+    reset: str = "reset"
+    setup: str = "setup"
+    shared_blocks: str = "shared blocks"
+    timestamp: str = "timestamp"
+    repeat_to: str = "to"
+    type: str = "type"
+    type_bits = "bits"
+    type_exception: str = "type exception"
+    type_uint16: str = "uint16"
+    type_uint32: str = "uint32"
+    type_float32: str = "float32"
+    type_string: str = "string"
+    uptime: str = "uptime"
+    value: str = "value"
+    write: str = "write"
+
+    @classmethod
+    def try_get(cls, key, config_part):
+        """Check if entry is present in config."""
+        if key not in config_part:
+            txt = f"ERROR Configuration invalid, missing {key} in {config_part}"
+            raise RuntimeError(txt)
+        return config_part[key]
+
+
+class Setup:
+    """Setup simulator.
+
+    :meta private:
+    """
+
+    def __init__(self, runtime):
+        """Initialize."""
+        self.runtime = runtime
+        self.config = {}
+        self.config_types: dict[str, dict[str, Any]] = {
+            Label.type_bits: {
+                Label.type: CellType.BITS,
+                Label.next: None,
+                Label.value: 0,
+                Label.action: None,
+                Label.method: self.handle_type_bits,
+            },
+            Label.type_uint16: {
+                Label.type: CellType.UINT16,
+                Label.next: None,
+                Label.value: 0,
+                Label.action: None,
+                Label.method: self.handle_type_uint16,
+            },
+            Label.type_uint32: {
+                Label.type: CellType.UINT32,
+                Label.next: CellType.NEXT,
+                Label.value: 0,
+                Label.action: None,
+                Label.method: self.handle_type_uint32,
+            },
+            Label.type_float32: {
+                Label.type: CellType.FLOAT32,
+                Label.next: CellType.NEXT,
+                Label.value: 0,
+                Label.action: None,
+                Label.method: self.handle_type_float32,
+            },
+            Label.type_string: {
+                Label.type: CellType.STRING,
+                Label.next: CellType.NEXT,
+                Label.value: 0,
+                Label.action: None,
+                Label.method: self.handle_type_string,
+            },
+        }
+
+    def handle_type_bits(self, start, stop, value, action, action_parameters):
+        """Handle type bits."""
+        for reg in self.runtime.registers[start:stop]:
+            if reg.type != CellType.INVALID:
+                raise RuntimeError(f'ERROR "{Label.type_bits}" {reg} used')
+            reg.value = value
+            reg.type = CellType.BITS
+            reg.action = action
+            reg.action_parameters = action_parameters
+
+    def handle_type_uint16(self, start, stop, value, action, action_parameters):
+        """Handle type uint16."""
+        for reg in self.runtime.registers[start:stop]:
+            if reg.type != CellType.INVALID:
+                raise RuntimeError(f'ERROR "{Label.type_uint16}" {reg} used')
+            reg.value = value
+            reg.type = CellType.UINT16
+            reg.action = action
+            reg.action_parameters = action_parameters
+
+    def handle_type_uint32(self, start, stop, value, action, action_parameters):
+        """Handle type uint32."""
+        regs_value = ModbusSimulatorContext.build_registers_from_value(value, True)
+        for i in range(start, stop, 2):
+            regs = self.runtime.registers[i : i + 2]
+            if regs[0].type != CellType.INVALID or regs[1].type != CellType.INVALID:
+                raise RuntimeError(f'ERROR "{Label.type_uint32}" {i},{i + 1} used')
+            regs[0].value = regs_value[0]
+            regs[0].type = CellType.UINT32
+            regs[0].action = action
+            regs[0].action_parameters = action_parameters
+            regs[1].value = regs_value[1]
+            regs[1].type = CellType.NEXT
+
+    def handle_type_float32(self, start, stop, value, action, action_parameters):
+        """Handle type uint32."""
+        regs_value = ModbusSimulatorContext.build_registers_from_value(value, False)
+        for i in range(start, stop, 2):
+            regs = self.runtime.registers[i : i + 2]
+            if regs[0].type != CellType.INVALID or regs[1].type != CellType.INVALID:
+                raise RuntimeError(f'ERROR "{Label.type_float32}" {i},{i + 1} used')
+            regs[0].value = regs_value[0]
+            regs[0].type = CellType.FLOAT32
+            regs[0].action = action
+            regs[0].action_parameters = action_parameters
+            regs[1].value = regs_value[1]
+            regs[1].type = CellType.NEXT
+
+    def handle_type_string(self, start, stop, value, action, action_parameters):
+        """Handle type string."""
+        regs = stop - start
+        reg_len = regs * 2
+        if len(value) > reg_len:
+            raise RuntimeError(
+                f'ERROR "{Label.type_string}" {start} too long "{value}"'
+            )
+        value = value.ljust(reg_len)
+        for i in range(stop - start):
+            reg = self.runtime.registers[start + i]
+            if reg.type != CellType.INVALID:
+                raise RuntimeError(f'ERROR "{Label.type_string}" {start + i} used')
+            j = i * 2
+            reg.value = int.from_bytes(bytes(value[j : j + 2], "UTF-8"), "big")
+            reg.type = CellType.NEXT
+        self.runtime.registers[start].type = CellType.STRING
+        self.runtime.registers[start].action = action
+        self.runtime.registers[start].action_parameters = action_parameters
+
+    def handle_setup_section(self):
+        """Load setup section."""
+        layout = Label.try_get(Label.setup, self.config)
+        self.runtime.fc_offset = {key: 0 for key in range(25)}
+        size_co = Label.try_get(Label.co_size, layout)
+        size_di = Label.try_get(Label.di_size, layout)
+        size_hr = Label.try_get(Label.hr_size, layout)
+        size_ir = Label.try_get(Label.ir_size, layout)
+        if Label.try_get(Label.shared_blocks, layout):
+            total_size = max(size_co, size_di, size_hr, size_ir)
+        else:
+            # set offset (block) for each function code
+            # starting with fc = 1, 5, 15
+            self.runtime.fc_offset[2] = size_co
+            total_size = size_co + size_di
+            self.runtime.fc_offset[4] = total_size
+            total_size += size_ir
+            for i in (3, 6, 16, 22, 23):
+                self.runtime.fc_offset[i] = total_size
+            total_size += size_hr
+        first_cell = Cell()
+        self.runtime.registers = [
+            dataclasses.replace(first_cell) for i in range(total_size)
+        ]
+        self.runtime.register_count = total_size
+        self.runtime.type_exception = bool(Label.try_get(Label.type_exception, layout))
+        defaults = Label.try_get(Label.defaults, layout)
+        defaults_value = Label.try_get(Label.value, defaults)
+        defaults_action = Label.try_get(Label.action, defaults)
+        for key, entry in self.config_types.items():
+            entry[Label.value] = Label.try_get(key, defaults_value)
+            if (
+                action := Label.try_get(key, defaults_action)
+            ) not in self.runtime.action_name_to_id:
+                raise RuntimeError(f"ERROR illegal action {key} in {defaults_action}")
+            entry[Label.action] = action
+        del self.config[Label.setup]
+
+    def handle_invalid_address(self):
+        """Handle invalid address."""
+        for entry in Label.try_get(Label.invalid, self.config):
+            if isinstance(entry, int):
+                entry = [entry, entry]
+            for i in range(entry[0], entry[1] + 1):
+                if i >= self.runtime.register_count:
+                    raise RuntimeError(
+                        f'Error section "{Label.invalid}" addr {entry} out of range'
+                    )
+                reg = self.runtime.registers[i]
+                reg.type = CellType.INVALID
+        del self.config[Label.invalid]
+
+    def handle_write_allowed(self):
+        """Handle write allowed."""
+        for entry in Label.try_get(Label.write, self.config):
+            if isinstance(entry, int):
+                entry = [entry, entry]
+            for i in range(entry[0], entry[1] + 1):
+                if i >= self.runtime.register_count:
+                    raise RuntimeError(
+                        f'Error section "{Label.write}" addr {entry} out of range'
+                    )
+                reg = self.runtime.registers[i]
+                if reg.type == CellType.INVALID:
+                    txt = f'ERROR Configuration invalid in section "write" register {i} not defined'
+                    raise RuntimeError(txt)
+                reg.access = True
+        del self.config[Label.write]
+
+    def handle_types(self):
+        """Handle the different types."""
+        for section, type_entry in self.config_types.items():
+            layout = Label.try_get(section, self.config)
+            for entry in layout:
+                if not isinstance(entry, dict):
+                    entry = {Label.addr: entry}
+                regs = Label.try_get(Label.addr, entry)
+                if not isinstance(regs, list):
+                    regs = [regs, regs]
+                start = regs[0]
+                if (stop := regs[1]) >= self.runtime.register_count:
+                    raise RuntimeError(f'Error "{section}" {start}, {stop} illegal')
+                type_entry[Label.method](
+                    start,
+                    stop + 1,
+                    entry.get(Label.value, type_entry[Label.value]),
+                    self.runtime.action_name_to_id[
+                        entry.get(Label.action, type_entry[Label.action])
+                    ],
+                    entry.get(Label.parameters, None),
+                )
+            del self.config[section]
+
+    def handle_repeat(self):
+        """Handle repeat."""
+        for entry in Label.try_get(Label.repeat, self.config):
+            addr = Label.try_get(Label.addr, entry)
+            copy_start = addr[0]
+            copy_end = addr[1]
+            copy_inx = copy_start - 1
+            addr_to = Label.try_get(Label.repeat_to, entry)
+            for inx in range(addr_to[0], addr_to[1] + 1):
+                copy_inx = copy_start if copy_inx >= copy_end else copy_inx + 1
+                if inx >= self.runtime.register_count:
+                    raise RuntimeError(
+                        f'Error section "{Label.repeat}" entry {entry} out of range'
+                    )
+                self.runtime.registers[inx] = dataclasses.replace(
+                    self.runtime.registers[copy_inx]
+                )
+        del self.config[Label.repeat]
+
+    def setup(self, config, custom_actions) -> None:
+        """Load layout from dict with json structure."""
+        actions = {
+            Label.increment: self.runtime.action_increment,
+            Label.random: self.runtime.action_random,
+            Label.reset: self.runtime.action_reset,
+            Label.timestamp: self.runtime.action_timestamp,
+            Label.uptime: self.runtime.action_uptime,
+        }
+        if custom_actions:
+            actions.update(custom_actions)
+        self.runtime.action_name_to_id = {None: 0}
+        self.runtime.action_id_to_name = [Label.none]
+        self.runtime.action_methods = [None]
+        i = 1
+        for key, method in actions.items():
+            self.runtime.action_name_to_id[key] = i
+            self.runtime.action_id_to_name.append(key)
+            self.runtime.action_methods.append(method)
+            i += 1
+        self.runtime.registerType_name_to_id = {
+            Label.type_bits: CellType.BITS,
+            Label.type_uint16: CellType.UINT16,
+            Label.type_uint32: CellType.UINT32,
+            Label.type_float32: CellType.FLOAT32,
+            Label.type_string: CellType.STRING,
+            Label.next: CellType.NEXT,
+            Label.invalid: CellType.INVALID,
+        }
+        self.runtime.registerType_id_to_name = [None] * len(
+            self.runtime.registerType_name_to_id
+        )
+        for name, cell_type in self.runtime.registerType_name_to_id.items():
+            self.runtime.registerType_id_to_name[cell_type] = name
+
+        self.config = config
+        self.handle_setup_section()
+        self.handle_invalid_address()
+        self.handle_types()
+        self.handle_write_allowed()
+        self.handle_repeat()
+        if self.config:
+            raise RuntimeError(f"INVALID key in setup: {self.config}")
+
+
+class ModbusSimulatorContext(ModbusBaseSlaveContext):
+    """Modbus simulator.
+
+    :param config: A dict with structure as shown below.
+    :param actions: A dict with "<name>": <function> structure.
+    :raises RuntimeError: if json contains errors (msg explains what)
+
+    It builds and maintains a virtual copy of a device, with simulation of
+    device specific functions.
+
+    The device is described in a dict, user supplied actions will
+    be added to the builtin actions.
+
+    It is used in conjunction with a pymodbus server.
+
+    Example::
+
+        store = ModbusSimulatorContext(<config dict>, <actions dict>)
+        StartAsyncTcpServer(<host>, context=store)
+
+        Now the server will simulate the defined device with features like:
+
+        - invalid addresses
+        - write protected addresses
+        - optional control of access for string, uint32, bit/bits
+        - builtin actions for e.g. reset/datetime, value increment by read
+        - custom actions
+
+    Description of the json file or dict to be supplied::
+
+        {
+            "setup": {
+                "di size": 0,  --> Size of discrete input block (8 bit)
+                "co size": 0,  --> Size of coils block (8 bit)
+                "ir size": 0,  --> Size of input registers block (16 bit)
+                "hr size": 0,  --> Size of holding registers block (16 bit)
+                "shared blocks": True,  --> share memory for all blocks (largest size wins)
+                "defaults": {
+                    "value": {  --> Initial values (can be overwritten)
+                        "bits": 0x01,
+                        "uint16": 122,
+                        "uint32": 67000,
+                        "float32": 127.4,
+                        "string": " ",
+                    },
+                    "action": {  --> default action (can be overwritten)
+                        "bits": None,
+                        "uint16": None,
+                        "uint32": None,
+                        "float32": None,
+                        "string": None,
+                    },
+                },
+                "type exception": False,  --> return IO exception if read/write on non boundary
+            },
+            "invalid": [  --> List of invalid addresses, IO exception returned
+                51,                --> single register
+                [78, 99],         --> start, end registers, repeated as needed
+            ],
+            "write": [   --> allow write, efault is ReadOnly
+                [5, 5]  --> start, end bytes, repeated as needed
+            ],
+            "bits": [  --> Define bits (1 register == 2 bytes)
+                [30, 31],  --> start, end registers, repeated as needed
+                {"addr": [32, 34], "value": 0xF1},  --> with value
+                {"addr": [35, 36], "action": "increment"},  --> with action
+                {"addr": [37, 38], "action": "increment", "value": 0xF1}  --> with action and value
+                {"addr": [37, 38], "action": "increment", "parameters": {"min": 0, "max": 100}}  --> with action with arguments
+            ],
+            "uint16": [  --> Define uint16 (1 register == 2 bytes)
+                --> same as type_bits
+            ],
+            "uint32": [  --> Define 32 bit integers (2 registers == 4 bytes)
+                --> same as type_bits
+            ],
+            "float32": [  --> Define 32 bit floats (2 registers == 4 bytes)
+                --> same as type_bits
+            ],
+            "string": [  --> Define strings (variable number of registers (each 2 bytes))
+                [21, 22],  --> start, end registers, define 1 string
+                {"addr": 23, 25], "value": "ups"},  --> with value
+                {"addr": 26, 27], "action": "user"},  --> with action
+                {"addr": 28, 29], "action": "", "value": "user"}  --> with action and value
+            ],
+            "repeat": [ --> allows to repeat section e.g. for n devices
+                {"addr": [100, 200], "to": [50, 275]}   --> Repeat registers 100-200 to 50+ until 275
+            ]
+        }
+    """
+
+    # --------------------------------------------
+    # External interfaces
+    # --------------------------------------------
+    start_time = int(datetime.now().timestamp())
+
+    def __init__(
+        self, config: dict[str, Any], custom_actions: dict[str, Callable] | None
+    ) -> None:
+        """Initialize."""
+        self.registers: list[Cell] = []
+        self.fc_offset: dict[int, int] = {}
+        self.register_count = 0
+        self.type_exception = False
+        self.action_name_to_id: dict[str, int] = {}
+        self.action_id_to_name: list[str] = []
+        self.action_methods: list[Callable] = []
+        self.registerType_name_to_id: dict[str, int] = {}
+        self.registerType_id_to_name: list[str] = []
+        Setup(self).setup(config, custom_actions)
+
+    # --------------------------------------------
+    # Simulator server interface
+    # --------------------------------------------
+    def get_text_register(self, register):
+        """Get raw register."""
+        reg = self.registers[register]
+        text_cell = TextCell()
+        text_cell.type = self.registerType_id_to_name[reg.type]
+        text_cell.access = str(reg.access)
+        text_cell.count_read = str(reg.count_read)
+        text_cell.count_write = str(reg.count_write)
+        text_cell.action = self.action_id_to_name[reg.action]
+        if reg.action_parameters:
+            text_cell.action = f"{text_cell.action}({reg.action_parameters})"
+        if reg.type in (CellType.INVALID, CellType.UINT16, CellType.NEXT):
+            text_cell.value = str(reg.value)
+            build_len = 0
+        elif reg.type == CellType.BITS:
+            text_cell.value = hex(reg.value)
+            build_len = 0
+        elif reg.type == CellType.UINT32:
+            tmp_regs = [reg.value, self.registers[register + 1].value]
+            text_cell.value = str(self.build_value_from_registers(tmp_regs, True))
+            build_len = 1
+        elif reg.type == CellType.FLOAT32:
+            tmp_regs = [reg.value, self.registers[register + 1].value]
+            text_cell.value = str(self.build_value_from_registers(tmp_regs, False))
+            build_len = 1
+        else:  # reg.type == CellType.STRING:
+            j = register
+            text_cell.value = ""
+            while True:
+                text_cell.value += str(
+                    self.registers[j].value.to_bytes(2, "big"),
+                    encoding="utf-8",
+                    errors="ignore",
+                )
+                j += 1
+                if self.registers[j].type != CellType.NEXT:
+                    break
+            build_len = j - register - 1
+        reg_txt = f"{register}-{register + build_len}" if build_len else f"{register}"
+        return reg_txt, text_cell
+
+    # --------------------------------------------
+    # Modbus server interface
+    # --------------------------------------------
+
+    _write_func_code = (5, 6, 15, 16, 22, 23)
+    _bits_func_code = (1, 2, 5, 15)
+
+    def loop_validate(self, address, end_address, fx_write):
+        """Validate entry in loop.
+
+        :meta private:
+        """
+        i = address
+        while i < end_address:
+            reg = self.registers[i]
+            if fx_write and not reg.access or reg.type == CellType.INVALID:
+                return False
+            if not self.type_exception:
+                i += 1
+                continue
+            if reg.type == CellType.NEXT:
+                return False
+            if reg.type in (CellType.BITS, CellType.UINT16):
+                i += 1
+            elif reg.type in (CellType.UINT32, CellType.FLOAT32):
+                if i + 1 >= end_address:
+                    return False
+                i += 2
+            else:
+                i += 1
+                while i < end_address:
+                    if self.registers[i].type == CellType.NEXT:
+                        i += 1
+        return True
+
+    def validate(self, func_code, address, count=1):
+        """Check to see if the request is in range.
+
+        :meta private:
+        """
+        if func_code in self._bits_func_code:
+            # Bit count, correct to register count
+            count = int((count + WORD_SIZE - 1) / WORD_SIZE)
+            address = int(address / 16)
+
+        real_address = self.fc_offset[func_code] + address
+        if real_address < 0 or real_address > self.register_count:
+            return False
+
+        fx_write = func_code in self._write_func_code
+        return self.loop_validate(real_address, real_address + count, fx_write)
+
+    def getValues(self, func_code, address, count=1):
+        """Return the requested values of the datastore.
+
+        :meta private:
+        """
+        result = []
+        if func_code not in self._bits_func_code:
+            real_address = self.fc_offset[func_code] + address
+            for i in range(real_address, real_address + count):
+                reg = self.registers[i]
+                parameters = reg.action_parameters if reg.action_parameters else {}
+                if reg.action:
+                    self.action_methods[reg.action](self.registers, i, reg, **parameters)
+                self.registers[i].count_read += 1
+                result.append(reg.value)
+        else:
+            # bit access
+            real_address = self.fc_offset[func_code] + int(address / 16)
+            bit_index = address % 16
+            reg_count = int((count + bit_index + 15) / 16)
+            for i in range(real_address, real_address + reg_count):
+                reg = self.registers[i]
+                if reg.action:
+                    parameters = reg.action_parameters or {}
+                    self.action_methods[reg.action](
+                        self.registers, i, reg, **parameters
+                    )
+                self.registers[i].count_read += 1
+                while count and bit_index < 16:
+                    result.append(bool(reg.value & (2**bit_index)))
+                    count -= 1
+                    bit_index += 1
+                bit_index = 0
+        return result
+
+    def setValues(self, func_code, address, values):
+        """Set the requested values of the datastore.
+
+        :meta private:
+        """
+        if func_code not in self._bits_func_code:
+            real_address = self.fc_offset[func_code] + address
+            for value in values:
+                self.registers[real_address].value = value
+                self.registers[real_address].count_write += 1
+                real_address += 1
+            return
+
+        # bit access
+        real_address = self.fc_offset[func_code] + int(address / 16)
+        bit_index = address % 16
+        for value in values:
+            bit_mask = 2**bit_index
+            if bool(value):
+                self.registers[real_address].value |= bit_mask
+            else:
+                self.registers[real_address].value &= ~bit_mask
+            self.registers[real_address].count_write += 1
+            bit_index += 1
+            if bit_index == 16:
+                bit_index = 0
+                real_address += 1
+        return
+
+    # --------------------------------------------
+    # Internal action methods
+    # --------------------------------------------
+
+    @classmethod
+    def action_random(cls, registers, inx, cell, minval=1, maxval=65536):
+        """Update with random value.
+
+        :meta private:
+        """
+        if cell.type in (CellType.BITS, CellType.UINT16):
+            registers[inx].value = random.randint(int(minval), int(maxval))
+        elif cell.type == CellType.FLOAT32:
+            regs = cls.build_registers_from_value(
+                random.uniform(float(minval), float(maxval)), False
+            )
+            registers[inx].value = regs[0]
+            registers[inx + 1].value = regs[1]
+        elif cell.type == CellType.UINT32:
+            regs = cls.build_registers_from_value(
+                random.randint(int(minval), int(maxval)), True
+            )
+            registers[inx].value = regs[0]
+            registers[inx + 1].value = regs[1]
+
+    @classmethod
+    def action_increment(cls, registers, inx, cell, minval=None, maxval=None):
+        """Increment value reset with overflow.
+
+        :meta private:
+        """
+        reg = registers[inx]
+        reg2 = registers[inx + 1]
+        if cell.type in (CellType.BITS, CellType.UINT16):
+            value = reg.value + 1
+            if maxval and value > maxval:
+                value = minval
+            if minval and value < minval:
+                value = minval
+            reg.value = value
+        elif cell.type == CellType.FLOAT32:
+            tmp_reg = [reg.value, reg2.value]
+            value = cls.build_value_from_registers(tmp_reg, False)
+            value += 1.0
+            if maxval and value > maxval:
+                value = minval
+            if minval and value < minval:
+                value = minval
+            new_regs = cls.build_registers_from_value(value, False)
+            reg.value = new_regs[0]
+            reg2.value = new_regs[1]
+        elif cell.type == CellType.UINT32:
+            tmp_reg = [reg.value, reg2.value]
+            value = cls.build_value_from_registers(tmp_reg, True)
+            value += 1
+            if maxval and value > maxval:
+                value = minval
+            if minval and value < minval:
+                value = minval
+            new_regs = cls.build_registers_from_value(value, True)
+            reg.value = new_regs[0]
+            reg2.value = new_regs[1]
+
+    @classmethod
+    def action_timestamp(cls, registers, inx, _cell, **_parameters):
+        """Set current time.
+
+        :meta private:
+        """
+        system_time = datetime.now()
+        registers[inx].value = system_time.year
+        registers[inx + 1].value = system_time.month - 1
+        registers[inx + 2].value = system_time.day
+        registers[inx + 3].value = system_time.weekday() + 1
+        registers[inx + 4].value = system_time.hour
+        registers[inx + 5].value = system_time.minute
+        registers[inx + 6].value = system_time.second
+
+    @classmethod
+    def action_reset(cls, _registers, _inx, _cell, **_parameters):
+        """Reboot server.
+
+        :meta private:
+        """
+        raise RuntimeError("RESET server")
+
+    @classmethod
+    def action_uptime(cls, registers, inx, cell, **_parameters):
+        """Return uptime in seconds.
+
+        :meta private:
+        """
+        value = int(datetime.now().timestamp()) - cls.start_time + 1
+
+        if cell.type in (CellType.BITS, CellType.UINT16):
+            registers[inx].value = value
+        elif cell.type == CellType.FLOAT32:
+            regs = cls.build_registers_from_value(value, False)
+            registers[inx].value = regs[0]
+            registers[inx + 1].value = regs[1]
+        elif cell.type == CellType.UINT32:
+            regs = cls.build_registers_from_value(value, True)
+            registers[inx].value = regs[0]
+            registers[inx + 1].value = regs[1]
+
+    # --------------------------------------------
+    # Internal helper methods
+    # --------------------------------------------
+
+    def validate_type(self, func_code, real_address, count) -> bool:
+        """Check if request is done against correct type.
+
+        :meta private:
+        """
+        check: tuple
+        if func_code in self._bits_func_code:
+            # Bit access
+            check = (CellType.BITS, -1)
+            reg_step = 1
+        elif count % 2:
+            # 16 bit access
+            check = (CellType.UINT16, CellType.STRING)
+            reg_step = 1
+        else:
+            check = (CellType.UINT32, CellType.FLOAT32, CellType.STRING)
+            reg_step = 2
+
+        for i in range(real_address, real_address + count, reg_step):
+            if self.registers[i].type in check:
+                continue
+            if self.registers[i].type is CellType.NEXT:
+                continue
+            return False
+        return True
+
+    @classmethod
+    def build_registers_from_value(cls, value, is_int):
+        """Build registers from int32 or float32."""
+        regs = [0, 0]
+        if is_int:
+            value_bytes = int.to_bytes(value, 4, "big")
+        else:
+            value_bytes = struct.pack(">f", value)
+        regs[0] = int.from_bytes(value_bytes[:2], "big")
+        regs[1] = int.from_bytes(value_bytes[-2:], "big")
+        return regs
+
+    @classmethod
+    def build_value_from_registers(cls, registers, is_int):
+        """Build int32 or float32 value from registers."""
+        value_bytes = int.to_bytes(registers[0], 2, "big") + int.to_bytes(
+            registers[1], 2, "big"
+        )
+        if is_int:
+            value = int.from_bytes(value_bytes, "big")
+        else:
+            value = struct.unpack(">f", value_bytes)[0]
+        return value
diff --git a/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/datastore/store.py b/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/datastore/store.py
new file mode 100644
index 00000000..51cfbef8
--- /dev/null
+++ b/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/datastore/store.py
@@ -0,0 +1,344 @@
+"""Modbus Server Datastore.
+
+For each server, you will create a ModbusServerContext and pass
+in the default address space for each data access.  The class
+will create and manage the data.
+
+Further modification of said data accesses should be performed
+with [get,set][access]Values(address, count)
+
+Datastore Implementation
+-------------------------
+
+There are two ways that the server datastore can be implemented.
+The first is a complete range from "address" start to "count"
+number of indices.  This can be thought of as a straight array::
+
+    data = range(1, 1 + count)
+    [1,2,3,...,count]
+
+The other way that the datastore can be implemented (and how
+many devices implement it) is a associate-array::
+
+    data = {1:"1", 3:"3", ..., count:"count"}
+    [1,3,...,count]
+
+The difference between the two is that the latter will allow
+arbitrary gaps in its datastore while the former will not.
+This is seen quite commonly in some modbus implementations.
+What follows is a clear example from the field:
+
+Say a company makes two devices to monitor power usage on a rack.
+One works with three-phase and the other with a single phase. The
+company will dictate a modbus data mapping such that registers::
+
+    n:      phase 1 power
+    n+1:    phase 2 power
+    n+2:    phase 3 power
+
+Using this, layout, the first device will implement n, n+1, and n+2,
+however, the second device may set the latter two values to 0 or
+will simply not implemented the registers thus causing a single read
+or a range read to fail.
+
+I have both methods implemented, and leave it up to the user to change
+based on their preference.
+"""
+# pylint: disable=missing-type-doc
+from __future__ import annotations
+
+from abc import ABC, abstractmethod
+from collections.abc import Iterable
+from typing import Any, Generic, TypeVar
+
+from pymodbus.exceptions import ParameterException
+
+
+# ---------------------------------------------------------------------------#
+#  Datablock Storage
+# ---------------------------------------------------------------------------#
+
+V = TypeVar('V', list, dict[int, Any])
+class BaseModbusDataBlock(ABC, Generic[V]):
+    """Base class for a modbus datastore.
+
+    Derived classes must create the following fields:
+            @address The starting address point
+            @defult_value The default value of the datastore
+            @values The actual datastore values
+
+    Derived classes must implemented the following methods:
+            validate(self, address, count=1)
+            getValues(self, address, count=1)
+            setValues(self, address, values)
+            reset(self)
+
+    Derived classes can implemented the following async methods:
+            async_getValues(self, address, count=1)
+            async_setValues(self, address, values)
+    but are not needed since these standard call the sync. methods.
+    """
+
+    values: V
+    address: int
+    default_value: Any
+
+    @abstractmethod
+    def validate(self, address:int, count=1) -> bool:
+        """Check to see if the request is in range.
+
+        :param address: The starting address
+        :param count: The number of values to test for
+        :raises TypeError:
+        """
+
+    async def async_getValues(self, address: int, count=1) -> Iterable:
+        """Return the requested values from the datastore.
+
+        :param address: The starting address
+        :param count: The number of values to retrieve
+        :raises TypeError:
+        """
+        return self.getValues(address, count)
+
+    @abstractmethod
+    def getValues(self, address:int, count=1) -> Iterable:
+        """Return the requested values from the datastore.
+
+        :param address: The starting address
+        :param count: The number of values to retrieve
+        :raises TypeError:
+        """
+
+    async def async_setValues(self, address: int, values: list[int|bool]) -> None:
+        """Set the requested values in the datastore.
+
+        :param address: The starting address
+        :param values: The values to store
+        :raises TypeError:
+        """
+        self.setValues(address, values)
+
+    @abstractmethod
+    def setValues(self, address:int, values) -> None:
+        """Set the requested values in the datastore.
+
+        :param address: The starting address
+        :param values: The values to store
+        :raises TypeError:
+        """
+
+    def __str__(self):
+        """Build a representation of the datastore.
+
+        :returns: A string representation of the datastore
+        """
+        return f"DataStore({len(self.values)}, {self.default_value})"
+
+    def __iter__(self):
+        """Iterate over the data block data.
+
+        :returns: An iterator of the data block data
+        """
+        if isinstance(self.values, dict):
+            return iter(self.values.items())
+        return enumerate(self.values, self.address)
+
+
+class ModbusSequentialDataBlock(BaseModbusDataBlock[list]):
+    """Creates a sequential modbus datastore."""
+
+    def __init__(self, address, values):
+        """Initialize the datastore.
+
+        :param address: The starting address of the datastore
+        :param values: Either a list or a dictionary of values
+        """
+        self.address = address
+        if hasattr(values, "__iter__"):
+            self.values = list(values)
+        else:
+            self.values = [values]
+        self.default_value = self.values[0].__class__()
+
+    @classmethod
+    def create(cls):
+        """Create a datastore.
+
+        With the full address space initialized to 0x00
+
+        :returns: An initialized datastore
+        """
+        return cls(0x00, [0x00] * 65536)
+
+    def default(self, count, value=False):
+        """Use to initialize a store to one value.
+
+        :param count: The number of fields to set
+        :param value: The default value to set to the fields
+        """
+        self.default_value = value
+        self.values = [self.default_value] * count
+        self.address = 0x00
+
+    def reset(self):
+        """Reset the datastore to the initialized default value."""
+        self.values = [self.default_value] * len(self.values)
+
+    def validate(self, address, count=1):
+        """Check to see if the request is in range.
+
+        :param address: The starting address
+        :param count: The number of values to test for
+        :returns: True if the request in within range, False otherwise
+        """
+        result = self.address <= address
+        result &= (self.address + len(self.values)) >= (address + count)
+        return result
+
+    def getValues(self, address, count=1):
+        """Return the requested values of the datastore.
+
+        :param address: The starting address
+        :param count: The number of values to retrieve
+        :returns: The requested values from a:a+c
+        """
+        start = address - self.address
+        return self.values[start : start + count]
+
+    def setValues(self, address, values):
+        """Set the requested values of the datastore.
+
+        :param address: The starting address
+        :param values: The new values to be set
+        """
+        if not isinstance(values, list):
+            values = [values]
+        start = address - self.address
+        self.values[start : start + len(values)] = values
+
+
+class ModbusSparseDataBlock(BaseModbusDataBlock[dict[int, Any]]):
+    """A sparse modbus datastore.
+
+    E.g Usage.
+    sparse = ModbusSparseDataBlock({10: [3, 5, 6, 8], 30: 1, 40: [0]*20})
+
+    This would create a datablock with 3 blocks
+    One starts at offset 10 with length 4, one at 30 with length 1, and one at 40 with length 20
+
+    sparse = ModbusSparseDataBlock([10]*100)
+    Creates a sparse datablock of length 100 starting at offset 0 and default value of 10
+
+    sparse = ModbusSparseDataBlock() --> Create empty datablock
+    sparse.setValues(0, [10]*10)  --> Add block 1 at offset 0 with length 10 (default value 10)
+    sparse.setValues(30, [20]*5)  --> Add block 2 at offset 30 with length 5 (default value 20)
+
+    Unless 'mutable' is set to True during initialization, the datablock cannot be altered with
+    setValues (new datablocks cannot be added)
+    """
+
+    def __init__(self, values=None, mutable=True):
+        """Initialize a sparse datastore.
+
+        Will only answer to addresses registered,
+        either initially here, or later via setValues()
+
+        :param values: Either a list or a dictionary of values
+        :param mutable: Whether the data-block can be altered later with setValues (i.e add more blocks)
+
+        If values is a list, a sequential datablock will be created.
+
+        If values is a dictionary, it should be in {offset: <int | list>} format
+        For each list, a sparse datablock is created, starting at 'offset' with the length of the list
+        For each integer, the value is set for the corresponding offset.
+
+        """
+        self.values = {}
+        self._process_values(values)
+        self.mutable = mutable
+        self.default_value = self.values.copy()
+
+    @classmethod
+    def create(cls, values=None):
+        """Create sparse datastore.
+
+        Use setValues to initialize registers.
+
+        :param values: Either a list or a dictionary of values
+        :returns: An initialized datastore
+        """
+        return cls(values)
+
+    def reset(self):
+        """Reset the store to the initially provided defaults."""
+        self.values = self.default_value.copy()
+
+    def validate(self, address, count=1):
+        """Check to see if the request is in range.
+
+        :param address: The starting address
+        :param count: The number of values to test for
+        :returns: True if the request in within range, False otherwise
+        """
+        if not count:
+            return False
+        handle = set(range(address, address + count))
+        return handle.issubset(set(iter(self.values.keys())))
+
+    def getValues(self, address, count=1):
+        """Return the requested values of the datastore.
+
+        :param address: The starting address
+        :param count: The number of values to retrieve
+        :returns: The requested values from a:a+c
+        """
+        return [self.values[i] for i in range(address, address + count)]
+
+    def _process_values(self, values):
+        """Process values."""
+
+        def _process_as_dict(values):
+            for idx, val in iter(values.items()):
+                if isinstance(val, (list, tuple)):
+                    for i, v_item in enumerate(val):
+                        self.values[idx + i] = v_item
+                else:
+                    self.values[idx] = int(val)
+
+        if isinstance(values, dict):
+            _process_as_dict(values)
+            return
+        if hasattr(values, "__iter__"):
+            values = dict(enumerate(values))
+        elif values is None:
+            values = {}  # Must make a new dict here per instance
+        else:
+            raise ParameterException(
+                "Values for datastore must be a list or dictionary"
+            )
+        _process_as_dict(values)
+
+    def setValues(self, address, values, use_as_default=False):
+        """Set the requested values of the datastore.
+
+        :param address: The starting address
+        :param values: The new values to be set
+        :param use_as_default: Use the values as default
+        :raises ParameterException:
+        """
+        if isinstance(values, dict):
+            new_offsets = list(set(values.keys()) - set(self.values.keys()))
+            if new_offsets and not self.mutable:
+                raise ParameterException(f"Offsets {new_offsets} not in range")
+            self._process_values(values)
+        else:
+            if not isinstance(values, list):
+                values = [values]
+            for idx, val in enumerate(values):
+                if address + idx not in self.values and not self.mutable:
+                    raise ParameterException("Offset {address+idx} not in range")
+                self.values[address + idx] = val
+        if use_as_default:
+            for idx, val in iter(self.values.items()):
+                self.default_value[idx] = val
diff --git a/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/device.py b/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/device.py
new file mode 100644
index 00000000..136ac0c8
--- /dev/null
+++ b/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/device.py
@@ -0,0 +1,586 @@
+"""Modbus Device Controller.
+
+These are the device management handlers.  They should be
+maintained in the server context and the various methods
+should be inserted in the correct locations.
+"""
+from __future__ import annotations
+
+
+__all__ = [
+    "ModbusPlusStatistics",
+    "ModbusDeviceIdentification",
+    "DeviceInformationFactory",
+]
+
+import struct
+
+# pylint: disable=missing-type-doc
+from collections import OrderedDict
+
+from pymodbus.constants import INTERNAL_ERROR, DeviceInformation
+from pymodbus.events import ModbusEvent
+from pymodbus.utilities import dict_property
+
+
+# ---------------------------------------------------------------------------#
+#  Modbus Plus Statistics
+# ---------------------------------------------------------------------------#
+class ModbusPlusStatistics:
+    """This is used to maintain the current modbus plus statistics count.
+
+    As of right now this is simply a stub to complete the modbus implementation.
+    For more information, see the modbus implementation guide page 87.
+    """
+
+    __data = OrderedDict(
+        {
+            "node_type_id": [0x00] * 2,  # 00
+            "software_version_number": [0x00] * 2,  # 01
+            "network_address": [0x00] * 2,  # 02
+            "mac_state_variable": [0x00] * 2,  # 03
+            "peer_status_code": [0x00] * 2,  # 04
+            "token_pass_counter": [0x00] * 2,  # 05
+            "token_rotation_time": [0x00] * 2,  # 06
+            "program_master_token_failed": [0x00],  # 07 hi
+            "data_master_token_failed": [0x00],  # 07 lo
+            "program_master_token_owner": [0x00],  # 08 hi
+            "data_master_token_owner": [0x00],  # 08 lo
+            "program_slave_token_owner": [0x00],  # 09 hi
+            "data_slave_token_owner": [0x00],  # 09 lo
+            "data_slave_command_transfer": [0x00],  # 10 hi
+            "__unused_10_lowbit": [0x00],  # 10 lo
+            "program_slave_command_transfer": [0x00],  # 11 hi
+            "program_master_rsp_transfer": [0x00],  # 11 lo
+            "program_slave_auto_logout": [0x00],  # 12 hi
+            "program_master_connect_status": [0x00],  # 12 lo
+            "receive_buffer_dma_overrun": [0x00],  # 13 hi
+            "pretransmit_deferral_error": [0x00],  # 13 lo
+            "frame_size_error": [0x00],  # 14 hi
+            "repeated_command_received": [0x00],  # 14 lo
+            "receiver_alignment_error": [0x00],  # 15 hi
+            "receiver_collision_abort_error": [0x00],  # 15 lo
+            "bad_packet_length_error": [0x00],  # 16 hi
+            "receiver_crc_error": [0x00],  # 16 lo
+            "transmit_buffer_dma_underrun": [0x00],  # 17 hi
+            "bad_link_address_error": [0x00],  # 17 lo
+            "bad_mac_function_code_error": [0x00],  # 18 hi
+            "internal_packet_length_error": [0x00],  # 18 lo
+            "communication_failed_error": [0x00],  # 19 hi
+            "communication_retries": [0x00],  # 19 lo
+            "no_response_error": [0x00],  # 20 hi
+            "good_receive_packet": [0x00],  # 20 lo
+            "unexpected_path_error": [0x00],  # 21 hi
+            "exception_response_error": [0x00],  # 21 lo
+            "forgotten_transaction_error": [0x00],  # 22 hi
+            "unexpected_response_error": [0x00],  # 22 lo
+            "active_station_bit_map": [0x00] * 8,  # 23-26
+            "token_station_bit_map": [0x00] * 8,  # 27-30
+            "global_data_bit_map": [0x00] * 8,  # 31-34
+            "receive_buffer_use_bit_map": [0x00] * 8,  # 35-37
+            "data_master_output_path": [0x00] * 8,  # 38-41
+            "data_slave_input_path": [0x00] * 8,  # 42-45
+            "program_master_outptu_path": [0x00] * 8,  # 46-49
+            "program_slave_input_path": [0x00] * 8,  # 50-53
+        }
+    )
+
+    def __init__(self):
+        """Initialize the modbus plus statistics with the default information."""
+        self.reset()
+
+    def __iter__(self):
+        """Iterate over the statistics.
+
+        :returns: An iterator of the modbus plus statistics
+        """
+        return iter(self.__data.items())
+
+    def reset(self):
+        """Clear all of the modbus plus statistics."""
+        for key in self.__data:
+            self.__data[key] = [0x00] * len(self.__data[key])
+
+    def summary(self):
+        """Return a summary of the modbus plus statistics.
+
+        :returns: 54 16-bit words representing the status
+        """
+        return iter(self.__data.values())
+
+    def encode(self):
+        """Return a summary of the modbus plus statistics.
+
+        :returns: 54 16-bit words representing the status
+        """
+        total, values = [], sum(self.__data.values(), [])  # noqa: RUF017
+        for i in range(0, len(values), 2):
+            total.append((values[i] << 8) | values[i + 1])
+        return total
+
+
+# ---------------------------------------------------------------------------#
+#  Device Information Control
+# ---------------------------------------------------------------------------#
+class ModbusDeviceIdentification:
+    """This is used to supply the device identification.
+
+    For the readDeviceIdentification function
+
+    For more information read section 6.21 of the modbus
+    application protocol.
+    """
+
+    __data = {
+        0x00: "",  # VendorName
+        0x01: "",  # ProductCode
+        0x02: "",  # MajorMinorRevision
+        0x03: "",  # VendorUrl
+        0x04: "",  # ProductName
+        0x05: "",  # ModelName
+        0x06: "",  # UserApplicationName
+        0x07: "",  # reserved
+        0x08: "",  # reserved
+        # 0x80 -> 0xFF are privatek
+    }
+
+    __names = [
+        "VendorName",
+        "ProductCode",
+        "MajorMinorRevision",
+        "VendorUrl",
+        "ProductName",
+        "ModelName",
+        "UserApplicationName",
+    ]
+
+    def __init__(self, info=None, info_name=None):
+        """Initialize the datastore with the elements you need.
+
+        (note acceptable range is [0x00-0x06,0x80-0xFF] inclusive)
+
+        :param info: A dictionary of {int:string} of values
+        :param set: A dictionary of {name:string} of values
+        """
+        if isinstance(info_name, dict):
+            for key in info_name:
+                inx = self.__names.index(key)
+                self.__data[inx] = info_name[key]
+
+        if isinstance(info, dict):
+            for key in info:
+                if (0x06 >= key >= 0x00) or (0xFF >= key >= 0x80):
+                    self.__data[key] = info[key]
+
+    def __iter__(self):
+        """Iterate over the device information.
+
+        :returns: An iterator of the device information
+        """
+        return iter(self.__data.items())
+
+    def summary(self):
+        """Return a summary of the main items.
+
+        :returns: An dictionary of the main items
+        """
+        return dict(zip(self.__names, iter(self.__data.values())))
+
+    def update(self, value):
+        """Update the values of this identity.
+
+        using another identify as the value
+
+        :param value: The value to copy values from
+        """
+        self.__data.update(value)
+
+    def __setitem__(self, key, value):
+        """Access the device information.
+
+        :param key: The register to set
+        :param value: The new value for referenced register
+        """
+        if key not in [0x07, 0x08]:
+            self.__data[key] = value
+
+    def __getitem__(self, key):
+        """Access the device information.
+
+        :param key: The register to read
+        """
+        return self.__data.setdefault(key, "")
+
+    def __str__(self):
+        """Build a representation of the device.
+
+        :returns: A string representation of the device
+        """
+        return "DeviceIdentity"
+
+    # -------------------------------------------------------------------------#
+    #  Properties
+    # -------------------------------------------------------------------------#
+    # fmt: off
+    VendorName = dict_property(lambda s: s.__data, 0)  # pylint: disable=protected-access
+    ProductCode = dict_property(lambda s: s.__data, 1)  # pylint: disable=protected-access
+    MajorMinorRevision = dict_property(lambda s: s.__data, 2)  # pylint: disable=protected-access
+    VendorUrl = dict_property(lambda s: s.__data, 3)  # pylint: disable=protected-access
+    ProductName = dict_property(lambda s: s.__data, 4)  # pylint: disable=protected-access
+    ModelName = dict_property(lambda s: s.__data, 5)  # pylint: disable=protected-access
+    UserApplicationName = dict_property(lambda s: s.__data, 6)  # pylint: disable=protected-access
+    # fmt: on
+
+
+class DeviceInformationFactory:  # pylint: disable=too-few-public-methods
+    """This is a helper.
+
+    That really just hides
+    some of the complexity of processing the device information
+    requests (function code 0x2b 0x0e).
+    """
+
+    __lookup = {
+        DeviceInformation.BASIC: lambda c, r, i: c.__gets(  # pylint: disable=protected-access
+            r, list(range(i, 0x03))
+        ),
+        DeviceInformation.REGULAR: lambda c, r, i: c.__gets(  # pylint: disable=protected-access
+            r,
+            list(range(i, 0x07))
+            if c.__get(r, i)[i]  # pylint: disable=protected-access
+            else list(range(0, 0x07)),
+        ),
+        DeviceInformation.EXTENDED: lambda c, r, i: c.__gets(  # pylint: disable=protected-access
+            r,
+            [x for x in range(i, 0x100) if x not in range(0x07, 0x80)]
+            if c.__get(r, i)[i]  # pylint: disable=protected-access
+            else [x for x in range(0, 0x100) if x not in range(0x07, 0x80)],
+        ),
+        DeviceInformation.SPECIFIC: lambda c, r, i: c.__get(  # pylint: disable=protected-access
+            r, i
+        ),
+    }
+
+    @classmethod
+    def get(cls, control, read_code=DeviceInformation.BASIC, object_id=0x00):
+        """Get the requested device data from the system.
+
+        :param control: The control block to pull data from
+        :param read_code: The read code to process
+        :param object_id: The specific object_id to read
+        :returns: The requested data (id, length, value)
+        """
+        identity = control.Identity
+        return cls.__lookup[read_code](cls, identity, object_id)
+
+    @classmethod
+    def __get(cls, identity, object_id):  # pylint: disable=unused-private-member
+        """Read a single object_id from the device information.
+
+        :param identity: The identity block to pull data from
+        :param object_id: The specific object id to read
+        :returns: The requested data (id, length, value)
+        """
+        return {object_id: identity[object_id]}
+
+    @classmethod
+    def __gets(cls, identity, object_ids):  # pylint: disable=unused-private-member
+        """Read multiple object_ids from the device information.
+
+        :param identity: The identity block to pull data from
+        :param object_ids: The specific object ids to read
+        :returns: The requested data (id, length, value)
+        """
+        return {oid: identity[oid] for oid in object_ids if identity[oid]}
+
+    def __init__(self):
+        """Prohibit objects."""
+        raise RuntimeError(INTERNAL_ERROR)
+
+
+# ---------------------------------------------------------------------------#
+#  Counters Handler
+# ---------------------------------------------------------------------------#
+class ModbusCountersHandler:
+    """This is a helper class to simplify the properties for the counters.
+
+    0x0B  1  Return Bus Message Count
+
+             Quantity of messages that the remote
+             device has detected on the communications system since its
+             last restart, clear counters operation, or power-up.  Messages
+             with bad CRC are not taken into account.
+
+    0x0C  2  Return Bus Communication Error Count
+
+             Quantity of CRC errors encountered by the remote device since its
+             last restart, clear counters operation, or power-up.  In case of
+             an error detected on the character level, (overrun, parity error),
+             or in case of a message length < 3 bytes, the receiving device is
+             not able to calculate the CRC. In such cases, this counter is
+             also incremented.
+
+    0x0D  3  Return Slave Exception Error Count
+
+             Quantity of MODBUS exception error detected by the remote device
+             since its last restart, clear counters operation, or power-up.
+             Exception errors are described and listed in "MODBUS Application
+             Protocol Specification" document.
+
+    0xOE  4  Return Slave Message Count
+
+             Quantity of messages addressed to the remote device that the remote
+             device has processed since its last restart, clear counters operation,
+             or power-up.
+
+    0x0F  5  Return Slave No Response Count
+
+             Quantity of messages received by the remote device for which it
+             returned no response (neither a normal response nor an exception
+             response), since its last restart, clear counters operation, or
+             power-up.
+
+    0x10  6  Return Slave NAK Count
+
+             Quantity of messages addressed to the remote device for which it
+             returned a Negative Acknowledge (NAK) exception response, since
+             its last restart, clear counters operation, or power-up. Exception
+             responses are described and listed in "MODBUS Application Protocol
+             Specification" document.
+
+    0x11  7  Return Slave Busy Count
+
+             Quantity of messages addressed to the remote device for which it
+             returned a Slave Device Busy exception response, since its last
+             restart, clear counters operation, or power-up. Exception
+             responses are described and listed in "MODBUS Application
+             Protocol Specification" document.
+
+    0x12  8  Return Bus Character Overrun Count
+
+             Quantity of messages addressed to the remote device that it could
+             not handle due to a character overrun condition, since its last
+             restart, clear counters operation, or power-up. A character
+             overrun is caused by data characters arriving at the port faster
+             than they can.
+
+    .. note:: I threw the event counter in here for convenience
+    """
+
+    __data = {i: 0x0000 for i in range(9)}
+    __names = [
+        "BusMessage",
+        "BusCommunicationError",
+        "SlaveExceptionError",
+        "SlaveMessage",
+        "SlaveNoResponse",
+        "SlaveNAK",
+        "SlaveBusy",
+        "BusCharacterOverrun",
+    ]
+
+    def __iter__(self):
+        """Iterate over the device counters.
+
+        :returns: An iterator of the device counters
+        """
+        return zip(self.__names, iter(self.__data.values()))
+
+    def update(self, values):
+        """Update the values of this identity.
+
+        using another identify as the value
+
+        :param values: The value to copy values from
+        """
+        for k, v_item in iter(values.items()):
+            v_item += self.__getattribute__(  # pylint: disable=unnecessary-dunder-call
+                k
+            )
+            self.__setattr__(k, v_item)  # pylint: disable=unnecessary-dunder-call
+
+    def reset(self):
+        """Clear all of the system counters."""
+        self.__data = {i: 0x0000 for i in range(9)}
+
+    def summary(self):
+        """Return a summary of the counters current status.
+
+        :returns: A byte with each bit representing each counter
+        """
+        count, result = 0x01, 0x00
+        for i in iter(self.__data.values()):
+            if i != 0x00:  # pylint: disable=compare-to-zero
+                result |= count
+            count <<= 1
+        return result
+
+    # -------------------------------------------------------------------------#
+    #  Properties
+    # -------------------------------------------------------------------------#
+    # fmt: off
+    BusMessage = dict_property(lambda s: s.__data, 0)  # pylint: disable=protected-access
+    BusCommunicationError = dict_property(lambda s: s.__data, 1)  # pylint: disable=protected-access
+    BusExceptionError = dict_property(lambda s: s.__data, 2)  # pylint: disable=protected-access
+    SlaveMessage = dict_property(lambda s: s.__data, 3)  # pylint: disable=protected-access
+    SlaveNoResponse = dict_property(lambda s: s.__data, 4)  # pylint: disable=protected-access
+    SlaveNAK = dict_property(lambda s: s.__data, 5)  # pylint: disable=protected-access
+    SlaveBusy = dict_property(lambda s: s.__data, 6)  # pylint: disable=protected-access
+    BusCharacterOverrun = dict_property(lambda s: s.__data, 7)  # pylint: disable=protected-access
+    Event = dict_property(lambda s: s.__data, 8)  # pylint: disable=protected-access
+    # fmt: on
+
+
+# ---------------------------------------------------------------------------#
+#  Main server control block
+# ---------------------------------------------------------------------------#
+class ModbusControlBlock:
+    """This is a global singleton that controls all system information.
+
+    All activity should be logged here and all diagnostic requests
+    should come from here.
+    """
+
+    _mode = "ASCII"
+    _diagnostic = [False] * 16
+    _listen_only = False
+    _delimiter = b"\r"
+    _counters = ModbusCountersHandler()
+    _identity = ModbusDeviceIdentification()
+    _plus = ModbusPlusStatistics()
+    _events: list[ModbusEvent] = []
+
+    # -------------------------------------------------------------------------#
+    #  Magic
+    # -------------------------------------------------------------------------#
+    def __str__(self):
+        """Build a representation of the control block.
+
+        :returns: A string representation of the control block
+        """
+        return "ModbusControl"
+
+    def __iter__(self):
+        """Iterate over the device counters.
+
+        :returns: An iterator of the device counters
+        """
+        return self._counters.__iter__()
+
+    def __new__(cls):
+        """Create a new instance."""
+        if "_inst" not in vars(cls):
+            cls._inst = object.__new__(cls)
+        return cls._inst
+
+    # -------------------------------------------------------------------------#
+    #  Events
+    # -------------------------------------------------------------------------#
+    def addEvent(self, event: ModbusEvent):
+        """Add a new event to the event log.
+
+        :param event: A new event to add to the log
+        """
+        self._events.insert(0, event)
+        self._events = self._events[0:64]  # chomp to 64 entries
+        self.Counter.Event += 1
+
+    def getEvents(self):
+        """Return an encoded collection of the event log.
+
+        :returns: The encoded events packet
+        """
+        events = [event.encode() for event in self._events]
+        return b"".join(events)
+
+    def clearEvents(self):
+        """Clear the current list of events."""
+        self._events = []
+
+    # -------------------------------------------------------------------------#
+    #  Other Properties
+    # -------------------------------------------------------------------------#
+    Identity = property(lambda s: s._identity)
+    Counter = property(lambda s: s._counters)
+    Events = property(lambda s: s._events)
+    Plus = property(lambda s: s._plus)
+
+    def reset(self):
+        """Clear all of the system counters and the diagnostic register."""
+        self._events = []
+        self._counters.reset()
+        self._diagnostic = [False] * 16
+
+    # -------------------------------------------------------------------------#
+    #  Listen Properties
+    # -------------------------------------------------------------------------#
+    def _setListenOnly(self, value):
+        """Toggle the listen only status.
+
+        :param value: The value to set the listen status to
+        """
+        self._listen_only = bool(value)
+
+    ListenOnly = property(lambda s: s._listen_only, _setListenOnly)
+
+    # -------------------------------------------------------------------------#
+    #  Mode Properties
+    # -------------------------------------------------------------------------#
+    def _setMode(self, mode):
+        """Toggle the current serial mode.
+
+        :param mode: The data transfer method in (RTU, ASCII)
+        """
+        if mode in {"ASCII", "RTU"}:
+            self._mode = mode
+
+    Mode = property(lambda s: s._mode, _setMode)
+
+    # -------------------------------------------------------------------------#
+    #  Delimiter Properties
+    # -------------------------------------------------------------------------#
+    def _setDelimiter(self, char):
+        """Change the serial delimiter character.
+
+        :param char: The new serial delimiter character
+        """
+        if isinstance(char, str):
+            self._delimiter = char.encode()
+        if isinstance(char, bytes):
+            self._delimiter = char
+        elif isinstance(char, int):
+            self._delimiter = struct.pack(">B", char)
+
+    Delimiter = property(lambda s: s._delimiter, _setDelimiter)
+
+    # -------------------------------------------------------------------------#
+    #  Diagnostic Properties
+    # -------------------------------------------------------------------------#
+    def setDiagnostic(self, mapping):
+        """Set the value in the diagnostic register.
+
+        :param mapping: Dictionary of key:value pairs to set
+        """
+        for entry in iter(mapping.items()):
+            if entry[0] >= 0 and entry[0] < len(self._diagnostic):
+                self._diagnostic[entry[0]] = bool(entry[1])
+
+    def getDiagnostic(self, bit):
+        """Get the value in the diagnostic register.
+
+        :param bit: The bit to get
+        :returns: The current value of the requested bit
+        """
+        try:
+            if bit and 0 <= bit < len(self._diagnostic):
+                return self._diagnostic[bit]
+        except Exception:  # pylint: disable=broad-except
+            return None
+        return None
+
+    def getDiagnosticRegister(self):
+        """Get the entire diagnostic register.
+
+        :returns: The diagnostic register collection
+        """
+        return self._diagnostic
diff --git a/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/events.py b/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/events.py
new file mode 100644
index 00000000..aa576a07
--- /dev/null
+++ b/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/events.py
@@ -0,0 +1,201 @@
+"""Modbus Remote Events.
+
+An event byte returned by the Get Communications Event Log function
+can be any one of four types. The type is defined by bit 7
+(the high-order bit) in each byte. It may be further defined by bit 6.
+"""
+# pylint: disable=missing-type-doc
+from abc import ABC, abstractmethod
+
+from pymodbus.exceptions import ParameterException
+from pymodbus.utilities import pack_bitstring, unpack_bitstring
+
+
+class ModbusEvent(ABC):
+    """Define modbus events."""
+
+    @abstractmethod
+    def encode(self) -> bytes:
+        """Encode the status bits to an event message."""
+
+    @abstractmethod
+    def decode(self, event):
+        """Decode the event message to its status bits.
+
+        :param event: The event to decode
+        """
+
+
+class RemoteReceiveEvent(ModbusEvent):
+    """Remote device MODBUS Receive Event.
+
+    The remote device stores this type of event byte when a query message
+    is received. It is stored before the remote device processes the message.
+    This event is defined by bit 7 set to logic "1". The other bits will be
+    set to a logic "1" if the corresponding condition is TRUE. The bit layout
+    is::
+
+        Bit Contents
+        ----------------------------------
+        0   Not Used
+        2   Not Used
+        3   Not Used
+        4   Character Overrun
+        5   Currently in Listen Only Mode
+        6   Broadcast Receive
+        7   1
+    """
+
+    def __init__(self, overrun=False, listen=False, broadcast=False):
+        """Initialize a new event instance."""
+        self.overrun = overrun
+        self.listen = listen
+        self.broadcast = broadcast
+
+    def encode(self) -> bytes:
+        """Encode the status bits to an event message.
+
+        :returns: The encoded event message
+        """
+        bits = [False] * 3
+        bits += [self.overrun, self.listen, self.broadcast, True]
+        packet = pack_bitstring(bits)
+        return packet
+
+    def decode(self, event: bytes) -> None:
+        """Decode the event message to its status bits.
+
+        :param event: The event to decode
+        """
+        bits = unpack_bitstring(event)
+        self.overrun = bits[4]
+        self.listen = bits[5]
+        self.broadcast = bits[6]
+
+
+class RemoteSendEvent(ModbusEvent):
+    """Remote device MODBUS Send Event.
+
+    The remote device stores this type of event byte when it finishes
+    processing a request message. It is stored if the remote device
+    returned a normal or exception response, or no response.
+
+    This event is defined by bit 7 set to a logic "0", with bit 6 set to a "1".
+    The other bits will be set to a logic "1" if the corresponding
+    condition is TRUE. The bit layout is::
+
+        Bit Contents
+        -----------------------------------------------------------
+        0   Read Exception Sent (Exception Codes 1-3)
+        1   Slave Abort Exception Sent (Exception Code 4)
+        2   Slave Busy Exception Sent (Exception Codes 5-6)
+        3   Slave Program NAK Exception Sent (Exception Code 7)
+        4   Write Timeout Error Occurred
+        5   Currently in Listen Only Mode
+        6   1
+        7   0
+    """
+
+    def __init__(self, read=False, slave_abort=False, slave_busy=False, slave_nak=False, write_timeout=False, listen=False):
+        """Initialize a new event instance."""
+        self.read = read
+        self.slave_abort = slave_abort
+        self.slave_busy = slave_busy
+        self.slave_nak = slave_nak
+        self.write_timeout = write_timeout
+        self.listen = listen
+
+    def encode(self):
+        """Encode the status bits to an event message.
+
+        :returns: The encoded event message
+        """
+        bits = [
+            self.read,
+            self.slave_abort,
+            self.slave_busy,
+            self.slave_nak,
+            self.write_timeout,
+            self.listen,
+        ]
+        bits += [True, False]
+        packet = pack_bitstring(bits)
+        return packet
+
+    def decode(self, event):
+        """Decode the event message to its status bits.
+
+        :param event: The event to decode
+        """
+        # todo fix the start byte count # pylint: disable=fixme
+        bits = unpack_bitstring(event)
+        self.read = bits[0]
+        self.slave_abort = bits[1]
+        self.slave_busy = bits[2]
+        self.slave_nak = bits[3]
+        self.write_timeout = bits[4]
+        self.listen = bits[5]
+
+
+class EnteredListenModeEvent(ModbusEvent):
+    """Enter Remote device Listen Only Mode.
+
+    The remote device stores this type of event byte when it enters
+    the Listen Only Mode. The event is defined by a content of 04 hex.
+    """
+
+    value = 0x04
+    __encoded = b"\x04"
+
+    def encode(self):
+        """Encode the status bits to an event message.
+
+        :returns: The encoded event message
+        """
+        return self.__encoded
+
+    def decode(self, event):
+        """Decode the event message to its status bits.
+
+        :param event: The event to decode
+        :raises ParameterException:
+        """
+        if event != self.__encoded:
+            raise ParameterException("Invalid decoded value")
+
+
+class CommunicationRestartEvent(ModbusEvent):
+    """Restart remote device Initiated Communication.
+
+    The remote device stores this type of event byte when its communications
+    port is restarted. The remote device can be restarted by the Diagnostics
+    function (code 08), with sub-function Restart Communications Option
+    (code 00 01).
+
+    That function also places the remote device into a "Continue on Error"
+    or "Stop on Error" mode. If the remote device is placed  into "Continue on
+    Error" mode, the event byte is added to the existing event log. If the
+    remote device is placed into "Stop on Error" mode, the byte is added to
+    the log and the rest of the log is cleared to zeros.
+
+    The event is defined by a content of zero.
+    """
+
+    value = 0x00
+    __encoded = b"\x00"
+
+    def encode(self):
+        """Encode the status bits to an event message.
+
+        :returns: The encoded event message
+        """
+        return self.__encoded
+
+    def decode(self, event):
+        """Decode the event message to its status bits.
+
+        :param event: The event to decode
+        :raises ParameterException:
+        """
+        if event != self.__encoded:
+            raise ParameterException("Invalid decoded value")
diff --git a/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/exceptions.py b/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/exceptions.py
new file mode 100644
index 00000000..b3238c27
--- /dev/null
+++ b/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/exceptions.py
@@ -0,0 +1,116 @@
+"""Pymodbus Exceptions.
+
+Custom exceptions to be used in the Modbus code.
+"""
+
+__all__ = [
+    "ModbusIOException",
+    "ParameterException",
+    "NotImplementedException",
+    "ConnectionException",
+    "NoSuchSlaveException",
+    "InvalidMessageReceivedException",
+    "MessageRegisterException",
+]
+
+
+class ModbusException(Exception):
+    """Base modbus exception."""
+
+    def __init__(self, string):
+        """Initialize the exception.
+
+        :param string: The message to append to the error
+        """
+        self.string = string
+        super().__init__(string)
+
+    def __str__(self):
+        """Return string representation."""
+        return f"Modbus Error: {self.string}"
+
+    def isError(self):
+        """Error"""
+        return True
+
+
+class ModbusIOException(ModbusException):
+    """Error resulting from data i/o."""
+
+    def __init__(self, string="", function_code=None):
+        """Initialize the exception.
+
+        :param string: The message to append to the error
+        """
+        self.fcode = function_code
+        self.message = f"[Input/Output] {string}"
+        ModbusException.__init__(self, self.message)
+
+
+class ParameterException(ModbusException):
+    """Error resulting from invalid parameter."""
+
+    def __init__(self, string=""):
+        """Initialize the exception.
+
+        :param string: The message to append to the error
+        """
+        message = f"[Invalid Parameter] {string}"
+        ModbusException.__init__(self, message)
+
+
+class NoSuchSlaveException(ModbusException):
+    """Error resulting from making a request to a slave that does not exist."""
+
+    def __init__(self, string=""):
+        """Initialize the exception.
+
+        :param string: The message to append to the error
+        """
+        message = f"[No Such Slave] {string}"
+        ModbusException.__init__(self, message)
+
+
+class NotImplementedException(ModbusException):
+    """Error resulting from not implemented function."""
+
+    def __init__(self, string=""):
+        """Initialize the exception.
+
+        :param string: The message to append to the error
+        """
+        message = f"[Not Implemented] {string}"
+        ModbusException.__init__(self, message)
+
+
+class ConnectionException(ModbusException):
+    """Error resulting from a bad connection."""
+
+    def __init__(self, string=""):
+        """Initialize the exception.
+
+        :param string: The message to append to the error
+        """
+        message = f"[Connection] {string}"
+        ModbusException.__init__(self, message)
+
+
+class InvalidMessageReceivedException(ModbusException):
+    """Error resulting from invalid response received or decoded."""
+
+    def __init__(self, string=""):
+        """Initialize the exception.
+
+        :param string: The message to append to the error
+        """
+        message = f"[Invalid Message] {string}"
+        ModbusException.__init__(self, message)
+
+
+class MessageRegisterException(ModbusException):
+    """Error resulting from failing to register a custom message request/response."""
+
+    def __init__(self, string=""):
+        """Initialize."""
+        message = f"[Error registering message] {string}"
+        ModbusException.__init__(self, message)
diff --git a/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/framer/__init__.py b/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/framer/__init__.py
new file mode 100644
index 00000000..aa40ccc9
--- /dev/null
+++ b/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/framer/__init__.py
@@ -0,0 +1,23 @@
+"""Framer."""
+__all__ = [
+    "FramerBase",
+    "FramerType",
+    "FramerAscii",
+    "FramerRTU",
+    "FramerSocket",
+    "FramerTLS"
+]
+
+from pymodbus.framer.ascii import FramerAscii
+from pymodbus.framer.base import FramerBase, FramerType
+from pymodbus.framer.rtu import FramerRTU
+from pymodbus.framer.socket import FramerSocket
+from pymodbus.framer.tls import FramerTLS
+
+
+FRAMER_NAME_TO_CLASS = {
+    FramerType.ASCII: FramerAscii,
+    FramerType.RTU: FramerRTU,
+    FramerType.SOCKET: FramerSocket,
+    FramerType.TLS: FramerTLS,
+}
diff --git a/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/framer/ascii.py b/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/framer/ascii.py
new file mode 100644
index 00000000..cc8a2f3a
--- /dev/null
+++ b/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/framer/ascii.py
@@ -0,0 +1,86 @@
+"""ModbusMessage layer.
+
+is extending ModbusProtocol to handle receiving and sending of messsagees.
+
+ModbusMessage provides a unified interface to send/receive Modbus requests/responses.
+"""
+from __future__ import annotations
+
+from binascii import a2b_hex, b2a_hex
+
+from pymodbus.framer.base import FramerBase
+from pymodbus.logging import Log
+
+
+class FramerAscii(FramerBase):
+    r"""Modbus ASCII Frame Controller.
+
+    Layout::
+        [ Start ][ Dev id ][ Function ][ Data ][ LRC ][ End ]
+          1c       2c        2c          N*2c    1c     2c
+
+        * data can be 1 - 2x252 chars
+        * end is "\\r\\n" (Carriage return line feed), however the line feed
+          character can be changed via a special command
+        * start is ":"
+
+    This framer is used for serial transmission.  Unlike the RTU protocol,
+    the data in this framer is transferred in plain text ascii.
+    """
+
+    START = b':'
+    END = b'\r\n'
+    MIN_SIZE = 10
+
+
+    def decode(self, data: bytes) -> tuple[int, int, int, bytes]:
+        """Decode ADU."""
+        used_len = 0
+        data_len = len(data)
+        while True:
+            if data_len - used_len < self.MIN_SIZE:
+                Log.debug("Short frame: {} wait for more data", data, ":hex")
+                return used_len, 0, 0, self.EMPTY
+            buffer = data[used_len:]
+            if buffer[0:1] != self.START:
+                if (i := buffer.find(self.START)) == -1:
+                    Log.debug("No frame start in data: {}, wait for data", data, ":hex")
+                    return data_len, 0, 0, self.EMPTY
+                used_len += i
+                continue
+            if (end := buffer.find(self.END)) == -1:
+                Log.debug("Incomplete frame: {} wait for more data", data, ":hex")
+                return used_len, 0, 0, self.EMPTY
+            dev_id = int(buffer[1:3], 16)
+            lrc = int(buffer[end - 2: end], 16)
+            msg = a2b_hex(buffer[1 : end - 2])
+            used_len += end + 2
+            if not self.check_LRC(msg, lrc):
+                Log.debug("LRC wrong in frame: {} skipping", data, ":hex")
+                continue
+            return used_len, dev_id, 0, msg[1:]
+
+    def encode(self, data: bytes, device_id: int, _tid: int) -> bytes:
+        """Encode ADU."""
+        dev_id = device_id.to_bytes(1,'big')
+        checksum = self.compute_LRC(dev_id + data)
+        frame = (
+            self.START +
+            f"{device_id:02x}".encode() +
+            b2a_hex(data) +
+            f"{checksum:02x}".encode() +
+            self.END
+        ).upper()
+        return frame
+
+    @classmethod
+    def compute_LRC(cls, data: bytes) -> int:
+        """Use to compute the longitudinal redundancy check against a string."""
+        lrc = sum(int(a) for a in data) & 0xFF
+        lrc = (lrc ^ 0xFF) + 1
+        return lrc & 0xFF
+
+    @classmethod
+    def check_LRC(cls, data: bytes, check: int) -> bool:
+        """Check if the passed in data matches the LRC."""
+        return cls.compute_LRC(data) == check
diff --git a/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/framer/base.py b/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/framer/base.py
new file mode 100644
index 00000000..ffdbf52c
--- /dev/null
+++ b/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/framer/base.py
@@ -0,0 +1,103 @@
+"""Framer implementations.
+
+The implementation is responsible for encoding/decoding requests/responses.
+
+According to the selected type of modbus frame a prefix/suffix is added/removed
+"""
+from __future__ import annotations
+
+from enum import Enum
+
+from pymodbus.exceptions import ModbusIOException
+from pymodbus.logging import Log
+from pymodbus.pdu import DecodePDU, ModbusPDU
+
+
+class FramerType(str, Enum):
+    """Type of Modbus frame."""
+
+    ASCII = "ascii"
+    RTU = "rtu"
+    SOCKET = "socket"
+    TLS = "tls"
+
+
+class FramerBase:
+    """Intern base."""
+
+    EMPTY = b''
+    MIN_SIZE = 0
+
+    def __init__(
+        self,
+        decoder: DecodePDU,
+    ) -> None:
+        """Initialize a ADU (framer) instance."""
+        self.decoder = decoder
+        self.databuffer = b""
+
+    def decode(self, _data: bytes) -> tuple[int, int, int, bytes]:
+        """Decode ADU.
+
+        returns:
+            used_len (int) or 0 to read more
+            dev_id,
+            tid,
+            modbus request/response (bytes)
+        """
+        return 0, 0, 0, self.EMPTY
+
+    def encode(self, data: bytes, _dev_id: int, _tid: int) -> bytes:
+        """Encode ADU.
+
+        returns:
+            modbus ADU (bytes)
+        """
+        return data
+
+    def buildFrame(self, message: ModbusPDU) -> bytes:
+        """Create a ready to send modbus packet.
+
+        :param message: The populated request/response to send
+        """
+        data = message.function_code.to_bytes(1,'big') + message.encode()
+        frame = self.encode(data, message.slave_id, message.transaction_id)
+        return frame
+
+    def processIncomingFrame(self, data: bytes) -> tuple[int, ModbusPDU | None]:
+        """Process new packet pattern.
+
+        This takes in a new request packet, adds it to the current
+        packet stream, and performs framing on it. That is, checks
+        for complete messages, and once found, will process all that
+        exist.
+        """
+        used_len = 0
+        while True:
+            data_len, pdu = self._processIncomingFrame(data[used_len:])
+            used_len += data_len
+            if not data_len:
+                return used_len, None
+            if pdu:
+                return used_len, pdu
+
+    def _processIncomingFrame(self, data: bytes) -> tuple[int, ModbusPDU | None]:
+        """Process new packet pattern.
+
+        This takes in a new request packet, adds it to the current
+        packet stream, and performs framing on it. That is, checks
+        for complete messages, and once found, will process all that
+        exist.
+        """
+        Log.debug("Processing: {}", data, ":hex")
+        if not data:
+            return 0, None
+        used_len, dev_id, tid, frame_data = self.decode(data)
+        if not frame_data:
+            return used_len, None
+        if (result := self.decoder.decode(frame_data)) is None:
+            raise ModbusIOException("Unable to decode request")
+        result.slave_id = dev_id
+        result.transaction_id = tid
+        Log.debug("Frame advanced, resetting header!!")
+        return used_len, result
diff --git a/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/framer/rtu.py b/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/framer/rtu.py
new file mode 100644
index 00000000..76fe0767
--- /dev/null
+++ b/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/framer/rtu.py
@@ -0,0 +1,159 @@
+"""Modbus RTU frame implementation."""
+from __future__ import annotations
+
+from pymodbus.framer.base import FramerBase
+from pymodbus.logging import Log
+
+
+class FramerRTU(FramerBase):
+    """Modbus RTU frame type.
+
+    Layout::
+
+        [ Start Wait ] [Address ][ Function Code] [ Data ][ CRC ]
+          3.5 chars     1b         1b               Nb      2b
+
+    .. note::
+
+            due to the USB converter and the OS drivers, timing cannot be quaranteed
+            neither when receiving nor when sending.
+
+    Decoding is a complicated process because the RTU frame does not have a fixed prefix
+    only suffix, therefore it is necessary to decode the content (PDU) to get length etc.
+    There are some protocol restrictions that help with the detection.
+
+    For client:
+       - a request causes 1 response !
+       - Multiple requests are NOT allowed (master-slave protocol)
+       - the server will not retransmit responses
+
+    this means decoding is always exactly 1 frame (response)
+
+    For server (Single device)
+       - only 1 request allowed (master-slave) protocol
+       - the client (master) may retransmit but in larger time intervals
+
+    this means decoding is always exactly 1 frame (request)
+
+    For server (Multidrop line --> devices in parallel)
+       - only 1 request allowed (master-slave) protocol
+       - other devices will send responses
+       - the client (master) may retransmit but in larger time intervals
+
+    this means decoding is always exactly 1 frame request, however some requests
+    will be for unknown slaves, which must be ignored together with the
+    response from the unknown slave.
+
+    Recovery from bad cabling and unstable USB etc is important,
+    the following scenarios is possible:
+
+        - garble data before frame
+        - garble data in frame
+        - garble data after frame
+        - data in frame garbled (wrong CRC)
+
+    decoding assumes the frame is sound, and if not enters a hunting mode.
+
+    The 3.5 byte transmission time at the slowest speed 1.200Bps is 31ms.
+    Device drivers will typically flush buffer after 10ms of silence.
+    If no data is received for 50ms the transmission / frame can be considered
+    complete.
+
+    The following table is a listing of the baud wait times for the specified
+    baud rates::
+
+        ------------------------------------------------------------------
+         Baud  1.5c (18 bits)   3.5c (38 bits)
+        ------------------------------------------------------------------
+         1200   13333.3 us       31666.7 us
+         4800    3333.3 us        7916.7 us
+         9600    1666.7 us        3958.3 us
+        19200     833.3 us        1979.2 us
+        38400     416.7 us         989.6 us
+        ------------------------------------------------------------------
+        1 Byte = start + 8 bits + parity + stop = 11 bits
+        (1/Baud)(bits) = delay seconds
+    """
+
+    MIN_SIZE = 4  # <slave id><function code><crc 2 bytes>
+
+    @classmethod
+    def generate_crc16_table(cls) -> list[int]:
+        """Generate a crc16 lookup table.
+
+        .. note:: This will only be generated once
+        """
+        result = []
+        for byte in range(256):
+            crc = 0x0000
+            for _ in range(8):
+                if (byte ^ crc) & 0x0001:
+                    crc = (crc >> 1) ^ 0xA001
+                else:
+                    crc >>= 1
+                byte >>= 1
+            result.append(crc)
+        return result
+    crc16_table: list[int] = [0]
+
+
+    def decode(self, data: bytes) -> tuple[int, int, int, bytes]:
+        """Decode ADU."""
+        data_len = len(data)
+        for used_len in range(data_len):
+            if data_len - used_len < self.MIN_SIZE:
+                Log.debug("Short frame: {} wait for more data", data, ":hex")
+                return used_len, 0, 0, self.EMPTY
+            dev_id = int(data[used_len])
+            func_code = int(data[used_len + 1])
+            if func_code & 0x7F not in self.decoder.lookup:
+                continue
+            pdu_class = self.decoder.lookupPduClass(func_code)
+            if not (size := pdu_class.calculateRtuFrameSize(data[used_len:])):
+                size = data_len +1
+            if data_len < used_len +size:
+                Log.debug("Frame - not ready")
+                return used_len, dev_id, 0, self.EMPTY
+            start_crc = used_len + size -2
+            crc = data[start_crc : start_crc + 2]
+            crc_val = (int(crc[0]) << 8) + int(crc[1])
+            if not FramerRTU.check_CRC(data[used_len : start_crc], crc_val):
+                Log.debug("Frame check failed, ignoring!!")
+                continue
+            return start_crc + 2, dev_id, 0, data[used_len + 1 : start_crc]
+        return 0, 0, 0, self.EMPTY
+
+
+    def encode(self, pdu: bytes, device_id: int, _tid: int) -> bytes:
+        """Encode ADU."""
+        frame = device_id.to_bytes(1,'big') + pdu
+        return frame + FramerRTU.compute_CRC(frame).to_bytes(2,'big')
+
+    @classmethod
+    def check_CRC(cls, data: bytes, check: int) -> bool:
+        """Check if the data matches the passed in CRC.
+
+        :param data: The data to create a crc16 of
+        :param check: The CRC to validate
+        :returns: True if matched, False otherwise
+        """
+        return cls.compute_CRC(data) == check
+
+    @classmethod
+    def compute_CRC(cls, data: bytes) -> int:
+        """Compute a crc16 on the passed in bytes.
+
+        The difference between modbus's crc16 and a normal crc16
+        is that modbus starts the crc value out at 0xffff.
+
+        :param data: The data to create a crc16 of
+        :returns: The calculated CRC
+        """
+        crc = 0xFFFF
+        for data_byte in data:
+            idx = cls.crc16_table[(crc ^ int(data_byte)) & 0xFF]
+            crc = ((crc >> 8) & 0xFF) ^ idx
+        swapped = ((crc << 8) & 0xFF00) | ((crc >> 8) & 0x00FF)
+        return swapped
+
+FramerRTU.crc16_table = FramerRTU.generate_crc16_table()
diff --git a/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/framer/socket.py b/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/framer/socket.py
new file mode 100644
index 00000000..eb261768
--- /dev/null
+++ b/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/framer/socket.py
@@ -0,0 +1,46 @@
+"""Modbus Socket frame implementation."""
+from __future__ import annotations
+
+from pymodbus.framer.base import FramerBase
+from pymodbus.logging import Log
+
+
+class FramerSocket(FramerBase):
+    """Modbus Socket frame type.
+
+    Layout::
+
+        [         MBAP Header         ] [ Function Code] [ Data ]
+        [ tid ][ pid ][ length ][ uid ]
+          2b     2b     2b        1b           1b           Nb
+
+    length = uid + function code + data
+    """
+
+    MIN_SIZE = 8
+
+    def decode(self, data: bytes) -> tuple[int, int, int, bytes]:
+        """Decode ADU."""
+        if (data_len := len(data)) < self.MIN_SIZE:
+          Log.debug("Very short frame (NO MBAP): {} wait for more data", data, ":hex")
+          return 0, 0, 0, self.EMPTY
+        tid = int.from_bytes(data[0:2], 'big')
+        msg_len = int.from_bytes(data[4:6], 'big') + 6
+        dev_id = int(data[6])
+        if data_len < msg_len:
+          Log.debug("Short frame: {} wait for more data", data, ":hex")
+          return 0, 0, 0, self.EMPTY
+        if msg_len == 8 and data_len == 9:
+            msg_len = 9
+        return msg_len, dev_id, tid, data[7:msg_len]
+
+    def encode(self, pdu: bytes, device_id: int, tid: int) -> bytes:
+        """Encode ADU."""
+        frame = (
+           tid.to_bytes(2, 'big') +
+           b'\x00\x00' +
+           (len(pdu) + 1).to_bytes(2, 'big') +
+           device_id.to_bytes(1, 'big') +
+           pdu
+        )
+        return frame
diff --git a/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/framer/tls.py b/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/framer/tls.py
new file mode 100644
index 00000000..dd10f1f6
--- /dev/null
+++ b/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/framer/tls.py
@@ -0,0 +1,21 @@
+"""Modbus TLS frame implementation."""
+from __future__ import annotations
+
+from pymodbus.framer.base import FramerBase
+
+
+class FramerTLS(FramerBase):
+    """Modbus TLS frame type.
+
+    Layout::
+      [ Function Code] [ Data ]
+        1b               Nb
+    """
+
+    def decode(self, data: bytes) -> tuple[int, int, int, bytes]:
+        """Decode ADU."""
+        return len(data), 0, 0, data
+
+    def encode(self, pdu: bytes, _device_id: int, _tid: int) -> bytes:
+        """Encode ADU."""
+        return pdu
diff --git a/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/logging.py b/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/logging.py
new file mode 100644
index 00000000..764eab21
--- /dev/null
+++ b/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/logging.py
@@ -0,0 +1,121 @@
+"""Pymodbus: Modbus Protocol Implementation.
+
+Released under the BSD license
+"""
+from __future__ import annotations
+
+import logging
+from binascii import b2a_hex
+from logging import NullHandler as __null
+
+from pymodbus.utilities import hexlify_packets
+
+
+# ---------------------------------------------------------------------------#
+#  Block unhandled logging
+# ---------------------------------------------------------------------------#
+logging.getLogger("pymodbus_internal").addHandler(__null())
+
+
+def pymodbus_apply_logging_config(
+    level: str | int = logging.DEBUG, log_file_name: str | None = None
+):
+    """Apply basic logging configuration used by default by Pymodbus maintainers.
+
+    :param level: (optional) set log level, if not set it is inherited.
+    :param log_file_name: (optional) log additional to file
+
+    Please call this function to format logging appropriately when opening issues.
+    """
+    if isinstance(level, str):
+        level = level.upper()
+    Log.apply_logging_config(level, log_file_name)
+
+
+class Log:
+    """Class to hide logging complexity.
+
+    :meta private:
+    """
+
+    _logger = logging.getLogger(__name__)
+
+    @classmethod
+    def apply_logging_config(cls, level, log_file_name):
+        """Apply basic logging configuration."""
+        if level == logging.NOTSET:
+            level = cls._logger.getEffectiveLevel()
+        if isinstance(level, str):
+            level = level.upper()
+        log_stream_handler = logging.StreamHandler()
+        log_formatter = logging.Formatter(
+            "%(asctime)s %(levelname)-5s %(module)s:%(lineno)s %(message)s"
+        )
+        log_stream_handler.setFormatter(log_formatter)
+        cls._logger.addHandler(log_stream_handler)
+        if log_file_name:
+            log_file_handler = logging.FileHandler(log_file_name)
+            log_file_handler.setFormatter(log_formatter)
+            cls._logger.addHandler(log_file_handler)
+        cls.setLevel(level)
+
+    @classmethod
+    def setLevel(cls, level):
+        """Apply basic logging level."""
+        cls._logger.setLevel(level)
+
+    @classmethod
+    def build_msg(cls, txt, *args):
+        """Build message."""
+        string_args = []
+        count_args = len(args) - 1
+        skip = False
+        for i in range(count_args + 1):
+            if skip:
+                skip = False
+                continue
+            if (
+                i < count_args
+                and isinstance(args[i + 1], str)
+                and args[i + 1][0] == ":"
+            ):
+                if args[i + 1] == ":hex":
+                    string_args.append(hexlify_packets(args[i]))
+                elif args[i + 1] == ":str":
+                    string_args.append(str(args[i]))
+                elif args[i + 1] == ":b2a":
+                    string_args.append(b2a_hex(args[i]))
+                skip = True
+            else:
+                string_args.append(args[i])
+        return txt.format(*string_args)
+
+    @classmethod
+    def info(cls, txt, *args):
+        """Log info messages."""
+        if cls._logger.isEnabledFor(logging.INFO):
+            cls._logger.info(cls.build_msg(txt, *args), stacklevel=2)
+
+    @classmethod
+    def debug(cls, txt, *args):
+        """Log debug messages."""
+        if cls._logger.isEnabledFor(logging.DEBUG):
+            cls._logger.debug(cls.build_msg(txt, *args), stacklevel=2)
+
+    @classmethod
+    def warning(cls, txt, *args):
+        """Log warning messages."""
+        if cls._logger.isEnabledFor(logging.WARNING):
+            cls._logger.warning(cls.build_msg(txt, *args), stacklevel=2)
+
+    @classmethod
+    def error(cls, txt, *args):
+        """Log error messages."""
+        if cls._logger.isEnabledFor(logging.ERROR):
+            cls._logger.error(cls.build_msg(txt, *args), stacklevel=2)
+
+    @classmethod
+    def critical(cls, txt, *args):
+        """Log critical messages."""
+        if cls._logger.isEnabledFor(logging.CRITICAL):
+            cls._logger.critical(cls.build_msg(txt, *args), stacklevel=2)
diff --git a/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/payload.py b/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/payload.py
new file mode 100644
index 00000000..b6ec8e1c
--- /dev/null
+++ b/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/payload.py
@@ -0,0 +1,452 @@
+"""Modbus Payload Builders.
+
+A collection of utilities for building and decoding
+modbus messages payloads.
+"""
+from __future__ import annotations
+
+
+__all__ = [
+    "BinaryPayloadBuilder",
+    "BinaryPayloadDecoder",
+]
+
+from array import array
+
+# pylint: disable=missing-type-doc
+from struct import pack, unpack
+
+from pymodbus.constants import Endian
+from pymodbus.exceptions import ParameterException
+from pymodbus.logging import Log
+from pymodbus.utilities import (
+    pack_bitstring,
+    unpack_bitstring,
+)
+
+
+class BinaryPayloadBuilder:
+    """A utility that helps build payload messages to be written with the various modbus messages.
+
+    It really is just a simple wrapper around the struct module,
+    however it saves time looking up the format strings.
+    What follows is a simple example::
+
+        builder = BinaryPayloadBuilder(byteorder=Endian.Little)
+        builder.add_8bit_uint(1)
+        builder.add_16bit_uint(2)
+        payload = builder.build()
+    """
+
+    def __init__(
+        self, payload=None, byteorder=Endian.LITTLE, wordorder=Endian.BIG, repack=False
+    ):
+        """Initialize a new instance of the payload builder.
+
+        :param payload: Raw binary payload data to initialize with
+        :param byteorder: The endianness of the bytes in the words
+        :param wordorder: The endianness of the word (when wordcount is >= 2)
+        :param repack: Repack the provided payload based on BO
+        """
+        self._payload = payload or []
+        self._byteorder = byteorder
+        self._wordorder = wordorder
+        self._repack = repack
+
+    def _pack_words(self, fstring: str, value) -> bytes:
+        """Pack words based on the word order and byte order.
+
+        # ---------------------------------------------- #
+        # pack in to network ordered value               #
+        # unpack in to network ordered  unsigned integer #
+        # Change Word order if little endian word order  #
+        # Pack values back based on correct byte order   #
+        # ---------------------------------------------- #
+
+        :param fstring:
+        :param value: Value to be packed
+        :return:
+        """
+        value = pack(f"!{fstring}", value)
+        if Endian.LITTLE in {self._byteorder, self._wordorder}:
+            value = array("H", value)
+            if self._byteorder == Endian.LITTLE:
+                value.byteswap()
+            if self._wordorder == Endian.LITTLE:
+                value.reverse()
+            value = value.tobytes()
+        return value
+
+    def encode(self) -> bytes:
+        """Get the payload buffer encoded in bytes."""
+        return b"".join(self._payload)
+
+    def __str__(self) -> str:
+        """Return the payload buffer as a string.
+
+        :returns: The payload buffer as a string
+        """
+        return self.encode().decode("utf-8")
+
+    def reset(self) -> None:
+        """Reset the payload buffer."""
+        self._payload = []
+
+    def to_registers(self):
+        """Convert the payload buffer to register layout that can be used as a context block.
+
+        :returns: The register layout to use as a block
+        """
+        # fstring = self._byteorder+"H"
+        fstring = "!H"
+        payload = self.build()
+        if self._repack:
+            payload = [unpack(self._byteorder + "H", value)[0] for value in payload]
+        else:
+            payload = [unpack(fstring, value)[0] for value in payload]
+        Log.debug("{}", payload)
+        return payload
+
+    def to_coils(self) -> list[bool]:
+        """Convert the payload buffer into a coil layout that can be used as a context block.
+
+        :returns: The coil layout to use as a block
+        """
+        payload = self.to_registers()
+        coils = [bool(int(bit)) for reg in payload for bit in format(reg, "016b")]
+        return coils
+
+    def build(self) -> list[bytes]:
+        """Return the payload buffer as a list.
+
+        This list is two bytes per element and can
+        thus be treated as a list of registers.
+
+        :returns: The payload buffer as a list
+        """
+        buffer = self.encode()
+        length = len(buffer)
+        buffer += b"\x00" * (length % 2)
+        return [buffer[i : i + 2] for i in range(0, length, 2)]
+
+    def add_bits(self, values: list[bool]) -> None:
+        """Add a collection of bits to be encoded.
+
+        If these are less than a multiple of eight,
+        they will be left padded with 0 bits to make
+        it so.
+
+        :param values: The value to add to the buffer
+        """
+        value = pack_bitstring(values)
+        self._payload.append(value)
+
+    def add_8bit_uint(self, value: int) -> None:
+        """Add a 8 bit unsigned int to the buffer.
+
+        :param value: The value to add to the buffer
+        """
+        fstring = self._byteorder + "B"
+        self._payload.append(pack(fstring, value))
+
+    def add_16bit_uint(self, value: int) -> None:
+        """Add a 16 bit unsigned int to the buffer.
+
+        :param value: The value to add to the buffer
+        """
+        fstring = self._byteorder + "H"
+        self._payload.append(pack(fstring, value))
+
+    def add_32bit_uint(self, value: int) -> None:
+        """Add a 32 bit unsigned int to the buffer.
+
+        :param value: The value to add to the buffer
+        """
+        fstring = "I"
+        # fstring = self._byteorder + "I"
+        p_string = self._pack_words(fstring, value)
+        self._payload.append(p_string)
+
+    def add_64bit_uint(self, value: int) -> None:
+        """Add a 64 bit unsigned int to the buffer.
+
+        :param value: The value to add to the buffer
+        """
+        fstring = "Q"
+        p_string = self._pack_words(fstring, value)
+        self._payload.append(p_string)
+
+    def add_8bit_int(self, value: int) -> None:
+        """Add a 8 bit signed int to the buffer.
+
+        :param value: The value to add to the buffer
+        """
+        fstring = self._byteorder + "b"
+        self._payload.append(pack(fstring, value))
+
+    def add_16bit_int(self, value: int) -> None:
+        """Add a 16 bit signed int to the buffer.
+
+        :param value: The value to add to the buffer
+        """
+        fstring = self._byteorder + "h"
+        self._payload.append(pack(fstring, value))
+
+    def add_32bit_int(self, value: int) -> None:
+        """Add a 32 bit signed int to the buffer.
+
+        :param value: The value to add to the buffer
+        """
+        fstring = "i"
+        p_string = self._pack_words(fstring, value)
+        self._payload.append(p_string)
+
+    def add_64bit_int(self, value: int) -> None:
+        """Add a 64 bit signed int to the buffer.
+
+        :param value: The value to add to the buffer
+        """
+        fstring = "q"
+        p_string = self._pack_words(fstring, value)
+        self._payload.append(p_string)
+
+    def add_16bit_float(self, value: float) -> None:
+        """Add a 16 bit float to the buffer.
+
+        :param value: The value to add to the buffer
+        """
+        fstring = "e"
+        p_string = self._pack_words(fstring, value)
+        self._payload.append(p_string)
+
+    def add_32bit_float(self, value: float) -> None:
+        """Add a 32 bit float to the buffer.
+
+        :param value: The value to add to the buffer
+        """
+        fstring = "f"
+        p_string = self._pack_words(fstring, value)
+        self._payload.append(p_string)
+
+    def add_64bit_float(self, value: float) -> None:
+        """Add a 64 bit float(double) to the buffer.
+
+        :param value: The value to add to the buffer
+        """
+        fstring = "d"
+        p_string = self._pack_words(fstring, value)
+        self._payload.append(p_string)
+
+    def add_string(self, value: str) -> None:
+        """Add a string to the buffer.
+
+        :param value: The value to add to the buffer
+        """
+        fstring = self._byteorder + str(len(value)) + "s"
+        self._payload.append(pack(fstring, value.encode()))
+
+
+class BinaryPayloadDecoder:
+    """A utility that helps decode payload messages from a modbus response message.
+
+    It really is just a simple wrapper around
+    the struct module, however it saves time looking up the format
+    strings. What follows is a simple example::
+
+        decoder = BinaryPayloadDecoder(payload)
+        first   = decoder.decode_8bit_uint()
+        second  = decoder.decode_16bit_uint()
+    """
+
+    def __init__(self, payload, byteorder=Endian.LITTLE, wordorder=Endian.BIG):
+        """Initialize a new payload decoder.
+
+        :param payload: The payload to decode with
+        :param byteorder: The endianness of the payload
+        :param wordorder: The endianness of the word (when wordcount is >= 2)
+        """
+        self._payload = payload
+        self._pointer = 0x00
+        self._byteorder = byteorder
+        self._wordorder = wordorder
+
+    @classmethod
+    def fromRegisters(
+        cls,
+        registers,
+        byteorder=Endian.LITTLE,
+        wordorder=Endian.BIG,
+    ):
+        """Initialize a payload decoder.
+
+        With the result of reading a collection of registers from a modbus device.
+
+        The registers are treated as a list of 2 byte values.
+        We have to do this because of how the data has already
+        been decoded by the rest of the library.
+
+        :param registers: The register results to initialize with
+        :param byteorder: The Byte order of each word
+        :param wordorder: The endianness of the word (when wordcount is >= 2)
+        :returns: An initialized PayloadDecoder
+        :raises ParameterException:
+        """
+        Log.debug("{}", registers)
+        if isinstance(registers, list):  # repack into flat binary
+            payload = pack(f"!{len(registers)}H", *registers)
+            return cls(payload, byteorder, wordorder)
+        raise ParameterException("Invalid collection of registers supplied")
+
+    @classmethod
+    def bit_chunks(cls, coils, size=8):
+        """Return bit chunks."""
+        chunks = [coils[i : i + size] for i in range(0, len(coils), size)]
+        return chunks
+
+    @classmethod
+    def fromCoils(
+        cls,
+        coils,
+        byteorder=Endian.LITTLE,
+        _wordorder=Endian.BIG,
+    ):
+        """Initialize a payload decoder with the result of reading of coils."""
+        if isinstance(coils, list):
+            payload = b""
+            if padding := len(coils) % 8:  # Pad zeros
+                extra = [False] * padding
+                coils = extra + coils
+            chunks = cls.bit_chunks(coils)
+            for chunk in chunks:
+                payload += pack_bitstring(chunk[::-1])
+            return cls(payload, byteorder)
+        raise ParameterException("Invalid collection of coils supplied")
+
+    def _unpack_words(self, handle) -> bytes:
+        """Unpack words based on the word order and byte order.
+
+        # ---------------------------------------------- #
+        # Unpack in to network ordered unsigned integer  #
+        # Change Word order if little endian word order  #
+        # Pack values back based on correct byte order   #
+        # ---------------------------------------------- #
+        """
+        if Endian.LITTLE in {self._byteorder, self._wordorder}:
+            handle = array("H", handle)
+            if self._byteorder == Endian.LITTLE:
+                handle.byteswap()
+            if self._wordorder == Endian.LITTLE:
+                handle.reverse()
+            handle = handle.tobytes()
+        Log.debug("handle: {}", handle)
+        return handle
+
+    def reset(self):
+        """Reset the decoder pointer back to the start."""
+        self._pointer = 0x00
+
+    def decode_8bit_uint(self):
+        """Decode a 8 bit unsigned int from the buffer."""
+        self._pointer += 1
+        fstring = self._byteorder + "B"
+        handle = self._payload[self._pointer - 1 : self._pointer]
+        return unpack(fstring, handle)[0]
+
+    def decode_bits(self, package_len=1):
+        """Decode a byte worth of bits from the buffer."""
+        self._pointer += package_len
+        # fstring = self._endian + "B"
+        handle = self._payload[self._pointer - 1 : self._pointer]
+        return unpack_bitstring(handle)
+
+    def decode_16bit_uint(self):
+        """Decode a 16 bit unsigned int from the buffer."""
+        self._pointer += 2
+        fstring = self._byteorder + "H"
+        handle = self._payload[self._pointer - 2 : self._pointer]
+        return unpack(fstring, handle)[0]
+
+    def decode_32bit_uint(self):
+        """Decode a 32 bit unsigned int from the buffer."""
+        self._pointer += 4
+        fstring = "I"
+        handle = self._payload[self._pointer - 4 : self._pointer]
+        handle = self._unpack_words(handle)
+        return unpack("!" + fstring, handle)[0]
+
+    def decode_64bit_uint(self):
+        """Decode a 64 bit unsigned int from the buffer."""
+        self._pointer += 8
+        fstring = "Q"
+        handle = self._payload[self._pointer - 8 : self._pointer]
+        handle = self._unpack_words(handle)
+        return unpack("!" + fstring, handle)[0]
+
+    def decode_8bit_int(self):
+        """Decode a 8 bit signed int from the buffer."""
+        self._pointer += 1
+        fstring = self._byteorder + "b"
+        handle = self._payload[self._pointer - 1 : self._pointer]
+        return unpack(fstring, handle)[0]
+
+    def decode_16bit_int(self):
+        """Decode a 16 bit signed int from the buffer."""
+        self._pointer += 2
+        fstring = self._byteorder + "h"
+        handle = self._payload[self._pointer - 2 : self._pointer]
+        return unpack(fstring, handle)[0]
+
+    def decode_32bit_int(self):
+        """Decode a 32 bit signed int from the buffer."""
+        self._pointer += 4
+        fstring = "i"
+        handle = self._payload[self._pointer - 4 : self._pointer]
+        handle = self._unpack_words(handle)
+        return unpack("!" + fstring, handle)[0]
+
+    def decode_64bit_int(self):
+        """Decode a 64 bit signed int from the buffer."""
+        self._pointer += 8
+        fstring = "q"
+        handle = self._payload[self._pointer - 8 : self._pointer]
+        handle = self._unpack_words(handle)
+        return unpack("!" + fstring, handle)[0]
+
+    def decode_16bit_float(self):
+        """Decode a 16 bit float from the buffer."""
+        self._pointer += 2
+        fstring = "e"
+        handle = self._payload[self._pointer - 2 : self._pointer]
+        handle = self._unpack_words(handle)
+        return unpack("!" + fstring, handle)[0]
+
+    def decode_32bit_float(self):
+        """Decode a 32 bit float from the buffer."""
+        self._pointer += 4
+        fstring = "f"
+        handle = self._payload[self._pointer - 4 : self._pointer]
+        handle = self._unpack_words(handle)
+        return unpack("!" + fstring, handle)[0]
+
+    def decode_64bit_float(self):
+        """Decode a 64 bit float(double) from the buffer."""
+        self._pointer += 8
+        fstring = "d"
+        handle = self._payload[self._pointer - 8 : self._pointer]
+        handle = self._unpack_words(handle)
+        return unpack("!" + fstring, handle)[0]
+
+    def decode_string(self, size=1):
+        """Decode a string from the buffer.
+
+        :param size: The size of the string to decode
+        """
+        self._pointer += size
+        return self._payload[self._pointer - size : self._pointer]
+
+    def skip_bytes(self, nbytes):
+        """Skip n bytes in the buffer.
+
+        :param nbytes: The number of bytes to skip
+        """
+        self._pointer += nbytes
diff --git a/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/pdu/__init__.py b/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/pdu/__init__.py
new file mode 100644
index 00000000..60ba42e5
--- /dev/null
+++ b/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/pdu/__init__.py
@@ -0,0 +1,11 @@
+"""Framer."""
+__all__ = [
+    "DecodePDU",
+    "ExceptionResponse",
+    "ExceptionResponse",
+    "ModbusExceptions",
+    "ModbusPDU",
+]
+
+from pymodbus.pdu.decoders import DecodePDU
+from pymodbus.pdu.pdu import ExceptionResponse, ModbusExceptions, ModbusPDU
diff --git a/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/pdu/bit_read_message.py b/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/pdu/bit_read_message.py
new file mode 100644
index 00000000..e37a6985
--- /dev/null
+++ b/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/pdu/bit_read_message.py
@@ -0,0 +1,258 @@
+"""Bit Reading Request/Response messages."""
+
+# pylint: disable=missing-type-doc
+import struct
+
+from pymodbus.pdu.pdu import ModbusExceptions as merror
+from pymodbus.pdu.pdu import ModbusPDU
+from pymodbus.utilities import pack_bitstring, unpack_bitstring
+
+
+class ReadBitsRequestBase(ModbusPDU):
+    """Base class for Messages Requesting bit values."""
+
+    _rtu_frame_size = 8
+
+    def __init__(self, address, count, slave, transaction, skip_encode):
+        """Initialize the read request data.
+
+        :param address: The start address to read from
+        :param count: The number of bits after "address" to read
+        :param slave: Modbus slave slave ID
+        """
+        super().__init__()
+        super().setData(slave, transaction, skip_encode)
+        self.address = address
+        self.count = count
+
+    def encode(self):
+        """Encode a request pdu.
+
+        :returns: The encoded pdu
+        """
+        return struct.pack(">HH", self.address, self.count)
+
+    def decode(self, data):
+        """Decode a request pdu.
+
+        :param data: The packet data to decode
+        """
+        self.address, self.count = struct.unpack(">HH", data)
+
+    def get_response_pdu_size(self):
+        """Get response pdu size.
+
+        Func_code (1 byte) + Byte Count(1 byte) + Quantity of Coils (n Bytes)/8,
+        if the remainder is different of 0 then N = N+1
+        :return:
+        """
+        count = self.count // 8
+        if self.count % 8:  # pragma: no cover
+            count += 1
+
+        return 1 + 1 + count
+
+    def __str__(self):
+        """Return a string representation of the instance."""
+        return f"ReadBitRequest({self.address},{self.count})"
+
+
+class ReadBitsResponseBase(ModbusPDU):
+    """Base class for Messages responding to bit-reading values.
+
+    The requested bits can be found in the .bits list.
+    """
+
+    _rtu_byte_count_pos = 2
+
+    def __init__(self, values, slave, transaction, skip_encode):
+        """Initialize a new instance.
+
+        :param values: The requested values to be returned
+        :param slave: Modbus slave slave ID
+        """
+        super().__init__()
+        super().setData(slave, transaction, skip_encode)
+
+        #: A list of booleans representing bit values
+        self.bits = values or []
+
+    def encode(self):
+        """Encode response pdu.
+
+        :returns: The encoded packet message
+        """
+        result = pack_bitstring(self.bits)
+        packet = struct.pack(">B", len(result)) + result
+        return packet
+
+    def decode(self, data):
+        """Decode response pdu.
+
+        :param data: The packet data to decode
+        """
+        self.byte_count = int(data[0])  # pylint: disable=attribute-defined-outside-init
+        self.bits = unpack_bitstring(data[1:])
+
+    def setBit(self, address, value=1):
+        """Set the specified bit.
+
+        :param address: The bit to set
+        :param value: The value to set the bit to
+        """
+        self.bits[address] = bool(value)
+
+    def resetBit(self, address):
+        """Set the specified bit to 0.
+
+        :param address: The bit to reset
+        """
+        self.setBit(address, 0)
+
+    def getBit(self, address):
+        """Get the specified bit's value.
+
+        :param address: The bit to query
+        :returns: The value of the requested bit
+        """
+        return self.bits[address]
+
+    def __str__(self):
+        """Return a string representation of the instance."""
+        return f"{self.__class__.__name__}({len(self.bits)})"
+
+
+class ReadCoilsRequest(ReadBitsRequestBase):
+    """This function code is used to read from 1 to 2000(0x7d0) contiguous status of coils in a remote device.
+
+    The Request PDU specifies the starting
+    address, ie the address of the first coil specified, and the number of
+    coils. In the PDU Coils are addressed starting at zero. Therefore coils
+    numbered 1-16 are addressed as 0-15.
+    """
+
+    function_code = 1
+    function_code_name = "read_coils"
+
+    def __init__(self, address=None, count=None, slave=1, transaction=0, skip_encode=False):
+        """Initialize a new instance.
+
+        :param address: The address to start reading from
+        :param count: The number of bits to read
+        :param slave: Modbus slave slave ID
+        """
+        ReadBitsRequestBase.__init__(self, address, count, slave, transaction, skip_encode)
+
+    async def update_datastore(self, context):  # pragma: no cover
+        """Run a read coils request against a datastore.
+
+        Before running the request, we make sure that the request is in
+        the max valid range (0x001-0x7d0). Next we make sure that the
+        request is valid against the current datastore.
+
+        :param context: The datastore to request from
+        :returns: An initialized :py:class:`~pymodbus.register_read_message.ReadCoilsResponse`, or an :py:class:`~pymodbus.pdu.ExceptionResponse` if an error occurred
+        """
+        if not (1 <= self.count <= 0x7D0):
+            return self.doException(merror.IllegalValue)
+        if not context.validate(self.function_code, self.address, self.count):
+            return self.doException(merror.IllegalAddress)
+        values = await context.async_getValues(
+            self.function_code, self.address, self.count
+        )
+        return ReadCoilsResponse(values)
+
+
+class ReadCoilsResponse(ReadBitsResponseBase):
+    """The coils in the response message are packed as one coil per bit of the data field.
+
+    Status is indicated as 1= ON and 0= OFF. The LSB of the
+    first data byte contains the output addressed in the query. The other
+    coils follow toward the high order end of this byte, and from low order
+    to high order in subsequent bytes.
+
+    If the returned output quantity is not a multiple of eight, the
+    remaining bits in the final data byte will be padded with zeros
+    (toward the high order end of the byte). The Byte Count field specifies
+    the quantity of complete bytes of data.
+
+    The requested coils can be found in boolean form in the .bits list.
+    """
+
+    function_code = 1
+
+    def __init__(self, values=None, slave=1, transaction=0, skip_encode=False):
+        """Initialize a new instance.
+
+        :param values: The request values to respond with
+        :param slave: Modbus slave slave ID
+        """
+        ReadBitsResponseBase.__init__(self, values, slave, transaction, skip_encode)
+
+
+class ReadDiscreteInputsRequest(ReadBitsRequestBase):
+    """This function code is used to read from 1 to 2000(0x7d0).
+
+    Contiguous status of discrete inputs in a remote device. The Request PDU specifies the
+    starting address, ie the address of the first input specified, and the
+    number of inputs. In the PDU Discrete Inputs are addressed starting at
+    zero. Therefore Discrete inputs numbered 1-16 are addressed as 0-15.
+    """
+
+    function_code = 2
+    function_code_name = "read_discrete_input"
+
+    def __init__(self, address=None, count=None, slave=1, transaction=0, skip_encode=False):
+        """Initialize a new instance.
+
+        :param address: The address to start reading from
+        :param count: The number of bits to read
+        :param slave: Modbus slave slave ID
+        """
+        ReadBitsRequestBase.__init__(self, address, count, slave, transaction, skip_encode)
+
+    async def update_datastore(self, context):  # pragma: no cover
+        """Run a read discrete input request against a datastore.
+
+        Before running the request, we make sure that the request is in
+        the max valid range (0x001-0x7d0). Next we make sure that the
+        request is valid against the current datastore.
+
+        :param context: The datastore to request from
+        :returns: An initialized :py:class:`~pymodbus.register_read_message.ReadDiscreteInputsResponse`, or an :py:class:`~pymodbus.pdu.ExceptionResponse` if an error occurred
+        """
+        if not (1 <= self.count <= 0x7D0):
+            return self.doException(merror.IllegalValue)
+        if not context.validate(self.function_code, self.address, self.count):
+            return self.doException(merror.IllegalAddress)
+        values = await context.async_getValues(
+            self.function_code, self.address, self.count
+        )
+        return ReadDiscreteInputsResponse(values)
+
+
+class ReadDiscreteInputsResponse(ReadBitsResponseBase):
+    """The discrete inputs in the response message are packed as one input per bit of the data field.
+
+    Status is indicated as 1= ON; 0= OFF. The LSB of
+    the first data byte contains the input addressed in the query. The other
+    inputs follow toward the high order end of this byte, and from low order
+    to high order in subsequent bytes.
+
+    If the returned input quantity is not a multiple of eight, the
+    remaining bits in the final data byte will be padded with zeros
+    (toward the high order end of the byte). The Byte Count field specifies
+    the quantity of complete bytes of data.
+
+    The requested coils can be found in boolean form in the .bits list.
+    """
+
+    function_code = 2
+
+    def __init__(self, values=None, slave=1, transaction=0, skip_encode=False):
+        """Initialize a new instance.
+
+        :param values: The request values to respond with
+        :param slave: Modbus slave slave ID
+        """
+        ReadBitsResponseBase.__init__(self, values, slave, transaction, skip_encode)
diff --git a/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/pdu/bit_write_message.py b/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/pdu/bit_write_message.py
new file mode 100644
index 00000000..ad995f9a
--- /dev/null
+++ b/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/pdu/bit_write_message.py
@@ -0,0 +1,273 @@
+"""Bit Writing Request/Response.
+
+TODO write mask request/response
+"""
+
+
+# pylint: disable=missing-type-doc
+import struct
+
+from pymodbus.constants import ModbusStatus
+from pymodbus.pdu.pdu import ModbusExceptions as merror
+from pymodbus.pdu.pdu import ModbusPDU
+from pymodbus.utilities import pack_bitstring, unpack_bitstring
+
+
+# ---------------------------------------------------------------------------#
+#  Local Constants
+# ---------------------------------------------------------------------------#
+#  These are defined in the spec to turn a coil on/off
+# ---------------------------------------------------------------------------#
+_turn_coil_on = struct.pack(">H", ModbusStatus.ON)
+_turn_coil_off = struct.pack(">H", ModbusStatus.OFF)
+
+
+class WriteSingleCoilRequest(ModbusPDU):
+    """This function code is used to write a single output to either ON or OFF in a remote device.
+
+    The requested ON/OFF state is specified by a constant in the request
+    data field. A value of FF 00 hex requests the output to be ON. A value
+    of 00 00 requests it to be OFF. All other values are illegal and will
+    not affect the output.
+
+    The Request PDU specifies the address of the coil to be forced. Coils
+    are addressed starting at zero. Therefore coil numbered 1 is addressed
+    as 0. The requested ON/OFF state is specified by a constant in the Coil
+    Value field. A value of 0XFF00 requests the coil to be ON. A value of
+    0X0000 requests the coil to be off. All other values are illegal and
+    will not affect the coil.
+    """
+
+    function_code = 5
+    function_code_name = "write_coil"
+
+    _rtu_frame_size = 8
+
+    def __init__(self, address=None, value=None, slave=None, transaction=0, skip_encode=0):
+        """Initialize a new instance.
+
+        :param address: The variable address to write
+        :param value: The value to write at address
+        """
+        super().__init__()
+        super().setData(slave, transaction, skip_encode)
+        self.address = address
+        self.value = bool(value)
+
+    def encode(self):
+        """Encode write coil request.
+
+        :returns: The byte encoded message
+        """
+        result = struct.pack(">H", self.address)
+        if self.value:  # pragma: no cover
+            result += _turn_coil_on
+        else:
+            result += _turn_coil_off  # pragma: no cover
+        return result
+
+    def decode(self, data):
+        """Decode a write coil request.
+
+        :param data: The packet data to decode
+        """
+        self.address, value = struct.unpack(">HH", data)
+        self.value = value == ModbusStatus.ON
+
+    async def update_datastore(self, context):  # pragma: no cover
+        """Run a write coil request against a datastore.
+
+        :param context: The datastore to request from
+        :returns: The populated response or exception message
+        """
+        # if self.value not in [ModbusStatus.Off, ModbusStatus.On]:
+        #    return self.doException(merror.IllegalValue)
+        if not context.validate(self.function_code, self.address, 1):
+            return self.doException(merror.IllegalAddress)
+
+        await context.async_setValues(self.function_code, self.address, [self.value])
+        values = await context.async_getValues(self.function_code, self.address, 1)
+        return WriteSingleCoilResponse(self.address, values[0])
+
+    def get_response_pdu_size(self):
+        """Get response pdu size.
+
+        Func_code (1 byte) + Output Address (2 byte) + Output Value  (2 Bytes)
+        :return:
+        """
+        return 1 + 2 + 2
+
+    def __str__(self):
+        """Return a string representation of the instance."""
+        return f"WriteCoilRequest({self.address}, {self.value}) => "
+
+
+class WriteSingleCoilResponse(ModbusPDU):
+    """The normal response is an echo of the request.
+
+    Returned after the coil state has been written.
+    """
+
+    function_code = 5
+    _rtu_frame_size = 8
+
+    def __init__(self, address=None, value=None, slave=1, transaction=0, skip_encode=False):
+        """Initialize a new instance.
+
+        :param address: The variable address written to
+        :param value: The value written at address
+        """
+        super().__init__()
+        super().setData(slave, transaction, skip_encode)
+        self.address = address
+        self.value = value
+
+    def encode(self):
+        """Encode write coil response.
+
+        :return: The byte encoded message
+        """
+        result = struct.pack(">H", self.address)
+        if self.value:  # pragma: no cover
+            result += _turn_coil_on
+        else:
+            result += _turn_coil_off  # pragma: no cover
+        return result
+
+    def decode(self, data):
+        """Decode a write coil response.
+
+        :param data: The packet data to decode
+        """
+        self.address, value = struct.unpack(">HH", data)
+        self.value = value == ModbusStatus.ON
+
+    def __str__(self):
+        """Return a string representation of the instance.
+
+        :returns: A string representation of the instance
+        """
+        return f"WriteCoilResponse({self.address}) => {self.value}"
+
+
+class WriteMultipleCoilsRequest(ModbusPDU):
+    """This function code is used to forcea sequence of coils.
+
+    To either ON or OFF in a remote device. The Request PDU specifies the coil
+    references to be forced. Coils are addressed starting at zero. Therefore
+    coil numbered 1 is addressed as 0.
+
+    The requested ON/OFF states are specified by contents of the request
+    data field. A logical "1" in a bit position of the field requests the
+    corresponding output to be ON. A logical "0" requests it to be OFF."
+    """
+
+    function_code = 15
+    function_code_name = "write_coils"
+    _rtu_byte_count_pos = 6
+
+    def __init__(self, address=0, values=None, slave=None, transaction=0, skip_encode=0):
+        """Initialize a new instance.
+
+        :param address: The starting request address
+        :param values: The values to write
+        """
+        super().__init__()
+        super().setData(slave, transaction, skip_encode)
+        self.address = address
+        if values is None:  # pragma: no cover
+            values = []
+        elif not hasattr(values, "__iter__"):
+            values = [values]
+        self.values = values
+        self.byte_count = (len(self.values) + 7) // 8
+
+    def encode(self):
+        """Encode write coils request.
+
+        :returns: The byte encoded message
+        """
+        count = len(self.values)
+        self.byte_count = (count + 7) // 8
+        packet = struct.pack(">HHB", self.address, count, self.byte_count)
+        packet += pack_bitstring(self.values)
+        return packet
+
+    def decode(self, data):
+        """Decode a write coils request.
+
+        :param data: The packet data to decode
+        """
+        self.address, count, self.byte_count = struct.unpack(">HHB", data[0:5])
+        values = unpack_bitstring(data[5:])
+        self.values = values[:count]
+
+    async def update_datastore(self, context):  # pragma: no cover
+        """Run a write coils request against a datastore.
+
+        :param context: The datastore to request from
+        :returns: The populated response or exception message
+        """
+        count = len(self.values)
+        if not 1 <= count <= 0x07B0:
+            return self.doException(merror.IllegalValue)
+        if self.byte_count != (count + 7) // 8:
+            return self.doException(merror.IllegalValue)
+        if not context.validate(self.function_code, self.address, count):
+            return self.doException(merror.IllegalAddress)
+
+        await context.async_setValues(
+            self.function_code, self.address, self.values
+        )
+        return WriteMultipleCoilsResponse(self.address, count)
+
+    def __str__(self):
+        """Return a string representation of the instance."""
+        return f"WriteNCoilRequest ({self.address}) => {len(self.values)}"
+
+    def get_response_pdu_size(self):
+        """Get response pdu size.
+
+        Func_code (1 byte) + Output Address (2 byte) + Quantity of Outputs  (2 Bytes)
+        :return:
+        """
+        return 1 + 2 + 2
+
+
+class WriteMultipleCoilsResponse(ModbusPDU):
+    """The normal response returns the function code.
+
+    Starting address, and quantity of coils forced.
+    """
+
+    function_code = 15
+    _rtu_frame_size = 8
+
+    def __init__(self, address=None, count=None, slave=1, transaction=0, skip_encode=False):
+        """Initialize a new instance.
+
+        :param address: The starting variable address written to
+        :param count: The number of values written
+        """
+        super().__init__()
+        super().setData(slave, transaction, skip_encode)
+        self.address = address
+        self.count = count
+
+    def encode(self):
+        """Encode write coils response.
+
+        :returns: The byte encoded message
+        """
+        return struct.pack(">HH", self.address, self.count)
+
+    def decode(self, data):
+        """Decode a write coils response.
+
+        :param data: The packet data to decode
+        """
+        self.address, self.count = struct.unpack(">HH", data)
+
+    def __str__(self):
+        """Return a string representation of the instance."""
+        return f"WriteNCoilResponse({self.address}, {self.count})"
diff --git a/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/pdu/decoders.py b/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/pdu/decoders.py
new file mode 100644
index 00000000..6df645b8
--- /dev/null
+++ b/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/pdu/decoders.py
@@ -0,0 +1,113 @@
+"""Modbus Request/Response Decoders."""
+from __future__ import annotations
+
+import pymodbus.pdu.bit_read_message as bit_r_msg
+import pymodbus.pdu.bit_write_message as bit_w_msg
+import pymodbus.pdu.diag_message as diag_msg
+import pymodbus.pdu.file_message as file_msg
+import pymodbus.pdu.mei_message as mei_msg
+import pymodbus.pdu.other_message as o_msg
+import pymodbus.pdu.pdu as base
+import pymodbus.pdu.register_read_message as reg_r_msg
+import pymodbus.pdu.register_write_message as reg_w_msg
+from pymodbus.exceptions import MessageRegisterException, ModbusException
+from pymodbus.logging import Log
+
+
+class DecodePDU:
+    """Decode pdu requests/responses (server/client)."""
+
+    _pdu_class_table: set[tuple[type[base.ModbusPDU], type[base.ModbusPDU]]] = {
+        (reg_r_msg.ReadHoldingRegistersRequest, reg_r_msg.ReadHoldingRegistersResponse),
+        (bit_r_msg.ReadDiscreteInputsRequest, bit_r_msg.ReadDiscreteInputsResponse),
+        (reg_r_msg.ReadInputRegistersRequest, reg_r_msg.ReadInputRegistersResponse),
+        (bit_r_msg.ReadCoilsRequest, bit_r_msg.ReadCoilsResponse),
+        (bit_w_msg.WriteMultipleCoilsRequest, bit_w_msg.WriteMultipleCoilsResponse),
+        (reg_w_msg.WriteMultipleRegistersRequest, reg_w_msg.WriteMultipleRegistersResponse),
+        (reg_w_msg.WriteSingleRegisterRequest, reg_w_msg.WriteSingleRegisterResponse),
+        (bit_w_msg.WriteSingleCoilRequest, bit_w_msg.WriteSingleCoilResponse),
+        (reg_r_msg.ReadWriteMultipleRegistersRequest, reg_r_msg.ReadWriteMultipleRegistersResponse),
+        (diag_msg.DiagnosticStatusRequest, diag_msg.DiagnosticStatusResponse),
+        (o_msg.ReadExceptionStatusRequest, o_msg.ReadExceptionStatusResponse),
+        (o_msg.GetCommEventCounterRequest, o_msg.GetCommEventCounterResponse),
+        (o_msg.GetCommEventLogRequest, o_msg.GetCommEventLogResponse),
+        (o_msg.ReportSlaveIdRequest, o_msg.ReportSlaveIdResponse),
+        (file_msg.ReadFileRecordRequest, file_msg.ReadFileRecordResponse),
+        (file_msg.WriteFileRecordRequest, file_msg.WriteFileRecordResponse),
+        (reg_w_msg.MaskWriteRegisterRequest, reg_w_msg.MaskWriteRegisterResponse),
+        (file_msg.ReadFifoQueueRequest, file_msg.ReadFifoQueueResponse),
+        (mei_msg.ReadDeviceInformationRequest, mei_msg.ReadDeviceInformationResponse),
+    }
+
+    _pdu_sub_class_table: set[tuple[type[base.ModbusPDU], type[base.ModbusPDU]]] = {
+        (diag_msg.ReturnQueryDataRequest, diag_msg.ReturnQueryDataResponse),
+        (diag_msg.RestartCommunicationsOptionRequest, diag_msg.RestartCommunicationsOptionResponse),
+        (diag_msg.ReturnDiagnosticRegisterRequest, diag_msg.ReturnDiagnosticRegisterResponse),
+        (diag_msg.ChangeAsciiInputDelimiterRequest, diag_msg.ChangeAsciiInputDelimiterResponse),
+        (diag_msg.ForceListenOnlyModeRequest, diag_msg.ForceListenOnlyModeResponse),
+        (diag_msg.ClearCountersRequest, diag_msg.ClearCountersResponse),
+        (diag_msg.ReturnBusMessageCountRequest, diag_msg.ReturnBusMessageCountResponse),
+        (diag_msg.ReturnBusCommunicationErrorCountRequest, diag_msg.ReturnBusCommunicationErrorCountResponse),
+        (diag_msg.ReturnBusExceptionErrorCountRequest, diag_msg.ReturnBusExceptionErrorCountResponse),
+        (diag_msg.ReturnSlaveMessageCountRequest, diag_msg.ReturnSlaveMessageCountResponse),
+        (diag_msg.ReturnSlaveNoResponseCountRequest, diag_msg.ReturnSlaveNoResponseCountResponse),
+        (diag_msg.ReturnSlaveNAKCountRequest, diag_msg.ReturnSlaveNAKCountResponse),
+        (diag_msg.ReturnSlaveBusyCountRequest, diag_msg.ReturnSlaveBusyCountResponse),
+        (diag_msg.ReturnSlaveBusCharacterOverrunCountRequest, diag_msg.ReturnSlaveBusCharacterOverrunCountResponse),
+        (diag_msg.ReturnIopOverrunCountRequest, diag_msg.ReturnIopOverrunCountResponse),
+        (diag_msg.ClearOverrunCountRequest, diag_msg.ClearOverrunCountResponse),
+        (diag_msg.GetClearModbusPlusRequest, diag_msg.GetClearModbusPlusResponse),
+        (mei_msg.ReadDeviceInformationRequest, mei_msg.ReadDeviceInformationResponse),
+    }
+
+    def __init__(self, is_server: bool) -> None:
+        """Initialize function_tables."""
+        inx = 0 if is_server else 1
+        self.lookup: dict[int, type[base.ModbusPDU]] = {cl[inx].function_code: cl[inx] for cl in self._pdu_class_table}
+        self.sub_lookup: dict[int, dict[int, type[base.ModbusPDU]]] = {f: {} for f in self.lookup}
+        for f in self._pdu_sub_class_table:
+            self.sub_lookup[f[inx].function_code][f[inx].sub_function_code] = f[inx]
+
+    def lookupPduClass(self, function_code: int) -> type[base.ModbusPDU]:
+        """Use `function_code` to determine the class of the PDU."""
+        return self.lookup.get(function_code, base.ExceptionResponse)
+
+    def register(self, custom_class: type[base.ModbusPDU]) -> None:
+        """Register a function and sub function class with the decoder."""
+        if not issubclass(custom_class, base.ModbusPDU):
+            raise MessageRegisterException(
+                f'"{custom_class.__class__.__name__}" is Not a valid Modbus Message'
+                ". Class needs to be derived from "
+                "`pymodbus.pdu.ModbusPDU` "
+            )
+        self.lookup[custom_class.function_code] = custom_class
+        if custom_class.sub_function_code >= 0:
+            if custom_class.function_code not in self.sub_lookup:
+                self.sub_lookup[custom_class.function_code] = {}
+            self.sub_lookup[custom_class.function_code][
+                custom_class.sub_function_code
+            ] = custom_class
+
+    def decode(self, frame: bytes) -> base.ModbusPDU | None:
+        """Decode a frame."""
+        try:
+            if (function_code := int(frame[0])) > 0x80:
+                pdu_exp = base.ExceptionResponse(function_code & 0x7F)
+                pdu_exp.decode(frame[1:])
+                return pdu_exp
+            if not (pdu_type := self.lookup.get(function_code, None)):
+                Log.debug("decode PDU failed for function code {}", function_code)
+                raise ModbusException(f"Unknown response {function_code}")
+            pdu = pdu_type()
+            pdu.setData(0, 0, False)
+            Log.debug("decode PDU for {}", function_code)
+            pdu.decode(frame[1:])
+
+            if pdu.sub_function_code >= 0:
+                lookup = self.sub_lookup.get(pdu.function_code, {})
+                if subtype := lookup.get(pdu.sub_function_code, None):
+                    pdu.__class__ = subtype
+            return pdu
+        except (ModbusException, ValueError, IndexError) as exc:
+            Log.warning("Unable to decode frame {}", exc)
+        return None
diff --git a/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/pdu/diag_message.py b/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/pdu/diag_message.py
new file mode 100644
index 00000000..3fa58033
--- /dev/null
+++ b/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/pdu/diag_message.py
@@ -0,0 +1,832 @@
+"""Diagnostic Record Read/Write.
+
+These need to be tied into a the current server context
+or linked to the appropriate data
+"""
+
+
+# pylint: disable=missing-type-doc
+import struct
+
+from pymodbus.constants import ModbusPlusOperation, ModbusStatus
+from pymodbus.device import ModbusControlBlock
+from pymodbus.exceptions import ModbusException, NotImplementedException
+from pymodbus.pdu.pdu import ModbusPDU
+from pymodbus.utilities import pack_bitstring
+
+
+_MCB = ModbusControlBlock()
+
+
+# ---------------------------------------------------------------------------#
+#  Diagnostic Function Codes Base Classes
+#  diagnostic 08, 00-18,20
+# ---------------------------------------------------------------------------#
+#  TODO Make sure all the data is decoded from the response # pylint: disable=fixme
+# ---------------------------------------------------------------------------#
+class DiagnosticStatusRequest(ModbusPDU):
+    """This is a base class for all of the diagnostic request functions."""
+
+    function_code = 0x08
+    function_code_name = "diagnostic_status"
+    sub_function_code = 9999
+    _rtu_frame_size = 8
+
+    def __init__(self, slave=1, transaction=0, skip_encode=False):
+        """Initialize a diagnostic request."""
+        super().__init__()
+        super().setData(slave, transaction, skip_encode)
+        self.message = None
+
+    def encode(self):
+        """Encode a diagnostic response.
+
+        we encode the data set in self.message
+
+        :returns: The encoded packet
+        """
+        packet = struct.pack(">H", self.sub_function_code)
+        if self.message is not None:
+            if isinstance(self.message, str):  # pragma: no cover
+                packet += self.message.encode()
+            elif isinstance(self.message, bytes):
+                packet += self.message
+            elif isinstance(self.message, (list, tuple)):
+                for piece in self.message:
+                    packet += struct.pack(">H", piece)
+            elif isinstance(self.message, int):  # pragma: no cover
+                packet += struct.pack(">H", self.message)
+        return packet
+
+    def decode(self, data):
+        """Decode a diagnostic request.
+
+        :param data: The data to decode into the function code
+        """
+        (self.sub_function_code, ) = struct.unpack(">H", data[:2])
+        if self.sub_function_code == ReturnQueryDataRequest.sub_function_code:  # pragma: no cover
+            self.message = data[2:]
+        else:
+            (self.message,) = struct.unpack(">H", data[2:])  # pragma: no cover
+
+    def get_response_pdu_size(self):
+        """Get response pdu size.
+
+        Func_code (1 byte) + Sub function code (2 byte) + Data (2 * N bytes)
+        :return:
+        """
+        if not isinstance(self.message, list):
+            self.message = [self.message]
+        return 1 + 2 + 2 * len(self.message)
+
+
+class DiagnosticStatusResponse(ModbusPDU):
+    """Diagnostic status.
+
+    This is a base class for all of the diagnostic response functions
+
+    It works by performing all of the encoding and decoding of variable
+    data and lets the higher classes define what extra data to append
+    and how to update_datastore a request
+    """
+
+    function_code = 0x08
+    sub_function_code = 9999
+    _rtu_frame_size = 8
+
+    def __init__(self, slave=1, transaction=0, skip_encode=False):
+        """Initialize a diagnostic response."""
+        super().__init__()
+        super().setData(slave, transaction, skip_encode)
+        self.message = None
+
+    def encode(self):
+        """Encode diagnostic response.
+
+        we encode the data set in self.message
+
+        :returns: The encoded packet
+        """
+        packet = struct.pack(">H", self.sub_function_code)
+        if self.message is not None:
+            if isinstance(self.message, str):  # pragma: no cover
+                packet += self.message.encode()
+            elif isinstance(self.message, bytes):
+                packet += self.message
+            elif isinstance(self.message, (list, tuple)):
+                for piece in self.message:
+                    packet += struct.pack(">H", piece)
+            elif isinstance(self.message, int):  # pragma: no cover
+                packet += struct.pack(">H", self.message)
+        return packet
+
+    def decode(self, data):
+        """Decode diagnostic response.
+
+        :param data: The data to decode into the function code
+        """
+        (self.sub_function_code, ) = struct.unpack(">H", data[:2])
+        data = data[2:]
+        if self.sub_function_code == ReturnQueryDataRequest.sub_function_code:
+            self.message = data
+        else:
+            word_len = len(data) // 2
+            if len(data) % 2:  # pragma: no cover
+                word_len += 1
+                data += b"0"
+            data = struct.unpack(">" + "H" * word_len, data)
+            self.message = data
+
+
+class DiagnosticStatusSimpleRequest(DiagnosticStatusRequest):
+    """Return diagnostic status.
+
+    A large majority of the diagnostic functions are simple
+    status request functions.  They work by sending 0x0000
+    as data and their function code and they are returned
+    2 bytes of data.
+
+    If a function inherits this, they only need to implement
+    the update_datastore method
+    """
+
+    def __init__(self, data=0x0000, slave=1, transaction=0, skip_encode=False):
+        """Initialize a simple diagnostic request.
+
+        The data defaults to 0x0000 if not provided as over half
+        of the functions require it.
+
+        :param data: The data to send along with the request
+        """
+        DiagnosticStatusRequest.__init__(self, slave=slave, transaction=transaction, skip_encode=skip_encode)
+        self.message = data
+
+    async def update_datastore(self, *args):  # pragma: no cover
+        """Raise if not implemented."""
+        raise NotImplementedException("Diagnostic Message Has No update_datastore Method")
+
+
+class DiagnosticStatusSimpleResponse(DiagnosticStatusResponse):
+    """Diagnostic status.
+
+    A large majority of the diagnostic functions are simple
+    status request functions.  They work by sending 0x0000
+    as data and their function code and they are returned
+    2 bytes of data.
+    """
+
+    def __init__(self, data=0x0000, slave=1, transaction=0, skip_encode=False):
+        """Return a simple diagnostic response.
+
+        :param data: The resulting data to return to the client
+        """
+        DiagnosticStatusResponse.__init__(self, slave=slave, transaction=transaction, skip_encode=skip_encode)
+        self.message = data
+
+
+# ---------------------------------------------------------------------------#
+#  Diagnostic Sub Code 00
+# ---------------------------------------------------------------------------#
+class ReturnQueryDataRequest(DiagnosticStatusRequest):
+    """Return query data.
+
+    The data passed in the request data field is to be returned (looped back)
+    in the response. The entire response message should be identical to the
+    request.
+    """
+
+    sub_function_code = 0x0000
+
+    def __init__(self, message=b"\x00\x00", slave=1, transaction=0, skip_encode=False):
+        """Initialize a new instance of the request.
+
+        :param message: The message to send to loopback
+        """
+        DiagnosticStatusRequest.__init__(self, slave=slave, transaction=transaction, skip_encode=skip_encode)
+        if not isinstance(message, bytes):  # pragma: no cover
+            raise ModbusException(f"message({type(message)}) must be bytes")
+        self.message = message
+
+    async def update_datastore(self, *_args):  # pragma: no cover
+        """update_datastore the loopback request (builds the response).
+
+        :returns: The populated loopback response message
+        """
+        return ReturnQueryDataResponse(self.message)
+
+
+class ReturnQueryDataResponse(DiagnosticStatusResponse):
+    """Return query data.
+
+    The data passed in the request data field is to be returned (looped back)
+    in the response. The entire response message should be identical to the
+    request.
+    """
+
+    sub_function_code = 0x0000
+
+    def __init__(self, message=b"\x00\x00", slave=1, transaction=0, skip_encode=False):
+        """Initialize a new instance of the response.
+
+        :param message: The message to loopback
+        """
+        DiagnosticStatusResponse.__init__(self, slave=slave, transaction=transaction, skip_encode=skip_encode)
+        if not isinstance(message, bytes):  # pragma: no cover
+            raise ModbusException(f"message({type(message)}) must be bytes")
+        self.message = message
+
+
+# ---------------------------------------------------------------------------#
+#  Diagnostic Sub Code 01
+# ---------------------------------------------------------------------------#
+class RestartCommunicationsOptionRequest(DiagnosticStatusRequest):
+    """Restart communication.
+
+    The remote device serial line port must be initialized and restarted, and
+    all of its communications event counters are cleared. If the port is
+    currently in Listen Only Mode, no response is returned. This function is
+    the only one that brings the port out of Listen Only Mode. If the port is
+    not currently in Listen Only Mode, a normal response is returned. This
+    occurs before the restart is update_datastored.
+    """
+
+    sub_function_code = 0x0001
+
+    def __init__(self, toggle=False, slave=1, transaction=0, skip_encode=False):
+        """Initialize a new request.
+
+        :param toggle: Set to True to toggle, False otherwise
+        """
+        DiagnosticStatusRequest.__init__(self, slave=slave, transaction=transaction, skip_encode=skip_encode)
+        if toggle:  # pragma: no cover
+            self.message = [ModbusStatus.ON]
+        else:
+            self.message = [ModbusStatus.OFF]  # pragma: no cover
+
+    async def update_datastore(self, *_args):  # pragma: no cover
+        """Clear event log and restart.
+
+        :returns: The initialized response message
+        """
+        # if _MCB.ListenOnly:
+        return RestartCommunicationsOptionResponse(self.message)
+
+
+class RestartCommunicationsOptionResponse(DiagnosticStatusResponse):
+    """Restart Communication.
+
+    The remote device serial line port must be initialized and restarted, and
+    all of its communications event counters are cleared. If the port is
+    currently in Listen Only Mode, no response is returned. This function is
+    the only one that brings the port out of Listen Only Mode. If the port is
+    not currently in Listen Only Mode, a normal response is returned. This
+    occurs before the restart is update_datastored.
+    """
+
+    sub_function_code = 0x0001
+
+    def __init__(self, toggle=False, slave=1, transaction=0, skip_encode=False):
+        """Initialize a new response.
+
+        :param toggle: Set to True if we toggled, False otherwise
+        """
+        DiagnosticStatusResponse.__init__(self, slave=slave, transaction=transaction, skip_encode=skip_encode)
+        if toggle:  # pragma: no cover
+            self.message = [ModbusStatus.ON]
+        else:
+            self.message = [ModbusStatus.OFF]  # pragma: no cover
+
+
+# ---------------------------------------------------------------------------#
+#  Diagnostic Sub Code 02
+# ---------------------------------------------------------------------------#
+class ReturnDiagnosticRegisterRequest(DiagnosticStatusSimpleRequest):
+    """The contents of the remote device's 16-bit diagnostic register are returned in the response."""
+
+    sub_function_code = 0x0002
+
+    async def update_datastore(self, *args):  # pragma: no cover
+        """update_datastore the diagnostic request on the given device.
+
+        :returns: The initialized response message
+        """
+        # if _MCB.isListenOnly():
+        register = pack_bitstring(_MCB.getDiagnosticRegister())
+        return ReturnDiagnosticRegisterResponse(register)
+
+
+class ReturnDiagnosticRegisterResponse(DiagnosticStatusSimpleResponse):
+    """Return diagnostic register.
+
+    The contents of the remote device's 16-bit diagnostic register are
+    returned in the response
+    """
+
+    sub_function_code = 0x0002
+
+
+# ---------------------------------------------------------------------------#
+#  Diagnostic Sub Code 03
+# ---------------------------------------------------------------------------#
+class ChangeAsciiInputDelimiterRequest(DiagnosticStatusSimpleRequest):
+    """Change ascii input delimiter.
+
+    The character "CHAR" passed in the request data field becomes the end of
+    message delimiter for future messages (replacing the default LF
+    character). This function is useful in cases of a Line Feed is not
+    required at the end of ASCII messages.
+    """
+
+    sub_function_code = 0x0003
+
+    async def update_datastore(self, *args):  # pragma: no cover
+        """update_datastore the diagnostic request on the given device.
+
+        :returns: The initialized response message
+        """
+        char = (self.message & 0xFF00) >> 8  # type: ignore[operator]
+        _MCB.Delimiter = char
+        return ChangeAsciiInputDelimiterResponse(self.message)
+
+
+class ChangeAsciiInputDelimiterResponse(DiagnosticStatusSimpleResponse):
+    """Change ascii input delimiter.
+
+    The character "CHAR" passed in the request data field becomes the end of
+    message delimiter for future messages (replacing the default LF
+    character). This function is useful in cases of a Line Feed is not
+    required at the end of ASCII messages.
+    """
+
+    sub_function_code = 0x0003
+
+
+# ---------------------------------------------------------------------------#
+#  Diagnostic Sub Code 04
+# ---------------------------------------------------------------------------#
+class ForceListenOnlyModeRequest(DiagnosticStatusSimpleRequest):
+    """Forces the addressed remote device to its Listen Only Mode for MODBUS communications.
+
+    This isolates it from the other devices on the network,
+    allowing them to continue communicating without interruption from the
+    addressed remote device. No response is returned.
+    """
+
+    sub_function_code = 0x0004
+
+    async def update_datastore(self, *args):  # pragma: no cover
+        """update_datastore the diagnostic request on the given device.
+
+        :returns: The initialized response message
+        """
+        _MCB.ListenOnly = True
+        return ForceListenOnlyModeResponse()
+
+
+class ForceListenOnlyModeResponse(DiagnosticStatusResponse):
+    """Forces the addressed remote device to its Listen Only Mode for MODBUS communications.
+
+    This isolates it from the other devices on the network,
+    allowing them to continue communicating without interruption from the
+    addressed remote device. No response is returned.
+
+    This does not send a response
+    """
+
+    sub_function_code = 0x0004
+
+    def __init__(self, slave=1, transaction=0, skip_encode=False):
+        """Initialize to block a return response."""
+        DiagnosticStatusResponse.__init__(self, slave=slave, transaction=transaction, skip_encode=skip_encode)
+        self.message = []
+
+
+# ---------------------------------------------------------------------------#
+#  Diagnostic Sub Code 10
+# ---------------------------------------------------------------------------#
+class ClearCountersRequest(DiagnosticStatusSimpleRequest):
+    """Clear ll counters and the diagnostic register.
+
+    Also, counters are cleared upon power-up
+    """
+
+    sub_function_code = 0x000A
+
+    async def update_datastore(self, *args):  # pragma: no cover
+        """update_datastore the diagnostic request on the given device.
+
+        :returns: The initialized response message
+        """
+        _MCB.reset()
+        return ClearCountersResponse(self.message)
+
+
+class ClearCountersResponse(DiagnosticStatusSimpleResponse):
+    """Clear ll counters and the diagnostic register.
+
+    Also, counters are cleared upon power-up
+    """
+
+    sub_function_code = 0x000A
+
+
+# ---------------------------------------------------------------------------#
+#  Diagnostic Sub Code 11
+# ---------------------------------------------------------------------------#
+class ReturnBusMessageCountRequest(DiagnosticStatusSimpleRequest):
+    """Return bus message count.
+
+    The response data field returns the quantity of messages that the
+    remote device has detected on the communications systems since its last
+    restart, clear counters operation, or power-up
+    """
+
+    sub_function_code = 0x000B
+
+    async def update_datastore(self, *args):  # pragma: no cover
+        """update_datastore the diagnostic request on the given device.
+
+        :returns: The initialized response message
+        """
+        count = _MCB.Counter.BusMessage
+        return ReturnBusMessageCountResponse(count)
+
+
+class ReturnBusMessageCountResponse(DiagnosticStatusSimpleResponse):
+    """Return bus message count.
+
+    The response data field returns the quantity of messages that the
+    remote device has detected on the communications systems since its last
+    restart, clear counters operation, or power-up
+    """
+
+    sub_function_code = 0x000B
+
+
+# ---------------------------------------------------------------------------#
+#  Diagnostic Sub Code 12
+# ---------------------------------------------------------------------------#
+class ReturnBusCommunicationErrorCountRequest(DiagnosticStatusSimpleRequest):
+    """Return bus comm. count.
+
+    The response data field returns the quantity of CRC errors encountered
+    by the remote device since its last restart, clear counter operation, or
+    power-up
+    """
+
+    sub_function_code = 0x000C
+
+    async def update_datastore(self, *args):  # pragma: no cover
+        """update_datastore the diagnostic request on the given device.
+
+        :returns: The initialized response message
+        """
+        count = _MCB.Counter.BusCommunicationError
+        return ReturnBusCommunicationErrorCountResponse(count)
+
+
+class ReturnBusCommunicationErrorCountResponse(DiagnosticStatusSimpleResponse):
+    """Return bus comm. error.
+
+    The response data field returns the quantity of CRC errors encountered
+    by the remote device since its last restart, clear counter operation, or
+    power-up
+    """
+
+    sub_function_code = 0x000C
+
+
+# ---------------------------------------------------------------------------#
+#  Diagnostic Sub Code 13
+# ---------------------------------------------------------------------------#
+class ReturnBusExceptionErrorCountRequest(DiagnosticStatusSimpleRequest):
+    """Return bus exception.
+
+    The response data field returns the quantity of modbus exception
+    responses returned by the remote device since its last restart,
+    clear counters operation, or power-up
+    """
+
+    sub_function_code = 0x000D
+
+    async def update_datastore(self, *args):  # pragma: no cover
+        """update_datastore the diagnostic request on the given device.
+
+        :returns: The initialized response message
+        """
+        count = _MCB.Counter.BusExceptionError
+        return ReturnBusExceptionErrorCountResponse(count)
+
+
+class ReturnBusExceptionErrorCountResponse(DiagnosticStatusSimpleResponse):
+    """Return bus exception.
+
+    The response data field returns the quantity of modbus exception
+    responses returned by the remote device since its last restart,
+    clear counters operation, or power-up
+    """
+
+    sub_function_code = 0x000D
+
+
+# ---------------------------------------------------------------------------#
+#  Diagnostic Sub Code 14
+# ---------------------------------------------------------------------------#
+class ReturnSlaveMessageCountRequest(DiagnosticStatusSimpleRequest):
+    """Return slave message count.
+
+    The response data field returns the quantity of messages addressed to the
+    remote device, that the remote device has processed since
+    its last restart, clear counters operation, or power-up
+    """
+
+    sub_function_code = 0x000E
+
+    async def update_datastore(self, *args):  # pragma: no cover
+        """update_datastore the diagnostic request on the given device.
+
+        :returns: The initialized response message
+        """
+        count = _MCB.Counter.SlaveMessage
+        return ReturnSlaveMessageCountResponse(count)
+
+
+class ReturnSlaveMessageCountResponse(DiagnosticStatusSimpleResponse):
+    """Return slave message count.
+
+    The response data field returns the quantity of messages addressed to the
+    remote device, that the remote device has processed since
+    its last restart, clear counters operation, or power-up
+    """
+
+    sub_function_code = 0x000E
+
+
+# ---------------------------------------------------------------------------#
+#  Diagnostic Sub Code 15
+# ---------------------------------------------------------------------------#
+class ReturnSlaveNoResponseCountRequest(DiagnosticStatusSimpleRequest):
+    """Return slave no response.
+
+    The response data field returns the quantity of messages addressed to the
+    remote device, that the remote device has processed since
+    its last restart, clear counters operation, or power-up
+    """
+
+    sub_function_code = 0x000F
+
+    async def update_datastore(self, *args):  # pragma: no cover
+        """update_datastore the diagnostic request on the given device.
+
+        :returns: The initialized response message
+        """
+        count = _MCB.Counter.SlaveNoResponse
+        return ReturnSlaveNoResponseCountResponse(count)
+
+
+class ReturnSlaveNoResponseCountResponse(DiagnosticStatusSimpleResponse):
+    """Return slave no response.
+
+    The response data field returns the quantity of messages addressed to the
+    remote device, that the remote device has processed since
+    its last restart, clear counters operation, or power-up
+    """
+
+    sub_function_code = 0x000F
+
+
+# ---------------------------------------------------------------------------#
+#  Diagnostic Sub Code 16
+# ---------------------------------------------------------------------------#
+class ReturnSlaveNAKCountRequest(DiagnosticStatusSimpleRequest):
+    """Return slave NAK count.
+
+    The response data field returns the quantity of messages addressed to the
+    remote device for which it returned a Negative Acknowledge (NAK) exception
+    response, since its last restart, clear counters operation, or power-up.
+    Exception responses are described and listed in section 7 .
+    """
+
+    sub_function_code = 0x0010
+
+    async def update_datastore(self, *args):  # pragma: no cover
+        """update_datastore the diagnostic request on the given device.
+
+        :returns: The initialized response message
+        """
+        count = _MCB.Counter.SlaveNAK
+        return ReturnSlaveNAKCountResponse(count)
+
+
+class ReturnSlaveNAKCountResponse(DiagnosticStatusSimpleResponse):
+    """Return slave NAK.
+
+    The response data field returns the quantity of messages addressed to the
+    remote device for which it returned a Negative Acknowledge (NAK) exception
+    response, since its last restart, clear counters operation, or power-up.
+    Exception responses are described and listed in section 7.
+    """
+
+    sub_function_code = 0x0010
+
+
+# ---------------------------------------------------------------------------#
+#  Diagnostic Sub Code 17
+# ---------------------------------------------------------------------------#
+class ReturnSlaveBusyCountRequest(DiagnosticStatusSimpleRequest):
+    """Return slave busy count.
+
+    The response data field returns the quantity of messages addressed to the
+    remote device for which it returned a Slave Device Busy exception response,
+    since its last restart, clear counters operation, or power-up.
+    """
+
+    sub_function_code = 0x0011
+
+    async def update_datastore(self, *args):  # pragma: no cover
+        """update_datastore the diagnostic request on the given device.
+
+        :returns: The initialized response message
+        """
+        count = _MCB.Counter.SlaveBusy
+        return ReturnSlaveBusyCountResponse(count)
+
+
+class ReturnSlaveBusyCountResponse(DiagnosticStatusSimpleResponse):
+    """Return slave busy count.
+
+    The response data field returns the quantity of messages addressed to the
+    remote device for which it returned a Slave Device Busy exception response,
+    since its last restart, clear counters operation, or power-up.
+    """
+
+    sub_function_code = 0x0011
+
+
+# ---------------------------------------------------------------------------#
+#  Diagnostic Sub Code 18
+# ---------------------------------------------------------------------------#
+class ReturnSlaveBusCharacterOverrunCountRequest(DiagnosticStatusSimpleRequest):
+    """Return slave character overrun.
+
+    The response data field returns the quantity of messages addressed to the
+    remote device that it could not handle due to a character overrun condition,
+    since its last restart, clear counters operation, or power-up. A character
+    overrun is caused by data characters arriving at the port faster than they
+    can be stored, or by the loss of a character due to a hardware malfunction.
+    """
+
+    sub_function_code = 0x0012
+
+    async def update_datastore(self, *args):  # pragma: no cover
+        """update_datastore the diagnostic request on the given device.
+
+        :returns: The initialized response message
+        """
+        count = _MCB.Counter.BusCharacterOverrun
+        return ReturnSlaveBusCharacterOverrunCountResponse(count)
+
+
+class ReturnSlaveBusCharacterOverrunCountResponse(DiagnosticStatusSimpleResponse):
+    """Return the quantity of messages addressed to the remote device unhandled due to a character overrun.
+
+    Since its last restart, clear counters operation, or power-up. A character
+    overrun is caused by data characters arriving at the port faster than they
+    can be stored, or by the loss of a character due to a hardware malfunction.
+    """
+
+    sub_function_code = 0x0012
+
+
+# ---------------------------------------------------------------------------#
+#  Diagnostic Sub Code 19
+# ---------------------------------------------------------------------------#
+class ReturnIopOverrunCountRequest(DiagnosticStatusSimpleRequest):
+    """Return IopOverrun.
+
+    An IOP overrun is caused by data characters arriving at the port
+    faster than they can be stored, or by the loss of a character due
+    to a hardware malfunction.  This function is specific to the 884.
+    """
+
+    sub_function_code = 0x0013
+
+    async def update_datastore(self, *args):  # pragma: no cover
+        """update_datastore the diagnostic request on the given device.
+
+        :returns: The initialized response message
+        """
+        count = _MCB.Counter.BusCharacterOverrun
+        return ReturnIopOverrunCountResponse(count)
+
+
+class ReturnIopOverrunCountResponse(DiagnosticStatusSimpleResponse):
+    """Return Iop overrun count.
+
+    The response data field returns the quantity of messages
+    addressed to the slave that it could not handle due to an 884
+    IOP overrun condition, since its last restart, clear counters
+    operation, or power-up.
+    """
+
+    sub_function_code = 0x0013
+
+
+# ---------------------------------------------------------------------------#
+#  Diagnostic Sub Code 20
+# ---------------------------------------------------------------------------#
+class ClearOverrunCountRequest(DiagnosticStatusSimpleRequest):
+    """Clear the overrun error counter and reset the error flag.
+
+    An error flag should be cleared, but nothing else in the
+    specification mentions is, so it is ignored.
+    """
+
+    sub_function_code = 0x0014
+
+    async def update_datastore(self, *args):  # pragma: no cover
+        """update_datastore the diagnostic request on the given device.
+
+        :returns: The initialized response message
+        """
+        _MCB.Counter.BusCharacterOverrun = 0x0000
+        return ClearOverrunCountResponse(self.message)
+
+
+class ClearOverrunCountResponse(DiagnosticStatusSimpleResponse):
+    """Clear the overrun error counter and reset the error flag."""
+
+    sub_function_code = 0x0014
+
+
+# ---------------------------------------------------------------------------#
+#  Diagnostic Sub Code 21
+# ---------------------------------------------------------------------------#
+class GetClearModbusPlusRequest(DiagnosticStatusSimpleRequest):
+    """Get/Clear modbus plus request.
+
+    In addition to the Function code (08) and Subfunction code
+    (00 15 hex) in the query, a two-byte Operation field is used
+    to specify either a "Get Statistics" or a "Clear Statistics"
+    operation.  The two operations are exclusive - the "Get"
+    operation cannot clear the statistics, and the "Clear"
+    operation does not return statistics prior to clearing
+    them. Statistics are also cleared on power-up of the slave
+    device.
+    """
+
+    sub_function_code = 0x0015
+
+    def __init__(self, data=0, slave=1, transaction=0, skip_encode=False):
+        """Initialize."""
+        super().__init__(slave=slave, transaction=transaction, skip_encode=skip_encode)
+        self.message=data
+
+    def get_response_pdu_size(self):
+        """Return a series of 54 16-bit words (108 bytes) in the data field of the response.
+
+        This function differs from the usual two-byte length of the data field.
+        The data contains the statistics for the Modbus Plus peer processor in the slave device.
+        Func_code (1 byte) + Sub function code (2 byte) + Operation (2 byte) + Data (108 bytes)
+        :return:
+        """
+        if self.message == ModbusPlusOperation.GET_STATISTICS:  # pragma: no cover
+            data = 2 + 108  # byte count(2) + data (54*2)
+        else:
+            data = 0
+        return 1 + 2 + 2 + 2 + data
+
+    async def update_datastore(self, *args):  # pragma: no cover
+        """update_datastore the diagnostic request on the given device.
+
+        :returns: The initialized response message
+        """
+        message = None  # the clear operation does not return info
+        if self.message == ModbusPlusOperation.CLEAR_STATISTICS:
+            _MCB.Plus.reset()
+            message = self.message
+        else:
+            message = [self.message]
+            message += _MCB.Plus.encode()
+        return GetClearModbusPlusResponse(message)
+
+    def encode(self):
+        """Encode a diagnostic response.
+
+        we encode the data set in self.message
+
+        :returns: The encoded packet
+        """
+        packet = struct.pack(">H", self.sub_function_code)
+        packet += struct.pack(">H", self.message)
+        return packet
+
+
+class GetClearModbusPlusResponse(DiagnosticStatusSimpleResponse):
+    """Return a series of 54 16-bit words (108 bytes) in the data field of the response.
+
+    This function differs from the usual two-byte length of the data field.
+    The data contains the statistics for the Modbus Plus peer processor in the slave device.
+    """
+
+    sub_function_code = 0x0015
diff --git a/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/pdu/file_message.py b/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/pdu/file_message.py
new file mode 100644
index 00000000..16337579
--- /dev/null
+++ b/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/pdu/file_message.py
@@ -0,0 +1,437 @@
+"""File Record Read/Write Messages.
+
+Currently none of these messages are implemented
+"""
+from __future__ import annotations
+
+# pylint: disable=missing-type-doc
+import struct
+
+from pymodbus.pdu.pdu import ModbusExceptions as merror
+from pymodbus.pdu.pdu import ModbusPDU
+
+
+# ---------------------------------------------------------------------------#
+#  File Record Types
+# ---------------------------------------------------------------------------#
+class FileRecord:  # pylint: disable=eq-without-hash
+    """Represents a file record and its relevant data."""
+
+    def __init__(self, reference_type=0x06, file_number=0x00, record_number=0x00, record_data=b'', record_length=None, response_length=None):
+        """Initialize a new instance.
+
+        :params reference_type: must be 0x06
+        :params file_number: Indicates which file number we are reading
+        :params record_number: Indicates which record in the file
+        :params record_data: The actual data of the record
+        :params record_length: The length in registers of the record
+        :params response_length: The length in bytes of the record
+        """
+        self.reference_type = reference_type
+        self.file_number = file_number
+        self.record_number = record_number
+        self.record_data = record_data
+
+        self.record_length = record_length if record_length else len(self.record_data) // 2
+        self.response_length = response_length if response_length else len(self.record_data) + 1
+
+    def __eq__(self, relf):
+        """Compare the left object to the right."""
+        return (  # pragma: no cover
+            self.reference_type == relf.reference_type
+            and self.file_number == relf.file_number
+            and self.record_number == relf.record_number
+            and self.record_length == relf.record_length
+            and self.record_data == relf.record_data
+        )
+
+    def __ne__(self, relf):
+        """Compare the left object to the right."""
+        return not self.__eq__(relf)  # pragma: no cover
+
+    def __repr__(self):  # pragma: no cover
+        """Give a representation of the file record."""
+        params = (self.file_number, self.record_number, self.record_length)
+        return (
+            "FileRecord(file=%d, record=%d, length=%d)"  # pylint: disable=consider-using-f-string
+            % params
+        )
+
+
+# ---------------------------------------------------------------------------#
+#  File Requests/Responses
+# ---------------------------------------------------------------------------#
+class ReadFileRecordRequest(ModbusPDU):
+    """Read file record request.
+
+    This function code is used to perform a file record read. All request
+    data lengths are provided in terms of number of bytes and all record
+    lengths are provided in terms of registers.
+
+    A file is an organization of records. Each file contains 10000 records,
+    addressed 0000 to 9999 decimal or 0x0000 to 0x270f. For example, record
+    12 is addressed as 12. The function can read multiple groups of
+    references. The groups can be separating (non-contiguous), but the
+    references within each group must be sequential. Each group is defined
+    in a separate "sub-request" field that contains seven bytes::
+
+        The reference type: 1 byte (must be 0x06)
+        The file number: 2 bytes
+        The starting record number within the file: 2 bytes
+        The length of the record to be read: 2 bytes
+
+    The quantity of registers to be read, combined with all other fields
+    in the expected response, must not exceed the allowable length of the
+    MODBUS PDU: 235 bytes.
+    """
+
+    function_code = 0x14
+    function_code_name = "read_file_record"
+    _rtu_byte_count_pos = 2
+
+    def __init__(self, records=None, slave=1, transaction=0, skip_encode=False):
+        """Initialize a new instance.
+
+        :param records: The file record requests to be read
+        """
+        super().__init__()
+        super().setData(slave, transaction, skip_encode)
+        self.records = records or []
+
+    def encode(self):
+        """Encode the request packet.
+
+        :returns: The byte encoded packet
+        """
+        packet = struct.pack("B", len(self.records) * 7)
+        for record in self.records:
+            packet += struct.pack(
+                ">BHHH",
+                0x06,
+                record.file_number,
+                record.record_number,
+                record.record_length,
+            )
+        return packet
+
+    def decode(self, data):
+        """Decode the incoming request.
+
+        :param data: The data to decode into the address
+        """
+        self.records = []
+        byte_count = int(data[0])
+        for count in range(1, byte_count, 7):
+            decoded = struct.unpack(">BHHH", data[count : count + 7])
+            record = FileRecord(
+                file_number=decoded[1],
+                record_number=decoded[2],
+                record_length=decoded[3],
+            )
+            if decoded[0] == 0x06:  # pragma: no cover
+                self.records.append(record)
+
+    def update_datastore(self, _context):  # pragma: no cover
+        """Run a read exception status request against the store.
+
+        :returns: The populated response
+        """
+        # TODO do some new context operation here # pylint: disable=fixme
+        # if file number, record number, or address + length
+        # is too big, return an error.
+        files: list[FileRecord] = []
+        return ReadFileRecordResponse(files)
+
+
+class ReadFileRecordResponse(ModbusPDU):
+    """Read file record response.
+
+    The normal response is a series of "sub-responses," one for each
+    "sub-request." The byte count field is the total combined count of
+    bytes in all "sub-responses." In addition, each "sub-response"
+    contains a field that shows its own byte count.
+    """
+
+    function_code = 0x14
+    _rtu_byte_count_pos = 2
+
+    def __init__(self, records=None, slave=1, transaction=0, skip_encode=False):
+        """Initialize a new instance.
+
+        :param records: The requested file records
+        """
+        super().__init__()
+        super().setData(slave, transaction, skip_encode)
+        self.records = records or []
+
+    def encode(self):
+        """Encode the response.
+
+        :returns: The byte encoded message
+        """
+        total = sum(record.response_length + 1 for record in self.records)
+        packet = struct.pack("B", total)
+        for record in self.records:
+            packet += struct.pack(">BB", record.response_length, 0x06)
+            packet += record.record_data
+        return packet
+
+    def decode(self, data):
+        """Decode the response.
+
+        :param data: The packet data to decode
+        """
+        count, self.records = 1, []
+        byte_count = int(data[0])
+        while count < byte_count:
+            response_length, reference_type = struct.unpack(
+                ">BB", data[count : count + 2]
+            )
+            count += 2
+
+            record_length = response_length - 1 # response length includes the type byte
+            record = FileRecord(
+                response_length=response_length,
+                record_data=data[count : count + record_length],
+            )
+            count += record_length
+            if reference_type == 0x06:  # pragma: no cover
+                self.records.append(record)
+
+
+class WriteFileRecordRequest(ModbusPDU):
+    """Write file record request.
+
+    This function code is used to perform a file record write. All
+    request data lengths are provided in terms of number of bytes
+    and all record lengths are provided in terms of the number of 16
+    bit words.
+    """
+
+    function_code = 0x15
+    function_code_name = "write_file_record"
+    _rtu_byte_count_pos = 2
+
+    def __init__(self, records=None, slave=1, transaction=0, skip_encode=False):
+        """Initialize a new instance.
+
+        :param records: The file record requests to be read
+        """
+        super().__init__()
+        super().setData(slave, transaction, skip_encode)
+        self.records = records or []
+
+    def encode(self):
+        """Encode the request packet.
+
+        :returns: The byte encoded packet
+        """
+        total_length = sum((record.record_length * 2) + 7 for record in self.records)
+        packet = struct.pack("B", total_length)
+
+        for record in self.records:
+            packet += struct.pack(
+                ">BHHH",
+                0x06,
+                record.file_number,
+                record.record_number,
+                record.record_length,
+            )
+            packet += record.record_data
+        return packet
+
+    def decode(self, data):
+        """Decode the incoming request.
+
+        :param data: The data to decode into the address
+        """
+        byte_count = int(data[0])
+        count, self.records = 1, []
+        while count < byte_count:
+            decoded = struct.unpack(">BHHH", data[count : count + 7])
+            response_length = decoded[3] * 2
+            count += response_length + 7
+            record = FileRecord(
+                record_length=decoded[3],
+                file_number=decoded[1],
+                record_number=decoded[2],
+                record_data=data[count - response_length : count],
+            )
+            if decoded[0] == 0x06:  # pragma: no cover
+                self.records.append(record)
+
+    def update_datastore(self, _context):  # pragma: no cover
+        """Run the write file record request against the context.
+
+        :returns: The populated response
+        """
+        # TODO do some new context operation here # pylint: disable=fixme
+        # if file number, record number, or address + length
+        # is too big, return an error.
+        return WriteFileRecordResponse(self.records)
+
+
+class WriteFileRecordResponse(ModbusPDU):
+    """The normal response is an echo of the request."""
+
+    function_code = 0x15
+    _rtu_byte_count_pos = 2
+
+    def __init__(self, records=None, slave=1, transaction=0, skip_encode=False):
+        """Initialize a new instance.
+
+        :param records: The file record requests to be read
+        """
+        super().__init__()
+        super().setData(slave, transaction, skip_encode)
+        self.records = records or []
+
+    def encode(self):
+        """Encode the response.
+
+        :returns: The byte encoded message
+        """
+        total_length = sum((record.record_length * 2) + 7 for record in self.records)
+        packet = struct.pack("B", total_length)
+        for record in self.records:
+            packet += struct.pack(
+                ">BHHH",
+                0x06,
+                record.file_number,
+                record.record_number,
+                record.record_length,
+            )
+            packet += record.record_data
+        return packet
+
+    def decode(self, data):
+        """Decode the incoming request.
+
+        :param data: The data to decode into the address
+        """
+        count, self.records = 1, []
+        byte_count = int(data[0])
+        while count < byte_count:
+            decoded = struct.unpack(">BHHH", data[count : count + 7])
+            response_length = decoded[3] * 2
+            count += response_length + 7
+            record = FileRecord(
+                record_length=decoded[3],
+                file_number=decoded[1],
+                record_number=decoded[2],
+                record_data=data[count - response_length : count],
+            )
+            if decoded[0] == 0x06:  # pragma: no cover
+                self.records.append(record)
+
+
+class ReadFifoQueueRequest(ModbusPDU):
+    """Read fifo queue request.
+
+    This function code allows to read the contents of a First-In-First-Out
+    (FIFO) queue of register in a remote device. The function returns a
+    count of the registers in the queue, followed by the queued data.
+    Up to 32 registers can be read: the count, plus up to 31 queued data
+    registers.
+
+    The queue count register is returned first, followed by the queued data
+    registers.  The function reads the queue contents, but does not clear
+    them.
+    """
+
+    function_code = 0x18
+    function_code_name = "read_fifo_queue"
+    _rtu_frame_size = 6
+
+    def __init__(self, address=0x0000, slave=1, transaction=0, skip_encode=False):
+        """Initialize a new instance.
+
+        :param address: The fifo pointer address (0x0000 to 0xffff)
+        """
+        super().__init__()
+        super().setData(slave, transaction, skip_encode)
+        self.address = address
+        self.values = []  # this should be added to the context
+
+    def encode(self):
+        """Encode the request packet.
+
+        :returns: The byte encoded packet
+        """
+        return struct.pack(">H", self.address)
+
+    def decode(self, data):
+        """Decode the incoming request.
+
+        :param data: The data to decode into the address
+        """
+        self.address = struct.unpack(">H", data)[0]
+
+    def update_datastore(self, _context):  # pragma: no cover
+        """Run a read exception status request against the store.
+
+        :returns: The populated response
+        """
+        if not 0x0000 <= self.address <= 0xFFFF:
+            return self.doException(merror.IllegalValue)
+        if len(self.values) > 31:
+            return self.doException(merror.IllegalValue)
+        # TODO pull the values from some context # pylint: disable=fixme
+        return ReadFifoQueueResponse(self.values)
+
+
+class ReadFifoQueueResponse(ModbusPDU):
+    """Read Fifo queue response.
+
+    In a normal response, the byte count shows the quantity of bytes to
+    follow, including the queue count bytes and value register bytes
+    (but not including the error check field).  The queue count is the
+    quantity of data registers in the queue (not including the count register).
+
+    If the queue count exceeds 31, an exception response is returned with an
+    error code of 03 (Illegal Data Value).
+    """
+
+    function_code = 0x18
+
+    @classmethod
+    def calculateRtuFrameSize(cls, buffer):  # pragma: no cover
+        """Calculate the size of the message.
+
+        :param buffer: A buffer containing the data that have been received.
+        :returns: The number of bytes in the response.
+        """
+        hi_byte = int(buffer[2])
+        lo_byte = int(buffer[3])
+        return (hi_byte << 16) + lo_byte + 6
+
+    def __init__(self, values=None, slave=1, transaction=0, skip_encode=False):
+        """Initialize a new instance.
+
+        :param values: The list of values of the fifo to return
+        """
+        super().__init__()
+        super().setData(slave, transaction, skip_encode)
+        self.values = values or []
+
+    def encode(self):
+        """Encode the response.
+
+        :returns: The byte encoded message
+        """
+        length = len(self.values) * 2
+        packet = struct.pack(">HH", 2 + length, length)
+        for value in self.values:
+            packet += struct.pack(">H", value)
+        return packet
+
+    def decode(self, data):
+        """Decode a the response.
+
+        :param data: The packet data to decode
+        """
+        self.values = []
+        _, count = struct.unpack(">HH", data[0:4])
+        for index in range(0, count - 4):  # pragma: no cover
+            idx = 4 + index * 2
+            self.values.append(struct.unpack(">H", data[idx : idx + 2])[0])
diff --git a/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/pdu/mei_message.py b/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/pdu/mei_message.py
new file mode 100644
index 00000000..0085f23d
--- /dev/null
+++ b/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/pdu/mei_message.py
@@ -0,0 +1,215 @@
+"""Encapsulated Interface (MEI) Transport Messages."""
+
+
+# pylint: disable=missing-type-doc
+import struct
+
+from pymodbus.constants import DeviceInformation, MoreData
+from pymodbus.device import DeviceInformationFactory, ModbusControlBlock
+from pymodbus.pdu.pdu import ModbusExceptions as merror
+from pymodbus.pdu.pdu import ModbusPDU
+
+
+_MCB = ModbusControlBlock()
+
+
+class _OutOfSpaceException(Exception):
+    """Internal out of space exception."""
+
+    # This exception exists here as a simple, local way to manage response
+    # length control for the only MODBUS command which requires it under
+    # standard, non-error conditions. It and the structures associated with
+    # it should ideally be refactored and applied to all responses, however,
+    # since a Client can make requests which result in disallowed conditions,
+    # such as, for instance, requesting a register read of more registers
+    # than will fit in a single PDU. As per the specification, the PDU is
+    # restricted to 253 bytes, irrespective of the transport used.
+    #
+    # See Page 5/50 of MODBUS Application Protocol Specification V1.1b3.
+
+    def __init__(self, oid):  # pragma: no cover
+        self.oid = oid
+        super().__init__()
+
+
+# ---------------------------------------------------------------------------#
+#  Read Device Information
+# ---------------------------------------------------------------------------#
+class ReadDeviceInformationRequest(ModbusPDU):
+    """Read device information.
+
+    This function code allows reading the identification and additional
+    information relative to the physical and functional description of a
+    remote device, only.
+
+    The Read Device Identification interface is modeled as an address space
+    composed of a set of addressable data elements. The data elements are
+    called objects and an object Id identifies them.
+    """
+
+    function_code = 0x2B
+    sub_function_code = 0x0E
+    function_code_name = "read_device_information"
+    _rtu_frame_size = 7
+
+    def __init__(self, read_code=None, object_id=0x00, slave=1, transaction=0, skip_encode=False):
+        """Initialize a new instance.
+
+        :param read_code: The device information read code
+        :param object_id: The object to read from
+        """
+        super().__init__()
+        super().setData(slave, transaction, skip_encode)
+        self.read_code = read_code or DeviceInformation.BASIC
+        self.object_id = object_id
+
+    def encode(self):
+        """Encode the request packet.
+
+        :returns: The byte encoded packet
+        """
+        packet = struct.pack(
+            ">BBB", self.sub_function_code, self.read_code, self.object_id
+        )
+        return packet
+
+    def decode(self, data):
+        """Decode data part of the message.
+
+        :param data: The incoming data
+        """
+        params = struct.unpack(">BBB", data)
+        self.sub_function_code, self.read_code, self.object_id = params
+
+    async def update_datastore(self, _context):  # pragma: no cover
+        """Run a read exception status request against the store.
+
+        :returns: The populated response
+        """
+        if not 0x00 <= self.object_id <= 0xFF:
+            return self.doException(merror.IllegalValue)
+        if not 0x00 <= self.read_code <= 0x04:
+            return self.doException(merror.IllegalValue)
+
+        information = DeviceInformationFactory.get(_MCB, self.read_code, self.object_id)
+        return ReadDeviceInformationResponse(self.read_code, information)
+
+    def __str__(self):
+        """Build a representation of the request.
+
+        :returns: The string representation of the request
+        """
+        params = (self.read_code, self.object_id)
+        return (
+            "ReadDeviceInformationRequest(%d,%d)"  # pylint: disable=consider-using-f-string
+            % params
+        )
+
+
+class ReadDeviceInformationResponse(ModbusPDU):
+    """Read device information response."""
+
+    function_code = 0x2B
+    sub_function_code = 0x0E
+
+    @classmethod
+    def calculateRtuFrameSize(cls, buffer):  # pragma: no cover
+        """Calculate the size of the message.
+
+        :param buffer: A buffer containing the data that have been received.
+        :returns: The number of bytes in the response.
+        """
+        size = 8  # skip the header information
+        count = int(buffer[7])
+
+        try:
+            while count > 0:
+                _, object_length = struct.unpack(">BB", buffer[size : size + 2])
+                size += object_length + 2
+                count -= 1
+            return size + 2
+        except struct.error as exc:
+            raise IndexError from exc
+
+    def __init__(self, read_code=None, information=None, slave=1, transaction=0, skip_encode=False):
+        """Initialize a new instance.
+
+        :param read_code: The device information read code
+        :param information: The requested information request
+        """
+        super().__init__()
+        super().setData(slave, transaction, skip_encode)
+        self.read_code = read_code or DeviceInformation.BASIC
+        self.information = information or {}
+        self.number_of_objects = 0
+        self.conformity = 0x83  # I support everything right now
+        self.next_object_id = 0x00
+        self.more_follows = MoreData.NOTHING
+        self.space_left = 253 - 6
+
+    def _encode_object(self, object_id, data):  # pragma: no cover
+        """Encode object."""
+        self.space_left -= 2 + len(data)
+        if self.space_left <= 0:
+            raise _OutOfSpaceException(object_id)
+        encoded_obj = struct.pack(">BB", object_id, len(data))
+        if isinstance(data, bytes):
+            encoded_obj += data
+        else:
+            encoded_obj += data.encode()
+        self.number_of_objects += 1
+        return encoded_obj
+
+    def encode(self):
+        """Encode the response.
+
+        :returns: The byte encoded message
+        """
+        packet = struct.pack(
+            ">BBB", self.sub_function_code, self.read_code, self.conformity
+        )
+        objects = b""
+        try:  # pragma: no cover
+            for object_id, data in iter(self.information.items()):
+                if isinstance(data, list):
+                    for item in data:
+                        objects += self._encode_object(object_id, item)
+                else:
+                    objects += self._encode_object(object_id, data)
+        except _OutOfSpaceException as exc:  # pragma: no cover
+            self.next_object_id = exc.oid
+            self.more_follows = MoreData.KEEP_READING
+
+        packet += struct.pack(
+            ">BBB", self.more_follows, self.next_object_id, self.number_of_objects
+        )
+        packet += objects
+        return packet
+
+    def decode(self, data):
+        """Decode a the response.
+
+        :param data: The packet data to decode
+        """
+        params = struct.unpack(">BBBBBB", data[0:6])
+        self.sub_function_code, self.read_code = params[0:2]
+        self.conformity, self.more_follows = params[2:4]
+        self.next_object_id, self.number_of_objects = params[4:6]
+        self.information, count = {}, 6  # skip the header information
+
+        while count < len(data):
+            object_id, object_length = struct.unpack(">BB", data[count : count + 2])
+            count += object_length + 2
+            if object_id not in self.information:  # pragma: no cover
+                self.information[object_id] = data[count - object_length : count]
+            elif isinstance(self.information[object_id], list):  # pragma: no cover
+                self.information[object_id].append(data[count - object_length : count])
+            else:
+                self.information[object_id] = [  # pragma: no cover
+                    self.information[object_id],
+                    data[count - object_length : count],
+                ]
+
+    def __str__(self):
+        """Build a representation of the response."""
+        return f"ReadDeviceInformationResponse({self.read_code})"
diff --git a/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/pdu/other_message.py b/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/pdu/other_message.py
new file mode 100644
index 00000000..597a37bd
--- /dev/null
+++ b/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/pdu/other_message.py
@@ -0,0 +1,469 @@
+"""Diagnostic record read/write.
+
+Currently not all implemented
+"""
+
+
+# pylint: disable=missing-type-doc
+import struct
+
+from pymodbus.constants import ModbusStatus
+from pymodbus.device import DeviceInformationFactory, ModbusControlBlock
+from pymodbus.pdu.pdu import ModbusPDU
+
+
+_MCB = ModbusControlBlock()
+
+
+# ---------------------------------------------------------------------------#
+#  TODO Make these only work on serial # pylint: disable=fixme
+# ---------------------------------------------------------------------------#
+class ReadExceptionStatusRequest(ModbusPDU):
+    """This function code is used to read the contents of eight Exception Status outputs in a remote device.
+
+    The function provides a simple method for
+    accessing this information, because the Exception Output references are
+    known (no output reference is needed in the function).
+    """
+
+    function_code = 0x07
+    function_code_name = "read_exception_status"
+    _rtu_frame_size = 4
+
+    def __init__(self, slave=None, transaction=0, skip_encode=0):
+        """Initialize a new instance."""
+        super().__init__()
+        super().setData(slave, transaction, skip_encode)
+
+    def encode(self):
+        """Encode the message."""
+        return b""
+
+    def decode(self, data):
+        """Decode data part of the message.
+
+        :param data: The incoming data
+        """
+
+    async def update_datastore(self, _context=None):  # pragma: no cover
+        """Run a read exception status request against the store.
+
+        :returns: The populated response
+        """
+        status = _MCB.Counter.summary()
+        return ReadExceptionStatusResponse(status)
+
+    def __str__(self):
+        """Build a representation of the request."""
+        return f"ReadExceptionStatusRequest({self.function_code})"
+
+
+class ReadExceptionStatusResponse(ModbusPDU):
+    """The normal response contains the status of the eight Exception Status outputs.
+
+    The outputs are packed into one data byte, with one bit
+    per output. The status of the lowest output reference is contained
+    in the least significant bit of the byte.  The contents of the eight
+    Exception Status outputs are device specific.
+    """
+
+    function_code = 0x07
+    _rtu_frame_size = 5
+
+    def __init__(self, status=0x00, slave=1, transaction=0, skip_encode=False):
+        """Initialize a new instance.
+
+        :param status: The status response to report
+        """
+        super().__init__()
+        super().setData(slave, transaction, skip_encode)
+        self.status = status if status < 256 else 255
+
+    def encode(self):
+        """Encode the response.
+
+        :returns: The byte encoded message
+        """
+        return struct.pack(">B", self.status)
+
+    def decode(self, data):
+        """Decode a the response.
+
+        :param data: The packet data to decode
+        """
+        self.status = int(data[0])
+
+    def __str__(self):
+        """Build a representation of the response."""
+        arguments = (self.function_code, self.status)
+        return (
+            "ReadExceptionStatusResponse(%d, %s)"  # pylint: disable=consider-using-f-string
+            % arguments
+        )
+
+
+# Encapsulate interface transport 43, 14
+# CANopen general reference 43, 13
+
+
+# ---------------------------------------------------------------------------#
+#  TODO Make these only work on serial # pylint: disable=fixme
+# ---------------------------------------------------------------------------#
+class GetCommEventCounterRequest(ModbusPDU):
+    """This function code is used to get a status word.
+
+    And an event count from the remote device's communication event counter.
+
+    By fetching the current count before and after a series of messages, a
+    client can determine whether the messages were handled normally by the
+    remote device.
+
+    The device's event counter is incremented once  for each successful
+    message completion. It is not incremented for exception responses,
+    poll commands, or fetch event counter commands.
+
+    The event counter can be reset by means of the Diagnostics function
+    (code 08), with a subfunction of Restart Communications Option
+    (code 00 01) or Clear Counters and Diagnostic Register (code 00 0A).
+    """
+
+    function_code = 0x0B
+    function_code_name = "get_event_counter"
+    _rtu_frame_size = 4
+
+    def __init__(self, slave=1, transaction=0, skip_encode=False):
+        """Initialize a new instance."""
+        super().__init__()
+        super().setData(slave, transaction, skip_encode)
+
+    def encode(self):
+        """Encode the message."""
+        return b""
+
+    def decode(self, data):
+        """Decode data part of the message.
+
+        :param data: The incoming data
+        """
+
+    async def update_datastore(self, _context=None):  # pragma: no cover
+        """Run a read exception status request against the store.
+
+        :returns: The populated response
+        """
+        status = _MCB.Counter.Event
+        return GetCommEventCounterResponse(status)
+
+    def __str__(self):
+        """Build a representation of the request."""
+        return f"GetCommEventCounterRequest({self.function_code})"
+
+
+class GetCommEventCounterResponse(ModbusPDU):
+    """Get comm event counter response.
+
+    The normal response contains a two-byte status word, and a two-byte
+    event count. The status word will be all ones (FF FF hex) if a
+    previously-issued program command is still being processed by the
+    remote device (a busy condition exists). Otherwise, the status word
+    will be all zeros.
+    """
+
+    function_code = 0x0B
+    _rtu_frame_size = 8
+
+    def __init__(self, count=0x0000, slave=1, transaction=0, skip_encode=False):
+        """Initialize a new instance.
+
+        :param count: The current event counter value
+        """
+        super().__init__()
+        super().setData(slave, transaction, skip_encode)
+        self.count = count
+        self.status = True  # this means we are ready, not waiting
+
+    def encode(self):
+        """Encode the response.
+
+        :returns: The byte encoded message
+        """
+        if self.status:  # pragma: no cover
+            ready = ModbusStatus.READY
+        else:
+            ready = ModbusStatus.WAITING  # pragma: no cover
+        return struct.pack(">HH", ready, self.count)
+
+    def decode(self, data):
+        """Decode a the response.
+
+        :param data: The packet data to decode
+        """
+        ready, self.count = struct.unpack(">HH", data)
+        self.status = ready == ModbusStatus.READY
+
+    def __str__(self):
+        """Build a representation of the response."""
+        arguments = (self.function_code, self.count, self.status)
+        return (
+            "GetCommEventCounterResponse(%d, %d, %d)"  # pylint: disable=consider-using-f-string
+            % arguments
+        )
+
+
+# ---------------------------------------------------------------------------#
+#  TODO Make these only work on serial # pylint: disable=fixme
+# ---------------------------------------------------------------------------#
+class GetCommEventLogRequest(ModbusPDU):
+    """This function code is used to get a status word.
+
+    Event count, message count, and a field of event bytes from the remote device.
+
+    The status word and event counts are identical  to that returned by
+    the Get Communications Event Counter function (11, 0B hex).
+
+    The message counter contains the quantity of  messages processed by the
+    remote device since its last restart, clear counters operation, or
+    power-up.  This count is identical to that returned by the Diagnostic
+    function (code 08), sub-function Return Bus Message Count (code 11,
+    0B hex).
+
+    The event bytes field contains 0-64 bytes, with each byte corresponding
+    to the status of one MODBUS send or receive operation for the remote
+    device.  The remote device enters the events into the field in
+    chronological order.  Byte 0 is the most recent event. Each new byte
+    flushes the oldest byte from the field.
+    """
+
+    function_code = 0x0C
+    function_code_name = "get_event_log"
+    _rtu_frame_size = 4
+
+    def __init__(self, slave=1, transaction=0, skip_encode=False):
+        """Initialize a new instance."""
+        super().__init__()
+        super().setData(slave, transaction, skip_encode)
+
+    def encode(self):
+        """Encode the message."""
+        return b""
+
+    def decode(self, data):
+        """Decode data part of the message.
+
+        :param data: The incoming data
+        """
+
+    async def update_datastore(self, _context=None):  # pragma: no cover
+        """Run a read exception status request against the store.
+
+        :returns: The populated response
+        """
+        results = {
+            "status": True,
+            "message_count": _MCB.Counter.BusMessage,
+            "event_count": _MCB.Counter.Event,
+            "events": _MCB.getEvents(),
+        }
+        return GetCommEventLogResponse(**results)
+
+    def __str__(self):
+        """Build a representation of the request.
+
+        :returns: The string representation of the request
+        """
+        return f"GetCommEventLogRequest({self.function_code})"
+
+
+class GetCommEventLogResponse(ModbusPDU):
+    """Get Comm event log response.
+
+    The normal response contains a two-byte status word field,
+    a two-byte event count field, a two-byte message count field,
+    and a field containing 0-64 bytes of events. A byte count
+    field defines the total length of the data in these four field
+    """
+
+    function_code = 0x0C
+    _rtu_byte_count_pos = 2
+
+    def __init__(self, status=True, message_count=0, event_count=0, events=None, slave=1, transaction=0, skip_encode=False):
+        """Initialize a new instance.
+
+        :param status: The status response to report
+        :param message_count: The current message count
+        :param event_count: The current event count
+        :param events: The collection of events to send
+        """
+        super().__init__()
+        super().setData(slave, transaction, skip_encode)
+        self.status = status
+        self.message_count = message_count
+        self.event_count = event_count
+        self.events = events if events else []
+
+    def encode(self):
+        """Encode the response.
+
+        :returns: The byte encoded message
+        """
+        if self.status:  # pragma: no cover
+            ready = ModbusStatus.READY
+        else:
+            ready = ModbusStatus.WAITING  # pragma: no cover
+        packet = struct.pack(">B", 6 + len(self.events))
+        packet += struct.pack(">H", ready)
+        packet += struct.pack(">HH", self.event_count, self.message_count)
+        packet += b"".join(struct.pack(">B", e) for e in self.events)
+        return packet
+
+    def decode(self, data):
+        """Decode a the response.
+
+        :param data: The packet data to decode
+        """
+        length = int(data[0])
+        status = struct.unpack(">H", data[1:3])[0]
+        self.status = status == ModbusStatus.READY
+        self.event_count = struct.unpack(">H", data[3:5])[0]
+        self.message_count = struct.unpack(">H", data[5:7])[0]
+
+        self.events = []
+        for i in range(7, length + 1):
+            self.events.append(int(data[i]))
+
+    def __str__(self):
+        """Build a representation of the response.
+
+        :returns: The string representation of the response
+        """
+        arguments = (
+            self.function_code,
+            self.status,
+            self.message_count,
+            self.event_count,
+        )
+        return (
+            "GetCommEventLogResponse(%d, %d, %d, %d)"  # pylint: disable=consider-using-f-string
+            % arguments
+        )
+
+
+# ---------------------------------------------------------------------------#
+#  TODO Make these only work on serial # pylint: disable=fixme
+# ---------------------------------------------------------------------------#
+class ReportSlaveIdRequest(ModbusPDU):
+    """This function code is used to read the description of the type.
+
+    The current status, and other information specific to a remote device.
+    """
+
+    function_code = 0x11
+    function_code_name = "report_slave_id"
+    _rtu_frame_size = 4
+
+    def __init__(self, slave=1, transaction=0, skip_encode=False):
+        """Initialize a new instance.
+
+        :param slave: Modbus slave slave ID
+
+        """
+        super().__init__()
+        super().setData(slave, transaction, skip_encode)
+
+    def encode(self):
+        """Encode the message."""
+        return b""
+
+    def decode(self, data):
+        """Decode data part of the message.
+
+        :param data: The incoming data
+        """
+
+    async def update_datastore(self, context=None):  # pragma: no cover
+        """Run a report slave id request against the store.
+
+        :returns: The populated response
+        """
+        report_slave_id_data = None
+        if context:
+            report_slave_id_data = getattr(context, "reportSlaveIdData", None)
+        if not report_slave_id_data:
+            information = DeviceInformationFactory.get(_MCB)
+
+            # Support identity values as bytes data and regular str data
+            id_data = []
+            for v_item in information.values():
+                if isinstance(v_item, bytes):
+                    id_data.append(v_item)
+                else:
+                    id_data.append(v_item.encode())
+
+            identifier = b"-".join(id_data)
+            identifier = identifier or b"Pymodbus"
+            report_slave_id_data = identifier
+        return ReportSlaveIdResponse(report_slave_id_data)
+
+    def __str__(self):
+        """Build a representation of the request.
+
+        :returns: The string representation of the request
+        """
+        return f"ReportSlaveIdRequest({self.function_code})"
+
+
+class ReportSlaveIdResponse(ModbusPDU):
+    """Show response.
+
+    The data contents are specific to each type of device.
+    """
+
+    function_code = 0x11
+    _rtu_byte_count_pos = 2
+
+    def __init__(self, identifier=b"\x00", status=True, slave=1, transaction=0, skip_encode=False):
+        """Initialize a new instance.
+
+        :param identifier: The identifier of the slave
+        :param status: The status response to report
+        """
+        super().__init__()
+        super().setData(slave, transaction, skip_encode)
+        self.identifier = identifier
+        self.status = status
+        self.byte_count = None
+
+    def encode(self):
+        """Encode the response.
+
+        :returns: The byte encoded message
+        """
+        if self.status:  # pragma: no cover
+            status = ModbusStatus.SLAVE_ON
+        else:
+            status = ModbusStatus.SLAVE_OFF  # pragma: no cover
+        length = len(self.identifier) + 1
+        packet = struct.pack(">B", length)
+        packet += self.identifier  # we assume it is already encoded
+        packet += struct.pack(">B", status)
+        return packet
+
+    def decode(self, data):
+        """Decode a the response.
+
+        Since the identifier is device dependent, we just return the
+        raw value that a user can decode to whatever it should be.
+
+        :param data: The packet data to decode
+        """
+        self.byte_count = int(data[0])
+        self.identifier = data[1 : self.byte_count + 1]
+        status = int(data[-1])
+        self.status = status == ModbusStatus.SLAVE_ON
+
+    def __str__(self) -> str:
+        """Build a representation of the response.
+
+        :returns: The string representation of the response
+        """
+        return f"ReportSlaveIdResponse({self.function_code}, {self.identifier}, {self.status})"
diff --git a/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/pdu/pdu.py b/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/pdu/pdu.py
new file mode 100644
index 00000000..5a07014e
--- /dev/null
+++ b/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/pdu/pdu.py
@@ -0,0 +1,128 @@
+"""Contains base classes for modbus request/response/error packets."""
+from __future__ import annotations
+
+import asyncio
+import struct
+from abc import abstractmethod
+
+from pymodbus.exceptions import NotImplementedException
+from pymodbus.logging import Log
+
+
+class ModbusPDU:
+    """Base class for all Modbus messages."""
+
+    function_code: int = 0
+    sub_function_code: int = -1
+    _rtu_frame_size: int = 0
+    _rtu_byte_count_pos: int = 0
+
+    def __init__(self) -> None:
+        """Initialize the base data for a modbus request."""
+        self.transaction_id: int
+        self.slave_id: int
+        self.skip_encode: bool
+        self.bits: list[bool]
+        self.registers: list[int]
+        self.fut: asyncio.Future
+
+    def setData(self, slave: int, transaction: int, skip_encode: bool) -> None:
+        """Set data common for all PDU."""
+        self.transaction_id = transaction
+        self.slave_id = slave
+        self.skip_encode = skip_encode
+
+    def doException(self, exception: int) -> ExceptionResponse:
+        """Build an error response based on the function."""
+        exc = ExceptionResponse(self.function_code, exception)
+        Log.error("Exception response {}", exc)
+        return exc
+
+    def isError(self) -> bool:
+        """Check if the error is a success or failure."""
+        return self.function_code > 0x80
+
+    def get_response_pdu_size(self) -> int:
+        """Calculate response pdu size."""
+        return 0
+
+    @abstractmethod
+    def encode(self) -> bytes:
+        """Encode the message."""
+
+    @abstractmethod
+    def decode(self, data: bytes) -> None:
+        """Decode data part of the message."""
+
+
+    @classmethod
+    def calculateRtuFrameSize(cls, data: bytes) -> int:
+        """Calculate the size of a PDU."""
+        if cls._rtu_frame_size:
+            return cls._rtu_frame_size
+        if cls._rtu_byte_count_pos:
+            if len(data) < cls._rtu_byte_count_pos +1:
+                return 0
+            return int(data[cls._rtu_byte_count_pos]) + cls._rtu_byte_count_pos + 3
+        raise NotImplementedException(
+            f"Cannot determine RTU frame size for {cls.__name__}"
+        )
+
+
+class ModbusExceptions:  # pylint: disable=too-few-public-methods
+    """An enumeration of the valid modbus exceptions."""
+
+    IllegalFunction = 0x01
+    IllegalAddress = 0x02
+    IllegalValue = 0x03
+    SlaveFailure = 0x04
+    Acknowledge = 0x05
+    SlaveBusy = 0x06
+    NegativeAcknowledge = 0x07
+    MemoryParityError = 0x08
+    GatewayPathUnavailable = 0x0A
+    GatewayNoResponse = 0x0B
+
+    @classmethod
+    def decode(cls, code: int) -> str | None:
+        """Give an error code, translate it to a string error name."""
+        values = {
+            v: k
+            for k, v in iter(cls.__dict__.items())
+            if not k.startswith("__") and not callable(v)
+        }
+        return values.get(code, None)
+
+
+class ExceptionResponse(ModbusPDU):
+    """Base class for a modbus exception PDU."""
+
+    _rtu_frame_size = 5
+
+    def __init__(
+            self,
+            function_code: int,
+            exception_code: int = 0,
+            slave: int = 1,
+            transaction: int = 0,
+            skip_encode: bool = False) -> None:
+        """Initialize the modbus exception response."""
+        super().__init__()
+        super().setData(slave, transaction, skip_encode)
+        self.function_code = function_code | 0x80
+        self.exception_code = exception_code
+
+    def encode(self) -> bytes:
+        """Encode a modbus exception response."""
+        return struct.pack(">B", self.exception_code)
+
+    def decode(self, data: bytes) -> None:
+        """Decode a modbus exception response."""
+        self.exception_code = int(data[0])
+
+    def __str__(self) -> str:
+        """Build a representation of an exception response."""
+        message = ModbusExceptions.decode(self.exception_code)
+        return (
+            f"Exception Response({self.function_code}, {self.function_code - 0x80}, {message})"
+        )
diff --git a/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/pdu/register_read_message.py b/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/pdu/register_read_message.py
new file mode 100644
index 00000000..524f0509
--- /dev/null
+++ b/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/pdu/register_read_message.py
@@ -0,0 +1,376 @@
+"""Register Reading Request/Response."""
+
+
+# pylint: disable=missing-type-doc
+import struct
+
+from pymodbus.exceptions import ModbusIOException
+from pymodbus.pdu.pdu import ExceptionResponse, ModbusPDU
+from pymodbus.pdu.pdu import ModbusExceptions as merror
+
+
+class ReadRegistersRequestBase(ModbusPDU):
+    """Base class for reading a modbus register."""
+
+    _rtu_frame_size = 8
+
+    def __init__(self, address, count, slave=1, transaction=0, skip_encode=False):
+        """Initialize a new instance.
+
+        :param address: The address to start the read from
+        :param count: The number of registers to read
+        :param slave: Modbus slave slave ID
+        """
+        super().__init__()
+        super().setData(slave, transaction, skip_encode)
+        self.address = address
+        self.count = count
+
+    def encode(self):
+        """Encode the request packet.
+
+        :return: The encoded packet
+        """
+        return struct.pack(">HH", self.address, self.count)
+
+    def decode(self, data):
+        """Decode a register request packet.
+
+        :param data: The request to decode
+        """
+        self.address, self.count = struct.unpack(">HH", data)
+
+    def get_response_pdu_size(self):
+        """Get response pdu size.
+
+        Func_code (1 byte) + Byte Count(1 byte) + 2 * Quantity of Coils (n Bytes).
+        """
+        return 1 + 1 + 2 * self.count
+
+    def __str__(self):
+        """Return a string representation of the instance.
+
+        :returns: A string representation of the instance
+        """
+        return f"{self.__class__.__name__} ({self.address},{self.count})"
+
+
+class ReadRegistersResponseBase(ModbusPDU):
+    """Base class for responding to a modbus register read.
+
+    The requested registers can be found in the .registers list.
+    """
+
+    _rtu_byte_count_pos = 2
+
+    def __init__(self, values, slave=1, transaction=0, skip_encode=False):
+        """Initialize a new instance.
+
+        :param values: The values to write to
+        :param slave: Modbus slave slave ID
+        """
+        super().__init__()
+        super().setData(slave, transaction, skip_encode)
+
+        #: A list of register values
+        self.registers = values or []
+
+    def encode(self):
+        """Encode the response packet.
+
+        :returns: The encoded packet
+        """
+        result = struct.pack(">B", len(self.registers) * 2)
+        for register in self.registers:
+            result += struct.pack(">H", register)
+        return result
+
+    def decode(self, data):
+        """Decode a register response packet.
+
+        :param data: The request to decode
+        """
+        byte_count = int(data[0])
+        if byte_count < 2 or byte_count > 252 or byte_count % 2 == 1 or byte_count != len(data) - 1:  # pragma: no cover
+            raise ModbusIOException(f"Invalid response {data} has byte count of {byte_count}")  # pragma: no cover
+        self.registers = []
+        for i in range(1, byte_count + 1, 2):
+            self.registers.append(struct.unpack(">H", data[i : i + 2])[0])
+
+    def getRegister(self, index):
+        """Get the requested register.
+
+        :param index: The indexed register to retrieve
+        :returns: The request register
+        """
+        return self.registers[index]  # pragma: no cover
+
+    def __str__(self):
+        """Return a string representation of the instance.
+
+        :returns: A string representation of the instance
+        """
+        return f"{self.__class__.__name__} ({len(self.registers)})"
+
+
+class ReadHoldingRegistersRequest(ReadRegistersRequestBase):
+    """Read holding registers.
+
+    This function code is used to read the contents of a contiguous block
+    of holding registers in a remote device. The Request PDU specifies the
+    starting register address and the number of registers. In the PDU
+    Registers are addressed starting at zero. Therefore registers numbered
+    1-16 are addressed as 0-15.
+    """
+
+    function_code = 3
+    function_code_name = "read_holding_registers"
+
+    def __init__(self, address=None, count=None, slave=1, transaction=0, skip_encode=0):
+        """Initialize a new instance of the request.
+
+        :param address: The starting address to read from
+        :param count: The number of registers to read from address
+        :param slave: Modbus slave slave ID
+        """
+        super().__init__(address, count, slave, transaction, skip_encode)
+
+    async def update_datastore(self, context):  # pragma: no cover
+        """Run a read holding request against a datastore.
+
+        :param context: The datastore to request from
+        :returns: An initialized :py:class:`~pymodbus.register_read_message.ReadHoldingRegistersResponse`
+        """
+        if not (1 <= self.count <= 0x7D):
+            return self.doException(merror.IllegalValue)
+        if not context.validate(self.function_code, self.address, self.count):
+            return self.doException(merror.IllegalAddress)
+        values = await context.async_getValues(
+            self.function_code, self.address, self.count
+        )
+        if isinstance(values, ExceptionResponse):
+            return values
+        return ReadHoldingRegistersResponse(values)
+
+
+class ReadHoldingRegistersResponse(ReadRegistersResponseBase):
+    """Read holding registers.
+
+    This function code is used to read the contents of a contiguous block
+    of holding registers in a remote device. The Request PDU specifies the
+    starting register address and the number of registers. In the PDU
+    Registers are addressed starting at zero. Therefore registers numbered
+    1-16 are addressed as 0-15.
+
+    The requested registers can be found in the .registers list.
+    """
+
+    function_code = 3
+
+    def __init__(self, values=None, slave=None, transaction=0, skip_encode=0):
+        """Initialize a new response instance.
+
+        :param values: The resulting register values
+        """
+        super().__init__(values, slave, transaction, skip_encode)
+
+
+class ReadInputRegistersRequest(ReadRegistersRequestBase):
+    """Read input registers.
+
+    This function code is used to read from 1 to approx. 125 contiguous
+    input registers in a remote device. The Request PDU specifies the
+    starting register address and the number of registers. In the PDU
+    Registers are addressed starting at zero. Therefore input registers
+    numbered 1-16 are addressed as 0-15.
+    """
+
+    function_code = 4
+    function_code_name = "read_input_registers"
+
+    def __init__(self, address=None, count=None, slave=1, transaction=0, skip_encode=0):
+        """Initialize a new instance of the request.
+
+        :param address: The starting address to read from
+        :param count: The number of registers to read from address
+        :param slave: Modbus slave slave ID
+        """
+        super().__init__(address, count, slave, transaction, skip_encode)
+
+    async def update_datastore(self, context):  # pragma: no cover
+        """Run a read input request against a datastore.
+
+        :param context: The datastore to request from
+        :returns: An initialized :py:class:`~pymodbus.register_read_message.ReadInputRegistersResponse`
+        """
+        if not (1 <= self.count <= 0x7D):
+            return self.doException(merror.IllegalValue)
+        if not context.validate(self.function_code, self.address, self.count):
+            return self.doException(merror.IllegalAddress)
+        values = await context.async_getValues(
+            self.function_code, self.address, self.count
+        )
+        if isinstance(values, ExceptionResponse):
+            return values
+        return ReadInputRegistersResponse(values)
+
+
+class ReadInputRegistersResponse(ReadRegistersResponseBase):
+    """Read/write input registers.
+
+    This function code is used to read from 1 to approx. 125 contiguous
+    input registers in a remote device. The Request PDU specifies the
+    starting register address and the number of registers. In the PDU
+    Registers are addressed starting at zero. Therefore input registers
+    numbered 1-16 are addressed as 0-15.
+
+    The requested registers can be found in the .registers list.
+    """
+
+    function_code = 4
+
+    def __init__(self, values=None, slave=None, transaction=0, skip_encode=0):
+        """Initialize a new response instance.
+
+        :param values: The resulting register values
+        """
+        super().__init__(values, slave, transaction, skip_encode)
+
+
+class ReadWriteMultipleRegistersRequest(ModbusPDU):
+    """Read/write multiple registers.
+
+    This function code performs a combination of one read operation and one
+    write operation in a single MODBUS transaction. The write
+    operation is performed before the read.
+
+    Holding registers are addressed starting at zero. Therefore holding
+    registers 1-16 are addressed in the PDU as 0-15.
+
+    The request specifies the starting address and number of holding
+    registers to be read as well as the starting address, number of holding
+    registers, and the data to be written. The byte count specifies the
+    number of bytes to follow in the write data field."
+    """
+
+    function_code = 23
+    function_code_name = "read_write_multiple_registers"
+    _rtu_byte_count_pos = 10
+
+    def __init__(self, read_address=0x00, read_count=0, write_address=0x00, write_registers=None, slave=1, transaction=0, skip_encode=False):
+        """Initialize a new request message.
+
+        :param read_address: The address to start reading from
+        :param read_count: The number of registers to read from address
+        :param write_address: The address to start writing to
+        :param write_registers: The registers to write to the specified address
+        """
+        super().__init__()
+        super().setData(slave, transaction, skip_encode)
+        self.read_address = read_address
+        self.read_count = read_count
+        self.write_address = write_address
+        self.write_registers = write_registers
+        if not hasattr(self.write_registers, "__iter__"):
+            self.write_registers = [self.write_registers]
+        self.write_count = len(self.write_registers)
+        self.write_byte_count = self.write_count * 2
+
+    def encode(self):
+        """Encode the request packet.
+
+        :returns: The encoded packet
+        """
+        result = struct.pack(
+            ">HHHHB",
+            self.read_address,
+            self.read_count,
+            self.write_address,
+            self.write_count,
+            self.write_byte_count,
+        )
+        for register in self.write_registers:
+            result += struct.pack(">H", register)
+        return result
+
+    def decode(self, data):
+        """Decode the register request packet.
+
+        :param data: The request to decode
+        """
+        (
+            self.read_address,
+            self.read_count,
+            self.write_address,
+            self.write_count,
+            self.write_byte_count,
+        ) = struct.unpack(">HHHHB", data[:9])
+        self.write_registers = []
+        for i in range(9, self.write_byte_count + 9, 2):
+            register = struct.unpack(">H", data[i : i + 2])[0]
+            self.write_registers.append(register)
+
+    async def update_datastore(self, context):  # pragma: no cover
+        """Run a write single register request against a datastore.
+
+        :param context: The datastore to request from
+        :returns: An initialized :py:class:`~pymodbus.register_read_message.ReadWriteMultipleRegistersResponse`
+        """
+        if not (1 <= self.read_count <= 0x07D):
+            return self.doException(merror.IllegalValue)
+        if not 1 <= self.write_count <= 0x079:
+            return self.doException(merror.IllegalValue)
+        if self.write_byte_count != self.write_count * 2:
+            return self.doException(merror.IllegalValue)
+        if not context.validate(
+            self.function_code, self.write_address, self.write_count
+        ):
+            return self.doException(merror.IllegalAddress)
+        if not context.validate(self.function_code, self.read_address, self.read_count):
+            return self.doException(merror.IllegalAddress)
+        await context.async_setValues(
+            self.function_code, self.write_address, self.write_registers
+        )
+        registers = await context.async_getValues(
+            self.function_code, self.read_address, self.read_count
+        )
+        if isinstance(registers, ExceptionResponse):
+            return registers
+        return ReadWriteMultipleRegistersResponse(registers)
+
+    def get_response_pdu_size(self):
+        """Get response pdu size.
+
+        Func_code (1 byte) + Byte Count(1 byte) + 2 * Quantity of Coils (n Bytes)
+        :return:
+        """
+        return 1 + 1 + 2 * self.read_count
+
+    def __str__(self):
+        """Return a string representation of the instance.
+
+        :returns: A string representation of the instance
+        """
+        params = (
+            self.read_address,
+            self.read_count,
+            self.write_address,
+            self.write_count,
+        )
+        return (
+            "ReadWriteNRegisterRequest R(%d,%d) W(%d,%d)"  # pylint: disable=consider-using-f-string
+            % params
+        )
+
+
+class ReadWriteMultipleRegistersResponse(ReadHoldingRegistersResponse):
+    """Read/write multiple registers.
+
+    The normal response contains the data from the group of registers that
+    were read. The byte count field specifies the quantity of bytes to
+    follow in the read data field.
+
+    The requested registers can be found in the .registers list.
+    """
+
+    function_code = 23
diff --git a/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/pdu/register_write_message.py b/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/pdu/register_write_message.py
new file mode 100644
index 00000000..bb94bad1
--- /dev/null
+++ b/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/pdu/register_write_message.py
@@ -0,0 +1,378 @@
+"""Register Writing Request/Response Messages."""
+
+
+# pylint: disable=missing-type-doc
+import struct
+
+from pymodbus.pdu.pdu import ModbusExceptions as merror
+from pymodbus.pdu.pdu import ModbusPDU
+
+
+class WriteSingleRegisterRequest(ModbusPDU):
+    """This function code is used to write a single holding register in a remote device.
+
+    The Request PDU specifies the address of the register to
+    be written. Registers are addressed starting at zero. Therefore register
+    numbered 1 is addressed as 0.
+    """
+
+    function_code = 6
+    function_code_name = "write_register"
+    _rtu_frame_size = 8
+
+    def __init__(self, address=None, value=None, slave=None, transaction=0, skip_encode=0):
+        """Initialize a new instance.
+
+        :param address: The address to start writing add
+        :param value: The values to write
+        """
+        super().__init__()
+        super().setData(slave, transaction, skip_encode)
+        self.address = address
+        self.value = value
+
+    def encode(self):
+        """Encode a write single register packet packet request.
+
+        :returns: The encoded packet
+        """
+        packet = struct.pack(">H", self.address)
+        if self.skip_encode or isinstance(self.value, bytes):  # pragma: no cover
+            packet += self.value  # pragma: no cover
+        else:
+            packet += struct.pack(">H", self.value)
+        return packet
+
+    def decode(self, data):
+        """Decode a write single register packet packet request.
+
+        :param data: The request to decode
+        """
+        self.address, self.value = struct.unpack(">HH", data)
+
+    async def update_datastore(self, context):  # pragma: no cover
+        """Run a write single register request against a datastore.
+
+        :param context: The datastore to request from
+        :returns: An initialized response, exception message otherwise
+        """
+        if not 0 <= self.value <= 0xFFFF:
+            return self.doException(merror.IllegalValue)
+        if not context.validate(self.function_code, self.address, 1):
+            return self.doException(merror.IllegalAddress)
+
+        await context.async_setValues(
+            self.function_code, self.address, [self.value]
+        )
+        values = await context.async_getValues(self.function_code, self.address, 1)
+        return WriteSingleRegisterResponse(self.address, values[0])
+
+    def get_response_pdu_size(self):
+        """Get response pdu size.
+
+        Func_code (1 byte) + Register Address(2 byte) + Register Value (2 bytes)
+        :return:
+        """
+        return 1 + 2 + 2
+
+    def __str__(self):
+        """Return a string representation of the instance.
+
+        :returns: A string representation of the instance
+        """
+        return f"WriteRegisterRequest {self.address}"
+
+
+class WriteSingleRegisterResponse(ModbusPDU):
+    """The normal response is an echo of the request.
+
+    Returned after the register contents have been written.
+    """
+
+    function_code = 6
+    _rtu_frame_size = 8
+
+    def __init__(self, address=0, value=0, slave=1, transaction=0, skip_encode=False):
+        """Initialize a new instance.
+
+        :param address: The address to start writing add
+        :param value: The values to write
+        """
+        super().__init__()
+        super().setData(slave, transaction, skip_encode)
+        self.address = address
+        self.value = value
+
+    def encode(self):
+        """Encode a write single register packet packet request.
+
+        :returns: The encoded packet
+        """
+        return struct.pack(">HH", self.address, self.value)
+
+    def decode(self, data):
+        """Decode a write single register packet packet request.
+
+        :param data: The request to decode
+        """
+        self.address, self.value = struct.unpack(">HH", data)
+
+    def get_response_pdu_size(self):
+        """Get response pdu size.
+
+        Func_code (1 byte) + Starting Address (2 byte) + And_mask (2 Bytes) + OrMask (2 Bytes)
+        :return:
+        """
+        return 1 + 2 + 2 + 2
+
+    def __str__(self):
+        """Return a string representation of the instance.
+
+        :returns: A string representation of the instance
+        """
+        params = (self.address, self.value)
+        return (
+            "WriteRegisterResponse %d => %d"  # pylint: disable=consider-using-f-string
+            % params
+        )
+
+
+# ---------------------------------------------------------------------------#
+#  Write Multiple Registers
+# ---------------------------------------------------------------------------#
+class WriteMultipleRegistersRequest(ModbusPDU):
+    """This function code is used to write a block.
+
+    Of contiguous registers (1 to approx. 120 registers) in a remote device.
+
+    The requested written values are specified in the request data field.
+    Data is packed as two bytes per register.
+    """
+
+    function_code = 16
+    function_code_name = "write_registers"
+    _rtu_byte_count_pos = 6
+    _pdu_length = 5  # func + adress1 + adress2 + outputQuant1 + outputQuant2
+
+    def __init__(self, address=0, values=None, slave=None, transaction=0, skip_encode=0):
+        """Initialize a new instance.
+
+        :param address: The address to start writing to
+        :param values: The values to write
+        """
+        super().__init__()
+        super().setData(slave, transaction, skip_encode)
+        self.address = address
+        if values is None:
+            values = []
+        elif not hasattr(values, "__iter__"):
+            values = [values]
+        self.values = values
+        self.count = len(self.values)
+        self.byte_count = self.count * 2
+
+    def encode(self):
+        """Encode a write single register packet packet request.
+
+        :returns: The encoded packet
+        """
+        packet = struct.pack(">HHB", self.address, self.count, self.byte_count)
+        if self.skip_encode:  # pragma: no cover
+            return packet + b"".join(self.values)  # pragma: no cover
+
+        for value in self.values:
+            if isinstance(value, bytes):  # pragma: no cover
+                packet += value  # pragma: no cover
+            else:
+                packet += struct.pack(">H", value)
+
+        return packet
+
+    def decode(self, data):
+        """Decode a write single register packet packet request.
+
+        :param data: The request to decode
+        """
+        self.address, self.count, self.byte_count = struct.unpack(">HHB", data[:5])
+        self.values = []  # reset
+        for idx in range(5, (self.count * 2) + 5, 2):
+            self.values.append(struct.unpack(">H", data[idx : idx + 2])[0])
+
+    async def update_datastore(self, context):  # pragma: no cover
+        """Run a write single register request against a datastore.
+
+        :param context: The datastore to request from
+        :returns: An initialized response, exception message otherwise
+        """
+        if not 1 <= self.count <= 0x07B:
+            return self.doException(merror.IllegalValue)
+        if self.byte_count != self.count * 2:
+            return self.doException(merror.IllegalValue)
+        if not context.validate(self.function_code, self.address, self.count):
+            return self.doException(merror.IllegalAddress)
+
+        await context.async_setValues(
+            self.function_code, self.address, self.values
+        )
+        return WriteMultipleRegistersResponse(self.address, self.count)
+
+    def get_response_pdu_size(self):
+        """Get response pdu size.
+
+        Func_code (1 byte) + Starting Address (2 byte) + Quantity of Registers  (2 Bytes)
+        :return:
+        """
+        return 1 + 2 + 2
+
+    def __str__(self):
+        """Return a string representation of the instance.
+
+        :returns: A string representation of the instance
+        """
+        params = (self.address, self.count)
+        return (
+            "WriteMultipleRegisterRequest %d => %d"  # pylint: disable=consider-using-f-string
+            % params
+        )
+
+
+class WriteMultipleRegistersResponse(ModbusPDU):
+    """The normal response returns the function code.
+
+    Starting address, and quantity of registers written.
+    """
+
+    function_code = 16
+    _rtu_frame_size = 8
+
+    def __init__(self, address=0, count=0, slave=1, transaction=0, skip_encode=False):
+        """Initialize a new instance.
+
+        :param address: The address to start writing to
+        :param count: The number of registers to write to
+        """
+        super().__init__()
+        super().setData(slave, transaction, skip_encode)
+        self.address = address
+        self.count = count
+
+    def encode(self):
+        """Encode a write single register packet packet request.
+
+        :returns: The encoded packet
+        """
+        return struct.pack(">HH", self.address, self.count)
+
+    def decode(self, data):
+        """Decode a write single register packet packet request.
+
+        :param data: The request to decode
+        """
+        self.address, self.count = struct.unpack(">HH", data)
+
+    def __str__(self):
+        """Return a string representation of the instance.
+
+        :returns: A string representation of the instance
+        """
+        params = (self.address, self.count)
+        return (
+            "WriteMultipleRegisterResponse (%d,%d)"  # pylint: disable=consider-using-f-string
+            % params
+        )
+
+
+class MaskWriteRegisterRequest(ModbusPDU):
+    """This function code is used to modify the contents.
+
+    Of a specified holding register using a combination of an AND mask,
+    an OR mask, and the register's current contents.
+    The function can be used to set or clear individual bits in the register.
+    """
+
+    function_code = 0x16
+    function_code_name = "mask_write_register"
+    _rtu_frame_size = 10
+
+    def __init__(self, address=0x0000, and_mask=0xFFFF, or_mask=0x0000, slave=1, transaction=0, skip_encode=False):
+        """Initialize a new instance.
+
+        :param address: The mask pointer address (0x0000 to 0xffff)
+        :param and_mask: The and bitmask to apply to the register address
+        :param or_mask: The or bitmask to apply to the register address
+        """
+        super().__init__()
+        super().setData(slave, transaction, skip_encode)
+        self.address = address
+        self.and_mask = and_mask
+        self.or_mask = or_mask
+
+    def encode(self):
+        """Encode the request packet.
+
+        :returns: The byte encoded packet
+        """
+        return struct.pack(">HHH", self.address, self.and_mask, self.or_mask)
+
+    def decode(self, data):
+        """Decode the incoming request.
+
+        :param data: The data to decode into the address
+        """
+        self.address, self.and_mask, self.or_mask = struct.unpack(">HHH", data)
+
+    async def update_datastore(self, context):  # pragma: no cover
+        """Run a mask write register request against the store.
+
+        :param context: The datastore to request from
+        :returns: The populated response
+        """
+        if not 0x0000 <= self.and_mask <= 0xFFFF:
+            return self.doException(merror.IllegalValue)
+        if not 0x0000 <= self.or_mask <= 0xFFFF:
+            return self.doException(merror.IllegalValue)
+        if not context.validate(self.function_code, self.address, 1):
+            return self.doException(merror.IllegalAddress)
+        values = (await context.async_getValues(self.function_code, self.address, 1))[0]
+        values = (values & self.and_mask) | (self.or_mask & ~self.and_mask)
+        await context.async_setValues(
+            self.function_code, self.address, [values]
+        )
+        return MaskWriteRegisterResponse(self.address, self.and_mask, self.or_mask)
+
+
+class MaskWriteRegisterResponse(ModbusPDU):
+    """The normal response is an echo of the request.
+
+    The response is returned after the register has been written.
+    """
+
+    function_code = 0x16
+    _rtu_frame_size = 10
+
+    def __init__(self, address=0x0000, and_mask=0xFFFF, or_mask=0x0000, slave=1, transaction=0, skip_encode=False):
+        """Initialize new instance.
+
+        :param address: The mask pointer address (0x0000 to 0xffff)
+        :param and_mask: The and bitmask applied to the register address
+        :param or_mask: The or bitmask applied to the register address
+        """
+        super().__init__()
+        super().setData(slave, transaction, skip_encode)
+        self.address = address
+        self.and_mask = and_mask
+        self.or_mask = or_mask
+
+    def encode(self):
+        """Encode the response.
+
+        :returns: The byte encoded message
+        """
+        return struct.pack(">HHH", self.address, self.and_mask, self.or_mask)
+
+    def decode(self, data):
+        """Decode a the response.
+
+        :param data: The packet data to decode
+        """
+        self.address, self.and_mask, self.or_mask = struct.unpack(">HHH", data)
diff --git a/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/py.typed b/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/py.typed
new file mode 100644
index 00000000..e69de29b
diff --git a/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/server/__init__.py b/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/server/__init__.py
new file mode 100644
index 00000000..2f7f5262
--- /dev/null
+++ b/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/server/__init__.py
@@ -0,0 +1,42 @@
+"""Server.
+
+import external classes, to make them easier to use:
+"""
+
+__all__ = [
+    "get_simulator_commandline",
+    "ModbusSerialServer",
+    "ModbusSimulatorServer",
+    "ModbusTcpServer",
+    "ModbusTlsServer",
+    "ModbusUdpServer",
+    "ServerAsyncStop",
+    "ServerStop",
+    "StartAsyncSerialServer",
+    "StartAsyncTcpServer",
+    "StartAsyncTlsServer",
+    "StartAsyncUdpServer",
+    "StartSerialServer",
+    "StartTcpServer",
+    "StartTlsServer",
+    "StartUdpServer",
+]
+
+from pymodbus.server.async_io import (
+    ModbusSerialServer,
+    ModbusTcpServer,
+    ModbusTlsServer,
+    ModbusUdpServer,
+    ServerAsyncStop,
+    ServerStop,
+    StartAsyncSerialServer,
+    StartAsyncTcpServer,
+    StartAsyncTlsServer,
+    StartAsyncUdpServer,
+    StartSerialServer,
+    StartTcpServer,
+    StartTlsServer,
+    StartUdpServer,
+)
+from pymodbus.server.simulator.http_server import ModbusSimulatorServer
+from pymodbus.server.simulator.main import get_commandline as get_simulator_commandline
diff --git a/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/server/async_io.py b/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/server/async_io.py
new file mode 100644
index 00000000..d0615460
--- /dev/null
+++ b/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/server/async_io.py
@@ -0,0 +1,737 @@
+"""Implementation of a Threaded Modbus Server."""
+# pylint: disable=missing-type-doc
+from __future__ import annotations
+
+import asyncio
+import os
+import traceback
+from contextlib import suppress
+
+from pymodbus.datastore import ModbusServerContext
+from pymodbus.device import ModbusControlBlock, ModbusDeviceIdentification
+from pymodbus.exceptions import ModbusException, NoSuchSlaveException
+from pymodbus.framer import FRAMER_NAME_TO_CLASS, FramerBase, FramerType
+from pymodbus.logging import Log
+from pymodbus.pdu import DecodePDU
+from pymodbus.pdu import ModbusExceptions as merror
+from pymodbus.pdu.pdu import ExceptionResponse
+from pymodbus.transport import CommParams, CommType, ModbusProtocol
+
+
+# --------------------------------------------------------------------------- #
+# Protocol Handlers
+# --------------------------------------------------------------------------- #
+
+
+class ModbusServerRequestHandler(ModbusProtocol):
+    """Implements modbus slave wire protocol.
+
+    This uses the asyncio.Protocol to implement the server protocol.
+
+    When a connection is established, a callback is called.
+    This callback will setup the connection and
+    create and schedule an asyncio.Task and assign it to running_task.
+    """
+
+    def __init__(self, owner):
+        """Initialize."""
+        params = CommParams(
+            comm_name="server",
+            comm_type=owner.comm_params.comm_type,
+            reconnect_delay=0.0,
+            reconnect_delay_max=0.0,
+            timeout_connect=0.0,
+            host=owner.comm_params.source_address[0],
+            port=owner.comm_params.source_address[1],
+        )
+        super().__init__(params, True)
+        self.server = owner
+        self.running = False
+        self.receive_queue: asyncio.Queue = asyncio.Queue()
+        self.handler_task = None  # coroutine to be run on asyncio loop
+        self.databuffer = b''
+        self.framer: FramerBase
+        self.loop = asyncio.get_running_loop()
+
+    def _log_exception(self):
+        """Show log exception."""
+        Log.debug(
+            "Handler for stream [{}] has been canceled", self.comm_params.comm_name
+        )
+
+    def callback_new_connection(self) -> ModbusProtocol:
+        """Call when listener receive new connection request."""
+        Log.debug("callback_new_connection called")
+        return ModbusServerRequestHandler(self)
+
+    def callback_connected(self) -> None:
+        """Call when connection is succcesfull."""
+        slaves = self.server.context.slaves()
+        if self.server.broadcast_enable:
+            if 0 not in slaves:
+                slaves.append(0)
+        try:
+            self.running = True
+            self.framer = self.server.framer(self.server.decoder)
+
+            # schedule the connection handler on the event loop
+            self.handler_task = asyncio.create_task(self.handle())
+            self.handler_task.set_name("server connection handler")
+        except Exception as exc:  # pylint: disable=broad-except
+            Log.error(
+                "Server callback_connected exception: {}; {}",
+                exc,
+                traceback.format_exc(),
+            )
+
+    def callback_disconnected(self, call_exc: Exception | None) -> None:
+        """Call when connection is lost."""
+        try:
+            if self.handler_task:
+                self.handler_task.cancel()
+            if hasattr(self.server, "on_connection_lost"):
+                self.server.on_connection_lost()
+            if call_exc is None:
+                self._log_exception()
+            else:
+                Log.debug(
+                    "Client Disconnection {} due to {}",
+                    self.comm_params.comm_name,
+                    call_exc,
+                )
+            self.running = False
+        except Exception as exc:  # pylint: disable=broad-except
+            Log.error(
+                "Datastore unable to fulfill request: {}; {}",
+                exc,
+                traceback.format_exc(),
+            )
+
+    async def inner_handle(self):
+        """Handle handler."""
+        # this is an asyncio.Queue await, it will never fail
+        data = await self._recv_()
+        if isinstance(data, tuple):
+            # addr is populated when talking over UDP
+            data, *addr = data
+        else:
+            addr = [None]
+
+        # if broadcast is enabled make sure to
+        # process requests to address 0
+        self.databuffer += data
+        Log.debug("Handling data: {}", self.databuffer, ":hex")
+        try:
+            used_len, pdu = self.framer.processIncomingFrame(self.databuffer)
+        except ModbusException:
+            pdu = ExceptionResponse(
+                40,
+                exception_code=merror.IllegalFunction
+            )
+            self.server_send(pdu, 0)
+            pdu = None
+            used_len = len(self.databuffer)
+        self.databuffer = self.databuffer[used_len:]
+        if pdu:
+           self.execute(pdu, *addr)
+
+    async def handle(self) -> None:
+        """Coroutine which represents a single master <=> slave conversation.
+
+        Once the client connection is established, the data chunks will be
+        fed to this coroutine via the asyncio.Queue object which is fed by
+        the ModbusServerRequestHandler class's callback Future.
+
+        This callback future gets data from either asyncio.BaseProtocol.data_received
+        or asyncio.DatagramProtocol.datagram_received.
+
+        This function will execute without blocking in the while-loop and
+        yield to the asyncio event loop when the frame is exhausted.
+        As a result, multiple clients can be interleaved without any
+        interference between them.
+        """
+        while self.running:
+            try:
+                await self.inner_handle()
+            except asyncio.CancelledError:
+                # catch and ignore cancellation errors
+                if self.running:
+                    self._log_exception()
+                    self.running = False
+            except Exception as exc:  # pylint: disable=broad-except
+                # force TCP socket termination as framer
+                # should handle application layer errors
+                Log.error(
+                    'Unknown exception "{}" on stream {} forcing disconnect',
+                    exc,
+                    self.comm_params.comm_name,
+                )
+                self.close()
+                self.callback_disconnected(exc)
+
+    def execute(self, request, *addr):
+        """Call with the resulting message.
+
+        :param request: The decoded request message
+        :param addr: the address
+        """
+        if self.server.request_tracer:
+            self.server.request_tracer(request, *addr)
+
+        asyncio.run_coroutine_threadsafe(self._async_execute(request, *addr), self.loop)
+
+    async def _async_execute(self, request, *addr):
+        broadcast = False
+        try:
+            if self.server.broadcast_enable and not request.slave_id:
+                broadcast = True
+                # if broadcasting then execute on all slave contexts,
+                # note response will be ignored
+                for slave_id in self.server.context.slaves():
+                    response = await request.update_datastore(self.server.context[slave_id])
+            else:
+                context = self.server.context[request.slave_id]
+                response = await request.update_datastore(context)
+
+        except NoSuchSlaveException:
+            Log.error("requested slave does not exist: {}", request.slave_id)
+            if self.server.ignore_missing_slaves:
+                return  # the client will simply timeout waiting for a response
+            response = request.doException(merror.GatewayNoResponse)
+        except Exception as exc:  # pylint: disable=broad-except
+            Log.error(
+                "Datastore unable to fulfill request: {}; {}",
+                exc,
+                traceback.format_exc(),
+            )
+            response = request.doException(merror.SlaveFailure)
+        # no response when broadcasting
+        if not broadcast:
+            response.transaction_id = request.transaction_id
+            response.slave_id = request.slave_id
+            skip_encoding = False
+            if self.server.response_manipulator:
+                response, skip_encoding = self.server.response_manipulator(response)
+            self.server_send(response, *addr, skip_encoding=skip_encoding)
+
+    def server_send(self, message, addr, **kwargs):
+        """Send message."""
+        if kwargs.get("skip_encoding", False):
+            self.send(message, addr=addr)
+        if not message:
+            Log.debug("Skipping sending response!!")
+        else:
+            pdu = self.framer.buildFrame(message)
+            self.send(pdu, addr=addr)
+
+    async def _recv_(self):
+        """Receive data from the network."""
+        try:
+            result = await self.receive_queue.get()
+        except RuntimeError:
+            Log.error("Event loop is closed")
+            result = None
+        return result
+
+    def callback_data(self, data: bytes, addr: tuple | None = ()) -> int:
+        """Handle received data."""
+        if addr != ():
+            self.receive_queue.put_nowait((data, addr))
+        else:
+            self.receive_queue.put_nowait(data)
+        return len(data)
+
+
+# --------------------------------------------------------------------------- #
+# Server Implementations
+# --------------------------------------------------------------------------- #
+
+
+class ModbusBaseServer(ModbusProtocol):
+    """Common functionality for all server classes."""
+
+    def __init__(
+        self,
+        params: CommParams,
+        context,
+        ignore_missing_slaves,
+        broadcast_enable,
+        response_manipulator,
+        request_tracer,
+        identity,
+        framer,
+    ) -> None:
+        """Initialize base server."""
+        super().__init__(
+            params,
+            True,
+        )
+        self.loop = asyncio.get_running_loop()
+        self.decoder = DecodePDU(True)
+        self.context = context or ModbusServerContext()
+        self.control = ModbusControlBlock()
+        self.ignore_missing_slaves = ignore_missing_slaves
+        self.broadcast_enable = broadcast_enable
+        self.response_manipulator = response_manipulator
+        self.request_tracer = request_tracer
+        self.handle_local_echo = False
+        if isinstance(identity, ModbusDeviceIdentification):
+            self.control.Identity.update(identity)
+
+        self.framer = FRAMER_NAME_TO_CLASS[framer]
+        self.serving: asyncio.Future = asyncio.Future()
+
+    def callback_new_connection(self):
+        """Handle incoming connect."""
+        return ModbusServerRequestHandler(self)
+
+    async def shutdown(self):
+        """Close server."""
+        if not self.serving.done():
+            self.serving.set_result(True)
+        self.close()
+
+    async def serve_forever(self):
+        """Start endless loop."""
+        if self.transport:
+            raise RuntimeError(
+                "Can't call serve_forever on an already running server object"
+            )
+        await self.listen()
+        Log.info("Server listening.")
+        await self.serving
+        Log.info("Server graceful shutdown.")
+
+    def callback_connected(self) -> None:
+        """Call when connection is succcesfull."""
+
+    def callback_disconnected(self, exc: Exception | None) -> None:
+        """Call when connection is lost."""
+        Log.debug("callback_disconnected called: {}", exc)
+
+    def callback_data(self, data: bytes, addr: tuple | None = None) -> int:
+        """Handle received data."""
+        Log.debug("callback_data called: {} addr={}", data, ":hex", addr)
+        return 0
+
+class ModbusTcpServer(ModbusBaseServer):
+    """A modbus threaded tcp socket server.
+
+    We inherit and overload the socket server so that we
+    can control the client threads as well as have a single
+    server context instance.
+    """
+
+    def __init__(
+        self,
+        context,
+        framer=FramerType.SOCKET,
+        identity=None,
+        address=("", 502),
+        ignore_missing_slaves=False,
+        broadcast_enable=False,
+        response_manipulator=None,
+        request_tracer=None,
+    ):
+        """Initialize the socket server.
+
+        If the identify structure is not passed in, the ModbusControlBlock
+        uses its own empty structure.
+
+        :param context: The ModbusServerContext datastore
+        :param framer: The framer strategy to use
+        :param identity: An optional identify structure
+        :param address: An optional (interface, port) to bind to.
+        :param ignore_missing_slaves: True to not send errors on a request
+                        to a missing slave
+        :param broadcast_enable: True to treat slave_id 0 as broadcast address,
+                        False to treat 0 as any other slave_id
+        :param response_manipulator: Callback method for manipulating the
+                                        response
+        :param request_tracer: Callback method for tracing
+        """
+        params = getattr(
+            self,
+            "tls_setup",
+            CommParams(
+                comm_type=CommType.TCP,
+                comm_name="server_listener",
+                reconnect_delay=0.0,
+                reconnect_delay_max=0.0,
+                timeout_connect=0.0,
+            ),
+        )
+        params.source_address = address
+        super().__init__(
+            params,
+            context,
+            ignore_missing_slaves,
+            broadcast_enable,
+            response_manipulator,
+            request_tracer,
+            identity,
+            framer,
+        )
+
+
+class ModbusTlsServer(ModbusTcpServer):
+    """A modbus threaded tls socket server.
+
+    We inherit and overload the socket server so that we
+    can control the client threads as well as have a single
+    server context instance.
+    """
+
+    def __init__(  # pylint: disable=too-many-arguments
+        self,
+        context,
+        framer=FramerType.TLS,
+        identity=None,
+        address=("", 502),
+        sslctx=None,
+        certfile=None,
+        keyfile=None,
+        password=None,
+        ignore_missing_slaves=False,
+        broadcast_enable=False,
+        response_manipulator=None,
+        request_tracer=None,
+    ):
+        """Overloaded initializer for the socket server.
+
+        If the identify structure is not passed in, the ModbusControlBlock
+        uses its own empty structure.
+
+        :param context: The ModbusServerContext datastore
+        :param framer: The framer strategy to use
+        :param identity: An optional identify structure
+        :param address: An optional (interface, port) to bind to.
+        :param sslctx: The SSLContext to use for TLS (default None and auto
+                       create)
+        :param certfile: The cert file path for TLS (used if sslctx is None)
+        :param keyfile: The key file path for TLS (used if sslctx is None)
+        :param password: The password for for decrypting the private key file
+        :param ignore_missing_slaves: True to not send errors on a request
+                        to a missing slave
+        :param broadcast_enable: True to treat slave_id 0 as broadcast address,
+                        False to treat 0 as any other slave_id
+        :param response_manipulator: Callback method for
+                        manipulating the response
+        """
+        self.tls_setup = CommParams(
+            comm_type=CommType.TLS,
+            comm_name="server_listener",
+            reconnect_delay=0.0,
+            reconnect_delay_max=0.0,
+            timeout_connect=0.0,
+            sslctx=CommParams.generate_ssl(
+                True, certfile, keyfile, password, sslctx=sslctx
+            ),
+        )
+        super().__init__(
+            context,
+            framer=framer,
+            identity=identity,
+            address=address,
+            ignore_missing_slaves=ignore_missing_slaves,
+            broadcast_enable=broadcast_enable,
+            response_manipulator=response_manipulator,
+            request_tracer=request_tracer,
+        )
+
+
+class ModbusUdpServer(ModbusBaseServer):
+    """A modbus threaded udp socket server.
+
+    We inherit and overload the socket server so that we
+    can control the client threads as well as have a single
+    server context instance.
+    """
+
+    def __init__(
+        self,
+        context,
+        framer=FramerType.SOCKET,
+        identity=None,
+        address=("", 502),
+        ignore_missing_slaves=False,
+        broadcast_enable=False,
+        response_manipulator=None,
+        request_tracer=None,
+    ):
+        """Overloaded initializer for the socket server.
+
+        If the identify structure is not passed in, the ModbusControlBlock
+        uses its own empty structure.
+
+        :param context: The ModbusServerContext datastore
+        :param framer: The framer strategy to use
+        :param identity: An optional identify structure
+        :param address: An optional (interface, port) to bind to.
+        :param ignore_missing_slaves: True to not send errors on a request
+                            to a missing slave
+        :param broadcast_enable: True to treat slave_id 0 as broadcast address,
+                            False to treat 0 as any other slave_id
+        :param response_manipulator: Callback method for
+                            manipulating the response
+        :param request_tracer: Callback method for tracing
+        """
+        # ----------------
+        super().__init__(
+            CommParams(
+                comm_type=CommType.UDP,
+                comm_name="server_listener",
+                source_address=address,
+                reconnect_delay=0.0,
+                reconnect_delay_max=0.0,
+                timeout_connect=0.0,
+            ),
+            context,
+            ignore_missing_slaves,
+            broadcast_enable,
+            response_manipulator,
+            request_tracer,
+            identity,
+            framer,
+        )
+
+
+class ModbusSerialServer(ModbusBaseServer):
+    """A modbus threaded serial socket server.
+
+    We inherit and overload the socket server so that we
+    can control the client threads as well as have a single
+    server context instance.
+    """
+
+    def __init__(
+        self, context, framer=FramerType.RTU, identity=None, **kwargs
+    ):
+        """Initialize the socket server.
+
+        If the identity structure is not passed in, the ModbusControlBlock
+        uses its own empty structure.
+        :param context: The ModbusServerContext datastore
+        :param framer: The framer strategy to use, default FramerType.RTU
+        :param identity: An optional identify structure
+        :param port: The serial port to attach to
+        :param stopbits: The number of stop bits to use
+        :param bytesize: The bytesize of the serial messages
+        :param parity: Which kind of parity to use
+        :param baudrate: The baud rate to use for the serial device
+        :param timeout: The timeout to use for the serial device
+        :param handle_local_echo: (optional) Discard local echo from dongle.
+        :param ignore_missing_slaves: True to not send errors on a request
+                            to a missing slave
+        :param broadcast_enable: True to treat slave_id 0 as broadcast address,
+                            False to treat 0 as any other slave_id
+        :param reconnect_delay: reconnect delay in seconds
+        :param response_manipulator: Callback method for
+                    manipulating the response
+        :param request_tracer: Callback method for tracing
+        """
+        super().__init__(
+            params=CommParams(
+                comm_type=CommType.SERIAL,
+                comm_name="server_listener",
+                reconnect_delay=kwargs.get("reconnect_delay", 2),
+                reconnect_delay_max=0.0,
+                timeout_connect=kwargs.get("timeout", 3),
+                source_address=(kwargs.get("port", 0), 0),
+                bytesize=kwargs.get("bytesize", 8),
+                parity=kwargs.get("parity", "N"),
+                baudrate=kwargs.get("baudrate", 19200),
+                stopbits=kwargs.get("stopbits", 1),
+            ),
+            context=context,
+            ignore_missing_slaves=kwargs.get("ignore_missing_slaves", False),
+            broadcast_enable=kwargs.get("broadcast_enable", False),
+            response_manipulator=kwargs.get("response_manipulator", None),
+            request_tracer=kwargs.get("request_tracer", None),
+            identity=kwargs.get("identity", None),
+            framer=framer,
+        )
+        self.handle_local_echo = kwargs.get("handle_local_echo", False)
+
+
+# --------------------------------------------------------------------------- #
+# Creation Factories
+# --------------------------------------------------------------------------- #
+
+
+class _serverList:
+    """Maintains information about the active server.
+
+    :meta private:
+    """
+
+    active_server: ModbusTcpServer | ModbusUdpServer | ModbusSerialServer
+
+    def __init__(self, server):
+        """Register new server."""
+        self.server = server
+        self.loop = asyncio.get_event_loop()
+
+    @classmethod
+    async def run(cls, server, custom_functions) -> None:
+        """Help starting/stopping server."""
+        for func in custom_functions:
+            server.decoder.register(func)
+        cls.active_server = _serverList(server)  # type: ignore[assignment]
+        with suppress(asyncio.exceptions.CancelledError):
+            await server.serve_forever()
+
+    @classmethod
+    async def async_stop(cls) -> None:
+        """Wait for server stop."""
+        if not cls.active_server:
+            raise RuntimeError("ServerAsyncStop called without server task active.")
+        await cls.active_server.server.shutdown()  # type: ignore[union-attr]
+        cls.active_server = None  # type: ignore[assignment]
+
+    @classmethod
+    def stop(cls):
+        """Wait for server stop."""
+        if not cls.active_server:
+            Log.info("ServerStop called without server task active.")
+            return
+        if not cls.active_server.loop.is_running():
+            Log.info("ServerStop called with loop stopped.")
+            return
+        future = asyncio.run_coroutine_threadsafe(cls.async_stop(), cls.active_server.loop)
+        future.result(timeout=10 if os.name == 'nt' else 0.1)
+
+
+async def StartAsyncTcpServer(  # pylint: disable=invalid-name,dangerous-default-value
+    context=None,
+    identity=None,
+    address=None,
+    custom_functions=[],
+    **kwargs,
+):
+    """Start and run a tcp modbus server.
+
+    :param context: The ModbusServerContext datastore
+    :param identity: An optional identify structure
+    :param address: An optional (interface, port) to bind to.
+    :param custom_functions: An optional list of custom function classes
+        supported by server instance.
+    :param kwargs: The rest
+    """
+    kwargs.pop("host", None)
+    server = ModbusTcpServer(
+        context, kwargs.pop("framer", FramerType.SOCKET), identity, address, **kwargs
+    )
+    await _serverList.run(server, custom_functions)
+
+
+async def StartAsyncTlsServer(  # pylint: disable=invalid-name,dangerous-default-value
+    context=None,
+    identity=None,
+    address=None,
+    sslctx=None,
+    certfile=None,
+    keyfile=None,
+    password=None,
+    custom_functions=[],
+    **kwargs,
+):
+    """Start and run a tls modbus server.
+
+    :param context: The ModbusServerContext datastore
+    :param identity: An optional identify structure
+    :param address: An optional (interface, port) to bind to.
+    :param sslctx: The SSLContext to use for TLS (default None and auto create)
+    :param certfile: The cert file path for TLS (used if sslctx is None)
+    :param keyfile: The key file path for TLS (used if sslctx is None)
+    :param password: The password for for decrypting the private key file
+    :param custom_functions: An optional list of custom function classes
+        supported by server instance.
+    :param kwargs: The rest
+    """
+    kwargs.pop("host", None)
+    server = ModbusTlsServer(
+        context,
+        kwargs.pop("framer", FramerType.TLS),
+        identity,
+        address,
+        sslctx,
+        certfile,
+        keyfile,
+        password,
+        **kwargs,
+    )
+    await _serverList.run(server, custom_functions)
+
+
+async def StartAsyncUdpServer(  # pylint: disable=invalid-name,dangerous-default-value
+    context=None,
+    identity=None,
+    address=None,
+    custom_functions=[],
+    **kwargs,
+):
+    """Start and run a udp modbus server.
+
+    :param context: The ModbusServerContext datastore
+    :param identity: An optional identify structure
+    :param address: An optional (interface, port) to bind to.
+    :param custom_functions: An optional list of custom function classes
+        supported by server instance.
+    :param kwargs:
+    """
+    kwargs.pop("host", None)
+    server = ModbusUdpServer(
+        context, kwargs.pop("framer", FramerType.SOCKET), identity, address, **kwargs
+    )
+    await _serverList.run(server, custom_functions)
+
+
+async def StartAsyncSerialServer(  # pylint: disable=invalid-name,dangerous-default-value
+    context=None,
+    identity=None,
+    custom_functions=[],
+    **kwargs,
+):
+    """Start and run a serial modbus server.
+
+    :param context: The ModbusServerContext datastore
+    :param identity: An optional identify structure
+    :param custom_functions: An optional list of custom function classes
+        supported by server instance.
+    :param kwargs: The rest
+    """
+    server = ModbusSerialServer(
+        context, kwargs.pop("framer", FramerType.RTU), identity=identity, **kwargs
+    )
+    await _serverList.run(server, custom_functions)
+
+
+def StartSerialServer(**kwargs):  # pylint: disable=invalid-name
+    """Start and run a serial modbus server."""
+    return asyncio.run(StartAsyncSerialServer(**kwargs))
+
+
+def StartTcpServer(**kwargs):  # pylint: disable=invalid-name
+    """Start and run a serial modbus server."""
+    return asyncio.run(StartAsyncTcpServer(**kwargs))
+
+
+def StartTlsServer(**kwargs):  # pylint: disable=invalid-name
+    """Start and run a serial modbus server."""
+    return asyncio.run(StartAsyncTlsServer(**kwargs))
+
+
+def StartUdpServer(**kwargs):  # pylint: disable=invalid-name
+    """Start and run a serial modbus server."""
+    return asyncio.run(StartAsyncUdpServer(**kwargs))
+
+
+async def ServerAsyncStop():  # pylint: disable=invalid-name
+    """Terminate server."""
+    await _serverList.async_stop()
+
+
+def ServerStop():  # pylint: disable=invalid-name
+    """Terminate server."""
+    _serverList.stop()
diff --git a/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/server/simulator/__init__.py b/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/server/simulator/__init__.py
new file mode 100644
index 00000000..8d16f202
--- /dev/null
+++ b/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/server/simulator/__init__.py
@@ -0,0 +1 @@
+"""Initialize."""
diff --git a/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/server/simulator/custom_actions.py b/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/server/simulator/custom_actions.py
new file mode 100644
index 00000000..50c0a93f
--- /dev/null
+++ b/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/server/simulator/custom_actions.py
@@ -0,0 +1,10 @@
+"""Datastore simulator, custom actions."""
+
+
+def device_reset(_registers, _inx, _cell):
+    """Use example custom action."""
+
+
+custom_actions_dict = {
+    "umg804_reset": device_reset,
+}
diff --git a/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/server/simulator/http_server.py b/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/server/simulator/http_server.py
new file mode 100644
index 00000000..aba2f343
--- /dev/null
+++ b/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/server/simulator/http_server.py
@@ -0,0 +1,809 @@
+"""HTTP server for modbus simulator."""
+from __future__ import annotations
+
+import asyncio
+import contextlib
+import dataclasses
+import importlib
+import json
+import os
+from time import sleep
+from typing import TYPE_CHECKING
+
+
+try:
+    from aiohttp import web
+
+    AIOHTTP_MISSING = False
+except ImportError:
+    AIOHTTP_MISSING = True
+    if TYPE_CHECKING:  # always False at runtime
+        # type checkers do not understand the Raise RuntimeError in __init__()
+        from aiohttp import web
+
+from pymodbus.datastore import ModbusServerContext, ModbusSimulatorContext
+from pymodbus.datastore.simulator import Label
+from pymodbus.device import ModbusDeviceIdentification
+from pymodbus.logging import Log
+from pymodbus.pdu import DecodePDU, ExceptionResponse
+from pymodbus.server.async_io import (
+    ModbusSerialServer,
+    ModbusTcpServer,
+    ModbusTlsServer,
+    ModbusUdpServer,
+)
+
+
+MAX_FILTER = 1000
+
+RESPONSE_INACTIVE = -1
+RESPONSE_NORMAL = 0
+RESPONSE_ERROR = 1
+RESPONSE_EMPTY = 2
+RESPONSE_JUNK = 3
+
+
+@dataclasses.dataclass()
+class CallTracer:
+    """Define call/response traces."""
+
+    call: bool = False
+    fc: int = -1
+    address: int = -1
+    count: int = -1
+    data: bytes = b""
+
+
+@dataclasses.dataclass()
+class CallTypeMonitor:
+    """Define Request/Response monitor."""
+
+    active: bool = False
+    trace_response: bool = False
+    range_start: int = -1
+    range_stop: int = -1
+    function: int = -1
+    hex: bool = False
+    decode: bool = False
+
+
+@dataclasses.dataclass()
+class CallTypeResponse:
+    """Define Response manipulation."""
+
+    active: int = RESPONSE_INACTIVE
+    split: int = 0
+    delay: int = 0
+    junk_len: int = 10
+    error_response: int = 0
+    change_rate: int = 0
+    clear_after: int = 1
+
+
+class ModbusSimulatorServer:
+    """**ModbusSimulatorServer**.
+
+    :param modbus_server: Server name in json file (default: "server")
+    :param modbus_device: Device name in json file (default: "client")
+    :param http_host: TCP host for HTTP (default: "localhost")
+    :param http_port: TCP port for HTTP (default: 8080)
+    :param json_file: setup file (default: "setup.json")
+    :param custom_actions_module: python module with custom actions (default: none)
+
+    if either http_port or http_host is none, HTTP will not be started.
+    This class starts a http server, that serves a couple of endpoints:
+
+    - **"<addr>/"** static files
+    - **"<addr>/api/log"** log handling, HTML with GET, REST-API with post
+    - **"<addr>/api/registers"** register handling, HTML with GET, REST-API with post
+    - **"<addr>/api/calls"** call (function code / message) handling, HTML with GET, REST-API with post
+    - **"<addr>/api/server"** server handling, HTML with GET, REST-API with post
+
+    Example::
+
+        from pymodbus.server import ModbusSimulatorServer
+
+        async def run():
+            simulator = ModbusSimulatorServer(
+                modbus_server="my server",
+                modbus_device="my device",
+                http_host="localhost",
+                http_port=8080)
+            await simulator.run_forever(only_start=True)
+            ...
+            await simulator.stop()
+    """
+
+    def __init__(
+        self,
+        modbus_server: str = "server",
+        modbus_device: str = "device",
+        http_host: str = "0.0.0.0",
+        http_port: int = 8080,
+        log_file: str = "server.log",
+        json_file: str = "setup.json",
+        custom_actions_module: str | None = None,
+    ):
+        """Initialize http interface."""
+        if AIOHTTP_MISSING:
+            raise RuntimeError(
+                "Simulator server requires aiohttp. "
+                'Please install with "pip install aiohttp" and try again.'
+            )
+        with open(json_file, encoding="utf-8") as file:
+            setup = json.load(file)
+
+        comm_class = {
+            "serial": ModbusSerialServer,
+            "tcp": ModbusTcpServer,
+            "tls": ModbusTlsServer,
+            "udp": ModbusUdpServer,
+        }
+        if custom_actions_module:
+            actions_module = importlib.import_module(custom_actions_module)
+            custom_actions_dict = actions_module.custom_actions_dict
+        else:
+            custom_actions_dict = {}
+        server = setup["server_list"][modbus_server]
+        if server["comm"] != "serial":
+            server["address"] = (server["host"], server["port"])
+            del server["host"]
+            del server["port"]
+        device = setup["device_list"][modbus_device]
+        self.datastore_context = ModbusSimulatorContext(
+            device, custom_actions_dict or {}
+        )
+        datastore = None
+        if "device_id" in server:
+            # Designated ModBus unit address. Will only serve data if the address matches
+            datastore = ModbusServerContext(slaves={int(server["device_id"]): self.datastore_context}, single=False)
+        else:
+            # Will server any request regardless of addressing
+            datastore = ModbusServerContext(slaves=self.datastore_context, single=True)
+
+        comm = comm_class[server.pop("comm")]
+        framer = server.pop("framer")
+        if "identity" in server:
+            server["identity"] = ModbusDeviceIdentification(
+                info_name=server["identity"]
+            )
+        self.modbus_server = comm(framer=framer, context=datastore, **server)
+        self.serving: asyncio.Future = asyncio.Future()
+        self.log_file = log_file
+        self.site: web.TCPSite | None = None
+        self.runner: web.AppRunner
+        self.http_host = http_host
+        self.http_port = http_port
+        self.web_path = os.path.join(os.path.dirname(__file__), "web")
+        self.web_app = web.Application()
+        self.web_app.add_routes(
+            [
+                web.get("/api/{tail:[a-z]*}", self.handle_html),
+                web.post("/restapi/{tail:[a-z]*}", self.handle_json),
+                web.get("/{tail:[a-z0-9.]*}", self.handle_html_static),
+                web.get("/", self.handle_html_static),
+            ]
+        )
+        self.web_app.on_startup.append(self.start_modbus_server)
+        self.web_app.on_shutdown.append(self.stop_modbus_server)
+        self.generator_html: dict[str, list] = {
+            "log": ["", self.build_html_log],
+            "registers": ["", self.build_html_registers],
+            "calls": ["", self.build_html_calls],
+            "server": ["", self.build_html_server],
+        }
+        self.generator_json = {
+            "log": self.build_json_log,
+            "registers": self.build_json_registers,
+            "calls": self.build_json_calls,
+            "server": self.build_json_server,
+        }
+        self.submit_html = {
+            "Clear": self.action_clear,
+            "Stop": self.action_stop,
+            "Reset": self.action_reset,
+            "Add": self.action_add,
+            "Monitor": self.action_monitor,
+            "Set": self.action_set,
+            "Simulate": self.action_simulate,
+        }
+        for entry in self.generator_html:  # pylint: disable=consider-using-dict-items
+            html_file = os.path.join(self.web_path, "generator", entry)
+            with open(html_file, encoding="utf-8") as handle:
+                self.generator_html[entry][0] = handle.read()
+        self.refresh_rate = 0
+        self.register_filter: list[int] = []
+        self.call_list: list[CallTracer] = []
+        self.request_lookup = DecodePDU(True).lookup
+        self.call_monitor = CallTypeMonitor()
+        self.call_response = CallTypeResponse()
+        app_key = getattr(web, 'AppKey', str)  # fall back to str for aiohttp < 3.9.0
+        self.api_key = app_key("modbus_server")
+
+    async def start_modbus_server(self, app):
+        """Start Modbus server as asyncio task."""
+        try:
+            if getattr(self.modbus_server, "start", None):
+                await self.modbus_server.start()
+            app[self.api_key] = asyncio.create_task(
+                self.modbus_server.serve_forever()
+            )
+            app[self.api_key].set_name("simulator modbus server")
+        except Exception as exc:
+            Log.error("Error starting modbus server, reason: {}", exc)
+            raise exc
+        Log.info(
+            "Modbus server started on {}", self.modbus_server.comm_params.source_address
+        )
+
+    async def stop_modbus_server(self, app):
+        """Stop modbus server."""
+        Log.info("Stopping modbus server")
+        await self.modbus_server.shutdown()
+        app[self.api_key].cancel()
+        with contextlib.suppress(asyncio.exceptions.CancelledError):
+            await app[self.api_key]
+
+        Log.info("Modbus server Stopped")
+
+    async def run_forever(self, only_start=False):
+        """Start modbus and http servers."""
+        try:
+            self.runner = web.AppRunner(self.web_app)
+            await self.runner.setup()
+            self.site = web.TCPSite(self.runner, self.http_host, self.http_port)
+            await self.site.start()
+        except Exception as exc:
+            Log.error("Error starting http server, reason: {}", exc)
+            raise exc
+        Log.info("HTTP server started on ({}:{})", self.http_host, self.http_port)
+        if only_start:
+            return
+        await self.serving
+
+    async def stop(self):
+        """Stop modbus and http servers."""
+        await self.runner.cleanup()
+        self.site = None
+        if not self.serving.done():
+            self.serving.set_result(True)
+        await asyncio.sleep(0)
+
+    async def handle_html_static(self, request):
+        """Handle static html."""
+        if not (page := request.path[1:]):
+            page = "index.html"
+        file = os.path.normpath(os.path.join(self.web_path, page))
+        if not file.startswith(self.web_path):
+            raise ValueError(f"File access outside {self.web_path} not permitted.")
+        try:
+            with open(file, encoding="utf-8"):
+                return web.FileResponse(file)
+        except (FileNotFoundError, IsADirectoryError) as exc:
+            raise web.HTTPNotFound(reason="File not found") from exc
+
+    async def handle_html(self, request):
+        """Handle html."""
+        page_type = request.path.split("/")[-1]
+        params = dict(request.query)
+        if refresh := params.pop("refresh", None):
+            self.refresh_rate = int(refresh)
+        if self.refresh_rate > 0:
+            html = self.generator_html[page_type][0].replace(
+                "<!--REFRESH-->",
+                f'<meta http-equiv="refresh" content="{self.refresh_rate}">',
+            )
+        else:
+            html = self.generator_html[page_type][0].replace("<!--REFRESH-->", "")
+        new_page = self.generator_html[page_type][1](params, html)
+        return web.Response(text=new_page, content_type="text/html")
+
+    async def handle_json(self, request):
+        """Handle api registers."""
+        command = request.path.split("/")[-1]
+        params = await request.json()
+        try:
+            result = self.generator_json[command](params)
+        except (KeyError, ValueError, TypeError, IndexError) as exc:
+            Log.error("Unhandled error during json request: {}", exc)
+            return web.json_response({"result": "error", "error": f"Unhandled error Error: {exc}"})
+        return web.json_response(result)
+
+    def build_html_registers(self, params, html):
+        """Build html registers page."""
+        result_txt, foot = self.helper_handle_submit(params, self.submit_html)
+        if not result_txt:
+            result_txt = "ok"
+        if not foot:
+            if self.register_filter:
+                foot = f"{len(self.register_filter)} register(s) monitored"
+            else:
+                foot = "Nothing selected"
+        register_types = "".join(
+            f"<option value={reg_id}>{name}</option>"
+            for name, reg_id in self.datastore_context.registerType_name_to_id.items()
+        )
+        register_actions = "".join(
+            f"<option value={action_id}>{name}</option>"
+            for name, action_id in self.datastore_context.action_name_to_id.items()
+        )
+        rows = ""
+        for i in self.register_filter:
+            inx, reg = self.datastore_context.get_text_register(i)
+            if reg.type == Label.next:
+                continue
+            row = "".join(
+                f"<td>{entry}</td>"
+                for entry in (
+                    inx,
+                    reg.type,
+                    reg.access,
+                    reg.action,
+                    reg.value,
+                    reg.count_read,
+                    reg.count_write,
+                )
+            )
+            rows += f"<tr>{row}</tr>"
+        new_html = (
+            html.replace("<!--REGISTER_ACTIONS-->", register_actions)
+            .replace("<!--REGISTER_TYPES-->", register_types)
+            .replace("<!--REGISTER_FOOT-->", foot)
+            .replace("<!--REGISTER_ROWS-->", rows)
+            .replace("<!--RESULT-->", result_txt)
+        )
+        return new_html
+
+    def build_html_calls(self, params: dict, html: str) -> str:
+        """Build html calls page."""
+        result_txt, foot = self.helper_handle_submit(params, self.submit_html)
+        if not foot:
+            foot = "Montitoring active" if self.call_monitor.active else "not active"
+        if not result_txt:
+            result_txt = "ok"
+
+        function_error = ""
+        for i, txt in (
+            (1, "IllegalFunction"),
+            (2, "IllegalAddress"),
+            (3, "IllegalValue"),
+            (4, "SlaveFailure"),
+            (5, "Acknowledge"),
+            (6, "SlaveBusy"),
+            (7, "MemoryParityError"),
+            (10, "GatewayPathUnavailable"),
+            (11, "GatewayNoResponse"),
+        ):
+            selected = "selected" if i == self.call_response.error_response else ""
+            function_error += f"<option value={i} {selected}>{txt}</option>"
+        range_start_html = (
+            str(self.call_monitor.range_start)
+            if self.call_monitor.range_start != -1
+            else ""
+        )
+        range_stop_html = (
+            str(self.call_monitor.range_stop)
+            if self.call_monitor.range_stop != -1
+            else ""
+        )
+        function_codes = ""
+        for function in self.request_lookup.values():
+            selected = (
+                "selected"
+                if function.function_code == self.call_monitor.function
+                else ""
+            )
+            function_codes += f"<option value={function.function_code} {selected}>{function.function_code_name}</option>"  #type: ignore[attr-defined]
+        simulation_action = (
+            "ACTIVE" if self.call_response.active != RESPONSE_INACTIVE else ""
+        )
+
+        max_len = MAX_FILTER if self.call_monitor.active else 0
+        while len(self.call_list) > max_len:
+            del self.call_list[0]
+        call_rows = ""
+        for entry in reversed(self.call_list):
+            # req_obj = self.request_lookup[entry[1]]
+            call_rows += f"<tr><td>{entry.call} - {entry.fc}</td><td>{entry.address}</td><td>{entry.count}</td><td>{entry.data.decode()}</td></tr>"
+            # line += req_obj.funcion_code_name
+        new_html = (
+            html.replace("<!--SIMULATION_ACTIVE-->", simulation_action)
+            .replace("FUNCTION_RANGE_START", range_start_html)
+            .replace("FUNCTION_RANGE_STOP", range_stop_html)
+            .replace("<!--FUNCTION_CODES-->", function_codes)
+            .replace(
+                "FUNCTION_SHOW_HEX_CHECKED", "checked" if self.call_monitor.hex else ""
+            )
+            .replace(
+                "FUNCTION_SHOW_DECODED_CHECKED",
+                "checked" if self.call_monitor.decode else "",
+            )
+            .replace(
+                "FUNCTION_RESPONSE_NORMAL_CHECKED",
+                "checked" if self.call_response.active == RESPONSE_NORMAL else "",
+            )
+            .replace(
+                "FUNCTION_RESPONSE_ERROR_CHECKED",
+                "checked" if self.call_response.active == RESPONSE_ERROR else "",
+            )
+            .replace(
+                "FUNCTION_RESPONSE_EMPTY_CHECKED",
+                "checked" if self.call_response.active == RESPONSE_EMPTY else "",
+            )
+            .replace(
+                "FUNCTION_RESPONSE_JUNK_CHECKED",
+                "checked" if self.call_response.active == RESPONSE_JUNK else "",
+            )
+            .replace(
+                "FUNCTION_RESPONSE_SPLIT_CHECKED",
+                "checked" if self.call_response.split > 0 else "",
+            )
+            .replace("FUNCTION_RESPONSE_SPLIT_DELAY", str(self.call_response.split))
+            .replace(
+                "FUNCTION_RESPONSE_CR_CHECKED",
+                "checked" if self.call_response.change_rate > 0 else "",
+            )
+            .replace("FUNCTION_RESPONSE_CR_PCT", str(self.call_response.change_rate))
+            .replace("FUNCTION_RESPONSE_DELAY", str(self.call_response.delay))
+            .replace("FUNCTION_RESPONSE_JUNK", str(self.call_response.junk_len))
+            .replace("<!--FUNCTION_ERROR-->", function_error)
+            .replace(
+                "FUNCTION_RESPONSE_CLEAR_AFTER", str(self.call_response.clear_after)
+            )
+            .replace("<!--FC_ROWS-->", call_rows)
+            .replace("<!--FC_FOOT-->", foot)
+        )
+        return new_html
+
+    def build_html_log(self, _params, html):
+        """Build html log page."""
+        return html
+
+    def build_html_server(self, _params, html):
+        """Build html server page."""
+        return html
+
+    def build_json_registers(self, params):
+        """Build json registers response."""
+        # Process params using the helper function
+        result_txt, foot = self.helper_handle_submit(params, {
+            "Set": self.action_set,
+        })
+
+        if not result_txt:
+            result_txt = "ok"
+        if not foot:
+            foot = "Operation completed successfully"
+
+        # Extract necessary parameters
+        try:
+            range_start = int(params.get("range_start", 0))
+            range_stop = int(params.get("range_stop", range_start))
+        except ValueError:
+            return {"result": "error", "error": "Invalid range parameters"}
+
+        # Retrieve register details
+        register_rows = []
+        for i in range(range_start, range_stop + 1):
+            inx, reg = self.datastore_context.get_text_register(i)
+            row = {
+                "index": inx,
+                "type": reg.type,
+                "access": reg.access,
+                "action": reg.action,
+                "value": reg.value,
+                "count_read": reg.count_read,
+                "count_write": reg.count_write
+            }
+            register_rows.append(row)
+
+        # Generate register types and actions (assume these are predefined mappings)
+        register_types = dict(self.datastore_context.registerType_name_to_id)
+        register_actions = dict(self.datastore_context.action_name_to_id)
+
+        # Build the JSON response
+        json_response = {
+            "result": result_txt,
+            "footer": foot,
+            "register_types": register_types,
+            "register_actions": register_actions,
+            "register_rows": register_rows,
+        }
+
+        return json_response
+
+    def build_json_calls(self, params: dict) -> dict:
+        """Build json calls response."""
+        result_txt, foot = self.helper_handle_submit(params, {
+            "Reset": self.action_reset,
+            "Add": self.action_add,
+            "Simulate": self.action_simulate,
+        })
+        if not foot:
+            foot = "Monitoring active" if self.call_monitor.active else "not active"
+        if not result_txt:
+            result_txt = "ok"
+
+        function_error = []
+        for i, txt in (
+            (1, "IllegalFunction"),
+            (2, "IllegalAddress"),
+            (3, "IllegalValue"),
+            (4, "SlaveFailure"),
+            (5, "Acknowledge"),
+            (6, "SlaveBusy"),
+            (7, "MemoryParityError"),
+            (10, "GatewayPathUnavailable"),
+            (11, "GatewayNoResponse"),
+        ):
+            function_error.append({
+                "value": i,
+                "text": txt,
+                "selected": i == self.call_response.error_response
+            })
+
+        range_start = (
+            self.call_monitor.range_start
+            if self.call_monitor.range_start != -1
+            else None
+        )
+        range_stop = (
+            self.call_monitor.range_stop
+            if self.call_monitor.range_stop != -1
+            else None
+        )
+
+        function_codes = []
+        for function in self.request_lookup.values():
+            function_codes.append({
+                "value": function.function_code,
+                "text": function.function_code_name,    # type: ignore[attr-defined]
+                "selected": function.function_code == self.call_monitor.function
+            })
+
+        simulation_action = "ACTIVE" if self.call_response.active != RESPONSE_INACTIVE else ""
+
+        max_len = MAX_FILTER if self.call_monitor.active else 0
+        while len(self.call_list) > max_len:
+            del self.call_list[0]
+        call_rows = []
+        for entry in reversed(self.call_list):
+            call_rows.append({
+                "call": entry.call,
+                "fc": entry.fc,
+                "address": entry.address,
+                "count": entry.count,
+                "data": entry.data.decode()
+            })
+
+        json_response = {
+            "simulation_action": simulation_action,
+            "range_start": range_start,
+            "range_stop": range_stop,
+            "function_codes": function_codes,
+            "function_show_hex_checked": self.call_monitor.hex,
+            "function_show_decoded_checked": self.call_monitor.decode,
+            "function_response_normal_checked": self.call_response.active == RESPONSE_NORMAL,
+            "function_response_error_checked": self.call_response.active == RESPONSE_ERROR,
+            "function_response_empty_checked": self.call_response.active == RESPONSE_EMPTY,
+            "function_response_junk_checked": self.call_response.active == RESPONSE_JUNK,
+            "function_response_split_checked": self.call_response.split > 0,
+            "function_response_split_delay": self.call_response.split,
+            "function_response_cr_checked": self.call_response.change_rate > 0,
+            "function_response_cr_pct": self.call_response.change_rate,
+            "function_response_delay": self.call_response.delay,
+            "function_response_junk": self.call_response.junk_len,
+            "function_error": function_error,
+            "function_response_clear_after": self.call_response.clear_after,
+            "call_rows": call_rows,
+            "foot": foot,
+            "result": result_txt
+        }
+
+        return json_response
+
+    def build_json_log(self, params):
+        """Build json log page."""
+        return {"result": "error", "error": "log endpoint not implemented", "params": params}
+
+    def build_json_server(self, params):
+        """Build html server page."""
+        return {"result": "error", "error": "server endpoint not implemented", "params": params}
+
+    def helper_handle_submit(self, params, submit_actions):
+        """Build html register submit."""
+        try:
+            range_start = int(params.get("range_start", -1))
+        except ValueError:
+            range_start = -1
+        try:
+            range_stop = int(params.get("range_stop", range_start))
+        except ValueError:
+            range_stop = -1
+        if (submit := params["submit"]) not in submit_actions:
+            return None, None
+        return submit_actions[submit](params, range_start, range_stop)
+
+    def action_clear(self, _params, _range_start, _range_stop):
+        """Clear register filter."""
+        self.register_filter = []
+        return None, None
+
+    def action_stop(self, _params, _range_start, _range_stop):
+        """Stop call monitoring."""
+        self.call_monitor = CallTypeMonitor()
+        self.modbus_server.response_manipulator = None
+        self.modbus_server.request_tracer = None
+        return None, "Stopped monitoring"
+
+    def action_reset(self, _params, _range_start, _range_stop):
+        """Reset call simulation."""
+        self.call_response = CallTypeResponse()
+        if not self.call_monitor.active:
+            self.modbus_server.response_manipulator = self.server_response_manipulator
+        return None, None
+
+    def action_add(self, params, range_start, range_stop):
+        """Build list of registers matching filter."""
+        reg_action = int(params.get("action", -1))
+        reg_writeable = "writeable" in params
+        reg_type = int(params.get("type", -1))
+        filter_updated = 0
+        if range_start != -1:
+            steps = range(range_start, range_stop + 1)
+        else:
+            steps = range(1, self.datastore_context.register_count)
+        for i in steps:
+            if range_start != -1 and (i < range_start or i > range_stop):
+                continue
+            reg = self.datastore_context.registers[i]
+            skip_filter = reg_writeable and not reg.access
+            skip_filter |= reg_type not in (-1, reg.type)
+            skip_filter |= reg_action not in (-1, reg.action)
+            skip_filter |= i in self.register_filter
+            if skip_filter:
+                continue
+            self.register_filter.append(i)
+            filter_updated += 1
+            if len(self.register_filter) >= MAX_FILTER:
+                self.register_filter.sort()
+                return None, f"Max. filter size {MAX_FILTER} exceeded!"
+        self.register_filter.sort()
+        return None, None
+
+    def action_monitor(self, params, range_start, range_stop):
+        """Start monitoring calls."""
+        self.call_monitor.range_start = range_start
+        self.call_monitor.range_stop = range_stop
+        self.call_monitor.function = (
+            int(params["function"]) if params["function"] else -1
+        )
+        self.call_monitor.hex = "show_hex" in params
+        self.call_monitor.decode = "show_decode" in params
+        self.call_monitor.active = True
+        self.modbus_server.response_manipulator = self.server_response_manipulator
+        self.modbus_server.request_tracer = self.server_request_tracer
+        return None, None
+
+    def action_set(self, params, _range_start, _range_stop):
+        """Set register value."""
+        if not (register := params["register"]):
+            return "Missing register", None
+        register = int(register)
+        if value := params["value"]:
+            self.datastore_context.registers[register].value = int(value)
+        if bool(params.get("writeable", False)):
+            self.datastore_context.registers[register].access = True
+        return None, None
+
+    def action_simulate(self, params, _range_start, _range_stop):
+        """Simulate responses."""
+        self.call_response.active = int(params["response_type"])
+        if "response_split" in params:
+            if params["split_delay"]:
+                self.call_response.split = int(params["split_delay"])
+            else:
+                self.call_response.split = 1
+        else:
+            self.call_response.split = 0
+        if "response_cr" in params:
+            if params["response_cr_pct"]:
+                self.call_response.change_rate = int(params["response_cr_pct"])
+            else:
+                self.call_response.change_rate = 0
+        else:
+            self.call_response.change_rate = 0
+        if params["response_delay"]:
+            self.call_response.delay = int(params["response_delay"])
+        else:
+            self.call_response.delay = 0
+        if params["response_junk_datalen"]:
+            self.call_response.junk_len = int(params["response_junk_datalen"])
+        else:
+            self.call_response.junk_len = 0
+        self.call_response.error_response = int(params["response_error"])
+        if params["response_clear_after"]:
+            self.call_response.clear_after = int(params["response_clear_after"])
+        else:
+            self.call_response.clear_after = 1
+        self.modbus_server.response_manipulator = self.server_response_manipulator
+        return None, None
+
+    def server_response_manipulator(self, response):
+        """Manipulate responses.
+
+        All server responses passes this filter before being sent.
+        The filter returns:
+
+        - response, either original or modified
+        - skip_encoding, signals whether or not to encode the response
+        """
+        if self.call_monitor.trace_response:
+            tracer = CallTracer(
+                call=False,
+                fc=response.function_code,
+                address=response.address if hasattr(response, "address") else -1,
+                count=response.count if hasattr(response, "count") else -1,
+                data=b"-",
+            )
+            self.call_list.append(tracer)
+            self.call_monitor.trace_response = False
+
+        if self.call_response.active != RESPONSE_INACTIVE:
+            return response, False
+
+        skip_encoding = False
+        if self.call_response.active == RESPONSE_EMPTY:
+            Log.warning("Sending empty response")
+            return None, False
+        if self.call_response.active == RESPONSE_NORMAL:
+            if self.call_response.delay:
+                Log.warning(
+                    "Delaying response by {}s for all incoming requests",
+                    self.call_response.delay,
+                )
+                sleep(self.call_response.delay)  # change to async
+            else:
+                pass
+                # self.call_response.change_rate
+                # self.call_response.split
+        elif self.call_response.active == RESPONSE_ERROR:
+            Log.warning("Sending error response for all incoming requests")
+            err_response = ExceptionResponse(
+                response.function_code, self.call_response.error_response
+            )
+            err_response.transaction_id = response.transaction_id
+            err_response.slave_id = response.slave_id
+        elif self.call_response.active == RESPONSE_JUNK:
+            response = os.urandom(self.call_response.junk_len)
+            skip_encoding = True
+
+        self.call_response.clear_after -= 1
+        if self.call_response.clear_after <= 0:
+            Log.info("Resetting manipulator due to clear_after")
+            self.call_response.active = RESPONSE_EMPTY
+        return response, skip_encoding
+
+    def server_request_tracer(self, request, *_addr):
+        """Trace requests.
+
+        All server requests passes this filter before being handled.
+        """
+        if self.call_monitor.function not in {-1, request.function_code}:
+            return
+        address = request.address if hasattr(request, "address") else -1
+        if self.call_monitor.range_start != -1 and address != -1:
+            if (
+                self.call_monitor.range_start > address
+                or self.call_monitor.range_stop < address
+            ):
+                return
+        tracer = CallTracer(
+            call=True,
+            fc=request.function_code,
+            address=address,
+            count=request.count if hasattr(request, "count") else -1,
+            data=b"-",
+        )
+        self.call_list.append(tracer)
+        self.call_monitor.trace_response = True
diff --git a/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/server/simulator/main.py b/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/server/simulator/main.py
new file mode 100644
index 00000000..bc7e230e
--- /dev/null
+++ b/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/server/simulator/main.py
@@ -0,0 +1,128 @@
+#!/usr/bin/env python3
+"""HTTP server for modbus simulator.
+
+The modbus simulator contain 3 distinct parts:
+
+- Datastore simulator, to define registers and their behaviour including actions: (simulator)(../../datastore/simulator.py)
+- Modbus server: (server)(./http_server.py)
+- HTTP server with REST API and web pages providing an online console in your browser
+
+Multiple setups for different server types and/or devices are prepared in a (json file)(./setup.json), the detailed configuration is explained in (doc)(README.rst)
+
+The command line parameters are kept to a minimum:
+
+usage: main.py [-h] [--modbus_server MODBUS_SERVER]
+               [--modbus_device MODBUS_DEVICE] [--http_host HTTP_HOST]
+               [--http_port HTTP_PORT]
+               [--log {critical,error,warning,info,debug}]
+               [--json_file JSON_FILE]
+               [--custom_actions_module CUSTOM_ACTIONS_MODULE]
+
+Modbus server with REST-API and web server
+
+options:
+  -h, --help            show this help message and exit
+  --modbus_server MODBUS_SERVER
+                        use <modbus_server> from server_list in json file
+  --modbus_device MODBUS_DEVICE
+                        use <modbus_device> from device_list in json file
+  --http_host HTTP_HOST
+                        use <http_host> as host to bind http listen
+  --http_port HTTP_PORT
+                        use <http_port> as port to bind http listen
+  --log {critical,error,warning,info,debug}
+                        set log level, default is info
+  --log_file LOG_FILE
+                        name of server log file, default is "server.log"
+  --json_file JSON_FILE
+                        name of json_file, default is "setup.json"
+  --custom_actions_module CUSTOM_ACTIONS_MODULE
+                        python file with custom actions, default is none
+"""
+import argparse
+import asyncio
+import os
+
+from pymodbus import pymodbus_apply_logging_config
+from pymodbus.logging import Log
+from pymodbus.server.simulator.http_server import ModbusSimulatorServer
+
+
+def get_commandline(extras=None, cmdline=None):
+    """Get command line arguments."""
+    parser = argparse.ArgumentParser(
+        description="Modbus server with REST-API and web server"
+    )
+    parser.add_argument(
+        "--modbus_server",
+        help="use <modbus_server> from server_list in json file",
+        type=str,
+    )
+    parser.add_argument(
+        "--modbus_device",
+        help="use <modbus_device> from device_list in json file",
+        type=str,
+    )
+    parser.add_argument(
+        "--http_host",
+        help="use <http_host> as host to bind http listen",
+        type=str,
+    )
+    parser.add_argument(
+        "--http_port",
+        help="use <http_port> as port to bind http listen",
+        type=str,
+        default=8081,
+    )
+    parser.add_argument(
+        "--log",
+        choices=["critical", "error", "warning", "info", "debug"],
+        help="set log level, default is info",
+        default="info",
+        type=str,
+    )
+    parser.add_argument(
+        "--json_file",
+        help='name of json file, default is "setup.json"',
+        type=str,
+        default=os.path.join(os.path.dirname(__file__), "setup.json"),
+    )
+    parser.add_argument(
+        "--log_file",
+        help='name of server log file, default is "server.log"',
+        type=str,
+    )
+    parser.add_argument(
+        "--custom_actions_module",
+        help="python file with custom actions, default is none",
+        type=str,
+    )
+    if extras:
+        for extra in extras:
+            parser.add_argument(extra[0], **extra[1])
+    args = parser.parse_args(cmdline)
+    pymodbus_apply_logging_config(args.log.upper())
+    Log.info("Start simulator")
+    cmd_args = {}
+    for argument in args.__dict__:
+        if argument == "log":
+            continue
+        if args.__dict__[argument] is not None:
+            cmd_args[argument] = args.__dict__[argument]
+    return cmd_args
+
+
+async def run_main():
+    """Run server async."""
+    cmd_args = get_commandline()
+    task = ModbusSimulatorServer(**cmd_args)
+    await task.run_forever()
+
+
+def main():
+    """Run server."""
+    asyncio.run(run_main(), debug=True)
+
+
+if __name__ == "__main__":
+    main()
diff --git a/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/server/simulator/setup.json b/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/server/simulator/setup.json
new file mode 100644
index 00000000..94c63f4f
--- /dev/null
+++ b/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/server/simulator/setup.json
@@ -0,0 +1,228 @@
+{
+    "server_list": {
+        "server": {
+            "comm": "tcp",
+            "host": "0.0.0.0",
+            "port": 5020,
+            "ignore_missing_slaves": false,
+            "framer": "socket",
+            "identity": {
+                "VendorName": "pymodbus",
+                "ProductCode": "PM",
+                "VendorUrl": "https://github.com/pymodbus-dev/pymodbus/",
+                "ProductName": "pymodbus Server",
+                "ModelName": "pymodbus Server",
+                "MajorMinorRevision": "3.1.0"
+            }
+        },
+        "server_try_serial": {
+            "comm": "serial",
+            "port": "/dev/tty0",
+            "stopbits": 1,
+            "bytesize": 8,
+            "parity": "N",
+            "baudrate": 9600,
+            "timeout": 3,
+            "reconnect_delay": 2,
+            "framer": "rtu",
+            "identity": {
+                "VendorName": "pymodbus",
+                "ProductCode": "PM",
+                "VendorUrl": "https://github.com/pymodbus-dev/pymodbus/",
+                "ProductName": "pymodbus Server",
+                "ModelName": "pymodbus Server",
+                "MajorMinorRevision": "3.1.0"
+            }
+        },
+        "server_try_tls": {
+            "comm": "tls",
+            "host": "0.0.0.0",
+            "port": 5020,
+            "certfile": "certificates/pymodbus.crt",
+            "keyfile": "certificates/pymodbus.key",
+            "ignore_missing_slaves": false,
+            "framer": "tls",
+            "identity": {
+                "VendorName": "pymodbus",
+                "ProductCode": "PM",
+                "VendorUrl": "https://github.com/pymodbus-dev/pymodbus/",
+                "ProductName": "pymodbus Server",
+                "ModelName": "pymodbus Server",
+                "MajorMinorRevision": "3.1.0"
+            }
+        },
+        "server_test_try_udp": {
+            "comm": "udp",
+            "host": "0.0.0.0",
+            "port": 5020,
+            "ignore_missing_slaves": false,
+            "framer": "socket",
+            "identity": {
+                "VendorName": "pymodbus",
+                "ProductCode": "PM",
+                "VendorUrl": "https://github.com/pymodbus-dev/pymodbus/",
+                "ProductName": "pymodbus Server",
+                "ModelName": "pymodbus Server",
+                "MajorMinorRevision": "3.1.0"
+            }
+        }
+    },
+    "device_list": {
+        "device": {
+            "setup": {
+                "co size": 63000,
+                "di size": 63000,
+                "hr size": 63000,
+                "ir size": 63000,
+                "shared blocks": true,
+                "type exception": true,
+                "defaults": {
+                    "value": {
+                        "bits": 0,
+                        "uint16": 0,
+                        "uint32": 0,
+                        "float32": 0.0,
+                        "string": " "
+                    },
+                    "action": {
+                        "bits": null,
+                        "uint16": "increment",
+                        "uint32": "increment",
+                        "float32": "increment",
+                        "string": null
+                    }
+                }
+            },
+            "invalid": [
+                1
+            ],
+            "write": [
+                3
+            ],
+            "bits": [
+                {"addr": 2, "value": 7}
+            ],
+            "uint16": [
+                {"addr": 3, "value": 17001, "action": null},
+                2100
+            ],
+            "uint32": [
+                {"addr": [4, 5], "value": 617001, "action": null},
+                [3037, 3038]
+            ],
+            "float32": [
+                {"addr": [6, 7], "value": 404.17},
+                [4100, 4101]
+            ],
+            "string": [
+                5047,
+                {"addr": [16, 20], "value": "A_B_C_D_E_"}
+            ],
+            "repeat": [
+            ]
+        },
+        "device_try": {
+            "setup": {
+                "co size": 63000,
+                "di size": 63000,
+                "hr size": 63000,
+                "ir size": 63000,
+                "shared blocks": true,
+                "type exception": true,
+                "defaults": {
+                    "value": {
+                        "bits": 0,
+                        "uint16": 0,
+                        "uint32": 0,
+                        "float32": 0.0,
+                        "string": " "
+                    },
+                    "action": {
+                        "bits": null,
+                        "uint16": null,
+                        "uint32": null,
+                        "float32": null,
+                        "string": null
+                    }
+                }
+            },
+            "invalid": [
+                [0, 5],
+                77
+            ],
+            "write": [
+                10
+            ],
+            "bits": [
+                10,
+                1009,
+                [1116, 1119],
+                {"addr": 1144, "value": 1},
+                {"addr": [1148,1149], "value": 32117},
+                {"addr": [1208, 1306], "action": "random"}
+            ],
+            "uint16": [
+                11,
+                2027,
+                [2126, 2129],
+                {"addr": 2164, "value": 1},
+                {"addr": [2168,2169], "value": 32117},
+                {"addr": [2208, 2304], "action": "increment"},
+                {"addr": 2305,
+                    "value": 50,
+                    "action": "increment",
+                    "parameters": {"minval": 45, "maxval": 155}
+                },
+                {"addr": 2306,
+                    "value": 50,
+                    "action": "random",
+                    "parameters": {"minval": 45, "maxval": 55}
+                }
+            ],
+            "uint32": [
+                [12, 13],
+                [3037, 3038],
+                [3136, 3139],
+                {"addr": [3174, 3175], "value": 1},
+                {"addr": [3188,3189], "value": 32514},
+                {"addr": [3308, 3407], "action": null},
+                {"addr": [3688, 3875], "value": 115, "action": "increment"},
+                {"addr": [3876, 3877],
+                    "value": 50000,
+                    "action": "increment",
+                    "parameters": {"minval": 45000, "maxval": 55000}
+                },
+                {"addr": [3878, 3879],
+                    "value": 50000,
+                    "action": "random",
+                    "parameters": {"minval": 45000, "maxval": 55000}
+                }
+            ],
+            "float32": [
+                [14, 15],
+                [4047, 4048],
+                [4146, 4149],
+                {"addr": [4184, 4185], "value": 1},
+                {"addr": [4188, 4191], "value": 32514.2},
+                {"addr": [4308, 4407], "action": null},
+                {"addr": [4688, 4875], "value": 115.7, "action": "increment"},
+                {"addr": [4876, 4877],
+                    "value": 50000.0,
+                    "action": "increment",
+                    "parameters": {"minval": 45000.0, "maxval": 55000.0}
+                },
+                {"addr": [4878, 48779],
+                    "value": 50000.0,
+                    "action": "random",
+                    "parameters": {"minval": 45000.0, "maxval": 55000.0}
+                }
+            ],
+            "string": [
+                {"addr": [16, 20], "value": "A_B_C_D_E_"},
+                {"addr": [529, 544], "value": "Brand name, 32 bytes...........X"}
+            ],
+            "repeat": [
+            ]
+        }
+    }
+}
diff --git a/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/server/simulator/web/apple120.png b/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/server/simulator/web/apple120.png
new file mode 100644
index 00000000..e14c3e3d
Binary files /dev/null and b/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/server/simulator/web/apple120.png differ
diff --git a/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/server/simulator/web/apple152.png b/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/server/simulator/web/apple152.png
new file mode 100644
index 00000000..ff01154f
Binary files /dev/null and b/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/server/simulator/web/apple152.png differ
diff --git a/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/server/simulator/web/apple60.png b/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/server/simulator/web/apple60.png
new file mode 100644
index 00000000..165b54d5
Binary files /dev/null and b/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/server/simulator/web/apple60.png differ
diff --git a/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/server/simulator/web/apple76.png b/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/server/simulator/web/apple76.png
new file mode 100644
index 00000000..582a03a0
Binary files /dev/null and b/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/server/simulator/web/apple76.png differ
diff --git a/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/server/simulator/web/favicon.ico b/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/server/simulator/web/favicon.ico
new file mode 100644
index 00000000..24cf2c39
Binary files /dev/null and b/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/server/simulator/web/favicon.ico differ
diff --git a/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/server/simulator/web/generator/calls b/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/server/simulator/web/generator/calls
new file mode 100644
index 00000000..d12c50ff
--- /dev/null
+++ b/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/server/simulator/web/generator/calls
@@ -0,0 +1,127 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+  <title>Modbus simulator</title>
+  <link rel="icon" type="image/x-icon" href="/favicon.ico">
+  <link rel="apple-touch-icon" href="/apple60.png">
+  <link rel="apple-touch-icon" sizes="76x76" href="/apple76.png">
+  <link rel="apple-touch-icon" sizes="120x120" href="/apple120.png">
+  <link rel="apple-touch-icon" sizes="152x152" href="/apple152.png">
+  <link rel="stylesheet" type="text/css" href="/pymodbus.css">
+  <!--REFRESH-->
+</head>
+<body>
+  <h1><center>Calls</center></h1>
+  <table width="80%" class="listbox">
+    <thead>
+      <tr>
+        <th width="20%">Call/Response</th>
+        <th width="10%">Address</th>
+        <th width="10%">Count</th>
+        <th width="60%">Data</th>
+      </tr>
+    </thead>
+    <tbody>
+      <!--FC_ROWS-->
+    </tbody>
+    <tfoot>
+      <tr>
+        <th colspan="4"><!--FC_FOOT--></th>
+      </tr>
+    </tfoot>
+  </table>
+  <fieldset class="tools_fieldset">
+    <legend>Monitor</legend>
+    <form action="/api/calls" method="get">
+      <table>
+        <tr>
+          <td><label>Register range</label></td>
+          <td>
+            <input type="number" value="FUNCTION_RANGE_START" name="range_start" />
+            <input type="number" value="FUNCTION_RANGE_STOP" name="range_stop" />
+          </td>
+        </tr>
+        <tr>
+          <td><label>Function</label></td>
+          <td>
+            <select name="function">
+              <option value=-1 selected>Any</option>
+              <!--FUNCTION_CODES-->
+            </select>
+          </td>
+        </tr>
+        <tr>
+          <td><label>Show as</label></td>
+          <td>
+            <input type="checkbox" FUNCTION_SHOW_HEX_CHECKED name="show_hex">Hex</input>
+            <input type="checkbox" FUNCTION_SHOW_DECODED_CHECKED name="show_decode">Decoded</input>
+          </td>
+        </tr>
+      </table>
+      <input type="submit" value="Monitor" name="submit" />
+      <input type="submit" value="Stop" name="submit" />
+    </form>
+  </fieldset>
+<fieldset class="tools_fieldset">
+    <legend>Simulate <b><!--SIMULATION_ACTIVE--></b></legend>
+    <form action="/api/calls" method="get">
+      <table>
+        <tr>
+          <td>
+            <input type="radio" name="response_type" value="2" FUNCTION_RESPONSE_EMPTY_CHECKED>Empty</input>
+          </td>
+          <td></td>
+          <td></td>
+        </tr>
+        <tr>
+          <td>
+            <input type="radio" name="response_type" value="0" FUNCTION_RESPONSE_NORMAL_CHECKED>Normal</input>
+          </td>
+          <td><Label>split response</Label></td>
+          <td>
+            <input type="checkbox" name="response_split" FUNCTION_RESPONSE_SPLIT_CHECKED/>
+            <input type="number" name="split_delay" value="FUNCTION_RESPONSE_SPLIT_DELAY"/>seconds delay
+          </td>
+        </tr>
+        <tr>
+          <td></td>
+          <td><Label>Change rate</Label></td>
+          <td>
+            <input type="checkbox" name="response_cr" FUNCTION_RESPONSE_CR_CHECKED/>
+            <input type="number" name="response_cr_pct" value="FUNCTION_RESPONSE_CR_PCT"/>%
+          </td>
+        </tr>
+        <tr>
+          <td></td>
+          <td><Label>Delay response</Label></td>
+          <td><input type="number" name="response_delay" value="FUNCTION_RESPONSE_DELAY"/>seconds</td>
+        </tr>
+        <tr>
+          <td>
+            <input type="radio" name="response_type" value="1" FUNCTION_RESPONSE_ERROR_CHECKED>Error</input>
+          </td>
+          <td></td>
+          <td>
+            <select name="response_error">
+              <!--FUNCTION_ERROR-->
+            </select>
+          </td>
+        </tr>
+        <tr>
+          <td>
+            <input type="radio" name="response_type" value="3" FUNCTION_RESPONSE_JUNK_CHECKED>Junk</input>
+          </td>
+          <td><Label>Datalength</Label></td>
+          <td><input type="number" name="response_junk_datalen" value="FUNCTION_RESPONSE_JUNK" />bytes</td>
+        </tr>
+        <tr>
+          <td colspan="2"><Label>Clear after</Label></td>
+          <td><input type="number" name="response_clear_after" value="FUNCTION_RESPONSE_CLEAR_AFTER" />requests</td>
+        </tr>
+      </table>
+      <input type="submit" value="Simulate" name="submit" />
+      <input type="submit" value="Reset" name="submit" />
+  </form>
+  </fieldset>
+</body>
+</html>
diff --git a/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/server/simulator/web/generator/log b/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/server/simulator/web/generator/log
new file mode 100644
index 00000000..db903a5c
--- /dev/null
+++ b/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/server/simulator/web/generator/log
@@ -0,0 +1,39 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+  <title>Modbus simulator.</title>
+  <link rel="icon" type="image/x-icon" href="/favicon.ico">
+  <link rel="apple-touch-icon" href="/apple60.png">
+  <link rel="apple-touch-icon" sizes="76x76" href="/apple76.png">
+  <link rel="apple-touch-icon" sizes="120x120" href="/apple120.png">
+  <link rel="apple-touch-icon" sizes="152x152" href="/apple152.png">
+  <link rel="stylesheet" type="text/css" href="/pymodbus.css">
+  <!--REFRESH-->
+</head>
+<body>
+  <center><h1>Log</h1></center>
+  <table width="80%" class="listbox">
+    <thead>
+      <tr>
+        <th width="10%">Log entries</th>
+      </tr>
+    </thead>
+    <tbody>
+      <!--LOG_ROWS-->
+    </tbody>
+    <tfoot>
+      <tr>
+        <th colspan="7"><!--LOG_FOOT--></th>
+      </tr>
+    </tfoot>
+  </table>
+  <fieldset class="tools_fieldset" width="30%">
+    <legend>Log</legend>
+    <form action="/api/log" method="get">
+      <input type="submit" value="Download" name="submit" />
+      <input type="submit" value="Monitor" name="submit" />
+      <input type="submit" value="Clear" name="submit" />
+    </form>
+  </fieldset>
+</body>
+</html>
diff --git a/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/server/simulator/web/generator/pymodbus_icon_original.png b/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/server/simulator/web/generator/pymodbus_icon_original.png
new file mode 100644
index 00000000..dfb60cd5
Binary files /dev/null and b/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/server/simulator/web/generator/pymodbus_icon_original.png differ
diff --git a/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/server/simulator/web/generator/registers b/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/server/simulator/web/generator/registers
new file mode 100644
index 00000000..be147550
--- /dev/null
+++ b/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/server/simulator/web/generator/registers
@@ -0,0 +1,93 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+  <title>Modbus simulator</title>
+  <link rel="icon" type="image/x-icon" href="/favicon.ico">
+  <link rel="apple-touch-icon" href="/apple60.png">
+  <link rel="apple-touch-icon" sizes="76x76" href="/apple76.png">
+  <link rel="apple-touch-icon" sizes="120x120" href="/apple120.png">
+  <link rel="apple-touch-icon" sizes="152x152" href="/apple152.png">
+  <link rel="stylesheet" type="text/css" href="/pymodbus.css">
+  <!--REFRESH-->
+</head>
+<body>
+  <h1><center>Registers</center></h1>
+  <table width="80%" class="listbox">
+    <thead>
+      <tr>
+        <th width="10%">Register</th>
+        <th width="10%">Type</th>
+        <th width="10%">Write</th>
+        <th width="10%">Action</th>
+        <th width="10%">Value</th>
+        <th width="10%"># read</th>
+        <th width="10%"># write</th>
+      </tr>
+    </thead>
+    <tbody>
+      <!--REGISTER_ROWS-->
+    </tbody>
+    <tfoot>
+      <tr>
+        <th colspan="7"><!--REGISTER_FOOT--></th>
+      </tr>
+    </tfoot>
+  </table>
+  <fieldset class="tools_fieldset" width="40%">
+    <legend>Filter registers</legend>
+    <form action="/api/registers" method="get">
+      <table>
+        <tr>
+          <td><label>Start/end</label></td>
+          <td>
+            <input type="number" name="range_start" />
+            <input type="number" name="range_stop" />
+          </td>
+        </tr>
+        <tr>
+          <td><label>Type</label></td>
+          <td>
+            <select name="type">
+              <option value=-1 selected>Any</option>
+              <!--REGISTER_TYPES-->
+            </select>
+          </td>
+        </tr>
+        <tr>
+          <td><label>Action</label></td>
+          <td>
+            <select name="action">
+              <option value=-1 selected>Any</option>
+              <!--REGISTER_ACTIONS-->
+            </select>
+          </td>
+        </tr>
+        <tr>
+          <td><label>Writeable</label></td>
+          <td><input type="checkbox" name="writeable" /><br></td>
+        </tr>
+      </table>
+      <input type="submit" value="Add" name="submit" />
+      <input type="submit" value="Clear" name="submit" />
+    </form>
+  </fieldset>
+  <fieldset class="tools_fieldset" width="20%">
+    <legend>Set</legend>
+    <form action="/api/registers" method="get">
+      <table>
+        <tr>
+          <td><label>Register</label></td>
+          <td><input type="number" name="register" /></td>
+        </tr>
+        <tr>
+          <td><label>Value</label></td>
+          <td><input type="text" name="value" /></td>
+        </tr>
+
+      </table>
+      <input type="submit" value="Set" name="submit" />
+    </form>
+  </fieldset><br>
+  <p>Result of last command: <!--RESULT--></p>
+</body>
+</html>
diff --git a/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/server/simulator/web/generator/server b/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/server/simulator/web/generator/server
new file mode 100644
index 00000000..b628e787
--- /dev/null
+++ b/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/server/simulator/web/generator/server
@@ -0,0 +1,27 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+  <title>Modbus simulator.</title>
+  <link rel="icon" type="image/x-icon" href="/favicon.ico">
+  <link rel="apple-touch-icon" href="/apple60.png">
+  <link rel="apple-touch-icon" sizes="76x76" href="/apple76.png">
+  <link rel="apple-touch-icon" sizes="120x120" href="/apple120.png">
+  <link rel="apple-touch-icon" sizes="152x152" href="/apple152.png">
+  <link rel="stylesheet" type="text/css" href="/pymodbus.css">
+  <!--REFRESH-->
+</head>
+<body>
+  <body>
+    <center><h1>Server</h1></center>
+    <fieldset class="tools_fieldset" width="30%">
+      <legend>Status</legend>
+      Uptime: <!--UPTIME-->
+    </fieldset>
+    <fieldset class="tools_fieldset" width="30%">
+      <legend>Status</legend>
+      <form action="/api/server" method="get">
+        <input type="submit" value="Restart" name="submit" />
+      </form>
+    </fieldset>
+  </body>
+</html>
diff --git a/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/server/simulator/web/index.html b/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/server/simulator/web/index.html
new file mode 100644
index 00000000..f0984420
--- /dev/null
+++ b/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/server/simulator/web/index.html
@@ -0,0 +1,61 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+  <title>Modbus simulator.</title>
+  <link rel="icon" type="image/x-icon" href="/favicon.ico">
+  <link rel="apple-touch-icon" href="/apple60.png">
+  <link rel="apple-touch-icon" sizes="76x76" href="/apple76.png">
+  <link rel="apple-touch-icon" sizes="120x120" href="/apple120.png">
+  <link rel="apple-touch-icon" sizes="152x152" href="/apple152.png">
+  <link rel="stylesheet" type="text/css" href="/pymodbus.css">
+  <style rel="stylesheet" type="text/css" media="screen">
+    .sidenav {
+      height: 100%;
+      width: 160px;
+      position: fixed;
+      z-index: 1;
+      top: 0;
+      left: 0;
+      background-color: gray;
+      overflow-x: hidden;
+      padding-top: 5px;
+    }
+    .main {
+      margin-left: 160px;
+      top: 0;
+      left: 0;
+      font-size: 28px;
+      padding: 0px 0px;
+      width: 100% - 160px;
+      height: 100%;
+    }
+    .sidenav legend {
+      color: white
+    }
+  </style>
+</head>
+<body>
+  <div class="sidenav">
+    <a href="welcome.html" target="editor">Welcome</a>
+    <form action="/api" method="get" target="editor">
+      <fieldset>
+        <legend>Refresh rate</legend>
+        <input type="number" style="width: 60%;" value=0 name="refresh">
+      </fieldset>
+      <fieldset>
+        <legend>View</legend>
+        <input type="submit" formaction="/api/registers" value="Registers" name="submit" /><br>
+        <input type="submit" formaction="/api/calls" value="Calls" name="submit" /><br>
+        <input type="submit" formaction="/api/log" value="Log" name="submit" /><br>
+        <input type="submit" formaction="/api/server" value="Server" name="submit" />
+      </fieldset>
+    </form>
+    <p>Powered by:
+      <a href="https://github.com/pymodbus-dev/pymodbus"><b>pymodbus</b></a>  an open source project, patches are welcome.
+    </p>
+  </div>
+  <div class="main">
+    <iframe name="editor" title="Simulator" src="welcome.html"/>
+  </div>
+</body>
+</html>
diff --git a/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/server/simulator/web/pymodbus.css b/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/server/simulator/web/pymodbus.css
new file mode 100644
index 00000000..bac637dc
--- /dev/null
+++ b/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/server/simulator/web/pymodbus.css
@@ -0,0 +1,62 @@
+html {
+  height: 100%;
+  width: 100%;
+}
+body {
+  height: 100%;
+  width: 100%;
+  background-color: bisque;
+}
+table.listbox {
+  border-collapse: collapse;
+  border: 1px solid black;
+}
+table.listbox th {
+  background-color: lightgray;
+  border: 1px solid black;
+  padding: 5px
+}
+table.listbox td {
+  border: 1px solid black;
+  text-align: right;
+  background-color: #f1f1f1;
+  padding: 5px
+}
+legend {
+  font-size:  18px;
+  font-weight:  bold;
+  position:  relative;
+  color: black;
+  padding: 5px 5px;
+}
+a {
+  padding: 2px 4px 2px 4px;
+  text-decoration: none;
+  font-size: 18px;
+  display: block;
+}
+a:hover {
+  color: #f1f1f1;
+}
+p {
+  padding: 2px 4px 6px 4px;
+  display: block;
+}
+iframe {
+  margin: 0;
+  padding: 0;
+  width: 100%;
+  height: 100%;
+  border: 0;
+}
+input[type="submit"] {
+  font-size:  14px;
+  background-color: lightblue;
+  margin-top: 10px;
+}
+
+
+.tools_fieldset {
+  display: inline;
+  vertical-align:top
+}
diff --git a/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/server/simulator/web/welcome.html b/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/server/simulator/web/welcome.html
new file mode 100644
index 00000000..46e18398
--- /dev/null
+++ b/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/server/simulator/web/welcome.html
@@ -0,0 +1,21 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+  <title>Modbus simulator.</title>
+  <link rel="icon" type="image/x-icon" href="/favicon.ico">
+  </style>
+</head>
+<body>
+  <center><h1>Welcome to the pymodbus simulator</h1></center>
+  <p>Thanks for using pymodbus.</p>
+  <p>the pymodbus development team</p>
+  <br><br>
+  The <b>View</b> to the left, are used to control the simulator.
+  <ul>
+    <li><b>Registers</b> are used to monitor and/or change registers in the configuration (non-resistent),</li>
+    <li><b>Calls</b> are used to show and/or modify call from clients,</li>
+    <li><b>Log</b> are used to show the server log,</li>
+    <li><b>Server</b> are used to control the server.</li>
+  </ul>
+</body>
+</html>
diff --git a/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/transaction.py b/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/transaction.py
new file mode 100644
index 00000000..1b2f9e61
--- /dev/null
+++ b/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/transaction.py
@@ -0,0 +1,435 @@
+"""Collection of transaction based abstractions."""
+from __future__ import annotations
+
+
+__all__ = [
+    "ModbusTransactionManager",
+    "SyncModbusTransactionManager",
+]
+
+import struct
+import time
+from contextlib import suppress
+from threading import RLock
+from typing import TYPE_CHECKING
+
+from pymodbus.exceptions import (
+    ConnectionException,
+    InvalidMessageReceivedException,
+    ModbusIOException,
+)
+from pymodbus.framer import (
+    FramerAscii,
+    FramerRTU,
+    FramerSocket,
+    FramerTLS,
+)
+from pymodbus.logging import Log
+from pymodbus.pdu import ModbusPDU
+from pymodbus.transport import CommType
+from pymodbus.utilities import ModbusTransactionState, hexlify_packets
+
+
+if TYPE_CHECKING:
+    from pymodbus.client.base import ModbusBaseSyncClient
+
+
+# --------------------------------------------------------------------------- #
+# The Global Transaction Manager
+# --------------------------------------------------------------------------- #
+class ModbusTransactionManager:
+    """Implement a transaction for a manager.
+
+    Results are keyed based on the supplied transaction id.
+    """
+
+    def __init__(self):
+        """Initialize an instance of the ModbusTransactionManager."""
+        self.tid = 0
+        self.transactions: dict[int, ModbusPDU] = {}
+
+    def __iter__(self):
+        """Iterate over the current managed transactions.
+
+        :returns: An iterator of the managed transactions
+        """
+        return iter(self.transactions.keys())
+
+    def addTransaction(self, request: ModbusPDU):
+        """Add a transaction to the handler.
+
+        This holds the request in case it needs to be resent.
+        After being sent, the request is removed.
+
+        :param request: The request to hold on to
+        """
+        tid = request.transaction_id
+        Log.debug("Adding transaction {}", tid)
+        self.transactions[tid] = request
+
+    def getTransaction(self, tid: int):
+        """Return a transaction matching the referenced tid.
+
+        If the transaction does not exist, None is returned
+
+        :param tid: The transaction to retrieve
+
+        """
+        Log.debug("Getting transaction {}", tid)
+        if not tid:
+            if self.transactions:
+                ret = self.transactions.popitem()[1]
+                self.transactions.clear()
+                return ret
+            return None
+        return self.transactions.pop(tid, None)
+
+    def delTransaction(self, tid: int):
+        """Remove a transaction matching the referenced tid.
+
+        :param tid: The transaction to remove
+        """
+        Log.debug("deleting transaction {}", tid)
+        self.transactions.pop(tid, None)
+
+    def getNextTID(self) -> int:
+        """Retrieve the next unique transaction identifier.
+
+        This handles incrementing the identifier after
+        retrieval
+
+        :returns: The next unique transaction identifier
+        """
+        if self.tid < 65000:
+            self.tid += 1
+        else:
+            self.tid = 1
+        return self.tid
+
+    def reset(self):
+        """Reset the transaction identifier."""
+        self.tid = 0
+        self.transactions = {}
+
+
+class SyncModbusTransactionManager(ModbusTransactionManager):
+    """Implement a transaction for a manager.
+
+    The transaction protocol can be represented by the following pseudo code::
+
+        count = 0
+        do
+          result = send(message)
+          if (timeout or result == bad)
+             count++
+          else break
+        while (count < 3)
+
+    This module helps to abstract this away from the framer and protocol.
+
+    Results are keyed based on the supplied transaction id.
+    """
+
+    def __init__(self, client: ModbusBaseSyncClient, retries):
+        """Initialize an instance of the ModbusTransactionManager."""
+        super().__init__()
+        self.client: ModbusBaseSyncClient = client
+        self.retries = retries
+        self._transaction_lock = RLock()
+        self._no_response_devices: list[int] = []
+        self.databuffer = b''
+        if client:
+            self._set_adu_size()
+
+    def _set_adu_size(self):
+        """Set adu size."""
+        # base ADU size of modbus frame in bytes
+        if isinstance(self.client.framer, FramerSocket):
+            self.base_adu_size = 7  # tid(2), pid(2), length(2), uid(1)
+        elif isinstance(self.client.framer, FramerRTU):
+            self.base_adu_size = 3  # address(1), CRC(2)
+        elif isinstance(self.client.framer, FramerAscii):
+            self.base_adu_size = 7  # start(1)+ Address(2), LRC(2) + end(2)
+        elif isinstance(self.client.framer, FramerTLS):
+            self.base_adu_size = 0  # no header and footer
+        else:
+            self.base_adu_size = -1
+
+    def _calculate_response_length(self, expected_pdu_size):
+        """Calculate response length."""
+        if self.base_adu_size == -1:
+            return None
+        return self.base_adu_size + expected_pdu_size
+
+    def _calculate_exception_length(self):
+        """Return the length of the Modbus Exception Response according to the type of Framer."""
+        if isinstance(self.client.framer, (FramerSocket, FramerTLS)):
+            return self.base_adu_size + 2  # Fcode(1), ExceptionCode(1)
+        if isinstance(self.client.framer, FramerAscii):
+            return self.base_adu_size + 4  # Fcode(2), ExceptionCode(2)
+        if isinstance(self.client.framer, FramerRTU):
+            return self.base_adu_size + 2  # Fcode(1), ExceptionCode(1)
+        return None
+
+    def _validate_response(self, response):
+        """Validate Incoming response against request."""
+        if not response:
+            return False
+        return True
+
+    def execute(self, no_response_expected: bool, request: ModbusPDU):  # noqa: C901
+        """Start the producer to send the next request to consumer.write(Frame(request))."""
+        with self._transaction_lock:
+            try:
+                Log.debug(
+                    "Current transaction state - {}",
+                    ModbusTransactionState.to_string(self.client.state),
+                )
+                retries = self.retries
+                if isinstance(self.client.framer, FramerSocket):
+                    request.transaction_id = self.getNextTID()
+                else:
+                    request.transaction_id = 0
+                Log.debug("Running transaction {}", request.transaction_id)
+                if _buffer := hexlify_packets(
+                    self.client.framer.databuffer
+                ):
+                    Log.debug("Clearing current Frame: - {}", _buffer)
+                    self.client.framer.databuffer = b''
+                expected_response_length = None
+                if not isinstance(self.client.framer, FramerSocket):
+                    response_pdu_size = request.get_response_pdu_size()
+                    if isinstance(self.client.framer, FramerAscii):
+                        response_pdu_size *= 2
+                    if response_pdu_size:
+                        expected_response_length = (
+                            self._calculate_response_length(response_pdu_size)
+                        )
+                if (  # pylint: disable=simplifiable-if-statement
+                    request.slave_id in self._no_response_devices
+                ):
+                    full = True
+                else:
+                    full = False
+                if self.client.comm_params.comm_type == CommType.UDP:
+                    full = True
+                    if not expected_response_length:
+                        expected_response_length = 1024
+                response, last_exception = self._transact(
+                    no_response_expected,
+                    request,
+                    expected_response_length,
+                    full=full,
+                )
+                if no_response_expected:
+                    return None
+                while retries > 0:
+                    if self._validate_response(response):
+                        if (
+                            request.slave_id in self._no_response_devices
+                            and response
+                        ):
+                            self._no_response_devices.remove(request.slave_id)
+                            Log.debug("Got response!!!")
+                        break
+                    if not response:
+                        if request.slave_id not in self._no_response_devices:
+                            self._no_response_devices.append(request.slave_id)
+                    # No response received and retries not enabled
+                    break
+                self.databuffer += response
+                used_len, pdu = self.client.framer.processIncomingFrame(self.databuffer)
+                self.databuffer = self.databuffer[used_len:]
+                if pdu:
+                    self.addTransaction(pdu)
+                if not (result := self.getTransaction(request.transaction_id)):
+                    if len(self.transactions):
+                        result = self.getTransaction(0)
+                    else:
+                        last_exception = last_exception or (
+                            "No Response received from the remote slave"
+                            "/Unable to decode response"
+                        )
+                        result = ModbusIOException(
+                            last_exception, request.function_code
+                        )
+                        self.client.close()
+                if hasattr(self.client, "state"):
+                    Log.debug(
+                        "Changing transaction state from "
+                        '"PROCESSING REPLY" to '
+                        '"TRANSACTION_COMPLETE"'
+                    )
+                    self.client.state = ModbusTransactionState.TRANSACTION_COMPLETE
+                return result
+            except ModbusIOException as exc:
+                # Handle decode errors method
+                Log.error("Modbus IO exception {}", exc)
+                self.client.state = ModbusTransactionState.TRANSACTION_COMPLETE
+                self.client.close()
+                return exc
+
+    def _retry_transaction(self, no_response_expected, retries, reason, packet, response_length, full=False):
+        """Retry transaction."""
+        Log.debug("Retry on {} response - {}", reason, retries)
+        Log.debug('Changing transaction state from "WAITING_FOR_REPLY" to "RETRYING"')
+        self.client.state = ModbusTransactionState.RETRYING
+        self.client.connect()
+        if hasattr(self.client, "_in_waiting"):
+            if (
+                in_waiting := self.client._in_waiting()  # pylint: disable=protected-access
+            ):
+                if response_length == in_waiting:
+                    result = self._recv(response_length, full)
+                    return result, None
+        return self._transact(no_response_expected, packet, response_length, full=full)
+
+    def _transact(self, no_response_expected: bool, request: ModbusPDU, response_length, full=False):
+        """Do a Write and Read transaction."""
+        last_exception = None
+        try:
+            self.client.connect()
+            packet = self.client.framer.buildFrame(request)
+            Log.debug("SEND: {}", packet, ":hex")
+            size = self._send(packet)
+            if (
+                isinstance(size, bytes)
+                and self.client.state == ModbusTransactionState.RETRYING
+            ):
+                Log.debug(
+                    "Changing transaction state from "
+                    '"RETRYING" to "PROCESSING REPLY"'
+                )
+                self.client.state = ModbusTransactionState.PROCESSING_REPLY
+                return size, None
+            if self.client.comm_params.handle_local_echo is True:
+                if self._recv(size, full) != packet:
+                    return b"", "Wrong local echo"
+            if no_response_expected:
+                if size:
+                    Log.debug(
+                        'Changing transaction state from "SENDING" '
+                        'to "TRANSACTION_COMPLETE"'
+                    )
+                    self.client.state = ModbusTransactionState.TRANSACTION_COMPLETE
+                return b"", None
+            if size:
+                Log.debug(
+                    'Changing transaction state from "SENDING" '
+                    'to "WAITING FOR REPLY"'
+                )
+                self.client.state = ModbusTransactionState.WAITING_FOR_REPLY
+            result = self._recv(response_length, full)
+            # result2 = self._recv(response_length, full)
+            Log.debug("RECV: {}", result, ":hex")
+        except (OSError, ModbusIOException, InvalidMessageReceivedException, ConnectionException) as msg:
+            self.client.close()
+            Log.debug("Transaction failed. ({}) ", msg)
+            last_exception = msg
+            result = b""
+        return result, last_exception
+
+    def _send(self, packet: bytes, _retrying=False):
+        """Send."""
+        return self.client.send(packet)
+
+    def _recv(self, expected_response_length, full) -> bytes:  # noqa: C901
+        """Receive."""
+        total = None
+        if not full:
+            exception_length = self._calculate_exception_length()
+            if isinstance(self.client.framer, FramerSocket):
+                min_size = 8
+            elif isinstance(self.client.framer, FramerRTU):
+                min_size = 4
+            elif isinstance(self.client.framer, FramerAscii):
+                min_size = 5
+            else:
+                min_size = expected_response_length
+
+            read_min = self.client.recv(min_size)
+            if min_size and len(read_min) != min_size:
+                msg_start = "Incomplete message" if read_min else "No response"
+                raise InvalidMessageReceivedException(
+                    f"{msg_start} received, expected at least {min_size} bytes "
+                    f"({len(read_min)} received)"
+                )
+            if read_min:
+                if isinstance(self.client.framer, FramerSocket):
+                    func_code = int(read_min[-1])
+                elif isinstance(self.client.framer, FramerRTU):
+                    func_code = int(read_min[1])
+                elif isinstance(self.client.framer, FramerAscii):
+                    func_code = int(read_min[3:5], 16)
+                else:
+                    func_code = -1
+
+                if func_code < 0x80:  # Not an error
+                    if isinstance(self.client.framer, FramerSocket):
+                        length = struct.unpack(">H", read_min[4:6])[0] - 1
+                        expected_response_length = 7 + length
+                    elif expected_response_length is None and isinstance(
+                        self.client.framer, FramerRTU
+                    ):
+                        with suppress(
+                            IndexError  # response length indeterminate with available bytes
+                        ):
+                            expected_response_length = (
+                                self._get_expected_response_length(
+                                    read_min
+                                )
+                            )
+                    if expected_response_length is not None:
+                        expected_response_length -= min_size
+                        total = expected_response_length + min_size
+                else:
+                    expected_response_length = exception_length - min_size
+                    total = expected_response_length + min_size
+            else:
+                total = expected_response_length
+            retries = 0
+            missing_len = expected_response_length
+            result = read_min
+            while missing_len and retries < self.retries:
+                if retries:
+                    time.sleep(0.1)
+                data = self.client.recv(expected_response_length)
+                result += data
+                missing_len -= len(data)
+                retries += 1
+        else:
+            read_min = b""
+            total = expected_response_length
+            result = self.client.recv(expected_response_length)
+            result = read_min + result
+        actual = len(result)
+        if total is not None and actual != total:
+            msg_start = "Incomplete message" if actual else "No response"
+            Log.debug(
+                "{} received, Expected {} bytes Received {} bytes !!!!",
+                msg_start,
+                total,
+                actual,
+            )
+        elif not actual:
+            # If actual == 0 and total is not None then the above
+            # should be triggered, so total must be None here
+            Log.debug("No response received to unbounded read !!!!")
+        if self.client.state != ModbusTransactionState.PROCESSING_REPLY:
+            Log.debug(
+                "Changing transaction state from "
+                '"WAITING FOR REPLY" to "PROCESSING REPLY"'
+            )
+            self.client.state = ModbusTransactionState.PROCESSING_REPLY
+        return result
+
+    def _get_expected_response_length(self, data) -> int:
+        """Get the expected response length.
+
+        :param data: Message data read so far
+        :raises IndexError: If not enough data to read byte count
+        :return: Total frame size
+        """
+        func_code = int(data[1])
+        pdu_class = self.client.framer.decoder.lookupPduClass(func_code)
+        return pdu_class.calculateRtuFrameSize(data)
diff --git a/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/transport/__init__.py b/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/transport/__init__.py
new file mode 100644
index 00000000..dd8ce04f
--- /dev/null
+++ b/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/transport/__init__.py
@@ -0,0 +1,14 @@
+"""Transport."""
+__all__ = [
+    "CommParams",
+    "CommType",
+    "ModbusProtocol",
+    "NULLMODEM_HOST",
+]
+
+from pymodbus.transport.transport import (
+    NULLMODEM_HOST,
+    CommParams,
+    CommType,
+    ModbusProtocol,
+)
diff --git a/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/transport/serialtransport.py b/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/transport/serialtransport.py
new file mode 100644
index 00000000..11759c2a
--- /dev/null
+++ b/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/transport/serialtransport.py
@@ -0,0 +1,181 @@
+"""asyncio serial support for modbus (based on pyserial)."""
+from __future__ import annotations
+
+import asyncio
+import contextlib
+import os
+import sys
+
+
+with contextlib.suppress(ImportError):
+    import serial
+
+
+class SerialTransport(asyncio.Transport):
+    """An asyncio serial transport."""
+
+    force_poll: bool = os.name == "nt"
+
+    def __init__(self, loop, protocol, url, baudrate, bytesize, parity, stopbits, timeout) -> None:
+        """Initialize."""
+        super().__init__()
+        if "serial" not in sys.modules:
+            raise RuntimeError(
+                "Serial client requires pyserial "
+                'Please install with "pip install pyserial" and try again.'
+            )
+        self.async_loop = loop
+        self.intern_protocol: asyncio.BaseProtocol = protocol
+        self.sync_serial = serial.serial_for_url(url, exclusive=True,
+            baudrate=baudrate, bytesize=bytesize, parity=parity, stopbits=stopbits, timeout=timeout
+)
+        self.intern_write_buffer: list[bytes] = []
+        self.poll_task: asyncio.Task | None = None
+        self._poll_wait_time = 0.0005
+        self.sync_serial.timeout = 0
+        self.sync_serial.write_timeout = 0
+
+    def setup(self) -> None:
+        """Prepare to read/write."""
+        if self.force_poll:
+            self.poll_task = asyncio.create_task(self.polling_task())
+            self.poll_task.set_name("SerialTransport poll")
+        else:
+            self.async_loop.add_reader(self.sync_serial.fileno(), self.intern_read_ready)
+        self.async_loop.call_soon(self.intern_protocol.connection_made, self)
+
+    def close(self, exc: Exception | None = None) -> None:
+        """Close the transport gracefully."""
+        if not self.sync_serial:
+            return
+        self.flush()
+        if self.poll_task:
+            self.poll_task.cancel()
+            self.poll_task = None
+        else:
+            self.async_loop.remove_reader(self.sync_serial.fileno())
+            self.async_loop.remove_writer(self.sync_serial.fileno())
+        self.sync_serial.close()
+        self.sync_serial = None  # type: ignore[assignment]
+        if exc:
+            with contextlib.suppress(Exception):
+                self.intern_protocol.connection_lost(exc)
+
+    def write(self, data) -> None:
+        """Write some data to the transport."""
+        self.intern_write_buffer.append(data)
+        if not self.force_poll:
+            self.async_loop.add_writer(self.sync_serial.fileno(), self.intern_write_ready)
+
+    def flush(self) -> None:
+        """Clear output buffer and stops any more data being written."""
+        if not self.poll_task:
+            self.async_loop.remove_writer(self.sync_serial.fileno())
+        self.intern_write_buffer.clear()
+
+    # ------------------------------------------------
+    # Dummy methods needed to please asyncio.Transport.
+    # ------------------------------------------------
+    @property
+    def loop(self):
+        """Return asyncio event loop."""
+        return self.async_loop
+
+    def get_protocol(self) -> asyncio.BaseProtocol:
+        """Return protocol."""
+        return self.intern_protocol
+
+    def set_protocol(self, protocol: asyncio.BaseProtocol) -> None:
+        """Set protocol."""
+        self.intern_protocol = protocol
+
+    def get_write_buffer_limits(self) -> tuple[int, int]:
+        """Return buffer sizes."""
+        return (1, 1024)
+
+    def can_write_eof(self):
+        """Return Serial do not support end-of-file."""
+        return False
+
+    def write_eof(self):
+        """Write end of file marker."""
+
+    def set_write_buffer_limits(self, high=None, low=None):
+        """Set the high- and low-water limits for write flow control."""
+
+    def get_write_buffer_size(self):
+        """Return The number of bytes in the write buffer."""
+        return len(self.intern_write_buffer)
+
+    def is_reading(self) -> bool:
+        """Return true if read is active."""
+        return True
+
+    def pause_reading(self):
+        """Pause receiver."""
+
+    def resume_reading(self):
+        """Resume receiver."""
+
+    def is_closing(self):
+        """Return True if the transport is closing or closed."""
+        return False
+
+    def abort(self) -> None:
+        """Alias for closing the connection."""
+        self.close()
+
+    # ------------------------------------------------
+
+    def intern_read_ready(self) -> None:
+        """Test if there are data waiting."""
+        try:
+            if data := self.sync_serial.read(1024):
+                self.intern_protocol.data_received(data)  # type: ignore[attr-defined]
+        except serial.SerialException as exc:
+            self.close(exc=exc)
+
+    def intern_write_ready(self) -> None:
+        """Asynchronously write buffered data."""
+        data = b"".join(self.intern_write_buffer)
+        try:
+            if (nlen := self.sync_serial.write(data)) and nlen < len(data):
+                self.intern_write_buffer = [data[nlen:]]
+                if not self.poll_task:
+                    self.async_loop.add_writer(
+                        self.sync_serial.fileno(), self.intern_write_ready
+                    )
+                return
+            self.flush()
+        except (BlockingIOError, InterruptedError):
+            return
+        except serial.SerialException as exc:
+            self.close(exc=exc)
+
+    async def polling_task(self):
+        """Poll and try to read/write."""
+        while self.sync_serial:
+            await asyncio.sleep(self._poll_wait_time)
+            while self.intern_write_buffer:
+                self.intern_write_ready()
+            if self.sync_serial.in_waiting:
+                self.intern_read_ready()
+
+async def create_serial_connection(
+    loop, protocol_factory, url,
+    baudrate=None,
+    bytesize=None,
+    parity=None,
+    stopbits=None,
+    timeout=None,
+) -> tuple[asyncio.Transport, asyncio.BaseProtocol]:
+    """Create a connection to a new serial port instance."""
+    protocol = protocol_factory()
+    transport = SerialTransport(loop, protocol, url,
+                    baudrate,
+                    bytesize,
+                    parity,
+                    stopbits,
+                    timeout)
+    loop.call_soon(transport.setup)
+    return transport, protocol
diff --git a/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/transport/transport.py b/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/transport/transport.py
new file mode 100644
index 00000000..7fa1a92d
--- /dev/null
+++ b/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/transport/transport.py
@@ -0,0 +1,660 @@
+"""ModbusProtocol layer.
+
+Contains pure transport methods needed to
+- connect/listen,
+- send/receive
+- close/abort connections
+for unix socket, tcp, tls and serial communications as well as a special
+null modem option.
+
+Contains high level methods like reconnect.
+
+All transport differences are handled in transport, providing a unified
+interface to upper layers.
+
+Host/Port/SourceAddress explanation:
+- SourceAddress (host, port):
+- server (host, port): Listen on host:port
+- server serial (comm_port, _): comm_port is device string
+- client (host, port): Bind host:port to interface
+- client serial: not used
+- Host
+- server: not used
+- client: remote host to connect to (as host:port)
+- client serial: host is comm_port device string
+- Port
+- server: not used
+- client: remote port to connect to (as host:port)
+- client serial: no used
+
+Pyserial allow the comm_port to be a socket e.g. "socket://localhost:502",
+this allows serial clients to connect to a tcp server with RTU framer.
+
+Pymodbus allows this format for both server and client.
+For clients the string is passed to pyserial,
+but for servers it is used to start a modbus tcp server.
+This allows for serial testing, without a serial cable.
+
+Pymodbus offers nullmodem for clients/servers running in the same process
+if <host> is set to NULLMODEM_HOST it will be automatically invoked.
+This allows testing without actual network traffic and is a lot faster.
+
+Class NullModem is a asyncio transport class,
+that replaces the socket class or pyserial.
+
+The class is designed to take care of differences between the different
+transport mediums, and provide a neutral interface for the upper layers.
+It basically provides a pipe, without caring about the actual data content.
+"""
+from __future__ import annotations
+
+import asyncio
+import dataclasses
+import ssl
+from abc import abstractmethod
+from collections.abc import Callable, Coroutine
+from contextlib import suppress
+from enum import Enum
+from functools import partial
+from typing import Any
+
+from pymodbus.logging import Log
+from pymodbus.transport.serialtransport import create_serial_connection
+
+
+NULLMODEM_HOST = "__pymodbus_nullmodem"
+
+
+class CommType(Enum):
+    """Type of transport."""
+
+    TCP = 1
+    TLS = 2
+    UDP = 3
+    SERIAL = 4
+
+
+@dataclasses.dataclass
+class CommParams:
+    """Parameter class."""
+
+    # generic
+    comm_name: str | None = None
+    comm_type: CommType | None = None
+    reconnect_delay: float | None = None
+    reconnect_delay_max: float = 0.0
+    timeout_connect: float = 0.0
+    host: str = "localhost" # On some machines this will now be ::1
+    port: int = 0
+    source_address: tuple[str, int] | None = None
+    handle_local_echo: bool = False
+
+    # tls
+    sslctx: ssl.SSLContext | None = None
+
+    # serial
+    baudrate: int = -1
+    bytesize: int = -1
+    parity: str = ''
+    stopbits: int = -1
+
+    @classmethod
+    def generate_ssl(
+        cls,
+        is_server: bool,
+        certfile: str | None = None,
+        keyfile: str | None = None,
+        password: str | None = None,
+        sslctx: ssl.SSLContext | None = None,
+    ) -> ssl.SSLContext:
+        """Generate sslctx from cert/key/password.
+
+        MODBUS/TCP Security Protocol Specification demands TLSv2 at least
+        """
+        if sslctx:
+            return sslctx
+        new_sslctx = ssl.SSLContext(
+            ssl.PROTOCOL_TLS_SERVER if is_server else ssl.PROTOCOL_TLS_CLIENT
+        )
+        new_sslctx.check_hostname = False
+        new_sslctx.verify_mode = ssl.CERT_NONE
+        new_sslctx.minimum_version = ssl.TLSVersion.TLSv1_2
+        new_sslctx.maximum_version = ssl.TLSVersion.TLSv1_3
+        if certfile:
+            new_sslctx.load_cert_chain(
+                certfile=certfile, keyfile=keyfile, password=password
+            )
+        return new_sslctx
+
+    def copy(self) -> CommParams:
+        """Create a copy."""
+        return dataclasses.replace(self)
+
+
+class ModbusProtocol(asyncio.BaseProtocol):
+    """Protocol layer including transport."""
+
+    def __init__(
+        self,
+        params: CommParams,
+        is_server: bool,
+    ) -> None:
+        """Initialize a transport instance.
+
+        :param params: parameter dataclass
+        :param is_server: true if object act as a server (listen/connect)
+        """
+        self.comm_params = params.copy()
+        self.is_server = is_server
+        self.is_closing = False
+
+        self.transport: asyncio.BaseTransport = None  # type: ignore[assignment]
+        self.loop: asyncio.AbstractEventLoop = asyncio.get_running_loop()
+        self.recv_buffer: bytes = b""
+        self.call_create: Callable[[], Coroutine[Any, Any, Any]] = None  # type: ignore[assignment]
+        self.reconnect_task: asyncio.Task | None = None
+        self.listener: ModbusProtocol | None = None
+        self.active_connections: dict[str, ModbusProtocol] = {}
+        self.unique_id: str = str(id(self))
+        self.reconnect_delay_current = 0.0
+        self.sent_buffer: bytes = b""
+        if self.is_server:
+            if self.comm_params.source_address is not None:
+                host = self.comm_params.source_address[0]
+                port = int(self.comm_params.source_address[1])
+            else:
+                # This behaviour isn't quite right.
+                # It listens on any IPv4 address rather than the more natural default of any address (v6 or v4).
+                host = "0.0.0.0" # Any IPv4 host
+                port = 0 # Server will select an ephemeral port for itself
+        else:
+            host = self.comm_params.host
+            port = int(self.comm_params.port)
+        if self.comm_params.comm_type == CommType.SERIAL and NULLMODEM_HOST in host:
+            host, port = NULLMODEM_HOST, int(host[9:].split(":")[1])
+        if host == NULLMODEM_HOST:
+            self.call_create = partial(self.create_nullmodem, port)
+            return
+        if (
+            self.comm_params.comm_type == CommType.SERIAL
+            and self.is_server
+            and host.startswith("socket")
+        ):
+            # format is "socket://<host>:port"
+            self.comm_params.comm_type = CommType.TCP
+            parts = host.split(":")
+            host, port = parts[1][2:], int(parts[2])
+        self.init_setup_connect_listen(host, port)
+
+    def init_setup_connect_listen(self, host: str, port: int) -> None:
+        """Handle connect/listen handler."""
+        if self.comm_params.comm_type == CommType.SERIAL:
+            self.call_create = partial(create_serial_connection,
+                self.loop,
+                self.handle_new_connection,
+                host,
+                baudrate=self.comm_params.baudrate,
+                bytesize=self.comm_params.bytesize,
+                parity=self.comm_params.parity,
+                stopbits=self.comm_params.stopbits,
+                timeout=self.comm_params.timeout_connect,
+            )
+            return
+        if self.comm_params.comm_type == CommType.UDP:
+            if self.is_server:
+                self.call_create = partial(self.loop.create_datagram_endpoint,
+                    self.handle_new_connection,
+                    local_addr=(host, port),
+                )
+            else:
+                self.call_create = partial(self.loop.create_datagram_endpoint,
+                    self.handle_new_connection,
+                    remote_addr=(host, port),
+                )
+            return
+        # TLS and TCP
+        if self.is_server:
+            self.call_create = partial(self.loop.create_server,
+                self.handle_new_connection,
+                host,
+                port,
+                ssl=self.comm_params.sslctx,
+                reuse_address=True,
+                start_serving=True,
+            )
+        else:
+            self.call_create = partial(self.loop.create_connection,
+                self.handle_new_connection,
+                host,
+                port,
+                local_addr=self.comm_params.source_address,
+                ssl=self.comm_params.sslctx,
+            )
+
+    async def connect(self) -> bool:
+        """Handle generic connect and call on to specific transport connect."""
+        Log.debug("Connecting {}", self.comm_params.comm_name)
+        self.is_closing = False
+        try:
+            self.transport, _protocol = await asyncio.wait_for(
+                self.call_create(),
+                timeout=self.comm_params.timeout_connect,
+            )
+        except (asyncio.TimeoutError, OSError) as exc:  # pylint: disable=overlapping-except
+            Log.warning("Failed to connect {}", exc)
+            return False
+        return bool(self.transport)
+
+    async def listen(self) -> bool:
+        """Handle generic listen and call on to specific transport listen."""
+        Log.debug("Awaiting connections {}", self.comm_params.comm_name)
+        self.is_closing = False
+        try:
+            self.transport = await self.call_create()
+            if isinstance(self.transport, tuple):
+                self.transport = self.transport[0]
+        except OSError as exc:
+            Log.warning("Failed to start server {}", exc)
+            self.__close()
+            return False
+        return True
+
+    # --------------------------------------- #
+    # ModbusProtocol asyncio standard methods #
+    # --------------------------------------- #
+    def connection_made(self, transport: asyncio.BaseTransport) -> None:
+        """Call from asyncio, when a connection is made.
+
+        :param transport: socket etc. representing the connection.
+        """
+        Log.debug("Connected to {}", self.comm_params.comm_name)
+        self.transport = transport
+        self.reset_delay()
+        self.callback_connected()
+
+    def connection_lost(self, reason: Exception | None) -> None:
+        """Call from asyncio, when the connection is lost or closed.
+
+        :param reason: None or an exception object
+        """
+        if not self.transport or self.is_closing:
+            return
+        Log.debug("Connection lost {} due to {}", self.comm_params.comm_name, reason)
+        self.__close()
+        if self.is_server:
+            self.reconnect_task = asyncio.create_task(self.do_relisten())
+            self.reconnect_task.set_name("transport relisten")
+        elif not self.listener and self.comm_params.reconnect_delay:
+            self.reconnect_task = asyncio.create_task(self.do_reconnect())
+            self.reconnect_task.set_name("transport reconnect")
+        self.callback_disconnected(reason)
+
+    def data_received(self, data: bytes) -> None:
+        """Call when some data is received.
+
+        :param data: non-empty bytes object with incoming data.
+        """
+        self.datagram_received(data, None)
+
+    def datagram_received(self, data: bytes, addr: tuple | None) -> None:
+        """Receive datagram (UDP connections)."""
+        if self.comm_params.handle_local_echo and self.sent_buffer:
+            if data.startswith(self.sent_buffer):
+                Log.debug(
+                    "recv skipping (local_echo): {} addr={}",
+                    self.sent_buffer,
+                    ":hex",
+                    addr,
+                )
+                data = data[len(self.sent_buffer) :]
+                self.sent_buffer = b""
+            elif self.sent_buffer.startswith(data):
+                Log.debug(
+                    "recv skipping (partial local_echo): {} addr={}", data, ":hex", addr
+                )
+                self.sent_buffer = self.sent_buffer[len(data) :]
+                return
+            else:
+                Log.debug("did not receive local echo: {} addr={}", data, ":hex", addr)
+                self.sent_buffer = b""
+            if not data:
+                return
+        Log.debug(
+            "recv: {} old_data: {} addr={}",
+            data,
+            ":hex",
+            self.recv_buffer,
+            ":hex",
+            addr,
+        )
+        self.recv_buffer += data
+        cut = self.callback_data(self.recv_buffer, addr=addr)
+        self.recv_buffer = self.recv_buffer[cut:]
+        if self.recv_buffer:
+            Log.debug(
+                "recv, unused data waiting for next packet: {}",
+                self.recv_buffer,
+                ":hex",
+            )
+
+    def eof_received(self) -> None:
+        """Accept other end terminates connection."""
+        Log.debug("-> transport: received eof")
+
+    def error_received(self, exc):
+        """Get error detected in UDP."""
+        Log.debug("-> error_received {}", exc)
+
+    # --------- #
+    # callbacks #
+    # --------- #
+    @abstractmethod
+    def callback_new_connection(self) -> ModbusProtocol:
+        """Call when listener receive new connection request."""
+
+    @abstractmethod
+    def callback_connected(self) -> None:
+        """Call when connection is succcesfull."""
+
+    @abstractmethod
+    def callback_disconnected(self, exc: Exception | None) -> None:
+        """Call when connection is lost."""
+
+    @abstractmethod
+    def callback_data(self, data: bytes, addr: tuple | None = None) -> int:
+        """Handle received data."""
+
+    # ----------------------------------- #
+    # Helper methods for external classes #
+    # ----------------------------------- #
+    def send(self, data: bytes, addr: tuple | None = None) -> None:
+        """Send modbus message.
+
+        :param data: non-empty bytes object with data to send.
+        :param addr: optional addr, only used for UDP server.
+        """
+        if not self.transport:
+            Log.error("Cancel send, because not connected!")
+            return
+        Log.debug("send: {}", data, ":hex")
+        self.recv_buffer = b""
+        if self.comm_params.handle_local_echo:
+            self.sent_buffer += data
+        if self.comm_params.comm_type == CommType.UDP:
+            if addr:
+                self.transport.sendto(data, addr=addr)  # type: ignore[attr-defined]
+            else:
+                self.transport.sendto(data)  # type: ignore[attr-defined]
+        else:
+            self.transport.write(data)  # type: ignore[attr-defined]
+
+    def __close(self, reconnect: bool = False) -> None:
+        """Close connection (internal).
+
+        :param reconnect: (default false), try to reconnect
+        """
+        if self.transport:
+            self.transport.close()
+            self.transport = None  # type: ignore[assignment]
+        self.recv_buffer = b""
+        if self.is_server:
+            for _key, value in self.active_connections.items():
+                value.listener = None
+                value.callback_disconnected(None)
+                value.close()
+            self.active_connections = {}
+            return
+        if not reconnect and self.reconnect_task:
+            self.reconnect_task.cancel()
+            self.reconnect_task = None
+            self.reconnect_delay_current = 0.0
+        if self.listener:
+            self.listener.active_connections.pop(self.unique_id)
+
+    def close(self, reconnect: bool = False) -> None:
+        """Close connection (external).
+
+        :param reconnect: (default false), try to reconnect
+        """
+        if self.is_closing:
+            return
+        self.is_closing = True
+        self.__close(reconnect=reconnect)
+
+    def reset_delay(self) -> None:
+        """Reset wait time before next reconnect to minimal period."""
+        self.reconnect_delay_current = self.comm_params.reconnect_delay or 0.0
+
+    def is_active(self) -> bool:
+        """Return true if connected/listening."""
+        return bool(self.transport)
+
+    # ---------------- #
+    # Internal methods #
+    # ---------------- #
+    async def create_nullmodem(
+        self, port
+    ) -> tuple[asyncio.Transport, asyncio.BaseProtocol]:
+        """Bypass create_ and use null modem."""
+        if self.is_server:
+            # Listener object
+            self.transport = NullModem.set_listener(port, self)
+            return self.transport, self
+
+        # connect object
+        return NullModem.set_connection(port, self)
+
+    def handle_new_connection(self) -> ModbusProtocol:
+        """Handle incoming connect."""
+        if not self.is_server:
+            # Clients reuse the same object.
+            return self
+
+        new_protocol = self.callback_new_connection()
+        self.active_connections[new_protocol.unique_id] = new_protocol
+        new_protocol.listener = self
+        return new_protocol
+
+    async def do_relisten(self) -> None:
+        """Handle reconnect as a task."""
+        try:
+            Log.debug("Wait 1s before reopening listener.")
+            await asyncio.sleep(1)
+            await self.listen()
+        except asyncio.CancelledError:
+            pass
+        self.reconnect_task = None
+
+    async def do_reconnect(self) -> None:
+        """Handle reconnect as a task."""
+        try:
+            self.reconnect_delay_current = self.comm_params.reconnect_delay or 0.0
+            while True:
+                Log.debug(
+                    "Wait {} {} ms before reconnecting.",
+                    self.comm_params.comm_name,
+                    self.reconnect_delay_current * 1000,
+                )
+                await asyncio.sleep(self.reconnect_delay_current)
+                if await self.connect():
+                    break
+                self.reconnect_delay_current = min(
+                    2 * self.reconnect_delay_current,
+                    self.comm_params.reconnect_delay_max,
+                )
+        except asyncio.CancelledError:
+            pass
+        self.reconnect_task = None
+
+    # ----------------- #
+    # The magic methods #
+    # ----------------- #
+    async def __aenter__(self) -> ModbusProtocol:
+        """Implement the client with async enter block."""
+        return self
+
+    async def __aexit__(self, _class, _value, _traceback) -> None:
+        """Implement the client with async exit block."""
+        self.close()
+
+    def __str__(self) -> str:
+        """Build a string representation of the connection."""
+        return f"{self.__class__.__name__}({self.comm_params.comm_name})"
+
+
+class NullModem(asyncio.DatagramTransport, asyncio.Transport):
+    """ModbusProtocol layer.
+
+    Contains methods to act as a null modem between 2 objects.
+    (Allowing tests to be shortcut without actual network calls)
+    """
+
+    listeners: dict[int, ModbusProtocol] = {}
+    connections: dict[NullModem, int] = {}
+
+    def __init__(self, protocol: ModbusProtocol, listen: int | None = None) -> None:
+        """Create half part of null modem."""
+        asyncio.DatagramTransport.__init__(self)
+        asyncio.Transport.__init__(self)
+        self.protocol: ModbusProtocol = protocol
+        self.other_modem: NullModem = None  # type: ignore[assignment]
+        self.listen = listen
+        self.manipulator: Callable[[bytes], list[bytes]] | None = None
+        self._is_closing = False
+
+    # -------------------------- #
+    # external nullmodem methods #
+    # -------------------------- #
+    @classmethod
+    def set_listener(cls, port: int, parent: ModbusProtocol) -> NullModem:
+        """Register listener."""
+        if port in cls.listeners:
+            raise AssertionError(f"Port {port} already listening !")
+        cls.listeners[port] = parent
+        return NullModem(parent, listen=port)
+
+    @classmethod
+    def set_connection(
+        cls, port: int, parent: ModbusProtocol
+    ) -> tuple[NullModem, ModbusProtocol]:
+        """Connect to listener."""
+        if port not in cls.listeners:
+            raise asyncio.TimeoutError(f"Port {port} not being listened on !")
+
+        client_protocol = parent.handle_new_connection()
+        server_protocol = NullModem.listeners[port].handle_new_connection()
+        client_transport = NullModem(client_protocol)
+        server_transport = NullModem(server_protocol)
+        cls.connections[client_transport] = port
+        cls.connections[server_transport] = -port
+        client_transport.other_modem = server_transport
+        server_transport.other_modem = client_transport
+        client_protocol.connection_made(client_transport)
+        server_protocol.connection_made(server_transport)
+        return client_transport, client_protocol
+
+    def set_manipulator(self, function: Callable[[bytes], list[bytes]]) -> None:
+        """Register a manipulator."""
+        self.manipulator = function
+
+    @classmethod
+    def is_dirty(cls):
+        """Check if everything is closed."""
+        dirty = False
+        if cls.connections:
+            Log.error(
+                "NullModem_FATAL missing close on port {} connect()",
+                [str(key) for key in cls.connections.values()],
+            )
+            dirty = True
+        if cls.listeners:
+            Log.error(
+                "NullModem_FATAL missing close on port {} listen()",
+                [str(value) for value in cls.listeners],
+            )
+            dirty = True
+        return dirty
+
+    # ---------------- #
+    # external methods #
+    # ---------------- #
+
+    def close(self) -> None:
+        """Close null modem."""
+        if self._is_closing:
+            return
+        self._is_closing = True
+        if self.listen:
+            del self.listeners[self.listen]
+            return
+        if self.connections:
+            with suppress(KeyError):
+                del self.connections[self]
+        if self.other_modem:
+            self.other_modem.other_modem = None  # type: ignore[assignment]
+            self.other_modem.close()
+            self.other_modem = None  # type: ignore[assignment]
+        if self.protocol:
+            self.protocol.connection_lost(None)
+
+    def sendto(self, data: bytes, _addr: Any = None) -> None:
+        """Send datagrame."""
+        self.write(data)
+
+    def write(self, data: bytes) -> None:
+        """Send data."""
+        if not self.manipulator:
+            if self.other_modem:
+                self.other_modem.protocol.data_received(data)
+            return
+        data_manipulated = self.manipulator(data)
+        for part in data_manipulated:
+            self.other_modem.protocol.data_received(part)
+
+    # ------------- #
+    # Dummy methods #
+    # ------------- #
+    def abort(self) -> None:
+        """Alias for closing the connection."""
+        self.close()
+
+    def can_write_eof(self) -> bool:
+        """Allow to write eof."""
+        return False
+
+    def get_write_buffer_size(self) -> int:
+        """Set write limit."""
+        return 1024
+
+    def get_write_buffer_limits(self) -> tuple[int, int]:
+        """Set flush limits."""
+        return (1, 1024)
+
+    def set_write_buffer_limits(
+        self, high: int | None = None, low: int | None = None
+    ) -> None:
+        """Set flush limits."""
+
+    def write_eof(self) -> None:
+        """Write eof."""
+
+    def get_protocol(self) -> ModbusProtocol | asyncio.BaseProtocol:
+        """Return current protocol."""
+        return self.protocol
+
+    def set_protocol(self, protocol: asyncio.BaseProtocol) -> None:
+        """Set current protocol."""
+
+    def is_closing(self) -> bool:
+        """Return true if closing."""
+        return self._is_closing
+
+    def is_reading(self) -> bool:
+        """Return true if read is active."""
+        return True
+
+    def pause_reading(self):
+        """Pause receiver."""
+
+    def resume_reading(self):
+        """Resume receiver."""
diff --git a/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/utilities.py b/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/utilities.py
new file mode 100644
index 00000000..1bbbc8f3
--- /dev/null
+++ b/custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.7.4/pymodbus/utilities.py
@@ -0,0 +1,160 @@
+"""Modbus Utilities.
+
+A collection of utilities for packing data, unpacking
+data computing checksums, and decode checksums.
+"""
+from __future__ import annotations
+
+
+__all__ = [
+    "pack_bitstring",
+    "unpack_bitstring",
+    "default",
+]
+
+# pylint: disable=missing-type-doc
+import struct
+
+
+class ModbusTransactionState:  # pylint: disable=too-few-public-methods
+    """Modbus Client States."""
+
+    IDLE = 0
+    SENDING = 1
+    WAITING_FOR_REPLY = 2
+    WAITING_TURNAROUND_DELAY = 3
+    PROCESSING_REPLY = 4
+    PROCESSING_ERROR = 5
+    TRANSACTION_COMPLETE = 6
+    RETRYING = 7
+    NO_RESPONSE_STATE = 8
+
+    @classmethod
+    def to_string(cls, state):
+        """Convert to string."""
+        states = {
+            ModbusTransactionState.IDLE: "IDLE",
+            ModbusTransactionState.SENDING: "SENDING",
+            ModbusTransactionState.WAITING_FOR_REPLY: "WAITING_FOR_REPLY",
+            ModbusTransactionState.WAITING_TURNAROUND_DELAY: "WAITING_TURNAROUND_DELAY",
+            ModbusTransactionState.PROCESSING_REPLY: "PROCESSING_REPLY",
+            ModbusTransactionState.PROCESSING_ERROR: "PROCESSING_ERROR",
+            ModbusTransactionState.TRANSACTION_COMPLETE: "TRANSACTION_COMPLETE",
+            ModbusTransactionState.RETRYING: "RETRYING TRANSACTION",
+        }
+        return states.get(state, None)
+
+
+# --------------------------------------------------------------------------- #
+# Helpers
+# --------------------------------------------------------------------------- #
+
+
+def default(value):
+    """Return the default value of object.
+
+    :param value: The value to get the default of
+    :returns: The default value
+    """
+    return type(value)()
+
+
+def dict_property(store, index):
+    """Create class properties from a dictionary.
+
+    Basically this allows you to remove a lot of possible
+    boilerplate code.
+
+    :param store: The store store to pull from
+    :param index: The index into the store to close over
+    :returns: An initialized property set
+    """
+    if hasattr(store, "__call__"):
+        getter = lambda self: store(  # pylint: disable=unnecessary-lambda-assignment
+            self
+        )[index]
+        setter = lambda self, value: store(  # pylint: disable=unnecessary-lambda-assignment
+            self
+        ).__setitem__(index, value)
+    elif isinstance(store, str):
+        getter = lambda self: self.__getattribute__(  # pylint: disable=unnecessary-dunder-call,unnecessary-lambda-assignment
+            store
+        )[index]
+        setter = lambda self, value: self.__getattribute__(  # pylint: disable=unnecessary-dunder-call,unnecessary-lambda-assignment
+            store
+        ).__setitem__(index, value)
+    else:
+        getter = (
+            lambda self: store[index]  # pylint: disable=unnecessary-lambda-assignment
+        )
+        setter = lambda self, value: store.__setitem__(  # pylint: disable=unnecessary-lambda-assignment
+            index, value
+        )
+
+    return property(getter, setter)
+
+
+# --------------------------------------------------------------------------- #
+# Bit packing functions
+# --------------------------------------------------------------------------- #
+def pack_bitstring(bits: list[bool]) -> bytes:
+    """Create a bytestring out of a list of bits.
+
+    :param bits: A list of bits
+
+    example::
+
+        bits   = [False, True, False, True]
+        result = pack_bitstring(bits)
+    """
+    ret = b""
+    i = packed = 0
+    for bit in bits:
+        if bit:
+            packed += 128
+        i += 1
+        if i == 8:
+            ret += struct.pack(">B", packed)
+            i = packed = 0
+        else:
+            packed >>= 1
+    if 0 < i < 8:
+        packed >>= 7 - i
+        ret += struct.pack(">B", packed)
+    return ret
+
+
+def unpack_bitstring(data: bytes) -> list[bool]:
+    """Create bit list out of a bytestring.
+
+    :param data: The modbus data packet to decode
+
+    example::
+
+        bytes  = "bytes to decode"
+        result = unpack_bitstring(bytes)
+    """
+    byte_count = len(data)
+    bits = []
+    for byte in range(byte_count):
+        value = int(data[byte])
+        for _ in range(8):
+            bits.append((value & 1) == 1)
+            value >>= 1
+    return bits
+
+
+# --------------------------------------------------------------------------- #
+# Error Detection Functions
+# --------------------------------------------------------------------------- #
+
+
+def hexlify_packets(packet):
+    """Return hex representation of bytestring received.
+
+    :param packet:
+    :return:
+    """
+    if not packet:
+        return ""
+    return " ".join([hex(int(x)) for x in packet])
diff --git a/pyproject.toml b/pyproject.toml
index 604a87cb..aba73930 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -9,6 +9,9 @@ warn_unused_configs = true
 warn_unused_ignores = true
 namespace_packages = true
 explicit_package_bases = true
+exclude = [
+    'custom_components/foxess_modbus/vendor',
+]
 
 [[tool.mypy.overrides]]
 module = 'pymodbus.*'
@@ -30,6 +33,9 @@ unfixable = [
     "F841", # unused-variable
 ]
 line-length = 120
+extend-exclude = [
+    'custom_components/foxess_modbus/vendor',
+]
 
 [tool.ruff.isort]
 force-single-line = true
diff --git a/requirements.txt b/requirements.txt
index 6c9ffa51..0af8e484 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -9,7 +9,6 @@ pytest-asyncio
 
 colorlog==6.7.0
 pre-commit==3.3.3
-black==23.9.0
 ruff==0.0.275
 # These are duplicated in .pre-commit-config.yaml
 reorder-python-imports==3.10.0
@@ -19,6 +18,4 @@ mypy==1.5.1
 types-python-slugify==8.0.0.2
 voluptuous-stubs==0.1.1
 # For mypy. Keep in sync with manifest.json and https://github.com/home-assistant/core/blob/master/requirements_all.txt.
-# If changed, make sure subclasses in modbus_client are still valid!
-pymodbus==3.7.4
 pyserial==3.5