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

1"""Battery Level Status characteristic implementation.""" 

2 

3from __future__ import annotations 

4 

5from enum import IntFlag 

6 

7import msgspec 

8 

9from bluetooth_sig.types.battery import ( 

10 BatteryChargeLevel, 

11 BatteryChargeState, 

12 BatteryChargingType, 

13 PowerConnectionState, 

14 ServiceRequiredState, 

15) 

16 

17from ..context import CharacteristicContext 

18from .base import BaseCharacteristic 

19from .utils.bit_field_utils import BitFieldUtils 

20from .utils.data_parser import DataParser 

21 

22 

23class BatteryLevelStatusFlags(IntFlag): 

24 """Battery Level Status flags.""" 

25 

26 IDENTIFIER_PRESENT = 1 << 0 

27 BATTERY_LEVEL_PRESENT = 1 << 1 

28 ADDITIONAL_STATUS_PRESENT = 1 << 2 

29 

30 

31class BatteryLevelStatus(msgspec.Struct): 

32 """Battery Level Status data structure.""" 

33 

34 # Flags as IntFlag 

35 flags: BatteryLevelStatusFlags 

36 

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 

47 

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 

53 

54 

55class BatteryLevelStatusCharacteristic(BaseCharacteristic[BatteryLevelStatus]): 

56 """Battery Level Status characteristic (0x2BED). 

57 

58 org.bluetooth.characteristic.battery_level_status 

59 """ 

60 

61 _manual_unit: str | None = None # Bitfield, no units 

62 

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 

82 

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 

88 

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) 

92 

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 

98 

99 # Parse flags (1 byte) 

100 flags = BatteryLevelStatusFlags(DataParser.parse_int8(data, offset, signed=False)) 

101 offset += 1 

102 

103 # Parse power state (2 bytes) 

104 power_state = DataParser.parse_int16(data, offset, signed=False) 

105 offset += 2 

106 

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) 

126 

127 # Optional identifier 

128 identifier = None 

129 if flags & BatteryLevelStatusFlags.IDENTIFIER_PRESENT: 

130 identifier = DataParser.parse_int16(data, offset, signed=False) 

131 offset += 2 

132 

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 

138 

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) 

150 

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 ) 

167 

168 def _encode_value(self, data: BatteryLevelStatus) -> bytearray: 

169 """Encode the battery level status value.""" 

170 result = bytearray() 

171 

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)) 

181 

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)) 

214 

215 # Optional identifier 

216 if data.identifier is not None: 

217 result.extend(DataParser.encode_int16(data.identifier, signed=False)) 

218 

219 # Optional battery level 

220 if data.battery_level is not None: 

221 result.extend(DataParser.encode_int8(data.battery_level, signed=False)) 

222 

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)) 

236 

237 return result