Coverage for src/bluetooth_sig/gatt/characteristics/recipe_control.py: 100%
32 statements
« prev ^ index » next coverage.py v7.14.3, created at 2026-06-28 01:26 +0000
« prev ^ index » next coverage.py v7.14.3, created at 2026-06-28 01:26 +0000
1"""Recipe Control characteristic (0x2C26)."""
3from __future__ import annotations
5from enum import IntEnum
7import msgspec
9from ..constants import SIZE_UINT8
10from ..context import CharacteristicContext
11from .base import BaseCharacteristic
12from .utils import DataParser
15class RecipeControlOpCode(IntEnum):
16 """Recipe Control operation codes."""
18 READ = 0x00
19 START = 0x01
20 STOP = 0x02
21 DELETE = 0x03
24class RecipeControlData(msgspec.Struct, frozen=True, kw_only=True):
25 """Decoded Recipe Control payload."""
27 op_code: RecipeControlOpCode
28 cooking_step_index: int | None = None
31class RecipeControlCharacteristic(BaseCharacteristic[RecipeControlData]):
32 """Recipe Control characteristic (0x2C26).
34 org.bluetooth.characteristic.recipe_control
35 """
37 min_length = SIZE_UINT8
38 max_length = 3
39 allow_variable_length = True
40 _FULL_PAYLOAD_LENGTH = 3
42 def _decode_value(
43 self, data: bytearray, ctx: CharacteristicContext | None = None, *, validate: bool = True
44 ) -> RecipeControlData:
45 if len(data) not in {SIZE_UINT8, self._FULL_PAYLOAD_LENGTH}:
46 raise ValueError("Recipe Control payload must be 1 or 3 bytes")
48 op_code = RecipeControlOpCode(DataParser.parse_int8(data, 0, signed=False))
49 cooking_step_index = (
50 DataParser.parse_int16(data, SIZE_UINT8, signed=False) if len(data) == self._FULL_PAYLOAD_LENGTH else None
51 )
52 return RecipeControlData(op_code=op_code, cooking_step_index=cooking_step_index)
54 def _encode_value(self, data: RecipeControlData) -> bytearray:
55 result = bytearray()
56 result.extend(DataParser.encode_int8(int(data.op_code), signed=False))
57 if data.cooking_step_index is not None:
58 result.extend(DataParser.encode_int16(data.cooking_step_index, signed=False))
59 return result