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
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-03 16:41 +0000
1"""Volume Control Point characteristic (0x2B7E)."""
3from __future__ import annotations
5from enum import IntEnum
7import msgspec
9from ...types.gatt_enums import CharacteristicRole
10from ..context import CharacteristicContext
11from .base import BaseCharacteristic
12from .utils import DataParser
15class VolumeControlPointOpCode(IntEnum):
16 """Volume Control Point operation codes."""
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
27_VOLUME_SETTING_MINIMUM_LENGTH = 3
30class VolumeControlPointData(msgspec.Struct, frozen=True, kw_only=True): # pylint: disable=too-few-public-methods
31 """Parsed data from Volume Control Point characteristic.
33 The parameter field contains opcode-specific data as raw bytes,
34 or None for opcodes with no additional parameters beyond change_counter.
35 """
37 op_code: VolumeControlPointOpCode
38 change_counter: int
39 volume_setting: int | None = None
42class VolumeControlPointCharacteristic(BaseCharacteristic[VolumeControlPointData]):
43 """Volume Control Point characteristic (0x2B7E).
45 org.bluetooth.characteristic.volume_control_point
47 Used for controlling volume settings in the Volume Control Service.
48 """
50 _manual_role = CharacteristicRole.CONTROL
51 min_length = 2
52 allow_variable_length = True
54 def _decode_value(
55 self, data: bytearray, ctx: CharacteristicContext | None = None, *, validate: bool = True
56 ) -> VolumeControlPointData:
57 """Parse Volume Control Point data.
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)
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)
68 return VolumeControlPointData(
69 op_code=op_code,
70 change_counter=change_counter,
71 volume_setting=volume_setting,
72 )
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