Home Assistant Unofficial Reference 2024.12.1
validation.py
Go to the documentation of this file.
1 """Validation helpers for KNX config schemas."""
2 
3 from collections.abc import Callable
4 from enum import Enum
5 import ipaddress
6 from typing import Any
7 
8 import voluptuous as vol
9 from xknx.dpt import DPTBase, DPTNumeric, DPTString
10 from xknx.exceptions import CouldNotParseAddress
11 from xknx.telegram.address import IndividualAddress, parse_device_group_address
12 
14 
15 
16 def dpt_subclass_validator(dpt_base_class: type[DPTBase]) -> Callable[[Any], str | int]:
17  """Validate that value is parsable as given sensor type."""
18 
19  def dpt_value_validator(value: Any) -> str | int:
20  """Validate that value is parsable as sensor type."""
21  if (
22  isinstance(value, (str, int))
23  and dpt_base_class.parse_transcoder(value) is not None
24  ):
25  return value
26  raise vol.Invalid(
27  f"type '{value}' is not a valid DPT identifier for"
28  f" {dpt_base_class.__name__}."
29  )
30 
31  return dpt_value_validator
32 
33 
34 dpt_base_type_validator = dpt_subclass_validator(DPTBase) # type: ignore[type-abstract]
35 numeric_type_validator = dpt_subclass_validator(DPTNumeric) # type: ignore[type-abstract]
36 string_type_validator = dpt_subclass_validator(DPTString)
37 sensor_type_validator = vol.Any(numeric_type_validator, string_type_validator)
38 
39 
40 def ga_validator(value: Any) -> str | int:
41  """Validate that value is parsable as GroupAddress or InternalGroupAddress."""
42  if not isinstance(value, (str, int)):
43  raise vol.Invalid(
44  f"'{value}' is not a valid KNX group address: Invalid type '{type(value).__name__}'"
45  )
46  try:
47  parse_device_group_address(value)
48  except CouldNotParseAddress as exc:
49  raise vol.Invalid(
50  f"'{value}' is not a valid KNX group address: {exc.message}"
51  ) from exc
52  return value
53 
54 
55 def maybe_ga_validator(value: Any) -> str | int | None:
56  """Validate a group address or None."""
57  # this is a version of vol.Maybe(ga_validator) that delivers the
58  # error message of ga_validator if validation fails.
59  return ga_validator(value) if value is not None else None
60 
61 
62 ga_list_validator = vol.All(
63  cv.ensure_list,
64  [ga_validator],
65  vol.IsTrue("value must be a group address or a list containing group addresses"),
66 )
67 
68 ga_list_validator_optional = vol.Maybe(
69  vol.All(
70  cv.ensure_list,
71  [ga_validator],
72  vol.Any(vol.IsTrue(), vol.SetTo(None)), # avoid empty lists -> None
73  )
74 )
75 
76 ia_validator = vol.Any(
77  vol.All(str, str.strip, cv.matches_regex(IndividualAddress.ADDRESS_RE.pattern)),
78  vol.All(vol.Coerce(int), vol.Range(min=1, max=65535)),
79  msg=(
80  "value does not match pattern for KNX individual address"
81  " '<area>.<line>.<device>' (eg.'1.1.100')"
82  ),
83 )
84 
85 
86 def ip_v4_validator(value: Any, multicast: bool | None = None) -> str:
87  """Validate that value is parsable as IPv4 address.
88 
89  Optionally check if address is in a reserved multicast block or is explicitly not.
90  """
91  try:
92  address = ipaddress.IPv4Address(value)
93  except ipaddress.AddressValueError as ex:
94  raise vol.Invalid(f"value '{value}' is not a valid IPv4 address: {ex}") from ex
95  if multicast is not None and address.is_multicast != multicast:
96  raise vol.Invalid(
97  f"value '{value}' is not a valid IPv4"
98  f" {'multicast' if multicast else 'unicast'} address"
99  )
100  return str(address)
101 
102 
103 sync_state_validator = vol.Any(
104  vol.All(vol.Coerce(int), vol.Range(min=2, max=1440)),
105  cv.boolean,
106  cv.matches_regex(r"^(init|expire|every)( \d*)?$"),
107 )
108 
109 
110 def backwards_compatible_xknx_climate_enum_member(enumClass: type[Enum]) -> vol.All:
111  """Transform a string to an enum member.
112 
113  Backwards compatible with member names of xknx 2.x climate DPT Enums
114  due to unintentional breaking change in HA 2024.8.
115  """
116 
117  def _string_transform(value: Any) -> str:
118  """Upper and slugify string and substitute old member names.
119 
120  Previously this was checked against Enum values instead of names. These
121  looked like `FAN_ONLY = "Fan only"`, therefore the upper & replace part.
122  """
123  if not isinstance(value, str):
124  raise vol.Invalid("value should be a string")
125  name = value.upper().replace(" ", "_")
126  match name:
127  case "NIGHT":
128  return "ECONOMY"
129  case "FROST_PROTECTION":
130  return "BUILDING_PROTECTION"
131  case "DRY":
132  return "DEHUMIDIFICATION"
133  case _:
134  return name
135 
136  return vol.All(
137  _string_transform,
138  vol.In(enumClass.__members__),
139  enumClass.__getitem__,
140  )
Callable[[Any], str|int] dpt_subclass_validator(type[DPTBase] dpt_base_class)
Definition: validation.py:16
str|int|None maybe_ga_validator(Any value)
Definition: validation.py:55
vol.All backwards_compatible_xknx_climate_enum_member(type[Enum] enumClass)
Definition: validation.py:110
str ip_v4_validator(Any value, bool|None multicast=None)
Definition: validation.py:86