Skip to content

Commit 893c65a

Browse files
authored
Merge pull request #749 from canton7/feature/vendor-pymodbus
Vendor pymodbus, rather than relying on the version which HA installs
2 parents e8f5e6a + 9a20e9f commit 893c65a

File tree

102 files changed

+15211
-78
lines changed

Some content is hidden

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

102 files changed

+15211
-78
lines changed

.devcontainer/devcontainer.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
"extensions": [
1212
"ms-python.python",
1313
"charliermarsh.ruff",
14-
"ms-python.black-formatter",
1514
"ms-python.vscode-pylance",
1615
"github.vscode-pull-request-github",
1716
"ryanluker.vscode-coverage-gutters",

.pre-commit-config.yaml

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,22 +5,15 @@ 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
9-
- repo: local
10-
hooks:
11-
- id: black
12-
name: black
13-
entry: black
14-
language: system
15-
types: [python]
16-
require_serial: true
1710
- repo: https://github.com/pre-commit/mirrors-prettier
1811
rev: v2.2.1
1912
hooks:
2013
- id: prettier
2114
- repo: https://github.com/astral-sh/ruff-pre-commit
2215
# Ruff version.
23-
rev: v0.0.275
16+
rev: v0.9.4
2417
hooks:
2518
- id: ruff
2619
# Temporarily disabled due to crash, probably caused by us having to use HA stubs which are out of date?

.prettierignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
tests/__snapshots__
2+
custom_components/foxess_modbus/vendor

.vscode/settings.json

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@
2626
"editor.codeActionsOnSave": {
2727
"source.fixAll": "explicit"
2828
},
29-
"editor.defaultFormatter": "ms-python.black-formatter"
30-
}
29+
"editor.defaultFormatter": "charliermarsh.ruff"
30+
},
31+
"python.analysis.extraPaths": [
32+
"./custom_components/foxess_modbus/vendor/pymodbus/pymodbus-3.6.9"
33+
]
3134
}

CONTRIBUTING.md

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,8 @@ Pull requests are the best way to propose changes to the codebase.
1515

1616
1. Fork the repo and create your branch from `master`.
1717
2. If you've changed something, update the documentation.
18-
3. Make sure your code lints (using black).
19-
4. Test you contribution.
20-
5. Issue that pull request!
18+
3. Test you contribution.
19+
4. Issue that pull request!
2120

2221
## Any contributions you make will be under the MIT Software License
2322

@@ -44,11 +43,7 @@ People _love_ thorough bug reports. I'm not even kidding.
4443

4544
## Use a Consistent Coding Style
4645

47-
Use [black](https://github.com/ambv/black) and [prettier](https://prettier.io/)
48-
to make sure the code follows the style.
49-
50-
Or use the `pre-commit` settings implemented in this repository
51-
(see deicated section below).
46+
Use the `pre-commit` settings implemented in this repository (see dedicated section below).
5247

5348
## Test your code modification
5449

custom_components/foxess_modbus/client/custom_modbus_tcp_client.py

Lines changed: 23 additions & 8 deletions
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

custom_components/foxess_modbus/client/modbus_client.py

Lines changed: 17 additions & 18 deletions
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

custom_components/foxess_modbus/client/protocol_pollserial.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,8 +68,8 @@ def read(self, size: int = 1) -> bytes:
6868
result == _PollResult.TIMEOUT
6969
or result == _PollResult.ABORT
7070
or timeout.expired()
71-
or (self._inter_byte_timeout is not None and self._inter_byte_timeout > 0)
72-
and not buf
71+
or ((self._inter_byte_timeout is not None and self._inter_byte_timeout > 0)
72+
and not buf)
7373
):
7474
break # early abort on timeout
7575
return bytes(read)

custom_components/foxess_modbus/common/types.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
"""Defines RegisterType"""
1+
"""Defines RegisterType""" # noqa: A005
22

33
from enum import Enum
44
from enum import Flag

custom_components/foxess_modbus/entities/base_validator.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,5 @@ class BaseValidator(ABC):
77
"""Base validator"""
88

99
@abstractmethod
10-
def validate(self, data: int | float) -> bool:
10+
def validate(self, data: float) -> bool:
1111
"""Validate a value against a set of rules"""

custom_components/foxess_modbus/entities/modbus_entity_mixin.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -153,8 +153,8 @@ def _get_entity_id(self, platform: Platform) -> str:
153153
def _validate(
154154
self,
155155
rules: list[BaseValidator],
156-
processed: float | int,
157-
original: float | int | None = None,
156+
processed: float,
157+
original: float | None = None,
158158
address_override: int | None = None,
159159
) -> bool:
160160
"""Validate against a set of rules"""

custom_components/foxess_modbus/entities/validation.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ def __init__(self, min_value: float, max_value: float) -> None:
1111
self._min = min_value
1212
self._max = max_value
1313

14-
def validate(self, data: int | float) -> bool:
14+
def validate(self, data: float) -> bool:
1515
"""Validate a value against a set of rules"""
1616

1717
return self._min <= data <= self._max
@@ -24,7 +24,7 @@ def __init__(self, min_value: float) -> None:
2424
"""Init"""
2525
self._min = min_value
2626

27-
def validate(self, data: int | float) -> bool:
27+
def validate(self, data: float) -> bool:
2828
"""Validate a value against a set of rules"""
2929

3030
return data >= self._min
@@ -37,7 +37,7 @@ def __init__(self, max_value: float) -> None:
3737
"""Init"""
3838
self._max = max_value
3939

40-
def validate(self, data: int | float) -> bool:
40+
def validate(self, data: float) -> bool:
4141
"""Validate a value against a set of rules"""
4242

4343
return data <= self._max
@@ -46,6 +46,6 @@ def validate(self, data: int | float) -> bool:
4646
class Time(BaseValidator):
4747
"""Time validator"""
4848

49-
def validate(self, data: int | float) -> bool:
49+
def validate(self, data: float) -> bool:
5050
"""Validate a value against a set of rules"""
5151
return isinstance(data, int) and is_time_value_valid(data)

custom_components/foxess_modbus/flow/adapter_flow_segment.py

Lines changed: 2 additions & 2 deletions
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

custom_components/foxess_modbus/manifest.json

Lines changed: 0 additions & 1 deletion
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
}

custom_components/foxess_modbus/modbus_controller.py

Lines changed: 3 additions & 3 deletions
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

@@ -234,7 +234,7 @@ async def write_registers(self, start_address: int, values: list[int]) -> None:
234234
self._notify_update(changed_addresses)
235235
except Exception as ex:
236236
# Failed writes are always bad
237-
_LOGGER.error("Failed to write registers", exc_info=True)
237+
_LOGGER.exception("Failed to write registers")
238238
raise ex
239239

240240
async def _refresh(self, _time: datetime) -> None:
@@ -538,7 +538,7 @@ async def autodetect(client: ModbusClient, slave: int, adapter_config: dict[str,
538538
_LOGGER.error("Did not recognise inverter model '%s' (%s)", full_model, register_values)
539539
raise UnsupportedInverterError(full_model)
540540
except Exception as ex:
541-
_LOGGER.error("Autodetect: failed to connect to (%s)", client, exc_info=True)
541+
_LOGGER.exceptino("Autodetect: failed to connect to (%s)", client)
542542
raise AutoconnectFailedError(spy_handler.records) from ex
543543
finally:
544544
pymodbus_logger.removeHandler(spy_handler)

custom_components/foxess_modbus/select.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
"""Sensor platform for foxess_modbus."""
1+
"""Sensor platform for foxess_modbus.""" # noqa: A005
22

33
import logging
44

custom_components/foxess_modbus/services/update_charge_period_service.py

Lines changed: 1 addition & 1 deletion
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__)

custom_components/foxess_modbus/services/write_registers_service.py

Lines changed: 1 addition & 1 deletion
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__)

0 commit comments

Comments
 (0)