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
« prev ^ index » next coverage.py v7.13.5, created at 2026-03-18 11:17 +0000
1"""New Alert characteristic (0x2A46) implementation.
3Represents a new alert with category, count, and optional text information.
4Used by Alert Notification Service (0x1811).
6Based on Bluetooth SIG GATT Specification:
7- New Alert: Variable length (Category ID + Number of New Alert + Text String)
8"""
10from __future__ import annotations
12import msgspec
14from ...types import ALERT_TEXT_MAX_LENGTH, AlertCategoryID
15from ..context import CharacteristicContext
16from .base import BaseCharacteristic
17from .utils import DataParser
19# Protocol constants
20_MIN_LENGTH_WITH_TEXT = 2 # Minimum length before text string information
23class NewAlertData(msgspec.Struct):
24 """New Alert characteristic data structure."""
26 category_id: AlertCategoryID
27 number_of_new_alert: int # 0-255
28 text_string_information: str # 0-18 characters
31class NewAlertCharacteristic(BaseCharacteristic[NewAlertData]):
32 """New Alert characteristic (0x2A46).
34 Represents the category, count, and brief text for a new alert.
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)
41 Used by Alert Notification Service (0x1811).
42 """
44 min_length: int = 2 # Category ID(1) + Number of New Alert(1)
45 allow_variable_length: bool = True # Optional text string
47 def _decode_value(
48 self, data: bytearray, ctx: CharacteristicContext | None = None, *, validate: bool = True
49 ) -> NewAlertData:
50 """Decode New Alert data from bytes.
52 Args:
53 data: Raw characteristic data (minimum 2 bytes)
54 ctx: Optional characteristic context
55 validate: Whether to validate ranges (default True)
57 Returns:
58 NewAlertData with all fields
60 Raises:
61 ValueError: If data contains invalid values
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)
68 # Parse Number of New Alert (1 byte)
69 number_of_new_alert = DataParser.parse_int8(data, 1, signed=False)
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")
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 )
85 def _encode_value(self, data: NewAlertData) -> bytearray:
86 """Encode New Alert data to bytes.
88 Args:
89 data: NewAlertData to encode
91 Returns:
92 Encoded new alert (variable length)
94 Raises:
95 ValueError: If data contains invalid values
97 """
98 result = bytearray()
100 # Encode Category ID (1 byte)
101 category_id_value = int(data.category_id)
102 result.append(category_id_value)
104 # Encode Number of New Alert (1 byte)
105 result.append(data.number_of_new_alert)
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)
114 return result