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

40 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-04-03 16:41 +0000

1"""Voltage Specification characteristic implementation.""" 

2 

3from __future__ import annotations 

4 

5import msgspec 

6 

7from ..constants import UINT16_MAX 

8from ..context import CharacteristicContext 

9from .base import BaseCharacteristic 

10from .utils import DataParser 

11 

12_VOLTAGE_RESOLUTION = 1 / 64.0 # 1/64 V per raw unit 

13_MAX_VOLTAGE = UINT16_MAX * _VOLTAGE_RESOLUTION # ~1023.98 V 

14 

15 

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

17 """Data class for voltage specification. 

18 

19 Three voltage values (1/64 V resolution): Minimum, Typical, Maximum. 

20 """ 

21 

22 minimum: float # Minimum voltage in Volts 

23 typical: float # Typical voltage in Volts 

24 maximum: float # Maximum voltage in Volts 

25 

26 def __post_init__(self) -> None: 

27 """Validate voltage specification data.""" 

28 if self.minimum > self.maximum: 

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

30 

31 for name, voltage in [ 

32 ("minimum", self.minimum), 

33 ("typical", self.typical), 

34 ("maximum", self.maximum), 

35 ]: 

36 if not 0.0 <= voltage <= _MAX_VOLTAGE: 

37 raise ValueError( 

38 f"{name.capitalize()} voltage {voltage} V is outside valid range (0.0 to {_MAX_VOLTAGE:.2f} V)" 

39 ) 

40 

41 

42class VoltageSpecificationCharacteristic(BaseCharacteristic[VoltageSpecificationData]): 

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

44 

45 org.bluetooth.characteristic.voltage_specification 

46 

47 Specifies minimum, typical, and maximum voltage values (uint16, 1/64 V resolution). 

48 """ 

49 

50 expected_length: int = 6 # 3x uint16 

51 min_length: int = 6 

52 

53 def _decode_value( 

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

55 ) -> VoltageSpecificationData: 

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

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

58 typical_voltage_raw = DataParser.parse_int16(data, 2, signed=False) 

59 max_voltage_raw = DataParser.parse_int16(data, 4, signed=False) 

60 

61 return VoltageSpecificationData( 

62 minimum=min_voltage_raw / 64.0, 

63 typical=typical_voltage_raw / 64.0, 

64 maximum=max_voltage_raw / 64.0, 

65 ) 

66 

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

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

69 if not isinstance(data, VoltageSpecificationData): 

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

71 

72 min_voltage_raw = round(data.minimum * 64) 

73 typical_voltage_raw = round(data.typical * 64) 

74 max_voltage_raw = round(data.maximum * 64) 

75 

76 # pylint: disable=duplicate-code 

77 for name, value in [ 

78 ("minimum", min_voltage_raw), 

79 ("typical", typical_voltage_raw), 

80 ("maximum", max_voltage_raw), 

81 ]: 

82 if not 0 <= value <= UINT16_MAX: 

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

84 

85 result = bytearray() 

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

87 result.extend(DataParser.encode_int16(typical_voltage_raw, signed=False)) 

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

89 

90 return result