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

331 statements  

« prev     ^ index     » next       coverage.py v7.13.1, created at 2026-01-11 20:14 +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 if isinstance(uuid, BluetoothUUID): 

278 input_uuid = uuid 

279 else: 

280 input_uuid = BluetoothUUID(uuid) 

281 return service_uuid == input_uuid 

282 except (ValueError, UUIDResolutionError): 

283 return False 

284 

285 @classmethod 

286 def get_expected_characteristics(cls) -> ServiceCharacteristicCollection: 

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

288 

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

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

291 

292 Returns: 

293 ServiceCharacteristicCollection mapping characteristic name to CharacteristicSpec 

294 

295 """ 

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

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

298 

299 # Check if the service defines a service_characteristics dictionary 

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

301 if svc_chars: 

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

303 char_class = CharacteristicRegistry.get_characteristic_class(char_name) 

304 if char_class: 

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

306 

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

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

309 return expected 

310 

311 @classmethod 

312 def get_required_characteristics(cls) -> ServiceCharacteristicCollection: 

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

314 

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

316 

317 Returns: 

318 ServiceCharacteristicCollection mapping characteristic name to CharacteristicSpec 

319 

320 """ 

321 expected = cls.get_expected_characteristics() 

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

323 

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

325 @classmethod 

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

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

328 

329 Override this method to provide strong typing for characteristics. 

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

331 

332 Returns: 

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

334 

335 """ 

336 return None 

337 

338 @classmethod 

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

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

341 

342 Override this method when using strongly-typed characteristics. 

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

344 

345 Returns: 

346 Set of required characteristic field names 

347 

348 """ 

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

350 

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

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

353 expected_uuids: set[BluetoothUUID] = set() 

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

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

356 try: 

357 lookup_name = char_name.value 

358 except AttributeError: 

359 lookup_name = str(char_name) 

360 char_info = uuid_registry.get_characteristic_info(lookup_name) 

361 if char_info: 

362 expected_uuids.add(char_info.uuid) 

363 return expected_uuids 

364 

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

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

367 required_uuids: set[BluetoothUUID] = set() 

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

369 try: 

370 lookup_name = char_name.value 

371 except AttributeError: 

372 lookup_name = str(char_name) 

373 char_info = uuid_registry.get_characteristic_info(lookup_name) 

374 if char_info: 

375 required_uuids.add(char_info.uuid) 

376 return required_uuids 

377 

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

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

380 

381 Args: 

382 characteristics: Dict mapping UUID to characteristic info 

383 

384 """ 

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

386 char_instance = CharacteristicRegistry.create_characteristic(uuid=uuid_obj.normalized) 

387 

388 if char_instance is None: 

389 # Create UnknownCharacteristic for unregistered characteristics 

390 char_instance = UnknownCharacteristic( 

391 info=CharacteristicInfo( 

392 uuid=uuid_obj, 

393 name=char_info.name or f"Unknown Characteristic ({uuid_obj})", 

394 unit=char_info.unit or "", 

395 value_type=char_info.value_type, 

396 ), 

397 properties=[], 

398 ) 

399 

400 self.characteristics[uuid_obj] = char_instance 

401 

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

403 """Get a characteristic by UUID.""" 

404 if isinstance(uuid, str): 

405 uuid = BluetoothUUID(uuid) 

406 return self.characteristics.get(uuid) 

407 

408 @property 

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

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

411 # Return set of characteristic instances, not UUID strings 

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

413 

414 @classmethod 

415 def get_optional_characteristics(cls) -> ServiceCharacteristicCollection: 

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

417 

418 Returns: 

419 ServiceCharacteristicCollection mapping characteristic name to CharacteristicSpec 

420 

421 """ 

422 expected = cls.get_expected_characteristics() 

423 required = cls.get_required_characteristics() 

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

425 

426 @classmethod 

427 def get_conditional_characteristics(cls) -> ServiceCharacteristicCollection: 

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

429 

430 Returns: 

431 ServiceCharacteristicCollection mapping characteristic name to CharacteristicSpec 

432 

433 Override in subclasses to specify conditional characteristics. 

434 

435 """ 

436 return {} 

437 

438 @classmethod 

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

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

441 

442 Returns: 

443 List of compliance issues found 

444 

445 Override in subclasses to provide service-specific validation. 

446 

447 """ 

448 issues: list[str] = [] 

449 

450 # Check if service has at least one required characteristic 

451 required = cls.get_required_characteristics() 

452 if not required: 

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

454 

455 # Check if all expected characteristics are valid 

456 expected = cls.get_expected_characteristics() 

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

458 char_info = uuid_registry.get_characteristic_info(char_name.value) 

459 if not char_info: 

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

461 

462 return issues 

463 

464 def _validate_characteristic_group( 

465 self, 

466 char_dict: ServiceCharacteristicCollection, 

467 result: ServiceValidationResult, 

468 is_required: bool, 

469 strict: bool, 

470 ) -> None: 

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

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

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

474 char_info = uuid_registry.get_characteristic_info(lookup_name) 

475 

476 if not char_info: 

477 if is_required: 

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

479 elif strict: 

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

481 continue 

482 

483 if char_info.uuid not in self.characteristics: 

484 missing_char = char_spec.char_class() 

485 if is_required: 

486 result.missing_required.append(missing_char) 

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

488 else: 

489 result.missing_optional.append(missing_char) 

490 if strict: 

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

492 

493 def _validate_conditional_characteristics( 

494 self, conditional_chars: ServiceCharacteristicCollection, result: ServiceValidationResult 

495 ) -> None: 

496 """Validate conditional characteristics.""" 

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

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

499 char_info = uuid_registry.get_characteristic_info(lookup_name) 

500 

501 if not char_info: 

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

503 continue 

504 

505 if char_info.uuid not in self.characteristics: 

506 result.warnings.append( 

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

508 ) 

509 

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

511 """Determine overall service health status.""" 

512 if result.missing_required: 

513 result.status = ( 

514 ServiceHealthStatus.INCOMPLETE 

515 if len(result.missing_required) >= required_count 

516 else ServiceHealthStatus.PARTIAL 

517 ) 

518 elif result.missing_optional and strict: 

519 result.status = ServiceHealthStatus.FUNCTIONAL 

520 elif result.warnings or result.invalid_characteristics: 

521 result.status = ServiceHealthStatus.FUNCTIONAL 

522 

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

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

525 

526 Args: 

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

528 

529 Returns: 

530 ServiceValidationResult with detailed status information 

531 

532 """ 

533 result = ServiceValidationResult(status=ServiceHealthStatus.COMPLETE) 

534 

535 # Validate required, optional, and conditional characteristics 

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

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

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

539 

540 # Validate existing characteristics 

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

542 try: 

543 _ = characteristic.uuid 

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

545 result.invalid_characteristics.append(characteristic) 

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

547 

548 # Determine overall health status 

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

550 

551 return result 

552 

553 def get_missing_characteristics( 

554 self, 

555 ) -> dict[CharacteristicName, ServiceCharacteristicInfo]: 

556 """Get detailed information about missing characteristics. 

557 

558 Returns: 

559 Dict mapping characteristic name to ServiceCharacteristicInfo 

560 

561 """ 

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

563 expected_chars = self.get_expected_characteristics() 

564 required_chars = self.get_required_characteristics() 

565 conditional_chars = self.get_conditional_characteristics() 

566 

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

568 char_info = uuid_registry.get_characteristic_info(char_name.value) 

569 if not char_info: 

570 continue 

571 

572 uuid_obj = char_info.uuid 

573 if uuid_obj not in self.characteristics: 

574 is_required = char_name in required_chars 

575 is_conditional = char_name in conditional_chars 

576 condition_desc = "" 

577 if is_conditional: 

578 conditional_spec = conditional_chars.get(char_name) 

579 if conditional_spec: 

580 condition_desc = conditional_spec.condition 

581 

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

583 char_class = None 

584 if hasattr(_char_spec, "char_class"): 

585 char_class = _char_spec.char_class # New format 

586 else: 

587 char_class = cast( 

588 type[BaseCharacteristic[Any]], _char_spec 

589 ) # Legacy format: value is the class directly 

590 

591 missing[char_name] = ServiceCharacteristicInfo( 

592 name=char_name.value, 

593 uuid=char_info.uuid, 

594 status=CharacteristicStatus.MISSING, 

595 is_required=is_required, 

596 is_conditional=is_conditional, 

597 condition_description=condition_desc, 

598 char_class=char_class, 

599 ) 

600 

601 return missing 

602 

603 def _find_characteristic_enum( 

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

605 ) -> CharacteristicName | None: 

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

607 

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

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

610 temporarily by migrating call sites. 

611 """ 

612 for enum_char in expected_chars.keys(): 

613 if enum_char.value == characteristic_name: 

614 return enum_char 

615 return None 

616 

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

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

619 

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

621 """ 

622 required_chars = self.get_required_characteristics() 

623 conditional_chars = self.get_conditional_characteristics() 

624 

625 is_required = char_enum in required_chars 

626 is_conditional = char_enum in conditional_chars 

627 condition_desc = "" 

628 if is_conditional: 

629 conditional_spec = conditional_chars.get(char_enum) 

630 if conditional_spec: 

631 condition_desc = conditional_spec.condition 

632 

633 return is_required, is_conditional, condition_desc 

634 

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

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

637 

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

639 """ 

640 uuid_obj = char_info.uuid 

641 if uuid_obj in self.characteristics: 

642 try: 

643 # Try to validate the characteristic 

644 char = self.characteristics[uuid_obj] 

645 _ = char.uuid # Basic validation 

646 return CharacteristicStatus.PRESENT 

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

648 return CharacteristicStatus.INVALID 

649 return CharacteristicStatus.MISSING 

650 

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

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

653 

654 Args: 

655 characteristic_name: CharacteristicName enum 

656 

657 Returns: 

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

659 

660 """ 

661 expected_chars = self.get_expected_characteristics() 

662 

663 char_enum = characteristic_name 

664 

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

666 if char_enum not in expected_chars: 

667 return None 

668 

669 char_info = uuid_registry.get_characteristic_info(char_enum.value) 

670 if not char_info: 

671 return None 

672 

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

674 

675 char_class = None 

676 if char_enum in expected_chars: 

677 char_spec = expected_chars[char_enum] 

678 if hasattr(char_spec, "char_class"): 

679 char_class = char_spec.char_class # New format 

680 else: 

681 char_class = cast( 

682 type[BaseCharacteristic[Any]], char_spec 

683 ) # Legacy format: value is the class directly 

684 

685 status = self._get_characteristic_status(char_info) 

686 

687 return ServiceCharacteristicInfo( 

688 name=characteristic_name.value, 

689 uuid=char_info.uuid, 

690 status=status, 

691 is_required=is_required, 

692 is_conditional=is_conditional, 

693 condition_description=condition_desc, 

694 char_class=char_class, 

695 ) 

696 

697 def get_service_completeness_report(self) -> ServiceCompletenessReport: 

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

699 

700 Returns: 

701 ServiceCompletenessReport with detailed service status information 

702 

703 """ 

704 validation = self.validate_service(strict=True) 

705 missing = self.get_missing_characteristics() 

706 

707 present_chars: list[str] = [] 

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

709 try: 

710 char_name = char.name if hasattr(char, "name") else f"UUID:{str(uuid)}" 

711 present_chars.append(char_name) 

712 except (AttributeError, ValueError, TypeError): 

713 present_chars.append(f"Invalid:{str(uuid)}") 

714 

715 missing_details = { 

716 name.value: ServiceCharacteristicInfo( 

717 name=info.name, 

718 uuid=info.uuid, 

719 status=info.status, 

720 is_required=info.is_required, 

721 is_conditional=info.is_conditional, 

722 condition_description=info.condition_description, 

723 ) 

724 for name, info in missing.items() 

725 } 

726 

727 return ServiceCompletenessReport( 

728 service_name=self.name, 

729 service_uuid=self.uuid, 

730 health_status=validation.status, 

731 is_healthy=validation.is_healthy, 

732 characteristics_present=len(self.characteristics), 

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

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

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

736 missing_required=validation.missing_required, 

737 missing_optional=validation.missing_optional, 

738 invalid_characteristics=validation.invalid_characteristics, 

739 warnings=validation.warnings, 

740 errors=validation.errors, 

741 missing_details=missing_details, 

742 ) 

743 

744 def has_minimum_functionality(self) -> bool: 

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

746 

747 Returns: 

748 True if service has all required characteristics and is usable 

749 

750 """ 

751 validation = self.validate_service() 

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