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

1"""Registry for Bluetooth SIG unit UUID metadata. 

2 

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 

7 

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""" 

12 

13from __future__ import annotations 

14 

15from bluetooth_sig.registry.base import BaseUUIDRegistry 

16from bluetooth_sig.types.registry.units import UnitInfo 

17from bluetooth_sig.types.uuid import BluetoothUUID 

18 

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} 

60 

61_SPECIAL_UNIT_NAMES: dict[str, str] = { 

62 "percentage": "%", 

63 "per mille": "‰", 

64 "unitless": "", 

65} 

66 

67 

68def _derive_symbol_from_name(name: str) -> str: 

69 """Derive SI symbol from unit name. 

70 

71 Handles formats like: 

72 - "thermodynamic temperature (degree celsius)" -> "°C" 

73 - "percentage" -> "%" 

74 - "length (metre)" -> "m" 

75 

76 Args: 

77 name: Unit name from YAML 

78 

79 Returns: 

80 SI symbol, or empty string if unknown 

81 """ 

82 name_lower = name.lower() 

83 

84 if name_lower in _SPECIAL_UNIT_NAMES: 

85 return _SPECIAL_UNIT_NAMES[name_lower] 

86 

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 

95 

96 return "" 

97 

98 

99class UnitsRegistry(BaseUUIDRegistry[UnitInfo]): 

100 """Registry for Bluetooth SIG unit UUIDs.""" 

101 

102 def _load_yaml_path(self) -> str: 

103 """Return the YAML file path relative to bluetooth_sig/ root.""" 

104 return "units.yaml" 

105 

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 ) 

115 

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 ) 

125 

126 def get_unit_info(self, uuid: str | BluetoothUUID) -> UnitInfo | None: 

127 """Get unit information by UUID. 

128 

129 Args: 

130 uuid: 16-bit UUID as string (with or without 0x) or BluetoothUUID 

131 

132 Returns: 

133 UnitInfo object, or None if not found 

134 """ 

135 return self.get_info(uuid) 

136 

137 def get_unit_info_by_name(self, name: str) -> UnitInfo | None: 

138 """Get unit information by name. 

139 

140 Args: 

141 name: Unit name (case-insensitive) 

142 

143 Returns: 

144 UnitInfo object, or None if not found 

145 """ 

146 return self.get_info(name) 

147 

148 def get_unit_info_by_id(self, unit_id: str) -> UnitInfo | None: 

149 """Get unit information by ID. 

150 

151 Args: 

152 unit_id: Unit ID (e.g., "org.bluetooth.unit.celsius") 

153 

154 Returns: 

155 UnitInfo object, or None if not found 

156 """ 

157 return self.get_info(unit_id) 

158 

159 def is_unit_uuid(self, uuid: str | BluetoothUUID) -> bool: 

160 """Check if a UUID is a registered unit UUID. 

161 

162 Args: 

163 uuid: UUID to check 

164 

165 Returns: 

166 True if the UUID is a unit UUID, False otherwise 

167 """ 

168 return self.get_unit_info(uuid) is not None 

169 

170 def get_all_units(self) -> list[UnitInfo]: 

171 """Get all registered units. 

172 

173 Returns: 

174 List of all UnitInfo objects 

175 """ 

176 self._ensure_loaded() 

177 with self._lock: 

178 return list(self._canonical_store.values()) 

179 

180 

181# Global instance 

182units_registry = UnitsRegistry()