From 53ca36939556241b2bbb23ef2cfa6a1889c3050c Mon Sep 17 00:00:00 2001 From: jan iversen Date: Mon, 18 Aug 2025 17:20:41 +0200 Subject: [PATCH] Do not start modbus update process until connection+delay. (#150796) --- homeassistant/components/modbus/entity.py | 14 ++++++++++-- homeassistant/components/modbus/modbus.py | 27 ++++++----------------- tests/components/modbus/conftest.py | 11 +++++++++ tests/components/modbus/test_init.py | 16 ++++++++++---- 4 files changed, 42 insertions(+), 26 deletions(-) diff --git a/homeassistant/components/modbus/entity.py b/homeassistant/components/modbus/entity.py index eaf13d5bca4..cde017d4dd7 100644 --- a/homeassistant/components/modbus/entity.py +++ b/homeassistant/components/modbus/entity.py @@ -92,7 +92,6 @@ class BasePlatform(Entity): self._scan_interval = int(entry[CONF_SCAN_INTERVAL]) self._cancel_timer: Callable[[], None] | None = None self._cancel_call: Callable[[], None] | None = None - self._attr_unique_id = entry.get(CONF_UNIQUE_ID) self._attr_name = entry[CONF_NAME] self._attr_device_class = entry.get(CONF_DEVICE_CLASS) @@ -177,9 +176,20 @@ class BasePlatform(Entity): self._attr_available = False self.async_write_ha_state() + async def async_await_connection(self, _now: Any) -> None: + """Wait for first connect.""" + await self._hub.event_connected.wait() + self.async_run() + async def async_base_added_to_hass(self) -> None: """Handle entity which will be added.""" - self.async_run() + self.async_on_remove( + async_call_later( + self.hass, + self._hub.config_delay + 0.1, + self.async_await_connection, + ) + ) self.async_on_remove( async_dispatcher_connect(self.hass, SIGNAL_STOP_ENTITY, self.async_hold) ) diff --git a/homeassistant/components/modbus/modbus.py b/homeassistant/components/modbus/modbus.py index 186720bb40a..7343ffd1787 100644 --- a/homeassistant/components/modbus/modbus.py +++ b/homeassistant/components/modbus/modbus.py @@ -4,7 +4,6 @@ from __future__ import annotations import asyncio from collections import namedtuple -from collections.abc import Callable from typing import Any from pymodbus.client import ( @@ -28,11 +27,10 @@ from homeassistant.const import ( CONF_TYPE, EVENT_HOMEASSISTANT_STOP, ) -from homeassistant.core import Event, HomeAssistant, ServiceCall, callback +from homeassistant.core import Event, HomeAssistant, ServiceCall from homeassistant.helpers import config_validation as cv from homeassistant.helpers.discovery import async_load_platform from homeassistant.helpers.dispatcher import async_dispatcher_send -from homeassistant.helpers.event import async_call_later from homeassistant.helpers.typing import ConfigType from homeassistant.util.hass_dict import HassKey @@ -254,13 +252,13 @@ class ModbusHub: self._client: ( AsyncModbusSerialClient | AsyncModbusTcpClient | AsyncModbusUdpClient | None ) = None - self._async_cancel_listener: Callable[[], None] | None = None self._in_error = False self._lock = asyncio.Lock() + self.event_connected = asyncio.Event() self.hass = hass self.name = client_config[CONF_NAME] self._config_type = client_config[CONF_TYPE] - self._config_delay = client_config[CONF_DELAY] + self.config_delay = client_config[CONF_DELAY] self._pb_request: dict[str, RunEntry] = {} self._connect_task: asyncio.Task self._last_log_error: str = "" @@ -325,10 +323,10 @@ class ModbusHub: _LOGGER.info(message) # Start counting down to allow modbus requests. - if self._config_delay: - self._async_cancel_listener = async_call_later( - self.hass, self._config_delay, self.async_end_delay - ) + if self.config_delay: + await asyncio.sleep(self.config_delay) + self.config_delay = 0 + self.event_connected.set() async def async_setup(self) -> bool: """Set up pymodbus client.""" @@ -349,12 +347,6 @@ class ModbusHub: ) return True - @callback - def async_end_delay(self, args: Any) -> None: - """End startup delay.""" - self._async_cancel_listener = None - self._config_delay = 0 - async def async_restart(self) -> None: """Reconnect client.""" if self._client: @@ -364,9 +356,6 @@ class ModbusHub: async def async_close(self) -> None: """Disconnect client.""" - if self._async_cancel_listener: - self._async_cancel_listener() - self._async_cancel_listener = None if not self._connect_task.done(): self._connect_task.cancel() @@ -426,8 +415,6 @@ class ModbusHub: use_call: str, ) -> ModbusPDU | None: """Convert async to sync pymodbus call.""" - if self._config_delay: - return None async with self._lock: if not self._client: return None diff --git a/tests/components/modbus/conftest.py b/tests/components/modbus/conftest.py index a35cc95605d..f7bd4b13a1b 100644 --- a/tests/components/modbus/conftest.py +++ b/tests/components/modbus/conftest.py @@ -23,6 +23,7 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util +from homeassistant.util.hass_dict import HassKey from tests.common import async_fire_time_changed, mock_restore_cache @@ -121,6 +122,7 @@ def mock_pymodbus_fixture(do_exception, register_words): async def mock_modbus_fixture( hass: HomeAssistant, caplog: pytest.LogCaptureFixture, + freezer: FrozenDateTimeFactory, check_config_loaded, config_addon, do_config, @@ -158,6 +160,15 @@ async def mock_modbus_fixture( result = await async_setup_component(hass, DOMAIN, config) assert result or not check_config_loaded await hass.async_block_till_done() + key = HassKey(DOMAIN) + if key not in hass.data: + return None + hub = hass.data[HassKey(DOMAIN)][TEST_MODBUS_NAME] + await hub.event_connected.wait() + assert hub.event_connected.is_set() + freezer.tick(timedelta(seconds=1)) + async_fire_time_changed(hass) + await hass.async_block_till_done() return mock_pymodbus diff --git a/tests/components/modbus/test_init.py b/tests/components/modbus/test_init.py index 3896d34146a..3816e9878cb 100644 --- a/tests/components/modbus/test_init.py +++ b/tests/components/modbus/test_init.py @@ -920,6 +920,9 @@ async def mock_modbus_read_pymodbus_fixture( freezer.tick(timedelta(seconds=DEFAULT_SCAN_INTERVAL + 60)) async_fire_time_changed(hass) await hass.async_block_till_done() + freezer.tick(timedelta(seconds=DEFAULT_SCAN_INTERVAL + 60)) + async_fire_time_changed(hass) + await hass.async_block_till_done() return mock_pymodbus @@ -1088,11 +1091,11 @@ async def test_delay( start_time = dt_util.utcnow() assert await async_setup_component(hass, DOMAIN, config) is True await hass.async_block_till_done() - assert hass.states.get(entity_id).state == STATE_UNKNOWN + assert hass.states.get(entity_id).state in (STATE_UNKNOWN, STATE_UNAVAILABLE) time_sensor_active = start_time + timedelta(seconds=2) time_after_delay = start_time + timedelta(seconds=(set_delay)) - time_after_scan = start_time + timedelta(seconds=(set_delay + set_scan_interval)) + time_after_scan = time_after_delay + timedelta(seconds=(set_scan_interval)) time_stop = time_after_scan + timedelta(seconds=10) now = start_time while now < time_stop: @@ -1105,8 +1108,13 @@ async def test_delay( await hass.async_block_till_done() if now > time_sensor_active: if now <= time_after_delay: - assert hass.states.get(entity_id).state == STATE_UNAVAILABLE - elif now > time_after_scan: + assert hass.states.get(entity_id).state in ( + STATE_UNKNOWN, + STATE_UNAVAILABLE, + ) + if now <= time_after_delay + timedelta(seconds=2): + continue + if now > time_after_scan + timedelta(seconds=2): assert hass.states.get(entity_id).state == STATE_ON