===== PkgPower ===== CPU package power-limit manager originally written for my personal laptop (%%ThinkPad%% P16 Gen 5, Core Ultra 7 365, Fedora Linux + KDE Plasma 6). Created 2026-10-06. A small bash script + systemd unit + udev rule + Plasma 6 panel applet that caps CPU package power (PL1/PL2) via the kernel's native RAPL interface. Replaces the unmaintained ''undervolt'' Python utility: only its power-limit writes (''-p1''/''-p2'', MSR 0x610) ever had effect — voltage offsets are locked on 10th-gen+ Intel CPUs and silently no-op. ==== How it works ==== * Interface: RAPL via powercap sysfs. ''/sys/class/powercap/intel-rapl:0/constraint_0_power_limit_uw'' = PL1 (long/sustained), ''constraint_1_power_limit_uw'' = PL2 (short/burst). Units are **microwatts**. * Stock firmware limits on this machine: **55 W / 55 W**. * Current mode persists in ''/var/lib/pkgpower/mode'' and is re-applied by a boot unit, a udev rule (AC plug/unplug), and the panel applet. * **One-writer rule:** all limit writes go through ''/usr/local/bin/pkgpower''; the applet only selects a mode. Never write raw wattages from two places (the TLP/powerdevil double-writer problem). ^ Mode ^ PL1/PL2 (W) ^ Use ^ | ''perf'' | 55 / 55 | stock, on wall power | | ''auto'' (default) | 25/35 on AC, 15/15 on battery | daily | | ''eco'' | 15 / 15 | quiet / battery | | ''max'' | 10 / 10 | maximum endurance | ==== Installation ==== === 1. The script: /usr/local/bin/pkgpower === #!/usr/bin/env bash # pkgpower — set CPU package power limits via RAPL (native, no MSR hacking) # pkgpower re-apply current mode # pkgpower set MODE MODE = perf | auto | eco | max set -euo pipefail PC=/sys/class/powercap/intel-rapl:0 STATE=/var/lib/pkgpower/mode on_ac() { grep -q 1 /sys/class/power_supply/*/online 2>/dev/null; } # only AC adapters have "online" set_limits() { echo $(( $1 * 1000000 )) > "$PC/constraint_0_power_limit_uw" # long/sustained echo $(( $2 * 1000000 )) > "$PC/constraint_1_power_limit_uw" # short/burst } apply() { local m; m=$(cat "$STATE" 2>/dev/null || echo auto) case "$m" in perf) set_limits 55 55 ;; eco) set_limits 15 15 ;; max) set_limits 10 10 ;; auto) if on_ac; then set_limits 25 35; else set_limits 15 15; fi ;; *) echo "invalid mode in $STATE: $m" >&2; exit 1 ;; esac printf 'mode=%s readback: PL1=%dW PL2=%dW\n' "$m" \ $(( $(<"$PC/constraint_0_power_limit_uw") / 1000000 )) \ $(( $(<"$PC/constraint_1_power_limit_uw") / 1000000 )) } case "${1:-apply}" in set) case "${2:-}" in perf|auto|eco|max) printf '%s\n' "$2" > "$STATE"; apply ;; *) echo "usage: pkgpower set perf|auto|eco|max" >&2; exit 1 ;; esac ;; *) apply ;; esac Either save it to a scratch file and install from that directory: sudo install -Dm755 -o root -g root pkgpower /usr/local/bin/pkgpower or paste directly into ''sudoedit /usr/local/bin/pkgpower'' and then: sudo chown root:root /usr/local/bin/pkgpower sudo chmod 755 /usr/local/bin/pkgpower Finish with the state directory and a first run: sudo mkdir -p /var/lib/pkgpower echo auto | sudo tee /var/lib/pkgpower/mode bash -n /usr/local/bin/pkgpower # silent = clean paste ls -l /usr/local/bin/pkgpower # want: -rwxr-xr-x 1 root root sudo /usr/local/bin/pkgpower # applies mode + prints readback **RAPL-lock test:** if the readback reports 55/55 after setting a lower mode, the firmware has locked package RAPL and only firmware presets remain (KDE power profiles). Not the case on panther — writes stick. === 2. Privileges (sudoers) === Lets the panel applet (user session, no TTY) run the script without a password prompt. Create with ''sudo visudo -f /etc/sudoers.d/pkgpower'' (replace //john// with the actual username): john ALL=(root) NOPASSWD: /usr/local/bin/pkgpower Safe because the script is root-owned 755 and only accepts whitelisted mode tokens. The file must stay root-owned and not user-writable — the NOPASSWD rule trusts it; edit only via ''sudoedit''. === 3. Boot unit + udev rule === ''/etc/systemd/system/pkgpower.service'': [Unit] Description=Apply pkgpower mode (CPU package power limits) [Service] Type=oneshot ExecStart=/usr/local/bin/pkgpower [Install] WantedBy=multi-user.target sudo systemctl daemon-reload && sudo systemctl enable --now pkgpower ''/etc/udev/rules.d/99-pkgpower.rules'': ACTION=="change", SUBSYSTEM=="power_supply", RUN+="/usr/local/bin/pkgpower" sudo udevadm control --reload-rules Keep the boot unit even with the udev rule: udev coldplug fires ''add'' events at boot, not ''change''. ==== Plasma 6 panel applet ==== File layout: ~/.local/share/plasma/plasmoids/org.panther.pkgpower/ ├── metadata.json └── contents/ └── ui/ └── main.qml ''metadata.json'' (Plasma 6 format — the old ''metadata.desktop'' is gone): { "KPackageStructure": "Plasma/Applet", "X-Plasma-API-Minimum-Version": "6.0", "KPlugin": { "Id": "org.panther.pkgpower", "Name": "Power Modes", "Description": "Switch CPU package power limits", "Icon": "battery-good", "Version": "1.0", "License": "MIT", "Authors": [{ "Name": "John" }] } } ''contents/ui/main.qml'' — the whole applet (verified on panther, Fedora Plasma 6): import QtQuick import QtQuick.Layouts import org.kde.plasma.plasmoid import org.kde.plasma.plasma5support as Plasma5Support import org.kde.plasma.components 3.0 as PC3 import org.kde.kirigami as Kirigami PlasmoidItem { id: root readonly property var modes: [ { id: "perf", label: "Stock · 55/55 W", icon: "battery-full" }, { id: "auto", label: "Auto · 25/35 AC, 15 bat.", icon: "battery-good" }, { id: "eco", label: "Eco · 15 W", icon: "battery-low" }, { id: "max", label: "Max Eco · 10 W", icon: "battery-caution" } ] property int current: 1 // Plasma 6: DataSource lives in org.kde.plasma.plasma5support now Plasma5Support.DataSource { id: runner; engine: "executable" } function exec(cmd) { runner.disconnectSource(cmd); runner.connectSource(cmd) } // poll current mode so the icon stays honest Plasma5Support.DataSource { id: status engine: "executable" interval: 3000 connectedSources: ["cat /var/lib/pkgpower/mode 2>/dev/null || echo auto"] onNewData: (source, data) => { const i = root.modes.findIndex(m => m.id === String(data.stdout ?? "").trim()) if (i >= 0) root.current = i } } compactRepresentation: Component { Item { Layout.minimumWidth: ico.implicitWidth Layout.minimumHeight: ico.implicitHeight Kirigami.Icon { // Plasma 6: PlasmaCore.IconItem was removed; Kirigami.Icon replaces it id: ico; anchors.fill: parent source: root.modes[root.current].icon active: area.containsMouse } MouseArea { id: area; anchors.fill: parent; hoverEnabled: true onClicked: root.expanded = !root.expanded } } } fullRepresentation: Component { ColumnLayout { Layout.minimumWidth: Kirigami.Units.gridUnit * 16 spacing: Kirigami.Units.smallSpacing Repeater { model: root.modes delegate: PC3.Button { required property var modelData required property int index Layout.fillWidth: true icon.name: modelData.icon text: modelData.label checkable: true // without this, `checked` has no visual effect checked: index === root.current onClicked: { root.exec("sudo /usr/local/bin/pkgpower set " + modelData.id) root.expanded = false } } } } } } Test, then add to the panel: sudo dnf install plasma-sdk plasmoidviewer -a ~/.local/share/plasma/plasmoids/org.panther.pkgpower # happy → right-click panel → Add Widgets → "Power Modes" # after edits to an installed copy: systemctl restart --user plasma-plasmashell.service # versioned reinstall: kpackagetool6 --type Plasma/Applet --upgrade ~/.local/share/plasma/plasmoids/org.panther.pkgpower === Misc === * ''plasmoidviewer'' console noise (DeclarativeDropArea overrides, isScreenUiReady errors, nullptr disconnect) comes from KDE's stock desktopcontainment inside the viewer, not from this applet. Judge only file:-home-... lines. * Clicking mode buttons in the viewer is live fire (NOPASSWD sudo); the icon flips within ~3 s (poll interval). ==== Verification ==== sudo dnf install kernel-tools stress-ng sudo turbostat --Summary --interval 5 # terminal 1 — watch PkgWatt stress-ng --cpu $(nproc) --timeout 60s # terminal 2 PkgWatt should pin at the cap. Quick checks without load: ''cat /sys/class/powercap/intel-rapl:0/constraint_0_power_limit_uw'' (µW), or legacy ''undervolt -r'' readback. ==== Troubleshooting ==== * **Readback reverts to 55/55** — firmware locked package RAPL; only firmware presets remain. * **Cap mysteriously reverts later** — ''thermald'' or a firmware power-profile switch rewrote it (switching KDE power profiles is the usual trigger); re-run ''sudo pkgpower''. * **Plasma 6 API drift (both hit during development):** ''PlasmaCore.IconItem'' was removed → use ''Kirigami.Icon''. ''DataSource'' is no longer in ''org.kde.plasma.core'' → import ''org.kde.plasma.plasma5support'' and use ''Plasma5Support.DataSource''. ''SvgItem''/''FrameSvgItem'' now live in ''org.kde.ksvg''. A "X is not a type" QML error almost always means Plasma 5-era code. * **Generic "document" icon** — the icon name does not resolve in the active theme. The lookup chain is Breeze → hicolor only, never into other installed themes (e.g. Adwaita, which is where ''power-profile-*-symbolic'' lives on this box). Verify names with ''find /usr/share/icons/breeze -name 'NAME*' ''. Beware: multiple ''-name'' predicates in one ''find'' are ANDed (match nothing) — filter with grep instead. * **Stale mode in applet** — 3 s poll interval; wait it out. * **Buttons do nothing in viewer** — confirm the ''executable'' dataengine exists (''plasmaengineexplorer'', from plasma-sdk). * **udev rule silently misfires** — check the journal for SELinux denials. * **Changing wattages** — edit the ''case'' table in the script; everything else follows. * Lighter-touch alternatives: ''/sys/devices/system/cpu/intel_pstate/max_perf_pct'' (frequency cap, %) and ''/sys/devices/system/cpu/intel_pstate/no_turbo'' (turbo switch). A low PL2 already tames bursty surging without killing turbo entirely.