Coverage for src / bluetooth_sig / gatt / descriptors / base.py: 87%
63 statements
« prev ^ index » next coverage.py v7.13.1, created at 2026-01-11 20:14 +0000
« prev ^ index » next coverage.py v7.13.1, created at 2026-01-11 20:14 +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 registry.
22 Provides parsing capabilities for descriptor values.
24 Attributes:
25 _descriptor_name: Optional explicit descriptor name for registry lookup.
26 _writable: Whether this descriptor type supports write operations.
27 Override to True in writable descriptor subclasses (CCCD, SCCD).
29 Note:
30 Most descriptors are read-only per Bluetooth SIG specification.
31 Some like CCCD (0x2902) and SCCD (0x2903) support writes.
32 """
34 # Class attributes for explicit overrides
35 _descriptor_name: str | None = None
36 _writable: bool = False # Override to True in writable descriptor subclasses
37 _info: DescriptorInfo # Populated in __post_init__
39 def __init__(self) -> None:
40 """Initialize descriptor with resolved information."""
41 self.__post_init__()
43 def __post_init__(self) -> None:
44 """Initialize descriptor with resolved information."""
45 self._info = self._resolve_info()
47 def _resolve_info(self) -> DescriptorInfo:
48 """Resolve descriptor information from registry using sophisticated name resolution."""
49 # Generate name variants using the same logic as characteristics
50 descriptor_name = getattr(self.__class__, "_descriptor_name", None)
51 variants = NameVariantGenerator.generate_descriptor_variants(self.__class__.__name__, descriptor_name)
53 # Try each variant
54 for variant in variants:
55 info = uuid_registry.get_descriptor_info(variant)
56 if info:
57 return info
59 # No resolution found
60 raise UUIDResolutionError(self.__class__.__name__, [self.__class__.__name__])
62 def _has_structured_data(self) -> bool:
63 """Check if this descriptor contains structured data."""
64 return False
66 def _get_data_format(self) -> str:
67 """Get the data format for this descriptor."""
68 return "bytes"
70 @property
71 def uuid(self) -> BluetoothUUID:
72 """Get the descriptor UUID."""
73 return self._info.uuid
75 @property
76 def name(self) -> str:
77 """Get the descriptor name."""
78 return self._info.name
80 @property
81 def info(self) -> DescriptorInfo:
82 """Get the descriptor information."""
83 return self._info
85 def parse_value(self, data: bytes) -> DescriptorData:
86 """Parse raw descriptor data into structured format.
88 Args:
89 data: Raw bytes from the descriptor read
91 Returns:
92 DescriptorData object with parsed value and metadata
93 """
94 try:
95 parsed_value = self._parse_descriptor_value(data)
96 return DescriptorData(
97 info=self._info,
98 value=parsed_value,
99 raw_data=data,
100 parse_success=True,
101 error_message="",
102 )
103 except Exception as e: # pylint: disable=broad-exception-caught
104 logger.warning("Failed to parse descriptor %s: %s", self.name, e)
105 return DescriptorData(
106 info=self._info,
107 value=None,
108 raw_data=data,
109 parse_success=False,
110 error_message=str(e),
111 )
113 def is_writable(self) -> bool:
114 """Check if descriptor type supports write operations.
116 Returns:
117 True if descriptor type supports writes, False otherwise.
119 Note:
120 Only checks descriptor type, not runtime permissions or security.
121 Example writable descriptors (CCCD, SCCD) override `_writable = True`.
122 """
123 return self._writable
125 @abstractmethod
126 def _parse_descriptor_value(self, data: bytes) -> Any: # noqa: ANN401 # Descriptors can return various types
127 """Parse the specific descriptor value format.
129 Args:
130 data: Raw bytes from the descriptor
132 Returns:
133 Parsed value in appropriate format
135 Raises:
136 NotImplementedError: If subclass doesn't implement parsing
137 """
138 raise NotImplementedError(f"{self.__class__.__name__} must implement _parse_descriptor_value()")
141class RangeDescriptorMixin:
142 """Mixin for descriptors that provide min/max value validation."""
144 def get_min_value(self, data: bytes) -> int | float:
145 """Get the minimum valid value.
147 Args:
148 data: Raw descriptor data
150 Returns:
151 Minimum valid value for the characteristic
152 """
153 parsed = self._parse_descriptor_value(data) # type: ignore[attr-defined]
154 return parsed.min_value # type: ignore[no-any-return]
156 def get_max_value(self, data: bytes) -> int | float:
157 """Get the maximum valid value.
159 Args:
160 data: Raw descriptor data
162 Returns:
163 Maximum valid value for the characteristic
164 """
165 parsed = self._parse_descriptor_value(data) # type: ignore[attr-defined]
166 return parsed.max_value # type: ignore[no-any-return]
168 def is_value_in_range(self, data: bytes, value: int | float) -> bool:
169 """Check if a value is within the valid range.
171 Args:
172 data: Raw descriptor data
173 value: Value to check
175 Returns:
176 True if value is within [min_value, max_value] range
177 """
178 parsed = self._parse_descriptor_value(data) # type: ignore[attr-defined]
179 min_val = parsed.min_value
180 max_val = parsed.max_value
181 return bool(min_val <= value <= max_val)