mirror of
https://github.com/home-assistant/core.git
synced 2025-04-22 16:27:56 +00:00
Move sensor rounding to frontend (#87330)
* Move sensor rounding to frontend * Update integrations * Add comment
This commit is contained in:
parent
ee6773236e
commit
bcc1aa03b4
@ -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",
|
||||
|
@ -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}"]),
|
||||
|
@ -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")
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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):
|
||||
|
@ -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):
|
||||
|
@ -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"}})
|
||||
|
@ -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):
|
||||
|
Loading…
x
Reference in New Issue
Block a user