Coverage for src / bluetooth_sig / gatt / characteristics / relative_runtime_in_a_generic_level_range.py: 88%
41 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-03-18 11:17 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-03-18 11:17 +0000
1"""Relative Runtime in a Generic Level Range characteristic implementation."""
3from __future__ import annotations
5import msgspec
7from ..constants import UINT8_MAX, UINT16_MAX
8from ..context import CharacteristicContext
9from .base import BaseCharacteristic
10from .utils import DataParser
12_PERCENTAGE_RESOLUTION = 0.5 # Percentage 8: M=1, d=0, b=-1 -> 0.5%
13# Generic Level: M=1, d=0, b=0 -> unitless, no scaling
16class RelativeRuntimeInAGenericLevelRangeData(msgspec.Struct, frozen=True, kw_only=True): # pylint: disable=too-few-public-methods
17 """Data class for relative runtime in a generic level range.
19 Combines a percentage (0.5% resolution) with a generic level range
20 (min/max as raw uint16 values, unitless).
21 """
23 relative_value: float # Percentage (0.5% resolution)
24 minimum_generic_level: int # Minimum generic level (unitless)
25 maximum_generic_level: int # Maximum generic level (unitless)
27 def __post_init__(self) -> None:
28 """Validate data fields."""
29 max_pct = UINT8_MAX * _PERCENTAGE_RESOLUTION
30 if not 0.0 <= self.relative_value <= max_pct:
31 raise ValueError(f"Relative value {self.relative_value}% is outside valid range (0.0 to {max_pct})")
32 if self.minimum_generic_level > self.maximum_generic_level:
33 raise ValueError(
34 f"Minimum generic level {self.minimum_generic_level} cannot exceed maximum {self.maximum_generic_level}"
35 )
36 for name, val in [
37 ("minimum_generic_level", self.minimum_generic_level),
38 ("maximum_generic_level", self.maximum_generic_level),
39 ]:
40 if not 0 <= val <= UINT16_MAX:
41 raise ValueError(f"{name} {val} is outside valid range (0 to {UINT16_MAX})")
44class RelativeRuntimeInAGenericLevelRangeCharacteristic(
45 BaseCharacteristic[RelativeRuntimeInAGenericLevelRangeData],
46):
47 """Relative Runtime in a Generic Level Range characteristic (0x2B08).
49 org.bluetooth.characteristic.relative_runtime_in_a_generic_level_range
51 Represents relative runtime within a generic level range. Fields:
52 Percentage 8 (uint8, 0.5%), min level (uint16, unitless),
53 max level (uint16, unitless).
54 """
56 expected_length: int = 5 # uint8 + 2 x uint16
57 min_length: int = 5
59 def _decode_value(
60 self, data: bytearray, ctx: CharacteristicContext | None = None, *, validate: bool = True
61 ) -> RelativeRuntimeInAGenericLevelRangeData:
62 """Parse relative runtime in a generic level range.
64 Args:
65 data: Raw bytes (5 bytes).
66 ctx: Optional CharacteristicContext.
67 validate: Whether to validate ranges (default True).
69 Returns:
70 RelativeRuntimeInAGenericLevelRangeData.
72 """
73 pct_raw = DataParser.parse_int8(data, 0, signed=False)
74 min_raw = DataParser.parse_int16(data, 1, signed=False)
75 max_raw = DataParser.parse_int16(data, 3, signed=False)
77 return RelativeRuntimeInAGenericLevelRangeData(
78 relative_value=pct_raw * _PERCENTAGE_RESOLUTION,
79 minimum_generic_level=min_raw,
80 maximum_generic_level=max_raw,
81 )
83 def _encode_value(self, data: RelativeRuntimeInAGenericLevelRangeData) -> bytearray:
84 """Encode relative runtime in a generic level range.
86 Args:
87 data: RelativeRuntimeInAGenericLevelRangeData instance.
89 Returns:
90 Encoded bytes (5 bytes).
92 """
93 pct_raw = round(data.relative_value / _PERCENTAGE_RESOLUTION)
95 if not 0 <= pct_raw <= UINT8_MAX:
96 raise ValueError(f"Percentage raw {pct_raw} exceeds uint8 range")
97 if not 0 <= data.minimum_generic_level <= UINT16_MAX:
98 raise ValueError(f"Min level {data.minimum_generic_level} exceeds uint16 range")
99 if not 0 <= data.maximum_generic_level <= UINT16_MAX:
100 raise ValueError(f"Max level {data.maximum_generic_level} exceeds uint16 range")
102 result = bytearray()
103 result.extend(DataParser.encode_int8(pct_raw, signed=False))
104 result.extend(DataParser.encode_int16(data.minimum_generic_level, signed=False))
105 result.extend(DataParser.encode_int16(data.maximum_generic_level, signed=False))
106 return result