Coverage for src / bluetooth_sig / gatt / characteristics / relative_runtime_in_a_current_range.py: 89%
45 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 Current 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_CURRENT_RESOLUTION = 0.01 # Electric Current: M=1, d=-2, b=0 -> 0.01 A
16class RelativeRuntimeInACurrentRangeData(msgspec.Struct, frozen=True, kw_only=True): # pylint: disable=too-few-public-methods
17 """Data class for relative runtime in a current range.
19 Combines a percentage (0.5% resolution) with a current range
20 (min/max in amperes, 0.01 A resolution).
21 """
23 relative_runtime: float # Percentage (0.5% resolution)
24 minimum_current: float # Minimum current in A
25 maximum_current: float # Maximum current in A
27 def __post_init__(self) -> None:
28 """Validate data fields."""
29 max_pct = UINT8_MAX * _PERCENTAGE_RESOLUTION
30 if not 0.0 <= self.relative_runtime <= max_pct:
31 raise ValueError(f"Relative runtime {self.relative_runtime}% is outside valid range (0.0 to {max_pct})")
32 if self.minimum_current > self.maximum_current:
33 raise ValueError(f"Minimum current {self.minimum_current} A cannot exceed maximum {self.maximum_current} A")
34 max_current = UINT16_MAX * _CURRENT_RESOLUTION
35 for name, val in [
36 ("minimum_current", self.minimum_current),
37 ("maximum_current", self.maximum_current),
38 ]:
39 if not 0.0 <= val <= max_current:
40 raise ValueError(f"{name} {val} A is outside valid range (0.0 to {max_current})")
43class RelativeRuntimeInACurrentRangeCharacteristic(
44 BaseCharacteristic[RelativeRuntimeInACurrentRangeData],
45):
46 """Relative Runtime in a Current Range characteristic (0x2B07).
48 org.bluetooth.characteristic.relative_runtime_in_a_current_range
50 Represents relative runtime within an electric current range. Fields:
51 Percentage 8 (uint8, 0.5%), min current (uint16, 0.01 A),
52 max current (uint16, 0.01 A).
53 """
55 expected_length: int = 5 # uint8 + 2 x uint16
56 min_length: int = 5
58 def _decode_value(
59 self, data: bytearray, ctx: CharacteristicContext | None = None, *, validate: bool = True
60 ) -> RelativeRuntimeInACurrentRangeData:
61 """Parse relative runtime in a current range.
63 Args:
64 data: Raw bytes (5 bytes).
65 ctx: Optional CharacteristicContext.
66 validate: Whether to validate ranges (default True).
68 Returns:
69 RelativeRuntimeInACurrentRangeData.
71 """
72 pct_raw = DataParser.parse_int8(data, 0, signed=False)
73 min_raw = DataParser.parse_int16(data, 1, signed=False)
74 max_raw = DataParser.parse_int16(data, 3, signed=False)
76 return RelativeRuntimeInACurrentRangeData(
77 relative_runtime=pct_raw * _PERCENTAGE_RESOLUTION,
78 minimum_current=min_raw * _CURRENT_RESOLUTION,
79 maximum_current=max_raw * _CURRENT_RESOLUTION,
80 )
82 def _encode_value(self, data: RelativeRuntimeInACurrentRangeData) -> bytearray:
83 """Encode relative runtime in a current range.
85 Args:
86 data: RelativeRuntimeInACurrentRangeData instance.
88 Returns:
89 Encoded bytes (5 bytes).
91 """
92 pct_raw = round(data.relative_runtime / _PERCENTAGE_RESOLUTION)
93 min_raw = round(data.minimum_current / _CURRENT_RESOLUTION)
94 max_raw = round(data.maximum_current / _CURRENT_RESOLUTION)
96 if not 0 <= pct_raw <= UINT8_MAX:
97 raise ValueError(f"Percentage raw {pct_raw} exceeds uint8 range")
98 if not 0 <= min_raw <= UINT16_MAX:
99 raise ValueError(f"Min current raw {min_raw} exceeds uint16 range")
100 if not 0 <= max_raw <= UINT16_MAX:
101 raise ValueError(f"Max current raw {max_raw} exceeds uint16 range")
103 result = bytearray()
104 result.extend(DataParser.encode_int8(pct_raw, signed=False))
105 result.extend(DataParser.encode_int16(min_raw, signed=False))
106 result.extend(DataParser.encode_int16(max_raw, signed=False))
107 return result