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

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(self, data: bytearray, ctx: CharacteristicContext | None = None) -> BatteryLevelStatus: # pylint: disable=too-many-locals 

94 """Decode the battery level status value.""" 

95 offset = 0 

96 

97 # Parse flags (1 byte) 

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

99 offset += 1 

100 

101 # Parse power state (2 bytes) 

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

103 offset += 2 

104 

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) 

124 

125 # Optional identifier 

126 identifier = None 

127 if flags & BatteryLevelStatusFlags.IDENTIFIER_PRESENT: 

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

129 offset += 2 

130 

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 

136 

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) 

148 

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 ) 

165 

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

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

168 result = bytearray() 

169 

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

179 

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

212 

213 # Optional identifier 

214 if data.identifier is not None: 

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

216 

217 # Optional battery level 

218 if data.battery_level is not None: 

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

220 

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

234 

235 return result