from __future__ import annotations from collections.abc import Callable import logging import pathlib import typing import warnings import voluptuous as vol import zigpy.config import zigpy.types as t import zigpy.zdo.types as zdo_t if typing.TYPE_CHECKING: import zigpy.ota.providers _LOGGER = logging.getLogger(__name__) def cv_boolean(value: bool | int | str) -> bool: """Validate and coerce a boolean value.""" if isinstance(value, bool): return value if isinstance(value, str): value = value.lower().strip() if value in ("1", "true", "yes", "on", "enable"): return True if value in ("0", "false", "no", "off", "disable"): return False elif isinstance(value, int): return bool(value) raise vol.Invalid(f"invalid boolean '{value}' value") def cv_hex(value: int | str) -> int: """Convert string with possible hex number into int.""" if isinstance(value, int): return value if not isinstance(value, str): raise vol.Invalid(f"{value} is not a valid hex number") try: if value.startswith("0x"): value = int(value, base=16) else: value = int(value) except ValueError as err: raise vol.Invalid(f"Could not convert '{value}' to number") from err return value def cv_key(key: list[int]) -> t.KeyData: """Validate a key.""" if not isinstance(key, list) or not all(isinstance(v, int) for v in key): raise vol.Invalid("key must be a list of integers") if len(key) != 16: raise vol.Invalid("key length must be 16") if not all(0 <= e <= 255 for e in key): raise vol.Invalid("Key bytes must be within (0..255) range") return t.KeyData(key) def cv_simple_descriptor(obj: dict[str, typing.Any]) -> zdo_t.SimpleDescriptor: """Validates a ZDO simple descriptor.""" if not isinstance(obj, dict): raise vol.Invalid("Not a dictionary") descriptor = zdo_t.SimpleDescriptor(**obj) if not descriptor.is_valid: raise vol.Invalid(f"Invalid simple descriptor {descriptor!r}") return descriptor def cv_deprecated(message: str) -> typing.Callable[[typing.Any], typing.Any]: """Factory function for creating a deprecation warning validator.""" def wrapper(obj: typing.Any) -> typing.Any: _LOGGER.warning(message) warnings.warn(message, DeprecationWarning, stacklevel=2) return obj return wrapper def cv_json_file(value: str) -> pathlib.Path: """Validate a JSON file.""" path = pathlib.Path(value) if not path.is_file(): raise vol.Invalid(f"{value} is not a JSON file") return path def cv_folder(value: str) -> pathlib.Path: """Validate a folder path.""" path = pathlib.Path(value) if not path.is_dir(): raise vol.Invalid(f"{value} is not a directory") return path def cv_ota_provider_name(name: str | None) -> type[zigpy.ota.providers.BaseOtaProvider]: """Validate OTA provider name.""" import zigpy.ota.providers # noqa: PLC0415 if name not in zigpy.ota.providers.OTA_PROVIDER_TYPES: raise vol.Invalid(f"Unknown OTA provider: {name!r}") return zigpy.ota.providers.OTA_PROVIDER_TYPES[name] def cv_ota_provider(obj: dict) -> zigpy.ota.providers.BaseOtaProvider: """Validate OTA provider.""" provider_type = obj.get(zigpy.config.CONF_OTA_PROVIDER_TYPE) provider_cls = cv_ota_provider_name(provider_type) kwargs = provider_cls.VOL_SCHEMA(obj) kwargs.pop(zigpy.config.CONF_OTA_PROVIDER_TYPE) return provider_cls(**kwargs) def cv_warn_if_greater(limit: int, message: str) -> Callable[[int], int]: """Factory function for creating a warning validator.""" def wrapper(value: int) -> int: if value > limit: _LOGGER.warning(message) warnings.warn(message, UserWarning, stacklevel=2) return value return wrapper