From c64cf5ff1295dd1b56f3f354e8184766a9fc9de4 Mon Sep 17 00:00:00 2001 From: thomas Date: Thu, 27 Aug 2026 14:11:27 +0100 Subject: [PATCH] Fix: Restored "time to charged" --- .../modules/bar/BatteryIndicator.qml | 66 ++++++++++++++++++- 1 file changed, 63 insertions(+), 3 deletions(-) diff --git a/dotfiles/.config/quickshell/modules/bar/BatteryIndicator.qml b/dotfiles/.config/quickshell/modules/bar/BatteryIndicator.qml index ca6d3e7..8192180 100644 --- a/dotfiles/.config/quickshell/modules/bar/BatteryIndicator.qml +++ b/dotfiles/.config/quickshell/modules/bar/BatteryIndicator.qml @@ -116,6 +116,68 @@ Item { } } + // device.timeToFull/timeToEmpty are computed by Quickshell from + // energy/energyCapacity/changeRate — when upowerd's EnergyFull glitches + // to 0 (the same daemon flake HEALTH routes around above), that formula + // divides by a bogus capacity and comes out negative rather than a + // real estimate (confirmed live: timeToFull sat at -3751 while `upower + // -i` itself still reported a normal 26.7 minutes). This recomputes + // the same estimate straight from sysfs as a fallback, only used when + // Quickshell's own value isn't positive. Unlike cycle_count/design + // capacity above, energy_now and power_now change continuously while + // (dis)charging, so this polls on a timer instead of reading once. + property real sysfsEnergyNowWh: -1 + property real sysfsPowerNowW: -1 + + readonly property real fallbackTimeToFullSeconds: (root.sysfsPowerNowW > 0 && root.energyFullWh > 0 && root.sysfsEnergyNowWh >= 0) + ? Math.max(0, (root.energyFullWh - root.sysfsEnergyNowWh) / root.sysfsPowerNowW * 3600) : -1 + readonly property real fallbackTimeToEmptySeconds: (root.sysfsPowerNowW > 0 && root.sysfsEnergyNowWh > 0) + ? (root.sysfsEnergyNowWh / root.sysfsPowerNowW * 3600) : -1 + + // Prefers upowerd's own live estimate when it's sane; only falls back + // to the sysfs-computed one when upowerd's has gone zero/negative. + readonly property real timeRemainingSeconds: { + const primary = root.onBattery ? root.device.timeToEmpty : root.device.timeToFull + if (primary > 0) return primary + return root.onBattery ? root.fallbackTimeToEmptySeconds : root.fallbackTimeToFullSeconds + } + + Timer { + id: sysfsRateTimer + interval: 15000 + repeat: true + running: root.batteryPresent + triggeredOnStart: true + onTriggered: { + if (!sysfsEnergyNowProc.running) sysfsEnergyNowProc.running = true + if (!sysfsPowerNowProc.running) sysfsPowerNowProc.running = true + } + } + + Process { + id: sysfsEnergyNowProc + command: ["sh", "-c", "cat \"$(ls -d /sys/class/power_supply/BAT* 2>/dev/null | head -n1)/energy_now\" 2>/dev/null"] + stdout: StdioCollector { + id: sysfsEnergyNowCollector + onStreamFinished: { + const n = parseFloat(sysfsEnergyNowCollector.text.trim()) + root.sysfsEnergyNowWh = (isNaN(n) || n < 0) ? -1 : (n / 1000000) + } + } + } + + Process { + id: sysfsPowerNowProc + command: ["sh", "-c", "cat \"$(ls -d /sys/class/power_supply/BAT* 2>/dev/null | head -n1)/power_now\" 2>/dev/null"] + stdout: StdioCollector { + id: sysfsPowerNowCollector + onStreamFinished: { + const n = parseFloat(sysfsPowerNowCollector.text.trim()) + root.sysfsPowerNowW = (isNaN(n) || n <= 0) ? -1 : (n / 1000000) + } + } + } + // Raw UPower state string (Charging/Discharging/PendingCharge/…) — more // granular than modeLabel below, which collapses those down to the // three phrases the hero text and STATE row show. @@ -176,9 +238,7 @@ Item { }, { label: root.onBattery ? "TIME LEFT" : "TIME TO FULL", - value: root.batteryPresent - ? root.formatDuration(root.onBattery ? root.device.timeToEmpty : root.device.timeToFull) - : "—" + value: root.batteryPresent ? root.formatDuration(root.timeRemainingSeconds) : "—" }, { label: "STATUS", value: root.rawStateLabel }, { label: "CYCLES", value: root.cycleCount > 0 ? String(root.cycleCount) : "—" }