Coverage for src / bluetooth_sig / gatt / services / base.py: 82%

327 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-03-18 11:17 +0000

1"""Base class for GATT service implementations.""" 

2 

3from __future__ import annotations 

4 

5from enum import Enum 

6from typing import Any, TypeVar, cast 

7 

8import msgspec 

9 

10from ...types import CharacteristicInfo, ServiceInfo 

11from ...types.gatt_services import CharacteristicCollection, CharacteristicSpec, ServiceDiscoveryData 

12from ...types.uuid import BluetoothUUID 

13from ..characteristics import BaseCharacteristic, CharacteristicRegistry 

14from ..characteristics.registry import CharacteristicName 

15from ..characteristics.unknown import UnknownCharacteristic 

16from ..exceptions import UUIDResolutionError 

17from ..resolver import ServiceRegistrySearch 

18from ..uuid_registry import uuid_registry 

19 

20# Type aliases 

21GattCharacteristic = BaseCharacteristic 

22# Strong-typed collection with enum keys 

23ServiceCharacteristicCollection = CharacteristicCollection # Alias for compatibility 

24 

25# Generic type variable for service-specific characteristic definitions 

26ServiceCharacteristics = TypeVar("ServiceCharacteristics") 

27 

28 

29# Internal collections are plain dicts keyed by `CharacteristicName` enums. 

30# Do not perform implicit string-based lookups here; callers must convert 

31# strings to `CharacteristicName` explicitly at public boundaries. 

32 

33 

34class ServiceValidationConfig(msgspec.Struct, kw_only=True): 

35 """Configuration for service validation constraints. 

36 

37 Groups validation parameters into a single, optional configuration object 

38 to simplify BaseGattService constructor signatures. 

39 """ 

40 

41 strict_validation: bool = False 

42 require_all_optional: bool = False 

43 

44 

45class SIGServiceResolver: 

46 """Resolves SIG service information from registry. 

47 

48 This class handles all SIG service resolution logic, separating 

49 concerns from the BaseGattService constructor. Uses shared utilities 

50 from the resolver module to avoid code duplication with characteristic resolution. 

51 """ 

52 

53 @staticmethod 

54 def resolve_for_class(service_class: type[BaseGattService]) -> ServiceInfo: 

55 """Resolve ServiceInfo for a SIG service class. 

56 

57 Args: 

58 service_class: The service class to resolve info for 

59 

60 Returns: 

61 ServiceInfo with resolved UUID, name, summary 

62 

63 Raises: 

64 UUIDResolutionError: If no UUID can be resolved for the class 

65 

66 """ 

67 # Try configured info first (for custom services) 

68 if hasattr(service_class, "_info") and service_class._info is not None: # pylint: disable=protected-access 

69 # NOTE: _info is a class attribute used for custom characteristic/service definitions 

70 # This is the established pattern in the codebase for providing static metadata 

71 # Protected access is necessary to maintain API consistency 

72 return service_class._info # pylint: disable=protected-access 

73 

74 # Try registry resolution 

75 registry_info = SIGServiceResolver.resolve_from_registry(service_class) 

76 if registry_info: 

77 return registry_info 

78 

79 # No resolution found 

80 raise UUIDResolutionError(service_class.__name__, [service_class.__name__]) 

81 

82 @staticmethod 

83 def resolve_from_registry(service_class: type[BaseGattService]) -> ServiceInfo | None: 

84 """Resolve service info from registry using shared search strategy.""" 

85 # Use shared registry search strategy 

86 search_strategy = ServiceRegistrySearch() 

87 service_name = getattr(service_class, "_service_name", None) 

88 return search_strategy.search(service_class, service_name) 

89 

90 

91class ServiceHealthStatus(Enum): 

92 """Health status of a GATT service.""" 

93 

94 COMPLETE = "complete" # All required characteristics present 

95 FUNCTIONAL = "functional" # Required characteristics present, some optional missing 

96 PARTIAL = "partial" # Some required characteristics missing but service still usable 

97 INCOMPLETE = "incomplete" # Critical required characteristics missing 

98 

99 

100class CharacteristicStatus(Enum): 

101 """Status of characteristics within a service.""" 

102 

103 PRESENT = "present" # Characteristic is present and functional 

104 MISSING = "missing" # Expected characteristic not found 

105 INVALID = "invalid" # Characteristic found but invalid/unusable 

106 

107 

108class ServiceValidationResult(msgspec.Struct, kw_only=True): 

109 """Result of service validation.""" 

110 

111 status: ServiceHealthStatus 

112 missing_required: list[BaseCharacteristic[Any]] = msgspec.field(default_factory=list) 

113 missing_optional: list[BaseCharacteristic[Any]] = msgspec.field(default_factory=list) 

114 invalid_characteristics: list[BaseCharacteristic[Any]] = msgspec.field(default_factory=list) 

115 warnings: list[str] = msgspec.field(default_factory=list) 

116 errors: list[str] = msgspec.field(default_factory=list) 

117 

118 @property 

119 def is_healthy(self) -> bool: 

120 """Check if service is in a healthy state.""" 

121 return self.status in ( 

122 ServiceHealthStatus.COMPLETE, 

123 ServiceHealthStatus.FUNCTIONAL, 

124 ) 

125 

126 @property 

127 def has_errors(self) -> bool: 

128 """Check if service has any errors.""" 

129 return len(self.errors) > 0 or len(self.missing_required) > 0 

130 

131 

132class ServiceCharacteristicInfo(CharacteristicInfo): 

133 """Service-specific information about a characteristic with context about its presence. 

134 

135 Provides status, requirement, and class context for a characteristic within a service. 

136 """ 

137 

138 status: CharacteristicStatus = CharacteristicStatus.INVALID 

139 is_required: bool = False 

140 is_conditional: bool = False 

141 condition_description: str = "" 

142 char_class: type[BaseCharacteristic[Any]] | None = None 

143 

144 

145class ServiceCompletenessReport(msgspec.Struct, kw_only=True): # pylint: disable=too-many-instance-attributes 

146 """Comprehensive report about service completeness and health.""" 

147 

148 service_name: str 

149 service_uuid: BluetoothUUID 

150 health_status: ServiceHealthStatus 

151 is_healthy: bool 

152 characteristics_present: int 

153 characteristics_expected: int 

154 characteristics_required: int 

155 present_characteristics: list[BaseCharacteristic[Any]] = msgspec.field(default_factory=list) 

156 missing_required: list[BaseCharacteristic[Any]] = msgspec.field(default_factory=list) 

157 missing_optional: list[BaseCharacteristic[Any]] = msgspec.field(default_factory=list) 

158 invalid_characteristics: list[BaseCharacteristic[Any]] = msgspec.field(default_factory=list) 

159 warnings: list[str] = msgspec.field(default_factory=list) 

160 errors: list[str] = msgspec.field(default_factory=list) 

161 missing_details: dict[str, ServiceCharacteristicInfo] = msgspec.field(default_factory=dict) 

162 

163 

164# All type definitions moved to src/bluetooth_sig/types/gatt_services.py 

165# Import them at the top of this file when needed 

166 

167 

168class BaseGattService: # pylint: disable=too-many-public-methods 

169 """Base class for all GATT services. 

170 

171 Automatically resolves UUID, name, and summary from Bluetooth SIG specifications. 

172 Follows the same pattern as BaseCharacteristic for consistency. 

173 """ 

174 

175 # Class attributes for explicit name overrides 

176 _service_name: str | None = None 

177 _info: ServiceInfo | None = None # Populated in __post_init__ 

178 

179 def __init__( 

180 self, 

181 info: ServiceInfo | None = None, 

182 validation: ServiceValidationConfig | None = None, 

183 ) -> None: 

184 """Initialize service with structured configuration. 

185 

186 Args: 

187 info: Complete service information (optional for SIG services) 

188 validation: Validation constraints configuration (optional) 

189 

190 """ 

191 # Store provided info or None (will be resolved in __post_init__) 

192 self._provided_info = info 

193 

194 self.characteristics: dict[BluetoothUUID, BaseCharacteristic[Any]] = {} 

195 

196 # Set validation attributes from ServiceValidationConfig 

197 if validation: 

198 self.strict_validation = validation.strict_validation 

199 self.require_all_optional = validation.require_all_optional 

200 else: 

201 self.strict_validation = False 

202 self.require_all_optional = False 

203 

204 # Call post-init to resolve service info 

205 self.__post_init__() 

206 

207 def __post_init__(self) -> None: 

208 """Initialize service with resolved information.""" 

209 # Use provided info if available, otherwise resolve from SIG specs 

210 if self._provided_info: 

211 self._info = self._provided_info 

212 else: 

213 # Resolve service information using proper resolver 

214 self._info = SIGServiceResolver.resolve_for_class(type(self)) 

215 

216 @property 

217 def uuid(self) -> BluetoothUUID: 

218 """Get the service UUID from _info.""" 

219 assert self._info is not None, "Service info should be initialized in __post_init__" 

220 return self._info.uuid 

221 

222 @property 

223 def name(self) -> str: 

224 """Get the service name from _info.""" 

225 assert self._info is not None, "Service info should be initialized in __post_init__" 

226 return self._info.name 

227 

228 @property 

229 def info(self) -> ServiceInfo: 

230 """Return the resolved service information for this instance. 

231 

232 The info property provides all metadata about the service, including UUID, name, and description. 

233 """ 

234 assert self._info is not None, "Service info should be initialized in __post_init__" 

235 return self._info 

236 

237 @classmethod 

238 def get_class_uuid(cls) -> BluetoothUUID: 

239 """Get the UUID for this service class without instantiation. 

240 

241 Returns: 

242 BluetoothUUID for this service class 

243 

244 Raises: 

245 UUIDResolutionError: If UUID cannot be resolved 

246 

247 """ 

248 info = SIGServiceResolver.resolve_for_class(cls) 

249 return info.uuid 

250 

251 @classmethod 

252 def get_name(cls) -> str: 

253 """Get the service name for this class without creating an instance. 

254 

255 Returns: 

256 The service name as registered in the UUID registry. 

257 

258 """ 

259 # Try configured info first (for custom services) 

260 if hasattr(cls, "_info") and cls._info is not None: # pylint: disable=protected-access 

261 return cls._info.name # pylint: disable=protected-access 

262 

263 # For SIG services, resolve from registry 

264 uuid = cls.get_class_uuid() 

265 service_info = uuid_registry.get_service_info(uuid) 

266 if service_info: 

267 return service_info.name 

268 

269 # Fallback to class name 

270 return cls.__name__ 

271 

272 @classmethod 

273 def matches_uuid(cls, uuid: str | BluetoothUUID) -> bool: 

274 """Check if this service matches the given UUID.""" 

275 try: 

276 service_uuid = cls.get_class_uuid() 

277 input_uuid = uuid if isinstance(uuid, BluetoothUUID) else BluetoothUUID(uuid) 

278 except (ValueError, UUIDResolutionError): 

279 return False 

280 

281 return service_uuid == input_uuid 

282 

283 @classmethod 

284 def get_expected_characteristics(cls) -> ServiceCharacteristicCollection: 

285 """Get the expected characteristics for this service from the service_characteristics dict. 

286 

287 Looks for a 'service_characteristics' class attribute containing a dictionary of 

288 CharacteristicName -> required flag, and automatically builds CharacteristicSpec objects. 

289 

290 Returns: 

291 ServiceCharacteristicCollection mapping characteristic name to CharacteristicSpec 

292 

293 """ 

294 # Build expected mapping keyed by CharacteristicName enum for type safety. 

295 expected: dict[CharacteristicName, CharacteristicSpec[BaseCharacteristic[Any]]] = {} 

296 

297 # Check if the service defines a service_characteristics dictionary 

298 svc_chars = getattr(cls, "service_characteristics", None) 

299 if svc_chars: 

300 for char_name, is_required in svc_chars.items(): 

301 char_class = CharacteristicRegistry.get_characteristic_class(char_name) 

302 if char_class: 

303 expected[char_name] = CharacteristicSpec(char_class=char_class, required=is_required) 

304 

305 # Return an enum-keyed dict for strong typing. Callers must perform 

306 # explicit conversions from strings to `CharacteristicName` where needed. 

307 return expected 

308 

309 @classmethod 

310 def get_required_characteristics(cls) -> ServiceCharacteristicCollection: 

311 """Get the required characteristics for this service from the characteristics dict. 

312 

313 Automatically filters the characteristics dictionary for required=True entries. 

314 

315 Returns: 

316 ServiceCharacteristicCollection mapping characteristic name to CharacteristicSpec 

317 

318 """ 

319 expected = cls.get_expected_characteristics() 

320 return {name: spec for name, spec in expected.items() if spec.required} 

321 

322 # New strongly-typed methods (optional to implement) 

323 @classmethod 

324 def get_characteristics_schema(cls) -> type | None: 

325 """Get the TypedDict schema for this service's characteristics. 

326 

327 Override this method to provide strong typing for characteristics. 

328 If not implemented, falls back to get_expected_characteristics(). 

329 

330 Returns: 

331 TypedDict class defining the service's characteristics, or None 

332 

333 """ 

334 return None 

335 

336 @classmethod 

337 def get_required_characteristic_keys(cls) -> set[CharacteristicName]: 

338 """Get the set of required characteristic keys from the schema. 

339 

340 Override this method when using strongly-typed characteristics. 

341 If not implemented, falls back to get_required_characteristics().keys(). 

342 

343 Returns: 

344 Set of required characteristic field names 

345 

346 """ 

347 return set(cls.get_required_characteristics().keys()) 

348 

349 def get_expected_characteristic_uuids(self) -> set[BluetoothUUID]: 

350 """Get the set of expected characteristic UUIDs for this service.""" 

351 expected_uuids: set[BluetoothUUID] = set() 

352 for char_name, _char_spec in self.get_expected_characteristics().items(): 

353 # char_name is expected to be a CharacteristicName enum; handle accordingly 

354 try: 

355 lookup_name = char_name.value 

356 except AttributeError: 

357 lookup_name = str(char_name) 

358 char_info = uuid_registry.get_characteristic_info(lookup_name) 

359 if char_info: 

360 expected_uuids.add(char_info.uuid) 

361 return expected_uuids 

362 

363 def get_required_characteristic_uuids(self) -> set[BluetoothUUID]: 

364 """Get the set of required characteristic UUIDs for this service.""" 

365 required_uuids: set[BluetoothUUID] = set() 

366 for char_name, _char_spec in self.get_required_characteristics().items(): 

367 try: 

368 lookup_name = char_name.value 

369 except AttributeError: 

370 lookup_name = str(char_name) 

371 char_info = uuid_registry.get_characteristic_info(lookup_name) 

372 if char_info: 

373 required_uuids.add(char_info.uuid) 

374 return required_uuids 

375 

376 def process_characteristics(self, characteristics: ServiceDiscoveryData) -> None: 

377 """Process the characteristics for this service (default implementation). 

378 

379 Args: 

380 characteristics: Dict mapping UUID to characteristic info 

381 

382 """ 

383 for uuid_obj, char_info in characteristics.items(): 

384 char_instance = CharacteristicRegistry.get_characteristic(uuid=uuid_obj.normalized) 

385 

386 if char_instance is None: 

387 # Create UnknownCharacteristic for unregistered characteristics 

388 char_instance = UnknownCharacteristic( 

389 info=CharacteristicInfo( 

390 uuid=uuid_obj, 

391 name=char_info.name or "", 

392 unit=char_info.unit or "", 

393 python_type=char_info.python_type, 

394 ), 

395 ) 

396 

397 self.characteristics[uuid_obj] = char_instance 

398 

399 def get_characteristic(self, uuid: BluetoothUUID) -> GattCharacteristic[Any] | None: 

400 """Get a characteristic by UUID.""" 

401 if isinstance(uuid, str): 

402 uuid = BluetoothUUID(uuid) 

403 return self.characteristics.get(uuid) 

404 

405 @property 

406 def supported_characteristics(self) -> set[BaseCharacteristic[Any]]: 

407 """Get the set of characteristic UUIDs supported by this service.""" 

408 # Return set of characteristic instances, not UUID strings 

409 return set(self.characteristics.values()) 

410 

411 @classmethod 

412 def get_optional_characteristics(cls) -> ServiceCharacteristicCollection: 

413 """Get the optional characteristics for this service by name and class. 

414 

415 Returns: 

416 ServiceCharacteristicCollection mapping characteristic name to CharacteristicSpec 

417 

418 """ 

419 expected = cls.get_expected_characteristics() 

420 required = cls.get_required_characteristics() 

421 return {name: char_spec for name, char_spec in expected.items() if name not in required} 

422 

423 @classmethod 

424 def get_conditional_characteristics(cls) -> ServiceCharacteristicCollection: 

425 """Get characteristics that are required only under certain conditions. 

426 

427 Returns: 

428 ServiceCharacteristicCollection mapping characteristic name to CharacteristicSpec 

429 

430 Override in subclasses to specify conditional characteristics. 

431 

432 """ 

433 return {} 

434 

435 @classmethod 

436 def validate_bluetooth_sig_compliance(cls) -> list[str]: 

437 """Validate compliance with Bluetooth SIG service specification. 

438 

439 Returns: 

440 List of compliance issues found 

441 

442 Override in subclasses to provide service-specific validation. 

443 

444 """ 

445 issues: list[str] = [] 

446 

447 # Check if service has at least one required characteristic 

448 required = cls.get_required_characteristics() 

449 if not required: 

450 issues.append("Service has no required characteristics defined") 

451 

452 # Check if all expected characteristics are valid 

453 expected = cls.get_expected_characteristics() 

454 for char_name, _char_spec in expected.items(): 

455 char_info = uuid_registry.get_characteristic_info(char_name.value) 

456 if not char_info: 

457 issues.append(f"Characteristic '{char_name.value}' not found in UUID registry") 

458 

459 return issues 

460 

461 def _validate_characteristic_group( 

462 self, 

463 char_dict: ServiceCharacteristicCollection, 

464 result: ServiceValidationResult, 

465 is_required: bool, 

466 strict: bool, 

467 ) -> None: 

468 """Validate a group of characteristics (required/optional/conditional).""" 

469 for char_name, char_spec in char_dict.items(): 

470 lookup_name = char_name.value if hasattr(char_name, "value") else str(char_name) 

471 char_info = uuid_registry.get_characteristic_info(lookup_name) 

472 

473 if not char_info: 

474 if is_required: 

475 result.errors.append(f"Unknown required characteristic: {lookup_name}") 

476 elif strict: 

477 result.warnings.append(f"Unknown optional characteristic: {lookup_name}") 

478 continue 

479 

480 if char_info.uuid not in self.characteristics: 

481 missing_char = char_spec.char_class() 

482 if is_required: 

483 result.missing_required.append(missing_char) 

484 result.errors.append(f"Missing required characteristic: {lookup_name}") 

485 else: 

486 result.missing_optional.append(missing_char) 

487 if strict: 

488 result.warnings.append(f"Missing optional characteristic: {lookup_name}") 

489 

490 def _validate_conditional_characteristics( 

491 self, conditional_chars: ServiceCharacteristicCollection, result: ServiceValidationResult 

492 ) -> None: 

493 """Validate conditional characteristics.""" 

494 for char_name, conditional_spec in conditional_chars.items(): 

495 lookup_name = char_name.value if hasattr(char_name, "value") else str(char_name) 

496 char_info = uuid_registry.get_characteristic_info(lookup_name) 

497 

498 if not char_info: 

499 result.warnings.append(f"Unknown conditional characteristic: {lookup_name}") 

500 continue 

501 

502 if char_info.uuid not in self.characteristics: 

503 result.warnings.append( 

504 f"Missing conditional characteristic: {lookup_name} (required when {conditional_spec.condition})" 

505 ) 

506 

507 def _determine_health_status(self, result: ServiceValidationResult, required_count: int, strict: bool) -> None: 

508 """Determine overall service health status.""" 

509 if result.missing_required: 

510 result.status = ( 

511 ServiceHealthStatus.INCOMPLETE 

512 if len(result.missing_required) >= required_count 

513 else ServiceHealthStatus.PARTIAL 

514 ) 

515 elif (result.missing_optional and strict) or result.warnings or result.invalid_characteristics: 

516 result.status = ServiceHealthStatus.FUNCTIONAL 

517 

518 def validate_service(self, strict: bool = False) -> ServiceValidationResult: # pylint: disable=too-many-branches 

519 """Validate the completeness and health of this service. 

520 

521 Args: 

522 strict: If True, missing optional characteristics are treated as warnings 

523 

524 Returns: 

525 ServiceValidationResult with detailed status information 

526 

527 """ 

528 result = ServiceValidationResult(status=ServiceHealthStatus.COMPLETE) 

529 

530 # Validate required, optional, and conditional characteristics 

531 self._validate_characteristic_group(self.get_required_characteristics(), result, True, strict) 

532 self._validate_characteristic_group(self.get_optional_characteristics(), result, False, strict) 

533 self._validate_conditional_characteristics(self.get_conditional_characteristics(), result) 

534 

535 # Validate existing characteristics 

536 for uuid, characteristic in self.characteristics.items(): 

537 try: 

538 _ = characteristic.uuid 

539 except (AttributeError, ValueError, TypeError) as e: 

540 result.invalid_characteristics.append(characteristic) 

541 result.errors.append(f"Invalid characteristic {uuid}: {e}") 

542 

543 # Determine overall health status 

544 self._determine_health_status(result, len(self.get_required_characteristics()), strict) 

545 

546 return result 

547 

548 def get_missing_characteristics( 

549 self, 

550 ) -> dict[CharacteristicName, ServiceCharacteristicInfo]: 

551 """Get detailed information about missing characteristics. 

552 

553 Returns: 

554 Dict mapping characteristic name to ServiceCharacteristicInfo 

555 

556 """ 

557 missing: dict[CharacteristicName, ServiceCharacteristicInfo] = {} 

558 expected_chars = self.get_expected_characteristics() 

559 required_chars = self.get_required_characteristics() 

560 conditional_chars = self.get_conditional_characteristics() 

561 

562 for char_name, _char_spec in expected_chars.items(): 

563 char_info = uuid_registry.get_characteristic_info(char_name.value) 

564 if not char_info: 

565 continue 

566 

567 uuid_obj = char_info.uuid 

568 if uuid_obj not in self.characteristics: 

569 is_required = char_name in required_chars 

570 is_conditional = char_name in conditional_chars 

571 condition_desc = "" 

572 if is_conditional: 

573 conditional_spec = conditional_chars.get(char_name) 

574 if conditional_spec: 

575 condition_desc = conditional_spec.condition 

576 

577 # Handle both new CharacteristicSpec format and legacy direct class format 

578 char_class = None 

579 if hasattr(_char_spec, "char_class"): 

580 char_class = _char_spec.char_class # New format 

581 else: 

582 char_class = cast( 

583 "type[BaseCharacteristic[Any]]", _char_spec 

584 ) # Legacy format: value is the class directly 

585 

586 missing[char_name] = ServiceCharacteristicInfo( 

587 name=char_name.value, 

588 uuid=char_info.uuid, 

589 status=CharacteristicStatus.MISSING, 

590 is_required=is_required, 

591 is_conditional=is_conditional, 

592 condition_description=condition_desc, 

593 char_class=char_class, 

594 ) 

595 

596 return missing 

597 

598 def _find_characteristic_enum( 

599 self, characteristic_name: str, expected_chars: dict[CharacteristicName, Any] 

600 ) -> CharacteristicName | None: 

601 """Find the enum that matches the characteristic name string. 

602 

603 NOTE: This is an internal helper. Public APIs should accept 

604 `CharacteristicName` enums directly; this helper will only be used 

605 temporarily by migrating call sites. 

606 """ 

607 for enum_char in expected_chars: 

608 if enum_char.value == characteristic_name: 

609 return enum_char 

610 return None 

611 

612 def _get_characteristic_metadata(self, char_enum: CharacteristicName) -> tuple[bool, bool, str]: 

613 """Get characteristic metadata (is_required, is_conditional, condition_desc). 

614 

615 Returns metadata about the characteristic's requirement and condition. 

616 """ 

617 required_chars = self.get_required_characteristics() 

618 conditional_chars = self.get_conditional_characteristics() 

619 

620 is_required = char_enum in required_chars 

621 is_conditional = char_enum in conditional_chars 

622 condition_desc = "" 

623 if is_conditional: 

624 conditional_spec = conditional_chars.get(char_enum) 

625 if conditional_spec: 

626 condition_desc = conditional_spec.condition 

627 

628 return is_required, is_conditional, condition_desc 

629 

630 def _get_characteristic_status(self, char_info: CharacteristicInfo) -> CharacteristicStatus: 

631 """Get the status of a characteristic (present, missing, or invalid). 

632 

633 Returns the status of the characteristic for reporting and validation. 

634 """ 

635 uuid_obj = char_info.uuid 

636 if uuid_obj in self.characteristics: 

637 try: 

638 # Try to validate the characteristic 

639 char = self.characteristics[uuid_obj] 

640 _ = char.uuid # Basic validation 

641 except (KeyError, AttributeError, ValueError, TypeError): 

642 return CharacteristicStatus.INVALID 

643 else: 

644 return CharacteristicStatus.PRESENT 

645 return CharacteristicStatus.MISSING 

646 

647 def get_characteristic_status(self, characteristic_name: CharacteristicName) -> ServiceCharacteristicInfo | None: 

648 """Get detailed status of a specific characteristic. 

649 

650 Args: 

651 characteristic_name: CharacteristicName enum 

652 

653 Returns: 

654 CharacteristicInfo if characteristic is expected by this service, None otherwise 

655 

656 """ 

657 expected_chars = self.get_expected_characteristics() 

658 

659 char_enum = characteristic_name 

660 

661 # Only return status for characteristics that are expected by this service 

662 if char_enum not in expected_chars: 

663 return None 

664 

665 char_info = uuid_registry.get_characteristic_info(char_enum.value) 

666 if not char_info: 

667 return None 

668 

669 is_required, is_conditional, condition_desc = self._get_characteristic_metadata(char_enum) 

670 

671 char_class = None 

672 if char_enum in expected_chars: 

673 char_spec = expected_chars[char_enum] 

674 if hasattr(char_spec, "char_class"): 

675 char_class = char_spec.char_class # New format 

676 else: 

677 char_class = cast( 

678 "type[BaseCharacteristic[Any]]", char_spec 

679 ) # Legacy format: value is the class directly 

680 

681 status = self._get_characteristic_status(char_info) 

682 

683 return ServiceCharacteristicInfo( 

684 name=characteristic_name.value, 

685 uuid=char_info.uuid, 

686 status=status, 

687 is_required=is_required, 

688 is_conditional=is_conditional, 

689 condition_description=condition_desc, 

690 char_class=char_class, 

691 ) 

692 

693 def get_service_completeness_report(self) -> ServiceCompletenessReport: 

694 """Get a comprehensive report about service completeness. 

695 

696 Returns: 

697 ServiceCompletenessReport with detailed service status information 

698 

699 """ 

700 validation = self.validate_service(strict=True) 

701 missing = self.get_missing_characteristics() 

702 

703 present_chars: list[str] = [] 

704 for uuid, char in self.characteristics.items(): 

705 try: 

706 char_name = char.name if hasattr(char, "name") else f"UUID:{uuid!s}" 

707 present_chars.append(char_name) 

708 except (AttributeError, ValueError, TypeError): 

709 present_chars.append(f"Invalid:{uuid!s}") 

710 

711 missing_details = { 

712 name.value: ServiceCharacteristicInfo( 

713 name=info.name, 

714 uuid=info.uuid, 

715 status=info.status, 

716 is_required=info.is_required, 

717 is_conditional=info.is_conditional, 

718 condition_description=info.condition_description, 

719 ) 

720 for name, info in missing.items() 

721 } 

722 

723 return ServiceCompletenessReport( 

724 service_name=self.name, 

725 service_uuid=self.uuid, 

726 health_status=validation.status, 

727 is_healthy=validation.is_healthy, 

728 characteristics_present=len(self.characteristics), 

729 characteristics_expected=len(self.get_expected_characteristics()), 

730 characteristics_required=len(self.get_required_characteristics()), 

731 present_characteristics=list(self.characteristics.values()), 

732 missing_required=validation.missing_required, 

733 missing_optional=validation.missing_optional, 

734 invalid_characteristics=validation.invalid_characteristics, 

735 warnings=validation.warnings, 

736 errors=validation.errors, 

737 missing_details=missing_details, 

738 ) 

739 

740 def has_minimum_functionality(self) -> bool: 

741 """Check if service has minimum required functionality. 

742 

743 Returns: 

744 True if service has all required characteristics and is usable 

745 

746 """ 

747 validation = self.validate_service() 

748 return (not validation.missing_required) and (validation.status != ServiceHealthStatus.INCOMPLETE)