Migrate InputNumber entity to inherit NumberEntity (#175772)

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
epenet
2026-07-09 18:47:32 +02:00
committed by GitHub
co-authored by Claude Opus 4.8
parent 2374291a8a
commit ab91e3202d
2 changed files with 67 additions and 73 deletions
@@ -6,7 +6,8 @@ from typing import Any, Self, override
import voluptuous as vol
from homeassistant.const import (
from homeassistant.components.number import NumberEntity
from homeassistant.const import ( # noqa: F401
ATTR_EDITABLE,
ATTR_MODE,
CONF_ICON,
@@ -149,7 +150,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
component.async_register_entity_service(
SERVICE_SET_VALUE,
{vol.Required(ATTR_VALUE): vol.Coerce(float)},
"async_set_value",
"async_set_native_value",
)
component.async_register_entity_service(SERVICE_INCREMENT, None, "async_increment")
@@ -199,20 +200,31 @@ class NumberStorageCollection(collection.DictStorageCollection):
return {CONF_ID: item[CONF_ID]} | update_data
class InputNumber(collection.CollectionEntity, RestoreEntity):
# pylint: disable-next=home-assistant-enforce-class-module
class InputNumber(collection.CollectionEntity, NumberEntity, RestoreEntity):
"""Representation of a slider."""
_unrecorded_attributes = frozenset(
{ATTR_EDITABLE, ATTR_MAX, ATTR_MIN, ATTR_MODE, ATTR_STEP}
)
_unrecorded_attributes = frozenset({ATTR_EDITABLE})
_attr_should_poll = False
editable: bool
def __init__(self, config: ConfigType) -> None:
"""Initialize an input number."""
self._config = config
self._current_value: float | None = config.get(CONF_INITIAL)
self._initial_value: float | None = config.get(CONF_INITIAL)
self._attr_native_value = self._initial_value
self._update_config_attributes(config)
def _update_config_attributes(self, config: ConfigType) -> None:
"""Update attributes based on the config."""
self._attr_icon = config.get(CONF_ICON)
self._attr_mode = config[CONF_MODE]
self._attr_name = config.get(CONF_NAME)
self._attr_native_min_value = config[CONF_MIN]
self._attr_native_max_value = config[CONF_MAX]
self._attr_native_step = config[CONF_STEP]
self._attr_unique_id = config[CONF_ID]
self._attr_native_unit_of_measurement = config.get(CONF_UNIT_OF_MEASUREMENT)
@classmethod
@override
@@ -231,69 +243,20 @@ class InputNumber(collection.CollectionEntity, RestoreEntity):
input_num.editable = False
return input_num
@property
def _minimum(self) -> float:
"""Return minimum allowed value."""
return self._config[CONF_MIN]
@property
def _maximum(self) -> float:
"""Return maximum allowed value."""
return self._config[CONF_MAX]
@property
@override
def name(self):
"""Return the name of the input slider."""
return self._config.get(CONF_NAME)
@property
@override
def icon(self) -> str | None:
"""Return the icon to be used for this entity."""
return self._config.get(CONF_ICON)
@property
@override
def state(self):
"""Return the state of the component."""
return self._current_value
@property
def _step(self) -> int:
"""Return entity's increment/decrement step."""
return self._config[CONF_STEP]
@property
@override
def unit_of_measurement(self):
"""Return the unit the value is expressed in."""
return self._config.get(CONF_UNIT_OF_MEASUREMENT)
@property
@override
def unique_id(self) -> str | None:
"""Return unique id of the entity."""
return self._config[CONF_ID]
@property
@override
def extra_state_attributes(self) -> dict[str, Any]:
"""Return the state attributes."""
return {
ATTR_INITIAL: self._config.get(CONF_INITIAL),
ATTR_INITIAL: self._initial_value,
ATTR_EDITABLE: self.editable,
ATTR_MIN: self._minimum,
ATTR_MAX: self._maximum,
ATTR_STEP: self._step,
ATTR_MODE: self._config[CONF_MODE],
}
@override
async def async_added_to_hass(self):
"""Run when entity about to be added to hass."""
await super().async_added_to_hass()
if self._current_value is not None:
if self._attr_native_value is not None:
return
value: float | None = None
@@ -302,39 +265,47 @@ class InputNumber(collection.CollectionEntity, RestoreEntity):
value = float(state.state)
# Check against None because value can be 0
if value is not None and self._minimum <= value <= self._maximum:
self._current_value = value
if (
value is not None
and self.native_min_value <= value <= self.native_max_value
):
self._attr_native_value = value
else:
self._current_value = self._minimum
self._attr_native_value = self.native_min_value
async def async_set_value(self, value):
@override
async def async_set_native_value(self, value):
"""Set new value."""
num_value = float(value)
if num_value < self._minimum or num_value > self._maximum:
if num_value < self.native_min_value or num_value > self.native_max_value:
raise vol.Invalid(
f"Invalid value for {self.entity_id}: {value} (range {self._minimum} -"
f" {self._maximum})"
f"Invalid value for {self.entity_id}: {value} (range "
f"{self.native_min_value} - {self.native_max_value})"
)
self._current_value = num_value
self._attr_native_value = num_value
self.async_write_ha_state()
async def async_increment(self):
"""Increment value."""
await self.async_set_value(min(self._current_value + self._step, self._maximum))
await self.async_set_native_value(
min(self._attr_native_value + self.native_step, self.native_max_value)
)
async def async_decrement(self):
"""Decrement value."""
await self.async_set_value(max(self._current_value - self._step, self._minimum))
await self.async_set_native_value(
max(self._attr_native_value - self.native_step, self.native_min_value)
)
@override
async def async_update_config(self, config: ConfigType) -> None:
"""Handle when the config is updated."""
self._config = config
self._update_config_attributes(config)
# just in case min/max values changed
if self._current_value is None:
if self._attr_native_value is None:
return
self._current_value = min(self._current_value, self._maximum)
self._current_value = max(self._current_value, self._minimum)
self._attr_native_value = min(self._attr_native_value, self.native_max_value)
self._attr_native_value = max(self._attr_native_value, self.native_min_value)
self.async_write_ha_state()
@@ -19,6 +19,7 @@ from homeassistant.const import (
ATTR_ENTITY_ID,
ATTR_FRIENDLY_NAME,
ATTR_NAME,
ATTR_UNIT_OF_MEASUREMENT,
)
from homeassistant.core import Context, CoreState, HomeAssistant, State
from homeassistant.exceptions import Unauthorized
@@ -237,6 +238,28 @@ async def test_mode(hass: HomeAssistant) -> None:
assert state.attributes["mode"] == "slider"
async def test_unit_of_measurement(hass: HomeAssistant) -> None:
"""Test unit of measurement is exposed in the state attributes."""
assert await async_setup_component(
hass,
DOMAIN,
{
DOMAIN: {
"with_unit": {"min": 0, "max": 100, "unit_of_measurement": "°C"},
"without_unit": {"min": 0, "max": 100},
}
},
)
state = hass.states.get("input_number.with_unit")
assert state
assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == "°C"
state = hass.states.get("input_number.without_unit")
assert state
assert ATTR_UNIT_OF_MEASUREMENT not in state.attributes
async def test_restore_state(hass: HomeAssistant) -> None:
"""Ensure states are restored on startup."""
mock_restore_cache(