From 11bc00038ee4c87a7cd1ae4df996e3d382a5ef14 Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Tue, 10 Mar 2026 19:27:48 +0100 Subject: [PATCH] KNX: add config for `device_class` and `unit_of_measurement` for yaml number entities (#165083) --- homeassistant/components/knx/number.py | 21 ++++++++++++-- homeassistant/components/knx/schema.py | 8 +++++- tests/components/knx/test_dpt.py | 11 +++++++- tests/components/knx/test_number.py | 39 ++++++++++++++++++++++++++ 4 files changed, 74 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/knx/number.py b/homeassistant/components/knx/number.py index 645715dc6aa..c8079dc583a 100644 --- a/homeassistant/components/knx/number.py +++ b/homeassistant/components/knx/number.py @@ -120,6 +120,19 @@ class KnxYamlNumber(_KnxNumber, KnxYamlEntity): value_type=config[CONF_TYPE], ), ) + dpt_string = self._device.sensor_value.dpt_class.dpt_number_str() + dpt_info = get_supported_dpts()[dpt_string] + + self._attr_device_class = config.get( + CONF_DEVICE_CLASS, + try_parse_enum( + # sensor device classes should, with some exceptions ("enum" etc.), align with number device classes + NumberDeviceClass, + dpt_info["sensor_device_class"], + ), + ) + self._attr_entity_category = config.get(CONF_ENTITY_CATEGORY) + self._attr_mode = config[CONF_MODE] self._attr_native_max_value = config.get( NumberConf.MAX, self._device.sensor_value.dpt_class.value_max, @@ -128,14 +141,16 @@ class KnxYamlNumber(_KnxNumber, KnxYamlEntity): NumberConf.MIN, self._device.sensor_value.dpt_class.value_min, ) - self._attr_mode = config[CONF_MODE] self._attr_native_step = config.get( NumberConf.STEP, self._device.sensor_value.dpt_class.resolution, ) - self._attr_entity_category = config.get(CONF_ENTITY_CATEGORY) + self._attr_native_unit_of_measurement = config.get( + CONF_UNIT_OF_MEASUREMENT, + dpt_info["unit"], + ) self._attr_unique_id = str(self._device.sensor_value.group_address) - self._attr_native_unit_of_measurement = self._device.unit_of_measurement() + self._device.sensor_value.value = max(0, self._attr_native_min_value) diff --git a/homeassistant/components/knx/schema.py b/homeassistant/components/knx/schema.py index 62b7e35047e..49aaf532431 100644 --- a/homeassistant/components/knx/schema.py +++ b/homeassistant/components/knx/schema.py @@ -20,7 +20,10 @@ from homeassistant.components.climate import FAN_OFF, HVACMode from homeassistant.components.cover import ( DEVICE_CLASSES_SCHEMA as COVER_DEVICE_CLASSES_SCHEMA, ) -from homeassistant.components.number import NumberMode +from homeassistant.components.number import ( + DEVICE_CLASSES_SCHEMA as NUMBER_DEVICE_CLASSES_SCHEMA, + NumberMode, +) from homeassistant.components.sensor import ( CONF_STATE_CLASS as CONF_SENSOR_STATE_CLASS, DEVICE_CLASSES_SCHEMA as SENSOR_DEVICE_CLASSES_SCHEMA, @@ -39,6 +42,7 @@ from homeassistant.const import ( CONF_NAME, CONF_PAYLOAD, CONF_TYPE, + CONF_UNIT_OF_MEASUREMENT, CONF_VALUE_TEMPLATE, Platform, ) @@ -787,6 +791,8 @@ class NumberSchema(KNXPlatformSchema): vol.Optional(NumberConf.MAX): vol.Coerce(float), vol.Optional(NumberConf.MIN): vol.Coerce(float), 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_ENTITY_CATEGORY): ENTITY_CATEGORIES_SCHEMA, } ), diff --git a/tests/components/knx/test_dpt.py b/tests/components/knx/test_dpt.py index e379fcfedd9..7840f9f7474 100644 --- a/tests/components/knx/test_dpt.py +++ b/tests/components/knx/test_dpt.py @@ -7,7 +7,10 @@ from homeassistant.components.knx.dpt import ( _sensor_state_class_overrides, _sensor_unit_overrides, ) -from homeassistant.components.knx.schema import _sensor_attribute_sub_validator +from homeassistant.components.knx.schema import ( + _number_limit_sub_validator, + _sensor_attribute_sub_validator, +) @pytest.mark.parametrize( @@ -31,3 +34,9 @@ def test_dpt_default_device_classes(dpt: str) -> None: # UI validation works the same way, but uses different schema for config {"type": dpt} ) + number_config = {"type": dpt} + if dpt.startswith("14"): + # DPT 14 has infinite range which isn't supported by HA + # this test shall still check for correct device_class and unit_of_measurement + number_config |= {"min": -500000, "max": 500000} + assert _number_limit_sub_validator(number_config) diff --git a/tests/components/knx/test_number.py b/tests/components/knx/test_number.py index 2c24e289011..f4b8856cabe 100644 --- a/tests/components/knx/test_number.py +++ b/tests/components/knx/test_number.py @@ -1,5 +1,6 @@ """Test KNX number.""" +import logging from typing import Any import pytest @@ -111,6 +112,44 @@ async def test_number_restore_and_respond(hass: HomeAssistant, knx: KNXTestKit) assert state.state == "9000.96" +@pytest.mark.parametrize( + "attribute_config", + [ + {"device_class": "energy"}, # invalid with uom of temperature DPT + {"device_class": "energy", "unit_of_measurement": "invalid"}, + {"device_class": "invalid"}, + ], +) +async def test_number_yaml_attribute_validation( + hass: HomeAssistant, + caplog: pytest.LogCaptureFixture, + knx: KNXTestKit, + attribute_config: dict[str, Any], +) -> None: + """Test creating a number with invalid unit or device_class.""" + with caplog.at_level(logging.ERROR): + await knx.setup_integration( + { + NumberSchema.PLATFORM: { + CONF_NAME: "test", + KNX_ADDRESS: "1/1/1", + CONF_TYPE: "9.001", # temperature 2 byte float + **attribute_config, + } + } + ) + assert len(caplog.messages) == 2 + record = caplog.records[0] + assert record.levelname == "ERROR" + assert "Invalid config for 'knx': " in record.message + + record = caplog.records[1] + assert record.levelname == "ERROR" + assert "Setup failed for 'knx': Invalid config." in record.message + + assert hass.states.get("number.test") is None + + @pytest.mark.parametrize( ("knx_config", "set_value", "expected_telegram", "expected_state"), [