Coverage for src/bluetooth_sig/gatt/characteristics/voltage_specification.py: 86%

37 statements  

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

1"""Voltage Specification 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 

11 

12 

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

14 """Data class for voltage specification.""" 

15 

16 minimum: float # Minimum voltage in Volts 

17 maximum: float # Maximum voltage in Volts 

18 

19 def __post_init__(self) -> None: 

20 """Validate voltage specification data.""" 

21 if self.minimum > self.maximum: 

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

23 

24 # Validate range for uint16 with 1/64 V resolution (0 to ~1024 V) 

25 max_voltage_value = UINT16_MAX / 64.0 # ~1024 V 

26 if not 0.0 <= self.minimum <= max_voltage_value: 

27 raise ValueError( 

28 f"Minimum voltage {self.minimum} V is outside valid range (0.0 to {max_voltage_value:.2f} V)" 

29 ) 

30 if not 0.0 <= self.maximum <= max_voltage_value: 

31 raise ValueError( 

32 f"Maximum voltage {self.maximum} V is outside valid range (0.0 to {max_voltage_value:.2f} V)" 

33 ) 

34 

35 

36class VoltageSpecificationCharacteristic(BaseCharacteristic): 

37 """Voltage Specification characteristic (0x2B19). 

38 

39 org.bluetooth.characteristic.voltage_specification 

40 

41 Voltage Specification characteristic. 

42 

43 Specifies minimum and maximum voltage values for electrical 

44 specifications. 

45 """ 

46 

47 # Override since decode_value returns structured VoltageSpecificationData 

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

49 

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

51 """Parse voltage specification data (2x uint16 in units of 1/64 V). 

52 

53 Args: 

54 data: Raw bytes from the characteristic read. 

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

56 

57 Returns: 

58 VoltageSpecificationData with 'minimum' and 'maximum' voltage specification values in Volts. 

59 

60 Raises: 

61 ValueError: If data is insufficient. 

62 

63 """ 

64 if len(data) < 4: 

65 raise ValueError("Voltage specification data must be at least 4 bytes") 

66 

67 # Convert 2x uint16 (little endian) to voltage specification in Volts 

68 min_voltage_raw = int.from_bytes(data[:2], byteorder="little", signed=False) 

69 max_voltage_raw = int.from_bytes(data[2:4], byteorder="little", signed=False) 

70 

71 return VoltageSpecificationData(minimum=min_voltage_raw / 64.0, maximum=max_voltage_raw / 64.0) 

72 

73 def encode_value(self, data: VoltageSpecificationData) -> bytearray: 

74 """Encode voltage specification value back to bytes. 

75 

76 Args: 

77 data: VoltageSpecificationData instance with 'minimum' and 'maximum' voltage values in Volts 

78 

79 Returns: 

80 Encoded bytes representing the voltage specification (2x uint16, 1/64 V resolution) 

81 

82 """ 

83 if not isinstance(data, VoltageSpecificationData): 

84 raise TypeError(f"Voltage specification data must be a VoltageSpecificationData, got {type(data).__name__}") 

85 # Convert Volts to raw values (multiply by 64 for 1/64 V resolution) 

86 min_voltage_raw = round(data.minimum * 64) 

87 max_voltage_raw = round(data.maximum * 64) 

88 

89 # Validate range for uint16 (0 to UINT16_MAX) 

90 # pylint: disable=duplicate-code 

91 # NOTE: This uint16 validation and encoding pattern is shared with VoltageStatisticsCharacteristic. 

92 # Both characteristics encode voltage values using the same 1/64V resolution and uint16 little-endian format 

93 # per Bluetooth SIG spec. Consolidation not practical as each has different field structures. 

94 for name, value in [("minimum", min_voltage_raw), ("maximum", max_voltage_raw)]: 

95 if not 0 <= value <= UINT16_MAX: 

96 raise ValueError(f"Voltage {name} value {value} exceeds uint16 range") 

97 

98 # Encode as 2 uint16 values (little endian) 

99 result = bytearray() 

100 result.extend(min_voltage_raw.to_bytes(2, byteorder="little", signed=False)) 

101 result.extend(max_voltage_raw.to_bytes(2, byteorder="little", signed=False)) 

102 

103 return result