Matthias Alphart d418a40856
Create, update and delete KNX entities from UI / WS-commands (#104079)
* knx entity CRUD - initial commit - switch

* platform dependent schema

* coerce empty GA-lists to None

* read entity configuration from WS

* use entity_id instead of unique_id for lookup

* Add device support

* Rename KNXEntityStore to KNXConfigStore

* fix test after rename

* Send schema options for creating / editing entities

* Return entity_id after entity creation

* remove device_class config in favour of more-info-dialog settings

* refactor group address schema for custom selector

* Rename GA keys and remove invalid keys from schema

* fix rebase

* Fix deleting devices and their entities

* Validate entity schema in extra step - return validation infos

* Use exception to signal validation error; return validated data

* Forward validation result when editing entities

* Get proper validation error message for optional GAs

* Add entity validation only WS command

* use ulid instead of uuid

* Fix error handling for edit unknown entity

* Remove unused optional group address sets from validated schema

* Add optional dpt field for ga_schema

* Move knx config things to sub-key

* Add light platform

* async_forward_entry_setups only once

* Test crate and remove devices

* Test removing entities of a removed device

* Test entity creation and storage

* Test deleting entities

* Test unsuccessful entity creation

* Test updating entity data

* Test get entity config

* Test validate entity

* Update entity data by entity_id instead of unique_id

* Remove unnecessary uid unique check

* remove schema_options

* test fixture for entity creation

* clean up group address schema

class can be used to add custom serializer later

* Revert: Add light platfrom

* remove unused optional_ga_schema

* Test GASelector

* lint tests

* Review

* group entities before adding

* fix / ignore mypy

* always has_entity_name

* Entity name: check for empty string when no device

* use constants instead of strings in schema

* Fix mypy errors for voluptuous schemas

---------

Co-authored-by: Marc Mueller <30130371+cdce8p@users.noreply.github.com>
2024-07-21 20:01:48 +02:00

106 lines
3.4 KiB
Python

"""Validation helpers for KNX config schemas."""
from collections.abc import Callable
import ipaddress
from typing import Any
import voluptuous as vol
from xknx.dpt import DPTBase, DPTNumeric, DPTString
from xknx.exceptions import CouldNotParseAddress
from xknx.telegram.address import IndividualAddress, parse_device_group_address
import homeassistant.helpers.config_validation as cv
def dpt_subclass_validator(dpt_base_class: type[DPTBase]) -> Callable[[Any], str | int]:
"""Validate that value is parsable as given sensor type."""
def dpt_value_validator(value: Any) -> str | int:
"""Validate that value is parsable as sensor type."""
if (
isinstance(value, (str, int))
and dpt_base_class.parse_transcoder(value) is not None
):
return value
raise vol.Invalid(
f"type '{value}' is not a valid DPT identifier for"
f" {dpt_base_class.__name__}."
)
return dpt_value_validator
numeric_type_validator = dpt_subclass_validator(DPTNumeric) # type: ignore[type-abstract]
sensor_type_validator = dpt_subclass_validator(DPTBase) # type: ignore[type-abstract]
string_type_validator = dpt_subclass_validator(DPTString)
def ga_validator(value: Any) -> str | int:
"""Validate that value is parsable as GroupAddress or InternalGroupAddress."""
if not isinstance(value, (str, int)):
raise vol.Invalid(
f"'{value}' is not a valid KNX group address: Invalid type '{type(value).__name__}'"
)
try:
parse_device_group_address(value)
except CouldNotParseAddress as exc:
raise vol.Invalid(
f"'{value}' is not a valid KNX group address: {exc.message}"
) from exc
return value
def maybe_ga_validator(value: Any) -> str | int | None:
"""Validate a group address or None."""
# this is a version of vol.Maybe(ga_validator) that delivers the
# error message of ga_validator if validation fails.
return ga_validator(value) if value is not None else None
ga_list_validator = vol.All(
cv.ensure_list,
[ga_validator],
vol.IsTrue("value must be a group address or a list containing group addresses"),
)
ga_list_validator_optional = vol.Maybe(
vol.All(
cv.ensure_list,
[ga_validator],
vol.Any(vol.IsTrue(), vol.SetTo(None)), # avoid empty lists -> None
)
)
ia_validator = vol.Any(
vol.All(str, str.strip, cv.matches_regex(IndividualAddress.ADDRESS_RE.pattern)),
vol.All(vol.Coerce(int), vol.Range(min=1, max=65535)),
msg=(
"value does not match pattern for KNX individual address"
" '<area>.<line>.<device>' (eg.'1.1.100')"
),
)
def ip_v4_validator(value: Any, multicast: bool | None = None) -> str:
"""Validate that value is parsable as IPv4 address.
Optionally check if address is in a reserved multicast block or is explicitly not.
"""
try:
address = ipaddress.IPv4Address(value)
except ipaddress.AddressValueError as ex:
raise vol.Invalid(f"value '{value}' is not a valid IPv4 address: {ex}") from ex
if multicast is not None and address.is_multicast != multicast:
raise vol.Invalid(
f"value '{value}' is not a valid IPv4"
f" {'multicast' if multicast else 'unicast'} address"
)
return str(address)
sync_state_validator = vol.Any(
vol.All(vol.Coerce(int), vol.Range(min=2, max=1440)),
cv.boolean,
cv.matches_regex(r"^(init|expire|every)( \d*)?$"),
)