Coverage for src/bluetooth_sig/gatt/descriptors/base.py: 87%
60 statements
« prev ^ index » next coverage.py v7.11.0, created at 2025-10-30 00:10 +0000
« prev ^ index » next coverage.py v7.11.0, created at 2025-10-30 00:10 +0000
1"""Base class for GATT descriptors."""
3from __future__ import annotations
5import logging
6from abc import ABC, abstractmethod
7from typing import Any
9from ...types import DescriptorData, DescriptorInfo
10from ...types.uuid import BluetoothUUID
11from ..exceptions import UUIDResolutionError
12from ..resolver import NameVariantGenerator
13from ..uuid_registry import uuid_registry
15logger = logging.getLogger(__name__)
18class BaseDescriptor(ABC):
19 """Base class for all GATT descriptors.
21 Automatically resolves UUID and name from Bluetooth SIG specifications.
22 Provides parsing capabilities for descriptor values.
23 """
25 # Class attributes for explicit overrides
26 _descriptor_name: str | None = None
27 _info: DescriptorInfo # Populated in __post_init__
29 def __init__(self) -> None:
30 """Initialize descriptor with resolved information."""
31 self.__post_init__()
33 def __post_init__(self) -> None:
34 """Initialize descriptor with resolved information."""
35 self._info = self._resolve_info()
37 def _resolve_info(self) -> DescriptorInfo:
38 """Resolve descriptor information from registry using sophisticated name resolution."""
39 # Generate name variants using the same logic as characteristics
40 descriptor_name = getattr(self.__class__, "_descriptor_name", None)
41 variants = NameVariantGenerator.generate_descriptor_variants(self.__class__.__name__, descriptor_name)
43 # Try each variant
44 for variant in variants:
45 info = uuid_registry.get_descriptor_info(variant)
46 if info:
47 return DescriptorInfo(
48 uuid=info.uuid,
49 name=info.name,
50 description=info.summary or "",
51 has_structured_data=self._has_structured_data(),
52 data_format=self._get_data_format(),
53 )
55 # No resolution found
56 raise UUIDResolutionError(self.__class__.__name__, [self.__class__.__name__])
58 def _has_structured_data(self) -> bool:
59 """Check if this descriptor contains structured data."""
60 return False
62 def _get_data_format(self) -> str:
63 """Get the data format for this descriptor."""
64 return "bytes"
66 @property
67 def uuid(self) -> BluetoothUUID:
68 """Get the descriptor UUID."""
69 return self._info.uuid
71 @property
72 def name(self) -> str:
73 """Get the descriptor name."""
74 return self._info.name
76 @property
77 def info(self) -> DescriptorInfo:
78 """Get the descriptor information."""
79 return self._info
81 def parse_value(self, data: bytes) -> DescriptorData:
82 """Parse raw descriptor data into structured format.
84 Args:
85 data: Raw bytes from the descriptor read
87 Returns:
88 DescriptorData object with parsed value and metadata
89 """
90 try:
91 parsed_value = self._parse_descriptor_value(data)
92 return DescriptorData(
93 info=self._info,
94 value=parsed_value,
95 raw_data=data,
96 parse_success=True,
97 error_message="",
98 )
99 except Exception as e: # pylint: disable=broad-exception-caught
100 logger.warning("Failed to parse descriptor %s: %s", self.name, e)
101 return DescriptorData(
102 info=self._info,
103 value=None,
104 raw_data=data,
105 parse_success=False,
106 error_message=str(e),
107 )
109 @abstractmethod
110 def _parse_descriptor_value(self, data: bytes) -> Any: # noqa: ANN401 # Descriptors can return various types
111 """Parse the specific descriptor value format.
113 Args:
114 data: Raw bytes from the descriptor
116 Returns:
117 Parsed value in appropriate format
119 Raises:
120 NotImplementedError: If subclass doesn't implement parsing
121 """
122 raise NotImplementedError(f"{self.__class__.__name__} must implement _parse_descriptor_value()")
125class RangeDescriptorMixin:
126 """Mixin for descriptors that provide min/max value validation."""
128 def get_min_value(self, data: bytes) -> int | float:
129 """Get the minimum valid value.
131 Args:
132 data: Raw descriptor data
134 Returns:
135 Minimum valid value for the characteristic
136 """
137 parsed = self._parse_descriptor_value(data) # type: ignore[attr-defined]
138 return parsed.min_value # type: ignore[no-any-return]
140 def get_max_value(self, data: bytes) -> int | float:
141 """Get the maximum valid value.
143 Args:
144 data: Raw descriptor data
146 Returns:
147 Maximum valid value for the characteristic
148 """
149 parsed = self._parse_descriptor_value(data) # type: ignore[attr-defined]
150 return parsed.max_value # type: ignore[no-any-return]
152 def is_value_in_range(self, data: bytes, value: int | float) -> bool:
153 """Check if a value is within the valid range.
155 Args:
156 data: Raw descriptor data
157 value: Value to check
159 Returns:
160 True if value is within [min_value, max_value] range
161 """
162 parsed = self._parse_descriptor_value(data) # type: ignore[attr-defined]
163 min_val = parsed.min_value
164 max_val = parsed.max_value
165 return bool(min_val <= value <= max_val)