Coverage for src / bluetooth_sig / gatt / characteristics / relative_value_in_an_illuminance_range.py: 89%

46 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-03-18 11:17 +0000

1"""Relative Value in an Illuminance Range characteristic implementation.""" 

2 

3from __future__ import annotations 

4 

5import msgspec 

6 

7from ..constants import UINT8_MAX, UINT24_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_ILLUMINANCE_RESOLUTION = 0.01 # Illuminance: M=1, d=-2, b=0 -> 0.01 lux 

14 

15 

16class RelativeValueInAnIlluminanceRangeData(msgspec.Struct, frozen=True, kw_only=True): # pylint: disable=too-few-public-methods 

17 """Data class for relative value in an illuminance range. 

18 

19 Combines a percentage (0.5% resolution) with an illuminance range 

20 (min/max in lux, 0.01 lux resolution). 

21 """ 

22 

23 relative_value: float # Percentage (0.5% resolution) 

24 minimum_illuminance: float # Minimum illuminance in lux 

25 maximum_illuminance: float # Maximum illuminance in lux 

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_illuminance > self.maximum_illuminance: 

33 raise ValueError( 

34 f"Minimum illuminance {self.minimum_illuminance} lux " 

35 f"cannot exceed maximum {self.maximum_illuminance} lux" 

36 ) 

37 max_lux = UINT24_MAX * _ILLUMINANCE_RESOLUTION 

38 for name, val in [ 

39 ("minimum_illuminance", self.minimum_illuminance), 

40 ("maximum_illuminance", self.maximum_illuminance), 

41 ]: 

42 if not 0.0 <= val <= max_lux: 

43 raise ValueError(f"{name} {val} lux is outside valid range (0.0 to {max_lux})") 

44 

45 

46class RelativeValueInAnIlluminanceRangeCharacteristic( 

47 BaseCharacteristic[RelativeValueInAnIlluminanceRangeData], 

48): 

49 """Relative Value in an Illuminance Range characteristic (0x2B0A). 

50 

51 org.bluetooth.characteristic.relative_value_in_an_illuminance_range 

52 

53 Represents a relative value within an illuminance range. Fields: 

54 Percentage 8 (uint8, 0.5%), min illuminance (uint24, 0.01 lux), 

55 max illuminance (uint24, 0.01 lux). 

56 """ 

57 

58 expected_length: int = 7 # uint8 + 2 x uint24 

59 min_length: int = 7 

60 expected_type = RelativeValueInAnIlluminanceRangeData 

61 

62 def _decode_value( 

63 self, data: bytearray, ctx: CharacteristicContext | None = None, *, validate: bool = True 

64 ) -> RelativeValueInAnIlluminanceRangeData: 

65 """Parse relative value in an illuminance range. 

66 

67 Args: 

68 data: Raw bytes (7 bytes). 

69 ctx: Optional CharacteristicContext. 

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

71 

72 Returns: 

73 RelativeValueInAnIlluminanceRangeData. 

74 

75 """ 

76 pct_raw = DataParser.parse_int8(data, 0, signed=False) 

77 min_raw = DataParser.parse_int24(data, 1, signed=False) 

78 max_raw = DataParser.parse_int24(data, 4, signed=False) 

79 

80 return RelativeValueInAnIlluminanceRangeData( 

81 relative_value=pct_raw * _PERCENTAGE_RESOLUTION, 

82 minimum_illuminance=min_raw * _ILLUMINANCE_RESOLUTION, 

83 maximum_illuminance=max_raw * _ILLUMINANCE_RESOLUTION, 

84 ) 

85 

86 def _encode_value(self, data: RelativeValueInAnIlluminanceRangeData) -> bytearray: 

87 """Encode relative value in an illuminance range. 

88 

89 Args: 

90 data: RelativeValueInAnIlluminanceRangeData instance. 

91 

92 Returns: 

93 Encoded bytes (7 bytes). 

94 

95 """ 

96 pct_raw = round(data.relative_value / _PERCENTAGE_RESOLUTION) 

97 min_raw = round(data.minimum_illuminance / _ILLUMINANCE_RESOLUTION) 

98 max_raw = round(data.maximum_illuminance / _ILLUMINANCE_RESOLUTION) 

99 

100 if not 0 <= pct_raw <= UINT8_MAX: 

101 raise ValueError(f"Percentage raw {pct_raw} exceeds uint8 range") 

102 if not 0 <= min_raw <= UINT24_MAX: 

103 raise ValueError(f"Min illuminance raw {min_raw} exceeds uint24 range") 

104 if not 0 <= max_raw <= UINT24_MAX: 

105 raise ValueError(f"Max illuminance raw {max_raw} exceeds uint24 range") 

106 

107 result = bytearray() 

108 result.extend(DataParser.encode_int8(pct_raw, signed=False)) 

109 result.extend(DataParser.encode_int24(min_raw, signed=False)) 

110 result.extend(DataParser.encode_int24(max_raw, signed=False)) 

111 return result