Coverage for src / bluetooth_sig / registry / uuids / units.py: 95%
41 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"""Registry for Bluetooth SIG unit UUID metadata.
3Loads unit definitions from the Bluetooth SIG units.yaml specification,
4providing UUID-to-name-to-symbol resolution. Used primarily for:
5- Resolving org.bluetooth.unit.* identifiers from GSS YAML files
6- Converting unit IDs to human-readable SI symbols for display
8Note: This is distinct from the domain enums in types/units.py which
9provide type-safe unit representations for decoded characteristic data.
10The symbols derived here match those enum values for consistency.
11"""
13from __future__ import annotations
15from bluetooth_sig.registry.base import BaseUUIDRegistry
16from bluetooth_sig.types.registry.units import UnitInfo
17from bluetooth_sig.types.uuid import BluetoothUUID
19# SI symbol mappings for unit names in parentheses
20_UNIT_NAME_TO_SYMBOL: dict[str, str] = {
21 "degree celsius": "°C",
22 "degree fahrenheit": "°F",
23 "kelvin": "K",
24 "pascal": "Pa",
25 "bar": "bar",
26 "millimetre of mercury": "mmHg",
27 "ampere": "A",
28 "volt": "V",
29 "coulomb": "C",
30 "farad": "F",
31 "ohm": "Ω",
32 "siemens": "S",
33 "weber": "Wb",
34 "tesla": "T",
35 "henry": "H",
36 "joule": "J",
37 "watt": "W",
38 "hertz": "Hz",
39 "metre": "m",
40 "kilogram": "kg",
41 "second": "s",
42 "metres per second": "m/s",
43 "metres per second squared": "m/s²",
44 "square metres": "m²",
45 "cubic metres": "m³",
46 "radian per second": "rad/s",
47 "newton": "N",
48 "candela": "cd",
49 "lux": "lx",
50 "lumen": "lm",
51 "becquerel": "Bq",
52 "gray": "Gy",
53 "sievert": "Sv",
54 "katal": "kat",
55 "degree": "°",
56 "radian": "rad",
57 "steradian": "sr",
58 "mole": "mol",
59}
61_SPECIAL_UNIT_NAMES: dict[str, str] = {
62 "percentage": "%",
63 "per mille": "‰",
64 "unitless": "",
65}
68def _derive_symbol_from_name(name: str) -> str:
69 """Derive SI symbol from unit name.
71 Handles formats like:
72 - "thermodynamic temperature (degree celsius)" -> "°C"
73 - "percentage" -> "%"
74 - "length (metre)" -> "m"
76 Args:
77 name: Unit name from YAML
79 Returns:
80 SI symbol, or empty string if unknown
81 """
82 name_lower = name.lower()
84 if name_lower in _SPECIAL_UNIT_NAMES:
85 return _SPECIAL_UNIT_NAMES[name_lower]
87 if "(" in name and ")" in name:
88 start = name.find("(") + 1
89 end = name.find(")", start)
90 if 0 < start < end:
91 parenthetical = name[start:end].strip().lower()
92 if parenthetical in _UNIT_NAME_TO_SYMBOL:
93 return _UNIT_NAME_TO_SYMBOL[parenthetical]
94 return parenthetical
96 return ""
99class UnitsRegistry(BaseUUIDRegistry[UnitInfo]):
100 """Registry for Bluetooth SIG unit UUIDs."""
102 def _load_yaml_path(self) -> str:
103 """Return the YAML file path relative to bluetooth_sig/ root."""
104 return "units.yaml"
106 def _create_info_from_yaml(self, uuid_data: dict[str, str], uuid: BluetoothUUID) -> UnitInfo:
107 """Create UnitInfo from YAML data with derived symbol."""
108 unit_name = uuid_data["name"]
109 return UnitInfo(
110 uuid=uuid,
111 name=unit_name,
112 id=uuid_data["id"],
113 symbol=_derive_symbol_from_name(unit_name),
114 )
116 def _create_runtime_info(self, entry: object, uuid: BluetoothUUID) -> UnitInfo:
117 """Create runtime UnitInfo from entry."""
118 unit_name = getattr(entry, "name", "")
119 return UnitInfo(
120 uuid=uuid,
121 name=unit_name,
122 id=getattr(entry, "id", ""),
123 symbol=getattr(entry, "symbol", "") or _derive_symbol_from_name(unit_name),
124 )
126 def get_unit_info(self, uuid: str | BluetoothUUID) -> UnitInfo | None:
127 """Get unit information by UUID.
129 Args:
130 uuid: 16-bit UUID as string (with or without 0x) or BluetoothUUID
132 Returns:
133 UnitInfo object, or None if not found
134 """
135 return self.get_info(uuid)
137 def get_unit_info_by_name(self, name: str) -> UnitInfo | None:
138 """Get unit information by name.
140 Args:
141 name: Unit name (case-insensitive)
143 Returns:
144 UnitInfo object, or None if not found
145 """
146 return self.get_info(name)
148 def get_unit_info_by_id(self, unit_id: str) -> UnitInfo | None:
149 """Get unit information by ID.
151 Args:
152 unit_id: Unit ID (e.g., "org.bluetooth.unit.celsius")
154 Returns:
155 UnitInfo object, or None if not found
156 """
157 return self.get_info(unit_id)
159 def is_unit_uuid(self, uuid: str | BluetoothUUID) -> bool:
160 """Check if a UUID is a registered unit UUID.
162 Args:
163 uuid: UUID to check
165 Returns:
166 True if the UUID is a unit UUID, False otherwise
167 """
168 return self.get_unit_info(uuid) is not None
170 def get_all_units(self) -> list[UnitInfo]:
171 """Get all registered units.
173 Returns:
174 List of all UnitInfo objects
175 """
176 self._ensure_loaded()
177 with self._lock:
178 return list(self._canonical_store.values())
181# Global instance
182units_registry = UnitsRegistry()