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

36 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-03-18 11:17 +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# Protocol constants 

20_MIN_LENGTH_WITH_TEXT = 2 # Minimum length before text string information 

21 

22 

23class NewAlertData(msgspec.Struct): 

24 """New Alert characteristic data structure.""" 

25 

26 category_id: AlertCategoryID 

27 number_of_new_alert: int # 0-255 

28 text_string_information: str # 0-18 characters 

29 

30 

31class NewAlertCharacteristic(BaseCharacteristic[NewAlertData]): 

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

33 

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

35 

36 Structure (variable length): 

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

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

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

40 

41 Used by Alert Notification Service (0x1811). 

42 """ 

43 

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

45 allow_variable_length: bool = True # Optional text string 

46 

47 def _decode_value( 

48 self, data: bytearray, ctx: CharacteristicContext | None = None, *, validate: bool = True 

49 ) -> NewAlertData: 

50 """Decode New Alert data from bytes. 

51 

52 Args: 

53 data: Raw characteristic data (minimum 2 bytes) 

54 ctx: Optional characteristic context 

55 validate: Whether to validate ranges (default True) 

56 

57 Returns: 

58 NewAlertData with all fields 

59 

60 Raises: 

61 ValueError: If data contains invalid values 

62 

63 """ 

64 # Parse Category ID (1 byte) 

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

66 category_id = AlertCategoryID(category_id_raw) 

67 

68 # Parse Number of New Alert (1 byte) 

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

70 

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

72 text_string_information = "" 

73 if len(data) > _MIN_LENGTH_WITH_TEXT: 

74 text_bytes = data[2:] 

75 if len(text_bytes) > ALERT_TEXT_MAX_LENGTH: 

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

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

78 

79 return NewAlertData( 

80 category_id=category_id, 

81 number_of_new_alert=number_of_new_alert, 

82 text_string_information=text_string_information, 

83 ) 

84 

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

86 """Encode New Alert data to bytes. 

87 

88 Args: 

89 data: NewAlertData to encode 

90 

91 Returns: 

92 Encoded new alert (variable length) 

93 

94 Raises: 

95 ValueError: If data contains invalid values 

96 

97 """ 

98 result = bytearray() 

99 

100 # Encode Category ID (1 byte) 

101 category_id_value = int(data.category_id) 

102 result.append(category_id_value) 

103 

104 # Encode Number of New Alert (1 byte) 

105 result.append(data.number_of_new_alert) 

106 

107 # Encode Text String Information (utf-8) 

108 if data.text_string_information: 

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

110 if len(text_bytes) > ALERT_TEXT_MAX_LENGTH: 

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

112 result.extend(text_bytes) 

113 

114 return result