mirror of
https://github.com/home-assistant/core.git
synced 2025-07-19 03:07:37 +00:00
Add number entity to KNX (#51786)
Co-authored-by: Franck Nijhof <frenck@frenck.nl>
This commit is contained in:
parent
4e88b44286
commit
742159a6a6
@ -49,6 +49,7 @@ from .schema import (
|
||||
FanSchema,
|
||||
LightSchema,
|
||||
NotifySchema,
|
||||
NumberSchema,
|
||||
SceneSchema,
|
||||
SensorSchema,
|
||||
SwitchSchema,
|
||||
@ -93,6 +94,7 @@ CONFIG_SCHEMA = vol.Schema(
|
||||
**FanSchema.platform_node(),
|
||||
**LightSchema.platform_node(),
|
||||
**NotifySchema.platform_node(),
|
||||
**NumberSchema.platform_node(),
|
||||
**SceneSchema.platform_node(),
|
||||
**SensorSchema.platform_node(),
|
||||
**SwitchSchema.platform_node(),
|
||||
|
@ -26,14 +26,15 @@ DOMAIN: Final = "knx"
|
||||
# Address is used for configuration and services by the same functions so the key has to match
|
||||
KNX_ADDRESS: Final = "address"
|
||||
|
||||
CONF_KNX_ROUTING: Final = "routing"
|
||||
CONF_KNX_TUNNELING: Final = "tunneling"
|
||||
CONF_KNX_INDIVIDUAL_ADDRESS: Final = "individual_address"
|
||||
CONF_INVERT: Final = "invert"
|
||||
CONF_KNX_EXPOSE: Final = "expose"
|
||||
CONF_KNX_INDIVIDUAL_ADDRESS: Final = "individual_address"
|
||||
CONF_KNX_ROUTING: Final = "routing"
|
||||
CONF_KNX_TUNNELING: Final = "tunneling"
|
||||
CONF_RESET_AFTER: Final = "reset_after"
|
||||
CONF_RESPOND_TO_READ = "respond_to_read"
|
||||
CONF_STATE_ADDRESS: Final = "state_address"
|
||||
CONF_SYNC_STATE: Final = "sync_state"
|
||||
CONF_RESET_AFTER: Final = "reset_after"
|
||||
|
||||
ATTR_COUNTER: Final = "counter"
|
||||
ATTR_SOURCE: Final = "source"
|
||||
@ -56,6 +57,7 @@ class SupportedPlatforms(Enum):
|
||||
FAN = "fan"
|
||||
LIGHT = "light"
|
||||
NOTIFY = "notify"
|
||||
NUMBER = "number"
|
||||
SCENE = "scene"
|
||||
SENSOR = "sensor"
|
||||
SWITCH = "switch"
|
||||
|
96
homeassistant/components/knx/number.py
Normal file
96
homeassistant/components/knx/number.py
Normal file
@ -0,0 +1,96 @@
|
||||
"""Support for KNX/IP numeric values."""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import cast
|
||||
|
||||
from xknx import XKNX
|
||||
from xknx.devices import NumericValue
|
||||
|
||||
from homeassistant.components.number import NumberEntity
|
||||
from homeassistant.const import CONF_NAME, CONF_TYPE, STATE_UNAVAILABLE, STATE_UNKNOWN
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.restore_state import RestoreEntity
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
|
||||
from .const import CONF_RESPOND_TO_READ, CONF_STATE_ADDRESS, DOMAIN, KNX_ADDRESS
|
||||
from .knx_entity import KnxEntity
|
||||
from .schema import NumberSchema
|
||||
|
||||
|
||||
async def async_setup_platform(
|
||||
hass: HomeAssistant,
|
||||
config: ConfigType,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
discovery_info: DiscoveryInfoType | None = None,
|
||||
) -> None:
|
||||
"""Set up number entities for KNX platform."""
|
||||
if not discovery_info or not discovery_info["platform_config"]:
|
||||
return
|
||||
|
||||
platform_config = discovery_info["platform_config"]
|
||||
xknx: XKNX = hass.data[DOMAIN].xknx
|
||||
|
||||
async_add_entities(
|
||||
KNXNumber(xknx, entity_config) for entity_config in platform_config
|
||||
)
|
||||
|
||||
|
||||
def _create_numeric_value(xknx: XKNX, config: ConfigType) -> NumericValue:
|
||||
"""Return a KNX NumericValue to be used within XKNX."""
|
||||
return NumericValue(
|
||||
xknx,
|
||||
name=config[CONF_NAME],
|
||||
group_address=config[KNX_ADDRESS],
|
||||
group_address_state=config.get(CONF_STATE_ADDRESS),
|
||||
respond_to_read=config[CONF_RESPOND_TO_READ],
|
||||
value_type=config[CONF_TYPE],
|
||||
)
|
||||
|
||||
|
||||
class KNXNumber(KnxEntity, NumberEntity, RestoreEntity):
|
||||
"""Representation of a KNX number."""
|
||||
|
||||
def __init__(self, xknx: XKNX, config: ConfigType) -> None:
|
||||
"""Initialize a KNX number."""
|
||||
self._device: NumericValue
|
||||
super().__init__(_create_numeric_value(xknx, config))
|
||||
self._unique_id = f"{self._device.sensor_value.group_address}"
|
||||
|
||||
self._attr_min_value = config.get(
|
||||
NumberSchema.CONF_MIN,
|
||||
self._device.sensor_value.dpt_class.value_min,
|
||||
)
|
||||
self._attr_max_value = config.get(
|
||||
NumberSchema.CONF_MAX,
|
||||
self._device.sensor_value.dpt_class.value_max,
|
||||
)
|
||||
self._attr_step = config.get(
|
||||
NumberSchema.CONF_STEP,
|
||||
self._device.sensor_value.dpt_class.resolution,
|
||||
)
|
||||
self._device.sensor_value.value = max(0, self.min_value)
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Restore last state."""
|
||||
await super().async_added_to_hass()
|
||||
if not self._device.sensor_value.readable and (
|
||||
last_state := await self.async_get_last_state()
|
||||
):
|
||||
if last_state.state not in (STATE_UNKNOWN, STATE_UNAVAILABLE):
|
||||
self._device.sensor_value.value = float(last_state.state)
|
||||
|
||||
@property
|
||||
def value(self) -> float:
|
||||
"""Return the entity value to represent the entity state."""
|
||||
# self._device.sensor_value.value is set in __init__ so it is never None
|
||||
return cast(float, self._device.resolve_state())
|
||||
|
||||
async def async_set_value(self, value: float) -> None:
|
||||
"""Set new value."""
|
||||
if value < self.min_value or value > self.max_value:
|
||||
raise ValueError(
|
||||
f"Invalid value for {self.entity_id}: {value} "
|
||||
f"(range {self.min_value} - {self.max_value})"
|
||||
)
|
||||
await self._device.set(value)
|
@ -2,12 +2,13 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from abc import ABC
|
||||
from collections import OrderedDict
|
||||
from typing import Any, ClassVar
|
||||
|
||||
import voluptuous as vol
|
||||
from xknx import XKNX
|
||||
from xknx.devices.climate import SetpointShiftMode
|
||||
from xknx.dpt import DPTBase
|
||||
from xknx.dpt import DPTBase, DPTNumeric
|
||||
from xknx.exceptions import CouldNotParseAddress
|
||||
from xknx.io import DEFAULT_MCAST_GRP, DEFAULT_MCAST_PORT
|
||||
from xknx.telegram.address import IndividualAddress, parse_device_group_address
|
||||
@ -33,6 +34,7 @@ from .const import (
|
||||
CONF_KNX_ROUTING,
|
||||
CONF_KNX_TUNNELING,
|
||||
CONF_RESET_AFTER,
|
||||
CONF_RESPOND_TO_READ,
|
||||
CONF_STATE_ADDRESS,
|
||||
CONF_SYNC_STATE,
|
||||
CONTROLLER_MODES,
|
||||
@ -70,6 +72,50 @@ ia_validator = vol.Any(
|
||||
)
|
||||
|
||||
|
||||
def number_limit_sub_validator(entity_config: OrderedDict) -> OrderedDict:
|
||||
"""Validate a number entity configurations dependent on configured value type."""
|
||||
value_type = entity_config[CONF_TYPE]
|
||||
min_config: float | None = entity_config.get(NumberSchema.CONF_MIN)
|
||||
max_config: float | None = entity_config.get(NumberSchema.CONF_MAX)
|
||||
step_config: float | None = entity_config.get(NumberSchema.CONF_STEP)
|
||||
dpt_class = DPTNumeric.parse_transcoder(value_type)
|
||||
|
||||
if dpt_class is None:
|
||||
raise vol.Invalid(f"'type: {value_type}' is not a valid numeric sensor type.")
|
||||
# Inifinity is not supported by Home Assistant frontend so user defined
|
||||
# config is required if if xknx DPTNumeric subclass defines it as limit.
|
||||
if min_config is None and dpt_class.value_min == float("-inf"):
|
||||
raise vol.Invalid(f"'min' key required for value type '{value_type}'")
|
||||
if min_config is not None and min_config < dpt_class.value_min:
|
||||
raise vol.Invalid(
|
||||
f"'min: {min_config}' undercuts possible minimum"
|
||||
f" of value type '{value_type}': {dpt_class.value_min}"
|
||||
)
|
||||
|
||||
if max_config is None and dpt_class.value_max == float("inf"):
|
||||
raise vol.Invalid(f"'max' key required for value type '{value_type}'")
|
||||
if max_config is not None and max_config > dpt_class.value_max:
|
||||
raise vol.Invalid(
|
||||
f"'max: {max_config}' exceeds possible maximum"
|
||||
f" of value type '{value_type}': {dpt_class.value_max}"
|
||||
)
|
||||
|
||||
if step_config is not None and step_config < dpt_class.resolution:
|
||||
raise vol.Invalid(
|
||||
f"'step: {step_config}' undercuts possible minimum step"
|
||||
f" of value type '{value_type}': {dpt_class.resolution}"
|
||||
)
|
||||
|
||||
return entity_config
|
||||
|
||||
|
||||
def numeric_type_validator(value: Any) -> str | int:
|
||||
"""Validate that value is parsable as numeric sensor type."""
|
||||
if isinstance(value, (str, int)) and DPTNumeric.parse_transcoder(value) is not None:
|
||||
return value
|
||||
raise vol.Invalid(f"value '{value}' is not a valid numeric sensor type.")
|
||||
|
||||
|
||||
def sensor_type_validator(value: Any) -> str | int:
|
||||
"""Validate that value is parsable as sensor type."""
|
||||
if isinstance(value, (str, int)) and DPTBase.parse_transcoder(value) is not None:
|
||||
@ -525,6 +571,33 @@ class NotifySchema(KNXPlatformSchema):
|
||||
)
|
||||
|
||||
|
||||
class NumberSchema(KNXPlatformSchema):
|
||||
"""Voluptuous schema for KNX numbers."""
|
||||
|
||||
PLATFORM_NAME = SupportedPlatforms.NUMBER.value
|
||||
|
||||
CONF_MAX = "max"
|
||||
CONF_MIN = "min"
|
||||
CONF_STEP = "step"
|
||||
DEFAULT_NAME = "KNX Number"
|
||||
|
||||
ENTITY_SCHEMA = vol.All(
|
||||
vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_RESPOND_TO_READ, default=False): cv.boolean,
|
||||
vol.Required(CONF_TYPE): numeric_type_validator,
|
||||
vol.Required(KNX_ADDRESS): ga_list_validator,
|
||||
vol.Optional(CONF_STATE_ADDRESS): ga_list_validator,
|
||||
vol.Optional(CONF_MAX): vol.Coerce(float),
|
||||
vol.Optional(CONF_MIN): vol.Coerce(float),
|
||||
vol.Optional(CONF_STEP): cv.positive_float,
|
||||
}
|
||||
),
|
||||
number_limit_sub_validator,
|
||||
)
|
||||
|
||||
|
||||
class SceneSchema(KNXPlatformSchema):
|
||||
"""Voluptuous schema for KNX scenes."""
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user