Skip to content

Commit 55c92b1

Browse files
authored
Merge pull request #491 from Pho3niX90/bugfix/interframe-fix
Enforce Solis-Compliant 310ms Modbus Inter-Frame Spacing
2 parents 3534cc0 + 6dd2e0b commit 55c92b1

2 files changed

Lines changed: 30 additions & 3 deletions

File tree

‎custom_components/solis_modbus/client_manager.py‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,12 @@
88

99
_LOGGER = logging.getLogger(__name__)
1010

11+
# Solis RS485_MODBUS protocol (Hybrid Inverter V3.1 / PV Grid-Connected Inverter V19),
12+
# section 3.2 "Inter-frame interval requirement": "More than 300ms communications frame
13+
# interval is required." Applies to every request on the link, read or write. 310ms gives
14+
# a small margin above the documented floor (see issue #480).
15+
MIN_INTER_FRAME_MS = 310
16+
1117

1218
class ModbusClientManager:
1319
_instance = None
@@ -102,7 +108,7 @@ async def inter_frame_wait(self, connection_id: str, is_write: bool = False) ->
102108
"""Minimum spacing between Modbus operations on one TCP/serial link, across all controllers sharing it."""
103109
if connection_id not in self._clients:
104110
return
105-
delay_ms = 100 if is_write else 50
111+
delay_ms = MIN_INTER_FRAME_MS
106112
entry = self._clients[connection_id]
107113
current_time = time.perf_counter()
108114
last = float(entry.get("last_modbus_request", 0.0))

‎tests/test_client_manager.py‎

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import unittest
22
from unittest import IsolatedAsyncioTestCase
3-
from unittest.mock import MagicMock, patch
3+
from unittest.mock import AsyncMock, MagicMock, patch
44

5-
from custom_components.solis_modbus.client_manager import ModbusClientManager
5+
from custom_components.solis_modbus.client_manager import MIN_INTER_FRAME_MS, ModbusClientManager
66

77

88
class TestModbusClientManager(unittest.TestCase):
@@ -91,6 +91,27 @@ async def test_inter_frame_wait_updates_shared_timestamp(self, mock_client_cls):
9191
await self.manager.inter_frame_wait("1.2.3.4:502", is_write=False)
9292
self.assertGreater(self.manager.get_last_modbus_request("1.2.3.4:502"), 0.0)
9393

94+
@patch("custom_components.solis_modbus.client_manager.AsyncModbusTcpClient")
95+
@patch("custom_components.solis_modbus.client_manager.time.perf_counter")
96+
@patch("custom_components.solis_modbus.client_manager.asyncio.sleep", new_callable=AsyncMock)
97+
async def test_inter_frame_wait_enforces_min_interval_for_reads_and_writes(self, mock_sleep, mock_perf_counter, mock_client_cls):
98+
mock_client_cls.return_value = MagicMock()
99+
# Two operations back-to-back on the same connection: each should independently
100+
# compute a wait based on MIN_INTER_FRAME_MS, regardless of is_write.
101+
# Call sequence: [read: current_time, read: recorded last] [write: current_time, write: recorded last]
102+
mock_perf_counter.side_effect = [0.0, 0.31, 0.32, 0.62]
103+
self.manager.get_tcp_client("1.2.3.4", 502)
104+
105+
await self.manager.inter_frame_wait("1.2.3.4:502", is_write=False)
106+
await self.manager.inter_frame_wait("1.2.3.4:502", is_write=True)
107+
108+
expected_delay_s = MIN_INTER_FRAME_MS / 1000
109+
self.assertEqual(2, mock_sleep.call_count)
110+
# Read: 0.0 elapsed since last (0.0) -> full MIN_INTER_FRAME_MS wait.
111+
self.assertAlmostEqual(expected_delay_s, mock_sleep.call_args_list[0].args[0])
112+
# Write: 10ms elapsed since last (0.31) -> remaining wait is MIN_INTER_FRAME_MS - 10ms.
113+
self.assertAlmostEqual(expected_delay_s - 0.01, mock_sleep.call_args_list[1].args[0])
114+
94115

95116
if __name__ == "__main__":
96117
unittest.main()

0 commit comments

Comments
 (0)