Coverage for src / bluetooth_sig / gatt / characteristics / volume_control_point.py: 100%

38 statements  

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

1"""Volume Control Point characteristic (0x2B7E).""" 

2 

3from __future__ import annotations 

4 

5from enum import IntEnum 

6 

7import msgspec 

8 

9from ...types.gatt_enums import CharacteristicRole 

10from ..context import CharacteristicContext 

11from .base import BaseCharacteristic 

12from .utils import DataParser 

13 

14 

15class VolumeControlPointOpCode(IntEnum): 

16 """Volume Control Point operation codes.""" 

17 

18 RELATIVE_VOLUME_DOWN = 0x00 

19 RELATIVE_VOLUME_UP = 0x01 

20 UNMUTE_RELATIVE_VOLUME_DOWN = 0x02 

21 UNMUTE_RELATIVE_VOLUME_UP = 0x03 

22 SET_ABSOLUTE_VOLUME = 0x04 

23 UNMUTE = 0x05 

24 MUTE = 0x06 

25 

26 

27_VOLUME_SETTING_MINIMUM_LENGTH = 3 

28 

29 

30class VolumeControlPointData(msgspec.Struct, frozen=True, kw_only=True): # pylint: disable=too-few-public-methods 

31 """Parsed data from Volume Control Point characteristic. 

32 

33 The parameter field contains opcode-specific data as raw bytes, 

34 or None for opcodes with no additional parameters beyond change_counter. 

35 """ 

36 

37 op_code: VolumeControlPointOpCode 

38 change_counter: int 

39 volume_setting: int | None = None 

40 

41 

42class VolumeControlPointCharacteristic(BaseCharacteristic[VolumeControlPointData]): 

43 """Volume Control Point characteristic (0x2B7E). 

44 

45 org.bluetooth.characteristic.volume_control_point 

46 

47 Used for controlling volume settings in the Volume Control Service. 

48 """ 

49 

50 _manual_role = CharacteristicRole.CONTROL 

51 min_length = 2 

52 allow_variable_length = True 

53 

54 def _decode_value( 

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

56 ) -> VolumeControlPointData: 

57 """Parse Volume Control Point data. 

58 

59 Format: opcode (uint8) + change_counter (uint8) + optional parameter. 

60 """ 

61 op_code = VolumeControlPointOpCode(DataParser.parse_int8(data, 0, signed=False)) 

62 change_counter = DataParser.parse_int8(data, 1, signed=False) 

63 

64 volume_setting = None 

65 if op_code == VolumeControlPointOpCode.SET_ABSOLUTE_VOLUME and len(data) >= _VOLUME_SETTING_MINIMUM_LENGTH: 

66 volume_setting = DataParser.parse_int8(data, 2, signed=False) 

67 

68 return VolumeControlPointData( 

69 op_code=op_code, 

70 change_counter=change_counter, 

71 volume_setting=volume_setting, 

72 ) 

73 

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

75 """Encode Volume Control Point data to bytes.""" 

76 result = bytearray() 

77 result += DataParser.encode_int8(int(data.op_code)) 

78 result += DataParser.encode_int8(data.change_counter) 

79 if data.volume_setting is not None: 

80 result += DataParser.encode_int8(data.volume_setting) 

81 return result