Coverage for src / bluetooth_sig / gatt / characteristics / templates / enum.py: 100%

60 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-03-18 11:17 +0000

1"""Enum template for IntEnum encoding/decoding with configurable byte size.""" 

2 

3from __future__ import annotations 

4 

5from enum import IntEnum 

6from typing import TypeVar 

7 

8from ...context import CharacteristicContext 

9from ...exceptions import InsufficientDataError, ValueRangeError 

10from ..utils.extractors import ( 

11 SINT8, 

12 SINT16, 

13 SINT32, 

14 UINT8, 

15 UINT16, 

16 UINT32, 

17 RawExtractor, 

18) 

19from ..utils.translators import ( 

20 IDENTITY, 

21 ValueTranslator, 

22) 

23from .base import CodingTemplate 

24 

25# Type variable for EnumTemplate - bound to IntEnum 

26T = TypeVar("T", bound=IntEnum) 

27 

28 

29class EnumTemplate(CodingTemplate[T]): 

30 """Template for IntEnum encoding/decoding with configurable byte size. 

31 

32 Maps raw integer bytes to Python IntEnum instances through extraction and validation. 

33 Supports any integer-based enum with any extractor (UINT8, UINT16, SINT8, etc.). 

34 

35 This template validates enum membership explicitly, supporting non-contiguous 

36 enum ranges (e.g., values 0, 2, 5, 10). 

37 

38 Pipeline Integration: 

39 bytes → [extractor] → raw_int → [IDENTITY translator] → int → enum constructor 

40 

41 Examples: 

42 >>> class Status(IntEnum): 

43 ... IDLE = 0 

44 ... ACTIVE = 1 

45 ... ERROR = 2 

46 >>> 

47 >>> # Create template with factory method 

48 >>> template = EnumTemplate.uint8(Status) 

49 >>> 

50 >>> # Or with explicit extractor 

51 >>> template = EnumTemplate(Status, UINT8) 

52 >>> 

53 >>> # Decode from bytes 

54 >>> status = template.decode_value(bytearray([0x01])) # Status.ACTIVE 

55 >>> 

56 >>> # Encode enum to bytes 

57 >>> data = template.encode_value(Status.ERROR) # bytearray([0x02]) 

58 >>> 

59 >>> # Encode int to bytes (also supported) 

60 >>> data = template.encode_value(2) # bytearray([0x02]) 

61 """ 

62 

63 def __init__(self, enum_class: type[T], extractor: RawExtractor) -> None: 

64 """Initialize with enum class and extractor. 

65 

66 Args: 

67 enum_class: IntEnum subclass to encode/decode 

68 extractor: Raw extractor defining byte size and signedness 

69 (e.g., UINT8, UINT16, SINT8, etc.) 

70 """ 

71 self._enum_class = enum_class 

72 self._extractor = extractor 

73 

74 @property 

75 def data_size(self) -> int: 

76 """Return byte size required for encoding.""" 

77 return self._extractor.byte_size 

78 

79 @property 

80 def extractor(self) -> RawExtractor: 

81 """Return extractor for pipeline access.""" 

82 return self._extractor 

83 

84 @property 

85 def translator(self) -> ValueTranslator[int]: 

86 """Get IDENTITY translator for enums (no scaling needed).""" 

87 return IDENTITY 

88 

89 def decode_value( 

90 self, data: bytearray, offset: int = 0, ctx: CharacteristicContext | None = None, *, validate: bool = True 

91 ) -> T: 

92 """Decode bytes to enum instance. 

93 

94 Args: 

95 data: Raw bytes from BLE characteristic 

96 offset: Starting offset in data buffer 

97 ctx: Optional context for parsing 

98 validate: Whether to validate enum membership (default True) 

99 

100 Returns: 

101 Enum instance of type T 

102 

103 Raises: 

104 InsufficientDataError: If data too short for required byte size 

105 ValueRangeError: If raw value not a valid enum member and validate=True 

106 """ 

107 # Check data length 

108 if validate and len(data) < offset + self.data_size: 

109 raise InsufficientDataError(self._enum_class.__name__, data[offset:], self.data_size) 

110 

111 # Extract raw integer value 

112 raw_value = self._extractor.extract(data, offset) 

113 

114 # Validate enum membership and construct 

115 try: 

116 return self._enum_class(raw_value) 

117 except ValueError as e: 

118 # Get valid range from enum members 

119 valid_values = [member.value for member in self._enum_class] 

120 min_val = min(valid_values) 

121 max_val = max(valid_values) 

122 raise ValueRangeError(self._enum_class.__name__, raw_value, min_val, max_val) from e 

123 

124 def encode_value(self, value: T | int, *, validate: bool = True) -> bytearray: 

125 """Encode enum instance or int to bytes. 

126 

127 Args: 

128 value: Enum instance or integer value to encode 

129 validate: Whether to validate enum membership (default True) 

130 

131 Returns: 

132 Encoded bytes 

133 

134 Raises: 

135 ValueError: If value not a valid enum member and validate=True 

136 """ 

137 # Convert to int if enum instance 

138 int_value = value.value if isinstance(value, self._enum_class) else int(value) 

139 

140 # Validate membership 

141 if validate: 

142 valid_values = [member.value for member in self._enum_class] 

143 if int_value not in valid_values: 

144 min_val = min(valid_values) 

145 max_val = max(valid_values) 

146 raise ValueError( 

147 f"{self._enum_class.__name__} value {int_value} is invalid. " 

148 f"Valid range: {min_val}-{max_val}, valid values: {sorted(valid_values)}" 

149 ) 

150 

151 # Pack to bytes 

152 return self._extractor.pack(int_value) 

153 

154 @classmethod 

155 def uint8(cls, enum_class: type[T]) -> EnumTemplate[T]: 

156 """Create EnumTemplate for 1-byte unsigned enum. 

157 

158 Args: 

159 enum_class: IntEnum subclass with values 0-255 

160 

161 Returns: 

162 Configured EnumTemplate instance 

163 

164 Example:: 

165 >>> class Status(IntEnum): 

166 ... IDLE = 0 

167 ... ACTIVE = 1 

168 >>> template = EnumTemplate.uint8(Status) 

169 """ 

170 return cls(enum_class, UINT8) 

171 

172 @classmethod 

173 def uint16(cls, enum_class: type[T]) -> EnumTemplate[T]: 

174 """Create EnumTemplate for 2-byte unsigned enum. 

175 

176 Args: 

177 enum_class: IntEnum subclass with values 0-65535 

178 

179 Returns: 

180 Configured EnumTemplate instance 

181 

182 Example:: 

183 >>> class ExtendedStatus(IntEnum): 

184 ... STATE_1 = 0x0100 

185 ... STATE_2 = 0x0200 

186 >>> template = EnumTemplate.uint16(ExtendedStatus) 

187 """ 

188 return cls(enum_class, UINT16) 

189 

190 @classmethod 

191 def uint32(cls, enum_class: type[T]) -> EnumTemplate[T]: 

192 """Create EnumTemplate for 4-byte unsigned enum. 

193 

194 Args: 

195 enum_class: IntEnum subclass with values 0-4294967295 

196 

197 Returns: 

198 Configured EnumTemplate instance 

199 """ 

200 return cls(enum_class, UINT32) 

201 

202 @classmethod 

203 def sint8(cls, enum_class: type[T]) -> EnumTemplate[T]: 

204 """Create EnumTemplate for 1-byte signed enum. 

205 

206 Args: 

207 enum_class: IntEnum subclass with values -128 to 127 

208 

209 Returns: 

210 Configured EnumTemplate instance 

211 

212 Example:: 

213 >>> class Temperature(IntEnum): 

214 ... FREEZING = -10 

215 ... NORMAL = 0 

216 ... HOT = 10 

217 >>> template = EnumTemplate.sint8(Temperature) 

218 """ 

219 return cls(enum_class, SINT8) 

220 

221 @classmethod 

222 def sint16(cls, enum_class: type[T]) -> EnumTemplate[T]: 

223 """Create EnumTemplate for 2-byte signed enum. 

224 

225 Args: 

226 enum_class: IntEnum subclass with values -32768 to 32767 

227 

228 Returns: 

229 Configured EnumTemplate instance 

230 """ 

231 return cls(enum_class, SINT16) 

232 

233 @classmethod 

234 def sint32(cls, enum_class: type[T]) -> EnumTemplate[T]: 

235 """Create EnumTemplate for 4-byte signed enum. 

236 

237 Args: 

238 enum_class: IntEnum subclass with values -2147483648 to 2147483647 

239 

240 Returns: 

241 Configured EnumTemplate instance 

242 """ 

243 return cls(enum_class, SINT32)