Coverage for src / bluetooth_sig / gatt / exceptions.py: 94%
153 statements
« prev ^ index » next coverage.py v7.13.1, created at 2026-01-11 20:14 +0000
« prev ^ index » next coverage.py v7.13.1, created at 2026-01-11 20:14 +0000
1"""GATT exceptions for the Bluetooth SIG library."""
3from __future__ import annotations
5from typing import Any
7from ..types import ParseFieldError as FieldError
8from ..types import SpecialValueResult
9from ..types.data_types import ValidationAccumulator
10from ..types.uuid import BluetoothUUID
13class BluetoothSIGError(Exception):
14 """Base exception for all Bluetooth SIG related errors."""
17class CharacteristicError(BluetoothSIGError):
18 """Base exception for characteristic-related errors."""
21class ServiceError(BluetoothSIGError):
22 """Base exception for service-related errors."""
25class UUIDResolutionError(BluetoothSIGError):
26 """Exception raised when UUID resolution fails."""
28 def __init__(self, name: str, attempted_names: list[str] | None = None) -> None:
29 """Initialise UUIDResolutionError.
31 Args:
32 name: The name for which UUID resolution failed.
33 attempted_names: List of attempted names.
35 """
36 self.name = name
37 self.attempted_names = attempted_names or []
38 message = f"No UUID found for: {name}"
39 if self.attempted_names:
40 message += f". Tried: {', '.join(self.attempted_names)}"
41 super().__init__(message)
44class DataParsingError(CharacteristicError):
45 """Exception raised when characteristic data parsing fails."""
47 def __init__(self, characteristic: str, data: bytes | bytearray, reason: str) -> None:
48 """Initialise DataParsingError.
50 Args:
51 characteristic: The characteristic name.
52 data: The raw data that failed to parse.
53 reason: Reason for the parsing failure.
55 """
56 self.characteristic = characteristic
57 self.data = data
58 self.reason = reason
59 hex_data = " ".join(f"{byte:02X}" for byte in data)
60 message = f"Failed to parse {characteristic} data [{hex_data}]: {reason}"
61 super().__init__(message)
64class ParseFieldError(DataParsingError):
65 """Exception raised when a specific field fails to parse.
67 This exception provides detailed context about which field failed, where it
68 failed in the data, and why it failed. This enables actionable error messages
69 and structured error reporting.
71 NOTE: This exception intentionally has more arguments than the standard limit
72 to provide complete field-level diagnostic information. The additional parameters
73 (field, offset) are essential for actionable error messages and field-level debugging.
75 Attributes:
76 field: Name of the field that failed (e.g., "temperature", "flags")
77 offset: Byte offset where the field starts in the raw data
78 expected: Description of what was expected
79 actual: Description of what was actually encountered
81 """
83 # pylint: disable=too-many-arguments,too-many-positional-arguments
84 # NOTE: More arguments than standard limit required for complete field-level diagnostics
85 def __init__(
86 self,
87 characteristic: str,
88 field: str,
89 data: bytes | bytearray,
90 reason: str,
91 offset: int | None = None,
92 ) -> None:
93 """Initialise ParseFieldError.
95 Args:
96 characteristic: The characteristic name.
97 field: The field name.
98 data: The raw data.
99 reason: Reason for the parsing failure.
100 offset: Optional offset in the data.
102 """
103 self.field = field
104 self.offset = offset
105 # Store the original reason before formatting
106 self.field_reason = reason
108 # Format message with field and offset information
109 field_info = f"field '{field}'"
110 if offset is not None:
111 field_info = f"{field_info} at offset {offset}"
113 detailed_reason = f"{field_info}: {reason}"
114 super().__init__(characteristic, data, detailed_reason)
117class DataEncodingError(CharacteristicError):
118 """Exception raised when characteristic data encoding fails."""
120 def __init__(self, characteristic: str, value: Any, reason: str) -> None: # noqa: ANN401 # Reports errors for any value type
121 """Initialise DataEncodingError.
123 Args:
124 characteristic: The characteristic name.
125 value: The value that failed to encode.
126 reason: Reason for the encoding failure.
128 """
129 self.characteristic = characteristic
130 self.value = value
131 self.reason = reason
132 message = f"Failed to encode {characteristic} value {value}: {reason}"
133 super().__init__(message)
136class DataValidationError(CharacteristicError):
137 """Exception raised when characteristic data validation fails."""
139 def __init__(self, field: str, value: Any, expected: str) -> None: # noqa: ANN401 # Validates any value type
140 """Initialise DataValidationError.
142 Args:
143 field: The field name.
144 value: The value that failed validation.
145 expected: Expected value or description.
147 """
148 self.field = field
149 self.value = value
150 self.expected = expected
151 message = f"Invalid {field}: {value} (expected {expected})"
152 super().__init__(message)
155class InsufficientDataError(DataParsingError):
156 """Exception raised when there is insufficient data for parsing."""
158 def __init__(self, characteristic: str, data: bytes | bytearray, required: int) -> None:
159 """Initialise InsufficientDataError.
161 Args:
162 characteristic: The characteristic name.
163 data: The raw data.
164 required: Required length of data.
166 """
167 self.required = required
168 self.actual = len(data)
169 reason = f"need {required} bytes, got {self.actual}"
170 super().__init__(characteristic, data, reason)
173class ValueRangeError(DataValidationError):
174 """Exception raised when a value is outside the expected range."""
176 def __init__(self, field: str, value: Any, min_val: Any, max_val: Any) -> None: # noqa: ANN401 # Validates ranges for various numeric types
177 """Initialise RangeValidationError.
179 Args:
180 field: The field name.
181 value: The value to validate.
182 min_val: Minimum valid value.
183 max_val: Maximum valid value.
185 """
186 self.min_val = min_val
187 self.max_val = max_val
188 expected = f"range [{min_val}, {max_val}]"
189 super().__init__(field, value, expected)
192class TypeMismatchError(DataValidationError):
193 """Exception raised when a value has an unexpected type."""
195 expected_type: type | tuple[type, ...]
196 actual_type: type
198 def __init__(self, field: str, value: Any, expected_type: type | tuple[type, ...]) -> None: # noqa: ANN401 # Reports type errors for any value
199 """Initialise TypeValidationError.
201 Args:
202 field: The field name.
203 value: The value to validate.
204 expected_type: Expected type(s).
206 """
207 self.expected_type = expected_type
208 self.actual_type = type(value)
210 # Handle tuple of types for display
211 if isinstance(expected_type, tuple):
212 type_names = " or ".join(t.__name__ for t in expected_type)
213 expected = f"type {type_names}, got {self.actual_type.__name__}"
214 else:
215 expected = f"type {expected_type.__name__}, got {self.actual_type.__name__}"
217 super().__init__(field, value, expected)
220class MissingDependencyError(CharacteristicError):
221 """Exception raised when a required dependency is missing for multi-characteristic parsing."""
223 def __init__(self, characteristic: str, missing_dependencies: list[str]) -> None:
224 """Initialise DependencyValidationError.
226 Args:
227 characteristic: The characteristic name.
228 missing_dependencies: List of missing dependencies.
230 """
231 self.characteristic = characteristic
232 self.missing_dependencies = missing_dependencies
233 dep_list = ", ".join(missing_dependencies)
234 message = f"{characteristic} requires missing dependencies: {dep_list}"
235 super().__init__(message)
238class EnumValueError(DataValidationError):
239 """Exception raised when an enum value is invalid."""
241 def __init__(self, field: str, value: Any, enum_class: type, valid_values: list[Any]) -> None: # noqa: ANN401 # Validates enum values of any type
242 """Initialise EnumValidationError.
244 Args:
245 field: The field name.
246 value: The value to validate.
247 enum_class: Enum class for validation.
248 valid_values: List of valid values.
250 """
251 self.enum_class = enum_class
252 self.valid_values = valid_values
253 expected = f"{enum_class.__name__} value from {valid_values}"
254 super().__init__(field, value, expected)
257class IEEE11073Error(DataParsingError):
258 """Exception raised when IEEE 11073 format parsing fails."""
260 def __init__(self, data: bytes | bytearray, format_type: str, reason: str) -> None:
261 """Initialise DataFormatError.
263 Args:
264 data: The raw data.
265 format_type: Format type expected.
266 reason: Reason for the format error.
268 """
269 self.format_type = format_type
270 characteristic = f"IEEE 11073 {format_type}"
271 super().__init__(characteristic, data, reason)
274class YAMLResolutionError(BluetoothSIGError):
275 """Exception raised when YAML specification resolution fails."""
277 def __init__(self, name: str, yaml_type: str) -> None:
278 """Initialise YAMLSchemaError.
280 Args:
281 name: Name of the YAML entity.
282 yaml_type: Type of the YAML entity.
284 """
285 self.name = name
286 self.yaml_type = yaml_type
287 message = f"Failed to resolve {yaml_type} specification for: {name}"
288 super().__init__(message)
291class ServiceCharacteristicMismatchError(ServiceError):
292 """Exception raised when expected characteristics are not found in a service.
294 service.
295 """
297 def __init__(self, service: str, missing_characteristics: list[str]) -> None:
298 """Initialise ExpectedCharacteristicNotFound.
300 Args:
301 service: The service name.
302 missing_characteristics: List of missing characteristics.
304 """
305 self.service = service
306 self.missing_characteristics = missing_characteristics
307 message = f"Service {service} missing required characteristics: {', '.join(missing_characteristics)}"
308 super().__init__(message)
311class TemplateConfigurationError(CharacteristicError):
312 """Exception raised when a template is incorrectly configured."""
314 def __init__(self, template: str, configuration_issue: str) -> None:
315 """Initialise TemplateConfigurationError.
317 Args:
318 template: The template name.
319 configuration_issue: Description of the configuration issue.
321 """
322 self.template = template
323 self.configuration_issue = configuration_issue
324 message = f"Template {template} configuration error: {configuration_issue}"
325 super().__init__(message)
328class UUIDRequiredError(BluetoothSIGError):
329 """Exception raised when a UUID is required but not provided or invalid."""
331 def __init__(self, class_name: str, entity_type: str) -> None:
332 """Initialise EntityRegistrationError.
334 Args:
335 class_name: Name of the class.
336 entity_type: Type of the entity.
338 """
339 self.class_name = class_name
340 self.entity_type = entity_type
341 message = (
342 f"Custom {entity_type} '{class_name}' requires a valid UUID. "
343 f"Provide a non-empty UUID when instantiating custom {entity_type}s."
344 )
345 super().__init__(message)
348class UUIDCollisionError(BluetoothSIGError):
349 """Exception raised when attempting to use a UUID that already exists in SIG registry."""
351 def __init__(self, uuid: BluetoothUUID | str, existing_name: str, class_name: str) -> None:
352 """Initialise UUIDRegistrationError.
354 Args:
355 uuid: The UUID value.
356 existing_name: Existing name for the UUID.
357 class_name: Name of the class.
359 """
360 self.uuid = uuid
361 self.existing_name = existing_name
362 self.class_name = class_name
363 message = (
364 f"UUID '{uuid}' is already used by SIG characteristic '{existing_name}'. "
365 f"Cannot create custom characteristic '{class_name}' with existing SIG UUID. "
366 f"Use allow_sig_override=True if you intentionally want to override this SIG characteristic."
367 )
368 super().__init__(message)
371class CharacteristicParseError(CharacteristicError):
372 """Raised when characteristic parsing fails.
374 Preserves all debugging context from parsing attempt.
376 Attributes:
377 message: Human-readable error message
378 name: Characteristic name
379 uuid: Characteristic UUID
380 raw_data: Exact bytes that failed (useful: hex dump debugging)
381 raw_int: Extracted integer value (useful: check bit patterns)
382 field_errors: Field-level errors (useful: complex multi-field characteristics)
383 parse_trace: Step-by-step execution log (useful: debug parser flow)
384 validation: Accumulated validation results (useful: see all warnings/errors)
386 """
388 # pylint: disable=too-many-arguments,too-many-positional-arguments
389 # NOTE: Multiple arguments required for complete diagnostic context
390 def __init__(
391 self,
392 message: str,
393 name: str,
394 uuid: BluetoothUUID,
395 raw_data: bytes,
396 raw_int: int | None = None,
397 field_errors: list[FieldError] | None = None,
398 parse_trace: list[str] | None = None,
399 validation: ValidationAccumulator | None = None,
400 ) -> None:
401 """Initialize parse error with diagnostic context.
403 Args:
404 message: Human-readable error message
405 name: Characteristic name
406 uuid: Characteristic UUID
407 raw_data: Raw bytes that failed to parse
408 raw_int: Extracted integer (if extraction succeeded)
409 field_errors: Field-level parsing errors
410 parse_trace: Step-by-step execution log
411 validation: Accumulated validation results
413 """
414 super().__init__(message)
415 self.name = name
416 self.uuid = uuid
417 self.raw_data = raw_data
418 self.raw_int = raw_int
419 self.field_errors = field_errors or []
420 self.parse_trace = parse_trace or []
421 self.validation = validation
423 def __str__(self) -> str:
424 """Format error with field-level details."""
425 base = f"{self.name} ({self.uuid}): {self.args[0]}"
426 if self.field_errors:
427 field_msgs = [f" - {e.field}: {e.reason}" for e in self.field_errors]
428 return f"{base}\nField errors:\n" + "\n".join(field_msgs)
429 return base
432class SpecialValueDetected(CharacteristicError):
433 """Raised when a special sentinel value is detected.
435 Special values represent exceptional conditions: "value is not known", "NaN",
436 "measurement not possible", etc. These are semantically distinct from parse failures.
438 This exception is raised when a valid parse detects a special sentinel value
439 (e.g., 0x8000 = "value is not known", 0x7FFFFFFF = "NaN"). The parsing succeeded,
440 but the result indicates an exceptional state rather than a normal value.
442 Most code should catch this separately from CharacteristicParseError to distinguish:
443 - Parse failure (malformed data, wrong length, invalid format)
444 - Special value detection (well-formed data indicating exceptional state)
446 Attributes:
447 special_value: The detected special value with meaning and raw bytes
448 name: Characteristic name
449 uuid: Characteristic UUID
450 raw_data: Raw bytes containing the special value
451 raw_int: The raw integer value (typically the sentinel value)
453 """
455 # pylint: disable=too-many-arguments,too-many-positional-arguments
456 def __init__(
457 self,
458 special_value: SpecialValueResult,
459 name: str,
460 uuid: BluetoothUUID,
461 raw_data: bytes,
462 raw_int: int | None = None,
463 ) -> None:
464 """Initialize special value detected exception.
466 Args:
467 special_value: The detected SpecialValueResult
468 name: Characteristic name
469 uuid: Characteristic UUID
470 raw_data: Raw bytes containing the special value
471 raw_int: The raw integer value
473 """
474 message = f"{name} ({uuid}): Special value detected: {special_value.meaning}"
475 super().__init__(message)
476 self.special_value = special_value
477 self.name = name
478 self.uuid = uuid
479 self.raw_data = raw_data
480 self.raw_int = raw_int
483class CharacteristicEncodeError(CharacteristicError):
484 """Raised when characteristic encoding fails.
486 Attributes:
487 message: Human-readable error message
488 name: Characteristic name
489 uuid: Characteristic UUID
490 value: The value that failed to encode
491 validation: Accumulated validation results
493 """
495 # pylint: disable=too-many-arguments,too-many-positional-arguments
496 def __init__(
497 self,
498 message: str,
499 name: str,
500 uuid: BluetoothUUID,
501 value: Any, # noqa: ANN401 # Any value type
502 validation: ValidationAccumulator | None = None,
503 ) -> None:
504 """Initialize encode error.
506 Args:
507 message: Human-readable error message
508 name: Characteristic name
509 uuid: Characteristic UUID
510 value: The value that failed to encode
511 validation: Accumulated validation results
513 """
514 super().__init__(message)
515 self.name = name
516 self.uuid = uuid
517 self.value = value
518 self.validation = validation