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
« prev ^ index » next coverage.py v7.13.5, created at 2026-03-18 11:17 +0000
1"""Cycling Power Feature characteristic implementation."""
3from __future__ import annotations
5from enum import IntFlag
7import msgspec
9from ..context import CharacteristicContext
10from .base import BaseCharacteristic
11from .utils import DataParser
14class CyclingPowerFeatures(IntFlag):
15 """Cycling Power Feature flags as per Bluetooth SIG specification."""
17 PEDAL_POWER_BALANCE_SUPPORTED = 0x01
18 ACCUMULATED_ENERGY_SUPPORTED = 0x02
19 WHEEL_REVOLUTION_DATA_SUPPORTED = 0x04
20 CRANK_REVOLUTION_DATA_SUPPORTED = 0x08
23class CyclingPowerFeatureData(msgspec.Struct, frozen=True, kw_only=True): # pylint: disable=too-few-public-methods
24 """Parsed data from Cycling Power Feature characteristic."""
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
33class CyclingPowerFeatureCharacteristic(BaseCharacteristic[CyclingPowerFeatureData]):
34 """Cycling Power Feature characteristic (0x2A65).
36 Used to expose the supported features of a cycling power sensor.
37 Contains a 32-bit bitmask indicating supported measurement
38 capabilities.
39 """
41 expected_length: int = 4
42 min_length: int = 4
44 def _decode_value(
45 self, data: bytearray, ctx: CharacteristicContext | None = None, *, validate: bool = True
46 ) -> CyclingPowerFeatureData:
47 """Parse cycling power feature data.
49 Format: 32-bit feature bitmask (little endian).
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)
56 Returns:
57 CyclingPowerFeatureData containing parsed feature flags.
59 Raises:
60 ValueError: If data format is invalid.
62 """
63 # Parse 32-bit unsigned integer (little endian)
64 feature_mask: int = DataParser.parse_int32(data, 0, signed=False)
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 )
75 def _encode_value(self, data: CyclingPowerFeatureData) -> bytearray:
76 """Encode cycling power feature value back to bytes.
78 Args:
79 data: CyclingPowerFeatureData containing cycling power feature data
81 Returns:
82 Encoded bytes representing the cycling power features (uint32)
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
96 return DataParser.encode_int32(features_bitmap, signed=False)