diff --git a/homeassistant/components/knx/__init__.py b/homeassistant/components/knx/__init__.py index e177a861af3..8a8e87b893f 100644 --- a/homeassistant/components/knx/__init__.py +++ b/homeassistant/components/knx/__init__.py @@ -60,6 +60,7 @@ from .const import ( CONF_KNX_SECURE_USER_ID, CONF_KNX_SECURE_USER_PASSWORD, CONF_KNX_STATE_UPDATER, + CONF_KNX_TELEGRAM_LOG_SIZE, CONF_KNX_TUNNELING, CONF_KNX_TUNNELING_TCP, CONF_KNX_TUNNELING_TCP_SECURE, @@ -68,6 +69,7 @@ from .const import ( DOMAIN, KNX_ADDRESS, SUPPORTED_PLATFORMS, + TELEGRAM_LOG_DEFAULT, ) from .device import KNXInterfaceDevice from .expose import KNXExposeSensor, KNXExposeTime, create_knx_exposure @@ -384,7 +386,12 @@ class KNXModule: self.xknx.connection_manager.register_connection_state_changed_cb( self.connection_state_changed_cb ) - self.telegrams = Telegrams(hass, self.xknx, self.project) + self.telegrams = Telegrams( + hass=hass, + xknx=self.xknx, + project=self.project, + log_size=entry.data.get(CONF_KNX_TELEGRAM_LOG_SIZE, TELEGRAM_LOG_DEFAULT), + ) self.interface_device = KNXInterfaceDevice( hass=hass, entry=entry, xknx=self.xknx ) diff --git a/homeassistant/components/knx/config_flow.py b/homeassistant/components/knx/config_flow.py index 81610d62dcf..0a405146d9c 100644 --- a/homeassistant/components/knx/config_flow.py +++ b/homeassistant/components/knx/config_flow.py @@ -49,12 +49,15 @@ from .const import ( CONF_KNX_SECURE_USER_ID, CONF_KNX_SECURE_USER_PASSWORD, CONF_KNX_STATE_UPDATER, + CONF_KNX_TELEGRAM_LOG_SIZE, CONF_KNX_TUNNEL_ENDPOINT_IA, CONF_KNX_TUNNELING, CONF_KNX_TUNNELING_TCP, CONF_KNX_TUNNELING_TCP_SECURE, DEFAULT_ROUTING_IA, DOMAIN, + TELEGRAM_LOG_DEFAULT, + TELEGRAM_LOG_MAX, KNXConfigEntryData, ) from .schema import ia_validator, ip_v4_validator @@ -70,6 +73,7 @@ DEFAULT_ENTRY_DATA = KNXConfigEntryData( rate_limit=CONF_KNX_DEFAULT_RATE_LIMIT, route_back=False, state_updater=CONF_KNX_DEFAULT_STATE_UPDATER, + telegram_log_size=TELEGRAM_LOG_DEFAULT, ) CONF_KEYRING_FILE: Final = "knxkeys_file" @@ -203,7 +207,11 @@ class KNXCommonFlow(ABC, FlowHandler): ) async def async_step_tunnel(self, user_input: dict | None = None) -> FlowResult: - """Select a tunnel from a list. Will be skipped if the gateway scan was unsuccessful or if only one gateway was found.""" + """Select a tunnel from a list. + + Will be skipped if the gateway scan was unsuccessful + or if only one gateway was found. + """ if user_input is not None: if user_input[CONF_KNX_GATEWAY] == OPTION_MANUAL_TUNNEL: if self._found_tunnels: @@ -804,6 +812,7 @@ class KNXOptionsFlow(KNXCommonFlow, OptionsFlow): self.new_entry_data = KNXConfigEntryData( state_updater=user_input[CONF_KNX_STATE_UPDATER], rate_limit=user_input[CONF_KNX_RATE_LIMIT], + telegram_log_size=user_input[CONF_KNX_TELEGRAM_LOG_SIZE], ) return self.finish_flow() @@ -811,15 +820,13 @@ class KNXOptionsFlow(KNXCommonFlow, OptionsFlow): vol.Required( CONF_KNX_STATE_UPDATER, default=self.initial_data.get( - CONF_KNX_STATE_UPDATER, - CONF_KNX_DEFAULT_STATE_UPDATER, + CONF_KNX_STATE_UPDATER, CONF_KNX_DEFAULT_STATE_UPDATER ), ): selector.BooleanSelector(), vol.Required( CONF_KNX_RATE_LIMIT, default=self.initial_data.get( - CONF_KNX_RATE_LIMIT, - CONF_KNX_DEFAULT_RATE_LIMIT, + CONF_KNX_RATE_LIMIT, CONF_KNX_DEFAULT_RATE_LIMIT ), ): vol.All( selector.NumberSelector( @@ -831,9 +838,27 @@ class KNXOptionsFlow(KNXCommonFlow, OptionsFlow): ), vol.Coerce(int), ), + vol.Required( + CONF_KNX_TELEGRAM_LOG_SIZE, + default=self.initial_data.get( + CONF_KNX_TELEGRAM_LOG_SIZE, TELEGRAM_LOG_DEFAULT + ), + ): vol.All( + selector.NumberSelector( + selector.NumberSelectorConfig( + min=0, + max=TELEGRAM_LOG_MAX, + mode=selector.NumberSelectorMode.BOX, + ), + ), + vol.Coerce(int), + ), } return self.async_show_form( step_id="communication_settings", data_schema=vol.Schema(data_schema), last_step=True, + description_placeholders={ + "telegram_log_size_max": f"{TELEGRAM_LOG_MAX}", + }, ) diff --git a/homeassistant/components/knx/const.py b/homeassistant/components/knx/const.py index 858f1cefea0..5546a2d6fd9 100644 --- a/homeassistant/components/knx/const.py +++ b/homeassistant/components/knx/const.py @@ -52,6 +52,10 @@ CONF_KNX_DEFAULT_RATE_LIMIT: Final = 0 DEFAULT_ROUTING_IA: Final = "0.0.240" +CONF_KNX_TELEGRAM_LOG_SIZE: Final = "telegram_log_size" +TELEGRAM_LOG_DEFAULT: Final = 50 +TELEGRAM_LOG_MAX: Final = 5000 # ~2 MB or ~5 hours of reasonable bus load + ## # Secure constants ## @@ -88,23 +92,26 @@ class KNXConfigEntryData(TypedDict, total=False): connection_type: str individual_address: str - local_ip: str | None + local_ip: str | None # not required multicast_group: str multicast_port: int - route_back: bool + route_back: bool # not required + host: str # only required for tunnelling + port: int # only required for tunnelling + tunnel_endpoint_ia: str | None + # KNX secure + user_id: int | None # not required + user_password: str | None # not required + device_authentication: str | None # not required + knxkeys_filename: str # not required + knxkeys_password: str # not required + backbone_key: str | None # not required + sync_latency_tolerance: int | None # not required + # OptionsFlow only state_updater: bool rate_limit: int - host: str - port: int - tunnel_endpoint_ia: str | None - - user_id: int | None - user_password: str | None - device_authentication: str | None - knxkeys_filename: str - knxkeys_password: str - backbone_key: str | None - sync_latency_tolerance: int | None + # Integration only (not forwarded to xknx) + telegram_log_size: int # not required class KNXBusMonitorMessage(TypedDict): diff --git a/homeassistant/components/knx/strings.json b/homeassistant/components/knx/strings.json index d4a1eae11c2..cdd61379567 100644 --- a/homeassistant/components/knx/strings.json +++ b/homeassistant/components/knx/strings.json @@ -133,11 +133,13 @@ "title": "Communication settings", "data": { "state_updater": "State updater", - "rate_limit": "Rate limit" + "rate_limit": "Rate limit", + "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.", - "rate_limit": "Maximum outgoing telegrams per second.\n`0` to disable limit. Recommended: 0 or 20 to 40" + "rate_limit": "Maximum outgoing telegrams per second.\n`0` to disable limit. Recommended: 0 or 20 to 40", + "telegram_log_size": "Telegrams to keep in memory for KNX panel group monitor. Maximum: {telegram_log_size_max}" } }, "connection_type": { diff --git a/homeassistant/components/knx/telegrams.py b/homeassistant/components/knx/telegrams.py index 4c5ac44f6b1..5b429b0bdc1 100644 --- a/homeassistant/components/knx/telegrams.py +++ b/homeassistant/components/knx/telegrams.py @@ -35,7 +35,13 @@ class TelegramDict(TypedDict): class Telegrams: """Class to handle KNX telegrams.""" - def __init__(self, hass: HomeAssistant, xknx: XKNX, project: KNXProject) -> None: + def __init__( + self, + hass: HomeAssistant, + xknx: XKNX, + project: KNXProject, + log_size: int, + ) -> None: """Initialize Telegrams class.""" self.hass = hass self.project = project @@ -46,7 +52,7 @@ class Telegrams: match_for_outgoing=True, ) ) - self.recent_telegrams: deque[TelegramDict] = deque(maxlen=50) + self.recent_telegrams: deque[TelegramDict] = deque(maxlen=log_size) async def _xknx_telegram_cb(self, telegram: Telegram) -> None: """Handle incoming and outgoing telegrams from xknx.""" diff --git a/tests/components/knx/test_config_flow.py b/tests/components/knx/test_config_flow.py index 054d7844714..ca804176ee9 100644 --- a/tests/components/knx/test_config_flow.py +++ b/tests/components/knx/test_config_flow.py @@ -37,6 +37,7 @@ from homeassistant.components.knx.const import ( CONF_KNX_SECURE_USER_ID, CONF_KNX_SECURE_USER_PASSWORD, CONF_KNX_STATE_UPDATER, + CONF_KNX_TELEGRAM_LOG_SIZE, CONF_KNX_TUNNEL_ENDPOINT_IA, CONF_KNX_TUNNELING, CONF_KNX_TUNNELING_TCP, @@ -820,7 +821,6 @@ async def test_tunneling_setup_for_multiple_found_gateways( CONF_PORT: 3675, CONF_KNX_INDIVIDUAL_ADDRESS: "0.0.240", CONF_KNX_ROUTE_BACK: False, - CONF_KNX_LOCAL_IP: None, CONF_KNX_TUNNEL_ENDPOINT_IA: None, CONF_KNX_SECURE_DEVICE_AUTHENTICATION: None, CONF_KNX_SECURE_USER_ID: None, @@ -900,9 +900,17 @@ async def test_form_with_automatic_connection_handling( assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["title"] == CONF_KNX_AUTOMATIC.capitalize() assert result2["data"] == { - **DEFAULT_ENTRY_DATA, + # don't use **DEFAULT_ENTRY_DATA here to check for correct usage of defaults CONF_KNX_CONNECTION_TYPE: CONF_KNX_AUTOMATIC, + CONF_KNX_INDIVIDUAL_ADDRESS: "0.0.240", + CONF_KNX_LOCAL_IP: None, + CONF_KNX_MCAST_PORT: DEFAULT_MCAST_PORT, + CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP, + CONF_KNX_RATE_LIMIT: 0, + CONF_KNX_ROUTE_BACK: False, CONF_KNX_TUNNEL_ENDPOINT_IA: None, + CONF_KNX_STATE_UPDATER: True, + CONF_KNX_TELEGRAM_LOG_SIZE: 50, } knx_setup.assert_called_once() @@ -1202,6 +1210,7 @@ async def test_options_flow_connection_type( CONF_KNX_SECURE_DEVICE_AUTHENTICATION: None, CONF_KNX_SECURE_USER_ID: None, CONF_KNX_SECURE_USER_PASSWORD: None, + CONF_KNX_TELEGRAM_LOG_SIZE: 50, } @@ -1331,6 +1340,7 @@ async def test_options_communication_settings( user_input={ CONF_KNX_STATE_UPDATER: False, CONF_KNX_RATE_LIMIT: 40, + CONF_KNX_TELEGRAM_LOG_SIZE: 3000, }, ) await hass.async_block_till_done() @@ -1341,6 +1351,7 @@ async def test_options_communication_settings( CONF_KNX_CONNECTION_TYPE: CONF_KNX_AUTOMATIC, CONF_KNX_STATE_UPDATER: False, CONF_KNX_RATE_LIMIT: 40, + CONF_KNX_TELEGRAM_LOG_SIZE: 3000, } knx_setup.assert_called_once()