Fix number rounding for protobuf messages (#93)

* Fix number rounding for protobuf messages

* Switch to converter_field
This commit is contained in:
Otto Winter 2021-08-24 01:39:18 +02:00 committed by GitHub
parent 4981b60f95
commit 738346c9cb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 81 additions and 9 deletions

View File

@ -14,6 +14,8 @@ from typing import (
cast,
)
from .util import fix_float_single_double_conversion
if TYPE_CHECKING:
from .api_pb2 import HomeassistantServiceMap # type: ignore
@ -407,9 +409,15 @@ class ClimateInfo(EntityInfo):
supported_modes: List[ClimateMode] = converter_field(
default_factory=list, converter=ClimateMode.convert_list
)
visual_min_temperature: float = 0.0
visual_max_temperature: float = 0.0
visual_temperature_step: float = 0.0
visual_min_temperature: float = converter_field(
default=0.0, converter=fix_float_single_double_conversion
)
visual_max_temperature: float = converter_field(
default=0.0, converter=fix_float_single_double_conversion
)
visual_temperature_step: float = converter_field(
default=0.0, converter=fix_float_single_double_conversion
)
legacy_supports_away: bool = False
supports_action: bool = False
supported_fan_modes: List[ClimateFanMode] = converter_field(
@ -446,10 +454,18 @@ class ClimateState(EntityState):
action: Optional[ClimateAction] = converter_field(
default=ClimateAction.OFF, converter=ClimateAction.convert
)
current_temperature: float = 0.0
target_temperature: float = 0.0
target_temperature_low: float = 0.0
target_temperature_high: float = 0.0
current_temperature: float = converter_field(
default=0.0, converter=fix_float_single_double_conversion
)
target_temperature: float = converter_field(
default=0.0, converter=fix_float_single_double_conversion
)
target_temperature_low: float = converter_field(
default=0.0, converter=fix_float_single_double_conversion
)
target_temperature_high: float = converter_field(
default=0.0, converter=fix_float_single_double_conversion
)
legacy_away: bool = False
fan_mode: Optional[ClimateFanMode] = converter_field(
default=ClimateFanMode.ON, converter=ClimateFanMode.convert
@ -475,12 +491,16 @@ class NumberInfo(EntityInfo):
icon: str = ""
min_value: float = 0.0
max_value: float = 0.0
step: float = 0.0
step: float = converter_field(
default=0.0, converter=fix_float_single_double_conversion
)
@dataclass(frozen=True)
class NumberState(EntityState):
state: float = 0.0
state: float = converter_field(
default=0.0, converter=fix_float_single_double_conversion
)
missing_state: bool = False

View File

@ -1,3 +1,4 @@
import math
from typing import Optional
@ -26,3 +27,26 @@ def bytes_to_varuint(value: bytes) -> Optional[int]:
if (val & 0x80) == 0:
return result
return None
def fix_float_single_double_conversion(value: float) -> float:
"""Fix precision for single-precision floats and return what was probably
meant as a float.
In ESPHome we work with single-precision floats internally for performance.
But python uses double-precision floats, and when protobuf reads the message
it's auto-converted to a double (which is possible losslessly).
Unfortunately the float representation of 0.1 converted to a double is not the
double representation of 0.1, but 0.10000000149011612.
This methods tries to round to the closest decimal value that a float of this
magnitude can accurately represent.
"""
if value == 0 or not math.isfinite(value):
return value
abs_val = abs(value)
# assume ~7 decimals of precision for floats to be safe
l10 = math.ceil(math.log10(abs_val))
prec = 7 - l10
return round(value, prec)

View File

@ -1,3 +1,5 @@
import math
import pytest
from aioesphomeapi import util
@ -20,3 +22,29 @@ def test_varuint_to_bytes(val, encoded):
@pytest.mark.parametrize("val, encoded", VARUINT_TESTCASES)
def test_bytes_to_varuint(val, encoded):
assert util.bytes_to_varuint(encoded) == val
@pytest.mark.parametrize(
"input, output",
[
(0, 0),
(float("inf"), float("inf")),
(float("-inf"), float("-inf")),
(0.1, 0.1),
(-0.0, -0.0),
(0.10000000149011612, 0.1),
(1, 1),
(-1, -1),
(-0.10000000149011612, -0.1),
(-152198557936981706463557226105667584, -152198600000000000000000000000000000),
(-0.0030539485160261, -0.003053949),
(0.5, 0.5),
(0.0000000000000019, 0.0000000000000019),
],
)
def test_fix_float_single_double_conversion(input, output):
assert util.fix_float_single_double_conversion(input) == output
def test_fix_float_single_double_conversion_nan():
assert math.isnan(util.fix_float_single_double_conversion(float("nan")))