From 633ff0ea42a327f38e54959c972bcf148dc4062c Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Wed, 28 Aug 2024 13:14:34 +0200 Subject: [PATCH] Handle KNX expose conversion exceptions and unavailable states (#124776) --- homeassistant/components/knx/expose.py | 24 +++++++++++++++------ tests/components/knx/test_expose.py | 29 ++++++++++++++++++++++---- 2 files changed, 43 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/knx/expose.py b/homeassistant/components/knx/expose.py index 921af6ba4a9..82bee48ba69 100644 --- a/homeassistant/components/knx/expose.py +++ b/homeassistant/components/knx/expose.py @@ -125,6 +125,8 @@ class KNXExposeSensor: def _get_expose_value(self, state: State | None) -> bool | int | float | str | None: """Extract value from state.""" if state is None or state.state in (STATE_UNKNOWN, STATE_UNAVAILABLE): + if self.expose_default is None: + return None value = self.expose_default elif self.expose_attribute is not None: _attr = state.attributes.get(self.expose_attribute) @@ -154,12 +156,22 @@ class KNXExposeSensor: if value is not None and ( isinstance(self.device.sensor_value, RemoteValueSensor) ): - if issubclass(self.device.sensor_value.dpt_class, DPTNumeric): - return float(value) - if issubclass(self.device.sensor_value.dpt_class, DPTString): - # DPT 16.000 only allows up to 14 Bytes - return str(value)[:14] - return value + try: + if issubclass(self.device.sensor_value.dpt_class, DPTNumeric): + return float(value) + if issubclass(self.device.sensor_value.dpt_class, DPTString): + # DPT 16.000 only allows up to 14 Bytes + return str(value)[:14] + except (ValueError, TypeError) as err: + _LOGGER.warning( + 'Could not expose %s %s value "%s" to KNX: Conversion failed: %s', + self.entity_id, + self.expose_attribute or "state", + value, + err, + ) + return None + return value # type: ignore[no-any-return] async def _async_entity_changed(self, event: Event[EventStateChangedData]) -> None: """Handle entity change.""" diff --git a/tests/components/knx/test_expose.py b/tests/components/knx/test_expose.py index c4d0acf0ce2..0fd790a3e33 100644 --- a/tests/components/knx/test_expose.py +++ b/tests/components/knx/test_expose.py @@ -108,6 +108,11 @@ async def test_expose_attribute(hass: HomeAssistant, knx: KNXTestKit) -> None: await hass.async_block_till_done() await knx.assert_telegram_count(0) + # Ignore "unavailable" state + hass.states.async_set(entity_id, "unavailable", {attribute: None}) + await hass.async_block_till_done() + await knx.assert_telegram_count(0) + async def test_expose_attribute_with_default( hass: HomeAssistant, knx: KNXTestKit @@ -131,7 +136,7 @@ async def test_expose_attribute_with_default( await knx.receive_read("1/1/8") await knx.assert_response("1/1/8", (0,)) - # Change state to "on"; no attribute + # Change state to "on"; no attribute -> default hass.states.async_set(entity_id, "on", {}) await hass.async_block_till_done() await knx.assert_write("1/1/8", (0,)) @@ -146,6 +151,11 @@ async def test_expose_attribute_with_default( await hass.async_block_till_done() await knx.assert_no_telegram() + # Use default for "unavailable" state + hass.states.async_set(entity_id, "unavailable") + await hass.async_block_till_done() + await knx.assert_write("1/1/8", (0,)) + # Change state and attribute hass.states.async_set(entity_id, "on", {attribute: 3}) await hass.async_block_till_done() @@ -290,8 +300,18 @@ async def test_expose_value_template( assert "Error rendering value template for KNX expose" in caplog.text +@pytest.mark.parametrize( + "invalid_attribute", + [ + 101.0, + "invalid", # can't cast to float + ], +) async def test_expose_conversion_exception( - hass: HomeAssistant, caplog: pytest.LogCaptureFixture, knx: KNXTestKit + hass: HomeAssistant, + caplog: pytest.LogCaptureFixture, + knx: KNXTestKit, + invalid_attribute: str, ) -> None: """Test expose throws exception.""" @@ -313,16 +333,17 @@ async def test_expose_conversion_exception( await knx.receive_read("1/1/8") await knx.assert_response("1/1/8", (3,)) + caplog.clear() # Change attribute: Expect no exception hass.states.async_set( entity_id, "on", - {attribute: 101}, + {attribute: invalid_attribute}, ) await hass.async_block_till_done() await knx.assert_no_telegram() assert ( - 'Could not expose fake.entity fake_attribute value "101.0" to KNX:' + f'Could not expose fake.entity fake_attribute value "{invalid_attribute}" to KNX:' in caplog.text )