Add temperature unit conversion support to NumberEntity (#73233)

* Add temperature unit conversion to number

* Remove type enforcements

* Lint

* Fix legacy unit_of_measurement

* Address review comments

* Fix unit_of_measurement, improve test coverage
This commit is contained in:
Erik Montnemery 2022-06-14 09:40:57 +02:00 committed by GitHub
parent e3b6c7a66f
commit 3da3503673
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 779 additions and 22 deletions

View File

@ -1,16 +1,20 @@
"""Component to allow numeric input for platforms.""" """Component to allow numeric input for platforms."""
from __future__ import annotations from __future__ import annotations
from collections.abc import Callable
from contextlib import suppress
from dataclasses import dataclass from dataclasses import dataclass
from datetime import timedelta from datetime import timedelta
import inspect
import logging import logging
from math import ceil, floor
from typing import Any, final from typing import Any, final
import voluptuous as vol import voluptuous as vol
from homeassistant.backports.enum import StrEnum from homeassistant.backports.enum import StrEnum
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_MODE from homeassistant.const import ATTR_MODE, TEMP_CELSIUS, TEMP_FAHRENHEIT
from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.core import HomeAssistant, ServiceCall
from homeassistant.helpers.config_validation import ( # noqa: F401 from homeassistant.helpers.config_validation import ( # noqa: F401
PLATFORM_SCHEMA, PLATFORM_SCHEMA,
@ -19,6 +23,7 @@ from homeassistant.helpers.config_validation import ( # noqa: F401
from homeassistant.helpers.entity import Entity, EntityDescription from homeassistant.helpers.entity import Entity, EntityDescription
from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.typing import ConfigType
from homeassistant.util import temperature as temperature_util
from .const import ( from .const import (
ATTR_MAX, ATTR_MAX,
@ -41,6 +46,13 @@ MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10)
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
class NumberDeviceClass(StrEnum):
"""Device class for numbers."""
# temperature (C/F)
TEMPERATURE = "temperature"
class NumberMode(StrEnum): class NumberMode(StrEnum):
"""Modes for number entities.""" """Modes for number entities."""
@ -49,6 +61,11 @@ class NumberMode(StrEnum):
SLIDER = "slider" SLIDER = "slider"
UNIT_CONVERSIONS: dict[str, Callable[[float, str, str], float]] = {
NumberDeviceClass.TEMPERATURE: temperature_util.convert,
}
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up Number entities.""" """Set up Number entities."""
component = hass.data[DOMAIN] = EntityComponent( component = hass.data[DOMAIN] = EntityComponent(
@ -72,6 +89,14 @@ async def async_set_value(entity: NumberEntity, service_call: ServiceCall) -> No
raise ValueError( raise ValueError(
f"Value {value} for {entity.name} is outside valid range {entity.min_value} - {entity.max_value}" f"Value {value} for {entity.name} is outside valid range {entity.min_value} - {entity.max_value}"
) )
try:
native_value = entity.convert_to_native_value(value)
# Clamp to the native range
native_value = min(
max(native_value, entity.native_min_value), entity.native_max_value
)
await entity.async_set_native_value(native_value)
except NotImplementedError:
await entity.async_set_value(value) await entity.async_set_value(value)
@ -93,8 +118,56 @@ class NumberEntityDescription(EntityDescription):
max_value: float | None = None max_value: float | None = None
min_value: float | None = None min_value: float | None = None
native_max_value: float | None = None
native_min_value: float | None = None
native_unit_of_measurement: str | None = None
native_step: float | None = None
step: float | None = None step: float | None = None
def __post_init__(self) -> None:
"""Post initialisation processing."""
if (
self.max_value is not None
or self.min_value is not None
or self.step is not None
or self.unit_of_measurement is not None
):
caller = inspect.stack()[2]
module = inspect.getmodule(caller[0])
if module and module.__file__ and "custom_components" in module.__file__:
report_issue = "report it to the custom component author."
else:
report_issue = (
"create a bug report at "
"https://github.com/home-assistant/core/issues?q=is%3Aopen+is%3Aissue"
)
_LOGGER.warning(
"%s is setting deprecated attributes on an instance of "
"NumberEntityDescription, this is not valid and will be unsupported "
"from Home Assistant 2022.10. Please %s",
module.__name__ if module else self.__class__.__name__,
report_issue,
)
self.native_unit_of_measurement = self.unit_of_measurement
def ceil_decimal(value: float, precision: float = 0) -> float:
"""Return the ceiling of f with d decimals.
This is a simple implementation which ignores floating point inexactness.
"""
factor = 10**precision
return ceil(value * factor) / factor
def floor_decimal(value: float, precision: float = 0) -> float:
"""Return the floor of f with d decimals.
This is a simple implementation which ignores floating point inexactness.
"""
factor = 10**precision
return floor(value * factor) / factor
class NumberEntity(Entity): class NumberEntity(Entity):
"""Representation of a Number entity.""" """Representation of a Number entity."""
@ -106,6 +179,12 @@ class NumberEntity(Entity):
_attr_step: float _attr_step: float
_attr_mode: NumberMode = NumberMode.AUTO _attr_mode: NumberMode = NumberMode.AUTO
_attr_value: float _attr_value: float
_attr_native_max_value: float
_attr_native_min_value: float
_attr_native_step: float
_attr_native_value: float
_attr_native_unit_of_measurement: str | None
_deprecated_number_entity_reported = False
@property @property
def capability_attributes(self) -> dict[str, Any]: def capability_attributes(self) -> dict[str, Any]:
@ -117,40 +196,84 @@ class NumberEntity(Entity):
ATTR_MODE: self.mode, ATTR_MODE: self.mode,
} }
@property
def native_min_value(self) -> float:
"""Return the minimum value."""
if hasattr(self, "_attr_native_min_value"):
return self._attr_native_min_value
if (
hasattr(self, "entity_description")
and self.entity_description.native_min_value is not None
):
return self.entity_description.native_min_value
return DEFAULT_MIN_VALUE
@property @property
def min_value(self) -> float: def min_value(self) -> float:
"""Return the minimum value.""" """Return the minimum value."""
if hasattr(self, "_attr_min_value"): if hasattr(self, "_attr_min_value"):
self._report_deprecated_number_entity()
return self._attr_min_value return self._attr_min_value
if ( if (
hasattr(self, "entity_description") hasattr(self, "entity_description")
and self.entity_description.min_value is not None and self.entity_description.min_value is not None
): ):
self._report_deprecated_number_entity()
return self.entity_description.min_value return self.entity_description.min_value
return DEFAULT_MIN_VALUE return self._convert_to_state_value(self.native_min_value, floor_decimal)
@property
def native_max_value(self) -> float:
"""Return the maximum value."""
if hasattr(self, "_attr_native_max_value"):
return self._attr_native_max_value
if (
hasattr(self, "entity_description")
and self.entity_description.native_max_value is not None
):
return self.entity_description.native_max_value
return DEFAULT_MAX_VALUE
@property @property
def max_value(self) -> float: def max_value(self) -> float:
"""Return the maximum value.""" """Return the maximum value."""
if hasattr(self, "_attr_max_value"): if hasattr(self, "_attr_max_value"):
self._report_deprecated_number_entity()
return self._attr_max_value return self._attr_max_value
if ( if (
hasattr(self, "entity_description") hasattr(self, "entity_description")
and self.entity_description.max_value is not None and self.entity_description.max_value is not None
): ):
self._report_deprecated_number_entity()
return self.entity_description.max_value return self.entity_description.max_value
return DEFAULT_MAX_VALUE return self._convert_to_state_value(self.native_max_value, ceil_decimal)
@property
def native_step(self) -> float | None:
"""Return the increment/decrement step."""
if hasattr(self, "_attr_native_step"):
return self._attr_native_step
if (
hasattr(self, "entity_description")
and self.entity_description.native_step is not None
):
return self.entity_description.native_step
return None
@property @property
def step(self) -> float: def step(self) -> float:
"""Return the increment/decrement step.""" """Return the increment/decrement step."""
if hasattr(self, "_attr_step"): if hasattr(self, "_attr_step"):
self._report_deprecated_number_entity()
return self._attr_step return self._attr_step
if ( if (
hasattr(self, "entity_description") hasattr(self, "entity_description")
and self.entity_description.step is not None and self.entity_description.step is not None
): ):
self._report_deprecated_number_entity()
return self.entity_description.step return self.entity_description.step
if (native_step := self.native_step) is not None:
return native_step
step = DEFAULT_STEP step = DEFAULT_STEP
value_range = abs(self.max_value - self.min_value) value_range = abs(self.max_value - self.min_value)
if value_range != 0: if value_range != 0:
@ -169,11 +292,60 @@ class NumberEntity(Entity):
"""Return the entity state.""" """Return the entity state."""
return self.value return self.value
@property
def native_unit_of_measurement(self) -> str | None:
"""Return the unit of measurement of the entity, if any."""
if hasattr(self, "_attr_native_unit_of_measurement"):
return self._attr_native_unit_of_measurement
if hasattr(self, "entity_description"):
return self.entity_description.native_unit_of_measurement
return None
@property
def unit_of_measurement(self) -> str | None:
"""Return the unit of measurement of the entity, after unit conversion."""
if hasattr(self, "_attr_unit_of_measurement"):
return self._attr_unit_of_measurement
if (
hasattr(self, "entity_description")
and self.entity_description.unit_of_measurement is not None
):
return self.entity_description.unit_of_measurement
native_unit_of_measurement = self.native_unit_of_measurement
if (
self.device_class == NumberDeviceClass.TEMPERATURE
and native_unit_of_measurement in (TEMP_CELSIUS, TEMP_FAHRENHEIT)
):
return self.hass.config.units.temperature_unit
return native_unit_of_measurement
@property
def native_value(self) -> float | None:
"""Return the value reported by the sensor."""
return self._attr_native_value
@property @property
def value(self) -> float | None: def value(self) -> float | None:
"""Return the entity value to represent the entity state.""" """Return the entity value to represent the entity state."""
if hasattr(self, "_attr_value"):
self._report_deprecated_number_entity()
return self._attr_value return self._attr_value
if (native_value := self.native_value) is None:
return native_value
return self._convert_to_state_value(native_value, round)
def set_native_value(self, value: float) -> None:
"""Set new value."""
raise NotImplementedError()
async def async_set_native_value(self, value: float) -> None:
"""Set new value."""
await self.hass.async_add_executor_job(self.set_native_value, value)
def set_value(self, value: float) -> None: def set_value(self, value: float) -> None:
"""Set new value.""" """Set new value."""
raise NotImplementedError() raise NotImplementedError()
@ -181,3 +353,69 @@ class NumberEntity(Entity):
async def async_set_value(self, value: float) -> None: async def async_set_value(self, value: float) -> None:
"""Set new value.""" """Set new value."""
await self.hass.async_add_executor_job(self.set_value, value) await self.hass.async_add_executor_job(self.set_value, value)
def _convert_to_state_value(self, value: float, method: Callable) -> float:
"""Convert a value in the number's native unit to the configured unit."""
native_unit_of_measurement = self.native_unit_of_measurement
unit_of_measurement = self.unit_of_measurement
device_class = self.device_class
if (
native_unit_of_measurement != unit_of_measurement
and device_class in UNIT_CONVERSIONS
):
assert native_unit_of_measurement
assert unit_of_measurement
value_s = str(value)
prec = len(value_s) - value_s.index(".") - 1 if "." in value_s else 0
# Suppress ValueError (Could not convert value to float)
with suppress(ValueError):
value_new: float = UNIT_CONVERSIONS[device_class](
value,
native_unit_of_measurement,
unit_of_measurement,
)
# Round to the wanted precision
value = method(value_new, prec)
return value
def convert_to_native_value(self, value: float) -> float:
"""Convert a value to the number's native unit."""
native_unit_of_measurement = self.native_unit_of_measurement
unit_of_measurement = self.unit_of_measurement
device_class = self.device_class
if (
value is not None
and native_unit_of_measurement != unit_of_measurement
and device_class in UNIT_CONVERSIONS
):
assert native_unit_of_measurement
assert unit_of_measurement
value = UNIT_CONVERSIONS[device_class](
value,
unit_of_measurement,
native_unit_of_measurement,
)
return value
def _report_deprecated_number_entity(self) -> None:
"""Report that the number entity has not been upgraded."""
if not self._deprecated_number_entity_reported:
self._deprecated_number_entity_reported = True
report_issue = self._suggest_report_issue()
_LOGGER.warning(
"Entity %s (%s) is using deprecated NumberEntity features which will "
"be unsupported from Home Assistant Core 2022.10, please %s",
self.entity_id,
type(self),
report_issue,
)

View File

@ -4,19 +4,138 @@ from unittest.mock import MagicMock
import pytest import pytest
from homeassistant.components.number import ( from homeassistant.components.number import (
ATTR_MAX,
ATTR_MIN,
ATTR_STEP, ATTR_STEP,
ATTR_VALUE, ATTR_VALUE,
DOMAIN, DOMAIN,
SERVICE_SET_VALUE, SERVICE_SET_VALUE,
NumberDeviceClass,
NumberEntity, NumberEntity,
NumberEntityDescription,
)
from homeassistant.const import (
ATTR_ENTITY_ID,
ATTR_UNIT_OF_MEASUREMENT,
CONF_PLATFORM,
TEMP_CELSIUS,
TEMP_FAHRENHEIT,
) )
from homeassistant.const import ATTR_ENTITY_ID, CONF_PLATFORM
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.setup import async_setup_component from homeassistant.setup import async_setup_component
from homeassistant.util.unit_system import IMPERIAL_SYSTEM, METRIC_SYSTEM
class MockDefaultNumberEntity(NumberEntity): class MockDefaultNumberEntity(NumberEntity):
"""Mock NumberEntity device to use in tests.""" """Mock NumberEntity device to use in tests.
This class falls back on defaults for min_value, max_value, step.
"""
@property
def native_value(self):
"""Return the current value."""
return 0.5
class MockNumberEntity(NumberEntity):
"""Mock NumberEntity device to use in tests.
This class customizes min_value, max_value as overridden methods.
Step is calculated based on the smaller max_value and min_value.
"""
@property
def native_max_value(self) -> float:
"""Return the max value."""
return 0.5
@property
def native_min_value(self) -> float:
"""Return the min value."""
return -0.5
@property
def native_unit_of_measurement(self):
"""Return the current value."""
return "native_cats"
@property
def native_value(self):
"""Return the current value."""
return 0.5
class MockNumberEntityAttr(NumberEntity):
"""Mock NumberEntity device to use in tests.
This class customizes min_value, max_value by setting _attr members.
Step is calculated based on the smaller max_value and min_value.
"""
_attr_native_max_value = 1000.0
_attr_native_min_value = -1000.0
_attr_native_step = 100.0
_attr_native_unit_of_measurement = "native_dogs"
_attr_native_value = 500.0
class MockNumberEntityDescr(NumberEntity):
"""Mock NumberEntity device to use in tests.
This class customizes min_value, max_value by entity description.
Step is calculated based on the smaller max_value and min_value.
"""
def __init__(self):
"""Initialize the clas instance."""
self.entity_description = NumberEntityDescription(
"test",
native_max_value=10.0,
native_min_value=-10.0,
native_step=2.0,
native_unit_of_measurement="native_rabbits",
)
@property
def native_value(self):
"""Return the current value."""
return None
class MockDefaultNumberEntityDeprecated(NumberEntity):
"""Mock NumberEntity device to use in tests.
This class falls back on defaults for min_value, max_value, step.
"""
@property
def native_value(self):
"""Return the current value."""
return 0.5
class MockNumberEntityDeprecated(NumberEntity):
"""Mock NumberEntity device to use in tests.
This class customizes min_value, max_value as overridden methods.
Step is calculated based on the smaller max_value and min_value.
"""
@property
def max_value(self) -> float:
"""Return the max value."""
return 0.5
@property
def min_value(self) -> float:
"""Return the min value."""
return -0.5
@property
def unit_of_measurement(self):
"""Return the current value."""
return "cats"
@property @property
def value(self): def value(self):
@ -24,13 +143,36 @@ class MockDefaultNumberEntity(NumberEntity):
return 0.5 return 0.5
class MockNumberEntity(NumberEntity): class MockNumberEntityAttrDeprecated(NumberEntity):
"""Mock NumberEntity device to use in tests.""" """Mock NumberEntity device to use in tests.
@property This class customizes min_value, max_value by setting _attr members.
def max_value(self) -> float: Step is calculated based on the smaller max_value and min_value.
"""Return the max value.""" """
return 1.0
_attr_max_value = 1000.0
_attr_min_value = -1000.0
_attr_step = 100.0
_attr_unit_of_measurement = "dogs"
_attr_value = 500.0
class MockNumberEntityDescrDeprecated(NumberEntity):
"""Mock NumberEntity device to use in tests.
This class customizes min_value, max_value by entity description.
Step is calculated based on the smaller max_value and min_value.
"""
def __init__(self):
"""Initialize the clas instance."""
self.entity_description = NumberEntityDescription(
"test",
max_value=10.0,
min_value=-10.0,
step=2.0,
unit_of_measurement="rabbits",
)
@property @property
def value(self): def value(self):
@ -41,12 +183,97 @@ class MockNumberEntity(NumberEntity):
async def test_step(hass: HomeAssistant) -> None: async def test_step(hass: HomeAssistant) -> None:
"""Test the step calculation.""" """Test the step calculation."""
number = MockDefaultNumberEntity() number = MockDefaultNumberEntity()
number.hass = hass
assert number.step == 1.0 assert number.step == 1.0
number_2 = MockNumberEntity() number_2 = MockNumberEntity()
number_2.hass = hass
assert number_2.step == 0.1 assert number_2.step == 0.1
async def test_attributes(hass: HomeAssistant) -> None:
"""Test the attributes."""
number = MockDefaultNumberEntity()
number.hass = hass
assert number.max_value == 100.0
assert number.min_value == 0.0
assert number.step == 1.0
assert number.unit_of_measurement is None
assert number.value == 0.5
number_2 = MockNumberEntity()
number_2.hass = hass
assert number_2.max_value == 0.5
assert number_2.min_value == -0.5
assert number_2.step == 0.1
assert number_2.unit_of_measurement == "native_cats"
assert number_2.value == 0.5
number_3 = MockNumberEntityAttr()
number_3.hass = hass
assert number_3.max_value == 1000.0
assert number_3.min_value == -1000.0
assert number_3.step == 100.0
assert number_3.unit_of_measurement == "native_dogs"
assert number_3.value == 500.0
number_4 = MockNumberEntityDescr()
number_4.hass = hass
assert number_4.max_value == 10.0
assert number_4.min_value == -10.0
assert number_4.step == 2.0
assert number_4.unit_of_measurement == "native_rabbits"
assert number_4.value is None
async def test_attributes_deprecated(hass: HomeAssistant, caplog) -> None:
"""Test overriding the deprecated attributes."""
number = MockDefaultNumberEntityDeprecated()
number.hass = hass
assert number.max_value == 100.0
assert number.min_value == 0.0
assert number.step == 1.0
assert number.unit_of_measurement is None
assert number.value == 0.5
number_2 = MockNumberEntityDeprecated()
number_2.hass = hass
assert number_2.max_value == 0.5
assert number_2.min_value == -0.5
assert number_2.step == 0.1
assert number_2.unit_of_measurement == "cats"
assert number_2.value == 0.5
number_3 = MockNumberEntityAttrDeprecated()
number_3.hass = hass
assert number_3.max_value == 1000.0
assert number_3.min_value == -1000.0
assert number_3.step == 100.0
assert number_3.unit_of_measurement == "dogs"
assert number_3.value == 500.0
number_4 = MockNumberEntityDescrDeprecated()
number_4.hass = hass
assert number_4.max_value == 10.0
assert number_4.min_value == -10.0
assert number_4.step == 2.0
assert number_4.unit_of_measurement == "rabbits"
assert number_4.value == 0.5
assert (
"Entity None (<class 'tests.components.number.test_init.MockNumberEntityAttrDeprecated'>) "
"is using deprecated NumberEntity features" in caplog.text
)
assert (
"Entity None (<class 'tests.components.number.test_init.MockNumberEntityDescrDeprecated'>) "
"is using deprecated NumberEntity features" in caplog.text
)
assert (
"tests.components.number.test_init is setting deprecated attributes on an "
"instance of NumberEntityDescription" in caplog.text
)
async def test_sync_set_value(hass: HomeAssistant) -> None: async def test_sync_set_value(hass: HomeAssistant) -> None:
"""Test if async set_value calls sync set_value.""" """Test if async set_value calls sync set_value."""
number = MockDefaultNumberEntity() number = MockDefaultNumberEntity()
@ -59,9 +286,7 @@ async def test_sync_set_value(hass: HomeAssistant) -> None:
assert number.set_value.call_args[0][0] == 42 assert number.set_value.call_args[0][0] == 42
async def test_custom_integration_and_validation( async def test_set_value(hass: HomeAssistant, enable_custom_integrations: None) -> None:
hass: HomeAssistant, enable_custom_integrations: None
) -> None:
"""Test we can only set valid values.""" """Test we can only set valid values."""
platform = getattr(hass.components, f"test.{DOMAIN}") platform = getattr(hass.components, f"test.{DOMAIN}")
platform.init() platform.init()
@ -79,9 +304,8 @@ async def test_custom_integration_and_validation(
{ATTR_VALUE: 60.0, ATTR_ENTITY_ID: "number.test"}, {ATTR_VALUE: 60.0, ATTR_ENTITY_ID: "number.test"},
blocking=True, blocking=True,
) )
hass.states.async_set("number.test", 60.0)
await hass.async_block_till_done() await hass.async_block_till_done()
state = hass.states.get("number.test") state = hass.states.get("number.test")
assert state.state == "60.0" assert state.state == "60.0"
@ -97,3 +321,252 @@ async def test_custom_integration_and_validation(
await hass.async_block_till_done() await hass.async_block_till_done()
state = hass.states.get("number.test") state = hass.states.get("number.test")
assert state.state == "60.0" assert state.state == "60.0"
async def test_deprecated_attributes(
hass: HomeAssistant, enable_custom_integrations: None
) -> None:
"""Test entity using deprecated attributes."""
platform = getattr(hass.components, f"test.{DOMAIN}")
platform.init(empty=True)
platform.ENTITIES.append(platform.LegacyMockNumberEntity())
entity = platform.ENTITIES[0]
entity._attr_name = "Test"
entity._attr_max_value = 25
entity._attr_min_value = -25
entity._attr_step = 2.5
entity._attr_value = 51.0
assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}})
await hass.async_block_till_done()
state = hass.states.get("number.test")
assert state.state == "51.0"
assert state.attributes.get(ATTR_MAX) == 25.0
assert state.attributes.get(ATTR_MIN) == -25.0
assert state.attributes.get(ATTR_STEP) == 2.5
await hass.services.async_call(
DOMAIN,
SERVICE_SET_VALUE,
{ATTR_VALUE: 0.0, ATTR_ENTITY_ID: "number.test"},
blocking=True,
)
await hass.async_block_till_done()
state = hass.states.get("number.test")
assert state.state == "0.0"
# test ValueError trigger
with pytest.raises(ValueError):
await hass.services.async_call(
DOMAIN,
SERVICE_SET_VALUE,
{ATTR_VALUE: 110.0, ATTR_ENTITY_ID: "number.test"},
blocking=True,
)
await hass.async_block_till_done()
state = hass.states.get("number.test")
assert state.state == "0.0"
async def test_deprecated_methods(
hass: HomeAssistant, enable_custom_integrations: None
) -> None:
"""Test entity using deprecated methods."""
platform = getattr(hass.components, f"test.{DOMAIN}")
platform.init(empty=True)
platform.ENTITIES.append(
platform.LegacyMockNumberEntity(
name="Test",
max_value=25.0,
min_value=-25.0,
step=2.5,
value=51.0,
)
)
assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}})
await hass.async_block_till_done()
state = hass.states.get("number.test")
assert state.state == "51.0"
assert state.attributes.get(ATTR_MAX) == 25.0
assert state.attributes.get(ATTR_MIN) == -25.0
assert state.attributes.get(ATTR_STEP) == 2.5
await hass.services.async_call(
DOMAIN,
SERVICE_SET_VALUE,
{ATTR_VALUE: 0.0, ATTR_ENTITY_ID: "number.test"},
blocking=True,
)
await hass.async_block_till_done()
state = hass.states.get("number.test")
assert state.state == "0.0"
# test ValueError trigger
with pytest.raises(ValueError):
await hass.services.async_call(
DOMAIN,
SERVICE_SET_VALUE,
{ATTR_VALUE: 110.0, ATTR_ENTITY_ID: "number.test"},
blocking=True,
)
await hass.async_block_till_done()
state = hass.states.get("number.test")
assert state.state == "0.0"
@pytest.mark.parametrize(
"unit_system, native_unit, state_unit, initial_native_value, initial_state_value, "
"updated_native_value, updated_state_value, native_max_value, state_max_value, "
"native_min_value, state_min_value, native_step, state_step",
[
(
IMPERIAL_SYSTEM,
TEMP_FAHRENHEIT,
TEMP_FAHRENHEIT,
100,
100,
50,
50,
140,
140,
-9,
-9,
3,
3,
),
(
IMPERIAL_SYSTEM,
TEMP_CELSIUS,
TEMP_FAHRENHEIT,
38,
100,
10,
50,
60,
140,
-23,
-10,
3,
3,
),
(
METRIC_SYSTEM,
TEMP_FAHRENHEIT,
TEMP_CELSIUS,
100,
38,
50,
10,
140,
60,
-9,
-23,
3,
3,
),
(
METRIC_SYSTEM,
TEMP_CELSIUS,
TEMP_CELSIUS,
38,
38,
10,
10,
60,
60,
-23,
-23,
3,
3,
),
],
)
async def test_temperature_conversion(
hass,
enable_custom_integrations,
unit_system,
native_unit,
state_unit,
initial_native_value,
initial_state_value,
updated_native_value,
updated_state_value,
native_max_value,
state_max_value,
native_min_value,
state_min_value,
native_step,
state_step,
):
"""Test temperature conversion."""
hass.config.units = unit_system
platform = getattr(hass.components, f"test.{DOMAIN}")
platform.init(empty=True)
platform.ENTITIES.append(
platform.MockNumberEntity(
name="Test",
native_max_value=native_max_value,
native_min_value=native_min_value,
native_step=native_step,
native_unit_of_measurement=native_unit,
native_value=initial_native_value,
device_class=NumberDeviceClass.TEMPERATURE,
)
)
entity0 = platform.ENTITIES[0]
assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}})
await hass.async_block_till_done()
state = hass.states.get(entity0.entity_id)
assert float(state.state) == pytest.approx(float(initial_state_value))
assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == state_unit
assert state.attributes[ATTR_MAX] == state_max_value
assert state.attributes[ATTR_MIN] == state_min_value
assert state.attributes[ATTR_STEP] == state_step
await hass.services.async_call(
DOMAIN,
SERVICE_SET_VALUE,
{ATTR_VALUE: updated_state_value, ATTR_ENTITY_ID: entity0.entity_id},
blocking=True,
)
await hass.async_block_till_done()
state = hass.states.get(entity0.entity_id)
assert float(state.state) == pytest.approx(float(updated_state_value))
assert entity0._values["native_value"] == updated_native_value
# Set to the minimum value
await hass.services.async_call(
DOMAIN,
SERVICE_SET_VALUE,
{ATTR_VALUE: state_min_value, ATTR_ENTITY_ID: entity0.entity_id},
blocking=True,
)
await hass.async_block_till_done()
state = hass.states.get(entity0.entity_id)
assert float(state.state) == pytest.approx(float(state_min_value), rel=0.1)
# Set to the maximum value
await hass.services.async_call(
DOMAIN,
SERVICE_SET_VALUE,
{ATTR_VALUE: state_max_value, ATTR_ENTITY_ID: entity0.entity_id},
blocking=True,
)
await hass.async_block_till_done()
state = hass.states.get(entity0.entity_id)
assert float(state.state) == pytest.approx(float(state_max_value), rel=0.1)

View File

@ -13,10 +13,55 @@ ENTITIES = []
class MockNumberEntity(MockEntity, NumberEntity): class MockNumberEntity(MockEntity, NumberEntity):
"""Mock Select class.""" """Mock number class."""
_attr_value = 50.0 @property
_attr_step = 1.0 def native_max_value(self):
"""Return the native native_max_value."""
return self._handle("native_max_value")
@property
def native_min_value(self):
"""Return the native native_min_value."""
return self._handle("native_min_value")
@property
def native_step(self):
"""Return the native native_step."""
return self._handle("native_step")
@property
def native_unit_of_measurement(self):
"""Return the native unit_of_measurement."""
return self._handle("native_unit_of_measurement")
@property
def native_value(self):
"""Return the native value of this sensor."""
return self._handle("native_value")
def set_native_value(self, value: float) -> None:
"""Change the selected option."""
self._values["native_value"] = value
class LegacyMockNumberEntity(MockEntity, NumberEntity):
"""Mock Number class using deprecated features."""
@property
def max_value(self):
"""Return the native max_value."""
return self._handle("max_value")
@property
def min_value(self):
"""Return the native min_value."""
return self._handle("min_value")
@property
def step(self):
"""Return the native step."""
return self._handle("step")
@property @property
def value(self): def value(self):
@ -25,7 +70,7 @@ class MockNumberEntity(MockEntity, NumberEntity):
def set_value(self, value: float) -> None: def set_value(self, value: float) -> None:
"""Change the selected option.""" """Change the selected option."""
self._attr_value = value self._values["value"] = value
def init(empty=False): def init(empty=False):
@ -39,6 +84,7 @@ def init(empty=False):
MockNumberEntity( MockNumberEntity(
name="test", name="test",
unique_id=UNIQUE_NUMBER, unique_id=UNIQUE_NUMBER,
native_value=50.0,
), ),
] ]
) )