From 9b60882d2cd9b45639ac4f3e9284000fa8454b0f Mon Sep 17 00:00:00 2001 From: farmio Date: Sat, 8 Aug 2026 14:14:40 +0200 Subject: [PATCH 1/2] Add sync_state to KNX YAML climate, cover, light, number, switch and text These platforms have state addresses but didn't support the `sync_state` option in YAML yet. Like in the UI schema, disabling state updates entirely is not allowed for entities having a writable group address. Co-Authored-By: Claude Opus 5 --- homeassistant/components/knx/climate.py | 3 +++ homeassistant/components/knx/cover.py | 1 + homeassistant/components/knx/light.py | 1 + homeassistant/components/knx/number.py | 1 + homeassistant/components/knx/schema.py | 15 +++++++++++++++ homeassistant/components/knx/switch.py | 1 + homeassistant/components/knx/text.py | 1 + homeassistant/components/knx/validation.py | 7 +++++++ tests/components/knx/test_climate.py | 3 ++- tests/components/knx/test_cover.py | 2 ++ tests/components/knx/test_light.py | 8 +++++++- tests/components/knx/test_number.py | 2 ++ tests/components/knx/test_switch.py | 2 ++ tests/components/knx/test_text.py | 2 ++ 14 files changed, 47 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/knx/climate.py b/homeassistant/components/knx/climate.py index 63e9950ead40eb..543f7196fb68b7 100644 --- a/homeassistant/components/knx/climate.py +++ b/homeassistant/components/knx/climate.py @@ -114,6 +114,7 @@ async def async_setup_entry( def _create_climate_yaml(xknx: XKNX, config: ConfigType) -> XknxClimate: """Return a KNX Climate device to be used within XKNX.""" + sync_state = config[CONF_SYNC_STATE] climate_mode = XknxClimateMode( xknx, name=f"{config[CONF_NAME]} Mode", @@ -151,6 +152,7 @@ def _create_climate_yaml(xknx: XKNX, config: ConfigType) -> XknxClimate: group_address_heat_cool_state=config.get( ClimateSchema.CONF_HEAT_COOL_STATE_ADDRESS ), + sync_state=sync_state, operation_modes=config.get(ClimateConf.OPERATION_MODES), controller_modes=config.get(ClimateConf.CONTROLLER_MODES), ) @@ -182,6 +184,7 @@ def _create_climate_yaml(xknx: XKNX, config: ConfigType) -> XknxClimate: group_address_command_value_state=config.get( ClimateSchema.CONF_COMMAND_VALUE_STATE_ADDRESS ), + sync_state=sync_state, min_temp=config.get(ClimateConf.MIN_TEMP), max_temp=config.get(ClimateConf.MAX_TEMP), mode=climate_mode, diff --git a/homeassistant/components/knx/cover.py b/homeassistant/components/knx/cover.py index ed05afa5a15468..22fee83012772b 100644 --- a/homeassistant/components/knx/cover.py +++ b/homeassistant/components/knx/cover.py @@ -261,6 +261,7 @@ def __init__(self, knx_module: KNXModule, config: ConfigType) -> None: invert_updown=config[CoverConf.INVERT_UPDOWN], invert_position=config[CoverConf.INVERT_POSITION], invert_angle=config[CoverConf.INVERT_ANGLE], + sync_state=config[CONF_SYNC_STATE], ) super().__init__( knx_module=knx_module, diff --git a/homeassistant/components/knx/light.py b/homeassistant/components/knx/light.py index 7ec0cca6072fb6..353796ae2307ee 100644 --- a/homeassistant/components/knx/light.py +++ b/homeassistant/components/knx/light.py @@ -199,6 +199,7 @@ def individual_color_addresses(color: str, feature: str) -> Any | None: color_temperature_type=color_temperature_type, min_kelvin=config[LightSchema.CONF_MIN_KELVIN], max_kelvin=config[LightSchema.CONF_MAX_KELVIN], + sync_state=config[CONF_SYNC_STATE], ) diff --git a/homeassistant/components/knx/number.py b/homeassistant/components/knx/number.py index 7c9168700b2787..0a1ed401563a0a 100644 --- a/homeassistant/components/knx/number.py +++ b/homeassistant/components/knx/number.py @@ -118,6 +118,7 @@ def __init__(self, knx_module: KNXModule, config: ConfigType) -> None: group_address=config[KNX_ADDRESS], group_address_state=config.get(CONF_STATE_ADDRESS), respond_to_read=config[CONF_RESPOND_TO_READ], + sync_state=config[CONF_SYNC_STATE], value_type=config[CONF_TYPE], ) super().__init__( diff --git a/homeassistant/components/knx/schema.py b/homeassistant/components/knx/schema.py index 5b819e5b13cfc0..19c0d0147ddd23 100644 --- a/homeassistant/components/knx/schema.py +++ b/homeassistant/components/knx/schema.py @@ -77,6 +77,7 @@ numeric_type_validator, sensor_type_validator, string_type_validator, + sync_state_no_false_validator, sync_state_validator, validate_number_attributes, validate_sensor_attributes, @@ -421,6 +422,9 @@ class ClimateSchema(KNXPlatformSchema): vol.Optional(CONF_SWING_HORIZONTAL_ADDRESS): ga_list_validator, vol.Optional(CONF_SWING_HORIZONTAL_STATE_ADDRESS): ga_list_validator, vol.Optional(CONF_HUMIDITY_STATE_ADDRESS): ga_list_validator, + vol.Optional( + CONF_SYNC_STATE, default=True + ): sync_state_no_false_validator, } ), ) @@ -461,6 +465,9 @@ class CoverSchema(KNXPlatformSchema): vol.Optional(CoverConf.INVERT_POSITION, default=False): cv.boolean, vol.Optional(CoverConf.INVERT_ANGLE, default=False): cv.boolean, vol.Optional(CONF_DEVICE_CLASS): COVER_DEVICE_CLASSES_SCHEMA, + vol.Optional( + CONF_SYNC_STATE, default=True + ): sync_state_no_false_validator, } ), vol.Any( @@ -703,6 +710,9 @@ class LightSchema(KNXPlatformSchema): vol.Optional(CONF_MAX_KELVIN, default=DEFAULT_MAX_KELVIN): vol.All( vol.Coerce(int), vol.Range(min=1) ), + vol.Optional( + CONF_SYNC_STATE, default=True + ): sync_state_no_false_validator, } ), vol.Any( @@ -776,6 +786,9 @@ class NumberSchema(KNXPlatformSchema): vol.Optional(NumberConf.STEP): cv.positive_float, vol.Optional(CONF_DEVICE_CLASS): NUMBER_DEVICE_CLASSES_SCHEMA, vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string, + vol.Optional( + CONF_SYNC_STATE, default=True + ): sync_state_no_false_validator, } ), _number_limit_sub_validator, @@ -869,6 +882,7 @@ class SwitchSchema(KNXPlatformSchema): vol.Required(KNX_ADDRESS): ga_list_validator, vol.Optional(CONF_STATE_ADDRESS): ga_list_validator, vol.Optional(CONF_DEVICE_CLASS): SWITCH_DEVICE_CLASSES_SCHEMA, + vol.Optional(CONF_SYNC_STATE, default=True): sync_state_no_false_validator, } ) @@ -885,6 +899,7 @@ class TextSchema(KNXPlatformSchema): vol.Optional(CONF_MODE, default=TextMode.TEXT): vol.Coerce(TextMode), vol.Required(KNX_ADDRESS): ga_list_validator, vol.Optional(CONF_STATE_ADDRESS): ga_list_validator, + vol.Optional(CONF_SYNC_STATE, default=True): sync_state_no_false_validator, } ) diff --git a/homeassistant/components/knx/switch.py b/homeassistant/components/knx/switch.py index 9b2cb99059b540..55d622b33dc782 100644 --- a/homeassistant/components/knx/switch.py +++ b/homeassistant/components/knx/switch.py @@ -117,6 +117,7 @@ def __init__(self, knx_module: KNXModule, config: ConfigType) -> None: group_address=config[KNX_ADDRESS], group_address_state=config.get(SwitchSchema.CONF_STATE_ADDRESS), respond_to_read=config[CONF_RESPOND_TO_READ], + sync_state=config[CONF_SYNC_STATE], invert=config[SwitchSchema.CONF_INVERT], ) super().__init__( diff --git a/homeassistant/components/knx/text.py b/homeassistant/components/knx/text.py index eee95d137eb021..af0a8d37a1dfca 100644 --- a/homeassistant/components/knx/text.py +++ b/homeassistant/components/knx/text.py @@ -123,6 +123,7 @@ def __init__(self, knx_module: KNXModule, config: ConfigType) -> None: group_address=config[KNX_ADDRESS], group_address_state=config.get(CONF_STATE_ADDRESS), respond_to_read=config[CONF_RESPOND_TO_READ], + sync_state=config[CONF_SYNC_STATE], value_type=config[CONF_TYPE], ) super().__init__( diff --git a/homeassistant/components/knx/validation.py b/homeassistant/components/knx/validation.py index ddd7c6775b5c9d..3cf259456ffece 100644 --- a/homeassistant/components/knx/validation.py +++ b/homeassistant/components/knx/validation.py @@ -121,6 +121,13 @@ def ip_v4_validator(value: Any, multicast: bool | None = None) -> str: cv.matches_regex(r"^(init|expire|every)( \d*)?$"), ) +# entities having a writable group address can omit the state address +# instead of disabling state updates entirely +sync_state_no_false_validator = vol.All( + sync_state_validator, + vol.IsTrue("Sync state can not be disabled for this platform"), +) + def backwards_compatible_xknx_climate_enum_member(enumClass: type[Enum]) -> vol.All: """Transform a string to an enum member. diff --git a/tests/components/knx/test_climate.py b/tests/components/knx/test_climate.py index 8dbb31afaa8b37..154681fc33ccd2 100644 --- a/tests/components/knx/test_climate.py +++ b/tests/components/knx/test_climate.py @@ -3,7 +3,7 @@ import pytest from homeassistant.components.climate import HVACMode -from homeassistant.components.knx.const import ClimateConf +from homeassistant.components.knx.const import CONF_SYNC_STATE, ClimateConf from homeassistant.components.knx.schema import ClimateSchema from homeassistant.const import CONF_NAME, STATE_IDLE, Platform from homeassistant.core import HomeAssistant @@ -32,6 +32,7 @@ async def test_climate_basic_temperature_set( ClimateSchema.CONF_TEMPERATURE_ADDRESS: "1/2/3", ClimateSchema.CONF_TARGET_TEMPERATURE_ADDRESS: "1/2/4", ClimateSchema.CONF_TARGET_TEMPERATURE_STATE_ADDRESS: "1/2/5", + CONF_SYNC_STATE: "init", } } ) diff --git a/tests/components/knx/test_cover.py b/tests/components/knx/test_cover.py index a8452f4ad7d5f5..ca0cf01c199126 100644 --- a/tests/components/knx/test_cover.py +++ b/tests/components/knx/test_cover.py @@ -10,6 +10,7 @@ CoverEntityFeature, CoverState, ) +from homeassistant.components.knx.const import CONF_SYNC_STATE from homeassistant.components.knx.schema import CoverSchema from homeassistant.const import CONF_NAME, STATE_UNAVAILABLE, STATE_UNKNOWN, Platform from homeassistant.core import HomeAssistant, State @@ -30,6 +31,7 @@ async def test_cover_basic(hass: HomeAssistant, knx: KNXTestKit) -> None: CoverSchema.CONF_MOVE_SHORT_ADDRESS: "1/0/1", CoverSchema.CONF_POSITION_STATE_ADDRESS: "1/0/2", CoverSchema.CONF_POSITION_ADDRESS: "1/0/3", + CONF_SYNC_STATE: "init", } } ) diff --git a/tests/components/knx/test_light.py b/tests/components/knx/test_light.py index 6e4ea363dd0531..4af68741891029 100644 --- a/tests/components/knx/test_light.py +++ b/tests/components/knx/test_light.py @@ -7,7 +7,12 @@ from xknx.core import XknxConnectionState from xknx.devices.light import Light as XknxLight -from homeassistant.components.knx.const import CONF_STATE_ADDRESS, KNX_ADDRESS, Platform +from homeassistant.components.knx.const import ( + CONF_STATE_ADDRESS, + CONF_SYNC_STATE, + KNX_ADDRESS, + Platform, +) from homeassistant.components.knx.schema import LightSchema from homeassistant.components.light import ( ATTR_BRIGHTNESS, @@ -151,6 +156,7 @@ async def test_light_color_temp_absolute(hass: HomeAssistant, knx: KNXTestKit) - LightSchema.CONF_COLOR_TEMP_ADDRESS: test_ct, LightSchema.CONF_COLOR_TEMP_STATE_ADDRESS: test_ct_state, LightSchema.CONF_COLOR_TEMP_MODE: "absolute", + CONF_SYNC_STATE: "init", }, ] } diff --git a/tests/components/knx/test_number.py b/tests/components/knx/test_number.py index e00e3dfe4c44d3..1bdea26fabf0b7 100644 --- a/tests/components/knx/test_number.py +++ b/tests/components/knx/test_number.py @@ -8,6 +8,7 @@ from homeassistant.components.knx.const import ( CONF_RESPOND_TO_READ, CONF_STATE_ADDRESS, + CONF_SYNC_STATE, KNX_ADDRESS, ) from homeassistant.components.knx.schema import NumberSchema @@ -139,6 +140,7 @@ async def test_number_state_restore(hass: HomeAssistant, knx: KNXTestKit) -> Non KNX_ADDRESS: test_address, CONF_STATE_ADDRESS: test_state_address, CONF_TYPE: "illuminance", + CONF_SYNC_STATE: "init", } } ) diff --git a/tests/components/knx/test_switch.py b/tests/components/knx/test_switch.py index 480a585abfa3e5..c78ce01766e7d1 100644 --- a/tests/components/knx/test_switch.py +++ b/tests/components/knx/test_switch.py @@ -3,6 +3,7 @@ from homeassistant.components.knx.const import ( CONF_RESPOND_TO_READ, CONF_STATE_ADDRESS, + CONF_SYNC_STATE, KNX_ADDRESS, ) from homeassistant.components.knx.schema import SwitchSchema @@ -64,6 +65,7 @@ async def test_switch_state(hass: HomeAssistant, knx: KNXTestKit) -> None: CONF_NAME: "test", KNX_ADDRESS: _ADDRESS, CONF_STATE_ADDRESS: _STATE_ADDRESS, + CONF_SYNC_STATE: "init", }, } ) diff --git a/tests/components/knx/test_text.py b/tests/components/knx/test_text.py index 7eb25399db5fcd..678837ad1209e6 100644 --- a/tests/components/knx/test_text.py +++ b/tests/components/knx/test_text.py @@ -3,6 +3,7 @@ from homeassistant.components.knx.const import ( CONF_RESPOND_TO_READ, CONF_STATE_ADDRESS, + CONF_SYNC_STATE, KNX_ADDRESS, ) from homeassistant.components.knx.schema import TextSchema @@ -120,6 +121,7 @@ async def test_text_state_restore(hass: HomeAssistant, knx: KNXTestKit) -> None: CONF_NAME: "test", KNX_ADDRESS: test_address, CONF_STATE_ADDRESS: test_state_address, + CONF_SYNC_STATE: "init", } } ) From acdd816b1ab09650101df34cd4a5ce96051928ce Mon Sep 17 00:00:00 2001 From: farmio Date: Sat, 8 Aug 2026 14:24:18 +0200 Subject: [PATCH 2/2] Test invalid sync_state: false for KNX YAML switch Co-Authored-By: Claude Opus 5 --- tests/components/knx/test_switch.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/tests/components/knx/test_switch.py b/tests/components/knx/test_switch.py index c78ce01766e7d1..b214efea0d52df 100644 --- a/tests/components/knx/test_switch.py +++ b/tests/components/knx/test_switch.py @@ -1,5 +1,7 @@ """Test KNX switch.""" +import pytest + from homeassistant.components.knx.const import ( CONF_RESPOND_TO_READ, CONF_STATE_ADDRESS, @@ -115,6 +117,24 @@ async def test_switch_state(hass: HomeAssistant, knx: KNXTestKit) -> None: await knx.assert_telegram_count(0) +async def test_switch_sync_state_false_invalid( + hass: HomeAssistant, knx: KNXTestKit, caplog: pytest.LogCaptureFixture +) -> None: + """Test entities having a writable address don't allow disabling state updates.""" + await knx.setup_integration( + { + SwitchSchema.PLATFORM: { + CONF_NAME: "test", + KNX_ADDRESS: "1/1/1", + CONF_STATE_ADDRESS: "2/2/2", + CONF_SYNC_STATE: False, + }, + } + ) + assert "Sync state can not be disabled for this platform" in caplog.text + assert hass.states.get("switch.test") is None + + async def test_switch_state_restore(hass: HomeAssistant, knx: KNXTestKit) -> None: """Test KNX switch with state_address restores last known state until bus read completes.""" _ADDRESS = "1/1/1"