From 5325fdcef6674a3d3d9de2c32479ab02b8397243 Mon Sep 17 00:00:00 2001 From: Teemu Rytilahti Date: Wed, 22 Jul 2026 02:06:44 +0300 Subject: [PATCH 1/3] Allow float values in miot value-range and range descriptors The MIoT schema allows floats for range bounds and step values, so int is too strict for these fields. --- miio/descriptors.py | 12 ++++++------ miio/miot_models.py | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/miio/descriptors.py b/miio/descriptors.py index beef5c2a5..5508c1fe3 100644 --- a/miio/descriptors.py +++ b/miio/descriptors.py @@ -22,9 +22,9 @@ class ValidSettingRange: """Describes a valid input range for a property.""" - min_value: int - max_value: int - step: int = 1 + min_value: float + max_value: float + step: float = 1 class AccessFlags(Flag): @@ -177,11 +177,11 @@ class RangeDescriptor(PropertyDescriptor): """ #: Minimum value for the property. - min_value: int + min_value: float #: Maximum value for the property. - max_value: int + max_value: float #: Step size for the property. - step: int + step: float #: Name of the attribute in the device class that returns the range. #: If set, this will override the individual min/max/step values. range_attribute: str | None = attr.ib(default=None) diff --git a/miio/miot_models.py b/miio/miot_models.py index 68ca82eb7..fe5a606be 100644 --- a/miio/miot_models.py +++ b/miio/miot_models.py @@ -244,7 +244,7 @@ class MiotProperty(MiotBaseModel): access: list[MiotAccess] = Field(default=[MiotAccess.Read]) unit: str | None = None - range: list[int] | None = Field(default=None, alias="value-range") + range: list[float] | None = Field(default=None, alias="value-range") choices: list[MiotEnumValue] | None = Field(default=None, alias="value-list") gatt_access: list[Any] | None = Field(default=None, alias="gatt-access") From e918798c5d011207ac69da094db8cc7c7e0d159a Mon Sep 17 00:00:00 2001 From: Teemu Rytilahti Date: Wed, 22 Jul 2026 02:13:37 +0300 Subject: [PATCH 2/3] Add test for float values in miot value-range --- miio/tests/test_miot_models.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/miio/tests/test_miot_models.py b/miio/tests/test_miot_models.py index 30306adc6..fd28de648 100644 --- a/miio/tests/test_miot_models.py +++ b/miio/tests/test_miot_models.py @@ -391,6 +391,21 @@ def test_get_descriptor_ranged_property(read_only, expected): assert desc.constraint == PropertyConstraint.Range +def test_get_descriptor_ranged_property_float_range(): + """Test that float values in value-range are accepted.""" + ranged_prop = load_fixture("ranged_property.json") + ranged_prop["format"] = "float" + ranged_prop["value-range"] = [-30, 100, 1e-05] + + prop = MiotProperty.model_validate(ranged_prop) + desc = prop.get_descriptor() + + assert isinstance(desc, RangeDescriptor) + assert desc.min_value == -30 + assert desc.max_value == 100 + assert desc.step == 1e-05 + + def test_get_descriptor_ranged_property_none_format(): """Test that a ranged property with format=none raises ValueError.""" ranged_prop = load_fixture("ranged_property.json") From af147a7be1d7e90d1eed3c0658d7a943b5183a34 Mon Sep 17 00:00:00 2001 From: Teemu Rytilahti Date: Wed, 22 Jul 2026 02:16:27 +0300 Subject: [PATCH 3/3] Coerce value-range to the property's format type --- miio/miot_models.py | 7 +++++++ miio/tests/test_miot_models.py | 24 +++++++++++++++++------- 2 files changed, 24 insertions(+), 7 deletions(-) diff --git a/miio/miot_models.py b/miio/miot_models.py index fe5a606be..ba567c676 100644 --- a/miio/miot_models.py +++ b/miio/miot_models.py @@ -254,6 +254,13 @@ class MiotProperty(MiotBaseModel): # there must be a better way to do this.. value: Any | None = None + @model_validator(mode="after") + def coerce_range_to_format_type(self) -> Self: + """Coerce range values to the property's numeric type.""" + if self.range is not None and self.format in (int, float): + self.range = [self.format(v) for v in self.range] + return self + @property def pretty_value(self): value = self.value diff --git a/miio/tests/test_miot_models.py b/miio/tests/test_miot_models.py index fd28de648..400eb5bad 100644 --- a/miio/tests/test_miot_models.py +++ b/miio/tests/test_miot_models.py @@ -391,19 +391,29 @@ def test_get_descriptor_ranged_property(read_only, expected): assert desc.constraint == PropertyConstraint.Range -def test_get_descriptor_ranged_property_float_range(): - """Test that float values in value-range are accepted.""" +@pytest.mark.parametrize( + ("format", "range_values", "expected_type"), + [ + ("float", [-30, 100, 1e-05], float), + ("uint8", [0, 100, 1], int), + ], +) +def test_get_descriptor_ranged_property_type_coercion( + format, range_values, expected_type +): + """Test that range values are coerced to the property's format type.""" ranged_prop = load_fixture("ranged_property.json") - ranged_prop["format"] = "float" - ranged_prop["value-range"] = [-30, 100, 1e-05] + ranged_prop["format"] = format + ranged_prop["value-range"] = range_values prop = MiotProperty.model_validate(ranged_prop) desc = prop.get_descriptor() assert isinstance(desc, RangeDescriptor) - assert desc.min_value == -30 - assert desc.max_value == 100 - assert desc.step == 1e-05 + assert all(type(v) is expected_type for v in prop.range) + assert desc.min_value == range_values[0] + assert desc.max_value == range_values[1] + assert desc.step == range_values[2] def test_get_descriptor_ranged_property_none_format():