Move sensor rounding to frontend (#87330)

* Move sensor rounding to frontend

* Update integrations

* Add comment
This commit is contained in:
Erik Montnemery 2023-02-04 20:54:36 +01:00 committed by GitHub
parent ee6773236e
commit bcc1aa03b4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 231 additions and 307 deletions

View File

@ -249,7 +249,7 @@ SENSOR_TYPES: tuple[AccuWeatherSensorDescription, ...] = (
metric_unit=UnitOfLength.METERS,
us_customary_unit=UnitOfLength.FEET,
value_fn=lambda data, unit: cast(float, data[unit][ATTR_VALUE]),
native_precision=0,
suggested_display_precision=0,
),
AccuWeatherSensorDescription(
key="CloudCover",

View File

@ -69,8 +69,8 @@ SENSOR_TYPES: tuple[AirlySensorEntityDescription, ...] = (
key=ATTR_API_CAQI,
icon="mdi:air-filter",
name=ATTR_API_CAQI,
native_precision=0,
native_unit_of_measurement="CAQI",
suggested_display_precision=0,
attrs=lambda data: {
ATTR_LEVEL: data[ATTR_API_CAQI_LEVEL],
ATTR_ADVICE: data[ATTR_API_ADVICE],
@ -81,17 +81,17 @@ SENSOR_TYPES: tuple[AirlySensorEntityDescription, ...] = (
key=ATTR_API_PM1,
device_class=SensorDeviceClass.PM1,
name="PM1.0",
native_precision=0,
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=0,
),
AirlySensorEntityDescription(
key=ATTR_API_PM25,
device_class=SensorDeviceClass.PM25,
name="PM2.5",
native_precision=0,
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=0,
attrs=lambda data: {
ATTR_LIMIT: data[f"{ATTR_API_PM25}_{SUFFIX_LIMIT}"],
ATTR_PERCENT: round(data[f"{ATTR_API_PM25}_{SUFFIX_PERCENT}"]),
@ -101,9 +101,9 @@ SENSOR_TYPES: tuple[AirlySensorEntityDescription, ...] = (
key=ATTR_API_PM10,
device_class=SensorDeviceClass.PM10,
name=ATTR_API_PM10,
native_precision=0,
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=0,
attrs=lambda data: {
ATTR_LIMIT: data[f"{ATTR_API_PM10}_{SUFFIX_LIMIT}"],
ATTR_PERCENT: round(data[f"{ATTR_API_PM10}_{SUFFIX_PERCENT}"]),
@ -113,32 +113,32 @@ SENSOR_TYPES: tuple[AirlySensorEntityDescription, ...] = (
key=ATTR_API_HUMIDITY,
device_class=SensorDeviceClass.HUMIDITY,
name=ATTR_API_HUMIDITY.capitalize(),
native_precision=1,
native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=1,
),
AirlySensorEntityDescription(
key=ATTR_API_PRESSURE,
device_class=SensorDeviceClass.PRESSURE,
name=ATTR_API_PRESSURE.capitalize(),
native_precision=0,
native_unit_of_measurement=UnitOfPressure.HPA,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=0,
),
AirlySensorEntityDescription(
key=ATTR_API_TEMPERATURE,
device_class=SensorDeviceClass.TEMPERATURE,
name=ATTR_API_TEMPERATURE.capitalize(),
native_precision=1,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=1,
),
AirlySensorEntityDescription(
key=ATTR_API_CO,
name="Carbon monoxide",
native_precision=0,
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=0,
attrs=lambda data: {
ATTR_LIMIT: data[f"{ATTR_API_CO}_{SUFFIX_LIMIT}"],
ATTR_PERCENT: round(data[f"{ATTR_API_CO}_{SUFFIX_PERCENT}"]),
@ -148,9 +148,9 @@ SENSOR_TYPES: tuple[AirlySensorEntityDescription, ...] = (
key=ATTR_API_NO2,
device_class=SensorDeviceClass.NITROGEN_DIOXIDE,
name="Nitrogen dioxide",
native_precision=0,
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=0,
attrs=lambda data: {
ATTR_LIMIT: data[f"{ATTR_API_NO2}_{SUFFIX_LIMIT}"],
ATTR_PERCENT: round(data[f"{ATTR_API_NO2}_{SUFFIX_PERCENT}"]),
@ -160,9 +160,9 @@ SENSOR_TYPES: tuple[AirlySensorEntityDescription, ...] = (
key=ATTR_API_SO2,
device_class=SensorDeviceClass.SULPHUR_DIOXIDE,
name="Sulphur dioxide",
native_precision=0,
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=0,
attrs=lambda data: {
ATTR_LIMIT: data[f"{ATTR_API_SO2}_{SUFFIX_LIMIT}"],
ATTR_PERCENT: round(data[f"{ATTR_API_SO2}_{SUFFIX_PERCENT}"]),
@ -172,9 +172,9 @@ SENSOR_TYPES: tuple[AirlySensorEntityDescription, ...] = (
key=ATTR_API_O3,
device_class=SensorDeviceClass.OZONE,
name="Ozone",
native_precision=0,
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=0,
attrs=lambda data: {
ATTR_LIMIT: data[f"{ATTR_API_O3}_{SUFFIX_LIMIT}"],
ATTR_PERCENT: round(data[f"{ATTR_API_O3}_{SUFFIX_PERCENT}"]),

View File

@ -7,7 +7,7 @@ from dataclasses import dataclass
from datetime import date, datetime, timedelta, timezone
from decimal import Decimal, InvalidOperation as DecimalInvalidOperation
import logging
from math import ceil, floor, log10
from math import floor, log10
import re
from typing import Any, Final, cast, final
@ -138,10 +138,10 @@ class SensorEntityDescription(EntityDescription):
device_class: SensorDeviceClass | None = None
last_reset: datetime | None = None
native_precision: int | None = None
native_unit_of_measurement: str | None = None
options: list[str] | None = None
state_class: SensorStateClass | str | None = None
suggested_display_precision: int | None = None
suggested_unit_of_measurement: str | None = None
unit_of_measurement: None = None # Type override, use native_unit_of_measurement
@ -152,12 +152,12 @@ class SensorEntity(Entity):
entity_description: SensorEntityDescription
_attr_device_class: SensorDeviceClass | None
_attr_last_reset: datetime | None
_attr_native_precision: int | None
_attr_native_unit_of_measurement: str | None
_attr_native_value: StateType | date | datetime | Decimal = None
_attr_options: list[str] | None
_attr_state_class: SensorStateClass | str | None
_attr_state: None = None # Subclasses of SensorEntity should not set this
_attr_suggested_display_precision: int | None
_attr_suggested_unit_of_measurement: str | None
_attr_unit_of_measurement: None = (
None # Subclasses of SensorEntity should not set this
@ -166,7 +166,7 @@ class SensorEntity(Entity):
_invalid_state_class_reported = False
_invalid_unit_of_measurement_reported = False
_last_reset_reported = False
_sensor_option_precision: int | None = None
_sensor_option_display_precision: int | None = None
_sensor_option_unit_of_measurement: str | None | UndefinedType = UNDEFINED
@callback
@ -236,7 +236,8 @@ class SensorEntity(Entity):
await super().async_internal_added_to_hass()
if not self.registry_entry:
return
self.async_registry_entry_updated()
self._async_read_entity_options()
self._update_suggested_precision()
@property
def device_class(self) -> SensorDeviceClass | None:
@ -254,7 +255,7 @@ class SensorEntity(Entity):
if (
self.state_class is not None
or self.native_unit_of_measurement is not None
or self.native_precision is not None
or self.suggested_display_precision is not None
):
return True
# Sensors with custom device classes are not considered numeric
@ -359,59 +360,14 @@ class SensorEntity(Entity):
return self._attr_native_value
@property
def native_precision(self) -> int | None:
"""Return the number of digits after the decimal point for the sensor's state.
If native_precision is None, no rounding is done unless the sensor is subject
to unit conversion.
The display precision is influenced by unit conversion, a sensor which has
native_unit_of_measurement 'Wh' and is converted to 'kWh' will have its
native_precision increased by 3.
"""
if hasattr(self, "_attr_native_precision"):
return self._attr_native_precision
def suggested_display_precision(self) -> int | None:
"""Return the suggested number of decimal digits for display."""
if hasattr(self, "_attr_suggested_display_precision"):
return self._attr_suggested_display_precision
if hasattr(self, "entity_description"):
return self.entity_description.native_precision
return self.entity_description.suggested_display_precision
return None
@final
@property
def precision(self) -> int | None:
"""Return the number of digits after the decimal point for the sensor's state.
This is the precision after unit conversion.
"""
# Highest priority, for registered entities: precision set by user
if self._sensor_option_precision is not None:
return self._sensor_option_precision
# Second priority, native precision
if (precision := self.native_precision) is None:
return None
device_class = self.device_class
native_unit_of_measurement = self.native_unit_of_measurement
unit_of_measurement = self.unit_of_measurement
if (
native_unit_of_measurement != unit_of_measurement
and device_class in UNIT_CONVERTERS
):
converter = UNIT_CONVERTERS[device_class]
# Scale the precision when converting to a larger or smaller unit
# For example 1.1 Wh should be rendered as 0.0011 kWh, not 0.0 kWh
ratio_log = log10(
converter.get_unit_ratio(
native_unit_of_measurement, unit_of_measurement
)
)
ratio_log = floor(ratio_log) if ratio_log > 0 else ceil(ratio_log)
precision = max(0, precision + ratio_log)
return precision
@property
def native_unit_of_measurement(self) -> str | None:
"""Return the unit of measurement of the sensor, if any."""
@ -576,7 +532,7 @@ class SensorEntity(Entity):
)
return value
precision = self.precision
suggested_precision = self.suggested_display_precision
# If the sensor has neither a device class, a state class, a unit of measurement
# nor a precision then there are no further checks or conversions
@ -593,13 +549,13 @@ class SensorEntity(Entity):
numerical_value = float(value) # type:ignore[arg-type]
except (TypeError, ValueError) as err:
# Raise if precision is not None, for other cases log a warning
if precision is not None:
if suggested_precision is not None:
raise ValueError(
f"Sensor {self.entity_id} has device class {device_class}, "
f"state class {state_class} unit {unit_of_measurement} and "
f"precision {precision} thus indicating it has a numeric value;"
f" however, it has the non-numeric value: {value} "
f"({type(value)})"
f"suggested precision {suggested_precision} thus indicating it "
f"has a numeric value; however, it has the non-numeric value: "
f"{value} ({type(value)})"
) from err
# This should raise in Home Assistant Core 2023.4
if not self._invalid_numeric_value_reported:
@ -629,7 +585,18 @@ class SensorEntity(Entity):
# Unit conversion needed
converter = UNIT_CONVERTERS[device_class]
if precision is None:
converted_numerical_value = UNIT_CONVERTERS[device_class].convert(
float(numerical_value),
native_unit_of_measurement,
unit_of_measurement,
)
# If unit conversion is happening, and there's no rounding for display,
# do a best effort rounding here.
if (
suggested_precision is None
and self._sensor_option_display_precision is None
):
# Deduce the precision by finding the decimal point, if any
value_s = str(value)
precision = (
@ -648,20 +615,10 @@ class SensorEntity(Entity):
)
precision = precision + floor(ratio_log)
converted_numerical_value = converter.convert(
float(numerical_value),
native_unit_of_measurement,
unit_of_measurement,
)
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)
elif precision is not None:
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)
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)
# Validate unit of measurement used for sensors with a device class
if (
@ -703,15 +660,35 @@ class SensorEntity(Entity):
return super().__repr__()
def _custom_precision_or_none(self) -> int | None:
"""Return a custom precisions or None if not set."""
def _suggested_precision_or_none(self) -> int | None:
"""Return suggested display precision, or None if not set."""
assert self.registry_entry
if (sensor_options := self.registry_entry.options.get(DOMAIN)) and (
precision := sensor_options.get("precision")
precision := sensor_options.get("suggested_display_precision")
) is not None:
return int(precision)
return cast(int, precision)
return None
def _update_suggested_precision(self) -> None:
"""Update suggested display precision stored in registry."""
assert self.registry_entry
display_precision = self.suggested_display_precision
if (
sensor_options := self.registry_entry.options.get(DOMAIN, {})
) and sensor_options.get("suggested_display_precision") == display_precision:
return
registry = er.async_get(self.hass)
sensor_options = dict(sensor_options)
sensor_options.pop("suggested_display_precision", None)
if display_precision is not None:
sensor_options["suggested_display_precision"] = display_precision
registry.async_update_entity_options(
self.entity_id, DOMAIN, sensor_options or None
)
def _custom_unit_or_undef(
self, primary_key: str, secondary_key: str
) -> str | None | UndefinedType:
@ -732,7 +709,16 @@ class SensorEntity(Entity):
@callback
def async_registry_entry_updated(self) -> None:
"""Run when the entity registry entry has been updated."""
self._sensor_option_precision = self._custom_precision_or_none()
self._async_read_entity_options()
@callback
def _async_read_entity_options(self) -> None:
"""Read entity options from entity registry.
Called when the entity registry entry has been updated and before the sensor is
added to the state machine.
"""
self._sensor_option_display_precision = self._suggested_precision_or_none()
assert self.registry_entry
if (
sensor_options := self.registry_entry.options.get(f"{DOMAIN}.private")

View File

@ -44,7 +44,7 @@ async def test_sensor_without_forecast(hass):
state = hass.states.get("sensor.home_cloud_ceiling")
assert state
assert state.state == "3200"
assert state.state == "3200.0"
assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION
assert state.attributes.get(ATTR_ICON) == "mdi:weather-fog"
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfLength.METERS
@ -54,6 +54,7 @@ async def test_sensor_without_forecast(hass):
entry = registry.async_get("sensor.home_cloud_ceiling")
assert entry
assert entry.unique_id == "0123456-ceiling"
assert entry.options["sensor"] == {"suggested_display_precision": 0}
state = hass.states.get("sensor.home_precipitation")
assert state
@ -665,7 +666,7 @@ async def test_availability(hass):
state = hass.states.get("sensor.home_cloud_ceiling")
assert state
assert state.state != STATE_UNAVAILABLE
assert state.state == "3200"
assert state.state == "3200.0"
future = utcnow() + timedelta(minutes=60)
with patch(
@ -696,7 +697,7 @@ async def test_availability(hass):
state = hass.states.get("sensor.home_cloud_ceiling")
assert state
assert state.state != STATE_UNAVAILABLE
assert state.state == "3200"
assert state.state == "3200.0"
async def test_manual_update_entity(hass):
@ -736,7 +737,7 @@ async def test_sensor_imperial_units(hass):
state = hass.states.get("sensor.home_cloud_ceiling")
assert state
assert state.state == "10500"
assert state.state == "10500.0"
assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION
assert state.attributes.get(ATTR_ICON) == "mdi:weather-fog"
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfLength.FEET
@ -749,7 +750,7 @@ async def test_state_update(hass):
state = hass.states.get("sensor.home_cloud_ceiling")
assert state
assert state.state != STATE_UNAVAILABLE
assert state.state == "3200"
assert state.state == "3200.0"
future = utcnow() + timedelta(minutes=60)

View File

@ -28,7 +28,7 @@ async def test_async_setup_entry(hass, aioclient_mock):
state = hass.states.get("sensor.home_pm2_5")
assert state is not None
assert state.state != STATE_UNAVAILABLE
assert state.state == "4"
assert state.state == "4.37"
async def test_config_not_ready(hass, aioclient_mock):

View File

@ -38,7 +38,7 @@ async def test_sensor(hass, aioclient_mock):
state = hass.states.get("sensor.home_caqi")
assert state
assert state.state == "7"
assert state.state == "7.29"
assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "CAQI"
assert state.attributes.get(ATTR_ICON) == "mdi:air-filter"
@ -46,10 +46,11 @@ async def test_sensor(hass, aioclient_mock):
entry = registry.async_get("sensor.home_caqi")
assert entry
assert entry.unique_id == "123-456-caqi"
assert entry.options["sensor"] == {"suggested_display_precision": 0}
state = hass.states.get("sensor.home_humidity")
assert state
assert state.state == "68.3"
assert state.state == "68.35"
assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE
assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.HUMIDITY
@ -58,10 +59,11 @@ async def test_sensor(hass, aioclient_mock):
entry = registry.async_get("sensor.home_humidity")
assert entry
assert entry.unique_id == "123-456-humidity"
assert entry.options["sensor"] == {"suggested_display_precision": 1}
state = hass.states.get("sensor.home_pm1_0")
assert state
assert state.state == "3"
assert state.state == "2.83"
assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION
assert (
state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
@ -73,10 +75,11 @@ async def test_sensor(hass, aioclient_mock):
entry = registry.async_get("sensor.home_pm1_0")
assert entry
assert entry.unique_id == "123-456-pm1"
assert entry.options["sensor"] == {"suggested_display_precision": 0}
state = hass.states.get("sensor.home_pm2_5")
assert state
assert state.state == "4"
assert state.state == "4.37"
assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION
assert (
state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
@ -88,10 +91,11 @@ async def test_sensor(hass, aioclient_mock):
entry = registry.async_get("sensor.home_pm2_5")
assert entry
assert entry.unique_id == "123-456-pm25"
assert entry.options["sensor"] == {"suggested_display_precision": 0}
state = hass.states.get("sensor.home_pm10")
assert state
assert state.state == "6"
assert state.state == "6.06"
assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION
assert (
state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
@ -103,16 +107,18 @@ async def test_sensor(hass, aioclient_mock):
entry = registry.async_get("sensor.home_pm10")
assert entry
assert entry.unique_id == "123-456-pm10"
assert entry.options["sensor"] == {"suggested_display_precision": 0}
state = hass.states.get("sensor.home_carbon_monoxide")
assert state
assert state.state == "162"
assert state.state == "162.49"
assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION
assert (
state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
== CONCENTRATION_MICROGRAMS_PER_CUBIC_METER
)
assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT
assert entry.options["sensor"] == {"suggested_display_precision": 0}
entry = registry.async_get("sensor.home_carbon_monoxide")
assert entry
@ -120,7 +126,7 @@ async def test_sensor(hass, aioclient_mock):
state = hass.states.get("sensor.home_nitrogen_dioxide")
assert state
assert state.state == "16"
assert state.state == "16.04"
assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION
assert (
state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
@ -132,10 +138,11 @@ async def test_sensor(hass, aioclient_mock):
entry = registry.async_get("sensor.home_nitrogen_dioxide")
assert entry
assert entry.unique_id == "123-456-no2"
assert entry.options["sensor"] == {"suggested_display_precision": 0}
state = hass.states.get("sensor.home_ozone")
assert state
assert state.state == "42"
assert state.state == "41.52"
assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION
assert (
state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
@ -147,10 +154,11 @@ async def test_sensor(hass, aioclient_mock):
entry = registry.async_get("sensor.home_ozone")
assert entry
assert entry.unique_id == "123-456-o3"
assert entry.options["sensor"] == {"suggested_display_precision": 0}
state = hass.states.get("sensor.home_sulphur_dioxide")
assert state
assert state.state == "14"
assert state.state == "13.97"
assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION
assert (
state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
@ -162,10 +170,11 @@ async def test_sensor(hass, aioclient_mock):
entry = registry.async_get("sensor.home_sulphur_dioxide")
assert entry
assert entry.unique_id == "123-456-so2"
assert entry.options["sensor"] == {"suggested_display_precision": 0}
state = hass.states.get("sensor.home_pressure")
assert state
assert state.state == "1020"
assert state.state == "1019.86"
assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfPressure.HPA
assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.PRESSURE
@ -174,10 +183,11 @@ async def test_sensor(hass, aioclient_mock):
entry = registry.async_get("sensor.home_pressure")
assert entry
assert entry.unique_id == "123-456-pressure"
assert entry.options["sensor"] == {"suggested_display_precision": 0}
state = hass.states.get("sensor.home_temperature")
assert state
assert state.state == "14.4"
assert state.state == "14.37"
assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfTemperature.CELSIUS
assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TEMPERATURE
@ -186,6 +196,7 @@ async def test_sensor(hass, aioclient_mock):
entry = registry.async_get("sensor.home_temperature")
assert entry
assert entry.unique_id == "123-456-temperature"
assert entry.options["sensor"] == {"suggested_display_precision": 1}
async def test_availability(hass, aioclient_mock):
@ -195,7 +206,7 @@ async def test_availability(hass, aioclient_mock):
state = hass.states.get("sensor.home_humidity")
assert state
assert state.state != STATE_UNAVAILABLE
assert state.state == "68.3"
assert state.state == "68.35"
aioclient_mock.clear_requests()
aioclient_mock.get(
@ -218,7 +229,7 @@ async def test_availability(hass, aioclient_mock):
state = hass.states.get("sensor.home_humidity")
assert state
assert state.state != STATE_UNAVAILABLE
assert state.state == "68.3"
assert state.state == "68.35"
async def test_manual_update_entity(hass, aioclient_mock):

View File

@ -518,195 +518,6 @@ async def test_custom_unit(
assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == state_unit
@pytest.mark.parametrize(
"device_class,native_unit,custom_unit,native_value,native_precision,default_state,custom_state",
[
(
SensorDeviceClass.ATMOSPHERIC_PRESSURE,
UnitOfPressure.HPA,
UnitOfPressure.INHG,
1000.0,
2,
"1000.00", # Native precision is 2
"29.530", # One digit of precision added when converting
),
(
SensorDeviceClass.ATMOSPHERIC_PRESSURE,
UnitOfPressure.INHG,
UnitOfPressure.HPA,
29.9211,
3,
"29.921", # Native precision is 3
"1013.24", # One digit of precision removed when converting
),
(
SensorDeviceClass.ATMOSPHERIC_PRESSURE,
UnitOfPressure.INHG,
UnitOfPressure.HPA,
-0.0001,
3,
"0.000", # Native precision is 3
"0.00", # One digit of precision removed when converting
),
],
)
async def test_native_precision_scaling(
hass,
enable_custom_integrations,
device_class,
native_unit,
custom_unit,
native_value,
native_precision,
default_state,
custom_state,
):
"""Test native precision is influenced by unit conversion."""
entity_registry = er.async_get(hass)
entry = entity_registry.async_get_or_create("sensor", "test", "very_unique")
platform = getattr(hass.components, "test.sensor")
platform.init(empty=True)
platform.ENTITIES["0"] = platform.MockSensor(
name="Test",
native_value=str(native_value),
native_precision=native_precision,
native_unit_of_measurement=native_unit,
device_class=device_class,
unique_id="very_unique",
)
entity0 = platform.ENTITIES["0"]
assert await async_setup_component(hass, "sensor", {"sensor": {"platform": "test"}})
await hass.async_block_till_done()
state = hass.states.get(entity0.entity_id)
assert state.state == default_state
assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == native_unit
entity_registry.async_update_entity_options(
entry.entity_id, "sensor", {"unit_of_measurement": custom_unit}
)
await hass.async_block_till_done()
state = hass.states.get(entity0.entity_id)
assert state.state == custom_state
assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == custom_unit
@pytest.mark.parametrize(
"device_class,native_unit,custom_precision,native_value,default_state,custom_state",
[
(
SensorDeviceClass.ATMOSPHERIC_PRESSURE,
UnitOfPressure.HPA,
4,
1000.0,
"1000.000",
"1000.0000",
),
(
SensorDeviceClass.DISTANCE,
UnitOfLength.KILOMETERS,
1,
-0.04,
"-0.040",
"0.0", # Make sure minus is dropped
),
],
)
async def test_custom_precision_native_precision(
hass,
enable_custom_integrations,
device_class,
native_unit,
custom_precision,
native_value,
default_state,
custom_state,
):
"""Test custom precision."""
entity_registry = er.async_get(hass)
entry = entity_registry.async_get_or_create("sensor", "test", "very_unique")
platform = getattr(hass.components, "test.sensor")
platform.init(empty=True)
platform.ENTITIES["0"] = platform.MockSensor(
name="Test",
native_value=str(native_value),
native_precision=3,
native_unit_of_measurement=native_unit,
device_class=device_class,
unique_id="very_unique",
)
entity0 = platform.ENTITIES["0"]
assert await async_setup_component(hass, "sensor", {"sensor": {"platform": "test"}})
await hass.async_block_till_done()
state = hass.states.get(entity0.entity_id)
assert state.state == default_state
assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == native_unit
entity_registry.async_update_entity_options(
entry.entity_id, "sensor", {"precision": custom_precision}
)
await hass.async_block_till_done()
state = hass.states.get(entity0.entity_id)
assert state.state == custom_state
assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == native_unit
@pytest.mark.parametrize(
"device_class,native_unit,custom_precision,native_value,custom_state",
[
(
SensorDeviceClass.ATMOSPHERIC_PRESSURE,
UnitOfPressure.HPA,
4,
1000.0,
"1000.0000",
),
],
)
async def test_custom_precision_no_native_precision(
hass,
enable_custom_integrations,
device_class,
native_unit,
custom_precision,
native_value,
custom_state,
):
"""Test custom precision."""
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", {"precision": custom_precision}
)
await hass.async_block_till_done()
platform = getattr(hass.components, "test.sensor")
platform.init(empty=True)
platform.ENTITIES["0"] = platform.MockSensor(
name="Test",
native_value=str(native_value),
native_unit_of_measurement=native_unit,
device_class=device_class,
unique_id="very_unique",
)
entity0 = platform.ENTITIES["0"]
assert await async_setup_component(hass, "sensor", {"sensor": {"platform": "test"}})
await hass.async_block_till_done()
state = hass.states.get(entity0.entity_id)
assert state.state == custom_state
assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == native_unit
@pytest.mark.parametrize(
"native_unit, custom_unit, state_unit, native_value, native_state, custom_state, device_class",
[
@ -1188,6 +999,121 @@ async def test_unit_conversion_priority_suggested_unit_change(
assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == original_unit
@pytest.mark.parametrize(
"native_unit, suggested_precision, native_value, device_class",
[
# Distance
(
UnitOfLength.KILOMETERS,
4,
1000,
SensorDeviceClass.DISTANCE,
),
],
)
async def test_suggested_precision_option(
hass,
enable_custom_integrations,
native_unit,
suggested_precision,
native_value,
device_class,
):
"""Test suggested precision is stored in the registry."""
entity_registry = er.async_get(hass)
platform = getattr(hass.components, "test.sensor")
platform.init(empty=True)
platform.ENTITIES["0"] = platform.MockSensor(
name="Test",
device_class=device_class,
native_unit_of_measurement=native_unit,
native_value=str(native_value),
suggested_display_precision=suggested_precision,
unique_id="very_unique",
)
entity0 = platform.ENTITIES["0"]
assert await async_setup_component(hass, "sensor", {"sensor": {"platform": "test"}})
await hass.async_block_till_done()
# Assert the suggested precision is stored in the registry
entry = entity_registry.async_get(entity0.entity_id)
assert entry.options == {
"sensor": {"suggested_display_precision": suggested_precision}
}
@pytest.mark.parametrize(
"native_unit, old_precision, new_precision, native_value, device_class",
[
(
UnitOfLength.KILOMETERS,
4,
1,
1000,
SensorDeviceClass.DISTANCE,
),
],
)
async def test_suggested_precision_option_update(
hass,
enable_custom_integrations,
native_unit,
old_precision,
new_precision,
native_value,
device_class,
):
"""Test suggested precision stored in the registry is updated."""
entity_registry = er.async_get(hass)
platform = getattr(hass.components, "test.sensor")
platform.init(empty=True)
# Pre-register entities
entry = entity_registry.async_get_or_create("sensor", "test", "very_unique")
entity_registry.async_update_entity_options(
entry.entity_id,
"sensor",
{
"suggested_display_precision": old_precision,
},
)
entity_registry.async_update_entity_options(
entry.entity_id,
"sensor.private",
{
"suggested_unit_of_measurement": native_unit,
},
)
platform.ENTITIES["0"] = platform.MockSensor(
name="Test",
device_class=device_class,
native_unit_of_measurement=native_unit,
native_value=str(native_value),
suggested_display_precision=new_precision,
unique_id="very_unique",
)
entity0 = platform.ENTITIES["0"]
assert await async_setup_component(hass, "sensor", {"sensor": {"platform": "test"}})
await hass.async_block_till_done()
# Assert the suggested precision is stored in the registry
entry = entity_registry.async_get(entity0.entity_id)
assert entry.options == {
"sensor": {
"suggested_display_precision": new_precision,
},
"sensor.private": {
"suggested_unit_of_measurement": native_unit,
},
}
@pytest.mark.parametrize(
"unit_system, native_unit, original_unit, native_value, original_value, device_class",
[
@ -1519,10 +1445,10 @@ async def test_non_numeric_validation_raise(
platform.ENTITIES["0"] = platform.MockSensor(
name="Test",
device_class=device_class,
native_precision=precision,
native_unit_of_measurement=unit,
native_value=native_value,
state_class=state_class,
suggested_display_precision=precision,
)
entity0 = platform.ENTITIES["0"]
@ -1650,7 +1576,7 @@ async def test_device_classes_with_invalid_state_class(
@pytest.mark.parametrize(
"device_class,state_class,native_unit_of_measurement,native_precision,is_numeric",
"device_class,state_class,native_unit_of_measurement,suggested_precision,is_numeric",
[
(SensorDeviceClass.ENUM, None, None, None, False),
(SensorDeviceClass.DATE, None, None, None, False),
@ -1669,7 +1595,7 @@ async def test_numeric_state_expected_helper(
device_class: SensorDeviceClass | None,
state_class: SensorStateClass | None,
native_unit_of_measurement: str | None,
native_precision: int | None,
suggested_precision: int | None,
is_numeric: bool,
) -> None:
"""Test numeric_state_expected helper."""
@ -1681,7 +1607,7 @@ async def test_numeric_state_expected_helper(
device_class=device_class,
state_class=state_class,
native_unit_of_measurement=native_unit_of_measurement,
native_precision=native_precision,
suggested_display_precision=suggested_precision,
)
assert await async_setup_component(hass, "sensor", {"sensor": {"platform": "test"}})

View File

@ -98,9 +98,9 @@ class MockSensor(MockEntity, SensorEntity):
return self._handle("last_reset")
@property
def native_precision(self):
def suggested_display_precision(self):
"""Return the number of digits after the decimal point."""
return self._handle("native_precision")
return self._handle("suggested_display_precision")
@property
def native_unit_of_measurement(self):