Coverage for src/bluetooth_sig/registry/members.py: 94%

48 statements  

« prev     ^ index     » next       coverage.py v7.11.0, created at 2025-10-30 00:10 +0000

1"""Member UUID registry for Bluetooth SIG member companies.""" 

2 

3from __future__ import annotations 

4 

5import threading 

6 

7import msgspec 

8 

9from bluetooth_sig.types.uuid import BluetoothUUID 

10 

11from .utils import find_bluetooth_sig_path, load_yaml_uuids, normalize_uuid_string, parse_bluetooth_uuid 

12 

13 

14class MemberInfo(msgspec.Struct, frozen=True, kw_only=True): 

15 """Information about a Bluetooth SIG member company.""" 

16 

17 uuid: BluetoothUUID 

18 name: str 

19 

20 

21class MembersRegistry: 

22 """Registry for Bluetooth SIG member company UUIDs.""" 

23 

24 def __init__(self) -> None: 

25 """Initialize the members registry.""" 

26 self._lock = threading.RLock() 

27 self._members: dict[str, MemberInfo] = {} # normalized_uuid -> MemberInfo 

28 self._members_by_name: dict[str, MemberInfo] = {} # lower_name -> MemberInfo 

29 

30 try: 

31 self._load_members() 

32 except (FileNotFoundError, Exception): # pylint: disable=broad-exception-caught 

33 # If YAML loading fails, continue with empty registry 

34 pass 

35 

36 def _load_members(self) -> None: 

37 """Load member UUIDs from YAML file.""" 

38 base_path = find_bluetooth_sig_path() 

39 if not base_path: 

40 return 

41 

42 # Load member UUIDs 

43 member_yaml = base_path / "member_uuids.yaml" 

44 if member_yaml.exists(): 

45 for uuid_info in load_yaml_uuids(member_yaml): 

46 uuid = normalize_uuid_string(uuid_info["uuid"]) 

47 

48 bt_uuid = BluetoothUUID(uuid) 

49 info = MemberInfo(uuid=bt_uuid, name=uuid_info["name"]) 

50 # Store using short form as key for easy lookup 

51 self._members[bt_uuid.short_form.upper()] = info 

52 # Also store by name for reverse lookup 

53 self._members_by_name[uuid_info["name"].lower()] = info 

54 

55 def get_member_name(self, uuid: str | int | BluetoothUUID) -> str | None: 

56 """Get member company name by UUID. 

57 

58 Args: 

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

60 

61 Returns: 

62 Member company name, or None if not found 

63 """ 

64 with self._lock: 

65 try: 

66 bt_uuid = parse_bluetooth_uuid(uuid) 

67 

68 # Get the short form (16-bit) for lookup 

69 short_key = bt_uuid.short_form.upper() 

70 if short_key in self._members: 

71 return self._members[short_key].name 

72 

73 return None 

74 except ValueError: 

75 return None 

76 

77 def is_member_uuid(self, uuid: str | int | BluetoothUUID) -> bool: 

78 """Check if a UUID is a registered member company UUID. 

79 

80 Args: 

81 uuid: UUID to check 

82 

83 Returns: 

84 True if the UUID is a member UUID, False otherwise 

85 """ 

86 return self.get_member_name(uuid) is not None 

87 

88 def get_all_members(self) -> list[MemberInfo]: 

89 """Get all registered member companies. 

90 

91 Returns: 

92 List of all MemberInfo objects 

93 """ 

94 with self._lock: 

95 return list(self._members.values()) 

96 

97 def get_member_info_by_name(self, name: str) -> MemberInfo | None: 

98 """Get member information by company name. 

99 

100 Args: 

101 name: Company name (case-insensitive) 

102 

103 Returns: 

104 MemberInfo object, or None if not found 

105 """ 

106 with self._lock: 

107 return self._members_by_name.get(name.lower()) 

108 

109 

110# Global instance 

111members_registry = MembersRegistry()