Coverage for src / bluetooth_sig / device / dependency_resolver.py: 45%
62 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"""Dependency resolution for characteristic reads.
3Resolves required and optional dependencies before reading a characteristic,
4building a ``CharacteristicContext`` will all resolved dependency values.
5"""
7from __future__ import annotations
9import logging
10from enum import Enum
11from typing import Any
13from ..gatt.characteristics.base import BaseCharacteristic
14from ..gatt.characteristics.registry import CharacteristicRegistry
15from ..gatt.characteristics.unknown import UnknownCharacteristic
16from ..gatt.context import CharacteristicContext, DeviceInfo
17from ..types import CharacteristicInfo
18from ..types.uuid import BluetoothUUID
19from .client import ClientManagerProtocol
20from .connected import DeviceConnected
22logger = logging.getLogger(__name__)
25class DependencyResolver:
26 """Resolves characteristic dependencies by reading them from the device.
28 Encapsulates the logic for:
29 - Discovering which dependencies a characteristic declares
30 - Reading dependency values from the device (with caching)
31 - Building a ``CharacteristicContext`` for the target characteristic
33 Uses ``DeviceConnected`` for characteristic instance caching and
34 ``ClientManagerProtocol`` for BLE reads.
35 """
37 def __init__(
38 self,
39 connection_manager: ClientManagerProtocol,
40 connected: DeviceConnected,
41 ) -> None:
42 """Initialise with connection manager and connected subsystem.
44 Args:
45 connection_manager: Connection manager for BLE reads
46 connected: Connected subsystem for characteristic cache
48 """
49 self._connection_manager = connection_manager
50 self._connected = connected
52 async def resolve(
53 self,
54 char_class: type[BaseCharacteristic[Any]],
55 resolution_mode: DependencyResolutionMode,
56 device_info: DeviceInfo,
57 ) -> CharacteristicContext:
58 """Ensure all dependencies for a characteristic are resolved.
60 Automatically reads feature characteristics needed for validation
61 of measurement characteristics. Feature characteristics are cached
62 after first read.
64 Args:
65 char_class: The characteristic class to resolve dependencies for
66 resolution_mode: How to handle dependency resolution
67 device_info: Current device info for context construction
69 Returns:
70 CharacteristicContext with resolved dependencies
72 Raises:
73 RuntimeError: If no connection manager is attached
75 """
76 optional_deps = getattr(char_class, "_optional_dependencies", [])
77 required_deps = getattr(char_class, "_required_dependencies", [])
79 context_chars: dict[str, Any] = {}
81 for dep_class in required_deps + optional_deps:
82 is_required = dep_class in required_deps
84 dep_uuid = dep_class.get_class_uuid()
85 if not dep_uuid:
86 if is_required:
87 raise ValueError(f"Required dependency {dep_class.__name__} has no UUID")
88 continue
90 dep_uuid_str = str(dep_uuid)
92 if resolution_mode == DependencyResolutionMode.SKIP_DEPENDENCIES:
93 continue
95 # Check cache (unless force refresh)
96 if resolution_mode != DependencyResolutionMode.FORCE_REFRESH:
97 cached_char = self._connected.get_cached_characteristic(dep_uuid)
98 if cached_char is not None and cached_char.last_parsed is not None:
99 context_chars[dep_uuid_str] = cached_char.last_parsed
100 continue
102 parsed_data = await self._resolve_single(dep_uuid, is_required, dep_class)
103 if parsed_data is not None:
104 context_chars[dep_uuid_str] = parsed_data
106 return CharacteristicContext(
107 device_info=device_info,
108 other_characteristics=context_chars,
109 )
111 async def _resolve_single(
112 self,
113 dep_uuid: BluetoothUUID,
114 is_required: bool,
115 dep_class: type[BaseCharacteristic[Any]],
116 ) -> Any | None: # noqa: ANN401 # Dependency can be any characteristic type
117 """Read and parse a single dependency characteristic.
119 Args:
120 dep_uuid: UUID of the dependency characteristic
121 is_required: Whether this is a required dependency
122 dep_class: The dependency characteristic class
124 Returns:
125 Parsed characteristic data, or None if optional and failed
127 Raises:
128 ValueError: If required dependency fails to read
130 """
131 dep_uuid_str = str(dep_uuid)
133 try:
134 raw_data = await self._connection_manager.read_gatt_char(dep_uuid)
136 char_instance = self._connected.get_cached_characteristic(dep_uuid)
137 if char_instance is None:
138 char_class_or_none = CharacteristicRegistry.get_characteristic_class_by_uuid(dep_uuid)
139 if char_class_or_none:
140 char_instance = char_class_or_none()
141 else:
142 char_info = CharacteristicInfo(uuid=dep_uuid, name=dep_uuid_str)
143 char_instance = UnknownCharacteristic(info=char_info)
145 self._connected.cache_characteristic(dep_uuid, char_instance)
147 return char_instance.parse_value(raw_data)
149 except Exception as e: # pylint: disable=broad-exception-caught
150 if is_required:
151 raise ValueError(
152 f"Failed to read required dependency {dep_class.__name__} ({dep_uuid_str}): {e}"
153 ) from e
154 logger.warning("Failed to read optional dependency %s: %s", dep_class.__name__, e)
155 return None
158class DependencyResolutionMode(Enum):
159 """Mode for automatic dependency resolution during characteristic reads.
161 Attributes:
162 NORMAL: Auto-resolve dependencies, use cache when available
163 SKIP_DEPENDENCIES: Skip dependency resolution and validation
164 FORCE_REFRESH: Re-read dependencies from device, ignoring cache
166 """
168 NORMAL = "normal"
169 SKIP_DEPENDENCIES = "skip_dependencies"
170 FORCE_REFRESH = "force_refresh"