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

32 statements  

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

1"""Volume Offset Control Point characteristic (0x2B82).""" 

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 VolumeOffsetControlPointOpCode(IntEnum): 

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

17 

18 SET_VOLUME_OFFSET = 0x01 

19 

20 

21_OFFSET_MINIMUM_LENGTH = 4 

22 

23 

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

25 """Parsed data from Volume Offset Control Point characteristic. 

26 

27 Contains the opcode, change counter, and optional volume offset parameter. 

28 """ 

29 

30 op_code: VolumeOffsetControlPointOpCode 

31 change_counter: int 

32 volume_offset: int | None = None 

33 

34 

35class VolumeOffsetControlPointCharacteristic(BaseCharacteristic[VolumeOffsetControlPointData]): 

36 """Volume Offset Control Point characteristic (0x2B82). 

37 

38 org.bluetooth.characteristic.volume_offset_control_point 

39 

40 Used for controlling volume offset in the Volume Offset Control Service. 

41 """ 

42 

43 _manual_role = CharacteristicRole.CONTROL 

44 min_length = 2 

45 allow_variable_length = True 

46 

47 def _decode_value( 

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

49 ) -> VolumeOffsetControlPointData: 

50 """Parse Volume Offset Control Point data. 

51 

52 Format: opcode (uint8) + change_counter (uint8) + optional sint16 offset. 

53 """ 

54 op_code = VolumeOffsetControlPointOpCode(DataParser.parse_int8(data, 0, signed=False)) 

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

56 

57 volume_offset = None 

58 if op_code == VolumeOffsetControlPointOpCode.SET_VOLUME_OFFSET and len(data) >= _OFFSET_MINIMUM_LENGTH: 

59 volume_offset = DataParser.parse_int16(data, 2, signed=True) 

60 

61 return VolumeOffsetControlPointData( 

62 op_code=op_code, 

63 change_counter=change_counter, 

64 volume_offset=volume_offset, 

65 ) 

66 

67 def _encode_value(self, data: VolumeOffsetControlPointData) -> bytearray: 

68 """Encode Volume Offset Control Point data to bytes.""" 

69 result = bytearray() 

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

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

72 if data.volume_offset is not None: 

73 result += DataParser.encode_int16(data.volume_offset, signed=True) 

74 return result