Coverage for src / bluetooth_sig / gatt / characteristics / descriptor_mixin.py: 81%
43 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-03-18 11:17 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-03-18 11:17 +0000
1"""Descriptor support mixin for GATT characteristics.
3Provides all descriptor-related methods (add, get, CCCD, context lookups)
4as a mixin that :class:`BaseCharacteristic` inherits from.
5"""
7from __future__ import annotations
9from ...types.registry.descriptor_types import DescriptorData
10from ...types.uuid import BluetoothUUID
11from ..context import CharacteristicContext
12from ..descriptor_utils import enhance_error_message_with_descriptors as _enhance_error_message
13from ..descriptor_utils import get_descriptor_from_context as _get_descriptor
14from ..descriptor_utils import get_presentation_format_from_context as _get_presentation_format
15from ..descriptor_utils import get_user_description_from_context as _get_user_description
16from ..descriptor_utils import get_valid_range_from_context as _get_valid_range
17from ..descriptor_utils import validate_value_against_descriptor_range as _validate_value_range
18from ..descriptors import BaseDescriptor
19from ..descriptors.cccd import CCCDDescriptor
20from ..descriptors.characteristic_presentation_format import CharacteristicPresentationFormatData
23class DescriptorMixin:
24 """Mixin providing descriptor management and context lookup helpers.
26 Expects the consuming class to initialise ``_descriptors`` as an empty
27 ``dict[str, BaseDescriptor]`` in ``__init__``.
28 """
30 _descriptors: dict[str, BaseDescriptor]
32 # ------------------------------------------------------------------
33 # Instance descriptor management
34 # ------------------------------------------------------------------
36 def add_descriptor(self, descriptor: BaseDescriptor) -> None:
37 """Add a descriptor to this characteristic.
39 Args:
40 descriptor: The descriptor instance to add.
41 """
42 self._descriptors[str(descriptor.uuid)] = descriptor
44 def get_descriptor(self, uuid: str | BluetoothUUID) -> BaseDescriptor | None:
45 """Get a descriptor by UUID.
47 Args:
48 uuid: Descriptor UUID (string or BluetoothUUID).
50 Returns:
51 Descriptor instance if found, ``None`` otherwise.
52 """
53 if isinstance(uuid, str):
54 try:
55 uuid_obj = BluetoothUUID(uuid)
56 except ValueError:
57 return None
58 else:
59 uuid_obj = uuid
61 return self._descriptors.get(uuid_obj.dashed_form)
63 def get_descriptors(self) -> dict[str, BaseDescriptor]:
64 """Get all descriptors for this characteristic.
66 Returns:
67 Dict mapping descriptor UUID strings to descriptor instances.
68 """
69 return self._descriptors.copy()
71 def get_cccd(self) -> BaseDescriptor | None:
72 """Get the Client Characteristic Configuration Descriptor (CCCD).
74 Returns:
75 CCCD descriptor instance if present, ``None`` otherwise.
76 """
77 return self.get_descriptor(CCCDDescriptor().uuid)
79 def can_notify(self) -> bool:
80 """Check if this characteristic supports notifications.
82 Returns:
83 ``True`` if the characteristic has a CCCD descriptor.
84 """
85 return self.get_cccd() is not None
87 # ------------------------------------------------------------------
88 # Context-based descriptor lookups
89 # ------------------------------------------------------------------
91 def get_descriptor_from_context(
92 self, ctx: CharacteristicContext | None, descriptor_class: type[BaseDescriptor]
93 ) -> DescriptorData | None:
94 """Get a descriptor of the specified type from the context.
96 Args:
97 ctx: Characteristic context containing descriptors.
98 descriptor_class: The descriptor class to look for.
100 Returns:
101 DescriptorData if found, ``None`` otherwise.
102 """
103 return _get_descriptor(ctx, descriptor_class)
105 def get_valid_range_from_context(
106 self,
107 ctx: CharacteristicContext | None = None,
108 ) -> tuple[int | float, int | float] | None:
109 """Get valid range from descriptor context if available.
111 Args:
112 ctx: Characteristic context containing descriptors.
114 Returns:
115 Tuple of (min, max) values if Valid Range descriptor present, ``None`` otherwise.
116 """
117 return _get_valid_range(ctx)
119 def get_presentation_format_from_context(
120 self,
121 ctx: CharacteristicContext | None = None,
122 ) -> CharacteristicPresentationFormatData | None:
123 """Get presentation format from descriptor context if available.
125 Args:
126 ctx: Characteristic context containing descriptors.
128 Returns:
129 CharacteristicPresentationFormatData if present, ``None`` otherwise.
130 """
131 return _get_presentation_format(ctx)
133 def get_user_description_from_context(self, ctx: CharacteristicContext | None = None) -> str | None:
134 """Get user description from descriptor context if available.
136 Args:
137 ctx: Characteristic context containing descriptors.
139 Returns:
140 User description string if present, ``None`` otherwise.
141 """
142 return _get_user_description(ctx)
144 def validate_value_against_descriptor_range(self, value: float, ctx: CharacteristicContext | None = None) -> bool:
145 """Validate a value against descriptor-defined valid range.
147 Args:
148 value: Value to validate.
149 ctx: Characteristic context containing descriptors.
151 Returns:
152 ``True`` if value is within valid range or no range defined.
153 """
154 return _validate_value_range(value, ctx)
156 def enhance_error_message_with_descriptors(
157 self,
158 base_message: str,
159 ctx: CharacteristicContext | None = None,
160 ) -> str:
161 """Enhance error message with descriptor information for better debugging.
163 Args:
164 base_message: Original error message.
165 ctx: Characteristic context containing descriptors.
167 Returns:
168 Enhanced error message with descriptor context.
169 """
170 return _enhance_error_message(base_message, ctx)