Coverage for src / bluetooth_sig / gatt / characteristics / power_specification.py: 98%

42 statements  

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

1"""Power Specification characteristic implementation.""" 

2 

3from __future__ import annotations 

4 

5from ..context import CharacteristicContext 

6from .base import BaseCharacteristic 

7from .utils.data_parser import DataParser 

8 

9# Special value constants for Power Specification characteristic 

10VALUE_NOT_VALID = 0xFFFFFE # Indicates value is not valid 

11VALUE_UNKNOWN = 0xFFFFFF # Indicates value is not known 

12 

13 

14class PowerSpecificationData: 

15 """Data class for Power Specification characteristic.""" 

16 

17 def __init__( 

18 self, 

19 minimum: float | None, 

20 typical: float | None, 

21 maximum: float | None, 

22 ) -> None: 

23 """Initialize Power Specification data. 

24 

25 Args: 

26 minimum: Minimum power value in watts, or None if not valid/known 

27 typical: Typical power value in watts, or None if not valid/known 

28 maximum: Maximum power value in watts, or None if not valid/known 

29 """ 

30 self.minimum = minimum 

31 self.typical = typical 

32 self.maximum = maximum 

33 

34 def __eq__(self, other: object) -> bool: 

35 """Check equality with another PowerSpecificationData.""" 

36 if not isinstance(other, PowerSpecificationData): 

37 return NotImplemented 

38 return self.minimum == other.minimum and self.typical == other.typical and self.maximum == other.maximum 

39 

40 def __repr__(self) -> str: 

41 """Return string representation.""" 

42 return f"PowerSpecificationData(minimum={self.minimum}, typical={self.typical}, maximum={self.maximum})" 

43 

44 

45class PowerSpecificationCharacteristic(BaseCharacteristic[PowerSpecificationData]): 

46 """Power Specification characteristic (0x2B06). 

47 

48 org.bluetooth.characteristic.power_specification 

49 

50 The Power Specification characteristic is used to represent a specification of power values. 

51 """ 

52 

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

54 """Decode the power specification values.""" 

55 # Parse three uint24 values (little-endian) 

56 minimum_raw = DataParser.parse_int24(data, 0, signed=False) 

57 typical_raw = DataParser.parse_int24(data, 3, signed=False) 

58 maximum_raw = DataParser.parse_int24(data, 6, signed=False) 

59 

60 # Convert to float values with 0.1 W resolution, handling special values 

61 def _convert_value(raw: int) -> float | None: 

62 if raw == VALUE_NOT_VALID: 

63 return None # Value is not valid 

64 if raw == VALUE_UNKNOWN: 

65 return None # Value is not known 

66 return raw * 0.1 # Resolution 0.1 W 

67 

68 return PowerSpecificationData( 

69 minimum=_convert_value(minimum_raw), 

70 typical=_convert_value(typical_raw), 

71 maximum=_convert_value(maximum_raw), 

72 ) 

73 

74 def _encode_value(self, data: PowerSpecificationData) -> bytearray: # noqa: D202 

75 """Encode the power specification values.""" 

76 

77 # Convert float values to uint24 with 0.1 W resolution, handling special values 

78 def _convert_to_raw(value: float | None) -> int: 

79 # NOTE: D202 disabled on method - blank line required by black formatter for nested function 

80 if value is None: 

81 return VALUE_UNKNOWN # Use "not known" as default for None 

82 return round(value / 0.1) 

83 

84 minimum_raw = _convert_to_raw(data.minimum) 

85 typical_raw = _convert_to_raw(data.typical) 

86 maximum_raw = _convert_to_raw(data.maximum) 

87 

88 # Encode three uint24 values (little-endian) 

89 result = bytearray() 

90 result.extend(DataParser.encode_int24(minimum_raw, signed=False)) 

91 result.extend(DataParser.encode_int24(typical_raw, signed=False)) 

92 result.extend(DataParser.encode_int24(maximum_raw, signed=False)) 

93 return result