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
« prev ^ index » next coverage.py v7.13.5, created at 2026-03-18 11:17 +0000
1"""Battery Energy Status characteristic implementation.
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.
7All flag bits use normal logic (1 = present, 0 = absent).
9References:
10 Bluetooth SIG Battery Service 1.1
11 org.bluetooth.characteristic.battery_energy_status (GSS YAML)
12"""
14from __future__ import annotations
16from enum import IntFlag
18import msgspec
20from ..context import CharacteristicContext
21from .base import BaseCharacteristic
22from .utils import DataParser, IEEE11073Parser
25class BatteryEnergyStatusFlags(IntFlag):
26 """Battery Energy Status flags as per Bluetooth SIG specification."""
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
36class BatteryEnergyStatus(msgspec.Struct, frozen=True, kw_only=True): # pylint: disable=too-few-public-methods
37 """Parsed data from Battery Energy Status characteristic.
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.
52 """
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
63class BatteryEnergyStatusCharacteristic(BaseCharacteristic[BatteryEnergyStatus]):
64 """Battery Energy Status characteristic (0x2BF0).
66 Reports battery energy information including voltage, energy capacity,
67 and charge/discharge rates.
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
78 All value fields are medfloat16 (IEEE 11073 SFLOAT, 2 bytes each).
80 """
82 expected_type = BatteryEnergyStatus
83 min_length: int = 1 # 1 byte flags only (all fields optional)
84 allow_variable_length: bool = True
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 )
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.
105 Args:
106 data: Raw bytearray from BLE characteristic.
107 ctx: Optional context (unused).
108 validate: Whether to validate ranges.
110 Returns:
111 BatteryEnergyStatus with all present fields populated.
113 """
114 flags = BatteryEnergyStatusFlags(DataParser.parse_int8(data, 0, signed=False))
115 offset = 1
116 values: dict[str, float | None] = {}
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
125 return BatteryEnergyStatus(flags=flags, **values)
127 def _encode_value(self, data: BatteryEnergyStatus) -> bytearray:
128 """Encode BatteryEnergyStatus back to BLE bytes.
130 Args:
131 data: BatteryEnergyStatus instance.
133 Returns:
134 Encoded bytearray matching the BLE wire format.
136 """
137 flags = BatteryEnergyStatusFlags(0)
139 for flag_bit, attr_name in self._FIELD_MAP:
140 if getattr(data, attr_name) is not None:
141 flags |= flag_bit
143 result = DataParser.encode_int8(int(flags), signed=False)
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))
150 return result