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..ba567c676 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") @@ -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 30306adc6..400eb5bad 100644 --- a/miio/tests/test_miot_models.py +++ b/miio/tests/test_miot_models.py @@ -391,6 +391,31 @@ def test_get_descriptor_ranged_property(read_only, expected): assert desc.constraint == PropertyConstraint.Range +@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"] = format + ranged_prop["value-range"] = range_values + + prop = MiotProperty.model_validate(ranged_prop) + desc = prop.get_descriptor() + + assert isinstance(desc, RangeDescriptor) + 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(): """Test that a ranged property with format=none raises ValueError.""" ranged_prop = load_fixture("ranged_property.json")