Coverage for src / bluetooth_sig / gatt / characteristics / custom.py: 89%
47 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"""Custom characteristic base class with auto-registration support."""
3from __future__ import annotations
5from typing import Any
7from ...types import CharacteristicInfo
8from .base import BaseCharacteristic, ValidationConfig
11class CustomBaseCharacteristic(BaseCharacteristic[Any]):
12 r"""Helper base class for custom characteristic implementations.
14 This class provides a wrapper around physical BLE characteristics that are not
15 defined in the Bluetooth SIG specification. It supports both manual info passing
16 and automatic class-level _info binding via __init_subclass__.
18 Auto-Registration:
19 Custom characteristics automatically register themselves with the global
20 BluetoothSIGTranslator singleton when first instantiated. No manual
21 registration needed!
23 Examples:
24 >>> from bluetooth_sig.types.data_types import CharacteristicInfo
25 >>> from bluetooth_sig.types.uuid import BluetoothUUID
26 >>> class MyCharacteristic(CustomBaseCharacteristic):
27 ... _info = CharacteristicInfo(uuid=BluetoothUUID("AAAA"), name="My Char")
28 >>> # Auto-registers with singleton on first instantiation
29 >>> char = MyCharacteristic() # Auto-registered!
30 >>> # Now accessible via the global translator
31 >>> from bluetooth_sig import BluetoothSIGTranslator
32 >>> translator = BluetoothSIGTranslator.get_instance()
33 >>> result = translator.parse_characteristic("AAAA", b"\x42")
34 """
36 _is_custom = True
37 _is_base_class = True # Exclude from registry validation tests
38 _configured_info: CharacteristicInfo | None = None # Stores class-level _info
39 _allows_sig_override = False # Default: no SIG override permission
40 _registry_tracker: set[str] = set() # Track registered UUIDs to avoid duplicates
42 @classmethod
43 def get_configured_info(cls) -> CharacteristicInfo | None:
44 """Get the class-level configured CharacteristicInfo.
46 Returns:
47 CharacteristicInfo if configured, None otherwise
49 """
50 return cls._configured_info
52 # pylint: disable=duplicate-code
53 # NOTE: __init_subclass__ and __init__ patterns are intentionally similar to CustomBaseService.
54 # This is by design - both custom characteristic and service classes need identical validation
55 # and info management patterns. Consolidation not possible due to different base types and info types.
56 def __init_subclass__(cls, allow_sig_override: bool = False, **kwargs: Any) -> None: # noqa: ANN401 # Receives subclass kwargs
57 """Automatically set up _info if provided as class attribute.
59 Args:
60 allow_sig_override: Set to True when intentionally overriding SIG UUIDs.
61 **kwargs: Additional subclass keyword arguments passed by callers or
62 metaclasses; these are accepted for compatibility and ignored
63 unless explicitly handled.
65 Raises:
66 ValueError: If class uses SIG UUID without override permission.
68 """
69 super().__init_subclass__(**kwargs)
71 # Store override permission for registry validation
72 cls._allows_sig_override = allow_sig_override
74 # If class has _info attribute, validate and store it
75 if hasattr(cls, "_info"):
76 info = getattr(cls, "_info", None)
77 if info is not None:
78 # Check for SIG UUID override (unless explicitly allowed)
79 if not allow_sig_override and info.uuid.is_sig_characteristic():
80 raise ValueError(
81 f"{cls.__name__} uses SIG UUID {info.uuid} without override flag. "
82 "Use custom UUID or add allow_sig_override=True parameter."
83 )
85 cls._configured_info = info
87 def __init__(
88 self,
89 info: CharacteristicInfo | None = None,
90 auto_register: bool = True,
91 validation: ValidationConfig | None = None,
92 ) -> None:
93 """Initialize a custom characteristic with automatic _info resolution and registration.
95 Args:
96 info: Optional override for class-configured _info
97 auto_register: If True (default), automatically register with global translator singleton
98 validation: Validation constraints configuration (optional)
100 Raises:
101 ValueError: If no valid info available from class or parameter
103 Examples:
104 >>> # Simple usage - auto-registers with global translator
105 >>> char = MyCharacteristic() # Auto-registered!
106 >>> # Opt-out of auto-registration if needed
107 >>> char = MyCharacteristic(auto_register=False)
109 """
110 # Use provided info, or fall back to class-configured _info
111 final_info = info or self.__class__.get_configured_info()
113 if not final_info:
114 raise ValueError(f"{self.__class__.__name__} requires either 'info' parameter or '_info' class attribute")
116 if not final_info.uuid or str(final_info.uuid) == "0000":
117 raise ValueError("Valid UUID is required for custom characteristics")
119 # Auto-register if requested and not already registered
120 if auto_register:
121 # TODO
122 # NOTE: Import here to avoid circular import (translator imports characteristics)
123 from ...core.translator import BluetoothSIGTranslator # pylint: disable=import-outside-toplevel
125 # Get the singleton translator instance
126 translator = BluetoothSIGTranslator.get_instance()
128 # Track registration to avoid duplicate registrations
129 uuid_str = str(final_info.uuid)
130 registry_key = f"{id(translator)}:{uuid_str}"
132 if registry_key not in CustomBaseCharacteristic._registry_tracker:
133 # Register this characteristic class with the translator
134 # Use override=True to allow re-registration (idempotent behaviour)
135 translator.register_custom_characteristic_class(
136 uuid_str,
137 self.__class__,
138 override=True, # Allow override for idempotent registration
139 )
140 CustomBaseCharacteristic._registry_tracker.add(registry_key)
142 # Call parent constructor with our info to maintain consistency
143 super().__init__(info=final_info, validation=validation)
145 def __post_init__(self) -> None:
146 """Override BaseCharacteristic.__post_init__ to use custom info management.
148 CustomBaseCharacteristic manages _info manually from provided or configured info,
149 bypassing SIG resolution that would fail for custom characteristics.
150 Then delegates to parent for remaining initialization.
151 """
152 # Use provided info if available (from manual override), otherwise use configured info
153 final_info = None
154 if hasattr(self, "_provided_info") and self._provided_info:
155 final_info = self._provided_info
156 else:
157 configured_info = self.__class__.get_configured_info()
158 if configured_info:
159 final_info = configured_info
160 else:
161 # This shouldn't happen if class setup is correct
162 raise ValueError(f"CustomBaseCharacteristic {self.__class__.__name__} has no valid info source")
164 # Set _provided_info so parent __post_init__ will use it instead of trying SIG resolution
165 self._provided_info = final_info
167 # Call parent to complete initialization (sets up _special_resolver, applies overrides, etc.)
168 # This is critical - without it, _special_resolver won't exist
169 super().__post_init__()