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
« prev ^ index » next coverage.py v7.13.1, created at 2026-01-11 20:14 +0000
1"""Base class for GATT service implementations."""
3from __future__ import annotations
5from enum import Enum
6from typing import Any, TypeVar, cast
8import msgspec
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
20# Type aliases
21GattCharacteristic = BaseCharacteristic
22# Strong-typed collection with enum keys
23ServiceCharacteristicCollection = CharacteristicCollection # Alias for compatibility
25# Generic type variable for service-specific characteristic definitions
26ServiceCharacteristics = TypeVar("ServiceCharacteristics")
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.
34class ServiceValidationConfig(msgspec.Struct, kw_only=True):
35 """Configuration for service validation constraints.
37 Groups validation parameters into a single, optional configuration object
38 to simplify BaseGattService constructor signatures.
39 """
41 strict_validation: bool = False
42 require_all_optional: bool = False
45class SIGServiceResolver:
46 """Resolves SIG service information from registry.
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 """
53 @staticmethod
54 def resolve_for_class(service_class: type[BaseGattService]) -> ServiceInfo:
55 """Resolve ServiceInfo for a SIG service class.
57 Args:
58 service_class: The service class to resolve info for
60 Returns:
61 ServiceInfo with resolved UUID, name, summary
63 Raises:
64 UUIDResolutionError: If no UUID can be resolved for the class
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
74 # Try registry resolution
75 registry_info = SIGServiceResolver.resolve_from_registry(service_class)
76 if registry_info:
77 return registry_info
79 # No resolution found
80 raise UUIDResolutionError(service_class.__name__, [service_class.__name__])
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)
91class ServiceHealthStatus(Enum):
92 """Health status of a GATT service."""
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
100class CharacteristicStatus(Enum):
101 """Status of characteristics within a service."""
103 PRESENT = "present" # Characteristic is present and functional
104 MISSING = "missing" # Expected characteristic not found
105 INVALID = "invalid" # Characteristic found but invalid/unusable
108class ServiceValidationResult(msgspec.Struct, kw_only=True):
109 """Result of service validation."""
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)
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 )
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
132class ServiceCharacteristicInfo(CharacteristicInfo):
133 """Service-specific information about a characteristic with context about its presence.
135 Provides status, requirement, and class context for a characteristic within a service.
136 """
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
145class ServiceCompletenessReport(msgspec.Struct, kw_only=True): # pylint: disable=too-many-instance-attributes
146 """Comprehensive report about service completeness and health."""
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)
164# All type definitions moved to src/bluetooth_sig/types/gatt_services.py
165# Import them at the top of this file when needed
168class BaseGattService: # pylint: disable=too-many-public-methods
169 """Base class for all GATT services.
171 Automatically resolves UUID, name, and summary from Bluetooth SIG specifications.
172 Follows the same pattern as BaseCharacteristic for consistency.
173 """
175 # Class attributes for explicit name overrides
176 _service_name: str | None = None
177 _info: ServiceInfo | None = None # Populated in __post_init__
179 def __init__(
180 self,
181 info: ServiceInfo | None = None,
182 validation: ServiceValidationConfig | None = None,
183 ) -> None:
184 """Initialize service with structured configuration.
186 Args:
187 info: Complete service information (optional for SIG services)
188 validation: Validation constraints configuration (optional)
190 """
191 # Store provided info or None (will be resolved in __post_init__)
192 self._provided_info = info
194 self.characteristics: dict[BluetoothUUID, BaseCharacteristic[Any]] = {}
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
204 # Call post-init to resolve service info
205 self.__post_init__()
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))
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
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
228 @property
229 def info(self) -> ServiceInfo:
230 """Return the resolved service information for this instance.
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
237 @classmethod
238 def get_class_uuid(cls) -> BluetoothUUID:
239 """Get the UUID for this service class without instantiation.
241 Returns:
242 BluetoothUUID for this service class
244 Raises:
245 UUIDResolutionError: If UUID cannot be resolved
247 """
248 info = SIGServiceResolver.resolve_for_class(cls)
249 return info.uuid
251 @classmethod
252 def get_name(cls) -> str:
253 """Get the service name for this class without creating an instance.
255 Returns:
256 The service name as registered in the UUID registry.
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
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
269 # Fallback to class name
270 return cls.__name__
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
285 @classmethod
286 def get_expected_characteristics(cls) -> ServiceCharacteristicCollection:
287 """Get the expected characteristics for this service from the service_characteristics dict.
289 Looks for a 'service_characteristics' class attribute containing a dictionary of
290 CharacteristicName -> required flag, and automatically builds CharacteristicSpec objects.
292 Returns:
293 ServiceCharacteristicCollection mapping characteristic name to CharacteristicSpec
295 """
296 # Build expected mapping keyed by CharacteristicName enum for type safety.
297 expected: dict[CharacteristicName, CharacteristicSpec[BaseCharacteristic[Any]]] = {}
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)
307 # Return an enum-keyed dict for strong typing. Callers must perform
308 # explicit conversions from strings to `CharacteristicName` where needed.
309 return expected
311 @classmethod
312 def get_required_characteristics(cls) -> ServiceCharacteristicCollection:
313 """Get the required characteristics for this service from the characteristics dict.
315 Automatically filters the characteristics dictionary for required=True entries.
317 Returns:
318 ServiceCharacteristicCollection mapping characteristic name to CharacteristicSpec
320 """
321 expected = cls.get_expected_characteristics()
322 return {name: spec for name, spec in expected.items() if spec.required}
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.
329 Override this method to provide strong typing for characteristics.
330 If not implemented, falls back to get_expected_characteristics().
332 Returns:
333 TypedDict class defining the service's characteristics, or None
335 """
336 return None
338 @classmethod
339 def get_required_characteristic_keys(cls) -> set[CharacteristicName]:
340 """Get the set of required characteristic keys from the schema.
342 Override this method when using strongly-typed characteristics.
343 If not implemented, falls back to get_required_characteristics().keys().
345 Returns:
346 Set of required characteristic field names
348 """
349 return set(cls.get_required_characteristics().keys())
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
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
378 def process_characteristics(self, characteristics: ServiceDiscoveryData) -> None:
379 """Process the characteristics for this service (default implementation).
381 Args:
382 characteristics: Dict mapping UUID to characteristic info
384 """
385 for uuid_obj, char_info in characteristics.items():
386 char_instance = CharacteristicRegistry.create_characteristic(uuid=uuid_obj.normalized)
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 )
400 self.characteristics[uuid_obj] = char_instance
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)
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())
414 @classmethod
415 def get_optional_characteristics(cls) -> ServiceCharacteristicCollection:
416 """Get the optional characteristics for this service by name and class.
418 Returns:
419 ServiceCharacteristicCollection mapping characteristic name to CharacteristicSpec
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}
426 @classmethod
427 def get_conditional_characteristics(cls) -> ServiceCharacteristicCollection:
428 """Get characteristics that are required only under certain conditions.
430 Returns:
431 ServiceCharacteristicCollection mapping characteristic name to CharacteristicSpec
433 Override in subclasses to specify conditional characteristics.
435 """
436 return {}
438 @classmethod
439 def validate_bluetooth_sig_compliance(cls) -> list[str]:
440 """Validate compliance with Bluetooth SIG service specification.
442 Returns:
443 List of compliance issues found
445 Override in subclasses to provide service-specific validation.
447 """
448 issues: list[str] = []
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")
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")
462 return issues
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)
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
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}")
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)
501 if not char_info:
502 result.warnings.append(f"Unknown conditional characteristic: {lookup_name}")
503 continue
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 )
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
523 def validate_service(self, strict: bool = False) -> ServiceValidationResult: # pylint: disable=too-many-branches
524 """Validate the completeness and health of this service.
526 Args:
527 strict: If True, missing optional characteristics are treated as warnings
529 Returns:
530 ServiceValidationResult with detailed status information
532 """
533 result = ServiceValidationResult(status=ServiceHealthStatus.COMPLETE)
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)
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}")
548 # Determine overall health status
549 self._determine_health_status(result, len(self.get_required_characteristics()), strict)
551 return result
553 def get_missing_characteristics(
554 self,
555 ) -> dict[CharacteristicName, ServiceCharacteristicInfo]:
556 """Get detailed information about missing characteristics.
558 Returns:
559 Dict mapping characteristic name to ServiceCharacteristicInfo
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()
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
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
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
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 )
601 return missing
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.
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
617 def _get_characteristic_metadata(self, char_enum: CharacteristicName) -> tuple[bool, bool, str]:
618 """Get characteristic metadata (is_required, is_conditional, condition_desc).
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()
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
633 return is_required, is_conditional, condition_desc
635 def _get_characteristic_status(self, char_info: CharacteristicInfo) -> CharacteristicStatus:
636 """Get the status of a characteristic (present, missing, or invalid).
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
651 def get_characteristic_status(self, characteristic_name: CharacteristicName) -> ServiceCharacteristicInfo | None:
652 """Get detailed status of a specific characteristic.
654 Args:
655 characteristic_name: CharacteristicName enum
657 Returns:
658 CharacteristicInfo if characteristic is expected by this service, None otherwise
660 """
661 expected_chars = self.get_expected_characteristics()
663 char_enum = characteristic_name
665 # Only return status for characteristics that are expected by this service
666 if char_enum not in expected_chars:
667 return None
669 char_info = uuid_registry.get_characteristic_info(char_enum.value)
670 if not char_info:
671 return None
673 is_required, is_conditional, condition_desc = self._get_characteristic_metadata(char_enum)
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
685 status = self._get_characteristic_status(char_info)
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 )
697 def get_service_completeness_report(self) -> ServiceCompletenessReport:
698 """Get a comprehensive report about service completeness.
700 Returns:
701 ServiceCompletenessReport with detailed service status information
703 """
704 validation = self.validate_service(strict=True)
705 missing = self.get_missing_characteristics()
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)}")
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 }
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 )
744 def has_minimum_functionality(self) -> bool:
745 """Check if service has minimum required functionality.
747 Returns:
748 True if service has all required characteristics and is usable
750 """
751 validation = self.validate_service()
752 return (not validation.missing_required) and (validation.status != ServiceHealthStatus.INCOMPLETE)