Coverage for src / bluetooth_sig / gatt / characteristics / cycling_power_feature.py: 100%

34 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-03-18 11:17 +0000

1"""Cycling Power Feature characteristic implementation.""" 

2 

3from __future__ import annotations 

4 

5from enum import IntFlag 

6 

7import msgspec 

8 

9from ..context import CharacteristicContext 

10from .base import BaseCharacteristic 

11from .utils import DataParser 

12 

13 

14class CyclingPowerFeatures(IntFlag): 

15 """Cycling Power Feature flags as per Bluetooth SIG specification.""" 

16 

17 PEDAL_POWER_BALANCE_SUPPORTED = 0x01 

18 ACCUMULATED_ENERGY_SUPPORTED = 0x02 

19 WHEEL_REVOLUTION_DATA_SUPPORTED = 0x04 

20 CRANK_REVOLUTION_DATA_SUPPORTED = 0x08 

21 

22 

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

24 """Parsed data from Cycling Power Feature characteristic.""" 

25 

26 features: CyclingPowerFeatures 

27 pedal_power_balance_supported: bool 

28 accumulated_energy_supported: bool 

29 wheel_revolution_data_supported: bool 

30 crank_revolution_data_supported: bool 

31 

32 

33class CyclingPowerFeatureCharacteristic(BaseCharacteristic[CyclingPowerFeatureData]): 

34 """Cycling Power Feature characteristic (0x2A65). 

35 

36 Used to expose the supported features of a cycling power sensor. 

37 Contains a 32-bit bitmask indicating supported measurement 

38 capabilities. 

39 """ 

40 

41 expected_length: int = 4 

42 min_length: int = 4 

43 

44 def _decode_value( 

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

46 ) -> CyclingPowerFeatureData: 

47 """Parse cycling power feature data. 

48 

49 Format: 32-bit feature bitmask (little endian). 

50 

51 Args: 

52 data: Raw bytearray from BLE characteristic. 

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

54 validate: Whether to validate ranges (default True) 

55 

56 Returns: 

57 CyclingPowerFeatureData containing parsed feature flags. 

58 

59 Raises: 

60 ValueError: If data format is invalid. 

61 

62 """ 

63 # Parse 32-bit unsigned integer (little endian) 

64 feature_mask: int = DataParser.parse_int32(data, 0, signed=False) 

65 

66 # Parse feature flags according to specification 

67 return CyclingPowerFeatureData( 

68 features=CyclingPowerFeatures(feature_mask), 

69 pedal_power_balance_supported=bool(feature_mask & CyclingPowerFeatures.PEDAL_POWER_BALANCE_SUPPORTED), 

70 accumulated_energy_supported=bool(feature_mask & CyclingPowerFeatures.ACCUMULATED_ENERGY_SUPPORTED), 

71 wheel_revolution_data_supported=bool(feature_mask & CyclingPowerFeatures.WHEEL_REVOLUTION_DATA_SUPPORTED), 

72 crank_revolution_data_supported=bool(feature_mask & CyclingPowerFeatures.CRANK_REVOLUTION_DATA_SUPPORTED), 

73 ) 

74 

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

76 """Encode cycling power feature value back to bytes. 

77 

78 Args: 

79 data: CyclingPowerFeatureData containing cycling power feature data 

80 

81 Returns: 

82 Encoded bytes representing the cycling power features (uint32) 

83 

84 """ 

85 # Reconstruct the features bitmap from individual flags 

86 features_bitmap = 0 

87 if data.pedal_power_balance_supported: 

88 features_bitmap |= CyclingPowerFeatures.PEDAL_POWER_BALANCE_SUPPORTED 

89 if data.accumulated_energy_supported: 

90 features_bitmap |= CyclingPowerFeatures.ACCUMULATED_ENERGY_SUPPORTED 

91 if data.wheel_revolution_data_supported: 

92 features_bitmap |= CyclingPowerFeatures.WHEEL_REVOLUTION_DATA_SUPPORTED 

93 if data.crank_revolution_data_supported: 

94 features_bitmap |= CyclingPowerFeatures.CRANK_REVOLUTION_DATA_SUPPORTED 

95 

96 return DataParser.encode_int32(features_bitmap, signed=False)