mirror of
https://github.com/home-assistant/core.git
synced 2025-07-16 09:47:13 +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
|
import logging
|
||||||
from math import ceil, floor, log10
|
from math import ceil, floor, log10
|
||||||
import re
|
import re
|
||||||
|
import sys
|
||||||
from typing import Any, Final, cast, final
|
from typing import Any, Final, cast, final
|
||||||
|
|
||||||
from typing_extensions import Self
|
from typing_extensions import Self
|
||||||
@ -92,6 +93,8 @@ ENTITY_ID_FORMAT: Final = DOMAIN + ".{}"
|
|||||||
|
|
||||||
NEGATIVE_ZERO_PATTERN = re.compile(r"^-(0\.?0*)$")
|
NEGATIVE_ZERO_PATTERN = re.compile(r"^-(0\.?0*)$")
|
||||||
|
|
||||||
|
PY_311 = sys.version_info >= (3, 11, 0)
|
||||||
|
|
||||||
SCAN_INTERVAL: Final = timedelta(seconds=30)
|
SCAN_INTERVAL: Final = timedelta(seconds=30)
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
@ -638,10 +641,12 @@ class SensorEntity(Entity):
|
|||||||
)
|
)
|
||||||
precision = precision + floor(ratio_log)
|
precision = precision + floor(ratio_log)
|
||||||
|
|
||||||
value = f"{converted_numerical_value:.{precision}f}"
|
if PY_311:
|
||||||
# This can be replaced with adding the z option when we drop support for
|
value = f"{converted_numerical_value:z.{precision}f}"
|
||||||
# Python 3.10
|
else:
|
||||||
value = NEGATIVE_ZERO_PATTERN.sub(r"\1", value)
|
value = f"{converted_numerical_value:.{precision}f}"
|
||||||
|
if value.startswith("-0") and NEGATIVE_ZERO_PATTERN.match(value):
|
||||||
|
value = value[1:]
|
||||||
else:
|
else:
|
||||||
value = converted_numerical_value
|
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
|
@callback
|
||||||
def async_rounded_state(hass: HomeAssistant, entity_id: str, state: State) -> str:
|
def async_rounded_state(hass: HomeAssistant, entity_id: str, state: State) -> str:
|
||||||
"""Return the state rounded for presentation."""
|
"""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
|
value = state.state
|
||||||
if (precision := display_precision()) is None:
|
if (precision := _display_precision(hass, entity_id)) is None:
|
||||||
return value
|
return value
|
||||||
|
|
||||||
with suppress(TypeError, ValueError):
|
with suppress(TypeError, ValueError):
|
||||||
numerical_value = float(value)
|
numerical_value = float(value)
|
||||||
value = f"{numerical_value:.{precision}f}"
|
if PY_311:
|
||||||
# This can be replaced with adding the z option when we drop support for
|
value = f"{numerical_value:z.{precision}f}"
|
||||||
# Python 3.10
|
else:
|
||||||
value = NEGATIVE_ZERO_PATTERN.sub(r"\1", value)
|
value = f"{numerical_value:.{precision}f}"
|
||||||
|
if value.startswith("-0") and NEGATIVE_ZERO_PATTERN.match(value):
|
||||||
|
value = value[1:]
|
||||||
|
|
||||||
return value
|
return value
|
||||||
|
@ -17,6 +17,7 @@ from homeassistant.components.sensor import (
|
|||||||
SensorEntity,
|
SensorEntity,
|
||||||
SensorEntityDescription,
|
SensorEntityDescription,
|
||||||
SensorStateClass,
|
SensorStateClass,
|
||||||
|
async_rounded_state,
|
||||||
async_update_suggested_units,
|
async_update_suggested_units,
|
||||||
)
|
)
|
||||||
from homeassistant.config_entries import ConfigEntry, ConfigFlow
|
from homeassistant.config_entries import ConfigEntry, ConfigFlow
|
||||||
@ -557,6 +558,22 @@ async def test_restore_sensor_restore_state(
|
|||||||
100,
|
100,
|
||||||
"38",
|
"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(
|
async def test_custom_unit(
|
||||||
@ -592,10 +609,15 @@ async def test_custom_unit(
|
|||||||
assert await async_setup_component(hass, "sensor", {"sensor": {"platform": "test"}})
|
assert await async_setup_component(hass, "sensor", {"sensor": {"platform": "test"}})
|
||||||
await hass.async_block_till_done()
|
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.state == custom_state
|
||||||
assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == state_unit
|
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(
|
@pytest.mark.parametrize(
|
||||||
(
|
(
|
||||||
@ -902,7 +924,7 @@ async def test_custom_unit_change(
|
|||||||
"1000000",
|
"1000000",
|
||||||
"1093613",
|
"1093613",
|
||||||
SensorDeviceClass.DISTANCE,
|
SensorDeviceClass.DISTANCE,
|
||||||
),
|
)
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
async def test_unit_conversion_priority(
|
async def test_unit_conversion_priority(
|
||||||
@ -1130,6 +1152,9 @@ async def test_unit_conversion_priority_precision(
|
|||||||
"sensor": {"suggested_display_precision": 2},
|
"sensor": {"suggested_display_precision": 2},
|
||||||
"sensor.private": {"suggested_unit_of_measurement": automatic_unit},
|
"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
|
# Unregistered entity -> Follow native unit
|
||||||
state = hass.states.get(entity1.entity_id)
|
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 float(state.state) == pytest.approx(custom_state)
|
||||||
assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == custom_unit
|
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(
|
@pytest.mark.parametrize(
|
||||||
(
|
(
|
||||||
@ -2362,3 +2401,39 @@ async def test_name(hass: HomeAssistant) -> None:
|
|||||||
|
|
||||||
state = hass.states.get(entity4.entity_id)
|
state = hass.states.get(entity4.entity_id)
|
||||||
assert state.attributes == {"device_class": "battery", "friendly_name": "Battery"}
|
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.test", "23", {ATTR_UNIT_OF_MEASUREMENT: "beers"})
|
||||||
hass.states.async_set("sensor.test2", "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
|
# state_with_unit property
|
||||||
tpl = template.Template("{{ states.sensor.test.state_with_unit }}", hass)
|
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
|
# AllStates.__call__ and rounded=True
|
||||||
tpl7 = template.Template("{{ states('sensor.test', rounded=True) }}", hass)
|
tpl7 = template.Template("{{ states('sensor.test', rounded=True) }}", hass)
|
||||||
tpl8 = template.Template("{{ states('sensor.test2', 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 tpl.async_render() == "23.00 beers"
|
||||||
assert tpl2.async_render() == "23 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 tpl6.async_render() == "23 beers"
|
||||||
assert tpl7.async_render() == 23.0
|
assert tpl7.async_render() == 23.0
|
||||||
assert tpl8.async_render() == 23
|
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.test", "23.015", {ATTR_UNIT_OF_MEASUREMENT: "beers"})
|
||||||
hass.states.async_set("sensor.test2", "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