ui: Show offline status for receiver-paired device batteries (#3217)

When a receiver-paired device goes offline, update its battery status
to OFFLINE so the tray and UI show "Battery: 58% (offline)" instead of
the stale "Battery: 58% (discharging)". The last known percentage is
preserved as useful context.

Only applies to devices paired through a receiver (Unifying, Bolt,
Lightspeed) which maintain a persistent connection while available.
Bluetooth devices that disconnect during idle keep their prior status
since going offline is normal power-saving behavior for them.

Add BatteryStatus.OFFLINE (0xFF) as a Solaar-internal value (not a
HID++ protocol value). Fresh battery data from read_battery() replaces
it when the device reconnects.
This commit is contained in:
Ken Sanislo 2026-05-13 14:32:03 -07:00 committed by GitHub
parent fc62ef00ad
commit 09cb2dddb9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 22 additions and 0 deletions

View File

@ -645,6 +645,8 @@ class BatteryStatus(Flag):
SLOW_RECHARGE = 0x04
INVALID_BATTERY = 0x05
THERMAL_ERROR = 0x06
# Solaar internal — not a HID++ protocol value
OFFLINE = 0xFF
class BatteryLevelApproximation(IntEnum):

View File

@ -40,6 +40,7 @@ from . import settings
from . import settings_templates
from .common import Alert
from .common import Battery
from .common import BatteryStatus
from .common import _read_usb_product_string
from .hidpp10_constants import NotificationFlag
from .hidpp20_constants import SupportedFeature
@ -609,6 +610,14 @@ class Device:
self.read_battery() # battery information may have changed so try to read it now
elif was_active and self.receiver and not isinstance(self.receiver, CenturionReceiver):
hidpp10.set_configuration_pending_flags(self.receiver, 0xFF)
if not active and self.receiver and self.battery_info is not None and self.battery_info.level is not None:
self.battery_info = Battery(
self.battery_info.level,
self.battery_info.next_level,
BatteryStatus.OFFLINE,
self.battery_info.voltage,
self.battery_info.light_level,
)
if logger.isEnabledFor(logging.DEBUG):
logger.debug("device %d changed: active=%s %s", self.number, self._active, self.battery_info)
if self.status_callback is not None:

View File

@ -306,6 +306,7 @@ def test_kw_exception():
"Battery: low (slow recharge)",
),
(common.BatteryStatus.DISCHARGING, None, True, False, ""),
(common.BatteryStatus.OFFLINE, None, True, False, ""),
],
)
def test_battery(status, expected_level, expected_ok, expected_charging, expected_string):
@ -326,3 +327,13 @@ def test_battery_2():
assert battery.ok()
assert not battery.charging()
assert battery.to_str() == "Battery: 50% (discharging)"
def test_battery_offline():
battery = common.Battery(58, None, common.BatteryStatus.OFFLINE, None)
assert battery.status == common.BatteryStatus.OFFLINE
assert battery.level == 58
assert battery.ok()
assert not battery.charging()
assert battery.to_str() == "Battery: 58% (offline)"