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

1"""Descriptor support mixin for GATT characteristics. 

2 

3Provides all descriptor-related methods (add, get, CCCD, context lookups) 

4as a mixin that :class:`BaseCharacteristic` inherits from. 

5""" 

6 

7from __future__ import annotations 

8 

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 

21 

22 

23class DescriptorMixin: 

24 """Mixin providing descriptor management and context lookup helpers. 

25 

26 Expects the consuming class to initialise ``_descriptors`` as an empty 

27 ``dict[str, BaseDescriptor]`` in ``__init__``. 

28 """ 

29 

30 _descriptors: dict[str, BaseDescriptor] 

31 

32 # ------------------------------------------------------------------ 

33 # Instance descriptor management 

34 # ------------------------------------------------------------------ 

35 

36 def add_descriptor(self, descriptor: BaseDescriptor) -> None: 

37 """Add a descriptor to this characteristic. 

38 

39 Args: 

40 descriptor: The descriptor instance to add. 

41 """ 

42 self._descriptors[str(descriptor.uuid)] = descriptor 

43 

44 def get_descriptor(self, uuid: str | BluetoothUUID) -> BaseDescriptor | None: 

45 """Get a descriptor by UUID. 

46 

47 Args: 

48 uuid: Descriptor UUID (string or BluetoothUUID). 

49 

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 

60 

61 return self._descriptors.get(uuid_obj.dashed_form) 

62 

63 def get_descriptors(self) -> dict[str, BaseDescriptor]: 

64 """Get all descriptors for this characteristic. 

65 

66 Returns: 

67 Dict mapping descriptor UUID strings to descriptor instances. 

68 """ 

69 return self._descriptors.copy() 

70 

71 def get_cccd(self) -> BaseDescriptor | None: 

72 """Get the Client Characteristic Configuration Descriptor (CCCD). 

73 

74 Returns: 

75 CCCD descriptor instance if present, ``None`` otherwise. 

76 """ 

77 return self.get_descriptor(CCCDDescriptor().uuid) 

78 

79 def can_notify(self) -> bool: 

80 """Check if this characteristic supports notifications. 

81 

82 Returns: 

83 ``True`` if the characteristic has a CCCD descriptor. 

84 """ 

85 return self.get_cccd() is not None 

86 

87 # ------------------------------------------------------------------ 

88 # Context-based descriptor lookups 

89 # ------------------------------------------------------------------ 

90 

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. 

95 

96 Args: 

97 ctx: Characteristic context containing descriptors. 

98 descriptor_class: The descriptor class to look for. 

99 

100 Returns: 

101 DescriptorData if found, ``None`` otherwise. 

102 """ 

103 return _get_descriptor(ctx, descriptor_class) 

104 

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. 

110 

111 Args: 

112 ctx: Characteristic context containing descriptors. 

113 

114 Returns: 

115 Tuple of (min, max) values if Valid Range descriptor present, ``None`` otherwise. 

116 """ 

117 return _get_valid_range(ctx) 

118 

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. 

124 

125 Args: 

126 ctx: Characteristic context containing descriptors. 

127 

128 Returns: 

129 CharacteristicPresentationFormatData if present, ``None`` otherwise. 

130 """ 

131 return _get_presentation_format(ctx) 

132 

133 def get_user_description_from_context(self, ctx: CharacteristicContext | None = None) -> str | None: 

134 """Get user description from descriptor context if available. 

135 

136 Args: 

137 ctx: Characteristic context containing descriptors. 

138 

139 Returns: 

140 User description string if present, ``None`` otherwise. 

141 """ 

142 return _get_user_description(ctx) 

143 

144 def validate_value_against_descriptor_range(self, value: float, ctx: CharacteristicContext | None = None) -> bool: 

145 """Validate a value against descriptor-defined valid range. 

146 

147 Args: 

148 value: Value to validate. 

149 ctx: Characteristic context containing descriptors. 

150 

151 Returns: 

152 ``True`` if value is within valid range or no range defined. 

153 """ 

154 return _validate_value_range(value, ctx) 

155 

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. 

162 

163 Args: 

164 base_message: Original error message. 

165 ctx: Characteristic context containing descriptors. 

166 

167 Returns: 

168 Enhanced error message with descriptor context. 

169 """ 

170 return _enhance_error_message(base_message, ctx)