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
« 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."""
3from __future__ import annotations
5from enum import IntEnum
6from typing import TypeVar
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
25# Type variable for EnumTemplate - bound to IntEnum
26T = TypeVar("T", bound=IntEnum)
29class EnumTemplate(CodingTemplate[T]):
30 """Template for IntEnum encoding/decoding with configurable byte size.
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.).
35 This template validates enum membership explicitly, supporting non-contiguous
36 enum ranges (e.g., values 0, 2, 5, 10).
38 Pipeline Integration:
39 bytes → [extractor] → raw_int → [IDENTITY translator] → int → enum constructor
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 """
63 def __init__(self, enum_class: type[T], extractor: RawExtractor) -> None:
64 """Initialize with enum class and extractor.
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
74 @property
75 def data_size(self) -> int:
76 """Return byte size required for encoding."""
77 return self._extractor.byte_size
79 @property
80 def extractor(self) -> RawExtractor:
81 """Return extractor for pipeline access."""
82 return self._extractor
84 @property
85 def translator(self) -> ValueTranslator[int]:
86 """Get IDENTITY translator for enums (no scaling needed)."""
87 return IDENTITY
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.
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)
100 Returns:
101 Enum instance of type T
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)
111 # Extract raw integer value
112 raw_value = self._extractor.extract(data, offset)
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
124 def encode_value(self, value: T | int, *, validate: bool = True) -> bytearray:
125 """Encode enum instance or int to bytes.
127 Args:
128 value: Enum instance or integer value to encode
129 validate: Whether to validate enum membership (default True)
131 Returns:
132 Encoded bytes
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)
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 )
151 # Pack to bytes
152 return self._extractor.pack(int_value)
154 @classmethod
155 def uint8(cls, enum_class: type[T]) -> EnumTemplate[T]:
156 """Create EnumTemplate for 1-byte unsigned enum.
158 Args:
159 enum_class: IntEnum subclass with values 0-255
161 Returns:
162 Configured EnumTemplate instance
164 Example::
165 >>> class Status(IntEnum):
166 ... IDLE = 0
167 ... ACTIVE = 1
168 >>> template = EnumTemplate.uint8(Status)
169 """
170 return cls(enum_class, UINT8)
172 @classmethod
173 def uint16(cls, enum_class: type[T]) -> EnumTemplate[T]:
174 """Create EnumTemplate for 2-byte unsigned enum.
176 Args:
177 enum_class: IntEnum subclass with values 0-65535
179 Returns:
180 Configured EnumTemplate instance
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)
190 @classmethod
191 def uint32(cls, enum_class: type[T]) -> EnumTemplate[T]:
192 """Create EnumTemplate for 4-byte unsigned enum.
194 Args:
195 enum_class: IntEnum subclass with values 0-4294967295
197 Returns:
198 Configured EnumTemplate instance
199 """
200 return cls(enum_class, UINT32)
202 @classmethod
203 def sint8(cls, enum_class: type[T]) -> EnumTemplate[T]:
204 """Create EnumTemplate for 1-byte signed enum.
206 Args:
207 enum_class: IntEnum subclass with values -128 to 127
209 Returns:
210 Configured EnumTemplate instance
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)
221 @classmethod
222 def sint16(cls, enum_class: type[T]) -> EnumTemplate[T]:
223 """Create EnumTemplate for 2-byte signed enum.
225 Args:
226 enum_class: IntEnum subclass with values -32768 to 32767
228 Returns:
229 Configured EnumTemplate instance
230 """
231 return cls(enum_class, SINT16)
233 @classmethod
234 def sint32(cls, enum_class: type[T]) -> EnumTemplate[T]:
235 """Create EnumTemplate for 4-byte signed enum.
237 Args:
238 enum_class: IntEnum subclass with values -2147483648 to 2147483647
240 Returns:
241 Configured EnumTemplate instance
242 """
243 return cls(enum_class, SINT32)