mirror of
https://github.com/home-assistant/core.git
synced 2025-04-24 01:08:12 +00:00
Allow customizing sensor state precision (#86074)
* Allow customizing sensor precision * Don't convert integer strings to floats * Tweak converting sensor state to number * Drop default rounding to 2 decimals * Adjust test * Tweak rounding, improve test coverage * Don't convert to a number if not necessary * Raise if native_precision is set and state is not numeric * Address review comments * Address comments, simplify * Don't call property twice * Make exception more helpful
This commit is contained in:
parent
ba63a9600e
commit
086a6460ef
@ -8,7 +8,7 @@ from dataclasses import dataclass
|
||||
from datetime import date, datetime, timedelta, timezone
|
||||
from decimal import Decimal, InvalidOperation as DecimalInvalidOperation
|
||||
import logging
|
||||
from math import floor, log10
|
||||
from math import ceil, floor, log10
|
||||
from typing import Any, Final, cast, final
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
@ -133,11 +133,12 @@ class SensorEntityDescription(EntityDescription):
|
||||
"""A class that describes sensor entities."""
|
||||
|
||||
device_class: SensorDeviceClass | None = None
|
||||
suggested_unit_of_measurement: str | None = None
|
||||
last_reset: datetime | None = None
|
||||
native_precision: int | None = None
|
||||
native_unit_of_measurement: str | None = None
|
||||
state_class: SensorStateClass | str | None = None
|
||||
options: list[str] | None = None
|
||||
state_class: SensorStateClass | str | None = None
|
||||
suggested_unit_of_measurement: str | None = None
|
||||
unit_of_measurement: None = None # Type override, use native_unit_of_measurement
|
||||
|
||||
|
||||
@ -147,6 +148,7 @@ 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
|
||||
@ -160,6 +162,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_unit_of_measurement: str | None | UndefinedType = UNDEFINED
|
||||
|
||||
@callback
|
||||
@ -337,6 +340,60 @@ class SensorEntity(Entity):
|
||||
"""Return the value reported by the sensor."""
|
||||
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
|
||||
if hasattr(self, "entity_description"):
|
||||
return self.entity_description.native_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."""
|
||||
@ -399,7 +456,7 @@ class SensorEntity(Entity):
|
||||
|
||||
@final
|
||||
@property
|
||||
def state(self) -> Any:
|
||||
def state(self) -> Any: # noqa: C901
|
||||
"""Return the state of the sensor and perform unit conversions, if needed."""
|
||||
native_unit_of_measurement = self.native_unit_of_measurement
|
||||
unit_of_measurement = self.unit_of_measurement
|
||||
@ -508,17 +565,36 @@ class SensorEntity(Entity):
|
||||
)
|
||||
return value
|
||||
|
||||
# If the sensor has neither a device class, a state class nor
|
||||
# a unit_of measurement then there are no further checks or conversions
|
||||
if not device_class and not state_class and not unit_of_measurement:
|
||||
precision = self.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
|
||||
if (
|
||||
not device_class
|
||||
and not state_class
|
||||
and not unit_of_measurement
|
||||
and precision is None
|
||||
):
|
||||
return value
|
||||
|
||||
if not self._invalid_numeric_value_reported and not isinstance(
|
||||
value, (int, float, Decimal)
|
||||
):
|
||||
# From here on a numerical value is expected
|
||||
numerical_value: int | float | Decimal
|
||||
if not isinstance(value, (int, float, Decimal)):
|
||||
try:
|
||||
_ = float(value) # type: ignore[arg-type]
|
||||
except (TypeError, ValueError):
|
||||
if isinstance(value, str) and "." not in value:
|
||||
numerical_value = int(value)
|
||||
else:
|
||||
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:
|
||||
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)})"
|
||||
) from err
|
||||
# This should raise in Home Assistant Core 2023.4
|
||||
self._invalid_numeric_value_reported = True
|
||||
report_issue = self._suggest_report_issue()
|
||||
@ -536,39 +612,43 @@ class SensorEntity(Entity):
|
||||
report_issue,
|
||||
)
|
||||
return value
|
||||
else:
|
||||
numerical_value = value
|
||||
|
||||
if (
|
||||
native_unit_of_measurement != unit_of_measurement
|
||||
and device_class in UNIT_CONVERTERS
|
||||
):
|
||||
# Unit conversion needed
|
||||
converter = UNIT_CONVERTERS[device_class]
|
||||
|
||||
value_s = str(value)
|
||||
prec = len(value_s) - value_s.index(".") - 1 if "." in value_s else 0
|
||||
|
||||
# Scale the precision when converting to a larger unit
|
||||
# For example 1.1 Wh should be rendered as 0.0011 kWh, not 0.0 kWh
|
||||
ratio_log = max(
|
||||
0,
|
||||
log10(
|
||||
converter.get_unit_ratio(
|
||||
native_unit_of_measurement, unit_of_measurement
|
||||
)
|
||||
),
|
||||
)
|
||||
prec = prec + floor(ratio_log)
|
||||
|
||||
# Suppress ValueError (Could not convert sensor_value to float)
|
||||
with suppress(ValueError):
|
||||
value_f = float(value) # type: ignore[arg-type]
|
||||
value_f_new = converter.convert(
|
||||
value_f,
|
||||
native_unit_of_measurement,
|
||||
unit_of_measurement,
|
||||
if precision is None:
|
||||
# Deduce the precision by finding the decimal point, if any
|
||||
value_s = str(value)
|
||||
precision = (
|
||||
len(value_s) - value_s.index(".") - 1 if "." in value_s else 0
|
||||
)
|
||||
|
||||
# Round to the wanted precision
|
||||
value = round(value_f_new) if prec == 0 else round(value_f_new, prec)
|
||||
# Scale the precision when converting to a larger unit
|
||||
# For example 1.1 Wh should be rendered as 0.0011 kWh, not 0.0 kWh
|
||||
ratio_log = max(
|
||||
0,
|
||||
log10(
|
||||
converter.get_unit_ratio(
|
||||
native_unit_of_measurement, unit_of_measurement
|
||||
)
|
||||
),
|
||||
)
|
||||
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}"
|
||||
elif precision is not None:
|
||||
value = f"{numerical_value:.{precision}f}"
|
||||
|
||||
# Validate unit of measurement used for sensors with a device class
|
||||
if (
|
||||
@ -608,6 +688,15 @@ class SensorEntity(Entity):
|
||||
|
||||
return super().__repr__()
|
||||
|
||||
def _custom_precision_or_none(self) -> int | None:
|
||||
"""Return a custom precisions or None if not set."""
|
||||
assert self.registry_entry
|
||||
if (sensor_options := self.registry_entry.options.get(DOMAIN)) and (
|
||||
precision := sensor_options.get("precision")
|
||||
) is not None:
|
||||
return int(precision)
|
||||
return None
|
||||
|
||||
def _custom_unit_or_undef(
|
||||
self, primary_key: str, secondary_key: str
|
||||
) -> str | None | UndefinedType:
|
||||
@ -628,6 +717,7 @@ 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._sensor_option_unit_of_measurement = self._custom_unit_or_undef(
|
||||
DOMAIN, CONF_UNIT_OF_MEASUREMENT
|
||||
)
|
||||
|
@ -20,7 +20,7 @@ async def test_airzone_create_sensors(
|
||||
|
||||
# Zones
|
||||
state = hass.states.get("sensor.despacho_temperature")
|
||||
assert state.state == "21.2"
|
||||
assert state.state == "21.20"
|
||||
|
||||
state = hass.states.get("sensor.despacho_humidity")
|
||||
assert state.state == "36"
|
||||
|
@ -41,23 +41,29 @@ from tests.common import mock_restore_cache_with_extra_data
|
||||
UnitOfTemperature.FAHRENHEIT,
|
||||
UnitOfTemperature.FAHRENHEIT,
|
||||
100,
|
||||
100,
|
||||
"100",
|
||||
),
|
||||
(
|
||||
US_CUSTOMARY_SYSTEM,
|
||||
UnitOfTemperature.CELSIUS,
|
||||
UnitOfTemperature.FAHRENHEIT,
|
||||
38,
|
||||
100,
|
||||
"100",
|
||||
),
|
||||
(
|
||||
METRIC_SYSTEM,
|
||||
UnitOfTemperature.FAHRENHEIT,
|
||||
UnitOfTemperature.CELSIUS,
|
||||
100,
|
||||
38,
|
||||
"38",
|
||||
),
|
||||
(
|
||||
METRIC_SYSTEM,
|
||||
UnitOfTemperature.CELSIUS,
|
||||
UnitOfTemperature.CELSIUS,
|
||||
38,
|
||||
"38",
|
||||
),
|
||||
(METRIC_SYSTEM, UnitOfTemperature.CELSIUS, UnitOfTemperature.CELSIUS, 38, 38),
|
||||
],
|
||||
)
|
||||
async def test_temperature_conversion(
|
||||
@ -85,7 +91,7 @@ async def test_temperature_conversion(
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get(entity0.entity_id)
|
||||
assert float(state.state) == approx(float(state_value))
|
||||
assert state.state == state_value
|
||||
assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == state_unit
|
||||
|
||||
|
||||
@ -407,7 +413,7 @@ async def test_restore_sensor_restore_state(
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"device_class,native_unit,custom_unit,state_unit,native_value,custom_value",
|
||||
"device_class, native_unit, custom_unit, state_unit, native_value, custom_state",
|
||||
[
|
||||
# Smaller to larger unit, InHg is ~33x larger than hPa -> 1 more decimal
|
||||
(
|
||||
@ -416,7 +422,7 @@ async def test_restore_sensor_restore_state(
|
||||
UnitOfPressure.INHG,
|
||||
UnitOfPressure.INHG,
|
||||
1000.0,
|
||||
29.53,
|
||||
"29.53",
|
||||
),
|
||||
(
|
||||
SensorDeviceClass.PRESSURE,
|
||||
@ -424,7 +430,15 @@ async def test_restore_sensor_restore_state(
|
||||
UnitOfPressure.HPA,
|
||||
UnitOfPressure.HPA,
|
||||
1.234,
|
||||
12.34,
|
||||
"12.340",
|
||||
),
|
||||
(
|
||||
SensorDeviceClass.ATMOSPHERIC_PRESSURE,
|
||||
UnitOfPressure.HPA,
|
||||
UnitOfPressure.MMHG,
|
||||
UnitOfPressure.MMHG,
|
||||
1000,
|
||||
"750",
|
||||
),
|
||||
(
|
||||
SensorDeviceClass.PRESSURE,
|
||||
@ -432,7 +446,7 @@ async def test_restore_sensor_restore_state(
|
||||
UnitOfPressure.MMHG,
|
||||
UnitOfPressure.MMHG,
|
||||
1000,
|
||||
750,
|
||||
"750",
|
||||
),
|
||||
# Not a supported pressure unit
|
||||
(
|
||||
@ -441,7 +455,7 @@ async def test_restore_sensor_restore_state(
|
||||
"peer_pressure",
|
||||
UnitOfPressure.HPA,
|
||||
1000,
|
||||
1000,
|
||||
"1000",
|
||||
),
|
||||
(
|
||||
SensorDeviceClass.TEMPERATURE,
|
||||
@ -449,7 +463,7 @@ async def test_restore_sensor_restore_state(
|
||||
UnitOfTemperature.FAHRENHEIT,
|
||||
UnitOfTemperature.FAHRENHEIT,
|
||||
37.5,
|
||||
99.5,
|
||||
"99.5",
|
||||
),
|
||||
(
|
||||
SensorDeviceClass.TEMPERATURE,
|
||||
@ -457,7 +471,7 @@ async def test_restore_sensor_restore_state(
|
||||
UnitOfTemperature.CELSIUS,
|
||||
UnitOfTemperature.CELSIUS,
|
||||
100,
|
||||
38.0,
|
||||
"38",
|
||||
),
|
||||
],
|
||||
)
|
||||
@ -469,7 +483,7 @@ async def test_custom_unit(
|
||||
custom_unit,
|
||||
state_unit,
|
||||
native_value,
|
||||
custom_value,
|
||||
custom_state,
|
||||
):
|
||||
"""Test custom unit."""
|
||||
entity_registry = er.async_get(hass)
|
||||
@ -495,12 +509,184 @@ async def test_custom_unit(
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get(entity0.entity_id)
|
||||
assert float(state.state) == approx(float(custom_value))
|
||||
assert state.state == custom_state
|
||||
assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == state_unit
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"native_unit,custom_unit,state_unit,native_value,custom_value,device_class",
|
||||
"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
|
||||
),
|
||||
],
|
||||
)
|
||||
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",
|
||||
),
|
||||
],
|
||||
)
|
||||
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",
|
||||
[
|
||||
# Distance
|
||||
(
|
||||
@ -508,7 +694,8 @@ async def test_custom_unit(
|
||||
UnitOfLength.MILES,
|
||||
UnitOfLength.MILES,
|
||||
1000,
|
||||
621,
|
||||
"1000",
|
||||
"621",
|
||||
SensorDeviceClass.DISTANCE,
|
||||
),
|
||||
(
|
||||
@ -516,7 +703,8 @@ async def test_custom_unit(
|
||||
UnitOfLength.INCHES,
|
||||
UnitOfLength.INCHES,
|
||||
7.24,
|
||||
2.85,
|
||||
"7.24",
|
||||
"2.85",
|
||||
SensorDeviceClass.DISTANCE,
|
||||
),
|
||||
(
|
||||
@ -524,7 +712,8 @@ async def test_custom_unit(
|
||||
"peer_distance",
|
||||
UnitOfLength.KILOMETERS,
|
||||
1000,
|
||||
1000,
|
||||
"1000",
|
||||
"1000",
|
||||
SensorDeviceClass.DISTANCE,
|
||||
),
|
||||
# Energy
|
||||
@ -533,7 +722,8 @@ async def test_custom_unit(
|
||||
UnitOfEnergy.MEGA_WATT_HOUR,
|
||||
UnitOfEnergy.MEGA_WATT_HOUR,
|
||||
1000,
|
||||
1.0,
|
||||
"1000",
|
||||
"1.000",
|
||||
SensorDeviceClass.ENERGY,
|
||||
),
|
||||
(
|
||||
@ -541,7 +731,8 @@ async def test_custom_unit(
|
||||
UnitOfEnergy.MEGA_WATT_HOUR,
|
||||
UnitOfEnergy.MEGA_WATT_HOUR,
|
||||
1000,
|
||||
278,
|
||||
"1000",
|
||||
"278",
|
||||
SensorDeviceClass.ENERGY,
|
||||
),
|
||||
(
|
||||
@ -549,7 +740,8 @@ async def test_custom_unit(
|
||||
"BTU",
|
||||
UnitOfEnergy.KILO_WATT_HOUR,
|
||||
1000,
|
||||
1000,
|
||||
"1000",
|
||||
"1000",
|
||||
SensorDeviceClass.ENERGY,
|
||||
),
|
||||
# Power factor
|
||||
@ -558,7 +750,8 @@ async def test_custom_unit(
|
||||
PERCENTAGE,
|
||||
PERCENTAGE,
|
||||
1.0,
|
||||
100,
|
||||
"1.0",
|
||||
"100.0",
|
||||
SensorDeviceClass.POWER_FACTOR,
|
||||
),
|
||||
(
|
||||
@ -566,7 +759,8 @@ async def test_custom_unit(
|
||||
None,
|
||||
None,
|
||||
100,
|
||||
1,
|
||||
"100",
|
||||
"1.00",
|
||||
SensorDeviceClass.POWER_FACTOR,
|
||||
),
|
||||
(
|
||||
@ -574,7 +768,8 @@ async def test_custom_unit(
|
||||
None,
|
||||
"Cos φ",
|
||||
1.0,
|
||||
1.0,
|
||||
"1.0",
|
||||
"1.0",
|
||||
SensorDeviceClass.POWER_FACTOR,
|
||||
),
|
||||
# Pressure
|
||||
@ -584,7 +779,8 @@ async def test_custom_unit(
|
||||
UnitOfPressure.INHG,
|
||||
UnitOfPressure.INHG,
|
||||
1000.0,
|
||||
29.53,
|
||||
"1000.0",
|
||||
"29.53",
|
||||
SensorDeviceClass.PRESSURE,
|
||||
),
|
||||
(
|
||||
@ -592,7 +788,8 @@ async def test_custom_unit(
|
||||
UnitOfPressure.HPA,
|
||||
UnitOfPressure.HPA,
|
||||
1.234,
|
||||
12.34,
|
||||
"1.234",
|
||||
"12.340",
|
||||
SensorDeviceClass.PRESSURE,
|
||||
),
|
||||
(
|
||||
@ -600,7 +797,8 @@ async def test_custom_unit(
|
||||
UnitOfPressure.MMHG,
|
||||
UnitOfPressure.MMHG,
|
||||
1000,
|
||||
750,
|
||||
"1000",
|
||||
"750",
|
||||
SensorDeviceClass.PRESSURE,
|
||||
),
|
||||
# Not a supported pressure unit
|
||||
@ -609,7 +807,8 @@ async def test_custom_unit(
|
||||
"peer_pressure",
|
||||
UnitOfPressure.HPA,
|
||||
1000,
|
||||
1000,
|
||||
"1000",
|
||||
"1000",
|
||||
SensorDeviceClass.PRESSURE,
|
||||
),
|
||||
# Speed
|
||||
@ -618,7 +817,8 @@ async def test_custom_unit(
|
||||
UnitOfSpeed.MILES_PER_HOUR,
|
||||
UnitOfSpeed.MILES_PER_HOUR,
|
||||
100,
|
||||
62,
|
||||
"100",
|
||||
"62",
|
||||
SensorDeviceClass.SPEED,
|
||||
),
|
||||
(
|
||||
@ -626,7 +826,8 @@ async def test_custom_unit(
|
||||
UnitOfVolumetricFlux.INCHES_PER_HOUR,
|
||||
UnitOfVolumetricFlux.INCHES_PER_HOUR,
|
||||
78,
|
||||
0.13,
|
||||
"78",
|
||||
"0.13",
|
||||
SensorDeviceClass.SPEED,
|
||||
),
|
||||
(
|
||||
@ -634,7 +835,8 @@ async def test_custom_unit(
|
||||
"peer_distance",
|
||||
UnitOfSpeed.KILOMETERS_PER_HOUR,
|
||||
100,
|
||||
100,
|
||||
"100",
|
||||
"100",
|
||||
SensorDeviceClass.SPEED,
|
||||
),
|
||||
# Volume
|
||||
@ -643,7 +845,8 @@ async def test_custom_unit(
|
||||
UnitOfVolume.CUBIC_FEET,
|
||||
UnitOfVolume.CUBIC_FEET,
|
||||
100,
|
||||
3531,
|
||||
"100",
|
||||
"3531",
|
||||
SensorDeviceClass.VOLUME,
|
||||
),
|
||||
(
|
||||
@ -651,7 +854,8 @@ async def test_custom_unit(
|
||||
UnitOfVolume.FLUID_OUNCES,
|
||||
UnitOfVolume.FLUID_OUNCES,
|
||||
2.3,
|
||||
77.8,
|
||||
"2.3",
|
||||
"77.8",
|
||||
SensorDeviceClass.VOLUME,
|
||||
),
|
||||
(
|
||||
@ -659,7 +863,8 @@ async def test_custom_unit(
|
||||
"peer_distance",
|
||||
UnitOfVolume.CUBIC_METERS,
|
||||
100,
|
||||
100,
|
||||
"100",
|
||||
"100",
|
||||
SensorDeviceClass.VOLUME,
|
||||
),
|
||||
# Weight
|
||||
@ -668,7 +873,8 @@ async def test_custom_unit(
|
||||
UnitOfMass.OUNCES,
|
||||
UnitOfMass.OUNCES,
|
||||
100,
|
||||
3.5,
|
||||
"100",
|
||||
"3.5",
|
||||
SensorDeviceClass.WEIGHT,
|
||||
),
|
||||
(
|
||||
@ -676,7 +882,8 @@ async def test_custom_unit(
|
||||
UnitOfMass.GRAMS,
|
||||
UnitOfMass.GRAMS,
|
||||
78,
|
||||
2211,
|
||||
"78",
|
||||
"2211",
|
||||
SensorDeviceClass.WEIGHT,
|
||||
),
|
||||
(
|
||||
@ -684,7 +891,8 @@ async def test_custom_unit(
|
||||
"peer_distance",
|
||||
UnitOfMass.GRAMS,
|
||||
100,
|
||||
100,
|
||||
"100",
|
||||
"100",
|
||||
SensorDeviceClass.WEIGHT,
|
||||
),
|
||||
],
|
||||
@ -696,7 +904,8 @@ async def test_custom_unit_change(
|
||||
custom_unit,
|
||||
state_unit,
|
||||
native_value,
|
||||
custom_value,
|
||||
native_state,
|
||||
custom_state,
|
||||
device_class,
|
||||
):
|
||||
"""Test custom unit changes are picked up."""
|
||||
@ -716,7 +925,7 @@ async def test_custom_unit_change(
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get(entity0.entity_id)
|
||||
assert float(state.state) == approx(float(native_value))
|
||||
assert state.state == native_state
|
||||
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == native_unit
|
||||
|
||||
entity_registry.async_update_entity_options(
|
||||
@ -725,7 +934,7 @@ async def test_custom_unit_change(
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get(entity0.entity_id)
|
||||
assert float(state.state) == approx(float(custom_value))
|
||||
assert state.state == custom_state
|
||||
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == state_unit
|
||||
|
||||
entity_registry.async_update_entity_options(
|
||||
@ -734,19 +943,19 @@ async def test_custom_unit_change(
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get(entity0.entity_id)
|
||||
assert float(state.state) == approx(float(native_value))
|
||||
assert state.state == native_state
|
||||
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == native_unit
|
||||
|
||||
entity_registry.async_update_entity_options("sensor.test", "sensor", None)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get(entity0.entity_id)
|
||||
assert float(state.state) == approx(float(native_value))
|
||||
assert state.state == native_state
|
||||
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == native_unit
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"unit_system, native_unit, automatic_unit, suggested_unit, custom_unit, native_value, automatic_value, suggested_value, custom_value, device_class",
|
||||
"unit_system, native_unit, automatic_unit, suggested_unit, custom_unit, native_value, native_state, automatic_state, suggested_state, custom_state, device_class",
|
||||
[
|
||||
# Distance
|
||||
(
|
||||
@ -756,9 +965,10 @@ async def test_custom_unit_change(
|
||||
UnitOfLength.METERS,
|
||||
UnitOfLength.YARDS,
|
||||
1000,
|
||||
621,
|
||||
1000000,
|
||||
1093613,
|
||||
"1000",
|
||||
"621",
|
||||
"1000000",
|
||||
"1093613",
|
||||
SensorDeviceClass.DISTANCE,
|
||||
),
|
||||
],
|
||||
@ -772,9 +982,10 @@ async def test_unit_conversion_priority(
|
||||
suggested_unit,
|
||||
custom_unit,
|
||||
native_value,
|
||||
automatic_value,
|
||||
suggested_value,
|
||||
custom_value,
|
||||
native_state,
|
||||
automatic_state,
|
||||
suggested_state,
|
||||
custom_state,
|
||||
device_class,
|
||||
):
|
||||
"""Test priority of unit conversion."""
|
||||
@ -826,7 +1037,7 @@ async def test_unit_conversion_priority(
|
||||
|
||||
# Registered entity -> Follow automatic unit conversion
|
||||
state = hass.states.get(entity0.entity_id)
|
||||
assert float(state.state) == approx(float(automatic_value))
|
||||
assert state.state == automatic_state
|
||||
assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == automatic_unit
|
||||
# Assert the automatic unit conversion is stored in the registry
|
||||
entry = entity_registry.async_get(entity0.entity_id)
|
||||
@ -836,12 +1047,12 @@ async def test_unit_conversion_priority(
|
||||
|
||||
# Unregistered entity -> Follow native unit
|
||||
state = hass.states.get(entity1.entity_id)
|
||||
assert float(state.state) == approx(float(native_value))
|
||||
assert state.state == native_state
|
||||
assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == native_unit
|
||||
|
||||
# Registered entity with suggested unit
|
||||
state = hass.states.get(entity2.entity_id)
|
||||
assert float(state.state) == approx(float(suggested_value))
|
||||
assert state.state == suggested_state
|
||||
assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == suggested_unit
|
||||
# Assert the suggested unit is stored in the registry
|
||||
entry = entity_registry.async_get(entity2.entity_id)
|
||||
@ -851,7 +1062,7 @@ async def test_unit_conversion_priority(
|
||||
|
||||
# Unregistered entity with suggested unit
|
||||
state = hass.states.get(entity3.entity_id)
|
||||
assert float(state.state) == approx(float(suggested_value))
|
||||
assert state.state == suggested_state
|
||||
assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == suggested_unit
|
||||
|
||||
# Set a custom unit, this should have priority over the automatic unit conversion
|
||||
@ -861,7 +1072,7 @@ async def test_unit_conversion_priority(
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get(entity0.entity_id)
|
||||
assert float(state.state) == approx(float(custom_value))
|
||||
assert state.state == custom_state
|
||||
assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == custom_unit
|
||||
|
||||
entity_registry.async_update_entity_options(
|
||||
@ -870,7 +1081,7 @@ async def test_unit_conversion_priority(
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get(entity2.entity_id)
|
||||
assert float(state.state) == approx(float(custom_value))
|
||||
assert state.state == custom_state
|
||||
assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == custom_unit
|
||||
|
||||
|
||||
@ -964,7 +1175,7 @@ async def test_unit_conversion_priority_suggested_unit_change(
|
||||
UnitOfLength.KILOMETERS,
|
||||
UnitOfLength.MILES,
|
||||
1000,
|
||||
621,
|
||||
621.0,
|
||||
SensorDeviceClass.DISTANCE,
|
||||
),
|
||||
(
|
||||
@ -1219,7 +1430,7 @@ async def test_device_classes_with_invalid_unit_of_measurement(
|
||||
(date(2012, 11, 10), "2012-11-10"),
|
||||
],
|
||||
)
|
||||
async def test_non_numeric_validation(
|
||||
async def test_non_numeric_validation_warn(
|
||||
hass: HomeAssistant,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
enable_custom_integrations: None,
|
||||
@ -1253,6 +1464,51 @@ async def test_non_numeric_validation(
|
||||
) in caplog.text
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"device_class,state_class,unit,precision", ((None, None, None, 1),)
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
"native_value,expected",
|
||||
[
|
||||
("abc", "abc"),
|
||||
("13.7.1", "13.7.1"),
|
||||
(datetime(2012, 11, 10, 7, 35, 1), "2012-11-10 07:35:01"),
|
||||
(date(2012, 11, 10), "2012-11-10"),
|
||||
],
|
||||
)
|
||||
async def test_non_numeric_validation_raise(
|
||||
hass: HomeAssistant,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
enable_custom_integrations: None,
|
||||
native_value: Any,
|
||||
expected: str,
|
||||
device_class: SensorDeviceClass | None,
|
||||
state_class: SensorStateClass | None,
|
||||
unit: str | None,
|
||||
precision,
|
||||
) -> None:
|
||||
"""Test error on expected numeric entities."""
|
||||
platform = getattr(hass.components, "test.sensor")
|
||||
platform.init(empty=True)
|
||||
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,
|
||||
)
|
||||
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 is None
|
||||
|
||||
assert ("Error adding entities for domain sensor with platform test") in caplog.text
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"device_class,state_class,unit",
|
||||
[
|
||||
|
@ -98,6 +98,11 @@ class MockSensor(MockEntity, SensorEntity):
|
||||
"""Return the last_reset of this sensor."""
|
||||
return self._handle("last_reset")
|
||||
|
||||
@property
|
||||
def native_precision(self):
|
||||
"""Return the number of digits after the decimal point."""
|
||||
return self._handle("native_precision")
|
||||
|
||||
@property
|
||||
def native_unit_of_measurement(self):
|
||||
"""Return the native unit_of_measurement of this sensor."""
|
||||
|
Loading…
x
Reference in New Issue
Block a user