Skip to content

Commit 405939e

Browse files
committed
Vendor pymodbus, rather than relying on the version which HA installs
This should protect us from HA breaking things in the future! Fixes: #748
1 parent e9d0178 commit 405939e

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

94 files changed

+15188
-35
lines changed

Diff for: .pre-commit-config.yaml

+1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ repos:
55
- id: check-added-large-files
66
- id: check-yaml
77
- id: end-of-file-fixer
8+
exclude: ^custom_components\/foxess_modbus\/vendor
89
- id: trailing-whitespace
910
- repo: local
1011
hooks:

Diff for: .prettierignore

+1
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
tests/__snapshots__
2+
custom_components/foxess_modbus/vendor

Diff for: .vscode/settings.json

+4-1
Original file line numberDiff line numberDiff line change
@@ -27,5 +27,8 @@
2727
"source.fixAll": "explicit"
2828
},
2929
"editor.defaultFormatter": "ms-python.black-formatter"
30-
}
30+
},
31+
"python.analysis.extraPaths": [
32+
"./custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.6.9"
33+
]
3134
}

Diff for: custom_components/foxess_modbus/client/custom_modbus_tcp_client.py

+23-8
Original file line numberDiff line numberDiff line change
@@ -5,23 +5,23 @@
55
from typing import Any
66
from typing import cast
77

8-
from pymodbus.client import ModbusTcpClient
9-
from pymodbus.exceptions import ConnectionException
8+
from ..vendor.pymodbus import ConnectionException
9+
from ..vendor.pymodbus import ModbusTcpClient
1010

1111
_LOGGER = logging.getLogger(__name__)
1212

1313

1414
class CustomModbusTcpClient(ModbusTcpClient):
1515
"""Custom ModbusTcpClient subclass with some hacks"""
1616

17-
def __init__(self, delay_on_connect: int | None = None, **kwargs: Any) -> None:
17+
def __init__(self, delay_on_connect: int | None, **kwargs: Any) -> None:
1818
super().__init__(**kwargs)
1919
self._delay_on_connect = delay_on_connect
2020

2121
def connect(self) -> bool:
2222
was_connected = self.socket is not None
2323
if not was_connected:
24-
_LOGGER.debug("Connecting to %s", self.comm_params)
24+
_LOGGER.debug("Connecting to %s", self.params)
2525
is_connected = cast(bool, super().connect())
2626
# pymodbus doesn't disable Nagle's algorithm. This slows down reads quite substantially as the
2727
# TCP stack waits to see if we're going to send anything else. Disable it ourselves.
@@ -34,7 +34,7 @@ def connect(self) -> bool:
3434

3535
# Replacement of ModbusTcpClient to use poll rather than select, see
3636
# https://github.com/nathanmarlor/foxess_modbus/issues/275
37-
def recv(self, size: int | None) -> bytes:
37+
def recv(self, size: int) -> bytes:
3838
"""Read data from the underlying descriptor."""
3939
super(ModbusTcpClient, self).recv(size)
4040
if not self.socket:
@@ -48,9 +48,9 @@ def recv(self, size: int | None) -> bytes:
4848
# is received or timeout is expired.
4949
# If timeout expires returns the read data, also if its length is
5050
# less than the expected size.
51-
self.socket.setblocking(False)
51+
self.socket.setblocking(0)
5252

53-
timeout = self.comm_params.timeout_connect or 0
53+
timeout = self.comm_params.timeout_connect
5454

5555
# If size isn't specified read up to 4096 bytes at a time.
5656
if size is None:
@@ -90,5 +90,20 @@ def recv(self, size: int | None) -> bytes:
9090
if time_ > end:
9191
break
9292

93-
self.last_frame_end = round(time.time(), 6)
9493
return b"".join(data)
94+
95+
# Replacement of ModbusTcpClient to use poll rather than select, see
96+
# https://github.com/nathanmarlor/foxess_modbus/issues/275
97+
def _check_read_buffer(self) -> bytes | None:
98+
"""Check read buffer."""
99+
time_ = time.time()
100+
end = time_ + self.params.timeout
101+
data = None
102+
103+
assert self.socket is not None
104+
poll = select.poll()
105+
poll.register(self.socket, select.POLLIN)
106+
poll_res = poll.poll(end - time_)
107+
if len(poll_res) > 0:
108+
data = self.socket.recv(1024)
109+
return data

Diff for: custom_components/foxess_modbus/client/modbus_client.py

+17-18
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,6 @@
1111

1212
import serial
1313
from homeassistant.core import HomeAssistant
14-
from pymodbus.client import ModbusSerialClient
15-
from pymodbus.client import ModbusUdpClient
16-
from pymodbus.framer import FramerType
17-
from pymodbus.pdu import ModbusPDU
18-
from pymodbus.pdu.register_read_message import ReadHoldingRegistersResponse
19-
from pymodbus.pdu.register_read_message import ReadInputRegistersResponse
20-
from pymodbus.pdu.register_write_message import WriteMultipleRegistersResponse
21-
from pymodbus.pdu.register_write_message import WriteSingleRegisterResponse
2214

2315
from .. import client
2416
from ..common.types import ConnectionType
@@ -28,6 +20,15 @@
2820
from ..const import TCP
2921
from ..const import UDP
3022
from ..inverter_adapters import InverterAdapter
23+
from ..vendor.pymodbus import ModbusResponse
24+
from ..vendor.pymodbus import ModbusRtuFramer
25+
from ..vendor.pymodbus import ModbusSerialClient
26+
from ..vendor.pymodbus import ModbusSocketFramer
27+
from ..vendor.pymodbus import ModbusUdpClient
28+
from ..vendor.pymodbus import ReadHoldingRegistersResponse
29+
from ..vendor.pymodbus import ReadInputRegistersResponse
30+
from ..vendor.pymodbus import WriteMultipleRegistersResponse
31+
from ..vendor.pymodbus import WriteSingleRegisterResponse
3132
from .custom_modbus_tcp_client import CustomModbusTcpClient
3233

3334
_LOGGER = logging.getLogger(__name__)
@@ -38,19 +39,19 @@
3839
_CLIENTS: dict[str, dict[str, Any]] = {
3940
SERIAL: {
4041
"client": ModbusSerialClient,
41-
"framer": FramerType.RTU,
42+
"framer": ModbusRtuFramer,
4243
},
4344
TCP: {
4445
"client": CustomModbusTcpClient,
45-
"framer": FramerType.SOCKET,
46+
"framer": ModbusSocketFramer,
4647
},
4748
UDP: {
4849
"client": ModbusUdpClient,
49-
"framer": FramerType.SOCKET,
50+
"framer": ModbusSocketFramer,
5051
},
5152
RTU_OVER_TCP: {
5253
"client": CustomModbusTcpClient,
53-
"framer": FramerType.RTU,
54+
"framer": ModbusRtuFramer,
5455
},
5556
}
5657

@@ -69,16 +70,14 @@ def __init__(self, hass: HomeAssistant, protocol: str, adapter: InverterAdapter,
6970

7071
client = _CLIENTS[protocol]
7172

73+
# Delaying for a second after establishing a connection seems to help the inverter stability,
74+
# see https://github.com/nathanmarlor/foxess_modbus/discussions/132
7275
config = {
7376
**config,
7477
"framer": client["framer"],
78+
"delay_on_connect": 1 if adapter.connection_type == ConnectionType.LAN else None,
7579
}
7680

77-
# Delaying for a second after establishing a connection seems to help the inverter stability,
78-
# see https://github.com/nathanmarlor/foxess_modbus/discussions/132
79-
if adapter.connection_type == ConnectionType.LAN:
80-
config["delay_on_connect"] = 1
81-
8281
# If our custom PosixPollSerial hack is supported, use that. This uses poll rather than select, which means we
8382
# don't break when there are more than 1024 fds. See #457.
8483
# Only supported on posix, see https://github.com/pyserial/pyserial/blob/7aeea35429d15f3eefed10bbb659674638903e3a/serial/__init__.py#L31
@@ -225,7 +224,7 @@ def __str__(self) -> str:
225224
class ModbusClientFailedError(Exception):
226225
"""Raised when the ModbusClient fails to read/write"""
227226

228-
def __init__(self, message: str, client: ModbusClient, response: ModbusPDU | Exception) -> None:
227+
def __init__(self, message: str, client: ModbusClient, response: ModbusResponse | Exception) -> None:
229228
super().__init__(f"{message} from {client}: {response}")
230229
self.message = message
231230
self.client = client

Diff for: custom_components/foxess_modbus/flow/adapter_flow_segment.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,6 @@
77
from homeassistant.data_entry_flow import FlowResult
88
from homeassistant.helpers import config_validation as cv
99
from homeassistant.helpers.selector import selector
10-
from pymodbus.exceptions import ConnectionException
11-
from pymodbus.exceptions import ModbusIOException
1210

1311
from ..client.modbus_client import ModbusClient
1412
from ..client.modbus_client import ModbusClientFailedError
@@ -23,6 +21,8 @@
2321
from ..inverter_adapters import InverterAdapter
2422
from ..inverter_adapters import InverterAdapterType
2523
from ..modbus_controller import ModbusController
24+
from ..vendor.pymodbus import ConnectionException
25+
from ..vendor.pymodbus import ModbusIOException
2626
from .flow_handler_mixin import FlowHandlerMixin
2727
from .flow_handler_mixin import ValidationFailedError
2828
from .inverter_data import InverterData

Diff for: custom_components/foxess_modbus/manifest.json

-1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,5 @@
88
"integration_type": "service",
99
"iot_class": "local_push",
1010
"issue_tracker": "https://github.com/nathanmarlor/foxess_modbus/issues",
11-
"requirements": ["pymodbus>=3.7.4"],
1211
"version": "1.0.0"
1312
}

Diff for: custom_components/foxess_modbus/modbus_controller.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818
from homeassistant.helpers import issue_registry
1919
from homeassistant.helpers.event import async_track_time_interval
2020
from homeassistant.helpers.issue_registry import IssueSeverity
21-
from pymodbus.exceptions import ConnectionException
2221

2322
from .client.modbus_client import ModbusClient
2423
from .client.modbus_client import ModbusClientFailedError
@@ -38,6 +37,7 @@
3837
from .inverter_profiles import INVERTER_PROFILES
3938
from .inverter_profiles import InverterModelConnectionTypeProfile
4039
from .remote_control_manager import RemoteControlManager
40+
from .vendor.pymodbus import ConnectionException
4141

4242
_LOGGER = logging.getLogger(__name__)
4343

Diff for: custom_components/foxess_modbus/services/update_charge_period_service.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,13 @@
1010
from homeassistant.core import ServiceCall
1111
from homeassistant.exceptions import HomeAssistantError
1212
from homeassistant.helpers import config_validation as cv
13-
from pymodbus.exceptions import ModbusIOException
1413

1514
from ..const import DOMAIN
1615
from ..entities.modbus_charge_period_sensors import is_time_value_valid
1716
from ..entities.modbus_charge_period_sensors import parse_time_value
1817
from ..entities.modbus_charge_period_sensors import serialize_time_to_value
1918
from ..modbus_controller import ModbusController
19+
from ..vendor.pymodbus import ModbusIOException
2020
from .utils import get_controller_from_friendly_name_or_device_id
2121

2222
_LOGGER: logging.Logger = logging.getLogger(__package__)

Diff for: custom_components/foxess_modbus/services/write_registers_service.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,10 @@
88
from homeassistant.core import ServiceCall
99
from homeassistant.exceptions import HomeAssistantError
1010
from homeassistant.helpers import config_validation as cv
11-
from pymodbus.exceptions import ModbusIOException
1211

1312
from ..const import DOMAIN
1413
from ..modbus_controller import ModbusController
14+
from ..vendor.pymodbus import ModbusIOException
1515
from .utils import get_controller_from_friendly_name_or_device_id
1616

1717
_LOGGER: logging.Logger = logging.getLogger(__package__)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import sys
2+
from pathlib import Path
3+
4+
# Update python.analysis.extraPaths in .vscode/settings.json if you change this.
5+
# If changed, make sure subclasses in modbus_client are still valid!
6+
sys.path.insert(0, str((Path(__file__).parent / "pymodbus-3.6.9").absolute()))
7+
8+
from pymodbus.client import ModbusSerialClient
9+
from pymodbus.client import ModbusTcpClient
10+
from pymodbus.client import ModbusUdpClient
11+
from pymodbus.exceptions import ConnectionException
12+
from pymodbus.exceptions import ModbusIOException
13+
from pymodbus.register_read_message import ReadHoldingRegistersResponse
14+
from pymodbus.register_read_message import ReadInputRegistersResponse
15+
from pymodbus.register_write_message import WriteMultipleRegistersResponse
16+
from pymodbus.register_write_message import WriteSingleRegisterResponse
17+
from pymodbus.pdu import ModbusResponse
18+
from pymodbus.transaction import ModbusRtuFramer
19+
from pymodbus.transaction import ModbusSocketFramer
20+
21+
sys.path.pop(0)
22+
23+
__all__ = [
24+
"ModbusSerialClient",
25+
"ModbusTcpClient",
26+
"ModbusUdpClient",
27+
"ConnectionException",
28+
"ModbusIOException",
29+
"ModbusPDU",
30+
"ReadHoldingRegistersResponse",
31+
"ReadInputRegistersResponse",
32+
"WriteMultipleRegistersResponse",
33+
"WriteSingleRegisterResponse",
34+
"ModbusResponse",
35+
"ModbusRtuFramer",
36+
"ModbusSocketFramer",
37+
]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
#!/usr/local/bin/python3.13
2+
# -*- coding: utf-8 -*-
3+
import re
4+
import sys
5+
from pymodbus.server.simulator.main import main
6+
if __name__ == '__main__':
7+
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
8+
sys.exit(main())

0 commit comments

Comments
 (0)