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:
J. Nick Koston 2023-07-02 20:53:50 -05:00 committed by GitHub
parent ab50069918
commit b24c6adc75
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 110 additions and 22 deletions

View File

@ -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

View File

@ -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"

View File

@ -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"})