diff --git a/homeassistant/components/sensor/__init__.py b/homeassistant/components/sensor/__init__.py index 89f39d4fb8c..e3ee566a855 100644 --- a/homeassistant/components/sensor/__init__.py +++ b/homeassistant/components/sensor/__init__.py @@ -675,22 +675,13 @@ class SensorEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): ): # Deduce the precision by finding the decimal point, if any value_s = str(value) - precision = ( - len(value_s) - value_s.index(".") - 1 if "." in value_s else 0 - ) - # Scale the precision when converting to a larger unit # For example 1.1 Wh should be rendered as 0.0011 kWh, not 0.0 kWh - ratio_log = max( - 0, - log10( - converter.get_unit_ratio( - native_unit_of_measurement, unit_of_measurement - ) - ), + precision = ( + len(value_s) - value_s.index(".") - 1 if "." in value_s else 0 + ) + converter.get_unit_floored_log_ratio( + native_unit_of_measurement, unit_of_measurement ) - precision = precision + floor(ratio_log) - value = f"{converted_numerical_value:z.{precision}f}" else: value = converted_numerical_value diff --git a/homeassistant/util/unit_conversion.py b/homeassistant/util/unit_conversion.py index 67258c9cd09..f2619c5dd61 100644 --- a/homeassistant/util/unit_conversion.py +++ b/homeassistant/util/unit_conversion.py @@ -4,6 +4,7 @@ from __future__ import annotations from collections.abc import Callable from functools import lru_cache +from math import floor, log10 from homeassistant.const import ( CONCENTRATION_PARTS_PER_BILLION, @@ -144,6 +145,15 @@ class BaseUnitConverter: from_ratio, to_ratio = cls._get_from_to_ratio(from_unit, to_unit) return from_ratio / to_ratio + @classmethod + @lru_cache + def get_unit_floored_log_ratio( + cls, from_unit: str | None, to_unit: str | None + ) -> float: + """Get floored base10 log ratio between units of measurement.""" + from_ratio, to_ratio = cls._get_from_to_ratio(from_unit, to_unit) + return floor(max(0, log10(from_ratio / to_ratio))) + @classmethod @lru_cache def _are_unit_inverses(cls, from_unit: str | None, to_unit: str | None) -> bool: diff --git a/tests/util/test_unit_conversion.py b/tests/util/test_unit_conversion.py index aeea4ad9a5a..3f55ceef242 100644 --- a/tests/util/test_unit_conversion.py +++ b/tests/util/test_unit_conversion.py @@ -902,8 +902,8 @@ def test_convert_nonnumeric_value( ("converter", "from_unit", "to_unit", "expected"), [ # Process all items in _GET_UNIT_RATIO - (converter, item[0], item[1], item[2]) - for converter, item in _GET_UNIT_RATIO.items() + (converter, from_unit, to_unit, expected) + for converter, (from_unit, to_unit, expected) in _GET_UNIT_RATIO.items() ], ) def test_get_unit_ratio( @@ -915,13 +915,34 @@ def test_get_unit_ratio( assert converter.get_unit_ratio(to_unit, from_unit) == pytest.approx(1 / ratio) +@pytest.mark.parametrize( + ("converter", "from_unit", "to_unit", "expected"), + [ + # Process all items in _GET_UNIT_RATIO + (converter, from_unit, to_unit, expected) + for converter, (from_unit, to_unit, expected) in _GET_UNIT_RATIO.items() + ], +) +def get_unit_floored_log_ratio( + converter: type[BaseUnitConverter], from_unit: str, to_unit: str, expected: float +) -> None: + """Test floored log unit ratio. + + Should not use pytest.approx since we are checking these + values are exact. + """ + ratio = converter.get_unit_floored_log_ratio(from_unit, to_unit) + assert ratio == expected + assert converter.get_unit_floored_log_ratio(to_unit, from_unit) == 1 / ratio + + @pytest.mark.parametrize( ("converter", "value", "from_unit", "expected", "to_unit"), [ # Process all items in _CONVERTED_VALUE - (converter, list_item[0], list_item[1], list_item[2], list_item[3]) + (converter, value, from_unit, expected, to_unit) for converter, item in _CONVERTED_VALUE.items() - for list_item in item + for value, from_unit, expected, to_unit in item ], ) def test_unit_conversion(