855 lines
25 KiB
Python
855 lines
25 KiB
Python
"""Component to allow numeric input for platforms."""
|
|
from __future__ import annotations
|
|
|
|
from collections.abc import Callable
|
|
from contextlib import suppress
|
|
import dataclasses
|
|
from datetime import timedelta
|
|
import inspect
|
|
import logging
|
|
from math import ceil, floor
|
|
from typing import Any, Final, final
|
|
|
|
import voluptuous as vol
|
|
|
|
from homeassistant.backports.enum import StrEnum
|
|
from homeassistant.config_entries import ConfigEntry
|
|
from homeassistant.const import ATTR_MODE, CONF_UNIT_OF_MEASUREMENT, UnitOfTemperature
|
|
from homeassistant.core import HomeAssistant, ServiceCall, callback
|
|
from homeassistant.helpers.config_validation import ( # noqa: F401
|
|
PLATFORM_SCHEMA,
|
|
PLATFORM_SCHEMA_BASE,
|
|
)
|
|
from homeassistant.helpers.entity import Entity, EntityDescription
|
|
from homeassistant.helpers.entity_component import EntityComponent
|
|
from homeassistant.helpers.restore_state import ExtraStoredData, RestoreEntity
|
|
from homeassistant.helpers.typing import ConfigType
|
|
from homeassistant.util.unit_conversion import BaseUnitConverter, TemperatureConverter
|
|
|
|
from .const import (
|
|
ATTR_MAX,
|
|
ATTR_MIN,
|
|
ATTR_STEP,
|
|
ATTR_VALUE,
|
|
DEFAULT_MAX_VALUE,
|
|
DEFAULT_MIN_VALUE,
|
|
DEFAULT_STEP,
|
|
DOMAIN,
|
|
SERVICE_SET_VALUE,
|
|
)
|
|
|
|
SCAN_INTERVAL = timedelta(seconds=30)
|
|
|
|
ENTITY_ID_FORMAT = DOMAIN + ".{}"
|
|
|
|
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10)
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
|
|
class NumberDeviceClass(StrEnum):
|
|
"""Device class for numbers."""
|
|
|
|
# NumberDeviceClass should be aligned with SensorDeviceClass
|
|
|
|
APPARENT_POWER = "apparent_power"
|
|
"""Apparent power.
|
|
|
|
Unit of measurement: `VA`
|
|
"""
|
|
|
|
AQI = "aqi"
|
|
"""Air Quality Index.
|
|
|
|
Unit of measurement: `None`
|
|
"""
|
|
|
|
ATMOSPHERIC_PRESSURE = "atmospheric_pressure"
|
|
"""Atmospheric pressure.
|
|
|
|
Unit of measurement: `UnitOfPressure` units
|
|
"""
|
|
|
|
BATTERY = "battery"
|
|
"""Percentage of battery that is left.
|
|
|
|
Unit of measurement: `%`
|
|
"""
|
|
|
|
CO = "carbon_monoxide"
|
|
"""Carbon Monoxide gas concentration.
|
|
|
|
Unit of measurement: `ppm` (parts per million)
|
|
"""
|
|
|
|
CO2 = "carbon_dioxide"
|
|
"""Carbon Dioxide gas concentration.
|
|
|
|
Unit of measurement: `ppm` (parts per million)
|
|
"""
|
|
|
|
CURRENT = "current"
|
|
"""Current.
|
|
|
|
Unit of measurement: `A`
|
|
"""
|
|
|
|
DATA_RATE = "data_rate"
|
|
"""Data rate.
|
|
|
|
Unit of measurement: UnitOfDataRate
|
|
"""
|
|
|
|
DATA_SIZE = "data_size"
|
|
"""Data size.
|
|
|
|
Unit of measurement: UnitOfInformation
|
|
"""
|
|
|
|
DISTANCE = "distance"
|
|
"""Generic distance.
|
|
|
|
Unit of measurement: `LENGTH_*` units
|
|
- SI /metric: `mm`, `cm`, `m`, `km`
|
|
- USCS / imperial: `in`, `ft`, `yd`, `mi`
|
|
"""
|
|
|
|
ENERGY = "energy"
|
|
"""Energy.
|
|
|
|
Unit of measurement: `Wh`, `kWh`, `MWh`, `GJ`
|
|
"""
|
|
|
|
FREQUENCY = "frequency"
|
|
"""Frequency.
|
|
|
|
Unit of measurement: `Hz`, `kHz`, `MHz`, `GHz`
|
|
"""
|
|
|
|
GAS = "gas"
|
|
"""Gas.
|
|
|
|
Unit of measurement:
|
|
- SI / metric: `m³`
|
|
- USCS / imperial: `ft³`, `CCF`
|
|
"""
|
|
|
|
HUMIDITY = "humidity"
|
|
"""Relative humidity.
|
|
|
|
Unit of measurement: `%`
|
|
"""
|
|
|
|
ILLUMINANCE = "illuminance"
|
|
"""Illuminance.
|
|
|
|
Unit of measurement: `lx`
|
|
"""
|
|
|
|
IRRADIANCE = "irradiance"
|
|
"""Irradiance.
|
|
|
|
Unit of measurement:
|
|
- SI / metric: `W/m²`
|
|
- USCS / imperial: `BTU/(h⋅ft²)`
|
|
"""
|
|
|
|
MOISTURE = "moisture"
|
|
"""Moisture.
|
|
|
|
Unit of measurement: `%`
|
|
"""
|
|
|
|
MONETARY = "monetary"
|
|
"""Amount of money.
|
|
|
|
Unit of measurement: ISO4217 currency code
|
|
|
|
See https://en.wikipedia.org/wiki/ISO_4217#Active_codes for active codes
|
|
"""
|
|
|
|
NITROGEN_DIOXIDE = "nitrogen_dioxide"
|
|
"""Amount of NO2.
|
|
|
|
Unit of measurement: `µg/m³`
|
|
"""
|
|
|
|
NITROGEN_MONOXIDE = "nitrogen_monoxide"
|
|
"""Amount of NO.
|
|
|
|
Unit of measurement: `µg/m³`
|
|
"""
|
|
|
|
NITROUS_OXIDE = "nitrous_oxide"
|
|
"""Amount of N2O.
|
|
|
|
Unit of measurement: `µg/m³`
|
|
"""
|
|
|
|
OZONE = "ozone"
|
|
"""Amount of O3.
|
|
|
|
Unit of measurement: `µg/m³`
|
|
"""
|
|
|
|
PM1 = "pm1"
|
|
"""Particulate matter <= 0.1 μm.
|
|
|
|
Unit of measurement: `µg/m³`
|
|
"""
|
|
|
|
PM10 = "pm10"
|
|
"""Particulate matter <= 10 μm.
|
|
|
|
Unit of measurement: `µg/m³`
|
|
"""
|
|
|
|
PM25 = "pm25"
|
|
"""Particulate matter <= 2.5 μm.
|
|
|
|
Unit of measurement: `µg/m³`
|
|
"""
|
|
|
|
POWER_FACTOR = "power_factor"
|
|
"""Power factor.
|
|
|
|
Unit of measurement: `%`
|
|
"""
|
|
|
|
POWER = "power"
|
|
"""Power.
|
|
|
|
Unit of measurement: `W`, `kW`
|
|
"""
|
|
|
|
PRECIPITATION = "precipitation"
|
|
"""Precipitation.
|
|
|
|
Unit of measurement: UnitOfPrecipitationDepth
|
|
- SI / metric: `cm`, `mm`
|
|
- USCS / imperial: `in`
|
|
"""
|
|
|
|
PRECIPITATION_INTENSITY = "precipitation_intensity"
|
|
"""Precipitation intensity.
|
|
|
|
Unit of measurement: UnitOfVolumetricFlux
|
|
- SI /metric: `mm/d`, `mm/h`
|
|
- USCS / imperial: `in/d`, `in/h`
|
|
"""
|
|
|
|
PRESSURE = "pressure"
|
|
"""Pressure.
|
|
|
|
Unit of measurement:
|
|
- `mbar`, `cbar`, `bar`
|
|
- `Pa`, `hPa`, `kPa`
|
|
- `inHg`
|
|
- `psi`
|
|
"""
|
|
|
|
REACTIVE_POWER = "reactive_power"
|
|
"""Reactive power.
|
|
|
|
Unit of measurement: `var`
|
|
"""
|
|
|
|
SIGNAL_STRENGTH = "signal_strength"
|
|
"""Signal strength.
|
|
|
|
Unit of measurement: `dB`, `dBm`
|
|
"""
|
|
|
|
SOUND_PRESSURE = "sound_pressure"
|
|
"""Sound pressure.
|
|
|
|
Unit of measurement: `dB`, `dBA`
|
|
"""
|
|
|
|
SPEED = "speed"
|
|
"""Generic speed.
|
|
|
|
Unit of measurement: `SPEED_*` units or `UnitOfVolumetricFlux`
|
|
- SI /metric: `mm/d`, `mm/h`, `m/s`, `km/h`
|
|
- USCS / imperial: `in/d`, `in/h`, `ft/s`, `mph`
|
|
- Nautical: `kn`
|
|
"""
|
|
|
|
SULPHUR_DIOXIDE = "sulphur_dioxide"
|
|
"""Amount of SO2.
|
|
|
|
Unit of measurement: `µg/m³`
|
|
"""
|
|
|
|
TEMPERATURE = "temperature"
|
|
"""Temperature.
|
|
|
|
Unit of measurement: `°C`, `°F`
|
|
"""
|
|
|
|
VOLATILE_ORGANIC_COMPOUNDS = "volatile_organic_compounds"
|
|
"""Amount of VOC.
|
|
|
|
Unit of measurement: `µg/m³`
|
|
"""
|
|
|
|
VOLTAGE = "voltage"
|
|
"""Voltage.
|
|
|
|
Unit of measurement: `V`
|
|
"""
|
|
|
|
VOLUME = "volume"
|
|
"""Generic volume.
|
|
|
|
Unit of measurement: `VOLUME_*` units
|
|
- SI / metric: `mL`, `L`, `m³`
|
|
- USCS / imperial: `ft³`, `CCF`, `fl. oz.`, `gal` (warning: volumes expressed in
|
|
USCS/imperial units are currently assumed to be US volumes)
|
|
"""
|
|
|
|
WATER = "water"
|
|
"""Water.
|
|
|
|
Unit of measurement:
|
|
- SI / metric: `m³`, `L`
|
|
- USCS / imperial: `ft³`, `CCF`, `gal` (warning: volumes expressed in
|
|
USCS/imperial units are currently assumed to be US volumes)
|
|
"""
|
|
|
|
WEIGHT = "weight"
|
|
"""Generic weight, represents a measurement of an object's mass.
|
|
|
|
Weight is used instead of mass to fit with every day language.
|
|
|
|
Unit of measurement: `MASS_*` units
|
|
- SI / metric: `µg`, `mg`, `g`, `kg`
|
|
- USCS / imperial: `oz`, `lb`
|
|
"""
|
|
|
|
WIND_SPEED = "wind_speed"
|
|
"""Wind speed.
|
|
|
|
Unit of measurement: `SPEED_*` units
|
|
- SI /metric: `m/s`, `km/h`
|
|
- USCS / imperial: `ft/s`, `mph`
|
|
- Nautical: `kn`
|
|
"""
|
|
|
|
|
|
DEVICE_CLASSES_SCHEMA: Final = vol.All(vol.Lower, vol.Coerce(NumberDeviceClass))
|
|
|
|
|
|
class NumberMode(StrEnum):
|
|
"""Modes for number entities."""
|
|
|
|
AUTO = "auto"
|
|
BOX = "box"
|
|
SLIDER = "slider"
|
|
|
|
|
|
UNIT_CONVERTERS: dict[str, type[BaseUnitConverter]] = {
|
|
NumberDeviceClass.TEMPERATURE: TemperatureConverter,
|
|
}
|
|
|
|
# mypy: disallow-any-generics
|
|
|
|
|
|
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
|
"""Set up Number entities."""
|
|
component = hass.data[DOMAIN] = EntityComponent[NumberEntity](
|
|
_LOGGER, DOMAIN, hass, SCAN_INTERVAL
|
|
)
|
|
await component.async_setup(config)
|
|
|
|
component.async_register_entity_service(
|
|
SERVICE_SET_VALUE,
|
|
{vol.Required(ATTR_VALUE): vol.Coerce(float)},
|
|
async_set_value,
|
|
)
|
|
|
|
return True
|
|
|
|
|
|
async def async_set_value(entity: NumberEntity, service_call: ServiceCall) -> None:
|
|
"""Service call wrapper to set a new value."""
|
|
value = service_call.data["value"]
|
|
if value < entity.min_value or value > entity.max_value:
|
|
raise ValueError(
|
|
f"Value {value} for {entity.name} is outside valid range"
|
|
f" {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)
|
|
|
|
|
|
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|
"""Set up a config entry."""
|
|
component: EntityComponent[NumberEntity] = hass.data[DOMAIN]
|
|
return await component.async_setup_entry(entry)
|
|
|
|
|
|
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|
"""Unload a config entry."""
|
|
component: EntityComponent[NumberEntity] = hass.data[DOMAIN]
|
|
return await component.async_unload_entry(entry)
|
|
|
|
|
|
@dataclasses.dataclass
|
|
class NumberEntityDescription(EntityDescription):
|
|
"""A class that describes number entities."""
|
|
|
|
device_class: NumberDeviceClass | None = None
|
|
max_value: None = None
|
|
min_value: 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: None = None
|
|
unit_of_measurement: None = None # Type override, use native_unit_of_measurement
|
|
|
|
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
|
|
):
|
|
if self.__class__.__name__ == "NumberEntityDescription": # type: ignore[unreachable]
|
|
caller = inspect.stack()[2]
|
|
module = inspect.getmodule(caller[0])
|
|
else:
|
|
module = inspect.getmodule(self)
|
|
if module and module.__file__ and "custom_components" in module.__file__:
|
|
report_issue = "report it to the custom integration 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."""
|
|
|
|
entity_description: NumberEntityDescription
|
|
_attr_device_class: NumberDeviceClass | None
|
|
_attr_max_value: None
|
|
_attr_min_value: None
|
|
_attr_mode: NumberMode = NumberMode.AUTO
|
|
_attr_state: None = None
|
|
_attr_step: None
|
|
_attr_unit_of_measurement: None # Subclasses of NumberEntity should not set this
|
|
_attr_value: None
|
|
_attr_native_max_value: float
|
|
_attr_native_min_value: float
|
|
_attr_native_step: float
|
|
_attr_native_value: float | None = None
|
|
_attr_native_unit_of_measurement: str | None
|
|
_deprecated_number_entity_reported = False
|
|
_number_option_unit_of_measurement: str | None = None
|
|
|
|
def __init_subclass__(cls, **kwargs: Any) -> None:
|
|
"""Post initialisation processing."""
|
|
super().__init_subclass__(**kwargs)
|
|
if any(
|
|
method in cls.__dict__
|
|
for method in (
|
|
"async_set_value",
|
|
"max_value",
|
|
"min_value",
|
|
"set_value",
|
|
"step",
|
|
"unit_of_measurement",
|
|
"value",
|
|
)
|
|
):
|
|
module = inspect.getmodule(cls)
|
|
if module and module.__file__ and "custom_components" in module.__file__:
|
|
report_issue = "report it to the custom integration author."
|
|
else:
|
|
report_issue = (
|
|
"create a bug report at "
|
|
"https://github.com/home-assistant/core/issues?q=is%3Aopen+is%3Aissue"
|
|
)
|
|
_LOGGER.warning(
|
|
(
|
|
"%s::%s is overriding deprecated methods on an instance of "
|
|
"NumberEntity, this is not valid and will be unsupported "
|
|
"from Home Assistant 2022.10. Please %s"
|
|
),
|
|
cls.__module__,
|
|
cls.__name__,
|
|
report_issue,
|
|
)
|
|
|
|
async def async_internal_added_to_hass(self) -> None:
|
|
"""Call when the number entity is added to hass."""
|
|
await super().async_internal_added_to_hass()
|
|
if not self.registry_entry:
|
|
return
|
|
self.async_registry_entry_updated()
|
|
|
|
@property
|
|
def capability_attributes(self) -> dict[str, Any]:
|
|
"""Return capability attributes."""
|
|
return {
|
|
ATTR_MIN: self.min_value,
|
|
ATTR_MAX: self.max_value,
|
|
ATTR_STEP: self.step,
|
|
ATTR_MODE: self.mode,
|
|
}
|
|
|
|
@property
|
|
def device_class(self) -> NumberDeviceClass | None:
|
|
"""Return the class of this entity."""
|
|
if hasattr(self, "_attr_device_class"):
|
|
return self._attr_device_class
|
|
if hasattr(self, "entity_description"):
|
|
return self.entity_description.device_class
|
|
return None
|
|
|
|
@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
|
|
@final
|
|
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 # type: ignore[return-value]
|
|
if (
|
|
hasattr(self, "entity_description")
|
|
and self.entity_description.min_value is not None
|
|
):
|
|
self._report_deprecated_number_entity() # type: ignore[unreachable]
|
|
return self.entity_description.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
|
|
@final
|
|
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 # type: ignore[return-value]
|
|
if (
|
|
hasattr(self, "entity_description")
|
|
and self.entity_description.max_value is not None
|
|
):
|
|
self._report_deprecated_number_entity() # type: ignore[unreachable]
|
|
return self.entity_description.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, "entity_description")
|
|
and self.entity_description.native_step is not None
|
|
):
|
|
return self.entity_description.native_step
|
|
return None
|
|
|
|
@property
|
|
@final
|
|
def step(self) -> float:
|
|
"""Return the increment/decrement step."""
|
|
if hasattr(self, "_attr_step"):
|
|
self._report_deprecated_number_entity()
|
|
return self._attr_step # type: ignore[return-value]
|
|
if (
|
|
hasattr(self, "entity_description")
|
|
and self.entity_description.step is not None
|
|
):
|
|
self._report_deprecated_number_entity() # type: ignore[unreachable]
|
|
return self.entity_description.step
|
|
if hasattr(self, "_attr_native_step"):
|
|
return self._attr_native_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:
|
|
while value_range <= step:
|
|
step /= 10.0
|
|
return step
|
|
|
|
@property
|
|
def mode(self) -> NumberMode:
|
|
"""Return the mode of the entity."""
|
|
return self._attr_mode
|
|
|
|
@property
|
|
@final
|
|
def state(self) -> float | None:
|
|
"""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
|
|
@final
|
|
def unit_of_measurement(self) -> str | None:
|
|
"""Return the unit of measurement of the entity, after unit conversion."""
|
|
if self._number_option_unit_of_measurement:
|
|
return self._number_option_unit_of_measurement
|
|
|
|
if hasattr(self, "_attr_unit_of_measurement"):
|
|
self._report_deprecated_number_entity()
|
|
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 # type: ignore[unreachable]
|
|
|
|
native_unit_of_measurement = self.native_unit_of_measurement
|
|
|
|
if (
|
|
self.device_class == NumberDeviceClass.TEMPERATURE
|
|
and native_unit_of_measurement
|
|
in (UnitOfTemperature.CELSIUS, UnitOfTemperature.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 number."""
|
|
return self._attr_native_value
|
|
|
|
@property
|
|
@final
|
|
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)
|
|
|
|
@final
|
|
def set_value(self, value: float) -> None:
|
|
"""Set new value."""
|
|
raise NotImplementedError()
|
|
|
|
@final
|
|
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, int], float]
|
|
) -> 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_CONVERTERS
|
|
):
|
|
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_CONVERTERS[device_class].convert(
|
|
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_CONVERTERS
|
|
):
|
|
assert native_unit_of_measurement
|
|
assert unit_of_measurement
|
|
|
|
value = UNIT_CONVERTERS[device_class].convert(
|
|
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,
|
|
)
|
|
|
|
@callback
|
|
def async_registry_entry_updated(self) -> None:
|
|
"""Run when the entity registry entry has been updated."""
|
|
assert self.registry_entry
|
|
if (
|
|
(number_options := self.registry_entry.options.get(DOMAIN))
|
|
and (custom_unit := number_options.get(CONF_UNIT_OF_MEASUREMENT))
|
|
and (device_class := self.device_class) in UNIT_CONVERTERS
|
|
and self.native_unit_of_measurement
|
|
in UNIT_CONVERTERS[device_class].VALID_UNITS
|
|
and custom_unit in UNIT_CONVERTERS[device_class].VALID_UNITS
|
|
):
|
|
self._number_option_unit_of_measurement = custom_unit
|
|
return
|
|
|
|
self._number_option_unit_of_measurement = None
|
|
|
|
|
|
@dataclasses.dataclass
|
|
class NumberExtraStoredData(ExtraStoredData):
|
|
"""Object to hold extra stored data."""
|
|
|
|
native_max_value: float | None
|
|
native_min_value: float | None
|
|
native_step: float | None
|
|
native_unit_of_measurement: str | None
|
|
native_value: float | None
|
|
|
|
def as_dict(self) -> dict[str, Any]:
|
|
"""Return a dict representation of the number data."""
|
|
return dataclasses.asdict(self)
|
|
|
|
@classmethod
|
|
def from_dict(cls, restored: dict[str, Any]) -> NumberExtraStoredData | None:
|
|
"""Initialize a stored number state from a dict."""
|
|
try:
|
|
return cls(
|
|
restored["native_max_value"],
|
|
restored["native_min_value"],
|
|
restored["native_step"],
|
|
restored["native_unit_of_measurement"],
|
|
restored["native_value"],
|
|
)
|
|
except KeyError:
|
|
return None
|
|
|
|
|
|
class RestoreNumber(NumberEntity, RestoreEntity):
|
|
"""Mixin class for restoring previous number state."""
|
|
|
|
@property
|
|
def extra_restore_state_data(self) -> NumberExtraStoredData:
|
|
"""Return number specific state data to be restored."""
|
|
return NumberExtraStoredData(
|
|
self.native_max_value,
|
|
self.native_min_value,
|
|
self.native_step,
|
|
self.native_unit_of_measurement,
|
|
self.native_value,
|
|
)
|
|
|
|
async def async_get_last_number_data(self) -> NumberExtraStoredData | None:
|
|
"""Restore native_*."""
|
|
if (restored_last_extra_data := await self.async_get_last_extra_data()) is None:
|
|
return None
|
|
return NumberExtraStoredData.from_dict(restored_last_extra_data.as_dict())
|