Coverage for src/bluetooth_sig/core/query.py: 85%
133 statements
« prev ^ index » next coverage.py v7.14.3, created at 2026-06-28 01:26 +0000
« prev ^ index » next coverage.py v7.14.3, created at 2026-06-28 01:26 +0000
1"""Characteristic and service query engine.
3Provides read-only lookup and metadata retrieval for characteristics and services
4using the SIG registries. Stateless — no mutable state.
5"""
7from __future__ import annotations
9import logging
10from typing import Any
12from ..gatt.characteristics.base import BaseCharacteristic
13from ..gatt.characteristics.registry import CharacteristicRegistry
14from ..gatt.services.registry import GattServiceRegistry
15from ..gatt.uuid_registry import get_uuid_registry
16from ..types import (
17 CharacteristicInfo,
18 ServiceInfo,
19 SIGInfo,
20)
21from ..types.gatt_enums import CharacteristicName, ServiceName
22from ..types.uuid import BluetoothUUID
24logger = logging.getLogger(__name__)
27class CharacteristicQueryEngine:
28 """Stateless query engine for characteristic and service metadata.
30 Provides all read-only lookup operations: supports, get_value_type,
31 get_*_info_*, list_supported_*, get_service_characteristics, get_sig_info_*.
32 """
34 def supports(self, uuid: str) -> bool:
35 """Check if a characteristic UUID is supported.
37 Args:
38 uuid: The characteristic UUID to check
40 Returns:
41 True if the characteristic has a parser/encoder, False otherwise
43 """
44 try:
45 bt_uuid = BluetoothUUID(uuid)
46 char_class = CharacteristicRegistry.get_characteristic_class_by_uuid(bt_uuid)
47 except (ValueError, TypeError):
48 return False
49 else:
50 return char_class is not None
52 def get_value_type(self, uuid: str) -> type | str | None:
53 """Get the expected Python type for a characteristic.
55 Args:
56 uuid: The characteristic UUID (16-bit short form or full 128-bit)
58 Returns:
59 Python type if characteristic is found, None otherwise
61 """
62 info = self.get_characteristic_info_by_uuid(uuid)
63 return info.python_type if info else None
65 def get_characteristic_info_by_uuid(self, uuid: str) -> CharacteristicInfo | None:
66 """Get information about a characteristic by UUID.
68 Args:
69 uuid: The characteristic UUID (16-bit short form or full 128-bit)
71 Returns:
72 CharacteristicInfo with metadata or None if not found
74 """
75 try:
76 bt_uuid = BluetoothUUID(uuid)
77 except ValueError:
78 return None
80 char_class = CharacteristicRegistry.get_characteristic_class_by_uuid(bt_uuid)
81 if not char_class:
82 return None
84 try:
85 temp_char = char_class()
86 except (TypeError, ValueError, AttributeError):
87 return None
88 else:
89 return temp_char.info
91 def get_characteristic_uuid_by_name(self, name: CharacteristicName) -> BluetoothUUID | None:
92 """Get the UUID for a characteristic name enum.
94 Args:
95 name: CharacteristicName enum
97 Returns:
98 Characteristic UUID or None if not found
100 """
101 info = self.get_characteristic_info_by_name(name)
102 return info.uuid if info else None
104 def get_service_uuid_by_name(self, name: str | ServiceName) -> BluetoothUUID | None:
105 """Get the UUID for a service name or enum.
107 Args:
108 name: Service name or enum
110 Returns:
111 Service UUID or None if not found
113 """
114 name_str = name.value if isinstance(name, ServiceName) else name
115 info = self.get_service_info_by_name(name_str)
116 return info.uuid if info else None
118 def get_characteristic_info_by_name(self, name: CharacteristicName) -> CharacteristicInfo | None:
119 """Get characteristic info by enum name.
121 Args:
122 name: CharacteristicName enum
124 Returns:
125 CharacteristicInfo if found, None otherwise
127 """
128 char_class = CharacteristicRegistry.get_characteristic_class(name)
129 if not char_class:
130 return None
132 info = char_class.get_configured_info()
133 if info:
134 return info
136 try:
137 temp_char = char_class()
138 except (TypeError, ValueError, AttributeError):
139 return None
140 else:
141 return temp_char.info
143 def get_service_info_by_name(self, name: str | ServiceName) -> ServiceInfo | None:
144 """Get service info by name or enum instead of UUID.
146 Args:
147 name: Service name string or ServiceName enum
149 Returns:
150 ServiceInfo if found, None otherwise
152 """
153 name_str = name.value if isinstance(name, ServiceName) else name
155 try:
156 uuid_info = get_uuid_registry().get_service_info(name_str)
157 if uuid_info:
158 return ServiceInfo(uuid=uuid_info.uuid, name=uuid_info.name, characteristics=[])
159 except Exception: # pylint: disable=broad-exception-caught
160 logger.warning("Failed to look up service info for name=%s", name_str, exc_info=True)
162 return None
164 def get_service_info_by_uuid(self, uuid: str) -> ServiceInfo | None:
165 """Get information about a service by UUID.
167 Args:
168 uuid: The service UUID
170 Returns:
171 ServiceInfo with metadata or None if not found
173 """
174 service_class = GattServiceRegistry.get_service_class_by_uuid(BluetoothUUID(uuid))
175 if not service_class:
176 return None
178 try:
179 temp_service = service_class()
180 char_infos: list[CharacteristicInfo] = []
181 for _, char_instance in temp_service.characteristics.items():
182 char_infos.append(char_instance.info)
183 return ServiceInfo(
184 uuid=temp_service.uuid,
185 name=temp_service.name,
186 characteristics=char_infos,
187 )
188 except (TypeError, ValueError, AttributeError):
189 return None
191 def list_supported_characteristics(self) -> dict[str, str]:
192 """List all supported characteristics with their names and UUIDs.
194 Returns:
195 Dictionary mapping characteristic names to UUIDs
197 """
198 result: dict[str, str] = {}
199 for name, char_class in CharacteristicRegistry.get_all_characteristics().items():
200 configured_info = char_class.get_configured_info()
201 if configured_info:
202 name_str = name.value if hasattr(name, "value") else str(name)
203 result[name_str] = str(configured_info.uuid)
204 return result
206 def list_supported_services(self) -> dict[str, str]:
207 """List all supported services with their names and UUIDs.
209 Returns:
210 Dictionary mapping service names to UUIDs
212 """
213 result: dict[str, str] = {}
214 for service_class in GattServiceRegistry.get_all_services():
215 try:
216 temp_service = service_class()
217 service_name = getattr(temp_service, "_service_name", service_class.__name__)
218 result[service_name] = str(temp_service.uuid)
219 except Exception: # pylint: disable=broad-exception-caught
220 continue
221 return result
223 def get_characteristics_info_by_uuids(self, uuids: list[str]) -> dict[str, CharacteristicInfo | None]:
224 """Get information about multiple characteristics by UUID.
226 Args:
227 uuids: List of characteristic UUIDs
229 Returns:
230 Dictionary mapping UUIDs to CharacteristicInfo (or None if not found)
232 """
233 results: dict[str, CharacteristicInfo | None] = {}
234 for uuid in uuids:
235 results[uuid] = self.get_characteristic_info_by_uuid(uuid)
236 return results
238 def get_service_characteristics(self, service_uuid: str) -> list[BaseCharacteristic[Any]]:
239 """Get the characteristic instances associated with a service.
241 Instantiates each required characteristic class from the service
242 definition and returns the live objects.
244 Args:
245 service_uuid: The service UUID
247 Returns:
248 List of BaseCharacteristic instances for this service's
249 required characteristics.
251 """
252 service_class = GattServiceRegistry.get_service_class_by_uuid(BluetoothUUID(service_uuid))
253 if not service_class:
254 return []
256 try:
257 temp_service = service_class()
258 required_chars = temp_service.get_required_characteristics()
259 result: list[BaseCharacteristic[Any]] = []
260 for spec in required_chars.values():
261 try:
262 result.append(spec.char_class())
263 except (TypeError, ValueError, AttributeError):
264 logger.debug("Could not instantiate %s", spec.char_class.__name__)
265 except Exception: # pylint: disable=broad-exception-caught
266 return []
267 else:
268 return result
270 def get_sig_info_by_name(self, name: str) -> SIGInfo | None:
271 """Get Bluetooth SIG information for a characteristic or service by name.
273 Args:
274 name: Characteristic or service name
276 Returns:
277 CharacteristicInfo or ServiceInfo if found, None otherwise
279 """
280 try:
281 char_info = get_uuid_registry().get_characteristic_info(name)
282 if char_info:
283 return CharacteristicInfo(
284 uuid=char_info.uuid,
285 name=char_info.name,
286 python_type=char_info.python_type,
287 unit=char_info.unit or "",
288 )
289 except (KeyError, ValueError, AttributeError):
290 logger.warning("Failed to look up SIG info by name: %s", name)
292 service_info = self.get_service_info_by_name(name)
293 if service_info:
294 return service_info
296 return None
298 def get_sig_info_by_uuid(self, uuid: str) -> SIGInfo | None:
299 """Get Bluetooth SIG information for a UUID.
301 Args:
302 uuid: UUID string (with or without dashes)
304 Returns:
305 CharacteristicInfo or ServiceInfo if found, None otherwise
307 """
308 char_info = self.get_characteristic_info_by_uuid(uuid)
309 if char_info:
310 return char_info
312 service_info = self.get_service_info_by_uuid(uuid)
313 if service_info:
314 return service_info
316 return None