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

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 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 

24 

25logger = logging.getLogger(__name__) 

26 

27 

28class CharacteristicQueryEngine: 

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

30 

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

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

33 """ 

34 

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

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

37 

38 Args: 

39 uuid: The characteristic UUID to check 

40 

41 Returns: 

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

43 

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 

52 

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

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

55 

56 Args: 

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

58 

59 Returns: 

60 Python type if characteristic is found, None otherwise 

61 

62 """ 

63 info = self.get_characteristic_info_by_uuid(uuid) 

64 return info.python_type if info else None 

65 

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

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

68 

69 Args: 

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

71 

72 Returns: 

73 CharacteristicInfo with metadata or None if not found 

74 

75 """ 

76 try: 

77 bt_uuid = BluetoothUUID(uuid) 

78 except ValueError: 

79 return None 

80 

81 char_class = CharacteristicRegistry.get_characteristic_class_by_uuid(bt_uuid) 

82 if not char_class: 

83 return None 

84 

85 try: 

86 temp_char = char_class() 

87 except (TypeError, ValueError, AttributeError): 

88 return None 

89 else: 

90 return temp_char.info 

91 

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

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

94 

95 Args: 

96 name: CharacteristicName enum 

97 

98 Returns: 

99 Characteristic UUID or None if not found 

100 

101 """ 

102 info = self.get_characteristic_info_by_name(name) 

103 return info.uuid if info else None 

104 

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

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

107 

108 Args: 

109 name: Service name or enum 

110 

111 Returns: 

112 Service UUID or None if not found 

113 

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 

118 

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

120 """Get characteristic info by enum name. 

121 

122 Args: 

123 name: CharacteristicName enum 

124 

125 Returns: 

126 CharacteristicInfo if found, None otherwise 

127 

128 """ 

129 char_class = CharacteristicRegistry.get_characteristic_class(name) 

130 if not char_class: 

131 return None 

132 

133 info = char_class.get_configured_info() 

134 if info: 

135 return info 

136 

137 try: 

138 temp_char = char_class() 

139 except (TypeError, ValueError, AttributeError): 

140 return None 

141 else: 

142 return temp_char.info 

143 

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

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

146 

147 Args: 

148 name: Service name string or ServiceName enum 

149 

150 Returns: 

151 ServiceInfo if found, None otherwise 

152 

153 """ 

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

155 

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) 

162 

163 return None 

164 

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

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

167 

168 Args: 

169 uuid: The service UUID 

170 

171 Returns: 

172 ServiceInfo with metadata or None if not found 

173 

174 """ 

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

176 if not service_class: 

177 return None 

178 

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 

191 

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

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

194 

195 Returns: 

196 Dictionary mapping characteristic names to UUIDs 

197 

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 

206 

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

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

209 

210 Returns: 

211 Dictionary mapping service names to UUIDs 

212 

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 

223 

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

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

226 

227 Args: 

228 uuids: List of characteristic UUIDs 

229 

230 Returns: 

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

232 

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 

238 

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

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

241 

242 Instantiates each required characteristic class from the service 

243 definition and returns the live objects. 

244 

245 Args: 

246 service_uuid: The service UUID 

247 

248 Returns: 

249 List of BaseCharacteristic instances for this service's 

250 required characteristics. 

251 

252 """ 

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

254 if not service_class: 

255 return [] 

256 

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 

270 

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

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

273 

274 Args: 

275 name: Characteristic or service name 

276 

277 Returns: 

278 CharacteristicInfo or ServiceInfo if found, None otherwise 

279 

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) 

292 

293 service_info = self.get_service_info_by_name(name) 

294 if service_info: 

295 return service_info 

296 

297 return None 

298 

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

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

301 

302 Args: 

303 uuid: UUID string (with or without dashes) 

304 

305 Returns: 

306 CharacteristicInfo or ServiceInfo if found, None otherwise 

307 

308 """ 

309 char_info = self.get_characteristic_info_by_uuid(uuid) 

310 if char_info: 

311 return char_info 

312 

313 service_info = self.get_service_info_by_uuid(uuid) 

314 if service_info: 

315 return service_info 

316 

317 return None