mirror of
https://github.com/home-assistant/core.git
synced 2025-07-19 03:07:37 +00:00
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:
parent
e3b6c7a66f
commit
3da3503673
@ -1,16 +1,20 @@
|
||||
"""Component to allow numeric input for platforms."""
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from contextlib import suppress
|
||||
from dataclasses import dataclass
|
||||
from datetime import timedelta
|
||||
import inspect
|
||||
import logging
|
||||
from math import ceil, floor
|
||||
from typing import Any, final
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.backports.enum import StrEnum
|
||||
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.helpers.config_validation import ( # noqa: F401
|
||||
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_component import EntityComponent
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.util import temperature as temperature_util
|
||||
|
||||
from .const import (
|
||||
ATTR_MAX,
|
||||
@ -41,6 +46,13 @@ MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10)
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class NumberDeviceClass(StrEnum):
|
||||
"""Device class for numbers."""
|
||||
|
||||
# temperature (C/F)
|
||||
TEMPERATURE = "temperature"
|
||||
|
||||
|
||||
class NumberMode(StrEnum):
|
||||
"""Modes for number entities."""
|
||||
|
||||
@ -49,6 +61,11 @@ class NumberMode(StrEnum):
|
||||
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:
|
||||
"""Set up Number entities."""
|
||||
component = hass.data[DOMAIN] = EntityComponent(
|
||||
@ -72,6 +89,14 @@ async def async_set_value(entity: NumberEntity, service_call: ServiceCall) -> No
|
||||
raise ValueError(
|
||||
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)
|
||||
|
||||
|
||||
@ -93,8 +118,56 @@ class NumberEntityDescription(EntityDescription):
|
||||
|
||||
max_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
|
||||
|
||||
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):
|
||||
"""Representation of a Number entity."""
|
||||
@ -106,6 +179,12 @@ class NumberEntity(Entity):
|
||||
_attr_step: float
|
||||
_attr_mode: NumberMode = NumberMode.AUTO
|
||||
_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
|
||||
def capability_attributes(self) -> dict[str, Any]:
|
||||
@ -117,40 +196,84 @@ class NumberEntity(Entity):
|
||||
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
|
||||
def min_value(self) -> float:
|
||||
"""Return the minimum value."""
|
||||
if hasattr(self, "_attr_min_value"):
|
||||
self._report_deprecated_number_entity()
|
||||
return self._attr_min_value
|
||||
if (
|
||||
hasattr(self, "entity_description")
|
||||
and self.entity_description.min_value is not None
|
||||
):
|
||||
self._report_deprecated_number_entity()
|
||||
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
|
||||
def max_value(self) -> float:
|
||||
"""Return the maximum value."""
|
||||
if hasattr(self, "_attr_max_value"):
|
||||
self._report_deprecated_number_entity()
|
||||
return self._attr_max_value
|
||||
if (
|
||||
hasattr(self, "entity_description")
|
||||
and self.entity_description.max_value is not None
|
||||
):
|
||||
self._report_deprecated_number_entity()
|
||||
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
|
||||
def step(self) -> float:
|
||||
"""Return the increment/decrement step."""
|
||||
if hasattr(self, "_attr_step"):
|
||||
self._report_deprecated_number_entity()
|
||||
return self._attr_step
|
||||
if (
|
||||
hasattr(self, "entity_description")
|
||||
and self.entity_description.step is not None
|
||||
):
|
||||
self._report_deprecated_number_entity()
|
||||
return self.entity_description.step
|
||||
if (native_step := self.native_step) is not None:
|
||||
return native_step
|
||||
step = DEFAULT_STEP
|
||||
value_range = abs(self.max_value - self.min_value)
|
||||
if value_range != 0:
|
||||
@ -169,11 +292,60 @@ class NumberEntity(Entity):
|
||||
"""Return the entity state."""
|
||||
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
|
||||
def value(self) -> float | None:
|
||||
"""Return the entity value to represent the entity state."""
|
||||
if hasattr(self, "_attr_value"):
|
||||
self._report_deprecated_number_entity()
|
||||
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:
|
||||
"""Set new value."""
|
||||
raise NotImplementedError()
|
||||
@ -181,3 +353,69 @@ class NumberEntity(Entity):
|
||||
async def async_set_value(self, value: float) -> None:
|
||||
"""Set new 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,
|
||||
)
|
||||
|
@ -4,19 +4,138 @@ from unittest.mock import MagicMock
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.number import (
|
||||
ATTR_MAX,
|
||||
ATTR_MIN,
|
||||
ATTR_STEP,
|
||||
ATTR_VALUE,
|
||||
DOMAIN,
|
||||
SERVICE_SET_VALUE,
|
||||
NumberDeviceClass,
|
||||
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.setup import async_setup_component
|
||||
from homeassistant.util.unit_system import IMPERIAL_SYSTEM, METRIC_SYSTEM
|
||||
|
||||
|
||||
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
|
||||
def value(self):
|
||||
@ -24,13 +143,36 @@ class MockDefaultNumberEntity(NumberEntity):
|
||||
return 0.5
|
||||
|
||||
|
||||
class MockNumberEntity(NumberEntity):
|
||||
"""Mock NumberEntity device to use in tests."""
|
||||
class MockNumberEntityAttrDeprecated(NumberEntity):
|
||||
"""Mock NumberEntity device to use in tests.
|
||||
|
||||
@property
|
||||
def max_value(self) -> float:
|
||||
"""Return the max value."""
|
||||
return 1.0
|
||||
This class customizes min_value, max_value by setting _attr members.
|
||||
Step is calculated based on the smaller max_value and min_value.
|
||||
"""
|
||||
|
||||
_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
|
||||
def value(self):
|
||||
@ -41,12 +183,97 @@ class MockNumberEntity(NumberEntity):
|
||||
async def test_step(hass: HomeAssistant) -> None:
|
||||
"""Test the step calculation."""
|
||||
number = MockDefaultNumberEntity()
|
||||
number.hass = hass
|
||||
assert number.step == 1.0
|
||||
|
||||
number_2 = MockNumberEntity()
|
||||
number_2.hass = hass
|
||||
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:
|
||||
"""Test if async set_value calls sync set_value."""
|
||||
number = MockDefaultNumberEntity()
|
||||
@ -59,9 +286,7 @@ async def test_sync_set_value(hass: HomeAssistant) -> None:
|
||||
assert number.set_value.call_args[0][0] == 42
|
||||
|
||||
|
||||
async def test_custom_integration_and_validation(
|
||||
hass: HomeAssistant, enable_custom_integrations: None
|
||||
) -> None:
|
||||
async def test_set_value(hass: HomeAssistant, enable_custom_integrations: None) -> None:
|
||||
"""Test we can only set valid values."""
|
||||
platform = getattr(hass.components, f"test.{DOMAIN}")
|
||||
platform.init()
|
||||
@ -79,9 +304,8 @@ async def test_custom_integration_and_validation(
|
||||
{ATTR_VALUE: 60.0, ATTR_ENTITY_ID: "number.test"},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
hass.states.async_set("number.test", 60.0)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get("number.test")
|
||||
assert state.state == "60.0"
|
||||
|
||||
@ -97,3 +321,252 @@ async def test_custom_integration_and_validation(
|
||||
await hass.async_block_till_done()
|
||||
state = hass.states.get("number.test")
|
||||
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)
|
||||
|
@ -13,10 +13,55 @@ ENTITIES = []
|
||||
|
||||
|
||||
class MockNumberEntity(MockEntity, NumberEntity):
|
||||
"""Mock Select class."""
|
||||
"""Mock number class."""
|
||||
|
||||
_attr_value = 50.0
|
||||
_attr_step = 1.0
|
||||
@property
|
||||
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
|
||||
def value(self):
|
||||
@ -25,7 +70,7 @@ class MockNumberEntity(MockEntity, NumberEntity):
|
||||
|
||||
def set_value(self, value: float) -> None:
|
||||
"""Change the selected option."""
|
||||
self._attr_value = value
|
||||
self._values["value"] = value
|
||||
|
||||
|
||||
def init(empty=False):
|
||||
@ -39,6 +84,7 @@ def init(empty=False):
|
||||
MockNumberEntity(
|
||||
name="test",
|
||||
unique_id=UNIQUE_NUMBER,
|
||||
native_value=50.0,
|
||||
),
|
||||
]
|
||||
)
|
||||
|
Loading…
x
Reference in New Issue
Block a user