Limit precision when stringifying float states (#48822)

* Limit precision when stringifying float states

* Add test

* Fix typing

* Move StateType

* Update

* Move conversion to entity helper

* Address review comments

* Tweak precision

* Tweak

* Make _stringify_state an instance method
This commit is contained in:
Erik Montnemery 2021-04-27 21:48:24 +02:00 committed by GitHub
parent 5e00fdccfd
commit d2fd504442
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 55 additions and 5 deletions

View File

@ -7,6 +7,8 @@ from collections.abc import Awaitable, Iterable, Mapping
from datetime import datetime, timedelta
import functools as ft
import logging
import math
import sys
from timeit import default_timer as timer
from typing import Any
@ -43,6 +45,10 @@ DATA_ENTITY_SOURCE = "entity_info"
SOURCE_CONFIG_ENTRY = "config_entry"
SOURCE_PLATFORM_CONFIG = "platform_config"
# Used when converting float states to string: limit precision according to machine
# epsilon to make the string representation readable
FLOAT_PRECISION = abs(int(math.floor(math.log10(abs(sys.float_info.epsilon))))) - 1
@callback
@bind_hass
@ -327,6 +333,19 @@ class Entity(ABC):
self._async_write_ha_state()
def _stringify_state(self) -> str:
"""Convert state to string."""
if not self.available:
return STATE_UNAVAILABLE
state = self.state
if state is None:
return STATE_UNKNOWN
if isinstance(state, float):
# If the entity's state is a float, limit precision according to machine
# epsilon to make the string representation readable
return f"{state:.{FLOAT_PRECISION}}"
return str(state)
@callback
def _async_write_ha_state(self) -> None:
"""Write the state to the state machine."""
@ -346,11 +365,8 @@ class Entity(ABC):
attr = self.capability_attributes
attr = dict(attr) if attr else {}
if not self.available:
state = STATE_UNAVAILABLE
else:
sstate = self.state
state = STATE_UNKNOWN if sstate is None else str(sstate)
state = self._stringify_state()
if self.available:
attr.update(self.state_attributes or {})
extra_state_attributes = self.extra_state_attributes
# Backwards compatibility for "device_state_attributes" deprecated in 2021.4

View File

@ -162,6 +162,26 @@ async def test_increment(hass):
assert float(state.state) == 51
async def test_rounding(hass):
"""Test increment introducing floating point error is rounded."""
assert await async_setup_component(
hass,
DOMAIN,
{DOMAIN: {"test_2": {"initial": 2.4, "min": 0, "max": 51, "step": 1.2}}},
)
entity_id = "input_number.test_2"
assert 2.4 + 1.2 != 3.6
state = hass.states.get(entity_id)
assert float(state.state) == 2.4
await increment(hass, entity_id)
await hass.async_block_till_done()
state = hass.states.get(entity_id)
assert float(state.state) == 3.6
async def test_decrement(hass):
"""Test decrement method."""
assert await async_setup_component(

View File

@ -774,3 +774,17 @@ async def test_get_supported_features_raises_on_unknown(hass):
"""Test get_supported_features raises on unknown entity_id."""
with pytest.raises(HomeAssistantError):
entity.get_supported_features(hass, "hello.world")
async def test_float_conversion(hass):
"""Test conversion of float state to string rounds."""
assert 2.4 + 1.2 != 3.6
with patch.object(entity.Entity, "state", PropertyMock(return_value=2.4 + 1.2)):
ent = entity.Entity()
ent.hass = hass
ent.entity_id = "hello.world"
ent.async_write_ha_state()
state = hass.states.get("hello.world")
assert state is not None
assert state.state == "3.6"