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
« prev ^ index » next coverage.py v7.13.5, created at 2026-03-18 11:17 +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 input_uuid = uuid if isinstance(uuid, BluetoothUUID) else BluetoothUUID(uuid)
278 except (ValueError, UUIDResolutionError):
279 return False
281 return service_uuid == input_uuid
283 @classmethod
284 def get_expected_characteristics(cls) -> ServiceCharacteristicCollection:
285 """Get the expected characteristics for this service from the service_characteristics dict.
287 Looks for a 'service_characteristics' class attribute containing a dictionary of
288 CharacteristicName -> required flag, and automatically builds CharacteristicSpec objects.
290 Returns:
291 ServiceCharacteristicCollection mapping characteristic name to CharacteristicSpec
293 """
294 # Build expected mapping keyed by CharacteristicName enum for type safety.
295 expected: dict[CharacteristicName, CharacteristicSpec[BaseCharacteristic[Any]]] = {}
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)
305 # Return an enum-keyed dict for strong typing. Callers must perform
306 # explicit conversions from strings to `CharacteristicName` where needed.
307 return expected
309 @classmethod
310 def get_required_characteristics(cls) -> ServiceCharacteristicCollection:
311 """Get the required characteristics for this service from the characteristics dict.
313 Automatically filters the characteristics dictionary for required=True entries.
315 Returns:
316 ServiceCharacteristicCollection mapping characteristic name to CharacteristicSpec
318 """
319 expected = cls.get_expected_characteristics()
320 return {name: spec for name, spec in expected.items() if spec.required}
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.
327 Override this method to provide strong typing for characteristics.
328 If not implemented, falls back to get_expected_characteristics().
330 Returns:
331 TypedDict class defining the service's characteristics, or None
333 """
334 return None
336 @classmethod
337 def get_required_characteristic_keys(cls) -> set[CharacteristicName]:
338 """Get the set of required characteristic keys from the schema.
340 Override this method when using strongly-typed characteristics.
341 If not implemented, falls back to get_required_characteristics().keys().
343 Returns:
344 Set of required characteristic field names
346 """
347 return set(cls.get_required_characteristics().keys())
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
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
376 def process_characteristics(self, characteristics: ServiceDiscoveryData) -> None:
377 """Process the characteristics for this service (default implementation).
379 Args:
380 characteristics: Dict mapping UUID to characteristic info
382 """
383 for uuid_obj, char_info in characteristics.items():
384 char_instance = CharacteristicRegistry.get_characteristic(uuid=uuid_obj.normalized)
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 )
397 self.characteristics[uuid_obj] = char_instance
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)
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())
411 @classmethod
412 def get_optional_characteristics(cls) -> ServiceCharacteristicCollection:
413 """Get the optional characteristics for this service by name and class.
415 Returns:
416 ServiceCharacteristicCollection mapping characteristic name to CharacteristicSpec
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}
423 @classmethod
424 def get_conditional_characteristics(cls) -> ServiceCharacteristicCollection:
425 """Get characteristics that are required only under certain conditions.
427 Returns:
428 ServiceCharacteristicCollection mapping characteristic name to CharacteristicSpec
430 Override in subclasses to specify conditional characteristics.
432 """
433 return {}
435 @classmethod
436 def validate_bluetooth_sig_compliance(cls) -> list[str]:
437 """Validate compliance with Bluetooth SIG service specification.
439 Returns:
440 List of compliance issues found
442 Override in subclasses to provide service-specific validation.
444 """
445 issues: list[str] = []
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")
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")
459 return issues
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)
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
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}")
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)
498 if not char_info:
499 result.warnings.append(f"Unknown conditional characteristic: {lookup_name}")
500 continue
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 )
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
518 def validate_service(self, strict: bool = False) -> ServiceValidationResult: # pylint: disable=too-many-branches
519 """Validate the completeness and health of this service.
521 Args:
522 strict: If True, missing optional characteristics are treated as warnings
524 Returns:
525 ServiceValidationResult with detailed status information
527 """
528 result = ServiceValidationResult(status=ServiceHealthStatus.COMPLETE)
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)
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}")
543 # Determine overall health status
544 self._determine_health_status(result, len(self.get_required_characteristics()), strict)
546 return result
548 def get_missing_characteristics(
549 self,
550 ) -> dict[CharacteristicName, ServiceCharacteristicInfo]:
551 """Get detailed information about missing characteristics.
553 Returns:
554 Dict mapping characteristic name to ServiceCharacteristicInfo
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()
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
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
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
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 )
596 return missing
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.
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
612 def _get_characteristic_metadata(self, char_enum: CharacteristicName) -> tuple[bool, bool, str]:
613 """Get characteristic metadata (is_required, is_conditional, condition_desc).
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()
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
628 return is_required, is_conditional, condition_desc
630 def _get_characteristic_status(self, char_info: CharacteristicInfo) -> CharacteristicStatus:
631 """Get the status of a characteristic (present, missing, or invalid).
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
647 def get_characteristic_status(self, characteristic_name: CharacteristicName) -> ServiceCharacteristicInfo | None:
648 """Get detailed status of a specific characteristic.
650 Args:
651 characteristic_name: CharacteristicName enum
653 Returns:
654 CharacteristicInfo if characteristic is expected by this service, None otherwise
656 """
657 expected_chars = self.get_expected_characteristics()
659 char_enum = characteristic_name
661 # Only return status for characteristics that are expected by this service
662 if char_enum not in expected_chars:
663 return None
665 char_info = uuid_registry.get_characteristic_info(char_enum.value)
666 if not char_info:
667 return None
669 is_required, is_conditional, condition_desc = self._get_characteristic_metadata(char_enum)
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
681 status = self._get_characteristic_status(char_info)
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 )
693 def get_service_completeness_report(self) -> ServiceCompletenessReport:
694 """Get a comprehensive report about service completeness.
696 Returns:
697 ServiceCompletenessReport with detailed service status information
699 """
700 validation = self.validate_service(strict=True)
701 missing = self.get_missing_characteristics()
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}")
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 }
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 )
740 def has_minimum_functionality(self) -> bool:
741 """Check if service has minimum required functionality.
743 Returns:
744 True if service has all required characteristics and is usable
746 """
747 validation = self.validate_service()
748 return (not validation.missing_required) and (validation.status != ServiceHealthStatus.INCOMPLETE)