Improve KNX config validation (#50980)

* remove dict repacking

* check binary_sensor device_class

* check cover device_class

* check sensor_type
This commit is contained in:
Matthias Alphart 2021-05-23 10:42:17 +02:00 committed by GitHub
parent 3141535d69
commit 5ca5b9ac89
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 27 additions and 16 deletions

View file

@ -6,7 +6,7 @@ from typing import Any
from xknx import XKNX from xknx import XKNX
from xknx.devices import BinarySensor as XknxBinarySensor from xknx.devices import BinarySensor as XknxBinarySensor
from homeassistant.components.binary_sensor import DEVICE_CLASSES, BinarySensorEntity from homeassistant.components.binary_sensor import BinarySensorEntity
from homeassistant.const import CONF_DEVICE_CLASS, CONF_NAME from homeassistant.const import CONF_DEVICE_CLASS, CONF_NAME
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
@ -64,9 +64,7 @@ class KNXBinarySensor(KnxEntity, BinarySensorEntity):
@property @property
def device_class(self) -> str | None: def device_class(self) -> str | None:
"""Return the class of this sensor.""" """Return the class of this sensor."""
if self._device_class in DEVICE_CLASSES: return self._device_class
return self._device_class
return None
@property @property
def is_on(self) -> bool: def is_on(self) -> bool:

View file

@ -12,7 +12,6 @@ from homeassistant.components.cover import (
ATTR_POSITION, ATTR_POSITION,
ATTR_TILT_POSITION, ATTR_TILT_POSITION,
DEVICE_CLASS_BLIND, DEVICE_CLASS_BLIND,
DEVICE_CLASSES,
SUPPORT_CLOSE, SUPPORT_CLOSE,
SUPPORT_CLOSE_TILT, SUPPORT_CLOSE_TILT,
SUPPORT_OPEN, SUPPORT_OPEN,
@ -127,7 +126,7 @@ class KNXCover(KnxEntity, CoverEntity):
@property @property
def device_class(self) -> str | None: def device_class(self) -> str | None:
"""Return the class of this device, from component DEVICE_CLASSES.""" """Return the class of this device, from component DEVICE_CLASSES."""
if self._device_class in DEVICE_CLASSES: if self._device_class:
return self._device_class return self._device_class
if self._device.supports_angle: if self._device.supports_angle:
return DEVICE_CLASS_BLIND return DEVICE_CLASS_BLIND

View file

@ -5,10 +5,15 @@ from typing import Any
import voluptuous as vol import voluptuous as vol
from xknx.devices.climate import SetpointShiftMode from xknx.devices.climate import SetpointShiftMode
from xknx.dpt import DPTBase
from xknx.exceptions import CouldNotParseAddress from xknx.exceptions import CouldNotParseAddress
from xknx.io import DEFAULT_MCAST_PORT from xknx.io import DEFAULT_MCAST_PORT
from xknx.telegram.address import IndividualAddress, parse_device_group_address from xknx.telegram.address import IndividualAddress, parse_device_group_address
from homeassistant.components.binary_sensor import (
DEVICE_CLASSES as BINARY_SENSOR_DEVICE_CLASSES,
)
from homeassistant.components.cover import DEVICE_CLASSES as COVER_DEVICE_CLASSES
from homeassistant.const import ( from homeassistant.const import (
CONF_DEVICE_CLASS, CONF_DEVICE_CLASS,
CONF_ENTITY_ID, CONF_ENTITY_ID,
@ -44,7 +49,8 @@ def ga_validator(value: Any) -> str | int:
except CouldNotParseAddress: except CouldNotParseAddress:
pass pass
raise vol.Invalid( raise vol.Invalid(
f"value '{value}' is not a valid KNX group address '<main>/<middle>/<sub>', '<main>/<sub>' or '<free>' (eg.'1/2/3', '9/234', '123'), nor xknx internal address 'i-<string>'." f"value '{value}' is not a valid KNX group address '<main>/<middle>/<sub>', '<main>/<sub>' "
"or '<free>' (eg.'1/2/3', '9/234', '123'), nor xknx internal address 'i-<string>'."
) )
@ -56,15 +62,20 @@ ia_validator = vol.Any(
msg="value does not match pattern for KNX individual address '<area>.<line>.<device>' (eg.'1.1.100')", msg="value does not match pattern for KNX individual address '<area>.<line>.<device>' (eg.'1.1.100')",
) )
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:
return value
raise vol.Invalid(f"value '{value}' is not a valid sensor type.")
sync_state_validator = vol.Any( sync_state_validator = vol.Any(
vol.All(vol.Coerce(int), vol.Range(min=2, max=1440)), vol.All(vol.Coerce(int), vol.Range(min=2, max=1440)),
cv.boolean, cv.boolean,
cv.matches_regex(r"^(init|expire|every)( \d*)?$"), cv.matches_regex(r"^(init|expire|every)( \d*)?$"),
) )
sensor_type_validator = vol.Any(int, str)
############## ##############
# CONNECTION # CONNECTION
############## ##############
@ -119,7 +130,7 @@ class BinarySensorSchema:
vol.Optional(CONF_CONTEXT_TIMEOUT): vol.All( vol.Optional(CONF_CONTEXT_TIMEOUT): vol.All(
vol.Coerce(float), vol.Range(min=0, max=10) vol.Coerce(float), vol.Range(min=0, max=10)
), ),
vol.Optional(CONF_DEVICE_CLASS): cv.string, vol.Optional(CONF_DEVICE_CLASS): vol.In(BINARY_SENSOR_DEVICE_CLASSES),
vol.Optional(CONF_RESET_AFTER): cv.positive_float, vol.Optional(CONF_RESET_AFTER): cv.positive_float,
} }
), ),
@ -222,10 +233,10 @@ class ClimateSchema:
CONF_ON_OFF_INVERT, default=DEFAULT_ON_OFF_INVERT CONF_ON_OFF_INVERT, default=DEFAULT_ON_OFF_INVERT
): cv.boolean, ): cv.boolean,
vol.Optional(CONF_OPERATION_MODES): vol.All( vol.Optional(CONF_OPERATION_MODES): vol.All(
cv.ensure_list, [vol.In({**PRESET_MODES})] cv.ensure_list, [vol.In(PRESET_MODES)]
), ),
vol.Optional(CONF_CONTROLLER_MODES): vol.All( vol.Optional(CONF_CONTROLLER_MODES): vol.All(
cv.ensure_list, [vol.In({**CONTROLLER_MODES})] cv.ensure_list, [vol.In(CONTROLLER_MODES)]
), ),
vol.Optional(CONF_MIN_TEMP): vol.Coerce(float), vol.Optional(CONF_MIN_TEMP): vol.Coerce(float),
vol.Optional(CONF_MAX_TEMP): vol.Coerce(float), vol.Optional(CONF_MAX_TEMP): vol.Coerce(float),
@ -280,7 +291,7 @@ class CoverSchema:
): cv.positive_float, ): cv.positive_float,
vol.Optional(CONF_INVERT_POSITION, default=False): cv.boolean, vol.Optional(CONF_INVERT_POSITION, default=False): cv.boolean,
vol.Optional(CONF_INVERT_ANGLE, default=False): cv.boolean, vol.Optional(CONF_INVERT_ANGLE, default=False): cv.boolean,
vol.Optional(CONF_DEVICE_CLASS): cv.string, vol.Optional(CONF_DEVICE_CLASS): vol.In(COVER_DEVICE_CLASSES),
} }
), ),
) )
@ -291,6 +302,7 @@ class ExposeSchema:
CONF_KNX_EXPOSE_TYPE = CONF_TYPE CONF_KNX_EXPOSE_TYPE = CONF_TYPE
CONF_KNX_EXPOSE_ATTRIBUTE = "attribute" CONF_KNX_EXPOSE_ATTRIBUTE = "attribute"
CONF_KNX_EXPOSE_BINARY = "binary"
CONF_KNX_EXPOSE_DEFAULT = "default" CONF_KNX_EXPOSE_DEFAULT = "default"
EXPOSE_TIME_TYPES = [ EXPOSE_TIME_TYPES = [
"time", "time",
@ -308,14 +320,16 @@ class ExposeSchema:
) )
EXPOSE_SENSOR_SCHEMA = vol.Schema( EXPOSE_SENSOR_SCHEMA = vol.Schema(
{ {
vol.Required(CONF_KNX_EXPOSE_TYPE): sensor_type_validator, vol.Required(CONF_KNX_EXPOSE_TYPE): vol.Any(
CONF_KNX_EXPOSE_BINARY, sensor_type_validator
),
vol.Required(KNX_ADDRESS): ga_validator, vol.Required(KNX_ADDRESS): ga_validator,
vol.Required(CONF_ENTITY_ID): cv.entity_id, vol.Required(CONF_ENTITY_ID): cv.entity_id,
vol.Optional(CONF_KNX_EXPOSE_ATTRIBUTE): cv.string, vol.Optional(CONF_KNX_EXPOSE_ATTRIBUTE): cv.string,
vol.Optional(CONF_KNX_EXPOSE_DEFAULT): cv.match_all, vol.Optional(CONF_KNX_EXPOSE_DEFAULT): cv.match_all,
} }
) )
SCHEMA = vol.Any(EXPOSE_TIME_SCHEMA, EXPOSE_SENSOR_SCHEMA) SCHEMA = vol.Any(EXPOSE_SENSOR_SCHEMA, EXPOSE_TIME_SCHEMA)
class FanSchema: class FanSchema: