diff --git a/CHANGELOG.md b/CHANGELOG.md index 6a0221e..d612a81 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Changelog +## 1.1.0 – 2026-10-07 + +### Added +- **Charging indicator**: live charge/discharge power in watts, battery graphic with charge bolt, + power history graph and estimated time until full / time remaining (respects the 80 % limit) + – on the Battery page, the Dashboard, in the panel widget (popup and tooltip) and in `predatorctl status` +- **pacman package**: `packaging/arch/PKGBUILD` builds `predator-control--any.pkg.tar.zst`; + the kernel module is shipped as DKMS source and rebuilt automatically for every kernel + ## 1.0.0 – 2026-10-04 First release. diff --git a/README.md b/README.md index 3c4b460..c2c71a4 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ desktop app, a **Plasma panel widget**, and a command line tool. | **Keyboard RGB** | 12 hardware effects (static, breathing, rainbow wave, ripple, neon, rain, explosion, pulse, stars, meteor, aura, …), colour, brightness, hidden effect variants, presets | | **RGB Studio** | Paint individual keys with the mouse, live preview on the keyboard, quick fills (WASD, arrows, F-row, numpad …), German ISO and US ANSI layouts | | **Lightbar** | All rear lightbar effects with colour, brightness and speed – or let it follow the keyboard | -| **Battery** | 80 % charge limit, calibration, live health / cycles / power draw | +| **Battery** | live charging indicator in watts with power graph and time until full / remaining, 80 % charge limit, calibration, health and cycles | | **Firmware options** | Boot animation & sound, LCD overdrive, keyboard backlight timeout, USB charging while off | | **Monitoring** | CPU/GPU temperatures, fan RPM, CPU load and clock, memory – without waking the dGPU | | **Panel widget** | Click the icon in the Plasma panel to change everything quickly; buttons open the full app directly on the right page (e.g. RGB Studio). Middle-click cycles the performance mode | @@ -41,6 +41,23 @@ interfaces will likely work. Reports are welcome – please include the output o ## Installation +### pacman package (Arch, CachyOS, Manjaro, EndeavourOS …) – recommended + +Download `predator-control--any.pkg.tar.zst` from the +[releases page](https://git.mrblake.cc/MrBlake/Cachy-Predator-Keyboard/releases) and install it: + +```bash +sudo pacman -S --needed linux-cachyos-headers clang lld llvm # headers for your kernel (stock Arch: linux-headers) +sudo pacman -U predator-control-1.1.0-1-any.pkg.tar.zst +``` + +The service starts automatically and the kernel module is built by DKMS for every installed kernel. +To build the package yourself: `cd packaging/arch && makepkg -f`. + +> Switching from `install.sh` to the package? Run `sudo ./uninstall.sh` first (your settings are kept). + +### Install script (any distribution) + Requirements: a kernel with `acer-wmi` (any recent kernel), Python 3, PySide6, DKMS and kernel headers. On Arch / CachyOS / Manjaro the installer pulls in everything. @@ -64,7 +81,7 @@ On other distributions install the dependencies yourself and run `sudo ./install signatures you have to sign `predator_wmi` yourself (e.g. with `sbctl` or a MOK key). Keyboard RGB and performance profiles work without the module. -Uninstall with `sudo ./uninstall.sh` (add `--purge` to also delete your settings). +Uninstall with `sudo pacman -R predator-control` (package) or `sudo ./uninstall.sh` (add `--purge` to also delete your settings). ## Usage diff --git a/docs/screenshots/battery.png b/docs/screenshots/battery.png new file mode 100644 index 0000000..8e8872b Binary files /dev/null and b/docs/screenshots/battery.png differ diff --git a/docs/screenshots/home.png b/docs/screenshots/home.png index c198308..98a2dbb 100644 Binary files a/docs/screenshots/home.png and b/docs/screenshots/home.png differ diff --git a/install.sh b/install.sh index 2941d8e..c5ca0ae 100755 --- a/install.sh +++ b/install.sh @@ -4,7 +4,7 @@ # sudo ./install.sh --no-deps skip package installation (non-Arch distros) set -euo pipefail -VERSION="1.0.0" +VERSION="1.1.0" MODULE="predator-wmi" SRC="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PREFIX="/usr" diff --git a/kernel/dkms.conf b/kernel/dkms.conf index 68fcc9b..820f3a8 100644 --- a/kernel/dkms.conf +++ b/kernel/dkms.conf @@ -1,5 +1,5 @@ PACKAGE_NAME="predator-wmi" -PACKAGE_VERSION="1.0" +PACKAGE_VERSION="1.1.0" BUILT_MODULE_NAME[0]="predator_wmi" DEST_MODULE_LOCATION[0]="/kernel/drivers/platform/x86" MAKE[0]="make KVER=${kernelver} KDIR=${kernel_source_dir}" diff --git a/packaging/arch/.gitignore b/packaging/arch/.gitignore new file mode 100644 index 0000000..4c43ba1 --- /dev/null +++ b/packaging/arch/.gitignore @@ -0,0 +1,3 @@ +*.pkg.tar.* +src/ +pkg/ diff --git a/packaging/arch/PKGBUILD b/packaging/arch/PKGBUILD new file mode 100644 index 0000000..f799951 --- /dev/null +++ b/packaging/arch/PKGBUILD @@ -0,0 +1,57 @@ +# Maintainer: Louis-Alexander Kerst +# +# Builds the pacman package from this source checkout: +# cd packaging/arch && makepkg -f +# sudo pacman -U predator-control-*.pkg.tar.zst + +pkgname=predator-control +pkgver=1.1.0 +pkgrel=1 +pkgdesc="PredatorSense replacement for Acer Predator laptops: performance modes, fans, keyboard RGB, lightbar, battery (Qt app + Plasma widget)" +arch=('any') +url="https://git.mrblake.cc/MrBlake/Cachy-Predator-Keyboard" +license=('GPL-2.0-only') +depends=('python' 'pyside6' 'dkms') +optdepends=('plasma-workspace: panel widget' + 'linux-headers: build the kernel module for the stock Arch kernel' + 'linux-cachyos-headers: build the kernel module for CachyOS kernels' + 'clang: needed by DKMS for clang-built kernels (CachyOS)' + 'lld: needed by DKMS for clang-built kernels (CachyOS)' + 'llvm: needed by DKMS for clang-built kernels (CachyOS)') +install=predator-control.install +source=() + +_src="$(realpath "$startdir/../..")" +_modver=1.1.0 + +package() { + cd "$_src" + + # Python package and launchers + install -d "$pkgdir/usr/lib/predator-control/predator_control/gui" + install -m644 predator_control/*.py "$pkgdir/usr/lib/predator-control/predator_control/" + install -m644 predator_control/gui/*.py "$pkgdir/usr/lib/predator-control/predator_control/gui/" + python -m compileall -q -d /usr/lib/predator-control "$pkgdir/usr/lib/predator-control" + for b in predatord predatorctl predator-control; do + install -Dm755 "bin/$b" "$pkgdir/usr/bin/$b" + done + + # Kernel module as DKMS source (built automatically by the dkms pacman hook) + install -d "$pkgdir/usr/src/predator-wmi-$_modver" + install -m644 kernel/{predator_wmi.c,Makefile,dkms.conf} "$pkgdir/usr/src/predator-wmi-$_modver/" + install -Dm644 data/modules-load-predator-control.conf "$pkgdir/usr/lib/modules-load.d/predator-control.conf" + + # Service, resume hook, desktop integration + install -Dm644 data/predatord.service "$pkgdir/usr/lib/systemd/system/predatord.service" + install -Dm755 data/system-sleep-predator-control "$pkgdir/usr/lib/systemd/system-sleep/predator-control" + install -Dm644 data/predator-control.desktop "$pkgdir/usr/share/applications/predator-control.desktop" + install -Dm644 data/predator-control.svg "$pkgdir/usr/share/icons/hicolor/scalable/apps/predator-control.svg" + + # Plasma panel widget + local p="$pkgdir/usr/share/plasma/plasmoids/org.predatorcontrol.panel" + install -Dm644 plasmoid/org.predatorcontrol.panel/metadata.json "$p/metadata.json" + install -Dm644 plasmoid/org.predatorcontrol.panel/contents/ui/main.qml "$p/contents/ui/main.qml" + + install -Dm644 README.md "$pkgdir/usr/share/doc/$pkgname/README.md" + install -Dm644 CHANGELOG.md "$pkgdir/usr/share/doc/$pkgname/CHANGELOG.md" +} diff --git a/packaging/arch/predator-control.install b/packaging/arch/predator-control.install new file mode 100644 index 0000000..415be10 --- /dev/null +++ b/packaging/arch/predator-control.install @@ -0,0 +1,25 @@ +post_install() { + systemctl daemon-reload + modprobe predator_wmi 2>/dev/null || echo ":: predator_wmi could not be loaded yet (kernel headers installed? Secure Boot?)" + systemctl enable --now predatord.service + echo ":: Predator Control installed." + echo ":: Add the panel widget: right-click the panel -> Add or Manage Widgets -> 'Predator Control'" + echo ":: Only members of the 'wheel' group can change settings." +} + +post_upgrade() { + systemctl daemon-reload + modprobe predator_wmi 2>/dev/null + systemctl try-restart predatord.service +} + +pre_remove() { + systemctl disable --now predatord.service 2>/dev/null + modprobe -r predator_wmi 2>/dev/null + true +} + +post_remove() { + systemctl daemon-reload + echo ":: Settings were kept in /etc/predator-control (delete manually if no longer needed)." +} diff --git a/plasmoid/org.predatorcontrol.panel/contents/ui/main.qml b/plasmoid/org.predatorcontrol.panel/contents/ui/main.qml index 0328fa5..e2f98b2 100644 --- a/plasmoid/org.predatorcontrol.panel/contents/ui/main.qml +++ b/plasmoid/org.predatorcontrol.panel/contents/ui/main.qml @@ -40,6 +40,21 @@ PlasmoidItem { return names[key] || key || "–" } function temp(v) { return v ? Math.round(v) + " °C" : "off" } + function duration(m) { + if (m === null || m === undefined) return "" + return m >= 60 ? Math.floor(m / 60) + " h " + (m % 60) + " min" : m + " min" + } + function batteryText() { + if (!ok || !st.battery.present) return "" + const b = st.battery + let t = b.capacity + " %" + if (b.charging) t += " · charging" + (b.power_w ? " at " + b.power_w.toFixed(1) + " W" : "") + else if (b.status === "Discharging") t += " · on battery" + (b.power_w ? ", " + b.power_w.toFixed(1) + " W" : "") + else t += b.ac_online ? " · plugged in" : "" + if (b.minutes_left !== null && b.minutes_left !== undefined) + t += " · " + duration(b.minutes_left) + (b.charging ? " until full" : " left") + return t + } function run(args) { // unique suffix so the executable engine runs identical commands again @@ -56,6 +71,7 @@ PlasmoidItem { toolTipSubText: ok ? profileLabel(st.profile) + " · CPU " + temp(st.sensors.cpu_temp) + " · GPU " + temp(st.sensors.gpu_temp) + "\nFans " + (st.sensors.cpu_fan || 0) + " / " + (st.sensors.gpu_fan || 0) + " RPM" + + "\nBattery " + batteryText() : "Service predatord not running" P5Support.DataSource { @@ -318,13 +334,15 @@ PlasmoidItem { Layout.fillWidth: true Layout.topMargin: Kirigami.Units.smallSpacing Kirigami.Icon { - source: root.ok && root.st.battery.ac_online ? "battery-charging" : "battery" + source: root.ok && root.st.battery.charging ? "battery-charging" : (root.ok && root.st.battery.ac_online ? "battery-full-charging" : "battery") Layout.preferredWidth: Kirigami.Units.iconSizes.smallMedium Layout.preferredHeight: Kirigami.Units.iconSizes.smallMedium } PC3.Label { Layout.fillWidth: true - text: root.ok ? root.st.battery.capacity + " % · " + (root.st.battery.ac_online ? "plugged in" : "on battery") : "" + text: root.batteryText() + wrapMode: Text.Wrap + color: root.ok && root.st.battery.charging ? Kirigami.Theme.positiveTextColor : Kirigami.Theme.textColor } PC3.CheckBox { text: "Limit to 80 %" diff --git a/plasmoid/org.predatorcontrol.panel/metadata.json b/plasmoid/org.predatorcontrol.panel/metadata.json index c3cf8d5..dd26946 100644 --- a/plasmoid/org.predatorcontrol.panel/metadata.json +++ b/plasmoid/org.predatorcontrol.panel/metadata.json @@ -12,7 +12,7 @@ "Id": "org.predatorcontrol.panel", "License": "GPL-2.0-only", "Name": "Predator Control", - "Version": "1.0.0", + "Version": "1.1.0", "Website": "https://git.mrblake.cc/MrBlake/Cachy-Predator-Keyboard" }, "X-Plasma-API-Minimum-Version": "6.0", diff --git a/predator_control/__init__.py b/predator_control/__init__.py index 7fcc9bf..012ec92 100644 --- a/predator_control/__init__.py +++ b/predator_control/__init__.py @@ -2,7 +2,7 @@ import os -__version__ = "1.0.0" +__version__ = "1.1.0" APP_NAME = "Predator Control" RUN_DIR = os.environ.get("PREDATOR_CONTROL_RUN_DIR", "/run/predator-control") diff --git a/predator_control/cli.py b/predator_control/cli.py index a1b867c..e68519e 100644 --- a/predator_control/cli.py +++ b/predator_control/cli.py @@ -84,7 +84,13 @@ def main(argv=None): print("CPU: %s °C %s %% fan %s RPM" % (s["cpu_temp"], st["cpu_load"], s["cpu_fan"])) print("GPU: %s fan %s RPM" % ("%s °C" % s["gpu_temp"] if s["gpu_temp"] else "asleep", s["gpu_fan"])) print("Fan mode: %s" % st["config"]["fans"]["mode"]) - print("Battery: %s %% (%s), AC: %s" % (b.get("capacity"), b.get("status"), "yes" if b["ac_online"] else "no")) + line = "Battery: %s %% (%s), AC: %s" % (b.get("capacity"), b.get("status"), "yes" if b["ac_online"] else "no") + if b.get("power_w"): + line += ", %s %.1f W" % ("charging at" if b.get("charging") else "drawing", b["power_w"]) + if b.get("minutes_left") is not None: + h, m = divmod(b["minutes_left"], 60) + line += ", %d:%02d h %s" % (h, m, "until full" if b.get("charging") else "left") + print(line) k = st["config"]["keyboard"] print("Keyboard: %s %s brightness %s" % (k["effect"], k["color"], k["brightness"])) for k, v in st["errors"].items(): diff --git a/predator_control/daemon.py b/predator_control/daemon.py index a02f56c..c0390bd 100644 --- a/predator_control/daemon.py +++ b/predator_control/daemon.py @@ -214,7 +214,7 @@ class Daemon: def build_state(self): s = hw.sensors() - bat = hw.battery() + bat = hw.battery(limit_80=bool(self.battery_limit)) st = { "version": __version__, "time": time.time(), diff --git a/predator_control/gui/app.py b/predator_control/gui/app.py index 522964b..a95e132 100644 --- a/predator_control/gui/app.py +++ b/predator_control/gui/app.py @@ -17,7 +17,7 @@ from PySide6.QtWidgets import (QApplication, QButtonGroup, QCheckBox, QComboBox, from .. import APP_NAME, __version__, layout from ..client import Client, DaemonError, read_state from . import theme -from .widgets import Bar, Card, ColorRow, CurveEditor, Gauge, KeyboardPainter +from .widgets import Bar, Card, ChargeIndicator, ColorRow, CurveEditor, Gauge, KeyboardPainter, format_minutes PAGES = [ ("home", "Dashboard"), @@ -146,8 +146,9 @@ class HomePage(Page): quick.body.addWidget(self.l_profile) self.l_fans = _muted("") quick.body.addWidget(self.l_fans) - self.l_bat = _muted("") - quick.body.addWidget(self.l_bat) + self.charge = ChargeIndicator(compact=True) + self.charge.setFixedHeight(54) + quick.body.addWidget(self.charge) b = QPushButton("Next profile") b.setObjectName("primary") b.clicked.connect(self._cycle) @@ -182,7 +183,7 @@ class HomePage(Page): self.l_profile.setText(PROFILE_INFO.get(st["profile"], (st["profile"],))[0]) self.l_fans.setText("Fans: %s" % st["config"]["fans"]["mode"].capitalize()) b = st["battery"] - self.l_bat.setText("Battery %s %% · %s" % (b.get("capacity"), "AC" if b.get("ac_online") else "on battery")) + self.charge.set_battery(b) class PerformancePage(Page): @@ -595,14 +596,13 @@ class BatteryPage(Page): self.lay.addWidget(_title("Battery", "Battery status and health features")) row = QHBoxLayout() row.setSpacing(16) - info = Card("Status") - self.cap = QLabel("–") - self.cap.setObjectName("big") - info.body.addWidget(self.cap) + info = Card("Charging") + self.indicator = ChargeIndicator() + info.body.addWidget(self.indicator) self.details = _muted("") info.body.addWidget(self.details) info.body.addStretch(1) - row.addWidget(info, 1) + row.addWidget(info, 3) health = Card("Battery health") self.limit = QCheckBox("Limit charging to 80 %") self.limit.clicked.connect(lambda v: self._set("set_battery_limit", v)) @@ -648,14 +648,17 @@ class BatteryPage(Page): def update_state(self, st): b = st["battery"] if not b.get("present"): - self.cap.setText("No battery") + self.details.setText("No battery found") return - self.cap.setText("%s %%" % b.get("capacity")) - lines = ["Status: %s" % b.get("status"), - "Power source: %s" % ("AC adapter" if b.get("ac_online") else "battery"), - "Power draw: %s W" % b.get("power_w"), + self.indicator.set_battery(b) + rate = "Charge rate" if b.get("charging") else "Power draw" + lines = ["Power source: %s" % ("AC adapter" if b.get("ac_online") else "battery"), + "%s: %s W" % (rate, b.get("power_w") if b.get("power_w") is not None else "–"), "Health: %s %% of design capacity" % b.get("health"), "Charge cycles: %s" % b.get("cycles")] + if b.get("minutes_left") is not None: + lines.insert(2, "%s: %s" % ("Time until full" + (" (80 % limit)" if st.get("battery_limit") else "") + if b.get("charging") else "Time remaining", format_minutes(b["minutes_left"]))) self.details.setText("\n".join(lines)) @@ -826,6 +829,9 @@ class MainWindow(QMainWindow): self.banner.hide() self.state = st self.stack.currentWidget().update_state(st) + bat_page = self.pages["battery"][0] + if self.stack.currentWidget() is not bat_page: + bat_page.indicator.set_battery(st["battery"]) # keep the power history going s = st["sensors"] self.footer.setText("%s\nCPU %s °C · GPU %s" % ( PROFILE_INFO.get(st["profile"], (st["profile"],))[0], s.get("cpu_temp"), diff --git a/predator_control/gui/widgets.py b/predator_control/gui/widgets.py index 7981cc0..38fb465 100644 --- a/predator_control/gui/widgets.py +++ b/predator_control/gui/widgets.py @@ -388,3 +388,139 @@ class ColorRow(QWidget): self.current = QColor(c) self.preview.setStyleSheet("background:%s;border-radius:6px;border:1px solid %s;" % (self.current.name(), theme.BORDER)) + + +def format_minutes(m): + if m is None: + return "" + return "%d h %02d min" % divmod(m, 60) if m >= 60 else "%d min" % m + + +class ChargeIndicator(QWidget): + """Battery graphic with fill level, charge bolt, wattage and a power history sparkline.""" + + HISTORY = 80 # samples (~2 min at 1.5 s) + + def __init__(self, compact=False, parent=None): + super().__init__(parent) + self.compact = compact + self.bat = {} + self.history = [] + self.setMinimumHeight(54 if compact else 190) + self.setMinimumWidth(300 if compact else 420) + self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred) + + def set_battery(self, bat): + self.bat = bat or {} + w = self.bat.get("power_w") + if w is not None: + self.history = (self.history + [w])[-self.HISTORY:] + self.update() + + def _state_text(self): + b = self.bat + st = b.get("status") or "" + if st == "Charging": + return "Charging" + if st == "Discharging": + return "On battery" + if b.get("ac_online"): + return "Plugged in · not charging" + return st or "Unknown" + + def paintEvent(self, _): + b = self.bat + p = QPainter(self) + p.setRenderHint(QPainter.Antialiasing) + h = self.height() - 8 if self.compact else min(self.height() * 0.42, 86) + bw, bh = h * 2.0, h + r = QRectF(4, 4, bw, bh) + cap = b.get("capacity") or 0 + charging = b.get("charging") + accent = QColor(theme.ACCENT if charging else (theme.DANGER if cap <= 15 else theme.TEXT)) + + # battery body + tip + p.setPen(QPen(QColor(theme.MUTED), 2.5)) + p.setBrush(Qt.NoBrush) + p.drawRoundedRect(r, h * 0.14, h * 0.14) + p.setPen(Qt.NoPen) + p.setBrush(QColor(theme.MUTED)) + p.drawRoundedRect(QRectF(r.right() + 2, r.top() + bh * 0.3, bh * 0.1, bh * 0.4), 2, 2) + inner = r.adjusted(5, 5, -5, -5) + fill = QColor(accent) + fill.setAlpha(170) + p.setBrush(fill) + p.drawRoundedRect(QRectF(inner.left(), inner.top(), inner.width() * cap / 100, inner.height()), + h * 0.08, h * 0.08) + if charging: + c = inner.center() + s = inner.height() * 0.42 + bolt = QPainterPath(QPointF(c.x() + s * 0.15, c.y() - s)) + bolt.lineTo(c.x() - s * 0.45, c.y() + s * 0.12) + bolt.lineTo(c.x() - s * 0.02, c.y() + s * 0.12) + bolt.lineTo(c.x() - s * 0.15, c.y() + s) + bolt.lineTo(c.x() + s * 0.45, c.y() - s * 0.12) + bolt.lineTo(c.x() + s * 0.02, c.y() - s * 0.12) + bolt.closeSubpath() + p.setBrush(QColor(theme.TEXT)) + p.setPen(QPen(QColor(theme.BG), 1.5)) + p.drawPath(bolt) + else: + f = QFont(self.font()) + f.setBold(True) + f.setPointSizeF(max(8, h * 0.22)) + p.setFont(f) + p.setPen(QColor(theme.TEXT)) + p.drawText(inner, Qt.AlignCenter, "%d %%" % cap) + + # text block + tx = r.right() + bh * 0.1 + 16 + f = QFont(self.font()) + w = b.get("power_w") + f.setBold(True) + f.setPointSizeF(max(10, h * (0.36 if self.compact else 0.34))) + p.setFont(f) + p.setPen(accent if charging else QColor(theme.TEXT)) + big = ("%.1f W" % w) if w else ("%d %%" % cap) + p.drawText(QRectF(tx, r.top(), self.width() - tx, bh * 0.55), Qt.AlignLeft | Qt.AlignVCenter, big) + f.setBold(False) + f.setPointSizeF(max(8, h * (0.17 if self.compact else 0.14))) + p.setFont(f) + p.setPen(QColor(theme.MUTED)) + sub = self._state_text() + if w: + sub = "%d %% · %s" % (cap, sub) + t = format_minutes(b.get("minutes_left")) + if t: + sub += " · %s %s" % (t, "until full" if charging else "left") + p.drawText(QRectF(tx, r.top() + bh * 0.5, self.width() - tx, bh * 0.5), Qt.AlignLeft | Qt.AlignVCenter, sub) + + if self.compact or len(self.history) < 2: + return + # power history sparkline + g = QRectF(4, r.bottom() + 22, self.width() - 8, self.height() - r.bottom() - 30) + if g.height() < 30: + return + p.setPen(QColor(theme.MUTED)) + f.setPointSizeF(8) + p.setFont(f) + top = max(10.0, max(self.history) * 1.15) + p.drawText(QRectF(g.left(), g.top() - 18, g.width(), 16), Qt.AlignLeft, + "Power over the last %d min" % max(1, round(len(self.history) * 1.5 / 60))) + p.drawText(QRectF(g.left(), g.top() - 18, g.width(), 16), Qt.AlignRight, "max %.0f W" % top) + p.setPen(QPen(QColor(theme.GRID), 1)) + p.drawLine(g.bottomLeft(), g.bottomRight()) + n = self.HISTORY - 1 + pts = [QPointF(g.left() + g.width() * (n - (len(self.history) - 1 - i)) / n, + g.bottom() - g.height() * v / top) for i, v in enumerate(self.history)] + path = QPainterPath(pts[0]) + for pt in pts[1:]: + path.lineTo(pt) + area = QPainterPath(path) + area.lineTo(pts[-1].x(), g.bottom()) + area.lineTo(pts[0].x(), g.bottom()) + c = QColor(theme.ACCENT) + c.setAlpha(45) + p.fillPath(area, c) + p.setPen(QPen(QColor(theme.ACCENT), 2)) + p.drawPath(path) diff --git a/predator_control/hardware.py b/predator_control/hardware.py index c49917f..b7a2b57 100644 --- a/predator_control/hardware.py +++ b/predator_control/hardware.py @@ -247,7 +247,7 @@ def set_lightbar(mode="static", color=(0, 200, 255), brightness=200, speed=5, di # --------------------------------------------------------------- Battery -def battery(): +def battery(limit_80=False): bat = next(iter(glob.glob("/sys/class/power_supply/BAT*")), None) ac = next((p for p in glob.glob("/sys/class/power_supply/*") if _read(p + "/type") == "Mains"), None) @@ -268,6 +268,19 @@ def battery(): "cycles": _read_int(bat + "/cycle_count"), "health": round(100.0 * full / design, 1) if full and design else None, }) + # remaining time in minutes (to full while charging, to empty while discharging) + now = _read_int(bat + "/charge_now") or _read_int(bat + "/energy_now") + rate = cur if _read_int(bat + "/charge_now") is not None else pwr + minutes = None + if now is not None and full and rate: + rate = abs(rate) + if out["status"] == "Charging": + target = full * 0.8 if limit_80 else full + minutes = int(max(0, target - now) / rate * 60) + elif out["status"] == "Discharging": + minutes = int(now / rate * 60) + out["minutes_left"] = minutes + out["charging"] = out["status"] == "Charging" return out