Coverage for src / bluetooth_sig / core / query.py: 85%
134 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"""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 import ServiceName
15from ..gatt.services.registry import GattServiceRegistry
16from ..gatt.uuid_registry import uuid_registry
17from ..types import (
18 CharacteristicInfo,
19 ServiceInfo,
20 SIGInfo,
21)
22from ..types.gatt_enums import CharacteristicName
23from ..types.uuid import BluetoothUUID
25logger = logging.getLogger(__name__)
28class CharacteristicQueryEngine:
29 """Stateless query engine for characteristic and service metadata.
31 Provides all read-only lookup operations: supports, get_value_type,
32 get_*_info_*, list_supported_*, get_service_characteristics, get_sig_info_*.
33 """
35 def supports(self, uuid: str) -> bool:
36 """Check if a characteristic UUID is supported.
38 Args:
39 uuid: The characteristic UUID to check
41 Returns:
42 True if the characteristic has a parser/encoder, False otherwise
44 """
45 try:
46 bt_uuid = BluetoothUUID(uuid)
47 char_class = CharacteristicRegistry.get_characteristic_class_by_uuid(bt_uuid)
48 except (ValueError, TypeError):
49 return False
50 else:
51 return char_class is not None
53 def get_value_type(self, uuid: str) -> type | str | None:
54 """Get the expected Python type for a characteristic.
56 Args:
57 uuid: The characteristic UUID (16-bit short form or full 128-bit)
59 Returns:
60 Python type if characteristic is found, None otherwise
62 """
63 info = self.get_characteristic_info_by_uuid(uuid)
64 return info.python_type if info else None
66 def get_characteristic_info_by_uuid(self, uuid: str) -> CharacteristicInfo | None:
67 """Get information about a characteristic by UUID.
69 Args:
70 uuid: The characteristic UUID (16-bit short form or full 128-bit)
72 Returns:
73 CharacteristicInfo with metadata or None if not found
75 """
76 try:
77 bt_uuid = BluetoothUUID(uuid)
78 except ValueError:
79 return None
81 char_class = CharacteristicRegistry.get_characteristic_class_by_uuid(bt_uuid)
82 if not char_class:
83 return None
85 try:
86 temp_char = char_class()
87 except (TypeError, ValueError, AttributeError):
88 return None
89 else:
90 return temp_char.info
92 def get_characteristic_uuid_by_name(self, name: CharacteristicName) -> BluetoothUUID | None:
93 """Get the UUID for a characteristic name enum.
95 Args:
96 name: CharacteristicName enum
98 Returns:
99 Characteristic UUID or None if not found
101 """
102 info = self.get_characteristic_info_by_name(name)
103 return info.uuid if info else None
105 def get_service_uuid_by_name(self, name: str | ServiceName) -> BluetoothUUID | None:
106 """Get the UUID for a service name or enum.
108 Args:
109 name: Service name or enum
111 Returns:
112 Service UUID or None if not found
114 """
115 name_str = name.value if isinstance(name, ServiceName) else name
116 info = self.get_service_info_by_name(name_str)
117 return info.uuid if info else None
119 def get_characteristic_info_by_name(self, name: CharacteristicName) -> CharacteristicInfo | None:
120 """Get characteristic info by enum name.
122 Args:
123 name: CharacteristicName enum
125 Returns:
126 CharacteristicInfo if found, None otherwise
128 """
129 char_class = CharacteristicRegistry.get_characteristic_class(name)
130 if not char_class:
131 return None
133 info = char_class.get_configured_info()
134 if info:
135 return info
137 try:
138 temp_char = char_class()
139 except (TypeError, ValueError, AttributeError):
140 return None
141 else:
142 return temp_char.info
144 def get_service_info_by_name(self, name: str | ServiceName) -> ServiceInfo | None:
145 """Get service info by name or enum instead of UUID.
147 Args:
148 name: Service name string or ServiceName enum
150 Returns:
151 ServiceInfo if found, None otherwise
153 """
154 name_str = name.value if isinstance(name, ServiceName) else name
156 try:
157 uuid_info = uuid_registry.get_service_info(name_str)
158 if uuid_info:
159 return ServiceInfo(uuid=uuid_info.uuid, name=uuid_info.name, characteristics=[])
160 except Exception: # pylint: disable=broad-exception-caught
161 logger.warning("Failed to look up service info for name=%s", name_str, exc_info=True)
163 return None
165 def get_service_info_by_uuid(self, uuid: str) -> ServiceInfo | None:
166 """Get information about a service by UUID.
168 Args:
169 uuid: The service UUID
171 Returns:
172 ServiceInfo with metadata or None if not found
174 """
175 service_class = GattServiceRegistry.get_service_class_by_uuid(BluetoothUUID(uuid))
176 if not service_class:
177 return None
179 try:
180 temp_service = service_class()
181 char_infos: list[CharacteristicInfo] = []
182 for _, char_instance in temp_service.characteristics.items():
183 char_infos.append(char_instance.info)
184 return ServiceInfo(
185 uuid=temp_service.uuid,
186 name=temp_service.name,
187 characteristics=char_infos,
188 )
189 except (TypeError, ValueError, AttributeError):
190 return None
192 def list_supported_characteristics(self) -> dict[str, str]:
193 """List all supported characteristics with their names and UUIDs.
195 Returns:
196 Dictionary mapping characteristic names to UUIDs
198 """
199 result: dict[str, str] = {}
200 for name, char_class in CharacteristicRegistry.get_all_characteristics().items():
201 configured_info = char_class.get_configured_info()
202 if configured_info:
203 name_str = name.value if hasattr(name, "value") else str(name)
204 result[name_str] = str(configured_info.uuid)
205 return result
207 def list_supported_services(self) -> dict[str, str]:
208 """List all supported services with their names and UUIDs.
210 Returns:
211 Dictionary mapping service names to UUIDs
213 """
214 result: dict[str, str] = {}
215 for service_class in GattServiceRegistry.get_all_services():
216 try:
217 temp_service = service_class()
218 service_name = getattr(temp_service, "_service_name", service_class.__name__)
219 result[service_name] = str(temp_service.uuid)
220 except Exception: # pylint: disable=broad-exception-caught
221 continue
222 return result
224 def get_characteristics_info_by_uuids(self, uuids: list[str]) -> dict[str, CharacteristicInfo | None]:
225 """Get information about multiple characteristics by UUID.
227 Args:
228 uuids: List of characteristic UUIDs
230 Returns:
231 Dictionary mapping UUIDs to CharacteristicInfo (or None if not found)
233 """
234 results: dict[str, CharacteristicInfo | None] = {}
235 for uuid in uuids:
236 results[uuid] = self.get_characteristic_info_by_uuid(uuid)
237 return results
239 def get_service_characteristics(self, service_uuid: str) -> list[BaseCharacteristic[Any]]:
240 """Get the characteristic instances associated with a service.
242 Instantiates each required characteristic class from the service
243 definition and returns the live objects.
245 Args:
246 service_uuid: The service UUID
248 Returns:
249 List of BaseCharacteristic instances for this service's
250 required characteristics.
252 """
253 service_class = GattServiceRegistry.get_service_class_by_uuid(BluetoothUUID(service_uuid))
254 if not service_class:
255 return []
257 try:
258 temp_service = service_class()
259 required_chars = temp_service.get_required_characteristics()
260 result: list[BaseCharacteristic[Any]] = []
261 for spec in required_chars.values():
262 try:
263 result.append(spec.char_class())
264 except (TypeError, ValueError, AttributeError):
265 logger.debug("Could not instantiate %s", spec.char_class.__name__)
266 except Exception: # pylint: disable=broad-exception-caught
267 return []
268 else:
269 return result
271 def get_sig_info_by_name(self, name: str) -> SIGInfo | None:
272 """Get Bluetooth SIG information for a characteristic or service by name.
274 Args:
275 name: Characteristic or service name
277 Returns:
278 CharacteristicInfo or ServiceInfo if found, None otherwise
280 """
281 try:
282 char_info = uuid_registry.get_characteristic_info(name)
283 if char_info:
284 return CharacteristicInfo(
285 uuid=char_info.uuid,
286 name=char_info.name,
287 python_type=char_info.python_type,
288 unit=char_info.unit or "",
289 )
290 except (KeyError, ValueError, AttributeError):
291 logger.warning("Failed to look up SIG info by name: %s", name)
293 service_info = self.get_service_info_by_name(name)
294 if service_info:
295 return service_info
297 return None
299 def get_sig_info_by_uuid(self, uuid: str) -> SIGInfo | None:
300 """Get Bluetooth SIG information for a UUID.
302 Args:
303 uuid: UUID string (with or without dashes)
305 Returns:
306 CharacteristicInfo or ServiceInfo if found, None otherwise
308 """
309 char_info = self.get_characteristic_info_by_uuid(uuid)
310 if char_info:
311 return char_info
313 service_info = self.get_service_info_by_uuid(uuid)
314 if service_info:
315 return service_info
317 return None