Round value in state_with_unit template function (#87619)

This commit is contained in:
Erik Montnemery 2023-02-09 12:54:59 +01:00 committed by GitHub
parent 773446946e
commit 2d2ff19949
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 171 additions and 5 deletions

View File

@ -3,6 +3,7 @@ from __future__ import annotations
import asyncio
from collections.abc import Mapping
from contextlib import suppress
from dataclasses import dataclass
from datetime import date, datetime, timedelta, timezone
from decimal import Decimal, InvalidOperation as DecimalInvalidOperation
@ -17,6 +18,7 @@ from homeassistant.config_entries import ConfigEntry
# pylint: disable=[hass-deprecated-import]
from homeassistant.const import ( # noqa: F401
ATTR_UNIT_OF_MEASUREMENT,
CONF_UNIT_OF_MEASUREMENT,
DEVICE_CLASS_AQI,
DEVICE_CLASS_BATTERY,
@ -48,7 +50,7 @@ from homeassistant.const import ( # noqa: F401
DEVICE_CLASS_VOLTAGE,
UnitOfTemperature,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.core import HomeAssistant, State, callback
from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.config_validation import (
PLATFORM_SCHEMA,
@ -828,3 +830,31 @@ def async_update_suggested_units(hass: HomeAssistant) -> None:
f"{DOMAIN}.private",
sensor_private_options,
)
@callback
def async_rounded_state(hass: HomeAssistant, entity_id: str, state: State) -> str:
"""Return the state rounded for presentation."""
def display_precision() -> int | None:
"""Return the display precision."""
if not (entry := er.async_get(hass).async_get(entity_id)) or not (
sensor_options := entry.options.get(DOMAIN)
):
return None
if (display_precision := sensor_options.get("display_precision")) is not None:
return cast(int, display_precision)
return sensor_options.get("suggested_display_precision")
value = state.state
if (precision := display_precision()) is None:
return value
with suppress(TypeError, ValueError):
numerical_value = float(value)
value = f"{numerical_value:.{precision}f}"
# This can be replaced with adding the z option when we drop support for
# Python 3.10
value = NEGATIVE_ZERO_PATTERN.sub(r"\1", value)
return value

View File

@ -733,10 +733,21 @@ class AllStates:
self._collect_all_lifecycle()
return self._hass.states.async_entity_ids_count()
def __call__(self, entity_id: str) -> str:
def __call__(
self,
entity_id: str,
rounded: bool | object = _SENTINEL,
with_unit: bool = False,
) -> str:
"""Return the states."""
state = _get_state(self._hass, entity_id)
return STATE_UNKNOWN if state is None else state.state
if state is None:
return STATE_UNKNOWN
if rounded is _SENTINEL:
rounded = with_unit
if rounded or with_unit:
return state.format_state(rounded, with_unit) # type: ignore[arg-type]
return state.state
def __repr__(self) -> str:
"""Representation of All States."""
@ -792,7 +803,7 @@ class DomainStates:
class TemplateStateBase(State):
"""Class to represent a state object in a template."""
__slots__ = ("_hass", "_collect", "_entity_id")
__slots__ = ("_hass", "_collect", "_entity_id", "__dict__")
_state: State
@ -886,9 +897,24 @@ class TemplateStateBase(State):
@property
def state_with_unit(self) -> str:
"""Return the state concatenated with the unit if available."""
return self.format_state(rounded=True, with_unit=True)
def format_state(self, rounded: bool, with_unit: bool) -> str:
"""Return a formatted version of the state."""
# Import here, not at top-level, to avoid circular import
# pylint: disable-next=import-outside-toplevel
from homeassistant.components.sensor import (
DOMAIN as SENSOR_DOMAIN,
async_rounded_state,
)
self._collect_state()
unit = self._state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
return f"{self._state.state} {unit}" if unit else self._state.state
if rounded and split_entity_id(self._entity_id)[0] == SENSOR_DOMAIN:
state = async_rounded_state(self._hass, self._entity_id, self._state)
else:
state = self._state.state
return f"{state} {unit}" if with_unit and unit else state
def __eq__(self, other: Any) -> bool:
"""Ensure we collect on equality check."""

View File

@ -3662,6 +3662,116 @@ def test_state_with_unit(hass: HomeAssistant) -> None:
assert tpl.async_render() == ""
def test_state_with_unit_and_rounding(hass: HomeAssistant) -> None:
"""Test formatting the state rounded and with unit."""
entity_registry = er.async_get(hass)
entry = entity_registry.async_get_or_create(
"sensor", "test", "very_unique", suggested_object_id="test"
)
entity_registry.async_update_entity_options(
entry.entity_id,
"sensor",
{
"suggested_display_precision": 2,
},
)
assert entry.entity_id == "sensor.test"
hass.states.async_set("sensor.test", "23", {ATTR_UNIT_OF_MEASUREMENT: "beers"})
hass.states.async_set("sensor.test2", "23", {ATTR_UNIT_OF_MEASUREMENT: "beers"})
# state_with_unit property
tpl = template.Template("{{ states.sensor.test.state_with_unit }}", hass)
tpl2 = template.Template("{{ states.sensor.test2.state_with_unit }}", hass)
# AllStates.__call__ defaults
tpl3 = template.Template("{{ states('sensor.test') }}", hass)
tpl4 = template.Template("{{ states('sensor.test2') }}", hass)
# AllStates.__call__ and with_unit=True
tpl5 = template.Template("{{ states('sensor.test', with_unit=True) }}", hass)
tpl6 = template.Template("{{ states('sensor.test2', with_unit=True) }}", hass)
# AllStates.__call__ and rounded=True
tpl7 = template.Template("{{ states('sensor.test', rounded=True) }}", hass)
tpl8 = template.Template("{{ states('sensor.test2', rounded=True) }}", hass)
assert tpl.async_render() == "23.00 beers"
assert tpl2.async_render() == "23 beers"
assert tpl3.async_render() == 23
assert tpl4.async_render() == 23
assert tpl5.async_render() == "23.00 beers"
assert tpl6.async_render() == "23 beers"
assert tpl7.async_render() == 23.0
assert tpl8.async_render() == 23
hass.states.async_set("sensor.test", "23.015", {ATTR_UNIT_OF_MEASUREMENT: "beers"})
hass.states.async_set("sensor.test2", "23.015", {ATTR_UNIT_OF_MEASUREMENT: "beers"})
assert tpl.async_render() == "23.02 beers"
assert tpl2.async_render() == "23.015 beers"
assert tpl3.async_render() == 23.015
assert tpl4.async_render() == 23.015
assert tpl5.async_render() == "23.02 beers"
assert tpl6.async_render() == "23.015 beers"
assert tpl7.async_render() == 23.02
assert tpl8.async_render() == 23.015
@pytest.mark.parametrize(
("rounded", "with_unit", "output1_1", "output1_2", "output2_1", "output2_2"),
[
(False, False, 23, 23.015, 23, 23.015),
(False, True, "23 beers", "23.015 beers", "23 beers", "23.015 beers"),
(True, False, 23.0, 23.02, 23, 23.015),
(True, True, "23.00 beers", "23.02 beers", "23 beers", "23.015 beers"),
],
)
def test_state_with_unit_and_rounding_options(
hass: HomeAssistant,
rounded: str,
with_unit: str,
output1_1,
output1_2,
output2_1,
output2_2,
) -> None:
"""Test formatting the state rounded and with unit."""
entity_registry = er.async_get(hass)
entry = entity_registry.async_get_or_create(
"sensor", "test", "very_unique", suggested_object_id="test"
)
entity_registry.async_update_entity_options(
entry.entity_id,
"sensor",
{
"suggested_display_precision": 2,
},
)
assert entry.entity_id == "sensor.test"
hass.states.async_set("sensor.test", "23", {ATTR_UNIT_OF_MEASUREMENT: "beers"})
hass.states.async_set("sensor.test2", "23", {ATTR_UNIT_OF_MEASUREMENT: "beers"})
tpl = template.Template(
f"{{{{ states('sensor.test', rounded={rounded}, with_unit={with_unit}) }}}}",
hass,
)
tpl2 = template.Template(
f"{{{{ states('sensor.test2', rounded={rounded}, with_unit={with_unit}) }}}}",
hass,
)
assert tpl.async_render() == output1_1
assert tpl2.async_render() == output2_1
hass.states.async_set("sensor.test", "23.015", {ATTR_UNIT_OF_MEASUREMENT: "beers"})
hass.states.async_set("sensor.test2", "23.015", {ATTR_UNIT_OF_MEASUREMENT: "beers"})
assert tpl.async_render() == output1_2
assert tpl2.async_render() == output2_2
def test_length_of_states(hass: HomeAssistant) -> None:
"""Test fetching the length of states."""
hass.states.async_set("sensor.test", "23")