mirror of
https://github.com/home-assistant/core.git
synced 2025-07-15 17:27:10 +00:00
Avoid regex for negative zero check in sensor (#95691)
* Avoid regex for negative zero check in sensor We can avoid calling the regex for every sensor value since most of the time values are not negative zero * tweak * tweak * Apply suggestions from code review * simpler * cover * safer and still fast * safer and still fast * prep for py3.11 * fix check * add missing cover * more coverage * coverage * coverage
This commit is contained in:
parent
ab50069918
commit
b24c6adc75
@ -10,6 +10,7 @@ from decimal import Decimal, InvalidOperation as DecimalInvalidOperation
|
||||
import logging
|
||||
from math import ceil, floor, log10
|
||||
import re
|
||||
import sys
|
||||
from typing import Any, Final, cast, final
|
||||
|
||||
from typing_extensions import Self
|
||||
@ -92,6 +93,8 @@ ENTITY_ID_FORMAT: Final = DOMAIN + ".{}"
|
||||
|
||||
NEGATIVE_ZERO_PATTERN = re.compile(r"^-(0\.?0*)$")
|
||||
|
||||
PY_311 = sys.version_info >= (3, 11, 0)
|
||||
|
||||
SCAN_INTERVAL: Final = timedelta(seconds=30)
|
||||
|
||||
__all__ = [
|
||||
@ -638,10 +641,12 @@ class SensorEntity(Entity):
|
||||
)
|
||||
precision = precision + floor(ratio_log)
|
||||
|
||||
value = f"{converted_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)
|
||||
if PY_311:
|
||||
value = f"{converted_numerical_value:z.{precision}f}"
|
||||
else:
|
||||
value = f"{converted_numerical_value:.{precision}f}"
|
||||
if value.startswith("-0") and NEGATIVE_ZERO_PATTERN.match(value):
|
||||
value = value[1:]
|
||||
else:
|
||||
value = converted_numerical_value
|
||||
|
||||
@ -883,29 +888,31 @@ def async_update_suggested_units(hass: HomeAssistant) -> None:
|
||||
)
|
||||
|
||||
|
||||
def _display_precision(hass: HomeAssistant, entity_id: str) -> 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")
|
||||
|
||||
|
||||
@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:
|
||||
if (precision := _display_precision(hass, entity_id)) 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)
|
||||
if PY_311:
|
||||
value = f"{numerical_value:z.{precision}f}"
|
||||
else:
|
||||
value = f"{numerical_value:.{precision}f}"
|
||||
if value.startswith("-0") and NEGATIVE_ZERO_PATTERN.match(value):
|
||||
value = value[1:]
|
||||
|
||||
return value
|
||||
|
@ -17,6 +17,7 @@ from homeassistant.components.sensor import (
|
||||
SensorEntity,
|
||||
SensorEntityDescription,
|
||||
SensorStateClass,
|
||||
async_rounded_state,
|
||||
async_update_suggested_units,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry, ConfigFlow
|
||||
@ -557,6 +558,22 @@ async def test_restore_sensor_restore_state(
|
||||
100,
|
||||
"38",
|
||||
),
|
||||
(
|
||||
SensorDeviceClass.ATMOSPHERIC_PRESSURE,
|
||||
UnitOfPressure.INHG,
|
||||
UnitOfPressure.HPA,
|
||||
UnitOfPressure.HPA,
|
||||
-0.00,
|
||||
"0.0",
|
||||
),
|
||||
(
|
||||
SensorDeviceClass.ATMOSPHERIC_PRESSURE,
|
||||
UnitOfPressure.INHG,
|
||||
UnitOfPressure.HPA,
|
||||
UnitOfPressure.HPA,
|
||||
-0.00001,
|
||||
"0",
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_custom_unit(
|
||||
@ -592,10 +609,15 @@ async def test_custom_unit(
|
||||
assert await async_setup_component(hass, "sensor", {"sensor": {"platform": "test"}})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get(entity0.entity_id)
|
||||
entity_id = entity0.entity_id
|
||||
state = hass.states.get(entity_id)
|
||||
assert state.state == custom_state
|
||||
assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == state_unit
|
||||
|
||||
assert (
|
||||
async_rounded_state(hass, entity_id, hass.states.get(entity_id)) == custom_state
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
(
|
||||
@ -902,7 +924,7 @@ async def test_custom_unit_change(
|
||||
"1000000",
|
||||
"1093613",
|
||||
SensorDeviceClass.DISTANCE,
|
||||
),
|
||||
)
|
||||
],
|
||||
)
|
||||
async def test_unit_conversion_priority(
|
||||
@ -1130,6 +1152,9 @@ async def test_unit_conversion_priority_precision(
|
||||
"sensor": {"suggested_display_precision": 2},
|
||||
"sensor.private": {"suggested_unit_of_measurement": automatic_unit},
|
||||
}
|
||||
assert float(async_rounded_state(hass, entity0.entity_id, state)) == pytest.approx(
|
||||
round(automatic_state, 2)
|
||||
)
|
||||
|
||||
# Unregistered entity -> Follow native unit
|
||||
state = hass.states.get(entity1.entity_id)
|
||||
@ -1172,6 +1197,20 @@ async def test_unit_conversion_priority_precision(
|
||||
assert float(state.state) == pytest.approx(custom_state)
|
||||
assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == custom_unit
|
||||
|
||||
# Set a display_precision, this should have priority over suggested_display_precision
|
||||
entity_registry.async_update_entity_options(
|
||||
entity0.entity_id,
|
||||
"sensor",
|
||||
{"suggested_display_precision": 2, "display_precision": 4},
|
||||
)
|
||||
entry0 = entity_registry.async_get(entity0.entity_id)
|
||||
assert entry0.options["sensor"]["suggested_display_precision"] == 2
|
||||
assert entry0.options["sensor"]["display_precision"] == 4
|
||||
await hass.async_block_till_done()
|
||||
assert float(async_rounded_state(hass, entity0.entity_id, state)) == pytest.approx(
|
||||
round(custom_state, 4)
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
(
|
||||
@ -2362,3 +2401,39 @@ async def test_name(hass: HomeAssistant) -> None:
|
||||
|
||||
state = hass.states.get(entity4.entity_id)
|
||||
assert state.attributes == {"device_class": "battery", "friendly_name": "Battery"}
|
||||
|
||||
|
||||
def test_async_rounded_state_unregistered_entity_is_passthrough(
|
||||
hass: HomeAssistant,
|
||||
) -> None:
|
||||
"""Test async_rounded_state on unregistered entity is passthrough."""
|
||||
hass.states.async_set("sensor.test", "1.004")
|
||||
state = hass.states.get("sensor.test")
|
||||
assert async_rounded_state(hass, "sensor.test", state) == "1.004"
|
||||
hass.states.async_set("sensor.test", "-0.0")
|
||||
state = hass.states.get("sensor.test")
|
||||
assert async_rounded_state(hass, "sensor.test", state) == "-0.0"
|
||||
|
||||
|
||||
def test_async_rounded_state_registered_entity_with_display_precision(
|
||||
hass: HomeAssistant,
|
||||
) -> None:
|
||||
"""Test async_rounded_state on registered with display precision.
|
||||
|
||||
The -0 should be dropped.
|
||||
"""
|
||||
entity_registry = er.async_get(hass)
|
||||
|
||||
entry = entity_registry.async_get_or_create("sensor", "test", "very_unique")
|
||||
entity_registry.async_update_entity_options(
|
||||
entry.entity_id,
|
||||
"sensor",
|
||||
{"suggested_display_precision": 2, "display_precision": 4},
|
||||
)
|
||||
entity_id = entry.entity_id
|
||||
hass.states.async_set(entity_id, "1.004")
|
||||
state = hass.states.get(entity_id)
|
||||
assert async_rounded_state(hass, entity_id, state) == "1.0040"
|
||||
hass.states.async_set(entity_id, "-0.0")
|
||||
state = hass.states.get(entity_id)
|
||||
assert async_rounded_state(hass, entity_id, state) == "0.0000"
|
||||
|
@ -3889,6 +3889,8 @@ def test_state_with_unit_and_rounding(hass: HomeAssistant) -> None:
|
||||
|
||||
hass.states.async_set("sensor.test", "23", {ATTR_UNIT_OF_MEASUREMENT: "beers"})
|
||||
hass.states.async_set("sensor.test2", "23", {ATTR_UNIT_OF_MEASUREMENT: "beers"})
|
||||
hass.states.async_set("sensor.test3", "-0.0", {ATTR_UNIT_OF_MEASUREMENT: "beers"})
|
||||
hass.states.async_set("sensor.test4", "-0", {ATTR_UNIT_OF_MEASUREMENT: "beers"})
|
||||
|
||||
# state_with_unit property
|
||||
tpl = template.Template("{{ states.sensor.test.state_with_unit }}", hass)
|
||||
@ -3905,6 +3907,8 @@ def test_state_with_unit_and_rounding(hass: HomeAssistant) -> None:
|
||||
# AllStates.__call__ and rounded=True
|
||||
tpl7 = template.Template("{{ states('sensor.test', rounded=True) }}", hass)
|
||||
tpl8 = template.Template("{{ states('sensor.test2', rounded=True) }}", hass)
|
||||
tpl9 = template.Template("{{ states('sensor.test3', rounded=True) }}", hass)
|
||||
tpl10 = template.Template("{{ states('sensor.test4', rounded=True) }}", hass)
|
||||
|
||||
assert tpl.async_render() == "23.00 beers"
|
||||
assert tpl2.async_render() == "23 beers"
|
||||
@ -3914,6 +3918,8 @@ def test_state_with_unit_and_rounding(hass: HomeAssistant) -> None:
|
||||
assert tpl6.async_render() == "23 beers"
|
||||
assert tpl7.async_render() == 23.0
|
||||
assert tpl8.async_render() == 23
|
||||
assert tpl9.async_render() == 0.0
|
||||
assert tpl10.async_render() == 0
|
||||
|
||||
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"})
|
||||
|
Loading…
x
Reference in New Issue
Block a user