Coverage for src / bluetooth_sig / gatt / exceptions.py: 94%

153 statements  

« prev     ^ index     » next       coverage.py v7.13.1, created at 2026-01-11 20:14 +0000

1"""GATT exceptions for the Bluetooth SIG library.""" 

2 

3from __future__ import annotations 

4 

5from typing import Any 

6 

7from ..types import ParseFieldError as FieldError 

8from ..types import SpecialValueResult 

9from ..types.data_types import ValidationAccumulator 

10from ..types.uuid import BluetoothUUID 

11 

12 

13class BluetoothSIGError(Exception): 

14 """Base exception for all Bluetooth SIG related errors.""" 

15 

16 

17class CharacteristicError(BluetoothSIGError): 

18 """Base exception for characteristic-related errors.""" 

19 

20 

21class ServiceError(BluetoothSIGError): 

22 """Base exception for service-related errors.""" 

23 

24 

25class UUIDResolutionError(BluetoothSIGError): 

26 """Exception raised when UUID resolution fails.""" 

27 

28 def __init__(self, name: str, attempted_names: list[str] | None = None) -> None: 

29 """Initialise UUIDResolutionError. 

30 

31 Args: 

32 name: The name for which UUID resolution failed. 

33 attempted_names: List of attempted names. 

34 

35 """ 

36 self.name = name 

37 self.attempted_names = attempted_names or [] 

38 message = f"No UUID found for: {name}" 

39 if self.attempted_names: 

40 message += f". Tried: {', '.join(self.attempted_names)}" 

41 super().__init__(message) 

42 

43 

44class DataParsingError(CharacteristicError): 

45 """Exception raised when characteristic data parsing fails.""" 

46 

47 def __init__(self, characteristic: str, data: bytes | bytearray, reason: str) -> None: 

48 """Initialise DataParsingError. 

49 

50 Args: 

51 characteristic: The characteristic name. 

52 data: The raw data that failed to parse. 

53 reason: Reason for the parsing failure. 

54 

55 """ 

56 self.characteristic = characteristic 

57 self.data = data 

58 self.reason = reason 

59 hex_data = " ".join(f"{byte:02X}" for byte in data) 

60 message = f"Failed to parse {characteristic} data [{hex_data}]: {reason}" 

61 super().__init__(message) 

62 

63 

64class ParseFieldError(DataParsingError): 

65 """Exception raised when a specific field fails to parse. 

66 

67 This exception provides detailed context about which field failed, where it 

68 failed in the data, and why it failed. This enables actionable error messages 

69 and structured error reporting. 

70 

71 NOTE: This exception intentionally has more arguments than the standard limit 

72 to provide complete field-level diagnostic information. The additional parameters 

73 (field, offset) are essential for actionable error messages and field-level debugging. 

74 

75 Attributes: 

76 field: Name of the field that failed (e.g., "temperature", "flags") 

77 offset: Byte offset where the field starts in the raw data 

78 expected: Description of what was expected 

79 actual: Description of what was actually encountered 

80 

81 """ 

82 

83 # pylint: disable=too-many-arguments,too-many-positional-arguments 

84 # NOTE: More arguments than standard limit required for complete field-level diagnostics 

85 def __init__( 

86 self, 

87 characteristic: str, 

88 field: str, 

89 data: bytes | bytearray, 

90 reason: str, 

91 offset: int | None = None, 

92 ) -> None: 

93 """Initialise ParseFieldError. 

94 

95 Args: 

96 characteristic: The characteristic name. 

97 field: The field name. 

98 data: The raw data. 

99 reason: Reason for the parsing failure. 

100 offset: Optional offset in the data. 

101 

102 """ 

103 self.field = field 

104 self.offset = offset 

105 # Store the original reason before formatting 

106 self.field_reason = reason 

107 

108 # Format message with field and offset information 

109 field_info = f"field '{field}'" 

110 if offset is not None: 

111 field_info = f"{field_info} at offset {offset}" 

112 

113 detailed_reason = f"{field_info}: {reason}" 

114 super().__init__(characteristic, data, detailed_reason) 

115 

116 

117class DataEncodingError(CharacteristicError): 

118 """Exception raised when characteristic data encoding fails.""" 

119 

120 def __init__(self, characteristic: str, value: Any, reason: str) -> None: # noqa: ANN401 # Reports errors for any value type 

121 """Initialise DataEncodingError. 

122 

123 Args: 

124 characteristic: The characteristic name. 

125 value: The value that failed to encode. 

126 reason: Reason for the encoding failure. 

127 

128 """ 

129 self.characteristic = characteristic 

130 self.value = value 

131 self.reason = reason 

132 message = f"Failed to encode {characteristic} value {value}: {reason}" 

133 super().__init__(message) 

134 

135 

136class DataValidationError(CharacteristicError): 

137 """Exception raised when characteristic data validation fails.""" 

138 

139 def __init__(self, field: str, value: Any, expected: str) -> None: # noqa: ANN401 # Validates any value type 

140 """Initialise DataValidationError. 

141 

142 Args: 

143 field: The field name. 

144 value: The value that failed validation. 

145 expected: Expected value or description. 

146 

147 """ 

148 self.field = field 

149 self.value = value 

150 self.expected = expected 

151 message = f"Invalid {field}: {value} (expected {expected})" 

152 super().__init__(message) 

153 

154 

155class InsufficientDataError(DataParsingError): 

156 """Exception raised when there is insufficient data for parsing.""" 

157 

158 def __init__(self, characteristic: str, data: bytes | bytearray, required: int) -> None: 

159 """Initialise InsufficientDataError. 

160 

161 Args: 

162 characteristic: The characteristic name. 

163 data: The raw data. 

164 required: Required length of data. 

165 

166 """ 

167 self.required = required 

168 self.actual = len(data) 

169 reason = f"need {required} bytes, got {self.actual}" 

170 super().__init__(characteristic, data, reason) 

171 

172 

173class ValueRangeError(DataValidationError): 

174 """Exception raised when a value is outside the expected range.""" 

175 

176 def __init__(self, field: str, value: Any, min_val: Any, max_val: Any) -> None: # noqa: ANN401 # Validates ranges for various numeric types 

177 """Initialise RangeValidationError. 

178 

179 Args: 

180 field: The field name. 

181 value: The value to validate. 

182 min_val: Minimum valid value. 

183 max_val: Maximum valid value. 

184 

185 """ 

186 self.min_val = min_val 

187 self.max_val = max_val 

188 expected = f"range [{min_val}, {max_val}]" 

189 super().__init__(field, value, expected) 

190 

191 

192class TypeMismatchError(DataValidationError): 

193 """Exception raised when a value has an unexpected type.""" 

194 

195 expected_type: type | tuple[type, ...] 

196 actual_type: type 

197 

198 def __init__(self, field: str, value: Any, expected_type: type | tuple[type, ...]) -> None: # noqa: ANN401 # Reports type errors for any value 

199 """Initialise TypeValidationError. 

200 

201 Args: 

202 field: The field name. 

203 value: The value to validate. 

204 expected_type: Expected type(s). 

205 

206 """ 

207 self.expected_type = expected_type 

208 self.actual_type = type(value) 

209 

210 # Handle tuple of types for display 

211 if isinstance(expected_type, tuple): 

212 type_names = " or ".join(t.__name__ for t in expected_type) 

213 expected = f"type {type_names}, got {self.actual_type.__name__}" 

214 else: 

215 expected = f"type {expected_type.__name__}, got {self.actual_type.__name__}" 

216 

217 super().__init__(field, value, expected) 

218 

219 

220class MissingDependencyError(CharacteristicError): 

221 """Exception raised when a required dependency is missing for multi-characteristic parsing.""" 

222 

223 def __init__(self, characteristic: str, missing_dependencies: list[str]) -> None: 

224 """Initialise DependencyValidationError. 

225 

226 Args: 

227 characteristic: The characteristic name. 

228 missing_dependencies: List of missing dependencies. 

229 

230 """ 

231 self.characteristic = characteristic 

232 self.missing_dependencies = missing_dependencies 

233 dep_list = ", ".join(missing_dependencies) 

234 message = f"{characteristic} requires missing dependencies: {dep_list}" 

235 super().__init__(message) 

236 

237 

238class EnumValueError(DataValidationError): 

239 """Exception raised when an enum value is invalid.""" 

240 

241 def __init__(self, field: str, value: Any, enum_class: type, valid_values: list[Any]) -> None: # noqa: ANN401 # Validates enum values of any type 

242 """Initialise EnumValidationError. 

243 

244 Args: 

245 field: The field name. 

246 value: The value to validate. 

247 enum_class: Enum class for validation. 

248 valid_values: List of valid values. 

249 

250 """ 

251 self.enum_class = enum_class 

252 self.valid_values = valid_values 

253 expected = f"{enum_class.__name__} value from {valid_values}" 

254 super().__init__(field, value, expected) 

255 

256 

257class IEEE11073Error(DataParsingError): 

258 """Exception raised when IEEE 11073 format parsing fails.""" 

259 

260 def __init__(self, data: bytes | bytearray, format_type: str, reason: str) -> None: 

261 """Initialise DataFormatError. 

262 

263 Args: 

264 data: The raw data. 

265 format_type: Format type expected. 

266 reason: Reason for the format error. 

267 

268 """ 

269 self.format_type = format_type 

270 characteristic = f"IEEE 11073 {format_type}" 

271 super().__init__(characteristic, data, reason) 

272 

273 

274class YAMLResolutionError(BluetoothSIGError): 

275 """Exception raised when YAML specification resolution fails.""" 

276 

277 def __init__(self, name: str, yaml_type: str) -> None: 

278 """Initialise YAMLSchemaError. 

279 

280 Args: 

281 name: Name of the YAML entity. 

282 yaml_type: Type of the YAML entity. 

283 

284 """ 

285 self.name = name 

286 self.yaml_type = yaml_type 

287 message = f"Failed to resolve {yaml_type} specification for: {name}" 

288 super().__init__(message) 

289 

290 

291class ServiceCharacteristicMismatchError(ServiceError): 

292 """Exception raised when expected characteristics are not found in a service. 

293 

294 service. 

295 """ 

296 

297 def __init__(self, service: str, missing_characteristics: list[str]) -> None: 

298 """Initialise ExpectedCharacteristicNotFound. 

299 

300 Args: 

301 service: The service name. 

302 missing_characteristics: List of missing characteristics. 

303 

304 """ 

305 self.service = service 

306 self.missing_characteristics = missing_characteristics 

307 message = f"Service {service} missing required characteristics: {', '.join(missing_characteristics)}" 

308 super().__init__(message) 

309 

310 

311class TemplateConfigurationError(CharacteristicError): 

312 """Exception raised when a template is incorrectly configured.""" 

313 

314 def __init__(self, template: str, configuration_issue: str) -> None: 

315 """Initialise TemplateConfigurationError. 

316 

317 Args: 

318 template: The template name. 

319 configuration_issue: Description of the configuration issue. 

320 

321 """ 

322 self.template = template 

323 self.configuration_issue = configuration_issue 

324 message = f"Template {template} configuration error: {configuration_issue}" 

325 super().__init__(message) 

326 

327 

328class UUIDRequiredError(BluetoothSIGError): 

329 """Exception raised when a UUID is required but not provided or invalid.""" 

330 

331 def __init__(self, class_name: str, entity_type: str) -> None: 

332 """Initialise EntityRegistrationError. 

333 

334 Args: 

335 class_name: Name of the class. 

336 entity_type: Type of the entity. 

337 

338 """ 

339 self.class_name = class_name 

340 self.entity_type = entity_type 

341 message = ( 

342 f"Custom {entity_type} '{class_name}' requires a valid UUID. " 

343 f"Provide a non-empty UUID when instantiating custom {entity_type}s." 

344 ) 

345 super().__init__(message) 

346 

347 

348class UUIDCollisionError(BluetoothSIGError): 

349 """Exception raised when attempting to use a UUID that already exists in SIG registry.""" 

350 

351 def __init__(self, uuid: BluetoothUUID | str, existing_name: str, class_name: str) -> None: 

352 """Initialise UUIDRegistrationError. 

353 

354 Args: 

355 uuid: The UUID value. 

356 existing_name: Existing name for the UUID. 

357 class_name: Name of the class. 

358 

359 """ 

360 self.uuid = uuid 

361 self.existing_name = existing_name 

362 self.class_name = class_name 

363 message = ( 

364 f"UUID '{uuid}' is already used by SIG characteristic '{existing_name}'. " 

365 f"Cannot create custom characteristic '{class_name}' with existing SIG UUID. " 

366 f"Use allow_sig_override=True if you intentionally want to override this SIG characteristic." 

367 ) 

368 super().__init__(message) 

369 

370 

371class CharacteristicParseError(CharacteristicError): 

372 """Raised when characteristic parsing fails. 

373 

374 Preserves all debugging context from parsing attempt. 

375 

376 Attributes: 

377 message: Human-readable error message 

378 name: Characteristic name 

379 uuid: Characteristic UUID 

380 raw_data: Exact bytes that failed (useful: hex dump debugging) 

381 raw_int: Extracted integer value (useful: check bit patterns) 

382 field_errors: Field-level errors (useful: complex multi-field characteristics) 

383 parse_trace: Step-by-step execution log (useful: debug parser flow) 

384 validation: Accumulated validation results (useful: see all warnings/errors) 

385 

386 """ 

387 

388 # pylint: disable=too-many-arguments,too-many-positional-arguments 

389 # NOTE: Multiple arguments required for complete diagnostic context 

390 def __init__( 

391 self, 

392 message: str, 

393 name: str, 

394 uuid: BluetoothUUID, 

395 raw_data: bytes, 

396 raw_int: int | None = None, 

397 field_errors: list[FieldError] | None = None, 

398 parse_trace: list[str] | None = None, 

399 validation: ValidationAccumulator | None = None, 

400 ) -> None: 

401 """Initialize parse error with diagnostic context. 

402 

403 Args: 

404 message: Human-readable error message 

405 name: Characteristic name 

406 uuid: Characteristic UUID 

407 raw_data: Raw bytes that failed to parse 

408 raw_int: Extracted integer (if extraction succeeded) 

409 field_errors: Field-level parsing errors 

410 parse_trace: Step-by-step execution log 

411 validation: Accumulated validation results 

412 

413 """ 

414 super().__init__(message) 

415 self.name = name 

416 self.uuid = uuid 

417 self.raw_data = raw_data 

418 self.raw_int = raw_int 

419 self.field_errors = field_errors or [] 

420 self.parse_trace = parse_trace or [] 

421 self.validation = validation 

422 

423 def __str__(self) -> str: 

424 """Format error with field-level details.""" 

425 base = f"{self.name} ({self.uuid}): {self.args[0]}" 

426 if self.field_errors: 

427 field_msgs = [f" - {e.field}: {e.reason}" for e in self.field_errors] 

428 return f"{base}\nField errors:\n" + "\n".join(field_msgs) 

429 return base 

430 

431 

432class SpecialValueDetected(CharacteristicError): 

433 """Raised when a special sentinel value is detected. 

434 

435 Special values represent exceptional conditions: "value is not known", "NaN", 

436 "measurement not possible", etc. These are semantically distinct from parse failures. 

437 

438 This exception is raised when a valid parse detects a special sentinel value 

439 (e.g., 0x8000 = "value is not known", 0x7FFFFFFF = "NaN"). The parsing succeeded, 

440 but the result indicates an exceptional state rather than a normal value. 

441 

442 Most code should catch this separately from CharacteristicParseError to distinguish: 

443 - Parse failure (malformed data, wrong length, invalid format) 

444 - Special value detection (well-formed data indicating exceptional state) 

445 

446 Attributes: 

447 special_value: The detected special value with meaning and raw bytes 

448 name: Characteristic name 

449 uuid: Characteristic UUID 

450 raw_data: Raw bytes containing the special value 

451 raw_int: The raw integer value (typically the sentinel value) 

452 

453 """ 

454 

455 # pylint: disable=too-many-arguments,too-many-positional-arguments 

456 def __init__( 

457 self, 

458 special_value: SpecialValueResult, 

459 name: str, 

460 uuid: BluetoothUUID, 

461 raw_data: bytes, 

462 raw_int: int | None = None, 

463 ) -> None: 

464 """Initialize special value detected exception. 

465 

466 Args: 

467 special_value: The detected SpecialValueResult 

468 name: Characteristic name 

469 uuid: Characteristic UUID 

470 raw_data: Raw bytes containing the special value 

471 raw_int: The raw integer value 

472 

473 """ 

474 message = f"{name} ({uuid}): Special value detected: {special_value.meaning}" 

475 super().__init__(message) 

476 self.special_value = special_value 

477 self.name = name 

478 self.uuid = uuid 

479 self.raw_data = raw_data 

480 self.raw_int = raw_int 

481 

482 

483class CharacteristicEncodeError(CharacteristicError): 

484 """Raised when characteristic encoding fails. 

485 

486 Attributes: 

487 message: Human-readable error message 

488 name: Characteristic name 

489 uuid: Characteristic UUID 

490 value: The value that failed to encode 

491 validation: Accumulated validation results 

492 

493 """ 

494 

495 # pylint: disable=too-many-arguments,too-many-positional-arguments 

496 def __init__( 

497 self, 

498 message: str, 

499 name: str, 

500 uuid: BluetoothUUID, 

501 value: Any, # noqa: ANN401 # Any value type 

502 validation: ValidationAccumulator | None = None, 

503 ) -> None: 

504 """Initialize encode error. 

505 

506 Args: 

507 message: Human-readable error message 

508 name: Characteristic name 

509 uuid: Characteristic UUID 

510 value: The value that failed to encode 

511 validation: Accumulated validation results 

512 

513 """ 

514 super().__init__(message) 

515 self.name = name 

516 self.uuid = uuid 

517 self.value = value 

518 self.validation = validation