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

1"""Relative Runtime in a Generic Level 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# Generic Level: M=1, d=0, b=0 -> unitless, no scaling 

14 

15 

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. 

18 

19 Combines a percentage (0.5% resolution) with a generic level range 

20 (min/max as raw uint16 values, unitless). 

21 """ 

22 

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) 

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

42 

43 

44class RelativeRuntimeInAGenericLevelRangeCharacteristic( 

45 BaseCharacteristic[RelativeRuntimeInAGenericLevelRangeData], 

46): 

47 """Relative Runtime in a Generic Level Range characteristic (0x2B08). 

48 

49 org.bluetooth.characteristic.relative_runtime_in_a_generic_level_range 

50 

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

55 

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

57 min_length: int = 5 

58 

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. 

63 

64 Args: 

65 data: Raw bytes (5 bytes). 

66 ctx: Optional CharacteristicContext. 

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

68 

69 Returns: 

70 RelativeRuntimeInAGenericLevelRangeData. 

71 

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) 

76 

77 return RelativeRuntimeInAGenericLevelRangeData( 

78 relative_value=pct_raw * _PERCENTAGE_RESOLUTION, 

79 minimum_generic_level=min_raw, 

80 maximum_generic_level=max_raw, 

81 ) 

82 

83 def _encode_value(self, data: RelativeRuntimeInAGenericLevelRangeData) -> bytearray: 

84 """Encode relative runtime in a generic level range. 

85 

86 Args: 

87 data: RelativeRuntimeInAGenericLevelRangeData instance. 

88 

89 Returns: 

90 Encoded bytes (5 bytes). 

91 

92 """ 

93 pct_raw = round(data.relative_value / _PERCENTAGE_RESOLUTION) 

94 

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

101 

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