From fe596d61ee55ab0810335e94a2da9033dcf949b5 Mon Sep 17 00:00:00 2001 From: Diego Barrios Date: Thu, 3 Sep 2026 16:45:33 -0600 Subject: [PATCH] Latch the low-battery warning until the battery recovers The service reset its "already notified" flag the instant the device was not both discharging and at/under the threshold. A brief charging, pending or unknown UPower state at a still-low percentage -- a loose charger, a weak dock, an energy-rate blip -- cleared the latch, so the next 30s poll re-fired the critical "Time to recharge!" notification. Critical notifications never auto-expire, so the copies piled up on screen. Keep the latch set once warned until the battery climbs back to ten points above the threshold, so the warning fires once per discharge and re-arms only after a real recovery. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01LVR5pzYf3W2WZqLiJEZRoU --- shell/plugins/services/battery/BatteryModel.js | 9 ++++++++- test/shell.d/battery-test.sh | 15 +++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/shell/plugins/services/battery/BatteryModel.js b/shell/plugins/services/battery/BatteryModel.js index 2aca5dbd92b..04883bdc63c 100644 --- a/shell/plugins/services/battery/BatteryModel.js +++ b/shell/plugins/services/battery/BatteryModel.js @@ -12,10 +12,17 @@ function shouldWarnLowBattery(device, onBattery, dischargingState, threshold, al if (level < 0) return { level: level, notify: false, notifiedLowBattery: false } var low = isDischarging(device, onBattery, dischargingState) && level <= threshold + + // Hysteresis: once warned, stay latched until the battery recovers well above + // the threshold. Clearing the latch the moment the device is not both + // discharging and at/under the threshold lets a brief charging/pending/unknown + // UPower blip at a still-low percentage re-arm the warning, so the next poll + // re-fires the critical notification and copies stack up on screen. + var recovered = level >= threshold + 10 return { level: level, notify: low && !alreadyNotified, - notifiedLowBattery: low + notifiedLowBattery: (alreadyNotified || low) && !recovered } } diff --git a/test/shell.d/battery-test.sh b/test/shell.d/battery-test.sh index 0447779a96f..591673a0ee0 100644 --- a/test/shell.d/battery-test.sh +++ b/test/shell.d/battery-test.sh @@ -28,4 +28,19 @@ assertDeepEqual( { level: 40, notify: false, notifiedLowBattery: false }, 'battery clears notified state after recovery' ) +assertDeepEqual( + battery.shouldWarnLowBattery({ isPresent: true, percentage: 0.1, state: discharging }, false, discharging, 10, true), + { level: 10, notify: false, notifiedLowBattery: true }, + 'battery holds the latch through a non-discharging blip at a still-low level' +) +assertDeepEqual( + battery.shouldWarnLowBattery({ isPresent: true, percentage: 0.15, state: discharging }, true, discharging, 10, true), + { level: 15, notify: false, notifiedLowBattery: true }, + 'battery holds the latch above the threshold until it recovers past the re-arm level' +) +assertDeepEqual( + battery.shouldWarnLowBattery({ isPresent: true, percentage: 0.2, state: discharging }, true, discharging, 10, true), + { level: 20, notify: false, notifiedLowBattery: false }, + 'battery clears the latch once it recovers to the re-arm level' +) JS