Coverage for src/bluetooth_sig/gatt/characteristics/supported_power_range.py: 83%

36 statements  

« prev     ^ index     » next       coverage.py v7.11.0, created at 2025-10-30 00:10 +0000

1"""Supported Power Range characteristic implementation.""" 

2 

3from __future__ import annotations 

4 

5import msgspec 

6 

7from ...types.gatt_enums import ValueType 

8from ..constants import SINT16_MAX, SINT16_MIN 

9from ..context import CharacteristicContext 

10from .base import BaseCharacteristic 

11 

12 

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

14 """Data class for supported power range.""" 

15 

16 minimum: int # Minimum power in Watts 

17 maximum: int # Maximum power in Watts 

18 

19 def __post_init__(self) -> None: 

20 """Validate power range data.""" 

21 if self.minimum > self.maximum: 

22 raise ValueError(f"Minimum power {self.minimum} W cannot be greater than maximum {self.maximum} W") 

23 

24 # Validate range for sint16 (SINT16_MIN to SINT16_MAX) 

25 if not SINT16_MIN <= self.minimum <= SINT16_MAX: 

26 raise ValueError(f"Minimum power {self.minimum} W is outside valid range (SINT16_MIN to SINT16_MAX W)") 

27 if not SINT16_MIN <= self.maximum <= SINT16_MAX: 

28 raise ValueError(f"Maximum power {self.maximum} W is outside valid range (SINT16_MIN to SINT16_MAX W)") 

29 

30 

31class SupportedPowerRangeCharacteristic(BaseCharacteristic): 

32 """Supported Power Range characteristic (0x2AD8). 

33 

34 org.bluetooth.characteristic.supported_power_range 

35 

36 Supported Power Range characteristic. 

37 

38 Specifies minimum and maximum power values for power capability 

39 specification. 

40 """ 

41 

42 _characteristic_name: str = "Supported Power Range" 

43 # Override since decode_value returns structured SupportedPowerRangeData 

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

45 

46 def decode_value(self, data: bytearray, ctx: CharacteristicContext | None = None) -> SupportedPowerRangeData: 

47 """Parse supported power range data (2x sint16 in watts). 

48 

49 Args: 

50 data: Raw bytes from the characteristic read. 

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

52 

53 Returns: 

54 SupportedPowerRangeData with minimum and maximum power values in Watts. 

55 

56 Raises: 

57 ValueError: If data is insufficient. 

58 

59 """ 

60 if len(data) < 4: 

61 raise ValueError("Supported power range data must be at least 4 bytes") 

62 

63 # Convert 2x sint16 (little endian) to power range in Watts 

64 min_power_raw = int.from_bytes(data[:2], byteorder="little", signed=True) 

65 max_power_raw = int.from_bytes(data[2:4], byteorder="little", signed=True) 

66 

67 return SupportedPowerRangeData(minimum=min_power_raw, maximum=max_power_raw) 

68 

69 def encode_value(self, data: SupportedPowerRangeData) -> bytearray: 

70 """Encode supported power range value back to bytes. 

71 

72 Args: 

73 data: SupportedPowerRangeData instance with 'minimum' and 'maximum' power values in Watts 

74 

75 Returns: 

76 Encoded bytes representing the power range (2x sint16) 

77 

78 """ 

79 if not isinstance(data, SupportedPowerRangeData): 

80 raise TypeError(f"Supported power range data must be a SupportedPowerRangeData, got {type(data).__name__}") 

81 

82 # Validate range for sint16 (SINT16_MIN to SINT16_MAX) 

83 if not SINT16_MIN <= data.minimum <= SINT16_MAX: 

84 raise ValueError(f"Minimum power {data.minimum} exceeds sint16 range") 

85 if not SINT16_MIN <= data.maximum <= SINT16_MAX: 

86 raise ValueError(f"Maximum power {data.maximum} exceeds sint16 range") 

87 # Encode as 2 sint16 values (little endian) 

88 result = bytearray() 

89 result.extend(data.minimum.to_bytes(2, byteorder="little", signed=True)) 

90 result.extend(data.maximum.to_bytes(2, byteorder="little", signed=True)) 

91 

92 return result