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 from datetime import datetime, timedelta
import functools as ft import functools as ft
import logging import logging
import math
import sys
from timeit import default_timer as timer from timeit import default_timer as timer
from typing import Any from typing import Any
@ -43,6 +45,10 @@ DATA_ENTITY_SOURCE = "entity_info"
SOURCE_CONFIG_ENTRY = "config_entry" SOURCE_CONFIG_ENTRY = "config_entry"
SOURCE_PLATFORM_CONFIG = "platform_config" 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 @callback
@bind_hass @bind_hass
@ -327,6 +333,19 @@ class Entity(ABC):
self._async_write_ha_state() 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 @callback
def _async_write_ha_state(self) -> None: def _async_write_ha_state(self) -> None:
"""Write the state to the state machine.""" """Write the state to the state machine."""
@ -346,11 +365,8 @@ class Entity(ABC):
attr = self.capability_attributes attr = self.capability_attributes
attr = dict(attr) if attr else {} attr = dict(attr) if attr else {}
if not self.available: state = self._stringify_state()
state = STATE_UNAVAILABLE if self.available:
else:
sstate = self.state
state = STATE_UNKNOWN if sstate is None else str(sstate)
attr.update(self.state_attributes or {}) attr.update(self.state_attributes or {})
extra_state_attributes = self.extra_state_attributes extra_state_attributes = self.extra_state_attributes
# Backwards compatibility for "device_state_attributes" deprecated in 2021.4 # 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 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): async def test_decrement(hass):
"""Test decrement method.""" """Test decrement method."""
assert await async_setup_component( 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.""" """Test get_supported_features raises on unknown entity_id."""
with pytest.raises(HomeAssistantError): with pytest.raises(HomeAssistantError):
entity.get_supported_features(hass, "hello.world") 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"