Coverage for src / bluetooth_sig / gatt / characteristics / electric_current_range.py: 85%

39 statements  

« prev     ^ index     » next       coverage.py v7.13.1, created at 2026-01-11 20:14 +0000

1"""Electric Current Range characteristic implementation.""" 

2 

3from __future__ import annotations 

4 

5import msgspec 

6 

7from ...types.gatt_enums import ValueType 

8from ..constants import UINT16_MAX 

9from ..context import CharacteristicContext 

10from .base import BaseCharacteristic 

11from .utils import DataParser 

12 

13 

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

15 """Data class for electric current range.""" 

16 

17 min: float # Minimum current in Amperes 

18 max: float # Maximum current in Amperes 

19 

20 def __post_init__(self) -> None: 

21 """Validate current range data.""" 

22 if self.min > self.max: 

23 raise ValueError(f"Minimum current {self.min} A cannot be greater than maximum {self.max} A") 

24 

25 # Validate range for uint16 with 0.01 A resolution (0 to 655.35 A) 

26 max_current_value = UINT16_MAX * 0.01 

27 if not 0.0 <= self.min <= max_current_value: 

28 raise ValueError(f"Minimum current {self.min} A is outside valid range (0.0 to {max_current_value} A)") 

29 if not 0.0 <= self.max <= max_current_value: 

30 raise ValueError(f"Maximum current {self.max} A is outside valid range (0.0 to {max_current_value} A)") 

31 

32 

33class ElectricCurrentRangeCharacteristic(BaseCharacteristic[ElectricCurrentRangeData]): 

34 """Electric Current Range characteristic (0x2AEF). 

35 

36 org.bluetooth.characteristic.electric_current_range 

37 

38 Electric Current Range characteristic. 

39 

40 Specifies lower and upper current bounds (2x uint16). 

41 """ 

42 

43 # Validation attributes 

44 expected_length: int = 4 # 2x uint16 

45 

46 # Override since decode_value returns structured ElectricCurrentRangeData 

47 _manual_value_type: ValueType | str | None = ValueType.DICT 

48 

49 def _decode_value(self, data: bytearray, ctx: CharacteristicContext | None = None) -> ElectricCurrentRangeData: 

50 """Parse current range data (2x uint16 in units of 0.01 A). 

51 

52 Args: 

53 data: Raw bytes from the characteristic read. 

54 ctx: Optional CharacteristicContext providing surrounding context (may be None). 

55 

56 Returns: 

57 ElectricCurrentRangeData with 'min' and 'max' current values in Amperes. 

58 

59 Raises: 

60 ValueError: If data is insufficient. 

61 

62 """ 

63 if len(data) < 4: 

64 raise ValueError("Electric current range data must be at least 4 bytes") 

65 

66 # Convert 2x uint16 (little endian) to current range in Amperes 

67 min_current_raw = DataParser.parse_int16(data, 0, signed=False) 

68 max_current_raw = DataParser.parse_int16(data, 2, signed=False) 

69 

70 return ElectricCurrentRangeData(min=min_current_raw * 0.01, max=max_current_raw * 0.01) 

71 

72 def _encode_value(self, data: ElectricCurrentRangeData) -> bytearray: 

73 """Encode electric current range value back to bytes. 

74 

75 Args: 

76 data: ElectricCurrentRangeData instance with 'min' and 'max' current values in Amperes 

77 

78 Returns: 

79 Encoded bytes representing the current range (2x uint16, 0.01 A resolution) 

80 

81 """ 

82 if not isinstance(data, ElectricCurrentRangeData): 

83 raise TypeError( 

84 f"Electric current range data must be an ElectricCurrentRangeData, got {type(data).__name__}" 

85 ) 

86 # Convert Amperes to raw values (multiply by 100 for 0.01 A resolution) 

87 min_current_raw = round(data.min * 100) 

88 max_current_raw = round(data.max * 100) 

89 

90 # Validate range for uint16 (0 to UINT16_MAX) 

91 for name, value in [("minimum", min_current_raw), ("maximum", max_current_raw)]: 

92 if not 0 <= value <= UINT16_MAX: 

93 raise ValueError(f"Current {name} value {value} exceeds uint16 range") 

94 

95 # Encode as 2 uint16 values (little endian) 

96 result = bytearray() 

97 result.extend(DataParser.encode_int16(min_current_raw, signed=False)) 

98 result.extend(DataParser.encode_int16(max_current_raw, signed=False)) 

99 

100 return result