|
30 | 30 | DEVICE_TYPE_LOAD, |
31 | 31 | DEVICE_TYPE_BATTERY, |
32 | 32 | DEVICE_TYPE_BACKUPBOX, |
| 33 | + SHARED_LOCK_TIMEOUT, |
| 34 | + DEFAULT_INTER_SLAVE_DELAY_MS, |
33 | 35 | ) |
34 | 36 |
|
35 | 37 | from .const import REGISTER_MAPS |
36 | 38 |
|
37 | | -from .growatt_modbus import GrowattModbus, GrowattData |
| 39 | +from .growatt_modbus import GrowattModbus, GrowattData, SharedModbusConnection |
38 | 40 |
|
39 | 41 | _LOGGER = logging.getLogger(__name__) |
40 | 42 |
|
@@ -97,11 +99,13 @@ def test_connection(config: dict) -> dict: |
97 | 99 | class GrowattModbusCoordinator(DataUpdateCoordinator[GrowattData]): |
98 | 100 | """Growatt Modbus data update coordinator.""" |
99 | 101 |
|
100 | | - def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None: |
| 102 | + def __init__(self, hass: HomeAssistant, entry: ConfigEntry, |
| 103 | + hub: 'SharedModbusConnection | None' = None) -> None: |
101 | 104 | """Initialize the coordinator.""" |
102 | 105 | self.entry = entry |
103 | 106 | self.config = entry.data |
104 | 107 | self.hass = hass |
| 108 | + self._hub = hub # Shared connection hub (TCP multi-entry same host:port) |
105 | 109 |
|
106 | 110 | self._slave_id = entry.data[CONF_SLAVE_ID] |
107 | 111 |
|
@@ -351,10 +355,17 @@ def _initialize_client(self): |
351 | 355 | slave_id=self.config[CONF_SLAVE_ID], |
352 | 356 | register_map=register_map, |
353 | 357 | timeout=timeout, |
354 | | - invert_battery_power=invert_battery_power |
| 358 | + invert_battery_power=invert_battery_power, |
| 359 | + shared_conn=self._hub, |
355 | 360 | ) |
356 | | - _LOGGER.debug("Initialized TCP Growatt client at %s:%s (invert_battery_power=%s)", |
357 | | - self.config[CONF_HOST], self.config[CONF_PORT], invert_battery_power) |
| 361 | + if self._hub: |
| 362 | + _LOGGER.debug( |
| 363 | + "Initialized TCP Growatt client at %s:%s (shared connection mode, invert_battery_power=%s)", |
| 364 | + self.config[CONF_HOST], self.config[CONF_PORT], invert_battery_power, |
| 365 | + ) |
| 366 | + else: |
| 367 | + _LOGGER.debug("Initialized TCP Growatt client at %s:%s (invert_battery_power=%s)", |
| 368 | + self.config[CONF_HOST], self.config[CONF_PORT], invert_battery_power) |
358 | 369 | else: # serial |
359 | 370 | self._client = GrowattModbus( |
360 | 371 | connection_type="serial", |
@@ -896,8 +907,60 @@ async def _async_update_data(self) -> GrowattData: |
896 | 907 | self.data = GrowattData() |
897 | 908 | return self.data |
898 | 909 |
|
| 910 | + def _fetch_data_shared(self) -> GrowattData | None: |
| 911 | + """Fetch data using the shared connection hub (holds hub lock for the full poll).""" |
| 912 | + hub = self._hub |
| 913 | + inter_slave_delay = self.config_entry.options.get( |
| 914 | + "inter_slave_delay", DEFAULT_INTER_SLAVE_DELAY_MS |
| 915 | + ) / 1000.0 |
| 916 | + |
| 917 | + acquired = hub._lock.acquire(timeout=SHARED_LOCK_TIMEOUT) |
| 918 | + if not acquired: |
| 919 | + _LOGGER.warning( |
| 920 | + "Shared Modbus connection busy (lock timeout %ds) for %s:%s slave %s — skipping this poll", |
| 921 | + SHARED_LOCK_TIMEOUT, |
| 922 | + self.config.get(CONF_HOST), |
| 923 | + self.config.get(CONF_PORT), |
| 924 | + self.config.get(CONF_SLAVE_ID), |
| 925 | + ) |
| 926 | + return None |
| 927 | + |
| 928 | + try: |
| 929 | + if not hub.ensure_connected(): |
| 930 | + _LOGGER.warning( |
| 931 | + "Shared Modbus connection could not connect to %s:%s", |
| 932 | + self.config.get(CONF_HOST), self.config.get(CONF_PORT), |
| 933 | + ) |
| 934 | + return None |
| 935 | + |
| 936 | + self._client._battery_voltage_range = self.config_entry.options.get( |
| 937 | + "battery_voltage_range", "Auto-detect" |
| 938 | + ) |
| 939 | + delay_s = self.config_entry.options.get("modbus_delay", 250) / 1000.0 |
| 940 | + self._client._default_min_read_interval = delay_s |
| 941 | + if not self._client._backed_off: |
| 942 | + self._client.min_read_interval = delay_s |
| 943 | + |
| 944 | + data = self._client.read_all_data() |
| 945 | + if data is not None and not self._serial_number: |
| 946 | + self._read_device_identification() |
| 947 | + |
| 948 | + time.sleep(inter_slave_delay) |
| 949 | + return data |
| 950 | + |
| 951 | + except Exception as err: |
| 952 | + _LOGGER.warning("Error during shared data fetch for slave %s: %s", |
| 953 | + self.config.get(CONF_SLAVE_ID), err) |
| 954 | + return None |
| 955 | + |
| 956 | + finally: |
| 957 | + hub._lock.release() |
| 958 | + |
899 | 959 | def _fetch_data(self) -> GrowattData | None: |
900 | 960 | """Fetch data from the inverter (runs in executor).""" |
| 961 | + if self._hub is not None: |
| 962 | + return self._fetch_data_shared() |
| 963 | + |
901 | 964 | max_retries = 3 |
902 | 965 | retry_delay = 3 # seconds - increased from 2 |
903 | 966 |
|
|
0 commit comments