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

1"""Characteristic and service query engine. 

2 

3Provides read-only lookup and metadata retrieval for characteristics and services 

4using the SIG registries. Stateless — no mutable state. 

5""" 

6 

7from __future__ import annotations 

8 

9import logging 

10from typing import Any 

11 

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 

23 

24logger = logging.getLogger(__name__) 

25 

26 

27class CharacteristicQueryEngine: 

28 """Stateless query engine for characteristic and service metadata. 

29 

30 Provides all read-only lookup operations: supports, get_value_type, 

31 get_*_info_*, list_supported_*, get_service_characteristics, get_sig_info_*. 

32 """ 

33 

34 def supports(self, uuid: str) -> bool: 

35 """Check if a characteristic UUID is supported. 

36 

37 Args: 

38 uuid: The characteristic UUID to check 

39 

40 Returns: 

41 True if the characteristic has a parser/encoder, False otherwise 

42 

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 

51 

52 def get_value_type(self, uuid: str) -> type | str | None: 

53 """Get the expected Python type for a characteristic. 

54 

55 Args: 

56 uuid: The characteristic UUID (16-bit short form or full 128-bit) 

57 

58 Returns: 

59 Python type if characteristic is found, None otherwise 

60 

61 """ 

62 info = self.get_characteristic_info_by_uuid(uuid) 

63 return info.python_type if info else None 

64 

65 def get_characteristic_info_by_uuid(self, uuid: str) -> CharacteristicInfo | None: 

66 """Get information about a characteristic by UUID. 

67 

68 Args: 

69 uuid: The characteristic UUID (16-bit short form or full 128-bit) 

70 

71 Returns: 

72 CharacteristicInfo with metadata or None if not found 

73 

74 """ 

75 try: 

76 bt_uuid = BluetoothUUID(uuid) 

77 except ValueError: 

78 return None 

79 

80 char_class = CharacteristicRegistry.get_characteristic_class_by_uuid(bt_uuid) 

81 if not char_class: 

82 return None 

83 

84 try: 

85 temp_char = char_class() 

86 except (TypeError, ValueError, AttributeError): 

87 return None 

88 else: 

89 return temp_char.info 

90 

91 def get_characteristic_uuid_by_name(self, name: CharacteristicName) -> BluetoothUUID | None: 

92 """Get the UUID for a characteristic name enum. 

93 

94 Args: 

95 name: CharacteristicName enum 

96 

97 Returns: 

98 Characteristic UUID or None if not found 

99 

100 """ 

101 info = self.get_characteristic_info_by_name(name) 

102 return info.uuid if info else None 

103 

104 def get_service_uuid_by_name(self, name: str | ServiceName) -> BluetoothUUID | None: 

105 """Get the UUID for a service name or enum. 

106 

107 Args: 

108 name: Service name or enum 

109 

110 Returns: 

111 Service UUID or None if not found 

112 

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 

117 

118 def get_characteristic_info_by_name(self, name: CharacteristicName) -> CharacteristicInfo | None: 

119 """Get characteristic info by enum name. 

120 

121 Args: 

122 name: CharacteristicName enum 

123 

124 Returns: 

125 CharacteristicInfo if found, None otherwise 

126 

127 """ 

128 char_class = CharacteristicRegistry.get_characteristic_class(name) 

129 if not char_class: 

130 return None 

131 

132 info = char_class.get_configured_info() 

133 if info: 

134 return info 

135 

136 try: 

137 temp_char = char_class() 

138 except (TypeError, ValueError, AttributeError): 

139 return None 

140 else: 

141 return temp_char.info 

142 

143 def get_service_info_by_name(self, name: str | ServiceName) -> ServiceInfo | None: 

144 """Get service info by name or enum instead of UUID. 

145 

146 Args: 

147 name: Service name string or ServiceName enum 

148 

149 Returns: 

150 ServiceInfo if found, None otherwise 

151 

152 """ 

153 name_str = name.value if isinstance(name, ServiceName) else name 

154 

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) 

161 

162 return None 

163 

164 def get_service_info_by_uuid(self, uuid: str) -> ServiceInfo | None: 

165 """Get information about a service by UUID. 

166 

167 Args: 

168 uuid: The service UUID 

169 

170 Returns: 

171 ServiceInfo with metadata or None if not found 

172 

173 """ 

174 service_class = GattServiceRegistry.get_service_class_by_uuid(BluetoothUUID(uuid)) 

175 if not service_class: 

176 return None 

177 

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 

190 

191 def list_supported_characteristics(self) -> dict[str, str]: 

192 """List all supported characteristics with their names and UUIDs. 

193 

194 Returns: 

195 Dictionary mapping characteristic names to UUIDs 

196 

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 

205 

206 def list_supported_services(self) -> dict[str, str]: 

207 """List all supported services with their names and UUIDs. 

208 

209 Returns: 

210 Dictionary mapping service names to UUIDs 

211 

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 

222 

223 def get_characteristics_info_by_uuids(self, uuids: list[str]) -> dict[str, CharacteristicInfo | None]: 

224 """Get information about multiple characteristics by UUID. 

225 

226 Args: 

227 uuids: List of characteristic UUIDs 

228 

229 Returns: 

230 Dictionary mapping UUIDs to CharacteristicInfo (or None if not found) 

231 

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 

237 

238 def get_service_characteristics(self, service_uuid: str) -> list[BaseCharacteristic[Any]]: 

239 """Get the characteristic instances associated with a service. 

240 

241 Instantiates each required characteristic class from the service 

242 definition and returns the live objects. 

243 

244 Args: 

245 service_uuid: The service UUID 

246 

247 Returns: 

248 List of BaseCharacteristic instances for this service's 

249 required characteristics. 

250 

251 """ 

252 service_class = GattServiceRegistry.get_service_class_by_uuid(BluetoothUUID(service_uuid)) 

253 if not service_class: 

254 return [] 

255 

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 

269 

270 def get_sig_info_by_name(self, name: str) -> SIGInfo | None: 

271 """Get Bluetooth SIG information for a characteristic or service by name. 

272 

273 Args: 

274 name: Characteristic or service name 

275 

276 Returns: 

277 CharacteristicInfo or ServiceInfo if found, None otherwise 

278 

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) 

291 

292 service_info = self.get_service_info_by_name(name) 

293 if service_info: 

294 return service_info 

295 

296 return None 

297 

298 def get_sig_info_by_uuid(self, uuid: str) -> SIGInfo | None: 

299 """Get Bluetooth SIG information for a UUID. 

300 

301 Args: 

302 uuid: UUID string (with or without dashes) 

303 

304 Returns: 

305 CharacteristicInfo or ServiceInfo if found, None otherwise 

306 

307 """ 

308 char_info = self.get_characteristic_info_by_uuid(uuid) 

309 if char_info: 

310 return char_info 

311 

312 service_info = self.get_service_info_by_uuid(uuid) 

313 if service_info: 

314 return service_info 

315 

316 return None