Move sensor rounding to frontend (#87330)
* Move sensor rounding to frontend * Update integrations * Add comment
This commit is contained in:
parent
ee6773236e
commit
bcc1aa03b4
8 changed files with 231 additions and 307 deletions
|
@ -249,7 +249,7 @@ SENSOR_TYPES: tuple[AccuWeatherSensorDescription, ...] = (
|
||||||
metric_unit=UnitOfLength.METERS,
|
metric_unit=UnitOfLength.METERS,
|
||||||
us_customary_unit=UnitOfLength.FEET,
|
us_customary_unit=UnitOfLength.FEET,
|
||||||
value_fn=lambda data, unit: cast(float, data[unit][ATTR_VALUE]),
|
value_fn=lambda data, unit: cast(float, data[unit][ATTR_VALUE]),
|
||||||
native_precision=0,
|
suggested_display_precision=0,
|
||||||
),
|
),
|
||||||
AccuWeatherSensorDescription(
|
AccuWeatherSensorDescription(
|
||||||
key="CloudCover",
|
key="CloudCover",
|
||||||
|
|
|
@ -69,8 +69,8 @@ SENSOR_TYPES: tuple[AirlySensorEntityDescription, ...] = (
|
||||||
key=ATTR_API_CAQI,
|
key=ATTR_API_CAQI,
|
||||||
icon="mdi:air-filter",
|
icon="mdi:air-filter",
|
||||||
name=ATTR_API_CAQI,
|
name=ATTR_API_CAQI,
|
||||||
native_precision=0,
|
|
||||||
native_unit_of_measurement="CAQI",
|
native_unit_of_measurement="CAQI",
|
||||||
|
suggested_display_precision=0,
|
||||||
attrs=lambda data: {
|
attrs=lambda data: {
|
||||||
ATTR_LEVEL: data[ATTR_API_CAQI_LEVEL],
|
ATTR_LEVEL: data[ATTR_API_CAQI_LEVEL],
|
||||||
ATTR_ADVICE: data[ATTR_API_ADVICE],
|
ATTR_ADVICE: data[ATTR_API_ADVICE],
|
||||||
|
@ -81,17 +81,17 @@ SENSOR_TYPES: tuple[AirlySensorEntityDescription, ...] = (
|
||||||
key=ATTR_API_PM1,
|
key=ATTR_API_PM1,
|
||||||
device_class=SensorDeviceClass.PM1,
|
device_class=SensorDeviceClass.PM1,
|
||||||
name="PM1.0",
|
name="PM1.0",
|
||||||
native_precision=0,
|
|
||||||
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
suggested_display_precision=0,
|
||||||
),
|
),
|
||||||
AirlySensorEntityDescription(
|
AirlySensorEntityDescription(
|
||||||
key=ATTR_API_PM25,
|
key=ATTR_API_PM25,
|
||||||
device_class=SensorDeviceClass.PM25,
|
device_class=SensorDeviceClass.PM25,
|
||||||
name="PM2.5",
|
name="PM2.5",
|
||||||
native_precision=0,
|
|
||||||
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
suggested_display_precision=0,
|
||||||
attrs=lambda data: {
|
attrs=lambda data: {
|
||||||
ATTR_LIMIT: data[f"{ATTR_API_PM25}_{SUFFIX_LIMIT}"],
|
ATTR_LIMIT: data[f"{ATTR_API_PM25}_{SUFFIX_LIMIT}"],
|
||||||
ATTR_PERCENT: round(data[f"{ATTR_API_PM25}_{SUFFIX_PERCENT}"]),
|
ATTR_PERCENT: round(data[f"{ATTR_API_PM25}_{SUFFIX_PERCENT}"]),
|
||||||
|
@ -101,9 +101,9 @@ SENSOR_TYPES: tuple[AirlySensorEntityDescription, ...] = (
|
||||||
key=ATTR_API_PM10,
|
key=ATTR_API_PM10,
|
||||||
device_class=SensorDeviceClass.PM10,
|
device_class=SensorDeviceClass.PM10,
|
||||||
name=ATTR_API_PM10,
|
name=ATTR_API_PM10,
|
||||||
native_precision=0,
|
|
||||||
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
suggested_display_precision=0,
|
||||||
attrs=lambda data: {
|
attrs=lambda data: {
|
||||||
ATTR_LIMIT: data[f"{ATTR_API_PM10}_{SUFFIX_LIMIT}"],
|
ATTR_LIMIT: data[f"{ATTR_API_PM10}_{SUFFIX_LIMIT}"],
|
||||||
ATTR_PERCENT: round(data[f"{ATTR_API_PM10}_{SUFFIX_PERCENT}"]),
|
ATTR_PERCENT: round(data[f"{ATTR_API_PM10}_{SUFFIX_PERCENT}"]),
|
||||||
|
@ -113,32 +113,32 @@ SENSOR_TYPES: tuple[AirlySensorEntityDescription, ...] = (
|
||||||
key=ATTR_API_HUMIDITY,
|
key=ATTR_API_HUMIDITY,
|
||||||
device_class=SensorDeviceClass.HUMIDITY,
|
device_class=SensorDeviceClass.HUMIDITY,
|
||||||
name=ATTR_API_HUMIDITY.capitalize(),
|
name=ATTR_API_HUMIDITY.capitalize(),
|
||||||
native_precision=1,
|
|
||||||
native_unit_of_measurement=PERCENTAGE,
|
native_unit_of_measurement=PERCENTAGE,
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
suggested_display_precision=1,
|
||||||
),
|
),
|
||||||
AirlySensorEntityDescription(
|
AirlySensorEntityDescription(
|
||||||
key=ATTR_API_PRESSURE,
|
key=ATTR_API_PRESSURE,
|
||||||
device_class=SensorDeviceClass.PRESSURE,
|
device_class=SensorDeviceClass.PRESSURE,
|
||||||
name=ATTR_API_PRESSURE.capitalize(),
|
name=ATTR_API_PRESSURE.capitalize(),
|
||||||
native_precision=0,
|
|
||||||
native_unit_of_measurement=UnitOfPressure.HPA,
|
native_unit_of_measurement=UnitOfPressure.HPA,
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
suggested_display_precision=0,
|
||||||
),
|
),
|
||||||
AirlySensorEntityDescription(
|
AirlySensorEntityDescription(
|
||||||
key=ATTR_API_TEMPERATURE,
|
key=ATTR_API_TEMPERATURE,
|
||||||
device_class=SensorDeviceClass.TEMPERATURE,
|
device_class=SensorDeviceClass.TEMPERATURE,
|
||||||
name=ATTR_API_TEMPERATURE.capitalize(),
|
name=ATTR_API_TEMPERATURE.capitalize(),
|
||||||
native_precision=1,
|
|
||||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
suggested_display_precision=1,
|
||||||
),
|
),
|
||||||
AirlySensorEntityDescription(
|
AirlySensorEntityDescription(
|
||||||
key=ATTR_API_CO,
|
key=ATTR_API_CO,
|
||||||
name="Carbon monoxide",
|
name="Carbon monoxide",
|
||||||
native_precision=0,
|
|
||||||
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
suggested_display_precision=0,
|
||||||
attrs=lambda data: {
|
attrs=lambda data: {
|
||||||
ATTR_LIMIT: data[f"{ATTR_API_CO}_{SUFFIX_LIMIT}"],
|
ATTR_LIMIT: data[f"{ATTR_API_CO}_{SUFFIX_LIMIT}"],
|
||||||
ATTR_PERCENT: round(data[f"{ATTR_API_CO}_{SUFFIX_PERCENT}"]),
|
ATTR_PERCENT: round(data[f"{ATTR_API_CO}_{SUFFIX_PERCENT}"]),
|
||||||
|
@ -148,9 +148,9 @@ SENSOR_TYPES: tuple[AirlySensorEntityDescription, ...] = (
|
||||||
key=ATTR_API_NO2,
|
key=ATTR_API_NO2,
|
||||||
device_class=SensorDeviceClass.NITROGEN_DIOXIDE,
|
device_class=SensorDeviceClass.NITROGEN_DIOXIDE,
|
||||||
name="Nitrogen dioxide",
|
name="Nitrogen dioxide",
|
||||||
native_precision=0,
|
|
||||||
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
suggested_display_precision=0,
|
||||||
attrs=lambda data: {
|
attrs=lambda data: {
|
||||||
ATTR_LIMIT: data[f"{ATTR_API_NO2}_{SUFFIX_LIMIT}"],
|
ATTR_LIMIT: data[f"{ATTR_API_NO2}_{SUFFIX_LIMIT}"],
|
||||||
ATTR_PERCENT: round(data[f"{ATTR_API_NO2}_{SUFFIX_PERCENT}"]),
|
ATTR_PERCENT: round(data[f"{ATTR_API_NO2}_{SUFFIX_PERCENT}"]),
|
||||||
|
@ -160,9 +160,9 @@ SENSOR_TYPES: tuple[AirlySensorEntityDescription, ...] = (
|
||||||
key=ATTR_API_SO2,
|
key=ATTR_API_SO2,
|
||||||
device_class=SensorDeviceClass.SULPHUR_DIOXIDE,
|
device_class=SensorDeviceClass.SULPHUR_DIOXIDE,
|
||||||
name="Sulphur dioxide",
|
name="Sulphur dioxide",
|
||||||
native_precision=0,
|
|
||||||
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
suggested_display_precision=0,
|
||||||
attrs=lambda data: {
|
attrs=lambda data: {
|
||||||
ATTR_LIMIT: data[f"{ATTR_API_SO2}_{SUFFIX_LIMIT}"],
|
ATTR_LIMIT: data[f"{ATTR_API_SO2}_{SUFFIX_LIMIT}"],
|
||||||
ATTR_PERCENT: round(data[f"{ATTR_API_SO2}_{SUFFIX_PERCENT}"]),
|
ATTR_PERCENT: round(data[f"{ATTR_API_SO2}_{SUFFIX_PERCENT}"]),
|
||||||
|
@ -172,9 +172,9 @@ SENSOR_TYPES: tuple[AirlySensorEntityDescription, ...] = (
|
||||||
key=ATTR_API_O3,
|
key=ATTR_API_O3,
|
||||||
device_class=SensorDeviceClass.OZONE,
|
device_class=SensorDeviceClass.OZONE,
|
||||||
name="Ozone",
|
name="Ozone",
|
||||||
native_precision=0,
|
|
||||||
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
suggested_display_precision=0,
|
||||||
attrs=lambda data: {
|
attrs=lambda data: {
|
||||||
ATTR_LIMIT: data[f"{ATTR_API_O3}_{SUFFIX_LIMIT}"],
|
ATTR_LIMIT: data[f"{ATTR_API_O3}_{SUFFIX_LIMIT}"],
|
||||||
ATTR_PERCENT: round(data[f"{ATTR_API_O3}_{SUFFIX_PERCENT}"]),
|
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 datetime import date, datetime, timedelta, timezone
|
||||||
from decimal import Decimal, InvalidOperation as DecimalInvalidOperation
|
from decimal import Decimal, InvalidOperation as DecimalInvalidOperation
|
||||||
import logging
|
import logging
|
||||||
from math import ceil, floor, log10
|
from math import floor, log10
|
||||||
import re
|
import re
|
||||||
from typing import Any, Final, cast, final
|
from typing import Any, Final, cast, final
|
||||||
|
|
||||||
|
@ -138,10 +138,10 @@ class SensorEntityDescription(EntityDescription):
|
||||||
|
|
||||||
device_class: SensorDeviceClass | None = None
|
device_class: SensorDeviceClass | None = None
|
||||||
last_reset: datetime | None = None
|
last_reset: datetime | None = None
|
||||||
native_precision: int | None = None
|
|
||||||
native_unit_of_measurement: str | None = None
|
native_unit_of_measurement: str | None = None
|
||||||
options: list[str] | None = None
|
options: list[str] | None = None
|
||||||
state_class: SensorStateClass | str | None = None
|
state_class: SensorStateClass | str | None = None
|
||||||
|
suggested_display_precision: int | None = None
|
||||||
suggested_unit_of_measurement: str | None = None
|
suggested_unit_of_measurement: str | None = None
|
||||||
unit_of_measurement: None = None # Type override, use native_unit_of_measurement
|
unit_of_measurement: None = None # Type override, use native_unit_of_measurement
|
||||||
|
|
||||||
|
@ -152,12 +152,12 @@ class SensorEntity(Entity):
|
||||||
entity_description: SensorEntityDescription
|
entity_description: SensorEntityDescription
|
||||||
_attr_device_class: SensorDeviceClass | None
|
_attr_device_class: SensorDeviceClass | None
|
||||||
_attr_last_reset: datetime | None
|
_attr_last_reset: datetime | None
|
||||||
_attr_native_precision: int | None
|
|
||||||
_attr_native_unit_of_measurement: str | None
|
_attr_native_unit_of_measurement: str | None
|
||||||
_attr_native_value: StateType | date | datetime | Decimal = None
|
_attr_native_value: StateType | date | datetime | Decimal = None
|
||||||
_attr_options: list[str] | None
|
_attr_options: list[str] | None
|
||||||
_attr_state_class: SensorStateClass | str | None
|
_attr_state_class: SensorStateClass | str | None
|
||||||
_attr_state: None = None # Subclasses of SensorEntity should not set this
|
_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_suggested_unit_of_measurement: str | None
|
||||||
_attr_unit_of_measurement: None = (
|
_attr_unit_of_measurement: None = (
|
||||||
None # Subclasses of SensorEntity should not set this
|
None # Subclasses of SensorEntity should not set this
|
||||||
|
@ -166,7 +166,7 @@ class SensorEntity(Entity):
|
||||||
_invalid_state_class_reported = False
|
_invalid_state_class_reported = False
|
||||||
_invalid_unit_of_measurement_reported = False
|
_invalid_unit_of_measurement_reported = False
|
||||||
_last_reset_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
|
_sensor_option_unit_of_measurement: str | None | UndefinedType = UNDEFINED
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
|
@ -236,7 +236,8 @@ class SensorEntity(Entity):
|
||||||
await super().async_internal_added_to_hass()
|
await super().async_internal_added_to_hass()
|
||||||
if not self.registry_entry:
|
if not self.registry_entry:
|
||||||
return
|
return
|
||||||
self.async_registry_entry_updated()
|
self._async_read_entity_options()
|
||||||
|
self._update_suggested_precision()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def device_class(self) -> SensorDeviceClass | None:
|
def device_class(self) -> SensorDeviceClass | None:
|
||||||
|
@ -254,7 +255,7 @@ class SensorEntity(Entity):
|
||||||
if (
|
if (
|
||||||
self.state_class is not None
|
self.state_class is not None
|
||||||
or self.native_unit_of_measurement 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
|
return True
|
||||||
# Sensors with custom device classes are not considered numeric
|
# Sensors with custom device classes are not considered numeric
|
||||||
|
@ -359,59 +360,14 @@ class SensorEntity(Entity):
|
||||||
return self._attr_native_value
|
return self._attr_native_value
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def native_precision(self) -> int | None:
|
def suggested_display_precision(self) -> int | None:
|
||||||
"""Return the number of digits after the decimal point for the sensor's state.
|
"""Return the suggested number of decimal digits for display."""
|
||||||
|
if hasattr(self, "_attr_suggested_display_precision"):
|
||||||
If native_precision is None, no rounding is done unless the sensor is subject
|
return self._attr_suggested_display_precision
|
||||||
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
|
|
||||||
if hasattr(self, "entity_description"):
|
if hasattr(self, "entity_description"):
|
||||||
return self.entity_description.native_precision
|
return self.entity_description.suggested_display_precision
|
||||||
return None
|
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
|
@property
|
||||||
def native_unit_of_measurement(self) -> str | None:
|
def native_unit_of_measurement(self) -> str | None:
|
||||||
"""Return the unit of measurement of the sensor, if any."""
|
"""Return the unit of measurement of the sensor, if any."""
|
||||||
|
@ -576,7 +532,7 @@ class SensorEntity(Entity):
|
||||||
)
|
)
|
||||||
return value
|
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
|
# 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
|
# 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]
|
numerical_value = float(value) # type:ignore[arg-type]
|
||||||
except (TypeError, ValueError) as err:
|
except (TypeError, ValueError) as err:
|
||||||
# Raise if precision is not None, for other cases log a warning
|
# 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(
|
raise ValueError(
|
||||||
f"Sensor {self.entity_id} has device class {device_class}, "
|
f"Sensor {self.entity_id} has device class {device_class}, "
|
||||||
f"state class {state_class} unit {unit_of_measurement} and "
|
f"state class {state_class} unit {unit_of_measurement} and "
|
||||||
f"precision {precision} thus indicating it has a numeric value;"
|
f"suggested precision {suggested_precision} thus indicating it "
|
||||||
f" however, it has the non-numeric value: {value} "
|
f"has a numeric value; however, it has the non-numeric value: "
|
||||||
f"({type(value)})"
|
f"{value} ({type(value)})"
|
||||||
) from err
|
) from err
|
||||||
# This should raise in Home Assistant Core 2023.4
|
# This should raise in Home Assistant Core 2023.4
|
||||||
if not self._invalid_numeric_value_reported:
|
if not self._invalid_numeric_value_reported:
|
||||||
|
@ -629,7 +585,18 @@ class SensorEntity(Entity):
|
||||||
# Unit conversion needed
|
# Unit conversion needed
|
||||||
converter = UNIT_CONVERTERS[device_class]
|
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
|
# Deduce the precision by finding the decimal point, if any
|
||||||
value_s = str(value)
|
value_s = str(value)
|
||||||
precision = (
|
precision = (
|
||||||
|
@ -648,20 +615,10 @@ class SensorEntity(Entity):
|
||||||
)
|
)
|
||||||
precision = precision + floor(ratio_log)
|
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}"
|
value = f"{converted_numerical_value:.{precision}f}"
|
||||||
# This can be replaced with adding the z option when we drop support for
|
# This can be replaced with adding the z option when we drop support for
|
||||||
# Python 3.10
|
# Python 3.10
|
||||||
value = NEGATIVE_ZERO_PATTERN.sub(r"\1", value)
|
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)
|
|
||||||
|
|
||||||
# Validate unit of measurement used for sensors with a device class
|
# Validate unit of measurement used for sensors with a device class
|
||||||
if (
|
if (
|
||||||
|
@ -703,15 +660,35 @@ class SensorEntity(Entity):
|
||||||
|
|
||||||
return super().__repr__()
|
return super().__repr__()
|
||||||
|
|
||||||
def _custom_precision_or_none(self) -> int | None:
|
def _suggested_precision_or_none(self) -> int | None:
|
||||||
"""Return a custom precisions or None if not set."""
|
"""Return suggested display precision, or None if not set."""
|
||||||
assert self.registry_entry
|
assert self.registry_entry
|
||||||
if (sensor_options := self.registry_entry.options.get(DOMAIN)) and (
|
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:
|
) is not None:
|
||||||
return int(precision)
|
return cast(int, precision)
|
||||||
return None
|
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(
|
def _custom_unit_or_undef(
|
||||||
self, primary_key: str, secondary_key: str
|
self, primary_key: str, secondary_key: str
|
||||||
) -> str | None | UndefinedType:
|
) -> str | None | UndefinedType:
|
||||||
|
@ -732,7 +709,16 @@ class SensorEntity(Entity):
|
||||||
@callback
|
@callback
|
||||||
def async_registry_entry_updated(self) -> None:
|
def async_registry_entry_updated(self) -> None:
|
||||||
"""Run when the entity registry entry has been updated."""
|
"""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
|
assert self.registry_entry
|
||||||
if (
|
if (
|
||||||
sensor_options := self.registry_entry.options.get(f"{DOMAIN}.private")
|
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")
|
state = hass.states.get("sensor.home_cloud_ceiling")
|
||||||
assert state
|
assert state
|
||||||
assert state.state == "3200"
|
assert state.state == "3200.0"
|
||||||
assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION
|
assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION
|
||||||
assert state.attributes.get(ATTR_ICON) == "mdi:weather-fog"
|
assert state.attributes.get(ATTR_ICON) == "mdi:weather-fog"
|
||||||
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfLength.METERS
|
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")
|
entry = registry.async_get("sensor.home_cloud_ceiling")
|
||||||
assert entry
|
assert entry
|
||||||
assert entry.unique_id == "0123456-ceiling"
|
assert entry.unique_id == "0123456-ceiling"
|
||||||
|
assert entry.options["sensor"] == {"suggested_display_precision": 0}
|
||||||
|
|
||||||
state = hass.states.get("sensor.home_precipitation")
|
state = hass.states.get("sensor.home_precipitation")
|
||||||
assert state
|
assert state
|
||||||
|
@ -665,7 +666,7 @@ async def test_availability(hass):
|
||||||
state = hass.states.get("sensor.home_cloud_ceiling")
|
state = hass.states.get("sensor.home_cloud_ceiling")
|
||||||
assert state
|
assert state
|
||||||
assert state.state != STATE_UNAVAILABLE
|
assert state.state != STATE_UNAVAILABLE
|
||||||
assert state.state == "3200"
|
assert state.state == "3200.0"
|
||||||
|
|
||||||
future = utcnow() + timedelta(minutes=60)
|
future = utcnow() + timedelta(minutes=60)
|
||||||
with patch(
|
with patch(
|
||||||
|
@ -696,7 +697,7 @@ async def test_availability(hass):
|
||||||
state = hass.states.get("sensor.home_cloud_ceiling")
|
state = hass.states.get("sensor.home_cloud_ceiling")
|
||||||
assert state
|
assert state
|
||||||
assert state.state != STATE_UNAVAILABLE
|
assert state.state != STATE_UNAVAILABLE
|
||||||
assert state.state == "3200"
|
assert state.state == "3200.0"
|
||||||
|
|
||||||
|
|
||||||
async def test_manual_update_entity(hass):
|
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")
|
state = hass.states.get("sensor.home_cloud_ceiling")
|
||||||
assert state
|
assert state
|
||||||
assert state.state == "10500"
|
assert state.state == "10500.0"
|
||||||
assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION
|
assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION
|
||||||
assert state.attributes.get(ATTR_ICON) == "mdi:weather-fog"
|
assert state.attributes.get(ATTR_ICON) == "mdi:weather-fog"
|
||||||
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfLength.FEET
|
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")
|
state = hass.states.get("sensor.home_cloud_ceiling")
|
||||||
assert state
|
assert state
|
||||||
assert state.state != STATE_UNAVAILABLE
|
assert state.state != STATE_UNAVAILABLE
|
||||||
assert state.state == "3200"
|
assert state.state == "3200.0"
|
||||||
|
|
||||||
future = utcnow() + timedelta(minutes=60)
|
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")
|
state = hass.states.get("sensor.home_pm2_5")
|
||||||
assert state is not None
|
assert state is not None
|
||||||
assert state.state != STATE_UNAVAILABLE
|
assert state.state != STATE_UNAVAILABLE
|
||||||
assert state.state == "4"
|
assert state.state == "4.37"
|
||||||
|
|
||||||
|
|
||||||
async def test_config_not_ready(hass, aioclient_mock):
|
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")
|
state = hass.states.get("sensor.home_caqi")
|
||||||
assert state
|
assert state
|
||||||
assert state.state == "7"
|
assert state.state == "7.29"
|
||||||
assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION
|
assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION
|
||||||
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "CAQI"
|
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "CAQI"
|
||||||
assert state.attributes.get(ATTR_ICON) == "mdi:air-filter"
|
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")
|
entry = registry.async_get("sensor.home_caqi")
|
||||||
assert entry
|
assert entry
|
||||||
assert entry.unique_id == "123-456-caqi"
|
assert entry.unique_id == "123-456-caqi"
|
||||||
|
assert entry.options["sensor"] == {"suggested_display_precision": 0}
|
||||||
|
|
||||||
state = hass.states.get("sensor.home_humidity")
|
state = hass.states.get("sensor.home_humidity")
|
||||||
assert state
|
assert state
|
||||||
assert state.state == "68.3"
|
assert state.state == "68.35"
|
||||||
assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION
|
assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION
|
||||||
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE
|
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE
|
||||||
assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.HUMIDITY
|
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")
|
entry = registry.async_get("sensor.home_humidity")
|
||||||
assert entry
|
assert entry
|
||||||
assert entry.unique_id == "123-456-humidity"
|
assert entry.unique_id == "123-456-humidity"
|
||||||
|
assert entry.options["sensor"] == {"suggested_display_precision": 1}
|
||||||
|
|
||||||
state = hass.states.get("sensor.home_pm1_0")
|
state = hass.states.get("sensor.home_pm1_0")
|
||||||
assert state
|
assert state
|
||||||
assert state.state == "3"
|
assert state.state == "2.83"
|
||||||
assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION
|
assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION
|
||||||
assert (
|
assert (
|
||||||
state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
|
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")
|
entry = registry.async_get("sensor.home_pm1_0")
|
||||||
assert entry
|
assert entry
|
||||||
assert entry.unique_id == "123-456-pm1"
|
assert entry.unique_id == "123-456-pm1"
|
||||||
|
assert entry.options["sensor"] == {"suggested_display_precision": 0}
|
||||||
|
|
||||||
state = hass.states.get("sensor.home_pm2_5")
|
state = hass.states.get("sensor.home_pm2_5")
|
||||||
assert state
|
assert state
|
||||||
assert state.state == "4"
|
assert state.state == "4.37"
|
||||||
assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION
|
assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION
|
||||||
assert (
|
assert (
|
||||||
state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
|
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")
|
entry = registry.async_get("sensor.home_pm2_5")
|
||||||
assert entry
|
assert entry
|
||||||
assert entry.unique_id == "123-456-pm25"
|
assert entry.unique_id == "123-456-pm25"
|
||||||
|
assert entry.options["sensor"] == {"suggested_display_precision": 0}
|
||||||
|
|
||||||
state = hass.states.get("sensor.home_pm10")
|
state = hass.states.get("sensor.home_pm10")
|
||||||
assert state
|
assert state
|
||||||
assert state.state == "6"
|
assert state.state == "6.06"
|
||||||
assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION
|
assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION
|
||||||
assert (
|
assert (
|
||||||
state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
|
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")
|
entry = registry.async_get("sensor.home_pm10")
|
||||||
assert entry
|
assert entry
|
||||||
assert entry.unique_id == "123-456-pm10"
|
assert entry.unique_id == "123-456-pm10"
|
||||||
|
assert entry.options["sensor"] == {"suggested_display_precision": 0}
|
||||||
|
|
||||||
state = hass.states.get("sensor.home_carbon_monoxide")
|
state = hass.states.get("sensor.home_carbon_monoxide")
|
||||||
assert state
|
assert state
|
||||||
assert state.state == "162"
|
assert state.state == "162.49"
|
||||||
assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION
|
assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION
|
||||||
assert (
|
assert (
|
||||||
state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
|
state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
|
||||||
== CONCENTRATION_MICROGRAMS_PER_CUBIC_METER
|
== CONCENTRATION_MICROGRAMS_PER_CUBIC_METER
|
||||||
)
|
)
|
||||||
assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT
|
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")
|
entry = registry.async_get("sensor.home_carbon_monoxide")
|
||||||
assert entry
|
assert entry
|
||||||
|
@ -120,7 +126,7 @@ async def test_sensor(hass, aioclient_mock):
|
||||||
|
|
||||||
state = hass.states.get("sensor.home_nitrogen_dioxide")
|
state = hass.states.get("sensor.home_nitrogen_dioxide")
|
||||||
assert state
|
assert state
|
||||||
assert state.state == "16"
|
assert state.state == "16.04"
|
||||||
assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION
|
assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION
|
||||||
assert (
|
assert (
|
||||||
state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
|
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")
|
entry = registry.async_get("sensor.home_nitrogen_dioxide")
|
||||||
assert entry
|
assert entry
|
||||||
assert entry.unique_id == "123-456-no2"
|
assert entry.unique_id == "123-456-no2"
|
||||||
|
assert entry.options["sensor"] == {"suggested_display_precision": 0}
|
||||||
|
|
||||||
state = hass.states.get("sensor.home_ozone")
|
state = hass.states.get("sensor.home_ozone")
|
||||||
assert state
|
assert state
|
||||||
assert state.state == "42"
|
assert state.state == "41.52"
|
||||||
assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION
|
assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION
|
||||||
assert (
|
assert (
|
||||||
state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
|
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")
|
entry = registry.async_get("sensor.home_ozone")
|
||||||
assert entry
|
assert entry
|
||||||
assert entry.unique_id == "123-456-o3"
|
assert entry.unique_id == "123-456-o3"
|
||||||
|
assert entry.options["sensor"] == {"suggested_display_precision": 0}
|
||||||
|
|
||||||
state = hass.states.get("sensor.home_sulphur_dioxide")
|
state = hass.states.get("sensor.home_sulphur_dioxide")
|
||||||
assert state
|
assert state
|
||||||
assert state.state == "14"
|
assert state.state == "13.97"
|
||||||
assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION
|
assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION
|
||||||
assert (
|
assert (
|
||||||
state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
|
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")
|
entry = registry.async_get("sensor.home_sulphur_dioxide")
|
||||||
assert entry
|
assert entry
|
||||||
assert entry.unique_id == "123-456-so2"
|
assert entry.unique_id == "123-456-so2"
|
||||||
|
assert entry.options["sensor"] == {"suggested_display_precision": 0}
|
||||||
|
|
||||||
state = hass.states.get("sensor.home_pressure")
|
state = hass.states.get("sensor.home_pressure")
|
||||||
assert state
|
assert state
|
||||||
assert state.state == "1020"
|
assert state.state == "1019.86"
|
||||||
assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION
|
assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION
|
||||||
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfPressure.HPA
|
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfPressure.HPA
|
||||||
assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.PRESSURE
|
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")
|
entry = registry.async_get("sensor.home_pressure")
|
||||||
assert entry
|
assert entry
|
||||||
assert entry.unique_id == "123-456-pressure"
|
assert entry.unique_id == "123-456-pressure"
|
||||||
|
assert entry.options["sensor"] == {"suggested_display_precision": 0}
|
||||||
|
|
||||||
state = hass.states.get("sensor.home_temperature")
|
state = hass.states.get("sensor.home_temperature")
|
||||||
assert state
|
assert state
|
||||||
assert state.state == "14.4"
|
assert state.state == "14.37"
|
||||||
assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION
|
assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION
|
||||||
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfTemperature.CELSIUS
|
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfTemperature.CELSIUS
|
||||||
assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TEMPERATURE
|
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")
|
entry = registry.async_get("sensor.home_temperature")
|
||||||
assert entry
|
assert entry
|
||||||
assert entry.unique_id == "123-456-temperature"
|
assert entry.unique_id == "123-456-temperature"
|
||||||
|
assert entry.options["sensor"] == {"suggested_display_precision": 1}
|
||||||
|
|
||||||
|
|
||||||
async def test_availability(hass, aioclient_mock):
|
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")
|
state = hass.states.get("sensor.home_humidity")
|
||||||
assert state
|
assert state
|
||||||
assert state.state != STATE_UNAVAILABLE
|
assert state.state != STATE_UNAVAILABLE
|
||||||
assert state.state == "68.3"
|
assert state.state == "68.35"
|
||||||
|
|
||||||
aioclient_mock.clear_requests()
|
aioclient_mock.clear_requests()
|
||||||
aioclient_mock.get(
|
aioclient_mock.get(
|
||||||
|
@ -218,7 +229,7 @@ async def test_availability(hass, aioclient_mock):
|
||||||
state = hass.states.get("sensor.home_humidity")
|
state = hass.states.get("sensor.home_humidity")
|
||||||
assert state
|
assert state
|
||||||
assert state.state != STATE_UNAVAILABLE
|
assert state.state != STATE_UNAVAILABLE
|
||||||
assert state.state == "68.3"
|
assert state.state == "68.35"
|
||||||
|
|
||||||
|
|
||||||
async def test_manual_update_entity(hass, aioclient_mock):
|
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
|
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(
|
@pytest.mark.parametrize(
|
||||||
"native_unit, custom_unit, state_unit, native_value, native_state, custom_state, device_class",
|
"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
|
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(
|
@pytest.mark.parametrize(
|
||||||
"unit_system, native_unit, original_unit, native_value, original_value, device_class",
|
"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(
|
platform.ENTITIES["0"] = platform.MockSensor(
|
||||||
name="Test",
|
name="Test",
|
||||||
device_class=device_class,
|
device_class=device_class,
|
||||||
native_precision=precision,
|
|
||||||
native_unit_of_measurement=unit,
|
native_unit_of_measurement=unit,
|
||||||
native_value=native_value,
|
native_value=native_value,
|
||||||
state_class=state_class,
|
state_class=state_class,
|
||||||
|
suggested_display_precision=precision,
|
||||||
)
|
)
|
||||||
entity0 = platform.ENTITIES["0"]
|
entity0 = platform.ENTITIES["0"]
|
||||||
|
|
||||||
|
@ -1650,7 +1576,7 @@ async def test_device_classes_with_invalid_state_class(
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@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.ENUM, None, None, None, False),
|
||||||
(SensorDeviceClass.DATE, 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,
|
device_class: SensorDeviceClass | None,
|
||||||
state_class: SensorStateClass | None,
|
state_class: SensorStateClass | None,
|
||||||
native_unit_of_measurement: str | None,
|
native_unit_of_measurement: str | None,
|
||||||
native_precision: int | None,
|
suggested_precision: int | None,
|
||||||
is_numeric: bool,
|
is_numeric: bool,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test numeric_state_expected helper."""
|
"""Test numeric_state_expected helper."""
|
||||||
|
@ -1681,7 +1607,7 @@ async def test_numeric_state_expected_helper(
|
||||||
device_class=device_class,
|
device_class=device_class,
|
||||||
state_class=state_class,
|
state_class=state_class,
|
||||||
native_unit_of_measurement=native_unit_of_measurement,
|
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"}})
|
assert await async_setup_component(hass, "sensor", {"sensor": {"platform": "test"}})
|
||||||
|
|
|
@ -98,9 +98,9 @@ class MockSensor(MockEntity, SensorEntity):
|
||||||
return self._handle("last_reset")
|
return self._handle("last_reset")
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def native_precision(self):
|
def suggested_display_precision(self):
|
||||||
"""Return the number of digits after the decimal point."""
|
"""Return the number of digits after the decimal point."""
|
||||||
return self._handle("native_precision")
|
return self._handle("suggested_display_precision")
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def native_unit_of_measurement(self):
|
def native_unit_of_measurement(self):
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue