Coverage for src / bluetooth_sig / types / appearance.py: 97%
32 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"""Appearance value types for Bluetooth device identification."""
3from __future__ import annotations
5import msgspec
7from bluetooth_sig.registry.core.appearance_values import appearance_values_registry
8from bluetooth_sig.types.registry.appearance_info import AppearanceInfo
11class AppearanceData(msgspec.Struct, frozen=True, kw_only=True):
12 """Appearance characteristic data with human-readable info.
14 Attributes:
15 raw_value: Raw 16-bit appearance code from BLE
16 info: Optional decoded appearance information from registry
17 """
19 raw_value: int
20 info: AppearanceInfo | None = None
22 @classmethod
23 def from_category(cls, category: str, subcategory: str | None = None) -> AppearanceData:
24 """Create AppearanceData from category and subcategory strings.
26 This helper validates the strings and finds the correct raw_value by
27 searching the registry. Useful when creating appearance data from
28 user-provided human-readable names.
30 Args:
31 category: Device category name (e.g., "Heart Rate Sensor")
32 subcategory: Optional subcategory name (e.g., "Heart Rate Belt")
34 Returns:
35 AppearanceData with validated info and correct raw_value
37 Raises:
38 ValueError: If category/subcategory combination is not found in registry
40 Example:
41 >>> data = AppearanceData.from_category("Heart Rate Sensor", "Heart Rate Belt")
42 >>> data.raw_value
43 833
44 """
45 # Use public method to find appearance info
46 info = appearance_values_registry.find_by_category_subcategory(category, subcategory)
48 if info is None:
49 # If not found, raise error
50 if subcategory:
51 raise ValueError(f"Unknown appearance: {category}: {subcategory}")
52 raise ValueError(f"Unknown appearance: {category}")
54 # Calculate raw_value from category and subcategory values
55 subcategory_value = info.subcategory.value if info.subcategory else 0
56 raw_value = (info.category_value << 6) | subcategory_value
57 return cls(raw_value=raw_value, info=info)
59 @property
60 def category(self) -> str | None:
61 """Get device category name.
63 Returns:
64 Category name or None if info not available
65 """
66 return self.info.category if self.info else None
68 @property
69 def subcategory(self) -> str | None:
70 """Get device subcategory name.
72 Returns:
73 Subcategory name or None if not available
74 """
75 if self.info and self.info.subcategory:
76 return self.info.subcategory.name
77 return None
79 @property
80 def full_name(self) -> str | None:
81 """Get full human-readable name.
83 Returns:
84 Full device type name or None if info not available
85 """
86 return self.info.full_name if self.info else None
88 def __int__(self) -> int:
89 """Allow casting to int for backward compatibility.
91 Returns:
92 Raw appearance value as integer
93 """
94 return self.raw_value
96 def __format__(self, format_spec: str) -> str:
97 """Format appearance value for backward compatibility.
99 Supports hex formatting for backward compatibility with code that
100 used raw int appearance values: f"{appearance:04X}"
102 Args:
103 format_spec: Format specification (e.g., "04X" for zero-padded uppercase hex)
105 Returns:
106 Formatted raw_value as string
108 Example:
109 >>> data = AppearanceData(raw_value=833, info=None)
110 >>> f"{data:04X}"
111 '0341'
112 """
113 return format(self.raw_value, format_spec)