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

39 statements  

« prev     ^ index     » next       coverage.py v7.13.1, created at 2026-01-11 20:14 +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 

11from .utils import DataParser 

12 

13 

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

15 """Data class for voltage specification.""" 

16 

17 minimum: float # Minimum voltage in Volts 

18 maximum: float # Maximum voltage in Volts 

19 

20 def __post_init__(self) -> None: 

21 """Validate voltage specification data.""" 

22 if self.minimum > self.maximum: 

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

24 

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

26 max_voltage_value = UINT16_MAX / 64.0 # ~1024 V 

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

28 raise ValueError( 

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

30 ) 

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

32 raise ValueError( 

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

34 ) 

35 

36 

37class VoltageSpecificationCharacteristic(BaseCharacteristic[VoltageSpecificationData]): 

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

39 

40 org.bluetooth.characteristic.voltage_specification 

41 

42 Voltage Specification characteristic. 

43 

44 Specifies minimum and maximum voltage values for electrical 

45 specifications. 

46 """ 

47 

48 min_length = 4 

49 # Override since decode_value returns structured VoltageSpecificationData 

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

51 

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

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

54 

55 Args: 

56 data: Raw bytes from the characteristic read. 

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

58 

59 Returns: 

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

61 

62 Raises: 

63 ValueError: If data is insufficient. 

64 

65 """ 

66 if len(data) < 4: 

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

68 

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

70 min_voltage_raw = DataParser.parse_int16(data, 0, signed=False) 

71 max_voltage_raw = DataParser.parse_int16(data, 2, signed=False) 

72 

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

74 

75 def _encode_value(self, data: VoltageSpecificationData) -> bytearray: 

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

77 

78 Args: 

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

80 

81 Returns: 

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

83 

84 """ 

85 if not isinstance(data, VoltageSpecificationData): 

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

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

88 min_voltage_raw = round(data.minimum * 64) 

89 max_voltage_raw = round(data.maximum * 64) 

90 

91 # Validate range for uint16 (0 to UINT16_MAX) 

92 # pylint: disable=duplicate-code 

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

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

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

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

97 if not 0 <= value <= UINT16_MAX: 

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

99 

100 # Encode as 2 uint16 values (little endian) 

101 result = bytearray() 

102 result.extend(DataParser.encode_int16(min_voltage_raw, signed=False)) 

103 result.extend(DataParser.encode_int16(max_voltage_raw, signed=False)) 

104 

105 return result