From af0f416497bc5fe4b09884b20615588f2d66cf7c Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Sun, 19 Jan 2025 12:53:09 +0100 Subject: [PATCH] Fix KNX default state updater option (#135611) --- homeassistant/components/knx/__init__.py | 10 ++- homeassistant/components/knx/const.py | 2 +- homeassistant/components/knx/strings.json | 2 +- tests/components/knx/conftest.py | 2 +- tests/components/knx/test_init.py | 85 ++++++++++++++++++++++- 5 files changed, 94 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/knx/__init__.py b/homeassistant/components/knx/__init__.py index 7925628c079..fa3439b02f4 100644 --- a/homeassistant/components/knx/__init__.py +++ b/homeassistant/components/knx/__init__.py @@ -10,6 +10,7 @@ from typing import Final import voluptuous as vol from xknx import XKNX from xknx.core import XknxConnectionState +from xknx.core.state_updater import StateTrackerType, TrackerOptions from xknx.core.telegram_queue import TelegramQueue from xknx.dpt import DPTBase from xknx.exceptions import ConversionError, CouldNotParseTelegram, XKNXException @@ -273,11 +274,18 @@ class KNXModule: self.project = KNXProject(hass=hass, entry=entry) self.config_store = KNXConfigStore(hass=hass, config_entry=entry) + default_state_updater = ( + TrackerOptions(tracker_type=StateTrackerType.EXPIRE, update_interval_min=60) + if self.entry.data[CONF_KNX_STATE_UPDATER] + else TrackerOptions( + tracker_type=StateTrackerType.INIT, update_interval_min=60 + ) + ) self.xknx = XKNX( address_format=self.project.get_address_format(), connection_config=self.connection_config(), rate_limit=self.entry.data[CONF_KNX_RATE_LIMIT], - state_updater=self.entry.data[CONF_KNX_STATE_UPDATER], + state_updater=default_state_updater, ) self.xknx.connection_manager.register_connection_state_changed_cb( self.connection_state_changed_cb diff --git a/homeassistant/components/knx/const.py b/homeassistant/components/knx/const.py index a946ded0359..3ef35479c4e 100644 --- a/homeassistant/components/knx/const.py +++ b/homeassistant/components/knx/const.py @@ -114,7 +114,7 @@ class KNXConfigEntryData(TypedDict, total=False): backbone_key: str | None # not required sync_latency_tolerance: int | None # not required # OptionsFlow only - state_updater: bool + state_updater: bool # default state updater: True -> expire 60; False -> init rate_limit: int # Integration only (not forwarded to xknx) telegram_log_size: int # not required diff --git a/homeassistant/components/knx/strings.json b/homeassistant/components/knx/strings.json index e7fbfcf5f2f..dadc8e84796 100644 --- a/homeassistant/components/knx/strings.json +++ b/homeassistant/components/knx/strings.json @@ -161,7 +161,7 @@ "telegram_log_size": "Telegram history limit" }, "data_description": { - "state_updater": "Set default for reading states from the KNX Bus. When disabled, Home Assistant will not actively retrieve entity states from the KNX Bus. Can be overridden by `sync_state` entity options.", + "state_updater": "Sets the default behavior for reading state addresses from the KNX Bus.\nWhen enabled, Home Assistant will monitor each group address and read it from the bus if no value has been received for one hour.\nWhen disabled, state addresses will only be read once after a bus connection is established.\nThis behavior can be overridden for individual entities using the `sync_state` option.", "rate_limit": "Maximum outgoing telegrams per second.\n`0` to disable limit. Recommended: `0` or between `20` and `40`", "telegram_log_size": "Telegrams to keep in memory for KNX panel group monitor. Maximum: {telegram_log_size_max}" } diff --git a/tests/components/knx/conftest.py b/tests/components/knx/conftest.py index 80d75769cdc..4e50836bb79 100644 --- a/tests/components/knx/conftest.py +++ b/tests/components/knx/conftest.py @@ -335,7 +335,7 @@ async def create_ui_entity( hass_ws_client: WebSocketGenerator, hass_storage: dict[str, Any], ) -> KnxEntityGenerator: - """Return a helper to create a KNX entities via WS. + """Return a helper to create KNX entities via WS. The KNX integration must be set up before using the helper. """ diff --git a/tests/components/knx/test_init.py b/tests/components/knx/test_init.py index d005487b8f2..75cd5d1eb21 100644 --- a/tests/components/knx/test_init.py +++ b/tests/components/knx/test_init.py @@ -1,7 +1,9 @@ """Test KNX init.""" +from datetime import timedelta from unittest.mock import patch +from freezegun.api import FrozenDateTimeFactory import pytest from xknx.io import ( DEFAULT_MCAST_GRP, @@ -11,7 +13,10 @@ from xknx.io import ( SecureConfig, ) -from homeassistant.components.knx.config_flow import DEFAULT_ROUTING_IA +from homeassistant.components.knx.config_flow import ( + DEFAULT_ENTRY_DATA, + DEFAULT_ROUTING_IA, +) from homeassistant.components.knx.const import ( CONF_KNX_AUTOMATIC, CONF_KNX_CONNECTION_TYPE, @@ -40,12 +45,13 @@ from homeassistant.components.knx.const import ( KNXConfigEntryData, ) from homeassistant.config_entries import ConfigEntryState -from homeassistant.const import CONF_HOST, CONF_PORT +from homeassistant.const import CONF_HOST, CONF_PORT, Platform from homeassistant.core import HomeAssistant +from . import KnxEntityGenerator from .conftest import KNXTestKit -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, async_fire_time_changed @pytest.mark.parametrize( @@ -262,6 +268,79 @@ async def test_init_connection_handling( ) +async def _init_switch_and_wait_for_first_state_updater_run( + hass: HomeAssistant, + knx: KNXTestKit, + create_ui_entity: KnxEntityGenerator, + freezer: FrozenDateTimeFactory, + config_entry_data: KNXConfigEntryData, +) -> None: + """Return a config entry with default data.""" + config_entry = MockConfigEntry( + title="KNX", domain=KNX_DOMAIN, data=config_entry_data + ) + knx.mock_config_entry = config_entry + await knx.setup_integration({}) + await create_ui_entity( + platform=Platform.SWITCH, + knx_data={ + "ga_switch": {"write": "1/1/1", "state": "2/2/2"}, + "respond_to_read": True, + "sync_state": True, # True uses xknx default state updater + "invert": False, + }, + ) + # created entity sends read-request to KNX bus on connection + await knx.assert_read("2/2/2") + await knx.receive_response("2/2/2", True) + + freezer.tick(timedelta(minutes=59)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + await hass.async_block_till_done() + await knx.assert_no_telegram() + + freezer.tick(timedelta(minutes=1)) # 60 minutes passed + async_fire_time_changed(hass) + await hass.async_block_till_done() + await hass.async_block_till_done() + + +async def test_default_state_updater_enabled( + hass: HomeAssistant, + knx: KNXTestKit, + create_ui_entity: KnxEntityGenerator, + freezer: FrozenDateTimeFactory, +) -> None: + """Test default state updater is applied to xknx device instances.""" + config_entry = DEFAULT_ENTRY_DATA | KNXConfigEntryData( + connection_type=CONF_KNX_AUTOMATIC, # missing in default data + state_updater=True, + ) + await _init_switch_and_wait_for_first_state_updater_run( + hass, knx, create_ui_entity, freezer, config_entry + ) + await knx.assert_read("2/2/2") + await knx.receive_response("2/2/2", True) + + +async def test_default_state_updater_disabled( + hass: HomeAssistant, + knx: KNXTestKit, + create_ui_entity: KnxEntityGenerator, + freezer: FrozenDateTimeFactory, +) -> None: + """Test default state updater is applied to xknx device instances.""" + config_entry = DEFAULT_ENTRY_DATA | KNXConfigEntryData( + connection_type=CONF_KNX_AUTOMATIC, # missing in default data + state_updater=False, + ) + await _init_switch_and_wait_for_first_state_updater_run( + hass, knx, create_ui_entity, freezer, config_entry + ) + await knx.assert_no_telegram() + + async def test_async_remove_entry( hass: HomeAssistant, knx: KNXTestKit,