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

47 statements  

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

1"""Battery Energy Status characteristic implementation. 

2 

3Implements the Battery Energy Status characteristic (0x2BF0) from the Battery 

4Service. An 8-bit flags field controls the presence of six optional medfloat16 

5(IEEE 11073 SFLOAT) fields. 

6 

7All flag bits use normal logic (1 = present, 0 = absent). 

8 

9References: 

10 Bluetooth SIG Battery Service 1.1 

11 org.bluetooth.characteristic.battery_energy_status (GSS YAML) 

12""" 

13 

14from __future__ import annotations 

15 

16from enum import IntFlag 

17 

18import msgspec 

19 

20from ..context import CharacteristicContext 

21from .base import BaseCharacteristic 

22from .utils import DataParser, IEEE11073Parser 

23 

24 

25class BatteryEnergyStatusFlags(IntFlag): 

26 """Battery Energy Status flags as per Bluetooth SIG specification.""" 

27 

28 EXTERNAL_SOURCE_POWER_PRESENT = 0x01 

29 PRESENT_VOLTAGE_PRESENT = 0x02 

30 AVAILABLE_ENERGY_PRESENT = 0x04 

31 AVAILABLE_BATTERY_CAPACITY_PRESENT = 0x08 

32 CHARGE_RATE_PRESENT = 0x10 

33 AVAILABLE_ENERGY_LAST_CHARGE_PRESENT = 0x20 

34 

35 

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

37 """Parsed data from Battery Energy Status characteristic. 

38 

39 Attributes: 

40 flags: Raw 8-bit flags field. 

41 external_source_power: Power consumed from external source (watts). 

42 None if absent. 

43 present_voltage: Terminal voltage of battery (volts). None if absent. 

44 available_energy: Available energy (kWh). None if absent. 

45 available_battery_capacity: Capacity at full charge (kWh). 

46 None if absent. 

47 charge_rate: Energy flow into battery (watts, negative = discharge). 

48 None if absent. 

49 available_energy_at_last_charge: Available energy at last charge (kWh). 

50 None if absent. 

51 

52 """ 

53 

54 flags: BatteryEnergyStatusFlags 

55 external_source_power: float | None = None 

56 present_voltage: float | None = None 

57 available_energy: float | None = None 

58 available_battery_capacity: float | None = None 

59 charge_rate: float | None = None 

60 available_energy_at_last_charge: float | None = None 

61 

62 

63class BatteryEnergyStatusCharacteristic(BaseCharacteristic[BatteryEnergyStatus]): 

64 """Battery Energy Status characteristic (0x2BF0). 

65 

66 Reports battery energy information including voltage, energy capacity, 

67 and charge/discharge rates. 

68 

69 Flag-bit assignments (from GSS YAML): 

70 Bit 0: External Source Power Present 

71 Bit 1: Present Voltage Present 

72 Bit 2: Available Energy Present 

73 Bit 3: Available Battery Capacity Present 

74 Bit 4: Charge Rate Present 

75 Bit 5: Available Energy at Last Charge Present 

76 Bits 6-7: Reserved for Future Use 

77 

78 All value fields are medfloat16 (IEEE 11073 SFLOAT, 2 bytes each). 

79 

80 """ 

81 

82 expected_type = BatteryEnergyStatus 

83 min_length: int = 1 # 1 byte flags only (all fields optional) 

84 allow_variable_length: bool = True 

85 

86 # Mapping: (flag_bit, attribute_name) in wire order 

87 _FIELD_MAP: tuple[tuple[BatteryEnergyStatusFlags, str], ...] = ( 

88 (BatteryEnergyStatusFlags.EXTERNAL_SOURCE_POWER_PRESENT, "external_source_power"), 

89 (BatteryEnergyStatusFlags.PRESENT_VOLTAGE_PRESENT, "present_voltage"), 

90 (BatteryEnergyStatusFlags.AVAILABLE_ENERGY_PRESENT, "available_energy"), 

91 (BatteryEnergyStatusFlags.AVAILABLE_BATTERY_CAPACITY_PRESENT, "available_battery_capacity"), 

92 (BatteryEnergyStatusFlags.CHARGE_RATE_PRESENT, "charge_rate"), 

93 (BatteryEnergyStatusFlags.AVAILABLE_ENERGY_LAST_CHARGE_PRESENT, "available_energy_at_last_charge"), 

94 ) 

95 

96 def _decode_value( 

97 self, 

98 data: bytearray, 

99 ctx: CharacteristicContext | None = None, 

100 *, 

101 validate: bool = True, 

102 ) -> BatteryEnergyStatus: 

103 """Parse Battery Energy Status from raw BLE bytes. 

104 

105 Args: 

106 data: Raw bytearray from BLE characteristic. 

107 ctx: Optional context (unused). 

108 validate: Whether to validate ranges. 

109 

110 Returns: 

111 BatteryEnergyStatus with all present fields populated. 

112 

113 """ 

114 flags = BatteryEnergyStatusFlags(DataParser.parse_int8(data, 0, signed=False)) 

115 offset = 1 

116 values: dict[str, float | None] = {} 

117 

118 for flag_bit, attr_name in self._FIELD_MAP: 

119 if flags & flag_bit: 

120 values[attr_name] = IEEE11073Parser.parse_sfloat(data, offset) 

121 offset += 2 

122 else: 

123 values[attr_name] = None 

124 

125 return BatteryEnergyStatus(flags=flags, **values) 

126 

127 def _encode_value(self, data: BatteryEnergyStatus) -> bytearray: 

128 """Encode BatteryEnergyStatus back to BLE bytes. 

129 

130 Args: 

131 data: BatteryEnergyStatus instance. 

132 

133 Returns: 

134 Encoded bytearray matching the BLE wire format. 

135 

136 """ 

137 flags = BatteryEnergyStatusFlags(0) 

138 

139 for flag_bit, attr_name in self._FIELD_MAP: 

140 if getattr(data, attr_name) is not None: 

141 flags |= flag_bit 

142 

143 result = DataParser.encode_int8(int(flags), signed=False) 

144 

145 for _flag_bit, attr_name in self._FIELD_MAP: 

146 value = getattr(data, attr_name) 

147 if value is not None: 

148 result.extend(IEEE11073Parser.encode_sfloat(value)) 

149 

150 return result