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

1"""Base class for GATT descriptors.""" 

2 

3from __future__ import annotations 

4 

5import logging 

6from abc import ABC, abstractmethod 

7from typing import Any 

8 

9from ...types import DescriptorData, DescriptorInfo 

10from ...types.uuid import BluetoothUUID 

11from ..exceptions import UUIDResolutionError 

12from ..resolver import NameVariantGenerator 

13from ..uuid_registry import uuid_registry 

14 

15logger = logging.getLogger(__name__) 

16 

17 

18class BaseDescriptor(ABC): 

19 """Base class for all GATT descriptors. 

20 

21 Automatically resolves UUID and name from Bluetooth SIG specifications. 

22 Provides parsing capabilities for descriptor values. 

23 """ 

24 

25 # Class attributes for explicit overrides 

26 _descriptor_name: str | None = None 

27 _info: DescriptorInfo # Populated in __post_init__ 

28 

29 def __init__(self) -> None: 

30 """Initialize descriptor with resolved information.""" 

31 self.__post_init__() 

32 

33 def __post_init__(self) -> None: 

34 """Initialize descriptor with resolved information.""" 

35 self._info = self._resolve_info() 

36 

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) 

42 

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 ) 

54 

55 # No resolution found 

56 raise UUIDResolutionError(self.__class__.__name__, [self.__class__.__name__]) 

57 

58 def _has_structured_data(self) -> bool: 

59 """Check if this descriptor contains structured data.""" 

60 return False 

61 

62 def _get_data_format(self) -> str: 

63 """Get the data format for this descriptor.""" 

64 return "bytes" 

65 

66 @property 

67 def uuid(self) -> BluetoothUUID: 

68 """Get the descriptor UUID.""" 

69 return self._info.uuid 

70 

71 @property 

72 def name(self) -> str: 

73 """Get the descriptor name.""" 

74 return self._info.name 

75 

76 @property 

77 def info(self) -> DescriptorInfo: 

78 """Get the descriptor information.""" 

79 return self._info 

80 

81 def parse_value(self, data: bytes) -> DescriptorData: 

82 """Parse raw descriptor data into structured format. 

83 

84 Args: 

85 data: Raw bytes from the descriptor read 

86 

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 ) 

108 

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. 

112 

113 Args: 

114 data: Raw bytes from the descriptor 

115 

116 Returns: 

117 Parsed value in appropriate format 

118 

119 Raises: 

120 NotImplementedError: If subclass doesn't implement parsing 

121 """ 

122 raise NotImplementedError(f"{self.__class__.__name__} must implement _parse_descriptor_value()") 

123 

124 

125class RangeDescriptorMixin: 

126 """Mixin for descriptors that provide min/max value validation.""" 

127 

128 def get_min_value(self, data: bytes) -> int | float: 

129 """Get the minimum valid value. 

130 

131 Args: 

132 data: Raw descriptor data 

133 

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] 

139 

140 def get_max_value(self, data: bytes) -> int | float: 

141 """Get the maximum valid value. 

142 

143 Args: 

144 data: Raw descriptor data 

145 

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] 

151 

152 def is_value_in_range(self, data: bytes, value: int | float) -> bool: 

153 """Check if a value is within the valid range. 

154 

155 Args: 

156 data: Raw descriptor data 

157 value: Value to check 

158 

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)