Table of Contents

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

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

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