mirror of
https://github.com/home-assistant/core.git
synced 2025-08-02 18:18:21 +00:00
Add unit compatibility to number entity platform
This commit is contained in:
parent
b132aef4b5
commit
9c859dbc3b
@ -31,6 +31,7 @@ from homeassistant.loader import async_suggest_report_issue
|
|||||||
from homeassistant.util.hass_dict import HassKey
|
from homeassistant.util.hass_dict import HassKey
|
||||||
|
|
||||||
from .const import ( # noqa: F401
|
from .const import ( # noqa: F401
|
||||||
|
AMBIGUOUS_UNITS,
|
||||||
ATTR_MAX,
|
ATTR_MAX,
|
||||||
ATTR_MIN,
|
ATTR_MIN,
|
||||||
ATTR_STEP,
|
ATTR_STEP,
|
||||||
@ -367,6 +368,15 @@ class NumberEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
|
|||||||
return self.entity_description.native_unit_of_measurement
|
return self.entity_description.native_unit_of_measurement
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
@final
|
||||||
|
@property
|
||||||
|
def __native_unit_of_measurement_compat(self) -> str | None:
|
||||||
|
"""Process ambiguous units."""
|
||||||
|
native_unit_of_measurement = self.native_unit_of_measurement
|
||||||
|
return AMBIGUOUS_UNITS.get(
|
||||||
|
native_unit_of_measurement, native_unit_of_measurement
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@final
|
@final
|
||||||
def unit_of_measurement(self) -> str | None:
|
def unit_of_measurement(self) -> str | None:
|
||||||
@ -374,7 +384,7 @@ class NumberEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
|
|||||||
if self._number_option_unit_of_measurement:
|
if self._number_option_unit_of_measurement:
|
||||||
return self._number_option_unit_of_measurement
|
return self._number_option_unit_of_measurement
|
||||||
|
|
||||||
native_unit_of_measurement = self.native_unit_of_measurement
|
native_unit_of_measurement = self.__native_unit_of_measurement_compat
|
||||||
# device_class is checked after native_unit_of_measurement since most
|
# device_class is checked after native_unit_of_measurement since most
|
||||||
# of the time we can avoid the device_class check
|
# of the time we can avoid the device_class check
|
||||||
if (
|
if (
|
||||||
@ -441,7 +451,7 @@ class NumberEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
|
|||||||
if device_class not in UNIT_CONVERTERS:
|
if device_class not in UNIT_CONVERTERS:
|
||||||
return value
|
return value
|
||||||
|
|
||||||
native_unit_of_measurement = self.native_unit_of_measurement
|
native_unit_of_measurement = self.__native_unit_of_measurement_compat
|
||||||
unit_of_measurement = self.unit_of_measurement
|
unit_of_measurement = self.unit_of_measurement
|
||||||
if native_unit_of_measurement != unit_of_measurement:
|
if native_unit_of_measurement != unit_of_measurement:
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
@ -470,7 +480,7 @@ class NumberEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
|
|||||||
if value is None or (device_class := self.device_class) not in UNIT_CONVERTERS:
|
if value is None or (device_class := self.device_class) not in UNIT_CONVERTERS:
|
||||||
return value
|
return value
|
||||||
|
|
||||||
native_unit_of_measurement = self.native_unit_of_measurement
|
native_unit_of_measurement = self.__native_unit_of_measurement_compat
|
||||||
unit_of_measurement = self.unit_of_measurement
|
unit_of_measurement = self.unit_of_measurement
|
||||||
if native_unit_of_measurement != unit_of_measurement:
|
if native_unit_of_measurement != unit_of_measurement:
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
@ -493,7 +503,7 @@ class NumberEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
|
|||||||
(number_options := self.registry_entry.options.get(DOMAIN))
|
(number_options := self.registry_entry.options.get(DOMAIN))
|
||||||
and (custom_unit := number_options.get(CONF_UNIT_OF_MEASUREMENT))
|
and (custom_unit := number_options.get(CONF_UNIT_OF_MEASUREMENT))
|
||||||
and (device_class := self.device_class) in UNIT_CONVERTERS
|
and (device_class := self.device_class) in UNIT_CONVERTERS
|
||||||
and self.native_unit_of_measurement
|
and self.__native_unit_of_measurement_compat
|
||||||
in UNIT_CONVERTERS[device_class].VALID_UNITS
|
in UNIT_CONVERTERS[device_class].VALID_UNITS
|
||||||
and custom_unit in UNIT_CONVERTERS[device_class].VALID_UNITS
|
and custom_unit in UNIT_CONVERTERS[device_class].VALID_UNITS
|
||||||
):
|
):
|
||||||
|
@ -8,6 +8,7 @@ from typing import Final
|
|||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
|
CONCENTRATION_MICROGRAMS_PER_CUBIC_FOOT,
|
||||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||||
CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER,
|
CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER,
|
||||||
CONCENTRATION_PARTS_PER_BILLION,
|
CONCENTRATION_PARTS_PER_BILLION,
|
||||||
@ -546,3 +547,14 @@ UNIT_CONVERTERS: dict[NumberDeviceClass, type[BaseUnitConverter]] = {
|
|||||||
NumberDeviceClass.TEMPERATURE: TemperatureConverter,
|
NumberDeviceClass.TEMPERATURE: TemperatureConverter,
|
||||||
NumberDeviceClass.VOLUME_FLOW_RATE: VolumeFlowRateConverter,
|
NumberDeviceClass.VOLUME_FLOW_RATE: VolumeFlowRateConverter,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
AMBIGUOUS_UNITS: dict[str | None, str] = {
|
||||||
|
"\u00b5Sv/h": "μSv/h", # aranet: radiation rate
|
||||||
|
"\u00b5S/cm": UnitOfConductivity.MICROSIEMENS_PER_CM,
|
||||||
|
"\u00b5V": UnitOfElectricPotential.MICROVOLT,
|
||||||
|
"\u00b5g/ft³": CONCENTRATION_MICROGRAMS_PER_CUBIC_FOOT,
|
||||||
|
"\u00b5g/m³": CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||||
|
"\u00b5mol/s⋅m²": "μmol/s⋅m²", # fyta: light
|
||||||
|
"\u00b5g": UnitOfMass.MICROGRAMS,
|
||||||
|
"\u00b5s": UnitOfTime.MICROSECONDS,
|
||||||
|
}
|
||||||
|
@ -476,7 +476,8 @@ class SensorEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
|
|||||||
"""Process ambiguous units."""
|
"""Process ambiguous units."""
|
||||||
native_unit_of_measurement = self.native_unit_of_measurement
|
native_unit_of_measurement = self.native_unit_of_measurement
|
||||||
return AMBIGUOUS_UNITS.get(
|
return AMBIGUOUS_UNITS.get(
|
||||||
native_unit_of_measurement, native_unit_of_measurement
|
native_unit_of_measurement,
|
||||||
|
native_unit_of_measurement,
|
||||||
)
|
)
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
|
@ -772,7 +772,7 @@ STATE_CLASS_UNITS: dict[SensorStateClass | str, set[type[StrEnum] | str | None]]
|
|||||||
SensorStateClass.MEASUREMENT_ANGLE: {DEGREE},
|
SensorStateClass.MEASUREMENT_ANGLE: {DEGREE},
|
||||||
}
|
}
|
||||||
|
|
||||||
AMBIGUOUS_UNITS: dict[str, str] = {
|
AMBIGUOUS_UNITS: dict[str | None, str] = {
|
||||||
"\u00b5Sv/h": "μSv/h", # aranet: radiation rate
|
"\u00b5Sv/h": "μSv/h", # aranet: radiation rate
|
||||||
"\u00b5S/cm": UnitOfConductivity.MICROSIEMENS_PER_CM,
|
"\u00b5S/cm": UnitOfConductivity.MICROSIEMENS_PER_CM,
|
||||||
"\u00b5V": UnitOfElectricPotential.MICROVOLT,
|
"\u00b5V": UnitOfElectricPotential.MICROVOLT,
|
||||||
|
@ -7,6 +7,7 @@ from unittest.mock import MagicMock, patch
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from homeassistant.components.number import (
|
from homeassistant.components.number import (
|
||||||
|
AMBIGUOUS_UNITS,
|
||||||
ATTR_MAX,
|
ATTR_MAX,
|
||||||
ATTR_MIN,
|
ATTR_MIN,
|
||||||
ATTR_MODE,
|
ATTR_MODE,
|
||||||
@ -48,6 +49,7 @@ from . import common
|
|||||||
|
|
||||||
from tests.common import (
|
from tests.common import (
|
||||||
MockConfigEntry,
|
MockConfigEntry,
|
||||||
|
MockEntity,
|
||||||
MockModule,
|
MockModule,
|
||||||
MockPlatform,
|
MockPlatform,
|
||||||
async_mock_restore_state_shutdown_restart,
|
async_mock_restore_state_shutdown_restart,
|
||||||
@ -61,6 +63,25 @@ from tests.common import (
|
|||||||
TEST_DOMAIN = "test"
|
TEST_DOMAIN = "test"
|
||||||
|
|
||||||
|
|
||||||
|
class MockNumber(MockEntity, NumberEntity):
|
||||||
|
"""Mock NumberEntity class to test unit of measurement."""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def device_class(self):
|
||||||
|
"""Return the class of this sensor."""
|
||||||
|
return self._handle("device_class")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def native_unit_of_measurement(self):
|
||||||
|
"""Return the native unit_of_measurement of this sensor."""
|
||||||
|
return self._handle("native_unit_of_measurement")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def native_value(self):
|
||||||
|
"""Return the native value of this sensor."""
|
||||||
|
return self._handle("native_value")
|
||||||
|
|
||||||
|
|
||||||
class MockDefaultNumberEntity(NumberEntity):
|
class MockDefaultNumberEntity(NumberEntity):
|
||||||
"""Mock NumberEntity device to use in tests.
|
"""Mock NumberEntity device to use in tests.
|
||||||
|
|
||||||
@ -900,6 +921,33 @@ async def test_translated_unit_with_native_unit_raises(
|
|||||||
assert entity0.entity_id is None
|
assert entity0.entity_id is None
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("ambiguous_unit", "normalized_unit"),
|
||||||
|
[
|
||||||
|
(ambiguous_unit, normalized_unit)
|
||||||
|
for ambiguous_unit, normalized_unit in AMBIGUOUS_UNITS.items()
|
||||||
|
],
|
||||||
|
)
|
||||||
|
async def test_ambiguous_unit_of_measurement_compat(
|
||||||
|
hass: HomeAssistant, ambiguous_unit: str, normalized_unit: str
|
||||||
|
) -> None:
|
||||||
|
"""Test ambiguous native_unit_of_measurement values are corrected."""
|
||||||
|
entity0 = MockNumber(
|
||||||
|
name="Test",
|
||||||
|
native_value="0.0",
|
||||||
|
native_unit_of_measurement=ambiguous_unit,
|
||||||
|
)
|
||||||
|
setup_test_component_platform(hass, DOMAIN, [entity0])
|
||||||
|
|
||||||
|
assert await async_setup_component(hass, "number", {"number": {"platform": "test"}})
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
# Check compatible unit is applied
|
||||||
|
state = hass.states.get(entity0.entity_id)
|
||||||
|
assert state.state == "0.0"
|
||||||
|
assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == normalized_unit
|
||||||
|
|
||||||
|
|
||||||
def test_device_classes_aligned() -> None:
|
def test_device_classes_aligned() -> None:
|
||||||
"""Make sure all sensor device classes are also available in NumberDeviceClass."""
|
"""Make sure all sensor device classes are also available in NumberDeviceClass."""
|
||||||
|
|
||||||
|
@ -159,7 +159,7 @@ async def test_temperature_conversion_wrong_device_class(
|
|||||||
assert await async_setup_component(hass, "sensor", {"sensor": {"platform": "test"}})
|
assert await async_setup_component(hass, "sensor", {"sensor": {"platform": "test"}})
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
# Check temperature is not converted
|
# Check compatible unit is applied
|
||||||
state = hass.states.get(entity0.entity_id)
|
state = hass.states.get(entity0.entity_id)
|
||||||
assert state.state == "0.0"
|
assert state.state == "0.0"
|
||||||
assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == UnitOfTemperature.FAHRENHEIT
|
assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == UnitOfTemperature.FAHRENHEIT
|
||||||
|
Loading…
x
Reference in New Issue
Block a user