Coverage for src / bluetooth_sig / gatt / characteristics / battery_level_status.py: 100%
121 statements
« prev ^ index » next coverage.py v7.13.1, created at 2026-01-11 20:14 +0000
« prev ^ index » next coverage.py v7.13.1, created at 2026-01-11 20:14 +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(self, data: bytearray, ctx: CharacteristicContext | None = None) -> BatteryLevelStatus: # pylint: disable=too-many-locals
94 """Decode the battery level status value."""
95 offset = 0
97 # Parse flags (1 byte)
98 flags = BatteryLevelStatusFlags(DataParser.parse_int8(data, offset, signed=False))
99 offset += 1
101 # Parse power state (2 bytes)
102 power_state = DataParser.parse_int16(data, offset, signed=False)
103 offset += 2
105 battery_present = BitFieldUtils.test_bit(power_state, self.BIT_START_BATTERY_PRESENT)
106 wired_external_power_connected = PowerConnectionState(
107 BitFieldUtils.extract_bit_field(power_state, self.BIT_START_WIRED_POWER, self.BIT_WIDTH_WIRED_POWER)
108 )
109 wireless_external_power_connected = PowerConnectionState(
110 BitFieldUtils.extract_bit_field(power_state, self.BIT_START_WIRELESS_POWER, self.BIT_WIDTH_WIRELESS_POWER)
111 )
112 battery_charge_state = BatteryChargeState(
113 BitFieldUtils.extract_bit_field(power_state, self.BIT_START_CHARGE_STATE, self.BIT_WIDTH_CHARGE_STATE)
114 )
115 battery_charge_level = BatteryChargeLevel(
116 BitFieldUtils.extract_bit_field(power_state, self.BIT_START_CHARGE_LEVEL, self.BIT_WIDTH_CHARGE_LEVEL)
117 )
118 charging_type = BatteryChargingType(
119 BitFieldUtils.extract_bit_field(power_state, self.BIT_START_CHARGING_TYPE, self.BIT_WIDTH_CHARGING_TYPE)
120 )
121 charging_fault_battery = BitFieldUtils.test_bit(power_state, self.BIT_START_FAULT_BATTERY)
122 charging_fault_external_power = BitFieldUtils.test_bit(power_state, self.BIT_START_FAULT_EXTERNAL)
123 charging_fault_other = BitFieldUtils.test_bit(power_state, self.BIT_START_FAULT_OTHER)
125 # Optional identifier
126 identifier = None
127 if flags & BatteryLevelStatusFlags.IDENTIFIER_PRESENT:
128 identifier = DataParser.parse_int16(data, offset, signed=False)
129 offset += 2
131 # Optional battery level
132 battery_level = None
133 if flags & BatteryLevelStatusFlags.BATTERY_LEVEL_PRESENT:
134 battery_level = DataParser.parse_int8(data, offset, signed=False)
135 offset += 1
137 # Optional additional status
138 service_required = None
139 battery_fault = None
140 if flags & BatteryLevelStatusFlags.ADDITIONAL_STATUS_PRESENT:
141 additional_status = DataParser.parse_int8(data, offset, signed=False)
142 service_required = ServiceRequiredState(
143 BitFieldUtils.extract_bit_field(
144 additional_status, self.BIT_START_SERVICE_REQUIRED, self.BIT_WIDTH_SERVICE_REQUIRED
145 )
146 )
147 battery_fault = BitFieldUtils.test_bit(additional_status, self.BIT_START_BATTERY_FAULT)
149 return BatteryLevelStatus(
150 flags=flags,
151 battery_present=battery_present,
152 wired_external_power_connected=wired_external_power_connected,
153 wireless_external_power_connected=wireless_external_power_connected,
154 battery_charge_state=battery_charge_state,
155 battery_charge_level=battery_charge_level,
156 charging_type=charging_type,
157 charging_fault_battery=charging_fault_battery,
158 charging_fault_external_power=charging_fault_external_power,
159 charging_fault_other=charging_fault_other,
160 identifier=identifier,
161 battery_level=battery_level,
162 service_required=service_required,
163 battery_fault=battery_fault,
164 )
166 def _encode_value(self, data: BatteryLevelStatus) -> bytearray:
167 """Encode the battery level status value."""
168 result = bytearray()
170 # Encode flags
171 flags = BatteryLevelStatusFlags(0)
172 if data.identifier is not None:
173 flags |= BatteryLevelStatusFlags.IDENTIFIER_PRESENT
174 if data.battery_level is not None:
175 flags |= BatteryLevelStatusFlags.BATTERY_LEVEL_PRESENT
176 if data.service_required is not None or data.battery_fault is not None:
177 flags |= BatteryLevelStatusFlags.ADDITIONAL_STATUS_PRESENT
178 result.extend(DataParser.encode_int8(flags.value, signed=False))
180 # Encode power state
181 power_state = 0
182 if data.battery_present:
183 power_state = BitFieldUtils.set_bit(power_state, self.BIT_START_BATTERY_PRESENT)
184 power_state = BitFieldUtils.set_bit_field(
185 power_state,
186 data.wired_external_power_connected.value,
187 self.BIT_START_WIRED_POWER,
188 self.BIT_WIDTH_WIRED_POWER,
189 )
190 power_state = BitFieldUtils.set_bit_field(
191 power_state,
192 data.wireless_external_power_connected.value,
193 self.BIT_START_WIRELESS_POWER,
194 self.BIT_WIDTH_WIRELESS_POWER,
195 )
196 power_state = BitFieldUtils.set_bit_field(
197 power_state, data.battery_charge_state.value, self.BIT_START_CHARGE_STATE, self.BIT_WIDTH_CHARGE_STATE
198 )
199 power_state = BitFieldUtils.set_bit_field(
200 power_state, data.battery_charge_level.value, self.BIT_START_CHARGE_LEVEL, self.BIT_WIDTH_CHARGE_LEVEL
201 )
202 power_state = BitFieldUtils.set_bit_field(
203 power_state, data.charging_type.value, self.BIT_START_CHARGING_TYPE, self.BIT_WIDTH_CHARGING_TYPE
204 )
205 if data.charging_fault_battery:
206 power_state = BitFieldUtils.set_bit(power_state, self.BIT_START_FAULT_BATTERY)
207 if data.charging_fault_external_power:
208 power_state = BitFieldUtils.set_bit(power_state, self.BIT_START_FAULT_EXTERNAL)
209 if data.charging_fault_other:
210 power_state = BitFieldUtils.set_bit(power_state, self.BIT_START_FAULT_OTHER)
211 result.extend(DataParser.encode_int16(power_state, signed=False))
213 # Optional identifier
214 if data.identifier is not None:
215 result.extend(DataParser.encode_int16(data.identifier, signed=False))
217 # Optional battery level
218 if data.battery_level is not None:
219 result.extend(DataParser.encode_int8(data.battery_level, signed=False))
221 # Optional additional status
222 if data.service_required is not None or data.battery_fault is not None:
223 additional_status = 0
224 if data.service_required is not None:
225 additional_status = BitFieldUtils.set_bit_field(
226 additional_status,
227 data.service_required.value,
228 self.BIT_START_SERVICE_REQUIRED,
229 self.BIT_WIDTH_SERVICE_REQUIRED,
230 )
231 if data.battery_fault:
232 additional_status = BitFieldUtils.set_bit(additional_status, self.BIT_START_BATTERY_FAULT)
233 result.extend(DataParser.encode_int8(additional_status, signed=False))
235 return result