Coverage for src / bluetooth_sig / gatt / characteristics / new_alert.py: 97%

35 statements  

« prev     ^ index     » next       coverage.py v7.13.1, created at 2026-01-11 20:14 +0000

1"""New Alert characteristic (0x2A46) implementation. 

2 

3Represents a new alert with category, count, and optional text information. 

4Used by Alert Notification Service (0x1811). 

5 

6Based on Bluetooth SIG GATT Specification: 

7- New Alert: Variable length (Category ID + Number of New Alert + Text String) 

8""" 

9 

10from __future__ import annotations 

11 

12import msgspec 

13 

14from ...types import ALERT_TEXT_MAX_LENGTH, AlertCategoryID 

15from ..context import CharacteristicContext 

16from .base import BaseCharacteristic 

17from .utils import DataParser 

18 

19 

20class NewAlertData(msgspec.Struct): 

21 """New Alert characteristic data structure.""" 

22 

23 category_id: AlertCategoryID 

24 number_of_new_alert: int # 0-255 

25 text_string_information: str # 0-18 characters 

26 

27 

28class NewAlertCharacteristic(BaseCharacteristic[NewAlertData]): 

29 """New Alert characteristic (0x2A46). 

30 

31 Represents the category, count, and brief text for a new alert. 

32 

33 Structure (variable length): 

34 - Category ID: uint8 (0=Simple Alert, 1=Email, etc.) 

35 - Number of New Alert: uint8 (0-255, count of new alerts) 

36 - Text String Information: utf8s (0-18 characters, optional brief text) 

37 

38 Used by Alert Notification Service (0x1811). 

39 """ 

40 

41 min_length: int = 2 # Category ID(1) + Number of New Alert(1) 

42 allow_variable_length: bool = True # Optional text string 

43 

44 def _decode_value(self, data: bytearray, ctx: CharacteristicContext | None = None) -> NewAlertData: 

45 """Decode New Alert data from bytes. 

46 

47 Args: 

48 data: Raw characteristic data (minimum 2 bytes) 

49 ctx: Optional characteristic context 

50 

51 Returns: 

52 NewAlertData with all fields 

53 

54 Raises: 

55 ValueError: If data contains invalid values 

56 

57 """ 

58 # Parse Category ID (1 byte) 

59 category_id_raw = DataParser.parse_int8(data, 0, signed=False) 

60 category_id = AlertCategoryID(category_id_raw) 

61 

62 # Parse Number of New Alert (1 byte) 

63 number_of_new_alert = DataParser.parse_int8(data, 1, signed=False) 

64 

65 # Parse Text String Information (remaining bytes, max ALERT_TEXT_MAX_LENGTH characters) 

66 text_string_information = "" 

67 if len(data) > 2: 

68 text_bytes = data[2:] 

69 if len(text_bytes) > ALERT_TEXT_MAX_LENGTH: 

70 raise ValueError(f"Text string too long: {len(text_bytes)} bytes (max {ALERT_TEXT_MAX_LENGTH})") 

71 text_string_information = text_bytes.decode("utf-8", errors="replace") 

72 

73 return NewAlertData( 

74 category_id=category_id, 

75 number_of_new_alert=number_of_new_alert, 

76 text_string_information=text_string_information, 

77 ) 

78 

79 def _encode_value(self, data: NewAlertData) -> bytearray: 

80 """Encode New Alert data to bytes. 

81 

82 Args: 

83 data: NewAlertData to encode 

84 

85 Returns: 

86 Encoded new alert (variable length) 

87 

88 Raises: 

89 ValueError: If data contains invalid values 

90 

91 """ 

92 result = bytearray() 

93 

94 # Encode Category ID (1 byte) 

95 category_id_value = int(data.category_id) 

96 result.append(category_id_value) 

97 

98 # Encode Number of New Alert (1 byte) 

99 result.append(data.number_of_new_alert) 

100 

101 # Encode Text String Information (utf-8) 

102 if data.text_string_information: 

103 text_bytes = data.text_string_information.encode("utf-8") 

104 if len(text_bytes) > ALERT_TEXT_MAX_LENGTH: 

105 raise ValueError(f"Text string too long: {len(text_bytes)} bytes (max {ALERT_TEXT_MAX_LENGTH})") 

106 result.extend(text_bytes) 

107 

108 return result