Coverage for src / bluetooth_sig / gatt / characteristics / call_control_point.py: 91%

43 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-04-03 16:41 +0000

1"""Call Control Point characteristic (0x2BBE).""" 

2 

3from __future__ import annotations 

4 

5from enum import IntEnum 

6 

7import msgspec 

8 

9from ..context import CharacteristicContext 

10from .base import BaseCharacteristic 

11from .utils import DataParser 

12 

13 

14class CallControlPointOpCode(IntEnum): 

15 """Call Control Point operation codes per TBS specification.""" 

16 

17 ACCEPT = 0x00 

18 TERMINATE = 0x01 

19 LOCAL_HOLD = 0x02 

20 LOCAL_RETRIEVE = 0x03 

21 ORIGINATE = 0x04 

22 JOIN = 0x05 

23 

24 

25class CallControlPointData(msgspec.Struct, frozen=True, kw_only=True): 

26 """Parsed data from Call Control Point characteristic. 

27 

28 For ACCEPT/TERMINATE/LOCAL_HOLD/LOCAL_RETRIEVE: call_index is set. 

29 For ORIGINATE: uri is set. 

30 For JOIN: call_indexes is set. 

31 """ 

32 

33 op_code: CallControlPointOpCode 

34 call_index: int | None = None 

35 uri: str | None = None 

36 call_indexes: tuple[int, ...] | None = None 

37 

38 

39class CallControlPointCharacteristic(BaseCharacteristic[CallControlPointData]): 

40 """Call Control Point characteristic (0x2BBE). 

41 

42 org.bluetooth.characteristic.call_control_point 

43 

44 Used to control calls on the telephone bearer. 

45 """ 

46 

47 min_length = 1 

48 allow_variable_length = True 

49 

50 def _decode_value( 

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

52 ) -> CallControlPointData: 

53 op_code = CallControlPointOpCode(DataParser.parse_int8(data, 0, signed=False)) 

54 

55 if op_code in ( 

56 CallControlPointOpCode.ACCEPT, 

57 CallControlPointOpCode.TERMINATE, 

58 CallControlPointOpCode.LOCAL_HOLD, 

59 CallControlPointOpCode.LOCAL_RETRIEVE, 

60 ): 

61 call_index = DataParser.parse_int8(data, 1, signed=False) if len(data) > 1 else None 

62 return CallControlPointData(op_code=op_code, call_index=call_index) 

63 

64 if op_code == CallControlPointOpCode.ORIGINATE: 

65 uri = DataParser.parse_utf8_string(data[1:]) if len(data) > 1 else None 

66 return CallControlPointData(op_code=op_code, uri=uri) 

67 

68 if op_code == CallControlPointOpCode.JOIN: 

69 indexes = tuple(data[i] for i in range(1, len(data))) 

70 return CallControlPointData(op_code=op_code, call_indexes=indexes) 

71 

72 return CallControlPointData(op_code=op_code) 

73 

74 def _encode_value(self, data: CallControlPointData) -> bytearray: 

75 result = bytearray([int(data.op_code)]) 

76 

77 if data.call_index is not None: 

78 result.extend(DataParser.encode_int8(data.call_index, signed=False)) 

79 elif data.uri is not None: 

80 result.extend(data.uri.encode("utf-8")) 

81 elif data.call_indexes is not None: 

82 for idx in data.call_indexes: 

83 result.append(idx) 

84 

85 return result