From 4dda0e3a5e40efbb4a9eb8498c29ff6ed5ac2274 Mon Sep 17 00:00:00 2001 From: Callum Wong Date: Wed, 15 Jul 2026 01:32:48 +1000 Subject: [PATCH] Smooth battery time remaining estimate (#2854) * feat(battery): smooth time remaining estimate with moving average * feat(battery): use a time-weighted EMA for time remaining estimate port 1.5 (cherry picked from commit bba550296025c06d600d0389f2bc313221f7b31c) --- quickshell/Services/BatteryService.qml | 40 ++++++++++++++++++++++++-- 1 file changed, 38 insertions(+), 2 deletions(-) diff --git a/quickshell/Services/BatteryService.qml b/quickshell/Services/BatteryService.qml index 9475710bf..553d70cd4 100644 --- a/quickshell/Services/BatteryService.qml +++ b/quickshell/Services/BatteryService.qml @@ -182,6 +182,10 @@ Singleton { } onIsChargingChanged: { + // Reset average when switching states + _smoothedChangeRate = (_hasKnownChargingState && changeRate > 0) ? changeRate : 0; + _lastRateSampleTime = _smoothedChangeRate > 0 ? Date.now() : 0; + if (isCharging) { _hasNotifiedLowBattery = false; _hasNotifiedCriticalBattery = false; @@ -233,6 +237,38 @@ Singleton { return val; } + // A time-weighted exponential moving average based on the aggregated charge/discharge rate + property real _smoothedChangeRate: 0 + property real _lastRateSampleTime: 0 + readonly property real _rateSmoothingHalfLife: 90 // in seconds + + function _updateSmoothedRate() { + if (!_hasKnownChargingState || changeRate <= 0) + return; + + const now = Date.now(); + if (_smoothedChangeRate <= 0 || _lastRateSampleTime <= 0) { + _smoothedChangeRate = changeRate; + _lastRateSampleTime = now; + return; + } + + const dt = (now - _lastRateSampleTime) / 1000; + _lastRateSampleTime = now; + if (dt <= 0) + return; + + const tau = _rateSmoothingHalfLife / Math.LN2; + const alpha = 1 - Math.exp(-dt / tau); + _smoothedChangeRate += alpha * (changeRate - _smoothedChangeRate); + } + + onChangeRateChanged: _updateSmoothedRate() + onBatteryAvailableChanged: if (!batteryAvailable) { + _smoothedChangeRate = 0; + _lastRateSampleTime = 0; + } + // Aggregated battery health readonly property string batteryHealth: { if (!batteryAvailable) @@ -341,8 +377,8 @@ Singleton { return "Unknown"; } - let totalTime = 0; - totalTime = (isCharging) ? ((batteryCapacity - batteryEnergy) / changeRate) : (batteryEnergy / changeRate); + const rate = _smoothedChangeRate > 0 ? _smoothedChangeRate : changeRate; + const totalTime = (isCharging) ? ((batteryCapacity - batteryEnergy) / rate) : (batteryEnergy / rate); const avgTime = Math.abs(totalTime * 3600); if (!avgTime || avgTime <= 0 || avgTime > 86400) return "Unknown";