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

1"""Relative Runtime in a Current Range characteristic implementation.""" 

2 

3from __future__ import annotations 

4 

5import msgspec 

6 

7from ..constants import UINT8_MAX, UINT16_MAX 

8from ..context import CharacteristicContext 

9from .base import BaseCharacteristic 

10from .utils import DataParser 

11 

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 

14 

15 

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. 

18 

19 Combines a percentage (0.5% resolution) with a current range 

20 (min/max in amperes, 0.01 A resolution). 

21 """ 

22 

23 relative_runtime: float # Percentage (0.5% resolution) 

24 minimum_current: float # Minimum current in A 

25 maximum_current: float # Maximum current in A 

26 

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})") 

41 

42 

43class RelativeRuntimeInACurrentRangeCharacteristic( 

44 BaseCharacteristic[RelativeRuntimeInACurrentRangeData], 

45): 

46 """Relative Runtime in a Current Range characteristic (0x2B07). 

47 

48 org.bluetooth.characteristic.relative_runtime_in_a_current_range 

49 

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 """ 

54 

55 expected_length: int = 5 # uint8 + 2 x uint16 

56 min_length: int = 5 

57 

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. 

62 

63 Args: 

64 data: Raw bytes (5 bytes). 

65 ctx: Optional CharacteristicContext. 

66 validate: Whether to validate ranges (default True). 

67 

68 Returns: 

69 RelativeRuntimeInACurrentRangeData. 

70 

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) 

75 

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 ) 

81 

82 def _encode_value(self, data: RelativeRuntimeInACurrentRangeData) -> bytearray: 

83 """Encode relative runtime in a current range. 

84 

85 Args: 

86 data: RelativeRuntimeInACurrentRangeData instance. 

87 

88 Returns: 

89 Encoded bytes (5 bytes). 

90 

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) 

95 

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") 

102 

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