Add number entity to KNX (#51786)

Co-authored-by: Franck Nijhof <frenck@frenck.nl>
This commit is contained in:
Matthias Alphart 2021-06-23 17:20:49 +02:00 committed by GitHub
parent 4e88b44286
commit 742159a6a6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 178 additions and 5 deletions

View File

@ -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(),

View File

@ -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"

View 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)

View File

@ -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."""