Coverage for src / bluetooth_sig / gatt / characteristics / battery_level_status.py: 100%
121 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 Level Status characteristic implementation."""
3from __future__ import annotations
5from enum import IntFlag
7import msgspec
9from bluetooth_sig.types.battery import (
10 BatteryChargeLevel,
11 BatteryChargeState,
12 BatteryChargingType,
13 PowerConnectionState,
14 ServiceRequiredState,
15)
17from ..context import CharacteristicContext
18from .base import BaseCharacteristic
19from .utils.bit_field_utils import BitFieldUtils
20from .utils.data_parser import DataParser
23class BatteryLevelStatusFlags(IntFlag):
24 """Battery Level Status flags."""
26 IDENTIFIER_PRESENT = 1 << 0
27 BATTERY_LEVEL_PRESENT = 1 << 1
28 ADDITIONAL_STATUS_PRESENT = 1 << 2
31class BatteryLevelStatus(msgspec.Struct):
32 """Battery Level Status data structure."""
34 # Flags as IntFlag
35 flags: BatteryLevelStatusFlags
37 # Power State
38 battery_present: bool
39 wired_external_power_connected: PowerConnectionState
40 wireless_external_power_connected: PowerConnectionState
41 battery_charge_state: BatteryChargeState
42 battery_charge_level: BatteryChargeLevel
43 charging_type: BatteryChargingType
44 charging_fault_battery: bool
45 charging_fault_external_power: bool
46 charging_fault_other: bool
48 # Optional fields
49 identifier: int | None = None # uint16
50 battery_level: int | None = None # uint8
51 service_required: ServiceRequiredState | None = None
52 battery_fault: bool | None = None
55class BatteryLevelStatusCharacteristic(BaseCharacteristic[BatteryLevelStatus]):
56 """Battery Level Status characteristic (0x2BED).
58 org.bluetooth.characteristic.battery_level_status
59 """
61 _manual_unit: str | None = None # Bitfield, no units
63 # Bit field constants for power state
64 BIT_START_BATTERY_PRESENT = 0
65 BIT_WIDTH_BATTERY_PRESENT = 1
66 BIT_START_WIRED_POWER = 1
67 BIT_WIDTH_WIRED_POWER = 2
68 BIT_START_WIRELESS_POWER = 3
69 BIT_WIDTH_WIRELESS_POWER = 2
70 BIT_START_CHARGE_STATE = 5
71 BIT_WIDTH_CHARGE_STATE = 2
72 BIT_START_CHARGE_LEVEL = 7
73 BIT_WIDTH_CHARGE_LEVEL = 2
74 BIT_START_CHARGING_TYPE = 9
75 BIT_WIDTH_CHARGING_TYPE = 3
76 BIT_START_FAULT_BATTERY = 12
77 BIT_WIDTH_FAULT_BATTERY = 1
78 BIT_START_FAULT_EXTERNAL = 13
79 BIT_WIDTH_FAULT_EXTERNAL = 1
80 BIT_START_FAULT_OTHER = 14
81 BIT_WIDTH_FAULT_OTHER = 1
83 # Bit field constants for additional status
84 BIT_START_SERVICE_REQUIRED = 0
85 BIT_WIDTH_SERVICE_REQUIRED = 2
86 BIT_START_BATTERY_FAULT = 2
87 BIT_WIDTH_BATTERY_FAULT = 1
89 allow_variable_length = True
90 min_length = 3 # flags (1) + power_state (2)
91 max_length = 7 # + identifier (2) + battery_level (1) + additional_status (1)
93 def _decode_value(
94 self, data: bytearray, ctx: CharacteristicContext | None = None, *, validate: bool = True
95 ) -> BatteryLevelStatus: # pylint: disable=too-many-locals # Battery status with many conditional fields
96 """Decode the battery level status value."""
97 offset = 0
99 # Parse flags (1 byte)
100 flags = BatteryLevelStatusFlags(DataParser.parse_int8(data, offset, signed=False))
101 offset += 1
103 # Parse power state (2 bytes)
104 power_state = DataParser.parse_int16(data, offset, signed=False)
105 offset += 2
107 battery_present = BitFieldUtils.test_bit(power_state, self.BIT_START_BATTERY_PRESENT)
108 wired_external_power_connected = PowerConnectionState(
109 BitFieldUtils.extract_bit_field(power_state, self.BIT_START_WIRED_POWER, self.BIT_WIDTH_WIRED_POWER)
110 )
111 wireless_external_power_connected = PowerConnectionState(
112 BitFieldUtils.extract_bit_field(power_state, self.BIT_START_WIRELESS_POWER, self.BIT_WIDTH_WIRELESS_POWER)
113 )
114 battery_charge_state = BatteryChargeState(
115 BitFieldUtils.extract_bit_field(power_state, self.BIT_START_CHARGE_STATE, self.BIT_WIDTH_CHARGE_STATE)
116 )
117 battery_charge_level = BatteryChargeLevel(
118 BitFieldUtils.extract_bit_field(power_state, self.BIT_START_CHARGE_LEVEL, self.BIT_WIDTH_CHARGE_LEVEL)
119 )
120 charging_type = BatteryChargingType(
121 BitFieldUtils.extract_bit_field(power_state, self.BIT_START_CHARGING_TYPE, self.BIT_WIDTH_CHARGING_TYPE)
122 )
123 charging_fault_battery = BitFieldUtils.test_bit(power_state, self.BIT_START_FAULT_BATTERY)
124 charging_fault_external_power = BitFieldUtils.test_bit(power_state, self.BIT_START_FAULT_EXTERNAL)
125 charging_fault_other = BitFieldUtils.test_bit(power_state, self.BIT_START_FAULT_OTHER)
127 # Optional identifier
128 identifier = None
129 if flags & BatteryLevelStatusFlags.IDENTIFIER_PRESENT:
130 identifier = DataParser.parse_int16(data, offset, signed=False)
131 offset += 2
133 # Optional battery level
134 battery_level = None
135 if flags & BatteryLevelStatusFlags.BATTERY_LEVEL_PRESENT:
136 battery_level = DataParser.parse_int8(data, offset, signed=False)
137 offset += 1
139 # Optional additional status
140 service_required = None
141 battery_fault = None
142 if flags & BatteryLevelStatusFlags.ADDITIONAL_STATUS_PRESENT:
143 additional_status = DataParser.parse_int8(data, offset, signed=False)
144 service_required = ServiceRequiredState(
145 BitFieldUtils.extract_bit_field(
146 additional_status, self.BIT_START_SERVICE_REQUIRED, self.BIT_WIDTH_SERVICE_REQUIRED
147 )
148 )
149 battery_fault = BitFieldUtils.test_bit(additional_status, self.BIT_START_BATTERY_FAULT)
151 return BatteryLevelStatus(
152 flags=flags,
153 battery_present=battery_present,
154 wired_external_power_connected=wired_external_power_connected,
155 wireless_external_power_connected=wireless_external_power_connected,
156 battery_charge_state=battery_charge_state,
157 battery_charge_level=battery_charge_level,
158 charging_type=charging_type,
159 charging_fault_battery=charging_fault_battery,
160 charging_fault_external_power=charging_fault_external_power,
161 charging_fault_other=charging_fault_other,
162 identifier=identifier,
163 battery_level=battery_level,
164 service_required=service_required,
165 battery_fault=battery_fault,
166 )
168 def _encode_value(self, data: BatteryLevelStatus) -> bytearray:
169 """Encode the battery level status value."""
170 result = bytearray()
172 # Encode flags
173 flags = BatteryLevelStatusFlags(0)
174 if data.identifier is not None:
175 flags |= BatteryLevelStatusFlags.IDENTIFIER_PRESENT
176 if data.battery_level is not None:
177 flags |= BatteryLevelStatusFlags.BATTERY_LEVEL_PRESENT
178 if data.service_required is not None or data.battery_fault is not None:
179 flags |= BatteryLevelStatusFlags.ADDITIONAL_STATUS_PRESENT
180 result.extend(DataParser.encode_int8(flags.value, signed=False))
182 # Encode power state
183 power_state = 0
184 if data.battery_present:
185 power_state = BitFieldUtils.set_bit(power_state, self.BIT_START_BATTERY_PRESENT)
186 power_state = BitFieldUtils.set_bit_field(
187 power_state,
188 data.wired_external_power_connected.value,
189 self.BIT_START_WIRED_POWER,
190 self.BIT_WIDTH_WIRED_POWER,
191 )
192 power_state = BitFieldUtils.set_bit_field(
193 power_state,
194 data.wireless_external_power_connected.value,
195 self.BIT_START_WIRELESS_POWER,
196 self.BIT_WIDTH_WIRELESS_POWER,
197 )
198 power_state = BitFieldUtils.set_bit_field(
199 power_state, data.battery_charge_state.value, self.BIT_START_CHARGE_STATE, self.BIT_WIDTH_CHARGE_STATE
200 )
201 power_state = BitFieldUtils.set_bit_field(
202 power_state, data.battery_charge_level.value, self.BIT_START_CHARGE_LEVEL, self.BIT_WIDTH_CHARGE_LEVEL
203 )
204 power_state = BitFieldUtils.set_bit_field(
205 power_state, data.charging_type.value, self.BIT_START_CHARGING_TYPE, self.BIT_WIDTH_CHARGING_TYPE
206 )
207 if data.charging_fault_battery:
208 power_state = BitFieldUtils.set_bit(power_state, self.BIT_START_FAULT_BATTERY)
209 if data.charging_fault_external_power:
210 power_state = BitFieldUtils.set_bit(power_state, self.BIT_START_FAULT_EXTERNAL)
211 if data.charging_fault_other:
212 power_state = BitFieldUtils.set_bit(power_state, self.BIT_START_FAULT_OTHER)
213 result.extend(DataParser.encode_int16(power_state, signed=False))
215 # Optional identifier
216 if data.identifier is not None:
217 result.extend(DataParser.encode_int16(data.identifier, signed=False))
219 # Optional battery level
220 if data.battery_level is not None:
221 result.extend(DataParser.encode_int8(data.battery_level, signed=False))
223 # Optional additional status
224 if data.service_required is not None or data.battery_fault is not None:
225 additional_status = 0
226 if data.service_required is not None:
227 additional_status = BitFieldUtils.set_bit_field(
228 additional_status,
229 data.service_required.value,
230 self.BIT_START_SERVICE_REQUIRED,
231 self.BIT_WIDTH_SERVICE_REQUIRED,
232 )
233 if data.battery_fault:
234 additional_status = BitFieldUtils.set_bit(additional_status, self.BIT_START_BATTERY_FAULT)
235 result.extend(DataParser.encode_int8(additional_status, signed=False))
237 return result